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