Skip to main content

scarab_protocol/
terminal_state.rs

1//! Safe abstraction layer for SharedState access
2//!
3//! This module provides a safe, validated interface for reading terminal state
4//! from shared memory, eliminating unsafe raw pointer dereferences.
5//!
6//! # Safety Architecture
7//!
8//! The `TerminalStateReader` trait abstracts away direct SharedState access with:
9//! - Bounds checking for all cell access
10//! - Validation of magic numbers and memory integrity
11//! - Lifetime tracking to prevent use-after-free
12//! - Type-safe cursor and sequence number access
13//!
14//! # Usage Example
15//!
16//! ```no_run
17//! use scarab_protocol::{SharedState, TerminalStateReader};
18//!
19//! fn render_terminal(state: &impl TerminalStateReader) {
20//!     // Safe access with automatic bounds checking
21//!     if let Some(cell) = state.cell(0, 0) {
22//!         println!("Top-left cell: {:?}", cell.char_codepoint);
23//!     }
24//!
25//!     let (cursor_x, cursor_y) = state.cursor_pos();
26//!     println!("Cursor at: ({}, {})", cursor_x, cursor_y);
27//! }
28//! ```
29
30use crate::Cell;
31
32/// Magic number for validating SharedState memory layout
33///
34/// This sentinel value helps detect:
35/// - Uninitialized memory
36/// - Memory corruption
37/// - Process crashes that left invalid state
38pub const SHARED_STATE_MAGIC: u64 = 0x5343_4152_4142_5348; // "SCARABSH" in hex
39
40/// Safe, validated interface for reading terminal state
41///
42/// This trait provides the abstraction layer over SharedState that:
43/// 1. Validates memory integrity before access
44/// 2. Performs bounds checking on all cell access
45/// 3. Provides ergonomic, type-safe getters
46/// 4. Enables testing with mock implementations
47pub trait TerminalStateReader {
48    /// Get cell at position, returns None if out of bounds
49    ///
50    /// # Arguments
51    /// * `row` - Zero-indexed row (0 to GRID_HEIGHT-1)
52    /// * `col` - Zero-indexed column (0 to GRID_WIDTH-1)
53    ///
54    /// # Returns
55    /// * `Some(&Cell)` if position is valid
56    /// * `None` if out of bounds
57    fn cell(&self, row: usize, col: usize) -> Option<&Cell>;
58
59    /// Get all cells as a slice
60    ///
61    /// Returns the full grid buffer. Prefer using `cell()` with bounds
62    /// checking when accessing individual cells.
63    fn cells(&self) -> &[Cell];
64
65    /// Get cursor position
66    ///
67    /// # Returns
68    /// Tuple of (x, y) cursor coordinates in grid space
69    fn cursor_pos(&self) -> (u16, u16);
70
71    /// Get current sequence number
72    ///
73    /// The sequence number increments with each state update.
74    /// Clients can poll this to detect changes.
75    ///
76    /// # Returns
77    /// Monotonically increasing sequence counter
78    fn sequence(&self) -> u64;
79
80    /// Check if state is valid
81    ///
82    /// Validates:
83    /// - Magic number matches expected value
84    /// - Memory appears properly initialized
85    /// - Cursor position is within bounds
86    ///
87    /// # Returns
88    /// `true` if state passes validation checks
89    fn is_valid(&self) -> bool;
90
91    /// Get grid dimensions
92    ///
93    /// # Returns
94    /// Tuple of (width, height) in cells
95    fn dimensions(&self) -> (usize, usize);
96
97    /// Check if dirty flag is set
98    ///
99    /// The dirty flag indicates pending updates that haven't been rendered.
100    fn is_dirty(&self) -> bool;
101
102    /// Check if error mode is active
103    ///
104    /// Error mode indicates the daemon encountered a fatal error (PTY/SHM unavailable)
105    /// and has written an error message to the grid. Clients should display this
106    /// error and exit gracefully.
107    ///
108    /// # Returns
109    /// `true` if daemon is in error mode
110    fn is_error_mode(&self) -> bool;
111
112    /// Get linear cell index from row/col coordinates
113    ///
114    /// # Arguments
115    /// * `row` - Zero-indexed row
116    /// * `col` - Zero-indexed column
117    ///
118    /// # Returns
119    /// * `Some(index)` if coordinates are valid
120    /// * `None` if out of bounds
121    fn cell_index(&self, row: usize, col: usize) -> Option<usize> {
122        let (width, height) = self.dimensions();
123        if row >= height || col >= width {
124            None
125        } else {
126            Some(row * width + col)
127        }
128    }
129
130    /// Iterate over all cells with their coordinates
131    ///
132    /// Yields tuples of (row, col, &Cell) for convenient iteration.
133    fn iter_cells(&self) -> CellIterator<'_, Self>
134    where
135        Self: Sized,
136    {
137        CellIterator {
138            reader: self,
139            index: 0,
140        }
141    }
142}
143
144/// Iterator over cells with coordinates
145pub struct CellIterator<'a, R: TerminalStateReader> {
146    reader: &'a R,
147    index: usize,
148}
149
150impl<'a, R: TerminalStateReader> Iterator for CellIterator<'a, R> {
151    type Item = (usize, usize, &'a Cell);
152
153    fn next(&mut self) -> Option<Self::Item> {
154        let (width, _height) = self.reader.dimensions();
155        let cells = self.reader.cells();
156
157        if self.index >= cells.len() {
158            return None;
159        }
160
161        let row = self.index / width;
162        let col = self.index % width;
163        let cell = &cells[self.index];
164        self.index += 1;
165
166        Some((row, col, cell))
167    }
168}
169
170/// Implementation note for SharedState
171///
172/// Due to `#[no_std]` constraint on scarab-protocol, we cannot directly
173/// implement TerminalStateReader on SharedState here. The implementation
174/// is provided in scarab-client as `SafeSharedState<'_>`.
175///
176/// This allows scarab-protocol to remain dependency-free while providing
177/// the trait definition for both client and daemon.
178
179#[cfg(test)]
180mod tests {
181    use super::*;
182
183    // Mock implementation for testing
184    struct MockState {
185        cells: alloc::vec::Vec<Cell>,
186        width: usize,
187        height: usize,
188        cursor: (u16, u16),
189        sequence: u64,
190        error_mode: bool,
191    }
192
193    impl TerminalStateReader for MockState {
194        fn cell(&self, row: usize, col: usize) -> Option<&Cell> {
195            self.cell_index(row, col)
196                .and_then(|idx| self.cells.get(idx))
197        }
198
199        fn cells(&self) -> &[Cell] {
200            &self.cells
201        }
202
203        fn cursor_pos(&self) -> (u16, u16) {
204            self.cursor
205        }
206
207        fn sequence(&self) -> u64 {
208            self.sequence
209        }
210
211        fn is_valid(&self) -> bool {
212            self.cells.len() == self.width * self.height
213                && (self.cursor.0 as usize) < self.width
214                && (self.cursor.1 as usize) < self.height
215        }
216
217        fn dimensions(&self) -> (usize, usize) {
218            (self.width, self.height)
219        }
220
221        fn is_dirty(&self) -> bool {
222            false
223        }
224
225        fn is_error_mode(&self) -> bool {
226            self.error_mode
227        }
228    }
229
230    #[test]
231    fn test_bounds_checking() {
232        let mock = MockState {
233            cells: alloc::vec![Cell::default(); 100],
234            width: 10,
235            height: 10,
236            cursor: (5, 5),
237            sequence: 42,
238            error_mode: false,
239        };
240
241        // Valid access
242        assert!(mock.cell(0, 0).is_some());
243        assert!(mock.cell(9, 9).is_some());
244
245        // Out of bounds
246        assert!(mock.cell(10, 0).is_none());
247        assert!(mock.cell(0, 10).is_none());
248        assert!(mock.cell(100, 100).is_none());
249    }
250
251    #[test]
252    fn test_validation() {
253        let valid = MockState {
254            cells: alloc::vec![Cell::default(); 100],
255            width: 10,
256            height: 10,
257            cursor: (5, 5),
258            sequence: 42,
259            error_mode: false,
260        };
261        assert!(valid.is_valid());
262
263        let invalid_cursor = MockState {
264            cells: alloc::vec![Cell::default(); 100],
265            width: 10,
266            height: 10,
267            cursor: (20, 5), // Out of bounds
268            sequence: 42,
269            error_mode: false,
270        };
271        assert!(!invalid_cursor.is_valid());
272
273        let invalid_size = MockState {
274            cells: alloc::vec![Cell::default(); 50], // Wrong size
275            width: 10,
276            height: 10,
277            cursor: (5, 5),
278            sequence: 42,
279            error_mode: false,
280        };
281        assert!(!invalid_size.is_valid());
282    }
283
284    #[test]
285    fn test_iterator() {
286        let mut cells = alloc::vec![Cell::default(); 6];
287        for i in 0..6 {
288            cells[i].char_codepoint = (b'A' + i as u8) as u32;
289        }
290
291        let mock = MockState {
292            cells,
293            width: 3,
294            height: 2,
295            cursor: (0, 0),
296            sequence: 1,
297            error_mode: false,
298        };
299
300        let collected: alloc::vec::Vec<_> = mock.iter_cells().collect();
301        assert_eq!(collected.len(), 6);
302
303        // Check first cell (0, 0)
304        assert_eq!(collected[0].0, 0); // row
305        assert_eq!(collected[0].1, 0); // col
306        assert_eq!(collected[0].2.char_codepoint, b'A' as u32);
307
308        // Check last cell (1, 2)
309        assert_eq!(collected[5].0, 1); // row
310        assert_eq!(collected[5].1, 2); // col
311        assert_eq!(collected[5].2.char_codepoint, b'F' as u32);
312    }
313}
314
315// Need alloc for tests with Vec
316#[cfg(test)]
317extern crate alloc;