1use std::collections::BTreeMap;
4
5use hyphae_query::Value;
6use thiserror::Error;
7
8const MAGIC: [u8; 8] = *b"HYDOC001";
9const DOCUMENT_FORMAT_VERSION: u16 = 1;
10const HEADER_LENGTH: usize = 56;
11const CHECKSUM_PREFIX_LENGTH: usize = 20;
12const DIGEST_PREFIX_LENGTH: usize = 24;
13const NULL: u8 = 0;
14const FALSE: u8 = 1;
15const TRUE: u8 = 2;
16const INTEGER: u8 = 3;
17const STRING: u8 = 4;
18const BYTES: u8 = 5;
19const ARRAY: u8 = 6;
20const OBJECT: u8 = 7;
21
22pub const MAX_DOCUMENT_BYTES: usize = 16 * 1024 * 1024;
24pub const MAX_DOCUMENT_DEPTH: usize = 64;
26pub const MAX_DOCUMENT_NODES: usize = 1_000_000;
28
29#[derive(Clone, Debug, Error, Eq, PartialEq)]
31pub enum DocumentError {
32 #[error("document payload is {actual} bytes; maximum is {maximum}")]
34 TooLarge {
35 actual: usize,
37 maximum: usize,
39 },
40
41 #[error("document depth exceeds maximum {maximum}")]
43 TooDeep {
44 maximum: usize,
46 },
47
48 #[error("document node count exceeds maximum {maximum}")]
50 TooManyNodes {
51 maximum: usize,
53 },
54
55 #[error("document length overflow")]
57 LengthOverflow,
58
59 #[error("invalid canonical document: {reason}")]
61 Invalid {
62 reason: &'static str,
64 },
65
66 #[error("unsupported document format {found}; supported format is {supported}")]
68 UnsupportedVersion {
69 found: u16,
71 supported: u16,
73 },
74
75 #[error("document CRC32C mismatch")]
77 ChecksumMismatch,
78
79 #[error("document BLAKE3 mismatch")]
81 DigestMismatch,
82
83 #[error("document contains invalid UTF-8")]
85 InvalidUtf8,
86}
87
88pub fn encode_document(value: &Value) -> Result<Vec<u8>, DocumentError> {
94 let mut encoder = Encoder {
95 payload: Vec::new(),
96 nodes: 0,
97 };
98 encoder.value(value, 0)?;
99 encode_envelope(&encoder.payload)
100}
101
102pub fn decode_document(encoded: &[u8]) -> Result<Value, DocumentError> {
109 if encoded.len() < HEADER_LENGTH {
110 return Err(DocumentError::Invalid {
111 reason: "truncated header",
112 });
113 }
114 if encoded[..8] != MAGIC {
115 return Err(DocumentError::Invalid {
116 reason: "bad magic",
117 });
118 }
119 let version = u16::from_le_bytes(copy_array(&encoded[8..10]));
120 if version != DOCUMENT_FORMAT_VERSION {
121 return Err(DocumentError::UnsupportedVersion {
122 found: version,
123 supported: DOCUMENT_FORMAT_VERSION,
124 });
125 }
126 if u16::from_le_bytes(copy_array(&encoded[10..12])) != 0 {
127 return Err(DocumentError::Invalid {
128 reason: "unsupported flags",
129 });
130 }
131 let payload_length = usize::try_from(u64::from_le_bytes(copy_array(&encoded[12..20])))
132 .map_err(|_| DocumentError::LengthOverflow)?;
133 if payload_length > MAX_DOCUMENT_BYTES {
134 return Err(DocumentError::TooLarge {
135 actual: payload_length,
136 maximum: MAX_DOCUMENT_BYTES,
137 });
138 }
139 let expected_length = HEADER_LENGTH
140 .checked_add(payload_length)
141 .ok_or(DocumentError::LengthOverflow)?;
142 if encoded.len() != expected_length {
143 return Err(DocumentError::Invalid {
144 reason: "file length mismatch",
145 });
146 }
147 let payload = &encoded[HEADER_LENGTH..];
148 let expected_checksum = u32::from_le_bytes(copy_array(&encoded[20..24]));
149 let actual_checksum =
150 crc32c::crc32c_append(crc32c::crc32c(&encoded[..CHECKSUM_PREFIX_LENGTH]), payload);
151 if actual_checksum != expected_checksum {
152 return Err(DocumentError::ChecksumMismatch);
153 }
154 let expected_digest: [u8; 32] = copy_array(&encoded[24..56]);
155 let mut hasher = blake3::Hasher::new();
156 hasher.update(&encoded[..DIGEST_PREFIX_LENGTH]);
157 hasher.update(payload);
158 if *hasher.finalize().as_bytes() != expected_digest {
159 return Err(DocumentError::DigestMismatch);
160 }
161
162 let mut decoder = Decoder {
163 payload,
164 position: 0,
165 nodes: 0,
166 };
167 let value = decoder.value(0)?;
168 if decoder.position != payload.len() {
169 return Err(DocumentError::Invalid {
170 reason: "trailing payload bytes",
171 });
172 }
173 Ok(value)
174}
175
176struct Encoder {
177 payload: Vec<u8>,
178 nodes: usize,
179}
180
181impl Encoder {
182 fn value(&mut self, value: &Value, depth: usize) -> Result<(), DocumentError> {
183 if depth > MAX_DOCUMENT_DEPTH {
184 return Err(DocumentError::TooDeep {
185 maximum: MAX_DOCUMENT_DEPTH,
186 });
187 }
188 self.nodes = self
189 .nodes
190 .checked_add(1)
191 .ok_or(DocumentError::TooManyNodes {
192 maximum: MAX_DOCUMENT_NODES,
193 })?;
194 if self.nodes > MAX_DOCUMENT_NODES {
195 return Err(DocumentError::TooManyNodes {
196 maximum: MAX_DOCUMENT_NODES,
197 });
198 }
199 match value {
200 Value::Null => self.append(&[NULL]),
201 Value::Boolean(false) => self.append(&[FALSE]),
202 Value::Boolean(true) => self.append(&[TRUE]),
203 Value::Integer(value) => {
204 self.append(&[INTEGER])?;
205 self.append(&value.to_le_bytes())
206 }
207 Value::String(value) => {
208 self.append(&[STRING])?;
209 self.length_prefixed(value.as_bytes())
210 }
211 Value::Bytes(value) => {
212 self.append(&[BYTES])?;
213 self.length_prefixed(value)
214 }
215 Value::Array(values) => {
216 self.append(&[ARRAY])?;
217 self.length(values.len())?;
218 let child_depth = depth.checked_add(1).ok_or(DocumentError::TooDeep {
219 maximum: MAX_DOCUMENT_DEPTH,
220 })?;
221 for value in values {
222 self.value(value, child_depth)?;
223 }
224 Ok(())
225 }
226 Value::Object(values) => {
227 self.append(&[OBJECT])?;
228 self.length(values.len())?;
229 let child_depth = depth.checked_add(1).ok_or(DocumentError::TooDeep {
230 maximum: MAX_DOCUMENT_DEPTH,
231 })?;
232 for (key, value) in values {
233 self.length_prefixed(key.as_bytes())?;
234 self.value(value, child_depth)?;
235 }
236 Ok(())
237 }
238 }
239 }
240
241 fn length_prefixed(&mut self, value: &[u8]) -> Result<(), DocumentError> {
242 self.length(value.len())?;
243 self.append(value)
244 }
245
246 fn length(&mut self, length: usize) -> Result<(), DocumentError> {
247 let encoded = u64::try_from(length).map_err(|_| DocumentError::LengthOverflow)?;
248 self.append(&encoded.to_le_bytes())
249 }
250
251 fn append(&mut self, bytes: &[u8]) -> Result<(), DocumentError> {
252 let next = self
253 .payload
254 .len()
255 .checked_add(bytes.len())
256 .ok_or(DocumentError::LengthOverflow)?;
257 if next > MAX_DOCUMENT_BYTES {
258 return Err(DocumentError::TooLarge {
259 actual: next,
260 maximum: MAX_DOCUMENT_BYTES,
261 });
262 }
263 self.payload.extend_from_slice(bytes);
264 Ok(())
265 }
266}
267
268struct Decoder<'payload> {
269 payload: &'payload [u8],
270 position: usize,
271 nodes: usize,
272}
273
274impl Decoder<'_> {
275 fn value(&mut self, depth: usize) -> Result<Value, DocumentError> {
276 if depth > MAX_DOCUMENT_DEPTH {
277 return Err(DocumentError::TooDeep {
278 maximum: MAX_DOCUMENT_DEPTH,
279 });
280 }
281 self.nodes = self
282 .nodes
283 .checked_add(1)
284 .ok_or(DocumentError::TooManyNodes {
285 maximum: MAX_DOCUMENT_NODES,
286 })?;
287 if self.nodes > MAX_DOCUMENT_NODES {
288 return Err(DocumentError::TooManyNodes {
289 maximum: MAX_DOCUMENT_NODES,
290 });
291 }
292 let tag = self.read(1)?[0];
293 match tag {
294 NULL => Ok(Value::Null),
295 FALSE => Ok(Value::Boolean(false)),
296 TRUE => Ok(Value::Boolean(true)),
297 INTEGER => Ok(Value::Integer(i64::from_le_bytes(copy_array(
298 self.read(8)?,
299 )))),
300 STRING => {
301 let bytes = self.length_prefixed()?;
302 let value = std::str::from_utf8(bytes).map_err(|_| DocumentError::InvalidUtf8)?;
303 Ok(Value::String(value.to_owned()))
304 }
305 BYTES => Ok(Value::Bytes(self.length_prefixed()?.to_vec())),
306 ARRAY => self.array(depth),
307 OBJECT => self.object(depth),
308 _ => Err(DocumentError::Invalid {
309 reason: "unknown value tag",
310 }),
311 }
312 }
313
314 fn array(&mut self, depth: usize) -> Result<Value, DocumentError> {
315 let count = self.length()?;
316 if count > MAX_DOCUMENT_NODES {
317 return Err(DocumentError::TooManyNodes {
318 maximum: MAX_DOCUMENT_NODES,
319 });
320 }
321 let child_depth = depth.checked_add(1).ok_or(DocumentError::TooDeep {
322 maximum: MAX_DOCUMENT_DEPTH,
323 })?;
324 let mut values = Vec::with_capacity(count);
325 for _ in 0..count {
326 values.push(self.value(child_depth)?);
327 }
328 Ok(Value::Array(values))
329 }
330
331 fn object(&mut self, depth: usize) -> Result<Value, DocumentError> {
332 let count = self.length()?;
333 if count > MAX_DOCUMENT_NODES {
334 return Err(DocumentError::TooManyNodes {
335 maximum: MAX_DOCUMENT_NODES,
336 });
337 }
338 let child_depth = depth.checked_add(1).ok_or(DocumentError::TooDeep {
339 maximum: MAX_DOCUMENT_DEPTH,
340 })?;
341 let mut values = BTreeMap::new();
342 let mut previous: Option<String> = None;
343 for _ in 0..count {
344 let bytes = self.length_prefixed()?;
345 let key = std::str::from_utf8(bytes)
346 .map_err(|_| DocumentError::InvalidUtf8)?
347 .to_owned();
348 if previous.as_ref().is_some_and(|previous| previous >= &key) {
349 return Err(DocumentError::Invalid {
350 reason: "object keys are not strictly sorted",
351 });
352 }
353 let value = self.value(child_depth)?;
354 previous = Some(key.clone());
355 values.insert(key, value);
356 }
357 Ok(Value::Object(values))
358 }
359
360 fn length_prefixed(&mut self) -> Result<&[u8], DocumentError> {
361 let length = self.length()?;
362 self.read(length)
363 }
364
365 fn length(&mut self) -> Result<usize, DocumentError> {
366 usize::try_from(u64::from_le_bytes(copy_array(self.read(8)?)))
367 .map_err(|_| DocumentError::LengthOverflow)
368 }
369
370 fn read(&mut self, length: usize) -> Result<&[u8], DocumentError> {
371 let end = self
372 .position
373 .checked_add(length)
374 .ok_or(DocumentError::LengthOverflow)?;
375 let Some(value) = self.payload.get(self.position..end) else {
376 return Err(DocumentError::Invalid {
377 reason: "truncated value payload",
378 });
379 };
380 self.position = end;
381 Ok(value)
382 }
383}
384
385fn encode_envelope(payload: &[u8]) -> Result<Vec<u8>, DocumentError> {
386 if payload.len() > MAX_DOCUMENT_BYTES {
387 return Err(DocumentError::TooLarge {
388 actual: payload.len(),
389 maximum: MAX_DOCUMENT_BYTES,
390 });
391 }
392 let payload_length = u64::try_from(payload.len()).map_err(|_| DocumentError::LengthOverflow)?;
393 let capacity = HEADER_LENGTH
394 .checked_add(payload.len())
395 .ok_or(DocumentError::LengthOverflow)?;
396 let mut encoded = vec![0_u8; capacity];
397 encoded[..8].copy_from_slice(&MAGIC);
398 encoded[8..10].copy_from_slice(&DOCUMENT_FORMAT_VERSION.to_le_bytes());
399 encoded[10..12].copy_from_slice(&0_u16.to_le_bytes());
400 encoded[12..20].copy_from_slice(&payload_length.to_le_bytes());
401 encoded[HEADER_LENGTH..].copy_from_slice(payload);
402 let checksum =
403 crc32c::crc32c_append(crc32c::crc32c(&encoded[..CHECKSUM_PREFIX_LENGTH]), payload);
404 encoded[20..24].copy_from_slice(&checksum.to_le_bytes());
405 let mut hasher = blake3::Hasher::new();
406 hasher.update(&encoded[..DIGEST_PREFIX_LENGTH]);
407 hasher.update(payload);
408 encoded[24..56].copy_from_slice(hasher.finalize().as_bytes());
409 Ok(encoded)
410}
411
412fn copy_array<const N: usize>(source: &[u8]) -> [u8; N] {
413 let mut output = [0_u8; N];
414 output.copy_from_slice(source);
415 output
416}
417
418#[cfg(test)]
419mod tests {
420 use std::collections::BTreeMap;
421
422 use hyphae_query::Value;
423
424 use super::{
425 DOCUMENT_FORMAT_VERSION, DocumentError, MAX_DOCUMENT_DEPTH, NULL, OBJECT, decode_document,
426 encode_document, encode_envelope,
427 };
428
429 #[test]
430 fn canonical_document_round_trips_binary_and_nested_values() -> Result<(), DocumentError> {
431 let value = Value::Object(BTreeMap::from([
432 ("bytes".to_owned(), Value::Bytes(vec![0, 255, 7])),
433 (
434 "nested".to_owned(),
435 Value::Array(vec![Value::Integer(-7), Value::Null]),
436 ),
437 ]));
438 let first = encode_document(&value)?;
439 let second = encode_document(&value)?;
440 assert_eq!(first, second);
441 assert_eq!(decode_document(&first)?, value);
442 Ok(())
443 }
444
445 #[test]
446 fn corruption_and_future_versions_fail_before_decoding() -> Result<(), DocumentError> {
447 let mut corrupted = encode_document(&Value::Integer(7))?;
448 let last = corrupted.len() - 1;
449 corrupted[last] ^= 1;
450 assert_eq!(
451 decode_document(&corrupted),
452 Err(DocumentError::ChecksumMismatch)
453 );
454
455 let mut future = encode_document(&Value::Null)?;
456 future[8..10].copy_from_slice(&(DOCUMENT_FORMAT_VERSION + 1).to_le_bytes());
457 assert_eq!(
458 decode_document(&future),
459 Err(DocumentError::UnsupportedVersion {
460 found: DOCUMENT_FORMAT_VERSION + 1,
461 supported: DOCUMENT_FORMAT_VERSION
462 })
463 );
464 Ok(())
465 }
466
467 #[test]
468 fn depth_is_bounded_during_encoding() {
469 let mut value = Value::Null;
470 for _ in 0..=MAX_DOCUMENT_DEPTH {
471 value = Value::Array(vec![value]);
472 }
473 assert_eq!(
474 encode_document(&value),
475 Err(DocumentError::TooDeep {
476 maximum: MAX_DOCUMENT_DEPTH
477 })
478 );
479 }
480
481 #[test]
482 fn decoder_rejects_noncanonical_object_key_order() -> Result<(), DocumentError> {
483 let mut payload = vec![OBJECT];
484 payload.extend_from_slice(&2_u64.to_le_bytes());
485 payload.extend_from_slice(&1_u64.to_le_bytes());
486 payload.extend_from_slice(b"b");
487 payload.push(NULL);
488 payload.extend_from_slice(&1_u64.to_le_bytes());
489 payload.extend_from_slice(b"a");
490 payload.push(NULL);
491 let encoded = encode_envelope(&payload)?;
492 assert_eq!(
493 decode_document(&encoded),
494 Err(DocumentError::Invalid {
495 reason: "object keys are not strictly sorted"
496 })
497 );
498 Ok(())
499 }
500}