Code-review fixes: bad-format visibility, view-aware sync, counter hygiene
- The RAM step line (and the plain send's PoR line) now name a bad-format
failure ("bad format 02 (wrong length)") instead of reading "PoR ok": the
step record carries por_bad_format_name and ramStepLine renders it.
- ramSyncCounter follows the view the notice came from ('ram'/'sp' - preset
and keyset selectors alike) and refreshes the keyset labels after a sync.
- The ETSI-release line no longer hides behind the memory block (it needs
only the ELF list).
- /api/send-ota reports final_cntr only for a packet the card accepted: the
failed-envelope path used to persist the packet's own counter on a
rejection (the audit trail's small bump on cntr_low).
- _ram_detect_format's docstring corrected (both expanded codings execute on
the live ISD; the earlier bad format was a length-mismatched template).
- Tests: ramStepLine bad format, the view-aware notice, the accepted/rejected
final_cntr cases, and an end-to-end /api/counter-probe endpoint test
(400s, no write on a rejected walk, accepted+1 stored).
- sw cache simple-v356.
This commit is contained in:
+64
-25
@@ -8335,6 +8335,8 @@ async function pysimSendOta() {
|
||||
if (por && por.response_status) {
|
||||
msg += ' | PoR status: ' + por.response_status + ' (TAR ' + por.tar + ')';
|
||||
if (por.cntr) msg += ' | PoR CNTR: ' + por.cntr;
|
||||
if (por.bad_format) msg += ' | ' + t('bad format') + ' ' + por.bad_format +
|
||||
(por.bad_format_name ? ' (' + por.bad_format_name + ')' : '');
|
||||
if (por.decoded && por.decoded.last_status_word) msg += ' | last SW ' + por.decoded.last_status_word + ' ' + (por.decoded.last_response_data || '');
|
||||
const okPor = por.response_status === 'por_ok';
|
||||
porStatusEl.textContent = 'PoR: ' + por.response_status;
|
||||
@@ -9503,26 +9505,33 @@ function ramDeriveElfVersions(elfs, apps) {
|
||||
// Render the full explorer result as structured HTML
|
||||
function ramRenderExploreHtml(mem, isd, apps, elfs, fmt, expanded, elfForm) {
|
||||
let html = '';
|
||||
// The card's ETSI release needs only the ELF list: it shows inside the
|
||||
// resources box when that exists, and on its own line when the memory
|
||||
// query failed
|
||||
const etsi = cardEtsiRelease(elfs);
|
||||
let etsiHtml = '';
|
||||
if (etsi) {
|
||||
let val;
|
||||
if (etsi.release) val = etsi.release + (etsi.basis ? ' (' + etsi.basis + ')' : '');
|
||||
else if (etsi.basis) val = t('unknown') + ' (' + etsi.basis + ')';
|
||||
else val = t('unknown') + ' \u2014 ' + ramElfVersionHint(elfForm);
|
||||
etsiHtml = '<div title="' + esc(t('Inferred from the card\'s installed uicc.toolkit package version (the same table as the CAP analysis); the expanded registry query provides the versions.')) + '">' +
|
||||
esc(t('ETSI release:')) + ' ' + esc(val) + '</div>';
|
||||
}
|
||||
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">';
|
||||
html += '<div class="font-semibold text-sm mb-1" title="' + esc(t('Extended Card Resources (GP tag FF21, ETSI TS 102 226 8.2.1.7.2): installed applications, free non-volatile and free volatile memory')) + '">' + esc(t('Card resources (GET DATA FF21)')) + '</div>';
|
||||
html += '<div>' + esc(t('Applications:')) + ' ' + (mem.appCount != null ? mem.appCount : '?') + '</div>';
|
||||
html += '<div>' + esc(t('Free NV:')) + ' ' + (mem.freeNV != null ? mem.freeNV + ' B' : '?') + '</div>';
|
||||
html += '<div>' + esc(t('Free Volatile:')) + ' ' + (mem.freeV != null ? mem.freeV + ' B' : '?') + '</div>';
|
||||
const etsi = cardEtsiRelease(elfs);
|
||||
if (etsi) {
|
||||
let val;
|
||||
if (etsi.release) val = etsi.release + (etsi.basis ? ' (' + etsi.basis + ')' : '');
|
||||
else if (etsi.basis) val = t('unknown') + ' (' + etsi.basis + ')';
|
||||
else val = t('unknown') + ' \u2014 ' + ramElfVersionHint(elfForm);
|
||||
html += '<div title="' + esc(t('Inferred from the card\'s installed uicc.toolkit package version (the same table as the CAP analysis); the expanded registry query provides the versions.')) + '">' +
|
||||
esc(t('ETSI release:')) + ' ' + esc(val) + '</div>';
|
||||
}
|
||||
html += etsiHtml;
|
||||
if (fmt) {
|
||||
html += '<div title="' + esc(t('compact = the TS 102 226 5.2.1 command string (one C-APDU per secured packet, e.g. GET STATUS + GET RESPONSE); expanded = the AA/22 Command Scripting template (5.2.1.0). Detected per operation with a read-only GET STATUS probe; a card that rejects the deprecated compact P2.b2=0 form answers the expanded one.')) + '">' +
|
||||
esc(t('RAM command format:')) + ' ' + esc(fmt) + '</div>';
|
||||
}
|
||||
html += '</div>';
|
||||
} else if (etsiHtml) {
|
||||
html += '<div class="mb-4 text-xs text-gray-600 dark:text-slate-400">' + etsiHtml + '</div>';
|
||||
}
|
||||
if (expanded) {
|
||||
html += '<div class="mb-4 text-xs text-gray-600 dark:text-slate-400">' + esc(expanded) + '</div>';
|
||||
@@ -10173,7 +10182,13 @@ function ramStepLine(s, idx, comps, blockSize) {
|
||||
if (s.bytes) line += ' \u00b7 ' + s.bytes + ' B';
|
||||
if (s.segments > 1) line += ' / ' + s.segments + ' SMS';
|
||||
} else {
|
||||
if (s.por_status === 'por_ok') line += t('PoR ok');
|
||||
if (s.por_bad_format) {
|
||||
// the card parsed the scripting template but aborted on a
|
||||
// malformed command (TS 102 226 5.2.2 Bad format TLV): there is no
|
||||
// remote status word to show
|
||||
line += t('bad format') + ' ' + s.por_bad_format +
|
||||
(s.por_bad_format_name ? ' (' + s.por_bad_format_name + ')' : '');
|
||||
} else if (s.por_status === 'por_ok') line += t('PoR ok');
|
||||
else if (s.por_status === 'no_por') line += t('no PoR');
|
||||
else if (s.por_status && s.por_status.indexOf('por_error') === 0) {
|
||||
const legacy = s.por_status.replace(/^por_error_?/, '');
|
||||
@@ -10519,8 +10534,9 @@ async function ramCompatTest(sp) {
|
||||
if (probeNote) modal += '<div class="mt-2 text-gray-500 dark:text-slate-400">' + esc(probeNote) + '</div>';
|
||||
modal += ramCompatDetailHtml(data, mem);
|
||||
if (spCntrLow(data)) {
|
||||
modal += ramCntrLowHtml(ramCntrLowPresetIdx());
|
||||
ramShowCntrLow();
|
||||
const v = ramCntrLowView();
|
||||
modal += ramCntrLowHtml(ramCntrLowPresetIdx(v), v);
|
||||
ramShowCntrLow(undefined, v);
|
||||
}
|
||||
ramOpModalFinish(verdict, !!data.imports_ok, modal, true);
|
||||
}
|
||||
@@ -10880,27 +10896,42 @@ function ramNvLine(data) {
|
||||
|
||||
// The preset the failed operation used (the RAM form's selection, else the
|
||||
// SCP80 form's) - the "Go to preset" target.
|
||||
function ramCntrLowPresetIdx() {
|
||||
// The view a low-counter notice came from ('ram' | 'sp'): the notice's
|
||||
// actions (sync probe, preset jump) must follow the failed operation's view -
|
||||
// the two views can sit on different presets.
|
||||
function ramCntrLowView() {
|
||||
const pick = id => {
|
||||
const el = document.getElementById(id);
|
||||
const i = el && el.value !== '' ? parseInt(el.value, 10) : NaN;
|
||||
return (!isNaN(i) && cards[i]) ? i : -1;
|
||||
};
|
||||
return pick('ram-card-sel') >= 0 ? 'ram' : 'sp';
|
||||
}
|
||||
|
||||
function ramCntrLowPresetIdx(view) {
|
||||
const pick = id => {
|
||||
const el = document.getElementById(id);
|
||||
const i = el && el.value !== '' ? parseInt(el.value, 10) : NaN;
|
||||
return (!isNaN(i) && cards[i]) ? i : -1;
|
||||
};
|
||||
const ram = pick('ram-card-sel');
|
||||
return ram >= 0 ? ram : pick('sp-card-sel');
|
||||
const sp = pick('sp-card-sel');
|
||||
if (view === 'sp') return sp >= 0 ? sp : ram;
|
||||
return ram >= 0 ? ram : sp;
|
||||
}
|
||||
|
||||
// Low-counter notice (pure HTML): the facts plus two actions - a bounded
|
||||
// counter sync probe and a button that opens the failed operation's preset in
|
||||
// the Cards tab, where the counter can be raised by hand.
|
||||
function ramCntrLowHtml(presetIdx) {
|
||||
function ramCntrLowHtml(presetIdx, view) {
|
||||
const idx = Number.isInteger(presetIdx) && presetIdx >= 0 ? presetIdx : -1;
|
||||
const v = view === 'sp' ? 'sp' : 'ram';
|
||||
const text = t('Low counter') + ' \u2014 ' + t('the card rejected the packet');
|
||||
const btnCls = 'px-2 py-1 text-xs rounded border border-red-300 dark:border-red-700 ' +
|
||||
'hover:bg-red-100 dark:hover:bg-red-900/40 whitespace-nowrap';
|
||||
return '<div class="flex items-center gap-3 text-sm text-red-600 dark:text-red-400">' +
|
||||
'<span>' + text + '</span>' +
|
||||
'<button type="button" onclick="ramSyncCounter(' + idx + ')" class="' + btnCls + '">' +
|
||||
'<button type="button" onclick="ramSyncCounter(' + idx + ', \'' + v + '\')" class="' + btnCls + '">' +
|
||||
esc(t('Sync counter')) + '</button>' +
|
||||
'<button type="button" onclick="ramGoToPreset(' + idx + ')" class="' + btnCls + '">' +
|
||||
esc(t('Go to preset')) + '</button></div>';
|
||||
@@ -10908,9 +10939,10 @@ function ramCntrLowHtml(presetIdx) {
|
||||
|
||||
// Show the low-counter notice in the RAM result area (and, when the operation
|
||||
// modal is still open, in its detail block).
|
||||
function ramShowCntrLow(presetIdx) {
|
||||
const idx = presetIdx === undefined ? ramCntrLowPresetIdx() : presetIdx;
|
||||
const html = ramCntrLowHtml(idx);
|
||||
function ramShowCntrLow(presetIdx, view) {
|
||||
const v = view === undefined ? ramCntrLowView() : view;
|
||||
const idx = presetIdx === undefined ? ramCntrLowPresetIdx(v) : presetIdx;
|
||||
const html = ramCntrLowHtml(idx, v);
|
||||
const el = document.getElementById('ram-cntr-low');
|
||||
if (el && el.classList) {
|
||||
el.innerHTML = html;
|
||||
@@ -10927,21 +10959,24 @@ function ramShowCntrLow(presetIdx) {
|
||||
// +4, ...) until the card accepts one, stops at a 32-bit ceiling and persists
|
||||
// accepted+1. A rejected attempt changes nothing on the card, so only the
|
||||
// accepted packet moves its counter (the card never reports its value).
|
||||
async function ramSyncCounter(presetIdx) {
|
||||
async function ramSyncCounter(presetIdx, view) {
|
||||
const i = Number(presetIdx);
|
||||
const preset = (Number.isInteger(i) && i >= 0 && cards[i]) ? cards[i] : null;
|
||||
const box = document.getElementById('ram-cntr-low');
|
||||
if (!preset) { alert(t('No card preset selected')); return; }
|
||||
// The keyset the failed operation used: the RAM form's selector, else the
|
||||
// SCP80 form's (both hold the keyset number as their option value).
|
||||
// The keyset the failed operation used: the originating view's selector
|
||||
// first, the other view's as the fallback (both hold the keyset number as
|
||||
// their option value).
|
||||
const kvnOf = id => {
|
||||
const sel = document.getElementById(id);
|
||||
const v = sel && sel.value !== '' ? parseInt(sel.value, 10) : NaN;
|
||||
return Number.isInteger(v) && v > 0 ? v : null;
|
||||
};
|
||||
const own = view === 'sp' ? 'sp-keyset-sel' : 'ram-keyset-sel';
|
||||
const other = own === 'ram-keyset-sel' ? 'sp-keyset-sel' : 'ram-keyset-sel';
|
||||
const val = id => (document.getElementById(id) || {}).value || '';
|
||||
const body = { preset_id: preset.id };
|
||||
const kvn = kvnOf('ram-keyset-sel') || kvnOf('sp-keyset-sel');
|
||||
const kvn = kvnOf(own) || kvnOf(other);
|
||||
if (kvn) body.kvn = kvn;
|
||||
const tar = val('sp-tar').replace(/[^0-9a-fA-F]/g, '').toUpperCase();
|
||||
if (tar) body.tar = tar;
|
||||
@@ -10955,6 +10990,8 @@ async function ramSyncCounter(presetIdx) {
|
||||
const data = await pysimFetch('/api/counter-probe', body);
|
||||
if (data.success) {
|
||||
await cardsFetch();
|
||||
// the keyset selects' labels carry the counter - refresh them too
|
||||
spKeysetSync();
|
||||
const cntrEl = document.getElementById('sp-cntr');
|
||||
if (cntrEl && data.stored_cntr) cntrEl.value = data.stored_cntr;
|
||||
const msg = t('Counter synced') + ': ' + esc(data.accepted_cntr) + ' \u2192 ' +
|
||||
@@ -11037,8 +11074,9 @@ function ramShowInstallResult(data, okText, capMem) {
|
||||
// low counter: not recoverable by retrying - state it and offer the
|
||||
// bounded sync probe or the preset jump (v3.6.58)
|
||||
if (spCntrLow(data)) {
|
||||
modal += ramCntrLowHtml(ramCntrLowPresetIdx());
|
||||
ramShowCntrLow();
|
||||
const v = ramCntrLowView();
|
||||
modal += ramCntrLowHtml(ramCntrLowPresetIdx(v), v);
|
||||
ramShowCntrLow(undefined, v);
|
||||
}
|
||||
ramOpModalFinish(resultEl.textContent, !!data.success, modal, true);
|
||||
}
|
||||
@@ -19644,6 +19682,7 @@ const LANG_RU = {
|
||||
'Free NV:': 'Свободно NV:',
|
||||
'ETSI release:': 'Релиз ETSI:',
|
||||
'unknown': 'неизвестно',
|
||||
'bad format': 'неверный формат',
|
||||
'Inferred from the card\'s installed uicc.toolkit package version (the same table as the CAP analysis); the expanded registry query provides the versions.': 'Определяется по версии установленного на карте пакета uicc.toolkit (та же таблица, что и в анализе CAP); версии даёт расширенный запрос реестра.',
|
||||
'Free Volatile:': 'Свободно volatile:',
|
||||
'AID:': 'AID:',
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
const CACHE = 'simple-v355';
|
||||
const CACHE = 'simple-v356';
|
||||
const URLS = [
|
||||
'index.html',
|
||||
'help.html',
|
||||
|
||||
@@ -27,7 +27,7 @@ const FNS = ['berLenStr', 'buildApdu', 'escHtml', 'esc', 'chainInit', 'chainRamB
|
||||
'stkParamsBuild', 'ramRemoteSwOk', 'spPorAccepted', 'ramIncrementCntr', 'ramDeleteFromExplorer', 'ramListingSpi2', 'ramRemoveFromExplorer', 'ramHasInstance', 'ramExpandedQueryApdu',
|
||||
'_parseRawElfEntry', '_parseRawAppEntry', 'ramParseElfStatus', 'ramParseAppStatus', 'ramParseModuleAids', 'parseTLV', '_parseE3Entry', '_parseMenuEntries',
|
||||
'ramExpandedReport', 'ramExpandedMore', 'ramExpandedTags', 'ramExpandedGroups', 'ramExpandedElfForm', 'ramElfVersionHint', 'cardEtsiRelease', 'ramChainGetResponse', 'ramDeriveElfVersions', 'ramElfAppletCandidate',
|
||||
'spCntrLow', 'ramCntrLowHtml', 'ramCntrLowPresetIdx', 'ramShowCntrLow',
|
||||
'spCntrLow', 'ramCntrLowHtml', 'ramCntrLowPresetIdx', 'ramCntrLowView', 'ramShowCntrLow',
|
||||
'spKeysetKvnOf', 'spKeysetList', 'spKeysetFor', 'spKeysetCheck',
|
||||
'spKeysetOptionsHtml', 'spPresetIdx', 'tarPresetIdx',
|
||||
'cardsKeysetsFromForm', 'spKeysetSync', 'spKeysetApply', 'spKeysetChanged',
|
||||
@@ -852,18 +852,21 @@ test('ramCntrLowHtml states the facts without a fabricated counter', () => {
|
||||
globalThis.t = s => s;
|
||||
const out = ramCntrLowHtml(2);
|
||||
const noPreset = ramCntrLowHtml(-1);
|
||||
// the notice follows the failed operation's view ('sp' -> its own keyset)
|
||||
const spView = ramCntrLowHtml(1, 'sp');
|
||||
delete globalThis.t;
|
||||
assert.ok(out.includes('Low counter \u2014 the card rejected the packet'), out);
|
||||
// the PoR's CNTR is a copy of the command's counter, never the card's: no
|
||||
// value may be shown (v3.9.x), only the verdict and the two actions
|
||||
assert.ok(!out.includes('card counter'), out);
|
||||
assert.ok(out.includes('onclick="ramSyncCounter(2)"'), out);
|
||||
assert.ok(out.includes('onclick="ramSyncCounter(2, \'ram\')"'), out);
|
||||
assert.ok(out.includes('Sync counter'), out);
|
||||
assert.ok(out.includes('onclick="ramGoToPreset(2)"'), out);
|
||||
assert.ok(out.includes('Go to preset'), out);
|
||||
// without a known preset the buttons still work (-1)
|
||||
assert.ok(noPreset.includes('onclick="ramGoToPreset(-1)"'), noPreset);
|
||||
assert.ok(noPreset.includes('onclick="ramSyncCounter(-1)"'), noPreset);
|
||||
assert.ok(noPreset.includes('onclick="ramSyncCounter(-1, \'ram\')"'), noPreset);
|
||||
assert.ok(spView.includes('onclick="ramSyncCounter(1, \'sp\')"'), spView);
|
||||
});
|
||||
|
||||
test('the explorer shows versions on app and ISD rows too', () => {
|
||||
@@ -877,6 +880,19 @@ test('the explorer shows versions on app and ISD rows too', () => {
|
||||
assert.ok(out.includes('Version: 9.3'), out);
|
||||
});
|
||||
|
||||
test('ramStepLine names a bad-format failure instead of "PoR ok"', () => {
|
||||
globalThis.t = s => s;
|
||||
const line = ramStepLine({ name: 'P2=02 (lean)', por_status: 'por_ok',
|
||||
por_error: 'bad format 02 (wrong length)', por_bad_format: '02',
|
||||
por_bad_format_name: 'wrong length' }, 0);
|
||||
const plain = ramStepLine({ name: 'LOAD', por_status: 'por_ok', por_sw: '9000' }, 1);
|
||||
delete globalThis.t;
|
||||
assert.ok(line.includes('bad format 02 (wrong length)'), line);
|
||||
assert.ok(!line.includes('PoR ok'), line);
|
||||
// a healthy step is unchanged
|
||||
assert.ok(plain.includes('9000') || plain.includes('PoR ok'), plain);
|
||||
});
|
||||
|
||||
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');
|
||||
@@ -908,9 +924,11 @@ test('every SCP80/RAM flow checks the low counter and stops', () => {
|
||||
// preset, the keyset the failed operation used and its SPI/TAR
|
||||
const sync = extractFunc(html, 'ramSyncCounter');
|
||||
assert.ok(sync.includes("'/api/counter-probe'"), sync);
|
||||
assert.ok(sync.includes("kvnOf('ram-keyset-sel')"), sync);
|
||||
assert.ok(sync.includes("kvnOf('sp-keyset-sel')"), sync);
|
||||
assert.ok(sync.includes("own = view === 'sp'"), sync);
|
||||
assert.ok(sync.includes('kvnOf(own)'), sync);
|
||||
assert.ok(sync.includes('kvnOf(other)'), sync);
|
||||
assert.ok(sync.includes("getElementById('sp-cntr')"), sync);
|
||||
assert.ok(sync.includes('spKeysetSync()'), 'the keyset labels must refresh');
|
||||
});
|
||||
|
||||
test('the RAM form offers the command format choice', () => {
|
||||
|
||||
@@ -30,7 +30,7 @@ let code = '';
|
||||
for (const fn of ['berLenStr', 'ramDeleteApdu', 'ramIncrementCntr', 'ramSaveCntr',
|
||||
'getRamSpParams', 'spPorAccepted', 'ramRemoteSwOk', 'ramShowProgress',
|
||||
'ramHideProgress', 'ramDeleteFromExplorer',
|
||||
'spCntrLow', 'ramCntrLowHtml', 'ramCntrLowPresetIdx', 'ramShowCntrLow',
|
||||
'spCntrLow', 'ramCntrLowHtml', 'ramCntrLowPresetIdx', 'ramCntrLowView', 'ramShowCntrLow',
|
||||
'escHtml', 'esc',
|
||||
'spKeysetKvnOf', 'spKeysetList', 'spKeysetFor', 'spKeysetCheck',
|
||||
'spKeysetOptionsHtml', 'cardsKeysetRowHtml', 'cardsKeysetRowsRender',
|
||||
|
||||
Reference in New Issue
Block a user