Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
215d0a0ba0 | ||
|
|
8f63368008 | ||
|
|
29d762a662 | ||
|
|
eeea7ca509 | ||
|
|
ad4289eabe | ||
|
|
04859051d3 | ||
|
|
2154912d2b | ||
|
|
6750f72cd5 | ||
|
|
465a43eef2 | ||
|
|
fdbff77188 | ||
|
|
1f8fa54c49 | ||
|
|
98926833f7 | ||
|
|
4fd2efe77c | ||
|
|
4a4232de5c | ||
|
|
cc7c23a9c7 | ||
|
|
8fdf1ddff9 | ||
|
|
e2d5d720f7 | ||
|
|
627198e57b | ||
|
|
77fbed80ce | ||
|
|
d1067e3e72 | ||
|
|
01e50d841f | ||
|
|
3878156646 | ||
|
|
105efe62da | ||
|
|
4835bc8585 | ||
|
|
b173e73437 | ||
|
|
a1892094f4 | ||
|
|
12ff552df8 | ||
|
|
e10228c37c | ||
|
|
fc27feba8e | ||
|
|
5ebcd0773d | ||
|
|
8e0a97f025 | ||
|
|
6d762b9d9c | ||
|
|
1d5a049ea1 | ||
|
|
504bf1d66e | ||
|
|
62c5b4e1a9 |
No files matched your search
+102
@@ -9,6 +9,108 @@ every change carries a descriptive commit message.
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [3.40.3] — 2026-10-27
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **Equipped card not always auto-selected in SCP80/RAM.** The ICCID-based
|
||||||
|
preset auto-selection latched its "already handled" guard even when no
|
||||||
|
matching preset was found, so a card equipped before `/api/presets` had
|
||||||
|
loaded (the connect-time race) never got auto-selected — navigating to
|
||||||
|
SCP80/RAM showed no preset until the card was re-inserted. The guard is
|
||||||
|
now latched only after a successful selection, and `cardsFetch` re-runs
|
||||||
|
the auto-selection once the store arrives.
|
||||||
|
|
||||||
|
## [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
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **File-tree filter** (the file manager gains a **Filter** dropdown, next to
|
||||||
|
Sort). A collapsed checkbox multiselect with: **All** (default), **Custom
|
||||||
|
files**, **Identifiers** (ICCID/IMSI/MSISDN/IMPI/IMPU/SUPI_NAI/GID1/GID2),
|
||||||
|
**PLMN selection** / **Provider names** / **WLAN/I-WLAN** / **SMS-related** /
|
||||||
|
**CB-related** (cell broadcast), and one option per file-backed service of
|
||||||
|
the cross-reference. When a filter is active, directories that are not on
|
||||||
|
the path of the selected files are hidden; the button turns blue to stand
|
||||||
|
out.
|
||||||
|
- **Expand all / Collapse all** buttons beside the sort pills; **Expand all**
|
||||||
|
walks the tree loading children on demand and skips directories the probe
|
||||||
|
found absent, and selecting any filter other than "All" auto-expands the
|
||||||
|
tree so the matched files are visible.
|
||||||
|
- **"To EF filter"** cross-link: each file-backed service row in the
|
||||||
|
SST/UST/IST/EST decoded editors carries a compact glyph that narrows the
|
||||||
|
file-tree filter to exactly that service's files.
|
||||||
|
|
||||||
|
## [3.39.0] — 2026-10-26
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **EF.SST tri-state editor.** The SIM service table (TS 51.011 10.3.7, two
|
||||||
|
bits per service — allocated + activated) now edits per service as
|
||||||
|
*deleted / allocated-off / on*, preserving unmodelled pairs and the file
|
||||||
|
length; it stops being read-only.
|
||||||
|
- **Cross-file Services editor** (a new Card-reader pill): SST / UST / EST /
|
||||||
|
IST shown side by side from a curated 161-row feature cross-reference, a
|
||||||
|
column rendered only for a file present on the card, per-file Save buttons,
|
||||||
|
and a **Check filesystem** action (with a throbber) that verifies the
|
||||||
|
declared services against the card's actual files.
|
||||||
|
- **Path-exact filesystem check.** Each feature's files are resolved by
|
||||||
|
(symbolic name, root) → canonical path through `uicc_files.json`, probed at
|
||||||
|
that exact path, and reported in both directions (declared-but-missing,
|
||||||
|
present-but-undeclared) plus an "unchecked" bucket for files not yet in the
|
||||||
|
registry.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- **EF resolution is DF-context aware.** `efDfFromPath` derives the parent DF
|
||||||
|
and `efFindDecoder`/`efFindEncoder` resolve DF → name → FID, so same-name/
|
||||||
|
same-FID files in different DFs are distinct; EF.ECC gained a USIM
|
||||||
|
linear-fixed decoder beside the SIM transparent one, and EF.AD is registered
|
||||||
|
per DF.
|
||||||
|
- **The file manager selects files by canonical path** (not node name), so two
|
||||||
|
same-named files under different DFs (EF.ECC/EF.AD, the DF.PHONEBOOK copies)
|
||||||
|
are distinguishable, and snapshot/profile paths root ADFs by AID are
|
||||||
|
normalized (`A0000000871002`→USIM, `…1004`→ISIM, `…100B`→USIM-non-IMSI).
|
||||||
|
- **The USIM service table is complete**: UST 147–150 (5MBS UE pre-configuration,
|
||||||
|
Operator Controlled Signal Threshold per Access Technology, A2X, IMS Data
|
||||||
|
Channel Indication) were missing.
|
||||||
|
- **`uicc_files.json`** gained the 13 optional files pySim does not model
|
||||||
|
(DF.MExE, DF.SoLSA, DF.BCAST, DF.ACDC, DF.TV, DF.A2X and their children),
|
||||||
|
emitted with distinct (name, path) so same-FID files in other DFs stay
|
||||||
|
separate.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **The start scripts name the address they were asked to bind.** The opening
|
||||||
|
`Starting pysim-simple-server on http://127.0.0.1:8080` line was hardcoded
|
||||||
|
while `--http-host`/`--http-port` were passed through to the server:
|
||||||
|
`./start.sh --http-host 10.0.28.161` bound correctly (the server's own
|
||||||
|
`listening on` banner named the address), but the first line still said
|
||||||
|
127.0.0.1. `start.sh` and `start.bat` now parse both flags (space and `=`
|
||||||
|
forms, the last value wins like argparse) and echo the effective address;
|
||||||
|
the defaults are unchanged.
|
||||||
|
- eCall Data's `EF.FDN` reference was a typo (`6F3A` is EF.ADN); corrected to
|
||||||
|
EF.SDN 6F49 / EF.FDN 6F3B.
|
||||||
|
|
||||||
## [3.38.1] — 2026-10-25
|
## [3.38.1] — 2026-10-25
|
||||||
|
|
||||||
### Performance
|
### Performance
|
||||||
|
|||||||
@@ -2,22 +2,27 @@
|
|||||||
|
|
||||||
> 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 frontend (PWA — a Progressive Web App) plus a local card server ([`pysim-simple-server`](pysim_simple_server/), a pySim/PC-SC bridge that also serves the frontend). It covers the full lab cycle:
|
||||||
|
|
||||||
- **APDU workbench** — SIM/USIM RFM and Expanded Script builders (TS 102 226), GlobalPlatform RAM commands, a C-APDU/R-APDU parser and a response decoder (ISO 7816 / TS 102 221 / GP);
|
- **APDU workbench** — SIM/USIM RFM and Expanded Script builders (TS 102 226), GlobalPlatform RAM commands, a C-APDU/R-APDU parser and a response decoder (ISO 7816 / TS 102 221 / GP);
|
||||||
- **OTA lab** — SCP80 secured packets (TS 102 225) and Java Card RAM installation, plus the terminal side of HTTP OTA (SCP81): BIP channel emulation, a PSK TLS administration server and APDU script execution;
|
- **OTA lab** — SCP80 secured packets (TS 102 225) and Java Card RAM installation, plus the terminal side of HTTP OTA (SCP81): BIP channel emulation, a PSK TLS administration server and APDU script execution;
|
||||||
- **Phone/CAT simulator** — SIM Toolkit menu browser, proactive command / event handling, TERMINAL PROFILE editing and the C-AT dialogue log;
|
- **Phone/CAT simulator** — SIM Toolkit menu browser, proactive command / event handling, TERMINAL PROFILE editing and the C-AT dialogue log;
|
||||||
- **Card profiler** — filesystem capture (profiles and immutable snapshots with timings), exact snapshot comparison and FCP/FCI decoding.
|
- **Card profiler** — filesystem capture (profiles and immutable snapshots with timings), exact snapshot comparison and FCP/FCI decoding.
|
||||||
|
|
||||||
**Demo:** [simple.atroshin.ru](https://simple.atroshin.ru) — the PWA alone, for experimenting. Install the server (below) for card-reader functions.
|
**Demo:** [simple.atroshin.ru](https://simple.atroshin.ru) — the frontend alone, for experimenting. Install the server (below) for card-reader functions.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- **Python 3.9+** with `pip`, and **Git** — the app runs on 3.9+, but the **SCP81 TLS-PSK listener needs Python 3.13+** (`ssl`'s PSK callbacks).
|
||||||
|
- **Python venv support** — `setup.sh` creates `.venv`; if that fails with `ensurepip is not available` (Debian/Ubuntu ship the venv module separately), install it (`sudo apt install python3-venv`) and run `./setup.sh` again. An incomplete `.venv` left by the failed run is removed automatically.
|
||||||
|
- **Smart card reader** (PC/SC or serial/FTDI) — PC/SC is preferable (`pcsc-lite` + `ccid` on Linux). `pyscard` has no Linux wheels, so it builds from source: install the PC/SC header package too (`libpcsclite-dev` on Debian/Ubuntu, `pcsc-lite-devel` on Fedora; on Arch `pcsclite` includes them).
|
||||||
|
- **Windows** — use **Python 3.13 (strongly recommended)**: `pyscard` ships precompiled wheels for it, so nothing needs compiling; on **3.14** the `pyscard` and some pysim dependency wheels are missing and build from source (needs MSVC C++ Build Tools).
|
||||||
|
|
||||||
## Quick start
|
## Quick start
|
||||||
|
|
||||||
**PWA only (client-side tools):** open `frontend/index.html` in any browser, or serve `frontend/` with any static server. No Python required.
|
**Install the server** — two ways to get it:
|
||||||
|
|
||||||
**Full (PWA + card server)** — two ways to get it:
|
|
||||||
|
|
||||||
**1. Download the release package (recommended).** From the
|
**1. Download the release package (recommended).** From the
|
||||||
[Releases](https://github.com/anttro/simple/releases) page take the latest package
|
[Releases](https://github.com/anttro/simple/releases) page take the latest package
|
||||||
@@ -25,7 +30,7 @@ SIMple is a workbench for UICC/SIM card specialists: an offline HTML/JS PWA plus
|
|||||||
|
|
||||||
```sh
|
```sh
|
||||||
./setup.sh # or setup.bat on Windows — creates .venv, installs pysim + server
|
./setup.sh # or setup.bat on Windows — creates .venv, installs pysim + server
|
||||||
./start.sh # or start.bat — starts the server (it serves the PWA too)
|
./start.sh # or start.bat — starts the server (it serves the frontend too)
|
||||||
```
|
```
|
||||||
|
|
||||||
The package is built from the tagged source — no git history, no development files.
|
The package is built from the tagged source — no git history, no development files.
|
||||||
@@ -47,7 +52,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
|
||||||
|
|
||||||
@@ -557,7 +562,7 @@ probe is refused when SPI1 has no counter check or the named preset does not def
|
|||||||
|
|
||||||
## Cards
|
## Cards
|
||||||
|
|
||||||
Stores saved card configurations (presets) in a **server-side store** — `~/.pysim-simple-server/card_presets.json` (`--card-presets` overrides it). A preset holds the cryptographic keys, SPI settings, TARs and the replay counters for SCP80 operations, the optional **ADM** key, plus the **PSK identity / PSK key** pair used by the SCP81 HTTP OTA listener. A preset carries **several keysets** — a keyset's number is the `b8..b5` nibble of its KIc/KID (TS 102 225 §5.1.2/A.2): KIc and KID of a keyset must carry the same number (`01`–`0F`; A.2 rejects a mismatch), `00` (no security) is not a preset keyset, and each keyset has its own counter on the card (Annex A.1) — the server owns those counters, persists every consumed increment and appends it to an audit trail (`card_presets.json.audit.jsonl`). The list is loaded when the PWA connects; JSON export/import (which also accepts the old flat presets) is the migration path from the browser-local store. Cards is a **top-level tab**. The form groups the fields into two bordered blocks — **SCP80 (GSM 03.48, ETSI TS 102 225)** and **SCP81 (HTTP OTA)** — with the optional **ADM** field in the top row; the keysets are edited as rows (one per number, **Add keyset** / **Remove keyset**). When the card is equipped its EF.ICCID is read and the preset with the same ICCID is selected automatically in all three SCP80 views; when the equipped card matches **no** preset — or the selected preset belongs to another card — all three SCP80 views show an amber warning above the preset selector. The header shows gray **SCP80** / **SCP81** markers and a key glyph on the **ADM** badge when the preset matching the equipped card's ICCID has those settings filled in.
|
Stores saved card configurations (presets) in a **server-side store** — `~/.pysim-simple-server/card_presets.json` (`--card-presets` overrides it). A preset holds the cryptographic keys, SPI settings, TARs and the replay counters for SCP80 operations, the optional **ADM** key, plus the **PSK identity / PSK key** pair used by the SCP81 HTTP OTA listener. A preset carries **several keysets** — a keyset's number is the `b8..b5` nibble of its KIc/KID (TS 102 225 §5.1.2/A.2): KIc and KID of a keyset must carry the same number (`01`–`0F`; A.2 rejects a mismatch), `00` (no security) is not a preset keyset, and each keyset has its own counter on the card (Annex A.1) — the server owns those counters, persists every consumed increment and appends it to an audit trail (`card_presets.json.audit.jsonl`). The list is loaded when the frontend connects; JSON export/import (which also accepts the old flat presets) is the migration path from the browser-local store. Cards is a **top-level tab**. The form groups the fields into two bordered blocks — **SCP80 (GSM 03.48, ETSI TS 102 225)** and **SCP81 (HTTP OTA)** — with the optional **ADM** field in the top row; the keysets are edited as rows (one per number, **Add keyset** / **Remove keyset**). When the card is equipped its EF.ICCID is read and the preset with the same ICCID is selected automatically in all three SCP80 views; when the equipped card matches **no** preset — or the selected preset belongs to another card — all three SCP80 views show an amber warning above the preset selector. The header shows gray **SCP80** / **SCP81** markers and a key glyph on the **ADM** badge when the preset matching the equipped card's ICCID has those settings filled in.
|
||||||
|
|
||||||
| Field | Description |
|
| Field | Description |
|
||||||
|---|---|
|
|---|---|
|
||||||
@@ -588,7 +593,7 @@ The **SCP81** column shows whether the preset supplies a usable PSK pair: **✓*
|
|||||||
|
|
||||||
Connects to the bundled [`pysim-simple-server`](pysim_simple_server/) for live card operations.
|
Connects to the bundled [`pysim-simple-server`](pysim_simple_server/) for live card operations.
|
||||||
|
|
||||||
> **Browser restriction:** when the PWA is served from a public HTTPS host, reaching the local server (`http://127.0.0.1:8080`) requires two things: the server must send `Access-Control-Allow-Private-Network: true` (pysim-simple-server ≥ 1.6.1 does this automatically), and the browser must be allowed to access the local network — in Chrome/Edge/Vivaldi: Site settings → Local network access → allow the site (or accept the permission prompt). Without the browser permission, the request to `127.0.0.1` is blocked before any preflight is sent.
|
> **Browser restriction:** when the frontend is served from a public HTTPS host, reaching the local server (`http://127.0.0.1:8080`) requires two things: the server must send `Access-Control-Allow-Private-Network: true` (pysim-simple-server ≥ 1.6.1 does this automatically), and the browser must be allowed to access the local network — in Chrome/Edge/Vivaldi: Site settings → Local network access → allow the site (or accept the permission prompt). Without the browser permission, the request to `127.0.0.1` is blocked before any preflight is sent.
|
||||||
|
|
||||||
### File Browser
|
### File Browser
|
||||||
|
|
||||||
@@ -743,21 +748,14 @@ The interface is in English with Russian support. The language is detected from
|
|||||||
|
|
||||||
## Server (pysim-simple-server)
|
## Server (pysim-simple-server)
|
||||||
|
|
||||||
The bundled Python server wraps [pySim](https://osmocom.org/projects/pysim/wiki) and serves both the SIMple PWA (from `frontend/`) and a JSON API under `/api/*`.
|
The bundled Python server wraps [pySim](https://osmocom.org/projects/pysim/wiki) and serves both the SIMple frontend (from `frontend/`) and a JSON API under `/api/*`.
|
||||||
|
|
||||||
### Prerequisites
|
|
||||||
|
|
||||||
- **Python 3.9+** with `pip`, and **Git** — the app runs on 3.9+, but the **SCP81 TLS-PSK listener needs Python 3.13+** (`ssl`'s PSK callbacks).
|
|
||||||
- **Python venv support** — `setup.sh` creates `.venv`; if that fails with `ensurepip is not available` (Debian/Ubuntu ship the venv module separately), install it (`sudo apt install python3-venv`) and run `./setup.sh` again. An incomplete `.venv` left by the failed run is removed automatically.
|
|
||||||
- **Smart card reader** (PC/SC or serial/FTDI) — PC/SC is preferable (`pcsc-lite` + `ccid` on Linux). `pyscard` has no Linux wheels, so it builds from source: install the PC/SC header package too (`libpcsclite-dev` on Debian/Ubuntu, `pcsc-lite-devel` on Fedora; on Arch `pcsclite` includes them).
|
|
||||||
- **Windows** — use **Python 3.13 (strongly recommended)**: `pyscard` has precompiled wheels up to 3.13 and the SCP81 TLS-PSK listener needs 3.13+. On 3.14 `pyscard` and some pysim dependency wheels are not available and build from source (needs MSVC C++ Build Tools). 3.9–3.12 run the app but not the SCP81 PSK listener. The SMPP bridge (`smpp.twisted3`) is intentionally skipped on Windows.
|
|
||||||
|
|
||||||
### Scripts
|
### Scripts
|
||||||
|
|
||||||
| Script | What it does |
|
| Script | What it does |
|
||||||
|--------|-------------|
|
|--------|-------------|
|
||||||
| `setup.sh` / `setup.bat` | Creates `.venv/` (or repairs an incomplete one), installs pysim and the server. Run once after cloning or after unzipping the release package. |
|
| `setup.sh` / `setup.bat` | Creates `.venv/` (or repairs an incomplete one), installs pysim and the server. Run once after cloning or after unzipping the release package. |
|
||||||
| `start.sh` / `start.bat` | Starts the server from the venv (serves the PWA + API on `:8080`). |
|
| `start.sh` / `start.bat` | Starts the server from the venv (serves the frontend + API on `:8080`). |
|
||||||
| `update.sh` / `update.bat` | Updates a packaged install in place: fetches the latest release package, replaces the app files and re-installs the app into the existing `.venv` (pysim is kept). `--check`, `--version X.Y.Z`, `--zip FILE`, `--no-pip`, `--force`. |
|
| `update.sh` / `update.bat` | Updates a packaged install in place: fetches the latest release package, replaces the app files and re-installs the app into the existing `.venv` (pysim is kept). `--check`, `--version X.Y.Z`, `--zip FILE`, `--no-pip`, `--force`. |
|
||||||
|
|
||||||
`start.sh` checks PC/SC access before starting: if the user may not use `pcscd` (or the service is not running) it prints the cause with the group/polkit guidance and exits — the server **no longer falls back to a serial reader silently**. With access, the server auto-detects the first PC/SC reader; `start.bat` always passes `-p 0` (PC/SC is built into Windows). An explicit serial/FTDI reader is still possible with `-d <device>` (the pre-flight check is skipped). Extra arguments are passed through to the server, e.g. `./start.sh --gsmtap` or `start.bat --gsmtap 10.0.0.5:4729`. If no PC/SC reader is found the server reports it and exits — connect the reader and restart (a reader present but no card is still a normal cardless start).
|
`start.sh` checks PC/SC access before starting: if the user may not use `pcscd` (or the service is not running) it prints the cause with the group/polkit guidance and exits — the server **no longer falls back to a serial reader silently**. With access, the server auto-detects the first PC/SC reader; `start.bat` always passes `-p 0` (PC/SC is built into Windows). An explicit serial/FTDI reader is still possible with `-d <device>` (the pre-flight check is skipped). Extra arguments are passed through to the server, e.g. `./start.sh --gsmtap` or `start.bat --gsmtap 10.0.0.5:4729`. If no PC/SC reader is found the server reports it and exits — connect the reader and restart (a reader present but no card is still a normal cardless start).
|
||||||
@@ -773,8 +771,8 @@ pysim-simple-server --http-port 8080
|
|||||||
```
|
```
|
||||||
|
|
||||||
On Windows use `setup.bat` instead: it installs pysim's dependencies from
|
On Windows use `setup.bat` instead: it installs pysim's dependencies from
|
||||||
`requirements-pysim.txt` and pysim itself with `--no-deps`, so the SMPP bridge (and
|
`requirements-pysim.txt` and pysim itself with `--no-deps`, so pysim's optional
|
||||||
its MSVC build chain) is not pulled in.
|
dependencies are not pulled in.
|
||||||
|
|
||||||
### CLI options
|
### CLI options
|
||||||
|
|
||||||
@@ -782,7 +780,7 @@ its MSVC build chain) is not pulled in.
|
|||||||
|--------|-------------|
|
|--------|-------------|
|
||||||
| `--http-host` | Bind address (default: `127.0.0.1`) |
|
| `--http-host` | Bind address (default: `127.0.0.1`) |
|
||||||
| `--http-port` | TCP port (default: `8080`) |
|
| `--http-port` | TCP port (default: `8080`) |
|
||||||
| `--web-dir` | Directory with PWA static files (default: `<repo>/frontend`) |
|
| `--web-dir` | Directory with frontend static files (default: `<repo>/frontend`) |
|
||||||
| `-p` / `--pcsc-device` | PC/SC reader slot number |
|
| `-p` / `--pcsc-device` | PC/SC reader slot number |
|
||||||
| `-d` / `--device` | Serial device path |
|
| `-d` / `--device` | Serial device path |
|
||||||
| `--no-card-init` | Skip card init to preserve the CAT session (no file manager) |
|
| `--no-card-init` | Skip card init to preserve the CAT session (no file manager) |
|
||||||
@@ -824,13 +822,13 @@ See [docs/api.md](docs/api.md) for the full endpoint reference.
|
|||||||
|
|
||||||
## Version compatibility
|
## Version compatibility
|
||||||
|
|
||||||
| PWA (SIMple) | Server | Status |
|
| frontend (SIMple) | Server | Status |
|
||||||
|-------------|--------|--------|
|
|-------------|--------|--------|
|
||||||
| any | same major | ✅ Compatible |
|
| any | same major | ✅ Compatible |
|
||||||
| any | older major | ❌ Outdated — update server |
|
| any | older major | ❌ Outdated — update server |
|
||||||
| any | newer major | ⚠️ Server newer — update PWA |
|
| any | newer major | ⚠️ Server newer — update frontend |
|
||||||
|
|
||||||
The PWA checks the server version on connect via `GET /api/version` and compares the major version (e.g. a 3.x PWA with a 3.x server; a 2.x server is flagged as outdated).
|
The frontend checks the server version on connect via `GET /api/version` and compares the major version (e.g. a 3.x frontend with a 3.x server; a 2.x server is flagged as outdated).
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
@@ -843,5 +841,5 @@ pySim itself is not included in this repository or in the release artifacts:
|
|||||||
`setup.sh` / `setup.bat` install it from its upstream git repository.
|
`setup.sh` / `setup.bat` install it from its upstream git repository.
|
||||||
|
|
||||||
Bundled third-party components (the MCC/MNC operator list and the AES/DES
|
Bundled third-party components (the MCC/MNC operator list and the AES/DES
|
||||||
bundles in the PWA) keep their own MIT licenses — see
|
bundles in the frontend) keep their own MIT licenses — see
|
||||||
[THIRD-PARTY-NOTICES](THIRD-PARTY-NOTICES).
|
[THIRD-PARTY-NOTICES](THIRD-PARTY-NOTICES).
|
||||||
+25
-27
@@ -2,22 +2,27 @@
|
|||||||
|
|
||||||
> Прежнее название — 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 — Progressive Web App) и локальный сервер карт ([`pysim-simple-server`](pysim_simple_server/) — мост к pySim/PC-SC, он же раздаёт фронтенд). Закрывает весь цикл лабораторной работы:
|
||||||
|
|
||||||
- **APDU-верстак** — конструкторы SIM/USIM RFM и Expanded Script (TS 102 226), команды GlobalPlatform RAM, парсер C-APDU/R-APDU и декодер ответов (ISO 7816 / TS 102 221 / GP);
|
- **APDU-верстак** — конструкторы SIM/USIM RFM и Expanded Script (TS 102 226), команды GlobalPlatform RAM, парсер C-APDU/R-APDU и декодер ответов (ISO 7816 / TS 102 221 / GP);
|
||||||
- **OTA-лаборатория** — защищённые пакеты SCP80 (TS 102 225) и установка Java Card RAM, а также терминальная сторона HTTP OTA (SCP81): эмуляция BIP-каналов, PSK-TLS-сервер администрирования и выполнение скриптов APDU;
|
- **OTA-лаборатория** — защищённые пакеты SCP80 (TS 102 225) и установка Java Card RAM, а также терминальная сторона HTTP OTA (SCP81): эмуляция BIP-каналов, PSK-TLS-сервер администрирования и выполнение скриптов APDU;
|
||||||
- **Симулятор** — браузер меню SIM Toolkit, обработка proactive-команд и событий, редактирование TERMINAL PROFILE, тест-скрипты карты и журнал C-AT-диалога;
|
- **Симулятор** — браузер меню SIM Toolkit, обработка proactive-команд и событий, редактирование TERMINAL PROFILE, тест-скрипты карты и журнал C-AT-диалога;
|
||||||
- **Профайлер карты** — захват файловой системы (профили и неизменяемые снимки с таймингами), точное сравнение снимков и декодер FCP/FCI.
|
- **Профайлер карты** — захват файловой системы (профили и неизменяемые снимки с таймингами), точное сравнение снимков и декодер FCP/FCI.
|
||||||
|
|
||||||
**Демо:** [simple.atroshin.ru](https://simple.atroshin.ru) — только PWA, для экспериментов. Для функций картридера установите сервер (ниже).
|
**Демо:** [simple.atroshin.ru](https://simple.atroshin.ru) — только фронтенд, для экспериментов. Для функций картридера установите сервер (ниже).
|
||||||
|
|
||||||
|
## Требования
|
||||||
|
|
||||||
|
- **Python 3.9+** с `pip`, и **Git** — приложение работает на 3.9+, но **SCP81 TLS-PSK-слушателю нужен Python 3.13+** (PSK-callback'и `ssl`).
|
||||||
|
- **Поддержка venv в Python** — `setup.sh` создаёт `.venv`; если создание завершается с `ensurepip is not available` (в Debian/Ubuntu модуль venv поставляется отдельно), установите его (`sudo apt install python3-venv`) и запустите `./setup.sh` снова. Неполный `.venv`, оставшийся от неудачного запуска, удаляется автоматически.
|
||||||
|
- **Смарт-картридер** (PC/SC или serial/FTDI) — предпочтителен PC/SC (`pcsc-lite` + `ccid` на Linux). У `pyscard` нет Linux-wheel, поэтому он собирается из исходников: поставьте и пакет с заголовками PC/SC (`libpcsclite-dev` в Debian/Ubuntu, `pcsc-lite-devel` в Fedora; в Arch они входят в `pcsclite`).
|
||||||
|
- **Windows** — используйте **Python 3.13 (настоятельно рекомендуется)**: для него `pyscard` поставляет готовые wheel, так что ничего компилировать не нужно; на **3.14** wheel для `pyscard` и части зависимостей pysim отсутствуют, и они собираются из исходников (нужны MSVC C++ Build Tools).
|
||||||
|
|
||||||
## Быстрый старт
|
## Быстрый старт
|
||||||
|
|
||||||
**Только PWA (клиентские функции):** откройте `frontend/index.html` в любом браузере или раздайте `frontend/` любым статическим сервером. Python не нужен.
|
**Установка сервера** — два способа:
|
||||||
|
|
||||||
**Полная установка (PWA + сервер карт)** — два способа:
|
|
||||||
|
|
||||||
**1. Скачать релизный пакет (рекомендуется).** На странице
|
**1. Скачать релизный пакет (рекомендуется).** На странице
|
||||||
[Releases](https://github.com/anttro/simple/releases) возьмите последний пакет
|
[Releases](https://github.com/anttro/simple/releases) возьмите последний пакет
|
||||||
@@ -25,7 +30,7 @@ SIMple — инструментарий для специалистов по UIC
|
|||||||
|
|
||||||
```sh
|
```sh
|
||||||
./setup.sh # или setup.bat в Windows — создаёт .venv, ставит pysim + сервер
|
./setup.sh # или setup.bat в Windows — создаёт .venv, ставит pysim + сервер
|
||||||
./start.sh # или start.bat — запускает сервер (он же раздаёт PWA)
|
./start.sh # или start.bat — запускает сервер (он же раздаёт фронтенд)
|
||||||
```
|
```
|
||||||
|
|
||||||
Пакет собран из помеченного тегом исходного кода — без истории git и без файлов для
|
Пакет собран из помеченного тегом исходного кода — без истории git и без файлов для
|
||||||
@@ -47,7 +52,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** в интерфейсе.
|
||||||
|
|
||||||
## Сборка
|
## Сборка
|
||||||
|
|
||||||
@@ -532,7 +537,7 @@ Delivery PoR (SPI2 `01`) проще — карта возвращает PoR на
|
|||||||
|
|
||||||
## Карты
|
## Карты
|
||||||
|
|
||||||
Хранит сохранённые конфигурации карт (пресеты) в **серверном хранилище** — `~/.pysim-simple-server/card_presets.json` (переопределяется `--card-presets`). Пресет содержит криптографические ключи, настройки SPI, TAR-ы и счётчики повторов для SCP80-операций, необязательный ключ **ADM**, а также пару **PSK identity / PSK key** для слушателя SCP81 HTTP OTA. Пресет содержит **несколько keysets** — номер keyset задаётся нибблом `b8..b5` его KIc/KID (TS 102 225 §5.1.2/A.2): KIc и KID одного keyset должны нести один и тот же номер (`01`–`0F`; A.2 отвергает несовпадение), `00` («без защиты») не является keyset пресета, и у каждого keyset на карте свой счётчик (Annex A.1) — счётчиками владеет сервер: он сохраняет каждый израсходованный инкремент и пишет его в журнал аудита (`card_presets.json.audit.jsonl`). Список загружается при подключении PWA; экспорт/импорт JSON (принимает и старые плоские пресеты) — путь миграции из браузерного хранилища. «Карты» — **верхнеуровневая вкладка**. Поля формы сгруппированы в два блока с рамкой — **SCP80 (GSM 03.48, ETSI TS 102 225)** и **SCP81 (HTTP OTA)**; необязательное поле **ADM** находится в верхней строке, а keysets редактируются строками (по одной на номер, **Добавить keyset** / **Удалить keyset**). При подключении карты читается её EF.ICCID, и пресет с тем же ICCID автоматически выбирается во всех трёх видах SCP80; если подключённая карта не совпадает ни с одним пресетом — или выбранный пресет принадлежит другой карте — все три вида SCP80 показывают жёлтое предупреждение над выбором пресета. В шапке отображаются серые маркеры **SCP80** / **SCP81** и значок ключа на бейдже **ADM**, когда в пресете с ICCID подключённой карты эти поля заполнены.
|
Хранит сохранённые конфигурации карт (пресеты) в **серверном хранилище** — `~/.pysim-simple-server/card_presets.json` (переопределяется `--card-presets`). Пресет содержит криптографические ключи, настройки SPI, TAR-ы и счётчики повторов для SCP80-операций, необязательный ключ **ADM**, а также пару **PSK identity / PSK key** для слушателя SCP81 HTTP OTA. Пресет содержит **несколько keysets** — номер keyset задаётся нибблом `b8..b5` его KIc/KID (TS 102 225 §5.1.2/A.2): KIc и KID одного keyset должны нести один и тот же номер (`01`–`0F`; A.2 отвергает несовпадение), `00` («без защиты») не является keyset пресета, и у каждого keyset на карте свой счётчик (Annex A.1) — счётчиками владеет сервер: он сохраняет каждый израсходованный инкремент и пишет его в журнал аудита (`card_presets.json.audit.jsonl`). Список загружается при подключении фронтенда; экспорт/импорт JSON (принимает и старые плоские пресеты) — путь миграции из браузерного хранилища. «Карты» — **верхнеуровневая вкладка**. Поля формы сгруппированы в два блока с рамкой — **SCP80 (GSM 03.48, ETSI TS 102 225)** и **SCP81 (HTTP OTA)**; необязательное поле **ADM** находится в верхней строке, а keysets редактируются строками (по одной на номер, **Добавить keyset** / **Удалить keyset**). При подключении карты читается её EF.ICCID, и пресет с тем же ICCID автоматически выбирается во всех трёх видах SCP80; если подключённая карта не совпадает ни с одним пресетом — или выбранный пресет принадлежит другой карте — все три вида SCP80 показывают жёлтое предупреждение над выбором пресета. В шапке отображаются серые маркеры **SCP80** / **SCP81** и значок ключа на бейдже **ADM**, когда в пресете с ICCID подключённой карты эти поля заполнены.
|
||||||
|
|
||||||
| Поле | Описание |
|
| Поле | Описание |
|
||||||
|---|---|
|
|---|---|
|
||||||
@@ -563,7 +568,7 @@ Delivery PoR (SPI2 `01`) проще — карта возвращает PoR на
|
|||||||
|
|
||||||
Подключение к встроенному [`pysim-simple-server`](pysim_simple_server/) для работы с картой.
|
Подключение к встроенному [`pysim-simple-server`](pysim_simple_server/) для работы с картой.
|
||||||
|
|
||||||
> **Ограничение браузера:** если PWA раздаётся с публичного HTTPS-хоста, для доступа к локальному серверу (`http://127.0.0.1:8080`) нужны два условия: сервер должен отправлять `Access-Control-Allow-Private-Network: true` (pysim-simple-server ≥ 1.6.1 делает это автоматически), и браузеру должно быть разрешено обращаться к локальной сети — в Chrome/Edge/Vivaldi: Настройки сайта → Доступ к локальной сети → разрешить сайт (или подтвердить запрос). Без разрешения браузера запрос к `127.0.0.1` блокируется ещё до отправки preflight.
|
> **Ограничение браузера:** если фронтенд раздаётся с публичного HTTPS-хоста, для доступа к локальному серверу (`http://127.0.0.1:8080`) нужны два условия: сервер должен отправлять `Access-Control-Allow-Private-Network: true` (pysim-simple-server ≥ 1.6.1 делает это автоматически), и браузеру должно быть разрешено обращаться к локальной сети — в Chrome/Edge/Vivaldi: Настройки сайта → Доступ к локальной сети → разрешить сайт (или подтвердить запрос). Без разрешения браузера запрос к `127.0.0.1` блокируется ещё до отправки preflight.
|
||||||
|
|
||||||
### Файловый менеджер
|
### Файловый менеджер
|
||||||
|
|
||||||
@@ -715,33 +720,26 @@ SIMple — Progressive Web App. Можно установить для offline-
|
|||||||
|
|
||||||
## Совместимость версий
|
## Совместимость версий
|
||||||
|
|
||||||
| PWA (SIMple) | Сервер | Статус |
|
| фронтенд (SIMple) | Сервер | Статус |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| любая | та же мажорная | ✅ Совместимы |
|
| любая | та же мажорная | ✅ Совместимы |
|
||||||
| любая | старее мажорная | ❌ Сервер устарел — обновите сервер |
|
| любая | старее мажорная | ❌ Сервер устарел — обновите сервер |
|
||||||
| любая | новее мажорная | ⚠️ Сервер новее — обновите PWA |
|
| любая | новее мажорная | ⚠️ Сервер новее — обновите фронтенд |
|
||||||
|
|
||||||
PWA проверяет версию сервера при подключении через `GET /api/version` и сравнивает мажорную версию (например, PWA 3.x с сервером 3.x; сервер 2.x помечается как устаревший).
|
фронтенд проверяет версию сервера при подключении через `GET /api/version` и сравнивает мажорную версию (например, фронтенд 3.x с сервером 3.x; сервер 2.x помечается как устаревший).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Сервер (pysim-simple-server)
|
## Сервер (pysim-simple-server)
|
||||||
|
|
||||||
Встроенный Python-сервер оборачивает [pySim](https://osmocom.org/projects/pysim/wiki) и раздаёт как PWA (из `frontend/`), так и JSON API по `/api/*`.
|
Встроенный Python-сервер оборачивает [pySim](https://osmocom.org/projects/pysim/wiki) и раздаёт как фронтенд (из `frontend/`), так и JSON API по `/api/*`.
|
||||||
|
|
||||||
### Требования
|
|
||||||
|
|
||||||
- **Python 3.9+** с `pip`, и **Git** — приложение работает на 3.9+, но **SCP81 TLS-PSK-слушателю нужен Python 3.13+** (PSK-callback'и `ssl`).
|
|
||||||
- **Поддержка venv в Python** — `setup.sh` создаёт `.venv`; если создание завершается с `ensurepip is not available` (в Debian/Ubuntu модуль venv поставляется отдельно), установите его (`sudo apt install python3-venv`) и запустите `./setup.sh` снова. Неполный `.venv`, оставшийся от неудачного запуска, удаляется автоматически.
|
|
||||||
- **Смарт-картридер** (PC/SC или serial/FTDI) — предпочтителен PC/SC (`pcsc-lite` + `ccid` на Linux). У `pyscard` нет Linux-wheel, поэтому он собирается из исходников: поставьте и пакет с заголовками PC/SC (`libpcsclite-dev` в Debian/Ubuntu, `pcsc-lite-devel` в Fedora; в Arch они входят в `pcsclite`).
|
|
||||||
- **Windows** — используйте **Python 3.13 (настоятельно рекомендуется)**: у `pyscard` есть готовые wheel до 3.13, и SCP81 TLS-PSK-слушателю нужен 3.13+. На 3.14 wheel для `pyscard` и части зависимостей pysim отсутствуют, они собираются из исходников (нужны MSVC C++ Build Tools). 3.9–3.12 запускают приложение, но не SCP81 PSK-слушатель. SMPP-мост (`smpp.twisted3`) в Windows сознательно не ставится.
|
|
||||||
|
|
||||||
### Скрипты
|
### Скрипты
|
||||||
|
|
||||||
| Скрипт | Назначение |
|
| Скрипт | Назначение |
|
||||||
|--------|-------------|
|
|--------|-------------|
|
||||||
| `setup.sh` / `setup.bat` | Создаёт `.venv/` (или исправляет неполный), ставит pysim и сервер. Запускать один раз после клонирования или распаковки релизного пакета. |
|
| `setup.sh` / `setup.bat` | Создаёт `.venv/` (или исправляет неполный), ставит pysim и сервер. Запускать один раз после клонирования или распаковки релизного пакета. |
|
||||||
| `start.sh` / `start.bat` | Запускает сервер из venv (раздаёт PWA + API на `:8080`). |
|
| `start.sh` / `start.bat` | Запускает сервер из venv (раздаёт фронтенд + API на `:8080`). |
|
||||||
| `update.sh` / `update.bat` | Обновляет пакетную установку на месте: скачивает последний релизный пакет, заменяет файлы приложения и переустанавливает приложение в существующий `.venv` (pysim сохраняется). `--check`, `--version X.Y.Z`, `--zip FILE`, `--no-pip`, `--force`. |
|
| `update.sh` / `update.bat` | Обновляет пакетную установку на месте: скачивает последний релизный пакет, заменяет файлы приложения и переустанавливает приложение в существующий `.venv` (pysim сохраняется). `--check`, `--version X.Y.Z`, `--zip FILE`, `--no-pip`, `--force`. |
|
||||||
|
|
||||||
`start.sh` перед запуском проверяет доступ к PC/SC: если пользователь не может использовать `pcscd` (или служба не запущена), он печатает причину и рекомендации (группа/polkit) и завершается — сервер **больше не откатывается на последовательный ридер молча**. При наличии доступа сервер сам находит первый PC/SC-ридер; `start.bat` всегда передаёт `-p 0` (в Windows PC/SC встроен). Явный serial/FTDI-ридер по-прежнему возможен через `-d <устройство>` (проверка пропускается). Дополнительные аргументы передаются серверу, например `./start.sh --gsmtap` или `start.bat --gsmtap 10.0.0.5:4729`. Если PC/SC-ридер не найден, сервер сообщает об этом и завершается — подключите ридер и перезапустите (ридер без карты — по-прежнему нормальный старт без карты).
|
`start.sh` перед запуском проверяет доступ к PC/SC: если пользователь не может использовать `pcscd` (или служба не запущена), он печатает причину и рекомендации (группа/polkit) и завершается — сервер **больше не откатывается на последовательный ридер молча**. При наличии доступа сервер сам находит первый PC/SC-ридер; `start.bat` всегда передаёт `-p 0` (в Windows PC/SC встроен). Явный serial/FTDI-ридер по-прежнему возможен через `-d <устройство>` (проверка пропускается). Дополнительные аргументы передаются серверу, например `./start.sh --gsmtap` или `start.bat --gsmtap 10.0.0.5:4729`. Если PC/SC-ридер не найден, сервер сообщает об этом и завершается — подключите ридер и перезапустите (ридер без карты — по-прежнему нормальный старт без карты).
|
||||||
@@ -757,8 +755,8 @@ pysim-simple-server --http-port 8080
|
|||||||
```
|
```
|
||||||
|
|
||||||
В Windows используйте `setup.bat`: он ставит зависимости pysim из
|
В Windows используйте `setup.bat`: он ставит зависимости pysim из
|
||||||
`requirements-pysim.txt`, а сам pysim — с `--no-deps`, поэтому SMPP-мост (и его сборка
|
`requirements-pysim.txt`, а сам pysim — с `--no-deps`, поэтому необязательные
|
||||||
через MSVC) не подтягивается.
|
зависимости pysim не подтягиваются.
|
||||||
|
|
||||||
### Параметры CLI
|
### Параметры CLI
|
||||||
|
|
||||||
@@ -766,7 +764,7 @@ pysim-simple-server --http-port 8080
|
|||||||
|----------|-------------|
|
|----------|-------------|
|
||||||
| `--http-host` | Адрес привязки (по умолчанию `127.0.0.1`) |
|
| `--http-host` | Адрес привязки (по умолчанию `127.0.0.1`) |
|
||||||
| `--http-port` | Порт (по умолчанию `8080`) |
|
| `--http-port` | Порт (по умолчанию `8080`) |
|
||||||
| `--web-dir` | Каталог со статикой PWA (по умолчанию `<repo>/frontend`) |
|
| `--web-dir` | Каталог со статикой фронтенда (по умолчанию `<repo>/frontend`) |
|
||||||
| `-p` / `--pcsc-device` | Номер слота PC/SC |
|
| `-p` / `--pcsc-device` | Номер слота PC/SC |
|
||||||
| `-d` / `--device` | Путь к serial-устройству |
|
| `-d` / `--device` | Путь к serial-устройству |
|
||||||
| `--no-card-init` | Пропустить инициализацию карты (сохранить CAT-сессию) |
|
| `--no-card-init` | Пропустить инициализацию карты (сохранить CAT-сессию) |
|
||||||
@@ -816,5 +814,5 @@ pysim-simple-server --http-port 8080
|
|||||||
Сам pySim не входит ни в этот репозиторий, ни в релизные артефакты: `setup.sh` /
|
Сам pySim не входит ни в этот репозиторий, ни в релизные артефакты: `setup.sh` /
|
||||||
`setup.bat` устанавливают его из его upstream git-репозитория.
|
`setup.bat` устанавливают его из его upstream git-репозитория.
|
||||||
|
|
||||||
Встроенные сторонние компоненты (список операторов MCC/MNC и AES/DES-бандлы в
|
Встроенные сторонние компоненты (список операторов MCC/MNC и AES/DES-бандлы во
|
||||||
PWA) сохраняют свои лицензии MIT — см. [THIRD-PARTY-NOTICES](THIRD-PARTY-NOTICES).
|
фронтенде) сохраняют свои лицензии MIT — см. [THIRD-PARTY-NOTICES](THIRD-PARTY-NOTICES).
|
||||||
+133
-16
@@ -64,10 +64,11 @@ editing candidates.
|
|||||||
| EF.IMSI | 6F07 | TS 31.102 4.2.2 | 9 B | length byte + parity/type nibble ('9' odd / '1' even) + BCD | S3 | **3.34** |
|
| EF.IMSI | 6F07 | TS 31.102 4.2.2 | 9 B | length byte + parity/type nibble ('9' odd / '1' even) + BCD | S3 | **3.34** |
|
||||||
| EF.IST | 6F07 | TS 31.102 4.2.2 (SAIP) | 1–2 B | service bits | S6 | **3.34** |
|
| EF.IST | 6F07 | TS 31.102 4.2.2 (SAIP) | 1–2 B | service bits | S6 | **3.34** |
|
||||||
| EF.UST | 6F38 | TS 31.102 4.2.8 | X ≥ 1 | service bits | S6 | **3.34** |
|
| EF.UST | 6F38 | TS 31.102 4.2.8 | X ≥ 1 | service bits | S6 | **3.34** |
|
||||||
| EF.SST | 6F38 | TS 51.011 10.3.7 | X ≥ 1 | **2 bits/service** (allocated + activated) | S6 (pending) | decoder fixed; editor later |
|
| EF.SST | 6F38 | TS 51.011 10.3.7 | X ≥ 1 | **2 bits/service** (allocated + activated) | S6 | **shipped** — tri-state editor (deleted / allocated-off / on) |
|
||||||
| EF.EST | 6F56 | TS 31.102 4.2.47 | X ≥ 1 | enabled-service bits (3 services) | S6 | **3.34** |
|
| EF.EST | 6F56 | TS 31.102 4.2.47 | X ≥ 1 | enabled-service bits (3 services) | S6 | **3.34** |
|
||||||
| EF.LI | 6F05 | TS 31.102 4.2.1 | 2n | ISO 639 letters (2 B each) | S6 | 3.35 |
|
| EF.LI | 6F05 | TS 31.102 4.2.1 | 2n | ISO 639 letters (2 B each) | S6 | 3.35 |
|
||||||
| EF.LP / EF.PL | 6F05 / 2F05 | TS 51.011 10.3.1 / 10.1.2 | 1–n | **1 byte per language** (DCS language coding) | S6 | 3.35 |
|
| EF.LP | 6F05 | TS 51.011 10.3.1 | n | **1 byte per language** (SMS 7-bit) | S6 | 3.35 |
|
||||||
|
| EF.PL | 2F05 | TS 102 221 13.3 | 2n | ISO 639 letters (2 B each, like LI) | S6 | 3.35 |
|
||||||
| EF.SPN | 6F46 | TS 31.102 4.2.12 | 17 B | condition byte + Annex A text | S2 | **3.33** |
|
| EF.SPN | 6F46 | TS 31.102 4.2.12 | 17 B | condition byte + Annex A text | S2 | **3.33** |
|
||||||
| EF.PLMNsel | 6F30 | TS 51.011 10.3.4 | 3n (n ≥ 8) | PLMN list | S4 | **3.34** |
|
| EF.PLMNsel | 6F30 | TS 51.011 10.3.4 | 3n (n ≥ 8) | PLMN list | S4 | **3.34** |
|
||||||
| EF.FPLMN | 6F7B | TS 31.102 4.2.16 | 3n (n ≥ 4) | PLMN list (FFFFFF holes anywhere) | S4 | **3.34** |
|
| EF.FPLMN | 6F7B | TS 31.102 4.2.16 | 3n (n ≥ 4) | PLMN list (FFFFFF holes anywhere) | S4 | **3.34** |
|
||||||
@@ -82,7 +83,8 @@ editing candidates.
|
|||||||
| EF.ACC | 6F78 | TS 31.102 4.2.15 | 2 B | class bits (b8…b1 = 15…8 / 7…0; ACC 10 written 0) | S6 | **3.34** |
|
| EF.ACC | 6F78 | TS 31.102 4.2.15 | 2 B | class bits (b8…b1 = 15…8 / 7…0; ACC 10 written 0) | S6 | **3.34** |
|
||||||
| EF.PHASE | 6FAE | TS 51.011 10.3.19 | 1 B | phase id (00/02/03) | S1 | **3.34** |
|
| EF.PHASE | 6FAE | TS 51.011 10.3.19 | 1 B | phase id (00/02/03) | S1 | **3.34** |
|
||||||
| EF.HPPLMN | 6F31 | TS 31.102 4.2.6 | 1 B | search-period multiplier | S1 | **3.34** |
|
| EF.HPPLMN | 6F31 | TS 31.102 4.2.6 | 1 B | search-period multiplier | S1 | **3.34** |
|
||||||
| EF.AD | 6FAD | TS 31.102 4.2.18 | 4+X | op-mode byte enum, info, MNC length (may be 0 or absent) | S1 | **3.34** |
|
| EF.AD (SIM) | 6FAD | TS 51.011 10.3.18 | 3+X | op-mode byte + additional info (no MNC length) | S1 | **3.34** |
|
||||||
|
| EF.AD (USIM) | 6FAD | TS 31.102 4.2.18 | 4+X | op-mode byte + info + MNC length (may be 0) | S1 | **3.34** |
|
||||||
| EF.CBMI | 6F45 | TS 31.102 4.2.14 | 2n | CB message ids | S6 | **3.34** |
|
| EF.CBMI | 6F45 | TS 31.102 4.2.14 | 2n | CB message ids | S6 | **3.34** |
|
||||||
| EF.CBMID | 6F48 | TS 31.102 4.2.20 | 2n | CB message ids | S6 | **3.34** |
|
| EF.CBMID | 6F48 | TS 31.102 4.2.20 | 2n | CB message ids | S6 | **3.34** |
|
||||||
| EF.CBMIR | 6F50 | TS 31.102 4.2.22 | 4n | CB id ranges | S6 | **3.34** |
|
| EF.CBMIR | 6F50 | TS 31.102 4.2.22 | 4n | CB id ranges | S6 | **3.34** |
|
||||||
@@ -126,6 +128,28 @@ must not be edited from a workbench without a dedicated flow (Keys, DCK,
|
|||||||
network parameters, START-HFN). Nothing in the survey forces a new shape
|
network parameters, START-HFN). Nothing in the survey forces a new shape
|
||||||
beyond S9/S10.
|
beyond S9/S10.
|
||||||
|
|
||||||
|
### 2.4 SIM ↔ USIM format divergence
|
||||||
|
|
||||||
|
Some features are represented by a different file (or the same FID with a
|
||||||
|
different structure) in the SIM (DF.GSM, TS 51.011) and USIM (ADF.USIM,
|
||||||
|
TS 31.102) applications. Three groups:
|
||||||
|
|
||||||
|
| group | SIM | USIM | purpose |
|
||||||
|
|---|---|---|---|
|
||||||
|
| A — same FID + name, different format | EF.ECC 6FB7 transparent 3n (code+ESC) | EF.ECC 6FB7 linear X+4 (code+alpha+ESC) | emergency codes |
|
||||||
|
| A | EF.AD 6FAD 3+X (no MNC length) | EF.AD 6FAD 4+X (+ MNC length) | administrative data |
|
||||||
|
| B — same FID, different name + format | EF.SST 6F38 (2-bit) | EF.UST 6F38 (1-bit) | service table |
|
||||||
|
| B | EF.LP 6F05 (1 B/lang) | EF.LI 6F05 (2 B/lang) | language preference |
|
||||||
|
| C — different FID + name + format | EF.PLMNsel 6F30 (3n, no AcT) | EF.PLMNwAcT 6F60 (5n, +AcT) | user PLMN selector |
|
||||||
|
| C | EF.Kc/KcGPRS 6F20/6F52 (9 B) | EF.Keys/KeysPS 6F08/6F09 (33 B) | ciphering keys |
|
||||||
|
| C | EF.CCP 6F3D (14 B) | EF.CCP2 6F4F (X≥15) | bearer capability |
|
||||||
|
|
||||||
|
Group A is the only one where the *name and FID both* collide, so those two
|
||||||
|
(EF.ECC, EF.AD) are what the DF-context resolver (finding 6) is for; groups B
|
||||||
|
and C are already disambiguated by name. EF.HPLMNwAcT (a list) vs the SIM
|
||||||
|
EF.HPPLMN (a 1-byte search period) is a related-but-different pair, and
|
||||||
|
OPLMNwAcT/OPL/PNN/EHPLMN/FPLMN/SPDI/IMG are USIM-only (no SIM counterpart).
|
||||||
|
|
||||||
## 3. Findings (defects and constraints the survey exposed)
|
## 3. Findings (defects and constraints the survey exposed)
|
||||||
|
|
||||||
1. **EF.BDN is X+15, not X+14.** The shared ADN codec assumes the last two
|
1. **EF.BDN is X+15, not X+14.** The shared ADN codec assumes the last two
|
||||||
@@ -133,10 +157,16 @@ beyond S9/S10.
|
|||||||
(`X+15`, TS 31.102 4.2.44). The read-only decoded view currently
|
(`X+15`, TS 31.102 4.2.44). The read-only decoded view currently
|
||||||
mis-parses BDN records, and the v3.33.0 encoder would corrupt them. The
|
mis-parses BDN records, and the v3.33.0 encoder would corrupt them. The
|
||||||
codec must take the layout (ADN vs BDN) and preserve the CMI pointer.
|
codec must take the layout (ADN vs BDN) and preserve the CMI pointer.
|
||||||
2. **EF.ECC has two layouts.** SIM: transparent 3n (TS 51.011 10.3.27);
|
2. **EF.ECC is two different files, not one file with two layouts.** They
|
||||||
USIM: linear fixed X+4 (TS 31.102 4.2.21). The decoder implements the SIM
|
share the FID `6FB7` **and the name `EF.ECC`**, but live in different DFs
|
||||||
layout only; on a USIM the alpha bytes are read as more codes. A
|
under different specs: DF.GSM — transparent 3n, code(2 BCD) + category
|
||||||
structure-aware decode is needed before an ECC editor.
|
(TS 51.011 10.3.27); ADF.USIM — linear fixed X+4, code(3 BCD) + alpha
|
||||||
|
(Annex A) + category (TS 31.102 4.2.21). The decoder implements the SIM
|
||||||
|
file only; on a USIM the alpha bytes are read as more codes. They also
|
||||||
|
need **different editors** (a plain code list vs a record form with a text
|
||||||
|
alpha), so they are two registry entries, resolved by the DF context —
|
||||||
|
not a name/FID lookup, which is ambiguous here (the name *and* the FID
|
||||||
|
collide).
|
||||||
3. **S8 is not one shape.** PNN (flat tag+length+value TLVs) is the simple
|
3. **S8 is not one shape.** PNN (flat tag+length+value TLVs) is the simple
|
||||||
case. DIR/ARR/PBR nest TLVs and cross-reference other files; they need
|
case. DIR/ARR/PBR nest TLVs and cross-reference other files; they need
|
||||||
either a tree editor or a structured read-only view first.
|
either a tree editor or a structured read-only view first.
|
||||||
@@ -150,7 +180,13 @@ beyond S9/S10.
|
|||||||
6. **FID collisions across DFs** (e.g. 6F02 = EF.IMPI in ISIM and EF.OCST in
|
6. **FID collisions across DFs** (e.g. 6F02 = EF.IMPI in ISIM and EF.OCST in
|
||||||
USIM) make the fid-only decoder fallback unsafe; the name-based lookup is
|
USIM) make the fid-only decoder fallback unsafe; the name-based lookup is
|
||||||
the primary one and encoders should not be fid-only where a collision is
|
the primary one and encoders should not be fid-only where a collision is
|
||||||
possible.
|
possible. **Name collisions across DFs** also exist — EF.ECC and EF.AD
|
||||||
|
(DF.GSM vs ADF.USIM) — where both the name *and* the FID collide. The
|
||||||
|
specs key files by **FID within their DF**, not by symbolic name (names are
|
||||||
|
a derived display label), so the resolver must be **(DF, FID)-first**: the
|
||||||
|
parent DF + FID is the authoritative key, and the name is only a label.
|
||||||
|
The FS check in the Services editor follows the same rule — it probes a
|
||||||
|
(DF, FID), never a name.
|
||||||
7. **Text coding is per file.** Annex A (TS 102 221) for SPN/ADN/SUUME;
|
7. **Text coding is per file.** Annex A (TS 102 221) for SPN/ADN/SUUME;
|
||||||
the Network Name IE (TS 24.008 10.5.3.5a) for PNN, where the first byte is
|
the Network Name IE (TS 24.008 10.5.3.5a) for PNN, where the first byte is
|
||||||
`0x80 | spare` for packed GSM 7-bit and `0x90` for UCS-2 — the real-card
|
`0x80 | spare` for packed GSM 7-bit and `0x90` for UCS-2 — the real-card
|
||||||
@@ -158,8 +194,8 @@ beyond S9/S10.
|
|||||||
8. **EF.SST is a 2-bit table** (TS 51.011 10.3.7): 4 services per byte, first
|
8. **EF.SST is a 2-bit table** (TS 51.011 10.3.7): 4 services per byte, first
|
||||||
bit of the pair = allocated, second = activated. The shared 1-bit
|
bit of the pair = allocated, second = activated. The shared 1-bit
|
||||||
UST/EST codec was used for it, so SIM service tables were both misread and
|
UST/EST codec was used for it, so SIM service tables were both misread and
|
||||||
miswritten; the decoder is fixed in v3.34.0, editing waits for an
|
miswritten; the decoder is fixed in v3.34.0, and the tri-state editor
|
||||||
allocated/activated UI.
|
(deleted / allocated-off / on) shipped with the DF-context work.
|
||||||
9. **EF.ACC class numbering** (TS 31.102 4.2.15): byte 1 b8…b1 = classes 15…8
|
9. **EF.ACC class numbering** (TS 31.102 4.2.15): byte 1 b8…b1 = classes 15…8
|
||||||
and byte 2 = 7…0 (class 10 is written as 0); the old decoder counted from
|
and byte 2 = 7…0 (class 10 is written as 0); the old decoder counted from
|
||||||
class 0 at the MSB. Fixed in v3.34.0.
|
class 0 at the MSB. Fixed in v3.34.0.
|
||||||
@@ -168,9 +204,11 @@ beyond S9/S10.
|
|||||||
or absent (3-byte SIM form). The decoder and encoder follow this now.
|
or absent (3-byte SIM form). The decoder and encoder follow this now.
|
||||||
11. **The IMSI parity/type nibble** (TS 24.008 10.5.1.4) is 9 for an odd digit
|
11. **The IMSI parity/type nibble** (TS 24.008 10.5.1.4) is 9 for an odd digit
|
||||||
count and 1 for an even one; the first encoder always wrote 9.
|
count and 1 for an even one; the first encoder always wrote 9.
|
||||||
12. **SIM EF.LP/EF.PL use the 1-byte DCS language coding**, unlike the USIM
|
12. **The SIM/USIM language files differ in width.** EF.LP (SIM, DF.GSM,
|
||||||
EF.LI's 2-byte ISO 639 letters — the shared decoder is wrong for the SIM
|
6F05) is 1 byte per language (SMS 7-bit); EF.LI (USIM, ADF.USIM, 6F05) is
|
||||||
files (queued with the language work).
|
the 2-byte ISO 639 pair; EF.PL (MF, 2F05) is 2 bytes, like LI — not 1
|
||||||
|
byte. The shared decoder is wrong for EF.LP only (queued with the
|
||||||
|
language work).
|
||||||
13. **EF.SMSP's parameter indicators are a five-bit presence map with inverted
|
13. **EF.SMSP's parameter indicators are a five-bit presence map with inverted
|
||||||
meaning** (TS 31.102 §4.2.27: bit 1 DA, 2 SCA, 3 PID, 4 DCS, 5 VP; bit = 1
|
meaning** (TS 31.102 §4.2.27: bit 1 DA, 2 SCA, 3 PID, 4 DCS, 5 VP; bit = 1
|
||||||
means *absent*; bits 6–8 reserved = 1). The decoder had the bits shifted
|
means *absent*; bits 6–8 reserved = 1). The decoder had the bits shifted
|
||||||
@@ -190,7 +228,7 @@ beyond S9/S10.
|
|||||||
(filterable bit grid), ACC, PHASE, HPPLMN, AD, the PLMN-only lists
|
(filterable bit grid), ACC, PHASE, HPPLMN, AD, the PLMN-only lists
|
||||||
(PLMNsel/FPLMN/EHPLMN), ICCID/IMSI (behind a confirmation) and
|
(PLMNsel/FPLMN/EHPLMN), ICCID/IMSI (behind a confirmation) and
|
||||||
CBMI/CBMID/CBMIR. Decoder fixes: ACC class numbering, the AD operation
|
CBMI/CBMID/CBMIR. Decoder fixes: ACC class numbering, the AD operation
|
||||||
mode / MNC-length rules, the SST 2-bit layout (read-only for now) and the
|
mode / MNC-length rules, the SST 2-bit layout and the
|
||||||
IMSI parity nibble.
|
IMSI parity nibble.
|
||||||
- **v3.35.0 (shipped)** — EF.SMSP as the first S10 dedicated builder: the full
|
- **v3.35.0 (shipped)** — EF.SMSP as the first S10 dedicated builder: the full
|
||||||
parameter form (alpha; TP-DA and TS-SCA with their own length conventions
|
parameter form (alpha; TP-DA and TS-SCA with their own length conventions
|
||||||
@@ -201,8 +239,10 @@ beyond S9/S10.
|
|||||||
- **v3.36.0** — LI/LP/PL (the SIM files need the 1-byte DCS language coding
|
- **v3.36.0** — LI/LP/PL (the SIM files need the 1-byte DCS language coding
|
||||||
and a name-dependent decoder), GID1/GID2 (the clause states no coding —
|
and a name-dependent decoder), GID1/GID2 (the clause states no coding —
|
||||||
verify TS 31.101 first), SPDI (the A3 > 80 wrapper), OPL (PNN record
|
verify TS 31.101 first), SPDI (the A3 > 80 wrapper), OPL (PNN record
|
||||||
select), ECC (SIM and USIM — needs a structure-aware decode), SUUME,
|
select), ECC (**two files, two editors** — DF.GSM transparent code list vs
|
||||||
IMPI/DOMAIN/IMPU, ACL; EXT1–EXT5 as a read-only structured view.
|
ADF.USIM record form with an alpha; needs the DF-context resolver from
|
||||||
|
finding 6), SUUME, IMPI/DOMAIN/IMPU, ACL; EXT1–EXT5 as a read-only
|
||||||
|
structured view.
|
||||||
- **Postponed by decision (read-only; do not implement without confirmation)**
|
- **Postponed by decision (read-only; do not implement without confirmation)**
|
||||||
— EF.DIR, EF.ARR, EF.SMS, EF.PBR: they rarely need manual changes and the
|
— EF.DIR, EF.ARR, EF.SMS, EF.PBR: they rarely need manual changes and the
|
||||||
decoded view is enough.
|
decoded view is enough.
|
||||||
@@ -222,3 +262,80 @@ beyond S9/S10.
|
|||||||
- **No capability by accident**: a file without an encoder must never show
|
- **No capability by accident**: a file without an encoder must never show
|
||||||
the editor (gating test), and a schema/label drift guard keeps the editors
|
the editor (gating test), and a schema/label drift guard keeps the editors
|
||||||
and decoders aligned.
|
and decoders aligned.
|
||||||
|
|
||||||
|
## 6. Cross-file "Services" editor (proposal)
|
||||||
|
|
||||||
|
A file-agnostic service editor for the Card reader tab, in addition to the
|
||||||
|
per-file decoded editors (which stay independent). It presents the four
|
||||||
|
service tables side by side and writes them back consistently. EF.SST also
|
||||||
|
has its own per-file tri-state editor (deleted / allocated-off / on); the
|
||||||
|
Services editor's SIM column only toggles on / allocated-off and never
|
||||||
|
de-allocates.
|
||||||
|
|
||||||
|
- **Scope**: EF.SST (DF.GSM, 2 bits/service: allocated + activated), EF.UST
|
||||||
|
(ADF.USIM, 1 bit), EF.EST (ADF.USIM, a 3-bit *enabled* subset of UST:
|
||||||
|
FDN/BDN/ACL = UST 2/6/35) and EF.IST (ADF.ISIM, 1 bit, IMS services).
|
||||||
|
- **The numbering does not align across files**: SST and UST (and EST/IST)
|
||||||
|
use different service-number -> name maps, so the editor is driven by a
|
||||||
|
curated **feature cross-reference** (by name), not by service number. Each
|
||||||
|
feature row belongs to *SIM-only*, *USIM-only*, *ISIM-only* or *shared*
|
||||||
|
(e.g. FDN = SST 3 / UST 2 / EST 1; SMS = SST 4 / UST 10). The table is
|
||||||
|
spec-derived and pinned by a test, like the event/TR vectors.
|
||||||
|
- **Card-aware and probed**: on open, the editor resolves the four files on
|
||||||
|
the current card (the file-manager presence state, or a targeted probe) and
|
||||||
|
renders a column per file that exists; an absent file (a card without ISIM
|
||||||
|
has no EF.IST) hides that column and its feature rows entirely. Presence is
|
||||||
|
cached per `card_session`, like the EF.ARR copies.
|
||||||
|
- **Semantics**: "enabled" = UST bit set = SST *allocated* **and** *activated*.
|
||||||
|
SST's intermediate "allocated but not activated" state has no UST
|
||||||
|
equivalent, so the SIM column is a per-feature toggle between *on*
|
||||||
|
(allocated + activated) and *off* (allocated but not activated) — the
|
||||||
|
Services editor never de-allocates, so a card-wide change keeps the SIM
|
||||||
|
able to re-enable the service. Explicit *deletion* (clearing the allocation
|
||||||
|
bit) is the per-file EF.SST editor's job: its grid is a tri-state per
|
||||||
|
service (deleted / allocated-off / on).
|
||||||
|
- **Write, per file**: the merged form carries the intended bytes for every
|
||||||
|
visible file, but writing is explicit and separate — one button per file
|
||||||
|
("Save to EF.SST", "Save to EF.UST", "Save to EF.EST", "Save to EF.IST").
|
||||||
|
Each button writes only that file via `/api/write`; an unchanged file is
|
||||||
|
skipped and the panel reports each write's result (there is no atomic
|
||||||
|
cross-file write).
|
||||||
|
- **Filesystem check** (implemented, step 4b): many services declare a DF or EF
|
||||||
|
(ADN -> EF.ADN 6F3A, SPN -> EF.SPN 6F46, GSM Access -> DF.GSM-ACCESS 5F3B,
|
||||||
|
GPRS -> EF.KcGPRS, …); a second curated table (`SERVICES_FS_MAP`, keyed by
|
||||||
|
feature name) maps a feature to the **symbolic files** its `Filesystem` cell
|
||||||
|
declares, resolved through the standard file list's `(name, root) -> path`
|
||||||
|
index — path-exact, so the same FID under another DF (EF.PBR/EF.SAI both
|
||||||
|
4F30, EF.SST/EF.UST both 6F38) no longer explains a service. A **Check
|
||||||
|
filesystem** action probes the file-manager tree once
|
||||||
|
(`pysimFsProbeAll`) and flags mismatches — a declared service whose file is
|
||||||
|
absent, or a present file whose service is not declared in any checked tree;
|
||||||
|
a declared feature the standard list cannot resolve is reported separately as
|
||||||
|
*unchecked*. Services that map to a capability rather than a file (CHV1
|
||||||
|
disable, Proactive SIM, …) carry no file check. The declarations are read
|
||||||
|
per application (SST -> MF, UST/EST -> ADF.USIM, IST -> ADF.ISIM) and a
|
||||||
|
USIM/ISIM role may resolve into the common DF.TELECOM subtree (the MF tree),
|
||||||
|
except the GSM application's mapped copies (EF.ADN/EF.EXT1/EF.ECCP, TS 31.102
|
||||||
|
4.5). The follow-ups are the `ADF.USIM-NON-IMSI` root alias and the files
|
||||||
|
missing from the standard list (`DF.GRAPHICS`/`EF.IMG`, the DF.TELECOM
|
||||||
|
DF.SoLSA/DF.MExE, EF.3GPPPSDATAOFFservicelist) — `docs/services-editor.md`
|
||||||
|
§B.0. The last report is kept in memory (it carries its resolved paths) and
|
||||||
|
dropped with the cached reads on a card-session change.
|
||||||
|
|
||||||
|
### 6.1 Candidates for the same cross-application treatment
|
||||||
|
|
||||||
|
The cross-file pattern of the Services editor generalises to the other
|
||||||
|
SIM/USIM feature pairs (group C in §2.4), where one operator action should
|
||||||
|
update both applications consistently:
|
||||||
|
|
||||||
|
- **PLMN selectors** — EF.PLMNsel (SIM, no AcT) ↔ EF.PLMNwAcT (USIM, +AcT):
|
||||||
|
a shared PLMN list, the SIM side dropping the AcT and the USIM side
|
||||||
|
re-applying it (AcT is not expressible on the SIM side).
|
||||||
|
- **Language preferences** — EF.LP (SIM, 1 B/lang) ↔ EF.LI (USIM, 2 B/lang)
|
||||||
|
↔ EF.PL (MF, 2 B/lang): a shared ordered language list re-encoded per file.
|
||||||
|
- **Bearer capabilities** — EF.CCP (SIM) ↔ EF.CCP2 (USIM): a shared
|
||||||
|
bearer-capability record re-encoded per file's layout.
|
||||||
|
|
||||||
|
None of these are committed; they are recorded here so the DF-context resolver
|
||||||
|
and the per-file encoders are shaped with them in mind (group A/C files must
|
||||||
|
not be fid-only).
|
||||||
@@ -0,0 +1,447 @@
|
|||||||
|
# Services editor — data tables (draft)
|
||||||
|
|
||||||
|
> **Status: DRAFT for human review — data only.** No code or UI has been written
|
||||||
|
> against these tables yet. They are a semantic merge of the four service lists
|
||||||
|
> from `frontend/index.html` (`EF_SST_SERVICES`, `EF_UST_SERVICES`,
|
||||||
|
> `EF_IST_SERVICES`, `EF_EST_SERVICES`) cross-referenced with the filesystem
|
||||||
|
> registry `docs/uicc/UICC_FILES.md` and the standard file tree
|
||||||
|
> `frontend/uicc_files.json`.
|
||||||
|
>
|
||||||
|
> Step 4a (the cross-file Services editor: §A's cross-reference, read, render,
|
||||||
|
> per-file save) is implemented against §A; the counts below were verified
|
||||||
|
> against the PWA's constants by `frontend/tests/services.test.js`. Step 4b
|
||||||
|
> (§B, the filesystem check) is implemented as well — see §B.0 for the
|
||||||
|
> representation and the path-exact `(name, root)` rule it uses.
|
||||||
|
|
||||||
|
**Counts:** SST **59** / UST **150** / IST **22** / EST **3** services →
|
||||||
|
**164 feature rows** (10 SIM-only, 85 USIM-only, 5 ISIM-only, 61 shared,
|
||||||
|
3 RFU/reserved). Reconciliation is exact; see Notes §4.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## A. Feature cross-reference
|
||||||
|
|
||||||
|
Columns: **SST #** (SIM, TS 51.011 §10.3.7, 2 bits/service: allocated + activated),
|
||||||
|
**UST #** (USIM, TS 31.102 §4.2.8, 1 bit), **IST #** (ISIM, TS 31.103 §4.2.7, 1 bit),
|
||||||
|
**EST #** (USIM Enabled Services Table, TS 31.102 §4.2.47). Class ∈
|
||||||
|
`SIM-only · USIM-only · ISIM-only · shared · RFU`. The **Filesystem** column is
|
||||||
|
deliverable B merged in (see §B for the legend and the full `capability`/`?` list).
|
||||||
|
|
||||||
|
Names are canonicalised; wording aliases are listed in Notes §1.
|
||||||
|
|
||||||
|
### A.1 Features present in the SIM list (SST), and their USIM/ISIM counterparts
|
||||||
|
|
||||||
|
| # | Feature | SST # | UST # | IST # | EST # | Class | Filesystem |
|
||||||
|
|---|---|---|---|---|---|---|---|
|
||||||
|
| 1 | CHV1 disable function | 1 | | | | SIM-only | capability |
|
||||||
|
| 2 | Local Phone Book (ADN) | 2 | 1 | | | shared | EF.ADN 6F3A (USIM: DF.PHONEBOOK 5F3A → EF.PBR 4F30) |
|
||||||
|
| 3 | Fixed Dialling Numbers (FDN) | 3 | 2 | | 1 | shared | EF.FDN 6F3B (+ EF.EXT2 6F4B) |
|
||||||
|
| 4 | Short Message Storage (SMS) | 4 | 10 | 6 | | shared | EF.SMS 6F3C |
|
||||||
|
| 5 | Advice of Charge (AoC) | 5 | 13 | | | shared | EF.ACM 6F39 · EF.ACMmax 6F37 · EF.PUCT 6F41 |
|
||||||
|
| 6 | Capability Configuration Parameters (CCP) | 6 | | | | SIM-only | EF.CCP 6F3D |
|
||||||
|
| 7 | Capability Configuration Parameters 2 (CCP2) | | 14 | | | USIM-only | EF.CCP2 6F4F |
|
||||||
|
| 8 | PLMN selector | 7 | | | | SIM-only | EF.PLMNsel 6F30 |
|
||||||
|
| 9 | MSISDN | 9 | 21 | | | shared | EF.MSISDN 6F40 |
|
||||||
|
| 10 | Extension 1 | 10 | | | | SIM-only | EF.EXT1 6F4A |
|
||||||
|
| 11 | Extension 2 | 11 | 3 | | | shared | EF.EXT2 6F4B |
|
||||||
|
| 12 | SMS Parameters (SMSP) | 12 | 12 | | | shared | EF.SMSP 6F42 |
|
||||||
|
| 13 | Last Number Dialled (LND) | 13 | | | | SIM-only | EF.LND 6F44 |
|
||||||
|
| 14 | Cell Broadcast Message Identifier (CBMI) | 14 | 15 | | | shared | EF.CBMI 6F45 |
|
||||||
|
| 15 | Group Identifier Level 1 (GID1) | 15 | 17 | | | shared | EF.GID1 6F3E |
|
||||||
|
| 16 | Group Identifier Level 2 (GID2) | 16 | 18 | | | shared | EF.GID2 6F3F |
|
||||||
|
| 17 | Service Provider Name (SPN) | 17 | 19 | | | shared | EF.SPN 6F46 |
|
||||||
|
| 18 | Service Dialling Numbers (SDN) | 18 | 4 | | | shared | EF.SDN 6F49 (+ EF.EXT3 6F4C) |
|
||||||
|
| 19 | Extension 3 | 19 | 5 | | | shared | EF.EXT3 6F4C |
|
||||||
|
| 20 | VGCS Group Identifier List | 21 | 57 | | | shared | EF.VGCS 6FB1 · EF.VGCSS 6FB2 |
|
||||||
|
| 21 | VBS Group Identifier List | 22 | 58 | | | shared | EF.VBS 6FB3 · EF.VBSS 6FB4 |
|
||||||
|
| 22 | enhanced Multi-Level Precedence and Pre-emption (eMLPP) | 23 | 24 | | | shared | EF.eMLPP 6FB5 |
|
||||||
|
| 23 | Automatic Answer for eMLPP (AAeM) | 24 | 25 | | | shared | EF.AAeM 6FB6 |
|
||||||
|
| 24 | Data download via SMS-CB | 25 | 29 | | | shared | capability (gate: EF.CBMID 6F48) |
|
||||||
|
| 25 | Data download via SMS-PP | 26 | 28 | | | shared | capability |
|
||||||
|
| 26 | Menu selection | 27 | | | | SIM-only | capability (menu title: EF.SUME 6F54) |
|
||||||
|
| 27 | Call control (by SIM/USIM) | 28 | 30 | | | shared | capability |
|
||||||
|
| 28 | Proactive SIM | 29 | | | | SIM-only | capability |
|
||||||
|
| 29 | Cell Broadcast Message Identifier Ranges (CBMIR) | 30 | 16 | | | shared | EF.CBMIR 6F50 |
|
||||||
|
| 30 | Barred Dialling Numbers (BDN) | 31 | 6 | | 2 | shared | EF.BDN 6F4D (+ EF.EXT4 6F4E/6F55) |
|
||||||
|
| 31 | Extension 4 | 32 | 7 | | | shared | EF.EXT4 6F4E (SIM) / 6F55 (USIM) |
|
||||||
|
| 32 | De-personalisation Control Keys (DCK) | 33 | 36 | | | shared | EF.DCK 6F2C |
|
||||||
|
| 33 | Co-operative Network List (CNL) | 34 | 37 | | | shared | EF.CNL 6F32 |
|
||||||
|
| 34 | Short Message Status Reports (SMSR) | 35 | 11 | 7 | | shared | EF.SMSR 6F47 |
|
||||||
|
| 35 | Network's indication of alerting in the MS (NIA) | 36 | 56 | | | shared | EF.NIA 6F51 (SIM) / 6FD3 (USIM) |
|
||||||
|
| 36 | MO-SMS control (by SIM/USIM) | 37 | 31 | | | shared | capability |
|
||||||
|
| 37 | GPRS | 38 | | | | SIM-only | EF.KcGPRS 6F52 (+ DF.GSM 7F20) |
|
||||||
|
| 38 | Image (IMG) | 39 | 22 | | | shared | DF.GRAPHICS 5F50 → EF.IMG 4F20 |
|
||||||
|
| 39 | SoLSA | 40 | 23 | | | shared | DF.SoLSA 5F70 → EF.SAI 4F30 · EF.SLL 4F31 |
|
||||||
|
| 40 | USSD string data object supported in Call Control | 41 | | | | SIM-only | capability |
|
||||||
|
| 41 | RUN AT COMMAND command | 42 | 32 | | | shared | capability |
|
||||||
|
| 42 | User controlled PLMN selector with Access Technology | 43 | 20 | | | shared | EF.PLMNwAcT 6F60 |
|
||||||
|
| 43 | Operator controlled PLMN selector with Access Technology | 44 | 42 | | | shared | EF.OPLMNwAcT 6F61 |
|
||||||
|
| 44 | HPLMN selector with Access Technology | 45 | 43 | | | shared | EF.HPLMNwAcT 6F62 |
|
||||||
|
| 45 | CPBCCH Information | 46 | 39 | | | shared | EF.CPBCCH 6F63 (SIM) / 4F63 (USIM) |
|
||||||
|
| 46 | Investigation Scan | 47 | 40 | | | shared | EF.InvScan 6F64 (SIM) / 4F64 (USIM) |
|
||||||
|
| 47 | Extended Capability Configuration Parameters (ECCP) | 48 | | | | SIM-only | EF.ECCP 6F4F |
|
||||||
|
| 48 | MExE | 49 | 41 | | | shared | DF.MExE 5F3C → EF.MExE-ST 4F40 |
|
||||||
|
| 49 | PLMN Network Name (PNN) | 51 | 45 | | | shared | EF.PNN 6FC5 |
|
||||||
|
| 50 | Operator PLMN List (OPL) | 52 | 46 | | | shared | EF.OPL 6FC6 |
|
||||||
|
| 51 | Mailbox Dialling Numbers (MBDN) | 53 | 47 | | | shared | EF.MBDN 6FC7 (+ EF.EXT6 6FC8) |
|
||||||
|
| 52 | Message Waiting Indication Status (MWIS) | 54 | 48 | | | shared | EF.MWIS 6FCA (+ EF.MBI 6FC9) |
|
||||||
|
| 53 | Call Forwarding Indication Status (CFIS) | 55 | 49 | | | shared | EF.CFIS 6FCB (+ EF.EXT7 6FCC) |
|
||||||
|
| 54 | Service Provider Display Information (SPDI) | 56 | 51 | | | shared | EF.SPDI 6FCD |
|
||||||
|
| 55 | Multimedia Messaging Service (MMS) | 57 | 52 | | | shared | EF.MMSN 6FCE (+ MMS set: EXT8/MMSICP/MMSUP/MMSUCP) |
|
||||||
|
| 56 | Extension 8 | 58 | 53 | | | shared | EF.EXT8 6FCF |
|
||||||
|
| 57 | MMS User Connectivity Parameters | 59 | 55 | | | shared | EF.MMSICP 6FD0 · EF.MMSUCP 6FD2 · EF.MMSUP 6FD1 |
|
||||||
|
|
||||||
|
### A.2 USIM-only, and USIM+ISIM shared features (no SIM entry)
|
||||||
|
|
||||||
|
| # | Feature | SST # | UST # | IST # | EST # | Class | Filesystem |
|
||||||
|
|---|---|---|---|---|---|---|---|
|
||||||
|
| 58 | Outgoing Call Information (OCI and OCT) | | 8 | | | USIM-only | EF.OCI 6F81 · EF.OCT 6F83 |
|
||||||
|
| 59 | Incoming Call Information (ICI and ICT) | | 9 | | | USIM-only | EF.ICI 6F80 · EF.ICT 6F82 |
|
||||||
|
| 60 | GSM Access | | 27 | | | USIM-only | DF.GSM-ACCESS 5F3B |
|
||||||
|
| 61 | Enabled Services Table | | 34 | | | USIM-only | EF.EST 6F56 |
|
||||||
|
| 62 | APN Control List (ACL) | | 35 | | 3 | USIM-only | EF.ACL 6F57 |
|
||||||
|
| 63 | GSM security context | | 38 | | | USIM-only | EF.Kc 4F20 · EF.KcGPRS 4F52 (DF.GSM-ACCESS 5F3B) |
|
||||||
|
| 64 | Extension 5 | | 44 | | | USIM-only | EF.EXT5 6F4E (USIM) |
|
||||||
|
| 65 | Call control on GPRS by USIM | | 54 | | | USIM-only | capability |
|
||||||
|
| 66 | Pseudonym | | 59 | | | USIM-only | EF.Pseudo 4F41 (DF.WLAN 5F40) |
|
||||||
|
| 67 | User Controlled PLMN selector for I-WLAN access | | 60 | | | USIM-only | EF.UPLMNWLAN 4F42 |
|
||||||
|
| 68 | Operator Controlled PLMN selector for I-WLAN access | | 61 | | | USIM-only | EF.OPLMNWLAN 4F43 |
|
||||||
|
| 69 | User controlled WSID list | | 62 | | | USIM-only | EF.UWSIDL 4F44 |
|
||||||
|
| 70 | Operator controlled WSID list | | 63 | | | USIM-only | EF.OWSIDL 4F45 |
|
||||||
|
| 71 | VGCS security | | 64 | | | USIM-only | EF.VGCSCA 6FD4 |
|
||||||
|
| 72 | VBS security | | 65 | | | USIM-only | EF.VBSCA 6FD5 |
|
||||||
|
| 73 | WLAN Reauthentication Identity | | 66 | | | USIM-only | EF.WRI 4F46 |
|
||||||
|
| 74 | Multimedia Messages Storage | | 67 | | | USIM-only | EF.MML 4F47 · EF.MMDF 4F48 (DF.MULTIMEDIA 5F3B) |
|
||||||
|
| 75 | Generic Bootstrapping Architecture (GBA) | | 68 | 2 | | shared | EF.GBABP 6FD6 (USIM) / 6FD5 (ISIM) · EF.GBANL 6FDA/6FD7 · EF.NAFKCA 6FDD |
|
||||||
|
| 76 | MBMS security | | 69 | | | USIM-only | EF.MSK 6FD7 · EF.MUK 6FD8 |
|
||||||
|
| 77 | Data download via USSD and USSD application mode | | 70 | | | USIM-only | capability |
|
||||||
|
| 78 | Equivalent HPLMN | | 71 | | | USIM-only | EF.EHPLMN 6FD9 |
|
||||||
|
| 79 | Additional TERMINAL PROFILE after UICC activation | | 72 | | | USIM-only | capability |
|
||||||
|
| 80 | Equivalent HPLMN Presentation Indication | | 73 | | | USIM-only | EF.EHPLMNPI 6FDB |
|
||||||
|
| 81 | Last RPLMN Selection Indication | | 74 | | | USIM-only | EF.LRPLMNSI 6FDC |
|
||||||
|
| 82 | OMA BCAST Smart Card Profile | | 75 | | | USIM-only | DF.BCAST 5F80 |
|
||||||
|
| 83 | GBA-based Local Key Establishment Mechanism | | 76 | 4 | | shared | capability |
|
||||||
|
| 84 | Terminal Applications | | 77 | | | USIM-only | capability |
|
||||||
|
| 85 | Service Provider Name Icon | | 78 | | | USIM-only | EF.SPNI 6FDE |
|
||||||
|
| 86 | PLMN Network Name Icon | | 79 | | | USIM-only | EF.PNNI 6FDF |
|
||||||
|
| 87 | Connectivity Parameters for USIM IP connections | | 80 | | | USIM-only | EF.NCP-IP 6FE2 |
|
||||||
|
| 88 | Home I-WLAN Specific Identifier List | | 81 | | | USIM-only | EF.HWSIDL 4F47 |
|
||||||
|
| 89 | I-WLAN Equivalent HPLMN Presentation Indication | | 82 | | | USIM-only | EF.WEHPLMNPI 4F48 |
|
||||||
|
| 90 | I-WLAN HPLMN Priority Indication | | 83 | | | USIM-only | EF.WHPI 4F49 |
|
||||||
|
| 91 | I-WLAN Last Registered PLMN | | 84 | | | USIM-only | EF.WLRPLMN 4F4A |
|
||||||
|
| 92 | EPS Mobility Management Information | | 85 | | | USIM-only | EF.EPSLOCI 6FE3 · EF.EPSNSC 6FE4 |
|
||||||
|
| 93 | Allowed CSG Lists and corresponding indications | | 86 | | | USIM-only | EF.ACSGL 4F81 · EF.CSGT 4F82 · EF.HNBN 4F83 |
|
||||||
|
| 94 | Call control on EPS PDN connection by USIM | | 87 | | | USIM-only | capability |
|
||||||
|
| 95 | HPLMN Direct Access | | 88 | | | USIM-only | EF.HPLMNDAI 4F4B |
|
||||||
|
| 96 | eCall Data | | 89 | | | USIM-only | EF.SDN 6F49 (+ EF.FDN 6F3B) |
|
||||||
|
| 97 | Operator CSG Lists and corresponding indications | | 90 | | | USIM-only | EF.OCSGL 4F84 · EF.OCSGT 4F85 · EF.OHNBN 4F86 |
|
||||||
|
| 98 | Support for SM-over-IP | | 91 | 8 | | shared | capability (ISIM-related: EF.PSISMSC 6FE5) |
|
||||||
|
| 99 | Support of CSG Display Control | | 92 | | | USIM-only | capability |
|
||||||
|
| 100 | Communication Control for IMS | | 93 | 9 | | shared | capability |
|
||||||
|
| 101 | Extended Terminal Applications | | 94 | | | USIM-only | capability |
|
||||||
|
| 102 | Support of UICC access to IMS | | 95 | 10 | | shared | capability |
|
||||||
|
| 103 | Non-Access Stratum configuration by USIM | | 96 | | | USIM-only | EF.NASCONFIG 6FE8 |
|
||||||
|
| 104 | PWS configuration by USIM | | 97 | | | USIM-only | EF.PWS 6FEC |
|
||||||
|
| 105 | URI support by UICC | | 99 | 11 | | shared | capability (URI files: EF.FDNURI 6FED · EF.BDNURI 6FEE · EF.SDNURI 6FEF) |
|
||||||
|
| 106 | Extended EARFCN support | | 100 | | | USIM-only | capability |
|
||||||
|
| 107 | ProSe | | 101 | | | USIM-only | DF.ProSe 5F90 → EF.PST 4F10 |
|
||||||
|
| 108 | USAT Application Pairing | | 102 | | | USIM-only | EF.IPS 6FF1 (+ EF.IAL 6FF0 · EF.IPD 6FF2) |
|
||||||
|
| 109 | Media Type support | | 103 | 12 | | shared | capability |
|
||||||
|
| 110 | IMS call disconnection cause | | 104 | 13 | | shared | capability |
|
||||||
|
| 111 | URI support for MO SHORT MESSAGE CONTROL | | 105 | 14 | | shared | capability |
|
||||||
|
| 112 | ePDG configuration Information support | | 106 | | | USIM-only | EF.ePDGSelection 6FF4 |
|
||||||
|
| 113 | ePDG configuration Information configured | | 107 | | | USIM-only | EF.ePDGId 6FF3 |
|
||||||
|
| 114 | ACDC support | | 108 | | | USIM-only | DF.ACDC 5FA0 → EF.ACDC_LIST 4F01 |
|
||||||
|
| 115 | MCPTT | | 109 | 15 | | shared | DF.MCS 5F3D → EF.MST 4F01 (USIM) |
|
||||||
|
| 116 | ePDG configuration Information for Emergency Service support | | 110 | | | USIM-only | EF.ePDGSelectionEm 6FF6 |
|
||||||
|
| 117 | ePDG configuration Information for Emergency Service configured | | 111 | | | USIM-only | EF.ePDGIdEm 6FF5 |
|
||||||
|
| 118 | eCall Data over IMS | | 112 | | | USIM-only | EF.SDNURI 6FEF (+ EF.FDNURI 6FED) |
|
||||||
|
| 119 | URI support for SMS-PP DOWNLOAD | | 113 | 16 | | shared | capability |
|
||||||
|
| 120 | From Preferred | | 114 | 17 | | shared | EF.FromPreferred 6FF7 |
|
||||||
|
| 121 | IMS configuration data | | 115 | 18 | | shared | EF.IMSConfigData 6FF8 |
|
||||||
|
| 122 | TV configuration | | 116 | | | USIM-only | EF.TVCONFIG 6FFB (+ DF.TV 5FB0 → EF.TVUSD) |
|
||||||
|
| 123 | 3GPP PS Data Off | | 117 | | | USIM-only | EF.3GPPPSDATAOFF 6FF9 |
|
||||||
|
| 124 | 3GPP PS Data Off Service List | | 118 | | | USIM-only | EF.3GPPPSDATAOFFservicelist 6FFA |
|
||||||
|
| 125 | V2X | | 119 | | | USIM-only | DF.V2X 5F3E → EF.VST 4F01 · EF.V2X_CONFIG 4F02 |
|
||||||
|
| 126 | XCAP Configuration Data | | 120 | 19 | | shared | EF.XCAPConfigData 6FFC |
|
||||||
|
| 127 | EARFCN list for MTC/NB-IOT UEs | | 121 | | | USIM-only | EF.EARFCNList 6FFD |
|
||||||
|
| 128 | 5GS Mobility Management Information | | 122 | | | USIM-only | DF.5GS 5FC0 → EF.5GS3GPPLOCI 4F01 · EF.5GSN3GPPLOCI 4F02 |
|
||||||
|
| 129 | 5G Security Parameters | | 123 | | | USIM-only | EF.5GAUTHKEYS 4F05 (+ EF.5GS3GPPNSC 4F03 · EF.5GSN3GPPNSC 4F04) |
|
||||||
|
| 130 | Subscription identifier privacy support | | 124 | | | USIM-only | capability (SUCI data: EF.SUCI_Calc_Info 4F07, gated by 125) |
|
||||||
|
| 131 | SUCI calculation by the USIM | | 125 | | | USIM-only | EF.SUCI_Calc_Info 4F07 |
|
||||||
|
| 132 | UAC Access Identities support | | 126 | | | USIM-only | EF.UAC_AIC 4F06 |
|
||||||
|
| 133 | Expect control plane-based Steering of Roaming information during initial registration in VPLMN | | 127 | | | USIM-only | capability (SOR data: EF.SOR-CMCI 4F0E, gated by 138) |
|
||||||
|
| 134 | Call control on PDU Session by USIM | | 128 | | | USIM-only | capability |
|
||||||
|
| 135 | 5GS Operator PLMN List | | 129 | | | USIM-only | EF.OPL5G 4F08 |
|
||||||
|
| 136 | Support for SUPI of type NSI or GLI or GCI | | 130 | | | USIM-only | EF.SUPI_NAI 4F09 |
|
||||||
|
| 137 | 3GPP PS Data Off separate Home and Roaming lists | | 131 | | | USIM-only | capability (extends EF.3GPPPSDATAOFFservicelist 6FFA) |
|
||||||
|
| 138 | Support for URSP by USIM | | 132 | | | USIM-only | EF.URSP 4F0B |
|
||||||
|
| 139 | 5G Security Parameters extended | | 133 | | | USIM-only | capability (extends EF.5GAUTHKEYS 4F05) |
|
||||||
|
| 140 | MuD and MiD configuration data | | 134 | 21 | | shared | EF.MuDMiDConfigData 6FFE |
|
||||||
|
| 141 | Support for Trusted non-3GPP access networks by USIM | | 135 | | | USIM-only | capability |
|
||||||
|
| 142 | Support for multiple records of NAS security context storage for multiple registration | | 136 | | | USIM-only | EF.5GS3GPPNSC 4F03 · EF.5GSN3GPPNSC 4F04 (2nd record) |
|
||||||
|
| 143 | Pre-configured CAG information list | | 137 | | | USIM-only | EF.CAG 4F0D |
|
||||||
|
| 144 | SOR-CMCI storage in USIM | | 138 | | | USIM-only | EF.SOR-CMCI 4F0E |
|
||||||
|
| 145 | 5G ProSe | | 139 | | | USIM-only | DF.5G_ProSe 5FF0 → EF.5G_PROSE_ST 4F01 |
|
||||||
|
| 146 | Storage of disaster roaming information in USIM | | 140 | | | USIM-only | EF.DRI 4F0F |
|
||||||
|
| 147 | Pre-configured eDRX parameters | | 141 | | | USIM-only | EF.5GSEDRX 4F10 |
|
||||||
|
| 148 | 5G NSWO support | | 142 | | | USIM-only | EF.5GNSWO_CONF 4F11 |
|
||||||
|
| 149 | PWS configuration for SNPN in USIM | | 143 | | | USIM-only | EF.PWS_SNPN 4F01 (DF.SNPN 5FE0) |
|
||||||
|
| 150 | Multiplier Coefficient for Higher Priority PLMN search via NG-RAN satellite access | | 144 | | | USIM-only | EF.MCHPPLMN 4F15 |
|
||||||
|
| 151 | K_AUSF derivation configuration | | 145 | | | USIM-only | EF.KAUSF_DERIVATION 4F16 |
|
||||||
|
| 152 | Network Identifier for SNPN (NID) | | 146 | | | USIM-only | EF.NID 4F02 (DF.SNPN 5FE0) |
|
||||||
|
| 153 | 5MBS UE pre-configuration | | 147 | | | USIM-only | DF.5MBSUECONFIG 5FF1 → EF.5MBSUECONFIG 4F01 |
|
||||||
|
| 154 | Operator Controlled Signal Threshold per Access Technology | | 148 | | | USIM-only | EF.OCST 6F02 |
|
||||||
|
| 155 | A2X | | 149 | | | USIM-only | DF.A2X 5F3F → EF.AST 4F01 |
|
||||||
|
| 156 | IMS Data Channel Indication | | 150 | | | USIM-only | EF.IMSDCI 6F0B |
|
||||||
|
|
||||||
|
### A.3 ISIM-only features
|
||||||
|
|
||||||
|
| # | Feature | SST # | UST # | IST # | EST # | Class | Filesystem |
|
||||||
|
|---|---|---|---|---|---|---|---|
|
||||||
|
| 153 | P-CSCF address | | | 1 | | ISIM-only | EF.P-CSCF 6F09 |
|
||||||
|
| 154 | HTTP Digest | | | 3 | | ISIM-only | capability |
|
||||||
|
| 155 | Support of P-CSCF discovery for IMS Local Break Out | | | 5 | | ISIM-only | capability (address store: EF.P-CSCF 6F09) |
|
||||||
|
| 156 | WebRTC URI | | | 20 | | ISIM-only | EF.WebRTCURI 6FFA |
|
||||||
|
| 157 | IMS Data Channel indication | | | 22 | | ISIM-only | EF.IMSDCI 6F0B |
|
||||||
|
|
||||||
|
### A.4 RFU / reserved (non-editable)
|
||||||
|
|
||||||
|
| # | Feature | SST # | UST # | IST # | EST # | Class | Filesystem |
|
||||||
|
|---|---|---|---|---|---|---|---|
|
||||||
|
| 158 | RFU | 8, 20 | 26, 98 | | | RFU | — |
|
||||||
|
| 159 | Reserved and shall be ignored | 50 | 50 | | | RFU | — |
|
||||||
|
| 160 | shall be set to 1 | | 33 | | | RFU | — |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## B. Feature → filesystem (FS check)
|
||||||
|
|
||||||
|
The FS column in §A is deliverable B, merged for readability. Legend:
|
||||||
|
|
||||||
|
- `EF.<name> <FID>` — the file the service declares (FID from `uicc_files.json`
|
||||||
|
or `UICC_FILES.md`; both agree unless noted).
|
||||||
|
- `DF.<name> <FID> → EF.<name> <FID>` — the service gates a whole DF; the
|
||||||
|
principal EF is shown. Companion files after `(+ …)` or `·`.
|
||||||
|
- `capability` — a procedural / terminal-side feature with no single file.
|
||||||
|
- `?` — no clear file found in either source; needs a human decision.
|
||||||
|
- `—` — RFU/reserved.
|
||||||
|
|
||||||
|
### B.0 Implemented check (`SERVICES_FS_MAP` + the Check filesystem action)
|
||||||
|
|
||||||
|
`frontend/index.html` carries the column as `SERVICES_FS_MAP`, a map keyed by
|
||||||
|
the §A feature name (the cross-reference's row identity) whose value is the
|
||||||
|
list of **symbolic file names the cell mentions**, in the cell's order:
|
||||||
|
|
||||||
|
- `EF.FDN 6F3B (+ EF.EXT2 6F4B)` → `['EF.FDN', 'EF.EXT2']`
|
||||||
|
- `DF.GRAPHICS 5F50 → EF.IMG 4F20` → `['DF.GRAPHICS', 'EF.IMG']` (a DF's own
|
||||||
|
name counts: the DF is the file the service gates)
|
||||||
|
- `EF.NIA 6F51 (SIM) / 6FD3 (USIM)` → `['EF.NIA']` — one name, a different
|
||||||
|
path per tree
|
||||||
|
- `capability` → `[]`; the parenthetical references a capability cell carries
|
||||||
|
(`gate: EF.CBMID 6F48`, `menu title: EF.SUME 6F54`, the URI set, `SUCI data`)
|
||||||
|
are informational — the service does not declare a file of its own. The
|
||||||
|
§A.4 RFU/reserved rows are not features and therefore have no entry.
|
||||||
|
|
||||||
|
The names are the standard file list's (`frontend/uicc_files.json`, built from
|
||||||
|
pySim by `pysim_simple_server/uicc_files.py`) `name` values, so the check can
|
||||||
|
resolve them **path-exactly**: a FID alone is ambiguous across applications
|
||||||
|
(`4F30` is EF.PBR *and* EF.SAI, `6F38` EF.SST *and* EF.UST, `6F4E` EF.EXT4
|
||||||
|
under DF.TELECOM *and* EF.EXT5 under ADF.USIM). `servicesPathIndexBuild()`
|
||||||
|
turns the list into a `NAME|ROOT → path` index (uppercased both sides; the one
|
||||||
|
duplicate — pySim models `EF.SUCI_Calc_Info` twice under ADF.USIM, in DF.5GS
|
||||||
|
`4F07` and in the eSIM profile package's DF.SAIP `4F01` — takes the list's
|
||||||
|
first entry, the DF.5GS one). `servicesPathIndex()` loads it once per page
|
||||||
|
through `uiccStdFilesLoad()`.
|
||||||
|
|
||||||
|
The **Check filesystem** button (Services pill, needs a card) reads the four
|
||||||
|
service tables, probes the file manager's tree once (`pysimFsProbeAll`; the
|
||||||
|
`/api/tree` model lists files without testing them, so a loaded-but-unprobed
|
||||||
|
tree proves nothing), collects the tree's **present paths** (every node with
|
||||||
|
`exists === true`, DFs included, as canonical `pysimFsNodePath` paths) and
|
||||||
|
compares the two sides:
|
||||||
|
|
||||||
|
- **declared per tree** — `servicesFsDeclaredTrees()`: EF.SST declares in the
|
||||||
|
**MF** tree, EF.UST/EF.EST in **ADF.USIM**, EF.IST in **ADF.ISIM** (the state
|
||||||
|
the editor shows: EF.SST only at *allocated + activated*). A tree whose
|
||||||
|
service table is not on the card declares nothing there (its declarations
|
||||||
|
are unknown, so it is not checked at all).
|
||||||
|
- `missing` — a declared feature whose resolved paths are absent, in §A order
|
||||||
|
then MF / ADF.USIM / ADF.ISIM; each entry carries the declaring `tree` and
|
||||||
|
the absent `paths` (in the cell's order).
|
||||||
|
- `extra` — a present path whose mapped features are declared in **none** of
|
||||||
|
the checked trees, sorted by path, with the path's own `tree`, its `role`
|
||||||
|
name and every mapping feature. A path **any** declared feature maps is
|
||||||
|
explained: several features share one file (DF.GSM-ACCESS across "GSM
|
||||||
|
Access"/"GSM security context", EXT2 across FDN/Extension 2, the 5G DFs'
|
||||||
|
`4F01`/`4F02`) and a declaration in one application may gate a file of
|
||||||
|
another (a UST service gating a DF.TELECOM DF — see below).
|
||||||
|
- `unchecked` — a declared feature with **no** path at all: the standard list
|
||||||
|
is missing the file, so its presence cannot be verified. Today:
|
||||||
|
`Image (IMG)` (`DF.GRAPHICS`/`EF.IMG` — pySim does not model DF.GRAPHICS),
|
||||||
|
`SoLSA` and `MExE` in the MF tree (the DF.TELECOM copies of DF.SoLSA/DF.MExE
|
||||||
|
are not in the list) and `3GPP PS Data Off Service List`
|
||||||
|
(EF.3GPPPSDATAOFFservicelist). Reported instead of guessed — the follow-up
|
||||||
|
is to add those files to `SUPPLEMENT` in `pysim_simple_server/uicc_files.py`.
|
||||||
|
|
||||||
|
**Resolution per declaring tree.** A role resolves in the declaring tree's
|
||||||
|
root first, then — for a USIM/ISIM declaration — in the **MF** tree, because
|
||||||
|
several UST-gated DFs are children of the common DF.TELECOM (TS 31.102 4.6.0:
|
||||||
|
DF.MULTIMEDIA `5F3B`, DF.MCS `5F3D`, DF.V2X `5F3E`, DF.A2X `5F3F`,
|
||||||
|
DF.GRAPHICS `5F50`), i.e. `MF/7F10/…` paths. The exceptions
|
||||||
|
(`SERVICES_FS_GSM_MAPPED`) are the files TS 31.102 4.5 lists as *mappings* of a
|
||||||
|
DF.PHONEBOOK file into DF.TELECOM "in case of a present GSM application" —
|
||||||
|
EF.ADN `6F3A`, EF.EXT1 `6F4A`, EF.ECCP `6F4F`: a USIM declaration's phonebook
|
||||||
|
is DF.PHONEBOOK + EF.PBR (ADR's `EF.ADN` is the SIM's file), so those are
|
||||||
|
never expected in a USIM/ISIM tree. A role the list knows for another root
|
||||||
|
only is skipped, not a failure — a feature's files are the union of its
|
||||||
|
applications' files (ADN = EF.ADN for the SIM, DF.PHONEBOOK + EF.PBR for the
|
||||||
|
USIM).
|
||||||
|
|
||||||
|
**Not covered (follow-ups):** `ADF.USIM-NON-IMSI` — the list mirrors the
|
||||||
|
ADF.USIM files under that root, but a card's tree names the application
|
||||||
|
`ADF.USIM-non-IMSI`, so a non-IMSI USIM card's UST/EST paths are not matched
|
||||||
|
yet (resolve it as an alias of ADF.USIM); and the registry gaps listed above.
|
||||||
|
|
||||||
|
The pure pieces (`servicesFsDeclaredTrees`, `servicesFsPresentPaths`,
|
||||||
|
`servicesPathIndexBuild`, `servicesFsRolePath`, `servicesFsReport`,
|
||||||
|
`servicesFsReportHtml`) are DOM/fetch-free and unit-tested; the last report is
|
||||||
|
kept in memory (it carries its resolved paths) and redrawn on a language
|
||||||
|
switch, and it is dropped with the cached reads on a card-session change.
|
||||||
|
|
||||||
|
### B.1 `capability` entries (no single file)
|
||||||
|
|
||||||
|
CHV1 disable · Data download via SMS-PP · Data download via SMS-CB (gate
|
||||||
|
EF.CBMID 6F48) · Menu selection (title EF.SUME 6F54) · Call control · Proactive
|
||||||
|
SIM · MO-SMS control · USSD string in Call Control · RUN AT COMMAND · Call
|
||||||
|
control on GPRS · Data download via USSD · Additional TERMINAL PROFILE ·
|
||||||
|
GBA-based Local Key Establishment · Terminal Applications · SM-over-IP (related
|
||||||
|
EF.PSISMSC 6FE5, ISIM) · CSG Display Control · Communication Control for IMS ·
|
||||||
|
Extended Terminal Applications · UICC access to IMS · URI support by UICC (URI
|
||||||
|
files EF.FDNURI/BDNURI/SDNURI) · Media Type support · IMS call disconnection
|
||||||
|
cause · URI for MO SHORT MESSAGE CONTROL · URI for SMS-PP DOWNLOAD · Call
|
||||||
|
control on EPS PDN · Call control on PDU Session · Subscription identifier
|
||||||
|
privacy (SUCI data in EF.SUCI_Calc_Info 4F07) · SOR expectation (data in
|
||||||
|
EF.SOR-CMCI 4F0E) · PS Data Off separate Home/Roaming (EF.3GPPPSDATAOFFservicelist
|
||||||
|
6FFA) · 5G Security Parameters extended (EF.5GAUTHKEYS 4F05) · Trusted non-3GPP
|
||||||
|
access · HTTP Digest · P-CSCF discovery Local Break Out (EF.P-CSCF 6F09).
|
||||||
|
|
||||||
|
### B.2 Resolved against TS 31.102 v18.10.0
|
||||||
|
|
||||||
|
The three initially-unresolved mappings were settled from the spec text
|
||||||
|
(`docs/uicc/ts_131102*.txt`):
|
||||||
|
|
||||||
|
- **eCall Data (89)** — no dedicated EF: the eCall test/reconfiguration
|
||||||
|
numbers live in **EF.SDN 6F49** ("If service n° 4 and/or n° 89 is available
|
||||||
|
this file shall be present … eCall test and reconfiguration numbers") and
|
||||||
|
EF.FDN 6F3B (the eCall procedures).
|
||||||
|
- **eCall Data over IMS (112)** — the eCall test/reconfiguration **URIs** live
|
||||||
|
in **EF.SDNURI 6FEF** (and EF.FDNURI 6FED).
|
||||||
|
- **EARFCN list for MTC/NB-IOT UEs (121)** — **EF.EARFCNList 6FFD** (§4.2.112,
|
||||||
|
whose clause title is literally "EARFCN list for MTC/NB-IOT UEs").
|
||||||
|
"Extended EARFCN support" (100) is a **capability** with no file.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Notes / open questions
|
||||||
|
|
||||||
|
### 1. Aliases used (case-insensitive exact name did not join these; merged manually)
|
||||||
|
|
||||||
|
| Canonical | SST wording | UST wording | IST wording |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Local Phone Book (ADN) | "Abbreviated Dialling Numbers (ADN)" (2) | "Local Phone Book" (1) | |
|
||||||
|
| SMS Parameters (SMSP) | "SMS Parameters" (12) | "Short Message Service Parameters (SMSP)" (12) | |
|
||||||
|
| Short Message Status Reports (SMSR) | "Short Message Status Reports" (35) | "…Status Reports (SMSR)" (11) | "…Status Reports (SMSR)" (7) |
|
||||||
|
| Network's indication of alerting in the MS (NIA) | "Network's indication of alerting in the MS" (36) | "…in the MS (NIA)" (56) | |
|
||||||
|
| Call control | "Call control" (28) | "Call Control by USIM" (30) | |
|
||||||
|
| MO-SMS control | "Mobile Originated Short Message control by SIM" (37) | "MO-SMS Control by USIM" (31) | |
|
||||||
|
| De-personalisation Control Keys (DCK) | "De-personalization Control Keys" (33) | "Depersonalisation Control Keys" (36) | |
|
||||||
|
| SoLSA | "SoLSA (Support of Local Service Area)" (40) | "Support of Localised Service Areas (SoLSA)" (23) | |
|
||||||
|
| eMLPP / MExE | "enhanced Multi-Level…" (23), "MExE" (49) | "Enhanced Multi-Level…" (24), "MexE" (41) | |
|
||||||
|
| PLMN selector w/ Access Technology (×3) | "…PLMN Selector with Access Technology" (43–45) | "…PLMN selector with Access Technology" (20/42/43) | |
|
||||||
|
| Extension 2 | "Extension2" (11) | "Extension 2" (3) | |
|
||||||
|
| SM-over-IP | | "Support for SM-over-IP" (91) | "Support for SM-over-IP including data download via SMS-PP as defined in TS 31.111 [31]" (8) |
|
||||||
|
| Communication Control for IMS | | "…by USIM" (93) | "…by ISIM" (9) |
|
||||||
|
| URI support for SMS-PP DOWNLOAD | | "…TS 31.111 [12]" (113) | "…TS 31.111 [31]" (16) |
|
||||||
|
|
||||||
|
The other shared rows joined on a case-insensitive exact name (e.g. "Service
|
||||||
|
Provider Name" SST 17 / UST 19, "MSISDN" SST 9 / UST 21, "Short Message Storage
|
||||||
|
(SMS)" SST 4 / UST 10 / IST 6).
|
||||||
|
|
||||||
|
### 2. Kept separate, flagged for review
|
||||||
|
|
||||||
|
- **CCP / CCP2 / ECCP.** SST carries two bearer-capability services — "Capability
|
||||||
|
Configuration Parameters (CCP)" (6, file EF.CCP 6F3D) and "Extended Capability
|
||||||
|
Configuration Parameters (ECCP)" (48, EF.ECCP 6F4F); UST carries one —
|
||||||
|
"Capability Configuration Parameters 2 (CCP2)" (14, EF.CCP2 6F4F). These were
|
||||||
|
kept as three rows. Note the structural mismatch: the USIM's CCP2 shares FID
|
||||||
|
**6F4F** with the SIM's **ECCP**, not with the SIM's CCP (6F3D), so a
|
||||||
|
CCP↔CCP2 merge is *not* obviously correct — decide whether CCP2 ≙ CCP or
|
||||||
|
CCP2 ≙ ECCP (or neither) before writing code.
|
||||||
|
|
||||||
|
### 3. FID / tree collisions to respect in the editor
|
||||||
|
|
||||||
|
- `6F4E` = EF.EXT4 in the GSM tree, EF.EXT5 in the USIM tree (UICC_FILES.md §2.3).
|
||||||
|
- `6F4F` = EF.ECCP (GSM) vs EF.CCP2 (USIM) — see §2.
|
||||||
|
- `6F07` = EF.IMSI (DF.GSM / ADF.USIM) vs EF.IST (ADF.ISIM).
|
||||||
|
- `6F38` = EF.SST (DF.GSM) vs EF.UST (ADF.USIM).
|
||||||
|
- `6FD7` = EF.MSK (USIM) vs EF.GBANL (ISIM).
|
||||||
|
- `EF.NIA` has different FIDs per tree: 6F51 (SIM) vs 6FD3 (USIM).
|
||||||
|
- `DF.GSM-ACCESS 5F3B` (ADF.USIM) vs `DF.MULTIMEDIA 5F3B` (DF.TELECOM) — same
|
||||||
|
FID, different parents; the child-DF summary (UICC_FILES.md §4.13) is the
|
||||||
|
authority for which DF a service gates.
|
||||||
|
|
||||||
|
The filesystem column already resolves these per-tree (e.g. EF.EXT4 shows both
|
||||||
|
FIDs); the editor's DF-context selection must do the same.
|
||||||
|
|
||||||
|
### 4. Reconciliation (completeness invariant)
|
||||||
|
|
||||||
|
- **SST 59:** 56 non-RFU service numbers (SST 1–59 minus {8,20,50}) spread over
|
||||||
|
the 57 rows of §A.1 (one of those rows, CCP2, carries no SST number) + 3
|
||||||
|
RFU/reserved slots (8, 20, 50) = 59. ✓
|
||||||
|
- **UST 150:** 46 shared-with-SIM + 15 shared-with-ISIM + 85 USIM-only (1 in
|
||||||
|
§A.1 = CCP2, 84 in §A.2) + 4 RFU/reserved (26, 33, 50, 98) = 150. ✓
|
||||||
|
- **IST 22:** 2 shared with SIM (SMS 6, SMSR 7) + 15 shared with USIM
|
||||||
|
(2,4,8,9,10,11,12,13,14,15,16,17,18,19,21) + 5 ISIM-only (1,3,5,20,22) = 22. ✓
|
||||||
|
- **EST 3:** FDN (1), BDN (2), ACL (3) — all present. ✓
|
||||||
|
|
||||||
|
The PWA's `EF_UST_SERVICES` constant previously stopped at **146**; it now
|
||||||
|
carries **147–150** (5MBS UE pre-configuration / Operator Controlled Signal
|
||||||
|
Threshold per Access Technology / A2X / IMS Data Channel Indication), matching
|
||||||
|
`UICC_FILES.md` §6.1 and TS 31.102 §4.2.8, so the tables above are complete.
|
||||||
|
|
||||||
|
### 5. Source-coverage gaps (the standard file list)
|
||||||
|
|
||||||
|
The Services editor is **card-only**: it probes the equipped card's service
|
||||||
|
tables by path and writes via `/api/write`. The **FS check** additionally
|
||||||
|
needs the *cardless* standard file list (`frontend/uicc_files.json`) to resolve
|
||||||
|
a feature's symbolic files to paths (`(name, root) -> path`, §B.0) — the card's
|
||||||
|
own tree supplies presence, the list supplies the paths. Part 1 of step 4b
|
||||||
|
added the service-gated files pySim's model omits to `SUPPLEMENT`
|
||||||
|
(`pysim_simple_server/uicc_files.py`): EF.LRPLMNSI, EF.IAL, EF.IPD,
|
||||||
|
EF.TVCONFIG, EF.3GPPPSDATAOFF, DF.MExE + its files, DF.SoLSA + EF.SAI/EF.SLL,
|
||||||
|
DF.BCAST, DF.ACDC + EF.ACDC_LIST, DF.TV, DF.A2X + EF.AST/EF.A2X_CONFIG.
|
||||||
|
|
||||||
|
Still missing from the list, and therefore reported as *unchecked* by the
|
||||||
|
check (a declared feature whose files have no path) rather than guessed:
|
||||||
|
|
||||||
|
- **DF.GRAPHICS 5F50 → EF.IMG 4F20** — the DF.TELECOM child (TS 31.102 4.6.0);
|
||||||
|
pySim's SIM model does not carry DF.GRAPHICS at all.
|
||||||
|
- **DF.SoLSA 5F70 / DF.MExE 5F3C under DF.TELECOM** — the list carries them
|
||||||
|
under ADF.USIM only, so an *SST* declaration of SoLSA (service 40) or MExE
|
||||||
|
(49) cannot be checked in the MF tree.
|
||||||
|
- **EF.3GPPPSDATAOFFservicelist 6FFA** — a pySim TODO.
|
||||||
|
|
||||||
|
A non-IMSI USIM (`ADF.USIM-NON-IMSI`) is mirrored in the list, but a card's
|
||||||
|
tree names the application `ADF.USIM-non-IMSI`; resolving the two as one root
|
||||||
|
is the follow-up (§B.0).
|
||||||
|
|
||||||
|
### 6. Deliberately unmapped
|
||||||
|
|
||||||
|
- "GSM Access" (UST 27) maps to **DF.GSM-ACCESS 5F3B** (UICC_FILES.md §4.13c
|
||||||
|
"DF_GSM-ACCESS — UST n°27"), not to DF.GSM 7F20 — DF.GSM is the classic SIM
|
||||||
|
tree and is not UST-gated. This corrects the DF.GSM hint in the drafting brief.
|
||||||
|
- EF.RPLMNAcT 6F65 (legacy, "deleted in Rel-6") has no current UST/SST service
|
||||||
|
and is intentionally absent.
|
||||||
|
- EF.ACC 6F78 (access-control class) and EF.Phase 6FAE are not service-gated in
|
||||||
|
either list, so they do not appear.
|
||||||
@@ -409,7 +409,7 @@
|
|||||||
<ul class="list-disc list-inside text-sm space-y-1 mb-3">
|
<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, …); таблица декодируется в браузере из тех же сырых байт (без серверного декодирования). Подсвеченная кнопка — текущий вид; нажатие любой из них (пере)читывает файл. «Декодировать» недоступна, если для файла нет клиентского декодера (проверка по имени/FID до чтения).</li>
|
<li><strong>Читать сырые данные / Декодировать</strong> — чтение выбранного файла в виде hex-дампа или таблицы декодированных полей (клиентские декодеры EF: IMSI, ICCID, SPN, списки PLMN, LOCI/PSLOCI/EPSLOCI, ADN/MSISDN, таблицы сервисов, SUME, …); таблица декодируется в браузере из тех же сырых байт (без серверного декодирования). Подсвеченная кнопка — текущий вид; нажатие любой из них (пере)читывает файл. «Декодировать» недоступна, если для файла нет клиентского декодера (проверка по имени/FID до чтения).</li>
|
||||||
<li><strong>Редактировать сырые данные</strong> — изменение hex-данных, <strong>Сохранить</strong> для записи (или <strong>Отмена</strong>); transparent-файлы редактируются в textarea, record-файлы — по одному полю на запись с флажками выбора. «Редактировать сырые данные» сначала переключает на hex-вид и при необходимости читает файл.</li>
|
<li><strong>Редактировать сырые данные</strong> — изменение hex-данных, <strong>Сохранить</strong> для записи (или <strong>Отмена</strong>); transparent-файлы редактируются в textarea, record-файлы — по одному полю на запись с флажками выбора. «Редактировать сырые данные» сначала переключает на hex-вид и при необходимости читает файл.</li>
|
||||||
<li><strong>Редактировать декодированные</strong> — редактирование файла в его собственной декодированной форме; кодирование выполняется в браузере, запись идёт обычным путём обновления. Кодировщики покрывают <strong>EF.SPN</strong>, <strong>EF.PNN</strong>, списки PLMN (<strong>PLMNsel / FPLMN / EHPLMN</strong> и списки с технологиями доступа <strong>PLMNwAcT / OPLMNwAcT / HPLMNwAcT</strong>), файлы номеров (<strong>ADN / FDN / SDN / MSISDN / LND / BDN</strong>), таблицы сервисов (<strong>UST / IST / EST</strong>; <strong>SST</strong> остаётся только для чтения до появления редактора allocated/activated), <strong>ACC</strong>, <strong>PHASE</strong>, <strong>HPPLMN</strong>, <strong>AD</strong>, <strong>ICCID / IMSI</strong> (с подтверждением — запись меняет идентичность карты) списки cell broadcast (<strong>CBMI / CBMID / CBMIR</strong>) и <strong>EF.SMSP</strong> (полная форма SMS-параметров: alpha, TP-DA и TS-SCA с их правилами адресации и TON, TP-PID, TP-DCS и относительный TP-VP; у каждого параметра — флажок <em>«отсутствует (действует значение ME по умолчанию)»</em> и живые подсказки по DCS и сроку действия). Для transparent-файла — одна форма (поля SPN, строки PLMN с добавлением/удалением, элементы PNN с добавлением/удалением); для record-файла — по форме на запись с флажком выбора, который ставится автоматически при изменении поля, поэтому записываются только изменённые записи. <strong>Сохранить</strong> проверяет все выбранные записи до записи; для файлов без кодировщика кнопка недоступна с подсказкой. Указатели расширений (EF.EXT1–EF.EXT5) и байт метода сравнения BDN сохраняются; редактирование номера, продолжение которого лежит в файле расширения, отклоняется.</li>
|
<li><strong>Редактировать декодированные</strong> — редактирование файла в его собственной декодированной форме; кодирование выполняется в браузере, запись идёт обычным путём обновления. Кодировщики покрывают <strong>EF.SPN</strong>, <strong>EF.PNN</strong>, списки PLMN (<strong>PLMNsel / FPLMN / EHPLMN</strong> и списки с технологиями доступа <strong>PLMNwAcT / OPLMNwAcT / HPLMNwAcT</strong>), файлы номеров (<strong>ADN / FDN / SDN / MSISDN / LND / BDN</strong>), таблицы сервисов (<strong>UST / IST / EST</strong> и <strong>SST</strong> — SIM-таблица, редактируется по каждой службе как удалена / выделена-выключена / включена), <strong>ACC</strong>, <strong>PHASE</strong>, <strong>HPPLMN</strong>, <strong>AD</strong>, <strong>ICCID / IMSI</strong> (с подтверждением — запись меняет идентичность карты) списки cell broadcast (<strong>CBMI / CBMID / CBMIR</strong>) и <strong>EF.SMSP</strong> (полная форма SMS-параметров: alpha, TP-DA и TS-SCA с их правилами адресации и TON, TP-PID, TP-DCS и относительный TP-VP; у каждого параметра — флажок <em>«отсутствует (действует значение ME по умолчанию)»</em> и живые подсказки по DCS и сроку действия). Для transparent-файла — одна форма (поля SPN, строки PLMN с добавлением/удалением, элементы PNN с добавлением/удалением); для record-файла — по форме на запись с флажком выбора, который ставится автоматически при изменении поля, поэтому записываются только изменённые записи. <strong>Сохранить</strong> проверяет все выбранные записи до записи; для файлов без кодировщика кнопка недоступна с подсказкой. Указатели расширений (EF.EXT1–EF.EXT5) и байт метода сравнения BDN сохраняются; редактирование номера, продолжение которого лежит в файле расширения, отклоняется.</li>
|
||||||
<li><strong>Проверить все файлы</strong> — обход всего дерева (включая пользовательские файлы) с пометкой каждого элемента: есть (обычный вид) или нет (красный ✗, без стрелки разворачивания); существующие пустые DF показывают <code class="font-mono text-sm">(пусто)</code>. Отображается прогресс <em>N / всего</em>, обход можно остановить; в конце — сводка «есть/нет». Файлы проверяются только при разворачивании или проверке — просмотр остаётся ленивым.</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>
|
<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>
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
+1
-1
@@ -408,7 +408,7 @@
|
|||||||
<ul class="list-disc list-inside text-sm space-y-1 mb-3">
|
<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 decoded table is built in the browser from the same raw bytes (no server-side decode). 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>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 decoded table is built in the browser from the same raw bytes (no server-side decode). 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. <strong>Edit raw</strong> switches to the hex view first and reads the file if needed.</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. <strong>Edit raw</strong> switches to the hex view first and reads the file if needed.</li>
|
||||||
<li><strong>Edit decoded</strong> — edit the file through its own decoded form, encoded in the browser and written with the normal update path. The encoders cover <strong>EF.SPN</strong>, <strong>EF.PNN</strong>, the PLMN lists (<strong>PLMNsel / FPLMN / EHPLMN</strong> and the access-technology lists <strong>PLMNwAcT / OPLMNwAcT / HPLMNwAcT</strong>), the dialling-number files (<strong>ADN / FDN / SDN / MSISDN / LND / BDN</strong>), the service tables (<strong>UST / IST / EST</strong>; <strong>SST</strong> stays read-only until the SIM allocated/activated editor exists), <strong>ACC</strong>, <strong>PHASE</strong>, <strong>HPPLMN</strong>, <strong>AD</strong>, <strong>ICCID / IMSI</strong> (behind a confirmation — writing them changes the card identity) the cell-broadcast lists (<strong>CBMI / CBMID / CBMIR</strong>) and <strong>EF.SMSP</strong> (the full SMS-parameter form: alpha, TP-DA and TS-SCA with their addressing rules and TON, TP-PID, TP-DCS and the relative TP-VP, each with an <em>absent (the ME default applies)</em> toggle and live DCS/validity hints). A transparent file gets one form (SPN fields, PLMN rows with add/remove, PNN items with add/remove); a record file gets one form per record with a selection checkbox that is auto-checked when a field changes, so only the edited records are written. <strong>Save</strong> validates every selected record before writing anything; files without an encoder keep the button disabled with a tooltip. Extension pointers (EF.EXT1–EF.EXT5) and the BDN comparison-method byte are preserved, and editing a number that continues in an extension file is refused.</li>
|
<li><strong>Edit decoded</strong> — edit the file through its own decoded form, encoded in the browser and written with the normal update path. The encoders cover <strong>EF.SPN</strong>, <strong>EF.PNN</strong>, the PLMN lists (<strong>PLMNsel / FPLMN / EHPLMN</strong> and the access-technology lists <strong>PLMNwAcT / OPLMNwAcT / HPLMNwAcT</strong>), the dialling-number files (<strong>ADN / FDN / SDN / MSISDN / LND / BDN</strong>), the service tables (<strong>UST / IST / EST</strong> and <strong>SST</strong> — the SIM table, edited per service as deleted / allocated-off / on), <strong>ACC</strong>, <strong>PHASE</strong>, <strong>HPPLMN</strong>, <strong>AD</strong>, <strong>ICCID / IMSI</strong> (behind a confirmation — writing them changes the card identity) the cell-broadcast lists (<strong>CBMI / CBMID / CBMIR</strong>) and <strong>EF.SMSP</strong> (the full SMS-parameter form: alpha, TP-DA and TS-SCA with their addressing rules and TON, TP-PID, TP-DCS and the relative TP-VP, each with an <em>absent (the ME default applies)</em> toggle and live DCS/validity hints). A transparent file gets one form (SPN fields, PLMN rows with add/remove, PNN items with add/remove); a record file gets one form per record with a selection checkbox that is auto-checked when a field changes, so only the edited records are written. <strong>Save</strong> validates every selected record before writing anything; files without an encoder keep the button disabled with a tooltip. Extension pointers (EF.EXT1–EF.EXT5) and the BDN comparison-method byte are preserved, and editing a number that continues in an extension file is refused.</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>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>
|
<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>
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
+1769
-91
File diff suppressed because it is too large.
Load diff
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
const CACHE = 'simple-v482';
|
const CACHE = 'simple-v488';
|
||||||
const URLS = [
|
const URLS = [
|
||||||
'index.html',
|
'index.html',
|
||||||
'help.html',
|
'help.html',
|
||||||
|
|||||||
@@ -146,6 +146,30 @@ test('cardsAutoSelectByIccid reacts to a card swap and to unreadable ICCIDs', ()
|
|||||||
assert.strictEqual(cardsAutoSelectByIccid('1234567890123456789'), -1);
|
assert.strictEqual(cardsAutoSelectByIccid('1234567890123456789'), -1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('a miss is not latched, so the store arriving later still auto-selects', () => {
|
||||||
|
// The card equips before /api/presets fills cards[] (the connect-time
|
||||||
|
// race): the auto-selection must not latch the ICCID on a miss, or the
|
||||||
|
// later cardsFetch would be a no-op and the preset would stay unselected
|
||||||
|
// until the card is re-inserted.
|
||||||
|
const els = fakeDoc();
|
||||||
|
const applied = [];
|
||||||
|
globalThis.cardsApply = idx => applied.push(idx);
|
||||||
|
globalThis.cards = []; // presets not loaded yet
|
||||||
|
globalThis._ramCardIdx = null;
|
||||||
|
globalThis._cardsAutoIccid = null;
|
||||||
|
assert.strictEqual(cardsAutoSelectByIccid(DIGITS), -1);
|
||||||
|
assert.strictEqual(globalThis._cardsAutoIccid, null,
|
||||||
|
'a missed auto-selection must not latch the ICCID');
|
||||||
|
// the presets arrive: the retry (from cardsFetch) finds and selects the card
|
||||||
|
globalThis.cards = [{ name: 'A', iccid: RAW_HEX }];
|
||||||
|
assert.strictEqual(cardsAutoSelectByIccid(DIGITS), 0);
|
||||||
|
assert.strictEqual(els['sp-card-sel'].value, '0');
|
||||||
|
assert.strictEqual(els['ram-card-sel'].value, '0');
|
||||||
|
assert.strictEqual(els['tar-card-sel'].value, '0');
|
||||||
|
assert.deepStrictEqual(applied, ['0']);
|
||||||
|
assert.strictEqual(globalThis._ramCardIdx, 0);
|
||||||
|
});
|
||||||
|
|
||||||
test('cardsFindDuplicateIccid catches duplicates across stored formats', () => {
|
test('cardsFindDuplicateIccid catches duplicates across stored formats', () => {
|
||||||
globalThis.cards = [
|
globalThis.cards = [
|
||||||
{ name: 'A', iccid: '1111111111111111111' },
|
{ name: 'A', iccid: '1111111111111111111' },
|
||||||
@@ -201,6 +225,13 @@ test('the card-state update wires the ICCID into the preset selection', () => {
|
|||||||
assert.ok(html.includes("(data.iccid ? ' | ICCID: <b>' + esc(data.iccid) + '</b>' : '')"));
|
assert.ok(html.includes("(data.iccid ? ' | ICCID: <b>' + esc(data.iccid) + '</b>' : '')"));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('cardsFetch retries the auto-selection once the presets load', () => {
|
||||||
|
// The equip may be reported before /api/presets resolves, so the fetch
|
||||||
|
// must re-run the auto-selection (a no-op once the card is selected).
|
||||||
|
assert.ok(html.includes('cardsAutoSelectByIccid(_pysimCardIccid);'),
|
||||||
|
'cardsFetch must re-run the auto-selection after loading the store');
|
||||||
|
});
|
||||||
|
|
||||||
test('cardsRender highlights the equipped card preset', () => {
|
test('cardsRender highlights the equipped card preset', () => {
|
||||||
globalThis.t = s => s;
|
globalThis.t = s => s;
|
||||||
const els = { 'cards-tbody': { innerHTML: '' } };
|
const els = { 'cards-tbody': { innerHTML: '' } };
|
||||||
|
|||||||
@@ -26,13 +26,13 @@ const FNS = [
|
|||||||
'efGsm7Octets', 'efAnnexA', 'efBcdAddress', 'efBcdAddressLen', 'efPlmn', 'efPlmnAct', 'efPnnText',
|
'efGsm7Octets', 'efAnnexA', 'efBcdAddress', 'efBcdAddressLen', 'efPlmn', 'efPlmnAct', 'efPnnText',
|
||||||
'efDecIccid', 'efDecImsi', 'efDecLi', 'efDecServiceTable', 'efDecSst', 'efDecPlmnList', 'efDecFplmn', 'efDecPlmnWact',
|
'efDecIccid', 'efDecImsi', 'efDecLi', 'efDecServiceTable', 'efDecSst', 'efDecPlmnList', 'efDecFplmn', 'efDecPlmnWact',
|
||||||
'efDecSpn', 'efDecLoci', 'efDecPsLoci', 'efDecEpsLoci', 'efDecEpsNsc', 'efDecKc', 'efDecAcc',
|
'efDecSpn', 'efDecLoci', 'efDecPsLoci', 'efDecEpsLoci', 'efDecEpsNsc', 'efDecKc', 'efDecAcc',
|
||||||
'efDecPhase', 'efDecCbmi', 'efDecCbmir', 'efDecHpplmn', 'efDecEcc', 'efDecOpl', 'efDecAd', 'efDecAcl',
|
'efDecPhase', 'efDecCbmi', 'efDecCbmir', 'efDecHpplmn', 'efDecEcc', 'efDecEccUsim', 'efEccCategory', 'efDecOpl', 'efDecAd', 'efDecAcl',
|
||||||
'efSmspDaNumber', 'efDecSmsp', 'efDecSpdi', 'efDecNai', 'efDecDir', 'efDecArr', 'efDecPnn', 'efDecAdn',
|
'efSmspDaNumber', 'efDecSmsp', 'efDecSpdi', 'efDecNai', 'efDecDir', 'efDecArr', 'efDecPnn', 'efDecAdn',
|
||||||
'pinKeyRefName', 'efArrAmLabel', 'efArrSc', 'efArrAmDo',
|
'pinKeyRefName', 'efArrAmLabel', 'efArrSc', 'efArrAmDo',
|
||||||
'efDecExt1', 'efDecSms', 'efDecSume', 'efPbrHex', 'efDecPbr', 'efFindDecoder', 'efFidFromPath', 'efDecodeBytes',
|
'efDecExt1', 'efDecSms', 'efDecSume', 'efPbrHex', 'efDecPbr', 'efFindDecoder', 'efFidFromPath', 'efDfFromPath', 'efDecodeBytes',
|
||||||
'efDecodeFile', 'efDecodeHex', 'efIsRawOnly', 'efFieldLabel', 'efPrimitive', 'efFlatten',
|
'efDecodeFile', 'efDecodeHex', 'efIsRawOnly', 'efFieldLabel', 'efPrimitive', 'efFlatten',
|
||||||
'efDiffData', 'efDataSummary', 'efContentDiff', 'gsm7Decode', 'efRenderFieldsHtml',
|
'efDiffData', 'efDataSummary', 'efContentDiff', 'gsm7Decode', 'efRenderFieldsHtml',
|
||||||
'pysimFsDecodedHtml', 'profilerRenderReport', 'profilerLabelText', 'profilerResultAspects',
|
'pysimFsDecodedHtml', 'pysimFsNodePath', 'pysimFsFindNodeByPath', 'profilerRenderReport', 'profilerLabelText', 'profilerResultAspects',
|
||||||
'profilerAspectSummary', 'profilerMatchedRecordsText', 'profilerRawDataCheck', 'profilerCustomNameForPath',
|
'profilerAspectSummary', 'profilerMatchedRecordsText', 'profilerRawDataCheck', 'profilerCustomNameForPath',
|
||||||
'pysimCustomNormPath',
|
'pysimCustomNormPath',
|
||||||
];
|
];
|
||||||
@@ -45,9 +45,11 @@ for (const c of ['GSM7_ALPHABET', 'GSM7_EXT_MAP', 'FCP_ARR_AM_BITS']) {
|
|||||||
for (const c of ['EF_UST_SERVICES', 'EF_SST_SERVICES', 'EF_IST_SERVICES', 'EF_EST_SERVICES', 'EF_PHASE_NAMES', 'EF_PBR_KINDS']) {
|
for (const c of ['EF_UST_SERVICES', 'EF_SST_SERVICES', 'EF_IST_SERVICES', 'EF_EST_SERVICES', 'EF_PHASE_NAMES', 'EF_PBR_KINDS']) {
|
||||||
code += html.match(new RegExp('const ' + c + ' = \\{.*\\};'))[0].replace('const ', 'var ') + '\n';
|
code += html.match(new RegExp('const ' + c + ' = \\{.*\\};'))[0].replace('const ', 'var ') + '\n';
|
||||||
}
|
}
|
||||||
|
code += html.match(/const EF_ECC_CATEGORIES = \[[\s\S]*?\n\];/)[0].replace('const ', 'var ') + '\n';
|
||||||
|
code += html.match(/const EF_ADF_RIDS = \[[\s\S]*?\n\];/)[0].replace('const ', 'var ') + '\n';
|
||||||
code += html.match(/const EF_FIELD_LABELS = \{[\s\S]*?\n\};/)[0].replace('const ', 'var ') + '\n';
|
code += html.match(/const EF_FIELD_LABELS = \{[\s\S]*?\n\};/)[0].replace('const ', 'var ') + '\n';
|
||||||
code += html.match(/const EF_DECODERS = \[[\s\S]*?\n\];/)[0].replace('const ', 'var ') + '\n';
|
code += html.match(/const EF_DECODERS = \[[\s\S]*?\n\];/)[0].replace('const ', 'var ') + '\n';
|
||||||
code += 'var pysimCustomFiles = [];\nvar pysimFsSelected = \'EF.IMSI\';\n';
|
code += 'var pysimCustomFiles = [];\nvar pysimFsSelected = \'EF.IMSI\';\nvar pysimFsTreeRoot = null;\n';
|
||||||
code += 'function esc(s){return String(s);}\nfunction escHtml(s){return String(s);}\nfunction t(k){return k;}\n';
|
code += 'function esc(s){return String(s);}\nfunction escHtml(s){return String(s);}\nfunction t(k){return k;}\n';
|
||||||
eval(code);
|
eval(code);
|
||||||
|
|
||||||
@@ -106,7 +108,7 @@ test('EF decoders match the pinned spec / pySim test vectors', () => {
|
|||||||
assert.deepStrictEqual(efDecAcc(efBytes('8000')), { access_classes: [15] });
|
assert.deepStrictEqual(efDecAcc(efBytes('8000')), { access_classes: [15] });
|
||||||
assert.deepStrictEqual(efDecAcc(efBytes('0001')), { access_classes: [0] });
|
assert.deepStrictEqual(efDecAcc(efBytes('0001')), { access_classes: [0] });
|
||||||
assert.deepStrictEqual(efDecAcc(efBytes('8001')), { access_classes: [15, 0] });
|
assert.deepStrictEqual(efDecAcc(efBytes('8001')), { access_classes: [15, 0] });
|
||||||
assert.deepStrictEqual(efDecEcc(efBytes('19f101')), { number: '911', esc: '0x01' });
|
assert.deepStrictEqual(efDecEcc(efBytes('19f101')), { number: '911', esc: 'police' });
|
||||||
assert.deepStrictEqual(efDecKc(efBytes('837d783609a3858f05')), { kc: '837D783609A3858F', ksi: 0 });
|
assert.deepStrictEqual(efDecKc(efBytes('837d783609a3858f05')), { kc: '837D783609A3858F', ksi: 0 });
|
||||||
assert.deepStrictEqual(efDecPlmnList(efBytes('22F860')), { plmns: [{ mcc: '228', mnc: '06' }] });
|
assert.deepStrictEqual(efDecPlmnList(efBytes('22F860')), { plmns: [{ mcc: '228', mnc: '06' }] });
|
||||||
// EF.FPLMN: a FFFFFF gap in any position is skipped, not a terminator
|
// EF.FPLMN: a FFFFFF gap in any position is skipped, not a terminator
|
||||||
@@ -357,6 +359,106 @@ test('decoder registry resolves by name first, then FID; unknown returns null',
|
|||||||
assert.strictEqual(efFidFromPath('ADF.USIM'), null);
|
assert.strictEqual(efFidFromPath('ADF.USIM'), null);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('EF.ECC (DF.GSM) decodes the Emergency Service Category like the USIM file', () => {
|
||||||
|
// TS 51.011 10.3.27: the category byte is the TS 24.008 10.5.135d coding,
|
||||||
|
// the same table the ADF.USIM file uses (bit n = service n)
|
||||||
|
assert.deepStrictEqual(efDecEcc(efBytes('19F104')), { number: '911', esc: 'fire brigade' });
|
||||||
|
assert.deepStrictEqual(efDecEcc(efBytes('19F102')), { number: '911', esc: 'ambulance' });
|
||||||
|
assert.deepStrictEqual(efDecEcc(efBytes('19F109')), { number: '911', esc: 'police, marine guard' });
|
||||||
|
// a byte without a known bit keeps its raw hex value
|
||||||
|
assert.deepStrictEqual(efDecEcc(efBytes('19F180')), { number: '911', esc: '0x80' });
|
||||||
|
// a record whose code is FF is still decoded (only an all-FF record is empty)
|
||||||
|
assert.deepStrictEqual(efDecEcc(efBytes('FFFF00')), { number: null, esc: '0x00' });
|
||||||
|
// several 3-byte records
|
||||||
|
assert.deepStrictEqual(efDecEcc(efBytes('19F10411F220')), {
|
||||||
|
numbers: [{ number: '911', esc: 'fire brigade' }, { number: '112', esc: 'manual eCall' }],
|
||||||
|
});
|
||||||
|
assert.deepStrictEqual(efDecEcc(efBytes('FFFFFFFF')), { empty: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('efDfFromPath reads the parent DF of a canonical path', () => {
|
||||||
|
assert.strictEqual(efDfFromPath('MF/7F20/6FAD'), '7F20');
|
||||||
|
assert.strictEqual(efDfFromPath('ADF.USIM/6FAD'), 'ADF.USIM');
|
||||||
|
assert.strictEqual(efDfFromPath('MF/ADF.USIM/6FAD'), 'ADF.USIM');
|
||||||
|
assert.strictEqual(efDfFromPath('mf/7f20/6fad'), '7F20');
|
||||||
|
assert.strictEqual(efDfFromPath('MF/7F10/5F3A/4F30'), '5F3A');
|
||||||
|
// a bare FID (and a bare root) carries no DF context
|
||||||
|
assert.strictEqual(efDfFromPath('6FAD'), '');
|
||||||
|
assert.strictEqual(efDfFromPath('MF'), '');
|
||||||
|
assert.strictEqual(efDfFromPath('ADF.USIM'), '');
|
||||||
|
assert.strictEqual(efDfFromPath(''), '');
|
||||||
|
assert.strictEqual(efDfFromPath(null), '');
|
||||||
|
// the two halves of the spec identity come from the same path
|
||||||
|
assert.strictEqual(efFidFromPath('MF/7F20/6FAD'), '6FAD');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('efDfFromPath folds an ADF AID root onto its symbolic ADF name', () => {
|
||||||
|
// a snapshot/profile path roots an ADF by the DF name (the AID) the card
|
||||||
|
// reports, not by "ADF.USIM" - the RID decides, whatever the PIX is
|
||||||
|
assert.strictEqual(efDfFromPath('A0000000871002/6F07'), 'ADF.USIM');
|
||||||
|
assert.strictEqual(efDfFromPath('A0000000871002FF49FF0589/6F07'), 'ADF.USIM');
|
||||||
|
assert.strictEqual(efDfFromPath('a0000000871002ff49ff0589/6fad'), 'ADF.USIM');
|
||||||
|
assert.strictEqual(efDfFromPath('A0000000871004/6F07'), 'ADF.ISIM');
|
||||||
|
assert.strictEqual(efDfFromPath('A0000000871004FF49FF0589/6F07'), 'ADF.ISIM');
|
||||||
|
// the non-IMSI USIM (TS 31.102 Annex N) repeats the ADF.USIM file set
|
||||||
|
assert.strictEqual(efDfFromPath('A000000087100B/6F38'), 'ADF.USIM-NON-IMSI');
|
||||||
|
assert.strictEqual(efDfFromPath('a000000087100bff49ff0589/6f38'), 'ADF.USIM-NON-IMSI');
|
||||||
|
// an unrelated AID is not an ADF name: it passes through as itself
|
||||||
|
assert.strictEqual(efDfFromPath('A0000000090003FFFF/6F07'), 'A0000000090003FFFF');
|
||||||
|
assert.strictEqual(efDfFromPath('F0414C46416001/6F07'), 'F0414C46416001');
|
||||||
|
// the AID root reads the same as the symbolic name and the plain FID do
|
||||||
|
assert.strictEqual(efFindDecoder('EF.ECC', '6FB7', efDfFromPath('A0000000871002/6FB7')).fn, efDecEccUsim);
|
||||||
|
assert.strictEqual(efFindDecoder('EF.ECC', '6FB7', efDfFromPath('MF/7F20/6FB7')).fn, efDecEcc);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('the decoder resolver is DF-first: EF.ECC is the SIM or the USIM file', () => {
|
||||||
|
// EF.ECC collides in name AND FID: only the parent DF separates the
|
||||||
|
// TS 51.011 transparent form from the TS 31.102 linear-fixed one
|
||||||
|
assert.strictEqual(efFindDecoder('EF.ECC', '6fb7', '7F20').fn, efDecEcc);
|
||||||
|
assert.strictEqual(efFindDecoder('EF.ECC', '6fb7', 'ADF.USIM').fn, efDecEccUsim);
|
||||||
|
assert.strictEqual(efFindDecoder('EF.ECC', '6fb7', 'adf.usim').fn, efDecEccUsim);
|
||||||
|
assert.strictEqual(efFindDecoder(null, '6FB7', 'ADF.USIM').fn, efDecEccUsim);
|
||||||
|
// without a DF the historical name-then-FID lookup answers (the first
|
||||||
|
// entry, the DF.GSM form, is unchanged behaviour)
|
||||||
|
assert.strictEqual(efFindDecoder('EF.ECC', '6fb7').fn, efDecEcc);
|
||||||
|
assert.strictEqual(efFindDecoder('EF.ECC', null).fn, efDecEcc);
|
||||||
|
// a DF carrying no entry of its own falls back to the generic lookup
|
||||||
|
assert.strictEqual(efFindDecoder('EF.ECC', '6fb7', 'ADF.ISIM').fn, efDecEcc);
|
||||||
|
// the FID fallback stays inside the DF
|
||||||
|
assert.strictEqual(efFindDecoder('EF.NOPE', '6FB7', 'ADF.USIM').fn, efDecEccUsim);
|
||||||
|
assert.strictEqual(efFindDecoder('EF.NOPE', 'abcd', 'ADF.USIM'), null);
|
||||||
|
// every EF.AD form has its own DF-keyed entry over one decoder
|
||||||
|
assert.strictEqual(efFindDecoder('EF.AD', '6fad', '7F20').fn, efDecAd);
|
||||||
|
assert.strictEqual(efFindDecoder('EF.AD', '6fad', 'ADF.USIM').fn, efDecAd);
|
||||||
|
assert.strictEqual(efFindDecoder('EF.AD', '6fad', 'ADF.ISIM').fn, efDecAd);
|
||||||
|
assert.strictEqual(efFindDecoder('EF.AD', '6fad', 'ADF.ISIM').df, 'ADF.ISIM');
|
||||||
|
// files without a collision are unaffected by the DF context
|
||||||
|
assert.strictEqual(efFindDecoder('EF.IMSI', '6f07', 'ADF.USIM').name, 'EF.IMSI');
|
||||||
|
assert.strictEqual(efFindDecoder(null, '2fe2', 'MF').name, 'EF.ICCID');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('EF.ECC (USIM) decodes a X+4 record: code, alpha and service category', () => {
|
||||||
|
// pySim's EF_ECC vectors (TS 31.102 4.2.21): 3 BCD code bytes, the
|
||||||
|
// optional annex A alpha, then the category byte
|
||||||
|
assert.deepStrictEqual(efDecEccUsim(efBytes('19F1FF01')), { code: '911', category: 'police' });
|
||||||
|
assert.deepStrictEqual(efDecEccUsim(efBytes('19F3FF02')), { code: '913', category: 'ambulance' });
|
||||||
|
// code 112 = 11 F2 FF (odd digit count, F padded)
|
||||||
|
assert.deepStrictEqual(efDecEccUsim(efBytes('11F2FF40')), { code: '112', category: 'automatic eCall' });
|
||||||
|
// with the optional alpha identifier (X = 3, GSM 7-bit octets)
|
||||||
|
assert.deepStrictEqual(efDecEccUsim(efBytes('19F1FF424F4204')),
|
||||||
|
{ code: '911', category: 'fire brigade', alpha: 'BOB' });
|
||||||
|
// a UCS-2 (0x80-prefixed) alpha, and two category bits at once
|
||||||
|
assert.deepStrictEqual(efDecEccUsim(efBytes('19F1FF800042004F004205')),
|
||||||
|
{ code: '911', category: 'police, fire brigade', alpha: 'BOB' });
|
||||||
|
// an unmodelled category byte stays visible as its hex value
|
||||||
|
assert.deepStrictEqual(efDecEccUsim(efBytes('19F1FF80')), { code: '911', category: '0x80' });
|
||||||
|
// unused records: all FF, or the mandatory code all FF
|
||||||
|
assert.deepStrictEqual(efDecEccUsim(efBytes('FFFFFFFFFFFF')), { empty: true });
|
||||||
|
assert.deepStrictEqual(efDecEccUsim(efBytes('FFFFFF02')), { empty: true });
|
||||||
|
// shorter than the mandatory fields: raw fallback, never a throw
|
||||||
|
assert.deepStrictEqual(efDecEccUsim(efBytes('19F1FF')), { raw: '19F1FF' });
|
||||||
|
});
|
||||||
|
|
||||||
test('efDecodeFile handles transparent and record content', () => {
|
test('efDecodeFile handles transparent and record content', () => {
|
||||||
const t = efDecodeFile('EF.IMSI', null, { kind: 'transparent', data: '082982608200002080' });
|
const t = efDecodeFile('EF.IMSI', null, { kind: 'transparent', data: '082982608200002080' });
|
||||||
assert.strictEqual(typeof t.fn, 'function');
|
assert.strictEqual(typeof t.fn, 'function');
|
||||||
@@ -398,6 +500,58 @@ test('efFlatten skips the all-FF empty marker', () => {
|
|||||||
assert.deepStrictEqual(efFlatten({ empty: true }), []);
|
assert.deepStrictEqual(efFlatten({ empty: true }), []);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('the file manager resolves two same-named files by their parent DF', () => {
|
||||||
|
// EF.ECC exists in DF.GSM and ADF.USIM with the same name and FID; the
|
||||||
|
// selection key is the canonical path, so each resolves to its own node
|
||||||
|
const root = { name: 'MF', fid: '3F00', children: [], parent: null };
|
||||||
|
const gsm = { name: 'DF.GSM', fid: '7F20', children: [], parent: root };
|
||||||
|
const usim = { name: 'ADF.USIM', fid: '7FFF', children: [], parent: root };
|
||||||
|
const gsmEcc = { name: 'EF.ECC', fid: '6FB7', children: null, parent: gsm };
|
||||||
|
const usimEcc = { name: 'EF.ECC', fid: '6FB7', children: null, parent: usim };
|
||||||
|
root.children = [gsm, usim];
|
||||||
|
gsm.children = [gsmEcc];
|
||||||
|
usim.children = [usimEcc];
|
||||||
|
pysimFsTreeRoot = root;
|
||||||
|
assert.strictEqual(pysimFsFindNodeByPath('MF/7F20/6FB7'), gsmEcc);
|
||||||
|
assert.strictEqual(pysimFsFindNodeByPath('ADF.USIM/6FB7'), usimEcc);
|
||||||
|
// a path that is not loaded resolves to nothing (the callers fall back to
|
||||||
|
// the path's own last segment / FID)
|
||||||
|
assert.strictEqual(pysimFsFindNodeByPath('MF/7F10/6FB7'), null);
|
||||||
|
// the decoded view's DF follows the resolved node: the USIM file is the
|
||||||
|
// linear-fixed X+4 form, the DF.GSM copy the SIM transparent one
|
||||||
|
pysimFsSelected = 'ADF.USIM/6FB7';
|
||||||
|
const usimHtml = pysimFsDecodedHtml(pysimFsFindNodeByPath(pysimFsSelected),
|
||||||
|
{ success: true, records: [{ num: 1, data: '19F1FF04' }] });
|
||||||
|
assert.ok(usimHtml.includes('Emergency call code'));
|
||||||
|
assert.ok(usimHtml.includes('fire brigade'));
|
||||||
|
pysimFsSelected = 'MF/7F20/6FB7';
|
||||||
|
const gsmHtml = pysimFsDecodedHtml(pysimFsFindNodeByPath(pysimFsSelected),
|
||||||
|
{ success: true, data: '19F104' });
|
||||||
|
assert.ok(gsmHtml.includes('Emergency category'));
|
||||||
|
assert.ok(gsmHtml.includes('fire brigade'));
|
||||||
|
assert.ok(!gsmHtml.includes('Emergency call code'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('pysimFsDecodedHtml decodes EF.ECC by the selected node\'s parent DF', () => {
|
||||||
|
// ADF.USIM's EF.ECC is the linear-fixed X+4 form (code/alpha/category)
|
||||||
|
const usim = { name: 'ADF.USIM', children: [] };
|
||||||
|
const usimEcc = { name: 'EF.ECC', fid: '6FB7', parent: usim };
|
||||||
|
usim.children.push(usimEcc);
|
||||||
|
const usimHtml = pysimFsDecodedHtml(usimEcc, { success: true, records: [{ num: 1, data: '19F1FF01' }] });
|
||||||
|
assert.ok(usimHtml.includes('Emergency call code'));
|
||||||
|
assert.ok(usimHtml.includes('911'));
|
||||||
|
assert.ok(usimHtml.includes('police'));
|
||||||
|
// DF.GSM's EF.ECC keeps the transparent code + category decode
|
||||||
|
const gsm = { name: 'DF.GSM', fid: '7F20', children: [] };
|
||||||
|
const gsmEcc = { name: 'EF.ECC', fid: '6FB7', parent: gsm };
|
||||||
|
gsm.children.push(gsmEcc);
|
||||||
|
const gsmHtml = pysimFsDecodedHtml(gsmEcc, { success: true, data: '19F101' });
|
||||||
|
assert.ok(gsmHtml.includes('Number'));
|
||||||
|
assert.ok(gsmHtml.includes('911'));
|
||||||
|
assert.ok(gsmHtml.includes('Emergency category'));
|
||||||
|
assert.ok(!gsmHtml.includes('Emergency call code'));
|
||||||
|
});
|
||||||
|
|
||||||
test('efDiffData names an all-FF side Empty (all FF)', () => {
|
test('efDiffData names an all-FF side Empty (all FF)', () => {
|
||||||
assert.deepStrictEqual(efDiffData({ empty: true }, { full: 'Miran' }),
|
assert.deepStrictEqual(efDiffData({ empty: true }, { full: 'Miran' }),
|
||||||
[['Full name', 'Empty (all FF)', 'Miran']]);
|
[['Full name', 'Empty (all FF)', 'Miran']]);
|
||||||
|
|||||||
@@ -51,13 +51,13 @@ const FNS = [
|
|||||||
'efEncPlmnList', 'efListRows', 'efPlmnRowOrig', 'efEncAdn', 'efEncAdnExt', 'efPnnNameBytes', 'efPnnHexBytes', 'efPnnOrigTlv', 'efEncPnn',
|
'efEncPlmnList', 'efListRows', 'efPlmnRowOrig', 'efEncAdn', 'efEncAdnExt', 'efPnnNameBytes', 'efPnnHexBytes', 'efPnnOrigTlv', 'efEncPnn',
|
||||||
'efFindEncoder', 'efHasEncoder', 'gsm7Decode', 'gsm7TextToSeptets',
|
'efFindEncoder', 'efHasEncoder', 'gsm7Decode', 'gsm7TextToSeptets',
|
||||||
'gsm7Encode', 'efEncError', 'efDecModelValues',
|
'gsm7Encode', 'efEncError', 'efDecModelValues',
|
||||||
'efDecServiceTable', 'efDecSst', 'efDecAcc', 'efDecPhase', 'efDecHpplmn', 'efDecAd', 'efDecCbmi', 'efDecCbmir', 'efDecIccid', 'efDecImsi', 'efDecPlmnList', 'efDecFplmn', 'efEncServiceTable', 'efEncAcc', 'efPhaseValue', 'efPhaseModel', 'efEncPhase', 'efEncHpplmn', 'efAdOpValue', 'efAdModel', 'efEncAd', 'efEncPlmnOnly', 'efEncIccid', 'efEncImsi', 'efCbmiModel', 'efEncCbmi', 'efCbmirModel', 'efEncCbmir', 'efSmspAddressModel', 'efSmspModel', 'efSmspTonOptions', 'efSmspParamRow', 'efSmspHtml', 'efSmspCollect', 'pysimFsSmspToggle', 'pysimFsSmspHexInput', 'efSmspHexByte', 'efSmspAddressBytes', 'efSmspAddrUnchanged', 'efSmspByteUnchanged', 'efEncSmsp', 'smspT', 'smspDcsHint', 'smspVpHint', 'smspDurationText', 'smspPluralIndex', 'smspCount', 'efSmspDaNumber',
|
'efDecServiceTable', 'efDecSst', 'efDecAcc', 'efDecPhase', 'efDecHpplmn', 'efDecAd', 'efDecCbmi', 'efDecCbmir', 'efDecIccid', 'efDecImsi', 'efDecPlmnList', 'efDecFplmn', 'efEncServiceTable', 'efEncSst', 'efEncAcc', 'efPhaseValue', 'efPhaseModel', 'efEncPhase', 'efEncHpplmn', 'efAdOpValue', 'efAdModel', 'efEncAd', 'efEncPlmnOnly', 'efEncIccid', 'efEncImsi', 'efCbmiModel', 'efEncCbmi', 'efCbmirModel', 'efEncCbmir', 'efSmspAddressModel', 'efSmspModel', 'efSmspTonOptions', 'efSmspParamRow', 'efSmspHtml', 'efSmspCollect', 'pysimFsSmspToggle', 'pysimFsSmspHexInput', 'efSmspHexByte', 'efSmspAddressBytes', 'efSmspAddrUnchanged', 'efSmspByteUnchanged', 'efEncSmsp', 'smspT', 'smspDcsHint', 'smspVpHint', 'smspDurationText', 'smspPluralIndex', 'smspCount', 'efSmspDaNumber',
|
||||||
];
|
];
|
||||||
|
|
||||||
let code = '';
|
let code = '';
|
||||||
for (const f of FNS) code += extractFunc(html, f) + '\n';
|
for (const f of FNS) code += extractFunc(html, f) + '\n';
|
||||||
for (const c of ['GSM7_ALPHABET', 'GSM7_EXT_MAP', 'EF_ACT_TECHS', 'EF_SPN_FIELDS',
|
for (const c of ['GSM7_ALPHABET', 'GSM7_EXT_MAP', 'EF_ACT_TECHS', 'EF_SPN_FIELDS',
|
||||||
'EF_ADN_FIELDS', 'EF_PLMN_LIST', 'EF_PNN_ITEMS', 'EF_UST_SERVICES', 'EF_SST_SERVICES', 'EF_IST_SERVICES', 'EF_EST_SERVICES', 'EF_PHASE_NAMES', 'EF_ACC_CLASSES', 'EF_UST_FIELDS', 'EF_IST_FIELDS', 'EF_EST_FIELDS', 'EF_ACC_FIELDS', 'EF_PHASE_FIELDS', 'EF_HPPLMN_FIELDS', 'EF_AD_OP_MODES', 'EF_AD_FIELDS', 'EF_PLMN_ONLY_LIST', 'EF_ICCID_FIELDS', 'EF_IMSI_FIELDS', 'EF_CBMI_LIST', 'EF_CBMIR_LIST', 'EF_READONLY_NAMES', 'EF_SMSP_PARAMS', 'EF_SMSP_CUSTOM', 'EF_ENCODERS']) {
|
'EF_ADN_FIELDS', 'EF_PLMN_LIST', 'EF_PNN_ITEMS', 'EF_UST_SERVICES', 'EF_SST_SERVICES', 'EF_IST_SERVICES', 'EF_EST_SERVICES', 'EF_PHASE_NAMES', 'EF_ACC_CLASSES', 'EF_UST_FIELDS', 'EF_IST_FIELDS', 'EF_EST_FIELDS', 'EF_SST_FIELDS', 'EF_ACC_FIELDS', 'EF_PHASE_FIELDS', 'EF_HPPLMN_FIELDS', 'EF_AD_OP_MODES', 'EF_AD_FIELDS', 'EF_PLMN_ONLY_LIST', 'EF_ICCID_FIELDS', 'EF_IMSI_FIELDS', 'EF_CBMI_LIST', 'EF_CBMIR_LIST', 'EF_READONLY_NAMES', 'EF_SMSP_PARAMS', 'EF_SMSP_CUSTOM', 'EF_ENCODERS']) {
|
||||||
code += extractConst(html, c);
|
code += extractConst(html, c);
|
||||||
}
|
}
|
||||||
eval(code);
|
eval(code);
|
||||||
@@ -218,6 +218,43 @@ test('EF.SPN and the ADN family share the encoder registry with the PLMN list',
|
|||||||
assert.strictEqual(efFindEncoder('EF.PLMNwAcT', null).list, efFindEncoder('EF.HPLMNwAcT', null).list);
|
assert.strictEqual(efFindEncoder('EF.PLMNwAcT', null).list, efFindEncoder('EF.HPLMNwAcT', null).list);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('the encoder resolver is DF-first: every EF.AD form shares one encoder', () => {
|
||||||
|
// EF.AD collides in name AND FID: one entry per DF (DF.GSM 3+X,
|
||||||
|
// ADF.USIM 4+X, ADF.ISIM 3+X), all over the same length-preserving codec
|
||||||
|
const sim = efFindEncoder('EF.AD', '6fad', '7F20');
|
||||||
|
const usim = efFindEncoder('EF.AD', '6fad', 'ADF.USIM');
|
||||||
|
const isim = efFindEncoder('EF.AD', '6fad', 'ADF.ISIM');
|
||||||
|
for (const e of [sim, usim, isim]) {
|
||||||
|
assert.strictEqual(e.enc, efEncAd);
|
||||||
|
assert.strictEqual(e.model, efAdModel);
|
||||||
|
assert.strictEqual(e.fields, EF_AD_FIELDS);
|
||||||
|
}
|
||||||
|
assert.notStrictEqual(sim, isim);
|
||||||
|
// without a DF the historical name-first lookup answers (DF.GSM's entry)
|
||||||
|
assert.strictEqual(efFindEncoder('EF.AD', '6fad'), sim);
|
||||||
|
assert.strictEqual(efFindEncoder('EF.AD', '6fad', 'ADF.NOPE'), sim);
|
||||||
|
// EF.ECC stays read-only in either DF; the shared 6F38 FID resolves to
|
||||||
|
// EF.SST under DF.GSM and to EF.UST under ADF.USIM (the FID fallback
|
||||||
|
// without a DF keeps the historical UST answer - SST's entry is DF-scoped)
|
||||||
|
assert.strictEqual(efHasEncoder('EF.ECC', '6fb7', '7F20'), false);
|
||||||
|
assert.strictEqual(efHasEncoder('EF.ECC', '6fb7', 'ADF.USIM'), false);
|
||||||
|
assert.strictEqual(efHasEncoder('EF.SST', '6f38', '7F20'), true);
|
||||||
|
assert.strictEqual(efHasEncoder('EF.UST', '6f38', 'ADF.USIM'), true);
|
||||||
|
assert.strictEqual(efFindEncoder('EF.SST', '6f38', '7F20').enc, efEncSst);
|
||||||
|
assert.strictEqual(efFindEncoder('EF.UST', '6f38', 'ADF.USIM').enc, efFindEncoder('EF.UST', '6f38').enc);
|
||||||
|
assert.strictEqual(efFindEncoder(null, '6f38', '7F20').name, 'EF.SST');
|
||||||
|
assert.strictEqual(efFindEncoder(null, '6f38', 'ADF.USIM').name, 'EF.UST');
|
||||||
|
assert.strictEqual(efFindEncoder(null, '6f38').name, 'EF.UST');
|
||||||
|
assert.strictEqual(efFindEncoder(null, '6f38', 'ADF.USIM').enc !== efEncSst, true);
|
||||||
|
assert.strictEqual(efFindEncoder('EF.NOPE', 'abcd', 'ADF.USIM'), null);
|
||||||
|
// the SIM 3+X and USIM 4+X forms keep their length on an untouched save
|
||||||
|
for (const hex of ['00' + '0000', '00' + '0000' + '02' + 'AABB', AD_HEX, 'FF'.repeat(6)]) {
|
||||||
|
for (const e of [sim, usim]) {
|
||||||
|
assert.strictEqual(e.enc(e.model(efDecAd(efBytes(hex))), hex), hex.toUpperCase(), hex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
test('encoder validation errors go through t() when the runtime provides it', () => {
|
test('encoder validation errors go through t() when the runtime provides it', () => {
|
||||||
globalThis.t = k => (k === 'Select at least one access technology' ? 'Выберите технологию' : k);
|
globalThis.t = k => (k === 'Select at least one access technology' ? 'Выберите технологию' : k);
|
||||||
try {
|
try {
|
||||||
@@ -573,7 +610,7 @@ test('F8: the ADN-family entries name their extension EF and use it in refusals'
|
|||||||
// Wave 2 fixtures.
|
// Wave 2 fixtures.
|
||||||
const UST_ONE = '00000010'; // service 29
|
const UST_ONE = '00000010'; // service 29
|
||||||
const UST_MULTI = '010204'; // services 1, 10, 19
|
const UST_MULTI = '010204'; // services 1, 10, 19
|
||||||
const UST_UNMODELLED = '000000' + '10' + '00'.repeat(14) + 'FC'; // 29 + bits 147-152
|
const UST_UNMODELLED = '000000' + '10' + '00'.repeat(14) + 'C0'; // 29 + bits 151-152 (above the 150-service table)
|
||||||
const EST_FF = 'FFFF';
|
const EST_FF = 'FFFF';
|
||||||
const ACC_ONE = '8001'; // classes 15 and 0
|
const ACC_ONE = '8001'; // classes 15 and 0
|
||||||
const AD_HEX = '00' + '0000' + '03' + 'AABB'; // op normal, info 0000, MNC 3, RFU
|
const AD_HEX = '00' + '0000' + '03' + 'AABB'; // op normal, info 0000, MNC 3, RFU
|
||||||
@@ -596,10 +633,13 @@ test('service tables: encode/decode round-trips and unmodelled bits', () => {
|
|||||||
const services = efDecServiceTable(efBytes(hex), table).services;
|
const services = efDecServiceTable(efBytes(hex), table).services;
|
||||||
assert.strictEqual(entry.enc({ services: services }, hex), hex, name + ' ' + hex);
|
assert.strictEqual(entry.enc({ services: services }, hex), hex, name + ' ' + hex);
|
||||||
}
|
}
|
||||||
// EF.SST has no editor (its 2-bit SIM layout needs a dedicated UI)
|
// EF.SST has its own 2-bit editor on the shared 6F38 FID (the DF-scoped
|
||||||
assert.strictEqual(efFindEncoder('EF.SST', '6f38'), null);
|
// entry keeps the UST answer for a bare FID / ADF context)
|
||||||
assert.strictEqual(efHasEncoder('EF.SST', '6f38'), false);
|
assert.strictEqual(efFindEncoder('EF.SST', '6f38').enc, efEncSst);
|
||||||
|
assert.strictEqual(efHasEncoder('EF.SST', '6f38'), true);
|
||||||
assert.strictEqual(efFindEncoder('EF.UST', '6f38').name, 'EF.UST');
|
assert.strictEqual(efFindEncoder('EF.UST', '6f38').name, 'EF.UST');
|
||||||
|
assert.strictEqual(efFindEncoder(null, '6f38').name, 'EF.UST');
|
||||||
|
assert.strictEqual(efFindEncoder(null, '6f38', '7F20').name, 'EF.SST');
|
||||||
// EST defines services 1-3; all-FF keeps the other bits too
|
// EST defines services 1-3; all-FF keeps the other bits too
|
||||||
assert.deepStrictEqual(efDecServiceTable(efBytes('07'), EF_EST_SERVICES).services,
|
assert.deepStrictEqual(efDecServiceTable(efBytes('07'), EF_EST_SERVICES).services,
|
||||||
[{ n: 1, name: 'Fixed Dialling Numbers (FDN)' },
|
[{ n: 1, name: 'Fixed Dialling Numbers (FDN)' },
|
||||||
@@ -615,20 +655,78 @@ test('service tables: encode/decode round-trips and unmodelled bits', () => {
|
|||||||
assert.strictEqual(entry.enc({ services: unmodelled }, UST_UNMODELLED), UST_UNMODELLED);
|
assert.strictEqual(entry.enc({ services: unmodelled }, UST_UNMODELLED), UST_UNMODELLED);
|
||||||
// unchecking everything clears the table bits but keeps the unmodelled byte
|
// unchecking everything clears the table bits but keeps the unmodelled byte
|
||||||
assert.strictEqual(entry.enc({ services: [] }, UST_UNMODELLED),
|
assert.strictEqual(entry.enc({ services: [] }, UST_UNMODELLED),
|
||||||
'000000' + '00' + '00'.repeat(14) + 'FC');
|
'000000' + '00' + '00'.repeat(14) + 'C0');
|
||||||
// length is kept
|
// length is kept
|
||||||
assert.strictEqual(entry.enc({ services: efDecServiceTable(efBytes('00000010' + '00'), EF_UST_SERVICES).services },
|
assert.strictEqual(entry.enc({ services: efDecServiceTable(efBytes('00000010' + '00'), EF_UST_SERVICES).services },
|
||||||
'0000001000'), '0000001000');
|
'0000001000'), '0000001000');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('service tables: unknown services and a too-short table are refused', () => {
|
test('service tables: unknown services and a too-short table are refused', () => {
|
||||||
assert.throws(() => efEncServiceTable({ services: [{ n: 147, name: 'RFU' }] },
|
assert.throws(() => efEncServiceTable({ services: [{ n: 151, name: 'RFU' }] },
|
||||||
'FF'.repeat(19), EF_UST_SERVICES), /Unknown service/);
|
'FF'.repeat(19), EF_UST_SERVICES), /Unknown service/);
|
||||||
assert.throws(() => efEncServiceTable({ services: [{ n: 9, name: 'MSISDN' }] },
|
assert.throws(() => efEncServiceTable({ services: [{ n: 9, name: 'MSISDN' }] },
|
||||||
'00', EF_UST_SERVICES), /too short for service/);
|
'00', EF_UST_SERVICES), /too short for service/);
|
||||||
assert.throws(() => efEncServiceTable({ services: [{ n: 0 }] }, '00', EF_UST_SERVICES), /Unknown service/);
|
assert.throws(() => efEncServiceTable({ services: [{ n: 0 }] }, '00', EF_UST_SERVICES), /Unknown service/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// EF.SST (TS 51.011 10.3.7): two bits per service, four per byte. 15 bytes =
|
||||||
|
// services 1-60; C7/1C/C1 carry on (11), off (10) and deleted (00) pairs and
|
||||||
|
// the last byte's bits 7-8 are service 60, above the 59-entry table.
|
||||||
|
const SST_MIX = 'C71CC1' + '00'.repeat(11) + 'C0';
|
||||||
|
|
||||||
|
test('EF.SST: the 2-bit encoder round-trips the decoded file', () => {
|
||||||
|
const dec = efDecSst(efBytes(SST_MIX));
|
||||||
|
assert.deepStrictEqual(dec.services.map(s => [s.n, s.allocated, s.activated]), [
|
||||||
|
[1, true, true], [2, true, false], [4, true, true], [6, true, true],
|
||||||
|
[7, true, false], [9, true, false], [12, true, true], [60, true, true],
|
||||||
|
]);
|
||||||
|
// a deleted (00) pair is omitted by the decoder, so decode->encode is exact
|
||||||
|
assert.strictEqual(efEncSst(dec, SST_MIX), SST_MIX);
|
||||||
|
// every state on: all-FF, unmodelled service 60 included
|
||||||
|
assert.strictEqual(efEncSst(efDecSst(efBytes('FF'.repeat(15))), 'FF'.repeat(15)), 'FF'.repeat(15));
|
||||||
|
// no service allocated at all
|
||||||
|
assert.strictEqual(efEncSst(efDecSst(efBytes('00'.repeat(15))), '00'.repeat(15)), '00'.repeat(15));
|
||||||
|
// clearing every modelled service keeps service 60's pair (byte 14 C0)
|
||||||
|
assert.strictEqual(efEncSst({ services: [] }, SST_MIX), '000000' + '00'.repeat(11) + 'C0');
|
||||||
|
// the file length is kept
|
||||||
|
assert.strictEqual(efEncSst(efDecSst(efBytes('0C')), '0C'), '0C');
|
||||||
|
assert.strictEqual(efEncSst(efDecSst(efBytes('0C00')), '0C00'), '0C00');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('EF.SST: deleted clears the pair, off sets 10 and on sets 11', () => {
|
||||||
|
const on = n => ({ n: n, allocated: true, activated: true });
|
||||||
|
const off = n => ({ n: n, allocated: true, activated: false });
|
||||||
|
const del = n => ({ n: n, allocated: false, activated: false });
|
||||||
|
// service 2 (byte 0, pair 2)
|
||||||
|
assert.strictEqual(efEncSst({ services: [on(2)] }, '00'), '0C');
|
||||||
|
assert.strictEqual(efEncSst({ services: [off(2)] }, '00'), '04');
|
||||||
|
assert.strictEqual(efEncSst({ services: [del(2)] }, '0C'), '00');
|
||||||
|
// every modelled pair is rebuilt from the list, the rest of the file stays
|
||||||
|
assert.strictEqual(efEncSst({ services: [on(2)] }, 'FF'.repeat(15)),
|
||||||
|
'0C' + '00'.repeat(13) + 'C0');
|
||||||
|
assert.strictEqual(efEncSst({ services: [on(2), off(4)] }, SST_MIX).slice(0, 2), '4C');
|
||||||
|
// a pair above the table is written like any other (a longer file)
|
||||||
|
const long = '00'.repeat(16);
|
||||||
|
assert.strictEqual(efEncSst({ services: [on(62)] }, long), '00'.repeat(15) + '0C');
|
||||||
|
// the decoder's illegal '08' pair (activated, not allocated) is refused
|
||||||
|
assert.deepStrictEqual(efDecSst(efBytes('08')), { services: [
|
||||||
|
{ n: 2, name: 'Abbreviated Dialling Numbers (ADN)', allocated: false, activated: true },
|
||||||
|
] });
|
||||||
|
assert.throws(() => efEncSst(efDecSst(efBytes('08')), '08'), /must be deleted, off or on/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('EF.SST: unknown services, an invalid state and a too-short file are refused', () => {
|
||||||
|
assert.throws(() => efEncSst({ services: [{ n: 5, allocated: true, activated: true }] }, '00'),
|
||||||
|
/too short for service/);
|
||||||
|
assert.throws(() => efEncSst({ services: [{ n: 0, allocated: true, activated: true }] }, '00'),
|
||||||
|
/Unknown service/);
|
||||||
|
assert.throws(() => efEncSst({ services: [{ n: 'x' }] }, '00'), /Unknown service/);
|
||||||
|
assert.throws(() => efEncSst({ services: [{ n: 2, allocated: false, activated: true }] }, '00'),
|
||||||
|
/must be deleted, off or on/);
|
||||||
|
// a bare service number carries no state
|
||||||
|
assert.throws(() => efEncSst({ services: [2] }, '00'), /must be deleted, off or on/);
|
||||||
|
});
|
||||||
|
|
||||||
test('F-ACC: the class numbering and the ACC10/encoder rules', () => {
|
test('F-ACC: the class numbering and the ACC10/encoder rules', () => {
|
||||||
assert.deepStrictEqual(efDecAcc(efBytes('8000')), { access_classes: [15] });
|
assert.deepStrictEqual(efDecAcc(efBytes('8000')), { access_classes: [15] });
|
||||||
assert.deepStrictEqual(efDecAcc(efBytes('0001')), { access_classes: [0] });
|
assert.deepStrictEqual(efDecAcc(efBytes('0001')), { access_classes: [0] });
|
||||||
|
|||||||
@@ -48,22 +48,24 @@ const FNS = [
|
|||||||
'efEncPlmnList', 'efListRows', 'efPlmnRowOrig', 'efEncAdn', 'efEncAdnExt', 'efPnnNameBytes', 'efPnnHexBytes', 'efPnnOrigTlv', 'efEncPnn',
|
'efEncPlmnList', 'efListRows', 'efPlmnRowOrig', 'efEncAdn', 'efEncAdnExt', 'efPnnNameBytes', 'efPnnHexBytes', 'efPnnOrigTlv', 'efEncPnn',
|
||||||
'efFindEncoder', 'efHasEncoder', 'efEncError',
|
'efFindEncoder', 'efHasEncoder', 'efEncError',
|
||||||
'efDecModelValues', 'efDecModelFor', 'efDecEditModel', 'efActTechList', 'efDecFieldHtml',
|
'efDecModelValues', 'efDecModelFor', 'efDecEditModel', 'efActTechList', 'efDecFieldHtml',
|
||||||
|
'servicesFeatureByNumber', 'pysimFsFilterBtnHtml',
|
||||||
'efDecRowHtml', 'efDecGridHtml', 'efDecItemRowHtml', 'efDecItemsHtml',
|
'efDecRowHtml', 'efDecGridHtml', 'efDecItemRowHtml', 'efDecItemsHtml',
|
||||||
'efDecRecordsHtml', 'efDecEditHtml',
|
'efDecRecordsHtml', 'efDecEditHtml',
|
||||||
'efDecCollectValues', 'efDecCollectRows', 'efDecCollectItems',
|
'efDecCollectValues', 'efDecCollectRows', 'efDecCollectItems',
|
||||||
'efBitsSelected', 'pysimFsDecBitsFilter', 'pysimFsDecRowCheck', 'pysimFsDecItemTag', 'pysimFsDecAddItem',
|
'efBitsSelected', 'pysimFsDecBitsFilter', 'pysimFsDecBits2Filter', 'pysimFsDecRowCheck', 'pysimFsDecItemTag', 'pysimFsDecAddItem',
|
||||||
'pysimFsDecRemoveItem', 'pysimFsFindNode',
|
'pysimFsDecRemoveItem', 'pysimFsFindNodeByPath', 'pysimCustomNormPath', 'pysimFsNodePath', 'pysimFsFallbackNode', 'efFidFromPath', 'efDfFromPath',
|
||||||
'pysimFsHasDecoder', 'pysimFsHasEncoder', 'pysimFsDecSave', 'pysimFsDecEdit',
|
'pysimFsHasDecoder', 'pysimFsHasEncoder', 'pysimFsDecSave', 'pysimFsDecEdit',
|
||||||
'pysimFsPillsEnabled', 'pysimFsSyncContentVisibility', 'gsm7Decode',
|
'pysimFsPillsEnabled', 'pysimFsSyncContentVisibility', 'gsm7Decode',
|
||||||
'gsm7TextToSeptets', 'gsm7Encode',
|
'gsm7TextToSeptets', 'gsm7Encode',
|
||||||
'efDecServiceTable', 'efDecSst', 'efDecAcc', 'efDecPhase', 'efDecHpplmn', 'efDecAd', 'efDecCbmi', 'efDecCbmir', 'efDecIccid', 'efDecImsi', 'efDecPlmnList', 'efDecFplmn', 'efEncServiceTable', 'efEncAcc', 'efPhaseValue', 'efPhaseModel', 'efEncPhase', 'efEncHpplmn', 'efAdOpValue', 'efAdModel', 'efEncAd', 'efEncPlmnOnly', 'efEncIccid', 'efEncImsi', 'efCbmiModel', 'efEncCbmi', 'efCbmirModel', 'efEncCbmir', 'efSmspAddressModel', 'efSmspModel', 'efSmspTonOptions', 'efSmspParamRow', 'efSmspHtml', 'efSmspCollect', 'pysimFsSmspToggle', 'pysimFsSmspHexInput', 'efSmspHexByte', 'efSmspAddressBytes', 'efSmspAddrUnchanged', 'efSmspByteUnchanged', 'efEncSmsp', 'smspT', 'smspDcsHint', 'smspVpHint', 'smspDurationText', 'smspPluralIndex', 'smspCount', 'efSmspDaNumber',
|
'efDecServiceTable', 'efDecSst', 'efDecAcc', 'efDecPhase', 'efDecHpplmn', 'efDecAd', 'efDecCbmi', 'efDecCbmir', 'efDecIccid', 'efDecImsi', 'efDecPlmnList', 'efDecFplmn', 'efEncServiceTable', 'efEncSst', 'efEncAcc', 'efPhaseValue', 'efPhaseModel', 'efEncPhase', 'efEncHpplmn', 'efAdOpValue', 'efAdModel', 'efEncAd', 'efEncPlmnOnly', 'efEncIccid', 'efEncImsi', 'efCbmiModel', 'efEncCbmi', 'efCbmirModel', 'efEncCbmir', 'efSmspAddressModel', 'efSmspModel', 'efSmspTonOptions', 'efSmspParamRow', 'efSmspHtml', 'efSmspCollect', 'pysimFsSmspToggle', 'pysimFsSmspHexInput', 'efSmspHexByte', 'efSmspAddressBytes', 'efSmspAddrUnchanged', 'efSmspByteUnchanged', 'efEncSmsp', 'smspT', 'smspDcsHint', 'smspVpHint', 'smspDurationText', 'smspPluralIndex', 'smspCount', 'efSmspDaNumber',
|
||||||
];
|
];
|
||||||
|
|
||||||
let code = 'var pysimFsSelected = null;\nvar pysimFsTreeRoot = null;\nvar pysimFsDecModel = null;\n'
|
let code = 'var pysimFsSelected = null;\nvar pysimFsTreeRoot = null;\nvar pysimFsDecModel = null;\n'
|
||||||
+ 'var pysimFsDecEditMode = false;\nvar pysimFsEditMode = false;\n';
|
+ 'var pysimFsDecEditMode = false;\nvar pysimFsEditMode = false;\nvar _servicesFeatureByNumber = null;\n';
|
||||||
for (const f of FNS) code += extractFunc(html, f) + '\n';
|
for (const f of FNS) code += extractFunc(html, f) + '\n';
|
||||||
for (const c of ['GSM7_ALPHABET', 'GSM7_EXT_MAP', 'EF_ACT_TECHS', 'EF_SPN_FIELDS',
|
for (const c of ['GSM7_ALPHABET', 'GSM7_EXT_MAP', 'EF_ACT_TECHS', 'EF_SPN_FIELDS',
|
||||||
'EF_ADN_FIELDS', 'EF_PLMN_LIST', 'EF_PNN_ITEMS', 'EF_UST_SERVICES', 'EF_SST_SERVICES', 'EF_IST_SERVICES', 'EF_EST_SERVICES', 'EF_PHASE_NAMES', 'EF_ACC_CLASSES', 'EF_UST_FIELDS', 'EF_IST_FIELDS', 'EF_EST_FIELDS', 'EF_ACC_FIELDS', 'EF_PHASE_FIELDS', 'EF_HPPLMN_FIELDS', 'EF_AD_OP_MODES', 'EF_AD_FIELDS', 'EF_PLMN_ONLY_LIST', 'EF_ICCID_FIELDS', 'EF_IMSI_FIELDS', 'EF_CBMI_LIST', 'EF_CBMIR_LIST', 'EF_READONLY_NAMES', 'EF_SMSP_PARAMS', 'EF_SMSP_CUSTOM', 'EF_ENCODERS']) {
|
'EF_ADN_FIELDS', 'EF_PLMN_LIST', 'EF_PNN_ITEMS', 'EF_UST_SERVICES', 'EF_SST_SERVICES', 'EF_IST_SERVICES', 'EF_EST_SERVICES', 'EF_PHASE_NAMES', 'EF_ACC_CLASSES', 'EF_UST_FIELDS', 'EF_IST_FIELDS', 'EF_EST_FIELDS', 'EF_SST_FIELDS', 'EF_ACC_FIELDS', 'EF_PHASE_FIELDS', 'EF_HPPLMN_FIELDS', 'EF_AD_OP_MODES', 'EF_AD_FIELDS', 'EF_PLMN_ONLY_LIST', 'EF_ICCID_FIELDS', 'EF_IMSI_FIELDS', 'EF_CBMI_LIST', 'EF_CBMIR_LIST', 'EF_READONLY_NAMES', 'EF_SMSP_PARAMS', 'EF_SMSP_CUSTOM', 'EF_ENCODERS', 'EF_ADF_RIDS',
|
||||||
|
'SERVICES_CROSS_REF', 'SERVICES_FS_MAP']) {
|
||||||
code += extractConst(html, c);
|
code += extractConst(html, c);
|
||||||
}
|
}
|
||||||
eval(code);
|
eval(code);
|
||||||
@@ -253,24 +255,50 @@ test('changing a record input checks its block', () => {
|
|||||||
assert.strictEqual(cb.checked, true);
|
assert.strictEqual(cb.checked, true);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('pysimFsHasEncoder resolves the selected file by name or FID', () => {
|
test('pysimFsHasEncoder resolves the selected file by its path or FID', () => {
|
||||||
pysimFsTreeRoot = { name: 'MF', children: [{ name: 'EF.SPN', fid: '6f46' }, { name: 'EF.ALIAS', fid: '6f3a' }] };
|
pysimFsTreeRoot = { name: 'MF', children: [] };
|
||||||
pysimFsSelected = 'EF.SPN';
|
const add = (name, fid) => {
|
||||||
|
const n = { name: name, fid: fid, parent: pysimFsTreeRoot };
|
||||||
|
pysimFsTreeRoot.children.push(n);
|
||||||
|
return n;
|
||||||
|
};
|
||||||
|
add('EF.SPN', '6f46');
|
||||||
|
add('EF.ALIAS', '6f3a');
|
||||||
|
pysimFsSelected = 'MF/6F46';
|
||||||
assert.strictEqual(pysimFsHasEncoder(), true);
|
assert.strictEqual(pysimFsHasEncoder(), true);
|
||||||
pysimFsSelected = 'EF.ALIAS';
|
pysimFsSelected = 'MF/6F3A';
|
||||||
assert.strictEqual(pysimFsHasEncoder(), true);
|
assert.strictEqual(pysimFsHasEncoder(), true);
|
||||||
|
// a bare FID selection still resolves through the FID fallback
|
||||||
pysimFsSelected = '6F46';
|
pysimFsSelected = '6F46';
|
||||||
assert.strictEqual(pysimFsHasEncoder(), true);
|
assert.strictEqual(pysimFsHasEncoder(), true);
|
||||||
pysimFsSelected = 'EF.LOCI';
|
pysimFsSelected = 'MF/6F7E';
|
||||||
assert.strictEqual(pysimFsHasEncoder(), false);
|
|
||||||
// the SIM service table shares 6F38 with UST but has no editor
|
|
||||||
pysimFsTreeRoot = { name: 'MF', children: [{ name: 'EF.SST', fid: '6f38' }] };
|
|
||||||
pysimFsSelected = 'EF.SST';
|
|
||||||
assert.strictEqual(pysimFsHasEncoder(), false);
|
assert.strictEqual(pysimFsHasEncoder(), false);
|
||||||
|
// the SIM service table shares 6F38 with UST but has its own 2-bit editor
|
||||||
|
pysimFsTreeRoot = { name: 'MF', children: [] };
|
||||||
|
const gsm = { name: 'DF.GSM', fid: '7F20', parent: pysimFsTreeRoot, children: [] };
|
||||||
|
pysimFsTreeRoot.children.push(gsm);
|
||||||
|
const sst = { name: 'EF.SST', fid: '6f38', parent: gsm };
|
||||||
|
gsm.children.push(sst);
|
||||||
|
pysimFsSelected = 'MF/7F20/6F38';
|
||||||
|
assert.strictEqual(pysimFsHasEncoder(), true);
|
||||||
pysimFsSelected = null;
|
pysimFsSelected = null;
|
||||||
assert.strictEqual(pysimFsHasEncoder(), false);
|
assert.strictEqual(pysimFsHasEncoder(), false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('the file-manager resolver call sites pass the parent DF', () => {
|
||||||
|
// the DF context comes from the selected node's canonical path (or the
|
||||||
|
// selected path itself when the node is not loaded); these sites
|
||||||
|
// gate/read/edit a selected file and must resolve the same entry
|
||||||
|
assert.ok(html.includes('return !!efFindDecoder(node && node.name, fid, df);'));
|
||||||
|
assert.ok(html.includes('return !!efFindEncoder(node && node.name, fid, df);'));
|
||||||
|
assert.ok(html.includes("const df = node ? efDfFromPath(pysimFsNodePath(node)) : efDfFromPath(pysimFsSelected);"));
|
||||||
|
assert.ok(html.includes('const model = efDecEditModel(node && node.name, fid, content,'));
|
||||||
|
for (const fn of ['pysimFsDecSave', 'pysimFsDecAddRow', 'pysimFsDecAddItem']) {
|
||||||
|
assert.ok(/efFindEncoder\([^;]*efDfFromPath\(pysimFsNodePath\(node\)\)/s.test(extractFunc(html, fn)),
|
||||||
|
fn + ' resolves the entry with the node DF');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
test('the Edit decoded button is encoder-gated next to Edit raw', () => {
|
test('the Edit decoded button is encoder-gated next to Edit raw', () => {
|
||||||
const btn = /<button[^>]*id="pysim-fs-edit-dec-btn"[^>]*>/.exec(html);
|
const btn = /<button[^>]*id="pysim-fs-edit-dec-btn"[^>]*>/.exec(html);
|
||||||
assert.ok(btn, 'Edit decoded button');
|
assert.ok(btn, 'Edit decoded button');
|
||||||
@@ -634,8 +662,9 @@ test('F2: untouched PNN items keep their TLV bytes through the DOM and the save'
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('F12: a selection change during the read abandons the decoded edit', async () => {
|
test('F12: a selection change during the read abandons the decoded edit', async () => {
|
||||||
pysimFsSelected = 'EF.SPN';
|
pysimFsSelected = 'MF/6F46';
|
||||||
pysimFsTreeRoot = { name: 'MF', children: [{ name: 'EF.SPN', fid: '6f46' }] };
|
pysimFsTreeRoot = { name: 'MF', children: [] };
|
||||||
|
pysimFsTreeRoot.children.push({ name: 'EF.SPN', fid: '6f46', parent: pysimFsTreeRoot });
|
||||||
const fakeEl = () => {
|
const fakeEl = () => {
|
||||||
const classes = new Set();
|
const classes = new Set();
|
||||||
return {
|
return {
|
||||||
@@ -668,7 +697,7 @@ test('F12: a selection change during the read abandons the decoded edit', async
|
|||||||
let resets = 0;
|
let resets = 0;
|
||||||
globalThis.pysimFsResetEdit = () => { resets++; };
|
globalThis.pysimFsResetEdit = () => { resets++; };
|
||||||
const pending = pysimFsDecEdit();
|
const pending = pysimFsDecEdit();
|
||||||
pysimFsSelected = 'EF.ADN';
|
pysimFsSelected = 'MF/6F3A';
|
||||||
release();
|
release();
|
||||||
await pending;
|
await pending;
|
||||||
assert.strictEqual(resets, 1);
|
assert.strictEqual(resets, 1);
|
||||||
@@ -689,8 +718,8 @@ test('the bits field renders a filtered checkbox grid and collects {n,name}', ()
|
|||||||
assert.match(out, /class="pysim-dec-bits /);
|
assert.match(out, /class="pysim-dec-bits /);
|
||||||
assert.match(out, /pysim-dec-bits-filter/);
|
assert.match(out, /pysim-dec-bits-filter/);
|
||||||
assert.match(out, /data-bit="29" data-bit-name="Data download via SMS-CB" checked/);
|
assert.match(out, /data-bit="29" data-bit-name="Data download via SMS-CB" checked/);
|
||||||
// the whole UST table is rendered (146 services)
|
// the whole UST table is rendered (150 services)
|
||||||
assert.strictEqual((out.match(/data-bit="/g) || []).length, 146);
|
assert.strictEqual((out.match(/data-bit="/g) || []).length, 150);
|
||||||
// ACC: 15 editable classes (10 is always written 0), 15 and 0 checked
|
// ACC: 15 editable classes (10 is always written 0), 15 and 0 checked
|
||||||
const acc = efDecEditHtml(efDecEditModel('EF.ACC', '6f78', { kind: 'transparent', data: ACC_ONE }), ACC_ONE);
|
const acc = efDecEditHtml(efDecEditModel('EF.ACC', '6f78', { kind: 'transparent', data: ACC_ONE }), ACC_ONE);
|
||||||
assert.strictEqual((acc.match(/data-bit="/g) || []).length, 15);
|
assert.strictEqual((acc.match(/data-bit="/g) || []).length, 15);
|
||||||
@@ -712,6 +741,20 @@ test('the bits field renders a filtered checkbox grid and collects {n,name}', ()
|
|||||||
] });
|
] });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('the service grids link file-backed rows to the file tree filter', () => {
|
||||||
|
const ust = efDecEditHtml(efDecEditModel('EF.UST', '6f38', { kind: 'transparent', data: UST_ONE }), UST_ONE);
|
||||||
|
// service 2 = FDN (files), service 99 = URI support by UICC (capability)
|
||||||
|
assert.match(ust, /pysimFsSetFilterToService\('ust', 2\)/);
|
||||||
|
assert.ok(!/pysimFsSetFilterToService\('ust', 99\)/.test(ust));
|
||||||
|
const sst = efDecEditHtml(efDecEditModel('EF.SST', '6f38', { kind: 'transparent', data: '4C' }, '7F20'), '4C');
|
||||||
|
// service 4 = SMS (files), service 1 = CHV1 disable function (capability)
|
||||||
|
assert.match(sst, /pysimFsSetFilterToService\('sst', 4\)/);
|
||||||
|
assert.ok(!/pysimFsSetFilterToService\('sst', 1\)/.test(sst));
|
||||||
|
// EF.ACC's bits grid is not a service table - no cross-link at all
|
||||||
|
const acc = efDecEditHtml(efDecEditModel('EF.ACC', '6f78', { kind: 'transparent', data: ACC_ONE }), ACC_ONE);
|
||||||
|
assert.ok(!/pysimFsSetFilterToService/.test(acc));
|
||||||
|
});
|
||||||
|
|
||||||
test('the bits filter hides the non-matching rows', () => {
|
test('the bits filter hides the non-matching rows', () => {
|
||||||
const rows = [
|
const rows = [
|
||||||
{ textContent: '1 Local Phone Book', style: {} },
|
{ textContent: '1 Local Phone Book', style: {} },
|
||||||
@@ -724,6 +767,91 @@ test('the bits filter hides the non-matching rows', () => {
|
|||||||
assert.strictEqual(rows[1].style.display, '');
|
assert.strictEqual(rows[1].style.display, '');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('the EF.SST grid renders one tri-state select per service', () => {
|
||||||
|
// 4C: service 2 on (11), service 4 off (10), the rest of the pairs deleted
|
||||||
|
const model = efDecEditModel('EF.SST', '6f38', { kind: 'transparent', data: '4C' }, '7F20');
|
||||||
|
assert.deepStrictEqual(model.values.services, [
|
||||||
|
{ n: 2, name: 'Abbreviated Dialling Numbers (ADN)', allocated: true, activated: true },
|
||||||
|
{ n: 4, name: 'Short Message Storage (SMS)', allocated: true, activated: false },
|
||||||
|
]);
|
||||||
|
const out = efDecEditHtml(model, '4C');
|
||||||
|
assert.match(out, /class="pysim-dec-bits /);
|
||||||
|
assert.match(out, /pysim-dec-bits-filter/);
|
||||||
|
assert.match(out, /data-bit2="2" data-bit2-name="Abbreviated Dialling Numbers \(ADN\)"/);
|
||||||
|
// the whole SST table is rendered (59 services, RFU 8/20/50 included)
|
||||||
|
assert.strictEqual((out.match(/data-bit2="/g) || []).length, 59);
|
||||||
|
assert.match(out, /data-bit2="8" data-bit2-name="RFU"/);
|
||||||
|
assert.match(out, /data-bit2="20" data-bit2-name="RFU"/);
|
||||||
|
assert.match(out, /data-bit2="50" data-bit2-name="Reserved and shall be ignored"/);
|
||||||
|
// each select is preselected from the decoded state, an absent pair is deleted
|
||||||
|
const sel = n => {
|
||||||
|
const m = new RegExp('<select data-bit2="' + n + '"[^>]*>([\\s\\S]*?)</select>').exec(out);
|
||||||
|
assert.ok(m, 'select for service ' + n);
|
||||||
|
return m[1];
|
||||||
|
};
|
||||||
|
assert.match(sel(2), /<option value="on" selected>On<\/option>/);
|
||||||
|
assert.match(sel(4), /<option value="off" selected>Off<\/option>/);
|
||||||
|
assert.match(sel(1), /<option value="deleted" selected>Deleted<\/option>/);
|
||||||
|
assert.match(sel(59), /<option value="deleted" selected>Deleted<\/option>/);
|
||||||
|
for (const n of [2, 4, 1, 59]) {
|
||||||
|
assert.match(sel(n), />Deleted<\/option>/);
|
||||||
|
assert.match(sel(n), />Off<\/option>/);
|
||||||
|
assert.match(sel(n), />On<\/option>/);
|
||||||
|
}
|
||||||
|
// an all-FF file preselects every modelled service 'on'
|
||||||
|
const ff = efDecEditHtml(efDecEditModel('EF.SST', '6f38',
|
||||||
|
{ kind: 'transparent', data: 'FF'.repeat(15) }, '7F20'), 'FF'.repeat(15));
|
||||||
|
assert.strictEqual((ff.match(/<option value="on" selected>/g) || []).length, 59);
|
||||||
|
assert.ok(!/<option value="(deleted|off)" selected>/.test(ff));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('the EF.SST collector yields {n, name, allocated, activated}', () => {
|
||||||
|
const row = (n, name, state) => ({
|
||||||
|
value: state,
|
||||||
|
getAttribute: k => k === 'data-bit2' ? String(n) : name,
|
||||||
|
});
|
||||||
|
const root = {
|
||||||
|
querySelectorAll: sel => sel === '[data-bit2]' ? [
|
||||||
|
row(2, 'Abbreviated Dialling Numbers (ADN)', 'on'),
|
||||||
|
row(1, 'CHV1 disable function', 'deleted'),
|
||||||
|
row(4, 'Short Message Storage (SMS)', 'off'),
|
||||||
|
] : [],
|
||||||
|
};
|
||||||
|
assert.deepStrictEqual(efDecCollectValues(root, EF_SST_FIELDS), { services: [
|
||||||
|
{ n: 2, name: 'Abbreviated Dialling Numbers (ADN)', allocated: true, activated: true },
|
||||||
|
{ n: 1, name: 'CHV1 disable function', allocated: false, activated: false },
|
||||||
|
{ n: 4, name: 'Short Message Storage (SMS)', allocated: true, activated: false },
|
||||||
|
] });
|
||||||
|
// an untouched form re-encodes to the original bytes
|
||||||
|
const dec = efDecSst(efBytes('4C'));
|
||||||
|
const values = efDecCollectValues({
|
||||||
|
querySelectorAll: () => [2, 4].map(n => row(n, EF_SST_SERVICES[n],
|
||||||
|
dec.services.find(s => s.n === n).activated ? 'on' : 'off')),
|
||||||
|
}, EF_SST_FIELDS);
|
||||||
|
assert.strictEqual(efFindEncoder('EF.SST', '6f38', '7F20').enc(values, '4C'), '4C');
|
||||||
|
// the EF.SST field is the tri-state kind, the 1-bit tables are untouched
|
||||||
|
assert.strictEqual(EF_SST_FIELDS[0].kind, 'bits2');
|
||||||
|
assert.strictEqual(EF_UST_FIELDS[0].kind, 'bits');
|
||||||
|
assert.strictEqual(EF_EST_FIELDS[0].kind, 'bits');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('the tri-state grid filter matches the row label, not the select options', () => {
|
||||||
|
const rows = [
|
||||||
|
{ getAttribute: k => k === 'data-label' ? '1 CHV1 disable function' : '', style: {} },
|
||||||
|
{ getAttribute: k => k === 'data-label' ? '4 Short Message Storage (SMS)' : '', style: {} },
|
||||||
|
];
|
||||||
|
const box = { querySelectorAll: sel => sel === '.pysim-dec-bit2-row' ? rows : [] };
|
||||||
|
const input = { value: 'sms', closest: sel => sel === '.pysim-dec-bits' ? box : null };
|
||||||
|
pysimFsDecBits2Filter(input);
|
||||||
|
assert.strictEqual(rows[0].style.display, 'none');
|
||||||
|
assert.strictEqual(rows[1].style.display, '');
|
||||||
|
// 'off' is a state option of every row, never part of a service name
|
||||||
|
pysimFsDecBits2Filter({ value: 'off', closest: () => box });
|
||||||
|
assert.strictEqual(rows[0].style.display, 'none');
|
||||||
|
assert.strictEqual(rows[1].style.display, 'none');
|
||||||
|
pysimFsDecBits2Filter(null);
|
||||||
|
});
|
||||||
|
|
||||||
test('the select keeps an unknown current value as an option', () => {
|
test('the select keeps an unknown current value as an option', () => {
|
||||||
const model = efDecEditModel('EF.PHASE', '6fae', { kind: 'transparent', data: '0F' });
|
const model = efDecEditModel('EF.PHASE', '6fae', { kind: 'transparent', data: '0F' });
|
||||||
assert.deepStrictEqual(model.values, { phase_hex: '0F' });
|
assert.deepStrictEqual(model.values, { phase_hex: '0F' });
|
||||||
|
|||||||
@@ -29,10 +29,12 @@ let code = 'var pysimFsDecodedMode = false;\n'
|
|||||||
+ 'var pysimFsSelected = null;\n'
|
+ 'var pysimFsSelected = null;\n'
|
||||||
+ 'var pysimFsTreeRoot = null;\n';
|
+ 'var pysimFsTreeRoot = null;\n';
|
||||||
for (const fn of ['pysimFsSetMode', 'pysimFsPillsEnabled', 'pysimFsEdit',
|
for (const fn of ['pysimFsSetMode', 'pysimFsPillsEnabled', 'pysimFsEdit',
|
||||||
'pysimFsCancel', 'pysimFsResetEdit', 'pysimFsFindNode', 'pysimFsHasDecoder',
|
'pysimFsCancel', 'pysimFsResetEdit', 'pysimFsFindNodeByPath', 'pysimCustomNormPath',
|
||||||
|
'pysimFsNodePath', 'efFidFromPath', 'efDfFromPath', 'pysimFsHasDecoder',
|
||||||
'pysimFsSyncContentVisibility']) {
|
'pysimFsSyncContentVisibility']) {
|
||||||
code += extractFunc(html, fn) + '\n';
|
code += extractFunc(html, fn) + '\n';
|
||||||
}
|
}
|
||||||
|
code += html.match(/const EF_ADF_RIDS = \[[\s\S]*?\n\];/)[0].replace('const ', 'var ') + '\n';
|
||||||
eval(code);
|
eval(code);
|
||||||
|
|
||||||
let events = [];
|
let events = [];
|
||||||
@@ -83,8 +85,9 @@ function setup(opts) {
|
|||||||
pysimFsDecodedMode = !!opts.decoded;
|
pysimFsDecodedMode = !!opts.decoded;
|
||||||
pysimFsEditMode = false;
|
pysimFsEditMode = false;
|
||||||
pysimFsEditData = null;
|
pysimFsEditData = null;
|
||||||
pysimFsSelected = 'EF.IMSI';
|
pysimFsSelected = 'MF/6F07';
|
||||||
pysimFsTreeRoot = { name: 'MF', fid: '3f00', children: [{ name: 'EF.IMSI', fid: '6f07' }] };
|
pysimFsTreeRoot = { name: 'MF', fid: '3f00', children: [] };
|
||||||
|
pysimFsTreeRoot.children.push({ name: 'EF.IMSI', fid: '6f07', parent: pysimFsTreeRoot });
|
||||||
globalThis.efFindDecoder = (name, fid) => {
|
globalThis.efFindDecoder = (name, fid) => {
|
||||||
const n = (name || '').toUpperCase();
|
const n = (name || '').toUpperCase();
|
||||||
const f = (fid || '').toLowerCase();
|
const f = (fid || '').toLowerCase();
|
||||||
@@ -205,12 +208,16 @@ test('Read decoded is ignored when the file has no decoder', () => {
|
|||||||
assert.ok(!h.pill('dec').classList.contains('bg-blue-600'));
|
assert.ok(!h.pill('dec').classList.contains('bg-blue-600'));
|
||||||
});
|
});
|
||||||
|
|
||||||
test('pysimFsHasDecoder resolves the selected file by name or FID', () => {
|
test('pysimFsHasDecoder resolves the selected file by its path or FID', () => {
|
||||||
setup();
|
setup();
|
||||||
assert.strictEqual(pysimFsHasDecoder(), true, 'EF.IMSI is in the tree');
|
assert.strictEqual(pysimFsHasDecoder(), true, 'EF.IMSI is in the tree');
|
||||||
// alias name: the decoder is matched by the FID fallback
|
// alias name: the decoder is matched by the FID fallback
|
||||||
pysimFsTreeRoot = { name: 'MF', children: [{ name: 'EF.ALIAS', fid: '6f07' }] };
|
pysimFsTreeRoot = { name: 'MF', children: [{ name: 'EF.ALIAS', fid: '6f07' }] };
|
||||||
pysimFsSelected = 'EF.ALIAS';
|
pysimFsTreeRoot.children[0].parent = pysimFsTreeRoot;
|
||||||
|
pysimFsSelected = 'MF/6F07';
|
||||||
|
assert.strictEqual(pysimFsHasDecoder(), true);
|
||||||
|
// a bare FID selection (an API caller) resolves by the FID fallback
|
||||||
|
pysimFsSelected = '6F07';
|
||||||
assert.strictEqual(pysimFsHasDecoder(), true);
|
assert.strictEqual(pysimFsHasDecoder(), true);
|
||||||
// unknown file -> no decoder
|
// unknown file -> no decoder
|
||||||
globalThis.efFindDecoder = () => null;
|
globalThis.efFindDecoder = () => null;
|
||||||
|
|||||||
@@ -0,0 +1,665 @@
|
|||||||
|
const { test } = require('node:test');
|
||||||
|
const assert = require('node:assert');
|
||||||
|
const fs = require('node:fs');
|
||||||
|
const path = require('node:path');
|
||||||
|
|
||||||
|
const html = fs.readFileSync(path.join(__dirname, '..', 'index.html'), 'utf8');
|
||||||
|
|
||||||
|
function extractFunc(src, name) {
|
||||||
|
const re = new RegExp('(?:async\\s+)?function\\s+' + name + '\\s*\\([^)]*\\)\\s*\\{');
|
||||||
|
const m = re.exec(src);
|
||||||
|
if (!m) throw new Error('function ' + name + ' not found');
|
||||||
|
let i = m.index + m[0].length - 1;
|
||||||
|
let depth = 0;
|
||||||
|
for (; i < src.length; i++) {
|
||||||
|
if (src[i] === '{') depth++;
|
||||||
|
else if (src[i] === '}') {
|
||||||
|
depth--;
|
||||||
|
if (depth === 0) break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return src.slice(m.index, i + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractConst(src, name) {
|
||||||
|
const m = new RegExp('const ' + name + ' = ').exec(src);
|
||||||
|
if (!m) throw new Error('const ' + name + ' not found');
|
||||||
|
const begin = m.index + m[0].length;
|
||||||
|
const opener = src[begin];
|
||||||
|
const closer = opener === '[' ? ']' : (opener === '{' ? '}' : null);
|
||||||
|
if (!closer) throw new Error('unsupported const ' + name);
|
||||||
|
let depth = 0, i = begin;
|
||||||
|
for (; i < src.length; i++) {
|
||||||
|
if (src[i] === opener) depth++;
|
||||||
|
else if (src[i] === closer) {
|
||||||
|
depth--;
|
||||||
|
if (depth === 0) { i++; break; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 'var ' + name + ' = ' + src.slice(begin, i) + ';\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractStrConst(src, name) {
|
||||||
|
const m = new RegExp("const " + name + " = '([^']*)';").exec(src);
|
||||||
|
if (!m) throw new Error('const ' + name + ' not found');
|
||||||
|
return 'var ' + name + " = '" + m[1] + "';\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
// module state the extracted functions close over
|
||||||
|
let pysimFsFilter = new Set(['all']);
|
||||||
|
let _fsFilterIndex = null;
|
||||||
|
// The shipped standard list stands in for the fetched one (uicc_files.json is
|
||||||
|
// what uiccStdFilesLoad caches; the filter reads its entries synchronously).
|
||||||
|
const stdAsset = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'uicc_files.json'), 'utf8'));
|
||||||
|
let _uiccStdFiles = stdAsset.files;
|
||||||
|
let _fsFilterPathIndex = null;
|
||||||
|
let _fsFilterPathIndexSrc = null;
|
||||||
|
let _fsFilterPaths = null;
|
||||||
|
let _fsFilterPathsKey = null;
|
||||||
|
let pysimCustomFiles = [];
|
||||||
|
const _store = {};
|
||||||
|
let _treeRenders = 0;
|
||||||
|
let pysimFsTreeRoot = null; // expand-all walks this; null = nothing to expand
|
||||||
|
let pysimFsLoadChildren = async node => { if (!node.children) node.children = []; };
|
||||||
|
|
||||||
|
globalThis.localStorage = {
|
||||||
|
getItem: k => (Object.prototype.hasOwnProperty.call(_store, k) ? _store[k] : null),
|
||||||
|
setItem: (k, v) => { _store[k] = String(v); },
|
||||||
|
removeItem: k => { delete _store[k]; },
|
||||||
|
};
|
||||||
|
globalThis.esc = s => String(s);
|
||||||
|
globalThis.t = k => k;
|
||||||
|
globalThis.pysimFsRenderTree = () => { _treeRenders++; };
|
||||||
|
|
||||||
|
// The filter's two DOM elements: the collapsed row's button and the hidden
|
||||||
|
// menu it expands into. The menu only needs a settable innerHTML (the
|
||||||
|
// render never reads the DOM back) and a classList for the open/closed state.
|
||||||
|
function fakeBtn() {
|
||||||
|
const classes = new Set();
|
||||||
|
return {
|
||||||
|
textContent: '',
|
||||||
|
attrs: {},
|
||||||
|
classes,
|
||||||
|
classList: {
|
||||||
|
contains: c => classes.has(c),
|
||||||
|
toggle(c, force) {
|
||||||
|
const on = force === undefined ? !classes.has(c) : !!force;
|
||||||
|
if (on) classes.add(c); else classes.delete(c);
|
||||||
|
return on;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
setAttribute(k, v) { this.attrs[k] = String(v); },
|
||||||
|
getAttribute(k) { return this.attrs[k]; },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function fakeMenu() {
|
||||||
|
const classes = new Set(['hidden']);
|
||||||
|
return {
|
||||||
|
_html: '',
|
||||||
|
classes,
|
||||||
|
classList: {
|
||||||
|
contains: c => classes.has(c),
|
||||||
|
toggle(c, force) {
|
||||||
|
const on = force === undefined ? !classes.has(c) : !!force;
|
||||||
|
if (on) classes.add(c); else classes.delete(c);
|
||||||
|
return on;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
get innerHTML() { return this._html; },
|
||||||
|
set innerHTML(v) { this._html = v; },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const dom = { menu: null, btn: null, clear: null };
|
||||||
|
globalThis.document = {
|
||||||
|
getElementById: id => (id === 'pysim-fs-filter-menu' ? dom.menu
|
||||||
|
: (id === 'pysim-fs-filter-btn' ? dom.btn
|
||||||
|
: (id === 'pysim-fs-filter-clear' ? dom.clear : null))),
|
||||||
|
};
|
||||||
|
|
||||||
|
let code = '';
|
||||||
|
for (const f of ['fsFilterServiceOptions', 'fsFilterIndexBuild', 'fsFilterIndex',
|
||||||
|
'fsFilterPathIndexBuild', 'fsFilterPathIndex', 'fsFilterCustomPaths',
|
||||||
|
'fsFilterPathsKeyOf', 'pysimFsFilterPathsInvalidate', 'pysimFsFilterMatchingPaths',
|
||||||
|
'pysimFsFilterMatch', 'pysimFsFilterShow', 'pysimFsFilterLabel',
|
||||||
|
'pysimFsFilterSummary', 'pysimFsFilterRender', 'pysimFsFilterToggle',
|
||||||
|
'pysimFsFilterSetEntry', 'pysimFsFilterClear', 'pysimFsFilterRestore', 'pysimFsNodePath',
|
||||||
|
'pysimFsExpandAll', 'pysimFsCollapseAll', 'pysimFsAutoExpandForFilter']) {
|
||||||
|
code += extractFunc(html, f) + '\n';
|
||||||
|
}
|
||||||
|
for (const c of ['FS_FILTER_GROUPS', 'FS_FILTER_GROUP_ORDER', 'SERVICES_FS_MAP']) code += extractConst(html, c);
|
||||||
|
code += extractStrConst(html, 'FS_FILTER_ALL');
|
||||||
|
code += extractStrConst(html, 'FS_FILTER_CUSTOM');
|
||||||
|
eval(code);
|
||||||
|
|
||||||
|
function withFilter(ids, fn) {
|
||||||
|
const saved = pysimFsFilter;
|
||||||
|
pysimFsFilter = new Set(ids);
|
||||||
|
try { return fn(); } finally { pysimFsFilter = saved; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// The checkbox rows the render wrote: [{id, checked}] in document order.
|
||||||
|
function renderedBoxes() {
|
||||||
|
const out = [];
|
||||||
|
for (const m of (dom.menu ? dom.menu.innerHTML : '').matchAll(
|
||||||
|
/<input type="checkbox" data-filter="([^"]*)"( checked)?/g)) {
|
||||||
|
out.push({ id: m[1], checked: !!m[2] });
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
const box = id => renderedBoxes().find(b => b.id === id);
|
||||||
|
|
||||||
|
const fileBackedFeatures = () => Object.keys(SERVICES_FS_MAP).filter(n => SERVICES_FS_MAP[n].length);
|
||||||
|
|
||||||
|
test('the file tree has a collapsed filter dropdown next to the sort pills', () => {
|
||||||
|
assert.match(html, /id="pysim-fs-filter-btn"[^>]*aria-haspopup="true"[^>]*aria-expanded="false"[^>]*onclick="pysimFsFilterToggle\(\)"/);
|
||||||
|
assert.match(html, /id="pysim-fs-filter-menu"[^>]*role="group"[^>]*class="hidden absolute/);
|
||||||
|
assert.match(html, /data-l10n="Filter:"/);
|
||||||
|
// the multiselect (and its onchange handler) is gone
|
||||||
|
assert.ok(!/id="pysim-fs-filter"/.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', () => {
|
||||||
|
const click = html.match(/document\.addEventListener\('click', e => \{([\s\S]{0,600}?)\n\}\);/);
|
||||||
|
assert.ok(click, 'no document click listener');
|
||||||
|
assert.match(click[1], /#pysim-fs-filter-btn/);
|
||||||
|
assert.match(click[1], /#pysim-fs-filter-menu/);
|
||||||
|
assert.match(click[1], /pysimFsFilterToggle\(false\)/);
|
||||||
|
const key = html.match(/document\.addEventListener\('keydown', e => \{([\s\S]{0,300}?)\n\}\);/);
|
||||||
|
assert.ok(key, 'no document keydown listener');
|
||||||
|
assert.match(key[1], /Escape/);
|
||||||
|
assert.match(key[1], /pysimFsFilterToggle\(false\)/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('the four category groups name their files', () => {
|
||||||
|
assert.deepStrictEqual(FS_FILTER_GROUPS.map(g => g.id),
|
||||||
|
['identifiers', 'plmn-selection', 'provider-names', 'wlan']);
|
||||||
|
const files = id => new Set(FS_FILTER_GROUPS.find(g => g.id === id).files);
|
||||||
|
assert.deepStrictEqual([...files('identifiers')],
|
||||||
|
['EF.ICCID', 'EF.IMSI', 'EF.MSISDN', 'EF.IMPI', 'EF.IMPU', 'EF.SUPI_NAI', 'EF.GID1', 'EF.GID2']);
|
||||||
|
assert.deepStrictEqual([...files('plmn-selection')],
|
||||||
|
['EF.PLMNsel', 'EF.PLMNwAcT', 'EF.OPLMNwAcT', 'EF.HPLMNwAcT', 'EF.EHPLMN', 'EF.FPLMN',
|
||||||
|
'EF.HPPLMN', 'EF.LRPLMNSI', 'EF.EHPLMNPI', 'EF.MCHPPLMN']);
|
||||||
|
assert.deepStrictEqual([...files('provider-names')],
|
||||||
|
['EF.SPN', 'EF.PNN', 'EF.OPL', 'EF.OPL5G', 'EF.SPDI', 'EF.SPNI', 'EF.PNNI', 'EF.CNL']);
|
||||||
|
assert.deepStrictEqual([...files('wlan')],
|
||||||
|
['EF.UPLMNWLAN', 'EF.OPLMNWLAN', 'EF.WRI', 'EF.HWSIDL', 'EF.WEHPLMNPI', 'EF.WHPI',
|
||||||
|
'EF.WLRPLMN', 'EF.HPLMNDAI']);
|
||||||
|
assert.strictEqual(FS_FILTER_ALL, 'all');
|
||||||
|
assert.strictEqual(FS_FILTER_CUSTOM, 'custom');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('the per-service options come from the file-backed features only', () => {
|
||||||
|
const services = fsFilterServiceOptions();
|
||||||
|
const features = fileBackedFeatures();
|
||||||
|
assert.ok(features.length > 50, 'expected the cross-reference to be available');
|
||||||
|
assert.strictEqual(services.length, features.length);
|
||||||
|
assert.ok(services.every(o => o.id === 'svc:' + o.label));
|
||||||
|
const spn = services.find(o => o.id === 'svc:Service Provider Name (SPN)');
|
||||||
|
assert.deepStrictEqual(spn.files, ['EF.SPN']);
|
||||||
|
// capability/RFU rows carry no files, so they are not offered
|
||||||
|
assert.ok(!services.some(o => o.id === 'svc:Proactive SIM' || o.id === 'svc:CHV1 disable function'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('the inverted index keys the uppercased names and merges memberships', () => {
|
||||||
|
const idx = fsFilterIndexBuild(FS_FILTER_GROUPS, fsFilterServiceOptions());
|
||||||
|
const ids = name => Array.from(idx.get(name.toUpperCase()) || []);
|
||||||
|
assert.ok(ids('EF.ICCID').includes('identifiers'));
|
||||||
|
assert.ok(ids('EF.PLMNwAcT').includes('plmn-selection'));
|
||||||
|
assert.ok(ids('EF.SPN').includes('provider-names'));
|
||||||
|
assert.ok(ids('EF.SPN').includes('svc:Service Provider Name (SPN)'));
|
||||||
|
// 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.CBMI').includes('svc:Cell Broadcast Message Identifier (CBMI)'));
|
||||||
|
// EF.PSISMSC is neither a category nor a file-backed service, so nothing
|
||||||
|
// maps it (pySim models no such file, and no service lists it either)
|
||||||
|
assert.ok(!idx.has('EF.PSISMSC'));
|
||||||
|
assert.ok(!fileBackedFeatures().some(n => (SERVICES_FS_MAP[n] || []).includes('EF.PSISMSC')));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('a file matches the selected categories, case-insensitively', () => {
|
||||||
|
withFilter(['identifiers'], () => {
|
||||||
|
assert.strictEqual(pysimFsFilterMatch({ name: 'EF.ICCID', fid: '2FE2' }), true);
|
||||||
|
assert.strictEqual(pysimFsFilterMatch({ name: 'ef.iccid', fid: '2FE2' }), true);
|
||||||
|
assert.strictEqual(pysimFsFilterMatch({ name: 'EF.SPN', fid: '6F46' }), false);
|
||||||
|
assert.strictEqual(pysimFsFilterMatch({ name: 'DF.GSM', fid: '7F20', isDir: true }), false);
|
||||||
|
});
|
||||||
|
withFilter(['svc:Service Provider Name (SPN)'], () => {
|
||||||
|
assert.strictEqual(pysimFsFilterMatch({ name: 'EF.SPN' }), true);
|
||||||
|
assert.strictEqual(pysimFsFilterMatch({ name: 'EF.PNN' }), false);
|
||||||
|
});
|
||||||
|
withFilter(['identifiers', 'svc:Short Message Status Reports (SMSR)'], () => {
|
||||||
|
assert.strictEqual(pysimFsFilterMatch({ name: 'EF.IMSI' }), true);
|
||||||
|
assert.strictEqual(pysimFsFilterMatch({ name: 'EF.SMSR' }), true);
|
||||||
|
assert.strictEqual(pysimFsFilterMatch({ name: 'EF.SPN' }), false);
|
||||||
|
});
|
||||||
|
withFilter(['svc:Short Message Storage (SMS)'], () => {
|
||||||
|
assert.strictEqual(pysimFsFilterMatch({ name: 'EF.ECC' }), false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('an empty filter and "all" show everything', () => {
|
||||||
|
withFilter([], () => {
|
||||||
|
assert.strictEqual(pysimFsFilterMatch({ name: 'EF.ADN' }), true);
|
||||||
|
assert.strictEqual(pysimFsFilterMatch({ name: 'DF.GSM', isDir: true }), true);
|
||||||
|
assert.strictEqual(pysimFsFilterMatch({ name: undefined }), true);
|
||||||
|
});
|
||||||
|
withFilter(['all'], () => {
|
||||||
|
assert.strictEqual(pysimFsFilterMatch({ name: 'EF.ADN' }), true);
|
||||||
|
assert.strictEqual(pysimFsFilterMatch({ name: 'EF.ZZZ' }), true);
|
||||||
|
});
|
||||||
|
// a stale multi-selection that still names all also shows everything
|
||||||
|
withFilter(['all', 'identifiers'], () => {
|
||||||
|
assert.strictEqual(pysimFsFilterMatch({ name: 'EF.ECC' }), true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('a capability-only service selection matches nothing', () => {
|
||||||
|
withFilter(['svc:Proactive SIM'], () => {
|
||||||
|
assert.strictEqual(pysimFsFilterMatch({ name: 'EF.ADN' }), false);
|
||||||
|
assert.strictEqual(pysimFsFilterMatch({ name: 'EF.UST' }), false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('"custom files" matches only the injected custom nodes', () => {
|
||||||
|
withFilter(['custom'], () => {
|
||||||
|
assert.strictEqual(pysimFsFilterMatch({ name: 'EF.ADN', custom: false }), false);
|
||||||
|
assert.strictEqual(pysimFsFilterMatch({ name: 'MyCustomEF', custom: true }), true);
|
||||||
|
assert.strictEqual(pysimFsFilterMatch({ name: 'EF.ICCID', custom: true }), true);
|
||||||
|
});
|
||||||
|
withFilter(['custom', 'identifiers'], () => {
|
||||||
|
assert.strictEqual(pysimFsFilterMatch({ name: 'EF.ICCID', custom: false }), true);
|
||||||
|
assert.strictEqual(pysimFsFilterMatch({ name: 'MyCustomEF', custom: true }), true);
|
||||||
|
assert.strictEqual(pysimFsFilterMatch({ name: 'EF.SPN', custom: false }), false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// A small tree with real parent links (pysimFsNodePath walks them): MF with
|
||||||
|
// DF.TELECOM (EF.MSISDN), DF.GSM (EF.SPN, EF.ECC) and ADF.USIM (DF.WLAN).
|
||||||
|
function fsTree() {
|
||||||
|
const root = { name: 'MF', fid: '3F00', isDir: true, children: [], parent: null, expanded: true };
|
||||||
|
const add = (parent, name, fid, isDir) => {
|
||||||
|
const n = { name, fid, isDir: !!isDir, children: isDir ? [] : null, parent, expanded: true };
|
||||||
|
parent.children.push(n);
|
||||||
|
return n;
|
||||||
|
};
|
||||||
|
const telecom = add(root, 'DF.TELECOM', '7F10', true);
|
||||||
|
const msisdn = add(telecom, 'EF.MSISDN', '6F40', false);
|
||||||
|
const gsm = add(root, 'DF.GSM', '7F20', true);
|
||||||
|
const spn = add(gsm, 'EF.SPN', '6F46', false);
|
||||||
|
const ecc = add(gsm, 'EF.ECC', '6FB7', false);
|
||||||
|
const usim = add(root, 'ADF.USIM', '7FFF', true);
|
||||||
|
const wlan = add(usim, 'DF.WLAN', '5F40', true);
|
||||||
|
return { root, telecom, msisdn, gsm, spn, ecc, usim, wlan };
|
||||||
|
}
|
||||||
|
|
||||||
|
test('a directory shows only on the path of a file the filter selects', () => {
|
||||||
|
const t = fsTree();
|
||||||
|
withFilter(['provider-names'], () => {
|
||||||
|
// the root always stays; DF.GSM holds EF.SPN (MF/7F20/6F46)
|
||||||
|
assert.strictEqual(pysimFsFilterShow(t.root), true);
|
||||||
|
assert.strictEqual(pysimFsFilterShow(t.gsm), true);
|
||||||
|
// ... and ADF.USIM holds its own EF.SPN copy
|
||||||
|
assert.strictEqual(pysimFsFilterShow(t.usim), true);
|
||||||
|
// neither DF.TELECOM nor DF.WLAN holds a provider-name file
|
||||||
|
assert.strictEqual(pysimFsFilterShow(t.telecom), false);
|
||||||
|
assert.strictEqual(pysimFsFilterShow(t.wlan), false);
|
||||||
|
// files keep their name rule
|
||||||
|
assert.strictEqual(pysimFsFilterShow(t.spn), true);
|
||||||
|
assert.strictEqual(pysimFsFilterShow(t.ecc), false);
|
||||||
|
});
|
||||||
|
withFilter(['identifiers'], () => {
|
||||||
|
// EF.MSISDN is in DF.TELECOM, EF.IMSI/EF.GID1/2 in DF.GSM
|
||||||
|
assert.strictEqual(pysimFsFilterShow(t.telecom), true);
|
||||||
|
assert.strictEqual(pysimFsFilterShow(t.msisdn), true);
|
||||||
|
assert.strictEqual(pysimFsFilterShow(t.gsm), true);
|
||||||
|
assert.strictEqual(pysimFsFilterShow(t.usim), true); // EF.IMSI/EF.MSISDN copies
|
||||||
|
assert.strictEqual(pysimFsFilterShow(t.wlan), false);
|
||||||
|
});
|
||||||
|
withFilter(['svc:Cell Broadcast Message Identifier (CBMI)'], () => {
|
||||||
|
assert.strictEqual(pysimFsFilterShow(t.gsm), true); // EF.CBMI
|
||||||
|
assert.strictEqual(pysimFsFilterShow(t.telecom), false);
|
||||||
|
assert.strictEqual(pysimFsFilterShow(t.wlan), false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('a directory named by the selection itself shows', () => {
|
||||||
|
const t = fsTree();
|
||||||
|
// the GPRS feature names DF.GSM (its SST bit covers the DF, not a file)
|
||||||
|
withFilter(['svc:GPRS'], () => {
|
||||||
|
assert.strictEqual(pysimFsFilterMatch(t.gsm), true);
|
||||||
|
assert.strictEqual(pysimFsFilterShow(t.gsm), true);
|
||||||
|
assert.strictEqual(pysimFsFilterShow(t.telecom), false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('"all" and an empty selection show every node', () => {
|
||||||
|
const t = fsTree();
|
||||||
|
for (const ids of [['all'], []]) {
|
||||||
|
withFilter(ids, () => {
|
||||||
|
assert.strictEqual(pysimFsFilterShow(t.root), true);
|
||||||
|
assert.strictEqual(pysimFsFilterShow(t.telecom), true);
|
||||||
|
assert.strictEqual(pysimFsFilterShow(t.wlan), true);
|
||||||
|
assert.strictEqual(pysimFsFilterShow(t.ecc), true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('directories are kept until the standard list is available', () => {
|
||||||
|
const t = fsTree();
|
||||||
|
const saved = _uiccStdFiles;
|
||||||
|
_uiccStdFiles = null;
|
||||||
|
try {
|
||||||
|
withFilter(['provider-names'], () => {
|
||||||
|
assert.strictEqual(pysimFsFilterMatchingPaths(), null);
|
||||||
|
// the ancestors cannot be judged, so nothing vanishes
|
||||||
|
assert.strictEqual(pysimFsFilterShow(t.telecom), true);
|
||||||
|
assert.strictEqual(pysimFsFilterShow(t.wlan), true);
|
||||||
|
// files still obey the name rule
|
||||||
|
assert.strictEqual(pysimFsFilterShow(t.ecc), false);
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
_uiccStdFiles = saved;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('the standard list gives a name every tree it lives in', () => {
|
||||||
|
const idx = fsFilterPathIndexBuild(_uiccStdFiles);
|
||||||
|
assert.deepStrictEqual(idx.get('EF.SPN').slice().sort(),
|
||||||
|
['ADF.USIM-NON-IMSI/6F46', 'ADF.USIM/6F46', 'MF/7F20/6F46']);
|
||||||
|
assert.deepStrictEqual(idx.get('EF.ICCID'), ['MF/2FE2']);
|
||||||
|
// the index is cached for the loaded list
|
||||||
|
assert.strictEqual(fsFilterPathIndex(), fsFilterPathIndex());
|
||||||
|
});
|
||||||
|
|
||||||
|
test('the selection resolves to the paths of its files', () => {
|
||||||
|
withFilter(['svc:Cell Broadcast Message Identifier (CBMI)'], () => {
|
||||||
|
const paths = pysimFsFilterMatchingPaths();
|
||||||
|
assert.ok(paths instanceof Set, 'expected a Set');
|
||||||
|
assert.deepStrictEqual(Array.from(paths).sort(), [
|
||||||
|
'ADF.USIM-NON-IMSI/6F45', 'ADF.USIM/6F45', 'MF/7F20/6F45',
|
||||||
|
]);
|
||||||
|
// cached for the same selection
|
||||||
|
assert.strictEqual(pysimFsFilterMatchingPaths(), paths);
|
||||||
|
});
|
||||||
|
withFilter(['custom'], () => {
|
||||||
|
assert.deepStrictEqual(Array.from(pysimFsFilterMatchingPaths()), []);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('"custom files" keeps the directories that hold them', () => {
|
||||||
|
const t = fsTree();
|
||||||
|
const custom = { name: 'EF.MYFILE', fid: '6F99', isDir: false, children: null, parent: t.gsm, custom: true };
|
||||||
|
t.gsm.children.push(custom);
|
||||||
|
pysimCustomFiles = [{ path: 'MF/7F20/6F99', name: 'EF.MYFILE', kind: 'ef' }];
|
||||||
|
try {
|
||||||
|
withFilter(['custom'], () => {
|
||||||
|
// custom files are outside the standard list: their own paths are
|
||||||
|
// the ancestry, so the holding DF stays
|
||||||
|
assert.strictEqual(pysimFsFilterShow(custom), true);
|
||||||
|
assert.strictEqual(pysimFsFilterShow(t.gsm), true);
|
||||||
|
assert.strictEqual(pysimFsFilterShow(t.root), true);
|
||||||
|
assert.strictEqual(pysimFsFilterShow(t.telecom), false);
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
pysimCustomFiles = [];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('the menu lists all, custom, the three headings and the services', () => {
|
||||||
|
pysimFsFilter = new Set(['all']);
|
||||||
|
dom.menu = fakeMenu();
|
||||||
|
dom.btn = fakeBtn();
|
||||||
|
pysimFsFilterRender();
|
||||||
|
const out = dom.menu.innerHTML;
|
||||||
|
assert.match(out, /^<label[^>]*><input type="checkbox" data-filter="all" checked/);
|
||||||
|
assert.match(out, /data-filter="custom"/);
|
||||||
|
assert.match(out, /<div class="[^"]*">Identifiers<\/div>/);
|
||||||
|
assert.match(out, /<div class="[^"]*">PLMN<\/div>/);
|
||||||
|
assert.match(out, /<div class="[^"]*">Services<\/div>/);
|
||||||
|
// the headings come in the declared order
|
||||||
|
assert.ok(out.indexOf('>Identifiers</div>') < out.indexOf('>PLMN</div>'));
|
||||||
|
assert.ok(out.indexOf('>PLMN</div>') < out.indexOf('>Services</div>'));
|
||||||
|
// the categories sit under their headings
|
||||||
|
assert.ok(out.indexOf('data-filter="provider-names"') < out.indexOf('>PLMN</div>'));
|
||||||
|
assert.ok(out.indexOf('data-filter="plmn-selection"') > out.indexOf('>PLMN</div>'));
|
||||||
|
// the per-service options sit under the Services heading
|
||||||
|
assert.ok(out.indexOf('data-filter="svc:') > out.indexOf('>Services</div>'));
|
||||||
|
const boxes = renderedBoxes();
|
||||||
|
assert.strictEqual(boxes.length, 2 + FS_FILTER_GROUPS.length + fsFilterServiceOptions().length);
|
||||||
|
assert.strictEqual(boxes[0].id, 'all');
|
||||||
|
assert.strictEqual(boxes[0].checked, true);
|
||||||
|
assert.strictEqual(box('custom').checked, false);
|
||||||
|
assert.ok(boxes.some(b => b.id === 'svc:Service Provider Name (SPN)'));
|
||||||
|
assert.ok(!boxes.some(b => b.id === 'svc:Proactive SIM'));
|
||||||
|
// the row shows the summary, and the checkbox calls the setter directly
|
||||||
|
assert.strictEqual(dom.btn.textContent, 'All');
|
||||||
|
assert.match(out, /onchange="pysimFsFilterSetEntry\(this\.dataset\.filter, this\.checked\)"/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('the button turns blue when a filter is active and back for all', () => {
|
||||||
|
dom.menu = fakeMenu();
|
||||||
|
dom.btn = fakeBtn();
|
||||||
|
pysimFsFilter = new Set(['identifiers']);
|
||||||
|
pysimFsFilterRender();
|
||||||
|
assert.strictEqual(dom.btn.classes.has('bg-blue-600'), true);
|
||||||
|
assert.strictEqual(dom.btn.classes.has('text-white'), true);
|
||||||
|
assert.strictEqual(dom.btn.classes.has('text-gray-700'), false);
|
||||||
|
pysimFsFilter = new Set(['all']);
|
||||||
|
pysimFsFilterRender();
|
||||||
|
assert.strictEqual(dom.btn.classes.has('bg-blue-600'), false);
|
||||||
|
assert.strictEqual(dom.btn.classes.has('text-white'), false);
|
||||||
|
assert.strictEqual(dom.btn.classes.has('text-gray-700'), true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('expand all and collapse all walk the directory tree, skipping absent DFs', async () => {
|
||||||
|
const telecom = { name: 'DF.TELECOM', fid: '7F10', isDir: true, exists: true, expanded: false,
|
||||||
|
parent: null, children: [
|
||||||
|
{ name: 'DF.PHONEBOOK', fid: '5F3A', isDir: true, exists: true, expanded: false, parent: null, children: null },
|
||||||
|
] };
|
||||||
|
const gsm = { name: 'DF.GSM', fid: '7F20', isDir: true, exists: false, expanded: false, parent: null, children: null };
|
||||||
|
pysimFsTreeRoot = { name: 'MF', fid: '3F00', isDir: true, exists: true, expanded: true, parent: null,
|
||||||
|
children: [gsm, telecom] };
|
||||||
|
let loads = 0;
|
||||||
|
pysimFsLoadChildren = async node => { if (!node.children) { node.children = []; } loads++; };
|
||||||
|
try {
|
||||||
|
_treeRenders = 0;
|
||||||
|
await pysimFsExpandAll();
|
||||||
|
assert.strictEqual(pysimFsTreeRoot.expanded, true);
|
||||||
|
assert.strictEqual(gsm.expanded, false, 'an absent DF is not expanded');
|
||||||
|
assert.strictEqual(telecom.expanded, true);
|
||||||
|
assert.strictEqual(telecom.children[0].expanded, true);
|
||||||
|
assert.ok(loads > 0, 'lazy children are loaded on demand');
|
||||||
|
assert.strictEqual(_treeRenders, 1);
|
||||||
|
|
||||||
|
pysimFsCollapseAll();
|
||||||
|
assert.strictEqual(pysimFsTreeRoot.expanded, false);
|
||||||
|
assert.strictEqual(telecom.expanded, false);
|
||||||
|
assert.strictEqual(telecom.children[0].expanded, false);
|
||||||
|
} finally {
|
||||||
|
pysimFsTreeRoot = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('auto-expand fires only for a non-"all" filter', () => {
|
||||||
|
// expand-all (with a null tree) still re-renders once; pin the branch
|
||||||
|
pysimFsFilter = new Set(['all']);
|
||||||
|
_treeRenders = 0;
|
||||||
|
pysimFsAutoExpandForFilter();
|
||||||
|
assert.strictEqual(_treeRenders, 0, '"all" does not expand');
|
||||||
|
pysimFsFilter = new Set(['identifiers']);
|
||||||
|
pysimFsAutoExpandForFilter();
|
||||||
|
assert.strictEqual(_treeRenders, 1, 'a category expands the tree');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('the menu marks the saved selection', () => {
|
||||||
|
pysimFsFilter = new Set(['svc:Short Message Storage (SMS)', 'identifiers']);
|
||||||
|
dom.menu = fakeMenu();
|
||||||
|
dom.btn = fakeBtn();
|
||||||
|
pysimFsFilterRender();
|
||||||
|
assert.deepStrictEqual(renderedBoxes().filter(b => b.checked).map(b => b.id),
|
||||||
|
['identifiers', 'svc:Short Message Storage (SMS)']);
|
||||||
|
assert.strictEqual(box('all').checked, false);
|
||||||
|
assert.strictEqual(dom.btn.textContent, 'Selected: 2');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('the button summary names the single entry or counts the selection', () => {
|
||||||
|
const summary = ids => withFilter(ids, () => pysimFsFilterSummary());
|
||||||
|
assert.strictEqual(summary(['all']), 'All');
|
||||||
|
// an empty selection also means everything
|
||||||
|
assert.strictEqual(summary([]), 'All');
|
||||||
|
assert.strictEqual(summary(['all', 'plmn-selection']), 'All');
|
||||||
|
assert.strictEqual(summary(['custom']), 'Custom files');
|
||||||
|
assert.strictEqual(summary(['plmn-selection']), 'PLMN selection');
|
||||||
|
assert.strictEqual(summary(['svc:Service Provider Name (SPN)']), 'Service Provider Name (SPN)');
|
||||||
|
assert.strictEqual(summary(['plmn-selection', 'wlan']), 'Selected: 2');
|
||||||
|
// an unknown id falls back to itself
|
||||||
|
assert.strictEqual(summary(['providers']), 'providers');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('the menu toggles open and closed', () => {
|
||||||
|
dom.menu = fakeMenu();
|
||||||
|
dom.btn = fakeBtn();
|
||||||
|
assert.strictEqual(dom.menu.classList.contains('hidden'), true);
|
||||||
|
pysimFsFilterToggle();
|
||||||
|
assert.strictEqual(dom.menu.classList.contains('hidden'), false);
|
||||||
|
assert.strictEqual(dom.btn.getAttribute('aria-expanded'), 'true');
|
||||||
|
pysimFsFilterToggle();
|
||||||
|
assert.strictEqual(dom.menu.classList.contains('hidden'), true);
|
||||||
|
assert.strictEqual(dom.btn.getAttribute('aria-expanded'), 'false');
|
||||||
|
// the forced forms (the outside click / Escape handlers pass false)
|
||||||
|
pysimFsFilterToggle(true);
|
||||||
|
assert.strictEqual(dom.menu.classList.contains('hidden'), false);
|
||||||
|
pysimFsFilterToggle(false);
|
||||||
|
assert.strictEqual(dom.menu.classList.contains('hidden'), true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('the checkbox rows keep "all" exclusive and persist the selection', () => {
|
||||||
|
dom.menu = fakeMenu();
|
||||||
|
dom.btn = fakeBtn();
|
||||||
|
delete _store.simple_fs_filter;
|
||||||
|
_treeRenders = 0;
|
||||||
|
pysimFsFilter = new Set(['all']);
|
||||||
|
pysimFsFilterSetEntry('all', true);
|
||||||
|
assert.deepStrictEqual([...pysimFsFilter], ['all']);
|
||||||
|
assert.strictEqual(_store.simple_fs_filter, '["all"]');
|
||||||
|
assert.strictEqual(_treeRenders, 1);
|
||||||
|
// choosing a category drops "all"
|
||||||
|
pysimFsFilterSetEntry('provider-names', true);
|
||||||
|
assert.deepStrictEqual([...pysimFsFilter], ['provider-names']);
|
||||||
|
assert.strictEqual(_store.simple_fs_filter, '["provider-names"]');
|
||||||
|
assert.strictEqual(_treeRenders, 3); // render + the auto-expand's render
|
||||||
|
assert.strictEqual(box('provider-names').checked, true);
|
||||||
|
assert.strictEqual(box('all').checked, false);
|
||||||
|
assert.strictEqual(dom.btn.textContent, 'Provider names');
|
||||||
|
// several categories stay selected
|
||||||
|
pysimFsFilterSetEntry('identifiers', true);
|
||||||
|
assert.deepStrictEqual([...pysimFsFilter], ['provider-names', 'identifiers']);
|
||||||
|
// unchecking one keeps the other
|
||||||
|
pysimFsFilterSetEntry('provider-names', false);
|
||||||
|
assert.deepStrictEqual([...pysimFsFilter], ['identifiers']);
|
||||||
|
// unchecking the last one restores the default
|
||||||
|
pysimFsFilterSetEntry('identifiers', false);
|
||||||
|
assert.deepStrictEqual([...pysimFsFilter], ['all']);
|
||||||
|
assert.strictEqual(_store.simple_fs_filter, '["all"]');
|
||||||
|
assert.strictEqual(box('all').checked, true);
|
||||||
|
// "all" is exclusive even when another entry is selected
|
||||||
|
pysimFsFilter = new Set(['provider-names']);
|
||||||
|
pysimFsFilterSetEntry('all', true);
|
||||||
|
assert.deepStrictEqual([...pysimFsFilter], ['all']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('the saved selection is restored, defaulting to "all"', () => {
|
||||||
|
delete _store.simple_fs_filter;
|
||||||
|
pysimFsFilterRestore();
|
||||||
|
assert.deepStrictEqual([...pysimFsFilter], ['all']);
|
||||||
|
_store.simple_fs_filter = JSON.stringify(['providers', 'svc:Service Provider Name (SPN)']);
|
||||||
|
pysimFsFilterRestore();
|
||||||
|
assert.deepStrictEqual([...pysimFsFilter], ['providers', 'svc:Service Provider Name (SPN)']);
|
||||||
|
// `all` is exclusive; an empty or broken save falls back to it
|
||||||
|
_store.simple_fs_filter = JSON.stringify(['all', 'plmn-selection']);
|
||||||
|
pysimFsFilterRestore();
|
||||||
|
assert.deepStrictEqual([...pysimFsFilter], ['all']);
|
||||||
|
_store.simple_fs_filter = '[]';
|
||||||
|
pysimFsFilterRestore();
|
||||||
|
assert.deepStrictEqual([...pysimFsFilter], ['all']);
|
||||||
|
_store.simple_fs_filter = '{not json';
|
||||||
|
pysimFsFilterRestore();
|
||||||
|
assert.deepStrictEqual([...pysimFsFilter], ['all']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('the render consults the filter when walking children', () => {
|
||||||
|
assert.match(html, /if \(!pysimFsFilterShow\(child\)\) continue;/);
|
||||||
|
assert.ok(!/pysimFsFilterMatch\(child\)/.test(html));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('the directory rule uses the registry paths, not the loaded children', () => {
|
||||||
|
const fn = extractFunc(html, 'pysimFsFilterShow');
|
||||||
|
assert.ok(!fn.includes('node.children'), fn);
|
||||||
|
assert.ok(fn.indexOf('pysimFsFilterMatchingPaths()') > 0, fn);
|
||||||
|
assert.ok(fn.indexOf("path === 'MF'") > 0, fn);
|
||||||
|
// the standard list is read synchronously from the cached array
|
||||||
|
assert.match(html, /let _uiccStdFiles = null;/);
|
||||||
|
assert.ok(extractFunc(html, 'pysimFsFilterMatchingPaths').indexOf('_uiccStdFiles') > 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('every filter change drops the path cache before the tree redraws', () => {
|
||||||
|
for (const name of ['pysimFsFilterSetEntry', 'pysimFsSetFilterToService', 'pysimFsFilterRestore']) {
|
||||||
|
const fn = extractFunc(html, name);
|
||||||
|
const at = fn.indexOf('pysimFsFilterPathsInvalidate()');
|
||||||
|
assert.ok(at > 0, name + ' does not invalidate the path cache');
|
||||||
|
const render = fn.indexOf('pysimFsRenderTree()');
|
||||||
|
if (render > 0) assert.ok(at < render, name + ': the cache is dropped before the redraw');
|
||||||
|
}
|
||||||
|
// the list arriving redraws a tree that was filtered while it loaded
|
||||||
|
const load = extractFunc(html, 'uiccStdFilesLoad');
|
||||||
|
assert.ok(load.indexOf('pysimFsRenderTree()') > 0, load);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('the filter state is persisted under simple_fs_filter', () => {
|
||||||
|
const fn = extractFunc(html, 'pysimFsFilterSetEntry');
|
||||||
|
assert.ok(fn.indexOf("localStorage.setItem('simple_fs_filter'") > 0, fn);
|
||||||
|
assert.ok(fn.indexOf('pysimFsRenderTree()') > 0, fn);
|
||||||
|
assert.ok(fn.indexOf('pysimFsFilterRender()') > 0, fn);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('the dropdown is re-rendered on language switch', () => {
|
||||||
|
const fn = extractFunc(html, 'refreshDynamicI18n');
|
||||||
|
assert.ok(fn.indexOf('pysimFsFilterRender()') > 0, fn);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('the filter labels have Russian entries', () => {
|
||||||
|
const dict = html.match(/const LANG_RU = \{([\s\S]*?)\n\};/)[1];
|
||||||
|
for (const key of ['All', 'Custom files', 'Identifiers', 'PLMN', 'Services', 'PLMN selection',
|
||||||
|
'Provider names', 'WLAN / I-WLAN', 'Selected: {n}']) {
|
||||||
|
assert.ok(dict.includes("'" + key + "':"), 'missing Russian entry: ' + key);
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -24,6 +24,9 @@ function extractFunc(src, name) {
|
|||||||
let code = 'var pysimFsSelected = null;\n'
|
let code = 'var pysimFsSelected = null;\n'
|
||||||
+ 'var pysimFsDecodedMode = false;\n'
|
+ 'var pysimFsDecodedMode = false;\n'
|
||||||
+ 'var pysimFsTreeRoot = null;\n';
|
+ 'var pysimFsTreeRoot = null;\n';
|
||||||
|
code += extractFunc(html, 'efFidFromPath') + '\n';
|
||||||
|
code += extractFunc(html, 'efDfFromPath') + '\n';
|
||||||
|
code += extractFunc(html, 'pysimFsFallbackNode') + '\n';
|
||||||
code += extractFunc(html, 'pysimFsSyncContentVisibility') + '\n';
|
code += extractFunc(html, 'pysimFsSyncContentVisibility') + '\n';
|
||||||
code += extractFunc(html, 'pysimFsRead') + '\n';
|
code += extractFunc(html, 'pysimFsRead') + '\n';
|
||||||
eval(code);
|
eval(code);
|
||||||
@@ -53,7 +56,7 @@ function setup() {
|
|||||||
};
|
};
|
||||||
globalThis.t = s => s;
|
globalThis.t = s => s;
|
||||||
globalThis.esc = s => s;
|
globalThis.esc = s => s;
|
||||||
globalThis.pysimFsFindNode = () => ({ name: 'EF.IMSI', fid: '6f07' });
|
globalThis.pysimFsFindNodeByPath = () => ({ name: 'EF.IMSI', fid: '6f07' });
|
||||||
globalThis.pysimFsSelectBody = () => ({});
|
globalThis.pysimFsSelectBody = () => ({});
|
||||||
globalThis.pysimFsShowError = (el, sw, err) => {
|
globalThis.pysimFsShowError = (el, sw, err) => {
|
||||||
errorShown = { sw, err };
|
errorShown = { sw, err };
|
||||||
@@ -63,7 +66,7 @@ function setup() {
|
|||||||
if (fetchResult instanceof Error) throw fetchResult;
|
if (fetchResult instanceof Error) throw fetchResult;
|
||||||
return fetchResult;
|
return fetchResult;
|
||||||
};
|
};
|
||||||
pysimFsSelected = 'EF.IMSI';
|
pysimFsSelected = 'ADF.USIM/6F07';
|
||||||
pysimFsDecodedMode = false;
|
pysimFsDecodedMode = false;
|
||||||
pysimFsTreeRoot = { name: 'MF' };
|
pysimFsTreeRoot = { name: 'MF' };
|
||||||
fetchResult = { success: true, sw: '9000', data: 'AABB' };
|
fetchResult = { success: true, sw: '9000', data: 'AABB' };
|
||||||
@@ -92,6 +95,17 @@ test('a successful read fills and shows the content pane', async () => {
|
|||||||
assert.strictEqual(status.textContent, 'SW: 9000 OK');
|
assert.strictEqual(status.textContent, 'SW: 9000 OK');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('a path whose node is not loaded selects by its last segment and FID', async () => {
|
||||||
|
setup();
|
||||||
|
globalThis.pysimFsFindNodeByPath = () => null;
|
||||||
|
let body = null;
|
||||||
|
globalThis.pysimFsSelectBody = node => { body = node; return {}; };
|
||||||
|
pysimFsSelected = 'MF/7F20/6FB7';
|
||||||
|
const ok = await pysimFsRead();
|
||||||
|
assert.strictEqual(ok, true);
|
||||||
|
assert.deepStrictEqual(body, { name: '6FB7', fid: '6FB7' });
|
||||||
|
});
|
||||||
|
|
||||||
test('a failed read keeps the content pane hidden', async () => {
|
test('a failed read keeps the content pane hidden', async () => {
|
||||||
setup();
|
setup();
|
||||||
fetchResult = { success: false, sw: '6982', error: 'Security status not satisfied' };
|
fetchResult = { success: false, sw: '6982', error: 'Security status not satisfied' };
|
||||||
|
|||||||
@@ -22,7 +22,11 @@ function extractFunc(src, name) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let code = 'var pysimFsSort = "fid";\n';
|
let code = 'var pysimFsSort = "fid";\n';
|
||||||
|
code += 'var pysimFsFilter = new Set();\n';
|
||||||
code += extractFunc(html, 'pysimFsSortChildren') + '\n';
|
code += extractFunc(html, 'pysimFsSortChildren') + '\n';
|
||||||
|
code += extractFunc(html, 'pysimFsNodePath') + '\n';
|
||||||
|
code += extractFunc(html, 'pysimFsFilterMatch') + '\n';
|
||||||
|
code += extractFunc(html, 'pysimFsFilterShow') + '\n';
|
||||||
code += extractFunc(html, 'pysimFsRenderNode') + '\n';
|
code += extractFunc(html, 'pysimFsRenderNode') + '\n';
|
||||||
code += 'globalThis.esc = s => s;\nglobalThis.t = s => s;\n';
|
code += 'globalThis.esc = s => s;\nglobalThis.t = s => s;\n';
|
||||||
eval(code);
|
eval(code);
|
||||||
@@ -65,3 +69,27 @@ test('non-existing directory hides previously loaded children', () => {
|
|||||||
assert.ok(!out.includes('(empty)'), out);
|
assert.ok(!out.includes('(empty)'), out);
|
||||||
assert.ok(!out.includes('pysimFsToggleDir'), out);
|
assert.ok(!out.includes('pysimFsToggleDir'), out);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('the row handlers carry the canonical path, so same-named files differ', () => {
|
||||||
|
// EF.ECC lives in DF.GSM and ADF.USIM with the same name AND FID: the
|
||||||
|
// onclick argument must be the path, never the ambiguous display name
|
||||||
|
const root = { name: 'MF', fid: '3F00', isDir: true, exists: true, children: [], expanded: true, parent: null };
|
||||||
|
const gsm = { name: 'DF.GSM', fid: '7F20', isDir: true, exists: true, children: [], expanded: true, parent: root };
|
||||||
|
const usim = { name: 'ADF.USIM', fid: '7FFF', isDir: true, exists: true, children: [], expanded: true, parent: root };
|
||||||
|
const gsmEcc = { name: 'EF.ECC', fid: '6FB7', isDir: false, exists: true, children: null, expanded: false, parent: gsm };
|
||||||
|
const usimEcc = { name: 'EF.ECC', fid: '6FB7', isDir: false, exists: true, children: null, expanded: false, parent: usim };
|
||||||
|
root.children = [gsm, usim];
|
||||||
|
gsm.children = [gsmEcc];
|
||||||
|
usim.children = [usimEcc];
|
||||||
|
assert.ok(pysimFsRenderNode(gsmEcc, 0).includes("pysimFsClickFile('MF/7F20/6FB7')"));
|
||||||
|
assert.ok(pysimFsRenderNode(usimEcc, 0).includes("pysimFsClickFile('ADF.USIM/6FB7')"));
|
||||||
|
assert.ok(pysimFsRenderNode(gsm, 0).includes("pysimFsToggleDir('MF/7F20')"));
|
||||||
|
assert.ok(pysimFsRenderNode(usim, 0).includes("pysimFsToggleDir('ADF.USIM')"));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('pysimFsRenderNode passes pysimFsNodePath(node) to the click/toggle handlers', () => {
|
||||||
|
const body = extractFunc(html, 'pysimFsRenderNode');
|
||||||
|
assert.ok(body.includes('const nodePath = pysimFsNodePath(node);'));
|
||||||
|
assert.match(body, /pysimFsToggleDir\(\\'' \+ esc\(nodePath\)/);
|
||||||
|
assert.match(body, /pysimFsClickFile\(\\'' \+ esc\(nodePath\)/);
|
||||||
|
});
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
// The service-table rows' cross-link to the file-tree filter: a table row
|
||||||
|
// that is backed by files gets a "To EF filter" button that narrows the tree
|
||||||
|
// to exactly that service's files.
|
||||||
|
const { test } = require('node:test');
|
||||||
|
const assert = require('node:assert');
|
||||||
|
const fs = require('node:fs');
|
||||||
|
const path = require('node:path');
|
||||||
|
|
||||||
|
const html = fs.readFileSync(path.join(__dirname, '..', 'index.html'), 'utf8');
|
||||||
|
|
||||||
|
function extractFunc(src, name) {
|
||||||
|
const re = new RegExp('(?:async\\s+)?function\\s+' + name + '\\s*\\([^)]*\\)\\s*\\{');
|
||||||
|
const m = re.exec(src);
|
||||||
|
if (!m) throw new Error('function ' + name + ' not found');
|
||||||
|
let i = m.index + m[0].length - 1;
|
||||||
|
let depth = 0;
|
||||||
|
for (; i < src.length; i++) {
|
||||||
|
if (src[i] === '{') depth++;
|
||||||
|
else if (src[i] === '}') {
|
||||||
|
depth--;
|
||||||
|
if (depth === 0) break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return src.slice(m.index, i + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractConst(src, name) {
|
||||||
|
const m = new RegExp('const ' + name + ' = ').exec(src);
|
||||||
|
if (!m) throw new Error('const ' + name + ' not found');
|
||||||
|
const begin = m.index + m[0].length;
|
||||||
|
const opener = src[begin];
|
||||||
|
const closer = opener === '[' ? ']' : (opener === '{' ? '}' : null);
|
||||||
|
if (!closer) throw new Error('unsupported const ' + name);
|
||||||
|
let depth = 0, i = begin;
|
||||||
|
for (; i < src.length; i++) {
|
||||||
|
if (src[i] === opener) depth++;
|
||||||
|
else if (src[i] === closer) {
|
||||||
|
depth--;
|
||||||
|
if (depth === 0) { i++; break; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 'var ' + name + ' = ' + src.slice(begin, i) + ';\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
// module state the extracted functions close over
|
||||||
|
let pysimFsFilter = new Set(['all']);
|
||||||
|
let _servicesFeatureByNumber = null;
|
||||||
|
// the tree filter's matching-path cache (pysimFsSetFilterToService drops it)
|
||||||
|
let _fsFilterPaths = null;
|
||||||
|
let _fsFilterPathsKey = null;
|
||||||
|
const _store = {};
|
||||||
|
let _renders = 0;
|
||||||
|
let _treeRenders = 0;
|
||||||
|
let pysimFsTreeRoot = null; // expand-all walks this; null = nothing to expand
|
||||||
|
|
||||||
|
globalThis.localStorage = {
|
||||||
|
getItem: k => (Object.prototype.hasOwnProperty.call(_store, k) ? _store[k] : null),
|
||||||
|
setItem: (k, v) => { _store[k] = String(v); },
|
||||||
|
removeItem: k => { delete _store[k]; },
|
||||||
|
};
|
||||||
|
globalThis.esc = s => String(s);
|
||||||
|
globalThis.t = k => k;
|
||||||
|
globalThis.pysimFsFilterRender = () => { _renders++; };
|
||||||
|
globalThis.pysimFsRenderTree = () => { _treeRenders++; };
|
||||||
|
|
||||||
|
let code = '';
|
||||||
|
for (const f of ['servicesFeatureByNumber', 'pysimFsFilterBtnHtml', 'pysimFsSetFilterToService',
|
||||||
|
'pysimFsFilterPathsInvalidate', 'pysimFsExpandAll', 'pysimFsCollapseAll',
|
||||||
|
'pysimFsAutoExpandForFilter']) {
|
||||||
|
code += extractFunc(html, f) + '\n';
|
||||||
|
}
|
||||||
|
for (const c of ['SERVICES_CROSS_REF', 'SERVICES_FS_MAP']) code += extractConst(html, c);
|
||||||
|
code += "var FS_FILTER_ALL = 'all';\n";
|
||||||
|
eval(code);
|
||||||
|
|
||||||
|
test('servicesFeatureByNumber inverts the cross-reference per file table', () => {
|
||||||
|
const map = servicesFeatureByNumber();
|
||||||
|
assert.strictEqual(map.ust[2], 'Fixed Dialling Numbers (FDN)');
|
||||||
|
assert.strictEqual(map.sst[2], 'Local Phone Book (ADN)');
|
||||||
|
assert.strictEqual(map.est[1], 'Fixed Dialling Numbers (FDN)');
|
||||||
|
assert.strictEqual(map.ist[1], 'P-CSCF address');
|
||||||
|
// the same number names a different feature in another table
|
||||||
|
assert.strictEqual(map.ust[1], 'Local Phone Book (ADN)');
|
||||||
|
assert.strictEqual(map.sst[1], 'CHV1 disable function');
|
||||||
|
// a capability-only number maps to a feature too
|
||||||
|
assert.strictEqual(map.ust[99], 'URI support by UICC');
|
||||||
|
// the map is built once
|
||||||
|
assert.strictEqual(servicesFeatureByNumber(), map);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('pysimFsFilterBtnHtml links a file-backed service row only', () => {
|
||||||
|
const out = pysimFsFilterBtnHtml('ust', 2);
|
||||||
|
assert.match(out, /^<button type="button" class="ml-1 px-1 text-xs text-blue-600 dark:text-blue-400 hover:underline"/);
|
||||||
|
assert.ok(out.indexOf("pysimFsSetFilterToService('ust', 2)") > 0, out);
|
||||||
|
assert.ok(out.indexOf('To EF filter') > 0, out);
|
||||||
|
// OCI is file-backed
|
||||||
|
assert.notStrictEqual(pysimFsFilterBtnHtml('ust', 8), '');
|
||||||
|
// a capability-only service, a missing table and an unmodelled number
|
||||||
|
assert.strictEqual(pysimFsFilterBtnHtml('ust', 99), '');
|
||||||
|
assert.strictEqual(pysimFsFilterBtnHtml(undefined, 1), '');
|
||||||
|
assert.strictEqual(pysimFsFilterBtnHtml('sst', 99), '');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('pysimFsSetFilterToService narrows the tree to that service', () => {
|
||||||
|
delete _store.simple_fs_filter;
|
||||||
|
_renders = 0;
|
||||||
|
_treeRenders = 0;
|
||||||
|
pysimFsFilter = new Set(['all']);
|
||||||
|
pysimFsSetFilterToService('ust', 2);
|
||||||
|
assert.deepStrictEqual([...pysimFsFilter], ['svc:Fixed Dialling Numbers (FDN)']);
|
||||||
|
assert.strictEqual(_store.simple_fs_filter, JSON.stringify(['svc:Fixed Dialling Numbers (FDN)']));
|
||||||
|
assert.strictEqual(_renders, 1);
|
||||||
|
assert.strictEqual(_treeRenders, 2); // render + the auto-expand's render
|
||||||
|
// a capability-only service changes nothing
|
||||||
|
pysimFsSetFilterToService('ust', 99);
|
||||||
|
assert.deepStrictEqual([...pysimFsFilter], ['svc:Fixed Dialling Numbers (FDN)']);
|
||||||
|
assert.strictEqual(_renders, 1);
|
||||||
|
assert.strictEqual(_treeRenders, 2);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('the four service-table fields carry their table, EF.ACC carries none', () => {
|
||||||
|
assert.match(html, /const EF_UST_FIELDS = \[\{[^\n]*table: 'ust'/);
|
||||||
|
assert.match(html, /const EF_IST_FIELDS = \[\{[^\n]*table: 'ist'/);
|
||||||
|
assert.match(html, /const EF_EST_FIELDS = \[\{[^\n]*table: 'est'/);
|
||||||
|
assert.match(html, /const EF_SST_FIELDS = \[\{[\s\S]{0,200}?table: 'sst'/);
|
||||||
|
const acc = /const EF_ACC_FIELDS = [^\n]*/.exec(html);
|
||||||
|
assert.ok(acc, 'EF_ACC_FIELDS not found');
|
||||||
|
assert.ok(acc[0].indexOf('table') < 0, acc[0]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('both service-grid renderers append the cross-link after their control', () => {
|
||||||
|
const fn = extractFunc(html, 'efDecFieldHtml');
|
||||||
|
assert.strictEqual((fn.match(/pysimFsFilterBtnHtml\(f\.table, n\)/g) || []).length, 2, fn);
|
||||||
|
});
|
||||||
@@ -446,7 +446,8 @@ test('file manager shows FCI info and keeps the selection in state, not the DOM'
|
|||||||
assert.ok(html.includes('pysimFsInfoHtml(sel, name, { arr: arrs, path: pysimFsNodePath(node) })'));
|
assert.ok(html.includes('pysimFsInfoHtml(sel, name, { arr: arrs, path: pysimFsNodePath(node) })'));
|
||||||
assert.ok(!html.includes('pysim-fs-filename'));
|
assert.ok(!html.includes('pysim-fs-filename'));
|
||||||
assert.ok(html.includes('let pysimFsSelected = null;'));
|
assert.ok(html.includes('let pysimFsSelected = null;'));
|
||||||
assert.ok(html.includes('pysimFsSelected = name;'));
|
assert.ok(html.includes('pysimFsSelected = node ? pysimFsNodePath(node) : path;'),
|
||||||
|
'the selection is keyed by the canonical path, not the display name');
|
||||||
assert.ok(!html.includes('pysimFsSelect()'));
|
assert.ok(!html.includes('pysimFsSelect()'));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,702 @@
|
|||||||
|
const { test } = require('node:test');
|
||||||
|
const assert = require('node:assert');
|
||||||
|
const fs = require('node:fs');
|
||||||
|
const path = require('node:path');
|
||||||
|
|
||||||
|
const html = fs.readFileSync(path.join(__dirname, '..', 'index.html'), 'utf8');
|
||||||
|
|
||||||
|
function extractFunc(src, name) {
|
||||||
|
const re = new RegExp('(?:async\\s+)?function\\s+' + name + '\\s*\\([^)]*\\)\\s*\\{');
|
||||||
|
const m = re.exec(src);
|
||||||
|
if (!m) throw new Error('function ' + name + ' not found');
|
||||||
|
let i = m.index + m[0].length - 1;
|
||||||
|
let depth = 0;
|
||||||
|
for (; i < src.length; i++) {
|
||||||
|
if (src[i] === '{') depth++;
|
||||||
|
else if (src[i] === '}') {
|
||||||
|
depth--;
|
||||||
|
if (depth === 0) break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return src.slice(m.index, i + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractConst(src, name) {
|
||||||
|
const m = new RegExp('const ' + name + ' = ').exec(src);
|
||||||
|
if (!m) throw new Error('const ' + name + ' not found');
|
||||||
|
const begin = m.index + m[0].length;
|
||||||
|
const opener = src[begin];
|
||||||
|
const closer = opener === '[' ? ']' : (opener === '{' ? '}' : null);
|
||||||
|
if (!closer) throw new Error('unsupported const ' + name);
|
||||||
|
let depth = 0, i = begin;
|
||||||
|
for (; i < src.length; i++) {
|
||||||
|
if (src[i] === opener) depth++;
|
||||||
|
else if (src[i] === closer) {
|
||||||
|
depth--;
|
||||||
|
if (depth === 0) { i++; break; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 'var ' + name + ' = ' + src.slice(begin, i) + ';\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
const FNS = [
|
||||||
|
'efBytes', 'efHex', 'efDecServiceTable', 'efDecSst', 'efEncServiceTable', 'efEncSst', 'efEncError',
|
||||||
|
'servicesDecode', 'servicesUnmodelled', 'servicesSstState', 'servicesBitState', 'servicesFeatureState',
|
||||||
|
'servicesSstAllocated', 'servicesSstPreserved', 'servicesBitsPreserved', 'servicesSstValues',
|
||||||
|
'servicesTableValues', 'servicesFileBytes', 'pysimServicesColumns', 'pysimServicesGridHtml',
|
||||||
|
'pysimServicesSaveRowHtml', 'pysimServicesCollect',
|
||||||
|
'servicesFsDeclaredTrees', 'servicesFsPresentPaths', 'servicesFsReport',
|
||||||
|
'servicesFsReportHtml', 'servicesFsPathKey', 'servicesFsNormPath', 'servicesFsRolePath',
|
||||||
|
'servicesPathIndexBuild', 'pysimFsNodePath',
|
||||||
|
];
|
||||||
|
|
||||||
|
let code = 'var _pysimServicesCache = null;\nvar _pysimServicesBusy = false;\n';
|
||||||
|
for (const f of FNS) code += extractFunc(html, f) + '\n';
|
||||||
|
for (const c of ['EF_UST_SERVICES', 'EF_SST_SERVICES', 'EF_IST_SERVICES', 'EF_EST_SERVICES',
|
||||||
|
'SERVICES_CROSS_REF', 'SERVICES_FILES', 'SERVICES_FS_MAP', 'SERVICES_FS_TREES',
|
||||||
|
'SERVICES_FS_TREE_ORDER', 'SERVICES_FS_GSM_MAPPED', 'SERVICES_TABLES']) {
|
||||||
|
code += extractConst(html, c);
|
||||||
|
}
|
||||||
|
code += 'globalThis.esc = s => String(s);\nglobalThis.t = k => k;\n';
|
||||||
|
eval(code);
|
||||||
|
|
||||||
|
// ---- data ----
|
||||||
|
|
||||||
|
test('SERVICES_CROSS_REF covers every non-RFU service of the four files exactly once', () => {
|
||||||
|
const tables = { sst: EF_SST_SERVICES, ust: EF_UST_SERVICES, est: EF_EST_SERVICES, ist: EF_IST_SERVICES };
|
||||||
|
// The doc's §A.4 RFU/reserved slots: SST 8/20/50, UST 26/33/50/98.
|
||||||
|
const rfu = { sst: [8, 20, 50], ust: [26, 33, 50, 98], est: [], ist: [] };
|
||||||
|
const seen = { sst: [], ust: [], est: [], ist: [] };
|
||||||
|
for (const f of SERVICES_CROSS_REF) {
|
||||||
|
for (const k of Object.keys(seen)) {
|
||||||
|
if (f[k] === null || f[k] === undefined) continue;
|
||||||
|
assert.ok(Number.isInteger(f[k]), 'non-integer ' + k + ' in ' + f.f);
|
||||||
|
seen[k].push(f[k]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const k of Object.keys(seen)) {
|
||||||
|
assert.strictEqual(new Set(seen[k]).size, seen[k].length, 'duplicate ' + k + ' numbers');
|
||||||
|
const expected = Object.keys(tables[k]).map(Number).filter(n => rfu[k].indexOf(n) < 0).sort((a, b) => a - b);
|
||||||
|
assert.deepStrictEqual(seen[k].slice().sort((a, b) => a - b), expected, k + ' coverage');
|
||||||
|
// the numbers a save copies from the file instead of editing
|
||||||
|
assert.deepStrictEqual(servicesUnmodelled(k), rfu[k], k + ' unmodelled slots');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('SERVICES_CROSS_REF has the doc\'s totals and no empty row', () => {
|
||||||
|
const counts = { sim: 0, usim: 0, isim: 0, shared: 0 };
|
||||||
|
for (const f of SERVICES_CROSS_REF) {
|
||||||
|
assert.ok(typeof f.f === 'string' && f.f.length > 0, 'feature name');
|
||||||
|
assert.ok(Object.prototype.hasOwnProperty.call(counts, f.cls), 'class: ' + f.cls);
|
||||||
|
counts[f.cls]++;
|
||||||
|
const nums = ['sst', 'ust', 'est', 'ist'].filter(k => f[k] !== null && f[k] !== undefined);
|
||||||
|
assert.ok(nums.length > 0, 'row without a service number: ' + f.f);
|
||||||
|
// the class says which applications carry the feature
|
||||||
|
if (f.cls === 'sim') assert.deepStrictEqual(nums, ['sst'], f.f);
|
||||||
|
else if (f.cls === 'isim') assert.deepStrictEqual(nums, ['ist'], f.f);
|
||||||
|
else if (f.cls === 'usim') assert.ok(nums.indexOf('ust') >= 0 && nums.indexOf('sst') < 0 && nums.indexOf('ist') < 0, f.f);
|
||||||
|
else assert.ok(nums.length > 1, 'shared row with one file: ' + f.f);
|
||||||
|
}
|
||||||
|
// docs/services-editor.md: 164 table rows = 161 features + 3 RFU rows
|
||||||
|
// (10 SIM-only, 85 USIM-only, 5 ISIM-only, 61 shared).
|
||||||
|
assert.deepStrictEqual(counts, { sim: 10, usim: 85, isim: 5, shared: 61 });
|
||||||
|
assert.strictEqual(SERVICES_CROSS_REF.length, 161);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('the four files are addressed by their canonical (DF, FID) path', () => {
|
||||||
|
assert.deepStrictEqual(SERVICES_FILES.map(f => f.key), ['sst', 'ust', 'est', 'ist']);
|
||||||
|
const paths = {};
|
||||||
|
for (const f of SERVICES_FILES) paths[f.key] = f.path;
|
||||||
|
assert.deepStrictEqual(paths, {
|
||||||
|
sst: 'DF.GSM/6F38', ust: 'ADF.USIM/6F38', est: 'ADF.USIM/6F56', ist: 'ADF.ISIM/6F07',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---- the edit-state helper ----
|
||||||
|
|
||||||
|
const dec = obj => obj;
|
||||||
|
function sstDec(pairs) {
|
||||||
|
// pairs: { <n>: [allocated, activated] } - a deleted service is absent
|
||||||
|
const services = [];
|
||||||
|
for (const k of Object.keys(pairs)) {
|
||||||
|
const n = Number(k);
|
||||||
|
if (!pairs[k][0] && !pairs[k][1]) continue;
|
||||||
|
services.push({ n, name: '', allocated: !!pairs[k][0], activated: !!pairs[k][1] });
|
||||||
|
}
|
||||||
|
return { services };
|
||||||
|
}
|
||||||
|
function bitDec(nums) {
|
||||||
|
return { services: nums.map(n => ({ n, name: '' })) };
|
||||||
|
}
|
||||||
|
|
||||||
|
test('servicesFeatureState: shared feature on/off/deleted', () => {
|
||||||
|
const adn = SERVICES_CROSS_REF.find(f => f.f === 'Local Phone Book (ADN)');
|
||||||
|
assert.strictEqual(adn.sst, 2);
|
||||||
|
assert.strictEqual(adn.ust, 1);
|
||||||
|
// allocated + activated, UST bit set
|
||||||
|
let st = servicesFeatureState(adn, sstDec({ 2: [1, 1] }), bitDec([1, 3]), null, null);
|
||||||
|
assert.deepStrictEqual(st, { sst: 'on', ust: true, est: null, ist: null });
|
||||||
|
// allocated but not activated -> 'off' (the editor never de-allocates)
|
||||||
|
st = servicesFeatureState(adn, sstDec({ 2: [1, 0] }), bitDec([]), null, null);
|
||||||
|
assert.deepStrictEqual(st, { sst: 'off', ust: false, est: null, ist: null });
|
||||||
|
// deleted on the SIM (the decoder drops the service entirely) -> 'off'
|
||||||
|
st = servicesFeatureState(adn, sstDec({ 3: [1, 1] }), bitDec([]), null, null);
|
||||||
|
assert.strictEqual(st.sst, 'off');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('servicesFeatureState: sim-only, usim-only, isim-only and EST', () => {
|
||||||
|
const chv1 = SERVICES_CROSS_REF.find(f => f.f === 'CHV1 disable function');
|
||||||
|
assert.strictEqual(chv1.cls, 'sim');
|
||||||
|
assert.deepStrictEqual(servicesFeatureState(chv1, sstDec({ 1: [1, 1] }), null, null, null),
|
||||||
|
{ sst: 'on', ust: null, est: null, ist: null });
|
||||||
|
|
||||||
|
const oci = SERVICES_CROSS_REF.find(f => f.f === 'Outgoing Call Information (OCI and OCT)');
|
||||||
|
assert.deepStrictEqual(servicesFeatureState(oci, null, bitDec([8]), null, null),
|
||||||
|
{ sst: null, ust: true, est: null, ist: null });
|
||||||
|
|
||||||
|
const pcscf = SERVICES_CROSS_REF.find(f => f.f === 'P-CSCF address');
|
||||||
|
assert.deepStrictEqual(servicesFeatureState(pcscf, null, null, null, bitDec([1])),
|
||||||
|
{ sst: null, ust: null, est: null, ist: true });
|
||||||
|
|
||||||
|
const fdn = SERVICES_CROSS_REF.find(f => f.f === 'Fixed Dialling Numbers (FDN)');
|
||||||
|
assert.strictEqual(fdn.est, 1);
|
||||||
|
assert.deepStrictEqual(servicesFeatureState(fdn, sstDec({ 3: [1, 1] }), bitDec([2]), bitDec([]), null),
|
||||||
|
{ sst: 'on', ust: true, est: false, ist: null });
|
||||||
|
|
||||||
|
// a feature without a number in a file is null even when the file is there
|
||||||
|
const ccp2 = SERVICES_CROSS_REF.find(f => f.f === 'Capability Configuration Parameters 2 (CCP2)');
|
||||||
|
assert.deepStrictEqual(servicesFeatureState(ccp2, sstDec({ 1: [1, 1] }), bitDec([14]), null, null).sst, null);
|
||||||
|
// an absent file leaves its column null
|
||||||
|
const adn = SERVICES_CROSS_REF.find(f => f.f === 'Local Phone Book (ADN)');
|
||||||
|
assert.strictEqual(servicesFeatureState(adn, null, bitDec([1]), null, null).sst, null);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---- render ----
|
||||||
|
|
||||||
|
test('the grid renders a column per present file and the right cell states', () => {
|
||||||
|
const adn = SERVICES_CROSS_REF.findIndex(f => f.f === 'Local Phone Book (ADN)');
|
||||||
|
const files = {
|
||||||
|
sst: { present: true, hex: '00300000', dec: sstDec({ 2: [1, 1] }) },
|
||||||
|
ust: { present: true, hex: '02000000', dec: bitDec([1]) },
|
||||||
|
est: { present: false },
|
||||||
|
ist: { present: false },
|
||||||
|
};
|
||||||
|
const out = pysimServicesGridHtml(files, {});
|
||||||
|
assert.ok(out.includes('>SST<'), out);
|
||||||
|
assert.ok(out.includes('>UST<'), out);
|
||||||
|
assert.ok(!out.includes('>EST<'), out);
|
||||||
|
assert.ok(!out.includes('>IST<'), out);
|
||||||
|
// the ADN row: SST select on 'on', UST checkbox checked
|
||||||
|
assert.ok(out.includes('data-svc="sst" data-row="' + adn + '"'));
|
||||||
|
assert.match(out, new RegExp('<option value="on" selected>[^<]*</option>'));
|
||||||
|
const adnRow = out.split('<tr').find(r => r.includes('Local Phone Book (ADN)'));
|
||||||
|
assert.ok(adnRow.includes('checked'), adnRow);
|
||||||
|
assert.ok(adnRow.includes('<select data-svc="sst"'), adnRow);
|
||||||
|
// an ISIM-only feature is hidden while EF.IST is absent
|
||||||
|
assert.ok(!out.includes('P-CSCF address'), out);
|
||||||
|
// and a UST-only feature is hidden while EF.UST is absent
|
||||||
|
const sstOnly = pysimServicesGridHtml({ sst: files.sst, ust: { present: false } }, {});
|
||||||
|
assert.ok(sstOnly.includes('>SST<'), sstOnly);
|
||||||
|
assert.ok(!sstOnly.includes('>UST<'), sstOnly);
|
||||||
|
assert.ok(!sstOnly.includes('Outgoing Call Information'), sstOnly);
|
||||||
|
assert.ok(sstOnly.includes('Local Phone Book (ADN)'), sstOnly);
|
||||||
|
// a feature without a number in a present file shows an empty cell, no input
|
||||||
|
const plmnRow = out.split('<tr').find(r => r.includes('>PLMN selector<'));
|
||||||
|
assert.ok(plmnRow, 'the PLMN selector row renders');
|
||||||
|
assert.ok(plmnRow.includes('data-svc="sst"'), plmnRow);
|
||||||
|
assert.ok(!plmnRow.includes('data-svc="ust"'), plmnRow);
|
||||||
|
assert.ok(plmnRow.includes('\u2014'), plmnRow);
|
||||||
|
// an EST column renders a checkbox per EST-numbered feature (FDN = 1)
|
||||||
|
const withEst = pysimServicesGridHtml({ ust: files.ust, est: { present: true, dec: bitDec([]) } }, {});
|
||||||
|
assert.ok(withEst.includes('>EST<'), withEst);
|
||||||
|
const fdnRow = withEst.split('<tr').find(r => r.includes('>Fixed Dialling Numbers (FDN)<'));
|
||||||
|
assert.ok(fdnRow.includes('data-svc="est"'), fdnRow);
|
||||||
|
assert.ok(!fdnRow.includes('data-svc="est" data-row="2" checked'), fdnRow);
|
||||||
|
// ... and is checked from the card's decode
|
||||||
|
const estOn = pysimServicesGridHtml({ ust: files.ust, est: { present: true, dec: bitDec([1]) } }, {});
|
||||||
|
const fdnOn = estOn.split('<tr').find(r => r.includes('>Fixed Dialling Numbers (FDN)<'));
|
||||||
|
assert.match(fdnOn, /data-svc="est" data-row="\d+" checked/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('no present file reports why, and the save row lists only present files', () => {
|
||||||
|
const out = pysimServicesGridHtml({}, {});
|
||||||
|
assert.ok(out.includes('No service table found on this card'), out);
|
||||||
|
const row = pysimServicesSaveRowHtml({
|
||||||
|
sst: { present: true }, ust: { present: true }, est: { present: false }, ist: { present: false },
|
||||||
|
});
|
||||||
|
assert.ok(row.includes('pysimServicesSave(\'sst\')'), row);
|
||||||
|
assert.ok(row.includes('pysimServicesSave(\'ust\')'), row);
|
||||||
|
assert.ok(!row.includes('pysimServicesSave(\'est\')'), row);
|
||||||
|
assert.ok(!row.includes('pysimServicesSave(\'ist\')'), row);
|
||||||
|
assert.ok(row.includes('Save to EF.SST'), row);
|
||||||
|
assert.ok(row.includes('Save to EF.UST'), row);
|
||||||
|
// the per-button status spans the writes report into
|
||||||
|
assert.ok(row.includes('id="pysim-services-save-status-sst"'), row);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('pysimServicesCollect reads the form controls into per-row state', () => {
|
||||||
|
const grid = {
|
||||||
|
querySelectorAll: () => [
|
||||||
|
{ getAttribute: k => ({ 'data-svc': 'sst', 'data-row': '3' })[k], type: 'select-one', value: 'off' },
|
||||||
|
{ getAttribute: k => ({ 'data-svc': 'ust', 'data-row': '3' })[k], type: 'checkbox', checked: true },
|
||||||
|
{ getAttribute: k => ({ 'data-svc': 'ust', 'data-row': '4' })[k], type: 'checkbox', checked: false },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
globalThis.document = { getElementById: id => (id === 'pysim-services-grid' ? grid : null) };
|
||||||
|
assert.deepStrictEqual(pysimServicesCollect(), { 3: { sst: 'off', ust: true }, 4: { ust: false } });
|
||||||
|
globalThis.document = { getElementById: () => null };
|
||||||
|
assert.deepStrictEqual(pysimServicesCollect(), {});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---- save ----
|
||||||
|
|
||||||
|
test('SST bytes: on allocates and activates, off never de-allocates', () => {
|
||||||
|
// service 3 (FDN) allocated but not activated -> 0x10 in byte 0
|
||||||
|
const files = { sst: { present: true, hex: '10000000' } };
|
||||||
|
const fdn = SERVICES_CROSS_REF.findIndex(f => f.f === 'Fixed Dialling Numbers (FDN)');
|
||||||
|
assert.strictEqual(efBytes('10000000')[0] & 0x30, 0x10); // sanity: pair 1
|
||||||
|
// "off" on an allocated-off service keeps it allocated (pair 1, not 0)
|
||||||
|
let out = servicesFileBytes('sst', { [fdn]: { sst: 'off' } }, files);
|
||||||
|
assert.strictEqual(out, '10000000');
|
||||||
|
// "on" allocates and activates (pair 3)
|
||||||
|
out = servicesFileBytes('sst', { [fdn]: { sst: 'on' } }, files);
|
||||||
|
assert.strictEqual(out, '30000000');
|
||||||
|
// switching an allocated+activated service to "off" de-activates but keeps
|
||||||
|
// the allocation (pair 1)
|
||||||
|
out = servicesFileBytes('sst', { [fdn]: { sst: 'off' } }, { sst: { present: true, hex: '30000000' } });
|
||||||
|
assert.strictEqual(out, '10000000');
|
||||||
|
// a deleted service stays deleted when the form renders it as "off"
|
||||||
|
out = servicesFileBytes('sst', { [fdn]: { sst: 'off' } }, { sst: { present: true, hex: '00000000' } });
|
||||||
|
assert.strictEqual(out, '00000000');
|
||||||
|
// ... and is allocated by an explicit "on"
|
||||||
|
out = servicesFileBytes('sst', { [fdn]: { sst: 'on' } }, { sst: { present: true, hex: '00000000' } });
|
||||||
|
assert.strictEqual(out, '30000000');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('SST bytes preserve the unmodelled RFU slots and the file length', () => {
|
||||||
|
// service 8 = RFU, in byte 1 (services 5-8), pair 4 -> bits 6+7 = 0xC0
|
||||||
|
const files = { sst: { present: true, hex: '00C000' } };
|
||||||
|
assert.deepStrictEqual(servicesUnmodelled('sst'), [8, 20, 50]);
|
||||||
|
assert.strictEqual(servicesFileBytes('sst', {}, files), '00C000',
|
||||||
|
'the RFU pair and the trailing byte survive');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('UST/EST/IST bytes come from efEncServiceTable over the original bytes', () => {
|
||||||
|
// UST service 1 (ADN) set in byte 0; service 26 (RFU) in byte 3 bit 1
|
||||||
|
const files = { ust: { present: true, hex: '01000002' } };
|
||||||
|
const adn = SERVICES_CROSS_REF.findIndex(f => f.f === 'Local Phone Book (ADN)');
|
||||||
|
const sms = SERVICES_CROSS_REF.findIndex(f => f.f === 'Short Message Storage (SMS)');
|
||||||
|
let out = servicesFileBytes('ust', { [adn]: { ust: true }, [sms]: { ust: true } }, files);
|
||||||
|
// service 10 (SMS) -> byte 1 bit 1 = 0x02
|
||||||
|
assert.strictEqual(out, '01020002');
|
||||||
|
out = servicesFileBytes('ust', { [adn]: { ust: false }, [sms]: { ust: false } }, files);
|
||||||
|
assert.strictEqual(out, '00000002', 'the RFU bit is preserved, the modelled bits cleared');
|
||||||
|
// EST has its own numbering: FDN = 1
|
||||||
|
const estFiles = { est: { present: true, hex: '00' } };
|
||||||
|
const fdn = SERVICES_CROSS_REF.findIndex(f => f.f === 'Fixed Dialling Numbers (FDN)');
|
||||||
|
assert.strictEqual(servicesFileBytes('est', { [fdn]: { est: true } }, estFiles), '01');
|
||||||
|
assert.strictEqual(servicesFileBytes('est', { [fdn]: { est: false } }, estFiles), '00');
|
||||||
|
// ISIM IST: P-CSCF address = 1
|
||||||
|
const istFiles = { ist: { present: true, hex: '00' } };
|
||||||
|
const pcscf = SERVICES_CROSS_REF.findIndex(f => f.f === 'P-CSCF address');
|
||||||
|
assert.strictEqual(servicesFileBytes('ist', { [pcscf]: { ist: true } }, istFiles), '01');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('an absent file yields no bytes', () => {
|
||||||
|
assert.strictEqual(servicesFileBytes('sst', {}, { sst: { present: false } }), null);
|
||||||
|
assert.strictEqual(servicesFileBytes('ust', {}, {}), null);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---- filesystem check: the feature -> symbolic files table (docs/services-editor.md §B) ----
|
||||||
|
|
||||||
|
const UICC_FILES = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'uicc_files.json'), 'utf8'));
|
||||||
|
const FS_INDEX = servicesPathIndexBuild(UICC_FILES.files);
|
||||||
|
const fsResolve = (role, root) => FS_INDEX.get(servicesFsPathKey(role, root));
|
||||||
|
|
||||||
|
test('SERVICES_FS_MAP covers every feature exactly once and carries symbolic file names', () => {
|
||||||
|
const names = SERVICES_CROSS_REF.map(f => f.f);
|
||||||
|
assert.deepStrictEqual(Object.keys(SERVICES_FS_MAP), names,
|
||||||
|
'one entry per cross-reference row, in table order');
|
||||||
|
let backed = 0, capability = 0;
|
||||||
|
for (const [name, roles] of Object.entries(SERVICES_FS_MAP)) {
|
||||||
|
assert.ok(Array.isArray(roles), 'not an array: ' + name);
|
||||||
|
assert.strictEqual(new Set(roles).size, roles.length, 'duplicate role: ' + name);
|
||||||
|
// the standard file list's own names, never FIDs: a FID is ambiguous
|
||||||
|
// across applications (4F30 = EF.PBR and EF.SAI)
|
||||||
|
for (const role of roles) assert.match(role, /^(EF|DF)\.[A-Za-z0-9_+.-]+$/, name + ': ' + role);
|
||||||
|
if (roles.length) backed++; else capability++;
|
||||||
|
}
|
||||||
|
// docs/services-editor.md §B.1 lists 34 `capability` cells; the remaining
|
||||||
|
// 127 rows declare at least one file.
|
||||||
|
assert.strictEqual(capability, 34);
|
||||||
|
assert.strictEqual(backed, 127);
|
||||||
|
assert.strictEqual(SERVICES_CROSS_REF.length, backed + capability);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('the capability features carry no file check', () => {
|
||||||
|
for (const name of [
|
||||||
|
'CHV1 disable function', 'Data download via SMS-CB', 'Data download via SMS-PP',
|
||||||
|
'Menu selection', 'Call control (by SIM/USIM)', 'Proactive SIM',
|
||||||
|
'MO-SMS control (by SIM/USIM)', 'RUN AT COMMAND command', 'Call control on GPRS by USIM',
|
||||||
|
'Additional TERMINAL PROFILE after UICC activation', 'Terminal Applications',
|
||||||
|
'URI support by UICC', 'HTTP Digest', 'Support of P-CSCF discovery for IMS Local Break Out',
|
||||||
|
]) {
|
||||||
|
assert.deepStrictEqual(SERVICES_FS_MAP[name], [], name);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('the well-known features map to their documented files', () => {
|
||||||
|
assert.deepStrictEqual(SERVICES_FS_MAP['Fixed Dialling Numbers (FDN)'], ['EF.FDN', 'EF.EXT2']);
|
||||||
|
assert.deepStrictEqual(SERVICES_FS_MAP['Service Provider Name (SPN)'], ['EF.SPN']);
|
||||||
|
assert.deepStrictEqual(SERVICES_FS_MAP['GSM Access'], ['DF.GSM-ACCESS']);
|
||||||
|
assert.deepStrictEqual(SERVICES_FS_MAP['Local Phone Book (ADN)'], ['EF.ADN', 'DF.PHONEBOOK', 'EF.PBR']);
|
||||||
|
assert.deepStrictEqual(SERVICES_FS_MAP['SoLSA'], ['DF.SoLSA', 'EF.SAI', 'EF.SLL']);
|
||||||
|
assert.deepStrictEqual(SERVICES_FS_MAP['MExE'], ['DF.MExE', 'EF.MExE-ST']);
|
||||||
|
assert.deepStrictEqual(SERVICES_FS_MAP['A2X'], ['DF.A2X', 'EF.AST', 'EF.A2X_CONFIG']);
|
||||||
|
assert.deepStrictEqual(SERVICES_FS_MAP['Advice of Charge (AoC)'], ['EF.ACM', 'EF.ACMmax', 'EF.PUCT']);
|
||||||
|
assert.deepStrictEqual(SERVICES_FS_MAP['Extension 4'], ['EF.EXT4'],
|
||||||
|
'one name: the SIM and the USIM copy have different FIDs but the same name');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('a role outside its feature\'s declaring tree lives in the shared DF.TELECOM subtree', () => {
|
||||||
|
// A UST/IST-gated DF may be a child of DF.TELECOM (TS 31.102 4.6.0), i.e.
|
||||||
|
// an MF path - the resolver's fallback. Anything else would be a mapping
|
||||||
|
// error, and the GSM application's mapped copies (EF.ADN/EF.EXT1/EF.ECCP)
|
||||||
|
// are deliberately not expected there.
|
||||||
|
const features = new Set();
|
||||||
|
for (const f of SERVICES_CROSS_REF) {
|
||||||
|
const trees = [];
|
||||||
|
if (f.sst !== null) trees.push('MF');
|
||||||
|
if (f.ust !== null || f.est !== null) trees.push('ADF.USIM');
|
||||||
|
if (f.ist !== null) trees.push('ADF.ISIM');
|
||||||
|
for (const role of (SERVICES_FS_MAP[f.f] || [])) {
|
||||||
|
if (trees.some(root => fsResolve(role, root))) continue;
|
||||||
|
const path = fsResolve(role, 'MF');
|
||||||
|
if (!path) continue; // a registry gap, pinned above
|
||||||
|
assert.match(path, /^MF\/7F10\//, f.f + ': ' + role + ' -> ' + path);
|
||||||
|
assert.ok(SERVICES_FS_GSM_MAPPED.indexOf(role) < 0, f.f + ': ' + role);
|
||||||
|
features.add(f.f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert.deepStrictEqual(Array.from(features),
|
||||||
|
['Multimedia Messages Storage', 'MCPTT', 'V2X', 'A2X']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('every mapped role is a standard file, except the documented registry gaps', () => { // The names that resolve in none of the checked trees: files the standard
|
||||||
|
// list does not carry yet (pySim omits DF.GRAPHICS and the DF.TELECOM
|
||||||
|
// copies of DF.SoLSA/DF.MExE; EF.3GPPPSDATAOFFservicelist is a pySim TODO).
|
||||||
|
// The report lists these as `unchecked` instead of guessing a path.
|
||||||
|
const gaps = {
|
||||||
|
'Image (IMG)': ['DF.GRAPHICS', 'EF.IMG'],
|
||||||
|
'3GPP PS Data Off Service List': ['EF.3GPPPSDATAOFFservicelist'],
|
||||||
|
};
|
||||||
|
const unresolved = {};
|
||||||
|
for (const [name, roles] of Object.entries(SERVICES_FS_MAP)) {
|
||||||
|
for (const role of roles) {
|
||||||
|
if (SERVICES_FS_TREE_ORDER.some(root => fsResolve(role, root))) continue;
|
||||||
|
(unresolved[name] = unresolved[name] || []).push(role);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert.deepStrictEqual(unresolved, gaps);
|
||||||
|
// a mapped role that does resolve resolves through exactly one list entry
|
||||||
|
for (const roles of Object.values(SERVICES_FS_MAP)) {
|
||||||
|
for (const role of roles) {
|
||||||
|
for (const root of SERVICES_FS_TREE_ORDER) {
|
||||||
|
const path = fsResolve(role, root);
|
||||||
|
if (!path) continue;
|
||||||
|
const entries = UICC_FILES.files.filter(f => f.name === role
|
||||||
|
&& f.root.toUpperCase() === root && f.path.toUpperCase() === path);
|
||||||
|
assert.strictEqual(entries.length, 1, role + ' ' + root + ' -> ' + path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---- filesystem check: the (name, root) -> path index ----
|
||||||
|
|
||||||
|
test('servicesPathIndexBuild keys the standard list by (name, root)', () => {
|
||||||
|
assert.ok(FS_INDEX.size >= 200, 'indexed files: ' + FS_INDEX.size);
|
||||||
|
// every value is one of the list's paths, and the list's roots are the keys
|
||||||
|
for (const [key, path] of FS_INDEX) {
|
||||||
|
const [name, root] = key.split('|');
|
||||||
|
assert.ok(UICC_FILES.files.some(f => f.name.toUpperCase() === name && f.root.toUpperCase() === root
|
||||||
|
&& f.path.toUpperCase() === path), key + ' -> ' + path);
|
||||||
|
}
|
||||||
|
assert.deepStrictEqual(servicesPathIndexBuild(null), new Map());
|
||||||
|
assert.deepStrictEqual(servicesPathIndexBuild([null, {}, { name: 'EF.X' }]), new Map());
|
||||||
|
// case-insensitive on both sides
|
||||||
|
assert.strictEqual(FS_INDEX.get(servicesFsPathKey('ef.spn', 'mf')), 'MF/7F20/6F46');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('the same FID under different names resolves to its own path', () => {
|
||||||
|
// EF.PBR and EF.SAI are both 4F30
|
||||||
|
assert.strictEqual(fsResolve('EF.PBR', 'ADF.USIM'), 'ADF.USIM/5F3A/4F30');
|
||||||
|
assert.strictEqual(fsResolve('EF.SAI', 'ADF.USIM'), 'ADF.USIM/5F70/4F30');
|
||||||
|
// EF.PBR exists in both trees, under different parents
|
||||||
|
assert.strictEqual(fsResolve('EF.PBR', 'MF'), 'MF/7F10/5F3A/4F30');
|
||||||
|
// a DF.TELECOM child resolves in the MF tree
|
||||||
|
assert.strictEqual(fsResolve('DF.A2X', 'MF'), 'MF/7F10/5F3F');
|
||||||
|
assert.strictEqual(fsResolve('EF.A2X_CONFIG', 'MF'), 'MF/7F10/5F3F/4F02');
|
||||||
|
// EF.SST and EF.UST are both 6F38
|
||||||
|
assert.strictEqual(fsResolve('EF.UST', 'ADF.USIM'), 'ADF.USIM/6F38');
|
||||||
|
assert.strictEqual(fsResolve('EF.IST', 'ADF.ISIM'), 'ADF.ISIM/6F07');
|
||||||
|
// the one duplicate (name, root): pySim models EF.SUCI_Calc_Info twice
|
||||||
|
// under ADF.USIM (DF.5GS/4F07 - the UST 125 file - and the eSIM profile
|
||||||
|
// package's DF.SAIP/4F01); the list's first entry, DF.5GS, wins
|
||||||
|
const suci = UICC_FILES.files.filter(f => f.name === 'EF.SUCI_Calc_Info' && f.root === 'ADF.USIM');
|
||||||
|
assert.strictEqual(suci.length, 2);
|
||||||
|
assert.strictEqual(fsResolve('EF.SUCI_Calc_Info', 'ADF.USIM'), 'ADF.USIM/5FC0/4F07');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('servicesFsRolePath falls back to the shared DF.TELECOM subtree', () => {
|
||||||
|
// DF.MCS is a child of DF.TELECOM (TS 31.102 4.6.0): a UST declaration
|
||||||
|
// resolves it in the MF tree
|
||||||
|
assert.strictEqual(servicesFsRolePath(FS_INDEX, 'DF.MCS', 'ADF.USIM'), 'MF/7F10/5F3D');
|
||||||
|
assert.strictEqual(servicesFsRolePath(FS_INDEX, 'EF.MST', 'ADF.USIM'), 'MF/7F10/5F3D/4F01');
|
||||||
|
// an own-tree path wins over the fallback
|
||||||
|
assert.strictEqual(servicesFsRolePath(FS_INDEX, 'EF.FDN', 'ADF.USIM'), 'ADF.USIM/6F3B');
|
||||||
|
assert.strictEqual(servicesFsRolePath(FS_INDEX, 'EF.FDN', 'MF'), 'MF/7F10/6F3B');
|
||||||
|
// the GSM application's mapped copies are not expected by a USIM
|
||||||
|
// declaration (TS 31.102 4.5) ...
|
||||||
|
assert.strictEqual(servicesFsRolePath(FS_INDEX, 'EF.ADN', 'ADF.USIM'), undefined);
|
||||||
|
assert.ok(SERVICES_FS_GSM_MAPPED.indexOf('EF.ADN') >= 0);
|
||||||
|
// ... and a SIM (MF) declaration never falls back to an application
|
||||||
|
assert.strictEqual(servicesFsRolePath(FS_INDEX, 'DF.SoLSA', 'MF'), undefined);
|
||||||
|
assert.strictEqual(servicesFsRolePath(null, 'EF.SPN', 'MF'), undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---- filesystem check: the report ----
|
||||||
|
|
||||||
|
const svcFiles = (sst, ust, est, ist) => ({
|
||||||
|
sst: sst ? { present: true, dec: sst } : { present: false },
|
||||||
|
ust: ust ? { present: true, dec: ust } : { present: false },
|
||||||
|
est: est ? { present: true, dec: est } : { present: false },
|
||||||
|
ist: ist ? { present: true, dec: ist } : { present: false },
|
||||||
|
});
|
||||||
|
|
||||||
|
// A file-manager tree with parent links (pysimFsNodePath walks up them).
|
||||||
|
function fsTree(children) {
|
||||||
|
const mk = (spec, parent) => {
|
||||||
|
const node = {
|
||||||
|
name: spec.name, fid: spec.fid, isDir: !!spec.isDir, exists: spec.exists,
|
||||||
|
children: null, parent: parent,
|
||||||
|
};
|
||||||
|
if (spec.children) node.children = spec.children.map(c => mk(c, node));
|
||||||
|
return node;
|
||||||
|
};
|
||||||
|
const root = { name: 'MF', path: 'MF', fid: '3F00', isDir: true, exists: true, parent: null, children: null };
|
||||||
|
root.children = (children || []).map(c => mk(c, root));
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
|
||||||
|
test('servicesFsPresentPaths collects the probed nodes\' canonical paths', () => {
|
||||||
|
const root = fsTree([
|
||||||
|
{ name: 'DF.TELECOM', fid: '7F10', isDir: true, exists: true, children: [
|
||||||
|
{ name: 'EF.SMS', fid: '6f3c', exists: true },
|
||||||
|
{ name: 'EF.LND', fid: '6F44', exists: false },
|
||||||
|
] },
|
||||||
|
{ name: 'EF.ICCID', fid: '2FE2', exists: null }, // never probed
|
||||||
|
{ name: 'ADF.USIM', isDir: true, exists: true, children: [
|
||||||
|
{ name: 'EF.UST', fid: '6F38', exists: true },
|
||||||
|
] },
|
||||||
|
{ name: 'DF.ABSENT', fid: '5F80', isDir: true, exists: false, children: [
|
||||||
|
{ name: 'EF.BCAST', fid: '4F01', exists: true }, // under an absent DF
|
||||||
|
] },
|
||||||
|
]);
|
||||||
|
assert.deepStrictEqual(servicesFsPresentPaths(root),
|
||||||
|
['MF/7F10', 'MF/7F10/6F3C', 'ADF.USIM', 'ADF.USIM/6F38']);
|
||||||
|
assert.deepStrictEqual(servicesFsPresentPaths(null), []);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('servicesFsDeclaredTrees groups the declared features by declaring application', () => {
|
||||||
|
const files = svcFiles(
|
||||||
|
sstDec({ 3: [1, 1], 4: [1, 0] }), // FDN on, SMS allocated-off
|
||||||
|
bitDec([2, 10]), // UST FDN + SMS
|
||||||
|
null, // no EF.EST on the card
|
||||||
|
bitDec([6])); // IST SMS
|
||||||
|
const trees = servicesFsDeclaredTrees(files);
|
||||||
|
assert.deepStrictEqual(Object.keys(trees), ['MF', 'ADF.USIM', 'ADF.ISIM']);
|
||||||
|
assert.ok(trees.MF.includes('Fixed Dialling Numbers (FDN)'), trees.MF.join(' | '));
|
||||||
|
assert.ok(!trees.MF.includes('Short Message Storage (SMS)'), 'an allocated-off SST service declares nothing');
|
||||||
|
assert.ok(trees['ADF.USIM'].includes('Fixed Dialling Numbers (FDN)'));
|
||||||
|
assert.ok(trees['ADF.USIM'].includes('Short Message Storage (SMS)'));
|
||||||
|
assert.ok(trees['ADF.ISIM'].includes('Short Message Storage (SMS)'));
|
||||||
|
// the tree's own order: cross-reference order, deduplicated
|
||||||
|
assert.deepStrictEqual(trees['ADF.USIM'], ['Fixed Dialling Numbers (FDN)', 'Short Message Storage (SMS)']);
|
||||||
|
// a tree whose service table is absent is not checked at all
|
||||||
|
assert.deepStrictEqual(servicesFsDeclaredTrees(svcFiles(null, bitDec([2]), null, null)),
|
||||||
|
{ 'ADF.USIM': ['Fixed Dialling Numbers (FDN)'] });
|
||||||
|
assert.deepStrictEqual(servicesFsDeclaredTrees(svcFiles(null, null, null, null)), {});
|
||||||
|
});
|
||||||
|
|
||||||
|
const fsReport = (declaredByTree, present) => servicesFsReport(declaredByTree, FS_INDEX, present);
|
||||||
|
|
||||||
|
test('servicesFsReport: a declared feature with an absent path', () => {
|
||||||
|
const rep = fsReport({ MF: ['Fixed Dialling Numbers (FDN)'] }, ['MF/7F10/6F3B']);
|
||||||
|
assert.deepStrictEqual(rep.missing,
|
||||||
|
[{ feature: 'Fixed Dialling Numbers (FDN)', tree: 'MF', paths: ['MF/7F10/6F4B'] }]);
|
||||||
|
assert.deepStrictEqual(rep.extra, []);
|
||||||
|
assert.deepStrictEqual(rep.unchecked, []);
|
||||||
|
// both files gone, in the map's (doc) role order
|
||||||
|
assert.deepStrictEqual(fsReport({ MF: ['Fixed Dialling Numbers (FDN)'] }, []).missing,
|
||||||
|
[{ feature: 'Fixed Dialling Numbers (FDN)', tree: 'MF', paths: ['MF/7F10/6F3B', 'MF/7F10/6F4B'] }]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('servicesFsReport: the same FID in another DF no longer explains a service', () => {
|
||||||
|
// EF.PBR and EF.SAI are both 4F30: a card holding the phonebook copy but
|
||||||
|
// no DF.SoLSA leaves SoLSA's paths missing
|
||||||
|
const rep = fsReport({ 'ADF.USIM': ['SoLSA'] }, ['ADF.USIM/5F3A', 'ADF.USIM/5F3A/4F30']);
|
||||||
|
assert.deepStrictEqual(rep.missing, [{ feature: 'SoLSA', tree: 'ADF.USIM',
|
||||||
|
paths: ['ADF.USIM/5F70', 'ADF.USIM/5F70/4F30', 'ADF.USIM/5F70/4F31'] }]);
|
||||||
|
// the phonebook paths are present but ADN is not declared
|
||||||
|
assert.deepStrictEqual(rep.extra.map(e => e.path), ['ADF.USIM/5F3A', 'ADF.USIM/5F3A/4F30']);
|
||||||
|
assert.deepStrictEqual(rep.extra[1],
|
||||||
|
{ path: 'ADF.USIM/5F3A/4F30', role: 'EF.PBR', tree: 'ADF.USIM', features: ['Local Phone Book (ADN)'] });
|
||||||
|
// with the SoLSA paths present the report is clean
|
||||||
|
const ok = fsReport({ 'ADF.USIM': ['SoLSA', 'Local Phone Book (ADN)'] },
|
||||||
|
['ADF.USIM/5F70', 'ADF.USIM/5F70/4F30', 'ADF.USIM/5F70/4F31', 'ADF.USIM/5F3A', 'ADF.USIM/5F3A/4F30']);
|
||||||
|
assert.deepStrictEqual(ok, { missing: [], extra: [], unchecked: [] });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('servicesFsReport: ADN\'s USIM files are the phonebook pair, not the GSM copy', () => {
|
||||||
|
// TS 31.102 4.5: EF.ADN under DF.TELECOM is the GSM application's mapping
|
||||||
|
const usim = fsReport({ 'ADF.USIM': ['Local Phone Book (ADN)'] }, ['ADF.USIM/5F3A', 'ADF.USIM/5F3A/4F30']);
|
||||||
|
assert.deepStrictEqual(usim, { missing: [], extra: [], unchecked: [] });
|
||||||
|
// ... so a USIM card without the mapped 6F3A is not missing anything
|
||||||
|
assert.deepStrictEqual(fsReport({ 'ADF.USIM': ['Local Phone Book (ADN)'] }, ['ADF.USIM/5F3A/4F30']).missing,
|
||||||
|
[{ feature: 'Local Phone Book (ADN)', tree: 'ADF.USIM', paths: ['ADF.USIM/5F3A'] }]);
|
||||||
|
// the SIM declaration does expect it
|
||||||
|
const sim = fsReport({ MF: ['Local Phone Book (ADN)'] }, ['MF/7F10/5F3A', 'MF/7F10/5F3A/4F30']);
|
||||||
|
assert.deepStrictEqual(sim.missing,
|
||||||
|
[{ feature: 'Local Phone Book (ADN)', tree: 'MF', paths: ['MF/7F10/6F3A'] }]);
|
||||||
|
assert.deepStrictEqual(sim.extra, []);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('servicesFsReport: a UST-declared DF.TELECOM file resolves in the MF tree', () => {
|
||||||
|
// MCPTT's DF.MCS is a child of DF.TELECOM (TS 31.102 4.6.0)
|
||||||
|
assert.deepStrictEqual(fsReport({ 'ADF.USIM': ['MCPTT'] }, ['MF/7F10/5F3D', 'MF/7F10/5F3D/4F01']),
|
||||||
|
{ missing: [], extra: [], unchecked: [] });
|
||||||
|
assert.deepStrictEqual(fsReport({ 'ADF.USIM': ['MCPTT'] }, []).missing,
|
||||||
|
[{ feature: 'MCPTT', tree: 'ADF.USIM', paths: ['MF/7F10/5F3D', 'MF/7F10/5F3D/4F01'] }]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('servicesFsReport: a declared feature the standard list cannot resolve is unchecked', () => {
|
||||||
|
const rep = fsReport({ 'ADF.USIM': ['Image (IMG)'] }, []);
|
||||||
|
assert.deepStrictEqual(rep, { missing: [], extra: [], unchecked: [
|
||||||
|
{ feature: 'Image (IMG)', tree: 'ADF.USIM', roles: ['DF.GRAPHICS', 'EF.IMG'] }] });
|
||||||
|
// a capability row has no files and is never unchecked
|
||||||
|
assert.deepStrictEqual(fsReport({ MF: ['CHV1 disable function'] }, []),
|
||||||
|
{ missing: [], extra: [], unchecked: [] });
|
||||||
|
// a tree the card does not have declares nothing there
|
||||||
|
assert.deepStrictEqual(fsReport({}, []), { missing: [], extra: [], unchecked: [] });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('servicesFsReport: a shared file is explained by any declared feature', () => {
|
||||||
|
// DF.GSM-ACCESS is gated by both "GSM Access" and "GSM security context";
|
||||||
|
// declaring either one explains the present path
|
||||||
|
const present = ['ADF.USIM/5F3B', 'ADF.USIM/5F3B/4F20', 'ADF.USIM/5F3B/4F52'];
|
||||||
|
assert.deepStrictEqual(fsReport({ 'ADF.USIM': ['GSM security context'] }, present),
|
||||||
|
{ missing: [], extra: [], unchecked: [] });
|
||||||
|
// with neither declared the path is an extra, naming every mapping feature
|
||||||
|
assert.deepStrictEqual(fsReport({ 'ADF.USIM': [] }, ['ADF.USIM/5F3B']).extra,
|
||||||
|
[{ path: 'ADF.USIM/5F3B', role: 'DF.GSM-ACCESS', tree: 'ADF.USIM',
|
||||||
|
features: ['GSM Access', 'GSM security context'] }]);
|
||||||
|
// a lower-case present path matches the list's upper-case one
|
||||||
|
assert.deepStrictEqual(fsReport({ MF: ['Service Provider Name (SPN)'] }, ['mf/7f20/6f46']),
|
||||||
|
{ missing: [], extra: [], unchecked: [] });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('servicesFsReport: a present path whose service is undeclared', () => {
|
||||||
|
const rep = fsReport({ MF: [] }, ['MF/7F10/6F3C']);
|
||||||
|
assert.deepStrictEqual(rep.missing, []);
|
||||||
|
assert.deepStrictEqual(rep.extra,
|
||||||
|
[{ path: 'MF/7F10/6F3C', role: 'EF.SMS', tree: 'MF', features: ['Short Message Storage (SMS)'] }]);
|
||||||
|
// extra is sorted by path, and a path without a mapped feature is ignored
|
||||||
|
const extra = fsReport({ MF: ['Service Provider Name (SPN)'] },
|
||||||
|
['MF/7F20/6F45', 'MF/7F10/6F44', 'MF/7F10/6F3C', 'MF/7F20/6F46', 'MF/2F06']).extra;
|
||||||
|
assert.deepStrictEqual(extra.map(e => e.path), ['MF/7F10/6F3C', 'MF/7F10/6F44', 'MF/7F20/6F45']);
|
||||||
|
assert.deepStrictEqual(extra[2].features, ['Cell Broadcast Message Identifier (CBMI)']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('servicesFsReport is deterministic and order-stable', () => {
|
||||||
|
const present = ['ADF.USIM/6F3B'];
|
||||||
|
const a = fsReport({ 'ADF.USIM': ['Service Provider Name (SPN)', 'Fixed Dialling Numbers (FDN)'] }, present);
|
||||||
|
const b = fsReport({ 'ADF.USIM': ['Fixed Dialling Numbers (FDN)', 'Service Provider Name (SPN)'] }, present);
|
||||||
|
assert.deepStrictEqual(a, b, 'cross-reference order, not input order');
|
||||||
|
assert.deepStrictEqual(a.missing, [
|
||||||
|
{ feature: 'Fixed Dialling Numbers (FDN)', tree: 'ADF.USIM', paths: ['ADF.USIM/6F4B'] },
|
||||||
|
{ feature: 'Service Provider Name (SPN)', tree: 'ADF.USIM', paths: ['ADF.USIM/6F46'] },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('servicesFsReportHtml renders the paths and the notes', () => {
|
||||||
|
const note = 'path-exact';
|
||||||
|
const empty = servicesFsReportHtml(fsReport({ MF: [] }, []));
|
||||||
|
assert.ok(empty.includes('The filesystem matches the declared services'), empty);
|
||||||
|
assert.ok(empty.includes(note), empty);
|
||||||
|
assert.ok(servicesFsReportHtml(null).includes('The filesystem matches the declared services'));
|
||||||
|
const rep = fsReport({ MF: ['Fixed Dialling Numbers (FDN)'] }, ['MF/7F10/6F3C']);
|
||||||
|
const html = servicesFsReportHtml(rep);
|
||||||
|
assert.ok(html.includes('Declared services whose files are missing'), html);
|
||||||
|
assert.ok(html.includes('Fixed Dialling Numbers (FDN)'), html);
|
||||||
|
assert.ok(html.includes('MF/7F10/6F4B'), 'the missing path is shown');
|
||||||
|
assert.ok(html.includes('Present files whose service is not declared'), html);
|
||||||
|
assert.ok(html.includes('MF/7F10/6F3C'), 'the extra path is shown');
|
||||||
|
assert.ok(html.includes('EF.SMS'), 'the extra path names its role');
|
||||||
|
assert.ok(html.includes('Short Message Storage (SMS)'), html);
|
||||||
|
assert.ok(html.includes(note), html);
|
||||||
|
assert.ok(!html.includes('The filesystem matches the declared services'), html);
|
||||||
|
// a single-sided report renders only its own heading
|
||||||
|
const one = servicesFsReportHtml(fsReport({ MF: ['Fixed Dialling Numbers (FDN)'] }, ['MF/7F10/6F3B']));
|
||||||
|
assert.ok(one.includes('Declared services whose files are missing'), one);
|
||||||
|
assert.ok(!one.includes('Present files whose service is not declared'), one);
|
||||||
|
assert.ok(!one.includes('Declared services that cannot be checked'), one);
|
||||||
|
// the unchecked bucket names the roles and explains itself
|
||||||
|
const unc = servicesFsReportHtml(fsReport({ 'ADF.USIM': ['Image (IMG)'] }, []));
|
||||||
|
assert.ok(unc.includes('Declared services that cannot be checked'), unc);
|
||||||
|
assert.ok(unc.includes('Image (IMG)') && unc.includes('DF.GRAPHICS') && unc.includes('EF.IMG'), unc);
|
||||||
|
assert.ok(unc.includes('cannot be verified'), unc);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---- filesystem check: the wiring ----
|
||||||
|
|
||||||
|
test('the Services pill carries the Check filesystem action and its report panel', () => {
|
||||||
|
const start = html.indexOf('id="pysim-sub-services"');
|
||||||
|
const pill = html.slice(start, html.indexOf('id="tab-phone"', start));
|
||||||
|
assert.match(pill, /id="pysim-services-fs-btn"[^>]*data-needs="card"[^>]*onclick="pysimServicesFsCheck\(\)"/);
|
||||||
|
assert.match(pill, /data-l10n="Check filesystem"/);
|
||||||
|
assert.match(pill, /id="pysim-services-fs-status"/);
|
||||||
|
assert.match(pill, /id="pysim-services-fs-spin"[^>]*class="hidden animate-spin/);
|
||||||
|
assert.match(pill, /id="pysim-services-fs-report"/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('the check probes the tree once per loaded session and keeps the report across renders', () => {
|
||||||
|
// /api/tree lists files without testing them: only a completed probe sets
|
||||||
|
// the presence flag, and a refresh (card change) clears it
|
||||||
|
assert.match(html, /_pysimFsProbed = !probe\.stop && !error;/);
|
||||||
|
assert.match(html, /async function pysimFsRefresh\(\) \{[\s\S]{0,200}_pysimFsProbed = false;/);
|
||||||
|
// the check loads the tree if missing, probes when unprobed, and renders
|
||||||
|
// the pure report from the per-tree declarations, the standard list's path
|
||||||
|
// index and the tree's present paths
|
||||||
|
const fn = extractFunc(html, 'pysimServicesFsCheck');
|
||||||
|
assert.ok(fn.indexOf('await pysimFsRefresh()') > 0, fn);
|
||||||
|
assert.ok(fn.indexOf('await pysimFsProbeAll()') > 0, fn);
|
||||||
|
// a throbber shows while the scan runs and hides when it settles
|
||||||
|
assert.ok(fn.indexOf('pysim-services-fs-spin') > 0, fn);
|
||||||
|
assert.ok(fn.indexOf("spinEl.classList.remove('hidden')") > 0, fn);
|
||||||
|
assert.ok(fn.indexOf("spinEl.classList.add('hidden')") > 0, fn);
|
||||||
|
assert.ok(fn.indexOf('servicesFsDeclaredTrees(cache.files)') > 0, fn);
|
||||||
|
assert.ok(fn.indexOf('await servicesPathIndex()') > 0, fn);
|
||||||
|
assert.ok(fn.indexOf('index.size') > 0, 'an unloadable list is an error, not a wall of unchecked rows');
|
||||||
|
assert.ok(fn.indexOf('servicesFsPresentPaths(pysimFsTreeRoot)') > 0, fn);
|
||||||
|
assert.ok(fn.indexOf('servicesFsReport(') > 0, fn);
|
||||||
|
// the path index is loaded once from the standard file list
|
||||||
|
const loader = extractFunc(html, 'servicesPathIndex');
|
||||||
|
assert.ok(loader.indexOf('uiccStdFilesLoad()') > 0, loader);
|
||||||
|
assert.ok(loader.indexOf('servicesPathIndexBuild(') > 0, loader);
|
||||||
|
// the grid's render redraws the kept report (language switch)
|
||||||
|
const render = extractFunc(html, 'pysimServicesRender');
|
||||||
|
assert.ok(render.indexOf('pysimServicesFsRender()') > 0, render);
|
||||||
|
// and a card-session change drops it with the cache
|
||||||
|
assert.match(html, /function pysimServicesReset\(\) \{\s*_pysimServicesCache = null;\s*_pysimServicesFsReport = null;/);
|
||||||
|
});
|
||||||
+297
-1
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"generated": "2026-09-23",
|
"generated": "2026-10-25",
|
||||||
"files": [
|
"files": [
|
||||||
{
|
{
|
||||||
"path": "MF/2F00",
|
"path": "MF/2F00",
|
||||||
@@ -161,6 +161,30 @@
|
|||||||
"kind": "ef",
|
"kind": "ef",
|
||||||
"root": "MF"
|
"root": "MF"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"path": "MF/7F10/5F3F",
|
||||||
|
"name": "DF.A2X",
|
||||||
|
"sym": "MF/DF.TELECOM/DF.A2X",
|
||||||
|
"fid": "5F3F",
|
||||||
|
"kind": "df",
|
||||||
|
"root": "MF"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "MF/7F10/5F3F/4F01",
|
||||||
|
"name": "EF.AST",
|
||||||
|
"sym": "MF/DF.TELECOM/DF.A2X/EF.AST",
|
||||||
|
"fid": "4F01",
|
||||||
|
"kind": "ef",
|
||||||
|
"root": "MF"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "MF/7F10/5F3F/4F02",
|
||||||
|
"name": "EF.A2X_CONFIG",
|
||||||
|
"sym": "MF/DF.TELECOM/DF.A2X/EF.A2X_CONFIG",
|
||||||
|
"fid": "4F02",
|
||||||
|
"kind": "ef",
|
||||||
|
"root": "MF"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"path": "MF/7F10/6F3A",
|
"path": "MF/7F10/6F3A",
|
||||||
"name": "EF.ADN",
|
"name": "EF.ADN",
|
||||||
@@ -1030,6 +1054,46 @@
|
|||||||
"kind": "ef",
|
"kind": "ef",
|
||||||
"root": "ADF.USIM"
|
"root": "ADF.USIM"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"path": "ADF.USIM/5F3C",
|
||||||
|
"name": "DF.MExE",
|
||||||
|
"sym": "ADF.USIM/DF.MExE",
|
||||||
|
"fid": "5F3C",
|
||||||
|
"kind": "df",
|
||||||
|
"root": "ADF.USIM"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "ADF.USIM/5F3C/4F40",
|
||||||
|
"name": "EF.MExE-ST",
|
||||||
|
"sym": "ADF.USIM/DF.MExE/EF.MExE-ST",
|
||||||
|
"fid": "4F40",
|
||||||
|
"kind": "ef",
|
||||||
|
"root": "ADF.USIM"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "ADF.USIM/5F3C/4F41",
|
||||||
|
"name": "EF.ORPK",
|
||||||
|
"sym": "ADF.USIM/DF.MExE/EF.ORPK",
|
||||||
|
"fid": "4F41",
|
||||||
|
"kind": "ef",
|
||||||
|
"root": "ADF.USIM"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "ADF.USIM/5F3C/4F42",
|
||||||
|
"name": "EF.ARPK",
|
||||||
|
"sym": "ADF.USIM/DF.MExE/EF.ARPK",
|
||||||
|
"fid": "4F42",
|
||||||
|
"kind": "ef",
|
||||||
|
"root": "ADF.USIM"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "ADF.USIM/5F3C/4F43",
|
||||||
|
"name": "EF.TPRPK",
|
||||||
|
"sym": "ADF.USIM/DF.MExE/EF.TPRPK",
|
||||||
|
"fid": "4F43",
|
||||||
|
"kind": "ef",
|
||||||
|
"root": "ADF.USIM"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"path": "ADF.USIM/5F40",
|
"path": "ADF.USIM/5F40",
|
||||||
"name": "DF.WLAN",
|
"name": "DF.WLAN",
|
||||||
@@ -1182,6 +1246,38 @@
|
|||||||
"kind": "ef",
|
"kind": "ef",
|
||||||
"root": "ADF.USIM"
|
"root": "ADF.USIM"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"path": "ADF.USIM/5F70",
|
||||||
|
"name": "DF.SoLSA",
|
||||||
|
"sym": "ADF.USIM/DF.SoLSA",
|
||||||
|
"fid": "5F70",
|
||||||
|
"kind": "df",
|
||||||
|
"root": "ADF.USIM"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "ADF.USIM/5F70/4F30",
|
||||||
|
"name": "EF.SAI",
|
||||||
|
"sym": "ADF.USIM/DF.SoLSA/EF.SAI",
|
||||||
|
"fid": "4F30",
|
||||||
|
"kind": "ef",
|
||||||
|
"root": "ADF.USIM"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "ADF.USIM/5F70/4F31",
|
||||||
|
"name": "EF.SLL",
|
||||||
|
"sym": "ADF.USIM/DF.SoLSA/EF.SLL",
|
||||||
|
"fid": "4F31",
|
||||||
|
"kind": "ef",
|
||||||
|
"root": "ADF.USIM"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "ADF.USIM/5F80",
|
||||||
|
"name": "DF.BCAST",
|
||||||
|
"sym": "ADF.USIM/DF.BCAST",
|
||||||
|
"fid": "5F80",
|
||||||
|
"kind": "df",
|
||||||
|
"root": "ADF.USIM"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"path": "ADF.USIM/5F90",
|
"path": "ADF.USIM/5F90",
|
||||||
"name": "DF.ProSe",
|
"name": "DF.ProSe",
|
||||||
@@ -1302,6 +1398,30 @@
|
|||||||
"kind": "ef",
|
"kind": "ef",
|
||||||
"root": "ADF.USIM"
|
"root": "ADF.USIM"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"path": "ADF.USIM/5FA0",
|
||||||
|
"name": "DF.ACDC",
|
||||||
|
"sym": "ADF.USIM/DF.ACDC",
|
||||||
|
"fid": "5FA0",
|
||||||
|
"kind": "df",
|
||||||
|
"root": "ADF.USIM"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "ADF.USIM/5FA0/4F01",
|
||||||
|
"name": "EF.ACDC_LIST",
|
||||||
|
"sym": "ADF.USIM/DF.ACDC/EF.ACDC_LIST",
|
||||||
|
"fid": "4F01",
|
||||||
|
"kind": "ef",
|
||||||
|
"root": "ADF.USIM"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "ADF.USIM/5FB0",
|
||||||
|
"name": "DF.TV",
|
||||||
|
"sym": "ADF.USIM/DF.TV",
|
||||||
|
"fid": "5FB0",
|
||||||
|
"kind": "df",
|
||||||
|
"root": "ADF.USIM"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"path": "ADF.USIM/5FC0",
|
"path": "ADF.USIM/5FC0",
|
||||||
"name": "DF.5GS",
|
"name": "DF.5GS",
|
||||||
@@ -2262,6 +2382,14 @@
|
|||||||
"kind": "ef",
|
"kind": "ef",
|
||||||
"root": "ADF.USIM"
|
"root": "ADF.USIM"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"path": "ADF.USIM/6FDC",
|
||||||
|
"name": "EF.LRPLMNSI",
|
||||||
|
"sym": "ADF.USIM/EF.LRPLMNSI",
|
||||||
|
"fid": "6FDC",
|
||||||
|
"kind": "ef",
|
||||||
|
"root": "ADF.USIM"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"path": "ADF.USIM/6FDD",
|
"path": "ADF.USIM/6FDD",
|
||||||
"name": "EF.NAFKCA",
|
"name": "EF.NAFKCA",
|
||||||
@@ -2358,6 +2486,14 @@
|
|||||||
"kind": "ef",
|
"kind": "ef",
|
||||||
"root": "ADF.USIM"
|
"root": "ADF.USIM"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"path": "ADF.USIM/6FF0",
|
||||||
|
"name": "EF.IAL",
|
||||||
|
"sym": "ADF.USIM/EF.IAL",
|
||||||
|
"fid": "6FF0",
|
||||||
|
"kind": "ef",
|
||||||
|
"root": "ADF.USIM"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"path": "ADF.USIM/6FF1",
|
"path": "ADF.USIM/6FF1",
|
||||||
"name": "EF.IPS",
|
"name": "EF.IPS",
|
||||||
@@ -2366,6 +2502,14 @@
|
|||||||
"kind": "ef",
|
"kind": "ef",
|
||||||
"root": "ADF.USIM"
|
"root": "ADF.USIM"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"path": "ADF.USIM/6FF2",
|
||||||
|
"name": "EF.IPD",
|
||||||
|
"sym": "ADF.USIM/EF.IPD",
|
||||||
|
"fid": "6FF2",
|
||||||
|
"kind": "ef",
|
||||||
|
"root": "ADF.USIM"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"path": "ADF.USIM/6FF3",
|
"path": "ADF.USIM/6FF3",
|
||||||
"name": "EF.ePDGId",
|
"name": "EF.ePDGId",
|
||||||
@@ -2414,6 +2558,22 @@
|
|||||||
"kind": "ef",
|
"kind": "ef",
|
||||||
"root": "ADF.USIM"
|
"root": "ADF.USIM"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"path": "ADF.USIM/6FF9",
|
||||||
|
"name": "EF.3GPPPSDATAOFF",
|
||||||
|
"sym": "ADF.USIM/EF.3GPPPSDATAOFF",
|
||||||
|
"fid": "6FF9",
|
||||||
|
"kind": "ef",
|
||||||
|
"root": "ADF.USIM"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "ADF.USIM/6FFB",
|
||||||
|
"name": "EF.TVCONFIG",
|
||||||
|
"sym": "ADF.USIM/EF.TVCONFIG",
|
||||||
|
"fid": "6FFB",
|
||||||
|
"kind": "ef",
|
||||||
|
"root": "ADF.USIM"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"path": "ADF.USIM/6FFC",
|
"path": "ADF.USIM/6FFC",
|
||||||
"name": "EF.XCAPConfigData",
|
"name": "EF.XCAPConfigData",
|
||||||
@@ -2527,6 +2687,46 @@
|
|||||||
"kind": "ef",
|
"kind": "ef",
|
||||||
"root": "ADF.USIM-NON-IMSI"
|
"root": "ADF.USIM-NON-IMSI"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"path": "ADF.USIM-NON-IMSI/5F3C",
|
||||||
|
"name": "DF.MExE",
|
||||||
|
"sym": "ADF.USIM-non-IMSI/DF.MExE",
|
||||||
|
"fid": "5F3C",
|
||||||
|
"kind": "df",
|
||||||
|
"root": "ADF.USIM-NON-IMSI"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "ADF.USIM-NON-IMSI/5F3C/4F40",
|
||||||
|
"name": "EF.MExE-ST",
|
||||||
|
"sym": "ADF.USIM-non-IMSI/DF.MExE/EF.MExE-ST",
|
||||||
|
"fid": "4F40",
|
||||||
|
"kind": "ef",
|
||||||
|
"root": "ADF.USIM-NON-IMSI"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "ADF.USIM-NON-IMSI/5F3C/4F41",
|
||||||
|
"name": "EF.ORPK",
|
||||||
|
"sym": "ADF.USIM-non-IMSI/DF.MExE/EF.ORPK",
|
||||||
|
"fid": "4F41",
|
||||||
|
"kind": "ef",
|
||||||
|
"root": "ADF.USIM-NON-IMSI"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "ADF.USIM-NON-IMSI/5F3C/4F42",
|
||||||
|
"name": "EF.ARPK",
|
||||||
|
"sym": "ADF.USIM-non-IMSI/DF.MExE/EF.ARPK",
|
||||||
|
"fid": "4F42",
|
||||||
|
"kind": "ef",
|
||||||
|
"root": "ADF.USIM-NON-IMSI"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "ADF.USIM-NON-IMSI/5F3C/4F43",
|
||||||
|
"name": "EF.TPRPK",
|
||||||
|
"sym": "ADF.USIM-non-IMSI/DF.MExE/EF.TPRPK",
|
||||||
|
"fid": "4F43",
|
||||||
|
"kind": "ef",
|
||||||
|
"root": "ADF.USIM-NON-IMSI"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"path": "ADF.USIM-NON-IMSI/5F40",
|
"path": "ADF.USIM-NON-IMSI/5F40",
|
||||||
"name": "DF.WLAN",
|
"name": "DF.WLAN",
|
||||||
@@ -2679,6 +2879,38 @@
|
|||||||
"kind": "ef",
|
"kind": "ef",
|
||||||
"root": "ADF.USIM-NON-IMSI"
|
"root": "ADF.USIM-NON-IMSI"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"path": "ADF.USIM-NON-IMSI/5F70",
|
||||||
|
"name": "DF.SoLSA",
|
||||||
|
"sym": "ADF.USIM-non-IMSI/DF.SoLSA",
|
||||||
|
"fid": "5F70",
|
||||||
|
"kind": "df",
|
||||||
|
"root": "ADF.USIM-NON-IMSI"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "ADF.USIM-NON-IMSI/5F70/4F30",
|
||||||
|
"name": "EF.SAI",
|
||||||
|
"sym": "ADF.USIM-non-IMSI/DF.SoLSA/EF.SAI",
|
||||||
|
"fid": "4F30",
|
||||||
|
"kind": "ef",
|
||||||
|
"root": "ADF.USIM-NON-IMSI"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "ADF.USIM-NON-IMSI/5F70/4F31",
|
||||||
|
"name": "EF.SLL",
|
||||||
|
"sym": "ADF.USIM-non-IMSI/DF.SoLSA/EF.SLL",
|
||||||
|
"fid": "4F31",
|
||||||
|
"kind": "ef",
|
||||||
|
"root": "ADF.USIM-NON-IMSI"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "ADF.USIM-NON-IMSI/5F80",
|
||||||
|
"name": "DF.BCAST",
|
||||||
|
"sym": "ADF.USIM-non-IMSI/DF.BCAST",
|
||||||
|
"fid": "5F80",
|
||||||
|
"kind": "df",
|
||||||
|
"root": "ADF.USIM-NON-IMSI"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"path": "ADF.USIM-NON-IMSI/5F90",
|
"path": "ADF.USIM-NON-IMSI/5F90",
|
||||||
"name": "DF.ProSe",
|
"name": "DF.ProSe",
|
||||||
@@ -2799,6 +3031,30 @@
|
|||||||
"kind": "ef",
|
"kind": "ef",
|
||||||
"root": "ADF.USIM-NON-IMSI"
|
"root": "ADF.USIM-NON-IMSI"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"path": "ADF.USIM-NON-IMSI/5FA0",
|
||||||
|
"name": "DF.ACDC",
|
||||||
|
"sym": "ADF.USIM-non-IMSI/DF.ACDC",
|
||||||
|
"fid": "5FA0",
|
||||||
|
"kind": "df",
|
||||||
|
"root": "ADF.USIM-NON-IMSI"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "ADF.USIM-NON-IMSI/5FA0/4F01",
|
||||||
|
"name": "EF.ACDC_LIST",
|
||||||
|
"sym": "ADF.USIM-non-IMSI/DF.ACDC/EF.ACDC_LIST",
|
||||||
|
"fid": "4F01",
|
||||||
|
"kind": "ef",
|
||||||
|
"root": "ADF.USIM-NON-IMSI"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "ADF.USIM-NON-IMSI/5FB0",
|
||||||
|
"name": "DF.TV",
|
||||||
|
"sym": "ADF.USIM-non-IMSI/DF.TV",
|
||||||
|
"fid": "5FB0",
|
||||||
|
"kind": "df",
|
||||||
|
"root": "ADF.USIM-NON-IMSI"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"path": "ADF.USIM-NON-IMSI/5FC0",
|
"path": "ADF.USIM-NON-IMSI/5FC0",
|
||||||
"name": "DF.5GS",
|
"name": "DF.5GS",
|
||||||
@@ -3751,6 +4007,14 @@
|
|||||||
"kind": "ef",
|
"kind": "ef",
|
||||||
"root": "ADF.USIM-NON-IMSI"
|
"root": "ADF.USIM-NON-IMSI"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"path": "ADF.USIM-NON-IMSI/6FDC",
|
||||||
|
"name": "EF.LRPLMNSI",
|
||||||
|
"sym": "ADF.USIM-non-IMSI/EF.LRPLMNSI",
|
||||||
|
"fid": "6FDC",
|
||||||
|
"kind": "ef",
|
||||||
|
"root": "ADF.USIM-NON-IMSI"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"path": "ADF.USIM-NON-IMSI/6FDD",
|
"path": "ADF.USIM-NON-IMSI/6FDD",
|
||||||
"name": "EF.NAFKCA",
|
"name": "EF.NAFKCA",
|
||||||
@@ -3847,6 +4111,14 @@
|
|||||||
"kind": "ef",
|
"kind": "ef",
|
||||||
"root": "ADF.USIM-NON-IMSI"
|
"root": "ADF.USIM-NON-IMSI"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"path": "ADF.USIM-NON-IMSI/6FF0",
|
||||||
|
"name": "EF.IAL",
|
||||||
|
"sym": "ADF.USIM-non-IMSI/EF.IAL",
|
||||||
|
"fid": "6FF0",
|
||||||
|
"kind": "ef",
|
||||||
|
"root": "ADF.USIM-NON-IMSI"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"path": "ADF.USIM-NON-IMSI/6FF1",
|
"path": "ADF.USIM-NON-IMSI/6FF1",
|
||||||
"name": "EF.IPS",
|
"name": "EF.IPS",
|
||||||
@@ -3855,6 +4127,14 @@
|
|||||||
"kind": "ef",
|
"kind": "ef",
|
||||||
"root": "ADF.USIM-NON-IMSI"
|
"root": "ADF.USIM-NON-IMSI"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"path": "ADF.USIM-NON-IMSI/6FF2",
|
||||||
|
"name": "EF.IPD",
|
||||||
|
"sym": "ADF.USIM-non-IMSI/EF.IPD",
|
||||||
|
"fid": "6FF2",
|
||||||
|
"kind": "ef",
|
||||||
|
"root": "ADF.USIM-NON-IMSI"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"path": "ADF.USIM-NON-IMSI/6FF3",
|
"path": "ADF.USIM-NON-IMSI/6FF3",
|
||||||
"name": "EF.ePDGId",
|
"name": "EF.ePDGId",
|
||||||
@@ -3903,6 +4183,22 @@
|
|||||||
"kind": "ef",
|
"kind": "ef",
|
||||||
"root": "ADF.USIM-NON-IMSI"
|
"root": "ADF.USIM-NON-IMSI"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"path": "ADF.USIM-NON-IMSI/6FF9",
|
||||||
|
"name": "EF.3GPPPSDATAOFF",
|
||||||
|
"sym": "ADF.USIM-non-IMSI/EF.3GPPPSDATAOFF",
|
||||||
|
"fid": "6FF9",
|
||||||
|
"kind": "ef",
|
||||||
|
"root": "ADF.USIM-NON-IMSI"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "ADF.USIM-NON-IMSI/6FFB",
|
||||||
|
"name": "EF.TVCONFIG",
|
||||||
|
"sym": "ADF.USIM-non-IMSI/EF.TVCONFIG",
|
||||||
|
"fid": "6FFB",
|
||||||
|
"kind": "ef",
|
||||||
|
"root": "ADF.USIM-NON-IMSI"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"path": "ADF.USIM-NON-IMSI/6FFC",
|
"path": "ADF.USIM-NON-IMSI/6FFC",
|
||||||
"name": "EF.XCAPConfigData",
|
"name": "EF.XCAPConfigData",
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "pysim-simple-server"
|
name = "pysim-simple-server"
|
||||||
version = "3.38.1"
|
version = "3.40.3"
|
||||||
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"
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ from osmocom.tlv import BER_TLV_IE
|
|||||||
from osmocom.utils import rpad
|
from osmocom.utils import rpad
|
||||||
|
|
||||||
|
|
||||||
VERSION = '3.38.1'
|
VERSION = '3.40.3'
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,11 @@ classes - the same model pySim assembles at equip time:
|
|||||||
It ships as ``frontend/uicc_files.json`` so the SIM/USIM RFM SELECT picker has
|
It ships as ``frontend/uicc_files.json`` so the SIM/USIM RFM SELECT picker has
|
||||||
a file list offline (no card, no server); when a card is equipped the picker
|
a file list offline (no card, no server); when a card is equipped the picker
|
||||||
merges the live tree over it and hides probed-absent files.
|
merges the live tree over it and hides probed-absent files.
|
||||||
|
The optional / service-gated files pySim's model does not carry (the SoLSA,
|
||||||
|
MExE, BCAST, ACDC, TV and A2X DFs, EF.LRPLMNSI, EF.IAL, EF.IPD, EF.TVCONFIG,
|
||||||
|
EF.3GPPPSDATAOFF) are appended from ``SUPPLEMENT`` with their spec locations -
|
||||||
|
the decoded-editing filesystem check resolves a file by its (name, root), so a
|
||||||
|
same-FID file under another DF must stay a distinct entry.
|
||||||
``tests/test_uicc_files.py`` regenerates the list and fails when pySim adds or
|
``tests/test_uicc_files.py`` regenerates the list and fails when pySim adds or
|
||||||
renames files, so the asset cannot silently drift.
|
renames files, so the asset cannot silently drift.
|
||||||
|
|
||||||
@@ -55,6 +60,62 @@ def _entry(fid_path, sym_path, name, fid, kind, root, aid=None):
|
|||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
# Files the pySim model does not carry (optional / service-gated), with their
|
||||||
|
# spec locations: TS 31.102 4.2.86 (EF.LRPLMNSI), 4.2.100 (EF.IAL), 4.2.102
|
||||||
|
# (EF.IPD), 4.2.108 (EF.TVCONFIG), 4.2.109 (EF.3GPPPSDATAOFF), 4.4.1
|
||||||
|
# (DF.SoLSA), 4.4.4 (DF.MExE), 4.4.7 (DF.BCAST - its files are OMA BCAST, so
|
||||||
|
# the DF is listed alone), 4.4.9 (DF.ACDC), 4.4.10 (DF.TV - EF.TVUSD is a
|
||||||
|
# variable 4Fxx, omitted) and 4.6.6 (DF.A2X); the FID map is
|
||||||
|
# docs/uicc/UICC_FILES.md 4.13. Entries are (root, parent FID chain, fid,
|
||||||
|
# name); a DF is a 'DF.' name and precedes its children so the symbolic chain
|
||||||
|
# can be resolved.
|
||||||
|
SUPPLEMENT = (
|
||||||
|
('ADF.USIM', (), '6FDC', 'EF.LRPLMNSI'),
|
||||||
|
('ADF.USIM', (), '6FF0', 'EF.IAL'),
|
||||||
|
('ADF.USIM', (), '6FF2', 'EF.IPD'),
|
||||||
|
('ADF.USIM', (), '6FF9', 'EF.3GPPPSDATAOFF'),
|
||||||
|
('ADF.USIM', (), '6FFB', 'EF.TVCONFIG'),
|
||||||
|
('ADF.USIM', (), '5F3C', 'DF.MExE'),
|
||||||
|
('ADF.USIM', ('5F3C',), '4F40', 'EF.MExE-ST'),
|
||||||
|
('ADF.USIM', ('5F3C',), '4F41', 'EF.ORPK'),
|
||||||
|
('ADF.USIM', ('5F3C',), '4F42', 'EF.ARPK'),
|
||||||
|
('ADF.USIM', ('5F3C',), '4F43', 'EF.TPRPK'),
|
||||||
|
('ADF.USIM', (), '5F70', 'DF.SoLSA'),
|
||||||
|
('ADF.USIM', ('5F70',), '4F30', 'EF.SAI'),
|
||||||
|
('ADF.USIM', ('5F70',), '4F31', 'EF.SLL'),
|
||||||
|
('ADF.USIM', (), '5F80', 'DF.BCAST'),
|
||||||
|
('ADF.USIM', (), '5FA0', 'DF.ACDC'),
|
||||||
|
('ADF.USIM', ('5FA0',), '4F01', 'EF.ACDC_LIST'),
|
||||||
|
('ADF.USIM', (), '5FB0', 'DF.TV'),
|
||||||
|
('MF', ('7F10',), '5F3F', 'DF.A2X'),
|
||||||
|
('MF', ('7F10', '5F3F'), '4F01', 'EF.AST'),
|
||||||
|
('MF', ('7F10', '5F3F'), '4F02', 'EF.A2X_CONFIG'),
|
||||||
|
)
|
||||||
|
|
||||||
|
# The non-IMSI USIM application repeats the ADF.USIM files under its own root
|
||||||
|
# (the walk emits it as a separate ADF with the same file set).
|
||||||
|
USIM_ROOT = 'ADF.USIM'
|
||||||
|
USIM_MIRROR = 'ADF.USIM-NON-IMSI'
|
||||||
|
|
||||||
|
|
||||||
|
def _add_supplement(out, seen, sym_by_path):
|
||||||
|
"""Append SUPPLEMENT, mirroring the ADF.USIM files under the non-IMSI root."""
|
||||||
|
for root, parents, fid, name in SUPPLEMENT:
|
||||||
|
roots = [root]
|
||||||
|
if root == USIM_ROOT and USIM_MIRROR in seen:
|
||||||
|
roots.append(USIM_MIRROR)
|
||||||
|
for target in roots:
|
||||||
|
key = '/'.join((target,) + tuple(parents) + (fid,)).upper()
|
||||||
|
if key in seen:
|
||||||
|
continue
|
||||||
|
parent_key = '/'.join((target,) + tuple(parents)).upper()
|
||||||
|
sym = '%s/%s' % (sym_by_path.get(parent_key, parent_key), name)
|
||||||
|
seen.add(key)
|
||||||
|
out.append(_entry(key, sym, name, fid,
|
||||||
|
'df' if name.startswith('DF.') else 'ef', target))
|
||||||
|
sym_by_path[key] = sym
|
||||||
|
|
||||||
|
|
||||||
def _walk(node, fid_path, sym_path, root, out, seen):
|
def _walk(node, fid_path, sym_path, root, out, seen):
|
||||||
"""Depth-first walk of a DF/ADF subtree (children is a fid -> CardFile dict)."""
|
"""Depth-first walk of a DF/ADF subtree (children is a fid -> CardFile dict)."""
|
||||||
for fid, child in sorted(node.children.items()):
|
for fid, child in sorted(node.children.items()):
|
||||||
@@ -116,6 +177,8 @@ def build():
|
|||||||
seen.add(key)
|
seen.add(key)
|
||||||
out.append(_entry(key, adf.name, adf.name, '', 'adf', adf.name, app.aid))
|
out.append(_entry(key, adf.name, adf.name, '', 'adf', adf.name, app.aid))
|
||||||
_walk(adf, key, adf.name, adf.name, out, seen)
|
_walk(adf, key, adf.name, adf.name, out, seen)
|
||||||
|
# The optional / service-gated files pySim's model does not carry.
|
||||||
|
_add_supplement(out, seen, {e['path']: e['sym'] for e in out})
|
||||||
# Stable order: MF first, then the ADFs alphabetically; DFs before their EFs
|
# Stable order: MF first, then the ADFs alphabetically; DFs before their EFs
|
||||||
# is implicit in the walk, paths sort consistently inside a root.
|
# is implicit in the walk, paths sort consistently inside a root.
|
||||||
out.sort(key=lambda e: (0 if e['root'] == 'MF' else 1, e['root'], e['path']))
|
out.sort(key=lambda e: (0 if e['root'] == 'MF' else 1, e['root'], e['path']))
|
||||||
|
|||||||
@@ -12,15 +12,41 @@ set VENV_DIR=%~dp0.venv
|
|||||||
set LOG=%~dp0simple_lastrun.log
|
set LOG=%~dp0simple_lastrun.log
|
||||||
set PYTHONUNBUFFERED=1
|
set PYTHONUNBUFFERED=1
|
||||||
|
|
||||||
|
REM The address the opening line names (v3.38.2). The server parses the same
|
||||||
|
REM flags; this mirrors its last-one-wins rule for the echo. %* is not
|
||||||
|
REM affected by SHIFT, so the server still receives every argument.
|
||||||
|
set "HTTP_HOST=127.0.0.1"
|
||||||
|
set "HTTP_PORT=8080"
|
||||||
|
:parse_http_args
|
||||||
|
if "%~1"=="" goto http_args_done
|
||||||
|
if /i "%~1"=="--http-host" (
|
||||||
|
set "HTTP_HOST=%~2"
|
||||||
|
shift
|
||||||
|
shift
|
||||||
|
goto parse_http_args
|
||||||
|
)
|
||||||
|
if /i "%~1"=="--http-port" (
|
||||||
|
set "HTTP_PORT=%~2"
|
||||||
|
shift
|
||||||
|
shift
|
||||||
|
goto parse_http_args
|
||||||
|
)
|
||||||
|
set "ARG=%~1"
|
||||||
|
if /i "%ARG:~0,12%"=="--http-host=" set "HTTP_HOST=%ARG:~12%"
|
||||||
|
if /i "%ARG:~0,12%"=="--http-port=" set "HTTP_PORT=%ARG:~12%"
|
||||||
|
shift
|
||||||
|
goto parse_http_args
|
||||||
|
:http_args_done
|
||||||
|
|
||||||
if exist "%VENV_DIR%\Scripts\pysim-simple-server.exe" (
|
if exist "%VENV_DIR%\Scripts\pysim-simple-server.exe" (
|
||||||
echo Starting pysim-simple-server from venv on http://127.0.0.1:8080
|
echo Starting pysim-simple-server from venv on http://%HTTP_HOST%:%HTTP_PORT%
|
||||||
echo Log: simple_lastrun.log (overwritten on each start^). Press Ctrl+C to stop.
|
echo Log: simple_lastrun.log (overwritten on each start^). Press Ctrl+C to stop.
|
||||||
"%VENV_DIR%\Scripts\pysim-simple-server.exe" --http-port 8080 -p 0 %* 2>&1 | powershell -NoProfile -Command "$input | Tee-Object -FilePath '%LOG%'"
|
"%VENV_DIR%\Scripts\pysim-simple-server.exe" --http-port 8080 -p 0 %* 2>&1 | powershell -NoProfile -Command "$input | Tee-Object -FilePath '%LOG%'"
|
||||||
goto :eof
|
goto :eof
|
||||||
)
|
)
|
||||||
|
|
||||||
if exist "%~dp0pysim_simple_server\__main__.py" (
|
if exist "%~dp0pysim_simple_server\__main__.py" (
|
||||||
echo Starting pysim-simple-server from source on http://127.0.0.1:8080
|
echo Starting pysim-simple-server from source on http://%HTTP_HOST%:%HTTP_PORT%
|
||||||
echo Log: simple_lastrun.log (overwritten on each start^). Press Ctrl+C to stop.
|
echo Log: simple_lastrun.log (overwritten on each start^). Press Ctrl+C to stop.
|
||||||
python -m pysim_simple_server --http-port 8080 -p 0 %* 2>&1 | powershell -NoProfile -Command "$input | Tee-Object -FilePath '%LOG%'"
|
python -m pysim_simple_server --http-port 8080 -p 0 %* 2>&1 | powershell -NoProfile -Command "$input | Tee-Object -FilePath '%LOG%'"
|
||||||
goto :eof
|
goto :eof
|
||||||
@@ -28,7 +54,7 @@ if exist "%~dp0pysim_simple_server\__main__.py" (
|
|||||||
|
|
||||||
where pysim-simple-server >nul 2>&1
|
where pysim-simple-server >nul 2>&1
|
||||||
if %errorlevel% equ 0 (
|
if %errorlevel% equ 0 (
|
||||||
echo Starting pysim-simple-server on http://127.0.0.1:8080
|
echo Starting pysim-simple-server on http://%HTTP_HOST%:%HTTP_PORT%
|
||||||
echo Log: simple_lastrun.log (overwritten on each start^). Press Ctrl+C to stop.
|
echo Log: simple_lastrun.log (overwritten on each start^). Press Ctrl+C to stop.
|
||||||
pysim-simple-server --http-port 8080 -p 0 %* 2>&1 | powershell -NoProfile -Command "$input | Tee-Object -FilePath '%LOG%'"
|
pysim-simple-server --http-port 8080 -p 0 %* 2>&1 | powershell -NoProfile -Command "$input | Tee-Object -FilePath '%LOG%'"
|
||||||
goto :eof
|
goto :eof
|
||||||
|
|||||||
@@ -10,6 +10,24 @@ set -e
|
|||||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
VENV_DIR="$SCRIPT_DIR/.venv"
|
VENV_DIR="$SCRIPT_DIR/.venv"
|
||||||
|
|
||||||
|
# The address the opening line names (v3.38.2). The server parses the same
|
||||||
|
# flags; start.sh passes its own --http-port 8080 before "$@", so a later user
|
||||||
|
# value wins (argparse keeps the last one) - mirror that here.
|
||||||
|
HTTP_HOST=127.0.0.1
|
||||||
|
HTTP_PORT=8080
|
||||||
|
PREV_ARG=""
|
||||||
|
for arg in "$@"; do
|
||||||
|
case "$PREV_ARG" in
|
||||||
|
--http-host) HTTP_HOST="$arg" ;;
|
||||||
|
--http-port) HTTP_PORT="$arg" ;;
|
||||||
|
esac
|
||||||
|
case "$arg" in
|
||||||
|
--http-host=*) HTTP_HOST="${arg#*=}" ;;
|
||||||
|
--http-port=*) HTTP_PORT="${arg#*=}" ;;
|
||||||
|
esac
|
||||||
|
PREV_ARG="$arg"
|
||||||
|
done
|
||||||
|
|
||||||
# Console + log file. The log contains card keys and request payloads (test
|
# Console + log file. The log contains card keys and request payloads (test
|
||||||
# cards) - it is gitignored and must not be committed.
|
# cards) - it is gitignored and must not be committed.
|
||||||
LOG_FILE="$SCRIPT_DIR/simple_lastrun.log"
|
LOG_FILE="$SCRIPT_DIR/simple_lastrun.log"
|
||||||
@@ -73,7 +91,7 @@ if [ -f "$VENV_DIR/bin/pysim-simple-server" ]; then
|
|||||||
elif command -v pysim-simple-server &> /dev/null; then
|
elif command -v pysim-simple-server &> /dev/null; then
|
||||||
SERVER="pysim-simple-server"
|
SERVER="pysim-simple-server"
|
||||||
elif [ -f "$SCRIPT_DIR/pysim_simple_server/__main__.py" ]; then
|
elif [ -f "$SCRIPT_DIR/pysim_simple_server/__main__.py" ]; then
|
||||||
echo "Starting pysim-simple-server from source on http://127.0.0.1:8080"
|
echo "Starting pysim-simple-server from source on http://$HTTP_HOST:$HTTP_PORT"
|
||||||
echo "Log: simple_lastrun.log (overwritten on each start)."
|
echo "Log: simple_lastrun.log (overwritten on each start)."
|
||||||
cd "$SCRIPT_DIR" && python3 -m pysim_simple_server --http-port 8080 $READER_ARGS "$@"
|
cd "$SCRIPT_DIR" && python3 -m pysim_simple_server --http-port 8080 $READER_ARGS "$@"
|
||||||
exit $?
|
exit $?
|
||||||
@@ -84,7 +102,7 @@ else
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "Starting pysim-simple-server on http://127.0.0.1:8080"
|
echo "Starting pysim-simple-server on http://$HTTP_HOST:$HTTP_PORT"
|
||||||
echo "Log: simple_lastrun.log (overwritten on each start)."
|
echo "Log: simple_lastrun.log (overwritten on each start)."
|
||||||
echo "Press Ctrl+C to stop."
|
echo "Press Ctrl+C to stop."
|
||||||
echo "Extra arguments are passed to the server (e.g. ./start.sh --gsmtap)."
|
echo "Extra arguments are passed to the server (e.g. ./start.sh --gsmtap)."
|
||||||
|
|||||||
@@ -150,6 +150,21 @@ class SourcePins(unittest.TestCase):
|
|||||||
# the old "is pcscd running" heuristic is gone
|
# the old "is pcscd running" heuristic is gone
|
||||||
self.assertNotIn('pgrep -x pcscd', src)
|
self.assertNotIn('pgrep -x pcscd', src)
|
||||||
|
|
||||||
|
def test_the_start_message_names_the_requested_address(self):
|
||||||
|
src = (Path(m.__file__).parent.parent / 'start.sh').read_text()
|
||||||
|
self.assertIn('--http-host=*) HTTP_HOST="${arg#*=}"', src)
|
||||||
|
self.assertIn('--http-port=*) HTTP_PORT="${arg#*=}"', src)
|
||||||
|
self.assertIn('http://$HTTP_HOST:$HTTP_PORT', src)
|
||||||
|
# the old hardcoded address is gone (the defaults are named separately)
|
||||||
|
self.assertNotIn('http://127.0.0.1:8080', src)
|
||||||
|
|
||||||
|
def test_the_start_bat_message_names_the_requested_address(self):
|
||||||
|
src = (Path(m.__file__).parent.parent / 'start.bat').read_text()
|
||||||
|
self.assertIn('--http-host=', src)
|
||||||
|
self.assertIn('--http-port=', src)
|
||||||
|
self.assertIn('%HTTP_HOST%:%HTTP_PORT%', src)
|
||||||
|
self.assertNotIn('http://127.0.0.1:8080', src)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
unittest.main()
|
unittest.main()
|
||||||
@@ -72,6 +72,60 @@ class TestUiccFilesAsset(unittest.TestCase):
|
|||||||
self.assertRegex(f['fid'], r'^[0-9A-F]{4}$')
|
self.assertRegex(f['fid'], r'^[0-9A-F]{4}$')
|
||||||
self.assertTrue(f['name'].startswith(('EF.', 'DF.')))
|
self.assertTrue(f['name'].startswith(('EF.', 'DF.')))
|
||||||
|
|
||||||
|
def test_optional_files_missing_from_the_pysim_model(self):
|
||||||
|
"""The service-gated files pySim does not model (the SUPPLEMENT)."""
|
||||||
|
files = _load()['files']
|
||||||
|
by_path = {f['path']: f for f in files}
|
||||||
|
expected = {
|
||||||
|
# TS 31.102 4.2.86 / 4.2.100 / 4.2.102 / 4.2.108 / 4.2.109
|
||||||
|
'ADF.USIM/6FDC': 'EF.LRPLMNSI',
|
||||||
|
'ADF.USIM/6FF0': 'EF.IAL',
|
||||||
|
'ADF.USIM/6FF2': 'EF.IPD',
|
||||||
|
'ADF.USIM/6FF9': 'EF.3GPPPSDATAOFF',
|
||||||
|
'ADF.USIM/6FFB': 'EF.TVCONFIG',
|
||||||
|
# 4.4.4 DF.MExE
|
||||||
|
'ADF.USIM/5F3C': 'DF.MExE',
|
||||||
|
'ADF.USIM/5F3C/4F40': 'EF.MExE-ST',
|
||||||
|
'ADF.USIM/5F3C/4F41': 'EF.ORPK',
|
||||||
|
'ADF.USIM/5F3C/4F42': 'EF.ARPK',
|
||||||
|
'ADF.USIM/5F3C/4F43': 'EF.TPRPK',
|
||||||
|
# 4.4.1 DF.SoLSA
|
||||||
|
'ADF.USIM/5F70': 'DF.SoLSA',
|
||||||
|
'ADF.USIM/5F70/4F30': 'EF.SAI',
|
||||||
|
'ADF.USIM/5F70/4F31': 'EF.SLL',
|
||||||
|
# 4.4.7 / 4.4.9 / 4.4.10
|
||||||
|
'ADF.USIM/5F80': 'DF.BCAST',
|
||||||
|
'ADF.USIM/5FA0': 'DF.ACDC',
|
||||||
|
'ADF.USIM/5FA0/4F01': 'EF.ACDC_LIST',
|
||||||
|
'ADF.USIM/5FB0': 'DF.TV',
|
||||||
|
# 4.6.6 DF.A2X (DF.TELECOM)
|
||||||
|
'MF/7F10/5F3F': 'DF.A2X',
|
||||||
|
'MF/7F10/5F3F/4F01': 'EF.AST',
|
||||||
|
'MF/7F10/5F3F/4F02': 'EF.A2X_CONFIG',
|
||||||
|
}
|
||||||
|
for path, name in expected.items():
|
||||||
|
self.assertIn(path, by_path)
|
||||||
|
self.assertEqual(by_path[path]['name'], name)
|
||||||
|
self.assertEqual(by_path[path]['root'], path.split('/')[0])
|
||||||
|
self.assertEqual(by_path[path]['kind'],
|
||||||
|
'df' if name.startswith('DF.') else 'ef')
|
||||||
|
# The same FID under another DF stays a distinct entry (name, path).
|
||||||
|
self.assertEqual(by_path['ADF.USIM/5F3A/4F30']['name'], 'EF.PBR')
|
||||||
|
self.assertEqual(by_path['MF/7F10/5F3A/4F30']['name'], 'EF.PBR')
|
||||||
|
# The ADF.USIM files are mirrored under the non-IMSI USIM root.
|
||||||
|
for path, name in expected.items():
|
||||||
|
if not path.startswith('ADF.USIM/'):
|
||||||
|
continue
|
||||||
|
mirror = 'ADF.USIM-NON-IMSI' + path[len('ADF.USIM'):]
|
||||||
|
self.assertIn(mirror, by_path)
|
||||||
|
self.assertEqual(by_path[mirror]['name'], name)
|
||||||
|
self.assertEqual(by_path[mirror]['root'], 'ADF.USIM-NON-IMSI')
|
||||||
|
# (name, root) resolves each SUPPLEMENT file to exactly one path.
|
||||||
|
for path, name in expected.items():
|
||||||
|
root = path.split('/')[0]
|
||||||
|
self.assertEqual([f['path'] for f in files
|
||||||
|
if f['name'] == name and f['root'] == root], [path])
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
unittest.main()
|
unittest.main()
|
||||||
Reference in new issue
Block a user