feat: ELF versions via P1=20 retry and per-AID app queries (v3.7.0)
No live card accepts a tag list for the ELF queries (tags/noea/lean all answer 6A88 on P1=10 - the log of 2026-09-30 11:11), so the version-carrying CE tag was unreachable there; the ELF listing itself (notags + chained GET RESPONSE) works and carries AID + life cycle + modules only. Two fallbacks: - when the ELF rows have no version and the details pass never got a tag-list form, the ladder is walked once more with P1=20 (a card's P1=10 implementation can be stricter) and merged by AID; - any version still missing is fetched per applet AID: the AID search qualifier (4F <len> <AID>, GP 11.4.2.3 Table 11-35) plus the apps tag list selects a single applet entry, whose CE is its package's version (the applet <-> package mapping is the AID prefix; at most 6 queries). Also: - the app and ISD rows show their parsed CE version (the expanded apps listing carries it); - an unfulfilled 6310 (the next-occurrence page refused, e.g. 6A86) is reported as '(first page only)' instead of looking complete; - the page loop only continues when the chained GET RESPONSE was accepted - a refused retry no longer costs an extra P2=03 packet. sw cache simple-v332; tests: 557 Python / 716 Node.
This commit is contained in:
+80
-11
@@ -1750,7 +1750,7 @@
|
|||||||
// ===== Version =====
|
// ===== Version =====
|
||||||
// Single source of truth for the PWA version: shown in the header and used
|
// Single source of truth for the PWA version: shown in the header and used
|
||||||
// by the server version check in pysimConnect().
|
// by the server version check in pysimConnect().
|
||||||
const SIMPLE_VERSION = '3.6.58';
|
const SIMPLE_VERSION = '3.7.0';
|
||||||
document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION;
|
document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION;
|
||||||
|
|
||||||
// ===== Tab switching =====
|
// ===== Tab switching =====
|
||||||
@@ -9184,6 +9184,7 @@ function ramExpandedGroups(report) {
|
|||||||
}
|
}
|
||||||
g.compact = g.compact || !!r.compact;
|
g.compact = g.compact || !!r.compact;
|
||||||
g.transport = g.transport || !!r.transport;
|
g.transport = g.transport || !!r.transport;
|
||||||
|
g.more = g.more || !!r.more;
|
||||||
if (r.form) {
|
if (r.form) {
|
||||||
const score = ((r.entries || 0) > 0 ? 100 : 0) +
|
const score = ((r.entries || 0) > 0 ? 100 : 0) +
|
||||||
(RAM_EXPANDED_FORMS.length - RAM_EXPANDED_FORMS.indexOf(r.form));
|
(RAM_EXPANDED_FORMS.length - RAM_EXPANDED_FORMS.indexOf(r.form));
|
||||||
@@ -9211,7 +9212,8 @@ function ramExpandedReport(report) {
|
|||||||
: (f === 'noea' ? t('tag list') : (f === 'lean' ? t('lean tag list') : t('no tag list')));
|
: (f === 'noea' ? t('tag list') : (f === 'lean' ? t('lean tag list') : t('no tag list')));
|
||||||
const parts = [];
|
const parts = [];
|
||||||
RAM_EXPANDED_FORMS.forEach(f => {
|
RAM_EXPANDED_FORMS.forEach(f => {
|
||||||
const labels = groups.filter(g => g.form === f).map(g => g.label);
|
const labels = groups.filter(g => g.form === f)
|
||||||
|
.map(g => g.label + (g.more ? ' (' + t('first page only') + ')' : ''));
|
||||||
if (labels.length) parts.push(formName(f) + ' (' + labels.join(', ') + ')');
|
if (labels.length) parts.push(formName(f) + ' (' + labels.join(', ') + ')');
|
||||||
});
|
});
|
||||||
groups.forEach(g => {
|
groups.forEach(g => {
|
||||||
@@ -9298,6 +9300,7 @@ function ramRenderExploreHtml(mem, isd, apps, elfs, fmt, expanded, elfForm) {
|
|||||||
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-blue-400">';
|
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-blue-400">';
|
||||||
html += '<div>' + esc(t('AID:')) + ' ' + (o.aid ? jcAidHtml(o.aid) : '?') + '</div>';
|
html += '<div>' + esc(t('AID:')) + ' ' + (o.aid ? jcAidHtml(o.aid) : '?') + '</div>';
|
||||||
html += '<div>' + esc(t('Lifecycle:')) + ' ' + ramFmtLifecycle(o.lifecycle || '', 'isd') + '</div>';
|
html += '<div>' + esc(t('Lifecycle:')) + ' ' + ramFmtLifecycle(o.lifecycle || '', 'isd') + '</div>';
|
||||||
|
if (o.version) html += '<div>' + esc(t('Version:')) + ' ' + esc(ramFmtVersion(o.version)) + '</div>';
|
||||||
if (o.privileges) html += '<div>' + esc(t('Privileges:')) + ' ' + ramFmtPrivileges(o.privileges) + ' (' + o.privileges + ')</div>';
|
if (o.privileges) html += '<div>' + esc(t('Privileges:')) + ' ' + ramFmtPrivileges(o.privileges) + ' (' + o.privileges + ')</div>';
|
||||||
if (o.elfAid) html += '<div>' + esc(t('Load File AID / Package AID:')) + ' ' + jcAidHtml(o.elfAid) + '</div>';
|
if (o.elfAid) html += '<div>' + esc(t('Load File AID / Package AID:')) + ' ' + jcAidHtml(o.elfAid) + '</div>';
|
||||||
if (o.sdAid) html += '<div>' + esc(t('SD AID:')) + ' ' + jcAidHtml(o.sdAid) + '</div>';
|
if (o.sdAid) html += '<div>' + esc(t('SD AID:')) + ' ' + jcAidHtml(o.sdAid) + '</div>';
|
||||||
@@ -9315,6 +9318,7 @@ function ramRenderExploreHtml(mem, isd, apps, elfs, fmt, expanded, elfForm) {
|
|||||||
html += '<div class="flex-1 min-w-0">';
|
html += '<div class="flex-1 min-w-0">';
|
||||||
html += '<div>' + esc(t('Application / Instance AID:')) + ' ' + (o.aid ? jcAidHtml(o.aid) : '?') + '</div>';
|
html += '<div>' + esc(t('Application / Instance AID:')) + ' ' + (o.aid ? jcAidHtml(o.aid) : '?') + '</div>';
|
||||||
html += '<div>' + esc(t('Lifecycle:')) + ' ' + ramFmtLifecycle(o.lifecycle || '', ramEntryKind(o)) + '</div>';
|
html += '<div>' + esc(t('Lifecycle:')) + ' ' + ramFmtLifecycle(o.lifecycle || '', ramEntryKind(o)) + '</div>';
|
||||||
|
if (o.version) html += '<div>' + esc(t('Version:')) + ' ' + esc(ramFmtVersion(o.version)) + '</div>';
|
||||||
if (o.privileges) html += '<div>' + esc(t('Privileges:')) + ' ' + ramFmtPrivileges(o.privileges) + ' (' + o.privileges + ')</div>';
|
if (o.privileges) html += '<div>' + esc(t('Privileges:')) + ' ' + ramFmtPrivileges(o.privileges) + ' (' + o.privileges + ')</div>';
|
||||||
if (o.implicitSel) html += '<div>' + esc(t('Implicit sel:')) + ' ' + o.implicitSel + '</div>';
|
if (o.implicitSel) html += '<div>' + esc(t('Implicit sel:')) + ' ' + o.implicitSel + '</div>';
|
||||||
if (o.elfAid) html += '<div>' + esc(t('Load File AID / Package AID:')) + ' ' + jcAidHtml(o.elfAid) + '</div>';
|
if (o.elfAid) html += '<div>' + esc(t('Load File AID / Package AID:')) + ' ' + jcAidHtml(o.elfAid) + '</div>';
|
||||||
@@ -9463,6 +9467,7 @@ async function ramExplore(sp) {
|
|||||||
return res.por.decoded ? (res.por.decoded.last_response_data || '') : '';
|
return res.por.decoded ? (res.por.decoded.last_response_data || '') : '';
|
||||||
};
|
};
|
||||||
let page = 0;
|
let page = 0;
|
||||||
|
let more = false;
|
||||||
while (accepted && page++ < 8) {
|
while (accepted && page++ < 8) {
|
||||||
const apdu = ramExpandedQueryApdu(p1, form, page > 1);
|
const apdu = ramExpandedQueryApdu(p1, form, page > 1);
|
||||||
const what = page > 1 ? 'P2=03 (' + form + ')' : 'P2=02 (' + form + ')';
|
const what = page > 1 ? 'P2=03 (' + form + ')' : 'P2=02 (' + form + ')';
|
||||||
@@ -9481,13 +9486,18 @@ async function ramExplore(sp) {
|
|||||||
// more occurrences, and a '61xx' page may mask that signal
|
// more occurrences, and a '61xx' page may mask that signal
|
||||||
// (the GET RESPONSE's own SW replaces it) - try one more page
|
// (the GET RESPONSE's own SW replaces it) - try one more page
|
||||||
if (p1 === '80') break;
|
if (p1 === '80') break;
|
||||||
if (sw !== '6310' && !pending) break;
|
// continue only when the answer really says 'more occurrences'
|
||||||
|
// or the chained retry was accepted (a '61xx' page masks the
|
||||||
|
// 6310 signal with the GET RESPONSE's own SW); a refused retry
|
||||||
|
// must not cost another packet (v3.7.0)
|
||||||
|
if (sw !== '6310' && !(pending && ramRemoteSwOk(sw))) break;
|
||||||
|
more = true;
|
||||||
}
|
}
|
||||||
const parsed = data ? parser(data) : [];
|
const parsed = data ? parser(data) : [];
|
||||||
if (parsed.length) {
|
if (parsed.length) {
|
||||||
expandedForm = form;
|
expandedForm = form;
|
||||||
return { form: form, parsed: parsed, sw: sw, entries: parsed.length,
|
return { form: form, parsed: parsed, sw: sw, entries: parsed.length,
|
||||||
transport: transportError };
|
transport: transportError, more: more };
|
||||||
}
|
}
|
||||||
lastSw = sw;
|
lastSw = sw;
|
||||||
lastTransport = transportError;
|
lastTransport = transportError;
|
||||||
@@ -9495,10 +9505,11 @@ async function ramExplore(sp) {
|
|||||||
if (ramRemoteSwOk(sw)) {
|
if (ramRemoteSwOk(sw)) {
|
||||||
// accepted but empty: nothing to gain from another form
|
// accepted but empty: nothing to gain from another form
|
||||||
return { form: form, parsed: [], sw: sw, entries: 0,
|
return { form: form, parsed: [], sw: sw, entries: 0,
|
||||||
transport: transportError };
|
transport: transportError, more: more };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return { form: null, parsed: [], sw: lastSw, entries: 0, transport: lastTransport };
|
return { form: null, parsed: [], sw: lastSw, entries: 0,
|
||||||
|
transport: lastTransport, more: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
async function paginate(p1, collector, parser, label, quiet) {
|
async function paginate(p1, collector, parser, label, quiet) {
|
||||||
@@ -9574,7 +9585,7 @@ async function ramExplore(sp) {
|
|||||||
const out = await expandedPass(p1, label, parser, spi2);
|
const out = await expandedPass(p1, label, parser, spi2);
|
||||||
expandedReport.push({ p1: p1, label: label, compact: collector.length > before,
|
expandedReport.push({ p1: p1, label: label, compact: collector.length > before,
|
||||||
form: out.form, sw: out.sw, entries: out.entries,
|
form: out.form, sw: out.sw, entries: out.entries,
|
||||||
transport: out.transport });
|
transport: out.transport, more: out.more });
|
||||||
if (collector.length === before) {
|
if (collector.length === before) {
|
||||||
// Nothing from the compact pass: the expanded result is the listing
|
// Nothing from the compact pass: the expanded result is the listing
|
||||||
collector.push(...out.parsed);
|
collector.push(...out.parsed);
|
||||||
@@ -9627,10 +9638,61 @@ async function ramExplore(sp) {
|
|||||||
await paginate('20', elfs, ramParseElfStatus, t('ELFs'), true);
|
await paginate('20', elfs, ramParseElfStatus, t('ELFs'), true);
|
||||||
if (!elfs.length) errors.push(...elfFailure);
|
if (!elfs.length) errors.push(...elfFailure);
|
||||||
}
|
}
|
||||||
|
// The ELF tag-list forms may be refused for P1=10 while P1=20 accepts
|
||||||
|
// them (a card's P1=10 implementation can be stricter): when the ELF rows
|
||||||
|
// have no version and the details pass never got a tag-list form, walk the
|
||||||
|
// ladder once more with P1=20 and merge by AID (v3.7.0).
|
||||||
|
async function elfTagListRetry() {
|
||||||
|
if (cntrLow || !elfs.length || elfs.some(o => o.version)) return;
|
||||||
|
const g = ramExpandedGroups(expandedReport).find(x => x.label === t('ELFs'));
|
||||||
|
if (g && ['tags', 'noea', 'lean'].indexOf(g.form) >= 0) return;
|
||||||
|
const saved = expandedForm;
|
||||||
|
expandedForm = null; // start the ladder at the richest form
|
||||||
|
try {
|
||||||
|
const out = await expandedPass('20', t('ELFs'), ramParseElfStatus, ramListingSpi2('20'));
|
||||||
|
expandedReport.push({ p1: '20', label: t('ELFs'), compact: true,
|
||||||
|
form: out.form, sw: out.sw, entries: out.entries,
|
||||||
|
transport: out.transport, more: out.more });
|
||||||
|
if (out.parsed.length) ramMergeExpanded(elfs, out.parsed);
|
||||||
|
} finally {
|
||||||
|
if (expandedForm === null) expandedForm = saved;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await elfTagListRetry();
|
||||||
|
|
||||||
// A package whose own listing carries no version may still be known from
|
// 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
|
// its applet instance (the app entry carries the load file version and the
|
||||||
// package AID); the rows are annotated when this inference is used.
|
// package AID); the rows are annotated when this inference is used.
|
||||||
ramDeriveElfVersions(elfs, apps);
|
ramDeriveElfVersions(elfs, apps);
|
||||||
|
|
||||||
|
// Still missing versions: query the apps listing once per applet AID (the
|
||||||
|
// AID search qualifier selects one entity). The apps tag list works even
|
||||||
|
// where the ELF one is refused, and an applet's CE is its package's
|
||||||
|
// version - the applet <-> package mapping is the AID prefix (v3.7.0).
|
||||||
|
async function elfPerAidVersions() {
|
||||||
|
const missing = elfs.filter(e => !e.version && e.aid);
|
||||||
|
for (const e of missing.slice(0, 6)) {
|
||||||
|
if (cntrLow) break;
|
||||||
|
const app = apps.find(a => a.aid && !a.version &&
|
||||||
|
String(a.aid).toUpperCase().indexOf(String(e.aid).toUpperCase()) === 0);
|
||||||
|
if (!app) continue;
|
||||||
|
ramShowProgress(t('Version') + ' ' + e.aid + '...');
|
||||||
|
const res = await sendOta(ramExpandedQueryApdu('40', 'tags', false, app.aid),
|
||||||
|
Object.assign({}, sp, { cntr, spi2: ramListingSpi2('40') }));
|
||||||
|
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)) continue;
|
||||||
|
const data = res.por.decoded ? (res.por.decoded.last_response_data || '') : '';
|
||||||
|
const parsed = data ? ramParseAppStatus(data) : [];
|
||||||
|
const hit = parsed.find(x => x.version);
|
||||||
|
if (hit) {
|
||||||
|
e.version = hit.version;
|
||||||
|
e.versionFrom = 'app';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await elfPerAidVersions();
|
||||||
// Only write the counter when a packet was actually accepted: a run whose
|
// Only write the counter when a packet was actually accepted: a run whose
|
||||||
// start value was stale must never lower the preset counter.
|
// start value was stale must never lower the preset counter.
|
||||||
if (cntr !== sp.cntr) ramSaveCntr(cntr);
|
if (cntr !== sp.cntr) ramSaveCntr(cntr);
|
||||||
@@ -11173,13 +11235,19 @@ function ramExpandedTags(p1, form) {
|
|||||||
return base + (elf ? '84' : '');
|
return base + (elf ? '84' : '');
|
||||||
}
|
}
|
||||||
|
|
||||||
// `next` = the next-occurrence page (P2.b1=1, GP 11.4.3.2) after a '6310'.
|
// `next` = the next-occurrence page (P2.b1=1, GP 11.4.3.2) after a '6310';
|
||||||
function ramExpandedQueryApdu(p1, form, next) {
|
// `aidHex` = an AID search qualifier (`4F <len> <AID>`, GP 11.4.2.3
|
||||||
|
// Table 11-35) that selects a single registry entity - used to fetch one
|
||||||
|
// applet's entry when the listing cannot be paged (v3.7.0).
|
||||||
|
function ramExpandedQueryApdu(p1, form, next, aidHex) {
|
||||||
const tags = ramExpandedTags(p1, form || 'tags');
|
const tags = ramExpandedTags(p1, form || 'tags');
|
||||||
const p2 = next ? '03' : '02';
|
const p2 = next ? '03' : '02';
|
||||||
if (!tags) return '80F2' + p1 + p2 + '02' + '4F00' + '00';
|
const aid = (aidHex || '').replace(/[^0-9a-fA-F]/g, '').toUpperCase();
|
||||||
const hex = n => n.toString(16).toUpperCase().padStart(2, '0');
|
const hex = n => n.toString(16).toUpperCase().padStart(2, '0');
|
||||||
return '80F2' + p1 + p2 + hex(tags.length / 2 + 4) + '4F005C' + hex(tags.length / 2) + tags + '00';
|
const filter = aid ? '4F' + hex(aid.length / 2) + aid : '4F00';
|
||||||
|
const lc = filter.length / 2 + (tags ? tags.length / 2 + 2 : 0);
|
||||||
|
return '80F2' + p1 + p2 + hex(lc) + filter +
|
||||||
|
(tags ? '5C' + hex(tags.length / 2) + tags : '') + '00';
|
||||||
}
|
}
|
||||||
|
|
||||||
// A '61xx' remote answer means the card holds the response data. The live
|
// A '61xx' remote answer means the card holds the response data. The live
|
||||||
@@ -18919,6 +18987,7 @@ const LANG_RU = {
|
|||||||
'card counter': 'счётчик карты',
|
'card counter': 'счётчик карты',
|
||||||
'the counter must be above it': 'счётчик должен быть больше',
|
'the counter must be above it': 'счётчик должен быть больше',
|
||||||
'Go to preset': 'К пресету',
|
'Go to preset': 'К пресету',
|
||||||
|
'first page only': 'только первая страница',
|
||||||
'the operation was stopped': 'операция остановлена',
|
'the operation was stopped': 'операция остановлена',
|
||||||
'STK menu entry:': 'Пункт меню STK:',
|
'STK menu entry:': 'Пункт меню STK:',
|
||||||
'position': 'позиция',
|
'position': 'позиция',
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
const CACHE = 'simple-v331';
|
const CACHE = 'simple-v332';
|
||||||
const URLS = [
|
const URLS = [
|
||||||
'index.html',
|
'index.html',
|
||||||
'help.html',
|
'help.html',
|
||||||
|
|||||||
@@ -580,6 +580,12 @@ test('ramExpandedQueryApdu builds the expanded query forms', () => {
|
|||||||
assert.strictEqual(ramExpandedQueryApdu('10', 'lean', true), '80F21003094F005C054F9F70CE8400');
|
assert.strictEqual(ramExpandedQueryApdu('10', 'lean', true), '80F21003094F005C054F9F70CE8400');
|
||||||
assert.strictEqual(ramExpandedQueryApdu('10', 'notags', true), '80F21003024F0000');
|
assert.strictEqual(ramExpandedQueryApdu('10', 'notags', true), '80F21003024F0000');
|
||||||
assert.deepStrictEqual(RAM_EXPANDED_FORMS, ['tags', 'noea', 'lean', 'notags']);
|
assert.deepStrictEqual(RAM_EXPANDED_FORMS, ['tags', 'noea', 'lean', 'notags']);
|
||||||
|
// an AID search qualifier (4F <len> <AID>) selects one entity: used to
|
||||||
|
// fetch a single applet's entry when the listing cannot be paged
|
||||||
|
assert.strictEqual(ramExpandedQueryApdu('40', 'tags', false, 'A1130001180001FFFFFFFF89A1003908'),
|
||||||
|
'80F240021C4F10A1130001180001FFFFFFFF89A10039085C084F9F70C5C4CCCEEA00');
|
||||||
|
assert.strictEqual(ramExpandedQueryApdu('40', 'notags', false, 'A1130001180001'),
|
||||||
|
'80F24002094F07A113000118000100');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('ramChainGetResponse chains the GET RESPONSE into the query string', () => {
|
test('ramChainGetResponse chains the GET RESPONSE into the query string', () => {
|
||||||
@@ -667,6 +673,10 @@ test('ramExpandedReport summarizes the query forms and the fallbacks', () => {
|
|||||||
{ p1: '10', label: 'ELFs', compact: true, form: 'lean', sw: '9000', entries: 3 },
|
{ p1: '10', label: 'ELFs', compact: true, form: 'lean', sw: '9000', entries: 3 },
|
||||||
{ p1: '20', label: 'ELFs', compact: true, form: 'notags', sw: '9000', entries: 5 },
|
{ p1: '20', label: 'ELFs', compact: true, form: 'notags', sw: '9000', entries: 5 },
|
||||||
]), 'Expanded registry: lean tag list (ELFs)');
|
]), 'Expanded registry: lean tag list (ELFs)');
|
||||||
|
// an unfulfilled 6310 (the next-occurrence page was refused) is noted
|
||||||
|
assert.strictEqual(ramExpandedReport([
|
||||||
|
{ p1: '40', label: 'Apps', compact: true, form: 'tags', sw: '6A86', entries: 2, more: true },
|
||||||
|
]), 'Expanded registry: tag list + EA (Apps (first page only))');
|
||||||
// a request/transport failure is not a remote status word
|
// a request/transport failure is not a remote status word
|
||||||
assert.strictEqual(ramExpandedReport([
|
assert.strictEqual(ramExpandedReport([
|
||||||
{ p1: '40', label: 'Apps', compact: true, form: 'tags', sw: '9000', entries: 9 },
|
{ p1: '40', label: 'Apps', compact: true, form: 'tags', sw: '9000', entries: 9 },
|
||||||
@@ -734,6 +744,26 @@ test('ramCntrLowHtml states the facts and offers the preset jump', () => {
|
|||||||
assert.strictEqual(ramCntrLowHtml(null, 0), '');
|
assert.strictEqual(ramCntrLowHtml(null, 0), '');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('the explorer shows versions on app and ISD rows too', () => {
|
||||||
|
globalThis.t = s => s;
|
||||||
|
const out = ramRenderExploreHtml(null,
|
||||||
|
[{ aid: 'A000000151000000', lifecycle: '0F', privileges: '82FC80', version: '0001' }],
|
||||||
|
[{ aid: 'A1130001180001FFFFFFFF89A1003908', lifecycle: '07', privileges: '000000', version: '0903' }],
|
||||||
|
[]);
|
||||||
|
delete globalThis.t;
|
||||||
|
assert.ok(out.includes('Version: 0.1'), out);
|
||||||
|
assert.ok(out.includes('Version: 9.3'), out);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('the Explore retries the ELF tag lists with P1=20 and per applet AID', () => {
|
||||||
|
const src = extractFunc(html, 'ramExplore');
|
||||||
|
assert.ok(src.includes('elfTagListRetry'), 'the P1=20 tag-list retry must exist');
|
||||||
|
assert.ok(src.includes("expandedPass('20'"), 'the retry must query P1=20');
|
||||||
|
assert.ok(src.includes('elfPerAidVersions'), 'the per-AID version queries must exist');
|
||||||
|
assert.ok(src.includes("ramExpandedQueryApdu('40', 'tags', false, app.aid)"),
|
||||||
|
'the per-AID query must use the AID filter');
|
||||||
|
});
|
||||||
|
|
||||||
test('every SCP80/RAM flow checks the low counter and stops', () => {
|
test('every SCP80/RAM flow checks the low counter and stops', () => {
|
||||||
// the Explore: the guarded sender, the loop guards and the final branch
|
// the Explore: the guarded sender, the loop guards and the final branch
|
||||||
const explore = extractFunc(html, 'ramExplore');
|
const explore = extractFunc(html, 'ramExplore');
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "pysim-simple-server"
|
name = "pysim-simple-server"
|
||||||
version = "3.6.58"
|
version = "3.7.0"
|
||||||
description = "HTTP REST server wrapping pysim for the SIMple PWA"
|
description = "HTTP REST server wrapping pysim for the SIMple PWA"
|
||||||
requires-python = ">=3.8"
|
requires-python = ">=3.8"
|
||||||
# pysim is a git-only dependency installed explicitly by setup.bat/setup.sh.
|
# pysim is a git-only dependency installed explicitly by setup.bat/setup.sh.
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ from osmocom.tlv import BER_TLV_IE
|
|||||||
from osmocom.utils import rpad
|
from osmocom.utils import rpad
|
||||||
|
|
||||||
|
|
||||||
VERSION = '3.6.58'
|
VERSION = '3.7.0'
|
||||||
|
|
||||||
MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE
|
MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user