restructure: move el compiler content into lang/

This commit is contained in:
Will Anderson
2026-05-05 01:38:51 -05:00
parent ce68f91a38
commit 1ae68962cf
143 changed files with 0 additions and 0 deletions
+150
View File
@@ -0,0 +1,150 @@
{
"allowlist": "{\"p\":[],\"br\":[],\"strong\":[],\"em\":[],\"u\":[],\"s\":[],\"code\":[],\"pre\":[],\"ul\":[],\"ol\":[],\"li\":[],\"h1\":[],\"h2\":[],\"h3\":[],\"h4\":[],\"blockquote\":[],\"a\":[\"href\",\"title\"]}",
"cases": [
{
"name": "01-pass-through-list",
"input": "<ol><li><strong>x</strong></li></ol>",
"expected": "<ol><li><strong>x</strong></li></ol>"
},
{
"name": "02-paragraphs-with-bold",
"input": "<p>hello <strong>world</strong></p><p>second</p>",
"expected": "<p>hello <strong>world</strong></p><p>second</p>"
},
{
"name": "03-pre-code-block",
"input": "<pre><code>npm install</code></pre>",
"expected": "<pre><code>npm install</code></pre>"
},
{
"name": "04-allowed-https-link",
"input": "<a href=\"https://example.com\">click</a>",
"expected": "<a href=\"https://example.com\">click</a>"
},
{
"name": "05-allowed-anchor-link",
"input": "<a href=\"#section\">jump</a>",
"expected": "<a href=\"#section\">jump</a>"
},
{
"name": "06-javascript-scheme-blocked",
"input": "<a href=\"javascript:alert(1)\">click</a>",
"expected": "<a>click</a>"
},
{
"name": "07-about-scheme-blocked",
"input": "<a href=\"about:blank#x\">click</a>",
"expected": "<a>click</a>"
},
{
"name": "08-data-uri-blocked",
"input": "<a href=\"data:text/html,<script>alert(1)</script>\">x</a>",
"expected": "<a>x</a>"
},
{
"name": "09-script-content-dropped",
"input": "before<script>alert(1)</script>after",
"expected": "beforeafter"
},
{
"name": "10-iframe-content-dropped",
"input": "<iframe src=\"evil\">junk</iframe>safe",
"expected": "safe"
},
{
"name": "11-form-content-dropped",
"input": "<form action=\"/steal\">x</form>safe",
"expected": "safe"
},
{
"name": "12-img-with-onerror-dropped",
"input": "<img src=x onerror=alert(2)>",
"expected": ""
},
{
"name": "13-comment-injection-bypass-blocked",
"input": "<form action=\"-->junk\">x</form>safe",
"expected": "safe"
},
{
"name": "14-mixed-legit-and-attack",
"input": "<p>hello</p><script>alert(1)</script><p>world</p>",
"expected": "<p>hello</p><p>world</p>"
},
{
"name": "15-pre-encoded-entities-preserved",
"input": "&lt;script&gt;alert(1)&lt;/script&gt;",
"expected": "&lt;script&gt;alert(1)&lt;/script&gt;"
},
{
"name": "16-unicode-in-href-preserved",
"input": "<a href=\"https://example.com/?q=日本語\">x</a>",
"expected": "<a href=\"https://example.com/?q=日本語\">x</a>"
},
{
"name": "17-unclosed-tag-passes-through",
"input": "<p>unclosed",
"expected": "<p>unclosed"
},
{
"name": "18-onclick-attribute-stripped-tag-survives",
"input": "<p onclick=\"x\">hi</p>",
"expected": "<p>hi</p>"
},
{
"name": "19-tab-bypass-in-scheme-blocked",
"input": "<a href=\"java\tscript:alert(1)\">x</a>",
"expected": "<a>x</a>"
},
{
"name": "20-uppercase-tag-and-attr-normalised",
"input": "<A HREF=\"https://example.com\">x</A>",
"expected": "<a href=\"https://example.com\">x</a>"
},
{
"name": "21-style-content-dropped",
"input": "<style>body{display:none}</style>visible",
"expected": "visible"
},
{
"name": "22-object-content-dropped",
"input": "<object data=\"x.swf\">flash</object>safe",
"expected": "safe"
},
{
"name": "23-svg-onload-dropped",
"input": "<svg onload=\"alert(1)\"><circle r=\"5\"/></svg>safe",
"expected": "safe"
},
{
"name": "24-blockquote-passthrough",
"input": "<blockquote>quoted text</blockquote>",
"expected": "<blockquote>quoted text</blockquote>"
},
{
"name": "25-headings-passthrough",
"input": "<h1>title</h1><h2>section</h2>",
"expected": "<h1>title</h1><h2>section</h2>"
},
{
"name": "26-attribute-value-with-gt-byte",
"input": "<a href=\"https://example.com/?q=1>2\" title=\"a > b\">x</a>",
"expected": "<a href=\"https://example.com/?q=1&gt;2\" title=\"a &gt; b\">x</a>"
},
{
"name": "27-nested-script-after-text",
"input": "lead<p>para</p><script>alert(1)</script>tail",
"expected": "lead<p>para</p>tail"
},
{
"name": "28-empty-input",
"input": "",
"expected": ""
},
{
"name": "29-plain-text",
"input": "just text, no tags",
"expected": "just text, no tags"
}
]
}
+32
View File
@@ -0,0 +1,32 @@
#!/usr/bin/env bash
# run.sh — build and execute the html_sanitizer acceptance corpus.
#
# Compiles tests/html_sanitizer/runner.el via the canonical native elc,
# links against the shared C runtime, then runs the binary against
# cases.json. Exits non-zero on any FAIL.
set -euo pipefail
cd "$(dirname "$0")"
EL_HOME="${EL_HOME:-$(cd ../.. && pwd)}"
ELC="${EL_HOME}/dist/platform/elc"
RUNTIME_DIR="${EL_HOME}/el-compiler/runtime"
if [ ! -x "${ELC}" ]; then
echo "elc not found at ${ELC}" >&2
exit 1
fi
OUT_C="$(mktemp -t html_sanitizer.XXXXXX).c"
OUT_BIN="$(mktemp -t html_sanitizer.XXXXXX)"
trap 'rm -f "${OUT_C}" "${OUT_BIN}"' EXIT
echo "==> Compiling runner.el via ${ELC}"
"${ELC}" runner.el > "${OUT_C}"
echo "==> Linking against ${RUNTIME_DIR}/el_runtime.c"
cc -O2 -I "${RUNTIME_DIR}" "${OUT_C}" "${RUNTIME_DIR}/el_runtime.c" \
-lcurl -lpthread -o "${OUT_BIN}"
echo "==> Running"
"${OUT_BIN}"
+59
View File
@@ -0,0 +1,59 @@
// runner.el acceptance tests for el_html_sanitize.
//
// Reads cases.json from the cwd, runs every case through
// el_html_sanitize, prints PASS/FAIL per case and a summary,
// and exits non-zero if any case failed.
//
// cases.json shape:
// {
// "allowlist": "<json string passed to el_html_sanitize>",
// "cases": [
// { "name": "...", "input": "...", "expected": "..." },
// ...
// ]
// }
fn run_case(name: String, input: String, expected: String, allowlist: String, idx: Int) -> Int {
let got: String = el_html_sanitize(input, allowlist)
if str_eq(got, expected) {
println("PASS " + name)
return 1
} else {
println("FAIL " + name)
println(" input: " + input)
println(" expected: " + expected)
println(" got: " + got)
return 0
}
}
fn main() -> Int {
let raw: String = fs_read("cases.json")
if str_eq(raw, "") {
println("FATAL could not read cases.json from cwd")
return 2
}
let allowlist: String = json_get(raw, "allowlist")
let cases_raw: String = json_get_raw(raw, "cases")
let n: Int = json_array_len(cases_raw)
let i: Int = 0
let pass: Int = 0
let fail: Int = 0
while i < n {
let case_obj: String = json_array_get(cases_raw, i)
let cname: String = json_get(case_obj, "name")
let cinp: String = json_get(case_obj, "input")
let cexp: String = json_get(case_obj, "expected")
let r: Int = run_case(cname, cinp, cexp, allowlist, i)
if r == 1 {
let pass = pass + 1
} else {
let fail = fail + 1
}
let i = i + 1
}
println("")
println(int_to_str(pass) + " passed, " + int_to_str(fail) + " failed")
if fail > 0 { return 1 }
return 0
}