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}
58
59impl NodeKind {
60 pub const fn from_u8(b: u8) -> Option<NodeKind> {
62 Some(match b {
63 0x01 => NodeKind::DocumentExact,
64 0x02 => NodeKind::SourceSlice,
65 0x03 => NodeKind::PdfRevision,
66 0x04 => NodeKind::PdfObject,
67 0x05 => NodeKind::PdfStreamEncoded,
68 0x06 => NodeKind::PdfStreamDecoded,
69 0x07 => NodeKind::ContentOperators,
70 0x08 => NodeKind::TextRuns,
71 0x09 => NodeKind::PageContent,
72 0x0A => NodeKind::PagePreview,
73 0x0B => NodeKind::ResourceRef,
74 0x0C => NodeKind::Concat,
75 0x0D => NodeKind::Literal,
76 _ => return None,
77 })
78 }
79
80 pub const fn name(self) -> &'static str {
82 match self {
83 NodeKind::DocumentExact => "DocumentExact",
84 NodeKind::SourceSlice => "SourceSlice",
85 NodeKind::PdfRevision => "PdfRevision",
86 NodeKind::PdfObject => "PdfObject",
87 NodeKind::PdfStreamEncoded => "PdfStreamEncoded",
88 NodeKind::PdfStreamDecoded => "PdfStreamDecoded",
89 NodeKind::ContentOperators => "ContentOperators",
90 NodeKind::TextRuns => "TextRuns",
91 NodeKind::PageContent => "PageContent",
92 NodeKind::PagePreview => "PagePreview",
93 NodeKind::ResourceRef => "ResourceRef",
94 NodeKind::Concat => "Concat",
95 NodeKind::Literal => "Literal",
96 }
97 }
98
99 pub const fn is_exact(self) -> bool {
104 matches!(
105 self,
106 NodeKind::DocumentExact
107 | NodeKind::SourceSlice
108 | NodeKind::PdfRevision
109 | NodeKind::PdfObject
110 | NodeKind::PdfStreamEncoded
111 | NodeKind::ResourceRef
112 | NodeKind::Concat
113 | NodeKind::Literal
114 )
115 }
116}
117
118#[derive(Debug, Clone, Copy, PartialEq, Eq)]
120pub struct NodeLimits {
121 pub max_output_bytes: u64,
123 pub max_depth: u16,
125 pub max_fanout: u16,
127}
128
129impl NodeLimits {
130 pub const DEFAULT: NodeLimits = NodeLimits {
132 max_output_bytes: 1 << 31,
133 max_depth: 64,
134 max_fanout: MAX_NODE_DEPS as u16,
135 };
136}
137
138#[derive(Debug, Clone, PartialEq, Eq)]
140pub struct SeedNode {
141 pub kind: NodeKind,
143 pub materializer_id: u16,
145 pub materializer_version: u16,
147 pub logical_output_len: u64,
149 pub params: Vec<u8>,
151 pub deps: Vec<NodeId>,
153 pub provenance: String,
155 pub limits: NodeLimits,
157}
158
159impl SeedNode {
160 pub fn new(
162 kind: NodeKind,
163 logical_output_len: u64,
164 params: Vec<u8>,
165 deps: Vec<NodeId>,
166 provenance: impl Into<String>,
167 ) -> Self {
168 SeedNode {
169 kind,
170 materializer_id: kind as u16,
171 materializer_version: MATERIALIZER_VERSION,
172 logical_output_len,
173 params,
174 deps,
175 provenance: provenance.into(),
176 limits: NodeLimits::DEFAULT,
177 }
178 }
179
180 pub fn encode_canonical(&self) -> Vec<u8> {
182 let mut out = Vec::with_capacity(64 + self.params.len());
183 out.push(NODE_MAGIC);
184 out.push(crate::store::SEED_FORMAT_VERSION);
185 out.push(self.kind as u8);
186 out.push(0); out.extend_from_slice(&self.materializer_id.to_le_bytes());
188 out.extend_from_slice(&self.materializer_version.to_le_bytes());
189 out.extend_from_slice(&self.logical_output_len.to_le_bytes());
190 out.extend_from_slice(&(self.deps.len() as u32).to_le_bytes());
191 out.extend_from_slice(&(self.params.len() as u32).to_le_bytes());
192 out.extend_from_slice(&self.limits.max_output_bytes.to_le_bytes());
193 out.extend_from_slice(&self.limits.max_depth.to_le_bytes());
194 out.extend_from_slice(&self.limits.max_fanout.to_le_bytes());
195 for dep in &self.deps {
196 out.extend_from_slice(dep.as_bytes());
197 }
198 out.extend_from_slice(&self.params);
199 let prov = self.provenance.as_bytes();
200 out.extend_from_slice(&(prov.len() as u32).to_le_bytes());
201 out.extend_from_slice(prov);
202 out
203 }
204
205 pub fn decode_canonical(bytes: &[u8]) -> Result<SeedNode> {
207 let mut r = Reader::new(bytes);
208 if r.u8()? != NODE_MAGIC {
209 return Err(Error::unsupported_version("seed node: bad magic"));
210 }
211 let version = r.u8()?;
212 if version != crate::store::SEED_FORMAT_VERSION {
213 return Err(Error::unsupported_version(format!(
214 "seed node format version {version} is not supported"
215 )));
216 }
217 let kind_byte = r.u8()?;
218 let kind = NodeKind::from_u8(kind_byte)
219 .ok_or_else(|| Error::unsupported_version(format!("unknown node kind {kind_byte}")))?;
220 let _reserved = r.u8()?;
221 let materializer_id = r.u16()?;
222 let materializer_version = r.u16()?;
223 let logical_output_len = r.u64()?;
224 let dep_count = r.u32()? as usize;
225 let param_len = r.u32()? as usize;
226 let max_output_bytes = r.u64()?;
227 let max_depth = r.u16()?;
228 let max_fanout = r.u16()?;
229 if dep_count > MAX_NODE_DEPS {
230 return Err(Error::resource_limit(format!(
231 "seed node declares {dep_count} deps (max {MAX_NODE_DEPS})"
232 )));
233 }
234 let mut deps = Vec::with_capacity(dep_count);
235 for _ in 0..dep_count {
236 let mut b = [0u8; 32];
237 b.copy_from_slice(r.bytes(32)?);
238 deps.push(NodeId::from_bytes(b));
239 }
240 let params = r.bytes(param_len)?.to_vec();
241 let prov_len = r.u32()? as usize;
242 let prov = r.bytes(prov_len)?;
243 let provenance = core::str::from_utf8(prov)
244 .map_err(|_| Error::usage("seed node provenance is not UTF-8"))?
245 .to_string();
246 if !r.at_end() {
247 return Err(Error::usage("seed node has trailing bytes"));
248 }
249 if materializer_id != kind as u16 {
250 return Err(Error::unsupported_version(
251 "seed node materializer id does not match its kind",
252 ));
253 }
254 Ok(SeedNode {
255 kind,
256 materializer_id,
257 materializer_version,
258 logical_output_len,
259 params,
260 deps,
261 provenance,
262 limits: NodeLimits {
263 max_output_bytes,
264 max_depth,
265 max_fanout,
266 },
267 })
268 }
269
270 pub fn content_id(&self) -> NodeId {
272 NodeId::of_node(&self.encode_canonical())
273 }
274
275 pub fn check_limits(&self, limits: &Limits) -> Result<()> {
277 if self.logical_output_len > self.limits.max_output_bytes {
278 return Err(Error::resource_limit(format!(
279 "seed node declares output {} > its own cap {}",
280 self.logical_output_len, self.limits.max_output_bytes
281 )));
282 }
283 let _ = limits;
284 Ok(())
285 }
286}
287
288struct Reader<'a> {
290 b: &'a [u8],
291 at: usize,
292}
293
294impl<'a> Reader<'a> {
295 fn new(b: &'a [u8]) -> Self {
296 Reader { b, at: 0 }
297 }
298 fn bytes(&mut self, n: usize) -> Result<&'a [u8]> {
299 let end = self
300 .at
301 .checked_add(n)
302 .ok_or_else(|| Error::usage("seed node read overflow"))?;
303 if end > self.b.len() {
304 return Err(Error::usage("truncated seed node"));
305 }
306 let s = &self.b[self.at..end];
307 self.at = end;
308 Ok(s)
309 }
310 fn u8(&mut self) -> Result<u8> {
311 Ok(self.bytes(1)?[0])
312 }
313 fn u16(&mut self) -> Result<u16> {
314 let b = self.bytes(2)?;
315 Ok(u16::from_le_bytes([b[0], b[1]]))
316 }
317 fn u32(&mut self) -> Result<u32> {
318 let b = self.bytes(4)?;
319 Ok(u32::from_le_bytes([b[0], b[1], b[2], b[3]]))
320 }
321 fn u64(&mut self) -> Result<u64> {
322 let b = self.bytes(8)?;
323 let mut a = [0u8; 8];
324 a.copy_from_slice(b);
325 Ok(u64::from_le_bytes(a))
326 }
327 fn at_end(&self) -> bool {
328 self.at == self.b.len()
329 }
330}
331
332pub fn span_params(offset: u64, len: u64) -> Vec<u8> {
334 let mut p = Vec::with_capacity(16);
335 p.extend_from_slice(&offset.to_le_bytes());
336 p.extend_from_slice(&len.to_le_bytes());
337 p
338}
339
340pub fn read_span_params(params: &[u8]) -> Result<(u64, u64)> {
342 if params.len() != 16 {
343 return Err(Error::usage("span params must be 16 bytes"));
344 }
345 let mut a = [0u8; 8];
346 a.copy_from_slice(¶ms[0..8]);
347 let offset = u64::from_le_bytes(a);
348 a.copy_from_slice(¶ms[8..16]);
349 let len = u64::from_le_bytes(a);
350 Ok((offset, len))
351}
352
353pub fn object_params(object: u32, generation: u16, extra: u64) -> Vec<u8> {
356 let mut p = Vec::with_capacity(16);
357 p.extend_from_slice(&object.to_le_bytes());
358 p.extend_from_slice(&generation.to_le_bytes());
359 p.extend_from_slice(&0u16.to_le_bytes());
360 p.extend_from_slice(&extra.to_le_bytes());
361 p
362}
363
364pub fn read_object_params(params: &[u8]) -> Result<(u32, u16, u64)> {
366 if params.len() != 16 {
367 return Err(Error::usage("object params must be 16 bytes"));
368 }
369 let object = u32::from_le_bytes([params[0], params[1], params[2], params[3]]);
370 let generation = u16::from_le_bytes([params[4], params[5]]);
371 let mut a = [0u8; 8];
372 a.copy_from_slice(¶ms[8..16]);
373 let extra = u64::from_le_bytes(a);
374 Ok((object, generation, extra))
375}
376
377pub fn u32_params(v: u32) -> Vec<u8> {
379 v.to_le_bytes().to_vec()
380}
381
382pub fn read_u32_params(params: &[u8]) -> Result<u32> {
384 if params.len() != 4 {
385 return Err(Error::usage("u32 params must be 4 bytes"));
386 }
387 Ok(u32::from_le_bytes([
388 params[0], params[1], params[2], params[3],
389 ]))
390}
391
392#[cfg(test)]
393mod tests {
394 use super::*;
395
396 #[test]
397 fn canonical_roundtrip_is_stable() {
398 let n = SeedNode::new(
399 NodeKind::PdfStreamDecoded,
400 4096,
401 span_params(100, 200),
402 vec![NodeId::from_bytes([7u8; 32])],
403 "pdf:stream-decoded",
404 );
405 let enc = n.encode_canonical();
406 let back = SeedNode::decode_canonical(&enc).unwrap();
407 assert_eq!(n, back);
408 assert_eq!(back.content_id(), n.content_id());
409 assert_eq!(n.encode_canonical(), enc);
410 }
411
412 #[test]
413 fn dependency_change_changes_id() {
414 let a = SeedNode::new(
415 NodeKind::Concat,
416 10,
417 vec![],
418 vec![NodeId::from_bytes([1u8; 32])],
419 "t",
420 );
421 let b = SeedNode::new(
422 NodeKind::Concat,
423 10,
424 vec![],
425 vec![NodeId::from_bytes([2u8; 32])],
426 "t",
427 );
428 assert_ne!(a.content_id(), b.content_id());
429 }
430
431 #[test]
432 fn unknown_kind_and_version_fail_closed() {
433 let mut n = SeedNode::new(NodeKind::Literal, 1, vec![9], vec![], "t").encode_canonical();
434 n[0] = 0x00;
435 assert!(SeedNode::decode_canonical(&n).is_err());
436 let mut n2 = SeedNode::new(NodeKind::Literal, 1, vec![9], vec![], "t").encode_canonical();
437 n2[1] = 0x7F;
438 assert!(SeedNode::decode_canonical(&n2).is_err());
439 }
440
441 #[test]
442 fn trailing_bytes_are_rejected() {
443 let mut enc = SeedNode::new(NodeKind::Literal, 1, vec![], vec![], "t").encode_canonical();
444 enc.push(0);
445 assert!(SeedNode::decode_canonical(&enc).is_err());
446 }
447
448 #[test]
449 fn param_helpers_roundtrip() {
450 let (o, l) = read_span_params(&span_params(5, 9)).unwrap();
451 assert_eq!((o, l), (5, 9));
452 let (ob, g, e) = read_object_params(&object_params(42, 3, 77)).unwrap();
453 assert_eq!((ob, g, e), (42, 3, 77));
454 assert_eq!(read_u32_params(&u32_params(1234)).unwrap(), 1234);
455 }
456}