diff --git a/.clang-tidy b/.clang-tidy index d07160e..76aaf28 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -2,6 +2,7 @@ # Magic number check is a bit noisy, so off by default # -altera-struct-pack-align (too specific performance issue) +# -boost-* (not using boost) # -cert-err58-cpp statics might throw. Allowing this for now. # -clang-analyzer-cplusplus.NewDelete (temporary due to https://github.com/boostorg/asio/issues/341) # -cppcoreguidelines-avoid-const-or-ref-data-members (TODO: Recheck later) @@ -20,6 +21,7 @@ # -misc-non-private-member-variables-in-classes (optimally I would like to allow protected but not private) # -modernize-avoid-c-arrays (covered by cppcore) # -modernize-use-trailing-return-type (prefer old style for now) +# -portability-avoid-pragma-once (pragma once is nicer) # -readability-braces-around-statements (can't handle constexpr if) # -readability-function-cognitive-complexity (no hard limit on this) # -readability-inconsistent-declaration-parameter-name (prefer different names on templates) @@ -30,6 +32,7 @@ Checks: '*, -abseil-*, -altera-*, -altera-struct-pack-align, + -boost-*, -cert-err58-cpp, -clang-analyzer-cplusplus.NewDelete, -cppcoreguidelines-avoid-const-or-ref-data-members, @@ -55,6 +58,7 @@ Checks: '*, -modernize-avoid-c-arrays, -modernize-use-trailing-return-type, -objc-*, + -portability-avoid-pragma-once, -readability-braces-around-statements, -readability-function-cognitive-complexity, -readability-identifier-length, @@ -65,8 +69,7 @@ Checks: '*, -readability-named-parameter, ' WarningsAsErrors: '*' -HeaderFilterRegex: '(src|tests)/*.h$' -AnalyzeTemporaryDtors: false +HeaderFilterRegex: '.*' FormatStyle: none User: db CheckOptions: diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c53801c..3f53acf 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -5,8 +5,8 @@ jobs: strategy: matrix: compiler: - - { os: ubuntu-24.04, bin: clang++-18 } - - { os: ubuntu-24.04, bin: g++-13 } + - { os: ubuntu-24.04, bin: clang++-22 } + - { os: ubuntu-24.04, bin: g++-14 } - { os: windows-2022, bin: cl.exe } runs-on: ${{ matrix.compiler.os }} @@ -30,7 +30,7 @@ jobs: - name: Apt install if: runner.os == 'Linux' - run: sudo apt install -y ninja-build clang-18 g++-12 + run: sudo apt install -y ninja-build clang-22 g++-14 - name: Configure CMake run: cmake -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} -GNinja -DDISABLE_LTO=ON -DUSE_CLANG_TIDY=OFF -DCMAKE_TOOLCHAIN_FILE=${{github.workspace}}/vcpkg/scripts/buildsystems/vcpkg.cmake diff --git a/CMakeLists.txt b/CMakeLists.txt index a4f069f..bc3b81c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -66,22 +66,24 @@ endif() include_directories(SYSTEM "external") if(UNIX) - add_compile_options(-Wall -Wextra -Werror -Weffc++ -Wconversion -Wsign-conversion) + add_compile_options(-Wall -Wextra -Werror -Wconversion -Wsign-conversion) if(CMAKE_CXX_COMPILER_ID STREQUAL "Clang") # Be strict and just disable unwanted or noisy warnings add_compile_options( -Weverything - -Wno-c++98-compat # Don't care about c++98 - -Wno-c++98-compat-pedantic # Don't care about c++98 - -Wno-switch-default # Conflicts with -Wcovered-switch-default - -Wno-ctad-maybe-unsupported # Triggers on use of scoped_lock - -Wno-padded # - -Wno-weak-vtables # - -Wno-global-constructors # performance startup-time - -Wno-exit-time-destructors # similar as ^ for destructors - -Wno-undefined-func-template # does not seem very important - -Wno-zero-as-null-pointer-constant # fails clang-10 on travis but not locally? - -Wno-disabled-macro-expansion # Does not work when interfacing with csignal + -Wno-c++98-compat # Don't care about c++98 + -Wno-c++98-compat-pedantic # Don't care about c++98 + -Wno-switch-default # Conflicts with -Wcovered-switch-default + -Wno-ctad-maybe-unsupported # Triggers on use of scoped_lock + -Wno-padded # + -Wno-weak-vtables # + -Wno-global-constructors # performance startup-time + -Wno-exit-time-destructors # similar as ^ for destructors + -Wno-undefined-func-template # does not seem very important + -Wno-zero-as-null-pointer-constant # fails clang-10 on travis but not locally? + -Wno-disabled-macro-expansion # Does not work when interfacing with csignal + -Wno-unsafe-buffer-usage-in-libc-call # Complains about fgets but would like finer grain control like https://github.com/llvm/llvm-project/issues/127849 + -Wno-unsafe-buffer-usage-in-container # We sametimes use spans to shorten the bound ) elseif(CMAKE_CXX_COMPILER_ID STREQUAL "GNU") add_compile_options( diff --git a/README.md b/README.md index 5157393..ebb97be 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,10 @@ # Build instructions +## Compilers + +Note that at the moment the tested compilers are clang 22 and gcc 14 on Linux. + ## CMake + vcpkg * `git submodule update --init --recursive` @@ -14,7 +18,3 @@ Or with --preset zit-gcc. Note that on Linux `pkg-config` and `clang` or `gcc` must be installed. - -## [Nix](https://nixos.org) - -* `nix-build` diff --git a/default.nix b/default.nix deleted file mode 100644 index 769da76..0000000 --- a/default.nix +++ /dev/null @@ -1,68 +0,0 @@ -{ pkgs ? import (fetchTarball { - url = - "https://github.com/NixOS/nixpkgs/archive/f02597fb8e07c57c969df77f39a63bda64b09848.tar.gz"; - sha256 = "sha256:1a76da1j10m60jhfn5xdi8fmy7c2a8lhijrimn62ysd990m795wf"; -}) { } }: - -let - - # Override asio to a custom version - asio = pkgs.asio.overrideAttrs (finalAttrs: previousAttrs: rec { - version = "1.24.0"; - src = pkgs.fetchurl { - url = "mirror://sourceforge/asio/asio-${version}.tar.bz2"; - sha256 = "iXaBLCShGGAPb88HGiBgZjCmmv5MCr7jsN6lKOaCxYU="; - }; - enableParallelBuilding = true; - }); - - openssl = pkgs.openssl_1_1; - -in pkgs.stdenv.mkDerivation rec { - pname = "zit"; - version = "0.2"; - - src = ./.; - - # Not strictly in sync with vcpkg.json, if that turns out to be a problem it can be synced then - buildInputs = - [ asio pkgs.cmake pkgs.gtest pkgs.ninja openssl.dev openssl.bin pkgs.spdlog pkgs.tbb pkgs.pkg-config ]; - - # Adding FindXXX.cmake for packages not providing CMake support - configurePhase = '' - echo ' - find_path(ASIO_INCLUDE_DIR NAMES asio.hpp PATHS ${asio}/include REQUIRED NO_DEFAULT_PATH) - message(STATUS "Found asio include: ''${ASIO_INCLUDE_DIR}") - set(ASIO_FOUND TRUE) - set(ASIO_INCLUDE_DIRS ''${ASIO_INCLUDE_DIR}) - add_library(asio::asio INTERFACE IMPORTED GLOBAL) - mark_as_advanced(ASIO_INCLUDE_DIR)' > Findasio.cmake - - echo ' - find_path(TBB_INCLUDE_DIR NAMES tbb PATHS ${pkgs.tbb}/include REQUIRED NO_DEFAULT_PATH) - message(STATUS "Found tbb include: ''${TBB_INCLUDE_DIR}") - find_library(TBB_LIBRARY NAMES tbb PATHS ${pkgs.tbb}/lib REQUIRED NO_DEFAULT_PATH) - message(STATUS "Found tbb lib: ''${TBB_LIBRARY}") - set(TBB_FOUND TRUE) - set(TBB_INCLUDE_DIRS ''${TBB_INCLUDE_DIR}) - set(TBB_LIBRARIES TBB::tbb) - add_library(TBB::tbb SHARED IMPORTED GLOBAL) - set_target_properties(TBB::tbb PROPERTIES - IMPORTED_LOCATION "''${TBB_LIBRARY}" - INTERFACE_INCLUDE_DIRECTORIES "''${TBB_INCLUDE_DIR}" - ) - mark_as_advanced(TBB_INCLUDE_DIR) - mark_as_advanced(TBB_LIBRARIES)' > FindTBB.cmake - - rm -vf CMakeCache.txt - cmake . -GNinja -DCMAKE_BUILD_TYPE=Release -DUSE_CLANG_TIDY=NO -DCMAKE_MODULE_PATH="$PWD" -DCMAKE_INSTALL_PREFIX="$out" - ''; - - buildPhase = '' - ninja - ''; - - installPhase = '' - ninja install - ''; -} diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 824cde3..3e53e27 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,4 +1,5 @@ option(USE_CLANG_TIDY "Use clang-tidy if available" ON) +option(USE_CLANG_TIDY_CACHE "Use clang-tidy-cache if available" ON) option(ASIO_DEBUG "Add asio debug output" OFF) option(DISABLE_LTO "Turn off lto" OFF) @@ -10,16 +11,18 @@ option(DISABLE_LTO "Turn off lto" OFF) # the compile time went from 16s to 48s. # if(USE_CLANG_TIDY) - find_program(CLANG_TIDY NAMES clang-tidy-18) + find_program(CLANG_TIDY NAMES clang-tidy-22) if(CLANG_TIDY) find_program(CLANG_TIDY_CACHE NAMES "clang-tidy-cache") - if(CLANG_TIDY_CACHE) - message(STATUS "Found clang-tidy-cache: ${CLANG_TIDY_CACHE}") - set(CLANG_TIDY_CACHE_PREFIX "${CLANG_TIDY_CACHE};") - else() - message(STATUS "No clang-tidy-cache found") + if(USE_CLANG_TIDY_CACHE) + if(CLANG_TIDY_CACHE) + message(STATUS "Found clang-tidy-cache: ${CLANG_TIDY_CACHE}") + set(CLANG_TIDY_CACHE_PREFIX "${CLANG_TIDY_CACHE};") + else() + message(STATUS "No clang-tidy-cache found") + endif() endif() message(STATUS "Enabling clang-tidy: ${CLANG_TIDY}") @@ -120,4 +123,5 @@ if(NOT WIN32) endif() endif() -add_subdirectory(socket_test) \ No newline at end of file +add_subdirectory(socket_test) +add_subdirectory(torrent_writer) diff --git a/src/arg_parser.cpp b/src/arg_parser.cpp index a334a6a..5cd2cd6 100644 --- a/src/arg_parser.cpp +++ b/src/arg_parser.cpp @@ -85,7 +85,7 @@ void ArgParser::parse(const std::vector& argv) { unsigned positional{0}; for (size_t i = 1; i < argv.size(); i++) { // NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-pointer-arithmetic) - const std::string& name = argv[i]; + const std::string& name = argv.at(i); auto m = find(name); if (m == m_options.end()) { // No named argument - check if we have positional @@ -113,7 +113,7 @@ void ArgParser::parse(const std::vector& argv) { throw runtime_error(fmt::format("Missing value for {}", name)); } // NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-pointer-arithmetic) - val = argv[i++ + 1]; + val = argv.at(i++ + 1); } switch (arg->get_type()) { diff --git a/src/arg_parser.hpp b/src/arg_parser.hpp index eb0beb5..b56f36c 100644 --- a/src/arg_parser.hpp +++ b/src/arg_parser.hpp @@ -19,7 +19,7 @@ namespace zit { */ class ArgParser { public: - enum class Type { INT, UINT, FLOAT, STRING, BOOL }; + enum class Type : std::uint8_t { INT, UINT, FLOAT, STRING, BOOL }; private: struct BaseArg { @@ -27,7 +27,6 @@ class ArgParser { : m_option(std::move(option)), m_type(type) {} virtual ~BaseArg() = default; - virtual void* dst() = 0; [[nodiscard]] auto get_option() const { return m_option; } [[nodiscard]] const auto& get_aliases() const { return m_aliases; } @@ -45,15 +44,15 @@ class ArgParser { protected: std::string m_option; - std::string m_help{}; - std::set m_aliases{}; + std::string m_help; + std::set m_aliases; Type m_type; bool m_provided = false; bool m_required = false; bool m_help_arg = false; bool m_is_multi = false; bool m_is_collecting = false; - std::optional m_position{}; + std::optional m_position; }; template @@ -86,15 +85,14 @@ class ArgParser { } } - void* dst() override { return &m_dst; } - [[nodiscard]] auto dst() const { return m_dst.empty() ? m_default : m_dst; } + void set_dst(T t) { if (!m_is_multi && !m_dst.empty()) { throw std::runtime_error( "Multiple values provided for single value option: " + m_option); } - m_dst.emplace_back(t); + m_dst.emplace_back(std::move(t)); } /** Help text for option */ @@ -111,7 +109,7 @@ class ArgParser { /** Default value for option */ auto& default_value(T t) { - m_default = {t}; + m_default = {std::move(t)}; return *this; } @@ -184,7 +182,7 @@ class ArgParser { void verify_no_duplicate_collecting() const; std::string m_desc; - std::vector> m_options{}; + std::vector> m_options; bool m_parsed = false; template diff --git a/src/bencode.hpp b/src/bencode.hpp index bc6efc7..ac921e2 100644 --- a/src/bencode.hpp +++ b/src/bencode.hpp @@ -83,7 +83,7 @@ template template [[nodiscard]] std::string encode(const T& in) { - using src = typename ArrayToPointerDecay::type; + using src = ArrayToPointerDecay::type; return encode_internal(in); } @@ -122,18 +122,18 @@ class Element : public std::enable_shared_from_this { template [[nodiscard]] auto to() const { - static_assert(std::is_base_of::value, + static_assert(std::is_base_of_v, "Can only return sublasses of Element"); auto ptr = std::dynamic_pointer_cast(shared_from_this()); if (!ptr) { throw BencodeConversionError("Could not convert to type"); } - return ptr; + return std::move(ptr); } template [[nodiscard]] auto is() const { - static_assert(std::is_base_of::value, + static_assert(std::is_base_of_v, "Can only return sublasses of Element"); return std::dynamic_pointer_cast(shared_from_this()) != nullptr; } @@ -141,7 +141,7 @@ class Element : public std::enable_shared_from_this { virtual std::ostream& print(std::ostream& os) = 0; /** - * First try used T&& val, but it caoused storage of int references when we + * First try used T&& val, but it caused storage of int references when we * want such values copied. See https://stackoverflow.com/q/17316386/11722 and * https://isocpp.org/blog/2012/11/universal-references-in-c11-scott-meyers * for details. @@ -169,7 +169,9 @@ class TypedElement : public Element { explicit TypedElement(std::add_rvalue_reference_t data) : m_data(std::forward(data)) {} - [[nodiscard]] std::string encode() const override { return bencode::encode(m_data); } + [[nodiscard]] std::string encode() const override { + return bencode::encode(m_data); + } [[nodiscard]] auto val() const { return m_data; } diff --git a/src/bitfield.cpp b/src/bitfield.cpp index 495e66b..ffa73f0 100644 --- a/src/bitfield.cpp +++ b/src/bitfield.cpp @@ -2,6 +2,8 @@ #include "bitfield.hpp" #include "types.hpp" +#include + #include #include #include @@ -11,8 +13,12 @@ #include #include #include +#include #include +// Using operator[] on purpose +// NOLINTBEGIN(cppcoreguidelines-pro-bounds-avoid-unchecked-container-access) + namespace zit { namespace { @@ -59,7 +65,7 @@ Bitfield::Proxy::operator bool() const { Bitfield::Bitfield(bytes::size_type count) { // Number of bytes that will fit given number of bits // i.e. ceil(count/8) - m_bytes.resize(count / 8 + (count % 8 != 0)); + m_bytes.resize((count / 8) + (count % 8 != 0)); } bool Bitfield::get(bytes::size_type i) const { @@ -75,6 +81,14 @@ Bitfield::Proxy Bitfield::operator[](bytes::size_type i) { return {*this, i}; } +bool Bitfield::at(bytes::size_type i) const { + return get(i); +} + +Bitfield::Proxy Bitfield::at(bytes::size_type i) { + return {*this, i}; +} + void Bitfield::fill(bytes::size_type count, bool val, bytes::size_type start) { std::cerr << fmt::format("Fill: {} {} {} {}", count, val, start, m_bytes.size()) @@ -184,7 +198,7 @@ std::optional Bitfield::next(bool val, // Check first partial byte if we have a bit offset if (bit_offset) { for (unsigned short i = bit_offset; i < 8; ++i) { - const auto pos = numeric_cast(byte_offset * 8 + i); + const auto pos = numeric_cast((byte_offset * 8) + i); if (operator[](pos) == val) { return pos; } @@ -239,3 +253,5 @@ std::string format_as(const Bitfield& bf) { } } // namespace zit + +// NOLINTEND(cppcoreguidelines-pro-bounds-avoid-unchecked-container-access) diff --git a/src/bitfield.hpp b/src/bitfield.hpp index 8c8835f..e459b70 100644 --- a/src/bitfield.hpp +++ b/src/bitfield.hpp @@ -82,6 +82,16 @@ class Bitfield { */ Proxy operator[](bytes::size_type i); + /** + * Get bit value. Alias for operator[] - both throws. + */ + [[nodiscard]] bool at(bytes::size_type i) const; + + /** + * Get bit value. Alias for operator[] - both throws. + */ + Proxy at(bytes::size_type i); + /** * Fill the first n bits with 0 or 1. * @@ -129,7 +139,7 @@ class Bitfield { [[nodiscard]] const bytes& data() const { return m_bytes; } private: - bytes m_bytes{}; + bytes m_bytes; }; std::ostream& operator<<(std::ostream& os, const Bitfield& bf); diff --git a/src/file_utils.hpp b/src/file_utils.hpp index f775b2f..bd12387 100644 --- a/src/file_utils.hpp +++ b/src/file_utils.hpp @@ -51,7 +51,7 @@ inline void write_file(const std::filesystem::path& file_name, } // namespace zit -// NOLINTNEXTLINE(cert-dcl58-cpp) +// NOLINTNEXTLINE(cert-dcl58-cpp,bugprone-std-namespace-modification) namespace std::filesystem { inline auto format_as(const path& p) { return p.string(); diff --git a/src/file_writer.cpp b/src/file_writer.cpp index a897d58..045ea45 100644 --- a/src/file_writer.cpp +++ b/src/file_writer.cpp @@ -30,7 +30,7 @@ using namespace std; namespace fs = filesystem; void FileWriter::add(Torrent* torrent, const shared_ptr& piece) { - const lock_guard lock(m_queue_mutex); + const scoped_lock lock(m_queue_mutex); logger()->debug("Piece {} added to queue", piece->id()); m_queue.emplace(torrent, piece); m_condition.notify_one(); @@ -45,7 +45,6 @@ void FileWriter::run() { logger()->info("FileWriter done"); } -// FIXME: Support for multi torrents bytes FileWriter::read_block(uint32_t offset, uint32_t length, const Torrent& torrent) { @@ -270,7 +269,7 @@ void FileWriter::write_next_piece() { auto [torrent, piece] = t_piece; try { auto sha = Sha1::calculateData(piece->data()); - if (sha != torrent->pieces()[piece->id()]) { + if (sha != torrent->pieces().at(piece->id())) { throw runtime_error("Piece data does not match expected Sha1"); } diff --git a/src/file_writer.hpp b/src/file_writer.hpp index ef22bc1..19dbad2 100644 --- a/src/file_writer.hpp +++ b/src/file_writer.hpp @@ -93,12 +93,12 @@ class FileWriter { private: void write_next_piece(); - std::queue m_queue{}; - std::mutex m_queue_mutex{}; - std::mutex m_file_mutex{}; - std::condition_variable m_condition{}; + std::queue m_queue; + std::mutex m_queue_mutex; + std::mutex m_file_mutex; + std::condition_variable m_condition; std::atomic_bool m_stop = false; - TorrentWrittenCallback m_torrent_written_callback{}; + TorrentWrittenCallback m_torrent_written_callback; }; /** @@ -131,7 +131,7 @@ class FileWriterThread { } private: - std::shared_ptr m_logger{}; + std::shared_ptr m_logger; FileWriter& m_file_writer; std::thread m_file_writer_thread; }; diff --git a/src/global_config.cpp b/src/global_config.cpp index 755458c..f9e0f38 100644 --- a/src/global_config.cpp +++ b/src/global_config.cpp @@ -119,6 +119,9 @@ std::optional parse_bool(const std::string& _str) { template const std::map settings_map; +// Should be fine - possibly try making them constexpr +// NOLINTBEGIN(bugprone-throwing-static-initialization) + template <> const std::map settings_map{ {"initiate_peer_connections", BoolSetting::INITIATE_PEER_CONNECTIONS}, @@ -137,6 +140,8 @@ template <> const std::map settings_map{ {"bind_address", StringSetting::BIND_ADDRESS}}; +// NOLINTEND(bugprone-throwing-static-initialization) + } // namespace std::ostream& operator<<(std::ostream& os, const Config& config) { @@ -170,9 +175,9 @@ bool FileConfig::try_file(const std::filesystem::path& config_file) { if (kv.size() != 2) { logger()->warn("Ignoring invalid config line: {}", line); } else { - trim(kv[0]); - trim(kv[1]); - update_value(kv[0], kv[1]); + trim(kv.at(0)); + trim(kv.at(1)); + update_value(kv.at(0), kv.at(1)); } } return true; diff --git a/src/global_config.hpp b/src/global_config.hpp index d10192c..fd85f36 100644 --- a/src/global_config.hpp +++ b/src/global_config.hpp @@ -8,7 +8,7 @@ #include namespace spdlog { -class logger; +class logger; // NOLINT(readability-identifier-naming) } namespace zit { @@ -16,7 +16,7 @@ namespace zit { /** * Boolean settings. In the config file they can be read as true/false or 1/0 */ -enum class BoolSetting { +enum class BoolSetting : std::uint8_t { /** * If this is true, Zit will initiate connections to peers from the tracker * even if we are just seeding. This works around a problem where some @@ -38,7 +38,7 @@ enum class BoolSetting { * Settings taking an integer. An invalid integer for the setting is a runtime * error. */ -enum class IntSetting { +enum class IntSetting : std::uint8_t { /** * Port on which the Zit client is listening to incoming peer connections. */ @@ -60,7 +60,7 @@ enum class IntSetting { RETRY_PEERS_INTERVAL_SECONDS }; -enum class StringSetting { +enum class StringSetting : std::uint8_t { /** * IPv4 address to accept incoming connections on, by default 127.0.0.1 */ diff --git a/src/messages.cpp b/src/messages.cpp index 5deaaaa..ea01a4b 100644 --- a/src/messages.cpp +++ b/src/messages.cpp @@ -114,7 +114,7 @@ string debugMsg(span msg) { ss.fill('0'); ss << hex; for (const auto b : msg) { - ss << setw(2) << int(b) << " "; + ss << setw(2) << static_cast(b) << " "; } return ss.str(); } @@ -131,21 +131,22 @@ optional HandshakeMsg::parse(const bytes& msg) { 'r'_b, 'e'_b, 'n'_b, 't'_b, ' '_b, 'p'_b, 'r'_b, 'o'_b, 't'_b, 'o'_b, 'c'_b, 'o'_b, 'l'_b}; - const auto it = - std::search(msg.begin(), msg.end(), BT_START.begin(), BT_START.end()); - if (it == msg.end()) { - logger()->trace("No handshake match:\nGot: {}\nExp: {}", - debugMsg(span(msg.begin(), std::min(msg.size(), BT_START.size()))), - debugMsg(BT_START)); + const auto sr = std::ranges::search(msg, BT_START); + if (sr.empty()) { + logger()->trace( + "No handshake match:\nGot: {}\nExp: {}", + debugMsg(span(msg.begin(), std::min(msg.size(), BT_START.size()))), + debugMsg(BT_START)); return {}; } - if (it != msg.begin()) { - logger()->debug("Found BT start at {}", std::distance(msg.begin(), it)); - return HandshakeMsg::parse(bytes{it, msg.end()}); + if (sr.begin() != msg.begin()) { + logger()->debug("Found BT start at {}", + std::distance(msg.begin(), sr.begin())); + return HandshakeMsg::parse(bytes{sr.begin(), msg.end()}); } - const bytes reserved(&msg[20], &msg[28]); + const bytes reserved(&msg.at(20), &msg.at(28)); const Sha1 info_hash = Sha1::fromBuffer(msg, 28); const string peer_id = from_bytes(msg, 48, 68); @@ -155,10 +156,10 @@ optional HandshakeMsg::parse(const bytes& msg) { logger()->error("Invalid handshake length: {}", msg.size()); return {}; } - if (to_peer_wire_id(msg[72]) != peer_wire_id::BITFIELD) { + if (to_peer_wire_id(msg.at(72)) != peer_wire_id::BITFIELD) { logger()->error("Expected bitfield id ({}) but got: {}", static_cast(peer_wire_id::BITFIELD), - static_cast(msg[72])); + static_cast(msg.at(72))); return {}; } // 4-byte big endian @@ -222,7 +223,7 @@ size_t Message::parse(PeerConnection& connection) { return 4; } if (m_msg.size() >= 5) { - auto id = to_peer_wire_id(m_msg[4]); + auto id = to_peer_wire_id(m_msg.at(4)); logger()->debug("{}: Received: {}", peer.str(), id); switch (id) { case peer_wire_id::CHOKE: diff --git a/src/net.cpp b/src/net.cpp index 00bfdc3..42a039b 100644 --- a/src/net.cpp +++ b/src/net.cpp @@ -5,10 +5,11 @@ #include #include #include +#include #include #include #include -#include +#include // NOLINT(misc-include-cleaner) #include #include #include @@ -16,6 +17,7 @@ #include #include #include +#include #include #include #include @@ -73,11 +75,11 @@ vector get_host_ip_addresses() { vector ip_addresses; asio::io_context io_context; asio::ip::tcp::resolver resolver(io_context); - asio::ip::tcp::resolver::query query(asio::ip::host_name(), ""); + const asio::ip::tcp::resolver::query query(asio::ip::host_name(), ""); asio::ip::tcp::resolver::iterator it = resolver.resolve(query); while (it != asio::ip::tcp::resolver::iterator()) { - asio::ip::tcp::endpoint endpoint = *it++; + const asio::ip::tcp::endpoint endpoint = *it++; ip_addresses.push_back(endpoint.address().to_string()); } @@ -167,7 +169,7 @@ std::tuple request(Sock& sock, const Url& url) { response_stream >> status_code; std::string status_message; std::getline(response_stream, status_message); - if (!response_stream || http_version.substr(0, 5) != "HTTP/") { + if (!response_stream || !http_version.starts_with("HTTP/")) { throw runtime_error("invalid response"); } @@ -321,13 +323,16 @@ void addWindowsCertificates(const SSL_CTX* ctx) { // Based on example at // https://www.boost.org/doc/libs/1_73_0/doc/html/boost_asio/overview/ssl.html -std::tuple httpsGet(const Url& url, - const Net::BindAddress& bind_address) { +std::tuple httpsGet( + const Url& url, + const Net::BindAddress& bind_address) { using ssl_socket = ssl::stream; // Create a context that uses the default paths for // finding CA certificates. ssl::context ctx(ssl::context::tlsv12); + // Both critical section and call is inside asio - so nothing we can control + // NOLINTNEXTLINE(clang-analyzer-unix.BlockInCriticalSection) if (!bind_address.get().empty() && bind_address.get() != "127.0.0.1") { throw std::runtime_error("bind for ssl not yet supported"); } @@ -339,15 +344,20 @@ std::tuple httpsGet(const Url& url, ctx.set_default_verify_paths(); // In my case the default path is not aligned with the systems on Ubuntu // (/usr/local/ssl/certs). Thus adding the standard linux directory in - // addition. This is likely due OpenSSL being built by Conan with that path. - // Note: With a newer conan openssl build this was no longer necessary + // addition. This is likely due to OpenSSL being built by Conan with that + // path. Note: With a newer conan openssl build this was no longer necessary // ctx.add_verify_path("/etc/ssl/certs"); // Open a socket and connect it to the remote host. asio::io_context io_context; + + // Both critical section and call is inside asio - so nothing we can control + // NOLINTNEXTLINE(clang-analyzer-unix.BlockInCriticalSection) ssl_socket sock(io_context, ctx); + tcp::resolver resolver(io_context); const tcp::resolver::query query(url.host(), url.service()); + // NOLINTNEXTLINE(misc-include-cleaner) asio::connect(sock.lowest_layer(), resolver.resolve(query)); sock.lowest_layer().set_option(tcp::no_delay(true)); @@ -419,8 +429,8 @@ std::tuple Net::httpGet( try { if (!bind_address.get().empty()) { socket.open(asio::ip::tcp::v4()); - socket.bind( - asio::ip::tcp::endpoint(asio::ip::make_address(bind_address.get()), 0)); + socket.bind(asio::ip::tcp::endpoint( + asio::ip::make_address(bind_address.get()), 0)); logger()->debug("Http request from {}:{}", socket.local_endpoint().address().to_string(), socket.local_endpoint().port()); @@ -430,6 +440,7 @@ std::tuple Net::httpGet( fmt::format("Could not bind to address: '{}'", bind_address.get()))); } asio::error_code error = asio::error::host_not_found; + // NOLINTNEXTLINE(misc-include-cleaner) asio::connect(socket, endpoints, error); if (error) { throw asio::system_error(error, url.str()); @@ -517,7 +528,7 @@ string Net::urlEncode(const string& value) { // Any other characters are percent-encoded escaped << uppercase; - escaped << '%' << setw(2) << int(static_cast(C)); + escaped << '%' << setw(2) << static_cast(static_cast(C)); escaped << nouppercase; } @@ -548,11 +559,14 @@ Url::Url(const string& url, Binary binary, Resolve resolve) { to_string(url.length())); } stringstream ss; - ss << to_string(uint8_t(url[0])) << "." << to_string(uint8_t(url[1])) << "." - << to_string(uint8_t(url[2])) << "." << to_string(uint8_t(url[3])); + ss << to_string(static_cast(url.at(0))) << "." + << to_string(static_cast(url.at(1))) << "." + << to_string(static_cast(url.at(2))) << "." + << to_string(static_cast(url.at(3))); m_host = ss.str(); - m_port = host_to_network_short(static_cast( - static_cast(url[4]) << 0 | static_cast(url[5]) << 8)); + m_port = host_to_network_short( + static_cast(static_cast(url.at(4)) << 0 | + static_cast(url.at(5)) << 8)); m_scheme = "http"; } diff --git a/src/net.hpp b/src/net.hpp index b5f1a92..8c2edc1 100644 --- a/src/net.hpp +++ b/src/net.hpp @@ -127,11 +127,11 @@ class Url { } private: - std::string m_scheme{}; - std::string m_host{}; - std::string m_path{}; - string_list m_params{}; - std::optional m_port{}; + std::string m_scheme; + std::string m_host; + std::string m_path; + string_list m_params; + std::optional m_port; }; inline std::ostream& operator<<(std::ostream& os, const zit::Url& url) { diff --git a/src/peer.cpp b/src/peer.cpp index 16ab500..2557f0f 100644 --- a/src/peer.cpp +++ b/src/peer.cpp @@ -1,9 +1,10 @@ // -*- mode:c++; c-basic-offset : 2; -*- #include "peer.hpp" +#include #include #include -#include +#include // NOLINT(misc-include-cleaner) #include #include #include @@ -11,11 +12,11 @@ #include #include #include -#include +#include // NOLINT(misc-include-cleaner) #include #include #include -#include +#include // NOLINT(misc-include-cleaner) #ifndef _MSC_VER #include @@ -70,8 +71,7 @@ PeerConnection::PeerConnection(IConnectionUrlProvider& peer, resolver_(io_service), socket_(socket ? std::move(socket) : make_unique(io_service, tcp::v4())), - m_connection_port(connection_port), - m_io_service(io_service) { + m_connection_port(connection_port) { // Note use of socket constructor that does not bind such // that we can set the options before that. } @@ -96,6 +96,20 @@ void PeerConnection::write(const optional& url, const std::string& msg) { } m_msg = msg; + // If socket is already open and connected (accepted incoming connection), + // use it directly without attempting to resolve/connect + if (socket_ && socket_->is_open() && !m_connected) { + // Check if socket is actually connected (has remote endpoint) + asio::error_code ec; + socket_->remote_endpoint(ec); + if (!ec) { + // Socket is connected - this is an accepted incoming connection + m_connected = true; + send(true); + return; + } + } + if (m_connected || endpoint_ != tcp::resolver::iterator()) { // We have already resolved and connected handle_connect(asio::error_code(), endpoint_); @@ -146,9 +160,10 @@ void PeerConnection::send(bool start_read) { m_sending = true; const string msg(m_msg); m_msg.clear(); + // NOLINTNEXTLINE(misc-include-cleaner) asio::async_write( *socket_, asio::buffer(msg.c_str(), msg.size()), - [this, start_read](auto err, auto len) { + [this, start_read, msg](auto err, auto len) { if (!err) { logger()->debug("{}: Data of len {} sent", peer_.str(), len); } else { @@ -223,6 +238,7 @@ void PeerConnection::handle_response(const asio::error_code& err, std::size_t) { // TODO: https://sourceforge.net/p/asio/mailman/message/23968189/ // mentions that maybe socket.async_read_some is better to read > 512 // bytes at a time + // NOLINTNEXTLINE(misc-include-cleaner) asio::async_read( *socket_, response_, asio::transfer_at_least(1), [this](const auto& ec, auto s) { handle_response(ec, s); }); @@ -249,8 +265,16 @@ PeerAcceptor::PeerAcceptor(ListeningPort port, m_io_context(io_context), m_bind_address(std::move(bind_address)) { logger()->trace(PRETTY_FUNCTION); + // Do not start the coroutine here: it must be started after the object is + // fully emplaced into the container that will own its lifetime. Call + // `start()` to begin the accept loop once the object is stored. +} + +void PeerAcceptor::start() { // For now rethrowing - can consider asio::detached later? - co_spawn(m_io_context, listen(), [](auto e) { std::rethrow_exception(e); }); + // NOLINTNEXTLINE(misc-include-cleaner) + co_spawn(m_io_context, listen(), + [](auto e) { std::rethrow_exception(std::move(e)); }); } asio::awaitable PeerAcceptor::listen() { @@ -279,6 +303,7 @@ asio::awaitable PeerAcceptor::listen() { // Read data from socket bytes buffer; + // NOLINTNEXTLINE(misc-include-cleaner) co_await asio::async_read(socket, asio::dynamic_buffer(buffer), asio::transfer_at_least(MIN_BT_MSG_LENGTH), asio::use_awaitable); @@ -303,8 +328,14 @@ asio::awaitable PeerAcceptor::listen() { return port; //} }(); - torrent->add_peer(make_shared( - Url{fmt::format("http://{}:{}", ip, accept_port)}, *torrent)); + auto peer = make_shared( + std::make_unique(std::move(socket)), + Url{fmt::format("http://{}:{}", ip, accept_port)}, *torrent); + if (torrent->add_peer(peer)) { + // Successfully added - send our bitfield + // Remote will send INTERESTED if they want our pieces + peer->report_bitfield(); + } } catch (const std::system_error& error) { logger()->error("Listen errored: {}", error.what()); } @@ -410,7 +441,7 @@ std::size_t Peer::request_next_block(unsigned short count) { return requests; } bytes req; - for (int i = 0; i < count; i++) { + for (unsigned short i = 0; i < count; i++) { // We can now start requesting pieces auto has_piece = next_piece(true); if (!has_piece) { @@ -483,7 +514,7 @@ void Peer::set_interested(bool interested) { } if (m_interested && !interested) { logger()->info("Peer is Not interested"); - // TODO: I guess we don't need to choke it since the peer told us + m_torrent.not_interested(this); } m_interested = interested; } @@ -504,7 +535,7 @@ void Peer::have(uint32_t id) { m_remote_pieces = Bitfield(number_of_pieces); m_remote_pieces.fill(number_of_pieces, true); } else { - m_remote_pieces[id] = true; + m_remote_pieces.at(id) = true; } request_next_block(); } diff --git a/src/peer.hpp b/src/peer.hpp index ee79f60..ac1510a 100644 --- a/src/peer.hpp +++ b/src/peer.hpp @@ -70,17 +70,16 @@ class PeerConnection { void send(bool start_read = false); - std::string m_msg{}; + std::string m_msg; IConnectionUrlProvider& peer_; asio::ip::tcp::resolver resolver_; - asio::streambuf response_{}; + asio::streambuf response_; socket_ptr socket_; - asio::ip::tcp::resolver::iterator endpoint_{}; - std::deque m_send_queue{}; + asio::ip::tcp::resolver::iterator endpoint_; + std::deque m_send_queue; bool m_connected = false; bool m_sending = false; ConnectionPort m_connection_port; - asio::io_service& m_io_service; }; class PeerAcceptor { @@ -89,13 +88,19 @@ class PeerAcceptor { asio::io_context& io_context, std::string bind_address); + void start(); + static void acceptOnPort(asio::io_context& io_context, ListeningPort port, const std::string& bind_address) { // FIXME: Add check that already existing port use the same io_context/bind // FIXME: Since we are using the io_context of the torrent, it should also // be removed/replaced when the torrent is stopped. - m_acceptors.try_emplace(port, port, io_context, bind_address); + auto [it, inserted] = + m_acceptors.try_emplace(port, port, io_context, bind_address); + if (inserted) { + it->second.start(); + } } private: @@ -115,11 +120,25 @@ class PeerAcceptor { */ class Peer : public IConnectionUrlProvider { public: - explicit Peer(Url url, Torrent& torrent) + /** + * Construct a new Peer object + */ + Peer(Url url, Torrent& torrent) : m_url(std::move(url)), m_torrent(torrent), m_last_activity(std::chrono::system_clock::now()) {} + /** + * Constructor for incoming connections with an already-accepted socket + */ + Peer(socket_ptr socket, Url url, Torrent& torrent) + : m_url(std::move(url)), + m_listening(true), + m_torrent(torrent), + m_last_activity(std::chrono::system_clock::now()) { + init_io_service(std::move(socket)); + } + /** * Return the url of this peer. */ @@ -241,7 +260,7 @@ class Peer : public IConnectionUrlProvider { [[nodiscard]] bool is_inactive() const; private: - std::optional m_url{}; + std::optional m_url; bool m_am_choking = true; bool m_am_interested = false; bool m_choking = true; @@ -250,15 +269,15 @@ class Peer : public IConnectionUrlProvider { void init_io_service(socket_ptr socket = nullptr); - Bitfield m_remote_pieces{}; + Bitfield m_remote_pieces; // order important - connection need to be destroyed first - std::unique_ptr m_io_service{}; - std::unique_ptr m_connection{}; + std::unique_ptr m_io_service; + std::unique_ptr m_connection; Torrent& m_torrent; - std::unique_ptr m_work{}; - std::chrono::system_clock::time_point m_last_activity{}; + std::unique_ptr m_work; + std::chrono::system_clock::time_point m_last_activity; }; inline std::ostream& operator<<(std::ostream& os, const zit::Peer& peer) { diff --git a/src/piece.cpp b/src/piece.cpp index 2056b2e..2a38aef 100644 --- a/src/piece.cpp +++ b/src/piece.cpp @@ -26,7 +26,7 @@ using namespace std; namespace zit { optional Piece::next_offset(bool mark) { - const lock_guard lock(m_mutex); + const scoped_lock lock(m_mutex); const auto req_or_done = m_blocks_requested + m_blocks_done; auto next = req_or_done.next(false); if (!next) { @@ -40,14 +40,14 @@ optional Piece::next_offset(bool mark) { } // Mark block as requested if (mark) { - m_blocks_requested[*next] = true; + m_blocks_requested.at(*next) = true; m_last_request = std::chrono::system_clock::now(); } return numeric_cast(*next) * m_block_size; } bytes Piece::data() const { - const lock_guard lock(m_mutex); + const scoped_lock lock(m_mutex); if (m_data.empty()) { logger()->warn("Retrieved empty data from piece {}", m_id); } @@ -66,15 +66,15 @@ bool Piece::set_block(uint32_t offset, bytes_span data) { } auto block_id = offset / m_block_size; - const lock_guard lock(m_mutex); - if (m_blocks_done[block_id]) { + const scoped_lock lock(m_mutex); + if (m_blocks_done.at(block_id)) { logger()->warn("Already got block {} for piece {}", block_id, m_id); } else { - if (!m_blocks_requested[block_id]) { + if (!m_blocks_requested.at(block_id)) { logger()->warn("Got data for non requested block?"); } ranges::copy(data, m_data.begin() + offset); - m_blocks_done[block_id] = true; + m_blocks_done.at(block_id) = true; logger()->debug("Block {}/{} of size {} stored for piece {}", block_id + 1, block_count(), data.size(), m_id); } @@ -94,8 +94,8 @@ bytes Piece::get_block(uint32_t offset, } auto block_id = offset / m_block_size; - const lock_guard lock(m_mutex); - if (!m_blocks_done[block_id]) { + const scoped_lock lock(m_mutex); + if (!m_blocks_done.at(block_id)) { logger()->warn("Block {} in piece {} not done", block_id, m_id); return {}; } @@ -111,15 +111,15 @@ bytes Piece::get_block(uint32_t offset, logger()->debug("Returning block {} in piece {} from disk", block_id, m_id); // Important to use the piece_length from the torrent, since the last // piece is often shorter and can't thus be used to get the offset. - const auto file_offset = torrent.piece_length() * m_id + offset; + const auto file_offset = (torrent.piece_length() * m_id) + offset; return FileWriter::getInstance().read_block(file_offset, length, torrent); } void Piece::set_piece_written(bool written) { m_piece_written = written; - const lock_guard lock(m_mutex); + const scoped_lock lock(m_mutex); for (uint32_t i = 0; i < block_count(); ++i) { - m_blocks_done[i] = true; + m_blocks_done.at(i) = true; } m_data.clear(); m_data.shrink_to_fit(); @@ -142,7 +142,7 @@ std::size_t Piece::retry_blocks() { logger()->warn( "Piece {} inactive for {} seconds. Marking for retry.", m_id, std::chrono::duration_cast(inactive).count()); - const lock_guard lock(m_mutex); + const scoped_lock lock(m_mutex); const auto retry = (m_blocks_requested - m_blocks_done).count(); // TODO: Should we really request the whole piece again? diff --git a/src/piece.hpp b/src/piece.hpp index 6d9b730..5019dd0 100644 --- a/src/piece.hpp +++ b/src/piece.hpp @@ -90,11 +90,11 @@ class Piece { Bitfield m_blocks_requested; Bitfield m_blocks_done; const uint32_t m_id; - mutable std::mutex m_mutex{}; - bytes m_data{}; + mutable std::mutex m_mutex; + bytes m_data; std::atomic_bool m_piece_written = false; - std::chrono::system_clock::time_point m_last_request{}; - std::chrono::system_clock::time_point m_last_block{}; + std::chrono::system_clock::time_point m_last_request; + std::chrono::system_clock::time_point m_last_block; }; } // namespace zit diff --git a/src/retry.hpp b/src/retry.hpp index 4441bd0..ac0bee3 100644 --- a/src/retry.hpp +++ b/src/retry.hpp @@ -38,7 +38,7 @@ inline auto retry_call( for (unsigned i = 0; i < retries; ++i) { last_call = Clock::now(); if (auto ret = callable(); ret || i == retries - 1) { - return ret; + return std::move(ret); } const auto time_spent = Clock::now() - last_call; if (i < (retries - 1)) { diff --git a/src/scope_guard.hpp b/src/scope_guard.hpp index bc9631c..15e2968 100644 --- a/src/scope_guard.hpp +++ b/src/scope_guard.hpp @@ -23,7 +23,7 @@ class ScopeGuard { } catch (...) { try { logger()->warn("Exception thrown from ScopeGuard"); - } catch (...) { + } catch (...) { // NOLINT(bugprone-empty-catch) } } } diff --git a/src/sha1.cpp b/src/sha1.cpp index 17c40a3..c956101 100644 --- a/src/sha1.cpp +++ b/src/sha1.cpp @@ -142,12 +142,12 @@ Sha1 Sha1::calculateFile(const std::filesystem::path& file) { } template -Sha1 Sha1::fromBuffer(const T& buffer, typename T::size_type offset) { +Sha1 Sha1::fromBuffer(const T& buffer, T::size_type offset) { if (offset + SHA_LENGTH > buffer.size()) { throw invalid_argument("Buffer too small for extracting sha1"); } Sha1 ret; - copy_n(reinterpret_cast(&buffer[offset]), SHA_LENGTH, + copy_n(reinterpret_cast(&buffer.at(offset)), SHA_LENGTH, ret.data()); return ret; } diff --git a/src/sha1.hpp b/src/sha1.hpp index 03532b6..69a34f8 100644 --- a/src/sha1.hpp +++ b/src/sha1.hpp @@ -45,7 +45,7 @@ class Sha1 : public std::array { * Implemented for zit::bytes and std::strings. */ template - static Sha1 fromBuffer(const T& buffer, typename T::size_type offset); + static Sha1 fromBuffer(const T& buffer, T::size_type offset); private: [[nodiscard]] static Sha1 calculate(const unsigned char* src, size_t count); diff --git a/src/socket_test/client.cpp b/src/socket_test/client.cpp index 9a8c137..edc7837 100644 --- a/src/socket_test/client.cpp +++ b/src/socket_test/client.cpp @@ -4,7 +4,7 @@ #include #include #include -#include +#include // NOLINT(misc-include-cleaner) #include #include #include @@ -16,8 +16,8 @@ #include #include #include -#include "asio_helpers.hpp" -#include "common.hpp" // NOLINT(misc-include-cleaner) +#include "asio_helpers.hpp" // NOLINT(misc-include-cleaner) +#include "common.hpp" // NOLINT(misc-include-cleaner) #include "logger.hpp" class Connection : public socket_test::ID { @@ -79,6 +79,7 @@ int main(int argc, char* argv[]) { // Note: asio::detached is often used in examples but it will ignore any // exceptions thrown by the coroutine. Using my own completion // handler here instead that just rethrows the exception instead. + // NOLINTNEXTLINE(misc-include-cleaner) co_spawn(io_context, connections.back()->connect(), socket_test::rethrow); } io_context.run(); diff --git a/src/socket_test/server.cpp b/src/socket_test/server.cpp index 7a64f1c..ba9ec90 100644 --- a/src/socket_test/server.cpp +++ b/src/socket_test/server.cpp @@ -2,10 +2,10 @@ // #define ASIO_ENABLE_HANDLER_TRACKING #include -#include +#include // NOLINT(misc-include-cleaner) #include #include -#include +#include // NOLINT(misc-include-cleaner) #include #include #include @@ -17,8 +17,8 @@ #include #include #include -#include "asio_helpers.hpp" -#include "common.hpp" // NOLINT(misc-include-cleaner) +#include "asio_helpers.hpp" // NOLINT(misc-include-cleaner) +#include "common.hpp" // NOLINT(misc-include-cleaner) #include "logger.hpp" class Connection : public socket_test::ID { @@ -44,6 +44,7 @@ class Connection : public socket_test::ID { // Read incoming data asio::streambuf buffer; + // NOLINTNEXTLINE(misc-include-cleaner) co_await asio::async_read_until(m_socket, buffer, '\n', asio::use_awaitable); @@ -93,6 +94,7 @@ int main(int argc, char* argv[]) { // Note: asio::detached is often used in examples but it will ignore any // exceptions thrown by the coroutine. Using my own completion // handler here instead that just rethrows the exception instead. + // NOLINTNEXTLINE(misc-include-cleaner) co_spawn(io_context, connections.back()->listen(), socket_test::rethrow); } io_context.run(); diff --git a/src/string_utils.hpp b/src/string_utils.hpp index 4c5438b..afc8df0 100644 --- a/src/string_utils.hpp +++ b/src/string_utils.hpp @@ -3,6 +3,7 @@ #include #include +#include #include #include #include @@ -39,9 +40,12 @@ inline std::string from_bytes(const bytes& buffer, } return ss.str(); #else + // Bounds checked above + // NOLINTBEGIN(cppcoreguidelines-pro-bounds-avoid-unchecked-container-access) return { reinterpret_cast(&buffer[start]), reinterpret_cast(&buffer[end == 0 ? buffer.size() : end])}; + // NOLINTEND(cppcoreguidelines-pro-bounds-avoid-unchecked-container-access) #endif // WIN32 } @@ -77,17 +81,18 @@ inline std::string to_hex(const bytes& data) { // trim from start (in place) inline void ltrim(std::string& s) { - s.erase(s.begin(), std::find_if(s.begin(), s.end(), [](unsigned char ch) { + s.erase(s.begin(), std::ranges::find_if(s, [](unsigned char ch) { return !std::isspace(ch); })); } // trim from end (in place) inline void rtrim(std::string& s) { - s.erase(std::find_if(s.rbegin(), s.rend(), - [](unsigned char ch) { return !std::isspace(ch); }) - .base(), - s.end()); + s.erase( + std::ranges::find_if(std::ranges::reverse_view(s), + [](unsigned char ch) { return !std::isspace(ch); }) + .base(), + s.end()); } // trim from both ends (in place) @@ -150,8 +155,8 @@ inline std::string bytesToHumanReadable(int64_t bytes) { if (abytes >= limit) { if (limit) { const auto w = abytes / limit; - const auto f = - static_cast(abytes - w * limit) / static_cast(limit); + const auto f = static_cast(abytes - (w * limit)) / + static_cast(limit); return fmt::format("{}{:.2f} {}", sign, static_cast(w) + f, unit); } diff --git a/src/torrent.cpp b/src/torrent.cpp index 7ec4ece..30fc190 100644 --- a/src/torrent.cpp +++ b/src/torrent.cpp @@ -28,6 +28,7 @@ #include #include #include +#include #include #include #include @@ -35,6 +36,7 @@ #include #include #include +#include #include #include #include @@ -61,7 +63,8 @@ using namespace std; namespace std { template -// NOLINTNEXTLINE(cert-dcl58-cpp) +// Various issues related to std extensions +// NOLINTNEXTLINE std::string format_as(const atomic& t) { return std::to_string(t.load()); } @@ -86,7 +89,7 @@ auto transform_all(const In& in, Out& out, Op func) { auto beDictToFileInfo(const Element& element) { const auto& dict = element.template to>()->val(); string md5; - if (dict.find("md5sum") != dict.end()) { + if (dict.contains("md5sum")) { md5 = dict.at("md5sum")->template to>()->val(); } const auto path_list = @@ -164,10 +167,10 @@ Torrent::Torrent(asio::io_context& io_context, info.at("piece length")->to>()->val()); // Either 'length' or 'files' is needed depending on mode - if (info.find("length") != info.end()) { + if (info.contains("length")) { m_length = info.at("length")->to>()->val(); } - if (info.find("files") != info.end()) { + if (info.contains("files")) { if (m_length != 0) { throw runtime_error("Invalid torrent: dual mode"); } @@ -204,14 +207,16 @@ Torrent::Torrent(asio::io_context& io_context, const auto& announce_list = root_dict.at("announce-list")->to>()->val(); - transform_all(announce_list, m_announce_list, [](const auto& tier) { - vector tier_output_list; - transform_all(tier->template to>()->val(), - tier_output_list, [](const auto& elm) { - return elm->template to>()->val(); - }); - return tier_output_list; - }); + transform_all( + announce_list, m_announce_list, [](const auto& tier) -> vector { + vector tier_output_list; + transform_all( + tier->template to>()->val(), + tier_output_list, [](const auto& elm) { + return elm->template to>()->val(); + }); + return tier_output_list; + }); } m_info_hash = Sha1::calculateData(encode(info)); @@ -220,7 +225,7 @@ Torrent::Torrent(asio::io_context& io_context, verify_existing_file(); { - scoped_lock lock(m_torrents_mutex); + const scoped_lock lock(m_torrents_mutex); if (m_torrents.contains(m_info_hash)) { throw runtime_error("Torrent already exists"); } @@ -230,7 +235,7 @@ Torrent::Torrent(asio::io_context& io_context, } Torrent::~Torrent() { - scoped_lock lock(m_torrents_mutex); + const scoped_lock lock(m_torrents_mutex); m_torrents.erase(m_info_hash); } @@ -260,8 +265,8 @@ void Torrent::verify_piece_single_file(uintmax_t file_length, if (sha1 == fsha1) { // Lock when updating num_pieces, inserting into // std::map is likely not thread safe. - const std::lock_guard lock(mutex); - m_client_pieces[id] = true; + const std::scoped_lock lock(mutex); + m_client_pieces.at(id) = true; m_active_pieces.emplace( id, make_shared(PieceId(id), PieceSize(piece_length(id)))); m_active_pieces[id]->set_piece_written(true); @@ -305,8 +310,8 @@ void Torrent::verify_piece_multi_file(std::atomic_uint32_t& num_pieces, if (sha1 == fsha1) { // Lock when updating num_pieces, inserting into std::map is // likely not thread safe. - const std::lock_guard lock(mutex); - m_client_pieces[id] = true; + const std::scoped_lock lock(mutex); + m_client_pieces.at(id) = true; m_active_pieces.emplace( id, make_shared(PieceId(id), PieceSize(piece_length(id)))); m_active_pieces[id]->set_piece_written(true); @@ -413,6 +418,12 @@ void Torrent::disconnected(Peer* peer) { } } +void Torrent::not_interested(Peer* peer) { + if (m_not_interested_callback) { + m_not_interested_callback(peer); + } +} + std::ostream& operator<<(std::ostream& os, const Torrent::TrackerEvent& te) { switch (te) { case Torrent::TrackerEvent::STARTED: @@ -471,8 +482,8 @@ Torrent::http_tracker_request(const Url& announce_url, TrackerEvent event) { logger()->info("HTTP Tracker request ({}): {}", event, url.str()); } - auto [headers, body] = - m_http_get(url, Net::BindAddress(m_config.get(StringSetting::BIND_ADDRESS))); + auto [headers, body] = m_http_get( + url, Net::BindAddress(m_config.get(StringSetting::BIND_ADDRESS))); std::vector> peers; @@ -493,13 +504,13 @@ Torrent::http_tracker_request(const Url& announce_url, TrackerEvent event) { logger()->debug("=====HEADER=====\n{}\n=====BODY=====\n{}", headers, reply); auto reply_dict = reply->to>()->val(); - if (reply_dict.find("failure reason") != reply_dict.end()) { + if (reply_dict.contains("failure reason")) { throw runtime_error( "Tracker request failed: " + reply_dict["failure reason"]->to>()->val()); } - if (reply_dict.find("peers") == reply_dict.end()) { + if (!reply_dict.contains("peers")) { throw runtime_error("Invalid tracker reply, no peer list"); } auto peers_dict = reply_dict["peers"]; @@ -663,10 +674,10 @@ class UDPTrackerRequest { logger()->debug("Parsing {} peers", peers_in_list); for (size_t i = 0; i < peers_in_list; ++i) { const auto ip = from_big_endian( - announce_response, peer_offset + i * size_of_peer); + announce_response, peer_offset + (i * size_of_peer)); const auto port = from_big_endian( announce_response, - peer_offset + i * size_of_peer + sizeof(int32_t)); + peer_offset + (i * size_of_peer) + sizeof(int32_t)); const auto ip_str = fmt::format("{}.{}.{}.{}", static_cast((ip >> 24) & 0xFF), @@ -688,7 +699,12 @@ class UDPTrackerRequest { } private: - enum class UdpAction { CONNECT = 0, ANNOUNCE = 1, SCRAPE = 2, ERR = 3 }; + enum class UdpAction : std::uint8_t { + CONNECT = 0, + ANNOUNCE = 1, + SCRAPE = 2, + ERR = 3 + }; /** Connect to UDP tracker */ bool connect() { @@ -785,7 +801,7 @@ class UDPTrackerRequest { Url m_announce_url; Torrent& m_torrent; - std::optional m_connection_id{}; + std::optional m_connection_id; TimePoint m_last_connection_id{ClockType::time_point::min()}; }; @@ -865,10 +881,11 @@ std::vector> Torrent::tracker_request( // To avoid connecting to our own listening peer const auto is_local_peer = [&](const auto& peer) { const auto& url = peer->url(); - static const auto local_ips = [] { + static const auto local_ips = [] -> string_list { auto resolved = get_host_ip_addresses(); resolved.emplace_back("localhost"); - static auto docker_ip{"172.17.0.1"}; + // FIXME: This should not be hardcoded + constexpr auto* docker_ip{"172.17.0.1"}; resolved.emplace_back(docker_ip); return resolved; }(); @@ -884,7 +901,7 @@ std::vector> Torrent::tracker_request( } void Torrent::start() { - scoped_lock lock(m_peers_mutex); + const scoped_lock lock(m_peers_mutex); if (!m_peers.empty()) { throw runtime_error("Local peer vector not empty"); } @@ -938,10 +955,15 @@ void Torrent::schedule_retry_peers() { void Torrent::run() { logger()->debug("Run loop start"); const auto is_stopped = [](auto& p) { return p->io_service().stopped(); }; - while (!m_stopped && (!done() || !ranges::all_of(m_peers, is_stopped))) { + // Keep running while not explicitly stopped AND either: + // - we're still downloading (!done()), OR + // - we have active peers, OR + // - the io_context has pending work (timers, acceptor, etc.) + while (!m_stopped && (!done() || !ranges::all_of(m_peers, is_stopped) || + !m_io_context.stopped())) { std::size_t ran = 0; { - scoped_lock lock(m_peers_mutex); + const scoped_lock lock(m_peers_mutex); for (auto& p : m_peers) { ran += p->io_service().poll_one(); } @@ -962,7 +984,7 @@ void Torrent::run() { } void Torrent::stop() { - scoped_lock lock(m_peers_mutex); + const scoped_lock lock(m_peers_mutex); for (auto& peer : m_peers) { peer->stop(); } @@ -971,7 +993,7 @@ void Torrent::stop() { } Torrent* Torrent::get(const Sha1& info_hash) { - scoped_lock lock(m_torrents_mutex); + const scoped_lock lock(m_torrents_mutex); if (auto match = m_torrents.find(info_hash); match != m_torrents.end()) { return match->second; } @@ -979,7 +1001,7 @@ Torrent* Torrent::get(const Sha1& info_hash) { } size_t Torrent::count() { - scoped_lock lock(m_torrents_mutex); + const scoped_lock lock(m_torrents_mutex); return m_torrents.size(); } @@ -1050,7 +1072,7 @@ void Torrent::retry_pieces() { // To hit different peers for each invocation - shuffle the list std::random_device rd; std::mt19937 g(rd()); - scoped_lock lock(m_peers_mutex); + const scoped_lock lock(m_peers_mutex); shuffle(m_peers.begin(), m_peers.end(), g); auto it = m_peers.begin(); @@ -1077,14 +1099,12 @@ bool Torrent::add_peer( shared_ptr peer, optional>>> peers) { const bool in_use = - std::find_if(m_peers.begin(), m_peers.end(), - [&peer](auto& existing_peer) { - const auto& lurl = peer->url(); - const auto& rurl = existing_peer->url(); - return lurl.has_value() && rurl.has_value() && - peer->url().value().str() == - existing_peer->url().value().str(); - }) != m_peers.end(); + std::ranges::find_if(m_peers, [&peer](auto& existing_peer) { + const auto& lurl = peer->url(); + const auto& rurl = existing_peer->url(); + return lurl.has_value() && rurl.has_value() && + peer->url().value().str() == existing_peer->url().value().str(); + }) != m_peers.end(); logger()->debug("Candidate {} in use: {}", peer->str(), in_use); if (!in_use) { peer->handshake(); @@ -1103,7 +1123,7 @@ void Torrent::retry_peers() { return; } - scoped_lock lock(m_peers_mutex); + const scoped_lock lock(m_peers_mutex); const ScopeGuard scope_guard([this]() { schedule_retry_peers(); }); logger()->debug("Checking peers for retry"); @@ -1142,9 +1162,9 @@ void Torrent::retry_peers() { // TODO: Yes, should group these in one type // NOLINTNEXTLINE(bugprone-easily-swappable-parameters) bool Torrent::set_block(uint32_t piece_id, uint32_t offset, bytes_span data) { - const std::lock_guard lock(m_mutex); + const std::scoped_lock lock(m_mutex); // Look up relevant piece object among active pieces - if (m_active_pieces.find(piece_id) != m_active_pieces.end()) { + if (m_active_pieces.contains(piece_id)) { auto piece = m_active_pieces[piece_id]; if (piece->set_block(offset, data)) { logger()->debug("Piece {} done!", piece_id); @@ -1157,7 +1177,7 @@ bool Torrent::set_block(uint32_t piece_id, uint32_t offset, bytes_span data) { } std::shared_ptr Torrent::active_piece(uint32_t id, bool create) { - const std::lock_guard lock(m_mutex); + const std::scoped_lock lock(m_mutex); auto piece = m_active_pieces.find(id); if (piece == m_active_pieces.end()) { if (!create) { @@ -1172,7 +1192,7 @@ std::shared_ptr Torrent::active_piece(uint32_t id, bool create) { } bool Torrent::done() const { - const std::lock_guard lock(m_mutex); + const std::scoped_lock lock(m_mutex); // If we haven't started on all pieces we are not done if (m_active_pieces.size() != m_pieces.size()) { @@ -1204,7 +1224,7 @@ void Torrent::last_piece_written() { logger()->info("{} completed. Notifying peers and tracker.", m_name); { - scoped_lock lock(m_peers_mutex); + const scoped_lock lock(m_peers_mutex); for (auto& peer : m_peers) { if (!peer->is_listening()) { peer->set_am_interested(false); @@ -1216,7 +1236,7 @@ void Torrent::last_piece_written() { } void Torrent::piece_done(std::shared_ptr& piece) { - m_client_pieces[piece->id()] = true; + m_client_pieces.at(piece->id()) = true; ranges::for_each(m_piece_callbacks, [&](const auto& cb) { cb(this, piece); }); } @@ -1244,13 +1264,13 @@ ostream& operator<<(ostream& os, const zit::Torrent& torrent) { if (!torrent.encoding().empty()) { os << "Encoding: " << torrent.encoding() << "\n"; } - os << "Piece length: " << torrent.piece_length() << "\n"; + os << "Piece count: " << torrent.pieces().size() << "\n"; + os << "Piece length: " << torrent.piece_length() << " bytes (" + << bytesToHumanReadable(torrent.piece_length()) << ")\n"; os << "Info hash: " << torrent.info_hash() << "\n"; os << "Private: " << (torrent.is_private() ? "Yes" : "No") << "\n"; + os << "Name: " << torrent.name() << "\n"; if (torrent.is_single_file()) { - os << "Name: " << torrent.name() << "\n"; - os << "Length: " << torrent.length() << " bytes (" - << bytesToHumanReadable(torrent.length()) << ")\n"; if (!torrent.md5sum().empty()) { os << "MD5Sum: " << torrent.md5sum() << "\n"; } @@ -1260,6 +1280,8 @@ ostream& operator<<(ostream& os, const zit::Torrent& torrent) { os << " " << fi << "\n"; } } + os << "Length: " << torrent.length() << " bytes (" + << bytesToHumanReadable(torrent.length()) << ")\n"; os << "Announce: " << torrent.announce() << "\n"; os << "Announce List:\n"; for (const auto& list : torrent.announce_list()) { diff --git a/src/torrent.hpp b/src/torrent.hpp index 34a2768..886e4c3 100644 --- a/src/torrent.hpp +++ b/src/torrent.hpp @@ -21,6 +21,7 @@ using PieceCallback = std::function&)>; using DisconnectCallback = std::function; +using NotInterestedCallback = std::function; /** * Function taking an Url + bind address and returning the resulting @@ -254,7 +255,17 @@ class Torrent { m_disconnect_callback = std::move(disconnect_callback); } + /** + * Callback when a peer is not interested anymore, i.e. the remote peer + * is not interested in our pieces anymore. + */ + void set_not_interested_callback( + NotInterestedCallback not_interested_callback) { + m_not_interested_callback = std::move(not_interested_callback); + } + void disconnected(Peer* peer); + void not_interested(Peer* peer); /** * The first request to the tracker. After calling this the peer list @@ -279,7 +290,7 @@ class Torrent { * m_client_pieces is also accessed by the file_writer, thus we need to lock. */ void init_client_pieces(bytes::size_type count) { - const std::lock_guard lock(m_mutex); + const std::scoped_lock lock(m_mutex); if (!m_client_pieces.size()) { m_client_pieces = Bitfield(count); } @@ -289,7 +300,7 @@ class Torrent { * The pieces that we have (on disk). */ [[nodiscard]] Bitfield client_pieces() const { - const std::lock_guard lock(m_mutex); + const std::scoped_lock lock(m_mutex); return m_client_pieces; } @@ -297,7 +308,7 @@ class Torrent { * Number of pieces on disk and in total. */ [[nodiscard]] auto piece_status() const { - const std::lock_guard lock(m_mutex); + const std::scoped_lock lock(m_mutex); return std::make_pair(m_client_pieces.count(), m_pieces.size()); } @@ -305,7 +316,7 @@ class Torrent { * Information about what pieces we don't have that a remote has. */ [[nodiscard]] Bitfield relevant_pieces(const Bitfield& remote_pieces) const { - const std::lock_guard lock(m_mutex); + const std::scoped_lock lock(m_mutex); return remote_pieces - m_client_pieces; } @@ -363,7 +374,12 @@ class Torrent { [[nodiscard]] std::tuple file_at_pos( int64_t pos) const; - enum class TrackerEvent { STARTED, STOPPED, COMPLETED, UNSPECIFIED }; + enum class TrackerEvent : std::uint8_t { + STARTED, + STOPPED, + COMPLETED, + UNSPECIFIED + }; /** * Find a torrent with a specific info hash among all existing torrents. @@ -456,40 +472,41 @@ class Torrent { asio::io_context& m_io_context; const Config& m_config; - std::string m_announce{}; - std::vector> m_announce_list{}; + std::string m_announce; + std::vector> m_announce_list; int64_t m_creation_date = 0; - std::string m_comment{}; - std::string m_created_by{}; - std::string m_encoding{}; + std::string m_comment; + std::string m_created_by; + std::string m_encoding; uint32_t m_piece_length = 0; - std::vector m_pieces{}; + std::vector m_pieces; bool m_private = false; - std::string m_name{}; + std::string m_name; int64_t m_length = 0; - std::string m_md5sum{}; - std::vector m_files{}; - Sha1 m_info_hash{}; + std::string m_md5sum; + std::vector m_files; + Sha1 m_info_hash; std::filesystem::path m_tmpfile{}; std::filesystem::path m_data_dir{}; std::filesystem::path m_torrent_file{}; - std::vector m_piece_callbacks{}; - DisconnectCallback m_disconnect_callback{}; - std::string m_peer_id{}; + std::vector m_piece_callbacks; + DisconnectCallback m_disconnect_callback; + NotInterestedCallback m_not_interested_callback; + std::string m_peer_id; ListeningPort m_listening_port; ConnectionPort m_connection_port; - std::mutex m_peers_mutex{}; - std::vector> m_peers{}; + std::mutex m_peers_mutex; + std::vector> m_peers; HttpGet m_http_get; asio::steady_timer m_retry_pieces_timer; asio::steady_timer m_retry_peers_timer; bool m_stopped = false; // Piece housekeeping - mutable std::mutex m_mutex{}; - Bitfield m_client_pieces{}; + mutable std::mutex m_mutex; + Bitfield m_client_pieces; /** Piece id -> Piece object */ - std::map> m_active_pieces{}; + std::map> m_active_pieces; // This is ok and a bug in clang-tidy // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) diff --git a/src/torrent_writer/CMakeLists.txt b/src/torrent_writer/CMakeLists.txt new file mode 100644 index 0000000..8884eb4 --- /dev/null +++ b/src/torrent_writer/CMakeLists.txt @@ -0,0 +1,6 @@ +add_executable(zit_torrent_writer torrent_writer.cpp) +target_link_libraries(zit_torrent_writer + spdlog::spdlog + zitlib +) +target_include_directories(zit_torrent_writer PRIVATE ${CMAKE_CURRENT_LIST_DIR}/..) \ No newline at end of file diff --git a/src/torrent_writer/torrent_writer.cpp b/src/torrent_writer/torrent_writer.cpp new file mode 100644 index 0000000..f0efbae --- /dev/null +++ b/src/torrent_writer/torrent_writer.cpp @@ -0,0 +1,254 @@ +#include "arg_parser.hpp" + +#include "bencode.hpp" +#include "logger.hpp" +#include "sha1.hpp" +#include "string_utils.hpp" +#include "strong_type.hpp" +#include "types.hpp" +#include "version.hpp" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using TorrentFile = StrongType; +using DataPath = StrongType; +using Comment = StrongType; +using AnnounceURL = StrongType; + +namespace rv = std::ranges::views; + +/** + * Get chunks for the pieces. + * + * @param is Input stream + * @param n First chunk size + * @param m Subsequent chunk size + * @return std::generator> + */ +std::generator get_chunks(std::shared_ptr is, + size_t n, + size_t m) { + const auto read_chunk = [&](size_t size) { + zit::bytes buf; + char c{}; + for (size_t i = 0; i < size && is->get(c); ++i) { + buf.push_back(static_cast(c)); + } + return buf; + }; + + // 1. Read the first chunk (size n) + if (auto first = read_chunk(n); !first.empty()) { + co_yield first; + } + + // 2. Read subsequent chunks (size m) + while (*is) { + if (auto next = read_chunk(m); !next.empty()) { + co_yield next; + } else { + break; + } + } +} + +void write_torrent(const TorrentFile& torrent_file, + const DataPath& data_path, + const Comment& comment, + const AnnounceURL& announce_url, + const uint32_t piece_length) { + auto root = bencode::BeDict(); + root["announce"] = bencode::Element::build(announce_url.get()); + root["comment"] = bencode::Element::build(comment.get()); + root["creation date"] = bencode::Element::build(time(nullptr)); + root["created by"] = bencode::Element::build( + fmt::format("Zit v{}.{}.{}", zit::MAJOR_VERSION, zit::MINOR_VERSION, + zit::PATCH_VERSION)); + root["encoding"] = bencode::Element::build("UTF-8"); + + // info dict + auto info = bencode::BeDict(); + std::stringstream pieces_ss; + // single file + if (std::filesystem::is_regular_file(data_path.get())) { + const auto file_size = std::filesystem::file_size(data_path.get()); + zit::logger()->debug("Adding file: {} ({} bytes)", data_path.get(), + file_size); + const auto name_str = + std::filesystem::path(data_path.get()).filename().string(); + // Some clients include a UTF-8 variant of the filename in the info + // dictionary (commonly `name.utf-8`). This is not required by the + // core BitTorrent spec but is widely used by several clients to + // preserve UTF-8 metadata. Include it here to improve compatibility + // and to ensure generated torrents match existing ones that include + // the same field. + info["name"] = bencode::Element::build(name_str); + info["name.utf-8"] = bencode::Element::build(name_str); + info["length"] = bencode::Element::build(static_cast(file_size)); + // md5sum is optional - we skip it for now + + // Read file piecewise and add sha1 per piece + std::ifstream file_stream(data_path.get(), std::ios::in | std::ios::binary); + if (!file_stream) { + throw std::runtime_error("Could not open data file for reading"); + } + auto file_range = + std::ranges::subrange(std::istreambuf_iterator(file_stream), + std::istreambuf_iterator()); + auto chunked_view = file_range | rv::chunk(piece_length); + + for (auto&& chunk : chunked_view) { + auto data = std::ranges::to(chunk); + pieces_ss << zit::Sha1::calculateData(data).str(); + } + + } else if (std::filesystem::is_directory(data_path.get())) { + // multi file + const auto base_name = + std::filesystem::path(data_path.get()).filename().string(); + info["name"] = bencode::Element::build(base_name); + info["name.utf-8"] = bencode::Element::build(base_name); + + auto files = std::vector{}; + zit::bytes piece_data; + for (const auto& entry : + std::filesystem::recursive_directory_iterator(data_path.get())) { + if (std::filesystem::is_regular_file(entry.path())) { + const auto file_size = std::filesystem::file_size(entry.path()); + zit::logger()->debug("Adding file: {} ({} bytes)", + entry.path().string(), file_size); + + // build path list relative to data_path + std::vector path_list; + for (const auto& part : + std::filesystem::relative(entry.path(), data_path.get())) { + path_list.emplace_back(bencode::Element::build(part.string())); + } + + auto file_dict = bencode::BeDict(); + file_dict["length"] = + bencode::Element::build(static_cast(file_size)); + file_dict["path"] = bencode::Element::build(path_list); + file_dict["path.utf-8"] = bencode::Element::build(path_list); + // md5sum is optional - we skip it for now + + files.emplace_back(bencode::Element::build(file_dict)); + + // Now read data for sha1 pieces + + auto is = std::make_shared( + entry.path(), std::ios::in | std::ios::binary); + if (!*is) { + throw std::runtime_error("Could not open data file for reading"); + } + + auto chunk_generator = + get_chunks(is, piece_length - piece_data.size(), piece_length); + for (auto&& chunk : chunk_generator) { + piece_data.insert(piece_data.end(), chunk.begin(), chunk.end()); + if (piece_data.size() == piece_length) { + pieces_ss << zit::Sha1::calculateData(piece_data).str(); + piece_data.clear(); + } + } + } + } + + // Handle any remaining piece data + if (!piece_data.empty()) { + pieces_ss << zit::Sha1::calculateData(piece_data).str(); + piece_data.clear(); + } + + info["files"] = bencode::Element::build(files); + + } else { + throw std::runtime_error(fmt::format( + "Data path '{}' is neither an accessible file nor directory", + data_path.get())); + } + + // common info fields + info["piece length"] = bencode::Element::build(piece_length); + // the "private" field is optional - we skip it for now + info["pieces"] = bencode::Element::build(pieces_ss.str()); + root["info"] = bencode::Element::build(info); + + const auto encoded = bencode::encode(root); + std::ofstream torrent_stream(torrent_file.get(), + std::ios::out | std::ios::binary); + if (!torrent_stream) { + throw std::runtime_error("Could not open torrent file for writing"); + } + torrent_stream << encoded; +} + +} // namespace + +int main(int argc, const char* argv[]) noexcept { + try { + zit::ArgParser parser("Zit - torrent client"); + parser.add_option("--help") + .aliases({"-h"}) + .help("Print help") + .help_arg(); + parser.add_option("--torrent") + .help("Torrent file to download or write") + .required(); + parser.add_option("--comment") + .help("Torrent file comment") + .default_value(""); + parser.add_option("--data") + .help("Directory or file with torrent content") + .required(); + parser.add_option("--announce") + .help( + "Announce URL for the torrent. Currently only single URL is " + "supported") + .required(); + constexpr uint32_t DEFAULT_PIECE_LENGTH{256 * 1024}; + parser.add_option("--piece-length") + .help("Piece length in bytes (default is " + + zit::bytesToHumanReadable(DEFAULT_PIECE_LENGTH) + ")") + .default_value(DEFAULT_PIECE_LENGTH); + const auto args = std::vector{argv, std::next(argv, argc)}; + parser.parse(args); + if (parser.get("--help")) { + std::cout << parser.usage(); + return 0; + } + + const auto torrent_file = parser.get("--torrent"); + const auto data_path = parser.get("--data"); + const auto comment = parser.get("--comment"); + const auto announce_url = parser.get("--announce"); + const auto piece_length = parser.get("--piece-length"); + + write_torrent(TorrentFile(torrent_file), DataPath(data_path), + Comment(comment), AnnounceURL(announce_url), piece_length); + + } catch (const std::exception& e) { + std::cerr << "Error: " << e.what() << "\n"; + return 1; + } + + return 0; +} diff --git a/src/types.hpp b/src/types.hpp index 56595c4..ddf02a0 100644 --- a/src/types.hpp +++ b/src/types.hpp @@ -24,14 +24,14 @@ using ListeningPort = StrongType; using ConnectionPort = StrongType; // numeric_cast ( Copied from https://codereview.stackexchange.com/a/26496/39248 -// ) by user Matt Whitlock - functions renamed. +// ) by user Matt Whitlock - functions renamed and modified. // FIXME: These casts could fall back to no checking at all if I == J template -static constexpr - typename std::enable_if_t && std::is_signed_v, I> - numeric_cast(J value, const char* msg = nullptr) { +static constexpr I numeric_cast(J value, const char* msg = nullptr) + requires(std::is_signed_v && std::is_signed_v) +{ if (value < static_cast(std::numeric_limits::min()) || value > static_cast(std::numeric_limits::max())) { throw std::out_of_range(msg ? msg : "out of range"); @@ -40,21 +40,21 @@ static constexpr } template -static constexpr - typename std::enable_if_t && std::is_unsigned_v, I> - numeric_cast(J value, const char* msg = nullptr) { - if (value > static_cast>( - std::numeric_limits::max())) { +static constexpr I numeric_cast(J value, const char* msg = nullptr) + requires(std::is_signed_v && std::is_unsigned_v) +{ + if (value > + static_cast>(std::numeric_limits::max())) { throw std::out_of_range(msg ? msg : "out of range"); } return static_cast(value); } template -static constexpr - typename std::enable_if_t && std::is_signed_v, I> - numeric_cast(J value, const char* msg = nullptr) { - if (value < 0 || static_cast>(value) > +static constexpr I numeric_cast(J value, const char* msg = nullptr) + requires(std::is_unsigned_v && std::is_signed_v) +{ + if (value < 0 || static_cast>(value) > std::numeric_limits::max()) { throw std::out_of_range(msg ? msg : "out of range"); } @@ -62,9 +62,9 @@ static constexpr } template -static constexpr - typename std::enable_if_t && std::is_unsigned_v, I> - numeric_cast(J value, const char* msg = nullptr) { +static constexpr I numeric_cast(J value, const char* msg = nullptr) + requires(std::is_unsigned_v && std::is_unsigned_v) +{ if (value > std::numeric_limits::max()) { throw std::out_of_range(msg ? msg : "out of range"); } @@ -115,6 +115,8 @@ static inline T from_big_endian(const bytes& buf, bytes::size_type offset = 0) { "Target range outside of buffer: ({},{})", offset, buf.size())); } + // NOLINTBEGIN(cppcoreguidelines-pro-bounds-avoid-unchecked-container-access) + if constexpr (size == 2) { return static_cast(static_cast(buf[offset + 1]) << 0 | static_cast(buf[offset + 0]) << 8); @@ -135,6 +137,8 @@ static inline T from_big_endian(const bytes& buf, bytes::size_type offset = 0) { } else { static_assert(!sizeof(T*), "Unhandled size"); } + + // NOLINTEND(cppcoreguidelines-pro-bounds-avoid-unchecked-container-access) } /** diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 16b77e1..da4a7ca 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -32,6 +32,7 @@ add_executable(zitest test_global_config.cpp test_integrate.cpp test_torrent.cpp + test_torrent_writer.cpp test_main.cpp test_net.cpp test_peer.cpp @@ -82,6 +83,10 @@ endif() target_compile_definitions(zitest PUBLIC DATA_DIR="${CMAKE_CURRENT_SOURCE_DIR}/data") +# Path to the torrent writer executable for tests +add_dependencies(zitest zit_torrent_writer) +target_compile_definitions(zitest PUBLIC TORRENT_WRITER_EXE="$") + # For getting backtraces use: # sudo ln -s /usr/bin/llvm-symbolizer-3.8 /usr/bin/llvm-symbolizer # export ASAN_SYMBOLIZER_PATH=/usr/bin/llvm-symbolizer diff --git a/tests/data/opentracker.whitelist b/tests/data/opentracker.whitelist index fddd134..922a6ef 100644 --- a/tests/data/opentracker.whitelist +++ b/tests/data/opentracker.whitelist @@ -1,2 +1,3 @@ F23537B079181D186B71487DF7A9677D0DCF3CC8 274EF6264F05A63BAAAF8532AA33BFE74F799B69 +FB0DD799BAC111630421AC87E074563D4704F18C diff --git a/tests/docker/opentracker.dockerfile b/tests/docker/opentracker.dockerfile index 99e3c4d..0b3b9ca 100644 --- a/tests/docker/opentracker.dockerfile +++ b/tests/docker/opentracker.dockerfile @@ -1,5 +1,5 @@ # Use fixed ubuntu image for reproducibility -FROM ubuntu:24.10 +FROM ubuntu:24.04 # Install necessary packages RUN apt-get update && apt-get install -y \ diff --git a/tests/docker/opentracker_entrypoint.sh b/tests/docker/opentracker_entrypoint.sh index a48d483..65ceaf7 100644 --- a/tests/docker/opentracker_entrypoint.sh +++ b/tests/docker/opentracker_entrypoint.sh @@ -1,14 +1,40 @@ #!/bin/sh set -e -# Resolve the host IP address -HOST_IP=$(getent ahostsv4 host.docker.internal | grep 'STREAM' | awk '{ print $1 }' | head -n 1) -#HOST_IP=192.168.0.18 +HOST_IP="${HOST_IP:-}" + +# 1) If user set HOST_IP env, use it (already handled by expansion above) + +# 2) Try getent ahostsv4 (Docker Desktop / some setups) +if [ -z "$HOST_IP" ] && command -v getent >/dev/null 2>&1; then + HOST_IP=$(getent ahostsv4 host.docker.internal 2>/dev/null | awk '{print $1; exit}' || true) +fi + +# 3) Try classic getent hosts lookup +if [ -z "$HOST_IP" ] && command -v getent >/dev/null 2>&1; then + HOST_IP=$(getent hosts host.docker.internal 2>/dev/null | awk '{print $1; exit}' || true) +fi + +# 4) Fallback: use container default gateway (usually the host's docker bridge IP) +if [ -z "$HOST_IP" ] && command -v ip >/dev/null 2>&1; then + HOST_IP=$(ip route 2>/dev/null | awk '/default/ {print $3; exit}' || true) +fi + +# 5) Last-resort: try /sbin/ip route (some minimal images) +if [ -z "$HOST_IP" ] && [ -x /sbin/ip ]; then + HOST_IP=$(/sbin/ip route 2>/dev/null | awk '/default/ {print $3; exit}' || true) +fi + +if [ -z "$HOST_IP" ]; then + echo "Warning: HOST_IP could not be determined inside the container." >&2 + echo "You can set HOST_IP env or run container with --add-host=host.docker.internal:host-gateway" >&2 + exit 1 +fi # Set up iptables rule to route traffic to host.docker.internal -# This is necessary because opentracker uses will use the Docker gateway IP +# This is necessary because opentracker will use the Docker gateway IP # for the peer list which will not work from within the container -iptables -t nat -A OUTPUT -d 172.17.0.1 -j DNAT --to-destination "$HOST_IP" +/usr/sbin/iptables -t nat -A OUTPUT -d 172.17.0.1 -j DNAT --to-destination "$HOST_IP" # Run opentracker with the provided arguments exec opentracker "$@" diff --git a/tests/docker/transmission.dockerfile b/tests/docker/transmission.dockerfile deleted file mode 100644 index 4e6bfe5..0000000 --- a/tests/docker/transmission.dockerfile +++ /dev/null @@ -1,34 +0,0 @@ -# Use fixed ubuntu image for reproducibility -FROM ubuntu:24.10 - -# Install necessary packages -RUN apt-get update && apt-get install -y \ - # Requirements from the repositories documentation: - # https://github.com/transmission/transmission/blob/main/docs/Building-Transmission.md#on-unix - build-essential cmake git libcurl4-openssl-dev libssl-dev \ - # Additional packages for the build process added by me: - ninja-build \ - && rm -rf /var/lib/apt/lists/* - -# Clone the Transmission repository -RUN git clone --recurse-submodules --branch 4.0.6 https://github.com/transmission/transmission.git /opt/transmission - -# Set the working directory -WORKDIR /opt/transmission - -# Build the Transmission project -RUN cmake -B build -G Ninja \ - -DENABLE_DAEMON=OFF \ - -DINSTALL_WEB=OFF \ - -DENABLE_UTILS=OFF \ - -DENABLE_CLI=ON \ - -DENABLE_TESTS=OFF \ - -DINSTALL_DOC=OFF \ - -DCMAKE_BUILD_TYPE=Release -RUN cmake --build build --target install - -# Expose the necessary ports -EXPOSE 51413 - -# Set the entry point to run transmission-cli -ENTRYPOINT ["/opt/transmission/build/cli/transmission-cli"] diff --git a/tests/docker/webtorrent.dockerfile b/tests/docker/webtorrent.dockerfile index d18a493..3f4d912 100644 --- a/tests/docker/webtorrent.dockerfile +++ b/tests/docker/webtorrent.dockerfile @@ -1,11 +1,10 @@ -FROM ubuntu:24.10 +FROM ubuntu:24.04 -# Install nodejs and npm +# Install nodejs, webtorrent-cli, iptables tools, and gosu for UID/GID drop RUN apt-get update && \ - apt-get install -y npm iptables && \ - rm -rf /var/lib/apt/lists/* - -RUN npm install webtorrent-cli -g + apt-get install -y npm iptables iproute2 gosu && \ + rm -rf /var/lib/apt/lists/* && \ + npm install webtorrent-cli -g COPY webtorrent_entrypoint.sh /usr/local/bin/webtorrent_entrypoint.sh RUN chmod +x /usr/local/bin/webtorrent_entrypoint.sh diff --git a/tests/docker/webtorrent_entrypoint.sh b/tests/docker/webtorrent_entrypoint.sh index 78e6ecf..48eff06 100644 --- a/tests/docker/webtorrent_entrypoint.sh +++ b/tests/docker/webtorrent_entrypoint.sh @@ -1,14 +1,48 @@ #!/bin/sh set -e -# Resolve the host IP address -HOST_IP=$(getent ahostsv4 host.docker.internal | grep 'STREAM' | awk '{ print $1 }' | head -n 1) -#HOST_IP=192.168.0.18 +HOST_IP="${HOST_IP:-}" + +# 1) If user set HOST_IP env, use it (already handled by expansion above) + +# 2) Try getent ahostsv4 (Docker Desktop / some setups) +if [ -z "$HOST_IP" ] && command -v getent >/dev/null 2>&1; then + HOST_IP=$(getent ahostsv4 host.docker.internal 2>/dev/null | awk '{print $1; exit}' || true) +fi + +# 3) Try classic getent hosts lookup +if [ -z "$HOST_IP" ] && command -v getent >/dev/null 2>&1; then + HOST_IP=$(getent hosts host.docker.internal 2>/dev/null | awk '{print $1; exit}' || true) +fi + +# 4) Fallback: use container default gateway (usually the host's docker bridge IP) +if [ -z "$HOST_IP" ] && command -v ip >/dev/null 2>&1; then + HOST_IP=$(ip route 2>/dev/null | awk '/default/ {print $3; exit}' || true) +fi + +# 5) Last-resort: try /sbin/ip route (some minimal images) +if [ -z "$HOST_IP" ] && [ -x /sbin/ip ]; then + HOST_IP=$(/sbin/ip route 2>/dev/null | awk '/default/ {print $3; exit}' || true) +fi + +if [ -z "$HOST_IP" ]; then + echo "Warning: HOST_IP could not be determined inside the container." >&2 + echo "You can set HOST_IP env or run container with --add-host=host.docker.internal:host-gateway" >&2 + exit 1 +fi # Set up iptables rule to route traffic to host.docker.internal -# This is necessary because opentracker uses will use the Docker gateway IP +# This is necessary because opentracker will use the Docker gateway IP # for the peer list which will not work from within the container -iptables -t nat -A OUTPUT -d 172.17.0.1 -j DNAT --to-destination "$HOST_IP" +/usr/sbin/iptables -t nat -A OUTPUT -d 172.17.0.1 -j DNAT --to-destination "$HOST_IP" + +# If host UID/GID are provided, fix ownership of the bind mount and +# drop privileges using gosu so files get correct ownership +if [ -n "$HOST_UID" ] && [ -n "$HOST_GID" ] && command -v gosu >/dev/null 2>&1; then + mkdir -p /data 2>/dev/null || true + chown -R "$HOST_UID:$HOST_GID" /data 2>/dev/null || true + exec gosu "$HOST_UID:$HOST_GID" webtorrent "$@" +fi -# Run opentracker with the provided arguments +# Fallback: run as root if HOST_UID/GID not provided exec webtorrent "$@" diff --git a/tests/test_integrate.cpp b/tests/test_integrate.cpp index 9bb0b48..cfa495a 100644 --- a/tests/test_integrate.cpp +++ b/tests/test_integrate.cpp @@ -1,6 +1,13 @@ /** * Currently using linux programs for integration testing * but inside docker containers. + * + * sudo apt install docker.io + * sudo usermod -aG docker $USER + * newgrp docker + * + * cd tests/docker + * ./build.sh */ /** @@ -8,10 +15,14 @@ * * - Tracker (opentracker) * - https://erdgeist.org/arts/software/opentracker/ - * - Seeder (transmission-cli) - * - sudo apt install transmission-cli + * - Seeder (webtorrent-cli) + * - https://github.com/webtorrent/webtorrent-cli + * + * Both currently handled by the docker images built above. */ +#include +#include #include #include #include @@ -22,6 +33,12 @@ #include "test_main.hpp" #include "test_utils.hpp" +// Path to the writer executable provided by CMake as a compile definition. +// Example: "C:/path/to/build/src/torrent_writer/zit_torrent_writer" +#ifndef TORRENT_WRITER_EXE +#error "TORRENT_WRITER_EXE not defined" +#endif + using namespace std; using namespace std::chrono_literals; using namespace std::string_literals; @@ -32,14 +49,7 @@ namespace fs = std::filesystem; class TestConfig : public zit::Config { public: - TestConfig() : zit::Config() { - // As long as we use Transmission and test with all processes on localhost - // we need to make sure we initiate the connections. - // m_bool_settings[zit::BoolSetting::INITIATE_PEER_CONNECTIONS] = false; - - // Transmission does not want to connect to localhost, so trick it - // m_string_settings[zit::StringSetting::BIND_ADDRESS] = TEST_BIND_ADDRESS; - } + TestConfig() : zit::Config() {} void set(IntSetting setting, int val) { m_int_settings[setting] = val; } void set(StringSetting setting, std::string val) { @@ -49,6 +59,53 @@ class TestConfig : public zit::Config { namespace { +std::string get_host_ip_from_host() { +#ifndef WIN32 + if (const char* env = getenv("HOST_IP")) { + if (strlen(env) > 0) + return std::string(env); + } + // Try to obtain the host IP used for outbound traffic (robust on Linux) + FILE* f = popen( + "ip route get 1.1.1.1 2>/dev/null | awk '{for(i=1;i<=NF;i++) " + "if($i==\"src\") {print $(i+1); exit}}'", + "r"); + if (f) { + char buf[128]; + if (fgets(buf, sizeof(buf), f)) { + pclose(f); + // strip newline + std::string s(buf); + if (!s.empty() && s.back() == '\n') + s.pop_back(); + if (!s.empty()) + return s; + } else { + pclose(f); + } + } + // Try docker bridge gateway as a fallback + f = popen( + "docker network inspect bridge --format '{{(index .IPAM.Config " + "0).Gateway}}' 2>/dev/null", + "r"); + if (f) { + char buf[128]; + if (fgets(buf, sizeof(buf), f)) { + pclose(f); + std::string s(buf); + if (!s.empty() && s.back() == '\n') + s.pop_back(); + if (!s.empty()) + return s; + } else { + pclose(f); + } + } +#endif // WIN32 + throw std::runtime_error("Failed to determine host IP address"); +} + [[maybe_unused]] fs::path home_dir() { const auto home = getenv("HOME"); if (!home || strlen(home) < 2) { @@ -57,21 +114,55 @@ namespace { return {home}; } +// Generate a .torrent file from data using the test torrent writer +// `TORRENT_WRITER_EXE` is expected to be set by the build system. +fs::path generate_torrent_with_announce(const fs::path& data, + const std::string& announce, + const fs::path& out_dir) { + const auto out_torrent = + out_dir / ("gen_" + data.filename().string() + ".torrent"); + const std::string cmd = std::string(TORRENT_WRITER_EXE) + " --torrent " + + out_torrent.generic_string() + " --data " + + data.generic_string() + + " --comment 'Generated for testing' --announce " + + announce + " --piece-length 32768"; + // Use exec() from test_utils.hpp which throws on non-zero exit + exec(cmd); + return out_torrent; +} + auto start_tracker(const fs::path& data_dir) { + // Ensure any previous tracker container is gone to avoid name conflicts + try { + exec("docker rm -f zit-opentracker > /dev/null 2>&1 || true"); + } catch (const std::exception&) { + // Ignore cleanup errors + } + + std::vector args = { + "docker", "run", "--rm", "--name", "zit-opentracker", "--volume", + fmt::format("{}:{}", data_dir.generic_string(), "/data"), + // Needed to be able to run iptables in the container + "--cap-add", "NET_ADMIN", + // + "--publish", "8000:8000"}; + // Add host mapping + const auto host_ip = get_host_ip_from_host(); + if (!host_ip.empty()) { + args.push_back("--add-host"); + args.push_back(fmt::format("host.docker.internal:{}", host_ip)); + } else { + args.push_back("--add-host"); + args.push_back("host.docker.internal:host-gateway"); + } + args.push_back("opentracker"); + args.push_back("-p"); + args.push_back("8000"); + args.push_back("-w"); + args.push_back("/data/opentracker.whitelist"); + auto tracker = - Process("tracker", - {"docker", "run", "--rm", "--name", "zit-opentracker", "--volume", - fmt::format("{}:{}", data_dir.generic_string(), "/data"), - // Needed to be able to run iptables in the container - "--cap-add", "NET_ADMIN", - // - "--publish", "8000:8000", "opentracker", "-p", "8000", - // The default opentracker build seem to be compiled in closed - // mode such that all torrent must be explicitly listed in a - // whitelist. It would be possible to recompile it without that - // but for now lets just use it like this. - "-w", "/data/opentracker.whitelist"}, - nullptr, {"docker", "stop", "zit-opentracker"}); + Process("tracker", args, nullptr, {"docker", "stop", "zit-opentracker"}); // Allow some time for the tracker to start this_thread::sleep_for(1s); @@ -91,34 +182,76 @@ auto start_seeder(const fs::path& data_dir, static int seeder_count{0}; const auto container_name = fmt::format("zit-webtorrent-{}", seeder_count++); - const auto port = std::to_string(webtorrent_seeding_port); - return Process( - name, - {"docker", "run", "--rm", "--name", container_name, "--publish", - fmt::format("{}:{}", port, port), "--volume", - fmt::format("{}:{}", data_dir.generic_string(), container_data_dir), - "--volume", - fmt::format("{}:{}", torrent_file.parent_path().generic_string(), - container_torrent_dir), - // Needed to be able to run iptables in the container - "--cap-add", "NET_ADMIN", - // For verbose output - "--env", - fmt::format("DEBUG={}", - logger()->should_log(spdlog::level::debug) ? "*" : ""), - // - "webtorrent", "seed", - // Torrent file - fmt::format("{}/{}", container_torrent_dir, torrent_file.filename()), - "--out", container_data_dir, "--torrent-port", - std::to_string(webtorrent_seeding_port++), "--port", - std::to_string(webtorrent_http_port++), "--keep-seeding"}, - nullptr, {"docker", "stop", container_name}); + + // Ensure any previous tracker container is gone to avoid name conflicts + try { + exec(fmt::format("docker rm -f {} > /dev/null 2>&1 || true", + container_name)); + } catch (const std::exception&) { + // Ignore cleanup errors + } + + std::vector args = { + "docker", "run", "--rm", "--name", container_name, +#ifndef WIN32 + // Pass host UID/GID so container can fix file ownership on exit + "--env", fmt::format("HOST_UID={}", getuid()), "--env", + fmt::format("HOST_GID={}", getgid()), +#endif // WIN32 + "--publish", fmt::format("{}:{}", port, port), "--volume", + fmt::format("{}:{}", data_dir.generic_string(), container_data_dir), + "--volume", + fmt::format("{}:{}", torrent_file.parent_path().generic_string(), + container_torrent_dir), + // Needed to be able to run iptables in the container + "--cap-add", "NET_ADMIN", + // For verbose output + "--env", + fmt::format("DEBUG={}", + logger()->should_log(spdlog::level::debug) ? "*" : "")}; + + // Add host mapping so container can resolve host.docker.internal to host LAN + // IP + const auto host_ip = get_host_ip_from_host(); + if (!host_ip.empty()) { + args.push_back("--add-host"); + args.push_back(fmt::format("host.docker.internal:{}", host_ip)); + } else { + args.push_back("--add-host"); + args.push_back("host.docker.internal:host-gateway"); + } + + // Command and arguments for webtorrent + args.push_back("webtorrent"); + args.push_back("seed"); + args.push_back( + fmt::format("{}/{}", container_torrent_dir, torrent_file.filename())); + args.push_back("--out"); + args.push_back(container_data_dir); + args.push_back("--torrent-port"); + args.push_back(std::to_string(webtorrent_seeding_port++)); + args.push_back("--port"); + args.push_back(std::to_string(webtorrent_http_port++)); + args.push_back("--keep-seeding"); + + return Process(name, args, nullptr, {"docker", "stop", container_name}); } auto start_leecher(const fs::path& target, const fs::path& torrent_file) { - fs::remove(target); + try { + if (fs::exists(target)) { + // Ensure the directory is empty to avoid leftover files + for (const auto& entry : fs::directory_iterator(target)) { + fs::remove_all(entry); + } + } + // Ensure the mount directory exists and is owned by the host user + fs::create_directories(target); + } catch (const std::exception& e) { + logger()->warn("Failed preparing target directory {}: {}", target.string(), + e.what()); + } return start_seeder(target, torrent_file, "leecher"); } @@ -222,7 +355,10 @@ TEST_P(IntegrateF, download) { TEST_P(IntegrateF, DISABLED_download) { #endif // INTEGRATION_TESTS const auto data_dir = fs::path(DATA_DIR); - const auto torrent_file = data_dir / "1MiB.torrent"; + const auto announce = + std::format("http://{}:8000/announce", get_host_ip_from_host()); + const auto torrent_file = generate_torrent_with_announce( + data_dir / "1MiB.dat", announce, tmp_dir()); const uint8_t number_of_seeders = GetParam(); auto tracker = start_tracker(data_dir); @@ -255,7 +391,10 @@ TEST_F(IntegrateOodF, download_ood) { TEST_F(IntegrateOodF, DISABLED_download_ood) { #endif // INTEGRATION_TESTS const auto data_dir = fs::path(DATA_DIR); - const auto torrent_file = data_dir / "1MiB.torrent"; + const auto announce = + std::format("http://{}:8000/announce", get_host_ip_from_host()); + const auto torrent_file = generate_torrent_with_announce( + data_dir / "1MiB.dat", announce, tmp_dir()); constexpr uint8_t number_of_seeders = 1; auto tracker = start_tracker(data_dir); @@ -298,8 +437,12 @@ TEST_F(IntegrateF, DISABLED_download_dual_torrents) { const auto download_dir = tmp_dir(); TestConfig test_config; - const auto torrent_file_1 = data_dir / "1MiB.torrent"; - const auto torrent_file_2 = data_dir / "multi.torrent"; + const auto torrent_file_1 = generate_torrent_with_announce( + data_dir / "1MiB.dat", + "http://" + get_host_ip_from_host() + ":8000/announce", tmp_dir()); + const auto torrent_file_2 = generate_torrent_with_announce( + data_dir / "multi", + "http://" + get_host_ip_from_host() + ":8000/announce", tmp_dir()); auto tracker = start_tracker(data_dir); @@ -332,7 +475,9 @@ TEST_F(Integrate, download_part) { TEST_P(IntegrateF, DISABLED_download_part) { #endif // INTEGRATION_TESTS const auto data_dir = fs::path(DATA_DIR); - const auto torrent_file = data_dir / "1MiB.torrent"; + const auto torrent_file = generate_torrent_with_announce( + data_dir / "1MiB.dat", + "http://" + get_host_ip_from_host() + ":8000/announce", tmp_dir()); const auto download_dir = tmp_dir(); auto tracker = start_tracker(data_dir); @@ -372,7 +517,9 @@ TEST_F(Integrate, download_multi_part) { TEST_F(Integrate, DISABLED_download_multi_part) { #endif // INTEGRATION_TESTS const auto data_dir = fs::path(DATA_DIR); - const auto torrent_file = data_dir / "multi.torrent"; + const auto torrent_file = generate_torrent_with_announce( + data_dir / "multi", + "http://" + get_host_ip_from_host() + ":8000/announce", tmp_dir()); const auto download_dir = tmp_dir(); auto tracker = start_tracker(data_dir); @@ -414,7 +561,9 @@ TEST_F(Integrate, download_multi_file) { TEST_F(Integrate, DISABLED_download_multi_file) { #endif // INTEGRATION_TESTS const auto data_dir = fs::path(DATA_DIR); - const auto torrent_file = data_dir / "multi.torrent"; + const auto torrent_file = generate_torrent_with_announce( + data_dir / "multi", + "http://" + get_host_ip_from_host() + ":8000/announce", tmp_dir()); auto tracker = start_tracker(data_dir); @@ -435,7 +584,9 @@ TEST_F(Integrate, upload) { TEST_F(Integrate, DISABLED_upload) { #endif // INTEGRATION_TESTS const auto data_dir = fs::path(DATA_DIR); - const auto torrent_file = data_dir / "1MiB.torrent"; + const auto torrent_file = generate_torrent_with_announce( + data_dir / "1MiB.dat", + "http://" + get_host_ip_from_host() + ":8000/announce", tmp_dir()); auto tracker = start_tracker(data_dir); @@ -453,7 +604,8 @@ TEST_F(Integrate, DISABLED_upload) { const auto target = tmp_dir() / "upload_test"; const auto destination = target / "1MiB.dat"; // Be fully sure we do not have the file there yet - ASSERT_FALSE(fs::exists(destination)); + ASSERT_FALSE(fs::exists(destination)) + << "File already exists: " << destination; std::optional leecher; asio::steady_timer timer{m_io_context}; @@ -463,21 +615,18 @@ TEST_F(Integrate, DISABLED_upload) { EXPECT_FALSE(ec); leecher = start_leecher(target, torrent_file); - torrent.set_disconnect_callback([&](zit::Peer*) { - logger()->info("Peer disconnect - stopping"); + torrent.set_not_interested_callback([&](zit::Peer*) { + logger()->info("Peer not interested - upload complete, stopping"); torrent.stop(); leecher->terminate(); }); }); - // FIXME: How to avoid this sleep? - // this_thread::sleep_for(5s); - // Connects to tracker and retrieves peers torrent.start(); // Run the peer connections - m_io_context.run(); + torrent.run(); // Transfer done - Verify content verify_download(torrent, destination); @@ -489,7 +638,9 @@ TEST_F(Integrate, multi_upload) { TEST_F(Integrate, DISABLED_multi_upload) { #endif // INTEGRATION_TESTS const auto data_dir = fs::path(DATA_DIR); - const auto torrent_file = data_dir / "multi.torrent"; + const auto torrent_file = generate_torrent_with_announce( + data_dir / "multi", + "http://" + get_host_ip_from_host() + ":8000/announce", tmp_dir()); auto tracker = start_tracker(data_dir); @@ -498,18 +649,32 @@ TEST_F(Integrate, DISABLED_multi_upload) { zit::Torrent torrent(m_io_context, torrent_file, data_dir, test_config); ASSERT_TRUE(torrent.done()); + sigint_function = [&](int /*s*/) { + logger()->warn("CTRL-C pressed. Stopping torrent..."); + torrent.stop(); + }; + // Start a leecher that we will upload to - auto target = tmp_dir() / "multi_upload_test"; - auto leecher = start_leecher(target, torrent_file); + const auto target = tmp_dir() / "multi_upload_test"; + const auto destination = target / "multi"; // folder name inside torrent + // Be fully sure we do not have the files there yet + ASSERT_FALSE(fs::exists(destination)) + << "File already exists: " << destination; - torrent.set_disconnect_callback([&](zit::Peer*) { - logger()->info("Peer disconnect - stopping"); - torrent.stop(); - leecher.terminate(); - }); + std::optional leecher; + asio::steady_timer timer{m_io_context}; + timer.expires_after(15s); - // FIXME: How to avoid this sleep? - this_thread::sleep_for(15s); + timer.async_wait([&](auto ec) { + EXPECT_FALSE(ec); + leecher = start_leecher(target, torrent_file); + + torrent.set_not_interested_callback([&](zit::Peer*) { + logger()->info("Peer not interested - upload complete, stopping"); + torrent.stop(); + leecher->terminate(); + }); + }); // Connects to tracker and retrieves peers torrent.start(); @@ -518,5 +683,5 @@ TEST_F(Integrate, DISABLED_multi_upload) { torrent.run(); // Transfer done - Verify content - verify_download(torrent, data_dir / torrent.name(), false); + verify_download(torrent, destination, false); } diff --git a/tests/test_torrent_writer.cpp b/tests/test_torrent_writer.cpp new file mode 100644 index 0000000..42763e5 --- /dev/null +++ b/tests/test_torrent_writer.cpp @@ -0,0 +1,164 @@ +#include + +#include +#include +#include + +#include "test_utils.hpp" +#include "torrent.hpp" + +namespace fs = std::filesystem; + +// Path to the writer executable provided by CMake as a compile definition. +// Example: "C:/path/to/build/src/torrent_writer/zit_torrent_writer" +#ifndef TORRENT_WRITER_EXE +#error "TORRENT_WRITER_EXE not defined" +#endif + +using TorrentWriter = TestWithTmpDir; + +TEST_F(TorrentWriter, GeneratedInfoHashEqualsReferenceSingle) { + const auto data_dir = fs::path(DATA_DIR); + const auto data_file = data_dir / "1MiB.dat"; + const auto out_torrent = tmp_dir() / "test_1MiB.torrent"; + + // Run the writer executable to create a torrent for 1MiB.dat + const std::string cmd = + std::string(TORRENT_WRITER_EXE) + " --torrent " + + out_torrent.generic_string() + " --data " + data_file.generic_string() + + " --comment '' --announce http://example/announce --piece-length 32768"; + + const int rc = std::system(cmd.c_str()); + ASSERT_EQ(rc, 0) << "torrent writer failed: " << cmd; + + // Read generated torrent and reference torrent using existing Torrent class. + // Construct each Torrent in its own scope so they are not live at the same + // time (the Torrent constructor registers the torrent by info_hash). + + zit::Sha1 gen_hash; + std::vector gen_pieces; + uint32_t gen_piece_length = 0; + int64_t gen_length = 0; + std::string gen_name; + + { + asio::io_context io1; + zit::Torrent generated(io1, out_torrent); + gen_hash = generated.info_hash(); + gen_pieces = generated.pieces(); + gen_piece_length = generated.piece_length(); + gen_length = generated.length(); + gen_name = generated.name(); + } + + zit::Sha1 ref_hash; + std::vector ref_pieces; + uint32_t ref_piece_length = 0; + int64_t ref_length = 0; + std::string ref_name; + + { + asio::io_context io2; + zit::Torrent reference(io2, data_dir / "1MiB.torrent"); + ref_hash = reference.info_hash(); + ref_pieces = reference.pieces(); + ref_piece_length = reference.piece_length(); + ref_length = reference.length(); + ref_name = reference.name(); + } + + EXPECT_EQ(gen_name, ref_name); + EXPECT_EQ(gen_length, ref_length); + EXPECT_EQ(gen_pieces.size(), ref_pieces.size()); + EXPECT_EQ(gen_piece_length, ref_piece_length); + EXPECT_EQ(gen_hash.hex(), ref_hash.hex()); + + // Compare individual pieces + for (size_t i = 0; i < gen_pieces.size(); ++i) { + EXPECT_EQ(gen_pieces[i].hex(), ref_pieces[i].hex()) << "Piece index " << i; + } +} + +TEST_F(TorrentWriter, GeneratedInfoHashEqualsReferenceMulti) { + const auto data_dir = fs::path(DATA_DIR); + const auto multi_dir = data_dir / "multi"; + const auto out_torrent = tmp_dir() / "test_multi.torrent"; + + // Run the writer executable to create a torrent for 1MiB.dat + const std::string cmd = + std::string(TORRENT_WRITER_EXE) + " --torrent " + + out_torrent.generic_string() + " --data " + multi_dir.generic_string() + + " --comment '' --announce http://example/announce --piece-length 262144"; + + const int rc = std::system(cmd.c_str()); + ASSERT_EQ(rc, 0) << "torrent writer failed: " << cmd; + + // Read generated torrent and reference torrent using existing Torrent class. + // Construct each Torrent in its own scope so they are not live at the same + // time (the Torrent constructor registers the torrent by info_hash). + + zit::Sha1 gen_hash; + std::vector gen_pieces; + uint32_t gen_piece_length = 0; + int64_t gen_length = 0; + std::string gen_name; + std::vector gen_files; + + { + asio::io_context io1; + zit::Torrent generated(io1, out_torrent); + gen_hash = generated.info_hash(); + gen_pieces = generated.pieces(); + gen_piece_length = generated.piece_length(); + gen_length = generated.length(); + gen_name = generated.name(); + gen_files = generated.files(); + } + + zit::Sha1 ref_hash; + std::vector ref_pieces; + uint32_t ref_piece_length = 0; + int64_t ref_length = 0; + std::string ref_name; + std::vector ref_files; + + { + asio::io_context io2; + zit::Torrent reference(io2, data_dir / "multi.torrent"); + ref_hash = reference.info_hash(); + ref_pieces = reference.pieces(); + ref_piece_length = reference.piece_length(); + ref_length = reference.length(); + ref_name = reference.name(); + ref_files = reference.files(); + } + + EXPECT_EQ(gen_name, ref_name); + EXPECT_EQ(gen_length, ref_length); + EXPECT_EQ(gen_pieces.size(), ref_pieces.size()); + EXPECT_EQ(gen_piece_length, ref_piece_length); + + // Compare file lists for both torrents + const auto path_order = [](const zit::FileInfo& a, const zit::FileInfo& b) { + return a.path() < b.path(); + }; + std::sort(gen_files.begin(), gen_files.end(), path_order); + std::sort(ref_files.begin(), ref_files.end(), path_order); + EXPECT_EQ(gen_files.size(), ref_files.size()); + for (size_t i = 0; i < gen_files.size(); ++i) { + EXPECT_EQ(gen_files[i].path(), ref_files[i].path()); + EXPECT_EQ(gen_files[i].length(), ref_files[i].length()); + } + + // The current reference was generated with a different file order and is thus + // not hash comparable. + // TODO: Generate a reference with a fixed order and + // enusure the writer uses the same order. + /* + EXPECT_EQ(gen_hash.hex(), ref_hash.hex()); + // Compare individual pieces + for (size_t i = 0; i < gen_pieces.size(); ++i) { + EXPECT_EQ(gen_pieces[i].hex(), ref_pieces[i].hex()) << "Piece index " << i; + } + */ +} diff --git a/tests/test_utils.hpp b/tests/test_utils.hpp index 9657bf5..e701a05 100644 --- a/tests/test_utils.hpp +++ b/tests/test_utils.hpp @@ -72,7 +72,8 @@ inline std::string exec(const std::string& cmd) { std::array buffer; std::string result; while (!feof(pipe)) { - if (fgets(buffer.data(), 128, pipe) != nullptr) { + if (fgets(buffer.data(), zit::numeric_cast(buffer.size()), pipe) != + nullptr) { result += buffer.data(); } } diff --git a/vcpkg b/vcpkg index 8ae59b5..74e6536 160000 --- a/vcpkg +++ b/vcpkg @@ -1 +1 @@ -Subproject commit 8ae59b5b1329a51875abc71d528da93d9c3e8972 +Subproject commit 74e6536215718009aae747d86d84b78376bf9e09 diff --git a/vcpkg.json b/vcpkg.json index 5f9bee4..7f69152 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -9,24 +9,24 @@ "overrides": [ { "name": "openssl", - "version-string": "3.3.1" + "version-string": "3.6.0" }, { "name": "asio", - "version": "1.30.2" + "version": "1.32.0" }, { "name": "gtest", - "version": "1.15.0" + "version": "1.17.0" }, { "name": "tbb", - "version": "2021.13.0" + "version": "2022.2.0" }, { "name": "spdlog", - "version": "1.14.1" + "version": "1.16.0" } ], - "builtin-baseline": "8ae59b5b1329a51875abc71d528da93d9c3e8972" + "builtin-baseline": "74e6536215718009aae747d86d84b78376bf9e09" } \ No newline at end of file