Skip to main content

bones_core/cache/
reader.rs

1//! Binary cache file reader.
2//!
3//! [`CacheReader`] opens and validates a binary cache file, then provides
4//! methods for decoding events — either all at once or a range — without
5//! needing to know the encoding details.
6
7use std::fs;
8use std::path::Path;
9
10use crate::cache::{CacheError, CacheHeader, decode_events};
11use crate::event::Event;
12
13// ---------------------------------------------------------------------------
14// CacheReader
15// ---------------------------------------------------------------------------
16
17/// Reads events from a binary columnar cache file.
18///
19/// The reader validates magic bytes, version, and CRC on [`open`](Self::open),
20/// then decodes columns using the [`ColumnCodec`](super::ColumnCodec) traits
21/// to reconstruct [`Event`] structs.
22///
23/// # Example
24///
25/// ```rust,no_run
26/// use bones_core::cache::reader::CacheReader;
27///
28/// let reader = CacheReader::open("path/to/events.bin").unwrap();
29/// println!("cache contains {} events", reader.event_count());
30/// let events = reader.read_all().unwrap();
31/// ```
32#[derive(Debug, Clone)]
33pub struct CacheReader {
34    /// Decoded header metadata.
35    header: CacheHeader,
36    /// Raw file bytes (kept for range decoding).
37    data: Vec<u8>,
38}
39
40impl CacheReader {
41    /// Open and validate a cache file.
42    ///
43    /// Reads the file into memory, validates the magic bytes, format version,
44    /// and CRC-64 checksum. Returns an error if any validation step fails.
45    ///
46    /// # Errors
47    ///
48    /// Returns [`CacheError`] if the file cannot be read, or if magic,
49    /// version, or CRC validation fails.
50    pub fn open(path: impl AsRef<Path>) -> Result<Self, CacheReaderError> {
51        let path = path.as_ref();
52        let data = fs::read(path).map_err(|e| CacheReaderError::Io {
53            path: path.display().to_string(),
54            source: e,
55        })?;
56
57        // Validate by decoding the header (checks magic, version, CRC)
58        let (header, _cols) = CacheHeader::decode(&data).map_err(CacheReaderError::Cache)?;
59
60        Ok(Self { header, data })
61    }
62
63    /// Create a reader from raw bytes (useful for testing).
64    ///
65    /// # Errors
66    ///
67    /// Returns [`CacheError`] if validation fails.
68    pub fn from_bytes(data: Vec<u8>) -> Result<Self, CacheReaderError> {
69        let (header, _cols) = CacheHeader::decode(&data).map_err(CacheReaderError::Cache)?;
70        Ok(Self { header, data })
71    }
72
73    /// Return the number of events (rows) in the cache file without decoding.
74    #[must_use]
75    pub fn event_count(&self) -> usize {
76        usize::try_from(self.header.row_count).unwrap_or(usize::MAX)
77    }
78
79    /// Return a reference to the cache header.
80    #[must_use]
81    pub const fn header(&self) -> &CacheHeader {
82        &self.header
83    }
84
85    /// Decode all events from the cache.
86    ///
87    /// **Note**: The `event_hash` field will be empty on reconstructed events.
88    /// Callers needing hashes must recompute them.
89    ///
90    /// # Errors
91    ///
92    /// Returns [`CacheReaderError`] if column decoding fails.
93    pub fn read_all(&self) -> Result<Vec<Event>, CacheReaderError> {
94        let (_header, events) = decode_events(&self.data).map_err(CacheReaderError::Cache)?;
95        Ok(events)
96    }
97
98    /// Decode a range of events from the cache.
99    ///
100    /// Returns events `[start .. start + count]`, clamped to the actual row
101    /// count. If `start >= event_count()`, returns an empty Vec.
102    ///
103    /// **Implementation note**: The current columnar format requires decoding
104    /// all rows and then slicing. A future optimisation could add per-column
105    /// offset tables for truly random access.
106    ///
107    /// # Errors
108    ///
109    /// Returns [`CacheReaderError`] if column decoding fails.
110    pub fn read_range(&self, start: usize, count: usize) -> Result<Vec<Event>, CacheReaderError> {
111        if start >= self.event_count() {
112            return Ok(Vec::new());
113        }
114
115        let all = self.read_all()?;
116        let end = (start + count).min(all.len());
117        Ok(all[start..end].to_vec())
118    }
119
120    /// Return the creation timestamp of the cache file (µs since epoch).
121    #[must_use]
122    pub const fn created_at_us(&self) -> u64 {
123        self.header.created_at_us
124    }
125
126    /// Return the stored CRC-64 checksum of the column data.
127    #[must_use]
128    pub const fn data_crc64(&self) -> u64 {
129        self.header.data_crc64
130    }
131
132    /// Return the total size of the raw cache data in bytes.
133    #[must_use]
134    pub const fn file_size(&self) -> usize {
135        self.data.len()
136    }
137}
138
139// ---------------------------------------------------------------------------
140// Error type
141// ---------------------------------------------------------------------------
142
143/// Errors returned by [`CacheReader`].
144#[derive(Debug, thiserror::Error)]
145pub enum CacheReaderError {
146    /// File I/O error.
147    #[error("failed to read cache file {path}: {source}")]
148    Io {
149        path: String,
150        #[source]
151        source: std::io::Error,
152    },
153
154    /// Cache format/validation error.
155    #[error("cache validation error: {0}")]
156    Cache(#[from] CacheError),
157}
158
159// ---------------------------------------------------------------------------
160// Tests
161// ---------------------------------------------------------------------------
162
163#[cfg(test)]
164mod tests {
165    use super::*;
166    use crate::cache::encode_events;
167    use crate::event::data::{CreateData, MoveData};
168    use crate::event::{Event, EventData, EventType};
169    use crate::model::item::{Kind, State, Urgency};
170    use crate::model::item_id::ItemId;
171    use std::collections::BTreeMap;
172    use tempfile::TempDir;
173
174    fn make_event(ts: i64, agent: &str, et: EventType, item: &str) -> Event {
175        let data = match et {
176            EventType::Create => EventData::Create(CreateData {
177                title: format!("Item {item}"),
178                kind: Kind::Task,
179                size: None,
180                urgency: Urgency::Default,
181                labels: vec![],
182                parent: None,
183                causation: None,
184                description: None,
185                extra: BTreeMap::new(),
186            }),
187            _ => EventData::Move(MoveData {
188                state: State::Doing,
189                reason: None,
190                extra: BTreeMap::new(),
191            }),
192        };
193        Event {
194            wall_ts_us: ts,
195            agent: agent.to_string(),
196            itc: "itc:AQ".to_string(),
197            parents: vec![],
198            event_type: et,
199            item_id: ItemId::new_unchecked(item),
200            data,
201            event_hash: format!("blake3:{ts:016x}"),
202        }
203    }
204
205    fn write_cache_file(path: &Path, events: &[Event]) {
206        let bytes = encode_events(events, 12345).unwrap();
207        std::fs::write(path, bytes).unwrap();
208    }
209
210    // === open ==============================================================
211
212    #[test]
213    fn open_valid_cache_file() {
214        let tmp = TempDir::new().unwrap();
215        let cache_path = tmp.path().join("events.bin");
216        let events = vec![
217            make_event(1000, "alice", EventType::Create, "bn-001"),
218            make_event(2000, "bob", EventType::Move, "bn-001"),
219        ];
220        write_cache_file(&cache_path, &events);
221
222        let reader = CacheReader::open(&cache_path).unwrap();
223        assert_eq!(reader.event_count(), 2);
224        assert_eq!(reader.created_at_us(), 12345);
225    }
226
227    #[test]
228    fn open_nonexistent_file_returns_io_error() {
229        let err = CacheReader::open("/tmp/nonexistent-bones-cache.bin").unwrap_err();
230        assert!(matches!(err, CacheReaderError::Io { .. }));
231    }
232
233    #[test]
234    fn open_corrupt_file_returns_cache_error() {
235        let tmp = TempDir::new().unwrap();
236        let cache_path = tmp.path().join("corrupt.bin");
237        std::fs::write(&cache_path, b"NOT A CACHE FILE").unwrap();
238
239        let err = CacheReader::open(&cache_path).unwrap_err();
240        assert!(matches!(err, CacheReaderError::Cache(_)));
241    }
242
243    // === from_bytes ========================================================
244
245    #[test]
246    fn from_bytes_valid() {
247        let events = vec![make_event(1000, "alice", EventType::Create, "bn-001")];
248        let bytes = encode_events(&events, 42).unwrap();
249        let reader = CacheReader::from_bytes(bytes).unwrap();
250        assert_eq!(reader.event_count(), 1);
251    }
252
253    // === read_all ==========================================================
254
255    #[test]
256    fn read_all_returns_all_events() {
257        let events = vec![
258            make_event(1000, "alice", EventType::Create, "bn-001"),
259            make_event(2000, "bob", EventType::Create, "bn-002"),
260            make_event(3000, "carol", EventType::Move, "bn-001"),
261        ];
262        let bytes = encode_events(&events, 0).unwrap();
263        let reader = CacheReader::from_bytes(bytes).unwrap();
264
265        let decoded = reader.read_all().unwrap();
266        assert_eq!(decoded.len(), 3);
267        assert_eq!(decoded[0].agent, "alice");
268        assert_eq!(decoded[1].agent, "bob");
269        assert_eq!(decoded[2].event_type, EventType::Move);
270    }
271
272    #[test]
273    fn read_all_empty_cache() {
274        let bytes = encode_events(&[], 0).unwrap();
275        let reader = CacheReader::from_bytes(bytes).unwrap();
276        assert!(reader.read_all().unwrap().is_empty());
277    }
278
279    // === read_range ========================================================
280
281    #[test]
282    fn read_range_subset() {
283        let events: Vec<Event> = (0..10)
284            .map(|i| make_event(i * 1000, "agent", EventType::Create, &format!("bn-{i:03}")))
285            .collect();
286        let bytes = encode_events(&events, 0).unwrap();
287        let reader = CacheReader::from_bytes(bytes).unwrap();
288
289        let range = reader.read_range(3, 4).unwrap();
290        assert_eq!(range.len(), 4);
291        assert_eq!(range[0].wall_ts_us, 3000);
292        assert_eq!(range[3].wall_ts_us, 6000);
293    }
294
295    #[test]
296    fn read_range_clamped_to_end() {
297        let events = vec![
298            make_event(1000, "a", EventType::Create, "bn-001"),
299            make_event(2000, "b", EventType::Create, "bn-002"),
300        ];
301        let bytes = encode_events(&events, 0).unwrap();
302        let reader = CacheReader::from_bytes(bytes).unwrap();
303
304        // Request more than available
305        let range = reader.read_range(1, 100).unwrap();
306        assert_eq!(range.len(), 1);
307        assert_eq!(range[0].wall_ts_us, 2000);
308    }
309
310    #[test]
311    fn read_range_start_past_end() {
312        let events = vec![make_event(1000, "a", EventType::Create, "bn-001")];
313        let bytes = encode_events(&events, 0).unwrap();
314        let reader = CacheReader::from_bytes(bytes).unwrap();
315
316        let range = reader.read_range(5, 10).unwrap();
317        assert!(range.is_empty());
318    }
319
320    // === metadata accessors ================================================
321
322    #[test]
323    fn file_size_matches_encoded_bytes() {
324        let events = vec![make_event(1000, "a", EventType::Create, "bn-001")];
325        let bytes = encode_events(&events, 0).unwrap();
326        let expected_size = bytes.len();
327        let reader = CacheReader::from_bytes(bytes).unwrap();
328        assert_eq!(reader.file_size(), expected_size);
329    }
330
331    #[test]
332    fn data_crc64_is_nonzero_for_nonempty() {
333        let events = vec![make_event(1000, "a", EventType::Create, "bn-001")];
334        let bytes = encode_events(&events, 0).unwrap();
335        let reader = CacheReader::from_bytes(bytes).unwrap();
336        assert_ne!(reader.data_crc64(), 0);
337    }
338}