feat: TAR probe becomes its own SCP80 pill with a TAR checklist (v3.8.0)
The probe is not a RAM operation, so it moves out of the RAM view into a third SCP80 pill (Secured packet | RAM | TAR probe, RU "TAR-сканер"), with its own preset and key-version selectors, its own equipped-card mismatch banner and its own result line; the help gains a "3.3 TAR probe" section and the anchor map follows the pill. The TAR field is a checklist now: pre-populated with the standard allocations of TS 101 220 V18.3.0 Annex D, Table D.1 (each row carrying the document it comes from, all checked by default, Select all / Select none), plus custom TARs added through a small form (three hex bytes + an optional description, removed with a x). Custom entries live in localStorage['simple_tar_probe'] on a best-effort basis - losing them is harmless, the standard list is always there. Only the checked TARs are sent; nothing checked is refused with a message. A Python test guards the PWA's list against the server's TAR_PROBE_TARS (the endpoint keeps its own default for API callers). RAM leftovers removed (the tar-probe operation entry, the #ram-tar-rows block, the ramExecute dispatch, ramTarProbe); getRamSpParams() takes the view's selector id; tarRender/tarApplyCard/tarKeysetChanged/tarKeysetGuard mirror the RAM view; spKeysetSync and spPresetWarningRender cover the new selectors. sw cache simple-v338. Tests: 740 Node (the checklist rows, the custom round-trip, the checked-list read, the renderer, the pill wiring) and 619 Python (the list drift guard).
This commit is contained in:
@@ -22,70 +22,144 @@ function extractFunc(src, name) {
|
||||
}
|
||||
|
||||
let code = '';
|
||||
for (const fn of ['esc', 'escHtml', 'ramTarVerdictLabel', 'ramTarProbeHtml']) {
|
||||
for (const fn of ['esc', 'escHtml', 'tarProbeVerdictLabel', 'tarProbeHtml',
|
||||
'tarProbeRowsHtml', 'tarProbeCustomLoad', 'tarProbeCustomSave',
|
||||
'tarProbeAllRows', 'tarProbeChecked']) {
|
||||
code += extractFunc(html, fn) + '\n';
|
||||
}
|
||||
code += "var TAR_PROBE_TARS = JSON.parse('" +
|
||||
JSON.stringify(eval('(' + html.match(/const TAR_PROBE_TARS = \[[\s\S]*?\n\];/)[0]
|
||||
.replace('const TAR_PROBE_TARS = ', '').replace(/;$/, '') + ')')) + "');\n";
|
||||
code += "var TAR_PROBE_STORAGE = 'simple_tar_probe';\n";
|
||||
code += 'function t(s){return s;}\n';
|
||||
eval(code);
|
||||
|
||||
const DATA = {
|
||||
success: true,
|
||||
registered: 2,
|
||||
total: 4,
|
||||
final_cntr: '0000000187',
|
||||
results: [
|
||||
{ tar: '000000', label: 'Issuer Security Domain (compact)', verdict: 'registered',
|
||||
sw: '9000', por_status: 'por_ok', por_sw: '6D00', por_data: '' },
|
||||
{ tar: 'B00000', label: 'UICC shared file system RFM (compact)', verdict: 'registered',
|
||||
sw: '9000', por_status: 'por_ok', por_sw: '6B00', por_data: '3F00' },
|
||||
{ tar: 'B00001', label: 'ADF RFM (compact)', verdict: 'no_por',
|
||||
sw: '9000', por_status: 'no_por', por_sw: null, por_data: null },
|
||||
{ tar: 'B00200', label: 'RFU (control)', verdict: 'refused',
|
||||
sw: '6200', por_status: 'envelope_error', por_sw: null, por_data: null },
|
||||
],
|
||||
};
|
||||
function fakeStorage() {
|
||||
const map = {};
|
||||
globalThis.localStorage = {
|
||||
getItem: k => (k in map ? map[k] : null),
|
||||
setItem: (k, v) => { map[k] = String(v); },
|
||||
};
|
||||
return map;
|
||||
}
|
||||
|
||||
test('ramTarVerdictLabel names every verdict', () => {
|
||||
assert.strictEqual(ramTarVerdictLabel('registered'), 'registered');
|
||||
assert.strictEqual(ramTarVerdictLabel('no_answer'), 'accepted, no answer');
|
||||
assert.strictEqual(ramTarVerdictLabel('no_por'), 'accepted, no PoR');
|
||||
assert.strictEqual(ramTarVerdictLabel('refused'), 'refused');
|
||||
assert.strictEqual(ramTarVerdictLabel('something'), 'something');
|
||||
test('the standard list covers the Annex D allocations', () => {
|
||||
const tars = TAR_PROBE_TARS.map(e => e.tar);
|
||||
assert.ok(tars.length >= 10, tars.length);
|
||||
for (const want of ['000000', 'B20100', 'B00000', 'B00001', 'B00010',
|
||||
'B00120', 'B00130', 'B00140', 'B20000', 'B20200', 'B20201', 'B00200']) {
|
||||
assert.ok(tars.includes(want), want);
|
||||
}
|
||||
// every row carries the spec document it comes from
|
||||
TAR_PROBE_TARS.forEach(e => {
|
||||
assert.match(e.tar, /^[0-9A-F]{6}$/);
|
||||
assert.ok(e.label, e.tar);
|
||||
});
|
||||
});
|
||||
|
||||
test('ramTarProbeHtml renders the summary and one row per TAR', () => {
|
||||
const out = ramTarProbeHtml(DATA);
|
||||
test('tarProbeRowsHtml renders checked rows and marks custom ones', () => {
|
||||
const out = tarProbeRowsHtml([
|
||||
{ tar: '000000', label: 'Issuer Security Domain (compact)', doc: 'TS 102 226' },
|
||||
{ tar: '123456', label: 'my applet', custom: true },
|
||||
]);
|
||||
assert.match(out, /value="000000" checked/);
|
||||
assert.match(out, /title="TS 102 226"/);
|
||||
assert.match(out, /Issuer Security Domain \(compact\)/);
|
||||
assert.match(out, /value="123456" checked/);
|
||||
assert.match(out, /tarProbeRemoveCustom\('123456'\)/, 'custom rows get a remove button');
|
||||
assert.ok(!/tarProbeRemoveCustom\('000000'\)/.test(out), 'standard rows have none');
|
||||
// escaping
|
||||
const evil = tarProbeRowsHtml([{ tar: 'AA<BB', label: '<i>x', custom: true }]);
|
||||
assert.ok(!evil.includes('AA<BB'));
|
||||
assert.ok(!evil.includes('<i>x'));
|
||||
});
|
||||
|
||||
test('custom TARs round-trip through localStorage (best effort)', () => {
|
||||
fakeStorage();
|
||||
assert.deepStrictEqual(tarProbeCustomLoad(), []);
|
||||
tarProbeCustomSave([{ tar: '123456', label: 'mine' }, { tar: 'aabbcc' }]);
|
||||
const back = tarProbeCustomLoad();
|
||||
assert.deepStrictEqual(back, [{ tar: '123456', label: 'mine', custom: true },
|
||||
{ tar: 'AABBCC', label: '', custom: true }]);
|
||||
// garbage is dropped, never fatal
|
||||
localStorage.setItem('simple_tar_probe', '{not json');
|
||||
assert.deepStrictEqual(tarProbeCustomLoad(), []);
|
||||
localStorage.setItem('simple_tar_probe', JSON.stringify([{ tar: 'zz' }, { tar: 'B0B0B0' }]));
|
||||
assert.deepStrictEqual(tarProbeCustomLoad().map(e => e.tar), ['B0B0B0']);
|
||||
});
|
||||
|
||||
test('tarProbeAllRows appends the custom TARs to the standard ones', () => {
|
||||
fakeStorage();
|
||||
tarProbeCustomSave([{ tar: '123456', label: 'mine' }]);
|
||||
const rows = tarProbeAllRows();
|
||||
assert.strictEqual(rows.length, TAR_PROBE_TARS.length + 1);
|
||||
assert.strictEqual(rows[rows.length - 1].tar, '123456');
|
||||
assert.strictEqual(rows[rows.length - 1].custom, true);
|
||||
});
|
||||
|
||||
test('tarProbeChecked reads the checked boxes in list order', () => {
|
||||
const boxes = [
|
||||
{ value: '000000', checked: true },
|
||||
{ value: 'B00000', checked: false },
|
||||
{ value: 'b00120', checked: true },
|
||||
];
|
||||
globalThis.document = { querySelectorAll: () => boxes };
|
||||
assert.deepStrictEqual(tarProbeChecked(), ['000000', 'B00120']);
|
||||
boxes.forEach(b => { b.checked = false; });
|
||||
assert.deepStrictEqual(tarProbeChecked(), []);
|
||||
});
|
||||
|
||||
test('tarProbeVerdictLabel names every verdict', () => {
|
||||
assert.strictEqual(tarProbeVerdictLabel('registered'), 'registered');
|
||||
assert.strictEqual(tarProbeVerdictLabel('no_answer'), 'accepted, no answer');
|
||||
assert.strictEqual(tarProbeVerdictLabel('no_por'), 'accepted, no PoR');
|
||||
assert.strictEqual(tarProbeVerdictLabel('refused'), 'refused');
|
||||
assert.strictEqual(tarProbeVerdictLabel('something'), 'something');
|
||||
});
|
||||
|
||||
test('tarProbeHtml renders the summary and one row per TAR', () => {
|
||||
const out = tarProbeHtml({
|
||||
success: true, registered: 2, total: 4, final_cntr: '0000000187',
|
||||
results: [
|
||||
{ tar: '000000', label: 'ISD', verdict: 'registered', sw: '9000', por_sw: '6D00', por_data: '' },
|
||||
{ tar: 'B00000', label: 'RFM', verdict: 'registered', sw: '9000', por_sw: '6B00', por_data: '3F00' },
|
||||
{ tar: 'B00001', label: 'ADF', verdict: 'no_por', sw: '9000' },
|
||||
{ tar: 'B00200', label: 'RFU', verdict: 'refused', sw: '6200' },
|
||||
],
|
||||
});
|
||||
assert.match(out, /Registered applications: <b>2 \/ 4<\/b>/);
|
||||
assert.match(out, /counter 0000000187/);
|
||||
for (const r of DATA.results) {
|
||||
assert.ok(out.includes(r.tar), r.tar);
|
||||
}
|
||||
assert.ok(out.includes('6D00') && out.includes('6B00'), 'the application SWs are shown');
|
||||
assert.ok(out.includes('6200'), 'the refused ENVELOPE SW is shown');
|
||||
// the verdict colours: registered = green, no_por = amber, refused = red
|
||||
assert.match(out, /text-emerald-700 dark:text-emerald-400">registered/);
|
||||
assert.match(out, /text-amber-600 dark:text-amber-400">accepted, no PoR/);
|
||||
assert.match(out, /text-red-600 dark:text-red-400">refused/);
|
||||
// an empty payload renders nothing
|
||||
assert.strictEqual(ramTarProbeHtml(null), '');
|
||||
assert.strictEqual(ramTarProbeHtml({}), '');
|
||||
assert.ok(out.includes('6D00') && out.includes('6200'));
|
||||
assert.strictEqual(tarProbeHtml(null), '');
|
||||
});
|
||||
|
||||
test('the probe escapes what the card returned', () => {
|
||||
const out = ramTarProbeHtml({ results: [
|
||||
{ tar: '<b>x', label: '<i>y', verdict: 'registered', sw: '<', por_sw: '>', por_data: '&' },
|
||||
], registered: 1, total: 1 });
|
||||
assert.ok(!out.includes('<b>x'), 'the TAR must be escaped');
|
||||
assert.ok(!out.includes('<i>y'), 'the label must be escaped');
|
||||
});
|
||||
|
||||
test('the RAM form wires the TAR probe', () => {
|
||||
assert.match(html, /<option value="tar-probe" data-l10n="Probe TARs \(registered OTA applications\)">/);
|
||||
assert.match(html, /id="ram-tar-rows" class="hidden mb-3"/);
|
||||
assert.match(html, /id="ram-tar-apdu"[^>]*value="00A40000023F00"/);
|
||||
assert.match(html, /id="ram-tar-list"/);
|
||||
assert.ok(html.includes("setHidden('ram-tar-rows', !isProbe);"));
|
||||
assert.ok(html.includes("else if (op === 'tar-probe') await ramTarProbe(sp);"));
|
||||
test('the SCP80 tab wires the TAR probe pill', () => {
|
||||
assert.match(html, /data-scp80-sub="tar" onclick="scp80SwitchSubtab\('tar'\)" data-l10n="TAR probe"/);
|
||||
assert.match(html, /<div id="scp80-sub-tar" class="hidden">/);
|
||||
assert.ok(html.includes("document.getElementById('scp80-sub-tar').classList.toggle('hidden', name !== 'tar');"));
|
||||
assert.ok(html.includes("tar: 'tar-probe'"), 'the help anchor points at the new section');
|
||||
// its own preset + keyset selectors and the warning banner
|
||||
assert.match(html, /id="tar-card-sel" onchange="tarApplyCard\(this.value\)"/);
|
||||
assert.match(html, /id="tar-keyset-sel" onchange="tarKeysetChanged\(\)"/);
|
||||
assert.match(html, /id="tar-preset-warning"/);
|
||||
// the checklist, the add form, the APDU and the Execute button
|
||||
assert.match(html, /id="tar-list"/);
|
||||
assert.match(html, /onclick="tarProbeSelectAll\(true\)"/);
|
||||
assert.match(html, /onclick="tarProbeAddCustom\(\)"/);
|
||||
assert.match(html, /id="tar-apdu"[^>]*value="00A40000023F00"/);
|
||||
assert.match(html, /onclick="tarExecute\(\)"/);
|
||||
assert.match(html, /id="tar-result"/);
|
||||
// the run
|
||||
assert.ok(html.includes("pysimFetch('/api/tar-probe', body)"));
|
||||
assert.ok(html.includes("if (data.final_cntr) ramSaveCntr(data.final_cntr);"));
|
||||
assert.ok(html.includes('tars: tars'));
|
||||
assert.ok(html.includes("getRamSpParams('tar-card-sel')"));
|
||||
assert.ok(html.includes('if (data.final_cntr) ramSaveCntr(data.final_cntr);'));
|
||||
// the RAM-era leftovers are gone
|
||||
assert.ok(!html.includes('id="ram-tar-rows"'));
|
||||
assert.ok(!html.includes('id="ram-tar-list"'));
|
||||
assert.ok(!html.includes("op === 'tar-probe'"));
|
||||
assert.ok(!html.includes('function ramTarProbe'));
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user