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 <noreply@anthropic.com>
This commit is contained in:
ookami125 2026-06-15 12:12:00 -04:00
commit 2178d6a70c
121 changed files with 12644 additions and 0 deletions

50
tests/bench/bench_hash.c Normal file
View file

@ -0,0 +1,50 @@
/* Hash throughput bench — the Phase 2 gate (SHA-256 >= 1.5 GB/s/core). */
#include "naut/hash.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
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;
}

114
tests/bench/bench_scale.c Normal file
View file

@ -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 <poll.h>
#include <sched.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
#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;
}

1
tests/fixtures/data/multi/a.txt vendored Normal file

File diff suppressed because one or more lines are too long

BIN
tests/fixtures/data/multi/sub/b.dat vendored Normal file

Binary file not shown.

BIN
tests/fixtures/data/single.bin vendored Normal file

Binary file not shown.

40
tests/fixtures/generate.py vendored Normal file
View file

@ -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)

BIN
tests/fixtures/hybrid.torrent vendored Normal file

Binary file not shown.

1
tests/fixtures/multi_v1.torrent vendored Normal file
View file

@ -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:¸2¾HGLµóžU÷JéJ(~5Ṹ2¾HGLµóžU÷JéJ(~5Ṹ2¾HGLµóžU÷JéJ(~5Ṹ2¾HGLµóžU÷JéJ(~5Ṹ2¾HGLµóžU÷JéJ(~5á¹_ålqy€ß5ØÜуþDæø+¯~^ÖPøLRD$¼•¨ŸÚ„Q«ûç-üº¢¸™3<E284A2>BøcŒ¯½í-R~,{ŽäúÊÙl1ÊQL!XñN*ee

7
tests/fixtures/phase7.lua vendored Normal file
View file

@ -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

1
tests/fixtures/single_v1.torrent vendored Normal file
View file

@ -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:í °±Ê%‰Ìß!Ÿ£°8·HÓåí °±Ê%‰Ìß!Ÿ£°8·HÓåí °±Ê%‰Ìß!Ÿ£°8·HÓåí °±Ê%‰Ìß!Ÿ£°8·HÓåí °±Ê%‰Ìß!Ÿ£°8·HÓåí °±Ê%‰Ìß!Ÿ£°8·HÓåí °±Ê%‰Ìß!Ÿ£°8·HÓåí °±Ê%‰Ìß!Ÿ£°8·HÓåí °±Ê%‰Ìß!Ÿ£°8·HÓåí °±Ê%‰Ìß!Ÿ£°8·HÓåí °±Ê%‰Ìß!Ÿ£°8·HÓåí °±Ê%‰Ìß!Ÿ£°8·HÓå<>L<EFBFBD>ï/éø®ÀÅÛ(vùlÝoGee

BIN
tests/fixtures/v2.torrent vendored Normal file

Binary file not shown.

11
tests/fuzz/fuzz_bencode.c Normal file
View file

@ -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;
}

71
tests/fuzz/fuzz_lite.c Normal file
View file

@ -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 <bencode|metainfo> <iterations> [seedfile ...]
*/
#include "naut/bencode.h"
#include "naut/metainfo.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
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 <bencode|metainfo> <iters> [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;
}

View file

@ -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;
}

View file

@ -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)

View file

@ -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)

View file

@ -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()

View file

@ -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

View file

@ -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

View file

@ -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"

View file

@ -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"

View file

@ -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=

View file

@ -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"

View file

@ -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"

View file

@ -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 <file.torrent> <save_path>
Runs until killed. Prints 'PORT <n>' 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)

View file

@ -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)

34
tests/unit/test.h Normal file
View file

@ -0,0 +1,34 @@
/* Minimal test harness: no dependencies, exit code = failure count. */
#ifndef NAUT_TEST_H
#define NAUT_TEST_H
#include <stdio.h>
#include <stdlib.h>
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

100
tests/unit/test_bencode.c Normal file
View file

@ -0,0 +1,100 @@
#include "naut/bencode.h"
#include "test.h"
#include <string.h>
#include <stdlib.h>
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();
}

View file

@ -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();
}

80
tests/unit/test_buf.c Normal file
View file

@ -0,0 +1,80 @@
#include "naut/buf.h"
#include "test.h"
#include <pthread.h>
#include <sched.h>
#include <string.h>
/* 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();
}

94
tests/unit/test_crypto.c Normal file
View file

@ -0,0 +1,94 @@
#include "naut/hash.h"
#include "naut/rc4.h"
#include "naut/merkle.h"
#include "test.h"
#include <string.h>
#include <stdlib.h>
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();
}

98
tests/unit/test_dht.c Normal file
View file

@ -0,0 +1,98 @@
#include "naut/bencode.h"
#include "naut/dht.h"
#include "test.h"
#include <stdlib.h>
#include <string.h>
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();
}

131
tests/unit/test_download.c Normal file
View file

@ -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 <poll.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#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();
}

View file

@ -0,0 +1,87 @@
#include "naut/extension.h"
#include "naut/peer.h"
#include "test.h"
#include <stdlib.h>
#include <string.h>
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();
}

116
tests/unit/test_filemove.c Normal file
View file

@ -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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#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();
}

143
tests/unit/test_metainfo.c Normal file
View file

@ -0,0 +1,143 @@
#include "naut/metainfo.h"
#include "naut/bencode.h"
#include "test.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#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();
}

63
tests/unit/test_mpmc.c Normal file
View file

@ -0,0 +1,63 @@
#include "naut/mpmc.h"
#include "test.h"
#include <pthread.h>
/* 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();
}

56
tests/unit/test_mse.c Normal file
View file

@ -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 <string.h>
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();
}

80
tests/unit/test_peer.c Normal file
View file

@ -0,0 +1,80 @@
#include "naut/peer.h"
#include "test.h"
#include <string.h>
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();
}

166
tests/unit/test_picker.c Normal file
View file

@ -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 <stdlib.h>
#include <string.h>
#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();
}

View file

@ -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();
}

53
tests/unit/test_plugin.c Normal file
View file

@ -0,0 +1,53 @@
#include "naut/event.h"
#include "naut/plugin.h"
#include "naut/rpc.h"
#include "test.h"
#include <string.h>
#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();
}

66
tests/unit/test_rpc.c Normal file
View file

@ -0,0 +1,66 @@
#include "naut/event.h"
#include "naut/rpc.h"
#include "test.h"
#include <string.h>
#include <sys/socket.h>
#include <unistd.h>
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();
}

78
tests/unit/test_script.c Normal file
View file

@ -0,0 +1,78 @@
#include "naut/script.h"
#include "test.h"
#include <pthread.h>
#include <stdatomic.h>
#include <string.h>
#include <unistd.h>
#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();
}

79
tests/unit/test_storage.c Normal file
View file

@ -0,0 +1,79 @@
#include "naut/storage.h"
#include "test.h"
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
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();
}

115
tests/unit/test_tracker.c Normal file
View file

@ -0,0 +1,115 @@
#include "naut/tracker.h"
#include "naut/bencode.h"
#include "test.h"
#include <string.h>
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:<two 6-byte peers>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();
}

61
tests/unit/test_worker.c Normal file
View file

@ -0,0 +1,61 @@
#include "naut/hash.h"
#include "naut/worker.h"
#include "test.h"
#include <poll.h>
#include <stdint.h>
#include <string.h>
#include <unistd.h>
#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();
}