Mickey Sweatt | 3b0bea6 | 2016-01-25 22:12:27 -0800 | [diff] [blame] | 1 | /* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */ |
| 2 | /** |
| 3 | * Copyright (c) 2016 Regents of the University of California. |
| 4 | * |
| 5 | * This file is part of the nTorrent codebase. |
| 6 | * |
| 7 | * nTorrent is free software: you can redistribute it and/or modify it under the |
| 8 | * terms of the GNU Lesser General Public License as published by the Free Software |
| 9 | * Foundation, either version 3 of the License, or (at your option) any later version. |
| 10 | * |
| 11 | * nTorrent is distributed in the hope that it will be useful, but WITHOUT ANY |
| 12 | * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A |
| 13 | * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. |
| 14 | * |
| 15 | * You should have received copies of the GNU General Public License and GNU Lesser |
| 16 | * General Public License along with nTorrent, e.g., in COPYING.md file. If not, see |
| 17 | * <http://www.gnu.org/licenses/>. |
| 18 | * |
| 19 | * See AUTHORS for complete list of nTorrent authors and contributors. |
| 20 | */ |
| 21 | #include "file-manifest.hpp" |
| 22 | |
Mickey Sweatt | ebc0195 | 2016-02-19 11:38:30 -0800 | [diff] [blame] | 23 | #include <limits> |
| 24 | |
Mickey Sweatt | 3b0bea6 | 2016-01-25 22:12:27 -0800 | [diff] [blame] | 25 | #include <boost/assert.hpp> |
Mickey Sweatt | ebc0195 | 2016-02-19 11:38:30 -0800 | [diff] [blame] | 26 | #include <boost/filesystem.hpp> |
| 27 | #include <boost/filesystem/fstream.hpp> |
| 28 | #include <boost/lexical_cast.hpp> |
Mickey Sweatt | 3b0bea6 | 2016-01-25 22:12:27 -0800 | [diff] [blame] | 29 | #include <boost/range/adaptors.hpp> |
Mickey Sweatt | ebc0195 | 2016-02-19 11:38:30 -0800 | [diff] [blame] | 30 | #include <boost/range/irange.hpp> |
Mickey Sweatt | 3b0bea6 | 2016-01-25 22:12:27 -0800 | [diff] [blame] | 31 | #include <boost/throw_exception.hpp> |
Mickey Sweatt | ebc0195 | 2016-02-19 11:38:30 -0800 | [diff] [blame] | 32 | #include <ndn-cxx/security/key-chain.hpp> |
| 33 | #include <ndn-cxx/security/signing-helpers.hpp> |
Mickey Sweatt | 3b0bea6 | 2016-01-25 22:12:27 -0800 | [diff] [blame] | 34 | |
Mickey Sweatt | 6de5dde | 2016-03-15 16:44:56 -0700 | [diff] [blame] | 35 | #include <ndn-cxx/encoding/tlv.hpp> |
| 36 | |
Mickey Sweatt | 3b0bea6 | 2016-01-25 22:12:27 -0800 | [diff] [blame] | 37 | using std::vector; |
Mickey Sweatt | ebc0195 | 2016-02-19 11:38:30 -0800 | [diff] [blame] | 38 | using std::streamsize; |
| 39 | using boost::irange; |
| 40 | |
| 41 | namespace fs = boost::filesystem; |
Mickey Sweatt | 3b0bea6 | 2016-01-25 22:12:27 -0800 | [diff] [blame] | 42 | |
| 43 | namespace ndn { |
| 44 | namespace ntorrent { |
| 45 | |
| 46 | BOOST_CONCEPT_ASSERT((boost::EqualityComparable<FileManifest>)); |
| 47 | BOOST_CONCEPT_ASSERT((WireEncodable<FileManifest>)); |
| 48 | BOOST_CONCEPT_ASSERT((WireDecodable<FileManifest>)); |
| 49 | static_assert(std::is_base_of<Data::Error, FileManifest::Error>::value, |
| 50 | "FileManifest::Error should inherit from Data::Error"); |
| 51 | |
Mickey Sweatt | ebc0195 | 2016-02-19 11:38:30 -0800 | [diff] [blame] | 52 | static ndn::Name |
| 53 | get_name_of_manifest(const std::string& filePath, const Name& manifestPrefix) |
| 54 | { |
| 55 | Name full_path(fs::system_complete(filePath).string()); |
| 56 | // Search the filePath for the leading component that matches |
| 57 | auto name_component_iter = std::find(full_path.rbegin(), |
| 58 | full_path.rend(), |
| 59 | *manifestPrefix.rbegin()); |
| 60 | |
| 61 | if (full_path.rend() == name_component_iter) { |
| 62 | BOOST_THROW_EXCEPTION(FileManifest::Error("No matching name component between" + |
| 63 | manifestPrefix.toUri() + " and " + |
| 64 | full_path.toUri())); |
| 65 | } |
Mickey Sweatt | be4b519 | 2016-03-28 16:46:34 -0700 | [diff] [blame] | 66 | ndn::Name manifestName = "/NTORRENT/"; |
Mickey Sweatt | ebc0195 | 2016-02-19 11:38:30 -0800 | [diff] [blame] | 67 | // Rebuild the name to be the suffix from the matching component |
| 68 | for (auto it = (name_component_iter.base() - 1); full_path.end() != it; ++it) { |
| 69 | manifestName.append(*it); |
| 70 | } |
| 71 | return manifestName; |
| 72 | } |
| 73 | |
| 74 | static std::vector<Data> |
Mickey Sweatt | be4b519 | 2016-03-28 16:46:34 -0700 | [diff] [blame] | 75 | packetize_file(const fs::path& filePath, |
Mickey Sweatt | ebc0195 | 2016-02-19 11:38:30 -0800 | [diff] [blame] | 76 | const ndn::Name& commonPrefix, |
| 77 | size_t dataPacketSize, |
| 78 | size_t subManifestSize, |
| 79 | size_t subManifestNum) |
| 80 | { |
| 81 | BOOST_ASSERT(0 < dataPacketSize); |
| 82 | size_t APPROX_BUFFER_SIZE = std::numeric_limits<int>::max(); // 2 * 1024 * 1024 *1024 |
| 83 | auto file_size = fs::file_size(filePath); |
| 84 | auto start_offset = subManifestNum * subManifestSize * dataPacketSize; |
| 85 | // determine the number of bytes in this submanifest |
| 86 | auto subManifestLength = subManifestSize * dataPacketSize; |
| 87 | auto remainingFileLength = file_size - start_offset; |
| 88 | subManifestLength = remainingFileLength < subManifestLength |
| 89 | ? remainingFileLength |
| 90 | : subManifestLength; |
| 91 | std::vector<Data> packets; |
| 92 | packets.reserve(subManifestLength/dataPacketSize + 1); |
| 93 | fs::ifstream fs(filePath, fs::ifstream::binary); |
| 94 | if (!fs) { |
| 95 | BOOST_THROW_EXCEPTION(FileManifest::Error("IO Error when opening" + filePath.string())); |
| 96 | } |
| 97 | // ensure that buffer is large enough to contain whole packets |
| 98 | // buffer size is either the entire file or the smallest number of data packets >= 2 GB |
| 99 | auto buffer_size = |
| 100 | subManifestLength < APPROX_BUFFER_SIZE ? |
| 101 | subManifestLength : |
| 102 | APPROX_BUFFER_SIZE % dataPacketSize == 0 ? |
| 103 | APPROX_BUFFER_SIZE : |
| 104 | APPROX_BUFFER_SIZE + dataPacketSize - (APPROX_BUFFER_SIZE % dataPacketSize); |
| 105 | std::vector<char> file_bytes; |
| 106 | file_bytes.reserve(buffer_size); |
| 107 | size_t bytes_read = 0; |
| 108 | fs.seekg(start_offset); |
| 109 | while(fs && bytes_read < subManifestLength && !fs.eof()) { |
| 110 | // read the file into the buffer |
| 111 | fs.read(&file_bytes.front(), buffer_size); |
| 112 | auto read_size = fs.gcount(); |
| 113 | if (fs.bad() || read_size < 0) { |
Mickey Sweatt | ebc0195 | 2016-02-19 11:38:30 -0800 | [diff] [blame] | 114 | BOOST_THROW_EXCEPTION(FileManifest::Error("IO Error when reading" + filePath.string())); |
| 115 | } |
| 116 | bytes_read += read_size; |
| 117 | char *curr_start = &file_bytes.front(); |
| 118 | for (size_t i = 0u; i < buffer_size; i += dataPacketSize) { |
| 119 | // Build a packet from the data |
| 120 | Name packetName = commonPrefix; |
| 121 | packetName.appendSequenceNumber(packets.size()); |
| 122 | Data d(packetName); |
| 123 | auto content_length = i + dataPacketSize > buffer_size ? buffer_size - i : dataPacketSize; |
| 124 | d.setContent(encoding::makeBinaryBlock(tlv::Content, curr_start, content_length)); |
| 125 | curr_start += content_length; |
| 126 | // append to the collection |
| 127 | packets.push_back(d); |
| 128 | } |
| 129 | file_bytes.clear(); |
| 130 | // recompute the buffer_size |
| 131 | buffer_size = |
| 132 | subManifestLength - bytes_read < APPROX_BUFFER_SIZE ? |
| 133 | subManifestLength - bytes_read : |
| 134 | APPROX_BUFFER_SIZE % dataPacketSize == 0 ? |
| 135 | APPROX_BUFFER_SIZE : |
| 136 | APPROX_BUFFER_SIZE + dataPacketSize - (APPROX_BUFFER_SIZE % dataPacketSize); |
| 137 | } |
| 138 | fs.close(); |
| 139 | packets.shrink_to_fit(); |
| 140 | security::KeyChain key_chain; |
| 141 | // sign all the packets |
| 142 | for (auto& p : packets) { |
| 143 | key_chain.sign(p, signingWithSha256()); |
| 144 | } |
| 145 | return packets; |
| 146 | } |
| 147 | |
| 148 | // CLASS METHODS |
| 149 | std::pair<std::vector<FileManifest>, std::vector<Data>> |
| 150 | FileManifest::generate(const std::string& filePath, |
| 151 | const Name& manifestPrefix, |
| 152 | size_t subManifestSize, |
| 153 | size_t dataPacketSize, |
| 154 | bool returnData) |
| 155 | { |
| 156 | BOOST_ASSERT(0 < subManifestSize); |
| 157 | BOOST_ASSERT(0 < dataPacketSize); |
| 158 | std::vector<FileManifest> manifests; |
| 159 | fs::path path(filePath); |
| 160 | if (!fs::exists(path)) { |
| 161 | BOOST_THROW_EXCEPTION(Error(filePath + ": no such file.")); |
| 162 | } |
| 163 | size_t file_length = fs::file_size(filePath); |
| 164 | // If the file_length is not evenly divisible by subManifestSize add 1, otherwise 0 |
| 165 | size_t numSubManifests = file_length / (subManifestSize * dataPacketSize) + |
| 166 | !!(file_length % (subManifestSize * dataPacketSize)); |
| 167 | // Find the prefix for the Catalog |
| 168 | auto manifestName = get_name_of_manifest(filePath, manifestPrefix); |
| 169 | std::vector<Data> allPackets; |
| 170 | if (returnData) { |
| 171 | allPackets.reserve(numSubManifests * subManifestSize); |
| 172 | } |
| 173 | manifests.reserve(numSubManifests); |
| 174 | for (auto subManifestNum : irange<size_t>(0, numSubManifests)) { |
| 175 | auto curr_manifest_name = manifestName; |
Mickey Sweatt | ebc0195 | 2016-02-19 11:38:30 -0800 | [diff] [blame] | 176 | // append the packet number |
| 177 | curr_manifest_name.appendSequenceNumber(manifests.size()); |
Mickey Sweatt | ebc0195 | 2016-02-19 11:38:30 -0800 | [diff] [blame] | 178 | FileManifest curr_manifest(curr_manifest_name, dataPacketSize, manifestPrefix); |
| 179 | auto packets = packetize_file(path, |
Mickey Sweatt | be4b519 | 2016-03-28 16:46:34 -0700 | [diff] [blame] | 180 | curr_manifest_name, |
Mickey Sweatt | ebc0195 | 2016-02-19 11:38:30 -0800 | [diff] [blame] | 181 | dataPacketSize, |
| 182 | subManifestSize, |
| 183 | subManifestNum); |
| 184 | if (returnData) { |
| 185 | allPackets.insert(allPackets.end(), packets.begin(), packets.end()); |
| 186 | } |
| 187 | curr_manifest.reserve(packets.size()); |
| 188 | // Collect all the Data packets into the sub-manifests |
| 189 | for (const auto& p: packets) { |
| 190 | curr_manifest.push_back(p.getFullName()); |
| 191 | } |
| 192 | // append the last manifest |
| 193 | manifests.push_back(curr_manifest); |
| 194 | } |
| 195 | allPackets.shrink_to_fit(); |
| 196 | manifests.shrink_to_fit(); |
| 197 | // Set all the submanifest_ptrs and sign all the manifests |
| 198 | security::KeyChain key_chain; |
| 199 | manifests.back().finalize(); |
| 200 | key_chain.sign(manifests.back(), signingWithSha256()); |
| 201 | for (auto it = manifests.rbegin() + 1; it != manifests.rend(); ++it) { |
| 202 | auto next = it - 1; |
| 203 | it->set_submanifest_ptr(std::make_shared<Name>(next->getFullName())); |
| 204 | it->finalize(); |
| 205 | key_chain.sign(*it, signingWithSha256()); |
| 206 | } |
| 207 | return {manifests, allPackets}; |
| 208 | } |
| 209 | |
Mickey Sweatt | 3b0bea6 | 2016-01-25 22:12:27 -0800 | [diff] [blame] | 210 | void |
| 211 | FileManifest::wireDecode(const Block& wire) |
| 212 | { |
| 213 | Data::wireDecode(wire); |
| 214 | this->decodeContent(); |
| 215 | } |
| 216 | |
Mickey Sweatt | 3b0bea6 | 2016-01-25 22:12:27 -0800 | [diff] [blame] | 217 | template<ndn::encoding::Tag TAG> |
| 218 | size_t |
| 219 | FileManifest::encodeContent(ndn::EncodingImpl<TAG>& encoder) const { |
Mickey Sweatt | 3b0bea6 | 2016-01-25 22:12:27 -0800 | [diff] [blame] | 220 | // ManifestContent ::= CONTENT-TYPE TLV-LENGTH |
| 221 | // DataPacketName* |
| 222 | // CatalogPrefix |
| 223 | // DataPacketSize |
| 224 | // FileManifestPtr? |
| 225 | |
| 226 | // DataPacketName ::= NAME-TYPE TLV-LENGTH |
| 227 | // Name |
| 228 | |
| 229 | // CatalogPrefix ::= NAME-TYPE TLV-LENGTH |
| 230 | // Name |
| 231 | |
Mickey Sweatt | 6de5dde | 2016-03-15 16:44:56 -0700 | [diff] [blame] | 232 | // DataPacketSize ::= CONTENT-TYPE TLV-LENGTH |
Mickey Sweatt | 3b0bea6 | 2016-01-25 22:12:27 -0800 | [diff] [blame] | 233 | // nonNegativeInteger |
| 234 | |
| 235 | // FileManifestPtr ::= NAME-TYPE TLV-LENGTH |
| 236 | // Name |
| 237 | |
| 238 | size_t totalLength = 0; |
| 239 | |
Mickey Sweatt | 6de5dde | 2016-03-15 16:44:56 -0700 | [diff] [blame] | 240 | // build suffix catalog |
Mickey Sweatt | 3b0bea6 | 2016-01-25 22:12:27 -0800 | [diff] [blame] | 241 | vector<Name> suffixCatalog; |
| 242 | suffixCatalog.reserve(m_catalog.size()); |
| 243 | for (auto name: m_catalog) { |
| 244 | if (!m_catalogPrefix.isPrefixOf(name)) { |
Mickey Sweatt | ebc0195 | 2016-02-19 11:38:30 -0800 | [diff] [blame] | 245 | BOOST_THROW_EXCEPTION(Error(name.toUri() + " does not have the prefix " |
Mickey Sweatt | 3b0bea6 | 2016-01-25 22:12:27 -0800 | [diff] [blame] | 246 | + m_catalogPrefix.toUri())); |
| 247 | } |
| 248 | name = name.getSubName(m_catalogPrefix.size()); |
| 249 | if (name.empty()) { |
| 250 | BOOST_THROW_EXCEPTION(Error("Manifest cannot include empty string")); |
| 251 | } |
| 252 | suffixCatalog.push_back(name); |
| 253 | } |
| 254 | |
| 255 | for (const auto& name : suffixCatalog | boost::adaptors::reversed) { |
| 256 | totalLength += name.wireEncode(encoder); |
| 257 | } |
| 258 | |
| 259 | totalLength += m_catalogPrefix.wireEncode(encoder); |
| 260 | |
| 261 | totalLength += prependNonNegativeIntegerBlock(encoder, tlv::Content, m_dataPacketSize); |
| 262 | |
| 263 | if (nullptr != m_submanifestPtr) { |
| 264 | totalLength += m_submanifestPtr->wireEncode(encoder); |
| 265 | } |
| 266 | |
| 267 | totalLength += encoder.prependVarNumber(totalLength); |
| 268 | totalLength += encoder.prependVarNumber(tlv::Content); |
| 269 | return totalLength; |
| 270 | |
| 271 | } |
| 272 | |
| 273 | // MANIPULATORS |
| 274 | void |
| 275 | FileManifest::push_back(const Name& name) |
| 276 | { |
| 277 | BOOST_ASSERT(name != m_catalogPrefix); |
| 278 | BOOST_ASSERT(m_catalogPrefix.isPrefixOf(name)); |
Mickey Sweatt | ebc0195 | 2016-02-19 11:38:30 -0800 | [diff] [blame] | 279 | // TODO(msweatt) Change this to use the copy constructor once bug fixed in Name() |
| 280 | m_catalog.push_back(name.toUri()); |
Mickey Sweatt | 3b0bea6 | 2016-01-25 22:12:27 -0800 | [diff] [blame] | 281 | } |
| 282 | |
| 283 | bool |
| 284 | FileManifest::remove(const ndn::Name& name) { |
Mickey Sweatt | ebc0195 | 2016-02-19 11:38:30 -0800 | [diff] [blame] | 285 | const auto it = std::find(m_catalog.begin(), m_catalog.end(), name); |
Mickey Sweatt | 3b0bea6 | 2016-01-25 22:12:27 -0800 | [diff] [blame] | 286 | if (m_catalog.end() == it) { |
| 287 | return false; |
| 288 | } |
| 289 | m_catalog.erase(it); |
| 290 | return true; |
| 291 | } |
| 292 | |
Mickey Sweatt | a768b24 | 2016-02-29 20:08:05 -0800 | [diff] [blame] | 293 | void |
| 294 | FileManifest::finalize() { |
| 295 | m_catalog.shrink_to_fit(); |
| 296 | encodeContent(); |
| 297 | } |
| 298 | |
Mickey Sweatt | 3b0bea6 | 2016-01-25 22:12:27 -0800 | [diff] [blame] | 299 | void FileManifest::encodeContent() { |
| 300 | // Name |
| 301 | // <file_name>/ImplicitDigest |
| 302 | // Content |
| 303 | // MetaData |
| 304 | // DataPtr* |
| 305 | // ManifestPointer? |
| 306 | // |
| 307 | // DataPtr := HashValue |
| 308 | // ManifestPtr := HashValue |
| 309 | // HashValue := OCTET[32] |
| 310 | |
| 311 | // MetaData := Property* |
| 312 | // Property := DataSize | Signature |
| 313 | onChanged(); |
| 314 | |
| 315 | EncodingEstimator estimator; |
| 316 | size_t estimatedSize = encodeContent(estimator); |
| 317 | |
| 318 | EncodingBuffer buffer(estimatedSize, 0); |
| 319 | encodeContent(buffer); |
| 320 | |
| 321 | setContentType(tlv::ContentType_Blob); |
| 322 | setContent(buffer.block()); |
| 323 | } |
| 324 | |
| 325 | void |
| 326 | FileManifest::decodeContent() { |
| 327 | // ManifestContent ::= CONTENT-TYPE TLV-LENGTH |
| 328 | // DataPacketName* |
| 329 | // CatalogPrefix |
| 330 | // DataPacketSize |
| 331 | // FileManifestPtr? |
| 332 | |
| 333 | |
| 334 | // DataPacketName ::= NAME-TYPE TLV-LENGTH |
| 335 | // Name |
| 336 | |
| 337 | // CatalogPrefix ::= NAME-TYPE TLV-LENGTH |
| 338 | // Name |
| 339 | |
| 340 | // DataPacketSize ::= CONTENT-TYPE TLV-LENGTH |
| 341 | // nonNegativeInteger |
| 342 | |
| 343 | // FileManifestPtr ::= NAME-TYPE TLV-LENGTH |
| 344 | // Name |
| 345 | |
| 346 | if (getContentType() != tlv::ContentType_Blob) { |
| 347 | BOOST_THROW_EXCEPTION(Error("Expected Content Type Blob")); |
| 348 | } |
| 349 | |
| 350 | const Block& content = Data::getContent(); |
| 351 | content.parse(); |
| 352 | |
| 353 | auto element = content.elements_begin(); |
Mickey Sweatt | 3b0bea6 | 2016-01-25 22:12:27 -0800 | [diff] [blame] | 354 | if (content.elements_end() == element) { |
| 355 | BOOST_THROW_EXCEPTION(Error("FileManifest with empty content")); |
| 356 | } |
Mickey Sweatt | 6de5dde | 2016-03-15 16:44:56 -0700 | [diff] [blame] | 357 | if (element->type() == tlv::Name) { |
| 358 | Name name(*element); |
Mickey Sweatt | 3b0bea6 | 2016-01-25 22:12:27 -0800 | [diff] [blame] | 359 | m_submanifestPtr = std::make_shared<Name>(name); |
| 360 | ++element; |
| 361 | } |
| 362 | |
| 363 | // DataPacketSize |
| 364 | m_dataPacketSize = readNonNegativeInteger(*element); |
| 365 | ++element; |
Mickey Sweatt | 3b0bea6 | 2016-01-25 22:12:27 -0800 | [diff] [blame] | 366 | // CatalogPrefix |
| 367 | m_catalogPrefix = Name(*element); |
Mickey Sweatt | 6de5dde | 2016-03-15 16:44:56 -0700 | [diff] [blame] | 368 | ++element; |
Mickey Sweatt | 3b0bea6 | 2016-01-25 22:12:27 -0800 | [diff] [blame] | 369 | // Catalog |
| 370 | m_catalog.clear(); |
| 371 | for (; element != content.elements_end(); ++element) { |
| 372 | element->parse(); |
Mickey Sweatt | 6de5dde | 2016-03-15 16:44:56 -0700 | [diff] [blame] | 373 | Name name = m_catalogPrefix; |
Mickey Sweatt | 3b0bea6 | 2016-01-25 22:12:27 -0800 | [diff] [blame] | 374 | name.append(Name(*element)); |
| 375 | if (name == m_catalogPrefix) { |
| 376 | BOOST_THROW_EXCEPTION(Error("Empty name included in a FileManifest")); |
| 377 | } |
| 378 | push_back(name); |
| 379 | } |
| 380 | } |
| 381 | |
| 382 | bool operator==(const FileManifest& lhs, const FileManifest& rhs) { |
| 383 | return lhs.name() == rhs.name() |
| 384 | && lhs.data_packet_size() == rhs.data_packet_size() |
| 385 | && (lhs.submanifest_ptr() == rhs.submanifest_ptr() /* shallow equality */ |
| 386 | || ( nullptr != lhs.submanifest_ptr() |
| 387 | && nullptr != rhs.submanifest_ptr() |
| 388 | && *rhs.submanifest_ptr() == *lhs.submanifest_ptr() |
| 389 | ) |
| 390 | ) |
| 391 | && lhs.catalog() == rhs.catalog(); |
| 392 | } |
| 393 | |
| 394 | bool operator!=(const FileManifest& lhs, const FileManifest& rhs) { |
| 395 | return lhs.name() != rhs.name() |
| 396 | || lhs.data_packet_size() != rhs.data_packet_size() |
| 397 | || (lhs.submanifest_ptr() != rhs.submanifest_ptr() /* shallow equality */ |
| 398 | && (nullptr == lhs.submanifest_ptr() |
| 399 | || nullptr == rhs.submanifest_ptr() |
| 400 | || *rhs.submanifest_ptr() != *lhs.submanifest_ptr() |
| 401 | ) |
| 402 | ) |
| 403 | || lhs.catalog() != rhs.catalog(); |
| 404 | } |
| 405 | |
| 406 | } // end ntorrent |
| 407 | } // end ndn |