1use std::collections::HashMap;
4use std::io::{Read, Write};
5use std::path::PathBuf;
6
7use chrono::Utc;
8use serde::{Deserialize, Serialize};
9
10use crate::error::{TimeError, TimeResult};
11use crate::TemporalId;
12
13pub const MAGIC: [u8; 4] = *b"ATIM";
15
16pub const VERSION: u32 = 1;
18
19#[derive(Debug, Clone)]
21#[repr(C)]
22pub struct FileHeader {
23 pub magic: [u8; 4],
25 pub version: u32,
27 pub flags: u32,
29 pub entity_count: u64,
31 pub index_offset: u64,
33 pub deadline_index_offset: u64,
35 pub decay_index_offset: u64,
37 pub created_at: u64,
39 pub modified_at: u64,
41 pub checksum: [u8; 32],
43}
44
45impl Default for FileHeader {
46 fn default() -> Self {
47 Self::new()
48 }
49}
50
51impl FileHeader {
52 pub const SIZE: usize = 4 + 4 + 4 + 8 + 8 + 8 + 8 + 8 + 8 + 32;
56
57 pub fn new() -> Self {
59 let now = Utc::now().timestamp_micros() as u64;
60 Self {
61 magic: MAGIC,
62 version: VERSION,
63 flags: 0,
64 entity_count: 0,
65 index_offset: 0,
66 deadline_index_offset: 0,
67 decay_index_offset: 0,
68 created_at: now,
69 modified_at: now,
70 checksum: [0; 32],
71 }
72 }
73
74 pub fn write_to<W: Write>(&self, writer: &mut W) -> TimeResult<()> {
76 writer.write_all(&self.magic)?;
77 writer.write_all(&self.version.to_le_bytes())?;
78 writer.write_all(&self.flags.to_le_bytes())?;
79 writer.write_all(&self.entity_count.to_le_bytes())?;
80 writer.write_all(&self.index_offset.to_le_bytes())?;
81 writer.write_all(&self.deadline_index_offset.to_le_bytes())?;
82 writer.write_all(&self.decay_index_offset.to_le_bytes())?;
83 writer.write_all(&self.created_at.to_le_bytes())?;
84 writer.write_all(&self.modified_at.to_le_bytes())?;
85 writer.write_all(&self.checksum)?;
86 Ok(())
87 }
88
89 pub fn read_from<R: Read>(reader: &mut R) -> TimeResult<Self> {
91 let mut magic = [0u8; 4];
92 reader.read_exact(&mut magic)?;
93
94 if magic != MAGIC {
95 return Err(TimeError::FileFormat(format!(
96 "Invalid magic bytes: {:?} (expected {:?}). File may be corrupted — try creating a new .atime file.",
97 magic, MAGIC
98 )));
99 }
100
101 let mut buf4 = [0u8; 4];
102 let mut buf8 = [0u8; 8];
103 let mut checksum = [0u8; 32];
104
105 reader.read_exact(&mut buf4)?;
106 let version = u32::from_le_bytes(buf4);
107
108 reader.read_exact(&mut buf4)?;
109 let flags = u32::from_le_bytes(buf4);
110
111 reader.read_exact(&mut buf8)?;
112 let entity_count = u64::from_le_bytes(buf8);
113
114 reader.read_exact(&mut buf8)?;
115 let index_offset = u64::from_le_bytes(buf8);
116
117 reader.read_exact(&mut buf8)?;
118 let deadline_index_offset = u64::from_le_bytes(buf8);
119
120 reader.read_exact(&mut buf8)?;
121 let decay_index_offset = u64::from_le_bytes(buf8);
122
123 reader.read_exact(&mut buf8)?;
124 let created_at = u64::from_le_bytes(buf8);
125
126 reader.read_exact(&mut buf8)?;
127 let modified_at = u64::from_le_bytes(buf8);
128
129 reader.read_exact(&mut checksum)?;
130
131 Ok(Self {
132 magic,
133 version,
134 flags,
135 entity_count,
136 index_offset,
137 deadline_index_offset,
138 decay_index_offset,
139 created_at,
140 modified_at,
141 checksum,
142 })
143 }
144}
145
146#[derive(Debug, Clone, Copy, PartialEq, Eq)]
148#[repr(u8)]
149pub enum EntityType {
150 Duration = 1,
152 Deadline = 2,
154 Schedule = 3,
156 Sequence = 4,
158 Decay = 5,
160}
161
162impl TryFrom<u8> for EntityType {
163 type Error = TimeError;
164
165 fn try_from(value: u8) -> Result<Self, Self::Error> {
166 match value {
167 1 => Ok(EntityType::Duration),
168 2 => Ok(EntityType::Deadline),
169 3 => Ok(EntityType::Schedule),
170 4 => Ok(EntityType::Sequence),
171 5 => Ok(EntityType::Decay),
172 _ => Err(TimeError::FileFormat(format!(
173 "Unknown entity type: {}",
174 value
175 ))),
176 }
177 }
178}
179
180#[derive(Debug, Clone)]
182pub struct EntityBlock {
183 pub entity_type: EntityType,
185 pub id: TemporalId,
187 pub payload_len: u32,
189 pub payload: Vec<u8>,
191}
192
193impl EntityBlock {
194 pub fn new<T: Serialize>(
196 entity_type: EntityType,
197 id: TemporalId,
198 entity: &T,
199 ) -> TimeResult<Self> {
200 let payload = serde_json::to_vec(entity)?;
201 Ok(Self {
202 entity_type,
203 id,
204 payload_len: payload.len() as u32,
205 payload,
206 })
207 }
208
209 pub fn write_to<W: Write>(&self, writer: &mut W) -> TimeResult<()> {
211 writer.write_all(&[self.entity_type as u8])?;
212 writer.write_all(self.id.0.as_bytes())?;
213 writer.write_all(&self.payload_len.to_le_bytes())?;
214 writer.write_all(&self.payload)?;
215 Ok(())
216 }
217
218 pub fn read_from<R: Read>(reader: &mut R) -> TimeResult<Self> {
220 let mut type_buf = [0u8; 1];
221 reader.read_exact(&mut type_buf)?;
222 let entity_type = EntityType::try_from(type_buf[0])?;
223
224 let mut id_buf = [0u8; 16];
225 reader.read_exact(&mut id_buf)?;
226 let id = TemporalId(uuid::Uuid::from_bytes(id_buf));
227
228 let mut len_buf = [0u8; 4];
229 reader.read_exact(&mut len_buf)?;
230 let payload_len = u32::from_le_bytes(len_buf);
231
232 let mut payload = vec![0u8; payload_len as usize];
233 reader.read_exact(&mut payload)?;
234
235 Ok(Self {
236 entity_type,
237 id,
238 payload_len,
239 payload,
240 })
241 }
242
243 pub fn deserialize<T: for<'de> Deserialize<'de>>(&self) -> TimeResult<T> {
245 Ok(serde_json::from_slice(&self.payload)?)
246 }
247}
248
249#[derive(Debug)]
251pub struct TimeFile {
252 pub path: PathBuf,
254 pub header: FileHeader,
256 entities: HashMap<TemporalId, EntityBlock>,
258}
259
260impl TimeFile {
261 pub fn create(path: impl Into<PathBuf>) -> TimeResult<Self> {
263 let path = path.into();
264 let header = FileHeader::new();
265
266 let file = Self {
267 path,
268 header,
269 entities: HashMap::new(),
270 };
271
272 file.save()?;
273 Ok(file)
274 }
275
276 pub fn open(path: impl Into<PathBuf>) -> TimeResult<Self> {
278 let path = path.into();
279 let mut file = std::fs::File::open(&path)?;
280
281 let header = FileHeader::read_from(&mut file)?;
282
283 let mut entities = HashMap::new();
284 for _ in 0..header.entity_count {
285 let block = EntityBlock::read_from(&mut file)?;
286 entities.insert(block.id, block);
287 }
288
289 Ok(Self {
290 path,
291 header,
292 entities,
293 })
294 }
295
296 pub fn save(&self) -> TimeResult<()> {
298 let tmp_path = self.path.with_extension("atime.tmp");
299 let mut file = std::fs::File::create(&tmp_path)?;
300
301 let mut header = self.header.clone();
302 header.entity_count = self.entities.len() as u64;
303 header.modified_at = Utc::now().timestamp_micros() as u64;
304
305 header.write_to(&mut file)?;
306
307 for block in self.entities.values() {
308 block.write_to(&mut file)?;
309 }
310
311 file.flush()?;
312 std::fs::rename(&tmp_path, &self.path)?;
313
314 Ok(())
315 }
316
317 pub fn add<T: Serialize>(
319 &mut self,
320 entity_type: EntityType,
321 id: TemporalId,
322 entity: &T,
323 ) -> TimeResult<()> {
324 let block = EntityBlock::new(entity_type, id, entity)?;
325 self.entities.insert(id, block);
326 Ok(())
327 }
328
329 pub fn get<T: for<'de> Deserialize<'de>>(&self, id: &TemporalId) -> TimeResult<Option<T>> {
331 match self.entities.get(id) {
332 Some(block) => Ok(Some(block.deserialize()?)),
333 None => Ok(None),
334 }
335 }
336
337 pub fn remove(&mut self, id: &TemporalId) -> bool {
339 self.entities.remove(id).is_some()
340 }
341
342 pub fn list_by_type(&self, entity_type: EntityType) -> Vec<&EntityBlock> {
344 self.entities
345 .values()
346 .filter(|b| b.entity_type == entity_type)
347 .collect()
348 }
349
350 pub fn entity_count(&self) -> usize {
352 self.entities.len()
353 }
354}
355
356#[cfg(test)]
357mod tests {
358 use super::*;
359 use crate::deadline::Deadline;
360 use chrono::Duration as ChronoDuration;
361 use tempfile::tempdir;
362
363 #[test]
364 fn test_create_and_reopen() {
365 let dir = tempdir().unwrap();
366 let path = dir.path().join("test.atime");
367
368 let mut tf = TimeFile::create(&path).unwrap();
370 let d = Deadline::new("Ship v1", Utc::now() + ChronoDuration::hours(24));
371 let id = d.id;
372 tf.add(EntityType::Deadline, id, &d).unwrap();
373 tf.save().unwrap();
374
375 let tf2 = TimeFile::open(&path).unwrap();
377 assert_eq!(tf2.entity_count(), 1);
378 let loaded: Deadline = tf2.get(&id).unwrap().unwrap();
379 assert_eq!(loaded.label, "Ship v1");
380 }
381
382 #[test]
383 fn test_remove_entity() {
384 let dir = tempdir().unwrap();
385 let path = dir.path().join("test.atime");
386
387 let mut tf = TimeFile::create(&path).unwrap();
388 let d = Deadline::new("Remove me", Utc::now() + ChronoDuration::hours(1));
389 let id = d.id;
390 tf.add(EntityType::Deadline, id, &d).unwrap();
391 assert_eq!(tf.entity_count(), 1);
392
393 assert!(tf.remove(&id));
394 assert_eq!(tf.entity_count(), 0);
395 }
396
397 #[test]
398 fn test_empty_file() {
399 let dir = tempdir().unwrap();
400 let path = dir.path().join("empty.atime");
401
402 let tf = TimeFile::create(&path).unwrap();
403 assert_eq!(tf.entity_count(), 0);
404 assert!(tf.list_by_type(EntityType::Deadline).is_empty());
405
406 let tf2 = TimeFile::open(&path).unwrap();
407 assert_eq!(tf2.entity_count(), 0);
408 }
409
410 #[test]
411 fn test_invalid_magic() {
412 let dir = tempdir().unwrap();
413 let path = dir.path().join("bad.atime");
414 std::fs::write(&path, b"BAD_DATA_NOT_ATIM").unwrap();
415
416 let result = TimeFile::open(&path);
417 assert!(result.is_err());
418 let err = result.unwrap_err().to_string();
419 assert!(err.contains("Invalid magic bytes"));
420 }
421}