| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| |
| pub const MAGIC: [u8; 4] = [0x57, 0x4F, 0x52, 0x4D]; |
|
|
| |
| pub const VERSION: u8 = 1; |
|
|
| |
| pub const HEADER_SIZE: usize = 152; |
|
|
| |
| pub const HASH_SIZE: usize = 32; |
|
|
| |
| pub const SIGNATURE_SIZE: usize = 64; |
|
|
| |
| pub const GENESIS_HASH: [u8; HASH_SIZE] = [0u8; HASH_SIZE]; |
|
|
| #[derive(Debug, Clone, PartialEq, Eq)] |
| 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 { |
| |
| 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], |
| } |
| } |
|
|
| |
| pub fn payload_size(&self) -> usize { |
| self.event_length as usize + self.data_length as usize + self.metadata_length as usize |
| } |
|
|
| |
| pub fn record_size(&self) -> usize { |
| HEADER_SIZE + self.payload_size() |
| } |
|
|
| |
| 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 |
| } |
|
|
| |
| 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() |
| } |
| } |
|
|
| #[cfg(test)] |
| mod tests { |
| use super::*; |
|
|
| #[test] |
| 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); |
| } |
|
|
| #[test] |
| 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()); |
| } |
|
|
| #[test] |
| 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); |
| } |
| } |
|
|