From f5e19d303814c9192cf68dba44e25b470ef29461 Mon Sep 17 00:00:00 2001 From: Ed Page Date: Thu, 4 Sep 2025 09:06:02 -0500 Subject: [PATCH] fix(cli): Don't panic with invalid utf-8 Fixes #1373 --- crates/typos-cli/src/bin/typos-cli/report.rs | 54 +++++++++++++------ .../tests/cmd/encodings.in/invalid_utf8.txt | 3 ++ crates/typos-cli/tests/cmd/encodings.toml | 6 +++ 3 files changed, 48 insertions(+), 15 deletions(-) create mode 100644 crates/typos-cli/tests/cmd/encodings.in/invalid_utf8.txt diff --git a/crates/typos-cli/src/bin/typos-cli/report.rs b/crates/typos-cli/src/bin/typos-cli/report.rs index a8f26776..c9931801 100644 --- a/crates/typos-cli/src/bin/typos-cli/report.rs +++ b/crates/typos-cli/src/bin/typos-cli/report.rs @@ -2,6 +2,7 @@ use std::borrow::Cow; use std::io::Write as _; +use std::ops::Range; use std::sync::{atomic, Mutex}; use annotate_snippets::Annotation; @@ -162,22 +163,24 @@ fn typo_to_group<'t>(msg: &'t Typo<'t>) -> Group<'t> { let group = match &msg.context { Some(Context::File(context)) => { let path = context.path.as_os_str().to_string_lossy(); - let line = String::from_utf8_lossy(msg.buffer.as_ref()); + let (line, span) = to_string(&msg.buffer, msg.byte_offset, msg.typo.len()); let snippet = Snippet::source(line) .path(path) .line_start(context.line_num); - append_corrections(msg, 0, snippet, group) + append_corrections(span, snippet, group) } Some(Context::Path(context)) => { - let path = context - .path - .parent() - .unwrap_or(std::path::Path::new(".")) - .join(""); - let path = path.as_os_str().to_string_lossy(); - let line = context.path.as_os_str().to_string_lossy(); + let parent = context.path.parent().unwrap_or(std::path::Path::new(".")); + let parent = parent.as_os_str().to_string_lossy(); + let mut line = parent.into_owned(); + line.push(std::path::MAIN_SEPARATOR); + let parent_len = line.len(); + let mut line = line.into_bytes(); + line.extend(msg.buffer.iter()); + let (line, span) = to_string(&line, parent_len + msg.byte_offset, msg.typo.len()); + let line = line.into_owned(); let snippet = Snippet::source(line); - append_corrections(msg, path.len(), snippet, group) + append_corrections(span, snippet, group) } Some(_) | None => group, }; @@ -185,18 +188,39 @@ fn typo_to_group<'t>(msg: &'t Typo<'t>) -> Group<'t> { } fn append_corrections<'t>( - msg: &'t Typo<'t>, - offset: usize, + span: Range, snippet: Snippet<'t, Annotation<'t>>, group: Group<'t>, ) -> Group<'t> { - let span_start = msg.byte_offset + offset; - let span_end = span_start + msg.typo.len(); - let span = span_start..span_end; let snippet = snippet.annotation(AnnotationKind::Primary.span(span)); group.element(snippet) } +fn to_string(line: &[u8], start: usize, len: usize) -> (Cow<'_, str>, Range) { + let end = start + len; + + if let Ok(line) = std::str::from_utf8(line) { + return (Cow::Borrowed(line), start..end); + } + + let prefix = &line[0..start]; + let prefix = String::from_utf8_lossy(prefix); + + let middle = &line[start..end]; + let middle = String::from_utf8_lossy(middle); + + let suffix = &line[end..]; + let suffix = String::from_utf8_lossy(suffix); + + let span_start = prefix.len(); + let span_end = span_start + middle.len(); + + ( + Cow::Owned(format!("{prefix}{middle}{suffix}")), + span_start..span_end, + ) +} + fn error_to_group<'e>(error: &'e Error<'e>) -> Group<'e> { let group = Group::with_title(Level::ERROR.primary_title(&error.msg)); match &error.context { diff --git a/crates/typos-cli/tests/cmd/encodings.in/invalid_utf8.txt b/crates/typos-cli/tests/cmd/encodings.in/invalid_utf8.txt new file mode 100644 index 00000000..6f5260e3 --- /dev/null +++ b/crates/typos-cli/tests/cmd/encodings.in/invalid_utf8.txt @@ -0,0 +1,3 @@ +Not valid UTF-8 + +vouloir lire la nouvelle page man mais ce qui suit vous aidera assurément : diff --git a/crates/typos-cli/tests/cmd/encodings.toml b/crates/typos-cli/tests/cmd/encodings.toml index 5c514272..f093b99b 100644 --- a/crates/typos-cli/tests/cmd/encodings.toml +++ b/crates/typos-cli/tests/cmd/encodings.toml @@ -1,4 +1,10 @@ bin.name = "typos" +status.code = 2 stdout = """ +error: `ment` should be `meant` + ╭▸ ./invalid_utf8.txt:3:70 + │ +3 │ vouloir lire la nouvelle page man mais ce qui suit vous aidera assur�ment : + ╰╴ ━━━━ """ stderr = ""