fix: decode the Response Scripting template and the compact listings (v3.6.5)

Explore still missed the F0414C46416101 package on a card whose responses
wrap the R-APDU in the TS 102 226 5.2.2 Response Scripting template
(`AB <len> 80 <count> 23 <len> <R-APDU>`): `_decode_por` parsed that as a
compact response, so the frontend got `last_status_word` 81d0/7680 and data
starting `80 01 01 23 ...` instead of the listing.

- server: `_parse_response_scripting()` (AB definite / AF 80 ... 00 00
  indefinite) extracts the executed-command count and the last R-APDU's SW
  and data; `_decode_por` exposes it as `response_type: scripting` in the
  same `decoded` shape as compact, so the RAM explore paging and the RAM
  install `por_sw` see the real 9000/6310.
- frontend: the compact listing walk is deterministic on the AID length
  (`len AID life ver`; P1=10 adds `module_count (len module_AID)*`).
  `_parseRawAppEntry` no longer guesses `rawLen-1` for >8-byte AIDs (16-byte
  A113 applet AIDs were truncated), and `_parseRawElfEntry` no longer scans
  for `0x10` (a length/AID byte equal to 0x10 derailed the walk: a live page
  parsed to 1 entry with no F0414C46416101).
- tests: exact trace fixtures - the ELF page lists F0414C46416101 (11
  entries), the P1=10 page attaches its F0414C4641610101 module, the app page
  keeps the 16-byte A113 AIDs; Python covers the scripting template
  (definite/indefinite) against the live `AB 12` ISD and listing vectors.

610 frontend / 495 Python green; version 3.6.5; sw simple-v278.
This commit is contained in:
2026-09-28 00:30:49 +03:00
parent 9e85f522f6
commit 40f20d539e
7 changed files with 180 additions and 81 deletions
+39 -32
View File
@@ -1665,7 +1665,7 @@
// ===== Version =====
// Single source of truth for the PWA version: shown in the header and used
// by the server version check in pysimConnect().
const SIMPLE_VERSION = '3.6.4';
const SIMPLE_VERSION = '3.6.5';
document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION;
// ===== Tab switching =====
@@ -8461,42 +8461,48 @@ function _parseE3Entry(hex) {
// Raw format (P2=00): ISD uses pure AID length, Apps use combined length (AID+lifecycle).
// Heuristic: length > 8 means combined (AID = length-1 bytes, lifecycle inside length).
// Raw format (P2=00): consecutive <aid_len><AID><lifecycle><privileges>.
// The length byte is the AID length (16-byte A113/D276 AIDs included - the old
// "rawLen-1" guess truncated exactly those).
function _parseRawAppEntry(hex, i) {
const rawLen = parseInt(hex.substr(i, 2), 16);
if (rawLen < 1 || i + 2 + rawLen * 2 + 2 > hex.length) return null;
const aidLen = rawLen > 8 ? rawLen - 1 : rawLen;
const aidLen = parseInt(hex.substr(i, 2), 16);
if (aidLen < 5 || aidLen > 16 || i + 2 + aidLen * 2 + 4 > hex.length) return null;
const aid = hex.substr(i + 2, aidLen * 2).toUpperCase();
let j = i + 2 + rawLen * 2;
let j = i + 2 + aidLen * 2;
const lifecycle = hex.substr(j, 2).toUpperCase(); j += 2;
const privileges = hex.substr(j, 2).toUpperCase(); j += 2;
return { aid, lifecycle, privileges, next: j };
}
// Raw format (P2=00): ELF header then trailing bytes with module entries.
// P1=20: <ELF_header> <00> <module_entries separated by 00>
// P1=10: <ELF_header> <version(2B)> <SD_AID> <module_entries...>
// In both, a module entry starts with 0x10 (len=16) followed by 15-byte AID + lifecycle.
// Scan for 0x10 markers, advance past each full entry to avoid AID-internal false positives.
function _parseRawElfEntry(hex, i) {
// Raw format (P2=00) ELF entry: <aid_len><AID><lifecycle><version>; the P1=10
// (ELF + modules) listing appends module entries (<len><AID>[, lifecycle]).
// The walk is deterministic on the AID length - the old "scan for 0x10"
// heuristic derailed whenever a length byte or an AID byte happened to be
// 0x10 (which silently dropped entries like F0414C46416101).
function _parseRawElfEntry(hex, i, withModules) {
const aidLen = parseInt(hex.substr(i, 2), 16);
if (aidLen < 1 || i + 2 + aidLen * 2 + 2 > hex.length) return null;
if (aidLen < 5 || aidLen > 16 || i + 2 + aidLen * 2 + 4 > hex.length) return null;
const aid = hex.substr(i + 2, aidLen * 2).toUpperCase();
let j = i + 2 + aidLen * 2;
const lifecycle = hex.substr(j, 2).toUpperCase(); j += 2;
// Scan: when 0x10 found, validate 15-byte AID follows, then advance past full entry
const version = hex.substr(j + 2, 2).toUpperCase(); j += 2;
const out = { aid, lifecycle, version, next: j };
if (!withModules) return out;
// P1=10 (ELF + modules) entry: ...<lifecycle><version><module_count>
// followed by <module_len><module_AID> entries. Verified against two
// live traces (a F0414C46416101 ELF with its F0414C4641610101 module).
if (j + 2 > hex.length) return out;
const modCount = parseInt(hex.substr(j, 2), 16); j += 2;
const moduleAids = [];
const seen = new Set();
while (j + 32 <= hex.length) {
const tag = parseInt(hex.substr(j, 2), 16);
if (tag === 0x10) {
const modAid = hex.substr(j + 2, 30).toUpperCase();
if (!seen.has(modAid)) { seen.add(modAid); moduleAids.push(modAid); }
j += 32; // skip marker(1) + AID(15), continue past lifecycle+appCount
} else {
j += 2;
}
for (let k = 0; k < modCount && j + 4 <= hex.length; k++) {
const len = parseInt(hex.substr(j, 2), 16);
if (len < 5 || len > 16 || j + 2 + len * 2 > hex.length) break;
moduleAids.push(hex.substr(j + 2, len * 2).toUpperCase());
j += 2 + len * 2;
}
return { aid, lifecycle, moduleAids: moduleAids.length ? moduleAids : undefined, next: j };
if (moduleAids.length) out.moduleAids = moduleAids;
out.next = j;
return out;
}
function ramParseAppStatus(hex) {
@@ -8523,7 +8529,7 @@ function ramParseAppStatus(hex) {
return out;
}
function ramParseElfStatus(hex) {
function ramParseElfStatus(hex, withModules) {
const s = (hex || '').toUpperCase();
if (!s) return [];
// TLV format (P2=02): E3 templates with structured tags
@@ -8535,14 +8541,15 @@ function ramParseElfStatus(hex) {
return r;
}).filter(r => r.aid);
}
// Raw format (P2=00 fallback): consecutive <aid_len><AID><lifecycle>
// Raw format (P2=00 fallback): consecutive <aid_len><AID><lifecycle><version>
// entries; P1=10 pages carry the module list (withModules). Resync one byte
// at a time instead of stopping at the first unparsable byte.
const out = [];
let i = 0;
while (i + 6 <= s.length) {
const r = _parseRawElfEntry(s, i);
if (!r) break;
out.push({ type: 'elf', ...r });
i = r.next;
while (i + 4 <= s.length) {
const r = _parseRawElfEntry(s, i, withModules);
if (r) { out.push({ type: 'elf', ...r }); i = r.next; continue; }
i += 2;
}
return out;
}
@@ -8807,7 +8814,7 @@ async function ramExplore(sp) {
await paginate('80', isd, ramParseAppStatus, t('ISD'));
await paginate('40', apps, ramParseAppStatus, t('Apps'));
await paginate('20', elfs, ramParseElfStatus, t('ELFs'));
await paginate('10', modules, ramParseElfStatus, t('ELF Modules'));
await paginate('10', modules, (hex) => ramParseElfStatus(hex, true), t('ELF Modules'));
ramMergeElfData(elfs, modules);
ramSaveCntr(cntr);