5 Commits
Author SHA1 Message Date
catarrh 2154912d2b docs: drop the CORS mention from Quick start
The UI and API are served from one origin by default, so CORS is a
non-issue — the quick start no longer calls it out.  Not re-added
elsewhere (kept unobtrusive).
2026-10-27 11:01:05 +03:00
catarrh 6750f72cd5 card reader: move Services pill next to File manager 2026-10-26 22:49:47 +03:00
catarrh 465a43eef2 docs: link the project website alongside the Telegram channel
release / release (push) Waiting to run
2026-10-26 22:11:38 +03:00
catarrh fdbff77188 release: 3.40.2 — file-filter reset (✕) button
A red ✕ next to the filter selector appears whenever the filter is not
"All" and resets it to "All" in one click (pysimFsFilterClear).  Bump
VERSION/pyproject/PWA header to 3.40.2 and the SW cache to simple-v487.
2026-10-26 21:01:29 +03:00
catarrh 1f8fa54c49 release: 3.40.1 — filter uses per-spec service names
release / release (push) Waiting to run
Drop the invented "SMS-related" / "CB-related" filter categories: those
files are service-declared and are already reached through their
per-service options (per-spec service names).  Bump VERSION/pyproject/
PWA header to 3.40.1 and the SW cache to simple-v486.
2026-10-26 19:41:32 +03:00
8 changed files with 99 additions and 53 deletions

No files matched your search

+19
View File
@@ -9,6 +9,25 @@ every change carries a descriptive commit message.
## [Unreleased] ## [Unreleased]
## [3.40.2] — 2026-10-26
### Added
- **File filter: reset (✕) button.** A red ✕ to the right of the filter
selector appears whenever the filter is not "All" and resets it to "All"
in one click.
## [3.40.1] — 2026-10-26
### Changed
- **File filter: drop the "SMS-related" / "CB-related" categories.** Those
files are service-declared and already reachable through their per-service
filter options, which carry the per-spec service names ("Short Message
Storage (SMS)", "SMS Parameters (SMSP)", "Short Message Status Reports
(SMSR)", "Cell Broadcast Message Identifier (CBMI)", "Cell Broadcast
Message Identifier Ranges (CBMIR)").
## [3.40.0] — 2026-10-26 ## [3.40.0] — 2026-10-26
### Added ### Added
+2 -2
View File
@@ -2,7 +2,7 @@
> Formerly known as OTAMan — renamed to SIMple on 2026-09-19. > Formerly known as OTAMan — renamed to SIMple on 2026-09-19.
📢 Project news and updates: [Telegram channel](https://t.me/youeyeseesee) 📢 Project news and updates: [website](https://atroshin.ru) · [Telegram channel](https://t.me/youeyeseesee)
SIMple is a workbench for UICC/SIM card specialists: an offline HTML/JS PWA plus a local card server ([`pysim-simple-server`](pysim_simple_server/), a pySim/PC-SC bridge that also serves the PWA). It covers the full lab cycle: SIMple is a workbench for UICC/SIM card specialists: an offline HTML/JS PWA plus a local card server ([`pysim-simple-server`](pysim_simple_server/), a pySim/PC-SC bridge that also serves the PWA). It covers the full lab cycle:
@@ -47,7 +47,7 @@ cd simple # `git pull` still updates it
./start.sh # or start.bat ./start.sh # or start.bat
``` ```
Then open http://127.0.0.1:8080 — the UI and API share one origin, so no CORS or browser-permission setup is needed. The server starts fine with an empty reader (no card is a normal state): insert a card and it is initialized automatically (auto-equip), or press **Equip card** in the UI. Then open http://127.0.0.1:8080 — the server starts fine with an empty reader (no card is a normal state): insert a card and it is initialized automatically (auto-equip), or press **Equip card** in the UI.
## Build ## Build
+2 -2
View File
@@ -2,7 +2,7 @@
> Прежнее название — OTAMan; переименован в SIMple 2026-09-19. > Прежнее название — OTAMan; переименован в SIMple 2026-09-19.
📢 Новости и обновления проекта: [Telegram-канал](https://t.me/youeyeseesee) 📢 Новости и обновления проекта: [сайт](https://atroshin.ru) · [Telegram-канал](https://t.me/youeyeseesee)
SIMple — инструментарий для специалистов по UICC/SIM-картам: автономный HTML/JS PWA и локальный сервер карт ([`pysim-simple-server`](pysim_simple_server/) — мост к pySim/PC-SC, он же раздаёт PWA). Закрывает весь цикл лабораторной работы: SIMple — инструментарий для специалистов по UICC/SIM-картам: автономный HTML/JS PWA и локальный сервер карт ([`pysim-simple-server`](pysim_simple_server/) — мост к pySim/PC-SC, он же раздаёт PWA). Закрывает весь цикл лабораторной работы:
@@ -47,7 +47,7 @@ cd simple # `git pull` всё равно
./start.sh # или start.bat ./start.sh # или start.bat
``` ```
Затем откройте http://127.0.0.1:8080 — интерфейс и API на одном origin, поэтому CORS и разрешения браузера не нужны. Сервер запускается и с пустым картридером (отсутствие карты — нормальное состояние): вставьте карту, и она инициализируется автоматически (auto-equip), либо нажмите **Equip card** в интерфейсе. Затем откройте http://127.0.0.1:8080 — сервер запускается и с пустым картридером (отсутствие карты — нормальное состояние): вставьте карту, и она инициализируется автоматически (auto-equip), либо нажмите **Equip card** в интерфейсе.
## Сборка ## Сборка
+17 -13
View File
@@ -1104,9 +1104,9 @@
<div id="pysim-body" class="hidden"> <div id="pysim-body" class="hidden">
<div class="flex flex-wrap justify-center gap-1 mb-2"> <div class="flex flex-wrap justify-center gap-1 mb-2">
<button class="pysim-subtab px-3 py-1 text-sm rounded-full bg-blue-600 text-white" data-pysim-sub="files" data-l10n="File manager">File manager</button> <button class="pysim-subtab px-3 py-1 text-sm rounded-full bg-blue-600 text-white" data-pysim-sub="files" data-l10n="File manager">File manager</button>
<button class="pysim-subtab px-3 py-1 text-sm rounded-full bg-gray-200 dark:bg-slate-700 text-gray-700 dark:text-slate-300 hover:bg-gray-300 dark:hover:bg-slate-600 disabled:opacity-40 disabled:cursor-not-allowed" data-pysim-sub="services" data-needs="card" data-l10n="Services">Services</button>
<button class="pysim-subtab px-3 py-1 text-sm rounded-full bg-gray-200 dark:bg-slate-700 text-gray-700 dark:text-slate-300 hover:bg-gray-300 dark:hover:bg-slate-600" data-pysim-sub="cmd" data-l10n="pySim command line">pySim command line</button> <button class="pysim-subtab px-3 py-1 text-sm rounded-full bg-gray-200 dark:bg-slate-700 text-gray-700 dark:text-slate-300 hover:bg-gray-300 dark:hover:bg-slate-600" data-pysim-sub="cmd" data-l10n="pySim command line">pySim command line</button>
<button class="pysim-subtab px-3 py-1 text-sm rounded-full bg-gray-200 dark:bg-slate-700 text-gray-700 dark:text-slate-300 hover:bg-gray-300 dark:hover:bg-slate-600" data-pysim-sub="apdu" data-l10n="Raw APDU">Raw APDU</button> <button class="pysim-subtab px-3 py-1 text-sm rounded-full bg-gray-200 dark:bg-slate-700 text-gray-700 dark:text-slate-300 hover:bg-gray-300 dark:hover:bg-slate-600" data-pysim-sub="apdu" data-l10n="Raw APDU">Raw APDU</button>
<button class="pysim-subtab px-3 py-1 text-sm rounded-full bg-gray-200 dark:bg-slate-700 text-gray-700 dark:text-slate-300 hover:bg-gray-300 dark:hover:bg-slate-600 disabled:opacity-40 disabled:cursor-not-allowed" data-pysim-sub="services" data-needs="card" data-l10n="Services">Services</button>
</div> </div>
<div id="pysim-sub-files"> <div id="pysim-sub-files">
@@ -1127,6 +1127,7 @@
<button type="button" id="pysim-fs-filter-btn" aria-haspopup="true" aria-expanded="false" onclick="pysimFsFilterToggle()" class="px-2 py-0.5 text-xs rounded border border-gray-300 dark:border-slate-600 hover:bg-gray-200 dark:hover:bg-slate-700 text-gray-700 dark:text-slate-300 max-w-xs truncate">All</button> <button type="button" id="pysim-fs-filter-btn" aria-haspopup="true" aria-expanded="false" onclick="pysimFsFilterToggle()" class="px-2 py-0.5 text-xs rounded border border-gray-300 dark:border-slate-600 hover:bg-gray-200 dark:hover:bg-slate-700 text-gray-700 dark:text-slate-300 max-w-xs truncate">All</button>
<div id="pysim-fs-filter-menu" role="group" aria-label="Filter" class="hidden absolute left-0 mt-1 z-50 w-64 max-h-64 overflow-y-auto rounded border border-gray-300 dark:border-slate-600 bg-white dark:bg-slate-800 p-2 shadow-lg"></div> <div id="pysim-fs-filter-menu" role="group" aria-label="Filter" class="hidden absolute left-0 mt-1 z-50 w-64 max-h-64 overflow-y-auto rounded border border-gray-300 dark:border-slate-600 bg-white dark:bg-slate-800 p-2 shadow-lg"></div>
</div> </div>
<button type="button" id="pysim-fs-filter-clear" onclick="pysimFsFilterClear()" class="hidden px-1 text-xs text-red-600 dark:text-red-400 hover:text-red-700 dark:hover:text-red-300" data-l10n-title="Reset filter" aria-label="Reset filter">✕</button>
</div> </div>
<button id="pysim-fs-probe-btn" data-needs="card" onclick="pysimFsProbeAll()" class="w-full mb-1 px-2 py-0.5 rounded bg-gray-600 text-white hover:bg-gray-700 disabled:opacity-40 disabled:cursor-not-allowed" data-l10n="Probe all files">Probe all files</button> <button id="pysim-fs-probe-btn" data-needs="card" onclick="pysimFsProbeAll()" class="w-full mb-1 px-2 py-0.5 rounded bg-gray-600 text-white hover:bg-gray-700 disabled:opacity-40 disabled:cursor-not-allowed" data-l10n="Probe all files">Probe all files</button>
<div id="pysim-fs-probe-status" class="hidden mb-1 text-gray-500 dark:text-slate-400"></div> <div id="pysim-fs-probe-status" class="hidden mb-1 text-gray-500 dark:text-slate-400"></div>
@@ -1989,7 +1990,7 @@
// ===== Version ===== // ===== Version =====
// Single source of truth for the PWA version: shown in the header and used // Single source of truth for the PWA version: shown in the header and used
// by the server version check in pysimConnect(). // by the server version check in pysimConnect().
const SIMPLE_VERSION = '3.40.0'; const SIMPLE_VERSION = '3.40.2';
document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION; document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION;
// ===== view history (the browser Back/Forward buttons) ===== // ===== view history (the browser Back/Forward buttons) =====
@@ -16094,8 +16095,8 @@ function pysimFsSetSort(mode) {
// can belong to several criteria (EF.SPN is a provider name and the file of // can belong to several criteria (EF.SPN is a provider name and the file of
// the "Service Provider Name" service). `all` is the exclusive default. // the "Service Provider Name" service). `all` is the exclusive default.
// The categories are shown under three headings (`group`): the subscriber // The categories are shown under three headings (`group`): the subscriber
// data, the network-selection data, and services (the SMS files plus one // data, the network-selection data, and services (one option per file-backed
// option per file-backed feature of the service cross-reference). // feature of the service cross-reference).
const FS_FILTER_ALL = 'all'; const FS_FILTER_ALL = 'all';
// "Custom files" is not a name-based criterion: it matches the injected // "Custom files" is not a name-based criterion: it matches the injected
// custom-file nodes by their `custom` flag (user-chosen names). // custom-file nodes by their `custom` flag (user-chosen names).
@@ -16111,12 +16112,6 @@ const FS_FILTER_GROUPS = [
{ id: 'wlan', label: 'WLAN / I-WLAN', group: 'PLMN', files: [ { id: 'wlan', label: 'WLAN / I-WLAN', group: 'PLMN', files: [
'EF.UPLMNWLAN', 'EF.OPLMNWLAN', 'EF.WRI', 'EF.HWSIDL', 'EF.WEHPLMNPI', 'EF.WHPI', 'EF.UPLMNWLAN', 'EF.OPLMNWLAN', 'EF.WRI', 'EF.HWSIDL', 'EF.WEHPLMNPI', 'EF.WHPI',
'EF.WLRPLMN', 'EF.HPLMNDAI'] }, 'EF.WLRPLMN', 'EF.HPLMNDAI'] },
// EF.PSISMSC is a standard name pySim does not model, so it has no tree
// node to match - it stays listed for completeness.
{ id: 'sms', label: 'SMS-related', group: 'Services', files: [
'EF.SMS', 'EF.SMSS', 'EF.SMSP', 'EF.SMSR', 'EF.PSISMSC'] },
{ id: 'cb', label: 'CB-related', group: 'Services', files: [
'EF.CBMI', 'EF.CBMID', 'EF.CBMIR'] },
]; ];
const FS_FILTER_GROUP_ORDER = ['Identifiers', 'PLMN', 'Services']; const FS_FILTER_GROUP_ORDER = ['Identifiers', 'PLMN', 'Services'];
@@ -16276,6 +16271,7 @@ function pysimFsFilterRender() {
const menu = document.getElementById('pysim-fs-filter-menu'); const menu = document.getElementById('pysim-fs-filter-menu');
const btn = document.getElementById('pysim-fs-filter-btn'); const btn = document.getElementById('pysim-fs-filter-btn');
if (!menu) return; if (!menu) return;
const active = pysimFsFilter.size > 0 && !pysimFsFilter.has(FS_FILTER_ALL);
const services = fsFilterServiceOptions(); const services = fsFilterServiceOptions();
const item = (id, label) => const item = (id, label) =>
'<label class="flex items-center gap-2 px-2 py-0.5 rounded cursor-pointer hover:bg-gray-100 dark:hover:bg-slate-700 text-xs text-gray-700 dark:text-slate-200">' + '<label class="flex items-center gap-2 px-2 py-0.5 rounded cursor-pointer hover:bg-gray-100 dark:hover:bg-slate-700 text-xs text-gray-700 dark:text-slate-200">' +
@@ -16302,7 +16298,6 @@ function pysimFsFilterRender() {
if (btn) { if (btn) {
btn.textContent = pysimFsFilterSummary(); btn.textContent = pysimFsFilterSummary();
// a non-"All" selection stands out (blue, like the active sort pill) // a non-"All" selection stands out (blue, like the active sort pill)
const active = pysimFsFilter.size > 0 && !pysimFsFilter.has(FS_FILTER_ALL);
btn.classList.toggle('bg-blue-600', active); btn.classList.toggle('bg-blue-600', active);
btn.classList.toggle('text-white', active); btn.classList.toggle('text-white', active);
btn.classList.toggle('text-gray-700', !active); btn.classList.toggle('text-gray-700', !active);
@@ -16310,6 +16305,11 @@ function pysimFsFilterRender() {
btn.classList.toggle('hover:bg-gray-200', !active); btn.classList.toggle('hover:bg-gray-200', !active);
btn.classList.toggle('dark:hover:bg-slate-700', !active); btn.classList.toggle('dark:hover:bg-slate-700', !active);
} }
const clearBtn = document.getElementById('pysim-fs-filter-clear');
if (clearBtn) {
clearBtn.classList.toggle('hidden', !active);
clearBtn.setAttribute('aria-label', t('Reset filter'));
}
} }
// Expand or collapse the menu (`force` overrides the toggle; the outside // Expand or collapse the menu (`force` overrides the toggle; the outside
@@ -16345,6 +16345,11 @@ function pysimFsFilterSetEntry(entry, on) {
pysimFsAutoExpandForFilter(); pysimFsAutoExpandForFilter();
} }
// The ✕ next to the filter selector: back to "All".
function pysimFsFilterClear() {
pysimFsFilterSetEntry(FS_FILTER_ALL, true);
}
// The cross-reference inverted to service number -> feature name, per file // The cross-reference inverted to service number -> feature name, per file
// table. Both maps are declared later in this file, so they are read on the // table. Both maps are declared later in this file, so they are read on the
// first call, never while the filter block itself is evaluated. // first call, never while the filter block itself is evaluated.
@@ -28106,13 +28111,12 @@ const LANG_RU = {
'Sort:': 'Сортировка:', 'Sort:': 'Сортировка:',
'Name': 'Имя', 'Name': 'Имя',
'Filter:': 'Фильтр:', 'Filter:': 'Фильтр:',
'Reset filter': 'Сбросить фильтр',
'(no filters yet)': '(фильтров пока нет)', '(no filters yet)': '(фильтров пока нет)',
'Identifiers': 'Идентификаторы', 'Identifiers': 'Идентификаторы',
'PLMN selection': 'Выбор PLMN', 'PLMN selection': 'Выбор PLMN',
'Provider names': 'Имена операторов', 'Provider names': 'Имена операторов',
'WLAN / I-WLAN': 'WLAN / I-WLAN', 'WLAN / I-WLAN': 'WLAN / I-WLAN',
'SMS-related': 'Связанное с SMS',
'CB-related': 'Связанное с CB',
'Custom files': 'Пользовательские файлы', 'Custom files': 'Пользовательские файлы',
'Selected: {n}': 'Выбрано: {n}', 'Selected: {n}': 'Выбрано: {n}',
'Expand all': 'Развернуть все', 'Expand all': 'Развернуть все',
+1 -1
View File
@@ -1,4 +1,4 @@
const CACHE = 'simple-v485'; const CACHE = 'simple-v487';
const URLS = [ const URLS = [
'index.html', 'index.html',
'help.html', 'help.html',
+56 -33
View File
@@ -111,10 +111,11 @@ function fakeMenu() {
}; };
} }
const dom = { menu: null, btn: null }; const dom = { menu: null, btn: null, clear: null };
globalThis.document = { globalThis.document = {
getElementById: id => (id === 'pysim-fs-filter-menu' ? dom.menu getElementById: id => (id === 'pysim-fs-filter-menu' ? dom.menu
: (id === 'pysim-fs-filter-btn' ? dom.btn : null)), : (id === 'pysim-fs-filter-btn' ? dom.btn
: (id === 'pysim-fs-filter-clear' ? dom.clear : null))),
}; };
let code = ''; let code = '';
@@ -123,7 +124,7 @@ for (const f of ['fsFilterServiceOptions', 'fsFilterIndexBuild', 'fsFilterIndex'
'fsFilterPathsKeyOf', 'pysimFsFilterPathsInvalidate', 'pysimFsFilterMatchingPaths', 'fsFilterPathsKeyOf', 'pysimFsFilterPathsInvalidate', 'pysimFsFilterMatchingPaths',
'pysimFsFilterMatch', 'pysimFsFilterShow', 'pysimFsFilterLabel', 'pysimFsFilterMatch', 'pysimFsFilterShow', 'pysimFsFilterLabel',
'pysimFsFilterSummary', 'pysimFsFilterRender', 'pysimFsFilterToggle', 'pysimFsFilterSummary', 'pysimFsFilterRender', 'pysimFsFilterToggle',
'pysimFsFilterSetEntry', 'pysimFsFilterRestore', 'pysimFsNodePath', 'pysimFsFilterSetEntry', 'pysimFsFilterClear', 'pysimFsFilterRestore', 'pysimFsNodePath',
'pysimFsExpandAll', 'pysimFsCollapseAll', 'pysimFsAutoExpandForFilter']) { 'pysimFsExpandAll', 'pysimFsCollapseAll', 'pysimFsAutoExpandForFilter']) {
code += extractFunc(html, f) + '\n'; code += extractFunc(html, f) + '\n';
} }
@@ -161,6 +162,31 @@ test('the file tree has a collapsed filter dropdown next to the sort pills', ()
assert.ok(!/pysimFsSetFilter\(/.test(html)); assert.ok(!/pysimFsSetFilter\(/.test(html));
}); });
test('the ✕ next to the selector resets a non-All filter to All', () => {
// markup: a hidden red ✕ that clears the filter
assert.match(html, /id="pysim-fs-filter-clear"[^>]*onclick="pysimFsFilterClear\(\)"/);
assert.match(html, /id="pysim-fs-filter-clear"[^>]*data-l10n-title="Reset filter"/);
// the clear handler resets to All
const fn = extractFunc(html, 'pysimFsFilterClear');
assert.ok(fn.indexOf('pysimFsFilterSetEntry(FS_FILTER_ALL, true)') > 0, fn);
// the render shows it only when a non-All filter is active
dom.menu = fakeMenu();
dom.btn = fakeBtn();
dom.clear = fakeBtn();
withFilter(['plmn-selection'], () => {
pysimFsFilterRender();
assert.strictEqual(dom.clear.classes.has('hidden'), false);
assert.strictEqual(dom.clear.attrs['aria-label'], 'Reset filter');
});
withFilter(['all'], () => {
pysimFsFilterRender();
assert.strictEqual(dom.clear.classes.has('hidden'), true);
});
// the Russian entry exists
const dict = html.match(/const LANG_RU = \{([\s\S]*?)\n\};/)[1];
assert.ok(dict.includes("'Reset filter':"), 'missing Russian entry');
});
test('an outside click or Escape closes the menu', () => { test('an outside click or Escape closes the menu', () => {
const click = html.match(/document\.addEventListener\('click', e => \{([\s\S]{0,600}?)\n\}\);/); const click = html.match(/document\.addEventListener\('click', e => \{([\s\S]{0,600}?)\n\}\);/);
assert.ok(click, 'no document click listener'); assert.ok(click, 'no document click listener');
@@ -173,9 +199,9 @@ test('an outside click or Escape closes the menu', () => {
assert.match(key[1], /pysimFsFilterToggle\(false\)/); assert.match(key[1], /pysimFsFilterToggle\(false\)/);
}); });
test('the six category groups name their files', () => { test('the four category groups name their files', () => {
assert.deepStrictEqual(FS_FILTER_GROUPS.map(g => g.id), assert.deepStrictEqual(FS_FILTER_GROUPS.map(g => g.id),
['identifiers', 'plmn-selection', 'provider-names', 'wlan', 'sms', 'cb']); ['identifiers', 'plmn-selection', 'provider-names', 'wlan']);
const files = id => new Set(FS_FILTER_GROUPS.find(g => g.id === id).files); const files = id => new Set(FS_FILTER_GROUPS.find(g => g.id === id).files);
assert.deepStrictEqual([...files('identifiers')], assert.deepStrictEqual([...files('identifiers')],
['EF.ICCID', 'EF.IMSI', 'EF.MSISDN', 'EF.IMPI', 'EF.IMPU', 'EF.SUPI_NAI', 'EF.GID1', 'EF.GID2']); ['EF.ICCID', 'EF.IMSI', 'EF.MSISDN', 'EF.IMPI', 'EF.IMPU', 'EF.SUPI_NAI', 'EF.GID1', 'EF.GID2']);
@@ -187,8 +213,6 @@ test('the six category groups name their files', () => {
assert.deepStrictEqual([...files('wlan')], assert.deepStrictEqual([...files('wlan')],
['EF.UPLMNWLAN', 'EF.OPLMNWLAN', 'EF.WRI', 'EF.HWSIDL', 'EF.WEHPLMNPI', 'EF.WHPI', ['EF.UPLMNWLAN', 'EF.OPLMNWLAN', 'EF.WRI', 'EF.HWSIDL', 'EF.WEHPLMNPI', 'EF.WHPI',
'EF.WLRPLMN', 'EF.HPLMNDAI']); 'EF.WLRPLMN', 'EF.HPLMNDAI']);
assert.deepStrictEqual([...files('sms')], ['EF.SMS', 'EF.SMSS', 'EF.SMSP', 'EF.SMSR', 'EF.PSISMSC']);
assert.deepStrictEqual([...files('cb')], ['EF.CBMI', 'EF.CBMID', 'EF.CBMIR']);
assert.strictEqual(FS_FILTER_ALL, 'all'); assert.strictEqual(FS_FILTER_ALL, 'all');
assert.strictEqual(FS_FILTER_CUSTOM, 'custom'); assert.strictEqual(FS_FILTER_CUSTOM, 'custom');
}); });
@@ -212,12 +236,12 @@ test('the inverted index keys the uppercased names and merges memberships', () =
assert.ok(ids('EF.PLMNwAcT').includes('plmn-selection')); assert.ok(ids('EF.PLMNwAcT').includes('plmn-selection'));
assert.ok(ids('EF.SPN').includes('provider-names')); assert.ok(ids('EF.SPN').includes('provider-names'));
assert.ok(ids('EF.SPN').includes('svc:Service Provider Name (SPN)')); assert.ok(ids('EF.SPN').includes('svc:Service Provider Name (SPN)'));
assert.ok(ids('EF.SMS').includes('sms')); // SMS/CB files are covered by their per-service options, not a category
assert.ok(ids('EF.SMS').includes('svc:Short Message Storage (SMS)')); assert.ok(ids('EF.SMS').includes('svc:Short Message Storage (SMS)'));
assert.ok(ids('EF.CBMI').includes('cb')); assert.ok(ids('EF.CBMI').includes('svc:Cell Broadcast Message Identifier (CBMI)'));
// EF.PSISMSC is only listed by the SMS category - pySim models no such // EF.PSISMSC is neither a category nor a file-backed service, so nothing
// file, so it never matches a tree node (no service lists it either) // maps it (pySim models no such file, and no service lists it either)
assert.deepStrictEqual([...idx.get('EF.PSISMSC')], ['sms']); assert.ok(!idx.has('EF.PSISMSC'));
assert.ok(!fileBackedFeatures().some(n => (SERVICES_FS_MAP[n] || []).includes('EF.PSISMSC'))); assert.ok(!fileBackedFeatures().some(n => (SERVICES_FS_MAP[n] || []).includes('EF.PSISMSC')));
}); });
@@ -232,12 +256,12 @@ test('a file matches the selected categories, case-insensitively', () => {
assert.strictEqual(pysimFsFilterMatch({ name: 'EF.SPN' }), true); assert.strictEqual(pysimFsFilterMatch({ name: 'EF.SPN' }), true);
assert.strictEqual(pysimFsFilterMatch({ name: 'EF.PNN' }), false); assert.strictEqual(pysimFsFilterMatch({ name: 'EF.PNN' }), false);
}); });
withFilter(['identifiers', 'sms'], () => { withFilter(['identifiers', 'svc:Short Message Status Reports (SMSR)'], () => {
assert.strictEqual(pysimFsFilterMatch({ name: 'EF.IMSI' }), true); assert.strictEqual(pysimFsFilterMatch({ name: 'EF.IMSI' }), true);
assert.strictEqual(pysimFsFilterMatch({ name: 'EF.SMSR' }), true); assert.strictEqual(pysimFsFilterMatch({ name: 'EF.SMSR' }), true);
assert.strictEqual(pysimFsFilterMatch({ name: 'EF.SPN' }), false); assert.strictEqual(pysimFsFilterMatch({ name: 'EF.SPN' }), false);
}); });
withFilter(['sms'], () => { withFilter(['svc:Short Message Storage (SMS)'], () => {
assert.strictEqual(pysimFsFilterMatch({ name: 'EF.ECC' }), false); assert.strictEqual(pysimFsFilterMatch({ name: 'EF.ECC' }), false);
}); });
}); });
@@ -320,8 +344,8 @@ test('a directory shows only on the path of a file the filter selects', () => {
assert.strictEqual(pysimFsFilterShow(t.usim), true); // EF.IMSI/EF.MSISDN copies assert.strictEqual(pysimFsFilterShow(t.usim), true); // EF.IMSI/EF.MSISDN copies
assert.strictEqual(pysimFsFilterShow(t.wlan), false); assert.strictEqual(pysimFsFilterShow(t.wlan), false);
}); });
withFilter(['cb'], () => { withFilter(['svc:Cell Broadcast Message Identifier (CBMI)'], () => {
assert.strictEqual(pysimFsFilterShow(t.gsm), true); // EF.CBMI/CBMID/CBMIR assert.strictEqual(pysimFsFilterShow(t.gsm), true); // EF.CBMI
assert.strictEqual(pysimFsFilterShow(t.telecom), false); assert.strictEqual(pysimFsFilterShow(t.telecom), false);
assert.strictEqual(pysimFsFilterShow(t.wlan), false); assert.strictEqual(pysimFsFilterShow(t.wlan), false);
}); });
@@ -377,13 +401,11 @@ test('the standard list gives a name every tree it lives in', () => {
}); });
test('the selection resolves to the paths of its files', () => { test('the selection resolves to the paths of its files', () => {
withFilter(['cb'], () => { withFilter(['svc:Cell Broadcast Message Identifier (CBMI)'], () => {
const paths = pysimFsFilterMatchingPaths(); const paths = pysimFsFilterMatchingPaths();
assert.ok(paths instanceof Set, 'expected a Set'); assert.ok(paths instanceof Set, 'expected a Set');
assert.deepStrictEqual(Array.from(paths).sort(), [ assert.deepStrictEqual(Array.from(paths).sort(), [
'ADF.USIM-NON-IMSI/6F45', 'ADF.USIM-NON-IMSI/6F48', 'ADF.USIM-NON-IMSI/6F50', 'ADF.USIM-NON-IMSI/6F45', 'ADF.USIM/6F45', 'MF/7F20/6F45',
'ADF.USIM/6F45', 'ADF.USIM/6F48', 'ADF.USIM/6F50',
'MF/7F20/6F45', 'MF/7F20/6F48', 'MF/7F20/6F50',
]); ]);
// cached for the same selection // cached for the same selection
assert.strictEqual(pysimFsFilterMatchingPaths(), paths); assert.strictEqual(pysimFsFilterMatchingPaths(), paths);
@@ -429,7 +451,8 @@ test('the menu lists all, custom, the three headings and the services', () => {
// the categories sit under their headings // the categories sit under their headings
assert.ok(out.indexOf('data-filter="provider-names"') < out.indexOf('>PLMN</div>')); assert.ok(out.indexOf('data-filter="provider-names"') < out.indexOf('>PLMN</div>'));
assert.ok(out.indexOf('data-filter="plmn-selection"') > out.indexOf('>PLMN</div>')); assert.ok(out.indexOf('data-filter="plmn-selection"') > out.indexOf('>PLMN</div>'));
assert.ok(out.indexOf('data-filter="sms"') > out.indexOf('>Services</div>')); // the per-service options sit under the Services heading
assert.ok(out.indexOf('data-filter="svc:') > out.indexOf('>Services</div>'));
const boxes = renderedBoxes(); const boxes = renderedBoxes();
assert.strictEqual(boxes.length, 2 + FS_FILTER_GROUPS.length + fsFilterServiceOptions().length); assert.strictEqual(boxes.length, 2 + FS_FILTER_GROUPS.length + fsFilterServiceOptions().length);
assert.strictEqual(boxes[0].id, 'all'); assert.strictEqual(boxes[0].id, 'all');
@@ -513,11 +536,11 @@ test('the button summary names the single entry or counts the selection', () =>
assert.strictEqual(summary(['all']), 'All'); assert.strictEqual(summary(['all']), 'All');
// an empty selection also means everything // an empty selection also means everything
assert.strictEqual(summary([]), 'All'); assert.strictEqual(summary([]), 'All');
assert.strictEqual(summary(['all', 'sms']), 'All'); assert.strictEqual(summary(['all', 'plmn-selection']), 'All');
assert.strictEqual(summary(['custom']), 'Custom files'); assert.strictEqual(summary(['custom']), 'Custom files');
assert.strictEqual(summary(['plmn-selection']), 'PLMN selection'); assert.strictEqual(summary(['plmn-selection']), 'PLMN selection');
assert.strictEqual(summary(['svc:Service Provider Name (SPN)']), 'Service Provider Name (SPN)'); assert.strictEqual(summary(['svc:Service Provider Name (SPN)']), 'Service Provider Name (SPN)');
assert.strictEqual(summary(['sms', 'cb']), 'Selected: 2'); assert.strictEqual(summary(['plmn-selection', 'wlan']), 'Selected: 2');
// an unknown id falls back to itself // an unknown id falls back to itself
assert.strictEqual(summary(['providers']), 'providers'); assert.strictEqual(summary(['providers']), 'providers');
}); });
@@ -550,18 +573,18 @@ test('the checkbox rows keep "all" exclusive and persist the selection', () => {
assert.strictEqual(_store.simple_fs_filter, '["all"]'); assert.strictEqual(_store.simple_fs_filter, '["all"]');
assert.strictEqual(_treeRenders, 1); assert.strictEqual(_treeRenders, 1);
// choosing a category drops "all" // choosing a category drops "all"
pysimFsFilterSetEntry('sms', true); pysimFsFilterSetEntry('provider-names', true);
assert.deepStrictEqual([...pysimFsFilter], ['sms']); assert.deepStrictEqual([...pysimFsFilter], ['provider-names']);
assert.strictEqual(_store.simple_fs_filter, '["sms"]'); assert.strictEqual(_store.simple_fs_filter, '["provider-names"]');
assert.strictEqual(_treeRenders, 3); // render + the auto-expand's render assert.strictEqual(_treeRenders, 3); // render + the auto-expand's render
assert.strictEqual(box('sms').checked, true); assert.strictEqual(box('provider-names').checked, true);
assert.strictEqual(box('all').checked, false); assert.strictEqual(box('all').checked, false);
assert.strictEqual(dom.btn.textContent, 'SMS-related'); assert.strictEqual(dom.btn.textContent, 'Provider names');
// several categories stay selected // several categories stay selected
pysimFsFilterSetEntry('identifiers', true); pysimFsFilterSetEntry('identifiers', true);
assert.deepStrictEqual([...pysimFsFilter], ['sms', 'identifiers']); assert.deepStrictEqual([...pysimFsFilter], ['provider-names', 'identifiers']);
// unchecking one keeps the other // unchecking one keeps the other
pysimFsFilterSetEntry('sms', false); pysimFsFilterSetEntry('provider-names', false);
assert.deepStrictEqual([...pysimFsFilter], ['identifiers']); assert.deepStrictEqual([...pysimFsFilter], ['identifiers']);
// unchecking the last one restores the default // unchecking the last one restores the default
pysimFsFilterSetEntry('identifiers', false); pysimFsFilterSetEntry('identifiers', false);
@@ -569,7 +592,7 @@ test('the checkbox rows keep "all" exclusive and persist the selection', () => {
assert.strictEqual(_store.simple_fs_filter, '["all"]'); assert.strictEqual(_store.simple_fs_filter, '["all"]');
assert.strictEqual(box('all').checked, true); assert.strictEqual(box('all').checked, true);
// "all" is exclusive even when another entry is selected // "all" is exclusive even when another entry is selected
pysimFsFilter = new Set(['sms']); pysimFsFilter = new Set(['provider-names']);
pysimFsFilterSetEntry('all', true); pysimFsFilterSetEntry('all', true);
assert.deepStrictEqual([...pysimFsFilter], ['all']); assert.deepStrictEqual([...pysimFsFilter], ['all']);
}); });
@@ -582,7 +605,7 @@ test('the saved selection is restored, defaulting to "all"', () => {
pysimFsFilterRestore(); pysimFsFilterRestore();
assert.deepStrictEqual([...pysimFsFilter], ['providers', 'svc:Service Provider Name (SPN)']); assert.deepStrictEqual([...pysimFsFilter], ['providers', 'svc:Service Provider Name (SPN)']);
// `all` is exclusive; an empty or broken save falls back to it // `all` is exclusive; an empty or broken save falls back to it
_store.simple_fs_filter = JSON.stringify(['all', 'sms']); _store.simple_fs_filter = JSON.stringify(['all', 'plmn-selection']);
pysimFsFilterRestore(); pysimFsFilterRestore();
assert.deepStrictEqual([...pysimFsFilter], ['all']); assert.deepStrictEqual([...pysimFsFilter], ['all']);
_store.simple_fs_filter = '[]'; _store.simple_fs_filter = '[]';
@@ -636,7 +659,7 @@ test('the dropdown is re-rendered on language switch', () => {
test('the filter labels have Russian entries', () => { test('the filter labels have Russian entries', () => {
const dict = html.match(/const LANG_RU = \{([\s\S]*?)\n\};/)[1]; const dict = html.match(/const LANG_RU = \{([\s\S]*?)\n\};/)[1];
for (const key of ['All', 'Custom files', 'Identifiers', 'PLMN', 'Services', 'PLMN selection', for (const key of ['All', 'Custom files', 'Identifiers', 'PLMN', 'Services', 'PLMN selection',
'Provider names', 'WLAN / I-WLAN', 'SMS-related', 'CB-related', 'Selected: {n}']) { 'Provider names', 'WLAN / I-WLAN', 'Selected: {n}']) {
assert.ok(dict.includes("'" + key + "':"), 'missing Russian entry: ' + key); assert.ok(dict.includes("'" + key + "':"), 'missing Russian entry: ' + key);
} }
}); });
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "pysim-simple-server" name = "pysim-simple-server"
version = "3.40.0" version = "3.40.2"
description = "HTTP REST server wrapping pysim for the SIMple PWA" description = "HTTP REST server wrapping pysim for the SIMple PWA"
license = { text = "GPL-2.0-or-later" } license = { text = "GPL-2.0-or-later" }
requires-python = ">=3.9" requires-python = ">=3.9"
+1 -1
View File
@@ -37,7 +37,7 @@ from osmocom.tlv import BER_TLV_IE
from osmocom.utils import rpad from osmocom.utils import rpad
VERSION = '3.40.0' VERSION = '3.40.2'
MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE