nautd/webui: scripting, labels, settings, set-location, pause fix

Session checkpoint on webui-plugin:
- engine dump (nautctl dump) + engine endgame integration
- per-file move locations persistence; torrent-level "Set location"
  with reset/keep-relative/leave-separate handling + residual prune
- Lua: naut.get_labels, define_settings/get_setting (script_host struct)
- daemon-owned labels (category+tags) + taxonomy persistence; webui write-through
- fix: pausing a completed/seeding torrent now sticks (stop wins over result)
- automation tab responsive layout; anime_sort label gating + settings

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
ookami125 2026-06-21 23:19:41 -04:00
parent 6dc711cf57
commit b633b7d216
40 changed files with 3305 additions and 3267 deletions

View file

@ -3,5 +3,8 @@ function on_torrent_finished(event)
end
function on_file_complete(event)
naut.move_file(event.torrent_id, event.index, event.path .. ".moved")
-- Labels surface to Lua as a plain array of strings; use the first to route.
local labels = naut.get_labels(event.torrent_id)
local suffix = labels[1] or "moved"
naut.move_file(event.torrent_id, event.index, event.path .. "." .. suffix)
end

View file

@ -54,7 +54,7 @@ for _ in $(seq 1 100); do
done
[[ -n "$port" && "$port" != 0 ]]
"$daemon" --socket "$socket" --plugin "$plugin" \
"$daemon" --socket "$socket" --plugin "$plugin" --state-dir "$tmp/state" \
>"$daemon_log" 2>&1 &
daemon_pid=$!

View file

@ -1,98 +0,0 @@
#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();
}

View file

@ -69,6 +69,20 @@ int main(void) {
CHECK(got && glen == olen && memcmp(got, orig, olen) == 0);
free(got);
/* Existing verified data should be reflected in progress before any peer
* requests are made. */
st = naut_storage_open(mi.files, mi.num_files, root, &err);
CHECK(st && err == NAUT_OK);
d = naut_download_create(&mi, st);
CHECK(d != NULL);
CHECK(naut_download_resume(d) == NAUT_OK);
CHECK(naut_download_complete(d));
CHECK_EQ(naut_download_pieces_done(d), naut_download_num_pieces(d));
CHECK_EQ((long long)naut_download_bytes_done(d), (long long)olen);
CHECK(!naut_download_next_request(d, &idx, &begin, &len));
naut_download_destroy(d);
naut_storage_close(st);
/* The same path with SHA-1 verification offloaded to bounded workers. */
{
char t2[] = "/tmp/naut_async_XXXXXX";

View file

@ -108,6 +108,22 @@ int main(void) {
memcmp(got1, global + mi.files[0].length, l1) == 0);
free(got1);
/* Simulate a daemon restart: file 0 now lives at `dest`, not under `root`.
* Reopening with its saved location as an override must pick it up in place
* so resume verifies every piece -- if the override were ignored, file 0
* would open as an empty placeholder under root and resume would fail. */
const char *overrides[2] = { dest, NULL };
naut_storage_opts ropts = { .preallocate = true, .overrides = overrides };
naut_storage *st2 =
naut_storage_open_opts(mi.files, mi.num_files, root, &ropts, &err);
CHECK(st2 && err == NAUT_OK);
naut_download *d2 = naut_download_create(&mi, st2);
CHECK(d2 != NULL);
CHECK(naut_download_resume(d2) == NAUT_OK);
CHECK(naut_download_complete(d2)); /* both files verified from their homes */
naut_download_destroy(d2);
naut_storage_close(st2);
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);

View file

@ -1,56 +0,0 @@
/* 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();
}

View file

@ -1,30 +0,0 @@
#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();
}

View file

@ -3,6 +3,7 @@
#include <pthread.h>
#include <stdatomic.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
@ -30,13 +31,29 @@ static naut_err capture_move(void *opaque, uint64_t torrent_id,
return NAUT_OK;
}
/* Hand the script a single label "anime" so the fixture can route on it. */
static char **capture_labels(void *opaque, uint64_t torrent_id, size_t *count) {
(void)opaque;
(void)torrent_id;
char **labels = malloc(sizeof *labels);
if (!labels) { *count = 0; return NULL; }
labels[0] = strdup("anime");
*count = labels[0] ? 1 : 0;
return labels;
}
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_host host = {
.move_file = capture_move,
.labels = capture_labels,
.context = &capture,
};
naut_script *script = naut_script_create(
events, NAUT_PHASE7_SCRIPT, 8, capture_move, &capture, &error);
events, NAUT_PHASE7_SCRIPT, 8, &host, &error);
CHECK(script && error == NAUT_OK);
naut_event event = {
@ -65,7 +82,8 @@ int main(void) {
CHECK(!pthread_equal(owner, capture.caller));
CHECK_EQ(capture.torrent_id, 42);
CHECK_EQ(capture.file_index, 3);
CHECK(strcmp(capture.destination, "/tmp/completed-file.moved") == 0);
/* Proves naut.get_labels surfaced the label string into Lua. */
CHECK(strcmp(capture.destination, "/tmp/completed-file.anime") == 0);
naut_script_stats stats;
naut_script_get_stats(script, &stats);

View file

@ -1,115 +0,0 @@
#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();
}