15d7f5e873
- notescraft-lint (rules, safe fixes, learned style) and notescraft-blocks (block model and edits); engine lint.* and block.* commands with CLI and MCP parity - Lint tab, underlines, slash menu with preview in Source and Visual, block grip and menu, Settings > Editor and lint, Vim block keys - Visual view: tables, note embeds, %% comments, wrapping property chips - Vault knowledge (Related tab, assistant sources, Settings) and calendar plugin - Sidebar top padding fixed, dot grid follows PaperCraft spacing, right-card tabs no longer cover the collapse button Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
103 lines
2.9 KiB
Rust
103 lines
2.9 KiB
Rust
//! Hostile input must not panic or return unbounded output.
|
|
|
|
use notescraft_lint::{Context, MAX_ISSUES, check, fix, style::Vote};
|
|
|
|
struct Lcg(u64);
|
|
|
|
impl Lcg {
|
|
fn next(&mut self) -> u64 {
|
|
self.0 = self.0.wrapping_mul(6_364_136_223_846_793_005).wrapping_add(1_442_695_040_888_963_407);
|
|
self.0 >> 33
|
|
}
|
|
}
|
|
|
|
fn exercise(text: &str, ctx: &Context) {
|
|
let issues = check(text, ctx);
|
|
assert!(issues.len() <= MAX_ISSUES);
|
|
let _ = fix::apply_all_safe(text, &issues);
|
|
for i in &issues {
|
|
for f in i.fix.iter().chain(&i.extra_fixes) {
|
|
let _ = fix::apply(text, f);
|
|
}
|
|
}
|
|
let mut vote = Vote::default();
|
|
vote.add_note(text);
|
|
vote.remove_note(text);
|
|
}
|
|
|
|
#[test]
|
|
fn pseudo_random_bytes_never_panic() {
|
|
let ctx = Context { titles: vec![("a.md".into(), "a".into())], ..Context::default() };
|
|
let alphabet: Vec<&str> = vec![
|
|
"#",
|
|
"##",
|
|
"-",
|
|
"*",
|
|
"+",
|
|
" ",
|
|
" ",
|
|
"\n",
|
|
"\r\n",
|
|
"`",
|
|
"```",
|
|
"~~~",
|
|
"[[",
|
|
"]]",
|
|
"|",
|
|
"---",
|
|
"é",
|
|
"漢",
|
|
"\u{1f600}",
|
|
"a",
|
|
"1.",
|
|
"2026-10-17",
|
|
":",
|
|
"\t",
|
|
">",
|
|
"!",
|
|
];
|
|
let mut rng = Lcg(42);
|
|
for _ in 0..400 {
|
|
let len = usize::try_from(rng.next() % 200).unwrap_or(0);
|
|
let mut text = String::new();
|
|
for _ in 0..len {
|
|
text.push_str(alphabet[usize::try_from(rng.next()).unwrap_or(0) % alphabet.len()]);
|
|
}
|
|
exercise(&text, &ctx);
|
|
}
|
|
for _ in 0..200 {
|
|
let bytes: Vec<u8> = (0..300).map(|_| u8::try_from(rng.next() & 0xff).unwrap_or(0)).collect();
|
|
exercise(&String::from_utf8_lossy(&bytes), &ctx);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn many_headings_and_unclosed_fences() {
|
|
let many = "#\n###\n".repeat(50_000);
|
|
exercise(&many, &Context::default());
|
|
exercise(&"```\n- a\n* b\n".repeat(10), &Context::default());
|
|
exercise(&"---\n".repeat(1000), &Context::default());
|
|
exercise(&format!("{}\n", "[[x]] `".repeat(50_000)), &Context { titles: vec![("a.md".into(), "a".into())], ..Context::default() });
|
|
exercise(&" ".repeat(3_000_000), &Context::default());
|
|
exercise("", &Context::default());
|
|
}
|
|
|
|
#[test]
|
|
fn issues_are_capped() {
|
|
let text = "a \n".repeat(10_000);
|
|
assert_eq!(check(&text, &Context::default()).len(), MAX_ISSUES);
|
|
}
|
|
|
|
#[test]
|
|
#[ignore = "timing"]
|
|
fn hundred_kb_note_checks_under_ten_ms() {
|
|
let para = "# Heading\n\nSome text with a [[link]] and `code`.\n\n- item one\n- item two\n\n";
|
|
let text = para.repeat(100_000 / para.len());
|
|
let ctx = Context { titles: vec![("link.md".into(), "link".into())], ..Context::default() };
|
|
let _ = check(&text, &ctx);
|
|
let start = std::time::Instant::now();
|
|
let _ = check(&text, &ctx);
|
|
let took = start.elapsed();
|
|
assert!(took.as_millis() < 10, "took {took:?}");
|
|
}
|