diff --git a/README.md b/README.md index bff83ad..45eafdd 100644 --- a/README.md +++ b/README.md @@ -150,15 +150,17 @@ These are load-bearing and enforced in the code: ## API at a glance -39 public `bt*` handlers (full signatures in **[api-reference](docs/api-reference.md)**): +56 public `bt*` handlers (full signatures in **[api-reference](docs/api-reference.md)**): | Group | Handlers | |---|---| | Session | `btStartSession` · `btStopSession` · `btLastError` · `btClearError` | | Settings | `btSetInt` · `btSetBool` · `btSetString` · `btGetSetting` · `btSetEncryption` | | Add / remove | `btAddMagnet` · `btAddTorrentFile` · `btAddTorrentWithResume` · `btRemoveTorrent` | -| Control | `btPause` · `btResume` · `btForceRecheck` · `btForceReannounce` | -| Priorities / limits | `btSetFilePriority` · `btSetFilePriorities` · `btSetPiecePriority` · `btSetTorrentLimits` | +| Control | `btPause` · `btResume` · `btForceRecheck` · `btForceReannounce` · `btScrapeTracker` · `btClearTorrentError` | +| Priorities / limits | `btSetFilePriority` · `btSetFilePriorities` · `btSetPiecePriority` · `btSetTorrentLimits` · `btSetMaxConnections` · `btSetMaxUploads` | +| Flags / modes | `btSetTorrentFlags` · `btUnsetTorrentFlags` · `btSetSequentialDownload` · `btSetAutoManaged` · `btSetSuperSeeding` · `btSetShareMode` · `btSetUploadMode` | +| Queue / storage | `btQueuePosition` · `btQueueUp` · `btQueueDown` · `btQueueTop` · `btQueueBottom` · `btMoveStorage` | | Inspect | `btTorrentStatus` · `btTorrentCount` · `btTorrentHandleAt` · `btInfoHash` · `btPieceBitfield` · `btPeerList` | | Events | `btPoll` | | DHT | `btDhtAddBootstrap` · `btDhtState` · `btDhtSaveState` · `btDhtLoadState` | diff --git a/docs/api-reference.md b/docs/api-reference.md index 76a46c2..2282f30 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -193,6 +193,84 @@ unlimited). Distinct from the session-wide `download_rate_limit` / `upload_rate_limit` settings. - **Usage:** command - `btSetTorrentLimits tH, "1000000", "0"`. +### `btSetMaxConnections(in pTorrent as Integer, in pMax as Integer) returns Integer` +Cap the number of peer connections for this torrent (libtorrent wants `>= 2`, or +`-1` for unlimited). +- **Usage:** command - `btSetMaxConnections tH, 80`. + +### `btSetMaxUploads(in pTorrent as Integer, in pMax as Integer) returns Integer` +Cap the number of simultaneously **unchoked** upload slots for this torrent. +- **Usage:** command - `btSetMaxUploads tH, 6`. + +### `btClearTorrentError(in pTorrent as Integer) returns Integer` +Clear a torrent's error state (e.g. after fixing a disk-full or permission problem +that paused it) so it can resume. Distinct from `btClearError`, which clears the +library's last-error string. +- **Usage:** command - `btClearTorrentError tH`. + +### `btScrapeTracker(in pTorrent as Integer) returns Integer` +Ask the tracker(s) for current seed / leecher counts. **Asynchronous**: the +numbers arrive later as a `scrapeReply` event. +- **Usage:** command - `btScrapeTracker tH`, then handle `scrapeReply`. + +### `btMoveStorage(in pTorrent as Integer, in pSavePath as String) returns Integer` +Move the torrent's downloaded files to a new directory. **Asynchronous**: success +arrives as a `storageMoved` event (or `fileError` on failure). The bytes move +engine-side; nothing crosses into script. +- **Usage:** command - `btMoveStorage tH, "/mnt/big/downloads"`. + +#### Torrent flags + +The full `torrent_flags_t` set is exposed as two primitives plus named +conveniences. Flag **values** are the `kFlag*` constants (decimal strings you add +together): `kFlagSeedMode` (1), `kFlagUploadMode` (2), `kFlagShareMode` (4), +`kFlagApplyIpFilter` (8), `kFlagPaused` (16), `kFlagAutoManaged` (32), +`kFlagSuperSeeding` (256), `kFlagSequentialDownload` (512), `kFlagStopWhenReady` +(1024). + +### `btSetTorrentFlags(in pTorrent as Integer, in pFlags as String, in pMask as String) returns Integer` +Set the bits named in `pFlags`, touching only the bits named in `pMask` (a +read-modify-write: `set(flags, mask)`). Both are decimal strings - add `kFlag*` +constants to combine them. +- **Usage:** command - `btSetTorrentFlags tH, kFlagSequentialDownload, kFlagSequentialDownload`. + +### `btUnsetTorrentFlags(in pTorrent as Integer, in pFlags as String) returns Integer` +Clear the bits named in `pFlags`. +- **Usage:** command - `btUnsetTorrentFlags tH, kFlagSequentialDownload`. + +### `btSetSequentialDownload(in pTorrent as Integer, in pOn as Boolean) returns Integer` +Convenience: turn in-order (streaming) download on or off. +- **Usage:** command - `btSetSequentialDownload tH, true`. + +### `btSetAutoManaged(in pTorrent as Integer, in pOn as Boolean) returns Integer` +Convenience: let libtorrent automatically queue / start / stop this torrent. +- **Usage:** command - `btSetAutoManaged tH, true`. + +### `btSetSuperSeeding(in pTorrent as Integer, in pOn as Boolean) returns Integer` +Convenience: super-seed (initial-seeding) mode - only meaningful on a complete seed. +- **Usage:** command - `btSetSuperSeeding tH, true`. + +### `btSetShareMode(in pTorrent as Integer, in pOn as Boolean) returns Integer` +Convenience: optimise this torrent for share-ratio rather than for completion. +- **Usage:** command - `btSetShareMode tH, true`. + +### `btSetUploadMode(in pTorrent as Integer, in pOn as Boolean) returns Integer` +Convenience: upload-only - serve pieces but never request any. +- **Usage:** command - `btSetUploadMode tH, true`. + +#### Download queue + +### `btQueuePosition(in pTorrent as Integer) returns Integer` +The torrent's 0-based position in the download queue, or `-1` if it is not queued +(or the handle is invalid). This getter returns `-1`, not `0`, for "no value", +because `0` is itself a real position - the one getter in the API that does so. +- **Usage:** function - `put btQueuePosition(tH) into tPos`. + +### `btQueueUp(in pTorrent as Integer) returns Integer` · `btQueueDown(...)` · `btQueueTop(...)` · `btQueueBottom(...)` +Move the torrent one step up / down, or all the way to the top / bottom of the +download queue. (Only meaningful for auto-managed torrents.) +- **Usage:** command - `btQueueTop tH`. + ### `btSaveResumeData(in pTorrent as Integer) returns Integer` **Request** resume data for the torrent. This is **asynchronous** (libtorrent's model): the bytes do not return here - they arrive later as a `resumeDataReady` diff --git a/src/btx_abi.h b/src/btx_abi.h index f0dfd6e..6d36fcb 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 3 +#define BTX_ABI_VERSION 4 /* ----------------------------------------------------------- export linkage */ @@ -170,6 +170,39 @@ BTX_API int BTX_CALL btx_set_piece_priority(int t, int pieceIndex, int priority) BTX_API int BTX_CALL btx_set_torrent_limits(int t, const char *downDec, const char *upDec); +/* ---- extended control (ABI v4): flags, slots, queue, storage --------------- + * More of libtorrent's torrent_handle surface. torrent_flags_t rides as a + * 64-bit decimal string (there is no 64-bit foreign int): set_flags writes only + * the bits named in `mask`, unset_flags clears the bits named in `flags`. The + * named bit values (sequential_download, auto_managed, share_mode, upload_mode, + * super_seeding, apply_ip_filter, stop_when_ready, ...) are stable libtorrent + * constants the LCB layer mirrors as kFlag*. */ +BTX_API int BTX_CALL btx_set_torrent_flags(int t, const char *flagsDec, + const char *maskDec); +BTX_API int BTX_CALL btx_unset_torrent_flags(int t, const char *flagsDec); + +/* Per-torrent caps: max peer connections, and max unchoked upload slots. */ +BTX_API int BTX_CALL btx_set_max_connections(int t, int maxConns); +BTX_API int BTX_CALL btx_set_max_uploads(int t, int maxUploads); + +/* Clear a torrent's error state so it can resume (e.g. after fixing a disk or + * permission problem that paused it). */ +BTX_API int BTX_CALL btx_torrent_clear_error(int t); + +/* Ask the tracker(s) for current seed/leecher counts; result -> A_SCRAPE_REPLY. */ +BTX_API int BTX_CALL btx_scrape_tracker(int t); + +/* Move the downloaded files to a new directory; result -> A_STORAGE_MOVED (or + * A_FILE_ERROR on failure). The bytes move engine-side, never through script. */ +BTX_API int BTX_CALL btx_move_storage(int t, const char *savePath); + +/* Download-queue positioning. btx_queue_position returns the 0-based position, + * or -1 when the torrent is not queued (or the handle is invalid). This is the + * ONE int-getter that uses -1 (not 0) for "no value", because 0 is itself a + * valid queue position. btx_queue_move op: 0=up 1=down 2=top 3=bottom. */ +BTX_API int BTX_CALL btx_queue_position(int t); +BTX_API int BTX_CALL btx_queue_move(int t, int op); + /* ====================================================================== * * Status — ONE snapshot per call (perf: never one FFI call per field, §8) * ====================================================================== */ diff --git a/src/torrent.lcb b/src/torrent.lcb index 36bd78d..9754b55 100644 --- a/src/torrent.lcb +++ b/src/torrent.lcb @@ -42,12 +42,25 @@ metadata title is "TorrentXT" -- ===================================================================== -- -- Must equal BTX_ABI_VERSION in src/btx_abi.h; _checkABI() throws on skew. -constant kABIVersion is 3 +constant kABIVersion is 4 -- libfoundation MCStringEncoding value for UTF-8 (ASCII=0, Windows1252=1, -- MacRoman=2, ISO8859_1=3, UTF8=4). Used by _decodeText via MCStringDecode. constant kEncodingUtf8 is 4 +-- torrent_flags_t bit VALUES (stable libtorrent constants; mirror of +-- libtorrent/torrent_flags.hpp). Passed to btSetTorrentFlags / btUnsetTorrentFlags +-- as decimal strings. Combine by adding the values (the bits are disjoint). +constant kFlagSeedMode is "1" -- bit 0: assume we already have all data +constant kFlagUploadMode is "2" -- bit 1: upload only, never request +constant kFlagShareMode is "4" -- bit 2: optimise for share-ratio +constant kFlagApplyIpFilter is "8" -- bit 3: subject this torrent to the IP filter +constant kFlagPaused is "16" -- bit 4: start paused +constant kFlagAutoManaged is "32" -- bit 5: let libtorrent queue/start/stop it +constant kFlagSuperSeeding is "256" -- bit 8: super-seed (initial-seed) mode +constant kFlagSequentialDownload is "512" -- bit 9: download pieces in order (streaming) +constant kFlagStopWhenReady is "1024" -- bit 10: pause as soon as checking finishes + -- Reusable buffer capacities (the single-thread performance playbook: allocate -- once, reuse every poll, never rebuild). Grown on demand if the shim reports a -- bigger -needed. @@ -201,6 +214,17 @@ private foreign handler _btx_set_file_priorities(in pT as CInt, in pPrios as Poi private foreign handler _btx_set_piece_priority(in pT as CInt, in pPiece as CInt, in pPrio as CInt) returns CInt binds to "c:torrentxt>btx_set_piece_priority!cdecl" private foreign handler _btx_set_torrent_limits(in pT as CInt, in pDown as ZStringUTF8, in pUp as ZStringUTF8) returns CInt binds to "c:torrentxt>btx_set_torrent_limits!cdecl" +-- extended control (ABI v4): flags, connection/upload caps, queue, storage +private foreign handler _btx_set_torrent_flags(in pT as CInt, in pFlags as ZStringUTF8, in pMask as ZStringUTF8) returns CInt binds to "c:torrentxt>btx_set_torrent_flags!cdecl" +private foreign handler _btx_unset_torrent_flags(in pT as CInt, in pFlags as ZStringUTF8) returns CInt binds to "c:torrentxt>btx_unset_torrent_flags!cdecl" +private foreign handler _btx_set_max_connections(in pT as CInt, in pMax as CInt) returns CInt binds to "c:torrentxt>btx_set_max_connections!cdecl" +private foreign handler _btx_set_max_uploads(in pT as CInt, in pMax as CInt) returns CInt binds to "c:torrentxt>btx_set_max_uploads!cdecl" +private foreign handler _btx_torrent_clear_error(in pT as CInt) returns CInt binds to "c:torrentxt>btx_torrent_clear_error!cdecl" +private foreign handler _btx_scrape_tracker(in pT as CInt) returns CInt binds to "c:torrentxt>btx_scrape_tracker!cdecl" +private foreign handler _btx_move_storage(in pT as CInt, in pPath as ZStringUTF8) returns CInt binds to "c:torrentxt>btx_move_storage!cdecl" +private foreign handler _btx_queue_position(in pT as CInt) returns CInt binds to "c:torrentxt>btx_queue_position!cdecl" +private foreign handler _btx_queue_move(in pT as CInt, in pOp as CInt) returns CInt binds to "c:torrentxt>btx_queue_move!cdecl" + private foreign handler _btx_torrent_status(in pT as CInt, in pOut as Pointer, in pCap as CInt) returns CInt binds to "c:torrentxt>btx_torrent_status!cdecl" private foreign handler _btx_torrent_count(in pS as CInt) returns CInt binds to "c:torrentxt>btx_torrent_count!cdecl" private foreign handler _btx_torrent_handle_at(in pS as CInt, in pIndex as CInt) returns CInt binds to "c:torrentxt>btx_torrent_handle_at!cdecl" @@ -848,6 +872,148 @@ public handler btSetTorrentLimits(in pTorrent as Integer, in pDownBytesPerSec as return tR end handler +-- ---- extended control (ABI v4): flags, caps, queue, storage -------------- + +-- Set/clear raw torrent_flags_t bits. pFlags and pMask are decimal strings; +-- combine the kFlag* constants by adding their values. set writes ONLY the +-- masked bits, so set(flags, mask) is the read-modify-write primitive. +public handler btSetTorrentFlags(in pTorrent as Integer, in pFlags as String, in pMask as String) returns Integer + variable tR as Integer + unsafe + put _btx_set_torrent_flags(pTorrent, pFlags, pMask) into tR + end unsafe + return tR +end handler + +public handler btUnsetTorrentFlags(in pTorrent as Integer, in pFlags as String) returns Integer + variable tR as Integer + unsafe + put _btx_unset_torrent_flags(pTorrent, pFlags) into tR + end unsafe + return tR +end handler + +-- Convenience: toggle one common flag by name (built on the generic pair). +public handler btSetSequentialDownload(in pTorrent as Integer, in pOn as Boolean) returns Integer + if pOn then + return btSetTorrentFlags(pTorrent, kFlagSequentialDownload, kFlagSequentialDownload) + end if + return btUnsetTorrentFlags(pTorrent, kFlagSequentialDownload) +end handler + +public handler btSetAutoManaged(in pTorrent as Integer, in pOn as Boolean) returns Integer + if pOn then + return btSetTorrentFlags(pTorrent, kFlagAutoManaged, kFlagAutoManaged) + end if + return btUnsetTorrentFlags(pTorrent, kFlagAutoManaged) +end handler + +public handler btSetSuperSeeding(in pTorrent as Integer, in pOn as Boolean) returns Integer + if pOn then + return btSetTorrentFlags(pTorrent, kFlagSuperSeeding, kFlagSuperSeeding) + end if + return btUnsetTorrentFlags(pTorrent, kFlagSuperSeeding) +end handler + +public handler btSetShareMode(in pTorrent as Integer, in pOn as Boolean) returns Integer + if pOn then + return btSetTorrentFlags(pTorrent, kFlagShareMode, kFlagShareMode) + end if + return btUnsetTorrentFlags(pTorrent, kFlagShareMode) +end handler + +public handler btSetUploadMode(in pTorrent as Integer, in pOn as Boolean) returns Integer + if pOn then + return btSetTorrentFlags(pTorrent, kFlagUploadMode, kFlagUploadMode) + end if + return btUnsetTorrentFlags(pTorrent, kFlagUploadMode) +end handler + +-- Per-torrent caps: max peer connections, and max unchoked upload slots. +public handler btSetMaxConnections(in pTorrent as Integer, in pMax as Integer) returns Integer + variable tR as Integer + unsafe + put _btx_set_max_connections(pTorrent, pMax) into tR + end unsafe + return tR +end handler + +public handler btSetMaxUploads(in pTorrent as Integer, in pMax as Integer) returns Integer + variable tR as Integer + unsafe + put _btx_set_max_uploads(pTorrent, pMax) into tR + end unsafe + return tR +end handler + +-- Clear a torrent's error state so it can resume (after fixing disk/permission). +public handler btClearTorrentError(in pTorrent as Integer) returns Integer + variable tR as Integer + unsafe + put _btx_torrent_clear_error(pTorrent) into tR + end unsafe + return tR +end handler + +-- Ask the tracker(s) for current seed/leecher counts (-> scrapeReply event). +public handler btScrapeTracker(in pTorrent as Integer) returns Integer + variable tR as Integer + unsafe + put _btx_scrape_tracker(pTorrent) into tR + end unsafe + return tR +end handler + +-- Move the downloaded files to a new folder (-> storageMoved / fileError event). +public handler btMoveStorage(in pTorrent as Integer, in pSavePath as String) returns Integer + variable tR as Integer + unsafe + put _btx_move_storage(pTorrent, pSavePath) into tR + end unsafe + return tR +end handler + +-- Download-queue position: 0-based, or -1 if not queued (or invalid handle). +public handler btQueuePosition(in pTorrent as Integer) returns Integer + variable tR as Integer + unsafe + put _btx_queue_position(pTorrent) into tR + end unsafe + return tR +end handler + +public handler btQueueUp(in pTorrent as Integer) returns Integer + variable tR as Integer + unsafe + put _btx_queue_move(pTorrent, 0) into tR + end unsafe + return tR +end handler + +public handler btQueueDown(in pTorrent as Integer) returns Integer + variable tR as Integer + unsafe + put _btx_queue_move(pTorrent, 1) into tR + end unsafe + return tR +end handler + +public handler btQueueTop(in pTorrent as Integer) returns Integer + variable tR as Integer + unsafe + put _btx_queue_move(pTorrent, 2) into tR + end unsafe + return tR +end handler + +public handler btQueueBottom(in pTorrent as Integer) returns Integer + variable tR as Integer + unsafe + put _btx_queue_move(pTorrent, 3) into tR + end unsafe + return tR +end handler + -- Request resume data (async; arrives later as a resumeDataReady event whose -- "resumeData" key holds the bytes to persist). public handler btSaveResumeData(in pTorrent as Integer) returns Integer diff --git a/src/torrent_shim.cpp b/src/torrent_shim.cpp index 1ba78e3..d255f67 100644 --- a/src/torrent_shim.cpp +++ b/src/torrent_shim.cpp @@ -842,6 +842,114 @@ extern "C" BTX_API int BTX_CALL btx_set_torrent_limits(int t, const char *downDe }); } +/* ====================================================================== * + * Control — extended (ABI v4): flags, connection/upload caps, error clear, + * scrape, storage move, download-queue positioning. More of libtorrent's + * torrent_handle surface, all fire-and-forget; scrape/move results ride the + * already-wired A_SCRAPE_REPLY / A_STORAGE_MOVED alerts. + * ====================================================================== */ + +extern "C" BTX_API int BTX_CALL btx_set_torrent_flags(int t, const char *flagsDec, + const char *maskDec) { + 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 (!flagsDec || !maskDec) { set_error("null flags"); return BTX_ERR_INVALID_ARG; } + /* torrent_flags_t is a 64-bit bitfield; it crosses as a decimal string. + * set_flags(flags, mask) writes only the masked bits. */ + std::uint64_t flags = std::strtoull(flagsDec, nullptr, 10); + std::uint64_t mask = std::strtoull(maskDec, nullptr, 10); + h.set_flags(lt::torrent_flags_t{flags}, lt::torrent_flags_t{mask}); + return BTX_OK; + }); +} + +extern "C" BTX_API int BTX_CALL btx_unset_torrent_flags(int t, const char *flagsDec) { + 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 (!flagsDec) { set_error("null flags"); return BTX_ERR_INVALID_ARG; } + std::uint64_t flags = std::strtoull(flagsDec, nullptr, 10); + h.unset_flags(lt::torrent_flags_t{flags}); + return BTX_OK; + }); +} + +extern "C" BTX_API int BTX_CALL btx_set_max_connections(int t, int maxConns) { + 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; } + /* libtorrent wants >= 2 (or -1 == unlimited); forward as-is, it clamps. */ + h.set_max_connections(maxConns); + return BTX_OK; + }); +} + +extern "C" BTX_API int BTX_CALL btx_set_max_uploads(int t, int maxUploads) { + 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.set_max_uploads(maxUploads); + return BTX_OK; + }); +} + +extern "C" BTX_API int BTX_CALL btx_torrent_clear_error(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_error(); + return BTX_OK; + }); +} + +extern "C" BTX_API int BTX_CALL btx_scrape_tracker(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; } + /* default idx -1 == scrape every tracker in the list; -> A_SCRAPE_REPLY. */ + h.scrape_tracker(); + return BTX_OK; + }); +} + +extern "C" BTX_API int BTX_CALL btx_move_storage(int t, const char *savePath) { + 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 (!savePath || !*savePath) { set_error("empty save path"); return BTX_ERR_INVALID_ARG; } + /* Async; result -> A_STORAGE_MOVED (or A_FILE_ERROR). Bytes move + * engine-side between the old and new directory, never through script. */ + h.move_storage(std::string(savePath)); + return BTX_OK; + }); +} + +extern "C" BTX_API int BTX_CALL btx_queue_position(int t) { + BTX_GUARD_INT({ + bool ok = false; lt::torrent_handle h = torrent_only(t, nullptr, &ok); + /* Not a buffer getter: -1 (libtorrent's own "not queued" sentinel) also + * stands in for an invalid handle, since 0 is a real queue position. */ + if (!ok) return -1; + return static_cast(h.queue_position()); + }); +} + +extern "C" BTX_API int BTX_CALL btx_queue_move(int t, int op) { + 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; } + switch (op) { + case 0: h.queue_position_up(); break; + case 1: h.queue_position_down(); break; + case 2: h.queue_position_top(); break; + case 3: h.queue_position_bottom(); break; + default: set_error("queue op must be 0..3"); return BTX_ERR_INVALID_ARG; + } + return BTX_OK; + }); +} + /* ====================================================================== * * Status — one KV-record snapshot per call (perf §8: never one call per field) * ====================================================================== */ diff --git a/tests/torrent_smoke_test.cpp b/tests/torrent_smoke_test.cpp index 14a1c05..20cb0c6 100644 --- a/tests/torrent_smoke_test.cpp +++ b/tests/torrent_smoke_test.cpp @@ -165,6 +165,21 @@ static void test_bogus_handles_are_noops() { const unsigned char prios[2] = {4, 4}; CHECK(btx_set_file_priorities(h, prios, 2) < 0); + /* --- ABI v4 extended control on a bogus torrent handle -> error --- */ + CHECK(btx_set_torrent_flags(h, "512", "512") < 0); + CHECK(btx_unset_torrent_flags(h, "512") < 0); + CHECK(btx_set_max_connections(h, 50) < 0); + CHECK(btx_set_max_uploads(h, 4) < 0); + CHECK(btx_torrent_clear_error(h) < 0); + CHECK(btx_scrape_tracker(h) < 0); + CHECK(btx_move_storage(h, "/tmp") < 0); + CHECK(btx_queue_move(h, 2) < 0); + /* btx_queue_position is the lone int-getter that returns -1 (not 0) for a + * bad handle, because 0 is a valid position. */ + CHECK(btx_queue_position(h) == -1); + /* argument validation independent of the (bogus) handle path. */ + CHECK(btx_queue_move(h, 99) < 0); /* out-of-range op */ + /* torrent getters on a bogus torrent -> 0/empty. */ CHECK(btx_torrent_status(h, buf, sizeof buf) == 0); CHECK(btx_info_hash_hex(h, buf, sizeof buf) == 0);