';
@@ -9255,7 +9302,7 @@ function ramRenderExploreHtml(mem, isd, apps, elfs, fmt, expanded) {
html += '
';
html += '
' + esc(t('Executable Load Files (ELFs) / Packages')) + ' (' + elfs.length + ')
';
if (!elfs.some(o => o.version)) {
- html += '
' + esc(t('Package versions come from the expanded registry query \u2014 this card did not return them.')) + '
';
+ html += '
' + esc(ramElfVersionHint(elfForm)) + '
';
}
elfs.forEach(o => {
html += '
';
@@ -9305,7 +9352,7 @@ function ramRenderExplorer() {
explorerEl.innerHTML = '';
return;
}
- const html = ramRenderExploreHtml(_ramExplorerData.mem, _ramExplorerData.isd, _ramExplorerData.apps, _ramExplorerData.elfs, _ramExplorerData.fmt, _ramExplorerData.expanded);
+ const html = ramRenderExploreHtml(_ramExplorerData.mem, _ramExplorerData.isd, _ramExplorerData.apps, _ramExplorerData.elfs, _ramExplorerData.fmt, _ramExplorerData.expanded, _ramExplorerData.elfForm);
explorerEl.innerHTML = html || esc(t('(no data)'));
explorerEl.classList.remove('hidden');
pysimApplyAvailability();
@@ -9329,7 +9376,11 @@ async function ramExplore(sp) {
// 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.
+ // A '61xx' answer means the card holds the response data (fetched with
+ // GET RESPONSE); a '6310' means more occurrences (re-issued as a
+ // next-occurrence page, P2.b1=1) - both are chained here, so an answer is
+ // only called empty or refused when it really is. Returns
+ // {form, parsed, sw, entries}; form is null when no form produced data.
async function expandedPass(p1, label, parser, spi2) {
let forms = expandedForm
? [expandedForm].concat(RAM_EXPANDED_FORMS.filter(f => f !== expandedForm))
@@ -9337,30 +9388,48 @@ async function ramExplore(sp) {
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;
+ let data = '';
+ let sw = '';
+ let accepted = true;
+ const sendOne = async (apdu, what) => {
+ ramShowProgress(label + ' P1=' + p1 + ' ' + what + '...');
+ const res = await ramSendOta(apdu, 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)) {
+ accepted = false;
+ sw = res.por ? res.por.response_status : (res.error || t('no data'));
+ return '';
+ }
+ sw = ((res.por.decoded && res.por.decoded.last_status_word) || '').toUpperCase();
+ return res.por.decoded ? (res.por.decoded.last_response_data || '') : '';
+ };
+ let page = 0;
+ while (accepted && page++ < 8) {
+ data += await sendOne(ramExpandedQueryApdu(p1, form, page > 1),
+ page > 1 ? 'P2=03 (' + form + ')' : 'P2=02 (' + form + ')');
+ if (!accepted) break;
+ let rounds = 0;
+ while (/^61[0-9A-F]{2}$/.test(sw) && rounds++ < 4) {
+ data += await sendOne(ramGetResponseApdu(sw), 'GET RESPONSE');
+ if (!accepted) break;
+ }
+ if (!accepted || sw !== '6310') 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 };
+ return { form: form, parsed: parsed, sw: sw, entries: parsed.length };
}
lastSw = sw;
+ if (!accepted) break;
if (ramRemoteSwOk(sw)) {
// accepted but empty: nothing to gain from another form
- return { form: form, parsed: [], sw: sw };
+ return { form: form, parsed: [], sw: sw, entries: 0 };
}
}
- return { form: null, parsed: [], sw: lastSw };
+ return { form: null, parsed: [], sw: lastSw, entries: 0 };
}
async function paginate(p1, collector, parser, label, quiet) {
@@ -9433,7 +9502,7 @@ async function ramExplore(sp) {
if (collector.length === before || wantDetails) {
const out = await expandedPass(p1, label, parser, spi2);
expandedReport.push({ p1: p1, label: label, compact: collector.length > before,
- form: out.form, sw: out.sw });
+ form: out.form, sw: out.sw, entries: out.entries });
if (collector.length === before) {
// Nothing from the compact pass: the expanded result is the listing
collector.push(...out.parsed);
@@ -9502,7 +9571,8 @@ async function ramExplore(sp) {
}
_ramExplorerData = { mem: mem, isd: isd, apps: apps, elfs: elfs, fmt: sp.ram_format || '',
- expanded: ramExpandedReport(expandedReport) };
+ expanded: ramExpandedReport(expandedReport),
+ elfForm: ramExpandedElfForm(expandedReport) };
ramRenderExplorer();
}
@@ -10897,10 +10967,13 @@ function ramGetStatusApdu(p1, p2) {
// 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:
+// Cards differ in what they accept, so the query is tried in four 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)
+// 'lean' - only the objects valid for the kind: the load file version and
+// modules for ELFs (the app-only C5/C4 make strict cards answer
+// 6A88), pySim's field list (AID, life cycle, privileges, SD AID)
+// for the ISD/apps
// '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)
@@ -10908,15 +10981,32 @@ function ramGetStatusApdu(p1, p2) {
// 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'];
+const RAM_EXPANDED_FORMS = ['tags', 'noea', 'lean', 'notags'];
-function ramExpandedQueryApdu(p1, form) {
- const f = form || 'tags';
- if (f === 'notags') return '80F2' + p1 + '02' + '02' + '4F00' + '00';
+// The tag list of one form for a listing ('' = the no-tag-list form).
+function ramExpandedTags(p1, form) {
const elf = p1 === '10' || p1 === '20';
- const tags = (f === 'noea' ? '4F9F70C5C4CCCE' : '4F9F70C5C4CCCEEA') + (elf ? '84' : '');
+ if (form === 'notags') return '';
+ if (form === 'lean') return elf ? '4F9F70CE84' : '4F9F70C5CC';
+ const base = form === 'noea' ? '4F9F70C5C4CCCE' : '4F9F70C5C4CCCEEA';
+ return base + (elf ? '84' : '');
+}
+
+// `next` = the next-occurrence page (P2.b1=1, GP 11.4.3.2) after a '6310'.
+function ramExpandedQueryApdu(p1, form, next) {
+ const tags = ramExpandedTags(p1, form || 'tags');
+ const p2 = next ? '03' : '02';
+ if (!tags) return '80F2' + p1 + p2 + '02' + '4F00' + '00';
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';
+ 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;
}
// The ISD (80) and memory responses fit the ENVELOPE; the Apps (40) and ELF
@@ -18632,12 +18722,14 @@ const LANG_RU = {
'tag list + EA': 'список тегов + EA',
'tag list': 'список тегов',
'no tag list': 'без списка тегов',
+ 'lean 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 эта карта их не вернула.',
+ '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 эта карта приняла только форму без списка тегов.',
'STK menu entry:': 'Пункт меню STK:',
'position': 'позиция',
'Other registry data:': 'Прочие данные реестра:',
diff --git a/frontend/sw.js b/frontend/sw.js
index 107a16d..f26f4f5 100644
--- a/frontend/sw.js
+++ b/frontend/sw.js
@@ -1,4 +1,4 @@
-const CACHE = 'simple-v327';
+const CACHE = 'simple-v328';
const URLS = [
'index.html',
'help.html',
diff --git a/frontend/tests/ram.test.js b/frontend/tests/ram.test.js
index bebd54f..801fd72 100644
--- a/frontend/tests/ram.test.js
+++ b/frontend/tests/ram.test.js
@@ -25,7 +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',
+ 'ramExpandedReport', 'ramExpandedTags', 'ramExpandedGroups', 'ramExpandedElfForm', 'ramElfVersionHint', 'ramGetResponseApdu',
'ramCardIdxAfterRemove', 'ramClearResults', 'ramHideProgress', 'ramOpChanged', 'ramRender', 'ramApplyCard', 'ramExecute', 'decodePrivileges', 'ramActionBtn', 'ramCapToolkitMode', 'ramOpProgressText',
'jcAidNorm', 'jcAidName', 'jcAidSuffix', 'jcAidHtml'];
let code = '';
@@ -549,7 +549,7 @@ test('ramCardIdxAfterRemove keeps the remembered index aligned', () => {
assert.strictEqual(ramCardIdxAfterRemove(null, 1), null);
});
-test('ramExpandedQueryApdu builds the three expanded query forms', () => {
+test('ramExpandedQueryApdu builds the 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),
@@ -564,30 +564,50 @@ test('ramExpandedQueryApdu builds the three expanded query forms', () => {
// 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');
+ // lean: only the objects valid for the kind - the load file version and
+ // modules for ELFs (C5 privileges / C4 ELF AID are application objects),
+ // pySim's field list for the ISD/apps
+ assert.strictEqual(ramExpandedQueryApdu('10', 'lean'), '80F21002094F005C054F9F70CE8400');
+ assert.strictEqual(ramExpandedQueryApdu('20', 'lean'), '80F22002094F005C054F9F70CE8400');
+ assert.strictEqual(ramExpandedQueryApdu('40', 'lean'), '80F24002094F005C054F9F70C5CC00');
+ assert.strictEqual(ramExpandedQueryApdu('80', 'lean'), '80F28002094F005C054F9F70C5CC00');
// 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']);
+ // the next-occurrence page (P2.b1=1) keeps the form and its tag list
+ assert.strictEqual(ramExpandedQueryApdu('40', 'tags', true), '80F240030C4F005C084F9F70C5C4CCCEEA00');
+ assert.strictEqual(ramExpandedQueryApdu('10', 'lean', true), '80F21003094F005C054F9F70CE8400');
+ assert.strictEqual(ramExpandedQueryApdu('10', 'notags', true), '80F21003024F0000');
+ assert.deepStrictEqual(RAM_EXPANDED_FORMS, ['tags', 'noea', 'lean', 'notags']);
+});
+
+test('ramGetResponseApdu fetches a 61xx response (the announced length)', () => {
+ assert.strictEqual(ramGetResponseApdu('6131'), '00C0000031');
+ assert.strictEqual(ramGetResponseApdu('617F'), '00C000007F');
+ assert.strictEqual(ramGetResponseApdu('613f'), '00C000003F');
+ // not a 61xx: ask for all available data
+ assert.strictEqual(ramGetResponseApdu('9000'), '00C0000000');
+ assert.strictEqual(ramGetResponseApdu(''), '00C0000000');
});
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' },
+ { p1: '80', label: 'ISD', compact: true, form: 'tags', sw: '9000', entries: 1 },
+ { p1: '40', label: 'Apps', compact: true, form: 'tags', sw: '9000', entries: 2 },
+ { p1: '10', label: 'ELFs', compact: true, form: 'tags', sw: '9000', entries: 4 },
]), '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' },
+ { p1: '40', label: 'Apps', compact: true, form: 'tags', sw: '9000', entries: 2 },
+ { p1: '10', label: 'ELFs', compact: true, form: 'noea', sw: '9000', entries: 4 },
]), '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' },
+ { p1: '10', label: 'ELFs', compact: true, form: 'notags', sw: '9000', entries: 4 },
]), '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([
@@ -598,17 +618,49 @@ test('ramExpandedReport summarizes the query forms and the fallbacks', () => {
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');
+ // the ELF listing queried twice (P1=10 accepted-but-empty, then the P1=20
+ // fallback with data): one mention, from the pass that produced entries
+ assert.strictEqual(ramExpandedReport([
+ { p1: '10', label: 'ELFs', compact: false, form: 'notags', sw: '9000', entries: 0 },
+ { p1: '20', label: 'ELFs', compact: false, form: 'notags', sw: '9000', entries: 5 },
+ ]), 'Expanded registry: no tag list (ELFs)');
+ // a refusal followed by a pass with data is not reported as a refusal
+ assert.strictEqual(ramExpandedReport([
+ { p1: '10', label: 'ELFs', compact: false, form: null, sw: '6A88' },
+ { p1: '20', label: 'ELFs', compact: false, form: 'lean', sw: '9000', entries: 5 },
+ ]), 'Expanded registry: lean tag list (ELFs)');
+ // the richer form wins when both produced entries
+ assert.strictEqual(ramExpandedReport([
+ { p1: '10', label: 'ELFs', compact: true, form: 'lean', sw: '9000', entries: 3 },
+ { p1: '20', label: 'ELFs', compact: true, form: 'notags', sw: '9000', entries: 5 },
+ ]), 'Expanded registry: lean tag list (ELFs)');
assert.strictEqual(ramExpandedReport([]), '');
delete globalThis.t;
});
-test('the explorer hints when the card returned no package versions', () => {
+test('ramExpandedElfForm names the ELF listing form for the version hint', () => {
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' }]);
+ assert.strictEqual(ramExpandedElfForm([
+ { p1: '10', label: 'ELFs', compact: false, form: 'notags', sw: '9000', entries: 5 },
+ ]), 'notags');
+ // an accepted-but-empty pass is no form for the hint
+ assert.strictEqual(ramExpandedElfForm([
+ { p1: '10', label: 'ELFs', compact: false, form: 'notags', sw: '9000', entries: 0 },
+ ]), '');
+ assert.strictEqual(ramExpandedElfForm([]), '');
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 explorer hints why the package versions are missing', () => {
+ globalThis.t = s => s;
+ const elfs = [{ aid: 'E1', lifecycle: '01', privileges: '00' }];
+ const noForm = ramRenderExploreHtml(null, [], [], elfs);
+ const noTags = ramRenderExploreHtml(null, [], [], elfs, '', '', 'notags');
+ const withVer = ramRenderExploreHtml(null, [], [], [{ aid: 'E1', lifecycle: '01', privileges: '00', version: '0100' }], '', '', 'notags');
+ delete globalThis.t;
+ assert.ok(noForm.includes('Package versions come from the expanded registry query'), noForm);
+ assert.ok(noTags.includes('only in the tag-list forms of the expanded registry query'), noTags);
+ assert.ok(!withVer.includes('Package versions'), withVer);
});
test('the Explore detects the expanded query form and reports it', () => {
@@ -619,6 +671,9 @@ test('the Explore detects the expanded query form and reports it', () => {
// 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');
+ // a 61xx answer holds the data (GET RESPONSE) and a 6310 pages
+ assert.ok(src.includes('ramGetResponseApdu(sw)'), '61xx must be answered with GET RESPONSE');
+ assert.ok(src.includes('ramExpandedQueryApdu(p1, form, page > 1)'), '6310 must page');
});
test('ramParseAppStatus parses the expanded E3 listing (vendor traces)', () => {
diff --git a/pyproject.toml b/pyproject.toml
index 2e4b8da..1c4ce56 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "pysim-simple-server"
-version = "3.6.54"
+version = "3.6.55"
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 d38cc49..43ed253 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.54'
+VERSION = '3.6.55'
MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE