diff --git a/README.md b/README.md index ef6c058..eac9881 100644 --- a/README.md +++ b/README.md @@ -150,11 +150,11 @@ These are load-bearing and enforced in the code: ## API at a glance -58 public `bt*` handlers (full signatures in **[api-reference](docs/api-reference.md)**): +69 public `bt*` handlers (full signatures in **[api-reference](docs/api-reference.md)**): | Group | Handlers | |---|---| -| Session | `btStartSession` · `btStopSession` · `btLastError` · `btClearError` | +| Session | `btStartSession` · `btStopSession` · `btLastError` · `btClearError` · `btSessionPause` · `btSessionResume` · `btSessionIsPaused` · `btListenPort` · `btFindTorrent` · `btDhtAnnounce` | | Settings | `btSetInt` · `btSetBool` · `btSetString` · `btGetSetting` · `btSetEncryption` | | Add / remove | `btAddMagnet` · `btAddTorrentFile` · `btAddTorrentWithResume` · `btRemoveTorrent` | | Control | `btPause` · `btResume` · `btForceRecheck` · `btForceReannounce` · `btScrapeTracker` · `btClearTorrentError` | @@ -162,6 +162,7 @@ These are load-bearing and enforced in the code: | Flags / modes | `btSetTorrentFlags` · `btUnsetTorrentFlags` · `btSetSequentialDownload` · `btSetAutoManaged` · `btSetSuperSeeding` · `btSetShareMode` · `btSetUploadMode` | | Queue / storage | `btQueuePosition` · `btQueueUp` · `btQueueDown` · `btQueueTop` · `btQueueBottom` · `btMoveStorage` | | Inspect | `btTorrentStatus` · `btTorrentCount` · `btTorrentHandleAt` · `btInfoHash` · `btPieceBitfield` · `btPeerList` · `btFileList` · `btPieceAvailability` | +| Trackers / seeds | `btTrackers` · `btAddTracker` · `btWebSeeds` · `btAddWebSeed` · `btRemoveWebSeed` | | Events | `btPoll` | | DHT | `btDhtAddBootstrap` · `btDhtState` · `btDhtSaveState` · `btDhtLoadState` | | DHT key-value (BEP44) | `btDhtKeypair` · `btDhtPutImmutable` · `btDhtGetImmutable` · `btDhtPutMutable` · `btDhtGetMutable` | diff --git a/docs/api-reference.md b/docs/api-reference.md index 849332b..ec1fe79 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -119,6 +119,36 @@ MSE / protocol-encryption (PE) policy, mapped straight to libtorrent's --- +## Session operations + +### `btSessionPause(in pSession as Integer) returns Integer` · `btSessionResume(in pSession as Integer) returns Integer` +Pause / resume the **whole session** - every torrent at once. Distinct from the +per-torrent `btPause` / `btResume`. +- **Usage:** command - `btSessionPause sSession`. + +### `btSessionIsPaused(in pSession as Integer) returns Boolean` +`true` if the session is paused. `false` on no session. +- **Usage:** function - `if btSessionIsPaused(sSession) then ...`. + +### `btListenPort(in pSession as Integer) returns Integer` +The TCP port the session actually ended up listening on, or `0` if it is not +listening yet (or on a bad handle). +- **Usage:** function - `put btListenPort(sSession) into tPort`. + +### `btFindTorrent(in pSession as Integer, in pInfoHash as String) returns Integer` +Look up an already-added torrent by its **40-hex (v1) info-hash**; returns the +torrent handle, or `0` if it is not in this session. Lets you recover a handle +from a hash you stored earlier instead of keeping the integer around. +- **Usage:** function - `put btFindTorrent(sSession, tHashHex) into tH`. + +### `btDhtAnnounce(in pSession as Integer, in pInfoHash as String, in pPort as Integer) returns Integer` +Classic **BEP 5** DHT peer announce (not BEP 44): advertise to the DHT that we +serve peers for `pInfoHash` (40-hex) on `pPort` (`0` == our listen port). Useful +for announcing your own content's info-hash so others can find you swarm-side. +- **Usage:** command - `btDhtAnnounce sSession, tHashHex, 0`. + +--- + ## Add / remove torrents ### `btAddMagnet(in pSession as Integer, in pURI as String, in pSavePath as String) returns Integer` @@ -337,6 +367,28 @@ have) for a full piece map. Empty `Data` until the torrent has metadata and peers, or on a bad handle. - **Usage:** function - `put btPieceAvailability(tH) into tAvail`, then `byte i of tAvail`. +### `btTrackers(in pTorrent as Integer) returns List` +The torrent's trackers as a `List` of `Array`s, each with keys `url`, `tier` +(0 == first tier tried), `verified` (`1` once the tracker has answered this +session), and `source` (the `announce_entry` source bitmask). Empty `List` on a +bad handle. +- **Usage:** function - `repeat for each element tTr in btTrackers(tH)` then read `tTr["url"]`. + +### `btAddTracker(in pTorrent as Integer, in pUrl as String, in pTier as Integer) returns Integer` +Add an announce URL at `pTier` (0 == first tier). A URL already in the list is +ignored by libtorrent. +- **Usage:** command - `btAddTracker tH, "udp://tracker.example:6969/announce", 0`. + +### `btWebSeeds(in pTorrent as Integer) returns List` +The torrent's HTTP (URL / web) seeds as a `List` of URL `String`s. Empty `List` +on a bad handle. +- **Usage:** function - `put btWebSeeds(tH) into tSeeds`. + +### `btAddWebSeed(in pTorrent as Integer, in pUrl as String) returns Integer` · `btRemoveWebSeed(in pTorrent as Integer, in pUrl as String) returns Integer` +Add or remove an HTTP (URL / web) seed (BEP 19) — a plain web server that can +serve the torrent's data alongside peers. +- **Usage:** command - `btAddWebSeed tH, "https://mirror.example/path/"`. + --- ## Events / poll @@ -527,6 +579,17 @@ arithmetic. | `progress` | 122 | int | bytes of this file downloaded | | `priority` | 123 | int | this file's download priority, `0..7` | +### Tracker entry (`btTrackers`, one array per tracker) + +| key | field id | type | meaning | +|---|---|---|---| +| `url` | 130 | utf8 | announce URL | +| `tier` | 131 | int | tracker tier (`0` == first) | +| `verified` | 132 | int | `1` once it has answered this session | +| `source` | 133 | int | `announce_entry` source bitmask | + +(`btWebSeeds` returns a plain list of URL strings, not records.) + ### DHT state (`btDhtState`) | key | field id | type | meaning | diff --git a/src/btx_abi.h b/src/btx_abi.h index 8248c84..fdbeb85 100644 --- a/src/btx_abi.h +++ b/src/btx_abi.h @@ -47,7 +47,7 @@ extern "C" { * signature, a new record fieldId or alert code, or a framing change. The LCB * layer hard-codes the matching number in checkABI() and refuses to run on * skew. Start at 1. */ -#define BTX_ABI_VERSION 5 +#define BTX_ABI_VERSION 7 /* ----------------------------------------------------------- export linkage */ @@ -129,6 +129,28 @@ BTX_API int BTX_CALL btx_get_setting(int s, const char *key, char *out, int cap) BTX_API int BTX_CALL btx_set_encryption_policy(int s, int inPolicy, int outPolicy, int level); +/* ====================================================================== * + * Session operations (ABI v7) — whole-session pause, listen port, look up a + * torrent by info-hash, classic (BEP5) DHT peer announce. + * ====================================================================== */ + +/* Pause / resume the WHOLE session (all torrents); is_paused returns 1/0 (0 on + * no session). Distinct from per-torrent btx_pause/btx_resume. */ +BTX_API int BTX_CALL btx_session_pause(int s); +BTX_API int BTX_CALL btx_session_resume(int s); +BTX_API int BTX_CALL btx_session_is_paused(int s); + +/* The TCP port we actually ended up listening on (0 == not listening yet). */ +BTX_API int BTX_CALL btx_listen_port(int s); + +/* Look up an already-added torrent by its 40-hex (v1) info-hash; returns OUR + * torrent handle id, or 0 if not found / bad args. */ +BTX_API int BTX_CALL btx_find_torrent(int s, const char *infoHashHex); + +/* Classic BEP5 DHT peer announce (NOT BEP44): tell the DHT we have peers for + * this 40-hex info-hash on `port` (0 == our listen port). Fire-and-forget. */ +BTX_API int BTX_CALL btx_dht_announce(int s, const char *infoHashHex, int port); + /* ====================================================================== * * Add / remove torrents * ====================================================================== */ @@ -236,6 +258,24 @@ BTX_API int BTX_CALL btx_file_list(int t, void *out, int cap); * availability grid. bytes-written / -needed / 0, like btx_piece_bitfield. */ BTX_API int BTX_CALL btx_piece_availability(int t, void *out, int cap); +/* ---- trackers & web seeds (ABI v6) ---------------------------------------- + * Inspect and edit a torrent's tracker list and HTTP/URL (web) seeds. The two + * listers return count-prefixed KV-record lists (the peer-list framing); the + * editors are fire-and-forget actions. */ + +/* The torrent's trackers as a list of KV records: url, tier, verified, source. */ +BTX_API int BTX_CALL btx_trackers(int t, void *out, int cap); + +/* Add an announce URL at the given tier (0 == first tier; libtorrent dedups). */ +BTX_API int BTX_CALL btx_add_tracker(int t, const char *url, int tier); + +/* Add / remove an HTTP (URL / web) seed (BEP 19). */ +BTX_API int BTX_CALL btx_add_url_seed(int t, const char *url); +BTX_API int BTX_CALL btx_remove_url_seed(int t, const char *url); + +/* The torrent's web seeds as a list of KV records (one F_URL_SEED field each). */ +BTX_API int BTX_CALL btx_url_seeds(int t, void *out, int cap); + /* ====================================================================== * * The alert drain (the event firehose) — one FFI round-trip per poll (§3) * ====================================================================== */ diff --git a/src/btx_record.h b/src/btx_record.h index 24a3e24..e86bb04 100644 --- a/src/btx_record.h +++ b/src/btx_record.h @@ -131,7 +131,14 @@ enum FieldId : uint8_t { F_FILE_PATH = 120, /* utf8: file path within the torrent (relative) */ F_FILE_SIZE = 121, /* int (64-bit): file size in bytes */ F_FILE_PROGRESS = 122, /* int (64-bit): bytes of this file downloaded */ - F_FILE_PRIORITY = 123 /* int 0..7: this file's download priority */ + F_FILE_PRIORITY = 123, /* int 0..7: this file's download priority */ + + /* ---- tracker + web-seed entries (130..139) ---- */ + F_TRACKER_URL = 130, /* utf8: announce URL */ + F_TRACKER_TIER = 131, /* int: tracker tier (0 == first tier) */ + F_TRACKER_VERIFIED = 132, /* int 0/1: has answered at least once this session */ + F_TRACKER_SOURCE = 133, /* int: announce_entry source bitmask */ + F_URL_SEED = 134 /* utf8: a web-seed (URL seed) address */ }; /* ------------------------------------------------------------- alert codes */ diff --git a/src/torrent.lcb b/src/torrent.lcb index 9496049..e5be67f 100644 --- a/src/torrent.lcb +++ b/src/torrent.lcb @@ -42,7 +42,7 @@ metadata title is "TorrentXT" -- ===================================================================== -- -- Must equal BTX_ABI_VERSION in src/btx_abi.h; _checkABI() throws on skew. -constant kABIVersion is 5 +constant kABIVersion is 7 -- libfoundation MCStringEncoding value for UTF-8 (ASCII=0, Windows1252=1, -- MacRoman=2, ISO8859_1=3, UTF8=4). Used by _decodeText via MCStringDecode. @@ -145,6 +145,13 @@ constant kFieldFileSize is 121 constant kFieldFileProgress is 122 constant kFieldFilePriority is 123 +-- tracker + web-seed entry fields (btTrackers / btWebSeeds) +constant kFieldTrackerUrl is 130 +constant kFieldTrackerTier is 131 +constant kFieldTrackerVerified is 132 +constant kFieldTrackerSource is 133 +constant kFieldUrlSeed is 134 + -- Stable alert type codes (mirror btx_record.h AlertType -> kAlert*). constant kAlertTorrentAdded is 1 constant kAlertMetadataReceived is 2 @@ -206,6 +213,14 @@ private foreign handler _btx_set_str(in pS as CInt, in pKey as ZStringUTF8, in p private foreign handler _btx_get_setting(in pS as CInt, in pKey as ZStringUTF8, in pOut as Pointer, in pCap as CInt) returns CInt binds to "c:torrentxt>btx_get_setting!cdecl" private foreign handler _btx_set_encryption_policy(in pS as CInt, in pIn as CInt, in pOut as CInt, in pLevel as CInt) returns CInt binds to "c:torrentxt>btx_set_encryption_policy!cdecl" +-- session operations (ABI v7) +private foreign handler _btx_session_pause(in pS as CInt) returns CInt binds to "c:torrentxt>btx_session_pause!cdecl" +private foreign handler _btx_session_resume(in pS as CInt) returns CInt binds to "c:torrentxt>btx_session_resume!cdecl" +private foreign handler _btx_session_is_paused(in pS as CInt) returns CInt binds to "c:torrentxt>btx_session_is_paused!cdecl" +private foreign handler _btx_listen_port(in pS as CInt) returns CInt binds to "c:torrentxt>btx_listen_port!cdecl" +private foreign handler _btx_find_torrent(in pS as CInt, in pHash as ZStringUTF8) returns CInt binds to "c:torrentxt>btx_find_torrent!cdecl" +private foreign handler _btx_dht_announce(in pS as CInt, in pHash as ZStringUTF8, in pPort as CInt) returns CInt binds to "c:torrentxt>btx_dht_announce!cdecl" + private foreign handler _btx_add_magnet(in pS as CInt, in pURI as ZStringUTF8, in pSave as ZStringUTF8) returns CInt binds to "c:torrentxt>btx_add_magnet!cdecl" private foreign handler _btx_add_torrent_file(in pS as CInt, in pData as Pointer, in pLen as CInt, in pSave as ZStringUTF8) returns CInt binds to "c:torrentxt>btx_add_torrent_file!cdecl" private foreign handler _btx_add_with_resume(in pS as CInt, in pData as Pointer, in pLen as CInt, in pSave as ZStringUTF8) returns CInt binds to "c:torrentxt>btx_add_with_resume!cdecl" @@ -239,6 +254,11 @@ private foreign handler _btx_piece_bitfield(in pT as CInt, in pOut as Pointer, i private foreign handler _btx_peer_list(in pT as CInt, in pOut as Pointer, in pCap as CInt) returns CInt binds to "c:torrentxt>btx_peer_list!cdecl" private foreign handler _btx_file_list(in pT as CInt, in pOut as Pointer, in pCap as CInt) returns CInt binds to "c:torrentxt>btx_file_list!cdecl" private foreign handler _btx_piece_availability(in pT as CInt, in pOut as Pointer, in pCap as CInt) returns CInt binds to "c:torrentxt>btx_piece_availability!cdecl" +private foreign handler _btx_trackers(in pT as CInt, in pOut as Pointer, in pCap as CInt) returns CInt binds to "c:torrentxt>btx_trackers!cdecl" +private foreign handler _btx_add_tracker(in pT as CInt, in pUrl as ZStringUTF8, in pTier as CInt) returns CInt binds to "c:torrentxt>btx_add_tracker!cdecl" +private foreign handler _btx_add_url_seed(in pT as CInt, in pUrl as ZStringUTF8) returns CInt binds to "c:torrentxt>btx_add_url_seed!cdecl" +private foreign handler _btx_remove_url_seed(in pT as CInt, in pUrl as ZStringUTF8) returns CInt binds to "c:torrentxt>btx_remove_url_seed!cdecl" +private foreign handler _btx_url_seeds(in pT as CInt, in pOut as Pointer, in pCap as CInt) returns CInt binds to "c:torrentxt>btx_url_seeds!cdecl" private foreign handler _btx_pop_alerts(in pS as CInt, in pOut as Pointer, in pCap as CInt) returns CInt binds to "c:torrentxt>btx_pop_alerts!cdecl" @@ -543,6 +563,16 @@ private handler _fieldKey(in pId as Integer) returns String return "progress" else if pId is kFieldFilePriority then return "priority" + else if pId is kFieldTrackerUrl then + return "url" + else if pId is kFieldTrackerTier then + return "tier" + else if pId is kFieldTrackerVerified then + return "verified" + else if pId is kFieldTrackerSource then + return "source" + else if pId is kFieldUrlSeed then + return "urlSeed" else return "" end if @@ -757,6 +787,63 @@ public handler btSetEncryption(in pSession as Integer, in pIn as Integer, in pOu return tR end handler +-- ---- session operations (ABI v7) ---------------------------------------- + +-- Pause / resume the WHOLE session (every torrent). Distinct from the +-- per-torrent btPause / btResume. +public handler btSessionPause(in pSession as Integer) returns Integer + variable tR as Integer + unsafe + put _btx_session_pause(pSession) into tR + end unsafe + return tR +end handler + +public handler btSessionResume(in pSession as Integer) returns Integer + variable tR as Integer + unsafe + put _btx_session_resume(pSession) into tR + end unsafe + return tR +end handler + +public handler btSessionIsPaused(in pSession as Integer) returns Boolean + variable tR as Integer + unsafe + put _btx_session_is_paused(pSession) into tR + end unsafe + return tR is not 0 +end handler + +-- The TCP port we actually ended up listening on (0 == not listening yet). +public handler btListenPort(in pSession as Integer) returns Integer + variable tR as Integer + unsafe + put _btx_listen_port(pSession) into tR + end unsafe + return tR +end handler + +-- Look up an already-added torrent by its 40-hex (v1) info-hash; returns the +-- torrent handle, or 0 if not found. +public handler btFindTorrent(in pSession as Integer, in pInfoHash as String) returns Integer + variable tR as Integer + unsafe + put _btx_find_torrent(pSession, pInfoHash) into tR + end unsafe + return tR +end handler + +-- Classic BEP5 DHT peer announce (NOT BEP44): tell the DHT we have peers for +-- this 40-hex info-hash on pPort (0 == our listen port). +public handler btDhtAnnounce(in pSession as Integer, in pInfoHash as String, in pPort as Integer) returns Integer + variable tR as Integer + unsafe + put _btx_dht_announce(pSession, pInfoHash, pPort) into tR + end unsafe + return tR +end handler + -- ===================================================================== -- -- Public API: add / remove torrents -- ===================================================================== -- @@ -1216,6 +1303,107 @@ public handler btPieceAvailability(in pTorrent as Integer) returns Data return _bufToData(sStatusPtr, tCount) end handler +-- The torrent's trackers as a List of Arrays, each with keys "url", "tier", +-- "verified" (0/1), "source". One FFI round-trip for the whole announce list. +public handler btTrackers(in pTorrent as Integer) returns List + variable tTrackers as List + variable tCount as Integer + variable tOffset as Integer + variable tTrackerCount as Integer + variable tBodyLen as Integer + variable tTracker as Array + variable tI as Integer + variable tBuf as Data + put [] into tTrackers + _ensureStatus(kStatusCap) + unsafe + put _btx_trackers(pTorrent, sStatusPtr, sStatusCap) into tCount + end unsafe + if tCount < 0 then + _ensureStatus(-tCount) + unsafe + put _btx_trackers(pTorrent, sStatusPtr, sStatusCap) into tCount + end unsafe + end if + if tCount <= 0 then + return tTrackers + end if + put _bufToData(sStatusPtr, tCount) into tBuf + put 1 into tOffset + put _readU16(tBuf, tOffset) into tTrackerCount + add 2 to tOffset + repeat with tI from 1 up to tTrackerCount + put _readU16(tBuf, tOffset) into tBodyLen + put _parseRecord(tBuf, tOffset + 2) into tTracker + push tTracker onto tTrackers + add (2 + tBodyLen) to tOffset + end repeat + return tTrackers +end handler + +-- Add an announce URL at the given tier (0 == first tier). Duplicates ignored. +public handler btAddTracker(in pTorrent as Integer, in pUrl as String, in pTier as Integer) returns Integer + variable tR as Integer + unsafe + put _btx_add_tracker(pTorrent, pUrl, pTier) into tR + end unsafe + return tR +end handler + +-- Add / remove an HTTP (URL / web) seed (BEP 19). +public handler btAddWebSeed(in pTorrent as Integer, in pUrl as String) returns Integer + variable tR as Integer + unsafe + put _btx_add_url_seed(pTorrent, pUrl) into tR + end unsafe + return tR +end handler + +public handler btRemoveWebSeed(in pTorrent as Integer, in pUrl as String) returns Integer + variable tR as Integer + unsafe + put _btx_remove_url_seed(pTorrent, pUrl) into tR + end unsafe + return tR +end handler + +-- The torrent's web seeds as a List of URL Strings. +public handler btWebSeeds(in pTorrent as Integer) returns List + variable tSeeds as List + variable tCount as Integer + variable tOffset as Integer + variable tSeedCount as Integer + variable tBodyLen as Integer + variable tRec as Array + variable tI as Integer + variable tBuf as Data + put [] into tSeeds + _ensureStatus(kStatusCap) + unsafe + put _btx_url_seeds(pTorrent, sStatusPtr, sStatusCap) into tCount + end unsafe + if tCount < 0 then + _ensureStatus(-tCount) + unsafe + put _btx_url_seeds(pTorrent, sStatusPtr, sStatusCap) into tCount + end unsafe + end if + if tCount <= 0 then + return tSeeds + end if + put _bufToData(sStatusPtr, tCount) into tBuf + put 1 into tOffset + put _readU16(tBuf, tOffset) into tSeedCount + add 2 to tOffset + repeat with tI from 1 up to tSeedCount + put _readU16(tBuf, tOffset) into tBodyLen + put _parseRecord(tBuf, tOffset + 2) into tRec + push tRec["urlSeed"] onto tSeeds + add (2 + tBodyLen) to tOffset + end repeat + return tSeeds +end handler + -- ===================================================================== -- -- Public API: the alert drain (the event firehose) -- ===================================================================== -- diff --git a/src/torrent_shim.cpp b/src/torrent_shim.cpp index 0250539..7488a99 100644 --- a/src/torrent_shim.cpp +++ b/src/torrent_shim.cpp @@ -82,6 +82,7 @@ /* ---- standard library ------------------------------------------------------ */ #include #include +#include #include #include #include @@ -624,6 +625,76 @@ extern "C" BTX_API int BTX_CALL btx_set_encryption_policy(int s, int inPolicy, }); } +/* ====================================================================== * + * Session operations (ABI v7) — whole-session pause, listen port, look up a + * torrent by info-hash, classic (BEP5) DHT peer announce. + * ====================================================================== */ + +extern "C" BTX_API int BTX_CALL btx_session_pause(int s) { + BTX_GUARD_ACTION({ + SessionState *st = session_for(s); + if (!st || !st->ses) { set_error("no live session"); return BTX_ERR_NO_SESSION; } + st->ses->pause(); + return BTX_OK; + }); +} + +extern "C" BTX_API int BTX_CALL btx_session_resume(int s) { + BTX_GUARD_ACTION({ + SessionState *st = session_for(s); + if (!st || !st->ses) { set_error("no live session"); return BTX_ERR_NO_SESSION; } + st->ses->resume(); + return BTX_OK; + }); +} + +extern "C" BTX_API int BTX_CALL btx_session_is_paused(int s) { + BTX_GUARD_INT({ + SessionState *st = session_for(s); + if (!st || !st->ses) return 0; /* no session -> "not paused" default */ + return st->ses->is_paused() ? 1 : 0; + }); +} + +extern "C" BTX_API int BTX_CALL btx_listen_port(int s) { + BTX_GUARD_INT({ + SessionState *st = session_for(s); + if (!st || !st->ses) return 0; /* 0 == not listening / no session */ + return static_cast(st->ses->listen_port()); + }); +} + +extern "C" BTX_API int BTX_CALL btx_find_torrent(int s, const char *infoHashHex) { + BTX_GUARD_INT({ + SessionState *st = session_for(s); + if (!st || !st->ses) return 0; + char buf[20]; + if (!hex_to_buf(infoHashHex, buf, 20)) return 0; /* not 40 hex -> not found */ + lt::torrent_handle h = st->ses->find_torrent(lt::sha1_hash(buf)); + if (!h.is_valid()) return 0; + /* map libtorrent's handle back to OUR int id via the reverse map. */ + auto it = st->idOf.find(h); + return it == st->idOf.end() ? 0 : it->second; + }); +} + +extern "C" BTX_API int BTX_CALL btx_dht_announce(int s, const char *infoHashHex, + int port) { + BTX_GUARD_ACTION({ + SessionState *st = session_for(s); + if (!st || !st->ses) { set_error("no live session"); return BTX_ERR_NO_SESSION; } + char buf[20]; + if (!hex_to_buf(infoHashHex, buf, 20)) { + set_error("info-hash must be 40 hex chars"); return BTX_ERR_INVALID_ARG; + } + if (port < 0 || port > 65535) { set_error("bad port"); return BTX_ERR_INVALID_ARG; } + /* classic BEP5 peer announce (distinct from the BEP44 KV calls): tell the + * DHT we serve peers for this info-hash on `port` (0 == our listen port). */ + st->ses->dht_announce(lt::sha1_hash(buf), port); + return BTX_OK; + }); +} + /* ====================================================================== * * Add / remove torrents * ====================================================================== */ @@ -1257,6 +1328,109 @@ extern "C" BTX_API int BTX_CALL btx_piece_availability(int t, void *out, int cap }); } +/* ====================================================================== * + * Trackers & web seeds (ABI v6) — inspect and edit the announce list and + * the HTTP/URL seed list. Listers mirror the peer-list framing; editors are + * fire-and-forget. (The downloaded data still rides engine ⇄ disk.) + * ====================================================================== */ + +extern "C" BTX_API int BTX_CALL btx_trackers(int t, void *out, int cap) { + BTX_GUARD_BUFFER({ + bool ok = false; lt::torrent_handle h = torrent_only(t, nullptr, &ok); + if (!ok) return 0; /* stale -> empty */ + + std::vector trs = h.trackers(); + btx::RecordWriter w(out, cap); + const size_t countAt = w.pos(); + w.put_u16(0); /* trackerCount placeholder */ + uint16_t emitted = 0; + for (const lt::announce_entry &ae : trs) { + const size_t bodyAt = w.pos(); + w.put_u16(0); /* bodyLen placeholder */ + const size_t bodyStart = w.pos(); + { + btx::KVRecord r(w); + r.put_str(btx::F_TRACKER_URL, ae.url); + r.put_int(btx::F_TRACKER_TIER, static_cast(ae.tier)); + r.put_bool(btx::F_TRACKER_VERIFIED, ae.verified); + r.put_int(btx::F_TRACKER_SOURCE, static_cast(ae.source)); + r.finish(); + } + w.patch_u16(bodyAt, static_cast(w.pos() - bodyStart)); + ++emitted; + } + w.patch_u16(countAt, emitted); + if (w.overflow()) return -static_cast(w.pos()); + return static_cast(w.pos()); + }); +} + +extern "C" BTX_API int BTX_CALL btx_add_tracker(int t, const char *url, int tier) { + BTX_GUARD_ACTION({ + bool ok = false; lt::torrent_handle h = torrent_only(t, nullptr, &ok); + if (!ok) { set_error("bad torrent handle"); return BTX_ERR_BAD_HANDLE; } + if (!url || !*url) { set_error("empty tracker url"); return BTX_ERR_INVALID_ARG; } + lt::announce_entry ae; + ae.url = url; + if (tier < 0) tier = 0; + if (tier > 255) tier = 255; + ae.tier = static_cast(tier); + /* libtorrent ignores a duplicate URL already in the list. */ + h.add_tracker(ae); + return BTX_OK; + }); +} + +extern "C" BTX_API int BTX_CALL btx_add_url_seed(int t, const char *url) { + BTX_GUARD_ACTION({ + bool ok = false; lt::torrent_handle h = torrent_only(t, nullptr, &ok); + if (!ok) { set_error("bad torrent handle"); return BTX_ERR_BAD_HANDLE; } + if (!url || !*url) { set_error("empty url seed"); return BTX_ERR_INVALID_ARG; } + h.add_url_seed(std::string(url)); + return BTX_OK; + }); +} + +extern "C" BTX_API int BTX_CALL btx_remove_url_seed(int t, const char *url) { + BTX_GUARD_ACTION({ + bool ok = false; lt::torrent_handle h = torrent_only(t, nullptr, &ok); + if (!ok) { set_error("bad torrent handle"); return BTX_ERR_BAD_HANDLE; } + if (!url || !*url) { set_error("empty url seed"); return BTX_ERR_INVALID_ARG; } + h.remove_url_seed(std::string(url)); + return BTX_OK; + }); +} + +extern "C" BTX_API int BTX_CALL btx_url_seeds(int t, void *out, int cap) { + BTX_GUARD_BUFFER({ + bool ok = false; lt::torrent_handle h = torrent_only(t, nullptr, &ok); + if (!ok) return 0; /* stale -> empty */ + + std::set seeds = h.url_seeds(); + /* one single-field KV record per seed, in the peer-list framing so the + * LCB walker reuses the same counted-loop parse. */ + btx::RecordWriter w(out, cap); + const size_t countAt = w.pos(); + w.put_u16(0); /* seedCount placeholder */ + uint16_t emitted = 0; + for (const std::string &u : seeds) { + const size_t bodyAt = w.pos(); + w.put_u16(0); /* bodyLen placeholder */ + const size_t bodyStart = w.pos(); + { + btx::KVRecord r(w); + r.put_str(btx::F_URL_SEED, u); + r.finish(); + } + w.patch_u16(bodyAt, static_cast(w.pos() - bodyStart)); + ++emitted; + } + w.patch_u16(countAt, emitted); + if (w.overflow()) return -static_cast(w.pos()); + return static_cast(w.pos()); + }); +} + /* ====================================================================== * * The alert drain (the event firehose) — one FFI round-trip per poll (§3) * ====================================================================== */ diff --git a/tests/torrent_smoke_test.cpp b/tests/torrent_smoke_test.cpp index 3b36146..9bafead 100644 --- a/tests/torrent_smoke_test.cpp +++ b/tests/torrent_smoke_test.cpp @@ -128,6 +128,16 @@ static void test_bogus_handles_are_noops() { CHECK(btx_set_encryption_policy(h, 1, 1, 3) < 0); CHECK(btx_dht_add_bootstrap(h, "router.bittorrent.com", 6881) < 0); + /* --- ABI v7 session ops on a bogus SESSION handle --- */ + CHECK(btx_session_pause(h) < 0); + CHECK(btx_session_resume(h) < 0); + CHECK(btx_session_is_paused(h) == 0); /* int-getter: 0 on no session */ + CHECK(btx_listen_port(h) == 0); + CHECK(btx_find_torrent(h, + "0123456789abcdef0123456789abcdef01234567") == 0); + CHECK(btx_dht_announce(h, + "0123456789abcdef0123456789abcdef01234567", 6881) < 0); + /* add-* on a bogus session return 0 (no handle made), not a crash. */ CHECK(btx_add_magnet(h, "magnet:?xt=urn:btih:" "0123456789abcdef0123456789abcdef01234567", "/tmp") == 0); @@ -187,6 +197,11 @@ static void test_bogus_handles_are_noops() { CHECK(btx_peer_list(h, buf, sizeof buf) == 0); CHECK(btx_file_list(h, buf, sizeof buf) == 0); /* ABI v5 */ CHECK(btx_piece_availability(h, buf, sizeof buf) == 0); /* ABI v5 */ + CHECK(btx_trackers(h, buf, sizeof buf) == 0); /* ABI v6 */ + CHECK(btx_url_seeds(h, buf, sizeof buf) == 0); /* ABI v6 */ + CHECK(btx_add_tracker(h, "udp://x.example:6969", 0) < 0); + CHECK(btx_add_url_seed(h, "http://x.example/seed") < 0); + CHECK(btx_remove_url_seed(h, "http://x.example/seed") < 0); } /* remove() needs a live session to even reach the torrent check; with one diff --git a/tools/check-record-registry.py b/tools/check-record-registry.py index 8e03917..79aec55 100644 --- a/tools/check-record-registry.py +++ b/tools/check-record-registry.py @@ -55,6 +55,17 @@ def parse_lcb_constants(text): return out +def parse_abi_version(path): + """Return the int N from `#define BTX_ABI_VERSION N`, or None if unreadable.""" + try: + with open(path, "r", encoding="utf-8") as f: + text = f.read() + except OSError: + return None + m = re.search(r"#define\s+BTX_ABI_VERSION\s+(\d+)", text) + return int(m.group(1)) if m else None + + def main(argv): header = argv[1] if len(argv) > 1 else os.path.join(HERE, "src", "btx_record.h") lcb = argv[2] if len(argv) > 2 else os.path.join(HERE, "src", "torrent.lcb") @@ -73,6 +84,21 @@ def main(argv): problems = [] checked = 0 + + # The ABI version must match between btx_abi.h (#define BTX_ABI_VERSION) and + # torrent.lcb (constant kABIVersion) - a skew makes _checkABI() throw at + # runtime, and a forgotten bump is an easy mistake to make. Catch it here. + abi_h = parse_abi_version(os.path.join(HERE, "src", "btx_abi.h")) + abi_lcb = lcb_consts.get("kABIVersion") + if abi_h is None: + problems.append("could not read BTX_ABI_VERSION from src/btx_abi.h") + elif abi_lcb is None: + problems.append("missing `constant kABIVersion` in torrent.lcb") + elif abi_h != abi_lcb: + problems.append("ABI version skew: btx_abi.h BTX_ABI_VERSION=%d but " + "torrent.lcb kABIVersion=%d" % (abi_h, abi_lcb)) + else: + checked += 1 for cpp_prefix, lcb_prefix in REGISTRIES: header_enum = parse_header_enum(htext, cpp_prefix) if not header_enum: