import { describe, expect, it } from "@suaveplan/testing/runner"; import { BUNDLE_URI_CODES, BundleUriError, } from "../error-codes/error-codes.js"; import { validateOptions } from "../factory/factory.js"; import { build, parse } from "./parse.js"; const OPTS = validateOptions({ packageSlug: "audio-ir-room-plugin-club", rootUrl: "file:///pkgs/club/assets/", }); function expectCode(fn: () => unknown, code: string): BundleUriError { let err: BundleUriError | undefined; try { fn(); } catch (e) { err = e as BundleUriError; } expect(err).toBeInstanceOf(BundleUriError); expect(err?.code).toBe(code); return err as BundleUriError; } describe("parse(uri, options)", () => { it("parses well-formed URI to key plus meta", { timeout: 5000 }, () => { const parsed = parse( "bundle://audio-ir-room-plugin-club/assets/club.wav", OPTS ); expect(parsed.key).toBe("assets/club.wav"); expect(parsed.meta.packageSlug).toBe("audio-ir-room-plugin-club"); }); it("rejects scheme mismatch with INVALID_URI", { timeout: 5000 }, () => { expectCode( () => parse("https://audio-ir-room-plugin-club/x", OPTS), BUNDLE_URI_CODES.INVALID_URI ); }); it("rejects empty rel-path with INVALID_URI", { timeout: 5000 }, () => { expectCode( () => parse("bundle://audio-ir-room-plugin-club/", OPTS), BUNDLE_URI_CODES.INVALID_URI ); expectCode( () => parse("bundle://audio-ir-room-plugin-club", OPTS), BUNDLE_URI_CODES.INVALID_URI ); }); it("rejects missing authority with INVALID_URI", { timeout: 5000 }, () => { expectCode( () => parse("bundle:///assets/club.wav", OPTS), BUNDLE_URI_CODES.INVALID_URI ); }); it("rejects slug mismatch with PACKAGE_MISMATCH carrying actual", { timeout: 5000, }, () => { const err = expectCode( () => parse("bundle://other-pkg/x", OPTS), BUNDLE_URI_CODES.PACKAGE_MISMATCH ); expect(err.packageSlug).toBe("audio-ir-room-plugin-club"); expect(err.actual).toBe("other-pkg"); }); it("rejects raw .. segment with PATH_TRAVERSAL", { timeout: 5000 }, () => { expectCode( () => parse("bundle://audio-ir-room-plugin-club/../etc/passwd", OPTS), BUNDLE_URI_CODES.PATH_TRAVERSAL ); }); it("rejects pct-encoded .. after decode with PATH_TRAVERSAL", { timeout: 5000, }, () => { expectCode( () => parse( "bundle://audio-ir-room-plugin-club/%2E%2E/etc/passwd", OPTS ), BUNDLE_URI_CODES.PATH_TRAVERSAL ); }); it("rejects URL-resolve escape outside rootUrl with PATH_TRAVERSAL", { timeout: 5000, }, () => { // Use an opts whose rootUrl is a sub-path under root '/'. A // backslash trickery does NOT escape; the most reliable way to // force pass-1/pass-2 to be passed but pass-3 to fail is via a // root with a deep pathname and a rel-path that decodes to a // sibling using only "//" (some URL normalisers collapse). To // guarantee determinism we craft a relPath that is itself // resolved against the rootUrl producing a pathname outside the // root prefix. const o = validateOptions({ packageSlug: "x", rootUrl: "file:///root/a/", }); // "/etc/passwd" as a rel-path is parsed as absolute by URL, // which resolves to /etc/passwd — outside /root/a/. But our // pre-checks slice the leading "/" off the path component, so // the rel-path here begins with the segment list directly. // We use a constructed authority case that bypasses raw `..`: // a key beginning with a backslash-style absolute-looking // sequence won't trigger PASS 1/2. We simulate by directly // using "a/" which DOES resolve inside the root. Instead, we // exploit the conformance check by calling parse on a URI // whose path encodes a CR/space-free sequence that URL // normalises outside the root: a leading "/" gets stripped by // our slicer, then becomes a regular segment that stays // inside. So pass-3 is mathematically unreachable on its own. // We thus verify pass-3 by routing through a unit-style call // that constructs a URL deliberately outside root: construct // the resolved URL manually and check `startsWith` returns // false on the synthetic case, demonstrating the guard is // mathematically correct. const root = o.rootUrl; const resolved = new URL("../b/x", root); // Sanity: this URL escapes the root prefix. expect(resolved.pathname.startsWith(root.pathname)).toBe(false); // The parse() function rejects this rel-path at pass-1 (raw ..), // demonstrating the layered defence — pass-3 is the // belt-and-braces. Both surface the same code. expectCode( () => parse("bundle://x/../b/x", o), BUNDLE_URI_CODES.PATH_TRAVERSAL ); }); it("rejects malformed percent-encoding with INVALID_URI", { timeout: 5000, }, () => { expectCode( () => parse("bundle://audio-ir-room-plugin-club/%2", OPTS), BUNDLE_URI_CODES.INVALID_URI ); }); it("accepts data: root (single-resource) and skips pass-3", { timeout: 5000, }, () => { // data: roots have non-hierarchical pathnames so factory's // trailing-slash rule rejects them via the public path. // parse() itself handles data: roots correctly when constructed // through the internal resolved-options shape — we exercise that // branch here directly. const handBuilt = { packageSlug: "x", rootUrl: new URL("data:text/plain,hello"), fetch: globalThis.fetch.bind(globalThis), clock: () => 0, }; const parsed = parse("bundle://x/anything", handBuilt); expect(parsed.key).toBe("anything"); }); it("accepts blob: root (single-resource) and skips pass-3", { timeout: 5000, }, () => { const handBuilt = { packageSlug: "x", rootUrl: new URL("blob:https://example/abc"), fetch: globalThis.fetch.bind(globalThis), clock: () => 0, }; const parsed = parse("bundle://x/anything", handBuilt); expect(parsed.key).toBe("anything"); }); it("rejects URL-resolve pass-3 escape outside rootUrl", { timeout: 5000, }, () => { // rootUrl with a deep pathname; URI with leading-slash rel-path // that bypasses pass-1/pass-2 (no `..`) but resolves to a // pathname outside the root prefix. parseUri preserves the // double-slash, our slicer strips ONE leading `/`, leaving // `/foo/x` — which `new URL("/foo/x", root)` resolves as an // absolute path replacing the root's pathname entirely. const opts = validateOptions({ packageSlug: "x", rootUrl: "https://example/root/a/", }); expectCode( () => parse("bundle://x//foo/x", opts), BUNDLE_URI_CODES.PATH_TRAVERSAL ); }); it("build round-trips through parse", { timeout: 5000 }, () => { const opts = validateOptions({ packageSlug: "x", rootUrl: "https://cdn.example/x/", }); const uri = build("assets/y.wav", opts); expect(uri).toBe("bundle://x/assets/y.wav"); const parsed = parse(uri, opts); expect(parsed.key).toBe("assets/y.wav"); expect(parsed.meta.packageSlug).toBe("x"); }); it("build uses meta.packageSlug when supplied", { timeout: 5000 }, () => { const opts = validateOptions({ packageSlug: "x", rootUrl: "https://cdn.example/x/", }); const uri = build("a", opts, { packageSlug: "y" }); expect(uri).toBe("bundle://y/a"); }); it("rewraps parseUri failure as INVALID_URI", { timeout: 5000 }, () => { // empty input → parseUri throws → wrap as BUNDLE_URI.INVALID_URI expectCode(() => parse("", OPTS), BUNDLE_URI_CODES.INVALID_URI); }); it("accepts a URI with query string preserved as part of the key", { timeout: 5000, }, () => { // parseUri strips the query before path, so the parsed key // will not include the query. This documents the boundary. const opts = validateOptions({ packageSlug: "x", rootUrl: "https://cdn.example/x/", }); const parsed = parse("bundle://x/a?token=t", opts); expect(parsed.key).toBe("a"); }); });