v3.34.0: Edit decoded wave 2 — service tables, scalars, PLMN lists, BCD, CB lists
New client-side editors: the service tables EF.UST/IST/EST (a filterable bit grid; unmodelled bits and the file length preserved), EF.ACC, EF.PHASE, EF.HPPLMN, EF.AD (op-mode enum, additional info, MNC length incl. 0 and the 3-byte SIM form), the PLMN-only lists EF.PLMNsel/FPLMN/EHPLMN, EF.ICCID/EF.IMSI behind a confirmation, and the cell-broadcast lists EF.CBMI/CBMID/CBMIR. Framework: a bits form kind, a per-entry model(dec) hook, a per-entry confirm prompt and hex fields. Decoder fixes: EF.ACC class numbering (byte1 b8..b1 = 15..8, byte2 = 7..0), EF.AD operation mode (whole byte) and its MNC length rules, EF.SST decoded with the SIM 2-bit allocated/activated layout (editing stays disabled until that UI exists), and the EF.IMSI parity nibble (9 odd / 1 even). A no-edit save of the new list files no longer writes all-FF (the collected rows and the encoders disagreed on the value shape). Decoded editing for EF.DIR/EF.ARR/EF.SMS/EF.PBR is postponed by decision (read-only viewing is enough); the survey records it. 1022 Python + 1033 frontend green; simple-v467.
This commit is contained in:
1 parent
101ef21e9f
commit
180dd50c06
13 files changed
+1219
-92
No files matched your search
@@ -9,6 +9,35 @@ every change carries a descriptive commit message.
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [3.34.0] — 2026-10-23
|
||||
|
||||
### Added
|
||||
|
||||
- **Edit decoded, wave 2** — new editors for the service tables **EF.UST / EF.IST /
|
||||
EF.EST** (a filterable bit grid preserving services the table does not model), the
|
||||
scalars **EF.ACC**, **EF.PHASE**, **EF.HPPLMN** and **EF.AD**, the PLMN lists
|
||||
**EF.PLMNsel / EF.FPLMN / EF.EHPLMN**, the identities **EF.ICCID / EF.IMSI** (behind a
|
||||
confirmation — writing them changes the card identity) and the cell-broadcast lists
|
||||
**EF.CBMI / EF.CBMID / EF.CBMIR**. **EF.SST** is deliberately left read-only: its
|
||||
SIM layout is 2 bits per service (allocated + activated), which needs its own editor.
|
||||
- The file-format survey (`docs/decoded-editing.md`) now records the shipped waves and
|
||||
marks **EF.DIR / EF.ARR / EF.SMS / EF.PBR** as postponed by decision (read-only).
|
||||
|
||||
### Fixed
|
||||
|
||||
- **EF.ACC** class numbering (TS 31.102 4.2.15): byte 1 b8…b1 codes classes 15…8 and
|
||||
byte 2 classes 7…0 — the decoder counted class 0 at the MSB (an all-FF ACC read as
|
||||
class 0 instead of classes 0–15).
|
||||
- **EF.AD** (TS 31.102 4.2.18): the operation mode is a whole-byte enum
|
||||
(00/80/01/81/02/04), not bits 0-1 plus a test bit; the MNC length may be 0 (service
|
||||
n°130 available) or absent (3-byte SIM form) — the editor previously refused both.
|
||||
- **EF.SST** (TS 51.011 10.3.7): the SIM service table is decoded with its 2-bits-per-
|
||||
service layout (allocated + activated) instead of the UST 1-bit codec.
|
||||
- **EF.IMSI**: the parity/type nibble is `9` for an odd digit count and `1` for an even
|
||||
one (TS 24.008 10.5.1.4); the encoder always wrote `9`.
|
||||
- Saving a PLMN/CB list with no edits no longer rewrites it as all-FF (the collected rows
|
||||
and the list encoders disagreed on the value shape).
|
||||
|
||||
## [3.33.0] — 2026-10-23
|
||||
|
||||
### Added
|
||||
|
||||
@@ -595,7 +595,7 @@ Browse the UICC filesystem in a tree view. Files are shown with names, FIDs, and
|
||||
- Entries are grouped with DFs above EFs and sorted by **FID** or symbolic **Name** (pills pinned above the tree together with **Probe all files**, remembered in `localStorage`)
|
||||
- **Read raw** / **Read decoded** — read the selected file as a hex dump or as a decoded field table (client-side decoders for IMSI, ICCID, SPN, PLMN lists, LOCI/EPSLOCI, ADN/MSISDN, service tables, SUME, …); the highlighted pill is the current view and clicking either pill (re-)reads the file. The decoded table is built in the browser from the same raw bytes (no server-side decode); **Read decoded** is disabled when the client-side decoders do not cover the file (resolved by name/FID before reading)
|
||||
- **Edit raw** — edit the raw hex data and **Save** to write back (or **Cancel**); transparent files use a textarea, record files one input per record with selection checkboxes. **Edit raw** switches to the hex view first and reads the file if needed
|
||||
- **Edit decoded** — edit through the file's own decoded form (client-side encoders; written via the normal update path): **EF.SPN**, **PLMNwAcT / OPLMNwAcT / HPLMNwAcT**, the dialling-number files **ADN / FDN / SDN / MSISDN / LND / BDN**, and **EF.PNN**. Transparent files get one form (PLMN rows and PNN items with add/remove); record files get one form per record with a checkbox that is auto-checked when a field changes, so only edited records are written. **Save** validates every selected record before writing; extension pointers (EF.EXT1–EF.EXT5) and the BDN comparison-method byte are preserved, and editing a number continued in an extension file is refused. The shape survey and the planned coverage are in `docs/decoded-editing.md`
|
||||
- **Edit decoded** — edit through the file's own decoded form (client-side encoders; written via the normal update path): **EF.SPN**, **EF.PNN**, the PLMN lists **PLMNsel / FPLMN / EHPLMN** and **PLMNwAcT / OPLMNwAcT / HPLMNwAcT**, the dialling-number files **ADN / FDN / SDN / MSISDN / LND / BDN**, the service tables **UST / IST / EST** (**SST** stays read-only until the SIM allocated/activated editor exists), **ACC**, **PHASE**, **HPPLMN**, **AD**, **ICCID / IMSI** (behind a confirmation), and the cell-broadcast lists **CBMI / CBMID / CBMIR**. Transparent files get one form (PLMN rows and PNN items with add/remove); record files get one form per record with a checkbox that is auto-checked when a field changes, so only edited records are written. **Save** validates every selected record before writing; extension pointers (EF.EXT1–EF.EXT5) and the BDN comparison-method byte are preserved, and editing a number continued in an extension file is refused. The shape survey and the planned coverage are in `docs/decoded-editing.md`
|
||||
- **Probe all files** — walks the whole tree (incl. custom files), marks every entry present/absent with *N / total* progress, stoppable, and ends with a summary; browsing itself stays lazy. Missing files are shown in red (✗); a present but empty DF shows `(empty)`
|
||||
- **ADM** — files that need the administrator PIN fail with `6982`/`9804`; when the matching card preset (same ICCID) carries an ADM key, a **Verify ADM** button appears next to the error and the header badge (`ADM ✓/✗ ⚿`) becomes clickable. Every wrong key consumes an attempt (the remaining attempts are shown and a retry asks for confirmation); a blocked ADM needs the card's unblock key
|
||||
|
||||
|
||||
+1
-1
@@ -570,7 +570,7 @@ Delivery PoR (SPI2 `01`) проще — карта возвращает PoR на
|
||||
- Элементы сгруппированы (DF выше EF) и отсортированы по **FID** или символьному **имени** (пиллы закреплены над деревом вместе с **«Проверить все файлы»**, выбор сохраняется в `localStorage`)
|
||||
- **Read raw** / **Read decoded** — чтение выбранного файла как hex-дампа или как таблицы декодированных полей (клиентские декодеры: IMSI, ICCID, SPN, списки PLMN, LOCI/EPSLOCI, ADN/MSISDN, таблицы сервисов, SUME, …); подсвеченная пилла — текущий вид, клик по любой из них перечитывает файл. таблица декодируется в браузере из тех же сырых байт (без серверного декодирования); **Read decoded** отключается, когда клиентские декодеры не покрывают файл (определяется по имени/FID до чтения)
|
||||
- **Edit raw** — редактирование сырых hex-данных и **Save** для записи (или **Cancel**); для transparent-файлов — textarea, для record-файлов — по полю на запись с флажками выбора. **Edit raw** сначала переключается на hex и при необходимости читает файл
|
||||
- **Edit decoded** — редактирование в собственной декодированной форме файла (клиентские кодировщики; запись обычным путём): **EF.SPN**, **PLMNwAcT / OPLMNwAcT / HPLMNwAcT**, файлы номеров **ADN / FDN / SDN / MSISDN / LND / BDN** и **EF.PNN**. Для transparent-файла — одна форма (строки PLMN и элементы PNN с добавлением/удалением); для record-файла — форма на запись с флажком, который ставится автоматически при изменении поля, поэтому записываются только изменённые записи. **Save** проверяет все выбранные записи до записи; указатели расширений (EF.EXT1–EF.EXT5) и байт метода сравнения BDN сохраняются; редактирование номера, продолжение которого лежит в файле расширения, отклоняется. Обзор форматов и план охвата — в `docs/decoded-editing.md`
|
||||
- **Edit decoded** — редактирование в собственной декодированной форме файла (клиентские кодировщики; запись обычным путём): **EF.SPN**, **EF.PNN**, списки PLMN **PLMNsel / FPLMN / EHPLMN** и **PLMNwAcT / OPLMNwAcT / HPLMNwAcT**, файлы номеров **ADN / FDN / SDN / MSISDN / LND / BDN**, таблицы сервисов **UST / IST / EST** (**SST** только для чтения до появления редактора allocated/activated), **ACC**, **PHASE**, **HPPLMN**, **AD**, **ICCID / IMSI** (с подтверждением) и списки cell broadcast **CBMI / CBMID / CBMIR**. Для transparent-файла — одна форма (строки PLMN и элементы PNN с добавлением/удалением); для record-файла — форма на запись с флажком, который ставится автоматически при изменении поля, поэтому записываются только изменённые записи. **Save** проверяет все выбранные записи до записи; указатели расширений (EF.EXT1–EF.EXT5) и байт метода сравнения BDN сохраняются; редактирование номера, продолжение которого лежит в файле расширения, отклоняется. Обзор форматов и план охвата — в `docs/decoded-editing.md`
|
||||
- **Проверить все файлы** — обход всего дерева (включая пользовательские) с пометкой «есть/нет», прогрессом *N / всего*, возможностью остановки и сводкой в конце; сам просмотр остаётся ленивым. Отсутствующие файлы показаны красным (✗); существующий пустой DF — `(пусто)`
|
||||
- **ADM** — файлы, требующие администраторский PIN, возвращают `6982`/`9804`; если в подходящем пресете карты (тот же ICCID) есть ключ ADM, рядом с ошибкой появляется кнопка **«Проверить ADM»**, а значок в заголовке (`ADM ✓/✗ ⚿`) становится кликабельным. Каждый неверный ключ расходует попытку (остаток показывается, повторная попытка требует подтверждения); заблокированный ADM требует ключа разблокировки карты
|
||||
|
||||
|
||||
+62
-36
@@ -1,6 +1,8 @@
|
||||
# Decoded editing — file-format survey and design
|
||||
|
||||
Status: design note for the **Edit decoded** feature (v3.33.0). It classifies
|
||||
Status: design note for the **Edit decoded** feature. Waves 1 and 2 shipped in
|
||||
v3.33.0 and v3.34.0; decoded editing for EF.DIR/EF.ARR/EF.SMS/EF.PBR is
|
||||
**postponed by decision** (their decoded view is enough). It classifies
|
||||
the EFs the workbench knows (the client-side decoder registry) by the spec's
|
||||
own structure and content coding, assigns each an editing *shape*, and sets the
|
||||
release waves. The reference is the pinned specs (`projects/docs/uicc/`):
|
||||
@@ -57,32 +59,32 @@ editing candidates.
|
||||
|
||||
| EF | FID | Spec | Size | Content | Shape | Wave |
|
||||
|---|---|---|---|---|---|---|
|
||||
| EF.ICCID | 2FE2 | TS 51.011 10.3.1 | 10 B | BCD (nibble-swapped), F padding | S3 | 3.34 |
|
||||
| EF.IMSI | 6F07 | TS 31.102 4.2.2 | 9 B | length byte + '9'-prefixed BCD | S3 | 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.SST | 6F38 | TS 51.011 10.3.7 | X ≥ 1 | service bits | S6 | 3.34 |
|
||||
| EF.EST | 6F56 | TS 31.102 4.2.47 | X ≥ 1 | enabled-service bits | S6 | 3.34 |
|
||||
| EF.LI | 6F05 | TS 31.102 4.2.1 | 2n | ISO 639 language codes | S6 | 3.34 |
|
||||
| EF.LP / EF.PL | 6F05 / 2F05 | TS 51.011 10.3.1/10.3.2 | 1–n | language codes | S6 | 3.34 |
|
||||
| EF.ICCID | 2FE2 | TS 51.011 10.3.1 | 10 B | BCD (nibble-swapped), F padding; UPDATE is NEV per TS 102 221 | 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.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.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.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.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.FPLMN | 6F7B | TS 31.102 4.2.16 | 3n (n ≥ 4) | PLMN list | S4 | 3.34 |
|
||||
| EF.EHPLMN | 6FD9 | TS 31.102 4.2.84 | 3n | 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.EHPLMN | 6FD9 | TS 31.102 4.2.84 | 3n | PLMN list | S4 | **3.34** |
|
||||
| EF.PLMNwAcT | 6F60 | TS 31.102 4.2.5 | 5n (n ≥ 8) | PLMN + ACT | S5 | **3.33** |
|
||||
| EF.OPLMNwAcT | 6F61 | TS 31.102 4.2.53 | 5n (n ≥ 8) | PLMN + ACT | S5 | **3.33** |
|
||||
| EF.HPLMNwAcT | 6F62 | TS 31.102 4.2.54 | 5n (n ≥ 1) | PLMN + ACT | S5 | **3.33** |
|
||||
| EF.SPDI | 6FCD | TS 31.102 4.2.66 | x | TLV: A3 > 80 > PLMN list | S4 | 3.34 |
|
||||
| EF.GID1 / GID2 | 6F3E / 6F3F | TS 31.102 4.2.10/4.2.11 | n | text (Annex A) | S2 | 3.34 |
|
||||
| EF.SPDI | 6FCD | TS 31.102 4.2.66 | x | TLV: A3 > 80 > PLMN list | S4 | 3.35 |
|
||||
| EF.GID1 / GID2 | 6F3E / 6F3F | TS 31.102 4.2.10/4.2.11 | n | identifier bytes (coding not stated in the clause) | S2 | 3.35 (verify TS 31.101) |
|
||||
| EF.SUUME | 6F54 | TS 51.011 10.3.34 | X+Y | alpha (Annex A) | S2 | 3.35 |
|
||||
| EF.IMPI/DOMAIN/IMPU | 6F02/6F03/6F04 | TS 31.102 (ISIM) | X | NAI text | S2 | 3.35 |
|
||||
| EF.ACC | 6F78 | TS 31.102 4.2.15 | 2 B | class bits | S1/S6 | 3.34 |
|
||||
| EF.PHASE | 6FAE | TS 51.011 10.3.19 | 1 B | phase id | S1 | 3.34 |
|
||||
| EF.HPPLMN | 6F31 | TS 31.102 4.2.6 | 1 B | search period | S1 | 3.34 |
|
||||
| EF.AD | 6FAD | TS 31.102 4.2.18 | 4+X | op mode, info, MNC length | S1 | 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.CBMIR | 6F50 | TS 31.102 4.2.22 | 4n | CB id ranges | 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.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.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.CBMIR | 6F50 | TS 31.102 4.2.22 | 4n | CB id ranges | S6 | **3.34** |
|
||||
| EF.ACL | 6F57 | TS 31.102 4.2.48 | X | version byte + TLV list (DD APNs) | S8-TR | 3.35 |
|
||||
| EF.LOCI | 6F7E | TS 31.102 4.2.17 | 11 B | TMSI/LAI/status | **S0** | — |
|
||||
| EF.PSLOCI | 6F73 | TS 31.102 4.2.23 | 14 B | P-TMSI/RAI/status | **S0** | — |
|
||||
@@ -103,14 +105,14 @@ editing candidates.
|
||||
| EF.BDN | 6F4D | TS 31.102 4.2.44 | **X+15** | ADN layout + **Comparison Method Pointer** | S7 | **3.33** (fix) |
|
||||
| EF.PNN | 6FC5 | TS 31.102 4.2.58 | X ≥ 3 | TLVs: 43 full / 45 short / 80 PLMN info | S8 | **3.33** |
|
||||
| EF.OPL | 6FC6 | TS 31.102 4.2.59 | 8 | LAI (PLMN+LAC) + PNN record id | S7 | 3.35 |
|
||||
| EF.ECC (SIM) | 6FB7 | TS 51.011 10.3.27 | 3n | emergency codes (TR) | S6 | 3.34 |
|
||||
| EF.ECC (SIM) | 6FB7 | TS 51.011 10.3.27 | 3n | emergency codes (TR) | S6 | 3.35 |
|
||||
| EF.ECC (USIM) | 6FB7 | TS 31.102 4.2.21 | X+4 | code(3) + alpha(X) + category(1) | S7 | 3.35 |
|
||||
| EF.EXT1–EXT5 | 6F4A–6F55 | TS 31.102 4.4.2.4 / 4.2.30/31/45/37 | 13 | type-dependent: additional data / subaddress | S9 | 3.36 |
|
||||
| EF.SMSP | 6F42 | TS 31.102 4.2.27 | 28+Y | alpha + parameter indicators + TP-DA/SCA/PID/DCS/VP | S10 | 3.36 |
|
||||
| EF.SMS | 6F3C | TS 31.102 4.2.25 | 176 | status byte + TPDU | S10 | 3.36 |
|
||||
| EF.ARR | 2F06/6F06 | TS 31.102 4.2.55 | X | access-rule TLVs (ISO 7816-4) | S8/S9 | 3.36 |
|
||||
| EF.DIR | 2F00 | TS 51.011 10.3.3 | X | application template TLVs (61 > 4F/50/73…) | S8/S9 | 3.36 |
|
||||
| EF.PBR | 4F30/4FXX | TS 31.102 4.4.2.1 | X | BER-TLV: A8/A9/AA containers, C0–CC links | S9 | later |
|
||||
| EF.SMSP | 6F42 | TS 31.102 4.2.27 | 28+Y | alpha + parameter indicators + TP-DA/SCA/PID/DCS/VP | S10 | on request only |
|
||||
| EF.SMS | 6F3C | TS 31.102 4.2.25 | 176 | status byte + TPDU | S10 | **postponed (read-only)** |
|
||||
| EF.ARR | 2F06/6F06 | TS 31.102 4.2.55 | X | access-rule TLVs (ISO 7816-4) | S8/S9 | **postponed (read-only)** |
|
||||
| EF.DIR | 2F00 | TS 51.011 10.3.3 | X | application template TLVs (61 > 4F/50/73…) | S8/S9 | **postponed (read-only)** |
|
||||
| EF.PBR | 4F30/4FXX | TS 31.102 4.4.2.1 | X | BER-TLV: A8/A9/AA containers, C0–CC links | S9 | **postponed (read-only)** |
|
||||
| EF.EPSNSC | 6FE4 | TS 31.102 4.2.92 | — | NAS security context | **S0** | — |
|
||||
| EF.ICI/OCI (call logs) | 6F80/6F81 | TS 31.102 4.2.33/4.2.34 | X+28/X+27 | ADN-like + time/date → cyclic, ME-written | **S0** (plan) | — |
|
||||
|
||||
@@ -152,21 +154,45 @@ beyond S9/S10.
|
||||
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
|
||||
vector `43 0A 82 D4 32 BB BC 7E B7 5D E4 32 …` confirms it.
|
||||
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
|
||||
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
|
||||
allocated/activated UI.
|
||||
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
|
||||
class 0 at the MSB. Fixed in v3.34.0.
|
||||
10. **EF.AD's operation mode is a whole byte** (00/80/01/81/02/04), not bits
|
||||
0-1 plus a test bit; the MNC-length byte may be 0 (service n°130 available)
|
||||
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
|
||||
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
|
||||
EF.LI's 2-byte ISO 639 letters — the shared decoder is wrong for the SIM
|
||||
files (queued with the 3.35 language work).
|
||||
|
||||
## 4. Release waves
|
||||
|
||||
- **v3.33.0 (current)** — framework: encoder registry, field/list/items
|
||||
- **v3.33.0 (shipped)** — framework: encoder registry, field/list/items
|
||||
schemas, record checkboxes + auto-check, save/validation; shapes S2, S5,
|
||||
S7, S8 for SPN, PLMNwAcT/OPLMNwAcT/HPLMNwAcT, ADN/FDN/SDN/MSISDN/LND,
|
||||
PNN; the BDN layout fix; the `efPlmnAct` EC-GSM-IoT-only (`0x88`) decode fix.
|
||||
- **v3.34.0** — S1/S3/S6 wave: UST/SST/IST/EST service toggles, LI/LP/PL,
|
||||
ACC, PHASE, HPPLMN, AD, CBMI/CBMID/CBMIR, ECC (SIM), ICCID, IMSI,
|
||||
PLMNsel/FPLMN/EHPLMN, SPDI, GID1/GID2.
|
||||
- **v3.35.0** — remaining S7 records: OPL (PNN record select), ECC (USIM),
|
||||
SUUME, IMPI/DOMAIN/IMPU; EXT1–5 as a read-only structured view.
|
||||
- **v3.36.0+** — S10/complex: SMSP (parameter form), SMS (TPDU editor,
|
||||
reusing the existing PDU builders), ARR (rule editor — the decoder already
|
||||
renders rule lines), DIR (application templates), PBR (nested tree).
|
||||
- **v3.34.0 (shipped)** — the S1/S3/S4/S6 wave: UST/IST/EST service tables
|
||||
(filterable bit grid), ACC, PHASE, HPPLMN, AD, the PLMN-only lists
|
||||
(PLMNsel/FPLMN/EHPLMN), ICCID/IMSI (behind a confirmation) and
|
||||
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
|
||||
IMSI parity nibble.
|
||||
- **v3.35.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 —
|
||||
verify TS 31.101 first), SPDI (the A3 > 80 wrapper), OPL (PNN record
|
||||
select), ECC (SIM and USIM — needs a structure-aware decode), SUUME,
|
||||
IMPI/DOMAIN/IMPU, ACL; EXT1–EXT5 as a read-only structured view.
|
||||
- **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
|
||||
decoded view is enough.
|
||||
- **On request only** — EF.SMSP (a parameter form; no TPDU/parameter builder
|
||||
exists in the PWA today).
|
||||
- **Read-only forever (S0)** unless a dedicated flow appears: LOCI/PSLOCI/
|
||||
EPSLOCI/EPSNSC/LOCIGPRS, Kc/KcGPRS, Keys/KeysPS, ACM, START-HFN/THRESHOLD,
|
||||
call logs (ME-written), DCK/NETPAR.
|
||||
|
||||
@@ -409,7 +409,7 @@
|
||||
<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-данных, <strong>Сохранить</strong> для записи (или <strong>Отмена</strong>); transparent-файлы редактируются в textarea, record-файлы — по одному полю на запись с флажками выбора. «Редактировать сырые данные» сначала переключает на hex-вид и при необходимости читает файл.</li>
|
||||
<li><strong>Редактировать декодированные</strong> — редактирование файла в его собственной декодированной форме; кодирование выполняется в браузере, запись идёт обычным путём обновления. Кодировщики покрывают <strong>EF.SPN</strong>, списки PLMN с технологиями доступа (<strong>PLMNwAcT / OPLMNwAcT / HPLMNwAcT</strong>), файлы номеров (<strong>ADN / FDN / SDN / MSISDN / LND / BDN</strong>) и <strong>EF.PNN</strong>. Для 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> остаётся только для чтения до появления редактора 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>). Для 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>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>
|
||||
|
||||
+1
-1
@@ -408,7 +408,7 @@
|
||||
<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>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>, the PLMN lists with access technology (<strong>PLMNwAcT / OPLMNwAcT / HPLMNwAcT</strong>), the dialling-number files (<strong>ADN / FDN / SDN / MSISDN / LND / BDN</strong>) and <strong>EF.PNN</strong>. 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>; <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) and the cell-broadcast lists (<strong>CBMI / CBMID / CBMIR</strong>). 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>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>
|
||||
|
||||
+564
-26
@@ -1944,7 +1944,7 @@
|
||||
// ===== Version =====
|
||||
// Single source of truth for the PWA version: shown in the header and used
|
||||
// by the server version check in pysimConnect().
|
||||
const SIMPLE_VERSION = '3.33.0';
|
||||
const SIMPLE_VERSION = '3.34.0';
|
||||
document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION;
|
||||
|
||||
// ===== view history (the browser Back/Forward buttons) =====
|
||||
@@ -16334,6 +16334,13 @@ async function pysimFsDecSave(node, rawName, statusEl, out, arrTouched) {
|
||||
: efDecCollectValues(root, entry.fields);
|
||||
const body = pysimFsSelectBody(node || { name: rawName, fid: null });
|
||||
body.data = entry.enc(values, orig);
|
||||
// identity writes ask once, and only when something actually changes
|
||||
if (entry.confirm && String(body.data).toUpperCase() !== String(orig).toUpperCase()) {
|
||||
if (!await pysimConfirm(t(entry.confirm))) {
|
||||
statusEl.textContent = t('Write cancelled');
|
||||
return;
|
||||
}
|
||||
}
|
||||
const data = await pysimFetch('/api/write', body);
|
||||
if (!data.success) {
|
||||
pysimFsShowError(statusEl, data.sw, data.error);
|
||||
@@ -22722,7 +22729,9 @@ window.addEventListener('resize', pysimFsFitTree);
|
||||
const EF_UST_SERVICES = {1: "Local Phone Book", 2: "Fixed Dialling Numbers (FDN)", 3: "Extension 2", 4: "Service Dialling Numbers (SDN)", 5: "Extension3", 6: "Barred Dialling Numbers (BDN)", 7: "Extension4", 8: "Outgoing Call Information (OCI and OCT)", 9: "Incoming Call Information (ICI and ICT)", 10: "Short Message Storage (SMS)", 11: "Short Message Status Reports (SMSR)", 12: "Short Message Service Parameters (SMSP)", 13: "Advice of Charge (AoC)", 14: "Capability Configuration Parameters 2 (CCP2)", 15: "Cell Broadcast Message Identifier", 16: "Cell Broadcast Message Identifier Ranges", 17: "Group Identifier Level 1", 18: "Group Identifier Level 2", 19: "Service Provider Name", 20: "User controlled PLMN selector with Access Technology", 21: "MSISDN", 22: "Image (IMG)", 23: "Support of Localised Service Areas (SoLSA)", 24: "Enhanced Multi-Level Precedence and Pre-emption Service", 25: "Automatic Answer for eMLPP", 26: "RFU", 27: "GSM Access", 28: "Data download via SMS-PP", 29: "Data download via SMS-CB", 30: "Call Control by USIM", 31: "MO-SMS Control by USIM", 32: "RUN AT COMMAND command", 33: "shall be set to 1", 34: "Enabled Services Table", 35: "APN Control List (ACL)", 36: "Depersonalisation Control Keys", 37: "Co-operative Network List", 38: "GSM security context", 39: "CPBCCH Information", 40: "Investigation Scan", 41: "MexE", 42: "Operator controlled PLMN selector with Access Technology", 43: "HPLMN selector with Access Technology", 44: "Extension 5", 45: "PLMN Network Name", 46: "Operator PLMN List", 47: "Mailbox Dialling Numbers", 48: "Message Waiting Indication Status", 49: "Call Forwarding Indication Status", 50: "Reserved and shall be ignored", 51: "Service Provider Display Information", 52: "Multimedia Messaging Service (MMS)", 53: "Extension 8", 54: "Call control on GPRS by USIM", 55: "MMS User Connectivity Parameters", 56: "Network's indication of alerting in the MS (NIA)", 57: "VGCS Group Identifier List (EFVGCS and EFVGCSS)", 58: "VBS Group Identifier List (EFVBS and EFVBSS)", 59: "Pseudonym", 60: "User Controlled PLMN selector for I-WLAN access", 61: "Operator Controlled PLMN selector for I-WLAN access", 62: "User controlled WSID list", 63: "Operator controlled WSID list", 64: "VGCS security", 65: "VBS security", 66: "WLAN Reauthentication Identity", 67: "Multimedia Messages Storage", 68: "Generic Bootstrapping Architecture (GBA)", 69: "MBMS security", 70: "Data download via USSD and USSD application mode", 71: "Equivalent HPLMN", 72: "Additional TERMINAL PROFILE after UICC activation", 73: "Equivalent HPLMN Presentation Indication", 74: "Last RPLMN Selection Indication", 75: "OMA BCAST Smart Card Profile", 76: "GBA-based Local Key Establishment Mechanism", 77: "Terminal Applications", 78: "Service Provider Name Icon", 79: "PLMN Network Name Icon", 80: "Connectivity Parameters for USIM IP connections", 81: "Home I-WLAN Specific Identifier List", 82: "I-WLAN Equivalent HPLMN Presentation Indication", 83: "I-WLAN HPLMN Priority Indication", 84: "I-WLAN Last Registered PLMN", 85: "EPS Mobility Management Information", 86: "Allowed CSG Lists and corresponding indications", 87: "Call control on EPS PDN connection by USIM", 88: "HPLMN Direct Access", 89: "eCall Data", 90: "Operator CSG Lists and corresponding indications", 91: "Support for SM-over-IP", 92: "Support of CSG Display Control", 93: "Communication Control for IMS by USIM", 94: "Extended Terminal Applications", 95: "Support of UICC access to IMS", 96: "Non-Access Stratum configuration by USIM", 97: "PWS configuration by USIM", 98: "RFU", 99: "URI support by UICC", 100: "Extended EARFCN support", 101: "ProSe", 102: "USAT Application Pairing", 103: "Media Type support", 104: "IMS call disconnection cause", 105: "URI support for MO SHORT MESSAGE CONTROL", 106: "ePDG configuration Information support", 107: "ePDG configuration Information configured", 108: "ACDC support", 109: "MCPTT", 110: "ePDG configuration Information for Emergency Service support", 111: "ePDG configuration Information for Emergency Service configured", 112: "eCall Data over IMS", 113: "URI support for SMS-PP DOWNLOAD as defined in 3GPP TS 31.111 [12]", 114: "From Preferred", 115: "IMS configuration data", 116: "TV configuration", 117: "3GPP PS Data Off", 118: "3GPP PS Data Off Service List", 119: "V2X", 120: "XCAP Configuration Data", 121: "EARFCN list for MTC/NB-IOT UEs", 122: "5GS Mobility Management Information", 123: "5G Security Parameters", 124: "Subscription identifier privacy support", 125: "SUCI calculation by the USIM", 126: "UAC Access Identities support", 127: "Expect control plane-based Steering of Roaming information during initial registration in VPLMN", 128: "Call control on PDU Session by USIM", 129: "5GS Operator PLMN List", 130: "Support for SUPI of type NSI or GLI or GCI", 131: "3GPP PS Data Off separate Home and Roaming lists", 132: "Support for URSP by USIM", 133: "5G Security Parameters extended", 134: "MuD and MiD configuration data", 135: Line truncated
|
||||
const EF_SST_SERVICES = {1: "CHV1 disable function", 2: "Abbreviated Dialling Numbers (ADN)", 3: "Fixed Dialling Numbers (FDN)", 4: "Short Message Storage (SMS)", 5: "Advice of Charge (AoC)", 6: "Capability Configuration Parameters (CCP)", 7: "PLMN selector", 8: "RFU", 9: "MSISDN", 10: "Extension1", 11: "Extension2", 12: "SMS Parameters", 13: "Last Number Dialled (LND)", 14: "Cell Broadcast Message Identifier", 15: "Group Identifier Level 1", 16: "Group Identifier Level 2", 17: "Service Provider Name", 18: "Service Dialling Numbers (SDN)", 19: "Extension3", 20: "RFU", 21: "VGCS Group Identifier List (EFVGCS and EFVGCSS)", 22: "VBS Group Identifier List (EFVBS and EFVBSS)", 23: "enhanced Multi-Level Precedence and Pre-emption Service", 24: "Automatic Answer for eMLPP", 25: "Data download via SMS-CB", 26: "Data download via SMS-PP", 27: "Menu selection", 28: "Call control", 29: "Proactive SIM", 30: "Cell Broadcast Message Identifier Ranges", 31: "Barred Dialling Numbers (BDN)", 32: "Extension4", 33: "De-personalization Control Keys", 34: "Co-operative Network List", 35: "Short Message Status Reports", 36: "Network's indication of alerting in the MS", 37: "Mobile Originated Short Message control by SIM", 38: "GPRS", 39: "Image (IMG)", 40: "SoLSA (Support of Local Service Area)", 41: "USSD string data object supported in Call Control", 42: "RUN AT COMMAND command", 43: "User controlled PLMN Selector with Access Technology", 44: "Operator controlled PLMN Selector with Access Technology", 45: "HPLMN Selector with Access Technology", 46: "CPBCCH Information", 47: "Investigation Scan", 48: "Extended Capability Configuration Parameters", 49: "MExE", 50: "Reserved and shall be ignored", 51: "PLMN Network Name", 52: "Operator PLMN List", 53: "Mailbox Dialling Numbers", 54: "Message Waiting Indication Status", 55: "Call Forwarding Indication Status", 56: "Service Provider Display Information", 57: "Multimedia Messaging Service (MMS)", 58: "Extension 8", 59: "MMS User Connectivity Parameters"};
|
||||
const EF_IST_SERVICES = {1: "P-CSCF address", 2: "Generic Bootstrapping Architecture (GBA)", 3: "HTTP Digest", 4: "GBA-based Local Key Establishment Mechanism", 5: "Support of P-CSCF discovery for IMS Local Break Out", 6: "Short Message Storage (SMS)", 7: "Short Message Status Reports (SMSR)", 8: "Support for SM-over-IP including data download via SMS-PP as defined in TS 31.111 [31]", 9: "Communication Control for IMS by ISIM", 10: "Support of UICC access to IMS", 11: "URI support by UICC", 12: "Media Type support", 13: "IMS call disconnection cause", 14: "URI support for MO SHORT MESSAGE CONTROL", 15: "MCPTT", 16: "URI support for SMS-PP DOWNLOAD as defined in 3GPP TS 31.111 [31]", 17: "From Preferred", 18: "IMS configuration data", 19: "XCAP Configuration Data", 20: "WebRTC URI", 21: "MuD and MiD configuration data", 22: "IMS Data Channel indication"};
|
||||
const EF_PHASE_NAMES = {0: "phase 1", 2: "phase 2", 3: "phase 2 and higher"};
|
||||
const EF_PHASE_NAMES = {0: "phase 1", 2: "phase 2", 3: "phase 2 and PROFILE DOWNLOAD required"};
|
||||
// TS 31.102 4.2.47 EF.EST: the enabled-services bits.
|
||||
const EF_EST_SERVICES = {1: "Fixed Dialling Numbers (FDN)", 2: "Barred Dialling Numbers (BDN)", 3: "APN Control List (ACL)"};
|
||||
// TS 31.102 §4.4.2.1 EF_PBR primitive tags (the phonebook kinds).
|
||||
const EF_PBR_KINDS = { 0xC0: 'EF.ADN', 0xC1: 'EF.IAP', 0xC2: 'EF.EXT1', 0xC3: 'EF.SNE', 0xC4: 'EF.ANR', 0xC5: 'EF.PBC', 0xC6: 'EF.GRP', 0xC7: 'EF.AAS', 0xC8: 'EF.GAS', 0xC9: 'EF.UID', 0xCA: 'EF.EMAIL', 0xCB: 'EF.CCP1', 0xCC: 'EF.PURI' };
|
||||
|
||||
@@ -23009,6 +23018,25 @@ function efDecServiceTable(b, table) {
|
||||
return { services: services };
|
||||
}
|
||||
|
||||
// TS 51.011 10.3.7 EF.SST (SIM): two bits per service, four per byte - the
|
||||
// first bit of the pair (b1/b3/b5/b7) is allocated, the second (b2/b4/b6/b8)
|
||||
// activated; service n -> byte (n-1)>>2, pair start 2*((n-1)&3).
|
||||
function efDecSst(b) {
|
||||
const services = [];
|
||||
for (let i = 0; i < b.length; i++) {
|
||||
for (let pair = 0; pair < 4; pair++) {
|
||||
const n = i * 4 + pair + 1;
|
||||
const allocated = !!(b[i] & (1 << (pair * 2)));
|
||||
const activated = !!(b[i] & (1 << (pair * 2 + 1)));
|
||||
if (allocated || activated) {
|
||||
services.push({ n: n, name: EF_SST_SERVICES[n] || 'RFU',
|
||||
allocated: allocated, activated: activated });
|
||||
}
|
||||
}
|
||||
}
|
||||
return { services: services };
|
||||
}
|
||||
|
||||
function efDecPlmnList(b) {
|
||||
const plmns = [];
|
||||
for (let i = 0; i + 2 < b.length; i += 3) {
|
||||
@@ -23125,10 +23153,38 @@ function efDecAcc(b) {
|
||||
if (b.length < 2) return { raw: efHex(b) };
|
||||
const val = (b[0] << 8) | b[1];
|
||||
const classes = [];
|
||||
for (let i = 0; i < 16; i++) if (val & (0x8000 >> i)) classes.push(i);
|
||||
// TS 31.102 4.2.15: byte 1 b8..b1 = ACC 15..8, byte 2 b8..b1 = ACC 7..0,
|
||||
// so the MSB of the 16-bit value is class 15 (8000 -> [15], 0001 -> [0]).
|
||||
for (let i = 0; i < 16; i++) if (val & (0x8000 >> i)) classes.push(15 - i);
|
||||
return { access_classes: classes };
|
||||
}
|
||||
|
||||
// TS 31.102 4.2.6 EF.HPPLMN: a 1-byte time-interval multiplier - a raw byte
|
||||
// whose scale depends on the radio technology (minutes on GERAN/UTRAN, hours
|
||||
// on NB-IoT/EC-GSM-IoT/eMTC-M1), so the editor exposes the hex value.
|
||||
function efDecHpplmn(b) {
|
||||
if (!b.length) return { raw: efHex(b) };
|
||||
return { interval: '0x' + b[0].toString(16).toUpperCase().padStart(2, '0') };
|
||||
}
|
||||
|
||||
// TS 31.102 4.2.18 EF.AD: byte 1 is the whole RFU/operation-mode enum, bytes
|
||||
// 2-3 additional information, byte 4 the MNC length, bytes 5.. RFU.
|
||||
const EF_AD_OP_MODES = {
|
||||
'00': 'normal operation', '80': 'type approval',
|
||||
'01': 'normal operation + specific facilities', '81': 'type approval + specific facilities',
|
||||
'02': 'maintenance (off line)', '04': 'cell test operation',
|
||||
};
|
||||
|
||||
function efDecAd(b) {
|
||||
if (efAllFf(b)) return { empty: true };
|
||||
if (!b.length) return { raw: efHex(b) };
|
||||
const op = b[0].toString(16).toUpperCase().padStart(2, '0');
|
||||
const out = { op_mode: EF_AD_OP_MODES[op] || '0x' + op };
|
||||
if (b.length >= 3) out.additional_info = efHex(b.subarray(1, 3));
|
||||
if (b.length >= 4 && b[3] !== 0xFF) out.mnc_length = b[3];
|
||||
return out;
|
||||
}
|
||||
|
||||
function efDecPhase(b) {
|
||||
const v = b.length ? b[0] : 0;
|
||||
return { phase: EF_PHASE_NAMES[v] || '0x' + v.toString(16).toUpperCase().padStart(2, '0') };
|
||||
@@ -23183,18 +23239,6 @@ function efDecOpl(b) {
|
||||
return recs.length === 1 ? recs[0] : { records: recs };
|
||||
}
|
||||
|
||||
function efDecAd(b) {
|
||||
if (efAllFf(b)) return { empty: true };
|
||||
const op = b[0] & 0x03;
|
||||
const out = {
|
||||
op_mode: { 0: 'normal operation', 1: 'specific features' }[op] || '0x' + op.toString(16).padStart(2, '0'),
|
||||
test_mode: !!(b[0] & 0x04),
|
||||
};
|
||||
if (b.length >= 3) out.additional_info = efHex(b.subarray(1, 3));
|
||||
if (b.length >= 4 && b[3] !== 0xFF) out.mnc_length = b[3];
|
||||
return out;
|
||||
}
|
||||
|
||||
function efDecAcl(b) {
|
||||
if (efAllFf(b)) return { empty: true };
|
||||
const apns = [];
|
||||
@@ -23543,7 +23587,9 @@ const EF_DECODERS = [
|
||||
{ name: 'EF.IMSI', fid: '6f07', fn: efDecImsi },
|
||||
{ name: 'EF.IST', fid: '6f07', fn: b => efDecServiceTable(b, EF_IST_SERVICES) },
|
||||
{ name: 'EF.UST', fid: '6f38', fn: b => efDecServiceTable(b, EF_UST_SERVICES) },
|
||||
{ name: 'EF.SST', fid: '6f38', fn: b => efDecServiceTable(b, EF_SST_SERVICES) },
|
||||
{ name: 'EF.SST', fid: '6f38', fn: efDecSst },
|
||||
{ name: 'EF.EST', fid: '6f56', fn: b => efDecServiceTable(b, EF_EST_SERVICES) },
|
||||
{ name: 'EF.HPPLMN', fid: '6f31', fn: efDecHpplmn },
|
||||
{ name: 'EF.LI', fid: '6f05', fn: efDecLi },
|
||||
{ name: 'EF.LP', fid: '6f05', fn: efDecLi },
|
||||
{ name: 'EF.PL', fid: '2f05', fn: efDecLi },
|
||||
@@ -23954,6 +24000,296 @@ function efEncPnn(items, origHex) {
|
||||
return efHex(bytes);
|
||||
}
|
||||
|
||||
// Service tables (TS 31.102 4.2.7/4.2.46/4.2.47, TS 51.011 10.3.7): service n
|
||||
// lives in byte (n-1)>>3, bit (n-1)&7 (LSB first). The original bytes are the
|
||||
// base, so bits of services the table does not define survive.
|
||||
function efEncServiceTable(values, origHex, table) {
|
||||
const orig = efBytes(origHex || '');
|
||||
const out = new Uint8Array(orig.length);
|
||||
out.set(orig, 0);
|
||||
for (const key of Object.keys(table || {})) {
|
||||
const n = Number(key);
|
||||
if (n < 1 || (n - 1) >= out.length * 8) continue;
|
||||
out[(n - 1) >> 3] &= ~(1 << ((n - 1) & 7));
|
||||
}
|
||||
for (const item of (values && values.services) || []) {
|
||||
const n = Number(item && typeof item === 'object' ? item.n : item);
|
||||
if (!Number.isInteger(n) || n < 1 || !Object.prototype.hasOwnProperty.call(table, n)) {
|
||||
throw efEncError('Unknown service', ': ' + (item && item.n));
|
||||
}
|
||||
if ((n - 1) >= out.length * 8) {
|
||||
throw efEncError('The service table is too short for service', ' ' + n);
|
||||
}
|
||||
out[(n - 1) >> 3] |= 1 << ((n - 1) & 7);
|
||||
}
|
||||
return efHex(out);
|
||||
}
|
||||
|
||||
// EF.ACC (TS 31.102 4.2.15): class c is bit c of the 16-bit value (class 15 is
|
||||
// the MSB); ACC 10 is not used and is written as 0.
|
||||
function efEncAcc(values, origHex) {
|
||||
const orig = efBytes(origHex || '');
|
||||
if (orig.length < 2) throw efEncError('The record is too short for the ACC layout');
|
||||
let val = 0;
|
||||
for (const item of (values && values.access_classes) || []) {
|
||||
const c = Number(item && typeof item === 'object' ? item.n : item);
|
||||
if (!Number.isInteger(c) || c < 0 || c > 15) {
|
||||
throw efEncError('Unknown access class', ': ' + (item && item.n));
|
||||
}
|
||||
val |= 1 << c;
|
||||
}
|
||||
val &= ~(1 << 10);
|
||||
const out = new Uint8Array(orig.length);
|
||||
out.set(orig, 0);
|
||||
out[0] = (val >> 8) & 0xFF;
|
||||
out[1] = val & 0xFF;
|
||||
return efHex(out);
|
||||
}
|
||||
|
||||
// EF.PHASE (TS 51.011 10.3.19): one byte - 00 phase 1, 02 phase 2, 03 phase 2
|
||||
// with PROFILE DOWNLOAD; the editor also keeps an unknown current value.
|
||||
function efPhaseValue(name) {
|
||||
const s = String(name === undefined || name === null ? '' : name);
|
||||
for (const k of Object.keys(EF_PHASE_NAMES)) {
|
||||
if (EF_PHASE_NAMES[k] === s) return Number(k).toString(16).toUpperCase().padStart(2, '0');
|
||||
}
|
||||
const m = /^0x([0-9a-fA-F]{1,2})$/.exec(s);
|
||||
return m ? m[1].toUpperCase().padStart(2, '0') : '';
|
||||
}
|
||||
|
||||
function efPhaseModel(dec) {
|
||||
return { phase_hex: efPhaseValue(dec && dec.phase) };
|
||||
}
|
||||
|
||||
function efEncPhase(values, origHex) {
|
||||
const orig = efBytes(origHex || '');
|
||||
const s = String(values && values.phase_hex !== undefined && values.phase_hex !== null
|
||||
? values.phase_hex : '').trim();
|
||||
if (!/^(0x)?[0-9a-fA-F]{1,2}$/.test(s)) throw efEncError('The phase must be a hex byte');
|
||||
const out = new Uint8Array(Math.max(orig.length, 1));
|
||||
out.fill(0xFF);
|
||||
out[0] = parseInt(s.replace(/^0x/i, ''), 16);
|
||||
return efHex(out);
|
||||
}
|
||||
|
||||
// EF.HPPLMN (TS 31.102 4.2.6): the raw 1-byte time-interval multiplier (the
|
||||
// scale depends on the radio technology, so the editor shows the hex value).
|
||||
function efEncHpplmn(values, origHex) {
|
||||
const orig = efBytes(origHex || '');
|
||||
const s = String(values && values.interval !== undefined && values.interval !== null
|
||||
? values.interval : '').trim();
|
||||
if (!/^(0x)?[0-9a-fA-F]{1,2}$/.test(s)) throw efEncError('The interval must be a hex byte');
|
||||
const out = new Uint8Array(Math.max(orig.length, 1));
|
||||
out.fill(0xFF);
|
||||
out[0] = parseInt(s.replace(/^0x/i, ''), 16);
|
||||
return efHex(out);
|
||||
}
|
||||
|
||||
// EF.AD (TS 31.102 4.2.18): the operation-mode byte, the 2-byte additional
|
||||
// information, the MNC length and the RFU tail (bytes 5.. preserved).
|
||||
function efAdOpValue(name) {
|
||||
const s = String(name === undefined || name === null ? '' : name);
|
||||
for (const k of Object.keys(EF_AD_OP_MODES)) if (EF_AD_OP_MODES[k] === s) return k;
|
||||
const m = /^0x([0-9a-fA-F]{1,2})$/.exec(s);
|
||||
return m ? m[1].toUpperCase().padStart(2, '0') : '';
|
||||
}
|
||||
|
||||
function efAdModel(dec) {
|
||||
dec = dec || {};
|
||||
// an all-FF record has no decoded op mode: expose FF so the untouched
|
||||
// form writes FF back instead of the select's first known value (00)
|
||||
const empty = dec.op_mode === undefined;
|
||||
return {
|
||||
op_mode: empty ? 'FF' : efAdOpValue(dec.op_mode),
|
||||
additional_info: dec.additional_info || '',
|
||||
mnc_length: dec.mnc_length === undefined ? '' : String(dec.mnc_length),
|
||||
};
|
||||
}
|
||||
|
||||
function efEncAd(values, origHex) {
|
||||
const orig = efBytes(origHex || '');
|
||||
// TS 31.102 4.2.18 is 4+X bytes; the SIM form (TS 51.011 10.3.18) may omit
|
||||
// byte 4, so a 3-byte record is valid too
|
||||
if (orig.length < 3) throw efEncError('The record is too short for the AD layout');
|
||||
const op = String(values && values.op_mode !== undefined && values.op_mode !== null
|
||||
? values.op_mode : '').trim();
|
||||
if (op !== '' && !/^(0x)?[0-9a-fA-F]{1,2}$/.test(op)) {
|
||||
throw efEncError('The operation mode must be a hex byte');
|
||||
}
|
||||
const info = String(values && values.additional_info !== undefined && values.additional_info !== null
|
||||
? values.additional_info : '').trim();
|
||||
if (info !== '' && !/^[0-9a-fA-F]{4}$/.test(info)) {
|
||||
throw efEncError('The additional information must be 4 hex digits');
|
||||
}
|
||||
// 0 is valid (service n°130 available), as are the USIM 2/3
|
||||
const mnc = String(values && values.mnc_length !== undefined && values.mnc_length !== null
|
||||
? values.mnc_length : '').trim();
|
||||
if (mnc !== '' && !/^[023]$/.test(mnc)) throw efEncError('The MNC length must be 0, 2 or 3 digits');
|
||||
if (orig.length < 4 && mnc !== '') throw efEncError('The record has no MNC length byte');
|
||||
const out = new Uint8Array(orig.length);
|
||||
out.set(orig, 0);
|
||||
out[0] = op === '' ? 0xFF : parseInt(op.replace(/^0x/i, ''), 16);
|
||||
const infoBytes = efBytes(info || 'FFFF');
|
||||
out[1] = infoBytes[0];
|
||||
out[2] = infoBytes[1];
|
||||
if (orig.length >= 4) out[3] = mnc === '' ? 0xFF : parseInt(mnc, 10);
|
||||
return efHex(out);
|
||||
}
|
||||
|
||||
// The decoded editor's list save passes the collected row ARRAY while the API
|
||||
// and the decoders' own output use the {key: rows} object; accept both (the
|
||||
// PLMNwAcT encoder keeps its array-only contract).
|
||||
function efListRows(values, key) {
|
||||
if (Array.isArray(values)) return values;
|
||||
return (values && values[key]) || [];
|
||||
}
|
||||
|
||||
// EF.PLMNsel / EF.HPPLMN / EF.EHPLMN: 3-byte PLMNs, no ACT. An unchanged list
|
||||
// keeps the original bytes (EF.FPLMN may carry FFFFFF holes); an edited one is
|
||||
// written compactly and FF-padded.
|
||||
function efEncPlmnOnly(values, origHex) {
|
||||
const orig = efBytes(origHex || '');
|
||||
const rows = efListRows(values, 'plmns');
|
||||
const capacity = Math.floor(orig.length / 3);
|
||||
if (rows.length > capacity) {
|
||||
throw efEncError('Too many entries for this file', ' (' + capacity + ' max)');
|
||||
}
|
||||
const chunks = [];
|
||||
for (let i = 0; i + 2 < orig.length; i += 3) {
|
||||
const chunk = orig.subarray(i, i + 3);
|
||||
if (efAllFf(chunk)) continue;
|
||||
const p = efPlmn(chunk);
|
||||
if (p) chunks.push(p);
|
||||
}
|
||||
const same = rows.length === chunks.length && rows.every((r, i) =>
|
||||
String(r.mcc).trim() === chunks[i].mcc && String(r.mnc).trim() === chunks[i].mnc);
|
||||
if (same) return efHex(orig);
|
||||
const out = new Uint8Array(orig.length);
|
||||
out.fill(0xFF);
|
||||
let pos = 0;
|
||||
for (const row of rows) {
|
||||
out.set(efBytes(efPlmnEnc(row.mcc, row.mnc)), pos);
|
||||
pos += 3;
|
||||
}
|
||||
return efHex(out);
|
||||
}
|
||||
|
||||
// EF.ICCID (TS 102 221 13.2): E.118 digits in nibble-swapped BCD, an odd digit
|
||||
// count padded with F and the file's FF tail kept.
|
||||
function efEncIccid(values, origHex) {
|
||||
const orig = efBytes(origHex || '');
|
||||
const digits = String(values && values.iccid !== undefined && values.iccid !== null
|
||||
? values.iccid : '').replace(/\s+/g, '');
|
||||
if (digits !== '' && !/^\d+$/.test(digits)) throw efEncError('The ICCID must be digits');
|
||||
if (digits.length > 20) throw efEncError('The ICCID is too long');
|
||||
const need = Math.ceil(digits.length / 2);
|
||||
if (need > orig.length) throw efEncError('The ICCID does not fit in the file');
|
||||
const out = new Uint8Array(orig.length);
|
||||
out.fill(0xFF);
|
||||
for (let i = 0; i < need; i++) {
|
||||
const lo = digits[i * 2] !== undefined ? digits[i * 2] : 'F';
|
||||
const hi = digits[i * 2 + 1] !== undefined ? digits[i * 2 + 1] : 'F';
|
||||
out[i] = parseInt(hi + lo, 16);
|
||||
}
|
||||
return efHex(out);
|
||||
}
|
||||
|
||||
// EF.IMSI (TS 31.102 4.2.2, TS 24.008 10.5.1.4): byte 1 = the body's nibble
|
||||
// count / 2; the body's first nibble is the identity type plus the parity
|
||||
// indicator - 9 for an odd digit count, 1 for an even one - followed by the
|
||||
// digits (odd total padded with F), nibble-swapped; FF-padded to the file.
|
||||
// 6F07 is also EF.IST: a short file is a service table, not an IMSI.
|
||||
function efEncImsi(values, origHex) {
|
||||
const orig = efBytes(origHex || '');
|
||||
if (orig.length <= 2) {
|
||||
throw efEncError('This file is a service table, not an IMSI');
|
||||
}
|
||||
const digits = String(values && values.imsi !== undefined && values.imsi !== null
|
||||
? values.imsi : '').replace(/\s+/g, '');
|
||||
if (digits !== '' && !/^\d{6,15}$/.test(digits)) throw efEncError('The IMSI must be 6-15 digits');
|
||||
const out = new Uint8Array(orig.length);
|
||||
out.fill(0xFF);
|
||||
if (digits === '') return efHex(out);
|
||||
let body = (digits.length % 2 ? '9' : '1') + digits;
|
||||
if (body.length % 2) body += 'F';
|
||||
const need = 1 + body.length / 2;
|
||||
if (need > orig.length) throw efEncError('The IMSI does not fit in the file');
|
||||
out[0] = body.length / 2;
|
||||
for (let i = 1; i < need; i++) {
|
||||
const lo = body[(i - 1) * 2];
|
||||
const hi = body[(i - 1) * 2 + 1];
|
||||
out[i] = parseInt(hi + lo, 16);
|
||||
}
|
||||
return efHex(out);
|
||||
}
|
||||
|
||||
// EF.CBMI / EF.CBMID: 2-byte message ids (FFFF = unused). The form's rows
|
||||
// carry 4-hex-digit ids; an unchanged list keeps the original bytes (holes).
|
||||
function efCbmiModel(dec) {
|
||||
const ids = dec && dec.message_ids ? dec.message_ids : [];
|
||||
return { message_ids: ids.map(n => ({ id: Number(n).toString(16).toUpperCase().padStart(4, '0') })) };
|
||||
}
|
||||
|
||||
function efEncCbmi(values, origHex) {
|
||||
const orig = efBytes(origHex || '');
|
||||
const rows = efListRows(values, 'message_ids');
|
||||
const capacity = Math.floor(orig.length / 2);
|
||||
if (rows.length > capacity) {
|
||||
throw efEncError('Too many entries for this file', ' (' + capacity + ' max)');
|
||||
}
|
||||
const ids = rows.map(r => {
|
||||
const s = String(r && r.id !== undefined && r.id !== null ? r.id : r).trim();
|
||||
if (!/^(0x)?[0-9a-fA-F]{1,4}$/.test(s)) throw efEncError('The message ID must be 1-4 hex digits');
|
||||
return parseInt(s.replace(/^0x/i, ''), 16);
|
||||
});
|
||||
const dec = efDecCbmi(orig).message_ids;
|
||||
if (ids.length === dec.length && ids.every((v, i) => v === dec[i])) return efHex(orig);
|
||||
const out = new Uint8Array(orig.length);
|
||||
out.fill(0xFF);
|
||||
for (let i = 0; i < ids.length; i++) {
|
||||
out[i * 2] = (ids[i] >> 8) & 0xFF;
|
||||
out[i * 2 + 1] = ids[i] & 0xFF;
|
||||
}
|
||||
return efHex(out);
|
||||
}
|
||||
|
||||
// EF.CBMIR: 4-byte ranges (low, high) of 2-byte message ids.
|
||||
function efCbmirModel(dec) {
|
||||
const ranges = dec && dec.ranges ? dec.ranges : [];
|
||||
return { ranges: ranges.map(r => ({ low: r[0], high: r[1] })) };
|
||||
}
|
||||
|
||||
function efEncCbmir(values, origHex) {
|
||||
const orig = efBytes(origHex || '');
|
||||
const rows = efListRows(values, 'ranges');
|
||||
const capacity = Math.floor(orig.length / 4);
|
||||
if (rows.length > capacity) {
|
||||
throw efEncError('Too many entries for this file', ' (' + capacity + ' max)');
|
||||
}
|
||||
const hex4 = v => Number(v).toString(16).toUpperCase().padStart(4, '0');
|
||||
const ranges = rows.map(r => {
|
||||
const low = String(r && r.low !== undefined ? r.low : '').trim();
|
||||
const high = String(r && r.high !== undefined ? r.high : '').trim();
|
||||
if (!/^(0x)?[0-9a-fA-F]{1,4}$/.test(low) || !/^(0x)?[0-9a-fA-F]{1,4}$/.test(high)) {
|
||||
throw efEncError('The range bounds must be 1-4 hex digits');
|
||||
}
|
||||
return [parseInt(low.replace(/^0x/i, ''), 16), parseInt(high.replace(/^0x/i, ''), 16)];
|
||||
});
|
||||
const dec = (efDecCbmir(orig).ranges || []).map(r => [r[0], r[1]]);
|
||||
const same = ranges.length === dec.length && ranges.every((r, i) =>
|
||||
hex4(r[0]) === dec[i][0].toUpperCase() && hex4(r[1]) === dec[i][1].toUpperCase());
|
||||
if (same) return efHex(orig);
|
||||
const out = new Uint8Array(orig.length);
|
||||
out.fill(0xFF);
|
||||
for (let i = 0; i < ranges.length; i++) {
|
||||
out[i * 4] = (ranges[i][0] >> 8) & 0xFF;
|
||||
out[i * 4 + 1] = ranges[i][0] & 0xFF;
|
||||
out[i * 4 + 2] = (ranges[i][1] >> 8) & 0xFF;
|
||||
out[i * 4 + 3] = ranges[i][1] & 0xFF;
|
||||
}
|
||||
return efHex(out);
|
||||
}
|
||||
|
||||
// The decoded editor's field schemas: one per file family, the keys matching
|
||||
// the decoder output. `kind` is text, digits, bool, select or techs (the
|
||||
// access technology checkboxes, the value being the decoder's ', '-joined
|
||||
@@ -23992,12 +24328,101 @@ const EF_PNN_ITEMS = {
|
||||
preserveUnknown: true,
|
||||
};
|
||||
|
||||
// Service-table and scalar editors (wave 2). The bits kind renders the whole
|
||||
// table as a filterable checkbox grid; its value is the decoder's list.
|
||||
const EF_ACC_CLASSES = {
|
||||
0: 'Class 0', 1: 'Class 1', 2: 'Class 2', 3: 'Class 3', 4: 'Class 4', 5: 'Class 5',
|
||||
6: 'Class 6', 7: 'Class 7', 8: 'Class 8', 9: 'Class 9', 11: 'Class 11',
|
||||
12: 'Class 12', 13: 'Class 13', 14: 'Class 14', 15: 'Class 15',
|
||||
};
|
||||
|
||||
const EF_UST_FIELDS = [{ key: 'services', label: 'Services', kind: 'bits', items: EF_UST_SERVICES }];
|
||||
const EF_IST_FIELDS = [{ key: 'services', label: 'Services', kind: 'bits', items: EF_IST_SERVICES }];
|
||||
const EF_EST_FIELDS = [{ key: 'services', label: 'Services', kind: 'bits', items: EF_EST_SERVICES }];
|
||||
|
||||
const EF_ACC_FIELDS = [{ key: 'access_classes', label: 'Access classes', kind: 'bits', items: EF_ACC_CLASSES }];
|
||||
|
||||
const EF_PHASE_FIELDS = [{
|
||||
key: 'phase_hex', label: 'Phase', kind: 'select',
|
||||
options: [
|
||||
{ v: '00', l: EF_PHASE_NAMES[0] },
|
||||
{ v: '02', l: EF_PHASE_NAMES[2] },
|
||||
{ v: '03', l: EF_PHASE_NAMES[3] },
|
||||
],
|
||||
}];
|
||||
|
||||
const EF_HPPLMN_FIELDS = [{
|
||||
key: 'interval', label: 'Interval', kind: 'hex', max: 4,
|
||||
hint: '00 = no higher-priority PLMN search; 01-FF = n minutes on GERAN/UTRAN '
|
||||
+ '(01 = n, 02 = 2n, ...). NB-IoT/EC-GSM-IoT/eMTC-M1 read the same byte in hours '
|
||||
+ '(01 = n hours (2 h), 02-28 = 2n hours, 29-50 = 4n-80 hours).',
|
||||
}];
|
||||
|
||||
const EF_AD_FIELDS = [
|
||||
{
|
||||
key: 'op_mode', label: 'Operation mode', kind: 'select',
|
||||
options: [
|
||||
{ v: '00', l: 'normal operation' },
|
||||
{ v: '80', l: 'type approval' },
|
||||
{ v: '01', l: 'normal operation + specific facilities' },
|
||||
{ v: '81', l: 'type approval + specific facilities' },
|
||||
{ v: '02', l: 'maintenance (off line)' },
|
||||
{ v: '04', l: 'cell test operation' },
|
||||
],
|
||||
},
|
||||
{ key: 'additional_info', label: 'Additional info', kind: 'hex', max: 4 },
|
||||
{ key: 'mnc_length', label: 'MNC length', kind: 'digits', max: 1 },
|
||||
];
|
||||
|
||||
// PLMN lists without ACT (EF.PLMNsel / EF.FPLMN / EF.EHPLMN): the PLMN row UI
|
||||
// without the technology field.
|
||||
const EF_PLMN_ONLY_LIST = {
|
||||
key: 'plmns', addLabel: 'Add network',
|
||||
itemFields: [
|
||||
{ key: 'mcc', label: 'MCC', kind: 'digits', max: 3, required: true },
|
||||
{ key: 'mnc', label: 'MNC', kind: 'digits', max: 3, required: true },
|
||||
],
|
||||
};
|
||||
|
||||
const EF_ICCID_FIELDS = [{ key: 'iccid', label: 'ICCID', kind: 'text' }];
|
||||
const EF_IMSI_FIELDS = [{ key: 'imsi', label: 'IMSI', kind: 'text' }];
|
||||
|
||||
const EF_CBMI_LIST = {
|
||||
key: 'message_ids', addLabel: 'Add message ID',
|
||||
itemFields: [{ key: 'id', label: 'Message ID', kind: 'hex', max: 4, required: true }],
|
||||
};
|
||||
|
||||
const EF_CBMIR_LIST = {
|
||||
key: 'ranges', addLabel: 'Add range',
|
||||
itemFields: [
|
||||
{ key: 'low', label: 'Low', kind: 'hex', max: 4, required: true },
|
||||
{ key: 'high', label: 'High', kind: 'hex', max: 4, required: true },
|
||||
],
|
||||
};
|
||||
|
||||
const EF_ENCODERS = [
|
||||
{ name: 'EF.ICCID', fid: '2fe2', enc: efEncIccid, fields: EF_ICCID_FIELDS,
|
||||
confirm: 'Writing the ICCID changes the card identity and may break network registration. Continue?' },
|
||||
{ name: 'EF.IMSI', fid: '6f07', enc: efEncImsi, fields: EF_IMSI_FIELDS,
|
||||
confirm: 'Writing the IMSI changes the card identity and may break network registration. Continue?' },
|
||||
{ name: 'EF.IST', fid: '6f07', enc: (v, o) => efEncServiceTable(v, o, EF_IST_SERVICES), fields: EF_IST_FIELDS },
|
||||
{ name: 'EF.UST', fid: '6f38', enc: (v, o) => efEncServiceTable(v, o, EF_UST_SERVICES), fields: EF_UST_FIELDS },
|
||||
{ name: 'EF.EST', fid: '6f56', enc: (v, o) => efEncServiceTable(v, o, EF_EST_SERVICES), fields: EF_EST_FIELDS },
|
||||
{ name: 'EF.ACC', fid: '6f78', enc: efEncAcc, fields: EF_ACC_FIELDS },
|
||||
{ name: 'EF.PHASE', fid: '6fae', enc: efEncPhase, model: efPhaseModel, fields: EF_PHASE_FIELDS },
|
||||
{ name: 'EF.HPPLMN', fid: '6f31', enc: efEncHpplmn, fields: EF_HPPLMN_FIELDS },
|
||||
{ name: 'EF.AD', fid: '6fad', enc: efEncAd, model: efAdModel, fields: EF_AD_FIELDS },
|
||||
{ name: 'EF.SPN', fid: '6f46', enc: efEncSpn, fields: EF_SPN_FIELDS },
|
||||
{ name: 'EF.PLMNsel', fid: '6f30', enc: efEncPlmnOnly, list: EF_PLMN_ONLY_LIST },
|
||||
{ name: 'EF.PLMNwAcT', fid: '6f60', enc: efEncPlmnList, list: EF_PLMN_LIST },
|
||||
{ name: 'EF.OPLMNwAcT', fid: '6f61', enc: efEncPlmnList, list: EF_PLMN_LIST },
|
||||
{ name: 'EF.HPLMNwAcT', fid: '6f62', enc: efEncPlmnList, list: EF_PLMN_LIST },
|
||||
{ name: 'EF.PNN', fid: '6fc5', enc: efEncPnn, items: EF_PNN_ITEMS },
|
||||
{ name: 'EF.FPLMN', fid: '6f7b', enc: efEncPlmnOnly, list: EF_PLMN_ONLY_LIST },
|
||||
{ name: 'EF.EHPLMN', fid: '6fd9', enc: efEncPlmnOnly, list: EF_PLMN_ONLY_LIST },
|
||||
{ name: 'EF.CBMI', fid: '6f45', enc: efEncCbmi, model: efCbmiModel, list: EF_CBMI_LIST },
|
||||
{ name: 'EF.CBMID', fid: '6f48', enc: efEncCbmi, model: efCbmiModel, list: EF_CBMI_LIST },
|
||||
{ name: 'EF.CBMIR', fid: '6f50', enc: efEncCbmir, model: efCbmirModel, list: EF_CBMIR_LIST },
|
||||
{ name: 'EF.ADN', fid: '6f3a', enc: efEncAdnExt(14, 'EXT1'), ext: 'EXT1', fields: EF_ADN_FIELDS },
|
||||
{ name: 'EF.FDN', fid: '6f3b', enc: efEncAdnExt(14, 'EXT2'), ext: 'EXT2', fields: EF_ADN_FIELDS },
|
||||
{ name: 'EF.SDN', fid: '6f49', enc: efEncAdnExt(14, 'EXT3'), ext: 'EXT3', fields: EF_ADN_FIELDS },
|
||||
@@ -24006,12 +24431,19 @@ const EF_ENCODERS = [
|
||||
{ name: 'EF.BDN', fid: '6f4d', enc: efEncAdnExt(15, 'EXT4'), ext: 'EXT4', fields: EF_ADN_FIELDS },
|
||||
];
|
||||
|
||||
// Decoder-only names that share a FID with an editable file: the FID fallback
|
||||
// must not pick the wrong layout (EF.SST is not EF.UST on 6F38 - its 2-bit
|
||||
// SIM layout has no editor yet).
|
||||
const EF_READONLY_NAMES = ['EF.SST'];
|
||||
|
||||
function efFindEncoder(name, fid) {
|
||||
let n = (name || '').toUpperCase();
|
||||
if (n.includes('/')) n = n.split('/').pop();
|
||||
for (const e of EF_ENCODERS) if (e.name.toUpperCase() === n) return e;
|
||||
const f = (fid || '').toLowerCase();
|
||||
if (f) for (const e of EF_ENCODERS) if (e.fid === f) return e;
|
||||
if (f && EF_READONLY_NAMES.indexOf(n) === -1) {
|
||||
for (const e of EF_ENCODERS) if (e.fid === f) return e;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -24081,7 +24513,8 @@ function efFlatten(data) {
|
||||
}
|
||||
arr.forEach((it, i) => {
|
||||
if (Array.isArray(it)) rows.push([label + ' ' + (i + 1), it.map(efPrimitive).join('\u2013')]);
|
||||
else if (it && it.n !== undefined && it.name !== undefined) rows.push(['Service ' + it.n, String(it.name)]);
|
||||
else if (it && it.n !== undefined && it.name !== undefined && it.allocated !== undefined) rows.push(['Service ' + it.n, String(it.name) + ' \u2014 ' + (it.allocated ? 'allocated' : 'not allocated') + ', ' + (it.activated ? 'activated' : 'not activated')]);
|
||||
else if (it && it.n !== undefined && it.name !== undefined) rows.push(['Service ' + it.n, String(it.name)]);
|
||||
else if (it && it.mcc !== undefined) rows.push(['PLMN ' + (i + 1), it.mcc + '-' + it.mnc + (it.access_tech ? ' (' + it.access_tech + ')' : '')]);
|
||||
else if (it && it.aid !== undefined) rows.push(['Application ' + (i + 1), (it.label ? it.label + ' \u2014 ' : '') + it.aid + (it.eap ? ' [' + (it.eap.eap_types || []).join(', ') + ']' : '')]);
|
||||
else if (it && it.link !== undefined && it.tag !== undefined) rows.push([label + ' ' + (i + 1), 'Type ' + it.link + ' \u00b7 ' + (it.kind || ('tag ' + it.tag)) + (it.fid ? ' \u2014 ' + it.fid + (it.sfi ? ' (SFI ' + it.sfi + ')' : '') : (it.raw ? ' \u2014 ' + it.raw : ''))]);
|
||||
@@ -24184,6 +24617,14 @@ function efDecModelValues(dec, hex) {
|
||||
return d;
|
||||
}
|
||||
|
||||
// The decoded values shaped for the form: an encoder entry may carry a
|
||||
// `model(decoded)` hook (PHASE/AD/CBMI/CBMIR map their display names/ids to
|
||||
// the form's values); the default is the decoded object itself.
|
||||
function efDecModelFor(entry, dec, hex) {
|
||||
const values = efDecModelValues(dec, hex);
|
||||
return entry && entry.model ? entry.model(values) : values;
|
||||
}
|
||||
|
||||
// Parse a record whose contents are a TLV list (EF.PNN): known name tags come
|
||||
// back as text (the decoder's own efPnnText), a known hex tag as its value
|
||||
// hex, anything else as {tag, hex, raw: true} preserved verbatim. `orig` is
|
||||
@@ -24217,10 +24658,10 @@ function efDecEditModel(name, fid, content) {
|
||||
mode: 'records',
|
||||
records: (content.records || []).map(r => entry.items
|
||||
? { num: r.num, orig: r.data || '', items: efDecItemsFromHex(entry, r.data || '') }
|
||||
: { num: r.num, orig: r.data || '', values: efDecModelValues(dec, r.data || '') }),
|
||||
: { num: r.num, orig: r.data || '', values: efDecModelFor(entry, dec, r.data || '') }),
|
||||
};
|
||||
}
|
||||
return { entry: entry, mode: 'tr', values: efDecModelValues(dec, (content && content.data) || '') };
|
||||
return { entry: entry, mode: 'tr', values: efDecModelFor(entry, dec, (content && content.data) || '') };
|
||||
}
|
||||
|
||||
// The value of a techs field (the decoder's ', '-joined string) as a list.
|
||||
@@ -24230,6 +24671,14 @@ function efActTechList(value) {
|
||||
.split(',').map(x => x.trim()).filter(Boolean);
|
||||
}
|
||||
|
||||
// The checked keys of a bits field: the decoder's list may hold service
|
||||
// objects ({n,name}) or plain class numbers.
|
||||
function efBitsSelected(value) {
|
||||
const set = new Set();
|
||||
for (const v of (value || [])) set.add(String(v && typeof v === 'object' ? v.n : v));
|
||||
return set;
|
||||
}
|
||||
|
||||
function efDecFieldHtml(f, value) {
|
||||
const key = esc(f.key);
|
||||
let input;
|
||||
@@ -24242,23 +24691,48 @@ function efDecFieldHtml(f, value) {
|
||||
'<label class="flex items-center gap-1"><input type="checkbox" data-tech="' + esc(name) + '"' +
|
||||
(sel.indexOf(name) !== -1 ? ' checked' : '') +
|
||||
' onchange="pysimFsDecRowCheck(this)">' + esc(name) + '</label>').join('') + '</span>';
|
||||
} else if (f.kind === 'bits') {
|
||||
const selected = efBitsSelected(value);
|
||||
const nums = Object.keys(f.items || {}).map(Number).sort((a, b) => a - b);
|
||||
input = '<div class="pysim-dec-bits border border-gray-200 dark:border-slate-700 rounded p-2">' +
|
||||
'<input type="text" class="pysim-dec-bits-filter w-full mb-1 border border-gray-300 dark:border-slate-600 rounded px-1 bg-white dark:bg-slate-800 text-gray-800 dark:text-slate-200" placeholder="' +
|
||||
esc(t('Filter services')) + '" oninput="pysimFsDecBitsFilter(this)">' +
|
||||
'<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(14rem,1fr));max-height:12rem;overflow-y:auto">' +
|
||||
nums.map(n => {
|
||||
const name = String(f.items[n]);
|
||||
return '<label class="pysim-dec-bit-row flex items-center gap-1">' +
|
||||
'<input type="checkbox" data-bit="' + n + '" data-bit-name="' + esc(name) + '"' +
|
||||
(selected.has(String(n)) ? ' checked' : '') + ' onchange="pysimFsDecRowCheck(this)">' +
|
||||
'<span class="font-mono text-gray-500 dark:text-slate-400">' + n + '</span>' +
|
||||
'<span class="break-all">' + esc(t(name)) + '</span></label>';
|
||||
}).join('') + '</div></div>';
|
||||
} else if (f.kind === 'select') {
|
||||
const cur = String(value === undefined || value === null ? '' : value);
|
||||
const opts = (f.options || []).slice();
|
||||
if (cur !== '' && !opts.some(o => String(o.v) === cur)) opts.unshift({ v: cur, l: 'Current', current: true });
|
||||
input = '<select data-key="' + key + '" onchange="pysimFsDecRowCheck(this)" class="border border-gray-300 dark:border-slate-600 rounded px-1 bg-white dark:bg-slate-800 text-gray-800 dark:text-slate-200">' +
|
||||
(f.options || []).map(o => {
|
||||
opts.map(o => {
|
||||
const v = String(o.v);
|
||||
return '<option value="' + esc(v) + '"' + (v === cur ? ' selected' : '') + '>' + esc(t(o.l || v)) + '</option>';
|
||||
const label = o.current ? t('Current') + ' ' + v : t(o.l || v);
|
||||
return '<option value="' + esc(v) + '"' + (v === cur ? ' selected' : '') + '>' + esc(label) + '</option>';
|
||||
}).join('') + '</select>';
|
||||
} else {
|
||||
const max = f.max ? ' maxlength="' + f.max + '"' : '';
|
||||
input = '<input type="text" data-key="' + key + '" value="' +
|
||||
const hexAttr = f.kind === 'hex' ? ' data-hex="1"' : '';
|
||||
input = '<input type="text" data-key="' + key + '"' + hexAttr + ' value="' +
|
||||
esc(value === undefined || value === null ? '' : String(value)) + '"' + max +
|
||||
' oninput="pysimFsDecRowCheck(this)" class="border border-gray-300 dark:border-slate-600 rounded px-1 bg-white dark:bg-slate-800 text-gray-800 dark:text-slate-200">';
|
||||
}
|
||||
const body = '<span class="text-gray-500 dark:text-slate-400">' + esc(t(f.label)) + '</span>' + input;
|
||||
return f.kind === 'techs'
|
||||
? '<div class="pysim-dec-item flex flex-wrap items-center gap-x-2 gap-y-1">' + body + '</div>'
|
||||
: '<label class="pysim-dec-item flex items-center gap-2">' + body + '</label>';
|
||||
const wide = f.kind === 'techs' || f.kind === 'bits';
|
||||
if (!f.hint) {
|
||||
return wide
|
||||
? '<div class="pysim-dec-item flex flex-wrap items-center gap-x-2 gap-y-1">' + body + '</div>'
|
||||
: '<label class="pysim-dec-item flex items-center gap-2">' + body + '</label>';
|
||||
}
|
||||
return '<div class="pysim-dec-item">' +
|
||||
(wide ? body : '<label class="flex items-center gap-2">' + body + '</label>') +
|
||||
'<div class="text-xs text-gray-500 dark:text-slate-400">' + esc(t(f.hint)) + '</div></div>';
|
||||
}
|
||||
|
||||
// One PLMN list row: the item fields + a remove button.
|
||||
@@ -24357,6 +24831,14 @@ function efDecCollectValues(root, fields) {
|
||||
if (cb.checked) techs.push(cb.getAttribute('data-tech'));
|
||||
});
|
||||
out[f.key] = techs.join(', ');
|
||||
} else if (f.kind === 'bits') {
|
||||
const items = [];
|
||||
root.querySelectorAll('[data-bit]').forEach(cb => {
|
||||
if (!cb.checked) return;
|
||||
items.push({ n: parseInt(cb.getAttribute('data-bit'), 10),
|
||||
name: cb.getAttribute('data-bit-name') || '' });
|
||||
});
|
||||
out[f.key] = items;
|
||||
} else {
|
||||
const el = root.querySelector('[data-key="' + f.key + '"]');
|
||||
if (!el) { out[f.key] = f.kind === 'bool' ? false : ''; continue; }
|
||||
@@ -24403,6 +24885,16 @@ function pysimFsDecRowCheck(el) {
|
||||
if (cb) cb.checked = true;
|
||||
}
|
||||
|
||||
// The bits grid's text filter: hide the rows whose label does not match.
|
||||
function pysimFsDecBitsFilter(input) {
|
||||
const box = input && input.closest ? input.closest('.pysim-dec-bits') : null;
|
||||
if (!box) return;
|
||||
const q = String(input.value || '').toLowerCase();
|
||||
box.querySelectorAll('.pysim-dec-bit-row').forEach(row => {
|
||||
row.style.display = row.textContent.toLowerCase().indexOf(q) === -1 ? 'none' : '';
|
||||
});
|
||||
}
|
||||
|
||||
function pysimFsDecAddRow() {
|
||||
if (!pysimFsSelected) return;
|
||||
const node = pysimFsFindNode(pysimFsSelected, pysimFsTreeRoot);
|
||||
@@ -25133,6 +25625,52 @@ const LANG_RU = {
|
||||
'The number continues in EF.EXT3 - editing it is not supported': 'Номер продолжается в EF.EXT3 — редактирование не поддерживается',
|
||||
'The number continues in EF.EXT4 - editing it is not supported': 'Номер продолжается в EF.EXT4 — редактирование не поддерживается',
|
||||
'The number continues in EF.EXT5 - editing it is not supported': 'Номер продолжается в EF.EXT5 — редактирование не поддерживается',
|
||||
'Services': 'Сервисы',
|
||||
'Filter services': 'Фильтр сервисов',
|
||||
'Access classes': 'Классы доступа',
|
||||
'Phase': 'Фаза',
|
||||
'Interval': 'Интервал',
|
||||
'Operation mode': 'Режим работы',
|
||||
'Additional info': 'Дополнительная информация',
|
||||
'MNC length': 'Длина MNC',
|
||||
'Current': 'Текущее',
|
||||
'Add message ID': 'Добавить ID сообщения',
|
||||
'Add range': 'Добавить диапазон',
|
||||
'Message ID': 'ID сообщения',
|
||||
'Low': 'Нижняя граница',
|
||||
'High': 'Верхняя граница',
|
||||
'phase 1': 'фаза 1',
|
||||
'phase 2': 'фаза 2',
|
||||
'phase 2 and PROFILE DOWNLOAD required': 'фаза 2 и требуется PROFILE DOWNLOAD',
|
||||
'normal operation': 'нормальная работа',
|
||||
'type approval': 'сертификационное испытание',
|
||||
'normal operation + specific facilities': 'нормальная работа + специальные возможности',
|
||||
'type approval + specific facilities': 'сертификационное испытание + специальные возможности',
|
||||
'maintenance (off line)': 'обслуживание (off line)',
|
||||
'cell test operation': 'тестирование соты',
|
||||
'00 = no higher-priority PLMN search; 01-FF = n minutes on GERAN/UTRAN (01 = n, 02 = 2n, ...). NB-IoT/EC-GSM-IoT/eMTC-M1 read the same byte in hours (01 = n hours (2 h), 02-28 = 2n hours, 29-50 = 4n-80 hours).': '00 — поиск PLMN с более высоким приоритетом не выполняется; 01-FF — n минут в GERAN/UTRAN (01 = n, 02 = 2n, ...). NB-IoT/EC-GSM-IoT/eMTC-M1 читают тот же байт в часах (01 = n часов (2 ч), 02-28 = 2n часов, 29-50 = 4n-80 часов).',
|
||||
'Unknown service': 'Неизвестный сервис',
|
||||
'The service table is too short for service': 'Таблица сервисов слишком коротка для сервиса',
|
||||
'The record is too short for the ACC layout': 'Запись слишком короткая для формата ACC',
|
||||
'Unknown access class': 'Неизвестный класс доступа',
|
||||
'The phase must be a hex byte': 'Фаза должна быть hex-байтом',
|
||||
'The interval must be a hex byte': 'Интервал должен быть hex-байтом',
|
||||
'The operation mode must be a hex byte': 'Режим работы должен быть hex-байтом',
|
||||
'The additional information must be 4 hex digits': 'Дополнительная информация должна состоять из 4 hex-цифр',
|
||||
'The MNC length must be 0, 2 or 3 digits': 'Длина MNC должна быть 0, 2 или 3 цифры',
|
||||
'The record has no MNC length byte': 'В записи нет байта длины MNC',
|
||||
'This file is a service table, not an IMSI': 'Это файл таблицы сервисов, а не IMSI',
|
||||
'The record is too short for the AD layout': 'Запись слишком короткая для формата AD',
|
||||
'The ICCID must be digits': 'ICCID должен состоять из цифр',
|
||||
'The ICCID is too long': 'ICCID слишком длинный',
|
||||
'The ICCID does not fit in the file': 'ICCID не помещается в файл',
|
||||
'The IMSI must be 6-15 digits': 'IMSI должен состоять из 6-15 цифр',
|
||||
'The IMSI does not fit in the file': 'IMSI не помещается в файл',
|
||||
'The message ID must be 1-4 hex digits': 'ID сообщения должен состоять из 1-4 hex-цифр',
|
||||
'The range bounds must be 1-4 hex digits': 'Границы диапазона должны состоять из 1-4 hex-цифр',
|
||||
'Write cancelled': 'Запись отменена',
|
||||
'Writing the ICCID changes the card identity and may break network registration. Continue?': 'Запись ICCID меняет идентичность карты и может нарушить регистрацию в сети. Продолжить?',
|
||||
'Writing the IMSI changes the card identity and may break network registration. Continue?': 'Запись IMSI меняет идентичность карты и может нарушить регистрацию в сети. Продолжить?',
|
||||
'Refresh': 'Обновить',
|
||||
'Last refresh': 'Последнее обновление',
|
||||
'Save': 'Сохранить',
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
const CACHE = 'simple-v466';
|
||||
const CACHE = 'simple-v467';
|
||||
const URLS = [
|
||||
'index.html',
|
||||
'help.html',
|
||||
|
||||
@@ -24,9 +24,9 @@ function extractFunc(src, name) {
|
||||
const FNS = [
|
||||
'efBytes', 'efHex', 'efSwapNibbles', 'efAllFf', 'efRstripFf', 'efTlv', 'efUcs2Be',
|
||||
'efGsm7Octets', 'efAnnexA', 'efBcdAddress', 'efBcdAddressLen', 'efPlmn', 'efPlmnAct', 'efPnnText',
|
||||
'efDecIccid', 'efDecImsi', 'efDecLi', 'efDecServiceTable', 'efDecPlmnList', 'efDecFplmn', 'efDecPlmnWact',
|
||||
'efDecIccid', 'efDecImsi', 'efDecLi', 'efDecServiceTable', 'efDecSst', 'efDecPlmnList', 'efDecFplmn', 'efDecPlmnWact',
|
||||
'efDecSpn', 'efDecLoci', 'efDecPsLoci', 'efDecEpsLoci', 'efDecEpsNsc', 'efDecKc', 'efDecAcc',
|
||||
'efDecPhase', 'efDecCbmi', 'efDecCbmir', 'efDecEcc', 'efDecOpl', 'efDecAd', 'efDecAcl',
|
||||
'efDecPhase', 'efDecCbmi', 'efDecCbmir', 'efDecHpplmn', 'efDecEcc', 'efDecOpl', 'efDecAd', 'efDecAcl',
|
||||
'efDecSmsp', 'efDecSpdi', 'efDecNai', 'efDecDir', 'efDecArr', 'efDecPnn', 'efDecAdn',
|
||||
'pinKeyRefName', 'efArrAmLabel', 'efArrSc', 'efArrAmDo',
|
||||
'efDecExt1', 'efDecSms', 'efDecSume', 'efPbrHex', 'efDecPbr', 'efFindDecoder', 'efFidFromPath', 'efDecodeBytes',
|
||||
@@ -42,7 +42,7 @@ for (const f of FNS) code += extractFunc(html, f) + '\n';
|
||||
for (const c of ['GSM7_ALPHABET', 'GSM7_EXT_MAP', 'FCP_ARR_AM_BITS']) {
|
||||
code += html.match(new RegExp('const ' + c + ' = [\\s\\S]*?\\n\\];'))[0].replace('const ', 'var ') + '\n';
|
||||
}
|
||||
for (const c of ['EF_UST_SERVICES', 'EF_SST_SERVICES', 'EF_IST_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(/const EF_FIELD_LABELS = \{[\s\S]*?\n\};/)[0].replace('const ', 'var ') + '\n';
|
||||
@@ -103,7 +103,9 @@ test('EF decoders match the pinned spec / pySim test vectors', () => {
|
||||
assert.deepStrictEqual(efDecLi(efBytes('FFFF')), { languages: [] });
|
||||
assert.deepStrictEqual(efDecSpn(efBytes('0147534d2d52204348ffffffffffffffff')), { show_in_hplmn: true, hide_in_oplmn: false, name: 'GSM-R CH' });
|
||||
assert.deepStrictEqual(efDecAcc(efBytes('0000')), { access_classes: [] });
|
||||
assert.deepStrictEqual(efDecAcc(efBytes('8000')), { access_classes: [0] });
|
||||
assert.deepStrictEqual(efDecAcc(efBytes('8000')), { access_classes: [15] });
|
||||
assert.deepStrictEqual(efDecAcc(efBytes('0001')), { access_classes: [0] });
|
||||
assert.deepStrictEqual(efDecAcc(efBytes('8001')), { access_classes: [15, 0] });
|
||||
assert.deepStrictEqual(efDecEcc(efBytes('19f101')), { number: '911', esc: '0x01' });
|
||||
assert.deepStrictEqual(efDecKc(efBytes('837d783609a3858f05')), { kc: '837D783609A3858F', ksi: 0 });
|
||||
assert.deepStrictEqual(efDecPlmnList(efBytes('22F860')), { plmns: [{ mcc: '228', mnc: '06' }] });
|
||||
@@ -115,7 +117,7 @@ test('EF decoders match the pinned spec / pySim test vectors', () => {
|
||||
assert.strictEqual(efFindDecoder('EF.FPLMN', null).fn, efDecFplmn);
|
||||
assert.deepStrictEqual(efDecCbmi(efBytes('0010FFFF0020')), { message_ids: [16, 32] });
|
||||
assert.deepStrictEqual(efDecCbmir(efBytes('0000FFFEFFFF')), { ranges: [['0000', 'FFFE']] });
|
||||
assert.deepStrictEqual(efDecPhase(efBytes('03')), { phase: 'phase 2 and higher' });
|
||||
assert.deepStrictEqual(efDecPhase(efBytes('03')), { phase: 'phase 2 and PROFILE DOWNLOAD required' });
|
||||
assert.deepStrictEqual(efDecNai(efBytes('8021696d732e6d6e633030302e6d63633733382e336770706e6574776f726b2e6f7267')),
|
||||
{ text: 'ims.mnc000.mcc738.3gppnetwork.org' });
|
||||
assert.deepStrictEqual(efDecImsi(efBytes('FFFF')), { empty: true });
|
||||
@@ -290,6 +292,29 @@ test('service tables: UST service bits and IST fallback for 6F07', () => {
|
||||
assert.deepStrictEqual(ist.services, [{ n: 1, name: 'P-CSCF address' }]);
|
||||
});
|
||||
|
||||
test('EF.SST is the SIM 2-bit service table (allocated/activated)', () => {
|
||||
// TS 51.011 10.3.7: service 2 ADN -> byte 0, pair start bit 2 (0x04
|
||||
// allocated, 0x08 activated)
|
||||
assert.deepStrictEqual(efDecSst(efBytes('0C')), { services: [
|
||||
{ n: 2, name: 'Abbreviated Dialling Numbers (ADN)', allocated: true, activated: true },
|
||||
] });
|
||||
assert.deepStrictEqual(efDecSst(efBytes('04')), { services: [
|
||||
{ n: 2, name: 'Abbreviated Dialling Numbers (ADN)', allocated: true, activated: false },
|
||||
] });
|
||||
assert.deepStrictEqual(efDecSst(efBytes('08')), { services: [
|
||||
{ n: 2, name: 'Abbreviated Dialling Numbers (ADN)', allocated: false, activated: true },
|
||||
] });
|
||||
// byte 1 pair 3 (bits 6/7) = service 4 SMS; byte 0 pair 1 = service 2
|
||||
assert.deepStrictEqual(efDecSst(efBytes('C0')), { services: [
|
||||
{ n: 4, name: 'Short Message Storage (SMS)', allocated: true, activated: true },
|
||||
] });
|
||||
assert.deepStrictEqual(efDecSst(efBytes('00')), { services: [] });
|
||||
assert.deepStrictEqual(efFindDecoder('EF.SST', null).fn(efBytes('0C')).services[0].activated, true);
|
||||
// the read-only view shows both bits
|
||||
assert.ok(efFlatten(efDecSst(efBytes('0C'))).some(r =>
|
||||
r[0] === 'Service 2' && r[1].includes('allocated') && r[1].includes('activated')));
|
||||
});
|
||||
|
||||
test('decoder registry resolves by name first, then FID; unknown returns null', () => {
|
||||
assert.strictEqual(efFindDecoder('EF.IMSI', null).fid, '6f07');
|
||||
assert.strictEqual(efFindDecoder(null, '2fe2').name, 'EF.ICCID');
|
||||
|
||||
@@ -48,15 +48,16 @@ const FNS = [
|
||||
'efDecSpn', 'efDecPlmnWact', 'efDecAdn', 'efDecPnn', 'efFindDecoder',
|
||||
'efDecodeBytes', 'efTlv', 'efPnnText', 'efDecItemsFromHex',
|
||||
'efGsm7PlainText', 'efEncTextBytes', 'efPlmnEnc', 'efActEnc', 'efEncSpn',
|
||||
'efEncPlmnList', 'efPlmnRowOrig', 'efEncAdn', 'efEncAdnExt', 'efPnnNameBytes', 'efPnnHexBytes', 'efPnnOrigTlv', 'efEncPnn',
|
||||
'efEncPlmnList', 'efListRows', 'efPlmnRowOrig', 'efEncAdn', 'efEncAdnExt', 'efPnnNameBytes', 'efPnnHexBytes', 'efPnnOrigTlv', 'efEncPnn',
|
||||
'efFindEncoder', 'efHasEncoder', 'gsm7Decode', 'gsm7TextToSeptets',
|
||||
'gsm7Encode', 'efEncError',
|
||||
'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',
|
||||
];
|
||||
|
||||
let code = '';
|
||||
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',
|
||||
'EF_ADN_FIELDS', 'EF_PLMN_LIST', 'EF_PNN_ITEMS', '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_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_ENCODERS']) {
|
||||
code += extractConst(html, c);
|
||||
}
|
||||
eval(code);
|
||||
@@ -211,7 +212,7 @@ test('EF.SPN and the ADN family share the encoder registry with the PLMN list',
|
||||
assert.strictEqual(efFindEncoder('EF.ALIAS', '6f60').enc, efEncPlmnList);
|
||||
assert.strictEqual(efFindEncoder('EF.NOPE', 'abcd'), null);
|
||||
assert.strictEqual(efHasEncoder('EF.ADN', null), true);
|
||||
assert.strictEqual(efHasEncoder('EF.IMSI', null), false);
|
||||
assert.strictEqual(efHasEncoder('EF.LOCI', null), false);
|
||||
// one shared schema for the ADN family and one for the three PLMN files
|
||||
assert.strictEqual(efFindEncoder('EF.ADN', null).fields, efFindEncoder('EF.BDN', null).fields);
|
||||
assert.strictEqual(efFindEncoder('EF.PLMNwAcT', null).list, efFindEncoder('EF.HPLMNwAcT', null).list);
|
||||
@@ -429,14 +430,36 @@ test('F14: the encoder messages go through t() (unknown technology, too many row
|
||||
});
|
||||
|
||||
test('unchanged content re-encodes byte-identically (all fixtures)', () => {
|
||||
const tables = { 'EF.UST': EF_UST_SERVICES, 'EF.IST': EF_IST_SERVICES,
|
||||
'EF.EST': EF_EST_SERVICES };
|
||||
const decode = (name, hex) => {
|
||||
const entry = efFindEncoder(name, null);
|
||||
const bytes = efBytes(hex);
|
||||
if (name === 'EF.SPN') return efDecSpn(bytes);
|
||||
if (name === 'EF.PLMNwAcT') return efDecPlmnWact(bytes).plmns;
|
||||
if (name === 'EF.BDN') return efDecAdn(bytes, 15);
|
||||
if (name === 'EF.ADN') return efDecAdn(bytes);
|
||||
if (name === 'EF.PNN') return efDecItemsFromHex(efFindEncoder('EF.PNN', null), hex);
|
||||
throw new Error('no decoder for ' + name);
|
||||
let dec;
|
||||
if (name === 'EF.SPN') dec = efDecSpn(bytes);
|
||||
else if (tables[name]) {
|
||||
// the form renders only the table's services; other set bits stay
|
||||
// in the original bytes
|
||||
dec = { services: efDecServiceTable(bytes, tables[name]).services
|
||||
.filter(s => Object.prototype.hasOwnProperty.call(tables[name], s.n)) };
|
||||
} else if (name === 'EF.ACC') dec = efDecAcc(bytes);
|
||||
else if (name === 'EF.PHASE') dec = efDecPhase(bytes);
|
||||
else if (name === 'EF.HPPLMN') dec = efDecHpplmn(bytes);
|
||||
else if (name === 'EF.AD') dec = efDecAd(bytes);
|
||||
else if (name === 'EF.PLMNsel' || name === 'EF.EHPLMN') dec = efDecPlmnList(bytes);
|
||||
else if (name === 'EF.FPLMN') dec = efDecFplmn(bytes);
|
||||
else if (name === 'EF.ICCID') dec = efDecIccid(bytes);
|
||||
else if (name === 'EF.IMSI') dec = efDecImsi(bytes);
|
||||
else if (name === 'EF.CBMI' || name === 'EF.CBMID') dec = efDecCbmi(bytes);
|
||||
else if (name === 'EF.CBMIR') dec = efDecCbmir(bytes);
|
||||
else if (name === 'EF.PLMNwAcT') dec = efDecPlmnWact(bytes).plmns;
|
||||
else if (name === 'EF.BDN') dec = efDecAdn(bytes, 15);
|
||||
else if (name === 'EF.ADN') dec = efDecAdn(bytes);
|
||||
else if (name === 'EF.PNN') dec = efDecItemsFromHex(efFindEncoder('EF.PNN', null), hex);
|
||||
else throw new Error('no decoder for ' + name);
|
||||
// the model's empty/raw marker yields the form's empty value set
|
||||
if (dec && (dec.empty === true || (dec.raw !== undefined && Object.keys(dec).length === 1))) dec = {};
|
||||
return entry.model ? entry.model(dec) : dec;
|
||||
};
|
||||
const cases = [
|
||||
{ name: 'EF.SPN', fid: '6f46', hex: SPN_ORIG },
|
||||
@@ -456,6 +479,38 @@ test('unchanged content re-encodes byte-identically (all fixtures)', () => {
|
||||
{ name: 'EF.PNN', fid: '6fc5', hex: PNN_2 },
|
||||
{ name: 'EF.PNN', fid: '6fc5', hex: '800400410042' + 'FF'.repeat(16) },
|
||||
{ name: 'EF.PNN', fid: '6fc5', hex: '430a82d432bbbc7eb75de4325a02aabb' + 'ff'.repeat(14) },
|
||||
// wave 2
|
||||
{ name: 'EF.UST', fid: '6f38', hex: UST_ONE },
|
||||
{ name: 'EF.UST', fid: '6f38', hex: UST_MULTI },
|
||||
{ name: 'EF.UST', fid: '6f38', hex: UST_UNMODELLED },
|
||||
{ name: 'EF.UST', fid: '6f38', hex: 'FF'.repeat(19) },
|
||||
{ name: 'EF.IST', fid: '6f07', hex: '01' },
|
||||
{ name: 'EF.EST', fid: '6f56', hex: '07' },
|
||||
{ name: 'EF.EST', fid: '6f56', hex: EST_FF },
|
||||
{ name: 'EF.ACC', fid: '6f78', hex: ACC_ONE },
|
||||
{ name: 'EF.PHASE', fid: '6fae', hex: '00' },
|
||||
{ name: 'EF.PHASE', fid: '6fae', hex: '02' },
|
||||
{ name: 'EF.PHASE', fid: '6fae', hex: '03' },
|
||||
{ name: 'EF.PHASE', fid: '6fae', hex: '0F' },
|
||||
{ name: 'EF.HPPLMN', fid: '6f31', hex: '00' },
|
||||
{ name: 'EF.HPPLMN', fid: '6f31', hex: '2A' },
|
||||
{ name: 'EF.AD', fid: '6fad', hex: AD_HEX },
|
||||
{ name: 'EF.AD', fid: '6fad', hex: '55' + '0000' + '03' + 'AABB' },
|
||||
{ name: 'EF.AD', fid: '6fad', hex: 'FF'.repeat(6) },
|
||||
{ name: 'EF.AD', fid: '6fad', hex: '00' + '0000' },
|
||||
{ name: 'EF.AD', fid: '6fad', hex: '00' + '0000' + '00' + 'AABB' },
|
||||
{ name: 'EF.PLMNsel', fid: '6f30', hex: PLMN_ONLY },
|
||||
{ name: 'EF.FPLMN', fid: '6f7b', hex: FPLMN_HOLES },
|
||||
{ name: 'EF.EHPLMN', fid: '6fd9', hex: PLMN_ONLY },
|
||||
{ name: 'EF.ICCID', fid: '2fe2', hex: ICCID_HEX },
|
||||
{ name: 'EF.ICCID', fid: '2fe2', hex: 'FF'.repeat(10) },
|
||||
{ name: 'EF.IMSI', fid: '6f07', hex: IMSI_HEX },
|
||||
{ name: 'EF.IMSI', fid: '6f07', hex: '0811325476981032F4' },
|
||||
{ name: 'EF.IMSI', fid: '6f07', hex: 'FF'.repeat(9) },
|
||||
{ name: 'EF.CBMI', fid: '6f45', hex: CBMI_HEX },
|
||||
{ name: 'EF.CBMID', fid: '6f48', hex: CBMI_HEX },
|
||||
{ name: 'EF.CBMIR', fid: '6f50', hex: CBMIR_HEX },
|
||||
{ name: 'EF.CBMIR', fid: '6f50', hex: 'FF'.repeat(4) },
|
||||
];
|
||||
for (const c of cases) {
|
||||
const entry = efFindEncoder(c.name, c.fid);
|
||||
@@ -511,3 +566,205 @@ test('F8: the ADN-family entries name their extension EF and use it in refusals'
|
||||
// the unchanged number still copies the extension slots verbatim
|
||||
assert.strictEqual(efEncAdn(efDecAdn(efBytes(fdn)), fdn, 14, 'EXT2'), fdn.toUpperCase());
|
||||
});
|
||||
|
||||
// Wave 2 fixtures.
|
||||
const UST_ONE = '00000010'; // service 29
|
||||
const UST_MULTI = '010204'; // services 1, 10, 19
|
||||
const UST_UNMODELLED = '000000' + '10' + '00'.repeat(14) + 'FC'; // 29 + bits 147-152
|
||||
const EST_FF = 'FFFF';
|
||||
const ACC_ONE = '8001'; // classes 15 and 0
|
||||
const AD_HEX = '00' + '0000' + '03' + 'AABB'; // op normal, info 0000, MNC 3, RFU
|
||||
const PLMN_ONLY = '62F21022F860';
|
||||
const FPLMN_HOLES = '22F860FFFFFF62F210';
|
||||
const ICCID_HEX = '988812010000400310f0';
|
||||
const IMSI_HEX = '082982608200002080';
|
||||
const CBMI_HEX = '0010FFFF0020';
|
||||
const CBMIR_HEX = '0000FFFE';
|
||||
|
||||
test('service tables: encode/decode round-trips and unmodelled bits', () => {
|
||||
const label = n => EF_UST_SERVICES[n] || 'RFU';
|
||||
assert.deepStrictEqual(efDecServiceTable(efBytes(UST_MULTI), EF_UST_SERVICES).services,
|
||||
[{ n: 1, name: label(1) }, { n: 10, name: label(10) }, { n: 19, name: label(19) }]);
|
||||
for (const [name, hex] of [['EF.UST', UST_ONE], ['EF.UST', UST_MULTI],
|
||||
['EF.IST', '01'], ['EF.EST', '07']]) {
|
||||
const entry = efFindEncoder(name, null);
|
||||
const table = name === 'EF.UST' ? EF_UST_SERVICES
|
||||
: (name === 'EF.IST' ? EF_IST_SERVICES : EF_EST_SERVICES);
|
||||
const services = efDecServiceTable(efBytes(hex), table).services;
|
||||
assert.strictEqual(entry.enc({ services: services }, hex), hex, name + ' ' + hex);
|
||||
}
|
||||
// EF.SST has no editor (its 2-bit SIM layout needs a dedicated UI)
|
||||
assert.strictEqual(efFindEncoder('EF.SST', '6f38'), null);
|
||||
assert.strictEqual(efHasEncoder('EF.SST', '6f38'), false);
|
||||
assert.strictEqual(efFindEncoder('EF.UST', '6f38').name, 'EF.UST');
|
||||
// EST defines services 1-3; all-FF keeps the other bits too
|
||||
assert.deepStrictEqual(efDecServiceTable(efBytes('07'), EF_EST_SERVICES).services,
|
||||
[{ n: 1, name: 'Fixed Dialling Numbers (FDN)' },
|
||||
{ n: 2, name: 'Barred Dialling Numbers (BDN)' },
|
||||
{ n: 3, name: 'APN Control List (ACL)' }]);
|
||||
assert.strictEqual(efFindEncoder('EF.EST', null).enc(
|
||||
{ services: efDecServiceTable(efBytes(EST_FF), EF_EST_SERVICES).services
|
||||
.filter(s => Object.prototype.hasOwnProperty.call(EF_EST_SERVICES, s.n)) }, EST_FF), EST_FF);
|
||||
// service bits outside the table are preserved from the original bytes
|
||||
const entry = efFindEncoder('EF.UST', null);
|
||||
const unmodelled = efDecServiceTable(efBytes(UST_UNMODELLED), EF_UST_SERVICES).services
|
||||
.filter(s => Object.prototype.hasOwnProperty.call(EF_UST_SERVICES, s.n));
|
||||
assert.strictEqual(entry.enc({ services: unmodelled }, UST_UNMODELLED), UST_UNMODELLED);
|
||||
// unchecking everything clears the table bits but keeps the unmodelled byte
|
||||
assert.strictEqual(entry.enc({ services: [] }, UST_UNMODELLED),
|
||||
'000000' + '00' + '00'.repeat(14) + 'FC');
|
||||
// length is kept
|
||||
assert.strictEqual(entry.enc({ services: efDecServiceTable(efBytes('00000010' + '00'), EF_UST_SERVICES).services },
|
||||
'0000001000'), '0000001000');
|
||||
});
|
||||
|
||||
test('service tables: unknown services and a too-short table are refused', () => {
|
||||
assert.throws(() => efEncServiceTable({ services: [{ n: 147, name: 'RFU' }] },
|
||||
'FF'.repeat(19), EF_UST_SERVICES), /Unknown service/);
|
||||
assert.throws(() => efEncServiceTable({ services: [{ n: 9, name: 'MSISDN' }] },
|
||||
'00', EF_UST_SERVICES), /too short for service/);
|
||||
assert.throws(() => efEncServiceTable({ services: [{ n: 0 }] }, '00', EF_UST_SERVICES), /Unknown service/);
|
||||
});
|
||||
|
||||
test('F-ACC: the class numbering and the ACC10/encoder rules', () => {
|
||||
assert.deepStrictEqual(efDecAcc(efBytes('8000')), { access_classes: [15] });
|
||||
assert.deepStrictEqual(efDecAcc(efBytes('0001')), { access_classes: [0] });
|
||||
assert.deepStrictEqual(efDecAcc(efBytes('8001')), { access_classes: [15, 0] });
|
||||
assert.strictEqual(efEncAcc({ access_classes: [{ n: 15 }] }, '0000'), '8000');
|
||||
assert.strictEqual(efEncAcc({ access_classes: [{ n: 0 }] }, '0000'), '0001');
|
||||
assert.strictEqual(efEncAcc({ access_classes: efDecAcc(efBytes(ACC_ONE)).access_classes }, ACC_ONE), ACC_ONE);
|
||||
// ACC 10 is not editable but the encoder always forces it to 0
|
||||
assert.strictEqual(efEncAcc({ access_classes: [{ n: 10 }] }, '0000'), '0000');
|
||||
assert.strictEqual(efEncAcc({ access_classes: [{ n: 15 }, { n: 10 }] }, '0000'), '8000');
|
||||
assert.throws(() => efEncAcc({ access_classes: [{ n: 16 }] }, '0000'), /Unknown access class/);
|
||||
assert.throws(() => efEncAcc({ access_classes: [] }, '00'), /too short/);
|
||||
});
|
||||
|
||||
test('PHASE: known and unknown values round-trip through the model', () => {
|
||||
for (const hex of ['00', '02', '03', '0F']) {
|
||||
const model = efPhaseModel(efDecPhase(efBytes(hex)));
|
||||
assert.strictEqual(efEncPhase(model, hex), hex, hex);
|
||||
}
|
||||
assert.deepStrictEqual(efPhaseModel(efDecPhase(efBytes('03'))), { phase_hex: '03' });
|
||||
assert.deepStrictEqual(efPhaseModel(efDecPhase(efBytes('0F'))), { phase_hex: '0F' });
|
||||
assert.strictEqual(efDecPhase(efBytes('0F')).phase, '0x0F');
|
||||
assert.throws(() => efEncPhase({ phase_hex: 'ZZ' }, '00'), /hex byte/);
|
||||
});
|
||||
|
||||
test('HPPLMN: the raw interval byte round-trips', () => {
|
||||
assert.deepStrictEqual(efDecHpplmn(efBytes('2A')), { interval: '0x2A' });
|
||||
for (const hex of ['00', '2A', 'FF']) {
|
||||
assert.strictEqual(efEncHpplmn(efDecHpplmn(efBytes(hex)), hex), hex, hex);
|
||||
}
|
||||
assert.strictEqual(efEncHpplmn({ interval: '2A' }, '2A'), '2A');
|
||||
assert.throws(() => efEncHpplmn({ interval: 'XYZ' }, '00'), /hex byte/);
|
||||
});
|
||||
|
||||
test('AD: the whole-byte operation mode, additional info, MNC length and RFU tail', () => {
|
||||
const dec = efDecAd(efBytes(AD_HEX));
|
||||
assert.deepStrictEqual(dec, { op_mode: 'normal operation', additional_info: '0000', mnc_length: 3 });
|
||||
assert.deepStrictEqual(efAdModel(dec), { op_mode: '00', additional_info: '0000', mnc_length: '3' });
|
||||
assert.strictEqual(efEncAd(efAdModel(dec), AD_HEX), AD_HEX);
|
||||
for (const [op, name] of [['80', 'type approval'], ['01', 'normal operation + specific facilities'],
|
||||
['81', 'type approval + specific facilities'], ['02', 'maintenance (off line)'],
|
||||
['04', 'cell test operation']]) {
|
||||
const hex = op + '0000' + '03';
|
||||
assert.strictEqual(efDecAd(efBytes(hex)).op_mode, name, op);
|
||||
assert.strictEqual(efEncAd(efAdModel(efDecAd(efBytes(hex))), hex), hex, op);
|
||||
}
|
||||
// an edited op mode / info / MNC keeps the RFU tail
|
||||
assert.strictEqual(efEncAd({ op_mode: '80', additional_info: '1234', mnc_length: '2' }, AD_HEX),
|
||||
'80123402AABB');
|
||||
// the SIM 3-byte form (no MNC-length byte) and the valid MNC length 0
|
||||
assert.strictEqual(efEncAd({ op_mode: '00', additional_info: '1234' }, '00' + '0000'), '001234');
|
||||
assert.strictEqual(efEncAd({ op_mode: '00', additional_info: '1234', mnc_length: '' }, '00' + '0000'), '001234');
|
||||
assert.throws(() => efEncAd({ op_mode: '00', additional_info: '1234', mnc_length: '2' }, '00' + '0000'),
|
||||
/no MNC length byte/);
|
||||
assert.strictEqual(efEncAd({ op_mode: '00', additional_info: '0000', mnc_length: '0' }, AD_HEX),
|
||||
'000000' + '00' + 'AABB');
|
||||
// an unknown op byte is shown and written back
|
||||
const unk = '55' + '0000' + '03' + 'AABB';
|
||||
assert.strictEqual(efDecAd(efBytes(unk)).op_mode, '0x55');
|
||||
assert.strictEqual(efAdModel(efDecAd(efBytes(unk))).op_mode, '55');
|
||||
assert.strictEqual(efEncAd(efAdModel(efDecAd(efBytes(unk))), unk), unk);
|
||||
// an all-FF record stays all-FF (the model is empty)
|
||||
assert.strictEqual(efEncAd(efAdModel({}), 'FF'.repeat(6)), 'FF'.repeat(6));
|
||||
assert.throws(() => efEncAd({ op_mode: 'ZZ' }, AD_HEX), /hex byte/);
|
||||
assert.throws(() => efEncAd({ op_mode: '00', additional_info: '123' }, AD_HEX), /4 hex digits/);
|
||||
assert.throws(() => efEncAd({ op_mode: '00', mnc_length: '4' }, AD_HEX), /0, 2 or 3/);
|
||||
assert.throws(() => efEncAd({ op_mode: '00' }, '00'), /too short/);
|
||||
});
|
||||
|
||||
test('PLMN-only lists: round-trip, holes, add/remove and capacity', () => {
|
||||
assert.deepStrictEqual(efDecPlmnList(efBytes(PLMN_ONLY)).plmns,
|
||||
[{ mcc: '262', mnc: '01' }, { mcc: '228', mnc: '06' }]);
|
||||
assert.strictEqual(efEncPlmnOnly({ plmns: efDecPlmnList(efBytes(PLMN_ONLY)).plmns }, PLMN_ONLY), PLMN_ONLY);
|
||||
// FPLMN FFFFFF holes anywhere are kept for an unchanged list
|
||||
assert.deepStrictEqual(efDecFplmn(efBytes(FPLMN_HOLES)).plmns,
|
||||
[{ mcc: '228', mnc: '06' }, { mcc: '262', mnc: '01' }]);
|
||||
assert.strictEqual(efEncPlmnOnly({ plmns: efDecFplmn(efBytes(FPLMN_HOLES)).plmns }, FPLMN_HOLES), FPLMN_HOLES);
|
||||
// an edit compacts and FF-pads
|
||||
assert.strictEqual(efEncPlmnOnly({ plmns: [{ mcc: '262', mnc: '01' }] }, FPLMN_HOLES),
|
||||
'62F210' + 'FF'.repeat(6));
|
||||
assert.strictEqual(efEncPlmnOnly({ plmns: [
|
||||
{ mcc: '262', mnc: '01' }, { mcc: '228', mnc: '06' }, { mcc: '250', mnc: '99' },
|
||||
] }, '62F21022F860' + 'FFFFFF'), '62F21022F86052F099');
|
||||
assert.throws(() => efEncPlmnOnly({ plmns: [{ mcc: '262', mnc: '01' }, { mcc: '228', mnc: '06' }] },
|
||||
'62F210'), /Too many entries/);
|
||||
assert.throws(() => efEncPlmnOnly({ plmns: [{ mcc: '26', mnc: '01' }] }, '62F210'), /MCC/);
|
||||
});
|
||||
|
||||
test('ICCID / IMSI: BCD round-trips, padding and refusals', () => {
|
||||
assert.deepStrictEqual(efDecIccid(efBytes(ICCID_HEX)), { iccid: '8988211000000430010' });
|
||||
assert.strictEqual(efEncIccid({ iccid: '8988211000000430010' }, ICCID_HEX), ICCID_HEX.toUpperCase());
|
||||
// odd digit count -> trailing F nibble
|
||||
assert.strictEqual(efEncIccid({ iccid: '12345' }, 'FF'.repeat(10)), '2143F5' + 'FF'.repeat(7));
|
||||
assert.strictEqual(efEncIccid({}, 'FF'.repeat(10)), 'FF'.repeat(10));
|
||||
assert.throws(() => efEncIccid({ iccid: '12A' }, 'FF'.repeat(10)), /digits/);
|
||||
assert.throws(() => efEncIccid({ iccid: '1'.repeat(21) }, 'FF'.repeat(10)), /too long/);
|
||||
assert.throws(() => efEncIccid({ iccid: '1'.repeat(20) }, 'FF'.repeat(9)), /does not fit/);
|
||||
|
||||
assert.deepStrictEqual(efDecImsi(efBytes(IMSI_HEX)), { imsi: '228062800000208' });
|
||||
assert.strictEqual(efEncImsi({ imsi: '228062800000208' }, IMSI_HEX), IMSI_HEX);
|
||||
// 14 digits (even): the type/parity nibble is 1 and the body is F-padded
|
||||
assert.strictEqual(efEncImsi({ imsi: '12345678901234' }, 'FF'.repeat(9)), '0811325476981032F4');
|
||||
assert.deepStrictEqual(efDecImsi(efBytes('0811325476981032F4')), { imsi: '12345678901234' });
|
||||
assert.strictEqual(efEncImsi({ imsi: '123456789012345' }, 'FF'.repeat(9)), '081932547698103254');
|
||||
assert.deepStrictEqual(efDecImsi(efBytes('081932547698103254')), { imsi: '123456789012345' });
|
||||
// 6F07 is also EF.IST: a short file is refused, not overwritten
|
||||
assert.throws(() => efEncImsi({ imsi: '228062800000208' }, '01'), /service table, not an IMSI/);
|
||||
assert.throws(() => efEncImsi({}, '01'), /service table, not an IMSI/);
|
||||
assert.strictEqual(efEncImsi({}, 'FF'.repeat(9)), 'FF'.repeat(9));
|
||||
assert.throws(() => efEncImsi({ imsi: '12345' }, 'FF'.repeat(9)), /6-15/);
|
||||
assert.throws(() => efEncImsi({ imsi: '1'.repeat(16) }, 'FF'.repeat(9)), /6-15/);
|
||||
assert.throws(() => efEncImsi({ imsi: '228062800000208' }, 'FF'.repeat(8)), /does not fit/);
|
||||
});
|
||||
|
||||
test('CBMI / CBMIR: message ids and ranges round-trip with FF padding', () => {
|
||||
assert.deepStrictEqual(efDecCbmi(efBytes(CBMI_HEX)), { message_ids: [16, 32] });
|
||||
const cbmi = efCbmiModel(efDecCbmi(efBytes(CBMI_HEX)));
|
||||
assert.deepStrictEqual(cbmi, { message_ids: [{ id: '0010' }, { id: '0020' }] });
|
||||
assert.strictEqual(efEncCbmi(cbmi, CBMI_HEX), CBMI_HEX); // the FFFF hole is kept
|
||||
assert.strictEqual(efEncCbmi({ message_ids: [{ id: '0010' }, { id: '0020' }, { id: '0044' }] },
|
||||
CBMI_HEX), '001000200044');
|
||||
assert.strictEqual(efEncCbmi({ message_ids: [{ id: '0010' }] }, '0010FFFFFFFF'), '0010FFFFFFFF');
|
||||
assert.strictEqual(efEncCbmi({ message_ids: [{ id: '0010' }, { id: '20' }] }, '0010FFFFFFFF'),
|
||||
'00100020FFFF');
|
||||
assert.throws(() => efEncCbmi({ message_ids: [{ id: 'XYZ' }] }, CBMI_HEX), /1-4 hex digits/);
|
||||
assert.throws(() => efEncCbmi({ message_ids: [{ id: '0010' }, { id: '0020' }, { id: '0030' }, { id: '0040' }] },
|
||||
'0010FFFF'), /Too many entries/);
|
||||
|
||||
assert.deepStrictEqual(efDecCbmir(efBytes(CBMIR_HEX)), { ranges: [['0000', 'FFFE']] });
|
||||
const cbmir = efCbmirModel(efDecCbmir(efBytes(CBMIR_HEX)));
|
||||
assert.deepStrictEqual(cbmir, { ranges: [{ low: '0000', high: 'FFFE' }] });
|
||||
assert.strictEqual(efEncCbmir(cbmir, CBMIR_HEX), CBMIR_HEX);
|
||||
assert.strictEqual(efEncCbmir({ ranges: [
|
||||
{ low: '0000', high: 'FFFE' }, { low: '0100', high: '02FF' },
|
||||
] }, CBMIR_HEX + 'FFFFFFFF'), '0000FFFE010002FF');
|
||||
assert.strictEqual(efEncCbmir({ ranges: [{ low: '0000', high: 'FFFE' }] },
|
||||
CBMIR_HEX + 'FFFFFFFF'), CBMIR_HEX + 'FFFFFFFF');
|
||||
assert.throws(() => efEncCbmir({ ranges: [{ low: '0000', high: 'XYZ' }] }, CBMIR_HEX), /1-4 hex digits/);
|
||||
assert.throws(() => efEncCbmir({ ranges: [
|
||||
{ low: '0000', high: 'FFFE' }, { low: '0100', high: '02FF' },
|
||||
] }, CBMIR_HEX), /Too many entries/);
|
||||
});
|
||||
@@ -45,24 +45,25 @@ const FNS = [
|
||||
'efDecAdn', 'efFindDecoder', 'efDecodeBytes', 'efIsRawOnly', 'efTlv',
|
||||
'efPnnText', 'efDecItemsFromHex',
|
||||
'efGsm7PlainText', 'efEncTextBytes', 'efPlmnEnc', 'efActEnc', 'efEncSpn',
|
||||
'efEncPlmnList', 'efPlmnRowOrig', 'efEncAdn', 'efEncAdnExt', 'efPnnNameBytes', 'efPnnHexBytes', 'efPnnOrigTlv', 'efEncPnn',
|
||||
'efEncPlmnList', 'efListRows', 'efPlmnRowOrig', 'efEncAdn', 'efEncAdnExt', 'efPnnNameBytes', 'efPnnHexBytes', 'efPnnOrigTlv', 'efEncPnn',
|
||||
'efFindEncoder', 'efHasEncoder', 'efEncError',
|
||||
'efDecModelValues', 'efDecEditModel', 'efActTechList', 'efDecFieldHtml',
|
||||
'efDecModelValues', 'efDecModelFor', 'efDecEditModel', 'efActTechList', 'efDecFieldHtml',
|
||||
'efDecRowHtml', 'efDecGridHtml', 'efDecItemRowHtml', 'efDecItemsHtml',
|
||||
'efDecRecordsHtml', 'efDecEditHtml',
|
||||
'efDecCollectValues', 'efDecCollectRows', 'efDecCollectItems',
|
||||
'pysimFsDecRowCheck', 'pysimFsDecItemTag', 'pysimFsDecAddItem',
|
||||
'efBitsSelected', 'pysimFsDecBitsFilter', 'pysimFsDecRowCheck', 'pysimFsDecItemTag', 'pysimFsDecAddItem',
|
||||
'pysimFsDecRemoveItem', 'pysimFsFindNode',
|
||||
'pysimFsHasDecoder', 'pysimFsHasEncoder', 'pysimFsDecSave', 'pysimFsDecEdit',
|
||||
'pysimFsPillsEnabled', 'pysimFsSyncContentVisibility', 'gsm7Decode',
|
||||
'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',
|
||||
];
|
||||
|
||||
let code = 'var pysimFsSelected = null;\nvar pysimFsTreeRoot = null;\nvar pysimFsDecModel = null;\n'
|
||||
+ 'var pysimFsDecEditMode = false;\nvar pysimFsEditMode = false;\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',
|
||||
'EF_ADN_FIELDS', 'EF_PLMN_LIST', 'EF_PNN_ITEMS', '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_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_ENCODERS']) {
|
||||
code += extractConst(html, c);
|
||||
}
|
||||
eval(code);
|
||||
@@ -70,6 +71,22 @@ eval(code);
|
||||
// The decoder entries these files resolve to (the full EF_DECODERS registry is
|
||||
// pinned by ef_decode.test.js; only the model's lookup path is exercised here).
|
||||
var EF_DECODERS = [
|
||||
{ name: 'EF.ICCID', fid: '2fe2', fn: efDecIccid },
|
||||
{ name: 'EF.IMSI', fid: '6f07', fn: efDecImsi },
|
||||
{ name: 'EF.IST', fid: '6f07', fn: b => efDecServiceTable(b, EF_IST_SERVICES) },
|
||||
{ name: 'EF.UST', fid: '6f38', fn: b => efDecServiceTable(b, EF_UST_SERVICES) },
|
||||
{ name: 'EF.SST', fid: '6f38', fn: efDecSst },
|
||||
{ name: 'EF.EST', fid: '6f56', fn: b => efDecServiceTable(b, EF_EST_SERVICES) },
|
||||
{ name: 'EF.HPPLMN', fid: '6f31', fn: efDecHpplmn },
|
||||
{ name: 'EF.ACC', fid: '6f78', fn: efDecAcc },
|
||||
{ name: 'EF.PHASE', fid: '6fae', fn: efDecPhase },
|
||||
{ name: 'EF.AD', fid: '6fad', fn: efDecAd },
|
||||
{ name: 'EF.PLMNsel', fid: '6f30', fn: efDecPlmnList },
|
||||
{ name: 'EF.FPLMN', fid: '6f7b', fn: efDecFplmn },
|
||||
{ name: 'EF.EHPLMN', fid: '6fd9', fn: efDecPlmnList },
|
||||
{ name: 'EF.CBMI', fid: '6f45', fn: efDecCbmi },
|
||||
{ name: 'EF.CBMID', fid: '6f48', fn: efDecCbmi },
|
||||
{ name: 'EF.CBMIR', fid: '6f50', fn: efDecCbmir },
|
||||
{ name: 'EF.SPN', fid: '6f46', fn: efDecSpn },
|
||||
{ name: 'EF.PLMNwAcT', fid: '6f60', fn: efDecPlmnWact },
|
||||
{ name: 'EF.OPLMNwAcT', fid: '6f61', fn: efDecPlmnWact },
|
||||
@@ -95,10 +112,10 @@ test('efFindEncoder / efHasEncoder resolve by name and FID, like the decoders',
|
||||
assert.strictEqual(efFindEncoder('ef.plmnwact', null).name, 'EF.PLMNwAcT');
|
||||
assert.strictEqual(efFindEncoder('MF/7F10/6F3A/EF.ADN', null).name, 'EF.ADN');
|
||||
assert.strictEqual(efFindEncoder('EF.ALIAS', '6f40').name, 'EF.MSISDN');
|
||||
assert.strictEqual(efFindEncoder('EF.IMSI', '6f07'), null);
|
||||
assert.strictEqual(efFindEncoder('EF.LOCI', '6f7e'), null);
|
||||
assert.strictEqual(efFindEncoder(null, null), null);
|
||||
assert.strictEqual(efHasEncoder('EF.SDN', null), true);
|
||||
assert.strictEqual(efHasEncoder('EF.IMSI', null), false);
|
||||
assert.strictEqual(efHasEncoder('EF.LOCI', null), false);
|
||||
});
|
||||
|
||||
test('the model is a TR value set for SPN / PLMNwAcT and record rows for ADN', () => {
|
||||
@@ -126,7 +143,7 @@ test('the model is a TR value set for SPN / PLMNwAcT and record rows for ADN', (
|
||||
assert.deepStrictEqual(adn.records[1].values, {});
|
||||
assert.strictEqual(adn.records[1].orig, ADN_FF);
|
||||
// no encoder -> no model, even when a decoder exists
|
||||
assert.strictEqual(efDecEditModel('EF.IMSI', null, { kind: 'transparent', data: '082982608200002080' }), null);
|
||||
assert.strictEqual(efDecEditModel('EF.LOCI', null, { kind: 'transparent', data: '082982608200002080' }), null);
|
||||
assert.strictEqual(efDecEditModel('EF.NOPE', 'abcd', { kind: 'transparent', data: '00' }), null);
|
||||
});
|
||||
|
||||
@@ -244,7 +261,11 @@ test('pysimFsHasEncoder resolves the selected file by name or FID', () => {
|
||||
assert.strictEqual(pysimFsHasEncoder(), true);
|
||||
pysimFsSelected = '6F46';
|
||||
assert.strictEqual(pysimFsHasEncoder(), true);
|
||||
pysimFsSelected = 'EF.IMSI';
|
||||
pysimFsSelected = 'EF.LOCI';
|
||||
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);
|
||||
pysimFsSelected = null;
|
||||
assert.strictEqual(pysimFsHasEncoder(), false);
|
||||
@@ -653,3 +674,234 @@ test('F12: a selection change during the read abandons the decoded edit', async
|
||||
assert.strictEqual(resets, 1);
|
||||
assert.strictEqual(content.innerHTML, '', 'the old file must not render over the new selection');
|
||||
});
|
||||
|
||||
// Wave 2 fixtures.
|
||||
const UST_ONE = '00000010'; // service 29
|
||||
const ACC_ONE = '8001'; // classes 15 and 0
|
||||
const CBMI_HEX = '0010FFFF0020';
|
||||
const CBMIR_HEX = '0000FFFE';
|
||||
const ICCID_HEX = '988812010000400310f0';
|
||||
|
||||
test('the bits field renders a filtered checkbox grid and collects {n,name}', () => {
|
||||
const model = efDecEditModel('EF.UST', '6f38', { kind: 'transparent', data: UST_ONE });
|
||||
assert.deepStrictEqual(model.values.services, [{ n: 29, name: 'Data download via SMS-CB' }]);
|
||||
const out = efDecEditHtml(model, UST_ONE);
|
||||
assert.match(out, /class="pysim-dec-bits /);
|
||||
assert.match(out, /pysim-dec-bits-filter/);
|
||||
assert.match(out, /data-bit="29" data-bit-name="Data download via SMS-CB" checked/);
|
||||
// the whole UST table is rendered (146 services)
|
||||
assert.strictEqual((out.match(/data-bit="/g) || []).length, 146);
|
||||
// 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);
|
||||
assert.strictEqual((acc.match(/data-bit="/g) || []).length, 15);
|
||||
assert.match(acc, /data-bit="15" data-bit-name="Class 15" checked/);
|
||||
assert.match(acc, /data-bit="0" data-bit-name="Class 0" checked/);
|
||||
assert.doesNotMatch(acc, /data-bit="10"/);
|
||||
assert.match(acc, /data-bit="11" data-bit-name="Class 11"/);
|
||||
// the collection yields the checked {n,name} list
|
||||
const root = {
|
||||
querySelectorAll: sel => sel === '[data-bit]' ? [
|
||||
{ checked: true, getAttribute: k => k === 'data-bit' ? '1' : 'Fixed Dialling Numbers (FDN)' },
|
||||
{ checked: false, getAttribute: () => '2' },
|
||||
{ checked: true, getAttribute: k => k === 'data-bit' ? '3' : 'APN Control List (ACL)' },
|
||||
] : [],
|
||||
};
|
||||
assert.deepStrictEqual(efDecCollectValues(root, EF_EST_FIELDS), { services: [
|
||||
{ n: 1, name: 'Fixed Dialling Numbers (FDN)' },
|
||||
{ n: 3, name: 'APN Control List (ACL)' },
|
||||
] });
|
||||
});
|
||||
|
||||
test('the bits filter hides the non-matching rows', () => {
|
||||
const rows = [
|
||||
{ textContent: '1 Local Phone Book', style: {} },
|
||||
{ textContent: '2 Fixed Dialling Numbers', style: {} },
|
||||
];
|
||||
const box = { querySelectorAll: sel => sel === '.pysim-dec-bit-row' ? rows : [] };
|
||||
const input = { value: 'fixed', closest: sel => sel === '.pysim-dec-bits' ? box : null };
|
||||
pysimFsDecBitsFilter(input);
|
||||
assert.strictEqual(rows[0].style.display, 'none');
|
||||
assert.strictEqual(rows[1].style.display, '');
|
||||
});
|
||||
|
||||
test('the select keeps an unknown current value as an option', () => {
|
||||
const model = efDecEditModel('EF.PHASE', '6fae', { kind: 'transparent', data: '0F' });
|
||||
assert.deepStrictEqual(model.values, { phase_hex: '0F' });
|
||||
const out = efDecEditHtml(model, '0F');
|
||||
assert.match(out, /<option value="0F" selected>Current 0F<\/option>/);
|
||||
const known = efDecEditHtml(efDecEditModel('EF.PHASE', '6fae', { kind: 'transparent', data: '03' }), '03');
|
||||
assert.match(known, /<option value="03" selected>phase 2 and PROFILE DOWNLOAD required<\/option>/);
|
||||
// AD unknown op mode
|
||||
const ad = efDecEditHtml(efDecEditModel('EF.AD', '6fad',
|
||||
{ kind: 'transparent', data: '55' + '0000' + '03' + 'AABB' }), '55' + '0000' + '03' + 'AABB');
|
||||
assert.match(ad, /<option value="55" selected>Current 55<\/option>/);
|
||||
assert.match(ad, /data-key="additional_info" data-hex="1" value="0000"/);
|
||||
assert.match(ad, /data-key="mnc_length" value="3"/);
|
||||
});
|
||||
|
||||
test('the hint fields render their help text', () => {
|
||||
const out = efDecEditHtml(efDecEditModel('EF.HPPLMN', '6f31', { kind: 'transparent', data: '2A' }), '2A');
|
||||
assert.match(out, /data-key="interval" data-hex="1" value="0x2A" maxlength="4"/);
|
||||
assert.match(out, /00 = no higher-priority PLMN search/);
|
||||
assert.match(out, /NB-IoT\/EC-GSM-IoT\/eMTC-M1 read the same byte in hours/);
|
||||
});
|
||||
|
||||
test('the wave-2 model hooks shape PHASE/AD/CBMI/CBMIR for the form', () => {
|
||||
assert.deepStrictEqual(efDecEditModel('EF.PHASE', '6fae', { kind: 'transparent', data: '02' }).values,
|
||||
{ phase_hex: '02' });
|
||||
assert.deepStrictEqual(efDecEditModel('EF.AD', '6fad', { kind: 'transparent', data: '80' + '1234' + '02' }).values,
|
||||
{ op_mode: '80', additional_info: '1234', mnc_length: '2' });
|
||||
const cbmi = efDecEditModel('EF.CBMI', '6f45', { kind: 'transparent', data: CBMI_HEX });
|
||||
assert.deepStrictEqual(cbmi.values, { message_ids: [{ id: '0010' }, { id: '0020' }] });
|
||||
const cbmir = efDecEditModel('EF.CBMIR', '6f50', { kind: 'transparent', data: CBMIR_HEX });
|
||||
assert.deepStrictEqual(cbmir.values, { ranges: [{ low: '0000', high: 'FFFE' }] });
|
||||
});
|
||||
|
||||
test('the CB list forms use the row editor with hex ids/bounds', () => {
|
||||
const cbmi = efDecEditModel('EF.CBMI', '6f45', { kind: 'transparent', data: CBMI_HEX });
|
||||
const out = efDecEditHtml(cbmi, CBMI_HEX);
|
||||
assert.strictEqual((out.match(/class="pysim-dec-row /g) || []).length, 2);
|
||||
assert.match(out, /data-key="id" data-hex="1" value="0010"/);
|
||||
assert.match(out, /data-hex="1"/);
|
||||
assert.match(out, />Add message ID</);
|
||||
const cbmir = efDecEditModel('EF.CBMIR', '6f50', { kind: 'transparent', data: CBMIR_HEX });
|
||||
const out2 = efDecEditHtml(cbmir, CBMIR_HEX);
|
||||
assert.strictEqual((out2.match(/class="pysim-dec-row /g) || []).length, 1);
|
||||
assert.match(out2, /data-key="low" data-hex="1" value="0000"/);
|
||||
assert.match(out2, /data-key="high" data-hex="1" value="FFFE"/);
|
||||
assert.match(out2, />Add range</);
|
||||
});
|
||||
|
||||
test('the confirm entries ask once and only for a changed identity', async () => {
|
||||
const decSave = extractFunc(html, 'pysimFsDecSave');
|
||||
assert.ok(decSave.includes('entry.confirm'));
|
||||
assert.ok(decSave.includes('pysimConfirm(t(entry.confirm))'));
|
||||
const entry = efFindEncoder('EF.ICCID', null);
|
||||
pysimFsDecModel = { entry: entry, mode: 'tr' };
|
||||
const makeOut = value => {
|
||||
const root = {
|
||||
getAttribute: k => k === 'data-orig' ? ICCID_HEX : null,
|
||||
querySelector: sel => sel === '[data-key="iccid"]' ? { value: value } : null,
|
||||
};
|
||||
return { querySelector: sel => sel === '.pysim-dec-edit' ? root : null, querySelectorAll: () => [] };
|
||||
};
|
||||
// unchanged -> no dialog
|
||||
let asked = 0;
|
||||
globalThis.pysimConfirm = async () => { asked++; return true; };
|
||||
let stubs = saveStubs({ success: true, sw: '9000' });
|
||||
let status = { textContent: '' };
|
||||
await pysimFsDecSave(null, 'EF.ICCID', status, makeOut('8988211000000430010'), false);
|
||||
assert.strictEqual(asked, 0);
|
||||
assert.strictEqual(stubs.bodies.length, 1);
|
||||
// changed + cancel -> nothing written
|
||||
globalThis.pysimConfirm = async () => { asked++; return false; };
|
||||
stubs = saveStubs({ success: true, sw: '9000' });
|
||||
status = { textContent: '' };
|
||||
await pysimFsDecSave(null, 'EF.ICCID', status, makeOut('8988211000000430011'), false);
|
||||
assert.strictEqual(asked, 1);
|
||||
assert.strictEqual(stubs.bodies.length, 0);
|
||||
assert.match(status.textContent, /Write cancelled/);
|
||||
// changed + accept -> written
|
||||
globalThis.pysimConfirm = async () => { asked++; return true; };
|
||||
stubs = saveStubs({ success: true, sw: '9000' });
|
||||
status = { textContent: '' };
|
||||
await pysimFsDecSave(null, 'EF.ICCID', status, makeOut('8988211000000430011'), false);
|
||||
assert.strictEqual(asked, 2);
|
||||
assert.strictEqual(stubs.bodies.length, 1);
|
||||
assert.strictEqual(stubs.bodies[0].body.data, '988812010000400310F1');
|
||||
});
|
||||
|
||||
// Finding 1: the list save passes the collected row ARRAY; the encoders must
|
||||
// not wipe the file when nothing was edited.
|
||||
const PLMN_ONLY = '62F21022F860';
|
||||
const FPLMN_HOLES = '22F860FFFFFF62F210';
|
||||
|
||||
function listSaveRoot(orig, rows) {
|
||||
const rowEls = rows.map(vals => ({
|
||||
querySelector: sel => {
|
||||
const m = /^\[data-key="(.+)"\]$/.exec(sel);
|
||||
return m && vals[m[1]] !== undefined ? { value: vals[m[1]] } : null;
|
||||
},
|
||||
querySelectorAll: () => [],
|
||||
}));
|
||||
return {
|
||||
getAttribute: k => k === 'data-orig' ? orig : null,
|
||||
querySelectorAll: sel => sel === '.pysim-dec-row' ? rowEls : [],
|
||||
};
|
||||
}
|
||||
|
||||
test('F1: a list save with no edits keeps the original bytes', async () => {
|
||||
const cases = [
|
||||
{ name: 'EF.PLMNsel', fid: '6f30', orig: PLMN_ONLY,
|
||||
rows: [{ mcc: '262', mnc: '01' }, { mcc: '228', mnc: '06' }] },
|
||||
{ name: 'EF.FPLMN', fid: '6f7b', orig: FPLMN_HOLES,
|
||||
rows: [{ mcc: '228', mnc: '06' }, { mcc: '262', mnc: '01' }] },
|
||||
{ name: 'EF.EHPLMN', fid: '6fd9', orig: PLMN_ONLY,
|
||||
rows: [{ mcc: '262', mnc: '01' }, { mcc: '228', mnc: '06' }] },
|
||||
{ name: 'EF.CBMI', fid: '6f45', orig: CBMI_HEX, rows: [{ id: '0010' }, { id: '0020' }] },
|
||||
{ name: 'EF.CBMID', fid: '6f48', orig: CBMI_HEX, rows: [{ id: '0010' }, { id: '0020' }] },
|
||||
{ name: 'EF.CBMIR', fid: '6f50', orig: CBMIR_HEX, rows: [{ low: '0000', high: 'FFFE' }] },
|
||||
];
|
||||
for (const c of cases) {
|
||||
pysimFsDecModel = { entry: efFindEncoder(c.name, c.fid), mode: 'tr' };
|
||||
const out = {
|
||||
querySelector: sel => sel === '.pysim-dec-edit' ? listSaveRoot(c.orig, c.rows) : null,
|
||||
querySelectorAll: () => [],
|
||||
};
|
||||
const stubs = saveStubs({ success: true, sw: '9000' });
|
||||
const status = { textContent: '' };
|
||||
await pysimFsDecSave(null, c.name, status, out, false);
|
||||
assert.strictEqual(stubs.bodies.length, 1, c.name);
|
||||
assert.strictEqual(stubs.bodies[0].body.data, c.orig,
|
||||
c.name + ': a no-edit list save must not wipe the file');
|
||||
}
|
||||
});
|
||||
|
||||
test('F1: an edited list row is written through the same save path', async () => {
|
||||
const cases = [
|
||||
{ name: 'EF.PLMNsel', fid: '6f30', orig: PLMN_ONLY,
|
||||
rows: [{ mcc: '262', mnc: '01' }, { mcc: '228', mnc: '07' }] },
|
||||
{ name: 'EF.CBMI', fid: '6f45', orig: CBMI_HEX,
|
||||
rows: [{ id: '0010' }, { id: '0030' }] },
|
||||
{ name: 'EF.CBMIR', fid: '6f50', orig: CBMIR_HEX,
|
||||
rows: [{ low: '0000', high: 'FF00' }] },
|
||||
];
|
||||
for (const c of cases) {
|
||||
const entry = efFindEncoder(c.name, c.fid);
|
||||
pysimFsDecModel = { entry: entry, mode: 'tr' };
|
||||
const out = {
|
||||
querySelector: sel => sel === '.pysim-dec-edit' ? listSaveRoot(c.orig, c.rows) : null,
|
||||
querySelectorAll: () => [],
|
||||
};
|
||||
const stubs = saveStubs({ success: true, sw: '9000' });
|
||||
const status = { textContent: '' };
|
||||
await pysimFsDecSave(null, c.name, status, out, false);
|
||||
const expected = entry.enc(c.rows, c.orig);
|
||||
assert.notStrictEqual(expected, c.orig, c.name + ': the edit must change the bytes');
|
||||
assert.strictEqual(stubs.bodies.length, 1, c.name);
|
||||
assert.strictEqual(stubs.bodies[0].body.data, expected, c.name);
|
||||
}
|
||||
});
|
||||
|
||||
test('F5: an untouched all-FF AD saves FF through the collected form value', async () => {
|
||||
const entry = efFindEncoder('EF.AD', null);
|
||||
const model = efDecEditModel('EF.AD', null, { kind: 'transparent', data: 'FF'.repeat(6) });
|
||||
assert.deepStrictEqual(model.values, { op_mode: 'FF', additional_info: '', mnc_length: '' });
|
||||
const form = efDecEditHtml(model, 'FF'.repeat(6));
|
||||
assert.match(form, /<option value="FF" selected>Current FF<\/option>/);
|
||||
pysimFsDecModel = model;
|
||||
const vals = { op_mode: 'FF', additional_info: '', mnc_length: '' };
|
||||
const root = {
|
||||
getAttribute: k => k === 'data-orig' ? 'FF'.repeat(6) : null,
|
||||
querySelector: sel => {
|
||||
const m = /^\[data-key="(.+)"\]$/.exec(sel);
|
||||
return m ? { value: vals[m[1]] } : null;
|
||||
},
|
||||
};
|
||||
const out = { querySelector: sel => sel === '.pysim-dec-edit' ? root : null, querySelectorAll: () => [] };
|
||||
const stubs = saveStubs({ success: true, sw: '9000' });
|
||||
const status = { textContent: '' };
|
||||
await pysimFsDecSave(null, 'EF.AD', status, out, false);
|
||||
assert.strictEqual(stubs.bodies.length, 1);
|
||||
assert.strictEqual(stubs.bodies[0].body.data, 'FF'.repeat(6));
|
||||
});
|
||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "pysim-simple-server"
|
||||
version = "3.33.0"
|
||||
version = "3.34.0"
|
||||
description = "HTTP REST server wrapping pysim for the SIMple PWA"
|
||||
license = { text = "GPL-2.0-or-later" }
|
||||
requires-python = ">=3.9"
|
||||
|
||||
@@ -37,7 +37,7 @@ from osmocom.tlv import BER_TLV_IE
|
||||
from osmocom.utils import rpad
|
||||
|
||||
|
||||
VERSION = '3.33.0'
|
||||
VERSION = '3.34.0'
|
||||
|
||||
MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE
|
||||
|
||||
|
||||
Reference in new issue
Block a user