fix: avoid ghostty utf8 grapheme buffer crash

refs #453
This commit is contained in:
Ogulcan Celik 2026-06-04 00:56:07 +03:00
parent e05bd5cb2a
commit 3cbe4ec793
2 changed files with 65 additions and 51 deletions

View File

@ -2546,63 +2546,51 @@ impl<'a> RowCellIter<'a> {
}
pub fn grapheme_text(&self) -> Result<String, Error> {
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<u8>, text: &mut String) -> Result<(), Error> {
pub fn grapheme_text_into(
&self,
codepoints: &mut Vec<u32>,
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é é 🏳️‍🌈 🚀\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();

View File

@ -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<u8>,
grapheme_codepoints: &mut Vec<u32>,
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);