';
- if (!elfs.some(o => o.version)) {
+ if (elfs.some(o => !o.version)) {
html += '
';
}
elfs.forEach(o => {
@@ -9309,7 +9327,11 @@ function ramRenderExploreHtml(mem, isd, apps, elfs, fmt, expanded, elfForm) {
html += '
';
html += '
' + esc(t('Load File AID / Package AID:')) + ' ' + (o.aid ? jcAidHtml(o.aid) : '?') + '
';
html += '
' + esc(t('Lifecycle:')) + ' ' + ramFmtLifecycle(o.lifecycle || '', 'elf') + '
';
- if (o.version) html += '
' + esc(t('Version:')) + ' ' + esc(ramFmtVersion(o.version)) + '
';
+ if (o.version) {
+ html += '
' + esc(t('Version:')) + ' ' + esc(ramFmtVersion(o.version)) +
+ (o.versionFrom === 'app' ? ' (' + esc(t('from the app instance')) + ')' : '') +
+ '
';
+ }
if (o.privileges && o.privileges !== '00') html += '
' + esc(t('Privileges:')) + ' ' + ramFmtPrivileges(o.privileges) + ' (' + o.privileges + ')
';
if (o.moduleAids && o.moduleAids.length) {
html += '
' + esc(t('Executable Module AIDs / Applet Class AIDs:')) + '
';
@@ -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:': 'Прочие данные реестра:',
diff --git a/frontend/sw.js b/frontend/sw.js
index f26f4f5..21e9ed9 100644
--- a/frontend/sw.js
+++ b/frontend/sw.js
@@ -1,4 +1,4 @@
-const CACHE = 'simple-v328';
+const CACHE = 'simple-v329';
const URLS = [
'index.html',
'help.html',
diff --git a/frontend/tests/ram.test.js b/frontend/tests/ram.test.js
index 801fd72..3c036c6 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', 'ramExpandedTags', 'ramExpandedGroups', 'ramExpandedElfForm', 'ramElfVersionHint', 'ramGetResponseApdu',
+ 'ramExpandedReport', 'ramExpandedTags', 'ramExpandedGroups', 'ramExpandedElfForm', 'ramElfVersionHint', 'ramChainGetResponse', 'ramDeriveElfVersions',
'ramCardIdxAfterRemove', 'ramClearResults', 'ramHideProgress', 'ramOpChanged', 'ramRender', 'ramApplyCard', 'ramExecute', 'decodePrivileges', 'ramActionBtn', 'ramCapToolkitMode', 'ramOpProgressText',
'jcAidNorm', 'jcAidName', 'jcAidSuffix', 'jcAidHtml'];
let code = '';
@@ -581,13 +581,45 @@ test('ramExpandedQueryApdu builds the expanded query forms', () => {
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('ramChainGetResponse chains the GET RESPONSE into the query string', () => {
+ // the live card rejects a standalone GET RESPONSE with 6700; the compact
+ // command string (query + C0000000) is how the compact path fetches too
+ assert.strictEqual(ramChainGetResponse('80F280020C4F005C084F9F70C5C4CCCEEA00'),
+ '80F280020C4F005C084F9F70C5C4CCCEEA00C0000000');
+ assert.strictEqual(ramChainGetResponse('80F21002094F005C054F9F70CE8400'),
+ '80F21002094F005C054F9F70CE8400C0000000');
+});
+
+test('ramDeriveElfVersions reads a package version from its applet instance', () => {
+ // the app entry carries CE (the load file version) and C4 (the package
+ // AID), so an ELF whose own listing refused every tag list still gets one
+ const elfs = [
+ { aid: 'A1130001180001FFFFFFFF89A1003900', lifecycle: '01', privileges: '00' },
+ { aid: 'F0414C46416001', lifecycle: '01', privileges: '00', version: '0100' },
+ { aid: 'DEADBEEF', lifecycle: '01', privileges: '00' },
+ ];
+ const apps = [
+ { aid: 'A1130001180001FFFFFFFF89A1003908', version: '0903', elfAid: 'A1130001180001FFFFFFFF89A1003900' },
+ { aid: 'A0000005591010FFFFFFFF8900001200', version: '0001', elfAid: 'A0000005591010FFFFFFFF8900000E00' },
+ ];
+ ramDeriveElfVersions(elfs, apps);
+ assert.strictEqual(elfs[0].version, '0903');
+ assert.strictEqual(elfs[0].versionFrom, 'app');
+ // an own version is never overwritten, and an unmatched package stays bare
+ assert.strictEqual(elfs[1].version, '0100');
+ assert.strictEqual(elfs[1].versionFrom, undefined);
+ assert.strictEqual(elfs[2].version, undefined);
+});
+
+test('the explorer annotates a derived package version', () => {
+ globalThis.t = s => s;
+ const out = ramRenderExploreHtml(null, [], [],
+ [{ aid: 'E1', lifecycle: '01', privileges: '00', version: '0903', versionFrom: 'app' }]);
+ const plain = ramRenderExploreHtml(null, [], [],
+ [{ aid: 'E1', lifecycle: '01', privileges: '00', version: '0903' }]);
+ delete globalThis.t;
+ assert.ok(out.includes('Version: 9.3
(from the app instance)'), out);
+ assert.ok(!plain.includes('from the app instance'), plain);
});
test('ramExpandedReport summarizes the query forms and the fallbacks', () => {
@@ -671,8 +703,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');
+ // a 61xx answer holds the data (chained GET RESPONSE) and a 6310 pages
+ assert.ok(src.includes('ramChainGetResponse(apdu)'), '61xx must be answered with a chained GET RESPONSE');
+ assert.ok(!src.includes('00C00000'), 'no standalone GET RESPONSE (it answers 6700)');
assert.ok(src.includes('ramExpandedQueryApdu(p1, form, page > 1)'), '6310 must page');
});
diff --git a/pyproject.toml b/pyproject.toml
index 1c4ce56..43a22e7 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "pysim-simple-server"
-version = "3.6.55"
+version = "3.6.56"
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 43ed253..2a3b464 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.55'
+VERSION = '3.6.56'
MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE