Skip to main content

agentic_reality/format/
header.rs

1//! .areal file header — 256 bytes.
2
3use crate::types::error::{RealityError, RealityResult};
4use crate::types::{AREAL_MAGIC, FORMAT_VERSION, HEADER_SIZE};
5use serde::{Deserialize, Serialize};
6
7/// File header flags.
8pub mod flags {
9    pub const COMPRESSED: u64 = 1 << 0;
10    pub const SENSITIVE: u64 = 1 << 1;
11    pub const ENCRYPTED: u64 = 1 << 2;
12    pub const MIGRATED: u64 = 1 << 3;
13    pub const SNAPSHOT: u64 = 1 << 4;
14    pub const FULL_TOPOLOGY: u64 = 1 << 5;
15    pub const HAS_INCARNATION_MEMORY: u64 = 1 << 6;
16}
17
18/// Compression method.
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
20#[repr(u8)]
21pub enum Compression {
22    None = 0,
23    Lz4 = 1,
24    Zstd = 2,
25}
26
27/// The 256-byte header of a .areal file.
28#[derive(Debug, Clone)]
29pub struct ArealHeader {
30    pub magic: [u8; 4],
31    pub version: u32,
32    pub flags: u64,
33    pub incarnation_id: [u8; 16],
34    pub created: i64,
35    pub modified: i64,
36    pub section_count: u32,
37    pub section_table_offset: u64,
38    pub compression: Compression,
39    pub checksum: [u8; 8],
40}
41
42impl ArealHeader {
43    pub fn new(incarnation_id: [u8; 16]) -> Self {
44        let now = chrono::Utc::now().timestamp();
45        Self {
46            magic: AREAL_MAGIC,
47            version: FORMAT_VERSION,
48            flags: 0,
49            incarnation_id,
50            created: now,
51            modified: now,
52            section_count: 0,
53            section_table_offset: HEADER_SIZE as u64,
54            compression: Compression::None,
55            checksum: [0u8; 8],
56        }
57    }
58
59    pub fn write_to(&self, buf: &mut Vec<u8>) -> RealityResult<()> {
60        buf.extend_from_slice(&self.magic);
61        buf.extend_from_slice(&self.version.to_le_bytes());
62        buf.extend_from_slice(&self.flags.to_le_bytes());
63        buf.extend_from_slice(&self.incarnation_id);
64        buf.extend_from_slice(&self.created.to_le_bytes());
65        buf.extend_from_slice(&self.modified.to_le_bytes());
66        buf.extend_from_slice(&self.section_count.to_le_bytes());
67        buf.extend_from_slice(&self.section_table_offset.to_le_bytes());
68        buf.push(self.compression as u8);
69        // Reserved: pad to 248 bytes (256 - 8 checksum)
70        let current = buf.len();
71        let target = HEADER_SIZE - 8;
72        if current < target {
73            buf.resize(target, 0);
74        }
75        buf.extend_from_slice(&self.checksum);
76        Ok(())
77    }
78
79    pub fn read_from(data: &[u8]) -> RealityResult<Self> {
80        if data.len() < HEADER_SIZE {
81            return Err(RealityError::InvalidFormat("header too short".into()));
82        }
83        let magic: [u8; 4] = data[0..4]
84            .try_into()
85            .map_err(|_| RealityError::InvalidMagic)?;
86        if magic != AREAL_MAGIC {
87            return Err(RealityError::InvalidMagic);
88        }
89        let version = u32::from_le_bytes(
90            data[4..8]
91                .try_into()
92                .map_err(|_| RealityError::InvalidFormat("version".into()))?,
93        );
94        if version != FORMAT_VERSION {
95            return Err(RealityError::VersionMismatch {
96                expected: FORMAT_VERSION,
97                got: version,
98            });
99        }
100        let flags = u64::from_le_bytes(
101            data[8..16]
102                .try_into()
103                .map_err(|_| RealityError::InvalidFormat("flags".into()))?,
104        );
105        let incarnation_id: [u8; 16] = data[16..32]
106            .try_into()
107            .map_err(|_| RealityError::InvalidFormat("incarnation_id".into()))?;
108        let created = i64::from_le_bytes(
109            data[32..40]
110                .try_into()
111                .map_err(|_| RealityError::InvalidFormat("created".into()))?,
112        );
113        let modified = i64::from_le_bytes(
114            data[40..48]
115                .try_into()
116                .map_err(|_| RealityError::InvalidFormat("modified".into()))?,
117        );
118        let section_count = u32::from_le_bytes(
119            data[48..52]
120                .try_into()
121                .map_err(|_| RealityError::InvalidFormat("section_count".into()))?,
122        );
123        let section_table_offset = u64::from_le_bytes(
124            data[52..60]
125                .try_into()
126                .map_err(|_| RealityError::InvalidFormat("section_table_offset".into()))?,
127        );
128        let compression = match data[60] {
129            0 => Compression::None,
130            1 => Compression::Lz4,
131            2 => Compression::Zstd,
132            v => {
133                return Err(RealityError::InvalidFormat(format!(
134                    "unknown compression: {}",
135                    v
136                )))
137            }
138        };
139        let checksum: [u8; 8] = data[HEADER_SIZE - 8..HEADER_SIZE]
140            .try_into()
141            .map_err(|_| RealityError::InvalidFormat("checksum".into()))?;
142
143        Ok(Self {
144            magic,
145            version,
146            flags,
147            incarnation_id,
148            created,
149            modified,
150            section_count,
151            section_table_offset,
152            compression,
153            checksum,
154        })
155    }
156}