test(examples): forbid authored browser javascript
req: examples/005
This commit is contained in:
@@ -711,6 +711,9 @@ what a valid business email is.
|
||||
### req: examples/004
|
||||
004 Canonical examples are compile-tested golden API contracts. Changing generated API shape requires updating the examples deliberately.
|
||||
|
||||
### req: examples/005
|
||||
005 Canonical examples must not contain user-authored browser JavaScript. They may load the shared slhx runtime (`/slhx.js`) and may use declarative `data-slhx-*` attributes; inline `<script>`, `on*=` event handlers, and `javascript:` URLs are forbidden outside opaque leaf-widget examples.
|
||||
|
||||
---
|
||||
|
||||
## milestone
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
#[test]
|
||||
fn canonical_examples_do_not_author_browser_javascript() {
|
||||
// req: examples/005
|
||||
let root = Path::new(env!("CARGO_MANIFEST_DIR")).parent().unwrap();
|
||||
let examples = root.join("examples");
|
||||
let mut failures = Vec::new();
|
||||
scan_examples(&examples, &mut |path, text| {
|
||||
if path.components().any(|part| part.as_os_str() == "tests") {
|
||||
return;
|
||||
}
|
||||
for (line_no, line) in text.lines().enumerate() {
|
||||
let trimmed = line.trim();
|
||||
if trimmed.contains("<script") && !trimmed.contains(r#"<script src="/slhx.js" defer></script>"#) {
|
||||
failures.push(format!("{}:{}: inline <script> is not allowed", path.display(), line_no + 1));
|
||||
}
|
||||
if contains_inline_event_handler(trimmed) {
|
||||
failures.push(format!("{}:{}: inline on*= handler is not allowed", path.display(), line_no + 1));
|
||||
}
|
||||
if trimmed.to_ascii_lowercase().contains("javascript:") {
|
||||
failures.push(format!("{}:{}: javascript: URL is not allowed", path.display(), line_no + 1));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
assert!(failures.is_empty(), "{}", failures.join("\n"));
|
||||
}
|
||||
|
||||
fn scan_examples(dir: &Path, visit: &mut impl FnMut(&Path, &str)) {
|
||||
for entry in std::fs::read_dir(dir).unwrap() {
|
||||
let entry = entry.unwrap();
|
||||
let path = entry.path();
|
||||
if path.is_dir() {
|
||||
scan_examples(&path, visit);
|
||||
continue;
|
||||
}
|
||||
if !is_example_source(&path) {
|
||||
continue;
|
||||
}
|
||||
let text = std::fs::read_to_string(&path).unwrap();
|
||||
visit(&path, &text);
|
||||
}
|
||||
}
|
||||
|
||||
fn is_example_source(path: &PathBuf) -> bool {
|
||||
matches!(path.extension().and_then(|ext| ext.to_str()), Some("rs" | "heml" | "html"))
|
||||
}
|
||||
|
||||
fn contains_inline_event_handler(line: &str) -> bool {
|
||||
let bytes = line.as_bytes();
|
||||
let mut i = 0;
|
||||
while i + 3 < bytes.len() {
|
||||
let boundary = bytes[i].is_ascii_whitespace() || bytes[i] == b'<';
|
||||
if boundary && bytes[i + 1] == b'o' && bytes[i + 2] == b'n' && bytes[i + 3].is_ascii_lowercase() {
|
||||
let mut j = i + 4;
|
||||
while j < bytes.len() && bytes[j].is_ascii_lowercase() {
|
||||
j += 1;
|
||||
}
|
||||
while j < bytes.len() && bytes[j].is_ascii_whitespace() {
|
||||
j += 1;
|
||||
}
|
||||
if j < bytes.len() && bytes[j] == b'=' {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
false
|
||||
}
|
||||
Reference in New Issue
Block a user