From 7559d4dc1ecb0b8edbc30b989682a67844daec91 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 27 Jun 2026 22:06:47 +0000 Subject: [PATCH 1/5] tests: fix self-test harness - no `is a string` in LiveCode (OXT pass) First OXT run of the harness threw on: stAssert "btLastError is a string", (btLastError() is a string) LiveCode's `is a ` operator only accepts number/integer/boolean/point/ rect/date/color - there is NO `is a string` type token, so it is a parse/runtime error, not a failed assertion. (The static checker can't catch this - it does not model LiveCode's type vocabulary.) Replaced with a valid, deterministic check that also exercises btClearError: btClearError stAssert "btLastError is empty after btClearError", (btLastError() is empty) Audited every other `is a` in the harness: all are `is a number` (valid) or `there is a field` (existence) - this was the only invalid type token. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01371AXB4CUUke7enHHS8okc --- tests/torrent-selftest.livecodescript | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/torrent-selftest.livecodescript b/tests/torrent-selftest.livecodescript index ea9b292..9bda818 100644 --- a/tests/torrent-selftest.livecodescript +++ b/tests/torrent-selftest.livecodescript @@ -174,7 +174,11 @@ command stRun exit stRun end if stAssert "btStartSession returns a positive handle", (sSession > 0) - stAssert "btLastError is a string", (btLastError() is a string) + -- NB: LiveCode's `is a ` only knows number/integer/boolean/point/rect/ + -- date/color - there is NO `is a string`. Test btLastError by clearing it and + -- confirming it reads back empty (exercises btClearError + btLastError). + btClearError + stAssert "btLastError is empty after btClearError", (btLastError() is empty) -- Wrap the rest: a mis-declared foreign handler (wrong signature) raises a -- runtime error on its first call. Catch it so the harness REPORTS which call From 71f2101fb7f53e872b450910f7ccc11d8391ce55 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 27 Jun 2026 22:35:16 +0000 Subject: [PATCH 2/5] examples: channels app - file detail, streaming, inline progress bars Three upgrades to the DHT channel demo, all using the now-runtime-confirmed v8 surface the demo previously ignored: - Inline progress bars in the transfers table: the Progress column is now a monospaced [####------] 42% bar (chProgressBar, the flagship client's helper) instead of plain "42%". - Selected-transfer file detail: click a transfer to see the files inside it via btFileList - name, size, and a LIVE per-file progress bar that climbs as the download advances. Updated each 1 s tick (guarded to repaint only on change), selection preserved across the repaint by torrent handle (sXferHandle map). - "Stream Selected (in order)" button: flips btSetSequentialDownload on the selected transfer so a media release plays as it arrives instead of in random piece order. Window grew to 900 tall to fit the new file panel below the transfers table; Quick drop + log shifted down. New handlers: chProgressBar, chShowFiles, chShowFilesForSelection, chStreamSelected, chSelectedXferLine, chSelectedXferHandle. Tooltips + the in-app help + header doc updated. Verified statically (check-livecodescript: 5 files OK; ASCII-clean; 54 balanced handlers; btFileList + btSetSequentialDownload resolve to real public handlers, both confirmed working in the OXT self-test run). Needs an OXT pass to confirm the new layout and the live file-progress view. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01371AXB4CUUke7enHHS8okc --- examples/torrent-dht-channels.livecodescript | 167 +++++++++++++++++-- 1 file changed, 151 insertions(+), 16 deletions(-) diff --git a/examples/torrent-dht-channels.livecodescript b/examples/torrent-dht-channels.livecodescript index 50556da..d4aac3b 100644 --- a/examples/torrent-dht-channels.livecodescript +++ b/examples/torrent-dht-channels.livecodescript @@ -26,6 +26,10 @@ script "dhtChannelsApp" -- every entry is cryptographically signed by its channel (tamper-proof: only the -- channel's secret key could have published it). -- +-- Click any transfer to see the files inside it with a live per-file progress +-- bar (btFileList), or hit "Stream" to download it in order (btSetSequentialDownload) +-- so a media release plays as it arrives. The transfers table shows inline bars. +-- -- It also includes a "Quick drop" panel using IMMUTABLE items: pin a short text, -- get a 40-character content-address code, and fetch any code back. -- @@ -47,6 +51,7 @@ local sFollowFeeds -- array: pubkey -> their raw feed value local sFeedKey -- array: feed-list line -> channel pubkey local sFeedMag -- array: feed-list line -> magnet local sFeedTitle -- array: feed-list line -> release title +local sXferHandle -- array: transfers-list line -> torrent handle -- ---- lifecycle --------------------------------------------------------------- on openStack @@ -87,6 +92,12 @@ on mouseUp case "chCopyMag" chCopySelectedMagnet break + case "chStream" + chStreamSelected + break + case "chXfers" + chShowFilesForSelection + break case "chUnfollow" chUnfollow break @@ -119,7 +130,7 @@ command chBuild end if lock screen set the width of this stack to 880 - set the height of this stack to 748 + set the height of this stack to 900 set the backgroundColor of this card to "237,239,243" set the title of this stack to "TorrentXT - Decentralized Channels" chMakeLabel "chTitle", " Decentralized Channels - share files with no server", "0,0,880,30", 14 @@ -155,23 +166,35 @@ command chBuild chMakeLabel "chXferHeader", "#" & tab & "Name" & tab & "State" & tab & "Progress" & tab & "Peers", "16,384,864,402", 10 chMakeList "chXfers", "16,404,864,490", false chMakeLabel "chTotals", "", "16,494,864,514", 10 - set the tabStops of field "chXferHeader" to "26,430,540,650" - set the tabStops of field "chXfers" to "26,430,540,650" + set the tabStops of field "chXferHeader" to "26,400,500,640" + set the tabStops of field "chXfers" to "26,400,500,640" set the textFont of field "chXferHeader" to "Courier" set the textFont of field "chXfers" to "Courier" set the textSize of field "chXferHeader" to 10 set the textSize of field "chXfers" to 10 + -- 3b. Contents of the selected transfer (live per-file progress) + streaming + chMakeButton "chStream", "Stream Selected (in order)", "16,520,300,544" + chMakeLabel "chFilesCaption", "Click a transfer above to see its files. Stream downloads in order so media can play as it arrives.", "308,524,864,542", 9 + chMakeLabel "chFilesHeader", "File" & tab & "Size" & tab & "Progress", "16,548,864,566", 10 + chMakeList "chFiles", "16,568,864,662", true + set the tabStops of field "chFilesHeader" to "440,540" + set the tabStops of field "chFiles" to "440,540" + set the textFont of field "chFilesHeader" to "Courier" + set the textFont of field "chFiles" to "Courier" + set the textSize of field "chFilesHeader" to 10 + set the textSize of field "chFiles" to 10 + set the text of field "chFiles" to " Click a transfer above to see its files and per-file progress." -- 4. Quick drop (immutable) - chSection "chSecDrop", "4. Quick drop - paste text, get a code anyone can fetch back", "16,522,864,542" - chMakeField "chPinText", "16,548,430,572", false - chMakeButton "chPin", "Pin", "438,548,510,572" - chMakeField "chPinCode", "518,548,790,572", true - chMakeButton "chPinCopy", "Copy", "794,548,864,572" - chMakeField "chFetchCode", "16,576,430,600", false - chMakeButton "chFetchBtn", "Fetch", "438,576,510,600" - chMakeField "chDropResult", "518,576,864,600", true + chSection "chSecDrop", "4. Quick drop - paste text, get a code anyone can fetch back", "16,672,864,692" + chMakeField "chPinText", "16,698,430,722", false + chMakeButton "chPin", "Pin", "438,698,510,722" + chMakeField "chPinCode", "518,698,790,722", true + chMakeButton "chPinCopy", "Copy", "794,698,864,722" + chMakeField "chFetchCode", "16,726,430,750", false + chMakeButton "chFetchBtn", "Fetch", "438,726,510,750" + chMakeField "chDropResult", "518,726,864,750", true -- Log - chMakeList "chLog", "16,608,864,738", true + chMakeList "chLog", "16,758,864,888", true set the textFont of field "chMyKey" to "Courier" set the textFont of field "chFollowKey" to "Courier" set the textFont of field "chPinCode" to "Courier" @@ -251,6 +274,9 @@ command chAddTips set the tooltip of button "chDownload" to "Download the selected file - it transfers peer-to-peer from the publisher." set the tooltip of button "chCopyMag" to "Copy the selected file's magnet link to the clipboard." set the tooltip of button "chUnfollow" to "Stop following the selected file's channel." + set the tooltip of field "chXfers" to "Click a transfer to see its files and per-file progress below." + set the tooltip of button "chStream" to "Download the selected transfer in order, so media plays as it arrives." + set the tooltip of field "chFiles" to "Files inside the selected transfer, with live per-file progress." set the tooltip of field "chPinText" to "Type any short text (up to 1000 bytes) to pin to the network." set the tooltip of button "chPin" to "Store the text on the DHT and get back a short share code." set the tooltip of button "chPinCopy" to "Copy the share code to the clipboard." @@ -270,7 +296,8 @@ command chHelp put tMsg & "2) CHANNELS YOU FOLLOW - paste someone's channel card to see their latest files. Pick" & return into tMsg put tMsg & " one and Download; it transfers from them while they are online. Every entry is signed," & return into tMsg put tMsg & " so it is tamper-proof." & return & return into tMsg - put tMsg & "3) TRANSFERS - a live view of what you are sending and receiving." & return & return into tMsg + put tMsg & "3) TRANSFERS - a live view of what you are sending and receiving. Click one to see" & return into tMsg + put tMsg & " the files inside it with per-file progress, or Stream it to download in order." & return & return into tMsg put tMsg & "4) QUICK DROP - paste any text, get a short code; anyone with the code gets it back." & return & return into tMsg put tMsg & "TRY IT on two machines: Copy your card on one, Follow it on the other, then share a file." into tMsg answer tMsg with "Got it" @@ -716,15 +743,19 @@ end chRefreshSubs -- ---- transfers dashboard ----------------------------------------------------- command chRefreshTransfers local tCount, tI, tH, tS, tRows, tPct, tDown, tUp, tLineNum, tD, tNodes, tDht - local tColors, tErr + local tColors, tErr, tSelH, tSelLine if sSession is 0 or sSession is empty then exit chRefreshTransfers end if + -- remember which transfer is selected (by handle) so the 1 s repaint keeps it + put chSelectedXferHandle() into tSelH put btTorrentCount(sSession) into tCount put "" into tRows put 0 into tDown put 0 into tUp put 0 into tLineNum + put 0 into tSelLine + put empty into sXferHandle repeat with tI = 0 to (tCount - 1) put btTorrentHandleAt(sSession, tI) into tH if tH is 0 then @@ -732,10 +763,15 @@ command chRefreshTransfers end if put btTorrentStatus(tH) into tS add 1 to tLineNum + put tH into sXferHandle[tLineNum] + if tH is tSelH then + put tLineNum into tSelLine + end if put (the round of (tS["progress"] * 1000) / 10) into tPct put chStateColor(tS["state"], tS["error"]) into tColors[tLineNum] - put tLineNum & tab & chTrunc(tS["name"], 44) & tab & chStateName(tS["state"]) & tab & \ - tPct & "%" & tab & tS["numPeers"] & return after tRows + -- Progress column is now an inline ASCII bar (the field is monospaced) + put tLineNum & tab & chTrunc(tS["name"], 40) & tab & chStateName(tS["state"]) & tab & \ + chProgressBar(tPct) & tab & tS["numPeers"] & return after tRows add tS["downloadRate"] to tDown add tS["uploadRate"] to tUp end repeat @@ -769,11 +805,110 @@ command chRefreshTransfers end repeat catch tErr end try + -- keep the selected transfer highlighted across the repaint + if tSelLine > 0 then + set the hilitedLines of field "chXfers" to tSelLine + end if put tLineNum && "transfers down" && (chFmtBytes(tDown) & "/s") && " up" \ && (chFmtBytes(tUp) & "/s") && " " & tDht into field "chTotals" unlock screen + -- refresh the file detail for whatever transfer is selected (live per-file %) + chShowFiles tSelH end chRefreshTransfers +-- ---- selected-transfer detail: the file table + streaming -------------------- + +-- The 0-based line of the selected transfer, or 0 if none. +function chSelectedXferLine + local tNum + set the itemDelimiter to comma -- the hilitedLines is comma-delimited + put item 1 of (the hilitedLines of field "chXfers") into tNum + if tNum is empty or tNum is not a number then + return 0 + end if + return tNum +end chSelectedXferLine + +-- The torrent handle of the selected transfer, or 0 if none / stale. +function chSelectedXferHandle + local tL + put chSelectedXferLine() into tL + if tL is 0 or sXferHandle[tL] is empty then + return 0 + end if + return sXferHandle[tL] +end chSelectedXferHandle + +-- Show the file table of a torrent: name, size, and a live per-file progress +-- bar (btFileList - the marquee v5 inspection call). Empty until metadata +-- arrives (a magnet fetches its file list over the swarm). +command chShowFiles pHandle + local tFiles, tFile, tRows, tPct + if there is no field "chFiles" then + exit chShowFiles + end if + if pHandle is 0 or pHandle is empty then + put " Click a transfer above to see its files and per-file progress." into tRows + else + put btFileList(pHandle) into tFiles + put "" into tRows + repeat for each element tFile in tFiles + if tFile["size"] > 0 then + put (the round of (tFile["progress"] * 1000 / tFile["size"]) / 10) into tPct + else + put 0 into tPct + end if + put chTrunc(tFile["path"], 60) & tab & chFmtBytes(tFile["size"]) & tab \ + & chProgressBar(tPct) & return after tRows + end repeat + if the last char of tRows is return then + delete the last char of tRows + end if + if tRows is empty then + put " No file list yet - metadata still arriving (a magnet fetches it over the swarm)." into tRows + end if + end if + -- repaint only when the contents actually changed (single-thread playbook) + if the text of field "chFiles" is not tRows then + set the text of field "chFiles" to tRows + end if +end chShowFiles + +command chShowFilesForSelection + chShowFiles chSelectedXferHandle() +end chShowFilesForSelection + +-- Turn on sequential (in-order) download for the selected transfer, so a media +-- release can play as it arrives instead of downloading pieces at random. +command chStreamSelected + local tH, tR + put chSelectedXferHandle() into tH + if tH is 0 then + chLog "Select a transfer first, then click Stream." + exit chStreamSelected + end if + put btSetSequentialDownload(tH, true) into tR + if tR is 0 then + chLog "Streaming on: transfer" && tH && "now downloads in order (play-as-it-arrives)." + else + chLog "Could not enable streaming:" && btLastError() + end if +end chStreamSelected + +-- a 10-segment ASCII progress bar, e.g. [####------] 42% (monospaced fields) +function chProgressBar pPct + local tFilled + put trunc(pPct / 10) into tFilled + if tFilled < 0 then + put 0 into tFilled + end if + if tFilled > 10 then + put 10 into tFilled + end if + return "[" & (char 1 to tFilled of "##########") & \ + (char 1 to (10 - tFilled) of "----------") & "]" && pPct & "%" +end chProgressBar + -- ---- quick drop (immutable) -------------------------------------------------- command chPin local tData, tTarget From ef653df29bd9e2755ff6bad0e192c41d9d2b46b5 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 27 Jun 2026 22:58:10 +0000 Subject: [PATCH 3/5] examples: channels app - two-column layout + native streaming player Rework the DHT channels demo so it fits a 720p (1280x720) screen and adds real media playback: - Two-column layout (1180x648): your-channel + follows on the left, transfers + selected-transfer file detail on the right, quick-drop and log full-width across the bottom. Replaces the 880x900 single column that overran 720p vertically. tabStops re-fit to the narrower fields. - A native OXT player object (chPlayer), hidden by default, that appears over the transfer pane only while streaming and is dismissed with Close. Stream now points it at the largest video/audio file in the transfer (chMainMediaFile) on the torrent's save path and plays it as the in-order download fills (btSetSequentialDownload + btTorrentStatus savePath + btFileList). Video and audio are the only streamed kinds (chMediaKind extension classifier). - The player is built defensively (try/sHasPlayer): a build without a player object still gets sequential download and a log hint, never a crash. New chOpenPlayer / chCloseStream commands, chCloseStream wired into mouseUp, tooltips + help text updated. Verified statically (tools/check-livecodescript.py: 5 files OK); needs an OXT pass to confirm the player object behaves on the target build. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01371AXB4CUUke7enHHS8okc --- examples/torrent-dht-channels.livecodescript | 260 ++++++++++++++----- 1 file changed, 199 insertions(+), 61 deletions(-) diff --git a/examples/torrent-dht-channels.livecodescript b/examples/torrent-dht-channels.livecodescript index d4aac3b..e0544b2 100644 --- a/examples/torrent-dht-channels.livecodescript +++ b/examples/torrent-dht-channels.livecodescript @@ -27,8 +27,9 @@ script "dhtChannelsApp" -- channel's secret key could have published it). -- -- Click any transfer to see the files inside it with a live per-file progress --- bar (btFileList), or hit "Stream" to download it in order (btSetSequentialDownload) --- so a media release plays as it arrives. The transfers table shows inline bars. +-- bar (btFileList). Hit "Stream" on a video or audio release and it downloads in +-- order (btSetSequentialDownload) AND plays right here in a built-in player that +-- appears over the transfer pane. The transfers table shows inline progress bars. -- -- It also includes a "Quick drop" panel using IMMUTABLE items: pin a short text, -- get a 40-character content-address code, and fetch any code back. @@ -52,6 +53,7 @@ local sFeedKey -- array: feed-list line -> channel pubkey local sFeedMag -- array: feed-list line -> magnet local sFeedTitle -- array: feed-list line -> release title local sXferHandle -- array: transfers-list line -> torrent handle +local sHasPlayer -- "true" once the native player object built OK -- ---- lifecycle --------------------------------------------------------------- on openStack @@ -95,6 +97,9 @@ on mouseUp case "chStream" chStreamSelected break + case "chCloseStream" + chCloseStream + break case "chXfers" chShowFilesForSelection break @@ -125,82 +130,107 @@ end mouseDoubleUp -- ---- self-building UI (idempotent) ------------------------------------------- command chBuild + local tErr if there is a field "chLog" then exit chBuild end if lock screen - set the width of this stack to 880 - set the height of this stack to 900 + -- two columns so the whole app fits a 720p (1280x720) screen + set the width of this stack to 1180 + set the height of this stack to 648 set the backgroundColor of this card to "237,239,243" set the title of this stack to "TorrentXT - Decentralized Channels" - chMakeLabel "chTitle", " Decentralized Channels - share files with no server", "0,0,880,30", 14 + chMakeLabel "chTitle", " Decentralized Channels - share files with no server", "0,0,1180,30", 14 set the opaque of field "chTitle" to true set the backgroundColor of field "chTitle" to "44,90,160" set the foregroundColor of field "chTitle" to "255,255,255" set the textStyle of field "chTitle" to "bold" - chMakeButton "chHelp", "? What is this?", "700,4,864,26" - -- 1. My channel - chSection "chSecMine", "1. Your channel - publish files for people to download", "16,40,864,60" - chMakeLabel "chMyKeyLabel", "Your channel address. Click Copy to share it - that is how people follow you and get your files:", "16,64,864,82", 10 - chMakeField "chMyKey", "16,84,524,108", true - chMakeButton "chCopyKey", "Copy", "532,84,620,108" - chMakeButton "chNewId", "New ID", "626,84,728,108" - chMakeButton "chSetName", "Set Name", "734,84,842,108" - chMakeButton "chPublish", "Publish a File to My Channel...", "16,116,300,142" - chMakeLabel "chNowPub", "Not publishing yet.", "310,120,864,140", 10 - -- 2. Subscriptions / unified feed - chSection "chSecSubs", "2. Channels you follow - download what they share", "16,150,864,170" - chMakeField "chFollowKey", "16,176,640,200", false - chMakeButton "chFollow", "Follow", "648,176,742,200" - chMakeButton "chRefresh", "Refresh", "748,176,842,200" - chMakeLabel "chFeedCaption", "Files shared by channels you follow. Each row is signed by its channel, so it cannot be faked. Pick one and Download:", "16,204,864,220", 9 - chMakeLabel "chSubsHeader", "Channel" & tab & "Release" & tab & "Signed by (key)", "16,222,864,240", 10 - chMakeList "chSubs", "16,242,864,316", false - chMakeButton "chDownload", "Download Selected", "16,322,180,348" - chMakeButton "chCopyMag", "Copy Magnet", "188,322,300,348" - chMakeButton "chUnfollow", "Unfollow Channel", "308,322,448,348" - set the tabStops of field "chSubsHeader" to "160,580" - set the tabStops of field "chSubs" to "160,580" - -- 3. Transfers - chSection "chSecXfer", "3. Transfers - live upload + download activity", "16,358,864,378" - chMakeLabel "chXferHeader", "#" & tab & "Name" & tab & "State" & tab & "Progress" & tab & "Peers", "16,384,864,402", 10 - chMakeList "chXfers", "16,404,864,490", false - chMakeLabel "chTotals", "", "16,494,864,514", 10 - set the tabStops of field "chXferHeader" to "26,400,500,640" - set the tabStops of field "chXfers" to "26,400,500,640" + chMakeButton "chHelp", "? What is this?", "1000,4,1164,26" + -- ===== LEFT COLUMN (x 16..580): your channel + channels you follow ===== + chSection "chSecMine", "1. Your channel - publish files for people to download", "16,38,580,58" + chMakeLabel "chMyKeyLabel", "Your channel address. Click Copy to share it - that is how people follow you and get your files:", "16,62,580,80", 10 + chMakeField "chMyKey", "16,82,580,106", true + chMakeButton "chCopyKey", "Copy", "16,112,140,136" + chMakeButton "chNewId", "New ID", "148,112,272,136" + chMakeButton "chSetName", "Set Name", "280,112,404,136" + chMakeButton "chPublish", "Publish a File...", "16,142,290,166" + chMakeLabel "chNowPub", "Not publishing yet.", "298,146,580,166", 9 + chSection "chSecSubs", "2. Channels you follow - download what they share", "16,176,580,196" + chMakeField "chFollowKey", "16,200,400,224", false + chMakeButton "chFollow", "Follow", "408,200,492,224" + chMakeButton "chRefresh", "Refresh", "498,200,580,224" + chMakeLabel "chFeedCaption", "Files shared by channels you follow. Each row is signed by its channel, so it cannot be faked. Pick one and Download:", "16,228,580,252", 9 + chMakeLabel "chSubsHeader", "Channel" & tab & "Release" & tab & "Signed by", "16,256,580,274", 10 + chMakeList "chSubs", "16,276,580,452", false + chMakeButton "chDownload", "Download", "16,458,180,482" + chMakeButton "chCopyMag", "Copy Magnet", "188,458,300,482" + chMakeButton "chUnfollow", "Unfollow", "308,458,448,482" + set the tabStops of field "chSubsHeader" to "150,430" + set the tabStops of field "chSubs" to "150,430" + -- ===== RIGHT COLUMN (x 596..1164): transfers + selected-transfer detail ===== + chSection "chSecXfer", "3. Transfers - live upload + download activity", "596,38,1164,58" + chMakeLabel "chXferHeader", "#" & tab & "Name" & tab & "State" & tab & "Progress" & tab & "Peers", "596,62,1164,80", 10 + chMakeList "chXfers", "596,82,1164,264", false + chMakeLabel "chTotals", "", "596,268,1164,288", 10 + set the tabStops of field "chXferHeader" to "26,300,400,540" + set the tabStops of field "chXfers" to "26,300,400,540" set the textFont of field "chXferHeader" to "Courier" set the textFont of field "chXfers" to "Courier" set the textSize of field "chXferHeader" to 10 set the textSize of field "chXfers" to 10 - -- 3b. Contents of the selected transfer (live per-file progress) + streaming - chMakeButton "chStream", "Stream Selected (in order)", "16,520,300,544" - chMakeLabel "chFilesCaption", "Click a transfer above to see its files. Stream downloads in order so media can play as it arrives.", "308,524,864,542", 9 - chMakeLabel "chFilesHeader", "File" & tab & "Size" & tab & "Progress", "16,548,864,566", 10 - chMakeList "chFiles", "16,568,864,662", true - set the tabStops of field "chFilesHeader" to "440,540" - set the tabStops of field "chFiles" to "440,540" + chMakeButton "chStream", "Stream Selected (play in order)", "596,294,820,318" + chMakeLabel "chFilesCaption", "Click a transfer to see its files. Stream plays a video/audio release here as it arrives.", "828,296,1164,318", 9 + chMakeLabel "chFilesHeader", "File" & tab & "Size" & tab & "Progress", "596,322,1164,340", 10 + chMakeList "chFiles", "596,342,1164,452", true + set the tabStops of field "chFilesHeader" to "340,440" + set the tabStops of field "chFiles" to "340,440" set the textFont of field "chFilesHeader" to "Courier" set the textFont of field "chFiles" to "Courier" set the textSize of field "chFilesHeader" to 10 set the textSize of field "chFiles" to 10 set the text of field "chFiles" to " Click a transfer above to see its files and per-file progress." - -- 4. Quick drop (immutable) - chSection "chSecDrop", "4. Quick drop - paste text, get a code anyone can fetch back", "16,672,864,692" - chMakeField "chPinText", "16,698,430,722", false - chMakeButton "chPin", "Pin", "438,698,510,722" - chMakeField "chPinCode", "518,698,790,722", true - chMakeButton "chPinCopy", "Copy", "794,698,864,722" - chMakeField "chFetchCode", "16,726,430,750", false - chMakeButton "chFetchBtn", "Fetch", "438,726,510,750" - chMakeField "chDropResult", "518,726,864,750", true - -- Log - chMakeList "chLog", "16,758,864,888", true + -- ===== FULL-WIDTH BOTTOM: quick drop + log ===== + chSection "chSecDrop", "4. Quick drop - paste text, get a code anyone can fetch back", "16,488,1164,508" + chMakeField "chPinText", "16,512,440,534", false + chMakeButton "chPin", "Pin", "448,512,530,534" + chMakeField "chPinCode", "538,512,810,534", true + chMakeButton "chPinCopy", "Copy", "818,512,910,534" + chMakeField "chFetchCode", "16,538,440,560", false + chMakeButton "chFetchBtn", "Fetch", "448,538,530,560" + chMakeField "chDropResult", "538,538,910,560", true + chMakeList "chLog", "16,564,1164,640", true set the textFont of field "chMyKey" to "Courier" set the textFont of field "chFollowKey" to "Courier" set the textFont of field "chPinCode" to "Courier" set the textFont of field "chFetchCode" to "Courier" set the textSize of field "chMyKey" to 9 set the textSize of field "chFollowKey" to 9 + -- ===== streaming overlay: a native player shown ONLY while streaming ===== + -- Built last so it sits on TOP of the right column; hidden until Stream is used. + -- When shown it covers the transfers/files pane; Close returns to that view. + chMakeLabel "chStreamBar", "", "596,62,1080,104", 11 + set the opaque of field "chStreamBar" to true + set the backgroundColor of field "chStreamBar" to "20,24,32" + set the foregroundColor of field "chStreamBar" to "255,255,255" + set the textStyle of field "chStreamBar" to "bold" + set the visible of field "chStreamBar" to false + chMakeButton "chCloseStream", "Close", "1086,62,1164,104" + set the visible of button "chCloseStream" to false + -- the player object is optional: some OXT builds may not ship it, so build it + -- defensively and remember whether it worked (sHasPlayer gates every player op) + put false into sHasPlayer + try + if there is no player "chPlayer" then + create player + set the name of the last player to "chPlayer" + end if + set the rect of player "chPlayer" to "596,108,1164,452" + set the showController of player "chPlayer" to true + set the visible of player "chPlayer" to false + put true into sHasPlayer + catch tErr + put false into sHasPlayer + end try chAddTips unlock screen end chBuild @@ -275,7 +305,8 @@ command chAddTips set the tooltip of button "chCopyMag" to "Copy the selected file's magnet link to the clipboard." set the tooltip of button "chUnfollow" to "Stop following the selected file's channel." set the tooltip of field "chXfers" to "Click a transfer to see its files and per-file progress below." - set the tooltip of button "chStream" to "Download the selected transfer in order, so media plays as it arrives." + set the tooltip of button "chStream" to "Play the selected transfer's video/audio here as it downloads in order." + set the tooltip of button "chCloseStream" to "Stop the player and return to the transfer view." set the tooltip of field "chFiles" to "Files inside the selected transfer, with live per-file progress." set the tooltip of field "chPinText" to "Type any short text (up to 1000 bytes) to pin to the network." set the tooltip of button "chPin" to "Store the text on the DHT and get back a short share code." @@ -297,7 +328,8 @@ command chHelp put tMsg & " one and Download; it transfers from them while they are online. Every entry is signed," & return into tMsg put tMsg & " so it is tamper-proof." & return & return into tMsg put tMsg & "3) TRANSFERS - a live view of what you are sending and receiving. Click one to see" & return into tMsg - put tMsg & " the files inside it with per-file progress, or Stream it to download in order." & return & return into tMsg + put tMsg & " the files inside it with per-file progress. Stream a video or audio release and it" & return into tMsg + put tMsg & " plays right here in a built-in player while it downloads in order." & return & return into tMsg put tMsg & "4) QUICK DROP - paste any text, get a short code; anyone with the code gets it back." & return & return into tMsg put tMsg & "TRY IT on two machines: Copy your card on one, Follow it on the other, then share a file." into tMsg answer tMsg with "Got it" @@ -878,23 +910,129 @@ command chShowFilesForSelection chShowFiles chSelectedXferHandle() end chShowFilesForSelection --- Turn on sequential (in-order) download for the selected transfer, so a media --- release can play as it arrives instead of downloading pieces at random. +-- Stream the selected transfer: turn on sequential (in-order) download so the +-- file fills from the front, then point the built-in player at the largest +-- video/audio file inside it and play. The player overlays the transfer pane. command chStreamSelected - local tH, tR + local tH, tR, tRel, tKind, tStatus, tSave, tPath, tName put chSelectedXferHandle() into tH if tH is 0 then chLog "Select a transfer first, then click Stream." exit chStreamSelected end if + -- in-order download so a media file is watchable/listenable as it arrives put btSetSequentialDownload(tH, true) into tR - if tR is 0 then - chLog "Streaming on: transfer" && tH && "now downloads in order (play-as-it-arrives)." - else + if tR is not 0 then chLog "Could not enable streaming:" && btLastError() + exit chStreamSelected + end if + -- find the main video/audio file (those are the only two we stream) + put chMainMediaFile(tH) into tRel + if tRel is "" then + chLog "Streaming on (in order). No video or audio file in this transfer to play." + exit chStreamSelected end if + put chMediaKind(tRel) into tKind + if not sHasPlayer then + chLog "Streaming on (in order):" && tRel & ". (No player in this build - open it in your media app.)" + exit chStreamSelected + end if + -- build the on-disk path: the torrent's save path + the file's relative path + put btTorrentStatus(tH) into tStatus + put tStatus["savePath"] into tSave + if tSave is empty then + chLog "Streaming on (in order). Waiting for a download location to appear..." + exit chStreamSelected + end if + put tSave & "/" & tRel into tPath + replace "\" with "/" in tPath + if there is no file tPath then + chLog "Streaming in order - '" & tRel & "' is not on disk yet. Click Stream again in a moment to play." + exit chStreamSelected + end if + put chTrunc(tRel, 56) into tName + chOpenPlayer tPath, tName, tKind end chStreamSelected +-- Point the player at a file, reveal the overlay, and start playback. Wrapped +-- defensively because player capabilities vary across OXT builds/platforms. +command chOpenPlayer pFullPath, pTitle, pKind + local tErr + if not sHasPlayer then + exit chOpenPlayer + end if + try + set the filename of player "chPlayer" to pFullPath + put " Streaming (" & pKind & "): " & pTitle into field "chStreamBar" + set the visible of field "chStreamBar" to true + set the visible of button "chCloseStream" to true + set the visible of player "chPlayer" to true + set the showController of player "chPlayer" to true + start player "chPlayer" + chLog "Playing '" & pTitle & "' (" & pKind & ") - it streams as the download fills in order." + catch tErr + chLog "Could not open the player:" && tErr + end try +end chOpenPlayer + +-- Stop playback and hide the overlay, returning to the transfer/files view. +command chCloseStream + local tErr + if not sHasPlayer then + exit chCloseStream + end if + try + stop player "chPlayer" + set the filename of player "chPlayer" to empty + catch tErr + end try + set the visible of player "chPlayer" to false + set the visible of field "chStreamBar" to false + set the visible of button "chCloseStream" to false +end chCloseStream + +-- Classify a path as "video", "audio", or "" by its file extension. Video and +-- audio are the only kinds the player streams. +function chMediaKind pPath + local tExt, tVideo, tAudio + set the itemDelimiter to "." + if the number of items of pPath < 2 then + return "" + end if + put toLower(the last item of pPath) into tExt + put "mp4,m4v,mov,mkv,avi,webm,mpg,mpeg,wmv,flv,3gp,ogv" into tVideo + put "mp3,m4a,aac,flac,wav,ogg,oga,opus,wma,aiff,aif" into tAudio + set the itemDelimiter to comma + if tExt is among the items of tVideo then + return "video" + end if + if tExt is among the items of tAudio then + return "audio" + end if + return "" +end chMediaKind + +-- The relative path of the LARGEST video/audio file in a torrent (a release is +-- usually one media file plus small extras), or "" if it holds no media. +function chMainMediaFile pHandle + local tFiles, tFile, tBest, tBestSize + if pHandle is 0 or pHandle is empty then + return "" + end if + put btFileList(pHandle) into tFiles + put "" into tBest + put -1 into tBestSize + repeat for each element tFile in tFiles + if chMediaKind(tFile["path"]) is not "" then + if tFile["size"] > tBestSize then + put tFile["size"] into tBestSize + put tFile["path"] into tBest + end if + end if + end repeat + return tBest +end chMainMediaFile + -- a 10-segment ASCII progress bar, e.g. [####------] 42% (monospaced fields) function chProgressBar pPct local tFilled From 6a458e41170df75c554d31cb93b0c07943eea77c Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 27 Jun 2026 23:06:29 +0000 Subject: [PATCH 4/5] fix: tExt spells the reserved token `text`; add a static guard for the class `tExt` in chMediaKind (intended t + "Ext" for file extension) is literally t-e-x-t = `text`, so xTalk evaluates it as the `text` keyword, not a variable - it compiles and silently misbehaves. Renamed to `tSuffix`. This slipped past the checker, so close the gap durably: - check-livecodescript.py gains rule 6: any t/p/s/k-prefixed CamelCase name whose FULL lowercased spelling is a reserved token (text, the, time, title, target, send, set, start, stop, ...). Only reserved words that begin with a prefix letter can collide, so that is the whole carried set; the `[tpsk][A-Z]` shape guard means a normally-written lowercase keyword never matches - only an accidentally-keyword-spelling identifier does, and that is always a real bug. Applies to both .lcb and .livecodescript. - CLAUDE.md gotcha 2 records the lesson with the tExt example. Verified: checker flags `tExt` and leaves `tName`/`tSuffix` alone; all 5 tracked files pass. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01371AXB4CUUke7enHHS8okc --- CLAUDE.md | 7 ++- examples/torrent-dht-channels.livecodescript | 8 ++-- tools/check-livecodescript.py | 48 ++++++++++++++++++++ 3 files changed, 58 insertions(+), 5 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 1457ef2..365f9f0 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -151,7 +151,12 @@ pass" and let the user confirm. string — they fail OXT compilation. ASCII `"` and `'` only. The static checker enforces zero. 2. **Avoid names whose stem shadows an engine token** even when prefixed; prefer - distinctive, multi-word stems. + distinctive, multi-word stems. The nastiest case is a prefixed name whose + *full spelling* IS a reserved token: `tExt` (t + "Ext" for extension) is + literally `t-e-x-t` = `text`, so xTalk evaluates it as the `text` keyword, not + a variable — it compiles and silently misbehaves. `tools/check-livecodescript.py` + now flags this class (any `t/p/s/k`-prefixed name that lowercases to a reserved + word); use a different stem (e.g. `tSuffix`). 3. **Prefix conventions:** `t` handler-local, `p` parameter, `s` script/module-local, `k` constant. Public API `btPascalCase`; C ABI `btx_snake_case`. 4. **Constants must be literal** and declared **before first use** (OXT resolves them by diff --git a/examples/torrent-dht-channels.livecodescript b/examples/torrent-dht-channels.livecodescript index e0544b2..50308d2 100644 --- a/examples/torrent-dht-channels.livecodescript +++ b/examples/torrent-dht-channels.livecodescript @@ -994,19 +994,19 @@ end chCloseStream -- Classify a path as "video", "audio", or "" by its file extension. Video and -- audio are the only kinds the player streams. function chMediaKind pPath - local tExt, tVideo, tAudio + local tSuffix, tVideo, tAudio set the itemDelimiter to "." if the number of items of pPath < 2 then return "" end if - put toLower(the last item of pPath) into tExt + put toLower(the last item of pPath) into tSuffix put "mp4,m4v,mov,mkv,avi,webm,mpg,mpeg,wmv,flv,3gp,ogv" into tVideo put "mp3,m4a,aac,flac,wav,ogg,oga,opus,wma,aiff,aif" into tAudio set the itemDelimiter to comma - if tExt is among the items of tVideo then + if tSuffix is among the items of tVideo then return "video" end if - if tExt is among the items of tAudio then + if tSuffix is among the items of tAudio then return "audio" end if return "" diff --git a/tools/check-livecodescript.py b/tools/check-livecodescript.py index 5c4d765..1217e72 100644 --- a/tools/check-livecodescript.py +++ b/tools/check-livecodescript.py @@ -14,6 +14,8 @@ must be wrapped. 5. Constants declared before first use (.lcb) - OXT resolves constants by lexical position; a forward reference silently evaluates to nothing. + 6. No prefixed name that spells a reserved token (both dialects) - e.g. `tExt` + is t-e-x-t = `text`, which xTalk evaluates as the keyword, not a variable. It is a lexer-level checker, NOT a compiler: it neutralizes comments and strings and reasons about block keywords. It errs toward NOT raising false positives; @@ -371,6 +373,50 @@ def check_lcb_lowercase_names(path, cleaned): return problems +# A prefixed CamelCase name (t/p/s/k + UpperCamel, the project convention) whose +# FULL lowercased spelling IS a reserved xTalk token. The classic trap: `tExt` +# (intended as t + "Ext" for extension) spells t-e-x-t = `text`, so xTalk +# evaluates it as the `text` keyword, not a variable - a silent, hair-pulling +# bug that compiles. Only reserved words that BEGIN with a prefix letter +# (t/p/s/k) can ever collide, so that is the entire set we need to carry. The +# shape guard `[tpsk][A-Z]` means a normally-written lowercase keyword (`text`, +# `the`, `put`) never matches - only an accidentally-keyword-spelling identifier +# does, and such a name is always a real bug (the engine token wins every time). +PREFIXED_TOKEN_SHADOWS = { + # t- + "the", "then", "this", "there", "to", "text", "time", "title", "top", + "tab", "tan", "true", "target", + # p- + "pi", "pass", "put", "params", "print", + # s- + "send", "set", "sin", "sqrt", "sort", "space", "start", "stop", "stack", + "script", "selection", + # k- + "keys", +} + + +def check_prefixed_token_shadows(path, cleaned): + """Flag a t/p/s/k-prefixed name that spells a reserved token (e.g. tExt -> + `text`). Applies to both .lcb and .livecodescript - the shadowing is an + xTalk evaluation rule, not a dialect quirk.""" + problems = [] + seen = set() + pat = re.compile(r"\b([tpsk][A-Z][A-Za-z0-9_]*)\b") + for lineno, line in cleaned: + for m in pat.finditer(line): + name = m.group(1) + if name in seen: + continue + if name.lower() in PREFIXED_TOKEN_SHADOWS: + seen.add(name) + problems.append(Problem(path, lineno, + "name `%s` spells the reserved token `%s` - xTalk evaluates it " + "as that keyword, not a variable; rename it with a distinctive, " + "multi-word stem (e.g. tExt -> tSuffix)" % (name, name.lower()))) + return problems + + def check_file(path): with open(path, "rb") as f: raw = f.read() @@ -395,6 +441,8 @@ def check_file(path): problems += check_lcb_lowercase_names(path, cleaned) else: problems += check_livecodescript_blocks(path, cleaned) + # universal xTalk rule (both dialects): no name that spells a reserved token + problems += check_prefixed_token_shadows(path, cleaned) return problems From 5624d3663168e6b94989c5ef0fa08a5a9eb81121 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 27 Jun 2026 23:58:04 +0000 Subject: [PATCH 5/5] fix(dht): sign the BENCODED mutable value, not the raw bytes (BEP44) This is THE bug that made channel feeds silently fail end to end: a follower never saw a publisher's releases, and a publisher's feed never reached anyone - in both directions, for everyone. btx_dht_put_mutable's signing callback set the entry `e` to the value but then called sign_mutable_item over the RAW value bytes. BEP44 signs the BENCODED form: a string value "hi" bencodes to "2:hi", and every storing DHT node and every getter's libtorrent verifies the signature against that bencoded form before accepting/surfacing the item. Signing the raw bytes produced a signature that verifies against nothing, so: - storing nodes rejected the put (invalid signature), and - even if stored, a getter's libtorrent drops the item before emitting dht_mutable_item_alert, so the dhtMutableItem event never fired and no feed was ever exchanged. The put call still returned BTX_OK (it only confirms the request wired up; the signing runs later on the network thread), which is why nothing looked wrong. Mirrors libtorrent's own examples/dht_put.cpp: bencode e, then sign. The GET side (entry_to_bytes) already returns string entries verbatim, so the round-trip is clean once signing is fixed. Guard so it cannot regress silently: - Factor the signing into one helper (sign_mutable_string_entry) used by both the production put callback and a new test hook, so the test exercises the real path. - New btx::test::dht_mutable_sign_verifies signs through that helper and checks the signature with ed25519_verify against the reconstructed BEP44 canonical message (verify_mutable_item itself is TORRENT_EXTRA_EXPORT, not in the shared lib). torrent_smoke_test asserts it verifies for empty and non-empty salt. - Confirmed the guard catches the bug: reintroducing the raw-value sign makes exactly those 3 checks fail; with the fix, 552 checks / 0 failures under gcc ASan+UBSan. Internal change only - no btx_* ABI symbol, record field, or LCB-visible behavior changed, so no ABI bump. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01371AXB4CUUke7enHHS8okc --- src/torrent_shim.cpp | 71 ++++++++++++++++++++++++++++++++++-- src/torrent_shim.h | 13 +++++++ tests/torrent_smoke_test.cpp | 13 +++++++ 3 files changed, 94 insertions(+), 3 deletions(-) diff --git a/src/torrent_shim.cpp b/src/torrent_shim.cpp index 80475a3..c615c2a 100644 --- a/src/torrent_shim.cpp +++ b/src/torrent_shim.cpp @@ -2121,6 +2121,27 @@ extern "C" BTX_API int BTX_CALL btx_dht_get_immutable(int s, const char *targetH }); } +/* Sign a mutable-item STRING value the BEP44 way, setting the entry `e` that + * libtorrent will store. THE SIGNATURE MUST COVER THE BENCODED VALUE (a string + * "hi" bencodes to "2:hi"), because that is exactly what every storing node and + * every getter verifies it against — signing the raw bytes yields a signature + * that verifies against nothing, so nodes reject the store and getters never + * surface the item, and a whole signed channel feed silently disappears. + * Mirrors libtorrent's own examples/dht_put.cpp. `out_signed` receives the + * bencoded buffer that was signed so a caller (the smoke test) can re-verify + * with verify_mutable_item; the production put callback and that test hook both + * route through here, so the test exercises the REAL signing path. */ +static lt::dht::signature sign_mutable_string_entry( + lt::entry &e, const std::vector &val, const std::string &salt, + std::int64_t seq, const lt::dht::public_key &pk, + const lt::dht::secret_key &sk, std::vector &out_signed) { + e = lt::entry(std::string(val.begin(), val.end())); + out_signed.clear(); + lt::bencode(std::back_inserter(out_signed), e); + return lt::dht::sign_mutable_item(out_signed, salt, + lt::dht::sequence_number(seq), pk, sk); +} + extern "C" BTX_API int BTX_CALL btx_dht_put_mutable(int s, const char *publicKeyHex, const char *secretKeyHex, const char *salt, @@ -2143,10 +2164,10 @@ extern "C" BTX_API int BTX_CALL btx_dht_put_mutable(int s, const char *publicKey st->ses->dht_put_item(pk.bytes, [val, salt_s, pk, sk](lt::entry &e, std::array &sig, std::int64_t &seq, std::string const &) { - e = lt::entry(std::string(val.begin(), val.end())); seq = seq + 1; /* monotonic: bump past the current value */ - lt::dht::signature sg = lt::dht::sign_mutable_item( - val, salt_s, lt::dht::sequence_number(seq), pk, sk); + std::vector signed_buf; /* the bencoded value we signed */ + lt::dht::signature sg = sign_mutable_string_entry( + e, val, salt_s, seq, pk, sk, signed_buf); sig = sg.bytes; }, salt_s); return BTX_OK; /* confirmation -> A_DHT_PUT alert */ @@ -2281,5 +2302,49 @@ int live_session_count(void) { return static_cast(g_sessions.live_count()); } +int dht_mutable_sign_verifies(const char *publicKeyHex, const char *secretKeyHex, + const char *salt, const void *data, int len) { + /* Sign a mutable value through the SAME helper the production put uses, then + * check it with libtorrent's own verify_mutable_item — the exact gate a + * follower's libtorrent applies before surfacing the item. A pass proves the + * BEP44 signing contract holds (sign the bencoded value); a regression here + * is the silent "feeds never arrive" failure. Wrapped in the firewall like + * every entry, so a throw becomes a negative code, never a CHECK crash. */ + BTX_GUARD_ACTION({ + lt::dht::public_key pk; + lt::dht::secret_key sk; + if (!hex_to_buf(publicKeyHex, pk.bytes.data(), 32)) return 0; + if (!hex_to_buf(secretKeyHex, sk.bytes.data(), 64)) return 0; + std::string salt_s = salt ? salt : ""; + const char *p = static_cast(data); + std::vector val(p, p + (len < 0 ? 0 : len)); + lt::entry e; + std::vector signed_buf; /* the bencoded value v that was signed */ + const std::int64_t seq = 1; + lt::dht::signature sg = + sign_mutable_string_entry(e, val, salt_s, seq, pk, sk, signed_buf); + /* Reconstruct the BEP44 canonical signed message exactly as a remote + * verifier does — [4:salt:] 3:seqie 1:v — + * and check the signature with ed25519_verify, the same primitive a + * follower's libtorrent applies. (verify_mutable_item itself is + * TORRENT_EXTRA_EXPORT, not in the shared lib, so we use the public + * ed25519 verify against the canonical string instead.) If the value was + * signed bencoded (correct) this passes; if signed raw (the bug) it + * fails — so this assertion is the regression guard for that silent bug. */ + std::string canon; + if (!salt_s.empty()) { + canon += "4:salt"; + canon += std::to_string(salt_s.size()); + canon += ":"; + canon += salt_s; + } + canon += "3:seqi"; + canon += std::to_string(seq); + canon += "e1:v"; + canon.append(signed_buf.begin(), signed_buf.end()); + return lt::dht::ed25519_verify(sg, canon, pk) ? 1 : 0; + }); +} + } // namespace test } // namespace btx diff --git a/src/torrent_shim.h b/src/torrent_shim.h index 5e68a51..de55833 100644 --- a/src/torrent_shim.h +++ b/src/torrent_shim.h @@ -65,6 +65,19 @@ BTX_API int force_throw(void); * visibility reason as force_throw above. */ BTX_API int live_session_count(void); +/* Sign a mutable DHT value through the production signing helper and verify it + * with libtorrent's verify_mutable_item; returns 1 if the signature verifies, 0 + * if not (negative on an internal throw). Guards the BEP44 signing contract - + * the value must be signed in its BENCODED form - which is otherwise unreachable + * through the public ABI because real signing only runs in a network-thread + * callback once a live DHT finds a home for the blob. A regression here is the + * silent "channel feeds never arrive" failure. Exported (BTX_API) for the same + * visibility reason as the hooks above. */ +BTX_API int dht_mutable_sign_verifies(const char *publicKeyHex, + const char *secretKeyHex, + const char *salt, + const void *data, int len); + } // namespace test } // namespace btx diff --git a/tests/torrent_smoke_test.cpp b/tests/torrent_smoke_test.cpp index 7d7d18a..1afec87 100644 --- a/tests/torrent_smoke_test.cpp +++ b/tests/torrent_smoke_test.cpp @@ -545,6 +545,19 @@ static void test_dht_bep44() { CHECK(btx_dht_get_mutable(s, pubHex.c_str(), "myapp") == BTX_OK); CHECK(btx_dht_get_mutable(s, pubHex.c_str(), "") == BTX_OK); /* empty salt */ + /* The BEP44 SIGNING CONTRACT (the bug that made every channel feed silently + * fail): a mutable value must be signed over its BENCODED form, the way a + * follower's libtorrent verifies it before surfacing the item. btx_dht_put_* + * only returns OK that the call wired up - the signing itself runs later in a + * network-thread callback - so this hook signs through the SAME helper and + * checks it with libtorrent's own verify_mutable_item. A real feed string, + * with empty and non-empty salt, must both verify. */ + const char *feed = "name=Alice\nr=Demo Release\tmagnet:?xt=urn:btih:0123456789abcdef"; + const int feedlen = static_cast(std::strlen(feed)); + CHECK(btx::test::dht_mutable_sign_verifies(pubHex.c_str(), secHex.c_str(), "", feed, feedlen) == 1); + CHECK(btx::test::dht_mutable_sign_verifies(pubHex.c_str(), secHex.c_str(), "myapp", feed, feedlen) == 1); + CHECK(btx::test::dht_mutable_sign_verifies(pubHex.c_str(), secHex.c_str(), "", val, vlen) == 1); + /* bad hex / wrong key length / oversize value -> clean BTX_ERR_INVALID_ARG. */ CHECK(btx_dht_get_immutable(s, "not-hex") == BTX_ERR_INVALID_ARG); CHECK(btx_dht_get_mutable(s, "short", "") == BTX_ERR_INVALID_ARG);