├── .gitmodules ├── Makefile ├── .github └── workflows │ └── test.yml ├── Jamfile ├── LICENSE ├── common.hpp ├── add.cpp ├── README.rst ├── test └── test.py ├── new.cpp ├── merge.cpp ├── modify.cpp └── print.cpp /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "libtorrent"] 2 | path = libtorrent 3 | url = git@github.com:arvidn/libtorrent.git 4 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | BUILD_CONFIG=release cxxstd=17 link=shared crypto=openssl libtorrent-link=system warnings=off address-model=64 2 | 3 | ifeq (${PREFIX},) 4 | PREFIX=/usr/local/ 5 | endif 6 | 7 | ALL: FORCE 8 | BOOST_ROOT="" b2 ${BUILD_CONFIG} stage 9 | 10 | install: FORCE 11 | BOOST_ROOT="" b2 ${BUILD_CONFIG} install --prefix=${PREFIX} 12 | 13 | clean: FORCE 14 | rm -rf bin 15 | 16 | check: ALL 17 | python test/test.py 18 | 19 | FORCE: 20 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | 8 | jobs: 9 | 10 | build: 11 | name: build 12 | runs-on: ${{ matrix.os }} 13 | 14 | strategy: 15 | matrix: 16 | os: [ubuntu-latest, macos-latest ] 17 | 18 | steps: 19 | - name: checkout 20 | uses: actions/checkout@v2.3.3 21 | with: 22 | submodules: recursive 23 | - name: update package lists (Linux) 24 | continue-on-error: true 25 | if: runner.os == 'Linux' 26 | run: sudo apt update 27 | - name: dependencies (Linux) 28 | if: runner.os == 'Linux' 29 | run: sudo apt install libboost-tools-dev libboost-dev libboost-system-dev 30 | - name: dependencies (MacOS) 31 | if: runner.os == 'macOS' 32 | run: | 33 | brew install boost-build boost 34 | echo "using darwin ;" >> ~/user-config.jam 35 | - name: build tools 36 | run: b2 link=static stage 37 | - name: test 38 | run: python3 test/test.py 39 | -------------------------------------------------------------------------------- /Jamfile: -------------------------------------------------------------------------------- 1 | import modules ; 2 | import feature : feature ; 3 | import package ; 4 | 5 | use-project /torrent : libtorrent ; 6 | 7 | feature libtorrent-link : submodule system : propagated ; 8 | 9 | lib libtorrent : : torrent-rasterbar shared ; 10 | 11 | rule linking ( properties * ) 12 | { 13 | if system in $(properties) 14 | { 15 | return libtorrent ; 16 | } 17 | if submodule in $(properties) 18 | { 19 | return /torrent//torrent ; 20 | } 21 | } 22 | 23 | project torrent-tools 24 | : requirements 25 | multi 26 | @linking 27 | : default-build 28 | static 29 | 17 30 | ; 31 | 32 | exe torrent-new : new.cpp ; 33 | exe torrent-merge : merge.cpp ; 34 | exe torrent-add : add.cpp ; 35 | exe torrent-modify : modify.cpp ; 36 | exe torrent-print : print.cpp ; 37 | 38 | install stage : torrent-print torrent-modify torrent-merge torrent-new torrent-add : . ; 39 | 40 | package.install install 41 | : : torrent-print torrent-modify torrent-merge torrent-new torrent-add ; 42 | 43 | install stage_dependencies 44 | : /torrent//torrent 45 | : dependencies 46 | on 47 | SHARED_LIB 48 | ; 49 | 50 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2021, Arvid Norberg 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions 6 | are met: 7 | 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | * Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in 12 | the documentation and/or other materials provided with the distribution. 13 | * Neither the name of the author nor the names of its 14 | contributors may be used to endorse or promote products derived 15 | from this software without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 18 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 19 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 20 | ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE 21 | LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 22 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 23 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 24 | INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 25 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 26 | ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 27 | POSSIBILITY OF SUCH DAMAGE. 28 | 29 | -------------------------------------------------------------------------------- /common.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Copyright (c) 2020, Arvid Norberg 4 | All rights reserved. 5 | 6 | You may use, distribute and modify this code under the terms of the BSD license, 7 | see LICENSE file. 8 | */ 9 | 10 | #pragma once 11 | 12 | #include "libtorrent/version.hpp" 13 | 14 | #include // for std::hash 15 | #include 16 | #include 17 | #include 18 | 19 | #if LIBTORRENT_VERSION_NUM <= 20002 20 | 21 | namespace std { 22 | 23 | template<> 24 | struct hash 25 | { 26 | using argument_type = lt::sha256_hash; 27 | using result_type = std::size_t; 28 | result_type operator()(argument_type const& s) const 29 | { 30 | result_type ret; 31 | std::memcpy(&ret, s.data(), sizeof(ret)); 32 | return ret; 33 | } 34 | }; 35 | } 36 | 37 | #endif 38 | 39 | inline std::vector load_file(std::string const& filename) 40 | { 41 | std::fstream in; 42 | in.exceptions(std::ifstream::failbit); 43 | in.open(filename.c_str(), std::ios_base::in | std::ios_base::binary); 44 | in.seekg(0, std::ios_base::end); 45 | size_t const size = size_t(in.tellg()); 46 | in.seekg(0, std::ios_base::beg); 47 | std::vector ret(size); 48 | in.read(ret.data(), int(ret.size())); 49 | return ret; 50 | } 51 | 52 | inline std::string branch_path(std::string const& f) 53 | { 54 | if (f.empty()) return f; 55 | 56 | #ifdef TORRENT_WINDOWS 57 | if (f == "\\\\") return ""; 58 | #endif 59 | if (f == "/") return ""; 60 | 61 | auto len = f.size(); 62 | // if the last character is / or \ ignore it 63 | if (f[len-1] == '/' || f[len-1] == '\\') --len; 64 | while (len > 0) { 65 | --len; 66 | if (f[len] == '/' || f[len] == '\\') 67 | break; 68 | } 69 | 70 | if (f[len] == '/' || f[len] == '\\') ++len; 71 | return std::string(f.c_str(), len); 72 | } 73 | 74 | inline std::pair left_split(std::string const& f) 75 | { 76 | if (f.empty()) return {}; 77 | 78 | for (std::size_t i = 0; i < f.size(); ++i) { 79 | if (f[i] == '/' || f[i] == '\\') 80 | return {f.substr(0, i), f.substr(i + 1)}; 81 | } 82 | return {f, {}}; 83 | } 84 | 85 | inline std::pair right_split(std::string const& f) 86 | { 87 | if (f.empty()) return {}; 88 | 89 | for (std::size_t i = f.size(); i > 0; --i) { 90 | if (f[i - 1] == '/' || f[i - 1] == '\\') 91 | return {f.substr(0, i - 1), f.substr(i)}; 92 | } 93 | return {f, {}}; 94 | } 95 | 96 | 97 | inline std::string replace_directory_element(std::string const& path, std::string const& name) 98 | { 99 | auto const [dir, rest] = left_split(path); 100 | #ifdef TORRENT_WINDOWS 101 | return name + '\\' + rest; 102 | #else 103 | return name + '/' + rest; 104 | #endif 105 | } 106 | 107 | -------------------------------------------------------------------------------- /add.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Copyright (c) 2020, Arvid Norberg 4 | All rights reserved. 5 | 6 | You may use, distribute and modify this code under the terms of the BSD license, 7 | see LICENSE file. 8 | */ 9 | 10 | 11 | #include 12 | #include 13 | 14 | #include "libtorrent/create_torrent.hpp" 15 | #include "libtorrent/entry.hpp" 16 | #include "libtorrent/bdecode.hpp" 17 | 18 | #include "common.hpp" 19 | 20 | using namespace std::string_view_literals; 21 | 22 | namespace { 23 | 24 | void print_usage() 25 | { 26 | std::cout << R"(USAGE: torrent-add torrent-file [OPTIONS] files... 27 | OPTIONS: 28 | -o, --out Print resulting torrent to the specified file. 29 | If not specified "a.torrent" is used. 30 | -m, --mtime Include modification time of files 31 | -l, --dont-follow-links Instead of following symlinks, store them as symlinks 32 | -h, --help Show this message 33 | -q Quiet, do not print log messages 34 | 35 | Reads torrent-file and adds the files, specified by "files...". The resulting 36 | torrent is written to the output file specified by -o (or a.torrent by 37 | default). 38 | 39 | Only BitTorrent v2 torrent files are supported. 40 | )"; 41 | } 42 | 43 | } // anonymous namespace 44 | 45 | int main(int argc_, char const* argv_[]) try 46 | { 47 | lt::span args(argv_, argc_); 48 | // strip executable name 49 | args = args.subspan(1); 50 | 51 | if (args.size() < 2) { 52 | print_usage(); 53 | return 1; 54 | } 55 | 56 | std::string input_file = args[0]; 57 | args = args.subspan(1); 58 | std::string output_file = "a.torrent"; 59 | bool quiet = false; 60 | lt::create_flags_t flags = lt::create_torrent::v2_only; 61 | 62 | while (args.size() > 0 && args[0][0] == '-') { 63 | 64 | if ((args[0] == "-o"sv || args[0] == "--out"sv) && args.size() > 1) { 65 | output_file = args[1]; 66 | args = args.subspan(1); 67 | } 68 | else if (args[0] == "-q"sv) { 69 | quiet = true; 70 | } 71 | else if (args[0] == "-m"sv || args[0] == "--mtime"sv) { 72 | flags |= lt::create_torrent::modification_time; 73 | } 74 | else if (args[0] == "-l"sv || args[0] == "--dont-follow-links"sv) { 75 | flags |= lt::create_torrent::symlinks; 76 | } 77 | else if (args[0] == "-h"sv || args[0] == "--help"sv) { 78 | print_usage(); 79 | return 0; 80 | } 81 | else { 82 | std::cerr << "unknown option " << args[0] << '\n'; 83 | print_usage(); 84 | return 1; 85 | } 86 | args = args.subspan(1); 87 | } 88 | 89 | if (args.empty()) { 90 | std::cerr << "no files to add\n"; 91 | print_usage(); 92 | return 1; 93 | } 94 | 95 | auto input = load_file(input_file); 96 | auto torrent_node = lt::bdecode(input); 97 | lt::entry torrent_e(torrent_node); 98 | 99 | int const piece_size = torrent_e["info"]["piece length"].integer(); 100 | 101 | std::cout << "piece size: " << piece_size << '\n'; 102 | 103 | auto& p_layers = torrent_e["piece layers"].dict(); 104 | auto& file_tree = torrent_e["info"]["file tree"].dict(); 105 | 106 | for (auto const file : args) { 107 | 108 | if (!quiet) std::cout << "adding " << file << '\n'; 109 | lt::file_storage fs; 110 | fs.set_piece_length(piece_size); 111 | lt::add_files(fs, file, [](std::string const&) { return true; }, flags); 112 | lt::create_torrent creator(fs, piece_size, flags); 113 | 114 | auto const num = creator.num_pieces(); 115 | lt::set_piece_hashes(creator, branch_path(file) 116 | , [num, quiet] (lt::piece_index_t const p) { 117 | if (quiet) return; 118 | std::cout << "\r" << p << "/" << num; 119 | std::cout.flush(); 120 | }); 121 | if (!quiet) std::cout << "\n"; 122 | 123 | auto e = creator.generate(); 124 | 125 | auto file_entry = *e["info"]["file tree"].dict().begin(); 126 | file_tree.insert(std::move(file_entry)); 127 | 128 | auto& new_p_layers = e["piece layers"].dict(); 129 | // not all files have a piece layer. Small ones for instance 130 | if (!new_p_layers.empty()) 131 | p_layers.insert(std::move(*new_p_layers.begin())); 132 | } 133 | 134 | std::vector torrent; 135 | lt::bencode(back_inserter(torrent), torrent_e); 136 | std::fstream out; 137 | out.exceptions(std::ifstream::failbit); 138 | out.open(output_file.c_str(), std::ios_base::out | std::ios_base::binary); 139 | 140 | if (!quiet) std::cout << "-> writing to " << output_file << "\n"; 141 | out.write(torrent.data(), int(torrent.size())); 142 | } 143 | catch (std::exception const& e) 144 | { 145 | std::cerr << "failed: " << e.what() << '\n'; 146 | } 147 | 148 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | torrent tools 2 | ============= 3 | 4 | This is a collection of tools for creating and manipulating BitTorrent v2 5 | torrent files. ``torrent-new`` can create hybrid torrents, but the other tools 6 | for manipulating torrents only supports v2. 7 | 8 | torrent-new 9 | Creates new torrent files 10 | 11 | torrent-merge 12 | merges multiple torrents and creates a new torrent with all files in it 13 | 14 | torrent-add 15 | add a new file to an existing torrent 16 | 17 | torrent-modify 18 | remove and rename files from a torrent. Add and remove trackers, DHT nodes, 19 | web sedds and other properties. 20 | 21 | torrent-print 22 | print the content of a .torrent file to stdout 23 | 24 | examples 25 | ======== 26 | 27 | Here's a demonstration of some of the things one can do with these tools. First, 28 | create two files to test:: 29 | 30 | $ dd count=16000 if=/dev/random of=file-number-1 31 | $ dd count=32000 if=/dev/random of=file-number-2 32 | 33 | create a torrent 34 | ---------------- 35 | 36 | We can create a torrent from one of them (this is a v2-only torrent):: 37 | 38 | $ ./torrent-new -o torrent-1.torrent -2 -m file-number-1 39 | /Users/arvid/Documents/dev/torrent-tools/file-number-1 40 | 249/250 41 | 42 | See what it looks like:: 43 | 44 | $ ./torrent-print torrent-1.torrent 45 | piece-count: 250 46 | piece length: 32768 47 | info hash: v2: 9a12b62cf2717146eeaf5519cd5fdc981a251fd31df1f92d2a664f5320513ec2 48 | comment: 49 | created by: torrent-tools 50 | name: file-number-1 51 | number of files: 1 52 | files: 53 | 8192000 ---- file-number-1 54 | 55 | extend a torrent 56 | ---------------- 57 | 58 | Now, we can extend torrent-1.torrent, by adding another file to it. This is done 59 | without re-hashing ``file-number-1``, just the file that's being added:: 60 | 61 | $ ./torrent-add torrent-1.torrent -o torrent-2.torrent -m file-number-2 62 | piece size: 32768 63 | adding file-number-2 64 | 499/500 65 | -> writing to torrent-2.torrent 66 | 67 | The new torrent looks like this:: 68 | 69 | $ ./torrent-print torrent-2.torrent 70 | piece-count: 750 71 | piece length: 32768 72 | info hash: v2: 982f09b470be0e2218147b0a1aa2fb24c42cfde2c6fbb922fcbf3eba1b9089bb 73 | comment: 74 | created by: torrent-tools 75 | name: file-number-1 76 | number of files: 2 77 | files: 78 | 8192000 ---- file-number-1/file-number-1 79 | 16384000 ---- file-number-1/file-number-2 80 | 81 | merge torrents 82 | -------------- 83 | 84 | Create two torrents, one file each:: 85 | 86 | $ ./torrent-new -o 1.torrent -2 -m file-number-1 87 | /Users/arvid/Documents/dev/torrent-tools/file-number-1 88 | 249/250 89 | $ ./torrent-new -o 2.torrent -2 -m file-number-2 90 | /Users/arvid/Documents/dev/torrent-tools/file-number-2 91 | 249/250 92 | 93 | Merge them:: 94 | 95 | $ ./torrent-merge -o merged.torrent 1.torrent 2.torrent 96 | -> 1.torrent 97 | 8545cbbe942e7c0d8061a8ed31b0badb32feadc1f4963ec2f6643df30d07500b 8192000 file-number-1 98 | -> 2.torrent 99 | d64405bd94ec88f708481612a1dd35203d88d864191b0a584e2ebc27cfa336dd 16384000 file-number-2 100 | new piece size: 65536 101 | -> writing to merged.torrent 102 | 103 | The new torrent now contains both files:: 104 | 105 | $ ./torrent-print merged.torrent 106 | piece-count: 375 107 | piece length: 65536 108 | info hash: v2: a2898893986df997ee10c7765a8ae8b523460e18363b89c8c6ba15070b9c985c 109 | comment: 110 | created by: torrent-tools 111 | name: file-number-1 112 | number of files: 2 113 | files: 114 | 8192000 ---- file-number-1/file-number-1 115 | 16384000 ---- file-number-1/file-number-2 116 | 117 | modify torrents 118 | --------------- 119 | 120 | The ``merged.torrent`` has a root directory called ``file-number-1``:: 121 | 122 | $ ./torrent-print --tree merged.torrent 123 | piece-count: 375 124 | piece length: 65536 125 | info hash: v2: a2898893986df997ee10c7765a8ae8b523460e18363b89c8c6ba15070b9c985c 126 | comment: 127 | created by: torrent-tools 128 | name: file-number-1 129 | number of files: 2 130 | files: 131 | └ file-number-1 132 | 8192000 ---- ├ file-number-1 133 | 16384000 ---- └ file-number-2 134 | 135 | Let's rename the root folder to "foobar":: 136 | 137 | $ ./torrent-modify --name foobar -o merged2.torrent merged.torrent 138 | 139 | The new torrent looks like this:: 140 | 141 | $ ./torrent-print --tree merged2.torrent 142 | piece-count: 375 143 | piece length: 65536 144 | info hash: v2: b2adfd009b31bc56f7af9f9962fc9fdc1d80282e598e8d8857c7e82aa9b55cd4 145 | comment: 146 | created by: torrent-tools 147 | name: foobar 148 | number of files: 2 149 | files: 150 | └ foobar 151 | 8192000 ---- ├ file-number-1 152 | 16384000 ---- └ file-number-2 153 | 154 | Now, set a comment:: 155 | 156 | $ ./torrent-modify --comment "This is a foobar" -o merged3.torrent merged2.torrent 157 | 158 | The new torrent looks like this:: 159 | 160 | $ ./torrent-print --tree merged3.torrent 161 | piece-count: 375 162 | piece length: 65536 163 | info hash: v2: b2adfd009b31bc56f7af9f9962fc9fdc1d80282e598e8d8857c7e82aa9b55cd4 164 | comment: This is a foobar 165 | created by: torrent-tools 166 | name: foobar 167 | number of files: 2 168 | files: 169 | └ foobar 170 | 8192000 ---- ├ file-number-1 171 | 16384000 ---- └ file-number-2 172 | 173 | -------------------------------------------------------------------------------- /test/test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # vim: set fileencoding=UTF-8 : 3 | 4 | import unittest 5 | import subprocess 6 | import os 7 | import itertools 8 | 9 | def run(args): 10 | out = subprocess.check_output(args).decode('utf-8') 11 | print(' '.join(args)) 12 | print(out) 13 | return out.strip().split('\n') 14 | 15 | class TestNew(unittest.TestCase): 16 | 17 | @classmethod 18 | def setUpClass(cls): 19 | # create some test files 20 | try: os.mkdir('test-files') 21 | except: pass 22 | global test_files_ 23 | global size_ 24 | 25 | test_files_ = ['test-files/file-number-1', 'test-files/file-number-2', 'test-files/file-number-3'] 26 | size_ = [16000, 32000, 300] 27 | 28 | for i in range(len(test_files_)): 29 | run(['dd', 'bs=512', f'count={size_[i]}', 'if=/dev/random', f'of={test_files_[i]}']) 30 | 31 | def test_single_file(self): 32 | for f in test_files_: 33 | run(['./torrent-new', '-o', 'test.torrent', f]) 34 | out = run(['./torrent-print', 'test.torrent']) 35 | 36 | self.assertEqual(out[5], 'name: %s' % os.path.split(f)[1]) 37 | self.assertEqual(out[6], 'number of files: 1') 38 | self.assertTrue(out[8].endswith(os.path.split(f)[1])) 39 | 40 | def test_private(self): 41 | run(['./torrent-new', '--private', '-o', 'test.torrent', test_files_[0]]) 42 | out = run(['./torrent-print', '--private', 'test.torrent']) 43 | 44 | self.assertEqual(out[0], 'private: yes') 45 | 46 | def test_multi_file(self): 47 | run(['./torrent-new', '-o', 'test.torrent', 'test-files']) 48 | out = run(['./torrent-print', '--name', 'test.torrent']) 49 | self.assertEqual(out[0], 'name: test-files') 50 | out = run(['./torrent-print', '--files', '--flat', 'test.torrent']) 51 | 52 | # strip out "files:" 53 | out = out[1:] 54 | names = [] 55 | sizes = [] 56 | for i in range(len(out)): 57 | names.append(out[i].strip().split(' ')[-1]) 58 | sizes.append(int(out[i].strip().split(' ')[0]) // 512) 59 | 60 | self.assertEqual(names, test_files_) 61 | self.assertEqual(sizes, size_) 62 | 63 | def test_piece_size(self): 64 | for f in test_files_: 65 | run(['./torrent-new', '-o', 'test.torrent', '--piece-size', '64', f]) 66 | out = run(['./torrent-print', '--piece-size', 'test.torrent']) 67 | self.assertEqual(out[0], 'piece size: 65536') 68 | 69 | def test_small_piece_size(self): 70 | # piece size must be at least 16 kiB 71 | with self.assertRaises(Exception): 72 | run(['./torrent-new', '-o', 'test.torrent', '--piece-size', '1', f]) 73 | 74 | def test_invalid_piece_size(self): 75 | # piece size must be a power of two 76 | with self.assertRaises(Exception): 77 | run(['./torrent-new', '-o', 'test.torrent', '--piece-size', '17', f]) 78 | 79 | def test_comment(self): 80 | for f in test_files_: 81 | run(['./torrent-new', '-o', 'test.torrent', '--comment', 'foobar', f]) 82 | out = run(['./torrent-print', '--comment', 'test.torrent']) 83 | self.assertEqual(out[0], 'comment: foobar') 84 | 85 | def test_creator(self): 86 | for f in test_files_: 87 | run(['./torrent-new', '-o', 'test.torrent', '--creator', 'foobar', f]) 88 | out = run(['./torrent-print', '--creator', 'test.torrent']) 89 | 90 | self.assertEqual(out[0], 'created by: foobar') 91 | 92 | def test_tracker(self): 93 | run(['./torrent-new', '--tracker', 'https://tracker.test/announce', '-o', 'test.torrent', 'test-files']) 94 | out = run(['./torrent-print', '--trackers', 'test.torrent']) 95 | 96 | # strip "trackers:" 97 | out = out[1:] 98 | self.assertEqual(out[0].strip(), '0: https://tracker.test/announce') 99 | 100 | def test_tracker_tiers(self): 101 | run(['./torrent-new', \ 102 | '--tracker', 'https://tracker1a.test/announce', \ 103 | '--tracker-tier', 'https://tracker1b.test/announce', \ 104 | '--tracker', 'https://tracker2a.test/announce', \ 105 | '--tracker-tier', 'https://tracker2b.test/announce', \ 106 | '-o', 'test.torrent', 'test-files']) 107 | out = run(['./torrent-print', '--trackers', 'test.torrent']) 108 | 109 | # strip "trackers:" 110 | out = out[1:] 111 | # the order within a tier is not defined, so we sort it to be able to 112 | # compare reliably 113 | out[0:2] = sorted(out[0:2]) 114 | out[2:4] = sorted(out[2:4]) 115 | self.assertEqual(out[0].strip(), '0: https://tracker1a.test/announce') 116 | self.assertEqual(out[1].strip(), '0: https://tracker1b.test/announce') 117 | self.assertEqual(out[2].strip(), '1: https://tracker2a.test/announce') 118 | self.assertEqual(out[3].strip(), '1: https://tracker2b.test/announce') 119 | 120 | def test_mtime(self): 121 | run(['./torrent-new', '--mtime', '-o', 'test.torrent', 'test-files']) 122 | out = run(['./torrent-print', '--file-mtime', '--files', '--flat', 'test.torrent']) 123 | for l in out[1:]: 124 | ts = l.split(' test-files')[0].strip().split(' ', 2)[-1] 125 | # validate timestamp 126 | # example: 2021-01-02 20:57:24 127 | self.assertEqual(len(ts), 19) 128 | year = int(ts[0:4]) 129 | self.assertTrue(year > 2020) 130 | self.assertEqual(ts[4], '-') 131 | month = int(ts[5:7]) 132 | self.assertTrue(month <= 12) 133 | self.assertTrue(month >= 1) 134 | self.assertEqual(ts[7], '-') 135 | day = int(ts[8:10]) 136 | self.assertTrue(day <= 31) 137 | self.assertTrue(day >= 1) 138 | self.assertEqual(ts[10], ' ') 139 | hour = int(ts[11:13]) 140 | self.assertTrue(hour <= 23) 141 | self.assertTrue(hour >= 0) 142 | self.assertEqual(ts[13], ':') 143 | minute = int(ts[14:16]) 144 | self.assertTrue(minute <= 59) 145 | self.assertTrue(minute >= 0) 146 | self.assertEqual(ts[16], ':') 147 | sec = int(ts[17:19]) 148 | self.assertTrue(sec <= 60) 149 | self.assertTrue(sec >= 0) 150 | 151 | def test_web_seeds_multifile(self): 152 | run(['./torrent-new', '--web-seed', 'https://web.com/torrent', '-o', 'test.torrent', 'test-files']) 153 | out = run(['./torrent-print', '--web-seeds', 'test.torrent']) 154 | 155 | # strip "web-seeds:" 156 | out = out[1:] 157 | self.assertEqual(out[0].strip(), 'BEP19 https://web.com/torrent/') 158 | 159 | def test_web_seeds_singlefile(self): 160 | run(['./torrent-new', '--web-seed', 'https://web.com/file', '-o', 'test.torrent', 'test-files/file-number-1']) 161 | out = run(['./torrent-print', '--web-seeds', 'test.torrent']) 162 | 163 | # strip "web-seeds:" 164 | out = out[1:] 165 | self.assertEqual(out[0].strip(), 'BEP19 https://web.com/file') 166 | 167 | def test_v2_only(self): 168 | run(['./torrent-new', '--v2-only', '-o', 'test.torrent', 'test-files']) 169 | out = run(['./torrent-print', '--info-hash', 'test.torrent']) 170 | # example: 171 | # info hash: v2: 172 | # 7791118351f7f15fe3333d7a6f793337b698492f61ca821daddd22cd2a3c2c19 173 | self.assertNotIn('v1:', out[0]) 174 | self.assertIn('v2:', out[0]) 175 | 176 | run(['./torrent-new', '-o', 'test.torrent', 'test-files']) 177 | out = run(['./torrent-print', '--info-hash', 'test.torrent']) 178 | # example: 179 | # info hash: v1: b193774831b16fe487281987dcce7edda40767b5 v2: 180 | # 7791118351f7f15fe3333d7a6f793337b698492f61ca821daddd22cd2a3c2c19 181 | self.assertIn('v1:', out[0]) 182 | self.assertIn('v2:', out[0]) 183 | 184 | def test_dht_nodes(self): 185 | run(['./torrent-new', '--dht-node', 'router1.com', '6881', '-o', 'test.torrent', 'test-files']) 186 | out = run(['./torrent-print', '--dht-nodes', 'test.torrent']) 187 | # example: 188 | # nodes: 189 | # router1.com: 6881 190 | self.assertIn('nodes:', out[0]) 191 | self.assertIn('router1.com: 6881', out[1]) 192 | 193 | # test_root_cert 194 | # test_symlinks 195 | 196 | 197 | class TestPrint(unittest.TestCase): 198 | 199 | def test_tree(self): 200 | run(['./torrent-new', '-o', 'test.torrent', 'bin']) 201 | 202 | opts = ['--file-mtime', '--no-file-size', '--file-piece-range', '--file-roots', '--human-readable', '--file-offsets', '--no-file-attributes'] 203 | 204 | for l in range(0, len(opts)): 205 | for opt in itertools.combinations(opts, l): 206 | opt = list(opt) 207 | print(opt) 208 | out = run(['./torrent-print', '--files', '--tree', '--no-colors'] + opt + ['test.torrent']) 209 | self.validate_tree(out) 210 | 211 | # makes sure the lines are correctly aligned 212 | def validate_tree(self, lines): 213 | self.assertEqual(lines[0], 'files:') 214 | lines = lines[1:] 215 | 216 | # cols indicates whether the given column is a vertical line or not 217 | width = len(max(lines, key=len)) 218 | cols = [False] * width 219 | 220 | # on the first line there should be exactly one └ 221 | self.assertEqual(lines[0].count('└'), 1) 222 | 223 | # every entry *may* be a directory, in which case a new vertical line 224 | # is allowed. Record the column index of such allowance here. This is 225 | # only valid for the next iteration through the loop and need to be 226 | # reset to None. Only one new vertical line may be started per row 227 | new_vertical = lines[0].index('└') + 2 228 | 229 | for l in lines[1:]: 230 | done = False 231 | for s in range(len(l)): 232 | if done: 233 | self.assertNotIn(l[s], '└├│') 234 | continue 235 | if cols[s]: 236 | self.assertIn(l[s], '└├│') 237 | # this terminates a vertical line 238 | if (l[s] == '└'): 239 | cols[s] = False 240 | if l[s] in '└├': 241 | new_vertical = s + 2 242 | done = True 243 | self.assertEqual(l[s+1], ' ') 244 | else: 245 | if s == new_vertical and l[s] == '├': 246 | new_vertical = s + 2 247 | cols[s] = True 248 | self.assertEqual(l[s+1], ' ') 249 | done = True 250 | elif s == new_vertical and l[s] == '└': 251 | new_vertical = s + 2 252 | self.assertEqual(l[s+1], ' ') 253 | done = True 254 | else: 255 | self.assertNotIn(l[s], '└├│') 256 | 257 | if __name__ == '__main__': 258 | unittest.main() 259 | -------------------------------------------------------------------------------- /new.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Copyright (c) 2020, Arvid Norberg 4 | All rights reserved. 5 | 6 | You may use, distribute and modify this code under the terms of the BSD license, 7 | see LICENSE file. 8 | */ 9 | 10 | #include "libtorrent/entry.hpp" 11 | #include "libtorrent/bencode.hpp" 12 | #include "libtorrent/torrent_info.hpp" 13 | #include "libtorrent/create_torrent.hpp" 14 | #include "libtorrent/settings_pack.hpp" 15 | 16 | #include "common.hpp" 17 | 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | 25 | #ifdef TORRENT_WINDOWS 26 | #include // for _getcwd 27 | #endif 28 | 29 | #include 30 | 31 | using namespace std::string_view_literals; 32 | 33 | namespace { 34 | 35 | using namespace std::placeholders; 36 | 37 | // do not include files and folders whose 38 | // name starts with a . 39 | bool file_filter(std::string const& f) 40 | { 41 | if (f.empty()) return false; 42 | 43 | char const* first = f.c_str(); 44 | char const* sep = strrchr(first, '/'); 45 | #if defined(TORRENT_WINDOWS) || defined(TORRENT_OS2) 46 | char const* altsep = strrchr(first, '\\'); 47 | if (sep == nullptr || altsep > sep) sep = altsep; 48 | #endif 49 | // if there is no parent path, just set 'sep' 50 | // to point to the filename. 51 | // if there is a parent path, skip the '/' character 52 | if (sep == nullptr) sep = first; 53 | else ++sep; 54 | 55 | // return false if the first character of the filename is a . 56 | if (sep[0] == '.') return false; 57 | 58 | std::cerr << f << "\n"; 59 | return true; 60 | } 61 | 62 | int const default_num_threads 63 | = std::max(1, static_cast(std::thread::hardware_concurrency())); 64 | 65 | void print_usage() 66 | { 67 | std::cerr << R"(USAGE: torrent-new [OPTIONS] file 68 | 69 | Generates a torrent file from the specified file 70 | or directory and writes it to an output .torrent file 71 | 72 | OPTIONS: 73 | -o, --out Print resulting torrent to the specified file. 74 | If not specified "a.torrent" is used. 75 | -t, --tracker Add as a tracker in a new tier. 76 | -T, --tracker-tier Add as a tracker in the current tier. 77 | -w, --web-seed Add as a web seed to the torrent. 78 | -d, --dht-node Add a DHT node to the torrent, that can be used to 79 | bootstrap the DHT network from. 80 | -C, --creator sets the "created by" field to . 81 | -c, --comment Sets the "comment" field to . 82 | -p, --private Set the "private" field to 1. 83 | -h, --help Show this message 84 | -l, --dont-follow-links Instead of following symlinks, store them as symlinks 85 | in the .torrent file 86 | -2, --v2-only Generate a BitTorrent v2-only torrent (not compatible with v1) 87 | -m, --mtime Include modification time of files 88 | -s, --piece-size Specifies the piece size, in kiB. This must be at least 89 | 16kiB and must be a power of 2. 90 | -r, --root-cert Embed the specified root certificate in the torrent file 91 | (for SSL torrents only). All peers and trackers must 92 | authenticate with a cert signed by this root, directly 93 | or indirectly. 94 | 95 | --threads Use threads to hash pieces. Defaults to )" 96 | << default_num_threads << R"(. 97 | 98 | To manage tracker tiers -t will add a new tier immediately before adding the 99 | tracker whereas -T will add the tracker to the current tier. If there is no 100 | tier, one will be created regardless of which flavour of -t and -T is used. e.g. 101 | 102 | -t https://foo.com -t https://bar.com 103 | 104 | Will add foo and bar as separate tiers. 105 | 106 | -t https://foo.com -T https://bar.com 107 | 108 | Will add foo and bar as the same tier. 109 | )"; 110 | } 111 | 112 | } // anonymous namespace 113 | 114 | int main(int argc_, char const* argv_[]) try 115 | { 116 | lt::span args(argv_, argc_); 117 | // strip executable name 118 | args = args.subspan(1); 119 | 120 | if (args.size() < 2) { 121 | print_usage(); 122 | return 1; 123 | } 124 | 125 | std::string creator = "torrent-tools"; 126 | std::string comment_str; 127 | bool private_torrent = false; 128 | std::vector web_seeds; 129 | std::vector> dht_nodes; 130 | std::vector> trackers; 131 | int piece_size = 0; 132 | lt::create_flags_t flags = {}; 133 | std::string root_cert; 134 | bool quiet = false; 135 | int num_threads = default_num_threads; 136 | 137 | std::string output_file = "a.torrent"; 138 | 139 | while (args.size() > 0 && args[0][0] == '-') { 140 | 141 | if ((args[0] == "-o"sv || args[0] == "--out"sv) && args.size() > 1) { 142 | output_file = args[1]; 143 | args = args.subspan(1); 144 | } 145 | else if (args[0] == "--threads"sv && args.size() > 1) { 146 | num_threads = atoi(args[1]); 147 | args = args.subspan(1); 148 | } 149 | else if ((args[0] == "-t"sv || args[0] == "--tracker"sv) && args.size() > 1) { 150 | std::string t = args[1]; 151 | args = args.subspan(1); 152 | trackers.emplace_back(std::vector{std::move(t)}); 153 | } 154 | else if ((args[0] == "-T"sv || args[0] == "--tracker-tier"sv) && args.size() > 1) { 155 | std::string t = args[1]; 156 | args = args.subspan(1); 157 | if (trackers.empty()) 158 | trackers.emplace_back(std::vector{std::move(t)}); 159 | else 160 | trackers.back().emplace_back(std::move(t)); 161 | } 162 | else if ((args[0] == "-w"sv || args[0] == "--web-seed"sv) && args.size() > 1) { 163 | web_seeds.emplace_back(args[1]); 164 | args = args.subspan(1); 165 | } 166 | else if (args[0] == "--dht-node"sv && args.size() > 2) { 167 | dht_nodes.emplace_back(args[1], std::atoi(args[2])); 168 | args = args.subspan(2); 169 | } 170 | else if ((args[0] == "-C"sv || args[0] == "--creator"sv) && args.size() > 1) { 171 | creator = args[1]; 172 | args = args.subspan(1); 173 | } 174 | else if ((args[0] == "-c"sv || args[0] == "--comment"sv) && args.size() > 1) { 175 | comment_str = args[1]; 176 | args = args.subspan(1); 177 | } 178 | else if (args[0] == "-p"sv || args[0] == "--private"sv) { 179 | private_torrent = true; 180 | } 181 | else if ((args[0] == "-s"sv || args[0] == "--piece-size"sv) && args.size() > 1) { 182 | piece_size = atoi(args[1]); 183 | args = args.subspan(1); 184 | if (piece_size == 0) { 185 | std::cerr << "invalid piece size: \"" << args[1] << "\"\n"; 186 | return 1; 187 | } 188 | if (piece_size < 16) { 189 | std::cerr << "piece size may not be smaller than 16 kiB\n"; 190 | return 1; 191 | } 192 | if ((piece_size & (piece_size - 1)) != 0) { 193 | std::cerr << "piece size must be a power of 2 (specified " << piece_size << ")\n"; 194 | return 1; 195 | } 196 | // convert kiB to Bytes 197 | piece_size *= 1024; 198 | } 199 | else if ((args[0] == "-r"sv || args[0] == "--root-cert"sv) && args.size() > 1) { 200 | root_cert = args[1]; 201 | args = args.subspan(1); 202 | } 203 | else if (args[0] == "-q"sv) { 204 | quiet = true; 205 | } 206 | else if (args[0] == "-h"sv || args[0] == "--help"sv) { 207 | print_usage(); 208 | return 0; 209 | } 210 | else if (args[0] == "-l"sv || args[0] == "--dont-follow-links"sv) { 211 | flags |= lt::create_torrent::symlinks; 212 | } 213 | else if (args[0] == "-2"sv || args[0] == "--v2-only"sv) { 214 | flags |= lt::create_torrent::v2_only; 215 | } 216 | else if (args[0] == "-m"sv || args[0] == "--mtime"sv) { 217 | flags |= lt::create_torrent::modification_time; 218 | } 219 | else { 220 | std::cerr << "unknown option (or missing argument) " << args[0] << '\n'; 221 | print_usage(); 222 | return 1; 223 | } 224 | args = args.subspan(1); 225 | } 226 | 227 | if (args.empty()) { 228 | print_usage(); 229 | std::cerr << "no files specified.\n"; 230 | return 1; 231 | } 232 | std::string full_path = args[0]; 233 | 234 | lt::file_storage fs; 235 | #ifdef TORRENT_WINDOWS 236 | if (full_path[1] != ':') 237 | #else 238 | if (full_path[0] != '/') 239 | #endif 240 | { 241 | char cwd[2048]; 242 | #ifdef TORRENT_WINDOWS 243 | #define getcwd_ _getcwd 244 | #else 245 | #define getcwd_ getcwd 246 | #endif 247 | 248 | char const* ret = getcwd_(cwd, sizeof(cwd)); 249 | if (ret == nullptr) { 250 | std::cerr << "failed to get current working directory: " 251 | << strerror(errno) << "\n"; 252 | return 1; 253 | } 254 | 255 | #undef getcwd_ 256 | #ifdef TORRENT_WINDOWS 257 | full_path = cwd + ("\\" + full_path); 258 | #else 259 | full_path = cwd + ("/" + full_path); 260 | #endif 261 | } 262 | 263 | lt::add_files(fs, full_path, file_filter, flags); 264 | if (fs.num_files() == 0) { 265 | std::cerr << "no files specified.\n"; 266 | return 1; 267 | } 268 | 269 | lt::create_torrent t(fs, piece_size, flags); 270 | int tier = 0; 271 | if (!trackers.empty()) { 272 | for (auto const& tt : trackers) { 273 | for (auto const& url : tt) { 274 | t.add_tracker(url, tier); 275 | } 276 | ++tier; 277 | } 278 | } 279 | 280 | for (std::string const& ws : web_seeds) 281 | t.add_url_seed(ws); 282 | 283 | for (auto const& n : dht_nodes) 284 | t.add_node(n); 285 | 286 | t.set_priv(private_torrent); 287 | 288 | lt::settings_pack sett; 289 | sett.set_int(lt::settings_pack::hashing_threads, num_threads); 290 | sett.set_int(lt::settings_pack::file_pool_size, 2); 291 | auto const num = t.num_pieces(); 292 | lt::set_piece_hashes(t, branch_path(full_path), sett 293 | , [num, quiet] (lt::piece_index_t const p) { 294 | if (quiet) return; 295 | std::cout << "\r" << (p + lt::piece_index_t::diff_type{1}) << "/" << num; 296 | std::cout.flush(); 297 | }); 298 | 299 | if (!quiet) std::cerr << "\n"; 300 | t.set_creator(creator.c_str()); 301 | if (!comment_str.empty()) { 302 | t.set_comment(comment_str.c_str()); 303 | } 304 | 305 | if (!root_cert.empty()) { 306 | if (!quiet) std::cout << "loading " << root_cert << '\n'; 307 | std::vector const pem = load_file(root_cert); 308 | t.set_root_cert(std::string(&pem[0], pem.size())); 309 | } 310 | 311 | // create the torrent and print it to stdout 312 | std::vector torrent; 313 | lt::bencode(back_inserter(torrent), t.generate()); 314 | 315 | std::fstream out; 316 | out.exceptions(std::ifstream::failbit); 317 | out.open(output_file.c_str(), std::ios_base::out | std::ios_base::binary); 318 | out.write(torrent.data(), int(torrent.size())); 319 | 320 | return 0; 321 | } 322 | catch (std::exception& e) { 323 | std::cerr << "ERROR: " << e.what() << "\n"; 324 | return 1; 325 | } 326 | -------------------------------------------------------------------------------- /merge.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Copyright (c) 2020, Arvid Norberg 4 | All rights reserved. 5 | 6 | You may use, distribute and modify this code under the terms of the BSD license, 7 | see LICENSE file. 8 | */ 9 | 10 | #include "libtorrent/bencode.hpp" 11 | #include "libtorrent/create_torrent.hpp" 12 | #include "libtorrent/sha1_hash.hpp" // for sha256_hash 13 | #include "libtorrent/torrent_info.hpp" 14 | 15 | #include "common.hpp" 16 | 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | 24 | using namespace std::string_view_literals; 25 | 26 | namespace { 27 | 28 | void print_usage() 29 | { 30 | std::cout << R"(USAGE: torrent-merge [OPTIONS] files... 31 | OPTIONS: 32 | -o, --out Store the resulting torrent to the specified file. 33 | If not specified "a.torrent" is used. 34 | -n, --name Set the name of the new torrent. If not specified, 35 | the name of the first torrent will be used 36 | -h, --help Show this message 37 | -q Quiet, do not print log messages 38 | 39 | Reads the torrent files, specified by "files..." and creates a new torrent 40 | containing all files in all torrents. Any file found in more than one torrent 41 | will only be included once in the output. 42 | 43 | Only BitTorrent v2 torrent files are supported. 44 | )"; 45 | } 46 | 47 | std::vector make_piece_layer(lt::span bytes) 48 | { 49 | if (bytes.size() % lt::sha256_hash::size() != 0) 50 | throw std::runtime_error("invalid piece layer size"); 51 | 52 | std::vector ret; 53 | ret.reserve(bytes.size() / lt::sha256_hash::size()); 54 | for (int i = 0; i < bytes.size(); i += lt::sha256_hash::size()) 55 | ret.emplace_back(bytes.data() + i); 56 | 57 | return ret; 58 | } 59 | 60 | struct file_metadata 61 | { 62 | std::string filename; 63 | 64 | // the piece size the piece_layer represents. We need to save this in case 65 | // the piece layer needs to be moved up to a larger piece size. 66 | int piece_size; 67 | 68 | std::int64_t file_size; 69 | 70 | // modication time of the file. 0 if not specified 71 | std::int64_t mtime; 72 | 73 | // file attributes 74 | lt::file_flags_t file_flags; 75 | 76 | // the piece hashes for this file 77 | // note that small files don't have a piece layer 78 | std::vector piece_layer; 79 | }; 80 | 81 | lt::sha256_hash merkle_pad(int blocks, int pieces) 82 | { 83 | TORRENT_ASSERT(blocks >= pieces); 84 | lt::sha256_hash ret{}; 85 | while (pieces < blocks) 86 | { 87 | lt::hasher256 h; 88 | h.update(ret); 89 | h.update(ret); 90 | ret = h.final(); 91 | pieces *= 2; 92 | } 93 | return ret; 94 | } 95 | 96 | std::size_t merkle_num_leafs(std::size_t const blocks) 97 | { 98 | TORRENT_ASSERT(blocks > 0); 99 | TORRENT_ASSERT(blocks <= std::numeric_limits::max() / 2); 100 | // round up to nearest 2 exponent 101 | std::size_t ret = 1; 102 | while (blocks > ret) ret <<= 1; 103 | return ret; 104 | } 105 | 106 | } // anonymous namespace 107 | 108 | int main(int argc_, char const* argv_[]) try 109 | { 110 | lt::span args(argv_, argc_); 111 | // strip executable name 112 | args = args.subspan(1); 113 | 114 | std::unordered_map files; 115 | 116 | std::string output_file = "a.torrent"; 117 | std::string name; 118 | std::string creator; 119 | std::string comment_str; 120 | std::time_t creation_date = 0; 121 | bool private_torrent = false; 122 | bool quiet = false; 123 | std::set web_seeds; 124 | std::set> dht_nodes; 125 | 126 | std::vector> trackers; 127 | 128 | if (args.empty()) { 129 | print_usage(); 130 | return 1; 131 | } 132 | 133 | while (args.size() > 0 && args[0][0] == '-') { 134 | 135 | if ((args[0] == "-o"sv || args[0] == "--out"sv) && args.size() > 1) { 136 | output_file = args[1]; 137 | args = args.subspan(1); 138 | } 139 | else if (args[0] == "-q"sv) { 140 | quiet = true; 141 | } 142 | else if (args[0] == "-h"sv || args[0] == "--help"sv) { 143 | print_usage(); 144 | return 0; 145 | } 146 | else if ((args[0] == "-n"sv || args[0] == "--name"sv) && args.size() > 1) { 147 | name = args[1]; 148 | args = args.subspan(1); 149 | } 150 | else { 151 | std::cerr << "unknown option " << args[0] << '\n'; 152 | print_usage(); 153 | return 1; 154 | } 155 | args = args.subspan(1); 156 | } 157 | 158 | // all remaining strings in args are expected to be .torrent files to be 159 | // loaded 160 | 161 | int max_piece_size = 0; 162 | for (auto const filename : args) { 163 | 164 | if (!quiet) std::cout << "-> " << filename << "\n"; 165 | lt::torrent_info t{std::string(filename)}; 166 | lt::file_storage const& fs = t.files(); 167 | 168 | if (name.empty()) name = fs.name(); 169 | 170 | for (auto const& ae : t.trackers()) { 171 | if (ae.tier >= trackers.size()) 172 | trackers.resize(std::size_t(ae.tier) + 1); 173 | 174 | trackers[ae.tier].insert(ae.url); 175 | } 176 | 177 | for (auto const& ws : t.web_seeds()) { 178 | if (ws.type != lt::web_seed_entry::url_seed) continue; 179 | web_seeds.insert(ws.url); 180 | } 181 | 182 | for (auto const& n : t.nodes()) { 183 | dht_nodes.insert(n); 184 | } 185 | 186 | if (creator.empty()) 187 | creator = t.creator(); 188 | 189 | if (comment_str.empty()) 190 | comment_str = t.comment(); 191 | 192 | creation_date = std::max(creation_date, t.creation_date()); 193 | 194 | // TODO: pull CA cert out 195 | 196 | private_torrent |= t.priv(); 197 | 198 | for (lt::file_index_t i : fs.file_range()) { 199 | 200 | if (fs.pad_file_at(i)) continue; 201 | 202 | lt::sha256_hash const root = fs.root(i); 203 | if (files.find(root) != files.end()) { 204 | if (!quiet) std::cout << "ignoring " << fs.file_name(i) << " (duplicate)\n"; 205 | continue; 206 | } 207 | 208 | if (fs.file_flags(i) & lt::file_storage::flag_symlink) { 209 | if (!quiet) std::cout << "ignoring " << fs.file_name(i) << " (symlinks not supported)\n"; 210 | continue; 211 | } 212 | 213 | // TODO: what to do about different files with the same name? They 214 | // are not allowed by the torrent format 215 | 216 | max_piece_size = std::max(t.piece_length(), max_piece_size); 217 | 218 | auto const piece_layer = t.piece_layer(i); 219 | 220 | file_metadata meta{std::string(fs.file_name(i)) 221 | , t.piece_length() 222 | , fs.file_size(i) 223 | , fs.mtime(i) 224 | , fs.file_flags(i) 225 | , make_piece_layer(piece_layer)}; 226 | files[root] = std::move(meta); 227 | 228 | if (!quiet) std::cout << " " << root << ' ' << fs.file_size(i) << ' ' << fs.file_name(i) << '\n'; 229 | } 230 | } 231 | 232 | if (!quiet) { 233 | std::cout << "piece size: " << max_piece_size << '\n'; 234 | 235 | if (!dht_nodes.empty()) { 236 | std::cout << "DHT nodes:\n"; 237 | for (auto const& n : dht_nodes) { 238 | std::cout << n.first << ":" << n.second << '\n'; 239 | } 240 | } 241 | 242 | if (!web_seeds.empty()) { 243 | std::cout << "web seeds:\n"; 244 | for (auto const& w : web_seeds) { 245 | std::cout << w << '\n'; 246 | } 247 | } 248 | 249 | if (!trackers.empty()) { 250 | std::cout << "trackers:\n"; 251 | int counter = 0; 252 | for (auto const& tier : trackers) { 253 | if (!tier.empty()) std::cout << " tier " << counter << '\n'; 254 | ++counter; 255 | for (auto const& url : tier) { 256 | std::cout << " " << url << '\n'; 257 | } 258 | } 259 | } 260 | 261 | if (!comment_str.empty()) { 262 | std::cout << "comment: " << comment_str << '\n'; 263 | } 264 | 265 | if (!creator.empty()) { 266 | std::cout << "created by: " << creator << '\n'; 267 | } 268 | 269 | if (private_torrent) 270 | std::cout << "private: Yes\n"; 271 | } 272 | 273 | lt::entry torrent_e; 274 | auto& p_layers = torrent_e["piece layers"]; 275 | auto& info_out = torrent_e["info"]; 276 | info_out["meta version"] = 2; 277 | info_out["piece length"] = max_piece_size; 278 | info_out["name"] = name; 279 | if (private_torrent) info_out["private"] = 1; 280 | if (!creator.empty()) { 281 | torrent_e["created by"] = creator; 282 | } 283 | if (!comment_str.empty()) { 284 | torrent_e["comment"] = comment_str; 285 | } 286 | torrent_e["creation date"] = creation_date ? creation_date : std::time(nullptr); 287 | if (!trackers.empty()) { 288 | if (trackers.size() == 1 && trackers.front().size() == 1) { 289 | torrent_e["announce"] = *trackers.front().begin(); 290 | } 291 | else { 292 | auto& tiers = torrent_e["announce"].list(); 293 | for (auto& tt : trackers) { 294 | auto& tier = tiers.emplace_back(); 295 | for (auto& url : tt) { 296 | tier.list().emplace_back(std::move(url)); 297 | } 298 | } 299 | } 300 | } 301 | if (!web_seeds.empty()) { 302 | auto& ws = torrent_e["url-list"]; 303 | if (web_seeds.size() == 1) { 304 | ws = *web_seeds.begin(); 305 | } 306 | else { 307 | for (auto& url : web_seeds) { 308 | ws.list().emplace_back(std::move(url)); 309 | } 310 | } 311 | } 312 | if (!dht_nodes.empty()) { 313 | auto& nodes = torrent_e["nodes"]; 314 | for (auto const& n : dht_nodes) { 315 | auto& l = nodes.list(); 316 | l.push_back(n.first); 317 | l.push_back(n.second); 318 | } 319 | } 320 | 321 | auto& file_tree = info_out["file tree"]; 322 | 323 | for (auto& [root, f] : files) { 324 | auto& entry = file_tree[f.filename][""]; 325 | entry["length"] = f.file_size; 326 | entry["pieces root"] = root; 327 | if (f.mtime != 0) { 328 | entry["mtime"] = f.mtime; 329 | } 330 | if (f.file_flags & lt::file_storage::flag_executable) 331 | entry["attr"].string() += 'x'; 332 | 333 | if (f.file_flags & lt::file_storage::flag_hidden) 334 | entry["attr"].string() += 'h'; 335 | 336 | if (f.piece_size != max_piece_size) { 337 | // in this case we need to combine some of the piece layer hashes to 338 | // raise them up to a higher level in the merkle tree 339 | lt::sha256_hash pad = merkle_pad(f.piece_size / 0x4000, 1); 340 | 341 | f.piece_layer.resize(merkle_num_leafs(f.piece_layer.size()), pad); 342 | 343 | while (f.piece_size < max_piece_size) { 344 | // reduce the piece layer by one level 345 | for (std::size_t i = 0; i < f.piece_layer.size(); i += 2) { 346 | auto const left = f.piece_layer[i]; 347 | auto const right = f.piece_layer[i + 1]; 348 | f.piece_layer[i / 2] = lt::hasher256().update(left).update(right).final(); 349 | } 350 | pad = lt::hasher256().update(pad).update(pad).final(); 351 | f.piece_layer.resize(f.piece_layer.size() / 2); 352 | f.piece_size *= 2; 353 | } 354 | 355 | // remove any remaining padding at the end 356 | while (!f.piece_layer.empty() && f.piece_layer.back() == pad) 357 | f.piece_layer.resize(f.piece_layer.size() - 1); 358 | } 359 | 360 | // not all files have piece lyers. Files that are just a single block 361 | // just have the block hash as the tree root 362 | if (!f.piece_layer.empty()) { 363 | std::string& pieces = p_layers[root.to_string()].string(); 364 | 365 | pieces.clear(); 366 | pieces.reserve(f.piece_layer.size() * lt::sha256_hash::size()); 367 | for (auto& p : f.piece_layer) 368 | pieces.append(reinterpret_cast(p.data()), p.size()); 369 | } 370 | } 371 | 372 | std::vector torrent; 373 | lt::bencode(back_inserter(torrent), torrent_e); 374 | std::fstream out; 375 | out.exceptions(std::ifstream::failbit); 376 | out.open(output_file.c_str(), std::ios_base::out | std::ios_base::binary); 377 | 378 | if (!quiet) std::cout << "-> writing to " << output_file << "\n"; 379 | out.write(torrent.data(), int(torrent.size())); 380 | } 381 | catch (std::exception const& e) 382 | { 383 | std::cerr << "failed: " << e.what() << '\n'; 384 | } 385 | 386 | -------------------------------------------------------------------------------- /modify.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Copyright (c) 2020, Arvid Norberg 4 | All rights reserved. 5 | 6 | You may use, distribute and modify this code under the terms of the BSD license, 7 | see LICENSE file. 8 | */ 9 | 10 | #include "libtorrent/entry.hpp" 11 | #include "libtorrent/bencode.hpp" 12 | #include "libtorrent/torrent_info.hpp" 13 | #include "libtorrent/create_torrent.hpp" 14 | 15 | #include "common.hpp" 16 | 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | 24 | #ifdef TORRENT_WINDOWS 25 | #include // for _getcwd 26 | #endif 27 | 28 | #include 29 | 30 | using namespace std::string_view_literals; 31 | 32 | namespace { 33 | 34 | using namespace std::placeholders; 35 | 36 | void print_usage() 37 | { 38 | std::cerr << R"(USAGE: torrent-modify [OPTIONS] file 39 | 40 | Loads the specified torrent file, modifies it according to the specified options 41 | and writes it to an output .torrent file (as specified by -o) 42 | 43 | OPTIONS: 44 | -o, --out Print resulting torrent to the specified file. 45 | If not specified "a.torrent" is used. 46 | 47 | adding fields: 48 | 49 | -n, --name Change name of the torrent to the specified one. This 50 | also affects the name of the root directory. 51 | -t, --tracker Add as a tracker in a new tier. 52 | -T, --tracker-tier Add as a tracker in the current tier. 53 | -w, --web-seed Add as a web seed to the torrent. 54 | -C, --creator sets the "created by" field to . 55 | -c, --comment Sets the "comment" field to . 56 | -d, --dht-node Add a DHT node with the specified hostname and port. 57 | --private Set the "private" field to 1. 58 | --root-cert Embed the specified root certificate in the torrent file 59 | (for SSL torrents only). All peers and trackers must 60 | authenticate with a cert signed by this root, directly 61 | or indirectly. 62 | 63 | Removing fields: 64 | 65 | --public Remove the "private" flag 66 | --drop-mtime Remove all mtime fields from files 67 | --drop-trackers Remove all trackers (this happens before any new 68 | trackers are added from the command line) 69 | --drop-web-seeds Remove all web seeds (this happens before any new web 70 | seeds are added from the command line) 71 | --drop-dht-nodes Remove DHT nodes from the torrent file (new DHT nodes 72 | can still be added with the --dht-node option) 73 | --drop-comment Remove comment 74 | --drop-creator Remove creator string 75 | --drop-creation-date Remove creation date field 76 | --drop-root-cert Remove the root certificate. 77 | 78 | Removing files: 79 | 80 | --drop-file Remove all files whose name exactly matches 81 | 82 | -h, --help Show this message 83 | 84 | TRACKER TIERS 85 | 86 | To manage tracker tiers -t will add a new tier immediately before adding the 87 | tracker whereas -T will add the tracker to the current tier. If there is no 88 | tier, one will be created regardless of which flavour of -t and -T is used. e.g. 89 | 90 | -t https://foo.com -t https://bar.com 91 | 92 | Will add foo and bar as separate tiers. 93 | 94 | -t https://foo.com -T https://bar.com 95 | 96 | Will add foo and bar as the same tier. 97 | )"; 98 | } 99 | 100 | struct file_metadata 101 | { 102 | lt::index_range pieces; 103 | lt::span piece_layer; 104 | char const* root_hash; 105 | lt::file_index_t idx; 106 | }; 107 | 108 | } // anonymous namespace 109 | 110 | int main(int argc_, char const* argv_[]) try 111 | { 112 | lt::span args(argv_, argc_); 113 | // strip executable name 114 | args = args.subspan(1); 115 | 116 | if (args.size() < 2) { 117 | print_usage(); 118 | return 1; 119 | } 120 | 121 | std::string creator; 122 | std::string name; 123 | std::string comment_str; 124 | bool make_private_torrent = false; 125 | bool make_public_torrent = false; 126 | std::vector web_seeds; 127 | std::vector> trackers; 128 | std::vector> dht_nodes; 129 | lt::create_flags_t flags = {}; 130 | std::string root_cert; 131 | bool quiet = false; 132 | std::set drop_file; 133 | std::map rename_file; 134 | 135 | bool drop_trackers = false; 136 | bool drop_mtime = false; 137 | bool drop_web_seeds = false; 138 | bool drop_dht_nodes = false; 139 | bool drop_comment = false; 140 | bool drop_creator = false; 141 | bool drop_creation_date = false; 142 | bool drop_root_cert = false; 143 | 144 | std::string output_file = "a.torrent"; 145 | 146 | while (args.size() > 0 && args[0][0] == '-') { 147 | 148 | if ((args[0] == "-o"sv || args[0] == "--out"sv) && args.size() > 1) { 149 | output_file = args[1]; 150 | args = args.subspan(1); 151 | } 152 | else if ((args[0] == "-t"sv || args[0] == "--tracker"sv) && args.size() > 1) { 153 | std::string t = args[1]; 154 | args = args.subspan(1); 155 | trackers.emplace_back(std::vector{std::move(t)}); 156 | } 157 | else if ((args[0] == "-T"sv || args[0] == "--tracker-tier"sv) && args.size() > 1) { 158 | std::string t = args[1]; 159 | args = args.subspan(1); 160 | if (trackers.empty()) 161 | trackers.emplace_back(std::vector{std::move(t)}); 162 | else 163 | trackers.back().emplace_back(std::move(t)); 164 | } 165 | else if ((args[0] == "-w"sv || args[0] == "--web-seed"sv) && args.size() > 1) { 166 | web_seeds.emplace_back(args[1]); 167 | args = args.subspan(1); 168 | } 169 | else if (args[0] == "--dht-node"sv && args.size() > 2) { 170 | dht_nodes.emplace_back(args[1], std::atoi(args[2])); 171 | args = args.subspan(2); 172 | } 173 | else if ((args[0] == "-C"sv || args[0] == "--creator"sv) && args.size() > 1) { 174 | creator = args[1]; 175 | args = args.subspan(1); 176 | } 177 | else if ((args[0] == "-c"sv || args[0] == "--comment"sv) && args.size() > 1) { 178 | comment_str = args[1]; 179 | args = args.subspan(1); 180 | } 181 | else if (args[0] == "--drop-file"sv && args.size() > 1) { 182 | drop_file.emplace(args[1]); 183 | args = args.subspan(1); 184 | } 185 | else if (args[0] == "--rename-file"sv && args.size() > 2) { 186 | rename_file.emplace(args[1], args[2]); 187 | args = args.subspan(2); 188 | } 189 | else if (args[0] == "--drop-trackers"sv) { 190 | drop_trackers = true; 191 | } 192 | else if (args[0] == "--drop-mtime"sv) { 193 | drop_mtime = true; 194 | } 195 | else if (args[0] == "--drop-web-seeds"sv) { 196 | drop_web_seeds = true; 197 | } 198 | else if (args[0] == "--drop-dht-nodes"sv) { 199 | drop_dht_nodes = true; 200 | } 201 | else if (args[0] == "--drop-comment"sv) { 202 | drop_comment = true; 203 | } 204 | else if (args[0] == "--drop-creator"sv) { 205 | drop_creator = true; 206 | } 207 | else if (args[0] == "--drop-creation-date"sv) { 208 | drop_creation_date = true; 209 | } 210 | else if (args[0] == "--drop-root-cert"sv) { 211 | drop_root_cert = true; 212 | } 213 | else if (args[0] == "--private"sv) { 214 | make_private_torrent = true; 215 | } 216 | else if (args[0] == "--public"sv) { 217 | make_public_torrent = true; 218 | } 219 | else if ((args[0] == "-r"sv || args[0] == "--root-cert"sv) && args.size() > 1) { 220 | std::string cert_path = args[1]; 221 | 222 | if (!quiet) std::cout << "loading " << cert_path << '\n'; 223 | std::vector const pem = load_file(cert_path); 224 | root_cert.assign(pem.data(), pem.size()); 225 | args = args.subspan(1); 226 | } 227 | else if (args[0] == "-q"sv) { 228 | quiet = true; 229 | } 230 | else if (args[0] == "-h"sv || args[0] == "--help"sv) { 231 | print_usage(); 232 | return 0; 233 | } 234 | else if ((args[0] == "-n"sv || args[0] == "--name"sv) && args.size() > 1) { 235 | name = args[1]; 236 | args = args.subspan(1); 237 | } 238 | else { 239 | std::cerr << "unknown option (or missing argument) " << args[0] << '\n'; 240 | print_usage(); 241 | return 1; 242 | } 243 | args = args.subspan(1); 244 | } 245 | 246 | if (make_public_torrent && make_private_torrent) { 247 | std::cerr << "the flags --public and --private are incompatible\n"; 248 | print_usage(); 249 | return 1; 250 | } 251 | 252 | if (args.empty()) { 253 | print_usage(); 254 | std::cerr << "no torrent file specified.\n"; 255 | return 1; 256 | } 257 | std::string full_path = args[0]; 258 | 259 | if (args.size() > 1) { 260 | print_usage(); 261 | std::cerr << "ignored command line arguments after input file\n"; 262 | return 1; 263 | } 264 | 265 | lt::torrent_info input(full_path); 266 | lt::file_storage const& input_fs = input.files(); 267 | 268 | // the new file storage 269 | lt::file_storage fs; 270 | std::vector file_info; 271 | 272 | int const piece_size = input.piece_length(); 273 | fs.set_piece_length(piece_size); 274 | 275 | for (auto f : input_fs.file_range()) { 276 | 277 | lt::file_flags_t const file_flags = input_fs.file_flags(f); 278 | if (file_flags & lt::file_storage::flag_pad_file) continue; 279 | 280 | std::int64_t const file_offset = input_fs.file_offset(f); 281 | if ((file_offset % piece_size) != 0) { 282 | std::cerr << "file " << f << " (" << input_fs.file_name(f) << ") is not piece-aligned\n"; 283 | return 1; 284 | } 285 | 286 | std::string path = input_fs.file_path(f); 287 | std::int64_t const file_size = input_fs.file_size(f); 288 | std::time_t const mtime = drop_mtime ? 0 : input_fs.mtime(f); 289 | std::string const symlink_path 290 | = file_flags & lt::file_storage::flag_symlink 291 | ? input_fs.symlink(f) : std::string(); 292 | char const* root_hash = input_fs.root_ptr(f); 293 | 294 | auto const [parent, filename] = right_split(path); 295 | 296 | // ignore files whose name match one in drop_file 297 | if (!drop_file.empty()) { 298 | if (drop_file.count(filename)) 299 | continue; 300 | } 301 | 302 | if (!name.empty()) { 303 | path = replace_directory_element(path, name); 304 | } 305 | 306 | if (auto it = rename_file.find(filename); it != rename_file.end()) { 307 | #ifdef TORRENT_WINDOWS 308 | path = parent + '\\' + it->second; 309 | #else 310 | path = parent + '/' + it->second; 311 | #endif 312 | } 313 | 314 | auto const idx = fs.end_file(); 315 | fs.add_file(path, file_size, file_flags, mtime, symlink_path, root_hash); 316 | file_info.push_back(file_metadata{ 317 | lt::index_range{ 318 | lt::piece_index_t(file_offset / piece_size) 319 | , lt::piece_index_t((file_offset + input_fs.file_size(f) + piece_size - 1) / piece_size)} 320 | , input.piece_layer(f) 321 | , root_hash 322 | , idx 323 | }); 324 | } 325 | 326 | lt::create_torrent t(fs, piece_size, flags); 327 | 328 | // comment 329 | if (!drop_comment && comment_str.empty()) 330 | comment_str = input.comment(); 331 | 332 | if (!comment_str.empty()) 333 | t.set_comment(comment_str.c_str()); 334 | 335 | // creator 336 | if (!drop_creator && creator.empty()) 337 | creator = input.creator(); 338 | 339 | if (!creator.empty()) 340 | t.set_creator(creator.c_str()); 341 | 342 | if (drop_creation_date) { 343 | t.set_creation_date(0); 344 | } 345 | else { 346 | t.set_creation_date(input.creation_date()); 347 | } 348 | 349 | // SSL root cert 350 | if (!drop_root_cert && root_cert.empty()) { 351 | root_cert = std::string(input.ssl_cert()); 352 | } 353 | 354 | if (!root_cert.empty()) { 355 | t.set_root_cert(root_cert); 356 | } 357 | 358 | // propagate trackers 359 | if (!drop_trackers) { 360 | for (auto const& tr : input.trackers()) { 361 | int const tier = tr.tier; 362 | if (int(trackers.size()) <= tier) trackers.resize(tier + 1); 363 | trackers[tier].emplace_back(tr.url); 364 | } 365 | } 366 | 367 | int tier = 0; 368 | if (!trackers.empty()) { 369 | for (auto const& tt : trackers) { 370 | for (auto const& url : tt) { 371 | t.add_tracker(url, tier); 372 | } 373 | ++tier; 374 | } 375 | } 376 | 377 | // propagate web seeds 378 | if (!drop_web_seeds) { 379 | for (auto const& ws : input.web_seeds()) 380 | web_seeds.emplace_back(ws.url); 381 | } 382 | for (std::string const& ws : web_seeds) 383 | t.add_url_seed(ws); 384 | 385 | // DHT nodes 386 | if (!drop_dht_nodes) { 387 | auto const& input_nodes = input.nodes(); 388 | dht_nodes.insert(dht_nodes.end(), input_nodes.begin(), input_nodes.end()); 389 | } 390 | for (auto const& n : dht_nodes) { 391 | t.add_node(n); 392 | } 393 | 394 | // propagate private flag 395 | if (make_private_torrent) 396 | t.set_priv(true); 397 | else if (make_public_torrent) 398 | t.set_priv(false); 399 | else 400 | t.set_priv(input.priv()); 401 | 402 | if (input.info_hashes().has_v1()) { 403 | lt::piece_index_t p{}; 404 | for (auto const& info : file_info) { 405 | for (lt::piece_index_t i : info.pieces) { 406 | t.set_hash(p++, input.hash_for_piece(i)); 407 | } 408 | } 409 | } 410 | 411 | if (input.info_hashes().has_v2()) { 412 | for (auto const& info : file_info) { 413 | if (fs.pad_file_at(info.idx)) 414 | continue; 415 | lt::piece_index_t::diff_type p{0}; 416 | for (int h = 0; h < int(info.piece_layer.size()); h += int(lt::sha256_hash::size())) { 417 | t.set_hash2(info.idx, p++, lt::sha256_hash(info.piece_layer.data() + h)); 418 | } 419 | } 420 | } 421 | 422 | // create the torrent and print it 423 | std::vector torrent; 424 | lt::bencode(back_inserter(torrent), t.generate()); 425 | 426 | std::fstream out; 427 | out.exceptions(std::ifstream::failbit); 428 | out.open(output_file.c_str(), std::ios_base::out | std::ios_base::binary); 429 | out.write(torrent.data(), int(torrent.size())); 430 | 431 | return 0; 432 | } 433 | catch (std::exception& e) { 434 | std::cerr << "ERROR: " << e.what() << "\n"; 435 | return 1; 436 | } 437 | 438 | -------------------------------------------------------------------------------- /print.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Copyright (c) 2020, Arvid Norberg 4 | All rights reserved. 5 | 6 | You may use, distribute and modify this code under the terms of the BSD license, 7 | see LICENSE file. 8 | */ 9 | 10 | #include // for snprintf 11 | #include // for PRId64 et.al. 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | 19 | #include "libtorrent/torrent_info.hpp" 20 | #include "libtorrent/span.hpp" 21 | #include "common.hpp" 22 | 23 | #if defined _WIN32 24 | #include // for _isatty 25 | #define isatty(x) _isatty(x) 26 | #define fileno(x) _fileno(x) 27 | #else 28 | #include // for isatty 29 | #endif 30 | 31 | using namespace std::string_view_literals; 32 | 33 | namespace { 34 | 35 | void print_usage() 36 | { 37 | std::cout << R"(usage: torrent-print [OPTIONS] torrent-files... 38 | 39 | -h, --help Show this message 40 | 41 | PRINT OPTIONS: 42 | -f, --files List files in torrent(s) 43 | -n, --piece-count Print number of pieces 44 | --piece-size Print the piece size 45 | --info-hash Print the info-hash(es), both v1 and v2 46 | --comment Print the comment field 47 | --creator Print the creator field 48 | --date Print the creation date field 49 | --name Print the torrent name 50 | --private Print the private field 51 | --trackers Print trackers 52 | --web-seeds Print web-seeds 53 | --dht-nodes Print DHT-nodes 54 | )" 55 | 56 | #if LIBTORRENT_VERSION_NUM >= 30000 57 | R"(--total-size Print the sum of all (non-pad) files 58 | )" 59 | #endif 60 | R"(FILE PRINT OPTIONS: 61 | --file-roots Print file merkle root hashes 62 | --no-file-attributes Don't print file attributes 63 | --file-offsets Print file offsets 64 | --file-piece-range Print first and last piece index for files 65 | --no-file-size Don't print file sizes 66 | --file-mtime Print file modification time (if available) 67 | --tree Print file structure as a tree (default) 68 | --flat Print file structure as a flat list 69 | --no-colors Disable color escape sequences in output 70 | --colors Force printing colors in output 71 | -H, --human-readable Print file sizes with SI prefixed units 72 | 73 | PARSE OPTIONS: 74 | --items-limit Set the upper limit of the number of bencode items 75 | in the torrent file. 76 | --depth-limit Set the recursion limit in the bdecoder 77 | --show-padfiles Show pad files in file list 78 | --max-pieces Set the upper limit on the number of pieces to 79 | load in the torrent. 80 | --max-size Reject files larger than this size limit, specified in MB 81 | 82 | By default, all properties of torrents are printed. If any option is specified 83 | to print a specific property, only those specified are printed. 84 | 85 | Colored output is enabled by default, as long as stdout is a TTY. Forcing color 86 | output on and off can be done with the --no-color and --color options. 87 | )"; 88 | } 89 | 90 | bool show_pad = false; 91 | bool print_file_roots = false; 92 | bool print_file_attributes = true; 93 | bool print_file_offsets = false; 94 | bool print_file_piece_range = false; 95 | bool print_file_size = true; 96 | bool print_file_mtime = false; 97 | bool print_tree = true; 98 | bool print_colors = true; 99 | bool print_human_readable = false; 100 | 101 | enum class element_t : std::uint8_t 102 | { 103 | directory, attributes, time_stamp, file_root 104 | }; 105 | 106 | bool pick_color(element_t const t) 107 | { 108 | if (!print_colors) return false; 109 | 110 | switch (t) 111 | { 112 | case element_t::directory: 113 | std::cout << "\x1b[34m"; 114 | return true; 115 | case element_t::attributes: 116 | std::cout << "\x1b[36m"; 117 | return true; 118 | case element_t::time_stamp: 119 | std::cout << "\x1b[36m"; 120 | return true; 121 | case element_t::file_root: 122 | std::cout << "\x1b[32m"; 123 | return true; 124 | } 125 | 126 | return false; 127 | } 128 | 129 | bool pick_file_color(lt::file_flags_t const flags) 130 | { 131 | if (!print_colors) return false; 132 | 133 | if (flags & lt::file_storage::flag_symlink) { 134 | std::cout << "\x1b[35m"; 135 | return true; 136 | } 137 | 138 | if (flags & lt::file_storage::flag_executable) { 139 | std::cout << "\x1b[31m"; 140 | return true; 141 | } 142 | 143 | if (flags & lt::file_storage::flag_hidden) { 144 | std::cout << "\x1b[36m"; 145 | return true; 146 | } 147 | 148 | if (flags & lt::file_storage::flag_pad_file) { 149 | std::cout << "\x1b[33m"; 150 | return true; 151 | } 152 | 153 | return false; 154 | } 155 | 156 | 157 | std::string human_readable(std::int64_t val) 158 | { 159 | std::stringstream ret; 160 | 161 | ret << std::fixed; 162 | if (val > std::int64_t(1024) * 1024 * 1024 * 1024) 163 | ret << std::setprecision(2) << double(val) / (std::int64_t(1024) * 1024 * 1024 * 1024) << " TiB"; 164 | else if (val > 1024 * 1024 * 1024) 165 | ret << std::setprecision(2) << double(val) / (1024 * 1024 * 1024) << " GiB"; 166 | else if (val > 1024 * 1024) 167 | ret << std::setprecision(2) << double(val) / (1024 * 1024) << " MiB"; 168 | else if (val > 1024) 169 | ret << std::setprecision(2) << double(val) / 1024 << " kiB"; 170 | else 171 | ret << val; 172 | return ret.str(); 173 | } 174 | 175 | std::string print_timestamp(std::time_t const t) 176 | { 177 | if (t == 0) return "-"; 178 | tm* fields = ::gmtime(&t); 179 | std::stringstream str; 180 | str << (fields->tm_year + 1900) << "-" 181 | << std::setw(2) << std::setfill('0') << (fields->tm_mon + 1) << "-" 182 | << std::setw(2) << std::setfill('0') << fields->tm_mday << " " 183 | << std::setw(2) << std::setfill('0') << fields->tm_hour << ":" 184 | << std::setw(2) << std::setfill('0') << fields->tm_min << ":" 185 | << std::setw(2) << std::setfill('0') << fields->tm_sec; 186 | return str.str(); 187 | } 188 | 189 | void print_file_attrs(lt::file_storage const& st, lt::file_index_t i, bool const v2) 190 | { 191 | if (print_file_offsets) { 192 | std::cout << std::setw(11) << st.file_offset(i) << " "; 193 | } 194 | 195 | if (print_file_size) { 196 | std::cout << std::setw(11); 197 | if (print_human_readable) 198 | std::cout << human_readable(st.file_size(i)); 199 | else 200 | std::cout << st.file_size(i); 201 | } 202 | 203 | if (print_file_attributes) { 204 | bool const terminate_color = pick_color(element_t::attributes); 205 | auto const flags = st.file_flags(i); 206 | std::cout << " " 207 | << ((flags & lt::file_storage::flag_pad_file)?'p':'-') 208 | << ((flags & lt::file_storage::flag_executable)?'x':'-') 209 | << ((flags & lt::file_storage::flag_hidden)?'h':'-') 210 | << ((flags & lt::file_storage::flag_symlink)?'l':'-') 211 | << " "; 212 | if (terminate_color) std::cout << "\x1b[39m"; 213 | } 214 | 215 | if (print_file_piece_range) { 216 | auto const first = st.map_file(i, 0, 0).piece; 217 | auto const last = st.map_file(i, std::max(std::int64_t(st.file_size(i)) - 1, std::int64_t(0)), 0).piece; 218 | std::cout << " [ " 219 | << std::setw(5) << static_cast(first) << ", " 220 | << std::setw(5) << static_cast(last) << " ] "; 221 | } 222 | 223 | if (print_file_mtime) { 224 | if (st.mtime(i) == 0) { 225 | std::cout << " "; 226 | } 227 | else { 228 | bool const terminate_color = pick_color(element_t::time_stamp); 229 | std::cout << print_timestamp(st.mtime(i)) << " "; 230 | if (terminate_color) std::cout << "\x1b[39m"; 231 | } 232 | } 233 | 234 | if (print_file_roots && v2) 235 | { 236 | if (st.root(i).is_all_zeros()) 237 | { 238 | std::cout << " "; 239 | } 240 | else 241 | { 242 | bool const terminate_color = pick_color(element_t::file_root); 243 | std::cout << st.root(i) << " "; 244 | if (terminate_color) std::cout << "\x1b[39m"; 245 | } 246 | } 247 | } 248 | 249 | void print_blank_attrs(bool const v2) 250 | { 251 | if (print_file_offsets) { 252 | std::cout << " "; 253 | } 254 | 255 | if (print_file_size) { 256 | std::cout << " "; 257 | } 258 | 259 | if (print_file_attributes) { 260 | std::cout << " "; 261 | } 262 | 263 | if (print_file_piece_range) { 264 | std::cout << " "; 265 | } 266 | 267 | if (print_file_mtime) { 268 | std::cout << " "; 269 | } 270 | 271 | if (print_file_roots && v2) 272 | { 273 | std::cout << " "; 274 | } 275 | } 276 | 277 | void print_file_list(lt::file_storage const& st) 278 | { 279 | for (auto const i : st.file_range()) 280 | { 281 | auto const flags = st.file_flags(i); 282 | if ((flags & lt::file_storage::flag_pad_file) && !show_pad) continue; 283 | 284 | print_file_attrs(st, i, st.v2()); 285 | 286 | bool const terminate_color = pick_file_color(flags); 287 | std::cout << st.file_path(i); 288 | if (terminate_color) std::cout << "\x1b[39m"; 289 | 290 | if (flags & lt::file_storage::flag_symlink) { 291 | std::cout << " -> " << st.symlink(i); 292 | } 293 | std::cout << '\n'; 294 | } 295 | } 296 | 297 | struct directory_entry; 298 | 299 | using directory_entry_t 300 | = std::variant< 301 | std::map, 302 | lt::file_index_t>; 303 | 304 | struct directory_entry { 305 | directory_entry_t e; 306 | }; 307 | 308 | void parse_single_file(std::map& dir 309 | , std::string path, lt::file_index_t idx) 310 | { 311 | auto const [left, right] = left_split(path); 312 | if (right.empty()) { 313 | // this is just the filename 314 | dir.insert({left, directory_entry{idx}}); 315 | } 316 | else { 317 | // this has a parent path. add it first 318 | directory_entry_t& d = dir[left].e; 319 | if (d.index() != 0) { 320 | throw std::runtime_error("file clash with directory"); 321 | } 322 | parse_single_file(std::get<0>(d), right, idx); 323 | } 324 | } 325 | 326 | directory_entry parse_file_list(lt::file_storage const& st) 327 | { 328 | directory_entry tree; 329 | for (auto const i : st.file_range()) 330 | { 331 | auto const flags = st.file_flags(i); 332 | if ((flags & lt::file_storage::flag_pad_file) && !show_pad) continue; 333 | parse_single_file(std::get<0>(tree.e), st.file_path(i), i); 334 | } 335 | return tree; 336 | } 337 | 338 | void print_tree_impl(lt::file_storage const& st, std::vector& levels 339 | , std::map const& tree) 340 | { 341 | std::size_t counter = 0; 342 | 343 | for (auto const& [name, e] : tree) { 344 | 345 | if (e.e.index() == 1) { 346 | print_file_attrs(st, std::get<1>(e.e), st.v2()); 347 | } 348 | else { 349 | // print the indentation 350 | print_blank_attrs(st.v2()); 351 | } 352 | 353 | ++counter; 354 | bool const last = counter == tree.size(); 355 | for (bool l : levels) { 356 | if (l) 357 | std::cout << " \u2502"; 358 | else 359 | std::cout << " "; 360 | } 361 | 362 | if (last) { 363 | std::cout << " \u2514 "; 364 | } 365 | else { 366 | std::cout << " \u251c "; 367 | } 368 | 369 | if (e.e.index() == 1) { 370 | auto const i = std::get<1>(e.e); 371 | auto const flags = st.file_flags(i); 372 | 373 | bool const terminate_color = pick_file_color(flags); 374 | std::cout << name; 375 | if (terminate_color) std::cout << "\x1b[39m"; 376 | 377 | if (flags & lt::file_storage::flag_symlink) { 378 | std::cout << " -> " << st.symlink(i); 379 | } 380 | } 381 | else { 382 | bool const terminate_color = pick_color(element_t::directory); 383 | std::cout << name; 384 | if (terminate_color) std::cout << "\x1b[39m"; 385 | } 386 | std::cout << '\n'; 387 | 388 | if (e.e.index() == 0) { 389 | // this is a directory, add another level 390 | levels.push_back(!last); 391 | print_tree_impl(st, levels, std::get<0>(e.e)); 392 | levels.resize(levels.size() - 1); 393 | } 394 | } 395 | } 396 | 397 | void print_file_tree(lt::file_storage const& st) 398 | { 399 | std::vector levels; 400 | print_tree_impl(st, levels, std::get<0>(parse_file_list(st).e)); 401 | } 402 | } 403 | 404 | int main(int argc, char const* argv[]) try 405 | { 406 | lt::span args(argv, argc); 407 | // strip executable name 408 | args = args.subspan(1); 409 | 410 | lt::load_torrent_limits cfg; 411 | bool print_files = false; 412 | bool print_piece_count = false; 413 | bool print_piece_size = false; 414 | bool print_info_hash = false; 415 | bool print_comment = false; 416 | bool print_creator = false; 417 | bool print_date = false; 418 | bool print_name = false; 419 | bool print_private = false; 420 | bool print_trackers = false; 421 | bool print_web_seeds = false; 422 | bool print_dht_nodes = false; 423 | #if LIBTORRENT_VERSION_NUM >= 30000 424 | bool print_size_on_disk = false; 425 | #endif 426 | 427 | bool print_all = true; 428 | 429 | if (!isatty(fileno(stdout))) { 430 | print_colors = false; 431 | } 432 | 433 | if (args.empty()) { 434 | print_usage(); 435 | return 1; 436 | } 437 | 438 | using namespace lt::literals; 439 | 440 | while (!args.empty() && args[0][0] == '-') { 441 | 442 | if (args[0] == "-f"sv || args[0] == "--files"sv) 443 | { 444 | print_files = true; 445 | print_all = false; 446 | } 447 | else if (args[0] == "-n"sv || args[0] == "--piece-count"sv) 448 | { 449 | print_piece_count = true; 450 | print_all = false; 451 | } 452 | else if (args[0] == "--piece-size"sv) 453 | { 454 | print_piece_size = true; 455 | print_all = false; 456 | } 457 | else if (args[0] == "--info-hash"sv) 458 | { 459 | print_info_hash = true; 460 | print_all = false; 461 | } 462 | else if (args[0] == "--comment"sv) 463 | { 464 | print_comment = true; 465 | print_all = false; 466 | } 467 | else if (args[0] == "--creator"sv) 468 | { 469 | print_creator = true; 470 | print_all = false; 471 | } 472 | else if (args[0] == "--date"sv) 473 | { 474 | print_date = true; 475 | print_all = false; 476 | } 477 | else if (args[0] == "--name"sv) 478 | { 479 | print_name = true; 480 | print_all = false; 481 | } 482 | else if (args[0] == "--private"sv) 483 | { 484 | print_private = true; 485 | print_all = false; 486 | } 487 | else if (args[0] == "--trackers"sv) 488 | { 489 | print_trackers = true; 490 | print_all = false; 491 | } 492 | else if (args[0] == "--web-seeds"sv) 493 | { 494 | print_web_seeds = true; 495 | print_all = false; 496 | } 497 | else if (args[0] == "--dht-nodes"sv) 498 | { 499 | print_dht_nodes = true; 500 | print_all = false; 501 | } 502 | #if LIBTORRENT_VERSION_NUM >= 30000 503 | else if (args[0] == "--total-size"sv) 504 | { 505 | print_size_on_disk = true; 506 | print_all = false; 507 | } 508 | #endif 509 | else if (args[0] == "-H"sv || args[0] == "--human-readable"sv) 510 | { 511 | print_human_readable = true; 512 | } 513 | else if (args[0] == "--tree"sv) 514 | { 515 | print_tree = true; 516 | } 517 | else if (args[0] == "--flat"sv) 518 | { 519 | print_tree = false; 520 | } 521 | else if (args[0] == "--colors"sv) 522 | { 523 | print_colors = true; 524 | } 525 | else if (args[0] == "--no-colors"sv) 526 | { 527 | print_colors = false; 528 | } 529 | else if (args[0] == "--file-roots"sv) 530 | { 531 | print_file_roots = true; 532 | } 533 | else if (args[0] == "--no-file-attributes"sv) 534 | { 535 | print_file_attributes = false; 536 | } 537 | else if (args[0] == "--file-offsets"sv) 538 | { 539 | print_file_offsets = true; 540 | } 541 | else if (args[0] == "--file-piece-range"sv) 542 | { 543 | print_file_piece_range = true; 544 | } 545 | else if (args[0] == "--no-file-size"sv) 546 | { 547 | print_file_size = false; 548 | } 549 | else if (args[0] == "--file-mtime"sv) 550 | { 551 | print_file_mtime = true; 552 | } 553 | else if (args[0] == "--items-limit"sv && args.size() > 1) 554 | { 555 | cfg.max_decode_tokens = atoi(args[1]); 556 | args = args.subspan(1); 557 | } 558 | else if (args[0] == "--depth-limit"sv && args.size() > 1) 559 | { 560 | cfg.max_decode_depth = atoi(args[1]); 561 | args = args.subspan(1); 562 | } 563 | else if (args[0] == "--max-pieces"sv && args.size() > 1) 564 | { 565 | cfg.max_pieces = atoi(args[1]); 566 | args = args.subspan(1); 567 | } 568 | else if (args[0] == "--max-size"sv && args.size() > 1) 569 | { 570 | cfg.max_buffer_size = atoi(args[1]) * 1024 * 1024; 571 | args = args.subspan(1); 572 | } 573 | else if (args[0] == "--show-padfiles"sv) 574 | { 575 | show_pad = true; 576 | } 577 | else if (args[0] == "-h"sv || args[0] == "--help"sv) { 578 | print_usage(); 579 | return 0; 580 | } 581 | else 582 | { 583 | std::cerr << "unknown option " << args[0] << '\n'; 584 | print_usage(); 585 | return 1; 586 | } 587 | args = args.subspan(1); 588 | } 589 | 590 | for (auto const filename : args) { 591 | 592 | lt::torrent_info const t(filename, cfg); 593 | 594 | if (args.size() > 1) { 595 | std::cout << filename << ":\n"; 596 | } 597 | 598 | // print info about torrent 599 | if ((print_all && !t.nodes().empty()) || print_dht_nodes) 600 | { 601 | std::cout << "nodes:\n"; 602 | for (auto const& i : t.nodes()) 603 | std::cout << i.first << ": " << i.second << "\n"; 604 | } 605 | 606 | #if LIBTORRENT_VERSION_NUM >= 30000 607 | if (print_all || print_size_on_disk) 608 | { 609 | std::cout << "size: " << t.size_on_disk() << "\n"; 610 | } 611 | #endif 612 | 613 | if ((print_all && !t.trackers().empty()) || print_trackers) 614 | { 615 | std::cout << "trackers:\n"; 616 | for (auto const& i : t.trackers()) 617 | std::cout << std::setw(2) << int(i.tier) << ": " << i.url << "\n"; 618 | } 619 | 620 | if ((print_all && !t.web_seeds().empty()) || print_web_seeds) { 621 | std::cout << "web seeds:\n"; 622 | for (auto const& ws : t.web_seeds()) 623 | { 624 | std::cout << (ws.type == lt::web_seed_entry::url_seed ? "BEP19" : "BEP17") 625 | << " " << ws.url << "\n"; 626 | } 627 | } 628 | 629 | if (print_all || print_piece_count) { 630 | std::cout << "piece-count: " << t.num_pieces() << '\n'; 631 | } 632 | 633 | if (print_all || print_piece_size ) { 634 | std::cout << "piece size: " << t.piece_length() << '\n'; 635 | } 636 | if (print_all || print_info_hash) { 637 | std::cout << "info hash:"; 638 | if (t.info_hashes().has_v1()) 639 | std::cout << " v1: " << t.info_hashes().v1; 640 | if (t.info_hashes().has_v2()) 641 | std::cout << " v2: " << t.info_hashes().v2; 642 | std::cout << '\n'; 643 | } 644 | 645 | if ((print_all && !t.comment().empty()) || print_comment) { 646 | std::cout << "comment: " << t.comment() << '\n'; 647 | } 648 | if ((print_all && !t.creator().empty()) || print_creator) { 649 | std::cout << "created by: " << t.creator() << '\n'; 650 | } 651 | if ((print_all && t.creation_date() != 0) || print_date) { 652 | std::cout << "creation date: " << print_timestamp(t.creation_date()) << '\n'; 653 | } 654 | if ((print_all && t.priv()) || print_private) { 655 | std::cout << "private: " << (t.priv() ? "yes" : "no") << "\n"; 656 | } 657 | if (print_all || print_name) { 658 | std::cout << "name: " << t.name() << '\n'; 659 | } 660 | if (print_all) { 661 | std::cout << "number of files: " << t.num_files() << '\n'; 662 | } 663 | 664 | if (print_all || print_files) { 665 | std::cout << "files:\n"; 666 | lt::file_storage const& st = t.files(); 667 | if (print_tree) { 668 | print_file_tree(st); 669 | } 670 | else { 671 | print_file_list(st); 672 | } 673 | } 674 | } 675 | } 676 | catch (std::exception const& e) 677 | { 678 | std::cerr << "failed: " << e.what() << '\n'; 679 | } 680 | --------------------------------------------------------------------------------