1#![cfg_attr(not(feature = "std"), no_std)]
29#![deny(unsafe_code)]
30
31#[cfg(not(feature = "std"))]
32extern crate alloc;
33
34#[cfg(not(feature = "std"))]
35use alloc::{string::String, vec::Vec};
36use core::{fmt::Write as _, str};
37#[cfg(feature = "std")]
38use std::{string::String, vec::Vec};
39
40#[derive(Debug, thiserror::Error, PartialEq, Eq)]
44#[non_exhaustive]
45pub enum IffError {
46 #[error("input is too short to be a valid IFF file")]
48 TooShort,
49
50 #[error("bad magic bytes: expected AT&T, got {got:?}")]
52 BadMagic { got: [u8; 4] },
53
54 #[error("unknown FORM type: {id:?}")]
59 UnknownFormType { id: [u8; 4] },
60
61 #[error(
63 "chunk {:?} claims {} bytes but only {} are available",
64 id,
65 claimed,
66 available
67 )]
68 ChunkTooLong {
69 id: [u8; 4],
70 claimed: u32,
71 available: usize,
72 },
73
74 #[error("IFF FORM nesting exceeds the maximum depth of {max}")]
76 DepthLimitExceeded {
77 max: usize,
79 },
80
81 #[error("unexpected end of input (truncated IFF data)")]
83 Truncated,
84
85 #[error("unsupported DjVu format version {version} (DjVuLibre rejects version >= 50)")]
91 UnsupportedVersion { version: u16 },
92}
93
94#[derive(Debug, Clone, PartialEq, Eq)]
96#[non_exhaustive]
97pub enum LegacyError {
98 UnexpectedEof,
100 InvalidMagic,
102 InvalidLength,
104 MissingChunk(&'static str),
106 Unsupported(&'static str),
108 FormatError(String),
110}
111
112impl core::fmt::Display for LegacyError {
113 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
114 match self {
115 LegacyError::UnexpectedEof => write!(f, "unexpected end of input"),
116 LegacyError::InvalidMagic => write!(f, "invalid magic number"),
117 LegacyError::InvalidLength => write!(f, "invalid length"),
118 LegacyError::MissingChunk(id) => write!(f, "missing required chunk: {}", id),
119 LegacyError::Unsupported(msg) => write!(f, "unsupported: {}", msg),
120 LegacyError::FormatError(msg) => write!(f, "format error: {}", msg),
121 }
122 }
123}
124
125#[cfg(feature = "std")]
126impl std::error::Error for LegacyError {}
127
128pub use LegacyError as Error;
130
131pub const MAGIC: [u8; 4] = *b"AT&T";
139
140pub type ChunkId = [u8; 4];
142
143#[derive(Debug, Clone, PartialEq, Eq)]
152pub struct ChunkRecord {
153 pub id: ChunkId,
155 pub form_type: Option<ChunkId>,
157 pub offset: usize,
159 pub length: usize,
161 pub depth: usize,
163 pub path: Vec<usize>,
165}
166
167#[derive(Debug, Clone)]
169pub enum Chunk {
170 Form {
172 secondary_id: ChunkId,
174 length: u32,
177 children: Vec<Chunk>,
179 },
180 Leaf {
182 id: ChunkId,
184 data: Vec<u8>,
186 },
187}
188
189impl Chunk {
190 pub fn data(&self) -> &[u8] {
192 match self {
193 Chunk::Form { .. } => &[],
194 Chunk::Leaf { data, .. } => data,
195 }
196 }
197
198 pub fn children(&self) -> &[Chunk] {
200 match self {
201 Chunk::Form { children, .. } => children,
202 Chunk::Leaf { .. } => &[],
203 }
204 }
205
206 pub fn payload_length(&self) -> u32 {
212 match self {
213 Chunk::Form { length, .. } => *length,
214 Chunk::Leaf { data, .. } => data.len() as u32,
215 }
216 }
217
218 pub fn find_first(&self, target_id: &[u8; 4]) -> Option<&Chunk> {
220 self.children().iter().find(|c| match c {
221 Chunk::Leaf { id, .. } => id == target_id,
222 _ => false,
223 })
224 }
225
226 pub fn find_all(&self, target_id: &[u8; 4]) -> Vec<&Chunk> {
228 self.children()
229 .iter()
230 .filter(|c| match c {
231 Chunk::Leaf { id, .. } => id == target_id,
232 _ => false,
233 })
234 .collect()
235 }
236}
237
238#[derive(Debug, Clone)]
240pub struct DjvuFile {
241 pub root: Chunk,
242}
243
244pub fn parse(data: &[u8]) -> Result<DjvuFile, Error> {
248 if data.len() < 4 {
249 return Err(Error::UnexpectedEof);
250 }
251 let (magic, rest) = if &data[..4] == b"AT&T" {
253 (&data[..4], &data[4..])
254 } else {
255 (&data[..0], data)
257 };
258 let _ = magic;
259
260 let (root, _) = parse_chunk(rest, 0, 0)?;
261 Ok(DjvuFile { root })
262}
263
264const MAX_IFF_DEPTH: u32 = 64;
269
270pub fn walk_chunks(data: &[u8]) -> Result<Vec<ChunkRecord>, IffError> {
282 if data.len() < 16 {
285 return Err(IffError::TooShort);
286 }
287
288 let magic = read_4(data, 0)?;
289 if magic != MAGIC {
290 return Err(IffError::BadMagic { got: magic });
291 }
292 if read_4(data, 4)? != *b"FORM" {
293 return Err(IffError::Truncated);
294 }
295
296 let mut records = Vec::new();
297 walk_chunk(data, 4, data.len(), 0, Vec::new(), &mut records)?;
298 Ok(records)
299}
300
301fn walk_chunk(
304 data: &[u8],
305 offset: usize,
306 limit: usize,
307 depth: usize,
308 path: Vec<usize>,
309 records: &mut Vec<ChunkRecord>,
310) -> Result<usize, IffError> {
311 if depth > MAX_IFF_DEPTH as usize {
312 return Err(IffError::DepthLimitExceeded {
313 max: MAX_IFF_DEPTH as usize,
314 });
315 }
316
317 let header_end = offset.checked_add(8).ok_or(IffError::Truncated)?;
318 if header_end > limit {
319 return Err(IffError::Truncated);
320 }
321
322 let id = read_4(data, offset)?;
323 let length = read_u32_be(data, offset + 4)? as usize;
324 let payload_end = match header_end.checked_add(length) {
330 Some(payload_end) if payload_end <= limit => payload_end,
331 _ => {
332 return Err(IffError::ChunkTooLong {
333 id,
334 claimed: length as u32,
335 available: limit.saturating_sub(header_end),
336 });
337 }
338 };
339
340 if id == *b"FORM" && length < 4 {
341 return Err(IffError::Truncated);
342 }
343 let form_type = if id == *b"FORM" {
344 Some(read_4(data, header_end)?)
345 } else {
346 None
347 };
348
349 records.push(ChunkRecord {
350 id,
351 form_type,
352 offset,
353 length,
354 depth,
355 path: path.clone(),
356 });
357
358 if id == *b"FORM" {
359 let mut child_offset = header_end + 4;
360 let mut child_index = 0;
361 while child_offset < payload_end {
362 if payload_end - child_offset < 8 {
366 break;
367 }
368
369 let mut child_path = path.clone();
370 child_path.push(child_index);
371 child_offset = walk_chunk(
372 data,
373 child_offset,
374 payload_end,
375 depth + 1,
376 child_path,
377 records,
378 )?;
379 child_index += 1;
380 }
381 }
382
383 payload_end
384 .checked_add(length & 1)
385 .ok_or(IffError::Truncated)
386}
387
388fn parse_chunk(data: &[u8], offset: usize, depth: u32) -> Result<(Chunk, usize), Error> {
391 if depth > MAX_IFF_DEPTH {
392 return Err(Error::InvalidLength);
393 }
394 if offset.checked_add(8).is_none_or(|end| end > data.len()) {
395 return Err(Error::UnexpectedEof);
396 }
397
398 let id: ChunkId = [
399 data[offset],
400 data[offset + 1],
401 data[offset + 2],
402 data[offset + 3],
403 ];
404 let length = u32::from_be_bytes([
405 data[offset + 4],
406 data[offset + 5],
407 data[offset + 6],
408 data[offset + 7],
409 ]);
410
411 let payload_start = offset + 8;
412 let payload_end = payload_start
416 .checked_add(length as usize)
417 .ok_or(Error::InvalidLength)?;
418
419 if payload_end > data.len() {
420 return Err(Error::UnexpectedEof);
421 }
422
423 let total = 8usize
425 .checked_add(length as usize)
426 .ok_or(Error::InvalidLength)?;
427 let padded_total = total.checked_add(total % 2).ok_or(Error::InvalidLength)?;
428
429 if &id == b"FORM" {
430 if length < 4 {
431 return Err(Error::InvalidLength);
432 }
433 let secondary_id: ChunkId = [
434 data[payload_start],
435 data[payload_start + 1],
436 data[payload_start + 2],
437 data[payload_start + 3],
438 ];
439
440 let children_start = payload_start + 4;
441 let children = parse_children(data, children_start, payload_end, depth + 1)?;
442
443 Ok((
444 Chunk::Form {
445 secondary_id,
446 length,
447 children,
448 },
449 padded_total,
450 ))
451 } else {
452 let chunk_data = data[payload_start..payload_end].to_vec();
453 Ok((
454 Chunk::Leaf {
455 id,
456 data: chunk_data,
457 },
458 padded_total,
459 ))
460 }
461}
462
463fn parse_children(data: &[u8], start: usize, end: usize, depth: u32) -> Result<Vec<Chunk>, Error> {
465 let mut chunks = Vec::new();
466 let mut pos = start;
467
468 while pos < end {
469 if pos + 8 > end {
470 break;
472 }
473 let (chunk, consumed) = parse_chunk(data, pos, depth)?;
474 chunks.push(chunk);
475 pos = pos.checked_add(consumed).ok_or(Error::InvalidLength)?;
478 }
479
480 Ok(chunks)
481}
482
483pub fn emit(file: &DjvuFile) -> Vec<u8> {
494 let mut out = Vec::with_capacity(64);
495 out.extend_from_slice(&MAGIC);
496 emit_chunk(&file.root, &mut out);
497 out
498}
499
500fn emit_chunk(chunk: &Chunk, out: &mut Vec<u8>) {
501 emit_chunk_inner(chunk, out, false);
502}
503
504fn emit_chunk_inner(chunk: &Chunk, out: &mut Vec<u8>, suppress_inner_pad: bool) {
505 match chunk {
506 Chunk::Form {
507 secondary_id,
508 length: stored_length,
509 children,
510 } => {
511 let suppress_last_pad = (*stored_length & 1) == 1;
520 let mut payload: Vec<u8> = Vec::new();
521 payload.extend_from_slice(secondary_id);
522 let n = children.len();
523 for (i, child) in children.iter().enumerate() {
524 let last = i + 1 == n;
525 emit_chunk_inner(child, &mut payload, last && suppress_last_pad);
526 }
527 let len = payload.len() as u32;
528 out.extend_from_slice(b"FORM");
529 out.extend_from_slice(&len.to_be_bytes());
530 out.extend_from_slice(&payload);
531 let total = 8 + payload.len();
534 if !suppress_inner_pad && total % 2 == 1 {
535 out.push(0);
536 }
537 }
538 Chunk::Leaf { id, data } => {
539 let len = data.len() as u32;
540 out.extend_from_slice(id);
541 out.extend_from_slice(&len.to_be_bytes());
542 out.extend_from_slice(data);
543 let total = 8 + data.len();
544 if !suppress_inner_pad && total % 2 == 1 {
545 out.push(0);
546 }
547 }
548 }
549}
550
551pub fn emitted_size(chunk: &Chunk) -> usize {
560 emitted_size_inner(chunk, false)
561}
562
563fn emitted_size_inner(chunk: &Chunk, suppress_inner_pad: bool) -> usize {
564 match chunk {
565 Chunk::Form {
566 length: stored_length,
567 children,
568 ..
569 } => {
570 let suppress_last_pad = (*stored_length & 1) == 1;
571 let n = children.len();
572 let mut payload = 4usize; for (i, child) in children.iter().enumerate() {
574 let last = i + 1 == n;
575 payload += emitted_size_inner(child, last && suppress_last_pad);
576 }
577 let total = 8 + payload;
578 total + usize::from(!suppress_inner_pad && total % 2 == 1)
579 }
580 Chunk::Leaf { data, .. } => {
581 let total = 8 + data.len();
582 total + usize::from(!suppress_inner_pad && total % 2 == 1)
583 }
584 }
585}
586
587pub enum EmitPart<'a> {
590 Chunk(&'a Chunk),
593 Verbatim(&'a [u8]),
598 Form(&'a [u8]),
605}
606
607pub fn partial_emit(secondary_id: ChunkId, parts: &[EmitPart<'_>]) -> Option<Vec<u8>> {
618 partial_emit_with_offsets(secondary_id, parts).map(|(bytes, _)| bytes)
619}
620
621pub fn partial_emit_with_offsets(
639 secondary_id: ChunkId,
640 parts: &[EmitPart<'_>],
641) -> Option<(Vec<u8>, Vec<usize>)> {
642 const PROLOGUE: usize = 12;
646 let mut payload = Vec::new();
647 payload.extend_from_slice(&secondary_id); let mut offsets = Vec::with_capacity(parts.len());
649 for part in parts {
650 offsets.push(PROLOGUE + payload.len());
651 match part {
652 EmitPart::Chunk(chunk) => emit_chunk(chunk, &mut payload),
653 EmitPart::Verbatim(bytes) => {
654 payload.extend_from_slice(bytes);
655 if payload.len() % 2 == 1 {
656 payload.push(0);
657 }
658 }
659 EmitPart::Form(body) => {
660 let len = u32::try_from(body.len()).ok()?;
661 payload.extend_from_slice(b"FORM");
662 payload.extend_from_slice(&len.to_be_bytes());
663 payload.extend_from_slice(body);
664 if payload.len() % 2 == 1 {
665 payload.push(0);
666 }
667 }
668 }
669 }
670 let len = u32::try_from(payload.len()).ok()?;
671 let mut out = Vec::with_capacity(8 + payload.len());
672 out.extend_from_slice(&MAGIC);
673 out.extend_from_slice(b"FORM");
674 out.extend_from_slice(&len.to_be_bytes());
675 out.extend_from_slice(&payload);
676 if (8 + payload.len()) % 2 == 1 {
679 out.push(0);
680 }
681 Some((out, offsets))
682}
683
684#[derive(Debug, Clone, Copy)]
692pub struct IffChunk<'a> {
693 pub id: [u8; 4],
695 pub data: &'a [u8],
697}
698
699#[derive(Debug)]
701pub struct Form<'a> {
702 pub form_type: [u8; 4],
704 pub chunks: Vec<IffChunk<'a>>,
706}
707
708pub fn parse_form(data: &[u8]) -> Result<Form<'_>, IffError> {
720 if data.len() < 16 {
722 return Err(IffError::TooShort);
723 }
724
725 let magic = read_4(data, 0)?;
727 if &magic != b"AT&T" {
728 return Err(IffError::BadMagic { got: magic });
729 }
730
731 let form_id = read_4(data, 4)?;
733 if &form_id != b"FORM" {
734 return Err(IffError::Truncated);
735 }
736
737 let form_len = read_u32_be(data, 8)? as usize;
739
740 let form_data_end = 12_usize.checked_add(form_len).ok_or(IffError::Truncated)?;
742 if form_data_end > data.len() {
743 return Err(IffError::ChunkTooLong {
744 id: *b"FORM",
745 claimed: form_len as u32,
746 available: data.len().saturating_sub(12),
747 });
748 }
749
750 if form_len < 4 {
752 return Err(IffError::Truncated);
753 }
754 let form_type = read_4(data, 12)?;
755
756 let body = data.get(16..form_data_end).ok_or(IffError::Truncated)?;
758
759 let chunks = parse_form_body(body)?;
760
761 Ok(Form { form_type, chunks })
762}
763
764pub fn parse_form_body(mut buf: &[u8]) -> Result<Vec<IffChunk<'_>>, IffError> {
774 let mut chunks = Vec::new();
775
776 while buf.len() >= 8 {
777 let id = read_4(buf, 0)?;
778 let data_len = read_u32_be(buf, 4)? as usize;
779
780 let data_start = 8_usize;
781 let data_end = data_start
782 .checked_add(data_len)
783 .ok_or(IffError::Truncated)?;
784
785 if data_end > buf.len() {
786 return Err(IffError::ChunkTooLong {
787 id,
788 claimed: data_len as u32,
789 available: buf.len().saturating_sub(data_start),
790 });
791 }
792
793 let chunk_data = buf.get(data_start..data_end).ok_or(IffError::Truncated)?;
794 chunks.push(IffChunk {
795 id,
796 data: chunk_data,
797 });
798
799 let padded_len = data_len + (data_len & 1);
801 let next = data_start
802 .checked_add(padded_len)
803 .ok_or(IffError::Truncated)?;
804
805 buf = buf.get(next.min(buf.len())..).ok_or(IffError::Truncated)?;
807 }
808
809 Ok(chunks)
810}
811
812#[inline]
814fn read_4(data: &[u8], offset: usize) -> Result<[u8; 4], IffError> {
815 data.get(offset..offset + 4)
816 .and_then(|s| s.try_into().ok())
817 .ok_or(IffError::Truncated)
818}
819
820#[inline]
822fn read_u32_be(data: &[u8], offset: usize) -> Result<u32, IffError> {
823 let b = read_4(data, offset)?;
824 Ok(u32::from_be_bytes(b))
825}
826
827pub fn dump(file: &DjvuFile) -> String {
834 let mut out = String::new();
835 dump_chunk(&file.root, 1, &mut out);
836 out
837}
838
839fn dump_chunk(chunk: &Chunk, depth: usize, out: &mut String) {
840 for _ in 0..depth {
841 out.push_str(" ");
842 }
843 match chunk {
844 Chunk::Form {
845 secondary_id,
846 length,
847 children,
848 } => {
849 let sec = str::from_utf8(secondary_id).unwrap_or("????");
850 let _ = writeln!(out, "FORM:{sec} [{length}] ");
851 for child in children {
852 dump_chunk(child, depth + 1, out);
853 }
854 }
855 Chunk::Leaf { id, data } => {
856 let id_str = str::from_utf8(id).unwrap_or("????");
857 let _ = writeln!(out, "{id_str} [{}] ", data.len());
858 }
859 }
860}
861
862#[cfg(test)]
863mod tests {
864 use super::*;
865
866 #[test]
871 fn overflowing_chunk_length_reports_chunk_too_long_with_identity() {
872 let mut buf = Vec::new();
874 buf.extend_from_slice(&MAGIC);
875 buf.extend_from_slice(b"FORM");
876 let inner_len = 4u32 + 8; buf.extend_from_slice(&inner_len.to_be_bytes());
878 buf.extend_from_slice(b"DJVU");
879 buf.extend_from_slice(b"BG44");
880 buf.extend_from_slice(&u32::MAX.to_be_bytes());
881
882 match walk_chunks(&buf) {
883 Err(IffError::ChunkTooLong { id, claimed, .. }) => {
884 assert_eq!(&id, b"BG44");
885 assert_eq!(claimed, u32::MAX);
886 }
887 other => panic!("expected ChunkTooLong, got {other:?}"),
888 }
889 }
890
891 #[test]
894 fn deeply_nested_forms_are_rejected_not_overflow() {
895 let mut buf = Vec::new();
897 buf.extend_from_slice(b"FORM");
898 buf.extend_from_slice(&4u32.to_be_bytes());
899 buf.extend_from_slice(b"DJVU");
900 for _ in 0..200 {
901 let inner = buf;
902 let len = 4 + inner.len();
903 let mut outer = Vec::new();
904 outer.extend_from_slice(b"FORM");
905 outer.extend_from_slice(&(len as u32).to_be_bytes());
906 outer.extend_from_slice(b"DJVU");
907 outer.extend_from_slice(&inner);
908 buf = outer;
909 }
910 let mut full = Vec::from(*b"AT&T");
911 full.extend_from_slice(&buf);
912 assert!(
913 parse(&full).is_err(),
914 "deep nesting must error, not overflow"
915 );
916 }
917
918 #[test]
921 fn overflowing_chunk_length_is_rejected() {
922 let mut data = Vec::from(*b"AT&T");
923 data.extend_from_slice(b"JUNK");
924 data.extend_from_slice(&u32::MAX.to_be_bytes()); data.extend_from_slice(b"\x00\x00");
926 assert!(parse(&data).is_err());
927 }
928
929 #[test]
930 fn walk_chunks_reports_nested_offsets_lengths_and_paths() {
931 let mut data = Vec::from(*b"AT&T");
935 data.extend_from_slice(b"FORM");
936 data.extend_from_slice(&36u32.to_be_bytes());
937 data.extend_from_slice(b"DJVM");
938 data.extend_from_slice(b"DIRM");
939 data.extend_from_slice(&2u32.to_be_bytes());
940 data.extend_from_slice(b"id");
941 data.extend_from_slice(b"FORM");
942 data.extend_from_slice(&14u32.to_be_bytes());
943 data.extend_from_slice(b"DJVU");
944 data.extend_from_slice(b"INFO");
945 data.extend_from_slice(&2u32.to_be_bytes());
946 data.extend_from_slice(b"xy");
947
948 let records = walk_chunks(&data).expect("synthetic document walks");
949 let dirm_offset = 16;
950 let nested_form_offset = dirm_offset + 8 + 2;
951 let info_offset = nested_form_offset + 8 + 4;
952
953 assert_eq!(
954 records,
955 vec![
956 ChunkRecord {
957 id: *b"FORM",
958 form_type: Some(*b"DJVM"),
959 offset: 4,
960 length: 36,
961 depth: 0,
962 path: vec![],
963 },
964 ChunkRecord {
965 id: *b"DIRM",
966 form_type: None,
967 offset: dirm_offset,
968 length: 2,
969 depth: 1,
970 path: vec![0],
971 },
972 ChunkRecord {
973 id: *b"FORM",
974 form_type: Some(*b"DJVU"),
975 offset: nested_form_offset,
976 length: 14,
977 depth: 1,
978 path: vec![1],
979 },
980 ChunkRecord {
981 id: *b"INFO",
982 form_type: None,
983 offset: info_offset,
984 length: 2,
985 depth: 2,
986 path: vec![1, 0],
987 },
988 ]
989 );
990 }
991
992 #[test]
993 fn walk_chunks_offsets_match_real_bundled_fixture() {
994 let data = std::fs::read(
995 std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
996 .join("../../tests/fixtures/DjVu3Spec_bundled.djvu"),
997 )
998 .expect("bundled fixture exists");
999 let parsed = parse_form(&data).expect("fixture parses");
1000 let records = walk_chunks(&data).expect("fixture walks");
1001
1002 let payload_offset = |payload: &[u8]| payload.as_ptr() as usize - data.as_ptr() as usize;
1003 let dirm = parsed
1004 .chunks
1005 .iter()
1006 .find(|chunk| chunk.id == *b"DIRM")
1007 .expect("fixture has DIRM");
1008 let component = parsed
1009 .chunks
1010 .iter()
1011 .find(|chunk| chunk.id == *b"FORM")
1012 .expect("fixture has an embedded component FORM");
1013
1014 let dirm_record = records
1015 .iter()
1016 .find(|record| record.id == *b"DIRM")
1017 .expect("walk reports DIRM");
1018 assert_eq!(dirm_record.offset, payload_offset(dirm.data) - 8);
1019 assert_eq!(dirm_record.length, dirm.data.len());
1020
1021 let component_record = records
1022 .iter()
1023 .find(|record| record.depth == 1 && record.id == *b"FORM")
1024 .expect("walk reports embedded component FORM");
1025 assert_eq!(component_record.offset, payload_offset(component.data) - 8);
1026 assert_eq!(component_record.length, component.data.len());
1027 assert_eq!(component_record.path.len(), 1);
1028 }
1029
1030 fn assets_path() -> std::path::PathBuf {
1031 std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
1032 .join("../../references/djvujs/library/assets")
1033 }
1034
1035 fn golden_path() -> std::path::PathBuf {
1036 std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../tests/golden/iff")
1037 }
1038
1039 fn normalize_dump(input: &str) -> Vec<String> {
1043 input
1044 .lines()
1045 .filter(|l| !l.trim().is_empty())
1046 .map(|line| {
1047 let trimmed = line.trim_end();
1048 if let Some(bracket_end) = trimmed.find(']') {
1049 let structural = &trimmed[..=bracket_end];
1050 structural.trim_end().to_string()
1051 } else {
1052 trimmed.to_string()
1053 }
1054 })
1055 .collect()
1056 }
1057
1058 fn assert_structure_matches(djvu_file: &str, golden_file: &str) {
1059 let data = std::fs::read(assets_path().join(djvu_file)).unwrap();
1060 let file = parse(&data).unwrap();
1061 let actual = dump(&file);
1062 let expected = std::fs::read_to_string(golden_path().join(golden_file)).unwrap();
1063
1064 let actual_lines = normalize_dump(&actual);
1065 let expected_lines = normalize_dump(&expected);
1066
1067 assert_eq!(
1068 actual_lines.len(),
1069 expected_lines.len(),
1070 "Line count mismatch for {} ({} vs {})",
1071 djvu_file,
1072 actual_lines.len(),
1073 expected_lines.len()
1074 );
1075
1076 for (i, (a, e)) in actual_lines.iter().zip(expected_lines.iter()).enumerate() {
1077 assert_eq!(
1078 a,
1079 e,
1080 "Line {} mismatch for {}\n actual: {:?}\n expected: {:?}",
1081 i + 1,
1082 djvu_file,
1083 a,
1084 e
1085 );
1086 }
1087 }
1088
1089 #[test]
1090 fn parse_boy_jb2_legacy() {
1091 let data = std::fs::read(assets_path().join("boy_jb2.djvu")).unwrap();
1092 let file = parse(&data).unwrap();
1093
1094 match &file.root {
1095 Chunk::Form {
1096 secondary_id,
1097 children,
1098 ..
1099 } => {
1100 assert_eq!(secondary_id, b"DJVU");
1101 assert_eq!(children.len(), 2);
1102 }
1103 _ => panic!("expected FORM root"),
1104 }
1105 }
1106
1107 #[test]
1108 fn structure_boy_jb2() {
1109 assert_structure_matches("boy_jb2.djvu", "boy_jb2.dump");
1110 }
1111
1112 #[test]
1113 fn structure_boy() {
1114 assert_structure_matches("boy.djvu", "boy.dump");
1115 }
1116
1117 #[test]
1118 fn structure_chicken() {
1119 assert_structure_matches("chicken.djvu", "chicken.dump");
1120 }
1121
1122 #[test]
1123 fn structure_carte() {
1124 assert_structure_matches("carte.djvu", "carte.dump");
1125 }
1126
1127 #[test]
1128 fn structure_navm_fgbz() {
1129 assert_structure_matches("navm_fgbz.djvu", "navm_fgbz.dump");
1130 }
1131
1132 #[test]
1133 fn structure_colorbook() {
1134 assert_structure_matches("colorbook.djvu", "colorbook.dump");
1135 }
1136
1137 #[test]
1138 fn structure_djvu3spec_bundled() {
1139 assert_structure_matches("DjVu3Spec_bundled.djvu", "djvu3spec_bundled.dump");
1140 }
1141
1142 #[test]
1143 fn structure_big_scanned_page() {
1144 assert_structure_matches("big-scanned-page.djvu", "big_scanned_page.dump");
1145 }
1146
1147 fn assert_emitted_size_matches_emit(name: &str) {
1154 let Ok(data) = std::fs::read(assets_path().join(name)) else {
1155 return; };
1157 let file = parse(&data).unwrap();
1158 let emitted = emit(&file);
1159 assert_eq!(
1160 emitted_size(&file.root),
1161 emitted.len() - 4,
1162 "emitted_size disagrees with emit() for {name}"
1163 );
1164 }
1165
1166 #[test]
1167 fn emitted_size_matches_emit_corpus() {
1168 for name in [
1169 "boy_jb2.djvu",
1170 "boy.djvu",
1171 "chicken.djvu",
1172 "carte.djvu",
1173 "navm_fgbz.djvu",
1174 "colorbook.djvu",
1175 "DjVu3Spec_bundled.djvu",
1176 "big-scanned-page.djvu",
1177 ] {
1178 assert_emitted_size_matches_emit(name);
1179 }
1180 }
1181
1182 #[test]
1183 fn partial_emit_verbatim_matches_chunk_framing() {
1184 let tree = DjvuFile {
1190 root: Chunk::Form {
1191 secondary_id: *b"DJVU",
1192 length: 0,
1193 children: vec![
1194 Chunk::Leaf {
1195 id: *b"INFO",
1196 data: vec![0xAA; 5], },
1198 Chunk::Leaf {
1199 id: *b"Sjbz",
1200 data: vec![0xBB; 4], },
1202 ],
1203 },
1204 };
1205 let canonical = emit(&tree); let Chunk::Form { children, .. } = &tree.root else {
1208 unreachable!()
1209 };
1210 let mut info_bytes = Vec::new();
1212 emit_chunk(&children[0], &mut info_bytes);
1213 let mut sjbz_bytes = Vec::new();
1214 emit_chunk(&children[1], &mut sjbz_bytes);
1215
1216 let via_verbatim = partial_emit(
1217 *b"DJVU",
1218 &[
1219 EmitPart::Verbatim(&info_bytes),
1220 EmitPart::Verbatim(&sjbz_bytes),
1221 ],
1222 )
1223 .expect("fits in u32");
1224 let via_chunks = partial_emit(
1225 *b"DJVU",
1226 &[EmitPart::Chunk(&children[0]), EmitPart::Chunk(&children[1])],
1227 )
1228 .expect("fits in u32");
1229
1230 assert_eq!(via_verbatim, canonical, "verbatim path must match emit");
1231 assert_eq!(via_chunks, canonical, "chunk path must match emit");
1232 }
1233
1234 #[test]
1235 fn partial_emit_pads_odd_verbatim_child() {
1236 let parts = [EmitPart::Verbatim(&[1u8, 2, 3])];
1239 let out = partial_emit(*b"DJVU", &parts).unwrap();
1240 assert_eq!(out.len(), 20);
1242 assert_eq!(&out[..8], b"AT&TFORM");
1243 assert_eq!(u32::from_be_bytes(out[8..12].try_into().unwrap()), 8);
1245 assert_eq!(&out[12..16], b"DJVU");
1246 assert_eq!(&out[16..19], &[1, 2, 3]);
1247 assert_eq!(out[19], 0);
1248 }
1249
1250 #[test]
1251 fn partial_emit_form_part_frames_nested_form() {
1252 let body: &[u8] = b"DJVUxyz"; let via_form = partial_emit(*b"DJVM", &[EmitPart::Form(body)]).unwrap();
1256
1257 let mut framed = Vec::new();
1259 framed.extend_from_slice(b"FORM");
1260 framed.extend_from_slice(&(body.len() as u32).to_be_bytes());
1261 framed.extend_from_slice(body);
1262 framed.push(0); let via_verbatim = partial_emit(*b"DJVM", &[EmitPart::Verbatim(&framed)]).unwrap();
1264
1265 assert_eq!(via_form, via_verbatim, "Form part must match framed FORM");
1266 assert_eq!(&via_form[..8], b"AT&TFORM");
1268 assert_eq!(&via_form[12..16], b"DJVM");
1269 assert_eq!(&via_form[16..20], b"FORM");
1270 assert_eq!(u32::from_be_bytes(via_form[20..24].try_into().unwrap()), 7);
1271 assert_eq!(&via_form[24..31], body);
1272 assert_eq!(via_form[31], 0); }
1274
1275 #[test]
1276 fn partial_emit_with_offsets_reports_part_starts() {
1277 let dirm = Chunk::Leaf {
1280 id: *b"DIRM",
1281 data: vec![0xAB; 5], };
1283 let comp0: &[u8] = b"DJVU0000"; let comp1: &[u8] = b"DJVIaa"; let parts = [
1286 EmitPart::Chunk(&dirm),
1287 EmitPart::Form(comp0),
1288 EmitPart::Form(comp1),
1289 ];
1290 let (bytes, offsets) = partial_emit_with_offsets(*b"DJVM", &parts).unwrap();
1291
1292 assert_eq!(offsets.len(), 3);
1293 assert_eq!(offsets[0], 16);
1295 assert_eq!(&bytes[offsets[0]..offsets[0] + 4], b"DIRM");
1296 for &off in &offsets[1..] {
1298 assert_eq!(&bytes[off..off + 4], b"FORM", "offset must point at FORM");
1299 }
1300 assert_eq!(offsets[2] - offsets[1], 16);
1302 }
1303
1304 fn minimal_djvu_bytes() -> Vec<u8> {
1308 let info_data: &[u8] = &[
1309 0x00, 0xB5, 0x00, 0xF0, 0x18, 0x00, 0x64, 0x00, 0x16, 0x00, ];
1317 let info_len = info_data.len() as u32;
1318
1319 let mut chunk = Vec::new();
1320 chunk.extend_from_slice(b"INFO");
1321 chunk.extend_from_slice(&info_len.to_be_bytes());
1322 chunk.extend_from_slice(info_data);
1323
1324 let mut form_body = Vec::new();
1325 form_body.extend_from_slice(b"DJVU");
1326 form_body.extend_from_slice(&chunk);
1327
1328 let form_len = form_body.len() as u32;
1329
1330 let mut file = Vec::new();
1331 file.extend_from_slice(b"AT&T");
1332 file.extend_from_slice(b"FORM");
1333 file.extend_from_slice(&form_len.to_be_bytes());
1334 file.extend_from_slice(&form_body);
1335
1336 file
1337 }
1338
1339 #[test]
1340 fn empty_input_is_error() {
1341 let result = parse_form(&[]);
1342 assert!(result.is_err());
1343 assert_eq!(result.unwrap_err(), IffError::TooShort);
1344 }
1345
1346 #[test]
1347 fn short_input_is_error() {
1348 let result = parse_form(&[0u8; 10]);
1349 assert!(result.is_err());
1350 assert_eq!(result.unwrap_err(), IffError::TooShort);
1351 }
1352
1353 #[test]
1354 fn bad_magic_is_error() {
1355 let mut data = minimal_djvu_bytes();
1356 data[0] = 0xFF;
1357 data[1] = 0xFF;
1358 data[2] = 0xFF;
1359 data[3] = 0xFF;
1360
1361 let result = parse_form(&data);
1362 assert!(result.is_err());
1363 assert_eq!(
1364 result.unwrap_err(),
1365 IffError::BadMagic {
1366 got: [0xFF, 0xFF, 0xFF, 0xFF]
1367 }
1368 );
1369 }
1370
1371 #[test]
1372 fn valid_single_page_parses() {
1373 let data = minimal_djvu_bytes();
1374 let form = parse_form(&data).expect("should parse successfully");
1375
1376 assert_eq!(&form.form_type, b"DJVU");
1377 assert_eq!(form.chunks.len(), 1);
1378 assert_eq!(&form.chunks[0].id, b"INFO");
1379 assert_eq!(form.chunks[0].data.len(), 10);
1380 }
1381
1382 #[test]
1383 fn truncated_chunk_is_error() {
1384 let mut data = minimal_djvu_bytes();
1385 let new_len = data.len() - 4;
1386 data.truncate(new_len);
1387
1388 let result = parse_form(&data);
1389 assert!(result.is_err());
1390 match result.unwrap_err() {
1391 IffError::ChunkTooLong { .. } | IffError::Truncated => {}
1392 other => panic!("expected ChunkTooLong or Truncated, got {:?}", other),
1393 }
1394 }
1395
1396 #[test]
1397 fn non_form_root_chunk_is_truncated_error() {
1398 let mut data = Vec::new();
1400 data.extend_from_slice(b"AT&T");
1401 data.extend_from_slice(b"INFO"); data.extend_from_slice(&10u32.to_be_bytes());
1403 data.extend_from_slice(&[0u8; 10]);
1404 assert_eq!(parse_form(&data).unwrap_err(), IffError::Truncated);
1405 }
1406
1407 #[test]
1408 fn form_too_short_for_secondary_id() {
1409 let mut data = Vec::new();
1412 data.extend_from_slice(b"AT&T");
1413 data.extend_from_slice(b"FORM");
1414 data.extend_from_slice(&3u32.to_be_bytes()); data.extend_from_slice(b"XYZ\x00"); assert_eq!(parse_form(&data).unwrap_err(), IffError::Truncated);
1417 }
1418
1419 #[test]
1420 fn sub_chunk_length_exceeds_body() {
1421 let mut body = Vec::new();
1424 body.extend_from_slice(b"DJVU"); body.extend_from_slice(b"INFO");
1426 body.extend_from_slice(&100u32.to_be_bytes()); body.extend_from_slice(&[0u8; 2]); let mut data = Vec::new();
1429 data.extend_from_slice(b"AT&T");
1430 data.extend_from_slice(b"FORM");
1431 data.extend_from_slice(&(body.len() as u32).to_be_bytes());
1432 data.extend_from_slice(&body);
1433 match parse_form(&data).unwrap_err() {
1434 IffError::ChunkTooLong { .. } => {}
1435 other => panic!("expected ChunkTooLong, got {other:?}"),
1436 }
1437 }
1438
1439 #[test]
1440 fn unknown_form_type_allowed() {
1441 let mut data = minimal_djvu_bytes();
1442 data[12] = b'X';
1443 data[13] = b'X';
1444 data[14] = b'X';
1445 data[15] = b'X';
1446
1447 let form = parse_form(&data).expect("unknown form type should still parse");
1448 assert_eq!(&form.form_type, b"XXXX");
1449 }
1450
1451 #[test]
1452 fn real_chicken_djvu_parses() {
1453 let path = assets_path().join("chicken.djvu");
1454 let data = std::fs::read(&path).expect("chicken.djvu must exist");
1455 let form = parse_form(&data).expect("chicken.djvu should parse");
1456
1457 assert_eq!(&form.form_type, b"DJVU");
1458 assert!(!form.chunks.is_empty(), "must have at least one chunk");
1459 assert_eq!(&form.chunks[0].id, b"INFO");
1460 assert!(form.chunks[0].data.len() >= 10);
1461 }
1462
1463 #[test]
1464 fn real_multipage_djvu_parses() {
1465 let path = assets_path().join("navm_fgbz.djvu");
1466 let data = std::fs::read(&path).expect("navm_fgbz.djvu must exist");
1467 let form = parse_form(&data).expect("navm_fgbz.djvu should parse");
1468
1469 assert_eq!(&form.form_type, b"DJVM");
1470 assert!(!form.chunks.is_empty());
1471 }
1472
1473 #[test]
1475 fn legacy_error_display_variants() {
1476 assert_eq!(
1477 LegacyError::UnexpectedEof.to_string(),
1478 "unexpected end of input"
1479 );
1480 assert_eq!(
1481 LegacyError::InvalidMagic.to_string(),
1482 "invalid magic number"
1483 );
1484 assert_eq!(LegacyError::InvalidLength.to_string(), "invalid length");
1485 assert_eq!(
1486 LegacyError::MissingChunk("INFO").to_string(),
1487 "missing required chunk: INFO"
1488 );
1489 assert_eq!(LegacyError::Unsupported("x").to_string(), "unsupported: x");
1490 assert_eq!(
1491 LegacyError::FormatError("y".to_string()).to_string(),
1492 "format error: y"
1493 );
1494 }
1495
1496 #[test]
1498 fn chunk_accessors_form_and_leaf() {
1499 let leaf = Chunk::Leaf {
1500 id: *b"INFO",
1501 data: vec![1, 2, 3],
1502 };
1503 let form = Chunk::Form {
1504 secondary_id: *b"DJVU",
1505 length: 10,
1506 children: vec![leaf.clone()],
1507 };
1508
1509 assert_eq!(form.data(), &[] as &[u8]);
1511 assert_eq!(leaf.data(), &[1u8, 2, 3]);
1512
1513 assert_eq!(form.children().len(), 1);
1515 assert!(leaf.children().is_empty());
1516
1517 assert_eq!(form.payload_length(), 10);
1519 assert_eq!(leaf.payload_length(), 3);
1520
1521 assert!(leaf.find_first(b"INFO").is_none());
1523
1524 let form2 = Chunk::Form {
1526 secondary_id: *b"DJVU",
1527 length: 0,
1528 children: vec![],
1529 };
1530 assert!(form2.find_first(b"INFO").is_none());
1531 }
1532
1533 #[test]
1534 fn find_all_returns_all_matching_leaves() {
1535 let leaf1 = Chunk::Leaf {
1536 id: *b"INFO",
1537 data: vec![1],
1538 };
1539 let leaf2 = Chunk::Leaf {
1540 id: *b"INFO",
1541 data: vec![2],
1542 };
1543 let leaf3 = Chunk::Leaf {
1544 id: *b"BG44",
1545 data: vec![3],
1546 };
1547 let child_form = Chunk::Form {
1549 secondary_id: *b"DJVU",
1550 length: 0,
1551 children: vec![],
1552 };
1553 let form = Chunk::Form {
1554 secondary_id: *b"DJVU",
1555 length: 0,
1556 children: vec![leaf1, leaf2, leaf3, child_form],
1557 };
1558 let all_info = form.find_all(b"INFO");
1559 assert_eq!(all_info.len(), 2);
1560 let all_bg44 = form.find_all(b"BG44");
1561 assert_eq!(all_bg44.len(), 1);
1562 let all_none = form.find_all(b"NONE");
1563 assert!(all_none.is_empty());
1564 }
1565
1566 #[test]
1567 fn find_first_skips_form_children() {
1568 let child_form = Chunk::Form {
1571 secondary_id: *b"DJVU",
1572 length: 0,
1573 children: vec![],
1574 };
1575 let leaf = Chunk::Leaf {
1576 id: *b"INFO",
1577 data: vec![42],
1578 };
1579 let form = Chunk::Form {
1580 secondary_id: *b"DJVU",
1581 length: 0,
1582 children: vec![child_form, leaf],
1583 };
1584 let found = form.find_first(b"INFO").expect("should find INFO");
1585 assert!(matches!(found, Chunk::Leaf { id, .. } if id == b"INFO"));
1586 }
1587
1588 #[test]
1589 fn parse_empty_input_returns_unexpected_eof() {
1590 assert!(matches!(parse(b""), Err(Error::UnexpectedEof)));
1592 assert!(matches!(parse(b"AT"), Err(Error::UnexpectedEof)));
1593 }
1594
1595 #[test]
1596 fn parse_form_length_too_small_returns_invalid_length() {
1597 let mut data = vec![];
1600 data.extend_from_slice(b"AT&T");
1601 data.extend_from_slice(b"FORM");
1602 data.extend_from_slice(&3u32.to_be_bytes()); data.extend_from_slice(b"XYZ");
1604 assert!(matches!(parse(&data), Err(Error::InvalidLength)));
1605 }
1606
1607 #[test]
1608 fn parse_children_skips_trailing_bytes() {
1609 let mut data = vec![];
1613 data.extend_from_slice(b"AT&T");
1614 data.extend_from_slice(b"FORM");
1615 let secondary_plus_junk = b"DJVU\x01\x02\x03\x04\x05"; data.extend_from_slice(&(secondary_plus_junk.len() as u32).to_be_bytes());
1617 data.extend_from_slice(secondary_plus_junk);
1618 let result = parse(&data);
1619 let djvu = result.expect("trailing bytes must not cause an error");
1621 assert!(matches!(djvu.root, Chunk::Form { .. }));
1622 assert!(djvu.root.children().is_empty());
1623 }
1624
1625 #[test]
1626 fn odd_length_chunk_padding() {
1627 let chunk1_data: &[u8] = &[0xAA, 0xBB, 0xCC, 0xDD, 0xEE]; let chunk2_data: &[u8] = &[0x01, 0x02]; let mut form_body: Vec<u8> = Vec::new();
1631 form_body.extend_from_slice(b"DJVU");
1632
1633 form_body.extend_from_slice(b"TST1");
1634 form_body.extend_from_slice(&5u32.to_be_bytes());
1635 form_body.extend_from_slice(chunk1_data);
1636 form_body.push(0x00); form_body.extend_from_slice(b"TST2");
1639 form_body.extend_from_slice(&2u32.to_be_bytes());
1640 form_body.extend_from_slice(chunk2_data);
1641
1642 let form_len = form_body.len() as u32;
1643
1644 let mut file: Vec<u8> = Vec::new();
1645 file.extend_from_slice(b"AT&T");
1646 file.extend_from_slice(b"FORM");
1647 file.extend_from_slice(&form_len.to_be_bytes());
1648 file.extend_from_slice(&form_body);
1649
1650 let form = parse_form(&file).expect("should parse padded chunk");
1651 assert_eq!(form.chunks.len(), 2);
1652 assert_eq!(&form.chunks[0].id, b"TST1");
1653 assert_eq!(form.chunks[0].data, chunk1_data);
1654 assert_eq!(&form.chunks[1].id, b"TST2");
1655 assert_eq!(form.chunks[1].data, chunk2_data);
1656 }
1657}