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};
36#[cfg(feature = "std")]
37use std::{string::String, vec::Vec};
38
39#[derive(Debug, thiserror::Error, PartialEq, Eq)]
43pub enum IffError {
44 #[error("input is too short to be a valid IFF file")]
46 TooShort,
47
48 #[error("bad magic bytes: expected AT&T, got {got:?}")]
50 BadMagic { got: [u8; 4] },
51
52 #[error("unknown FORM type: {id:?}")]
57 UnknownFormType { id: [u8; 4] },
58
59 #[error(
61 "chunk {:?} claims {} bytes but only {} are available",
62 id,
63 claimed,
64 available
65 )]
66 ChunkTooLong {
67 id: [u8; 4],
68 claimed: u32,
69 available: usize,
70 },
71
72 #[error("unexpected end of input (truncated IFF data)")]
74 Truncated,
75}
76
77#[derive(Debug, Clone, PartialEq, Eq)]
79pub enum LegacyError {
80 UnexpectedEof,
82 InvalidMagic,
84 InvalidLength,
86 MissingChunk(&'static str),
88 Unsupported(&'static str),
90 FormatError(String),
92}
93
94impl core::fmt::Display for LegacyError {
95 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
96 match self {
97 LegacyError::UnexpectedEof => write!(f, "unexpected end of input"),
98 LegacyError::InvalidMagic => write!(f, "invalid magic number"),
99 LegacyError::InvalidLength => write!(f, "invalid length"),
100 LegacyError::MissingChunk(id) => write!(f, "missing required chunk: {}", id),
101 LegacyError::Unsupported(msg) => write!(f, "unsupported: {}", msg),
102 LegacyError::FormatError(msg) => write!(f, "format error: {}", msg),
103 }
104 }
105}
106
107#[cfg(feature = "std")]
108impl std::error::Error for LegacyError {}
109
110pub use LegacyError as Error;
112
113pub const MAGIC: [u8; 4] = *b"AT&T";
121
122pub type ChunkId = [u8; 4];
124
125#[derive(Debug, Clone)]
127pub enum Chunk {
128 Form {
130 secondary_id: ChunkId,
132 length: u32,
135 children: Vec<Chunk>,
137 },
138 Leaf {
140 id: ChunkId,
142 data: Vec<u8>,
144 },
145}
146
147impl Chunk {
148 pub fn data(&self) -> &[u8] {
150 match self {
151 Chunk::Form { .. } => &[],
152 Chunk::Leaf { data, .. } => data,
153 }
154 }
155
156 pub fn children(&self) -> &[Chunk] {
158 match self {
159 Chunk::Form { children, .. } => children,
160 Chunk::Leaf { .. } => &[],
161 }
162 }
163
164 pub fn payload_length(&self) -> u32 {
170 match self {
171 Chunk::Form { length, .. } => *length,
172 Chunk::Leaf { data, .. } => data.len() as u32,
173 }
174 }
175
176 pub fn find_first(&self, target_id: &[u8; 4]) -> Option<&Chunk> {
178 self.children().iter().find(|c| match c {
179 Chunk::Leaf { id, .. } => id == target_id,
180 _ => false,
181 })
182 }
183
184 pub fn find_all(&self, target_id: &[u8; 4]) -> Vec<&Chunk> {
186 self.children()
187 .iter()
188 .filter(|c| match c {
189 Chunk::Leaf { id, .. } => id == target_id,
190 _ => false,
191 })
192 .collect()
193 }
194}
195
196#[derive(Debug, Clone)]
198pub struct DjvuFile {
199 pub root: Chunk,
200}
201
202pub fn parse(data: &[u8]) -> Result<DjvuFile, Error> {
206 if data.len() < 4 {
207 return Err(Error::UnexpectedEof);
208 }
209 let (magic, rest) = if &data[..4] == b"AT&T" {
211 (&data[..4], &data[4..])
212 } else {
213 (&data[..0], data)
215 };
216 let _ = magic;
217
218 let (root, _) = parse_chunk(rest, 0, 0)?;
219 Ok(DjvuFile { root })
220}
221
222const MAX_IFF_DEPTH: u32 = 64;
227
228fn parse_chunk(data: &[u8], offset: usize, depth: u32) -> Result<(Chunk, usize), Error> {
231 if depth > MAX_IFF_DEPTH {
232 return Err(Error::InvalidLength);
233 }
234 if offset.checked_add(8).is_none_or(|end| end > data.len()) {
235 return Err(Error::UnexpectedEof);
236 }
237
238 let id: ChunkId = [
239 data[offset],
240 data[offset + 1],
241 data[offset + 2],
242 data[offset + 3],
243 ];
244 let length = u32::from_be_bytes([
245 data[offset + 4],
246 data[offset + 5],
247 data[offset + 6],
248 data[offset + 7],
249 ]);
250
251 let payload_start = offset + 8;
252 let payload_end = payload_start
256 .checked_add(length as usize)
257 .ok_or(Error::InvalidLength)?;
258
259 if payload_end > data.len() {
260 return Err(Error::UnexpectedEof);
261 }
262
263 let total = 8usize
265 .checked_add(length as usize)
266 .ok_or(Error::InvalidLength)?;
267 let padded_total = total.checked_add(total % 2).ok_or(Error::InvalidLength)?;
268
269 if &id == b"FORM" {
270 if length < 4 {
271 return Err(Error::InvalidLength);
272 }
273 let secondary_id: ChunkId = [
274 data[payload_start],
275 data[payload_start + 1],
276 data[payload_start + 2],
277 data[payload_start + 3],
278 ];
279
280 let children_start = payload_start + 4;
281 let children = parse_children(data, children_start, payload_end, depth + 1)?;
282
283 Ok((
284 Chunk::Form {
285 secondary_id,
286 length,
287 children,
288 },
289 padded_total,
290 ))
291 } else {
292 let chunk_data = data[payload_start..payload_end].to_vec();
293 Ok((
294 Chunk::Leaf {
295 id,
296 data: chunk_data,
297 },
298 padded_total,
299 ))
300 }
301}
302
303fn parse_children(data: &[u8], start: usize, end: usize, depth: u32) -> Result<Vec<Chunk>, Error> {
305 let mut chunks = Vec::new();
306 let mut pos = start;
307
308 while pos < end {
309 if pos + 8 > end {
310 break;
312 }
313 let (chunk, consumed) = parse_chunk(data, pos, depth)?;
314 chunks.push(chunk);
315 pos = pos.checked_add(consumed).ok_or(Error::InvalidLength)?;
318 }
319
320 Ok(chunks)
321}
322
323pub fn emit(file: &DjvuFile) -> Vec<u8> {
334 let mut out = Vec::with_capacity(64);
335 out.extend_from_slice(&MAGIC);
336 emit_chunk(&file.root, &mut out);
337 out
338}
339
340fn emit_chunk(chunk: &Chunk, out: &mut Vec<u8>) {
341 emit_chunk_inner(chunk, out, false);
342}
343
344fn emit_chunk_inner(chunk: &Chunk, out: &mut Vec<u8>, suppress_inner_pad: bool) {
345 match chunk {
346 Chunk::Form {
347 secondary_id,
348 length: stored_length,
349 children,
350 } => {
351 let suppress_last_pad = (*stored_length & 1) == 1;
360 let mut payload: Vec<u8> = Vec::new();
361 payload.extend_from_slice(secondary_id);
362 let n = children.len();
363 for (i, child) in children.iter().enumerate() {
364 let last = i + 1 == n;
365 emit_chunk_inner(child, &mut payload, last && suppress_last_pad);
366 }
367 let len = payload.len() as u32;
368 out.extend_from_slice(b"FORM");
369 out.extend_from_slice(&len.to_be_bytes());
370 out.extend_from_slice(&payload);
371 let total = 8 + payload.len();
374 if !suppress_inner_pad && total % 2 == 1 {
375 out.push(0);
376 }
377 }
378 Chunk::Leaf { id, data } => {
379 let len = data.len() as u32;
380 out.extend_from_slice(id);
381 out.extend_from_slice(&len.to_be_bytes());
382 out.extend_from_slice(data);
383 let total = 8 + data.len();
384 if !suppress_inner_pad && total % 2 == 1 {
385 out.push(0);
386 }
387 }
388 }
389}
390
391pub fn emitted_size(chunk: &Chunk) -> usize {
400 emitted_size_inner(chunk, false)
401}
402
403fn emitted_size_inner(chunk: &Chunk, suppress_inner_pad: bool) -> usize {
404 match chunk {
405 Chunk::Form {
406 length: stored_length,
407 children,
408 ..
409 } => {
410 let suppress_last_pad = (*stored_length & 1) == 1;
411 let n = children.len();
412 let mut payload = 4usize; for (i, child) in children.iter().enumerate() {
414 let last = i + 1 == n;
415 payload += emitted_size_inner(child, last && suppress_last_pad);
416 }
417 let total = 8 + payload;
418 total + usize::from(!suppress_inner_pad && total % 2 == 1)
419 }
420 Chunk::Leaf { data, .. } => {
421 let total = 8 + data.len();
422 total + usize::from(!suppress_inner_pad && total % 2 == 1)
423 }
424 }
425}
426
427pub enum EmitPart<'a> {
430 Chunk(&'a Chunk),
433 Verbatim(&'a [u8]),
438 Form(&'a [u8]),
445}
446
447pub fn partial_emit(secondary_id: ChunkId, parts: &[EmitPart<'_>]) -> Option<Vec<u8>> {
458 partial_emit_with_offsets(secondary_id, parts).map(|(bytes, _)| bytes)
459}
460
461pub fn partial_emit_with_offsets(
479 secondary_id: ChunkId,
480 parts: &[EmitPart<'_>],
481) -> Option<(Vec<u8>, Vec<usize>)> {
482 const PROLOGUE: usize = 12;
486 let mut payload = Vec::new();
487 payload.extend_from_slice(&secondary_id); let mut offsets = Vec::with_capacity(parts.len());
489 for part in parts {
490 offsets.push(PROLOGUE + payload.len());
491 match part {
492 EmitPart::Chunk(chunk) => emit_chunk(chunk, &mut payload),
493 EmitPart::Verbatim(bytes) => {
494 payload.extend_from_slice(bytes);
495 if payload.len() % 2 == 1 {
496 payload.push(0);
497 }
498 }
499 EmitPart::Form(body) => {
500 let len = u32::try_from(body.len()).ok()?;
501 payload.extend_from_slice(b"FORM");
502 payload.extend_from_slice(&len.to_be_bytes());
503 payload.extend_from_slice(body);
504 if payload.len() % 2 == 1 {
505 payload.push(0);
506 }
507 }
508 }
509 }
510 let len = u32::try_from(payload.len()).ok()?;
511 let mut out = Vec::with_capacity(8 + payload.len());
512 out.extend_from_slice(&MAGIC);
513 out.extend_from_slice(b"FORM");
514 out.extend_from_slice(&len.to_be_bytes());
515 out.extend_from_slice(&payload);
516 if (8 + payload.len()) % 2 == 1 {
519 out.push(0);
520 }
521 Some((out, offsets))
522}
523
524#[derive(Debug, Clone, Copy)]
532pub struct IffChunk<'a> {
533 pub id: [u8; 4],
535 pub data: &'a [u8],
537}
538
539#[derive(Debug)]
541pub struct Form<'a> {
542 pub form_type: [u8; 4],
544 pub chunks: Vec<IffChunk<'a>>,
546}
547
548pub fn parse_form(data: &[u8]) -> Result<Form<'_>, IffError> {
560 if data.len() < 16 {
562 return Err(IffError::TooShort);
563 }
564
565 let magic = read_4(data, 0)?;
567 if &magic != b"AT&T" {
568 return Err(IffError::BadMagic { got: magic });
569 }
570
571 let form_id = read_4(data, 4)?;
573 if &form_id != b"FORM" {
574 return Err(IffError::Truncated);
575 }
576
577 let form_len = read_u32_be(data, 8)? as usize;
579
580 let form_data_end = 12_usize.checked_add(form_len).ok_or(IffError::Truncated)?;
582 if form_data_end > data.len() {
583 return Err(IffError::ChunkTooLong {
584 id: *b"FORM",
585 claimed: form_len as u32,
586 available: data.len().saturating_sub(12),
587 });
588 }
589
590 if form_len < 4 {
592 return Err(IffError::Truncated);
593 }
594 let form_type = read_4(data, 12)?;
595
596 let body = data.get(16..form_data_end).ok_or(IffError::Truncated)?;
598
599 let chunks = parse_form_body(body)?;
600
601 Ok(Form { form_type, chunks })
602}
603
604pub fn parse_form_body(mut buf: &[u8]) -> Result<Vec<IffChunk<'_>>, IffError> {
614 let mut chunks = Vec::new();
615
616 while buf.len() >= 8 {
617 let id = read_4(buf, 0)?;
618 let data_len = read_u32_be(buf, 4)? as usize;
619
620 let data_start = 8_usize;
621 let data_end = data_start
622 .checked_add(data_len)
623 .ok_or(IffError::Truncated)?;
624
625 if data_end > buf.len() {
626 return Err(IffError::ChunkTooLong {
627 id,
628 claimed: data_len as u32,
629 available: buf.len().saturating_sub(data_start),
630 });
631 }
632
633 let chunk_data = buf.get(data_start..data_end).ok_or(IffError::Truncated)?;
634 chunks.push(IffChunk {
635 id,
636 data: chunk_data,
637 });
638
639 let padded_len = data_len + (data_len & 1);
641 let next = data_start
642 .checked_add(padded_len)
643 .ok_or(IffError::Truncated)?;
644
645 buf = buf.get(next.min(buf.len())..).ok_or(IffError::Truncated)?;
647 }
648
649 Ok(chunks)
650}
651
652#[inline]
654fn read_4(data: &[u8], offset: usize) -> Result<[u8; 4], IffError> {
655 data.get(offset..offset + 4)
656 .and_then(|s| s.try_into().ok())
657 .ok_or(IffError::Truncated)
658}
659
660#[inline]
662fn read_u32_be(data: &[u8], offset: usize) -> Result<u32, IffError> {
663 let b = read_4(data, offset)?;
664 Ok(u32::from_be_bytes(b))
665}
666
667#[cfg(test)]
671pub fn dump(file: &DjvuFile) -> String {
672 let mut out = String::new();
673 dump_chunk(&file.root, 1, &mut out);
674 out
675}
676
677#[cfg(test)]
678fn dump_chunk(chunk: &Chunk, depth: usize, out: &mut String) {
679 let indent = " ".repeat(depth);
680 match chunk {
681 Chunk::Form {
682 secondary_id,
683 length,
684 children,
685 } => {
686 let sec = std::str::from_utf8(secondary_id).unwrap_or("????");
687 out.push_str(&format!("{}FORM:{} [{}] \n", indent, sec, length));
688 for child in children {
689 dump_chunk(child, depth + 1, out);
690 }
691 }
692 Chunk::Leaf { id, data } => {
693 let id_str = std::str::from_utf8(id).unwrap_or("????");
694 out.push_str(&format!("{}{} [{}] \n", indent, id_str, data.len()));
695 }
696 }
697}
698
699#[cfg(test)]
700mod tests {
701 use super::*;
702
703 #[test]
706 fn deeply_nested_forms_are_rejected_not_overflow() {
707 let mut buf = Vec::new();
709 buf.extend_from_slice(b"FORM");
710 buf.extend_from_slice(&4u32.to_be_bytes());
711 buf.extend_from_slice(b"DJVU");
712 for _ in 0..200 {
713 let inner = buf;
714 let len = 4 + inner.len();
715 let mut outer = Vec::new();
716 outer.extend_from_slice(b"FORM");
717 outer.extend_from_slice(&(len as u32).to_be_bytes());
718 outer.extend_from_slice(b"DJVU");
719 outer.extend_from_slice(&inner);
720 buf = outer;
721 }
722 let mut full = Vec::from(*b"AT&T");
723 full.extend_from_slice(&buf);
724 assert!(
725 parse(&full).is_err(),
726 "deep nesting must error, not overflow"
727 );
728 }
729
730 #[test]
733 fn overflowing_chunk_length_is_rejected() {
734 let mut data = Vec::from(*b"AT&T");
735 data.extend_from_slice(b"JUNK");
736 data.extend_from_slice(&u32::MAX.to_be_bytes()); data.extend_from_slice(b"\x00\x00");
738 assert!(parse(&data).is_err());
739 }
740
741 fn assets_path() -> std::path::PathBuf {
742 std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
743 .join("../../references/djvujs/library/assets")
744 }
745
746 fn golden_path() -> std::path::PathBuf {
747 std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../tests/golden/iff")
748 }
749
750 fn normalize_dump(input: &str) -> Vec<String> {
754 input
755 .lines()
756 .filter(|l| !l.trim().is_empty())
757 .map(|line| {
758 let trimmed = line.trim_end();
759 if let Some(bracket_end) = trimmed.find(']') {
760 let structural = &trimmed[..=bracket_end];
761 structural.trim_end().to_string()
762 } else {
763 trimmed.to_string()
764 }
765 })
766 .collect()
767 }
768
769 fn assert_structure_matches(djvu_file: &str, golden_file: &str) {
770 let data = std::fs::read(assets_path().join(djvu_file)).unwrap();
771 let file = parse(&data).unwrap();
772 let actual = dump(&file);
773 let expected = std::fs::read_to_string(golden_path().join(golden_file)).unwrap();
774
775 let actual_lines = normalize_dump(&actual);
776 let expected_lines = normalize_dump(&expected);
777
778 assert_eq!(
779 actual_lines.len(),
780 expected_lines.len(),
781 "Line count mismatch for {} ({} vs {})",
782 djvu_file,
783 actual_lines.len(),
784 expected_lines.len()
785 );
786
787 for (i, (a, e)) in actual_lines.iter().zip(expected_lines.iter()).enumerate() {
788 assert_eq!(
789 a,
790 e,
791 "Line {} mismatch for {}\n actual: {:?}\n expected: {:?}",
792 i + 1,
793 djvu_file,
794 a,
795 e
796 );
797 }
798 }
799
800 #[test]
801 fn parse_boy_jb2_legacy() {
802 let data = std::fs::read(assets_path().join("boy_jb2.djvu")).unwrap();
803 let file = parse(&data).unwrap();
804
805 match &file.root {
806 Chunk::Form {
807 secondary_id,
808 children,
809 ..
810 } => {
811 assert_eq!(secondary_id, b"DJVU");
812 assert_eq!(children.len(), 2);
813 }
814 _ => panic!("expected FORM root"),
815 }
816 }
817
818 #[test]
819 fn structure_boy_jb2() {
820 assert_structure_matches("boy_jb2.djvu", "boy_jb2.dump");
821 }
822
823 #[test]
824 fn structure_boy() {
825 assert_structure_matches("boy.djvu", "boy.dump");
826 }
827
828 #[test]
829 fn structure_chicken() {
830 assert_structure_matches("chicken.djvu", "chicken.dump");
831 }
832
833 #[test]
834 fn structure_carte() {
835 assert_structure_matches("carte.djvu", "carte.dump");
836 }
837
838 #[test]
839 fn structure_navm_fgbz() {
840 assert_structure_matches("navm_fgbz.djvu", "navm_fgbz.dump");
841 }
842
843 #[test]
844 fn structure_colorbook() {
845 assert_structure_matches("colorbook.djvu", "colorbook.dump");
846 }
847
848 #[test]
849 fn structure_djvu3spec_bundled() {
850 assert_structure_matches("DjVu3Spec_bundled.djvu", "djvu3spec_bundled.dump");
851 }
852
853 #[test]
854 fn structure_big_scanned_page() {
855 assert_structure_matches("big-scanned-page.djvu", "big_scanned_page.dump");
856 }
857
858 fn assert_emitted_size_matches_emit(name: &str) {
865 let Ok(data) = std::fs::read(assets_path().join(name)) else {
866 return; };
868 let file = parse(&data).unwrap();
869 let emitted = emit(&file);
870 assert_eq!(
871 emitted_size(&file.root),
872 emitted.len() - 4,
873 "emitted_size disagrees with emit() for {name}"
874 );
875 }
876
877 #[test]
878 fn emitted_size_matches_emit_corpus() {
879 for name in [
880 "boy_jb2.djvu",
881 "boy.djvu",
882 "chicken.djvu",
883 "carte.djvu",
884 "navm_fgbz.djvu",
885 "colorbook.djvu",
886 "DjVu3Spec_bundled.djvu",
887 "big-scanned-page.djvu",
888 ] {
889 assert_emitted_size_matches_emit(name);
890 }
891 }
892
893 #[test]
894 fn partial_emit_verbatim_matches_chunk_framing() {
895 let tree = DjvuFile {
901 root: Chunk::Form {
902 secondary_id: *b"DJVU",
903 length: 0,
904 children: vec![
905 Chunk::Leaf {
906 id: *b"INFO",
907 data: vec![0xAA; 5], },
909 Chunk::Leaf {
910 id: *b"Sjbz",
911 data: vec![0xBB; 4], },
913 ],
914 },
915 };
916 let canonical = emit(&tree); let Chunk::Form { children, .. } = &tree.root else {
919 unreachable!()
920 };
921 let mut info_bytes = Vec::new();
923 emit_chunk(&children[0], &mut info_bytes);
924 let mut sjbz_bytes = Vec::new();
925 emit_chunk(&children[1], &mut sjbz_bytes);
926
927 let via_verbatim = partial_emit(
928 *b"DJVU",
929 &[
930 EmitPart::Verbatim(&info_bytes),
931 EmitPart::Verbatim(&sjbz_bytes),
932 ],
933 )
934 .expect("fits in u32");
935 let via_chunks = partial_emit(
936 *b"DJVU",
937 &[EmitPart::Chunk(&children[0]), EmitPart::Chunk(&children[1])],
938 )
939 .expect("fits in u32");
940
941 assert_eq!(via_verbatim, canonical, "verbatim path must match emit");
942 assert_eq!(via_chunks, canonical, "chunk path must match emit");
943 }
944
945 #[test]
946 fn partial_emit_pads_odd_verbatim_child() {
947 let parts = [EmitPart::Verbatim(&[1u8, 2, 3])];
950 let out = partial_emit(*b"DJVU", &parts).unwrap();
951 assert_eq!(out.len(), 20);
953 assert_eq!(&out[..8], b"AT&TFORM");
954 assert_eq!(u32::from_be_bytes(out[8..12].try_into().unwrap()), 8);
956 assert_eq!(&out[12..16], b"DJVU");
957 assert_eq!(&out[16..19], &[1, 2, 3]);
958 assert_eq!(out[19], 0);
959 }
960
961 #[test]
962 fn partial_emit_form_part_frames_nested_form() {
963 let body: &[u8] = b"DJVUxyz"; let via_form = partial_emit(*b"DJVM", &[EmitPart::Form(body)]).unwrap();
967
968 let mut framed = Vec::new();
970 framed.extend_from_slice(b"FORM");
971 framed.extend_from_slice(&(body.len() as u32).to_be_bytes());
972 framed.extend_from_slice(body);
973 framed.push(0); let via_verbatim = partial_emit(*b"DJVM", &[EmitPart::Verbatim(&framed)]).unwrap();
975
976 assert_eq!(via_form, via_verbatim, "Form part must match framed FORM");
977 assert_eq!(&via_form[..8], b"AT&TFORM");
979 assert_eq!(&via_form[12..16], b"DJVM");
980 assert_eq!(&via_form[16..20], b"FORM");
981 assert_eq!(u32::from_be_bytes(via_form[20..24].try_into().unwrap()), 7);
982 assert_eq!(&via_form[24..31], body);
983 assert_eq!(via_form[31], 0); }
985
986 #[test]
987 fn partial_emit_with_offsets_reports_part_starts() {
988 let dirm = Chunk::Leaf {
991 id: *b"DIRM",
992 data: vec![0xAB; 5], };
994 let comp0: &[u8] = b"DJVU0000"; let comp1: &[u8] = b"DJVIaa"; let parts = [
997 EmitPart::Chunk(&dirm),
998 EmitPart::Form(comp0),
999 EmitPart::Form(comp1),
1000 ];
1001 let (bytes, offsets) = partial_emit_with_offsets(*b"DJVM", &parts).unwrap();
1002
1003 assert_eq!(offsets.len(), 3);
1004 assert_eq!(offsets[0], 16);
1006 assert_eq!(&bytes[offsets[0]..offsets[0] + 4], b"DIRM");
1007 for &off in &offsets[1..] {
1009 assert_eq!(&bytes[off..off + 4], b"FORM", "offset must point at FORM");
1010 }
1011 assert_eq!(offsets[2] - offsets[1], 16);
1013 }
1014
1015 fn minimal_djvu_bytes() -> Vec<u8> {
1019 let info_data: &[u8] = &[
1020 0x00, 0xB5, 0x00, 0xF0, 0x18, 0x00, 0x64, 0x00, 0x16, 0x00, ];
1028 let info_len = info_data.len() as u32;
1029
1030 let mut chunk = Vec::new();
1031 chunk.extend_from_slice(b"INFO");
1032 chunk.extend_from_slice(&info_len.to_be_bytes());
1033 chunk.extend_from_slice(info_data);
1034
1035 let mut form_body = Vec::new();
1036 form_body.extend_from_slice(b"DJVU");
1037 form_body.extend_from_slice(&chunk);
1038
1039 let form_len = form_body.len() as u32;
1040
1041 let mut file = Vec::new();
1042 file.extend_from_slice(b"AT&T");
1043 file.extend_from_slice(b"FORM");
1044 file.extend_from_slice(&form_len.to_be_bytes());
1045 file.extend_from_slice(&form_body);
1046
1047 file
1048 }
1049
1050 #[test]
1051 fn empty_input_is_error() {
1052 let result = parse_form(&[]);
1053 assert!(result.is_err());
1054 assert_eq!(result.unwrap_err(), IffError::TooShort);
1055 }
1056
1057 #[test]
1058 fn short_input_is_error() {
1059 let result = parse_form(&[0u8; 10]);
1060 assert!(result.is_err());
1061 assert_eq!(result.unwrap_err(), IffError::TooShort);
1062 }
1063
1064 #[test]
1065 fn bad_magic_is_error() {
1066 let mut data = minimal_djvu_bytes();
1067 data[0] = 0xFF;
1068 data[1] = 0xFF;
1069 data[2] = 0xFF;
1070 data[3] = 0xFF;
1071
1072 let result = parse_form(&data);
1073 assert!(result.is_err());
1074 assert_eq!(
1075 result.unwrap_err(),
1076 IffError::BadMagic {
1077 got: [0xFF, 0xFF, 0xFF, 0xFF]
1078 }
1079 );
1080 }
1081
1082 #[test]
1083 fn valid_single_page_parses() {
1084 let data = minimal_djvu_bytes();
1085 let form = parse_form(&data).expect("should parse successfully");
1086
1087 assert_eq!(&form.form_type, b"DJVU");
1088 assert_eq!(form.chunks.len(), 1);
1089 assert_eq!(&form.chunks[0].id, b"INFO");
1090 assert_eq!(form.chunks[0].data.len(), 10);
1091 }
1092
1093 #[test]
1094 fn truncated_chunk_is_error() {
1095 let mut data = minimal_djvu_bytes();
1096 let new_len = data.len() - 4;
1097 data.truncate(new_len);
1098
1099 let result = parse_form(&data);
1100 assert!(result.is_err());
1101 match result.unwrap_err() {
1102 IffError::ChunkTooLong { .. } | IffError::Truncated => {}
1103 other => panic!("expected ChunkTooLong or Truncated, got {:?}", other),
1104 }
1105 }
1106
1107 #[test]
1108 fn non_form_root_chunk_is_truncated_error() {
1109 let mut data = Vec::new();
1111 data.extend_from_slice(b"AT&T");
1112 data.extend_from_slice(b"INFO"); data.extend_from_slice(&10u32.to_be_bytes());
1114 data.extend_from_slice(&[0u8; 10]);
1115 assert_eq!(parse_form(&data).unwrap_err(), IffError::Truncated);
1116 }
1117
1118 #[test]
1119 fn form_too_short_for_secondary_id() {
1120 let mut data = Vec::new();
1123 data.extend_from_slice(b"AT&T");
1124 data.extend_from_slice(b"FORM");
1125 data.extend_from_slice(&3u32.to_be_bytes()); data.extend_from_slice(b"XYZ\x00"); assert_eq!(parse_form(&data).unwrap_err(), IffError::Truncated);
1128 }
1129
1130 #[test]
1131 fn sub_chunk_length_exceeds_body() {
1132 let mut body = Vec::new();
1135 body.extend_from_slice(b"DJVU"); body.extend_from_slice(b"INFO");
1137 body.extend_from_slice(&100u32.to_be_bytes()); body.extend_from_slice(&[0u8; 2]); let mut data = Vec::new();
1140 data.extend_from_slice(b"AT&T");
1141 data.extend_from_slice(b"FORM");
1142 data.extend_from_slice(&(body.len() as u32).to_be_bytes());
1143 data.extend_from_slice(&body);
1144 match parse_form(&data).unwrap_err() {
1145 IffError::ChunkTooLong { .. } => {}
1146 other => panic!("expected ChunkTooLong, got {other:?}"),
1147 }
1148 }
1149
1150 #[test]
1151 fn unknown_form_type_allowed() {
1152 let mut data = minimal_djvu_bytes();
1153 data[12] = b'X';
1154 data[13] = b'X';
1155 data[14] = b'X';
1156 data[15] = b'X';
1157
1158 let form = parse_form(&data).expect("unknown form type should still parse");
1159 assert_eq!(&form.form_type, b"XXXX");
1160 }
1161
1162 #[test]
1163 fn real_chicken_djvu_parses() {
1164 let path = assets_path().join("chicken.djvu");
1165 let data = std::fs::read(&path).expect("chicken.djvu must exist");
1166 let form = parse_form(&data).expect("chicken.djvu should parse");
1167
1168 assert_eq!(&form.form_type, b"DJVU");
1169 assert!(!form.chunks.is_empty(), "must have at least one chunk");
1170 assert_eq!(&form.chunks[0].id, b"INFO");
1171 assert!(form.chunks[0].data.len() >= 10);
1172 }
1173
1174 #[test]
1175 fn real_multipage_djvu_parses() {
1176 let path = assets_path().join("navm_fgbz.djvu");
1177 let data = std::fs::read(&path).expect("navm_fgbz.djvu must exist");
1178 let form = parse_form(&data).expect("navm_fgbz.djvu should parse");
1179
1180 assert_eq!(&form.form_type, b"DJVM");
1181 assert!(!form.chunks.is_empty());
1182 }
1183
1184 #[test]
1186 fn legacy_error_display_variants() {
1187 assert_eq!(
1188 LegacyError::UnexpectedEof.to_string(),
1189 "unexpected end of input"
1190 );
1191 assert_eq!(
1192 LegacyError::InvalidMagic.to_string(),
1193 "invalid magic number"
1194 );
1195 assert_eq!(LegacyError::InvalidLength.to_string(), "invalid length");
1196 assert_eq!(
1197 LegacyError::MissingChunk("INFO").to_string(),
1198 "missing required chunk: INFO"
1199 );
1200 assert_eq!(LegacyError::Unsupported("x").to_string(), "unsupported: x");
1201 assert_eq!(
1202 LegacyError::FormatError("y".to_string()).to_string(),
1203 "format error: y"
1204 );
1205 }
1206
1207 #[test]
1209 fn chunk_accessors_form_and_leaf() {
1210 let leaf = Chunk::Leaf {
1211 id: *b"INFO",
1212 data: vec![1, 2, 3],
1213 };
1214 let form = Chunk::Form {
1215 secondary_id: *b"DJVU",
1216 length: 10,
1217 children: vec![leaf.clone()],
1218 };
1219
1220 assert_eq!(form.data(), &[] as &[u8]);
1222 assert_eq!(leaf.data(), &[1u8, 2, 3]);
1223
1224 assert_eq!(form.children().len(), 1);
1226 assert!(leaf.children().is_empty());
1227
1228 assert_eq!(form.payload_length(), 10);
1230 assert_eq!(leaf.payload_length(), 3);
1231
1232 assert!(leaf.find_first(b"INFO").is_none());
1234
1235 let form2 = Chunk::Form {
1237 secondary_id: *b"DJVU",
1238 length: 0,
1239 children: vec![],
1240 };
1241 assert!(form2.find_first(b"INFO").is_none());
1242 }
1243
1244 #[test]
1245 fn find_all_returns_all_matching_leaves() {
1246 let leaf1 = Chunk::Leaf {
1247 id: *b"INFO",
1248 data: vec![1],
1249 };
1250 let leaf2 = Chunk::Leaf {
1251 id: *b"INFO",
1252 data: vec![2],
1253 };
1254 let leaf3 = Chunk::Leaf {
1255 id: *b"BG44",
1256 data: vec![3],
1257 };
1258 let child_form = Chunk::Form {
1260 secondary_id: *b"DJVU",
1261 length: 0,
1262 children: vec![],
1263 };
1264 let form = Chunk::Form {
1265 secondary_id: *b"DJVU",
1266 length: 0,
1267 children: vec![leaf1, leaf2, leaf3, child_form],
1268 };
1269 let all_info = form.find_all(b"INFO");
1270 assert_eq!(all_info.len(), 2);
1271 let all_bg44 = form.find_all(b"BG44");
1272 assert_eq!(all_bg44.len(), 1);
1273 let all_none = form.find_all(b"NONE");
1274 assert!(all_none.is_empty());
1275 }
1276
1277 #[test]
1278 fn find_first_skips_form_children() {
1279 let child_form = Chunk::Form {
1282 secondary_id: *b"DJVU",
1283 length: 0,
1284 children: vec![],
1285 };
1286 let leaf = Chunk::Leaf {
1287 id: *b"INFO",
1288 data: vec![42],
1289 };
1290 let form = Chunk::Form {
1291 secondary_id: *b"DJVU",
1292 length: 0,
1293 children: vec![child_form, leaf],
1294 };
1295 let found = form.find_first(b"INFO").expect("should find INFO");
1296 assert!(matches!(found, Chunk::Leaf { id, .. } if id == b"INFO"));
1297 }
1298
1299 #[test]
1300 fn parse_empty_input_returns_unexpected_eof() {
1301 assert!(matches!(parse(b""), Err(Error::UnexpectedEof)));
1303 assert!(matches!(parse(b"AT"), Err(Error::UnexpectedEof)));
1304 }
1305
1306 #[test]
1307 fn parse_form_length_too_small_returns_invalid_length() {
1308 let mut data = vec![];
1311 data.extend_from_slice(b"AT&T");
1312 data.extend_from_slice(b"FORM");
1313 data.extend_from_slice(&3u32.to_be_bytes()); data.extend_from_slice(b"XYZ");
1315 assert!(matches!(parse(&data), Err(Error::InvalidLength)));
1316 }
1317
1318 #[test]
1319 fn parse_children_skips_trailing_bytes() {
1320 let mut data = vec![];
1324 data.extend_from_slice(b"AT&T");
1325 data.extend_from_slice(b"FORM");
1326 let secondary_plus_junk = b"DJVU\x01\x02\x03\x04\x05"; data.extend_from_slice(&(secondary_plus_junk.len() as u32).to_be_bytes());
1328 data.extend_from_slice(secondary_plus_junk);
1329 let result = parse(&data);
1330 let djvu = result.expect("trailing bytes must not cause an error");
1332 assert!(matches!(djvu.root, Chunk::Form { .. }));
1333 assert!(djvu.root.children().is_empty());
1334 }
1335
1336 #[test]
1337 fn odd_length_chunk_padding() {
1338 let chunk1_data: &[u8] = &[0xAA, 0xBB, 0xCC, 0xDD, 0xEE]; let chunk2_data: &[u8] = &[0x01, 0x02]; let mut form_body: Vec<u8> = Vec::new();
1342 form_body.extend_from_slice(b"DJVU");
1343
1344 form_body.extend_from_slice(b"TST1");
1345 form_body.extend_from_slice(&5u32.to_be_bytes());
1346 form_body.extend_from_slice(chunk1_data);
1347 form_body.push(0x00); form_body.extend_from_slice(b"TST2");
1350 form_body.extend_from_slice(&2u32.to_be_bytes());
1351 form_body.extend_from_slice(chunk2_data);
1352
1353 let form_len = form_body.len() as u32;
1354
1355 let mut file: Vec<u8> = Vec::new();
1356 file.extend_from_slice(b"AT&T");
1357 file.extend_from_slice(b"FORM");
1358 file.extend_from_slice(&form_len.to_be_bytes());
1359 file.extend_from_slice(&form_body);
1360
1361 let form = parse_form(&file).expect("should parse padded chunk");
1362 assert_eq!(form.chunks.len(), 2);
1363 assert_eq!(&form.chunks[0].id, b"TST1");
1364 assert_eq!(form.chunks[0].data, chunk1_data);
1365 assert_eq!(&form.chunks[1].id, b"TST2");
1366 assert_eq!(form.chunks[1].data, chunk2_data);
1367 }
1368}