mod components;
pub use components::{AppletEntry, CapComponents};
use crate::aid::Aid;
use crate::limits::{INFLATE_WINDOW, LOAD_BLOCK_DATA};
const LFDB_COMPONENTS: usize = 11;
const COMPONENT_NAMES: [&[u8]; LFDB_COMPONENTS] = [
b"Header.cap",
b"Directory.cap",
b"Import.cap",
b"Applet.cap",
b"Class.cap",
b"Method.cap",
b"StaticField.cap",
b"Export.cap",
b"ConstantPool.cap",
b"RefLocation.cap",
b"Descriptor.cap",
];
const IDX_HEADER: usize = 0;
const IDX_IMPORT: usize = 2;
const IDX_APPLET: usize = 3;
const HEADER_MAGIC: u32 = 0xDECA_FFED;
const METHOD_STORED: u16 = 0;
const METHOD_DEFLATE: u16 = 8;
#[derive(Clone, Copy)]
struct CompLoc {
method: u16,
data_off: usize,
comp_size: usize,
uncomp_size: usize,
}
pub struct CapFile<'a> {
pub package_aid: Aid,
pub components: CapComponents,
pub(crate) zip: &'a [u8],
locs: [Option<CompLoc>; LFDB_COMPONENTS],
}
impl<'a> CapFile<'a> {
#[must_use]
pub fn lfdb(&self) -> LoadFileDataBlock<'a> {
let content_len = self.locs.iter().flatten().map(|c| c.uncomp_size).sum();
LoadFileDataBlock {
zip: self.zip,
locs: self.locs,
comp: 0,
cursor: CompCursor::new(),
header: LfdbHeader::new(content_len),
}
}
}
pub struct InflateCtx {
window: [u8; INFLATE_WINDOW],
state: miniz_oxide::inflate::core::DecompressorOxide,
}
impl Default for InflateCtx {
fn default() -> Self {
Self::new()
}
}
impl InflateCtx {
#[must_use]
#[expect(
clippy::large_stack_arrays,
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"
)]
pub fn new() -> Self {
Self {
window: [0u8; INFLATE_WINDOW],
state: miniz_oxide::inflate::core::DecompressorOxide::new(),
}
}
pub fn reset(&mut self) {
self.state = miniz_oxide::inflate::core::DecompressorOxide::new();
}
}
#[derive(Clone, Copy)]
struct CompCursor {
in_pos: usize,
produced: usize,
emitted: usize,
done: bool,
started: bool,
}
impl CompCursor {
const fn new() -> Self {
Self {
in_pos: 0,
produced: 0,
emitted: 0,
done: false,
started: false,
}
}
}
#[derive(Clone, Copy)]
struct LfdbHeader {
buf: [u8; 5],
len: u8,
emitted: u8,
}
impl LfdbHeader {
#[allow(clippy::cast_possible_truncation)] const fn new(content_len: usize) -> Self {
let mut buf = [0u8; 5];
buf[0] = 0xC4;
let len: u8 = if content_len < 0x80 {
buf[1] = content_len as u8;
2
} else if content_len <= 0xFF {
buf[1] = 0x81;
buf[2] = content_len as u8;
3
} else if content_len <= 0xFFFF {
buf[1] = 0x82;
buf[2] = (content_len >> 8) as u8;
buf[3] = content_len as u8;
4
} else {
buf[1] = 0x83;
buf[2] = (content_len >> 16) as u8;
buf[3] = (content_len >> 8) as u8;
buf[4] = content_len as u8;
5
};
Self {
buf,
len,
emitted: 0,
}
}
const fn remaining(self) -> usize {
(self.len - self.emitted) as usize
}
#[allow(clippy::cast_possible_truncation)] fn emit(&mut self, out: &mut [u8]) -> usize {
let from = self.emitted as usize;
let take = self.remaining().min(out.len());
out[..take].copy_from_slice(&self.buf[from..from + take]);
self.emitted += take as u8;
take
}
fn reset(&mut self) {
self.emitted = 0;
}
}
pub struct LoadFileDataBlock<'a> {
zip: &'a [u8],
locs: [Option<CompLoc>; LFDB_COMPONENTS],
comp: usize,
cursor: CompCursor,
header: LfdbHeader,
}
impl LoadFileDataBlock<'_> {
#[must_use]
pub fn len(&self) -> usize {
self.header.len as usize + self.content_len()
}
#[must_use]
pub fn content_len(&self) -> usize {
self.locs.iter().flatten().map(|c| c.uncomp_size).sum()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.content_len() == 0
}
pub fn next_block(&mut self, infl: &mut InflateCtx, out: &mut [u8]) -> Result<usize, CapError> {
let mut written = 0;
if self.header.remaining() > 0 {
written += self.header.emit(out);
}
while written < out.len() {
let Some(loc) = self.current_loc() else {
if self.comp >= LFDB_COMPONENTS {
break; }
self.advance();
continue;
};
let n = if loc.method == METHOD_DEFLATE {
self.emit_deflate(&loc, infl, &mut out[written..])?
} else {
self.emit_stored(&loc, &mut out[written..])
};
written += n;
if self.component_exhausted(&loc) {
self.advance();
} else if n == 0 {
break;
}
}
Ok(written)
}
pub fn reset(&mut self) {
self.comp = 0;
self.cursor = CompCursor::new();
self.header.reset();
}
fn current_loc(&self) -> Option<CompLoc> {
self.locs.get(self.comp).copied().flatten()
}
fn advance(&mut self) {
self.comp += 1;
self.cursor = CompCursor::new();
}
fn component_exhausted(&self, loc: &CompLoc) -> bool {
self.cursor.emitted >= loc.uncomp_size
}
fn emit_stored(&mut self, loc: &CompLoc, out: &mut [u8]) -> usize {
let start = loc.data_off + self.cursor.emitted;
let remaining = loc.uncomp_size - self.cursor.emitted;
let take = remaining
.min(out.len())
.min(self.zip.len().saturating_sub(start));
out[..take].copy_from_slice(&self.zip[start..start + take]);
self.cursor.emitted += take;
take
}
fn emit_deflate(
&mut self,
loc: &CompLoc,
infl: &mut InflateCtx,
out: &mut [u8],
) -> Result<usize, CapError> {
if !self.cursor.started {
infl.reset();
self.cursor.started = true;
}
let mut w = 0;
while w < out.len() {
let pending = self.cursor.produced - self.cursor.emitted;
if pending == 0 {
if self.cursor.done {
break;
}
self.pump(loc, infl)?;
if self.cursor.produced == self.cursor.emitted && self.cursor.done {
break;
}
continue;
}
let mask = INFLATE_WINDOW - 1;
let start = self.cursor.emitted & mask;
let take = pending.min(out.len() - w).min(INFLATE_WINDOW - start);
out[w..w + take].copy_from_slice(&infl.window[start..start + take]);
self.cursor.emitted += take;
w += take;
}
Ok(w)
}
fn pump(&mut self, loc: &CompLoc, infl: &mut InflateCtx) -> Result<(), CapError> {
use miniz_oxide::inflate::core::decompress;
use miniz_oxide::inflate::TINFLStatus;
let comp_end = loc.data_off + loc.comp_size;
if comp_end > self.zip.len() || loc.data_off + self.cursor.in_pos > comp_end {
return Err(CapError::Inflate);
}
let input = &self.zip[loc.data_off + self.cursor.in_pos..comp_end];
let ring_pos = self.cursor.produced & (INFLATE_WINDOW - 1);
let (status, in_consumed, out_written) =
decompress(&mut infl.state, input, &mut infl.window, ring_pos, 0);
self.cursor.in_pos += in_consumed;
self.cursor.produced += out_written;
match status {
TINFLStatus::Done => {
self.cursor.done = true;
Ok(())
}
TINFLStatus::HasMoreOutput | TINFLStatus::NeedsMoreInput => {
if in_consumed == 0 && out_written == 0 {
Err(CapError::Inflate)
} else {
Ok(())
}
}
_ => Err(CapError::Inflate),
}
}
}
pub fn parse<'a>(cap_zip: &'a [u8], infl: &mut InflateCtx) -> Result<CapFile<'a>, CapError> {
let locs = walk_zip(cap_zip)?;
let header_loc = locs[IDX_HEADER].ok_or(CapError::MissingComponent("Header.cap"))?;
let header = read_component(cap_zip, &header_loc, infl)?;
let (package_aid, jc_platform_version) = parse_header(header)?;
let mut components = CapComponents {
jc_platform_version,
imports: heapless::Vec::new(),
applets: heapless::Vec::new(),
};
if let Some(import_loc) = locs[IDX_IMPORT] {
let bytes = read_component(cap_zip, &import_loc, infl)?;
parse_imports(bytes, &mut components)?;
}
if let Some(applet_loc) = locs[IDX_APPLET] {
let bytes = read_component(cap_zip, &applet_loc, infl)?;
parse_applets(bytes, &mut components)?;
}
infl.reset();
Ok(CapFile {
package_aid,
components,
zip: cap_zip,
locs,
})
}
fn read_component<'a>(
zip: &[u8],
loc: &CompLoc,
infl: &'a mut InflateCtx,
) -> Result<&'a [u8], CapError> {
use miniz_oxide::inflate::core::decompress;
use miniz_oxide::inflate::core::inflate_flags::TINFL_FLAG_USING_NON_WRAPPING_OUTPUT_BUF;
use miniz_oxide::inflate::TINFLStatus;
let end = loc
.data_off
.checked_add(loc.comp_size)
.ok_or(CapError::Malformed)?;
if end > zip.len() {
return Err(CapError::Malformed);
}
let input = &zip[loc.data_off..end];
match loc.method {
METHOD_STORED => {
if input.len() > infl.window.len() {
return Err(CapError::Malformed);
}
infl.window[..input.len()].copy_from_slice(input);
Ok(&infl.window[..input.len()])
}
METHOD_DEFLATE => {
infl.reset();
let (status, _in, written) = decompress(
&mut infl.state,
input,
&mut infl.window,
0,
TINFL_FLAG_USING_NON_WRAPPING_OUTPUT_BUF,
);
match status {
TINFLStatus::Done => Ok(&infl.window[..written]),
_ => Err(CapError::Inflate),
}
}
_ => Err(CapError::Malformed),
}
}
const SIG_EOCD: u32 = 0x0605_4b50;
const SIG_CDH: u32 = 0x0201_4b50;
const SIG_LFH: u32 = 0x0403_4b50;
const EOCD_MIN: usize = 22;
fn walk_zip(zip: &[u8]) -> Result<[Option<CompLoc>; LFDB_COMPONENTS], CapError> {
let eocd = find_eocd(zip).ok_or(CapError::NotAZip)?;
let total = usize::from(u16::from_le_bytes([zip[eocd + 10], zip[eocd + 11]]));
let cd_off = read_u32(zip, eocd + 16).ok_or(CapError::Malformed)? as usize;
let mut locs: [Option<CompLoc>; LFDB_COMPONENTS] = [None; LFDB_COMPONENTS];
let mut pos = cd_off;
for _ in 0..total {
if read_u32(zip, pos) != Some(SIG_CDH) {
return Err(CapError::Malformed);
}
let method = read_u16(zip, pos + 10).ok_or(CapError::Malformed)?;
let comp_size = read_u32(zip, pos + 20).ok_or(CapError::Malformed)? as usize;
let uncomp_size = read_u32(zip, pos + 24).ok_or(CapError::Malformed)? as usize;
let name_len = usize::from(read_u16(zip, pos + 28).ok_or(CapError::Malformed)?);
let extra_len = usize::from(read_u16(zip, pos + 30).ok_or(CapError::Malformed)?);
let comment_len = usize::from(read_u16(zip, pos + 32).ok_or(CapError::Malformed)?);
let local_off = read_u32(zip, pos + 42).ok_or(CapError::Malformed)? as usize;
let name_start = pos + 46;
let name_end = name_start
.checked_add(name_len)
.ok_or(CapError::Malformed)?;
if name_end > zip.len() {
return Err(CapError::Malformed);
}
let name = &zip[name_start..name_end];
if let Some(idx) = component_index(name) {
if locs[idx].is_none() {
let data_off = local_data_offset(zip, local_off)?;
locs[idx] = Some(CompLoc {
method,
data_off,
comp_size,
uncomp_size,
});
}
}
pos = name_end
.checked_add(extra_len)
.and_then(|p| p.checked_add(comment_len))
.ok_or(CapError::Malformed)?;
}
Ok(locs)
}
fn find_eocd(zip: &[u8]) -> Option<usize> {
if zip.len() < EOCD_MIN {
return None;
}
let max_back = zip.len() - EOCD_MIN;
let limit = max_back.saturating_sub(0xFFFF);
let mut i = max_back;
loop {
if read_u32(zip, i) == Some(SIG_EOCD) {
return Some(i);
}
if i == 0 || i == limit {
return None;
}
i -= 1;
}
}
fn local_data_offset(zip: &[u8], local_off: usize) -> Result<usize, CapError> {
if read_u32(zip, local_off) != Some(SIG_LFH) {
return Err(CapError::Malformed);
}
let name_len = usize::from(read_u16(zip, local_off + 26).ok_or(CapError::Malformed)?);
let extra_len = usize::from(read_u16(zip, local_off + 28).ok_or(CapError::Malformed)?);
local_off
.checked_add(30)
.and_then(|p| p.checked_add(name_len))
.and_then(|p| p.checked_add(extra_len))
.filter(|&p| p <= zip.len())
.ok_or(CapError::Malformed)
}
fn component_index(name: &[u8]) -> Option<usize> {
let base = match name.iter().rposition(|&b| b == b'/' || b == b'\\') {
Some(i) => &name[i + 1..],
None => name,
};
COMPONENT_NAMES.iter().position(|&n| n == base)
}
fn parse_header(b: &[u8]) -> Result<(Aid, (u8, u8, u8)), CapError> {
let magic = read_u32_be(b, 3).ok_or(CapError::Malformed)?;
if magic != HEADER_MAGIC {
return Err(CapError::Malformed);
}
let minor = *b.get(7).ok_or(CapError::Malformed)?;
let major = *b.get(8).ok_or(CapError::Malformed)?;
let aid_len = usize::from(*b.get(12).ok_or(CapError::Malformed)?);
let aid_start = 13usize;
let aid_end = aid_start.checked_add(aid_len).ok_or(CapError::Malformed)?;
let aid_bytes = b.get(aid_start..aid_end).ok_or(CapError::Malformed)?;
let aid = Aid::new(aid_bytes).map_err(|_| CapError::Malformed)?;
Ok((aid, (major, minor, 0)))
}
fn parse_imports(b: &[u8], out: &mut CapComponents) -> Result<(), CapError> {
let count = usize::from(*b.get(3).ok_or(CapError::Malformed)?);
let mut p = 4;
for _ in 0..count {
let len = usize::from(*b.get(p + 2).ok_or(CapError::Malformed)?);
let aid_start = p + 3;
let aid_end = aid_start.checked_add(len).ok_or(CapError::Malformed)?;
let aid_bytes = b.get(aid_start..aid_end).ok_or(CapError::Malformed)?;
let aid = Aid::new(aid_bytes).map_err(|_| CapError::Malformed)?;
out.imports.push(aid).map_err(|_| CapError::Malformed)?;
p = aid_end;
}
Ok(())
}
fn parse_applets(b: &[u8], out: &mut CapComponents) -> Result<(), CapError> {
let count = usize::from(*b.get(3).ok_or(CapError::Malformed)?);
let mut p = 4;
for _ in 0..count {
let len = usize::from(*b.get(p).ok_or(CapError::Malformed)?);
let aid_start = p + 1;
let aid_end = aid_start.checked_add(len).ok_or(CapError::Malformed)?;
let aid_bytes = b.get(aid_start..aid_end).ok_or(CapError::Malformed)?;
let aid = Aid::new(aid_bytes).map_err(|_| CapError::Malformed)?;
let install_method_offset = read_u16_be(b, aid_end).ok_or(CapError::Malformed)?;
out.applets
.push(AppletEntry {
class_aid: aid,
install_method_offset,
})
.map_err(|_| CapError::Malformed)?;
p = aid_end + 2;
}
Ok(())
}
fn read_u16(b: &[u8], at: usize) -> Option<u16> {
let s = b.get(at..at + 2)?;
Some(u16::from_le_bytes([s[0], s[1]]))
}
fn read_u32(b: &[u8], at: usize) -> Option<u32> {
let s = b.get(at..at + 4)?;
Some(u32::from_le_bytes([s[0], s[1], s[2], s[3]]))
}
fn read_u16_be(b: &[u8], at: usize) -> Option<u16> {
let s = b.get(at..at + 2)?;
Some(u16::from_be_bytes([s[0], s[1]]))
}
fn read_u32_be(b: &[u8], at: usize) -> Option<u32> {
let s = b.get(at..at + 4)?;
Some(u32::from_be_bytes([s[0], s[1], s[2], s[3]]))
}
#[derive(thiserror::Error, Debug)]
#[non_exhaustive]
pub enum CapError {
#[error("input is not a ZIP")]
NotAZip,
#[error("missing CAP component: {0}")]
MissingComponent(&'static str),
#[error("malformed CAP structure")]
Malformed,
#[error("CAP component inflate failed")]
Inflate,
}
const _: usize = LOAD_BLOCK_DATA;
#[cfg(test)]
mod tests {
use super::*;
const STORED: &[u8] = include_bytes!("testdata/minimal_stored.cap");
const DEFLATE: &[u8] = include_bytes!("testdata/streaming_deflate.cap");
const PKG_AID: &[u8] = &[0xA0, 0x00, 0x00, 0x00, 0x62, 0x03, 0x01];
const IMPORT_AID: &[u8] = &[0xA0, 0x00, 0x00, 0x00, 0x62, 0x01, 0x01];
const APPLET_AID: &[u8] = &[0xA0, 0x00, 0x00, 0x00, 0x62, 0x03, 0x01, 0x0A];
fn stream_all(cf: &CapFile<'_>, infl: &mut InflateCtx) -> (usize, std::vec::Vec<u8>) {
let mut s = cf.lfdb();
let total = s.len();
let mut out: std::vec::Vec<u8> = std::vec::Vec::new();
let mut buf = [0u8; LOAD_BLOCK_DATA];
loop {
let n = s.next_block(infl, &mut buf).expect("inflate ok");
if n == 0 {
break;
}
out.extend_from_slice(&buf[..n]);
}
(total, out)
}
fn assert_metadata(cf: &CapFile<'_>) {
assert_eq!(cf.package_aid.as_bytes(), PKG_AID);
assert_eq!(cf.components.jc_platform_version, (2, 1, 0));
assert_eq!(cf.components.imports.len(), 1);
assert_eq!(cf.components.imports[0].as_bytes(), IMPORT_AID);
assert_eq!(cf.components.applets.len(), 1);
assert_eq!(cf.components.applets[0].class_aid.as_bytes(), APPLET_AID);
assert_eq!(cf.components.applets[0].install_method_offset, 0x001F);
}
#[test]
fn parses_stored_metadata() {
let mut infl = InflateCtx::new();
let cf = parse(STORED, &mut infl).expect("parse stored");
assert_metadata(&cf);
}
#[test]
fn parses_deflate_metadata() {
let mut infl = InflateCtx::new();
let cf = parse(DEFLATE, &mut infl).expect("parse deflate");
assert_metadata(&cf);
}
fn header_len(content_len: usize) -> usize {
LfdbHeader::new(content_len).len as usize
}
#[test]
fn stored_lfdb_is_concatenation_in_c2_order() {
let mut infl = InflateCtx::new();
let cf = parse(STORED, &mut infl).expect("parse");
let (total, out) = stream_all(&cf, &mut infl);
assert_eq!(total, out.len());
let content = 20 + 3 + 14 + 15 + 2 + 4 + 2 + 2 + 2 + 2 + 2;
let header = LfdbHeader::new(content);
let hdr = header.len as usize;
assert_eq!(total, hdr + content);
assert_eq!(&out[..hdr], &header.buf[..hdr]);
assert_eq!(&out[hdr + 3..hdr + 7], &[0xDE, 0xCA, 0xFF, 0xED]);
assert!(!out.windows(3).any(|w| w == b"DBG"));
}
#[test]
fn deflate_lfdb_streams_oversized_component_through_ring() {
let big = {
let mut v = std::vec::Vec::new();
while v.len() < 52_000 {
v.extend_from_slice(b"METHOD-BYTES-");
}
v.truncate(52_000);
v
};
let mut infl = InflateCtx::new();
let cf = parse(DEFLATE, &mut infl).expect("parse");
let (total, out) = stream_all(&cf, &mut infl);
assert_eq!(total, out.len());
let method_start_content = 20 + 3 + 14 + 15 + 2;
let content = method_start_content + big.len() + 2 + 2 + 2 + 2 + 2;
let hdr = header_len(content);
assert_eq!(total, hdr + content);
let method_start = hdr + method_start_content;
assert_eq!(&out[method_start..method_start + big.len()], &big[..]);
}
#[test]
fn lfdb_reset_re_streams_identically() {
let mut infl = InflateCtx::new();
let cf = parse(STORED, &mut infl).expect("parse");
let mut s = cf.lfdb();
let mut buf = [0u8; LOAD_BLOCK_DATA];
let first = s.next_block(&mut infl, &mut buf).expect("ok");
let head_a = buf[..first].to_vec();
s.reset();
infl.reset();
let second = s.next_block(&mut infl, &mut buf).expect("ok");
assert_eq!(first, second);
assert_eq!(head_a.as_slice(), &buf[..second]);
}
#[test]
fn not_a_zip_is_rejected() {
let mut infl = InflateCtx::new();
assert!(matches!(
parse(b"definitely not a zip", &mut infl),
Err(CapError::NotAZip)
));
}
#[test]
fn empty_input_is_rejected() {
let mut infl = InflateCtx::new();
assert!(matches!(parse(&[], &mut infl), Err(CapError::NotAZip)));
}
#[test]
fn missing_header_component_is_reported() {
let mut z = STORED.to_vec();
if let Some(p) = z.windows(10).position(|w| w == b"Header.cap") {
z[p] = b'X';
if let Some(p2) = z[p + 1..].windows(10).position(|w| w == b"Header.cap") {
z[p + 1 + p2] = b'X';
}
}
let mut infl = InflateCtx::new();
assert!(matches!(
parse(&z, &mut infl),
Err(CapError::MissingComponent("Header.cap"))
));
}
#[test]
fn truncated_zip_does_not_panic() {
let mut infl = InflateCtx::new();
for cut in [1usize, 5, 22, 40, 100, 200] {
let n = cut.min(STORED.len());
let _ = parse(&STORED[..n], &mut infl); }
}
#[test]
fn corrupt_deflate_stream_errors_cleanly() {
let mut z = DEFLATE.to_vec();
let mid = z.len() / 2;
z[mid] ^= 0xFF;
z[mid + 1] ^= 0xFF;
let mut infl = InflateCtx::new();
if let Ok(cf) = parse(&z, &mut infl) {
let mut s = cf.lfdb();
let mut buf = [0u8; LOAD_BLOCK_DATA];
while let Ok(n) = s.next_block(&mut infl, &mut buf) {
if n == 0 {
break; }
}
}
}
#[test]
fn component_index_matches_basename_only() {
assert_eq!(component_index(b"p/javacard/Header.cap"), Some(IDX_HEADER));
assert_eq!(component_index(b"Import.cap"), Some(IDX_IMPORT));
assert_eq!(component_index(b"Applet.cap"), Some(IDX_APPLET));
assert_eq!(component_index(b"Debug.cap"), None); assert_eq!(component_index(b"NotMyHeader.cap"), None);
assert_eq!(component_index(b"weird\\Class.cap"), Some(4));
}
}