Files
Marcos Paulo 5e8020aa38
ci / ci (macos-latest) (push) Has been cancelled
ci / ci (ubuntu-latest) (push) Has been cancelled
ci / ci (windows-latest) (push) Has been cancelled
feat: Obsidian basics, editor previews, graph rework, query blocks, stacked tabs, pop-out windows
Editor previews (colour swatches, arrows, embeds), the 18 obsidian-basics tasks (find and replace
with regex, back/forward, auto-pair, footnotes, folding, search operators, split view, PDF/audio/
video embeds, spelling, drag and drop, pinned tabs, settings) and round 2: ```query blocks, stacked
tabs, pop-out windows, tasks ticked inside embeds, text around an embed, embeds that follow edits,
block menu shortcut. New crates: notescraft-regex (linear-time engine) and notescraft-syntax.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-19 11:21:19 -03:00

242 lines
11 KiB
Rust

//! Hostile input for the JSON half of the blocks crate: random alphabet soup, deep nesting,
//! half a million escaped quotes and absurd numbers.
//!
//! Invariant everywhere: nothing panics, the size and nesting caps are errors, and for any input
//! `format_json` accepts, formatting is idempotent and minifying loses nothing.
use notescraft_blocks::{Edited, check_json, format_json, format_json_block, json_span, looks_like_json, minify_json, minify_json_block};
use serde_json::Value;
/// Strings in the random sweep.
const RANDOM_CASES: usize = 3000;
/// Longest random string, in chars.
const RANDOM_MAX: usize = 120;
/// Bytes of the escaped-quote monster.
const QUOTE_PAIR: &str = "\\\"";
/// xorshift64: deterministic, tiny, no dependency.
struct Rng(u64);
impl Rng {
fn next(&mut self) -> u64 {
let mut x = self.0;
x ^= x << 13;
x ^= x >> 7;
x ^= x << 17;
self.0 = x;
x
}
/// Bytes from the JSON alphabet plus NUL and a few multi-byte shapes.
fn text(&mut self, len: usize) -> String {
const ALPHABET: &[char] = &[
'{', '}', '[', ']', '"', ',', ':', ' ', '\n', '\t', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'e', 'E', '.', '+', '-', 't', 'f', 'n', 'u',
'l', '\0', '\u{7f}', 'é', '😀', '\u{2028}', '\u{feff}',
];
(0..len)
.map(|_| {
let at = u64::try_from(ALPHABET.len()).unwrap_or(u64::MAX);
let at = usize::try_from(self.next() % at).unwrap_or(0);
*ALPHABET.get(at).unwrap_or(&'?')
})
.collect()
}
}
/// One note with the hostile text as the body of a `json` code block.
fn note(body: &str) -> String {
format!("# n\n\n```json\n{body}\n```\n\nafter\n")
}
/// The edit must be exactly one splice of the old note, and the caret must be inside the new text.
fn assert_edit_is_consistent(old: &str, edited: &Edited) {
let (range, new) = &edited.replace;
let head = old.get(..range.start).unwrap_or_default();
let tail = old.get(range.end..).unwrap_or_default();
let rebuilt = format!("{head}{new}{tail}");
assert_eq!(rebuilt, edited.text, "the edit is one splice of the old text");
assert!(edited.caret <= edited.text.len(), "the caret is past the text");
assert!(edited.text.is_char_boundary(edited.caret), "the caret is not on a char boundary");
assert!(edited.text.contains("after\n"), "the rest of the note survives");
}
/// Whatever the input, formatting must be idempotent and minifying must lose nothing.
fn assert_format_properties(text: &str) {
let Ok(formatted) = format_json(text) else { return };
let minified = minify_json(text).unwrap_or_default();
assert_eq!(minify_json(&formatted).unwrap_or_default(), minified, "minify(format(x)) == minify(x) for {text:.40}");
assert_eq!(format_json(&formatted).unwrap_or_default(), formatted, "format is idempotent for {text:.40}");
assert_eq!(serde_json::from_str::<Value>(text).ok(), serde_json::from_str::<Value>(&formatted).ok(), "same value for {text:.40}");
assert!(check_json(&formatted).is_ok());
}
#[test]
fn random_alphabet_soup_never_panics() {
let mut rng = Rng(0x1064_5dca_9e37_79b9);
for _ in 0..RANDOM_CASES {
let cap = u64::try_from(RANDOM_MAX).unwrap_or(u64::MAX).saturating_add(1);
let len = usize::try_from(rng.next() % cap).unwrap_or(0);
let text = rng.text(len);
let _ = check_json(&text);
let _ = looks_like_json(&text);
if let Some(span) = json_span(&text) {
assert!(span.start <= span.end && span.end <= text.len(), "span out of the body: {text:.40}");
assert!(text.is_char_boundary(span.start) && text.is_char_boundary(span.end), "span off a char boundary");
let inner = text.get(span.clone()).unwrap_or_default();
assert!(check_json(inner).is_ok(), "the span must hold valid JSON");
assert!(matches!((inner.chars().next(), inner.chars().next_back()), (Some('{'), Some('}')) | (Some('['), Some(']'))));
assert!(looks_like_json(&text));
}
assert_format_properties(&text);
}
}
/// `{"a":{"a":...1...}}` `depth` levels deep.
fn nest(depth: usize) -> String {
format!("{}1{}", "{\"a\":".repeat(depth), "}".repeat(depth))
}
#[test]
fn deep_nesting_is_handled_without_a_stack_overflow() {
let deep_ok = nest(100);
assert!(check_json(&deep_ok).is_ok(), "100 levels is valid JSON");
assert_format_properties(&deep_ok);
// 129 levels: valid JSON, and nothing overflows. See the ignored BUG test below: the rustdoc
// claims serde_json's limit of 128 applies, but `IgnoredAny` skips values iteratively.
let at_129 = nest(129);
assert!(check_json(&at_129).is_ok(), "129 levels parses");
assert!(check_json(&format!("{}0{}", "[".repeat(129), "]".repeat(129))).is_ok());
assert_format_properties(&at_129);
// Deep but small: under the byte cap, so the parser sees it and survives it.
let deep = format!("{}0{}", "[".repeat(200_000), "]".repeat(200_000));
assert!(deep.len() <= notescraft_blocks::MAX_JSON_BYTES, "{} bytes", deep.len());
assert!(check_json(&deep).is_ok(), "no stack overflow on deep nesting");
assert!(minify_json(&deep).is_ok());
// Unbalanced nesting is an error, at any depth.
for (open, close) in [("{", ""), ("", "}"), ("[[", "]]"), ("{\"a\":", ""), ("{\"a\": 1,", "")] {
let junk = format!("{}{}", open.repeat(200), close);
assert!(check_json(&junk).is_err(), "{junk:.20}");
assert!(format_json(&junk).is_err(), "{junk:.20}");
assert!(minify_json(&junk).is_err(), "{junk:.20}");
}
}
#[test]
#[ignore = "BUG: check_json's rustdoc says the parser's nesting limit applies, but 129 levels is accepted (IgnoredAny skips iteratively)"]
fn nesting_over_128_is_refused() {
let too_deep = nest(129);
assert!(check_json(&too_deep).is_err(), "129 levels should be over serde_json's limit of 128");
assert!(format_json(&too_deep).is_err());
assert!(minify_json(&too_deep).is_err());
}
#[test]
#[ignore = "BUG: format_json expands quadratically with depth: 2000 levels (6 KB in) already make a 4 MB string, so 1 MiB of nesting would try to build ~275 GB"]
fn formatting_deep_json_stays_bounded() {
let text = nest(2_000);
let formatted = format_json(&text).unwrap_or_default();
assert!(formatted.len() <= notescraft_blocks::MAX_JSON_BYTES, "{} bytes out of {} in", formatted.len(), text.len());
}
#[test]
fn a_string_with_half_a_million_escaped_quotes_survives() {
let text = format!("[{}]{}]", "\"", "").to_owned() + &QUOTE_PAIR.repeat(500_000) + "\"]";
assert!(text.len() > 1_000_000 && text.len() < notescraft_blocks::MAX_JSON_BYTES, "{} bytes", text.len());
assert!(check_json(&text).is_ok(), "a monster string is still valid JSON");
assert_format_properties(&text);
let formatted = format_json(&text).unwrap_or_default();
assert_eq!(formatted.matches(QUOTE_PAIR).count(), 500_000, "no escape is lost");
assert_eq!(minify_json(&formatted).unwrap_or_default(), minify_json(&text).unwrap_or_default());
}
#[test]
fn number_spelling_survives_formatting_byte_for_byte() {
for number in ["1e999999999", "-0", "0.1e-5", "1E5", "-0.0e0", "123456789012345678901234567890", "0", "-1.7976931348623157e308", "1e-999999"] {
let text = format!("{{\"a\":{number},\"b\":[{number}]}}");
let formatted = format_json(&text).unwrap_or_default();
assert_eq!(formatted.matches(number).count(), 2, "{number} must survive byte for byte: {formatted}");
assert_eq!(minify_json(&formatted).unwrap_or_default(), minify_json(&text).unwrap_or_default());
assert_eq!(format_json(&formatted).unwrap_or_default(), formatted.clone());
assert_eq!(serde_json::from_str::<Value>(&text).ok(), serde_json::from_str::<Value>(&formatted).ok(), "{number}");
}
assert_format_properties(r#"{"deep":[[[[{"x":1e999999999}]]]],"neg":[-0,-0.1e-5]}"#);
}
#[test]
fn the_size_cap_is_an_error_and_huge_bodies_do_not_panic() {
let over = format!("[\"{}\"]", "x".repeat(notescraft_blocks::MAX_JSON_BYTES));
assert!(check_json(&over).is_err(), "over the size cap");
assert!(format_json(&over).is_err() && minify_json(&over).is_err());
assert!(!looks_like_json(&over), "an oversized block holds no offerable JSON");
assert_eq!(json_span(&over), None);
let under = format!("[\"{}\"]", "x".repeat(100_000));
assert!(check_json(&under).is_ok());
assert_format_properties(&under);
}
#[test]
fn block_edits_on_hostile_bodies_never_panic_and_stay_consistent() {
let mut rng = Rng(0x5851_f42d_4c95_7f2d);
for case in 0..400 {
let len = usize::try_from(rng.next() % 80).unwrap_or(0);
let text = note(&rng.text(len));
let formatted = format_json_block(&text, 0);
let minified = minify_json_block(&text, 0);
if let Ok(edited) = &formatted {
assert_edit_is_consistent(&text, edited);
}
if let Ok(edited) = &minified {
assert_edit_is_consistent(&text, edited);
}
if let (Ok(f), Ok(m)) = (&formatted, &minified) {
assert_eq!(f.text, m.text, "case {case}: formatting and minifying differ");
}
}
// Absurd block indices are errors, never panics; only the fence (index 1) holds JSON.
for index in [0usize, 2, 3, 9, 10_000, usize::MAX] {
let text = note("{\"a\":1}");
assert!(format_json_block(&text, index).is_err(), "index {index}");
assert!(minify_json_block(&text, index).is_err(), "index {index}");
}
// Hostile shapes inside a fence: backticks, new lines, nul, fences of their own.
let bodies: Vec<String> = vec![
"`{\"a\":1}`".to_owned(),
"`{\"a\":${x}}`".to_owned(),
"`{}` + `{}`".to_owned(),
"{\"a\":\"```\"}".to_owned(),
"\u{0}{\"a\":1}".to_owned(),
"{\"a\":\"\\u0000\\uD83D\\uDE00\"}".to_owned(),
"```".to_owned(),
"{\"a\":1}\n{\"b\":2}".to_owned(),
"\u{feff}{\"a\":1}\u{2028}".to_owned(),
"\t{\"a\" : 1}\t".to_owned(),
"[".repeat(500),
format!("[\"{}\"]", "x".repeat(50_000)),
];
for body in &bodies {
let text = note(body);
let before = json_span(body).and_then(|span| serde_json::from_str::<Value>(body.get(span).unwrap_or_default()).ok());
for index in [0usize, 1, usize::MAX] {
if let Ok(edited) = format_json_block(&text, index) {
assert_edit_is_consistent(&text, &edited);
let after_body = json_of(&edited.text);
if let (Some(span), Some(want)) = (json_span(after_body), before.clone()) {
let got = after_body.get(span).and_then(|inner| serde_json::from_str::<Value>(inner).ok());
assert_eq!(got, Some(want), "formatting keeps the value of {body:.20}");
}
}
if let Ok(edited) = minify_json_block(&text, index) {
assert_edit_is_consistent(&text, &edited);
}
}
}
}
/// The body of the first fenced block in `text`.
fn json_of(text: &str) -> &str {
text.split("```").nth(1).unwrap_or_default()
}