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:
commit
2178d6a70c
121 changed files with 12644 additions and 0 deletions
50
tests/bench/bench_hash.c
Normal file
50
tests/bench/bench_hash.c
Normal 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
114
tests/bench/bench_scale.c
Normal 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
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
BIN
tests/fixtures/data/multi/sub/b.dat
vendored
Normal file
Binary file not shown.
BIN
tests/fixtures/data/single.bin
vendored
Normal file
BIN
tests/fixtures/data/single.bin
vendored
Normal file
Binary file not shown.
40
tests/fixtures/generate.py
vendored
Normal file
40
tests/fixtures/generate.py
vendored
Normal 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
BIN
tests/fixtures/hybrid.torrent
vendored
Normal file
Binary file not shown.
1
tests/fixtures/multi_v1.torrent
vendored
Normal file
1
tests/fixtures/multi_v1.torrent
vendored
Normal 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
7
tests/fixtures/phase7.lua
vendored
Normal 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
1
tests/fixtures/single_v1.torrent
vendored
Normal 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
BIN
tests/fixtures/v2.torrent
vendored
Normal file
Binary file not shown.
11
tests/fuzz/fuzz_bencode.c
Normal file
11
tests/fuzz/fuzz_bencode.c
Normal 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
71
tests/fuzz/fuzz_lite.c
Normal 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;
|
||||
}
|
||||
7
tests/fuzz/fuzz_metainfo.c
Normal file
7
tests/fuzz/fuzz_metainfo.c
Normal 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;
|
||||
}
|
||||
38
tests/integration/dht_fixture.py
Normal file
38
tests/integration/dht_fixture.py
Normal 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)
|
||||
69
tests/integration/echo_client.py
Normal file
69
tests/integration/echo_client.py
Normal 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)
|
||||
31
tests/integration/http_tracker.py
Normal file
31
tests/integration/http_tracker.py
Normal 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()
|
||||
59
tests/integration/run_echo_scale.sh
Normal file
59
tests/integration/run_echo_scale.sh
Normal 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
|
||||
41
tests/integration/run_interop.sh
Normal file
41
tests/integration/run_interop.sh
Normal 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
|
||||
65
tests/integration/run_magnet_dht.sh
Normal file
65
tests/integration/run_magnet_dht.sh
Normal 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"
|
||||
49
tests/integration/run_mse.sh
Normal file
49
tests/integration/run_mse.sh
Normal 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"
|
||||
98
tests/integration/run_phase7.sh
Normal file
98
tests/integration/run_phase7.sh
Normal 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=
|
||||
69
tests/integration/run_swarm.sh
Normal file
69
tests/integration/run_swarm.sh
Normal 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"
|
||||
129
tests/integration/run_tracker_swarm.sh
Normal file
129
tests/integration/run_tracker_swarm.sh
Normal 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"
|
||||
37
tests/integration/seeder.py
Normal file
37
tests/integration/seeder.py
Normal 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)
|
||||
27
tests/integration/udp_tracker.py
Normal file
27
tests/integration/udp_tracker.py
Normal 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
34
tests/unit/test.h
Normal 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
100
tests/unit/test_bencode.c
Normal 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();
|
||||
}
|
||||
60
tests/unit/test_bitfield.c
Normal file
60
tests/unit/test_bitfield.c
Normal 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
80
tests/unit/test_buf.c
Normal 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
94
tests/unit/test_crypto.c
Normal 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
98
tests/unit/test_dht.c
Normal 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
131
tests/unit/test_download.c
Normal 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();
|
||||
}
|
||||
87
tests/unit/test_extension.c
Normal file
87
tests/unit/test_extension.c
Normal 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
116
tests/unit/test_filemove.c
Normal 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
143
tests/unit/test_metainfo.c
Normal 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
63
tests/unit/test_mpmc.c
Normal 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
56
tests/unit/test_mse.c
Normal 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
80
tests/unit/test_peer.c
Normal 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
166
tests/unit/test_picker.c
Normal 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();
|
||||
}
|
||||
30
tests/unit/test_pipeline.c
Normal file
30
tests/unit/test_pipeline.c
Normal 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
53
tests/unit/test_plugin.c
Normal 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
66
tests/unit/test_rpc.c
Normal 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
78
tests/unit/test_script.c
Normal 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
79
tests/unit/test_storage.c
Normal 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
115
tests/unit/test_tracker.c
Normal 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
61
tests/unit/test_worker.c
Normal 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();
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue