commit 2178d6a70c3e13fe78f01b7483b631dabc4a92db Author: ookami125 Date: Mon Jun 15 12:12:00 2026 -0400 Initial commit: Naut-Torrent — from-scratch 10 GbE BitTorrent client A maintainable, extensible BitTorrent client (C11, Linux/io_uring) targeting 10 GbE saturation. All torrent functionality is built from scratch; liburing is the only linked third-party dependency on the data path. Implements Phases 1-7 of the roadmap: - core: page-aligned buffer pool, MPMC/Treiber queues, bitfields, worker pool - crypto: SHA-1/256 (SHA-NI + scalar), Merkle (BEP-52), RC4 (MSE) - bencode/metainfo: zero-copy parser, v1/v2/hybrid .torrent + magnet - peer: sans-IO wire codec, MSE/PE handshake state machine, BEP-10, ut_metadata, PEX - piece/storage: block-level multi-peer engine, rarest-first + endgame, per-file completion events + single-file relocate (move-as-you-finish) - tracker/dht: HTTP + UDP (BEP-15) trackers, BEP-5 KRPC iterative lookup - platform: io_uring reactor (SQPOLL, registered buffers, SEND_ZC) - surface: versioned RPC, native plugin ABI, sandboxed Lua scripting, nautd/nautctl Verified against libtorrent (single/multi/hybrid, MSE, magnet-via-DHT, swarm); unit + interop tests green; ASan/UBSan/TSan clean. Scripting reference in docs/scripting.md. Co-Authored-By: Claude Opus 4.8 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..36c54f1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +# Build trees +/build/ +/build-*/ + +# Editor / tooling caches and generated compilation database +.cache/ +compile_commands.json + +# Test scratch +/tmp/ diff --git a/Big Buck Bunny.torrent b/Big Buck Bunny.torrent new file mode 100644 index 0000000..a7dbde0 Binary files /dev/null and b/Big Buck Bunny.torrent differ diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..7d4f7a6 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,282 @@ +cmake_minimum_required(VERSION 3.20) +project(naut_torrent C) + +set(CMAKE_C_STANDARD 11) +set(CMAKE_C_STANDARD_REQUIRED ON) +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) + +if(NOT CMAKE_BUILD_TYPE) + set(CMAKE_BUILD_TYPE Release) +endif() + +add_compile_definitions(_GNU_SOURCE) +add_compile_options(-Wall -Wextra -Wshadow -Wvla -Wpointer-arith + -fno-omit-frame-pointer) +set(CMAKE_C_FLAGS_RELEASE "-O3 -march=native -DNDEBUG") +set(CMAKE_C_FLAGS_DEBUG "-O0 -g3") + +# Sanitizer convenience build: -DNAUT_SAN=address|thread|undefined +if(NAUT_SAN) + add_compile_options(-fsanitize=${NAUT_SAN} -g) + add_link_options(-fsanitize=${NAUT_SAN}) +endif() + +include_directories(${CMAKE_SOURCE_DIR}/include) + +# --- liburing (Phase 1 platform backend) ------------------------------------ +find_library(URING_LIB uring) +find_path(URING_INC liburing.h) +if(NOT URING_LIB OR NOT URING_INC) + message(FATAL_ERROR "liburing not found (install liburing-dev)") +endif() +find_package(OpenSSL REQUIRED COMPONENTS Crypto) +find_package(PkgConfig REQUIRED) +pkg_check_modules(JANSSON REQUIRED IMPORTED_TARGET jansson) +pkg_check_modules(LUA REQUIRED IMPORTED_TARGET lua) + +# --- core: zero-dependency foundation --------------------------------------- +add_library(naut_core STATIC + src/core/common.c + src/core/buf.c + src/core/mpmc.c + src/core/bitfield.c + src/core/log.c + src/core/worker.c +) +target_link_libraries(naut_core PUBLIC pthread) + +# --- crypto: hashing + stream cipher (throughput-critical) ------------------ +add_library(naut_crypto STATIC + src/crypto/sha1.c + src/crypto/sha256.c + src/crypto/rc4.c + src/crypto/merkle.c +) +target_link_libraries(naut_crypto PUBLIC naut_core) + +# --- bencode: parser/encoder (fuzz target) ---------------------------------- +add_library(naut_bencode STATIC src/bencode/bencode.c) +target_link_libraries(naut_bencode PUBLIC naut_core) + +# --- metainfo: .torrent + magnet parsing ------------------------------------ +add_library(naut_metainfo STATIC src/metainfo/metainfo.c src/metainfo/magnet.c) +target_link_libraries(naut_metainfo PUBLIC naut_bencode naut_crypto) + +# --- tracker: HTTP + UDP announce (codec + blocking fetch) ------------------ +add_library(naut_tracker STATIC + src/tracker/tracker.c src/tracker/udp.c src/tracker/fetch.c) +target_link_libraries(naut_tracker PUBLIC naut_bencode) + +# --- dht: BEP-5 KRPC codec + bounded iterative peer lookup ------------------ +add_library(naut_dht STATIC src/dht/dht.c src/dht/fetch.c) +target_link_libraries(naut_dht PUBLIC naut_bencode naut_tracker) + +# --- peer: wire protocol codec (sans-IO) ------------------------------------ +add_library(naut_peer STATIC + src/peer/wire.c src/peer/extension.c src/peer/metadata.c src/peer/mse.c + src/peer/pipeline.c) +target_link_libraries(naut_peer PUBLIC + naut_core naut_crypto naut_bencode naut_tracker OpenSSL::Crypto m) + +# --- storage: file backend -------------------------------------------------- +add_library(naut_storage STATIC src/storage/storage.c) +target_link_libraries(naut_storage PUBLIC naut_core naut_metainfo) + +# --- piece: download state machine (request/assemble/verify/persist) -------- +add_library(naut_piece STATIC src/piece/piece.c) +target_link_libraries(naut_piece PUBLIC naut_storage naut_crypto naut_metainfo) + +# --- platform: the only code that touches the kernel ------------------------ +add_library(naut_platform STATIC + src/platform/uring.c + src/platform/net.c + src/platform/system.c +) +target_include_directories(naut_platform PUBLIC ${URING_INC}) +target_link_libraries(naut_platform PUBLIC naut_core ${URING_LIB}) + +# --- session + extensibility control plane --------------------------------- +add_library(naut_session STATIC src/session/event.c) +target_link_libraries(naut_session PUBLIC naut_core) + +# torrent registry: maps control-plane ids -> storage (move_file resolution) +add_library(naut_torrents STATIC src/session/session.c) +target_link_libraries(naut_torrents PUBLIC naut_storage) + +add_library(naut_rpc STATIC src/rpc/rpc.c) +target_link_libraries(naut_rpc PUBLIC + naut_session naut_core PkgConfig::JANSSON) + +add_library(naut_plugin STATIC src/plugin/plugin.c) +target_link_libraries(naut_plugin PUBLIC naut_rpc naut_session dl) + +add_library(naut_script STATIC src/script/script.c) +target_link_libraries(naut_script PUBLIC + naut_session naut_core PkgConfig::LUA) + +add_library(naut_example MODULE plugins/example/example.c) +target_include_directories(naut_example PRIVATE ${CMAKE_SOURCE_DIR}/include) +set_target_properties(naut_example PROPERTIES PREFIX "") + +# --- echo: Phase 1 gate (io_uring echo server on the buffer pool) ----------- +add_executable(naut_echo apps/echo/main.c) +target_link_libraries(naut_echo PRIVATE naut_platform naut_core) + +# --- leech: Phase 3 gate (single-peer download, byte-correct + verified) ---- +add_executable(naut_leech apps/leech/main.c) +target_link_libraries(naut_leech PRIVATE naut_piece naut_peer naut_metainfo) + +# --- swarm: Phase 4 gate (multi-peer download, rarest-first + endgame) ------- +add_executable(naut_swarm apps/swarm/main.c) +target_link_libraries(naut_swarm PRIVATE + naut_piece naut_peer naut_metainfo naut_tracker naut_dht naut_platform) + +# --- daemon + CLI: Phase 7 extensibility surface --------------------------- +add_executable(nautd apps/nautd/main.c) +target_link_libraries(nautd PRIVATE + naut_plugin naut_script naut_rpc naut_session naut_torrents naut_metainfo) + +add_executable(nautctl apps/nautctl/main.c) +target_link_libraries(nautctl PRIVATE naut_rpc) + +# --- tests ------------------------------------------------------------------ +enable_testing() +foreach(t test_buf test_mpmc test_bitfield) + add_executable(${t} tests/unit/${t}.c) + target_link_libraries(${t} PRIVATE naut_core) + add_test(NAME ${t} COMMAND ${t}) +endforeach() + +add_executable(test_worker tests/unit/test_worker.c) +target_link_libraries(test_worker PRIVATE naut_core naut_crypto) +add_test(NAME test_worker COMMAND test_worker) + +add_executable(test_pipeline tests/unit/test_pipeline.c) +target_link_libraries(test_pipeline PRIVATE naut_peer) +add_test(NAME test_pipeline COMMAND test_pipeline) + +add_executable(test_rpc tests/unit/test_rpc.c) +target_link_libraries(test_rpc PRIVATE naut_rpc) +add_test(NAME test_rpc COMMAND test_rpc) + +add_executable(test_plugin tests/unit/test_plugin.c) +target_link_libraries(test_plugin PRIVATE naut_plugin) +target_compile_definitions(test_plugin PRIVATE + NAUT_EXAMPLE_PLUGIN="$") +add_dependencies(test_plugin naut_example) +add_test(NAME test_plugin COMMAND test_plugin) + +add_executable(test_script tests/unit/test_script.c) +target_link_libraries(test_script PRIVATE naut_script) +target_compile_definitions(test_script PRIVATE + NAUT_PHASE7_SCRIPT="${CMAKE_SOURCE_DIR}/tests/fixtures/phase7.lua") +add_test(NAME test_script COMMAND test_script) + +# --- benchmarks ------------------------------------------------------------- +add_executable(bench_hash tests/bench/bench_hash.c) +target_link_libraries(bench_hash PRIVATE naut_crypto) + +add_executable(bench_scale tests/bench/bench_scale.c) +target_link_libraries(bench_scale PRIVATE naut_core naut_crypto) + +# --- fuzz-lite: mutational fuzzer (run under -DNAUT_SAN=address for value) --- +add_executable(fuzz_lite tests/fuzz/fuzz_lite.c) +target_link_libraries(fuzz_lite PRIVATE naut_metainfo) + +add_executable(test_bencode tests/unit/test_bencode.c) +target_link_libraries(test_bencode PRIVATE naut_bencode) +add_test(NAME test_bencode COMMAND test_bencode) + +add_executable(test_metainfo tests/unit/test_metainfo.c) +target_link_libraries(test_metainfo PRIVATE naut_metainfo) +target_compile_definitions(test_metainfo PRIVATE + NAUT_FIXTURES="${CMAKE_SOURCE_DIR}/tests/fixtures") +add_test(NAME test_metainfo COMMAND test_metainfo) + +add_executable(test_peer tests/unit/test_peer.c) +target_link_libraries(test_peer PRIVATE naut_peer) +add_test(NAME test_peer COMMAND test_peer) + +add_executable(test_extension tests/unit/test_extension.c) +target_link_libraries(test_extension PRIVATE naut_peer) +add_test(NAME test_extension COMMAND test_extension) + +add_executable(test_mse tests/unit/test_mse.c) +target_link_libraries(test_mse PRIVATE naut_peer) +add_test(NAME test_mse COMMAND test_mse) + +add_executable(test_tracker tests/unit/test_tracker.c) +target_link_libraries(test_tracker PRIVATE naut_tracker) +add_test(NAME test_tracker COMMAND test_tracker) + +add_executable(test_dht tests/unit/test_dht.c) +target_link_libraries(test_dht PRIVATE naut_dht) +add_test(NAME test_dht COMMAND test_dht) + +add_executable(test_storage tests/unit/test_storage.c) +target_link_libraries(test_storage PRIVATE naut_storage) +add_test(NAME test_storage COMMAND test_storage) + +add_executable(test_download tests/unit/test_download.c) +target_link_libraries(test_download PRIVATE naut_piece) +target_compile_definitions(test_download PRIVATE + NAUT_FIXTURES="${CMAKE_SOURCE_DIR}/tests/fixtures") +add_test(NAME test_download COMMAND test_download) + +add_executable(test_filemove tests/unit/test_filemove.c) +target_link_libraries(test_filemove PRIVATE naut_piece) +target_compile_definitions(test_filemove PRIVATE + NAUT_FIXTURES="${CMAKE_SOURCE_DIR}/tests/fixtures") +add_test(NAME test_filemove COMMAND test_filemove) + +add_executable(test_picker tests/unit/test_picker.c) +target_link_libraries(test_picker PRIVATE naut_piece) +add_test(NAME test_picker COMMAND test_picker) + +# Phase 3 interop gate: download from a real libtorrent seed (SKIPs without it). +add_test(NAME interop_leech + COMMAND bash ${CMAKE_SOURCE_DIR}/tests/integration/run_interop.sh $) +set_tests_properties(interop_leech PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 180) + +add_test(NAME interop_mse + COMMAND bash ${CMAKE_SOURCE_DIR}/tests/integration/run_mse.sh $) +set_tests_properties(interop_mse PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 60) + +add_test(NAME interop_magnet_dht + COMMAND bash ${CMAKE_SOURCE_DIR}/tests/integration/run_magnet_dht.sh + $) +set_tests_properties(interop_magnet_dht PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 60) + +# Phase 4 interop gate: both libtorrent seeds must contribute to one download. +add_test(NAME interop_swarm + COMMAND bash ${CMAKE_SOURCE_DIR}/tests/integration/run_swarm.sh $) +set_tests_properties(interop_swarm PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 60) + +add_test(NAME interop_tracker_swarm + COMMAND bash ${CMAKE_SOURCE_DIR}/tests/integration/run_tracker_swarm.sh + $ http) +set_tests_properties(interop_tracker_swarm PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 60) + +add_test(NAME interop_udp_tracker_swarm + COMMAND bash ${CMAKE_SOURCE_DIR}/tests/integration/run_tracker_swarm.sh + $ udp) +set_tests_properties(interop_udp_tracker_swarm PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 60) + +add_test(NAME interop_echo_scale + COMMAND bash ${CMAKE_SOURCE_DIR}/tests/integration/run_echo_scale.sh + $) +set_tests_properties(interop_echo_scale PROPERTIES TIMEOUT 30) + +add_test(NAME phase7_extensibility + COMMAND bash ${CMAKE_SOURCE_DIR}/tests/integration/run_phase7.sh + $ $ + $ + ${CMAKE_SOURCE_DIR}/tests/fixtures/phase7.lua) +set_tests_properties(phase7_extensibility PROPERTIES TIMEOUT 15) + +add_executable(test_crypto tests/unit/test_crypto.c) +target_link_libraries(test_crypto PRIVATE naut_crypto) +add_test(NAME test_crypto COMMAND test_crypto) +# Same vectors, scalar SHA-256 path forced, to prove both backends agree. +add_test(NAME test_crypto_scalar COMMAND test_crypto) +set_tests_properties(test_crypto_scalar PROPERTIES ENVIRONMENT "NAUT_NO_SHANI=1") diff --git a/README.md b/README.md new file mode 100644 index 0000000..8422ade --- /dev/null +++ b/README.md @@ -0,0 +1,216 @@ +# Naut-Torrent + +A maintainable, extensible BitTorrent client engineered to **saturate a 10 GbE +link (~1.25 GB/s) in both directions** on Linux. Written in C11. + +- **Platform:** Linux-only, built hard around **io_uring** (network *and* disk). +- **Protocol:** BitTorrent **v1 + v2 hybrid** (SHA-1 pieces + SHA-256 Merkle). +- **Workload:** saturate download *and* upload, with **MSE/PE encryption** in the hot path. +- **Extensible:** clean module boundaries + control **RPC** + **BEP-10** extensions + + a versioned native **plugin ABI** + embedded **scripting**. + +The full architecture rationale lives in [`plan.md`](plan.md). The short version: a +**shared-nothing, thread-per-core reactor** model with **offload pools** for +hashing/crypto, and a **one-buffer-zero-copy** data path so bytes flow +recv → decrypt → hash → disk/send without a single `memcpy`. + +## Layout + +``` +include/naut/ public headers +src/core/ zero-dependency foundation (buffers, queues, bitfields, log) +src/platform/ the ONLY code that touches the kernel (io_uring, sockets) +src/dht/ BEP-5 KRPC codec + bounded iterative peer discovery +src/peer/ wire protocol, MSE/RC4, BEP-10, ut_metadata, and PEX +apps/echo/ Phase 1 gate: io_uring echo server on the buffer pool +apps/leech/ Phase 3 gate: verified single-peer download +apps/swarm/ tracker/DHT discovery, magnets, and concurrent peers +tests/unit/ unit + concurrency tests +``` + +## Build, test, run + +```sh +cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release +ninja -C build +ctest --test-dir build --output-on-failure + +# sanitizer build (address|thread|undefined) +cmake -S . -B build-tsan -G Ninja -DCMAKE_BUILD_TYPE=Debug -DNAUT_SAN=thread +ninja -C build-tsan && ./build-tsan/test_buf + +# run the Phase 1 echo gate +./build/naut_echo 9000 + +# download from explicit peers, or omit them to use the torrent's trackers +./build/naut_swarm file.torrent output/ 192.0.2.10:6881 192.0.2.11:6881 +./build/naut_swarm file.torrent output/ + +# trackerless magnet start through DHT (override bootstraps when needed) +./build/naut_swarm 'magnet:?xt=urn:btih:...' output/ +NAUT_DHT_BOOTSTRAP=127.0.0.1:6881 ./build/naut_swarm 'magnet:?xt=urn:btih:...' output/ + +# force an encrypted single-peer MSE/RC4 connection +./build/naut_leech --mse file.torrent output/ 192.0.2.10 6881 + +# Phase 6 CPU and reactor benchmarks +./build/bench_hash +./build/bench_scale 8 8 +bash tests/integration/run_echo_scale.sh ./build/naut_echo + +# optional data-path tuning +NAUT_DIRECT_IO=1 NAUT_WORKERS=8 ./build/naut_swarm file.torrent output/ +NAUT_CPU=2 NAUT_SQPOLL=1 NAUT_HUGEPAGES=1 NAUT_NUMA_NODE=0 ./build/naut_echo 9000 +``` + +Requirements: Linux ≥ 6.0, `liburing` (≥ 2.x), OpenSSL `libcrypto`, Jansson, +Lua, CMake ≥ 3.20, gcc/clang, Ninja. + +## Daemon, RPC, plugins, and scripts + +Phase 7 adds a headless control process and thin CLI over a versioned, +length-prefixed JSON protocol on a Unix socket: + +```sh +./build/nautd \ + --socket /tmp/nautd.sock \ + --plugin ./build/naut_example.so \ + --script ./tests/fixtures/phase7.lua + +./build/nautctl ping +./build/nautctl plugins +./build/nautctl status +./build/nautctl events +``` + +`nautctl` accepts an optional JSON value after the method: + +```sh +# register a torrent's storage so a move command can resolve + relocate its files +./build/nautctl add_torrent \ + '{"torrent_id":7,"torrent":"file.torrent","root":"output/"}' +./build/nautctl emit \ + '{"type":"torrent_finished","torrent_id":7}' +./build/nautctl shutdown +``` + +The native ABI is declared in `include/naut/naut_plugin.h`. Plugins export +`naut_plugin_register()`, receive the versioned host API, and may register RPC +methods, storage backends, and event handlers. `plugins/example/example.c` +provides the reference in-memory storage backend. + +Lua scripts run on a dedicated thread behind a bounded event queue. Supported +hooks are `on_torrent_added`, `on_piece_complete`, `on_file_complete`, +`on_torrent_finished`, `on_peer_connected`, and `on_alert`. The sandbox removes +filesystem, process, package-loading, debug, and raw chunk-loading globals +(`os`, `io`, `package`/`require`, `debug`, `dofile`/`loadfile`, and +`load`/`loadstring` — the bytecode loaders are denied so a crafted binary chunk +can't escape the VM). `naut.move_file()` submits a bounded command from the +script thread to the daemon owner thread; the owner resolves it through the +torrent registry (`naut_session`) and performs the relocate with +`naut_storage_relocate()`. Register a torrent's storage first with the +`add_torrent` RPC so the id resolves. `phase7_extensibility` drives this +end to end and asserts the file actually moves on disk. + +The full script-visible surface — every event hook, the `event` object's +fields, and the `naut` API table — is documented in +[`docs/scripting.md`](docs/scripting.md). + +## Roadmap (status) + +| Phase | Scope | State | +|------|-------|-------| +| 1 | Foundation: `platform/` io_uring + `core/` buffer pool/queues/bitfields | **done — built, tested, TSan-clean; echo ~30 Gbit/s on one core** | +| 2 | `crypto/` (SHA-1/256 + SHA-NI, Merkle, RC4) · `bencode/` · `metainfo/` (v1/v2/hybrid + magnet) | **done — FIPS/RC4 vectors pass, SHA-256 2.27 GB/s/core, info-hashes verified vs libtorrent, parsers fuzz-clean (3M iters, ASan+UBSan)** | +| 3 | Single-peer transfer: `peer/` · `piece/` · `storage/` · `verify/` | **done — `naut_leech` downloads single/multi/hybrid from a libtorrent seed, byte-identical + SHA-1 verified (`interop_leech` test)** | +| 4 | Trackers + swarm: HTTP/UDP trackers, choking, rarest-first, endgame | **done — `naut_swarm` discovers peers or accepts explicit endpoints; two-peer and live HTTP/UDP tracker interop gates pass against libtorrent** | +| 5 | MSE encryption · DHT · PEX · ut_metadata · magnet-only start | **done — forced RC4 interop passes against libtorrent; trackerless magnet gate discovers a peer through DHT, verifies BEP-9 metadata, and completes byte-identically** | +| 6 | Scale to 10 GbE: adaptive pipelining, SEND_ZC, registered bufs, SQPOLL, NUMA | **implementation complete; local 8-connection echo gate measured 15.11 Gbit/s with byte verification, while the plan's two-machine ≥9.4 Gbit/s NIC gate remains external hardware validation** | +| 7 | Extensibility surface: RPC · plugin ABI · scripting · `nautctl` | **done — versioned Unix RPC with event streaming, reference storage plugin, sandboxed Lua hooks, bounded move command marshalling, and an end-to-end integration gate** | + +## Foundation design notes + +- **`naut_buf` pool** (`src/core/buf.c`): page-aligned, refcounted blocks from + one mmap'd slab. `get()` is single-consumer (owning reactor); `put()`/`ref()` + are multi-producer. Because only the owner pops, the Treiber-stack freelist is + ABA-free without tagging. The whole slab can be registered with io_uring as a + fixed-buffer region. +- **`naut_mpmc`** (`src/core/mpmc.c`): Vyukov bounded queue — one implementation + serves every cross-thread hand-off (control→reactor, reactor→hash-pool, back). +- **`naut_bitfield`** (`src/core/bitfield.c`): popcount/ctz-based; includes the + BEP-3 MSB-first wire conversion that the peer protocol needs. +- **`platform/`** wraps every syscall so "Linux-only now" stays "portable later": + a future epoll backend is a new file, not a refactor. +- **`crypto/`**: SHA-256 picks a SHA-NI or scalar backend at startup + (`NAUT_NO_SHANI=1` forces scalar); both are cross-checked against FIPS vectors + in CI. RC4 carries the MSE 1024-byte keystream drop. Merkle implements BEP-52 + zero-hash padding. *Deferred by design:* MSE Diffie-Hellman lands in Phase 5 + (where it's used); a SHA-NI SHA-1 path is a Phase 6 optimization for + v1-heavy swarms (scalar SHA-1 is ~0.27 GB/s, fine behind the hash pool). +- **`metainfo/`**: info-hashes are computed over the raw `info` bytes and were + verified against libtorrent for v1, v2, and hybrid. Fixtures are regenerated + with `python3 tests/fixtures/generate.py` (needs python `libtorrent`). +- **Fuzzing**: `fuzz_lite [seeds...]` is a mutational + fuzzer; build with `-DNAUT_SAN=address` and seed from `tests/fixtures/*.torrent`. +- **`peer/`**: a *sans-IO* wire codec — pure functions over byte buffers, no + sockets — so the same code is driven by the blocking `naut_leech` now and the + io_uring reactor in Phase 6. `naut_leech ` + downloads from one peer; a piece is assembled in RAM, SHA-1 verified, then + written, so a corrupt piece never reaches disk. +- **`tracker/` + `naut_swarm`**: BEP-3 compact HTTP and BEP-15 UDP announces + feed a deduplicated peer set. The swarm driver tracks HAVE/BITFIELD + availability, respects choke/unchoke, expires stalled requests, schedules + rarest-first, and bounds endgame races to two distinct peers per block. + Redundant requests are canceled as soon as one copy arrives. `interop_swarm` + requires two independent libtorrent seeds to contribute, while + `interop_tracker_swarm` and `interop_udp_tracker_swarm` prove live discovery. +- **Phase 5 peer discovery and transport**: outgoing MSE is a *sans-IO* state + machine (`naut_mse_handshake_*`) — feed bytes, pull bytes, no sockets — so the + io_uring reactor can drive an encrypted handshake without blocking a core; the + blocking `naut_mse_client_handshake` is a thin wrapper over it. It performs the + 768-bit Diffie-Hellman exchange, offers RC4-only PE, drops the first 1024 + keystream bytes, and keeps independent connection-owned send/receive states. + `naut_dht` builds and validates BEP-5 KRPC and performs a bounded iterative + IPv4 `get_peers` lookup. BEP-10 handshakes advertise `ut_metadata` and PEX; + metadata is assembled in 16 KiB blocks and rejected unless its raw SHA-1 + matches the magnet's `btih`. `interop_mse` and `interop_magnet_dht` are the + deterministic local gates for both required Phase 5 outcomes. +- **Phase 6 scaling path**: each swarm peer adjusts its request window from + smoothed throughput × RTT rather than a fixed depth. Completed pieces submit + SHA-1 jobs to a bounded MPMC worker pool and return to the owner through + eventfd; storage writes and callbacks remain owner-thread operations. + Piece buffers are page-aligned, and `NAUT_DIRECT_IO=1` uses O_DIRECT for + aligned bulk regions with buffered edge fallback. The io_uring seam supports + SQPOLL CPU affinity, registered slabs, fixed-buffer receive where the kernel + accepts it, and SEND_ZC with notification-lifetime tracking. Two + compatibility notes baked in: `IORING_SETUP_SQPOLL` is mutually exclusive with + `COOP_TASKRUN` (the kernel `-EINVAL`s the combo), so the ring pairs SQPOLL with + `SINGLE_ISSUER` only; and a kernel that rejects `IORING_RECVSEND_FIXED_BUF` on + plain recv is detected per-operation and every affected connection retries + unfixed (not just the first), so fixed-buffer fallback never tears a peer down. + `run_echo_scale.sh` asserts the server echoed *every* byte across all + connections, turning any such drop into a hard failure. If a transport + repeatedly reports copied SEND_ZC operations (as loopback does), the ring + degrades to normal sends instead of paying useless notification overhead. + Hugepage and NUMA slab placement are controlled by `NAUT_HUGEPAGES` and + `NAUT_NUMA_NODE`; worker count/affinity use `NAUT_WORKERS` and + `NAUT_WORKER_CPU_BASE`. +- **Measured Phase 6 CPU budget on this host** (8 workers): SHA-1 1.62 GB/s, + SHA-256 14.75 GB/s, and RC4 3.21 GB/s. The single-core SHA-256 gate measured + 1.87 GB/s. These clear the 1.25 GB/s per-direction processing budget, but do + not replace the physical 10 GbE two-host test required by `plan.md`. +- **`storage/`** maps the torrent's flat byte space across files (a write may + straddle a file boundary) and recognises BEP-47 padding files in hybrid + torrents, routing them out of the content tree. Interop is proven against + libtorrent via `tests/integration/run_interop.sh` (also run by ctest). +- **Per-file completion / move-as-you-go** (a headline feature): `naut_download` + fires `on_file_complete(file_index, path)` the instant a file's last covering + piece verifies — before the torrent finishes — and `naut_storage_relocate()` + moves that file out safely (even mid-download, while other files' pieces are + still arriving). `test_filemove` proves a file is relocated mid-download with + no corruption. The scripting layer (Phase 7) forwards the event to an + `on_file_complete` hook and exposes `move_file`; the daemon resolves the + command through the `naut_session` torrent registry (`src/session/session.c`) + and calls `naut_storage_relocate()` on its owner thread. `phase7_extensibility` + exercises the whole chain — script thread → bounded queue → owner thread → + storage — and asserts the file moves on disk. diff --git a/apps/echo/main.c b/apps/echo/main.c new file mode 100644 index 0000000..a27b579 --- /dev/null +++ b/apps/echo/main.c @@ -0,0 +1,214 @@ +/* naut_echo — Phase 1 gate. + * + * A single-reactor io_uring echo server that proves the foundation works end to + * end: multishot accept, recv/send driven entirely off the page-aligned buffer + * pool with ZERO per-operation allocation in steady state. Throughput on + * loopback should be limited by memory bandwidth / the single core, not by the + * allocator or syscalls. + * + * It is intentionally one-in-flight-op-per-connection (recv -> send -> recv). + * The real peer reactor (later phase) uses multishot recv + provided buffers + * and pipelines; this is the minimal honest exercise of the primitives. + * + * usage: naut_echo [port] (default 9000) + */ +#include "naut/uring.h" +#include "naut/net.h" +#include "naut/buf.h" +#include "naut/log.h" +#include "naut/system.h" + +#include +#include +#include +#include +#include +#include +#include + +#define ECHO_BLOCK (128u * 1024u) +#define ECHO_BUFS 4096u +#define RING_ENTRIES 4096u + +/* user_data tagging: low 3 bits = op, high bits = conn* (16-byte aligned). */ +enum { TAG_ACCEPT = 1, TAG_RECV = 2, TAG_SEND = 3 }; +#define UD(p, tag) ((__u64)(uintptr_t)(p) | (unsigned)(tag)) +#define UD_TAG(ud) ((unsigned)((ud) & 0x7u)) +#define UD_PTR(ud) ((conn *)(uintptr_t)((ud) & ~(__u64)0x7u)) + +typedef struct conn { + int fd; + uint32_t sent; /* bytes of buf->len already written (partial sends) */ + naut_buf *buf; + bool awaiting_notif; + bool recv_fixed; /* the in-flight recv used the fixed buffer */ +} conn; + +static volatile sig_atomic_t g_stop = 0; +static void on_signal(int s) { (void)s; g_stop = 1; } + +static naut_bufpool *g_pool; +static _Atomic uint64_t g_bytes = 0, g_conns = 0, g_zc_copied = 0; + +static void arm_recv(naut_ring *owner, conn *c) { + struct io_uring_sqe *sqe = io_uring_get_sqe(&owner->ring); + c->recv_fixed = + naut_ring_prep_recv(owner, sqe, c->fd, c->buf->data, c->buf->cap, 0); + io_uring_sqe_set_data64(sqe, UD(c, TAG_RECV)); +} + +static void arm_send(naut_ring *owner, conn *c) { + struct io_uring_sqe *sqe = io_uring_get_sqe(&owner->ring); + c->awaiting_notif = naut_ring_prep_send( + owner, sqe, c->fd, c->buf->data + c->sent, + c->buf->len - c->sent, MSG_NOSIGNAL, true); + io_uring_sqe_set_data64(sqe, UD(c, TAG_SEND)); +} + +static void conn_close(conn *c) { + close(c->fd); + naut_buf_put(c->buf); + free(c); +} + +int main(int argc, char **argv) { + uint16_t port = (argc > 1) ? (uint16_t)atoi(argv[1]) : 9000; + int cpu = getenv("NAUT_CPU") ? atoi(getenv("NAUT_CPU")) : -1; + int numa_node = + getenv("NAUT_NUMA_NODE") ? atoi(getenv("NAUT_NUMA_NODE")) : -1; + bool sqpoll = getenv("NAUT_SQPOLL") != NULL; + bool hugepages = getenv("NAUT_HUGEPAGES") != NULL; + signal(SIGINT, on_signal); + signal(SIGTERM, on_signal); + signal(SIGPIPE, SIG_IGN); + + if (cpu >= 0 && naut_pin_current_thread(cpu) != NAUT_OK) + NAUT_WARN("failed to pin reactor to CPU %d", cpu); + naut_ring r; + if (naut_ring_init_cpu(&r, RING_ENTRIES, sqpoll, cpu) != NAUT_OK) + return 1; + if (naut_ring_probe(&r) != NAUT_OK) { naut_ring_close(&r); return 1; } + struct io_uring *ring = &r.ring; + + int lfd = naut_net_listen(port, 1024, true); + if (lfd < 0) { naut_ring_close(&r); return 1; } + + g_pool = naut_bufpool_create_on_node( + ECHO_BLOCK, ECHO_BUFS, hugepages, numa_node); + if (!g_pool) { close(lfd); naut_ring_close(&r); return 1; } + (void)naut_ring_register_bufpool(&r, g_pool); + + /* prime the multishot accept */ + struct io_uring_sqe *sqe = io_uring_get_sqe(ring); + io_uring_prep_multishot_accept(sqe, lfd, NULL, NULL, 0); + io_uring_sqe_set_data64(sqe, UD(NULL, TAG_ACCEPT)); + + NAUT_INFO("echo listening on :%u", port); + + while (!g_stop) { + int rc = io_uring_submit_and_wait(ring, 1); + if (rc < 0 && rc != -EINTR) { NAUT_ERROR("submit_and_wait: %s", strerror(-rc)); break; } + + unsigned head, count = 0; + struct io_uring_cqe *cqe; + io_uring_for_each_cqe(ring, head, cqe) { + count++; + __u64 ud = cqe->user_data; + int res = cqe->res; + + switch (UD_TAG(ud)) { + case TAG_ACCEPT: { + if (res < 0) { + if (res != -ECANCELED) NAUT_WARN("accept: %s", strerror(-res)); + } else { + int cfd = res; + naut_net_tune_peer(cfd); + naut_buf *b = naut_buf_get(g_pool); + if (!b) { NAUT_WARN("pool exhausted, dropping conn"); close(cfd); } + else { + conn *c = calloc(1, sizeof(*c)); + c->fd = cfd; c->buf = b; + atomic_fetch_add(&g_conns, 1); + arm_recv(&r, c); + } + } + /* re-arm if the kernel dropped the multishot registration */ + if (!(cqe->flags & IORING_CQE_F_MORE)) { + struct io_uring_sqe *s = io_uring_get_sqe(ring); + io_uring_prep_multishot_accept(s, lfd, NULL, NULL, 0); + io_uring_sqe_set_data64(s, UD(NULL, TAG_ACCEPT)); + } + break; + } + case TAG_RECV: { + conn *c = UD_PTR(ud); + if (res <= 0) { + /* A fixed-buffer recv rejected with -EINVAL means this + * kernel doesn't support IORING_RECVSEND_FIXED_BUF on plain + * recv. Disable it ring-wide and retry THIS connection + * unfixed. We key off the per-conn flag, not the ring flag, + * so every connection that armed a fixed recv before the + * flag flipped recovers too (otherwise all but the first + * would be torn down). */ + if (res == -EINVAL && c->recv_fixed) { + if (r.recv_fixed) { + NAUT_WARN("fixed-buffer recv unsupported at runtime; " + "falling back to normal recv"); + r.recv_fixed = false; + } + arm_recv(&r, c); + break; + } + if (res < 0) + NAUT_WARN("recv completion: %s", strerror(-res)); + conn_close(c); + break; + } + c->buf->len = (uint32_t)res; + c->sent = 0; + arm_send(&r, c); + break; + } + case TAG_SEND: { + conn *c = UD_PTR(ud); + if (cqe->flags & IORING_CQE_F_NOTIF) { + if (res & IORING_NOTIF_USAGE_ZC_COPIED) { + uint64_t copied = + atomic_fetch_add(&g_zc_copied, 1) + 1; + if (copied == 8) { + NAUT_WARN("SEND_ZC is copying on this transport; " + "disabling it for this ring"); + r.send_zc = false; + } + } + c->awaiting_notif = false; + if (c->sent < c->buf->len) arm_send(&r, c); + else { c->buf->len = 0; arm_recv(&r, c); } + break; + } + if (res <= 0) { conn_close(c); break; } + c->sent += (uint32_t)res; + atomic_fetch_add(&g_bytes, (uint64_t)res); + if (!c->awaiting_notif) { + if (c->sent < c->buf->len) arm_send(&r, c); + else { c->buf->len = 0; arm_recv(&r, c); } + } + break; + } + default: + NAUT_PANIC("bad user_data tag %u", UD_TAG(ud)); + } + } + io_uring_cq_advance(ring, count); + } + + NAUT_INFO("shutting down: %llu conns, %llu bytes echoed, %llu SEND_ZC copied notifications", + (unsigned long long)atomic_load(&g_conns), + (unsigned long long)atomic_load(&g_bytes), + (unsigned long long)atomic_load(&g_zc_copied)); + close(lfd); + naut_ring_unregister_buffers(&r); + naut_bufpool_destroy(g_pool); + naut_ring_close(&r); + return 0; +} diff --git a/apps/leech/main.c b/apps/leech/main.c new file mode 100644 index 0000000..cb980cc --- /dev/null +++ b/apps/leech/main.c @@ -0,0 +1,212 @@ +/* naut_leech — Phase 3 gate: download a torrent from a single peer and write a + * byte-correct, hash-verified file to disk. + * + * Blocking-socket driver around the sans-IO peer codec + download engine. The + * point of this phase is protocol correctness and interop (it downloads from a + * libtorrent seed in the integration test), not peak throughput — the io_uring + * reactor that drives thousands of these comes in Phase 6. + * + * usage: naut_leech [--mse] + */ +#include "naut/metainfo.h" +#include "naut/storage.h" +#include "naut/piece.h" +#include "naut/peer.h" +#include "naut/mse.h" +#include "naut/log.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define PIPELINE_DEPTH 512 /* outstanding requests (~8 MiB in flight) */ + +static double now(void) { + struct timespec t; clock_gettime(CLOCK_MONOTONIC, &t); + return t.tv_sec + t.tv_nsec * 1e-9; +} + +static uint8_t *slurp(const char *path, size_t *len) { + FILE *f = fopen(path, "rb"); + if (!f) { NAUT_ERROR("open %s: %s", path, strerror(errno)); return NULL; } + fseek(f, 0, SEEK_END); long n = ftell(f); fseek(f, 0, SEEK_SET); + uint8_t *b = malloc(n); + if (fread(b, 1, n, f) != (size_t)n) { fclose(f); free(b); return NULL; } + fclose(f); *len = (size_t)n; return b; +} + +static int connect_peer(const char *ip, uint16_t port) { + int fd = socket(AF_INET, SOCK_STREAM, 0); + if (fd < 0) return -1; + struct sockaddr_in a; memset(&a, 0, sizeof a); + a.sin_family = AF_INET; a.sin_port = htons(port); + if (inet_pton(AF_INET, ip, &a.sin_addr) != 1) { close(fd); return -1; } + if (connect(fd, (struct sockaddr *)&a, sizeof a) != 0) { + NAUT_ERROR("connect %s:%u: %s", ip, port, strerror(errno)); + close(fd); return -1; + } + int one = 1; setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &one, sizeof one); + return fd; +} + +/* send up to PIPELINE_DEPTH outstanding requests */ +static bool refill(int fd, naut_mse_stream *mse, + naut_download *d, int *outstanding) { + uint32_t idx, begin, len; + while (*outstanding < PIPELINE_DEPTH) { + if (!naut_download_next_request(d, &idx, &begin, &len)) break; + uint8_t req[17]; + naut_peer_msg_request(req, idx, begin, len); + if (!naut_mse_send_all(fd, mse, req, sizeof req)) return false; + (*outstanding)++; + } + return true; +} + +int main(int argc, char **argv) { + bool use_mse = argc > 1 && strcmp(argv[1], "--mse") == 0; + int arg = use_mse ? 2 : 1; + if (argc - arg != 4) { + fprintf(stderr, "usage: %s [--mse] \n", + argv[0]); + return 2; + } + naut_log_set_level(NAUT_LOG_INFO); + + size_t tlen; + uint8_t *tor = slurp(argv[arg], &tlen); + if (!tor) return 1; + naut_metainfo mi; + if (naut_metainfo_parse(tor, tlen, &mi) != NAUT_OK) { NAUT_ERROR("bad torrent"); return 1; } + free(tor); + + char hex[41]; naut_infohash_v1_hex(&mi, hex); + NAUT_INFO("torrent '%s': %u pieces, %lld bytes, infohash %s", + mi.name, mi.num_pieces, (long long)mi.total_length, hex); + + naut_err err; + naut_storage_opts storage_opts = { + .direct_io = getenv("NAUT_DIRECT_IO") != NULL, + .preallocate = true, + }; + naut_storage *st = naut_storage_open_opts( + mi.files, mi.num_files, argv[arg + 1], &storage_opts, &err); + if (!st) { NAUT_ERROR("storage: %s", naut_strerror(err)); return 1; } + naut_download *d = naut_download_create(&mi, st); + if (!d) return 1; + + int fd = connect_peer(argv[arg + 2], (uint16_t)atoi(argv[arg + 3])); + if (fd < 0) return 1; + + /* handshake */ + uint8_t peerid[20]; memcpy(peerid, "-NT0001-", 8); + for (int i = 8; i < 20; i++) peerid[i] = (uint8_t)(rand() & 0xff); + uint8_t hs[NAUT_HANDSHAKE_LEN]; + naut_peer_handshake_build(hs, mi.infohash_v1, peerid, 0); + naut_mse_stream mse = {0}; + uint8_t remote_hs[NAUT_HANDSHAKE_LEN]; + bool hs_done = false; + if (use_mse) { + naut_err mse_err = naut_mse_client_handshake( + fd, mi.infohash_v1, peerid, 0, &mse, remote_hs); + if (mse_err != NAUT_OK) { + NAUT_ERROR("MSE handshake failed: %s", naut_strerror(mse_err)); + return 1; + } + hs_done = true; + NAUT_INFO("MSE/RC4 peer transport established"); + uint8_t intr[5]; + naut_peer_msg_simple(intr, NAUT_MSG_INTERESTED); + if (!naut_mse_send_all(fd, &mse, intr, sizeof intr)) { + NAUT_ERROR("interested send failed"); + return 1; + } + } else if (!naut_mse_send_all(fd, &mse, hs, sizeof hs)) { + NAUT_ERROR("handshake send failed"); + return 1; + } + + /* recv buffer */ + size_t cap = 4u << 20, len = 0; + uint8_t *buf = malloc(cap); + bool unchoked = false; + int outstanding = 0; + double t0 = now(); + + while (!naut_download_complete(d)) { + if (len == cap) { cap *= 2; buf = realloc(buf, cap); } + ssize_t r = naut_mse_recv(fd, &mse, buf + len, cap - len); + if (r < 0) { NAUT_ERROR("recv: %s", strerror(errno)); break; } + if (r == 0) { NAUT_ERROR("peer closed (%.1f%% done)", + 100.0 * naut_download_pieces_done(d) / mi.num_pieces); break; } + len += (size_t)r; + + size_t pos = 0; + if (!hs_done) { + if (len < NAUT_HANDSHAKE_LEN) continue; + uint8_t ih[20], pid[20]; + if (!naut_peer_handshake_parse(buf, ih, pid, NULL) || + memcmp(ih, mi.infohash_v1, 20) != 0) { + NAUT_ERROR("handshake mismatch"); break; + } + pos = NAUT_HANDSHAKE_LEN; + hs_done = true; + uint8_t intr[5]; naut_peer_msg_simple(intr, NAUT_MSG_INTERESTED); + if (!naut_mse_send_all(fd, &mse, intr, 5)) break; + } + + /* parse all complete messages */ + for (;;) { + naut_msg m; + int c = naut_peer_msg_parse(buf + pos, len - pos, &m); + if (c == 0) break; + if (c < 0) { NAUT_ERROR("protocol error"); goto done; } + pos += (size_t)c; + switch (m.type) { + case NAUT_MSG_UNCHOKE: unchoked = true; break; + case NAUT_MSG_CHOKE: unchoked = false; break; + case NAUT_MSG_PIECE: { + outstanding--; + bool pdone = false; + naut_err e = naut_download_on_block(d, m.index, m.begin, m.payload, + (uint32_t)m.payload_len, &pdone); + if (e != NAUT_OK) { NAUT_ERROR("block rejected: %s", naut_strerror(e)); goto done; } + break; + } + default: break; /* bitfield/have/keepalive/port: ignore for a seed */ + } + } + /* compact consumed bytes */ + memmove(buf, buf + pos, len - pos); + len -= pos; + + if (unchoked && !refill(fd, &mse, d, &outstanding)) { + NAUT_ERROR("request send failed"); break; + } + } +done:; + double dt = now() - t0; + bool ok = naut_download_complete(d); + if (ok) { + double mb = (double)mi.total_length / 1e6; + NAUT_INFO("COMPLETE: %u/%u pieces, %.1f MB in %.2fs (%.1f MB/s), all SHA-1 verified", + naut_download_pieces_done(d), mi.num_pieces, mb, dt, mb / dt); + } else { + NAUT_ERROR("INCOMPLETE: %u/%u pieces", naut_download_pieces_done(d), mi.num_pieces); + } + + naut_storage_sync(st); + close(fd); + naut_download_destroy(d); + naut_storage_close(st); + naut_metainfo_free(&mi); + free(buf); + return ok ? 0 : 1; +} diff --git a/apps/nautctl/main.c b/apps/nautctl/main.c new file mode 100644 index 0000000..a009c11 --- /dev/null +++ b/apps/nautctl/main.c @@ -0,0 +1,93 @@ +#include "naut/rpc.h" + +#include +#include +#include +#include +#include +#include + +#define DEFAULT_SOCKET "/tmp/nautd.sock" + +static void usage(const char *program) { + fprintf(stderr, + "usage: %s [--socket PATH] METHOD [PARAMS_JSON]\n" + " %s [--socket PATH] events\n", program, program); +} + +static json_t *parse_params(const char *text) { + if (!text) return json_object(); + json_error_t error; + return json_loads(text, JSON_REJECT_DUPLICATES | JSON_DECODE_ANY, &error); +} + +static int print_json(json_t *json) { + if (!json) return 1; + if (json_dumpf(json, stdout, JSON_INDENT(2) | JSON_SORT_KEYS) != 0) + return 1; + putchar('\n'); + return 0; +} + +static int stream_events(const char *socket_path) { + int fd = naut_rpc_connect_unix(socket_path); + if (fd < 0) return 1; + json_t *request = json_pack("{s:s,s:o}", "method", "subscribe", + "params", json_object()); + if (!request || + naut_rpc_send_json(fd, NAUT_RPC_REQUEST, request) != NAUT_OK) { + json_decref(request); + close(fd); + return 1; + } + json_decref(request); + for (;;) { + naut_rpc_frame_type type; + json_t *payload = NULL; + if (naut_rpc_recv_json(fd, &type, &payload) != NAUT_OK) { + close(fd); + return 1; + } + print_json(payload); + fflush(stdout); + json_decref(payload); + } +} + +int main(int argc, char **argv) { + const char *socket_path = getenv("NAUT_SOCKET"); + if (!socket_path || !*socket_path) socket_path = DEFAULT_SOCKET; + int arg = 1; + if (arg < argc && strcmp(argv[arg], "--socket") == 0) { + if (arg + 1 >= argc) { + usage(argv[0]); + return 2; + } + socket_path = argv[arg + 1]; + arg += 2; + } + if (arg >= argc || arg + 2 < argc) { + usage(argv[0]); + return 2; + } + signal(SIGPIPE, SIG_IGN); + const char *method = argv[arg++]; + if (strcmp(method, "events") == 0) + return stream_events(socket_path); + json_t *params = parse_params(arg < argc ? argv[arg] : NULL); + if (!params) { + fprintf(stderr, "nautctl: invalid JSON parameters\n"); + return 2; + } + json_t *reply = NULL; + naut_err error = naut_rpc_call(socket_path, method, params, &reply); + json_decref(params); + if (error != NAUT_OK) { + fprintf(stderr, "nautctl: RPC failed: %s\n", naut_strerror(error)); + return 1; + } + int result = print_json(reply); + bool ok = json_is_true(json_object_get(reply, "ok")); + json_decref(reply); + return result || !ok; +} diff --git a/apps/nautd/main.c b/apps/nautd/main.c new file mode 100644 index 0000000..1924e9f --- /dev/null +++ b/apps/nautd/main.c @@ -0,0 +1,507 @@ +#include "naut/event.h" +#include "naut/log.h" +#include "naut/metainfo.h" +#include "naut/plugin.h" +#include "naut/rpc.h" +#include "naut/script.h" +#include "naut/session.h" +#include "naut/storage.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define DEFAULT_SOCKET "/tmp/nautd.sock" +#define MOVE_QUEUE_CAPACITY 64 +#define MAX_SUBSCRIBERS 64 + +typedef struct { + uint64_t torrent_id; + uint32_t file_index; + char destination[PATH_MAX]; +} move_command; + +typedef struct { + naut_event_bus *events; + naut_rpc_registry *rpc; + naut_plugin_manager *plugins; + naut_script *script; + naut_session *session; + pthread_mutex_t move_lock; + move_command moves[MOVE_QUEUE_CAPACITY]; + size_t move_head; + size_t move_count; + uint64_t moves_processed; + pthread_mutex_t subscriber_lock; + int subscribers[MAX_SUBSCRIBERS]; + size_t subscriber_count; + bool stopping; +} daemon_state; + +static volatile sig_atomic_t interrupted; + +static void on_signal(int signal_number) { + (void)signal_number; + interrupted = 1; +} + +static json_t *rpc_ping(void *opaque, const json_t *params, naut_err *error) { + (void)opaque; + (void)params; + json_t *result = json_object(); + if (!result) { + *error = NAUT_ERR_NOMEM; + return NULL; + } + json_object_set_new(result, "protocol", json_integer(NAUT_RPC_VERSION)); + json_object_set_new(result, "service", json_string("nautd")); + *error = NAUT_OK; + return result; +} + +static json_t *rpc_status(void *opaque, const json_t *params, + naut_err *error) { + (void)params; + daemon_state *state = opaque; + naut_script_stats stats = {0}; + if (state->script) naut_script_get_stats(state->script, &stats); + pthread_mutex_lock(&state->move_lock); + uint64_t moves = state->moves_processed; + size_t pending = state->move_count; + pthread_mutex_unlock(&state->move_lock); + json_t *result = json_object(); + json_t *script = json_object(); + if (!result || !script) { + json_decref(result); + json_decref(script); + *error = NAUT_ERR_NOMEM; + return NULL; + } + json_object_set_new(result, "protocol", json_integer(NAUT_RPC_VERSION)); + json_object_set_new(result, "plugins", + json_integer((json_int_t)naut_plugin_count( + state->plugins))); + json_object_set_new(result, "storage_backends", + json_integer((json_int_t)naut_plugin_storage_count( + state->plugins))); + json_object_set_new(script, "queued", json_integer(stats.queued)); + json_object_set_new(script, "handled", json_integer(stats.handled)); + json_object_set_new(script, "dropped", json_integer(stats.dropped)); + json_object_set_new(script, "errors", json_integer(stats.errors)); + json_object_set_new(script, "move_requests", + json_integer(stats.move_requests)); + json_object_set_new(result, "script", script); + json_object_set_new(result, "move_commands", json_integer(moves)); + json_object_set_new(result, "pending_move_commands", + json_integer((json_int_t)pending)); + *error = NAUT_OK; + return result; +} + +static json_t *rpc_plugins(void *opaque, const json_t *params, + naut_err *error) { + (void)params; + daemon_state *state = opaque; + json_t *plugins = json_array(); + json_t *storage = json_array(); + if (!plugins || !storage) { + json_decref(plugins); + json_decref(storage); + *error = NAUT_ERR_NOMEM; + return NULL; + } + for (size_t i = 0; i < naut_plugin_count(state->plugins); i++) + json_array_append_new(plugins, + json_string(naut_plugin_name(state->plugins, i))); + for (size_t i = 0; i < naut_plugin_storage_count(state->plugins); i++) + json_array_append_new(storage, + json_string(naut_plugin_storage_name(state->plugins, i))); + json_t *result = json_object(); + if (!result) { + json_decref(plugins); + json_decref(storage); + *error = NAUT_ERR_NOMEM; + return NULL; + } + json_object_set_new(result, "plugins", plugins); + json_object_set_new(result, "storage_backends", storage); + *error = NAUT_OK; + return result; +} + +static json_t *rpc_emit(void *opaque, const json_t *params, naut_err *error) { + daemon_state *state = opaque; + if (!json_is_object(params)) { + *error = NAUT_ERR_INVAL; + return NULL; + } + const char *type_name = json_string_value(json_object_get(params, "type")); + naut_event_type type; + if (!type_name || !naut_event_type_parse(type_name, &type)) { + *error = NAUT_ERR_INVAL; + return NULL; + } + json_int_t torrent_id = + json_integer_value(json_object_get(params, "torrent_id")); + json_int_t index = json_integer_value(json_object_get(params, "index")); + if (torrent_id < 0 || index < 0 || (uint64_t)index > UINT32_MAX) { + *error = NAUT_ERR_RANGE; + return NULL; + } + naut_event event = { + .type = type, + .torrent_id = (uint64_t)torrent_id, + .index = (uint32_t)index, + .message = json_string_value(json_object_get(params, "message")), + .path = json_string_value(json_object_get(params, "path")), + }; + naut_event_emit(state->events, &event); + *error = NAUT_OK; + return json_true(); +} + +static json_t *rpc_shutdown(void *opaque, const json_t *params, + naut_err *error) { + (void)params; + daemon_state *state = opaque; + state->stopping = true; + *error = NAUT_OK; + return json_true(); +} + +static uint8_t *read_file(const char *path, size_t *len) { + FILE *f = fopen(path, "rb"); + if (!f) return NULL; + if (fseek(f, 0, SEEK_END) != 0) { fclose(f); return NULL; } + long n = ftell(f); + if (n < 0 || fseek(f, 0, SEEK_SET) != 0) { fclose(f); return NULL; } + uint8_t *buf = malloc((size_t)n); + if (!buf) { fclose(f); return NULL; } + if (fread(buf, 1, (size_t)n, f) != (size_t)n) { + free(buf); fclose(f); return NULL; + } + fclose(f); + *len = (size_t)n; + return buf; +} + +/* add_torrent {torrent_id, torrent: <.torrent path>, root: } opens + * the torrent's storage and registers it so move_file can later relocate one of + * its files. This is the control-plane seam that binds a script's move command + * to a concrete naut_storage; it runs on the daemon owner thread. */ +static json_t *rpc_add_torrent(void *opaque, const json_t *params, + naut_err *error) { + daemon_state *state = opaque; + if (!json_is_object(params)) { *error = NAUT_ERR_INVAL; return NULL; } + json_int_t torrent_id = + json_integer_value(json_object_get(params, "torrent_id")); + const char *torrent_path = + json_string_value(json_object_get(params, "torrent")); + const char *root = json_string_value(json_object_get(params, "root")); + if (torrent_id < 0 || !torrent_path || !root) { + *error = NAUT_ERR_INVAL; + return NULL; + } + if (naut_session_has(state->session, (uint64_t)torrent_id)) { + *error = NAUT_ERR_INVAL; + return NULL; + } + size_t len = 0; + uint8_t *raw = read_file(torrent_path, &len); + if (!raw) { *error = NAUT_ERR_IO; return NULL; } + naut_metainfo mi; + naut_err err = naut_metainfo_parse(raw, len, &mi); + free(raw); + if (err != NAUT_OK) { *error = err; return NULL; } + naut_storage *storage = + naut_storage_open(mi.files, mi.num_files, root, &err); + if (!storage) { + naut_metainfo_free(&mi); + *error = err != NAUT_OK ? err : NAUT_ERR_IO; + return NULL; + } + naut_metainfo_free(&mi); + err = naut_session_add(state->session, (uint64_t)torrent_id, storage); + if (err != NAUT_OK) { + naut_storage_close(storage); + *error = err; + return NULL; + } + *error = NAUT_OK; + return json_true(); +} + +static naut_err queue_move(void *opaque, uint64_t torrent_id, + uint32_t file_index, const char *destination) { + daemon_state *state = opaque; + pthread_mutex_lock(&state->move_lock); + if (state->move_count == MOVE_QUEUE_CAPACITY) { + pthread_mutex_unlock(&state->move_lock); + return NAUT_ERR_FULL; + } + size_t tail = (state->move_head + state->move_count) % + MOVE_QUEUE_CAPACITY; + state->moves[tail] = (move_command) { + .torrent_id = torrent_id, + .file_index = file_index, + }; + snprintf(state->moves[tail].destination, + sizeof state->moves[tail].destination, "%s", destination); + state->move_count++; + pthread_mutex_unlock(&state->move_lock); + return NAUT_OK; +} + +static void drain_moves(daemon_state *state) { + /* Copy each pending command out under the lock, then perform the relocate + * with the lock released (so the script thread can keep enqueuing). All + * relocates run on this, the owner thread, as the session requires. */ + for (;;) { + move_command command; + pthread_mutex_lock(&state->move_lock); + if (state->move_count == 0) { + pthread_mutex_unlock(&state->move_lock); + return; + } + command = state->moves[state->move_head]; + state->move_head = (state->move_head + 1) % MOVE_QUEUE_CAPACITY; + state->move_count--; + state->moves_processed++; + pthread_mutex_unlock(&state->move_lock); + + naut_err err = naut_session_move_file(state->session, + command.torrent_id, + command.file_index, + command.destination); + if (err == NAUT_OK) + NAUT_INFO("moved torrent=%llu file=%u -> %s", + (unsigned long long)command.torrent_id, + command.file_index, command.destination); + else + NAUT_WARN("move torrent=%llu file=%u -> %s failed: %s", + (unsigned long long)command.torrent_id, + command.file_index, command.destination, + naut_strerror(err)); + } +} + +static void broadcast_event(void *opaque, const naut_event *event) { + daemon_state *state = opaque; + json_t *payload = naut_rpc_event_json(event); + if (!payload) return; + pthread_mutex_lock(&state->subscriber_lock); + for (size_t i = 0; i < state->subscriber_count;) { + if (naut_rpc_send_json(state->subscribers[i], NAUT_RPC_EVENT, + payload) == NAUT_OK) { + i++; + continue; + } + close(state->subscribers[i]); + state->subscribers[i] = + state->subscribers[--state->subscriber_count]; + } + pthread_mutex_unlock(&state->subscriber_lock); + json_decref(payload); +} + +static int listen_unix(const char *path) { + int fd = socket(AF_UNIX, SOCK_STREAM, 0); + if (fd < 0) return -1; + struct sockaddr_un address; + memset(&address, 0, sizeof address); + address.sun_family = AF_UNIX; + if (strlen(path) >= sizeof address.sun_path) { + close(fd); + return -1; + } + strcpy(address.sun_path, path); + unlink(path); + if (bind(fd, (struct sockaddr *)&address, sizeof address) != 0 || + listen(fd, 32) != 0) { + close(fd); + return -1; + } + return fd; +} + +static json_t *response(bool ok, json_t *result, naut_err error) { + json_t *reply = json_object(); + if (!reply) return NULL; + json_object_set_new(reply, "ok", json_boolean(ok)); + if (ok) { + json_object_set(reply, "result", result ? result : json_null()); + } else { + json_object_set_new(reply, "code", json_integer(error)); + json_object_set_new(reply, "error", + json_string(naut_strerror(error))); + } + return reply; +} + +static void add_subscriber(daemon_state *state, int fd) { + int flags = fcntl(fd, F_GETFL, 0); + if (flags >= 0) fcntl(fd, F_SETFL, flags | O_NONBLOCK); + pthread_mutex_lock(&state->subscriber_lock); + if (state->subscriber_count < MAX_SUBSCRIBERS) { + state->subscribers[state->subscriber_count++] = fd; + fd = -1; + } + pthread_mutex_unlock(&state->subscriber_lock); + if (fd >= 0) close(fd); +} + +static void handle_client(daemon_state *state, int fd) { + struct timeval timeout = {.tv_sec = 2}; + setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof timeout); + naut_rpc_frame_type type; + json_t *request = NULL; + naut_err error = naut_rpc_recv_json(fd, &type, &request); + if (error != NAUT_OK || type != NAUT_RPC_REQUEST || + !json_is_object(request)) { + json_decref(request); + close(fd); + return; + } + const char *method = + json_string_value(json_object_get(request, "method")); + json_t *params = json_object_get(request, "params"); + if (method && strcmp(method, "subscribe") == 0) { + json_t *subscribed = json_string("subscribed"); + json_t *reply = response(true, subscribed, NAUT_OK); + json_decref(subscribed); + if (reply && + naut_rpc_send_json(fd, NAUT_RPC_RESPONSE, reply) == NAUT_OK) + add_subscriber(state, fd); + else + close(fd); + json_decref(reply); + json_decref(request); + return; + } + if (!method) error = NAUT_ERR_INVAL; + json_t *result = method + ? naut_rpc_dispatch(state->rpc, method, params, &error) : NULL; + json_t *reply = response(error == NAUT_OK && result, result, error); + json_decref(result); + if (reply) { + naut_rpc_send_json(fd, NAUT_RPC_RESPONSE, reply); + json_decref(reply); + } + json_decref(request); + close(fd); +} + +static bool register_commands(daemon_state *state) { + return naut_rpc_register(state->rpc, "ping", rpc_ping, state) == NAUT_OK && + naut_rpc_register(state->rpc, "status", rpc_status, state) == NAUT_OK && + naut_rpc_register(state->rpc, "plugins", rpc_plugins, state) == NAUT_OK && + naut_rpc_register(state->rpc, "emit", rpc_emit, state) == NAUT_OK && + naut_rpc_register(state->rpc, "add_torrent", rpc_add_torrent, state) == NAUT_OK && + naut_rpc_register(state->rpc, "shutdown", rpc_shutdown, state) == NAUT_OK; +} + +static void usage(const char *program) { + fprintf(stderr, + "usage: %s [--socket PATH] [--plugin PATH]... [--script PATH]\n", + program); +} + +int main(int argc, char **argv) { + const char *socket_path = DEFAULT_SOCKET; + const char *script_path = NULL; + const char *plugin_paths[64]; + size_t plugin_count = 0; + for (int i = 1; i < argc; i++) { + if (strcmp(argv[i], "--socket") == 0 && i + 1 < argc) + socket_path = argv[++i]; + else if (strcmp(argv[i], "--plugin") == 0 && i + 1 < argc && + plugin_count < NAUT_ARRAY_LEN(plugin_paths)) + plugin_paths[plugin_count++] = argv[++i]; + else if (strcmp(argv[i], "--script") == 0 && i + 1 < argc) + script_path = argv[++i]; + else { + usage(argv[0]); + return 2; + } + } + + signal(SIGINT, on_signal); + signal(SIGTERM, on_signal); + signal(SIGPIPE, SIG_IGN); + daemon_state state = {0}; + pthread_mutex_init(&state.move_lock, NULL); + pthread_mutex_init(&state.subscriber_lock, NULL); + state.events = naut_event_bus_create(); + state.rpc = naut_rpc_registry_create(); + state.plugins = naut_plugin_manager_create(state.rpc, state.events); + state.session = naut_session_create(); + if (!state.events || !state.rpc || !state.plugins || !state.session || + !register_commands(&state)) { + fprintf(stderr, "nautd: failed to initialize control plane\n"); + return 1; + } + for (size_t i = 0; i < plugin_count; i++) { + if (naut_plugin_load(state.plugins, plugin_paths[i]) != NAUT_OK) { + fprintf(stderr, "nautd: failed to load plugin %s\n", + plugin_paths[i]); + return 1; + } + } + if (script_path) { + naut_err error; + state.script = naut_script_create(state.events, script_path, 256, + queue_move, &state, &error); + if (!state.script) { + fprintf(stderr, "nautd: failed to load script %s: %s\n", + script_path, naut_strerror(error)); + return 1; + } + } + uint64_t event_subscription; + if (naut_event_subscribe(state.events, broadcast_event, &state, + &event_subscription) != NAUT_OK) + return 1; + int listener = listen_unix(socket_path); + if (listener < 0) { + perror("nautd: listen"); + return 1; + } + NAUT_INFO("nautd listening on %s", socket_path); + while (!state.stopping && !interrupted) { + struct pollfd pollfd = {.fd = listener, .events = POLLIN}; + int ready = poll(&pollfd, 1, 100); + if (ready > 0 && (pollfd.revents & POLLIN)) { + int client = accept(listener, NULL, NULL); + if (client >= 0) handle_client(&state, client); + } else if (ready < 0 && errno != EINTR) { + break; + } + drain_moves(&state); + } + + close(listener); + unlink(socket_path); + naut_event_unsubscribe(state.events, event_subscription); + pthread_mutex_lock(&state.subscriber_lock); + for (size_t i = 0; i < state.subscriber_count; i++) + close(state.subscribers[i]); + pthread_mutex_unlock(&state.subscriber_lock); + naut_script_destroy(state.script); /* joins the script thread */ + drain_moves(&state); /* flush any moves it left queued */ + naut_plugin_manager_destroy(state.plugins); + naut_rpc_registry_destroy(state.rpc); + naut_session_destroy(state.session); + naut_event_bus_destroy(state.events); + pthread_mutex_destroy(&state.subscriber_lock); + pthread_mutex_destroy(&state.move_lock); + return 0; +} diff --git a/apps/swarm/main.c b/apps/swarm/main.c new file mode 100644 index 0000000..63d7f5c --- /dev/null +++ b/apps/swarm/main.c @@ -0,0 +1,801 @@ +/* naut_swarm — Phase 4 gate: download from a SWARM of peers concurrently. + * + * A poll()-based multi-socket driver around the same sans-IO peer codec and the + * multi-peer engine (rarest-first + bounded endgame duplication). Peers can be + * supplied explicitly for deterministic testing, or discovered from the + * torrent's HTTP/UDP trackers. + * + * usage: naut_swarm [ ...] + */ +#include "naut/dht.h" +#include "naut/extension.h" +#include "naut/metainfo.h" +#include "naut/storage.h" +#include "naut/piece.h" +#include "naut/peer.h" +#include "naut/tracker.h" +#include "naut/bitfield.h" +#include "naut/log.h" +#include "naut/pipeline.h" +#include "naut/system.h" +#include "naut/worker.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define REQUEST_TIMEOUT 15.0 +#define EXT_RESERVED 0x0000000000100000ULL + +typedef struct { + uint32_t piece, begin, length; + double sent_at; +} req_t; + +typedef struct { + naut_peer_addr addr; + char name[32]; +} endpoint_t; + +typedef struct { + int fd; + char name[40]; + uint8_t *rbuf; size_t rcap, rlen; + bool hs_done, peer_choking; + naut_bitfield have; + req_t *inflight; size_t nflight, cflight; + uint64_t blocks_received; + naut_ext_handshake extensions; + uint64_t pex_received; + naut_pipeline pipeline; + bool dead, availability_removed; +} peer_t; + +static double now(void) { struct timespec t; clock_gettime(CLOCK_MONOTONIC, &t); return t.tv_sec + t.tv_nsec*1e-9; } + +static uint8_t *slurp(const char *path, size_t *len) { + FILE *f = fopen(path, "rb"); if (!f) return NULL; + fseek(f, 0, SEEK_END); long n = ftell(f); fseek(f, 0, SEEK_SET); + uint8_t *b = malloc(n); + if (fread(b, 1, n, f) != (size_t)n) { fclose(f); free(b); return NULL; } + fclose(f); *len = (size_t)n; return b; +} +static bool send_all(int fd, const void *p, size_t n) { + const uint8_t *b = p; + while (n) { ssize_t w = send(fd, b, n, MSG_NOSIGNAL); + if (w < 0) { if (errno == EINTR) continue; return false; } b += w; n -= (size_t)w; } + return true; +} + +static bool endpoint_add(endpoint_t **v, size_t *n, size_t *cap, + const naut_peer_addr *addr) { + if (addr->port == 0) return true; + for (size_t i = 0; i < *n; i++) + if ((*v)[i].addr.port == addr->port && + memcmp((*v)[i].addr.ip, addr->ip, sizeof addr->ip) == 0) + return true; + if (*n == *cap) { + size_t newcap = *cap ? *cap * 2 : 32; + endpoint_t *next = realloc(*v, newcap * sizeof(*next)); + if (!next) return false; + *v = next; + *cap = newcap; + } + endpoint_t *ep = &(*v)[(*n)++]; + ep->addr = *addr; + snprintf(ep->name, sizeof ep->name, "%u.%u.%u.%u:%u", + addr->ip[0], addr->ip[1], addr->ip[2], addr->ip[3], addr->port); + return true; +} + +static bool endpoint_parse(const char *s, naut_peer_addr *out) { + char host[INET_ADDRSTRLEN]; + const char *colon = strrchr(s, ':'); + if (!colon || colon == s || (size_t)(colon - s) >= sizeof host) return false; + char *end = NULL; + unsigned long port = strtoul(colon + 1, &end, 10); + if (!end || *end || port == 0 || port > UINT16_MAX) return false; + memcpy(host, s, (size_t)(colon - s)); + host[colon - s] = 0; + struct in_addr ip; + if (inet_pton(AF_INET, host, &ip) != 1) return false; + memcpy(out->ip, &ip, sizeof out->ip); + out->port = (uint16_t)port; + return true; +} + +static bool parse_udp_tracker(const char *url, char *host, size_t hostsz, + uint16_t *port) { + if (strncmp(url, "udp://", 6) != 0) return false; + const char *start = url + 6; + const char *slash = strchr(start, '/'); + const char *end = slash ? slash : start + strlen(start); + const char *colon = memchr(start, ':', (size_t)(end - start)); + if (!colon || colon == start) return false; + size_t hlen = (size_t)(colon - start); + if (hlen >= hostsz) return false; + char pbuf[16]; + size_t plen = (size_t)(end - colon - 1); + if (plen == 0 || plen >= sizeof pbuf) return false; + memcpy(host, start, hlen); + host[hlen] = 0; + memcpy(pbuf, colon + 1, plen); + pbuf[plen] = 0; + char *tail = NULL; + unsigned long parsed = strtoul(pbuf, &tail, 10); + if (!tail || *tail || parsed == 0 || parsed > UINT16_MAX) return false; + *port = (uint16_t)parsed; + return true; +} + +static bool discover_trackers(const uint8_t info_hash[20], uint64_t total_length, + char *const *trackers, size_t num_trackers, + const uint8_t peerid[20], + endpoint_t **eps, size_t *neps, size_t *cap) { + naut_announce_req req; + memset(&req, 0, sizeof req); + memcpy(req.info_hash, info_hash, sizeof req.info_hash); + memcpy(req.peer_id, peerid, sizeof req.peer_id); + req.port = 6881; + req.left = total_length; + req.event = NAUT_TEV_STARTED; + req.numwant = 100; + req.key = (uint32_t)rand(); + + for (size_t i = 0; i < num_trackers; i++) { + const char *tracker = trackers[i]; + naut_tracker_response response; + naut_err e = NAUT_ERR_INVAL; + if (strncmp(tracker, "http://", 7) == 0) { + char url[4096]; + if (naut_tracker_http_url(tracker, &req, url, sizeof url) != 0) + e = naut_tracker_announce_http(url, &response); + } else if (strncmp(tracker, "udp://", 6) == 0) { + char host[256]; + uint16_t port; + if (parse_udp_tracker(tracker, host, sizeof host, &port)) + e = naut_tracker_announce_udp(host, port, &req, &response); + } else { + NAUT_WARN("tracker scheme unsupported: %s", tracker); + continue; + } + if (e != NAUT_OK) { + NAUT_WARN("tracker announce failed: %s", tracker); + continue; + } + NAUT_INFO("tracker %s returned %zu peers", tracker, response.num_peers); + for (size_t p = 0; p < response.num_peers; p++) { + if (!endpoint_add(eps, neps, cap, &response.peers[p])) { + naut_tracker_response_free(&response); + return false; + } + } + naut_tracker_response_free(&response); + } + return true; +} + +static bool discover_dht(const uint8_t info_hash[20], + endpoint_t **eps, size_t *neps, size_t *cap) { + static const char *defaults[] = { + "router.bittorrent.com:6881", + "router.utorrent.com:6881", + "dht.transmissionbt.com:6881", + }; + const char *const *bootstrap = defaults; + size_t num_bootstrap = NAUT_ARRAY_LEN(defaults); + char *copy = NULL; + const char *custom[32]; + + const char *env = getenv("NAUT_DHT_BOOTSTRAP"); + if (env && *env) { + copy = strdup(env); + if (!copy) return false; + num_bootstrap = 0; + char *save = NULL; + for (char *part = strtok_r(copy, ",", &save); + part && num_bootstrap < NAUT_ARRAY_LEN(custom); + part = strtok_r(NULL, ",", &save)) + custom[num_bootstrap++] = part; + bootstrap = custom; + } + + naut_peer_addr *peers = NULL; + size_t num_peers = 0; + naut_err e = num_bootstrap + ? naut_dht_get_peers(bootstrap, num_bootstrap, info_hash, + &peers, &num_peers) + : NAUT_ERR_INVAL; + free(copy); + if (e != NAUT_OK) { + NAUT_WARN("DHT lookup found no peers"); + return true; + } + NAUT_INFO("DHT returned %zu peers", num_peers); + for (size_t i = 0; i < num_peers; i++) { + if (!endpoint_add(eps, neps, cap, &peers[i])) { + free(peers); + return false; + } + } + free(peers); + return true; +} + +static bool peer_reserve_inflight(peer_t *p) { + if (p->nflight == p->cflight) { + size_t cap = p->cflight ? p->cflight * 2 : 64; + req_t *v = realloc(p->inflight, cap * sizeof(*v)); + if (!v) return false; + p->inflight = v; + p->cflight = cap; + } + return true; +} + +static void peer_add_inflight(peer_t *p, uint32_t piece, uint32_t begin, + uint32_t length) { + p->inflight[p->nflight].piece = piece; + p->inflight[p->nflight].begin = begin; + p->inflight[p->nflight].length = length; + p->inflight[p->nflight].sent_at = now(); + p->nflight++; +} +static bool peer_del_inflight(peer_t *p, uint32_t piece, uint32_t begin, + req_t *removed) { + for (size_t i = 0; i < p->nflight; i++) + if (p->inflight[i].piece == piece && p->inflight[i].begin == begin) { + if (removed) *removed = p->inflight[i]; + p->inflight[i] = p->inflight[--p->nflight]; + return true; + } + return false; +} + +static bool peer_has_request(void *ctx, uint32_t piece, uint32_t begin) { + peer_t *p = ctx; + for (size_t i = 0; i < p->nflight; i++) + if (p->inflight[i].piece == piece && p->inflight[i].begin == begin) + return true; + return false; +} + +/* return remaining inflight blocks to the picker (choke / disconnect) */ +static void peer_release(naut_download *d, peer_t *p) { + for (size_t i = 0; i < p->nflight; i++) + naut_download_unrequest(d, p->inflight[i].piece, p->inflight[i].begin); + p->nflight = 0; +} + +static void peer_drop(naut_download *d, peer_t *p) { + if (!p->availability_removed) { + naut_download_remove_bitfield(d, &p->have); + p->availability_removed = true; + } + peer_release(d, p); + if (p->fd >= 0) close(p->fd); + p->fd = -1; + p->dead = true; +} + +static bool refill_one(naut_download *d, peer_t *p) { + if (p->dead || !p->hs_done || p->peer_choking || + p->nflight >= naut_pipeline_depth(&p->pipeline)) + return false; + if (!peer_reserve_inflight(p)) { + peer_drop(d, p); + return false; + } + uint32_t i, b, l; + if (!naut_download_pick_for_peer(d, &p->have, peer_has_request, p, + &i, &b, &l)) + return false; + uint8_t req[17]; + naut_peer_msg_request(req, i, b, l); + if (!send_all(p->fd, req, sizeof req)) { + naut_download_unrequest(d, i, b); + peer_drop(d, p); + return false; + } + peer_add_inflight(p, i, b, l); + return true; +} + +static void expire_requests(naut_download *d, peer_t *p, double t) { + size_t i = 0; + while (i < p->nflight) { + if (t - p->inflight[i].sent_at < REQUEST_TIMEOUT) { + i++; + continue; + } + naut_download_unrequest(d, p->inflight[i].piece, p->inflight[i].begin); + p->inflight[i] = p->inflight[--p->nflight]; + } +} + +static int connect_to(const endpoint_t *ep) { + int fd = socket(AF_INET, SOCK_STREAM, 0); + if (fd < 0) return -1; + struct sockaddr_in a; memset(&a, 0, sizeof a); + a.sin_family = AF_INET; + a.sin_port = htons(ep->addr.port); + memcpy(&a.sin_addr, ep->addr.ip, sizeof ep->addr.ip); + if (connect(fd, (struct sockaddr *)&a, sizeof a) != 0) { close(fd); return -1; } + int one = 1; setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &one, sizeof one); + return fd; +} + +/* process all complete messages currently buffered for peer p */ +static void cancel_block(naut_download *d, peer_t *peers, int npeers, + peer_t *source, uint32_t piece, uint32_t begin) { + for (int i = 0; i < npeers; i++) { + peer_t *p = &peers[i]; + if (p == source || p->dead) continue; + req_t old; + if (!peer_del_inflight(p, piece, begin, &old)) continue; + uint8_t msg[17]; + naut_peer_msg_cancel(msg, old.piece, old.begin, old.length); + if (!send_all(p->fd, msg, sizeof msg)) peer_drop(d, p); + } +} + +static void cancel_piece(naut_download *d, peer_t *peers, int npeers, + uint32_t piece) { + for (int i = 0; i < npeers; i++) { + peer_t *p = &peers[i]; + size_t j = 0; + while (j < p->nflight) { + req_t old = p->inflight[j]; + if (old.piece != piece) { + j++; + continue; + } + p->inflight[j] = p->inflight[--p->nflight]; + naut_download_unrequest(d, old.piece, old.begin); + if (!p->dead) { + uint8_t msg[17]; + naut_peer_msg_cancel(msg, old.piece, old.begin, old.length); + if (!send_all(p->fd, msg, sizeof msg)) { + peer_drop(d, p); + break; + } + } + } + } +} + +static void merge_bitfield(naut_download *d, const naut_metainfo *mi, + peer_t *p, const uint8_t *wire, size_t wire_len) { + naut_bitfield incoming; + if (naut_bitfield_init(&incoming, mi->num_pieces) != NAUT_OK) { + peer_drop(d, p); + return; + } + naut_bitfield_from_wire(&incoming, wire, wire_len); + for (uint32_t piece = 0; piece < mi->num_pieces; piece++) { + if (naut_bitfield_test(&incoming, piece) && + !naut_bitfield_test(&p->have, piece)) { + naut_bitfield_set(&p->have, piece); + naut_download_inc_avail(d, piece); + } + } + naut_bitfield_free(&incoming); +} + +static naut_err peer_process(naut_download *d, const naut_metainfo *mi, + peer_t *peers, int npeers, peer_t *p) { + size_t pos = 0; + if (!p->hs_done) { + if (p->rlen < NAUT_HANDSHAKE_LEN) return NAUT_OK; + uint8_t ih[20], pid[20]; + if (!naut_peer_handshake_parse(p->rbuf, ih, pid, NULL) || + memcmp(ih, mi->infohash_v1, 20) != 0) { + p->dead = true; + return NAUT_OK; + } + pos = NAUT_HANDSHAKE_LEN; + p->hs_done = true; + } + for (;;) { + naut_msg m; + int c = naut_peer_msg_parse(p->rbuf + pos, p->rlen - pos, &m); + if (c == 0) break; + if (c < 0) { p->dead = true; break; } + pos += (size_t)c; + switch (m.type) { + case NAUT_MSG_BITFIELD: + merge_bitfield(d, mi, p, m.payload, m.payload_len); + if (p->dead) goto parsed; + break; + case NAUT_MSG_HAVE: + if (m.index < mi->num_pieces && !naut_bitfield_test(&p->have, m.index)) { + naut_bitfield_set(&p->have, m.index); + naut_download_inc_avail(d, m.index); + } + break; + case NAUT_MSG_UNCHOKE: p->peer_choking = false; break; + case NAUT_MSG_CHOKE: p->peer_choking = true; peer_release(d, p); break; + case NAUT_MSG_EXTENDED: + if (m.payload_len < 1) { + peer_drop(d, p); + goto parsed; + } + if (m.payload[0] == 0) { + if (naut_ext_parse_handshake( + m.payload + 1, m.payload_len - 1, + &p->extensions) != NAUT_OK) { + peer_drop(d, p); + goto parsed; + } + } else if (m.payload[0] == NAUT_EXT_UT_PEX) { + naut_pex_msg pex; + if (naut_pex_parse(m.payload + 1, m.payload_len - 1, + &pex) != NAUT_OK) { + peer_drop(d, p); + goto parsed; + } + p->pex_received += pex.num_added; + naut_pex_free(&pex); + } + break; + case NAUT_MSG_PIECE: { + req_t request; + bool expected = peer_del_inflight(p, m.index, m.begin, &request); + if (expected) { + naut_pipeline_on_block(&p->pipeline, request.length, + request.sent_at, now()); + naut_download_unrequest(d, m.index, m.begin); + if (request.length != m.payload_len) { + peer_drop(d, p); + goto parsed; + } + } + bool pdone = false; + naut_err e = naut_download_on_block(d, m.index, m.begin, m.payload, + (uint32_t)m.payload_len, &pdone); + if (e == NAUT_OK) { + p->blocks_received++; + cancel_block(d, peers, npeers, p, m.index, m.begin); + } else if (e == NAUT_ERR_PROTO) { + if (expected) cancel_piece(d, peers, npeers, m.index); + else peer_drop(d, p); + } else { + return e; + } + break; + } + default: break; + } + } +parsed: + memmove(p->rbuf, p->rbuf + pos, p->rlen - pos); + p->rlen -= pos; + return NAUT_OK; +} + +int main(int argc, char **argv) { + if (argc < 3) { + fprintf(stderr, + "usage: %s [ip:port ...]\n", + argv[0]); + return 2; + } + naut_log_set_level(NAUT_LOG_INFO); + + uint8_t peerid[20]; memcpy(peerid, "-NT0001-", 8); + srand((unsigned)time(NULL) ^ (unsigned)getpid()); + for (int i = 8; i < 20; i++) peerid[i] = (uint8_t)(rand() & 0xff); + + bool from_magnet = strncmp(argv[1], "magnet:?", 8) == 0; + naut_metainfo mi; + memset(&mi, 0, sizeof mi); + naut_magnet magnet; + memset(&magnet, 0, sizeof magnet); + if (from_magnet) { + if (naut_magnet_parse(argv[1], &magnet) != NAUT_OK || + !magnet.has_v1) { + NAUT_ERROR("magnet must contain a v1 btih hash"); + naut_magnet_free(&magnet); + return 1; + } + } else { + size_t tlen; + uint8_t *tor = slurp(argv[1], &tlen); + if (!tor) { NAUT_ERROR("read torrent"); return 1; } + if (naut_metainfo_parse(tor, tlen, &mi) != NAUT_OK) { + NAUT_ERROR("parse torrent"); + free(tor); + return 1; + } + free(tor); + } + + endpoint_t *endpoints = NULL; + size_t neps = 0, epcap = 0; + if (argc > 3) { + for (int i = 3; i < argc; i++) { + naut_peer_addr addr; + if (!endpoint_parse(argv[i], &addr)) { + NAUT_WARN("invalid peer address: %s", argv[i]); + continue; + } + if (!endpoint_add(&endpoints, &neps, &epcap, &addr)) { + NAUT_ERROR("out of memory collecting peers"); + naut_metainfo_free(&mi); + naut_magnet_free(&magnet); + free(endpoints); + return 1; + } + } + } else { + const uint8_t *hash = + from_magnet ? magnet.infohash_v1 : mi.infohash_v1; + char *const *trackers = + from_magnet ? magnet.trackers : mi.trackers; + size_t num_trackers = + from_magnet ? magnet.num_trackers : mi.num_trackers; + uint64_t total = from_magnet ? 0 : (uint64_t)mi.total_length; + if (!discover_trackers(hash, total, trackers, num_trackers, peerid, + &endpoints, &neps, &epcap) || + (neps == 0 && + !discover_dht(hash, &endpoints, &neps, &epcap))) { + NAUT_ERROR("out of memory collecting discovered peers"); + naut_metainfo_free(&mi); + naut_magnet_free(&magnet); + free(endpoints); + return 1; + } + } + + if (from_magnet && neps) { + uint8_t *info = NULL; + size_t info_len = 0; + naut_err metadata_error = NAUT_ERR_EMPTY; + for (size_t i = 0; i < neps; i++) { + metadata_error = naut_metadata_fetch( + &endpoints[i].addr, magnet.infohash_v1, peerid, + &info, &info_len); + if (metadata_error == NAUT_OK) break; + NAUT_WARN("metadata fetch from %s failed", endpoints[i].name); + } + if (metadata_error != NAUT_OK || + naut_metainfo_parse_info( + info, info_len, (const char *const *)magnet.trackers, + magnet.num_trackers, &mi) != NAUT_OK) { + NAUT_ERROR("unable to fetch valid magnet metadata"); + free(info); + naut_magnet_free(&magnet); + free(endpoints); + return 1; + } + free(info); + NAUT_INFO("magnet metadata verified: %u pieces, %lld bytes", + mi.num_pieces, (long long)mi.total_length); + } + naut_magnet_free(&magnet); + + if (neps == 0) { + NAUT_ERROR(argc > 3 ? "no valid peer addresses" : + "tracker and DHT discovery returned no peers"); + naut_metainfo_free(&mi); + free(endpoints); + return 1; + } + + naut_err err; + naut_storage_opts storage_opts = { + .direct_io = getenv("NAUT_DIRECT_IO") != NULL, + .preallocate = true, + }; + naut_storage *st = naut_storage_open_opts( + mi.files, mi.num_files, argv[2], &storage_opts, &err); + if (!st) { + NAUT_ERROR("storage: %s", naut_strerror(err)); + naut_metainfo_free(&mi); + free(endpoints); + return 1; + } + naut_download *d = naut_download_create(&mi, st); + if (!d) { + naut_storage_close(st); + naut_metainfo_free(&mi); + free(endpoints); + return 1; + } + int online_cpus = naut_online_cpus(); + uint32_t worker_count = + (uint32_t)NAUT_MAX(1, NAUT_MIN(8, online_cpus / 2)); + if (getenv("NAUT_WORKERS")) { + unsigned long configured = strtoul(getenv("NAUT_WORKERS"), NULL, 10); + if (configured > 0 && configured <= 256) + worker_count = (uint32_t)configured; + } + int worker_cpu_base = getenv("NAUT_WORKER_CPU_BASE") + ? atoi(getenv("NAUT_WORKER_CPU_BASE")) : -1; + naut_worker_pool *workers = + naut_worker_pool_create(worker_count, 1024, worker_cpu_base); + if (!workers) { + NAUT_ERROR("unable to create hash worker pool"); + naut_download_destroy(d); + naut_storage_close(st); + naut_metainfo_free(&mi); + free(endpoints); + return 1; + } + naut_download_set_worker_pool(d, workers); + + int npeers = (int)neps; + peer_t *peers = calloc(neps, sizeof(*peers)); + struct pollfd *pfd = calloc(neps + 1, sizeof(*pfd)); + int *idx_map = calloc(neps + 1, sizeof(*idx_map)); + if (!peers || !pfd || !idx_map) { + NAUT_ERROR("out of memory creating swarm"); + free(peers); free(pfd); free(idx_map); + naut_worker_pool_destroy(workers); + naut_download_destroy(d); + naut_storage_close(st); + naut_metainfo_free(&mi); + free(endpoints); + return 1; + } + for (int i = 0; i < npeers; i++) peers[i].fd = -1; + + int active = 0; + for (int i = 0; i < npeers; i++) { + int fd = connect_to(&endpoints[i]); + if (fd < 0) { + NAUT_WARN("connect %s failed", endpoints[i].name); + peers[i].dead = true; + continue; + } + peer_t *p = &peers[i]; + p->fd = fd; + snprintf(p->name, sizeof p->name, "%s", endpoints[i].name); + p->peer_choking = true; + naut_pipeline_init(&p->pipeline, NAUT_BLOCK, 4, 1024, 32); + p->rcap = 1 << 18; + p->rbuf = malloc(p->rcap); + if (!p->rbuf || naut_bitfield_init(&p->have, mi.num_pieces) != NAUT_OK) { + free(p->rbuf); + p->rbuf = NULL; + close(fd); + p->fd = -1; + p->dead = true; + continue; + } + uint8_t hs[NAUT_HANDSHAKE_LEN]; + naut_peer_handshake_build(hs, mi.infohash_v1, peerid, EXT_RESERVED); + uint8_t intr[5]; naut_peer_msg_simple(intr, NAUT_MSG_INTERESTED); + uint8_t *ext = NULL; + size_t ext_len = 0; + naut_err ext_error = naut_ext_build_handshake( + NAUT_EXT_UT_METADATA, NAUT_EXT_UT_PEX, 0, 0, &ext, &ext_len); + bool sent = ext_error == NAUT_OK && + send_all(fd, hs, sizeof hs) && + send_all(fd, ext, ext_len) && + send_all(fd, intr, 5); + free(ext); + if (!sent) { + peer_drop(d, p); + continue; + } + active++; + } + free(endpoints); + if (!active) { + NAUT_ERROR("no peers reachable"); + goto done; + } + NAUT_INFO("swarm: %d peers, %u pieces, %lld bytes", active, mi.num_pieces, (long long)mi.total_length); + + double t0 = now(); + naut_err run_error = NAUT_OK; + while (!naut_download_complete(d) && run_error == NAUT_OK) { + int nf = 0; + for (int i = 0; i < npeers; i++) { + if (peers[i].dead) continue; + pfd[nf].fd = peers[i].fd; pfd[nf].events = POLLIN; pfd[nf].revents = 0; + idx_map[nf] = i; nf++; + } + int live_peers = nf; + pfd[nf].fd = naut_worker_eventfd(workers); + pfd[nf].events = POLLIN; + pfd[nf].revents = 0; + idx_map[nf] = -1; + nf++; + if (live_peers == 0) { + uint32_t completed = 0; + run_error = naut_download_poll(d, &completed); + if (naut_download_complete(d)) break; + NAUT_ERROR("all peers gone (%.0f%% done)", + 100.0 * naut_download_pieces_done(d) / mi.num_pieces); + break; + } + int r = poll(pfd, nf, 2000); + if (r < 0) { if (errno == EINTR) continue; break; } + + for (int k = 0; k < nf; k++) { + if (idx_map[k] < 0) { + if (pfd[k].revents & POLLIN) { + uint64_t count; + (void)read(pfd[k].fd, &count, sizeof count); + uint32_t completed = 0; + run_error = naut_download_poll(d, &completed); + } + continue; + } + peer_t *p = &peers[idx_map[k]]; + if (!(pfd[k].revents & (POLLIN | POLLHUP | POLLERR))) continue; + if (p->rlen == p->rcap) { + size_t cap = p->rcap * 2; + uint8_t *buf = realloc(p->rbuf, cap); + if (!buf) { peer_drop(d, p); continue; } + p->rbuf = buf; + p->rcap = cap; + } + ssize_t got = recv(p->fd, p->rbuf + p->rlen, p->rcap - p->rlen, 0); + if (got <= 0) { peer_drop(d, p); continue; } + p->rlen += (size_t)got; + run_error = peer_process(d, &mi, peers, npeers, p); + if (run_error != NAUT_OK) break; + if (p->dead) peer_drop(d, p); + } + if (run_error != NAUT_OK) break; + double t = now(); + for (int i = 0; i < npeers; i++) { + peer_t *p = &peers[i]; + if (!p->dead) expire_requests(d, p, t); + } + for (;;) { + bool sent = false; + for (int i = 0; i < npeers; i++) + if (refill_one(d, &peers[i])) sent = true; + if (!sent) break; + } + } + + double dt = now() - t0; + bool ok = naut_download_complete(d); + if (ok) { + double mb = (double)mi.total_length / 1e6; + NAUT_INFO("COMPLETE: %u/%u pieces from swarm in %.2fs (%.1f MB/s), all SHA-1 verified%s", + naut_download_pieces_done(d), mi.num_pieces, dt, mb/dt, + naut_download_in_endgame(d) ? " (passed through endgame)" : ""); + } else { + if (run_error != NAUT_OK) + NAUT_ERROR("swarm stopped: %s", naut_strerror(run_error)); + NAUT_ERROR("INCOMPLETE: %u/%u pieces", naut_download_pieces_done(d), mi.num_pieces); + } + +done: + ok = naut_download_complete(d); + naut_storage_sync(st); + for (int i = 0; i < npeers; i++) { + if (peers[i].blocks_received) + NAUT_INFO("peer %s delivered %llu blocks (pipeline %u, RTT %.1f ms, %.1f MB/s)", + peers[i].name, + (unsigned long long)peers[i].blocks_received, + naut_pipeline_depth(&peers[i].pipeline), + peers[i].pipeline.rtt_seconds * 1000.0, + peers[i].pipeline.bytes_per_second / 1e6); + if (peers[i].pex_received) + NAUT_INFO("peer %s advertised %llu peers through PEX", + peers[i].name, + (unsigned long long)peers[i].pex_received); + if (!peers[i].dead) peer_drop(d, &peers[i]); + free(peers[i].rbuf); free(peers[i].inflight); + if (peers[i].have.words) naut_bitfield_free(&peers[i].have); + } + free(peers); free(pfd); free(idx_map); + naut_worker_pool_destroy(workers); + naut_download_destroy(d); naut_storage_close(st); naut_metainfo_free(&mi); + return ok ? 0 : 1; +} diff --git a/docs/scripting.md b/docs/scripting.md new file mode 100644 index 0000000..e40cc49 --- /dev/null +++ b/docs/scripting.md @@ -0,0 +1,281 @@ +# Naut-Torrent Lua scripting reference + +Naut-Torrent embeds a sandboxed Lua interpreter so you can react to engine +events (a torrent finished, a file completed, a peer connected) and drive a +small set of actions — most importantly **moving a file out of the download +directory the moment it finishes**, without waiting for the whole torrent. + +This document describes the entire script-visible surface: how scripts are +loaded and run, the sandbox, every event hook, the `event` object passed to +them, and the `naut` API table. + +> Implemented in `src/script/script.c`; the host API is `include/naut/script.h`. +> The daemon wiring is in `apps/nautd/main.c`. + +--- + +## 1. Loading a script + +Pass a script to the daemon with `--script`: + +```sh +nautd --socket /tmp/nautd.sock --script ./my-rules.lua +``` + +- **One script per daemon.** If `--script` is given more than once, the last + path wins. +- The file is **loaded and executed once at startup**, on the main thread, + before the event worker starts. Use this top-level run to define your hook + functions (and any state they need). If the file fails to load or its + top-level code raises, the daemon refuses to start and prints the Lua error. +- After startup the script is **event-driven**: the functions you defined are + called as matching events occur. + +```lua +-- my-rules.lua — top level runs once at load +local moved = 0 + +function on_file_complete(event) -- called later, per event + naut.move_file(event.torrent_id, event.index, "/archive/" .. event.path) + moved = moved + 1 +end +``` + +--- + +## 2. Execution & threading model + +- Hooks run on a **single dedicated script thread**, one event at a time, in + the order events were emitted. Your hooks never run concurrently with each + other, so ordinary Lua locals/tables need no locking. +- Events reach the script through a **bounded queue** (`queue_capacity`, 256 in + `nautd`). The engine never blocks waiting for your script: if the queue is + full because a hook is slow, **new events are dropped** (and counted — see + §7). Keep hooks short; offload nothing back onto the engine threads. +- The event is **copied** before it is handed to your hook. Two copied fields + are length-bounded: `message` is truncated to 255 bytes and `path` to + `PATH_MAX-1`. Longer values are silently shortened. +- A hook that raises an error does **not** crash the daemon or stop the script: + the error is recorded and the next event proceeds (see §7). +- **Hook return values are ignored.** + +--- + +## 3. The sandbox + +The standard libraries are opened and then the dangerous globals are removed, so +a script cannot touch the filesystem, spawn processes, load native modules, or +compile raw chunks. + +**Removed (set to `nil`):** + +| Global | Why | +|---|---| +| `os` | process/clock/`os.execute`/`os.remove` | +| `io` | filesystem handles | +| `package`, `require` | native/Lua module loading | +| `debug` | introspection / sandbox escape | +| `dofile`, `loadfile` | run code from a file | +| `load`, `loadstring` | compile arbitrary chunks — the default `"bt"` mode accepts **binary** bytecode, which can escape the VM, so the loaders are denied as defense-in-depth | + +**Available:** the rest of the base library (`print`, `pcall`, `error`, +`assert`, `type`, `tostring`, `tonumber`, `pairs`, `ipairs`, `select`, +`setmetatable`, `next`, …) plus `string`, `table`, `math`, `coroutine`, and +`utf8`. Plus the `naut` table (§6). + +`print` writes to the daemon's stdout/log — handy for debugging rules. + +--- + +## 4. Event hooks + +Define any subset of these as **global functions**. Each is optional; an event +with no matching global is simply ignored. Each hook is called with one +argument, the [`event` object](#5-the-event-object). + +| Hook | Fired when | Most relevant fields | +|---|---|---| +| `on_torrent_added(event)` | a torrent is registered with the session | `torrent_id` | +| `on_piece_complete(event)` | a piece verifies and is written | `torrent_id`, `index` = piece index | +| `on_file_complete(event)` | a file's last covering piece verifies — the file is final on disk and safe to move, **before the torrent finishes** | `torrent_id`, `index` = file index, `path` = file path | +| `on_torrent_finished(event)` | every piece of the torrent is complete | `torrent_id` | +| `on_peer_connected(event)` | a peer connection is established | `torrent_id`, `message` = peer address (when provided) | +| `on_alert(event)` | a general engine alert/notice | `message` = alert text | + +```lua +function on_torrent_finished(event) + print("torrent " .. event.torrent_id .. " complete") +end +``` + +> Which fields carry meaningful data depends on the component that emits the +> event (engine, a plugin, or the `emit` RPC). The table above lists the +> **conventional** payload for each type. Always guard optional fields +> (`if event.path then ... end`) — `message` and `path` are `nil` when the +> emitter didn't set them. + +--- + +## 5. The `event` object + +The single argument to every hook is a plain Lua table with these fields: + +| Field | Lua type | Always present | Meaning | +|---|---|---|---| +| `event.type` | `string` | yes | the event name, e.g. `"file_complete"` (see §8) | +| `event.torrent_id` | `integer` | yes | the torrent this event belongs to (0 if not torrent-scoped) | +| `event.index` | `integer` | yes | a type-specific index — piece index for `piece_complete`, file index for `file_complete`; 0 otherwise | +| `event.message` | `string` | no | free-form text; `nil` when unset | +| `event.path` | `string` | no | a filesystem path (e.g. the completed file); `nil` when unset | + +The table is freshly created per call; mutating it has no effect on the engine +and the table is not reused between events. + +```lua +function on_file_complete(event) + assert(event.type == "file_complete") + print(("file #%d of torrent %d done: %s") + :format(event.index, event.torrent_id, event.path or "?")) +end +``` + +--- + +## 6. The `naut` API table + +A single global table, `naut`, exposes the actions a script may take. + +### `naut.move_file(torrent_id, file_index, destination)` + +Move one **completed** file of a torrent to `destination` (the headline +"move files as they finish" feature). Typically called from `on_file_complete`. + +**Arguments** + +| # | Name | Lua type | Notes | +|---|---|---|---| +| 1 | `torrent_id` | `integer` | must be ≥ 0; identifies a torrent registered with the daemon (see prerequisite below) | +| 2 | `file_index` | `integer` | must be in `[0, 2³²-1]`; index of the file within the torrent | +| 3 | `destination` | `string` | target path to move the file to | + +**Return value:** none on success. + +**Asynchronous semantics — important.** `move_file` does **not** perform the +move inline on the script thread. It enqueues a bounded command that the daemon +**owner thread** executes shortly after (this preserves the engine's +shared-nothing threading: the script thread never touches storage directly). +So a successful call means *"the move was accepted"*, not *"the file has moved."* +The actual relocate (a `rename`, or copy+unlink across filesystems) runs later; +its success or failure is reported in the **daemon log** and reflected in the +move/relocate side effects — it is **not** returned to the script. + +**Errors (raised as Lua errors — catch with `pcall` if you don't want them to +abort the hook):** + +| Message | Cause | +|---|---| +| `move_file arguments out of range` | `torrent_id < 0`, `file_index < 0`, or `file_index > 2³²-1` | +| `move_file is unavailable` | the host did not wire a move handler | +| `move_file failed: ` | the host rejected the command; the integer is a `naut_err` — most commonly `-8` (queue full / backpressure: the owner thread is behind on draining moves) | + +```lua +function on_file_complete(event) + local ok, err = pcall(naut.move_file, + event.torrent_id, event.index, + "/archive/" .. event.path) + if not ok then + print("could not queue move: " .. tostring(err)) + end +end +``` + +**Prerequisite — register the torrent's storage first.** `move_file` resolves +`torrent_id` through the daemon's torrent registry (`naut_session`). Register a +torrent's storage with the `add_torrent` RPC before any move command can take +effect: + +```sh +nautctl --socket /tmp/nautd.sock add_torrent \ + '{"torrent_id":42,"torrent":"file.torrent","root":"/downloads/42"}' +``` + +If `torrent_id` is unknown when the move drains, the relocate fails with +"not found" in the daemon log (the Lua call itself still succeeded, because it +only *queued* the command). + +--- + +## 7. Error handling & observability + +- A hook that raises is caught; the error text is stored as the script's + **last error** and an error counter is incremented. The script keeps running. +- A global named after a hook that is **not a function** (e.g. you set + `on_alert = 5`) is treated as an error for that event. +- Counters are exposed over RPC via `nautctl status` under the `script` object: + + | Counter | Meaning | + |---|---| + | `queued` | events accepted into the script queue | + | `handled` | hook invocations that returned without error | + | `dropped` | events discarded because the queue was full | + | `errors` | hooks that raised, weren't functions, or had an unknown type | + | `move_requests` | successful `naut.move_file` calls (commands queued) | + +```sh +nautctl --socket /tmp/nautd.sock status +# { ... "script": { "queued": 12, "handled": 12, "dropped": 0, +# "errors": 0, "move_requests": 3 }, ... } +``` + +--- + +## 8. Event type name strings + +`event.type` (and the `emit` RPC `type` field) uses these exact strings: + +`torrent_added`, `piece_complete`, `file_complete`, `torrent_finished`, +`peer_connected`, `alert`. + +You can inject any of them for testing with the `emit` RPC: + +```sh +nautctl --socket /tmp/nautd.sock emit \ + '{"type":"file_complete","torrent_id":42,"index":0,"path":"/downloads/42/movie.mkv"}' +``` + +--- + +## 9. Complete example + +```lua +-- archive-on-finish.lua +-- Move each file to /archive as it completes, and log torrent completion. + +local archive = "/archive" + +function on_file_complete(event) + if not event.path then return end + local name = event.path:match("[^/]+$") or event.path + local ok, err = pcall(naut.move_file, + event.torrent_id, event.index, + archive .. "/" .. name) + if ok then + print(("archived file #%d of torrent %d -> %s/%s") + :format(event.index, event.torrent_id, archive, name)) + else + print("move failed to queue: " .. tostring(err)) + end +end + +function on_torrent_finished(event) + print("torrent " .. event.torrent_id .. " fully downloaded") +end +``` + +Run it: + +```sh +nautd --socket /tmp/nautd.sock --script ./archive-on-finish.lua & +nautctl --socket /tmp/nautd.sock add_torrent \ + '{"torrent_id":1,"torrent":"big.torrent","root":"/downloads/1"}' +``` diff --git a/include/naut/bencode.h b/include/naut/bencode.h new file mode 100644 index 0000000..29b31ae --- /dev/null +++ b/include/naut/bencode.h @@ -0,0 +1,77 @@ +/* bencode.h — bencode parser/encoder for .torrent, tracker, and DHT messages. + * + * The parser is zero-copy: strings are slices into the caller's buffer, never + * duplicated. Every parsed value also records its exact raw byte span, which is + * what lets us compute an info-hash over the *original* encoding of the `info` + * dict without re-serializing (re-serialization would risk a non-canonical + * byte sequence and a wrong hash). Container children live in an arena owned by + * the document, so the whole tree frees in one shot. + * + * Hardened for hostile input (it parses bytes straight off the wire): explicit + * depth and node-count limits, strict integer/string syntax, no unbounded + * recursion blow-up. This is a primary fuzz target. + */ +#ifndef NAUT_BENCODE_H +#define NAUT_BENCODE_H + +#include "naut/common.h" + +typedef enum { + NAUT_BC_INT, NAUT_BC_STR, NAUT_BC_LIST, NAUT_BC_DICT +} naut_bc_type; + +typedef struct naut_bc naut_bc; + +typedef struct { + const uint8_t *kp; size_t kn; /* key slice (bencode dict keys are strings) */ + const naut_bc *val; +} naut_bc_pair; + +struct naut_bc { + naut_bc_type type; + union { + int64_t i; + struct { const uint8_t *p; size_t n; } str; + struct { const naut_bc *items; size_t count; } list; + struct { const naut_bc_pair *pairs; size_t count; } dict; + } v; + const uint8_t *raw; /* exact encoding span of this value... */ + size_t raw_len; /* ...used for info-hash over the original bytes */ +}; + +typedef struct naut_bc_doc naut_bc_doc; + +/* Parse the entire buffer as one bencode value. Rejects trailing garbage. + * On success *out owns the tree (free with naut_bc_free). */ +naut_err naut_bc_parse(const uint8_t *data, size_t len, naut_bc_doc **out); +/* Parse one value from the start of a larger buffer. `consumed` receives the + * encoded value length; trailing bytes remain owned by the caller. */ +naut_err naut_bc_parse_prefix(const uint8_t *data, size_t len, + naut_bc_doc **out, size_t *consumed); +const naut_bc *naut_bc_root(const naut_bc_doc *doc); +void naut_bc_free(naut_bc_doc *doc); + +/* Accessors (return NULL / defaults on type mismatch). */ +const naut_bc *naut_bc_dict_get(const naut_bc *d, const char *key); +const naut_bc *naut_bc_list_at(const naut_bc *l, size_t i); +bool naut_bc_get_int(const naut_bc *v, int64_t *out); +bool naut_bc_get_str(const naut_bc *v, const uint8_t **p, size_t *n); +/* true if a STR value equals the given C string exactly */ +bool naut_bc_str_eq(const naut_bc *v, const char *s); + +/* --- encoder (DHT/tracker/.torrent writing): append to a growable buffer --- */ +typedef struct { + uint8_t *buf; size_t len, cap; + naut_err err; +} naut_bc_writer; + +void naut_bc_w_init(naut_bc_writer *w); +void naut_bc_w_free(naut_bc_writer *w); +void naut_bc_w_int(naut_bc_writer *w, int64_t v); +void naut_bc_w_bytes(naut_bc_writer *w, const void *p, size_t n); +void naut_bc_w_cstr(naut_bc_writer *w, const char *s); +void naut_bc_w_list_begin(naut_bc_writer *w); +void naut_bc_w_dict_begin(naut_bc_writer *w); +void naut_bc_w_end(naut_bc_writer *w); /* closes the current list/dict */ + +#endif /* NAUT_BENCODE_H */ diff --git a/include/naut/bitfield.h b/include/naut/bitfield.h new file mode 100644 index 0000000..6e1cf6b --- /dev/null +++ b/include/naut/bitfield.h @@ -0,0 +1,49 @@ +/* bitfield.h — fixed-size bitset over uint64 words with hardware popcount. + * + * Backs every "set of pieces/blocks" in the engine: a peer's have-set, our own + * completed pieces, the in-flight request map, interested/choked flags. The + * count()/find operations use __builtin_popcountll and __builtin_ctzll so a + * rarest-first picker can scan availability cheaply even for huge torrents. + */ +#ifndef NAUT_BITFIELD_H +#define NAUT_BITFIELD_H + +#include "naut/common.h" + +typedef struct naut_bitfield { + uint64_t *words; + size_t nbits; + size_t nwords; +} naut_bitfield; + +naut_err naut_bitfield_init(naut_bitfield *bf, size_t nbits); +void naut_bitfield_free(naut_bitfield *bf); + +NAUT_INLINE bool naut_bitfield_test(const naut_bitfield *bf, size_t i) { + return (bf->words[i >> 6] >> (i & 63)) & 1u; +} +NAUT_INLINE void naut_bitfield_set(naut_bitfield *bf, size_t i) { + bf->words[i >> 6] |= (uint64_t)1 << (i & 63); +} +NAUT_INLINE void naut_bitfield_clear(naut_bitfield *bf, size_t i) { + bf->words[i >> 6] &= ~((uint64_t)1 << (i & 63)); +} + +void naut_bitfield_set_all(naut_bitfield *bf); +void naut_bitfield_clear_all(naut_bitfield *bf); + +/* number of set bits */ +size_t naut_bitfield_count(const naut_bitfield *bf); +/* true when all nbits bits are set (torrent complete) */ +bool naut_bitfield_all_set(const naut_bitfield *bf); + +/* index of first 0 / first 1 bit at or after `from`, or SIZE_MAX if none */ +size_t naut_bitfield_find_zero(const naut_bitfield *bf, size_t from); +size_t naut_bitfield_find_set(const naut_bitfield *bf, size_t from); + +/* Load/serialize the BEP-3 wire format: MSB-first within each byte. The wire + * order differs from our little-endian word order, so these are not memcpy. */ +void naut_bitfield_from_wire(naut_bitfield *bf, const uint8_t *bytes, size_t nbytes); +void naut_bitfield_to_wire(const naut_bitfield *bf, uint8_t *bytes, size_t nbytes); + +#endif /* NAUT_BITFIELD_H */ diff --git a/include/naut/buf.h b/include/naut/buf.h new file mode 100644 index 0000000..cd335ea --- /dev/null +++ b/include/naut/buf.h @@ -0,0 +1,63 @@ +/* buf.h — page-aligned, refcounted buffer pool. + * + * This is the spine of the zero-copy data path. One physical buffer is filled + * by recv (io_uring registered buffer), decrypted in place, hashed in place by + * a worker thread, then written to disk via O_DIRECT or sent via SEND_ZC — the + * same pages throughout, never memcpy'd. + * + * Ownership model (ABA-free without tagging): + * - naut_buf_get() pops from the freelist and is SINGLE-CONSUMER: only the + * pool's owning reactor thread may call it. + * - naut_buf_put()/naut_buf_ref() are MULTI-PRODUCER: any thread (e.g. a hash + * worker that finished with a buffer) may call them. put() pushes back onto + * the freelist only on the 1->0 refcount transition. + * Because only the owner pops, a buffer in flight is never re-pushed by another + * thread, so the Treiber-stack pop has no ABA hazard. + */ +#ifndef NAUT_BUF_H +#define NAUT_BUF_H + +#include "naut/common.h" + +typedef struct naut_bufpool naut_bufpool; + +typedef struct naut_buf { + _Atomic uint32_t refcnt; /* live references; 0 => on freelist */ + uint32_t len; /* bytes of valid payload in data[] */ + uint32_t cap; /* == pool block_size */ + uint32_t idx; /* index within the pool (for registered bufs)*/ + struct naut_buf *fnext; /* freelist link (owner-thread access only) */ + naut_bufpool *pool; + uint8_t *data; /* page-aligned, cap bytes */ +} naut_buf; + +/* block_size must be a multiple of NAUT_PAGE (O_DIRECT alignment). + * If use_hugepages, the data slab is mmap'd with MAP_HUGETLB (falls back to + * normal pages if unavailable). */ +naut_bufpool *naut_bufpool_create(uint32_t block_size, uint32_t block_count, + bool use_hugepages); +naut_bufpool *naut_bufpool_create_on_node(uint32_t block_size, + uint32_t block_count, + bool use_hugepages, + int numa_node); +void naut_bufpool_destroy(naut_bufpool *p); + +/* Owner thread only. Returns NULL when exhausted (caller applies backpressure). + * Returned buffer has refcnt==1 and len==0. */ +naut_buf *naut_buf_get(naut_bufpool *p) NAUT_MUST_USE; + +/* Any thread. */ +NAUT_INLINE void naut_buf_ref(naut_buf *b) { + atomic_fetch_add_explicit(&b->refcnt, 1, memory_order_relaxed); +} +void naut_buf_put(naut_buf *b); + +/* Introspection (approximate under concurrency). */ +uint32_t naut_bufpool_capacity(const naut_bufpool *p); +uint32_t naut_bufpool_available(const naut_bufpool *p); + +/* Base of the contiguous data slab + total bytes — used to register the whole + * region with io_uring as a single fixed-buffer area. */ +void *naut_bufpool_slab(const naut_bufpool *p, size_t *out_bytes); + +#endif /* NAUT_BUF_H */ diff --git a/include/naut/common.h b/include/naut/common.h new file mode 100644 index 0000000..2884041 --- /dev/null +++ b/include/naut/common.h @@ -0,0 +1,59 @@ +/* common.h — project-wide types, attributes, and error codes. + * + * Pure C11. No allocation, no platform calls; safe to include everywhere. + */ +#ifndef NAUT_COMMON_H +#define NAUT_COMMON_H + +#include +#include +#include +#include + +/* --- sizes ------------------------------------------------------------- */ +#define NAUT_CACHELINE 64u +#define NAUT_PAGE 4096u +/* BitTorrent wire block size (BEP-3). The fundamental transfer unit. */ +#define NAUT_BLOCK (16u * 1024u) + +/* --- compiler attributes ----------------------------------------------- */ +#define NAUT_LIKELY(x) __builtin_expect(!!(x), 1) +#define NAUT_UNLIKELY(x) __builtin_expect(!!(x), 0) +#define NAUT_INLINE static inline __attribute__((always_inline)) +#define NAUT_ALIGNED(n) __attribute__((aligned(n))) +#define NAUT_CACHE_ALIGNED __attribute__((aligned(NAUT_CACHELINE))) +#define NAUT_NORETURN __attribute__((noreturn)) +#define NAUT_UNUSED __attribute__((unused)) +#define NAUT_PACKED __attribute__((packed)) +#define NAUT_MUST_USE __attribute__((warn_unused_result)) +#define NAUT_PRINTF(fi, ai) __attribute__((format(printf, fi, ai))) + +/* --- small helpers ----------------------------------------------------- */ +#define NAUT_ARRAY_LEN(a) (sizeof(a) / sizeof((a)[0])) +#define NAUT_MIN(a, b) ((a) < (b) ? (a) : (b)) +#define NAUT_MAX(a, b) ((a) > (b) ? (a) : (b)) +#define NAUT_ALIGN_UP(x, a) (((uintptr_t)(x) + ((a) - 1)) & ~((uintptr_t)(a) - 1)) +#define NAUT_ALIGN_DOWN(x, a) ((uintptr_t)(x) & ~((uintptr_t)(a) - 1)) +#define NAUT_IS_POW2(x) ((x) != 0 && (((x) & ((x) - 1)) == 0)) +#define NAUT_CONTAINER_OF(ptr, type, member) \ + ((type *)((char *)(1 ? (ptr) : &((type *)0)->member) - offsetof(type, member))) + +/* --- error codes ------------------------------------------------------- */ +typedef int naut_err; +enum { + NAUT_OK = 0, + NAUT_ERR_NOMEM = -1, + NAUT_ERR_INVAL = -2, + NAUT_ERR_IO = -3, + NAUT_ERR_AGAIN = -4, /* would block / retry */ + NAUT_ERR_PROTO = -5, /* protocol violation */ + NAUT_ERR_RANGE = -6, + NAUT_ERR_NOSYS = -7, /* unsupported by kernel/build */ + NAUT_ERR_FULL = -8, + NAUT_ERR_EMPTY = -9, + NAUT_ERR_NOTFOUND = -10, +}; + +const char *naut_strerror(naut_err e); + +#endif /* NAUT_COMMON_H */ diff --git a/include/naut/dht.h b/include/naut/dht.h new file mode 100644 index 0000000..d5b7a38 --- /dev/null +++ b/include/naut/dht.h @@ -0,0 +1,66 @@ +/* dht.h - BEP-5 KRPC codec and bounded IPv4 get_peers traversal. */ +#ifndef NAUT_DHT_H +#define NAUT_DHT_H + +#include "naut/common.h" +#include "naut/tracker.h" + +#define NAUT_DHT_ID_LEN 20 +#define NAUT_DHT_MAX_NODES 256 +#define NAUT_DHT_MAX_PEERS 256 + +typedef struct { + uint8_t id[NAUT_DHT_ID_LEN]; + uint8_t ip[4]; + uint16_t port; +} naut_dht_node; + +typedef enum { + NAUT_DHT_RESPONSE, + NAUT_DHT_ERROR +} naut_dht_message_type; + +typedef struct { + naut_dht_message_type type; + uint8_t transaction[8]; + size_t transaction_len; + uint8_t id[NAUT_DHT_ID_LEN]; + bool has_id; + uint8_t token[64]; + size_t token_len; + naut_dht_node *nodes; + size_t num_nodes; + naut_peer_addr *peers; + size_t num_peers; + int error_code; +} naut_dht_response; + +naut_err naut_dht_build_ping(const uint8_t *tx, size_t tx_len, + const uint8_t id[20], + uint8_t **out, size_t *out_len); +naut_err naut_dht_build_find_node(const uint8_t *tx, size_t tx_len, + const uint8_t id[20], + const uint8_t target[20], + uint8_t **out, size_t *out_len); +naut_err naut_dht_build_get_peers(const uint8_t *tx, size_t tx_len, + const uint8_t id[20], + const uint8_t info_hash[20], + uint8_t **out, size_t *out_len); +naut_err naut_dht_build_announce_peer(const uint8_t *tx, size_t tx_len, + const uint8_t id[20], + const uint8_t info_hash[20], + uint16_t port, bool implied_port, + const void *token, size_t token_len, + uint8_t **out, size_t *out_len); + +naut_err naut_dht_parse_response(const uint8_t *data, size_t len, + naut_dht_response *out); +void naut_dht_response_free(naut_dht_response *response); + +/* Query bootstrap endpoints ("host:port") and iteratively follow returned + * compact nodes until peers are found or the bounded traversal is exhausted. */ +naut_err naut_dht_get_peers(const char *const *bootstrap, size_t num_bootstrap, + const uint8_t info_hash[20], + naut_peer_addr **peers, size_t *num_peers); + +#endif /* NAUT_DHT_H */ diff --git a/include/naut/event.h b/include/naut/event.h new file mode 100644 index 0000000..b64c2be --- /dev/null +++ b/include/naut/event.h @@ -0,0 +1,42 @@ +/* event.h - control-plane event bus, never called from byte-processing workers. */ +#ifndef NAUT_EVENT_H +#define NAUT_EVENT_H + +#include "naut/common.h" + +typedef enum { + NAUT_EVENT_TORRENT_ADDED, + NAUT_EVENT_PIECE_COMPLETE, + NAUT_EVENT_FILE_COMPLETE, + NAUT_EVENT_TORRENT_FINISHED, + NAUT_EVENT_PEER_CONNECTED, + NAUT_EVENT_ALERT, +} naut_event_type; + +typedef struct { + naut_event_type type; + uint64_t torrent_id; + uint32_t index; + const char *message; + const char *path; +} naut_event; + +typedef void (*naut_event_cb)(void *context, const naut_event *event); + +typedef struct naut_event_bus naut_event_bus; + +naut_event_bus *naut_event_bus_create(void); +void naut_event_bus_destroy(naut_event_bus *bus); + +/* Subscribe/unsubscribe are control-thread operations. emit snapshots the + * subscriber list, allowing callbacks to register work without holding a bus + * lock. */ +naut_err naut_event_subscribe(naut_event_bus *bus, naut_event_cb callback, + void *context, uint64_t *subscription_id); +void naut_event_unsubscribe(naut_event_bus *bus, uint64_t subscription_id); +void naut_event_emit(naut_event_bus *bus, const naut_event *event); + +const char *naut_event_type_name(naut_event_type type); +bool naut_event_type_parse(const char *name, naut_event_type *type); + +#endif /* NAUT_EVENT_H */ diff --git a/include/naut/extension.h b/include/naut/extension.h new file mode 100644 index 0000000..b14f4de --- /dev/null +++ b/include/naut/extension.h @@ -0,0 +1,70 @@ +/* extension.h - BEP-10 transport, BEP-9 metadata, and BEP-11 PEX codecs. */ +#ifndef NAUT_EXTENSION_H +#define NAUT_EXTENSION_H + +#include "naut/common.h" +#include "naut/tracker.h" + +#define NAUT_EXT_UT_METADATA 1 +#define NAUT_EXT_UT_PEX 2 +#define NAUT_METADATA_BLOCK (16u * 1024u) +#define NAUT_METADATA_MAX (4u * 1024u * 1024u) + +typedef struct { + uint8_t ut_metadata; + uint8_t ut_pex; + uint32_t metadata_size; + uint32_t reqq; + uint16_t port; +} naut_ext_handshake; + +/* Build a complete peer-wire extended message (length, message ID 20, + * extension ID, payload). The returned buffer is malloc-owned. */ +naut_err naut_ext_build_handshake(uint8_t ut_metadata_id, uint8_t ut_pex_id, + uint32_t metadata_size, uint16_t port, + uint8_t **out, size_t *out_len); +naut_err naut_ext_parse_handshake(const uint8_t *payload, size_t len, + naut_ext_handshake *out); + +typedef enum { + NAUT_METADATA_REQUEST = 0, + NAUT_METADATA_DATA = 1, + NAUT_METADATA_REJECT = 2 +} naut_metadata_type; + +typedef struct { + naut_metadata_type type; + uint32_t piece; + uint32_t total_size; + const uint8_t *data; + size_t data_len; +} naut_metadata_msg; + +naut_err naut_metadata_build(uint8_t ext_id, naut_metadata_type type, + uint32_t piece, uint32_t total_size, + const void *data, size_t data_len, + uint8_t **out, size_t *out_len); +naut_err naut_metadata_parse(const uint8_t *payload, size_t len, + naut_metadata_msg *out); + +typedef struct { + naut_peer_addr *added; + uint8_t *added_flags; + size_t num_added; + naut_peer_addr *dropped; + size_t num_dropped; +} naut_pex_msg; + +naut_err naut_pex_build(uint8_t ext_id, const naut_pex_msg *msg, + uint8_t **out, size_t *out_len); +naut_err naut_pex_parse(const uint8_t *payload, size_t len, naut_pex_msg *out); +void naut_pex_free(naut_pex_msg *msg); + +/* Fetch and SHA-1 verify a v1 torrent's raw info dictionary from one peer. + * The returned buffer is malloc-owned. */ +naut_err naut_metadata_fetch(const naut_peer_addr *peer, + const uint8_t info_hash[20], + const uint8_t peer_id[20], + uint8_t **info, size_t *info_len); + +#endif /* NAUT_EXTENSION_H */ diff --git a/include/naut/hash.h b/include/naut/hash.h new file mode 100644 index 0000000..ea002d1 --- /dev/null +++ b/include/naut/hash.h @@ -0,0 +1,45 @@ +/* hash.h — SHA-1 and SHA-256, the throughput-critical primitives. + * + * SHA-1 backs BitTorrent v1 (piece hashes + info-hash). SHA-256 backs v2 + * (Merkle leaves + info-hash). SHA-256 has a runtime-dispatched SHA-NI path so + * a single core verifies well above the 1.25 GB/s the 10 GbE target demands; + * the portable scalar path is the fallback and the cross-check oracle. + */ +#ifndef NAUT_HASH_H +#define NAUT_HASH_H + +#include "naut/common.h" + +#define NAUT_SHA1_LEN 20 +#define NAUT_SHA256_LEN 32 + +/* --- SHA-1 ------------------------------------------------------------- */ +typedef struct { + uint32_t h[5]; + uint64_t len; /* total bytes hashed */ + uint8_t block[64]; + size_t used; +} naut_sha1_ctx; + +void naut_sha1_init(naut_sha1_ctx *c); +void naut_sha1_update(naut_sha1_ctx *c, const void *data, size_t len); +void naut_sha1_final(naut_sha1_ctx *c, uint8_t out[NAUT_SHA1_LEN]); +void naut_sha1(const void *data, size_t len, uint8_t out[NAUT_SHA1_LEN]); + +/* --- SHA-256 ----------------------------------------------------------- */ +typedef struct { + uint32_t h[8]; + uint64_t len; + uint8_t block[64]; + size_t used; +} naut_sha256_ctx; + +void naut_sha256_init(naut_sha256_ctx *c); +void naut_sha256_update(naut_sha256_ctx *c, const void *data, size_t len); +void naut_sha256_final(naut_sha256_ctx *c, uint8_t out[NAUT_SHA256_LEN]); +void naut_sha256(const void *data, size_t len, uint8_t out[NAUT_SHA256_LEN]); + +/* Which SHA-256 backend was selected at startup ("sha-ni" or "scalar"). */ +const char *naut_sha256_backend(void); + +#endif /* NAUT_HASH_H */ diff --git a/include/naut/list.h b/include/naut/list.h new file mode 100644 index 0000000..92113bf --- /dev/null +++ b/include/naut/list.h @@ -0,0 +1,47 @@ +/* list.h — intrusive circular doubly-linked list (header-only). + * + * Zero allocation: the node lives inside your struct. Recover the owner with + * NAUT_CONTAINER_OF. This is the workhorse list for peer sets, freelists of + * objects, timer wheels, etc. + */ +#ifndef NAUT_LIST_H +#define NAUT_LIST_H + +#include "naut/common.h" + +typedef struct naut_list { + struct naut_list *prev; + struct naut_list *next; +} naut_list; + +NAUT_INLINE void naut_list_init(naut_list *l) { l->prev = l; l->next = l; } +NAUT_INLINE bool naut_list_empty(const naut_list *l) { return l->next == l; } + +NAUT_INLINE void naut__link(naut_list *n, naut_list *p, naut_list *x) { + n->prev = p; n->next = x; p->next = n; x->prev = n; +} + +/* insert n at head / tail of list l */ +NAUT_INLINE void naut_list_push_front(naut_list *l, naut_list *n) { naut__link(n, l, l->next); } +NAUT_INLINE void naut_list_push_back(naut_list *l, naut_list *n) { naut__link(n, l->prev, l); } + +NAUT_INLINE void naut_list_del(naut_list *n) { + n->prev->next = n->next; + n->next->prev = n->prev; + n->prev = n->next = n; /* safe to del again / detect detached */ +} + +NAUT_INLINE naut_list *naut_list_front(const naut_list *l) { return l->next; } +NAUT_INLINE naut_list *naut_list_back(const naut_list *l) { return l->prev; } + +#define naut_list_entry(ptr, type, member) NAUT_CONTAINER_OF(ptr, type, member) + +#define naut_list_for_each(it, l) \ + for ((it) = (l)->next; (it) != (l); (it) = (it)->next) + +/* safe against deletion of the current node */ +#define naut_list_for_each_safe(it, tmp, l) \ + for ((it) = (l)->next, (tmp) = (it)->next; \ + (it) != (l); (it) = (tmp), (tmp) = (it)->next) + +#endif /* NAUT_LIST_H */ diff --git a/include/naut/log.h b/include/naut/log.h new file mode 100644 index 0000000..7ced41f --- /dev/null +++ b/include/naut/log.h @@ -0,0 +1,45 @@ +/* log.h — leveled logging. + * + * Phase 1: a straightforward thread-safe stderr logger (one writev per record, + * so lines never interleave). The data path does not log per-message; this is + * for lifecycle, errors, and stats. A lock-free per-thread ring drain is a + * later optimization behind the same macros, so call sites never change. + */ +#ifndef NAUT_LOG_H +#define NAUT_LOG_H + +#include "naut/common.h" + +typedef enum { + NAUT_LOG_ERROR = 0, + NAUT_LOG_WARN, + NAUT_LOG_INFO, + NAUT_LOG_DEBUG, + NAUT_LOG_TRACE, +} naut_log_level; + +void naut_log_set_level(naut_log_level lvl); +naut_log_level naut_log_get_level(void); + +void naut_log_emit(naut_log_level lvl, const char *file, int line, + const char *fmt, ...) NAUT_PRINTF(4, 5); + +#define NAUT_LOG(lvl, ...) \ + do { if ((lvl) <= naut_log_get_level()) \ + naut_log_emit((lvl), __FILE__, __LINE__, __VA_ARGS__); } while (0) + +#define NAUT_ERROR(...) NAUT_LOG(NAUT_LOG_ERROR, __VA_ARGS__) +#define NAUT_WARN(...) NAUT_LOG(NAUT_LOG_WARN, __VA_ARGS__) +#define NAUT_INFO(...) NAUT_LOG(NAUT_LOG_INFO, __VA_ARGS__) +#define NAUT_DEBUG(...) NAUT_LOG(NAUT_LOG_DEBUG, __VA_ARGS__) +#define NAUT_TRACE(...) NAUT_LOG(NAUT_LOG_TRACE, __VA_ARGS__) + +/* Fatal: log and abort(). Use only for unrecoverable invariant violations. */ +NAUT_NORETURN void naut_panic(const char *file, int line, const char *fmt, ...) + NAUT_PRINTF(3, 4); +#define NAUT_PANIC(...) naut_panic(__FILE__, __LINE__, __VA_ARGS__) + +#define NAUT_ASSERT(cond) \ + do { if (NAUT_UNLIKELY(!(cond))) NAUT_PANIC("assertion failed: %s", #cond); } while (0) + +#endif /* NAUT_LOG_H */ diff --git a/include/naut/merkle.h b/include/naut/merkle.h new file mode 100644 index 0000000..fd4ee2f --- /dev/null +++ b/include/naut/merkle.h @@ -0,0 +1,40 @@ +/* merkle.h — BitTorrent v2 (BEP-52) SHA-256 Merkle trees. + * + * In v2 each file is split into 16 KiB leaf blocks; the leaf hashes form a + * binary Merkle tree whose interior nodes are SHA-256(left || right). When the + * leaf count is not a power of two, the tree is padded with *zero hashes* (a + * block of 32 zero bytes at the leaf level, then SHA-256 of two children up the + * tree) so the shape is a perfect binary tree. The root at the "piece layer" + * boundary gives per-piece verifiability; the whole-file root goes in the + * metainfo file tree. + * + * This module provides the tree primitive; metainfo/storage wire it to files. + */ +#ifndef NAUT_MERKLE_H +#define NAUT_MERKLE_H + +#include "naut/common.h" +#include "naut/hash.h" + +#define NAUT_MERKLE_LEAF (16u * 1024u) /* BEP-52 block size */ + +/* Compute the Merkle root of `nleaves` 32-byte leaf hashes, padding up to the + * next power of two with zero hashes. nleaves==0 yields the all-zero hash. + * `leaves` is nleaves*32 bytes; out is 32 bytes. Scratch is allocated + * internally. Returns NAUT_OK or NAUT_ERR_NOMEM. */ +naut_err naut_merkle_root(const uint8_t *leaves, size_t nleaves, + uint8_t out[NAUT_SHA256_LEN]); + +/* As above but pad to a fixed `block_count` (>= nleaves, power of two) rather + * than the next power of two — used to compute a piece-layer root where the + * tree height is fixed by the piece size. */ +naut_err naut_merkle_root_padded(const uint8_t *leaves, size_t nleaves, + size_t block_count, + uint8_t out[NAUT_SHA256_LEN]); + +/* Hash a contiguous data buffer into leaf hashes (one SHA-256 per 16 KiB, last + * leaf may be short). out must hold ceil(len/16KiB)*32 bytes. Returns the leaf + * count. */ +size_t naut_merkle_leaves(const uint8_t *data, size_t len, uint8_t *out); + +#endif /* NAUT_MERKLE_H */ diff --git a/include/naut/metainfo.h b/include/naut/metainfo.h new file mode 100644 index 0000000..4358e0d --- /dev/null +++ b/include/naut/metainfo.h @@ -0,0 +1,68 @@ +/* metainfo.h — .torrent and magnet parsing (v1 / v2 / hybrid). + * + * The info-hash is computed over the *raw* bytes of the `info` dictionary as + * they appear in the file (bencode preserves that span), never by re-encoding: + * v1 info-hash = SHA-1 (info-bytes) + * v2 info-hash = SHA-256(info-bytes) + * A hybrid torrent carries both and so joins both the v1 and v2 swarms. + * + * Phase 2 parses v1 fully (name, piece length, file list, the 20-byte piece + * hash table) and computes the v2 info-hash + version for hybrid/v2 files; the + * full v2 file-tree / piece-layer plumbing is wired in Phase 3 (storage). + */ +#ifndef NAUT_METAINFO_H +#define NAUT_METAINFO_H + +#include "naut/common.h" +#include "naut/hash.h" + +typedef struct { + char *path; /* '/'-joined, NUL-terminated */ + int64_t length; +} naut_file; + +typedef struct naut_metainfo { + bool has_v1, has_v2; + uint8_t infohash_v1[NAUT_SHA1_LEN]; + uint8_t infohash_v2[NAUT_SHA256_LEN]; + + char *name; + int64_t piece_length; + int64_t total_length; + + /* v1 piece hash table: num_pieces * 20 bytes (owned copy) */ + uint32_t num_pieces; + const uint8_t *piece_hashes; + + naut_file *files; size_t num_files; + char **trackers; size_t num_trackers; /* announce + announce-list, flattened */ + + /* internals kept alive so piece_hashes/name stay valid */ + void *_owned; +} naut_metainfo; + +naut_err naut_metainfo_parse(const uint8_t *data, size_t len, naut_metainfo *out); +/* Parse a raw bencoded info dictionary obtained through BEP-9. Optional + * tracker URLs are copied into the resulting metainfo. */ +naut_err naut_metainfo_parse_info(const uint8_t *info, size_t info_len, + const char *const *trackers, + size_t num_trackers, + naut_metainfo *out); +void naut_metainfo_free(naut_metainfo *mi); + +/* hex of the v1 info-hash (41 bytes incl NUL) — for logging/RPC. */ +void naut_infohash_v1_hex(const naut_metainfo *mi, char out[41]); + +/* --- magnet links -------------------------------------------------------- */ +typedef struct { + bool has_v1, has_v2; + uint8_t infohash_v1[NAUT_SHA1_LEN]; + uint8_t infohash_v2[NAUT_SHA256_LEN]; + char *name; /* dn (display name), may be NULL */ + char **trackers; size_t num_trackers; /* tr= params */ +} naut_magnet; + +naut_err naut_magnet_parse(const char *uri, naut_magnet *out); +void naut_magnet_free(naut_magnet *m); + +#endif /* NAUT_METAINFO_H */ diff --git a/include/naut/mpmc.h b/include/naut/mpmc.h new file mode 100644 index 0000000..5685a2e --- /dev/null +++ b/include/naut/mpmc.h @@ -0,0 +1,34 @@ +/* mpmc.h — bounded lock-free queue of pointers (Dmitry Vyukov's algorithm). + * + * One implementation covers every cross-thread hand-off in the engine: + * - control-plane -> reactor command queues (MPSC usage) + * - reactor -> hash worker pool job queue (MPMC usage) + * - worker -> reactor completion queue (MPSC usage) + * Wait-free in the common case, no per-op allocation. Capacity is fixed at + * init and must be a power of two. + */ +#ifndef NAUT_MPMC_H +#define NAUT_MPMC_H + +#include "naut/common.h" + +typedef struct naut_mpmc_cell { + _Atomic size_t seq; + void *data; +} naut_mpmc_cell; + +typedef struct naut_mpmc { + NAUT_CACHE_ALIGNED naut_mpmc_cell *buffer; + size_t mask; + NAUT_CACHE_ALIGNED _Atomic size_t enqueue_pos; + NAUT_CACHE_ALIGNED _Atomic size_t dequeue_pos; +} naut_mpmc; + +naut_err naut_mpmc_init(naut_mpmc *q, size_t capacity_pow2); +void naut_mpmc_destroy(naut_mpmc *q); + +/* Both return false without blocking when full/empty respectively. */ +bool naut_mpmc_push(naut_mpmc *q, void *p); +bool naut_mpmc_pop(naut_mpmc *q, void **out); + +#endif /* NAUT_MPMC_H */ diff --git a/include/naut/mse.h b/include/naut/mse.h new file mode 100644 index 0000000..7d76dc3 --- /dev/null +++ b/include/naut/mse.h @@ -0,0 +1,88 @@ +/* mse.h - BitTorrent Message Stream Encryption (MSE/PE) transport. */ +#ifndef NAUT_MSE_H +#define NAUT_MSE_H + +#include "naut/common.h" +#include "naut/peer.h" +#include "naut/rc4.h" + +#include + +#define NAUT_MSE_DH_LEN 96 + +typedef struct { + naut_rc4 send; + naut_rc4 recv; + bool active; +} naut_mse_stream; + +/* ---- sans-IO handshake state machine ------------------------------------- * + * The outgoing MSE/PE handshake as a pure state machine over byte buffers — no + * sockets — so the same logic drives the blocking apps and the io_uring reactor + * (where blocking in a handshake would stall a whole core's worth of peers). + * + * Drive it like a codec: pump NEED_WRITE bytes out, feed NEED_READ bytes in, + * repeat until DONE or ERROR, then call _finish(). + * + * h = naut_mse_handshake_begin(info_hash, peer_id, reserved); + * for (;;) switch (naut_mse_handshake_status(h)) { + * case NAUT_MSE_HS_NEED_WRITE: pull bytes, write them to the peer; break; + * case NAUT_MSE_HS_NEED_READ: read bytes from the peer, feed them; break; + * case NAUT_MSE_HS_DONE: naut_mse_handshake_finish(h, ...); goto ok; + * case NAUT_MSE_HS_ERROR: ... ; goto err; + * } + */ +typedef enum { + NAUT_MSE_HS_NEED_READ, + NAUT_MSE_HS_NEED_WRITE, + NAUT_MSE_HS_DONE, + NAUT_MSE_HS_ERROR, +} naut_mse_hs_status; + +typedef struct naut_mse_handshake naut_mse_handshake; + +naut_mse_handshake *naut_mse_handshake_begin( + const uint8_t info_hash[20], + const uint8_t peer_id[NAUT_PEERID_LEN], + uint64_t reserved); +void naut_mse_handshake_free(naut_mse_handshake *h); + +naut_mse_hs_status naut_mse_handshake_status(const naut_mse_handshake *h); + +/* Copy pending outgoing bytes into buf (up to cap); returns the count, 0 when + * nothing is queued. Call repeatedly until it returns 0. */ +size_t naut_mse_handshake_pull(naut_mse_handshake *h, uint8_t *buf, size_t cap); + +/* Feed received bytes; *consumed reports how many were absorbed (the rest, if + * any, must be re-fed — after DONE that remainder is the start of the encrypted + * payload stream). Returns the new status. */ +naut_mse_hs_status naut_mse_handshake_feed(naut_mse_handshake *h, + const uint8_t *data, size_t len, + size_t *consumed); + +/* Valid once status is DONE: hand out the negotiated stream and the peer's + * decrypted BitTorrent handshake. */ +naut_err naut_mse_handshake_finish(naut_mse_handshake *h, + naut_mse_stream *stream, + uint8_t remote_handshake[NAUT_HANDSHAKE_LEN]); + +/* Blocking convenience wrapper over the state machine: perform the whole + * outgoing handshake on a blocking socket, offering RC4 only. The BitTorrent + * handshake is carried as IA; the peer's decrypted handshake is returned in + * remote_handshake. */ +naut_err naut_mse_client_handshake( + int fd, + const uint8_t info_hash[20], + const uint8_t peer_id[NAUT_PEERID_LEN], + uint64_t reserved, + naut_mse_stream *stream, + uint8_t remote_handshake[NAUT_HANDSHAKE_LEN]); + +/* Stream I/O after a successful handshake. Encryption/decryption is in-place + * with connection-owned RC4 state. send_all preserves the caller's buffer. */ +bool naut_mse_send_all(int fd, naut_mse_stream *stream, + const void *data, size_t len); +ssize_t naut_mse_recv(int fd, naut_mse_stream *stream, + void *data, size_t len); + +#endif /* NAUT_MSE_H */ diff --git a/include/naut/naut_plugin.h b/include/naut/naut_plugin.h new file mode 100644 index 0000000..5613591 --- /dev/null +++ b/include/naut/naut_plugin.h @@ -0,0 +1,49 @@ +/* naut_plugin.h - stable versioned native plugin ABI. */ +#ifndef NAUT_PLUGIN_H +#define NAUT_PLUGIN_H + +#include "naut/common.h" +#include "naut/event.h" + +#define NAUT_PLUGIN_ABI_VERSION 1u + +typedef struct { + uint32_t abi_version; + uint32_t struct_size; + const char *name; + void *(*open)(const char *root, naut_err *error); + void (*close)(void *storage); + naut_err (*read)(void *storage, int64_t offset, void *buffer, size_t length); + naut_err (*write)(void *storage, int64_t offset, + const void *buffer, size_t length); +} naut_storage_backend_v1; + +/* request_json is a JSON object or null. response_json must be malloc-owned + * compact JSON on success; the host frees it after parsing. */ +typedef naut_err (*naut_plugin_rpc_fn)(void *context, + const char *request_json, + char **response_json); +typedef void (*naut_plugin_event_fn)(void *context, + const naut_event *event); + +typedef struct naut_host_api { + uint32_t abi_version; + uint32_t struct_size; + void *host_context; + + naut_err (*set_plugin_name)(void *host_context, const char *name); + naut_err (*register_rpc)(void *host_context, const char *method, + naut_plugin_rpc_fn callback, void *context); + naut_err (*register_storage_backend)( + void *host_context, const naut_storage_backend_v1 *backend); + naut_err (*subscribe_event)(void *host_context, + naut_plugin_event_fn callback, + void *context); + void (*emit_event)(void *host_context, const naut_event *event); + void (*log)(void *host_context, int level, const char *message); +} naut_host_api; + +/* Every plugin exports this exact symbol. */ +typedef naut_err (*naut_plugin_register_fn)(const naut_host_api *host); + +#endif /* NAUT_PLUGIN_H */ diff --git a/include/naut/net.h b/include/naut/net.h new file mode 100644 index 0000000..b88ffce --- /dev/null +++ b/include/naut/net.h @@ -0,0 +1,26 @@ +/* net.h — socket setup (the kernel-facing seam, part 1). + * + * All socket option choices that matter for 10 GbE live here: SO_REUSEPORT so + * each reactor owns a listen queue and the kernel shards inbound peers across + * cores, TCP_NODELAY (BitTorrent is latency-sensitive on small control msgs), + * and large socket buffers so the BDP fits. + */ +#ifndef NAUT_NET_H +#define NAUT_NET_H + +#include "naut/common.h" +#include + +/* Create a TCP listener bound to `port`. With reuseport=true, multiple reactors + * may each create one on the same port and the kernel load-balances accepts. */ +int naut_net_listen(uint16_t port, int backlog, bool reuseport); + +/* Tune an accepted/connected peer socket for throughput. */ +void naut_net_tune_peer(int fd); + +/* Set send/recv socket buffer sizes (bytes); 0 leaves the kernel default. */ +void naut_net_set_bufsizes(int fd, int sndbuf, int rcvbuf); + +int naut_net_set_nonblock(int fd, bool on); + +#endif /* NAUT_NET_H */ diff --git a/include/naut/peer.h b/include/naut/peer.h new file mode 100644 index 0000000..976d309 --- /dev/null +++ b/include/naut/peer.h @@ -0,0 +1,72 @@ +/* peer.h — BitTorrent peer wire protocol (BEP-3), sans-IO. + * + * This is a pure codec: it never touches a socket. You feed it bytes and it + * yields parsed messages; you ask it to build a message and it writes bytes. + * That keeps the protocol unit-testable in isolation and lets the SAME codec be + * driven by the simple blocking leecher (Phase 3) and by the io_uring reactor + * (Phase 6) without change — the I/O strategy is somebody else's problem. + */ +#ifndef NAUT_PEER_H +#define NAUT_PEER_H + +#include "naut/common.h" + +#define NAUT_HANDSHAKE_LEN 68 +#define NAUT_PEERID_LEN 20 +/* Reject absurd length prefixes early: largest legitimate message is a PIECE, + * 9 + block. Allow generous slack over the 16 KiB default block. */ +#define NAUT_MSG_MAX (1u << 20) + +typedef enum { + NAUT_MSG_CHOKE = 0, + NAUT_MSG_UNCHOKE = 1, + NAUT_MSG_INTERESTED = 2, + NAUT_MSG_NOT_INTERESTED = 3, + NAUT_MSG_HAVE = 4, + NAUT_MSG_BITFIELD = 5, + NAUT_MSG_REQUEST = 6, + NAUT_MSG_PIECE = 7, + NAUT_MSG_CANCEL = 8, + NAUT_MSG_PORT = 9, + NAUT_MSG_EXTENDED = 20, /* BEP-10 extension payload */ + NAUT_MSG_KEEPALIVE = 255, /* synthetic: length-prefix of 0 */ +} naut_msg_type; + +typedef struct { + naut_msg_type type; + uint32_t index; /* HAVE/REQUEST/PIECE/CANCEL */ + uint32_t begin; /* REQUEST/PIECE/CANCEL */ + uint32_t length; /* REQUEST/CANCEL; PIECE: block length */ + const uint8_t *payload; /* PIECE: block bytes; BITFIELD: bytes */ + size_t payload_len; +} naut_msg; + +/* --- handshake ----------------------------------------------------------- */ +void naut_peer_handshake_build(uint8_t out[NAUT_HANDSHAKE_LEN], + const uint8_t infohash[20], + const uint8_t peerid[NAUT_PEERID_LEN], + uint64_t reserved); +/* Returns true on a well-formed handshake with the expected protocol string. */ +bool naut_peer_handshake_parse(const uint8_t in[NAUT_HANDSHAKE_LEN], + uint8_t infohash[20], + uint8_t peerid[NAUT_PEERID_LEN], + uint64_t *reserved); + +/* --- decode -------------------------------------------------------------- */ +/* Parse one message from buf[0..len). Returns bytes consumed (>0) with *out + * filled (payload pointers alias into buf), 0 if more bytes are needed, or + * NAUT_ERR_PROTO (<0) on a malformed/oversized frame. */ +int naut_peer_msg_parse(const uint8_t *buf, size_t len, naut_msg *out); + +/* --- encode (all return bytes written) ----------------------------------- */ +size_t naut_peer_keepalive(uint8_t out[4]); +size_t naut_peer_msg_simple(uint8_t out[5], naut_msg_type t); /* choke..not_interested */ +size_t naut_peer_msg_have(uint8_t out[9], uint32_t index); +size_t naut_peer_msg_request(uint8_t out[17], uint32_t index, uint32_t begin, uint32_t length); +size_t naut_peer_msg_cancel(uint8_t out[17], uint32_t index, uint32_t begin, uint32_t length); +/* PIECE header (13 bytes); the block bytes follow separately on the wire. */ +size_t naut_peer_msg_piece_header(uint8_t out[13], uint32_t index, uint32_t begin, uint32_t block_len); +/* BITFIELD into out (must hold 5 + nbytes); returns total length. */ +size_t naut_peer_msg_bitfield(uint8_t *out, const uint8_t *bf, size_t nbytes); + +#endif /* NAUT_PEER_H */ diff --git a/include/naut/piece.h b/include/naut/piece.h new file mode 100644 index 0000000..59ffded --- /dev/null +++ b/include/naut/piece.h @@ -0,0 +1,86 @@ +/* piece.h — download state: block requests, piece assembly, verify, persist. + * + * A piece is assembled in memory as its blocks arrive, verified against the + * metainfo hash (SHA-1 for v1/hybrid), then written to storage in one shot — + * so a corrupt piece never reaches disk. + */ +#ifndef NAUT_PIECE_H +#define NAUT_PIECE_H + +#include "naut/common.h" +#include "naut/metainfo.h" +#include "naut/storage.h" +#include "naut/bitfield.h" +#include "naut/worker.h" + +typedef struct naut_download naut_download; + +naut_download *naut_download_create(const naut_metainfo *mi, naut_storage *st); +void naut_download_destroy(naut_download *d); + +/* Optional hash offload. Completed-piece SHA-1 jobs run on the worker pool; + * naut_download_poll() finalizes verified pieces on the owning engine thread. + * The pool must outlive the download. */ +void naut_download_set_worker_pool(naut_download *d, naut_worker_pool *pool); +naut_err naut_download_poll(naut_download *d, uint32_t *pieces_completed); + +/* ---- multi-peer swarm interface (Phase 4) ------------------------------- * + * Availability: report what each peer has so rarest-first can rank pieces. A + * peer's bitfield is added on connect (BITFIELD) and removed on disconnect; a + * single HAVE bumps one piece. */ +void naut_download_add_bitfield(naut_download *d, const naut_bitfield *peer_have); +void naut_download_remove_bitfield(naut_download *d, const naut_bitfield *peer_have); +void naut_download_inc_avail(naut_download *d, uint32_t piece); + +/* Pick the next block to request for a peer with `peer_have`. Uses rarest-first, + * prefers finishing in-progress pieces, and switches to endgame (allowing a + * block to be requested from multiple peers) when few blocks remain. Returns + * false if this peer has nothing useful to request right now. */ +bool naut_download_pick(naut_download *d, const naut_bitfield *peer_have, + uint32_t *index, uint32_t *begin, uint32_t *length); + +/* Endgame may duplicate a block across peers, but never back to the same peer. + * `peer_has_request` lets the caller expose that peer's current request set. + * Outside endgame this behaves exactly like naut_download_pick(). */ +typedef bool (*naut_request_active_cb)(void *ctx, uint32_t index, uint32_t begin); +bool naut_download_pick_for_peer(naut_download *d, const naut_bitfield *peer_have, + naut_request_active_cb peer_has_request, void *ctx, + uint32_t *index, uint32_t *begin, uint32_t *length); + +/* Release a request (peer disconnected, or cancel) so the block can be re-picked. */ +void naut_download_unrequest(naut_download *d, uint32_t index, uint32_t begin); + +bool naut_download_have(const naut_download *d, uint32_t piece); +bool naut_download_in_endgame(const naut_download *d); + +/* Per-file completion: fired the moment the last piece overlapping a file's byte + * range verifies (so the file's bytes on disk are final and it is safe to move). + * This is the engine seam for the user's "move files as they finish" feature — + * the scripting layer (Phase 7) forwards this to an on_file_complete hook and may + * then call naut_storage_relocate(). The callback runs on the engine thread; a + * script must marshal any action back through the command queue. + * + * NOTE: fires during naut_download_on_block(); a single block may complete + * several files (small files packed into one piece). Empty files are reported as + * complete via naut_download_file_complete() but do not fire the callback. */ +typedef void (*naut_file_complete_cb)(void *ctx, uint32_t file_index, const char *path); +void naut_download_set_file_cb(naut_download *d, naut_file_complete_cb cb, void *ctx); +bool naut_download_file_complete(const naut_download *d, uint32_t file_index); + +/* Hand out the next block to request. false => nothing left to hand out right + * now (all blocks have been requested). */ +bool naut_download_next_request(naut_download *d, + uint32_t *index, uint32_t *begin, uint32_t *length); + +/* Feed a received PIECE block. *piece_done is set true iff this block completed + * a piece that then verified and was written to storage. With a worker pool, + * completion is reported later through naut_download_poll(). */ +naut_err naut_download_on_block(naut_download *d, uint32_t index, uint32_t begin, + const uint8_t *data, uint32_t len, bool *piece_done); + +bool naut_download_complete(const naut_download *d); +uint32_t naut_download_num_pieces(const naut_download *d); +uint32_t naut_download_pieces_done(const naut_download *d); +uint64_t naut_download_bytes_done(const naut_download *d); + +#endif /* NAUT_PIECE_H */ diff --git a/include/naut/pipeline.h b/include/naut/pipeline.h new file mode 100644 index 0000000..76305dd --- /dev/null +++ b/include/naut/pipeline.h @@ -0,0 +1,29 @@ +/* pipeline.h - adaptive request window based on observed bandwidth-delay product. */ +#ifndef NAUT_PIPELINE_H +#define NAUT_PIPELINE_H + +#include "naut/common.h" + +typedef struct { + double rtt_seconds; + double bytes_per_second; + double last_sample_at; + uint32_t depth; + uint32_t min_depth; + uint32_t max_depth; + uint32_t block_size; +} naut_pipeline; + +void naut_pipeline_init(naut_pipeline *p, uint32_t block_size, + uint32_t min_depth, uint32_t max_depth, + uint32_t initial_depth); + +/* Record one completed request. sent_at and received_at are monotonic seconds. + * The controller smooths RTT and delivery rate, then targets 2x BDP to absorb + * scheduling jitter without allowing an unbounded request window. */ +void naut_pipeline_on_block(naut_pipeline *p, uint32_t bytes, + double sent_at, double received_at); + +uint32_t naut_pipeline_depth(const naut_pipeline *p); + +#endif /* NAUT_PIPELINE_H */ diff --git a/include/naut/plugin.h b/include/naut/plugin.h new file mode 100644 index 0000000..b93ab36 --- /dev/null +++ b/include/naut/plugin.h @@ -0,0 +1,25 @@ +/* plugin.h - native plugin loader and registered backend inventory. */ +#ifndef NAUT_PLUGIN_HOST_H +#define NAUT_PLUGIN_HOST_H + +#include "naut/event.h" +#include "naut/naut_plugin.h" +#include "naut/rpc.h" + +typedef struct naut_plugin_manager naut_plugin_manager; + +naut_plugin_manager *naut_plugin_manager_create( + naut_rpc_registry *rpc, naut_event_bus *events); +void naut_plugin_manager_destroy(naut_plugin_manager *manager); + +naut_err naut_plugin_load(naut_plugin_manager *manager, const char *path); + +size_t naut_plugin_count(const naut_plugin_manager *manager); +const char *naut_plugin_name(const naut_plugin_manager *manager, size_t index); +size_t naut_plugin_storage_count(const naut_plugin_manager *manager); +const char *naut_plugin_storage_name(const naut_plugin_manager *manager, + size_t index); +const naut_storage_backend_v1 *naut_plugin_storage_backend( + const naut_plugin_manager *manager, const char *name); + +#endif /* NAUT_PLUGIN_HOST_H */ diff --git a/include/naut/rc4.h b/include/naut/rc4.h new file mode 100644 index 0000000..0a37c83 --- /dev/null +++ b/include/naut/rc4.h @@ -0,0 +1,21 @@ +/* rc4.h — ARC4 stream cipher for MSE/PE (BitTorrent message-stream encryption). + * + * MSE keys RC4 from a SHA-1 of the negotiated DH secret and *discards the first + * 1024 keystream bytes* before use (the well-known RC4 keystream-bias defense), + * so naut_rc4_init takes a drop count. This is obfuscation, not strong crypto — + * its job is firewall/ISP evasion, and the engineering concern here is that it + * costs real CPU at 10 GbE (see the throughput budget), hence it lives behind a + * tight, in-place API. + */ +#ifndef NAUT_RC4_H +#define NAUT_RC4_H + +#include "naut/common.h" + +typedef struct { uint8_t s[256]; uint8_t i, j; } naut_rc4; + +void naut_rc4_init(naut_rc4 *c, const void *key, size_t keylen, size_t drop); +/* XOR keystream into buf in place (encrypt == decrypt). */ +void naut_rc4_xor(naut_rc4 *c, void *buf, size_t len); + +#endif /* NAUT_RC4_H */ diff --git a/include/naut/rpc.h b/include/naut/rpc.h new file mode 100644 index 0000000..eefefef --- /dev/null +++ b/include/naut/rpc.h @@ -0,0 +1,45 @@ +/* rpc.h - versioned length-prefixed control protocol and command registry. */ +#ifndef NAUT_RPC_H +#define NAUT_RPC_H + +#include "naut/common.h" +#include "naut/event.h" + +#include + +#define NAUT_RPC_VERSION 1 +#define NAUT_RPC_MAX_PAYLOAD (1u << 20) + +typedef enum { + NAUT_RPC_REQUEST = 1, + NAUT_RPC_RESPONSE = 2, + NAUT_RPC_EVENT = 3, +} naut_rpc_frame_type; + +typedef struct naut_rpc_registry naut_rpc_registry; +typedef json_t *(*naut_rpc_handler)(void *context, const json_t *params, + naut_err *error); + +naut_rpc_registry *naut_rpc_registry_create(void); +void naut_rpc_registry_destroy(naut_rpc_registry *registry); +naut_err naut_rpc_register(naut_rpc_registry *registry, const char *method, + naut_rpc_handler handler, void *context); +void naut_rpc_unregister(naut_rpc_registry *registry, const char *method); +json_t *naut_rpc_dispatch(naut_rpc_registry *registry, const char *method, + const json_t *params, naut_err *error); + +naut_err naut_rpc_send_json(int fd, naut_rpc_frame_type type, + const json_t *payload); +naut_err naut_rpc_recv_json(int fd, naut_rpc_frame_type *type, + json_t **payload); + +int naut_rpc_connect_unix(const char *socket_path); + +/* Connect to a UNIX socket and perform one request/response exchange. */ +naut_err naut_rpc_call(const char *socket_path, const char *method, + const json_t *params, json_t **response); + +/* Convert an event into the stable JSON event representation. */ +json_t *naut_rpc_event_json(const naut_event *event); + +#endif /* NAUT_RPC_H */ diff --git a/include/naut/script.h b/include/naut/script.h new file mode 100644 index 0000000..2ec8bb8 --- /dev/null +++ b/include/naut/script.h @@ -0,0 +1,36 @@ +/* script.h - sandboxed Lua event hooks on a dedicated bounded worker. */ +#ifndef NAUT_SCRIPT_H +#define NAUT_SCRIPT_H + +#include "naut/event.h" + +typedef struct naut_script naut_script; + +typedef naut_err (*naut_script_move_file_cb)(void *context, + uint64_t torrent_id, + uint32_t file_index, + const char *destination); + +typedef struct { + uint64_t queued; + uint64_t handled; + uint64_t dropped; + uint64_t errors; + uint64_t move_requests; +} naut_script_stats; + +/* script_path is loaded before the worker starts. queue_capacity bounds copied + * events and must be non-zero. The VM owns no filesystem or process APIs. */ +naut_script *naut_script_create(naut_event_bus *events, + const char *script_path, + size_t queue_capacity, + naut_script_move_file_cb move_file, + void *move_context, + naut_err *error); +void naut_script_destroy(naut_script *script); + +void naut_script_get_stats(const naut_script *script, + naut_script_stats *stats); +const char *naut_script_last_error(naut_script *script); + +#endif /* NAUT_SCRIPT_H */ diff --git a/include/naut/session.h b/include/naut/session.h new file mode 100644 index 0000000..2bbf3c6 --- /dev/null +++ b/include/naut/session.h @@ -0,0 +1,41 @@ +/* session.h — minimal torrent registry for the control plane. + * + * The control plane (RPC / plugins / scripts) refers to torrents by a stable + * uint64 id; the engine refers to them by their storage. This registry is the + * single map between the two, so a script-driven command like move_file can be + * resolved to a concrete naut_storage and acted on. + * + * Threading: the registry is *owner-thread confined*. In nautd every call + * (add/remove/move, all from RPC handlers and the move-drain) runs on the + * daemon's main thread, so no internal locking is needed. Commands originating + * on other threads (e.g. a script's naut.move_file) must be marshalled onto the + * owner thread first — which is exactly what nautd's bounded move queue does. + */ +#ifndef NAUT_SESSION_H +#define NAUT_SESSION_H + +#include "naut/common.h" +#include "naut/storage.h" + +typedef struct naut_session naut_session; + +naut_session *naut_session_create(void); +/* Closes every storage still registered. */ +void naut_session_destroy(naut_session *s); + +/* Register `storage` under `id`; the session takes ownership and will close it + * on remove/destroy. Fails with NAUT_ERR_INVAL if `id` is already present. */ +naut_err naut_session_add(naut_session *s, uint64_t id, naut_storage *storage); + +/* Close and forget the torrent. NAUT_ERR_NOTFOUND if unknown. */ +naut_err naut_session_remove(naut_session *s, uint64_t id); + +bool naut_session_has(const naut_session *s, uint64_t id); +size_t naut_session_count(const naut_session *s); + +/* Resolve `id` and relocate one completed file to `dest` (the storage half of + * "move files as they finish"). NAUT_ERR_NOTFOUND if the torrent is unknown. */ +naut_err naut_session_move_file(naut_session *s, uint64_t id, + uint32_t file_index, const char *dest); + +#endif /* NAUT_SESSION_H */ diff --git a/include/naut/storage.h b/include/naut/storage.h new file mode 100644 index 0000000..af64fab --- /dev/null +++ b/include/naut/storage.h @@ -0,0 +1,46 @@ +/* storage.h — file backend mapping the torrent's flat byte space to files. + * + * A torrent is one contiguous byte space [0, total_length); this layer splits a + * read/write at any global offset across the underlying files (a single block + * write can straddle a file boundary). Phase 3 uses positional pread/pwrite for + * correctness; the io_uring O_DIRECT fast path is a Phase 6 swap behind this + * same interface. The backend is a vtable so a memory/object-store backend can + * be registered later (the "extensible" goal). + */ +#ifndef NAUT_STORAGE_H +#define NAUT_STORAGE_H + +#include "naut/common.h" +#include "naut/metainfo.h" + +typedef struct naut_storage naut_storage; + +typedef struct { + bool direct_io; + bool preallocate; +} naut_storage_opts; + +/* Open (creating + preallocating) all files under `root`. */ +naut_storage *naut_storage_open(const naut_file *files, size_t nfiles, + const char *root, naut_err *err); +naut_storage *naut_storage_open_opts(const naut_file *files, size_t nfiles, + const char *root, + const naut_storage_opts *opts, + naut_err *err); +void naut_storage_close(naut_storage *s); + +naut_err naut_storage_write(naut_storage *s, int64_t offset, const void *buf, size_t len); +naut_err naut_storage_read(naut_storage *s, int64_t offset, void *buf, size_t len); +naut_err naut_storage_sync(naut_storage *s); + +/* Move one completed file out to `dest` (rename, or copy+unlink across file + * systems). The caller must guarantee the file is complete — every piece + * overlapping it verified — so no further writes target it. After this the slot + * is "externalized": subsequent I/O to its region returns NAUT_ERR_RANGE. This + * is the storage half of the "move files as they finish" feature. */ +naut_err naut_storage_relocate(naut_storage *s, size_t file_index, const char *dest); + +int64_t naut_storage_total(const naut_storage *s); +bool naut_storage_direct_enabled(const naut_storage *s); + +#endif /* NAUT_STORAGE_H */ diff --git a/include/naut/system.h b/include/naut/system.h new file mode 100644 index 0000000..caaa4c6 --- /dev/null +++ b/include/naut/system.h @@ -0,0 +1,10 @@ +/* system.h - CPU and memory placement controls for reactor threads. */ +#ifndef NAUT_SYSTEM_H +#define NAUT_SYSTEM_H + +#include "naut/common.h" + +naut_err naut_pin_current_thread(int cpu); +int naut_online_cpus(void); + +#endif /* NAUT_SYSTEM_H */ diff --git a/include/naut/tracker.h b/include/naut/tracker.h new file mode 100644 index 0000000..3141e69 --- /dev/null +++ b/include/naut/tracker.h @@ -0,0 +1,68 @@ +/* tracker.h — HTTP and UDP tracker clients (BEP-3/BEP-23, BEP-15). + * + * Split into pure codec (URL building, bencode response parsing, UDP packet + * encode/decode — all unit-testable without a socket) and thin blocking fetch + * helpers used by the swarm app. HTTPS/TLS is deferred to a later transport + * backend; the built-ins in this phase are plaintext HTTP and UDP. + */ +#ifndef NAUT_TRACKER_H +#define NAUT_TRACKER_H + +#include "naut/common.h" + +/* IPv4 compact peer (BEP-23). IPv6 (BEP-7) is a later addition. */ +typedef struct { uint8_t ip[4]; uint16_t port; } naut_peer_addr; + +typedef enum { + NAUT_TEV_NONE = 0, NAUT_TEV_COMPLETED = 1, NAUT_TEV_STARTED = 2, NAUT_TEV_STOPPED = 3 +} naut_tracker_event; /* values match BEP-15 UDP event codes */ + +typedef struct { + uint8_t info_hash[20]; + uint8_t peer_id[20]; + uint16_t port; + uint64_t uploaded, downloaded, left; + naut_tracker_event event; + int32_t numwant; /* -1 for default */ + uint32_t key; +} naut_announce_req; + +typedef struct { + int32_t interval; + int32_t seeders, leechers; /* -1 if absent */ + naut_peer_addr *peers; + size_t num_peers; + char *failure; /* tracker "failure reason", or NULL */ +} naut_tracker_response; + +void naut_tracker_response_free(naut_tracker_response *r); + +/* --- HTTP --- */ +/* Build the full announce GET URL (base?...params) with percent-encoded binary + * info_hash/peer_id. Returns bytes written (excl NUL) or 0 on overflow. */ +size_t naut_tracker_http_url(const char *base, const naut_announce_req *req, + char *out, size_t outsz); +/* Parse a bencoded HTTP tracker response body (compact or dict peer list). */ +naut_err naut_tracker_parse_http(const uint8_t *body, size_t len, + naut_tracker_response *out); + +/* --- UDP (BEP-15): pure packet codec --- */ +#define NAUT_UDP_CONNECT_REQ_LEN 16 +#define NAUT_UDP_ANNOUNCE_REQ_LEN 98 +void naut_udp_build_connect(uint8_t out[16], uint32_t txid); +naut_err naut_udp_parse_connect(const uint8_t *in, size_t len, uint32_t txid, + uint64_t *connection_id); +void naut_udp_build_announce(uint8_t out[98], uint64_t connection_id, + uint32_t txid, const naut_announce_req *req); +naut_err naut_udp_parse_announce(const uint8_t *in, size_t len, uint32_t txid, + naut_tracker_response *out); + +/* --- live fetch helpers (blocking) --- */ +/* HTTP GET the announce URL; fills out. Only http:// (no TLS yet). */ +naut_err naut_tracker_announce_http(const char *url, naut_tracker_response *out); +/* Full UDP connect+announce handshake against host:port. */ +naut_err naut_tracker_announce_udp(const char *host, uint16_t port, + const naut_announce_req *req, + naut_tracker_response *out); + +#endif /* NAUT_TRACKER_H */ diff --git a/include/naut/uring.h b/include/naut/uring.h new file mode 100644 index 0000000..7677373 --- /dev/null +++ b/include/naut/uring.h @@ -0,0 +1,56 @@ +/* uring.h — io_uring lifecycle (the kernel-facing seam, part 2). + * + * Thin ownership wrapper over liburing so the rest of the engine never calls + * io_uring_* directly — that keeps the "Linux-only now, portable later" seam + * intact (a future epoll/kqueue backend implements the same reactor contract). + * Each reactor thread owns exactly one naut_ring for both network and disk. + */ +#ifndef NAUT_URING_H +#define NAUT_URING_H + +#include "naut/common.h" +#include "naut/buf.h" +#include + +typedef struct naut_ring { + struct io_uring ring; + bool sqpoll; + bool send_zc; + bool msg_ring; + bool buffers_registered; + bool recv_fixed; +} naut_ring; + +/* entries: SQ depth (rounded up to a power of two by the kernel). + * sqpoll: dedicate a kernel thread to submission polling — removes the + * io_uring_enter syscall from the hot path once the queue is warm (needs + * CAP_SYS_NICE or /proc/sys tuning on some setups; falls back if it can't). */ +naut_err naut_ring_init(naut_ring *r, unsigned entries, bool sqpoll); +naut_err naut_ring_init_cpu(naut_ring *r, unsigned entries, bool sqpoll, + int sqpoll_cpu); +void naut_ring_close(naut_ring *r); + +/* Detect kernel support for the features the data path relies on. Logs a + * summary; returns NAUT_ERR_NOSYS if a hard requirement is missing. */ +naut_err naut_ring_probe(naut_ring *r); + +/* Register the pool's contiguous slab as fixed buffer index 0. Registration + * may fail under a low RLIMIT_MEMLOCK; callers can continue in degraded mode. */ +naut_err naut_ring_register_bufpool(naut_ring *r, const naut_bufpool *pool); +void naut_ring_unregister_buffers(naut_ring *r); + +/* Prepare a send using SEND_ZC when supported and requested, otherwise normal + * SEND. Returns true when the caller must retain the buffer until a CQE with + * IORING_CQE_F_NOTIF arrives. */ +bool naut_ring_prep_send(naut_ring *r, struct io_uring_sqe *sqe, int fd, + const void *buf, size_t len, int flags, + bool prefer_zero_copy); +/* Returns true when this recv was armed against the registered fixed buffer. + * Some kernels accept READ_FIXED but reject IORING_RECVSEND_FIXED_BUF on plain + * recv with -EINVAL; the caller should remember this per-operation so it can + * distinguish "fixed-buffer unsupported, retry unfixed" from a real error + * (rather than tearing the connection down). */ +bool naut_ring_prep_recv(naut_ring *r, struct io_uring_sqe *sqe, int fd, + void *buf, size_t len, int flags); + +#endif /* NAUT_URING_H */ diff --git a/include/naut/worker.h b/include/naut/worker.h new file mode 100644 index 0000000..c6207da --- /dev/null +++ b/include/naut/worker.h @@ -0,0 +1,32 @@ +/* worker.h - bounded worker pool for hash/crypto jobs off the reactor path. */ +#ifndef NAUT_WORKER_H +#define NAUT_WORKER_H + +#include "naut/common.h" + +typedef struct naut_worker_pool naut_worker_pool; +typedef struct naut_job naut_job; +typedef void (*naut_job_fn)(naut_job *job); + +struct naut_job { + naut_job_fn run; + void *context; + naut_err result; +}; + +/* Jobs are caller-owned and must remain alive until popped from completions. + * queue_capacity must be a power of two. cpu_base < 0 disables affinity. */ +naut_worker_pool *naut_worker_pool_create(uint32_t threads, + size_t queue_capacity, + int cpu_base); +void naut_worker_pool_destroy(naut_worker_pool *pool); + +bool naut_worker_submit(naut_worker_pool *pool, naut_job *job); +bool naut_worker_complete(naut_worker_pool *pool, naut_job **job); + +/* Readable when one or more jobs complete. The caller drains the eventfd and + * then pops completions. */ +int naut_worker_eventfd(const naut_worker_pool *pool); +uint32_t naut_worker_threads(const naut_worker_pool *pool); + +#endif /* NAUT_WORKER_H */ diff --git a/plan.md b/plan.md new file mode 100644 index 0000000..478b3e4 --- /dev/null +++ b/plan.md @@ -0,0 +1,168 @@ +# Naut-Torrent — Architecture & Implementation Plan + +## Context + +This is a greenfield project (`Naut-Torrent/` is empty). The goal is a **maintainable, extensible BitTorrent client** capable of **saturating a 10 Gigabit Ethernet link (~1.25 GB/s) in both directions**. The implementation language is C-equivalent, so the plan is expressed in C terms (structs, function-pointer vtables, manual memory management, no exceptions/RAII). + +Decisions confirmed with the user: + +- **Platform:** Linux-only, optimized hard around **io_uring** (network *and* disk). +- **Protocol:** BitTorrent **v1 + v2 hybrid** (BEP-3 SHA-1 pieces *and* BEP-52 SHA-256 Merkle trees). +- **Workload:** Saturate **both download and upload**, with **MSE/PE encryption (RC4)** in the hot path. +- **Extensibility:** Clean module boundaries + control **RPC** + full **BEP-10 extension protocol** + a versioned **native plugin ABI** + an **embedded scripting** runtime. + +The architecture is driven first by the throughput budget below — every structural decision (shared-nothing reactors, work offload, zero-copy buffers) exists to hit 1.25 GB/s on commodity multicore hardware. + +--- + +## 1. Throughput Budget (why the architecture looks the way it does) + +At **1.25 GB/s sustained**, the per-byte costs that must be parallelized: + +| Work item | Cost (per core, conservative) | Cores @ 1.25 GB/s | +|---|---|---| +| MSE RC4 encrypt/decrypt | ~500 MB/s/core | ~2.5 per active direction | +| SHA-256 verify (v2, SHA-NI) | ~1.5 GB/s/core | ~1 (download) | +| SHA-1 verify (v1, SHA-NI) | ~2.5 GB/s/core | <1 | +| Network recv/send + framing | high, but DMA-bound | spread across reactors | +| Disk write/read (O_DIRECT NVMe) | 5–7 GB/s/device | I/O-bound, not CPU | +| memcpy | **eliminate** via registered buffers | ~0 | + +**Conclusions that shape the design:** + +1. RC4 is the surprise cost — encryption alone wants several cores. The hot path **must scale linearly across cores** (shared-nothing reactors, no global locks on the data path). +2. Hashing and crypto are **offloadable, embarrassingly parallel** units of work → dedicated **worker pools** fed by lock-free queues, never run inline on a reactor. +3. **memcpy must be designed out**: kernel→userspace via io_uring registered buffers, hash/crypto operate in place, disk writes come straight from the same buffers (O_DIRECT, page-aligned). +4. A ~8–16 core box with one Gen4 NVMe and a 10 GbE NIC should saturate the link; the software just has to not get in the way (syscalls, locks, copies, allocator churn). + +--- + +## 2. High-Level Architecture: Shared-Nothing Thread-Per-Core Reactors + Offload Pools + +``` + ┌──────────────────────── Control plane (1 thread) ───────────────────────┐ + │ RPC server · plugin host · script VM · session/torrent registry · stats │ + └───────┬─────────────────────────────────────────────────────────────────┘ + │ message passing (MPSC command queues, no shared locks on hot path) + ┌──────────────┬───┴──────────┬──────────────┐ + │ Reactor 0 │ Reactor 1 │ … Reactor N-1 │ (one pinned thread per I/O core) + │ own io_uring │ own io_uring │ own io_uring │ network + disk SQ/CQ, shared-nothing + │ owns a shard │ owns a shard │ owns a shard │ of peer connections (SO_REUSEPORT) + └──────┬───────┴──────┬───────┴───────┬───────┘ + │ submit jobs │ │ job results returned via io_uring msg_ring / eventfd + ┌──────┴───────────────┴───────────────┴───────┐ + │ Hash worker pool Crypto-assist pool │ (pinned to remaining cores, + │ (SHA-1 / SHA-256 / Merkle, MPMC job queue) │ pull jobs, post completions) + └───────────────────────────────────────────────┘ +``` + +**Sharding model (the central decision):** + +- **Connections are sharded across reactors.** A `SO_REUSEPORT` listen socket per reactor lets the kernel hash inbound peers across cores; outbound peers are assigned by hash. Each reactor **exclusively owns** its connections → all per-connection state (recv/send buffers, MSE keystream, message parser) is **lock-free**. +- **Torrent piece-state is shared** (peers of one torrent live on many reactors). It is guarded by a **per-torrent lock that protects only bookkeeping** (the picker's rarity counts, request map, have-bitfield). The lock is held for microseconds; all heavy work (decrypt, hash, disk) happens **outside** it. Rationale: the expensive bytes never touch a lock; only the tiny "which block next / mark block received" decisions do. +- **Heavy work is offloaded**, not run on reactors: completed piece buffers go to the **hash pool**; crypto can run inline on the reactor (cheap per-message) or be batched to a crypto-assist pool under load. Results return to the **owning reactor** via `io_uring` `msg_ring` (cross-ring wakeup) or eventfd. +- **Control plane is off the hot path entirely.** RPC, plugins, and scripts run on their own thread(s) and communicate with reactors only via per-reactor MPSC command queues + event fan-out. A misbehaving plugin/script can never stall the data path. + +**Single-torrent scaling note:** because crypto/hash/disk for one torrent fan out to all cores via the pools, even a lone large torrent uses the whole machine. Only the per-torrent picker lock is single-point; if it ever becomes hot, upgrade the picker to the lock-free variant (§6, optimization). + +**io_uring features exploited:** multishot `accept`/`recv`, registered buffers (`PROVIDE_BUFFERS` ring) for zero-copy recv, registered files, `SEND_ZC` (zero-copy send) for seeding, `SQPOLL` (kernel-side submission polling to cut syscalls under load), `msg_ring` for cross-thread completions, linked SQEs for read→hash chaining. NUMA-aware + hugepage buffer pools per reactor. + +--- + +## 3. Module / Layer Breakdown + +Layers are bottom-up; each is independently unit-testable and has explicit extension seams. Suggested repo layout under `src/`. + +### Foundation + +- **`platform/`** — the *only* code that touches the kernel. io_uring lifecycle (ring setup, SQE build, CQE reaping), socket setup (`SO_REUSEPORT`, `TCP_NODELAY`, large `SO_RCV/SNDBUF`, `TCP_FASTOPEN`), file ops (`O_DIRECT`, `fallocate`, `fadvise`), `eventfd`/`timerfd`, CPU pinning, hugepage/NUMA allocation, monotonic clock. This is the seam that keeps "Linux-only now" from becoming "Linux-only forever." +- **`core/`** — data-structure toolbox, zero dependencies: + - **Buffer pool**: slab allocator of fixed-size (16 KiB block + piece-sized) page-aligned blocks, per-reactor freelists, refcounted so one buffer flows recv→decrypt→hash→disk without copy. + - Intrusive doubly-linked lists, open-addressing hash maps, dynamic arrays, **bitfields with popcount** (have/interested/request maps), object pools, **SPSC + MPSC + MPMC lock-free queues**, per-thread lock-free logging ring, config parser, lock-free stats counters. +- **`crypto/`** — SHA-1, SHA-256 with **runtime SHA-NI dispatch** (scalar fallback), **Merkle tree** builder/verifier (v2 piece layers), **RC4** (MSE), **Diffie-Hellman** (MSE handshake, RFC 2631 768-bit group), CSPRNG. Designed for in-place operation on pooled buffers. + +### Protocol + +- **`bencode/`** — streaming, allocation-light parser producing **zero-copy slices** into the source buffer; encoder. Hardened against malicious input (depth/size limits) — primary fuzz target. +- **`metainfo/`** — `.torrent` parse for v1, v2, and hybrid; file tree + piece layers; **magnet URI** parsing; info-hash computation (both v1 SHA-1 and v2 SHA-256 truncated). +- **`tracker/`** — interface `tracker_backend` with built-in **HTTP(S)** (BEP-3/BEP-23 compact) and **UDP** (BEP-15) backends; announce scheduling, scrape, multi-tier (BEP-12). *Extension seam: register custom backends.* +- **`dht/`** — Kademlia routing table, `get_peers`/`announce_peer`/`find_node`, bootstrap, token management, BEP-32 IPv6, BEP-51 infohash indexing. Runs as its own UDP endpoint on a reactor. +- **`peer/`** — wire codec (handshake, all BEP-3 messages), **MSE/PE layer** (DH handshake, RC4 keystream, plaintext fallback), framing, and a **BEP-10 extension registry** (`extension_handler` vtable) with built-ins: **PEX (BEP-11)**, **ut_metadata (BEP-9)**, **LTEP** negotiation. *Extension seam: register custom extension message handlers.* + +### Engine + +- **`piece/`** — `piece_picker` interface (vtable) with built-in **rarest-first**, **sequential/streaming**, **priority**, and **endgame** strategies; per-peer **adaptive request pipelining** (depth auto-tuned from observed throughput × RTT, not a fixed window — essential for filling a 10 GbE BDP); block accounting; per-torrent lock holder. *Extension seam: pluggable picker strategy.* +- **`storage/`** — `storage_backend` interface with built-in **file backend**: maps (piece,offset)→(file,offset) across multi-file torrents, **O_DIRECT** aligned read/write via the reactor's io_uring, write coalescing, bounded read cache, configurable fsync policy, **fast-resume** (persisted bitfield + partial-piece state), preallocation. *Extension seam: register custom storage (memory, network, object-store).* +- **`verify/`** — hashing job dispatcher: full-piece SHA-1 (v1) and incremental **Merkle leaf/branch** hashing (v2) submitted to the hash pool; on completion marks piece valid/invalid on the owning reactor. +- **`scheduler/`** — token-bucket **rate limiting** (global + per-torrent + per-peer), **choking** algorithm (tit-for-tat + optimistic unchoke, BEP-3), connection budget, bandwidth fairness across torrents, super-seeding (BEP-16) option. +- **`session/`** — torrent lifecycle state machine, peer-set management, alert/event bus, aggregate stats. The single registry the control plane talks to. + +### Surface (the "extensible" layer) + +- **`rpc/`** — control protocol over a UNIX domain socket (and optional TCP): command/response + **streaming event subscription**. Versioned, length-prefixed binary frames (compact) with an optional JSON mode for tooling. A stable **command registry** so plugins can add RPC verbs. This is how UIs/automation drive the client. +- **`plugin/`** — **versioned C ABI**: host passes a `naut_host_api` struct of function pointers; the plugin (`.so`) exports `naut_plugin_register(host)`. Plugins hook the same vtables the core uses: `tracker_backend`, `storage_backend`, `piece_picker`, `extension_handler`, RPC commands, and the event bus. ABI version checked at load; plugins run on the control thread, never the data path. +- **`script/`** — embedded **Lua-style VM** bound to the event bus (`on_torrent_added`, `on_piece_complete`, **`on_file_complete`**, `on_torrent_finished`, `on_peer_connected`, `on_alert`) and a sandboxed control API. Runs on a dedicated thread with a bounded work queue; cannot block reactors. + - **User feature — move files as they finish (libtorrent can't):** the engine seam is already built (Phase 3): `naut_download` fires `on_file_complete(file_index, path)` the moment a file's last covering piece verifies, and `naut_storage_relocate()` moves a single completed file safely (even mid-download). Phase 7 forwards the event to the `on_file_complete` script hook and exposes a `move_file` API; the relocate must be marshalled onto the owning reactor thread via the command queue (scripts run on their own thread). + +--- + +## 4. Concurrency & Memory Model (invariants) + +- **Data path is lock-free.** A connection is touched by exactly one reactor. The only data-path lock is the per-torrent picker mutex, held only for O(1)–O(log n) bookkeeping. +- **One buffer, one lifetime, zero copies.** A pooled, refcounted, page-aligned buffer is filled by `recv` (registered buffer), decrypted in place, hashed in place by a worker, then written by `SEND_ZC`/`O_DIRECT` write — same physical pages throughout. +- **Cross-thread communication is message passing**, never shared mutable state: MPSC command queues into reactors, MPMC job queue into the hash pool, `msg_ring`/eventfd for completions back out. +- **No allocation on the hot path.** All buffers, connection objects, request objects come from preallocated per-reactor pools sized from `peers × pipeline_depth × block_size`. Allocator is only touched at torrent add/remove. +- **Backpressure is explicit.** Bounded queues everywhere; when the hash pool or disk falls behind, reactors stop issuing `recv` (flow control) rather than growing memory unboundedly. + +--- + +## 5. Build, Repo & Quality + +``` +Naut-Torrent/ +├── src/{platform,core,crypto,bencode,metainfo,tracker,dht,peer,piece,storage,verify,scheduler,session,rpc,plugin,script}/ +├── include/naut/ # public headers incl. versioned plugin ABI (naut_plugin.h) +├── apps/{nautd,nautctl}/ # daemon + CLI client over RPC +├── plugins/example/ # reference plugin against the ABI +├── tests/{unit,fuzz,integration,bench}/ +└── build/ # build-system outputs +``` + +- **Daemon/CLI split**: `nautd` (engine) + `nautctl` (thin RPC client). UIs are just RPC consumers — keeps the core headless and embeddable. +- **Tooling**: AddressSanitizer/UBSan/ThreadSanitizer builds; `perf`/eBPF-friendly; built-in per-core stats exported over RPC. + +--- + +## 6. Implementation Phases (milestones, each independently demoable) + +1. **Foundation** — `platform/` io_uring echo server + `core/` buffer pool, queues, bitfields, hash map. *Gate: loopback echo saturates a core with zero per-op allocation.* +2. **Crypto + parsers** — SHA-1/256 (+SHA-NI), Merkle, RC4, DH; `bencode/`, `metainfo/` (v1/v2/hybrid + magnet). *Gate: parse real torrents; hash throughput benchmark ≥1.5 GB/s/core.* +3. **Single-peer transfer** — `peer/` handshake + messages (plaintext), `piece/` basic picker, `storage/` file backend, `verify/`. Download a real torrent from one peer to disk, verified. *Gate: byte-correct file from a known seed.* +4. **Trackers + swarm** — HTTP/UDP `tracker/`, choking `scheduler/`, multi-peer, rarest-first, endgame. *Gate: download from a public/local swarm; interop with libtorrent/Transmission.* +5. **MSE + DHT + extensions** — RC4 MSE handshake, `dht/`, PEX, ut_metadata, magnet-only start. *Gate: magnet link with no trackers completes via DHT; encrypted peers work.* +6. **Scale to 10 GbE** — adaptive pipelining, `SEND_ZC`, registered buffers, `SQPOLL`, hash/crypto pools, NUMA/hugepages, O_DIRECT tuning. *Gate: two boxes (or two NICs) sustain ≥9.4 Gbit/s both directions.* Optional: lock-free picker if the per-torrent lock shows contention in `perf`. +7. **Extensibility surface** — `rpc/`, `plugin/` ABI + reference plugin, `script/` VM + event hooks, `nautctl`. *Gate: a sample plugin adds a storage backend and a script reacts to `on_torrent_finished`.* + +--- + +## 7. Verification (end-to-end) + +- **Microbenchmarks** (`tests/bench/`): hash GB/s/core, RC4 GB/s/core, bencode parse MB/s, buffer-pool alloc/free ns, picker decisions/s. Each has a regression threshold tied to the §1 budget. +- **Correctness**: unit tests per module; **fuzzers** (`tests/fuzz/`) on bencode, peer-message, tracker-response, and metainfo parsers (the attack surface); byte-for-byte file verification after download; resume-from-partial test. +- **Interop**: run against **libtorrent/qBittorrent** and **Transmission** as both seed and leech, plaintext and MSE; magnet + DHT-only bootstrap test against the public DHT. +- **Throughput (the headline test)**: + 1. Baseline the link with `iperf3` to confirm ~9.4 Gbit/s is achievable end-to-end. + 2. Seed a large (≥50 GB) torrent from box A, download on box B over the 10 GbE link (or two NICs on one box via loopback-to-NIC). Measure with the client's own per-core RPC stats + `nstat`/`ifstat`. + 3. Confirm sustained throughput is link-bound (not CPU/disk/lock-bound) via `perf top` — no single thread pinned at 100% on locks/copies, hash & crypto spread across pool cores. + 4. Reverse roles to validate upload saturation (`SEND_ZC` path). +- **Soak**: 24 h multi-torrent run under ASan-off release build; assert flat memory (pools, no leaks), no descriptor growth, stable throughput. + +--- + +## 8. Key Risks & Mitigations + +- **Per-torrent picker lock contention** at 10 GbE → keep heavy work outside the lock; escalate to lock-free claim-by-CAS picker (already an interface, so it's a swap not a rewrite). +- **RC4/MSE CPU cost** underestimated → crypto-assist pool + prefer plaintext when both peers allow; measure early in Phase 5. +- **io_uring portability lock-in** → all kernel calls behind `platform/`; a future epoll backend is a new file, not a refactor. +- **O_DIRECT alignment complexity** → enforce page-aligned pooled buffers from day one (Phase 1), so storage never has to bounce-buffer. +- **Hybrid v1/v2 data-model complexity** → model the piece/file/Merkle layout once in `metainfo/` + `storage/` and treat v1 as the degenerate single-layer case. diff --git a/plugins/example/example.c b/plugins/example/example.c new file mode 100644 index 0000000..3af3db4 --- /dev/null +++ b/plugins/example/example.c @@ -0,0 +1,93 @@ +#include "naut/naut_plugin.h" + +#include +#include +#include + +typedef struct { + uint8_t *data; + size_t capacity; +} memory_storage; + +static unsigned finished_events; + +static void *memory_open(const char *root, naut_err *error) { + (void)root; + memory_storage *storage = calloc(1, sizeof(*storage)); + if (!storage) { + *error = NAUT_ERR_NOMEM; + return NULL; + } + storage->capacity = 1u << 20; + storage->data = calloc(1, storage->capacity); + if (!storage->data) { + free(storage); + *error = NAUT_ERR_NOMEM; + return NULL; + } + *error = NAUT_OK; + return storage; +} + +static void memory_close(void *opaque) { + memory_storage *storage = opaque; + free(storage->data); + free(storage); +} + +static naut_err memory_read(void *opaque, int64_t offset, + void *buffer, size_t length) { + memory_storage *storage = opaque; + if (offset < 0 || (uint64_t)offset + length > storage->capacity) + return NAUT_ERR_RANGE; + memcpy(buffer, storage->data + offset, length); + return NAUT_OK; +} + +static naut_err memory_write(void *opaque, int64_t offset, + const void *buffer, size_t length) { + memory_storage *storage = opaque; + if (offset < 0 || (uint64_t)offset + length > storage->capacity) + return NAUT_ERR_RANGE; + memcpy(storage->data + offset, buffer, length); + return NAUT_OK; +} + +static naut_err example_events(void *context, const char *request, + char **response) { + (void)context; + (void)request; + char buffer[64]; + snprintf(buffer, sizeof buffer, "{\"finished\":%u}", finished_events); + *response = strdup(buffer); + return *response ? NAUT_OK : NAUT_ERR_NOMEM; +} + +static void on_event(void *context, const naut_event *event) { + (void)context; + if (event->type == NAUT_EVENT_TORRENT_FINISHED) finished_events++; +} + +naut_err naut_plugin_register(const naut_host_api *host) { + if (!host || host->abi_version != NAUT_PLUGIN_ABI_VERSION || + host->struct_size < sizeof(*host)) + return NAUT_ERR_INVAL; + static const naut_storage_backend_v1 storage = { + .abi_version = NAUT_PLUGIN_ABI_VERSION, + .struct_size = sizeof(storage), + .name = "memory", + .open = memory_open, + .close = memory_close, + .read = memory_read, + .write = memory_write, + }; + naut_err error = host->set_plugin_name(host->host_context, "example"); + if (error == NAUT_OK) + error = host->register_storage_backend(host->host_context, &storage); + if (error == NAUT_OK) + error = host->register_rpc(host->host_context, "example.events", + example_events, NULL); + if (error == NAUT_OK) + error = host->subscribe_event(host->host_context, on_event, NULL); + return error; +} diff --git a/src/bencode/bencode.c b/src/bencode/bencode.c new file mode 100644 index 0000000..c895a31 --- /dev/null +++ b/src/bencode/bencode.c @@ -0,0 +1,310 @@ +#include "naut/bencode.h" +#include +#include +#include + +#define MAX_DEPTH 100 +#define MAX_NODES (4u * 1024 * 1024) /* hard cap on tree size */ + +/* --- arena: stable-address bump allocator -------------------------------- */ +typedef struct arena_chunk { + struct arena_chunk *next; + size_t used, cap; + uint8_t data[]; +} arena_chunk; + +struct naut_bc_doc { + arena_chunk *chunks; + naut_bc *root; + size_t nodes; +}; + +static void *arena_alloc(naut_bc_doc *d, size_t n) { + n = (n + 7) & ~(size_t)7; + arena_chunk *c = d->chunks; + if (!c || c->used + n > c->cap) { + size_t cap = n > 65536 ? n : 65536; + c = malloc(sizeof(arena_chunk) + cap); + if (!c) return NULL; + c->next = d->chunks; c->used = 0; c->cap = cap; + d->chunks = c; + } + void *p = c->data + c->used; + c->used += n; + return p; +} + +/* --- parser -------------------------------------------------------------- */ +typedef struct { + const uint8_t *p, *end; + naut_bc_doc *doc; + naut_err err; +} P; + +/* temporary growable vector of naut_bc used while a container's size is unknown */ +typedef struct { naut_bc *v; size_t n, cap; } vec; +static bool vec_push(vec *x, naut_bc item) { + if (x->n == x->cap) { + size_t nc = x->cap ? x->cap * 2 : 8; + naut_bc *nv = realloc(x->v, nc * sizeof(naut_bc)); + if (!nv) return false; + x->v = nv; x->cap = nc; + } + x->v[x->n++] = item; + return true; +} + +static bool parse_value(P *s, naut_bc *out, int depth); + +static bool parse_uint(P *s, size_t *out, uint8_t term) { + /* decimal, no leading zeros (except a lone "0"), terminated by `term` */ + if (s->p >= s->end) return false; + size_t val = 0; + const uint8_t *start = s->p; + if (*s->p == '0') { /* only "0" then term */ + s->p++; + if (s->p >= s->end || *s->p != term) return false; + *out = 0; s->p++; + return true; + } + while (s->p < s->end && *s->p >= '0' && *s->p <= '9') { + if (val > (SIZE_MAX - 9) / 10) return false; /* overflow guard */ + val = val * 10 + (size_t)(*s->p - '0'); + s->p++; + } + if (s->p == start) return false; /* no digits */ + if (s->p >= s->end || *s->p != term) return false; + s->p++; + *out = val; + return true; +} + +static bool parse_int(P *s, naut_bc *out) { + s->p++; /* 'i' */ + bool neg = false; + if (s->p < s->end && *s->p == '-') { neg = true; s->p++; } + /* digits up to 'e', no leading zero, no "-0" */ + if (s->p >= s->end) return false; + const uint8_t *d0 = s->p; + int64_t val = 0; + if (*s->p == '0') { + s->p++; + if (s->p >= s->end || *s->p != 'e') return false; /* "i0e" only */ + if (neg) return false; /* "-0" illegal */ + } else { + while (s->p < s->end && *s->p >= '0' && *s->p <= '9') { + if (val > (INT64_MAX - 9) / 10) return false; + val = val * 10 + (*s->p - '0'); + s->p++; + } + if (s->p == d0) return false; + if (s->p >= s->end || *s->p != 'e') return false; + } + s->p++; /* 'e' */ + out->type = NAUT_BC_INT; + out->v.i = neg ? -val : val; + return true; +} + +static bool parse_string(P *s, naut_bc *out) { + size_t n; + if (!parse_uint(s, &n, ':')) return false; + if ((size_t)(s->end - s->p) < n) return false; + out->type = NAUT_BC_STR; + out->v.str.p = s->p; + out->v.str.n = n; + s->p += n; + return true; +} + +static bool parse_list(P *s, naut_bc *out, int depth) { + s->p++; /* 'l' */ + vec items = {0}; + while (s->p < s->end && *s->p != 'e') { + naut_bc item; + if (!parse_value(s, &item, depth + 1)) { free(items.v); return false; } + if (!vec_push(&items, item)) { free(items.v); s->err = NAUT_ERR_NOMEM; return false; } + } + if (s->p >= s->end) { free(items.v); return false; } /* missing 'e' */ + s->p++; + naut_bc *arr = NULL; + if (items.n) { + arr = arena_alloc(s->doc, items.n * sizeof(naut_bc)); + if (!arr) { free(items.v); s->err = NAUT_ERR_NOMEM; return false; } + memcpy(arr, items.v, items.n * sizeof(naut_bc)); + } + free(items.v); + out->type = NAUT_BC_LIST; + out->v.list.items = arr; + out->v.list.count = items.n; + return true; +} + +static bool parse_dict(P *s, naut_bc *out, int depth) { + s->p++; /* 'd' */ + naut_bc_pair *pairs = NULL; size_t n = 0, cap = 0; + while (s->p < s->end && *s->p != 'e') { + naut_bc key; + if (s->p >= s->end || *s->p < '0' || *s->p > '9') goto fail; /* key must be string */ + if (!parse_string(s, &key)) goto fail; + naut_bc *val = arena_alloc(s->doc, sizeof(naut_bc)); + if (!val) { s->err = NAUT_ERR_NOMEM; goto fail; } + if (!parse_value(s, val, depth + 1)) goto fail; + if (n == cap) { + size_t nc = cap ? cap * 2 : 8; + naut_bc_pair *np = realloc(pairs, nc * sizeof(*np)); + if (!np) { s->err = NAUT_ERR_NOMEM; goto fail; } + pairs = np; cap = nc; + } + pairs[n].kp = key.v.str.p; pairs[n].kn = key.v.str.n; pairs[n].val = val; + n++; + } + if (s->p >= s->end) goto fail; /* missing 'e' */ + s->p++; + { + naut_bc_pair *arr = NULL; + if (n) { + arr = arena_alloc(s->doc, n * sizeof(naut_bc_pair)); + if (!arr) { s->err = NAUT_ERR_NOMEM; goto fail; } + memcpy(arr, pairs, n * sizeof(naut_bc_pair)); + } + free(pairs); + out->type = NAUT_BC_DICT; + out->v.dict.pairs = arr; + out->v.dict.count = n; + return true; + } +fail: + free(pairs); + return false; +} + +static bool parse_value(P *s, naut_bc *out, int depth) { + if (depth > MAX_DEPTH) { s->err = NAUT_ERR_PROTO; return false; } + if (++s->doc->nodes > MAX_NODES) { s->err = NAUT_ERR_PROTO; return false; } + if (s->p >= s->end) return false; + const uint8_t *raw0 = s->p; + bool ok; + switch (*s->p) { + case 'i': ok = parse_int(s, out); break; + case 'l': ok = parse_list(s, out, depth); break; + case 'd': ok = parse_dict(s, out, depth); break; + default: + if (*s->p >= '0' && *s->p <= '9') ok = parse_string(s, out); + else { s->err = NAUT_ERR_PROTO; return false; } + } + if (ok) { out->raw = raw0; out->raw_len = (size_t)(s->p - raw0); } + return ok; +} + +naut_err naut_bc_parse_prefix(const uint8_t *data, size_t len, + naut_bc_doc **out, size_t *consumed) { + if (!data || !out || !consumed) return NAUT_ERR_INVAL; + naut_bc_doc *doc = calloc(1, sizeof(*doc)); + if (!doc) return NAUT_ERR_NOMEM; + + P s = { .p = data, .end = data + len, .doc = doc, .err = NAUT_ERR_PROTO }; + naut_bc *root = arena_alloc(doc, sizeof(naut_bc)); + if (!root) { naut_bc_free(doc); return NAUT_ERR_NOMEM; } + + if (!parse_value(&s, root, 0)) { + naut_err e = s.err ? s.err : NAUT_ERR_PROTO; + naut_bc_free(doc); + return e; + } + doc->root = root; + *out = doc; + *consumed = (size_t)(s.p - data); + return NAUT_OK; +} + +naut_err naut_bc_parse(const uint8_t *data, size_t len, naut_bc_doc **out) { + size_t consumed = 0; + naut_err e = naut_bc_parse_prefix(data, len, out, &consumed); + if (e != NAUT_OK) return e; + if (consumed != len) { + naut_bc_free(*out); + *out = NULL; + return NAUT_ERR_PROTO; + } + return NAUT_OK; +} + +const naut_bc *naut_bc_root(const naut_bc_doc *doc) { return doc ? doc->root : NULL; } + +void naut_bc_free(naut_bc_doc *doc) { + if (!doc) return; + arena_chunk *c = doc->chunks; + while (c) { arena_chunk *n = c->next; free(c); c = n; } + free(doc); +} + +/* --- accessors ----------------------------------------------------------- */ +const naut_bc *naut_bc_dict_get(const naut_bc *d, const char *key) { + if (!d || d->type != NAUT_BC_DICT) return NULL; + size_t klen = strlen(key); + for (size_t i = 0; i < d->v.dict.count; i++) { + const naut_bc_pair *p = &d->v.dict.pairs[i]; + if (p->kn == klen && memcmp(p->kp, key, klen) == 0) return p->val; + } + return NULL; +} + +const naut_bc *naut_bc_list_at(const naut_bc *l, size_t i) { + if (!l || l->type != NAUT_BC_LIST || i >= l->v.list.count) return NULL; + return &l->v.list.items[i]; +} + +bool naut_bc_get_int(const naut_bc *v, int64_t *out) { + if (!v || v->type != NAUT_BC_INT) { *out = 0; return false; } + *out = v->v.i; return true; +} + +bool naut_bc_get_str(const naut_bc *v, const uint8_t **p, size_t *n) { + if (!v || v->type != NAUT_BC_STR) { *p = NULL; *n = 0; return false; } + *p = v->v.str.p; *n = v->v.str.n; return true; +} + +bool naut_bc_str_eq(const naut_bc *v, const char *s) { + if (!v || v->type != NAUT_BC_STR) return false; + size_t n = strlen(s); + return v->v.str.n == n && memcmp(v->v.str.p, s, n) == 0; +} + +/* --- encoder ------------------------------------------------------------- */ +void naut_bc_w_init(naut_bc_writer *w) { memset(w, 0, sizeof(*w)); } +void naut_bc_w_free(naut_bc_writer *w) { free(w->buf); memset(w, 0, sizeof(*w)); } + +static bool w_reserve(naut_bc_writer *w, size_t extra) { + if (w->err) return false; + if (w->len + extra > w->cap) { + size_t nc = w->cap ? w->cap * 2 : 256; + while (nc < w->len + extra) nc *= 2; + uint8_t *nb = realloc(w->buf, nc); + if (!nb) { w->err = NAUT_ERR_NOMEM; return false; } + w->buf = nb; w->cap = nc; + } + return true; +} +static void w_putc(naut_bc_writer *w, char c) { + if (w_reserve(w, 1)) w->buf[w->len++] = (uint8_t)c; +} +static void w_raw(naut_bc_writer *w, const void *p, size_t n) { + if (w_reserve(w, n)) { memcpy(w->buf + w->len, p, n); w->len += n; } +} +static void w_decimal(naut_bc_writer *w, int64_t v) { + char tmp[24]; + int n = snprintf(tmp, sizeof tmp, "%lld", (long long)v); + w_raw(w, tmp, (size_t)n); +} + +void naut_bc_w_int(naut_bc_writer *w, int64_t v) { + w_putc(w, 'i'); w_decimal(w, v); w_putc(w, 'e'); +} +void naut_bc_w_bytes(naut_bc_writer *w, const void *p, size_t n) { + w_decimal(w, (int64_t)n); w_putc(w, ':'); w_raw(w, p, n); +} +void naut_bc_w_cstr(naut_bc_writer *w, const char *s) { naut_bc_w_bytes(w, s, strlen(s)); } +void naut_bc_w_list_begin(naut_bc_writer *w) { w_putc(w, 'l'); } +void naut_bc_w_dict_begin(naut_bc_writer *w) { w_putc(w, 'd'); } +void naut_bc_w_end(naut_bc_writer *w) { w_putc(w, 'e'); } diff --git a/src/core/bitfield.c b/src/core/bitfield.c new file mode 100644 index 0000000..f942a12 --- /dev/null +++ b/src/core/bitfield.c @@ -0,0 +1,98 @@ +#include "naut/bitfield.h" + +#include +#include + +naut_err naut_bitfield_init(naut_bitfield *bf, size_t nbits) { + size_t nwords = (nbits + 63) / 64; + bf->words = nwords ? calloc(nwords, sizeof(uint64_t)) : NULL; + if (nwords && !bf->words) return NAUT_ERR_NOMEM; + bf->nbits = nbits; + bf->nwords = nwords; + return NAUT_OK; +} + +void naut_bitfield_free(naut_bitfield *bf) { + free(bf->words); + bf->words = NULL; + bf->nbits = bf->nwords = 0; +} + +/* Clear the unused high bits of the last word so count/all_set stay correct. */ +static void mask_tail(naut_bitfield *bf) { + size_t rem = bf->nbits & 63; + if (rem && bf->nwords) + bf->words[bf->nwords - 1] &= (((uint64_t)1 << rem) - 1); +} + +void naut_bitfield_set_all(naut_bitfield *bf) { + memset(bf->words, 0xff, bf->nwords * sizeof(uint64_t)); + mask_tail(bf); +} +void naut_bitfield_clear_all(naut_bitfield *bf) { + memset(bf->words, 0, bf->nwords * sizeof(uint64_t)); +} + +size_t naut_bitfield_count(const naut_bitfield *bf) { + size_t n = 0; + for (size_t i = 0; i < bf->nwords; i++) + n += (size_t)__builtin_popcountll(bf->words[i]); + return n; +} + +bool naut_bitfield_all_set(const naut_bitfield *bf) { + if (bf->nwords == 0) return true; + for (size_t i = 0; i + 1 < bf->nwords; i++) + if (bf->words[i] != ~(uint64_t)0) return false; + size_t rem = bf->nbits & 63; + uint64_t last = bf->words[bf->nwords - 1]; + uint64_t want = rem ? (((uint64_t)1 << rem) - 1) : ~(uint64_t)0; + return last == want; +} + +size_t naut_bitfield_find_set(const naut_bitfield *bf, size_t from) { + if (from >= bf->nbits) return SIZE_MAX; + size_t w = from >> 6; + uint64_t word = bf->words[w] & (~(uint64_t)0 << (from & 63)); + for (;;) { + if (word) { + size_t bit = (w << 6) + (size_t)__builtin_ctzll(word); + return bit < bf->nbits ? bit : SIZE_MAX; + } + if (++w >= bf->nwords) return SIZE_MAX; + word = bf->words[w]; + } +} + +size_t naut_bitfield_find_zero(const naut_bitfield *bf, size_t from) { + if (from >= bf->nbits) return SIZE_MAX; + size_t w = from >> 6; + uint64_t word = ~bf->words[w] & (~(uint64_t)0 << (from & 63)); + for (;;) { + if (word) { + size_t bit = (w << 6) + (size_t)__builtin_ctzll(word); + return bit < bf->nbits ? bit : SIZE_MAX; + } + if (++w >= bf->nwords) return SIZE_MAX; + word = ~bf->words[w]; + } +} + +void naut_bitfield_from_wire(naut_bitfield *bf, const uint8_t *bytes, size_t nbytes) { + naut_bitfield_clear_all(bf); + size_t bits = NAUT_MIN(bf->nbits, nbytes * 8); + for (size_t i = 0; i < bits; i++) { + /* BEP-3: bit 0 is the MSB of byte 0 */ + if ((bytes[i >> 3] >> (7 - (i & 7))) & 1u) + naut_bitfield_set(bf, i); + } +} + +void naut_bitfield_to_wire(const naut_bitfield *bf, uint8_t *bytes, size_t nbytes) { + memset(bytes, 0, nbytes); + size_t bits = NAUT_MIN(bf->nbits, nbytes * 8); + for (size_t i = 0; i < bits; i++) { + if (naut_bitfield_test(bf, i)) + bytes[i >> 3] |= (uint8_t)(1u << (7 - (i & 7))); + } +} diff --git a/src/core/buf.c b/src/core/buf.c new file mode 100644 index 0000000..7c0c3ea --- /dev/null +++ b/src/core/buf.c @@ -0,0 +1,154 @@ +#include "naut/buf.h" +#include "naut/log.h" + +#include +#include +#include +#include +#include +#include +#include + +struct naut_bufpool { + _Atomic(naut_buf *) free_head; /* Treiber stack: MP push, SC pop */ + _Atomic uint32_t avail; + naut_buf *meta; /* block_count headers */ + uint8_t *slab; /* mmap'd, page-aligned data */ + size_t slab_bytes; + uint32_t block_size; + uint32_t block_count; + bool hugepages; +}; + +static void *map_slab(size_t bytes, bool huge, int numa_node) { + int flags = MAP_PRIVATE | MAP_ANONYMOUS; + void *p = MAP_FAILED; + if (huge) { + p = mmap(NULL, bytes, PROT_READ | PROT_WRITE, + flags | MAP_HUGETLB, -1, 0); + if (p == MAP_FAILED) + NAUT_WARN("hugepage slab (%zu bytes) failed, using 4K pages", bytes); + } + if (p == MAP_FAILED) + p = mmap(NULL, bytes, PROT_READ | PROT_WRITE, flags, -1, 0); + if (p != MAP_FAILED) { +#ifdef SYS_mbind + if (numa_node >= 0 && + numa_node < (int)(sizeof(unsigned long) * 8)) { + unsigned long mask = 1ul << (unsigned)numa_node; + long rc = syscall(SYS_mbind, p, bytes, MPOL_BIND, &mask, + sizeof(mask) * 8, 0); + if (rc != 0) + NAUT_WARN("NUMA bind node %d failed: %s", + numa_node, strerror(errno)); + } +#else + (void)numa_node; +#endif +#ifdef MADV_HUGEPAGE + if (!huge) (void)madvise(p, bytes, MADV_HUGEPAGE); +#endif +#ifdef MADV_DONTDUMP + (void)madvise(p, bytes, MADV_DONTDUMP); +#endif + } + return p == MAP_FAILED ? NULL : p; +} + +naut_bufpool *naut_bufpool_create(uint32_t block_size, uint32_t block_count, + bool use_hugepages) { + return naut_bufpool_create_on_node(block_size, block_count, + use_hugepages, -1); +} + +naut_bufpool *naut_bufpool_create_on_node(uint32_t block_size, + uint32_t block_count, + bool use_hugepages, + int numa_node) { + if (block_size == 0 || (block_size & (NAUT_PAGE - 1)) != 0 || block_count == 0) { + NAUT_ERROR("bufpool: block_size must be a nonzero multiple of %u", NAUT_PAGE); + return NULL; + } + naut_bufpool *p = calloc(1, sizeof(*p)); + if (!p) return NULL; + + p->block_size = block_size; + p->block_count = block_count; + p->hugepages = use_hugepages; + p->slab_bytes = (size_t)block_size * block_count; + + p->meta = calloc(block_count, sizeof(naut_buf)); + p->slab = map_slab(p->slab_bytes, use_hugepages, numa_node); + if (!p->meta || !p->slab) { naut_bufpool_destroy(p); return NULL; } + + /* Build the freelist. Index 0 ends at the bottom of the stack. */ + atomic_store_explicit(&p->free_head, NULL, memory_order_relaxed); + for (uint32_t i = 0; i < block_count; i++) { + naut_buf *b = &p->meta[i]; + b->cap = block_size; + b->idx = i; + b->pool = p; + b->data = p->slab + (size_t)i * block_size; + atomic_store_explicit(&b->refcnt, 0, memory_order_relaxed); + b->fnext = atomic_load_explicit(&p->free_head, memory_order_relaxed); + atomic_store_explicit(&p->free_head, b, memory_order_relaxed); + } + atomic_store_explicit(&p->avail, block_count, memory_order_relaxed); + NAUT_INFO("bufpool: %u blocks x %u bytes (%zu MiB%s%s)", + block_count, block_size, p->slab_bytes >> 20, + use_hugepages ? ", hugepages" : "", + numa_node >= 0 ? ", NUMA-bound" : ""); + return p; +} + +void naut_bufpool_destroy(naut_bufpool *p) { + if (!p) return; + if (p->slab) munmap(p->slab, p->slab_bytes); + free(p->meta); + free(p); +} + +naut_buf *naut_buf_get(naut_bufpool *p) { + /* Single-consumer pop: only the owning reactor calls this, so reading + * head->fnext is safe — no other thread can pop `head` from under us. */ + naut_buf *head = atomic_load_explicit(&p->free_head, memory_order_acquire); + for (;;) { + if (NAUT_UNLIKELY(!head)) return NULL; /* exhausted */ + naut_buf *next = head->fnext; + if (atomic_compare_exchange_weak_explicit( + &p->free_head, &head, next, + memory_order_acquire, memory_order_acquire)) + break; + } + atomic_fetch_sub_explicit(&p->avail, 1, memory_order_relaxed); + head->len = 0; + head->fnext = NULL; + atomic_store_explicit(&head->refcnt, 1, memory_order_relaxed); + return head; +} + +void naut_buf_put(naut_buf *b) { + if (!b) return; + /* release so a consumer that later pops sees our writes to data[] */ + if (atomic_fetch_sub_explicit(&b->refcnt, 1, memory_order_release) != 1) + return; /* still referenced */ + atomic_thread_fence(memory_order_acquire); + + naut_bufpool *p = b->pool; + naut_buf *head = atomic_load_explicit(&p->free_head, memory_order_relaxed); + do { + b->fnext = head; /* MP push */ + } while (!atomic_compare_exchange_weak_explicit( + &p->free_head, &head, b, + memory_order_release, memory_order_relaxed)); + atomic_fetch_add_explicit(&p->avail, 1, memory_order_relaxed); +} + +uint32_t naut_bufpool_capacity(const naut_bufpool *p) { return p->block_count; } +uint32_t naut_bufpool_available(const naut_bufpool *p) { + return atomic_load_explicit(&p->avail, memory_order_relaxed); +} +void *naut_bufpool_slab(const naut_bufpool *p, size_t *out_bytes) { + if (out_bytes) *out_bytes = p->slab_bytes; + return p->slab; +} diff --git a/src/core/common.c b/src/core/common.c new file mode 100644 index 0000000..af93026 --- /dev/null +++ b/src/core/common.c @@ -0,0 +1,18 @@ +#include "naut/common.h" + +const char *naut_strerror(naut_err e) { + switch (e) { + case NAUT_OK: return "ok"; + case NAUT_ERR_NOMEM: return "out of memory"; + case NAUT_ERR_INVAL: return "invalid argument"; + case NAUT_ERR_IO: return "i/o error"; + case NAUT_ERR_AGAIN: return "would block"; + case NAUT_ERR_PROTO: return "protocol error"; + case NAUT_ERR_RANGE: return "out of range"; + case NAUT_ERR_NOSYS: return "unsupported"; + case NAUT_ERR_FULL: return "full"; + case NAUT_ERR_EMPTY: return "empty"; + case NAUT_ERR_NOTFOUND: return "not found"; + default: return "unknown error"; + } +} diff --git a/src/core/log.c b/src/core/log.c new file mode 100644 index 0000000..ef0dd77 --- /dev/null +++ b/src/core/log.c @@ -0,0 +1,83 @@ +#include "naut/log.h" + +#include +#include +#include +#include +#include +#include +#include + +#if defined(__linux__) +#include +static inline long naut__tid(void) { return syscall(SYS_gettid); } +#else +static inline long naut__tid(void) { return 0; } +#endif + +static _Atomic naut_log_level g_level = NAUT_LOG_INFO; + +void naut_log_set_level(naut_log_level lvl) { + atomic_store_explicit(&g_level, lvl, memory_order_relaxed); +} +naut_log_level naut_log_get_level(void) { + return atomic_load_explicit(&g_level, memory_order_relaxed); +} + +static const char *level_str(naut_log_level lvl) { + switch (lvl) { + case NAUT_LOG_ERROR: return "ERROR"; + case NAUT_LOG_WARN: return "WARN "; + case NAUT_LOG_INFO: return "INFO "; + case NAUT_LOG_DEBUG: return "DEBUG"; + case NAUT_LOG_TRACE: return "TRACE"; + default: return "?????"; + } +} + +/* Format one record into a stack buffer and emit with a single write() so + * concurrent loggers never interleave a line. */ +static void emit_v(naut_log_level lvl, const char *file, int line, + const char *fmt, va_list ap) { + char hdr[96]; + char msg[1024]; + + struct timespec ts; + clock_gettime(CLOCK_REALTIME, &ts); + struct tm tm; + localtime_r(&ts.tv_sec, &tm); + + const char *base = strrchr(file, '/'); + base = base ? base + 1 : file; + + int hn = snprintf(hdr, sizeof(hdr), + "%02d:%02d:%02d.%03ld [%s] %-5ld %s:%d: ", + tm.tm_hour, tm.tm_min, tm.tm_sec, ts.tv_nsec / 1000000, + level_str(lvl), naut__tid(), base, line); + int mn = vsnprintf(msg, sizeof(msg), fmt, ap); + if (hn < 0) hn = 0; + if (mn < 0) mn = 0; + if (mn >= (int)sizeof(msg)) mn = sizeof(msg) - 1; + + struct iovec iov[3] = { + { hdr, (size_t)hn }, + { msg, (size_t)mn }, + { (void *)"\n", 1 }, + }; + ssize_t w = writev(STDERR_FILENO, iov, 3); + (void)w; +} + +void naut_log_emit(naut_log_level lvl, const char *file, int line, + const char *fmt, ...) { + va_list ap; va_start(ap, fmt); + emit_v(lvl, file, line, fmt, ap); + va_end(ap); +} + +void naut_panic(const char *file, int line, const char *fmt, ...) { + va_list ap; va_start(ap, fmt); + emit_v(NAUT_LOG_ERROR, file, line, fmt, ap); + va_end(ap); + abort(); +} diff --git a/src/core/mpmc.c b/src/core/mpmc.c new file mode 100644 index 0000000..b802ae7 --- /dev/null +++ b/src/core/mpmc.c @@ -0,0 +1,69 @@ +#include "naut/mpmc.h" + +#include + +naut_err naut_mpmc_init(naut_mpmc *q, size_t capacity_pow2) { + if (!NAUT_IS_POW2(capacity_pow2)) return NAUT_ERR_INVAL; + q->buffer = aligned_alloc(NAUT_CACHELINE, + capacity_pow2 * sizeof(naut_mpmc_cell)); + if (!q->buffer) return NAUT_ERR_NOMEM; + q->mask = capacity_pow2 - 1; + for (size_t i = 0; i < capacity_pow2; i++) { + atomic_store_explicit(&q->buffer[i].seq, i, memory_order_relaxed); + q->buffer[i].data = NULL; + } + atomic_store_explicit(&q->enqueue_pos, 0, memory_order_relaxed); + atomic_store_explicit(&q->dequeue_pos, 0, memory_order_relaxed); + return NAUT_OK; +} + +void naut_mpmc_destroy(naut_mpmc *q) { + free(q->buffer); + q->buffer = NULL; +} + +bool naut_mpmc_push(naut_mpmc *q, void *p) { + naut_mpmc_cell *cell; + size_t pos = atomic_load_explicit(&q->enqueue_pos, memory_order_relaxed); + for (;;) { + cell = &q->buffer[pos & q->mask]; + size_t seq = atomic_load_explicit(&cell->seq, memory_order_acquire); + intptr_t diff = (intptr_t)seq - (intptr_t)pos; + if (diff == 0) { + if (atomic_compare_exchange_weak_explicit( + &q->enqueue_pos, &pos, pos + 1, + memory_order_relaxed, memory_order_relaxed)) + break; + } else if (diff < 0) { + return false; /* full */ + } else { + pos = atomic_load_explicit(&q->enqueue_pos, memory_order_relaxed); + } + } + cell->data = p; + atomic_store_explicit(&cell->seq, pos + 1, memory_order_release); + return true; +} + +bool naut_mpmc_pop(naut_mpmc *q, void **out) { + naut_mpmc_cell *cell; + size_t pos = atomic_load_explicit(&q->dequeue_pos, memory_order_relaxed); + for (;;) { + cell = &q->buffer[pos & q->mask]; + size_t seq = atomic_load_explicit(&cell->seq, memory_order_acquire); + intptr_t diff = (intptr_t)seq - (intptr_t)(pos + 1); + if (diff == 0) { + if (atomic_compare_exchange_weak_explicit( + &q->dequeue_pos, &pos, pos + 1, + memory_order_relaxed, memory_order_relaxed)) + break; + } else if (diff < 0) { + return false; /* empty */ + } else { + pos = atomic_load_explicit(&q->dequeue_pos, memory_order_relaxed); + } + } + *out = cell->data; + atomic_store_explicit(&cell->seq, pos + q->mask + 1, memory_order_release); + return true; +} diff --git a/src/core/worker.c b/src/core/worker.c new file mode 100644 index 0000000..8d39d40 --- /dev/null +++ b/src/core/worker.c @@ -0,0 +1,142 @@ +#include "naut/worker.h" +#include "naut/mpmc.h" + +#include +#include +#include +#include +#include +#include +#include + +struct naut_worker_pool { + naut_mpmc pending; + naut_mpmc completed; + pthread_t *threads; + uint32_t num_threads; + int event_fd; + int cpu_base; + sem_t work; /* counts queued jobs (+ stop tokens at shutdown) */ + bool work_ready; /* sem_init succeeded */ + _Atomic bool stop; +}; + +typedef struct { + naut_worker_pool *pool; + uint32_t index; +} worker_arg; + +static void pin_thread(int cpu) { + if (cpu < 0) return; + cpu_set_t set; + CPU_ZERO(&set); + CPU_SET((unsigned)cpu, &set); + (void)pthread_setaffinity_np(pthread_self(), sizeof set, &set); +} + +static void *worker_main(void *opaque) { + worker_arg arg = *(worker_arg *)opaque; + free(opaque); + naut_worker_pool *pool = arg.pool; + pin_thread(pool->cpu_base < 0 ? -1 : + pool->cpu_base + (int)arg.index); + for (;;) { + /* Block until a job is submitted (or a shutdown token is posted) + * instead of spinning on sched_yield — idle workers cost nothing. */ + while (sem_wait(&pool->work) != 0 && errno == EINTR) {} + if (atomic_load_explicit(&pool->stop, memory_order_acquire)) break; + void *item = NULL; + if (!naut_mpmc_pop(&pool->pending, &item)) continue; + naut_job *job = item; + job->run(job); + while (!naut_mpmc_push(&pool->completed, job) && + !atomic_load_explicit(&pool->stop, memory_order_acquire)) + sched_yield(); + uint64_t one = 1; + while (write(pool->event_fd, &one, sizeof one) < 0 && + errno == EINTR) {} + } + return NULL; +} + +naut_worker_pool *naut_worker_pool_create(uint32_t threads, + size_t queue_capacity, + int cpu_base) { + if (threads == 0 || !NAUT_IS_POW2(queue_capacity)) return NULL; + naut_worker_pool *pool = calloc(1, sizeof(*pool)); + if (!pool) return NULL; + pool->event_fd = -1; + pool->cpu_base = cpu_base; + if (naut_mpmc_init(&pool->pending, queue_capacity) != NAUT_OK || + naut_mpmc_init(&pool->completed, queue_capacity) != NAUT_OK) + goto fail; + if (sem_init(&pool->work, 0, 0) != 0) goto fail; + pool->work_ready = true; + pool->event_fd = eventfd(0, EFD_CLOEXEC | EFD_NONBLOCK); + pool->threads = calloc(threads, sizeof(*pool->threads)); + if (pool->event_fd < 0 || !pool->threads) goto fail; + for (uint32_t i = 0; i < threads; i++) { + worker_arg *arg = malloc(sizeof(*arg)); + if (!arg) goto fail_threads; + arg->pool = pool; + arg->index = i; + if (pthread_create(&pool->threads[i], NULL, worker_main, arg) != 0) { + free(arg); + goto fail_threads; + } + pool->num_threads++; + } + return pool; + +fail_threads: + atomic_store_explicit(&pool->stop, true, memory_order_release); + for (uint32_t i = 0; i < pool->num_threads; i++) sem_post(&pool->work); + for (uint32_t i = 0; i < pool->num_threads; i++) + if (pool->threads[i]) pthread_join(pool->threads[i], NULL); +fail: + if (pool->work_ready) sem_destroy(&pool->work); + if (pool->event_fd >= 0) close(pool->event_fd); + free(pool->threads); + if (pool->pending.buffer) naut_mpmc_destroy(&pool->pending); + if (pool->completed.buffer) naut_mpmc_destroy(&pool->completed); + free(pool); + return NULL; +} + +void naut_worker_pool_destroy(naut_worker_pool *pool) { + if (!pool) return; + atomic_store_explicit(&pool->stop, true, memory_order_release); + /* Wake every worker so the blocking sem_wait returns and sees `stop`. */ + for (uint32_t i = 0; i < pool->num_threads; i++) sem_post(&pool->work); + for (uint32_t i = 0; i < pool->num_threads; i++) + pthread_join(pool->threads[i], NULL); + sem_destroy(&pool->work); + close(pool->event_fd); + naut_mpmc_destroy(&pool->pending); + naut_mpmc_destroy(&pool->completed); + free(pool->threads); + free(pool); +} + +bool naut_worker_submit(naut_worker_pool *pool, naut_job *job) { + if (!pool || !job || !job->run) return false; + if (!naut_mpmc_push(&pool->pending, job)) return false; + sem_post(&pool->work); /* wake one blocked worker */ + return true; +} + +bool naut_worker_complete(naut_worker_pool *pool, naut_job **job) { + void *item = NULL; + if (!pool || !job || !naut_mpmc_pop(&pool->completed, &item)) + return false; + *job = item; + return true; +} + +int naut_worker_eventfd(const naut_worker_pool *pool) { + return pool ? pool->event_fd : -1; +} + +uint32_t naut_worker_threads(const naut_worker_pool *pool) { + return pool ? pool->num_threads : 0; +} diff --git a/src/crypto/merkle.c b/src/crypto/merkle.c new file mode 100644 index 0000000..886487e --- /dev/null +++ b/src/crypto/merkle.c @@ -0,0 +1,59 @@ +#include "naut/merkle.h" +#include +#include + +size_t naut_merkle_leaves(const uint8_t *data, size_t len, uint8_t *out) { + size_t n = 0; + size_t off = 0; + do { + size_t chunk = len - off; + if (chunk > NAUT_MERKLE_LEAF) chunk = NAUT_MERKLE_LEAF; + naut_sha256(data + off, chunk, out + n * NAUT_SHA256_LEN); + off += chunk; + n++; + } while (off < len); + return n; /* len==0 => one leaf hashing the empty string */ +} + +static size_t next_pow2(size_t n) { + size_t p = 1; + while (p < n) p <<= 1; + return p; +} + +/* Reduce `count` (power of two) leaf hashes in `nodes` up to a single root. + * Slots [present, count) are assumed to already hold the correct zero-padding + * hash for the leaf level. Operates in place. */ +static void reduce(uint8_t *nodes, size_t count) { + while (count > 1) { + for (size_t i = 0; i < count / 2; i++) { + naut_sha256(nodes + (2*i) * NAUT_SHA256_LEN, + 2 * NAUT_SHA256_LEN, + nodes + i * NAUT_SHA256_LEN); + } + count /= 2; + } +} + +naut_err naut_merkle_root_padded(const uint8_t *leaves, size_t nleaves, + size_t block_count, + uint8_t out[NAUT_SHA256_LEN]) { + if (block_count == 0) block_count = 1; + if (!NAUT_IS_POW2(block_count) || nleaves > block_count) return NAUT_ERR_INVAL; + + uint8_t *nodes = calloc(block_count, NAUT_SHA256_LEN); /* zero-filled pad */ + if (!nodes) return NAUT_ERR_NOMEM; + if (nleaves) memcpy(nodes, leaves, nleaves * NAUT_SHA256_LEN); + /* slots [nleaves, block_count) stay all-zero: the v2 zero leaf hash */ + + reduce(nodes, block_count); + memcpy(out, nodes, NAUT_SHA256_LEN); + free(nodes); + return NAUT_OK; +} + +naut_err naut_merkle_root(const uint8_t *leaves, size_t nleaves, + uint8_t out[NAUT_SHA256_LEN]) { + size_t bc = nleaves ? next_pow2(nleaves) : 1; + return naut_merkle_root_padded(leaves, nleaves, bc, out); +} diff --git a/src/crypto/rc4.c b/src/crypto/rc4.c new file mode 100644 index 0000000..fe1e6fd --- /dev/null +++ b/src/crypto/rc4.c @@ -0,0 +1,34 @@ +#include "naut/rc4.h" + +void naut_rc4_init(naut_rc4 *c, const void *key, size_t keylen, size_t drop) { + const uint8_t *k = key; + for (int i = 0; i < 256; i++) c->s[i] = (uint8_t)i; + uint8_t j = 0; + for (int i = 0; i < 256; i++) { + j = (uint8_t)(j + c->s[i] + k[i % keylen]); + uint8_t t = c->s[i]; c->s[i] = c->s[j]; c->s[j] = t; + } + c->i = 0; c->j = 0; + if (drop) { + /* discard `drop` keystream bytes */ + uint8_t scratch[256]; + while (drop) { + size_t n = drop < sizeof scratch ? drop : sizeof scratch; + for (size_t x = 0; x < n; x++) scratch[x] = 0; + naut_rc4_xor(c, scratch, n); + drop -= n; + } + } +} + +void naut_rc4_xor(naut_rc4 *c, void *buf, size_t len) { + uint8_t *p = buf; + uint8_t i = c->i, j = c->j; + for (size_t n = 0; n < len; n++) { + i = (uint8_t)(i + 1); + j = (uint8_t)(j + c->s[i]); + uint8_t t = c->s[i]; c->s[i] = c->s[j]; c->s[j] = t; + p[n] ^= c->s[(uint8_t)(c->s[i] + c->s[j])]; + } + c->i = i; c->j = j; +} diff --git a/src/crypto/sha1.c b/src/crypto/sha1.c new file mode 100644 index 0000000..8ef73e8 --- /dev/null +++ b/src/crypto/sha1.c @@ -0,0 +1,72 @@ +#include "naut/hash.h" +#include + +static inline uint32_t rol(uint32_t x, int n) { return (x << n) | (x >> (32 - n)); } + +static void sha1_block(uint32_t h[5], const uint8_t *p, size_t nblocks) { + for (size_t b = 0; b < nblocks; b++, p += 64) { + uint32_t w[80]; + for (int i = 0; i < 16; i++) + w[i] = ((uint32_t)p[i*4] << 24) | ((uint32_t)p[i*4+1] << 16) | + ((uint32_t)p[i*4+2] << 8) | (uint32_t)p[i*4+3]; + for (int i = 16; i < 80; i++) + w[i] = rol(w[i-3] ^ w[i-8] ^ w[i-14] ^ w[i-16], 1); + + uint32_t a = h[0], bb = h[1], c = h[2], d = h[3], e = h[4]; + for (int i = 0; i < 80; i++) { + uint32_t f, k; + if (i < 20) { f = (bb & c) | (~bb & d); k = 0x5A827999; } + else if (i < 40) { f = bb ^ c ^ d; k = 0x6ED9EBA1; } + else if (i < 60) { f = (bb & c) | (bb & d) | (c & d); k = 0x8F1BBCDC; } + else { f = bb ^ c ^ d; k = 0xCA62C1D6; } + uint32_t t = rol(a, 5) + f + e + k + w[i]; + e = d; d = c; c = rol(bb, 30); bb = a; a = t; + } + h[0] += a; h[1] += bb; h[2] += c; h[3] += d; h[4] += e; + } +} + +void naut_sha1_init(naut_sha1_ctx *c) { + c->h[0] = 0x67452301; c->h[1] = 0xEFCDAB89; c->h[2] = 0x98BADCFE; + c->h[3] = 0x10325476; c->h[4] = 0xC3D2E1F0; + c->len = 0; c->used = 0; +} + +void naut_sha1_update(naut_sha1_ctx *c, const void *data, size_t len) { + const uint8_t *p = data; + c->len += len; + if (c->used) { + size_t need = 64 - c->used; + size_t take = len < need ? len : need; + memcpy(c->block + c->used, p, take); + c->used += take; p += take; len -= take; + if (c->used == 64) { sha1_block(c->h, c->block, 1); c->used = 0; } + } + if (len >= 64) { + size_t nb = len / 64; + sha1_block(c->h, p, nb); + p += nb * 64; len -= nb * 64; + } + if (len) { memcpy(c->block, p, len); c->used = len; } +} + +void naut_sha1_final(naut_sha1_ctx *c, uint8_t out[NAUT_SHA1_LEN]) { + uint64_t bits = c->len * 8; + uint8_t pad = 0x80; + naut_sha1_update(c, &pad, 1); + uint8_t zero = 0; + while (c->used != 56) naut_sha1_update(c, &zero, 1); + uint8_t lenbe[8]; + for (int i = 0; i < 8; i++) lenbe[i] = (uint8_t)(bits >> (56 - i*8)); + naut_sha1_update(c, lenbe, 8); + for (int i = 0; i < 5; i++) { + out[i*4] = (uint8_t)(c->h[i] >> 24); + out[i*4+1] = (uint8_t)(c->h[i] >> 16); + out[i*4+2] = (uint8_t)(c->h[i] >> 8); + out[i*4+3] = (uint8_t)(c->h[i]); + } +} + +void naut_sha1(const void *data, size_t len, uint8_t out[NAUT_SHA1_LEN]) { + naut_sha1_ctx c; naut_sha1_init(&c); naut_sha1_update(&c, data, len); naut_sha1_final(&c, out); +} diff --git a/src/crypto/sha256.c b/src/crypto/sha256.c new file mode 100644 index 0000000..2f54710 --- /dev/null +++ b/src/crypto/sha256.c @@ -0,0 +1,202 @@ +#include "naut/hash.h" +#include +#include + +#if defined(__x86_64__) || defined(__i386__) +#include +#define NAUT_HAVE_SHANI 1 +#endif + +static const uint32_t K[64] = { + 0x428a2f98,0x71374491,0xb5c0fbcf,0xe9b5dba5,0x3956c25b,0x59f111f1,0x923f82a4,0xab1c5ed5, + 0xd807aa98,0x12835b01,0x243185be,0x550c7dc3,0x72be5d74,0x80deb1fe,0x9bdc06a7,0xc19bf174, + 0xe49b69c1,0xefbe4786,0x0fc19dc6,0x240ca1cc,0x2de92c6f,0x4a7484aa,0x5cb0a9dc,0x76f988da, + 0x983e5152,0xa831c66d,0xb00327c8,0xbf597fc7,0xc6e00bf3,0xd5a79147,0x06ca6351,0x14292967, + 0x27b70a85,0x2e1b2138,0x4d2c6dfc,0x53380d13,0x650a7354,0x766a0abb,0x81c2c92e,0x92722c85, + 0xa2bfe8a1,0xa81a664b,0xc24b8b70,0xc76c51a3,0xd192e819,0xd6990624,0xf40e3585,0x106aa070, + 0x19a4c116,0x1e376c08,0x2748774c,0x34b0bcb5,0x391c0cb3,0x4ed8aa4a,0x5b9cca4f,0x682e6ff3, + 0x748f82ee,0x78a5636f,0x84c87814,0x8cc70208,0x90befffa,0xa4506ceb,0xbef9a3f7,0xc67178f2, +}; + +#define ROR(x,n) (((x) >> (n)) | ((x) << (32 - (n)))) + +static void sha256_scalar(uint32_t s[8], const uint8_t *p, size_t nblocks) { + for (size_t b = 0; b < nblocks; b++, p += 64) { + uint32_t w[64]; + for (int i = 0; i < 16; i++) + w[i] = ((uint32_t)p[i*4]<<24)|((uint32_t)p[i*4+1]<<16)|((uint32_t)p[i*4+2]<<8)|p[i*4+3]; + for (int i = 16; i < 64; i++) { + uint32_t s0 = ROR(w[i-15],7) ^ ROR(w[i-15],18) ^ (w[i-15] >> 3); + uint32_t s1 = ROR(w[i-2],17) ^ ROR(w[i-2],19) ^ (w[i-2] >> 10); + w[i] = w[i-16] + s0 + w[i-7] + s1; + } + uint32_t a=s[0],bb=s[1],c=s[2],d=s[3],e=s[4],f=s[5],g=s[6],h=s[7]; + for (int i = 0; i < 64; i++) { + uint32_t S1 = ROR(e,6) ^ ROR(e,11) ^ ROR(e,25); + uint32_t ch = (e & f) ^ (~e & g); + uint32_t t1 = h + S1 + ch + K[i] + w[i]; + uint32_t S0 = ROR(a,2) ^ ROR(a,13) ^ ROR(a,22); + uint32_t maj = (a & bb) ^ (a & c) ^ (bb & c); + uint32_t t2 = S0 + maj; + h=g; g=f; f=e; e=d+t1; d=c; c=bb; bb=a; a=t1+t2; + } + s[0]+=a;s[1]+=bb;s[2]+=c;s[3]+=d;s[4]+=e;s[5]+=f;s[6]+=g;s[7]+=h; + } +} + +#ifdef NAUT_HAVE_SHANI +__attribute__((target("sha,sse4.1,ssse3"))) +static void sha256_shani(uint32_t state[8], const uint8_t *data, size_t nblocks) { + __m128i STATE0, STATE1, MSG, TMP, MSG0, MSG1, MSG2, MSG3, ABEF, CDGH; + const __m128i MASK = _mm_set_epi64x(0x0c0d0e0f08090a0bULL, 0x0405060700010203ULL); + + TMP = _mm_loadu_si128((const __m128i*)&state[0]); + STATE1 = _mm_loadu_si128((const __m128i*)&state[4]); + TMP = _mm_shuffle_epi32(TMP, 0xB1); /* CDAB */ + STATE1 = _mm_shuffle_epi32(STATE1, 0x1B); /* EFGH */ + STATE0 = _mm_alignr_epi8(TMP, STATE1, 8); /* ABEF */ + STATE1 = _mm_blend_epi16(STATE1, TMP, 0xF0); /* CDGH */ + + for (size_t n = 0; n < nblocks; n++, data += 64) { + ABEF = STATE0; CDGH = STATE1; + + MSG0 = _mm_shuffle_epi8(_mm_loadu_si128((const __m128i*)(data+0)), MASK); + MSG = _mm_add_epi32(MSG0, _mm_set_epi64x(0xE9B5DBA5B5C0FBCFULL,0x71374491428A2F98ULL)); + STATE1 = _mm_sha256rnds2_epu32(STATE1, STATE0, MSG); + STATE0 = _mm_sha256rnds2_epu32(STATE0, STATE1, _mm_shuffle_epi32(MSG,0x0E)); + + MSG1 = _mm_shuffle_epi8(_mm_loadu_si128((const __m128i*)(data+16)), MASK); + MSG = _mm_add_epi32(MSG1, _mm_set_epi64x(0xAB1C5ED5923F82A4ULL,0x59F111F13956C25BULL)); + STATE1 = _mm_sha256rnds2_epu32(STATE1, STATE0, MSG); + STATE0 = _mm_sha256rnds2_epu32(STATE0, STATE1, _mm_shuffle_epi32(MSG,0x0E)); + MSG0 = _mm_sha256msg1_epu32(MSG0, MSG1); + + MSG2 = _mm_shuffle_epi8(_mm_loadu_si128((const __m128i*)(data+32)), MASK); + MSG = _mm_add_epi32(MSG2, _mm_set_epi64x(0x550C7DC3243185BEULL,0x12835B01D807AA98ULL)); + STATE1 = _mm_sha256rnds2_epu32(STATE1, STATE0, MSG); + STATE0 = _mm_sha256rnds2_epu32(STATE0, STATE1, _mm_shuffle_epi32(MSG,0x0E)); + MSG1 = _mm_sha256msg1_epu32(MSG1, MSG2); + + MSG3 = _mm_shuffle_epi8(_mm_loadu_si128((const __m128i*)(data+48)), MASK); + MSG = _mm_add_epi32(MSG3, _mm_set_epi64x(0xC19BF1749BDC06A7ULL,0x80DEB1FE72BE5D74ULL)); + STATE1 = _mm_sha256rnds2_epu32(STATE1, STATE0, MSG); + TMP = _mm_alignr_epi8(MSG3, MSG2, 4); + MSG0 = _mm_sha256msg2_epu32(_mm_add_epi32(MSG0, TMP), MSG3); + STATE0 = _mm_sha256rnds2_epu32(STATE0, STATE1, _mm_shuffle_epi32(MSG,0x0E)); + MSG2 = _mm_sha256msg1_epu32(MSG2, MSG3); + + /* rounds 16..63: 12 near-identical message-schedule steps */ + /* Call sites pass the K pair in (low64, high64) order, matching how the + * rounds 0-15 blocks above are written; emit set(high, low). */ + #define RND4(Ma, Mb, Mc, Md, KL, KH) \ + MSG = _mm_add_epi32(Ma, _mm_set_epi64x(KH, KL)); \ + STATE1 = _mm_sha256rnds2_epu32(STATE1, STATE0, MSG); \ + TMP = _mm_alignr_epi8(Ma, Md, 4); \ + Mb = _mm_sha256msg2_epu32(_mm_add_epi32(Mb, TMP), Ma); \ + STATE0 = _mm_sha256rnds2_epu32(STATE0, STATE1, _mm_shuffle_epi32(MSG,0x0E)); \ + Mc = _mm_sha256msg1_epu32(Mc, Ma); + + RND4(MSG0, MSG1, MSG3, MSG3, 0xEFBE4786E49B69C1ULL, 0x240CA1CC0FC19DC6ULL); + RND4(MSG1, MSG2, MSG0, MSG0, 0x4A7484AA2DE92C6FULL, 0x76F988DA5CB0A9DCULL); + RND4(MSG2, MSG3, MSG1, MSG1, 0xA831C66D983E5152ULL, 0xBF597FC7B00327C8ULL); + RND4(MSG3, MSG0, MSG2, MSG2, 0xD5A79147C6E00BF3ULL, 0x1429296706CA6351ULL); + RND4(MSG0, MSG1, MSG3, MSG3, 0x2E1B213827B70A85ULL, 0x53380D134D2C6DFCULL); + RND4(MSG1, MSG2, MSG0, MSG0, 0x766A0ABB650A7354ULL, 0x92722C8581C2C92EULL); + RND4(MSG2, MSG3, MSG1, MSG1, 0xA81A664BA2BFE8A1ULL, 0xC76C51A3C24B8B70ULL); + RND4(MSG3, MSG0, MSG2, MSG2, 0xD6990624D192E819ULL, 0x106AA070F40E3585ULL); + RND4(MSG0, MSG1, MSG3, MSG3, 0x1E376C0819A4C116ULL, 0x34B0BCB52748774CULL); + RND4(MSG1, MSG2, MSG0, MSG0, 0x4ED8AA4A391C0CB3ULL, 0x682E6FF35B9CCA4FULL); + #undef RND4 + + /* rounds 56..63 (no more message scheduling) */ + MSG = _mm_add_epi32(MSG2, _mm_set_epi64x(0x8CC7020884C87814ULL,0x78A5636F748F82EEULL)); + STATE1 = _mm_sha256rnds2_epu32(STATE1, STATE0, MSG); + TMP = _mm_alignr_epi8(MSG2, MSG1, 4); + MSG3 = _mm_sha256msg2_epu32(_mm_add_epi32(MSG3, TMP), MSG2); + STATE0 = _mm_sha256rnds2_epu32(STATE0, STATE1, _mm_shuffle_epi32(MSG,0x0E)); + + MSG = _mm_add_epi32(MSG3, _mm_set_epi64x(0xC67178F2BEF9A3F7ULL,0xA4506CEB90BEFFFAULL)); + STATE1 = _mm_sha256rnds2_epu32(STATE1, STATE0, MSG); + STATE0 = _mm_sha256rnds2_epu32(STATE0, STATE1, _mm_shuffle_epi32(MSG,0x0E)); + + STATE0 = _mm_add_epi32(STATE0, ABEF); + STATE1 = _mm_add_epi32(STATE1, CDGH); + } + + TMP = _mm_shuffle_epi32(STATE0, 0x1B); /* FEBA */ + STATE1 = _mm_shuffle_epi32(STATE1, 0xB1); /* DCHG */ + STATE0 = _mm_blend_epi16(TMP, STATE1, 0xF0); /* DCBA */ + STATE1 = _mm_alignr_epi8(STATE1, TMP, 8); /* ABEF */ + _mm_storeu_si128((__m128i*)&state[0], STATE0); + _mm_storeu_si128((__m128i*)&state[4], STATE1); +} +#endif /* NAUT_HAVE_SHANI */ + +typedef void (*compress_fn)(uint32_t[8], const uint8_t *, size_t); +static compress_fn g_compress; +static const char *g_backend = "scalar"; + +static compress_fn select_compress(void) { +#ifdef NAUT_HAVE_SHANI + if (!getenv("NAUT_NO_SHANI") && __builtin_cpu_supports("sha")) { + g_backend = "sha-ni"; + return sha256_shani; + } +#endif + g_backend = "scalar"; + return sha256_scalar; +} + +NAUT_INLINE compress_fn compress(void) { + compress_fn f = __atomic_load_n(&g_compress, __ATOMIC_RELAXED); + if (NAUT_UNLIKELY(!f)) { + f = select_compress(); + __atomic_store_n(&g_compress, f, __ATOMIC_RELAXED); + } + return f; +} + +const char *naut_sha256_backend(void) { (void)compress(); return g_backend; } + +void naut_sha256_init(naut_sha256_ctx *c) { + static const uint32_t iv[8] = { + 0x6a09e667,0xbb67ae85,0x3c6ef372,0xa54ff53a, + 0x510e527f,0x9b05688c,0x1f83d9ab,0x5be0cd19}; + memcpy(c->h, iv, sizeof iv); + c->len = 0; c->used = 0; +} + +void naut_sha256_update(naut_sha256_ctx *c, const void *data, size_t len) { + const uint8_t *p = data; + compress_fn f = compress(); + c->len += len; + if (c->used) { + size_t need = 64 - c->used, take = len < need ? len : need; + memcpy(c->block + c->used, p, take); + c->used += take; p += take; len -= take; + if (c->used == 64) { f(c->h, c->block, 1); c->used = 0; } + } + if (len >= 64) { size_t nb = len/64; f(c->h, p, nb); p += nb*64; len -= nb*64; } + if (len) { memcpy(c->block, p, len); c->used = len; } +} + +void naut_sha256_final(naut_sha256_ctx *c, uint8_t out[NAUT_SHA256_LEN]) { + uint64_t bits = c->len * 8; + uint8_t pad = 0x80, zero = 0; + naut_sha256_update(c, &pad, 1); + while (c->used != 56) naut_sha256_update(c, &zero, 1); + uint8_t lenbe[8]; + for (int i = 0; i < 8; i++) lenbe[i] = (uint8_t)(bits >> (56 - i*8)); + naut_sha256_update(c, lenbe, 8); + for (int i = 0; i < 8; i++) { + out[i*4] = (uint8_t)(c->h[i] >> 24); + out[i*4+1] = (uint8_t)(c->h[i] >> 16); + out[i*4+2] = (uint8_t)(c->h[i] >> 8); + out[i*4+3] = (uint8_t)(c->h[i]); + } +} + +void naut_sha256(const void *data, size_t len, uint8_t out[NAUT_SHA256_LEN]) { + naut_sha256_ctx c; naut_sha256_init(&c); naut_sha256_update(&c, data, len); + naut_sha256_final(&c, out); +} diff --git a/src/dht/dht.c b/src/dht/dht.c new file mode 100644 index 0000000..2a9922d --- /dev/null +++ b/src/dht/dht.c @@ -0,0 +1,227 @@ +#include "naut/dht.h" +#include "naut/bencode.h" + +#include +#include + +static naut_err finish(naut_bc_writer *w, uint8_t **out, size_t *out_len) { + if (w->err != NAUT_OK) { + naut_err e = w->err; + naut_bc_w_free(w); + return e; + } + *out = w->buf; + *out_len = w->len; + w->buf = NULL; + naut_bc_w_free(w); + return NAUT_OK; +} + +static bool valid_common(const uint8_t *tx, size_t tx_len, + const uint8_t id[20], uint8_t **out, size_t *out_len) { + return tx && tx_len > 0 && tx_len <= 8 && id && out && out_len; +} + +naut_err naut_dht_build_ping(const uint8_t *tx, size_t tx_len, + const uint8_t id[20], + uint8_t **out, size_t *out_len) { + if (!valid_common(tx, tx_len, id, out, out_len)) return NAUT_ERR_INVAL; + naut_bc_writer w; naut_bc_w_init(&w); + naut_bc_w_dict_begin(&w); + naut_bc_w_cstr(&w, "a"); naut_bc_w_dict_begin(&w); + naut_bc_w_cstr(&w, "id"); naut_bc_w_bytes(&w, id, 20); + naut_bc_w_end(&w); + naut_bc_w_cstr(&w, "q"); naut_bc_w_cstr(&w, "ping"); + naut_bc_w_cstr(&w, "t"); naut_bc_w_bytes(&w, tx, tx_len); + naut_bc_w_cstr(&w, "y"); naut_bc_w_cstr(&w, "q"); + naut_bc_w_end(&w); + return finish(&w, out, out_len); +} + +static naut_err build_target_query(const char *query, const char *target_key, + const uint8_t *tx, size_t tx_len, + const uint8_t id[20], + const uint8_t target[20], + uint8_t **out, size_t *out_len) { + if (!query || !target_key || !target || + !valid_common(tx, tx_len, id, out, out_len)) return NAUT_ERR_INVAL; + naut_bc_writer w; naut_bc_w_init(&w); + naut_bc_w_dict_begin(&w); + naut_bc_w_cstr(&w, "a"); naut_bc_w_dict_begin(&w); + naut_bc_w_cstr(&w, "id"); naut_bc_w_bytes(&w, id, 20); + naut_bc_w_cstr(&w, target_key); naut_bc_w_bytes(&w, target, 20); + naut_bc_w_end(&w); + naut_bc_w_cstr(&w, "q"); naut_bc_w_cstr(&w, query); + naut_bc_w_cstr(&w, "t"); naut_bc_w_bytes(&w, tx, tx_len); + naut_bc_w_cstr(&w, "y"); naut_bc_w_cstr(&w, "q"); + naut_bc_w_end(&w); + return finish(&w, out, out_len); +} + +naut_err naut_dht_build_find_node(const uint8_t *tx, size_t tx_len, + const uint8_t id[20], + const uint8_t target[20], + uint8_t **out, size_t *out_len) { + return build_target_query("find_node", "target", tx, tx_len, id, target, + out, out_len); +} + +naut_err naut_dht_build_get_peers(const uint8_t *tx, size_t tx_len, + const uint8_t id[20], + const uint8_t info_hash[20], + uint8_t **out, size_t *out_len) { + return build_target_query("get_peers", "info_hash", tx, tx_len, id, + info_hash, out, out_len); +} + +naut_err naut_dht_build_announce_peer(const uint8_t *tx, size_t tx_len, + const uint8_t id[20], + const uint8_t info_hash[20], + uint16_t port, bool implied_port, + const void *token, size_t token_len, + uint8_t **out, size_t *out_len) { + if (!valid_common(tx, tx_len, id, out, out_len) || !info_hash || + !token || token_len == 0 || token_len > 64 || (!implied_port && port == 0)) + return NAUT_ERR_INVAL; + naut_bc_writer w; naut_bc_w_init(&w); + naut_bc_w_dict_begin(&w); + naut_bc_w_cstr(&w, "a"); naut_bc_w_dict_begin(&w); + naut_bc_w_cstr(&w, "id"); naut_bc_w_bytes(&w, id, 20); + naut_bc_w_cstr(&w, "implied_port"); naut_bc_w_int(&w, implied_port ? 1 : 0); + naut_bc_w_cstr(&w, "info_hash"); naut_bc_w_bytes(&w, info_hash, 20); + naut_bc_w_cstr(&w, "port"); naut_bc_w_int(&w, port); + naut_bc_w_cstr(&w, "token"); naut_bc_w_bytes(&w, token, token_len); + naut_bc_w_end(&w); + naut_bc_w_cstr(&w, "q"); naut_bc_w_cstr(&w, "announce_peer"); + naut_bc_w_cstr(&w, "t"); naut_bc_w_bytes(&w, tx, tx_len); + naut_bc_w_cstr(&w, "y"); naut_bc_w_cstr(&w, "q"); + naut_bc_w_end(&w); + return finish(&w, out, out_len); +} + +static naut_err parse_nodes(const uint8_t *p, size_t n, + naut_dht_node **out, size_t *count) { + if (n % 26 != 0 || n / 26 > NAUT_DHT_MAX_NODES) return NAUT_ERR_PROTO; + size_t num = n / 26; + naut_dht_node *nodes = calloc(num ? num : 1, sizeof(*nodes)); + if (!nodes) return NAUT_ERR_NOMEM; + for (size_t i = 0; i < num; i++) { + const uint8_t *entry = p + i * 26; + memcpy(nodes[i].id, entry, 20); + memcpy(nodes[i].ip, entry + 20, 4); + nodes[i].port = ((uint16_t)entry[24] << 8) | entry[25]; + if (nodes[i].port == 0) { + free(nodes); + return NAUT_ERR_PROTO; + } + } + *out = nodes; + *count = num; + return NAUT_OK; +} + +static bool peer_duplicate(const naut_peer_addr *peers, size_t n, + const naut_peer_addr *candidate) { + for (size_t i = 0; i < n; i++) + if (peers[i].port == candidate->port && + memcmp(peers[i].ip, candidate->ip, 4) == 0) + return true; + return false; +} + +static naut_err parse_values(const naut_bc *values, + naut_peer_addr **out, size_t *count) { + if (!values || values->type != NAUT_BC_LIST || + values->v.list.count > NAUT_DHT_MAX_PEERS) return NAUT_ERR_PROTO; + naut_peer_addr *peers = calloc(values->v.list.count ? values->v.list.count : 1, + sizeof(*peers)); + if (!peers) return NAUT_ERR_NOMEM; + size_t num = 0; + for (size_t i = 0; i < values->v.list.count; i++) { + const uint8_t *p; size_t n; + if (!naut_bc_get_str(naut_bc_list_at(values, i), &p, &n) || n != 6) { + free(peers); + return NAUT_ERR_PROTO; + } + naut_peer_addr peer; + memcpy(peer.ip, p, 4); + peer.port = ((uint16_t)p[4] << 8) | p[5]; + if (peer.port && !peer_duplicate(peers, num, &peer)) + peers[num++] = peer; + } + *out = peers; + *count = num; + return NAUT_OK; +} + +naut_err naut_dht_parse_response(const uint8_t *data, size_t len, + naut_dht_response *out) { + if (!data || !out) return NAUT_ERR_INVAL; + memset(out, 0, sizeof(*out)); + naut_bc_doc *doc = NULL; + naut_err err = naut_bc_parse(data, len, &doc); + if (err != NAUT_OK) return err; + const naut_bc *root = naut_bc_root(doc); + const uint8_t *p; size_t n; + if (!root || root->type != NAUT_BC_DICT || + !naut_bc_get_str(naut_bc_dict_get(root, "t"), &p, &n) || + n == 0 || n > sizeof out->transaction) { + err = NAUT_ERR_PROTO; + goto done; + } + memcpy(out->transaction, p, n); + out->transaction_len = n; + const naut_bc *y = naut_bc_dict_get(root, "y"); + if (naut_bc_str_eq(y, "e")) { + const naut_bc *e = naut_bc_dict_get(root, "e"); + int64_t code; + if (!e || e->type != NAUT_BC_LIST || e->v.list.count < 1 || + !naut_bc_get_int(naut_bc_list_at(e, 0), &code)) { + err = NAUT_ERR_PROTO; + goto done; + } + out->type = NAUT_DHT_ERROR; + out->error_code = (int)code; + goto done; + } + if (!naut_bc_str_eq(y, "r")) { + err = NAUT_ERR_PROTO; + goto done; + } + out->type = NAUT_DHT_RESPONSE; + const naut_bc *r = naut_bc_dict_get(root, "r"); + if (!r || r->type != NAUT_BC_DICT) { + err = NAUT_ERR_PROTO; + goto done; + } + if (naut_bc_get_str(naut_bc_dict_get(r, "id"), &p, &n)) { + if (n != 20) { err = NAUT_ERR_PROTO; goto done; } + memcpy(out->id, p, 20); + out->has_id = true; + } + if (naut_bc_get_str(naut_bc_dict_get(r, "token"), &p, &n)) { + if (n == 0 || n > sizeof out->token) { err = NAUT_ERR_PROTO; goto done; } + memcpy(out->token, p, n); + out->token_len = n; + } + if (naut_bc_get_str(naut_bc_dict_get(r, "nodes"), &p, &n)) { + err = parse_nodes(p, n, &out->nodes, &out->num_nodes); + if (err != NAUT_OK) goto done; + } + const naut_bc *values = naut_bc_dict_get(r, "values"); + if (values) { + err = parse_values(values, &out->peers, &out->num_peers); + if (err != NAUT_OK) goto done; + } +done: + naut_bc_free(doc); + if (err != NAUT_OK) naut_dht_response_free(out); + return err; +} + +void naut_dht_response_free(naut_dht_response *response) { + if (!response) return; + free(response->nodes); + free(response->peers); + memset(response, 0, sizeof(*response)); +} diff --git a/src/dht/fetch.c b/src/dht/fetch.c new file mode 100644 index 0000000..c76b8bd --- /dev/null +++ b/src/dht/fetch.c @@ -0,0 +1,154 @@ +#include "naut/dht.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +typedef struct { + struct sockaddr_in addr; + bool queried; +} candidate; + +static bool parse_endpoint(const char *text, struct sockaddr_in *out) { + const char *colon = strrchr(text, ':'); + if (!colon || colon == text) return false; + char host[256], port[16]; + size_t host_len = (size_t)(colon - text); + size_t port_len = strlen(colon + 1); + if (host_len >= sizeof host || port_len == 0 || port_len >= sizeof port) + return false; + memcpy(host, text, host_len); host[host_len] = 0; + memcpy(port, colon + 1, port_len + 1); + struct addrinfo hints, *result = NULL; + memset(&hints, 0, sizeof hints); + hints.ai_family = AF_INET; + hints.ai_socktype = SOCK_DGRAM; + if (getaddrinfo(host, port, &hints, &result) != 0) return false; + memcpy(out, result->ai_addr, sizeof(*out)); + freeaddrinfo(result); + return true; +} + +static bool same_addr(const struct sockaddr_in *a, const struct sockaddr_in *b) { + return a->sin_port == b->sin_port && a->sin_addr.s_addr == b->sin_addr.s_addr; +} + +static bool add_candidate(candidate *v, size_t *n, const struct sockaddr_in *addr) { + if (addr->sin_port == 0) return true; + for (size_t i = 0; i < *n; i++) + if (same_addr(&v[i].addr, addr)) return true; + if (*n == NAUT_DHT_MAX_NODES) return false; + v[*n].addr = *addr; + v[*n].queried = false; + (*n)++; + return true; +} + +static bool add_peer(naut_peer_addr *v, size_t *n, const naut_peer_addr *peer) { + for (size_t i = 0; i < *n; i++) + if (v[i].port == peer->port && memcmp(v[i].ip, peer->ip, 4) == 0) + return true; + if (*n == NAUT_DHT_MAX_PEERS) return false; + v[(*n)++] = *peer; + return true; +} + +static void node_id(uint8_t id[20]) { + int fd = open("/dev/urandom", O_RDONLY); + if (fd >= 0) { + size_t done = 0; + while (done < 20) { + ssize_t n = read(fd, id + done, 20 - done); + if (n <= 0) break; + done += (size_t)n; + } + close(fd); + if (done == 20) return; + } + for (size_t i = 0; i < 20; i++) id[i] = (uint8_t)rand(); +} + +naut_err naut_dht_get_peers(const char *const *bootstrap, size_t num_bootstrap, + const uint8_t info_hash[20], + naut_peer_addr **peers, size_t *num_peers) { + if (!bootstrap || num_bootstrap == 0 || !info_hash || !peers || !num_peers) + return NAUT_ERR_INVAL; + *peers = NULL; *num_peers = 0; + candidate nodes[NAUT_DHT_MAX_NODES]; + size_t node_count = 0; + for (size_t i = 0; i < num_bootstrap; i++) { + struct sockaddr_in addr; + if (parse_endpoint(bootstrap[i], &addr)) + add_candidate(nodes, &node_count, &addr); + } + if (node_count == 0) return NAUT_ERR_INVAL; + + int fd = socket(AF_INET, SOCK_DGRAM, 0); + if (fd < 0) return NAUT_ERR_IO; + naut_peer_addr found[NAUT_DHT_MAX_PEERS]; + size_t found_count = 0; + uint8_t id[20]; + node_id(id); + uint16_t tx_counter = 1; + size_t queries = 0; + + while (queries < 64 && found_count < NAUT_DHT_MAX_PEERS) { + size_t index = SIZE_MAX; + for (size_t i = 0; i < node_count; i++) + if (!nodes[i].queried) { index = i; break; } + if (index == SIZE_MAX) break; + nodes[index].queried = true; + queries++; + uint8_t tx[2] = { (uint8_t)(tx_counter >> 8), (uint8_t)tx_counter }; + tx_counter++; + uint8_t *query = NULL; size_t query_len = 0; + if (naut_dht_build_get_peers(tx, sizeof tx, id, info_hash, + &query, &query_len) != NAUT_OK) + continue; + ssize_t sent = sendto(fd, query, query_len, 0, + (struct sockaddr *)&nodes[index].addr, + sizeof(nodes[index].addr)); + free(query); + if (sent < 0) continue; + + struct pollfd pfd = { .fd = fd, .events = POLLIN }; + if (poll(&pfd, 1, 1000) <= 0) continue; + uint8_t packet[65536]; + ssize_t received = recv(fd, packet, sizeof packet, 0); + if (received <= 0) continue; + naut_dht_response response; + if (naut_dht_parse_response(packet, (size_t)received, &response) != NAUT_OK) + continue; + if (response.transaction_len != sizeof tx || + memcmp(response.transaction, tx, sizeof tx) != 0 || + response.type != NAUT_DHT_RESPONSE) { + naut_dht_response_free(&response); + continue; + } + for (size_t i = 0; i < response.num_peers; i++) + add_peer(found, &found_count, &response.peers[i]); + for (size_t i = 0; i < response.num_nodes; i++) { + struct sockaddr_in addr; + memset(&addr, 0, sizeof addr); + addr.sin_family = AF_INET; + memcpy(&addr.sin_addr, response.nodes[i].ip, 4); + addr.sin_port = htons(response.nodes[i].port); + add_candidate(nodes, &node_count, &addr); + } + naut_dht_response_free(&response); + } + close(fd); + if (found_count == 0) return NAUT_ERR_EMPTY; + naut_peer_addr *result = malloc(found_count * sizeof(*result)); + if (!result) return NAUT_ERR_NOMEM; + memcpy(result, found, found_count * sizeof(*result)); + *peers = result; + *num_peers = found_count; + return NAUT_OK; +} diff --git a/src/metainfo/magnet.c b/src/metainfo/magnet.c new file mode 100644 index 0000000..7a82578 --- /dev/null +++ b/src/metainfo/magnet.c @@ -0,0 +1,116 @@ +#include "naut/metainfo.h" +#include +#include +#include + +static int hexval(int c) { + if (c >= '0' && c <= '9') return c - '0'; + if (c >= 'a' && c <= 'f') return c - 'a' + 10; + if (c >= 'A' && c <= 'F') return c - 'A' + 10; + return -1; +} + +static bool hex_decode(const char *s, size_t slen, uint8_t *out, size_t outlen) { + if (slen != outlen * 2) return false; + for (size_t i = 0; i < outlen; i++) { + int hi = hexval(s[i*2]), lo = hexval(s[i*2+1]); + if (hi < 0 || lo < 0) return false; + out[i] = (uint8_t)((hi << 4) | lo); + } + return true; +} + +/* RFC 4648 base32 (no padding needed for the 32-char btih form -> 20 bytes) */ +static bool base32_decode(const char *s, size_t slen, uint8_t *out, size_t outlen) { + static const char *A = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; + uint32_t buf = 0; int bits = 0; size_t o = 0; + for (size_t i = 0; i < slen; i++) { + char c = (char)toupper((unsigned char)s[i]); + const char *pos = strchr(A, c); + if (!pos || c == 0) return false; + buf = (buf << 5) | (uint32_t)(pos - A); + bits += 5; + if (bits >= 8) { + bits -= 8; + if (o >= outlen) return false; + out[o++] = (uint8_t)((buf >> bits) & 0xff); + } + } + return o == outlen; +} + +/* in-place percent-decode of a query-component (also '+' -> space) */ +static char *url_decode(const char *s, size_t n) { + char *out = malloc(n + 1); + if (!out) return NULL; + size_t o = 0; + for (size_t i = 0; i < n; i++) { + if (s[i] == '%' && i + 2 < n) { + int hi = hexval(s[i+1]), lo = hexval(s[i+2]); + if (hi >= 0 && lo >= 0) { out[o++] = (char)((hi << 4) | lo); i += 2; continue; } + } + out[o++] = (s[i] == '+') ? ' ' : s[i]; + } + out[o] = 0; + return out; +} + +static void set_xt(naut_magnet *m, const char *val) { + /* urn:btih: or urn:btmh:1220 */ + if (!strncmp(val, "urn:btih:", 9)) { + const char *h = val + 9; size_t n = strlen(h); + if (n == 40 && hex_decode(h, 40, m->infohash_v1, 20)) m->has_v1 = true; + else if (n == 32 && base32_decode(h, 32, m->infohash_v1, 20)) m->has_v1 = true; + } else if (!strncmp(val, "urn:btmh:", 9)) { + const char *h = val + 9; + /* multihash: 0x12 = sha2-256, 0x20 = length 32 -> prefix "1220" */ + if (strlen(h) == 68 && !strncmp(h, "1220", 4) && + hex_decode(h + 4, 64, m->infohash_v2, 32)) + m->has_v2 = true; + } +} + +naut_err naut_magnet_parse(const char *uri, naut_magnet *out) { + memset(out, 0, sizeof(*out)); + if (!uri || strncmp(uri, "magnet:?", 8) != 0) return NAUT_ERR_INVAL; + + const char *q = uri + 8; + size_t tcap = 0; + while (*q) { + const char *amp = strchr(q, '&'); + size_t plen = amp ? (size_t)(amp - q) : strlen(q); + const char *eq = memchr(q, '=', plen); + if (eq) { + size_t klen = (size_t)(eq - q); + const char *vstart = eq + 1; + size_t vlen = plen - klen - 1; + char *val = url_decode(vstart, vlen); + if (val) { + if (klen == 2 && !strncmp(q, "xt", 2)) { + set_xt(out, val); + } else if (klen == 2 && !strncmp(q, "dn", 2)) { + free(out->name); out->name = val; val = NULL; + } else if (klen == 2 && !strncmp(q, "tr", 2)) { + if (out->num_trackers == tcap) { + tcap = tcap ? tcap * 2 : 4; + out->trackers = realloc(out->trackers, tcap * sizeof(char *)); + } + out->trackers[out->num_trackers++] = val; val = NULL; + } + free(val); + } + } + if (!amp) break; + q = amp + 1; + } + if (!out->has_v1 && !out->has_v2) { naut_magnet_free(out); return NAUT_ERR_PROTO; } + return NAUT_OK; +} + +void naut_magnet_free(naut_magnet *m) { + if (!m) return; + free(m->name); + for (size_t i = 0; i < m->num_trackers; i++) free(m->trackers[i]); + free(m->trackers); + memset(m, 0, sizeof(*m)); +} diff --git a/src/metainfo/metainfo.c b/src/metainfo/metainfo.c new file mode 100644 index 0000000..84277a2 --- /dev/null +++ b/src/metainfo/metainfo.c @@ -0,0 +1,289 @@ +#include "naut/metainfo.h" +#include "naut/bencode.h" +#include "naut/log.h" + +#include +#include +#include + +/* The doc + a copy of the source bytes are kept alive in `_owned` so that the + * zero-copy piece-hash slice remains valid for the life of the metainfo. */ +typedef struct { + naut_bc_doc *doc; + uint8_t *src; +} owned; + +static char *dup_cstr(const uint8_t *p, size_t n) { + char *s = malloc(n + 1); + if (!s) return NULL; + memcpy(s, p, n); s[n] = 0; + return s; +} + +/* collect a single announce string or an announce-list (list of tiers) */ +static void collect_trackers(const naut_bc *root, naut_metainfo *mi) { + size_t cap = 0; + const naut_bc *al = naut_bc_dict_get(root, "announce-list"); + if (al && al->type == NAUT_BC_LIST) { + for (size_t t = 0; t < al->v.list.count; t++) { + const naut_bc *tier = naut_bc_list_at(al, t); + if (!tier || tier->type != NAUT_BC_LIST) continue; + for (size_t u = 0; u < tier->v.list.count; u++) { + const naut_bc *url = naut_bc_list_at(tier, u); + const uint8_t *p; size_t n; + if (!naut_bc_get_str(url, &p, &n)) continue; + if (mi->num_trackers == cap) { + cap = cap ? cap * 2 : 8; + mi->trackers = realloc(mi->trackers, cap * sizeof(char *)); + } + mi->trackers[mi->num_trackers++] = dup_cstr(p, n); + } + } + } + if (mi->num_trackers == 0) { + const uint8_t *p; size_t n; + if (naut_bc_get_str(naut_bc_dict_get(root, "announce"), &p, &n)) { + mi->trackers = malloc(sizeof(char *)); + mi->trackers[mi->num_trackers++] = dup_cstr(p, n); + } + } +} + +/* v1 file list: single-file (info.length) or multi-file (info.files[]) */ +static naut_err collect_files_v1(const naut_bc *info, naut_metainfo *mi) { + const uint8_t *np = NULL; size_t nn = 0; + if (naut_bc_get_str(naut_bc_dict_get(info, "name"), &np, &nn)) + mi->name = dup_cstr(np, nn); + else + mi->name = dup_cstr((const uint8_t *)"unnamed", 7); + + int64_t single_len; + const naut_bc *files = naut_bc_dict_get(info, "files"); + if (naut_bc_get_int(naut_bc_dict_get(info, "length"), &single_len)) { + mi->files = calloc(1, sizeof(naut_file)); + if (!mi->files) return NAUT_ERR_NOMEM; + mi->files[0].path = dup_cstr((const uint8_t *)mi->name, strlen(mi->name)); + mi->files[0].length = single_len; + mi->num_files = 1; + mi->total_length = single_len; + } else if (files && files->type == NAUT_BC_LIST) { + mi->files = calloc(files->v.list.count, sizeof(naut_file)); + if (!mi->files) return NAUT_ERR_NOMEM; + for (size_t i = 0; i < files->v.list.count; i++) { + const naut_bc *f = naut_bc_list_at(files, i); + int64_t flen = 0; + naut_bc_get_int(naut_bc_dict_get(f, "length"), &flen); + char joined[4096]; size_t jl = 0; + + /* BEP-47 padding file (attr contains 'p'): it occupies the flat byte + * space for v2 piece alignment but is not real content. Keep it in + * the storage layout (offsets stay correct) but route it out of the + * content tree to a root-level .pad/ path. */ + const uint8_t *attr; size_t attrn; + bool is_pad = false; + if (naut_bc_get_str(naut_bc_dict_get(f, "attr"), &attr, &attrn)) + for (size_t a = 0; a < attrn; a++) if (attr[a] == 'p') is_pad = true; + if (is_pad) { + jl = (size_t)snprintf(joined, sizeof joined, ".pad/%zu", i); + mi->files[i].path = dup_cstr((const uint8_t *)joined, jl); + mi->files[i].length = flen; + mi->total_length += flen; + mi->num_files++; + continue; + } + + /* BEP-3 multi-file layout is /; root the path at name */ + const naut_bc *pth = naut_bc_dict_get(f, "path"); + size_t namelen = strlen(mi->name); + if (namelen < sizeof joined - 1) { memcpy(joined, mi->name, namelen); jl = namelen; } + if (pth && pth->type == NAUT_BC_LIST) { + for (size_t k = 0; k < pth->v.list.count; k++) { + const uint8_t *cp; size_t cn; + if (!naut_bc_get_str(naut_bc_list_at(pth, k), &cp, &cn)) continue; + if (jl < sizeof joined - 1) joined[jl++] = '/'; + size_t room = sizeof joined - 1 - jl; + if (cn > room) cn = room; + memcpy(joined + jl, cp, cn); jl += cn; + } + } + joined[jl] = 0; + mi->files[i].path = dup_cstr((const uint8_t *)joined, jl); + mi->files[i].length = flen; + mi->total_length += flen; + mi->num_files++; + } + } else { + return NAUT_ERR_PROTO; /* neither length nor files */ + } + return NAUT_OK; +} + +/* v2 (BEP-52) "file tree": nested dicts; a leaf is a dict with an empty-string + * key mapping to {length, pieces root}. Build '/'-joined paths and sum lengths. */ +static void add_file(naut_metainfo *mi, size_t *cap, const char *path, int64_t len) { + if (mi->num_files == *cap) { + *cap = *cap ? *cap * 2 : 8; + mi->files = realloc(mi->files, *cap * sizeof(naut_file)); + } + mi->files[mi->num_files].path = dup_cstr((const uint8_t *)path, strlen(path)); + mi->files[mi->num_files].length = len; + mi->num_files++; + mi->total_length += len; +} + +static void walk_tree(const naut_bc *node, naut_metainfo *mi, size_t *cap, + char *prefix, size_t plen) { + if (!node || node->type != NAUT_BC_DICT) return; + for (size_t i = 0; i < node->v.dict.count; i++) { + const naut_bc_pair *pr = &node->v.dict.pairs[i]; + if (pr->kn == 0) { /* leaf: this prefix is a file */ + int64_t flen = 0; + naut_bc_get_int(naut_bc_dict_get(pr->val, "length"), &flen); + prefix[plen] = 0; + add_file(mi, cap, prefix, flen); + continue; + } + char sub[4096]; + memcpy(sub, prefix, plen); + size_t sl = plen; + if (sl && sl < sizeof sub - 1) sub[sl++] = '/'; + size_t room = sizeof sub - 1 - sl; + size_t cn = pr->kn < room ? pr->kn : room; + memcpy(sub + sl, pr->kp, cn); sl += cn; + walk_tree(pr->val, mi, cap, sub, sl); + } +} + +static void collect_files_v2(const naut_bc *info, naut_metainfo *mi) { + const uint8_t *np; size_t nn; + if (!mi->name && naut_bc_get_str(naut_bc_dict_get(info, "name"), &np, &nn)) + mi->name = dup_cstr(np, nn); + const naut_bc *tree = naut_bc_dict_get(info, "file tree"); + if (!tree) return; + size_t cap = 0; + char prefix[4096]; + walk_tree(tree, mi, &cap, prefix, 0); +} + +naut_err naut_metainfo_parse(const uint8_t *data, size_t len, naut_metainfo *out) { + memset(out, 0, sizeof(*out)); + owned *o = calloc(1, sizeof(owned)); + if (!o) return NAUT_ERR_NOMEM; + + /* own a copy so slices outlive the caller's buffer */ + o->src = malloc(len ? len : 1); + if (!o->src) { free(o); return NAUT_ERR_NOMEM; } + memcpy(o->src, data, len); + + naut_err e = naut_bc_parse(o->src, len, &o->doc); + if (e != NAUT_OK) { free(o->src); free(o); return e; } + + const naut_bc *root = naut_bc_root(o->doc); + const naut_bc *info = naut_bc_dict_get(root, "info"); + if (!info || info->type != NAUT_BC_DICT) { + naut_bc_free(o->doc); free(o->src); free(o); + return NAUT_ERR_PROTO; + } + + /* info-hashes over the raw info-dict bytes */ + const naut_bc *pieces = naut_bc_dict_get(info, "pieces"); + int64_t meta_ver = 0; + naut_bc_get_int(naut_bc_dict_get(info, "meta version"), &meta_ver); + + if (pieces && pieces->type == NAUT_BC_STR) { /* v1 / hybrid */ + naut_sha1(info->raw, info->raw_len, out->infohash_v1); + out->has_v1 = true; + } + if (meta_ver == 2) { /* v2 / hybrid */ + naut_sha256(info->raw, info->raw_len, out->infohash_v2); + out->has_v2 = true; + } + if (!out->has_v1 && !out->has_v2) { + naut_bc_free(o->doc); free(o->src); free(o); + return NAUT_ERR_PROTO; + } + + naut_bc_get_int(naut_bc_dict_get(info, "piece length"), &out->piece_length); + + if (out->has_v1) { + if (pieces->v.str.n % NAUT_SHA1_LEN != 0) { + naut_bc_free(o->doc); free(o->src); free(o); + return NAUT_ERR_PROTO; + } + out->num_pieces = (uint32_t)(pieces->v.str.n / NAUT_SHA1_LEN); + out->piece_hashes = pieces->v.str.p; /* slice into o->src */ + } + + if (out->has_v1) { + e = collect_files_v1(info, out); + if (e != NAUT_OK) { out->_owned = o; naut_metainfo_free(out); return e; } + } else { + collect_files_v2(info, out); /* v2-only: walk the file tree */ + } + collect_trackers(root, out); + + out->_owned = o; + return NAUT_OK; +} + +naut_err naut_metainfo_parse_info(const uint8_t *info, size_t info_len, + const char *const *trackers, + size_t num_trackers, + naut_metainfo *out) { + if (!info || info_len == 0 || !out || + (num_trackers && !trackers)) + return NAUT_ERR_INVAL; + if (info_len > SIZE_MAX - 8) return NAUT_ERR_RANGE; + uint8_t *torrent = malloc(info_len + 8); + if (!torrent) return NAUT_ERR_NOMEM; + memcpy(torrent, "d4:info", 7); + memcpy(torrent + 7, info, info_len); + torrent[7 + info_len] = 'e'; + naut_err e = naut_metainfo_parse(torrent, info_len + 8, out); + free(torrent); + if (e != NAUT_OK) return e; + + if (num_trackers) { + out->trackers = calloc(num_trackers, sizeof(*out->trackers)); + if (!out->trackers) { + naut_metainfo_free(out); + return NAUT_ERR_NOMEM; + } + for (size_t i = 0; i < num_trackers; i++) { + out->trackers[i] = + dup_cstr((const uint8_t *)trackers[i], strlen(trackers[i])); + if (!out->trackers[i]) { + out->num_trackers = i; + naut_metainfo_free(out); + return NAUT_ERR_NOMEM; + } + } + out->num_trackers = num_trackers; + } + return NAUT_OK; +} + +void naut_metainfo_free(naut_metainfo *mi) { + if (!mi) return; + free(mi->name); + for (size_t i = 0; i < mi->num_files; i++) free(mi->files[i].path); + free(mi->files); + for (size_t i = 0; i < mi->num_trackers; i++) free(mi->trackers[i]); + free(mi->trackers); + if (mi->_owned) { + owned *o = mi->_owned; + naut_bc_free(o->doc); + free(o->src); + free(o); + } + memset(mi, 0, sizeof(*mi)); +} + +void naut_infohash_v1_hex(const naut_metainfo *mi, char out[41]) { + static const char *hx = "0123456789abcdef"; + for (int i = 0; i < NAUT_SHA1_LEN; i++) { + out[i*2] = hx[mi->infohash_v1[i] >> 4]; + out[i*2+1] = hx[mi->infohash_v1[i] & 15]; + } + out[40] = 0; +} diff --git a/src/peer/extension.c b/src/peer/extension.c new file mode 100644 index 0000000..1e6982b --- /dev/null +++ b/src/peer/extension.c @@ -0,0 +1,296 @@ +#include "naut/extension.h" +#include "naut/bencode.h" +#include "naut/peer.h" + +#include +#include + +static void wr32(uint8_t *p, uint32_t v) { + p[0] = (uint8_t)(v >> 24); p[1] = (uint8_t)(v >> 16); + p[2] = (uint8_t)(v >> 8); p[3] = (uint8_t)v; +} + +static naut_err frame(uint8_t ext_id, const uint8_t *payload, size_t payload_len, + uint8_t **out, size_t *out_len) { + if (!out || !out_len || payload_len > UINT32_MAX - 2) return NAUT_ERR_INVAL; + size_t n = 6 + payload_len; + uint8_t *buf = malloc(n); + if (!buf) return NAUT_ERR_NOMEM; + wr32(buf, (uint32_t)(2 + payload_len)); + buf[4] = NAUT_MSG_EXTENDED; + buf[5] = ext_id; + if (payload_len) memcpy(buf + 6, payload, payload_len); + *out = buf; + *out_len = n; + return NAUT_OK; +} + +naut_err naut_ext_build_handshake(uint8_t ut_metadata_id, uint8_t ut_pex_id, + uint32_t metadata_size, uint16_t port, + uint8_t **out, size_t *out_len) { + naut_bc_writer w; + naut_bc_w_init(&w); + naut_bc_w_dict_begin(&w); + naut_bc_w_cstr(&w, "m"); + naut_bc_w_dict_begin(&w); + if (ut_metadata_id) { + naut_bc_w_cstr(&w, "ut_metadata"); + naut_bc_w_int(&w, ut_metadata_id); + } + if (ut_pex_id) { + naut_bc_w_cstr(&w, "ut_pex"); + naut_bc_w_int(&w, ut_pex_id); + } + naut_bc_w_end(&w); + if (metadata_size) { + naut_bc_w_cstr(&w, "metadata_size"); + naut_bc_w_int(&w, metadata_size); + } + if (port) { + naut_bc_w_cstr(&w, "p"); + naut_bc_w_int(&w, port); + } + naut_bc_w_cstr(&w, "reqq"); + naut_bc_w_int(&w, 256); + naut_bc_w_cstr(&w, "v"); + naut_bc_w_cstr(&w, "Naut/0.1"); + naut_bc_w_end(&w); + if (w.err != NAUT_OK) { + naut_err e = w.err; + naut_bc_w_free(&w); + return e; + } + naut_err e = frame(0, w.buf, w.len, out, out_len); + naut_bc_w_free(&w); + return e; +} + +static bool get_u32(const naut_bc *dict, const char *key, uint32_t *out) { + int64_t v; + if (!naut_bc_get_int(naut_bc_dict_get(dict, key), &v) || + v < 0 || v > UINT32_MAX) return false; + *out = (uint32_t)v; + return true; +} + +naut_err naut_ext_parse_handshake(const uint8_t *payload, size_t len, + naut_ext_handshake *out) { + if (!payload || !out) return NAUT_ERR_INVAL; + memset(out, 0, sizeof(*out)); + naut_bc_doc *doc = NULL; + naut_err e = naut_bc_parse(payload, len, &doc); + if (e != NAUT_OK) return e; + const naut_bc *root = naut_bc_root(doc); + if (!root || root->type != NAUT_BC_DICT) { + naut_bc_free(doc); + return NAUT_ERR_PROTO; + } + const naut_bc *m = naut_bc_dict_get(root, "m"); + uint32_t v; + if (m && m->type != NAUT_BC_DICT) { + naut_bc_free(doc); + return NAUT_ERR_PROTO; + } + if (m && get_u32(m, "ut_metadata", &v) && v <= UINT8_MAX) + out->ut_metadata = (uint8_t)v; + if (m && get_u32(m, "ut_pex", &v) && v <= UINT8_MAX) + out->ut_pex = (uint8_t)v; + if (get_u32(root, "metadata_size", &v)) { + if (v == 0 || v > NAUT_METADATA_MAX) { + naut_bc_free(doc); + return NAUT_ERR_PROTO; + } + out->metadata_size = v; + } + if (get_u32(root, "reqq", &v)) out->reqq = v; + if (get_u32(root, "p", &v) && v <= UINT16_MAX) out->port = (uint16_t)v; + naut_bc_free(doc); + return NAUT_OK; +} + +naut_err naut_metadata_build(uint8_t ext_id, naut_metadata_type type, + uint32_t piece, uint32_t total_size, + const void *data, size_t data_len, + uint8_t **out, size_t *out_len) { + if (!ext_id || type > NAUT_METADATA_REJECT || + (type == NAUT_METADATA_DATA && (!data || total_size == 0)) || + (type != NAUT_METADATA_DATA && data_len != 0)) + return NAUT_ERR_INVAL; + naut_bc_writer w; + naut_bc_w_init(&w); + naut_bc_w_dict_begin(&w); + naut_bc_w_cstr(&w, "msg_type"); naut_bc_w_int(&w, type); + naut_bc_w_cstr(&w, "piece"); naut_bc_w_int(&w, piece); + if (type == NAUT_METADATA_DATA) { + naut_bc_w_cstr(&w, "total_size"); naut_bc_w_int(&w, total_size); + } + naut_bc_w_end(&w); + if (w.err != NAUT_OK) { + naut_err e = w.err; + naut_bc_w_free(&w); + return e; + } + if (data_len > SIZE_MAX - w.len) { + naut_bc_w_free(&w); + return NAUT_ERR_RANGE; + } + size_t payload_len = w.len + data_len; + uint8_t *payload = malloc(payload_len ? payload_len : 1); + if (!payload) { + naut_bc_w_free(&w); + return NAUT_ERR_NOMEM; + } + memcpy(payload, w.buf, w.len); + if (data_len) memcpy(payload + w.len, data, data_len); + naut_err e = frame(ext_id, payload, payload_len, out, out_len); + free(payload); + naut_bc_w_free(&w); + return e; +} + +naut_err naut_metadata_parse(const uint8_t *payload, size_t len, + naut_metadata_msg *out) { + if (!payload || !out) return NAUT_ERR_INVAL; + memset(out, 0, sizeof(*out)); + naut_bc_doc *doc = NULL; + size_t used = 0; + naut_err e = naut_bc_parse_prefix(payload, len, &doc, &used); + if (e != NAUT_OK) return e; + const naut_bc *root = naut_bc_root(doc); + uint32_t type, piece; + if (!root || root->type != NAUT_BC_DICT || + !get_u32(root, "msg_type", &type) || type > NAUT_METADATA_REJECT || + !get_u32(root, "piece", &piece)) { + naut_bc_free(doc); + return NAUT_ERR_PROTO; + } + out->type = (naut_metadata_type)type; + out->piece = piece; + if (out->type == NAUT_METADATA_DATA) { + if (!get_u32(root, "total_size", &out->total_size) || + out->total_size == 0 || out->total_size > NAUT_METADATA_MAX) { + naut_bc_free(doc); + return NAUT_ERR_PROTO; + } + out->data = payload + used; + out->data_len = len - used; + uint32_t pieces = (out->total_size + NAUT_METADATA_BLOCK - 1) / + NAUT_METADATA_BLOCK; + if (piece >= pieces || + out->data_len != (piece + 1 < pieces + ? NAUT_METADATA_BLOCK + : out->total_size - (size_t)piece * NAUT_METADATA_BLOCK)) { + naut_bc_free(doc); + return NAUT_ERR_PROTO; + } + } else if (used != len) { + naut_bc_free(doc); + return NAUT_ERR_PROTO; + } + naut_bc_free(doc); + return NAUT_OK; +} + +static naut_err parse_compact(const uint8_t *p, size_t n, + naut_peer_addr **out, size_t *count) { + if (n % 6 != 0 || n / 6 > 200) return NAUT_ERR_PROTO; + size_t num = n / 6; + naut_peer_addr *v = calloc(num ? num : 1, sizeof(*v)); + if (!v) return NAUT_ERR_NOMEM; + for (size_t i = 0; i < num; i++) { + memcpy(v[i].ip, p + i * 6, 4); + v[i].port = ((uint16_t)p[i * 6 + 4] << 8) | p[i * 6 + 5]; + if (v[i].port == 0) { + free(v); + return NAUT_ERR_PROTO; + } + } + *out = v; + *count = num; + return NAUT_OK; +} + +naut_err naut_pex_parse(const uint8_t *payload, size_t len, naut_pex_msg *out) { + if (!payload || !out) return NAUT_ERR_INVAL; + memset(out, 0, sizeof(*out)); + naut_bc_doc *doc = NULL; + naut_err e = naut_bc_parse(payload, len, &doc); + if (e != NAUT_OK) return e; + const naut_bc *root = naut_bc_root(doc); + const uint8_t *p; size_t n; + if (!root || root->type != NAUT_BC_DICT) { + e = NAUT_ERR_PROTO; + goto done; + } + if (naut_bc_get_str(naut_bc_dict_get(root, "added"), &p, &n)) { + e = parse_compact(p, n, &out->added, &out->num_added); + if (e != NAUT_OK) goto done; + } + const uint8_t *flags; size_t flags_n; + if (naut_bc_get_str(naut_bc_dict_get(root, "added.f"), &flags, &flags_n)) { + if (flags_n != out->num_added) { e = NAUT_ERR_PROTO; goto done; } + out->added_flags = malloc(flags_n ? flags_n : 1); + if (!out->added_flags) { e = NAUT_ERR_NOMEM; goto done; } + memcpy(out->added_flags, flags, flags_n); + } + if (naut_bc_get_str(naut_bc_dict_get(root, "dropped"), &p, &n)) { + e = parse_compact(p, n, &out->dropped, &out->num_dropped); + if (e != NAUT_OK) goto done; + } + if (out->num_added == 0 && out->num_dropped == 0) e = NAUT_ERR_PROTO; +done: + naut_bc_free(doc); + if (e != NAUT_OK) naut_pex_free(out); + return e; +} + +naut_err naut_pex_build(uint8_t ext_id, const naut_pex_msg *msg, + uint8_t **out, size_t *out_len) { + if (!ext_id || !msg || (msg->num_added == 0 && msg->num_dropped == 0) || + msg->num_added > 200 || msg->num_dropped > 200) + return NAUT_ERR_INVAL; + naut_bc_writer w; + naut_bc_w_init(&w); + naut_bc_w_dict_begin(&w); + if (msg->num_added) { + uint8_t compact[200 * 6]; + for (size_t i = 0; i < msg->num_added; i++) { + memcpy(compact + i * 6, msg->added[i].ip, 4); + compact[i * 6 + 4] = (uint8_t)(msg->added[i].port >> 8); + compact[i * 6 + 5] = (uint8_t)msg->added[i].port; + } + naut_bc_w_cstr(&w, "added"); + naut_bc_w_bytes(&w, compact, msg->num_added * 6); + if (msg->added_flags) { + naut_bc_w_cstr(&w, "added.f"); + naut_bc_w_bytes(&w, msg->added_flags, msg->num_added); + } + } + if (msg->num_dropped) { + uint8_t compact[200 * 6]; + for (size_t i = 0; i < msg->num_dropped; i++) { + memcpy(compact + i * 6, msg->dropped[i].ip, 4); + compact[i * 6 + 4] = (uint8_t)(msg->dropped[i].port >> 8); + compact[i * 6 + 5] = (uint8_t)msg->dropped[i].port; + } + naut_bc_w_cstr(&w, "dropped"); + naut_bc_w_bytes(&w, compact, msg->num_dropped * 6); + } + naut_bc_w_end(&w); + if (w.err != NAUT_OK) { + naut_err e = w.err; + naut_bc_w_free(&w); + return e; + } + naut_err e = frame(ext_id, w.buf, w.len, out, out_len); + naut_bc_w_free(&w); + return e; +} + +void naut_pex_free(naut_pex_msg *msg) { + if (!msg) return; + free(msg->added); + free(msg->added_flags); + free(msg->dropped); + memset(msg, 0, sizeof(*msg)); +} diff --git a/src/peer/metadata.c b/src/peer/metadata.c new file mode 100644 index 0000000..3d8c065 --- /dev/null +++ b/src/peer/metadata.c @@ -0,0 +1,235 @@ +#include "naut/extension.h" +#include "naut/hash.h" +#include "naut/peer.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define EXT_RESERVED 0x0000000000100000ULL + +static bool send_all(int fd, const void *data, size_t len) { + const uint8_t *p = data; + while (len) { + ssize_t n = send(fd, p, len, MSG_NOSIGNAL); + if (n < 0) { + if (errno == EINTR) continue; + return false; + } + if (n == 0) return false; + p += n; + len -= (size_t)n; + } + return true; +} + +static bool recv_exact(int fd, void *data, size_t len) { + uint8_t *p = data; + while (len) { + ssize_t n = recv(fd, p, len, 0); + if (n < 0) { + if (errno == EINTR) continue; + return false; + } + if (n == 0) return false; + p += n; + len -= (size_t)n; + } + return true; +} + +static int connect_peer(const naut_peer_addr *peer) { + int fd = socket(AF_INET, SOCK_STREAM, 0); + if (fd < 0) return -1; + struct sockaddr_in addr; + memset(&addr, 0, sizeof addr); + addr.sin_family = AF_INET; + addr.sin_port = htons(peer->port); + memcpy(&addr.sin_addr, peer->ip, sizeof peer->ip); + if (connect(fd, (struct sockaddr *)&addr, sizeof addr) != 0) { + close(fd); + return -1; + } + int one = 1; + setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &one, sizeof one); + struct timeval timeout = { .tv_sec = 10 }; + setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof timeout); + setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &timeout, sizeof timeout); + return fd; +} + +naut_err naut_metadata_fetch(const naut_peer_addr *peer, + const uint8_t info_hash[20], + const uint8_t peer_id[20], + uint8_t **info, size_t *info_len) { + if (!peer || !info_hash || !peer_id || !info || !info_len) + return NAUT_ERR_INVAL; + *info = NULL; + *info_len = 0; + + int fd = connect_peer(peer); + if (fd < 0) return NAUT_ERR_IO; + naut_err result = NAUT_ERR_IO; + uint8_t *metadata = NULL, *frame = NULL, *buffer = NULL; + bool *received = NULL; + + uint8_t handshake[NAUT_HANDSHAKE_LEN]; + naut_peer_handshake_build(handshake, info_hash, peer_id, EXT_RESERVED); + if (!send_all(fd, handshake, sizeof handshake) || + !recv_exact(fd, handshake, sizeof handshake)) + goto done; + uint8_t remote_hash[20], remote_id[20]; + uint64_t reserved = 0; + if (!naut_peer_handshake_parse(handshake, remote_hash, remote_id, + &reserved) || + memcmp(remote_hash, info_hash, 20) != 0 || + (reserved & EXT_RESERVED) == 0) { + result = NAUT_ERR_PROTO; + goto done; + } + + size_t frame_len = 0; + result = naut_ext_build_handshake(NAUT_EXT_UT_METADATA, NAUT_EXT_UT_PEX, + 0, 0, &frame, &frame_len); + if (result != NAUT_OK || !send_all(fd, frame, frame_len)) { + result = NAUT_ERR_IO; + goto done; + } + free(frame); + frame = NULL; + + size_t cap = 128 * 1024, len = 0; + buffer = malloc(cap); + if (!buffer) { + result = NAUT_ERR_NOMEM; + goto done; + } + naut_ext_handshake remote_ext = {0}; + uint32_t piece_count = 0, received_count = 0; + + while (!metadata || received_count < piece_count) { + if (len == cap) { + if (cap >= NAUT_METADATA_MAX + (1u << 20)) { + result = NAUT_ERR_PROTO; + goto done; + } + size_t next_cap = cap * 2; + uint8_t *next = realloc(buffer, next_cap); + if (!next) { + result = NAUT_ERR_NOMEM; + goto done; + } + buffer = next; + cap = next_cap; + } + ssize_t n = recv(fd, buffer + len, cap - len, 0); + if (n < 0) { + if (errno == EINTR) continue; + result = NAUT_ERR_IO; + goto done; + } + if (n == 0) { + result = NAUT_ERR_IO; + goto done; + } + len += (size_t)n; + + size_t pos = 0; + for (;;) { + naut_msg msg; + int consumed = naut_peer_msg_parse(buffer + pos, len - pos, &msg); + if (consumed == 0) break; + if (consumed < 0) { + result = NAUT_ERR_PROTO; + goto done; + } + pos += (size_t)consumed; + if (msg.type != NAUT_MSG_EXTENDED || msg.payload_len < 1) + continue; + + uint8_t ext_id = msg.payload[0]; + const uint8_t *payload = msg.payload + 1; + size_t payload_len = msg.payload_len - 1; + if (ext_id == 0) { + result = naut_ext_parse_handshake(payload, payload_len, + &remote_ext); + if (result != NAUT_OK || remote_ext.ut_metadata == 0 || + remote_ext.metadata_size == 0) { + result = NAUT_ERR_PROTO; + goto done; + } + if (!metadata) { + metadata = malloc(remote_ext.metadata_size); + piece_count = + (remote_ext.metadata_size + NAUT_METADATA_BLOCK - 1) / + NAUT_METADATA_BLOCK; + received = calloc(piece_count, sizeof(*received)); + if (!metadata || !received) { + result = NAUT_ERR_NOMEM; + goto done; + } + for (uint32_t piece = 0; piece < piece_count; piece++) { + result = naut_metadata_build( + remote_ext.ut_metadata, NAUT_METADATA_REQUEST, + piece, 0, NULL, 0, &frame, &frame_len); + if (result != NAUT_OK || + !send_all(fd, frame, frame_len)) { + result = NAUT_ERR_IO; + goto done; + } + free(frame); + frame = NULL; + } + } + } else if (metadata && + (ext_id == NAUT_EXT_UT_METADATA || + ext_id == remote_ext.ut_metadata)) { + naut_metadata_msg metadata_msg; + result = naut_metadata_parse(payload, payload_len, + &metadata_msg); + if (result != NAUT_OK || + metadata_msg.type == NAUT_METADATA_REJECT || + metadata_msg.total_size != remote_ext.metadata_size || + metadata_msg.piece >= piece_count) { + result = NAUT_ERR_PROTO; + goto done; + } + if (metadata_msg.type == NAUT_METADATA_DATA && + !received[metadata_msg.piece]) { + memcpy(metadata + + (size_t)metadata_msg.piece * NAUT_METADATA_BLOCK, + metadata_msg.data, metadata_msg.data_len); + received[metadata_msg.piece] = true; + received_count++; + } + } + } + memmove(buffer, buffer + pos, len - pos); + len -= pos; + } + + uint8_t digest[20]; + naut_sha1(metadata, remote_ext.metadata_size, digest); + if (memcmp(digest, info_hash, 20) != 0) { + result = NAUT_ERR_PROTO; + goto done; + } + *info = metadata; + *info_len = remote_ext.metadata_size; + metadata = NULL; + result = NAUT_OK; + +done: + close(fd); + free(metadata); + free(received); + free(frame); + free(buffer); + return result; +} diff --git a/src/peer/mse.c b/src/peer/mse.c new file mode 100644 index 0000000..44df867 --- /dev/null +++ b/src/peer/mse.c @@ -0,0 +1,466 @@ +#include "naut/mse.h" +#include "naut/hash.h" + +#include +#include +#include +#include +#include +#include + +#define MSE_PAD_MAX 512 +#define MSE_CRYPTO_RC4 2u + +static const char DH_PRIME_HEX[] = + "FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC" + "74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF2" + "5F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A63A3621000000" + "0000090563"; + +static void wr16(uint8_t *p, uint16_t value) { + p[0] = (uint8_t)(value >> 8); + p[1] = (uint8_t)value; +} + +static void wr32(uint8_t *p, uint32_t value) { + p[0] = (uint8_t)(value >> 24); + p[1] = (uint8_t)(value >> 16); + p[2] = (uint8_t)(value >> 8); + p[3] = (uint8_t)value; +} + +static uint16_t rd16(const uint8_t *p) { + return ((uint16_t)p[0] << 8) | p[1]; +} + +static uint32_t rd32(const uint8_t *p) { + return ((uint32_t)p[0] << 24) | ((uint32_t)p[1] << 16) | + ((uint32_t)p[2] << 8) | p[3]; +} + +static void hash_parts(const char label[4], + const uint8_t *first, size_t first_len, + const uint8_t *second, size_t second_len, + uint8_t out[20]) { + naut_sha1_ctx sha; + naut_sha1_init(&sha); + naut_sha1_update(&sha, label, 4); + naut_sha1_update(&sha, first, first_len); + if (second && second_len) naut_sha1_update(&sha, second, second_len); + naut_sha1_final(&sha, out); +} + +static void init_rc4(const uint8_t secret[NAUT_MSE_DH_LEN], + const uint8_t info_hash[20], + naut_mse_stream *stream) { + uint8_t key_a[20], key_b[20]; + hash_parts("keyA", secret, NAUT_MSE_DH_LEN, info_hash, 20, key_a); + hash_parts("keyB", secret, NAUT_MSE_DH_LEN, info_hash, 20, key_b); + naut_rc4_init(&stream->send, key_a, sizeof key_a, 1024); + naut_rc4_init(&stream->recv, key_b, sizeof key_b, 1024); + memset(key_a, 0, sizeof key_a); + memset(key_b, 0, sizeof key_b); +} + +/* ---- sans-IO handshake state machine ------------------------------------- */ + +enum { + PH_RECV_PUBKEY, /* waiting for the peer's 96-byte DH public key */ + PH_SYNC_VC, /* scanning past PadB for the encrypted VC */ + PH_RECV_SELECT, /* crypto_select + len(PadD) */ + PH_RECV_PAD, /* PadD bytes (discarded) */ + PH_RECV_HS, /* the peer's encrypted BitTorrent handshake */ +}; + +struct naut_mse_handshake { + int phase; + naut_err err; + bool done; + + uint8_t info_hash[20]; + uint8_t peer_id[NAUT_PEERID_LEN]; + uint64_t reserved; + + /* DH state retained until the shared secret is computed. */ + BN_CTX *ctx; + BIGNUM *prime; + BIGNUM *priv; + + naut_mse_stream stream; + uint8_t expected_vc[8]; + size_t vc_scanned; + size_t pad_remaining; + uint8_t remote_handshake[NAUT_HANDSHAKE_LEN]; + + uint8_t out[256]; + size_t out_len, out_off; + + uint8_t in[1024]; + size_t in_len; +}; + +static void dh_free(naut_mse_handshake *h) { + BN_CTX_free(h->ctx); h->ctx = NULL; + BN_free(h->prime); h->prime = NULL; + BN_clear_free(h->priv); h->priv = NULL; +} + +/* Generate our private key and public value, writing the 96-byte public key + * into the outgoing buffer. Retains prime/priv/ctx for dh_complete(). */ +static naut_err dh_begin(naut_mse_handshake *h) { + naut_err result = NAUT_ERR_IO; + BIGNUM *generator = BN_new(); + BIGNUM *local = BN_new(); + h->ctx = BN_CTX_new(); + h->priv = BN_new(); + if (!generator || !local || !h->ctx || !h->priv || + !BN_hex2bn(&h->prime, DH_PRIME_HEX) || !BN_set_word(generator, 2)) + goto done; + do { + if (!BN_rand_range(h->priv, h->prime)) goto done; + } while (BN_cmp(h->priv, generator) < 0); + if (!BN_mod_exp(local, generator, h->priv, h->prime, h->ctx) || + BN_bn2binpad(local, h->out, NAUT_MSE_DH_LEN) != NAUT_MSE_DH_LEN) + goto done; + h->out_len = NAUT_MSE_DH_LEN; + h->out_off = 0; + result = NAUT_OK; +done: + BN_free(generator); + BN_free(local); + if (result != NAUT_OK) dh_free(h); + return result; +} + +/* Validate the peer's public key and derive the shared secret. */ +static naut_err dh_complete(naut_mse_handshake *h, const uint8_t remote_bytes[96], + uint8_t secret[NAUT_MSE_DH_LEN]) { + naut_err result = NAUT_ERR_IO; + BIGNUM *remote = BN_new(); + BIGNUM *shared = BN_new(); + BIGNUM *limit = BN_new(); + BIGNUM *two = BN_new(); + if (!remote || !shared || !limit || !two || + !BN_bin2bn(remote_bytes, NAUT_MSE_DH_LEN, remote) || + !BN_set_word(two, 2) || !BN_copy(limit, h->prime) || + !BN_sub_word(limit, 1)) + goto done; + if (BN_cmp(remote, two) < 0 || BN_cmp(remote, limit) >= 0) { + result = NAUT_ERR_PROTO; + goto done; + } + if (!BN_mod_exp(shared, remote, h->priv, h->prime, h->ctx) || + BN_bn2binpad(shared, secret, NAUT_MSE_DH_LEN) != NAUT_MSE_DH_LEN) + goto done; + result = NAUT_OK; +done: + BN_free(remote); + BN_clear_free(shared); + BN_free(limit); + BN_free(two); + return result; +} + +naut_mse_handshake *naut_mse_handshake_begin( + const uint8_t info_hash[20], + const uint8_t peer_id[NAUT_PEERID_LEN], + uint64_t reserved) { + if (!info_hash || !peer_id) return NULL; + naut_mse_handshake *h = calloc(1, sizeof(*h)); + if (!h) return NULL; + memcpy(h->info_hash, info_hash, 20); + memcpy(h->peer_id, peer_id, NAUT_PEERID_LEN); + h->reserved = reserved; + h->phase = PH_RECV_PUBKEY; + if (dh_begin(h) != NAUT_OK) { + naut_mse_handshake_free(h); + return NULL; + } + return h; +} + +void naut_mse_handshake_free(naut_mse_handshake *h) { + if (!h) return; + dh_free(h); + /* keystream state is sensitive; scrub before release */ + memset(h, 0, sizeof(*h)); + free(h); +} + +static void consume(naut_mse_handshake *h, size_t n) { + memmove(h->in, h->in + n, h->in_len - n); + h->in_len -= n; +} + +/* Build req1/req2 + encrypted offer (VC, crypto_provide, PadC, IA) into out. */ +static void build_request(naut_mse_handshake *h, const uint8_t secret[96]) { + uint8_t req1[20], req2[20], req3[20]; + hash_parts("req1", secret, NAUT_MSE_DH_LEN, NULL, 0, req1); + hash_parts("req2", h->info_hash, 20, NULL, 0, req2); + hash_parts("req3", secret, NAUT_MSE_DH_LEN, NULL, 0, req3); + for (size_t i = 0; i < sizeof req2; i++) req2[i] ^= req3[i]; + + init_rc4(secret, h->info_hash, &h->stream); + + uint8_t *p = h->out; + memcpy(p, req1, 20); + memcpy(p + 20, req2, 20); + p += 40; + + uint8_t *offer = p; /* VC(8) crypto_provide(4) padlen(2) ialen(2) IA */ + memset(offer, 0, 8); + wr32(offer + 8, MSE_CRYPTO_RC4); + wr16(offer + 12, 0); + wr16(offer + 14, NAUT_HANDSHAKE_LEN); + naut_peer_handshake_build(offer + 16, h->info_hash, h->peer_id, h->reserved); + size_t offer_len = 16 + NAUT_HANDSHAKE_LEN; + naut_rc4_xor(&h->stream.send, offer, offer_len); + + h->out_len = 40 + offer_len; + h->out_off = 0; + + /* expected_vc = our recv keystream applied to 8 zero bytes at position 0, + * without advancing the real recv state (we resync on it). */ + naut_rc4 probe = h->stream.recv; + uint8_t vc[8] = {0}; + naut_rc4_xor(&probe, vc, sizeof vc); + memcpy(h->expected_vc, vc, sizeof vc); + h->vc_scanned = 0; +} + +static void advance(naut_mse_handshake *h) { + for (;;) { + switch (h->phase) { + case PH_RECV_PUBKEY: { + if (h->in_len < NAUT_MSE_DH_LEN) return; + uint8_t secret[NAUT_MSE_DH_LEN]; + naut_err e = dh_complete(h, h->in, secret); + if (e != NAUT_OK) { h->err = e; return; } + consume(h, NAUT_MSE_DH_LEN); + dh_free(h); /* DH no longer needed */ + build_request(h, secret); + memset(secret, 0, sizeof secret); + h->phase = PH_SYNC_VC; + return; /* out now holds req+offer: NEED_WRITE */ + } + case PH_SYNC_VC: { + while (h->in_len >= sizeof h->expected_vc) { + if (memcmp(h->in, h->expected_vc, sizeof h->expected_vc) == 0) { + uint8_t vc[8]; + memcpy(vc, h->in, sizeof vc); + naut_rc4_xor(&h->stream.recv, vc, sizeof vc); + static const uint8_t zero8[8] = {0}; + if (memcmp(vc, zero8, sizeof vc) != 0) { + h->err = NAUT_ERR_PROTO; + return; + } + consume(h, sizeof vc); + h->phase = PH_RECV_SELECT; + break; + } + consume(h, 1); + if (++h->vc_scanned > MSE_PAD_MAX) { + h->err = NAUT_ERR_PROTO; + return; + } + } + if (h->phase == PH_SYNC_VC) return; /* need more bytes */ + continue; + } + case PH_RECV_SELECT: { + if (h->in_len < 6) return; + uint8_t hdr[6]; + memcpy(hdr, h->in, sizeof hdr); + naut_rc4_xor(&h->stream.recv, hdr, sizeof hdr); + consume(h, sizeof hdr); + if (rd32(hdr) != MSE_CRYPTO_RC4) { h->err = NAUT_ERR_PROTO; return; } + h->pad_remaining = rd16(hdr + 4); + if (h->pad_remaining > MSE_PAD_MAX) { h->err = NAUT_ERR_PROTO; return; } + h->phase = PH_RECV_PAD; + continue; + } + case PH_RECV_PAD: { + if (h->pad_remaining > 0) { + size_t n = h->pad_remaining < h->in_len ? h->pad_remaining + : h->in_len; + if (n == 0) return; + naut_rc4_xor(&h->stream.recv, h->in, n); /* advance keystream */ + consume(h, n); + h->pad_remaining -= n; + if (h->pad_remaining > 0) return; + } + h->phase = PH_RECV_HS; + continue; + } + case PH_RECV_HS: { + if (h->in_len < NAUT_HANDSHAKE_LEN) return; + memcpy(h->remote_handshake, h->in, NAUT_HANDSHAKE_LEN); + naut_rc4_xor(&h->stream.recv, h->remote_handshake, NAUT_HANDSHAKE_LEN); + consume(h, NAUT_HANDSHAKE_LEN); + uint8_t remote_hash[20], remote_id[20]; + if (!naut_peer_handshake_parse(h->remote_handshake, remote_hash, + remote_id, NULL) || + memcmp(remote_hash, h->info_hash, 20) != 0) { + h->err = NAUT_ERR_PROTO; + return; + } + h->stream.active = true; + h->done = true; + return; + } + default: + h->err = NAUT_ERR_PROTO; + return; + } + } +} + +naut_mse_hs_status naut_mse_handshake_status(const naut_mse_handshake *h) { + if (!h || h->err != NAUT_OK) return NAUT_MSE_HS_ERROR; + if (h->done) return NAUT_MSE_HS_DONE; + if (h->out_off < h->out_len) return NAUT_MSE_HS_NEED_WRITE; + return NAUT_MSE_HS_NEED_READ; +} + +size_t naut_mse_handshake_pull(naut_mse_handshake *h, uint8_t *buf, size_t cap) { + if (!h || !buf) return 0; + size_t avail = h->out_len - h->out_off; + size_t n = avail < cap ? avail : cap; + if (n) { + memcpy(buf, h->out + h->out_off, n); + h->out_off += n; + if (h->out_off == h->out_len) h->out_len = h->out_off = 0; + } + return n; +} + +naut_mse_hs_status naut_mse_handshake_feed(naut_mse_handshake *h, + const uint8_t *data, size_t len, + size_t *consumed) { + if (consumed) *consumed = 0; + if (!h) return NAUT_MSE_HS_ERROR; + if (h->err == NAUT_OK && !h->done && data && len) { + size_t space = sizeof h->in - h->in_len; + size_t take = len < space ? len : space; + memcpy(h->in + h->in_len, data, take); + h->in_len += take; + if (consumed) *consumed = take; + advance(h); + } + return naut_mse_handshake_status(h); +} + +naut_err naut_mse_handshake_finish(naut_mse_handshake *h, + naut_mse_stream *stream, + uint8_t remote_handshake[NAUT_HANDSHAKE_LEN]) { + if (!h || !stream || !remote_handshake) return NAUT_ERR_INVAL; + if (h->err != NAUT_OK) return h->err; + if (!h->done) return NAUT_ERR_AGAIN; + *stream = h->stream; + memcpy(remote_handshake, h->remote_handshake, NAUT_HANDSHAKE_LEN); + return NAUT_OK; +} + +/* ---- blocking I/O helpers + convenience wrapper -------------------------- */ + +static bool raw_send_all(int fd, const void *data, size_t len) { + const uint8_t *p = data; + while (len) { + ssize_t n = send(fd, p, len, MSG_NOSIGNAL); + if (n < 0) { + if (errno == EINTR) continue; + return false; + } + if (n == 0) return false; + p += n; + len -= (size_t)n; + } + return true; +} + +static bool raw_recv_exact(int fd, void *data, size_t len) { + uint8_t *p = data; + while (len) { + ssize_t n = recv(fd, p, len, 0); + if (n < 0) { + if (errno == EINTR) continue; + return false; + } + if (n == 0) return false; + p += n; + len -= (size_t)n; + } + return true; +} + +naut_err naut_mse_client_handshake( + int fd, + const uint8_t info_hash[20], + const uint8_t peer_id[NAUT_PEERID_LEN], + uint64_t reserved, + naut_mse_stream *stream, + uint8_t remote_handshake[NAUT_HANDSHAKE_LEN]) { + if (fd < 0 || !info_hash || !peer_id || !stream || !remote_handshake) + return NAUT_ERR_INVAL; + memset(stream, 0, sizeof(*stream)); + + naut_mse_handshake *h = + naut_mse_handshake_begin(info_hash, peer_id, reserved); + if (!h) return NAUT_ERR_NOMEM; + + naut_err rc = NAUT_ERR_PROTO; + for (;;) { + naut_mse_hs_status st = naut_mse_handshake_status(h); + if (st == NAUT_MSE_HS_NEED_WRITE) { + uint8_t buf[256]; + size_t n; + bool ok = true; + while ((n = naut_mse_handshake_pull(h, buf, sizeof buf)) > 0) + if (!raw_send_all(fd, buf, n)) { ok = false; break; } + if (!ok) { rc = NAUT_ERR_IO; break; } + } else if (st == NAUT_MSE_HS_NEED_READ) { + /* One byte at a time: the handshake is tiny and one-shot, and this + * keeps the wrapper from over-reading into the payload stream. */ + uint8_t byte; + if (!raw_recv_exact(fd, &byte, 1)) { rc = NAUT_ERR_IO; break; } + naut_mse_handshake_feed(h, &byte, 1, NULL); + } else if (st == NAUT_MSE_HS_DONE) { + rc = naut_mse_handshake_finish(h, stream, remote_handshake); + break; + } else { + rc = h->err != NAUT_OK ? h->err : NAUT_ERR_PROTO; + break; + } + } + naut_mse_handshake_free(h); + return rc; +} + +/* ---- post-handshake stream I/O ------------------------------------------- */ + +bool naut_mse_send_all(int fd, naut_mse_stream *stream, + const void *data, size_t len) { + if (!stream || !stream->active) return raw_send_all(fd, data, len); + const uint8_t *p = data; + uint8_t block[16 * 1024]; + while (len) { + size_t n = len < sizeof block ? len : sizeof block; + memcpy(block, p, n); + naut_rc4_xor(&stream->send, block, n); + if (!raw_send_all(fd, block, n)) return false; + p += n; + len -= n; + } + return true; +} + +ssize_t naut_mse_recv(int fd, naut_mse_stream *stream, + void *data, size_t len) { + ssize_t n; + do { + n = recv(fd, data, len, 0); + } while (n < 0 && errno == EINTR); + if (n > 0 && stream && stream->active) + naut_rc4_xor(&stream->recv, data, (size_t)n); + return n; +} diff --git a/src/peer/pipeline.c b/src/peer/pipeline.c new file mode 100644 index 0000000..6b0989b --- /dev/null +++ b/src/peer/pipeline.c @@ -0,0 +1,64 @@ +#include "naut/pipeline.h" + +#include + +static uint32_t clamp_depth(const naut_pipeline *p, uint32_t depth) { + if (depth < p->min_depth) return p->min_depth; + if (depth > p->max_depth) return p->max_depth; + return depth; +} + +void naut_pipeline_init(naut_pipeline *p, uint32_t block_size, + uint32_t min_depth, uint32_t max_depth, + uint32_t initial_depth) { + if (!p) return; + if (block_size == 0) block_size = NAUT_BLOCK; + if (min_depth == 0) min_depth = 1; + if (max_depth < min_depth) max_depth = min_depth; + p->rtt_seconds = 0; + p->bytes_per_second = 0; + p->last_sample_at = 0; + p->min_depth = min_depth; + p->max_depth = max_depth; + p->block_size = block_size; + p->depth = clamp_depth(p, initial_depth); +} + +void naut_pipeline_on_block(naut_pipeline *p, uint32_t bytes, + double sent_at, double received_at) { + if (!p || bytes == 0 || sent_at <= 0 || received_at <= sent_at) return; + double rtt = received_at - sent_at; + if (rtt > 60.0) return; + + if (p->rtt_seconds == 0) p->rtt_seconds = rtt; + else p->rtt_seconds = p->rtt_seconds * 0.875 + rtt * 0.125; + + double interval = p->last_sample_at > 0 + ? received_at - p->last_sample_at : rtt; + if (interval <= 0) interval = rtt; + double rate = bytes / interval; + if (p->bytes_per_second == 0) p->bytes_per_second = rate; + else p->bytes_per_second = p->bytes_per_second * 0.8 + rate * 0.2; + p->last_sample_at = received_at; + + double blocks = (2.0 * p->bytes_per_second * p->rtt_seconds) / + p->block_size; + uint32_t target = blocks >= UINT32_MAX ? UINT32_MAX : + (uint32_t)ceil(blocks); + target = clamp_depth(p, target); + + /* Grow quickly enough to fill a fast path; shrink one eighth at a time so + * transient delayed samples do not collapse the pipe. */ + if (target > p->depth) { + uint32_t step = p->depth / 4 + 1; + p->depth = clamp_depth(p, NAUT_MIN(target, p->depth + step)); + } else if (target < p->depth) { + uint32_t step = p->depth / 8 + 1; + p->depth = clamp_depth(p, target > p->depth - step + ? target : p->depth - step); + } +} + +uint32_t naut_pipeline_depth(const naut_pipeline *p) { + return p ? p->depth : 0; +} diff --git a/src/peer/wire.c b/src/peer/wire.c new file mode 100644 index 0000000..46d86d2 --- /dev/null +++ b/src/peer/wire.c @@ -0,0 +1,113 @@ +#include "naut/peer.h" +#include + +static const char PSTR[] = "BitTorrent protocol"; /* 19 bytes */ +#define PSTRLEN 19 + +static inline void wr32(uint8_t *p, uint32_t v) { + p[0] = (uint8_t)(v >> 24); p[1] = (uint8_t)(v >> 16); + p[2] = (uint8_t)(v >> 8); p[3] = (uint8_t)v; +} +static inline uint32_t rd32(const uint8_t *p) { + return ((uint32_t)p[0] << 24) | ((uint32_t)p[1] << 16) | + ((uint32_t)p[2] << 8) | (uint32_t)p[3]; +} + +void naut_peer_handshake_build(uint8_t out[NAUT_HANDSHAKE_LEN], + const uint8_t infohash[20], + const uint8_t peerid[NAUT_PEERID_LEN], + uint64_t reserved) { + out[0] = PSTRLEN; + memcpy(out + 1, PSTR, PSTRLEN); + for (int i = 0; i < 8; i++) out[20 + i] = (uint8_t)(reserved >> (56 - i*8)); + memcpy(out + 28, infohash, 20); + memcpy(out + 48, peerid, 20); +} + +bool naut_peer_handshake_parse(const uint8_t in[NAUT_HANDSHAKE_LEN], + uint8_t infohash[20], + uint8_t peerid[NAUT_PEERID_LEN], + uint64_t *reserved) { + if (in[0] != PSTRLEN || memcmp(in + 1, PSTR, PSTRLEN) != 0) return false; + if (reserved) { + uint64_t r = 0; + for (int i = 0; i < 8; i++) r = (r << 8) | in[20 + i]; + *reserved = r; + } + memcpy(infohash, in + 28, 20); + memcpy(peerid, in + 48, 20); + return true; +} + +int naut_peer_msg_parse(const uint8_t *buf, size_t len, naut_msg *out) { + if (len < 4) return 0; + uint32_t n = rd32(buf); + if (n == 0) { out->type = NAUT_MSG_KEEPALIVE; return 4; } /* keep-alive */ + if (n > NAUT_MSG_MAX) return NAUT_ERR_PROTO; + if (len < 4 + (size_t)n) return 0; /* need more */ + + const uint8_t *p = buf + 4; + uint8_t id = p[0]; + const uint8_t *body = p + 1; + uint32_t blen = n - 1; + memset(out, 0, sizeof(*out)); + out->type = (naut_msg_type)id; + + switch (id) { + case NAUT_MSG_CHOKE: case NAUT_MSG_UNCHOKE: + case NAUT_MSG_INTERESTED: case NAUT_MSG_NOT_INTERESTED: + if (blen != 0) return NAUT_ERR_PROTO; + break; + case NAUT_MSG_HAVE: + if (blen != 4) return NAUT_ERR_PROTO; + out->index = rd32(body); + break; + case NAUT_MSG_BITFIELD: + out->payload = body; out->payload_len = blen; + break; + case NAUT_MSG_REQUEST: case NAUT_MSG_CANCEL: + if (blen != 12) return NAUT_ERR_PROTO; + out->index = rd32(body); out->begin = rd32(body + 4); out->length = rd32(body + 8); + break; + case NAUT_MSG_PIECE: + if (blen < 8) return NAUT_ERR_PROTO; + out->index = rd32(body); out->begin = rd32(body + 4); + out->payload = body + 8; out->payload_len = blen - 8; + out->length = blen - 8; + break; + case NAUT_MSG_PORT: + if (blen != 2) return NAUT_ERR_PROTO; + out->index = ((uint32_t)body[0] << 8) | body[1]; /* port in index */ + break; + default: + /* unknown/extended: surface type + raw payload, let caller decide */ + out->payload = body; out->payload_len = blen; + break; + } + return (int)(4 + n); +} + +size_t naut_peer_keepalive(uint8_t out[4]) { wr32(out, 0); return 4; } + +size_t naut_peer_msg_simple(uint8_t out[5], naut_msg_type t) { + wr32(out, 1); out[4] = (uint8_t)t; return 5; +} +size_t naut_peer_msg_have(uint8_t out[9], uint32_t index) { + wr32(out, 5); out[4] = NAUT_MSG_HAVE; wr32(out + 5, index); return 9; +} +size_t naut_peer_msg_request(uint8_t out[17], uint32_t index, uint32_t begin, uint32_t length) { + wr32(out, 13); out[4] = NAUT_MSG_REQUEST; + wr32(out + 5, index); wr32(out + 9, begin); wr32(out + 13, length); return 17; +} +size_t naut_peer_msg_cancel(uint8_t out[17], uint32_t index, uint32_t begin, uint32_t length) { + wr32(out, 13); out[4] = NAUT_MSG_CANCEL; + wr32(out + 5, index); wr32(out + 9, begin); wr32(out + 13, length); return 17; +} +size_t naut_peer_msg_piece_header(uint8_t out[13], uint32_t index, uint32_t begin, uint32_t block_len) { + wr32(out, 9 + block_len); out[4] = NAUT_MSG_PIECE; + wr32(out + 5, index); wr32(out + 9, begin); return 13; +} +size_t naut_peer_msg_bitfield(uint8_t *out, const uint8_t *bf, size_t nbytes) { + wr32(out, (uint32_t)(1 + nbytes)); out[4] = NAUT_MSG_BITFIELD; + memcpy(out + 5, bf, nbytes); return 5 + nbytes; +} diff --git a/src/piece/piece.c b/src/piece/piece.c new file mode 100644 index 0000000..fc59ce2 --- /dev/null +++ b/src/piece/piece.c @@ -0,0 +1,385 @@ +#include "naut/piece.h" +#include "naut/bitfield.h" +#include "naut/hash.h" +#include "naut/log.h" + +#include +#include + +#define BLK NAUT_BLOCK /* 16 KiB */ +#define ENDGAME_BLOCKS 8 /* switch to endgame when this few remain */ +#define ENDGAME_COPIES 2 /* at most two peers race a missing block */ + +/* per-piece in-progress state, lazily allocated and freed on completion */ +typedef struct { + uint8_t *recv_bits; /* received block bitmap */ + uint8_t *req_count; /* outstanding requests per block */ + uint8_t *buf; /* assembly buffer, piece_size bytes */ + uint32_t nblocks; + uint32_t nrecv; + bool verifying; + naut_job verify_job; + struct naut_download *download; + uint32_t piece; + uint8_t digest[NAUT_SHA1_LEN]; +} pstate; + +struct naut_download { + const naut_metainfo *mi; + naut_storage *st; + + uint32_t num_pieces; + uint64_t piece_len; + uint64_t total; + + naut_bitfield have; + uint32_t *avail; /* [num_pieces] swarm availability count */ + pstate **ps; /* [num_pieces] in-progress state or NULL */ + + uint32_t cur_piece; /* sequential cursor for next_request() */ + + uint64_t total_blocks, recv_blocks; + uint32_t pieces_done; + uint64_t bytes_done; + bool endgame; + naut_worker_pool *workers; + + size_t num_files; + uint32_t *file_first, *file_last, *file_remain; + bool *file_done; + naut_file_complete_cb file_cb; + void *file_cb_ctx; +}; + +static bool bget(const uint8_t *a, uint32_t i) { return (a[i>>3] >> (i&7)) & 1; } +static void bset(uint8_t *a, uint32_t i) { a[i>>3] |= (uint8_t)(1u << (i&7)); } + +static uint64_t piece_size(const naut_download *d, uint32_t p) { + if (p + 1 < d->num_pieces) return d->piece_len; + return d->total - (uint64_t)p * d->piece_len; +} +static uint32_t nblocks(const naut_download *d, uint32_t p) { + return (uint32_t)((piece_size(d, p) + BLK - 1) / BLK); +} +static uint32_t block_len(const naut_download *d, uint32_t p, uint32_t b) { + uint64_t rem = piece_size(d, p) - (uint64_t)b * BLK; + return rem < BLK ? (uint32_t)rem : BLK; +} + +static pstate *ensure_ps(naut_download *d, uint32_t p) { + if (d->ps[p]) return d->ps[p]; + pstate *s = calloc(1, sizeof(*s)); + if (!s) return NULL; + s->nblocks = nblocks(d, p); + s->download = d; + s->piece = p; + size_t bm = (s->nblocks + 7) / 8; + s->recv_bits = calloc(1, bm); + s->req_count = calloc(s->nblocks, sizeof(uint8_t)); + size_t alloc_size = + (size_t)NAUT_ALIGN_UP(piece_size(d, p), NAUT_PAGE); + s->buf = aligned_alloc(NAUT_PAGE, alloc_size); + if (!s->recv_bits || !s->req_count || !s->buf) { + free(s->recv_bits); free(s->req_count); free(s->buf); free(s); + return NULL; + } + d->ps[p] = s; + return s; +} +static void free_ps(naut_download *d, uint32_t p) { + pstate *s = d->ps[p]; + if (!s) return; + free(s->recv_bits); free(s->req_count); free(s->buf); free(s); + d->ps[p] = NULL; +} + +naut_download *naut_download_create(const naut_metainfo *mi, naut_storage *st) { + if (!mi->has_v1 || mi->num_pieces == 0 || mi->piece_length <= 0 || + mi->total_length <= 0) { + NAUT_ERROR("download: needs a v1/hybrid torrent (SHA-1 pieces)"); + return NULL; + } + uint64_t total = (uint64_t)mi->total_length; + uint64_t piece_len = (uint64_t)mi->piece_length; + uint64_t expected_pieces = 1 + (total - 1) / piece_len; + if (expected_pieces != mi->num_pieces || piece_len > UINT32_MAX * (uint64_t)BLK) { + NAUT_ERROR("download: inconsistent piece geometry"); + return NULL; + } + naut_download *d = calloc(1, sizeof(*d)); + if (!d) return NULL; + d->mi = mi; d->st = st; + d->num_pieces = mi->num_pieces; + d->piece_len = (uint64_t)mi->piece_length; + d->total = (uint64_t)mi->total_length; + d->avail = calloc(d->num_pieces, sizeof(uint32_t)); + d->ps = calloc(d->num_pieces, sizeof(pstate *)); + if (!d->avail || !d->ps || naut_bitfield_init(&d->have, d->num_pieces) != NAUT_OK) { + naut_download_destroy(d); return NULL; + } + for (uint32_t p = 0; p < d->num_pieces; p++) d->total_blocks += nblocks(d, p); + + d->num_files = mi->num_files; + d->file_first = calloc(mi->num_files, sizeof(uint32_t)); + d->file_last = calloc(mi->num_files, sizeof(uint32_t)); + d->file_remain = calloc(mi->num_files, sizeof(uint32_t)); + d->file_done = calloc(mi->num_files, sizeof(bool)); + if (mi->num_files && (!d->file_first || !d->file_last || !d->file_remain || !d->file_done)) { + naut_download_destroy(d); return NULL; + } + uint64_t off = 0; + for (size_t f = 0; f < mi->num_files; f++) { + uint64_t flen = (uint64_t)mi->files[f].length; + if (flen == 0) { d->file_first[f] = 1; d->file_last[f] = 0; d->file_done[f] = true; } + else { + d->file_first[f] = (uint32_t)(off / d->piece_len); + d->file_last[f] = (uint32_t)((off + flen - 1) / d->piece_len); + d->file_remain[f] = d->file_last[f] - d->file_first[f] + 1; + } + off += flen; + } + return d; +} + +void naut_download_destroy(naut_download *d) { + if (!d) return; + if (d->ps) for (uint32_t p = 0; p < d->num_pieces; p++) free_ps(d, p); + free(d->ps); free(d->avail); + free(d->file_first); free(d->file_last); free(d->file_remain); free(d->file_done); + naut_bitfield_free(&d->have); + free(d); +} + +void naut_download_set_worker_pool(naut_download *d, naut_worker_pool *pool) { + if (d) d->workers = pool; +} + +void naut_download_set_file_cb(naut_download *d, naut_file_complete_cb cb, void *ctx) { + d->file_cb = cb; d->file_cb_ctx = ctx; +} +bool naut_download_file_complete(const naut_download *d, uint32_t f) { + return f < d->num_files && d->file_done[f]; +} + +static void notify_files(naut_download *d, uint32_t p) { + size_t lo = 0, hi = d->num_files; + while (lo < hi) { size_t mid = (lo + hi) / 2; + if (d->file_last[mid] < p) lo = mid + 1; else hi = mid; } + for (size_t f = lo; f < d->num_files && d->file_first[f] <= p; f++) { + if (d->file_done[f]) continue; + if (--d->file_remain[f] == 0) { + d->file_done[f] = true; + if (d->file_cb) d->file_cb(d->file_cb_ctx, (uint32_t)f, d->mi->files[f].path); + } + } +} + +/* --- availability -------------------------------------------------------- */ +void naut_download_inc_avail(naut_download *d, uint32_t p) { + if (p < d->num_pieces) d->avail[p]++; +} +void naut_download_add_bitfield(naut_download *d, const naut_bitfield *bf) { + uint32_t limit = (uint32_t)NAUT_MIN((size_t)d->num_pieces, bf->nbits); + for (uint32_t p = 0; p < limit; p++) + if (naut_bitfield_test(bf, p)) d->avail[p]++; +} +void naut_download_remove_bitfield(naut_download *d, const naut_bitfield *bf) { + uint32_t limit = (uint32_t)NAUT_MIN((size_t)d->num_pieces, bf->nbits); + for (uint32_t p = 0; p < limit; p++) + if (naut_bitfield_test(bf, p) && d->avail[p]) d->avail[p]--; +} + +/* --- request selection --------------------------------------------------- */ +/* Find the first missing block with no outstanding request. */ +static uint32_t first_unreq(const pstate *s) { + if (s->verifying) return UINT32_MAX; + for (uint32_t b = 0; b < s->nblocks; b++) + if (!bget(s->recv_bits, b) && s->req_count[b] == 0) return b; + return UINT32_MAX; +} + +static bool hand_out(naut_download *d, uint32_t p, uint32_t b, + uint32_t *index, uint32_t *begin, uint32_t *length) { + d->ps[p]->req_count[b]++; + *index = p; *begin = b * BLK; *length = block_len(d, p, b); + return true; +} + +bool naut_download_pick_for_peer(naut_download *d, const naut_bitfield *peer_have, + naut_request_active_cb peer_has_request, void *ctx, + uint32_t *index, uint32_t *begin, uint32_t *length) { + d->endgame = (d->total_blocks - d->recv_blocks) <= ENDGAME_BLOCKS; + + /* pass 1: finish an in-progress piece the peer has (reduces fragmentation) */ + for (uint32_t p = 0; p < d->num_pieces; p++) { + if (naut_bitfield_test(&d->have, p) || !d->ps[p]) continue; + if (p >= peer_have->nbits || !naut_bitfield_test(peer_have, p)) continue; + uint32_t b = first_unreq(d->ps[p]); + if (b != UINT32_MAX) return hand_out(d, p, b, index, begin, length); + } + /* pass 2: start the rarest new piece the peer has */ + uint32_t best = UINT32_MAX, best_av = UINT32_MAX; + for (uint32_t p = 0; p < d->num_pieces; p++) { + if (naut_bitfield_test(&d->have, p) || d->ps[p]) continue; + if (p >= peer_have->nbits || !naut_bitfield_test(peer_have, p) || + d->avail[p] == 0) continue; + if (d->avail[p] < best_av) { best = p; best_av = d->avail[p]; } + } + if (best != UINT32_MAX) { + if (!ensure_ps(d, best)) return false; + return hand_out(d, best, 0, index, begin, length); + } + /* pass 3: endgame — race each missing block on at most two distinct peers */ + if (d->endgame) { + for (uint8_t copies = 1; copies < ENDGAME_COPIES; copies++) { + for (uint32_t p = 0; p < d->num_pieces; p++) { + if (naut_bitfield_test(&d->have, p) || + p >= peer_have->nbits || + !naut_bitfield_test(peer_have, p)) continue; + if (!ensure_ps(d, p)) continue; + pstate *s = d->ps[p]; + for (uint32_t b = 0; b < s->nblocks; b++) { + uint32_t block_begin = b * BLK; + if (bget(s->recv_bits, b) || s->req_count[b] != copies) continue; + if (peer_has_request && + peer_has_request(ctx, p, block_begin)) continue; + return hand_out(d, p, b, index, begin, length); + } + } + } + } + return false; +} + +bool naut_download_pick(naut_download *d, const naut_bitfield *peer_have, + uint32_t *index, uint32_t *begin, uint32_t *length) { + return naut_download_pick_for_peer(d, peer_have, NULL, NULL, + index, begin, length); +} + +void naut_download_unrequest(naut_download *d, uint32_t index, uint32_t begin) { + if (index >= d->num_pieces || !d->ps[index]) return; + uint32_t b = begin / BLK; + if (b < d->ps[index]->nblocks && !bget(d->ps[index]->recv_bits, b) && + d->ps[index]->req_count[b] != 0) + d->ps[index]->req_count[b]--; +} + +/* sequential single-peer convenience (Phase 3 leecher + tests) */ +bool naut_download_next_request(naut_download *d, + uint32_t *index, uint32_t *begin, uint32_t *length) { + while (d->cur_piece < d->num_pieces) { + if (naut_bitfield_test(&d->have, d->cur_piece)) { d->cur_piece++; continue; } + pstate *s = ensure_ps(d, d->cur_piece); + if (!s) return false; + uint32_t b = first_unreq(s); + if (b != UINT32_MAX) return hand_out(d, d->cur_piece, b, index, begin, length); + d->cur_piece++; + } + return false; +} + +/* --- block ingest -------------------------------------------------------- */ +static naut_err finish_verified(naut_download *d, uint32_t p, + const uint8_t digest[NAUT_SHA1_LEN], + bool *done) { + pstate *s = d->ps[p]; + uint64_t ps = piece_size(d, p); + if (memcmp(digest, + d->mi->piece_hashes + (size_t)p * NAUT_SHA1_LEN, + NAUT_SHA1_LEN) != 0) { + NAUT_WARN("piece %u failed SHA-1; discarding for re-download", p); + d->recv_blocks -= s->nrecv; /* roll back so it can be refetched */ + free_ps(d, p); + return NAUT_ERR_PROTO; + } + naut_err e = naut_storage_write(d->st, (int64_t)p * (int64_t)d->piece_len, s->buf, ps); + if (e != NAUT_OK) return e; + naut_bitfield_set(&d->have, p); + d->pieces_done++; + d->bytes_done += ps; + free_ps(d, p); + *done = true; + notify_files(d, p); + return NAUT_OK; +} + +static void verify_job_run(naut_job *job) { + pstate *state = job->context; + naut_sha1(state->buf, piece_size(state->download, state->piece), + state->digest); + job->result = NAUT_OK; +} + +naut_err naut_download_poll(naut_download *d, uint32_t *pieces_completed) { + if (!d) return NAUT_ERR_INVAL; + if (pieces_completed) *pieces_completed = 0; + if (!d->workers) return NAUT_OK; + naut_job *job; + naut_err result = NAUT_OK; + while (naut_worker_complete(d->workers, &job)) { + pstate *state = job->context; + uint32_t piece = state->piece; + if (state->download != d || piece >= d->num_pieces || + d->ps[piece] != state || !state->verifying) { + result = NAUT_ERR_PROTO; + continue; + } + bool done = false; + naut_err e = finish_verified(d, piece, state->digest, &done); + if (e == NAUT_ERR_PROTO) { + /* Hash mismatch already reset the piece for re-download. The + * worker cannot attribute corruption to one peer, so keep the + * torrent alive and let the picker request it again. */ + continue; + } + if (e != NAUT_OK) { + result = e; + continue; + } + if (done && pieces_completed) (*pieces_completed)++; + } + return result; +} + +naut_err naut_download_on_block(naut_download *d, uint32_t index, uint32_t begin, + const uint8_t *data, uint32_t len, bool *piece_done) { + *piece_done = false; + if (index >= d->num_pieces) return NAUT_ERR_RANGE; + if (naut_bitfield_test(&d->have, index)) return NAUT_OK; /* already complete */ + if (begin % BLK != 0) return NAUT_ERR_PROTO; + uint32_t b = begin / BLK; + if (b >= nblocks(d, index) || len != block_len(d, index, b)) return NAUT_ERR_PROTO; + + pstate *s = ensure_ps(d, index); + if (!s) return NAUT_ERR_NOMEM; + if (bget(s->recv_bits, b)) return NAUT_OK; /* duplicate, ignore */ + + memcpy(s->buf + begin, data, len); + bset(s->recv_bits, b); + s->req_count[b] = 0; + s->nrecv++; + d->recv_blocks++; + if (s->nrecv == s->nblocks) { + if (d->workers) { + s->verifying = true; + s->verify_job.run = verify_job_run; + s->verify_job.context = s; + s->verify_job.result = NAUT_ERR_AGAIN; + if (naut_worker_submit(d->workers, &s->verify_job)) + return NAUT_OK; + s->verifying = false; + } + uint8_t digest[NAUT_SHA1_LEN]; + naut_sha1(s->buf, piece_size(d, index), digest); + return finish_verified(d, index, digest, piece_done); + } + return NAUT_OK; +} + +bool naut_download_complete(const naut_download *d) { return d->pieces_done == d->num_pieces; } +bool naut_download_have(const naut_download *d, uint32_t p) { return naut_bitfield_test(&d->have, p); } +bool naut_download_in_endgame(const naut_download *d) { return d->endgame; } +uint32_t naut_download_num_pieces(const naut_download *d) { return d->num_pieces; } +uint32_t naut_download_pieces_done(const naut_download *d) { return d->pieces_done; } +uint64_t naut_download_bytes_done(const naut_download *d) { return d->bytes_done; } diff --git a/src/platform/net.c b/src/platform/net.c new file mode 100644 index 0000000..4f66b19 --- /dev/null +++ b/src/platform/net.c @@ -0,0 +1,62 @@ +#include "naut/net.h" +#include "naut/log.h" + +#include +#include +#include +#include +#include +#include + +static bool setopt(int fd, int level, int opt, int val) { + return setsockopt(fd, level, opt, &val, sizeof(val)) == 0; +} + +int naut_net_listen(uint16_t port, int backlog, bool reuseport) { + int fd = socket(AF_INET6, SOCK_STREAM, 0); + if (fd < 0) { NAUT_ERROR("socket: %s", strerror(errno)); return -1; } + + setopt(fd, SOL_SOCKET, SO_REUSEADDR, 1); + if (reuseport && !setopt(fd, SOL_SOCKET, SO_REUSEPORT, 1)) + NAUT_WARN("SO_REUSEPORT unavailable: %s", strerror(errno)); + /* dual-stack v4+v6 */ + setopt(fd, IPPROTO_IPV6, IPV6_V6ONLY, 0); + + struct sockaddr_in6 a; + memset(&a, 0, sizeof(a)); + a.sin6_family = AF_INET6; + a.sin6_addr = in6addr_any; + a.sin6_port = htons(port); + if (bind(fd, (struct sockaddr *)&a, sizeof(a)) != 0) { + NAUT_ERROR("bind(:%u): %s", port, strerror(errno)); + close(fd); + return -1; + } + if (listen(fd, backlog) != 0) { + NAUT_ERROR("listen: %s", strerror(errno)); + close(fd); + return -1; + } + return fd; +} + +void naut_net_tune_peer(int fd) { + setopt(fd, IPPROTO_TCP, TCP_NODELAY, 1); + setopt(fd, SOL_SOCKET, SO_SNDBUF, 4 * 1024 * 1024); + setopt(fd, SOL_SOCKET, SO_RCVBUF, 4 * 1024 * 1024); +#ifdef TCP_QUICKACK + setopt(fd, IPPROTO_TCP, TCP_QUICKACK, 1); +#endif +} + +void naut_net_set_bufsizes(int fd, int sndbuf, int rcvbuf) { + if (sndbuf > 0) setopt(fd, SOL_SOCKET, SO_SNDBUF, sndbuf); + if (rcvbuf > 0) setopt(fd, SOL_SOCKET, SO_RCVBUF, rcvbuf); +} + +int naut_net_set_nonblock(int fd, bool on) { + int fl = fcntl(fd, F_GETFL, 0); + if (fl < 0) return NAUT_ERR_IO; + fl = on ? (fl | O_NONBLOCK) : (fl & ~O_NONBLOCK); + return fcntl(fd, F_SETFL, fl) == 0 ? NAUT_OK : NAUT_ERR_IO; +} diff --git a/src/platform/system.c b/src/platform/system.c new file mode 100644 index 0000000..e463936 --- /dev/null +++ b/src/platform/system.c @@ -0,0 +1,19 @@ +#include "naut/system.h" + +#include +#include + +naut_err naut_pin_current_thread(int cpu) { + int online = naut_online_cpus(); + if (cpu < 0 || cpu >= online) return NAUT_ERR_RANGE; + cpu_set_t set; + CPU_ZERO(&set); + CPU_SET((unsigned)cpu, &set); + return sched_setaffinity(0, sizeof set, &set) == 0 + ? NAUT_OK : NAUT_ERR_IO; +} + +int naut_online_cpus(void) { + long count = sysconf(_SC_NPROCESSORS_ONLN); + return count > 0 && count <= INT32_MAX ? (int)count : 1; +} diff --git a/src/platform/uring.c b/src/platform/uring.c new file mode 100644 index 0000000..960e9c5 --- /dev/null +++ b/src/platform/uring.c @@ -0,0 +1,151 @@ +#include "naut/uring.h" +#include "naut/log.h" + +#include +#include +#include + +naut_err naut_ring_init(naut_ring *r, unsigned entries, bool sqpoll) { + return naut_ring_init_cpu(r, entries, sqpoll, -1); +} + +/* Build the params for one setup attempt. COOP_TASKRUN runs completion task + * work in the submitter's context to cut IPIs when one thread owns the ring — + * but it is *mutually exclusive* with SQPOLL (a kernel thread submits there, so + * there is no cooperative submitter context), and the kernel rejects the combo + * with -EINVAL. So pick exactly one of the two. */ +static void ring_params(struct io_uring_params *p, bool sqpoll, int sqpoll_cpu) { + memset(p, 0, sizeof(*p)); + p->flags = IORING_SETUP_SINGLE_ISSUER; + if (sqpoll) { + p->flags |= IORING_SETUP_SQPOLL; + p->sq_thread_idle = 1000; /* ms before the poll thread sleeps */ + if (sqpoll_cpu >= 0) { + p->flags |= IORING_SETUP_SQ_AFF; + p->sq_thread_cpu = (unsigned)sqpoll_cpu; + } + } else { + p->flags |= IORING_SETUP_COOP_TASKRUN; + } +} + +naut_err naut_ring_init_cpu(naut_ring *r, unsigned entries, bool sqpoll, + int sqpoll_cpu) { + struct io_uring_params p; + ring_params(&p, sqpoll, sqpoll_cpu); + + int rc = io_uring_queue_init_params(entries, &r->ring, &p); + if (rc < 0 && sqpoll) { + NAUT_WARN("io_uring SQPOLL setup failed (%s), retrying without it", + strerror(-rc)); + ring_params(&p, false, -1); + rc = io_uring_queue_init_params(entries, &r->ring, &p); + sqpoll = false; + } + if (rc < 0) { + /* Older fallbacks: SINGLE_ISSUER needs ~6.0; we have it, but be safe. */ + rc = io_uring_queue_init(entries, &r->ring, 0); + if (rc < 0) { + NAUT_ERROR("io_uring_queue_init: %s", strerror(-rc)); + return NAUT_ERR_NOSYS; + } + sqpoll = false; + } + r->sqpoll = sqpoll; + r->send_zc = false; + r->msg_ring = false; + r->buffers_registered = false; + r->recv_fixed = false; + NAUT_INFO("io_uring: %u entries%s", entries, sqpoll ? ", sqpoll" : ""); + return NAUT_OK; +} + +void naut_ring_close(naut_ring *r) { + naut_ring_unregister_buffers(r); + io_uring_queue_exit(&r->ring); +} + +naut_err naut_ring_probe(naut_ring *r) { + struct io_uring_probe *p = io_uring_get_probe(); + if (!p) { NAUT_WARN("io_uring_get_probe failed"); return NAUT_OK; } + + struct { int op; const char *name; bool required; } want[] = { + { IORING_OP_RECV, "recv", true }, + { IORING_OP_SEND, "send", true }, + { IORING_OP_ACCEPT, "accept", true }, + { IORING_OP_READ_FIXED, "read_fixed", true }, + { IORING_OP_WRITE_FIXED, "write_fixed", true }, + { IORING_OP_SEND_ZC, "send_zc", false }, /* seed fast-path */ + { IORING_OP_MSG_RING, "msg_ring", false }, /* cross-ring wake */ + }; + naut_err result = NAUT_OK; + for (size_t i = 0; i < NAUT_ARRAY_LEN(want); i++) { + bool ok = io_uring_opcode_supported(p, want[i].op); + if (want[i].op == IORING_OP_SEND_ZC) r->send_zc = ok; + if (want[i].op == IORING_OP_MSG_RING) r->msg_ring = ok; + if (!ok && want[i].required) { + NAUT_ERROR("io_uring missing required op: %s", want[i].name); + result = NAUT_ERR_NOSYS; + } else if (!ok) { + NAUT_WARN("io_uring optional op unavailable: %s (degraded)", want[i].name); + } + } + io_uring_free_probe(p); + NAUT_INFO("io_uring optional features: send_zc=%s, msg_ring=%s", + r->send_zc ? "yes" : "no", + r->msg_ring ? "yes" : "no"); + return result; +} + +naut_err naut_ring_register_bufpool(naut_ring *r, const naut_bufpool *pool) { + if (!r || !pool || r->buffers_registered) return NAUT_ERR_INVAL; + size_t bytes = 0; + void *slab = naut_bufpool_slab(pool, &bytes); + if (!slab || bytes == 0) return NAUT_ERR_INVAL; + struct iovec iov = { .iov_base = slab, .iov_len = bytes }; + int rc = io_uring_register_buffers(&r->ring, &iov, 1); + if (rc < 0) { + NAUT_WARN("io_uring fixed-buffer registration failed: %s", + strerror(-rc)); + return rc == -ENOMEM || rc == -EPERM ? NAUT_ERR_NOSYS : NAUT_ERR_IO; + } + r->buffers_registered = true; + r->recv_fixed = true; + NAUT_INFO("io_uring: registered %zu MiB buffer slab", bytes >> 20); + return NAUT_OK; +} + +void naut_ring_unregister_buffers(naut_ring *r) { + if (!r || !r->buffers_registered) return; + int rc = io_uring_unregister_buffers(&r->ring); + if (rc < 0) + NAUT_WARN("io_uring unregister buffers: %s", strerror(-rc)); + r->buffers_registered = false; + r->recv_fixed = false; +} + +bool naut_ring_prep_send(naut_ring *r, struct io_uring_sqe *sqe, int fd, + const void *buf, size_t len, int flags, + bool prefer_zero_copy) { + if (prefer_zero_copy && r && r->send_zc) { + if (r->buffers_registered) + io_uring_prep_send_zc_fixed(sqe, fd, buf, len, flags, 0, 0); + else + io_uring_prep_send_zc(sqe, fd, buf, len, flags, 0); + sqe->ioprio |= IORING_SEND_ZC_REPORT_USAGE; + return true; + } + io_uring_prep_send(sqe, fd, buf, len, flags); + return false; +} + +bool naut_ring_prep_recv(naut_ring *r, struct io_uring_sqe *sqe, int fd, + void *buf, size_t len, int flags) { + io_uring_prep_recv(sqe, fd, buf, len, flags); + if (r && r->buffers_registered && r->recv_fixed) { + sqe->ioprio |= IORING_RECVSEND_FIXED_BUF; + sqe->buf_index = 0; + return true; + } + return false; +} diff --git a/src/plugin/plugin.c b/src/plugin/plugin.c new file mode 100644 index 0000000..d9f42ab --- /dev/null +++ b/src/plugin/plugin.c @@ -0,0 +1,354 @@ +#include "naut/plugin.h" +#include "naut/log.h" + +#include +#include +#include + +typedef struct { + naut_plugin_rpc_fn callback; + void *context; + char *method; +} rpc_adapter; + +typedef struct { + naut_plugin_event_fn callback; + void *context; +} event_adapter; + +typedef struct { + void *handle; + char *path; + char *name; + uint64_t *subscriptions; + size_t subscription_count; + size_t subscription_capacity; +} loaded_plugin; + +struct naut_plugin_manager { + naut_rpc_registry *rpc; + naut_event_bus *events; + loaded_plugin *plugins; + size_t plugin_count; + size_t plugin_capacity; + naut_storage_backend_v1 *storage; + size_t storage_count; + size_t storage_capacity; + rpc_adapter **rpc_adapters; + size_t rpc_count; + size_t rpc_capacity; + event_adapter **event_adapters; + size_t event_count; + size_t event_capacity; + loaded_plugin *loading; +}; + +static json_t *plugin_rpc_adapter(void *opaque, const json_t *params, + naut_err *error) { + rpc_adapter *adapter = opaque; + char *request = + json_dumps(params ? params : json_null(), JSON_COMPACT | JSON_ENCODE_ANY); + if (!request) { + *error = NAUT_ERR_NOMEM; + return NULL; + } + char *response = NULL; + *error = adapter->callback(adapter->context, request, &response); + free(request); + if (*error != NAUT_OK) { + free(response); + return NULL; + } + if (!response) return json_null(); + json_error_t json_error; + json_t *json = json_loads(response, JSON_REJECT_DUPLICATES, &json_error); + free(response); + if (!json) { + *error = NAUT_ERR_PROTO; + return NULL; + } + return json; +} + +static void plugin_event_adapter(void *opaque, const naut_event *event) { + event_adapter *adapter = opaque; + adapter->callback(adapter->context, event); +} + +static naut_err host_set_name(void *opaque, const char *name) { + naut_plugin_manager *manager = opaque; + if (!manager->loading || !name || !*name || manager->loading->name) + return NAUT_ERR_INVAL; + manager->loading->name = strdup(name); + return manager->loading->name ? NAUT_OK : NAUT_ERR_NOMEM; +} + +static naut_err reserve_pointer(void ***items, size_t *count, size_t *capacity, + void *item) { + if (*count == *capacity) { + size_t next_capacity = *capacity ? *capacity * 2 : 8; + void **next = realloc(*items, next_capacity * sizeof(*next)); + if (!next) return NAUT_ERR_NOMEM; + *items = next; + *capacity = next_capacity; + } + (*items)[(*count)++] = item; + return NAUT_OK; +} + +static naut_err host_register_rpc(void *opaque, const char *method, + naut_plugin_rpc_fn callback, void *context) { + naut_plugin_manager *manager = opaque; + if (!manager->loading || !method || !*method || !callback) + return NAUT_ERR_INVAL; + rpc_adapter *adapter = malloc(sizeof(*adapter)); + if (!adapter) return NAUT_ERR_NOMEM; + adapter->callback = callback; + adapter->context = context; + adapter->method = strdup(method); + if (!adapter->method) { + free(adapter); + return NAUT_ERR_NOMEM; + } + naut_err error = naut_rpc_register(manager->rpc, method, + plugin_rpc_adapter, adapter); + if (error != NAUT_OK) { + free(adapter->method); + free(adapter); + return error; + } + error = reserve_pointer((void ***)&manager->rpc_adapters, + &manager->rpc_count, &manager->rpc_capacity, + adapter); + if (error != NAUT_OK) { + naut_rpc_unregister(manager->rpc, method); + free(adapter->method); + free(adapter); + } + return error; +} + +static naut_err host_register_storage( + void *opaque, const naut_storage_backend_v1 *backend) { + naut_plugin_manager *manager = opaque; + if (!manager->loading || !backend || + backend->abi_version != NAUT_PLUGIN_ABI_VERSION || + backend->struct_size < sizeof(*backend) || + !backend->name || !backend->open || !backend->close || + !backend->read || !backend->write) + return NAUT_ERR_INVAL; + for (size_t i = 0; i < manager->storage_count; i++) + if (strcmp(manager->storage[i].name, backend->name) == 0) + return NAUT_ERR_INVAL; + if (manager->storage_count == manager->storage_capacity) { + size_t capacity = + manager->storage_capacity ? manager->storage_capacity * 2 : 8; + naut_storage_backend_v1 *next = + realloc(manager->storage, capacity * sizeof(*next)); + if (!next) return NAUT_ERR_NOMEM; + manager->storage = next; + manager->storage_capacity = capacity; + } + manager->storage[manager->storage_count++] = *backend; + return NAUT_OK; +} + +static naut_err host_subscribe_event(void *opaque, + naut_plugin_event_fn callback, + void *context) { + naut_plugin_manager *manager = opaque; + if (!manager->loading || !callback) return NAUT_ERR_INVAL; + event_adapter *adapter = malloc(sizeof(*adapter)); + if (!adapter) return NAUT_ERR_NOMEM; + adapter->callback = callback; + adapter->context = context; + uint64_t id = 0; + naut_err error = naut_event_subscribe(manager->events, + plugin_event_adapter, + adapter, &id); + if (error != NAUT_OK) { + free(adapter); + return error; + } + loaded_plugin *plugin = manager->loading; + if (plugin->subscription_count == plugin->subscription_capacity) { + size_t capacity = plugin->subscription_capacity + ? plugin->subscription_capacity * 2 : 4; + uint64_t *next = + realloc(plugin->subscriptions, capacity * sizeof(*next)); + if (!next) { + naut_event_unsubscribe(manager->events, id); + free(adapter); + return NAUT_ERR_NOMEM; + } + plugin->subscriptions = next; + plugin->subscription_capacity = capacity; + } + plugin->subscriptions[plugin->subscription_count++] = id; + error = reserve_pointer((void ***)&manager->event_adapters, + &manager->event_count, + &manager->event_capacity, adapter); + if (error != NAUT_OK) { + plugin->subscription_count--; + naut_event_unsubscribe(manager->events, id); + free(adapter); + } + return error; +} + +static void host_emit_event(void *opaque, const naut_event *event) { + naut_plugin_manager *manager = opaque; + naut_event_emit(manager->events, event); +} + +static void host_log(void *opaque, int level, const char *message) { + (void)opaque; + if (!message) return; + if (level <= 0) NAUT_ERROR("plugin: %s", message); + else if (level == 1) NAUT_WARN("plugin: %s", message); + else NAUT_INFO("plugin: %s", message); +} + +naut_plugin_manager *naut_plugin_manager_create( + naut_rpc_registry *rpc, naut_event_bus *events) { + if (!rpc || !events) return NULL; + naut_plugin_manager *manager = calloc(1, sizeof(*manager)); + if (!manager) return NULL; + manager->rpc = rpc; + manager->events = events; + return manager; +} + +void naut_plugin_manager_destroy(naut_plugin_manager *manager) { + if (!manager) return; + for (size_t i = 0; i < manager->rpc_count; i++) + naut_rpc_unregister(manager->rpc, manager->rpc_adapters[i]->method); + for (size_t i = 0; i < manager->plugin_count; i++) { + loaded_plugin *plugin = &manager->plugins[i]; + for (size_t s = 0; s < plugin->subscription_count; s++) + naut_event_unsubscribe(manager->events, plugin->subscriptions[s]); + free(plugin->subscriptions); + free(plugin->name); + free(plugin->path); + if (plugin->handle) dlclose(plugin->handle); + } + for (size_t i = 0; i < manager->rpc_count; i++) { + free(manager->rpc_adapters[i]->method); + free(manager->rpc_adapters[i]); + } + for (size_t i = 0; i < manager->event_count; i++) + free(manager->event_adapters[i]); + free(manager->rpc_adapters); + free(manager->event_adapters); + free(manager->plugins); + free(manager->storage); + free(manager); +} + +naut_err naut_plugin_load(naut_plugin_manager *manager, const char *path) { + if (!manager || !path || !*path || manager->loading) + return NAUT_ERR_INVAL; + if (manager->plugin_count == manager->plugin_capacity) { + size_t capacity = + manager->plugin_capacity ? manager->plugin_capacity * 2 : 4; + loaded_plugin *next = + realloc(manager->plugins, capacity * sizeof(*next)); + if (!next) return NAUT_ERR_NOMEM; + manager->plugins = next; + manager->plugin_capacity = capacity; + } + loaded_plugin *plugin = &manager->plugins[manager->plugin_count]; + memset(plugin, 0, sizeof(*plugin)); + plugin->path = strdup(path); + plugin->handle = dlopen(path, RTLD_NOW | RTLD_LOCAL); + if (!plugin->path || !plugin->handle) { + NAUT_ERROR("plugin load %s: %s", path, dlerror()); + free(plugin->path); + memset(plugin, 0, sizeof(*plugin)); + return NAUT_ERR_IO; + } + dlerror(); + naut_plugin_register_fn register_plugin = + (naut_plugin_register_fn)dlsym(plugin->handle, + "naut_plugin_register"); + const char *symbol_error = dlerror(); + if (symbol_error || !register_plugin) { + NAUT_ERROR("plugin %s has no naut_plugin_register: %s", + path, symbol_error ? symbol_error : "missing"); + dlclose(plugin->handle); + free(plugin->path); + memset(plugin, 0, sizeof(*plugin)); + return NAUT_ERR_PROTO; + } + naut_host_api host = { + .abi_version = NAUT_PLUGIN_ABI_VERSION, + .struct_size = sizeof(host), + .host_context = manager, + .set_plugin_name = host_set_name, + .register_rpc = host_register_rpc, + .register_storage_backend = host_register_storage, + .subscribe_event = host_subscribe_event, + .emit_event = host_emit_event, + .log = host_log, + }; + size_t rpc_start = manager->rpc_count; + size_t event_start = manager->event_count; + size_t storage_start = manager->storage_count; + manager->loading = plugin; + naut_err error = register_plugin(&host); + manager->loading = NULL; + if (error != NAUT_OK || !plugin->name) { + NAUT_ERROR("plugin registration failed: %s", path); + for (size_t i = 0; i < plugin->subscription_count; i++) + naut_event_unsubscribe(manager->events, + plugin->subscriptions[i]); + for (size_t i = event_start; i < manager->event_count; i++) + free(manager->event_adapters[i]); + manager->event_count = event_start; + for (size_t i = rpc_start; i < manager->rpc_count; i++) { + naut_rpc_unregister(manager->rpc, + manager->rpc_adapters[i]->method); + free(manager->rpc_adapters[i]->method); + free(manager->rpc_adapters[i]); + } + manager->rpc_count = rpc_start; + manager->storage_count = storage_start; + dlclose(plugin->handle); + free(plugin->path); + free(plugin->name); + free(plugin->subscriptions); + memset(plugin, 0, sizeof(*plugin)); + return error != NAUT_OK ? error : NAUT_ERR_PROTO; + } + manager->plugin_count++; + NAUT_INFO("loaded plugin '%s' from %s", plugin->name, path); + return NAUT_OK; +} + +size_t naut_plugin_count(const naut_plugin_manager *manager) { + return manager ? manager->plugin_count : 0; +} + +const char *naut_plugin_name(const naut_plugin_manager *manager, size_t index) { + return manager && index < manager->plugin_count + ? manager->plugins[index].name : NULL; +} + +size_t naut_plugin_storage_count(const naut_plugin_manager *manager) { + return manager ? manager->storage_count : 0; +} + +const char *naut_plugin_storage_name(const naut_plugin_manager *manager, + size_t index) { + return manager && index < manager->storage_count + ? manager->storage[index].name : NULL; +} + +const naut_storage_backend_v1 *naut_plugin_storage_backend( + const naut_plugin_manager *manager, const char *name) { + if (!manager || !name) return NULL; + for (size_t i = 0; i < manager->storage_count; i++) + if (strcmp(manager->storage[i].name, name) == 0) + return &manager->storage[i]; + return NULL; +} diff --git a/src/rpc/rpc.c b/src/rpc/rpc.c new file mode 100644 index 0000000..5cab20c --- /dev/null +++ b/src/rpc/rpc.c @@ -0,0 +1,261 @@ +#include "naut/rpc.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#define RPC_MAGIC 0x4e545250u + +typedef struct { + char *method; + naut_rpc_handler handler; + void *context; +} command; + +struct naut_rpc_registry { + pthread_mutex_t lock; + command *commands; + size_t count; + size_t capacity; +}; + +typedef struct NAUT_PACKED { + uint32_t magic; + uint16_t version; + uint16_t type; + uint32_t length; +} frame_header; + +static bool write_all(int fd, const void *data, size_t length) { + const uint8_t *p = data; + while (length) { + ssize_t n = write(fd, p, length); + if (n < 0) { + if (errno == EINTR) continue; + return false; + } + if (n == 0) return false; + p += n; + length -= (size_t)n; + } + return true; +} + +static bool read_all(int fd, void *data, size_t length) { + uint8_t *p = data; + while (length) { + ssize_t n = recv(fd, p, length, 0); + if (n < 0) { + if (errno == EINTR) continue; + return false; + } + if (n == 0) return false; + p += n; + length -= (size_t)n; + } + return true; +} + +naut_rpc_registry *naut_rpc_registry_create(void) { + naut_rpc_registry *registry = calloc(1, sizeof(*registry)); + if (!registry) return NULL; + if (pthread_mutex_init(®istry->lock, NULL) != 0) { + free(registry); + return NULL; + } + return registry; +} + +void naut_rpc_registry_destroy(naut_rpc_registry *registry) { + if (!registry) return; + for (size_t i = 0; i < registry->count; i++) + free(registry->commands[i].method); + free(registry->commands); + pthread_mutex_destroy(®istry->lock); + free(registry); +} + +naut_err naut_rpc_register(naut_rpc_registry *registry, const char *method, + naut_rpc_handler handler, void *context) { + if (!registry || !method || !*method || !handler) return NAUT_ERR_INVAL; + pthread_mutex_lock(®istry->lock); + for (size_t i = 0; i < registry->count; i++) { + if (strcmp(registry->commands[i].method, method) == 0) { + pthread_mutex_unlock(®istry->lock); + return NAUT_ERR_INVAL; + } + } + if (registry->count == registry->capacity) { + size_t capacity = registry->capacity ? registry->capacity * 2 : 16; + command *next = realloc(registry->commands, + capacity * sizeof(*next)); + if (!next) { + pthread_mutex_unlock(®istry->lock); + return NAUT_ERR_NOMEM; + } + registry->commands = next; + registry->capacity = capacity; + } + char *copy = strdup(method); + if (!copy) { + pthread_mutex_unlock(®istry->lock); + return NAUT_ERR_NOMEM; + } + registry->commands[registry->count++] = (command) { + .method = copy, + .handler = handler, + .context = context, + }; + pthread_mutex_unlock(®istry->lock); + return NAUT_OK; +} + +void naut_rpc_unregister(naut_rpc_registry *registry, const char *method) { + if (!registry || !method) return; + pthread_mutex_lock(®istry->lock); + for (size_t i = 0; i < registry->count; i++) { + if (strcmp(registry->commands[i].method, method) != 0) continue; + free(registry->commands[i].method); + registry->commands[i] = registry->commands[--registry->count]; + break; + } + pthread_mutex_unlock(®istry->lock); +} + +json_t *naut_rpc_dispatch(naut_rpc_registry *registry, const char *method, + const json_t *params, naut_err *error) { + if (error) *error = NAUT_ERR_INVAL; + if (!registry || !method) return NULL; + pthread_mutex_lock(®istry->lock); + naut_rpc_handler handler = NULL; + void *context = NULL; + for (size_t i = 0; i < registry->count; i++) { + if (strcmp(registry->commands[i].method, method) == 0) { + handler = registry->commands[i].handler; + context = registry->commands[i].context; + break; + } + } + pthread_mutex_unlock(®istry->lock); + if (!handler) return NULL; + if (error) *error = NAUT_OK; + return handler(context, params, error); +} + +naut_err naut_rpc_send_json(int fd, naut_rpc_frame_type type, + const json_t *payload) { + if (fd < 0 || !payload) return NAUT_ERR_INVAL; + char *text = + json_dumps(payload, JSON_COMPACT | JSON_SORT_KEYS | JSON_ENCODE_ANY); + if (!text) return NAUT_ERR_NOMEM; + size_t length = strlen(text); + if (length > NAUT_RPC_MAX_PAYLOAD) { + free(text); + return NAUT_ERR_RANGE; + } + frame_header header = { + .magic = htonl(RPC_MAGIC), + .version = htons(NAUT_RPC_VERSION), + .type = htons((uint16_t)type), + .length = htonl((uint32_t)length), + }; + bool ok = write_all(fd, &header, sizeof header) && + write_all(fd, text, length); + free(text); + return ok ? NAUT_OK : NAUT_ERR_IO; +} + +naut_err naut_rpc_recv_json(int fd, naut_rpc_frame_type *type, + json_t **payload) { + if (fd < 0 || !type || !payload) return NAUT_ERR_INVAL; + *payload = NULL; + frame_header header; + if (!read_all(fd, &header, sizeof header)) return NAUT_ERR_IO; + if (ntohl(header.magic) != RPC_MAGIC || + ntohs(header.version) != NAUT_RPC_VERSION) + return NAUT_ERR_PROTO; + uint16_t raw_type = ntohs(header.type); + uint32_t length = ntohl(header.length); + if (raw_type < NAUT_RPC_REQUEST || raw_type > NAUT_RPC_EVENT || + length > NAUT_RPC_MAX_PAYLOAD) + return NAUT_ERR_PROTO; + char *text = malloc((size_t)length + 1); + if (!text) return NAUT_ERR_NOMEM; + if (!read_all(fd, text, length)) { + free(text); + return NAUT_ERR_IO; + } + text[length] = 0; + json_error_t json_error; + json_t *json = json_loadb(text, length, JSON_REJECT_DUPLICATES, + &json_error); + free(text); + if (!json) return NAUT_ERR_PROTO; + *type = (naut_rpc_frame_type)raw_type; + *payload = json; + return NAUT_OK; +} + +int naut_rpc_connect_unix(const char *path) { + if (!path || !*path) return -1; + int fd = socket(AF_UNIX, SOCK_STREAM, 0); + if (fd < 0) return -1; + struct sockaddr_un address; + memset(&address, 0, sizeof address); + address.sun_family = AF_UNIX; + if (strlen(path) >= sizeof address.sun_path) { + close(fd); + return -1; + } + strcpy(address.sun_path, path); + if (connect(fd, (struct sockaddr *)&address, sizeof address) != 0) { + close(fd); + return -1; + } + return fd; +} + +naut_err naut_rpc_call(const char *socket_path, const char *method, + const json_t *params, json_t **response) { + if (!socket_path || !method || !response) return NAUT_ERR_INVAL; + *response = NULL; + int fd = naut_rpc_connect_unix(socket_path); + if (fd < 0) return NAUT_ERR_IO; + json_t *request = json_object(); + json_object_set_new(request, "method", json_string(method)); + json_object_set(request, "params", + params ? (json_t *)params : json_null()); + naut_err error = naut_rpc_send_json(fd, NAUT_RPC_REQUEST, request); + json_decref(request); + if (error == NAUT_OK) { + naut_rpc_frame_type type; + error = naut_rpc_recv_json(fd, &type, response); + if (error == NAUT_OK && type != NAUT_RPC_RESPONSE) { + json_decref(*response); + *response = NULL; + error = NAUT_ERR_PROTO; + } + } + close(fd); + return error; +} + +json_t *naut_rpc_event_json(const naut_event *event) { + if (!event) return NULL; + json_t *json = json_object(); + json_object_set_new(json, "event", + json_string(naut_event_type_name(event->type))); + json_object_set_new(json, "torrent_id", + json_integer((json_int_t)event->torrent_id)); + json_object_set_new(json, "index", json_integer(event->index)); + if (event->message) + json_object_set_new(json, "message", json_string(event->message)); + if (event->path) + json_object_set_new(json, "path", json_string(event->path)); + return json; +} diff --git a/src/script/script.c b/src/script/script.c new file mode 100644 index 0000000..7988633 --- /dev/null +++ b/src/script/script.c @@ -0,0 +1,312 @@ +#include "naut/script.h" + +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +typedef struct { + naut_event event; + char message[256]; + char path[PATH_MAX]; +} script_job; + +struct naut_script { + naut_event_bus *events; + uint64_t subscription; + lua_State *lua; + pthread_t thread; + pthread_mutex_t lock; + pthread_cond_t ready; + script_job *queue; + size_t capacity; + size_t head; + size_t count; + bool stopping; + naut_script_move_file_cb move_file; + void *move_context; + _Atomic uint64_t queued; + _Atomic uint64_t handled; + _Atomic uint64_t dropped; + _Atomic uint64_t errors; + _Atomic uint64_t move_requests; + char last_error[256]; +}; + +static const char *hook_names[] = { + [NAUT_EVENT_TORRENT_ADDED] = "on_torrent_added", + [NAUT_EVENT_PIECE_COMPLETE] = "on_piece_complete", + [NAUT_EVENT_FILE_COMPLETE] = "on_file_complete", + [NAUT_EVENT_TORRENT_FINISHED] = "on_torrent_finished", + [NAUT_EVENT_PEER_CONNECTED] = "on_peer_connected", + [NAUT_EVENT_ALERT] = "on_alert", +}; + +static void set_last_error(naut_script *script, const char *message) { + pthread_mutex_lock(&script->lock); + snprintf(script->last_error, sizeof script->last_error, "%s", + message ? message : "unknown Lua error"); + pthread_mutex_unlock(&script->lock); +} + +static naut_script *lua_script(lua_State *lua) { + return lua_touserdata(lua, lua_upvalueindex(1)); +} + +static int lua_move_file(lua_State *lua) { + naut_script *script = lua_script(lua); + lua_Integer torrent_id = luaL_checkinteger(lua, 1); + lua_Integer file_index = luaL_checkinteger(lua, 2); + const char *destination = luaL_checkstring(lua, 3); + if (torrent_id < 0 || file_index < 0 || + (uint64_t)file_index > UINT32_MAX) + return luaL_error(lua, "move_file arguments out of range"); + if (!script->move_file) + return luaL_error(lua, "move_file is unavailable"); + naut_err error = script->move_file(script->move_context, + (uint64_t)torrent_id, + (uint32_t)file_index, + destination); + if (error != NAUT_OK) + return luaL_error(lua, "move_file failed: %d", error); + atomic_fetch_add_explicit(&script->move_requests, 1, + memory_order_relaxed); + return 0; +} + +static void sandbox(lua_State *lua) { + /* Remove every documented route to the filesystem, subprocesses, native + * module loading, and raw chunk compilation. `load`/`loadstring` are + * blocked too: with the default "bt" mode they accept *binary* chunks, and + * a crafted bytecode chunk can escape the VM entirely — so even though + * scripts are operator-supplied, we deny the bytecode-loader as + * defense-in-depth. */ + static const char *blocked[] = { + "debug", "dofile", "io", "load", "loadfile", "loadstring", + "os", "package", "require", + }; + for (size_t i = 0; i < NAUT_ARRAY_LEN(blocked); i++) { + lua_pushnil(lua); + lua_setglobal(lua, blocked[i]); + } +} + +static void install_api(naut_script *script) { + lua_State *lua = script->lua; + lua_newtable(lua); + lua_pushlightuserdata(lua, script); + lua_pushcclosure(lua, lua_move_file, 1); + lua_setfield(lua, -2, "move_file"); + lua_setglobal(lua, "naut"); +} + +static void push_event(lua_State *lua, const naut_event *event) { + lua_createtable(lua, 0, 5); + lua_pushstring(lua, naut_event_type_name(event->type)); + lua_setfield(lua, -2, "type"); + lua_pushinteger(lua, (lua_Integer)event->torrent_id); + lua_setfield(lua, -2, "torrent_id"); + lua_pushinteger(lua, (lua_Integer)event->index); + lua_setfield(lua, -2, "index"); + if (event->message) { + lua_pushstring(lua, event->message); + lua_setfield(lua, -2, "message"); + } + if (event->path) { + lua_pushstring(lua, event->path); + lua_setfield(lua, -2, "path"); + } +} + +static void run_hook(naut_script *script, const naut_event *event) { + if ((size_t)event->type >= NAUT_ARRAY_LEN(hook_names)) { + atomic_fetch_add_explicit(&script->errors, 1, memory_order_relaxed); + set_last_error(script, "unknown event type"); + return; + } + const char *hook = hook_names[event->type]; + lua_getglobal(script->lua, hook); + if (lua_isnil(script->lua, -1)) { + lua_pop(script->lua, 1); + return; + } + if (!lua_isfunction(script->lua, -1)) { + lua_pop(script->lua, 1); + atomic_fetch_add_explicit(&script->errors, 1, memory_order_relaxed); + set_last_error(script, "event hook is not a function"); + return; + } + push_event(script->lua, event); + if (lua_pcall(script->lua, 1, 0, 0) != LUA_OK) { + atomic_fetch_add_explicit(&script->errors, 1, memory_order_relaxed); + set_last_error(script, lua_tostring(script->lua, -1)); + lua_pop(script->lua, 1); + return; + } + atomic_fetch_add_explicit(&script->handled, 1, memory_order_relaxed); +} + +static bool pop_job(naut_script *script, script_job *job) { + pthread_mutex_lock(&script->lock); + while (!script->stopping && script->count == 0) + pthread_cond_wait(&script->ready, &script->lock); + if (script->count == 0) { + pthread_mutex_unlock(&script->lock); + return false; + } + *job = script->queue[script->head]; + if (job->event.message) job->event.message = job->message; + if (job->event.path) job->event.path = job->path; + script->head = (script->head + 1) % script->capacity; + script->count--; + pthread_mutex_unlock(&script->lock); + return true; +} + +static void *script_worker(void *opaque) { + naut_script *script = opaque; + script_job job; + while (pop_job(script, &job)) + run_hook(script, &job.event); + return NULL; +} + +static void queue_event(void *opaque, const naut_event *event) { + naut_script *script = opaque; + pthread_mutex_lock(&script->lock); + if (script->stopping || script->count == script->capacity) { + pthread_mutex_unlock(&script->lock); + atomic_fetch_add_explicit(&script->dropped, 1, memory_order_relaxed); + return; + } + size_t tail = (script->head + script->count) % script->capacity; + script_job *job = &script->queue[tail]; + memset(job, 0, sizeof(*job)); + job->event = *event; + if (event->message) { + snprintf(job->message, sizeof job->message, "%s", event->message); + job->event.message = job->message; + } + if (event->path) { + snprintf(job->path, sizeof job->path, "%s", event->path); + job->event.path = job->path; + } + script->count++; + pthread_cond_signal(&script->ready); + pthread_mutex_unlock(&script->lock); + atomic_fetch_add_explicit(&script->queued, 1, memory_order_relaxed); +} + +naut_script *naut_script_create(naut_event_bus *events, + const char *script_path, + size_t queue_capacity, + naut_script_move_file_cb move_file, + void *move_context, + naut_err *error) { + if (error) *error = NAUT_ERR_INVAL; + if (!events || !script_path || !*script_path || queue_capacity == 0) + return NULL; + naut_script *script = calloc(1, sizeof(*script)); + if (!script) { + if (error) *error = NAUT_ERR_NOMEM; + return NULL; + } + script->events = events; + script->capacity = queue_capacity; + script->move_file = move_file; + script->move_context = move_context; + script->queue = calloc(queue_capacity, sizeof(*script->queue)); + if (!script->queue) { + if (error) *error = NAUT_ERR_NOMEM; + free(script); + return NULL; + } + if (pthread_mutex_init(&script->lock, NULL) != 0) { + if (error) *error = NAUT_ERR_NOMEM; + free(script->queue); + free(script); + return NULL; + } + if (pthread_cond_init(&script->ready, NULL) != 0) { + if (error) *error = NAUT_ERR_NOMEM; + pthread_mutex_destroy(&script->lock); + free(script->queue); + free(script); + return NULL; + } + script->lua = luaL_newstate(); + if (!script->lua) goto fail; + luaL_openlibs(script->lua); + sandbox(script->lua); + install_api(script); + if (luaL_loadfile(script->lua, script_path) != LUA_OK || + lua_pcall(script->lua, 0, 0, 0) != LUA_OK) { + set_last_error(script, lua_tostring(script->lua, -1)); + if (error) *error = NAUT_ERR_PROTO; + goto fail; + } + if (naut_event_subscribe(events, queue_event, script, + &script->subscription) != NAUT_OK) + goto fail; + if (pthread_create(&script->thread, NULL, script_worker, script) != 0) { + naut_event_unsubscribe(events, script->subscription); + script->subscription = 0; + goto fail; + } + if (error) *error = NAUT_OK; + return script; + +fail: + if (error && *error == NAUT_ERR_INVAL) *error = NAUT_ERR_NOMEM; + if (script->lua) lua_close(script->lua); + pthread_cond_destroy(&script->ready); + pthread_mutex_destroy(&script->lock); + free(script->queue); + free(script); + return NULL; +} + +void naut_script_destroy(naut_script *script) { + if (!script) return; + naut_event_unsubscribe(script->events, script->subscription); + pthread_mutex_lock(&script->lock); + script->stopping = true; + pthread_cond_broadcast(&script->ready); + pthread_mutex_unlock(&script->lock); + pthread_join(script->thread, NULL); + lua_close(script->lua); + pthread_cond_destroy(&script->ready); + pthread_mutex_destroy(&script->lock); + free(script->queue); + free(script); +} + +void naut_script_get_stats(const naut_script *script, + naut_script_stats *stats) { + if (!script || !stats) return; + *stats = (naut_script_stats) { + .queued = atomic_load_explicit(&script->queued, memory_order_relaxed), + .handled = atomic_load_explicit(&script->handled, + memory_order_relaxed), + .dropped = atomic_load_explicit(&script->dropped, + memory_order_relaxed), + .errors = atomic_load_explicit(&script->errors, memory_order_relaxed), + .move_requests = atomic_load_explicit(&script->move_requests, + memory_order_relaxed), + }; +} + +const char *naut_script_last_error(naut_script *script) { + if (!script) return ""; + pthread_mutex_lock(&script->lock); + static _Thread_local char copy[256]; + snprintf(copy, sizeof copy, "%s", script->last_error); + pthread_mutex_unlock(&script->lock); + return copy; +} diff --git a/src/session/event.c b/src/session/event.c new file mode 100644 index 0000000..eab2274 --- /dev/null +++ b/src/session/event.c @@ -0,0 +1,111 @@ +#include "naut/event.h" + +#include +#include +#include + +typedef struct { + uint64_t id; + naut_event_cb callback; + void *context; +} subscriber; + +struct naut_event_bus { + pthread_mutex_t lock; + subscriber *subscribers; + size_t count; + size_t capacity; + uint64_t next_id; +}; + +naut_event_bus *naut_event_bus_create(void) { + naut_event_bus *bus = calloc(1, sizeof(*bus)); + if (!bus) return NULL; + if (pthread_mutex_init(&bus->lock, NULL) != 0) { + free(bus); + return NULL; + } + bus->next_id = 1; + return bus; +} + +void naut_event_bus_destroy(naut_event_bus *bus) { + if (!bus) return; + pthread_mutex_destroy(&bus->lock); + free(bus->subscribers); + free(bus); +} + +naut_err naut_event_subscribe(naut_event_bus *bus, naut_event_cb callback, + void *context, uint64_t *subscription_id) { + if (!bus || !callback) return NAUT_ERR_INVAL; + pthread_mutex_lock(&bus->lock); + if (bus->count == bus->capacity) { + size_t capacity = bus->capacity ? bus->capacity * 2 : 8; + subscriber *next = + realloc(bus->subscribers, capacity * sizeof(*next)); + if (!next) { + pthread_mutex_unlock(&bus->lock); + return NAUT_ERR_NOMEM; + } + bus->subscribers = next; + bus->capacity = capacity; + } + uint64_t id = bus->next_id++; + bus->subscribers[bus->count++] = (subscriber) { + .id = id, + .callback = callback, + .context = context, + }; + pthread_mutex_unlock(&bus->lock); + if (subscription_id) *subscription_id = id; + return NAUT_OK; +} + +void naut_event_unsubscribe(naut_event_bus *bus, uint64_t subscription_id) { + if (!bus || subscription_id == 0) return; + pthread_mutex_lock(&bus->lock); + for (size_t i = 0; i < bus->count; i++) { + if (bus->subscribers[i].id != subscription_id) continue; + bus->subscribers[i] = bus->subscribers[--bus->count]; + break; + } + pthread_mutex_unlock(&bus->lock); +} + +void naut_event_emit(naut_event_bus *bus, const naut_event *event) { + if (!bus || !event) return; + pthread_mutex_lock(&bus->lock); + size_t count = bus->count; + subscriber *snapshot = + count ? malloc(count * sizeof(*snapshot)) : NULL; + if (snapshot) memcpy(snapshot, bus->subscribers, count * sizeof(*snapshot)); + pthread_mutex_unlock(&bus->lock); + if (count && !snapshot) return; + for (size_t i = 0; i < count; i++) + snapshot[i].callback(snapshot[i].context, event); + free(snapshot); +} + +const char *naut_event_type_name(naut_event_type type) { + static const char *names[] = { + "torrent_added", + "piece_complete", + "file_complete", + "torrent_finished", + "peer_connected", + "alert", + }; + return type < NAUT_ARRAY_LEN(names) ? names[type] : "unknown"; +} + +bool naut_event_type_parse(const char *name, naut_event_type *type) { + if (!name || !type) return false; + for (int i = NAUT_EVENT_TORRENT_ADDED; i <= NAUT_EVENT_ALERT; i++) { + if (strcmp(name, naut_event_type_name((naut_event_type)i)) == 0) { + *type = (naut_event_type)i; + return true; + } + } + return false; +} diff --git a/src/session/session.c b/src/session/session.c new file mode 100644 index 0000000..66e98a8 --- /dev/null +++ b/src/session/session.c @@ -0,0 +1,71 @@ +#include "naut/session.h" + +#include + +typedef struct { + uint64_t id; + naut_storage *storage; +} entry; + +struct naut_session { + entry *entries; + size_t count; + size_t capacity; +}; + +naut_session *naut_session_create(void) { + return calloc(1, sizeof(naut_session)); +} + +void naut_session_destroy(naut_session *s) { + if (!s) return; + for (size_t i = 0; i < s->count; i++) + naut_storage_close(s->entries[i].storage); + free(s->entries); + free(s); +} + +static entry *find(const naut_session *s, uint64_t id) { + for (size_t i = 0; i < s->count; i++) + if (s->entries[i].id == id) return &s->entries[i]; + return NULL; +} + +naut_err naut_session_add(naut_session *s, uint64_t id, naut_storage *storage) { + if (!s || !storage) return NAUT_ERR_INVAL; + if (find(s, id)) return NAUT_ERR_INVAL; + if (s->count == s->capacity) { + size_t capacity = s->capacity ? s->capacity * 2 : 8; + entry *next = realloc(s->entries, capacity * sizeof(*next)); + if (!next) return NAUT_ERR_NOMEM; + s->entries = next; + s->capacity = capacity; + } + s->entries[s->count++] = (entry){ .id = id, .storage = storage }; + return NAUT_OK; +} + +naut_err naut_session_remove(naut_session *s, uint64_t id) { + if (!s) return NAUT_ERR_INVAL; + entry *e = find(s, id); + if (!e) return NAUT_ERR_NOTFOUND; + naut_storage_close(e->storage); + *e = s->entries[--s->count]; + return NAUT_OK; +} + +bool naut_session_has(const naut_session *s, uint64_t id) { + return s && find(s, id); +} + +size_t naut_session_count(const naut_session *s) { + return s ? s->count : 0; +} + +naut_err naut_session_move_file(naut_session *s, uint64_t id, + uint32_t file_index, const char *dest) { + if (!s || !dest) return NAUT_ERR_INVAL; + entry *e = find(s, id); + if (!e) return NAUT_ERR_NOTFOUND; + return naut_storage_relocate(e->storage, file_index, dest); +} diff --git a/src/storage/storage.c b/src/storage/storage.c new file mode 100644 index 0000000..23b137f --- /dev/null +++ b/src/storage/storage.c @@ -0,0 +1,237 @@ +#include "naut/storage.h" +#include "naut/log.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +typedef struct { + int fd; + int direct_fd; + int64_t start; /* global offset of this file's first byte */ + int64_t length; + char *path; /* full on-disk path (for relocate) */ + bool externalized; /* moved out; region no longer backed here */ +} file_slot; + +struct naut_storage { + file_slot *files; + size_t nfiles; + int64_t total; + bool direct_enabled; +}; + +/* mkdir -p for the directory portion of `path` (path includes the filename). */ +static naut_err make_parents(char *path) { + for (char *p = strchr(path + 1, '/'); p; p = strchr(p + 1, '/')) { + *p = 0; + if (mkdir(path, 0777) != 0 && errno != EEXIST) { *p = '/'; return NAUT_ERR_IO; } + *p = '/'; + } + return NAUT_OK; +} + +naut_storage *naut_storage_open(const naut_file *files, size_t nfiles, + const char *root, naut_err *err) { + const naut_storage_opts opts = { + .direct_io = false, + .preallocate = true, + }; + return naut_storage_open_opts(files, nfiles, root, &opts, err); +} + +naut_storage *naut_storage_open_opts(const naut_file *files, size_t nfiles, + const char *root, + const naut_storage_opts *opts, + naut_err *err) { + if (!files || nfiles == 0 || !root || !opts) { + if (err) *err = NAUT_ERR_INVAL; + return NULL; + } + naut_storage *s = calloc(1, sizeof(*s)); + if (!s) { if (err) *err = NAUT_ERR_NOMEM; return NULL; } + s->files = calloc(nfiles, sizeof(file_slot)); + if (!s->files) { free(s); if (err) *err = NAUT_ERR_NOMEM; return NULL; } + + int64_t off = 0; + for (size_t i = 0; i < nfiles; i++) { + s->files[i].fd = -1; + s->files[i].direct_fd = -1; + char path[4096]; + int n = snprintf(path, sizeof path, "%s/%s", root, files[i].path); + if (n < 0 || n >= (int)sizeof path) goto fail_io; + + if (make_parents(path) != NAUT_OK) goto fail_io; + int fd = open(path, O_RDWR | O_CREAT, 0666); + if (fd < 0) { NAUT_ERROR("open %s: %s", path, strerror(errno)); goto fail_io; } + if (ftruncate(fd, files[i].length) != 0) { + NAUT_ERROR("ftruncate %s: %s", path, strerror(errno)); + close(fd); goto fail_io; + } + if (opts->preallocate && files[i].length > 0) { + int rc = posix_fallocate(fd, 0, files[i].length); + if (rc != 0 && rc != EOPNOTSUPP && rc != ENOSYS) + NAUT_WARN("preallocate %s: %s", path, strerror(rc)); + } + s->files[i].fd = fd; +#ifdef O_DIRECT + if (opts->direct_io && files[i].length > 0) { + int direct_fd = open(path, O_RDWR | O_DIRECT); + if (direct_fd >= 0) { + s->files[i].direct_fd = direct_fd; + s->direct_enabled = true; + } else { + NAUT_WARN("O_DIRECT unavailable for %s: %s", path, + strerror(errno)); + } + } +#endif + s->files[i].start = off; + s->files[i].length = files[i].length; + s->files[i].path = strdup(path); + off += files[i].length; + s->nfiles++; + } + s->total = off; + if (opts->direct_io) + NAUT_INFO("storage: O_DIRECT %s with buffered edge fallback", + s->direct_enabled ? "enabled" : "unavailable"); + if (err) *err = NAUT_OK; + return s; + +fail_io: + naut_storage_close(s); + if (err) *err = NAUT_ERR_IO; + return NULL; +} + +void naut_storage_close(naut_storage *s) { + if (!s) return; + for (size_t i = 0; i < s->nfiles; i++) { + if (s->files[i].direct_fd >= 0) close(s->files[i].direct_fd); + if (s->files[i].fd >= 0) close(s->files[i].fd); + free(s->files[i].path); + } + free(s->files); + free(s); +} + +/* binary search for the file containing global offset */ +static const file_slot *locate(const naut_storage *s, int64_t off) { + size_t lo = 0, hi = s->nfiles; + while (lo < hi) { + size_t mid = (lo + hi) / 2; + const file_slot *f = &s->files[mid]; + if (off < f->start) hi = mid; + else if (off >= f->start + f->length) lo = mid + 1; + else return f; + } + return NULL; +} + +static naut_err io_at(naut_storage *s, int64_t offset, void *buf, size_t len, bool write) { + if (offset < 0 || (int64_t)(offset + (int64_t)len) > s->total) return NAUT_ERR_RANGE; + uint8_t *p = buf; + while (len > 0) { + const file_slot *f = locate(s, offset); + if (!f) return NAUT_ERR_RANGE; /* zero-length file region */ + if (f->externalized) return NAUT_ERR_RANGE; /* moved out; not backed here */ + off_t fo = (off_t)(offset - f->start); + size_t chunk = len; + int64_t avail = f->length - fo; + if ((int64_t)chunk > avail) chunk = (size_t)avail; + if (chunk == 0) { offset++; continue; } /* skip past empty file */ + + bool aligned = f->direct_fd >= 0 && + ((uintptr_t)p & (NAUT_PAGE - 1)) == 0 && + ((uint64_t)fo & (NAUT_PAGE - 1)) == 0 && + (chunk & (NAUT_PAGE - 1)) == 0; + int io_fd = aligned ? f->direct_fd : f->fd; + ssize_t done = write ? pwrite(io_fd, p, chunk, fo) + : pread(io_fd, p, chunk, fo); + if (done < 0) { + if (errno == EINTR) continue; + return NAUT_ERR_IO; + } + if (done == 0 && !write) return NAUT_ERR_IO; /* unexpected EOF */ + p += done; offset += done; len -= (size_t)done; + } + return NAUT_OK; +} + +naut_err naut_storage_write(naut_storage *s, int64_t offset, const void *buf, size_t len) { + return io_at(s, offset, (void *)buf, len, true); +} +naut_err naut_storage_read(naut_storage *s, int64_t offset, void *buf, size_t len) { + return io_at(s, offset, buf, len, false); +} + +static naut_err copy_file(const char *src, const char *dst) { + int in = open(src, O_RDONLY); + if (in < 0) return NAUT_ERR_IO; + int out = open(dst, O_WRONLY | O_CREAT | O_TRUNC, 0666); + if (out < 0) { close(in); return NAUT_ERR_IO; } + naut_err e = NAUT_OK; + char buf[1 << 16]; + for (;;) { + ssize_t r = read(in, buf, sizeof buf); + if (r < 0) { if (errno == EINTR) continue; e = NAUT_ERR_IO; break; } + if (r == 0) break; + for (ssize_t off = 0; off < r; ) { + ssize_t w = write(out, buf + off, (size_t)(r - off)); + if (w < 0) { if (errno == EINTR) continue; e = NAUT_ERR_IO; goto done; } + off += w; + } + } +done: + close(in); close(out); + return e; +} + +naut_err naut_storage_relocate(naut_storage *s, size_t file_index, const char *dest) { + if (file_index >= s->nfiles) return NAUT_ERR_RANGE; + file_slot *f = &s->files[file_index]; + if (f->externalized) return NAUT_ERR_INVAL; + + if (f->direct_fd >= 0) { + fsync(f->direct_fd); + close(f->direct_fd); + f->direct_fd = -1; + } + if (f->fd >= 0) { fsync(f->fd); close(f->fd); f->fd = -1; } + + /* ensure the destination directory exists */ + char dcopy[4096]; + if ((size_t)snprintf(dcopy, sizeof dcopy, "%s", dest) >= sizeof dcopy) return NAUT_ERR_INVAL; + if (make_parents(dcopy) != NAUT_OK) return NAUT_ERR_IO; + + if (rename(f->path, dest) != 0) { + if (errno != EXDEV) { NAUT_ERROR("rename %s -> %s: %s", f->path, dest, strerror(errno)); return NAUT_ERR_IO; } + naut_err e = copy_file(f->path, dest); /* cross-filesystem */ + if (e != NAUT_OK) return e; + if (unlink(f->path) != 0) NAUT_WARN("unlink %s after copy: %s", f->path, strerror(errno)); + } + f->externalized = true; + NAUT_INFO("relocated file %zu -> %s", file_index, dest); + return NAUT_OK; +} + +naut_err naut_storage_sync(naut_storage *s) { + for (size_t i = 0; i < s->nfiles; i++) + if ((s->files[i].direct_fd >= 0 && + fsync(s->files[i].direct_fd) != 0) || + (s->files[i].fd >= 0 && + fsync(s->files[i].fd) != 0)) + return NAUT_ERR_IO; + return NAUT_OK; +} + +int64_t naut_storage_total(const naut_storage *s) { return s->total; } +bool naut_storage_direct_enabled(const naut_storage *s) { + return s && s->direct_enabled; +} diff --git a/src/tracker/fetch.c b/src/tracker/fetch.c new file mode 100644 index 0000000..ac52faa --- /dev/null +++ b/src/tracker/fetch.c @@ -0,0 +1,162 @@ +#include "naut/tracker.h" +#include "naut/log.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define TRACKER_RESPONSE_MAX (16u << 20) + +static int dial(const char *host, const char *port, int socktype) { + struct addrinfo hints, *res = NULL, *ai; + memset(&hints, 0, sizeof hints); + hints.ai_family = AF_INET; /* IPv4 for now (compact peers are v4) */ + hints.ai_socktype = socktype; + if (getaddrinfo(host, port, &hints, &res) != 0) return -1; + int fd = -1; + for (ai = res; ai; ai = ai->ai_next) { + fd = socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol); + if (fd < 0) continue; + struct timeval tv = { .tv_sec = 10, .tv_usec = 0 }; + setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof tv); + if (connect(fd, ai->ai_addr, ai->ai_addrlen) == 0) break; + close(fd); fd = -1; + } + freeaddrinfo(res); + return fd; +} + +/* split "http://host[:port]/path" */ +static bool parse_http_url(const char *url, char *host, size_t hostsz, + char *port, size_t portsz, const char **path) { + if (strncmp(url, "http://", 7) != 0) return false; + const char *h = url + 7; + const char *slash = strchr(h, '/'); + const char *hostend = slash ? slash : h + strlen(h); + const char *colon = memchr(h, ':', (size_t)(hostend - h)); + size_t hlen = colon ? (size_t)(colon - h) : (size_t)(hostend - h); + if (hlen >= hostsz) return false; + memcpy(host, h, hlen); host[hlen] = 0; + if (colon) { + size_t plen = (size_t)(hostend - colon - 1); + if (plen >= portsz) return false; + memcpy(port, colon + 1, plen); port[plen] = 0; + } else { snprintf(port, portsz, "80"); } + *path = slash ? slash : "/"; + return true; +} + +static bool write_all(int fd, const void *data, size_t len) { + const uint8_t *p = data; + while (len) { + ssize_t n = write(fd, p, len); + if (n < 0) { + if (errno == EINTR) continue; + return false; + } + p += (size_t)n; + len -= (size_t)n; + } + return true; +} + +naut_err naut_tracker_announce_http(const char *url, naut_tracker_response *out) { + char host[256], port[16]; const char *path; + if (!parse_http_url(url, host, sizeof host, port, sizeof port, &path)) + return NAUT_ERR_INVAL; + int fd = dial(host, port, SOCK_STREAM); + if (fd < 0) { NAUT_WARN("tracker connect %s:%s failed", host, port); return NAUT_ERR_IO; } + + char req[2048]; + int rn = snprintf(req, sizeof req, + "GET %s HTTP/1.0\r\nHost: %s\r\nUser-Agent: Naut/0.1\r\nAccept: */*\r\n\r\n", + path, host); + if (rn < 0 || (size_t)rn >= sizeof req || + !write_all(fd, req, (size_t)rn)) { + close(fd); + return NAUT_ERR_IO; + } + + /* read whole response (server closes on HTTP/1.0) */ + size_t cap = 1 << 16, len = 0; + uint8_t *buf = malloc(cap); + if (!buf) { close(fd); return NAUT_ERR_NOMEM; } + naut_err read_error = NAUT_OK; + for (;;) { + if (len == cap) { + if (cap == TRACKER_RESPONSE_MAX) { + read_error = NAUT_ERR_FULL; + break; + } + size_t next_cap = NAUT_MIN(cap * 2, (size_t)TRACKER_RESPONSE_MAX); + uint8_t *next = realloc(buf, next_cap); + if (!next) { + read_error = NAUT_ERR_NOMEM; + break; + } + buf = next; + cap = next_cap; + } + ssize_t r = read(fd, buf + len, cap - len); + if (r < 0) { + if (errno == EINTR) continue; + read_error = NAUT_ERR_IO; + break; + } + if (r == 0) break; + len += (size_t)r; + } + close(fd); + if (read_error != NAUT_OK) { + free(buf); + return read_error; + } + + /* find body after CRLFCRLF */ + uint8_t *body = NULL; size_t blen = 0; + for (size_t i = 0; i + 3 < len; i++) + if (buf[i]=='\r'&&buf[i+1]=='\n'&&buf[i+2]=='\r'&&buf[i+3]=='\n') { + body = buf + i + 4; blen = len - (i + 4); break; + } + bool success = len >= 12 && memcmp(buf, "HTTP/", 5) == 0 && + buf[9] == '2'; + naut_err e = success && body + ? naut_tracker_parse_http(body, blen, out) + : NAUT_ERR_PROTO; + free(buf); + return e; +} + +naut_err naut_tracker_announce_udp(const char *host, uint16_t port, + const naut_announce_req *req, + naut_tracker_response *out) { + char portstr[16]; snprintf(portstr, sizeof portstr, "%u", port); + int fd = dial(host, portstr, SOCK_DGRAM); + if (fd < 0) return NAUT_ERR_IO; + + srand((unsigned)time(NULL) ^ (unsigned)getpid()); + uint32_t txid = (uint32_t)rand(); + + uint8_t pkt[98], resp[1500]; + naut_udp_build_connect(pkt, txid); + if (write(fd, pkt, 16) != 16) { close(fd); return NAUT_ERR_IO; } + ssize_t r = read(fd, resp, sizeof resp); + uint64_t cid; + if (r < 0 || naut_udp_parse_connect(resp, (size_t)r, txid, &cid) != NAUT_OK) { + close(fd); return NAUT_ERR_IO; + } + txid++; + naut_udp_build_announce(pkt, cid, txid, req); + if (write(fd, pkt, 98) != 98) { close(fd); return NAUT_ERR_IO; } + r = read(fd, resp, sizeof resp); + naut_err e = (r < 0) ? NAUT_ERR_IO + : naut_udp_parse_announce(resp, (size_t)r, txid, out); + close(fd); + return e; +} diff --git a/src/tracker/tracker.c b/src/tracker/tracker.c new file mode 100644 index 0000000..cf6be74 --- /dev/null +++ b/src/tracker/tracker.c @@ -0,0 +1,120 @@ +#include "naut/tracker.h" +#include "naut/bencode.h" + +#include +#include +#include + +void naut_tracker_response_free(naut_tracker_response *r) { + free(r->peers); r->peers = NULL; r->num_peers = 0; + free(r->failure); r->failure = NULL; +} + +/* percent-encode raw bytes per RFC 3986 (unreserved chars pass through) */ +static size_t pct_encode(const uint8_t *in, size_t n, char *out, size_t outsz) { + static const char hx[] = "0123456789ABCDEF"; + size_t o = 0; + for (size_t i = 0; i < n; i++) { + uint8_t c = in[i]; + bool unreserved = (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || + (c >= '0' && c <= '9') || c == '-' || c == '_' || + c == '.' || c == '~'; + if (unreserved) { + if (o + 1 >= outsz) return 0; + out[o++] = (char)c; + } else { + if (o + 3 >= outsz) return 0; + out[o++] = '%'; out[o++] = hx[c >> 4]; out[o++] = hx[c & 15]; + } + } + return o; +} + +size_t naut_tracker_http_url(const char *base, const naut_announce_req *req, + char *out, size_t outsz) { + static const char *ev[] = { "", "completed", "started", "stopped" }; + if (req->event < NAUT_TEV_NONE || req->event > NAUT_TEV_STOPPED) return 0; + char ih[61], pid[61]; /* 20*3 = 60 worst case + NUL */ + size_t ihn = pct_encode(req->info_hash, 20, ih, sizeof ih); + size_t pidn = pct_encode(req->peer_id, 20, pid, sizeof pid); + if (!ihn || !pidn) return 0; + ih[ihn] = 0; pid[pidn] = 0; + + const char *sep = strchr(base, '?') ? "&" : "?"; + int n = snprintf(out, outsz, + "%s%sinfo_hash=%s&peer_id=%s&port=%u&uploaded=%llu&downloaded=%llu" + "&left=%llu&compact=1&numwant=%d%s%s&key=%u", + base, sep, ih, pid, req->port, + (unsigned long long)req->uploaded, (unsigned long long)req->downloaded, + (unsigned long long)req->left, req->numwant < 0 ? 50 : req->numwant, + req->event ? "&event=" : "", ev[req->event], req->key); + if (n < 0 || (size_t)n >= outsz) return 0; + return (size_t)n; +} + +static naut_err parse_peers(const naut_bc *peers, naut_tracker_response *out) { + const uint8_t *p; size_t n; + if (naut_bc_get_str(peers, &p, &n)) { /* compact: 6 bytes each */ + if (n % 6 != 0) return NAUT_ERR_PROTO; + out->num_peers = n / 6; + out->peers = calloc(out->num_peers ? out->num_peers : 1, sizeof(naut_peer_addr)); + if (!out->peers) return NAUT_ERR_NOMEM; + for (size_t i = 0; i < out->num_peers; i++) { + memcpy(out->peers[i].ip, p + i*6, 4); + out->peers[i].port = ((uint16_t)p[i*6+4] << 8) | p[i*6+5]; + } + return NAUT_OK; + } + if (peers && peers->type == NAUT_BC_LIST) { /* dict form */ + out->peers = calloc(peers->v.list.count ? peers->v.list.count : 1, sizeof(naut_peer_addr)); + if (!out->peers) return NAUT_ERR_NOMEM; + for (size_t i = 0; i < peers->v.list.count; i++) { + const naut_bc *pe = naut_bc_list_at(peers, i); + const uint8_t *ips; size_t ipn; int64_t port; + if (!naut_bc_get_str(naut_bc_dict_get(pe, "ip"), &ips, &ipn)) continue; + if (!naut_bc_get_int(naut_bc_dict_get(pe, "port"), &port)) continue; + unsigned a, b, c, dd; + char tmp[64]; + if (ipn >= sizeof tmp) continue; + memcpy(tmp, ips, ipn); tmp[ipn] = 0; + if (sscanf(tmp, "%u.%u.%u.%u", &a, &b, &c, &dd) != 4) continue; + if (a > 255 || b > 255 || c > 255 || dd > 255 || + port <= 0 || port > UINT16_MAX) continue; + naut_peer_addr *pa = &out->peers[out->num_peers++]; + pa->ip[0]=(uint8_t)a; pa->ip[1]=(uint8_t)b; pa->ip[2]=(uint8_t)c; pa->ip[3]=(uint8_t)dd; + pa->port = (uint16_t)port; + } + return NAUT_OK; + } + return NAUT_ERR_PROTO; +} + +naut_err naut_tracker_parse_http(const uint8_t *body, size_t len, + naut_tracker_response *out) { + memset(out, 0, sizeof(*out)); + out->seeders = out->leechers = -1; + naut_bc_doc *doc = NULL; + naut_err e = naut_bc_parse(body, len, &doc); + if (e != NAUT_OK) return e; + const naut_bc *root = naut_bc_root(doc); + + const uint8_t *fp; size_t fn; + if (naut_bc_get_str(naut_bc_dict_get(root, "failure reason"), &fp, &fn)) { + out->failure = malloc(fn + 1); + if (out->failure) { memcpy(out->failure, fp, fn); out->failure[fn] = 0; } + naut_bc_free(doc); + return NAUT_ERR_PROTO; /* tracker reported failure */ + } + + int64_t iv = 0; + naut_bc_get_int(naut_bc_dict_get(root, "interval"), &iv); + out->interval = (int32_t)iv; + int64_t sc; + if (naut_bc_get_int(naut_bc_dict_get(root, "complete"), &sc)) out->seeders = (int32_t)sc; + if (naut_bc_get_int(naut_bc_dict_get(root, "incomplete"), &sc)) out->leechers = (int32_t)sc; + + e = parse_peers(naut_bc_dict_get(root, "peers"), out); + naut_bc_free(doc); + if (e != NAUT_OK) { naut_tracker_response_free(out); return e; } + return NAUT_OK; +} diff --git a/src/tracker/udp.c b/src/tracker/udp.c new file mode 100644 index 0000000..adb7c26 --- /dev/null +++ b/src/tracker/udp.c @@ -0,0 +1,81 @@ +#include "naut/tracker.h" +#include +#include + +#define UDP_PROTOCOL_ID 0x41727101980ULL /* BEP-15 magic */ +#define ACTION_CONNECT 0 +#define ACTION_ANNOUNCE 1 +#define ACTION_ERROR 3 + +static void wr16(uint8_t *p, uint16_t v) { p[0]=(uint8_t)(v>>8); p[1]=(uint8_t)v; } +static void wr32(uint8_t *p, uint32_t v) { + p[0]=(uint8_t)(v>>24); p[1]=(uint8_t)(v>>16); p[2]=(uint8_t)(v>>8); p[3]=(uint8_t)v; +} +static void wr64(uint8_t *p, uint64_t v) { wr32(p, (uint32_t)(v>>32)); wr32(p+4, (uint32_t)v); } +static uint32_t rd32(const uint8_t *p) { + return ((uint32_t)p[0]<<24)|((uint32_t)p[1]<<16)|((uint32_t)p[2]<<8)|p[3]; +} +static uint64_t rd64(const uint8_t *p) { return ((uint64_t)rd32(p)<<32) | rd32(p+4); } + +void naut_udp_build_connect(uint8_t out[16], uint32_t txid) { + wr64(out, UDP_PROTOCOL_ID); + wr32(out + 8, ACTION_CONNECT); + wr32(out + 12, txid); +} + +naut_err naut_udp_parse_connect(const uint8_t *in, size_t len, uint32_t txid, + uint64_t *connection_id) { + if (len < 16) return NAUT_ERR_PROTO; + if (rd32(in) != ACTION_CONNECT) return NAUT_ERR_PROTO; + if (rd32(in + 4) != txid) return NAUT_ERR_PROTO; + *connection_id = rd64(in + 8); + return NAUT_OK; +} + +void naut_udp_build_announce(uint8_t out[98], uint64_t connection_id, + uint32_t txid, const naut_announce_req *req) { + wr64(out + 0, connection_id); + wr32(out + 8, ACTION_ANNOUNCE); + wr32(out + 12, txid); + memcpy(out + 16, req->info_hash, 20); + memcpy(out + 36, req->peer_id, 20); + wr64(out + 56, req->downloaded); + wr64(out + 64, req->left); + wr64(out + 72, req->uploaded); + wr32(out + 80, (uint32_t)req->event); + wr32(out + 84, 0); /* IP: 0 = source */ + wr32(out + 88, req->key); + wr32(out + 92, (uint32_t)(req->numwant < 0 ? 50 : req->numwant)); + wr16(out + 96, req->port); +} + +naut_err naut_udp_parse_announce(const uint8_t *in, size_t len, uint32_t txid, + naut_tracker_response *out) { + memset(out, 0, sizeof(*out)); + out->seeders = out->leechers = -1; + if (len < 8) return NAUT_ERR_PROTO; + uint32_t action = rd32(in); + if (rd32(in + 4) != txid) return NAUT_ERR_PROTO; + if (action == ACTION_ERROR) { + size_t mn = len - 8; + out->failure = malloc(mn + 1); + if (out->failure) { memcpy(out->failure, in + 8, mn); out->failure[mn] = 0; } + return NAUT_ERR_PROTO; + } + if (action != ACTION_ANNOUNCE || len < 20 || (len - 20) % 6 != 0) + return NAUT_ERR_PROTO; + out->interval = (int32_t)rd32(in + 8); + out->leechers = (int32_t)rd32(in + 12); + out->seeders = (int32_t)rd32(in + 16); + + size_t avail = (len - 20) / 6; + out->peers = calloc(avail ? avail : 1, sizeof(naut_peer_addr)); + if (!out->peers) return NAUT_ERR_NOMEM; + for (size_t i = 0; i < avail; i++) { + const uint8_t *p = in + 20 + i*6; + memcpy(out->peers[i].ip, p, 4); + out->peers[i].port = ((uint16_t)p[4] << 8) | p[5]; + } + out->num_peers = avail; + return NAUT_OK; +} diff --git a/tests/bench/bench_hash.c b/tests/bench/bench_hash.c new file mode 100644 index 0000000..e2eb3a2 --- /dev/null +++ b/tests/bench/bench_hash.c @@ -0,0 +1,50 @@ +/* Hash throughput bench — the Phase 2 gate (SHA-256 >= 1.5 GB/s/core). */ +#include "naut/hash.h" +#include +#include +#include +#include + +static double now(void) { + struct timespec t; clock_gettime(CLOCK_MONOTONIC, &t); + return t.tv_sec + t.tv_nsec * 1e-9; +} + +static double bench(const char *name, void (*h)(const void *, size_t, uint8_t *), + uint8_t *buf, size_t len, int iters, int outlen) { + uint8_t out[32]; + /* warm */ + h(buf, len, out); + double t0 = now(); + for (int i = 0; i < iters; i++) h(buf, len, out); + double dt = now() - t0; + double gb = (double)len * iters / 1e9; + printf(" %-10s %6.2f GB/s (%d-byte digest)\n", name, gb / dt, outlen); + return gb / dt; +} + +static void s1(const void *d, size_t n, uint8_t *o) { naut_sha1(d, n, o); } +static void s256(const void *d, size_t n, uint8_t *o) { naut_sha256(d, n, o); } + +int main(int argc, char **argv) { + size_t len = (argc > 1) ? (size_t)atoll(argv[1]) * 1024 * 1024 : 256u * 1024 * 1024; + int iters = (argc > 2) ? atoi(argv[2]) : 8; + + uint8_t *buf = malloc(len); + if (!buf) { perror("malloc"); return 1; } + memset(buf, 0xa5, len); + + printf("buffer %zu MiB x %d iters | sha256 backend: %s\n", + len >> 20, iters, naut_sha256_backend()); + double s256_gbs = bench("sha256", s256, buf, len, iters, 32); + bench("sha1", s1, buf, len, iters, 20); + + free(buf); + /* gate: a single core must clear the 1.25 GB/s line rate with margin */ + if (s256_gbs < 1.5) { + fprintf(stderr, "GATE FAIL: sha256 %.2f GB/s < 1.5 GB/s\n", s256_gbs); + return 1; + } + printf("GATE PASS: sha256 %.2f GB/s >= 1.5 GB/s\n", s256_gbs); + return 0; +} diff --git a/tests/bench/bench_scale.c b/tests/bench/bench_scale.c new file mode 100644 index 0000000..ad30e5f --- /dev/null +++ b/tests/bench/bench_scale.c @@ -0,0 +1,114 @@ +/* Multicore hash/RC4 offload benchmark for the Phase 6 CPU budget. */ +#include "naut/hash.h" +#include "naut/rc4.h" +#include "naut/worker.h" + +#include +#include +#include +#include +#include +#include +#include + +#define JOB_BYTES (1u << 20) +#define JOB_COUNT 64 + +typedef enum { BENCH_SHA1, BENCH_SHA256, BENCH_RC4 } bench_kind; + +typedef struct { + naut_job base; + bench_kind kind; + uint8_t *data; + uint8_t digest[NAUT_SHA256_LEN]; +} bench_job; + +static double now_seconds(void) { + struct timespec time; + clock_gettime(CLOCK_MONOTONIC, &time); + return time.tv_sec + time.tv_nsec * 1e-9; +} + +static void run_job(naut_job *base) { + bench_job *job = base->context; + if (job->kind == BENCH_SHA1) { + naut_sha1(job->data, JOB_BYTES, job->digest); + } else if (job->kind == BENCH_SHA256) { + naut_sha256(job->data, JOB_BYTES, job->digest); + } else { + static const uint8_t key[20] = { + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, + 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, + }; + naut_rc4 rc4; + naut_rc4_init(&rc4, key, sizeof key, 1024); + naut_rc4_xor(&rc4, job->data, JOB_BYTES); + } + base->result = NAUT_OK; +} + +static double run(naut_worker_pool *pool, bench_job *jobs, + bench_kind kind, int rounds) { + for (int i = 0; i < JOB_COUNT; i++) jobs[i].kind = kind; + int total_completed = 0; + double start = now_seconds(); + for (int round = 0; round < rounds; round++) { + for (int i = 0; i < JOB_COUNT; i++) { + while (!naut_worker_submit(pool, &jobs[i].base)) + sched_yield(); + } + int completed = 0; + while (completed < JOB_COUNT) { + naut_job *base; + if (naut_worker_complete(pool, &base)) { + (void)base; + completed++; + total_completed++; + continue; + } + struct pollfd pfd = { + .fd = naut_worker_eventfd(pool), + .events = POLLIN, + }; + if (poll(&pfd, 1, 5000) <= 0) break; + uint64_t count; + (void)read(pfd.fd, &count, sizeof count); + } + if (completed != JOB_COUNT) break; + } + double seconds = now_seconds() - start; + return ((double)total_completed * JOB_BYTES / 1e9) / seconds; +} + +int main(int argc, char **argv) { + int threads = argc > 1 ? atoi(argv[1]) : 8; + int rounds = argc > 2 ? atoi(argv[2]) : 16; + if (threads < 1 || rounds < 1) return 2; + naut_worker_pool *pool = + naut_worker_pool_create((uint32_t)threads, 128, -1); + if (!pool) return 1; + + bench_job *jobs = calloc(JOB_COUNT, sizeof(*jobs)); + uint8_t *slab = aligned_alloc(NAUT_PAGE, JOB_COUNT * JOB_BYTES); + if (!jobs || !slab) return 1; + memset(slab, 0xa5, JOB_COUNT * JOB_BYTES); + for (int i = 0; i < JOB_COUNT; i++) { + jobs[i].base.run = run_job; + jobs[i].base.context = &jobs[i]; + jobs[i].data = slab + (size_t)i * JOB_BYTES; + } + + double sha1 = run(pool, jobs, BENCH_SHA1, rounds); + double sha256 = run(pool, jobs, BENCH_SHA256, rounds); + double rc4 = run(pool, jobs, BENCH_RC4, rounds); + printf("%d workers, %.2f GiB processed per primitive\n", + threads, (double)JOB_COUNT * rounds * JOB_BYTES / (1u << 30)); + printf(" sha1 %.2f GB/s\n", sha1); + printf(" sha256 %.2f GB/s\n", sha256); + printf(" rc4 %.2f GB/s\n", rc4); + + free(slab); + free(jobs); + naut_worker_pool_destroy(pool); + return 0; +} diff --git a/tests/fixtures/data/multi/a.txt b/tests/fixtures/data/multi/a.txt new file mode 100644 index 0000000..d37436e --- /dev/null +++ b/tests/fixtures/data/multi/a.txt @@ -0,0 +1 @@ +hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut hello naut \ No newline at end of file diff --git a/tests/fixtures/data/multi/sub/b.dat b/tests/fixtures/data/multi/sub/b.dat new file mode 100644 index 0000000..e48370e Binary files /dev/null and b/tests/fixtures/data/multi/sub/b.dat differ diff --git a/tests/fixtures/data/single.bin b/tests/fixtures/data/single.bin new file mode 100644 index 0000000..8e41798 Binary files /dev/null and b/tests/fixtures/data/single.bin differ diff --git a/tests/fixtures/generate.py b/tests/fixtures/generate.py new file mode 100644 index 0000000..493aaad --- /dev/null +++ b/tests/fixtures/generate.py @@ -0,0 +1,40 @@ +import libtorrent as lt, os, hashlib, shutil + +root = "tests/fixtures" +data = os.path.join(root, "data") +shutil.rmtree(data, ignore_errors=True) +os.makedirs(os.path.join(data, "multi", "sub"), exist_ok=True) + +# deterministic content +with open(os.path.join(data, "single.bin"), "wb") as f: + f.write(bytes((i*131+7) & 0xff for i in range(200000))) +with open(os.path.join(data, "multi", "a.txt"), "wb") as f: + f.write(b"hello naut " * 5000) +with open(os.path.join(data, "multi", "sub", "b.dat"), "wb") as f: + f.write(bytes((i*7) & 0xff for i in range(90000))) + +def make(name, src, flags): + fs = lt.file_storage() + lt.add_files(fs, src) + t = lt.create_torrent(fs, piece_size=16384, flags=flags) + parent = os.path.dirname(src) if os.path.isfile(src) else os.path.dirname(src.rstrip("/")) + t.add_tracker("http://tracker.example.com:8080/announce", 0) + t.add_tracker("udp://tracker.example.com:8080", 1) + lt.set_piece_hashes(t, parent) + ent = t.generate() + blob = lt.bencode(ent) + path = os.path.join(root, name) + with open(path, "wb") as f: f.write(blob) + ti = lt.torrent_info(ent) + ih = ti.info_hashes() + v1 = str(ih.v1) if ih.has_v1() else "-" + v2 = str(ih.v2) if ih.has_v2() else "-" + print(f"{name}\tv1={v1}\tv2={v2}\tpieces={ti.num_pieces()}\tsize={ti.total_size()}") + return path + +V1 = lt.create_torrent.v1_only +V2 = lt.create_torrent.v2_only +make("single_v1.torrent", os.path.join(data, "single.bin"), V1) +make("multi_v1.torrent", os.path.join(data, "multi"), V1) +make("hybrid.torrent", os.path.join(data, "multi"), 0) +make("v2.torrent", os.path.join(data, "multi"), V2) diff --git a/tests/fixtures/hybrid.torrent b/tests/fixtures/hybrid.torrent new file mode 100644 index 0000000..948455b Binary files /dev/null and b/tests/fixtures/hybrid.torrent differ diff --git a/tests/fixtures/multi_v1.torrent b/tests/fixtures/multi_v1.torrent new file mode 100644 index 0000000..d1888e1 --- /dev/null +++ b/tests/fixtures/multi_v1.torrent @@ -0,0 +1 @@ +d8:announce40:http://tracker.example.com:8080/announce13:announce-listll40:http://tracker.example.com:8080/announceel30:udp://tracker.example.com:8080ee13:creation datei1781498759e4:infod5:filesld6:lengthi90000e4:pathl3:sub5:b.dateed6:lengthi55000e4:pathl5:a.txteee4:name5:multi12:piece lengthi16384e6:pieces180:2HGLUJJ(~52HGLUJJ(~52HGLUJJ(~52HGLUJJ(~52HGLUJJ(~5_lqy5уD+~^PLRD$ڄQ-3Bc-R~,{úl1QL!XN*ee \ No newline at end of file diff --git a/tests/fixtures/phase7.lua b/tests/fixtures/phase7.lua new file mode 100644 index 0000000..922cf8f --- /dev/null +++ b/tests/fixtures/phase7.lua @@ -0,0 +1,7 @@ +function on_torrent_finished(event) + naut.move_file(event.torrent_id, 0, "/tmp/naut-phase7-finished") +end + +function on_file_complete(event) + naut.move_file(event.torrent_id, event.index, event.path .. ".moved") +end diff --git a/tests/fixtures/single_v1.torrent b/tests/fixtures/single_v1.torrent new file mode 100644 index 0000000..28d74a2 --- /dev/null +++ b/tests/fixtures/single_v1.torrent @@ -0,0 +1 @@ +d8:announce40:http://tracker.example.com:8080/announce13:announce-listll40:http://tracker.example.com:8080/announceel30:udp://tracker.example.com:8080ee13:creation datei1781498759e4:infod6:lengthi200000e4:name10:single.bin12:piece lengthi16384e6:pieces260: %!8H %!8H %!8H %!8H %!8H %!8H %!8H %!8H %!8H %!8H %!8H %!8HL/(vloGee \ No newline at end of file diff --git a/tests/fixtures/v2.torrent b/tests/fixtures/v2.torrent new file mode 100644 index 0000000..6cefd66 Binary files /dev/null and b/tests/fixtures/v2.torrent differ diff --git a/tests/fuzz/fuzz_bencode.c b/tests/fuzz/fuzz_bencode.c new file mode 100644 index 0000000..585498d --- /dev/null +++ b/tests/fuzz/fuzz_bencode.c @@ -0,0 +1,11 @@ +#include "naut/bencode.h" +/* libFuzzer entry: parse arbitrary bytes; ASan/UBSan catch any memory or UB. */ +int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { + naut_bc_doc *d = NULL; + if (naut_bc_parse(data, size, &d) == NAUT_OK) { + const naut_bc *r = naut_bc_root(d); + (void)naut_bc_dict_get(r, "info"); /* exercise accessors */ + naut_bc_free(d); + } + return 0; +} diff --git a/tests/fuzz/fuzz_lite.c b/tests/fuzz/fuzz_lite.c new file mode 100644 index 0000000..78c19d3 --- /dev/null +++ b/tests/fuzz/fuzz_lite.c @@ -0,0 +1,71 @@ +/* fuzz_lite — dependency-free mutational fuzzer. Seeds from the fixture corpus, + * applies random mutations, and feeds both parsers. Run under -fsanitize= + * address,undefined so any out-of-bounds / UB aborts. Not a replacement for + * libFuzzer coverage-guidance, but it exercises the hostile-input paths hard. + * + * usage: fuzz_lite [seedfile ...] + */ +#include "naut/bencode.h" +#include "naut/metainfo.h" +#include +#include +#include + +static void run_one(int meta, const uint8_t *p, size_t n) { + if (meta) { + naut_metainfo mi; + if (naut_metainfo_parse(p, n, &mi) == NAUT_OK) naut_metainfo_free(&mi); + } else { + naut_bc_doc *d = NULL; + if (naut_bc_parse(p, n, &d) == NAUT_OK) { + (void)naut_bc_dict_get(naut_bc_root(d), "info"); + naut_bc_free(d); + } + } +} + +int main(int argc, char **argv) { + if (argc < 3) { fprintf(stderr, "usage: %s [seed..]\n", argv[0]); return 2; } + int meta = strcmp(argv[1], "metainfo") == 0; + long iters = atol(argv[2]); + + /* load seeds */ + uint8_t *seed[16]; size_t seedlen[16]; int nseed = 0; + for (int i = 3; i < argc && nseed < 16; i++) { + FILE *f = fopen(argv[i], "rb"); if (!f) continue; + fseek(f, 0, SEEK_END); long sz = ftell(f); fseek(f, 0, SEEK_SET); + seed[nseed] = malloc(sz ? sz : 1); + if (fread(seed[nseed], 1, sz, f) == (size_t)sz) { seedlen[nseed] = sz; nseed++; } + fclose(f); + } + + srand(1234); + size_t cap = 1 << 20; + uint8_t *buf = malloc(cap); + + for (long it = 0; it < iters; it++) { + size_t n; + if (nseed && (rand() & 3)) { /* mutate a seed */ + int s = rand() % nseed; + n = seedlen[s]; + if (n > cap) n = cap; + memcpy(buf, seed[s], n); + int muts = 1 + rand() % 16; + for (int m = 0; m < muts && n; m++) { + int op = rand() % 3; + if (op == 0) buf[rand() % n] ^= (uint8_t)(1 << (rand() & 7)); /* bit flip */ + else if (op == 1) buf[rand() % n] = (uint8_t)rand(); /* byte set */ + else n = rand() % (n + 1); /* truncate */ + } + } else { /* pure random */ + n = rand() % 4096; + for (size_t i = 0; i < n; i++) buf[i] = (uint8_t)rand(); + } + run_one(meta, buf, n); + } + + printf("fuzz_lite %s: %ld iterations clean\n", argv[1], iters); + free(buf); + for (int i = 0; i < nseed; i++) free(seed[i]); + return 0; +} diff --git a/tests/fuzz/fuzz_metainfo.c b/tests/fuzz/fuzz_metainfo.c new file mode 100644 index 0000000..c6efdf4 --- /dev/null +++ b/tests/fuzz/fuzz_metainfo.c @@ -0,0 +1,7 @@ +#include "naut/metainfo.h" +/* libFuzzer entry: parse arbitrary bytes as a .torrent. */ +int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { + naut_metainfo mi; + if (naut_metainfo_parse(data, size, &mi) == NAUT_OK) naut_metainfo_free(&mi); + return 0; +} diff --git a/tests/integration/dht_fixture.py b/tests/integration/dht_fixture.py new file mode 100644 index 0000000..79efb43 --- /dev/null +++ b/tests/integration/dht_fixture.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python3 +"""Minimal BEP-5 get_peers responder for the trackerless magnet gate.""" +import socket +import struct +import sys + +peer_port = int(sys.argv[1]) +sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) +sock.bind(("127.0.0.1", 0)) +print("DHT_PORT %d" % sock.getsockname()[1], flush=True) + +node_id = bytes(range(20)) +compact_peer = socket.inet_aton("127.0.0.1") + struct.pack("!H", peer_port) + +while True: + packet, address = sock.recvfrom(65535) + marker = b"1:t" + start = packet.find(marker) + if start < 0: + continue + length_start = start + len(marker) + colon = packet.find(b":", length_start) + if colon < 0: + continue + try: + tx_len = int(packet[length_start:colon]) + except ValueError: + continue + tx = packet[colon + 1:colon + 1 + tx_len] + if len(tx) != tx_len: + continue + response = ( + b"d1:rd2:id20:" + node_id + + b"6:valuesl6:" + compact_peer + + b"ee1:t" + str(tx_len).encode() + b":" + tx + + b"1:y1:re" + ) + sock.sendto(response, address) diff --git a/tests/integration/echo_client.py b/tests/integration/echo_client.py new file mode 100644 index 0000000..58210f3 --- /dev/null +++ b/tests/integration/echo_client.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python3 +"""Exercise the io_uring fixed-buffer/SEND_ZC echo path with exact data.""" +import socket +import sys +import threading +import time + +port = int(sys.argv[1]) +total = int(sys.argv[2]) if len(sys.argv) > 2 else 64 * 1024 * 1024 +connections = int(sys.argv[3]) if len(sys.argv) > 3 else 1 +chunk = bytes((i * 31 + 7) & 0xFF for i in range(128 * 1024)) + +start = time.monotonic() +errors = [] + +def connection_worker(connection_bytes): + sock = socket.create_connection(("127.0.0.1", port), timeout=5) + send_error = [] + + def sender(): + try: + sent = 0 + while sent < connection_bytes: + payload = chunk[:min(len(chunk), connection_bytes - sent)] + sock.sendall(payload) + sent += len(payload) + sock.shutdown(socket.SHUT_WR) + except Exception as error: + send_error.append(error) + + thread = threading.Thread(target=sender) + thread.start() + try: + done = 0 + while done < connection_bytes: + part = sock.recv(min(1024 * 1024, connection_bytes - done)) + if not part: + raise RuntimeError("echo server closed early") + offset = 0 + while offset < len(part): + pattern_offset = (done + offset) % len(chunk) + count = min(len(part) - offset, len(chunk) - pattern_offset) + if part[offset:offset + count] != chunk[pattern_offset:pattern_offset + count]: + raise RuntimeError("echo content mismatch") + offset += count + done += len(part) + thread.join() + if send_error: + raise send_error[0] + except Exception as error: + errors.append(error) + finally: + sock.close() + +per_connection = total // connections +workers = [ + threading.Thread(target=connection_worker, args=(per_connection,)) + for _ in range(connections) +] +for worker in workers: + worker.start() +for worker in workers: + worker.join() +if errors: + raise errors[0] +elapsed = time.monotonic() - start +print("ECHO %.2f Gbit/s (%d connections)" % + (per_connection * connections * 8 / elapsed / 1e9, connections), + flush=True) diff --git a/tests/integration/http_tracker.py b/tests/integration/http_tracker.py new file mode 100644 index 0000000..9c3538c --- /dev/null +++ b/tests/integration/http_tracker.py @@ -0,0 +1,31 @@ +#!/usr/bin/env python3 +"""Minimal compact-peer HTTP tracker for the Phase 4 integration test.""" +from http.server import BaseHTTPRequestHandler, HTTPServer +import socket +import struct +import sys + +peer_port = int(sys.argv[1]) +body = ( + b"d8:intervali1800e5:peers6:" + + socket.inet_aton("127.0.0.1") + + struct.pack("!H", peer_port) + + b"e" +) + + +class Tracker(BaseHTTPRequestHandler): + def do_GET(self): + self.send_response(200) + self.send_header("Content-Type", "text/plain") + self.send_header("Content-Length", str(len(body))) + self.end_headers() + self.wfile.write(body) + + def log_message(self, fmt, *args): + print("REQUEST " + (fmt % args), flush=True) + + +server = HTTPServer(("127.0.0.1", 0), Tracker) +print("PORT %d" % server.server_port, flush=True) +server.serve_forever() diff --git a/tests/integration/run_echo_scale.sh b/tests/integration/run_echo_scale.sh new file mode 100644 index 0000000..795dead --- /dev/null +++ b/tests/integration/run_echo_scale.sh @@ -0,0 +1,59 @@ +#!/usr/bin/env bash +# Phase 6 local platform gate: fixed-buffer recv + SEND_ZC echo integrity. +set -euo pipefail +ROOT="$(cd "$(dirname "$0")/../.." && pwd)" +ECHO="${1:-$ROOT/build/naut_echo}" +PORT="${NAUT_ECHO_PORT:-39127}" +LOG="$(mktemp /tmp/naut_echo_scale.XXXXXX)" +cleanup() { + [ -n "${server:-}" ] && kill -TERM "$server" 2>/dev/null || true + [ -n "${server:-}" ] && wait "$server" 2>/dev/null || true + rm -f "$LOG" +} +trap cleanup EXIT + +NAUT_SQPOLL=1 "$ECHO" "$PORT" >"$LOG" 2>&1 & +server=$! +for _ in $(seq 1 100); do + grep -q "echo listening" "$LOG" && break + kill -0 "$server" 2>/dev/null || { + echo "FAIL: echo server exited"; cat "$LOG"; exit 1; + } + sleep 0.05 +done +grep -q "echo listening" "$LOG" || { + echo "FAIL: echo server did not listen"; cat "$LOG"; exit 1; +} + +BYTES=$((256 * 1024 * 1024)) +CONNS=8 +python3 "$ROOT/tests/integration/echo_client.py" "$PORT" "$BYTES" "$CONNS" +kill -TERM "$server" +wait "$server" +server="" +grep -q "bytes echoed" "$LOG" || { + echo "FAIL: echo server did not shut down cleanly"; cat "$LOG"; exit 1; +} +grep "bytes echoed" "$LOG" + +# Server-side accounting must match exactly: every byte the client sent was +# echoed back, and no connection was dropped mid-stream. (A truncated echo is +# also caught client-side, but asserting the count here makes a server-side +# drop a hard, deterministic failure rather than a timing-dependent one.) +echoed=$(grep -oE '[0-9]+ bytes echoed' "$LOG" | grep -oE '^[0-9]+') +if [ "$echoed" != "$BYTES" ]; then + echo "FAIL: echoed $echoed bytes, expected $BYTES (a connection was dropped)" + cat "$LOG"; exit 1 +fi +conns=$(grep -oE '[0-9]+ conns' "$LOG" | grep -oE '^[0-9]+') +if [ "$conns" != "$CONNS" ]; then + echo "FAIL: served $conns connections, expected $CONNS"; cat "$LOG"; exit 1 +fi + +# Report whether the SQPOLL path was actually exercised (it falls back cleanly +# where the kernel/privileges disallow it — informational, not a failure). +if grep -q "sqpoll" "$LOG"; then + echo "PASS: io_uring SQPOLL fixed-buffer/SEND_ZC echo path is byte-correct" +else + echo "PASS: io_uring echo path byte-correct (SQPOLL unavailable, used fallback)" +fi diff --git a/tests/integration/run_interop.sh b/tests/integration/run_interop.sh new file mode 100644 index 0000000..28b368a --- /dev/null +++ b/tests/integration/run_interop.sh @@ -0,0 +1,41 @@ +#!/usr/bin/env bash +# Phase 3 interop gate: seed each fixture torrent with libtorrent and download +# it with the from-scratch naut_leech, asserting a byte-identical content tree. +# Skips (exit 77 = ctest SKIP) if python libtorrent is unavailable. +set -u +ROOT="$(cd "$(dirname "$0")/../.." && pwd)" +LEECH="${1:-$ROOT/build/naut_leech}" +SEEDER="$ROOT/tests/integration/seeder.py" +DATA="$ROOT/tests/fixtures/data" + +python3 -c 'import libtorrent' 2>/dev/null || { echo "SKIP: python libtorrent not available"; exit 77; } +[ -x "$LEECH" ] || { echo "FAIL: $LEECH not built"; exit 1; } + +fail=0 +run_one() { + local tor="$1" cmp="$2" + local out; out="$(mktemp -d /tmp/naut_interop.XXXXXX)" + local log; log="$(mktemp /tmp/naut_seed.XXXXXX)" + python3 "$SEEDER" "$ROOT/tests/fixtures/$tor" "$DATA" > "$log" 2>&1 & + local seed=$! + local port="" + for _ in $(seq 1 100); do port=$(grep -oP 'PORT \K[0-9]+' "$log" 2>/dev/null); [ -n "$port" ] && break; sleep 0.1; done + if [ -z "$port" ]; then echo "FAIL[$tor]: seeder did not start"; cat "$log"; kill "$seed" 2>/dev/null; fail=1; rm -rf "$out" "$log"; return; fi + + if timeout 30 "$LEECH" "$ROOT/tests/fixtures/$tor" "$out" 127.0.0.1 "$port" 2>&1 | grep -q "COMPLETE"; then + if diff -r "$out/$cmp" "$DATA/$cmp" >/dev/null; then + echo "PASS[$tor]: byte-identical content tree" + else + echo "FAIL[$tor]: content mismatch"; fail=1 + fi + else + echo "FAIL[$tor]: download did not complete"; fail=1 + fi + kill "$seed" 2>/dev/null; rm -rf "$out" "$log" +} + +run_one single_v1.torrent single.bin +run_one multi_v1.torrent multi +run_one hybrid.torrent multi + +exit $fail diff --git a/tests/integration/run_magnet_dht.sh b/tests/integration/run_magnet_dht.sh new file mode 100644 index 0000000..dfcf1fd --- /dev/null +++ b/tests/integration/run_magnet_dht.sh @@ -0,0 +1,65 @@ +#!/usr/bin/env bash +# Phase 5 gate: trackerless magnet -> DHT peer -> ut_metadata -> verified data. +set -u +ROOT="$(cd "$(dirname "$0")/../.." && pwd)" +SWARM="${1:-$ROOT/build/naut_swarm}" +SEEDER="$ROOT/tests/integration/seeder.py" +DHT="$ROOT/tests/integration/dht_fixture.py" +TORRENT="$ROOT/tests/fixtures/single_v1.torrent" +DATA="$ROOT/tests/fixtures/data" +MAGNET="magnet:?xt=urn:btih:7f2555dfd18ba1c4a024264e939d7a5f8b25311b" + +python3 -c 'import libtorrent' 2>/dev/null || { + echo "SKIP: python libtorrent not available"; exit 77; +} +[ -x "$SWARM" ] || { echo "FAIL: $SWARM not built"; exit 1; } + +out="$(mktemp -d /tmp/naut_magnet.XXXXXX)" +seed_log="$(mktemp /tmp/naut_magnet_seed.XXXXXX)" +dht_log="$(mktemp /tmp/naut_magnet_dht.XXXXXX)" +client_log="$(mktemp /tmp/naut_magnet_client.XXXXXX)" +cleanup() { + [ -n "${seed:-}" ] && kill "$seed" 2>/dev/null || true + [ -n "${dht_pid:-}" ] && kill "$dht_pid" 2>/dev/null || true + rm -rf "$out" "$seed_log" "$dht_log" "$client_log" +} +trap cleanup EXIT + +python3 "$SEEDER" "$TORRENT" "$DATA" >"$seed_log" 2>&1 & +seed=$! +peer_port="" +for _ in $(seq 1 100); do + peer_port=$(grep -oP 'PORT \K[0-9]+' "$seed_log" 2>/dev/null) + [ -n "$peer_port" ] && break + sleep 0.1 +done +[ -n "$peer_port" ] || { + echo "FAIL: seeder did not start"; cat "$seed_log"; exit 1; +} + +python3 "$DHT" "$peer_port" >"$dht_log" 2>&1 & +dht_pid=$! +dht_port="" +for _ in $(seq 1 100); do + dht_port=$(grep -oP 'DHT_PORT \K[0-9]+' "$dht_log" 2>/dev/null) + [ -n "$dht_port" ] && break + sleep 0.1 +done +[ -n "$dht_port" ] || { + echo "FAIL: DHT fixture did not start"; cat "$dht_log"; exit 1; +} + +if ! NAUT_DHT_BOOTSTRAP="127.0.0.1:$dht_port" \ + timeout 30 "$SWARM" "$MAGNET" "$out" >"$client_log" 2>&1; then + echo "FAIL: trackerless magnet download did not complete" + cat "$client_log" + cat "$seed_log" + exit 1 +fi +grep -q "magnet metadata verified" "$client_log" || { + echo "FAIL: metadata was not verified"; cat "$client_log"; exit 1; +} +diff -r "$out/single.bin" "$DATA/single.bin" >/dev/null || { + echo "FAIL: magnet content mismatch"; exit 1; +} +echo "PASS: DHT-only magnet completed with verified metadata and content" diff --git a/tests/integration/run_mse.sh b/tests/integration/run_mse.sh new file mode 100644 index 0000000..9ee05b5 --- /dev/null +++ b/tests/integration/run_mse.sh @@ -0,0 +1,49 @@ +#!/usr/bin/env bash +# Phase 5 MSE gate: a libtorrent seed requires encrypted incoming connections. +set -u +ROOT="$(cd "$(dirname "$0")/../.." && pwd)" +LEECH="${1:-$ROOT/build/naut_leech}" +SEEDER="$ROOT/tests/integration/seeder.py" +TORRENT="$ROOT/tests/fixtures/single_v1.torrent" +DATA="$ROOT/tests/fixtures/data" + +python3 -c 'import libtorrent' 2>/dev/null || { + echo "SKIP: python libtorrent not available"; exit 77; +} +[ -x "$LEECH" ] || { echo "FAIL: $LEECH not built"; exit 1; } + +out="$(mktemp -d /tmp/naut_mse.XXXXXX)" +log="$(mktemp /tmp/naut_mse_seed.XXXXXX)" +client_log="$(mktemp /tmp/naut_mse_client.XXXXXX)" +cleanup() { + [ -n "${seed:-}" ] && kill "$seed" 2>/dev/null || true + rm -rf "$out" "$log" "$client_log" +} +trap cleanup EXIT + +NAUT_FORCE_MSE=1 python3 "$SEEDER" "$TORRENT" "$DATA" > "$log" 2>&1 & +seed=$! +port="" +for _ in $(seq 1 100); do + port=$(grep -oP 'PORT \K[0-9]+' "$log" 2>/dev/null) + [ -n "$port" ] && break + sleep 0.1 +done +[ -n "$port" ] || { echo "FAIL: encrypted seeder did not start"; cat "$log"; exit 1; } + +if ! timeout 30 "$LEECH" --mse "$TORRENT" "$out" 127.0.0.1 "$port" \ + >"$client_log" 2>&1; then + echo "FAIL: MSE download did not complete" + cat "$client_log" + cat "$log" + exit 1 +fi +grep -q "COMPLETE" "$client_log" || { + echo "FAIL: client exited without completing" + cat "$client_log" + exit 1 +} +diff -r "$out/single.bin" "$DATA/single.bin" >/dev/null || { + echo "FAIL: MSE content mismatch"; exit 1; +} +echo "PASS: forced MSE/RC4 download is byte-identical" diff --git a/tests/integration/run_phase7.sh b/tests/integration/run_phase7.sh new file mode 100644 index 0000000..5811ad6 --- /dev/null +++ b/tests/integration/run_phase7.sh @@ -0,0 +1,98 @@ +#!/usr/bin/env bash +set -euo pipefail + +daemon=$1 +ctl=$2 +plugin=$3 +script=$4 +tmp=$(mktemp -d) +socket="$tmp/nautd.sock" +daemon_log="$tmp/nautd.log" +events_log="$tmp/events.log" + +cleanup() { + result=$? + if [[ -n "${daemon_pid:-}" ]]; then + kill "$daemon_pid" 2>/dev/null || true + wait "$daemon_pid" 2>/dev/null || true + fi + if [[ -n "${events_pid:-}" ]]; then + kill "$events_pid" 2>/dev/null || true + wait "$events_pid" 2>/dev/null || true + fi + if [[ "$result" -ne 0 ]]; then + cat "$daemon_log" >&2 2>/dev/null || true + cat "$events_log" >&2 2>/dev/null || true + fi + rm -rf "$tmp" + return "$result" +} +trap cleanup EXIT + +"$daemon" --socket "$socket" --plugin "$plugin" --script "$script" \ + >"$daemon_log" 2>&1 & +daemon_pid=$! + +for _ in $(seq 1 100); do + [[ -S "$socket" ]] && break + sleep 0.02 +done +[[ -S "$socket" ]] + +"$ctl" --socket "$socket" ping | grep -q '"service": "nautd"' +"$ctl" --socket "$socket" plugins | grep -q '"memory"' + +timeout 5 "$ctl" --socket "$socket" events >"$events_log" & +events_pid=$! +sleep 0.1 +"$ctl" --socket "$socket" emit \ + '{"type":"torrent_finished","torrent_id":7}' >/dev/null + +status= +for _ in $(seq 1 100); do + status=$("$ctl" --socket "$socket" status) + if grep -q '"move_commands": 1' <<<"$status" && + grep -q '"handled": 1' <<<"$status"; then + break + fi + sleep 0.02 +done +grep -q '"move_commands": 1' <<<"$status" +grep -q '"handled": 1' <<<"$status" +grep -q '"errors": 0' <<<"$status" + +plugin_status=$("$ctl" --socket "$socket" example.events) +grep -q '"finished": 1' <<<"$plugin_status" + +for _ in $(seq 1 100); do + grep -q '"event": "torrent_finished"' "$events_log" && break + sleep 0.02 +done +grep -q '"event": "torrent_finished"' "$events_log" + +# --- end-to-end move-as-you-finish: register a real torrent's storage, fire a +# file_complete event, and confirm the script-driven move actually relocates the +# file on disk (script thread -> bounded queue -> owner thread -> storage). ---- +fixtures=$(dirname "$script") +root="$tmp/torrent-data" +"$ctl" --socket "$socket" add_torrent \ + "{\"torrent_id\":42,\"torrent\":\"$fixtures/single_v1.torrent\",\"root\":\"$root\"}" \ + | grep -q '"ok": true' + +src=$(find "$root" -type f | head -n1) +[[ -n "$src" ]] + +"$ctl" --socket "$socket" emit \ + "{\"type\":\"file_complete\",\"torrent_id\":42,\"index\":0,\"path\":\"$src\"}" \ + >/dev/null + +for _ in $(seq 1 100); do + [[ -f "$src.moved" ]] && break + sleep 0.02 +done +[[ -f "$src.moved" ]] +[[ ! -f "$src" ]] + +"$ctl" --socket "$socket" shutdown >/dev/null +wait "$daemon_pid" +daemon_pid= diff --git a/tests/integration/run_swarm.sh b/tests/integration/run_swarm.sh new file mode 100644 index 0000000..723a349 --- /dev/null +++ b/tests/integration/run_swarm.sh @@ -0,0 +1,69 @@ +#!/usr/bin/env bash +# Phase 4 interop gate: download one torrent concurrently from two independent +# libtorrent seeds and require both peers to contribute blocks. +set -u +ROOT="$(cd "$(dirname "$0")/../.." && pwd)" +SWARM="${1:-$ROOT/build/naut_swarm}" +SEEDER="$ROOT/tests/integration/seeder.py" +TOR="$ROOT/tests/fixtures/single_v1.torrent" +DATA="$ROOT/tests/fixtures/data" + +python3 -c 'import libtorrent' 2>/dev/null || { + echo "SKIP: python libtorrent not available" + exit 77 +} +[ -x "$SWARM" ] || { echo "FAIL: $SWARM not built"; exit 1; } + +out="$(mktemp -d /tmp/naut_swarm.XXXXXX)" +log1="$(mktemp /tmp/naut_seed1.XXXXXX)" +log2="$(mktemp /tmp/naut_seed2.XXXXXX)" +slog="$(mktemp /tmp/naut_swarm_log.XXXXXX)" +seed1="" +seed2="" + +cleanup() { + [ -n "$seed1" ] && kill "$seed1" 2>/dev/null || true + [ -n "$seed2" ] && kill "$seed2" 2>/dev/null || true + rm -rf "$out" "$log1" "$log2" "$slog" +} +trap cleanup EXIT + +python3 "$SEEDER" "$TOR" "$DATA" > "$log1" 2>&1 & +seed1=$! +python3 "$SEEDER" "$TOR" "$DATA" > "$log2" 2>&1 & +seed2=$! + +port1="" +port2="" +for _ in $(seq 1 100); do + port1="$(grep -oP 'PORT \K[0-9]+' "$log1" 2>/dev/null || true)" + port2="$(grep -oP 'PORT \K[0-9]+' "$log2" 2>/dev/null || true)" + [ -n "$port1" ] && [ "$port1" != 0 ] && + [ -n "$port2" ] && [ "$port2" != 0 ] && break + sleep 0.1 +done +if [ -z "$port1" ] || [ "$port1" = 0 ] || + [ -z "$port2" ] || [ "$port2" = 0 ]; then + echo "FAIL: seeders did not start" + cat "$log1" "$log2" + exit 1 +fi + +if ! timeout 30 "$SWARM" "$TOR" "$out" \ + "127.0.0.1:$port1" "127.0.0.1:$port2" > "$slog" 2>&1; then + echo "FAIL: swarm download did not complete" + cat "$slog" + exit 1 +fi +if ! diff -r "$out/single.bin" "$DATA/single.bin" >/dev/null; then + echo "FAIL: content mismatch" + exit 1 +fi +contributors="$(grep -Ec 'delivered [1-9][0-9]* blocks' "$slog" || true)" +if [ "$contributors" -lt 2 ]; then + echo "FAIL: expected both peers to contribute" + cat "$slog" + exit 1 +fi + +echo "PASS: two-peer swarm produced byte-identical output" diff --git a/tests/integration/run_tracker_swarm.sh b/tests/integration/run_tracker_swarm.sh new file mode 100644 index 0000000..869526b --- /dev/null +++ b/tests/integration/run_tracker_swarm.sh @@ -0,0 +1,129 @@ +#!/usr/bin/env bash +# Phase 4 tracker gate: discover a real libtorrent seed from a localhost HTTP +# tracker, then complete the download without explicit peer arguments. +set -u +ROOT="$(cd "$(dirname "$0")/../.." && pwd)" +SWARM="${1:-$ROOT/build/naut_swarm}" +MODE="${2:-http}" +SEEDER="$ROOT/tests/integration/seeder.py" +if [ "$MODE" = "udp" ]; then + TRACKER="$ROOT/tests/integration/udp_tracker.py" +else + TRACKER="$ROOT/tests/integration/http_tracker.py" +fi +SOURCE_TOR="$ROOT/tests/fixtures/single_v1.torrent" +DATA="$ROOT/tests/fixtures/data" + +python3 -c 'import libtorrent' 2>/dev/null || { + echo "SKIP: python libtorrent not available" + exit 77 +} +[ -x "$SWARM" ] || { echo "FAIL: $SWARM not built"; exit 1; } + +out="$(mktemp -d /tmp/naut_tracker_swarm.XXXXXX)" +seed_log="$(mktemp /tmp/naut_tracker_seed.XXXXXX)" +tracker_log="$(mktemp /tmp/naut_http_tracker.XXXXXX)" +swarm_log="$(mktemp /tmp/naut_tracker_client.XXXXXX)" +tor="$(mktemp /tmp/naut_tracker.XXXXXX.torrent)" +seed="" +tracker="" + +cleanup() { + [ -n "$seed" ] && kill "$seed" 2>/dev/null || true + [ -n "$tracker" ] && kill "$tracker" 2>/dev/null || true + rm -rf "$out" "$seed_log" "$tracker_log" "$swarm_log" "$tor" +} +trap cleanup EXIT + +python3 "$SEEDER" "$SOURCE_TOR" "$DATA" > "$seed_log" 2>&1 & +seed=$! +seed_port="" +for _ in $(seq 1 100); do + seed_port="$(grep -oP 'PORT \K[0-9]+' "$seed_log" 2>/dev/null || true)" + [ -n "$seed_port" ] && [ "$seed_port" != 0 ] && break + sleep 0.1 +done +if [ -z "$seed_port" ] || [ "$seed_port" = 0 ]; then + echo "FAIL: seeder did not start" + cat "$seed_log" + exit 1 +fi + +python3 "$TRACKER" "$seed_port" > "$tracker_log" 2>&1 & +tracker=$! +tracker_port="" +for _ in $(seq 1 100); do + tracker_port="$(grep -oP 'PORT \K[0-9]+' "$tracker_log" 2>/dev/null || true)" + [ -n "$tracker_port" ] && break + sleep 0.05 +done +if [ -z "$tracker_port" ]; then + echo "FAIL: tracker did not start" + cat "$tracker_log" + exit 1 +fi + +# Replace only the top-level dictionary. The raw info dictionary is copied +# byte-for-byte so its SHA-1 info hash remains unchanged. +if [ "$MODE" = "udp" ]; then + announce="udp://127.0.0.1:$tracker_port/announce" +else + announce="http://127.0.0.1:$tracker_port/announce" +fi +python3 - "$SOURCE_TOR" "$tor" "$announce" <<'PY' +import sys + + +def skip(data, pos): + token = data[pos] + if token == ord("i"): + return data.index(b"e", pos) + 1 + if token in (ord("l"), ord("d")): + pos += 1 + while data[pos] != ord("e"): + pos = skip(data, pos) + if token == ord("d"): + pos = skip(data, pos) + return pos + 1 + colon = data.index(b":", pos) + size = int(data[pos:colon]) + return colon + 1 + size + + +source, target, announce = sys.argv[1], sys.argv[2], sys.argv[3].encode() +data = open(source, "rb").read() +pos = 1 +raw_info = None +while data[pos] != ord("e"): + colon = data.index(b":", pos) + key_len = int(data[pos:colon]) + key_start = colon + 1 + key = data[key_start:key_start + key_len] + pos = key_start + key_len + value_start = pos + pos = skip(data, pos) + if key == b"info": + raw_info = data[value_start:pos] +assert raw_info is not None +rewritten = ( + b"d8:announce" + str(len(announce)).encode() + b":" + announce + + b"4:info" + raw_info + b"e" +) +open(target, "wb").write(rewritten) +PY + +if ! timeout 30 "$SWARM" "$tor" "$out" > "$swarm_log" 2>&1; then + echo "FAIL: tracker-discovered swarm did not complete" + cat "$swarm_log" "$tracker_log" + exit 1 +fi +if ! diff -r "$out/single.bin" "$DATA/single.bin" >/dev/null; then + echo "FAIL: content mismatch" + exit 1 +fi +if ! grep -q "REQUEST" "$tracker_log"; then + echo "FAIL: tracker received no announce" + exit 1 +fi + +echo "PASS: $MODE tracker discovery produced byte-identical output" diff --git a/tests/integration/seeder.py b/tests/integration/seeder.py new file mode 100644 index 0000000..322e46a --- /dev/null +++ b/tests/integration/seeder.py @@ -0,0 +1,37 @@ +#!/usr/bin/env python3 +"""Seed a .torrent with libtorrent on 127.0.0.1 and print the listen port. +Usage: seeder.py +Runs until killed. Prints 'PORT ' once it is seeding.""" +import libtorrent as lt, os, sys, time + +enc_policy = 0 if os.environ.get("NAUT_FORCE_MSE") == "1" else 1 + +torrent, save_path = sys.argv[1], sys.argv[2] +ses = lt.session({ + "listen_interfaces": "127.0.0.1:0", + "unchoke_slots_limit": 64, # unchoke leechers fast + "in_enc_policy": enc_policy, + "out_enc_policy": enc_policy, + "alert_mask": lt.alert_category.all, +}) + +atp = lt.add_torrent_params() +atp.ti = lt.torrent_info(torrent) +atp.save_path = save_path +atp.flags |= lt.torrent_flags.seed_mode # data already present; skip recheck +atp.flags &= ~lt.torrent_flags.paused # must be active to accept peers +atp.flags &= ~lt.torrent_flags.auto_managed # don't let the queue re-pause it +h = ses.add_torrent(atp) +h.resume() + +deadline = time.time() + 30 +while not h.status().is_seeding and time.time() < deadline: + time.sleep(0.05) +if not h.status().is_seeding: + print("ERROR: not seeding", flush=True); sys.exit(1) + +print("PORT %d" % ses.listen_port(), flush=True) +while True: + for a in ses.pop_alerts(): + print("ALERT %s: %s" % (type(a).__name__, a.message()), flush=True) + time.sleep(0.2) diff --git a/tests/integration/udp_tracker.py b/tests/integration/udp_tracker.py new file mode 100644 index 0000000..4a86a4e --- /dev/null +++ b/tests/integration/udp_tracker.py @@ -0,0 +1,27 @@ +#!/usr/bin/env python3 +"""Minimal BEP-15 UDP tracker for the Phase 4 integration test.""" +import socket +import struct +import sys + +peer_port = int(sys.argv[1]) +connection_id = 0x0102030405060708 +sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) +sock.bind(("127.0.0.1", 0)) +print("PORT %d" % sock.getsockname()[1], flush=True) + +while True: + packet, addr = sock.recvfrom(2048) + if len(packet) >= 16 and packet[8:12] == b"\x00\x00\x00\x00": + sock.sendto(struct.pack("!IIQ", 0, struct.unpack("!I", packet[12:16])[0], + connection_id), addr) + continue + if len(packet) >= 98 and packet[8:12] == b"\x00\x00\x00\x01": + txid = struct.unpack("!I", packet[12:16])[0] + response = ( + struct.pack("!IIIII", 1, txid, 1800, 0, 1) + + socket.inet_aton("127.0.0.1") + + struct.pack("!H", peer_port) + ) + sock.sendto(response, addr) + print("REQUEST announce", flush=True) diff --git a/tests/unit/test.h b/tests/unit/test.h new file mode 100644 index 0000000..04a6926 --- /dev/null +++ b/tests/unit/test.h @@ -0,0 +1,34 @@ +/* Minimal test harness: no dependencies, exit code = failure count. */ +#ifndef NAUT_TEST_H +#define NAUT_TEST_H +#include +#include + +static int naut_test_fails = 0; +static int naut_test_count = 0; + +#define CHECK(cond) do { \ + naut_test_count++; \ + if (!(cond)) { \ + naut_test_fails++; \ + fprintf(stderr, " FAIL %s:%d: %s\n", __FILE__, __LINE__, #cond); \ + } \ +} while (0) + +#define CHECK_EQ(a, b) do { \ + long long _a = (long long)(a), _b = (long long)(b); \ + naut_test_count++; \ + if (_a != _b) { \ + naut_test_fails++; \ + fprintf(stderr, " FAIL %s:%d: %s (%lld) == %s (%lld)\n", \ + __FILE__, __LINE__, #a, _a, #b, _b); \ + } \ +} while (0) + +#define TEST_MAIN_END() do { \ + fprintf(stderr, "%s: %d checks, %d failures\n", \ + __FILE__, naut_test_count, naut_test_fails); \ + return naut_test_fails ? 1 : 0; \ +} while (0) + +#endif diff --git a/tests/unit/test_bencode.c b/tests/unit/test_bencode.c new file mode 100644 index 0000000..90e54ad --- /dev/null +++ b/tests/unit/test_bencode.c @@ -0,0 +1,100 @@ +#include "naut/bencode.h" +#include "test.h" +#include +#include + +static naut_bc_doc *ok(const char *s) { + naut_bc_doc *d = NULL; + naut_err e = naut_bc_parse((const uint8_t *)s, strlen(s), &d); + if (e != NAUT_OK) { fprintf(stderr, " unexpected reject: \"%s\" (%s)\n", s, naut_strerror(e)); } + return e == NAUT_OK ? d : NULL; +} +static int rejects(const char *s, size_t len) { + naut_bc_doc *d = NULL; + naut_err e = naut_bc_parse((const uint8_t *)s, len, &d); + if (e == NAUT_OK) { naut_bc_free(d); return 0; } + return 1; +} +#define REJECT(s) CHECK(rejects(s, sizeof(s) - 1)) + +int main(void) { + /* integers */ + naut_bc_doc *d; + int64_t iv; + d = ok("i42e"); CHECK(d && naut_bc_get_int(naut_bc_root(d), &iv) && iv == 42); naut_bc_free(d); + d = ok("i0e"); CHECK(d && naut_bc_get_int(naut_bc_root(d), &iv) && iv == 0); naut_bc_free(d); + d = ok("i-7e"); CHECK(d && naut_bc_get_int(naut_bc_root(d), &iv) && iv == -7); naut_bc_free(d); + REJECT("ie"); REJECT("i03e"); REJECT("i-0e"); REJECT("i-e"); REJECT("i1 e"); REJECT("i42"); + + /* strings (zero-copy + binary-safe) */ + const uint8_t *sp; size_t sn; + d = ok("4:spam"); CHECK(d && naut_bc_get_str(naut_bc_root(d), &sp, &sn) && sn == 4 && !memcmp(sp,"spam",4)); + /* slice points into the original buffer, not a copy */ + naut_bc_free(d); + REJECT("4:spa"); REJECT("01:a"); /* leading zero length */ + + /* binary string with embedded NUL */ + { + const char buf[] = "5:a\0b\0c"; /* len prefix "5:" then a \0 b \0 c */ + naut_bc_doc *bd = NULL; + CHECK(naut_bc_parse((const uint8_t *)buf, 7, &bd) == NAUT_OK); + CHECK(bd && naut_bc_get_str(naut_bc_root(bd), &sp, &sn) && sn == 5 && sp[1] == 0 && sp[3] == 0); + naut_bc_free(bd); + } + + /* list */ + d = ok("l4:spami42ee"); + CHECK(d); + const naut_bc *l = naut_bc_root(d); + CHECK(l && l->type == NAUT_BC_LIST && l->v.list.count == 2); + CHECK(naut_bc_str_eq(naut_bc_list_at(l, 0), "spam")); + CHECK(naut_bc_get_int(naut_bc_list_at(l, 1), &iv) && iv == 42); + naut_bc_free(d); + + /* dict + raw span preservation (crucial for info-hash) */ + d = ok("d3:bar4:spam3:fooi42ee"); + const naut_bc *root = naut_bc_root(d); + CHECK(naut_bc_str_eq(naut_bc_dict_get(root, "bar"), "spam")); + CHECK(naut_bc_get_int(naut_bc_dict_get(root, "foo"), &iv) && iv == 42); + const naut_bc *bar = naut_bc_dict_get(root, "bar"); + CHECK(bar->raw_len == 6 && memcmp(bar->raw, "4:spam", 6) == 0); /* exact encoding */ + CHECK(naut_bc_dict_get(root, "missing") == NULL); + naut_bc_free(d); + + /* structural rejects */ + REJECT("i1ex"); /* trailing garbage */ + REJECT("d3:bar4:spam"); /* unterminated dict */ + REJECT("l1:a"); /* unterminated list */ + REJECT("di1e1:ae"); /* non-string dict key */ + REJECT(""); /* empty */ + + /* depth bomb: 200 nested lists must be rejected, not overflow the stack */ + { + char bomb[512]; + memset(bomb, 'l', sizeof bomb); + CHECK(rejects(bomb, sizeof bomb)); + } + + /* encoder round-trips back to an equal parse */ + { + naut_bc_writer w; naut_bc_w_init(&w); + naut_bc_w_dict_begin(&w); + naut_bc_w_cstr(&w, "foo"); naut_bc_w_int(&w, 7); + naut_bc_w_cstr(&w, "list"); naut_bc_w_list_begin(&w); + naut_bc_w_cstr(&w, "x"); naut_bc_w_int(&w, -1); + naut_bc_w_end(&w); + naut_bc_w_end(&w); + CHECK(w.err == NAUT_OK); + + naut_bc_doc *rd = NULL; + CHECK(naut_bc_parse(w.buf, w.len, &rd) == NAUT_OK); + const naut_bc *r = naut_bc_root(rd); + CHECK(naut_bc_get_int(naut_bc_dict_get(r, "foo"), &iv) && iv == 7); + const naut_bc *lst = naut_bc_dict_get(r, "list"); + CHECK(lst && lst->type == NAUT_BC_LIST && lst->v.list.count == 2); + naut_bc_free(rd); + naut_bc_w_free(&w); + } + + TEST_MAIN_END(); +} diff --git a/tests/unit/test_bitfield.c b/tests/unit/test_bitfield.c new file mode 100644 index 0000000..70767a9 --- /dev/null +++ b/tests/unit/test_bitfield.c @@ -0,0 +1,60 @@ +#include "naut/bitfield.h" +#include "test.h" + +int main(void) { + naut_bitfield bf; + /* deliberately not a multiple of 64 to exercise tail masking */ + CHECK(naut_bitfield_init(&bf, 1000) == NAUT_OK); + CHECK_EQ(bf.nwords, 16); + CHECK_EQ(naut_bitfield_count(&bf), 0); + CHECK(!naut_bitfield_all_set(&bf)); + + naut_bitfield_set(&bf, 0); + naut_bitfield_set(&bf, 63); + naut_bitfield_set(&bf, 64); + naut_bitfield_set(&bf, 999); + CHECK(naut_bitfield_test(&bf, 0)); + CHECK(naut_bitfield_test(&bf, 999)); + CHECK(!naut_bitfield_test(&bf, 1)); + CHECK_EQ(naut_bitfield_count(&bf), 4); + + CHECK_EQ(naut_bitfield_find_set(&bf, 0), 0); + CHECK_EQ(naut_bitfield_find_set(&bf, 1), 63); + CHECK_EQ(naut_bitfield_find_set(&bf, 65), 999); + CHECK_EQ(naut_bitfield_find_set(&bf, 1000), SIZE_MAX); + + CHECK_EQ(naut_bitfield_find_zero(&bf, 0), 1); + CHECK_EQ(naut_bitfield_find_zero(&bf, 63), 65); + + naut_bitfield_clear(&bf, 63); + CHECK(!naut_bitfield_test(&bf, 63)); + CHECK_EQ(naut_bitfield_count(&bf), 3); + + /* all-set respects nbits, not nwords*64 */ + naut_bitfield_set_all(&bf); + CHECK_EQ(naut_bitfield_count(&bf), 1000); + CHECK(naut_bitfield_all_set(&bf)); + CHECK_EQ(naut_bitfield_find_zero(&bf, 0), SIZE_MAX); + + /* wire round-trip (BEP-3 MSB-first) */ + naut_bitfield_clear_all(&bf); + naut_bitfield_set(&bf, 0); /* -> byte 0, bit 0x80 */ + naut_bitfield_set(&bf, 7); /* -> byte 0, bit 0x01 */ + naut_bitfield_set(&bf, 8); /* -> byte 1, bit 0x80 */ + uint8_t wire[125]; /* ceil(1000/8) */ + naut_bitfield_to_wire(&bf, wire, sizeof(wire)); + CHECK_EQ(wire[0], 0x81); + CHECK_EQ(wire[1], 0x80); + + naut_bitfield bf2; + naut_bitfield_init(&bf2, 1000); + naut_bitfield_from_wire(&bf2, wire, sizeof(wire)); + CHECK(naut_bitfield_test(&bf2, 0)); + CHECK(naut_bitfield_test(&bf2, 7)); + CHECK(naut_bitfield_test(&bf2, 8)); + CHECK_EQ(naut_bitfield_count(&bf2), 3); + + naut_bitfield_free(&bf); + naut_bitfield_free(&bf2); + TEST_MAIN_END(); +} diff --git a/tests/unit/test_buf.c b/tests/unit/test_buf.c new file mode 100644 index 0000000..0864582 --- /dev/null +++ b/tests/unit/test_buf.c @@ -0,0 +1,80 @@ +#include "naut/buf.h" +#include "test.h" +#include +#include +#include + +/* Concurrency test: the owner thread allocates buffers and hands them to N + * worker threads which put() them back. Mirrors recv-on-reactor / free-on-hash + * -worker. At the end every buffer must be back on the freelist. */ + +#define NBLOCKS 1024 +#define NWORKERS 8 + +static naut_bufpool *pool; +static _Atomic int bad_cap; /* worker-thread failures (harness CHECK is not MT-safe) */ + +struct chan { _Atomic(naut_buf *) slot[NBLOCKS]; _Atomic int head, tail; }; +static struct chan ch; + +static void *worker(void *arg) { + (void)arg; + for (;;) { + int t = atomic_load(&ch.tail); + if (t >= NBLOCKS) break; + if (!atomic_compare_exchange_weak(&ch.tail, &t, t + 1)) continue; + /* spin until producer publishes slot t */ + naut_buf *b; + while (!(b = atomic_load_explicit(&ch.slot[t], memory_order_acquire))) + sched_yield(); + if (b->cap != NAUT_BLOCK) atomic_fetch_add(&bad_cap, 1); + memset(b->data, 0xab, b->cap); /* touch the pages */ + naut_buf_put(b); + } + return NULL; +} + +int main(void) { + pool = naut_bufpool_create(NAUT_BLOCK, NBLOCKS, false); + CHECK(pool != NULL); + CHECK_EQ(naut_bufpool_capacity(pool), NBLOCKS); + CHECK_EQ(naut_bufpool_available(pool), NBLOCKS); + + /* basic single-thread get/put */ + naut_buf *a = naut_buf_get(pool); + CHECK(a != NULL); + CHECK_EQ(naut_bufpool_available(pool), NBLOCKS - 1); + naut_buf_ref(a); /* refcnt 1 -> 2 */ + naut_buf_put(a); /* 2 -> 1, stays out */ + CHECK_EQ(naut_bufpool_available(pool), NBLOCKS - 1); + naut_buf_put(a); /* 1 -> 0, returns */ + CHECK_EQ(naut_bufpool_available(pool), NBLOCKS); + + /* exhaustion */ + naut_buf *held[NBLOCKS]; + for (int i = 0; i < NBLOCKS; i++) { held[i] = naut_buf_get(pool); CHECK(held[i]); } + CHECK_EQ(naut_bufpool_available(pool), 0); + CHECK(naut_buf_get(pool) == NULL); /* empty => NULL, no crash */ + + /* concurrent producer/consumer */ + atomic_store(&ch.head, 0); + atomic_store(&ch.tail, 0); + for (int i = 0; i < NBLOCKS; i++) + atomic_store_explicit(&ch.slot[i], NULL, memory_order_relaxed); + + pthread_t th[NWORKERS]; + for (int i = 0; i < NWORKERS; i++) pthread_create(&th[i], NULL, worker, NULL); + for (int i = 0; i < NBLOCKS; i++) /* publish */ + atomic_store_explicit(&ch.slot[i], held[i], memory_order_release); + for (int i = 0; i < NWORKERS; i++) pthread_join(th[i], NULL); + + CHECK_EQ(atomic_load(&bad_cap), 0); + CHECK_EQ(naut_bufpool_available(pool), NBLOCKS); + /* and we can drain the whole pool again afterwards */ + int got = 0; + while (naut_buf_get(pool)) got++; + CHECK_EQ(got, NBLOCKS); + + naut_bufpool_destroy(pool); + TEST_MAIN_END(); +} diff --git a/tests/unit/test_crypto.c b/tests/unit/test_crypto.c new file mode 100644 index 0000000..1c6c3ec --- /dev/null +++ b/tests/unit/test_crypto.c @@ -0,0 +1,94 @@ +#include "naut/hash.h" +#include "naut/rc4.h" +#include "naut/merkle.h" +#include "test.h" +#include +#include + +static int hexeq(const uint8_t *got, const char *hex, size_t n) { + for (size_t i = 0; i < n; i++) { + unsigned b; + sscanf(hex + i*2, "%2x", &b); + if (got[i] != (uint8_t)b) return 0; + } + return 1; +} + +int main(void) { + uint8_t d[32]; + + /* --- SHA-1 (FIPS) --- */ + naut_sha1("abc", 3, d); + CHECK(hexeq(d, "a9993e364706816aba3e25717850c26c9cd0d89d", 20)); + naut_sha1("", 0, d); + CHECK(hexeq(d, "da39a3ee5e6b4b0d3255bfef95601890afd80709", 20)); + + /* --- SHA-256 (FIPS) --- */ + fprintf(stderr, "sha256 backend: %s\n", naut_sha256_backend()); + naut_sha256("abc", 3, d); + CHECK(hexeq(d, "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad", 32)); + naut_sha256("", 0, d); + CHECK(hexeq(d, "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", 32)); + + /* multi-block + streaming: 1,000,000 'a' fed in odd-sized chunks */ + { + naut_sha256_ctx c; naut_sha256_init(&c); + char chunk[7]; memset(chunk, 'a', sizeof chunk); + size_t left = 1000000; + while (left) { size_t n = left < sizeof chunk ? left : sizeof chunk; + naut_sha256_update(&c, chunk, n); left -= n; } + naut_sha256_final(&c, d); + CHECK(hexeq(d, "cdc76e5c9914fb9281a1c7e284d73e67f1809a48a497200e046d39ccc7112cd0", 32)); + } + + /* --- RC4 (Wikipedia test vectors, drop=0) --- */ + { + naut_rc4 c; uint8_t buf[16]; + memcpy(buf, "Plaintext", 9); + naut_rc4_init(&c, "Key", 3, 0); naut_rc4_xor(&c, buf, 9); + CHECK(hexeq(buf, "bbf316e8d940af0ad3", 9)); + + memcpy(buf, "pedia", 5); + naut_rc4_init(&c, "Wiki", 4, 0); naut_rc4_xor(&c, buf, 5); + CHECK(hexeq(buf, "1021bf0420", 5)); + + /* round-trip with MSE-style drop=1024 */ + uint8_t msg[64], orig[64]; + for (int i = 0; i < 64; i++) orig[i] = msg[i] = (uint8_t)(i * 7); + naut_rc4_init(&c, "secretkey", 9, 1024); naut_rc4_xor(&c, msg, 64); + CHECK(memcmp(msg, orig, 64) != 0); /* actually encrypted */ + naut_rc4_init(&c, "secretkey", 9, 1024); naut_rc4_xor(&c, msg, 64); + CHECK(memcmp(msg, orig, 64) == 0); /* decrypts back */ + } + + /* --- Merkle (BEP-52 structure) --- */ + { + /* 3 leaf hashes -> pad to 4 with a zero leaf */ + uint8_t leaves[3*32]; + for (int i = 0; i < 3; i++) { char s[2] = { (char)('a'+i), 0 }; + naut_sha256(s, 1, leaves + i*32); } + + uint8_t root[32], expect[32]; + CHECK(naut_merkle_root(leaves, 3, root) == NAUT_OK); + + /* recompute by hand: n0=H(l0||l1), n1=H(l2||zero), root=H(n0||n1) */ + uint8_t zero[32] = {0}, n0[32], n1[32], cat[64]; + memcpy(cat, leaves+0, 32); memcpy(cat+32, leaves+32, 32); naut_sha256(cat, 64, n0); + memcpy(cat, leaves+64, 32); memcpy(cat+32, zero, 32); naut_sha256(cat, 64, n1); + memcpy(cat, n0, 32); memcpy(cat+32, n1, 32); naut_sha256(cat, 64, expect); + CHECK(memcmp(root, expect, 32) == 0); + + /* single leaf: root == leaf */ + CHECK(naut_merkle_root(leaves, 1, root) == NAUT_OK); + CHECK(memcmp(root, leaves, 32) == 0); + + /* leaves from a 40 KiB buffer -> 3 leaves (16K,16K,8K) */ + size_t len = 40*1024; + uint8_t *blob = malloc(len); memset(blob, 0x5a, len); + uint8_t lv[3*32]; + CHECK_EQ(naut_merkle_leaves(blob, len, lv), 3); + free(blob); + } + + TEST_MAIN_END(); +} diff --git a/tests/unit/test_dht.c b/tests/unit/test_dht.c new file mode 100644 index 0000000..c8370d1 --- /dev/null +++ b/tests/unit/test_dht.c @@ -0,0 +1,98 @@ +#include "naut/bencode.h" +#include "naut/dht.h" +#include "test.h" + +#include +#include + +static void check_get_peers_query(void) { + uint8_t tx[2] = { 0x12, 0x34 }; + uint8_t id[20], hash[20]; + for (size_t i = 0; i < 20; i++) { + id[i] = (uint8_t)i; + hash[i] = (uint8_t)(0x80 + i); + } + + uint8_t *query = NULL; + size_t query_len = 0; + CHECK(naut_dht_build_get_peers(tx, sizeof tx, id, hash, + &query, &query_len) == NAUT_OK); + + naut_bc_doc *doc = NULL; + CHECK(naut_bc_parse(query, query_len, &doc) == NAUT_OK); + const naut_bc *root = naut_bc_root(doc); + CHECK(naut_bc_str_eq(naut_bc_dict_get(root, "y"), "q")); + CHECK(naut_bc_str_eq(naut_bc_dict_get(root, "q"), "get_peers")); + + const naut_bc *args = naut_bc_dict_get(root, "a"); + const uint8_t *p = NULL; + size_t n = 0; + CHECK(naut_bc_get_str(naut_bc_dict_get(args, "id"), &p, &n)); + CHECK(n == 20 && memcmp(p, id, 20) == 0); + CHECK(naut_bc_get_str(naut_bc_dict_get(args, "info_hash"), &p, &n)); + CHECK(n == 20 && memcmp(p, hash, 20) == 0); + + naut_bc_free(doc); + free(query); +} + +static void check_response(void) { + uint8_t packet[256]; + size_t len = 0; + const char *prefix = "d1:rd2:id20:"; + memcpy(packet + len, prefix, strlen(prefix)); + len += strlen(prefix); + for (size_t i = 0; i < 20; i++) packet[len++] = (uint8_t)(0x20 + i); + + const char *nodes = "5:nodes26:"; + memcpy(packet + len, nodes, strlen(nodes)); + len += strlen(nodes); + for (size_t i = 0; i < 20; i++) packet[len++] = (uint8_t)(0x40 + i); + packet[len++] = 192; packet[len++] = 0; packet[len++] = 2; packet[len++] = 9; + packet[len++] = 0x1a; packet[len++] = 0xe1; + + const char *suffix = "5:token3:abc6:valuesl6:"; + memcpy(packet + len, suffix, strlen(suffix)); + len += strlen(suffix); + packet[len++] = 203; packet[len++] = 0; packet[len++] = 113; packet[len++] = 7; + packet[len++] = 0xc8; packet[len++] = 0xd5; + const char *tail = "ee1:t2:aa1:y1:re"; + memcpy(packet + len, tail, strlen(tail)); + len += strlen(tail); + + naut_dht_response response; + CHECK(naut_dht_parse_response(packet, len, &response) == NAUT_OK); + CHECK(response.type == NAUT_DHT_RESPONSE); + CHECK(response.transaction_len == 2 && + memcmp(response.transaction, "aa", 2) == 0); + CHECK(response.has_id && response.id[0] == 0x20); + CHECK(response.token_len == 3 && + memcmp(response.token, "abc", 3) == 0); + CHECK(response.num_nodes == 1); + CHECK(response.nodes[0].ip[0] == 192 && + response.nodes[0].port == 6881); + CHECK(response.num_peers == 1); + CHECK(response.peers[0].ip[0] == 203 && + response.peers[0].port == 51413); + naut_dht_response_free(&response); +} + +int main(void) { + check_get_peers_query(); + check_response(); + + const char error[] = "d1:eli203e12:Server errore1:t2:zz1:y1:ee"; + naut_dht_response response; + CHECK(naut_dht_parse_response((const uint8_t *)error, sizeof error - 1, + &response) == NAUT_OK); + CHECK(response.type == NAUT_DHT_ERROR && response.error_code == 203); + naut_dht_response_free(&response); + + const char malformed[] = + "d1:rd2:id20:abcdefghijklmnopqrst5:nodes1:xe1:t1:a1:y1:re"; + CHECK(naut_dht_parse_response((const uint8_t *)malformed, + sizeof malformed - 1, + &response) == NAUT_ERR_PROTO); + + TEST_MAIN_END(); +} diff --git a/tests/unit/test_download.c b/tests/unit/test_download.c new file mode 100644 index 0000000..51ab06e --- /dev/null +++ b/tests/unit/test_download.c @@ -0,0 +1,131 @@ +/* Drives the download state machine without a network: feeds blocks straight + * from the original file (as a perfect seed would) and checks the reassembled, + * hash-verified output on disk is byte-identical. Also checks a corrupt block + * is rejected at piece completion. */ +#include "naut/piece.h" +#include "naut/metainfo.h" +#include "naut/storage.h" +#include "naut/worker.h" +#include "test.h" +#include +#include +#include +#include +#include + +#ifndef NAUT_FIXTURES +#define NAUT_FIXTURES "tests/fixtures" +#endif + +static uint8_t *slurp(const char *path, size_t *len) { + FILE *f = fopen(path, "rb"); + if (!f) { fprintf(stderr, " open %s failed\n", path); return NULL; } + fseek(f, 0, SEEK_END); long n = ftell(f); fseek(f, 0, SEEK_SET); + uint8_t *b = malloc(n); + if (fread(b, 1, n, f) != (size_t)n) { fclose(f); free(b); return NULL; } + fclose(f); *len = (size_t)n; return b; +} + +int main(void) { + /* load torrent + its original payload */ + size_t tlen, olen; + uint8_t *tor = slurp(NAUT_FIXTURES "/single_v1.torrent", &tlen); + uint8_t *orig = slurp(NAUT_FIXTURES "/data/single.bin", &olen); + CHECK(tor && orig); + if (!tor || !orig) return 1; + + naut_metainfo mi; + CHECK(naut_metainfo_parse(tor, tlen, &mi) == NAUT_OK); + CHECK_EQ(mi.total_length, (int64_t)olen); + + char tmpl[] = "/tmp/naut_dl_XXXXXX"; + char *root = mkdtemp(tmpl); + naut_err err; + naut_storage *st = naut_storage_open(mi.files, mi.num_files, root, &err); + CHECK(st && err == NAUT_OK); + + naut_download *d = naut_download_create(&mi, st); + CHECK(d != NULL); + + /* feed every requested block straight from the original */ + uint32_t idx, begin, len; + int completed = 0; + while (naut_download_next_request(d, &idx, &begin, &len)) { + uint64_t global = (uint64_t)idx * (uint64_t)mi.piece_length + begin; + bool done = false; + CHECK(naut_download_on_block(d, idx, begin, orig + global, len, &done) == NAUT_OK); + if (done) completed++; + } + CHECK(naut_download_complete(d)); + CHECK_EQ(completed, (int)naut_download_num_pieces(d)); + CHECK_EQ((long long)naut_download_bytes_done(d), (long long)olen); + naut_download_destroy(d); + + /* on-disk result must be byte-identical to the original */ + naut_storage_sync(st); + naut_storage_close(st); + char outpath[512]; snprintf(outpath, sizeof outpath, "%s/%s", root, mi.files[0].path); + size_t glen; uint8_t *got = slurp(outpath, &glen); + CHECK(got && glen == olen && memcmp(got, orig, olen) == 0); + free(got); + + /* The same path with SHA-1 verification offloaded to bounded workers. */ + { + char t2[] = "/tmp/naut_async_XXXXXX"; + char *r2 = mkdtemp(t2); + naut_storage *st2 = naut_storage_open(mi.files, mi.num_files, r2, &err); + naut_download *d2 = naut_download_create(&mi, st2); + naut_worker_pool *workers = naut_worker_pool_create(2, 64, -1); + CHECK(st2 && d2 && workers); + naut_download_set_worker_pool(d2, workers); + while (naut_download_next_request(d2, &idx, &begin, &len)) { + uint64_t global = + (uint64_t)idx * (uint64_t)mi.piece_length + begin; + bool done = false; + CHECK(naut_download_on_block( + d2, idx, begin, orig + global, len, &done) == NAUT_OK); + CHECK(!done); + } + uint32_t async_completed = 0; + while (!naut_download_complete(d2)) { + struct pollfd pfd = { + .fd = naut_worker_eventfd(workers), + .events = POLLIN, + }; + CHECK(poll(&pfd, 1, 5000) > 0); + uint64_t count; + (void)read(pfd.fd, &count, sizeof count); + uint32_t batch = 0; + CHECK(naut_download_poll(d2, &batch) == NAUT_OK); + async_completed += batch; + } + CHECK_EQ(async_completed, (int)mi.num_pieces); + naut_worker_pool_destroy(workers); + naut_download_destroy(d2); + naut_storage_close(st2); + char cmd[256]; + snprintf(cmd, sizeof cmd, "rm -rf '%s'", r2); + if (system(cmd)) {} + } + + /* corrupt-block rejection: a bad first block fails SHA-1 at completion */ + { + char t2[] = "/tmp/naut_dl2_XXXXXX"; char *r2 = mkdtemp(t2); + naut_storage *st2 = naut_storage_open(mi.files, mi.num_files, r2, &err); + naut_download *d2 = naut_download_create(&mi, st2); + CHECK(naut_download_next_request(d2, &idx, &begin, &len)); /* piece 0, block 0 */ + uint8_t bad[NAUT_BLOCK]; + memcpy(bad, orig, len); bad[0] ^= 0xff; /* flip a bit */ + bool done = false; + CHECK(naut_download_on_block(d2, idx, begin, bad, len, &done) == NAUT_ERR_PROTO); + CHECK(!done); + naut_download_destroy(d2); + naut_storage_close(st2); + char cmd[256]; snprintf(cmd, sizeof cmd, "rm -rf '%s'", r2); if (system(cmd)) {} + } + + char cmd[256]; snprintf(cmd, sizeof cmd, "rm -rf '%s'", root); if (system(cmd)) {} + naut_metainfo_free(&mi); + free(tor); free(orig); + TEST_MAIN_END(); +} diff --git a/tests/unit/test_extension.c b/tests/unit/test_extension.c new file mode 100644 index 0000000..040d269 --- /dev/null +++ b/tests/unit/test_extension.c @@ -0,0 +1,87 @@ +#include "naut/extension.h" +#include "naut/peer.h" +#include "test.h" + +#include +#include + +static const uint8_t *ext_payload(const uint8_t *frame, size_t len, + uint8_t *ext_id, size_t *payload_len) { + naut_msg msg; + CHECK(naut_peer_msg_parse(frame, len, &msg) == (int)len); + CHECK(msg.type == NAUT_MSG_EXTENDED && msg.payload_len >= 1); + *ext_id = msg.payload[0]; + *payload_len = msg.payload_len - 1; + return msg.payload + 1; +} + +int main(void) { + uint8_t *frame = NULL; + size_t frame_len = 0, payload_len = 0; + uint8_t ext_id = 0; + + CHECK(naut_ext_build_handshake(NAUT_EXT_UT_METADATA, NAUT_EXT_UT_PEX, + 31235, 6881, &frame, &frame_len) == NAUT_OK); + const uint8_t *payload = ext_payload(frame, frame_len, &ext_id, &payload_len); + CHECK_EQ(ext_id, 0); + naut_ext_handshake hs; + CHECK(naut_ext_parse_handshake(payload, payload_len, &hs) == NAUT_OK); + CHECK_EQ(hs.ut_metadata, NAUT_EXT_UT_METADATA); + CHECK_EQ(hs.ut_pex, NAUT_EXT_UT_PEX); + CHECK_EQ(hs.metadata_size, 31235); + CHECK_EQ(hs.port, 6881); + CHECK_EQ(hs.reqq, 256); + free(frame); + + CHECK(naut_metadata_build(3, NAUT_METADATA_REQUEST, 7, 0, NULL, 0, + &frame, &frame_len) == NAUT_OK); + payload = ext_payload(frame, frame_len, &ext_id, &payload_len); + CHECK_EQ(ext_id, 3); + naut_metadata_msg mm; + CHECK(naut_metadata_parse(payload, payload_len, &mm) == NAUT_OK); + CHECK(mm.type == NAUT_METADATA_REQUEST && mm.piece == 7 && mm.data_len == 0); + free(frame); + + uint8_t metadata[100]; + for (size_t i = 0; i < sizeof metadata; i++) metadata[i] = (uint8_t)i; + CHECK(naut_metadata_build(3, NAUT_METADATA_DATA, 1, + NAUT_METADATA_BLOCK + sizeof metadata, + metadata, sizeof metadata, + &frame, &frame_len) == NAUT_OK); + payload = ext_payload(frame, frame_len, &ext_id, &payload_len); + CHECK(naut_metadata_parse(payload, payload_len, &mm) == NAUT_OK); + CHECK(mm.type == NAUT_METADATA_DATA && mm.piece == 1); + CHECK_EQ(mm.total_size, NAUT_METADATA_BLOCK + sizeof metadata); + CHECK_EQ(mm.data_len, sizeof metadata); + CHECK(memcmp(mm.data, metadata, sizeof metadata) == 0); + free(frame); + + naut_peer_addr added[2] = { + { { 1, 2, 3, 4 }, 6881 }, + { { 10, 0, 0, 2 }, 51413 }, + }; + naut_peer_addr dropped[1] = { { { 192, 0, 2, 7 }, 7000 } }; + uint8_t flags[2] = { 0x01, 0x12 }; + naut_pex_msg pex = { + .added = added, .added_flags = flags, .num_added = 2, + .dropped = dropped, .num_dropped = 1, + }; + CHECK(naut_pex_build(4, &pex, &frame, &frame_len) == NAUT_OK); + payload = ext_payload(frame, frame_len, &ext_id, &payload_len); + CHECK_EQ(ext_id, 4); + naut_pex_msg parsed; + CHECK(naut_pex_parse(payload, payload_len, &parsed) == NAUT_OK); + CHECK_EQ(parsed.num_added, 2); + CHECK_EQ(parsed.num_dropped, 1); + CHECK(parsed.added[0].port == 6881 && parsed.added[1].port == 51413); + CHECK(parsed.added_flags[0] == 0x01 && parsed.added_flags[1] == 0x12); + CHECK(parsed.dropped[0].ip[0] == 192 && parsed.dropped[0].port == 7000); + naut_pex_free(&parsed); + free(frame); + + const uint8_t malformed[] = "d5:added5:abcdee"; + CHECK(naut_pex_parse(malformed, sizeof malformed - 1, &parsed) == + NAUT_ERR_PROTO); + + TEST_MAIN_END(); +} diff --git a/tests/unit/test_filemove.c b/tests/unit/test_filemove.c new file mode 100644 index 0000000..8b2b79b --- /dev/null +++ b/tests/unit/test_filemove.c @@ -0,0 +1,116 @@ +/* Proves the "move files as they finish" feature at the engine level: + * - per-file completion fires the moment a file's last piece verifies, + * BEFORE the whole torrent is done; + * - a completed file can be relocated mid-download while later pieces (for + * other files) keep arriving, with no corruption. */ +#include "naut/piece.h" +#include "naut/metainfo.h" +#include "naut/storage.h" +#include "test.h" +#include +#include +#include + +#ifndef NAUT_FIXTURES +#define NAUT_FIXTURES "tests/fixtures" +#endif + +static uint8_t *slurp(const char *path, size_t *len) { + FILE *f = fopen(path, "rb"); if (!f) return NULL; + fseek(f, 0, SEEK_END); long n = ftell(f); fseek(f, 0, SEEK_SET); + uint8_t *b = malloc(n); + if (fread(b, 1, n, f) != (size_t)n) { fclose(f); free(b); return NULL; } + fclose(f); *len = (size_t)n; return b; +} + +struct cbctx { + naut_download *d; + naut_storage *st; + char destdir[256]; + int order[8], norder; + bool moved0; + bool file0_done_while_incomplete; +}; + +static void on_file(void *ctx, uint32_t fidx, const char *path) { + struct cbctx *c = ctx; + c->order[c->norder++] = (int)fidx; + if (fidx == 0 && !c->moved0) { + c->file0_done_while_incomplete = !naut_download_complete(c->d); + char dest[512]; snprintf(dest, sizeof dest, "%s/done_a.txt", c->destdir); + /* relocate file 0 RIGHT NOW, mid-download */ + CHECK(naut_storage_relocate(c->st, 0, dest) == NAUT_OK); + c->moved0 = true; + (void)path; + } +} + +int main(void) { + size_t tlen; + uint8_t *tor = slurp(NAUT_FIXTURES "/multi_v1.torrent", &tlen); + CHECK(tor != NULL); if (!tor) return 1; + naut_metainfo mi; + CHECK(naut_metainfo_parse(tor, tlen, &mi) == NAUT_OK); + CHECK_EQ(mi.num_files, 2); + + /* build the flat byte space from the original files, in torrent order */ + uint8_t *global = malloc(mi.total_length); + uint64_t goff = 0; + for (size_t i = 0; i < mi.num_files; i++) { + char p[512]; snprintf(p, sizeof p, "%s/data/%s", NAUT_FIXTURES, mi.files[i].path); + size_t fl; uint8_t *fb = slurp(p, &fl); + CHECK(fb && fl == (size_t)mi.files[i].length); + memcpy(global + goff, fb, fl); goff += fl; free(fb); + } + + char tmpl[] = "/tmp/naut_fm_XXXXXX"; char *root = mkdtemp(tmpl); + char destdir[256]; snprintf(destdir, sizeof destdir, "%s_moved", root); + + naut_err err; + naut_storage *st = naut_storage_open(mi.files, mi.num_files, root, &err); + CHECK(st && err == NAUT_OK); + naut_download *d = naut_download_create(&mi, st); + CHECK(d != NULL); + + struct cbctx ctx; memset(&ctx, 0, sizeof ctx); + ctx.d = d; ctx.st = st; snprintf(ctx.destdir, sizeof ctx.destdir, "%s", destdir); + naut_download_set_file_cb(d, on_file, &ctx); + + uint32_t idx, begin, len; + while (naut_download_next_request(d, &idx, &begin, &len)) { + uint64_t g = (uint64_t)idx * (uint64_t)mi.piece_length + begin; + bool done = false; + CHECK(naut_download_on_block(d, idx, begin, global + g, len, &done) == NAUT_OK); + } + CHECK(naut_download_complete(d)); + + /* file 0 finished and was moved while the torrent was still incomplete */ + CHECK_EQ(ctx.norder, 2); + CHECK_EQ(ctx.order[0], 0); /* file 0 completed first */ + CHECK_EQ(ctx.order[1], 1); + CHECK(ctx.file0_done_while_incomplete); /* fired before whole-torrent done */ + CHECK(ctx.moved0); + CHECK(naut_download_file_complete(d, 0) && naut_download_file_complete(d, 1)); + + naut_storage_sync(st); + naut_storage_close(st); + + /* relocated file 0 is at the destination, byte-correct */ + char dest[512]; snprintf(dest, sizeof dest, "%s/done_a.txt", destdir); + size_t dl; uint8_t *got0 = slurp(dest, &dl); + CHECK(got0 && dl == (size_t)mi.files[0].length && memcmp(got0, global, dl) == 0); + free(got0); + + /* file 1 stayed put and is byte-correct (pieces after the move landed fine) */ + char p1[512]; snprintf(p1, sizeof p1, "%s/%s", root, mi.files[1].path); + size_t l1; uint8_t *got1 = slurp(p1, &l1); + CHECK(got1 && l1 == (size_t)mi.files[1].length && + memcmp(got1, global + mi.files[0].length, l1) == 0); + free(got1); + + naut_download_destroy(d); + free(global); free(tor); naut_metainfo_free(&mi); + char cmd[600]; snprintf(cmd, sizeof cmd, "rm -rf '%s' '%s'", root, destdir); + if (system(cmd)) {} + TEST_MAIN_END(); +} diff --git a/tests/unit/test_metainfo.c b/tests/unit/test_metainfo.c new file mode 100644 index 0000000..2a8f9cc --- /dev/null +++ b/tests/unit/test_metainfo.c @@ -0,0 +1,143 @@ +#include "naut/metainfo.h" +#include "naut/bencode.h" +#include "test.h" +#include +#include +#include + +#ifndef NAUT_FIXTURES +#define NAUT_FIXTURES "tests/fixtures" +#endif + +static uint8_t *slurp(const char *name, size_t *len) { + char path[1024]; + snprintf(path, sizeof path, "%s/%s", NAUT_FIXTURES, name); + FILE *f = fopen(path, "rb"); + if (!f) { fprintf(stderr, " cannot open %s\n", path); return NULL; } + fseek(f, 0, SEEK_END); long n = ftell(f); fseek(f, 0, SEEK_SET); + uint8_t *b = malloc(n); + if (fread(b, 1, n, f) != (size_t)n) { fclose(f); free(b); return NULL; } + fclose(f); *len = (size_t)n; return b; +} + +static int hexeq(const uint8_t *got, const char *hex, size_t n) { + for (size_t i = 0; i < n; i++) { unsigned b; sscanf(hex + i*2, "%2x", &b); + if (got[i] != (uint8_t)b) return 0; } + return 1; +} + +static naut_metainfo load(const char *name) { + naut_metainfo mi; memset(&mi, 0, sizeof mi); + size_t len; uint8_t *b = slurp(name, &len); + if (!b) { return mi; } + naut_err e = naut_metainfo_parse(b, len, &mi); + free(b); + if (e != NAUT_OK) fprintf(stderr, " parse %s failed: %s\n", name, naut_strerror(e)); + return mi; +} + +int main(void) { + /* --- v1 single-file: info-hash must match libtorrent exactly --- */ + { + naut_metainfo mi = load("single_v1.torrent"); + CHECK(mi.has_v1 && !mi.has_v2); + CHECK(hexeq(mi.infohash_v1, "7f2555dfd18ba1c4a024264e939d7a5f8b25311b", 20)); + CHECK_EQ(mi.num_pieces, 13); + CHECK_EQ(mi.piece_length, 16384); + CHECK_EQ(mi.total_length, 200000); + CHECK_EQ(mi.num_files, 1); + CHECK_EQ(mi.num_trackers, 2); + char hex[41]; naut_infohash_v1_hex(&mi, hex); + CHECK(strcmp(hex, "7f2555dfd18ba1c4a024264e939d7a5f8b25311b") == 0); + naut_metainfo_free(&mi); + } + + /* --- v1 multi-file --- */ + { + naut_metainfo mi = load("multi_v1.torrent"); + CHECK(mi.has_v1); + CHECK(hexeq(mi.infohash_v1, "a1ea0ef182fd34532e8c4bddbc1dd25669a874ac", 20)); + CHECK_EQ(mi.num_pieces, 9); + CHECK_EQ(mi.total_length, 145000); + CHECK(mi.num_files == 2); + /* multi-file paths are '/'-joined; one lives under sub/ */ + int found_sub = 0; + for (size_t i = 0; i < mi.num_files; i++) + if (strstr(mi.files[i].path, "sub/")) found_sub = 1; + CHECK(found_sub); + naut_metainfo_free(&mi); + } + + /* --- hybrid: BOTH info-hashes present and correct --- */ + { + naut_metainfo mi = load("hybrid.torrent"); + CHECK(mi.has_v1 && mi.has_v2); + CHECK(hexeq(mi.infohash_v1, "7646920c5608655e18c7c3a8425a4f8f767e2fcf", 20)); + CHECK(hexeq(mi.infohash_v2, + "89e5335bbee10fecdfa5f6856db99c13cde84cdb5e1267aa7463574e87c8e6be", 32)); + naut_metainfo_free(&mi); + } + + /* --- v2-only: v2 hash, no v1 --- */ + { + naut_metainfo mi = load("v2.torrent"); + CHECK(!mi.has_v1 && mi.has_v2); + CHECK(hexeq(mi.infohash_v2, + "c8aab2c350b7f887135bc867ec4d2afccbc019d240584a443fd4bdbf1cca5f55", 32)); + naut_metainfo_free(&mi); + } + + /* --- magnet: hex btih + btmh + dn + tr --- */ + { + naut_magnet m; + const char *uri = + "magnet:?xt=urn:btih:7646920c5608655e18c7c3a8425a4f8f767e2fcf" + "&xt=urn:btmh:122089e5335bbee10fecdfa5f6856db99c13cde84cdb5e1267aa7463574e87c8e6be" + "&dn=multi+folder&tr=udp%3A%2F%2Ftracker.example.com%3A8080"; + CHECK(naut_magnet_parse(uri, &m) == NAUT_OK); + CHECK(m.has_v1 && m.has_v2); + CHECK(hexeq(m.infohash_v1, "7646920c5608655e18c7c3a8425a4f8f767e2fcf", 20)); + CHECK(hexeq(m.infohash_v2, + "89e5335bbee10fecdfa5f6856db99c13cde84cdb5e1267aa7463574e87c8e6be", 32)); + CHECK(m.name && strcmp(m.name, "multi folder") == 0); /* + and %xx decoded */ + CHECK_EQ(m.num_trackers, 1); + CHECK(strcmp(m.trackers[0], "udp://tracker.example.com:8080") == 0); + naut_magnet_free(&m); + } + + /* --- magnet: base32 btih --- */ + { + naut_magnet m; + CHECK(naut_magnet_parse( + "magnet:?xt=urn:btih:P4SVLX6RROQ4JIBEEZHJHHL2L6FSKMI3", &m) == NAUT_OK); + CHECK(m.has_v1); + CHECK(hexeq(m.infohash_v1, "7f2555dfd18ba1c4a024264e939d7a5f8b25311b", 20)); + naut_magnet_free(&m); + + CHECK(naut_magnet_parse("magnet:?dn=nohash", &m) == NAUT_ERR_PROTO); + CHECK(naut_magnet_parse("http://not-a-magnet", &m) == NAUT_ERR_INVAL); + } + + /* --- BEP-9 raw info dictionary entry point preserves its exact hash --- */ + { + size_t len; + uint8_t *torrent = slurp("single_v1.torrent", &len); + naut_bc_doc *doc = NULL; + CHECK(torrent && naut_bc_parse(torrent, len, &doc) == NAUT_OK); + const naut_bc *info = + naut_bc_dict_get(naut_bc_root(doc), "info"); + const char *trackers[] = { "udp://tracker.example:80" }; + naut_metainfo mi; + CHECK(info && naut_metainfo_parse_info( + info->raw, info->raw_len, trackers, 1, &mi) == NAUT_OK); + CHECK(hexeq(mi.infohash_v1, + "7f2555dfd18ba1c4a024264e939d7a5f8b25311b", 20)); + CHECK(mi.num_trackers == 1 && + strcmp(mi.trackers[0], trackers[0]) == 0); + naut_metainfo_free(&mi); + naut_bc_free(doc); + free(torrent); + } + + TEST_MAIN_END(); +} diff --git a/tests/unit/test_mpmc.c b/tests/unit/test_mpmc.c new file mode 100644 index 0000000..a1bcf1b --- /dev/null +++ b/tests/unit/test_mpmc.c @@ -0,0 +1,63 @@ +#include "naut/mpmc.h" +#include "test.h" +#include + +/* Many producers and many consumers pass tagged integers through the queue; + * verify nothing is lost or duplicated. */ + +#define CAP 1024 +#define NPROD 4 +#define NCONS 4 +#define PER_PROD 100000 + +static naut_mpmc q; +static _Atomic long produced_sum, consumed_sum; +static _Atomic int consumed_cnt; +static _Atomic int prod_done; + +static void *producer(void *arg) { + long base = (long)(intptr_t)arg * PER_PROD + 1; + for (long i = 0; i < PER_PROD; i++) { + long v = base + i; + while (!naut_mpmc_push(&q, (void *)(intptr_t)v)) sched_yield(); + atomic_fetch_add(&produced_sum, v); + } + atomic_fetch_add(&prod_done, 1); + return NULL; +} + +static void *consumer(void *arg) { + (void)arg; + void *p; + for (;;) { + if (naut_mpmc_pop(&q, &p)) { + atomic_fetch_add(&consumed_sum, (long)(intptr_t)p); + atomic_fetch_add(&consumed_cnt, 1); + } else if (atomic_load(&prod_done) == NPROD) { + if (!naut_mpmc_pop(&q, &p)) break; /* drained */ + atomic_fetch_add(&consumed_sum, (long)(intptr_t)p); + atomic_fetch_add(&consumed_cnt, 1); + } else { + sched_yield(); + } + } + return NULL; +} + +int main(void) { + CHECK(naut_mpmc_init(&q, CAP) == NAUT_OK); + CHECK(naut_mpmc_init(&q, 1000) == NAUT_ERR_INVAL); /* not pow2 */ + + pthread_t pr[NPROD], co[NCONS]; + for (int i = 0; i < NCONS; i++) pthread_create(&co[i], NULL, consumer, NULL); + for (int i = 0; i < NPROD; i++) + pthread_create(&pr[i], NULL, producer, (void *)(intptr_t)i); + for (int i = 0; i < NPROD; i++) pthread_join(pr[i], NULL); + for (int i = 0; i < NCONS; i++) pthread_join(co[i], NULL); + + CHECK_EQ(atomic_load(&consumed_cnt), NPROD * PER_PROD); + CHECK_EQ(atomic_load(&consumed_sum), atomic_load(&produced_sum)); + + naut_mpmc_destroy(&q); + TEST_MAIN_END(); +} diff --git a/tests/unit/test_mse.c b/tests/unit/test_mse.c new file mode 100644 index 0000000..abb6849 --- /dev/null +++ b/tests/unit/test_mse.c @@ -0,0 +1,56 @@ +/* Drives the MSE handshake state machine without a socket. Full-handshake + * correctness is proven against libtorrent in interop_mse; this guards the + * sans-IO plumbing (state transitions, fragmented pull, DH validation) so it + * stays covered even where libtorrent is unavailable. */ +#include "naut/mse.h" +#include "test.h" +#include + +int main(void) { + uint8_t info_hash[20], peer_id[NAUT_PEERID_LEN]; + memset(info_hash, 0xAB, sizeof info_hash); + memset(peer_id, 0xCD, sizeof peer_id); + + /* begin → must want to write its 96-byte public key first. */ + naut_mse_handshake *h = naut_mse_handshake_begin(info_hash, peer_id, 0); + CHECK(h != NULL); + CHECK_EQ(naut_mse_handshake_status(h), NAUT_MSE_HS_NEED_WRITE); + + /* Drain the public key one byte at a time; it must be exactly 96 bytes, + * after which the machine flips to waiting for the peer's key. */ + uint8_t pub[128]; + size_t total = 0, n; + while ((n = naut_mse_handshake_pull(h, pub + total, 1)) > 0) total += n; + CHECK_EQ((int)total, NAUT_MSE_DH_LEN); + CHECK_EQ(naut_mse_handshake_status(h), NAUT_MSE_HS_NEED_READ); + /* A real DH public key is never all-zero. */ + uint8_t zero[NAUT_MSE_DH_LEN] = {0}; + CHECK(memcmp(pub, zero, NAUT_MSE_DH_LEN) != 0); + + /* finish() before completion must refuse rather than hand out junk. */ + naut_mse_stream stream; + uint8_t remote_hs[NAUT_HANDSHAKE_LEN]; + CHECK_EQ(naut_mse_handshake_finish(h, &stream, remote_hs), NAUT_ERR_AGAIN); + + /* Feed an invalid (zero) peer public key fragmented across calls; the DH + * validation must reject it (0 < 2) and latch the error state. */ + size_t consumed_total = 0; + naut_mse_hs_status st = NAUT_MSE_HS_NEED_READ; + for (int i = 0; i < NAUT_MSE_DH_LEN; i++) { + size_t consumed = 0; + uint8_t b = 0; + st = naut_mse_handshake_feed(h, &b, 1, &consumed); + consumed_total += consumed; + if (st == NAUT_MSE_HS_ERROR) break; + } + CHECK_EQ(st, NAUT_MSE_HS_ERROR); + CHECK(consumed_total <= NAUT_MSE_DH_LEN); + CHECK_EQ(naut_mse_handshake_finish(h, &stream, remote_hs), NAUT_ERR_PROTO); + naut_mse_handshake_free(h); + + /* Bad arguments are rejected, not crashed on. */ + CHECK(naut_mse_handshake_begin(NULL, peer_id, 0) == NULL); + CHECK(naut_mse_handshake_begin(info_hash, NULL, 0) == NULL); + + TEST_MAIN_END(); +} diff --git a/tests/unit/test_peer.c b/tests/unit/test_peer.c new file mode 100644 index 0000000..80ec4a7 --- /dev/null +++ b/tests/unit/test_peer.c @@ -0,0 +1,80 @@ +#include "naut/peer.h" +#include "test.h" +#include + +int main(void) { + uint8_t ih[20], pid[20], ih2[20], pid2[20]; + for (int i = 0; i < 20; i++) { ih[i] = (uint8_t)(i + 1); pid[i] = (uint8_t)(100 + i); } + + /* handshake round-trip, with the BEP-10 extension reserved bit set */ + uint8_t hs[NAUT_HANDSHAKE_LEN]; + uint64_t reserved = 0x0000000000100000ULL; /* ext protocol bit */ + naut_peer_handshake_build(hs, ih, pid, reserved); + CHECK_EQ(hs[0], 19); + CHECK(memcmp(hs + 1, "BitTorrent protocol", 19) == 0); + uint64_t got_res = 0; + CHECK(naut_peer_handshake_parse(hs, ih2, pid2, &got_res)); + CHECK(memcmp(ih, ih2, 20) == 0 && memcmp(pid, pid2, 20) == 0); + CHECK_EQ((long long)got_res, (long long)reserved); + hs[3] = 'X'; /* corrupt protocol string */ + CHECK(!naut_peer_handshake_parse(hs, ih2, pid2, &got_res)); + + naut_msg m; + uint8_t b[64]; + + /* simple messages */ + size_t n = naut_peer_msg_simple(b, NAUT_MSG_INTERESTED); + CHECK_EQ(n, 5); + CHECK_EQ(naut_peer_msg_parse(b, n, &m), 5); + CHECK_EQ(m.type, NAUT_MSG_INTERESTED); + + /* keep-alive */ + n = naut_peer_keepalive(b); + CHECK_EQ(naut_peer_msg_parse(b, n, &m), 4); + CHECK_EQ(m.type, NAUT_MSG_KEEPALIVE); + + /* have */ + n = naut_peer_msg_have(b, 0x01020304); + CHECK_EQ(naut_peer_msg_parse(b, n, &m), 9); + CHECK(m.type == NAUT_MSG_HAVE && m.index == 0x01020304); + + /* request */ + n = naut_peer_msg_request(b, 7, 16384, 16384); + CHECK_EQ(naut_peer_msg_parse(b, n, &m), 17); + CHECK(m.type == NAUT_MSG_REQUEST && m.index == 7 && m.begin == 16384 && m.length == 16384); + + /* piece: header + block, payload aliases input */ + { + uint8_t blk[16384]; + for (int i = 0; i < 16384; i++) blk[i] = (uint8_t)(i & 0xff); + uint8_t frame[13 + 16384]; + naut_peer_msg_piece_header(frame, 3, 32768, 16384); + memcpy(frame + 13, blk, 16384); + int c = naut_peer_msg_parse(frame, sizeof frame, &m); + CHECK_EQ(c, (int)sizeof frame); + CHECK(m.type == NAUT_MSG_PIECE && m.index == 3 && m.begin == 32768); + CHECK_EQ(m.payload_len, 16384); + CHECK(m.payload == frame + 13 && memcmp(m.payload, blk, 16384) == 0); + } + + /* streaming: partial frame returns 0 (need more), completes when filled */ + n = naut_peer_msg_have(b, 42); + CHECK_EQ(naut_peer_msg_parse(b, 3, &m), 0); /* < 4 length bytes */ + CHECK_EQ(naut_peer_msg_parse(b, 7, &m), 0); /* length known, body short */ + CHECK_EQ(naut_peer_msg_parse(b, 9, &m), 9); + + /* bitfield */ + uint8_t bf[4] = { 0xff, 0x80, 0x00, 0x01 }; + n = naut_peer_msg_bitfield(b, bf, 4); + CHECK_EQ(naut_peer_msg_parse(b, n, &m), (int)n); + CHECK(m.type == NAUT_MSG_BITFIELD && m.payload_len == 4 && memcmp(m.payload, bf, 4) == 0); + + /* malformed: oversized length prefix rejected */ + uint8_t bad[4] = { 0xff, 0xff, 0xff, 0xff }; + CHECK(naut_peer_msg_parse(bad, 4, &m) < 0); + /* malformed: HAVE whose body length (5) != required 4 */ + uint8_t badhave[10] = { 0,0,0,6, NAUT_MSG_HAVE, 0,0,0,1, 0 }; + CHECK(naut_peer_msg_parse(badhave, 10, &m) < 0); + + TEST_MAIN_END(); +} diff --git a/tests/unit/test_picker.c b/tests/unit/test_picker.c new file mode 100644 index 0000000..c2e5cce --- /dev/null +++ b/tests/unit/test_picker.c @@ -0,0 +1,166 @@ +/* Exercises the multi-peer swarm logic with a synthetic torrent (real SHA-1 + * piece hashes, so verification actually runs): rarest-first selection, two + * peers collaborating on one piece, duplicate-block dedup, unrequest, endgame, + * and a full multi-peer completion. */ +#include "naut/piece.h" +#include "naut/metainfo.h" +#include "naut/storage.h" +#include "naut/hash.h" +#include "naut/bitfield.h" +#include "test.h" +#include +#include + +#define NP 10 +#define PLEN 32768 /* 2 blocks per piece */ +#define TOTAL ((uint64_t)NP * PLEN) + +typedef struct { + uint32_t piece[4], begin[4]; + size_t count; +} active_requests; + +static bool is_active(void *ctx, uint32_t piece, uint32_t begin) { + active_requests *a = ctx; + for (size_t i = 0; i < a->count; i++) + if (a->piece[i] == piece && a->begin[i] == begin) return true; + return false; +} + +static void add_active(active_requests *a, uint32_t piece, uint32_t begin) { + a->piece[a->count] = piece; + a->begin[a->count] = begin; + a->count++; +} + +int main(void) { + uint8_t *data = malloc(TOTAL); + for (uint64_t i = 0; i < TOTAL; i++) data[i] = (uint8_t)(i * 1103515245u + 12345u); + uint8_t hashes[NP * NAUT_SHA1_LEN]; + for (int p = 0; p < NP; p++) naut_sha1(data + (uint64_t)p * PLEN, PLEN, hashes + p * NAUT_SHA1_LEN); + + naut_file files[1] = { { (char *)"data.bin", (int64_t)TOTAL } }; + naut_metainfo mi; memset(&mi, 0, sizeof mi); + mi.has_v1 = true; mi.num_pieces = NP; mi.piece_length = PLEN; mi.total_length = TOTAL; + mi.piece_hashes = hashes; mi.files = files; mi.num_files = 1; mi.name = (char *)"t"; + + char tmpl[] = "/tmp/naut_pick_XXXXXX"; char *root = mkdtemp(tmpl); + naut_err err; + naut_storage *st = naut_storage_open(files, 1, root, &err); + CHECK(st && err == NAUT_OK); + naut_download *d = naut_download_create(&mi, st); + CHECK(d != NULL); + + /* peer A has pieces 0..8, peer B has all 0..9 -> piece 9 is rarest (avail 1) */ + naut_bitfield ha, hb; + naut_bitfield_init(&ha, NP); naut_bitfield_init(&hb, NP); + for (int p = 0; p < NP; p++) { naut_bitfield_set(&hb, p); if (p < 9) naut_bitfield_set(&ha, p); } + naut_download_add_bitfield(d, &ha); + naut_download_add_bitfield(d, &hb); + + /* rarest-first: B's first pick must be the unique piece 9 */ + uint32_t idx, begin, len; + CHECK(naut_download_pick(d, &hb, &idx, &begin, &len)); + CHECK_EQ(idx, 9); + CHECK_EQ(begin, 0); + + /* collaboration: next pick for B finishes piece 9's second block */ + CHECK(naut_download_pick(d, &hb, &idx, &begin, &len)); + CHECK(idx == 9 && begin == NAUT_BLOCK); + /* both blocks of piece 9 now requested; A (lacks 9) gets a different piece */ + uint32_t ia, ba, la; + CHECK(naut_download_pick(d, &ha, &ia, &ba, &la)); + CHECK(ia != 9); + + /* unrequest releases a block for re-pick */ + naut_download_unrequest(d, ia, ba); + uint32_t ia2, ba2, la2; + CHECK(naut_download_pick(d, &ha, &ia2, &ba2, &la2)); + CHECK(ia2 == ia && ba2 == ba); + + /* duplicate block is ignored */ + bool done = false; + uint64_t g9 = (uint64_t)9 * PLEN; + CHECK(naut_download_on_block(d, 9, 0, data + g9, NAUT_BLOCK, &done) == NAUT_OK); + CHECK(naut_download_on_block(d, 9, 0, data + g9, NAUT_BLOCK, &done) == NAUT_OK); /* dup */ + CHECK(!done); + + /* endgame flag is off this early (20 blocks, only a couple received) */ + CHECK(!naut_download_in_endgame(d)); + + naut_download_destroy(d); + + /* full multi-peer download: alternate peers, all pieces verify */ + d = naut_download_create(&mi, st); + naut_download_add_bitfield(d, &hb); /* one peer that has everything */ + int guard = 0; + while (!naut_download_complete(d) && guard++ < 10000) { + if (!naut_download_pick(d, &hb, &idx, &begin, &len)) break; + uint64_t g = (uint64_t)idx * PLEN + begin; + CHECK(naut_download_on_block(d, idx, begin, data + g, len, &done) == NAUT_OK); + } + CHECK(naut_download_complete(d)); + CHECK(naut_download_in_endgame(d)); /* must have passed through endgame near the end */ + CHECK_EQ((long long)naut_download_bytes_done(d), (long long)TOTAL); + + naut_download_destroy(d); + + /* Endgame races each block on at most two distinct peers. Releasing one + * peer's copy must not erase the other peer's outstanding request. */ + uint8_t small_hash[NAUT_SHA1_LEN]; + naut_sha1(data, PLEN, small_hash); + naut_file small_file[1] = { { (char *)"small.bin", PLEN } }; + naut_metainfo small; memset(&small, 0, sizeof small); + small.has_v1 = true; small.num_pieces = 1; small.piece_length = PLEN; + small.total_length = PLEN; small.piece_hashes = small_hash; + small.files = small_file; small.num_files = 1; small.name = (char *)"small"; + d = naut_download_create(&small, st); + CHECK(d != NULL); + naut_bitfield one; + naut_bitfield_init(&one, 1); + naut_bitfield_set(&one, 0); + naut_download_add_bitfield(d, &one); + naut_download_add_bitfield(d, &one); + + active_requests pa = {0}, pb = {0}; + CHECK(naut_download_pick_for_peer(d, &one, is_active, &pa, + &idx, &begin, &len)); + add_active(&pa, idx, begin); + CHECK(naut_download_pick_for_peer(d, &one, is_active, &pa, + &idx, &begin, &len)); + add_active(&pa, idx, begin); + CHECK(!naut_download_pick_for_peer(d, &one, is_active, &pa, + &idx, &begin, &len)); + + CHECK(naut_download_pick_for_peer(d, &one, is_active, &pb, + &idx, &begin, &len)); + add_active(&pb, idx, begin); + CHECK(naut_download_pick_for_peer(d, &one, is_active, &pb, + &idx, &begin, &len)); + add_active(&pb, idx, begin); + CHECK(!naut_download_pick_for_peer(d, &one, is_active, &pb, + &idx, &begin, &len)); + + naut_download_unrequest(d, pa.piece[0], pa.begin[0]); + CHECK(!naut_download_pick_for_peer(d, &one, is_active, &pb, + &idx, &begin, &len)); + pa.piece[0] = pa.piece[1]; pa.begin[0] = pa.begin[1]; pa.count = 1; + CHECK(naut_download_pick_for_peer(d, &one, is_active, &pa, + &idx, &begin, &len)); + CHECK_EQ(begin, 0); + + naut_bitfield_free(&one); + naut_storage_sync(st); + /* on-disk bytes match the source */ + uint8_t *rb = malloc(TOTAL); + CHECK(naut_storage_read(st, 0, rb, TOTAL) == NAUT_OK); + CHECK(memcmp(rb, data, TOTAL) == 0); + free(rb); + + naut_download_destroy(d); + naut_storage_close(st); + naut_bitfield_free(&ha); naut_bitfield_free(&hb); + free(data); + char cmd[256]; snprintf(cmd, sizeof cmd, "rm -rf '%s'", root); if (system(cmd)) {} + TEST_MAIN_END(); +} diff --git a/tests/unit/test_pipeline.c b/tests/unit/test_pipeline.c new file mode 100644 index 0000000..220fcb8 --- /dev/null +++ b/tests/unit/test_pipeline.c @@ -0,0 +1,30 @@ +#include "naut/pipeline.h" +#include "test.h" + +int main(void) { + naut_pipeline pipeline; + naut_pipeline_init(&pipeline, NAUT_BLOCK, 4, 1024, 32); + CHECK_EQ(naut_pipeline_depth(&pipeline), 32); + + /* 16 KiB every 100 us with 20 ms RTT is about 164 MB/s and a 200-block + * BDP. Repeated samples should grow the window substantially. */ + double now = 1.0; + for (int i = 0; i < 100; i++) { + now += 0.0001; + naut_pipeline_on_block(&pipeline, NAUT_BLOCK, now - 0.020, now); + } + CHECK(naut_pipeline_depth(&pipeline) > 128); + CHECK(naut_pipeline_depth(&pipeline) <= 1024); + + uint32_t high = naut_pipeline_depth(&pipeline); + for (int i = 0; i < 100; i++) { + now += 0.050; + naut_pipeline_on_block(&pipeline, NAUT_BLOCK, now - 0.005, now); + } + CHECK(naut_pipeline_depth(&pipeline) < high); + CHECK(naut_pipeline_depth(&pipeline) >= 4); + + naut_pipeline_init(&pipeline, 0, 0, 0, 0); + CHECK_EQ(naut_pipeline_depth(&pipeline), 1); + TEST_MAIN_END(); +} diff --git a/tests/unit/test_plugin.c b/tests/unit/test_plugin.c new file mode 100644 index 0000000..c792e17 --- /dev/null +++ b/tests/unit/test_plugin.c @@ -0,0 +1,53 @@ +#include "naut/event.h" +#include "naut/plugin.h" +#include "naut/rpc.h" +#include "test.h" + +#include + +#ifndef NAUT_EXAMPLE_PLUGIN +#define NAUT_EXAMPLE_PLUGIN "naut_example.so" +#endif + +int main(void) { + naut_event_bus *events = naut_event_bus_create(); + naut_rpc_registry *rpc = naut_rpc_registry_create(); + naut_plugin_manager *plugins = + naut_plugin_manager_create(rpc, events); + CHECK(events && rpc && plugins); + CHECK(naut_plugin_load(plugins, NAUT_EXAMPLE_PLUGIN) == NAUT_OK); + CHECK_EQ(naut_plugin_count(plugins), 1); + CHECK(strcmp(naut_plugin_name(plugins, 0), "example") == 0); + CHECK_EQ(naut_plugin_storage_count(plugins), 1); + + const naut_storage_backend_v1 *backend = + naut_plugin_storage_backend(plugins, "memory"); + CHECK(backend != NULL); + naut_err error; + void *storage = backend->open("", &error); + CHECK(storage && error == NAUT_OK); + const char value[] = "plugin-storage"; + char result[sizeof value]; + CHECK(backend->write(storage, 17, value, sizeof value) == NAUT_OK); + CHECK(backend->read(storage, 17, result, sizeof result) == NAUT_OK); + CHECK(memcmp(value, result, sizeof value) == 0); + backend->close(storage); + + naut_event event = { + .type = NAUT_EVENT_TORRENT_FINISHED, + .torrent_id = 9, + }; + naut_event_emit(events, &event); + json_t *reply = naut_rpc_dispatch( + rpc, "example.events", json_null(), &error); + CHECK(error == NAUT_OK && reply); + CHECK(json_integer_value(json_object_get(reply, "finished")) == 1); + json_decref(reply); + + naut_plugin_manager_destroy(plugins); + reply = naut_rpc_dispatch(rpc, "example.events", json_null(), &error); + CHECK(reply == NULL && error == NAUT_ERR_INVAL); + naut_rpc_registry_destroy(rpc); + naut_event_bus_destroy(events); + TEST_MAIN_END(); +} diff --git a/tests/unit/test_rpc.c b/tests/unit/test_rpc.c new file mode 100644 index 0000000..aaad232 --- /dev/null +++ b/tests/unit/test_rpc.c @@ -0,0 +1,66 @@ +#include "naut/event.h" +#include "naut/rpc.h" +#include "test.h" + +#include +#include +#include + +static json_t *echo(void *context, const json_t *params, naut_err *error) { + (void)context; + *error = NAUT_OK; + return json_deep_copy(params); +} + +int main(void) { + naut_rpc_registry *registry = naut_rpc_registry_create(); + CHECK(registry != NULL); + CHECK(naut_rpc_register(registry, "echo", echo, NULL) == NAUT_OK); + CHECK(naut_rpc_register(registry, "echo", echo, NULL) == NAUT_ERR_INVAL); + json_t *params = json_pack("{s:i}", "value", 42); + naut_err error; + json_t *response = + naut_rpc_dispatch(registry, "echo", params, &error); + CHECK(error == NAUT_OK && response != NULL); + CHECK(json_integer_value(json_object_get(response, "value")) == 42); + json_decref(response); + CHECK(naut_rpc_dispatch(registry, "missing", params, &error) == NULL); + CHECK(error == NAUT_ERR_INVAL); + json_decref(params); + + int sockets[2]; + CHECK(socketpair(AF_UNIX, SOCK_STREAM, 0, sockets) == 0); + naut_event event = { + .type = NAUT_EVENT_TORRENT_FINISHED, + .torrent_id = 7, + }; + json_t *event_json = naut_rpc_event_json(&event); + CHECK(event_json != NULL); + naut_err send_error = + naut_rpc_send_json(sockets[0], NAUT_RPC_EVENT, event_json); + CHECK_EQ(send_error, NAUT_OK); + json_decref(event_json); + if (send_error != NAUT_OK) { + close(sockets[0]); + close(sockets[1]); + naut_rpc_registry_destroy(registry); + TEST_MAIN_END(); + } + naut_rpc_frame_type frame_type; + json_t *payload = NULL; + CHECK(naut_rpc_recv_json(sockets[1], &frame_type, &payload) == NAUT_OK); + CHECK(frame_type == NAUT_RPC_EVENT); + CHECK(json_integer_value(json_object_get(payload, "torrent_id")) == 7); + CHECK(strcmp(json_string_value(json_object_get(payload, "event")), + "torrent_finished") == 0); + json_decref(payload); + close(sockets[0]); + close(sockets[1]); + + naut_event_type type; + CHECK(naut_event_type_parse("file_complete", &type)); + CHECK(type == NAUT_EVENT_FILE_COMPLETE); + CHECK(!naut_event_type_parse("bad", &type)); + naut_rpc_registry_destroy(registry); + TEST_MAIN_END(); +} diff --git a/tests/unit/test_script.c b/tests/unit/test_script.c new file mode 100644 index 0000000..ce55b01 --- /dev/null +++ b/tests/unit/test_script.c @@ -0,0 +1,78 @@ +#include "naut/script.h" +#include "test.h" + +#include +#include +#include +#include + +#ifndef NAUT_PHASE7_SCRIPT +#define NAUT_PHASE7_SCRIPT "tests/fixtures/phase7.lua" +#endif + +typedef struct { + _Atomic unsigned calls; + pthread_t caller; + uint64_t torrent_id; + uint32_t file_index; + char destination[128]; +} move_capture; + +static naut_err capture_move(void *opaque, uint64_t torrent_id, + uint32_t file_index, const char *destination) { + move_capture *capture = opaque; + capture->caller = pthread_self(); + capture->torrent_id = torrent_id; + capture->file_index = file_index; + snprintf(capture->destination, sizeof capture->destination, "%s", + destination); + atomic_fetch_add(&capture->calls, 1); + return NAUT_OK; +} + +int main(void) { + pthread_t owner = pthread_self(); + move_capture capture = {0}; + naut_event_bus *events = naut_event_bus_create(); + naut_err error; + naut_script *script = naut_script_create( + events, NAUT_PHASE7_SCRIPT, 8, capture_move, &capture, &error); + CHECK(script && error == NAUT_OK); + + naut_event event = { + .type = NAUT_EVENT_TORRENT_FINISHED, + .torrent_id = 42, + }; + naut_event_emit(events, &event); + for (unsigned i = 0; i < 100 && atomic_load(&capture.calls) == 0; i++) + usleep(1000); + CHECK_EQ(atomic_load(&capture.calls), 1); + CHECK(!pthread_equal(owner, capture.caller)); + CHECK_EQ(capture.torrent_id, 42); + CHECK_EQ(capture.file_index, 0); + CHECK(strcmp(capture.destination, "/tmp/naut-phase7-finished") == 0); + + event = (naut_event) { + .type = NAUT_EVENT_FILE_COMPLETE, + .torrent_id = 42, + .index = 3, + .path = "/tmp/completed-file", + }; + naut_event_emit(events, &event); + for (unsigned i = 0; i < 100 && atomic_load(&capture.calls) < 2; i++) + usleep(1000); + CHECK_EQ(atomic_load(&capture.calls), 2); + CHECK_EQ(capture.file_index, 3); + CHECK(strcmp(capture.destination, "/tmp/completed-file.moved") == 0); + + naut_script_stats stats; + naut_script_get_stats(script, &stats); + CHECK_EQ(stats.queued, 2); + CHECK_EQ(stats.handled, 2); + CHECK_EQ(stats.errors, 0); + CHECK_EQ(stats.move_requests, 2); + + naut_script_destroy(script); + naut_event_bus_destroy(events); + TEST_MAIN_END(); +} diff --git a/tests/unit/test_storage.c b/tests/unit/test_storage.c new file mode 100644 index 0000000..9b2ffd5 --- /dev/null +++ b/tests/unit/test_storage.c @@ -0,0 +1,79 @@ +#include "naut/storage.h" +#include "test.h" +#include +#include +#include + +int main(void) { + char tmpl[] = "/tmp/naut_stor_XXXXXX"; + char *root = mkdtemp(tmpl); + CHECK(root != NULL); + + /* multi-file torrent: a write/read can straddle the file boundary */ + naut_file files[3] = { + { (char *)"a.bin", 100 }, + { (char *)"d/b.bin", 50 }, + { (char *)"d/e/c.bin", 30 }, + }; + naut_err err; + naut_storage *s = naut_storage_open(files, 3, root, &err); + CHECK(s && err == NAUT_OK); + CHECK_EQ(naut_storage_total(s), 180); + CHECK(!naut_storage_direct_enabled(s)); + + /* fill the whole space with a known pattern in one straddling write */ + uint8_t pattern[180]; + for (int i = 0; i < 180; i++) pattern[i] = (uint8_t)(i * 3 + 1); + CHECK(naut_storage_write(s, 0, pattern, 180) == NAUT_OK); + + /* read back across boundaries at an awkward offset */ + uint8_t rb[120]; + CHECK(naut_storage_read(s, 40, rb, 120) == NAUT_OK); /* spans all 3 files */ + CHECK(memcmp(rb, pattern + 40, 120) == 0); + + /* out-of-range rejected */ + CHECK(naut_storage_write(s, 170, pattern, 20) == NAUT_ERR_RANGE); + CHECK(naut_storage_sync(s) == NAUT_OK); + naut_storage_close(s); + + /* reopen and confirm persistence + nested files exist on disk */ + s = naut_storage_open(files, 3, root, &err); + CHECK(s && err == NAUT_OK); + uint8_t all[180]; + CHECK(naut_storage_read(s, 0, all, 180) == NAUT_OK); + CHECK(memcmp(all, pattern, 180) == 0); + naut_storage_close(s); + + /* Optional O_DIRECT uses aligned bulk I/O and buffered edge fallback. */ + { + char direct_tmpl[] = "/tmp/naut_direct_XXXXXX"; + char *direct_root = mkdtemp(direct_tmpl); + naut_file direct_file = { (char *)"direct.bin", 8192 }; + naut_storage_opts opts = { + .direct_io = true, + .preallocate = true, + }; + naut_storage *direct = naut_storage_open_opts( + &direct_file, 1, direct_root, &opts, &err); + CHECK(direct && err == NAUT_OK); + uint8_t *write_buf = aligned_alloc(NAUT_PAGE, 8192); + uint8_t *read_buf = aligned_alloc(NAUT_PAGE, 8192); + CHECK(write_buf && read_buf); + memset(write_buf, 0x5a, 8192); + CHECK(naut_storage_write(direct, 0, write_buf, 8192) == NAUT_OK); + CHECK(naut_storage_read(direct, 0, read_buf, 8192) == NAUT_OK); + CHECK(memcmp(write_buf, read_buf, 8192) == 0); + free(write_buf); + free(read_buf); + naut_storage_close(direct); + char direct_cmd[256]; + snprintf(direct_cmd, sizeof direct_cmd, "rm -rf '%s'", direct_root); + if (system(direct_cmd) != 0) {} + } + + /* cleanup */ + char cmd[256]; snprintf(cmd, sizeof cmd, "rm -rf '%s'", root); + if (system(cmd) != 0) { /* best effort */ } + + TEST_MAIN_END(); +} diff --git a/tests/unit/test_tracker.c b/tests/unit/test_tracker.c new file mode 100644 index 0000000..0d6cf2c --- /dev/null +++ b/tests/unit/test_tracker.c @@ -0,0 +1,115 @@ +#include "naut/tracker.h" +#include "naut/bencode.h" +#include "test.h" +#include + +int main(void) { + naut_announce_req req; + memset(&req, 0, sizeof req); + for (int i = 0; i < 20; i++) { req.info_hash[i] = (uint8_t)i; req.peer_id[i] = (uint8_t)(0x80 + i); } + req.port = 6881; req.left = 1000; req.numwant = -1; req.key = 0xdeadbeef; + req.event = NAUT_TEV_STARTED; + + /* --- HTTP announce URL --- */ + char url[1024]; + size_t n = naut_tracker_http_url("http://t.example/announce", &req, url, sizeof url); + CHECK(n > 0); + CHECK(strstr(url, "info_hash=%00%01%02") != NULL); /* binary pct-encoded */ + CHECK(strstr(url, "port=6881") != NULL); + CHECK(strstr(url, "compact=1") != NULL); + CHECK(strstr(url, "event=started") != NULL); + /* base already having a query uses '&' */ + naut_tracker_http_url("http://t.example/announce?x=1", &req, url, sizeof url); + CHECK(strstr(url, "announce?x=1&info_hash=") != NULL); + req.event = (naut_tracker_event)99; + CHECK(naut_tracker_http_url("http://t.example/announce", &req, + url, sizeof url) == 0); + req.event = NAUT_TEV_STARTED; + + /* --- HTTP response parse: compact peers --- */ + { + /* d8:intervali1800e5:peers12:e */ + uint8_t body[128]; size_t b = 0; + const char *pre = "d8:intervali1800e8:completei5e10:incompletei2e5:peers12:"; + memcpy(body, pre, strlen(pre)); b = strlen(pre); + uint8_t peers[12] = { 1,2,3,4, 0x1a,0xe1, 10,0,0,1, 0x1a,0xe2 }; + memcpy(body + b, peers, 12); b += 12; + body[b++] = 'e'; + + naut_tracker_response r; + CHECK(naut_tracker_parse_http(body, b, &r) == NAUT_OK); + CHECK_EQ(r.interval, 1800); + CHECK_EQ(r.seeders, 5); + CHECK_EQ(r.leechers, 2); + CHECK_EQ(r.num_peers, 2); + CHECK(r.peers[0].ip[0]==1 && r.peers[0].ip[3]==4 && r.peers[0].port==0x1ae1); + CHECK(r.peers[1].ip[0]==10 && r.peers[1].port==0x1ae2); + naut_tracker_response_free(&r); + } + + /* --- failure reason --- */ + { + const char *body = "d14:failure reason17:torrent not founde"; + naut_tracker_response r; + CHECK(naut_tracker_parse_http((const uint8_t *)body, strlen(body), &r) == NAUT_ERR_PROTO); + CHECK(r.failure && strcmp(r.failure, "torrent not found") == 0); + naut_tracker_response_free(&r); + } + + /* --- UDP connect codec --- */ + { + uint8_t pkt[98]; + naut_udp_build_connect(pkt, 0x11223344); + /* protocol id 0x41727101980, action 0, txid */ + CHECK(pkt[0]==0 && pkt[1]==0 && pkt[2]==0x04 && pkt[3]==0x17 && + pkt[4]==0x27 && pkt[5]==0x10 && pkt[6]==0x19 && pkt[7]==0x80); + CHECK(pkt[8]==0 && pkt[11]==0); /* action connect */ + CHECK(pkt[12]==0x11 && pkt[15]==0x44); /* txid */ + + /* build a fake connect response and parse it */ + uint8_t resp[16] = {0}; + resp[3] = 0; /* action connect */ + resp[4]=0x11; resp[5]=0x22; resp[6]=0x33; resp[7]=0x44; /* txid */ + for (int i = 0; i < 8; i++) resp[8+i] = (uint8_t)(0xA0 + i); /* conn id */ + uint64_t cid = 0; + CHECK(naut_udp_parse_connect(resp, 16, 0x11223344, &cid) == NAUT_OK); + CHECK(cid == 0xA0A1A2A3A4A5A6A7ULL); + CHECK(naut_udp_parse_connect(resp, 16, 0x99999999, &cid) == NAUT_ERR_PROTO); /* wrong txid */ + } + + /* --- UDP announce codec round-trip --- */ + { + uint8_t pkt[98]; + naut_udp_build_announce(pkt, 0xA0A1A2A3A4A5A6A7ULL, 0x55667788, &req); + CHECK(pkt[11] == 1); /* action announce */ + CHECK(memcmp(pkt + 16, req.info_hash, 20) == 0); + CHECK(memcmp(pkt + 36, req.peer_id, 20) == 0); + CHECK(pkt[83] == NAUT_TEV_STARTED); /* event low byte */ + CHECK((pkt[96]<<8 | pkt[97]) == 6881); /* port */ + + /* fake announce response: action=1, txid, interval, leech, seed, 1 peer */ + uint8_t resp[26] = {0}; + resp[3] = 1; + resp[4]=0x55; resp[5]=0x66; resp[6]=0x77; resp[7]=0x88; + resp[11] = 0x84; /* interval 0x84 = 132 */ + resp[15] = 3; /* leechers */ + resp[19] = 7; /* seeders */ + resp[20]=192; resp[21]=168; resp[22]=0; resp[23]=5; resp[24]=0x1a; resp[25]=0xe1; + naut_tracker_response r; + CHECK(naut_udp_parse_announce(resp, 26, 0x55667788, &r) == NAUT_OK); + CHECK_EQ(r.interval, 132); + CHECK_EQ(r.leechers, 3); + CHECK_EQ(r.seeders, 7); + CHECK_EQ(r.num_peers, 1); + CHECK(r.peers[0].ip[0]==192 && r.peers[0].ip[3]==5 && r.peers[0].port==0x1ae1); + naut_tracker_response_free(&r); + + uint8_t malformed[27]; + memcpy(malformed, resp, sizeof resp); + malformed[26] = 0; + CHECK(naut_udp_parse_announce(malformed, sizeof malformed, + 0x55667788, &r) == NAUT_ERR_PROTO); + } + + TEST_MAIN_END(); +} diff --git a/tests/unit/test_worker.c b/tests/unit/test_worker.c new file mode 100644 index 0000000..bd6906a --- /dev/null +++ b/tests/unit/test_worker.c @@ -0,0 +1,61 @@ +#include "naut/hash.h" +#include "naut/worker.h" +#include "test.h" + +#include +#include +#include +#include + +#define JOBS 256 + +typedef struct { + naut_job job; + uint8_t input[4096]; + uint8_t digest[NAUT_SHA256_LEN]; +} hash_job; + +static void run_hash(naut_job *base) { + hash_job *job = base->context; + naut_sha256(job->input, sizeof job->input, job->digest); + base->result = NAUT_OK; +} + +int main(void) { + naut_worker_pool *pool = naut_worker_pool_create(4, 512, -1); + CHECK(pool != NULL); + CHECK_EQ(naut_worker_threads(pool), 4); + CHECK(naut_worker_eventfd(pool) >= 0); + + hash_job jobs[JOBS]; + for (int i = 0; i < JOBS; i++) { + memset(jobs[i].input, i, sizeof jobs[i].input); + jobs[i].job.run = run_hash; + jobs[i].job.context = &jobs[i]; + jobs[i].job.result = NAUT_ERR_AGAIN; + CHECK(naut_worker_submit(pool, &jobs[i].job)); + } + + int complete = 0; + while (complete < JOBS) { + struct pollfd pfd = { + .fd = naut_worker_eventfd(pool), + .events = POLLIN, + }; + CHECK(poll(&pfd, 1, 5000) > 0); + uint64_t count; + (void)read(pfd.fd, &count, sizeof count); + naut_job *base; + while (naut_worker_complete(pool, &base)) { + hash_job *job = base->context; + uint8_t expected[NAUT_SHA256_LEN]; + naut_sha256(job->input, sizeof job->input, expected); + CHECK(base->result == NAUT_OK); + CHECK(memcmp(job->digest, expected, sizeof expected) == 0); + complete++; + } + } + CHECK_EQ(complete, JOBS); + naut_worker_pool_destroy(pool); + TEST_MAIN_END(); +}