1use std::collections::{BTreeMap, btree_map::Entry};
8
9use crate::{
10 dirm::{DirmComponentKind, DirmPayload},
11 iff::{parse_form, parse_form_body},
12};
13
14const MAX_GRAPH_DEPTH: usize = u16::MAX as usize + 1;
19
20const MAX_GRAPH_VISITS: usize = 1_000_000;
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub enum ComponentNodeKind {
29 Page,
31 Dictionary,
33 Annotation,
35 SharedOther,
37 Thumbnail,
39}
40
41#[derive(Debug, Clone)]
43pub struct ComponentNode {
44 pub id: String,
46 pub kind: ComponentNodeKind,
48 pub dirm_index: usize,
50 pub includes: Vec<usize>,
52 pub included_by: Vec<usize>,
54}
55
56#[derive(Debug, Clone, PartialEq, Eq)]
58#[non_exhaustive]
59pub enum GraphError {
60 MissingTarget {
62 source: String,
64 target: String,
66 },
67 DuplicateIdentity {
69 id: String,
71 },
72 InvalidComponentType {
74 id: String,
76 form: [u8; 4],
78 },
79 Cycle {
81 path: Vec<String>,
83 },
84 Malformed(String),
86}
87
88pub struct ComponentGraph {
90 nodes: Vec<ComponentNode>,
91 id_to_index: BTreeMap<String, usize>,
92 validation_errors: Vec<GraphError>,
93}
94
95impl ComponentGraph {
96 pub fn parse(bytes: &[u8]) -> Result<ComponentGraph, GraphError> {
103 let document =
104 parse_form(bytes).map_err(|error| GraphError::Malformed(error.to_string()))?;
105 if document.form_type != *b"DJVM" {
106 return Err(GraphError::Malformed(
107 "component graphs require a FORM:DJVM document".to_string(),
108 ));
109 }
110
111 let dirm_chunk = document
112 .chunks
113 .iter()
114 .find(|chunk| chunk.id == *b"DIRM")
115 .ok_or_else(|| GraphError::Malformed("missing DIRM chunk".to_string()))?;
116 let dirm = DirmPayload::decode(dirm_chunk.data)
117 .map_err(|error| GraphError::Malformed(error.to_string()))?;
118 if !dirm.is_bundled() {
119 return Err(GraphError::Malformed(
120 "component graphs support bundled FORM:DJVM documents only".to_string(),
121 ));
122 }
123
124 let directory = dirm.components();
125 let component_forms: Vec<_> = document
126 .chunks
127 .iter()
128 .filter(|chunk| chunk.id == *b"FORM")
129 .collect();
130 if component_forms.len() < directory.len() {
131 return Err(GraphError::Malformed(
132 "DIRM entry count exceeds embedded FORM components".to_string(),
133 ));
134 }
135
136 let mut nodes = Vec::with_capacity(directory.len());
137 let mut id_to_index = BTreeMap::new();
138 let mut validation_errors = Vec::new();
139 let mut unresolved_edges = Vec::new();
140
141 for (dirm_index, entry) in directory.iter().enumerate() {
142 let component = component_forms[dirm_index];
143 let form = component
144 .data
145 .get(..4)
146 .and_then(|bytes| bytes.try_into().ok())
147 .ok_or_else(|| {
148 GraphError::Malformed("component FORM body too short".to_string())
149 })?;
150 let body = component
151 .data
152 .get(4..)
153 .ok_or_else(|| GraphError::Malformed("component FORM body missing".to_string()))?;
154 let chunks =
155 parse_form_body(body).map_err(|error| GraphError::Malformed(error.to_string()))?;
156
157 if form != expected_form(entry.kind) || !is_component_form(form) {
158 validation_errors.push(GraphError::InvalidComponentType {
159 id: entry.id.clone(),
160 form,
161 });
162 }
163
164 let node_index = nodes.len();
165 match id_to_index.entry(entry.id.clone()) {
166 Entry::Occupied(_) => validation_errors.push(GraphError::DuplicateIdentity {
167 id: entry.id.clone(),
168 }),
169 Entry::Vacant(slot) => {
170 slot.insert(node_index);
171 }
172 }
173
174 for chunk in chunks.iter().filter(|chunk| chunk.id == *b"INCL") {
175 if unresolved_edges.len() == MAX_GRAPH_VISITS {
176 return Err(GraphError::Malformed(
177 "component graph INCL edge limit exceeded".to_string(),
178 ));
179 }
180 let target = component_id_from_incl(chunk.data)?;
181 unresolved_edges.push((node_index, target));
182 }
183
184 nodes.push(ComponentNode {
185 id: entry.id.clone(),
186 kind: classify_component(form, &chunks, entry.kind),
187 dirm_index,
188 includes: Vec::new(),
189 included_by: Vec::new(),
190 });
191 }
192
193 for (source, target) in unresolved_edges {
194 if let Some(&target_index) = id_to_index.get(&target) {
195 nodes[source].includes.push(target_index);
196 } else {
197 validation_errors.push(GraphError::MissingTarget {
198 source: nodes[source].id.clone(),
199 target,
200 });
201 }
202 }
203
204 let mut reverse_edges = vec![Vec::new(); nodes.len()];
205 for (source, node) in nodes.iter().enumerate() {
206 for &target in &node.includes {
207 reverse_edges[target].push(source);
208 }
209 }
210 for (node, mut included_by) in nodes.iter_mut().zip(reverse_edges) {
211 included_by.sort_unstable();
212 included_by.dedup();
213 node.included_by = included_by;
214 }
215
216 Ok(ComponentGraph {
217 nodes,
218 id_to_index,
219 validation_errors,
220 })
221 }
222
223 pub fn nodes(&self) -> &[ComponentNode] {
225 &self.nodes
226 }
227
228 pub fn node(&self, id: &str) -> Option<&ComponentNode> {
233 self.id_to_index
234 .get(id)
235 .and_then(|&index| self.nodes.get(index))
236 }
237
238 pub fn includes(&self, id: &str) -> Vec<&ComponentNode> {
240 self.node(id)
241 .map(|node| {
242 node.includes
243 .iter()
244 .filter_map(|&index| self.nodes.get(index))
245 .collect()
246 })
247 .unwrap_or_default()
248 }
249
250 pub fn included_by(&self, id: &str) -> Vec<&ComponentNode> {
252 self.node(id)
253 .map(|node| {
254 node.included_by
255 .iter()
256 .filter_map(|&index| self.nodes.get(index))
257 .collect()
258 })
259 .unwrap_or_default()
260 }
261
262 pub fn transitive_closure(&self, roots: &[&str]) -> Vec<usize> {
267 let root_indices = roots
268 .iter()
269 .filter_map(|id| self.id_to_index.get(*id).copied())
270 .collect();
271 self.bounded_closure(root_indices)
272 }
273
274 pub fn unreachable_components(&self) -> Vec<usize> {
276 let roots = self
277 .nodes
278 .iter()
279 .enumerate()
280 .filter_map(|(index, node)| (node.kind == ComponentNodeKind::Page).then_some(index))
281 .collect();
282 let reachable = self.bounded_closure(roots);
283 let mut seen = vec![false; self.nodes.len()];
284 for index in reachable {
285 seen[index] = true;
286 }
287 seen.into_iter()
288 .enumerate()
289 .filter_map(|(index, reachable)| (!reachable).then_some(index))
290 .collect()
291 }
292
293 pub fn validate(&self) -> Vec<GraphError> {
298 let mut errors = self.validation_errors.clone();
299 for error in self.cycles() {
300 push_unique(&mut errors, error);
301 }
302 errors
303 }
304
305 fn bounded_closure(&self, roots: Vec<usize>) -> Vec<usize> {
306 let mut seen = vec![false; self.nodes.len()];
307 let mut closure = Vec::new();
308 let mut stack = Vec::new();
309 for root in roots.into_iter().rev() {
310 if !seen[root] {
311 seen[root] = true;
312 stack.push((root, 0usize));
313 }
314 }
315
316 let mut visits = 0usize;
317 while let Some((index, depth)) = stack.pop() {
318 if !consume_visit(&mut visits) {
319 break;
320 }
321 closure.push(index);
322 if depth == MAX_GRAPH_DEPTH {
323 continue;
324 }
325
326 for &target in self.nodes[index].includes.iter().rev() {
327 if !consume_visit(&mut visits) {
328 return closure;
329 }
330 if !seen[target] {
331 seen[target] = true;
332 stack.push((target, depth + 1));
333 }
334 }
335 }
336 closure
337 }
338
339 fn cycles(&self) -> Vec<GraphError> {
340 const UNSEEN: u8 = 0;
341 const ACTIVE: u8 = 1;
342 const COMPLETE: u8 = 2;
343
344 let mut colors = vec![UNSEEN; self.nodes.len()];
345 let mut errors = Vec::new();
346 let mut visits = 0usize;
347
348 for start in 0..self.nodes.len() {
349 if colors[start] != UNSEEN {
350 continue;
351 }
352 if !consume_visit(&mut visits) {
353 errors.push(GraphError::Malformed(
354 "component graph validation visit limit exceeded".to_string(),
355 ));
356 break;
357 }
358
359 colors[start] = ACTIVE;
360 let mut stack = vec![(start, 0usize)];
361 while let Some((index, edge_index)) = stack.last_mut() {
362 if *edge_index == self.nodes[*index].includes.len() {
363 colors[*index] = COMPLETE;
364 stack.pop();
365 continue;
366 }
367
368 let target = self.nodes[*index].includes[*edge_index];
369 *edge_index += 1;
370 if !consume_visit(&mut visits) {
371 errors.push(GraphError::Malformed(
372 "component graph validation visit limit exceeded".to_string(),
373 ));
374 return errors;
375 }
376
377 match colors[target] {
378 UNSEEN => {
379 if stack.len() == MAX_GRAPH_DEPTH {
380 errors.push(GraphError::Malformed(
381 "component graph validation depth limit exceeded".to_string(),
382 ));
383 return errors;
384 }
385 colors[target] = ACTIVE;
386 stack.push((target, 0));
387 }
388 ACTIVE => {
389 let cycle_start = stack
390 .iter()
391 .position(|(node, _)| *node == target)
392 .expect("active node is always present in DFS stack");
393 let mut path = stack[cycle_start..]
394 .iter()
395 .map(|(node, _)| self.nodes[*node].id.clone())
396 .collect::<Vec<_>>();
397 path.push(self.nodes[target].id.clone());
398 push_unique(&mut errors, GraphError::Cycle { path });
399 }
400 COMPLETE => {}
401 _ => unreachable!("color state is one of the three constants"),
402 }
403 }
404 }
405
406 errors
407 }
408}
409
410fn expected_form(kind: DirmComponentKind) -> [u8; 4] {
411 match kind {
412 DirmComponentKind::Page => *b"DJVU",
413 DirmComponentKind::Shared => *b"DJVI",
414 DirmComponentKind::Thumbnail => *b"THUM",
415 }
416}
417
418fn is_component_form(form: [u8; 4]) -> bool {
419 form == *b"DJVU" || form == *b"DJVI" || form == *b"THUM"
420}
421
422fn classify_component(
423 form: [u8; 4],
424 chunks: &[crate::iff::IffChunk<'_>],
425 directory_kind: DirmComponentKind,
426) -> ComponentNodeKind {
427 if form == *b"DJVU" {
428 ComponentNodeKind::Page
429 } else if form == *b"THUM" {
430 ComponentNodeKind::Thumbnail
431 } else if form == *b"DJVI" {
432 if chunks.iter().any(|chunk| chunk.id == *b"Djbz") {
433 ComponentNodeKind::Dictionary
434 } else if chunks
435 .iter()
436 .any(|chunk| chunk.id == *b"ANTa" || chunk.id == *b"ANTz")
437 {
438 ComponentNodeKind::Annotation
439 } else {
440 ComponentNodeKind::SharedOther
441 }
442 } else {
443 match directory_kind {
444 DirmComponentKind::Page => ComponentNodeKind::Page,
445 DirmComponentKind::Shared => ComponentNodeKind::SharedOther,
446 DirmComponentKind::Thumbnail => ComponentNodeKind::Thumbnail,
447 }
448 }
449}
450
451fn component_id_from_incl(data: &[u8]) -> Result<String, GraphError> {
452 let end = data
453 .iter()
454 .rposition(|byte| *byte != 0 && !byte.is_ascii_whitespace())
455 .map_or(0, |index| index + 1);
456 core::str::from_utf8(&data[..end])
457 .map(str::to_owned)
458 .map_err(|_| GraphError::Malformed("INCL component id is not valid UTF-8".to_string()))
459}
460
461fn consume_visit(visits: &mut usize) -> bool {
462 if *visits == MAX_GRAPH_VISITS {
463 false
464 } else {
465 *visits += 1;
466 true
467 }
468}
469
470fn push_unique(errors: &mut Vec<GraphError>, error: GraphError) {
471 if !errors.contains(&error) {
472 errors.push(error);
473 }
474}
475
476#[cfg(test)]
477mod tests {
478 use super::*;
479 use crate::{
480 dirm::DirmPayload,
481 iff::{self, Chunk, EmitPart},
482 };
483
484 struct FixtureComponent {
485 id: &'static str,
486 dirm_flag: u8,
487 form: [u8; 4],
488 chunks: Vec<([u8; 4], Vec<u8>)>,
489 }
490
491 fn component(
492 id: &'static str,
493 dirm_flag: u8,
494 form: [u8; 4],
495 chunks: Vec<([u8; 4], Vec<u8>)>,
496 ) -> FixtureComponent {
497 FixtureComponent {
498 id,
499 dirm_flag,
500 form,
501 chunks,
502 }
503 }
504
505 fn incl(id: &[u8]) -> ([u8; 4], Vec<u8>) {
506 (*b"INCL", id.to_vec())
507 }
508
509 fn component_body(component: &FixtureComponent) -> Vec<u8> {
510 let chunks = component
511 .chunks
512 .iter()
513 .map(|(id, data)| Chunk::Leaf {
514 id: *id,
515 data: data.clone(),
516 })
517 .collect::<Vec<_>>();
518 let parts = chunks.iter().map(EmitPart::Chunk).collect::<Vec<_>>();
519 let bytes = iff::partial_emit(component.form, &parts).expect("small fixture FORM");
520 let length = u32::from_be_bytes(bytes[8..12].try_into().unwrap()) as usize;
521 bytes[12..12 + length].to_vec()
522 }
523
524 fn bundled(components: Vec<FixtureComponent>) -> Vec<u8> {
525 let bodies = components.iter().map(component_body).collect::<Vec<_>>();
526 let ids = components
527 .iter()
528 .map(|component| component.id.to_string())
529 .collect::<Vec<_>>();
530 let flags = components
531 .iter()
532 .map(|component| component.dirm_flag)
533 .collect::<Vec<_>>();
534 let sizes = bodies
535 .iter()
536 .map(|body| u32::try_from(8 + body.len()).unwrap())
537 .collect::<Vec<_>>();
538 let mut dirm = DirmPayload::build_bundled(components.len(), &flags, &ids, &sizes);
539
540 let emit = |dirm: &DirmPayload| {
541 let dirm_chunk = Chunk::Leaf {
542 id: *b"DIRM",
543 data: dirm.encode(),
544 };
545 let mut parts = vec![EmitPart::Chunk(&dirm_chunk)];
546 parts.extend(bodies.iter().map(|body| EmitPart::Form(body)));
547 iff::partial_emit_with_offsets(*b"DJVM", &parts).expect("small bundled fixture")
548 };
549
550 let (_, offsets) = emit(&dirm);
551 dirm.offsets = offsets[1..]
552 .iter()
553 .map(|&offset| u32::try_from(offset).unwrap())
554 .collect();
555 emit(&dirm).0
556 }
557
558 fn page_dictionary_annotation_thumbnail() -> ComponentGraph {
559 ComponentGraph::parse(&bundled(vec![
560 component(
561 "page.djvu",
562 1,
563 *b"DJVU",
564 vec![incl(b"dict.djvi\0 \t"), incl(b"anno.djvi")],
565 ),
566 component("dict.djvi", 0, *b"DJVI", vec![(*b"Djbz", vec![1])]),
567 component("anno.djvi", 0, *b"DJVI", vec![(*b"ANTz", vec![2])]),
568 component("thumb.thum", 2, *b"THUM", vec![]),
569 ]))
570 .expect("fixture parses")
571 }
572
573 #[test]
574 fn classifies_components_and_builds_reverse_edges() {
575 let graph = page_dictionary_annotation_thumbnail();
576 assert_eq!(
577 graph
578 .nodes()
579 .iter()
580 .map(|node| node.kind)
581 .collect::<Vec<_>>(),
582 vec![
583 ComponentNodeKind::Page,
584 ComponentNodeKind::Dictionary,
585 ComponentNodeKind::Annotation,
586 ComponentNodeKind::Thumbnail,
587 ]
588 );
589 assert_eq!(graph.nodes()[0].includes, vec![1, 2]);
590 assert_eq!(graph.nodes()[1].included_by, vec![0]);
591 assert_eq!(graph.nodes()[2].included_by, vec![0]);
592 assert!(graph.nodes()[3].included_by.is_empty());
593 assert_eq!(
594 graph
595 .included_by("dict.djvi")
596 .into_iter()
597 .map(|node| node.id.as_str())
598 .collect::<Vec<_>>(),
599 vec!["page.djvu"]
600 );
601 }
602
603 #[test]
604 fn retains_every_incl_chunk_in_order() {
605 let graph = page_dictionary_annotation_thumbnail();
606 assert_eq!(
607 graph
608 .includes("page.djvu")
609 .into_iter()
610 .map(|node| node.id.as_str())
611 .collect::<Vec<_>>(),
612 vec!["dict.djvi", "anno.djvi"]
613 );
614 }
615
616 #[test]
617 fn validates_missing_incl_target() {
618 let graph = ComponentGraph::parse(&bundled(vec![component(
619 "page.djvu",
620 1,
621 *b"DJVU",
622 vec![incl(b"missing.djvi")],
623 )]))
624 .expect("container remains parseable");
625
626 assert!(graph.validate().contains(&GraphError::MissingTarget {
627 source: "page.djvu".to_string(),
628 target: "missing.djvi".to_string(),
629 }));
630 }
631
632 #[test]
633 fn validates_cyclic_shared_components_without_recursing() {
634 let graph = ComponentGraph::parse(&bundled(vec![
635 component("one.djvi", 0, *b"DJVI", vec![incl(b"two.djvi")]),
636 component("two.djvi", 0, *b"DJVI", vec![incl(b"one.djvi")]),
637 ]))
638 .expect("container remains parseable");
639
640 assert!(graph.validate().iter().any(|error| {
641 matches!(
642 error,
643 GraphError::Cycle { path }
644 if path == &vec!["one.djvi".to_string(), "two.djvi".to_string(), "one.djvi".to_string()]
645 )
646 }));
647 }
648
649 #[test]
650 fn validates_duplicate_dirm_identity() {
651 let graph = ComponentGraph::parse(&bundled(vec![
652 component("same.djvu", 1, *b"DJVU", vec![]),
653 component("same.djvu", 1, *b"DJVU", vec![]),
654 ]))
655 .expect("container remains parseable");
656
657 assert!(graph.validate().contains(&GraphError::DuplicateIdentity {
658 id: "same.djvu".to_string(),
659 }));
660 assert_eq!(graph.node("same.djvu").unwrap().dirm_index, 0);
661 }
662
663 #[test]
664 fn finds_unreachable_shared_components() {
665 let graph = ComponentGraph::parse(&bundled(vec![
666 component("page.djvu", 1, *b"DJVU", vec![incl(b"used.djvi")]),
667 component("used.djvi", 0, *b"DJVI", vec![(*b"Djbz", vec![])]),
668 component("orphan.djvi", 0, *b"DJVI", vec![]),
669 ]))
670 .expect("fixture parses");
671
672 assert_eq!(graph.nodes()[2].kind, ComponentNodeKind::SharedOther);
673 assert_eq!(graph.unreachable_components(), vec![2]);
674 }
675
676 #[test]
677 fn computes_transitive_closure_including_roots() {
678 let graph = page_dictionary_annotation_thumbnail();
679 assert_eq!(graph.transitive_closure(&["page.djvu"]), vec![0, 1, 2]);
680 }
681
682 #[test]
683 fn records_component_form_mismatches_without_rejecting_the_container() {
684 let graph = ComponentGraph::parse(&bundled(vec![component(
685 "page.djvu",
686 1,
687 *b"DJVI",
688 vec![(*b"Djbz", vec![])],
689 )]))
690 .expect("container remains parseable");
691
692 assert_eq!(graph.nodes()[0].kind, ComponentNodeKind::Dictionary);
693 assert!(
694 graph
695 .validate()
696 .contains(&GraphError::InvalidComponentType {
697 id: "page.djvu".to_string(),
698 form: *b"DJVI",
699 })
700 );
701 }
702}