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>
147 lines
5.5 KiB
C
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();
|
|
}
|