feat: chained GET RESPONSE + ELF versions from app instances (v3.6.56)

The live log (2026-09-30) showed our standalone GET RESPONSE was wrong: the
card answered 6700 (wrong length) to 00C00000<xx> for the 61xx answers of
the expanded ISD query and the ELF notags query.  The compact path proves
how this card wants it: GET STATUS and GET RESPONSE chained in one compact
command string (80F2..C0000000).

- a 61xx answer now retries the same query with ramChainGetResponse
  (+ C0000000, compact format only; the expanded AA/22 form has no command
  string, so no retry there);
- a 61xx page still continues to the next occurrence (the GET RESPONSE's own
  SW replaces the 6310 signal); the ISD stays a single occurrence;
- the standalone 00C00000<xx> attempt is gone (it only ever answered 6700).

This card also refuses every tag list for the ELF queries (tags/noea/lean
all 6A88), so CE cannot come from the ELF listing.  Its apps listing does
carry the load file version (CE) together with the package AID (C4), so
ramDeriveElfVersions fills a missing ELF version from its applet instance
and the row annotates it: 'Version: 9.3 (from the app instance)'.  The
version hint now shows whenever any ELF row lacks a version.

sw cache simple-v329; tests: 550 Python / 711 Node.
This commit is contained in:
2026-09-30 10:08:24 +03:00
parent b7798a7fd9
commit 61bb1ab621
5 changed files with 99 additions and 30 deletions
+53 -17
View File
@@ -1749,7 +1749,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.55';
const SIMPLE_VERSION = '3.6.56';
document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION;
// ===== Tab switching =====
@@ -9238,6 +9238,24 @@ function ramElfVersionHint(elfForm) {
return t('Package versions come from the expanded registry query \u2014 this card did not return them.');
}
// Fill a missing ELF version from an application entry: cards commonly return
// the load file version (CE) on the app entry together with its package AID
// (C4), so the package's version can be read from its applet instance (live
// 2026-09-30: the ELF listing refused every tag list while the apps listing
// carried CE). The row is annotated (`versionFrom`) so the inference stays
// visible. Packages without an applet instance get no version this way.
function ramDeriveElfVersions(elfs, apps) {
(elfs || []).forEach(e => {
if (e.version || !e.aid) return;
const app = (apps || []).find(a => a.version && a.elfAid &&
String(a.elfAid).toUpperCase() === String(e.aid).toUpperCase());
if (app) {
e.version = app.version;
e.versionFrom = 'app';
}
});
}
// Render the full explorer result as structured HTML
function ramRenderExploreHtml(mem, isd, apps, elfs, fmt, expanded, elfForm) {
let html = '';
@@ -9301,7 +9319,7 @@ function ramRenderExploreHtml(mem, isd, apps, elfs, fmt, expanded, elfForm) {
if (elfs && elfs.length) {
html += '<div class="mb-4">';
html += '<div class="font-semibold text-sm mb-1">' + esc(t('Executable Load Files (ELFs) / Packages')) + ' (' + elfs.length + ')</div>';
if (!elfs.some(o => o.version)) {
if (elfs.some(o => !o.version)) {
html += '<div class="mb-1 text-xs text-gray-500 dark:text-slate-400">' + esc(ramElfVersionHint(elfForm)) + '</div>';
}
elfs.forEach(o => {
@@ -9309,7 +9327,11 @@ function ramRenderExploreHtml(mem, isd, apps, elfs, fmt, expanded, elfForm) {
html += '<div class="flex-1 min-w-0">';
html += '<div>' + esc(t('Load File AID / Package AID:')) + ' ' + (o.aid ? jcAidHtml(o.aid) : '?') + '</div>';
html += '<div>' + esc(t('Lifecycle:')) + ' ' + ramFmtLifecycle(o.lifecycle || '', 'elf') + '</div>';
if (o.version) html += '<div>' + esc(t('Version:')) + ' ' + esc(ramFmtVersion(o.version)) + '</div>';
if (o.version) {
html += '<div>' + esc(t('Version:')) + ' ' + esc(ramFmtVersion(o.version)) +
(o.versionFrom === 'app' ? ' <span class="text-gray-500 dark:text-slate-400">(' + esc(t('from the app instance')) + ')</span>' : '') +
'</div>';
}
if (o.privileges && o.privileges !== '00') html += '<div>' + esc(t('Privileges:')) + ' ' + ramFmtPrivileges(o.privileges) + ' (' + o.privileges + ')</div>';
if (o.moduleAids && o.moduleAids.length) {
html += '<div>' + esc(t('Executable Module AIDs / Applet Class AIDs:')) + '</div>';
@@ -9407,15 +9429,24 @@ async function ramExplore(sp) {
};
let page = 0;
while (accepted && page++ < 8) {
data += await sendOne(ramExpandedQueryApdu(p1, form, page > 1),
page > 1 ? 'P2=03 (' + form + ')' : 'P2=02 (' + form + ')');
const apdu = ramExpandedQueryApdu(p1, form, page > 1);
const what = page > 1 ? 'P2=03 (' + form + ')' : 'P2=02 (' + form + ')';
data += await sendOne(apdu, what);
if (!accepted) break;
let rounds = 0;
while (/^61[0-9A-F]{2}$/.test(sw) && rounds++ < 4) {
data += await sendOne(ramGetResponseApdu(sw), 'GET RESPONSE');
// '61xx': the card holds the response data - the GET RESPONSE
// must ride in the same compact command string (a standalone
// one answers 6700 on the live card); the expanded AA/22
// format has no command string, so there is no retry there.
const pending = /^61[0-9A-F]{2}$/.test(sw);
if (pending && sp.ram_format !== 'expanded') {
data += await sendOne(ramChainGetResponse(apdu), what + ' + GET RESPONSE');
if (!accepted) break;
}
if (!accepted || sw !== '6310') break;
// the ISD is a single occurrence (GP 11.4.2.2); '6310' means
// more occurrences, and a '61xx' page may mask that signal
// (the GET RESPONSE's own SW replaces it) - try one more page
if (p1 === '80') break;
if (sw !== '6310' && !pending) break;
}
const parsed = data ? parser(data) : [];
if (parsed.length) {
@@ -9541,8 +9572,7 @@ async function ramExplore(sp) {
// P1 per GP Card Spec v2.3.1 table 11-33:
// 80=ISD, 40=Applications, 20=Executable Load Files (ELFs), 10=ELF+modules
await paginate('80', isd, ramParseAppStatus, t('ISD'));
await paginate('40', apps, ramParseAppStatus, t('Apps'));
// GP 11.4.2.1: '10' returns the ELFs *with* their modules - the superset of
await paginate('40', apps, ramParseAppStatus, t('Apps')); // GP 11.4.2.1: '10' returns the ELFs *with* their modules - the superset of
// '20' (ELFs only). All four P1 values shall be supported, but cards
// deviate: the live card answered 6A86 (incorrect P1-P2) to '20' while
// '10' works (2026-09-28). Query '10' first; fall back to '20' (no
@@ -9555,6 +9585,10 @@ async function ramExplore(sp) {
await paginate('20', elfs, ramParseElfStatus, t('ELFs'), true);
if (!elfs.length) errors.push(...elfFailure);
}
// A package whose own listing carries no version may still be known from
// its applet instance (the app entry carries the load file version and the
// package AID); the rows are annotated when this inference is used.
ramDeriveElfVersions(elfs, apps);
// Only write the counter when a packet was actually accepted: a run whose
// start value was stale must never lower the preset counter.
if (cntr !== sp.cntr) ramSaveCntr(cntr);
@@ -11001,12 +11035,13 @@ function ramExpandedQueryApdu(p1, form, next) {
return '80F2' + p1 + p2 + hex(tags.length / 2 + 4) + '4F005C' + hex(tags.length / 2) + tags + '00';
}
// GET RESPONSE for a '61xx' remote status word: the card holds xx bytes of
// response data (the compact path chains C0000000 for the same situation).
function ramGetResponseApdu(sw) {
const s = String(sw || '').toUpperCase();
const len = /^61[0-9A-F]{2}$/.test(s) ? s.substr(2, 2) : '00';
return '00C00000' + len;
// A '61xx' remote answer means the card holds the response data. The live
// card (2026-09-30) rejects a standalone GET RESPONSE with 6700 - the GET
// RESPONSE must ride in the same compact command string as the query, exactly
// like the compact path does (`80F2..C0000000`). Only the compact command
// string can chain commands; the expanded AA/22 format has no retry.
function ramChainGetResponse(apdu) {
return apdu + 'C0000000';
}
// The ISD (80) and memory responses fit the ENVELOPE; the Apps (40) and ELF
@@ -18730,6 +18765,7 @@ const LANG_RU = {
'compact listing only (package versions unavailable)': 'только компактный список (версии пакетов недоступны)',
'Package versions come from the expanded registry query \u2014 this card did not return them.': 'Версии пакетов приходят из расширенного запроса реестра \u2014 эта карта их не вернула.',
'Package versions are only in the tag-list forms of the expanded registry query \u2014 this card accepted only the no-tag-list form.': 'Версии пакетов есть только в формах расширенного запроса со списком тегов \u2014 эта карта приняла только форму без списка тегов.',
'from the app instance': 'от экземпляра апплета',
'STK menu entry:': 'Пункт меню STK:',
'position': 'позиция',
'Other registry data:': 'Прочие данные реестра:',