ui: gate Read decoded on client-side decoder availability (v2.7.10)
Files without a client-side decoder used to fall back to a raw view when
the Decoded pill was pressed. The decoder is resolved by name/FID
(efFindDecoder) before any read, so the pill can be disabled instead.
- pysimFsHasDecoder() mirrors the decoded view's resolution (tree node
name, FID fallback).
- pysimApplyAvailability() honours a new data-needs-decoder marker:
disabled with the existing 'No decoder for this file' tooltip; the
card hint still wins when no card is equipped.
- pysimFsClickFile() re-runs the availability pass after selecting a
file; pysimFsSetMode('dec') ignores the call without a decoder.
- tests: indicator (enable/disable/tooltip precedence), fs_edit
(decoded guard, name/FID resolution); help EN/RU.
This commit is contained in:
@@ -352,7 +352,7 @@
|
||||
<h3 id="file-manager" class="text-lg font-medium mb-2">6.1 Файловый менеджер</h3>
|
||||
<p class="text-sm mb-2">Дерево файловой системы отображается слева; выбор файла открывает панель деталей справа. Элементы сгруппированы: DF выше EF, сортировка по <strong>FID</strong> или символьному <strong>имени</strong> (пиллы и кнопка <strong>«Проверить все файлы»</strong> закреплены над прокручиваемым деревом; выбор сохраняется в <code class="font-mono text-sm">localStorage</code>). При выборе файла над содержимым также показываются FID, тип файла, размер / структура записей и декодированный FCI.</p>
|
||||
<ul class="list-disc list-inside text-sm space-y-1 mb-3">
|
||||
<li><strong>Читать сырые данные / Декодировать</strong> — чтение выбранного файла в виде hex-дампа или таблицы декодированных полей (клиентские декодеры EF: IMSI, ICCID, SPN, списки PLMN, LOCI/PSLOCI/EPSLOCI, ADN/MSISDN, таблицы сервисов, SUME, …); серверный pySim JSON того же чтения остаётся доступен в свёрнутом блоке <em>pySim JSON (сервер)</em>. Подсвеченная кнопка — текущий вид; нажатие любой из них (пере)читывает файл.</li>
|
||||
<li><strong>Читать сырые данные / Декодировать</strong> — чтение выбранного файла в виде hex-дампа или таблицы декодированных полей (клиентские декодеры EF: IMSI, ICCID, SPN, списки PLMN, LOCI/PSLOCI/EPSLOCI, ADN/MSISDN, таблицы сервисов, SUME, …); серверный pySim JSON того же чтения остаётся доступен в свёрнутом блоке <em>pySim JSON (сервер)</em>. Подсвеченная кнопка — текущий вид; нажатие любой из них (пере)читывает файл. «Декодировать» недоступна, если для файла нет клиентского декодера (проверка по имени/FID до чтения).</li>
|
||||
<li><strong>Редактировать сырые данные</strong> — изменение hex-данных, <strong>Сохранить</strong> для записи (или <strong>Отмена</strong>); transparent-файлы редактируются в textarea, record-файлы — по одному полю на запись с флажками выбора. Декодированный вид доступен только для чтения — «Редактировать сырые данные» сначала переключает на hex-вид и при необходимости читает файл.</li>
|
||||
<li><strong>Проверить все файлы</strong> — обход всего дерева (включая пользовательские файлы) с пометкой каждого элемента: есть (обычный вид) или нет (красный ✗, без стрелки разворачивания); существующие пустые DF показывают <code class="font-mono text-sm">(пусто)</code>. Отображается прогресс <em>N / всего</em>, обход можно остановить; в конце — сводка «есть/нет». Файлы проверяются только при разворачивании или проверке — просмотр остаётся ленивым.</li>
|
||||
<li><strong>ADM</strong> — файлы, требующие администраторский PIN, возвращают <code class="font-mono text-sm">6982</code>/<code class="font-mono text-sm">9804</code>; если в подходящей предустановке карты (тот же ICCID) есть ключ ADM, рядом с ошибкой появляется кнопка <strong>«Проверить ADM»</strong>, а значок в заголовке (<code class="font-mono text-sm">ADM ✓/✗ ⚿</code>) становится кликабельным. Каждый неверный ключ расходует попытку: остаток показывается, а повторная попытка требует подтверждения. Заблокированный ADM здесь не восстановить — нужен ключ разблокировки карты.</li>
|
||||
|
||||
+1
-1
@@ -352,7 +352,7 @@
|
||||
<h3 id="file-manager" class="text-lg font-medium mb-2">6.1 File manager</h3>
|
||||
<p class="text-sm mb-2">The file system tree is displayed on the left; selecting a file opens its detail pane on the right. Entries are grouped with DFs above EFs and sorted by <strong>FID</strong> or symbolic <strong>Name</strong> (pills pinned above the scrolling tree together with <strong>Probe all files</strong>; the choice is remembered in <code class="font-mono text-sm">localStorage</code>). Selecting a file also shows its FID, file type, size / record layout and the decoded FCI above the content pane.</p>
|
||||
<ul class="list-disc list-inside text-sm space-y-1 mb-3">
|
||||
<li><strong>Read raw / Read decoded</strong> — read the selected file as a hex dump or as a decoded field table (client-side EF decoders: IMSI, ICCID, SPN, PLMN lists, LOCI/PSLOCI/EPSLOCI, ADN/MSISDN, service tables, SUME, …); the server-side pySim JSON of the same read stays available in the collapsed <em>pySim JSON (server)</em> disclosure. The highlighted pill is the current view; clicking either pill (re-)reads the file.</li>
|
||||
<li><strong>Read raw / Read decoded</strong> — read the selected file as a hex dump or as a decoded field table (client-side EF decoders: IMSI, ICCID, SPN, PLMN lists, LOCI/PSLOCI/EPSLOCI, ADN/MSISDN, service tables, SUME, …); the server-side pySim JSON of the same read stays available in the collapsed <em>pySim JSON (server)</em> disclosure. The highlighted pill is the current view; clicking either pill (re-)reads the file. <strong>Read decoded</strong> is disabled when the client-side decoders do not cover the file (name/FID check, before reading).</li>
|
||||
<li><strong>Edit raw</strong> — edit the raw hex data, <strong>Save</strong> to write back (or <strong>Cancel</strong>); transparent files use a textarea, record files one input per record with selection checkboxes. The decoded view is read-only — <strong>Edit raw</strong> switches to the hex view first and reads the file if needed.</li>
|
||||
<li><strong>Probe all files</strong> — walks the whole tree (including custom files) and marks every entry present (normal) or absent (red ✗, no expand arrow); empty-but-present DFs show <code class="font-mono text-sm">(empty)</code>. Shows progress <em>N / total</em>, can be stopped, and finishes with a present/absent summary. Files are only verified when expanded or probed — browsing stays lazy.</li>
|
||||
<li><strong>ADM</strong> — files that need the administrator PIN fail with <code class="font-mono text-sm">6982</code>/<code class="font-mono text-sm">9804</code>; if the matching card preset (same ICCID) carries an ADM key, a <strong>Verify ADM</strong> button appears next to the error and the header badge (<code class="font-mono text-sm">ADM ✓/✗ ⚿</code>) becomes clickable. Every wrong key consumes an attempt: the remaining attempts are shown and a retry asks for confirmation. A blocked ADM cannot be recovered here — it needs the card’s unblock key.</li>
|
||||
|
||||
+22
-3
@@ -862,7 +862,7 @@
|
||||
<div id="pysim-fs-content" class="w-full font-mono text-xs border border-gray-300 dark:border-slate-600 rounded px-2 py-1.5 bg-gray-100 dark:bg-slate-800 min-h-20 overflow-y-auto"></div>
|
||||
<div class="flex gap-2 mt-1 items-center">
|
||||
<button type="button" data-needs="card" onclick="pysimFsSetMode('raw')" id="pysim-fs-read-raw-btn" class="pysim-mode-pill px-2 py-0.5 text-xs rounded cursor-pointer bg-blue-600 text-white disabled:opacity-40 disabled:cursor-not-allowed" data-mode="raw" data-l10n="Read raw">Read raw</button>
|
||||
<button type="button" data-needs="card" onclick="pysimFsSetMode('dec')" id="pysim-fs-read-dec-btn" class="pysim-mode-pill px-2 py-0.5 text-xs rounded cursor-pointer bg-gray-200 dark:bg-slate-700 text-gray-700 dark:text-slate-300 disabled:opacity-40 disabled:cursor-not-allowed" data-mode="dec" data-l10n="Read decoded">Read decoded</button>
|
||||
<button type="button" data-needs="card" data-needs-decoder onclick="pysimFsSetMode('dec')" id="pysim-fs-read-dec-btn" class="pysim-mode-pill px-2 py-0.5 text-xs rounded cursor-pointer bg-gray-200 dark:bg-slate-700 text-gray-700 dark:text-slate-300 disabled:opacity-40 disabled:cursor-not-allowed" data-mode="dec" data-l10n="Read decoded">Read decoded</button>
|
||||
<div class="ml-auto flex gap-2">
|
||||
<button data-needs="card" onclick="pysimFsSave()" id="pysim-fs-save-btn" class="px-2 py-1 text-xs rounded bg-emerald-600 text-white hover:bg-emerald-700 hidden disabled:opacity-40 disabled:cursor-not-allowed" data-l10n="Save">Save</button>
|
||||
<button onclick="pysimFsCancel()" id="pysim-fs-cancel-btn" class="px-2 py-1 text-xs rounded bg-gray-600 text-white hover:bg-gray-700 hidden" data-l10n="Cancel">Cancel</button>
|
||||
@@ -1413,7 +1413,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 = '2.7.9';
|
||||
const SIMPLE_VERSION = '2.7.10';
|
||||
document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION;
|
||||
|
||||
// ===== Tab switching =====
|
||||
@@ -4929,8 +4929,15 @@ function pysimApplyAvailability() {
|
||||
// From-card buttons need a readable ICCID, not just a session.
|
||||
disabled = pysimControlDisabled('card', state) || !_pysimCardIccid;
|
||||
}
|
||||
let hint = disabled ? pysimNeedsHint(needs, state) : null;
|
||||
if (el.hasAttribute('data-needs-decoder') && !pysimFsHasDecoder()) {
|
||||
// Client-side decoder required (Read decoded) — the card hint wins
|
||||
// when the card itself is the blocker.
|
||||
disabled = true;
|
||||
if (!hint) hint = t('No decoder for this file');
|
||||
}
|
||||
el.disabled = disabled;
|
||||
if (disabled) el.setAttribute('title', pysimNeedsHint(needs, state));
|
||||
if (hint) el.setAttribute('title', hint);
|
||||
else el.removeAttribute('title');
|
||||
});
|
||||
pysimUpdateCardActionLabels();
|
||||
@@ -7105,6 +7112,16 @@ function pysimFsFindNode(name, root) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Decoder availability for the selected file, resolved exactly like the
|
||||
// decoded view does (name from the tree node, FID fallback) — the content is
|
||||
// not needed, so the Read decoded pill can be gated before any read.
|
||||
function pysimFsHasDecoder() {
|
||||
if (!pysimFsSelected) return false;
|
||||
const node = pysimFsFindNode(pysimFsSelected, pysimFsTreeRoot);
|
||||
const fid = (node && node.fid) || (/^[0-9a-fA-F]{4}$/.test(pysimFsSelected) ? pysimFsSelected : null);
|
||||
return !!efFindDecoder(node && node.name, fid);
|
||||
}
|
||||
|
||||
// Find a node by its canonical path ("MF/7F20/6F46", "ADF.USIM/6F07"), so
|
||||
// custom-file parents can be resolved against the loaded tree (standard DFs
|
||||
// included, at any depth). Returns null when the path is not loaded yet.
|
||||
@@ -7172,6 +7189,7 @@ async function pysimFsClickFile(name) {
|
||||
} else {
|
||||
document.getElementById('pysim-fs-detail').classList.add('hidden');
|
||||
}
|
||||
pysimApplyAvailability();
|
||||
pysimRefresh();
|
||||
}
|
||||
|
||||
@@ -7368,6 +7386,7 @@ async function pysimFsSave() {
|
||||
// they are inert: re-rendering would discard unsaved changes.
|
||||
function pysimFsSetMode(mode, skipRead) {
|
||||
if (pysimFsEditMode) return;
|
||||
if (mode === 'dec' && !pysimFsHasDecoder()) return;
|
||||
pysimFsDecodedMode = mode === 'dec';
|
||||
document.querySelectorAll('.pysim-mode-pill').forEach(el => {
|
||||
const active = el.dataset.mode === mode;
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
const CACHE = 'simple-v212';
|
||||
const CACHE = 'simple-v213';
|
||||
const URLS = [
|
||||
'index.html',
|
||||
'help.html',
|
||||
|
||||
@@ -24,9 +24,10 @@ function extractFunc(src, name) {
|
||||
let code = 'var pysimFsDecodedMode = false;\n'
|
||||
+ 'var pysimFsEditMode = false;\n'
|
||||
+ 'var pysimFsEditData = null;\n'
|
||||
+ 'var pysimFsSelected = null;\n';
|
||||
+ 'var pysimFsSelected = null;\n'
|
||||
+ 'var pysimFsTreeRoot = null;\n';
|
||||
for (const fn of ['pysimFsSetMode', 'pysimFsPillsEnabled', 'pysimFsEdit',
|
||||
'pysimFsCancel', 'pysimFsResetEdit']) {
|
||||
'pysimFsCancel', 'pysimFsResetEdit', 'pysimFsFindNode', 'pysimFsHasDecoder']) {
|
||||
code += extractFunc(html, fn) + '\n';
|
||||
}
|
||||
eval(code);
|
||||
@@ -80,6 +81,12 @@ function setup(opts) {
|
||||
pysimFsEditMode = false;
|
||||
pysimFsEditData = null;
|
||||
pysimFsSelected = 'EF.IMSI';
|
||||
pysimFsTreeRoot = { name: 'MF', fid: '3f00', children: [{ name: 'EF.IMSI', fid: '6f07' }] };
|
||||
globalThis.efFindDecoder = (name, fid) => {
|
||||
const n = (name || '').toUpperCase();
|
||||
const f = (fid || '').toLowerCase();
|
||||
return (n === 'EF.IMSI' || f === '6f07') ? { name: 'EF.IMSI', fid: '6f07' } : null;
|
||||
};
|
||||
|
||||
globalThis.document = {
|
||||
getElementById: id => id === 'pysim-fs-content' ? content : (buttons[id] || null),
|
||||
@@ -179,10 +186,36 @@ test('Reset edit re-enables the pills', () => {
|
||||
assert.ok(!h.pill('dec').classList.contains('opacity-50'));
|
||||
});
|
||||
|
||||
test('Read decoded is ignored when the file has no decoder', () => {
|
||||
const h = setup();
|
||||
globalThis.efFindDecoder = () => null;
|
||||
pysimFsSetMode('dec');
|
||||
assert.strictEqual(pysimFsDecodedMode, false);
|
||||
assert.strictEqual(h.reads(), 0);
|
||||
assert.ok(!h.pill('dec').classList.contains('bg-blue-600'));
|
||||
});
|
||||
|
||||
test('pysimFsHasDecoder resolves the selected file by name or FID', () => {
|
||||
setup();
|
||||
assert.strictEqual(pysimFsHasDecoder(), true, 'EF.IMSI is in the tree');
|
||||
// alias name: the decoder is matched by the FID fallback
|
||||
pysimFsTreeRoot = { name: 'MF', children: [{ name: 'EF.ALIAS', fid: '6f07' }] };
|
||||
pysimFsSelected = 'EF.ALIAS';
|
||||
assert.strictEqual(pysimFsHasDecoder(), true);
|
||||
// unknown file -> no decoder
|
||||
globalThis.efFindDecoder = () => null;
|
||||
assert.strictEqual(pysimFsHasDecoder(), false);
|
||||
// nothing selected -> no decoder
|
||||
pysimFsSelected = null;
|
||||
assert.strictEqual(pysimFsHasDecoder(), false);
|
||||
});
|
||||
|
||||
test('the file manager row exposes the read pills and Edit raw', () => {
|
||||
assert.ok(!html.includes('id="pysim-fs-read-btn"'), 'the redundant Read button is gone');
|
||||
assert.ok(html.includes('data-l10n="Read raw">Read raw<'));
|
||||
assert.ok(html.includes('data-l10n="Read decoded">Read decoded<'));
|
||||
assert.ok(html.includes('data-l10n="Edit raw">Edit raw<'));
|
||||
assert.ok(html.includes('id="pysim-fs-read-raw-btn"') && html.includes('data-needs="card"'));
|
||||
const dec = /<button[^>]*id="pysim-fs-read-dec-btn"[^>]*>/.exec(html);
|
||||
assert.ok(dec && dec[0].includes('data-needs-decoder'), 'the decoded pill is decoder-gated');
|
||||
});
|
||||
|
||||
@@ -49,6 +49,7 @@ function fakeEl() {
|
||||
},
|
||||
setAttribute(k, v) { this.attrs[k] = v; },
|
||||
getAttribute(k) { return this.attrs[k]; },
|
||||
hasAttribute(k) { return k in this.attrs; },
|
||||
removeAttribute(k) { delete this.attrs[k]; },
|
||||
};
|
||||
}
|
||||
@@ -225,6 +226,32 @@ test('card-iccid controls need an equipped card with a readable ICCID', () => {
|
||||
assert.strictEqual(el.disabled, true);
|
||||
});
|
||||
|
||||
test('Read decoded needs a client-side decoder for the selected file', () => {
|
||||
const run = (hasDecoder, cardEquipped) => {
|
||||
const el = fakeEl();
|
||||
el.setAttribute('data-needs', 'card');
|
||||
el.setAttribute('data-needs-decoder', '');
|
||||
globalThis.document = { querySelectorAll: () => [el], getElementById: () => null };
|
||||
_pysimServerAvailable = true;
|
||||
_pysimCardEquipped = cardEquipped;
|
||||
globalThis.pysimFsHasDecoder = () => hasDecoder;
|
||||
pysimApplyAvailability();
|
||||
return el;
|
||||
};
|
||||
// decoder present, card equipped -> enabled, no tooltip
|
||||
let el = run(true, true);
|
||||
assert.strictEqual(el.disabled, false);
|
||||
assert.strictEqual(el.attrs.title, undefined);
|
||||
// no decoder -> disabled with its own hint
|
||||
el = run(false, true);
|
||||
assert.strictEqual(el.disabled, true);
|
||||
assert.strictEqual(el.attrs.title, 'No decoder for this file');
|
||||
// card missing and no decoder -> the card hint wins
|
||||
el = run(false, false);
|
||||
assert.strictEqual(el.disabled, true);
|
||||
assert.strictEqual(el.attrs.title, 'Insert and equip a card');
|
||||
});
|
||||
|
||||
test('indicator image stays within the 32px header row budget', () => {
|
||||
const m = /id="state-indicator-img"[^>]*style="width:(\d+)px;height:(\d+)px"/.exec(html);
|
||||
assert.ok(m, 'inline image size not found');
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "pysim-simple-server"
|
||||
version = "2.7.9"
|
||||
version = "2.7.10"
|
||||
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.
|
||||
|
||||
@@ -26,7 +26,7 @@ from osmocom.tlv import BER_TLV_IE
|
||||
from osmocom.utils import rpad
|
||||
|
||||
|
||||
VERSION = '2.7.9'
|
||||
VERSION = '2.7.10'
|
||||
|
||||
MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE
|
||||
|
||||
|
||||
Reference in New Issue
Block a user