1#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
8#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
9#[repr(u8)]
10pub enum DerivationType {
11 Clone = 0,
13 Filter = 1,
15 Merge = 2,
17 Quantize = 3,
19 Reindex = 4,
21 Transform = 5,
23 Snapshot = 6,
25 UserDefined = 0xFF,
27}
28
29impl TryFrom<u8> for DerivationType {
30 type Error = u8;
31
32 fn try_from(value: u8) -> Result<Self, Self::Error> {
33 match value {
34 0 => Ok(Self::Clone),
35 1 => Ok(Self::Filter),
36 2 => Ok(Self::Merge),
37 3 => Ok(Self::Quantize),
38 4 => Ok(Self::Reindex),
39 5 => Ok(Self::Transform),
40 6 => Ok(Self::Snapshot),
41 0xFF => Ok(Self::UserDefined),
42 other => Err(other),
43 }
44 }
45}
46
47#[derive(Clone, Copy, Debug, PartialEq, Eq)]
60#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
61#[repr(C)]
62pub struct FileIdentity {
63 pub file_id: [u8; 16],
65 pub parent_id: [u8; 16],
67 pub parent_hash: [u8; 32],
69 pub lineage_depth: u32,
71}
72
73const _: () = assert!(core::mem::size_of::<FileIdentity>() == 68);
75
76impl FileIdentity {
77 pub const fn new_root(file_id: [u8; 16]) -> Self {
79 Self {
80 file_id,
81 parent_id: [0u8; 16],
82 parent_hash: [0u8; 32],
83 lineage_depth: 0,
84 }
85 }
86
87 pub fn is_root(&self) -> bool {
89 self.parent_id == [0u8; 16] && self.lineage_depth == 0
90 }
91
92 pub const fn zeroed() -> Self {
94 Self {
95 file_id: [0u8; 16],
96 parent_id: [0u8; 16],
97 parent_hash: [0u8; 32],
98 lineage_depth: 0,
99 }
100 }
101
102 pub fn to_bytes(&self) -> [u8; 68] {
104 let mut buf = [0u8; 68];
105 buf[0..16].copy_from_slice(&self.file_id);
106 buf[16..32].copy_from_slice(&self.parent_id);
107 buf[32..64].copy_from_slice(&self.parent_hash);
108 buf[64..68].copy_from_slice(&self.lineage_depth.to_le_bytes());
109 buf
110 }
111
112 pub fn from_bytes(data: &[u8; 68]) -> Self {
114 let mut file_id = [0u8; 16];
115 file_id.copy_from_slice(&data[0..16]);
116 let mut parent_id = [0u8; 16];
117 parent_id.copy_from_slice(&data[16..32]);
118 let mut parent_hash = [0u8; 32];
119 parent_hash.copy_from_slice(&data[32..64]);
120 let lineage_depth = u32::from_le_bytes([data[64], data[65], data[66], data[67]]);
123 Self {
124 file_id,
125 parent_id,
126 parent_hash,
127 lineage_depth,
128 }
129 }
130}
131
132#[derive(Clone, Debug, PartialEq, Eq)]
149#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
150pub struct LineageRecord {
151 pub file_id: [u8; 16],
153 pub parent_id: [u8; 16],
155 pub parent_hash: [u8; 32],
157 pub derivation_type: DerivationType,
159 pub mutation_count: u32,
161 pub timestamp_ns: u64,
163 pub description_len: u8,
165 pub description: [u8; 47],
167}
168
169pub const LINEAGE_RECORD_SIZE: usize = 128;
171
172impl LineageRecord {
173 pub fn new(
175 file_id: [u8; 16],
176 parent_id: [u8; 16],
177 parent_hash: [u8; 32],
178 derivation_type: DerivationType,
179 mutation_count: u32,
180 timestamp_ns: u64,
181 desc: &str,
182 ) -> Self {
183 let desc_bytes = desc.as_bytes();
184 let desc_len = desc_bytes.len().min(47) as u8;
185 let mut description = [0u8; 47];
186 description[..desc_len as usize].copy_from_slice(&desc_bytes[..desc_len as usize]);
187 Self {
188 file_id,
189 parent_id,
190 parent_hash,
191 derivation_type,
192 mutation_count,
193 timestamp_ns,
194 description_len: desc_len,
195 description,
196 }
197 }
198
199 pub fn description_str(&self) -> &str {
201 let len = (self.description_len as usize).min(47);
202 core::str::from_utf8(&self.description[..len]).unwrap_or("")
203 }
204}
205
206pub const WITNESS_DERIVATION: u8 = 0x09;
210pub const WITNESS_LINEAGE_MERGE: u8 = 0x0A;
212pub const WITNESS_LINEAGE_SNAPSHOT: u8 = 0x0B;
214pub const WITNESS_LINEAGE_TRANSFORM: u8 = 0x0C;
216pub const WITNESS_LINEAGE_VERIFY: u8 = 0x0D;
218
219#[cfg(test)]
220mod tests {
221 use super::*;
222
223 #[test]
224 fn file_identity_size() {
225 assert_eq!(core::mem::size_of::<FileIdentity>(), 68);
226 }
227
228 #[test]
229 fn file_identity_fits_in_reserved() {
230 assert!(core::mem::size_of::<FileIdentity>() <= 252);
232 }
233
234 #[test]
235 fn file_identity_root() {
236 let id = [0x42u8; 16];
237 let fi = FileIdentity::new_root(id);
238 assert!(fi.is_root());
239 assert_eq!(fi.file_id, id);
240 assert_eq!(fi.parent_id, [0u8; 16]);
241 assert_eq!(fi.parent_hash, [0u8; 32]);
242 assert_eq!(fi.lineage_depth, 0);
243 }
244
245 #[test]
246 fn file_identity_zeroed_is_root() {
247 let fi = FileIdentity::zeroed();
248 assert!(fi.is_root());
249 }
250
251 #[test]
252 fn file_identity_round_trip() {
253 let fi = FileIdentity {
254 file_id: [1u8; 16],
255 parent_id: [2u8; 16],
256 parent_hash: [3u8; 32],
257 lineage_depth: 42,
258 };
259 let bytes = fi.to_bytes();
260 let decoded = FileIdentity::from_bytes(&bytes);
261 assert_eq!(fi, decoded);
262 }
263
264 #[test]
265 fn file_identity_non_root() {
266 let fi = FileIdentity {
267 file_id: [1u8; 16],
268 parent_id: [2u8; 16],
269 parent_hash: [3u8; 32],
270 lineage_depth: 1,
271 };
272 assert!(!fi.is_root());
273 }
274
275 #[test]
276 fn derivation_type_round_trip() {
277 let cases: &[(u8, DerivationType)] = &[
278 (0, DerivationType::Clone),
279 (1, DerivationType::Filter),
280 (2, DerivationType::Merge),
281 (3, DerivationType::Quantize),
282 (4, DerivationType::Reindex),
283 (5, DerivationType::Transform),
284 (6, DerivationType::Snapshot),
285 (0xFF, DerivationType::UserDefined),
286 ];
287 for &(raw, expected) in cases {
288 assert_eq!(DerivationType::try_from(raw), Ok(expected));
289 assert_eq!(expected as u8, raw);
290 }
291 }
292
293 #[test]
294 fn derivation_type_unknown() {
295 assert_eq!(DerivationType::try_from(7), Err(7));
296 assert_eq!(DerivationType::try_from(0xFE), Err(0xFE));
297 }
298
299 #[test]
300 fn lineage_record_description() {
301 let record = LineageRecord::new(
302 [1u8; 16],
303 [2u8; 16],
304 [3u8; 32],
305 DerivationType::Filter,
306 5,
307 1_000_000_000,
308 "filtered by category",
309 );
310 assert_eq!(record.description_str(), "filtered by category");
311 assert_eq!(record.description_len, 20);
312 }
313
314 #[test]
315 fn lineage_record_long_description_truncated() {
316 let long_desc = "a]".repeat(50); let record = LineageRecord::new(
318 [0u8; 16],
319 [0u8; 16],
320 [0u8; 32],
321 DerivationType::Clone,
322 0,
323 0,
324 &long_desc,
325 );
326 assert_eq!(record.description_len, 47);
327 }
328
329 #[test]
330 fn witness_type_constants() {
331 assert_eq!(WITNESS_DERIVATION, 0x09);
332 assert_eq!(WITNESS_LINEAGE_MERGE, 0x0A);
333 assert_eq!(WITNESS_LINEAGE_SNAPSHOT, 0x0B);
334 assert_eq!(WITNESS_LINEAGE_TRANSFORM, 0x0C);
335 assert_eq!(WITNESS_LINEAGE_VERIFY, 0x0D);
336 }
337}