Skip to content

Conversation

@cppcoffee
Copy link
Contributor

Closes #40962

Release Notes:

  • Fixed avoid byte slicing crash in short status labels

@cla-bot cla-bot bot added the cla-signed The user has signed the Contributor License Agreement label Oct 23, 2025
@cppcoffee cppcoffee changed the title go_to_line: avoid byte slicing crash in short status labels go_to_line: Avoid byte slicing crash in short status labels Oct 23, 2025
parinya-ao

This comment was marked as abuse.

Copy link
Contributor

@SomeoneToIgnore SomeoneToIgnore left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any chance we can have a test for this?

@SomeoneToIgnore SomeoneToIgnore self-assigned this Oct 23, 2025
@cppcoffee
Copy link
Contributor Author

Hi @SomeoneToIgnore , test_first_char_extraction_utf8_safety has already tested the fixed core logic. However, the current test cases do not call the interface. Should this test be retained?

Copy link
Contributor

@SomeoneToIgnore SomeoneToIgnore left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

test_first_char_extraction_utf8_safety has already tested the fixed core logic

Whatever it tested, it was not panicking, right?
My point is to recreate a panic that we fix in a separate test: otherwise it might come back later, because nothing ensures it's not broken.

Thank you for adding the new test, but this test does not fail on main for me — which means that we're not recreating a panic that we fix yet.

@cppcoffee cppcoffee closed this Oct 23, 2025
@cppcoffee cppcoffee reopened this Oct 23, 2025
@cppcoffee
Copy link
Contributor Author

In cursor_position.rs, the short-format branch performs &name[..1] on the label, which only takes the first byte. When the label is a multi-byte character (like "行"), this slice lands in the middle of a UTF-8 sequence, triggering core::str::slice_error_fail and causing a panic.

Introduce the short_label helper function to safely truncate short labels at character boundaries, completely eliminating UTF-8 slice panics in write_position.

Signed-off-by: Xiaobo Liu <[email protected]>
@SomeoneToIgnore
Copy link
Contributor

SomeoneToIgnore commented Oct 23, 2025

Those are great pieces of context — thank you for explaining the issue, this is usually very hepful to have in the PR initially.
I am insisting on having the test here because it seems to be relatively simple to reproduce, right?
All it needs is a specific text in the buffer + certain input into the go to line modal.

Yet, I have to repeat myself:

but this test does not fail on main for me — which means that we're not recreating a panic that we fix yet.

I've taken this PR's diff and restored all the logic that got fixed in this PR:

image

with this diff

diff --git a/crates/go_to_line/src/cursor_position.rs b/crates/go_to_line/src/cursor_position.rs
index 5c10537e28..fc730c7881 100644
--- a/crates/go_to_line/src/cursor_position.rs
+++ b/crates/go_to_line/src/cursor_position.rs
@@ -162,6 +162,18 @@ impl CursorPosition {
             // Do not write out anything if we have just one empty selection.
             return;
         }
+        self.write_position_with_labels(
+            text,
+            cx,
+            LabelTexts {
+                line: "line",
+                selection: "selection",
+                character: "character",
+            },
+        );
+    }
+
+    fn write_position_with_labels(&self, text: &mut String, cx: &App, labels: LabelTexts<'_>) {
         let SelectionStats {
             lines,
             characters,
@@ -199,12 +211,25 @@ impl CursorPosition {
         &self.selected_count
     }
 
+    fn short_label(name: &str) -> &str {
+        match name.char_indices().nth(1) {
+            Some((index, _)) => &name[..index],
+            None => name,
+        }
+    }
+
     #[cfg(test)]
     pub(crate) fn position(&self) -> Option<UserCaretPosition> {
         self.position
     }
 }
 
+struct LabelTexts<'a> {
+    line: &'a str,
+    selection: &'a str,
+    character: &'a str,
+}
+
 impl Render for CursorPosition {
     fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
         if !StatusBarSettings::get_global(cx).cursor_position_button {
@@ -308,3 +333,66 @@ impl Settings for LineIndicatorFormat {
         content.line_indicator_format.unwrap().into()
     }
 }
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use gpui::{AppContext, TestAppContext};
+    use project::{FakeFs, Project};
+    use serde_json::json;
+    use std::sync::Arc;
+    use util::path;
+    use workspace::{AppState, Workspace};
+    fn init_test(cx: &mut TestAppContext) -> Arc<AppState> {
+        cx.update(|cx| {
+            let state = AppState::test(cx);
+            language::init(cx);
+            crate::init(cx);
+            editor::init(cx);
+            workspace::init_settings(cx);
+            Project::init_settings(cx);
+            state
+        })
+    }
+
+    #[gpui::test]
+    async fn write_position_handles_multibyte_short_labels(cx: &mut TestAppContext) {
+        init_test(cx);
+        let fs = FakeFs::new(cx.executor());
+        fs.insert_tree(path!("/dir"), json!({ "file.txt": "" }))
+            .await;
+        let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
+        let (workspace, window_cx) =
+            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
+        let cursor_position = workspace.update(&mut *window_cx, |workspace, cx| {
+            cx.new(|_| CursorPosition::new(workspace))
+        });
+        cx.update(|cx| {
+            LineIndicatorFormat::override_global(LineIndicatorFormat::Short, cx);
+        });
+        cursor_position.update(cx, |cursor_position, cx| {
+            cursor_position.selected_count = SelectionStats {
+                lines: 2,
+                characters: 3,
+                selections: 2,
+            };
+            let mut text = "1:1".to_string();
+            cursor_position.write_position_with_labels(
+                &mut text,
+                cx,
+                LabelTexts {
+                    line: "行",
+                    selection: "选区",
+                    character: "字符",
+                },
+            );
+            let expected = format!(
+                "1:1 (2 {}, 2 {}, 3 {})",
+                CursorPosition::short_label("选区"),
+                CursorPosition::short_label("行"),
+                CursorPosition::short_label("字符"),
+            );
+            assert_eq!(text, expected);
+        });
+    }
+}

I'm getting an assertion failure, not a panic that's mentioned in the issue:

thread 'cursor_position::tests::write_position_handles_multibyte_short_labels' panicked at crates/go_to_line/src/cursor_position.rs:395:13:
assertion `left == right` failed
  left: "1:1 (2 s, 2 l, 3 c)"
 right: "1:1 (2 选, 2 行, 3 字)"
stack backtrace:

Am I doing something wrong, or the test does not really reproduce the panic?

@mikayla-maki
Copy link
Member

mikayla-maki commented Oct 24, 2025

Sorry, this doesn't actually seem to be a bug, nor is it related to the crash reported.

Thanks for your submission!

@cppcoffee
Copy link
Contributor Author

cppcoffee commented Oct 25, 2025

This is because when you "copied the old logic," you didn't actually restore that single-byte truncation of write_position back to the original &name[..1]. In the new implementation, write_position has been split into write_position_with_labels which calls CursorPosition::short_label, so even if you add back the label struct and tests, the code still safely truncates at character boundaries—the test can only reach the assertion, see the English abbreviation, and fail, without triggering a UTF-8 slice panic.

To reproduce the crash, change this line:

let name = if is_short_format {
Self::short_label(name)
} else {
name
};

back to the old logic:

let name = if is_short_format { &name[..1] } else { name };

Then run the unit tests; this will now panic directly ("byte index 1 is not a char boundary"), just like the crash report, proving that the test does indeed cover the path that causes the crash. Then, restore the fix logic, and the test will pass again.

截屏2025-10-25 17 01 54 image

@SomeoneToIgnore
Copy link
Contributor

SomeoneToIgnore commented Oct 25, 2025

This is because when you "copied the old logic," you didn't actually restore that single-byte truncation of write_position back to the original &name[..1]

Yes I did, did not you see that in the diff provided here:

#40963 (comment)

?

let name = if is_short_format { &name[..1] } else { name };

is unchanged there.

Can you provide me a diff that I can git apply on main and reproduce the panic?

@cppcoffee
Copy link
Contributor Author

You're right, I've analyzed it and it was indeed my mistake. I'm sorry for the trouble.

@cppcoffee cppcoffee deleted the goto-line branch October 25, 2025 12:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

cla-signed The user has signed the Contributor License Agreement community champion

Projects

None yet

Development

Successfully merging this pull request may close these issues.

MacOS: zed crash

4 participants