From 3cbe4ec7936745543e16d1ceb37f6db6c38d65ac Mon Sep 17 00:00:00 2001 From: Ogulcan Celik Date: Thu, 4 Jun 2026 00:56:07 +0300 Subject: [PATCH] fix: avoid ghostty utf8 grapheme buffer crash refs #453 --- src/ghostty/mod.rs | 104 ++++++++++++++++++++++++------------------- src/pane/terminal.rs | 12 ++--- 2 files changed, 65 insertions(+), 51 deletions(-) diff --git a/src/ghostty/mod.rs b/src/ghostty/mod.rs index 20161d80..a64fece1 100644 --- a/src/ghostty/mod.rs +++ b/src/ghostty/mod.rs @@ -2546,63 +2546,51 @@ impl<'a> RowCellIter<'a> { } pub fn grapheme_text(&self) -> Result { - let mut bytes = Vec::new(); + let mut codepoints = Vec::new(); let mut text = String::new(); - self.grapheme_text_into(&mut bytes, &mut text)?; + self.grapheme_text_into(&mut codepoints, &mut text)?; Ok(text) } - pub fn grapheme_text_into(&self, bytes: &mut Vec, text: &mut String) -> Result<(), Error> { + pub fn grapheme_text_into( + &self, + codepoints: &mut Vec, + text: &mut String, + ) -> Result<(), Error> { text.clear(); - bytes.clear(); + codepoints.clear(); - let mut buffer = ffi::GhosttyBuffer { - ptr: ptr::null_mut(), - cap: 0, - len: 0, - }; - let result = unsafe { - ffi::ghostty_render_state_row_cells_get( - self.cells.raw, - ffi::GhosttyRenderStateRowCellsData_GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_GRAPHEMES_UTF8, - (&mut buffer as *mut ffi::GhosttyBuffer).cast(), - ) - }; - match result { - ffi::GhosttyResult_GHOSTTY_SUCCESS if buffer.len == 0 => { - return self.raw_cell_text_into(text); - } - ffi::GhosttyResult_GHOSTTY_SUCCESS => { - return Err(Error(ffi::GhosttyResult_GHOSTTY_INVALID_VALUE)); - } - ffi::GhosttyResult_GHOSTTY_OUT_OF_SPACE => {} - other => return Err(Error(other)), - } - - if buffer.len == 0 { - return self.raw_cell_text_into(text); - } - bytes.resize(buffer.len, 0); - let mut buffer = ffi::GhosttyBuffer { - ptr: bytes.as_mut_ptr(), - cap: bytes.len(), - len: 0, - }; + let mut len = 0u32; unsafe { ffi::ghostty_render_state_row_cells_get( self.cells.raw, - ffi::GhosttyRenderStateRowCellsData_GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_GRAPHEMES_UTF8, - (&mut buffer as *mut ffi::GhosttyBuffer).cast(), + ffi::GhosttyRenderStateRowCellsData_GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_GRAPHEMES_LEN, + (&mut len as *mut u32).cast(), + ) + .into_result()?; + }; + + if len == 0 { + return self.raw_cell_text_into(text); + } + + // Avoid GRAPHEMES_UTF8 here. In libghostty-vt 0.6.7's vendor, its + // required-byte query can underreport multi-codepoint graphemes. + codepoints.resize(len as usize, 0); + unsafe { + ffi::ghostty_render_state_row_cells_get( + self.cells.raw, + ffi::GhosttyRenderStateRowCellsData_GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_GRAPHEMES_BUF, + codepoints.as_mut_ptr().cast(), ) .into_result()?; } - if buffer.len > bytes.len() { - return Err(Error(ffi::GhosttyResult_GHOSTTY_OUT_OF_SPACE)); - } - bytes.truncate(buffer.len); - match std::str::from_utf8(bytes) { - Ok(value) => text.push_str(value), - Err(_) => text.push_str(&String::from_utf8_lossy(bytes)), + + for codepoint in codepoints.iter().copied() { + match char::from_u32(codepoint) { + Some(ch) => text.push(ch), + None => text.push(char::REPLACEMENT_CHARACTER), + } } Ok(()) } @@ -2947,6 +2935,32 @@ mod tests { assert_eq!(render_state.dirty().unwrap(), Dirty::Clean); } + #[test] + fn render_cells_handle_issue_453_unicode_payload() { + let mut terminal = Terminal::new(80, 3, 100).unwrap(); + terminal.write("README ๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ฆ ๐Ÿง‘โ€๐Ÿ’ป โœ… โšก ๆผขๅญ— cafรฉ eฬ ๐Ÿณ๏ธโ€๐ŸŒˆ ๐Ÿš€\r\n".as_bytes()); + + let mut render_state = RenderState::new().unwrap(); + render_state.update(&terminal).unwrap(); + + let mut row_iterator = RowIterator::new().unwrap(); + let mut rows = render_state + .populate_row_iterator(&mut row_iterator) + .unwrap(); + let mut row_cells = RowCells::new().unwrap(); + let mut codepoints = Vec::new(); + let mut text = String::new(); + + while rows.next() { + let mut cells = rows.populate_cells(&mut row_cells).unwrap(); + while cells.next() { + cells + .grapheme_text_into(&mut codepoints, &mut text) + .unwrap(); + } + } + } + #[test] fn render_state_row_dirty_can_be_cleared_independently() { let mut terminal = Terminal::new(8, 3, 100).unwrap(); diff --git a/src/pane/terminal.rs b/src/pane/terminal.rs index 0a67f11a..88924d36 100644 --- a/src/pane/terminal.rs +++ b/src/pane/terminal.rs @@ -1058,7 +1058,7 @@ impl GhosttyPaneTerminal { Ok(rows) => rows, Err(_) => return, }; - let mut grapheme_bytes = Vec::new(); + let mut grapheme_codepoints = Vec::new(); let mut symbol_scratch = String::new(); let mut y = 0u16; while y < area.height && rows.next() { @@ -1081,7 +1081,7 @@ impl GhosttyPaneTerminal { &cells, basic.wide, hide_kitty_placeholders, - &mut grapheme_bytes, + &mut grapheme_codepoints, &mut symbol_scratch, ) { Ok(symbol) => symbol, @@ -1226,7 +1226,7 @@ fn ghostty_collect_dirty_patch( let Ok(mut rows) = render_state.populate_row_iterator(&mut row_iterator) else { fallback!("populate_rows_error"); }; - let mut grapheme_bytes = Vec::new(); + let mut grapheme_codepoints = Vec::new(); let mut symbol_scratch = String::new(); let mut patch_rows = Vec::new(); let mut y = 0u16; @@ -1264,7 +1264,7 @@ fn ghostty_collect_dirty_patch( &cells, basic.wide, hide_kitty_placeholders, - &mut grapheme_bytes, + &mut grapheme_codepoints, &mut symbol_scratch, ) { Ok(symbol) => symbol.to_owned(), @@ -1539,7 +1539,7 @@ fn ghostty_buffer_symbol_into<'a>( cells: &crate::ghostty::RowCellIter<'_>, wide: crate::ghostty::CellWide, hide_kitty_placeholders: bool, - grapheme_bytes: &mut Vec, + grapheme_codepoints: &mut Vec, symbol_scratch: &'a mut String, ) -> Result<&'a str, crate::ghostty::Error> { symbol_scratch.clear(); @@ -1547,7 +1547,7 @@ fn ghostty_buffer_symbol_into<'a>( crate::ghostty::CellWide::SpacerTail => {} crate::ghostty::CellWide::SpacerHead => symbol_scratch.push(' '), crate::ghostty::CellWide::Narrow | crate::ghostty::CellWide::Wide => { - cells.grapheme_text_into(grapheme_bytes, symbol_scratch)?; + cells.grapheme_text_into(grapheme_codepoints, symbol_scratch)?; let hidden_kitty_placeholder = hide_kitty_placeholders && symbol_scratch.chars().next().map(u32::from) == Some(crate::ghostty::KITTY_UNICODE_PLACEHOLDER);