Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 55 additions & 3 deletions src/row.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,11 @@ impl Row {
let wide = self.cells[usize::from(i)].is_wide();
self.clear_wide(i);
self.cells[usize::from(i)].clear(attrs);
if i == self.cols() - if wide { 2 } else { 1 } {
// `wide` is captured pre-erase on purpose: a 2-wide char occupies the
// logical last column at cols() - 2. saturating_sub guards the
// degenerate case of an orphaned wide cell in a one-column row, where
// cols() - 2 would underflow.
if i == self.cols().saturating_sub(if wide { 2 } else { 1 }) {
self.wrapped = false;
}
}
Expand All @@ -71,8 +75,19 @@ impl Row {
}

pub fn resize(&mut self, len: u16, cell: crate::Cell) {
let old_len = self.cells.len();
self.cells.resize(usize::from(len), cell);
self.wrapped = false;
// When shrinking, a wide char at the new last position may
// have lost its continuation cell — clear it to avoid
// dangling state. Matches what Row::truncate already does.
let new_len = usize::from(len);
if new_len > 0 && new_len < old_len {
let last_cell = &mut self.cells[new_len - 1];
if last_cell.is_wide() {
last_cell.clear(*last_cell.attrs());
}
}
}

pub fn wrap(&mut self, wrap: bool) {
Expand All @@ -84,9 +99,20 @@ impl Row {
}

pub fn clear_wide(&mut self, col: u16) {
let cell = &self.cells[usize::from(col)];
let col_idx = usize::from(col);
let cell = &self.cells[col_idx];
let other = if cell.is_wide() {
&mut self.cells[usize::from(col + 1)]
let next = col_idx + 1;
if next >= self.cells.len() {
// Wide char at the last column with no continuation
// (e.g. a shrinking resize that split the pair).
// Clear the cell in place instead of indexing past
// the end.
let attrs = *self.cells[col_idx].attrs();
self.cells[col_idx].clear(attrs);
return;
Comment thread
Junyi-99 marked this conversation as resolved.
}
&mut self.cells[next]
} else if cell.is_wide_continuation() {
&mut self.cells[usize::from(col - 1)]
} else {
Expand Down Expand Up @@ -472,3 +498,29 @@ impl Row {
(prev_pos, prev_attrs)
}
}

#[cfg(test)]
mod tests {
use super::*;

// An orphaned wide cell (wide flag set, no continuation) in a
// one-column row must not make Row::erase underflow when it derives the
// logical last column as cols() - 2. Covers the Copilot review note on
// the clear_wide OOB fix: clear_wide itself was hardened, but its caller
// erase computed cols() - 2 == 1 - 2 from the captured `wide` value.
#[test]
fn erase_wide_cell_in_one_column_row_does_not_underflow() {
let attrs = crate::attrs::Attrs::default();
let mut row = Row::new(1);
// A 2-wide char in the only column: a wide cell with no room for a
// continuation cell.
row.get_mut(0).unwrap().set('中', attrs);
assert!(row.get(0).unwrap().is_wide());

row.erase(0, attrs);

let cell = row.get(0).unwrap();
assert!(!cell.is_wide(), "erased cell must no longer be wide");
assert!(!cell.has_contents(), "erased cell must be empty");
}
}
13 changes: 13 additions & 0 deletions tests/basic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -122,3 +122,16 @@ fn cell_attrs() {

assert!(parser.screen().cell(0, 4).unwrap().italic());
}

// Regression test for the panic where shrinking a screen truncates the
// continuation cell of a wide character, then erasing the line tries
// to clear past the end of the row. The repro is from
// https://github.com/doy/vt100-rust/issues/28.
#[test]
fn resize_truncating_wide_char_then_erase_line_does_not_panic() {
let mut parser = vt100::Parser::new(2, 4, 0);
parser.process("你".as_bytes());
parser.screen_mut().set_size(2, 1);
parser.process(b"\x1b[K");
assert_eq!(parser.screen().size(), (2, 1));
}