Files
2026-10-13 19:21:58 -03:00

126 lines
5.7 KiB
Rust

//! Hostile input: huge notes, thousands of links, unclosed constructs, deep
//! nesting and byte soup. Nothing here may panic or take unbounded memory.
use std::fmt::Write as _;
use notescraft_index::{Error, Index, Resolution};
#[test]
fn a_one_megabyte_note_is_indexed_without_blowing_up() {
let mut body = String::with_capacity(1 << 20);
while body.len() < 1_048_576 {
let line = format!("Paragraph {} with [[Target]] and #tag/n{} and text.\n", body.len(), body.len() % 7);
body.push_str(&line);
}
let mut index = Index::new();
index.update_note("huge.md", &body).unwrap();
index.update_note("target.md", "# Target\n").unwrap();
let note = index.note("huge.md").expect("indexed");
assert!(note.links.len() > 10_000, "got {}", note.links.len());
assert!(note.links.iter().all(|l| l.context.chars().count() <= notescraft_index::MAX_CONTEXT_CHARS + 1));
assert!(note.links.iter().all(|l| l.range.end <= body.len() && body.is_char_boundary(l.range.start)));
assert_eq!(index.backlinks("target.md").len(), note.links.len());
assert_eq!(index.unresolved(), Vec::new());
assert_eq!(index.tags().find("tag").map(|t| t.count), Some(1));
}
#[test]
fn ten_thousand_links_in_one_note_are_all_resolved() {
let body = (0..10_000).fold(String::new(), |mut out, i| {
let _ = writeln!(out, "[[Target#H{i}]]");
out
});
let headings = (0..10_000).fold(String::new(), |mut out, i| {
let _ = writeln!(out, "# H{i}");
out
});
let mut index = Index::new();
index.update_note("target.md", &headings).unwrap();
index.update_note("many.md", &body).unwrap();
assert_eq!(index.outgoing("many.md").len(), 10_000);
assert_eq!(index.backlinks("target.md").len(), 10_000);
assert_eq!(index.unresolved(), Vec::new());
assert!(index.outgoing("many.md").iter().all(|l| matches!(l.resolution, Resolution::Resolved { subpath_ok: true, .. })));
assert_eq!(index.all_link_edges(), vec![(0, 1)], "many.md is node 0, target.md is node 1");
}
#[test]
fn unclosed_and_byte_soup_constructs_are_kept_as_unresolved() {
let soup = "[[[[[]]]]] ![[ ![[|]] #[[ ^ $$ == %% ]]\n\0\u{feff}\u{202e}\n[[a\\|b]] [[ #]] [[#]]\n";
let mut index = Index::new();
index.update_note("soup.md", soup).unwrap();
index.update_note("a|b.md", "").unwrap();
let note = index.note("soup.md").expect("indexed");
assert!(note.links.iter().all(|l| l.range.end <= soup.len()));
assert_eq!(index.unresolved().len(), index.outgoing("soup.md").iter().filter(|l| !l.resolution.is_resolved()).count());
}
#[test]
fn deeply_nested_structures_do_not_overflow() {
let deep_quote = format!("{}#tag [[Target]]\n", "> ".repeat(2_000));
let deep_list = format!("{}[[Target]]\n", "- ".repeat(2_000));
let deep_callout = format!("{}> body [[Target]]\n", "> ".repeat(1_000));
let mut index = Index::new();
index.update_note("target.md", "").unwrap();
for (i, body) in [deep_quote, deep_list, deep_callout].iter().enumerate() {
let path = format!("deep{i}.md");
index.update_note(&path, body).unwrap();
assert!(index.note(&path).is_some());
}
assert_eq!(index.notes().len(), 4);
}
#[test]
fn a_very_long_target_and_a_very_long_tag_are_bounded() {
let mut index = Index::new();
let target = "x".repeat(2_000);
index.update_note(&format!("{target}.md"), "").unwrap();
index.update_note("src.md", &format!("[[{target}]]\n")).unwrap();
assert!(index.resolve("src.md", &target).is_resolved());
let deep_tag = (0..500).map(|i| format!("t{i}")).collect::<Vec<_>>().join("/");
index.update_note("tagged.md", &format!("#{deep_tag}\n")).unwrap();
assert_eq!(index.tags().len(), notescraft_index::MAX_TAG_DEPTH, "tag nesting is capped");
assert_eq!(index.note("tagged.md").map(|n| n.tags.len()), Some(1));
}
#[test]
fn many_notes_with_the_same_basename_stay_unambiguous_and_bounded() {
let mut index = Index::new();
for i in 0..500 {
index.update_note(&format!("f{i}/Same.md"), &format!("[[Same]] [[f{}/Same]]\n", (i + 1) % 500)).unwrap();
}
assert_eq!(index.notes().len(), 500);
assert!(matches!(index.resolve("f0/a.md", "Same"), Resolution::Resolved { ref path, ambiguous: true, .. } if path == "f0/Same.md"));
assert!(!matches!(index.resolve("f7/a.md", "f7/Same"), Resolution::Resolved { ambiguous: true, .. }), "an explicit path is exact");
assert_eq!(index.all_link_edges().len(), 500, "the cycle f0→f1→…→f499→f0, self loops dropped");
assert_eq!(index.backlinks("f0/Same.md").len(), 2, "the basename link and the explicit one");
}
#[test]
fn a_note_that_is_only_whitespace_or_delimiters_is_still_indexable() {
let mut index = Index::new();
for (i, body) in ["", "\n\n\n", "---\n---\n", "# \n", "[[", "![[", "==", "%%", "^", "$"].iter().enumerate() {
index.update_note(&format!("e{i}.md"), body).unwrap();
assert_eq!(index.note(&format!("e{i}.md")).map(|n| n.title.clone()), Some(format!("e{i}")));
}
assert_eq!(index.notes().len(), 10);
assert_eq!(index.unresolved(), Vec::new());
}
#[test]
fn reserved_and_hostile_paths_are_refused_everywhere() {
let mut index = Index::new();
for bad in [".obsidian/workspace.md", ".notescraft/cache/x.md", ".hidden.md", "a/.git/x.md"] {
assert!(matches!(index.update_note(bad, "x"), Err(Error::ReservedPath(_))), "{bad}");
assert!(matches!(index.add_file(bad), Err(Error::ReservedPath(_))), "{bad}");
}
let nul = "nul\0.md";
assert!(matches!(index.update_note(nul, "x"), Err(Error::InvalidPath(_))));
assert!(matches!(index.add_file(nul), Err(Error::InvalidPath(_))));
assert_eq!(index.notes(), Vec::<&str>::new());
assert_eq!(index.files(), Vec::<&str>::new());
}