Skip to content

Commit ebbce60

Browse files
committed
fuzzer
1 parent c917d5a commit ebbce60

File tree

5 files changed

+263
-0
lines changed

5 files changed

+263
-0
lines changed

examples/fuzz/index.html

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
6+
<link rel="icon" type="image/svg+xml" href="data:" />
7+
<title>dhtml fuzzer</title>
8+
<script type="importmap">
9+
{
10+
"imports": {
11+
"dhtml": "./node_modules/dhtml/index.js",
12+
"dhtml/client": "./node_modules/dhtml/client.js",
13+
"dhtml/server": "./node_modules/dhtml/server.js"
14+
}
15+
}
16+
</script>
17+
<script type="module" src="main.js"></script>
18+
</head>
19+
<body></body>
20+
</html>

examples/fuzz/main.js

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
import { html } from 'dhtml'
2+
import { createRoot } from 'dhtml/client'
3+
import { renderToString } from 'dhtml/server'
4+
5+
function canonicalize(str) {
6+
const t = document.createElement('template')
7+
t.innerHTML = str
8+
return t.innerHTML
9+
}
10+
11+
/**
12+
* @param {...string} strings
13+
* @returns {TemplateStringsArray}
14+
*/
15+
function tsa(...strings) {
16+
return Object.assign(strings, { raw: strings })
17+
}
18+
19+
function tag(name) {
20+
const child = random()
21+
22+
return {
23+
render() {
24+
return html(tsa(`<${name}>`, `</${name}>`), child)
25+
},
26+
toString() {
27+
return `<${name}>${child}</${name}>`
28+
},
29+
}
30+
}
31+
function voidTag(name) {
32+
return {
33+
render() {
34+
return html(tsa(`<${name}>`))
35+
},
36+
toString() {
37+
return `<${name}>`
38+
},
39+
}
40+
}
41+
function text(raw) {
42+
return {
43+
render() {
44+
return raw
45+
},
46+
toString() {
47+
return raw
48+
.replaceAll('&', '&amp;')
49+
.replaceAll('<', '&lt;')
50+
.replaceAll('>', '&gt;')
51+
.replaceAll('"', '&quot;')
52+
.replaceAll("'", '&#39;')
53+
},
54+
}
55+
}
56+
function sequence(length) {
57+
const items = Array.from({ length }, () => random())
58+
return {
59+
render() {
60+
return items
61+
},
62+
toString() {
63+
return items.join('')
64+
},
65+
}
66+
}
67+
68+
const choices = [
69+
() => tag('a'),
70+
() => tag('p'),
71+
() => tag('span'),
72+
() => tag('div'),
73+
() => tag('table'),
74+
() => tag('tbody'),
75+
() => tag('tr'),
76+
() => tag('td'),
77+
() => tag('form'),
78+
() => tag('button'),
79+
() => tag('input'),
80+
() => voidTag('br'),
81+
() => tag('h1'),
82+
() => tag('h2'),
83+
() => tag('h3'),
84+
() => tag('ul'),
85+
() => tag('ol'),
86+
() => tag('li'),
87+
() => tag('section'),
88+
() => tag('article'),
89+
() => tag('header'),
90+
() => tag('footer'),
91+
() => tag('nav'),
92+
() => tag('main'),
93+
() => tag('aside'),
94+
() => tag('strong'),
95+
() => tag('em'),
96+
() => tag('code'),
97+
() => tag('pre'),
98+
() => tag('blockquote'),
99+
() => tag('label'),
100+
() => tag('select'),
101+
() => tag('option'),
102+
// () => tag('textarea'), // causes issues
103+
() => tag('fieldset'),
104+
() => tag('legend'),
105+
() => tag('figure'),
106+
() => tag('figcaption'),
107+
() => tag('caption'),
108+
() => tag('thead'),
109+
() => tag('tfoot'),
110+
() => tag('th'),
111+
() => tag('small'),
112+
() => tag('b'),
113+
() => tag('i'),
114+
() => tag('u'),
115+
() => tag('sub'),
116+
() => tag('sup'),
117+
() => tag('mark'),
118+
() => tag('del'),
119+
() => tag('ins'),
120+
() => tag('abbr'),
121+
() => tag('cite'),
122+
() => tag('q'),
123+
() => tag('dfn'),
124+
() => tag('time'),
125+
() => tag('address'),
126+
() => tag('details'),
127+
() => tag('summary'),
128+
() => tag('dialog'),
129+
() => tag('data'),
130+
() => tag('output'),
131+
() => tag('progress'),
132+
() => tag('meter'),
133+
() => voidTag('hr'),
134+
() => voidTag('img'),
135+
() => voidTag('area'),
136+
() => voidTag('base'),
137+
() => voidTag('col'),
138+
() => voidTag('embed'),
139+
() => voidTag('link'),
140+
() => voidTag('meta'),
141+
() => voidTag('param'),
142+
() => voidTag('source'),
143+
() => voidTag('track'),
144+
() => voidTag('wbr'),
145+
() => text('text'),
146+
() => text(''),
147+
() => text('<hello>'),
148+
() => sequence(2),
149+
() => sequence(3),
150+
() => sequence(4),
151+
() => sequence(5),
152+
() => ({
153+
render: () => null,
154+
toString: () => '',
155+
}),
156+
]
157+
function random() {
158+
return choices[Math.floor(Math.random() * choices.length)]()
159+
}
160+
161+
for (let i = 0; i < 10000; i++) {
162+
const app = random()
163+
164+
const el = document.createElement('div')
165+
const root = createRoot(el)
166+
const str = app.toString()
167+
168+
try {
169+
root.render(app)
170+
} catch (error) {
171+
console.warn(str, app)
172+
throw error
173+
}
174+
175+
const str2 = renderToString(app)
176+
177+
if (str !== str2) {
178+
console.log('ssr mismatch, expected:', str, 'got:', str2)
179+
}
180+
181+
if (canonicalize(str) !== str) continue
182+
183+
if (el.innerHTML !== str) {
184+
console.log('rendering mismatch, expected:', str, 'got:', el.innerHTML)
185+
}
186+
}
187+
console.log('done')

examples/fuzz/package.json

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"name": "@dhtml-examples/fuzz",
3+
"private": true,
4+
"type": "module",
5+
"scripts": {
6+
"check": "tsc"
7+
},
8+
"devDependencies": {
9+
"typescript": "~5.8.3"
10+
},
11+
"dependencies": {
12+
"dhtml": "file:../../dist"
13+
}
14+
}

examples/fuzz/tsconfig.json

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"compilerOptions": {
3+
"checkJs": true,
4+
"noEmit": true,
5+
"allowImportingTsExtensions": true,
6+
"verbatimModuleSyntax": true,
7+
"moduleResolution": "bundler",
8+
"module": "preserve",
9+
"target": "es2020"
10+
}
11+
}

package-lock.json

Lines changed: 31 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)