import { expect, test } from "bun:test"; import { isCI, isDebug } from "harness"; interface InvalidFuzzOptions { maxLength: number; strategy: "syntax" | "structure" | "encoding" | "memory" | "all"; iterations: number; } const shutup = process.env.CSS_FUZZ_SHUTUP === "1"; const log = shutup ? () => {} : console.log; // Collection of invalid CSS generation strategies const invalidGenerators = { // Syntax errors syntax: { unclosedRules: () => ` .test { color: red .another { padding: 10px }`, invalidSelectors: () => [ "}{color:red}", "&*#@.class{color:red}", "..double.dot{color:red}", ".{color:red}", "#{color:red}", ], malformedProperties: () => [ ".test{color:}", ".test{:red}", ".test{color::red}", ".test{;color:red}", ".test{color:red;;;}", ], unclosedComments: () => [ "/* unclosed comment .test{color:red}", ".test{color:red} /* unclosed", "/**//**//* .test{color:red}", ], } as const, // Structural errors structure: { nestedRules: () => [ ".outer { .inner { color: red } }", // Invalid nesting without @rules "@media screen { @media print { } ", // Unclosed nested at-rule "@keyframes { @keyframes { } }", // Invalid nesting of @keyframes ], malformedAtRules: () => ["@media ;", "@import url('test.css'", "@{color:red}", "@media screen and and {color:red}"], invalidImports: () => ["@import 'file' 'screen';", "@import url(;", "@import url('test.css') print"], } as const, // Encoding and character issues encoding: { invalidUTF8: () => [ `.test{content:"${Buffer.from([0xc0, 0x80]).toString()}"}`, `.test{content:"${Buffer.from([0xe0, 0x80, 0x80]).toString()}"}`, `.test{content:"${Buffer.from([0xf0, 0x80, 0x80, 0x80]).toString()}"}`, ], nullBytes: () => [`.test{color:red${"\0"};}`, `.te${"\0"}st{color:red}`, `${"\0"}.test{color:red}`], controlCharacters: () => { const controls = Array.from({ length: 32 }, (_, i) => String.fromCharCode(i)); return controls.map(char => `.test{color:${char}red}`); }, } as const, // Memory and resource stress memory: { deepNesting: (depth: number = 300) => { let css = ""; for (let i = 0; i < depth; i++) { css += "@media screen {"; } css += ".test{color:red}"; for (let i = 0; i < depth; i++) { css += "}"; } return css; }, longSelectors: (length: number = 100000) => { const selector = ".test".repeat(length); return `${selector}{color:red}`; }, manyProperties: (count: number = 10000) => { const properties = Array(count).fill("color:red;").join("\n"); return `.test{${properties}}`; }, } as const, } as const; // Helper to randomly corrupt CSS function corruptCSS(css: string): string { const corruptions = [ (s: string) => (s + "").replace(/{/g, "}"), (s: string) => (s + "").replace(/}/g, "{"), (s: string) => (s + "").replace(/:/g, ";"), (s: string) => (s + "").replace(/;/g, ":"), (s: string) => (s + "").slice(Math.floor(Math.random() * (s + "").length)), (s: string) => s + "" + "}}".repeat(Math.floor(Math.random() * 5)), (s: string) => (s + "").split("").reverse().join(""), (s: string) => (s + "").replace(/[a-z]/g, c => String.fromCharCode(97 + Math.floor(Math.random() * 26))), ]; const numCorruptions = Math.floor(Math.random() * 3) + 1; let corrupted = css; for (let i = 0; i < numCorruptions; i++) { const corruption = corruptions[Math.floor(Math.random() * corruptions.length)]; corrupted = corruption(corrupted); } return corrupted; } // TODO: if (!isCI) { // Main fuzzing test suite for invalid inputs test.each( [["syntax", 1000], ["structure", 1000], ["encoding", 500], !isDebug ? ["memory", 100] : []].filter( xs => xs.length > 0, ), )( "CSS Parser Invalid Input Fuzzing - %s (%d iterations)", async (strategy, iterations) => { const options: InvalidFuzzOptions = { maxLength: 10000, strategy: strategy as any, iterations, }; let crashCount = 0; let errorCount = 0; const startTime = performance.now(); for (let i = 0; i < options.iterations; i++) { let invalidCSS = ""; switch (strategy) { case "syntax": invalidCSS = invalidGenerators.syntax[ Object.keys(invalidGenerators.syntax)[ Math.floor(Math.random() * Object.keys(invalidGenerators.syntax).length) ] ]()[Math.floor(Math.random() * 5)]; break; case "structure": invalidCSS = invalidGenerators.structure[ Object.keys(invalidGenerators.structure)[ Math.floor(Math.random() * Object.keys(invalidGenerators.structure).length) ] ]()[Math.floor(Math.random() * 3)]; break; case "encoding": invalidCSS = invalidGenerators.encoding[ Object.keys(invalidGenerators.encoding)[ Math.floor(Math.random() * Object.keys(invalidGenerators.encoding).length) ] ]()[0]; break; case "memory": const memoryFuncs = Object.keys(invalidGenerators.memory); const selectedFunc = memoryFuncs[Math.floor(Math.random() * memoryFuncs.length)]; invalidCSS = invalidGenerators.memory[selectedFunc](); break; } // Further corrupt the CSS randomly if (Math.random() < 0.3) { invalidCSS = corruptCSS(invalidCSS); } log("--- CSS Fuzz ---"); invalidCSS = invalidCSS + ""; log(JSON.stringify(invalidCSS, null, 2)); await Bun.write("invalid.css", invalidCSS); try { const result = await Bun.build({ entrypoints: ["invalid.css"], }); // We expect the parser to either throw an error or return a valid result // If it returns undefined/null, that's a potential issue if (result === undefined || result === null) { crashCount++; console.error(`Parser returned ${result} for input:\n${invalidCSS.slice(0, 100)}...`); } } catch (error) { // Expected behavior for invalid CSS errorCount++; // Check for specific error types we want to track if (error instanceof RangeError || error instanceof TypeError) { console.warn(`Unexpected error type: ${error.constructor.name} for input:\n${invalidCSS.slice(0, 100)}...`); } } // Memory check every 100 iterations if (i % 100 === 0) { const heapUsed = process.memoryUsage().heapUsed / 1024 / 1024; expect(heapUsed).toBeLessThan(500); // Alert if memory usage exceeds 500MB } } const endTime = performance.now(); const duration = endTime - startTime; console.log(` Strategy: ${strategy} Total iterations: ${iterations} Crashes: ${crashCount} Expected errors: ${errorCount} Duration: ${duration.toFixed(2)}ms Average time per test: ${(duration / iterations).toFixed(2)}ms `); // We expect some errors for invalid input, but no crashes expect(crashCount).toBe(0); expect(errorCount).toBeGreaterThan(0); }, 10 * 1000, ); // Additional test for mixed valid/invalid input test("CSS Parser Mixed Input Fuzzing", async () => { const validCSS = ".test{color:red}"; for (let i = 0; i < 100; i++) { const mixedCSS = ` ${validCSS} ${corruptCSS(validCSS)} ${validCSS} `; console.log("--- Mixed CSS ---"); console.log(JSON.stringify(mixedCSS, null, 2)); await Bun.write("invalid.css", mixedCSS); try { await Bun.build({ entrypoints: ["invalid.css"], }); } catch (error) { // Expected to throw, but shouldn't crash expect(error).toBeDefined(); } } }); }