Naut/tests/unit/test_metainfo.c
ookami125 8dde48c05a webui: replace nautctl web server with a loadable plugin
Drop the web UI that was compiled into nautctl and serve the
torrent-ui front end (../torrent-ui/public) from a native plugin
(plugins/webui) loaded via `nautd --plugin`. The plugin talks to the
engine only through the host call_rpc ABI and adapts the daemon's RPC
surface to the qBittorrent-style contract the UI expects (snapshot/SSE,
torrent detail tabs, add/delete, cookie auth).

Also folds in the daemon refactor that owns per-torrent worker threads
and the swarm engine (naut_swarm) used by the plugin's data source.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 00:21:48 -04:00

147 lines
5.5 KiB
C

#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);
CHECK(mi.tracker_tiers != NULL);
CHECK_EQ(mi.tracker_tiers[0], 0);
CHECK_EQ(mi.tracker_tiers[1], 1);
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);
CHECK(mi.tracker_tiers && mi.tracker_tiers[0] == 0);
naut_metainfo_free(&mi);
naut_bc_free(doc);
free(torrent);
}
TEST_MAIN_END();
}