From 6e92f64b9e8134afb642a3a702c9a237f42113a4 Mon Sep 17 00:00:00 2001 From: DWAA1660 Date: Thu, 12 Nov 2026 10:46:44 -0700 Subject: [PATCH] fix(ink): cut the SVG head at a character's edge `svg::is_ink` looked at the first 4096 bytes with a plain slice, which panicked when a character crossed byte 4096. The page scan reads such a head from every SVG beside a page, so an ordinary picture with a long comment took the window down while its page opened (#126). The head is now cut with `floor_char_boundary`. Fixes #126 Co-Authored-By: Claude Opus 5.5 --- crates/ink/src/svg.rs | 22 ++++++++++++++++++++-- crates/store/src/page.rs | 19 +++++++++++++++++++ 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/crates/ink/src/svg.rs b/crates/ink/src/svg.rs index fd05924f..352649c3 100644 --- a/crates/ink/src/svg.rs +++ b/crates/ink/src/svg.rs @@ -74,9 +74,11 @@ fn num(v: f32) -> String { /// the same strokes without the pen's readings - is not an ink file either, which is what /// keeps it from being adopted back as one. /// -/// Only the opening tag is read, so this can be asked of a large file cheaply. +/// Only the opening tag is read, so this can be asked of a large file cheaply. The first +/// 4096 bytes are cut at a character's edge: a character across that byte panicked the slice +/// and took down the page that held the file (issue #126). pub fn is_ink(text: &str) -> bool { - let head = &text[..text.len().min(4096)]; + let head = &text[..text.floor_char_boundary(4096)]; match find_tag(head, " open.contains(NS), None => false, @@ -290,6 +292,22 @@ mod tests { assert!(!super::is_ink("not svg at all")); } + /// Issue #126: a character across byte 4096 does not panic the look at the head, whether + /// the text is whole or a head read lossily, which can end in a replacement character. + #[test] + fn a_character_across_byte_4096_is_not_split() { + let mut svg = ""); + assert!(!svg.is_char_boundary(4096)); + assert!(!super::is_ink(&svg)); + let lossy = String::from_utf8_lossy(&svg.as_bytes()[..4096]).into_owned(); + assert!(lossy.ends_with('\u{fffd}') && !lossy.is_char_boundary(4096)); + assert!(!super::is_ink(&lossy)); + let ours = format!("{}\u{e9}", super::NS, "a".repeat(4096)); + assert!(super::is_ink(&ours), "ink is still known with long text after its tag"); + } + use super::*; fn sample() -> Drawing { diff --git a/crates/store/src/page.rs b/crates/store/src/page.rs index 86cf71f2..c7ead0d3 100644 --- a/crates/store/src/page.rs +++ b/crates/store/src/page.rs @@ -549,6 +549,25 @@ mod tests { assert_eq!(ink_files(&page), vec!["ink-2.svg".to_string(), "ink.svg".to_string()], "named in order, and only ours"); } + /// Issue #126: an ordinary SVG with a character across byte 4096 is read as a picture. + /// The head the scan reads is cut at 4096 bytes, which split the character, and looking + /// for the namespace in it panicked while the page was being opened. + #[test] + fn a_character_across_the_head_of_an_svg_does_not_stop_the_page_opening() { + let t = crate::testing::TempDir::new("ink-files-unicode"); + let s = crate::settings::Settings::at(t.path().join("s.toml")); + let nb = crate::ops::create_notebook(&t.path().join("nb"), "NB", None, &s).expect("nb"); + let page = crate::ops::create_node(&nb, "P", notecraft_model::NodeKind::Page, None, &s).expect("page"); + let mut svg = ""); + assert!(!svg.is_char_boundary(4096), "the character crosses byte 4096"); + fs::write(page.join("ordinary.svg"), &svg).expect("write"); + + assert!(ink_files(&page).is_empty(), "an ordinary picture, not ink"); + assert!(load_page(&page).is_ok()); + } + use super::*; use crate::ops; use crate::settings::Settings;