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