From 696e4776ec6213d70a015b7cd2c2a934242f62c4 Mon Sep 17 00:00:00 2001 From: Thomas Stokes Date: Sat, 12 Jul 2025 14:44:49 +0800 Subject: [PATCH] fuzzer --- examples/fuzz/index.html | 20 ++++ examples/fuzz/main.js | 187 ++++++++++++++++++++++++++++++++++++ examples/fuzz/package.json | 14 +++ examples/fuzz/tsconfig.json | 11 +++ 4 files changed, 232 insertions(+) create mode 100644 examples/fuzz/index.html create mode 100644 examples/fuzz/main.js create mode 100644 examples/fuzz/package.json create mode 100644 examples/fuzz/tsconfig.json diff --git a/examples/fuzz/index.html b/examples/fuzz/index.html new file mode 100644 index 00000000..f4ee82fc --- /dev/null +++ b/examples/fuzz/index.html @@ -0,0 +1,20 @@ + + + + + + + dhtml fuzzer + + + + + diff --git a/examples/fuzz/main.js b/examples/fuzz/main.js new file mode 100644 index 00000000..ba8d8ecc --- /dev/null +++ b/examples/fuzz/main.js @@ -0,0 +1,187 @@ +import { html } from 'dhtml' +import { createRoot } from 'dhtml/client' +import { renderToString } from 'dhtml/server' + +function canonicalize(str) { + const t = document.createElement('template') + t.innerHTML = str + return t.innerHTML +} + +/** + * @param {...string} strings + * @returns {TemplateStringsArray} + */ +function tsa(...strings) { + return Object.assign(strings, { raw: strings }) +} + +function tag(name) { + const child = random() + + return { + render() { + return html(tsa(`<${name}>`, ``), child) + }, + toString() { + return `<${name}>${child}` + }, + } +} +function voidTag(name) { + return { + render() { + return html(tsa(`<${name}>`)) + }, + toString() { + return `<${name}>` + }, + } +} +function text(raw) { + return { + render() { + return raw + }, + toString() { + return raw + .replaceAll('&', '&') + .replaceAll('<', '<') + .replaceAll('>', '>') + .replaceAll('"', '"') + .replaceAll("'", ''') + }, + } +} +function sequence(length) { + const items = Array.from({ length }, () => random()) + return { + render() { + return items + }, + toString() { + return items.join('') + }, + } +} + +const choices = [ + () => tag('a'), + () => tag('p'), + () => tag('span'), + () => tag('div'), + () => tag('table'), + () => tag('tbody'), + () => tag('tr'), + () => tag('td'), + () => tag('form'), + () => tag('button'), + () => tag('input'), + () => voidTag('br'), + () => tag('h1'), + () => tag('h2'), + () => tag('h3'), + () => tag('ul'), + () => tag('ol'), + () => tag('li'), + () => tag('section'), + () => tag('article'), + () => tag('header'), + () => tag('footer'), + () => tag('nav'), + () => tag('main'), + () => tag('aside'), + () => tag('strong'), + () => tag('em'), + () => tag('code'), + () => tag('pre'), + () => tag('blockquote'), + () => tag('label'), + () => tag('select'), + () => tag('option'), + // () => tag('textarea'), // causes issues + () => tag('fieldset'), + () => tag('legend'), + () => tag('figure'), + () => tag('figcaption'), + () => tag('caption'), + () => tag('thead'), + () => tag('tfoot'), + () => tag('th'), + () => tag('small'), + () => tag('b'), + () => tag('i'), + () => tag('u'), + () => tag('sub'), + () => tag('sup'), + () => tag('mark'), + () => tag('del'), + () => tag('ins'), + () => tag('abbr'), + () => tag('cite'), + () => tag('q'), + () => tag('dfn'), + () => tag('time'), + () => tag('address'), + () => tag('details'), + () => tag('summary'), + () => tag('dialog'), + () => tag('data'), + () => tag('output'), + () => tag('progress'), + () => tag('meter'), + () => voidTag('hr'), + () => voidTag('img'), + () => voidTag('area'), + () => voidTag('base'), + () => voidTag('col'), + () => voidTag('embed'), + () => voidTag('link'), + () => voidTag('meta'), + () => voidTag('param'), + () => voidTag('source'), + () => voidTag('track'), + () => voidTag('wbr'), + () => text('text'), + () => text(''), + () => text(''), + () => sequence(2), + () => sequence(3), + () => sequence(4), + () => sequence(5), + () => ({ + render: () => null, + toString: () => '', + }), +] +function random() { + return choices[Math.floor(Math.random() * choices.length)]() +} + +for (let i = 0; i < 10000; i++) { + const app = random() + + const el = document.createElement('div') + const root = createRoot(el) + const str = app.toString() + + try { + root.render(app) + } catch (error) { + console.warn(str, app) + throw error + } + + const str2 = renderToString(app) + + if (str !== str2) { + console.log('ssr mismatch, expected:', str, 'got:', str2) + } + + if (canonicalize(str) !== str) continue + + if (el.innerHTML !== str) { + console.log('rendering mismatch, expected:', str, 'got:', el.innerHTML) + } +} +console.log('done') diff --git a/examples/fuzz/package.json b/examples/fuzz/package.json new file mode 100644 index 00000000..a77fe281 --- /dev/null +++ b/examples/fuzz/package.json @@ -0,0 +1,14 @@ +{ + "name": "@dhtml-examples/fuzz", + "private": true, + "type": "module", + "scripts": { + "check": "tsc" + }, + "devDependencies": { + "typescript": "~5.8.3" + }, + "dependencies": { + "dhtml": "file:../../dist" + } +} diff --git a/examples/fuzz/tsconfig.json b/examples/fuzz/tsconfig.json new file mode 100644 index 00000000..48cb72d3 --- /dev/null +++ b/examples/fuzz/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "checkJs": true, + "noEmit": true, + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleResolution": "bundler", + "module": "preserve", + "target": "es2020" + } +}