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(/