import { gzipSync, gunzipSync, strFromU8 } from "fflate"; import { describe, expect, it } from "vitest"; import { exportFileName, sanitizeFileName } from "../../src/export/file-name"; import { resolveRasterSize } from "../../src/export/raster-export"; import { createSvgExport, gunzipWithLimit, readSvgFile, } from "../../src/export/svg-export"; const source = '\r\n รค๐Ÿ˜€\r\n\r\n'; describe("SVG, SVGZ and filename export", () => { it("exports exact UTF-8 SVG bytes without formatting or editor metadata", () => { const artifact = createSvgExport(source, "svg", "../CON: diagram.svgz"); expect(new TextDecoder().decode(artifact.bytes)).toBe(source); expect(artifact.fileName).toBe("CON- diagram.svg"); expect(artifact.blob.type).toBe("image/svg+xml"); }); it("creates deterministic SVGZ and decompresses to the exact source", () => { const first = createSvgExport(source, "svgz", "drawing.svg"); const second = createSvgExport(source, "svgz", "drawing.svg"); expect(first.bytes).toEqual(second.bytes); expect(strFromU8(gunzipSync(first.bytes))).toBe(source); expect(first.fileName).toBe("drawing.svgz"); expect(first.blob.type).toBe("application/gzip"); }); it("reads plain SVG and magic-byte SVGZ locally", async () => { const plain = await readSvgFile( new File([source], "drawing.svg", { type: "image/svg+xml" }), ); expect(plain.source).toBe(source); const compressed = createSvgExport(source, "svgz").bytes; const zipped = await readSvgFile( new File([compressed.buffer as ArrayBuffer], "mystery.bin"), ); expect(zipped.source).toBe(source); }); it("stops SVGZ expansion at the configured output boundary", () => { const compressed = gzipSync(new TextEncoder().encode("x".repeat(512))); expect(() => gunzipWithLimit(compressed, 64)).toThrow(/processing limit/u); }); it("rejects malformed XML and non-SVG roots", () => { expect(() => createSvgExport(" createSvgExport('', "svg"), ).toThrow(/root must be an SVG/u); }); it("normalizes dangerous names and preserves the requested extension", () => { expect(sanitizeFileName("../../\u202eaux:?*.svg")).not.toMatch( /[<>:"/\\|?*\u202e]/u, ); expect(exportFileName("image.jpeg", "webp")).toBe("image.webp"); expect(exportFileName("diagram.svgtools.json", "project")).toBe( "diagram.svgtools.json", ); expect( new TextEncoder().encode(exportFileName("๐Ÿ˜€".repeat(100), "project")) .byteLength, ).toBeLessThanOrEqual(180); }); }); describe("raster safety and sizing", () => { it("preserves aspect ratio and applies scale after resolving size", () => { expect(resolveRasterSize(source, { height: 300 })).toEqual({ width: 600, height: 300, aspectRatio: 2, }); expect(resolveRasterSize(source, { scale: 2 })).toEqual({ width: 400, height: 200, aspectRatio: 2, }); }); it("rejects active content and external resources at the raster boundary", () => { expect(() => resolveRasterSize( '', ), ).toThrow(/blocked/u); expect(() => resolveRasterSize( '', ), ).toThrow(/blocked/u); expect(() => resolveRasterSize( '', ), ).toThrow(/external resource/u); expect(() => resolveRasterSize( '', ), ).toThrow(/external CSS/u); }); it("allows local references and bounded image data in the sanitized projection", () => { expect(() => resolveRasterSize( '', ), ).not.toThrow(); }); it("enforces canvas safety limits", () => { expect(() => resolveRasterSize(source, { width: 16_000, height: 16_000 }), ).toThrow(/canvas safety limit/u); }); });