diff --git a/README.md b/README.md index eac9881..b8475a0 100644 --- a/README.md +++ b/README.md @@ -150,13 +150,14 @@ These are load-bearing and enforced in the code: ## API at a glance -69 public `bt*` handlers (full signatures in **[api-reference](docs/api-reference.md)**): +75 public `bt*` handlers (full signatures in **[api-reference](docs/api-reference.md)**): | Group | Handlers | |---|---| | Session | `btStartSession` · `btStopSession` · `btLastError` · `btClearError` · `btSessionPause` · `btSessionResume` · `btSessionIsPaused` · `btListenPort` · `btFindTorrent` · `btDhtAnnounce` | | Settings | `btSetInt` · `btSetBool` · `btSetString` · `btGetSetting` · `btSetEncryption` | -| Add / remove | `btAddMagnet` · `btAddTorrentFile` · `btAddTorrentWithResume` · `btRemoveTorrent` | +| Add / remove | `btAddMagnet` · `btAddTorrentFile` · `btAddTorrentWithResume` · `btRemoveTorrent` · `btAddMagnetEx` · `btAddTorrentFileEx` | +| Filter / streaming | `btIpFilterAdd` · `btIpFilterClear` · `btSetPieceDeadline` · `btClearPieceDeadlines` | | Control | `btPause` · `btResume` · `btForceRecheck` · `btForceReannounce` · `btScrapeTracker` · `btClearTorrentError` | | Priorities / limits | `btSetFilePriority` · `btSetFilePriorities` · `btSetPiecePriority` · `btSetTorrentLimits` · `btSetMaxConnections` · `btSetMaxUploads` | | Flags / modes | `btSetTorrentFlags` · `btUnsetTorrentFlags` · `btSetSequentialDownload` · `btSetAutoManaged` · `btSetSuperSeeding` · `btSetShareMode` · `btSetUploadMode` | diff --git a/docs/api-reference.md b/docs/api-reference.md index ec1fe79..2c84985 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -149,6 +149,31 @@ for announcing your own content's info-hash so others can find you swarm-side. --- +## Filtering & streaming + +### `btIpFilterAdd(in pSession as Integer, in pStartIp as String, in pEndIp as String, in pBlock as Boolean) returns Integer` +Add an **inclusive IP range** to the session's IP filter; `pBlock = true` blocks +it (the default-empty filter allows everything). Rules accumulate. `pStartIp` and +`pEndIp` are textual addresses of the **same family** (both IPv4 or both IPv6), +e.g. `"1.2.3.0"` .. `"1.2.3.255"`. A blocked peer is dropped before connecting. +- **Usage:** command - `btIpFilterAdd sSession, "1.2.3.0", "1.2.3.255", true`. + +### `btIpFilterClear(in pSession as Integer) returns Integer` +Remove all IP-filter rules (back to "allow everything"). +- **Usage:** command - `btIpFilterClear sSession`. + +### `btSetPieceDeadline(in pTorrent as Integer, in pPieceIndex as Integer, in pDeadlineMs as Integer) returns Integer` +**Streaming**: ask libtorrent to fetch a piece within `pDeadlineMs` milliseconds +from now, reordering requests so the soonest deadlines come first. Set deadlines +across the pieces you are about to play to stream in order / seek smoothly. +- **Usage:** command - `btSetPieceDeadline tH, 0, 5000`. + +### `btClearPieceDeadlines(in pTorrent as Integer) returns Integer` +Remove all piece deadlines on the torrent. +- **Usage:** command - `btClearPieceDeadlines tH`. + +--- + ## Add / remove torrents ### `btAddMagnet(in pSession as Integer, in pURI as String, in pSavePath as String) returns Integer` @@ -177,6 +202,14 @@ Remove a torrent from the session. If `pDeleteFiles` is `true`, the downloaded files are deleted too. Returns `0` / negative. - **Usage:** command - `btRemoveTorrent sSession, tH, false`. +### `btAddMagnetEx(in pSession as Integer, in pURI as String, in pSavePath as String, in pFlags as String, in pMask as String) returns Integer` · `btAddTorrentFileEx(in pSession as Integer, in pData as Data, in pSavePath as String, in pFlags as String, in pMask as String) returns Integer` +Like `btAddMagnet` / `btAddTorrentFile`, but apply **add-time `torrent_flags`** — +set the bits named in `pFlags`, touching only the bits named in `pMask` (decimal +strings; combine the `kFlag*` constants). The classic use is **adding paused** +(`kFlagPaused`) so you can set file priorities *before* it starts downloading, or +`kFlagSequentialDownload` for immediate streaming. Returns the torrent handle. +- **Usage:** function - `put btAddMagnetEx(sSession, tURI, tPath, kFlagPaused, kFlagPaused) into tH` (added paused), then set priorities, then `btResume tH`. + --- ## Control diff --git a/src/btx_abi.h b/src/btx_abi.h index fdbeb85..630d5c7 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 7 +#define BTX_ABI_VERSION 8 /* ----------------------------------------------------------- export linkage */ @@ -151,6 +151,24 @@ BTX_API int BTX_CALL btx_find_torrent(int s, const char *infoHashHex); * 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); +/* ====================================================================== * + * Filtering & streaming (ABI v8) + * ====================================================================== */ + +/* Add an inclusive IP range to the session IP filter; block != 0 blocks it + * (the default-empty filter allows everything). Rules accumulate (read-modify- + * write). `startIp`/`endIp` are textual addresses of the SAME family (both IPv4 + * or both IPv6), e.g. "1.2.3.0".."1.2.3.255". btx_ip_filter_clear removes all. */ +BTX_API int BTX_CALL btx_ip_filter_add(int s, const char *startIp, + const char *endIp, int block); +BTX_API int BTX_CALL btx_ip_filter_clear(int s); + +/* Streaming: ask libtorrent to fetch a piece by a deadline (milliseconds from + * now), reordering requests so the soonest deadlines come first. Clear removes + * all deadlines. The data still rides engine ⇄ disk; only the hint crosses. */ +BTX_API int BTX_CALL btx_set_piece_deadline(int t, int pieceIndex, int deadlineMs); +BTX_API int BTX_CALL btx_clear_piece_deadlines(int t); + /* ====================================================================== * * Add / remove torrents * ====================================================================== */ @@ -170,6 +188,19 @@ BTX_API int BTX_CALL btx_add_torrent_file(int s, const void *data, int len, BTX_API int BTX_CALL btx_add_with_resume(int s, const void *resume, int len, const char *savePath); +/* Extended add (ABI v8): like btx_add_magnet / btx_add_torrent_file but apply + * add-time torrent_flags — set the bits named in `maskDec` to `flagsDec`, + * leaving the rest at libtorrent's default. The common use is adding PAUSED + * (kFlagPaused) so you can set file priorities before it starts, or + * kFlagSequentialDownload for immediate streaming. */ +BTX_API int BTX_CALL btx_add_magnet_ex(int s, const char *uri, + const char *savePath, + const char *flagsDec, const char *maskDec); +BTX_API int BTX_CALL btx_add_torrent_file_ex(int s, const void *data, int len, + const char *savePath, + const char *flagsDec, + const char *maskDec); + /* Remove a torrent; deleteFiles != 0 also deletes downloaded files. */ BTX_API int BTX_CALL btx_remove(int s, int t, int deleteFiles); diff --git a/src/torrent.lcb b/src/torrent.lcb index e5be67f..d5594ea 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 7 +constant kABIVersion is 8 -- libfoundation MCStringEncoding value for UTF-8 (ASCII=0, Windows1252=1, -- MacRoman=2, ISO8859_1=3, UTF8=4). Used by _decodeText via MCStringDecode. @@ -223,6 +223,14 @@ private foreign handler _btx_dht_announce(in pS as CInt, in pHash as ZStringUTF8 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" + +-- extended add + filtering/streaming (ABI v8) +private foreign handler _btx_add_magnet_ex(in pS as CInt, in pURI as ZStringUTF8, in pSave as ZStringUTF8, in pFlags as ZStringUTF8, in pMask as ZStringUTF8) returns CInt binds to "c:torrentxt>btx_add_magnet_ex!cdecl" +private foreign handler _btx_add_torrent_file_ex(in pS as CInt, in pData as Pointer, in pLen as CInt, in pSave as ZStringUTF8, in pFlags as ZStringUTF8, in pMask as ZStringUTF8) returns CInt binds to "c:torrentxt>btx_add_torrent_file_ex!cdecl" +private foreign handler _btx_ip_filter_add(in pS as CInt, in pStart as ZStringUTF8, in pEnd as ZStringUTF8, in pBlock as CInt) returns CInt binds to "c:torrentxt>btx_ip_filter_add!cdecl" +private foreign handler _btx_ip_filter_clear(in pS as CInt) returns CInt binds to "c:torrentxt>btx_ip_filter_clear!cdecl" +private foreign handler _btx_set_piece_deadline(in pT as CInt, in pPiece as CInt, in pDeadline as CInt) returns CInt binds to "c:torrentxt>btx_set_piece_deadline!cdecl" +private foreign handler _btx_clear_piece_deadlines(in pT as CInt) returns CInt binds to "c:torrentxt>btx_clear_piece_deadlines!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" private foreign handler _btx_remove(in pS as CInt, in pT as CInt, in pDel as CInt) returns CInt binds to "c:torrentxt>btx_remove!cdecl" @@ -872,6 +880,74 @@ public handler btAddTorrentFile(in pSession as Integer, in pData as Data, in pSa return tHandle end handler +-- ---- extended add: add with add-time torrent_flags (ABI v8) -------------- +-- pFlags / pMask are decimal strings (combine kFlag* constants). The common +-- use is adding PAUSED (kFlagPaused) to set file priorities before it starts. + +public handler btAddMagnetEx(in pSession as Integer, in pURI as String, in pSavePath as String, in pFlags as String, in pMask as String) returns Integer + variable tHandle as Integer + unsafe + put _btx_add_magnet_ex(pSession, pURI, pSavePath, pFlags, pMask) into tHandle + end unsafe + return tHandle +end handler + +public handler btAddTorrentFileEx(in pSession as Integer, in pData as Data, in pSavePath as String, in pFlags as String, in pMask as String) returns Integer + variable tHandle as Integer + variable tPtr as Pointer + put 0 into tHandle + if the number of bytes in pData > 0 then + unsafe + put MCDataGetBytePtr(pData) into tPtr + put _btx_add_torrent_file_ex(pSession, tPtr, the number of bytes in pData, pSavePath, pFlags, pMask) into tHandle + end unsafe + end if + return tHandle +end handler + +-- ---- IP filter + streaming (ABI v8) -------------------------------------- + +-- Add an inclusive IP range to the session filter; pBlock true blocks it. +-- Rules accumulate. Start/end must be the same family (both v4 or both v6). +public handler btIpFilterAdd(in pSession as Integer, in pStartIp as String, in pEndIp as String, in pBlock as Boolean) returns Integer + variable tR as Integer + variable tB as Integer + if pBlock then + put 1 into tB + else + put 0 into tB + end if + unsafe + put _btx_ip_filter_add(pSession, pStartIp, pEndIp, tB) into tR + end unsafe + return tR +end handler + +public handler btIpFilterClear(in pSession as Integer) returns Integer + variable tR as Integer + unsafe + put _btx_ip_filter_clear(pSession) into tR + end unsafe + return tR +end handler + +-- Streaming: fetch a piece by a deadline (ms from now); soonest deadlines win. +public handler btSetPieceDeadline(in pTorrent as Integer, in pPieceIndex as Integer, in pDeadlineMs as Integer) returns Integer + variable tR as Integer + unsafe + put _btx_set_piece_deadline(pTorrent, pPieceIndex, pDeadlineMs) into tR + end unsafe + return tR +end handler + +public handler btClearPieceDeadlines(in pTorrent as Integer) returns Integer + variable tR as Integer + unsafe + put _btx_clear_piece_deadlines(pTorrent) into tR + end unsafe + return tR +end handler + public handler btAddTorrentWithResume(in pSession as Integer, in pResume as Data, in pSavePath as String) returns Integer variable tHandle as Integer variable tPtr as Pointer diff --git a/src/torrent_shim.cpp b/src/torrent_shim.cpp index 7488a99..80475a3 100644 --- a/src/torrent_shim.cpp +++ b/src/torrent_shim.cpp @@ -64,6 +64,8 @@ #include #include #include +#include /* set_ip_filter (ABI v8) */ +#include /* make_address for IP-filter rules */ #include #include #include @@ -695,12 +697,85 @@ extern "C" BTX_API int BTX_CALL btx_dht_announce(int s, const char *infoHashHex, }); } +/* ====================================================================== * + * Filtering & streaming (ABI v8) — IP filter rules and piece deadlines. + * ====================================================================== */ + +extern "C" BTX_API int BTX_CALL btx_ip_filter_add(int s, const char *startIp, + const char *endIp, int block) { + BTX_GUARD_ACTION({ + SessionState *st = session_for(s); + if (!st || !st->ses) { set_error("no live session"); return BTX_ERR_NO_SESSION; } + if (!startIp || !endIp) { set_error("null ip range"); return BTX_ERR_INVALID_ARG; } + lt::error_code ec; + lt::address first = lt::make_address(startIp, ec); + if (ec) { set_error("bad start address"); return BTX_ERR_INVALID_ARG; } + lt::address last = lt::make_address(endIp, ec); + if (ec) { set_error("bad end address"); return BTX_ERR_INVALID_ARG; } + if (first.is_v4() != last.is_v4()) { + set_error("ip range start/end must be the same family"); + return BTX_ERR_INVALID_ARG; + } + /* read-modify-write the session filter so rules accumulate. */ + lt::ip_filter f = st->ses->get_ip_filter(); + std::uint32_t flags = block + ? static_cast(lt::ip_filter::blocked) : 0u; + f.add_rule(first, last, flags); + st->ses->set_ip_filter(f); + return BTX_OK; + }); +} + +extern "C" BTX_API int BTX_CALL btx_ip_filter_clear(int s) { + BTX_GUARD_ACTION({ + SessionState *st = session_for(s); + if (!st || !st->ses) { set_error("no live session"); return BTX_ERR_NO_SESSION; } + /* an empty filter allows everything. */ + st->ses->set_ip_filter(lt::ip_filter()); + return BTX_OK; + }); +} + +extern "C" BTX_API int BTX_CALL btx_set_piece_deadline(int t, int pieceIndex, + int deadlineMs) { + 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 (pieceIndex < 0) { set_error("negative piece index"); return BTX_ERR_INVALID_ARG; } + /* deadline is milliseconds from now; libtorrent reorders requests to hit + * the soonest deadlines first (streaming / seeking). */ + h.set_piece_deadline(lt::piece_index_t{pieceIndex}, deadlineMs); + return BTX_OK; + }); +} + +extern "C" BTX_API int BTX_CALL btx_clear_piece_deadlines(int t) { + 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; } + h.clear_piece_deadlines(); + return BTX_OK; + }); +} + /* ====================================================================== * * Add / remove torrents * ====================================================================== */ namespace { +/* Apply add-time torrent_flags from two decimal strings: set the bits named in + * `maskDec` to the values in `flagsDec`, leaving everything else at libtorrent's + * default. Used by the _ex add variants (e.g. add PAUSED, or sequential). A null + * pair is a no-op (the plain add path). */ +void apply_add_flags(lt::add_torrent_params &atp, const char *flagsDec, + const char *maskDec) { + if (!flagsDec || !maskDec) return; + lt::torrent_flags_t fl{std::strtoull(flagsDec, nullptr, 10)}; + lt::torrent_flags_t mk{std::strtoull(maskDec, nullptr, 10)}; + atp.flags = (atp.flags & ~mk) | (fl & mk); +} + /* Register a freshly-added handle in the torrent table + reverse map and return * our int id (0 if the table is full). Centralised so add_magnet / add_file / * add_resume all map identically. */ @@ -791,6 +866,49 @@ extern "C" BTX_API int BTX_CALL btx_add_with_resume(int s, const void *resume, }); } +/* Extended add (ABI v8): the plain add path + add-time torrent_flags. */ +extern "C" BTX_API int BTX_CALL btx_add_magnet_ex(int s, const char *uri, + const char *savePath, + const char *flagsDec, + const char *maskDec) { + BTX_GUARD_INT({ + SessionState *st = session_for(s); + if (!st || !st->ses) { set_error("no live session"); return 0; } + if (!uri || !*uri) { set_error("empty magnet URI"); return 0; } + lt::error_code ec; + lt::add_torrent_params atp = lt::parse_magnet_uri(uri, ec); + if (ec) { set_error("bad magnet URI: " + ec.message()); return 0; } + if (savePath && *savePath) atp.save_path = savePath; + apply_add_flags(atp, flagsDec, maskDec); + lt::torrent_handle h = st->ses->add_torrent(std::move(atp), ec); + if (ec) { set_error("add_torrent failed: " + ec.message()); return 0; } + return register_torrent(st, h); + }); +} + +extern "C" BTX_API int BTX_CALL btx_add_torrent_file_ex(int s, const void *data, + int len, + const char *savePath, + const char *flagsDec, + const char *maskDec) { + BTX_GUARD_INT({ + SessionState *st = session_for(s); + if (!st || !st->ses) { set_error("no live session"); return 0; } + if (!data || len <= 0) { set_error("empty .torrent buffer"); return 0; } + lt::error_code ec; + auto ti = std::make_shared( + reinterpret_cast(data), len, ec); + if (ec) { set_error("invalid .torrent: " + ec.message()); return 0; } + lt::add_torrent_params atp; + atp.ti = ti; + if (savePath && *savePath) atp.save_path = savePath; + apply_add_flags(atp, flagsDec, maskDec); + lt::torrent_handle h = st->ses->add_torrent(std::move(atp), ec); + if (ec) { set_error("add_torrent failed: " + ec.message()); return 0; } + return register_torrent(st, h); + }); +} + extern "C" BTX_API int BTX_CALL btx_remove(int s, int t, int deleteFiles) { BTX_GUARD_ACTION({ bool ok = false; diff --git a/tests/torrent_smoke_test.cpp b/tests/torrent_smoke_test.cpp index 9bafead..7d7d18a 100644 --- a/tests/torrent_smoke_test.cpp +++ b/tests/torrent_smoke_test.cpp @@ -138,6 +138,16 @@ static void test_bogus_handles_are_noops() { CHECK(btx_dht_announce(h, "0123456789abcdef0123456789abcdef01234567", 6881) < 0); + /* --- ABI v8 filtering / streaming / extended add on a bogus handle --- */ + CHECK(btx_ip_filter_add(h, "1.2.3.0", "1.2.3.255", 1) < 0); + CHECK(btx_ip_filter_clear(h) < 0); + CHECK(btx_set_piece_deadline(h, 0, 5000) < 0); + CHECK(btx_clear_piece_deadlines(h) < 0); + CHECK(btx_add_magnet_ex(h, "magnet:?xt=urn:btih:" + "0123456789abcdef0123456789abcdef01234567", "/tmp", "16", "16") == 0); + const unsigned char d8[4] = {'d', '3', ':', 'e'}; + CHECK(btx_add_torrent_file_ex(h, d8, sizeof d8, "/tmp", "16", "16") == 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);