From e820a7ed419f9e2f35d69de2f38ffe3510b84d81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BD=D1=82=D0=BE=D0=BD=20=D0=A2=D1=80=D0=BE=D1=88?= =?UTF-8?q?=D0=B8=D0=BD?= Date: Mon, 28 Sep 2026 23:49:00 +0300 Subject: [PATCH] feat: toolkit block emphasised + auto CA/EA from the CAP (v3.6.32) The RAM install form's toolkit parameters are the make-or-break field for toolkit applets, and CA vs EA is easy to get wrong (the earlier live 6A80s). - "SIM/UICC Toolkit parameters" is now an accented bordered fieldset (blue border/legend, always visible in the cap / install / make-selectable modes) with the checkbox in the legend and the fields inside. - ramCapToolkitMode(imports) maps the CAP's linked libraries to the install parameter family: sim.* (AID tree A0000000090003FFFFFFFF89...) -> SIM Toolkit (Tag CA), uicc.* / uicc.usim.* (A0000000090005FFFFFFFF89..., A0000000871005FFFFFFFF89...) -> UICC Toolkit (Tag EA) - family-based, so access-only applets get the right field too. Applied when the block is enabled and when the CAP is swapped while it is on, with a "detected from the CAP" hint under the select; no CAP keeps the manual choice. - Rebuilt style.css for the new accent classes; tests pin the fieldset and the family mapping. 663 frontend / 504 Python green; version 3.6.32; sw simple-v305. --- frontend/index.html | 60 +++++++++++++++++++++++++++++++---- frontend/style.css | 23 ++++++++++++++ frontend/sw.js | 2 +- frontend/tests/capmem.test.js | 3 ++ frontend/tests/ram.test.js | 29 ++++++++++++++++- pyproject.toml | 2 +- pysim_simple_server/server.py | 2 +- 7 files changed, 110 insertions(+), 11 deletions(-) diff --git a/frontend/index.html b/frontend/index.html index 1fd1453..d2371af 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -751,13 +751,13 @@ -
-
-
+ -
+
@@ -1713,7 +1714,7 @@ // ===== Version ===== // Single source of truth for the PWA version: shown in the header and used // by the server version check in pysimConnect(). -const SIMPLE_VERSION = '3.6.31'; +const SIMPLE_VERSION = '3.6.32'; document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION; // ===== Tab switching ===== @@ -2545,9 +2546,52 @@ function updateRcPriv() { document.getElementById('rc-priv').value = hex; } +// The install-parameter family a CAP belongs to, from its Import component: +// sim.* (sim.toolkit / sim.access, AID tree A0000000090003FFFFFFFF89...) uses +// the CA field (SIM Toolkit); uicc.* / uicc.usim.* (uicc.toolkit / +// uicc.access / ..., AID trees A0000000090005FFFFFFFF89... and +// A0000000871005FFFFFFFF89...) uses the EA field (UICC Toolkit). Returns +// 'ca', 'ea' or null when the CAP links against neither. +function ramCapToolkitMode(imports) { + const aids = (imports || []).map(im => (im.aid || '').toUpperCase()); + if (aids.some(a => a.startsWith('A0000000090005FFFFFFFF89') || + a.startsWith('A0000000871005FFFFFFFF89'))) return 'ea'; + if (aids.some(a => a.startsWith('A0000000090003FFFFFFFF89'))) return 'ca'; + return null; +} + +// Pick the toolkit mode of the analysed RAM CAP (when its libraries decide it) +// and, when the block is in use, apply it: never make the user guess CA vs EA. +function ramApplyCapToolkitMode() { + const st = _capAnalysis.ram; + const mode = ramCapToolkitMode((st.memory && st.memory.imports) || []); + st.tk_mode = mode; + const sel = document.getElementById('rc-tk-mode'); + const hint = document.getElementById('rc-tk-mode-hint'); + if (hint) { + if (mode) { + hint.textContent = t('detected from the CAP') + ': ' + sel.options[sel.selectedIndex].text; + hint.classList.remove('hidden'); + } else { + hint.classList.add('hidden'); + } + } + const enable = document.getElementById('rc-toolkit-enable'); + if (mode && enable && enable.checked) { + sel.value = mode; + if (hint) hint.textContent = t('detected from the CAP') + ': ' + + (mode === 'ea' ? 'UICC Toolkit (Tag EA)' : 'SIM Toolkit (Tag CA)'); + updateRcTkMode(); + updateStkParamsHex(); + } +} + function updateRcToolkit() { const checked = document.getElementById('rc-toolkit-enable').checked; document.getElementById('rc-toolkit-body').classList.toggle('hidden', !checked); + // A CAP linked against sim.* / uicc.* decides the toolkit mode + const st = _capAnalysis.ram; + if (checked && st && st.tk_mode) document.getElementById('rc-tk-mode').value = st.tk_mode; updateRcTkMode(); updateStkParamsHex(); } @@ -8454,6 +8498,7 @@ async function capAnalyzeFile(file, kind) { if (resp && resp.ok) { st.status = 'ok'; st.memory = resp.memory || {}; + if (kind === 'ram') ramApplyCapToolkitMode(); } else { st.status = 'error'; st.error = (resp && resp.error) || t('Analysis failed'); @@ -16782,6 +16827,7 @@ const LANG_RU = { 'Byte 1': 'Байт 1', 'Byte 2': 'Байт 2', 'SIM/UICC Toolkit parameters': 'Параметры SIM/UICC Toolkit', + 'detected from the CAP': 'определено по CAP', 'Toolkit mode': 'Режим Toolkit', 'Max services': 'Максимум сервисов', 'Priority': 'Приоритет', diff --git a/frontend/style.css b/frontend/style.css index f54efca..50d0c68 100644 --- a/frontend/style.css +++ b/frontend/style.css @@ -1296,6 +1296,11 @@ video { border-color: rgb(251 191 36 / var(--tw-border-opacity, 1)); } +.border-blue-300 { + --tw-border-opacity: 1; + border-color: rgb(147 197 253 / var(--tw-border-opacity, 1)); +} + .border-blue-400 { --tw-border-opacity: 1; border-color: rgb(96 165 250 / var(--tw-border-opacity, 1)); @@ -1359,6 +1364,10 @@ video { background-color: rgb(219 234 254 / var(--tw-bg-opacity, 1)); } +.bg-blue-50\/50 { + background-color: rgb(239 246 255 / 0.5); +} + .bg-blue-600 { --tw-bg-opacity: 1; background-color: rgb(37 99 235 / var(--tw-bg-opacity, 1)); @@ -1693,6 +1702,11 @@ video { color: rgb(29 78 216 / var(--tw-text-opacity, 1)); } +.text-blue-800 { + --tw-text-opacity: 1; + color: rgb(30 64 175 / var(--tw-text-opacity, 1)); +} + .text-emerald-600 { --tw-text-opacity: 1; color: rgb(5 150 105 / var(--tw-text-opacity, 1)); @@ -1954,6 +1968,11 @@ video { border-color: rgb(217 119 6 / var(--tw-border-opacity, 1)); } +.dark\:border-blue-800:is(.dark *) { + --tw-border-opacity: 1; + border-color: rgb(30 64 175 / var(--tw-border-opacity, 1)); +} + .dark\:border-red-800:is(.dark *) { --tw-border-opacity: 1; border-color: rgb(153 27 27 / var(--tw-border-opacity, 1)); @@ -1987,6 +2006,10 @@ video { background-color: rgb(59 130 246 / var(--tw-bg-opacity, 1)); } +.dark\:bg-blue-900\/10:is(.dark *) { + background-color: rgb(30 58 138 / 0.1); +} + .dark\:bg-blue-900\/40:is(.dark *) { background-color: rgb(30 58 138 / 0.4); } diff --git a/frontend/sw.js b/frontend/sw.js index faef1a0..52d071e 100644 --- a/frontend/sw.js +++ b/frontend/sw.js @@ -1,4 +1,4 @@ -const CACHE = 'simple-v304'; +const CACHE = 'simple-v305'; const URLS = [ 'index.html', 'help.html', diff --git a/frontend/tests/capmem.test.js b/frontend/tests/capmem.test.js index 9178720..de37e85 100644 --- a/frontend/tests/capmem.test.js +++ b/frontend/tests/capmem.test.js @@ -41,6 +41,9 @@ eval(extractFunc(html, 'jcAidFamily')); eval(extractFunc(html, 'capMemBytes')); eval(extractFunc(html, 'capMemHtml')); eval(extractFunc(html, 'capAnalyzeFile', true)); +// The RAM CAP analysis also applies the detected toolkit mode (DOM-bound; its +// own logic is covered in ram.test.js) - stub it for these state tests. +globalThis.ramApplyCapToolkitMode = () => {}; globalThis.esc = s => s; globalThis.t = s => s; diff --git a/frontend/tests/ram.test.js b/frontend/tests/ram.test.js index 94a7bec..70d60cf 100644 --- a/frontend/tests/ram.test.js +++ b/frontend/tests/ram.test.js @@ -25,7 +25,7 @@ function extractFunc(src, name) { const FNS = ['berLenStr', 'buildApdu', 'escHtml', 'esc', 'chainInit', 'chainRamBuildRowHex', 'ramFmtLifecycle', 'ramFmtPrivileges', 'ramRenderExploreHtml', 'ramStepLine', 'ramGetStatusApdu', 'ramDeleteApdu', 'stkParamsBuild', 'ramRemoteSwOk', 'spPorAccepted', 'ramIncrementCntr', 'ramDeleteFromExplorer', 'ramListingSpi2', 'ramRemoveFromExplorer', 'ramHasInstance', 'ramExpandedQueryApdu', '_parseRawElfEntry', '_parseRawAppEntry', 'ramParseElfStatus', 'ramParseAppStatus', 'parseTLV', '_parseE3Entry', - 'ramCardIdxAfterRemove', 'ramClearResults', 'ramHideProgress', 'ramOpChanged', 'ramRender', 'ramApplyCard', 'ramExecute', 'decodePrivileges', 'ramActionBtn', + 'ramCardIdxAfterRemove', 'ramClearResults', 'ramHideProgress', 'ramOpChanged', 'ramRender', 'ramApplyCard', 'ramExecute', 'decodePrivileges', 'ramActionBtn', 'ramCapToolkitMode', 'jcAidNorm', 'jcAidName', 'jcAidSuffix', 'jcAidHtml']; let code = ''; for (const f of FNS) { @@ -274,6 +274,33 @@ test('ramFmtPrivileges decodes the GP privilege bytes (live ISD values)', () => 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(/]*>[\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)'); diff --git a/pyproject.toml b/pyproject.toml index e65f739..e7a5df6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "pysim-simple-server" -version = "3.6.31" +version = "3.6.32" description = "HTTP REST server wrapping pysim for the SIMple PWA" requires-python = ">=3.8" # pysim is a git-only dependency installed explicitly by setup.bat/setup.sh. diff --git a/pysim_simple_server/server.py b/pysim_simple_server/server.py index 294f026..7cc469e 100644 --- a/pysim_simple_server/server.py +++ b/pysim_simple_server/server.py @@ -31,7 +31,7 @@ from osmocom.tlv import BER_TLV_IE from osmocom.utils import rpad -VERSION = '3.6.31' +VERSION = '3.6.32' MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE