aboutsummaryrefslogtreecommitdiff
path: root/src/ghostty
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-06-04 00:56:07 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-06-04 00:56:07 +0300
commit3cbe4ec7936745543e16d1ceb37f6db6c38d65ac (patch)
tree0252b71e0b92653c4e81168a98b7a341a7717424 /src/ghostty
parente05bd5cb2a26e3690d73ca241e1e457d3eb664cb (diff)
fix: avoid ghostty utf8 grapheme buffer crash
refs #453
Diffstat (limited to 'src/ghostty')
-rw-r--r--src/ghostty/mod.rs90
1 files changed, 52 insertions, 38 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<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 {
+ 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()?;
};
- 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 {
+ if 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,
- };
+
+ // 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_UTF8,
- (&mut buffer as *mut ffi::GhosttyBuffer).cast(),
+ 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(())
}
@@ -2948,6 +2936,32 @@ mod tests {
}
#[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();
let mut render_state = RenderState::new().unwrap();