alopex_core/storage/format/
header.rs1use std::convert::TryInto;
4use std::mem;
5
6#[cfg(not(target_arch = "wasm32"))]
7use std::time::{SystemTime, UNIX_EPOCH};
8
9use crate::storage::checksum::ChecksumAlgorithm;
10use crate::storage::compression::CompressionAlgorithm;
11use crate::storage::format::{FileVersion, FormatError, HEADER_SIZE, MAGIC};
12
13#[repr(C)]
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
16pub struct FileFlags(pub u32);
17
18impl FileFlags {
19 pub const ENCRYPTED: u32 = 1 << 1;
21 pub const VALUE_SEPARATED: u32 = 1 << 2;
23
24 pub fn bits(&self) -> u32 {
26 self.0
27 }
28
29 pub fn is_encrypted(&self) -> bool {
31 self.0 & Self::ENCRYPTED != 0
32 }
33
34 pub fn is_value_separated(&self) -> bool {
36 self.0 & Self::VALUE_SEPARATED != 0
37 }
38}
39
40#[repr(C)]
55#[derive(Debug, Clone, Copy, PartialEq, Eq)]
56pub struct FileHeader {
57 pub magic: [u8; 4],
59 pub version: FileVersion,
61 pub checksum_algorithm: ChecksumAlgorithm,
63 pub compression_algorithm: CompressionAlgorithm,
65 pub flags: FileFlags,
67 pub created_at: u64,
69 pub modified_at: u64,
71 pub schema_version: u64,
73 pub reserved: [u8; 24],
75}
76
77const _: () = assert!(mem::size_of::<FileHeader>() == HEADER_SIZE);
78
79impl FileHeader {
80 pub const SIZE: usize = HEADER_SIZE;
82
83 pub fn new(version: FileVersion, flags: FileFlags) -> Self {
88 let now = now_micros();
89 Self {
90 magic: MAGIC,
91 version,
92 checksum_algorithm: ChecksumAlgorithm::Crc32,
93 compression_algorithm: CompressionAlgorithm::Snappy,
94 flags,
95 created_at: now,
96 modified_at: now,
97 schema_version: 0,
98 reserved: [0u8; 24],
99 }
100 }
101
102 pub fn from_bytes(bytes: &[u8; HEADER_SIZE]) -> Result<Self, FormatError> {
104 let mut magic = [0u8; 4];
105 magic.copy_from_slice(&bytes[0..4]);
106
107 let version = FileVersion {
108 major: u16::from_le_bytes([bytes[4], bytes[5]]),
109 minor: u16::from_le_bytes([bytes[6], bytes[7]]),
110 patch: u16::from_le_bytes([bytes[8], bytes[9]]),
111 };
112
113 let checksum_algorithm = match bytes[10] {
114 0 => ChecksumAlgorithm::Crc32,
115 1 => ChecksumAlgorithm::Xxh64,
116 other => return Err(FormatError::UnsupportedChecksum { algorithm: other }),
117 };
118
119 let compression_algorithm = match bytes[11] {
120 0 => CompressionAlgorithm::None,
121 1 => CompressionAlgorithm::Snappy,
122 2 => CompressionAlgorithm::Zstd,
123 3 => CompressionAlgorithm::Lz4,
124 other => return Err(FormatError::UnsupportedCompression { algorithm: other }),
125 };
126
127 let flags = FileFlags(u32::from_le_bytes(
128 bytes[12..16].try_into().expect("fixed slice length"),
129 ));
130
131 let created_at = u64::from_le_bytes(bytes[16..24].try_into().expect("fixed slice length"));
132 let modified_at = u64::from_le_bytes(bytes[24..32].try_into().expect("fixed slice length"));
133 let schema_version =
134 u64::from_le_bytes(bytes[32..40].try_into().expect("fixed slice length"));
135
136 let mut reserved = [0u8; 24];
137 reserved.copy_from_slice(&bytes[40..64]);
138
139 let header = Self {
140 magic,
141 version,
142 checksum_algorithm,
143 compression_algorithm,
144 flags,
145 created_at,
146 modified_at,
147 schema_version,
148 reserved,
149 };
150
151 header.validate_magic()?;
152 Ok(header)
153 }
154
155 pub fn to_bytes(&self) -> [u8; HEADER_SIZE] {
157 let mut bytes = [0u8; HEADER_SIZE];
158 bytes[0..4].copy_from_slice(&self.magic);
159 bytes[4..6].copy_from_slice(&self.version.major.to_le_bytes());
160 bytes[6..8].copy_from_slice(&self.version.minor.to_le_bytes());
161 bytes[8..10].copy_from_slice(&self.version.patch.to_le_bytes());
162 bytes[10] = self.checksum_algorithm as u8;
163 bytes[11] = self.compression_algorithm as u8;
164 bytes[12..16].copy_from_slice(&self.flags.bits().to_le_bytes());
165 bytes[16..24].copy_from_slice(&self.created_at.to_le_bytes());
166 bytes[24..32].copy_from_slice(&self.modified_at.to_le_bytes());
167 bytes[32..40].copy_from_slice(&self.schema_version.to_le_bytes());
168 bytes[40..64].copy_from_slice(&self.reserved);
169 bytes
170 }
171
172 pub fn validate_magic(&self) -> Result<(), FormatError> {
174 if self.magic == MAGIC {
175 Ok(())
176 } else {
177 Err(FormatError::InvalidMagic { found: self.magic })
178 }
179 }
180
181 pub fn check_compatibility(&self, reader_version: &FileVersion) -> Result<(), FormatError> {
185 if self.version > *reader_version {
186 Err(FormatError::IncompatibleVersion {
187 file: self.version,
188 reader: *reader_version,
189 })
190 } else {
191 Ok(())
192 }
193 }
194}
195
196#[cfg(not(target_arch = "wasm32"))]
197fn now_micros() -> u64 {
198 SystemTime::now()
199 .duration_since(UNIX_EPOCH)
200 .unwrap_or_default()
201 .as_micros() as u64
202}
203
204#[cfg(target_arch = "wasm32")]
205fn now_micros() -> u64 {
206 #[cfg(feature = "wasm-indexeddb")]
207 {
208 (js_sys::Date::now() * 1000.0) as u64
209 }
210 #[cfg(not(feature = "wasm-indexeddb"))]
211 {
212 0
213 }
214}