Files
bun.sh/test/js/bun/css/css-fuzz.test.ts

255 lines
8.0 KiB
TypeScript

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();
}
}
});
}