0.0.002
ddat interop with dat js cable interop with cabal js ddat's frontend = dat-thang cable fe = cable-thang dillo support based on cel's dillo-dat bootstrap node xplatform support Signed-off-by: divo <418@cryspace.space>
This commit is contained in:
1 parent
f0f91540be
commit
b735900f2e
323 files changed
+134347
-343
No files matched your search
Vendored
+13
@@ -0,0 +1,13 @@
|
||||
# Test fixtures
|
||||
|
||||
Keep reusable test input here, outside the disposable `build/` tree.
|
||||
|
||||
- `dat-history/` preserves archive snapshots used during version-bar work.
|
||||
- `dillo-cache/archives/` preserves the historical Dillo cache used for
|
||||
Dat/Dillo compatibility checks. Its legacy `dat.json` is test input; new
|
||||
archives from this project use `dat.scm`.
|
||||
- `dillo-dpip/cold-input` is a canned DPIP request sequence.
|
||||
- `windows/hello.c` is the tiny MSYS2 compiler smoke source.
|
||||
|
||||
Per-machine keys, bootstrap identity, and test run output belong under the
|
||||
ignored `local-data/` directory and are not source-distribution contents.
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1 @@
|
||||
’ڱ‡á«ĚŐ.␍(ÓkyČ<zÜcŁéaA7@Âî8
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,11 @@
|
||||
|
||||
|
||||
hyperdrive ’Œ±‡á«ÌÕ.␍(ÓkyÈ<zÜc£éaA7@Âî8
|
||||
dat.jsonÿƒ [(·™Àœ‘408
|
||||
|
||||
index.htmlÿƒ ¾ (┿œ‘408
|
||||
|
||||
index.htmlÿƒ ì(©ÖÈœ‘408
|
||||
dat-logo.pngÿƒ ÙÂ(Ûä꜑408
|
||||
|
||||
index.htmlÿƒ ø(‡³þœ‘408
|
||||
@@ -0,0 +1 @@
|
||||
�ыr␍*T�ю/*и]8�{Utр�в@ќаУG}Г2 ўЂ
|
||||
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Whitespace-only changes.
@@ -0,0 +1 @@
|
||||
4.–^К Ёb»PМПrь{…ґ=®‰иf[еrФ-C
|
||||
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
@@ -0,0 +1,3 @@
|
||||
|
||||
|
||||
hyperdrive 4.某ハ�ィbサPフマr��=ョ芽f[蚌ヤ-C
|
||||
@@ -0,0 +1 @@
|
||||
�P����j#���ZL�������l�NF3�1N
|
||||
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Whitespace-only changes.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
@@ -0,0 +1 @@
|
||||
3Ìp=õ{“³©xs–ó«Hb4>a9u]HåŠÜ±
|
||||
BIN
Binary file not shown.
Binary file not shown.
Whitespace-only changes.
BIN
Binary file not shown.
Whitespace-only changes.
@@ -0,0 +1,2 @@
|
||||
þ¥w€� ŒÏ…vZÍ?kDá/j
|
||||
»¤=än÷Å�%½
|
||||
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
@@ -0,0 +1,4 @@
|
||||
|
||||
|
||||
hyperdrive þ¥w€� ŒÏ…vZÍ?kDá/j
|
||||
»¤=än÷Å�%½
|
||||
@@ -0,0 +1 @@
|
||||
�G��3�*Uf�'����������~~�U
|
||||
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Whitespace-only changes.
@@ -0,0 +1 @@
|
||||
Fpº£B„7¼[a5¬‚ñ´Þ°šG�“õtÿrå(Má
|
||||
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
@@ -0,0 +1,3 @@
|
||||
|
||||
|
||||
hyperdrive Fpº£B„7¼[a5¬‚ñ´Þ°šG�“õtÿrå(Má
|
||||
@@ -0,0 +1 @@
|
||||
7pèI¤¤óäùVCÙ½ö¶ÂjVb©ëòMŽO„
|
||||
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
+1
@@ -0,0 +1 @@
|
||||
’ڱ‡á«ĚŐ.␍(ÓkyČ<zÜcŁéaA7@Âî8
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
+11
@@ -0,0 +1,11 @@
|
||||
|
||||
|
||||
hyperdrive ’Œ±‡á«ÌÕ.␍(ÓkyÈ<zÜc£éaA7@Âî8
|
||||
dat.jsonÿƒ [(·™Àœ‘408
|
||||
|
||||
index.htmlÿƒ ¾ (┿œ‘408
|
||||
|
||||
index.htmlÿƒ ì(©ÖÈœ‘408
|
||||
dat-logo.pngÿƒ ÙÂ(Ûä꜑408
|
||||
|
||||
index.htmlÿƒ ø(‡³þœ‘408
|
||||
+1
@@ -0,0 +1 @@
|
||||
�ыr␍*T�ю/*и]8�{Utр�в@ќаУG}Г2 ўЂ
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
|
After Width: | Height: | Size: 24 KiB |
+4
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"title": "Dillo setup test",
|
||||
"description": "Dillo Dat browsing and version check"
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Site is online</title>
|
||||
<style>
|
||||
:root { color-scheme: light; font: 16px/1.55 system-ui, sans-serif; color: #263238; background: #f3f5f6; }
|
||||
body { margin: 0; min-height: 100vh; display: grid; place-items: center; }
|
||||
main { box-sizing: border-box; width: min(42rem, calc(100% - 2rem)); padding: 2.25rem; border: 1px solid #d8dee2; border-top: 4px solid #438c54; border-radius: .4rem; background: #fff; box-shadow: 0 1rem 2.5rem #26323812; }
|
||||
.logo { display: block; width: 9rem; height: auto; margin: 0 auto 1.5rem; }
|
||||
h1 { margin: 0 0 .5rem; font-size: 1.55rem; font-weight: 600; }
|
||||
p { margin: .6rem 0; }
|
||||
.muted { color: #65727a; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<img class="logo" src="dat-logo.png" alt="dat:// logo">
|
||||
<h1>The site is online</h1>
|
||||
<p>This is the current version of the Dat Dillo test archive.</p>
|
||||
<p class="muted">The earlier “site is being set up” page remains available at its pinned archive version.</p>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
<cmd='auth' msg='abcdef1234567890' '>
|
||||
<cmd='open_url' url='dat://4a93f5824c7f4a39d485c84a4de0f563a33915954e7ee3f10ea3f6699f2a9ee8/' '>
|
||||
<cmd='DpiBye' '>
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
int main(void){return 0;}
|
||||
@@ -0,0 +1,24 @@
|
||||
#include <X11/Xlib.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
static void walk(Display *d, Window w, int depth) {
|
||||
Window root, parent, *children = NULL;
|
||||
unsigned int n = 0, i;
|
||||
char *name = NULL;
|
||||
if (XFetchName(d, w, &name) && name) {
|
||||
if (strstr(name, "Dillo") || strstr(name, "Site is online")) printf("%.*s%s\n", depth * 2, " ", name);
|
||||
XFree(name);
|
||||
}
|
||||
if (XQueryTree(d, w, &root, &parent, &children, &n)) {
|
||||
for (i = 0; i < n; i++) walk(d, children[i], depth + 1);
|
||||
if (children) XFree(children);
|
||||
}
|
||||
}
|
||||
int main(void) {
|
||||
Display *d = XOpenDisplay(NULL);
|
||||
if (!d) { fputs("XOpenDisplay failed\n", stderr); return 1; }
|
||||
walk(d, DefaultRootWindow(d), 0);
|
||||
XCloseDisplay(d);
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
# Dillo integration checks
|
||||
|
||||
These scripts were moved out of `build/` so cleaning or recreating compiler
|
||||
outputs cannot remove the test procedure.
|
||||
|
||||
- `cold-site-dpip.sh` invokes the local Linux DPI with a canned DPIP exchange
|
||||
and writes its response and diagnostics to ignored `local-data/`.
|
||||
- `real-site-dpip.sh` invokes an installed Dillo DPI against the seeded test
|
||||
archive. Set `DILLO_DPID_KEY_FILE` when the dpid key is outside
|
||||
`$HOME/.dillo/dpid_comm_keys`; set `DAT_DPI_BIN` if the DPI executable is at
|
||||
a different path. Its captured response goes to ignored `local-data/`.
|
||||
- `../../test_dpi.sh` is the automated socket-framing check run by `make test`.
|
||||
|
||||
These checks need a running Cesspool bootstrap and a peer seeding the requested
|
||||
archive to exercise a network fetch. Cached responses can be inspected under
|
||||
`local-data/test-runs/dillo/` after running them.
|
||||
@@ -0,0 +1,14 @@
|
||||
#!/bin/bash
|
||||
set -eu
|
||||
repo=$(CDPATH= cd -- "$(dirname -- "$0")/../../.." && pwd)
|
||||
cold="$repo/local-data/test-runs/dpi-cold-parity-20261007"
|
||||
output="$repo/local-data/test-runs/dillo"
|
||||
mkdir -p "$cold/.dillo" "$cold/.config/dat-gpt"
|
||||
mkdir -p "$output"
|
||||
printf '12345 abcdef1234567890\n' > "$cold/.dillo/dpid_comm_keys"
|
||||
printf '127.0.0.1:3282\n' > "$cold/.config/dat-gpt/bootstrap"
|
||||
{
|
||||
printf "<cmd='auth' msg='abcdef1234567890' '>\n"
|
||||
printf "%s\n" "<cmd='open_url' url='dat://4a93f5824c7f4a39d485c84a4de0f563a33915954e7ee3f10ea3f6699f2a9ee8/' '>"
|
||||
printf "%s\n" "<cmd='DpiBye' '>"
|
||||
} | HOME="$cold" "$repo/build/linux/dat.dpi" > "$output/dpi-cold-site.response" 2> "$output/dpi-cold-site.error"
|
||||
@@ -0,0 +1,15 @@
|
||||
#!/bin/bash
|
||||
set -eu
|
||||
repo=$(CDPATH= cd -- "$(dirname -- "$0")/../../.." && pwd)
|
||||
key_file=${DILLO_DPID_KEY_FILE:-"$HOME/.dillo/dpid_comm_keys"}
|
||||
dpi_bin=${DAT_DPI_BIN:-/usr/local/lib/dillo/dpi/dat/dat.dpi.exe}
|
||||
output="$repo/local-data/test-runs/dillo/real-site-dpi.response"
|
||||
mkdir -p "$(dirname -- "$output")"
|
||||
read -r _ secret _ < "$key_file"
|
||||
{
|
||||
printf "<cmd='auth' msg='%s' '>\n" "$secret"
|
||||
printf "%s\n" "<cmd='open_url' url='dat://4a93f5824c7f4a39d485c84a4de0f563a33915954e7ee3f10ea3f6699f2a9ee8/' '>"
|
||||
printf "%s\n" "<cmd='open_url' url='dat://4a93f5824c7f4a39d485c84a4de0f563a33915954e7ee3f10ea3f6699f2a9ee8/dat-logo.png' '>"
|
||||
printf "%s\n" "<cmd='DpiBye' '>"
|
||||
} | "$dpi_bin" > "$output"
|
||||
grep -E "start_send_page|Site is online|The site is online|Content-Type|Content-Length" "$output"
|
||||
@@ -0,0 +1,150 @@
|
||||
'use strict'
|
||||
|
||||
// Cabal's JS implementation is the reference side. Cable is exercised via
|
||||
// its CLI against the exact room key and admin URI generated by that side.
|
||||
// This deliberately does not call a Cable feed API: Cabal feed wire support
|
||||
// has not been connected to Cable yet.
|
||||
const assert = require('assert')
|
||||
const fs = require('fs')
|
||||
const os = require('os')
|
||||
const path = require('path')
|
||||
const { spawnSync } = require('child_process')
|
||||
const Cabal = require(process.env.CABAL_JS_MODULE || 'cabal-core')
|
||||
|
||||
function callback (fn, ...args) {
|
||||
return new Promise((resolve, reject) => fn(...args, (err, value) => err ? reject(err) : resolve(value)))
|
||||
}
|
||||
|
||||
function close (cabal) {
|
||||
return new Promise((resolve, reject) => cabal.close(err => err ? reject(err) : resolve()))
|
||||
}
|
||||
|
||||
function cable (binary, home, ...args) {
|
||||
let command = binary
|
||||
let commandArgs = args
|
||||
let env = { ...process.env, HOME: home }
|
||||
if (process.env.CABLE_RUNNER === 'wsl') {
|
||||
// Node and cabal-core may run on Windows while Cable was built in WSL.
|
||||
// Convert both the executable and per-test HOME to paths visible in WSL.
|
||||
const wslPath = value => {
|
||||
const match = /^([A-Za-z]):[\\/](.*)$/.exec(value)
|
||||
if (!match) return value.replace(/\\/g, '/')
|
||||
return `/mnt/${match[1].toLowerCase()}/${match[2].replace(/\\/g, '/')}`
|
||||
}
|
||||
command = process.env.WSL_BIN || 'wsl.exe'
|
||||
commandArgs = ['--exec', 'env', `HOME=${wslPath(home)}`, wslPath(binary), ...args]
|
||||
env = { ...process.env }
|
||||
}
|
||||
const result = spawnSync(command, commandArgs, { encoding: 'utf8', env })
|
||||
if (result.error) throw result.error
|
||||
if (result.status !== 0) throw new Error(`cable ${args.join(' ')} failed: ${result.stderr}`)
|
||||
return result.stdout.trim()
|
||||
}
|
||||
|
||||
function waitForMessage (cabal, expected, timeoutMs) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
cabal.messages.events.removeListener('message', onMessage)
|
||||
reject(new Error('Cabal JS replica did not receive chat/text record'))
|
||||
}, timeoutMs)
|
||||
function onMessage (message) {
|
||||
if (!message || !message.value || !message.value.content ||
|
||||
message.value.content.text !== expected) return
|
||||
clearTimeout(timeout)
|
||||
cabal.messages.events.removeListener('message', onMessage)
|
||||
resolve(message)
|
||||
}
|
||||
cabal.messages.events.on('message', onMessage)
|
||||
})
|
||||
}
|
||||
|
||||
async function main () {
|
||||
const root = fs.mkdtempSync(path.join(process.env.CABAL_TEST_DIR || os.tmpdir(), 'cabal-cable-'))
|
||||
const home = path.join(root, 'cable-home')
|
||||
const binary = path.resolve(process.env.CABLE_BIN || 'build/linux/cable')
|
||||
fs.mkdirSync(home)
|
||||
let source, replica, otherRoom, sourceStream, replicaStream
|
||||
try {
|
||||
const sourceStorage = path.join(root, 'js-source')
|
||||
source = Cabal(sourceStorage)
|
||||
await callback(source.ready.bind(source))
|
||||
const roomKey = String(source.key).replace(/^cabal:\/\//, '').toLowerCase()
|
||||
assert.match(roomKey, /^[0-9a-f]{64}$/, 'Cabal JS must create a 32-byte room key')
|
||||
|
||||
replica = Cabal(path.join(root, 'js-replica'), roomKey)
|
||||
await callback(replica.ready.bind(replica))
|
||||
const jsUserKey = await callback(source.getLocalKey.bind(source))
|
||||
assert.match(jsUserKey, /^[0-9a-f]{64}$/, 'Cabal JS local writer key must be 32 bytes')
|
||||
|
||||
const expectedText = 'hello from Cabal JS to its replicated room'
|
||||
const received = waitForMessage(replica, expectedText, 15000)
|
||||
sourceStream = source.replicate(true)
|
||||
replicaStream = replica.replicate(false)
|
||||
sourceStream.on('error', () => {})
|
||||
replicaStream.on('error', () => {})
|
||||
sourceStream.pipe(replicaStream).pipe(sourceStream)
|
||||
|
||||
await callback(source.publishNick.bind(source), 'cable-side-by-side-user')
|
||||
await callback(source.publish.bind(source), {
|
||||
type: 'chat/text',
|
||||
content: { channel: 'cable-interop', text: expectedText }
|
||||
})
|
||||
const replicated = await received
|
||||
assert.strictEqual(replicated.value.type, 'chat/text')
|
||||
assert.strictEqual(replicated.value.content.channel, 'cable-interop')
|
||||
|
||||
const nickRecord = await callback(source.getMessage.bind(source), { key: jsUserKey, seq: 0 })
|
||||
// cabal-core releases have returned either the Hypercore node wrapper or
|
||||
// its decoded value from getMessage; accept both documented representations.
|
||||
const nickValue = nickRecord.value || nickRecord
|
||||
assert.strictEqual(nickValue.type, 'about')
|
||||
assert.strictEqual(nickValue.content.name, 'cable-side-by-side-user')
|
||||
const replicatedNick = await callback(replica.getMessage.bind(replica), { key: jsUserKey, seq: 0 })
|
||||
const replicatedNickValue = replicatedNick.value || replicatedNick
|
||||
assert.strictEqual(replicatedNickValue.type, 'about')
|
||||
assert.strictEqual(replicatedNickValue.content.name, 'cable-side-by-side-user')
|
||||
|
||||
sourceStream.destroy()
|
||||
replicaStream.destroy()
|
||||
sourceStream = null
|
||||
replicaStream = null
|
||||
await close(source)
|
||||
source = Cabal(sourceStorage, roomKey)
|
||||
await callback(source.ready.bind(source))
|
||||
assert.strictEqual(await callback(source.getLocalKey.bind(source)), jsUserKey,
|
||||
'Cabal JS local user/feed address must persist for the same room')
|
||||
|
||||
const adminKey = 'a'.repeat(64)
|
||||
const roomAddress = `cabal://${roomKey}?admin=${adminKey}`
|
||||
assert.strictEqual(cable(binary, home, 'address', roomAddress),
|
||||
`cable://${roomKey}?admin=${adminKey}`)
|
||||
|
||||
const cableUser = cable(binary, home, 'whoami', roomAddress)
|
||||
assert.match(cableUser, /^cable:\/\/[0-9a-f]{64}$/)
|
||||
assert.notStrictEqual(cableUser.slice(8), roomKey, 'room key is not a user address')
|
||||
assert.notStrictEqual(cableUser.slice(8), adminKey, 'room admin key is not a user address')
|
||||
assert.notStrictEqual(cableUser.slice(8), jsUserKey,
|
||||
'Cabal JS writer and Cable CLI represent separate local users')
|
||||
assert.strictEqual(cable(binary, home, 'whoami', `cable://${roomKey}`), cableUser,
|
||||
'Cable user address must persist for the same room, regardless of URI scheme/admin option')
|
||||
|
||||
otherRoom = Cabal(path.join(root, 'js-other-room'))
|
||||
await callback(otherRoom.ready.bind(otherRoom))
|
||||
const otherRoomKey = String(otherRoom.key).replace(/^cabal:\/\//, '').toLowerCase()
|
||||
const otherJsUserKey = await callback(otherRoom.getLocalKey.bind(otherRoom))
|
||||
assert.notStrictEqual(otherJsUserKey, jsUserKey,
|
||||
'Cabal JS local writer keys must be independent across rooms')
|
||||
const otherCableUser = cable(binary, home, 'whoami', `cabal://${otherRoomKey}`)
|
||||
assert.notStrictEqual(otherCableUser, cableUser,
|
||||
'Cable user address must be room-scoped like Cabal local writer feeds')
|
||||
|
||||
process.stdout.write(`Cabal JS nick/feed replication + Cable room/user identity: PASS room=cabal://${roomKey} js-user=${jsUserKey} cable-user=${cableUser}\n`)
|
||||
} finally {
|
||||
for (const stream of [sourceStream, replicaStream]) if (stream) stream.destroy()
|
||||
for (const cabal of [source, replica, otherRoom]) if (cabal) await close(cabal)
|
||||
if (process.env.CABAL_TEST_KEEP === '1') process.stdout.write(`kept Cabal test state at ${root}\n`)
|
||||
else fs.rmSync(root, { recursive: true, force: true })
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(err => { console.error(err); process.exitCode = 1 })
|
||||
@@ -0,0 +1,162 @@
|
||||
'use strict'
|
||||
|
||||
// Exercise the APIs that cabal-cli 15.x uses, with two independent local
|
||||
// Cabal clients connected by the same live Hypercore replication streams.
|
||||
const assert = require('assert')
|
||||
const fs = require('fs')
|
||||
const os = require('os')
|
||||
const path = require('path')
|
||||
const EventEmitter = require('events')
|
||||
const Module = require('module')
|
||||
const Client = require(process.env.CABAL_CLIENT_MODULE || 'cabal-client')
|
||||
|
||||
function once (emitter, event, predicate = () => true, timeoutMs = 12000) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const timer = setTimeout(() => {
|
||||
emitter.removeListener(event, listener)
|
||||
reject(new Error(`timed out waiting for Cabal ${event}`))
|
||||
}, timeoutMs)
|
||||
function listener (value) {
|
||||
if (!predicate(value)) return
|
||||
clearTimeout(timer)
|
||||
emitter.removeListener(event, listener)
|
||||
resolve(value)
|
||||
}
|
||||
emitter.on(event, listener)
|
||||
})
|
||||
}
|
||||
|
||||
function close (client, details) {
|
||||
return new Promise((resolve, reject) => client.removeCabal(details.key, err => err ? reject(err) : resolve()))
|
||||
}
|
||||
|
||||
function waitForUserName (details, name, timeoutMs = 12000) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const deadline = Date.now() + timeoutMs
|
||||
const poll = () => {
|
||||
const user = Object.values(details.getUsers()).find(candidate => candidate.name === name)
|
||||
if (user) return resolve(user)
|
||||
if (Date.now() >= deadline) return reject(new Error(`${name} nickname did not replicate`))
|
||||
setTimeout(poll, 25)
|
||||
}
|
||||
poll()
|
||||
})
|
||||
}
|
||||
|
||||
function createCliScreen (client) {
|
||||
const clientPath = require.resolve(process.env.CABAL_CLIENT_MODULE || 'cabal-client')
|
||||
const cliDirectory = path.resolve(path.dirname(clientPath), '..', '..')
|
||||
const screenPath = path.join(cliDirectory, 'neat-screen.js')
|
||||
const originalLoad = Module._load
|
||||
function fakeNeatLog () {
|
||||
const input = new EventEmitter()
|
||||
input.cursor = 0
|
||||
input.rawLine = () => ''
|
||||
input.set = () => {}
|
||||
return {
|
||||
input,
|
||||
render: () => {},
|
||||
clear: () => {},
|
||||
use: plugin => {
|
||||
const state = {}
|
||||
plugin(state, new EventEmitter())
|
||||
}
|
||||
}
|
||||
}
|
||||
Module._load = function (request, parent, isMain) {
|
||||
if (request === 'neat-log') return fakeNeatLog
|
||||
return originalLoad.call(this, request, parent, isMain)
|
||||
}
|
||||
try {
|
||||
const NeatScreen = require(screenPath)
|
||||
return new NeatScreen({
|
||||
client,
|
||||
frontendConfig: { messageTimeformat: '%H:%M:%S', messageIndent: 'nick' }
|
||||
})
|
||||
} finally {
|
||||
Module._load = originalLoad
|
||||
}
|
||||
}
|
||||
|
||||
async function main () {
|
||||
const root = fs.mkdtempSync(path.join(process.env.CABAL_TEST_DIR || os.tmpdir(), 'cabal-cli-features-'))
|
||||
const sourceClient = new Client({ config: { dbdir: path.join(root, 'alice'), preferredPort: 0 } })
|
||||
const peerClient = new Client({ config: { dbdir: path.join(root, 'bob'), preferredPort: 0 } })
|
||||
let alice, bob, aliceStream, bobStream, screen
|
||||
try {
|
||||
alice = await sourceClient.createCabal()
|
||||
const roomKey = alice.key
|
||||
assert.match(roomKey, /^[0-9a-f]{64}$/i, 'create room returns a Cabal room key')
|
||||
|
||||
bob = await peerClient.addCabal(`cabal://${roomKey}`)
|
||||
aliceStream = alice.core.replicate(true)
|
||||
bobStream = bob.core.replicate(false)
|
||||
aliceStream.on('error', () => {})
|
||||
bobStream.on('error', () => {})
|
||||
aliceStream.pipe(bobStream).pipe(aliceStream)
|
||||
|
||||
const aliceNick = once(alice, 'publish-nick', update => update.name === 'alice')
|
||||
alice.processLine('/nick alice')
|
||||
await aliceNick
|
||||
const aliceChannel = once(alice, 'channel-join', update => update.channel === 'lobby')
|
||||
alice.processLine('/join lobby')
|
||||
await aliceChannel
|
||||
const secondChannel = once(alice, 'channel-join', update => update.channel === 'garden')
|
||||
alice.processLine('/join garden')
|
||||
await secondChannel
|
||||
assert(alice.getJoinedChannels().includes('lobby'))
|
||||
assert(alice.getJoinedChannels().includes('garden'))
|
||||
|
||||
screen = createCliScreen(sourceClient)
|
||||
const cliChannels = alice.getChannels({ includePM: true, onlyJoined: true })
|
||||
alice.focusChannel(cliChannels[0])
|
||||
screen.neat.input.emit('ctrl-n')
|
||||
assert.strictEqual(alice.getCurrentChannel(), cliChannels[1], 'the cabal-cli Ctrl+N handler advances channels')
|
||||
screen.neat.input.emit('ctrl-n')
|
||||
assert.strictEqual(alice.getCurrentChannel(), cliChannels[2], 'the cabal-cli Ctrl+N handler continues the cycle')
|
||||
alice.focusChannel('lobby')
|
||||
|
||||
const bobJoined = once(alice, 'channel-join', update => update.channel === 'lobby' && !update.isLocal)
|
||||
const bobChannel = once(bob, 'channel-join', update => update.channel === 'lobby')
|
||||
bob.processLine('/join lobby')
|
||||
await bobChannel
|
||||
await bobJoined
|
||||
const bobNick = once(bob, 'publish-nick', update => update.name === 'bob')
|
||||
bob.processLine('/nick bob')
|
||||
await bobNick
|
||||
await waitForUserName(alice, 'bob')
|
||||
|
||||
const fromAlice = 'hello from Alice in the lobby'
|
||||
const gotAlice = once(bob, 'new-message', update =>
|
||||
update.channel === 'lobby' && update.message.value.content.text === fromAlice)
|
||||
alice.processLine(fromAlice)
|
||||
await gotAlice
|
||||
|
||||
// Verify the actual room stream carries the published nickname record to Bob.
|
||||
await waitForUserName(bob, 'alice')
|
||||
|
||||
const fromBob = 'hello back from Bob'
|
||||
const gotBob = once(alice, 'new-message', update =>
|
||||
update.channel === 'lobby' && update.message.value.content.text === fromBob)
|
||||
bob.processLine(fromBob)
|
||||
await gotBob
|
||||
|
||||
aliceStream.destroy()
|
||||
bobStream.destroy()
|
||||
aliceStream = bobStream = null
|
||||
process.stdout.write(`cabal-cli 15.x room, /join, /nick, Ctrl+N channel cycle, and two-peer chat: PASS room=cabal://${roomKey}\n`)
|
||||
} finally {
|
||||
if (aliceStream) aliceStream.destroy()
|
||||
if (bobStream) bobStream.destroy()
|
||||
if (screen) {
|
||||
if (screen.updateTimer) clearTimeout(screen.updateTimer)
|
||||
if (alice && screen._updateHandler && screen._updateHandler[alice.key])
|
||||
alice.removeListener('update', screen._updateHandler[alice.key])
|
||||
}
|
||||
if (alice) await close(sourceClient, alice)
|
||||
if (bob) await close(peerClient, bob)
|
||||
fs.rmSync(root, { recursive: true, force: true })
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(err => { console.error(err); process.exitCode = 1 })
|
||||
@@ -0,0 +1,118 @@
|
||||
'use strict'
|
||||
|
||||
// Direct Cabal Core live discovery/chat check. The cross-process SDL/C to JS
|
||||
// integration is covered separately by cable_js_c_live.js.
|
||||
const crypto = require('crypto')
|
||||
const fs = require('fs')
|
||||
const os = require('os')
|
||||
const path = require('path')
|
||||
const dgram = require('dgram')
|
||||
const { startHyperswarm, loadCabalCore } = require('./cable_hyperswarm')
|
||||
|
||||
function freeUdpPort () {
|
||||
return new Promise((resolve, reject) => {
|
||||
const socket = dgram.createSocket('udp4')
|
||||
socket.once('error', reject)
|
||||
socket.bind(0, '127.0.0.1', () => {
|
||||
const port = socket.address().port
|
||||
socket.close(err => err ? reject(err) : resolve(port))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const { Cabal, requireCabal } = loadCabalCore()
|
||||
const DHT = requireCabal('hyperdht')
|
||||
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'cabal-core-hyperswarm-'))
|
||||
const roomKey = crypto.randomBytes(32).toString('hex')
|
||||
const clients = []
|
||||
let bootstrapper = null
|
||||
|
||||
function ready (cabal) { return new Promise((resolve, reject) => cabal.ready(err => err ? reject(err) : resolve())) }
|
||||
function localKey (cabal) { return new Promise((resolve, reject) => cabal.getLocalKey((err, key) => err ? reject(err) : resolve(key))) }
|
||||
function swarm (cabal) { return new Promise((resolve, reject) => cabal.swarm({ preferredPort: 0 }, err => err ? reject(err) : resolve())) }
|
||||
function publish (cabal, value) { return new Promise((resolve, reject) => cabal.publish(value, err => err ? reject(err) : resolve())) }
|
||||
function nick (cabal, value) { return new Promise((resolve, reject) => cabal.publishNick(value, err => err ? reject(err) : resolve())) }
|
||||
|
||||
function waitFor (predicate, description, timeout = 90000) {
|
||||
const started = Date.now()
|
||||
return new Promise((resolve, reject) => {
|
||||
const timer = setInterval(() => {
|
||||
if (Date.now() - started > timeout) { clearInterval(timer); reject(new Error('timeout: ' + description)); return }
|
||||
try { const result = predicate(); if (result) { clearInterval(timer); resolve(result) } } catch (err) { clearInterval(timer); reject(err) }
|
||||
}, 100)
|
||||
})
|
||||
}
|
||||
|
||||
async function main () {
|
||||
const port = await freeUdpPort()
|
||||
bootstrapper = DHT.bootstrapper(port, '127.0.0.1', { host: '127.0.0.1' })
|
||||
await bootstrapper.ready()
|
||||
let bootstrapRequests = 0
|
||||
bootstrapper.on('request', request => {
|
||||
bootstrapRequests++
|
||||
if (process.env.CABLE_TRACE_DHT) console.log('local DHT request command=' + request.command)
|
||||
})
|
||||
console.log('local Hyperswarm DHT bootstrap node listening at ' + bootstrapper.address().host + ':' + bootstrapper.address().port)
|
||||
process.env.CABLE_BOOTSTRAP = `127.0.0.1:${port}`
|
||||
process.env.CABLE_DHT_HOST = '127.0.0.1'
|
||||
const alice = Cabal(path.join(tempRoot, 'alice'), roomKey)
|
||||
const bob = Cabal(path.join(tempRoot, 'bob'), roomKey)
|
||||
clients.push(alice, bob)
|
||||
await Promise.all([ready(alice), ready(bob)])
|
||||
const [aliceKey, bobKey] = await Promise.all([localKey(alice), localKey(bob)])
|
||||
const [alicePort, bobPort] = await Promise.all([freeUdpPort(), freeUdpPort()])
|
||||
if (aliceKey === bobKey) throw new Error('Cabal Core reused a room feed identity across clients')
|
||||
const events = { alice: [], bob: [] }
|
||||
alice.on('peer-added', key => events.alice.push({ kind: 'peer', key }))
|
||||
bob.on('peer-added', key => events.bob.push({ kind: 'peer', key }))
|
||||
alice.kcore.api.messages.events.on('message', message => events.alice.push({ kind: 'message', message }))
|
||||
bob.kcore.api.messages.events.on('message', message => events.bob.push({ kind: 'message', message }))
|
||||
await startHyperswarm(alice, { host: '127.0.0.1', port: alicePort })
|
||||
await startHyperswarm(bob, { host: '127.0.0.1', port: bobPort })
|
||||
console.log('both Cabal Core clients joined the shared discovery topic')
|
||||
await Promise.all([alice._swarm.dht.fullyBootstrapped(), bob._swarm.dht.fullyBootstrapped()])
|
||||
console.log('both Cabal Core DHTs bootstrapped')
|
||||
await Promise.all([alice._swarm.flush(), bob._swarm.flush()])
|
||||
await Promise.all([alice._swarm.__cabalDiscovery.flushed(), bob._swarm.__cabalDiscovery.flushed()])
|
||||
console.log(`Hyperswarm connections queued: ${alice._swarm.peers.size} / ${bob._swarm.peers.size}`)
|
||||
await Promise.all([nick(alice, 'Alice'), nick(bob, 'Bob')])
|
||||
await Promise.all([
|
||||
publish(alice, { type: 'channel/join', content: { channel: 'default' } }),
|
||||
publish(bob, { type: 'channel/join', content: { channel: 'default' } })
|
||||
])
|
||||
setTimeout(() => console.log('DHT state', JSON.stringify({
|
||||
bootstrap: bootstrapper.toArray(),
|
||||
bootstrapRequests,
|
||||
bootstrapStats: bootstrapper.stats.requests,
|
||||
configuredBootstraps: [alice._swarm.dht.bootstrapNodes, bob._swarm.dht.bootstrapNodes],
|
||||
alice: alice._swarm.dht.toArray(), bob: bob._swarm.dht.toArray(),
|
||||
queries: [alice._swarm.dht.stats.queries, bob._swarm.dht.stats.queries],
|
||||
requests: [alice._swarm.dht.stats.requests, bob._swarm.dht.stats.requests],
|
||||
commands: [alice._swarm.dht.stats.commands, bob._swarm.dht.stats.commands],
|
||||
announcedPeers: [Array.from(alice._swarm.__cabalDiscovery.discovery._discovered),
|
||||
Array.from(bob._swarm.__cabalDiscovery.discovery._discovered)],
|
||||
aliceConnections: alice._swarm.peers.size, bobConnections: bob._swarm.peers.size,
|
||||
aliceConnecting: alice._swarm.connecting, bobConnecting: bob._swarm.connecting
|
||||
})), 10000).unref()
|
||||
await Promise.all([
|
||||
waitFor(() => events.alice.find(event => event.kind === 'peer' && event.key === bobKey), 'Alice discovers Bob over Hyperswarm', 20000),
|
||||
waitFor(() => events.bob.find(event => event.kind === 'peer' && event.key === aliceKey), 'Bob discovers Alice over Hyperswarm', 20000)
|
||||
])
|
||||
await publish(alice, { type: 'chat/text', content: { channel: 'default', text: 'JS-to-Cable bridge path A' } })
|
||||
await waitFor(() => events.bob.find(event => event.kind === 'message' && event.message.value.content.text === 'JS-to-Cable bridge path A'), 'Alice-to-Bob Cabal feed replication')
|
||||
await publish(bob, { type: 'chat/text', content: { channel: 'default', text: 'JS-to-Cable bridge path B' } })
|
||||
await waitFor(() => events.alice.find(event => event.kind === 'message' && event.message.value.content.text === 'JS-to-Cable bridge path B'), 'Bob-to-Alice Cabal feed replication')
|
||||
console.log('Cabal Core Hyperswarm discovery and bidirectional Hypercore chat: PASS')
|
||||
}
|
||||
|
||||
main().catch(err => { console.error(err.stack || err); process.exitCode = 1 }).finally(async () => {
|
||||
for (const cabal of clients) if (cabal._swarm && cabal._swarm.__markShutdown) cabal._swarm.__markShutdown()
|
||||
for (const cabal of clients) {
|
||||
if (cabal._swarm && cabal._swarm.__shutdown) { try { await cabal._swarm.__shutdown() } catch (_) {} }
|
||||
try { await cabal.close() } catch (_) {}
|
||||
}
|
||||
if (bootstrapper) { try { await bootstrapper.destroy() } catch (_) {} }
|
||||
const resolved = path.resolve(tempRoot)
|
||||
if (resolved.startsWith(path.resolve(os.tmpdir()) + path.sep) && path.basename(resolved).startsWith('cabal-core-hyperswarm-'))
|
||||
fs.rmSync(resolved, { recursive: true, force: true })
|
||||
})
|
||||
@@ -0,0 +1,33 @@
|
||||
'use strict'
|
||||
|
||||
// Run a public Cabal-compatible HyperDHT bootstrap node in the foreground.
|
||||
// Put this under a service manager and advertise the reachable IPv4 host/UDP port.
|
||||
const { loadCabalCore } = require('./cable_hyperswarm')
|
||||
|
||||
async function main () {
|
||||
const host = process.argv[2]
|
||||
const port = Number(process.argv[3])
|
||||
if (!host || !Number.isInteger(port) || port < 1 || port > 65535) {
|
||||
throw new Error('usage: cable bootstrap serve <public-ipv4-host> <udp-port>')
|
||||
}
|
||||
const { requireCabal } = loadCabalCore()
|
||||
const DHT = requireCabal('hyperdht')
|
||||
const node = DHT.bootstrapper(port, host)
|
||||
await node.ready()
|
||||
const address = node.address()
|
||||
process.stdout.write('Cabal Hyperswarm bootstrap serving ' + host + ':' + address.port +
|
||||
' (bound ' + address.host + ':' + address.port + ')\n')
|
||||
const close = async () => {
|
||||
try { await node.destroy() } finally { process.exit(0) }
|
||||
}
|
||||
process.once('SIGINT', close)
|
||||
process.once('SIGTERM', close)
|
||||
}
|
||||
|
||||
if (require.main === module) main().catch(err => {
|
||||
process.stderr.write((err && err.stack) || String(err))
|
||||
process.stderr.write('\n')
|
||||
process.exitCode = 1
|
||||
})
|
||||
|
||||
module.exports = { main }
|
||||
@@ -0,0 +1,78 @@
|
||||
'use strict'
|
||||
|
||||
// Live Cabal Core/Hyperswarm smoke test for the same S-expression IPC used by
|
||||
// cable-thang. Run with `node tests/interop/cabal/cable_cabal_hyperswarm_live.js`.
|
||||
const { spawn } = require('child_process')
|
||||
const crypto = require('crypto')
|
||||
const fs = require('fs')
|
||||
const os = require('os')
|
||||
const path = require('path')
|
||||
const readline = require('readline')
|
||||
|
||||
const root = path.resolve(__dirname, '../..')
|
||||
const bridgePath = path.join(__dirname, 'cable_hyperswarm.js')
|
||||
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'dat-gpt-cabal-live-'))
|
||||
const room = crypto.randomBytes(32).toString('hex')
|
||||
const peers = []
|
||||
|
||||
function startPeer (name) {
|
||||
const child = spawn(process.execPath, [bridgePath], { cwd: root, stdio: ['pipe', 'pipe', 'inherit'] })
|
||||
const peer = { name, child, lines: [], error: null, rl: readline.createInterface({ input: child.stdout }) }
|
||||
peer.rl.on('line', line => { peer.lines.push(line); process.stdout.write(name + ' ' + line + '\n') })
|
||||
child.on('error', err => { peer.error = err })
|
||||
child.on('exit', (code, signal) => { if (code && code !== 0) peer.error = new Error(`${name} exited ${code} (${signal || 'no signal'})`) })
|
||||
peers.push(peer)
|
||||
return peer
|
||||
}
|
||||
|
||||
function send (peer, expression) {
|
||||
if (!peer.child.stdin.write(expression + '\n')) throw new Error(`${peer.name} bridge stdin closed`)
|
||||
}
|
||||
|
||||
async function waitFor (peer, predicate, description, timeout = 90000) {
|
||||
const start = Date.now()
|
||||
while (Date.now() - start < timeout) {
|
||||
if (peer.error) throw peer.error
|
||||
const found = peer.lines.find(predicate)
|
||||
if (found) return found
|
||||
await new Promise(resolve => setTimeout(resolve, 100))
|
||||
}
|
||||
throw new Error(`timeout waiting for ${description}; ${peer.name} last output: ${peer.lines.slice(-5).join(' | ')}`)
|
||||
}
|
||||
|
||||
function startExpression (peer, nick) {
|
||||
const storage = path.join(tempRoot, peer.name)
|
||||
return `(start ${JSON.stringify(room)} ${JSON.stringify(storage)} ${JSON.stringify(nick)} "default")`
|
||||
}
|
||||
|
||||
async function main () {
|
||||
const alice = startPeer('alice')
|
||||
const bob = startPeer('bob')
|
||||
send(alice, startExpression(alice, 'Alice'))
|
||||
send(bob, startExpression(bob, 'Bob'))
|
||||
await Promise.all([waitFor(alice, line => line.startsWith('(event ready '), 'Alice Cabal feed'),
|
||||
waitFor(bob, line => line.startsWith('(event ready '), 'Bob Cabal feed')])
|
||||
await Promise.all([waitFor(alice, line => line.startsWith('(event peer-up '), 'Alice Hyperswarm peer discovery'),
|
||||
waitFor(bob, line => line.startsWith('(event peer-up '), 'Bob Hyperswarm peer discovery')])
|
||||
|
||||
send(alice, `(send "default" "Cable bridge says hello")`)
|
||||
await waitFor(bob, line => line.includes('Cable bridge says hello'), 'Alice-to-Bob Cabal message')
|
||||
send(bob, `(send "default" "Cabal bridge replies")`)
|
||||
await waitFor(alice, line => line.includes('Cabal bridge replies'), 'Bob-to-Alice Cabal message')
|
||||
console.log('Cabal Core Hyperswarm discovery and bidirectional chat: PASS')
|
||||
}
|
||||
|
||||
main().catch(err => { console.error(err.stack || err); process.exitCode = 1 }).finally(async () => {
|
||||
for (const peer of peers) {
|
||||
try { send(peer, '(close)') } catch (_) {}
|
||||
peer.child.stdin.end()
|
||||
}
|
||||
await Promise.all(peers.map(peer => new Promise(resolve => {
|
||||
if (peer.child.exitCode !== null) return resolve()
|
||||
const timer = setTimeout(() => { peer.child.kill('SIGTERM'); resolve() }, 5000)
|
||||
peer.child.once('exit', () => { clearTimeout(timer); resolve() })
|
||||
})))
|
||||
const resolvedRoot = path.resolve(tempRoot)
|
||||
if (resolvedRoot.startsWith(path.resolve(os.tmpdir()) + path.sep) && path.basename(resolvedRoot).startsWith('dat-gpt-cabal-live-'))
|
||||
fs.rmSync(resolvedRoot, { recursive: true, force: true })
|
||||
})
|
||||
@@ -0,0 +1,226 @@
|
||||
#ifdef _WIN32
|
||||
#include "cable_hyperswarm_win.inc"
|
||||
#else
|
||||
#define _POSIX_C_SOURCE 200809L
|
||||
#include "cable_hyperswarm.h"
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <signal.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/wait.h>
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
#include <sodium.h>
|
||||
#include "cable_media.h"
|
||||
|
||||
static const char *find_bridge_script(char *path, size_t capacity)
|
||||
{
|
||||
const char *configured = getenv("CABLE_HYPERSWARM_BRIDGE");
|
||||
static const char *candidates[] = {
|
||||
"src/cable_hyperswarm.js", "./cable_hyperswarm.js",
|
||||
"/usr/local/share/datgpt/cable_hyperswarm.js",
|
||||
"/usr/share/datgpt/cable_hyperswarm.js"
|
||||
};
|
||||
size_t i;
|
||||
if (configured && *configured) {
|
||||
if (strlen(configured) >= capacity) { errno = ENAMETOOLONG; return NULL; }
|
||||
strcpy(path, configured);
|
||||
return access(path, R_OK) == 0 ? path : NULL;
|
||||
}
|
||||
for (i = 0; i < sizeof(candidates) / sizeof(candidates[0]); i++)
|
||||
if (access(candidates[i], R_OK) == 0) {
|
||||
if (strlen(candidates[i]) >= capacity) { errno = ENAMETOOLONG; return NULL; }
|
||||
strcpy(path, candidates[i]);
|
||||
return path;
|
||||
}
|
||||
errno = ENOENT;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static int send_form(cable_hyperswarm_t *bridge, const char *verb,
|
||||
const char *a, const char *b, const char *c, const char *d)
|
||||
{
|
||||
s7_scheme *sc;
|
||||
s7_pointer port, form;
|
||||
const char *serialized;
|
||||
size_t length, offset = 0;
|
||||
const char *values[4] = {a, b, c, d};
|
||||
s7_pointer args[5];
|
||||
int i;
|
||||
if (!bridge || !bridge->scheme.scheme || bridge->input_fd < 0 || !verb) { errno = EINVAL; return -1; }
|
||||
sc = bridge->scheme.scheme;
|
||||
args[0] = s7_make_symbol(sc, verb);
|
||||
length = 1;
|
||||
for (i = 0; i < 4 && values[i]; i++) args[length++] = s7_make_string(sc, values[i]);
|
||||
form = s7_nil(sc);
|
||||
while (length) form = s7_cons(sc, args[--length], form);
|
||||
port = s7_open_output_string(sc);
|
||||
if (!port) { errno = ENOMEM; return -1; }
|
||||
(void)s7_write(sc, form, port);
|
||||
serialized = s7_get_output_string(sc, port);
|
||||
if (!serialized) { errno = EIO; return -1; }
|
||||
length = strlen(serialized);
|
||||
while (offset < length) {
|
||||
ssize_t written = write(bridge->input_fd, serialized + offset, length - offset);
|
||||
if (written < 0 && errno == EINTR) continue;
|
||||
if (written <= 0) return -1;
|
||||
offset += (size_t)written;
|
||||
}
|
||||
return write(bridge->input_fd, "\n", 1) == 1 ? 0 : -1;
|
||||
}
|
||||
|
||||
static int parse_event_line(cable_hyperswarm_t *bridge, const char *line,
|
||||
size_t length, cable_hyperswarm_event_fn callback,
|
||||
void *context)
|
||||
{
|
||||
s7_scheme *sc = bridge->scheme.scheme;
|
||||
s7_pointer parser, event;
|
||||
if (length > 8192) { errno = EMSGSIZE; return -1; }
|
||||
{
|
||||
char *terminated = malloc(length + 1);
|
||||
if (!terminated) return -1;
|
||||
memcpy(terminated, line, length); terminated[length] = '\0';
|
||||
parser = s7_name_to_value(sc, "cable:parse-bridge-event");
|
||||
event = s7_call(sc, parser, s7_list(sc, 1, s7_make_string(sc, terminated)));
|
||||
free(terminated);
|
||||
}
|
||||
if (!event || s7_is_eq(event, s7_f(sc))) { errno = EPROTO; return -1; }
|
||||
callback(context, event);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int cable_hyperswarm_start(cable_hyperswarm_t *bridge, const char *room_key,
|
||||
const char *storage, const char *nick,
|
||||
const char *channel)
|
||||
{
|
||||
int child_in[2] = {-1, -1}, child_out[2] = {-1, -1};
|
||||
char script[2048];
|
||||
const char *node = getenv("CABLE_NODE");
|
||||
pid_t pid;
|
||||
if (!bridge || !room_key || !storage || !nick || !channel) { errno = EINVAL; return -1; }
|
||||
memset(bridge, 0, sizeof(*bridge)); bridge->input_fd = -1; bridge->output_fd = -1;
|
||||
if (!find_bridge_script(script, sizeof(script))) return -1;
|
||||
if (!node || !*node) node = "node";
|
||||
if (dat_scheme_runtime_init(&bridge->scheme)) { errno = EINVAL; return -1; }
|
||||
{
|
||||
char scheme_file[2048];
|
||||
const char *dir = getenv("DAT_SCHEME_DIR");
|
||||
int n;
|
||||
if (!dir || !*dir) dir = access("scheme/cable-room.scm", R_OK) == 0 ? "scheme" : "/usr/local/share/datgpt/scheme";
|
||||
n = snprintf(scheme_file, sizeof(scheme_file), "%s/cable-room.scm", dir);
|
||||
if (n < 0 || (size_t)n >= sizeof(scheme_file) || !s7_load(bridge->scheme.scheme, scheme_file)) {
|
||||
dat_scheme_runtime_close(&bridge->scheme); errno = ENOENT; return -1;
|
||||
}
|
||||
}
|
||||
if (pipe(child_in) || pipe(child_out)) goto fail;
|
||||
pid = fork();
|
||||
if (pid < 0) goto fail;
|
||||
if (pid == 0) {
|
||||
(void)signal(SIGPIPE, SIG_DFL);
|
||||
(void)dup2(child_in[0], STDIN_FILENO);
|
||||
(void)dup2(child_out[1], STDOUT_FILENO);
|
||||
close(child_in[0]); close(child_in[1]); close(child_out[0]); close(child_out[1]);
|
||||
execlp(node, node, script, (char *)NULL);
|
||||
_exit(127);
|
||||
}
|
||||
/* A sidecar shutdown must become EPIPE here, not terminate SDL. */
|
||||
(void)signal(SIGPIPE, SIG_IGN);
|
||||
close(child_in[0]); child_in[0] = -1;
|
||||
close(child_out[1]); child_out[1] = -1;
|
||||
bridge->pid = pid; bridge->input_fd = child_in[1]; bridge->output_fd = child_out[0];
|
||||
if (fcntl(bridge->output_fd, F_SETFL, fcntl(bridge->output_fd, F_GETFL) | O_NONBLOCK) < 0 ||
|
||||
send_form(bridge, "start", room_key, storage, nick, channel)) goto fail;
|
||||
return 0;
|
||||
fail:
|
||||
{
|
||||
int saved = errno;
|
||||
if (child_in[0] >= 0) close(child_in[0]);
|
||||
if (child_in[1] >= 0) close(child_in[1]);
|
||||
if (child_out[0] >= 0) close(child_out[0]);
|
||||
if (child_out[1] >= 0) close(child_out[1]);
|
||||
if (bridge->pid > 0) { kill(bridge->pid, SIGTERM); (void)waitpid(bridge->pid, NULL, 0); }
|
||||
bridge->pid = 0; bridge->input_fd = -1; bridge->output_fd = -1;
|
||||
dat_scheme_runtime_close(&bridge->scheme); errno = saved; return -1;
|
||||
}
|
||||
}
|
||||
|
||||
int cable_hyperswarm_send(cable_hyperswarm_t *bridge, const char *channel, const char *text)
|
||||
{ return send_form(bridge, "send", channel, text, NULL, NULL); }
|
||||
int cable_hyperswarm_nick(cable_hyperswarm_t *bridge, const char *nick)
|
||||
{ return send_form(bridge, "nick", nick, NULL, NULL, NULL); }
|
||||
int cable_hyperswarm_join(cable_hyperswarm_t *bridge, const char *channel)
|
||||
{ return send_form(bridge, "join", channel, NULL, NULL, NULL); }
|
||||
|
||||
int cable_hyperswarm_live(cable_hyperswarm_t *bridge,
|
||||
const uint8_t *packet, size_t packet_length)
|
||||
{
|
||||
size_t capacity;
|
||||
char *encoded;
|
||||
int result;
|
||||
if (!packet || !packet_length || packet_length > CABLE_MEDIA_MAX_PACKET_BYTES) {
|
||||
errno = EMSGSIZE; return -1;
|
||||
}
|
||||
capacity = sodium_base64_encoded_len(packet_length, sodium_base64_VARIANT_ORIGINAL);
|
||||
encoded = (char *)malloc(capacity);
|
||||
if (!encoded) return -1;
|
||||
if (!sodium_bin2base64(encoded, capacity, packet, packet_length,
|
||||
sodium_base64_VARIANT_ORIGINAL)) {
|
||||
free(encoded); errno = EINVAL; return -1;
|
||||
}
|
||||
result = send_form(bridge, "live", encoded, NULL, NULL, NULL);
|
||||
free(encoded);
|
||||
return result;
|
||||
}
|
||||
|
||||
int cable_hyperswarm_poll(cable_hyperswarm_t *bridge,
|
||||
cable_hyperswarm_event_fn callback, void *context)
|
||||
{
|
||||
char incoming[4096];
|
||||
ssize_t got;
|
||||
if (!bridge || bridge->output_fd < 0 || !callback) { errno = EINVAL; return -1; }
|
||||
while ((got = read(bridge->output_fd, incoming, sizeof(incoming))) > 0) {
|
||||
size_t i;
|
||||
for (i = 0; i < (size_t)got; i++) {
|
||||
if (incoming[i] == '\n') {
|
||||
if (parse_event_line(bridge, bridge->pending, bridge->pending_length, callback, context)) return -1;
|
||||
bridge->pending_length = 0;
|
||||
} else {
|
||||
if (bridge->pending_length + 1 >= sizeof(bridge->pending)) { errno = EMSGSIZE; return -1; }
|
||||
bridge->pending[bridge->pending_length++] = incoming[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
if (got < 0 && errno != EAGAIN && errno != EWOULDBLOCK && errno != EINTR) return -1;
|
||||
if (bridge->pid > 0) {
|
||||
int status;
|
||||
pid_t ended = waitpid(bridge->pid, &status, WNOHANG);
|
||||
if (ended == bridge->pid) { bridge->pid = 0; errno = EPIPE; return -1; }
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
void cable_hyperswarm_stop(cable_hyperswarm_t *bridge)
|
||||
{
|
||||
if (!bridge) return;
|
||||
if (bridge->input_fd >= 0) { (void)send_form(bridge, "close", NULL, NULL, NULL, NULL); close(bridge->input_fd); }
|
||||
if (bridge->output_fd >= 0) close(bridge->output_fd);
|
||||
if (bridge->pid > 0) {
|
||||
int status;
|
||||
unsigned tries;
|
||||
for (tries = 0; tries < 20; tries++) {
|
||||
pid_t ended = waitpid(bridge->pid, &status, WNOHANG);
|
||||
if (ended == bridge->pid || (ended < 0 && errno == ECHILD)) break;
|
||||
{
|
||||
struct timespec delay = {0, 10000000L};
|
||||
(void)nanosleep(&delay, NULL);
|
||||
}
|
||||
}
|
||||
if (tries == 20) { kill(bridge->pid, SIGTERM); (void)waitpid(bridge->pid, &status, 0); }
|
||||
}
|
||||
dat_scheme_runtime_close(&bridge->scheme);
|
||||
memset(bridge, 0, sizeof(*bridge)); bridge->input_fd = -1; bridge->output_fd = -1;
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,42 @@
|
||||
#ifndef CABLE_HYPERSWARM_H
|
||||
#define CABLE_HYPERSWARM_H
|
||||
|
||||
#include "dat_s7.h"
|
||||
#include "s7.h"
|
||||
#include <stddef.h>
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
#else
|
||||
#include <sys/types.h>
|
||||
#endif
|
||||
|
||||
typedef struct cable_hyperswarm {
|
||||
#ifdef _WIN32
|
||||
HANDLE process_handle;
|
||||
#else
|
||||
pid_t pid;
|
||||
#endif
|
||||
int input_fd;
|
||||
int output_fd;
|
||||
char pending[16384];
|
||||
size_t pending_length;
|
||||
dat_scheme_runtime_t scheme;
|
||||
} cable_hyperswarm_t;
|
||||
|
||||
typedef void (*cable_hyperswarm_event_fn)(void *context, s7_pointer event);
|
||||
|
||||
int cable_hyperswarm_start(cable_hyperswarm_t *bridge, const char *room_key,
|
||||
const char *storage, const char *nick,
|
||||
const char *channel);
|
||||
int cable_hyperswarm_send(cable_hyperswarm_t *bridge, const char *channel,
|
||||
const char *text);
|
||||
int cable_hyperswarm_nick(cable_hyperswarm_t *bridge, const char *nick);
|
||||
int cable_hyperswarm_join(cable_hyperswarm_t *bridge, const char *channel);
|
||||
/* Carry one bounded, binary-safe live media packet over Cable's room peers. */
|
||||
int cable_hyperswarm_live(cable_hyperswarm_t *bridge,
|
||||
const uint8_t *packet, size_t packet_length);
|
||||
int cable_hyperswarm_poll(cable_hyperswarm_t *bridge,
|
||||
cable_hyperswarm_event_fn callback, void *context);
|
||||
void cable_hyperswarm_stop(cable_hyperswarm_t *bridge);
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,362 @@
|
||||
'use strict'
|
||||
|
||||
// Cable's SDL-facing bridge speaks S-expressions on stdin/stdout. Cabal Core
|
||||
// owns the network boundary and continues to exchange its canonical JSON
|
||||
// Hypercore blocks over Hyperswarm.
|
||||
const fs = require('fs')
|
||||
const { createRequire } = require('module')
|
||||
const os = require('os')
|
||||
const path = require('path')
|
||||
const readline = require('readline')
|
||||
|
||||
function loadCabalCore () {
|
||||
const configured = process.env.CABLE_CABAL_CORE || process.env.CABAL_JS_MODULE
|
||||
if (configured) {
|
||||
const entry = require.resolve(configured)
|
||||
const requireCabal = createRequire(entry)
|
||||
return { Cabal: requireCabal(entry), requireCabal }
|
||||
}
|
||||
try {
|
||||
const entry = require.resolve('cabal-core')
|
||||
const requireCabal = createRequire(entry)
|
||||
return { Cabal: requireCabal(entry), requireCabal }
|
||||
} catch (_) {}
|
||||
const home = os.homedir()
|
||||
const candidates = [
|
||||
path.join(home, 'AppData', 'Roaming', 'npm', 'node_modules', 'cabal-cli', 'node_modules', 'cabal-core'),
|
||||
path.join(home, '.npm-global', 'lib', 'node_modules', 'cabal-cli', 'node_modules', 'cabal-core'),
|
||||
'/usr/local/lib/node_modules/cabal-cli/node_modules/cabal-core',
|
||||
'/usr/lib/node_modules/cabal-cli/node_modules/cabal-core'
|
||||
]
|
||||
for (const candidate of candidates) {
|
||||
if (fs.existsSync(path.join(candidate, 'index.js'))) {
|
||||
const entry = path.join(candidate, 'index.js')
|
||||
const requireCabal = createRequire(entry)
|
||||
return { Cabal: requireCabal(entry), requireCabal }
|
||||
}
|
||||
}
|
||||
throw new Error('Cabal Core not found; install cabal-cli or set CABLE_CABAL_CORE')
|
||||
}
|
||||
|
||||
function parseBootstrap (requireCabal) {
|
||||
const setting = process.env.CABLE_BOOTSTRAP || ''
|
||||
const configPath = path.join(os.homedir(), '.config', 'dat-gpt', 'cable-bootstrap')
|
||||
let saved = ''
|
||||
try { saved = fs.readFileSync(configPath, 'utf8') } catch (err) {
|
||||
if (err.code !== 'ENOENT') throw err
|
||||
}
|
||||
const endpoints = (setting + '\n' + saved).split(/[;,\s]+/).filter(Boolean)
|
||||
if (!endpoints.length) return requireCabal('./bootstrap_nodes')
|
||||
return Array.from(new Set(endpoints))
|
||||
}
|
||||
|
||||
// Mirrors Cabal Core's swarm.js replication and peer-id extension, while
|
||||
// allowing the same DHT bootstrap list to be selected by the local client.
|
||||
async function startHyperswarm (cabal, networkOptions = {}) {
|
||||
const loaded = loadCabalCore()
|
||||
const DHT = loaded.requireCabal('hyperdht')
|
||||
const Swarm = loaded.requireCabal('hyperswarm')
|
||||
const crypto = loaded.requireCabal('hypercore-crypto')
|
||||
const pump = loaded.requireCabal('pump')
|
||||
const localKey = await new Promise((resolve, reject) =>
|
||||
cabal.getLocalKey((err, key) => err ? reject(err) : resolve(key)))
|
||||
const dhtOptions = { bootstrap: parseBootstrap(loaded.requireCabal) }
|
||||
const host = networkOptions.host || process.env.CABLE_DHT_HOST
|
||||
const port = networkOptions.port || process.env.CABLE_DHT_PORT
|
||||
if (host) dhtOptions.host = host
|
||||
if (port) {
|
||||
const numericPort = Number(port)
|
||||
if (!Number.isInteger(numericPort) || numericPort < 1 || numericPort > 65535)
|
||||
throw new Error('CABLE_DHT_PORT must be an integer from 1 to 65535')
|
||||
dhtOptions.port = numericPort
|
||||
}
|
||||
const advertiseHost = networkOptions.advertiseHost || process.env.CABLE_DHT_ADVERTISE_HOST
|
||||
if (advertiseHost) dhtOptions.firewalled = false
|
||||
const dht = new DHT(dhtOptions)
|
||||
if (advertiseHost && Number.isInteger(dhtOptions.port) && dht._nat && typeof dht._nat.add === 'function')
|
||||
dht._nat.add(advertiseHost, dhtOptions.port)
|
||||
const swarm = new Swarm({ dht })
|
||||
mediaExtensions.clear()
|
||||
mediaSeen.clear()
|
||||
swarm.on('error', err => {
|
||||
if (process.env.CABLE_DEBUG) process.stderr.write('Cable Hyperswarm error: ' + err.message + '\n')
|
||||
})
|
||||
const discoveryKey = crypto.discoveryKey(Buffer.from(cabal.key, 'hex'))
|
||||
if (process.env.CABLE_DEBUG) process.stderr.write('Cable swarm pid=' + process.pid + ' room=' + cabal.key +
|
||||
' discovery=' + discoveryKey.toString('hex') + '\n')
|
||||
const discovery = swarm.join(discoveryKey, { server: true, client: true })
|
||||
let shuttingDown = false
|
||||
swarm.__cabalDiscovery = discovery
|
||||
swarm.__markShutdown = () => { shuttingDown = true }
|
||||
swarm.__shutdown = async function () {
|
||||
swarm.__markShutdown()
|
||||
await discovery.destroy()
|
||||
await swarm.destroy()
|
||||
}
|
||||
swarm.on('connection', (socket, info) => {
|
||||
if (process.env.CABLE_DEBUG) process.stderr.write('Cable Hyperswarm connection opened pid=' + process.pid +
|
||||
' client=' + Boolean(info.client) + ' peer=' + (info.publicKey ? info.publicKey.toString('hex') : 'unknown') + '\n')
|
||||
socket.on('error', err => {
|
||||
if (process.env.CABLE_DEBUG) process.stderr.write('Cable Hyperswarm socket error: ' + err.message + '\n')
|
||||
})
|
||||
let remoteKey
|
||||
const replication = cabal.replicate(info.client)
|
||||
if (process.env.CABLE_DEBUG) replication.on('error', err => process.stderr.write('Cable Hypercore replication error: ' + err.message + '\n'))
|
||||
pump(socket, replication, socket, err => {
|
||||
if (err && !shuttingDown && err.message !== 'Readable stream closed' && err.code !== 'ECONNRESET')
|
||||
process.stderr.write('Cable replication: ' + err.message + '\n')
|
||||
if (remoteKey) cabal._removeConnection(remoteKey)
|
||||
})
|
||||
const extension = replication.registerExtension('peer-id', {
|
||||
encoding: 'json',
|
||||
onmessage (message) {
|
||||
if (remoteKey || !message || !message.id) return
|
||||
const key = Buffer.from(message.id, 'hex')
|
||||
if (key.length !== 32) return
|
||||
remoteKey = message.id
|
||||
cabal._addConnection(remoteKey)
|
||||
if (process.env.CABLE_DEBUG) process.stderr.write('Cable Cabal peer id ' + remoteKey + '\n')
|
||||
}
|
||||
})
|
||||
const liveExtension = replication.registerExtension('cable-live', {
|
||||
onmessage (message) {
|
||||
const packet = Buffer.isBuffer(message) ? message : Buffer.from(message)
|
||||
if (packet.length > 4096) return
|
||||
relayMediaPacket(packet, liveExtension)
|
||||
if (packet.length >= 42 && packet.toString('ascii', 0, 4) === 'CBLM')
|
||||
emit('live', remoteKey || '', packet.toString('base64'))
|
||||
}
|
||||
})
|
||||
mediaExtensions.add(liveExtension)
|
||||
socket.once('close', () => mediaExtensions.delete(liveExtension))
|
||||
extension.send({ id: localKey })
|
||||
if (process.env.CABLE_DEBUG) process.stderr.write('Cable local peer id sent: ' + localKey + '\n')
|
||||
})
|
||||
cabal._swarm = swarm
|
||||
if (networkOptions.waitForBootstrap || process.env.CABLE_WAIT_DHT === '1') {
|
||||
await dht.fullyBootstrapped()
|
||||
await swarm.flush()
|
||||
}
|
||||
if (!(await discovery.flushed())) {
|
||||
await swarm.__shutdown()
|
||||
throw new Error('Hyperswarm topic discovery did not complete')
|
||||
}
|
||||
return swarm
|
||||
}
|
||||
|
||||
function quote (value) {
|
||||
const text = String(value == null ? '' : value)
|
||||
let quoted = '"'
|
||||
for (const character of text) {
|
||||
switch (character) {
|
||||
case '"': quoted += '\\"'; break
|
||||
case '\\': quoted += '\\\\'; break
|
||||
case '\b': quoted += '\\b'; break
|
||||
case '\f': quoted += '\\f'; break
|
||||
case '\n': quoted += '\\n'; break
|
||||
case '\r': quoted += '\\r'; break
|
||||
case '\t': quoted += '\\t'; break
|
||||
default: quoted += character
|
||||
}
|
||||
}
|
||||
return quoted + '"'
|
||||
}
|
||||
|
||||
function emit (kind, ...fields) {
|
||||
process.stdout.write('(event ' + kind + (fields.length ? ' ' + fields.map(value =>
|
||||
typeof value === 'number' ? String(value) : quote(value)).join(' ') : '') + ')\n')
|
||||
}
|
||||
|
||||
function parse (source) {
|
||||
let at = 0
|
||||
function white () { while (/\s/.test(source[at] || '')) at++ }
|
||||
function value () {
|
||||
white()
|
||||
if (source[at] === '(') {
|
||||
at++
|
||||
const list = []
|
||||
while (true) {
|
||||
white()
|
||||
if (source[at] === ')') { at++; return list }
|
||||
if (at >= source.length) throw new Error('unterminated S-expression')
|
||||
list.push(value())
|
||||
}
|
||||
}
|
||||
if (source[at] === '"') {
|
||||
at++
|
||||
let text = ''
|
||||
while (at < source.length) {
|
||||
const c = source[at++]
|
||||
if (c === '"') return text
|
||||
if (c !== '\\') { text += c; continue }
|
||||
if (at >= source.length) throw new Error('incomplete S-expression escape')
|
||||
const escape = source[at++]
|
||||
switch (escape) {
|
||||
case '"': text += '"'; break
|
||||
case '\\': text += '\\'; break
|
||||
case 'b': text += '\b'; break
|
||||
case 'f': text += '\f'; break
|
||||
case 'n': text += '\n'; break
|
||||
case 'r': text += '\r'; break
|
||||
case 't': text += '\t'; break
|
||||
default: throw new Error('unsupported S-expression escape: \\' + escape)
|
||||
}
|
||||
}
|
||||
throw new Error('unterminated S-expression string')
|
||||
}
|
||||
const begin = at
|
||||
while (at < source.length && !/[\s()]/.test(source[at])) at++
|
||||
if (begin === at) throw new Error('expected S-expression atom')
|
||||
const atom = source.slice(begin, at)
|
||||
if (/^-?[0-9]+$/.test(atom)) return Number(atom)
|
||||
return atom
|
||||
}
|
||||
const result = value()
|
||||
white()
|
||||
if (at !== source.length) throw new Error('trailing S-expression data')
|
||||
return result
|
||||
}
|
||||
|
||||
let cabal = null
|
||||
let localKey = null
|
||||
let starting = false
|
||||
const names = new Map()
|
||||
const pending = []
|
||||
const mediaExtensions = new Set()
|
||||
const mediaSeen = new Map()
|
||||
|
||||
function markMediaPacket (packet) {
|
||||
if (!Buffer.isBuffer(packet) || packet.length < 42 || packet.length > 4096 ||
|
||||
packet.toString('ascii', 0, 4) !== 'CBLM' || packet[4] !== 1 ||
|
||||
packet[5] < 1 || packet[5] > 6 || packet.readUInt16BE(40) !== packet.length - 42) return false
|
||||
const id = packet.subarray(8, 32).toString('hex')
|
||||
if (mediaSeen.has(id)) return false
|
||||
mediaSeen.set(id, true)
|
||||
if (mediaSeen.size > 2048) mediaSeen.delete(mediaSeen.keys().next().value)
|
||||
return true
|
||||
}
|
||||
|
||||
function relayMediaPacket (packet, source) {
|
||||
if (!markMediaPacket(packet)) return
|
||||
for (const extension of mediaExtensions) {
|
||||
if (extension === source) continue
|
||||
try { extension.send(packet) } catch (_) {}
|
||||
}
|
||||
}
|
||||
|
||||
function channelName (msg) {
|
||||
return msg && msg.value && msg.value.content && msg.value.content.channel
|
||||
}
|
||||
|
||||
async function start (roomKey, storage, nick, channel) {
|
||||
if (starting || cabal) throw new Error('Cabal room already started')
|
||||
starting = true
|
||||
try {
|
||||
if (process.env.CABLE_DEBUG) process.stderr.write('Cable Core storage: ' + storage + '\n')
|
||||
const { Cabal } = loadCabalCore()
|
||||
fs.mkdirSync(storage, { recursive: true })
|
||||
cabal = Cabal(storage, roomKey)
|
||||
await new Promise((resolve, reject) => cabal.ready(err => err ? reject(err) : resolve()))
|
||||
localKey = await new Promise((resolve, reject) => cabal.getLocalKey((err, key) => err ? reject(err) : resolve(key)))
|
||||
|
||||
cabal.on('peer-added', key => emit('peer-up', key))
|
||||
cabal.on('peer-dropped', key => emit('peer-down', key))
|
||||
cabal.kcore.api.users.events.on('update', (key, msg) => {
|
||||
if (key === localKey) return
|
||||
const name = msg && msg.value && msg.value.content && msg.value.content.name
|
||||
if (typeof name === 'string') names.set(key, name)
|
||||
emit('nick', key, names.get(key) || key.slice(0, 8))
|
||||
})
|
||||
cabal.kcore.api.messages.events.on('message', msg => {
|
||||
const val = msg && msg.value
|
||||
const content = val && val.content
|
||||
const key = msg && msg.key
|
||||
if (!val || !content || key === localKey || !key) return
|
||||
if (val.type === 'chat/text' && typeof content.channel === 'string' && typeof content.text === 'string') {
|
||||
emit('message', key, names.get(key) || key.slice(0, 8), content.channel,
|
||||
content.text, val.timestamp || 0)
|
||||
} else if (val.type === 'channel/join' && typeof content.channel === 'string') {
|
||||
emit('joined', key, names.get(key) || key.slice(0, 8), content.channel)
|
||||
}
|
||||
})
|
||||
|
||||
await startHyperswarm(cabal)
|
||||
emit('ready', localKey)
|
||||
await new Promise((resolve, reject) => cabal.publishNick(nick, err => err ? reject(err) : resolve()))
|
||||
await publish({ type: 'channel/join', content: { channel } })
|
||||
emit('joined-local', channel)
|
||||
for (const command of pending.splice(0)) await handle(command)
|
||||
} finally {
|
||||
starting = false
|
||||
}
|
||||
}
|
||||
|
||||
function publish (message) {
|
||||
return new Promise((resolve, reject) => cabal.publish(message, err => err ? reject(err) : resolve()))
|
||||
}
|
||||
|
||||
async function handle (command) {
|
||||
if (!Array.isArray(command) || !command.length) throw new Error('expected a command list')
|
||||
const [verb, ...args] = command
|
||||
if (verb === 'start') return start(...args)
|
||||
if (verb === 'close') {
|
||||
if (cabal && cabal._swarm && cabal._swarm.__shutdown) await cabal._swarm.__shutdown()
|
||||
if (cabal) await cabal.close()
|
||||
cabal = null
|
||||
localKey = null
|
||||
emit('closed')
|
||||
return
|
||||
}
|
||||
if (!cabal) {
|
||||
if (starting) { pending.push(command); return }
|
||||
throw new Error('Cabal room is not started')
|
||||
}
|
||||
if (verb === 'send') {
|
||||
const [channel, text] = args
|
||||
if (typeof channel !== 'string' || typeof text !== 'string') throw new Error('send expects channel and text strings')
|
||||
return publish({ type: 'chat/text', content: { channel, text } })
|
||||
}
|
||||
if (verb === 'nick') {
|
||||
const [nick] = args
|
||||
if (typeof nick !== 'string') throw new Error('nick expects a string')
|
||||
return new Promise((resolve, reject) => cabal.publishNick(nick, err => err ? reject(err) : resolve()))
|
||||
}
|
||||
if (verb === 'join') {
|
||||
const [channel] = args
|
||||
if (typeof channel !== 'string') throw new Error('join expects a channel string')
|
||||
return publish({ type: 'channel/join', content: { channel } })
|
||||
}
|
||||
if (verb === 'live') {
|
||||
const [encoded] = args
|
||||
if (typeof encoded !== 'string' || encoded.length > 5500)
|
||||
throw new Error('live expects one bounded base64 media packet')
|
||||
const packet = Buffer.from(encoded, 'base64')
|
||||
if (!markMediaPacket(packet)) throw new Error('invalid or repeated Cable live packet')
|
||||
for (const extension of mediaExtensions) {
|
||||
try { extension.send(packet) } catch (_) {}
|
||||
}
|
||||
return
|
||||
}
|
||||
throw new Error('unknown Cable bridge command: ' + verb)
|
||||
}
|
||||
|
||||
module.exports = { startHyperswarm, loadCabalCore, quote, parse }
|
||||
|
||||
if (require.main === module) {
|
||||
process.stdout.on('error', err => {
|
||||
if (err && err.code === 'EPIPE') process.exit(0)
|
||||
throw err
|
||||
})
|
||||
const input = readline.createInterface({ input: process.stdin, crlfDelay: Infinity })
|
||||
input.on('line', line => {
|
||||
if (!line.trim()) return
|
||||
let result
|
||||
try { result = handle(parse(line)) } catch (err) { emit('error', err.message); return }
|
||||
Promise.resolve(result).catch(err => emit('error', err && err.message ? err.message : String(err)))
|
||||
})
|
||||
input.on('close', async () => {
|
||||
if (cabal && cabal._swarm && cabal._swarm.__shutdown) { try { await cabal._swarm.__shutdown() } catch (_) {} }
|
||||
if (cabal) { try { await cabal.close() } catch (_) {} }
|
||||
process.exit(0)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,226 @@
|
||||
#define _POSIX_C_SOURCE 200809L
|
||||
#include "cable_hyperswarm.h"
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <io.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sodium.h>
|
||||
#include "cable_media.h"
|
||||
|
||||
static const char *find_bridge_script(char *path, size_t capacity)
|
||||
{
|
||||
const char *configured = getenv("CABLE_HYPERSWARM_BRIDGE");
|
||||
static const char *candidates[] = {"src/cable_hyperswarm.js", ".\\cable_hyperswarm.js", "cable_hyperswarm.js"};
|
||||
char module[MAX_PATH * 4], sibling[MAX_PATH * 4];
|
||||
size_t i;
|
||||
if (configured && *configured) {
|
||||
if (strlen(configured) >= capacity) { errno = ENAMETOOLONG; return NULL; }
|
||||
strcpy(path, configured);
|
||||
return _access(path, 4) == 0 ? path : NULL;
|
||||
}
|
||||
for (i = 0; i < sizeof(candidates) / sizeof(candidates[0]); i++)
|
||||
if (_access(candidates[i], 4) == 0) {
|
||||
if (strlen(candidates[i]) >= capacity) { errno = ENAMETOOLONG; return NULL; }
|
||||
strcpy(path, candidates[i]);
|
||||
return path;
|
||||
}
|
||||
{
|
||||
DWORD length = GetModuleFileNameA(NULL, module, (DWORD)sizeof(module));
|
||||
char *separator;
|
||||
int n;
|
||||
if (length && length < sizeof(module) && (separator = strrchr(module, '\\')) != NULL) {
|
||||
*separator = '\0';
|
||||
n = snprintf(sibling, sizeof(sibling), "%s\\cable_hyperswarm.js", module);
|
||||
if (n >= 0 && (size_t)n < sizeof(sibling) && _access(sibling, 4) == 0 &&
|
||||
strlen(sibling) < capacity) { strcpy(path, sibling); return path; }
|
||||
}
|
||||
}
|
||||
errno = ENOENT;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static int send_form(cable_hyperswarm_t *bridge, const char *verb,
|
||||
const char *a, const char *b, const char *c, const char *d)
|
||||
{
|
||||
s7_scheme *sc;
|
||||
s7_pointer port, form, args[5];
|
||||
const char *serialized;
|
||||
const char *values[4] = {a,b,c,d};
|
||||
size_t length, offset = 0;
|
||||
int count = 1, i;
|
||||
if (!bridge || !bridge->scheme.scheme || bridge->input_fd < 0 || !verb) { errno = EINVAL; return -1; }
|
||||
sc = bridge->scheme.scheme;
|
||||
args[0] = s7_make_symbol(sc, verb);
|
||||
for (i = 0; i < 4 && values[i]; i++) args[count++] = s7_make_string(sc, values[i]);
|
||||
form = s7_nil(sc);
|
||||
while (count) form = s7_cons(sc, args[--count], form);
|
||||
port = s7_open_output_string(sc);
|
||||
if (!port) { errno = ENOMEM; return -1; }
|
||||
(void)s7_write(sc, form, port);
|
||||
serialized = s7_get_output_string(sc, port);
|
||||
if (!serialized) { errno = EIO; return -1; }
|
||||
length = strlen(serialized);
|
||||
while (offset < length) {
|
||||
unsigned int chunk = length - offset > 0x7fffffffu ? 0x7fffffffu : (unsigned int)(length - offset);
|
||||
int written = _write(bridge->input_fd, serialized + offset, chunk);
|
||||
if (written <= 0) { errno = EPIPE; return -1; }
|
||||
offset += (size_t)written;
|
||||
}
|
||||
return _write(bridge->input_fd, "\n", 1) == 1 ? 0 : -1;
|
||||
}
|
||||
|
||||
static int parse_event_line(cable_hyperswarm_t *bridge, const char *line,
|
||||
size_t length, cable_hyperswarm_event_fn callback,
|
||||
void *context)
|
||||
{
|
||||
s7_scheme *sc = bridge->scheme.scheme;
|
||||
s7_pointer parser, event;
|
||||
char *terminated;
|
||||
if (length > 8192) { errno = EMSGSIZE; return -1; }
|
||||
terminated = (char *)malloc(length + 1);
|
||||
if (!terminated) return -1;
|
||||
memcpy(terminated, line, length); terminated[length] = '\0';
|
||||
parser = s7_name_to_value(sc, "cable:parse-bridge-event");
|
||||
event = s7_call(sc, parser, s7_list(sc, 1, s7_make_string(sc, terminated)));
|
||||
free(terminated);
|
||||
if (!event || s7_is_eq(event, s7_f(sc))) { errno = EPROTO; return -1; }
|
||||
callback(context, event);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int cable_hyperswarm_start(cable_hyperswarm_t *bridge, const char *room_key,
|
||||
const char *storage, const char *nick, const char *channel)
|
||||
{
|
||||
SECURITY_ATTRIBUTES security;
|
||||
HANDLE child_in = NULL, parent_in = NULL, parent_out = NULL, child_out = NULL;
|
||||
STARTUPINFOA startup;
|
||||
PROCESS_INFORMATION process;
|
||||
char script[2048], command[4096];
|
||||
const char *node = getenv("CABLE_NODE");
|
||||
const char *dir = getenv("DAT_SCHEME_DIR");
|
||||
char scheme_file[2048];
|
||||
char module[MAX_PATH * 4];
|
||||
int n;
|
||||
if (!bridge || !room_key || !storage || !nick || !channel) { errno = EINVAL; return -1; }
|
||||
memset(bridge, 0, sizeof(*bridge)); bridge->input_fd = -1; bridge->output_fd = -1;
|
||||
if (!find_bridge_script(script, sizeof(script))) return -1;
|
||||
if (!node || !*node) node = "node";
|
||||
if (dat_scheme_runtime_init(&bridge->scheme)) { errno = EINVAL; return -1; }
|
||||
if (dir && *dir) n = snprintf(scheme_file, sizeof(scheme_file), "%s\\cable-room.scm", dir);
|
||||
else if (_access("scheme/cable-room.scm", 4) == 0) n = snprintf(scheme_file, sizeof(scheme_file), "scheme\\cable-room.scm");
|
||||
else {
|
||||
DWORD length = GetModuleFileNameA(NULL, module, (DWORD)sizeof(module));
|
||||
char *separator = length && length < sizeof(module) ? strrchr(module, '\\') : NULL;
|
||||
if (separator) { *separator = '\0'; n = snprintf(scheme_file, sizeof(scheme_file), "%s\\scheme\\cable-room.scm", module); }
|
||||
else n = snprintf(scheme_file, sizeof(scheme_file), "share\\datgpt\\scheme\\cable-room.scm");
|
||||
}
|
||||
if (n < 0 || (size_t)n >= sizeof(scheme_file) || !s7_load(bridge->scheme.scheme, scheme_file)) {
|
||||
dat_scheme_runtime_close(&bridge->scheme); errno = ENOENT; return -1;
|
||||
}
|
||||
security.nLength = sizeof(security); security.lpSecurityDescriptor = NULL; security.bInheritHandle = TRUE;
|
||||
if (!CreatePipe(&child_in, &parent_in, &security, 0) || !CreatePipe(&parent_out, &child_out, &security, 0)) goto fail;
|
||||
SetHandleInformation(parent_in, HANDLE_FLAG_INHERIT, 0);
|
||||
SetHandleInformation(parent_out, HANDLE_FLAG_INHERIT, 0);
|
||||
memset(&startup, 0, sizeof(startup)); startup.cb = sizeof(startup);
|
||||
startup.dwFlags = STARTF_USESTDHANDLES;
|
||||
startup.hStdInput = child_in; startup.hStdOutput = child_out; startup.hStdError = GetStdHandle(STD_ERROR_HANDLE);
|
||||
memset(&process, 0, sizeof(process));
|
||||
n = snprintf(command, sizeof(command), "\"%s\" \"%s\"", node, script);
|
||||
if (n < 0 || (size_t)n >= sizeof(command) ||
|
||||
!CreateProcessA(NULL, command, NULL, NULL, TRUE, CREATE_NO_WINDOW, NULL, NULL, &startup, &process)) {
|
||||
errno = ENOENT; goto fail;
|
||||
}
|
||||
CloseHandle(child_in); child_in = NULL;
|
||||
CloseHandle(child_out); child_out = NULL;
|
||||
CloseHandle(process.hThread);
|
||||
bridge->process_handle = process.hProcess;
|
||||
bridge->input_fd = _open_osfhandle((intptr_t)parent_in, _O_BINARY | _O_WRONLY); parent_in = NULL;
|
||||
bridge->output_fd = _open_osfhandle((intptr_t)parent_out, _O_BINARY | _O_RDONLY); parent_out = NULL;
|
||||
if (bridge->input_fd < 0 || bridge->output_fd < 0) { errno = EIO; goto fail; }
|
||||
if (send_form(bridge, "start", room_key, storage, nick, channel)) goto fail;
|
||||
return 0;
|
||||
fail:
|
||||
if (child_in) CloseHandle(child_in);
|
||||
if (parent_in) CloseHandle(parent_in);
|
||||
if (parent_out) CloseHandle(parent_out);
|
||||
if (child_out) CloseHandle(child_out);
|
||||
if (bridge->input_fd >= 0) _close(bridge->input_fd);
|
||||
if (bridge->output_fd >= 0) _close(bridge->output_fd);
|
||||
if (bridge->process_handle) { TerminateProcess(bridge->process_handle, 1); WaitForSingleObject(bridge->process_handle, INFINITE); CloseHandle(bridge->process_handle); }
|
||||
bridge->input_fd = bridge->output_fd = -1; bridge->process_handle = NULL;
|
||||
dat_scheme_runtime_close(&bridge->scheme);
|
||||
return -1;
|
||||
}
|
||||
|
||||
int cable_hyperswarm_send(cable_hyperswarm_t *bridge, const char *channel, const char *text)
|
||||
{ return send_form(bridge, "send", channel, text, NULL, NULL); }
|
||||
int cable_hyperswarm_nick(cable_hyperswarm_t *bridge, const char *nick)
|
||||
{ return send_form(bridge, "nick", nick, NULL, NULL, NULL); }
|
||||
int cable_hyperswarm_join(cable_hyperswarm_t *bridge, const char *channel)
|
||||
{ return send_form(bridge, "join", channel, NULL, NULL, NULL); }
|
||||
|
||||
int cable_hyperswarm_live(cable_hyperswarm_t *bridge,
|
||||
const uint8_t *packet, size_t packet_length)
|
||||
{
|
||||
size_t capacity;
|
||||
char *encoded;
|
||||
int result;
|
||||
if (!packet || !packet_length || packet_length > CABLE_MEDIA_MAX_PACKET_BYTES) {
|
||||
errno = EMSGSIZE; return -1;
|
||||
}
|
||||
capacity = sodium_base64_encoded_len(packet_length, sodium_base64_VARIANT_ORIGINAL);
|
||||
encoded = (char *)malloc(capacity);
|
||||
if (!encoded) return -1;
|
||||
if (!sodium_bin2base64(encoded, capacity, packet, packet_length,
|
||||
sodium_base64_VARIANT_ORIGINAL)) {
|
||||
free(encoded); errno = EINVAL; return -1;
|
||||
}
|
||||
result = send_form(bridge, "live", encoded, NULL, NULL, NULL);
|
||||
free(encoded);
|
||||
return result;
|
||||
}
|
||||
|
||||
int cable_hyperswarm_poll(cable_hyperswarm_t *bridge,
|
||||
cable_hyperswarm_event_fn callback, void *context)
|
||||
{
|
||||
char incoming[4096];
|
||||
HANDLE pipe;
|
||||
if (!bridge || bridge->output_fd < 0 || !callback) { errno = EINVAL; return -1; }
|
||||
pipe = (HANDLE)_get_osfhandle(bridge->output_fd);
|
||||
for (;;) {
|
||||
DWORD available = 0, received = 0;
|
||||
size_t i;
|
||||
if (!PeekNamedPipe(pipe, NULL, 0, NULL, &available, NULL)) { errno = EPIPE; return -1; }
|
||||
if (!available) break;
|
||||
if (!ReadFile(pipe, incoming, available < sizeof(incoming) ? available : (DWORD)sizeof(incoming), &received, NULL)) { errno = EPIPE; return -1; }
|
||||
for (i = 0; i < (size_t)received; i++) {
|
||||
if (incoming[i] == '\n') {
|
||||
if (parse_event_line(bridge, bridge->pending, bridge->pending_length, callback, context)) return -1;
|
||||
bridge->pending_length = 0;
|
||||
} else {
|
||||
if (bridge->pending_length + 1 >= sizeof(bridge->pending)) { errno = EMSGSIZE; return -1; }
|
||||
bridge->pending[bridge->pending_length++] = incoming[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
if (bridge->process_handle && WaitForSingleObject(bridge->process_handle, 0) == WAIT_OBJECT_0) { errno = EPIPE; return -1; }
|
||||
return 0;
|
||||
}
|
||||
|
||||
void cable_hyperswarm_stop(cable_hyperswarm_t *bridge)
|
||||
{
|
||||
if (!bridge) return;
|
||||
if (bridge->input_fd >= 0) { (void)send_form(bridge, "close", NULL, NULL, NULL, NULL); _close(bridge->input_fd); }
|
||||
if (bridge->output_fd >= 0) _close(bridge->output_fd);
|
||||
if (bridge->process_handle) {
|
||||
if (WaitForSingleObject(bridge->process_handle, 200) == WAIT_TIMEOUT) {
|
||||
TerminateProcess(bridge->process_handle, 1);
|
||||
WaitForSingleObject(bridge->process_handle, INFINITE);
|
||||
}
|
||||
CloseHandle(bridge->process_handle);
|
||||
}
|
||||
dat_scheme_runtime_close(&bridge->scheme);
|
||||
memset(bridge, 0, sizeof(*bridge)); bridge->input_fd = -1; bridge->output_fd = -1;
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
'use strict'
|
||||
|
||||
// One SDL/C Cable client and one Cabal Core JavaScript client share a local
|
||||
// Hyperswarm bootstrap node, discover one another, and exchange chat records.
|
||||
const assert = require('assert')
|
||||
const { spawn } = require('child_process')
|
||||
const crypto = require('crypto')
|
||||
const dgram = require('dgram')
|
||||
const fs = require('fs')
|
||||
const os = require('os')
|
||||
const path = require('path')
|
||||
const readline = require('readline')
|
||||
const { startHyperswarm, loadCabalCore } = require('./cable_hyperswarm')
|
||||
|
||||
const binary = path.resolve(process.argv[2] || 'build/linux/test_cable_thang')
|
||||
const { Cabal, requireCabal } = loadCabalCore()
|
||||
const DHT = requireCabal('hyperdht')
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cable-js-c-live-'))
|
||||
const roomKey = crypto.randomBytes(32).toString('hex')
|
||||
const address = 'cabal://' + roomKey
|
||||
const cHome = path.join(root, 'c-client')
|
||||
const jsHome = path.join(root, 'js-client')
|
||||
const cLines = []
|
||||
const cWaiters = []
|
||||
let cClient = null
|
||||
let cabal = null
|
||||
let bootstrapper = null
|
||||
let relay = null
|
||||
|
||||
function freeUdpPort () {
|
||||
return new Promise((resolve, reject) => {
|
||||
const socket = dgram.createSocket('udp4')
|
||||
socket.once('error', reject)
|
||||
socket.bind(0, '127.0.0.1', () => {
|
||||
const port = socket.address().port
|
||||
socket.close(err => err ? reject(err) : resolve(port))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function ready (instance) {
|
||||
return new Promise((resolve, reject) => instance.ready(err => err ? reject(err) : resolve()))
|
||||
}
|
||||
|
||||
function waitFor (predicate, description, timeoutMs = 90000) {
|
||||
const started = Date.now()
|
||||
return new Promise((resolve, reject) => {
|
||||
const timer = setInterval(() => {
|
||||
if (Date.now() - started > timeoutMs) {
|
||||
clearInterval(timer)
|
||||
reject(new Error('timeout waiting for ' + description))
|
||||
return
|
||||
}
|
||||
try {
|
||||
const value = predicate()
|
||||
if (value) { clearInterval(timer); resolve(value) }
|
||||
} catch (err) { clearInterval(timer); reject(err) }
|
||||
}, 50)
|
||||
})
|
||||
}
|
||||
|
||||
function cLine (line) {
|
||||
cLines.push(line)
|
||||
process.stdout.write('C> ' + line + '\n')
|
||||
for (let i = cWaiters.length - 1; i >= 0; i--) {
|
||||
if (cWaiters[i].predicate(line)) {
|
||||
const waiter = cWaiters.splice(i, 1)[0]
|
||||
clearTimeout(waiter.timer)
|
||||
waiter.resolve(line)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function waitC (predicate, description, timeoutMs = 90000) {
|
||||
const existing = cLines.find(predicate)
|
||||
if (existing) return Promise.resolve(existing)
|
||||
return new Promise((resolve, reject) => {
|
||||
const waiter = { predicate, resolve, reject, timer: null }
|
||||
waiter.timer = setTimeout(() => {
|
||||
const index = cWaiters.indexOf(waiter)
|
||||
if (index >= 0) cWaiters.splice(index, 1)
|
||||
reject(new Error('timeout waiting for C client ' + description))
|
||||
}, timeoutMs)
|
||||
cWaiters.push(waiter)
|
||||
})
|
||||
}
|
||||
|
||||
async function main () {
|
||||
fs.mkdirSync(cHome, { recursive: true })
|
||||
fs.mkdirSync(jsHome, { recursive: true })
|
||||
const port = await freeUdpPort()
|
||||
const relayPort = await freeUdpPort()
|
||||
const jsPort = await freeUdpPort()
|
||||
const cPort = await freeUdpPort()
|
||||
bootstrapper = DHT.bootstrapper(port, '127.0.0.1', { host: '127.0.0.1' })
|
||||
await bootstrapper.ready()
|
||||
relay = new DHT({ bootstrap: ['127.0.0.1:' + port], host: '127.0.0.1', port: relayPort,
|
||||
ephemeral: false, firewalled: false })
|
||||
await relay.ready()
|
||||
const endpoint = '127.0.0.1:' + relayPort
|
||||
process.env.CABLE_BOOTSTRAP = endpoint
|
||||
process.env.CABLE_DHT_HOST = '127.0.0.1'
|
||||
process.env.CABLE_DHT_ADVERTISE_HOST = '127.0.0.1'
|
||||
process.env.CABLE_WAIT_DHT = '1'
|
||||
console.log('Local Hyperswarm bootstrap listening at ' + endpoint)
|
||||
|
||||
cabal = Cabal(jsHome, roomKey)
|
||||
await ready(cabal)
|
||||
if (process.env.CABLE_DEBUG) {
|
||||
const key = await new Promise((resolve, reject) => cabal.getLocalKey((err, value) => err ? reject(err) : resolve(value)))
|
||||
console.log('JS test storage: ' + jsHome)
|
||||
console.log('JS Cabal user key: ' + key)
|
||||
}
|
||||
const jsPeers = new Set()
|
||||
const jsMessages = []
|
||||
cabal.on('peer-added', key => { jsPeers.add(key); console.log('JS peer up: ' + key) })
|
||||
cabal.on('peer-dropped', key => jsPeers.delete(key))
|
||||
cabal.kcore.api.messages.events.on('message', message => {
|
||||
const value = message && message.value
|
||||
const content = value && value.content
|
||||
if (value && value.type === 'chat/text' && content && typeof content.text === 'string') {
|
||||
jsMessages.push(content.text)
|
||||
console.log('JS chat: ' + content.text)
|
||||
}
|
||||
})
|
||||
cClient = spawn(binary, [address], {
|
||||
cwd: process.cwd(),
|
||||
env: {
|
||||
...process.env,
|
||||
HOME: cHome,
|
||||
SDL_VIDEODRIVER: 'dummy',
|
||||
SDL_AUDIODRIVER: 'dummy',
|
||||
CABLE_BOOTSTRAP: endpoint,
|
||||
CABLE_DHT_HOST: '127.0.0.1',
|
||||
CABLE_DHT_PORT: String(cPort)
|
||||
},
|
||||
stdio: ['pipe', 'pipe', 'inherit']
|
||||
})
|
||||
cClient.stdin.on('error', () => {})
|
||||
cClient.once('error', err => {
|
||||
for (const waiter of cWaiters) { clearTimeout(waiter.timer); waiter.reject(err) }
|
||||
cWaiters.length = 0
|
||||
})
|
||||
readline.createInterface({ input: cClient.stdout }).on('line', cLine)
|
||||
const cReady = await waitC(line => line.startsWith('C_READY '), 'S7-backed Cabal room startup')
|
||||
const cFeedKey = cReady.slice('C_READY '.length)
|
||||
await startHyperswarm(cabal, { host: '127.0.0.1', port: jsPort,
|
||||
advertiseHost: '127.0.0.1', waitForBootstrap: true })
|
||||
await Promise.all([
|
||||
waitFor(() => jsPeers.size > 0, 'C client discovered by JS Cabal Core'),
|
||||
waitC(line => line === 'PEER_UP', 'JS client discovered by C Cable')
|
||||
])
|
||||
const jsKey = await new Promise((resolve, reject) =>
|
||||
cabal.getLocalKey((err, key) => err ? reject(err) : resolve(key)))
|
||||
assert.notStrictEqual(jsKey, cFeedKey, 'independent Cabal users must have distinct feed keys')
|
||||
assert(jsPeers.has(cFeedKey), 'JS Cabal Core must identify the C client feed')
|
||||
|
||||
await new Promise((resolve, reject) => cabal.publishNick('js-peer', err => err ? reject(err) : resolve()))
|
||||
await new Promise((resolve, reject) => cabal.publish({
|
||||
type: 'chat/text', content: { channel: 'default', text: 'message from cabal js' }
|
||||
}, err => err ? reject(err) : resolve()))
|
||||
await waitC(line => line.includes('message from cabal js'), 'JS-to-C message delivery')
|
||||
|
||||
cClient.stdin.write('SEND message from sdl c\n')
|
||||
await waitFor(() => jsMessages.includes('message from sdl c'), 'C-to-JS Hypercore feed replication')
|
||||
console.log('C SDL / Cabal JS Hyperswarm discovery and bidirectional chat: PASS')
|
||||
}
|
||||
|
||||
main().catch(err => { console.error(err.stack || err); process.exitCode = 1 }).finally(async () => {
|
||||
if (cClient && cClient.exitCode === null) {
|
||||
try { cClient.stdin.write('QUIT\n') } catch (_) {}
|
||||
await Promise.race([new Promise(resolve => cClient.once('exit', resolve)), new Promise(resolve => setTimeout(resolve, 2000))])
|
||||
if (cClient.exitCode === null) cClient.kill('SIGTERM')
|
||||
}
|
||||
if (cabal) {
|
||||
if (cabal._swarm && cabal._swarm.__shutdown) { try { await cabal._swarm.__shutdown() } catch (_) {} }
|
||||
try { await cabal.close() } catch (_) {}
|
||||
}
|
||||
if (relay) { try { await relay.destroy() } catch (_) {} }
|
||||
if (bootstrapper) { try { await bootstrapper.destroy() } catch (_) {} }
|
||||
const resolved = path.resolve(root)
|
||||
if (resolved.startsWith(path.resolve(os.tmpdir()) + path.sep) && path.basename(resolved).startsWith('cable-js-c-live-'))
|
||||
fs.rmSync(resolved, { recursive: true, force: true })
|
||||
})
|
||||
@@ -0,0 +1,15 @@
|
||||
'use strict'
|
||||
|
||||
const assert = require('assert')
|
||||
const { quote, parse } = require('./cable_hyperswarm')
|
||||
|
||||
for (const value of [
|
||||
'', 'plain text', 'quote " slash \\', 'line one\nline two',
|
||||
'tabs\tand\bbackspace\f formfeed\r', 'Unicode: 媒腻㐤┖ꈳ埳 🐬'
|
||||
]) {
|
||||
assert.deepStrictEqual(parse('(send "general" ' + quote(value) + ')'), ['send', 'general', value])
|
||||
}
|
||||
|
||||
assert.throws(() => parse('(send "unterminated)'), /unterminated/)
|
||||
assert.throws(() => parse('(send "bad\\q")'), /unsupported S-expression escape/)
|
||||
console.log('Cable S-expression IPC: PASS')
|
||||
@@ -0,0 +1,99 @@
|
||||
'use strict'
|
||||
|
||||
// Narrow diagnostic: validates the installed Hyperswarm/HyperDHT package and
|
||||
// the local bootstrap node independently of Cabal's Hypercore replication.
|
||||
const assert = require('assert')
|
||||
const crypto = require('crypto')
|
||||
const dgram = require('dgram')
|
||||
const { loadCabalCore } = require('./cable_hyperswarm')
|
||||
|
||||
const { requireCabal } = loadCabalCore()
|
||||
const DHT = requireCabal('hyperdht')
|
||||
const Swarm = requireCabal('hyperswarm')
|
||||
|
||||
function freePort () {
|
||||
return new Promise((resolve, reject) => {
|
||||
const socket = dgram.createSocket('udp4')
|
||||
socket.once('error', reject)
|
||||
socket.bind(0, '127.0.0.1', () => {
|
||||
const port = socket.address().port
|
||||
socket.close(err => err ? reject(err) : resolve(port))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
async function main () {
|
||||
const portA = await freePort()
|
||||
const portB = await freePort()
|
||||
const relayPort = await freePort()
|
||||
const useBuiltin = process.env.HYPERSWARM_TEST_BUILTIN === '1'
|
||||
const bootstrapPort = useBuiltin ? 0 : await freePort()
|
||||
const bootstrap = useBuiltin ? null : DHT.bootstrapper(bootstrapPort, '127.0.0.1', { host: '127.0.0.1' })
|
||||
const relay = useBuiltin ? null : new DHT({
|
||||
bootstrap: ['127.0.0.1:' + bootstrapPort], host: '127.0.0.1', port: relayPort,
|
||||
ephemeral: false, firewalled: false
|
||||
})
|
||||
const swarms = []
|
||||
try {
|
||||
if (bootstrap) await bootstrap.ready()
|
||||
if (relay) await relay.ready()
|
||||
if (bootstrap) console.log('bootstrap state', { id: Boolean(bootstrap.id), ephemeral: bootstrap.ephemeral, storage: Boolean(bootstrap._persistent) })
|
||||
if (relay) console.log('relay state', { id: Boolean(relay.id), ephemeral: relay.ephemeral, storage: Boolean(relay._persistent) })
|
||||
const endpoint = useBuiltin ? undefined : '127.0.0.1:' + relayPort
|
||||
for (const port of [portA, portB]) {
|
||||
const dht = new DHT({ ...(useBuiltin ? {} : { bootstrap: [endpoint] }), host: '127.0.0.1', port,
|
||||
ephemeral: false, firewalled: false })
|
||||
dht._nat.add('127.0.0.1', port)
|
||||
const swarm = new Swarm({ dht })
|
||||
dht.on('error', err => console.error('DHT error:', err.message))
|
||||
swarm.on('error', err => console.error('swarm error:', err.message))
|
||||
swarms.push(swarm)
|
||||
}
|
||||
const [a, b] = swarms
|
||||
const topic = crypto.randomBytes(32)
|
||||
let connected = 0
|
||||
const onConnection = socket => { connected++; socket.on('error', () => {}) }
|
||||
a.on('connection', onConnection)
|
||||
b.on('connection', onConnection)
|
||||
const onDiscoveryError = (label) => err => console.error(label + ' discovery error:', err && (err.stack || err))
|
||||
const discoveryA = a.join(topic, { server: true, client: false, onerror: onDiscoveryError('A') })
|
||||
const aFlushed = await discoveryA.flushed()
|
||||
const discoveryB = b.join(topic, { server: false, client: true, onerror: onDiscoveryError('B') })
|
||||
const bFlushed = await discoveryB.flushed()
|
||||
console.log('Hyperswarm discovery flush:', aFlushed + '/' + bFlushed)
|
||||
console.log('DHT node tables:', {
|
||||
a: a.dht.toArray(), b: b.dht.toArray(),
|
||||
relay: relay && relay.table.toArray().map(node => ({ host: node.host, port: node.port }))
|
||||
})
|
||||
if (relay) console.log('relay stored topic peers:', relay._persistent.records.get(topic.toString('hex'), 20).length)
|
||||
const lookup = b.dht.lookup(topic)
|
||||
const lookupResults = []
|
||||
lookup.on('data', result => lookupResults.push(result.peers.length))
|
||||
await lookup.finished()
|
||||
console.log('explicit topic lookup peer counts:', lookupResults)
|
||||
const deadline = Date.now() + 20000
|
||||
while (connected < 2 && Date.now() < deadline) await new Promise(resolve => setTimeout(resolve, 100))
|
||||
const discovered = connected >= 2
|
||||
console.log('Hyperswarm state:', { connections: [a.connections.size, b.connections.size],
|
||||
queued: [a.connecting, b.connecting], peerEntries: [a.peers.size, b.peers.size] })
|
||||
if (!discovered) {
|
||||
console.log('DHT discovery did not connect both local nodes; trying direct Hyperswarm peers to isolate transport')
|
||||
a.joinPeer(b.keyPair.publicKey)
|
||||
b.joinPeer(a.keyPair.publicKey)
|
||||
const directDeadline = Date.now() + 10000
|
||||
while (connected < 2 && Date.now() < directDeadline) await new Promise(resolve => setTimeout(resolve, 100))
|
||||
}
|
||||
assert(connected >= 2, 'local Hyperswarm peers failed direct transport too')
|
||||
console.log('local Hyperswarm transport: PASS')
|
||||
if (!discovered) {
|
||||
console.error('local DHT discovery: FAIL (the bootstrap node did not yield peer addresses)')
|
||||
process.exitCode = 1
|
||||
} else console.log('local Hyperswarm DHT discovery: PASS')
|
||||
} finally {
|
||||
for (const swarm of swarms) { try { await swarm.destroy() } catch (_) {} }
|
||||
if (relay) { try { await relay.destroy() } catch (_) {} }
|
||||
if (bootstrap) { try { await bootstrap.destroy() } catch (_) {} }
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(err => { console.error(err.stack || err); process.exitCode = 1 })
|
||||
@@ -0,0 +1,87 @@
|
||||
'use strict'
|
||||
|
||||
// Opens the native Cable user log with the bundled legacy Hypercore 7.7.1,
|
||||
// appends a JS record, and leaves the C side to read it back in the runner.
|
||||
const assert = require('assert')
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
|
||||
const root = path.resolve(__dirname, '../../..')
|
||||
const home = path.resolve(process.argv[2])
|
||||
const roomKey = process.argv[3]
|
||||
assert.match(roomKey, /^[0-9a-f]{64}$/)
|
||||
const roomDir = path.join(home, '.cable', 'rooms', roomKey)
|
||||
const feedDir = path.join(roomDir, '0')
|
||||
const feedKey = fs.readFileSync(path.join(feedDir, 'key'))
|
||||
const secretKey = fs.readFileSync(path.join(roomDir, 'user-feed.key'))
|
||||
assert.strictEqual(feedKey.length, 32)
|
||||
assert.strictEqual(secretKey.length, 64)
|
||||
|
||||
const Hypercore = require(path.join(root, 'extra/dillo-dat/node_modules/hypercore'))
|
||||
const randomAccessFile = require(path.join(root, 'extra/dillo-dat/node_modules/random-access-file'))
|
||||
function memoryStorage () {
|
||||
let bytes = Buffer.alloc(0)
|
||||
return {
|
||||
read (offset, length, callback) {
|
||||
const value = Buffer.alloc(length)
|
||||
bytes.copy(value, 0, offset, offset + length)
|
||||
callback(null, value)
|
||||
},
|
||||
write (offset, value, callback) {
|
||||
if (bytes.length < offset + value.length) {
|
||||
const grown = Buffer.alloc(offset + value.length)
|
||||
bytes.copy(grown)
|
||||
bytes = grown
|
||||
}
|
||||
value.copy(bytes, offset)
|
||||
callback(null)
|
||||
},
|
||||
stat (callback) { callback(null, { size: bytes.length }) },
|
||||
close (callback) { callback(null) }
|
||||
}
|
||||
}
|
||||
const storage = name => name === 'secret_key'
|
||||
? memoryStorage()
|
||||
: randomAccessFile(name, { directory: feedDir })
|
||||
|
||||
function ready (feed) {
|
||||
return new Promise((resolve, reject) => feed.ready(err => err ? reject(err) : resolve()))
|
||||
}
|
||||
|
||||
function get (feed, index) {
|
||||
return new Promise((resolve, reject) => feed.get(index, (err, value) => err ? reject(err) : resolve(value)))
|
||||
}
|
||||
|
||||
function append (feed, value) {
|
||||
return new Promise((resolve, reject) => feed.append(Buffer.from(JSON.stringify(value)), err => err ? reject(err) : resolve()))
|
||||
}
|
||||
|
||||
function close (feed) {
|
||||
return new Promise((resolve, reject) => feed.close(err => err ? reject(err) : resolve()))
|
||||
}
|
||||
|
||||
async function main () {
|
||||
const feed = Hypercore(storage, feedKey, { secretKey })
|
||||
feed.on('error', err => { throw err })
|
||||
try {
|
||||
await ready(feed)
|
||||
assert.strictEqual(feed.length, 2, 'C should have written two Cabal records')
|
||||
const nick = JSON.parse((await get(feed, 0)).toString('utf8'))
|
||||
const message = JSON.parse((await get(feed, 1)).toString('utf8'))
|
||||
assert.strictEqual(nick.type, 'about')
|
||||
assert.strictEqual(nick.content.name, 'cable-c-user')
|
||||
assert.strictEqual(message.type, 'chat/text')
|
||||
assert.strictEqual(message.content.channel, 'interop')
|
||||
assert.strictEqual(message.content.text, 'hello from C Hypercore')
|
||||
await append(feed, {
|
||||
type: 'chat/text',
|
||||
content: { channel: 'interop', text: 'hello from Cabal JavaScript Hypercore' },
|
||||
timestamp: Date.now()
|
||||
})
|
||||
process.stdout.write(`Cabal Hypercore v7.7.1 read C user feed and appended reply: PASS key=${feedKey.toString('hex')}\n`)
|
||||
} finally {
|
||||
await close(feed)
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(err => { console.error(err); process.exitCode = 1 })
|
||||
@@ -0,0 +1,44 @@
|
||||
param([switch]$KeepFixture)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$root = (Resolve-Path (Join-Path $PSScriptRoot '..\..\..')).Path
|
||||
$buildDir = [System.IO.Path]::GetFullPath((Join-Path $root 'build\linux'))
|
||||
$binary = Join-Path $buildDir 'cable'
|
||||
$nodeScript = Join-Path $PSScriptRoot 'cable_hypercore_compat.js'
|
||||
$runRoot = [System.IO.Path]::GetFullPath((Join-Path $root 'local-data\test-runs'))
|
||||
$fixtureHome = Join-Path $runRoot ('cable-hypercore-interop-' + [guid]::NewGuid().ToString('N'))
|
||||
$linuxRoot = '/mnt/' + $root.Substring(0, 1).ToLower() + '/' + $root.Substring(3).Replace('\', '/')
|
||||
$linuxHome = $linuxRoot + '/local-data/test-runs/' + (Split-Path $fixtureHome -Leaf)
|
||||
$room = '42' * 32
|
||||
|
||||
function Invoke-WslCable([string[]]$Arguments) {
|
||||
& wsl.exe --exec env "HOME=$linuxHome" "$linuxRoot/build/linux/cable" @Arguments
|
||||
if ($LASTEXITCODE -ne 0) { throw "C cable command failed with exit code $LASTEXITCODE" }
|
||||
}
|
||||
|
||||
New-Item -ItemType Directory -Path $fixtureHome -Force | Out-Null
|
||||
try {
|
||||
& wsl.exe --exec make -C $linuxRoot build/linux/cable
|
||||
if ($LASTEXITCODE -ne 0) { throw "WSL Cable build failed with exit code $LASTEXITCODE" }
|
||||
Invoke-WslCable @('nick', "cabal://$room", 'cable-c-user') | Out-Null
|
||||
Invoke-WslCable @('publish', "cabal://$room", 'interop', 'hello from C Hypercore') | Out-Null
|
||||
|
||||
& node $nodeScript $fixtureHome $room
|
||||
if ($LASTEXITCODE -ne 0) { throw "JavaScript Hypercore check failed with exit code $LASTEXITCODE" }
|
||||
|
||||
$records = Invoke-WslCable @('feed', "cabal://$room")
|
||||
if (($records -join "`n") -notmatch 'hello from Cabal JavaScript Hypercore') {
|
||||
throw 'C did not read the JavaScript-appended block'
|
||||
}
|
||||
Write-Output 'Cabal Core room/0 storage + Hypercore v7.7.1 read/write: PASS'
|
||||
Write-Output 'C read JavaScript-appended Hypercore block: PASS'
|
||||
} finally {
|
||||
if (-not $KeepFixture -and (Test-Path -LiteralPath $fixtureHome)) {
|
||||
$resolvedFixture = [System.IO.Path]::GetFullPath((Resolve-Path -LiteralPath $fixtureHome).Path)
|
||||
$runPrefix = $runRoot.TrimEnd('\') + '\'
|
||||
if (-not $resolvedFixture.StartsWith($runPrefix, [System.StringComparison]::OrdinalIgnoreCase)) {
|
||||
throw "Refusing to remove fixture outside local-data/test-runs/: $resolvedFixture"
|
||||
}
|
||||
Remove-Item -LiteralPath $resolvedFixture -Recurse -Force
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
const fs = require('fs')
|
||||
const os = require('os')
|
||||
const path = require('path')
|
||||
const hyperdrive = require('../../extra/dillo-dat/node_modules/hyperdrive')
|
||||
const hyperdiscovery = require('../../extra/dillo-dat/node_modules/hyperdiscovery')
|
||||
|
||||
function ready (archive) {
|
||||
return new Promise((resolve, reject) => archive.ready(err => err ? reject(err) : resolve()))
|
||||
}
|
||||
|
||||
function writeFile (archive, name, data) {
|
||||
return new Promise((resolve, reject) => archive.writeFile(name, data, err => err ? reject(err) : resolve()))
|
||||
}
|
||||
|
||||
function readFile (archive, name) {
|
||||
return new Promise((resolve, reject) => archive.readFile(name, (err, data) => err ? reject(err) : resolve(data)))
|
||||
}
|
||||
|
||||
function listening (swarm) {
|
||||
if (swarm.address()) return Promise.resolve(swarm.address())
|
||||
return new Promise(resolve => swarm.once('listening', () => resolve(swarm.address())))
|
||||
}
|
||||
|
||||
function sleep (ms) { return new Promise(resolve => setTimeout(resolve, ms)) }
|
||||
|
||||
async function main () {
|
||||
const root = fs.mkdtempSync(path.join(process.env.DDAT_TEST_DIR || os.tmpdir(), 'ddat-js-p2p-'))
|
||||
let source, replica, sourceSwarm, replicaSwarm
|
||||
try {
|
||||
source = hyperdrive(path.join(root, 'source'))
|
||||
await ready(source)
|
||||
const key = source.key.toString('hex')
|
||||
const address = 'dat://' + key
|
||||
await writeFile(source, 'hello.txt', 'hello from old dillo-dat v1')
|
||||
await writeFile(source, 'hello.txt', 'hello from old dillo-dat v2')
|
||||
if (source.key.toString('hex') !== key) throw new Error('JS archive address changed after update')
|
||||
replica = hyperdrive(path.join(root, 'replica'), source.key, {sparse: true})
|
||||
await ready(replica)
|
||||
|
||||
sourceSwarm = hyperdiscovery(source, {port: 0, discovery: false, dht: false, dns: false, utp: false})
|
||||
replicaSwarm = hyperdiscovery(replica, {port: 0, discovery: false, dht: false, dns: false, utp: false})
|
||||
const sourceAddress = await listening(sourceSwarm)
|
||||
const replicaAddress = await listening(replicaSwarm)
|
||||
const topic = source.discoveryKey
|
||||
sourceSwarm.addPeer(topic, {host: '127.0.0.1', port: replicaAddress.port})
|
||||
replicaSwarm.addPeer(topic, {host: '127.0.0.1', port: sourceAddress.port})
|
||||
|
||||
const deadline = Date.now() + 15000
|
||||
let result
|
||||
while (Date.now() < deadline) {
|
||||
try { result = await readFile(replica, 'hello.txt'); break } catch (_) { await sleep(100) }
|
||||
}
|
||||
if (!result || result.toString() !== 'hello from old dillo-dat v2') throw new Error('old JS replication did not deliver the latest file')
|
||||
process.stdout.write('old Dillo Dat JS create/update/address/xfer: PASS ' + address +
|
||||
' metadata=' + source.metadata.length + ' content=' + source.content.length + '\n')
|
||||
} finally {
|
||||
if (sourceSwarm) sourceSwarm.destroy()
|
||||
if (replicaSwarm) replicaSwarm.destroy()
|
||||
if (source) source.close()
|
||||
if (replica) replica.close()
|
||||
if (process.env.DDAT_TEST_KEEP === '1') {
|
||||
process.stdout.write('kept old JS source and replica at ' + root + '\n')
|
||||
} else {
|
||||
fs.rmSync(root, {recursive: true, force: true})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(err => { console.error(err); process.exitCode = 1 })
|
||||
@@ -0,0 +1,34 @@
|
||||
(define sample
|
||||
(dat:json-parse
|
||||
"{\"title\":\"dat \\\"thang\\\"\",\"items\":[true,false,null,-2,1.25e2],\"line\":\"a\\nb\"}"))
|
||||
(unless (equal? (dat:json-object-ref sample "title") "dat \"thang\"")
|
||||
(error 'test-failed "JSON strings and escaped quotes"))
|
||||
(unless (equal? (dat:json-object-ref sample "items")
|
||||
'(json:array #t #f json:null -2 125.0))
|
||||
(error 'test-failed "JSON arrays, literals, and numbers"))
|
||||
(unless (equal? (dat:json-object-ref sample "line") "a\nb")
|
||||
(error 'test-failed "JSON escaped newline"))
|
||||
(unless (equal? (dat:json-parse "\"\\uD83D\\uDE00\"")
|
||||
(list->string (map integer->char '(#xf0 #x9f #x98 #x80))))
|
||||
(error 'test-failed "JSON UTF-16 surrogate pair"))
|
||||
(unless (json:number-token? "-0.4E+3")
|
||||
(error 'test-failed "valid JSON number"))
|
||||
(when (json:number-token? "+1")
|
||||
(error 'test-failed "reject Scheme-only number spelling"))
|
||||
(let* ((wire '(json:object ("type" . "chat/text")
|
||||
("content" . (json:object ("channel" . "general")
|
||||
("text" . "hello\n🐬")))
|
||||
("timestamp" . 42)))
|
||||
(encoded (dat:json-stringify wire))
|
||||
(decoded (dat:json-parse encoded)))
|
||||
(unless (equal? (dat:json-object-ref decoded "type") "chat/text")
|
||||
(error 'test-failed "S-expression JSON adapter object roundtrip"))
|
||||
(unless (equal? (dat:json-object-ref
|
||||
(dat:json-object-ref decoded "content") "text") "hello\n🐬")
|
||||
(error 'test-failed "S-expression JSON adapter escaped Unicode string")))
|
||||
(let ((caught #f))
|
||||
(catch #t
|
||||
(lambda () (dat:json-parse "[1,]"))
|
||||
(lambda args (set! caught #t)))
|
||||
(unless caught (error 'test-failed "reject trailing array comma")))
|
||||
(display "S7 JSON/S-expression adapter: PASS\n")
|
||||
@@ -0,0 +1,225 @@
|
||||
#define _POSIX_C_SOURCE 200809L
|
||||
#include "dat.h"
|
||||
#include "sleep_verify.h"
|
||||
#include "sleep_feed.h"
|
||||
#include "sleep.h"
|
||||
#include <assert.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
static int changes_seen;
|
||||
static uint64_t last_change_version;
|
||||
static int directory_entries_seen;
|
||||
|
||||
static int count_change(const sleep_metadata_entry_t *entry, uint64_t version,
|
||||
void *context)
|
||||
{
|
||||
(void)entry;
|
||||
(void)context;
|
||||
changes_seen++;
|
||||
last_change_version = version;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int count_directory_entry(const sleep_metadata_entry_t *entry,
|
||||
const char *name, void *context)
|
||||
{
|
||||
(void)entry;
|
||||
(void)context;
|
||||
assert(!strcmp(name, "sample.bin"));
|
||||
directory_entries_seen++;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
char directory[] = "/tmp/datgpt-archive-XXXXXX";
|
||||
char sync_directory[] = "/tmp/datgpt-sync-XXXXXX";
|
||||
char secrets_directory[] = "/tmp/datgpt-secrets-XXXXXX";
|
||||
dat_archive_t archive, reopened;
|
||||
dat_archive_t sync_archive;
|
||||
sleep_metadata_entry_t entry;
|
||||
uint8_t *input, *output;
|
||||
uint8_t parsed_key[DAT_KEY_BYTES];
|
||||
char url[DAT_URL_BYTES];
|
||||
char hex[65];
|
||||
char legacy_url[DAT_URL_BYTES];
|
||||
char link_text[DAT_LINK_BYTES];
|
||||
char probe_path[256];
|
||||
char secret_path[1024], discovery_hex[65];
|
||||
uint8_t discovery_key[32];
|
||||
struct stat secret_stat;
|
||||
FILE *probe;
|
||||
dat_link_t link, parsed_link;
|
||||
dat_checkout_t checkout;
|
||||
dat_url_t parsed_url;
|
||||
dat_path_info_t path_info;
|
||||
int serve_listing;
|
||||
uint64_t old_version, new_version, sync_version;
|
||||
sleep_feed_t remote_feed;
|
||||
uint8_t remote_key[32] = {0x42}, remote_proof[] = {0x01, 0x02};
|
||||
uint8_t remote_value[] = {0x61, 0x00, 0x62}, remote_value3[] = {0x63};
|
||||
uint8_t *remote_read;
|
||||
static const uint8_t old_text[] = "old archive value\n";
|
||||
static const uint8_t new_text[] = "new archive value\n";
|
||||
size_t input_len = DAT_BLOCK_BYTES * 2 + 123;
|
||||
size_t output_len, i;
|
||||
|
||||
assert(mkdtemp(directory) != NULL);
|
||||
assert(mkdtemp(secrets_directory) != NULL);
|
||||
memset(&remote_feed, 0, sizeof(remote_feed));
|
||||
remote_feed.remote_only = 1;
|
||||
memcpy(remote_feed.keypair.pubkey, remote_key, sizeof(remote_key));
|
||||
assert(sleep_feed_append_remote(&remote_feed, 0, remote_value, sizeof(remote_value),
|
||||
remote_proof, sizeof(remote_proof)) == 0);
|
||||
assert(sleep_feed_read(&remote_feed, 0, &remote_read, &output_len) == 0);
|
||||
assert(output_len == sizeof(remote_value) && !memcmp(remote_read, remote_value, output_len));
|
||||
free(remote_read);
|
||||
assert(sleep_feed_append_remote(&remote_feed, 3, remote_value3, sizeof(remote_value3),
|
||||
remote_proof, sizeof(remote_proof)) == 0);
|
||||
assert(sleep_feed_length(&remote_feed) == 4);
|
||||
assert(sleep_feed_has(&remote_feed, 2) == 0);
|
||||
assert(sleep_feed_read(&remote_feed, 2, &remote_read, &output_len) != 0);
|
||||
assert(sleep_feed_read(&remote_feed, 3, &remote_read, &output_len) == 0);
|
||||
assert(output_len == sizeof(remote_value3) && remote_read[0] == remote_value3[0]);
|
||||
free(remote_read);
|
||||
assert(sleep_feed_append(&remote_feed, remote_value, sizeof(remote_value)) != 0);
|
||||
assert(sleep_feed_flush(&remote_feed) != 0);
|
||||
sleep_feed_close(&remote_feed);
|
||||
input = malloc(input_len);
|
||||
assert(input != NULL);
|
||||
for (i = 0; i < input_len; i++)
|
||||
input[i] = (uint8_t)((i * 31u + 7u) & 0xffu);
|
||||
|
||||
if (dat_archive_create(&archive, directory) != 0) {
|
||||
perror("dat_archive_create");
|
||||
return 1;
|
||||
}
|
||||
assert(dat_archive_save_secrets(&archive, secrets_directory) == 0);
|
||||
assert(sleep_compute_discovery_key(archive.metadata_key, discovery_key) == 0);
|
||||
assert(dat_key_to_hex(discovery_key, discovery_hex) == 0);
|
||||
snprintf(secret_path, sizeof(secret_path), "%s/%.2s/%s", secrets_directory,
|
||||
discovery_hex, discovery_hex + 2);
|
||||
assert(stat(secret_path, &secret_stat) == 0 && secret_stat.st_size == 64);
|
||||
assert(sleep_compute_discovery_key(archive.content_key, discovery_key) == 0);
|
||||
assert(dat_key_to_hex(discovery_key, discovery_hex) == 0);
|
||||
snprintf(secret_path, sizeof(secret_path), "%s/%.2s/%s", secrets_directory,
|
||||
discovery_hex, discovery_hex + 2);
|
||||
assert(stat(secret_path, &secret_stat) == 0 && secret_stat.st_size == 64);
|
||||
assert(dat_archive_open_writable(&reopened, directory, secrets_directory) == 0);
|
||||
dat_archive_close(&reopened);
|
||||
assert(dat_key_to_url(archive.metadata_key, url) == 0);
|
||||
assert(strlen(url) == 70);
|
||||
assert(strncmp(url, "dat://", 6) == 0);
|
||||
assert(dat_url_to_key(url, parsed_key) == 0);
|
||||
assert(memcmp(parsed_key, archive.metadata_key, DAT_KEY_BYTES) == 0);
|
||||
assert(dat_key_to_compact_url(archive.metadata_key, url) == 0);
|
||||
assert(strlen(url) < 70);
|
||||
assert(dat_url_to_key(url, parsed_key) == 0);
|
||||
assert(memcmp(parsed_key, archive.metadata_key, DAT_KEY_BYTES) == 0);
|
||||
assert(dat_key_to_hex(archive.metadata_key, hex) == 0);
|
||||
snprintf(legacy_url, sizeof(legacy_url), "dat://%s", hex);
|
||||
assert(dat_url_to_key(legacy_url, parsed_key) == 0);
|
||||
assert(memcmp(parsed_key, archive.metadata_key, DAT_KEY_BYTES) == 0);
|
||||
assert(dat_archive_append_directory(&archive, "/docs", 0755, 1) == 0);
|
||||
assert(dat_archive_append_file(&archive, "/docs/sample.bin", input,
|
||||
input_len, 0644, 2) == 0);
|
||||
assert(dat_archive_append_symlink(&archive, "/latest",
|
||||
"/docs/sample.bin", 3) == 0);
|
||||
assert(dat_archive_append_file(&archive, "/versioned.txt", old_text,
|
||||
sizeof(old_text) - 1, 0644, 4) == 0);
|
||||
old_version = dat_archive_version(&archive);
|
||||
assert(dat_archive_append_file(&archive, "/versioned.txt", new_text,
|
||||
sizeof(new_text) - 1, 0644, 5) == 0);
|
||||
new_version = dat_archive_version(&archive);
|
||||
assert(new_version == old_version + 1);
|
||||
assert(dat_checkout_init(&checkout, &archive, old_version) == 0);
|
||||
assert(dat_checkout_find(&checkout, "/versioned.txt", &entry) == 0);
|
||||
assert(dat_checkout_read(&checkout, &entry, &output, &output_len) == 0);
|
||||
assert(output_len == sizeof(old_text) - 1);
|
||||
assert(memcmp(output, old_text, output_len) == 0);
|
||||
free(output);
|
||||
assert(dat_archive_find(&archive, "/versioned.txt", &entry) == 0);
|
||||
assert(dat_archive_read_file(&archive, &entry, &output, &output_len) == 0);
|
||||
assert(output_len == sizeof(new_text) - 1);
|
||||
assert(memcmp(output, new_text, output_len) == 0);
|
||||
free(output);
|
||||
changes_seen = 0;
|
||||
last_change_version = 0;
|
||||
assert(dat_archive_changes(&archive, old_version, new_version,
|
||||
count_change, NULL) == 0);
|
||||
assert(changes_seen == 1 && last_change_version == new_version);
|
||||
memset(&link, 0, sizeof(link));
|
||||
memcpy(link.key, archive.metadata_key, DAT_KEY_BYTES);
|
||||
link.has_version = 1;
|
||||
link.version = old_version;
|
||||
strcpy(link.path, "versioned.txt");
|
||||
assert(dat_link_format(&link, link_text, sizeof(link_text)) == 0);
|
||||
assert(strstr(link_text, "+") != NULL);
|
||||
assert(strlen(link_text) >= 70);
|
||||
assert(dat_link_parse(link_text, &parsed_link) == 0);
|
||||
assert(parsed_link.has_version && parsed_link.version == old_version);
|
||||
assert(!strcmp(parsed_link.path, "versioned.txt"));
|
||||
assert(!memcmp(parsed_link.key, archive.metadata_key, DAT_KEY_BYTES));
|
||||
assert(dat_link_format_compact(&link, link_text, sizeof(link_text)) == 0);
|
||||
assert(dat_link_parse(link_text, &parsed_link) == 0);
|
||||
assert(!memcmp(parsed_link.key, archive.metadata_key, DAT_KEY_BYTES));
|
||||
assert(dat_url_parse("dat://protozoa.nz/path%20name?version=7&stat=1", &parsed_url) == 0);
|
||||
assert(!strcmp(parsed_url.host, "protozoa.nz"));
|
||||
assert(!strcmp(parsed_url.path, "/path name"));
|
||||
assert(parsed_url.has_version && parsed_url.version == 7 && parsed_url.wants_stat);
|
||||
assert(dat_url_parse("dat://protozoa.nz+8/dir//", &parsed_url) == 0);
|
||||
assert(parsed_url.has_version && parsed_url.version == 8 && parsed_url.force_directory_listing);
|
||||
assert(dat_url_parse("dat://protozoa.nz/dir/../page", &parsed_url) == 0);
|
||||
assert(!strcmp(parsed_url.path, "/page"));
|
||||
assert(dat_archive_stat_at(&archive, old_version, "/docs", &path_info) == 0);
|
||||
assert(path_info.exists && path_info.is_directory);
|
||||
directory_entries_seen = 0;
|
||||
assert(dat_archive_readdir_at(&archive, old_version, "/docs", count_directory_entry, NULL) == 0);
|
||||
assert(directory_entries_seen == 1);
|
||||
assert(dat_archive_resolve_at(&archive, old_version, "/docs/",
|
||||
0, &path_info, &serve_listing) == 0);
|
||||
assert(serve_listing);
|
||||
assert(dat_archive_find(&archive, "/docs/sample.bin", &entry) == 0);
|
||||
assert(entry.block_count == 3);
|
||||
assert(entry.size == input_len);
|
||||
assert(dat_archive_read_file(&archive, &entry, &output, &output_len) == 0);
|
||||
assert(output_len == input_len);
|
||||
assert(memcmp(input, output, input_len) == 0);
|
||||
free(output);
|
||||
dat_archive_close(&archive);
|
||||
|
||||
assert(dat_archive_open(&reopened, directory) == 0);
|
||||
assert(dat_archive_find(&reopened, "/docs/sample.bin", &entry) == 0);
|
||||
assert(dat_archive_read_file(&reopened, &entry, &output, &output_len) == 0);
|
||||
assert(output_len == input_len);
|
||||
assert(memcmp(input, output, input_len) == 0);
|
||||
free(output);
|
||||
free(input);
|
||||
assert(sleep_verify_dat(reopened.dat_path) == 0);
|
||||
assert(dat_archive_append_file(&reopened, "/page.html",
|
||||
(const uint8_t *)"<h1>page</h1>", 13, 0644, 6) == 0);
|
||||
assert(dat_archive_resolve_at(&reopened, dat_archive_version(&reopened), "/page",
|
||||
0, &path_info, &serve_listing) == 0);
|
||||
assert(!serve_listing && !strcmp(path_info.entry.path, "/page.html"));
|
||||
assert(!strcmp(dat_content_type(path_info.entry.path), "text/html"));
|
||||
dat_archive_close(&reopened);
|
||||
assert(mkdtemp(sync_directory) != NULL);
|
||||
assert(dat_archive_create(&sync_archive, sync_directory) == 0);
|
||||
assert(snprintf(probe_path, sizeof(probe_path), "%s/mtime-probe", sync_directory) > 0);
|
||||
probe = fopen(probe_path, "wb");
|
||||
assert(probe != NULL);
|
||||
assert(fwrite("mtime", 1, 5, probe) == 5);
|
||||
assert(fclose(probe) == 0);
|
||||
assert(dat_archive_sync_path(&sync_archive, sync_directory, "") == 0);
|
||||
assert(dat_archive_find(&sync_archive, "mtime-probe", &entry) == 0);
|
||||
assert(entry.mtime > UINT64_C(1000000000000)); /* Hyperdrive stores epoch milliseconds. */
|
||||
sync_version = dat_archive_version(&sync_archive);
|
||||
assert(dat_archive_sync_path(&sync_archive, sync_directory, "") == 0);
|
||||
assert(dat_archive_version(&sync_archive) == sync_version); /* unchanged paths do not append */
|
||||
dat_archive_close(&sync_archive);
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
#include "base32768.h"
|
||||
#include "dat.h"
|
||||
#include <assert.h>
|
||||
#include <errno.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
int main(void)
|
||||
{
|
||||
static const uint8_t hello[] = {104,101,108,108,111,32,119,111,114,108,100};
|
||||
static const char encoded_hello[] = "媒腻㐤┖ꈳ埳";
|
||||
uint8_t input[257], decoded[257];
|
||||
char encoded[1200];
|
||||
char archive_url[DAT_URL_BYTES];
|
||||
char compact_link[DAT_LINK_BYTES];
|
||||
dat_url_t parsed_url;
|
||||
static const char historical_address[] =
|
||||
"dat://59d8099399510e23a3c693fb5887b7bba1586f4117f7ef1c5872622ada0c1181";
|
||||
static const char compact_historical_address[] =
|
||||
"dat://卌Ꭴ馊㜢䍞䂏鴑╗萰类躂ꜞ齂蠩檵聬♠ʟ";
|
||||
uint8_t archive_key[DAT_KEY_BYTES];
|
||||
size_t length, i, decoded_length;
|
||||
assert(base32768_encode(hello, sizeof(hello), encoded, sizeof(encoded)) == 0);
|
||||
assert(!strcmp(encoded, encoded_hello));
|
||||
assert(base32768_decode(encoded_hello, decoded, sizeof(decoded), &decoded_length) == 0);
|
||||
assert(decoded_length == sizeof(hello) && !memcmp(decoded, hello, sizeof(hello)));
|
||||
for (length = 0; length <= sizeof(input); length++) {
|
||||
for (i = 0; i < length; i++) input[i] = (uint8_t)(i * 37u + length);
|
||||
assert(base32768_encode(input, length, encoded, sizeof(encoded)) == 0);
|
||||
assert(base32768_decode(encoded, decoded, sizeof(decoded), &decoded_length) == 0);
|
||||
assert(decoded_length == length && !memcmp(decoded, input, length));
|
||||
}
|
||||
assert(base32768_decode("A", decoded, sizeof(decoded), &decoded_length) == -1);
|
||||
assert(errno == EILSEQ);
|
||||
assert(dat_url_to_key(historical_address, archive_key) == 0);
|
||||
assert(dat_key_to_compact_url(archive_key, archive_url) == 0);
|
||||
assert(!strcmp(archive_url, compact_historical_address));
|
||||
snprintf(compact_link, sizeof(compact_link), "%s+7/readme.txt", archive_url);
|
||||
assert(dat_url_parse(compact_link, &parsed_url) == 0);
|
||||
assert(parsed_url.has_version && parsed_url.version == 7);
|
||||
assert(!strcmp(parsed_url.host, compact_historical_address + 6));
|
||||
assert(dat_url_to_key(parsed_url.host, decoded) == 0);
|
||||
assert(!memcmp(decoded, archive_key, sizeof(archive_key)));
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
#include "sleep_bitfield.h"
|
||||
#include <assert.h>
|
||||
#include <string.h>
|
||||
int main(void){sleep_bitfield_t a,b;unsigned char encoded[sizeof(a)];sleep_bitfield_clear(&a);assert(sizeof(a)==3584);assert(sleep_bitfield_set_data(&a,0,1)==0);assert(sleep_bitfield_set_data(&a,9,1)==0);assert(a.data[0]==0x80&&a.data[1]==0x40);assert(sleep_bitfield_get_data(&a,9)==1);assert(sleep_bitfield_set_tree(&a,3,1)==0);assert(sleep_bitfield_encode(&a,encoded,sizeof(encoded))==0);memset(&b,0,sizeof(b));assert(sleep_bitfield_decode(&b,encoded,sizeof(encoded))==0);assert(sleep_bitfield_get_tree(&b,3)==1);return 0;}
|
||||
@@ -0,0 +1,100 @@
|
||||
#define _POSIX_C_SOURCE 200809L
|
||||
#include "cable.h"
|
||||
#include "sleep_crypto.h"
|
||||
#include <assert.h>
|
||||
#include <errno.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
#include <sodium.h>
|
||||
|
||||
int main(void)
|
||||
{
|
||||
uint8_t key[32], parsed[32], plain[900], sealed[1000], opened[900];
|
||||
char address[160],identity_dir[]="/tmp/cable-test-XXXXXX",identity_path[256];size_t n,sealed_n,opened_n;enum cable_address_scheme scheme;
|
||||
sleep_keypair_t alice,bob;cable_chat_message_t message,decoded;
|
||||
assert(sleep_crypto_init()==0);
|
||||
memset(key,0xa5,sizeof(key));
|
||||
assert(cable_address_format(key,CABLE_ADDRESS_CABLE,address,sizeof(address))==0);
|
||||
assert(!strncmp(address,"cable://",8));
|
||||
assert(cable_address_parse(address,parsed,&scheme)==0&&scheme==CABLE_ADDRESS_CABLE);
|
||||
assert(!memcmp(key,parsed,32));
|
||||
assert(cable_address_parse("hyper://aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",parsed,&scheme)==0);
|
||||
assert(scheme==CABLE_ADDRESS_HYPER);
|
||||
assert(cable_address_parse("dat://aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",parsed,&scheme)<0);
|
||||
{
|
||||
cable_room_address_t room,parsed_room;
|
||||
char room_text[160];
|
||||
memset(&room,0,sizeof(room));memset(room.key,0x11,32);memset(room.admin_key,0x22,32);room.has_admin=1;
|
||||
assert(cable_room_address_format(&room,CABLE_ADDRESS_CABAL,room_text,sizeof(room_text))==0);
|
||||
assert(strstr(room_text,"?admin=")!=NULL);
|
||||
assert(cable_room_address_parse(room_text,&parsed_room)==0);
|
||||
assert(parsed_room.has_admin&&memcmp(room.key,parsed_room.key,32)==0&&memcmp(room.admin_key,parsed_room.admin_key,32)==0);
|
||||
}
|
||||
assert(mkdtemp(identity_dir)!=NULL);
|
||||
assert(snprintf(identity_path,sizeof(identity_path),"%s/user-feed.key",identity_dir)>0);
|
||||
{
|
||||
sleep_keypair_t first,second;struct stat info;
|
||||
assert(cable_user_identity_load_or_create(identity_path,&first)==0);
|
||||
assert(stat(identity_path,&info)==0&&(info.st_mode&077)==0);
|
||||
assert(cable_user_address_format(first.pubkey,CABLE_ADDRESS_CABLE,address,sizeof(address))==0);
|
||||
assert(cable_user_identity_load_or_create(identity_path,&second)==0);
|
||||
assert(memcmp(first.pubkey,second.pubkey,32)==0&&memcmp(first.seckey,second.seckey,64)==0);
|
||||
sodium_memzero(&first,sizeof(first));sodium_memzero(&second,sizeof(second));
|
||||
}
|
||||
assert(unlink(identity_path)==0);assert(rmdir(identity_dir)==0);
|
||||
{
|
||||
uint8_t room_a[32]={1},room_b[32]={2};char home[]="/tmp/cable-home-XXXXXX";
|
||||
char path_a[512],path_b[512],dir_a[512],dir_b[512],feed_dir[520],rooms[512],base[512];
|
||||
sleep_keypair_t a1,a2,b;
|
||||
sleep_feed_t feed;
|
||||
uint8_t user_key[32],*block=NULL;
|
||||
size_t block_length=0;
|
||||
assert(mkdtemp(home)!=NULL);
|
||||
assert(cable_user_identity_path(home,room_a,path_a,sizeof(path_a))==0);
|
||||
assert(cable_user_identity_path(home,room_b,path_b,sizeof(path_b))==0);
|
||||
assert(strcmp(path_a,path_b)!=0);
|
||||
assert(cable_user_identity_load_or_create(path_a,&a1)==0);
|
||||
assert(cable_user_identity_load_or_create(path_a,&a2)==0);
|
||||
assert(cable_user_identity_load_or_create(path_b,&b)==0);
|
||||
assert(memcmp(a1.pubkey,a2.pubkey,32)==0&&memcmp(a1.pubkey,b.pubkey,32)!=0);
|
||||
assert(cable_user_feed_publish(home,room_a,"about","cable-test-user",NULL,NULL,user_key)==0);
|
||||
assert(!memcmp(user_key,a1.pubkey,sizeof(user_key)));
|
||||
assert(cable_user_feed_publish(home,room_a,"chat/text",NULL,"test-channel",
|
||||
"Cabal-shaped message",user_key)==0);
|
||||
assert(cable_user_feed_open(home,room_a,&a2,&feed)==0);
|
||||
assert(sleep_feed_length(&feed)==2);
|
||||
assert(sleep_feed_read(&feed,1,&block,&block_length)==0);
|
||||
assert(strstr((const char *)block,"\"type\":\"chat/text\"")!=NULL);
|
||||
assert(strstr((const char *)block,"Cabal-shaped message")!=NULL);
|
||||
free(block);block=NULL;sleep_feed_close(&feed);
|
||||
strcpy(dir_a,path_a);*strrchr(dir_a,'/')='\0';strcpy(dir_b,path_b);*strrchr(dir_b,'/')='\0';
|
||||
snprintf(feed_dir,sizeof(feed_dir),"%s/0",dir_a);
|
||||
{
|
||||
static const char *aliases[]={"feed.tree","feed.signatures","feed.bitfield","feed.data","feed.key","secret_key","localname"};
|
||||
static const char *storage[]={"tree","signatures","bitfield","data","key"};
|
||||
char file[600];size_t i;
|
||||
for(i=0;i<sizeof(aliases)/sizeof(aliases[0]);i++){snprintf(file,sizeof(file),"%s/%s",feed_dir,aliases[i]);assert(unlink(file)==0);}
|
||||
for(i=0;i<sizeof(storage)/sizeof(storage[0]);i++){snprintf(file,sizeof(file),"%s/%s",feed_dir,storage[i]);assert(unlink(file)==0);}
|
||||
}
|
||||
assert(rmdir(feed_dir)==0);
|
||||
assert(unlink(path_a)==0&&unlink(path_b)==0);assert(rmdir(dir_a)==0&&rmdir(dir_b)==0);
|
||||
snprintf(rooms,sizeof(rooms),"%s/.cable/rooms",home);assert(rmdir(rooms)==0);
|
||||
snprintf(base,sizeof(base),"%s/.cable",home);assert(rmdir(base)==0);assert(rmdir(home)==0);
|
||||
sodium_memzero(&a1,sizeof(a1));sodium_memzero(&a2,sizeof(a2));sodium_memzero(&b,sizeof(b));
|
||||
}
|
||||
memset(&message,0,sizeof(message));message.timestamp_ms=123456789;strcpy(message.text,"hello cable");message.text_length=strlen(message.text);
|
||||
assert(cable_chat_message_encode(&message,plain,sizeof(plain),&n)==0);
|
||||
assert(cable_chat_message_decode(&decoded,plain,n)==0);
|
||||
assert(decoded.timestamp_ms==message.timestamp_ms&&!strcmp(decoded.text,message.text));
|
||||
assert(sleep_gen_keypair(&alice)==0&&sleep_gen_keypair(&bob)==0);
|
||||
assert(cable_chat_seal(bob.pubkey,plain,n,sealed,sizeof(sealed),&sealed_n)==0);
|
||||
assert(cable_chat_open(&bob,sealed,sealed_n,opened,sizeof(opened),&opened_n)==0);
|
||||
assert(opened_n==n&&!memcmp(opened,plain,n));
|
||||
assert(cable_chat_open(&alice,sealed,sealed_n,opened,sizeof(opened),&opened_n)<0&&errno==EACCES);
|
||||
sealed[0]^=0x80;
|
||||
assert(cable_chat_open(&bob,sealed,sealed_n,opened,sizeof(opened),&opened_n)<0);
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
#define _POSIX_C_SOURCE 200809L
|
||||
#define main cable_thang_embedded_main
|
||||
#include "../src/ddat_gui.c"
|
||||
#undef main
|
||||
|
||||
#include <assert.h>
|
||||
#include <sys/select.h>
|
||||
#include <time.h>
|
||||
|
||||
static void announce_new_messages(gui_state_t *state, size_t *seen)
|
||||
{
|
||||
uint8_t self[32];
|
||||
size_t i;
|
||||
if (cable_hex_key(state->cable_feed_key, self)) return;
|
||||
for (i = *seen; i < state->cable_line_count; i++) {
|
||||
const cable_room_event_t *event = &state->cable_lines[i].event;
|
||||
if (event->type == CABLE_ROOM_TEXT && memcmp(event->sender, self, sizeof(self)) != 0) {
|
||||
printf("RX\t%s\t%s\n", event->nick, event->text);
|
||||
fflush(stdout);
|
||||
}
|
||||
}
|
||||
*seen = state->cable_line_count;
|
||||
}
|
||||
|
||||
static void read_command(gui_state_t *state, char *line, int *running)
|
||||
{
|
||||
size_t length = strlen(line);
|
||||
while (length && (line[length - 1] == '\n' || line[length - 1] == '\r')) line[--length] = '\0';
|
||||
if (!strcmp(line, "QUIT")) { *running = 0; return; }
|
||||
if (!strncmp(line, "SEND ", 5)) {
|
||||
if (strlen(line + 5) > CABLE_CHAT_TEXT_BYTES) { puts("ERR message-too-long"); fflush(stdout); return; }
|
||||
strcpy(state->cable_input, line + 5);
|
||||
if (cable_room_submit(state)) { puts("ERR send-failed"); fflush(stdout); }
|
||||
else { puts("SENT"); fflush(stdout); }
|
||||
return;
|
||||
}
|
||||
if (!strncmp(line, "NICK ", 5)) {
|
||||
if (strlen(line + 5) > CABLE_ROOM_NICK_BYTES) { puts("ERR nick-too-long"); fflush(stdout); return; }
|
||||
snprintf(state->cable_input, sizeof(state->cable_input), "/nick %s", line + 5);
|
||||
if (cable_room_submit(state)) { puts("ERR nick-failed"); fflush(stdout); }
|
||||
else { puts("NICKED"); fflush(stdout); }
|
||||
return;
|
||||
}
|
||||
if (!strncmp(line, "JOIN ", 5)) {
|
||||
if (strlen(line + 5) > CABLE_ROOM_CHANNEL_BYTES) { puts("ERR channel-too-long"); fflush(stdout); return; }
|
||||
snprintf(state->cable_input, sizeof(state->cable_input), "/join %s", line + 5);
|
||||
if (cable_room_submit(state)) { puts("ERR join-failed"); fflush(stdout); }
|
||||
else { puts("JOINED"); fflush(stdout); }
|
||||
return;
|
||||
}
|
||||
puts("ERR unknown-command");
|
||||
fflush(stdout);
|
||||
}
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
static gui_state_t state;
|
||||
SDL_Window *window;
|
||||
SDL_Renderer *renderer;
|
||||
char input[2048];
|
||||
size_t seen_lines = 0;
|
||||
int running = 1, ready_reported = 0, peer_reported = 0;
|
||||
if (argc != 2) { fputs("usage: test_cable_thang <cabal-room-address>\n", stderr); return 2; }
|
||||
if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_TIMER) != 0) { fprintf(stderr, "SDL init: %s\n", SDL_GetError()); return 1; }
|
||||
window = SDL_CreateWindow("cable-thang live test", SDL_WINDOWPOS_UNDEFINED,
|
||||
SDL_WINDOWPOS_UNDEFINED, 1000, 640,
|
||||
SDL_WINDOW_HIDDEN | SDL_WINDOW_RESIZABLE);
|
||||
if (!window) { fprintf(stderr, "SDL window: %s\n", SDL_GetError()); SDL_Quit(); return 1; }
|
||||
renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_SOFTWARE);
|
||||
if (!renderer) {
|
||||
fprintf(stderr, "SDL renderer: %s\n", SDL_GetError());
|
||||
SDL_DestroyWindow(window); SDL_Quit(); return 1;
|
||||
}
|
||||
state.cable_thang_mode = 1;
|
||||
if (cable_thang_room_init(&state, argv[1]) || cable_thang_network_start(&state)) {
|
||||
perror("Cable SDL room startup"); SDL_DestroyRenderer(renderer); SDL_DestroyWindow(window); SDL_Quit(); return 1;
|
||||
}
|
||||
while (running) {
|
||||
fd_set reads;
|
||||
struct timeval tick = {0, 10000};
|
||||
int result;
|
||||
SDL_Event event;
|
||||
while (SDL_PollEvent(&event)) if (event.type == SDL_QUIT) running = 0;
|
||||
network_pump(&state, SDL_GetTicks());
|
||||
if (!ready_reported && state.cable_feed_key[0]) {
|
||||
printf("C_READY %s\n", state.cable_feed_key); fflush(stdout); ready_reported = 1;
|
||||
}
|
||||
if (!peer_reported && state.connected_peers) { puts("PEER_UP"); fflush(stdout); peer_reported = 1; }
|
||||
announce_new_messages(&state, &seen_lines);
|
||||
cable_thang_draw(renderer, 1000, 640, &state);
|
||||
SDL_RenderPresent(renderer);
|
||||
FD_ZERO(&reads); FD_SET(STDIN_FILENO, &reads);
|
||||
result = select(STDIN_FILENO + 1, &reads, NULL, NULL, &tick);
|
||||
if (result > 0 && FD_ISSET(STDIN_FILENO, &reads)) {
|
||||
if (!fgets(input, sizeof(input), stdin)) running = 0;
|
||||
else read_command(&state, input, &running);
|
||||
} else if (result < 0 && errno != EINTR) running = 0;
|
||||
}
|
||||
network_stop(&state);
|
||||
SDL_DestroyRenderer(renderer);
|
||||
SDL_DestroyWindow(window);
|
||||
SDL_Quit();
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
dat=${1:-build/linux/dat}
|
||||
ddat=${2:-build/linux/ddat}
|
||||
root=$(mktemp -d /tmp/datgpt-cli-parity.XXXXXX)
|
||||
trap 'rm -rf "$root"' EXIT HUP INT TERM
|
||||
mkdir -p "$root/home" "$root/from-dat" "$root/from-ddat"
|
||||
export HOME="$root/home"
|
||||
|
||||
test "$("$dat" --version)" = "$("$ddat" --version)"
|
||||
"$dat" --help | grep -q '^dat '
|
||||
"$ddat" --help | grep -q '^ddat '
|
||||
"$dat" --help | sed 's/ddat/CLI/g; s/dat/CLI/g' > "$root/dat-help"
|
||||
"$ddat" --help | sed 's/ddat/CLI/g; s/dat/CLI/g' > "$root/ddat-help"
|
||||
cmp "$root/dat-help" "$root/ddat-help"
|
||||
|
||||
printf 'dat wrote this first\n' > "$root/from-dat/shared.txt"
|
||||
"$dat" create "$root/from-dat" --title 'created by dat' > "$root/dat-url"
|
||||
"$dat" sync "$root/from-dat" >/dev/null
|
||||
"$ddat" status "$root/from-dat" | grep -q "$(cat "$root/dat-url")"
|
||||
"$ddat" files "$root/from-dat" | grep -q shared.txt
|
||||
old_dat_version=$("$ddat" log "$root/from-dat" | awk '/shared.txt/{print $1; exit}')
|
||||
printf 'ddat updated this second\n' > "$root/from-dat/shared.txt"
|
||||
"$ddat" sync "$root/from-dat" >/dev/null
|
||||
test "$("$dat" cat "$old_dat_version" /shared.txt "$root/from-dat")" = 'dat wrote this first'
|
||||
"$dat" verify "$root/from-dat" >/dev/null
|
||||
"$dat" url "$old_dat_version" /shared.txt "$root/from-dat" | \
|
||||
grep -Eq '^dat://[0-9a-f]{64}\+[0-9]+/shared.txt$'
|
||||
"$ddat" status "$root/from-dat" | grep -q "$(cat "$root/dat-url")"
|
||||
|
||||
printf 'ddat created this first\n' > "$root/from-ddat/shared.txt"
|
||||
"$ddat" create "$root/from-ddat" --title 'created by ddat' > "$root/ddat-url"
|
||||
"$ddat" sync "$root/from-ddat" >/dev/null
|
||||
"$dat" status "$root/from-ddat" | grep -q "$(cat "$root/ddat-url")"
|
||||
"$dat" files "$root/from-ddat" | grep -q shared.txt
|
||||
old_ddat_version=$("$dat" log "$root/from-ddat" | awk '/shared.txt/{print $1; exit}')
|
||||
printf 'dat updated this second\n' > "$root/from-ddat/shared.txt"
|
||||
"$dat" sync "$root/from-ddat" >/dev/null
|
||||
test "$("$ddat" cat "$old_ddat_version" /shared.txt "$root/from-ddat")" = 'ddat created this first'
|
||||
"$ddat" verify "$root/from-ddat" >/dev/null
|
||||
"$dat" status "$root/from-ddat" | grep -q "$(cat "$root/ddat-url")"
|
||||
|
||||
echo 'dat/ddat shared archive, address, read, update, and history: PASS'
|
||||
@@ -0,0 +1,234 @@
|
||||
#include "cesspool.h"
|
||||
#include "dat_wire.h"
|
||||
#include "sleep_crypto.h"
|
||||
#include "sleep.h"
|
||||
|
||||
#include <assert.h>
|
||||
#include <errno.h>
|
||||
#ifdef _WIN32
|
||||
#include <winsock2.h>
|
||||
#include <ws2tcpip.h>
|
||||
#else
|
||||
#include <arpa/inet.h>
|
||||
#include <netinet/in.h>
|
||||
#endif
|
||||
#include <sodium.h>
|
||||
#include <string.h>
|
||||
#ifndef _WIN32
|
||||
#include <sys/socket.h>
|
||||
#endif
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
typedef SOCKET test_socket_t;
|
||||
#else
|
||||
#include <unistd.h>
|
||||
typedef int test_socket_t;
|
||||
#endif
|
||||
|
||||
static uint16_t free_udp_port(void)
|
||||
{
|
||||
test_socket_t fd=socket(AF_INET,SOCK_DGRAM,0);struct sockaddr_in address;
|
||||
uint16_t port;
|
||||
#ifdef _WIN32
|
||||
int length=sizeof(address);
|
||||
assert(fd!=INVALID_SOCKET);
|
||||
#else
|
||||
socklen_t length=sizeof(address);
|
||||
assert(fd>=0);
|
||||
#endif
|
||||
memset(&address,0,sizeof(address));address.sin_family=AF_INET;address.sin_addr.s_addr=htonl(INADDR_LOOPBACK);
|
||||
assert(bind(fd,(struct sockaddr*)&address,sizeof(address))==0);assert(getsockname(fd,(struct sockaddr*)&address,&length)==0);
|
||||
port=ntohs(address.sin_port);
|
||||
#ifdef _WIN32
|
||||
closesocket(fd);
|
||||
#else
|
||||
close(fd);
|
||||
#endif
|
||||
return port;
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
static const uint8_t hypercore[] = "hypercore";
|
||||
static const uint8_t namespace[] = DAT_NETWORK_PROTOCOL;
|
||||
uint8_t key[32], expected[32], actual[32], nonce[24], input[513];
|
||||
uint8_t whole[513], chunked[513];
|
||||
memset(key, 0x42, sizeof(key));
|
||||
#ifdef _WIN32
|
||||
{
|
||||
WSADATA winsock;
|
||||
assert(WSAStartup(MAKEWORD(2, 2), &winsock) == 0);
|
||||
}
|
||||
#endif
|
||||
assert(crypto_generichash(expected, sizeof(expected), hypercore,
|
||||
sizeof(hypercore) - 1, key, sizeof(key)) == 0);
|
||||
assert(sleep_compute_discovery_key(key, actual) == 0);
|
||||
assert(!memcmp(expected, actual, sizeof(actual)));
|
||||
/* Generated by extra/dillo-dat/node_modules/hypercore-crypto for a
|
||||
* 32-byte feed key filled with 0x42 (historical Dat v6/v7 stack). */
|
||||
{
|
||||
static const uint8_t js_hypercore_discovery_key[32] = {
|
||||
0xb0,0xfa,0xa6,0x16,0x21,0x2d,0x99,0x23,
|
||||
0x9f,0x50,0x7a,0x68,0x8f,0x50,0xcd,0x60,
|
||||
0xe6,0x51,0x4d,0xfb,0x62,0xae,0xf4,0xfc,
|
||||
0xfc,0x82,0x59,0xd6,0x46,0x96,0x35,0x91
|
||||
};
|
||||
assert(!memcmp(actual, js_hypercore_discovery_key, sizeof(actual)));
|
||||
}
|
||||
memset(nonce, 0x19, sizeof(nonce));
|
||||
for (size_t i = 0; i < sizeof(input); i++) input[i] = (uint8_t)(i * 37u + 11u);
|
||||
assert(sleep_stream_xsalsa20_xor(whole, input, sizeof(input), nonce, key, 0) == 0);
|
||||
{
|
||||
size_t offset = 0;
|
||||
static const size_t pieces[] = {1, 63, 64, 65, 127, 193};
|
||||
for (size_t i = 0; i < sizeof(pieces) / sizeof(pieces[0]); i++) {
|
||||
assert(sleep_stream_xsalsa20_xor(chunked + offset, input + offset,
|
||||
pieces[i], nonce, key, offset) == 0);
|
||||
offset += pieces[i];
|
||||
}
|
||||
assert(offset == sizeof(input));
|
||||
}
|
||||
assert(!memcmp(whole, chunked, sizeof(whole)));
|
||||
{
|
||||
cesspool_t transactions;
|
||||
uint16_t id;
|
||||
memset(&transactions, 0, sizeof(transactions));
|
||||
assert(cesspool_transaction_begin(&transactions, CESSPOOL_LOOKUP, key,
|
||||
100, &id) == 0);
|
||||
assert(cesspool_transaction_finish(&transactions, id) == 0);
|
||||
assert(cesspool_transaction_begin(&transactions, CESSPOOL_PING, key,
|
||||
10, &id) == 0);
|
||||
assert(cesspool_tick(&transactions, 10) == 0);
|
||||
errno = 0;
|
||||
assert(cesspool_transaction_finish(&transactions, id) != 0 && errno == ENOENT);
|
||||
}
|
||||
assert(crypto_generichash(expected, sizeof(expected), namespace,
|
||||
sizeof(namespace) - 1, key, sizeof(key)) == 0);
|
||||
assert(cesspool_discovery_key(key, actual) == 0);
|
||||
assert(!memcmp(expected, actual, sizeof(actual)));
|
||||
{
|
||||
cesspool_t relay, publisher, lookup;
|
||||
cesspool_peer_t *peers[4];
|
||||
uint8_t relay_id[32] = {0x10}, publisher_id[32] = {0x11}, lookup_id[32] = {0x22};
|
||||
uint8_t topic[32] = {0x33}, archive_topic_a[32] = {0x34};
|
||||
uint8_t archive_topic_b[32] = {0x35};
|
||||
uint8_t packet[80];
|
||||
cesspool_message_t message, decoded;
|
||||
size_t packet_len;
|
||||
uint16_t relay_port = free_udp_port(), publisher_port = free_udp_port(), lookup_port = free_udp_port();
|
||||
assert(cesspool_init(&relay, relay_id) == 0);
|
||||
assert(cesspool_init(&publisher, publisher_id) == 0);
|
||||
assert(cesspool_init(&lookup, lookup_id) == 0);
|
||||
assert(cesspool_listen(&relay, "localhost", relay_port) == 0);
|
||||
assert(cesspool_listen(&publisher, "localhost", publisher_port) == 0);
|
||||
assert(cesspool_listen(&lookup, "localhost", lookup_port) == 0);
|
||||
assert(relay_port && publisher_port && lookup_port && relay_port != publisher_port && relay_port != lookup_port && publisher_port != lookup_port);
|
||||
assert(cesspool_set_bootstrap_server(&relay, 1) == 0);
|
||||
assert(cesspool_add_bootstrap(&publisher, "localhost", relay_port) == 0);
|
||||
assert(cesspool_add_bootstrap(&lookup, "127.0.0.1", relay_port) == 0);
|
||||
/* A third party's REPLY packet must not mark the configured node
|
||||
* online or inject forged rendezvous entries. */
|
||||
{
|
||||
cesspool_message_t forged;
|
||||
uint8_t forged_packet[80];
|
||||
size_t forged_length;
|
||||
struct sockaddr_in *target;
|
||||
dat_peer_t forged_target;
|
||||
memset(&forged, 0, sizeof(forged));
|
||||
forged.type = CESSPOOL_REPLY;
|
||||
memcpy(forged.peer_id, publisher_id, sizeof(publisher_id));
|
||||
memcpy(forged.topic, topic, sizeof(topic));
|
||||
forged.payload[0] = 0x1a; /* malformed one-byte peer record */
|
||||
forged.payload[1] = 0x01;
|
||||
forged.payload[2] = 0xff;
|
||||
forged.payload_len = 3;
|
||||
assert(cesspool_encode(&forged, forged_packet, sizeof(forged_packet), &forged_length) == 0);
|
||||
memset(&forged_target, 0, sizeof(forged_target));
|
||||
target = (struct sockaddr_in *)&forged_target.addr;
|
||||
target->sin_family = AF_INET;
|
||||
target->sin_addr.s_addr = htonl(INADDR_LOOPBACK);
|
||||
target->sin_port = htons(lookup_port);
|
||||
forged_target.addrlen = sizeof(*target);
|
||||
assert(datnet_send(&publisher.net, &forged_target, forged_packet, forged_length) == 0);
|
||||
assert(cesspool_poll(&lookup, 100) >= 0);
|
||||
assert(!lookup.bootstrap_reachable && lookup.announcement_count == 0);
|
||||
}
|
||||
assert(cesspool_join(&publisher, topic, publisher_port, 1, 0) == 0);
|
||||
assert(cesspool_poll(&relay, 100) >= 0);
|
||||
/* One client identity may announce a room and multiple archives. The
|
||||
* rendezvous counters must report one peer across three distinct topics. */
|
||||
assert(cesspool_join(&publisher, archive_topic_a, publisher_port, 1, 0) == 0);
|
||||
assert(cesspool_poll(&relay, 100) >= 0);
|
||||
assert(cesspool_join(&publisher, archive_topic_b, publisher_port, 1, 0) == 0);
|
||||
assert(cesspool_poll(&relay, 100) >= 0);
|
||||
assert(cesspool_announced_peer_count(&relay) == 1);
|
||||
assert(cesspool_announced_topic_count(&relay) == 3);
|
||||
assert(cesspool_join(&lookup, topic, lookup_port, 0, 1) == 0);
|
||||
assert(cesspool_poll(&relay, 100) >= 0);
|
||||
assert(cesspool_poll(&lookup, 100) >= 0);
|
||||
assert(lookup.bootstrap_reachable);
|
||||
assert(cesspool_topic_peers(&lookup, topic, peers, 4) >= 1);
|
||||
assert(!memcmp(peers[0]->public_key, publisher_id, 32));
|
||||
/* Expiring the lookup record must clear the client's Discovered row. */
|
||||
for (size_t a = 0; a < lookup.announcement_count; a++)
|
||||
lookup.announcements[a].last_seen = 0;
|
||||
lookup.bootstrap_count = 0;
|
||||
for (size_t t = 0; t < lookup.topic_count; t++)
|
||||
lookup.topics[t].refresh_at = UINT64_MAX;
|
||||
assert(cesspool_tick(&lookup, 0) == 0);
|
||||
assert(cesspool_topic_peers(&lookup, topic, peers, 4) == 0);
|
||||
assert(!cesspool_find_peer(&lookup, publisher_id)->discovered);
|
||||
assert(cesspool_leave(&publisher, archive_topic_a) == 0);
|
||||
assert(cesspool_poll(&relay, 100) >= 0);
|
||||
assert(cesspool_announced_peer_count(&relay) == 1);
|
||||
assert(cesspool_announced_topic_count(&relay) == 2);
|
||||
assert(cesspool_tick(&relay, UINT64_MAX) == 0);
|
||||
assert(cesspool_announced_peer_count(&relay) == 1); /* Fresh announcements keep it live. */
|
||||
memset(&message, 0, sizeof(message));
|
||||
message.type = CESSPOOL_PING;
|
||||
assert(cesspool_encode(&message, packet, sizeof(packet), &packet_len) == 0);
|
||||
assert(packet_len == 72);
|
||||
assert(cesspool_decode(&decoded, packet, packet_len) == 0);
|
||||
assert(decoded.type == CESSPOOL_PING);
|
||||
assert(cesspool_decode(&decoded, packet, packet_len + 1) != 0);
|
||||
memset(&message, 0, sizeof(message));
|
||||
message.type = CESSPOOL_REPLY;
|
||||
message.payload_len = sizeof(message.payload);
|
||||
assert(cesspool_encode(&message, packet, sizeof(packet), &packet_len) != 0);
|
||||
assert(cesspool_leave(&publisher, topic) == 0);
|
||||
assert(cesspool_leave(&publisher, archive_topic_b) == 0);
|
||||
assert(cesspool_leave(&lookup, topic) == 0);
|
||||
assert(cesspool_destroy(&relay) == 0);
|
||||
assert(cesspool_destroy(&publisher) == 0);
|
||||
assert(cesspool_destroy(&lookup) == 0);
|
||||
}
|
||||
{
|
||||
cesspool_t routing;
|
||||
cesspool_response_t response;
|
||||
cesspool_peer_t *closest[4];
|
||||
uint8_t local_prefix[2] = {192, 168}, local_id[32] = {0};
|
||||
struct sockaddr_in *address;
|
||||
memset(&response, 0, sizeof(response));
|
||||
response.peers_len = 6;
|
||||
response.peers[0] = 127; response.peers[3] = 1;
|
||||
response.peers[4] = 0x0c; response.peers[5] = 0xd2;
|
||||
response.local_peers_len = 4;
|
||||
response.local_peers[0] = 10; response.local_peers[1] = 1;
|
||||
response.local_peers[2] = 0x0c; response.local_peers[3] = 0xd2;
|
||||
assert(cesspool_init(&routing, local_id) == 0);
|
||||
assert(cesspool_process_response(&routing, &response, local_prefix) == 0);
|
||||
assert(routing.peer_count == 2 && routing.peers[1].local);
|
||||
address = (struct sockaddr_in *)&routing.peers[1].address.addr;
|
||||
assert(ntohs(address->sin_port) == 3282);
|
||||
assert(((uint8_t *)&address->sin_addr)[0] == 192 &&
|
||||
((uint8_t *)&address->sin_addr)[1] == 168 &&
|
||||
((uint8_t *)&address->sin_addr)[2] == 10 &&
|
||||
((uint8_t *)&address->sin_addr)[3] == 1);
|
||||
assert(cesspool_closest(&routing, local_id, closest, 4) == 2);
|
||||
assert(cesspool_destroy(&routing) == 0);
|
||||
}
|
||||
#ifdef _WIN32
|
||||
WSACleanup();
|
||||
#endif
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
#define _DEFAULT_SOURCE
|
||||
#include "dat_dns.h"
|
||||
|
||||
#include <assert.h>
|
||||
#include <errno.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
#ifdef _WIN32
|
||||
#include <direct.h>
|
||||
#include <io.h>
|
||||
#include <process.h>
|
||||
#define test_unlink _unlink
|
||||
#define test_rmdir _rmdir
|
||||
static int test_setenv(const char *name, const char *value, int overwrite)
|
||||
{
|
||||
if (!overwrite && getenv(name)) return 0;
|
||||
return _putenv_s(name, value);
|
||||
}
|
||||
static int test_unsetenv(const char *name) { return _putenv_s(name, ""); }
|
||||
static char *test_mkdtemp(char *path)
|
||||
{
|
||||
int i;
|
||||
for (i = 0; i < 1000; i++) {
|
||||
char candidate[512];
|
||||
int n = snprintf(candidate, sizeof(candidate), "%s-%ld-%d", path,
|
||||
(long)_getpid(), i);
|
||||
if (n < 0 || (size_t)n >= sizeof(candidate)) return NULL;
|
||||
if (_mkdir(candidate) == 0) { strcpy(path, candidate); return path; }
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
#else
|
||||
#include <unistd.h>
|
||||
#define test_unlink unlink
|
||||
#define test_rmdir rmdir
|
||||
#define test_setenv setenv
|
||||
#define test_unsetenv unsetenv
|
||||
#define test_mkdtemp mkdtemp
|
||||
#endif
|
||||
|
||||
static size_t add_txt(uint8_t *packet, size_t off, const char *text, size_t len)
|
||||
{
|
||||
packet[off++] = (uint8_t)len;
|
||||
memcpy(packet + off, text, len);
|
||||
return off + len;
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
uint8_t packet[160] = {0};
|
||||
uint8_t key[32];
|
||||
uint32_t ttl = 0;
|
||||
char value[] = "datkey=0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
|
||||
size_t off = 12;
|
||||
#ifdef _WIN32
|
||||
char temporary[512] = "build/windows/dat-dns-test";
|
||||
#else
|
||||
char temporary[] = "/tmp/dat-dns-test-XXXXXX";
|
||||
#endif
|
||||
char old_home[1024], resolved_name[256];
|
||||
uint8_t local_key[DAT_KEY_BYTES], resolved_key[DAT_KEY_BYTES];
|
||||
int had_home = getenv("HOME") != NULL;
|
||||
size_t i;
|
||||
if (had_home) snprintf(old_home, sizeof(old_home), "%s", getenv("HOME"));
|
||||
assert(test_mkdtemp(temporary) != NULL);
|
||||
assert(test_setenv("HOME", temporary, 1) == 0);
|
||||
packet[2] = 0x81; packet[3] = 0x80;
|
||||
packet[5] = 1; /* one question */
|
||||
packet[7] = 1; /* one answer */
|
||||
packet[off++] = 7; memcpy(packet + off, "example", 7); off += 7;
|
||||
packet[off++] = 3; memcpy(packet + off, "com", 3); off += 3;
|
||||
packet[off++] = 0;
|
||||
packet[off++] = 0; packet[off++] = 16; /* TXT question */
|
||||
packet[off++] = 0; packet[off++] = 1; /* IN */
|
||||
packet[off++] = 0xc0; packet[off++] = 0x0c;
|
||||
packet[off++] = 0; packet[off++] = 16; /* TXT */
|
||||
packet[off++] = 0; packet[off++] = 1; /* IN */
|
||||
packet[off++] = 0; packet[off++] = 0; packet[off++] = 0; packet[off++] = 60;
|
||||
packet[off++] = 0; packet[off++] = 73;
|
||||
off = add_txt(packet, off, value, 20);
|
||||
off = add_txt(packet, off, value + 20, strlen(value) - 20);
|
||||
assert(off == 12 + 13 + 4 + 2 + 2 + 2 + 4 + 2 + 73);
|
||||
assert(dat_dns_parse_answer(packet, (unsigned)off, key, &ttl) == 0);
|
||||
assert(ttl == 60);
|
||||
assert(key[0] == 0x01 && key[1] == 0x23 && key[31] == 0xef);
|
||||
packet[off - 73] = 0xff; /* TXT string length exceeds the RDATA boundary */
|
||||
assert(dat_dns_parse_answer(packet, (unsigned)off, key, &ttl) == -1);
|
||||
assert(errno == EPROTO);
|
||||
for (i = 0; i < sizeof(local_key); i++) local_key[i] = (uint8_t)(i * 9u + 3u);
|
||||
assert(dat_dns_name_set("Archive.Example.test", local_key) == 0);
|
||||
assert(dat_dns_name_resolve("archive.example.test", resolved_key) == 0);
|
||||
assert(!memcmp(local_key, resolved_key, sizeof(local_key)));
|
||||
assert(dat_dns_resolve("ARCHIVE.EXAMPLE.TEST", resolved_key, &ttl) == 0 && ttl == 0);
|
||||
assert(!memcmp(local_key, resolved_key, sizeof(local_key)));
|
||||
assert(dat_dns_name_for_key(local_key, resolved_name, sizeof(resolved_name)) == 0);
|
||||
assert(!strcmp(resolved_name, "archive.example.test"));
|
||||
assert(dat_dns_name_remove("archive.example.test") == 0);
|
||||
assert(dat_dns_name_resolve("archive.example.test", resolved_key) == -1);
|
||||
if (had_home) assert(test_setenv("HOME", old_home, 1) == 0);
|
||||
else assert(test_unsetenv("HOME") == 0);
|
||||
{
|
||||
char names[1200];
|
||||
snprintf(names, sizeof(names), "%s/.dat/dat-dns", temporary);
|
||||
test_unlink(names);
|
||||
snprintf(names, sizeof(names), "%s/.dat", temporary);
|
||||
test_rmdir(names);
|
||||
}
|
||||
test_rmdir(temporary);
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
dat_dpi=${1:-build/linux/dat.dpi}
|
||||
dat=${2:-build/linux/dat}
|
||||
socket_probe=${3:-build/linux/test_dpi_socket}
|
||||
tmp=$(mktemp -d /tmp/datgpt-dpi.XXXXXX)
|
||||
trap 'rm -rf "$tmp"' EXIT HUP INT TERM
|
||||
export HOME="$tmp/home"
|
||||
mkdir -p "$HOME/.dillo" "$tmp/archive/docs/adir" "$tmp/archive/docs/zdir"
|
||||
printf '12345 abcdef1234567890\n' > "$HOME/.dillo/dpid_comm_keys"
|
||||
"$dat" create "$tmp/archive" --yes >/dev/null
|
||||
archive_address=$("$dat" url / "$tmp/archive")
|
||||
printf '((title "Dillo parity") (description "Scheme metadata") (url "%s") (unsafe (begin (display "EXECUTED"))))\n' \
|
||||
"$archive_address" > "$tmp/archive/dat.scm"
|
||||
printf 'nested entry\n' > "$tmp/archive/docs/nested.txt"
|
||||
printf 'a entry\n' > "$tmp/archive/docs/a.txt"
|
||||
printf 'z entry\n' > "$tmp/archive/docs/z.txt"
|
||||
printf '<h1>historic setup page</h1>\n' > "$tmp/archive/index.html"
|
||||
"$dat" sync "$tmp/archive" >/dev/null
|
||||
old_version=$("$dat" log "$tmp/archive" | awk '$NF == "index.html" { version = $1 } END { print version }')
|
||||
old_address=$("$dat" url "$old_version" / "$tmp/archive")
|
||||
printf '<h1>current live page</h1>\n' > "$tmp/archive/index.html"
|
||||
"$dat" sync "$tmp/archive" >/dev/null
|
||||
new_version=$("$dat" log "$tmp/archive" | awk '$NF == "index.html" { version = $1 } END { print version }')
|
||||
new_address=$("$dat" url "$new_version" / "$tmp/archive")
|
||||
latest_address=$("$dat" url / "$tmp/archive")
|
||||
archive_root=${latest_address%/}
|
||||
listing_address="${archive_root}/docs/"
|
||||
parent_address="${archive_root}/docs/../"
|
||||
stat_address="${archive_root}/index.html?stat=1"
|
||||
key=$(od -An -vtx1 "$tmp/archive/.dat/metadata.key" | tr -d ' \n')
|
||||
unswarm_address="dpi:/dat/?key=${key}&reload=dpi:/dat/&unswarm=unswarm"
|
||||
cache="$HOME/.dillo/dat/archives/${key%${key#??}}/${key#??}"
|
||||
mkdir -p "$cache"
|
||||
cp -R "$tmp/archive/." "$cache/"
|
||||
printf "<cmd='auth' msg='abcdef1234567890' '>\n<cmd='open_url' url='%s' '>\n<cmd='open_url' url='%s' '>\n<cmd='open_url' url='%s' '>\n<cmd='open_url' url='%s' '>\n<cmd='open_url' url='%s' '>\n<cmd='open_url' url='%s' '>\n<cmd='open_url' url='dpi:/dat/' '>\n<cmd='open_url' url='%s' '>\n<cmd='open_url' url='dpi:/dat/' '>\n<cmd='DpiBye' '>\n" \
|
||||
"$old_address" "$new_address" "$latest_address" "$listing_address" "$parent_address" "$stat_address" "$unswarm_address" |
|
||||
"$dat_dpi" --stdio-test > "$tmp/response"
|
||||
grep -q 'start_send_page' "$tmp/response" || { cat "$tmp/response"; exit 1; }
|
||||
if grep -q '^HTTP/1\.[01] ' "$tmp/response"; then
|
||||
cat "$tmp/response"
|
||||
echo 'Dillo DPIP response must start with MIME headers, not an HTTP status line' >&2
|
||||
exit 1
|
||||
fi
|
||||
grep -q 'historic setup page' "$tmp/response" || { cat "$tmp/response"; exit 1; }
|
||||
grep -q 'current live page' "$tmp/response" || { cat "$tmp/response"; exit 1; }
|
||||
test "$(grep -o 'current live page' "$tmp/response" | wc -l)" -eq 3
|
||||
grep -q 'nested.txt' "$tmp/response" || { cat "$tmp/response"; exit 1; }
|
||||
grep -q 'data:image/png;base64,' "$tmp/response" || { cat "$tmp/response"; exit 1; }
|
||||
listing_order=$(grep -oE '<a href="[^"]+">(zdir|adir|z.txt|nested.txt|a.txt)' "$tmp/response" | sed -E 's/.*>(.*)$/\1/' | tr '\n' ' ')
|
||||
test "$listing_order" = 'zdir adir z.txt nested.txt a.txt ' || { cat "$tmp/response"; exit 1; }
|
||||
grep -q 'Dillo parity' "$tmp/response" || { cat "$tmp/response"; exit 1; }
|
||||
if grep -q 'EXECUTED' "$tmp/response"; then cat "$tmp/response"; echo 'archive dat.scm was evaluated' >&2; exit 1; fi
|
||||
grep -q '"directory": false' "$tmp/response" || { cat "$tmp/response"; exit 1; }
|
||||
grep -q 'dpi:/dat/' "$tmp/response" || { cat "$tmp/response"; exit 1; }
|
||||
grep -q "<cmd='reload_request' url='dpi:/dat/'" "$tmp/response" || { cat "$tmp/response"; exit 1; }
|
||||
grep -q 'unswarming archive' "$tmp/response" || { cat "$tmp/response"; exit 1; }
|
||||
grep -q '<p>None</p>' "$tmp/response" || { cat "$tmp/response"; exit 1; }
|
||||
printf "<cmd='auth' msg='abcdef1234567890' '>\n<cmd='open_url' url='dpi:/dat/?quit=Quit' '>\n" |
|
||||
"$dat_dpi" --stdio-test > "$tmp/quit-response"
|
||||
grep -q 'Quitting' "$tmp/quit-response" || { cat "$tmp/quit-response"; exit 1; }
|
||||
"$socket_probe" "$dat_dpi" "$latest_address" > "$tmp/socket-response"
|
||||
grep -q "<cmd='start_send_page' url='" "$tmp/socket-response" || { cat "$tmp/socket-response"; exit 1; }
|
||||
grep -q '^Content-Type: text/html' "$tmp/socket-response" || { cat "$tmp/socket-response"; exit 1; }
|
||||
grep -q 'current live page' "$tmp/socket-response" || { cat "$tmp/socket-response"; exit 1; }
|
||||
if grep -q '^Content-Length:' "$tmp/socket-response"; then
|
||||
cat "$tmp/socket-response"
|
||||
echo 'historical Dillo DPIP pages are terminated by closing the request socket, not Content-Length' >&2
|
||||
exit 1
|
||||
fi
|
||||
echo 'Dillo DPIP auth, socket lifecycle/EOF framing, versions, Scheme metadata, listing, stat, dashboard, unswarm/reload, quit: PASS'
|
||||
@@ -0,0 +1,118 @@
|
||||
#define _POSIX_C_SOURCE 200809L
|
||||
#include <arpa/inet.h>
|
||||
#include <errno.h>
|
||||
#include <netinet/in.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/wait.h>
|
||||
#include <unistd.h>
|
||||
|
||||
static int connect_local(unsigned short port)
|
||||
{
|
||||
struct sockaddr_in address;
|
||||
int fd = socket(AF_INET, SOCK_STREAM, 0);
|
||||
if (fd < 0) return -1;
|
||||
memset(&address, 0, sizeof(address));
|
||||
address.sin_family = AF_INET;
|
||||
address.sin_port = htons(port);
|
||||
address.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
|
||||
if (connect(fd, (struct sockaddr *)&address, sizeof(address))) {
|
||||
close(fd);
|
||||
return -1;
|
||||
}
|
||||
return fd;
|
||||
}
|
||||
|
||||
static int send_all(int fd, const char *data, size_t length)
|
||||
{
|
||||
while (length) {
|
||||
ssize_t sent = send(fd, data, length, 0);
|
||||
if (sent < 0 && errno == EINTR) continue;
|
||||
if (sent <= 0) return -1;
|
||||
data += sent;
|
||||
length -= (size_t)sent;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int request_page(unsigned short port, const char *url)
|
||||
{
|
||||
const char *key = "abcdef1234567890";
|
||||
char request[4096], buffer[4096];
|
||||
int fd, n;
|
||||
int length = snprintf(request, sizeof(request),
|
||||
"<cmd='auth' msg='%s' '>\n<cmd='open_url' url='%s' '>", key, url);
|
||||
if (length < 0 || (size_t)length >= sizeof(request)) return -1;
|
||||
fd = connect_local(port);
|
||||
if (fd < 0) return -1;
|
||||
if (send_all(fd, request, (size_t)length) || shutdown(fd, SHUT_WR)) {
|
||||
close(fd);
|
||||
return -1;
|
||||
}
|
||||
while ((n = (int)recv(fd, buffer, sizeof(buffer), 0)) > 0) {
|
||||
if (fwrite(buffer, 1, (size_t)n, stdout) != (size_t)n) {
|
||||
close(fd);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
close(fd);
|
||||
if (n < 0) return -1;
|
||||
return fflush(stdout) ? -1 : 0;
|
||||
}
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
struct sockaddr_in address;
|
||||
socklen_t address_length = sizeof(address);
|
||||
unsigned short port;
|
||||
int listener;
|
||||
pid_t child;
|
||||
int status, fd;
|
||||
if (argc != 3) {
|
||||
fprintf(stderr, "usage: %s DPI-HELPER URL\n", argv[0]);
|
||||
return 2;
|
||||
}
|
||||
listener = socket(AF_INET, SOCK_STREAM, 0);
|
||||
if (listener < 0) { perror("socket"); return 1; }
|
||||
memset(&address, 0, sizeof(address));
|
||||
address.sin_family = AF_INET;
|
||||
address.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
|
||||
address.sin_port = 0;
|
||||
if (bind(listener, (struct sockaddr *)&address, sizeof(address)) ||
|
||||
listen(listener, 8) || getsockname(listener, (struct sockaddr *)&address, &address_length)) {
|
||||
perror("bind/listen"); close(listener); return 1;
|
||||
}
|
||||
port = ntohs(address.sin_port);
|
||||
child = fork();
|
||||
if (child < 0) { perror("fork"); close(listener); return 1; }
|
||||
if (!child) {
|
||||
if (dup2(listener, STDIN_FILENO) < 0) _exit(126);
|
||||
close(listener);
|
||||
execl(argv[1], argv[1], (char *)NULL);
|
||||
perror("exec"); _exit(127);
|
||||
}
|
||||
close(listener);
|
||||
if (request_page(port, argv[2])) {
|
||||
perror("request page");
|
||||
kill(child, SIGTERM);
|
||||
waitpid(child, NULL, 0);
|
||||
return 1;
|
||||
}
|
||||
fd = connect_local(port);
|
||||
{
|
||||
const char *bye = "<cmd='auth' msg='abcdef1234567890' '>\n<cmd='DpiBye' '>";
|
||||
if (fd < 0 || send_all(fd, bye, strlen(bye))) {
|
||||
perror("send DpiBye");
|
||||
kill(child, SIGTERM);
|
||||
waitpid(child, NULL, 0);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
shutdown(fd, SHUT_WR);
|
||||
close(fd);
|
||||
if (waitpid(child, &status, 0) < 0) { perror("waitpid"); return 1; }
|
||||
return WIFEXITED(status) ? WEXITSTATUS(status) : 1;
|
||||
}
|
||||
@@ -0,0 +1,391 @@
|
||||
#define _POSIX_C_SOURCE 200809L
|
||||
#define main dat_thang_program_main
|
||||
#include "../src/ddat_gui.c"
|
||||
#undef main
|
||||
|
||||
#include <assert.h>
|
||||
#include <fcntl.h>
|
||||
#include <time.h>
|
||||
#ifdef _WIN32
|
||||
#include <direct.h>
|
||||
#include <process.h>
|
||||
#define test_mkdir(path) _mkdir(path)
|
||||
#define test_setenv(name, value) _putenv_s((name), (value))
|
||||
#define test_pid _getpid
|
||||
#define test_close_socket(fd) closesocket(fd)
|
||||
typedef int test_socklen_t;
|
||||
typedef SOCKET test_socket_t;
|
||||
#define test_socket_valid(fd) ((fd) != INVALID_SOCKET)
|
||||
#else
|
||||
#include <sys/socket.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
#define test_mkdir(path) mkdir((path), 0700)
|
||||
#define test_setenv(name, value) setenv((name), (value), 1)
|
||||
#define test_pid getpid
|
||||
#define test_close_socket(fd) close(fd)
|
||||
typedef socklen_t test_socklen_t;
|
||||
typedef int test_socket_t;
|
||||
#define test_socket_valid(fd) ((fd) >= 0)
|
||||
#endif
|
||||
|
||||
static uint16_t unused_udp_port(void)
|
||||
{
|
||||
test_socket_t fd = socket(AF_INET, SOCK_DGRAM, 0);
|
||||
struct sockaddr_in address;
|
||||
test_socklen_t length = sizeof(address);
|
||||
uint16_t port;
|
||||
assert(test_socket_valid(fd));
|
||||
memset(&address, 0, sizeof(address));
|
||||
address.sin_family = AF_INET;
|
||||
address.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
|
||||
assert(bind(fd, (struct sockaddr *)&address, sizeof(address)) == 0);
|
||||
assert(getsockname(fd, (struct sockaddr *)&address, &length) == 0);
|
||||
port = ntohs(address.sin_port);
|
||||
test_close_socket(fd);
|
||||
return port;
|
||||
}
|
||||
|
||||
static void pause_one_millisecond(void)
|
||||
{
|
||||
SDL_Delay(1);
|
||||
}
|
||||
|
||||
static void set_network_environment(uint16_t local_port, uint16_t bootstrap_port)
|
||||
{
|
||||
char value[32];
|
||||
snprintf(value, sizeof(value), "%u", (unsigned)local_port);
|
||||
assert(test_setenv("DDAT_PORT", value) == 0);
|
||||
snprintf(value, sizeof(value), "127.0.0.1:%u", (unsigned)bootstrap_port);
|
||||
assert(test_setenv("DDAT_BOOTSTRAP", value) == 0);
|
||||
}
|
||||
|
||||
static int peer_ready(const gui_state_t *state)
|
||||
{
|
||||
size_t i;
|
||||
if (!state->session_initialized) return 0;
|
||||
for (i = 0; i < state->session.peer_count; i++)
|
||||
if (state->session.peers[i].state == DAT_SESSION_PEER_READY) return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
#ifdef DAT_GUI_TEST_MEDIA
|
||||
static int test_fm_audio_is_playing(gui_state_t *state)
|
||||
{
|
||||
gui_fm_voice_t *voice = (gui_fm_voice_t *)state->fm_voice_ptr;
|
||||
int active;
|
||||
if (!voice || !state->audio_device) return 0;
|
||||
SDL_LockAudioDevice(state->audio_device);
|
||||
active = voice->active;
|
||||
SDL_UnlockAudioDevice(state->audio_device);
|
||||
return active;
|
||||
}
|
||||
#endif
|
||||
|
||||
int main(void)
|
||||
{
|
||||
static gui_state_t left, right;
|
||||
char root[1024];
|
||||
char left_home[1024], right_home[1024], file_path[1200], destination[1200];
|
||||
char feed_directory[1024];
|
||||
uint8_t content_key[32];
|
||||
uint16_t left_port, right_port;
|
||||
Uint32 now;
|
||||
int elapsed;
|
||||
#ifdef _WIN32
|
||||
WSADATA winsock;
|
||||
int fixture_attempt;
|
||||
assert(WSAStartup(MAKEWORD(2, 2), &winsock) == 0);
|
||||
for (fixture_attempt = 0; fixture_attempt < 1000; fixture_attempt++) {
|
||||
assert(snprintf(root, sizeof(root), "build/windows/test-gui-p2p-%ld-%d",
|
||||
(long)test_pid(), fixture_attempt) < (int)sizeof(root));
|
||||
if (test_mkdir(root) == 0) break;
|
||||
}
|
||||
assert(fixture_attempt < 1000);
|
||||
#else
|
||||
strcpy(root, "/tmp/ddat-gui-p2p-XXXXXX");
|
||||
#endif
|
||||
memset(&left, 0, sizeof(left));
|
||||
memset(&right, 0, sizeof(right));
|
||||
left_port = unused_udp_port();
|
||||
right_port = unused_udp_port();
|
||||
if (right_port == left_port) right_port = (uint16_t)(left_port + 1);
|
||||
#ifndef _WIN32
|
||||
assert(mkdtemp(root) != NULL);
|
||||
#endif
|
||||
assert(snprintf(file_path, sizeof(file_path), "%s/payload.txt", root) <
|
||||
(int)sizeof(file_path));
|
||||
{
|
||||
FILE *file = fopen(file_path, "wb");
|
||||
assert(file != NULL);
|
||||
assert(fputs("a real GUI Cesspool to uTP to SLEEP block", file) >= 0);
|
||||
assert(fclose(file) == 0);
|
||||
}
|
||||
assert(snprintf(left_home, sizeof(left_home), "%s/left-home", root) < (int)sizeof(left_home));
|
||||
assert(snprintf(right_home, sizeof(right_home), "%s/right-home", root) < (int)sizeof(right_home));
|
||||
assert(test_mkdir(left_home) == 0);
|
||||
assert(test_mkdir(right_home) == 0);
|
||||
assert(snprintf(destination, sizeof(destination), "%s/download", root) < (int)sizeof(destination));
|
||||
assert(test_mkdir(destination) == 0);
|
||||
assert(test_setenv("HOME", left_home) == 0);
|
||||
assert(test_setenv("USERPROFILE", left_home) == 0);
|
||||
#ifdef DAT_GUI_TEST_MEDIA
|
||||
assert(SDL_Init(SDL_INIT_TIMER | SDL_INIT_AUDIO) == 0);
|
||||
#else
|
||||
assert(SDL_Init(SDL_INIT_TIMER) == 0);
|
||||
#endif
|
||||
assert(dat_scheme_runtime_init(&left.metadata_scheme) == 0);
|
||||
strcpy(left.archive_path, root);
|
||||
strcpy(right.archive_path, destination);
|
||||
assert(prepare_archive(&left) == 0);
|
||||
assert(left.archive_open && left.version >= 1);
|
||||
left.selected_version = 0;
|
||||
assert(click_archive_version_bar(&left, 210, 80, 84));
|
||||
assert(left.selected_version == 0);
|
||||
assert(!click_archive_version_bar(&left, 209, 80, 84));
|
||||
assert(!click_archive_version_bar(&left, 210, 73, 84));
|
||||
assert(!click_archive_version_bar(&left, 242, 80, 84));
|
||||
assert(click_archive_version_bar(&left, 226, 80, 84));
|
||||
assert(left.selected_version == 1);
|
||||
left.selected_version = left.version;
|
||||
assert(click_archive_version_bar(&left, 226, 80, 84));
|
||||
assert(left.selected_version == left.version);
|
||||
assert(click_archive_version_bar(&left, 210, 80, 84));
|
||||
assert(left.selected_version == left.version - 1);
|
||||
{
|
||||
dat_link_t link, parsed;
|
||||
char address[DAT_LINK_BYTES];
|
||||
memset(&link, 0, sizeof(link));
|
||||
memcpy(link.key, left.archive_key_bytes, sizeof(link.key));
|
||||
link.has_version = 1;
|
||||
link.version = left.selected_version;
|
||||
assert(dat_link_format(&link, address, sizeof(address)) == 0);
|
||||
assert(dat_link_parse(address, &parsed) == 0);
|
||||
assert(parsed.has_version && parsed.version == left.selected_version);
|
||||
}
|
||||
{
|
||||
dat_archive_t source;
|
||||
char uri[DAT_URL_BYTES];
|
||||
assert(dat_archive_open(&source, root) == 0);
|
||||
memcpy(right.remote_metadata_key, source.metadata_key, sizeof(right.remote_metadata_key));
|
||||
right.remote_key_valid = 1;
|
||||
assert(dat_key_to_url(source.metadata_key, uri) == 0);
|
||||
strcpy(right.remote_address, uri);
|
||||
strcpy(right.remote_archive, uri + 6);
|
||||
strcpy(right.remote_site, "P2P TEST SOURCE");
|
||||
strcpy(right.remote_version, "LATEST");
|
||||
assert(strlen(destination) + sizeof("/.dat") <= sizeof(feed_directory));
|
||||
strcpy(feed_directory, destination);
|
||||
strcat(feed_directory, "/.dat");
|
||||
memcpy(content_key, source.content_key, sizeof(content_key));
|
||||
dat_archive_close(&source);
|
||||
}
|
||||
assert(sleep_crypto_init() == 0);
|
||||
set_network_environment(left_port, right_port);
|
||||
network_start(&left);
|
||||
assert(left.network_requested && left.session_initialized);
|
||||
assert(!left.online); /* Reachability starts false until a response arrives. */
|
||||
assert(test_setenv("HOME", right_home) == 0);
|
||||
assert(test_setenv("USERPROFILE", right_home) == 0);
|
||||
set_network_environment(right_port, left_port);
|
||||
network_start_remote_import(&right);
|
||||
assert(right.network_requested && right.session_initialized);
|
||||
assert(!right.online);
|
||||
assert(!memcmp(right.identity.pubkey, right.pool.net.peerid, 32));
|
||||
for (elapsed = 0; elapsed < 12000; elapsed++) {
|
||||
now = SDL_GetTicks();
|
||||
network_pump(&left, now);
|
||||
if (memcmp(right.identity.pubkey, right.pool.net.peerid, 32)) {
|
||||
fprintf(stderr, "left client pump overwrote right Cesspool identity at %d ms\\n", elapsed);
|
||||
abort();
|
||||
}
|
||||
network_pump(&right, now);
|
||||
if (memcmp(right.identity.pubkey, right.pool.net.peerid, 32)) {
|
||||
size_t changed;
|
||||
for (changed = 0; changed < 32 &&
|
||||
right.identity.pubkey[changed] == right.pool.net.peerid[changed]; changed++) {}
|
||||
fprintf(stderr, "right identity changed during P2P pump at %d ms: byte %lu %02x -> %02x pool=%d online=%d status=%s\n",
|
||||
elapsed, (unsigned long)changed, right.identity.pubkey[changed],
|
||||
right.pool.net.peerid[changed], right.pool_initialized, right.online,
|
||||
right.network_status);
|
||||
abort();
|
||||
}
|
||||
if (peer_ready(&left) && peer_ready(&right) && right.session.content_key_known &&
|
||||
sleep_feed_has(&right.content_feed, 1)) break;
|
||||
pause_one_millisecond();
|
||||
}
|
||||
if (!peer_ready(&left) || !peer_ready(&right)) {
|
||||
fprintf(stderr, "peer setup timeout: L=%02x%02x R=%02x%02x left discovered=%lu peers=%lu ps=%d mask=%u rx=%lu tx=%llu/%llu status=%s; right discovered=%lu peers=%lu ps=%d mask=%u rx=%lu tx=%llu/%llu status=%s\n",
|
||||
left.identity.pubkey[0],left.identity.pubkey[1],right.identity.pubkey[0],right.identity.pubkey[1],
|
||||
(unsigned long)discovered_peer_count(&left),
|
||||
(unsigned long)left.session.peer_count,
|
||||
left.session.peers[0].state, left.session.peers[0].remote_feed_mask,
|
||||
(unsigned long)left.session.peers[0].rx_buffer_len,
|
||||
(unsigned long long)left.bytes_received,
|
||||
(unsigned long long)left.bytes_sent, left.network_status,
|
||||
(unsigned long)discovered_peer_count(&right),
|
||||
(unsigned long)right.session.peer_count,
|
||||
right.session.peers[0].state, right.session.peers[0].remote_feed_mask,
|
||||
(unsigned long)right.session.peers[0].rx_buffer_len,
|
||||
(unsigned long long)right.bytes_received,
|
||||
(unsigned long long)right.bytes_sent, right.network_status);
|
||||
for (size_t i = 0; i < GUI_TRANSPORT_MAX; i++) {
|
||||
if (left.transports[i].used)
|
||||
fprintf(stderr, "left transport %lu state=%d wait=%d auth=%d hello=%d/%d ack=%d/%d seq=%u recv=%u inflight=%lu\n",
|
||||
(unsigned long)i, left.transports[i].utp.state,
|
||||
left.transports[i].waiting_incoming, left.transports[i].authenticated,
|
||||
left.transports[i].hello_sent, left.transports[i].hello_received,
|
||||
left.transports[i].ack_sent, left.transports[i].ack_received,
|
||||
left.transports[i].utp.sequence, left.transports[i].utp.acknowledgement,
|
||||
(unsigned long)left.transports[i].utp.bytes_in_flight);
|
||||
if (right.transports[i].used)
|
||||
fprintf(stderr, "right transport %lu state=%d wait=%d auth=%d hello=%d/%d ack=%d/%d seq=%u recv=%u inflight=%lu\n",
|
||||
(unsigned long)i, right.transports[i].utp.state,
|
||||
right.transports[i].waiting_incoming, right.transports[i].authenticated,
|
||||
right.transports[i].hello_sent, right.transports[i].hello_received,
|
||||
right.transports[i].ack_sent, right.transports[i].ack_received,
|
||||
right.transports[i].utp.sequence, right.transports[i].utp.acknowledgement,
|
||||
(unsigned long)right.transports[i].utp.bytes_in_flight);
|
||||
}
|
||||
for (size_t i = 0; i < left.pool.peer_count; i++)
|
||||
fprintf(stderr, "left known peer %lu port=%u topic=%d\n", (unsigned long)i,
|
||||
ntohs(((struct sockaddr_in *)&left.pool.peers[i].address.addr)->sin_port),
|
||||
!memcmp(left.pool.peers[i].discovery_topic, left.discovery_topic, 32));
|
||||
for (size_t i = 0; i < right.pool.peer_count; i++)
|
||||
fprintf(stderr, "right known peer %lu port=%u topic=%d\n", (unsigned long)i,
|
||||
ntohs(((struct sockaddr_in *)&right.pool.peers[i].address.addr)->sin_port),
|
||||
!memcmp(right.pool.peers[i].discovery_topic, right.discovery_topic, 32));
|
||||
abort();
|
||||
}
|
||||
assert(right.content_feed.length > 1);
|
||||
assert(left.session.peers[0].payload_bytes_sent > 0);
|
||||
assert(right.session.peers[0].payload_bytes_received > 0);
|
||||
assert(sleep_feed_has(&right.content_feed, 1));
|
||||
if (!transport_by_id(&left, right.identity.pubkey)) {
|
||||
fprintf(stderr, "identity mismatch: L identity=%02x pool=%02x session=%02x; R identity=%02x pool=%02x session=%02x\n",
|
||||
left.identity.pubkey[0], left.pool.net.peerid[0], left.session.peer_id[0],
|
||||
right.identity.pubkey[0], right.pool.net.peerid[0], right.session.peer_id[0]);
|
||||
for (size_t i = 0; i < GUI_TRANSPORT_MAX; i++)
|
||||
fprintf(stderr, "left transport slot %lu used=%d state=%d auth=%d peer=%02x expected=%02x sessionpeer=%02x session=%d\n",
|
||||
(unsigned long)i, left.transports[i].used, left.transports[i].utp.state,
|
||||
left.transports[i].authenticated, left.transports[i].peer_id[0],
|
||||
right.identity.pubkey[0], left.session.peers[0].peer_id[0],
|
||||
left.session.peers[0].state);
|
||||
abort();
|
||||
}
|
||||
assert(transport_by_id(&left, right.identity.pubkey)->authenticated);
|
||||
assert(transport_by_id(&right, left.identity.pubkey) != NULL);
|
||||
assert(transport_by_id(&right, left.identity.pubkey)->authenticated);
|
||||
transfer_disconnect(&left, 0, 0);
|
||||
assert(left.transfer_paused[0] && left.session.peers[0].paused);
|
||||
assert(peer_ready(&left) && peer_ready(&right));
|
||||
transfer_resume(&left, 0);
|
||||
assert(!left.transfer_paused[0] && !left.session.peers[0].paused);
|
||||
for (elapsed = 0; elapsed < 5000; elapsed++) {
|
||||
now = SDL_GetTicks();
|
||||
network_pump(&left, now);
|
||||
network_pump(&right, now);
|
||||
if (peer_ready(&left) && peer_ready(&right)) break;
|
||||
pause_one_millisecond();
|
||||
}
|
||||
if (!peer_ready(&left) || !peer_ready(&right))
|
||||
fprintf(stderr, "resume timeout IDs=%02x/%02x: left session=%d transport=%d/%d wait=%d auth=%d hello=%d/%d ack=%d/%d right session=%d transport=%d/%d wait=%d auth=%d hello=%d/%d ack=%d/%d statuses=%s | %s\n",
|
||||
left.identity.pubkey[0], right.identity.pubkey[0],
|
||||
left.session.peers[0].state, left.transports[0].used,
|
||||
left.transports[0].utp.state, left.transports[0].waiting_incoming,
|
||||
left.transports[0].authenticated, left.transports[0].hello_sent,
|
||||
left.transports[0].hello_received, left.transports[0].ack_sent,
|
||||
left.transports[0].ack_received, right.session.peers[0].state,
|
||||
right.transports[0].used, right.transports[0].utp.state,
|
||||
right.transports[0].waiting_incoming, right.transports[0].authenticated,
|
||||
right.transports[0].hello_sent, right.transports[0].hello_received,
|
||||
right.transports[0].ack_sent, right.transports[0].ack_received,
|
||||
left.network_status, right.network_status);
|
||||
assert(peer_ready(&left) && peer_ready(&right));
|
||||
assert(left.online && right.online);
|
||||
memcpy(left.chat_peer_id,right.identity.pubkey,32);
|
||||
left.chat_open=1;strcpy(left.chat_input,"hello from cable over the authenticated peer link");
|
||||
if (cable_chat_send(&left) != 0) {
|
||||
gui_transport_peer_t *chat_transport = transport_by_id(&left, left.chat_peer_id);
|
||||
fprintf(stderr, "chat after resume: transport=%p used=%d auth=%d utp=%d session=%d errno=%d\n",
|
||||
(void *)chat_transport, chat_transport ? chat_transport->used : 0,
|
||||
chat_transport ? chat_transport->authenticated : 0,
|
||||
chat_transport ? chat_transport->utp.state : 0,
|
||||
left.session.peers[0].state, errno);
|
||||
abort();
|
||||
}
|
||||
for(elapsed=0;elapsed<3000;elapsed++){
|
||||
now=SDL_GetTicks();network_pump(&left,now);network_pump(&right,now);
|
||||
if(right.chat_line_count)break;
|
||||
pause_one_millisecond();
|
||||
}
|
||||
assert(right.chat_line_count==1);
|
||||
assert(!strcmp(right.chat_lines[0].text,"hello from cable over the authenticated peer link"));
|
||||
assert(!right.chat_lines[0].outgoing);
|
||||
{
|
||||
uint8_t *received = NULL;
|
||||
size_t received_length = 0;
|
||||
dat_archive_t downloaded;
|
||||
sleep_metadata_entry_t entry;
|
||||
const char expected[] = "a real GUI Cesspool to uTP to SLEEP block";
|
||||
assert(dat_archive_open(&downloaded, destination) == 0);
|
||||
assert(dat_archive_find(&downloaded, "payload.txt", &entry) == 0);
|
||||
assert(dat_archive_read_file(&downloaded, &entry, &received, &received_length) == 0);
|
||||
assert(received_length == sizeof(expected) - 1);
|
||||
assert(!memcmp(received, expected, received_length));
|
||||
free(received);
|
||||
received = NULL;
|
||||
dat_archive_close(&downloaded);
|
||||
assert(sleep_feed_read(&right.content_feed, 1, &received, &received_length) == 0);
|
||||
/* A content feed block is not the same thing as one source file: the
|
||||
* archive reader reconstructs files from metadata offsets and blocks. */
|
||||
assert(received_length > 0);
|
||||
free(received);
|
||||
}
|
||||
sleep_feed_close(&right.content_feed);
|
||||
assert(sleep_feed_open_remote(&right.content_feed, feed_directory,
|
||||
"content", content_key) == 0);
|
||||
assert(sleep_feed_has(&right.content_feed, 1));
|
||||
{
|
||||
sleep_feed_t restored;
|
||||
memset(&restored, 0, sizeof(restored));
|
||||
strcpy(restored.directory, feed_directory);
|
||||
strcpy(restored.name, "content");
|
||||
assert(sleep_feed_load_remote(&restored, content_key) == 0);
|
||||
assert(sleep_feed_has(&restored, 1));
|
||||
sleep_feed_close(&restored);
|
||||
}
|
||||
#ifdef DAT_GUI_TEST_MEDIA
|
||||
left.audio_enabled = 1;
|
||||
fm_audio_start(&left);
|
||||
assert(left.audio_device != 0);
|
||||
audio_cue_broadcast(&right, 1);
|
||||
for (elapsed = 0; elapsed < 2000; elapsed++) {
|
||||
now = SDL_GetTicks();
|
||||
network_pump(&left, now);
|
||||
network_pump(&right, now);
|
||||
if (((gui_fm_voice_t *)left.fm_voice_ptr)->dolphin && test_fm_audio_is_playing(&left)) break;
|
||||
pause_one_millisecond();
|
||||
}
|
||||
assert(((gui_fm_voice_t *)left.fm_voice_ptr)->dolphin);
|
||||
assert(test_fm_audio_is_playing(&left));
|
||||
fm_audio_play(&left);
|
||||
assert(test_fm_audio_is_playing(&left));
|
||||
SDL_Delay(2100);
|
||||
assert(!test_fm_audio_is_playing(&left));
|
||||
fm_audio_stop(&left);
|
||||
#endif
|
||||
network_stop(&right);
|
||||
network_stop(&left);
|
||||
dat_scheme_runtime_close(&left.metadata_scheme);
|
||||
SDL_Quit();
|
||||
#ifdef _WIN32
|
||||
WSACleanup();
|
||||
#endif
|
||||
#ifdef DAT_GUI_TEST_MEDIA
|
||||
puts("dat-thang authenticated Cesspool transfer, chat, and dolphin cue: PASS");
|
||||
#else
|
||||
puts("dat-thang authenticated Cesspool archive transfer, pause/resume, and chat: PASS");
|
||||
#endif
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
#include "dat_paths.h"
|
||||
|
||||
#include <assert.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#ifdef _WIN32
|
||||
#include <process.h>
|
||||
#define test_pid _getpid
|
||||
#else
|
||||
#include <unistd.h>
|
||||
#define test_pid getpid
|
||||
#endif
|
||||
|
||||
static void write_text(const char *path, const char *text)
|
||||
{
|
||||
FILE *file = fopen(path, "wb");
|
||||
assert(file);
|
||||
assert(fwrite(text, 1, strlen(text), file) == strlen(text));
|
||||
assert(fclose(file) == 0);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
char destination[256], temporary[256], contents[32];
|
||||
FILE *file;
|
||||
int n;
|
||||
n = snprintf(destination, sizeof(destination), "build/test-paths-%ld", (long)test_pid());
|
||||
assert(n > 0 && (size_t)n < sizeof(destination));
|
||||
n = snprintf(temporary, sizeof(temporary), "%s.tmp", destination);
|
||||
assert(n > 0 && (size_t)n < sizeof(temporary));
|
||||
write_text(destination, "before");
|
||||
write_text(temporary, "after");
|
||||
assert(dat_replace_file(temporary, destination) == 0);
|
||||
file = fopen(destination, "rb");
|
||||
assert(file);
|
||||
assert(fgets(contents, sizeof(contents), file));
|
||||
assert(fclose(file) == 0);
|
||||
assert(strcmp(contents, "after") == 0);
|
||||
assert(remove(destination) == 0);
|
||||
puts("user-state atomic replacement: PASS");
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
#include "sleep_proto.h"
|
||||
#include <assert.h>
|
||||
#include <string.h>
|
||||
int main(void){sleep_metadata_header_t a,b;unsigned char buf[128];size_t n;memset(&a,0,sizeof(a));strcpy(a.type,"hyperdrive");memset(a.content,0x5a,32);a.content_len=32;assert(sleep_metadata_header_encode(&a,buf,sizeof(buf),&n)==0);assert(n==46);assert(sleep_metadata_header_decode(&b,buf,n)==0);assert(!strcmp(b.type,"hyperdrive"));assert(b.content_len==32&&!memcmp(a.content,b.content,32));return 0;}
|
||||
Loaded 100 of 323 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user