|
| 1 | +/** |
| 2 | + * Portions adapted from web-platform-tests at revision 863077959ca8c1a7ceecfbe2534b75d2527b9013: |
| 3 | + * - html/webappapis/atob/base64.any.js (btoa reference encoder, input list, and atob WebIDL cases) |
| 4 | + * - fetch/data-urls/resources/base64.json (copied to fixtures/wpt-base64.json) |
| 5 | + * - WebCryptoAPI/randomUUID.https.any.js |
| 6 | + * |
| 7 | + * Copyright © web-platform-tests contributors. Governed by the 3-Clause BSD license in LICENSE.wpt. |
| 8 | + * |
| 9 | + * `assert_throws_dom("InvalidCharacterError", …)` becomes a check on `error.name`: CodeMode has no |
| 10 | + * DOMException, so the name is carried on a plain Error. |
| 11 | + */ |
| 12 | +import { describe, expect, test } from "bun:test" |
| 13 | +import { Effect } from "effect" |
| 14 | +import { CodeMode } from "../src/index.js" |
| 15 | + |
| 16 | +const base64Cases = (await Bun.file(new URL("./fixtures/wpt-base64.json", import.meta.url)).json()) as Array< |
| 17 | + [string, Array<number> | null] |
| 18 | +> |
| 19 | + |
| 20 | +const value = async (code: string) => { |
| 21 | + const result = await Effect.runPromise(CodeMode.execute({ code, tools: {} })) |
| 22 | + if (!result.ok) throw new Error(`expected success, got ${result.error.kind}: ${result.error.message}`) |
| 23 | + return result.value |
| 24 | +} |
| 25 | + |
| 26 | +// The reference encoder from base64.any.js, run inside the interpreter so btoa is checked against |
| 27 | +// an independent implementation rather than against the host's btoa. |
| 28 | +const referenceEncoder = ` |
| 29 | + function btoaLookup(idx) { |
| 30 | + if (idx < 26) return String.fromCharCode(idx + "A".charCodeAt(0)) |
| 31 | + if (idx < 52) return String.fromCharCode(idx - 26 + "a".charCodeAt(0)) |
| 32 | + if (idx < 62) return String.fromCharCode(idx - 52 + "0".charCodeAt(0)) |
| 33 | + if (idx == 62) return "+" |
| 34 | + if (idx == 63) return "/" |
| 35 | + } |
| 36 | + function mybtoa(s) { |
| 37 | + s = String(s) |
| 38 | + for (var i = 0; i < s.length; i++) if (s.charCodeAt(i) > 255) return "INVALID_CHARACTER_ERR" |
| 39 | + var out = "" |
| 40 | + for (var i = 0; i < s.length; i += 3) { |
| 41 | + var groupsOfSix = [undefined, undefined, undefined, undefined] |
| 42 | + groupsOfSix[0] = s.charCodeAt(i) >> 2 |
| 43 | + groupsOfSix[1] = (s.charCodeAt(i) & 0x03) << 4 |
| 44 | + if (s.length > i + 1) { |
| 45 | + groupsOfSix[1] |= s.charCodeAt(i + 1) >> 4 |
| 46 | + groupsOfSix[2] = (s.charCodeAt(i + 1) & 0x0f) << 2 |
| 47 | + } |
| 48 | + if (s.length > i + 2) { |
| 49 | + groupsOfSix[2] |= s.charCodeAt(i + 2) >> 6 |
| 50 | + groupsOfSix[3] = s.charCodeAt(i + 2) & 0x3f |
| 51 | + } |
| 52 | + for (var j = 0; j < groupsOfSix.length; j++) { |
| 53 | + out += typeof groupsOfSix[j] == "undefined" ? "=" : btoaLookup(groupsOfSix[j]) |
| 54 | + } |
| 55 | + } |
| 56 | + return out |
| 57 | + } |
| 58 | + function testBtoa(input) { |
| 59 | + var expected = mybtoa(input) |
| 60 | + if (expected === "INVALID_CHARACTER_ERR") { |
| 61 | + try { btoa(input) } catch (error) { return error.name === "InvalidCharacterError" ? "ok" : error.name } |
| 62 | + return "did not throw" |
| 63 | + } |
| 64 | + if (btoa(input) !== expected) return "btoa mismatch" |
| 65 | + if (atob(btoa(input)) !== String(input)) return "roundtrip mismatch" |
| 66 | + return "ok" |
| 67 | + } |
| 68 | +` |
| 69 | + |
| 70 | +describe("btoa WPT parity (html/webappapis/atob/base64.any.js)", () => { |
| 71 | + test("every input encodes like the reference encoder and round-trips through atob", async () => { |
| 72 | + expect( |
| 73 | + await value(` |
| 74 | + ${referenceEncoder} |
| 75 | + var tests = ["עברית", "", "ab", "abc", "abcd", "abcde", "\\xff\\xff\\xc0", "\\0a", "a\\0b", |
| 76 | + undefined, null, 7, 12, 1.5, true, false, NaN, +Infinity, -Infinity, 0, -0] |
| 77 | + for (var i = 0; i < 258; i++) tests.push(String.fromCharCode(i)) |
| 78 | + tests.push(String.fromCharCode(10000), String.fromCharCode(65534), String.fromCharCode(65535)) |
| 79 | + tests.push(String.fromCharCode(0xd800, 0xdc00)) |
| 80 | + var everything = "" |
| 81 | + for (var i = 0; i < 256; i++) everything += String.fromCharCode(i) |
| 82 | + tests.push(everything) |
| 83 | + return tests.map(testBtoa).filter((outcome) => outcome !== "ok") |
| 84 | + `), |
| 85 | + ).toEqual([]) |
| 86 | + }) |
| 87 | +}) |
| 88 | + |
| 89 | +describe("atob WPT parity (fetch/data-urls/resources/base64.json)", () => { |
| 90 | + const idlCases: Array<[unknown, Array<number> | null]> = [ |
| 91 | + [undefined, null], |
| 92 | + [null, [158, 233, 101]], |
| 93 | + [7, null], |
| 94 | + [12, [215]], |
| 95 | + [1.5, null], |
| 96 | + [true, [182, 187]], |
| 97 | + [false, null], |
| 98 | + [NaN, [53, 163]], |
| 99 | + [Infinity, [34, 119, 226, 158, 43, 114]], |
| 100 | + [-Infinity, null], |
| 101 | + [0, null], |
| 102 | + [-0, null], |
| 103 | + ] |
| 104 | + |
| 105 | + test(`${base64Cases.length} forgiving-base64 inputs decode to the expected bytes or throw InvalidCharacterError`, async () => { |
| 106 | + expect( |
| 107 | + await value(` |
| 108 | + const cases = ${JSON.stringify(base64Cases)} |
| 109 | + return cases.flatMap(([input, output]) => { |
| 110 | + try { |
| 111 | + const result = atob(input) |
| 112 | + if (output === null) return [[input, "expected throw"]] |
| 113 | + const bytes = Array.from({ length: result.length }, (_, i) => result.charCodeAt(i)) |
| 114 | + return JSON.stringify(bytes) === JSON.stringify(output) ? [] : [[input, bytes]] |
| 115 | + } catch (error) { |
| 116 | + return output === null && error.name === "InvalidCharacterError" ? [] : [[input, error.name]] |
| 117 | + } |
| 118 | + }) |
| 119 | + `), |
| 120 | + ).toEqual([]) |
| 121 | + }) |
| 122 | + |
| 123 | + test("WebIDL argument conversion stringifies non-string inputs", async () => { |
| 124 | + const literal = (input: unknown) => |
| 125 | + Object.is(input, -0) |
| 126 | + ? "-0" |
| 127 | + : typeof input === "number" || input === undefined |
| 128 | + ? String(input) |
| 129 | + : JSON.stringify(input) |
| 130 | + expect( |
| 131 | + await value(` |
| 132 | + const cases = [${idlCases.map(([input, output]) => `[${literal(input)}, ${JSON.stringify(output)}]`).join(",")}] |
| 133 | + return cases.flatMap(([input, output]) => { |
| 134 | + try { |
| 135 | + const result = atob(input) |
| 136 | + if (output === null) return [[String(input), "expected throw"]] |
| 137 | + // The source loop checks only the listed prefix of the decoded bytes. |
| 138 | + const bytes = output.map((_, i) => result.charCodeAt(i)) |
| 139 | + return JSON.stringify(bytes) === JSON.stringify(output) ? [] : [[String(input), bytes]] |
| 140 | + } catch (error) { |
| 141 | + return output === null && error.name === "InvalidCharacterError" ? [] : [[String(input), error.name]] |
| 142 | + } |
| 143 | + }) |
| 144 | + `), |
| 145 | + ).toEqual([]) |
| 146 | + }) |
| 147 | +}) |
| 148 | + |
| 149 | +describe("crypto.randomUUID WPT parity (WebCryptoAPI/randomUUID.https.any.js)", () => { |
| 150 | + test("namespace format, version, and variant bits over 256 iterations without collision", async () => { |
| 151 | + expect( |
| 152 | + await value(` |
| 153 | + const uuids = new Set() |
| 154 | + const randomUUID = () => { |
| 155 | + const uuid = crypto.randomUUID() |
| 156 | + if (uuids.has(uuid)) throw new Error("uuid collision " + uuid) |
| 157 | + uuids.add(uuid) |
| 158 | + return uuid |
| 159 | + } |
| 160 | + const UUIDRegex = /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/ |
| 161 | + let format = true, version = true, variant = true |
| 162 | + for (let i = 0; i < 256; i++) format = format && UUIDRegex.test(randomUUID()) |
| 163 | + for (let i = 0; i < 256; i++) version = version && (parseInt(randomUUID().split("-")[2].slice(0, 2), 16) & 0b11110000) === 0b01000000 |
| 164 | + for (let i = 0; i < 256; i++) variant = variant && (parseInt(randomUUID().split("-")[3].slice(0, 2), 16) & 0b11000000) === 0b10000000 |
| 165 | + return [format, version, variant, uuids.size] |
| 166 | + `), |
| 167 | + ).toEqual([true, true, true, 768]) |
| 168 | + }) |
| 169 | +}) |
0 commit comments