#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[repr(u8)]
pub enum DerivationType {
Clone = 0,
Filter = 1,
Merge = 2,
Quantize = 3,
Reindex = 4,
Transform = 5,
Snapshot = 6,
UserDefined = 0xFF,
}
impl TryFrom<u8> for DerivationType {
type Error = u8;
fn try_from(value: u8) -> Result<Self, Self::Error> {
match value {
0 => Ok(Self::Clone),
1 => Ok(Self::Filter),
2 => Ok(Self::Merge),
3 => Ok(Self::Quantize),
4 => Ok(Self::Reindex),
5 => Ok(Self::Transform),
6 => Ok(Self::Snapshot),
0xFF => Ok(Self::UserDefined),
other => Err(other),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[repr(C)]
pub struct FileIdentity {
pub file_id: [u8; 16],
pub parent_id: [u8; 16],
pub parent_hash: [u8; 32],
pub lineage_depth: u32,
}
const _: () = assert!(core::mem::size_of::<FileIdentity>() == 68);
impl FileIdentity {
pub const fn new_root(file_id: [u8; 16]) -> Self {
Self {
file_id,
parent_id: [0u8; 16],
parent_hash: [0u8; 32],
lineage_depth: 0,
}
}
pub fn is_root(&self) -> bool {
self.parent_id == [0u8; 16] && self.lineage_depth == 0
}
pub const fn zeroed() -> Self {
Self {
file_id: [0u8; 16],
parent_id: [0u8; 16],
parent_hash: [0u8; 32],
lineage_depth: 0,
}
}
pub fn to_bytes(&self) -> [u8; 68] {
let mut buf = [0u8; 68];
buf[0..16].copy_from_slice(&self.file_id);
buf[16..32].copy_from_slice(&self.parent_id);
buf[32..64].copy_from_slice(&self.parent_hash);
buf[64..68].copy_from_slice(&self.lineage_depth.to_le_bytes());
buf
}
pub fn from_bytes(data: &[u8; 68]) -> Self {
let mut file_id = [0u8; 16];
file_id.copy_from_slice(&data[0..16]);
let mut parent_id = [0u8; 16];
parent_id.copy_from_slice(&data[16..32]);
let mut parent_hash = [0u8; 32];
parent_hash.copy_from_slice(&data[32..64]);
let lineage_depth = u32::from_le_bytes([data[64], data[65], data[66], data[67]]);
Self {
file_id,
parent_id,
parent_hash,
lineage_depth,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct LineageRecord {
pub file_id: [u8; 16],
pub parent_id: [u8; 16],
pub parent_hash: [u8; 32],
pub derivation_type: DerivationType,
pub mutation_count: u32,
pub timestamp_ns: u64,
pub description_len: u8,
pub description: [u8; 47],
}
pub const LINEAGE_RECORD_SIZE: usize = 128;
impl LineageRecord {
pub fn new(
file_id: [u8; 16],
parent_id: [u8; 16],
parent_hash: [u8; 32],
derivation_type: DerivationType,
mutation_count: u32,
timestamp_ns: u64,
desc: &str,
) -> Self {
let desc_bytes = desc.as_bytes();
let desc_len = desc_bytes.len().min(47) as u8;
let mut description = [0u8; 47];
description[..desc_len as usize].copy_from_slice(&desc_bytes[..desc_len as usize]);
Self {
file_id,
parent_id,
parent_hash,
derivation_type,
mutation_count,
timestamp_ns,
description_len: desc_len,
description,
}
}
pub fn description_str(&self) -> &str {
let len = (self.description_len as usize).min(47);
core::str::from_utf8(&self.description[..len]).unwrap_or("")
}
}
pub const WITNESS_DERIVATION: u8 = 0x09;
pub const WITNESS_LINEAGE_MERGE: u8 = 0x0A;
pub const WITNESS_LINEAGE_SNAPSHOT: u8 = 0x0B;
pub const WITNESS_LINEAGE_TRANSFORM: u8 = 0x0C;
pub const WITNESS_LINEAGE_VERIFY: u8 = 0x0D;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn file_identity_size() {
assert_eq!(core::mem::size_of::<FileIdentity>(), 68);
}
#[test]
fn file_identity_fits_in_reserved() {
assert!(core::mem::size_of::<FileIdentity>() <= 252);
}
#[test]
fn file_identity_root() {
let id = [0x42u8; 16];
let fi = FileIdentity::new_root(id);
assert!(fi.is_root());
assert_eq!(fi.file_id, id);
assert_eq!(fi.parent_id, [0u8; 16]);
assert_eq!(fi.parent_hash, [0u8; 32]);
assert_eq!(fi.lineage_depth, 0);
}
#[test]
fn file_identity_zeroed_is_root() {
let fi = FileIdentity::zeroed();
assert!(fi.is_root());
}
#[test]
fn file_identity_round_trip() {
let fi = FileIdentity {
file_id: [1u8; 16],
parent_id: [2u8; 16],
parent_hash: [3u8; 32],
lineage_depth: 42,
};
let bytes = fi.to_bytes();
let decoded = FileIdentity::from_bytes(&bytes);
assert_eq!(fi, decoded);
}
#[test]
fn file_identity_non_root() {
let fi = FileIdentity {
file_id: [1u8; 16],
parent_id: [2u8; 16],
parent_hash: [3u8; 32],
lineage_depth: 1,
};
assert!(!fi.is_root());
}
#[test]
fn derivation_type_round_trip() {
let cases: &[(u8, DerivationType)] = &[
(0, DerivationType::Clone),
(1, DerivationType::Filter),
(2, DerivationType::Merge),
(3, DerivationType::Quantize),
(4, DerivationType::Reindex),
(5, DerivationType::Transform),
(6, DerivationType::Snapshot),
(0xFF, DerivationType::UserDefined),
];
for &(raw, expected) in cases {
assert_eq!(DerivationType::try_from(raw), Ok(expected));
assert_eq!(expected as u8, raw);
}
}
#[test]
fn derivation_type_unknown() {
assert_eq!(DerivationType::try_from(7), Err(7));
assert_eq!(DerivationType::try_from(0xFE), Err(0xFE));
}
#[test]
fn lineage_record_description() {
let record = LineageRecord::new(
[1u8; 16],
[2u8; 16],
[3u8; 32],
DerivationType::Filter,
5,
1_000_000_000,
"filtered by category",
);
assert_eq!(record.description_str(), "filtered by category");
assert_eq!(record.description_len, 20);
}
#[test]
fn lineage_record_long_description_truncated() {
let long_desc = "a]".repeat(50); let record = LineageRecord::new(
[0u8; 16],
[0u8; 16],
[0u8; 32],
DerivationType::Clone,
0,
0,
&long_desc,
);
assert_eq!(record.description_len, 47);
}
#[test]
fn witness_type_constants() {
assert_eq!(WITNESS_DERIVATION, 0x09);
assert_eq!(WITNESS_LINEAGE_MERGE, 0x0A);
assert_eq!(WITNESS_LINEAGE_SNAPSHOT, 0x0B);
assert_eq!(WITNESS_LINEAGE_TRANSFORM, 0x0C);
assert_eq!(WITNESS_LINEAGE_VERIFY, 0x0D);
}
}