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;