Files
simple/frontend/tests/ram.test.js
T
catarrh 9a688207ba v3.30.4: the in-app dialogs (native alert/confirm/prompt replaced)
The native browser dialogs block the page and look foreign inside the
app.  One shared modal (#pysim-dialog-modal) with the promise-based
helpers pysimAlert/pysimConfirm/pysimPrompt replaces all 71 call sites
(60 alert, 10 confirm, 1 prompt in profilerNew); deferredPrompt.prompt()
is the PWA install prompt and is untouched.

The helpers serialize through ONE promise queue (overlapping dialogs
appear in order, no message is overwritten).  Escape cancels; Enter
accepts except with the Cancel button focused (native behaviour); key
repeats/IME are ignored; a POINTER answer within 500 ms of a dialog
opening is dropped (the buttons keep the same position, so a
double-click's second click would otherwise answer the next queued
dialog - e.g. an alert queued before a destructive confirm).  Backdrop
clicks are ignored.  The dialog is role="dialog" aria-modal="true" and
traps Tab (the handler cycles the controls the dialog shows: alert OK;
confirm Cancel/OK; prompt Cancel/input/OK), so the focus can neither
leave the modal nor reach a background control (Space would activate
it).  When the modal is absent the helpers fall back to the native
dialogs - the Node tests extract single functions without the markup.

Alerts are fire-and-forget (a notification, not a decision); the 10
confirms and the prompt are awaited, which made five functions async
(scriptsDelete, pysimCustomRemove, profilerNew, profilerDelete,
snapshotDelete).  Because the modal no longer freezes the page, the
delete flows that mutated by index after the confirmation mutate by
identity now (profilerDelete/snapshotDelete/pysimCustomRemove/
testSuiteDeleteScript/testSuiteDelete): a list reorder during the open
dialog would otherwise delete the wrong entry.

The nine dialog messages that were still raw English literals are
t()-wrapped - three of them (No secured packet to verify/send, Card
reader server is not connected) activated RU entries that had existed
unused because they were never wrapped; Import error and Snapshot save
failed are new keys.

Drive-by: the batch modal's #ram-op-batch-info used whitespace-pre-line,
which has no rule in the prebuilt style.css (the v3.30.0 stale-style
trap), so its notice never line-broke - now whitespace-pre-wrap.

Tests: a new frontend/tests/dialogs.test.js (15 tests: the modal path,
the queue, the double-answer guard, the keyboard, the grace, the trap,
the fallback, the sweep pin, the stylesheet pin) plus the async/helper
updates in 8 existing test files.  945 frontend + 952 Python;
sw cache simple-v454.
2026-10-18 00:30:06 +03:00

1799 lines
92 KiB
JavaScript

const { test } = require('node:test');
const assert = require('node:assert');
const fs = require('node:fs');
const path = require('node:path');
const html = fs.readFileSync(path.join(__dirname, '..', 'index.html'), 'utf8');
function extractFunc(src, name) {
const re = new RegExp('(?:async\\s+)?function\\s+' + name + '\\s*\\([^)]*\\)\\s*\\{');
const m = re.exec(src);
if (!m) throw new Error('function ' + name + ' not found');
let i = m.index + m[0].length - 1;
let depth = 0;
for (; i < src.length; i++) {
if (src[i] === '{') depth++;
else if (src[i] === '}') {
depth--;
if (depth === 0) break;
}
}
return src.slice(m.index, i + 1);
}
// Extract chain builder functions and dependencies
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',
'ramFormatIsExpanded', 'ramFmtLadder',
'stkParamsBuild', 'ramRemoteSwOk', 'spPorAccepted', 'ramIncrementCntr', 'ramDeleteFromExplorer', 'ramListingSpi2', 'ramRemoveFromExplorer', 'ramHasInstance', 'ramExpandedQueryApdu',
'_parseRawElfEntry', '_parseRawAppEntry', 'ramParseElfStatus', 'ramParseAppStatus', 'ramParseModuleAids', 'parseTLV', '_parseE3Entry', '_parseMenuEntries',
'ramExpandedReport', 'ramExpandedMore', 'ramPageFormats', 'ramAppsMissingDetails', 'ramExpandedTags', 'ramExpandedGroups', 'ramExpandedElfForm', 'ramElfVersionHint', 'cardEtsiRelease', 'ramChainGetResponse', 'ramDeriveElfVersions', 'ramElfAppletCandidate', 'ramElfAppMatch',
'spCntrLow', 'ramCntrLowHtml', 'ramCntrLowPresetIdx', 'ramCntrLowView', 'ramShowCntrLow',
'ramCntrLowBox', 'ramCntrLowClear',
'spKeysetKvnOf', 'spKeysetList', 'spKeysetFor', 'spKeysetCheck',
'spTarOptionsHtml', 'spTarRender', 'spSpi1Seed', 'spTarKeysetGuard', 'cardsTarKeysetCell', 'cardsTarKeysetFromForm', 'cardsTarKeysetRefresh', 'cardsTarRowsRender', 'cardsTarRowHtml', 'cardsTarsFromTable', 'cardsFreeTarsFromForm', 'spPresetIdx', 'tarPresetIdx',
'cardsKeysetsFromForm', 'spKeysetSync', 'spKeysetApply',
'ramPresetIdx', 'spKeysetGuard', 'ramKeysetGuard',
'ramCardIdxAfterRemove', 'ramClearResults', 'ramHideProgress', 'ramIsBatchOp', 'ramOpChanged', 'ramRender', 'ramApplyCard', 'ramExecute', 'decodePrivileges', 'ramActionBtn', 'ramCapToolkitMode', 'ramOpProgressText',
'jcAidNorm', 'jcAidName', 'jcAidSuffix', 'jcAidHtml'];
let code = '';
for (const f of FNS) {
code += extractFunc(html, f) + '\n';
}
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\};/);
if (an) code += an[0].replace(/^const /, 'var ') + '\n';
const ar = html.match(/const JC_AID_RIDS = \{[\s\S]*?\n\};/);
if (ar) code += ar[0].replace(/^const /, 'var ') + '\n';
const et = html.match(/const ETSI_TOOLKIT_RELEASE = \{[\s\S]*?\n\};/);
if (et) code += et[0].replace(/^const /, 'var ') + '\n';
const ua = html.match(/const UICC_TOOLKIT_AID = '[^']*';/);
if (ua) code += ua[0].replace(/^const /, 'var ') + '\n';
globalThis.spPresetWarningRender = () => {};
globalThis.pysimApplyAvailability = () => {};
eval(code);
code += 'var _ramCardIdx = null;\nvar _ramOpLast = null;\nvar _ramExplorerData = null;\n';
eval(code);
// The dialog sweep replaced the native alert/confirm with the helpers: route
// them to the native stubs each test installs on globalThis.
globalThis.pysimAlert = m => globalThis.alert(m);
globalThis.pysimConfirm = async m => globalThis.confirm(m);
globalThis.pysimPrompt = async (m, v) => globalThis.prompt(m, v);
const els = {};
const doc = { getElementById: (id) => { if (!els[id]) els[id] = {value:''}; return els[id]; } };
global.document = doc;
// ramApplyCard/ramExecute delegate to the SP-form helpers; their real
// behaviour (and the preset re-read before an operation) is covered by
// cards_counter.test.js, so keep them as global stubs here.
globalThis.cardsApply = () => {};
globalThis.spRefreshFromPreset = () => '';
function reset() { for (const id of Object.keys(els)) delete els[id]; }
function genRamResult(fields) {
const row = { cmd: fields.cmd, fields: fields };
return chainRamBuildRowHex(0, row);
}
function genRamApdu() {
return genRamResult({
cmd: 'install-install',
aid: 'A000000151000000',
elfAid: '',
modAid: '',
priv: '00',
tkEnabled: true,
tkMode: 'ea',
tkPriority: '0',
tkTimers: '0',
tkTextlen: '0',
tkMenus: '2',
tkFirstpos: '1',
tkFirstid: '01',
tkLastpos: '2',
tkLastid: '02',
tkChannels: '0',
tkMsl: '16',
tkTar: 'AF4D01',
tkAd: '',
tkServices: '0',
});
}
test('UICC toolkit nested inside EA (m=2, services 0)', () => {
const apdu = genRamApdu();
assert.ok(apdu.includes('EA13801100000002010102020002011603AF4D0100'), apdu);
});
test('UICC m=1 emits single pair', () => {
const apdu = genRamResult({
cmd: 'install-install', aid: 'A000000151000000', priv: '00',
tkEnabled: true, tkMode: 'ea', tkPriority: '0', tkTimers: '0', tkTextlen: '0',
tkMenus: '1', tkFirstpos: '1', tkFirstid: '01', tkLastpos: '0', tkLastid: '00',
tkChannels: '0', tkMsl: '16', tkTar: 'AF4D01', tkAd: '', tkServices: '0',
});
assert.ok(apdu.includes('EA11800F0000000101010002011603AF4D0100'), apdu);
});
test('UICC m=3 fills middle pair with 0000', () => {
const apdu = genRamResult({
cmd: 'install-install', aid: 'A000000151000000', priv: '00',
tkEnabled: true, tkMode: 'ea', tkPriority: '0', tkTimers: '0', tkTextlen: '0',
tkMenus: '3', tkFirstpos: '1', tkFirstid: '01', tkLastpos: '3', tkLastid: '03',
tkChannels: '0', tkMsl: '16', tkTar: 'AF4D01', tkAd: '', tkServices: '0',
});
assert.ok(apdu.includes('EA158013000000030101000003030002011603AF4D0100'), apdu);
});
test('UICC services 7 appended as final byte', () => {
const apdu = genRamResult({
cmd: 'install-install', aid: 'A000000151000000', priv: '00',
tkEnabled: true, tkMode: 'ea', tkPriority: '0', tkTimers: '0', tkTextlen: '0',
tkMenus: '2', tkFirstpos: '1', tkFirstid: '01', tkLastpos: '2', tkLastid: '02',
tkChannels: '0', tkMsl: '16', tkTar: 'AF4D01', tkAd: '', tkServices: '7',
});
assert.ok(apdu.includes('EA13801100000002010102020002011603AF4D0107'), apdu);
});
test('SIM (CA) access domain FIRST, no services byte', () => {
const apdu = genRamResult({
cmd: 'install-install', aid: 'A000000151000000', priv: '00',
tkEnabled: true, tkMode: 'ca', tkPriority: '0', tkTimers: '0', tkTextlen: '0',
tkMenus: '2', tkFirstpos: '1', tkFirstid: '01', tkLastpos: '2', tkLastid: '02',
tkChannels: '0', tkMsl: '16', tkTar: 'AF4D01', tkAd: '5A', tkServices: '0',
});
assert.ok(apdu.includes('EF14CA12015A00000002010102020002011603AF4D01'), apdu);
});
test('SIM (CA) blank access domain emits length byte 00', () => {
const apdu = genRamResult({
cmd: 'install-install', aid: 'A000000151000000', priv: '00',
tkEnabled: true, tkMode: 'ca', tkPriority: '0', tkTimers: '0', tkTextlen: '0',
tkMenus: '2', tkFirstpos: '1', tkFirstid: '01', tkLastpos: '2', tkLastid: '02',
tkChannels: '0', tkMsl: '16', tkTar: 'AF4D01', tkAd: '', tkServices: '0',
});
assert.ok(apdu.includes('EF13CA110000000002010102020002011603AF4D01'), apdu);
});
test('SIM (CA) m=3, no TAR, blank access domain', () => {
const apdu = genRamResult({
cmd: 'install-install', aid: 'A000000151000000', priv: '00',
tkEnabled: true, tkMode: 'ca', tkPriority: '0', tkTimers: '0', tkTextlen: '0',
tkMenus: '3', tkFirstpos: '1', tkFirstid: '01', tkLastpos: '3', tkLastid: '03',
tkChannels: '0', tkMsl: '16', tkTar: '', tkAd: '', tkServices: '0',
});
assert.ok(apdu.includes('EF12CA1000000000030101000003030002011600'), apdu);
});
test('UICC m=60 uses long-form BER lengths (EA 81 87 / inner 81 84)', () => {
const apdu = genRamResult({
cmd: 'install-install', aid: 'A000000151000000', priv: '00',
tkEnabled: true, tkMode: 'ea', tkPriority: '0', tkTimers: '0', tkTextlen: '0',
tkMenus: '60', tkFirstpos: '1', tkFirstid: '01', tkLastpos: '60', tkLastid: '3C',
tkChannels: '0', tkMsl: '16', tkTar: 'AF4D01', tkAd: '', tkServices: '0',
});
assert.ok(apdu.includes('EA8188808185'), apdu);
});
test('LOAD P1 fixed to 80 (last block)', () => {
const apdu = genRamResult({ cmd: 'load', data: 'AABBCC', block: '0' });
assert.ok(apdu.startsWith('80E88000'), apdu);
});
test('DELETE: P1=00, mode in P2', () => {
let apdu = genRamResult({ cmd: 'delete', aid: 'AA1902BC225501', delMode: '00' });
assert.ok(apdu.startsWith('80E40000'), apdu);
apdu = genRamResult({ cmd: 'delete', aid: 'AA1902BC225501', delMode: '80' });
assert.ok(apdu.startsWith('80E40080'), apdu);
});
test('STORE DATA ram-enc P1 values 00/40/80/C0/E0', () => {
for (const [enc, p1] of [['00','00'],['40','40'],['80','80'],['C0','C0'],['E0','E0']]) {
const apdu = genRamResult({ cmd: 'store-data', data: 'AABB', enc: enc, block: '0' });
assert.ok(apdu.startsWith('80E2' + p1 + '00'), enc + ' -> P1 ' + p1);
}
});
test('ramRenderExploreHtml puts the command format after the resource lines', () => {
global.t = s => s;
const out = ramRenderExploreHtml({ appCount: 5, freeNV: 100, freeV: 50 }, [], [], [], 'compact');
delete global.t;
const iNv = out.indexOf('Free NV:');
const iV = out.indexOf('Free Volatile:');
const iFmt = out.indexOf('RAM command format:');
assert.ok(iNv >= 0 && iV >= 0 && iFmt > iV, out);
});
test('explorer action buttons share a fixed-width column, Install is green', () => {
global.t = s => s;
const out = ramRenderExploreHtml(
{ appCount: 1, freeNV: 10, freeV: 5 },
[],
[{ aid: 'A000000151000000', lifecycle: '03', privileges: '' }],
[{ aid: 'ELF1', lifecycle: '01', version: '1.0', moduleAids: ['M1'] }]
);
delete global.t;
// every action cell is the same fixed-width column
assert.ok(out.includes('w-28 shrink-0'), out);
// rows are visible chips (background + hover) so the button belongs to its row
assert.ok(out.includes('bg-gray-50 dark:bg-slate-800/60'), out);
assert.ok(out.includes('hover:bg-gray-100 dark:hover:bg-slate-700/50'), out);
// the Install button (module without an instance) is green, not blue
const inst = /<button onclick="ramInstallFromExplorer[^>]*class="([^"]*)"/.exec(out);
assert.ok(inst, out);
assert.ok(inst[1].includes('bg-emerald-100'), inst[1]);
assert.ok(!inst[1].includes('bg-blue-'), inst[1]);
// the delete buttons keep the danger tone
const del = /<button onclick="ramDeleteFromExplorer[^>]*class="([^"]*)"/.exec(out);
assert.ok(del && del[1].includes('bg-red-100'), del && del[1]);
});
test('ramRenderExploreHtml localizes every label and button', () => {
const seen = [];
global.t = s => { seen.push(s); return 'XX' + s; };
const out = ramRenderExploreHtml(
{ appCount: 5, freeNV: 100, freeV: 50 },
[{ aid: 'A000000151000000', lifecycle: '07', privileges: '', sdAid: 'A000000151000000' }],
[{ aid: 'A1130001180001', lifecycle: '07', privileges: '80', implicitSel: '00', elfAid: 'ELF1',
menuEntries: [{ pos: '01', id: '80', state: '01' }], rawTlvs: [{ tag: '8F', value: '752B' }] }],
[{ aid: 'ELF1', lifecycle: '01', version: '1.0', moduleAids: ['M1'], sdAid: null }]
);
delete global.t;
assert.ok(out.includes('XXDelete'), out);
assert.ok(out.includes('XXDelete All'), out);
assert.ok(out.includes('XXApplications:'), out);
assert.ok(out.includes('XXFree NV:'), out);
assert.ok(out.includes('XXFree Volatile:'), out);
assert.ok(out.includes('XXAID:'), out);
assert.ok(out.includes('XXLifecycle:'), out);
assert.ok(out.includes('XXPrivileges:'), out);
assert.ok(out.includes('XXSD AID:'), out);
assert.ok(out.includes('XXImplicit sel:'), out);
assert.ok(out.includes('XXVersion:'), out);
assert.ok(seen.includes('Application / Instance AID:'));
assert.ok(seen.includes('Load File AID / Package AID:'));
assert.ok(seen.includes('Executable Module AIDs / Applet Class AIDs:'));
assert.ok(seen.includes('STK menu entry:'));
assert.ok(seen.includes('position'));
assert.ok(seen.includes('Other registry data:'));
assert.ok(!out.includes('data-l10n'), out);
// the well-known ISD AID is annotated (GP Card Spec v2.3.1 H.1.3)
assert.ok(out.includes('A000000151000000 <span class="text-gray-400 dark:text-slate-500">(GlobalPlatform Issuer Security Domain)</span>'), out);
});
test('ramRenderExploreHtml annotates standard package AIDs, vendor AIDs stay bare', () => {
global.t = s => s;
const out = ramRenderExploreHtml(null, [], [], [
{ aid: 'A0000000871005FFFFFFFF8913200000', lifecycle: '01', version: '1.0',
moduleAids: ['A000000062010101'], sdAid: 'A0000001515350' },
]);
delete global.t;
assert.ok(out.includes('(uicc.usim.toolkit)'), out);
assert.ok(out.includes('(javacard.framework.service)'), out);
assert.ok(out.includes('(GlobalPlatform RID)'), out);
});
test('ramFmtPrivileges decodes the GP privilege bytes (live ISD values)', () => {
global.t = s => s;
// GP Table 11-7: 0x9E = Security Domain + Card Lock + Card Terminate +
// Card Reset + CVM Management (the live ISD privilege byte)
assert.strictEqual(ramFmtPrivileges('9E'),
'Security Domain, Card Lock, Card Terminate, Card Reset, CVM Management');
assert.strictEqual(ramFmtPrivileges('9A'),
'Security Domain, Card Lock, Card Terminate, CVM Management');
// DAP Verification (b8+b7) vs Mandated DAP Verification (b8+b7+b1)
assert.strictEqual(ramFmtPrivileges('C0'), 'Security Domain, DAP Verification');
assert.strictEqual(ramFmtPrivileges('C1'), 'Security Domain, Mandated DAP Verification');
// byte 2 / byte 3 spot checks
assert.strictEqual(ramFmtPrivileges('0001'), 'Global Service');
assert.strictEqual(ramFmtPrivileges('000080'), 'Receipt Generation');
delete global.t;
});
test('ramOpProgressText renders the step headline', () => {
global.t = s => s;
assert.strictEqual(
ramOpProgressText({ step: 3, total: 11, name: 'LOAD (2/9)', elapsed: 4.2 }),
'Step 3/11 \u2014 LOAD (2/9) \u2014 4.2 s');
assert.strictEqual(ramOpProgressText({ step: 0, total: 0, name: '' }), 'Working...');
assert.strictEqual(ramOpProgressText(null), '');
delete global.t;
});
test('ramCapToolkitMode picks CA/EA from the CAP linked libraries', () => {
// sim.toolkit / sim.access (A0000000090003...) -> SIM Toolkit (CA field)
assert.strictEqual(ramCapToolkitMode([{ aid: 'A0000000090003FFFFFFFF8910710002' }]), 'ca');
assert.strictEqual(ramCapToolkitMode([{ aid: 'A0000000090003FFFFFFFF8910710001' }]), 'ca');
// uicc.toolkit / uicc.access (A0000000090005...) -> UICC Toolkit (EA field)
assert.strictEqual(ramCapToolkitMode([{ aid: 'A0000000090005FFFFFFFF8912000000' }]), 'ea');
assert.strictEqual(ramCapToolkitMode([{ aid: 'A0000000090005FFFFFFFF8911000000' }]), 'ea');
// uicc.usim.* (A0000000871005...) -> UA family wins over a SIM library
assert.strictEqual(ramCapToolkitMode([{ aid: 'A0000000871005FFFFFFFF8913200000' }]), 'ea');
assert.strictEqual(ramCapToolkitMode([
{ aid: 'A0000000090003FFFFFFFF8910710002' },
{ aid: 'A0000000871005FFFFFFFF8913200000' }]), 'ea');
// no toolkit/access library: leave the user's choice alone
assert.strictEqual(ramCapToolkitMode([{ aid: 'A0000000620101' }, { aid: 'A0000000620001' }]), null);
assert.strictEqual(ramCapToolkitMode([]), null);
assert.strictEqual(ramCapToolkitMode(undefined), null);
});
test('the toolkit block is an accented bordered fieldset', () => {
const m = /id="rc-toolkit-row"[^>]*class="([^"]*)"/.exec(html);
assert.ok(m, 'rc-toolkit-row not found');
assert.ok(m[1].includes('border'), m[1]);
assert.ok(m[1].includes('p-3'), m[1]);
assert.ok(/<legend[^>]*>[\s\S]*?rc-toolkit-enable[\s\S]*?<\/legend>/.test(html),
'the enable checkbox lives in the fieldset legend');
});
test('ramFmtPrivileges uses the translated (none) placeholder', () => {
global.t = s => 'XX' + s;
assert.strictEqual(ramFmtPrivileges(''), 'XX(none)');
assert.strictEqual(ramFmtPrivileges('00'), 'XX(none)');
delete global.t;
});
function fakeClassList() {
const set = new Set();
return {
add: (...cs) => cs.forEach(c => set.add(c)),
remove: (...cs) => cs.forEach(c => set.delete(c)),
contains: c => set.has(c),
toggle: (c, on) => { if (on === undefined ? !set.has(c) : on) set.add(c); else set.delete(c); },
};
}
function fakeEl(id) {
return {
id,
value: '',
innerHTML: '',
textContent: '',
classList: fakeClassList(),
options: [],
appendChild(opt) { this.options.push(opt); },
};
}
function fakeRamDocument(ids) {
const els = {};
for (const id of ids) els[id] = fakeEl(id);
const sel = els['ram-card-sel'];
if (sel) {
Object.defineProperty(sel, 'innerHTML', {
get() { return this._html || ''; },
set(v) { this._html = v; this.value = ''; },
});
}
globalThis.document = {
getElementById: id => els[id] || null,
createElement: () => fakeEl('option'),
};
return els;
}
test('ramGetStatusApdu builds the compact chain, never the expanded form', () => {
// GP 11.4.2.2: compact listings (P2.b2=0) carry the chained GET RESPONSE
assert.strictEqual(ramGetStatusApdu('80', '00'), '80F28000024F0000C0000000');
assert.strictEqual(ramGetStatusApdu('20', '01'), '80F22001024F0000C0000000');
// the expanded command format must not carry a GET RESPONSE (TS 102 226
// 5.2.1.1): the reference TCA loader sends the same query wrapped unchained
// (`AA0A2208 80F24000024F0000`), Le='00' returns the listing directly
assert.strictEqual(ramGetStatusApdu('40', '00', 'expanded'), '80F24000024F0000');
assert.strictEqual(ramGetStatusApdu('40', '01', 'expanded-ae'), '80F24001024F0000');
assert.ok(!ramGetStatusApdu('40', '00', 'expanded').includes('C0000000'),
'no GET RESPONSE in the expanded format');
// the expanded TLV form (P2=02/03) takes no GET RESPONSE and is not built:
// its GET STATUS bytes would be `80F2<P1>02 04 4F00 5C.. 00`
assert.ok(!ramGetStatusApdu('20', '01').startsWith('80F2200204'),
'expanded form must not be generated by this builder');
assert.ok(!html.includes("for (const p2Init of ['02', '00'])"),
'the P2=02 attempt must be gone');
// the ISD-only query is a single occurrence: no next-occurrence paging
assert.ok(/if \(p1 === '80'\) break;/.test(html), 'ISD next-occurrence guard missing');
});
test('ramStepLine shows the PoR verdict and the remote status word', () => {
globalThis.t = s => s;
globalThis.lookupSw = (a, b) => (a + b === '6700' ? 'Wrong length in Lc' : '');
// successful steps are compact: verdict symbol, remote SW (no name),
// size; the SMS count only for concatenated packets
const okLine = ramStepLine({ name: 'INSTALL [for load]', por_status: 'por_ok',
por_sw: '9000', sw: '9000', bytes: 50, segments: 1 }, 0);
assert.strictEqual(okLine, '\u2705 Step 1: INSTALL [for load] \u2014 9000 \u00b7 50 B');
const multi = ramStepLine({ name: 'LOAD (1/64)', por_status: 'por_ok',
por_sw: '6101', bytes: 274, segments: 3 }, 1);
assert.strictEqual(multi, '\u2705 Step 2: LOAD (1/64) \u2014 6101 \u00b7 274 B / 3 SMS');
const badLine = ramStepLine({ name: 'LOAD (1/9)', por_status: 'por_ok', por_sw: '6700',
por_error: 'remote SW 6700', sw: '9000', bytes: 274, segments: 3 }, 1);
assert.ok(badLine.startsWith('\u274c'), badLine);
assert.ok(badLine.includes('remote SW 6700 (Wrong length in Lc)'), badLine);
const porLine = ramStepLine({ name: 'LOAD', por_status: 'por_error_cntr_low' }, 2);
assert.ok(porLine.includes('PoR error cntr_low'), porLine);
const noPor = ramStepLine({ name: 'LOAD', por_status: 'no_por' }, 3);
assert.ok(noPor.startsWith('\u2705') && noPor.includes('no PoR'), noPor);
});
test('ramStepLine marks a tolerated not-found delete step (v3.29.0)', () => {
globalThis.t = s => s;
// a delete step's 'not found' (6A88/6A82) is not a failure: the object was
// already gone, the chain continued - the line says so instead of showing
// the failure style
const line = ramStepLine({ name: 'DELETE (application)', por_sw: '6A88',
por_error: 'remote SW 6A88', delete_not_found: true }, 0);
assert.strictEqual(line,
'\u2796 Step 1: DELETE (application) \u2014 6A88 (not found)');
});
test('ramStepComponents maps a LOAD step to the CAP components it covers', () => {
// RemMobileID-like layout: sizes include each component's 3-byte header
const comps = [
{ name: 'Header', size: 20 }, { name: 'Directory', size: 34 },
{ name: 'Applet', size: 15 }, { name: 'Import', size: 101 },
{ name: 'ConstantPool', size: 1121 }, { name: 'Method', size: 8506 },
];
// 240 B block 1 (0..239) completes the Import component (its end is 170)
let sc = ramStepComponents('LOAD (1/64)', comps, 240);
assert.deepStrictEqual(sc.completed, ['Header', 'Directory', 'Applet', 'Import']);
assert.deepStrictEqual(sc.names.slice(0, 2), ['Header', 'Directory']);
// 64 B blocks: 1 completes Header/Directory, 3 completes Import
assert.deepStrictEqual(ramStepComponents('LOAD (1/240)', comps, 64).completed, ['Header', 'Directory']);
sc = ramStepComponents('LOAD (2/240)', comps, 64);
assert.deepStrictEqual(sc.names, ['Applet', 'Import']);
assert.deepStrictEqual(sc.completed, ['Applet']);
sc = ramStepComponents('LOAD (3/240)', comps, 64);
assert.deepStrictEqual(sc.names, ['Import', 'ConstantPool']);
assert.deepStrictEqual(sc.completed, ['Import']);
// other step names and missing data produce nothing
assert.strictEqual(ramStepComponents('INSTALL [for load]', comps, 64), null);
assert.strictEqual(ramStepComponents('LOAD (1/64)', comps, 0), null);
assert.strictEqual(ramStepComponents('LOAD (1/64)', null, 64), null);
});
test('ramStepLine annotates a LOAD step with the completing component', () => {
globalThis.t = s => s;
globalThis.lookupSw = () => '';
const comps = [{ name: 'Header', size: 20 }, { name: 'Import', size: 101 },
{ name: 'Method', size: 8506 }];
const line = ramStepLine({ name: 'LOAD (1/64)', por_status: 'por_ok', por_sw: '9000' }, 0, comps, 240);
assert.ok(line.includes('completes Import'), line);
// block 2 of 40 B blocks (40..79) lies strictly inside Import
const inside = ramStepLine({ name: 'LOAD (2/64)', por_status: 'por_ok', por_sw: '9000' }, 1, comps, 40);
assert.ok(inside.includes('inside Import'), inside);
// without a component list the line is unchanged
const plain = ramStepLine({ name: 'LOAD (1/64)', por_status: 'por_ok', por_sw: '9000' }, 0);
assert.ok(!plain.includes('completes') && !plain.includes('inside'), plain);
});
test('ramInstallFailHint names the CAP import requirement for a rejected LOAD', () => {
globalThis.t = s => s;
const failedLoad = {
success: false, failed_step: 3,
steps: [{ name: 'INSTALL [for load]' }, { name: 'LOAD (1/240)' }, { name: 'LOAD (2/240)', por_sw: '6985' }],
};
const hint = ramInstallFailHint(failedLoad, { requires_java_card: '2.2.2' });
// v3.23.9: a rejected LOAD points at the compatibility test (the typical
// library-version gate) - the old "smaller LOAD block" advice is gone
assert.ok(hint.includes('CAP compatibility test'), hint);
assert.ok(hint.includes('(Java Card \u2265 2.2.2)'), hint);
assert.ok(!hint.includes('smaller LOAD block'), hint);
// without a CAP analysis (INSTALL [for install] ops) the level is omitted
const noMem = ramInstallFailHint(failedLoad, null);
assert.ok(noMem.includes('CAP compatibility test'), noMem);
assert.ok(!noMem.includes('Java Card'), noMem);
// an INSTALL [for load] refusal gets its own advice (a duplicate load
// file from an earlier run is the usual cause), not the LOAD hint
const installFail = ramInstallFailHint(
{ success: false, failed_step: 1,
steps: [{ name: 'INSTALL [for load]', por_sw: '6985' }] }, null);
assert.ok(installFail.includes('refused to open the load file'), installFail);
assert.ok(installFail.includes('delete it first'), installFail);
assert.ok(installFail.includes('Delete All'), installFail);
assert.ok(!installFail.includes('compatibility test'), installFail);
// a low-counter rejection is not a CAP problem: the card never executed
// the command, so the hint stays silent (the counter box is the action)
assert.strictEqual(ramInstallFailHint(
{ success: false, failed_step: 2, cntr_low: true,
steps: [{ name: 'INSTALL [for load]' }, { name: 'LOAD (1/240)' }] }, null), '');
// only a load-related step gets the hint
assert.strictEqual(ramInstallFailHint(
{ success: false, failed_step: 2, steps: [{ name: 'INSTALL [for load]' }, { name: 'INSTALL [for install]' }] }, null), '');
assert.strictEqual(ramInstallFailHint({ success: true, failed_step: 1, steps: [] }, null), '');
// with the CAP analysis the completing component of the failing block is
// named (the card check lands on the component boundary)
const comps = [{ name: 'Header', size: 20 }, { name: 'Import', size: 101 }];
const withBoundary = ramInstallFailHint(
{ success: false, failed_step: 2, load_block_size: 64,
steps: [{ name: 'INSTALL [for load]' }, { name: 'LOAD (2/240)', por_sw: '6438' }] },
{ requires_java_card: '2.2.2', components: comps });
assert.ok(withBoundary.startsWith('the failed step completes Import. '), withBoundary);
});
test('the failed install offers the one-click compatibility test', () => {
// the modal carries the button for a failed LOAD; the handler switches the
// RAM operation to the compatibility test and runs it (the CAP file input
// still holds the same file)
const install = extractFunc(html, 'ramShowInstallResult');
assert.ok(install.includes('ramRunCompatTest()'), install.slice(0, 400));
assert.ok(install.includes("t('Run the CAP compatibility test')"), install);
// a low-counter rejection keeps the counter box as the only action
assert.ok(install.includes('!spCntrLow(data)'), install);
const run = extractFunc(html, 'ramRunCompatTest');
assert.ok(run.includes("sel.value = 'compat'"), run);
assert.ok(run.includes('ramOpChanged()'), run);
assert.ok(run.includes('ramExecute()'), run);
});
test('ramOpChanged clears the executed status only on a real op change', () => {
const els = fakeRamDocument(['ram-op', 'ram-install-params', 'ram-result', 'ram-explorer', 'ram-steps', 'ram-progress']);
_ramOpLast = null;
els['ram-op'].value = 'explore';
ramOpChanged();
assert.ok(!els['ram-result'].classList.contains('hidden'));
els['ram-result'].classList.remove('hidden');
els['ram-steps'].classList.remove('hidden');
ramOpChanged();
assert.ok(!els['ram-result'].classList.contains('hidden'), 'same op must keep the result');
els['ram-op'].value = 'install-cap';
ramOpChanged();
assert.ok(els['ram-result'].classList.contains('hidden'));
assert.ok(els['ram-steps'].classList.contains('hidden'));
assert.ok(els['ram-explorer'].classList.contains('hidden'));
assert.ok(els['ram-progress'].classList.contains('hidden'));
assert.ok(!els['ram-install-params'].classList.contains('hidden'));
});
test('ramRender keeps the selected card preset across rebuilds', () => {
const els = fakeRamDocument(['ram-card-sel', 'ram-op', 'ram-install-params', 'ram-result', 'ram-explorer', 'ram-steps', 'ram-progress']);
globalThis.cards = [{ name: 'A' }, { name: 'B' }, { name: 'C' }];
_ramCardIdx = null;
_ramOpLast = 'explore';
els['ram-op'].value = 'explore';
ramRender();
assert.strictEqual(els['ram-card-sel'].value, '');
els['ram-card-sel'].value = '1';
ramRender();
assert.strictEqual(els['ram-card-sel'].value, '1');
els['ram-card-sel'].value = '';
_ramCardIdx = 2;
ramRender();
assert.strictEqual(els['ram-card-sel'].value, '2');
globalThis.cards = [{ name: 'A' }];
_ramCardIdx = 2;
ramRender();
assert.strictEqual(els['ram-card-sel'].value, '');
delete globalThis.cards;
});
test('ramApplyCard remembers a valid picked preset', () => {
globalThis.cards = [{ name: 'A' }, { name: 'B' }];
let applied = null;
globalThis.cardsApply = i => { applied = i; };
_ramCardIdx = null;
ramApplyCard('1');
assert.strictEqual(_ramCardIdx, 1);
assert.strictEqual(applied, '1');
ramApplyCard('');
assert.strictEqual(_ramCardIdx, 1, 'invalid pick must not forget the preset');
delete globalThis.cards;
globalThis.cardsApply = () => {};
delete globalThis.cards;
});
test('ramExecute commits the dropdown selection before running', async () => {
const els = fakeRamDocument(['ram-card-sel', 'ram-op', 'ram-install-params', 'ram-result', 'ram-explorer', 'ram-steps', 'ram-progress']);
globalThis.cards = [{ name: 'A' }];
globalThis.getRamSpParams = () => ({ kicKey: '11', kidKey: '22' });
let explored = false;
globalThis.ramExplore = async () => { explored = true; };
globalThis.alert = () => {};
_ramCardIdx = null;
els['ram-card-sel'].value = '0';
els['ram-op'].value = 'explore';
await ramExecute();
assert.strictEqual(_ramCardIdx, 0);
assert.ok(explored);
delete globalThis.cards;
delete globalThis.getRamSpParams;
delete globalThis.ramExplore;
delete globalThis.alert;
});
test('ramCardIdxAfterRemove keeps the remembered index aligned', () => {
assert.strictEqual(ramCardIdxAfterRemove(2, 0), 1);
assert.strictEqual(ramCardIdxAfterRemove(0, 0), null);
assert.strictEqual(ramCardIdxAfterRemove(0, 2), 0);
assert.strictEqual(ramCardIdxAfterRemove(null, 1), null);
});
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),
'80F2' + p1 + '020C4F005C084F9F70C5C4CCCEEA00');
assert.strictEqual(ramExpandedQueryApdu(p1, 'tags'), ramExpandedQueryApdu(p1));
}
// 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');
// 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');
// 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']);
// 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', () => {
// 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('ramDeriveElfVersions falls back to the package module AIDs', () => {
// live 2026-10-02 (test-eSIM): the card returns C4 = the applet's own AID,
// so the C4 rule matches nothing - the package's module AIDs (the applet
// instance AIDs) are the mapping that works there
const elfs = [
{ aid: 'A1130001180001FFFFFFFF89A1003900', moduleAids: ['A1130001180001FFFFFFFF89A1003908'] },
{ aid: 'A1130001180002FFF7100E8904000200', moduleAids: ['A1130001180002FFF7100E8904000208', 'A1130001180002FFF7100E89494D4508'] },
{ aid: 'F0414C46416001', moduleAids: ['F0414C4641600101'] },
{ aid: 'A00000006203010101', moduleAids: [] },
];
const apps = [
{ aid: 'A1130001180001FFFFFFFF89A1003908', version: '0903', elfAid: 'A1130001180001FFFFFFFF89A1003908' },
{ aid: 'A1130001180002FFF7100E8904000208', version: '0101', elfAid: 'A1130001180002FFF7100E8904000208' },
{ aid: 'A1130001180002FFF7100E89494D4508', version: '0101', elfAid: 'A1130001180002FFF7100E89494D4508' },
{ aid: 'F0414C4641600101', version: '0001', elfAid: 'F0414C4641600101' },
];
ramDeriveElfVersions(elfs, apps);
assert.strictEqual(elfs[0].version, '0903');
assert.strictEqual(elfs[0].versionFrom, 'app');
assert.strictEqual(elfs[1].version, '0101');
assert.strictEqual(elfs[2].version, '0001');
// no applet instance: nothing to derive from
assert.strictEqual(elfs[3].version, undefined);
});
test('ramElfVersionHint names a package with no applet instance', () => {
globalThis.t = s => s;
const elfs = [{ aid: 'A1130001180001FFFFFFFF89A1003900', moduleAids: ['A1130001180001FFFFFFFF89A1003908'] }];
const apps = [{ aid: 'A1130001180001FFFFFFFF89A1003908', version: '0903' }];
// a versionless package with a matching applet instance needs no clause
const ok = ramElfVersionHint('notags', elfs, apps);
// ...but a package with no applet instance is called out
const orphan = ramElfVersionHint('notags', [{ aid: 'A00000006203010101', moduleAids: [] }], apps);
// the base hints stay form-aware
const base = ramElfVersionHint('tags', [], []);
delete globalThis.t;
assert.match(ok, /no-tag-list form\.$/);
assert.match(orphan, /no version to derive either\.$/);
assert.match(base, /did not return them\.$/);
});
test('ramElfAppletCandidate maps a package to its applet instance', () => {
// live 2026-09-30: the applet instance AIDs are the package's module AIDs
// (they differ from the package AID in the last byte)
const apps = [
{ aid: 'A1130001180002FFF7100E8904000208' },
{ aid: 'A1130001180002FFF7100E89494D4508' },
{ aid: 'F0414C4641600101' },
{ aid: 'A1130001180001FFFFFFFF89A1003908', version: '0903' },
];
assert.strictEqual(ramElfAppletCandidate({ aid: 'A1130001180002FFF7100E8904000200',
moduleAids: ['A1130001180002FFF7100E8904000208', 'A1130001180002FFF7100E89494D4508'] }, apps).aid,
'A1130001180002FFF7100E8904000208');
assert.strictEqual(ramElfAppletCandidate({ aid: 'F0414C46416001',
moduleAids: ['F0414C4641600101'] }, apps).aid, 'F0414C4641600101');
// an applet that already carries a version is skipped, and a package whose
// module is that applet has no other candidate
assert.strictEqual(ramElfAppletCandidate({ aid: 'A1130001180001FFFFFFFF89A1003900',
moduleAids: ['A1130001180001FFFFFFFF89A1003908'] }, apps), null);
// a package with no applet at all
assert.strictEqual(ramElfAppletCandidate({ aid: 'A00000006203010101', moduleAids: [] }, apps), null);
assert.strictEqual(ramElfAppletCandidate(null, apps), null);
// the shared-prefix rule is the fallback
assert.strictEqual(ramElfAppletCandidate({ aid: 'ABCD' }, [{ aid: 'ABCDEF' }]).aid, 'ABCDEF');
// the app's C4 (Load File AID) is the authoritative mapping when the card
// reports the real package AID
assert.strictEqual(ramElfAppletCandidate({ aid: 'A0000005591010FFFFFFFF8900000E00' },
[{ aid: 'A0000005591010FFFFFFFF8900001200', elfAid: 'A0000005591010FFFFFFFF8900000E00' }]).aid,
'A0000005591010FFFFFFFF8900001200');
});
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 <span class="text-gray-500 dark:text-slate-400">(from the app instance)</span>'), out);
assert.ok(!plain.includes('from the app instance'), plain);
});
test('the expanded formats never chain a GET RESPONSE (TCA loader form)', () => {
assert.strictEqual(ramFormatIsExpanded('expanded'), true);
assert.strictEqual(ramFormatIsExpanded('expanded-ae'), true);
assert.strictEqual(ramFormatIsExpanded('compact'), false);
assert.strictEqual(ramFormatIsExpanded('auto'), false);
assert.strictEqual(ramFormatIsExpanded(undefined), false);
const src = extractFunc(html, 'ramExplore');
// the listing pass follows the run's command format and pins it per send,
// so a chain can never ride inside an expanded packet
assert.ok(src.includes('ramFormatIsExpanded(sp.ram_format)'), 'the listing format must follow the run');
assert.ok(src.includes('ramGetStatusApdu(p1, p2, listingFmt)'), 'the builder must follow the format');
assert.ok(src.includes('ram_format: listingFmt'), 'the listing format must be pinned per send');
// the registry pass: the chained retry is compact-only, the format ladder
// covers the expanded codings, and the working format is remembered
assert.ok(src.includes('!ramFormatIsExpanded(usedFmt)'), 'the chained retry must be compact-only');
assert.ok(src.includes('ramFmtLadder(runFmt, expandedFmt)'),
'the format ladder (with its remembered format) must drive the retries');
assert.ok(src.includes('expandedFmt = fmt'), 'the working format must be remembered');
assert.ok(src.includes('fmt: out.fmt'), 'the report must carry the serving format');
});
test('ramFmtLadder orders the registry command formats cautiously', () => {
// the run's format first (null = no override), then the expanded codings,
// then compact as the last resort; the run's own format is not repeated
assert.deepStrictEqual(ramFmtLadder('compact', null), [null, 'expanded', 'expanded-ae']);
assert.deepStrictEqual(ramFmtLadder('expanded', null), [null, 'expanded-ae', 'compact']);
assert.deepStrictEqual(ramFmtLadder('expanded-ae', null), [null, 'expanded', 'compact']);
assert.deepStrictEqual(ramFmtLadder('auto', null), [null, 'expanded', 'expanded-ae', 'compact']);
// a format that already served data is tried first and never repeated
assert.deepStrictEqual(ramFmtLadder('compact', 'expanded'), ['expanded', 'expanded-ae']);
assert.deepStrictEqual(ramFmtLadder('expanded-ae', 'expanded-ae'), ['expanded-ae', 'expanded', 'compact']);
});
test('cardEtsiRelease infers the card ETSI release from uicc.toolkit', () => {
const tk = v => ({ aid: 'A0000000090005FFFFFFFF8912000000', version: v });
// the CE tag is raw hex: 010C is 1.12 (ramFmtVersion, as the ELF rows show)
assert.deepStrictEqual(cardEtsiRelease([tk('010C')]),
{ release: 'REL-15/16/17', basis: 'uicc.toolkit 1.12' });
assert.deepStrictEqual(cardEtsiRelease([tk('010B')]),
{ release: 'REL-12', basis: 'uicc.toolkit 1.11' });
assert.deepStrictEqual(cardEtsiRelease([tk('0105')]),
{ release: 'REL-9', basis: 'uicc.toolkit 1.5' });
// a version outside the table: the basis is still named
assert.deepStrictEqual(cardEtsiRelease([tk('01FF')]),
{ release: '', basis: 'uicc.toolkit 1.255' });
// a value that is not 4-hex passes through ramFmtVersion unchanged
assert.deepStrictEqual(cardEtsiRelease([tk('1.12')]),
{ release: 'REL-15/16/17', basis: 'uicc.toolkit 1.12' });
// the compact listing carries no versions (the expanded CE tag does)
assert.deepStrictEqual(cardEtsiRelease([tk('')]), { release: '', basis: '' });
// no uicc.toolkit (SIM-only card): no label, like SIM-only CAPs
assert.strictEqual(cardEtsiRelease([{ aid: 'A0000000090003FFFFFFFF8910710002', version: '2.6' }]), null);
assert.strictEqual(cardEtsiRelease(null), null);
});
test('the explorer shows the inferred ETSI release', () => {
globalThis.t = s => s;
const out = ramRenderExploreHtml({ appCount: 1, freeNV: 2, freeV: 3 }, [], [],
[{ aid: 'A0000000090005FFFFFFFF8912000000', version: '010C' }], 'compact', '', '');
delete globalThis.t;
assert.ok(out.includes('ETSI release: REL-15/16/17 (uicc.toolkit 1.12)'), out);
globalThis.t = s => s;
const none = ramRenderExploreHtml({ appCount: 1, freeNV: 2, freeV: 3 }, [], [], [], 'compact', '', '');
delete globalThis.t;
assert.ok(!none.includes('ETSI release:'), none);
});
test('ramExpandedReport names the expanded serving format', () => {
globalThis.t = s => s;
const out = ramExpandedReport([
{ p1: '80', label: 'ISD', compact: false, form: 'lean', sw: '6310', entries: 3, fmt: 'expanded' },
{ p1: '40', label: 'Apps', compact: false, form: 'lean', sw: '9000', entries: 2, fmt: 'expanded-ae' },
]);
delete globalThis.t;
assert.ok(out.startsWith('Expanded registry:'), out);
assert.ok(out.includes('ISD [AA]'), out);
assert.ok(out.includes('Apps [AE 80]'), out);
});
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', 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', 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', 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([
{ 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');
// 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)');
// 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
assert.strictEqual(ramExpandedReport([
{ p1: '40', label: 'Apps', compact: true, form: 'tags', sw: '9000', entries: 9 },
{ p1: '10', label: 'ELFs', compact: true, form: null, sw: 'index out of range', transport: true },
]), 'Expanded registry: tag list + EA (Apps) \u00b7 ELFs: request error (index out of range) \u2014 compact listing used');
assert.strictEqual(ramExpandedReport([
{ p1: '10', label: 'ELFs', compact: false, form: null, sw: 'index out of range', transport: true },
]), 'Expanded registry refused by the card \u2014 compact listing only (package versions unavailable) ELFs: request error (index out of range)');
assert.strictEqual(ramExpandedReport([]), '');
delete globalThis.t;
});
test('ramExpandedElfForm names the ELF listing form for the version hint', () => {
globalThis.t = s => s;
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;
});
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('spCntrLow reads the low-counter verdict of a send response', () => {
// the server's explicit flag
assert.strictEqual(spCntrLow({ cntr_low: true }), true);
// derived from the PoR alone (a 9000/91xx ENVELOPE can carry a cntr_low PoR)
assert.strictEqual(spCntrLow({ por: { response_status: 'cntr_low', cntr: '00000000BB' } }), true);
// every other verdict (and a missing response) is not a low counter
assert.strictEqual(spCntrLow({ por: { response_status: 'por_ok', cntr: '01' } }), false);
assert.strictEqual(spCntrLow({ por: { response_status: 'cntr_high' } }), false);
assert.strictEqual(spCntrLow({}), false);
assert.strictEqual(spCntrLow(null), false);
});
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, \'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, \'ram\')"'), noPreset);
assert.ok(spView.includes('onclick="ramSyncCounter(1, \'sp\')"'), spView);
// v3.23.3: the probe button is amber, the preset jump green
const syncBtn = out.match(/<button[^>]*ramSyncCounter[^>]*>/)[0];
assert.ok(syncBtn.includes('border-amber-300'), syncBtn);
assert.ok(!syncBtn.includes('border-red-300'), syncBtn);
const goBtn = out.match(/<button[^>]*ramGoToPreset[^>]*>/)[0];
assert.ok(goBtn.includes('border-emerald-300'), goBtn);
// the notice targets the originating view's box; the probe renders the
// inline throbber (the RAM inline-progress spinner, no modal)
assert.match(extractFunc(html, 'ramShowCntrLow'), /ramCntrLowBox\(v\)/);
assert.match(extractFunc(html, 'ramSyncCounter'), /animate-spin/);
assert.match(extractFunc(html, 'ramSyncCounter'), /ramCntrLowBox\(/);
assert.match(html, /id="sp-cntr-low"/);
});
test('ramShowCntrLow targets the originating view box', () => {
// the notice must land in the pill the failed operation ran in: the
// Secured packet notice used to be written into the hidden RAM container,
// so the buttons were unreachable from the plain send (v3.23.3)
globalThis.t = s => s;
const els = fakeRamDocument(['ram-cntr-low', 'sp-cntr-low']);
ramShowCntrLow(2, 'sp');
assert.ok(els['sp-cntr-low'].innerHTML.includes("ramSyncCounter(2, 'sp')"),
els['sp-cntr-low'].innerHTML);
assert.ok(!els['sp-cntr-low'].classList.contains('hidden'));
assert.strictEqual(els['ram-cntr-low'].innerHTML, '');
assert.ok(els['ram-cntr-low'].classList.contains('hidden'));
ramShowCntrLow(1, 'ram');
assert.ok(els['ram-cntr-low'].innerHTML.includes("ramSyncCounter(1, 'ram')"),
els['ram-cntr-low'].innerHTML);
assert.ok(!els['ram-cntr-low'].classList.contains('hidden'));
assert.ok(els['sp-cntr-low'].classList.contains('hidden'));
delete globalThis.t;
});
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('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 for both P1 values and per applet AID', () => {
const src = extractFunc(html, 'ramExplore');
assert.ok(src.includes('elfTagListRetry'), 'the tag-list retry must exist');
assert.ok(src.includes("for (const p1 of ['10', '20'])"), 'the retry must walk both P1 values');
assert.ok(src.includes('expandedTried[p1]'), 'already-tried forms must be skipped');
assert.ok(src.includes("['tags', 'noea', 'lean'].filter"), 'only the tag-list forms are retried');
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');
assert.ok(src.includes('ramElfAppletCandidate(e, apps)'),
'the module->applet mapping must be used');
assert.ok(src.includes('ramChainGetResponse(apdu)'),
'the single-AID 61xx answer must be fetched with a chained GET RESPONSE');
// the per-AID budget counts queries, not list positions (a package with
// no applet instance is skipped before the cap)
assert.ok(src.includes('if (app) missing.push({ elf: e, app: app });'),
'the per-AID candidates must be filtered before the cap');
});
test('every SCP80/RAM flow checks the low counter and stops', () => {
// the Explore: the guarded sender, the loop guards and the final branch
const explore = extractFunc(html, 'ramExplore');
assert.ok(explore.includes('spCntrLow(res)'), 'the Explore must check every send');
assert.ok(explore.includes('if (cntrLow) break;'), 'the loops must stop');
assert.ok(explore.includes("ramShowCntrLow(undefined, 'ram')"),
'the verdict must be shown in the RAM view');
// the delete flow and the plain send check it too
assert.ok(extractFunc(html, 'ramDeleteFromExplorer').includes('spCntrLow(res)'),
'the delete flow must stop');
// the preset jump opens the Cards tab, selects the preset and focuses the counter
const go = extractFunc(html, 'ramGoToPreset');
assert.ok(go.includes("switchTab('cards')"), go);
assert.ok(go.includes('cardsEdit('), go);
assert.ok(go.includes("getElementById('cards-ks-' + row + '-cntr')"), go);
// the notice's sync probe posts to the bounded server endpoint with the
// preset, the operation's TAR (v3.26.0: the RAM view uses the ISD entry,
// the server resolves the keyset) and its SPI
const sync = extractFunc(html, 'ramSyncCounter');
assert.ok(sync.includes("'/api/counter-probe'"), sync);
assert.ok(sync.includes("view === 'ram' ? cardsRoleTar(preset, 'isd')"), sync);
assert.ok(sync.includes('spKeysetSync()'), 'the availability must refresh');
assert.ok(sync.includes('spCntrPlaceholder()'),
'the stored value must refresh the field placeholder (the field stays an override)');
assert.ok(sync.includes("ramListingSpi2('40')"),
'the RAM view must probe with the RAM listing transport (a PoR is required)');
assert.ok(sync.includes('data.counter_saved'),
'the saved-on-error case must be reported in amber');
});
test('the RAM form offers the command format choice', () => {
// auto (detect) is the default; the two expanded codings can be pinned
assert.match(html, /<select id="ram-format"[\s\S]*?<option value="auto"/,
'the RAM format selector must exist and default to auto');
for (const v of ['compact', 'expanded', 'expanded-ae']) {
assert.ok(html.includes('<option value="' + v + '"'), 'missing format ' + v);
}
assert.ok(extractFunc(html, 'getRamSpParams').includes("getElementById('ram-format')"),
'the RAM params must carry the selected format');
});
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');
// 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');
// a transport failure must not be reported as a remote status word
assert.ok(src.includes('transportError'), 'request errors are marked as such');
});
test('ramParseAppStatus parses the expanded E3 listing (vendor traces)', () => {
// Explore_NC.log, ISD: E3 { 4F AID, 9F70 LC, C5 privileges }
const isd = ramParseAppStatus('E3134F08A0000000030000009F70010FC5039AFE80');
assert.strictEqual(isd.length, 1);
assert.strictEqual(isd[0].aid, 'A000000003000000');
assert.strictEqual(isd[0].lifecycle, '0F');
assert.strictEqual(isd[0].privileges, '9AFE80');
assert.strictEqual(isd[0].type, 'app');
// Explore_ET.log, application entry: adds the ELF AID (C4) and SD AID (CC)
const apps = ramParseAppStatus('E3374F10A1130001180001FFFFFFFF89A10039089F700107C503000000C410A1130001180001FFFFFFFF89A1003900CC08A000000151000000');
assert.strictEqual(apps.length, 1);
assert.strictEqual(apps[0].aid, 'A1130001180001FFFFFFFF89A1003908');
assert.strictEqual(apps[0].elfAid, 'A1130001180001FFFFFFFF89A1003900');
assert.strictEqual(apps[0].sdAid, 'A000000151000000');
assert.strictEqual(apps[0].privileges, '000000');
});
test('ramParseElfStatus parses the expanded E3 listing (NP trace)', () => {
const elfs = ramParseElfStatus('E31B4F07A00000015153509F700101CE020100CC08A000000151000000');
assert.strictEqual(elfs.length, 1);
assert.strictEqual(elfs[0].aid, 'A0000001515350');
assert.strictEqual(elfs[0].lifecycle, '01');
assert.strictEqual(elfs[0].version, '0100');
assert.strictEqual(elfs[0].sdAid, 'A000000151000000');
assert.strictEqual(elfs[0].type, 'elf');
});
test('ramParseElfStatus lists the compact ELF and module listings (F0414C46416101)', () => {
// Exact bytes from a live RAM Explore: the P1=20 ELF page and the P1=10
// (ELF+modules) page. The old 0x10-scan walk dropped everything after the
// first entry; the deterministic AID walk lists them all.
const elfPage = '10A0000000090005FFFFFFFF8911000000010010A0000000871005FFFFFFFF8913100000010010A0000000871005FFFFFFFF8914100000010010A0000000090005FFFFFFFF8912000000010010A0000000871005FFFFFFFF8913200000010010A0000000090005FFFFFFFF8913000000010010A0000000090005FFFFFFFF8911010000010010D2760001180002FF49100A89AA060F00010010A1130001180001FFFFFFFF89A1003900010010A1130001180002FFF7100E8904000200010007F0414C464161010100';
const elfs = ramParseElfStatus(elfPage);
const f041 = elfs.find(r => r.aid === 'F0414C46416101');
assert.ok(f041, JSON.stringify(elfs.map(r => r.aid)));
assert.strictEqual(f041.lifecycle, '01');
// the 4th byte is the privileges byte (the legacy GET STATUS shape);
// the compact listing has no version - it comes from the expanded CE tag
assert.strictEqual(f041.privileges, '00');
assert.strictEqual(f041.version, undefined);
assert.ok(elfs.some(r => r.aid === 'A1130001180002FFF7100E8904000200'), 'A113 ELF missing');
assert.ok(elfs.some(r => r.aid === 'A0000000090005FFFFFFFF8912000000'), 'uicc.toolkit ELF missing');
const modulesPage = '10A1130001180001FFFFFFFF89A100390001000110A1130001180001FFFFFFFF89A100390810A1130001180002FFF7100E890400020001000210A1130001180002FFF7100E890400020810A1130001180002FFF7100E89494D450807F0414C4641610101000108F0414C4641610101';
const mods = ramParseElfStatus(modulesPage, true);
const f041Row = mods.find(r => r.aid === 'F0414C46416101');
assert.ok(f041Row, JSON.stringify(mods.map(r => r.aid)));
assert.deepStrictEqual(f041Row.moduleAids, ['F0414C4641610101']);
});
test('_parseE3Entry collects Executable Module AIDs from every 84 TLV', () => {
// GP Table 11-37: one Executable Module AID per '84' TLV (the reference
// GPPro coding), repeated per module.
const one = _parseE3Entry('4F10A1130001180001FFFFFFFF89A10039008410A1130001180001FFFFFFFF89A1003908');
assert.deepStrictEqual(one.moduleAids, ['A1130001180001FFFFFFFF89A1003908']);
const two = _parseE3Entry('4F10A1130001180002FFF7100E89040002008410A1130001180002FFF7100E89040002088410A1130001180002FFF7100E89494D4508');
assert.deepStrictEqual(two.moduleAids,
['A1130001180002FFF7100E8904000208', 'A1130001180002FFF7100E89494D4508']);
// a card concatenating 4F<len><aid> TLVs inside one 84 still parses
const concat = _parseE3Entry('4F10A1130001180002FFF7100E890400020084244F10A1130001180002FFF7100E89040002084F10A1130001180002FFF7100E89494D4508');
assert.deepStrictEqual(concat.moduleAids,
['A1130001180002FFF7100E8904000208', 'A1130001180002FFF7100E89494D4508']);
});
test('_parseE3Entry reads the SCP Registry Data menu entries (NP trace)', () => {
// Explore_NP.log app entry: ... EA 05 80 03 01 80 01 = menu entry
// position 1, identifier 0x80, enabled (TS 102 226 Tables 8.2/8.3).
const r = _parseE3Entry('4F10A1130001180001FFFFFFFF89A10039089F70020700C503000000C410A1130001180001FFFFFFFF89A1003900CE020903CC07A0000001510000EA058003018001');
assert.strictEqual(r.aid, 'A1130001180001FFFFFFFF89A1003908');
assert.strictEqual(r.lifecycle, '0700');
assert.strictEqual(r.version, '0903');
assert.strictEqual(r.sdAid, 'A0000001510000');
assert.strictEqual(r.elfAid, 'A1130001180001FFFFFFFF89A1003900');
assert.deepStrictEqual(r.menuEntries, [{ pos: '01', id: '80', state: '01' }]);
assert.strictEqual(r.rawTlvs, undefined);
// an empty EA (an ISD with no menu entries) is not an error and adds nothing
const empty = _parseE3Entry('4F08A000000151000000EA00');
assert.strictEqual(empty.menuEntries, undefined);
assert.strictEqual(empty.rawTlvs, undefined);
});
test('_parseE3Entry preserves registry data objects it does not decode', () => {
// Explore_NP.log ISD entry: the card returned 8F/90/91/92 beyond the
// requested GP tags - shown raw, never dropped.
const r = _parseE3Entry('4F07A00000015100009F70020F00C5039AFF80C407A0000001515350CE020001CC07A00000015100008F02752B9002012D910265D592020BD3');
assert.deepStrictEqual(r.rawTlvs, [
{ tag: '8F', value: '752B' }, { tag: '90', value: '012D' },
{ tag: '91', value: '65D5' }, { tag: '92', value: '0BD3' },
]);
assert.strictEqual(r.privileges, '9AFF80');
});
test('ramFmtVersion formats the 2-byte CE version as major.minor', () => {
// GP Table 11-37 Note 1: Java Card CAP major/minor attributes, in order.
assert.strictEqual(ramFmtVersion('0903'), '9.3');
assert.strictEqual(ramFmtVersion('0100'), '1.0');
assert.strictEqual(ramFmtVersion('0A0B'), '10.11');
// other lengths stay raw (the format depends on the load file format)
assert.strictEqual(ramFmtVersion('09'), '09');
assert.strictEqual(ramFmtVersion(''), '');
});
test('ramFmtLifecycle decodes per GP object kind (Tables 11-3..11-6)', () => {
// ISD inherits the card life cycle (Table 11-6)
assert.strictEqual(ramFmtLifecycle('01', 'isd'), 'OP_READY');
assert.strictEqual(ramFmtLifecycle('07', 'isd'), 'INITIALIZED');
assert.strictEqual(ramFmtLifecycle('0F', 'isd'), 'SECURED');
assert.strictEqual(ramFmtLifecycle('7F', 'isd'), 'CARD_LOCKED');
assert.strictEqual(ramFmtLifecycle('FF', 'isd'), 'TERMINATED');
// Executable Load File (Table 11-3)
assert.strictEqual(ramFmtLifecycle('01', 'elf'), 'LOADED');
// Application (Table 11-4)
assert.strictEqual(ramFmtLifecycle('03', 'app'), 'INSTALLED');
assert.strictEqual(ramFmtLifecycle('07', 'app'), 'SELECTABLE');
assert.strictEqual(ramFmtLifecycle('0F', 'app'), 'SELECTABLE (0x0F)');
assert.strictEqual(ramFmtLifecycle('83', 'app'), 'LOCKED');
// Security Domain (Table 11-5)
assert.strictEqual(ramFmtLifecycle('0F', 'sd'), 'PERSONALIZED');
assert.strictEqual(ramFmtLifecycle('8F', 'sd'), 'LOCKED');
// a card may append extra bytes (NP trace: 9F70 02 0700)
assert.strictEqual(ramFmtLifecycle('0700', 'app'), 'SELECTABLE');
assert.strictEqual(ramFmtLifecycle('0701', 'app'), 'SELECTABLE (0x01)');
// unknown values stay visible
assert.strictEqual(ramFmtLifecycle('42', 'app'), '0x42');
assert.strictEqual(ramFmtLifecycle('', 'app'), '');
});
test('ramMenuState and ramEntryKind decode menu state and SD detection', () => {
global.t = s => s;
assert.strictEqual(ramMenuState('00'), 'Disabled');
assert.strictEqual(ramMenuState('01'), 'Enabled');
assert.strictEqual(ramMenuState('02'), '0x02');
delete global.t;
// GP Table 11-7 byte 1 b8 = Security Domain privilege
assert.strictEqual(ramEntryKind({ privileges: '9AFE80' }), 'sd');
assert.strictEqual(ramEntryKind({ privileges: '04' }), 'app');
assert.strictEqual(ramEntryKind({ type: 'elf', privileges: '00' }), 'elf');
assert.strictEqual(ramEntryKind(null), 'app');
});
test('the explorer shows versions, menu entries and unknown registry data', () => {
global.t = s => s;
const out = ramRenderExploreHtml(
{ appCount: 1, freeNV: 10, freeV: 5 },
[],
[{ aid: 'A1130001180001FFFFFFFF89A1003908', lifecycle: '0700', privileges: '000000',
elfAid: 'A1130001180001FFFFFFFF89A1003900', sdAid: 'A0000001510000',
menuEntries: [{ pos: '01', id: '80', state: '01' }], rawTlvs: [{ tag: '8F', value: '752B' }] }],
[{ aid: 'A1130001180001FFFFFFFF89A1003900', lifecycle: '01', privileges: '00', version: '0309' }]
);
delete global.t;
// the app row: SELECTABLE from the 2-byte 0700, menu entry, raw TLV
assert.ok(out.includes('SELECTABLE'), out);
assert.ok(out.includes('STK menu entry: 0x80 (position 1, Enabled)'), out);
assert.ok(out.includes('Other registry data: 8F=752B'), out);
// the ELF row: LOADED, formatted version, no privileges noise for '00'
assert.ok(out.includes('LOADED'), out);
assert.ok(out.includes('Version: 3.9'), out);
const elfSection = out.slice(out.indexOf('Executable Load Files'));
assert.ok(!elfSection.includes('Privileges:'), elfSection);
});
test('ramMergeExpanded fills only what the compact listing lacks', () => {
// the compact ELF entry has no version (v3.6.51): the expanded CE version
// and SD AID merge in - this was blocked while the bogus compact "version"
// was still set
const compact = [{ type: 'elf', aid: 'A1130001180001FFFFFFFF89A1003900', lifecycle: '01', privileges: '00' }];
const expanded = [{ aid: 'A1130001180001FFFFFFFF89A1003900', version: '0309', sdAid: 'A0000001510000', lifecycle: '01' }];
ramMergeExpanded(compact, expanded);
assert.strictEqual(compact[0].version, '0309');
assert.strictEqual(compact[0].sdAid, 'A0000001510000');
// a field the compact entry already carries is never overwritten
const apps = [{ aid: 'A1', privileges: '04' }];
ramMergeExpanded(apps, [{ aid: 'A1', privileges: '9AFE80', menuEntries: [{ pos: '01', id: '80', state: '01' }] }]);
assert.strictEqual(apps[0].privileges, '04');
assert.deepStrictEqual(apps[0].menuEntries, [{ pos: '01', id: '80', state: '01' }]);
// module lists come from the expanded listing when it has them
const elfs = [{ aid: 'E1' }];
ramMergeExpanded(elfs, [{ aid: 'E1', moduleAids: ['M1'] }]);
assert.deepStrictEqual(elfs[0].moduleAids, ['M1']);
});
test('ramNvLine reports the measured NV footprint', () => {
global.t = s => s;
assert.strictEqual(ramNvLine({ nv_before: 50646, nv_after: 23400 }),
'NV used: ~27246 B (free 50646 \u2192 23400)');
assert.strictEqual(ramNvLine({ nv_before: 100, nv_after: 150 }),
'NV freed: ~50 B (free 100 \u2192 150)');
assert.strictEqual(ramNvLine({ nv_before: 100 }), '');
assert.strictEqual(ramNvLine({}), '');
assert.strictEqual(ramNvLine(null), '');
delete global.t;
});
test('ramParseAppStatus keeps 16-byte AIDs (no rawLen-1 truncation)', () => {
const page = '08D276000005AA3F010704'
+ '0FD276000005AA060200000000B000000700'
+ '0FD276000005AA060200000000B00001070010A1130001180001FFFFFFFF89A10039080700'
+ '10A1130001180002FFF7100E8904000208070010A1130001180002FFF7100E89494D45080700';
const apps = ramParseAppStatus(page);
assert.deepStrictEqual(apps.map(r => r.aid), [
'D276000005AA3F01', 'D276000005AA060200000000B00000',
'D276000005AA060200000000B00001',
'A1130001180001FFFFFFFF89A1003908', 'A1130001180002FFF7100E8904000208',
'A1130001180002FFF7100E89494D4508',
]);
assert.strictEqual(apps[3].lifecycle, '07');
});
test('ramDeleteApdu builds the GP DELETE with the 4F AID TLV (F0414C46416101)', () => {
// GP Card Spec v2.3.1 Table 11-20/23: P1=00, P2.b8 = object / object+related,
// data = '4F' AID TLV, case 3 (no Le - a trailing Le byte becomes a
// phantom command on the card's SCP80 layer, live 2026-09-28). The old
// handler called an undefined helper and no APDU was ever sent.
assert.strictEqual(ramDeleteApdu('F0414C46416101', false),
'80E40000094F07F0414C46416101');
assert.strictEqual(ramDeleteApdu('F0414C46416101', true),
'80E40080094F07F0414C46416101');
assert.strictEqual(ramDeleteApdu('A1130001180002FFF7100E8904000200', true),
'80E40080124F10A1130001180002FFF7100E8904000200');
assert.strictEqual(ramDeleteApdu('', false), '');
// the dead helper reference must not come back
assert.ok(!html.includes('_ber_len('), 'undefined _ber_len() call is back');
});
test('ramRemoteSwOk mirrors the server success set', () => {
for (const sw of ['9000', '6113', '62F1', '6310', 'CAFE']) {
assert.ok(ramRemoteSwOk(sw), sw);
}
for (const sw of ['6700', '6F00', '6A88', '', null]) {
assert.ok(!ramRemoteSwOk(sw), String(sw));
}
});
test('ramHasInstance matches instances by AID prefix', () => {
const apps = [{ aid: 'F0414C4641610101' },
{ aid: 'A1130001180001FFFFFFFF89A1003908' }];
assert.strictEqual(ramHasInstance(apps, 'f0414c46416101'), true);
assert.strictEqual(ramHasInstance(apps, 'F0414C4641610101'), true);
assert.strictEqual(ramHasInstance(apps, 'F0414C46416001'), false);
assert.strictEqual(ramHasInstance([], 'F0414C46416101'), false);
});
test('the Explore result offers Install / Make selectable actions', () => {
global.t = s => s;
const out = ramRenderExploreHtml(
null,
[],
[{ aid: 'A000000151000000', lifecycle: '03', privileges: '' }],
[{ aid: 'F0414C46416101', lifecycle: '01', version: '0.0',
moduleAids: ['F0414C4641610101'] }]
);
// the module has no instance yet -> Install; the INSTALLED instance -> Make selectable
assert.ok(out.includes("ramInstallFromExplorer('F0414C46416101','F0414C4641610101')"), out);
assert.ok(out.includes("ramMakeSelectableFromExplorer('A000000151000000')"), out);
delete global.t;
});
test('the single-APDU ops are wired', () => {
assert.ok(/value="install-app"/.test(html), 'the INSTALL [for install] op is missing');
assert.ok(/value="make-selectable"/.test(html), 'the make selectable op is missing');
assert.ok(/async function ramInstallApp\(sp, op\)/.test(html), 'ramInstallApp is missing');
assert.ok(/pysimFetch\('\/api\/ram-install-app'/.test(html), 'the endpoint call is missing');
});
test('ramRemoveFromExplorer drops the object locally (cascade drops its applets)', () => {
let renders = 0;
globalThis.ramRenderExplorer = () => { renders++; };
_ramExplorerData = {
apps: [{ aid: 'F0414C4641610101' }, { aid: 'F0414C4641610199' },
{ aid: 'A1130001180001FFFFFFFF89A1003908' }],
elfs: [{ aid: 'F0414C46416101' }, { aid: 'A1130001180001FFFFFFFF89A1003900' }],
};
assert.strictEqual(ramRemoveFromExplorer('f0414c4641610101', false), true);
assert.deepStrictEqual(_ramExplorerData.apps.map(a => a.aid),
['F0414C4641610199', 'A1130001180001FFFFFFFF89A1003908']);
assert.strictEqual(renders, 1);
assert.strictEqual(ramRemoveFromExplorer('F0414C46416101', true), true);
assert.deepStrictEqual(_ramExplorerData.elfs.map(e => e.aid),
['A1130001180001FFFFFFFF89A1003900']);
assert.deepStrictEqual(_ramExplorerData.apps.map(a => a.aid),
['A1130001180001FFFFFFFF89A1003908'], 'an unrelated applet stays');
assert.strictEqual(renders, 2);
assert.strictEqual(ramRemoveFromExplorer('DEADBEEF', false), false);
delete globalThis.ramRenderExplorer;
});
test('getRamSpParams reads the computed SPI2 byte, not the base select', () => {
const fn = extractFunc(html, 'getRamSpParams');
assert.ok(/spi2: document\.getElementById\('sp-spi2-hex'\)/.test(fn), fn);
});
test('getRamSpParams sends the preset ISD TAR and no keyset fields (v3.26.0)', () => {
// the server resolves the keyset from the preset's ISD TAR entry - the
// PWA sends only the preset id, the ISD TAR, SPI and the counter override
const fn = extractFunc(html, 'getRamSpParams');
assert.ok(fn.includes("tar: preset ? cardsRoleTar(preset, 'isd') : ''"), fn);
assert.ok(!fn.includes('kic:'), 'the keyset fields are resolved server-side');
assert.ok(!fn.includes('kicKey:'), 'the keys travel from the preset store');
});
test('ramListingSpi2 requests the SMS-submit PoR for the listing queries', () => {
// Apps (40) and ELF (20/10) listings can exceed the ENVELOPE response;
// with SPI2=0x01 the card answers actual_response_sms_submit and the data
// never arrives (the live "Partial - Apps" bug).
for (const p1 of ['40', '20', '10']) assert.strictEqual(ramListingSpi2(p1), '21', p1);
for (const p1 of ['80', '00', 'FF']) assert.strictEqual(ramListingSpi2(p1), '01', p1);
});
function stubDeleteEnv(sendResult) {
// ramShowProgress/ramHideProgress are the real extracted helpers
const els = fakeRamDocument(['ram-result', 'ram-steps', 'ram-progress', 'ram-progress-text']);
const calls = { refresh: [], saved: [], sent: [], explored: null, removed: null };
globalThis.t = s => s;
globalThis.spRefreshFromPreset = sel => { calls.refresh.push(sel); };
globalThis.getRamSpParams = () => ({ cntr: '0000000005', kicKey: 'AA', kidKey: 'BB' });
globalThis.confirm = () => true;
globalThis.alert = () => {};
globalThis.ramShowProgress = () => {}; // not part of the extracted FNS
globalThis.ramSendOta = async (apdu, sp) => {
calls.sent.push({ apdu: apdu, cntr: sp.cntr });
return sendResult;
};
globalThis.ramSaveCntr = c => { calls.saved.push(c); };
globalThis.ramExplore = async sp => { calls.explored = sp.cntr; };
return { els, calls };
}
function unstubDeleteEnv() {
for (const k of ['getRamSpParams', 'confirm', 'alert', 'ramShowProgress',
'ramSendOta', 'ramSaveCntr', 'ramExplore']) {
delete globalThis[k];
}
globalThis.spRefreshFromPreset = () => ''; // the top-level stub
}
test('ramDeleteFromExplorer refreshes the preset and drops the record locally', async () => {
const { els, calls } = stubDeleteEnv({ success: true,
por: { response_status: 'por_ok', decoded: { last_status_word: '9000' } } });
_ramExplorerData = { apps: [{ aid: 'F0414C4641610101' }], elfs: [{ aid: 'F0414C46416101' }] };
globalThis.ramRenderExplorer = () => {};
await ramDeleteFromExplorer('F0414C46416101', true);
delete globalThis.ramRenderExplorer;
assert.deepStrictEqual(calls.refresh, ['ram-card-sel'],
'the preset must be re-read before the operation');
assert.strictEqual(calls.sent.length, 1);
assert.ok(calls.sent[0].apdu.startsWith('80E40080'), calls.sent[0].apdu);
assert.strictEqual(calls.sent[0].cntr, '0000000005');
assert.deepStrictEqual(calls.saved, ['0000000006'],
'the accepted packet advances the saved counter');
assert.deepStrictEqual(_ramExplorerData.elfs, [],
'a successful cascade delete drops the package from the Explore result');
assert.deepStrictEqual(_ramExplorerData.apps, [],
'and the package applets (AID prefix) with it');
assert.strictEqual(calls.explored, null, 'no re-explore is run');
assert.strictEqual(els['ram-result'].textContent, 'OK');
unstubDeleteEnv();
});
test('ramDeleteFromExplorer leaves the counter untouched on a rejected packet', async () => {
const { calls } = stubDeleteEnv({ success: true, por: { response_status: 'cntr_low' } });
await ramDeleteFromExplorer('F0414C46416101', false);
assert.deepStrictEqual(calls.saved, [], 'a rejected packet must not advance the counter');
assert.strictEqual(calls.explored, null, 'no re-explore after a rejected delete');
unstubDeleteEnv();
});
test('ramDeleteFromExplorer advances but does not re-explore on a failed remote command', async () => {
const { calls } = stubDeleteEnv({ success: true,
por: { response_status: 'por_ok', decoded: { last_status_word: '6A88' } } });
await ramDeleteFromExplorer('F0414C46416101', false);
assert.deepStrictEqual(calls.saved, ['0000000006'],
'the card consumed the packet, so the counter still advances');
assert.strictEqual(calls.explored, null, 'a failed DELETE must not re-explore');
unstubDeleteEnv();
});
test('ramProbeParse reads AID=version lines', () => {
globalThis.t = s => s;
let r = ramProbeParse('A0000000620101=0.0\n\n# comment\n0102030405 = 1.2');
assert.deepStrictEqual(r.map, { 'A0000000620101': '0.0', '0102030405': '1.2' });
// 'all' and lower-case AIDs are accepted, normalized to upper case
r = ramProbeParse('all=0.0');
assert.deepStrictEqual(r.map, { all: '0.0' });
r = ramProbeParse('a0000000620101=1.3');
assert.deepStrictEqual(r.map, { 'A0000000620101': '1.3' });
// malformed lines and an empty field are errors
assert.ok(ramProbeParse('A0000000620101').error);
assert.ok(ramProbeParse('A0000000620101=x.y').error);
// an empty list is valid: the test then runs with the CAP's own versions
assert.deepStrictEqual(ramProbeParse(''), { map: {}, additions: [] });
assert.deepStrictEqual(ramProbeParse(' \n# only comments\n'), { map: {}, additions: [] });
// '+AID=version' appends a synthetic import (a card-capability query)
r = ramProbeParse('+A0000000090005FFFFFFFF8912000000=1.11');
assert.deepStrictEqual(r.map, {});
assert.deepStrictEqual(r.additions,
[{ aid: 'A0000000090005FFFFFFFF8912000000', version: '1.11' }]);
r = ramProbeParse('A0000000620101=0.0\n+0102030405=1.0');
assert.deepStrictEqual(r.map, { 'A0000000620101': '0.0' });
assert.strictEqual(r.additions.length, 1);
assert.ok(ramProbeParse('+all=1.0').error);
});
test('ramCompatVerdict distinguishes where the test failed', () => {
globalThis.t = s => s;
const step = (name, ok, sw) => ok
? { name: name, por_status: 'por_ok', por_sw: '9000' }
: { name: name, por_status: 'por_ok', por_sw: sw || '6438', por_error: 'remote SW ' + (sw || '6438') };
assert.strictEqual(ramCompatVerdict({ imports_ok: true, boundary_block: 3, total_blocks: 240,
steps: [step('FORMAT CHECK (compact)', true), step('INSTALL [for load]', true), step('LOAD (3/240)', true)] }),
'Import list accepted \u2014 tested through LOAD 3/240');
assert.strictEqual(ramCompatVerdict({ imports_ok: false, boundary_block: 3, total_blocks: 240,
error: 'remote SW 6438',
steps: [step('FORMAT CHECK (compact)', true), step('INSTALL [for load]', true), step('LOAD (3/240)', false)] }),
'Import list rejected \u2014 tested through LOAD 3/240 \u00b7 remote SW 6438');
// the status word name is shown when the SW map knows it (GP 6985)
assert.strictEqual(ramCompatVerdict({ imports_ok: false, boundary_block: 1, total_blocks: 12,
error: 'LOAD (1/12): remote SW 6985',
steps: [step('FORMAT CHECK (compact)', true), step('INSTALL [for load]', true), step('LOAD (1/12)', false, '6985')] }),
'Import list rejected \u2014 tested through LOAD 1/12 \u00b7 remote SW 6985 (Conditions of use not satisfied)');
// the card never answered the format probe
assert.strictEqual(ramCompatVerdict({ imports_ok: false, boundary_block: 1, total_blocks: 240,
steps: [step('FORMAT CHECK (compact)', false), step('FORMAT CHECK (expanded)', false)] }),
'The card did not answer the format probe');
// the INSTALL [for load] request itself was refused
assert.strictEqual(ramCompatVerdict({ imports_ok: false, boundary_block: 1, total_blocks: 240,
error: 'remote SW 6985',
steps: [step('FORMAT CHECK (compact)', true), step('INSTALL [for load]', false, '6985')] }),
'Load request rejected \u2014 remote SW 6985 (Conditions of use not satisfied)');
// a request error without steps
assert.strictEqual(ramCompatVerdict({ error: 'import probe: invalid version' }),
'Error: import probe: invalid version');
assert.strictEqual(ramCompatVerdict(null), '');
});
test('ramCompatProbeNote summarizes the applied probe', () => {
globalThis.t = s => s;
assert.strictEqual(ramCompatProbeNote({ probe_imports: [
{ aid: 'A1', from: '1.3', to: '0.0' }, { aid: 'A2', from: '1.4', to: '0.0' },
{ aid: 'A3', from: null, to: '1.0' }] }),
'probe: 2 imports \u2192 0.0 \u00b7 +1 appended');
assert.strictEqual(ramCompatProbeNote({ probe_imports: [
{ aid: 'A1', from: '1.3', to: '0.0' }, { aid: 'A2', from: '1.4', to: '1.0' }] }),
'probe: 2 imports \u2192 mixed');
// the unmatched AIDs are a list of their own in the modal detail, not a
// comma string in the summary line
assert.strictEqual(ramCompatProbeNote({ probe_imports: [], probe_unmatched: ['DEADBEEF'] }), '');
assert.strictEqual(ramCompatProbeNote({}), '');
});
test('ramCompatImportRows merges the CAP imports, the probe and the unmatched lines', () => {
const mem = {
imports: [
{ aid: 'A0000000620101', major: 1, minor: 3 },
{ aid: 'A0000000620102', major: 1, minor: 3 },
{ aid: 'A0000000090005FFFFFFFF8911000000', major: 1, minor: 2 },
],
};
const data = {
probe_imports: [
{ aid: 'A0000000620102', from: '1.3', to: '0.0' },
{ aid: 'A0000000620201', from: null, to: '1.0' },
],
probe_unmatched: [{ aid: 'A0000000090005FFFFFFFF8912000000', version: '0.0' }],
};
assert.deepStrictEqual(ramCompatImportRows(data, mem), [
{ aid: 'A0000000620101', capVersion: '1.3', testedVersion: '1.3', state: 'cap' },
{ aid: 'A0000000620102', capVersion: '1.3', testedVersion: '0.0', state: 'lowered' },
{ aid: 'A0000000090005FFFFFFFF8911000000', capVersion: '1.2', testedVersion: '1.2', state: 'cap' },
{ aid: 'A0000000620201', capVersion: null, testedVersion: '1.0', state: 'appended' },
{ aid: 'A0000000090005FFFFFFFF8912000000', capVersion: null, testedVersion: '0.0', state: 'unmatched' },
]);
// without the CAP analysis at hand the probe entries still produce rows
assert.deepStrictEqual(ramCompatImportRows(data, null).map(r => r.aid),
['A0000000620102', 'A0000000620201', 'A0000000090005FFFFFFFF8912000000']);
});
test('ramCompatRowsHtml lists AIDs with names, versions and probe state', () => {
globalThis.t = s => s;
const out = ramCompatRowsHtml([
{ aid: 'A0000000620101', capVersion: '1.3', testedVersion: '1.3', state: 'cap' },
{ aid: 'A0000000620102', capVersion: '1.3', testedVersion: '0.0', state: 'lowered' },
{ aid: 'A0000000620201', capVersion: null, testedVersion: '1.0', state: 'appended' },
{ aid: 'A0000000090005FFFFFFFF8912000000', capVersion: null, testedVersion: '0.0', state: 'unmatched' },
]);
delete globalThis.t;
assert.ok(out.includes('A0000000620101 <span class="text-gray-400 dark:text-slate-500">(javacard.framework)</span> \u2014 1.3'), out);
assert.ok(out.includes('1.3 \u2192 0.0 <span class="text-gray-500 dark:text-slate-400">(probe)</span>'), out);
assert.ok(out.includes('(appended \u2014 not in the CAP)'), out);
assert.ok(out.includes('(not in the CAP&#39;s import list)'), out);
assert.ok(out.includes('(uicc.toolkit)'), out);
assert.strictEqual(ramCompatRowsHtml([]), '');
});
test('ramCompatFailureHtml names the failing CAP section and the import list', () => {
globalThis.t = s => s;
const mem = {
imports: [
{ aid: 'A0000000620101', major: 1, minor: 3 },
{ aid: 'A0000000620102', major: 1, minor: 3 },
{ aid: 'A0000000871005FFFFFFFF8913200000', major: 1, minor: 9 },
{ aid: 'A0000000090005FFFFFFFF8911000000', major: 1, minor: 2 },
],
components: [
{ name: 'Header', size: 100 },
{ name: 'Directory', size: 100 },
{ name: 'Import', size: 40 },
{ name: 'Method', size: 200 },
],
code: { load_file: 440 },
};
const data = {
success: false, imports_ok: false, failed_step: 3, boundary_block: 1, total_blocks: 2,
load_block_size: 240, error: 'LOAD (1/2): remote SW 6985',
steps: [
{ name: 'FORMAT CHECK (compact)', por_status: 'por_ok', por_sw: '9000' },
{ name: 'INSTALL [for load]', por_status: 'por_ok', por_sw: '9000' },
{ name: 'LOAD (1/2)', por_status: 'por_ok', por_sw: '6985', por_error: 'remote SW 6985' },
],
probe_imports: [
{ aid: 'A0000000620102', from: '1.3', to: '0.0' },
{ aid: 'A0000000090005FFFFFFFF8911000000', from: '1.2', to: '0.0' },
{ aid: 'A0000000620201', from: null, to: '1.0' },
],
probe_unmatched: [{ aid: 'A0000000090005FFFFFFFF8912000000', version: '0.0' }],
};
const out = ramCompatFailureHtml(data, mem);
assert.ok(out.includes('Failed in: LOAD (1/2) \u2014 load-file bytes 0\u2013239 / 440 \u00b7 covers Header, Directory, Import \u00b7 completes Import'), out);
assert.ok(out.includes("The card verifies the load file&#39;s import list when the Import component is complete (JC VM spec 4.5.2)."), out);
assert.ok(out.includes('remote SW 6985 \u2014 Conditions of use not satisfied'), out);
assert.ok(out.includes('Import list under test (6):'), out);
assert.ok(out.includes('(javacard.framework)'), out);
// two imports are still at their CAP versions -> the lower-them hint
assert.ok(out.includes('lower the versions (Set all 0.0)'), out);
assert.ok(!out.includes('Every import was tested at 0.0'), out);
// a run with every import lowered: the list cannot be the cause
const allZero = Object.assign({}, data, {
probe_imports: mem.imports.map(im => ({ aid: im.aid, from: im.major + '.' + im.minor, to: '0.0' })),
probe_unmatched: [],
});
const out2 = ramCompatFailureHtml(allZero, mem);
assert.ok(out2.includes('Every import was tested at 0.0'), out2);
// a failure later in the file: the import list already passed
const later = Object.assign({}, data, {
failed_step: 4,
steps: [
{ name: 'FORMAT CHECK (compact)', por_status: 'por_ok', por_sw: '9000' },
{ name: 'INSTALL [for load]', por_status: 'por_ok', por_sw: '9000' },
{ name: 'LOAD (1/2)', por_status: 'por_ok', por_sw: '9000' },
{ name: 'LOAD (2/2)', por_status: 'por_ok', por_sw: '6581', por_error: 'remote SW 6581' },
],
});
const out3 = ramCompatFailureHtml(later, mem);
assert.ok(out3.includes('The import list already passed \u2014 the Import component completed at LOAD (1/2).'), out3);
assert.ok(out3.includes('remote SW 6581'), out3);
assert.ok(!out3.includes('4.5.2'), out3);
// a real install run (no probe): the hint points at the compatibility test
const install = { success: false, failed_step: 3, boundary_block: 1, total_blocks: 2,
load_block_size: 240, error: 'LOAD (1/2): remote SW 6985', steps: data.steps };
const out4 = ramCompatFailureHtml(install, mem);
assert.ok(out4.includes('the CAP compatibility test can confirm it'), out4);
assert.ok(!out4.includes('lower the versions (Set all 0.0)'), out4);
assert.ok(out4.includes('Import list under test (4):'), out4);
delete globalThis.t;
// not a LOAD failure at all: no block analysis
assert.strictEqual(ramCompatFailureHtml(Object.assign({}, data, {
failed_step: 1, steps: [{ name: 'FORMAT CHECK (compact)', por_status: 'por_error' }] }), mem), '');
assert.strictEqual(ramCompatFailureHtml({ success: true, imports_ok: true }, mem), '');
});
test('ramCompatDetailHtml shows the tested list on success and the analysis on failure', () => {
globalThis.t = s => s;
const mem = { imports: [{ aid: 'A0000000620101', major: 1, minor: 3 }] };
const ok = ramCompatDetailHtml({ imports_ok: true, probe_imports: [] }, mem);
assert.ok(ok.includes('Import list under test (1):'), ok);
assert.ok(ok.includes('(javacard.framework)'), ok);
assert.ok(!ok.includes('Failed in:'), ok);
const bad = ramCompatDetailHtml({ imports_ok: false, failed_step: 1, load_block_size: 240,
steps: [{ name: 'LOAD (1/1)', por_status: 'por_ok', por_sw: '6985', por_error: 'remote SW 6985' }] }, mem);
assert.ok(bad.includes('Failed in: LOAD (1/1)'), bad);
delete globalThis.t;
assert.strictEqual(ramCompatDetailHtml(null, mem), '');
});
test('the compatibility test runs in the modal like the install', () => {
const src = extractFunc(html, 'ramCompatTest');
assert.ok(src.includes('ramOpModalBegin'), 'must open the operation modal');
assert.ok(src.includes('ramOpModalFinish'), 'must finish it with the verdict');
assert.ok(src.includes('ramCompatDetailHtml'), 'the per-AID detail must reach the modal');
assert.ok(!src.includes('ramShowProgress'), 'the inline progress bar is replaced by the modal');
});
test('a failed install carries the LOAD failure analysis into the modal', () => {
const src = extractFunc(html, 'ramShowInstallResult');
assert.ok(src.includes('ramCompatDetailHtml(data, capMem'), 'the analysis must reach the modal');
assert.ok(/ramOpModalFinish\(resultEl\.textContent, !!data\.success, modal, true\)/.test(src),
'the modal content is HTML now');
assert.ok(src.includes('ramInstallFailHint'), 'the short inline hint stays');
assert.ok(src.includes('ramNvLine'), 'the NV footprint line stays');
});
test('ramExecute dispatches the compatibility test op', () => {
const src = extractFunc(html, 'ramExecute');
assert.ok(/op === 'compat'/.test(src), 'the compat op must be dispatched');
});
test('ramRenderExploreHtml counts apps and ELFs in the section headings', () => {
global.t = s => s;
const out = ramRenderExploreHtml(null,
[{ aid: 'A1', lifecycle: '07' }],
[{ aid: 'A2', lifecycle: '07' }],
[{ aid: 'E1', lifecycle: '01', moduleAids: [] }]);
delete global.t;
assert.ok(out.includes('Applications / Applet Instances (1)'), out);
assert.ok(out.includes('Executable Load Files (ELFs) / Packages (1)'), out);
// the ISD is a single occurrence (GP 11.4.2.1): no count in its heading
assert.ok(out.includes('ISD (Issuer Security Domain)'), out);
assert.ok(!out.includes('ISD (Issuer Security Domain) ('), out);
});
test('the RAM command format line explains compact vs expanded', () => {
global.t = s => s;
const out = ramRenderExploreHtml({ appCount: 1, freeNV: 1, freeV: 1 }, [], [], [], 'compact');
delete global.t;
assert.ok(out.includes('title="compact = the TS 102 226 5.2.1 command string'), out);
});
test('ramExplore runs inline, without the blocking modal', () => {
const src = extractFunc(html, 'ramExplore');
assert.ok(!/ramOpModal(Begin|Finish|Status)/.test(src),
'Explore must not use the RAM modal (installs only)');
});
test('the expanded-details checkbox is a sticky preference', () => {
const els = fakeRamDocument(['ram-expanded-details']);
const store = {};
globalThis.localStorage = {
getItem: k => (k in store ? store[k] : null),
setItem: (k, v) => { store[k] = String(v); },
};
els['ram-expanded-details'].checked = false;
ramExpandedDetailsInit();
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'); // unticking stores it
delete globalThis.localStorage;
});
test('SIM toolkit mode hides the UICC file-access row', () => {
// the file-access group (checkboxes + ADF AID) is one spanning row now:
// it must follow the CA/EA visibility (v3.6.50)
const src = extractFunc(html, 'updateRcTkMode');
assert.ok(src.includes('rc-tk-access-row'),
'the access row must follow the CA/EA visibility');
});
test('ramExpandedMore marks only an unfulfilled 6310', () => {
// no promise -> nothing to report
assert.strictEqual(ramExpandedMore(false, '9000', 1), false);
// the follow-up page was accepted and ended the listing
assert.strictEqual(ramExpandedMore(true, '9000', 2), false);
// the follow-up page was refused
assert.strictEqual(ramExpandedMore(true, '6A86', 2), true);
// the page cap stopped the paging
assert.strictEqual(ramExpandedMore(true, '6310', 8), true);
// a transport failure (no remote status word) leaves it unfulfilled
assert.strictEqual(ramExpandedMore(true, 'index out of range', 2), true);
// a probe after a '61xx' page (nothing promised) never flags the listing
assert.strictEqual(ramExpandedMore(false, '6A86', 2), false);
});
test('ramPageFormats tries the expanded codings first, compact last', () => {
// the compact command string is last: a compact page attempt can consume
// the page without delivering it (the live card's 61xx + failed chain)
assert.deepStrictEqual(ramPageFormats('compact'), ['expanded', 'expanded-ae', 'compact']);
assert.deepStrictEqual(ramPageFormats('expanded'), ['expanded', 'expanded-ae', 'compact']);
assert.deepStrictEqual(ramPageFormats('expanded-ae'), ['expanded-ae', 'expanded', 'compact']);
// the run format ('auto') is never a page format: the server's detection
// would cost an extra packet per page
assert.deepStrictEqual(ramPageFormats('auto'), ['expanded', 'expanded-ae', 'compact']);
assert.deepStrictEqual(ramPageFormats(null), ['expanded', 'expanded-ae', 'compact']);
});
test('ramAppsMissingDetails picks only the entries without expanded fields', () => {
assert.deepStrictEqual(ramAppsMissingDetails([
{ aid: 'A1', version: '1.0' },
{ aid: 'A2' },
{ aid: 'A3', sdAid: 'AA' },
{ aid: 'A4', menuEntries: [{ position: 1 }] },
{ aid: 'A5', rawTlvs: ['8F'] },
{ aid: '' },
]).map(e => e.aid), ['A2']);
assert.deepStrictEqual(ramAppsMissingDetails(null), []);
});