// @vitest-environment node import { createHash } from "node:crypto"; import { readFile } from "node:fs/promises"; import path from "node:path"; import { pathToFileURL } from "node:url"; import { afterAll, beforeAll, describe, expect, it } from "vitest"; import { RUST_ENGINE_VERSION, RUST_FLAVOUR_VERSION, RustEngineAdapter, } from "../../src/regex/execution/adapters/rust/RustEngineAdapter"; import { RustWasmBridge, type RustRegexWasmModule, } from "../../src/regex/execution/adapters/rust/RustWasmBridge"; import type { RegexExecutionRequest, RegexReplacementRequest, } from "../../src/regex/model/match"; const pack = path.resolve("public/engines/rust"); let adapter: RustEngineAdapter; function request( pattern: string, subject: string, overrides: Partial = {}, ): RegexExecutionRequest { return { flavour: "rust", flavourVersion: RUST_FLAVOUR_VERSION, pattern, flags: ["g"], subject, captureMetadata: [], scanAll: false, maximumMatches: 100, maximumCaptureRows: 1_000, ...overrides, }; } function replacement( pattern: string, subject: string, replacementText: string, overrides: Partial = {}, ): RegexReplacementRequest { return { ...request(pattern, subject), replacement: replacementText, maximumOutputBytes: 1_024, ...overrides, }; } beforeAll(async () => { const imported = (await import( `${pathToFileURL(path.join(pack, "rust-regex.mjs")).href}?conformance` )) as RustRegexWasmModule; imported.initSync({ module: await WebAssembly.compile( await readFile(path.join(pack, "rust-regex_bg.wasm")), ), }); adapter = new RustEngineAdapter( new RustWasmBridge(imported), "Node.js conformance", ); await adapter.load(); }); afterAll(() => { adapter?.terminate(); }); describe("Rust regex 1.13.1 WebAssembly conformance", () => { it("loads the exact crate and compiler identity", async () => { await expect(adapter.load()).resolves.toEqual( expect.objectContaining({ flavour: "rust", engineName: "Rust regex crate", engineVersion: RUST_ENGINE_VERSION, runtimeVersion: expect.stringContaining( "rustc 1.97.1 wasm32-unknown-unknown", ), offsetUnit: "utf8-byte", }), ); }); it("returns native UTF-8 byte offsets and named captures", async () => { const result = await adapter.execute( request("(?PπŸ˜€+)([A-Z]+)?", "xπŸ˜€πŸ˜€AB πŸ˜€"), ); expect(result.accepted).toBe(true); expect( result.matches.map(({ nativeRange, range }) => ({ nativeRange, range })), ).toEqual([ { nativeRange: { start: 1, end: 11, unit: "utf8-byte" }, range: { startUtf16: 1, endUtf16: 7 }, }, { nativeRange: { start: 12, end: 16, unit: "utf8-byte" }, range: { startUtf16: 8, endUtf16: 10 }, }, ]); expect(result.matches[0]?.captures[0]).toEqual( expect.objectContaining({ groupNumber: 1, groupName: "emoji", value: "πŸ˜€πŸ˜€", nativeRange: { start: 1, end: 9, unit: "utf8-byte" }, }), ); expect(result.matches[1]?.captures[1]).toEqual({ groupNumber: 2, status: "did-not-participate", }); }); it("uses Rust's native Unicode default and rejects unsupported constructs", async () => { const unicode = await adapter.execute( request("^\\w+$", "Grüße", { flags: [] }), ); expect(unicode.matches).toHaveLength(1); const unsupported = await adapter.execute(request("(?<=a)b", "ab")); expect(unsupported.accepted).toBe(false); expect(unsupported.diagnostics[0]).toEqual( expect.objectContaining({ code: "compile-error" }), ); }); it("maps i, m, s, U, u, x and R through RegexBuilder", async () => { const cases = [ ["a", "A", ["i"]], ["^b$", "a\nb\nc", ["m"]], ["a.b", "a\nb", ["s"]], ["a+?", "aaa", ["U"]], ["^ a + $", "aaa", ["x"]], ["^b$", "a\r\nb\r\nc", ["m", "R"]], ["\\p{Greek}+", "Ξ»", ["u"]], ] as const; for (const [pattern, subject, flags] of cases) { const result = await adapter.execute( request(pattern, subject, { flags: [...flags] }), ); expect(result.matches, `${flags.join("")}: ${pattern}`).toHaveLength(1); } const ungreedy = await adapter.execute( request("a+", "aaa", { flags: ["U"] }), ); expect(ungreedy.matches[0]?.value).toBe("a"); }); it("uses native Captures::expand replacement syntax", async () => { const result = await adapter.replace( replacement( "(?P[a-z]+)-(\\d+)", "a-1 b-22", "${word}[$2]/$word/$$", ), ); expect(result.execution.accepted).toBe(true); expect(result.output).toBe("a[1]/a/$ b[22]/b/$"); const longest = await adapter.replace(replacement("(a)", "a", "$1x|${1}x")); expect(longest.output).toBe("|ax"); }); it("bounds capture amplification and preserves the untouched partial suffix", async () => { const bounded = await adapter.replace( replacement("(a+)", "a".repeat(256 * 1024), "$1".repeat(4_096), { maximumOutputBytes: 5, }), ); expect(bounded.execution.accepted).toBe(true); expect(bounded.output).toBe("aaaaa"); expect(bounded.outputBytes).toBe(5); expect(bounded.outputTruncated).toBe(true); const partial = await adapter.replace( replacement("a", "aaa", "x", { maximumMatches: 1 }), ); expect(partial.execution.matches).toHaveLength(1); expect(partial.execution.truncated).toBe(true); expect(partial.output).toBe("xaa"); expect(partial.outputTruncated).toBe(false); }); it("enforces match/capture/output bounds without splitting UTF-8", async () => { const limited = await adapter.execute( request("a", "aaa", { maximumMatches: 1 }), ); expect(limited.matches).toHaveLength(1); expect(limited.truncated).toBe(true); const rows = await adapter.execute( request("(a)(b)", "ab", { maximumCaptureRows: 1 }), ); expect(rows.matches).toHaveLength(0); expect(rows.truncated).toBe(true); const output = await adapter.replace( replacement(".", "abc", "πŸ˜€", { maximumOutputBytes: 5 }), ); expect(output.output).toBe("πŸ˜€"); expect(output.outputBytes).toBe(4); expect(output.outputTruncated).toBe(true); }); it("rejects lone UTF-16 surrogates before entering Rust", async () => { await expect(adapter.execute(request(".", "\ud800"))).rejects.toThrow( /unpaired UTF-16 surrogate/u, ); }); it("accepts control-heavy subjects within the decoded 16 MiB contract", async () => { const subject = "\u0000".repeat(3 * 1024 * 1024); const result = await adapter.execute( request("^\\x00+$", subject, { flags: [], maximumMatches: 1, maximumCaptureRows: 1, }), ); expect(result.accepted).toBe(true); expect(result.matches[0]?.nativeRange).toEqual({ start: 0, end: subject.length, unit: "utf8-byte", }); }); it("ships verified metadata, licences, Cargo.lock and checksums", async () => { const metadata = JSON.parse( await readFile(path.join(pack, "engine-metadata.json"), "utf8"), ) as { engineVersion: string; source: { crateChecksumSha256: string; license: string }; toolchain: { rustcCommitSha1: string; wasmBindgenVersion: string; }; }; expect(metadata).toEqual( expect.objectContaining({ engineVersion: "1.13.1", source: expect.objectContaining({ crateChecksumSha256: "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d", license: "MIT OR Apache-2.0", }), toolchain: expect.objectContaining({ rustcCommitSha1: "8bab26f4f68e0e26f0bb7960be334d5b520ea452", wasmBindgenVersion: "0.2.126", }), }), ); const checksums = await readFile(path.join(pack, "SHA256SUMS"), "utf8"); for (const line of checksums.trim().split("\n")) { const [expected, name] = line.split(/ {2}/u); const value = await readFile(path.join(pack, name ?? "")); expect(createHash("sha256").update(value).digest("hex"), name).toBe( expected, ); } expect( await readFile(path.join(pack, "LICENSE-MIT.txt"), "utf8"), ).toContain("Permission is hereby granted"); expect( await readFile(path.join(pack, "LICENSE-Apache-2.0.txt"), "utf8"), ).toContain("Apache License"); }); });