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