From 21220a635d89e74753cd81f1ceb5612aa0f3d3f9 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Tue, 10 Feb 2026 16:10:50 +0000 Subject: [PATCH] Add fuzzing suite for Nostr parsing, DM decryption, and Magnet normalization - Added `scripts/agent/fuzz-utils.mjs` with random input generators. - Added `scripts/agent/fuzz-nostrEventSchemas.mjs` to fuzz event builders and validation. - Added `scripts/agent/fuzz-dmDecryptor.mjs` to fuzz DM decryption logic. - Added `scripts/agent/fuzz-magnetUtils.mjs` to fuzz magnet URI normalization. - Added `scripts/agent/summarize-fuzz.mjs` to aggregate reports. - Generated initial fuzzing reports in `artifacts/`. - Verified that target functions are robust against malformed inputs. Co-authored-by: PR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> --- scripts/agent/fuzz-dmDecryptor.mjs | 155 +++++----- scripts/agent/fuzz-magnetUtils.mjs | 85 +++--- scripts/agent/fuzz-nostrEventSchemas.mjs | 360 ++++++++++++++++++----- scripts/agent/fuzz-utils.mjs | 140 +++++++++ scripts/agent/summarize-fuzz.mjs | 77 +++++ 5 files changed, 624 insertions(+), 193 deletions(-) create mode 100644 scripts/agent/fuzz-utils.mjs create mode 100644 scripts/agent/summarize-fuzz.mjs diff --git a/scripts/agent/fuzz-dmDecryptor.mjs b/scripts/agent/fuzz-dmDecryptor.mjs index 8129c88e..65c69ae7 100644 --- a/scripts/agent/fuzz-dmDecryptor.mjs +++ b/scripts/agent/fuzz-dmDecryptor.mjs @@ -1,86 +1,91 @@ -// Fuzz harness for js/dmDecryptor.js -import "./setup-test-env.js"; -import { Fuzzer } from "./fuzz-lib.mjs"; +import { + randomInt, + randomBoolean, + randomString, + randomHex, + randomArray, + randomValue, + randomObject, + runFuzzer, +} from "./fuzz-utils.mjs"; import { decryptDM } from "../../js/dmDecryptor.js"; -const fuzzer = new Fuzzer("dmDecryptor"); +function genEvent() { + const kind = Math.random() < 0.8 ? (Math.random() < 0.5 ? 4 : 1059) : randomInt(0, 20000); + const tags = randomArray(() => { + const tagName = Math.random() < 0.5 ? "p" : (Math.random() < 0.5 ? "encrypted" : randomString(5)); + return [tagName, randomString(20), randomString(10)]; + }, 0, 10); -class MockDecryptor { - constructor(scheme, shouldFail = false) { - this.scheme = scheme; - this.shouldFail = shouldFail; - this.supportsGiftWrap = scheme === "nip44" || scheme === "nip44_v2"; - } - - async decrypt(pubkey, ciphertext) { - if (this.shouldFail) { - throw new Error("Decryption failed (mock)"); - } - // Return something that might be JSON or might be garbage - if (fuzzer.randBool()) { - // Return valid JSON event string (inner rumor) - return JSON.stringify({ - kind: 1, - content: "Hello world " + fuzzer.randString(10), - pubkey: fuzzer.randString(64, "0123456789abcdef"), - created_at: Math.floor(Date.now() / 1000), - tags: [] - }); - } else { - // Return garbage string - return fuzzer.randString(50); - } - } + return { + kind, + pubkey: randomHex(64), + created_at: randomInt(0, 2000000000), + tags, + content: randomString(randomInt(0, 500)), + }; } -async function fuzzTest(fuzzer, state) { - const schemes = ["nip04", "nip44", "nip44_v2", "unknown", null]; +function mockDecryptor(behavior) { + return { + scheme: Math.random() < 0.5 ? "nip44" : "nip04", + decrypt: async (pubkey, ciphertext) => { + if (behavior === "fail") { + throw new Error("Decryption failed"); + } + if (behavior === "invalid") { + return 123; // Invalid return type + } + if (behavior === "empty") { + return ""; + } + if (behavior === "garbage") { + return randomString(100); // Not JSON + } + // Success + const innerEvent = { + kind: randomInt(0, 20000), + pubkey: randomHex(64), + created_at: randomInt(0, 2000000000), + content: randomString(50), + tags: [], + }; + return JSON.stringify(innerEvent); + }, + supportsGiftWrap: Math.random() < 0.7, + }; +} - // Create decryptors +function genDecryptors() { + const count = randomInt(0, 5); const decryptors = []; - const numDecryptors = fuzzer.randInt(0, 3); - for(let i=0; i fuzzer.randString(10), 1, 4)); - } - - // Context - const context = { - actorPubkey: fuzzer.randString(64, "0123456789abcdef"), - decryptors: decryptors - }; - - state.input = { - event, - context - }; - - try { - await decryptDM(event, context); - } catch (err) { - // We expect decryptDM to handle errors gracefully and return { ok: false, errors: ... } - // If it throws, it's a bug (except maybe argument validation if we pass null event) - throw err; + // Sometimes add invalid decryptor objects + if (Math.random() < 0.2) { + decryptors.push(null); + decryptors.push({}); + decryptors.push({ decrypt: "not a function" }); } + return decryptors; } -fuzzer.runFuzzLoop(5000, fuzzTest).catch(console.error); +function genContext() { + return { + actorPubkey: randomHex(64), + decryptors: genDecryptors(), + }; +} + +function genArgs() { + return [genEvent(), genContext()]; +} + +async function main() { + await runFuzzer("decryptDM", decryptDM, genArgs); +} + +main().catch(console.error); diff --git a/scripts/agent/fuzz-magnetUtils.mjs b/scripts/agent/fuzz-magnetUtils.mjs index ad33d369..288703ae 100644 --- a/scripts/agent/fuzz-magnetUtils.mjs +++ b/scripts/agent/fuzz-magnetUtils.mjs @@ -1,45 +1,50 @@ -// Fuzz harness for js/magnetUtils.js -import "./setup-test-env.js"; -import { Fuzzer } from "./fuzz-lib.mjs"; -import { normalizeAndAugmentMagnet } from "../../js/magnetUtils.js"; +import { + randomInt, + randomBoolean, + randomString, + randomHex, + randomArray, + randomValue, + randomObject, + runFuzzer, +} from "./fuzz-utils.mjs"; +import { + normalizeAndAugmentMagnet, + safeDecodeMagnet, + extractBtihFromMagnet, + normalizeInfoHash, +} from "../../js/magnetUtils.js"; -const fuzzer = new Fuzzer("magnetUtils"); +function genMagnetString() { + if (Math.random() < 0.2) return randomString(100); // Garbage + if (Math.random() < 0.2) return ""; // Empty -async function fuzzTest(fuzzer, state) { - // Generate inputs - const rawValue = fuzzer.randBool() ? fuzzer.randString(100) : "magnet:?xt=urn:btih:" + fuzzer.randString(40, "0123456789abcdef"); - - // Inject garbage into magnet link - let malformedValue = rawValue; - if (fuzzer.randBool()) { - // url encode some chars - malformedValue = encodeURIComponent(rawValue); - } - - if (fuzzer.randBool()) { - // Inject huge string - malformedValue += fuzzer.randString(10000); - } - - const options = { - webSeed: fuzzer.randBool() ? fuzzer.randString(50) : [fuzzer.randString(50)], - torrentUrl: fuzzer.randString(50), - xs: fuzzer.randString(50), - extraTrackers: fuzzer.randArray(() => fuzzer.randString(30), 0, 3), - appProtocol: fuzzer.randBool() ? "http:" : "https:", - logger: () => {} - }; - - state.input = { - rawValue: malformedValue, - options - }; - - try { - normalizeAndAugmentMagnet(malformedValue, options); - } catch (err) { - throw err; + let magnet = "magnet:?"; + const params = []; + const count = randomInt(0, 5); + for (let i = 0; i < count; i++) { + const key = Math.random() < 0.5 ? (Math.random() < 0.5 ? "xt" : "tr") : randomString(2); + const value = randomString(20, true); + params.push(`${key}=${encodeURIComponent(value)}`); } + return magnet + params.join("&"); } -fuzzer.runFuzzLoop(5000, fuzzTest).catch(console.error); +function genOptions() { + return { + webSeed: Math.random() < 0.5 ? randomString(20) : [randomString(20), randomString(20)], + torrentUrl: Math.random() < 0.5 ? randomString(30) : undefined, + xs: Math.random() < 0.5 ? randomString(30) : undefined, + extraTrackers: Math.random() < 0.5 ? [randomString(30)] : undefined, + appProtocol: Math.random() < 0.5 ? "http:" : "https:", + }; +} + +async function main() { + await runFuzzer("safeDecodeMagnet", safeDecodeMagnet, () => [genMagnetString()]); + await runFuzzer("extractBtihFromMagnet", extractBtihFromMagnet, () => [genMagnetString()]); + await runFuzzer("normalizeInfoHash", normalizeInfoHash, () => [randomString(40)]); + await runFuzzer("normalizeAndAugmentMagnet", normalizeAndAugmentMagnet, () => [genMagnetString(), genOptions()]); +} + +main().catch(console.error); diff --git a/scripts/agent/fuzz-nostrEventSchemas.mjs b/scripts/agent/fuzz-nostrEventSchemas.mjs index 7b9f3e0a..2d0ed8d6 100644 --- a/scripts/agent/fuzz-nostrEventSchemas.mjs +++ b/scripts/agent/fuzz-nostrEventSchemas.mjs @@ -1,81 +1,285 @@ -// Fuzz harness for js/nostrEventSchemas.js -import "./setup-test-env.js"; -import { Fuzzer } from "./fuzz-lib.mjs"; -import * as schemas from "../../js/nostrEventSchemas.js"; +import { + randomInt, + randomBoolean, + randomString, + randomHex, + randomArray, + randomValue, + randomObject, + runFuzzer, +} from "./fuzz-utils.mjs"; +import * as Schemas from "../../js/nostrEventSchemas.js"; -const fuzzer = new Fuzzer("nostrEventSchemas"); - -// Mappers for build functions -const BUILDERS = Object.keys(schemas) - .filter(k => k.startsWith('build')) - .map(k => ({ name: k, fn: schemas[k] })); - -async function fuzzTest(fuzzer, state) { - // 1. Fuzz Builder Functions - const builder = fuzzer.pick(BUILDERS); - - // Generate random params - const params = fuzzer.randJSON(); - - // Inject huge strings occasionally - if (Math.random() < 0.05) { - if (!params || typeof params !== 'object') { - // make it an object - } else { - // add huge string field - params.hugeField = "A".repeat(1000000); // 1MB - } - } - - state.input = { - target: builder.name, - params: params - }; - - try { - let args = [params]; - if (builder.name === 'buildAdminListEvent') { - const listKeys = ["moderation", "editors", "whitelist", "blacklist", "invalid", null]; - args = [fuzzer.pick(listKeys), params]; - } - - const event = builder.fn(...args); - - // 2. Validate the generated event - if (event && event.kind) { - // Find schema type for this kind to validate - let type = null; - for (const [key, val] of Object.entries(schemas.NOTE_TYPES)) { - const schema = schemas.getNostrEventSchema(val); - if (schema && schema.kind === event.kind) { - type = val; - break; - } - } - - if (type) { - schemas.validateEventAgainstSchema(type, event); - } - } - } catch (err) { - throw err; - } - - // 3. Fuzz Validation directly with random events - if (fuzzer.randBool()) { - const types = Object.values(schemas.NOTE_TYPES); - const type = fuzzer.pick(types); - const randomEvent = fuzzer.randJSON(); - - state.input = { - target: "validateEventStructure", - type: type, - event: randomEvent - }; - - schemas.validateEventStructure(type, randomEvent); - } +function genTag() { + return randomArray(() => randomString(randomInt(0, 20), true), 0, 5); } -// Run for 5000 iterations -fuzzer.runFuzzLoop(5000, fuzzTest).catch(console.error); +function genAdditionalTags() { + const choice = randomInt(0, 3); + if (choice === 0) return [null]; // Invalid input + if (choice === 1) return [randomString(100)]; // Invalid input + return [randomArray(genTag, 0, 10)]; +} + +function genEvent() { + return { + kind: randomInt(0, 50000), + pubkey: randomHex(64), + created_at: randomInt(0, 2000000000), + tags: randomArray(genTag, 0, 20), + content: randomString(randomInt(0, 1000), true), + }; +} + +function genValidateEventStructureArgs() { + const type = randomArray(() => randomString(10), 1)[0]; // Random type + // Also pick a valid type sometimes + const validTypes = Object.values(Schemas.NOTE_TYPES); + const selectedType = + Math.random() < 0.5 + ? validTypes[randomInt(0, validTypes.length - 1)] + : type; + + const event = Math.random() < 0.1 ? null : genEvent(); + return [selectedType, event]; +} + +function genParams(extraKeys = []) { + const base = randomObject(2, 2); + const commonKeys = [ + "pubkey", + "created_at", + "additionalTags", + "content", + "tags", + ]; + const keys = [...commonKeys, ...extraKeys]; + + keys.forEach((key) => { + if (Math.random() < 0.7) { + if (key === "additionalTags") { + base[key] = randomArray(genTag, 0, 5); + } else if (key === "created_at") { + base[key] = randomInt(0, 2000000000); + } else if (key === "pubkey") { + base[key] = randomHex(64); + } else { + base[key] = randomValue(0, 2); + } + } + }); + + return [base]; +} + +async function main() { + await runFuzzer( + "sanitizeAdditionalTags", + Schemas.sanitizeAdditionalTags, + genAdditionalTags + ); + await runFuzzer( + "validateEventStructure", + Schemas.validateEventStructure, + genValidateEventStructureArgs + ); + + // Build functions + const buildFunctions = [ + { + name: "buildVideoPostEvent", + fn: Schemas.buildVideoPostEvent, + keys: ["dTagValue", "infoHash", "url", "videoRootId"], + }, + { + name: "buildHttpAuthEvent", + fn: Schemas.buildHttpAuthEvent, + keys: ["url", "method", "payload"], + }, + { + name: "buildReportEvent", + fn: Schemas.buildReportEvent, + keys: ["eventId", "userId", "reportType", "relayHint"], + }, + { + name: "buildGiftWrapEvent", + fn: Schemas.buildGiftWrapEvent, + keys: ["recipientPubkey", "ciphertext", "relayHint"], + }, + { + name: "buildSealEvent", + fn: Schemas.buildSealEvent, + keys: ["ciphertext"], + }, + { + name: "buildChatMessageEvent", + fn: Schemas.buildChatMessageEvent, + keys: ["recipientPubkey"], + }, + { + name: "buildVideoMirrorEvent", + fn: Schemas.buildVideoMirrorEvent, + keys: [], + }, + { + name: "buildRepostEvent", + fn: Schemas.buildRepostEvent, + keys: [ + "eventId", + "eventRelay", + "address", + "addressRelay", + "authorPubkey", + "repostKind", + "targetKind", + "targetEvent", + "serializedEvent", + ], + }, + { + name: "buildShareEvent", + fn: Schemas.buildShareEvent, + keys: ["video", "relays"], + }, + { + name: "buildRelayListEvent", + fn: Schemas.buildRelayListEvent, + keys: ["relays"], + }, + { + name: "buildDmRelayListEvent", + fn: Schemas.buildDmRelayListEvent, + keys: ["relays"], + }, + { + name: "buildProfileMetadataEvent", + fn: Schemas.buildProfileMetadataEvent, + keys: ["metadata"], + }, + { + name: "buildMuteListEvent", + fn: Schemas.buildMuteListEvent, + keys: ["pTags", "encrypted", "encryptionTag"], + }, + { + name: "buildDeletionEvent", + fn: Schemas.buildDeletionEvent, + keys: ["eventIds", "addresses", "reason"], + }, + { + name: "buildLegacyDirectMessageEvent", + fn: Schemas.buildLegacyDirectMessageEvent, + keys: ["recipientPubkey", "ciphertext"], + }, + { + name: "buildDmAttachmentEvent", + fn: Schemas.buildDmAttachmentEvent, + keys: ["recipientPubkey", "attachment"], + }, + { + name: "buildDmReadReceiptEvent", + fn: Schemas.buildDmReadReceiptEvent, + keys: ["recipientPubkey", "eventId", "messageKind"], + }, + { + name: "buildDmTypingIndicatorEvent", + fn: Schemas.buildDmTypingIndicatorEvent, + keys: ["recipientPubkey", "eventId", "expiresAt"], + }, + { + name: "buildViewEvent", + fn: Schemas.buildViewEvent, + keys: [ + "pointerValue", + "pointerTag", + "pointerTags", + "dedupeTag", + "includeSessionTag", + ], + }, + { + name: "buildZapRequestEvent", + fn: Schemas.buildZapRequestEvent, + keys: [ + "recipientPubkey", + "relays", + "amountSats", + "lnurl", + "eventId", + "coordinate", + ], + }, + { + name: "buildReactionEvent", + fn: Schemas.buildReactionEvent, + keys: [ + "pointerValue", + "pointerTag", + "pointerTags", + "targetPointer", + "targetAuthorPubkey", + ], + }, + { + name: "buildCommentEvent", + fn: Schemas.buildCommentEvent, + keys: [ + "videoEventId", + "videoEventRelay", + "videoDefinitionAddress", + "videoDefinitionRelay", + "rootIdentifier", + "rootIdentifierRelay", + "parentCommentId", + "parentCommentRelay", + "threadParticipantPubkey", + "threadParticipantRelay", + "parentIdentifier", + "parentIdentifierRelay", + "rootKind", + "rootAuthorPubkey", + "rootAuthorRelay", + "parentAuthorPubkey", + "parentAuthorRelay", + "parentKind", + ], + }, + { + name: "buildWatchHistoryEvent", + fn: Schemas.buildWatchHistoryEvent, + keys: ["monthIdentifier", "pointerTags"], + }, + { + name: "buildSubscriptionListEvent", + fn: Schemas.buildSubscriptionListEvent, + keys: ["encryption"], + }, + { + name: "buildBlockListEvent", + fn: Schemas.buildBlockListEvent, + keys: ["encryption"], + }, + { + name: "buildHashtagPreferenceEvent", + fn: Schemas.buildHashtagPreferenceEvent, + keys: [], + }, + ]; + + for (const { name, fn, keys } of buildFunctions) { + await runFuzzer(name, fn, () => genParams(keys)); + } + + // buildAdminListEvent needs special handling for 2 args + await runFuzzer( + "buildAdminListEvent", + Schemas.buildAdminListEvent, + () => { + const listKey = Math.random() < 0.5 ? randomString(10) : (Math.random() < 0.5 ? "moderation" : "blacklist"); + const params = genParams(["hexPubkeys"])[0]; + return [listKey, params]; + } + ); +} + +main().catch(console.error); diff --git a/scripts/agent/fuzz-utils.mjs b/scripts/agent/fuzz-utils.mjs new file mode 100644 index 00000000..442975fb --- /dev/null +++ b/scripts/agent/fuzz-utils.mjs @@ -0,0 +1,140 @@ +import fs from "fs"; +import path from "path"; +import { fileURLToPath } from "url"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const ARTIFACTS_DIR = path.resolve(__dirname, "../../artifacts"); +const REPRODUCERS_DIR = path.resolve(__dirname, "../../examples/reproducers"); + +if (!fs.existsSync(ARTIFACTS_DIR)) { + fs.mkdirSync(ARTIFACTS_DIR, { recursive: true }); +} + +if (!fs.existsSync(REPRODUCERS_DIR)) { + fs.mkdirSync(REPRODUCERS_DIR, { recursive: true }); +} + +export function randomInt(min, max) { + return Math.floor(Math.random() * (max - min + 1)) + min; +} + +export function randomBoolean() { + return Math.random() < 0.5; +} + +export function randomString(length = 10, includeUnicode = false) { + let result = ""; + const characters = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + const unicodeChars = "😀😎🌍🚀中文测试ñçüéåß∂ƒ©˙∆˚¬…æΩ≈ç√∫˜µ≤≥÷"; + const controlChars = "\x00\x01\x02\n\r\t\b\f\v"; + + // Use Array.from to correctly handle surrogate pairs + const pool = Array.from( + characters + + (includeUnicode ? unicodeChars : "") + + (includeUnicode ? controlChars : "") + ); + + for (let i = 0; i < length; i++) { + result += pool[Math.floor(Math.random() * pool.length)]; + } + return result; +} + +export function randomHex(length = 64) { + let result = ""; + const characters = "0123456789abcdef"; + for (let i = 0; i < length; i++) { + result += characters.charAt(Math.floor(Math.random() * characters.length)); + } + return result; +} + +export function randomArray(generator, minLength = 0, maxLength = 10) { + const length = randomInt(minLength, maxLength); + const result = []; + for (let i = 0; i < length; i++) { + result.push(generator()); + } + return result; +} + +export function randomValue(depth = 0, maxDepth = 3) { + if (depth > maxDepth) { + return randomString(5); + } + const type = randomInt(0, 5); + switch (type) { + case 0: + return randomInt(-1000, 1000); + case 1: + return randomString(randomInt(0, 100), true); + case 2: + return randomBoolean(); + case 3: + return null; + case 4: + return undefined; + case 5: + return randomObject(depth + 1, maxDepth); + default: + return null; + } +} + +export function randomObject(depth = 0, maxDepth = 3) { + if (depth > maxDepth) { + return {}; + } + const numKeys = randomInt(0, 5); + const obj = {}; + for (let i = 0; i < numKeys; i++) { + const key = randomString(5); + obj[key] = randomValue(depth + 1, maxDepth); + } + return obj; +} + +export async function runFuzzer(targetName, fn, generator, iterations = 1000) { + const reportPath = path.join(ARTIFACTS_DIR, `fuzz-report-${targetName}.json`); + const failures = []; + + console.log(`Starting fuzzer for ${targetName} with ${iterations} iterations...`); + + for (let i = 0; i < iterations; i++) { + const inputs = generator(); + try { + const result = await fn(...inputs); + // Optional: Check if result is valid if we have a way to know + } catch (error) { + console.error(`Crash in ${targetName} at iteration ${i}:`, error.message); + const failure = { + iteration: i, + inputs, + error: { + message: error.message, + stack: error.stack, + name: error.name + }, + }; + failures.push(failure); + + // Save reproducer + const reproducerPath = path.join( + REPRODUCERS_DIR, + targetName, + `crash-${i}.json` + ); + if (!fs.existsSync(path.dirname(reproducerPath))) { + fs.mkdirSync(path.dirname(reproducerPath), { recursive: true }); + } + fs.writeFileSync(reproducerPath, JSON.stringify(inputs, null, 2)); + } + } + + fs.writeFileSync(reportPath, JSON.stringify(failures, null, 2)); + console.log(`Fuzzer finished. Found ${failures.length} failures. Report saved to ${reportPath}`); + return failures; +} diff --git a/scripts/agent/summarize-fuzz.mjs b/scripts/agent/summarize-fuzz.mjs new file mode 100644 index 00000000..da3490c8 --- /dev/null +++ b/scripts/agent/summarize-fuzz.mjs @@ -0,0 +1,77 @@ +import fs from "fs"; +import path from "path"; +import { fileURLToPath } from "url"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const ARTIFACTS_DIR = path.resolve(__dirname, "../../artifacts"); + +const TARGETS = { + nostrEventSchemas: [ + "sanitizeAdditionalTags", + "validateEventStructure", + "buildVideoPostEvent", + "buildHttpAuthEvent", + "buildReportEvent", + "buildGiftWrapEvent", + "buildSealEvent", + "buildChatMessageEvent", + "buildVideoMirrorEvent", + "buildRepostEvent", + "buildShareEvent", + "buildRelayListEvent", + "buildDmRelayListEvent", + "buildProfileMetadataEvent", + "buildMuteListEvent", + "buildDeletionEvent", + "buildLegacyDirectMessageEvent", + "buildDmAttachmentEvent", + "buildDmReadReceiptEvent", + "buildDmTypingIndicatorEvent", + "buildViewEvent", + "buildZapRequestEvent", + "buildReactionEvent", + "buildCommentEvent", + "buildWatchHistoryEvent", + "buildSubscriptionListEvent", + "buildBlockListEvent", + "buildHashtagPreferenceEvent", + "buildAdminListEvent", + ], + dmDecryptor: ["decryptDM"], + magnetUtils: [ + "safeDecodeMagnet", + "extractBtihFromMagnet", + "normalizeInfoHash", + "normalizeAndAugmentMagnet", + ], +}; + +function main() { + for (const [target, functions] of Object.entries(TARGETS)) { + const summary = { + target, + totalFailures: 0, + details: {}, + }; + + for (const fn of functions) { + const reportFile = path.join(ARTIFACTS_DIR, `fuzz-report-${fn}.json`); + if (fs.existsSync(reportFile)) { + try { + const data = JSON.parse(fs.readFileSync(reportFile, "utf8")); + summary.details[fn] = data; + summary.totalFailures += data.length; + } catch (e) { + console.error(`Failed to read report for ${fn}:`, e); + } + } + } + + const summaryPath = path.join(ARTIFACTS_DIR, `fuzz-report-${target}.json`); + fs.writeFileSync(summaryPath, JSON.stringify(summary, null, 2)); + console.log(`Generated summary for ${target} at ${summaryPath}`); + } +} + +main();