feat: show the CAP's required libraries, package identity and components (v3.5.16)
The Import component (JC VM spec 6.6) is now exposed by /api/cap-info and rendered in the CAP analysis box: - imports: the libraries the CAP is linked against with the export-file versions and the number of distinct constant-pool references (6.7), displayed as "name >= version" (a card resolves an import only with the same major and a minor >= the recorded one, 4.5.2). Standard names come from the AID table; each gets a family label (Oracle JavaCard / ETSI SIM 2G / ETSI UICC / 3GPP USIM-ISIM / GlobalPlatform) and, for javacard.framework, a Java Card SDK release hint derived from the local Oracle SDK kit corpus (jc211..jc305u4 exports; unknown versions stay unhinted). Vendor/applet AIDs stay bare. - Header package flags (Table 6-4: int / exports / applet package) and the optional JC 2.2 package_name (absent in all CAP 2.1 files). - components: every archive entry in load-file order with its size and share of the load file (including the Directory/Export entries capmem does not parse); the sizes sum to load_file_bytes. - The PWA box leads with "Requires: ..." when imports exist, keeps the compiled-against line (Java Card hint + CAP format), the package/applet identity, the import details (family, refs, AID) and the component breakdown in Details; a memory-only response still renders. Tests: Python +2 (imports/flags/name/components; header flags + package name) with the synthetic CAP builder extended; frontend +2 renderer cases (unknown AIDs, no-import responses) plus jcAidNorm/jcAidFamily tests; the jcAidNorm extraction added to the ram/scp81 harnesses. Help EN/RU, docs/api.md, AGENTS. 591 frontend / 473 Python green; version 3.5.16; sw simple-v269.
This commit is contained in:
@@ -291,6 +291,18 @@ class Header(CapComponent):
|
||||
self.package_major = self.stream.read_u1()
|
||||
aid_len = self.stream.read_u1()
|
||||
self.aid = self.stream.read_bytes(aid_len)
|
||||
# JC VM spec 2.2.2 6.3: JC 2.2+ headers carry the package name as
|
||||
# package_name_info { u1 name_length; u1 name[name_length] } (UTF-8,
|
||||
# internal form, e.g. "javacard/framework"). CAP 2.1 files have no
|
||||
# bytes left after the AID.
|
||||
self.package_name = None
|
||||
if self.stream.available() >= 1:
|
||||
name_len = self.stream.read_u1()
|
||||
if 0 < name_len <= self.stream.available():
|
||||
try:
|
||||
self.package_name = self.stream.read_bytes(name_len).decode('utf-8')
|
||||
except UnicodeDecodeError:
|
||||
self.package_name = None
|
||||
|
||||
def package_version_str(self):
|
||||
return f"{self.package_major}.{self.package_minor}"
|
||||
@@ -763,14 +775,17 @@ class CAP:
|
||||
"""`data` is the .cap archive (a ZIP) as bytes."""
|
||||
self.data = data
|
||||
self.components = {}
|
||||
self.files = [] # [(component name, byte size)] in ZIP order
|
||||
self._read_cap()
|
||||
|
||||
def _read_cap(self):
|
||||
# ZIP archive of nested *.cap components
|
||||
with zipfile.ZipFile(io.BytesIO(self.data), 'r') as z:
|
||||
for name in z.namelist():
|
||||
if name.endswith('.cap'):
|
||||
self._add_component(z.read(name))
|
||||
if name.endswith('.cap') and not name.lower().endswith('.capx'):
|
||||
data = z.read(name)
|
||||
self.files.append((name.split('/')[-1][:-4], len(data)))
|
||||
self._add_component(data)
|
||||
|
||||
def _add_component(self, data):
|
||||
tag = data[0]
|
||||
@@ -1407,6 +1422,28 @@ def scan_method_bytecode(ms, cap, resolver, warnings=None):
|
||||
# Main Analysis
|
||||
# ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
def _external_ref_counts(cap):
|
||||
"""Distinct constant-pool references per imported package token (6.7):
|
||||
ClassRef/MethodRef/FieldRef/StaticRef entries whose package token points
|
||||
into the Import table. A package linked against but never referenced
|
||||
stays at 0."""
|
||||
counts = {}
|
||||
cp = cap.components.get('constant_pool')
|
||||
if not cp:
|
||||
return counts
|
||||
for e in cp.entries:
|
||||
ref = None
|
||||
if isinstance(e, (CPClassRef, CPMethodOrFieldRef)):
|
||||
ref = e.class_ref
|
||||
elif isinstance(e, CPStaticFieldRef):
|
||||
ref = e.static_field_ref
|
||||
elif isinstance(e, CPStaticMethodRef):
|
||||
ref = e.static_method_ref
|
||||
if ref is not None and not ref.is_internal:
|
||||
counts[ref.package_token] = counts.get(ref.package_token, 0) + 1
|
||||
return counts
|
||||
|
||||
|
||||
def analyze_bytes(cap_bytes, verbose=False):
|
||||
"""Analyze a CAP archive (bytes) and return (report, memory)."""
|
||||
cap = CAP(cap_bytes)
|
||||
@@ -1419,16 +1456,25 @@ def analyze_bytes(cap_bytes, verbose=False):
|
||||
report['package_version'] = h.package_version_str()
|
||||
report['package_aid'] = h.aid.hex().upper()
|
||||
|
||||
# Import packages
|
||||
# Import packages (JC VM spec 6.6): the libraries the package is linked
|
||||
# against, with the export-file versions recorded in the CAP
|
||||
if cap.has('import'):
|
||||
pkgs = cap.components['import'].packages
|
||||
refs = _external_ref_counts(cap)
|
||||
report['import_count'] = len(pkgs)
|
||||
report['packages'] = []
|
||||
for p in pkgs:
|
||||
report['packages'].append({
|
||||
'version': f'{p.major}.{p.minor}',
|
||||
'aid': p.aid_hex,
|
||||
})
|
||||
report['imports'] = [
|
||||
{'aid': p.aid_hex, 'minor': p.minor, 'major': p.major,
|
||||
'refs': refs.get(0x80 + i, 0)}
|
||||
for i, p in enumerate(pkgs)]
|
||||
|
||||
# Header package flags (Table 6-4) and the optional package name
|
||||
if cap.has('header'):
|
||||
h = cap.components['header']
|
||||
report['flags'] = h.flags
|
||||
report['package_name'] = h.package_name
|
||||
|
||||
# Component sizes (the load file order) for the breakdown display
|
||||
report['components'] = [{'name': name, 'size': size} for name, size in cap.files]
|
||||
|
||||
# Applets
|
||||
if cap.has('applet'):
|
||||
@@ -1653,6 +1699,18 @@ def memory_json(report, memory, load_file_bytes=None):
|
||||
'package_aid': report.get('package_aid'),
|
||||
'applet_count': report.get('applet_count', 0),
|
||||
'applets': [a.get('aid') for a in report.get('applets', [])],
|
||||
# libraries the CAP is linked against (JC VM spec 6.6) with the
|
||||
# distinct constant-pool reference counts (6.7)
|
||||
'imports': report.get('imports', []),
|
||||
# header package flags (Table 6-4: 0x01 int, 0x02 exports, 0x04 applet)
|
||||
'flags': {
|
||||
'raw': report.get('flags', 0),
|
||||
'int': bool(report.get('flags', 0) & 0x01),
|
||||
'export': bool(report.get('flags', 0) & 0x02),
|
||||
'applet': bool(report.get('flags', 0) & 0x04),
|
||||
},
|
||||
'package_name': report.get('package_name'),
|
||||
'components': report.get('components', []),
|
||||
'class_count': report.get('class_count', 0),
|
||||
'method_count': report.get('method_count', 0),
|
||||
'code': {
|
||||
|
||||
Reference in New Issue
Block a user