1mod components;
24pub use components::{AppletEntry, CapComponents};
25
26use crate::aid::Aid;
27use crate::limits::{INFLATE_WINDOW, LOAD_BLOCK_DATA};
28
29const LFDB_COMPONENTS: usize = 11;
33
34const COMPONENT_NAMES: [&[u8]; LFDB_COMPONENTS] = [
37 b"Header.cap",
38 b"Directory.cap",
39 b"Import.cap",
40 b"Applet.cap",
41 b"Class.cap",
42 b"Method.cap",
43 b"StaticField.cap",
44 b"Export.cap",
45 b"ConstantPool.cap",
46 b"RefLocation.cap",
47 b"Descriptor.cap",
48];
49
50const IDX_HEADER: usize = 0;
52const IDX_IMPORT: usize = 2;
54const IDX_APPLET: usize = 3;
56
57const HEADER_MAGIC: u32 = 0xDECA_FFED;
59
60const METHOD_STORED: u16 = 0;
62const METHOD_DEFLATE: u16 = 8;
64
65#[derive(Clone, Copy)]
71struct CompLoc {
72 method: u16,
74 data_off: usize,
76 comp_size: usize,
78 uncomp_size: usize,
80}
81
82pub struct CapFile<'a> {
85 pub package_aid: Aid,
86 pub components: CapComponents,
88 pub(crate) zip: &'a [u8],
90 locs: [Option<CompLoc>; LFDB_COMPONENTS],
92}
93
94impl<'a> CapFile<'a> {
95 #[must_use]
100 pub fn lfdb(&self) -> LoadFileDataBlock<'a> {
101 let content_len = self.locs.iter().flatten().map(|c| c.uncomp_size).sum();
102 LoadFileDataBlock {
103 zip: self.zip,
104 locs: self.locs,
105 comp: 0,
106 cursor: CompCursor::new(),
107 header: LfdbHeader::new(content_len),
108 }
109 }
110}
111
112pub struct InflateCtx {
119 window: [u8; INFLATE_WINDOW],
120 state: miniz_oxide::inflate::core::DecompressorOxide,
121}
122
123impl Default for InflateCtx {
124 fn default() -> Self {
125 Self::new()
126 }
127}
128
129impl InflateCtx {
130 #[must_use]
132 #[expect(
133 clippy::large_stack_arrays,
134 reason = "32 KiB inflate window is intentional; the alloc-free no_std design lends it from a static or generous stack (PDD §5.4a) — heap allocation is unavailable"
135 )]
136 pub fn new() -> Self {
137 Self {
138 window: [0u8; INFLATE_WINDOW],
139 state: miniz_oxide::inflate::core::DecompressorOxide::new(),
140 }
141 }
142
143 pub fn reset(&mut self) {
145 self.state = miniz_oxide::inflate::core::DecompressorOxide::new();
146 }
147}
148
149#[derive(Clone, Copy)]
153struct CompCursor {
154 in_pos: usize,
156 produced: usize,
159 emitted: usize,
161 done: bool,
163 started: bool,
165}
166
167impl CompCursor {
168 const fn new() -> Self {
169 Self {
170 in_pos: 0,
171 produced: 0,
172 emitted: 0,
173 done: false,
174 started: false,
175 }
176 }
177}
178
179#[derive(Clone, Copy)]
187struct LfdbHeader {
188 buf: [u8; 5],
189 len: u8,
190 emitted: u8,
191}
192
193impl LfdbHeader {
194 #[allow(clippy::cast_possible_truncation)] const fn new(content_len: usize) -> Self {
201 let mut buf = [0u8; 5];
202 buf[0] = 0xC4;
203 let len: u8 = if content_len < 0x80 {
204 buf[1] = content_len as u8;
205 2
206 } else if content_len <= 0xFF {
207 buf[1] = 0x81;
208 buf[2] = content_len as u8;
209 3
210 } else if content_len <= 0xFFFF {
211 buf[1] = 0x82;
212 buf[2] = (content_len >> 8) as u8;
213 buf[3] = content_len as u8;
214 4
215 } else {
216 buf[1] = 0x83;
217 buf[2] = (content_len >> 16) as u8;
218 buf[3] = (content_len >> 8) as u8;
219 buf[4] = content_len as u8;
220 5
221 };
222 Self {
223 buf,
224 len,
225 emitted: 0,
226 }
227 }
228
229 const fn remaining(self) -> usize {
231 (self.len - self.emitted) as usize
232 }
233
234 #[allow(clippy::cast_possible_truncation)] fn emit(&mut self, out: &mut [u8]) -> usize {
237 let from = self.emitted as usize;
238 let take = self.remaining().min(out.len());
239 out[..take].copy_from_slice(&self.buf[from..from + take]);
240 self.emitted += take as u8;
241 take
242 }
243
244 fn reset(&mut self) {
246 self.emitted = 0;
247 }
248}
249
250pub struct LoadFileDataBlock<'a> {
254 zip: &'a [u8],
255 locs: [Option<CompLoc>; LFDB_COMPONENTS],
256 comp: usize,
258 cursor: CompCursor,
259 header: LfdbHeader,
261}
262
263impl LoadFileDataBlock<'_> {
264 #[must_use]
270 pub fn len(&self) -> usize {
271 self.header.len as usize + self.content_len()
272 }
273
274 #[must_use]
280 pub fn content_len(&self) -> usize {
281 self.locs.iter().flatten().map(|c| c.uncomp_size).sum()
282 }
283
284 #[must_use]
286 pub fn is_empty(&self) -> bool {
287 self.content_len() == 0
288 }
289
290 pub fn next_block(&mut self, infl: &mut InflateCtx, out: &mut [u8]) -> Result<usize, CapError> {
302 let mut written = 0;
303 if self.header.remaining() > 0 {
306 written += self.header.emit(out);
307 }
308 while written < out.len() {
309 let Some(loc) = self.current_loc() else {
311 if self.comp >= LFDB_COMPONENTS {
312 break; }
314 self.advance();
315 continue;
316 };
317 let n = if loc.method == METHOD_DEFLATE {
318 self.emit_deflate(&loc, infl, &mut out[written..])?
319 } else {
320 self.emit_stored(&loc, &mut out[written..])
321 };
322 written += n;
323 if self.component_exhausted(&loc) {
324 self.advance();
325 } else if n == 0 {
326 break;
328 }
329 }
330 Ok(written)
331 }
332
333 pub fn reset(&mut self) {
336 self.comp = 0;
337 self.cursor = CompCursor::new();
338 self.header.reset();
339 }
340
341 fn current_loc(&self) -> Option<CompLoc> {
344 self.locs.get(self.comp).copied().flatten()
345 }
346
347 fn advance(&mut self) {
349 self.comp += 1;
350 self.cursor = CompCursor::new();
351 }
352
353 fn component_exhausted(&self, loc: &CompLoc) -> bool {
355 self.cursor.emitted >= loc.uncomp_size
356 }
357
358 fn emit_stored(&mut self, loc: &CompLoc, out: &mut [u8]) -> usize {
360 let start = loc.data_off + self.cursor.emitted;
361 let remaining = loc.uncomp_size - self.cursor.emitted;
362 let take = remaining
363 .min(out.len())
364 .min(self.zip.len().saturating_sub(start));
365 out[..take].copy_from_slice(&self.zip[start..start + take]);
366 self.cursor.emitted += take;
367 take
368 }
369
370 fn emit_deflate(
372 &mut self,
373 loc: &CompLoc,
374 infl: &mut InflateCtx,
375 out: &mut [u8],
376 ) -> Result<usize, CapError> {
377 if !self.cursor.started {
378 infl.reset();
379 self.cursor.started = true;
380 }
381 let mut w = 0;
382 while w < out.len() {
383 let pending = self.cursor.produced - self.cursor.emitted;
384 if pending == 0 {
385 if self.cursor.done {
386 break;
387 }
388 self.pump(loc, infl)?;
389 if self.cursor.produced == self.cursor.emitted && self.cursor.done {
390 break;
391 }
392 continue;
393 }
394 let mask = INFLATE_WINDOW - 1;
395 let start = self.cursor.emitted & mask;
396 let take = pending.min(out.len() - w).min(INFLATE_WINDOW - start);
397 out[w..w + take].copy_from_slice(&infl.window[start..start + take]);
398 self.cursor.emitted += take;
399 w += take;
400 }
401 Ok(w)
402 }
403
404 fn pump(&mut self, loc: &CompLoc, infl: &mut InflateCtx) -> Result<(), CapError> {
406 use miniz_oxide::inflate::core::decompress;
407 use miniz_oxide::inflate::TINFLStatus;
408
409 let comp_end = loc.data_off + loc.comp_size;
410 if comp_end > self.zip.len() || loc.data_off + self.cursor.in_pos > comp_end {
411 return Err(CapError::Inflate);
412 }
413 let input = &self.zip[loc.data_off + self.cursor.in_pos..comp_end];
414 let ring_pos = self.cursor.produced & (INFLATE_WINDOW - 1);
415 let (status, in_consumed, out_written) =
417 decompress(&mut infl.state, input, &mut infl.window, ring_pos, 0);
418 self.cursor.in_pos += in_consumed;
419 self.cursor.produced += out_written;
420 match status {
421 TINFLStatus::Done => {
422 self.cursor.done = true;
423 Ok(())
424 }
425 TINFLStatus::HasMoreOutput | TINFLStatus::NeedsMoreInput => {
426 if in_consumed == 0 && out_written == 0 {
427 Err(CapError::Inflate)
429 } else {
430 Ok(())
431 }
432 }
433 _ => Err(CapError::Inflate),
434 }
435 }
436}
437
438pub fn parse<'a>(cap_zip: &'a [u8], infl: &mut InflateCtx) -> Result<CapFile<'a>, CapError> {
452 let locs = walk_zip(cap_zip)?;
453
454 let header_loc = locs[IDX_HEADER].ok_or(CapError::MissingComponent("Header.cap"))?;
455 let header = read_component(cap_zip, &header_loc, infl)?;
458 let (package_aid, jc_platform_version) = parse_header(header)?;
459
460 let mut components = CapComponents {
461 jc_platform_version,
462 imports: heapless::Vec::new(),
463 applets: heapless::Vec::new(),
464 };
465
466 if let Some(import_loc) = locs[IDX_IMPORT] {
467 let bytes = read_component(cap_zip, &import_loc, infl)?;
468 parse_imports(bytes, &mut components)?;
469 }
470 if let Some(applet_loc) = locs[IDX_APPLET] {
471 let bytes = read_component(cap_zip, &applet_loc, infl)?;
472 parse_applets(bytes, &mut components)?;
473 }
474
475 infl.reset();
476 Ok(CapFile {
477 package_aid,
478 components,
479 zip: cap_zip,
480 locs,
481 })
482}
483
484fn read_component<'a>(
491 zip: &[u8],
492 loc: &CompLoc,
493 infl: &'a mut InflateCtx,
494) -> Result<&'a [u8], CapError> {
495 use miniz_oxide::inflate::core::decompress;
496 use miniz_oxide::inflate::core::inflate_flags::TINFL_FLAG_USING_NON_WRAPPING_OUTPUT_BUF;
497 use miniz_oxide::inflate::TINFLStatus;
498
499 let end = loc
500 .data_off
501 .checked_add(loc.comp_size)
502 .ok_or(CapError::Malformed)?;
503 if end > zip.len() {
504 return Err(CapError::Malformed);
505 }
506 let input = &zip[loc.data_off..end];
507 match loc.method {
508 METHOD_STORED => {
509 if input.len() > infl.window.len() {
510 return Err(CapError::Malformed);
511 }
512 infl.window[..input.len()].copy_from_slice(input);
513 Ok(&infl.window[..input.len()])
514 }
515 METHOD_DEFLATE => {
516 infl.reset();
517 let (status, _in, written) = decompress(
518 &mut infl.state,
519 input,
520 &mut infl.window,
521 0,
522 TINFL_FLAG_USING_NON_WRAPPING_OUTPUT_BUF,
523 );
524 match status {
525 TINFLStatus::Done => Ok(&infl.window[..written]),
526 _ => Err(CapError::Inflate),
529 }
530 }
531 _ => Err(CapError::Malformed),
532 }
533}
534
535const SIG_EOCD: u32 = 0x0605_4b50;
539const SIG_CDH: u32 = 0x0201_4b50;
541const SIG_LFH: u32 = 0x0403_4b50;
543const EOCD_MIN: usize = 22;
545
546fn walk_zip(zip: &[u8]) -> Result<[Option<CompLoc>; LFDB_COMPONENTS], CapError> {
548 let eocd = find_eocd(zip).ok_or(CapError::NotAZip)?;
549 let total = usize::from(u16::from_le_bytes([zip[eocd + 10], zip[eocd + 11]]));
550 let cd_off = read_u32(zip, eocd + 16).ok_or(CapError::Malformed)? as usize;
551
552 let mut locs: [Option<CompLoc>; LFDB_COMPONENTS] = [None; LFDB_COMPONENTS];
553 let mut pos = cd_off;
554 for _ in 0..total {
555 if read_u32(zip, pos) != Some(SIG_CDH) {
556 return Err(CapError::Malformed);
557 }
558 let method = read_u16(zip, pos + 10).ok_or(CapError::Malformed)?;
559 let comp_size = read_u32(zip, pos + 20).ok_or(CapError::Malformed)? as usize;
560 let uncomp_size = read_u32(zip, pos + 24).ok_or(CapError::Malformed)? as usize;
561 let name_len = usize::from(read_u16(zip, pos + 28).ok_or(CapError::Malformed)?);
562 let extra_len = usize::from(read_u16(zip, pos + 30).ok_or(CapError::Malformed)?);
563 let comment_len = usize::from(read_u16(zip, pos + 32).ok_or(CapError::Malformed)?);
564 let local_off = read_u32(zip, pos + 42).ok_or(CapError::Malformed)? as usize;
565
566 let name_start = pos + 46;
567 let name_end = name_start
568 .checked_add(name_len)
569 .ok_or(CapError::Malformed)?;
570 if name_end > zip.len() {
571 return Err(CapError::Malformed);
572 }
573 let name = &zip[name_start..name_end];
574
575 if let Some(idx) = component_index(name) {
576 if locs[idx].is_none() {
577 let data_off = local_data_offset(zip, local_off)?;
578 locs[idx] = Some(CompLoc {
579 method,
580 data_off,
581 comp_size,
582 uncomp_size,
583 });
584 }
585 }
586
587 pos = name_end
588 .checked_add(extra_len)
589 .and_then(|p| p.checked_add(comment_len))
590 .ok_or(CapError::Malformed)?;
591 }
592 Ok(locs)
593}
594
595fn find_eocd(zip: &[u8]) -> Option<usize> {
597 if zip.len() < EOCD_MIN {
598 return None;
599 }
600 let max_back = zip.len() - EOCD_MIN;
601 let limit = max_back.saturating_sub(0xFFFF);
603 let mut i = max_back;
604 loop {
605 if read_u32(zip, i) == Some(SIG_EOCD) {
606 return Some(i);
607 }
608 if i == 0 || i == limit {
609 return None;
610 }
611 i -= 1;
612 }
613}
614
615fn local_data_offset(zip: &[u8], local_off: usize) -> Result<usize, CapError> {
617 if read_u32(zip, local_off) != Some(SIG_LFH) {
618 return Err(CapError::Malformed);
619 }
620 let name_len = usize::from(read_u16(zip, local_off + 26).ok_or(CapError::Malformed)?);
621 let extra_len = usize::from(read_u16(zip, local_off + 28).ok_or(CapError::Malformed)?);
622 local_off
623 .checked_add(30)
624 .and_then(|p| p.checked_add(name_len))
625 .and_then(|p| p.checked_add(extra_len))
626 .filter(|&p| p <= zip.len())
627 .ok_or(CapError::Malformed)
628}
629
630fn component_index(name: &[u8]) -> Option<usize> {
632 let base = match name.iter().rposition(|&b| b == b'/' || b == b'\\') {
633 Some(i) => &name[i + 1..],
634 None => name,
635 };
636 COMPONENT_NAMES.iter().position(|&n| n == base)
637}
638
639fn parse_header(b: &[u8]) -> Result<(Aid, (u8, u8, u8)), CapError> {
644 let magic = read_u32_be(b, 3).ok_or(CapError::Malformed)?;
646 if magic != HEADER_MAGIC {
647 return Err(CapError::Malformed);
648 }
649 let minor = *b.get(7).ok_or(CapError::Malformed)?;
650 let major = *b.get(8).ok_or(CapError::Malformed)?;
651 let aid_len = usize::from(*b.get(12).ok_or(CapError::Malformed)?);
652 let aid_start = 13usize;
653 let aid_end = aid_start.checked_add(aid_len).ok_or(CapError::Malformed)?;
654 let aid_bytes = b.get(aid_start..aid_end).ok_or(CapError::Malformed)?;
655 let aid = Aid::new(aid_bytes).map_err(|_| CapError::Malformed)?;
656 Ok((aid, (major, minor, 0)))
657}
658
659fn parse_imports(b: &[u8], out: &mut CapComponents) -> Result<(), CapError> {
662 let count = usize::from(*b.get(3).ok_or(CapError::Malformed)?);
664 let mut p = 4;
665 for _ in 0..count {
666 let len = usize::from(*b.get(p + 2).ok_or(CapError::Malformed)?);
667 let aid_start = p + 3;
668 let aid_end = aid_start.checked_add(len).ok_or(CapError::Malformed)?;
669 let aid_bytes = b.get(aid_start..aid_end).ok_or(CapError::Malformed)?;
670 let aid = Aid::new(aid_bytes).map_err(|_| CapError::Malformed)?;
671 out.imports.push(aid).map_err(|_| CapError::Malformed)?;
672 p = aid_end;
673 }
674 Ok(())
675}
676
677fn parse_applets(b: &[u8], out: &mut CapComponents) -> Result<(), CapError> {
680 let count = usize::from(*b.get(3).ok_or(CapError::Malformed)?);
682 let mut p = 4;
683 for _ in 0..count {
684 let len = usize::from(*b.get(p).ok_or(CapError::Malformed)?);
685 let aid_start = p + 1;
686 let aid_end = aid_start.checked_add(len).ok_or(CapError::Malformed)?;
687 let aid_bytes = b.get(aid_start..aid_end).ok_or(CapError::Malformed)?;
688 let aid = Aid::new(aid_bytes).map_err(|_| CapError::Malformed)?;
689 let install_method_offset = read_u16_be(b, aid_end).ok_or(CapError::Malformed)?;
690 out.applets
691 .push(AppletEntry {
692 class_aid: aid,
693 install_method_offset,
694 })
695 .map_err(|_| CapError::Malformed)?;
696 p = aid_end + 2;
697 }
698 Ok(())
699}
700
701fn read_u16(b: &[u8], at: usize) -> Option<u16> {
704 let s = b.get(at..at + 2)?;
705 Some(u16::from_le_bytes([s[0], s[1]]))
706}
707
708fn read_u32(b: &[u8], at: usize) -> Option<u32> {
709 let s = b.get(at..at + 4)?;
710 Some(u32::from_le_bytes([s[0], s[1], s[2], s[3]]))
711}
712
713fn read_u16_be(b: &[u8], at: usize) -> Option<u16> {
714 let s = b.get(at..at + 2)?;
715 Some(u16::from_be_bytes([s[0], s[1]]))
716}
717
718fn read_u32_be(b: &[u8], at: usize) -> Option<u32> {
719 let s = b.get(at..at + 4)?;
720 Some(u32::from_be_bytes([s[0], s[1], s[2], s[3]]))
721}
722
723#[derive(thiserror::Error, Debug)]
725#[non_exhaustive]
726pub enum CapError {
727 #[error("input is not a ZIP")]
728 NotAZip,
729 #[error("missing CAP component: {0}")]
730 MissingComponent(&'static str),
731 #[error("malformed CAP structure")]
732 Malformed,
733 #[error("CAP component inflate failed")]
736 Inflate,
737}
738
739const _: usize = LOAD_BLOCK_DATA;
742
743#[cfg(test)]
744mod tests {
745 use super::*;
746
747 const STORED: &[u8] = include_bytes!("testdata/minimal_stored.cap");
750 const DEFLATE: &[u8] = include_bytes!("testdata/streaming_deflate.cap");
751
752 const PKG_AID: &[u8] = &[0xA0, 0x00, 0x00, 0x00, 0x62, 0x03, 0x01];
754 const IMPORT_AID: &[u8] = &[0xA0, 0x00, 0x00, 0x00, 0x62, 0x01, 0x01];
755 const APPLET_AID: &[u8] = &[0xA0, 0x00, 0x00, 0x00, 0x62, 0x03, 0x01, 0x0A];
756
757 fn stream_all(cf: &CapFile<'_>, infl: &mut InflateCtx) -> (usize, std::vec::Vec<u8>) {
758 let mut s = cf.lfdb();
759 let total = s.len();
760 let mut out: std::vec::Vec<u8> = std::vec::Vec::new();
761 let mut buf = [0u8; LOAD_BLOCK_DATA];
762 loop {
763 let n = s.next_block(infl, &mut buf).expect("inflate ok");
764 if n == 0 {
765 break;
766 }
767 out.extend_from_slice(&buf[..n]);
768 }
769 (total, out)
770 }
771
772 fn assert_metadata(cf: &CapFile<'_>) {
773 assert_eq!(cf.package_aid.as_bytes(), PKG_AID);
774 assert_eq!(cf.components.jc_platform_version, (2, 1, 0));
775 assert_eq!(cf.components.imports.len(), 1);
776 assert_eq!(cf.components.imports[0].as_bytes(), IMPORT_AID);
777 assert_eq!(cf.components.applets.len(), 1);
778 assert_eq!(cf.components.applets[0].class_aid.as_bytes(), APPLET_AID);
779 assert_eq!(cf.components.applets[0].install_method_offset, 0x001F);
780 }
781
782 #[test]
783 fn parses_stored_metadata() {
784 let mut infl = InflateCtx::new();
785 let cf = parse(STORED, &mut infl).expect("parse stored");
786 assert_metadata(&cf);
787 }
788
789 #[test]
790 fn parses_deflate_metadata() {
791 let mut infl = InflateCtx::new();
792 let cf = parse(DEFLATE, &mut infl).expect("parse deflate");
793 assert_metadata(&cf);
794 }
795
796 fn header_len(content_len: usize) -> usize {
797 LfdbHeader::new(content_len).len as usize
798 }
799
800 #[test]
801 fn stored_lfdb_is_concatenation_in_c2_order() {
802 let mut infl = InflateCtx::new();
805 let cf = parse(STORED, &mut infl).expect("parse");
806 let (total, out) = stream_all(&cf, &mut infl);
807 assert_eq!(total, out.len());
808 let content = 20 + 3 + 14 + 15 + 2 + 4 + 2 + 2 + 2 + 2 + 2;
809 let header = LfdbHeader::new(content);
810 let hdr = header.len as usize;
811 assert_eq!(total, hdr + content);
814 assert_eq!(&out[..hdr], &header.buf[..hdr]);
815 assert_eq!(&out[hdr + 3..hdr + 7], &[0xDE, 0xCA, 0xFF, 0xED]);
818 assert!(!out.windows(3).any(|w| w == b"DBG"));
820 }
821
822 #[test]
823 fn deflate_lfdb_streams_oversized_component_through_ring() {
824 let big = {
827 let mut v = std::vec::Vec::new();
828 while v.len() < 52_000 {
829 v.extend_from_slice(b"METHOD-BYTES-");
830 }
831 v.truncate(52_000);
832 v
833 };
834 let mut infl = InflateCtx::new();
835 let cf = parse(DEFLATE, &mut infl).expect("parse");
836 let (total, out) = stream_all(&cf, &mut infl);
837 assert_eq!(total, out.len());
838 let method_start_content = 20 + 3 + 14 + 15 + 2;
841 let content = method_start_content + big.len() + 2 + 2 + 2 + 2 + 2;
842 let hdr = header_len(content);
843 assert_eq!(total, hdr + content);
844 let method_start = hdr + method_start_content;
846 assert_eq!(&out[method_start..method_start + big.len()], &big[..]);
847 }
848
849 #[test]
850 fn lfdb_reset_re_streams_identically() {
851 let mut infl = InflateCtx::new();
852 let cf = parse(STORED, &mut infl).expect("parse");
853 let mut s = cf.lfdb();
854 let mut buf = [0u8; LOAD_BLOCK_DATA];
855 let first = s.next_block(&mut infl, &mut buf).expect("ok");
856 let head_a = buf[..first].to_vec();
857 s.reset();
858 infl.reset();
859 let second = s.next_block(&mut infl, &mut buf).expect("ok");
860 assert_eq!(first, second);
861 assert_eq!(head_a.as_slice(), &buf[..second]);
862 }
863
864 #[test]
865 fn not_a_zip_is_rejected() {
866 let mut infl = InflateCtx::new();
867 assert!(matches!(
868 parse(b"definitely not a zip", &mut infl),
869 Err(CapError::NotAZip)
870 ));
871 }
872
873 #[test]
874 fn empty_input_is_rejected() {
875 let mut infl = InflateCtx::new();
876 assert!(matches!(parse(&[], &mut infl), Err(CapError::NotAZip)));
877 }
878
879 #[test]
880 fn missing_header_component_is_reported() {
881 let mut z = STORED.to_vec();
885 if let Some(p) = z.windows(10).position(|w| w == b"Header.cap") {
887 z[p] = b'X';
888 if let Some(p2) = z[p + 1..].windows(10).position(|w| w == b"Header.cap") {
890 z[p + 1 + p2] = b'X';
891 }
892 }
893 let mut infl = InflateCtx::new();
894 assert!(matches!(
895 parse(&z, &mut infl),
896 Err(CapError::MissingComponent("Header.cap"))
897 ));
898 }
899
900 #[test]
901 fn truncated_zip_does_not_panic() {
902 let mut infl = InflateCtx::new();
903 for cut in [1usize, 5, 22, 40, 100, 200] {
904 let n = cut.min(STORED.len());
905 let _ = parse(&STORED[..n], &mut infl); }
907 }
908
909 #[test]
910 fn corrupt_deflate_stream_errors_cleanly() {
911 let mut z = DEFLATE.to_vec();
915 let mid = z.len() / 2;
916 z[mid] ^= 0xFF;
917 z[mid + 1] ^= 0xFF;
918 let mut infl = InflateCtx::new();
919 if let Ok(cf) = parse(&z, &mut infl) {
922 let mut s = cf.lfdb();
923 let mut buf = [0u8; LOAD_BLOCK_DATA];
924 while let Ok(n) = s.next_block(&mut infl, &mut buf) {
925 if n == 0 {
926 break; }
928 }
929 }
930 }
931
932 #[test]
933 fn component_index_matches_basename_only() {
934 assert_eq!(component_index(b"p/javacard/Header.cap"), Some(IDX_HEADER));
935 assert_eq!(component_index(b"Import.cap"), Some(IDX_IMPORT));
936 assert_eq!(component_index(b"Applet.cap"), Some(IDX_APPLET));
937 assert_eq!(component_index(b"Debug.cap"), None); assert_eq!(component_index(b"NotMyHeader.cap"), None);
939 assert_eq!(component_index(b"weird\\Class.cap"), Some(4));
940 }
941}