1use crate::error::{Error, Result};
15use crate::limits::Limits;
16use crate::store::NodeId;
17
18pub const NODE_MAGIC: u8 = 0xB1;
20pub const MAX_NODE_BYTES: usize = 64 * 1024;
22pub const MAX_NODE_DEPS: usize = 256;
24pub const MATERIALIZER_VERSION: u16 = 1;
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29#[repr(u8)]
30pub enum NodeKind {
31 DocumentExact = 0x01,
33 SourceSlice = 0x02,
35 PdfRevision = 0x03,
37 PdfObject = 0x04,
39 PdfStreamEncoded = 0x05,
41 PdfStreamDecoded = 0x06,
43 ContentOperators = 0x07,
45 TextRuns = 0x08,
47 PageContent = 0x09,
49 PagePreview = 0x0A,
51 ResourceRef = 0x0B,
53 Concat = 0x0C,
55 Literal = 0x0D,
57 PackageRoot = 0x0E,
60 PackageMemberRaw = 0x0F,
63 PackageMemberDecoded = 0x10,
66 PackageOpcModel = 0x11,
70 DocxModel = 0x12,
74 DocxStory = 0x13,
77 EpubModel = 0x14,
82 EpubContent = 0x15,
86 ResourceBlob = 0x16,
92 OdtModel = 0x17,
97 OdtContent = 0x18,
102}
103
104impl NodeKind {
105 pub const fn from_u8(b: u8) -> Option<NodeKind> {
107 Some(match b {
108 0x01 => NodeKind::DocumentExact,
109 0x02 => NodeKind::SourceSlice,
110 0x03 => NodeKind::PdfRevision,
111 0x04 => NodeKind::PdfObject,
112 0x05 => NodeKind::PdfStreamEncoded,
113 0x06 => NodeKind::PdfStreamDecoded,
114 0x07 => NodeKind::ContentOperators,
115 0x08 => NodeKind::TextRuns,
116 0x09 => NodeKind::PageContent,
117 0x0A => NodeKind::PagePreview,
118 0x0B => NodeKind::ResourceRef,
119 0x0C => NodeKind::Concat,
120 0x0D => NodeKind::Literal,
121 0x0E => NodeKind::PackageRoot,
122 0x0F => NodeKind::PackageMemberRaw,
123 0x10 => NodeKind::PackageMemberDecoded,
124 0x11 => NodeKind::PackageOpcModel,
125 0x12 => NodeKind::DocxModel,
126 0x13 => NodeKind::DocxStory,
127 0x14 => NodeKind::EpubModel,
128 0x15 => NodeKind::EpubContent,
129 0x16 => NodeKind::ResourceBlob,
130 0x17 => NodeKind::OdtModel,
131 0x18 => NodeKind::OdtContent,
132 _ => return None,
133 })
134 }
135
136 pub const fn name(self) -> &'static str {
138 match self {
139 NodeKind::DocumentExact => "DocumentExact",
140 NodeKind::SourceSlice => "SourceSlice",
141 NodeKind::PdfRevision => "PdfRevision",
142 NodeKind::PdfObject => "PdfObject",
143 NodeKind::PdfStreamEncoded => "PdfStreamEncoded",
144 NodeKind::PdfStreamDecoded => "PdfStreamDecoded",
145 NodeKind::ContentOperators => "ContentOperators",
146 NodeKind::TextRuns => "TextRuns",
147 NodeKind::PageContent => "PageContent",
148 NodeKind::PagePreview => "PagePreview",
149 NodeKind::ResourceRef => "ResourceRef",
150 NodeKind::Concat => "Concat",
151 NodeKind::Literal => "Literal",
152 NodeKind::PackageRoot => "PackageRoot",
153 NodeKind::PackageMemberRaw => "PackageMemberRaw",
154 NodeKind::PackageMemberDecoded => "PackageMemberDecoded",
155 NodeKind::PackageOpcModel => "PackageOpcModel",
156 NodeKind::DocxModel => "DocxModel",
157 NodeKind::DocxStory => "DocxStory",
158 NodeKind::EpubModel => "EpubModel",
159 NodeKind::EpubContent => "EpubContent",
160 NodeKind::ResourceBlob => "ResourceBlob",
161 NodeKind::OdtModel => "OdtModel",
162 NodeKind::OdtContent => "OdtContent",
163 }
164 }
165
166 pub const fn is_exact(self) -> bool {
171 matches!(
172 self,
173 NodeKind::DocumentExact
174 | NodeKind::SourceSlice
175 | NodeKind::PdfRevision
176 | NodeKind::PdfObject
177 | NodeKind::PdfStreamEncoded
178 | NodeKind::ResourceRef
179 | NodeKind::Concat
180 | NodeKind::Literal
181 | NodeKind::PackageRoot
184 | NodeKind::PackageMemberRaw
185 | NodeKind::ResourceBlob
187 )
188 }
189}
190
191#[derive(Debug, Clone, Copy, PartialEq, Eq)]
193pub struct NodeLimits {
194 pub max_output_bytes: u64,
196 pub max_depth: u16,
198 pub max_fanout: u16,
200}
201
202impl NodeLimits {
203 pub const DEFAULT: NodeLimits = NodeLimits {
205 max_output_bytes: 1 << 31,
206 max_depth: 64,
207 max_fanout: MAX_NODE_DEPS as u16,
208 };
209}
210
211#[derive(Debug, Clone, PartialEq, Eq)]
213pub struct SeedNode {
214 pub kind: NodeKind,
216 pub materializer_id: u16,
218 pub materializer_version: u16,
220 pub logical_output_len: u64,
222 pub params: Vec<u8>,
224 pub deps: Vec<NodeId>,
226 pub provenance: String,
228 pub limits: NodeLimits,
230}
231
232impl SeedNode {
233 pub fn new(
235 kind: NodeKind,
236 logical_output_len: u64,
237 params: Vec<u8>,
238 deps: Vec<NodeId>,
239 provenance: impl Into<String>,
240 ) -> Self {
241 SeedNode {
242 kind,
243 materializer_id: kind as u16,
244 materializer_version: MATERIALIZER_VERSION,
245 logical_output_len,
246 params,
247 deps,
248 provenance: provenance.into(),
249 limits: NodeLimits::DEFAULT,
250 }
251 }
252
253 pub fn encode_canonical(&self) -> Vec<u8> {
255 let mut out = Vec::with_capacity(64 + self.params.len());
256 out.push(NODE_MAGIC);
257 out.push(crate::store::SEED_FORMAT_VERSION);
258 out.push(self.kind as u8);
259 out.push(0); out.extend_from_slice(&self.materializer_id.to_le_bytes());
261 out.extend_from_slice(&self.materializer_version.to_le_bytes());
262 out.extend_from_slice(&self.logical_output_len.to_le_bytes());
263 out.extend_from_slice(&(self.deps.len() as u32).to_le_bytes());
264 out.extend_from_slice(&(self.params.len() as u32).to_le_bytes());
265 out.extend_from_slice(&self.limits.max_output_bytes.to_le_bytes());
266 out.extend_from_slice(&self.limits.max_depth.to_le_bytes());
267 out.extend_from_slice(&self.limits.max_fanout.to_le_bytes());
268 for dep in &self.deps {
269 out.extend_from_slice(dep.as_bytes());
270 }
271 out.extend_from_slice(&self.params);
272 let prov = self.provenance.as_bytes();
273 out.extend_from_slice(&(prov.len() as u32).to_le_bytes());
274 out.extend_from_slice(prov);
275 out
276 }
277
278 pub fn decode_canonical(bytes: &[u8]) -> Result<SeedNode> {
280 let mut r = Reader::new(bytes);
281 if r.u8()? != NODE_MAGIC {
282 return Err(Error::unsupported_version("seed node: bad magic"));
283 }
284 let version = r.u8()?;
285 if version != crate::store::SEED_FORMAT_VERSION {
286 return Err(Error::unsupported_version(format!(
287 "seed node format version {version} is not supported"
288 )));
289 }
290 let kind_byte = r.u8()?;
291 let kind = NodeKind::from_u8(kind_byte)
292 .ok_or_else(|| Error::unsupported_version(format!("unknown node kind {kind_byte}")))?;
293 let _reserved = r.u8()?;
294 let materializer_id = r.u16()?;
295 let materializer_version = r.u16()?;
296 let logical_output_len = r.u64()?;
297 let dep_count = r.u32()? as usize;
298 let param_len = r.u32()? as usize;
299 let max_output_bytes = r.u64()?;
300 let max_depth = r.u16()?;
301 let max_fanout = r.u16()?;
302 if dep_count > MAX_NODE_DEPS {
303 return Err(Error::resource_limit(format!(
304 "seed node declares {dep_count} deps (max {MAX_NODE_DEPS})"
305 )));
306 }
307 let mut deps = Vec::with_capacity(dep_count);
308 for _ in 0..dep_count {
309 let mut b = [0u8; 32];
310 b.copy_from_slice(r.bytes(32)?);
311 deps.push(NodeId::from_bytes(b));
312 }
313 let params = r.bytes(param_len)?.to_vec();
314 let prov_len = r.u32()? as usize;
315 let prov = r.bytes(prov_len)?;
316 let provenance = core::str::from_utf8(prov)
317 .map_err(|_| Error::usage("seed node provenance is not UTF-8"))?
318 .to_string();
319 if !r.at_end() {
320 return Err(Error::usage("seed node has trailing bytes"));
321 }
322 if materializer_id != kind as u16 {
323 return Err(Error::unsupported_version(
324 "seed node materializer id does not match its kind",
325 ));
326 }
327 Ok(SeedNode {
328 kind,
329 materializer_id,
330 materializer_version,
331 logical_output_len,
332 params,
333 deps,
334 provenance,
335 limits: NodeLimits {
336 max_output_bytes,
337 max_depth,
338 max_fanout,
339 },
340 })
341 }
342
343 pub fn content_id(&self) -> NodeId {
345 NodeId::of_node(&self.encode_canonical())
346 }
347
348 pub fn check_limits(&self, limits: &Limits) -> Result<()> {
350 if self.logical_output_len > self.limits.max_output_bytes {
351 return Err(Error::resource_limit(format!(
352 "seed node declares output {} > its own cap {}",
353 self.logical_output_len, self.limits.max_output_bytes
354 )));
355 }
356 let _ = limits;
357 Ok(())
358 }
359}
360
361struct Reader<'a> {
363 b: &'a [u8],
364 at: usize,
365}
366
367impl<'a> Reader<'a> {
368 fn new(b: &'a [u8]) -> Self {
369 Reader { b, at: 0 }
370 }
371 fn bytes(&mut self, n: usize) -> Result<&'a [u8]> {
372 let end = self
373 .at
374 .checked_add(n)
375 .ok_or_else(|| Error::usage("seed node read overflow"))?;
376 if end > self.b.len() {
377 return Err(Error::usage("truncated seed node"));
378 }
379 let s = &self.b[self.at..end];
380 self.at = end;
381 Ok(s)
382 }
383 fn u8(&mut self) -> Result<u8> {
384 Ok(self.bytes(1)?[0])
385 }
386 fn u16(&mut self) -> Result<u16> {
387 let b = self.bytes(2)?;
388 Ok(u16::from_le_bytes([b[0], b[1]]))
389 }
390 fn u32(&mut self) -> Result<u32> {
391 let b = self.bytes(4)?;
392 Ok(u32::from_le_bytes([b[0], b[1], b[2], b[3]]))
393 }
394 fn u64(&mut self) -> Result<u64> {
395 let b = self.bytes(8)?;
396 let mut a = [0u8; 8];
397 a.copy_from_slice(b);
398 Ok(u64::from_le_bytes(a))
399 }
400 fn at_end(&self) -> bool {
401 self.at == self.b.len()
402 }
403}
404
405pub fn span_params(offset: u64, len: u64) -> Vec<u8> {
407 let mut p = Vec::with_capacity(16);
408 p.extend_from_slice(&offset.to_le_bytes());
409 p.extend_from_slice(&len.to_le_bytes());
410 p
411}
412
413pub fn read_span_params(params: &[u8]) -> Result<(u64, u64)> {
415 if params.len() != 16 {
416 return Err(Error::usage("span params must be 16 bytes"));
417 }
418 let mut a = [0u8; 8];
419 a.copy_from_slice(¶ms[0..8]);
420 let offset = u64::from_le_bytes(a);
421 a.copy_from_slice(¶ms[8..16]);
422 let len = u64::from_le_bytes(a);
423 Ok((offset, len))
424}
425
426pub fn object_params(object: u32, generation: u16, extra: u64) -> Vec<u8> {
429 let mut p = Vec::with_capacity(16);
430 p.extend_from_slice(&object.to_le_bytes());
431 p.extend_from_slice(&generation.to_le_bytes());
432 p.extend_from_slice(&0u16.to_le_bytes());
433 p.extend_from_slice(&extra.to_le_bytes());
434 p
435}
436
437pub fn read_object_params(params: &[u8]) -> Result<(u32, u16, u64)> {
439 if params.len() != 16 {
440 return Err(Error::usage("object params must be 16 bytes"));
441 }
442 let object = u32::from_le_bytes([params[0], params[1], params[2], params[3]]);
443 let generation = u16::from_le_bytes([params[4], params[5]]);
444 let mut a = [0u8; 8];
445 a.copy_from_slice(¶ms[8..16]);
446 let extra = u64::from_le_bytes(a);
447 Ok((object, generation, extra))
448}
449
450pub fn u32_params(v: u32) -> Vec<u8> {
452 v.to_le_bytes().to_vec()
453}
454
455pub fn read_u32_params(params: &[u8]) -> Result<u32> {
457 if params.len() != 4 {
458 return Err(Error::usage("u32 params must be 4 bytes"));
459 }
460 Ok(u32::from_le_bytes([
461 params[0], params[1], params[2], params[3],
462 ]))
463}
464
465#[cfg(test)]
466mod tests {
467 use super::*;
468
469 #[test]
470 fn canonical_roundtrip_is_stable() {
471 let n = SeedNode::new(
472 NodeKind::PdfStreamDecoded,
473 4096,
474 span_params(100, 200),
475 vec![NodeId::from_bytes([7u8; 32])],
476 "pdf:stream-decoded",
477 );
478 let enc = n.encode_canonical();
479 let back = SeedNode::decode_canonical(&enc).unwrap();
480 assert_eq!(n, back);
481 assert_eq!(back.content_id(), n.content_id());
482 assert_eq!(n.encode_canonical(), enc);
483 }
484
485 #[test]
486 fn dependency_change_changes_id() {
487 let a = SeedNode::new(
488 NodeKind::Concat,
489 10,
490 vec![],
491 vec![NodeId::from_bytes([1u8; 32])],
492 "t",
493 );
494 let b = SeedNode::new(
495 NodeKind::Concat,
496 10,
497 vec![],
498 vec![NodeId::from_bytes([2u8; 32])],
499 "t",
500 );
501 assert_ne!(a.content_id(), b.content_id());
502 }
503
504 #[test]
505 fn unknown_kind_and_version_fail_closed() {
506 let mut n = SeedNode::new(NodeKind::Literal, 1, vec![9], vec![], "t").encode_canonical();
507 n[0] = 0x00;
508 assert!(SeedNode::decode_canonical(&n).is_err());
509 let mut n2 = SeedNode::new(NodeKind::Literal, 1, vec![9], vec![], "t").encode_canonical();
510 n2[1] = 0x7F;
511 assert!(SeedNode::decode_canonical(&n2).is_err());
512 }
513
514 #[test]
515 fn trailing_bytes_are_rejected() {
516 let mut enc = SeedNode::new(NodeKind::Literal, 1, vec![], vec![], "t").encode_canonical();
517 enc.push(0);
518 assert!(SeedNode::decode_canonical(&enc).is_err());
519 }
520
521 #[test]
522 fn param_helpers_roundtrip() {
523 let (o, l) = read_span_params(&span_params(5, 9)).unwrap();
524 assert_eq!((o, l), (5, 9));
525 let (ob, g, e) = read_object_params(&object_params(42, 3, 77)).unwrap();
526 assert_eq!((ob, g, e), (42, 3, 77));
527 assert_eq!(read_u32_params(&u32_params(1234)).unwrap(), 1234);
528 }
529}