Download rust/crates/hyperkitty-worm/src/header.rs from Snapkitty/hyperkitty-constraint-dsl: direct link, hf CLI and curl.
- Browser
- Download file 6.73 kB
-
https://huggingface.co/Snapkitty/hyperkitty-constraint-dsl/resolve/main/rust/crates/hyperkitty-worm/src/header.rs
- Command line
-
hf download hf://Snapkitty/hyperkitty-constraint-dsl/rust/crates/hyperkitty-worm/src/header.rs
-
curl -L -o header.rs https://huggingface.co/Snapkitty/hyperkitty-constraint-dsl/resolve/main/rust/crates/hyperkitty-worm/src/header.rs
6.73 kB
| /// Binary WORM record header — 152 bytes fixed-size. | |
| /// | |
| /// Layout (big-endian): | |
| /// ```text | |
| /// Field Bytes Offset | |
| /// magic 4 0 "WORM" = [0x57, 0x4F, 0x52, 0x4D] | |
| /// version 1 4 | |
| /// flags 1 5 | |
| /// event_length 2 6 (u16 big-endian) | |
| /// data_length 4 8 (u32 big-endian) | |
| /// metadata_length 4 12 (u32 big-endian) | |
| /// timestamp_ns 8 16 (u64 big-endian) | |
| /// previous_hash 32 24 Blake2b-256 of previous record | |
| /// content_hash 32 56 Blake2b-256 of payload | |
| /// signature 64 88 Ed25519 signature | |
| /// ``` | |
| /// | |
| /// Total: 4+1+1+2+4+4+8+32+32+64 = 152 bytes | |
| /// Magic bytes: ASCII "WORM" | |
| pub const MAGIC: [u8; 4] = [0x57, 0x4F, 0x52, 0x4D]; | |
| /// Current format version | |
| pub const VERSION: u8 = 1; | |
| /// Fixed header size in bytes | |
| pub const HEADER_SIZE: usize = 152; | |
| /// Hash size (Blake2b-256) | |
| pub const HASH_SIZE: usize = 32; | |
| /// Signature size (Ed25519) | |
| pub const SIGNATURE_SIZE: usize = 64; | |
| /// The genesis record uses all-zero previous_hash | |
| pub const GENESIS_HASH: [u8; HASH_SIZE] = [0u8; HASH_SIZE]; | |
| pub struct WormHeader { | |
| pub magic: [u8; 4], | |
| pub version: u8, | |
| pub flags: u8, | |
| pub event_length: u16, | |
| pub data_length: u32, | |
| pub metadata_length: u32, | |
| pub timestamp_ns: u64, | |
| pub previous_hash: [u8; HASH_SIZE], | |
| pub content_hash: [u8; HASH_SIZE], | |
| pub signature: [u8; SIGNATURE_SIZE], | |
| } | |
| impl WormHeader { | |
| /// Create a new header with default magic and version. | |
| pub fn new() -> Self { | |
| Self { | |
| magic: MAGIC, | |
| version: VERSION, | |
| flags: 0, | |
| event_length: 0, | |
| data_length: 0, | |
| metadata_length: 0, | |
| timestamp_ns: 0, | |
| previous_hash: GENESIS_HASH, | |
| content_hash: GENESIS_HASH, | |
| signature: [0u8; SIGNATURE_SIZE], | |
| } | |
| } | |
| /// Total payload size following the header. | |
| pub fn payload_size(&self) -> usize { | |
| self.event_length as usize + self.data_length as usize + self.metadata_length as usize | |
| } | |
| /// Total record size (header + payload). | |
| pub fn record_size(&self) -> usize { | |
| HEADER_SIZE + self.payload_size() | |
| } | |
| /// Serialize header to bytes (big-endian) | |
| pub fn to_bytes(&self) -> Vec<u8> { | |
| let mut bytes = Vec::with_capacity(HEADER_SIZE); | |
| bytes.extend_from_slice(&self.magic); | |
| bytes.push(self.version); | |
| bytes.push(self.flags); | |
| bytes.extend_from_slice(&self.event_length.to_be_bytes()); | |
| bytes.extend_from_slice(&self.data_length.to_be_bytes()); | |
| bytes.extend_from_slice(&self.metadata_length.to_be_bytes()); | |
| bytes.extend_from_slice(&self.timestamp_ns.to_be_bytes()); | |
| bytes.extend_from_slice(&self.previous_hash); | |
| bytes.extend_from_slice(&self.content_hash); | |
| bytes.extend_from_slice(&self.signature); | |
| bytes | |
| } | |
| /// Deserialize header from bytes (big-endian) | |
| pub fn from_bytes(bytes: &[u8]) -> hyperkitty_core::Result<Self> { | |
| if bytes.len() < HEADER_SIZE { | |
| return Err(hyperkitty_core::Error::StorageError( | |
| format!("header too short: {} < {}", bytes.len(), HEADER_SIZE), | |
| )); | |
| } | |
| let mut offset = 0; | |
| let mut magic = [0u8; 4]; | |
| magic.copy_from_slice(&bytes[offset..offset + 4]); | |
| offset += 4; | |
| if magic != MAGIC { | |
| return Err(hyperkitty_core::Error::StorageError( | |
| "invalid magic bytes".to_string(), | |
| )); | |
| } | |
| let version = bytes[offset]; | |
| offset += 1; | |
| if version != VERSION { | |
| return Err(hyperkitty_core::Error::StorageError( | |
| format!("unsupported version: {}", version), | |
| )); | |
| } | |
| let flags = bytes[offset]; | |
| offset += 1; | |
| let event_length = u16::from_be_bytes([bytes[offset], bytes[offset + 1]]); | |
| offset += 2; | |
| let data_length = u32::from_be_bytes([ | |
| bytes[offset], | |
| bytes[offset + 1], | |
| bytes[offset + 2], | |
| bytes[offset + 3], | |
| ]); | |
| offset += 4; | |
| let metadata_length = u32::from_be_bytes([ | |
| bytes[offset], | |
| bytes[offset + 1], | |
| bytes[offset + 2], | |
| bytes[offset + 3], | |
| ]); | |
| offset += 4; | |
| let timestamp_ns = u64::from_be_bytes([ | |
| bytes[offset], | |
| bytes[offset + 1], | |
| bytes[offset + 2], | |
| bytes[offset + 3], | |
| bytes[offset + 4], | |
| bytes[offset + 5], | |
| bytes[offset + 6], | |
| bytes[offset + 7], | |
| ]); | |
| offset += 8; | |
| let mut previous_hash = [0u8; HASH_SIZE]; | |
| previous_hash.copy_from_slice(&bytes[offset..offset + HASH_SIZE]); | |
| offset += HASH_SIZE; | |
| let mut content_hash = [0u8; HASH_SIZE]; | |
| content_hash.copy_from_slice(&bytes[offset..offset + HASH_SIZE]); | |
| offset += HASH_SIZE; | |
| let mut signature = [0u8; SIGNATURE_SIZE]; | |
| signature.copy_from_slice(&bytes[offset..offset + SIGNATURE_SIZE]); | |
| Ok(Self { | |
| magic, | |
| version, | |
| flags, | |
| event_length, | |
| data_length, | |
| metadata_length, | |
| timestamp_ns, | |
| previous_hash, | |
| content_hash, | |
| signature, | |
| }) | |
| } | |
| } | |
| impl Default for WormHeader { | |
| fn default() -> Self { | |
| Self::new() | |
| } | |
| } | |
| mod tests { | |
| use super::*; | |
| fn header_round_trip() { | |
| let h = WormHeader::new(); | |
| let bytes = h.to_bytes(); | |
| assert_eq!(bytes.len(), HEADER_SIZE); | |
| let h2 = WormHeader::from_bytes(&bytes).unwrap(); | |
| assert_eq!(h.magic, h2.magic); | |
| assert_eq!(h.version, h2.version); | |
| assert_eq!(h.flags, h2.flags); | |
| } | |
| fn header_magic_validation() { | |
| let mut bad_header = vec![0u8; HEADER_SIZE]; | |
| bad_header[0] = 0xFF; | |
| let result = WormHeader::from_bytes(&bad_header); | |
| assert!(result.is_err()); | |
| } | |
| fn header_with_data() { | |
| let mut h = WormHeader::new(); | |
| h.event_length = 100; | |
| h.data_length = 200; | |
| h.metadata_length = 50; | |
| h.timestamp_ns = 12345678; | |
| let bytes = h.to_bytes(); | |
| let h2 = WormHeader::from_bytes(&bytes).unwrap(); | |
| assert_eq!(h2.event_length, 100); | |
| assert_eq!(h2.data_length, 200); | |
| assert_eq!(h2.metadata_length, 50); | |
| assert_eq!(h2.timestamp_ns, 12345678); | |
| } | |
| } | |