Skip to main content

vterm/term/
checkpoint.rs

1//! Checkpoints: a [`Term`]'s whole state as bytes, and back.
2//!
3//! A terminal that keeps its history on disk (foldwave) rebuilds a pane by
4//! restoring the nearest checkpoint and replaying the log after it, instead
5//! of replaying everything. A checkpoint therefore has to capture *all*
6//! state that affects how later bytes land: both screens, the cursor, the
7//! current style, the scroll region, tab stops, the saved cursor, the modes,
8//! and a synchronized update in progress.
9//!
10//! It deliberately does not capture the tokenizer. Checkpoints are only taken
11//! when the tokenizer is idle ([`Term::can_checkpoint`]): no escape sequence
12//! open and no UTF-8 character half-decoded. At such a point a fresh parser is
13//! indistinguishable from the running one, so there is nothing to save. On
14//! agent output about 96 % of byte offsets qualify (foldwave E4), so a caller
15//! simply takes the checkpoint at the next chunk boundary that does.
16//!
17//! Format (little-endian, versioned): magic `VTCK`, version, then the fields
18//! below. Screens are run-length encoded, since most cells of most screens are
19//! blank. [`Term::restore`] validates everything and never panics on bad
20//! input.
21
22use super::Term;
23use ansi::{Cell, CellWidth, Color, Cursor, Parser, Screen, Style, Utf8Decoder};
24use std::fmt;
25
26const MAGIC: &[u8; 4] = b"VTCK";
27const VERSION: u16 = 3;
28/// Refuse to allocate grids larger than this when restoring untrusted bytes.
29const MAX_CELLS: usize = 16 * 1024 * 1024;
30
31/// Why a checkpoint could not be restored.
32#[derive(Debug, Clone, PartialEq, Eq)]
33pub struct RestoreError(&'static str);
34
35impl fmt::Display for RestoreError {
36    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
37        write!(f, "invalid terminal checkpoint: {}", self.0)
38    }
39}
40
41impl std::error::Error for RestoreError {}
42
43impl Term {
44    /// True when a checkpoint can be taken now: the tokenizer is between
45    /// sequences and no UTF-8 character is half-decoded.
46    pub fn can_checkpoint(&self) -> bool {
47        self.parser.is_ground() && !self.decoder.is_pending()
48    }
49
50    /// The emulator's whole state as bytes, or `None` if a sequence or
51    /// character is in progress ([`can_checkpoint`](Term::can_checkpoint)).
52    /// Deterministic: equal states give equal bytes.
53    pub fn checkpoint(&self) -> Option<Vec<u8>> {
54        if !self.can_checkpoint() {
55            return None;
56        }
57        let mut w = Vec::with_capacity(4096);
58        w.extend_from_slice(MAGIC);
59        put_u16(&mut w, VERSION);
60        let flags = (self.in_alt as u8)
61            | (self.wrap_pending as u8) << 1
62            | (self.autowrap as u8) << 2
63            | (self.cursor_visible as u8) << 3
64            | (self.in_sync as u8) << 4
65            | (self.saved.is_some() as u8) << 5
66            | (self.sync_frame.is_some() as u8) << 6
67            | (self.newline_mode as u8) << 7;
68        w.push(flags);
69        // Version 2: a second flags byte.
70        w.push(self.bracketed_paste as u8);
71        put_style(&mut w, &self.style);
72        put_u32(&mut w, self.scroll_top as u32);
73        put_u32(&mut w, self.scroll_bottom as u32);
74        if let Some((cursor, style)) = &self.saved {
75            put_u32(&mut w, cursor.row as u32);
76            put_u32(&mut w, cursor.col as u32);
77            put_style(&mut w, style);
78        }
79        put_u32(&mut w, self.tabs.len() as u32);
80        for chunk in self.tabs.chunks(8) {
81            let mut byte = 0u8;
82            for (i, &t) in chunk.iter().enumerate() {
83                byte |= (t as u8) << i;
84            }
85            w.push(byte);
86        }
87        put_screen(&mut w, &self.primary);
88        put_screen(&mut w, &self.alt);
89        if let Some(frame) = &self.sync_frame {
90            put_screen(&mut w, frame);
91        }
92        Some(w)
93    }
94
95    /// Rebuild an emulator from [`checkpoint`](Term::checkpoint) bytes. Feeding
96    /// it the same bytes afterwards gives the same result as feeding the
97    /// original. Bad input is an error, never a panic.
98    pub fn restore(bytes: &[u8]) -> Result<Term, RestoreError> {
99        let mut r = Reader {
100            bytes,
101            at: 0,
102            version: 0,
103        };
104        if r.take(4)? != MAGIC {
105            return Err(RestoreError("not a checkpoint"));
106        }
107        let version = r.u16()?;
108        if !(1..=VERSION).contains(&version) {
109            return Err(RestoreError("unsupported version"));
110        }
111        r.version = version;
112        let flags = r.u8()?;
113        // Version 1 had one flags byte; its checkpoints restore with the
114        // later modes off, as they were when taken. Version 3 added wrap marks
115        // to each screen.
116        let flags2 = if version >= 2 { r.u8()? } else { 0 };
117        let bit = |n: u8| flags & (1 << n) != 0;
118        let style = r.style()?;
119        let scroll_top = r.u32()? as usize;
120        let scroll_bottom = r.u32()? as usize;
121        let saved = if bit(5) {
122            let row = r.u32()? as usize;
123            let col = r.u32()? as usize;
124            Some((Cursor::new(row, col), r.style()?))
125        } else {
126            None
127        };
128        let ntabs = r.u32()? as usize;
129        let tab_bytes = r.take(ntabs.div_ceil_compat(8))?;
130        let tabs: Vec<bool> = (0..ntabs)
131            .map(|i| tab_bytes[i / 8] & (1 << (i % 8)) != 0)
132            .collect();
133        let primary = r.screen()?;
134        let alt = r.screen()?;
135        let sync_frame = if bit(6) { Some(r.screen()?) } else { None };
136        if r.at != bytes.len() {
137            return Err(RestoreError("trailing bytes"));
138        }
139
140        let (rows, cols) = (primary.rows(), primary.cols());
141        let same = |s: &Screen| s.rows() == rows && s.cols() == cols;
142        if !same(&alt) || !sync_frame.as_ref().map_or(true, same) {
143            return Err(RestoreError("screens disagree on size"));
144        }
145        if tabs.len() != cols {
146            return Err(RestoreError("tab stops do not match the width"));
147        }
148        if scroll_top > scroll_bottom || scroll_bottom >= rows {
149            return Err(RestoreError("scroll region outside the screen"));
150        }
151        if saved.is_some_and(|(c, _)| c.row >= rows || c.col >= cols) {
152            return Err(RestoreError("saved cursor outside the screen"));
153        }
154        if bit(4) != sync_frame.is_some() {
155            return Err(RestoreError("synchronized update without its frame"));
156        }
157
158        let mut term = Term::new(rows, cols);
159        term.primary = primary;
160        term.alt = alt;
161        term.in_alt = bit(0);
162        term.wrap_pending = bit(1);
163        term.autowrap = bit(2);
164        term.cursor_visible = bit(3);
165        // Bit 7 was always 0 before LNM existed, so older checkpoints restore
166        // with the mode off, as they were taken.
167        term.newline_mode = bit(7);
168        term.bracketed_paste = flags2 & 1 != 0;
169        term.in_sync = bit(4);
170        term.style = style;
171        term.scroll_top = scroll_top;
172        term.scroll_bottom = scroll_bottom;
173        term.saved = saved;
174        term.tabs = tabs;
175        term.sync_frame = sync_frame;
176        term.parser = Parser::new();
177        term.decoder = Utf8Decoder::new();
178        term.dirty = true;
179        Ok(term)
180    }
181}
182
183/// `div_ceil` for `usize`, which is newer than the MSRV.
184trait DivCeil {
185    fn div_ceil_compat(self, d: usize) -> usize;
186}
187
188impl DivCeil for usize {
189    fn div_ceil_compat(self, d: usize) -> usize {
190        (self + d - 1) / d
191    }
192}
193
194fn put_u16(w: &mut Vec<u8>, v: u16) {
195    w.extend_from_slice(&v.to_le_bytes());
196}
197
198fn put_u32(w: &mut Vec<u8>, v: u32) {
199    w.extend_from_slice(&v.to_le_bytes());
200}
201
202fn put_color(w: &mut Vec<u8>, c: Color) {
203    match c {
204        Color::Default => w.extend_from_slice(&[0, 0, 0, 0]),
205        Color::Indexed(i) => w.extend_from_slice(&[1, i, 0, 0]),
206        Color::Rgb(r, g, b) => w.extend_from_slice(&[2, r, g, b]),
207    }
208}
209
210fn put_style(w: &mut Vec<u8>, s: &Style) {
211    put_color(w, s.fg);
212    put_color(w, s.bg);
213    w.push(
214        (s.bold as u8)
215            | (s.dim as u8) << 1
216            | (s.italic as u8) << 2
217            | (s.underline as u8) << 3
218            | (s.reverse as u8) << 4
219            | (s.strike as u8) << 5,
220    );
221}
222
223fn put_cell(w: &mut Vec<u8>, c: &Cell) {
224    put_u32(w, c.ch as u32);
225    w.push(match c.width {
226        CellWidth::Continuation => 0,
227        CellWidth::Single => 1,
228        CellWidth::Wide => 2,
229    });
230    put_style(w, &c.style);
231}
232
233/// Dimensions, cursor, then runs of identical cells in row-major order.
234fn put_screen(w: &mut Vec<u8>, s: &Screen) {
235    put_u32(w, s.rows() as u32);
236    put_u32(w, s.cols() as u32);
237    let cursor = s.cursor();
238    put_u32(w, cursor.row as u32);
239    put_u32(w, cursor.col as u32);
240    let mut run: Option<(Cell, u32)> = None;
241    for row in 0..s.rows() {
242        for col in 0..s.cols() {
243            let cell = s.cell(row, col);
244            match &mut run {
245                Some((c, n)) if *c == cell => *n += 1,
246                _ => {
247                    if let Some((c, n)) = run.take() {
248                        put_u32(w, n);
249                        put_cell(w, &c);
250                    }
251                    run = Some((cell, 1));
252                }
253            }
254        }
255    }
256    if let Some((c, n)) = run {
257        put_u32(w, n);
258        put_cell(w, &c);
259    }
260    // Version 3: which rows wrapped onto the next, a bit per row.
261    for chunk in (0..s.rows()).collect::<Vec<_>>().chunks(8) {
262        let mut byte = 0u8;
263        for (i, &r) in chunk.iter().enumerate() {
264            byte |= (s.row_wrapped(r) as u8) << i;
265        }
266        w.push(byte);
267    }
268}
269
270struct Reader<'a> {
271    bytes: &'a [u8],
272    at: usize,
273    /// The checkpoint's format version, once read.
274    version: u16,
275}
276
277impl<'a> Reader<'a> {
278    fn take(&mut self, n: usize) -> Result<&'a [u8], RestoreError> {
279        let end = self.at.checked_add(n).ok_or(RestoreError("truncated"))?;
280        let s = self
281            .bytes
282            .get(self.at..end)
283            .ok_or(RestoreError("truncated"))?;
284        self.at = end;
285        Ok(s)
286    }
287    fn u8(&mut self) -> Result<u8, RestoreError> {
288        Ok(self.take(1)?[0])
289    }
290    fn u16(&mut self) -> Result<u16, RestoreError> {
291        let b = self.take(2)?;
292        Ok(u16::from_le_bytes([b[0], b[1]]))
293    }
294    fn u32(&mut self) -> Result<u32, RestoreError> {
295        let b = self.take(4)?;
296        Ok(u32::from_le_bytes([b[0], b[1], b[2], b[3]]))
297    }
298    fn color(&mut self) -> Result<Color, RestoreError> {
299        let b = self.take(4)?;
300        match b[0] {
301            0 => Ok(Color::Default),
302            1 => Ok(Color::Indexed(b[1])),
303            2 => Ok(Color::Rgb(b[1], b[2], b[3])),
304            _ => Err(RestoreError("unknown colour")),
305        }
306    }
307    fn style(&mut self) -> Result<Style, RestoreError> {
308        let fg = self.color()?;
309        let bg = self.color()?;
310        let a = self.u8()?;
311        if a >> 6 != 0 {
312            return Err(RestoreError("unknown style attribute"));
313        }
314        Ok(Style {
315            fg,
316            bg,
317            bold: a & 1 != 0,
318            dim: a & 2 != 0,
319            italic: a & 4 != 0,
320            underline: a & 8 != 0,
321            reverse: a & 16 != 0,
322            strike: a & 32 != 0,
323        })
324    }
325    fn cell(&mut self) -> Result<Cell, RestoreError> {
326        let ch = char::from_u32(self.u32()?).ok_or(RestoreError("invalid character"))?;
327        let width = match self.u8()? {
328            0 => CellWidth::Continuation,
329            1 => CellWidth::Single,
330            2 => CellWidth::Wide,
331            _ => return Err(RestoreError("unknown cell width")),
332        };
333        Ok(Cell {
334            ch,
335            style: self.style()?,
336            width,
337        })
338    }
339    fn screen(&mut self) -> Result<Screen, RestoreError> {
340        let rows = self.u32()? as usize;
341        let cols = self.u32()? as usize;
342        let total = rows
343            .checked_mul(cols)
344            .filter(|&n| n > 0 && n <= MAX_CELLS)
345            .ok_or(RestoreError("screen size out of range"))?;
346        let (crow, ccol) = (self.u32()? as usize, self.u32()? as usize);
347        if crow >= rows || ccol >= cols {
348            return Err(RestoreError("cursor outside the screen"));
349        }
350        let mut screen = Screen::new(rows, cols);
351        let mut filled = 0usize;
352        while filled < total {
353            let n = self.u32()? as usize;
354            if n == 0 || n > total - filled {
355                return Err(RestoreError("bad cell run"));
356            }
357            let cell = self.cell()?;
358            for k in filled..filled + n {
359                screen.set(k / cols, k % cols, cell);
360            }
361            filled += n;
362        }
363        screen.set_cursor(Cursor::new(crow, ccol));
364        // Versions before 3 carried no wrap marks: every row unwrapped.
365        if self.version >= 3 {
366            let marks = self.take(rows.div_ceil_compat(8))?;
367            for r in 0..rows {
368                screen.set_row_wrapped(r, marks[r / 8] & (1 << (r % 8)) != 0);
369            }
370        }
371        Ok(screen)
372    }
373}