diff --git a/server/src/chat/transport/matrix/commands/mod.rs b/server/src/chat/transport/matrix/commands/mod.rs index 0244318..08e21a8 100644 --- a/server/src/chat/transport/matrix/commands/mod.rs +++ b/server/src/chat/transport/matrix/commands/mod.rs @@ -228,10 +228,8 @@ fn strip_bot_mention<'a>(message: &'a str, bot_name: &str, bot_user_id: &str) -> /// Case-insensitive prefix strip that also requires the match to end at a /// word boundary (whitespace, punctuation, or end-of-string). fn strip_prefix_ci<'a>(text: &'a str, prefix: &str) -> Option<&'a str> { - if text.len() < prefix.len() { - return None; - } - if !text[..prefix.len()].eq_ignore_ascii_case(prefix) { + let candidate = text.get(..prefix.len())?; + if !candidate.eq_ignore_ascii_case(prefix) { return None; } let rest = &text[prefix.len()..]; @@ -451,6 +449,22 @@ pub(crate) mod tests { assert_eq!(strip_prefix_ci("hello", "hello"), Some("")); } + #[test] + fn strip_prefix_ci_multibyte_no_panic_smart_quote() { + // "abcde\u{2019}xyz" — U+2019 is 3 bytes starting at byte 5. + // A prefix of length 6 (e.g. "abcdef") lands inside the 3-byte char. + // Previously this caused: "byte index 6 is not a char boundary". + let text = "abcde\u{2019}xyz"; + assert_eq!(strip_prefix_ci(text, "abcdef"), None); + } + + #[test] + fn strip_prefix_ci_multibyte_no_panic_emoji() { + // U+1F600 is 4 bytes starting at byte 3. Prefix length 4 lands inside it. + let text = "abc\u{1F600}def"; + assert_eq!(strip_prefix_ci(text, "abcd"), None); + } + // -- commands registry -------------------------------------------------- #[test]