' + esc(t('Package versions come from the expanded registry query \u2014 this card did not return them.')) + '
';
html += '
';
@@ -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:': 'Прочие данные реестра:',
diff --git a/frontend/sw.js b/frontend/sw.js
index 7c4d8e7..107a16d 100644
--- a/frontend/sw.js
+++ b/frontend/sw.js
@@ -1,4 +1,4 @@
-const CACHE = 'simple-v326';
+const CACHE = 'simple-v327';
const URLS = [
'index.html',
'help.html',
diff --git a/frontend/tests/ram.test.js b/frontend/tests/ram.test.js
index 3d0a70c..bebd54f 100644
--- a/frontend/tests/ram.test.js
+++ b/frontend/tests/ram.test.js
@@ -25,6 +25,7 @@ function extractFunc(src, name) {
const FNS = ['berLenStr', 'buildApdu', 'escHtml', 'esc', 'chainInit', 'chainRamBuildRowHex', 'ramFmtLifecycle', 'ramFmtVersion', 'ramMenuState', 'ramEntryKind', 'ramMenuHtml', 'ramRawTlvsHtml', 'ramNvLine', 'ramFmtPrivileges', 'ramRenderExploreHtml', 'ramMergeExpanded', 'ramStepLine', 'ramStepComponents', 'ramInstallFailHint', 'ramProbeParse', 'ramCompatVerdict', 'ramCompatProbeNote', 'ramCompatImportRows', 'ramCompatRowsHtml', 'ramCompatFailureHtml', 'ramCompatDetailHtml', 'lookupSw', 'ramExpandedDetailsInit', 'ramExpandedDetailsChanged', 'ramGetStatusApdu', 'ramDeleteApdu',
'stkParamsBuild', 'ramRemoteSwOk', 'spPorAccepted', 'ramIncrementCntr', 'ramDeleteFromExplorer', 'ramListingSpi2', 'ramRemoveFromExplorer', 'ramHasInstance', 'ramExpandedQueryApdu',
'_parseRawElfEntry', '_parseRawAppEntry', 'ramParseElfStatus', 'ramParseAppStatus', 'ramParseModuleAids', 'parseTLV', '_parseE3Entry', '_parseMenuEntries',
+ 'ramExpandedReport',
'ramCardIdxAfterRemove', 'ramClearResults', 'ramHideProgress', 'ramOpChanged', 'ramRender', 'ramApplyCard', 'ramExecute', 'decodePrivileges', 'ramActionBtn', 'ramCapToolkitMode', 'ramOpProgressText',
'jcAidNorm', 'jcAidName', 'jcAidSuffix', 'jcAidHtml'];
let code = '';
@@ -35,6 +36,8 @@ const m = html.match(/const _chains = \{\};/);
if (m) code += m[0].replace(/^const /, 'var ') + '\n';
const swm = html.match(/const SW_MAP = \{[\s\S]*?\n\};/);
if (swm) code += swm[0].replace(/^const /, 'var ') + '\n';
+const ef = html.match(/const RAM_EXPANDED_FORMS = \[[^\]]*\];/);
+if (ef) code += ef[0].replace(/^const /, 'var ') + '\n';
const pn = html.match(/const PRIVILEGE_NAMES = \[[\s\S]*?\n\];/);
if (pn) code += pn[0].replace(/^const /, 'var ') + '\n';
const an = html.match(/const JC_AID_NAMES = \{[\s\S]*?\n\};/);
@@ -546,19 +549,76 @@ test('ramCardIdxAfterRemove keeps the remembered index aligned', () => {
assert.strictEqual(ramCardIdxAfterRemove(null, 1), null);
});
-test('ramExpandedQueryApdu matches the reference trace bytes', () => {
- // TCA Loader: `AA14 2212 80F28002 0C 4F00 5C08 4F9F70C5C4CCCEEA 00`
+test('ramExpandedQueryApdu builds the three expanded query forms', () => {
+ // the reference trace form (richest: the tag list incl. the TS 102 226 EA)
for (const p1 of ['80', '40']) {
assert.strictEqual(ramExpandedQueryApdu(p1),
'80F2' + p1 + '020C4F005C084F9F70C5C4CCCEEA00');
+ assert.strictEqual(ramExpandedQueryApdu(p1, 'tags'), ramExpandedQueryApdu(p1));
}
- // The ELF queries add '84' (Executable Module AIDs): with a tag list the
- // card returns only the listed tags, so the expanded fallback would
- // otherwise list packages without their modules (v3.6.51).
+ // the ELF queries add '84' (Executable Module AIDs)
for (const p1 of ['20', '10']) {
assert.strictEqual(ramExpandedQueryApdu(p1),
'80F2' + p1 + '020D4F005C094F9F70C5C4CCCEEA8400');
}
+ // without EA: a card that errors on the requested-but-absent EA object
+ assert.strictEqual(ramExpandedQueryApdu('40', 'noea'), '80F240020B4F005C074F9F70C5C4CCCE00');
+ assert.strictEqual(ramExpandedQueryApdu('10', 'noea'), '80F210020C4F005C084F9F70C5C4CCCE8400');
+ // no tag list at all (GPPro's GET STATUS data field: 4F00 only)
+ assert.strictEqual(ramExpandedQueryApdu('80', 'notags'), '80F28002024F0000');
+ assert.strictEqual(ramExpandedQueryApdu('10', 'notags'), '80F21002024F0000');
+ assert.deepStrictEqual(RAM_EXPANDED_FORMS, ['tags', 'noea', 'notags']);
+});
+
+test('ramExpandedReport summarizes the query forms and the fallbacks', () => {
+ globalThis.t = s => s;
+ // every listing answered the reference form
+ assert.strictEqual(ramExpandedReport([
+ { p1: '80', label: 'ISD', compact: true, form: 'tags', sw: '9000' },
+ { p1: '40', label: 'Apps', compact: true, form: 'tags', sw: '9000' },
+ { p1: '10', label: 'ELFs', compact: true, form: 'tags', sw: '9000' },
+ ]), 'Expanded registry: tag list + EA (ISD, Apps, ELFs)');
+ // the ISD commonly has no expanded data (6A88/6A86 is normal there)
+ assert.strictEqual(ramExpandedReport([
+ { p1: '80', label: 'ISD', compact: true, form: null, sw: '6A88' },
+ { p1: '40', label: 'Apps', compact: true, form: 'tags', sw: '9000' },
+ { p1: '10', label: 'ELFs', compact: true, form: 'noea', sw: '9000' },
+ ]), 'Expanded registry: tag list + EA (Apps) \u00b7 tag list (ELFs) \u00b7 ISD: no expanded data (6A88) \u2014 compact listing used');
+ // a listing the card refused entirely: the compact listing carried it
+ assert.strictEqual(ramExpandedReport([
+ { p1: '40', label: 'Apps', compact: true, form: null, sw: '6A88' },
+ { p1: '10', label: 'ELFs', compact: true, form: 'notags', sw: '9000' },
+ ]), 'Expanded registry: no tag list (ELFs) \u00b7 Apps: refused (6A88) \u2014 compact listing used');
+ // nothing answered: the headline says the versions are unavailable
+ assert.strictEqual(ramExpandedReport([
+ { p1: '80', label: 'ISD', compact: true, form: null, sw: '6A88' },
+ { p1: '10', label: 'ELFs', compact: true, form: null, sw: '6A86' },
+ ]), 'Expanded registry refused by the card \u2014 compact listing only (package versions unavailable) ISD: no expanded data (6A88) \u2014 compact listing used \u00b7 ELFs: refused (6A86) \u2014 compact listing used');
+ // a listing with no compact data either reports its own status word
+ assert.strictEqual(ramExpandedReport([
+ { p1: '10', label: 'ELFs', compact: false, form: null, sw: '6A88' },
+ ]), 'Expanded registry refused by the card \u2014 compact listing only (package versions unavailable) ELFs: 6A88');
+ assert.strictEqual(ramExpandedReport([]), '');
+ delete globalThis.t;
+});
+
+test('the explorer hints when the card returned no package versions', () => {
+ globalThis.t = s => s;
+ const noVer = ramRenderExploreHtml(null, [], [], [{ aid: 'E1', lifecycle: '01', privileges: '00' }]);
+ const withVer = ramRenderExploreHtml(null, [], [], [{ aid: 'E1', lifecycle: '01', privileges: '00', version: '0100' }]);
+ delete globalThis.t;
+ assert.ok(noVer.includes('Package versions come from the expanded registry query'), noVer);
+ assert.ok(!withVer.includes('Package versions come from the expanded registry query'), withVer);
+});
+
+test('the Explore detects the expanded query form and reports it', () => {
+ const src = extractFunc(html, 'ramExplore');
+ assert.ok(src.includes('RAM_EXPANDED_FORMS'), 'the form ladder must be used');
+ assert.ok(src.includes('expandedReport.push'), 'each listing outcome must be recorded');
+ assert.ok(src.includes('ramExpandedReport(expandedReport)'), 'the report must reach the explorer');
+ // the ISD gets a single attempt: its 6A88 must not poison the detection
+ assert.ok(src.includes("if (p1 === '80') forms = [expandedForm || 'tags'];"),
+ 'the ISD must not walk the ladder');
});
test('ramParseAppStatus parses the expanded E3 listing (vendor traces)', () => {
@@ -1199,13 +1259,16 @@ test('the expanded-details checkbox is a sticky preference', () => {
};
els['ram-expanded-details'].checked = false;
ramExpandedDetailsInit();
- assert.strictEqual(els['ram-expanded-details'].checked, false); // default: off
+ assert.strictEqual(els['ram-expanded-details'].checked, true); // default: on (versions live there)
+ store['simple_ram_expanded'] = '0';
+ ramExpandedDetailsInit();
+ assert.strictEqual(els['ram-expanded-details'].checked, false); // an explicit opt-out is honoured
store['simple_ram_expanded'] = '1';
ramExpandedDetailsInit();
assert.strictEqual(els['ram-expanded-details'].checked, true); // stored choice applied
els['ram-expanded-details'].checked = false;
ramExpandedDetailsChanged();
- assert.strictEqual(store['simple_ram_expanded'], '0'); // ticking stores it
+ assert.strictEqual(store['simple_ram_expanded'], '0'); // unticking stores it
delete globalThis.localStorage;
});
diff --git a/pyproject.toml b/pyproject.toml
index bb40826..2e4b8da 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "pysim-simple-server"
-version = "3.6.53"
+version = "3.6.54"
description = "HTTP REST server wrapping pysim for the SIMple PWA"
requires-python = ">=3.8"
# pysim is a git-only dependency installed explicitly by setup.bat/setup.sh.
diff --git a/pysim_simple_server/server.py b/pysim_simple_server/server.py
index 3d61524..d38cc49 100644
--- a/pysim_simple_server/server.py
+++ b/pysim_simple_server/server.py
@@ -31,7 +31,7 @@ from osmocom.tlv import BER_TLV_IE
from osmocom.utils import rpad
-VERSION = '3.6.53'
+VERSION = '3.6.54'
MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE