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