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
563pub fn framed_size(chunk: &Chunk) -> usize {
568 emitted_size_inner(chunk, true)
569}
570
571fn emitted_size_inner(chunk: &Chunk, suppress_inner_pad: bool) -> usize {
572 match chunk {
573 Chunk::Form {
574 length: stored_length,
575 children,
576 ..
577 } => {
578 let suppress_last_pad = (*stored_length & 1) == 1;
579 let n = children.len();
580 let mut payload = 4usize; for (i, child) in children.iter().enumerate() {
582 let last = i + 1 == n;
583 payload += emitted_size_inner(child, last && suppress_last_pad);
584 }
585 let total = 8 + payload;
586 total + usize::from(!suppress_inner_pad && total % 2 == 1)
587 }
588 Chunk::Leaf { data, .. } => {
589 let total = 8 + data.len();
590 total + usize::from(!suppress_inner_pad && total % 2 == 1)
591 }
592 }
593}
594
595pub enum EmitPart<'a> {
598 Chunk(&'a Chunk),
601 Verbatim(&'a [u8]),
606 Form(&'a [u8]),
613}
614
615pub fn partial_emit(secondary_id: ChunkId, parts: &[EmitPart<'_>]) -> Option<Vec<u8>> {
626 partial_emit_with_offsets(secondary_id, parts).map(|(bytes, _)| bytes)
627}
628
629pub fn partial_emit_with_offsets(
647 secondary_id: ChunkId,
648 parts: &[EmitPart<'_>],
649) -> Option<(Vec<u8>, Vec<usize>)> {
650 const PROLOGUE: usize = 12;
654 let mut payload = Vec::new();
655 payload.extend_from_slice(&secondary_id); let mut offsets = Vec::with_capacity(parts.len());
657 for part in parts {
658 offsets.push(PROLOGUE + payload.len());
659 match part {
660 EmitPart::Chunk(chunk) => emit_chunk(chunk, &mut payload),
661 EmitPart::Verbatim(bytes) => {
662 payload.extend_from_slice(bytes);
663 if payload.len() % 2 == 1 {
664 payload.push(0);
665 }
666 }
667 EmitPart::Form(body) => {
668 let len = u32::try_from(body.len()).ok()?;
669 payload.extend_from_slice(b"FORM");
670 payload.extend_from_slice(&len.to_be_bytes());
671 payload.extend_from_slice(body);
672 if payload.len() % 2 == 1 {
673 payload.push(0);
674 }
675 }
676 }
677 }
678 let len = u32::try_from(payload.len()).ok()?;
679 let mut out = Vec::with_capacity(8 + payload.len());
680 out.extend_from_slice(&MAGIC);
681 out.extend_from_slice(b"FORM");
682 out.extend_from_slice(&len.to_be_bytes());
683 out.extend_from_slice(&payload);
684 if (8 + payload.len()) % 2 == 1 {
687 out.push(0);
688 }
689 Some((out, offsets))
690}
691
692#[derive(Debug, Clone, Copy)]
700pub struct IffChunk<'a> {
701 pub id: [u8; 4],
703 pub data: &'a [u8],
705}
706
707#[derive(Debug)]
709pub struct Form<'a> {
710 pub form_type: [u8; 4],
712 pub chunks: Vec<IffChunk<'a>>,
714}
715
716pub fn parse_form(data: &[u8]) -> Result<Form<'_>, IffError> {
728 if data.len() < 16 {
730 return Err(IffError::TooShort);
731 }
732
733 let magic = read_4(data, 0)?;
735 if &magic != b"AT&T" {
736 return Err(IffError::BadMagic { got: magic });
737 }
738
739 let form_id = read_4(data, 4)?;
741 if &form_id != b"FORM" {
742 return Err(IffError::Truncated);
743 }
744
745 let form_len = read_u32_be(data, 8)? as usize;
747
748 let form_data_end = 12_usize.checked_add(form_len).ok_or(IffError::Truncated)?;
750 if form_data_end > data.len() {
751 return Err(IffError::ChunkTooLong {
752 id: *b"FORM",
753 claimed: form_len as u32,
754 available: data.len().saturating_sub(12),
755 });
756 }
757
758 if form_len < 4 {
760 return Err(IffError::Truncated);
761 }
762 let form_type = read_4(data, 12)?;
763
764 let body = data.get(16..form_data_end).ok_or(IffError::Truncated)?;
766
767 let chunks = parse_form_body(body)?;
768
769 Ok(Form { form_type, chunks })
770}
771
772pub fn parse_form_body(mut buf: &[u8]) -> Result<Vec<IffChunk<'_>>, IffError> {
782 let mut chunks = Vec::new();
783
784 while buf.len() >= 8 {
785 let id = read_4(buf, 0)?;
786 let data_len = read_u32_be(buf, 4)? as usize;
787
788 let data_start = 8_usize;
789 let data_end = data_start
790 .checked_add(data_len)
791 .ok_or(IffError::Truncated)?;
792
793 if data_end > buf.len() {
794 return Err(IffError::ChunkTooLong {
795 id,
796 claimed: data_len as u32,
797 available: buf.len().saturating_sub(data_start),
798 });
799 }
800
801 let chunk_data = buf.get(data_start..data_end).ok_or(IffError::Truncated)?;
802 chunks.push(IffChunk {
803 id,
804 data: chunk_data,
805 });
806
807 let padded_len = data_len + (data_len & 1);
809 let next = data_start
810 .checked_add(padded_len)
811 .ok_or(IffError::Truncated)?;
812
813 buf = buf.get(next.min(buf.len())..).ok_or(IffError::Truncated)?;
815 }
816
817 Ok(chunks)
818}
819
820#[inline]
822fn read_4(data: &[u8], offset: usize) -> Result<[u8; 4], IffError> {
823 data.get(offset..offset + 4)
824 .and_then(|s| s.try_into().ok())
825 .ok_or(IffError::Truncated)
826}
827
828#[inline]
830fn read_u32_be(data: &[u8], offset: usize) -> Result<u32, IffError> {
831 let b = read_4(data, offset)?;
832 Ok(u32::from_be_bytes(b))
833}
834
835pub fn dump(file: &DjvuFile) -> String {
842 let mut out = String::new();
843 dump_chunk(&file.root, 1, &mut out);
844 out
845}
846
847fn dump_chunk(chunk: &Chunk, depth: usize, out: &mut String) {
848 for _ in 0..depth {
849 out.push_str(" ");
850 }
851 match chunk {
852 Chunk::Form {
853 secondary_id,
854 length,
855 children,
856 } => {
857 let sec = str::from_utf8(secondary_id).unwrap_or("????");
858 let _ = writeln!(out, "FORM:{sec} [{length}] ");
859 for child in children {
860 dump_chunk(child, depth + 1, out);
861 }
862 }
863 Chunk::Leaf { id, data } => {
864 let id_str = str::from_utf8(id).unwrap_or("????");
865 let _ = writeln!(out, "{id_str} [{}] ", data.len());
866 }
867 }
868}
869
870#[cfg(test)]
871mod tests {
872 use super::*;
873
874 #[test]
879 fn overflowing_chunk_length_reports_chunk_too_long_with_identity() {
880 let mut buf = Vec::new();
882 buf.extend_from_slice(&MAGIC);
883 buf.extend_from_slice(b"FORM");
884 let inner_len = 4u32 + 8; buf.extend_from_slice(&inner_len.to_be_bytes());
886 buf.extend_from_slice(b"DJVU");
887 buf.extend_from_slice(b"BG44");
888 buf.extend_from_slice(&u32::MAX.to_be_bytes());
889
890 match walk_chunks(&buf) {
891 Err(IffError::ChunkTooLong { id, claimed, .. }) => {
892 assert_eq!(&id, b"BG44");
893 assert_eq!(claimed, u32::MAX);
894 }
895 other => panic!("expected ChunkTooLong, got {other:?}"),
896 }
897 }
898
899 #[test]
902 fn deeply_nested_forms_are_rejected_not_overflow() {
903 let mut buf = Vec::new();
905 buf.extend_from_slice(b"FORM");
906 buf.extend_from_slice(&4u32.to_be_bytes());
907 buf.extend_from_slice(b"DJVU");
908 for _ in 0..200 {
909 let inner = buf;
910 let len = 4 + inner.len();
911 let mut outer = Vec::new();
912 outer.extend_from_slice(b"FORM");
913 outer.extend_from_slice(&(len as u32).to_be_bytes());
914 outer.extend_from_slice(b"DJVU");
915 outer.extend_from_slice(&inner);
916 buf = outer;
917 }
918 let mut full = Vec::from(*b"AT&T");
919 full.extend_from_slice(&buf);
920 assert!(
921 parse(&full).is_err(),
922 "deep nesting must error, not overflow"
923 );
924 }
925
926 #[test]
929 fn overflowing_chunk_length_is_rejected() {
930 let mut data = Vec::from(*b"AT&T");
931 data.extend_from_slice(b"JUNK");
932 data.extend_from_slice(&u32::MAX.to_be_bytes()); data.extend_from_slice(b"\x00\x00");
934 assert!(parse(&data).is_err());
935 }
936
937 #[test]
938 fn walk_chunks_reports_nested_offsets_lengths_and_paths() {
939 let mut data = Vec::from(*b"AT&T");
943 data.extend_from_slice(b"FORM");
944 data.extend_from_slice(&36u32.to_be_bytes());
945 data.extend_from_slice(b"DJVM");
946 data.extend_from_slice(b"DIRM");
947 data.extend_from_slice(&2u32.to_be_bytes());
948 data.extend_from_slice(b"id");
949 data.extend_from_slice(b"FORM");
950 data.extend_from_slice(&14u32.to_be_bytes());
951 data.extend_from_slice(b"DJVU");
952 data.extend_from_slice(b"INFO");
953 data.extend_from_slice(&2u32.to_be_bytes());
954 data.extend_from_slice(b"xy");
955
956 let records = walk_chunks(&data).expect("synthetic document walks");
957 let dirm_offset = 16;
958 let nested_form_offset = dirm_offset + 8 + 2;
959 let info_offset = nested_form_offset + 8 + 4;
960
961 assert_eq!(
962 records,
963 vec![
964 ChunkRecord {
965 id: *b"FORM",
966 form_type: Some(*b"DJVM"),
967 offset: 4,
968 length: 36,
969 depth: 0,
970 path: vec![],
971 },
972 ChunkRecord {
973 id: *b"DIRM",
974 form_type: None,
975 offset: dirm_offset,
976 length: 2,
977 depth: 1,
978 path: vec![0],
979 },
980 ChunkRecord {
981 id: *b"FORM",
982 form_type: Some(*b"DJVU"),
983 offset: nested_form_offset,
984 length: 14,
985 depth: 1,
986 path: vec![1],
987 },
988 ChunkRecord {
989 id: *b"INFO",
990 form_type: None,
991 offset: info_offset,
992 length: 2,
993 depth: 2,
994 path: vec![1, 0],
995 },
996 ]
997 );
998 }
999
1000 #[test]
1001 fn walk_chunks_offsets_match_real_bundled_fixture() {
1002 let data = std::fs::read(
1003 std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
1004 .join("../../tests/fixtures/DjVu3Spec_bundled.djvu"),
1005 )
1006 .expect("bundled fixture exists");
1007 let parsed = parse_form(&data).expect("fixture parses");
1008 let records = walk_chunks(&data).expect("fixture walks");
1009
1010 let payload_offset = |payload: &[u8]| payload.as_ptr() as usize - data.as_ptr() as usize;
1011 let dirm = parsed
1012 .chunks
1013 .iter()
1014 .find(|chunk| chunk.id == *b"DIRM")
1015 .expect("fixture has DIRM");
1016 let component = parsed
1017 .chunks
1018 .iter()
1019 .find(|chunk| chunk.id == *b"FORM")
1020 .expect("fixture has an embedded component FORM");
1021
1022 let dirm_record = records
1023 .iter()
1024 .find(|record| record.id == *b"DIRM")
1025 .expect("walk reports DIRM");
1026 assert_eq!(dirm_record.offset, payload_offset(dirm.data) - 8);
1027 assert_eq!(dirm_record.length, dirm.data.len());
1028
1029 let component_record = records
1030 .iter()
1031 .find(|record| record.depth == 1 && record.id == *b"FORM")
1032 .expect("walk reports embedded component FORM");
1033 assert_eq!(component_record.offset, payload_offset(component.data) - 8);
1034 assert_eq!(component_record.length, component.data.len());
1035 assert_eq!(component_record.path.len(), 1);
1036 }
1037
1038 fn assets_path() -> std::path::PathBuf {
1039 std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
1040 .join("../../references/djvujs/library/assets")
1041 }
1042
1043 fn golden_path() -> std::path::PathBuf {
1044 std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../tests/golden/iff")
1045 }
1046
1047 fn normalize_dump(input: &str) -> Vec<String> {
1051 input
1052 .lines()
1053 .filter(|l| !l.trim().is_empty())
1054 .map(|line| {
1055 let trimmed = line.trim_end();
1056 if let Some(bracket_end) = trimmed.find(']') {
1057 let structural = &trimmed[..=bracket_end];
1058 structural.trim_end().to_string()
1059 } else {
1060 trimmed.to_string()
1061 }
1062 })
1063 .collect()
1064 }
1065
1066 fn assert_structure_matches(djvu_file: &str, golden_file: &str) {
1067 let data = std::fs::read(assets_path().join(djvu_file)).unwrap();
1068 let file = parse(&data).unwrap();
1069 let actual = dump(&file);
1070 let expected = std::fs::read_to_string(golden_path().join(golden_file)).unwrap();
1071
1072 let actual_lines = normalize_dump(&actual);
1073 let expected_lines = normalize_dump(&expected);
1074
1075 assert_eq!(
1076 actual_lines.len(),
1077 expected_lines.len(),
1078 "Line count mismatch for {} ({} vs {})",
1079 djvu_file,
1080 actual_lines.len(),
1081 expected_lines.len()
1082 );
1083
1084 for (i, (a, e)) in actual_lines.iter().zip(expected_lines.iter()).enumerate() {
1085 assert_eq!(
1086 a,
1087 e,
1088 "Line {} mismatch for {}\n actual: {:?}\n expected: {:?}",
1089 i + 1,
1090 djvu_file,
1091 a,
1092 e
1093 );
1094 }
1095 }
1096
1097 #[test]
1098 fn parse_boy_jb2_legacy() {
1099 let data = std::fs::read(assets_path().join("boy_jb2.djvu")).unwrap();
1100 let file = parse(&data).unwrap();
1101
1102 match &file.root {
1103 Chunk::Form {
1104 secondary_id,
1105 children,
1106 ..
1107 } => {
1108 assert_eq!(secondary_id, b"DJVU");
1109 assert_eq!(children.len(), 2);
1110 }
1111 _ => panic!("expected FORM root"),
1112 }
1113 }
1114
1115 #[test]
1116 fn structure_boy_jb2() {
1117 assert_structure_matches("boy_jb2.djvu", "boy_jb2.dump");
1118 }
1119
1120 #[test]
1121 fn structure_boy() {
1122 assert_structure_matches("boy.djvu", "boy.dump");
1123 }
1124
1125 #[test]
1126 fn structure_chicken() {
1127 assert_structure_matches("chicken.djvu", "chicken.dump");
1128 }
1129
1130 #[test]
1131 fn structure_carte() {
1132 assert_structure_matches("carte.djvu", "carte.dump");
1133 }
1134
1135 #[test]
1136 fn structure_navm_fgbz() {
1137 assert_structure_matches("navm_fgbz.djvu", "navm_fgbz.dump");
1138 }
1139
1140 #[test]
1141 fn structure_colorbook() {
1142 assert_structure_matches("colorbook.djvu", "colorbook.dump");
1143 }
1144
1145 #[test]
1146 fn structure_djvu3spec_bundled() {
1147 assert_structure_matches("DjVu3Spec_bundled.djvu", "djvu3spec_bundled.dump");
1148 }
1149
1150 #[test]
1151 fn structure_big_scanned_page() {
1152 assert_structure_matches("big-scanned-page.djvu", "big_scanned_page.dump");
1153 }
1154
1155 fn assert_emitted_size_matches_emit(name: &str) {
1162 let Ok(data) = std::fs::read(assets_path().join(name)) else {
1163 return; };
1165 let file = parse(&data).unwrap();
1166 let emitted = emit(&file);
1167 assert_eq!(
1168 emitted_size(&file.root),
1169 emitted.len() - 4,
1170 "emitted_size disagrees with emit() for {name}"
1171 );
1172 }
1173
1174 #[test]
1175 fn emitted_size_matches_emit_corpus() {
1176 for name in [
1177 "boy_jb2.djvu",
1178 "boy.djvu",
1179 "chicken.djvu",
1180 "carte.djvu",
1181 "navm_fgbz.djvu",
1182 "colorbook.djvu",
1183 "DjVu3Spec_bundled.djvu",
1184 "big-scanned-page.djvu",
1185 ] {
1186 assert_emitted_size_matches_emit(name);
1187 }
1188 }
1189
1190 #[test]
1191 fn partial_emit_verbatim_matches_chunk_framing() {
1192 let tree = DjvuFile {
1198 root: Chunk::Form {
1199 secondary_id: *b"DJVU",
1200 length: 0,
1201 children: vec![
1202 Chunk::Leaf {
1203 id: *b"INFO",
1204 data: vec![0xAA; 5], },
1206 Chunk::Leaf {
1207 id: *b"Sjbz",
1208 data: vec![0xBB; 4], },
1210 ],
1211 },
1212 };
1213 let canonical = emit(&tree); let Chunk::Form { children, .. } = &tree.root else {
1216 unreachable!()
1217 };
1218 let mut info_bytes = Vec::new();
1220 emit_chunk(&children[0], &mut info_bytes);
1221 let mut sjbz_bytes = Vec::new();
1222 emit_chunk(&children[1], &mut sjbz_bytes);
1223
1224 let via_verbatim = partial_emit(
1225 *b"DJVU",
1226 &[
1227 EmitPart::Verbatim(&info_bytes),
1228 EmitPart::Verbatim(&sjbz_bytes),
1229 ],
1230 )
1231 .expect("fits in u32");
1232 let via_chunks = partial_emit(
1233 *b"DJVU",
1234 &[EmitPart::Chunk(&children[0]), EmitPart::Chunk(&children[1])],
1235 )
1236 .expect("fits in u32");
1237
1238 assert_eq!(via_verbatim, canonical, "verbatim path must match emit");
1239 assert_eq!(via_chunks, canonical, "chunk path must match emit");
1240 }
1241
1242 #[test]
1243 fn partial_emit_pads_odd_verbatim_child() {
1244 let parts = [EmitPart::Verbatim(&[1u8, 2, 3])];
1247 let out = partial_emit(*b"DJVU", &parts).unwrap();
1248 assert_eq!(out.len(), 20);
1250 assert_eq!(&out[..8], b"AT&TFORM");
1251 assert_eq!(u32::from_be_bytes(out[8..12].try_into().unwrap()), 8);
1253 assert_eq!(&out[12..16], b"DJVU");
1254 assert_eq!(&out[16..19], &[1, 2, 3]);
1255 assert_eq!(out[19], 0);
1256 }
1257
1258 #[test]
1259 fn partial_emit_form_part_frames_nested_form() {
1260 let body: &[u8] = b"DJVUxyz"; let via_form = partial_emit(*b"DJVM", &[EmitPart::Form(body)]).unwrap();
1264
1265 let mut framed = Vec::new();
1267 framed.extend_from_slice(b"FORM");
1268 framed.extend_from_slice(&(body.len() as u32).to_be_bytes());
1269 framed.extend_from_slice(body);
1270 framed.push(0); let via_verbatim = partial_emit(*b"DJVM", &[EmitPart::Verbatim(&framed)]).unwrap();
1272
1273 assert_eq!(via_form, via_verbatim, "Form part must match framed FORM");
1274 assert_eq!(&via_form[..8], b"AT&TFORM");
1276 assert_eq!(&via_form[12..16], b"DJVM");
1277 assert_eq!(&via_form[16..20], b"FORM");
1278 assert_eq!(u32::from_be_bytes(via_form[20..24].try_into().unwrap()), 7);
1279 assert_eq!(&via_form[24..31], body);
1280 assert_eq!(via_form[31], 0); }
1282
1283 #[test]
1284 fn partial_emit_with_offsets_reports_part_starts() {
1285 let dirm = Chunk::Leaf {
1288 id: *b"DIRM",
1289 data: vec![0xAB; 5], };
1291 let comp0: &[u8] = b"DJVU0000"; let comp1: &[u8] = b"DJVIaa"; let parts = [
1294 EmitPart::Chunk(&dirm),
1295 EmitPart::Form(comp0),
1296 EmitPart::Form(comp1),
1297 ];
1298 let (bytes, offsets) = partial_emit_with_offsets(*b"DJVM", &parts).unwrap();
1299
1300 assert_eq!(offsets.len(), 3);
1301 assert_eq!(offsets[0], 16);
1303 assert_eq!(&bytes[offsets[0]..offsets[0] + 4], b"DIRM");
1304 for &off in &offsets[1..] {
1306 assert_eq!(&bytes[off..off + 4], b"FORM", "offset must point at FORM");
1307 }
1308 assert_eq!(offsets[2] - offsets[1], 16);
1310 }
1311
1312 fn minimal_djvu_bytes() -> Vec<u8> {
1316 let info_data: &[u8] = &[
1317 0x00, 0xB5, 0x00, 0xF0, 0x18, 0x00, 0x64, 0x00, 0x16, 0x00, ];
1325 let info_len = info_data.len() as u32;
1326
1327 let mut chunk = Vec::new();
1328 chunk.extend_from_slice(b"INFO");
1329 chunk.extend_from_slice(&info_len.to_be_bytes());
1330 chunk.extend_from_slice(info_data);
1331
1332 let mut form_body = Vec::new();
1333 form_body.extend_from_slice(b"DJVU");
1334 form_body.extend_from_slice(&chunk);
1335
1336 let form_len = form_body.len() as u32;
1337
1338 let mut file = Vec::new();
1339 file.extend_from_slice(b"AT&T");
1340 file.extend_from_slice(b"FORM");
1341 file.extend_from_slice(&form_len.to_be_bytes());
1342 file.extend_from_slice(&form_body);
1343
1344 file
1345 }
1346
1347 #[test]
1348 fn empty_input_is_error() {
1349 let result = parse_form(&[]);
1350 assert!(result.is_err());
1351 assert_eq!(result.unwrap_err(), IffError::TooShort);
1352 }
1353
1354 #[test]
1355 fn short_input_is_error() {
1356 let result = parse_form(&[0u8; 10]);
1357 assert!(result.is_err());
1358 assert_eq!(result.unwrap_err(), IffError::TooShort);
1359 }
1360
1361 #[test]
1362 fn bad_magic_is_error() {
1363 let mut data = minimal_djvu_bytes();
1364 data[0] = 0xFF;
1365 data[1] = 0xFF;
1366 data[2] = 0xFF;
1367 data[3] = 0xFF;
1368
1369 let result = parse_form(&data);
1370 assert!(result.is_err());
1371 assert_eq!(
1372 result.unwrap_err(),
1373 IffError::BadMagic {
1374 got: [0xFF, 0xFF, 0xFF, 0xFF]
1375 }
1376 );
1377 }
1378
1379 #[test]
1380 fn valid_single_page_parses() {
1381 let data = minimal_djvu_bytes();
1382 let form = parse_form(&data).expect("should parse successfully");
1383
1384 assert_eq!(&form.form_type, b"DJVU");
1385 assert_eq!(form.chunks.len(), 1);
1386 assert_eq!(&form.chunks[0].id, b"INFO");
1387 assert_eq!(form.chunks[0].data.len(), 10);
1388 }
1389
1390 #[test]
1391 fn truncated_chunk_is_error() {
1392 let mut data = minimal_djvu_bytes();
1393 let new_len = data.len() - 4;
1394 data.truncate(new_len);
1395
1396 let result = parse_form(&data);
1397 assert!(result.is_err());
1398 match result.unwrap_err() {
1399 IffError::ChunkTooLong { .. } | IffError::Truncated => {}
1400 other => panic!("expected ChunkTooLong or Truncated, got {:?}", other),
1401 }
1402 }
1403
1404 #[test]
1405 fn non_form_root_chunk_is_truncated_error() {
1406 let mut data = Vec::new();
1408 data.extend_from_slice(b"AT&T");
1409 data.extend_from_slice(b"INFO"); data.extend_from_slice(&10u32.to_be_bytes());
1411 data.extend_from_slice(&[0u8; 10]);
1412 assert_eq!(parse_form(&data).unwrap_err(), IffError::Truncated);
1413 }
1414
1415 #[test]
1416 fn form_too_short_for_secondary_id() {
1417 let mut data = Vec::new();
1420 data.extend_from_slice(b"AT&T");
1421 data.extend_from_slice(b"FORM");
1422 data.extend_from_slice(&3u32.to_be_bytes()); data.extend_from_slice(b"XYZ\x00"); assert_eq!(parse_form(&data).unwrap_err(), IffError::Truncated);
1425 }
1426
1427 #[test]
1428 fn sub_chunk_length_exceeds_body() {
1429 let mut body = Vec::new();
1432 body.extend_from_slice(b"DJVU"); body.extend_from_slice(b"INFO");
1434 body.extend_from_slice(&100u32.to_be_bytes()); body.extend_from_slice(&[0u8; 2]); let mut data = Vec::new();
1437 data.extend_from_slice(b"AT&T");
1438 data.extend_from_slice(b"FORM");
1439 data.extend_from_slice(&(body.len() as u32).to_be_bytes());
1440 data.extend_from_slice(&body);
1441 match parse_form(&data).unwrap_err() {
1442 IffError::ChunkTooLong { .. } => {}
1443 other => panic!("expected ChunkTooLong, got {other:?}"),
1444 }
1445 }
1446
1447 #[test]
1448 fn unknown_form_type_allowed() {
1449 let mut data = minimal_djvu_bytes();
1450 data[12] = b'X';
1451 data[13] = b'X';
1452 data[14] = b'X';
1453 data[15] = b'X';
1454
1455 let form = parse_form(&data).expect("unknown form type should still parse");
1456 assert_eq!(&form.form_type, b"XXXX");
1457 }
1458
1459 #[test]
1460 fn real_chicken_djvu_parses() {
1461 let path = assets_path().join("chicken.djvu");
1462 let data = std::fs::read(&path).expect("chicken.djvu must exist");
1463 let form = parse_form(&data).expect("chicken.djvu should parse");
1464
1465 assert_eq!(&form.form_type, b"DJVU");
1466 assert!(!form.chunks.is_empty(), "must have at least one chunk");
1467 assert_eq!(&form.chunks[0].id, b"INFO");
1468 assert!(form.chunks[0].data.len() >= 10);
1469 }
1470
1471 #[test]
1472 fn real_multipage_djvu_parses() {
1473 let path = assets_path().join("navm_fgbz.djvu");
1474 let data = std::fs::read(&path).expect("navm_fgbz.djvu must exist");
1475 let form = parse_form(&data).expect("navm_fgbz.djvu should parse");
1476
1477 assert_eq!(&form.form_type, b"DJVM");
1478 assert!(!form.chunks.is_empty());
1479 }
1480
1481 #[test]
1483 fn legacy_error_display_variants() {
1484 assert_eq!(
1485 LegacyError::UnexpectedEof.to_string(),
1486 "unexpected end of input"
1487 );
1488 assert_eq!(
1489 LegacyError::InvalidMagic.to_string(),
1490 "invalid magic number"
1491 );
1492 assert_eq!(LegacyError::InvalidLength.to_string(), "invalid length");
1493 assert_eq!(
1494 LegacyError::MissingChunk("INFO").to_string(),
1495 "missing required chunk: INFO"
1496 );
1497 assert_eq!(LegacyError::Unsupported("x").to_string(), "unsupported: x");
1498 assert_eq!(
1499 LegacyError::FormatError("y".to_string()).to_string(),
1500 "format error: y"
1501 );
1502 }
1503
1504 #[test]
1506 fn chunk_accessors_form_and_leaf() {
1507 let leaf = Chunk::Leaf {
1508 id: *b"INFO",
1509 data: vec![1, 2, 3],
1510 };
1511 let form = Chunk::Form {
1512 secondary_id: *b"DJVU",
1513 length: 10,
1514 children: vec![leaf.clone()],
1515 };
1516
1517 assert_eq!(form.data(), &[] as &[u8]);
1519 assert_eq!(leaf.data(), &[1u8, 2, 3]);
1520
1521 assert_eq!(form.children().len(), 1);
1523 assert!(leaf.children().is_empty());
1524
1525 assert_eq!(form.payload_length(), 10);
1527 assert_eq!(leaf.payload_length(), 3);
1528
1529 assert!(leaf.find_first(b"INFO").is_none());
1531
1532 let form2 = Chunk::Form {
1534 secondary_id: *b"DJVU",
1535 length: 0,
1536 children: vec![],
1537 };
1538 assert!(form2.find_first(b"INFO").is_none());
1539 }
1540
1541 #[test]
1542 fn find_all_returns_all_matching_leaves() {
1543 let leaf1 = Chunk::Leaf {
1544 id: *b"INFO",
1545 data: vec![1],
1546 };
1547 let leaf2 = Chunk::Leaf {
1548 id: *b"INFO",
1549 data: vec![2],
1550 };
1551 let leaf3 = Chunk::Leaf {
1552 id: *b"BG44",
1553 data: vec![3],
1554 };
1555 let child_form = Chunk::Form {
1557 secondary_id: *b"DJVU",
1558 length: 0,
1559 children: vec![],
1560 };
1561 let form = Chunk::Form {
1562 secondary_id: *b"DJVU",
1563 length: 0,
1564 children: vec![leaf1, leaf2, leaf3, child_form],
1565 };
1566 let all_info = form.find_all(b"INFO");
1567 assert_eq!(all_info.len(), 2);
1568 let all_bg44 = form.find_all(b"BG44");
1569 assert_eq!(all_bg44.len(), 1);
1570 let all_none = form.find_all(b"NONE");
1571 assert!(all_none.is_empty());
1572 }
1573
1574 #[test]
1575 fn find_first_skips_form_children() {
1576 let child_form = Chunk::Form {
1579 secondary_id: *b"DJVU",
1580 length: 0,
1581 children: vec![],
1582 };
1583 let leaf = Chunk::Leaf {
1584 id: *b"INFO",
1585 data: vec![42],
1586 };
1587 let form = Chunk::Form {
1588 secondary_id: *b"DJVU",
1589 length: 0,
1590 children: vec![child_form, leaf],
1591 };
1592 let found = form.find_first(b"INFO").expect("should find INFO");
1593 assert!(matches!(found, Chunk::Leaf { id, .. } if id == b"INFO"));
1594 }
1595
1596 #[test]
1597 fn parse_empty_input_returns_unexpected_eof() {
1598 assert!(matches!(parse(b""), Err(Error::UnexpectedEof)));
1600 assert!(matches!(parse(b"AT"), Err(Error::UnexpectedEof)));
1601 }
1602
1603 #[test]
1604 fn parse_form_length_too_small_returns_invalid_length() {
1605 let mut data = vec![];
1608 data.extend_from_slice(b"AT&T");
1609 data.extend_from_slice(b"FORM");
1610 data.extend_from_slice(&3u32.to_be_bytes()); data.extend_from_slice(b"XYZ");
1612 assert!(matches!(parse(&data), Err(Error::InvalidLength)));
1613 }
1614
1615 #[test]
1616 fn parse_children_skips_trailing_bytes() {
1617 let mut data = vec![];
1621 data.extend_from_slice(b"AT&T");
1622 data.extend_from_slice(b"FORM");
1623 let secondary_plus_junk = b"DJVU\x01\x02\x03\x04\x05"; data.extend_from_slice(&(secondary_plus_junk.len() as u32).to_be_bytes());
1625 data.extend_from_slice(secondary_plus_junk);
1626 let result = parse(&data);
1627 let djvu = result.expect("trailing bytes must not cause an error");
1629 assert!(matches!(djvu.root, Chunk::Form { .. }));
1630 assert!(djvu.root.children().is_empty());
1631 }
1632
1633 #[test]
1634 fn odd_length_chunk_padding() {
1635 let chunk1_data: &[u8] = &[0xAA, 0xBB, 0xCC, 0xDD, 0xEE]; let chunk2_data: &[u8] = &[0x01, 0x02]; let mut form_body: Vec<u8> = Vec::new();
1639 form_body.extend_from_slice(b"DJVU");
1640
1641 form_body.extend_from_slice(b"TST1");
1642 form_body.extend_from_slice(&5u32.to_be_bytes());
1643 form_body.extend_from_slice(chunk1_data);
1644 form_body.push(0x00); form_body.extend_from_slice(b"TST2");
1647 form_body.extend_from_slice(&2u32.to_be_bytes());
1648 form_body.extend_from_slice(chunk2_data);
1649
1650 let form_len = form_body.len() as u32;
1651
1652 let mut file: Vec<u8> = Vec::new();
1653 file.extend_from_slice(b"AT&T");
1654 file.extend_from_slice(b"FORM");
1655 file.extend_from_slice(&form_len.to_be_bytes());
1656 file.extend_from_slice(&form_body);
1657
1658 let form = parse_form(&file).expect("should parse padded chunk");
1659 assert_eq!(form.chunks.len(), 2);
1660 assert_eq!(&form.chunks[0].id, b"TST1");
1661 assert_eq!(form.chunks[0].data, chunk1_data);
1662 assert_eq!(&form.chunks[1].id, b"TST2");
1663 assert_eq!(form.chunks[1].data, chunk2_data);
1664 }
1665}