Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 20 additions & 2 deletions crates/ink/src/svg.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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, "<svg") {
Some(open) => open.contains(NS),
None => false,
Expand Down Expand Up @@ -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 = "<svg xmlns=\"http://www.w3.org/2000/svg\"><!--".to_string();
svg.push_str(&"a".repeat(4095 - svg.len()));
svg.push_str("\u{e9}--><rect width=\"1\" height=\"1\"/></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!("<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:nc=\"{}\">{}\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 {
Expand Down
19 changes: 19 additions & 0 deletions crates/store/src/page.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 = "<svg xmlns=\"http://www.w3.org/2000/svg\"><!--".to_string();
svg.push_str(&"a".repeat(4095 - svg.len()));
svg.push_str("\u{e9}--><rect width=\"1\" height=\"1\"/></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;
Expand Down
Loading