1use anyhow::{bail, Result};
20use byteorder::{BigEndian, ByteOrder};
21use std::sync::OnceLock;
22
23use super::stuffit::{ForkCodec, ForkInfo, StuffItArchive, StuffItEntry};
24use fs_mac::hfs::decode_mac_filename;
25
26const FILE_METADATA_LEN: usize = 45;
29
30pub fn is_compactpro(bytes: &[u8]) -> bool {
38 validate_index(bytes).unwrap_or(false)
39}
40
41fn validate_index(bytes: &[u8]) -> Option<bool> {
45 if bytes.len() < 8 || bytes[0] != 1 {
46 return Some(false);
47 }
48 let offset = BigEndian::read_u32(bytes.get(4..8)?) as usize;
49 let correct = BigEndian::read_u32(bytes.get(offset..offset + 4)?);
51 let mut p = offset + 4;
52
53 let head = bytes.get(p..p + 3)?;
54 let mut crc = crc32_update(0xffff_ffff, head);
55 let numentries = BigEndian::read_u16(&head[0..2]) as usize;
56 let commentsize = head[2] as usize;
57 p += 3;
58
59 let comment = bytes.get(p..p + commentsize)?;
60 crc = crc32_update(crc, comment);
61 p += commentsize;
62
63 for _ in 0..numentries {
64 let namelen = *bytes.get(p)?;
65 crc = crc32_update(crc, &[namelen]);
66 p += 1;
67 let nl = (namelen & 0x7f) as usize;
68 let namebytes = bytes.get(p..p + nl)?;
69 crc = crc32_update(crc, namebytes);
70 p += nl;
71 let metadatasize = if namelen & 0x80 != 0 {
72 2
73 } else {
74 FILE_METADATA_LEN
75 };
76 let meta = bytes.get(p..p + metadatasize)?;
77 crc = crc32_update(crc, meta);
78 p += metadatasize;
79 }
80
81 Some(crc == correct)
82}
83
84pub fn parse(bytes: &[u8]) -> Result<StuffItArchive> {
91 if bytes.len() < 8 || bytes[0] != 1 {
92 bail!("not a Compact Pro archive");
93 }
94 let index_offset = BigEndian::read_u32(&bytes[4..8]) as usize;
95 let mut pos = index_offset;
96
97 if pos + 7 > bytes.len() {
99 bail!("Compact Pro: truncated archive index");
100 }
101 pos += 4; let numentries = BigEndian::read_u16(&bytes[pos..pos + 2]) as usize;
103 pos += 2;
104 let commentlen = bytes[pos] as usize;
105 pos += 1;
106 pos = pos
107 .checked_add(commentlen)
108 .filter(|&p| p <= bytes.len())
109 .ok_or_else(|| anyhow::anyhow!("Compact Pro: comment runs past end of archive"))?;
110
111 let mut entries = Vec::new();
112 parse_directory(bytes, &mut pos, &[], numentries, &mut entries)?;
113 Ok(StuffItArchive {
114 entries,
115 truncated: None,
116 })
117}
118
119fn parse_directory(
120 bytes: &[u8],
121 pos: &mut usize,
122 parent: &[String],
123 mut numentries: usize,
124 entries: &mut Vec<StuffItEntry>,
125) -> Result<()> {
126 while numentries > 0 {
127 let namelen = *bytes
128 .get(*pos)
129 .ok_or_else(|| anyhow::anyhow!("Compact Pro: index truncated mid-entry"))?;
130 *pos += 1;
131 let nl = (namelen & 0x7f) as usize;
132 let namebytes = bytes
133 .get(*pos..*pos + nl)
134 .ok_or_else(|| anyhow::anyhow!("Compact Pro: name runs past end of archive"))?;
135 let name = decode_mac_filename(namebytes);
136 *pos += nl;
137
138 let mut path = parent.to_vec();
139 path.push(name.clone());
140
141 if namelen & 0x80 != 0 {
142 let numdir = read_u16(bytes, pos)? as usize;
144 entries.push(StuffItEntry {
145 path: path.clone(),
146 name,
147 is_dir: true,
148 type_code: [0; 4],
149 creator_code: [0; 4],
150 finder_flags: 0,
151 create_date: 0,
152 mod_date: 0,
153 data: None,
154 rsrc: None,
155 });
156 parse_directory(bytes, pos, &path, numdir, entries)?;
157 numentries = numentries
158 .checked_sub(numdir + 1)
159 .ok_or_else(|| anyhow::anyhow!("Compact Pro: inconsistent directory count"))?;
160 } else {
161 let meta = bytes
163 .get(*pos..*pos + FILE_METADATA_LEN)
164 .ok_or_else(|| anyhow::anyhow!("Compact Pro: file metadata past end of archive"))?;
165 *pos += FILE_METADATA_LEN;
166
167 let fileoffs = BigEndian::read_u32(&meta[1..5]) as u64;
171 let mut type_code = [0u8; 4];
172 type_code.copy_from_slice(&meta[5..9]);
173 let mut creator_code = [0u8; 4];
174 creator_code.copy_from_slice(&meta[9..13]);
175 let create_date = BigEndian::read_u32(&meta[13..17]);
176 let mod_date = BigEndian::read_u32(&meta[17..21]);
177 let finder_flags = BigEndian::read_u16(&meta[21..23]);
178 let crc = BigEndian::read_u32(&meta[23..27]);
179 let flags = BigEndian::read_u16(&meta[27..29]);
180 let resourcelength = BigEndian::read_u32(&meta[29..33]);
181 let datalength = BigEndian::read_u32(&meta[33..37]);
182 let resourcecomplen = BigEndian::read_u32(&meta[37..41]);
183 let datacomplen = BigEndian::read_u32(&meta[41..45]);
184
185 let encrypted = flags & 1 != 0;
186
187 let rsrc = if resourcelength > 0 {
191 Some(ForkInfo {
192 method: 0,
193 codec: ForkCodec::CompactPro {
194 lzh: flags & 2 != 0,
195 },
196 encrypted,
197 uncompressed_len: resourcelength,
198 compressed_len: resourcecomplen,
199 crc: 0,
200 crc32: crc,
201 offset: fileoffs,
202 })
203 } else {
204 None
205 };
206 let data = if datalength > 0 || resourcelength == 0 {
209 Some(ForkInfo {
210 method: 0,
211 codec: ForkCodec::CompactPro {
212 lzh: flags & 4 != 0,
213 },
214 encrypted,
215 uncompressed_len: datalength,
216 compressed_len: datacomplen,
217 crc: 0,
218 crc32: crc,
219 offset: fileoffs + resourcecomplen as u64,
220 })
221 } else {
222 None
223 };
224
225 entries.push(StuffItEntry {
226 path,
227 name,
228 is_dir: false,
229 type_code,
230 creator_code,
231 finder_flags,
232 create_date,
233 mod_date,
234 data,
235 rsrc,
236 });
237 numentries -= 1;
238 }
239 }
240 Ok(())
241}
242
243fn read_u16(bytes: &[u8], pos: &mut usize) -> Result<u16> {
244 let v = bytes
245 .get(*pos..*pos + 2)
246 .ok_or_else(|| anyhow::anyhow!("Compact Pro: index truncated"))?;
247 *pos += 2;
248 Ok(BigEndian::read_u16(v))
249}
250
251pub fn decompress_fork(archive: &[u8], fork: &ForkInfo) -> Result<Vec<u8>> {
260 let lzh = match fork.codec {
261 ForkCodec::CompactPro { lzh } => lzh,
262 ForkCodec::StuffIt => bail!("compactpro::decompress_fork called on a StuffIt fork"),
263 };
264 if fork.encrypted {
265 bail!("Compact Pro: encrypted entries are not supported");
266 }
267
268 let start = fork.offset as usize;
269 let end = start
270 .checked_add(fork.compressed_len as usize)
271 .ok_or_else(|| anyhow::anyhow!("Compact Pro: fork length overflow"))?;
272 if end > archive.len() {
273 bail!("Compact Pro: fork data extends past end of archive");
274 }
275 let comp = &archive[start..end];
276 let out_len = fork.uncompressed_len as usize;
277
278 if lzh {
279 let mut src = CpLzh::new(comp);
280 rle_decode(&mut src, out_len)
281 } else {
282 let mut src = SliceSource::new(comp);
283 rle_decode(&mut src, out_len)
284 }
285}
286
287pub fn verify_entry_crc(rsrc: &[u8], data: &[u8], expected: u32) -> Result<()> {
293 let got = crc32_update(crc32_update(0xffff_ffff, rsrc), data);
294 if got != expected {
295 bail!("Compact Pro: file CRC mismatch (got {got:#010x}, expected {expected:#010x})");
296 }
297 Ok(())
298}
299
300trait ByteSource {
307 fn next_byte(&mut self) -> Option<u8>;
308}
309
310struct SliceSource<'a> {
311 data: &'a [u8],
312 pos: usize,
313}
314
315impl<'a> SliceSource<'a> {
316 fn new(data: &'a [u8]) -> Self {
317 SliceSource { data, pos: 0 }
318 }
319}
320
321impl ByteSource for SliceSource<'_> {
322 fn next_byte(&mut self) -> Option<u8> {
323 let v = self.data.get(self.pos).copied();
324 if v.is_some() {
325 self.pos += 1;
326 }
327 v
328 }
329}
330
331#[derive(Default)]
332struct RleState {
333 saved: u8,
334 repeat: i64,
337 halfescaped: bool,
338}
339
340fn rle_next(st: &mut RleState, src: &mut dyn ByteSource) -> Option<u8> {
345 if st.repeat != 0 {
346 st.repeat -= 1;
347 return Some(st.saved);
348 }
349
350 let byte = if st.halfescaped {
351 st.halfescaped = false;
352 0x81
353 } else {
354 src.next_byte()?
355 };
356
357 if byte != 0x81 {
358 st.saved = byte;
359 return Some(byte);
360 }
361
362 let b2 = src.next_byte()?;
363 if b2 == 0x82 {
364 let count = src.next_byte()?;
365 if count != 0 {
366 st.repeat = count as i64 - 2;
367 Some(st.saved)
368 } else {
369 st.repeat = 1;
370 st.saved = 0x82;
371 Some(0x81)
372 }
373 } else if b2 == 0x81 {
374 st.halfescaped = true;
375 st.saved = 0x81;
376 Some(0x81)
377 } else {
378 st.repeat = 1;
379 st.saved = b2;
380 Some(0x81)
381 }
382}
383
384fn rle_decode(src: &mut dyn ByteSource, out_len: usize) -> Result<Vec<u8>> {
385 let mut out = Vec::with_capacity(out_len);
386 let mut st = RleState::default();
387 while out.len() < out_len {
388 match rle_next(&mut st, src) {
389 Some(b) => out.push(b),
390 None => bail!(
391 "Compact Pro: unexpected end of compressed data ({} of {} bytes)",
392 out.len(),
393 out_len
394 ),
395 }
396 }
397 Ok(out)
398}
399
400const LZH_BLOCKSIZE: i64 = 0x1fff0;
405const LZH_WINDOW_MASK: u64 = 0x1fff; enum Token {
408 Literal(u8),
409 Match { offset: u32, length: i32 },
410}
411
412struct CpLzh<'a> {
413 input: CpInput<'a>,
414 window: Vec<u8>,
415 pos: u64,
416 matchlength: i32,
417 matchoffset: u64,
418 blockcount: i64,
419 blockstart: i64,
420 literalcode: Option<Huff>,
421 lengthcode: Option<Huff>,
422 offsetcode: Option<Huff>,
423 ended: bool,
424}
425
426impl<'a> CpLzh<'a> {
427 fn new(data: &'a [u8]) -> Self {
428 CpLzh {
429 input: CpInput::new(data),
430 window: vec![0u8; (LZH_WINDOW_MASK + 1) as usize],
431 pos: 0,
432 matchlength: 0,
433 matchoffset: 0,
434 blockcount: LZH_BLOCKSIZE, blockstart: 0,
436 literalcode: None,
437 lengthcode: None,
438 offsetcode: None,
439 ended: false,
440 }
441 }
442
443 fn produce(&mut self) -> Option<u8> {
444 if self.ended {
445 return None;
446 }
447 if self.matchlength == 0 {
448 match self.next_token() {
449 Some(Token::Literal(v)) => {
450 self.window[(self.pos & LZH_WINDOW_MASK) as usize] = v;
451 self.pos = self.pos.wrapping_add(1);
452 return Some(v);
453 }
454 Some(Token::Match { offset, length }) => {
455 self.matchlength = length;
456 self.matchoffset = self.pos.wrapping_sub(offset as u64);
457 }
459 None => {
460 self.ended = true;
461 return None;
462 }
463 }
464 }
465
466 self.matchlength -= 1;
467 let byte = self.window[(self.matchoffset & LZH_WINDOW_MASK) as usize];
468 self.matchoffset = self.matchoffset.wrapping_add(1);
469 self.window[(self.pos & LZH_WINDOW_MASK) as usize] = byte;
470 self.pos = self.pos.wrapping_add(1);
471 Some(byte)
472 }
473
474 fn next_token(&mut self) -> Option<Token> {
475 if self.blockcount >= LZH_BLOCKSIZE {
476 if self.blockstart != 0 {
477 self.input.skip_to_byte_boundary();
481 if (self.input.buffer_offset() - self.blockstart) & 1 != 0 {
482 self.input.skip_bytes(3);
483 } else {
484 self.input.skip_bytes(2);
485 }
486 }
487 self.literalcode = Some(self.parse_code(256)?);
488 self.lengthcode = Some(self.parse_code(64)?);
489 self.offsetcode = Some(self.parse_code(128)?);
490 self.blockcount = 0;
491 self.blockstart = self.input.buffer_offset();
492 }
493
494 if self.input.next_bit() != 0 {
495 self.blockcount += 2;
496 let sym = Huff::decode(self.literalcode.as_ref()?, &mut self.input)?;
497 Some(Token::Literal(sym as u8))
498 } else {
499 self.blockcount += 3;
500 let length = Huff::decode(self.lengthcode.as_ref()?, &mut self.input)?;
501 let osym = Huff::decode(self.offsetcode.as_ref()?, &mut self.input)?;
502 let offset = ((osym as u32) << 6) | self.input.next_bits(6);
503 Some(Token::Match { offset, length })
504 }
505 }
506
507 fn parse_code(&mut self, size: usize) -> Option<Huff> {
510 let numbytes = self.input.next_byte()? as usize;
511 if numbytes * 2 > size {
512 return None;
513 }
514 let mut lengths = vec![0i32; size];
515 for i in 0..numbytes {
516 let val = self.input.next_byte()? as i32;
517 lengths[2 * i] = val >> 4;
518 lengths[2 * i + 1] = val & 0x0f;
519 }
520 Huff::build(&lengths)
521 }
522}
523
524impl ByteSource for CpLzh<'_> {
525 fn next_byte(&mut self) -> Option<u8> {
526 self.produce()
527 }
528}
529
530const HUFF_MAX_BITS: usize = 15;
535
536struct Huff {
539 counts: [u16; HUFF_MAX_BITS + 1],
541 symbols: Vec<i32>,
543}
544
545impl Huff {
546 fn build(lengths: &[i32]) -> Option<Huff> {
547 let mut counts = [0u16; HUFF_MAX_BITS + 1];
548 for &l in lengths {
549 if !(0..=HUFF_MAX_BITS as i32).contains(&l) {
550 return None;
551 }
552 counts[l as usize] += 1;
553 }
554 let mut left: i64 = 1;
557 for &count in &counts[1..=HUFF_MAX_BITS] {
558 left = (left << 1) - count as i64;
559 if left < 0 {
560 return None;
561 }
562 }
563
564 let mut offs = [0u16; HUFF_MAX_BITS + 2];
566 for len in 1..=HUFF_MAX_BITS {
567 offs[len + 1] = offs[len] + counts[len];
568 }
569 let total = offs[HUFF_MAX_BITS + 1] as usize;
570 let mut symbols = vec![0i32; total];
571 for (sym, &len) in lengths.iter().enumerate() {
572 if len != 0 {
573 symbols[offs[len as usize] as usize] = sym as i32;
574 offs[len as usize] += 1;
575 }
576 }
577
578 Some(Huff { counts, symbols })
579 }
580
581 fn decode(&self, input: &mut CpInput) -> Option<i32> {
585 let mut code: i32 = 0;
586 let mut first: i32 = 0;
587 let mut index: i32 = 0;
588 for len in 1..=HUFF_MAX_BITS {
589 code |= input.next_bit() as i32;
590 let count = self.counts[len] as i32;
591 if code - count < first {
592 return self.symbols.get((index + (code - first)) as usize).copied();
593 }
594 index += count;
595 first += count;
596 first <<= 1;
597 code <<= 1;
598 }
599 None
600 }
601}
602
603struct CpInput<'a> {
612 data: &'a [u8],
613 currbyte: usize,
614 bits: u32,
615 numbits: u32,
616}
617
618impl<'a> CpInput<'a> {
619 fn new(data: &'a [u8]) -> Self {
620 CpInput {
621 data,
622 currbyte: 0,
623 bits: 0,
624 numbits: 0,
625 }
626 }
627
628 fn peek_byte(&self, offs: usize) -> u32 {
629 match self.data.get(self.currbyte + offs) {
630 Some(&b) => b as u32,
631 None => 0,
632 }
633 }
634
635 fn fill_bits(&mut self) {
636 let mut numbytes = ((32 - self.numbits) >> 3) as usize;
637 let left = self.data.len().saturating_sub(self.currbyte);
638 if numbytes > left {
639 numbytes = left;
640 }
641 let s = (self.numbits >> 3) as usize;
642 match numbytes {
643 4 => {
644 self.bits = (self.peek_byte(s) << 24)
645 | (self.peek_byte(s + 1) << 16)
646 | (self.peek_byte(s + 2) << 8)
647 | self.peek_byte(s + 3);
648 }
649 3 => {
650 self.bits |= ((self.peek_byte(s) << 16)
651 | (self.peek_byte(s + 1) << 8)
652 | self.peek_byte(s + 2))
653 << (8 - self.numbits);
654 }
655 2 => {
656 self.bits |=
657 ((self.peek_byte(s) << 8) | self.peek_byte(s + 1)) << (16 - self.numbits);
658 }
659 1 => {
660 self.bits |= self.peek_byte(s) << (24 - self.numbits);
661 }
662 _ => {}
663 }
664 self.numbits += (numbytes as u32) * 8;
665 }
666
667 fn peek_bits(&mut self, n: u32) -> u32 {
668 if n == 0 {
669 return 0;
670 }
671 if n > self.numbits {
672 self.fill_bits();
673 }
674 self.bits >> (32 - n)
675 }
676
677 fn skip_peeked(&mut self, n: u32) {
678 let numbytes = ((n as i64) - ((self.numbits & 7) as i64) + 7) >> 3;
679 self.currbyte += numbytes.max(0) as usize;
680 self.bits = if n >= 32 { 0 } else { self.bits << n };
681 self.numbits = self.numbits.saturating_sub(n);
682 }
683
684 fn next_bit(&mut self) -> u32 {
685 let b = self.peek_bits(1);
686 self.skip_peeked(1);
687 b
688 }
689
690 fn next_bits(&mut self, n: u32) -> u32 {
691 if n == 0 {
692 return 0;
693 }
694 let b = self.peek_bits(n);
695 self.skip_peeked(n);
696 b
697 }
698
699 fn next_byte(&mut self) -> Option<u8> {
702 let v = self.data.get(self.currbyte).copied();
703 if v.is_some() {
704 self.currbyte += 1;
705 }
706 v
707 }
708
709 fn skip_to_byte_boundary(&mut self) {
710 self.bits = 0;
711 self.numbits = 0;
712 }
713
714 fn skip_bytes(&mut self, n: usize) {
715 self.currbyte += n;
716 }
717
718 fn buffer_offset(&self) -> i64 {
719 self.currbyte as i64
720 }
721}
722
723fn crc_table() -> &'static [u32; 256] {
729 static TABLE: OnceLock<[u32; 256]> = OnceLock::new();
730 TABLE.get_or_init(|| {
731 let mut t = [0u32; 256];
732 let mut n = 0usize;
733 while n < 256 {
734 let mut c = n as u32;
735 let mut k = 0;
736 while k < 8 {
737 c = if c & 1 != 0 {
738 0xedb8_8320 ^ (c >> 1)
739 } else {
740 c >> 1
741 };
742 k += 1;
743 }
744 t[n] = c;
745 n += 1;
746 }
747 t
748 })
749}
750
751fn crc32_update(mut crc: u32, data: &[u8]) -> u32 {
752 let t = crc_table();
753 for &b in data {
754 crc = t[((crc ^ b as u32) & 0xff) as usize] ^ (crc >> 8);
755 }
756 crc
757}
758
759#[cfg(test)]
760mod tests {
761 use super::*;
762
763 #[test]
765 fn rle_passthrough_without_escapes() {
766 let mut src = SliceSource::new(&[1, 2, 3, 4, 5]);
767 let out = rle_decode(&mut src, 5).unwrap();
768 assert_eq!(out, vec![1, 2, 3, 4, 5]);
769 }
770
771 #[test]
773 fn rle_run_repeats_previous_byte() {
774 let mut src = SliceSource::new(&[b'A', 0x81, 0x82, 0x04]);
775 let out = rle_decode(&mut src, 4).unwrap();
776 assert_eq!(out, vec![b'A', b'A', b'A', b'A']);
777 }
778
779 #[test]
781 fn rle_literal_escape_pair() {
782 let mut src = SliceSource::new(&[0x81, 0x82, 0x00]);
783 let out = rle_decode(&mut src, 2).unwrap();
784 assert_eq!(out, vec![0x81, 0x82]);
785 }
786
787 #[test]
790 fn crc32_empty_is_seed() {
791 assert_eq!(crc32_update(0xffff_ffff, &[]), 0xffff_ffff);
792 }
793
794 #[test]
797 fn crc32_check_value() {
798 let stored = crc32_update(0xffff_ffff, b"123456789");
799 assert_eq!(stored ^ 0xffff_ffff, 0xCBF4_3926);
800 }
801
802 #[test]
809 fn extract_real_compactpro_if_present() {
810 let path = std::path::Path::new("/Users/dani/Downloads/Dunjin44.cpt.hqx");
811 if !path.is_file() {
812 return;
813 }
814 let raw = std::fs::read(path).unwrap();
815 let bh = fs_mac::binhex::parse_binhex(&raw).expect("binhex decode");
816 assert!(is_compactpro(&bh.data_fork), "should detect Compact Pro");
817 let archive = parse(&bh.data_fork).expect("compactpro parse");
818 let files: Vec<_> = archive.entries.iter().filter(|e| !e.is_dir).collect();
819 assert!(!files.is_empty());
820
821 let mut verified = 0;
822 let mut saw_multiblock_fork = false;
823 for e in &files {
824 let decode = |f: &ForkInfo| {
825 decompress_fork(&bh.data_fork, f).unwrap_or_else(|err| panic!("{}: {err}", e.name))
826 };
827 let data = e.data.as_ref().map(&decode).unwrap_or_default();
828 let rsrc = e.rsrc.as_ref().map(&decode).unwrap_or_default();
829 if data.len() >= 150_000 || rsrc.len() >= 150_000 {
831 saw_multiblock_fork = true;
832 }
833 if let Some(crc) = e.data.as_ref().or(e.rsrc.as_ref()).map(|f| f.crc32) {
835 verify_entry_crc(&rsrc, &data, crc)
836 .unwrap_or_else(|err| panic!("{}: {err}", e.name));
837 verified += 1;
838 }
839 }
840 assert!(
841 saw_multiblock_fork,
842 "expected the 150 KB multi-block Dunjin Text fork"
843 );
844 assert!(verified > 0);
845 eprintln!("Compact Pro: CRC-verified {verified} entries (incl. multi-block)");
846 }
847
848 #[test]
851 fn cpinput_reads_bits_msb_first() {
852 let mut inp = CpInput::new(&[0b1011_0010]);
853 let bits: Vec<u32> = (0..8).map(|_| inp.next_bit()).collect();
854 assert_eq!(bits, vec![1, 0, 1, 1, 0, 0, 1, 0]);
855
856 let mut inp2 = CpInput::new(&[0b1100_1010, 0b1111_0000]);
857 assert_eq!(inp2.next_bits(4), 0b1100);
858 assert_eq!(inp2.next_bits(4), 0b1010);
859 assert_eq!(inp2.next_bits(8), 0b1111_0000);
860 }
861
862 #[test]
866 fn cpinput_byte_offset_is_ceil_of_consumed_bits() {
867 let data = [0xAC, 0x35, 0xFF, 0x00, 0x42];
868 let mut inp = CpInput::new(&data);
869 for k in 1..=(data.len() as u32 * 8) {
870 let _ = inp.next_bit();
871 assert_eq!(
872 inp.buffer_offset(),
873 k.div_ceil(8) as i64,
874 "after consuming {k} bits"
875 );
876 }
877 }
878
879 #[test]
883 fn cpinput_skip_to_byte_boundary_then_byte_read_aligns() {
884 let mut inp = CpInput::new(&[0xAB, 0xCD, 0xEF, 0x12]);
885 for _ in 0..3 {
886 inp.next_bit();
887 }
888 assert_eq!(inp.buffer_offset(), 1, "3 bits -> ceil = byte 1");
889 inp.skip_to_byte_boundary();
890 assert_eq!(inp.buffer_offset(), 1, "boundary holds at byte 1");
891 assert_eq!(inp.next_byte(), Some(0xCD), "byte read resumes at byte 1");
892 assert_eq!(inp.buffer_offset(), 2);
893 inp.skip_bytes(1);
895 assert_eq!(inp.next_byte(), Some(0x12));
896 }
897
898 #[test]
900 fn huff_single_symbol() {
901 let mut lengths = vec![0i32; 4];
902 lengths[2] = 1; let huff = Huff::build(&lengths).expect("valid code");
904 let mut input = CpInput::new(&[0x00]);
905 assert_eq!(huff.decode(&mut input), Some(2));
906 }
907
908 #[test]
911 fn verify_entry_crc_covers_rsrc_then_data() {
912 assert!(verify_entry_crc(&[], &[], 0xffff_ffff).is_ok());
914
915 let rsrc = b"RSRC-BYTES";
916 let data = b"DATA-BYTES";
917 let expected = crc32_update(crc32_update(0xffff_ffff, rsrc), data);
918 assert!(verify_entry_crc(rsrc, data, expected).is_ok());
919 assert!(verify_entry_crc(data, rsrc, expected).is_err());
921 assert!(verify_entry_crc(rsrc, data, expected ^ 1).is_err());
923 }
924
925 #[test]
928 fn huff_rejects_oversubscribed() {
929 let lengths = vec![1, 1, 1];
931 assert!(Huff::build(&lengths).is_none());
932 }
933}