char/avril
(wip) why'd you have to go and make things so complicaaaaated?
git clone https://git.t4t.associates/char/avril
4d5ee6a
main
1use std:: fs; 2use std:: io:: Read ; 3use std:: path:: Path ; 4use std:: process::{ Command , Output , Stdio }; 5use std:: time::{ Duration , Instant }; 6 7const DEADLINE : Duration =Duration :: from_secs ( 30 ); 8 9fn run ( dir : & Path , script : & str , source : & str , flags : & [ & str ]) ->Output { 10 fs:: write ( dir. join ( script), source). unwrap (); 11let mut child =Command :: new ( env! ( "CARGO_BIN_EXE_avril" )) 12. arg ( "run" ) 13. current_dir ( dir) 14. args ( flags) 15. arg ( script) 16. stdin ( Stdio :: null ()) 17. stdout ( Stdio :: piped ()) 18. stderr ( Stdio :: piped ()) 19. spawn () 20. unwrap (); 21let mut stdout = child. stdout . take (). unwrap (); 22let mut stderr = child. stderr . take (). unwrap (); 23let threads = std:: thread:: scope ( |scope|{ 24let out = scope. spawn ( ||{ 25let mut bytes =Vec :: new (); 26 stdout. read_to_end ( & mut bytes). unwrap (); 27 bytes 28}); 29let err = scope. spawn ( ||{ 30let mut bytes =Vec :: new (); 31 stderr. read_to_end ( & mut bytes). unwrap (); 32 bytes 33}); 34let started =Instant :: now (); 35let status =loop { 36if let Some ( status) = child. try_wait (). unwrap () { 37break status; 38} 39if started. elapsed () >=DEADLINE { 40 child. kill (). unwrap (); 41panic! ( "avril run did not exit within {DEADLINE:?}" ); 42} 43 std:: thread:: sleep ( Duration :: from_millis ( 10 )); 44}; 45( out. join (). unwrap (), err. join (). unwrap (), status) 46}); 47Output { 48status : threads. 2 , 49stdout : threads. 0 , 50stderr : threads. 1 , 51} 52} 53 54fn assert_marker ( output : & Output , marker : & str ) { 55assert! ( 56 output. status . success (), 57"exit={}\nstdout={}\nstderr={}" , 58 output. status , 59String :: from_utf8_lossy ( & output. stdout ), 60String :: from_utf8_lossy ( & output. stderr ) 61); 62assert! ( 63String :: from_utf8_lossy ( & output. stdout ). contains ( marker), 64"missing {marker}\nstdout={}\nstderr={}" , 65String :: from_utf8_lossy ( & output. stdout ), 66String :: from_utf8_lossy ( & output. stderr ) 67); 68} 69 70const PRELUDE : & str =r#" 71const failures = []; 72function check(cond, message) { 73if (!cond) failures[failures.length] = message; 74} 75function done(name) { 76if (failures.length !== 0) { 77for (let i = 0; i < failures.length; i++) console.error("FAIL: " + failures[i]); 78throw new Error(failures.length + " failure(s) in " + name); 79} 80console.log("KV-OK " + name); 81} 82"# ; 83 84# [ test ] 85fn roundtrip_and_persistence () { 86let dir = tempfile:: tempdir (). unwrap (); 87let write =format! ( 88r#"{PRELUDE} 89import {{ openKv }} from "avril:kv"; 90const db = await openKv("state.kv", {{ create: true }}); 91const ks = db.keyspace("v"); 92// Strings and equal byte sequences are the same key and value. 93await ks.set("str", "hello"); 94await ks.set(new Uint8Array([98, 121, 116, 101, 115]), new Uint8Array([1, 2, 3])); 95await ks.set("abc", "as-string"); 96await ks.set(new Uint8Array([120]), "json-value"); 97const first = await ks.set("rewrite", "one"); 98const second = await ks.set("rewrite", "two"); 99check(first.versionstamp !== second.versionstamp, "versionstamp changes on rewrite"); 100check((await db.keyspaces()).join(",") === "v", "keyspaces lists v"); 101db.close(); 102const reopened = await openKv("state.kv", {{ create: false }}); 103const again = reopened.keyspace("v"); 104const entry = await again.entry("str"); 105check(entry !== null && entry.keyspace === "v", "entry keyspace"); 106check(entry !== null && Array.from(entry.key).join(",") === "115,116,114", "entry key is canonical bytes"); 107check(entry !== null && typeof entry.versionstamp === "bigint", "entry carries a versionstamp"); 108const str = await again.get("str"); 109check(str !== null && Array.from(str).join(",") === "104,101,108,108,111", "get returns raw bytes"); 110const text = await again.getText("str"); 111check(text === "hello", "getText decodes strings"); 112const asBytes = await again.get(new Uint8Array([115, 116, 114])); 113check(asBytes !== null && asBytes.length === 5, "byte key hits string key"); 114const overwritten = await again.get("abc"); 115check(overwritten !== null && overwritten instanceof Uint8Array, "overwritten value is bytes"); 116await again.set("abc", "as-string-again"); 117const back = await again.get(new Uint8Array([97, 98, 99])); 118check(back !== null && (await again.getText(new Uint8Array([97, 98, 99]))) === "as-string-again", "bytes and strings are one key"); 119const json = await again.getText("json-holder"); 120check(json === null, "missing key reads null"); 121await again.set("json-holder", JSON.stringify({{ list: [1, "two"], map: {{ a: 1 }} }})); 122const parsed = JSON.parse((await again.getText("json-holder")) ?? "null"); 123check(parsed.list[1] === "two" && parsed.map.a === 1, "JSON roundtrips through strings"); 124// Non-UTF-8 bytes: get works byte-exact, getText rejects. 125await again.set("raw", new Uint8Array([0xff, 0x00, 0xfe])); 126const raw = await again.get("raw"); 127check(raw !== null && Array.from(raw).join(",") === "255,0,254", "non-UTF8 roundtrips as bytes"); 128try {{ 129await again.getText("raw"); 130check(false, "getText of non-UTF8 must reject"); 131}} catch (e) {{ 132check(e instanceof TypeError, "getText rejection is a TypeError, got: " + e); 133}} 134done("roundtrip"); 135"# 136); 137assert_marker ( & run ( dir. path (), "write.js" , & write, & []), "KV-OK roundtrip" ); 138let read =r#"import { openKv } from "avril:kv"; 139const failures = []; 140function check(cond, message) { 141if (!cond) failures[failures.length] = message; 142} 143const db = await openKv("state.kv", { create: false }); 144const ks = db.keyspace("v"); 145const str = await ks.getText("str"); 146check(str === "hello", "string persists across processes"); 147const bytes = await ks.get("bytes"); 148check(bytes !== null && Array.from(bytes).join(",") === "1,2,3", "bytes persist across processes"); 149const missing = await ks.get("nope"); 150check(missing === null, "missing reads null"); 151if (failures.length !== 0) { 152for (let i = 0; i < failures.length; i++) console.error("FAIL: " + failures[i]); 153throw new Error(failures.length + " failure(s)"); 154} 155console.log("KV-OK reread"); 156"# ; 157assert_marker ( & run ( dir. path (), "read.js" , read, & []), "KV-OK reread" ); 158} 159 160# [ test ] 161fn keys_and_ordering () { 162let dir = tempfile:: tempdir (). unwrap (); 163let source =r#"import { openKv } from "avril:kv"; 164const failures = []; 165function check(cond, message) { 166if (!cond) failures[failures.length] = message; 167} 168const db = await openKv("state.kv", { create: true }); 169const ks = db.keyspace("o"); 170// "b" as a string and as bytes [98] is one key; the second write wins. 171await ks.set("b", "string-b"); 172await ks.set(new Uint8Array([98]), "bytes-b"); 173await ks.set("", "empty"); 174await ks.set(new Uint8Array([0]), "zero"); 175await ks.set("a", "a"); 176await ks.set("a\x00", "a-nul"); 177await ks.set("a\x00b", "a-nul-b"); 178await ks.set("aa", "aa"); 179await ks.set(new Uint8Array([255]), "max"); 180const tag = (key) => Array.from(key).join(","); 181const listed = []; 182for await (const e of ks.list()) listed[listed.length] = tag(e.key); 183check(JSON.stringify(listed) === JSON.stringify(["", "0", "97", "97,0", "97,0,98", "97,97", "98", "255"]), "byte ordering:\n" + JSON.stringify(listed)); 184const b = await ks.getText("b"); 185check(b === "bytes-b", "string and bytes are one key"); 186const prefixed = []; 187for await (const e of ks.list({ prefix: "user:" })) prefixed[prefixed.length] = tag(e.key); 188const emptyBefore = prefixed.length; 189await ks.set("user:1", "u1"); 190await ks.set("user:2", "u2"); 191await ks.set("admin:1", "a1"); 192const prefixedAfter = []; 193for await (const e of ks.list({ prefix: "user:" })) prefixedAfter[prefixedAfter.length] = tag(e.key); 194check(emptyBefore === 0, "no user: keys before insert"); 195check(JSON.stringify(prefixedAfter) === JSON.stringify(["117,115,101,114,58,49", "117,115,101,114,58,50"]), "string prefix listing"); 196const bytePrefixed = []; 197for await (const e of ks.list({ prefix: new Uint8Array([0x89, 0x50]) })) bytePrefixed[bytePrefixed.length] = tag(e.key); 198check(bytePrefixed.length === 0, "binary prefix with no match"); 199await ks.set(new Uint8Array([0x89, 0x50, 0x01]), "bin1"); 200await ks.set(new Uint8Array([0x89, 0x50, 0x02]), "bin2"); 201await ks.set(new Uint8Array([0x89, 0x51]), "other"); 202const bytePrefixedAfter = []; 203for await (const e of ks.list({ prefix: new Uint8Array([0x89, 0x50]) })) bytePrefixedAfter[bytePrefixedAfter.length] = tag(e.key); 204check(JSON.stringify(bytePrefixedAfter) === JSON.stringify(["137,80,1", "137,80,2"]), "binary prefix listing"); 205const bounded = []; 206for await (const e of ks.list({ start: "a", end: "b" })) bounded[bounded.length] = tag(e.key); 207check(JSON.stringify(bounded) === JSON.stringify(["97", "97,0", "97,0,98", "97,97", "97,100,109,105,110,58,49"]), "start/end bounds"); 208const limited = []; 209for await (const e of ks.list({ limit: 2 })) limited[limited.length] = tag(e.key); 210check(limited.length === 2, "limit"); 211const reversed = []; 212for await (const e of ks.list({ reverse: true })) reversed[reversed.length] = tag(e.key); 213const forward = []; 214for await (const e of ks.list()) forward[forward.length] = tag(e.key); 215check(JSON.stringify(reversed) === JSON.stringify(forward.slice().reverse()), "reverse"); 216const all = []; 217for await (const e of ks.list({ prefix: "" })) all[all.length] = tag(e.key); 218check(all.length === forward.length, "empty prefix scans all"); 219if (failures.length !== 0) { 220for (let i = 0; i < failures.length; i++) console.error("FAIL: " + failures[i]); 221throw new Error(failures.length + " failure(s)"); 222} 223console.log("KV-OK ordering"); 224"# ; 225assert_marker ( & run ( dir. path (), "main.js" , source, & []), "KV-OK ordering" ); 226} 227 228# [ test ] 229fn keyspaces_isolation_clear_and_get_many () { 230let dir = tempfile:: tempdir (). unwrap (); 231let source =r#"import { openKv } from "avril:kv"; 232const failures = []; 233function check(cond, message) { 234if (!cond) failures[failures.length] = message; 235} 236const db = await openKv("state.kv", { create: true }); 237const a = db.keyspace("a"); 238const b = db.keyspace("b"); 239check(a.name === "a" && b.name === "b", "keyspace names"); 240check(a === db.keyspace("a"), "keyspace identity is stable"); 241await a.set("k", "from-a"); 242await b.set("k", "from-b"); 243const ra = await a.get("k"); 244const rb = await b.get("k"); 245check(ra !== null && Array.from(ra).join(",") === "102,114,111,109,45,97", "space a isolated"); 246check(rb !== null && (await b.getText("k")) === "from-b", "space b isolated"); 247check(JSON.stringify(await db.keyspaces()) === '["a","b"]', "keyspaces listed"); 248const many = await b.getMany(["k", "missing"]); 249check(many.length === 2 && many[0] !== null && many[1] === null, "getMany"); 250await a.clear(); 251check(await a.get("k") === null, "clear empties its space"); 252const still = await b.getText("k"); 253check(still === "from-b", "clear scoped to its space"); 254if (failures.length !== 0) { 255for (let i = 0; i < failures.length; i++) console.error("FAIL: " + failures[i]); 256throw new Error(failures.length + " failure(s)"); 257} 258console.log("KV-OK spaces"); 259"# ; 260assert_marker ( & run ( dir. path (), "main.js" , source, & []), "KV-OK spaces" ); 261} 262 263# [ test ] 264fn atomic_batches () { 265let dir = tempfile:: tempdir (). unwrap (); 266let source =r#"import { openKv } from "avril:kv"; 267const failures = []; 268function check(cond, message) { 269if (!cond) failures[failures.length] = message; 270} 271const db = await openKv("state.kv", { create: true }); 272const other = await openKv("other.kv", { create: true }); 273const a = db.keyspace("a"); 274const b = db.keyspace("b"); 275const stamp = (await a.set("k1", "v1")).versionstamp; 276const entryBefore = await a.entry("k1"); 277check(entryBefore !== null && entryBefore.versionstamp === stamp, "entry() exposes the commit stamp"); 278const ok = await db.atomic((tx) => { 279tx.check(a, "k1", stamp); 280tx.set(a, "k1", "v2"); 281tx.set(b, "k2", "w2"); 282tx.delete(a, "ghost"); 283}); 284check(ok.ok === true, "commit succeeds"); 285const ra = await a.get("k1"); 286const rb = await b.get("k2"); 287check(ra !== null && rb !== null && (await a.entry("k1"))?.versionstamp === (await b.entry("k2"))?.versionstamp, "same stamp across keyspaces"); 288check(ra !== null && (await a.getText("k1")) === "v2" && (await b.getText("k2")) === "w2", "both writes visible"); 289const bad = await db.atomic((tx) => { 290tx.check(a, "k1", 255n); 291tx.set(a, "k1", "lost"); 292tx.set(b, "k2", "lost"); 293}); 294check(bad.ok === false, "bad check aborts"); 295check((await a.getText("k1")) === "v2", "aborted set invisible"); 296check(await b.get("missing") === null, "aborted second-space write invisible"); 297const fresh = await db.atomic((tx) => { 298tx.check(b, "fresh", null); 299tx.set(b, "fresh", "1"); 300}); 301check(fresh.ok === true, "null check passes when absent"); 302const stale = await db.atomic((tx) => { 303tx.check(b, "fresh", null); 304tx.set(b, "fresh", "2"); 305}); 306check(stale.ok === false, "null check fails when present"); 307const only = await db.atomic((tx) => { 308tx.check(b, "definitely-absent", null); 309}); 310check(only.ok === true && only.versionstamp === null, "check-only batch has null versionstamp"); 311const manyText = await b.getTextMany(["fresh", "missing", "fresh"]); 312check(JSON.stringify(manyText) === JSON.stringify(["1", null, "1"]), "getTextMany preserves order and nulls"); 313const stamped = await a.entries(["k1", "missing"]); 314check(stamped.length === 2 && stamped[0] !== null && stamped[1] === null, "entries preserves order and nulls"); 315const accepted = await db.atomic((tx) => { 316tx.check(a, "k1", stamped[0].versionstamp); 317tx.set(a, "k1", "v3"); 318}); 319check(accepted.ok === true, "entries() stamps are accepted by check"); 320check((await a.getText("k1")) === "v3", "check-then-set via entry stamp commits"); 321try { 322await db.atomic(() => { 323throw new Error("boom"); 324}); 325check(false, "throwing callback must reject"); 326} catch (e) { 327check(e.message === "boom", "callback error propagates"); 328} 329check((await b.getText("fresh")) === "1", "thrown callback commits nothing"); 330try { 331await db.atomic(async (tx) => { 332tx.set(a, "async", "v"); 333}); 334check(false, "async callback must reject"); 335} catch (e) { 336check(e instanceof TypeError, "async callback is a TypeError"); 337} 338check(await a.get("async") === null, "async callback commits nothing"); 339let stolen = null; 340await db.atomic((tx) => { 341stolen = tx; 342}); 343try { 344stolen.set(a, "late", "v"); 345check(false, "retained tx must throw"); 346} catch (e) { 347check(e instanceof TypeError, "retained tx is a TypeError"); 348} 349try { 350await db.atomic((tx) => { 351tx.set(other.keyspace("x"), "k", "v"); 352}); 353check(false, "foreign keyspace must throw"); 354} catch (e) { 355check(e instanceof TypeError, "foreign keyspace is a TypeError"); 356} 357if (failures.length !== 0) { 358for (let i = 0; i < failures.length; i++) console.error("FAIL: " + failures[i]); 359throw new Error(failures.length + " failure(s)"); 360} 361console.log("KV-OK atomic"); 362"# ; 363assert_marker ( & run ( dir. path (), "main.js" , source, & []), "KV-OK atomic" ); 364} 365 366# [ test ] 367fn modes_exclusive_blocks_second_process_and_read_only () { 368let dir = tempfile:: tempdir (). unwrap (); 369 fs:: write ( 370 dir. path (). join ( "hold.js" ), 371r#"import { openKv } from "avril:kv"; 372import { stdin } from "avril:std"; 373const db = await openKv("held.kv", { access: "exclusive", create: true }); 374await db.keyspace("a").set("k", "held"); 375console.log("READY"); 376const reader = stdin.getReader(); 377while (!(await reader.read()).done) {} 378"# , 379) 380. unwrap (); 381let mut holder =Command :: new ( env! ( "CARGO_BIN_EXE_avril" )) 382. arg ( "run" ) 383. current_dir ( dir. path ()) 384. arg ( "hold.js" ) 385. stdin ( Stdio :: piped ()) 386. stdout ( Stdio :: piped ()) 387. stderr ( Stdio :: piped ()) 388. spawn () 389. unwrap (); 390let ( ready_tx, ready_rx) = std:: sync:: mpsc:: channel (); 391let mut holder_stdout = holder. stdout . take (). unwrap (); 392 std:: thread:: spawn ( move ||{ 393let mut prefix =[ 0u8 ; 6 ]; 394let _ = holder_stdout. read_exact ( & mut prefix); 395let _ = ready_tx. send ( prefix); 396}); 397let started =Instant :: now (); 398loop { 399if let Ok ( prefix) = ready_rx. try_recv () { 400assert_eq! ( & prefix, b"READY\n" , "holder handshake" ); 401break ; 402} 403if started. elapsed () >=DEADLINE { 404 holder. kill (). unwrap (); 405panic! ( "holder did not print READY" ); 406} 407 std:: thread:: sleep ( Duration :: from_millis ( 10 )); 408} 409let attempt =run ( 410 dir. path (), 411"take.js" , 412r#"import { openKv } from "avril:kv"; 413await openKv("held.kv", { access: "exclusive", create: false }); 414console.log("OPENED-UNEXPECTEDLY"); 415"# , 416& [], 417); 418assert! ( !attempt. status . success (), "second exclusive open must fail" ); 419assert! ( 420String :: from_utf8_lossy ( & attempt. stderr ). contains ( "already open" ), 421"already-open error, stderr={}" , 422String :: from_utf8_lossy ( & attempt. stderr ) 423); 424drop ( holder. stdin . take ()); 425let status =loop { 426if let Some ( status) = holder. try_wait (). unwrap () { 427break status; 428} 429if started. elapsed () >=DEADLINE { 430panic! ( "holder did not exit" ); 431} 432 std:: thread:: sleep ( Duration :: from_millis ( 10 )); 433}; 434assert! ( status. success (), "holder exits cleanly" ); 435let modes =r#"import { openKv } from "avril:kv"; 436const failures = []; 437function check(cond, message) { 438if (!cond) failures[failures.length] = message; 439} 440try { 441await openKv("missing.kv", { create: false }); 442check(false, "create:false on missing must reject"); 443} catch (e) { 444check(String(e.message).includes("does not exist"), "missing message: " + e.message); 445} 446const ro = await openKv("missing.kv", { access: "read-only" }); 447check(await ro.keyspace("a").get("k") === null, "read-only missing reads null"); 448try { 449await ro.keyspace("a").set("k", "v"); 450check(false, "read-only set must reject"); 451} catch (e) { 452check(String(e.message).includes("read-only"), "read-only set rejects: " + e.message); 453} 454const existing = await openKv("held.kv", { access: "read-only" }); 455const held = await existing.keyspace("a").getText("k"); 456check(held === "held", "read-only reads existing"); 457if (failures.length !== 0) { 458for (let i = 0; i < failures.length; i++) console.error("FAIL: " + failures[i]); 459throw new Error(failures.length + " failure(s)"); 460} 461console.log("KV-OK modes"); 462"# ; 463assert_marker ( & run ( dir. path (), "modes.js" , modes, & []), "KV-OK modes" ); 464} 465 466# [ test ] 467fn closed_handles_reject () { 468let dir = tempfile:: tempdir (). unwrap (); 469let source =r#"import { openKv } from "avril:kv"; 470const failures = []; 471function check(cond, message) { 472if (!cond) failures[failures.length] = message; 473} 474const db = await openKv("state.kv", { create: true }); 475const ks = db.keyspace("a"); 476await ks.set("k", "v"); 477db.close(); 478db.close(); 479try { 480await ks.get("k"); 481check(false, "get after close must reject"); 482} catch (e) { 483check(e instanceof TypeError && e.message.includes("closed"), "get rejects: " + e.message); 484} 485try { 486db.keyspace("b"); 487check(false, "keyspace after close must throw"); 488} catch (e) { 489check(e instanceof TypeError, "keyspace throws TypeError"); 490} 491await db[Symbol.asyncDispose](); 492let retained = null; 493{ 494await using live = await openKv("state.kv", { create: true }); 495retained = live.keyspace("a"); 496} 497try { 498await retained.get("k"); 499check(false, "use after await using must reject"); 500} catch (e) { 501check(e instanceof TypeError, "await using disposes"); 502} 503if (failures.length !== 0) { 504for (let i = 0; i < failures.length; i++) console.error("FAIL: " + failures[i]); 505throw new Error(failures.length + " failure(s)"); 506} 507console.log("KV-OK closed"); 508"# ; 509assert_marker ( & run ( dir. path (), "main.js" , source, & []), "KV-OK closed" ); 510} 511 512# [ test ] 513fn capability_denial_at_open_and_after_pledge () { 514let dir = tempfile:: tempdir (). unwrap (); 515let denied =run ( 516 dir. path (), 517"main.js" , 518r#"import { openKv } from "avril:kv"; 519try { 520await openKv("state.kv", { create: true }); 521console.log("OPENED-UNEXPECTEDLY"); 522} catch (e) { 523console.log("KV-OK denied: " + e.message); 524} 525"# , 526& [ "--deny-write=." ], 527); 528assert_marker ( & denied, "KV-OK denied: write state.kv" ); 529assert! ( 530 fs:: metadata ( dir. path (). join ( "state.kv" )). is_err (), 531"denied open must not create the file" 532); 533let pledged =run ( 534 dir. path (), 535"pledge.js" , 536r#"import { openKv } from "avril:kv"; 537import { pledge } from "avril:std"; 538const failures = []; 539function check(cond, message) { 540if (!cond) failures[failures.length] = message; 541} 542const db = await openKv("state.kv", { create: true }); 543await db.keyspace("a").set("k", "v"); 544pledge({ read: false, write: false }); 545try { 546await db.keyspace("a").get("k"); 547check(false, "get after pledge must reject"); 548} catch (e) { 549check(String(e.message).includes("read state.kv"), "get denial: " + e.message); 550} 551try { 552await db.keyspace("a").set("k", "v2"); 553check(false, "set after pledge must reject"); 554} catch (e) { 555// Reads are re-authorized before writes, so the read denial surfaces first. 556check(String(e.message).includes("read state.kv"), "set denial: " + e.message); 557} 558if (failures.length !== 0) { 559for (let i = 0; i < failures.length; i++) console.error("FAIL: " + failures[i]); 560throw new Error(failures.length + " failure(s)"); 561} 562console.log("KV-OK pledge"); 563"# , 564& [], 565); 566assert_marker ( & pledged, "KV-OK pledge" ); 567} 568 569# [ test ] 570fn invalid_keys_and_values_reject () { 571let dir = tempfile:: tempdir (). unwrap (); 572let source =r#"import { openKv } from "avril:kv"; 573const failures = []; 574function check(cond, message) { 575if (!cond) failures[failures.length] = message; 576} 577async function rejectsTypeError(fn, label) { 578try { 579await fn(); 580} catch (e) { 581check(e instanceof TypeError, label + " is a TypeError, got: " + e); 582return; 583} 584check(false, label + " must reject"); 585} 586const db = await openKv("state.kv", { create: true }); 587const ks = db.keyspace("a"); 588await rejectsTypeError(() => ks.set("k", 42), "number value"); 589await rejectsTypeError(() => ks.set("k", { nested: true }), "object value"); 590await rejectsTypeError(() => ks.set("k", () => {}), "function value"); 591await rejectsTypeError(() => ks.set("k", Symbol("s")), "symbol value"); 592await rejectsTypeError(() => ks.set("k", undefined), "top-level undefined"); 593await rejectsTypeError(() => ks.get(42), "number key"); 594await rejectsTypeError(() => ks.get(Object.create(Uint8Array.prototype)), "forged Uint8Array key"); 595class Bytes extends Uint8Array {} 596await ks.set(new Bytes([7]), "subclass"); 597check((await ks.getText(new Uint8Array([7]))) === "subclass", "Uint8Array subclass key works"); 598await rejectsTypeError(() => ks.set("k", "\ud800"), "lone-surrogate value"); 599await rejectsTypeError(() => ks.get("\ud800"), "ill-formed string key"); 600await rejectsTypeError(() => db.atomic((tx) => tx.check(ks, "k", 42)), "number stamp"); 601await rejectsTypeError(() => db.atomic((tx) => tx.check(ks, "k", "1")), "string stamp"); 602try { 603db.keyspace(""); 604check(false, "empty keyspace name must throw"); 605} catch (e) { 606check(e instanceof TypeError, "empty keyspace is a TypeError"); 607} 608// Empty key and empty value are valid: bytes, including empty, are keys. 609await ks.set("", "empty-key"); 610await ks.set("empty-value", new Uint8Array([])); 611const ek = await ks.getText(""); 612check(ek === "empty-key", "empty string key works"); 613const ev = await ks.get("empty-value"); 614check(ev !== null && ev instanceof Uint8Array && ev.length === 0, "empty bytes value works"); 615// Non-UTF-8 bytes: get works, getText rejects. 616await ks.set("raw", new Uint8Array([0xff, 0x00])); 617const gotRaw = await ks.get("raw"); 618check(gotRaw !== null && Array.from(gotRaw).join(",") === "255,0", "non-UTF8 set works and reads bytes"); 619try { 620await ks.getText("raw"); 621check(false, "getText of non-UTF8 must reject"); 622} catch (e) { 623check(e instanceof TypeError, "getText rejection is a TypeError, got: " + e); 624} 625// Oversized batches: the offending tx call itself throws RangeError, so the 626// callback aborts and nothing commits. 627const huge = new Uint8Array(4 * 1024 * 1024); 628try { 629await db.atomic((tx) => { 630tx.set(ks, "huge", huge); 631}); 632check(false, "oversized atomic batch must throw"); 633} catch (e) { 634check(e instanceof RangeError, "oversized atomic batch is a RangeError, got: " + e); 635} 636check(await ks.get("huge") === null, "oversized atomic batch commits nothing"); 637let opCount = 0; 638try { 639await db.atomic((tx) => { 640for (; opCount <= 1000; opCount++) tx.delete(ks, "k" + opCount); 641}); 642check(false, "too many atomic ops must throw"); 643} catch (e) { 644check(e instanceof RangeError && opCount === 1000, "op 1001 is a RangeError, got: " + e + " at " + opCount); 645} 646if (failures.length !== 0) { 647for (let i = 0; i < failures.length; i++) console.error("FAIL: " + failures[i]); 648throw new Error(failures.length + " failure(s)"); 649} 650console.log("KV-OK invalid"); 651"# ; 652assert_marker ( & run ( dir. path (), "main.js" , source, & []), "KV-OK invalid" ); 653} 654 655# [ test ] 656fn poisoned_builtins_do_not_affect_roundtrip () { 657let dir = tempfile:: tempdir (). unwrap (); 658let source =r#"import { openKv } from "avril:kv"; 659const failures = []; 660function check(cond, message) { 661if (!cond) failures[failures.length] = message; 662} 663const db = await openKv("state.kv", { create: true }); 664const ks = db.keyspace("p"); 665await ks.set("s", "hello"); 666await ks.set(new Uint8Array([1, 2]), new Uint8Array([3, 4])); 667Array.prototype.push = function () { throw new Error("poisoned"); }; 668Map.prototype.set = function () { throw new Error("poisoned"); }; 669Object.prototype.evil = 1; 670const text = await ks.getText("s"); 671check(text === "hello", "string roundtrip under poisoning"); 672const bytes = await ks.get(new Uint8Array([1, 2])); 673check(bytes !== null && Array.from(bytes).join(",") === "3,4", "bytes roundtrip under poisoning"); 674const listed = []; 675for await (const e of ks.list()) listed[listed.length] = e.key.length; 676check(listed.length === 2, "list works under poisoning"); 677if (failures.length !== 0) { 678for (let i = 0; i < failures.length; i++) console.error("FAIL: " + failures[i]); 679throw new Error(failures.length + " failure(s)"); 680} 681console.log("KV-OK poison"); 682"# ; 683assert_marker ( & run ( dir. path (), "main.js" , source, & []), "KV-OK poison" ); 684}