feat: expanded registry on by default, form ladder + fallback report (v3.6.54)

The 'Fetch expanded registry details' checkbox is now on by default: the
expanded pass carries the package versions, the SD/load-file associations
and the applet menu entries; an explicit stored opt-out is still honoured.

Live 2026-09-30: a card with a non-default ISD AID answered 6A88 to the
reference expanded query (P2=02 + the tag list), so the ELF rows had no
versions.  Cards differ in what they accept, so the query is now tried in
three forms (RAM_EXPANDED_FORMS):

  tags   - the reference tag list (incl. the TS 102 226 EA SCP Registry Data)
  noea   - the same list without EA (a card that errors on it)
  notags - no tag list at all (GPPro's 4F00-only data field); the card
           builds the default E3

The first form that answers is remembered for the run and tried first for
the later listings; the ISD gets a single attempt - its 6A88/6A86 is normal
('no data to report', GPPro) and must not poison the detection for the
apps/ELF queries.

Every listing's outcome is reported in the explorer (ramExpandedReport):
'Expanded registry: tag list + EA (Apps, ELFs) - ISD: no expanded data
(6A88) - compact listing used', or 'Expanded registry refused by the card -
compact listing only (package versions unavailable)'; the ELF section hints
when no version came back.  Tooltip/help (EN/RU) updated.

sw cache simple-v327; tests: 550 Python / 707 Node.
This commit is contained in:
2026-09-30 09:44:39 +03:00
parent 4906c91463
commit aa4bf8e008
7 changed files with 217 additions and 44 deletions
+142 -32
View File
@@ -703,7 +703,7 @@
<button data-needs="card" data-cap-gate="ram" onclick="ramExecute()" class="px-5 py-1.5 bg-blue-600 text-white text-sm font-medium rounded hover:bg-blue-700 disabled:opacity-40 disabled:cursor-not-allowed" data-l10n="Execute">Execute</button>
</div>
<label class="flex items-center gap-1 mb-3 text-sm text-gray-700 dark:text-slate-300" data-l10n-title="Runs one extra expanded query per listing (P2=02, tag list 4F 9F70 C5 C4 CC CE EA, plus 84 for the ELF queries). The compact listing carries AID, life cycle and privileges only - it has no version; the expanded pass adds the Executable Load File version, the Load File AID, the Security Domain AID, the module lists and the applet STK menu entries (SCP Registry Data). It costs extra secured packets and some cards reject it - the Explore already uses it automatically when the compact listing returns nothing."><input type="checkbox" id="ram-expanded-details" class="rounded border-gray-300 dark:border-slate-600" onchange="ramExpandedDetailsChanged()"> <span data-l10n="Fetch expanded registry details (load-file / SD associations)">Fetch expanded registry details (load-file / SD associations)</span></label>
<label class="flex items-center gap-1 mb-3 text-sm text-gray-700 dark:text-slate-300" data-l10n-title="Runs one extra expanded query per listing (P2=02; on by default). The compact listing carries AID, life cycle and privileges only - it has no version; the expanded pass adds the Executable Load File version, the Load File AID, the Security Domain AID, the module lists and the applet STK menu entries (SCP Registry Data). Cards differ in what they accept, so three query forms are tried (tag list with EA, tag list without EA, no tag list - the GPPro form); the explorer reports which form worked and where the run fell back to the compact listing (package versions are then unavailable). It costs a few extra secured packets per Explore; untick to skip it."><input type="checkbox" id="ram-expanded-details" class="rounded border-gray-300 dark:border-slate-600" onchange="ramExpandedDetailsChanged()"> <span data-l10n="Fetch expanded registry details (load-file / SD associations)">Fetch expanded registry details (load-file / SD associations)</span></label>
<div id="ram-cap-rows" class="hidden mb-3">
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300" data-l10n="CAP file (max 48 kB)">CAP file (max 48 kB)</label>
@@ -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.53';
const SIMPLE_VERSION = '3.6.54';
document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION;
// ===== Tab switching =====
@@ -8274,7 +8274,10 @@ function ramExpandedDetailsInit() {
if (!el) return;
let v = null;
try { v = localStorage.getItem('simple_ram_expanded'); } catch (e) { /* private mode */ }
if (v !== null) el.checked = v === '1';
// Default ON: the expanded pass carries the package versions, the SD /
// load-file associations and the applet menu entries (it costs a few
// extra secured packets per Explore); an explicit opt-out is honoured.
el.checked = v === null ? true : v === '1';
}
function ramExpandedDetailsChanged() {
@@ -9151,8 +9154,45 @@ function ramMergeExpanded(collector, parsed) {
});
}
// Summary of the expanded-registry pass (pure). `report` holds one entry per
// listing: {label, compact, form, sw} - `form` is the query form the card
// accepted ('tags' | 'noea' | 'notags') or null when it refused every form,
// `compact` says whether that listing's compact query worked, `sw` the last
// remote status word. Shown in the explorer so a fallback to the compact
// listing (and the missing package versions) is never silent.
function ramExpandedReport(report) {
const rows = (report || []).filter(r => r && r.label);
if (!rows.length) return '';
const formName = f => f === 'tags' ? t('tag list + EA')
: (f === 'noea' ? t('tag list') : t('no tag list'));
const parts = [];
RAM_EXPANDED_FORMS.forEach(f => {
const labels = rows.filter(r => r.form === f).map(r => r.label);
if (labels.length) parts.push(formName(f) + ' (' + labels.join(', ') + ')');
});
rows.forEach(r => {
if (r.form) return;
const why = r.sw ? ' (' + r.sw + ')' : '';
if (!r.compact) {
parts.push(r.label + ': ' + (r.sw || t('no data')));
} else if (r.p1 === '80') {
// cards commonly report nothing for the ISD in the expanded form
// (GPPro treats its 6A88/6A86 as 'no data to report')
parts.push(r.label + ': ' + t('no expanded data') + why + ' \u2014 ' + t('compact listing used'));
} else {
parts.push(r.label + ': ' + t('refused') + why + ' \u2014 ' + t('compact listing used'));
}
});
const anyForm = rows.some(r => r.form);
const head = anyForm
? t('Expanded registry:')
: t('Expanded registry refused by the card') + ' \u2014 ' +
t('compact listing only (package versions unavailable)');
return head + ' ' + parts.join(' \u00b7 ');
}
// Render the full explorer result as structured HTML
function ramRenderExploreHtml(mem, isd, apps, elfs, fmt) {
function ramRenderExploreHtml(mem, isd, apps, elfs, fmt, expanded) {
let html = '';
if (mem && (mem.appCount != null || mem.freeNV != null || mem.freeV != null)) {
html += '<div class="mb-4 p-3 bg-gray-50 dark:bg-slate-800 rounded">';
@@ -9166,6 +9206,9 @@ function ramRenderExploreHtml(mem, isd, apps, elfs, fmt) {
}
html += '</div>';
}
if (expanded) {
html += '<div class="mb-4 text-xs text-gray-600 dark:text-slate-400">' + esc(expanded) + '</div>';
}
if (isd && isd.length) {
html += '<div class="mb-4">';
html += '<div class="font-semibold text-sm mb-1">' + esc(t('ISD (Issuer Security Domain)')) + '</div>';
@@ -9211,6 +9254,9 @@ function ramRenderExploreHtml(mem, isd, apps, elfs, fmt) {
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)) {
html += '<div class="mb-1 text-xs text-gray-500 dark:text-slate-400">' + esc(t('Package versions come from the expanded registry query \u2014 this card did not return them.')) + '</div>';
}
elfs.forEach(o => {
html += '<div class="mb-1 pl-2 pr-2 py-1 rounded bg-gray-50 dark:bg-slate-800/60 hover:bg-gray-100 dark:hover:bg-slate-700/50 border-l-2 border-purple-400 flex items-start gap-3">';
html += '<div class="flex-1 min-w-0">';
@@ -9259,7 +9305,7 @@ function ramRenderExplorer() {
explorerEl.innerHTML = '';
return;
}
const html = ramRenderExploreHtml(_ramExplorerData.mem, _ramExplorerData.isd, _ramExplorerData.apps, _ramExplorerData.elfs, _ramExplorerData.fmt);
const html = ramRenderExploreHtml(_ramExplorerData.mem, _ramExplorerData.isd, _ramExplorerData.apps, _ramExplorerData.elfs, _ramExplorerData.fmt, _ramExplorerData.expanded);
explorerEl.innerHTML = html || esc(t('(no data)'));
explorerEl.classList.remove('hidden');
pysimApplyAvailability();
@@ -9273,6 +9319,49 @@ async function ramExplore(sp) {
const errors = [];
const mem = { appCount: null, freeNV: null, freeV: null };
const isd = [], apps = [], elfs = [];
// Expanded-registry state for this run: the query form the card accepted
// (remembered and tried first for the later listings) and the per-listing
// outcome report shown in the explorer.
let expandedForm = null;
const expandedReport = [];
// One expanded query with the form ladder (see RAM_EXPANDED_FORMS): try
// the remembered form first, then the others. The ISD gets a single
// attempt - cards commonly answer 6A88/6A86 there ("no data to report",
// GPPro), which must not poison the detection for the apps/ELF queries.
// Returns {form, parsed, sw}; form is null when every attempt was refused.
async function expandedPass(p1, label, parser, spi2) {
let forms = expandedForm
? [expandedForm].concat(RAM_EXPANDED_FORMS.filter(f => f !== expandedForm))
: RAM_EXPANDED_FORMS.slice();
if (p1 === '80') forms = [expandedForm || 'tags'];
let lastSw = '';
for (const form of forms) {
ramShowProgress(label + ' P1=' + p1 + ' P2=02 (' + form + ')...');
const res = await ramSendOta(ramExpandedQueryApdu(p1, form), Object.assign({}, sp, { cntr, spi2 }));
if (res.ram_format) sp.ram_format = res.ram_format;
if (res.final_cntr) cntr = res.final_cntr;
else if (res.success && spPorAccepted(res.por)) cntr = ramIncrementCntr(cntr);
if (!res.success || !res.por || !spPorAccepted(res.por)) {
// the packet itself was not accepted: another form will not help
lastSw = res.por ? res.por.response_status : (res.error || t('no data'));
break;
}
const data = res.por.decoded ? (res.por.decoded.last_response_data || '') : '';
const sw = ((res.por.decoded && res.por.decoded.last_status_word) || '').toUpperCase();
const parsed = data ? parser(data) : [];
if (parsed.length) {
expandedForm = form;
return { form: form, parsed: parsed, sw: sw };
}
lastSw = sw;
if (ramRemoteSwOk(sw)) {
// accepted but empty: nothing to gain from another form
return { form: form, parsed: [], sw: sw };
}
}
return { form: null, parsed: [], sw: lastSw };
}
async function paginate(p1, collector, parser, label, quiet) {
// Compact listing format (GP 11.4.2.2: P2.b2=0) with the chained GET
@@ -9335,25 +9424,22 @@ async function ramExplore(sp) {
p2 = '01';
}
// Expanded pass: the fallback when the compact listing produced
// nothing (a card may reject P2.b2=0 - it is deprecated), and the
// optional details source (privileges, ELF/SD associations) behind the
// "Fetch expanded registry details" checkbox.
// nothing (a card may reject P2.b2=0 - it is deprecated) and the
// details source (package versions, SD associations, menu entries)
// behind the "Fetch expanded registry details" checkbox (on by
// default). The form ladder and the per-listing report live in
// expandedPass()/expandedReport.
const wantDetails = !!(document.getElementById('ram-expanded-details') || {}).checked;
if (collector.length === before || wantDetails) {
ramShowProgress(label + ' P1=' + p1 + ' P2=02 (expanded)...');
const res = await ramSendOta(ramExpandedQueryApdu(p1), Object.assign({}, sp, { cntr, spi2 }));
if (res.ram_format) sp.ram_format = res.ram_format;
if (res.final_cntr) cntr = res.final_cntr;
else if (res.success && spPorAccepted(res.por)) cntr = ramIncrementCntr(cntr);
const data = (res.success && res.por && res.por.decoded)
? (res.por.decoded.last_response_data || '') : '';
const parsed = data ? parser(data) : [];
const out = await expandedPass(p1, label, parser, spi2);
expandedReport.push({ p1: p1, label: label, compact: collector.length > before,
form: out.form, sw: out.sw });
if (collector.length === before) {
// Nothing from the compact pass: the expanded result is the listing
collector.push(...parsed);
} else if (parsed.length) {
collector.push(...out.parsed);
} else if (out.parsed.length) {
// Keep the compact entries, add the expanded-only fields
ramMergeExpanded(collector, parsed);
ramMergeExpanded(collector, out.parsed);
}
}
// The compact error only surfaces when the expanded pass did not rescue it
@@ -9415,7 +9501,8 @@ async function ramExplore(sp) {
resultEl.classList.remove('hidden', 'text-red-600'); resultEl.classList.add('text-green-600');
}
_ramExplorerData = { mem: mem, isd: isd, apps: apps, elfs: elfs, fmt: sp.ram_format || '' };
_ramExplorerData = { mem: mem, isd: isd, apps: apps, elfs: elfs, fmt: sp.ram_format || '',
expanded: ramExpandedReport(expandedReport) };
ramRenderExplorer();
}
@@ -10805,18 +10892,31 @@ function ramGetStatusApdu(p1, p2) {
return '80F2' + p1 + p2 + '024F0000' + 'C0000000';
}
// P2=02 + the reference tag list: the expanded (E3) registry listing
// (GP Card Spec 11.4.2.3/11.4.3.1). Used as the fallback when a card rejects
// the deprecated compact format and for the optional details pass; the bytes
// are the reference trace form (`80F28002 0C 4F00 5C08 4F9F70C5C4CCCEEA 00`).
// The ELF queries add '84' (Executable Module AIDs, GP Table 11-37): with a
// tag list present the card returns only the listed tags (11.4.3.1), so
// without it the expanded fallback would list packages without their modules
// (no module rows / Install buttons on cards that reject the compact form).
function ramExpandedQueryApdu(p1) {
// P2=02 expanded (E3) registry listing (GP Card Spec 11.4.2.3/11.4.3.1).
// Used as the fallback when a card rejects the deprecated compact format and
// for the details pass (on by default: it carries the package versions, the
// SD/load-file associations and the applet menu entries).
//
// Cards differ in what they accept, so the query is tried in three forms:
// 'tags' - the reference trace tag list (`4F9F70C5C4CCCEEA`), the richest
// 'noea' - the same list without the TS 102 226 'EA' SCP Registry Data
// (a card that errors on the requested-but-absent EA object)
// 'notags' - no tag list (GPPro's GET STATUS data field `4F00` only): the
// card builds the default E3 (AID + life cycle + privileges or
// modules; cards commonly include the load file version anyway)
// The ELF forms add '84' (Executable Module AIDs, GP Table 11-37): with a tag
// list present the card returns only the listed tags, so without it the
// expanded fallback would list packages without their modules (no module rows
// / Install buttons on cards that reject the compact form).
const RAM_EXPANDED_FORMS = ['tags', 'noea', 'notags'];
function ramExpandedQueryApdu(p1, form) {
const f = form || 'tags';
if (f === 'notags') return '80F2' + p1 + '02' + '02' + '4F00' + '00';
const elf = p1 === '10' || p1 === '20';
const tags = '4F9F70C5C4CCCEEA' + (elf ? '84' : '');
return '80F2' + p1 + '02' + (elf ? '0D' : '0C') + '4F005C' + (elf ? '09' : '08') + tags + '00';
const tags = (f === 'noea' ? '4F9F70C5C4CCCE' : '4F9F70C5C4CCCEEA') + (elf ? '84' : '');
const hex = n => n.toString(16).toUpperCase().padStart(2, '0');
return '80F2' + p1 + '02' + hex(tags.length / 2 + 4) + '4F005C' + hex(tags.length / 2) + tags + '00';
}
// The ISD (80) and memory responses fit the ENVELOPE; the Apps (40) and ELF
@@ -18527,7 +18627,17 @@ const LANG_RU = {
'KIc key and KID key are required': 'Укажите ключ KIc и ключ KID',
'RAM command format:': 'Формат команд RAM:',
'Fetch expanded registry details (load-file / SD associations)': 'Запрашивать расширенные данные реестра (связи load-файла и SD)',
'Runs one extra expanded query per listing (P2=02, tag list 4F 9F70 C5 C4 CC CE EA, plus 84 for the ELF queries). The compact listing carries AID, life cycle and privileges only - it has no version; the expanded pass adds the Executable Load File version, the Load File AID, the Security Domain AID, the module lists and the applet STK menu entries (SCP Registry Data). It costs extra secured packets and some cards reject it - the Explore already uses it automatically when the compact listing returns nothing.': 'Запускает дополнительный расширенный запрос для каждого списка (P2=02, список тегов 4F 9F70 C5 C4 CC CE EA, плюс 84 для запросов ELF). Компактный список содержит только AID, состояние и привилегии — версии в нём нет; расширенный проход добавляет версию исполняемого load-файла, AID load-файла, AID Security Domain, списки модулей и пункты меню STK апплета (SCP Registry Data). Он расходует дополнительные защищённые пакеты, и не все карты его поддерживают — Explore и так применяет его автоматически, когда компактный список ничего не вернул.',
'Runs one extra expanded query per listing (P2=02; on by default). The compact listing carries AID, life cycle and privileges only - it has no version; the expanded pass adds the Executable Load File version, the Load File AID, the Security Domain AID, the module lists and the applet STK menu entries (SCP Registry Data). Cards differ in what they accept, so three query forms are tried (tag list with EA, tag list without EA, no tag list - the GPPro form); the explorer reports which form worked and where the run fell back to the compact listing (package versions are then unavailable). It costs a few extra secured packets per Explore; untick to skip it.': 'Запускает дополнительный расширенный запрос для каждого списка (P2=02; включено по умолчанию). Компактный список содержит только AID, состояние и привилегии — версии в нём нет; расширенный проход добавляет версию исполняемого load-файла, AID load-файла, AID Security Domain, списки модулей и пункты меню STK апплета (SCP Registry Data). Карты различаются в том, что принимают, поэтому пробуются три формы запроса (список тегов с EA, список тегов без EA, без списка тегов — форма GPPro); обзор сообщает, какая форма сработала и где выполнен откат к компактному списку (версии пакетов тогда недоступны). Это стоит несколько дополнительных защищённых пакетов на каждый обзор; снимите флажок, чтобы пропустить.',
'Expanded registry:': 'Расширенный реестр:',
'tag list + EA': 'список тегов + EA',
'tag list': 'список тегов',
'no tag list': 'без списка тегов',
'refused': 'отказ',
'compact listing used': 'использован компактный список',
'no expanded data': 'нет расширенных данных',
'Expanded registry refused by the card': 'Карта отклонила расширенный реестр',
'compact listing only (package versions unavailable)': 'только компактный список (версии пакетов недоступны)',
'Package versions come from the expanded registry query \u2014 this card did not return them.': 'Версии пакетов приходят из расширенного запроса реестра \u2014 эта карта их не вернула.',
'STK menu entry:': 'Пункт меню STK:',
'position': 'позиция',
'Other registry data:': 'Прочие данные реестра:',