From dd4bd806c6cd7578a63b4d8e945a8eb645725fdf Mon Sep 17 00:00:00 2001 From: Adam Hitchcock Date: Sat, 3 Oct 2026 05:29:57 -0700 Subject: [PATCH] fix(github): a multibyte char after `<` no longer panics the markdown sanitizer (#1076) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `starts_with_tag` sliced `rest[..name.len()]` by bytes, so an issue or PR body with `<` followed by a multibyte char (e.g. `<日本`) panicked the GitHub panel's render and quit the app. It now uses `str::get`, so a non-boundary is just "not a tag". A regression test fails on the old code and passes now. Co-authored-by: Claude Opus 5.5 --- crates/tty7-core/src/core/github/markdown.rs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/crates/tty7-core/src/core/github/markdown.rs b/crates/tty7-core/src/core/github/markdown.rs index 9801044b..dfe641b1 100644 --- a/crates/tty7-core/src/core/github/markdown.rs +++ b/crates/tty7-core/src/core/github/markdown.rs @@ -256,8 +256,9 @@ fn starts_with_tag(s: &str, name: &str) -> bool { let Some(rest) = s.strip_prefix('<') else { return false; }; - rest.len() > name.len() - && rest[..name.len()].eq_ignore_ascii_case(name) + // `get`, not a slice: `name.len()` can land inside a multibyte char. + rest.get(..name.len()) + .is_some_and(|head| head.eq_ignore_ascii_case(name)) && rest[name.len()..] .chars() .next() @@ -609,6 +610,13 @@ mod tests { sanitize(src, "image") } + #[test] + fn a_multibyte_char_after_lt_is_prose() { + assert!(s("a <日本 b").contains("日本")); + assert!(!starts_with_tag("<日本", "img")); + assert!(starts_with_tag("", "img")); + } + #[test] fn github_hosted_images_stay_images() { let pasted = "https://github.com/user-attachments/assets/352d14e0-d11c";