vole_document/field/
manifest.rs1use crate::error::{Error, Result};
9use crate::integrity::to_hex;
10use crate::store::{Id, NodeId};
11
12pub const FIELD_ROOT_DOMAIN: &[u8] = b"VOLE:VFIELD:v1";
14pub const FIELD_FORMAT_VERSION: u8 = 1;
16pub const FIELD_MAGIC: &[u8; 8] = b"VOLDFLD1";
18
19pub const ABSENT_ROOT: [u8; 32] = [0u8; 32];
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
24pub struct FieldId([u8; 32]);
25
26impl FieldId {
27 pub const fn from_bytes(b: [u8; 32]) -> Self {
29 FieldId(b)
30 }
31 pub const fn as_bytes(&self) -> &[u8; 32] {
33 &self.0
34 }
35 pub fn of_manifest(canonical: &[u8]) -> Self {
37 let mut h = blake3::Hasher::new();
38 h.update(FIELD_ROOT_DOMAIN);
39 h.update(canonical);
40 FieldId(*h.finalize().as_bytes())
41 }
42 pub fn to_hex(&self) -> String {
44 to_hex(&self.0)
45 }
46 pub fn from_hex(s: &str) -> Result<Self> {
48 Id::from_hex(s).map(|id| FieldId(*id.as_bytes()))
49 }
50}
51
52impl core::fmt::Display for FieldId {
53 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
54 f.write_str(&self.to_hex())
55 }
56}
57
58#[derive(Debug, Clone, PartialEq, Eq)]
60pub struct FieldRoot {
61 pub universe_id: [u8; 16],
63 pub source_sha256: [u8; 32],
65 pub source_len: u64,
67 pub descriptor_id: [u8; 32],
69 pub root_node: NodeId,
71 pub index_root: [u8; 32],
73 pub node_count: u64,
75 pub index_node_count: u64,
77 pub provenance: String,
79}
80
81impl FieldRoot {
82 pub fn encode_canonical(&self) -> Vec<u8> {
84 let mut out = Vec::with_capacity(256);
85 out.extend_from_slice(FIELD_MAGIC);
86 out.push(FIELD_FORMAT_VERSION);
87 out.push(0); out.extend_from_slice(&self.universe_id);
89 out.extend_from_slice(&self.source_sha256);
90 out.extend_from_slice(&self.source_len.to_le_bytes());
91 out.extend_from_slice(&self.descriptor_id);
92 out.extend_from_slice(self.root_node.as_bytes());
93 out.extend_from_slice(&self.index_root);
94 out.extend_from_slice(&self.node_count.to_le_bytes());
95 out.extend_from_slice(&self.index_node_count.to_le_bytes());
96 let prov = self.provenance.as_bytes();
97 out.extend_from_slice(&(prov.len() as u32).to_le_bytes());
98 out.extend_from_slice(prov);
99 out
100 }
101
102 pub fn decode_canonical(bytes: &[u8]) -> Result<FieldRoot> {
104 if bytes.len() < 8 || &bytes[0..8] != FIELD_MAGIC {
105 return Err(Error::unsupported_version("field manifest: bad magic"));
106 }
107 const FIXED: usize = 8 + 1 + 1 + 16 + 32 + 8 + 32 + 32 + 32 + 8 + 8;
110 if bytes.len() < FIXED + 4 {
111 return Err(Error::usage("truncated field manifest"));
112 }
113 if bytes[8] != FIELD_FORMAT_VERSION {
114 return Err(Error::unsupported_version(format!(
115 "field manifest version {} is not supported",
116 bytes[8]
117 )));
118 }
119 let mut at = 10;
120 let mut universe_id = [0u8; 16];
121 universe_id.copy_from_slice(&bytes[at..at + 16]);
122 at += 16;
123 let mut source_sha256 = [0u8; 32];
124 source_sha256.copy_from_slice(&bytes[at..at + 32]);
125 at += 32;
126 let source_len = u64::from_le_bytes(bytes[at..at + 8].try_into().unwrap());
127 at += 8;
128 let mut descriptor_id = [0u8; 32];
129 descriptor_id.copy_from_slice(&bytes[at..at + 32]);
130 at += 32;
131 let mut root = [0u8; 32];
132 root.copy_from_slice(&bytes[at..at + 32]);
133 at += 32;
134 let mut index_root = [0u8; 32];
135 index_root.copy_from_slice(&bytes[at..at + 32]);
136 at += 32;
137 let node_count = u64::from_le_bytes(bytes[at..at + 8].try_into().unwrap());
138 at += 8;
139 let index_node_count = u64::from_le_bytes(bytes[at..at + 8].try_into().unwrap());
140 at += 8;
141 let prov_len = u32::from_le_bytes(bytes[at..at + 4].try_into().unwrap()) as usize;
142 at += 4;
143 let end = at
144 .checked_add(prov_len)
145 .ok_or_else(|| Error::usage("field manifest provenance overflow"))?;
146 if end != bytes.len() {
147 return Err(Error::usage(
148 "field manifest provenance length does not match the buffer",
149 ));
150 }
151 let provenance = core::str::from_utf8(&bytes[at..end])
152 .map_err(|_| Error::usage("field manifest provenance is not UTF-8"))?
153 .to_string();
154 Ok(FieldRoot {
155 universe_id,
156 source_sha256,
157 source_len,
158 descriptor_id,
159 root_node: NodeId::from_bytes(root),
160 index_root,
161 node_count,
162 index_node_count,
163 provenance,
164 })
165 }
166
167 pub fn content_id(&self) -> FieldId {
169 FieldId::of_manifest(&self.encode_canonical())
170 }
171
172 pub fn has_index(&self) -> bool {
174 self.index_root != ABSENT_ROOT
175 }
176}
177
178pub fn provenance_counter(provenance: &str, key: &str) -> u64 {
184 provenance
185 .split(';')
186 .find_map(|token| token.strip_prefix(key).and_then(|v| v.strip_prefix('=')))
187 .and_then(|v| v.parse().ok())
188 .unwrap_or(0)
189}
190
191#[cfg(test)]
192mod tests {
193 use super::*;
194 use crate::integrity::sha256;
195
196 fn sample() -> FieldRoot {
197 FieldRoot {
198 universe_id: [3u8; 16],
199 source_sha256: sha256(b"source"),
200 source_len: 6,
201 descriptor_id: [4u8; 32],
202 root_node: NodeId::from_bytes([5u8; 32]),
203 index_root: ABSENT_ROOT,
204 node_count: 12,
205 index_node_count: 0,
206 provenance: "pdf:test".into(),
207 }
208 }
209
210 #[test]
211 fn manifest_roundtrips_and_ids() {
212 let m = sample();
213 let enc = m.encode_canonical();
214 let back = FieldRoot::decode_canonical(&enc).unwrap();
215 assert_eq!(m, back);
216 assert_eq!(m.content_id(), back.content_id());
217 assert_eq!(m.encode_canonical(), enc);
218 assert!(!m.has_index());
219 }
220
221 #[test]
222 fn bad_magic_and_version_fail_closed() {
223 let mut enc = sample().encode_canonical();
224 enc[0] = b'X';
225 assert!(FieldRoot::decode_canonical(&enc).is_err());
226 let mut enc2 = sample().encode_canonical();
227 enc2[8] = 0x7F;
228 assert!(FieldRoot::decode_canonical(&enc2).is_err());
229 }
230
231 #[test]
232 fn truncation_and_trailing_fail_closed() {
233 let enc = sample().encode_canonical();
234 assert!(FieldRoot::decode_canonical(&enc[..enc.len() - 1]).is_err());
235 let mut longer = enc.clone();
236 longer.push(0);
237 assert!(FieldRoot::decode_canonical(&longer).is_err());
238 }
239}