dejadb_core/format/
header.rs1use sha2::{Digest, Sha256};
2
3use crate::error::{DejaDbError, Hash, Result};
4use crate::types::GrainType;
5
6#[derive(Debug, Clone, serde::Serialize)]
8pub struct MgHeader {
9 pub version: u8,
10 pub flags: u8,
11 pub grain_type: u8,
12 pub ns_hash: u16,
13 pub created_at_sec: u32,
14}
15
16impl MgHeader {
17 pub fn new(grain_type: GrainType, namespace: Option<&str>, created_at_ms: i64) -> Self {
19 let ns_hash = match namespace {
20 Some(ns) if !ns.is_empty() => {
21 let hash = Sha256::digest(ns.as_bytes());
22 u16::from_be_bytes([hash[0], hash[1]])
23 }
24 _ => 0u16,
25 };
26 let epoch_secs = created_at_ms / 1000;
27 let created_at_sec = if epoch_secs < 0 {
28 0u32 } else {
30 u32::try_from(epoch_secs).unwrap_or(u32::MAX) };
32
33 MgHeader {
34 version: 0x01,
35 flags: 0x00,
36 grain_type: grain_type.type_byte(),
37 ns_hash,
38 created_at_sec,
39 }
40 }
41
42 pub fn set_is_signed(&mut self, signed: bool) {
44 if signed {
45 self.flags |= 0x01;
46 } else {
47 self.flags &= !0x01;
48 }
49 }
50
51 pub fn set_has_content_refs(&mut self, has: bool) {
53 if has {
54 self.flags |= 0x08; } else {
56 self.flags &= !0x08;
57 }
58 }
59
60 pub fn set_has_embedding_refs(&mut self, has: bool) {
62 if has {
63 self.flags |= 0x10; } else {
65 self.flags &= !0x10;
66 }
67 }
68
69 pub fn set_ai_generated(&mut self, is_ai: bool) {
71 if is_ai {
72 self.flags |= 0x20; } else {
74 self.flags &= !0x20;
75 }
76 }
77
78 pub fn set_sensitivity(&mut self, level: u8) {
80 self.flags = (self.flags & 0x3F) | ((level & 0x03) << 6);
81 }
82
83 pub fn to_bytes(&self) -> [u8; 9] {
85 let ns_bytes = self.ns_hash.to_be_bytes();
86 let ts_bytes = self.created_at_sec.to_be_bytes();
87 [
88 self.version,
89 self.flags,
90 self.grain_type,
91 ns_bytes[0],
92 ns_bytes[1],
93 ts_bytes[0],
94 ts_bytes[1],
95 ts_bytes[2],
96 ts_bytes[3],
97 ]
98 }
99
100 pub fn from_bytes(bytes: &[u8]) -> Result<Self> {
102 if bytes.len() < 9 {
103 return Err(DejaDbError::Format("header must be at least 9 bytes".into()));
104 }
105 if bytes[0] != 0x01 {
106 return Err(DejaDbError::Format(format!(
107 "unsupported version: {:#x}",
108 bytes[0]
109 )));
110 }
111 Ok(MgHeader {
112 version: bytes[0],
113 flags: bytes[1],
114 grain_type: bytes[2],
115 ns_hash: u16::from_be_bytes([bytes[3], bytes[4]]),
116 created_at_sec: u32::from_be_bytes([bytes[5], bytes[6], bytes[7], bytes[8]]),
117 })
118 }
119
120 pub fn is_signed(&self) -> bool {
121 self.flags & 0x01 != 0
122 }
123
124 pub fn is_encrypted(&self) -> bool {
125 self.flags & 0x02 != 0
126 }
127
128 pub fn is_compressed(&self) -> bool {
129 self.flags & 0x04 != 0
130 }
131
132 pub fn has_content_refs(&self) -> bool {
133 self.flags & 0x08 != 0
134 }
135
136 pub fn has_embedding_refs(&self) -> bool {
137 self.flags & 0x10 != 0
138 }
139
140 pub fn is_ai_generated(&self) -> bool {
141 self.flags & 0x20 != 0
142 }
143
144 pub fn sensitivity(&self) -> u8 {
145 (self.flags >> 6) & 0x03
146 }
147}
148
149pub fn content_address(blob: &[u8]) -> Hash {
151 let digest: [u8; 32] = Sha256::digest(blob).into();
152 Hash::from_bytes(&digest)
153}
154
155#[cfg(test)]
156mod tests {
157 use super::*;
158
159 #[test]
160 fn test_header_roundtrip() {
161 let header = MgHeader::new(GrainType::Fact, Some("shared"), 1768471200000);
162 let bytes = header.to_bytes();
163 let parsed = MgHeader::from_bytes(&bytes).unwrap();
164 assert_eq!(parsed.version, 0x01);
165 assert_eq!(parsed.flags, 0x00);
166 assert_eq!(parsed.grain_type, 0x01);
167 assert_eq!(parsed.ns_hash, header.ns_hash);
168 assert_eq!(parsed.created_at_sec, 1768471200);
169 }
170
171 #[test]
172 fn test_namespace_hash_shared() {
173 let header = MgHeader::new(GrainType::Fact, Some("shared"), 1768471200000);
175 assert_eq!(header.ns_hash, 0xa4d2);
176 }
177
178 #[test]
179 fn test_header_bytes_vector1() {
180 let header = MgHeader::new(GrainType::Fact, Some("shared"), 1768471200000);
182 let bytes = header.to_bytes();
183 assert_eq!(
184 bytes,
185 [0x01, 0x00, 0x01, 0xa4, 0xd2, 0x69, 0x68, 0xba, 0xa0]
186 );
187 }
188}