Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 851ff65af6 | |||
| 68a22e854b | |||
| 39d384636e | |||
| 14d15b1fe8 | |||
| 110dc1f1ad | |||
| fb65644328 | |||
| 194b88e7d1 | |||
| d5e67360fe | |||
| 87bf4acbbc | |||
| 180dd50c06 |
@@ -9,6 +9,82 @@ every change carries a descriptive commit message.
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [3.36.0] — 2026-10-23
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- The batch **"waiting for the next card"** state is now a deliberate cue: the status
|
||||||
|
modal shows a card-into-reader animation next to the prompt, a short chime sounds
|
||||||
|
**once per card** when the run enters the waiting phase (Web Audio — no audio asset;
|
||||||
|
edge-triggered in the 2 s poll, so it does not repeat), and while the tab is hidden the
|
||||||
|
title hints "Insert next card". The speaker button next to the status mutes the chime
|
||||||
|
and the choice is remembered (`simple_ram_batch_sound`); the chime is unlocked by the
|
||||||
|
Start click (browser autoplay policy); reduced-motion users get the static card-above-
|
||||||
|
the-slot picture. The card follows the app's SIM silhouette (`sim.svg`): a cut corner
|
||||||
|
at the top and the contact pad near the bottom edge, sliding behind the reader bar.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- A **batch delete that finds the object already gone** is still a success — the goal is met
|
||||||
|
and the run continues — but it is no longer reported as a plain "OK": the attempt's verdict
|
||||||
|
reads **Already absent**, the delete run's counts separate real deletions from no-ops
|
||||||
|
(`Deleted: N · Already absent: M`), and the notice says the card was already absent. The
|
||||||
|
counts gain an `already_absent` bucket (the attempt result is `already_absent`); a
|
||||||
|
reinstall's tolerated pre-delete is *not* affected — its outcome is still the install.
|
||||||
|
- The READMEs link the project's news channel: [Telegram — @youeyeseesee](https://t.me/youeyeseesee).
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- The prebuilt `style.css` had gone stale: a rebuild restored utility classes the UI
|
||||||
|
already used but the output lacked (e.g. `bg-blue-50` on the batch notice, `inline-flex`,
|
||||||
|
`mx-2`, `max-h-48`, `w-44`, `min-w-24`).
|
||||||
|
|
||||||
|
## [3.35.0] — 2026-10-23
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **EF.SMSP decoded editor** — the full SMS-parameter form (TS 31.102 §4.2.27): alpha,
|
||||||
|
TP-Destination Address and TS-Service Centre Address (each with its own length
|
||||||
|
convention and a TON select; an empty SCA means the default SMSC), TP-PID, TP-DCS and
|
||||||
|
the relative TP-VP — every parameter with an *absent (the ME default applies)* toggle.
|
||||||
|
Live hints decode the DCS (TS 23.038 §4: coding group, alphabet, class, compression,
|
||||||
|
message-waiting) and the relative validity period (TS 23.040 §9.2.3.12.1). Unchanged
|
||||||
|
parameters keep their exact bytes (a card quirk such as a DCS marked absent with byte
|
||||||
|
`00` survives), alphanumeric TON addresses are preserved read-only, and all checked
|
||||||
|
records are validated before anything is written.
|
||||||
|
- **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
|
||||||
|
|
||||||
|
- `efDecSmsp` mapped the parameter indicators shifted (SCA on bit 5, PID on bit 2, DCS
|
||||||
|
on bit 3, VP on bit 4); the spec order is DA/SCA/PID/DCS/VP on bits 1–5 with bits 6–8
|
||||||
|
reserved — a record with a mixed indicator (e.g. `E9`) showed DCS and hid VP.
|
||||||
|
- The read-only EF.SMSP view decoded the TP-DA length as octets, although it is the digit
|
||||||
|
count (TS 23.040 §9.1.2.5); a slot padded with `00` gained phantom digits.
|
||||||
|
- The **Read decoded** pill no longer looks disabled when it is enabled: an inactive-but-
|
||||||
|
enabled pill is now an outlined button (blue on white/slate), and the grey fill and
|
||||||
|
dimming are left to the actually disabled state.
|
||||||
|
- **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
|
## [3.33.0] — 2026-10-23
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
> Formerly known as OTAMan — renamed to SIMple on 2026-09-19.
|
> Formerly known as OTAMan — renamed to SIMple on 2026-09-19.
|
||||||
|
|
||||||
|
📢 Project news and updates: [Telegram channel](https://t.me/youeyeseesee)
|
||||||
|
|
||||||
SIMple is a workbench for UICC/SIM card specialists: an offline HTML/JS PWA plus a local card server ([`pysim-simple-server`](pysim_simple_server/), a pySim/PC-SC bridge that also serves the PWA). It covers the full lab cycle:
|
SIMple is a workbench for UICC/SIM card specialists: an offline HTML/JS PWA plus a local card server ([`pysim-simple-server`](pysim_simple_server/), a pySim/PC-SC bridge that also serves the PWA). It covers the full lab cycle:
|
||||||
|
|
||||||
- **APDU workbench** — SIM/USIM RFM and Expanded Script builders (TS 102 226), GlobalPlatform RAM commands, a C-APDU/R-APDU parser and a response decoder (ISO 7816 / TS 102 221 / GP);
|
- **APDU workbench** — SIM/USIM RFM and Expanded Script builders (TS 102 226), GlobalPlatform RAM commands, a C-APDU/R-APDU parser and a response decoder (ISO 7816 / TS 102 221 / GP);
|
||||||
@@ -595,7 +597,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`)
|
- 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)
|
- **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 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), the cell-broadcast lists **CBMI / CBMID / CBMIR**, and **EF.SMSP** (the full SMS-parameter form: alpha, TP-DA/TS-SCA with their addressing rules and TON, TP-PID, TP-DCS and relative TP-VP, each with an absent/default toggle and live DCS/validity hints). 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)`
|
- **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
|
- **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
|
||||||
|
|
||||||
|
|||||||
+3
-1
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
> Прежнее название — OTAMan; переименован в SIMple 2026-09-19.
|
> Прежнее название — OTAMan; переименован в SIMple 2026-09-19.
|
||||||
|
|
||||||
|
📢 Новости и обновления проекта: [Telegram-канал](https://t.me/youeyeseesee)
|
||||||
|
|
||||||
SIMple — инструментарий для специалистов по UICC/SIM-картам: автономный HTML/JS PWA и локальный сервер карт ([`pysim-simple-server`](pysim_simple_server/) — мост к pySim/PC-SC, он же раздаёт PWA). Закрывает весь цикл лабораторной работы:
|
SIMple — инструментарий для специалистов по UICC/SIM-картам: автономный HTML/JS PWA и локальный сервер карт ([`pysim-simple-server`](pysim_simple_server/) — мост к pySim/PC-SC, он же раздаёт PWA). Закрывает весь цикл лабораторной работы:
|
||||||
|
|
||||||
- **APDU-верстак** — конструкторы SIM/USIM RFM и Expanded Script (TS 102 226), команды GlobalPlatform RAM, парсер C-APDU/R-APDU и декодер ответов (ISO 7816 / TS 102 221 / GP);
|
- **APDU-верстак** — конструкторы SIM/USIM RFM и Expanded Script (TS 102 226), команды GlobalPlatform RAM, парсер C-APDU/R-APDU и декодер ответов (ISO 7816 / TS 102 221 / GP);
|
||||||
@@ -570,7 +572,7 @@ Delivery PoR (SPI2 `01`) проще — карта возвращает PoR на
|
|||||||
- Элементы сгруппированы (DF выше EF) и отсортированы по **FID** или символьному **имени** (пиллы закреплены над деревом вместе с **«Проверить все файлы»**, выбор сохраняется в `localStorage`)
|
- Элементы сгруппированы (DF выше EF) и отсортированы по **FID** или символьному **имени** (пиллы закреплены над деревом вместе с **«Проверить все файлы»**, выбор сохраняется в `localStorage`)
|
||||||
- **Read raw** / **Read decoded** — чтение выбранного файла как hex-дампа или как таблицы декодированных полей (клиентские декодеры: IMSI, ICCID, SPN, списки PLMN, LOCI/EPSLOCI, ADN/MSISDN, таблицы сервисов, SUME, …); подсвеченная пилла — текущий вид, клик по любой из них перечитывает файл. таблица декодируется в браузере из тех же сырых байт (без серверного декодирования); **Read decoded** отключается, когда клиентские декодеры не покрывают файл (определяется по имени/FID до чтения)
|
- **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 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** и **EF.SMSP** (полная форма SMS-параметров: alpha, TP-DA/TS-SCA с их правилами адресации и TON, TP-PID, TP-DCS и относительный TP-VP; у каждого — флажок «отсутствует/по умолчанию» и живые подсказки по DCS и сроку действия). Для transparent-файла — одна форма (строки PLMN и элементы PNN с добавлением/удалением); для record-файла — форма на запись с флажком, который ставится автоматически при изменении поля, поэтому записываются только изменённые записи. **Save** проверяет все выбранные записи до записи; указатели расширений (EF.EXT1–EF.EXT5) и байт метода сравнения BDN сохраняются; редактирование номера, продолжение которого лежит в файле расширения, отклоняется. Обзор форматов и план охвата — в `docs/decoded-editing.md`
|
||||||
- **Проверить все файлы** — обход всего дерева (включая пользовательские) с пометкой «есть/нет», прогрессом *N / всего*, возможностью остановки и сводкой в конце; сам просмотр остаётся ленивым. Отсутствующие файлы показаны красным (✗); существующий пустой DF — `(пусто)`
|
- **Проверить все файлы** — обход всего дерева (включая пользовательские) с пометкой «есть/нет», прогрессом *N / всего*, возможностью остановки и сводкой в конце; сам просмотр остаётся ленивым. Отсутствующие файлы показаны красным (✗); существующий пустой DF — `(пусто)`
|
||||||
- **ADM** — файлы, требующие администраторский PIN, возвращают `6982`/`9804`; если в подходящем пресете карты (тот же ICCID) есть ключ ADM, рядом с ошибкой появляется кнопка **«Проверить ADM»**, а значок в заголовке (`ADM ✓/✗ ⚿`) становится кликабельным. Каждый неверный ключ расходует попытку (остаток показывается, повторная попытка требует подтверждения); заблокированный ADM требует ключа разблокировки карты
|
- **ADM** — файлы, требующие администраторский PIN, возвращают `6982`/`9804`; если в подходящем пресете карты (тот же ICCID) есть ключ ADM, рядом с ошибкой появляется кнопка **«Проверить ADM»**, а значок в заголовке (`ADM ✓/✗ ⚿`) становится кликабельным. Каждый неверный ключ расходует попытку (остаток показывается, повторная попытка требует подтверждения); заблокированный ADM требует ключа разблокировки карты
|
||||||
|
|
||||||
|
|||||||
+75
-36
@@ -1,6 +1,9 @@
|
|||||||
# Decoded editing — file-format survey and design
|
# 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–3 shipped in
|
||||||
|
v3.33.0 (SPN/PLMN/ADN/PNN), v3.34.0 (service tables/scalars/PLMN lists/BCD/CB)
|
||||||
|
and v3.35.0 (EF.SMSP); 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
|
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
|
own structure and content coding, assigns each an editing *shape*, and sets the
|
||||||
release waves. The reference is the pinned specs (`projects/docs/uicc/`):
|
release waves. The reference is the pinned specs (`projects/docs/uicc/`):
|
||||||
@@ -57,32 +60,32 @@ editing candidates.
|
|||||||
|
|
||||||
| EF | FID | Spec | Size | Content | Shape | Wave |
|
| 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.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 + '9'-prefixed BCD | S3 | 3.34 |
|
| EF.IMSI | 6F07 | TS 31.102 4.2.2 | 9 B | length byte + parity/type nibble ('9' odd / '1' even) + BCD | S3 | **3.34** |
|
||||||
| EF.IST | 6F07 | TS 31.102 4.2.2 (SAIP) | 1–2 B | service bits | S6 | 3.34 |
|
| EF.IST | 6F07 | TS 31.102 4.2.2 (SAIP) | 1–2 B | service bits | S6 | **3.34** |
|
||||||
| EF.UST | 6F38 | TS 31.102 4.2.8 | X ≥ 1 | service bits | S6 | 3.34 |
|
| EF.UST | 6F38 | TS 31.102 4.2.8 | X ≥ 1 | service bits | S6 | **3.34** |
|
||||||
| EF.SST | 6F38 | TS 51.011 10.3.7 | X ≥ 1 | 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 | S6 | 3.34 |
|
| EF.EST | 6F56 | TS 31.102 4.2.47 | X ≥ 1 | enabled-service bits (3 services) | S6 | **3.34** |
|
||||||
| EF.LI | 6F05 | TS 31.102 4.2.1 | 2n | ISO 639 language codes | 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.3.2 | 1–n | language codes | S6 | 3.34 |
|
| 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.SPN | 6F46 | TS 31.102 4.2.12 | 17 B | condition byte + Annex A text | S2 | **3.33** |
|
||||||
| EF.PLMNsel | 6F30 | TS 51.011 10.3.4 | 3n (n ≥ 8) | PLMN list | S4 | 3.34 |
|
| EF.PLMNsel | 6F30 | TS 51.011 10.3.4 | 3n (n ≥ 8) | PLMN list | S4 | **3.34** |
|
||||||
| EF.FPLMN | 6F7B | TS 31.102 4.2.16 | 3n (n ≥ 4) | PLMN list | 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.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.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.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.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.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 | text (Annex A) | S2 | 3.34 |
|
| 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.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.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.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 | S1 | 3.34 |
|
| EF.PHASE | 6FAE | TS 51.011 10.3.19 | 1 B | phase id (00/02/03) | S1 | **3.34** |
|
||||||
| EF.HPPLMN | 6F31 | TS 31.102 4.2.6 | 1 B | search period | 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, info, MNC length | 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.CBMI | 6F45 | TS 31.102 4.2.14 | 2n | CB message ids | S6 | **3.34** |
|
||||||
| EF.CBMID | 6F48 | TS 31.102 4.2.20 | 2n | CB message ids | S6 | 3.34 |
|
| EF.CBMID | 6F48 | TS 31.102 4.2.20 | 2n | CB message ids | S6 | **3.34** |
|
||||||
| EF.CBMIR | 6F50 | TS 31.102 4.2.22 | 4n | CB id ranges | S6 | 3.34 |
|
| EF.CBMIR | 6F50 | TS 31.102 4.2.22 | 4n | CB id ranges | S6 | **3.34** |
|
||||||
| EF.ACL | 6F57 | TS 31.102 4.2.48 | X | version byte + TLV list (DD APNs) | S8-TR | 3.35 |
|
| 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.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** | — |
|
| EF.PSLOCI | 6F73 | TS 31.102 4.2.23 | 14 B | P-TMSI/RAI/status | **S0** | — |
|
||||||
@@ -103,14 +106,14 @@ editing candidates.
|
|||||||
| EF.BDN | 6F4D | TS 31.102 4.2.44 | **X+15** | ADN layout + **Comparison Method Pointer** | S7 | **3.33** (fix) |
|
| 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.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.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.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.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.SMSP | 6F42 | TS 31.102 4.2.27 | 28+Y | alpha + parameter indicators + TP-DA/SCA/PID/DCS/VP | S10 | **3.35** |
|
||||||
| EF.SMS | 6F3C | TS 31.102 4.2.25 | 176 | status byte + TPDU | S10 | 3.36 |
|
| 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 | 3.36 |
|
| 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 | 3.36 |
|
| 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 | later |
|
| 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.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) | — |
|
| 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 +155,57 @@ beyond S9/S10.
|
|||||||
the Network Name IE (TS 24.008 10.5.3.5a) for PNN, where the first byte is
|
the Network Name IE (TS 24.008 10.5.3.5a) for PNN, where the first byte is
|
||||||
`0x80 | spare` for packed GSM 7-bit and `0x90` for UCS-2 — the real-card
|
`0x80 | spare` for packed GSM 7-bit and `0x90` for UCS-2 — the real-card
|
||||||
vector `43 0A 82 D4 32 BB BC 7E B7 5D E4 32 …` confirms it.
|
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 language work).
|
||||||
|
13. **EF.SMSP's parameter indicators are a five-bit presence map with inverted
|
||||||
|
meaning** (TS 31.102 §4.2.27: bit 1 DA, 2 SCA, 3 PID, 4 DCS, 5 VP; bit = 1
|
||||||
|
means *absent*; bits 6–8 reserved = 1). The decoder had the bits shifted
|
||||||
|
(SCA on 5, PID on 2, DCS on 3, VP on 4), so mixed indicators were misread.
|
||||||
|
14. **The two address slots use different length conventions**: TP-DA's length
|
||||||
|
is the digit count (TS 23.040 §9.1.2.5), TS-SCA's is the octet count of
|
||||||
|
TON/NPI + digits with 0 = default SMSC (TS 24.011 §8.2.5.2); the decoder
|
||||||
|
read the DA slot as octets. Both are handled explicitly now.
|
||||||
|
|
||||||
## 4. Release waves
|
## 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,
|
schemas, record checkboxes + auto-check, save/validation; shapes S2, S5,
|
||||||
S7, S8 for SPN, PLMNwAcT/OPLMNwAcT/HPLMNwAcT, ADN/FDN/SDN/MSISDN/LND,
|
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.
|
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,
|
- **v3.34.0 (shipped)** — the S1/S3/S4/S6 wave: UST/IST/EST service tables
|
||||||
ACC, PHASE, HPPLMN, AD, CBMI/CBMID/CBMIR, ECC (SIM), ICCID, IMSI,
|
(filterable bit grid), ACC, PHASE, HPPLMN, AD, the PLMN-only lists
|
||||||
PLMNsel/FPLMN/EHPLMN, SPDI, GID1/GID2.
|
(PLMNsel/FPLMN/EHPLMN), ICCID/IMSI (behind a confirmation) and
|
||||||
- **v3.35.0** — remaining S7 records: OPL (PNN record select), ECC (USIM),
|
CBMI/CBMID/CBMIR. Decoder fixes: ACC class numbering, the AD operation
|
||||||
SUUME, IMPI/DOMAIN/IMPU; EXT1–5 as a read-only structured view.
|
mode / MNC-length rules, the SST 2-bit layout (read-only for now) and the
|
||||||
- **v3.36.0+** — S10/complex: SMSP (parameter form), SMS (TPDU editor,
|
IMSI parity nibble.
|
||||||
reusing the existing PDU builders), ARR (rule editor — the decoder already
|
- **v3.35.0 (shipped)** — EF.SMSP as the first S10 dedicated builder: the full
|
||||||
renders rule lines), DIR (application templates), PBR (nested tree).
|
parameter form (alpha; TP-DA and TS-SCA with their own length conventions
|
||||||
|
and TON; TP-PID; TP-DCS with TS 23.038 hints; the relative TP-VP with the
|
||||||
|
TS 23.040 ladder), a per-parameter *absent (the ME default applies)* state,
|
||||||
|
verbatim preservation of unchanged slots (including alphanumeric TON
|
||||||
|
addresses) and the parameter-indicator and TP-DA-length decoder fixes.
|
||||||
|
- **v3.36.0** — LI/LP/PL (the SIM files need the 1-byte DCS language coding
|
||||||
|
and a name-dependent decoder), GID1/GID2 (the clause states no coding —
|
||||||
|
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.
|
||||||
- **Read-only forever (S0)** unless a dedicated flow appears: LOCI/PSLOCI/
|
- **Read-only forever (S0)** unless a dedicated flow appears: LOCI/PSLOCI/
|
||||||
EPSLOCI/EPSNSC/LOCIGPRS, Kc/KcGPRS, Keys/KeysPS, ACM, START-HFN/THRESHOLD,
|
EPSLOCI/EPSNSC/LOCIGPRS, Kc/KcGPRS, Keys/KeysPS, ACM, START-HFN/THRESHOLD,
|
||||||
call logs (ME-written), DCK/NETPAR.
|
call logs (ME-written), DCK/NETPAR.
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
+3
-3
@@ -324,13 +324,13 @@
|
|||||||
<tr><td class="py-1 px-2">Install Package (.cap file)</td><td class="py-1 px-2">Sends a <code class="font-mono text-sm">.cap</code> file to the card via the server: INSTALL[for load] → LOAD ×N → INSTALL[for install (+make selectable)]. The load file is split into LOAD APDUs of the <strong>LOAD block size</strong> (1–240 bytes of payload, 240 by default); each secured packet is delivered as a single SMS or, when larger, as a concatenated SMS-PP download of up to 5 segments, so a large <code class="font-mono text-sm">.cap</code> simply takes several SMS. As soon as the file is selected it is validated and its <strong>memory requirements are estimated</strong> in the form: the <strong>NVRAM requirement</strong> (the code image + persistent data, the GlobalPlatform Card Spec C6+C8 style total) and the RAM estimate. The same box reports the <strong>libraries the CAP is linked against</strong> (its Import component: library name, AID and the minimum version <code class="font-mono text-sm">≥ x.y</code>, with standard names and families resolved and a Java Card SDK hint where known), the package/applet identity and the component breakdown (sizes and shares in load-file order) — grouped under the <strong>Package</strong>, <strong>Requires</strong>, <strong>Memory</strong> and <strong>Components</strong> headings. The <strong>Platform</strong> line names the minimum Java Card level the card must provide (derived from the <code class="font-mono text-sm">javacard.framework</code> import; the header <code class="font-mono text-sm">int</code> flag additionally requires int support, Java Card 2.2+) and the <strong>ETSI release</strong> the UICC API imports come from (<code class="font-mono text-sm">uicc.toolkit</code> version → REL-N, per the workspace <code class="font-mono text-sm">sim-tools</code> corpus; shown with its basis in brackets): the JCRE checks every import against its own packages when it verifies the load file (JC VM spec 4.5.2), so a card whose platform is older rejects the LOAD as soon as the <strong>Import</strong> component is complete (the step line carries the card's remote SW and names the component the block completed, e.g. <em>completes Import</em>): the failure hint and the popup then point at the <strong>CAP compatibility test</strong> (one click — the same <code class="font-mono text-sm">.cap</code> is still selected) to find the missing package and version; a smaller LOAD block only helps when the block exceeds the card's buffer. A corrupt or wrong-format file fails the analysis, and <strong>Execute</strong> stays disabled until a successful analysis. The estimate is informational only and is never used for installation: NVRAM is the code image plus the persistent data (the GlobalPlatform C6+C8 style total, Table 11-48 — card memory management, rounding and the registry entry are not included, and the static field image is counted in both parts); the suggested <strong>C6</strong> follows the load file, and the suggested quotas are decimal byte counts (the hex in parentheses is how the C7/C8 install parameters encode them: C6 = non-volatile code, C7 = volatile RAM, C8 = non-volatile data). The <strong>Delete the package before installing (with its applications)</strong> checkbox prepends a cascade GP <code class="font-mono text-sm">DELETE</code> on the package AID (the load file with its modules and applications, P2=<code class="font-mono text-sm">0x80</code>) — a leftover package blocks INSTALL[for load]; 'not found' (a fresh card) proceeds, any other delete failure stops the chain with its reason.</td></tr>
|
<tr><td class="py-1 px-2">Install Package (.cap file)</td><td class="py-1 px-2">Sends a <code class="font-mono text-sm">.cap</code> file to the card via the server: INSTALL[for load] → LOAD ×N → INSTALL[for install (+make selectable)]. The load file is split into LOAD APDUs of the <strong>LOAD block size</strong> (1–240 bytes of payload, 240 by default); each secured packet is delivered as a single SMS or, when larger, as a concatenated SMS-PP download of up to 5 segments, so a large <code class="font-mono text-sm">.cap</code> simply takes several SMS. As soon as the file is selected it is validated and its <strong>memory requirements are estimated</strong> in the form: the <strong>NVRAM requirement</strong> (the code image + persistent data, the GlobalPlatform Card Spec C6+C8 style total) and the RAM estimate. The same box reports the <strong>libraries the CAP is linked against</strong> (its Import component: library name, AID and the minimum version <code class="font-mono text-sm">≥ x.y</code>, with standard names and families resolved and a Java Card SDK hint where known), the package/applet identity and the component breakdown (sizes and shares in load-file order) — grouped under the <strong>Package</strong>, <strong>Requires</strong>, <strong>Memory</strong> and <strong>Components</strong> headings. The <strong>Platform</strong> line names the minimum Java Card level the card must provide (derived from the <code class="font-mono text-sm">javacard.framework</code> import; the header <code class="font-mono text-sm">int</code> flag additionally requires int support, Java Card 2.2+) and the <strong>ETSI release</strong> the UICC API imports come from (<code class="font-mono text-sm">uicc.toolkit</code> version → REL-N, per the workspace <code class="font-mono text-sm">sim-tools</code> corpus; shown with its basis in brackets): the JCRE checks every import against its own packages when it verifies the load file (JC VM spec 4.5.2), so a card whose platform is older rejects the LOAD as soon as the <strong>Import</strong> component is complete (the step line carries the card's remote SW and names the component the block completed, e.g. <em>completes Import</em>): the failure hint and the popup then point at the <strong>CAP compatibility test</strong> (one click — the same <code class="font-mono text-sm">.cap</code> is still selected) to find the missing package and version; a smaller LOAD block only helps when the block exceeds the card's buffer. A corrupt or wrong-format file fails the analysis, and <strong>Execute</strong> stays disabled until a successful analysis. The estimate is informational only and is never used for installation: NVRAM is the code image plus the persistent data (the GlobalPlatform C6+C8 style total, Table 11-48 — card memory management, rounding and the registry entry are not included, and the static field image is counted in both parts); the suggested <strong>C6</strong> follows the load file, and the suggested quotas are decimal byte counts (the hex in parentheses is how the C7/C8 install parameters encode them: C6 = non-volatile code, C7 = volatile RAM, C8 = non-volatile data). The <strong>Delete the package before installing (with its applications)</strong> checkbox prepends a cascade GP <code class="font-mono text-sm">DELETE</code> on the package AID (the load file with its modules and applications, P2=<code class="font-mono text-sm">0x80</code>) — a leftover package blocks INSTALL[for load]; 'not found' (a fresh card) proceeds, any other delete failure stops the chain with its reason.</td></tr>
|
||||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">Batch install (.cap file, per card)</td><td class="py-1 px-2">The bulk workflow: one <code class="font-mono text-sm">.cap</code> and one set of install parameters, applied to card after card. <strong>Start batch</strong> needs no card — the server owns the loop: it waits for a card, installs the moment the equip finishes, then waits for the next one. Every card's <strong>preset is matched by ICCID</strong> and supplies the keyset (the ISD role TAR's default, v3.26.0), the keys and the counter; a card with no matching preset or no usable ISD keyset is reported and skipped. Each attempt lands in the list with its time, ICCID, preset, verdict and duration; a failure shows the failing step and the remote status word — the batch never retries and never stops on a failure (failures are yours to manage afterwards). A card is attempted <strong>once per run</strong>: re-equipping it reports “already processed” — restart the batch to retry it. <strong>Stop</strong> finishes the current card first (an interrupted LOAD chain can leave a half-loaded package) and then ends the run; <strong>Clear</strong> drops the finished report. The loop runs in the server, so closing the tab, switching views or reloading the page does not interrupt it; a server restart ends it. A batch and a test run exclude each other. The batch shares the install form, so the <strong>Delete the package before installing</strong> option applies to every card.</td></tr>
|
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">Batch install (.cap file, per card)</td><td class="py-1 px-2">The bulk workflow: one <code class="font-mono text-sm">.cap</code> and one set of install parameters, applied to card after card. <strong>Start batch</strong> needs no card — the server owns the loop: it waits for a card, installs the moment the equip finishes, then waits for the next one. Every card's <strong>preset is matched by ICCID</strong> and supplies the keyset (the ISD role TAR's default, v3.26.0), the keys and the counter; a card with no matching preset or no usable ISD keyset is reported and skipped. Each attempt lands in the list with its time, ICCID, preset, verdict and duration; a failure shows the failing step and the remote status word — the batch never retries and never stops on a failure (failures are yours to manage afterwards). A card is attempted <strong>once per run</strong>: re-equipping it reports “already processed” — restart the batch to retry it. <strong>Stop</strong> finishes the current card first (an interrupted LOAD chain can leave a half-loaded package) and then ends the run; <strong>Clear</strong> drops the finished report. The loop runs in the server, so closing the tab, switching views or reloading the page does not interrupt it; a server restart ends it. A batch and a test run exclude each other. The batch shares the install form, so the <strong>Delete the package before installing</strong> option applies to every card.</td></tr>
|
||||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">Batch reinstall app (per card)</td><td class="py-1 px-2">The repair workflow: for every equipped card, delete the applet instance (tolerating 'not found') and run INSTALL [for install] again from the already loaded package — no <code class="font-mono text-sm">.cap</code> and no LOAD. The fields are the single INSTALL [for install] parameters (Load File / Module / Instance AIDs, privileges, the toolkit/STK parameters, <em>make selectable</em>); the preset, keyset and counter come from each card's own preset like the other batch operations, and the attempts list shows both verdicts (delete and install).</td></tr>
|
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">Batch reinstall app (per card)</td><td class="py-1 px-2">The repair workflow: for every equipped card, delete the applet instance (tolerating 'not found') and run INSTALL [for install] again from the already loaded package — no <code class="font-mono text-sm">.cap</code> and no LOAD. The fields are the single INSTALL [for install] parameters (Load File / Module / Instance AIDs, privileges, the toolkit/STK parameters, <em>make selectable</em>); the preset, keyset and counter come from each card's own preset like the other batch operations, and the attempts list shows both verdicts (delete and install).</td></tr>
|
||||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">Batch delete (per card)</td><td class="py-1 px-2">Deletes one object on every equipped card: an application (instance only, P2=<code class="font-mono text-sm">0x00</code>) or a package with its applications (cascade, P2=<code class="font-mono text-sm">0x80</code>). Fields: the AID and the target. A 'not found' result (the object is already gone) is reported as such and counted OK; any other failure is reported with the card's status word.</td></tr>
|
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">Batch delete (per card)</td><td class="py-1 px-2">Deletes one object on every equipped card: an application (instance only, P2=<code class="font-mono text-sm">0x00</code>) or a package with its applications (cascade, P2=<code class="font-mono text-sm">0x80</code>). Fields: the AID and the target. A 'not found' result (the object is already gone) is reported as <strong>Already absent</strong> and counted separately from real deletions — it is still a success (the goal is met) and the run continues; any other failure is reported with the card's status word.</td></tr>
|
||||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">CAP compatibility test</td><td class="py-1 px-2">Answers one question without installing anything: <strong>can this card satisfy the CAP's import list?</strong> The server runs the format check, INSTALL[for load] and the LOAD blocks only up to the block that completes the <strong>Import</strong> component — the point where the JCRE verifies the import list — then stops: no last-block flag and no INSTALL[for install], so nothing is committed and nothing has to be deleted afterwards. The <strong>Import versions to test</strong> list is pre-filled from the CAP analysis: <code class="font-mono text-sm">AID=version</code> rewrites an import's requested version, <code class="font-mono text-sm">+AID=version</code> appends a synthetic import (a card-capability query: the LOAD passes only if the card exports that package at ≥ that version), and <strong>Set all 0.0</strong> drops every requirement. Two-step use: press Set all 0.0 — if the test passes, the import list was the blocker; then restore one line at a time to find the package and version the card lacks. The verdict states it covers the <strong>LOAD/import gate only</strong>: the link gate (whether the applet's actual API calls exist) is what a real install checks at INSTALL[for install]. The run and its result appear in a popup (like the install): the step lines, the verdict and a per-AID detail block — on a rejection it names the CAP section the failing block covers (load-file byte range and components, and whether that block completes <strong>Import</strong>), the remote status word with its meaning, the import list exactly as tested (CAP versions, probe rewrites, appended synthetic entries, unmatched lines) and what the probe state implies; on success it lists the import list that passed.</td></tr>
|
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">CAP compatibility test</td><td class="py-1 px-2">Answers one question without installing anything: <strong>can this card satisfy the CAP's import list?</strong> The server runs the format check, INSTALL[for load] and the LOAD blocks only up to the block that completes the <strong>Import</strong> component — the point where the JCRE verifies the import list — then stops: no last-block flag and no INSTALL[for install], so nothing is committed and nothing has to be deleted afterwards. The <strong>Import versions to test</strong> list is pre-filled from the CAP analysis: <code class="font-mono text-sm">AID=version</code> rewrites an import's requested version, <code class="font-mono text-sm">+AID=version</code> appends a synthetic import (a card-capability query: the LOAD passes only if the card exports that package at ≥ that version), and <strong>Set all 0.0</strong> drops every requirement. Two-step use: press Set all 0.0 — if the test passes, the import list was the blocker; then restore one line at a time to find the package and version the card lacks. The verdict states it covers the <strong>LOAD/import gate only</strong>: the link gate (whether the applet's actual API calls exist) is what a real install checks at INSTALL[for install]. The run and its result appear in a popup (like the install): the step lines, the verdict and a per-AID detail block — on a rejection it names the CAP section the failing block covers (load-file byte range and components, and whether that block completes <strong>Import</strong>), the remote status word with its meaning, the import list exactly as tested (CAP versions, probe rewrites, appended synthetic entries, unmatched lines) and what the probe state implies; on success it lists the import list that passed.</td></tr>
|
||||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">INSTALL [for install] (loaded package)</td><td class="py-1 px-2">For a package that is already loaded on the card (its Load File AID and Module AID, privileges, install/STK parameters and an optional <em>make selectable</em>). The applet instance is deleted first (tolerating 'not found' — a fresh card just installs; any other delete failure stops the chain with its reason), so the install parameters can be iterated without re-loading the <code class="font-mono text-sm">.cap</code>.</td></tr>
|
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">INSTALL [for install] (loaded package)</td><td class="py-1 px-2">For a package that is already loaded on the card (its Load File AID and Module AID, privileges, install/STK parameters and an optional <em>make selectable</em>). The applet instance is deleted first (tolerating 'not found' — a fresh card just installs; any other delete failure stops the chain with its reason), so the install parameters can be iterated without re-loading the <code class="font-mono text-sm">.cap</code>.</td></tr>
|
||||||
<tr><td class="py-1 px-2">INSTALL [for make selectable]</td><td class="py-1 px-2">One INSTALL [for make selectable] APDU (the instance AID) for an INSTALLED, non-selectable applet instance.</td></tr>
|
<tr><td class="py-1 px-2">INSTALL [for make selectable]</td><td class="py-1 px-2">One INSTALL [for make selectable] APDU (the instance AID) for an INSTALLED, non-selectable applet instance.</td></tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<p class="text-sm mb-3">A batch operation shows its status in a modal window: the live progress (or “Insert next card. Waiting for a card...”) on top and the processed cards below, with <strong>Stop</strong> and <strong>Clear</strong> inside. It opens on Start and on a page load while a run is active; <strong>Close</strong> stays disabled until the run ends — the run itself is server-side and survives a reload. After a finished run the panel’s <strong>Show status</strong> button reopens the report.</p>
|
<p class="text-sm mb-3">A batch operation shows its status in a modal window: the live progress (or “Insert next card. Waiting for a card...”) on top and the processed cards below, with <strong>Stop</strong> and <strong>Clear</strong> inside. It opens on Start and on a page load while a run is active; <strong>Close</strong> stays disabled until the run ends — the run itself is server-side and survives a reload. After a finished run the panel’s <strong>Show status</strong> button reopens the report. While the run waits for a card the modal shows a card-into-reader animation and plays a short chime once per card (the speaker button next to the status mutes it and the choice is remembered); when the window is in the background the tab title asks for the next card.</p>
|
||||||
|
|
||||||
<h4 id="ram-explorer" class="font-medium mb-1">Explorer View</h4>
|
<h4 id="ram-explorer" class="font-medium mb-1">Explorer View</h4>
|
||||||
<p class="text-sm mb-2">After “Explore Card” runs, the explorer displays:</p>
|
<p class="text-sm mb-2">After “Explore Card” runs, the explorer displays:</p>
|
||||||
@@ -408,7 +408,7 @@
|
|||||||
<ul class="list-disc list-inside text-sm space-y-1 mb-3">
|
<ul class="list-disc list-inside text-sm space-y-1 mb-3">
|
||||||
<li><strong>Read raw / Read decoded</strong> — read the selected file as a hex dump or as a decoded field table (client-side EF decoders: IMSI, ICCID, SPN, PLMN lists, LOCI/PSLOCI/EPSLOCI, ADN/MSISDN, service tables, SUME, …); the decoded table is built in the browser from the same raw bytes (no server-side decode). The highlighted pill is the current view; clicking either pill (re-)reads the file. <strong>Read decoded</strong> is disabled when the client-side decoders do not cover the file (name/FID check, before reading).</li>
|
<li><strong>Read raw / Read decoded</strong> — read the selected file as a hex dump or as a decoded field table (client-side EF decoders: IMSI, ICCID, SPN, PLMN lists, LOCI/PSLOCI/EPSLOCI, ADN/MSISDN, service tables, SUME, …); the decoded table is built in the browser from the same raw bytes (no server-side decode). The highlighted pill is the current view; clicking either pill (re-)reads the file. <strong>Read decoded</strong> is disabled when the client-side decoders do not cover the file (name/FID check, before reading).</li>
|
||||||
<li><strong>Edit raw</strong> — edit the raw hex data, <strong>Save</strong> to write back (or <strong>Cancel</strong>); transparent files use a textarea, record files one input per record with selection checkboxes. <strong>Edit raw</strong> switches to the hex view first and reads the file if needed.</li>
|
<li><strong>Edit raw</strong> — edit the raw hex data, <strong>Save</strong> to write back (or <strong>Cancel</strong>); transparent files use a textarea, record files one input per record with selection checkboxes. <strong>Edit raw</strong> switches to the hex view first and reads the file if needed.</li>
|
||||||
<li><strong>Edit decoded</strong> — edit the file through its own decoded form, encoded in the browser and written with the normal update path. The encoders cover <strong>EF.SPN</strong>, 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) the cell-broadcast lists (<strong>CBMI / CBMID / CBMIR</strong>) and <strong>EF.SMSP</strong> (the full SMS-parameter form: alpha, TP-DA and TS-SCA with their addressing rules and TON, TP-PID, TP-DCS and the relative TP-VP, each with an <em>absent (the ME default applies)</em> toggle and live DCS/validity hints). A transparent file gets one form (SPN fields, PLMN rows with add/remove, PNN items with add/remove); a record file gets one form per record with a selection checkbox that is auto-checked when a field changes, so only the edited records are written. <strong>Save</strong> validates every selected record before writing anything; files without an encoder keep the button disabled with a tooltip. Extension pointers (EF.EXT1–EF.EXT5) and the BDN comparison-method byte are preserved, and editing a number that continues in an extension file is refused.</li>
|
||||||
<li><strong>Probe all files</strong> — walks the whole tree (including custom files) and marks every entry present (normal) or absent (red ✗, no expand arrow); empty-but-present DFs show <code class="font-mono text-sm">(empty)</code>. Shows progress <em>N / total</em>, can be stopped, and finishes with a present/absent summary. Files are only verified when expanded or probed — browsing stays lazy.</li>
|
<li><strong>Probe all files</strong> — walks the whole tree (including custom files) and marks every entry present (normal) or absent (red ✗, no expand arrow); empty-but-present DFs show <code class="font-mono text-sm">(empty)</code>. Shows progress <em>N / total</em>, can be stopped, and finishes with a present/absent summary. Files are only verified when expanded or probed — browsing stays lazy.</li>
|
||||||
<li><strong>ADM</strong> — files that need the administrator PIN fail with <code class="font-mono text-sm">6982</code>/<code class="font-mono text-sm">9804</code>; if the matching card preset (same ICCID) carries an ADM key, a <strong>Verify ADM</strong> button appears next to the error and the header badge (<code class="font-mono text-sm">ADM ✓/✗ ⚿</code>) becomes clickable. Every wrong key consumes an attempt: the remaining attempts are shown and a retry asks for confirmation. A blocked ADM cannot be recovered here — it needs the card’s unblock key.</li>
|
<li><strong>ADM</strong> — files that need the administrator PIN fail with <code class="font-mono text-sm">6982</code>/<code class="font-mono text-sm">9804</code>; if the matching card preset (same ICCID) carries an ADM key, a <strong>Verify ADM</strong> button appears next to the error and the header badge (<code class="font-mono text-sm">ADM ✓/✗ ⚿</code>) becomes clickable. Every wrong key consumes an attempt: the remaining attempts are shown and a retry asks for confirmation. A blocked ADM cannot be recovered here — it needs the card’s unblock key.</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
+1125
-51
File diff suppressed because one or more lines are too long
@@ -5,3 +5,27 @@
|
|||||||
.dark .text-heading {
|
.dark .text-heading {
|
||||||
color: #f1f5f9;
|
color: #f1f5f9;
|
||||||
}
|
}
|
||||||
|
/* RAM batch: the "insert the next card" cue (v3.36.0).
|
||||||
|
The card slides into the reader slot and back; the slot glows while the
|
||||||
|
run waits. The static transform is the reduced-motion fallback: the card
|
||||||
|
rests above the slot. */
|
||||||
|
.ram-insert-card {
|
||||||
|
/* the resting position is the card just above the slot (0); the insert
|
||||||
|
slide takes it 20px down, behind the reader front */
|
||||||
|
animation: ram-insert 2.8s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
@keyframes ram-insert {
|
||||||
|
0%, 12% { transform: translateY(0); }
|
||||||
|
45%, 55% { transform: translateY(20px); }
|
||||||
|
88%, 100% { transform: translateY(0); }
|
||||||
|
}
|
||||||
|
.ram-insert-glow {
|
||||||
|
animation: ram-insert-glow 1.4s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
@keyframes ram-insert-glow {
|
||||||
|
0%, 100% { opacity: 0.35; }
|
||||||
|
50% { opacity: 0.95; }
|
||||||
|
}
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.ram-insert-card, .ram-insert-glow { animation: none; }
|
||||||
|
}
|
||||||
|
|||||||
+97
-10
@@ -616,6 +616,10 @@ video {
|
|||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.\!visible {
|
||||||
|
visibility: visible !important;
|
||||||
|
}
|
||||||
|
|
||||||
.visible {
|
.visible {
|
||||||
visibility: visible;
|
visibility: visible;
|
||||||
}
|
}
|
||||||
@@ -684,6 +688,11 @@ video {
|
|||||||
grid-column: span 4 / span 4;
|
grid-column: span 4 / span 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx-2 {
|
||||||
|
margin-left: 0.5rem;
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
.mx-3 {
|
.mx-3 {
|
||||||
margin-left: 0.75rem;
|
margin-left: 0.75rem;
|
||||||
margin-right: 0.75rem;
|
margin-right: 0.75rem;
|
||||||
@@ -807,6 +816,10 @@ video {
|
|||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.inline-flex {
|
||||||
|
display: inline-flex;
|
||||||
|
}
|
||||||
|
|
||||||
.table {
|
.table {
|
||||||
display: table;
|
display: table;
|
||||||
}
|
}
|
||||||
@@ -843,6 +856,10 @@ video {
|
|||||||
max-height: 8rem;
|
max-height: 8rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.max-h-48 {
|
||||||
|
max-height: 12rem;
|
||||||
|
}
|
||||||
|
|
||||||
.max-h-56 {
|
.max-h-56 {
|
||||||
max-height: 14rem;
|
max-height: 14rem;
|
||||||
}
|
}
|
||||||
@@ -935,6 +952,10 @@ video {
|
|||||||
width: 10rem;
|
width: 10rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.w-44 {
|
||||||
|
width: 11rem;
|
||||||
|
}
|
||||||
|
|
||||||
.w-48 {
|
.w-48 {
|
||||||
width: 12rem;
|
width: 12rem;
|
||||||
}
|
}
|
||||||
@@ -980,6 +1001,10 @@ video {
|
|||||||
min-width: 0px;
|
min-width: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.min-w-24 {
|
||||||
|
min-width: 6rem;
|
||||||
|
}
|
||||||
|
|
||||||
.min-w-\[12rem\] {
|
.min-w-\[12rem\] {
|
||||||
min-width: 12rem;
|
min-width: 12rem;
|
||||||
}
|
}
|
||||||
@@ -1395,6 +1420,11 @@ video {
|
|||||||
background-color: rgb(219 234 254 / var(--tw-bg-opacity, 1));
|
background-color: rgb(219 234 254 / var(--tw-bg-opacity, 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.bg-blue-50 {
|
||||||
|
--tw-bg-opacity: 1;
|
||||||
|
background-color: rgb(239 246 255 / var(--tw-bg-opacity, 1));
|
||||||
|
}
|
||||||
|
|
||||||
.bg-blue-50\/50 {
|
.bg-blue-50\/50 {
|
||||||
background-color: rgb(239 246 255 / 0.5);
|
background-color: rgb(239 246 255 / 0.5);
|
||||||
}
|
}
|
||||||
@@ -1796,6 +1826,11 @@ video {
|
|||||||
color: rgb(31 41 55 / var(--tw-text-opacity, 1));
|
color: rgb(31 41 55 / var(--tw-text-opacity, 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.text-gray-900 {
|
||||||
|
--tw-text-opacity: 1;
|
||||||
|
color: rgb(17 24 39 / var(--tw-text-opacity, 1));
|
||||||
|
}
|
||||||
|
|
||||||
.text-green-600 {
|
.text-green-600 {
|
||||||
--tw-text-opacity: 1;
|
--tw-text-opacity: 1;
|
||||||
color: rgb(22 163 74 / var(--tw-text-opacity, 1));
|
color: rgb(22 163 74 / var(--tw-text-opacity, 1));
|
||||||
@@ -1893,10 +1928,61 @@ video {
|
|||||||
filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow);
|
filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.transition {
|
||||||
|
transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter;
|
||||||
|
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
transition-duration: 150ms;
|
||||||
|
}
|
||||||
|
|
||||||
.dark .text-heading {
|
.dark .text-heading {
|
||||||
color: #f1f5f9;
|
color: #f1f5f9;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* RAM batch: the "insert the next card" cue (v3.36.0).
|
||||||
|
The card slides into the reader slot and back; the slot glows while the
|
||||||
|
run waits. The static transform is the reduced-motion fallback: the card
|
||||||
|
rests above the slot. */
|
||||||
|
|
||||||
|
.ram-insert-card {
|
||||||
|
/* the resting position is the card just above the slot (0); the insert
|
||||||
|
slide takes it 20px down, behind the reader front */
|
||||||
|
animation: ram-insert 2.8s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes ram-insert {
|
||||||
|
0%, 12% {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
45%, 55% {
|
||||||
|
transform: translateY(20px);
|
||||||
|
}
|
||||||
|
|
||||||
|
88%, 100% {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ram-insert-glow {
|
||||||
|
animation: ram-insert-glow 1.4s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes ram-insert-glow {
|
||||||
|
0%, 100% {
|
||||||
|
opacity: 0.35;
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
opacity: 0.95;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.ram-insert-card, .ram-insert-glow {
|
||||||
|
animation: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.hover\:bg-amber-100:hover {
|
.hover\:bg-amber-100:hover {
|
||||||
--tw-bg-opacity: 1;
|
--tw-bg-opacity: 1;
|
||||||
background-color: rgb(254 243 199 / var(--tw-bg-opacity, 1));
|
background-color: rgb(254 243 199 / var(--tw-bg-opacity, 1));
|
||||||
@@ -1956,11 +2042,6 @@ video {
|
|||||||
background-color: rgb(55 65 81 / var(--tw-bg-opacity, 1));
|
background-color: rgb(55 65 81 / var(--tw-bg-opacity, 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
.hover\:bg-red-100:hover {
|
|
||||||
--tw-bg-opacity: 1;
|
|
||||||
background-color: rgb(254 226 226 / var(--tw-bg-opacity, 1));
|
|
||||||
}
|
|
||||||
|
|
||||||
.hover\:bg-red-200:hover {
|
.hover\:bg-red-200:hover {
|
||||||
--tw-bg-opacity: 1;
|
--tw-bg-opacity: 1;
|
||||||
background-color: rgb(254 202 202 / var(--tw-bg-opacity, 1));
|
background-color: rgb(254 202 202 / var(--tw-bg-opacity, 1));
|
||||||
@@ -2093,6 +2174,10 @@ video {
|
|||||||
background-color: rgb(30 58 138 / 0.1);
|
background-color: rgb(30 58 138 / 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dark\:bg-blue-900\/20:is(.dark *) {
|
||||||
|
background-color: rgb(30 58 138 / 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
.dark\:bg-blue-900\/40:is(.dark *) {
|
.dark\:bg-blue-900\/40:is(.dark *) {
|
||||||
background-color: rgb(30 58 138 / 0.4);
|
background-color: rgb(30 58 138 / 0.4);
|
||||||
}
|
}
|
||||||
@@ -2258,10 +2343,6 @@ video {
|
|||||||
background-color: rgb(6 78 59 / 0.4);
|
background-color: rgb(6 78 59 / 0.4);
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark\:hover\:bg-red-900\/40:hover:is(.dark *) {
|
|
||||||
background-color: rgb(127 29 29 / 0.4);
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark\:hover\:bg-red-900\/50:hover:is(.dark *) {
|
.dark\:hover\:bg-red-900\/50:hover:is(.dark *) {
|
||||||
background-color: rgb(127 29 29 / 0.5);
|
background-color: rgb(127 29 29 / 0.5);
|
||||||
}
|
}
|
||||||
@@ -2289,6 +2370,11 @@ video {
|
|||||||
color: rgb(209 213 219 / var(--tw-text-opacity, 1));
|
color: rgb(209 213 219 / var(--tw-text-opacity, 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dark\:hover\:text-slate-200:hover:is(.dark *) {
|
||||||
|
--tw-text-opacity: 1;
|
||||||
|
color: rgb(226 232 240 / var(--tw-text-opacity, 1));
|
||||||
|
}
|
||||||
|
|
||||||
.dark\:hover\:text-slate-300:hover:is(.dark *) {
|
.dark\:hover\:text-slate-300:hover:is(.dark *) {
|
||||||
--tw-text-opacity: 1;
|
--tw-text-opacity: 1;
|
||||||
color: rgb(203 213 225 / var(--tw-text-opacity, 1));
|
color: rgb(203 213 225 / var(--tw-text-opacity, 1));
|
||||||
@@ -2328,7 +2414,8 @@ video {
|
|||||||
.lg\:flex-row {
|
.lg\:flex-row {
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
}
|
}
|
||||||
}/* ===== Theme contrast adjustments ===== */
|
}
|
||||||
|
/* ===== Theme contrast adjustments ===== */
|
||||||
|
|
||||||
/* Light theme: darken softer (muted) text one step for higher contrast */
|
/* Light theme: darken softer (muted) text one step for higher contrast */
|
||||||
.text-gray-400 {
|
.text-gray-400 {
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
const CACHE = 'simple-v466';
|
const CACHE = 'simple-v473';
|
||||||
const URLS = [
|
const URLS = [
|
||||||
'index.html',
|
'index.html',
|
||||||
'help.html',
|
'help.html',
|
||||||
|
|||||||
@@ -24,10 +24,10 @@ function extractFunc(src, name) {
|
|||||||
const FNS = [
|
const FNS = [
|
||||||
'efBytes', 'efHex', 'efSwapNibbles', 'efAllFf', 'efRstripFf', 'efTlv', 'efUcs2Be',
|
'efBytes', 'efHex', 'efSwapNibbles', 'efAllFf', 'efRstripFf', 'efTlv', 'efUcs2Be',
|
||||||
'efGsm7Octets', 'efAnnexA', 'efBcdAddress', 'efBcdAddressLen', 'efPlmn', 'efPlmnAct', 'efPnnText',
|
'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',
|
'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',
|
'efSmspDaNumber', 'efDecSmsp', 'efDecSpdi', 'efDecNai', 'efDecDir', 'efDecArr', 'efDecPnn', 'efDecAdn',
|
||||||
'pinKeyRefName', 'efArrAmLabel', 'efArrSc', 'efArrAmDo',
|
'pinKeyRefName', 'efArrAmLabel', 'efArrSc', 'efArrAmDo',
|
||||||
'efDecExt1', 'efDecSms', 'efDecSume', 'efPbrHex', 'efDecPbr', 'efFindDecoder', 'efFidFromPath', 'efDecodeBytes',
|
'efDecExt1', 'efDecSms', 'efDecSume', 'efPbrHex', 'efDecPbr', 'efFindDecoder', 'efFidFromPath', 'efDecodeBytes',
|
||||||
'efDecodeFile', 'efDecodeHex', 'efIsRawOnly', 'efFieldLabel', 'efPrimitive', 'efFlatten',
|
'efDecodeFile', 'efDecodeHex', 'efIsRawOnly', 'efFieldLabel', 'efPrimitive', 'efFlatten',
|
||||||
@@ -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']) {
|
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';
|
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(new RegExp('const ' + c + ' = \\{.*\\};'))[0].replace('const ', 'var ') + '\n';
|
||||||
}
|
}
|
||||||
code += html.match(/const EF_FIELD_LABELS = \{[\s\S]*?\n\};/)[0].replace('const ', 'var ') + '\n';
|
code += html.match(/const EF_FIELD_LABELS = \{[\s\S]*?\n\};/)[0].replace('const ', 'var ') + '\n';
|
||||||
@@ -103,7 +103,9 @@ test('EF decoders match the pinned spec / pySim test vectors', () => {
|
|||||||
assert.deepStrictEqual(efDecLi(efBytes('FFFF')), { languages: [] });
|
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(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('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(efDecEcc(efBytes('19f101')), { number: '911', esc: '0x01' });
|
||||||
assert.deepStrictEqual(efDecKc(efBytes('837d783609a3858f05')), { kc: '837D783609A3858F', ksi: 0 });
|
assert.deepStrictEqual(efDecKc(efBytes('837d783609a3858f05')), { kc: '837D783609A3858F', ksi: 0 });
|
||||||
assert.deepStrictEqual(efDecPlmnList(efBytes('22F860')), { plmns: [{ mcc: '228', mnc: '06' }] });
|
assert.deepStrictEqual(efDecPlmnList(efBytes('22F860')), { plmns: [{ mcc: '228', mnc: '06' }] });
|
||||||
@@ -115,7 +117,7 @@ test('EF decoders match the pinned spec / pySim test vectors', () => {
|
|||||||
assert.strictEqual(efFindDecoder('EF.FPLMN', null).fn, efDecFplmn);
|
assert.strictEqual(efFindDecoder('EF.FPLMN', null).fn, efDecFplmn);
|
||||||
assert.deepStrictEqual(efDecCbmi(efBytes('0010FFFF0020')), { message_ids: [16, 32] });
|
assert.deepStrictEqual(efDecCbmi(efBytes('0010FFFF0020')), { message_ids: [16, 32] });
|
||||||
assert.deepStrictEqual(efDecCbmir(efBytes('0000FFFEFFFF')), { ranges: [['0000', 'FFFE']] });
|
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')),
|
assert.deepStrictEqual(efDecNai(efBytes('8021696d732e6d6e633030302e6d63633733382e336770706e6574776f726b2e6f7267')),
|
||||||
{ text: 'ims.mnc000.mcc738.3gppnetwork.org' });
|
{ text: 'ims.mnc000.mcc738.3gppnetwork.org' });
|
||||||
assert.deepStrictEqual(efDecImsi(efBytes('FFFF')), { empty: true });
|
assert.deepStrictEqual(efDecImsi(efBytes('FFFF')), { empty: true });
|
||||||
@@ -276,6 +278,9 @@ test('EF record decoders: SMS, OPL, DIR, ARR, IMPI/SMSP', () => {
|
|||||||
assert.strictEqual(smsp.sca, '+4915790109999');
|
assert.strictEqual(smsp.sca, '+4915790109999');
|
||||||
assert.strictEqual(smsp.tp_pid, '0x00');
|
assert.strictEqual(smsp.tp_pid, '0x00');
|
||||||
assert.strictEqual(smsp.tp_dcs, '0x00');
|
assert.strictEqual(smsp.tp_dcs, '0x00');
|
||||||
|
assert.strictEqual(smsp.tp_vp, '0xA9');
|
||||||
|
assert.strictEqual(smsp.indicators, '0xE1');
|
||||||
|
assert.strictEqual(smsp.tp_da, undefined);
|
||||||
// EF.SMS status byte + skip SMSC + TPDU
|
// EF.SMS status byte + skip SMSC + TPDU
|
||||||
const sms = efDecSms(efBytes('030891945197109099f90000'));
|
const sms = efDecSms(efBytes('030891945197109099f90000'));
|
||||||
assert.strictEqual(sms.direction, 'MT');
|
assert.strictEqual(sms.direction, 'MT');
|
||||||
@@ -283,6 +288,31 @@ test('EF record decoders: SMS, OPL, DIR, ARR, IMPI/SMSP', () => {
|
|||||||
assert.strictEqual(sms.tpdu, '0000');
|
assert.strictEqual(sms.tpdu, '0000');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('EF.SMSP decodes the indicator bits (TS 31.102 4.2.27)', () => {
|
||||||
|
// live card record: DA absent (0x01), SCA present, PID present, DCS absent
|
||||||
|
// (0x08), VP present; bits 6..8 reserved = 1 -> 0xE9
|
||||||
|
const live = efDecSmsp(efBytes('ffffffffffffffffffffffffffe9ffffffffffffffffffffffff07919733824009f0ffffffff0000ff'));
|
||||||
|
assert.deepStrictEqual(live, {
|
||||||
|
indicators: '0xE9',
|
||||||
|
sca: '+79332804900',
|
||||||
|
tp_pid: '0x00',
|
||||||
|
tp_vp: '0xFF',
|
||||||
|
});
|
||||||
|
assert.strictEqual(live.tp_da, undefined);
|
||||||
|
assert.strictEqual(live.tp_dcs, undefined); // DCS absent, its 00 slot kept raw for the editor
|
||||||
|
});
|
||||||
|
|
||||||
|
test('EF.SMSP reads the TP-DA length as a digit count (00 padding is not ABS)', () => {
|
||||||
|
// alpha FF x13 | indicator FE (DA present, rest absent) |
|
||||||
|
// DA 04 81 21 43 00 00 FF... ("1234" padded with 00, not FF)
|
||||||
|
const hex = 'FF'.repeat(13) + 'FE' + '048121430000FFFFFFFFFFFF'
|
||||||
|
+ 'FF'.repeat(12) + 'FF' + 'FF' + 'FF';
|
||||||
|
assert.deepStrictEqual(efDecSmsp(efBytes(hex)), {
|
||||||
|
indicators: '0xFE',
|
||||||
|
tp_da: '1234',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
test('service tables: UST service bits and IST fallback for 6F07', () => {
|
test('service tables: UST service bits and IST fallback for 6F07', () => {
|
||||||
const ust = efDecServiceTable(efBytes('00000010'), EF_UST_SERVICES);
|
const ust = efDecServiceTable(efBytes('00000010'), EF_UST_SERVICES);
|
||||||
assert.deepStrictEqual(ust.services, [{ n: 29, name: 'Data download via SMS-CB' }]);
|
assert.deepStrictEqual(ust.services, [{ n: 29, name: 'Data download via SMS-CB' }]);
|
||||||
@@ -290,6 +320,29 @@ test('service tables: UST service bits and IST fallback for 6F07', () => {
|
|||||||
assert.deepStrictEqual(ist.services, [{ n: 1, name: 'P-CSCF address' }]);
|
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', () => {
|
test('decoder registry resolves by name first, then FID; unknown returns null', () => {
|
||||||
assert.strictEqual(efFindDecoder('EF.IMSI', null).fid, '6f07');
|
assert.strictEqual(efFindDecoder('EF.IMSI', null).fid, '6f07');
|
||||||
assert.strictEqual(efFindDecoder(null, '2fe2').name, 'EF.ICCID');
|
assert.strictEqual(efFindDecoder(null, '2fe2').name, 'EF.ICCID');
|
||||||
|
|||||||
@@ -44,19 +44,20 @@ function extractConst(src, name) {
|
|||||||
|
|
||||||
const FNS = [
|
const FNS = [
|
||||||
'efBytes', 'efHex', 'efSwapNibbles', 'efAllFf', 'efRstripFf', 'efUcs2Be',
|
'efBytes', 'efHex', 'efSwapNibbles', 'efAllFf', 'efRstripFf', 'efUcs2Be',
|
||||||
'efGsm7Octets', 'efAnnexA', 'efBcdAddress', 'efPlmn', 'efPlmnAct',
|
'efGsm7Octets', 'efAnnexA', 'efBcdAddress', 'efBcdAddressLen', 'efPlmn', 'efPlmnAct',
|
||||||
'efDecSpn', 'efDecPlmnWact', 'efDecAdn', 'efDecPnn', 'efFindDecoder',
|
'efDecSpn', 'efDecPlmnWact', 'efDecAdn', 'efDecPnn', 'efDecSmsp', 'efFindDecoder',
|
||||||
'efDecodeBytes', 'efTlv', 'efPnnText', 'efDecItemsFromHex',
|
'efDecodeBytes', 'efTlv', 'efPnnText', 'efDecItemsFromHex',
|
||||||
'efGsm7PlainText', 'efEncTextBytes', 'efPlmnEnc', 'efActEnc', 'efEncSpn',
|
'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',
|
'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', 'efSmspAddressModel', 'efSmspModel', 'efSmspTonOptions', 'efSmspParamRow', 'efSmspHtml', 'efSmspCollect', 'pysimFsSmspToggle', 'pysimFsSmspHexInput', 'efSmspHexByte', 'efSmspAddressBytes', 'efSmspAddrUnchanged', 'efSmspByteUnchanged', 'efEncSmsp', 'smspT', 'smspDcsHint', 'smspVpHint', 'smspDurationText', 'smspPluralIndex', 'smspCount', 'efSmspDaNumber',
|
||||||
];
|
];
|
||||||
|
|
||||||
let code = '';
|
let code = '';
|
||||||
for (const f of FNS) code += extractFunc(html, f) + '\n';
|
for (const f of FNS) code += extractFunc(html, f) + '\n';
|
||||||
for (const c of ['GSM7_ALPHABET', 'GSM7_EXT_MAP', 'EF_ACT_TECHS', 'EF_SPN_FIELDS',
|
for (const c of ['GSM7_ALPHABET', 'GSM7_EXT_MAP', 'EF_ACT_TECHS', 'EF_SPN_FIELDS',
|
||||||
'EF_ADN_FIELDS', 'EF_PLMN_LIST', 'EF_PNN_ITEMS', 'EF_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_SMSP_PARAMS', 'EF_SMSP_CUSTOM', 'EF_ENCODERS']) {
|
||||||
code += extractConst(html, c);
|
code += extractConst(html, c);
|
||||||
}
|
}
|
||||||
eval(code);
|
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.ALIAS', '6f60').enc, efEncPlmnList);
|
||||||
assert.strictEqual(efFindEncoder('EF.NOPE', 'abcd'), null);
|
assert.strictEqual(efFindEncoder('EF.NOPE', 'abcd'), null);
|
||||||
assert.strictEqual(efHasEncoder('EF.ADN', null), true);
|
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
|
// 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.ADN', null).fields, efFindEncoder('EF.BDN', null).fields);
|
||||||
assert.strictEqual(efFindEncoder('EF.PLMNwAcT', null).list, efFindEncoder('EF.HPLMNwAcT', null).list);
|
assert.strictEqual(efFindEncoder('EF.PLMNwAcT', null).list, efFindEncoder('EF.HPLMNwAcT', null).list);
|
||||||
@@ -429,14 +430,37 @@ test('F14: the encoder messages go through t() (unknown technology, too many row
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('unchanged content re-encodes byte-identically (all fixtures)', () => {
|
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 decode = (name, hex) => {
|
||||||
|
const entry = efFindEncoder(name, null);
|
||||||
const bytes = efBytes(hex);
|
const bytes = efBytes(hex);
|
||||||
if (name === 'EF.SPN') return efDecSpn(bytes);
|
let dec;
|
||||||
if (name === 'EF.PLMNwAcT') return efDecPlmnWact(bytes).plmns;
|
if (name === 'EF.SPN') dec = efDecSpn(bytes);
|
||||||
if (name === 'EF.BDN') return efDecAdn(bytes, 15);
|
else if (tables[name]) {
|
||||||
if (name === 'EF.ADN') return efDecAdn(bytes);
|
// the form renders only the table's services; other set bits stay
|
||||||
if (name === 'EF.PNN') return efDecItemsFromHex(efFindEncoder('EF.PNN', null), hex);
|
// in the original bytes
|
||||||
throw new Error('no decoder for ' + name);
|
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 if (name === 'EF.SMSP') dec = efSmspModel(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 = [
|
const cases = [
|
||||||
{ name: 'EF.SPN', fid: '6f46', hex: SPN_ORIG },
|
{ name: 'EF.SPN', fid: '6f46', hex: SPN_ORIG },
|
||||||
@@ -456,6 +480,40 @@ test('unchanged content re-encodes byte-identically (all fixtures)', () => {
|
|||||||
{ name: 'EF.PNN', fid: '6fc5', hex: PNN_2 },
|
{ name: 'EF.PNN', fid: '6fc5', hex: PNN_2 },
|
||||||
{ name: 'EF.PNN', fid: '6fc5', hex: '800400410042' + 'FF'.repeat(16) },
|
{ name: 'EF.PNN', fid: '6fc5', hex: '800400410042' + 'FF'.repeat(16) },
|
||||||
{ name: 'EF.PNN', fid: '6fc5', hex: '430a82d432bbbc7eb75de4325a02aabb' + 'ff'.repeat(14) },
|
{ 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.SMSP', fid: '6f42', hex: SMSP_FIX },
|
||||||
|
{ name: 'EF.SMSP', fid: '6f42', hex: SMSP_VEC },
|
||||||
|
{ name: 'EF.CBMIR', fid: '6f50', hex: 'FF'.repeat(4) },
|
||||||
];
|
];
|
||||||
for (const c of cases) {
|
for (const c of cases) {
|
||||||
const entry = efFindEncoder(c.name, c.fid);
|
const entry = efFindEncoder(c.name, c.fid);
|
||||||
@@ -511,3 +569,439 @@ test('F8: the ADN-family entries name their extension EF and use it in refusals'
|
|||||||
// the unchanged number still copies the extension slots verbatim
|
// the unchanged number still copies the extension slots verbatim
|
||||||
assert.strictEqual(efEncAdn(efDecAdn(efBytes(fdn)), fdn, 14, 'EXT2'), fdn.toUpperCase());
|
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/);
|
||||||
|
});
|
||||||
|
|
||||||
|
// EF.SMSP (TS 31.102 4.2.27): alpha (Y = len-28), the indicator byte, the
|
||||||
|
// DA/SCA slots, PID, DCS and VP. The live fixture and the 44-byte vector.
|
||||||
|
const SMSP_FIX = 'ffffffffffffffffffffffffffe9ffffffffffffffffffffffff07919733824009f0ffffffff0000ff';
|
||||||
|
const SMSP_VEC = '534d5343ffffffffffffffffffffffffe1ffffffffffffffffffffffff0891945197109099f9ffffff0000a9';
|
||||||
|
|
||||||
|
test('EF.SMSP: the live fixture model and the no-edit round-trip', () => {
|
||||||
|
const entry = efFindEncoder('EF.SMSP', null);
|
||||||
|
assert.strictEqual(entry.fid, '6f42');
|
||||||
|
assert.ok(entry.custom, 'the SMSP entry has a custom builder');
|
||||||
|
assert.deepStrictEqual(efSmspModel(SMSP_FIX), {
|
||||||
|
alpha: '',
|
||||||
|
params: {
|
||||||
|
da: { present: false, orig: 'FFFFFFFFFFFFFFFFFFFFFFFF', number: '', ton: 1, alnum: false },
|
||||||
|
sca: { present: true, orig: '07919733824009F0FFFFFFFF', number: '+79332804900', ton: 1, alnum: false },
|
||||||
|
pid: { present: true, orig: '00', value: '0x00' },
|
||||||
|
dcs: { present: false, orig: '00', value: '0x00' },
|
||||||
|
vp: { present: true, orig: 'FF', value: '0xFF' },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
assert.strictEqual(efEncSmsp(efSmspModel(SMSP_FIX), SMSP_FIX), SMSP_FIX.toUpperCase());
|
||||||
|
assert.strictEqual(efEncSmsp(efSmspModel(SMSP_VEC), SMSP_VEC), SMSP_VEC.toUpperCase());
|
||||||
|
});
|
||||||
|
|
||||||
|
test('EF.SMSP: SCA/DA edits and the two length conventions', () => {
|
||||||
|
const base = efSmspModel(SMSP_FIX);
|
||||||
|
const clone = () => JSON.parse(JSON.stringify(base));
|
||||||
|
// SCA length = TON/NPI + BCD octets
|
||||||
|
let v = clone();
|
||||||
|
v.params.sca.number = '+4915790109999';
|
||||||
|
let out = efEncSmsp(v, SMSP_FIX);
|
||||||
|
assert.strictEqual(out.slice(52, 56), '0891'); // len 8, TON/NPI 91
|
||||||
|
assert.deepStrictEqual(efSmspModel(out).params.sca.number, '+4915790109999');
|
||||||
|
// SCA empty = the default SMSC (length 0)
|
||||||
|
v = clone();
|
||||||
|
v.params.sca.number = '';
|
||||||
|
out = efEncSmsp(v, SMSP_FIX);
|
||||||
|
assert.strictEqual(out.slice(52, 56), '00FF');
|
||||||
|
assert.strictEqual(efSmspModel(out).params.sca.number, '');
|
||||||
|
// DA length = the digit count; TON 0 -> TON/NPI 01
|
||||||
|
v = clone();
|
||||||
|
Object.assign(v.params.da, { present: true, number: '12345', ton: 0 });
|
||||||
|
out = efEncSmsp(v, SMSP_FIX);
|
||||||
|
assert.strictEqual(out.slice(26, 28), 'E8'); // DA bit cleared
|
||||||
|
assert.strictEqual(out.slice(28, 52), '05812143F5FFFFFFFFFFFFFF');
|
||||||
|
assert.deepStrictEqual(efSmspModel(out).params.da.number, '12345');
|
||||||
|
// 20 digits fit, 21 are refused
|
||||||
|
v = clone();
|
||||||
|
Object.assign(v.params.da, { present: true, number: '1'.repeat(20), ton: 1 });
|
||||||
|
assert.strictEqual(efEncSmsp(v, SMSP_FIX).slice(28, 30), '14');
|
||||||
|
v.params.da.number = '1'.repeat(21);
|
||||||
|
assert.throws(() => efEncSmsp(v, SMSP_FIX), /max 20 digits/);
|
||||||
|
assert.throws(() => efEncSmsp({ ...clone(), params: { ...base.params,
|
||||||
|
da: { present: true, number: '', ton: 1, orig: '' } } }, SMSP_FIX), /TP-DA number is required/);
|
||||||
|
assert.throws(() => efEncSmsp({ ...clone(), params: { ...base.params,
|
||||||
|
sca: { present: true, number: '12A', ton: 1, orig: '' } } }, SMSP_FIX), /digits/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('EF.SMSP: presence toggles and the indicator byte', () => {
|
||||||
|
const base = efSmspModel(SMSP_FIX);
|
||||||
|
const clone = () => JSON.parse(JSON.stringify(base));
|
||||||
|
// DCS newly present: E9 -> E1 (bit 4 cleared), the value written
|
||||||
|
let v = clone();
|
||||||
|
v.params.dcs.present = true;
|
||||||
|
v.params.dcs.value = '0x08';
|
||||||
|
let out = efEncSmsp(v, SMSP_FIX);
|
||||||
|
assert.strictEqual(out.slice(26, 28), 'E1');
|
||||||
|
assert.strictEqual(out.slice(78, 80), '08');
|
||||||
|
// SCA newly absent: the bit set and the slot FF-filled
|
||||||
|
v = clone();
|
||||||
|
v.params.sca.present = false;
|
||||||
|
out = efEncSmsp(v, SMSP_FIX);
|
||||||
|
assert.strictEqual(out.slice(26, 28), 'EB');
|
||||||
|
assert.strictEqual(out.slice(28, 52), 'FF'.repeat(12));
|
||||||
|
// PID newly absent: E9 -> ED
|
||||||
|
v = clone();
|
||||||
|
v.params.pid.present = false;
|
||||||
|
out = efEncSmsp(v, SMSP_FIX);
|
||||||
|
assert.strictEqual(out.slice(26, 28), 'ED');
|
||||||
|
assert.strictEqual(out.slice(76, 78), 'FF');
|
||||||
|
// an untouched file keeps the original indicator byte (reserved bits too)
|
||||||
|
assert.strictEqual(efEncSmsp(base, SMSP_FIX).slice(26, 28), 'E9');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('EF.SMSP: alpha, PID/DCS/VP hex and the refusals', () => {
|
||||||
|
const base = efSmspModel(SMSP_FIX);
|
||||||
|
const clone = () => JSON.parse(JSON.stringify(base));
|
||||||
|
let v = clone();
|
||||||
|
v.alpha = 'Test';
|
||||||
|
let out = efEncSmsp(v, SMSP_FIX);
|
||||||
|
assert.strictEqual(out.slice(0, 8), '54657374');
|
||||||
|
assert.strictEqual(efSmspModel(out).alpha, 'Test');
|
||||||
|
assert.throws(() => efEncSmsp({ ...clone(), alpha: 'A'.repeat(14) }, SMSP_FIX), /alpha identifier is too long/);
|
||||||
|
v = clone();
|
||||||
|
v.params.pid.value = '0x7F';
|
||||||
|
v.params.vp.value = 'A8';
|
||||||
|
v.params.dcs = { present: true, orig: '', value: 'F4' };
|
||||||
|
out = efEncSmsp(v, SMSP_FIX);
|
||||||
|
assert.deepStrictEqual([out.slice(76, 78), out.slice(78, 80), out.slice(80, 82)], ['7F', 'F4', 'A8']);
|
||||||
|
for (const key of ['pid', 'dcs', 'vp']) {
|
||||||
|
const bad = clone();
|
||||||
|
bad.params[key] = { ...bad.params[key], present: true, value: 'ZZ' };
|
||||||
|
assert.throws(() => efEncSmsp(bad, SMSP_FIX), /2 hex digits/, key);
|
||||||
|
}
|
||||||
|
assert.throws(() => efEncSmsp({ ...clone(), params: { ...base.params,
|
||||||
|
sca: { present: true, number: '12A', ton: 1, orig: '' } } }, SMSP_FIX), /digits/);
|
||||||
|
assert.throws(() => efEncSmsp(base, 'FF'.repeat(27)), /too short for the SMSP layout/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('EF.SMSP: an all-FF 28-byte record round-trips', () => {
|
||||||
|
const ff = 'FF'.repeat(28);
|
||||||
|
const model = efSmspModel(ff);
|
||||||
|
assert.strictEqual(model.alpha, '');
|
||||||
|
for (const key of ['da', 'sca', 'pid', 'dcs', 'vp']) assert.strictEqual(model.params[key].present, false, key);
|
||||||
|
assert.strictEqual(efEncSmsp(model, ff), ff);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('EF.SMSP: the TS 23.038 DCS hint', () => {
|
||||||
|
assert.strictEqual(smspDcsHint('00'), 'general data coding, GSM 7-bit, uncompressed');
|
||||||
|
assert.strictEqual(smspDcsHint('10'), 'general data coding, GSM 7-bit, uncompressed, class 0');
|
||||||
|
assert.strictEqual(smspDcsHint('08'), 'general data coding, UCS2, uncompressed');
|
||||||
|
assert.strictEqual(smspDcsHint('F4'), 'data coding, 8-bit data, class 0');
|
||||||
|
assert.strictEqual(smspDcsHint('D0'), 'MWI store, GSM 7-bit, inactive, voicemail');
|
||||||
|
assert.strictEqual(smspDcsHint('C0'), 'MWI discard, GSM 7-bit, inactive, voicemail');
|
||||||
|
assert.strictEqual(smspDcsHint('88'), 'reserved coding group');
|
||||||
|
assert.strictEqual(smspDcsHint('ZZ'), '');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('EF.SMSP: the TS 23.040 relative VP hint', () => {
|
||||||
|
const cases = { '00': '5 min', '8F': '12 h', '90': '12 h 30 min', 'A7': '24 h',
|
||||||
|
'A8': '2 days', 'C4': '30 days', 'C5': '5 weeks', 'FF': '63 weeks' };
|
||||||
|
for (const [hex, want] of Object.entries(cases)) assert.strictEqual(smspVpHint(hex), want, hex);
|
||||||
|
assert.strictEqual(smspVpHint('ZZ'), '');
|
||||||
|
});
|
||||||
|
|
||||||
|
// A present DA slot with TON 5 (alphanumeric, TS 23.040 9.1.2.5).
|
||||||
|
const SMSP_ALNUM = 'FF'.repeat(13) + 'FE' + '0750414243444546FFFFFFFF'
|
||||||
|
+ 'FF'.repeat(12) + 'FF' + 'FF' + 'FF';
|
||||||
|
// The live fixture with the SCA absent (indicator EB, FF slot).
|
||||||
|
const SMSP_SCA_ABSENT = SMSP_FIX.slice(0, 26) + 'eb' + SMSP_FIX.slice(28, 52)
|
||||||
|
+ 'FF'.repeat(12) + SMSP_FIX.slice(76);
|
||||||
|
// The live fixture with VP absent (indicator F9, FF slot).
|
||||||
|
const SMSP_VP_ABSENT = SMSP_FIX.slice(0, 26) + 'f9' + SMSP_FIX.slice(28, 80) + 'ff';
|
||||||
|
|
||||||
|
test('EF.SMSP: a TON 5 alphanumeric address is kept verbatim and only toggles', () => {
|
||||||
|
const model = efSmspModel(SMSP_ALNUM);
|
||||||
|
assert.strictEqual(model.params.da.present, true);
|
||||||
|
assert.strictEqual(model.params.da.alnum, true);
|
||||||
|
assert.strictEqual(model.params.da.ton, 5);
|
||||||
|
assert.strictEqual(model.params.da.number, '');
|
||||||
|
// an untouched round trip is byte-identical (no BCD re-encode)
|
||||||
|
assert.strictEqual(efEncSmsp(model, SMSP_ALNUM), SMSP_ALNUM.toUpperCase());
|
||||||
|
// editing another field leaves the alphanumeric slot verbatim
|
||||||
|
const edited = JSON.parse(JSON.stringify(model));
|
||||||
|
edited.alpha = 'X';
|
||||||
|
const out = efEncSmsp(edited, SMSP_ALNUM);
|
||||||
|
assert.strictEqual(out.slice(0, 4), '58FF');
|
||||||
|
assert.strictEqual(out.slice(28, 52), '0750414243444546FFFFFFFF');
|
||||||
|
// toggling it absent FF-fills the slot and sets its indicator bit
|
||||||
|
const absent = JSON.parse(JSON.stringify(model));
|
||||||
|
absent.params.da.present = false;
|
||||||
|
const off = efEncSmsp(absent, SMSP_ALNUM);
|
||||||
|
assert.strictEqual(off.slice(26, 28), 'FF');
|
||||||
|
assert.strictEqual(off.slice(28, 52), 'FF'.repeat(12));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('EF.SMSP: the VP presence bit (0x10) on decode and encode', () => {
|
||||||
|
const dec = efDecSmsp(efBytes(SMSP_VP_ABSENT));
|
||||||
|
assert.strictEqual(dec.indicators, '0xF9');
|
||||||
|
assert.strictEqual(dec.tp_vp, undefined);
|
||||||
|
assert.strictEqual(dec.sca, '+79332804900');
|
||||||
|
assert.strictEqual(efEncSmsp(efSmspModel(SMSP_VP_ABSENT), SMSP_VP_ABSENT), SMSP_VP_ABSENT.toUpperCase());
|
||||||
|
const model = efSmspModel(SMSP_FIX);
|
||||||
|
model.params.vp.present = false;
|
||||||
|
const out = efEncSmsp(model, SMSP_FIX);
|
||||||
|
assert.strictEqual(out.slice(26, 28), 'F9');
|
||||||
|
assert.strictEqual(out.slice(80, 82), 'FF');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('EF.SMSP: a newly present empty SCA writes the default-SMSC slot', () => {
|
||||||
|
const model = efSmspModel(SMSP_SCA_ABSENT);
|
||||||
|
assert.strictEqual(model.params.sca.present, false);
|
||||||
|
model.params.sca.present = true;
|
||||||
|
model.params.sca.number = '';
|
||||||
|
const out = efEncSmsp(model, SMSP_SCA_ABSENT);
|
||||||
|
assert.strictEqual(out.slice(26, 28), 'E9'); // the SCA bit cleared
|
||||||
|
assert.strictEqual(out.slice(52, 56), '00FF'); // length 0 + FF fill
|
||||||
|
});
|
||||||
|
|
||||||
|
test('EF.SMSP: a leading + forces TON 1 (international)', () => {
|
||||||
|
const base = efSmspModel(SMSP_FIX);
|
||||||
|
const v = JSON.parse(JSON.stringify(base));
|
||||||
|
v.params.sca = { present: true, number: '+4915790109999', ton: 0 };
|
||||||
|
const out = efEncSmsp(v, SMSP_FIX);
|
||||||
|
assert.strictEqual(out.slice(54, 56), '91');
|
||||||
|
const da = JSON.parse(JSON.stringify(base));
|
||||||
|
Object.assign(da.params.da, { present: true, number: '+12345', ton: 2 });
|
||||||
|
const out2 = efEncSmsp(da, SMSP_FIX);
|
||||||
|
assert.strictEqual(out2.slice(30, 32), '91'); // extension + TON 1 + NPI
|
||||||
|
});
|
||||||
|
|
||||||
|
test('EF.SMSP: the day/week VP ladder boundaries', () => {
|
||||||
|
assert.strictEqual(smspVpHint('A8'), '2 days');
|
||||||
|
assert.strictEqual(smspVpHint('AD'), '7 days');
|
||||||
|
assert.strictEqual(smspVpHint('C2'), '28 days');
|
||||||
|
assert.strictEqual(smspVpHint('C4'), '30 days');
|
||||||
|
assert.strictEqual(smspVpHint('C5'), '5 weeks');
|
||||||
|
assert.strictEqual(smspVpHint('FF'), '63 weeks');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('EF.SMSP: the remaining DCS hint branches', () => {
|
||||||
|
assert.strictEqual(smspDcsHint('40'), 'automatic deletion, GSM 7-bit, uncompressed');
|
||||||
|
assert.strictEqual(smspDcsHint('0C'), 'general data coding, reserved, uncompressed');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('EF.SMSP: the duration plural helper (Russian 1 / 2-4 / 5+)', () => {
|
||||||
|
assert.deepStrictEqual([1, 2, 4, 5, 11, 21, 22, 25].map(smspPluralIndex), [0, 1, 1, 2, 2, 0, 1, 2]);
|
||||||
|
globalThis.currentLang = 'ru';
|
||||||
|
globalThis.t = k => ({
|
||||||
|
day: 'день', 'days-few': 'дня', days: 'дней',
|
||||||
|
week: 'неделя', 'weeks-few': 'недели', weeks: 'недель',
|
||||||
|
})[k] || k;
|
||||||
|
try {
|
||||||
|
assert.strictEqual(smspCount(1, 'day', 'days-few', 'days'), '1 день');
|
||||||
|
assert.strictEqual(smspCount(2, 'day', 'days-few', 'days'), '2 дня');
|
||||||
|
assert.strictEqual(smspCount(5, 'day', 'days-few', 'days'), '5 дней');
|
||||||
|
assert.strictEqual(smspCount(2, 'week', 'weeks-few', 'weeks'), '2 недели');
|
||||||
|
assert.strictEqual(smspCount(5, 'week', 'weeks-few', 'weeks'), '5 недель');
|
||||||
|
} finally {
|
||||||
|
delete globalThis.currentLang;
|
||||||
|
delete globalThis.t;
|
||||||
|
}
|
||||||
|
assert.strictEqual(smspCount(2, 'day', 'days-few', 'days'), '2 days');
|
||||||
|
});
|
||||||
|
|||||||
@@ -40,29 +40,30 @@ function extractConst(src, name) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const FNS = [
|
const FNS = [
|
||||||
'efBytes', 'efHex', 'efAllFf', 'efRstripFf', 'efUcs2Be', 'efGsm7Octets',
|
'efBytes', 'efHex', 'efSwapNibbles', 'efAllFf', 'efRstripFf', 'efUcs2Be', 'efGsm7Octets',
|
||||||
'efAnnexA', 'efBcdAddress', 'efPlmn', 'efPlmnAct', 'efDecSpn', 'efDecPlmnWact',
|
'efAnnexA', 'efBcdAddress', 'efPlmn', 'efPlmnAct', 'efDecSpn', 'efDecPlmnWact',
|
||||||
'efDecAdn', 'efFindDecoder', 'efDecodeBytes', 'efIsRawOnly', 'efTlv',
|
'efDecAdn', 'efFindDecoder', 'efDecodeBytes', 'efIsRawOnly', 'efTlv',
|
||||||
'efPnnText', 'efDecItemsFromHex',
|
'efPnnText', 'efDecItemsFromHex',
|
||||||
'efGsm7PlainText', 'efEncTextBytes', 'efPlmnEnc', 'efActEnc', 'efEncSpn',
|
'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',
|
'efFindEncoder', 'efHasEncoder', 'efEncError',
|
||||||
'efDecModelValues', 'efDecEditModel', 'efActTechList', 'efDecFieldHtml',
|
'efDecModelValues', 'efDecModelFor', 'efDecEditModel', 'efActTechList', 'efDecFieldHtml',
|
||||||
'efDecRowHtml', 'efDecGridHtml', 'efDecItemRowHtml', 'efDecItemsHtml',
|
'efDecRowHtml', 'efDecGridHtml', 'efDecItemRowHtml', 'efDecItemsHtml',
|
||||||
'efDecRecordsHtml', 'efDecEditHtml',
|
'efDecRecordsHtml', 'efDecEditHtml',
|
||||||
'efDecCollectValues', 'efDecCollectRows', 'efDecCollectItems',
|
'efDecCollectValues', 'efDecCollectRows', 'efDecCollectItems',
|
||||||
'pysimFsDecRowCheck', 'pysimFsDecItemTag', 'pysimFsDecAddItem',
|
'efBitsSelected', 'pysimFsDecBitsFilter', 'pysimFsDecRowCheck', 'pysimFsDecItemTag', 'pysimFsDecAddItem',
|
||||||
'pysimFsDecRemoveItem', 'pysimFsFindNode',
|
'pysimFsDecRemoveItem', 'pysimFsFindNode',
|
||||||
'pysimFsHasDecoder', 'pysimFsHasEncoder', 'pysimFsDecSave', 'pysimFsDecEdit',
|
'pysimFsHasDecoder', 'pysimFsHasEncoder', 'pysimFsDecSave', 'pysimFsDecEdit',
|
||||||
'pysimFsPillsEnabled', 'pysimFsSyncContentVisibility', 'gsm7Decode',
|
'pysimFsPillsEnabled', 'pysimFsSyncContentVisibility', 'gsm7Decode',
|
||||||
'gsm7TextToSeptets', 'gsm7Encode',
|
'gsm7TextToSeptets', 'gsm7Encode',
|
||||||
|
'efDecServiceTable', 'efDecSst', 'efDecAcc', 'efDecPhase', 'efDecHpplmn', 'efDecAd', 'efDecCbmi', 'efDecCbmir', 'efDecIccid', 'efDecImsi', 'efDecPlmnList', 'efDecFplmn', 'efEncServiceTable', 'efEncAcc', 'efPhaseValue', 'efPhaseModel', 'efEncPhase', 'efEncHpplmn', 'efAdOpValue', 'efAdModel', 'efEncAd', 'efEncPlmnOnly', 'efEncIccid', 'efEncImsi', 'efCbmiModel', 'efEncCbmi', 'efCbmirModel', 'efEncCbmir', 'efSmspAddressModel', 'efSmspModel', 'efSmspTonOptions', 'efSmspParamRow', 'efSmspHtml', 'efSmspCollect', 'pysimFsSmspToggle', 'pysimFsSmspHexInput', 'efSmspHexByte', 'efSmspAddressBytes', 'efSmspAddrUnchanged', 'efSmspByteUnchanged', 'efEncSmsp', 'smspT', 'smspDcsHint', 'smspVpHint', 'smspDurationText', 'smspPluralIndex', 'smspCount', 'efSmspDaNumber',
|
||||||
];
|
];
|
||||||
|
|
||||||
let code = 'var pysimFsSelected = null;\nvar pysimFsTreeRoot = null;\nvar pysimFsDecModel = null;\n'
|
let code = 'var pysimFsSelected = null;\nvar pysimFsTreeRoot = null;\nvar pysimFsDecModel = null;\n'
|
||||||
+ 'var pysimFsDecEditMode = false;\nvar pysimFsEditMode = false;\n';
|
+ 'var pysimFsDecEditMode = false;\nvar pysimFsEditMode = false;\n';
|
||||||
for (const f of FNS) code += extractFunc(html, f) + '\n';
|
for (const f of FNS) code += extractFunc(html, f) + '\n';
|
||||||
for (const c of ['GSM7_ALPHABET', 'GSM7_EXT_MAP', 'EF_ACT_TECHS', 'EF_SPN_FIELDS',
|
for (const c of ['GSM7_ALPHABET', 'GSM7_EXT_MAP', 'EF_ACT_TECHS', 'EF_SPN_FIELDS',
|
||||||
'EF_ADN_FIELDS', 'EF_PLMN_LIST', 'EF_PNN_ITEMS', 'EF_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_SMSP_PARAMS', 'EF_SMSP_CUSTOM', 'EF_ENCODERS']) {
|
||||||
code += extractConst(html, c);
|
code += extractConst(html, c);
|
||||||
}
|
}
|
||||||
eval(code);
|
eval(code);
|
||||||
@@ -70,6 +71,22 @@ eval(code);
|
|||||||
// The decoder entries these files resolve to (the full EF_DECODERS registry is
|
// 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).
|
// pinned by ef_decode.test.js; only the model's lookup path is exercised here).
|
||||||
var EF_DECODERS = [
|
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.SPN', fid: '6f46', fn: efDecSpn },
|
||||||
{ name: 'EF.PLMNwAcT', fid: '6f60', fn: efDecPlmnWact },
|
{ name: 'EF.PLMNwAcT', fid: '6f60', fn: efDecPlmnWact },
|
||||||
{ name: 'EF.OPLMNwAcT', fid: '6f61', 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('ef.plmnwact', null).name, 'EF.PLMNwAcT');
|
||||||
assert.strictEqual(efFindEncoder('MF/7F10/6F3A/EF.ADN', null).name, 'EF.ADN');
|
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.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(efFindEncoder(null, null), null);
|
||||||
assert.strictEqual(efHasEncoder('EF.SDN', null), true);
|
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', () => {
|
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.deepStrictEqual(adn.records[1].values, {});
|
||||||
assert.strictEqual(adn.records[1].orig, ADN_FF);
|
assert.strictEqual(adn.records[1].orig, ADN_FF);
|
||||||
// no encoder -> no model, even when a decoder exists
|
// 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);
|
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);
|
assert.strictEqual(pysimFsHasEncoder(), true);
|
||||||
pysimFsSelected = '6F46';
|
pysimFsSelected = '6F46';
|
||||||
assert.strictEqual(pysimFsHasEncoder(), true);
|
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);
|
assert.strictEqual(pysimFsHasEncoder(), false);
|
||||||
pysimFsSelected = null;
|
pysimFsSelected = null;
|
||||||
assert.strictEqual(pysimFsHasEncoder(), false);
|
assert.strictEqual(pysimFsHasEncoder(), false);
|
||||||
@@ -653,3 +674,435 @@ test('F12: a selection change during the read abandons the decoded edit', async
|
|||||||
assert.strictEqual(resets, 1);
|
assert.strictEqual(resets, 1);
|
||||||
assert.strictEqual(content.innerHTML, '', 'the old file must not render over the new selection');
|
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));
|
||||||
|
});
|
||||||
|
|
||||||
|
// EF.SMSP: the custom (dedicated builder) entry kind.
|
||||||
|
const SMSP_FIX = 'ffffffffffffffffffffffffffe9ffffffffffffffffffffffff07919733824009f0ffffffff0000ff';
|
||||||
|
|
||||||
|
function smspSaveBlock(orig, num, values) {
|
||||||
|
const inputs = { alpha: { value: values.alpha } };
|
||||||
|
for (const key of ['da', 'sca', 'pid', 'dcs', 'vp']) {
|
||||||
|
const p = values.params[key];
|
||||||
|
if (key === 'da' || key === 'sca') {
|
||||||
|
inputs[key + '.number'] = { value: p.number };
|
||||||
|
inputs[key + '.ton'] = { value: String(p.ton) };
|
||||||
|
} else {
|
||||||
|
inputs[key + '.value'] = { value: p.value };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const rows = {};
|
||||||
|
for (const key of ['da', 'sca', 'pid', 'dcs', 'vp']) {
|
||||||
|
rows[key] = {
|
||||||
|
querySelector: sel => sel === '[data-smsp-absent="' + key + '"]'
|
||||||
|
? { checked: !values.params[key].present } : null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
querySelector: sel => {
|
||||||
|
if (sel === '.pysim-fs-record-cb') return { checked: true };
|
||||||
|
let m = /^\[data-smsp="(.+)"\]$/.exec(sel);
|
||||||
|
if (m) return inputs[m[1]] || null;
|
||||||
|
m = /^\[data-smsp-orig="(.+)"\]$/.exec(sel);
|
||||||
|
return m ? (rows[m[1]] || null) : null;
|
||||||
|
},
|
||||||
|
querySelectorAll: () => [],
|
||||||
|
getAttribute: k => k === 'data-rec' ? String(num) : (k === 'data-orig' ? orig : null),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
test('the SMSP custom form renders the parameter rows, presence and hints', () => {
|
||||||
|
const model = efDecEditModel('EF.SMSP', '6f42', { kind: 'record', records: [{ num: 1, data: SMSP_FIX }] });
|
||||||
|
assert.strictEqual(model.mode, 'records');
|
||||||
|
assert.strictEqual(model.records[0].values.params.sca.number, '+79332804900');
|
||||||
|
const out = efDecEditHtml(model);
|
||||||
|
assert.match(out, /class="pysim-dec-items/);
|
||||||
|
assert.match(out, /data-smsp="alpha" value=""/);
|
||||||
|
assert.match(out, /data-smsp-absent="da" checked/);
|
||||||
|
assert.doesNotMatch(out, /data-smsp-absent="sca" checked/);
|
||||||
|
assert.match(out, /data-smsp="sca.number" value="\+79332804900"/);
|
||||||
|
assert.match(out, /data-smsp="pid.value" value="0x00"/);
|
||||||
|
assert.match(out, /data-smsp="dcs.value" value="0x00"/);
|
||||||
|
assert.match(out, /data-smsp="vp.value" value="0xFF"/);
|
||||||
|
// an absent DCS shows the neutral ME-default note (not a decoded hint);
|
||||||
|
// the present VP still shows its relative-VP hint
|
||||||
|
assert.match(out, /data-smsp-hint>absent — the ME default applies/);
|
||||||
|
assert.doesNotMatch(out, /general data coding/);
|
||||||
|
assert.match(out, /63 weeks/);
|
||||||
|
assert.match(out, /TS 23.040 TP-PID \(raw\)/);
|
||||||
|
// a present DCS renders its decoded TS 23.038 hint (the 44-byte vector)
|
||||||
|
const vec = efDecEditModel('EF.SMSP', '6f42', { kind: 'record', records: [{ num: 1,
|
||||||
|
data: '534d5343ffffffffffffffffffffffffe1ffffffffffffffffffffffff0891945197109099f9ffffff0000a9' }] });
|
||||||
|
assert.match(efDecEditHtml(vec), /general data coding, GSM 7-bit, uncompressed/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('the SMSP custom collect reads the rows back with their original slots', () => {
|
||||||
|
const model = efDecEditModel('EF.SMSP', '6f42', { kind: 'record', records: [{ num: 1, data: SMSP_FIX }] });
|
||||||
|
const block = smspSaveBlock(SMSP_FIX, 1, model.records[0].values);
|
||||||
|
assert.deepStrictEqual(efSmspCollect(block), {
|
||||||
|
alpha: '',
|
||||||
|
params: {
|
||||||
|
da: { present: false, number: '', ton: 1 },
|
||||||
|
sca: { present: true, number: '+79332804900', ton: 1 },
|
||||||
|
pid: { present: true, value: '0x00' },
|
||||||
|
dcs: { present: false, value: '0x00' },
|
||||||
|
vp: { present: true, value: '0xFF' },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('an untouched SMSP record saves byte-identically; an edit re-encodes', async () => {
|
||||||
|
const entry = efFindEncoder('EF.SMSP', null);
|
||||||
|
const model = efDecEditModel('EF.SMSP', '6f42', { kind: 'record', records: [{ num: 1, data: SMSP_FIX }] });
|
||||||
|
pysimFsDecModel = model;
|
||||||
|
const out = { querySelector: () => null,
|
||||||
|
querySelectorAll: sel => sel === '.pysim-dec-rec'
|
||||||
|
? [smspSaveBlock(SMSP_FIX, 1, model.records[0].values)] : [] };
|
||||||
|
let stubs = saveStubs({ success: true, sw: '9000' });
|
||||||
|
let status = { textContent: '' };
|
||||||
|
await pysimFsDecSave(null, 'EF.SMSP', status, out, false);
|
||||||
|
assert.strictEqual(stubs.bodies.length, 1);
|
||||||
|
assert.strictEqual(stubs.bodies[0].body.record_nr, 1);
|
||||||
|
assert.strictEqual(stubs.bodies[0].body.data, SMSP_FIX.toUpperCase());
|
||||||
|
// edit the SCA number
|
||||||
|
const edited = JSON.parse(JSON.stringify(model.records[0].values));
|
||||||
|
edited.params.sca.number = '+4915790109999';
|
||||||
|
stubs = saveStubs({ success: true, sw: '9000' });
|
||||||
|
status = { textContent: '' };
|
||||||
|
const out2 = { querySelector: () => null,
|
||||||
|
querySelectorAll: sel => sel === '.pysim-dec-rec' ? [smspSaveBlock(SMSP_FIX, 1, edited)] : [] };
|
||||||
|
await pysimFsDecSave(null, 'EF.SMSP', status, out2, false);
|
||||||
|
assert.strictEqual(stubs.bodies[0].body.data, efEncSmsp(edited, SMSP_FIX));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('the SMSP absent toggle disables the row inputs and the hex hint is live', () => {
|
||||||
|
const inputs = [{ disabled: false }, { disabled: false }];
|
||||||
|
const hint = { textContent: '' };
|
||||||
|
const row = {
|
||||||
|
getAttribute: () => 'dcs',
|
||||||
|
querySelector: sel => sel === '[data-smsp-hint]' ? hint : null,
|
||||||
|
querySelectorAll: sel => sel === '[data-smsp]' ? inputs : [],
|
||||||
|
};
|
||||||
|
const cb = { checked: true, closest: sel => sel === '[data-smsp-orig]' ? row : null };
|
||||||
|
pysimFsSmspToggle(cb);
|
||||||
|
assert.ok(inputs.every(i => i.disabled));
|
||||||
|
pysimFsSmspHexInput({ value: 'F4', closest: sel => sel === '[data-smsp-orig]' ? row : null });
|
||||||
|
assert.strictEqual(hint.textContent, 'data coding, 8-bit data, class 0');
|
||||||
|
const vpHint = { textContent: '' };
|
||||||
|
const vpRow = {
|
||||||
|
getAttribute: () => 'vp',
|
||||||
|
querySelector: sel => sel === '[data-smsp-hint]' ? vpHint : null,
|
||||||
|
};
|
||||||
|
pysimFsSmspHexInput({ value: '90', closest: () => vpRow });
|
||||||
|
assert.strictEqual(vpHint.textContent, '12 h 30 min');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('the SMSP entry is wired into the encoder registry', () => {
|
||||||
|
const entry = efFindEncoder('EF.SMSP', null);
|
||||||
|
assert.strictEqual(entry.fid, '6f42');
|
||||||
|
assert.strictEqual(typeof entry.custom.html, 'function');
|
||||||
|
assert.strictEqual(typeof entry.custom.collect, 'function');
|
||||||
|
assert.strictEqual(typeof entry.custom.model, 'function');
|
||||||
|
assert.strictEqual(efHasEncoder('EF.SMSP', '6f42'), true);
|
||||||
|
});
|
||||||
|
|
||||||
|
const SMSP_ALNUM = 'FF'.repeat(13) + 'FE' + '0750414243444546FFFFFFFF'
|
||||||
|
+ 'FF'.repeat(12) + 'FF' + 'FF' + 'FF';
|
||||||
|
const SMSP_VEC = '534d5343ffffffffffffffffffffffffe1ffffffffffffffffffffffff0891945197109099f9ffffff0000a9';
|
||||||
|
|
||||||
|
test('the SMSP TON 5 alphanumeric slot is read-only in the form', () => {
|
||||||
|
const model = efDecEditModel('EF.SMSP', '6f42', { kind: 'record', records: [{ num: 1, data: SMSP_ALNUM }] });
|
||||||
|
assert.strictEqual(model.records[0].values.params.da.alnum, true);
|
||||||
|
const out = efDecEditHtml(model);
|
||||||
|
assert.doesNotMatch(out, /data-smsp="da.number"/);
|
||||||
|
assert.doesNotMatch(out, /data-smsp="da.ton"/);
|
||||||
|
assert.match(out, /alphanumeric address — not editable here; it is preserved as is/);
|
||||||
|
assert.match(out, /0750414243444546FFFFFFFF/);
|
||||||
|
// still togglable absent, and the encoder keeps it verbatim while present
|
||||||
|
const block = smspSaveBlock(SMSP_ALNUM, 1, model.records[0].values);
|
||||||
|
const vals = efSmspCollect(block);
|
||||||
|
assert.strictEqual(vals.params.da.present, true);
|
||||||
|
assert.strictEqual(efEncSmsp(vals, SMSP_ALNUM), SMSP_ALNUM.toUpperCase());
|
||||||
|
});
|
||||||
|
|
||||||
|
test('the SMSP TON select keeps an unknown current value', () => {
|
||||||
|
const model = efSmspModel(SMSP_FIX);
|
||||||
|
model.params.sca.ton = 3;
|
||||||
|
const html = efSmspHtml(model);
|
||||||
|
assert.match(html, /<option value="3" selected>Current 3<\/option>/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('a multi-record SMSP save scopes each block to its own record', async () => {
|
||||||
|
const entry = efFindEncoder('EF.SMSP', null);
|
||||||
|
// record 1 untouched, record 2 (the 44-byte vector) with an edited alpha
|
||||||
|
const model = efDecEditModel('EF.SMSP', '6f42', { kind: 'record', records: [
|
||||||
|
{ num: 1, data: SMSP_FIX }, { num: 2, data: SMSP_VEC },
|
||||||
|
] });
|
||||||
|
const edited = JSON.parse(JSON.stringify(model.records[1].values));
|
||||||
|
edited.alpha = 'Two';
|
||||||
|
pysimFsDecModel = model;
|
||||||
|
const blocks = [smspSaveBlock(SMSP_FIX, 1, model.records[0].values),
|
||||||
|
smspSaveBlock(SMSP_VEC, 2, edited)];
|
||||||
|
const out = { querySelector: () => null,
|
||||||
|
querySelectorAll: sel => sel === '.pysim-dec-rec' ? blocks : [] };
|
||||||
|
const stubs = saveStubs({ success: true, sw: '9000' });
|
||||||
|
const status = { textContent: '' };
|
||||||
|
await pysimFsDecSave(null, 'EF.SMSP', status, out, false);
|
||||||
|
assert.strictEqual(stubs.bodies.length, 2);
|
||||||
|
assert.deepStrictEqual(stubs.bodies.map(b => b.body.record_nr), [1, 2]);
|
||||||
|
assert.strictEqual(stubs.bodies[0].body.data, SMSP_FIX.toUpperCase());
|
||||||
|
assert.strictEqual(stubs.bodies[1].body.data, efEncSmsp(edited, SMSP_VEC));
|
||||||
|
assert.notStrictEqual(stubs.bodies[1].body.data, SMSP_VEC.toUpperCase());
|
||||||
|
});
|
||||||
|
|
||||||
|
test('the SMSP DCS hint follows the absent toggle', () => {
|
||||||
|
const hint = { textContent: 'general data coding, GSM 7-bit, uncompressed' };
|
||||||
|
const input = { value: '00' };
|
||||||
|
const cb = { checked: false };
|
||||||
|
const row = {
|
||||||
|
getAttribute: () => 'dcs',
|
||||||
|
querySelector: sel => {
|
||||||
|
if (sel === '[data-smsp-hint]') return hint;
|
||||||
|
if (sel === '[data-smsp-absent]') return cb;
|
||||||
|
if (sel === '[data-smsp="dcs.value"]') return input;
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
querySelectorAll: () => [],
|
||||||
|
};
|
||||||
|
cb.checked = true;
|
||||||
|
cb.closest = sel => sel === '[data-smsp-orig]' ? row : null;
|
||||||
|
pysimFsSmspToggle(cb);
|
||||||
|
assert.strictEqual(hint.textContent, 'absent — the ME default applies');
|
||||||
|
cb.checked = false;
|
||||||
|
pysimFsSmspToggle(cb);
|
||||||
|
assert.strictEqual(hint.textContent, 'general data coding, GSM 7-bit, uncompressed');
|
||||||
|
});
|
||||||
|
|||||||
@@ -117,12 +117,19 @@ test('Read raw / Read decoded pills switch the view and read the file', () => {
|
|||||||
assert.strictEqual(h.reads(), 0, 'skipRead updates the pills only');
|
assert.strictEqual(h.reads(), 0, 'skipRead updates the pills only');
|
||||||
assert.ok(h.pill('raw').classList.contains('bg-blue-600'));
|
assert.ok(h.pill('raw').classList.contains('bg-blue-600'));
|
||||||
assert.ok(!h.pill('dec').classList.contains('bg-blue-600'));
|
assert.ok(!h.pill('dec').classList.contains('bg-blue-600'));
|
||||||
|
// an enabled inactive pill is outlined, not grey: the grey fill reads as
|
||||||
|
// "disabled" although the pill is clickable (reported live)
|
||||||
|
assert.ok(h.pill('dec').classList.contains('bg-white'));
|
||||||
|
assert.ok(h.pill('dec').classList.contains('text-blue-600'));
|
||||||
|
assert.ok(!h.pill('dec').classList.contains('bg-gray-200'));
|
||||||
|
|
||||||
pysimFsSetMode('dec');
|
pysimFsSetMode('dec');
|
||||||
assert.strictEqual(pysimFsDecodedMode, true);
|
assert.strictEqual(pysimFsDecodedMode, true);
|
||||||
assert.strictEqual(h.reads(), 1, 'a pill click reads the selected file');
|
assert.strictEqual(h.reads(), 1, 'a pill click reads the selected file');
|
||||||
assert.ok(h.pill('dec').classList.contains('bg-blue-600'));
|
assert.ok(h.pill('dec').classList.contains('bg-blue-600'));
|
||||||
assert.ok(!h.pill('raw').classList.contains('bg-blue-600'));
|
assert.ok(!h.pill('raw').classList.contains('bg-blue-600'));
|
||||||
|
assert.ok(h.pill('raw').classList.contains('bg-white'));
|
||||||
|
assert.ok(!h.pill('raw').classList.contains('bg-gray-200'));
|
||||||
});
|
});
|
||||||
|
|
||||||
test('pill clicks are ignored while editing', () => {
|
test('pill clicks are ignored while editing', () => {
|
||||||
@@ -221,4 +228,7 @@ test('the file manager row exposes the read pills and Edit raw', () => {
|
|||||||
assert.ok(html.includes('id="pysim-fs-read-raw-btn"') && html.includes('data-needs="card"'));
|
assert.ok(html.includes('id="pysim-fs-read-raw-btn"') && html.includes('data-needs="card"'));
|
||||||
const dec = /<button[^>]*id="pysim-fs-read-dec-btn"[^>]*>/.exec(html);
|
const dec = /<button[^>]*id="pysim-fs-read-dec-btn"[^>]*>/.exec(html);
|
||||||
assert.ok(dec && dec[0].includes('data-needs-decoder'), 'the decoded pill is decoder-gated');
|
assert.ok(dec && dec[0].includes('data-needs-decoder'), 'the decoded pill is decoder-gated');
|
||||||
|
// the enabled-inactive look is an outline; the grey fill is for disabled
|
||||||
|
assert.ok(dec[0].includes('bg-white') && dec[0].includes('text-blue-600'));
|
||||||
|
assert.ok(!dec[0].includes('bg-gray-200'));
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -28,7 +28,10 @@ for (const fn of ['ramIsBatchOp', 'ramBatchPanelVisible', 'ramBatchMergedState',
|
|||||||
'ramBatchStepsText',
|
'ramBatchStepsText',
|
||||||
'ramBatchAttemptsHtml', 'ramBatchRender', 'ramBatchApplyStatus', 'ramBatchFetch',
|
'ramBatchAttemptsHtml', 'ramBatchRender', 'ramBatchApplyStatus', 'ramBatchFetch',
|
||||||
'ramBatchStart', 'ramBatchStop', 'ramBatchClear', 'ramOpProgressText',
|
'ramBatchStart', 'ramBatchStop', 'ramBatchClear', 'ramOpProgressText',
|
||||||
'ramOpModalClose', 'esc']) {
|
'ramOpModalClose', 'esc',
|
||||||
|
'ramBatchSoundEnabled', 'ramBatchUpdateSoundButton', 'ramBatchToggleSound',
|
||||||
|
'ramBatchAudioUnlock', 'ramBatchChime', 'ramBatchTitleHint',
|
||||||
|
'ramBatchWaitingCue']) {
|
||||||
code += extractFunc(html, fn, ASYNC_FNS.indexOf(fn) >= 0) + '\n';
|
code += extractFunc(html, fn, ASYNC_FNS.indexOf(fn) >= 0) + '\n';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,7 +42,9 @@ const els = {};
|
|||||||
function setEl(id, value) {
|
function setEl(id, value) {
|
||||||
els[id] = {
|
els[id] = {
|
||||||
id: id, value: value === undefined ? '' : value, textContent: '', innerHTML: '',
|
id: id, value: value === undefined ? '' : value, textContent: '', innerHTML: '',
|
||||||
disabled: false, style: {},
|
disabled: false, style: {}, title: '', attrs: {},
|
||||||
|
setAttribute(k, v) { els[id].attrs[k] = String(v); },
|
||||||
|
getAttribute(k) { return els[id].attrs[k]; },
|
||||||
classes: {},
|
classes: {},
|
||||||
classList: {
|
classList: {
|
||||||
add(c) { els[id].classes[c] = true; },
|
add(c) { els[id].classes[c] = true; },
|
||||||
@@ -64,7 +69,8 @@ function clearEls() {
|
|||||||
function setModalEls() {
|
function setModalEls() {
|
||||||
for (const id of ['ram-op-modal', 'ram-op-title', 'ram-op-status', 'ram-op-bar',
|
for (const id of ['ram-op-modal', 'ram-op-title', 'ram-op-status', 'ram-op-bar',
|
||||||
'ram-op-steps', 'ram-op-spin', 'ram-op-warn', 'ram-op-close',
|
'ram-op-steps', 'ram-op-spin', 'ram-op-warn', 'ram-op-close',
|
||||||
'ram-op-batch-info', 'ram-op-batch-stop', 'ram-op-batch-clear']) setEl(id, '');
|
'ram-op-batch-info', 'ram-op-batch-stop', 'ram-op-batch-clear',
|
||||||
|
'ram-op-insert', 'ram-op-sound']) setEl(id, '');
|
||||||
}
|
}
|
||||||
function setPanelEls() {
|
function setPanelEls() {
|
||||||
for (const id of ['ram-op', 'ram-batch-panel', 'ram-batch-start', 'ram-batch-stop',
|
for (const id of ['ram-op', 'ram-batch-panel', 'ram-batch-start', 'ram-batch-stop',
|
||||||
@@ -75,7 +81,17 @@ function setPanelEls() {
|
|||||||
|
|
||||||
const app = new Function('els', `
|
const app = new Function('els', `
|
||||||
var sandbox = { fetched: [], replies: {}, fetchError: null, alerts: [], visibility: true };
|
var sandbox = { fetched: [], replies: {}, fetchError: null, alerts: [], visibility: true };
|
||||||
var document = { getElementById: id => els[id] || null };
|
var document = {
|
||||||
|
getElementById: id => els[id] || null,
|
||||||
|
hidden: false, title: 'SIMple',
|
||||||
|
addEventListener: () => {},
|
||||||
|
};
|
||||||
|
var _ls = {};
|
||||||
|
var localStorage = {
|
||||||
|
getItem: k => (k in _ls ? _ls[k] : null),
|
||||||
|
setItem: (k, v) => { _ls[k] = String(v); },
|
||||||
|
removeItem: k => { delete _ls[k]; },
|
||||||
|
};
|
||||||
var t = k => k;
|
var t = k => k;
|
||||||
var isViewVisible = () => sandbox.visibility;
|
var isViewVisible = () => sandbox.visibility;
|
||||||
var alert = m => sandbox.alerts.push(String(m));
|
var alert = m => sandbox.alerts.push(String(m));
|
||||||
@@ -109,6 +125,9 @@ const app = new Function('els', `
|
|||||||
var _ramBatchFetchAt = 0;
|
var _ramBatchFetchAt = 0;
|
||||||
var _ramBatchSummary = null;
|
var _ramBatchSummary = null;
|
||||||
var _ramBatchProgress = null;
|
var _ramBatchProgress = null;
|
||||||
|
var _ramBatchWaiting = false;
|
||||||
|
var _ramBatchAudio = null;
|
||||||
|
var _ramBatchTitleBase = '';
|
||||||
var RAM_BATCH_FETCH_STALE_MS = ${RAM_BATCH_FETCH_STALE_MS};
|
var RAM_BATCH_FETCH_STALE_MS = ${RAM_BATCH_FETCH_STALE_MS};
|
||||||
${code}
|
${code}
|
||||||
sandbox.getLast = () => _ramBatchLast;
|
sandbox.getLast = () => _ramBatchLast;
|
||||||
@@ -134,6 +153,16 @@ const app = new Function('els', `
|
|||||||
sandbox.ramBatchStart = ramBatchStart;
|
sandbox.ramBatchStart = ramBatchStart;
|
||||||
sandbox.ramBatchStop = ramBatchStop;
|
sandbox.ramBatchStop = ramBatchStop;
|
||||||
sandbox.ramBatchClear = ramBatchClear;
|
sandbox.ramBatchClear = ramBatchClear;
|
||||||
|
sandbox.ramBatchWaitingCue = ramBatchWaitingCue;
|
||||||
|
sandbox.ramBatchTitleHint = ramBatchTitleHint;
|
||||||
|
sandbox.ramBatchSoundEnabled = ramBatchSoundEnabled;
|
||||||
|
sandbox.ramBatchUpdateSoundButton = ramBatchUpdateSoundButton;
|
||||||
|
sandbox.ramBatchToggleSound = ramBatchToggleSound;
|
||||||
|
sandbox.setHidden = v => { document.hidden = !!v; };
|
||||||
|
sandbox.getTitle = () => document.title;
|
||||||
|
sandbox.resetWaiting = () => { _ramBatchWaiting = false; };
|
||||||
|
sandbox.storage = _ls;
|
||||||
|
sandbox.resetStorage = () => { for (const k of Object.keys(_ls)) delete _ls[k]; };
|
||||||
return sandbox;
|
return sandbox;
|
||||||
`)(els);
|
`)(els);
|
||||||
|
|
||||||
@@ -639,3 +668,96 @@ test('the stop and clear calls hit their endpoints', async () => {
|
|||||||
await new Promise(r => setImmediate(r));
|
await new Promise(r => setImmediate(r));
|
||||||
assert.deepStrictEqual(app.fetched, ['/api/ram-batch/stop', '/api/ram-batch/clear', '/api/ram-batch/status']);
|
assert.deepStrictEqual(app.fetched, ['/api/ram-batch/stop', '/api/ram-batch/clear', '/api/ram-batch/status']);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
test('the waiting cue fires once per card and drives the insert animation (v3.36.0)', () => {
|
||||||
|
clearEls();
|
||||||
|
setPanelEls();
|
||||||
|
setModalEls();
|
||||||
|
app.resetWaiting();
|
||||||
|
assert.strictEqual(app.ramBatchWaitingCue(true), true, 'entering waiting cues');
|
||||||
|
assert.strictEqual(app.ramBatchWaitingCue(true), false, 'still waiting: no repeat');
|
||||||
|
assert.strictEqual(app.ramBatchWaitingCue(false), false, 'installing resets');
|
||||||
|
assert.strictEqual(app.ramBatchWaitingCue(true), true, 'the next card cues again');
|
||||||
|
assert.ok(els['ram-op-insert'].classes['flex'] === true);
|
||||||
|
assert.ok(!els['ram-op-insert'].classes['hidden']);
|
||||||
|
app.ramBatchWaitingCue(false);
|
||||||
|
assert.ok(els['ram-op-insert'].classes['hidden']);
|
||||||
|
// the render drives it from the phase
|
||||||
|
app.setLast({ active: true, phase: 'waiting_card', attempts: [], counts: {}, attempts_total: 0, notice: '' });
|
||||||
|
app.ramBatchRender();
|
||||||
|
assert.ok(!els['ram-op-insert'].classes['hidden']);
|
||||||
|
app.setLast({ active: true, phase: 'installing', current: { iccid: '8970119000004600098' },
|
||||||
|
attempts: [], counts: {}, attempts_total: 1 });
|
||||||
|
app.ramBatchRender();
|
||||||
|
assert.ok(els['ram-op-insert'].classes['hidden']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('the sound toggle persists and updates the button (v3.36.0)', () => {
|
||||||
|
clearEls();
|
||||||
|
setModalEls();
|
||||||
|
app.resetStorage();
|
||||||
|
assert.strictEqual(app.ramBatchSoundEnabled(), true, 'sound is on by default');
|
||||||
|
app.ramBatchUpdateSoundButton();
|
||||||
|
assert.strictEqual(els['ram-op-sound'].textContent, '\u{1F50A}');
|
||||||
|
assert.strictEqual(els['ram-op-sound'].attrs['aria-pressed'], 'true');
|
||||||
|
app.ramBatchToggleSound();
|
||||||
|
assert.strictEqual(app.ramBatchSoundEnabled(), false);
|
||||||
|
assert.strictEqual(app.storage['simple_ram_batch_sound'], '0');
|
||||||
|
assert.strictEqual(els['ram-op-sound'].textContent, '\u{1F507}');
|
||||||
|
assert.strictEqual(els['ram-op-sound'].attrs['aria-pressed'], 'false');
|
||||||
|
app.ramBatchToggleSound();
|
||||||
|
assert.strictEqual(app.ramBatchSoundEnabled(), true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('the tab title hints while the run waits and the tab is hidden (v3.36.0)', () => {
|
||||||
|
app.setHidden(false);
|
||||||
|
app.ramBatchTitleHint(true);
|
||||||
|
assert.strictEqual(app.getTitle(), 'SIMple');
|
||||||
|
app.setHidden(true);
|
||||||
|
app.ramBatchTitleHint(true);
|
||||||
|
assert.strictEqual(app.getTitle(), 'Insert next card \u2014 SIMple');
|
||||||
|
app.setHidden(false);
|
||||||
|
app.ramBatchTitleHint(true);
|
||||||
|
assert.strictEqual(app.getTitle(), 'SIMple');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('the waiting cue is wired into the poll render, the Start gesture and the CSS (v3.36.0)', () => {
|
||||||
|
const render = extractFunc(html, 'ramBatchRender');
|
||||||
|
assert.match(render, /const waiting = active && phase !== 'installing' && phase !== 'stopping';/);
|
||||||
|
assert.match(render, /if \(ramBatchWaitingCue\(waiting\)\) ramBatchChime\(\);/);
|
||||||
|
assert.match(extractFunc(html, 'ramBatchStart'), /ramBatchAudioUnlock\(\)/);
|
||||||
|
assert.match(html, /id="ram-op-insert"/);
|
||||||
|
assert.match(html, /id="ram-op-sound"/);
|
||||||
|
const css = fs.readFileSync(path.join(__dirname, '..', 'style.css'), 'utf8');
|
||||||
|
assert.match(css, /\.ram-insert-card/);
|
||||||
|
assert.match(css, /@keyframes ram-insert/);
|
||||||
|
assert.match(css, /\.ram-insert-glow/);
|
||||||
|
assert.match(css, /prefers-reduced-motion: reduce/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('an already-absent delete is reported as such, not as a deletion (v3.36.0)', () => {
|
||||||
|
clearEls();
|
||||||
|
setPanelEls();
|
||||||
|
setModalEls();
|
||||||
|
app.resetBatchModal();
|
||||||
|
app.setLast({ active: false, phase: 'finished', kind: 'delete',
|
||||||
|
attempts: [{
|
||||||
|
iccid: '8970119000000115026', preset: 'Alfa', result: 'already_absent',
|
||||||
|
steps: [{ name: 'DELETE (application)', sw: '6A88', ok: true, not_found: true }],
|
||||||
|
steps_total: 1, duration_ms: 1200,
|
||||||
|
}],
|
||||||
|
counts: { installed: 2, already_absent: 1, failed: 0, skipped: 0 },
|
||||||
|
attempts_total: 3, notice: 'Card 8970119000000115026 was already absent.' });
|
||||||
|
app.ramBatchRender();
|
||||||
|
// the counts separate a real deletion from a no-op
|
||||||
|
assert.match(els['ram-batch-counts'].textContent, /Deleted: 2/);
|
||||||
|
assert.match(els['ram-batch-counts'].textContent, /Already absent: 1/);
|
||||||
|
// the attempt's verdict says it - not a plain OK - and keeps the step line
|
||||||
|
assert.match(els['ram-batch-attempts'].innerHTML, /Already absent/);
|
||||||
|
assert.match(els['ram-batch-attempts'].innerHTML, /6A88 \(not found\)/);
|
||||||
|
// the modal carries the same counts
|
||||||
|
app.ramBatchModalOpen();
|
||||||
|
assert.match(els['ram-op-batch-info'].textContent, /Deleted: 2/);
|
||||||
|
assert.match(els['ram-op-batch-info'].textContent, /Already absent: 1/);
|
||||||
|
});
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "pysim-simple-server"
|
name = "pysim-simple-server"
|
||||||
version = "3.33.0"
|
version = "3.36.0"
|
||||||
description = "HTTP REST server wrapping pysim for the SIMple PWA"
|
description = "HTTP REST server wrapping pysim for the SIMple PWA"
|
||||||
license = { text = "GPL-2.0-or-later" }
|
license = { text = "GPL-2.0-or-later" }
|
||||||
requires-python = ">=3.9"
|
requires-python = ">=3.9"
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ from osmocom.tlv import BER_TLV_IE
|
|||||||
from osmocom.utils import rpad
|
from osmocom.utils import rpad
|
||||||
|
|
||||||
|
|
||||||
VERSION = '3.33.0'
|
VERSION = '3.36.0'
|
||||||
|
|
||||||
MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE
|
MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE
|
||||||
|
|
||||||
@@ -2914,7 +2914,7 @@ def _ram_batch_new_state(plan):
|
|||||||
return {'active': True, 'phase': 'waiting_card', 'stop': False,
|
return {'active': True, 'phase': 'waiting_card', 'stop': False,
|
||||||
'started': time.time(), 'stopped': None, 'plan': plan,
|
'started': time.time(), 'stopped': None, 'plan': plan,
|
||||||
'session': None, 'seen': {}, 'attempts': [], 'attempts_total': 0,
|
'session': None, 'seen': {}, 'attempts': [], 'attempts_total': 0,
|
||||||
'counts': {'installed': 0, 'failed': 0, 'skipped': 0},
|
'counts': {'installed': 0, 'already_absent': 0, 'failed': 0, 'skipped': 0},
|
||||||
'current': None, 'notice': ''}
|
'current': None, 'notice': ''}
|
||||||
|
|
||||||
|
|
||||||
@@ -2939,6 +2939,8 @@ def _ram_batch_record(state, rec):
|
|||||||
verb = {'delete': 'deleted',
|
verb = {'delete': 'deleted',
|
||||||
'reinstall-app': 'reinstalled'}.get(kind, 'installed')
|
'reinstall-app': 'reinstalled'}.get(kind, 'installed')
|
||||||
state['notice'] = 'Card %s %s.' % (who, verb)
|
state['notice'] = 'Card %s %s.' % (who, verb)
|
||||||
|
elif result == 'already_absent':
|
||||||
|
state['notice'] = 'Card %s was already absent.' % who
|
||||||
elif result == 'failed':
|
elif result == 'failed':
|
||||||
step = rec.get('failed_step') or ''
|
step = rec.get('failed_step') or ''
|
||||||
sw = rec.get('por_sw') or rec.get('sw') or ''
|
sw = rec.get('por_sw') or rec.get('sw') or ''
|
||||||
@@ -2947,8 +2949,9 @@ def _ram_batch_record(state, rec):
|
|||||||
else:
|
else:
|
||||||
state['notice'] = 'Card %s skipped: %s.' % (who, rec.get('reason') or '')
|
state['notice'] = 'Card %s skipped: %s.' % (who, rec.get('reason') or '')
|
||||||
detail = rec.get('reason') or rec.get('failed_step') or ''
|
detail = rec.get('reason') or rec.get('failed_step') or ''
|
||||||
|
label = {'already_absent': 'already absent'}.get(result, result)
|
||||||
sys.stderr.write('RAM-BATCH: %s%s%s\n' % (
|
sys.stderr.write('RAM-BATCH: %s%s%s\n' % (
|
||||||
result, (' ' + who) if rec.get('iccid') else '', (' - ' + detail) if detail else ''))
|
label, (' ' + who) if rec.get('iccid') else '', (' - ' + detail) if detail else ''))
|
||||||
|
|
||||||
|
|
||||||
def _ram_batch_failure_fields(result):
|
def _ram_batch_failure_fields(result):
|
||||||
@@ -3044,6 +3047,15 @@ def _ram_batch_handle_card(state, server):
|
|||||||
'module_aid': result.get('module_aid'),
|
'module_aid': result.get('module_aid'),
|
||||||
'nv_delta': result.get('nv_delta')}
|
'nv_delta': result.get('nv_delta')}
|
||||||
rec.update(_ram_batch_steps_fields(result))
|
rec.update(_ram_batch_steps_fields(result))
|
||||||
|
# A delete run whose DELETE answered 'not found' is a success (the
|
||||||
|
# object is gone, the goal is met) but not a deletion: report it as
|
||||||
|
# "already absent" and count it separately (v3.36.0). Only the delete
|
||||||
|
# kind: a reinstall's tolerated pre-delete is not the outcome.
|
||||||
|
if (state.get('plan') or {}).get('kind') == 'delete' and any(
|
||||||
|
s.get('delete_not_found') for s in (result.get('steps') or [])):
|
||||||
|
outcome = 'already_absent'
|
||||||
|
rec['result'] = outcome
|
||||||
|
rec['not_found'] = True
|
||||||
elif result is not None:
|
elif result is not None:
|
||||||
outcome = 'failed'
|
outcome = 'failed'
|
||||||
rec = {'iccid': iccid, 'preset': name, 'result': outcome,
|
rec = {'iccid': iccid, 'preset': name, 'result': outcome,
|
||||||
@@ -3178,7 +3190,7 @@ def _ram_batch_status():
|
|||||||
if _RAM_BATCH is None:
|
if _RAM_BATCH is None:
|
||||||
return {'active': False, 'phase': 'idle', 'attempts': [],
|
return {'active': False, 'phase': 'idle', 'attempts': [],
|
||||||
'attempts_total': 0, 'current': None, 'notice': '',
|
'attempts_total': 0, 'current': None, 'notice': '',
|
||||||
'counts': {'installed': 0, 'failed': 0, 'skipped': 0},
|
'counts': {'installed': 0, 'already_absent': 0, 'failed': 0, 'skipped': 0},
|
||||||
'started': None, 'stopped': None, 'progress': None,
|
'started': None, 'stopped': None, 'progress': None,
|
||||||
'kind': None}
|
'kind': None}
|
||||||
return {'active': bool(_RAM_BATCH.get('active')),
|
return {'active': bool(_RAM_BATCH.get('active')),
|
||||||
|
|||||||
+45
-1
@@ -88,7 +88,8 @@ class BatchStateTests(unittest.TestCase):
|
|||||||
'failed_step': 'LOAD (3/7)', 'por_sw': '6A80'})
|
'failed_step': 'LOAD (3/7)', 'por_sw': '6A80'})
|
||||||
srv._ram_batch_record(st, {'iccid': '8970119000004600114', 'result': 'skipped',
|
srv._ram_batch_record(st, {'iccid': '8970119000004600114', 'result': 'skipped',
|
||||||
'reason': 'no preset matches this ICCID'})
|
'reason': 'no preset matches this ICCID'})
|
||||||
self.assertEqual(st['counts'], {'installed': 1, 'failed': 1, 'skipped': 1})
|
self.assertEqual(st['counts'], {'installed': 1, 'already_absent': 0,
|
||||||
|
'failed': 1, 'skipped': 1})
|
||||||
self.assertEqual(st['attempts_total'], 3)
|
self.assertEqual(st['attempts_total'], 3)
|
||||||
# the notice names the last attempt's outcome
|
# the notice names the last attempt's outcome
|
||||||
self.assertIn('skipped: no preset matches this ICCID', st['notice'])
|
self.assertIn('skipped: no preset matches this ICCID', st['notice'])
|
||||||
@@ -230,6 +231,49 @@ class BatchCardTests(unittest.TestCase):
|
|||||||
self.assertEqual(seen['current']['iccid'], '8970119000004600098')
|
self.assertEqual(seen['current']['iccid'], '8970119000004600098')
|
||||||
self.assertIn('installed', st['notice']) # ...and set for the new outcome
|
self.assertIn('installed', st['notice']) # ...and set for the new outcome
|
||||||
|
|
||||||
|
def test_a_delete_that_finds_nothing_is_reported_already_absent(self):
|
||||||
|
"""A delete run whose DELETE answered 6A88 is a success (the object is
|
||||||
|
gone, the goal is met) but not a deletion: it is counted in its own
|
||||||
|
bucket and the notice says so (v3.36.0)."""
|
||||||
|
st = srv._ram_batch_new_state(_plan('delete'))
|
||||||
|
preset = _preset('8970119000004600098', 'Alfa')
|
||||||
|
|
||||||
|
def fake_run(server, body, plan, scc):
|
||||||
|
return {'success': True, 'final_cntr': '0000000002',
|
||||||
|
'steps': [{'name': 'DELETE (application)', 'sw': '9000',
|
||||||
|
'por_status': 'por_ok', 'por_sw': '6A88',
|
||||||
|
'delete_not_found': True}]}
|
||||||
|
|
||||||
|
with mock.patch.object(srv, '_ram_plan_run', fake_run):
|
||||||
|
srv._ram_batch_handle_card(st, _fake_server('8970119000004600098',
|
||||||
|
_FakeStore([preset])))
|
||||||
|
rec = st['attempts'][0]
|
||||||
|
self.assertEqual(rec['result'], 'already_absent')
|
||||||
|
self.assertTrue(rec['not_found'])
|
||||||
|
self.assertEqual(st['counts']['already_absent'], 1)
|
||||||
|
self.assertEqual(st['counts']['installed'], 0)
|
||||||
|
self.assertIn('already absent', st['notice'])
|
||||||
|
self.assertEqual(st['seen']['8970119000004600098'], 'already_absent')
|
||||||
|
|
||||||
|
def test_a_reinstall_with_a_tolerated_pre_delete_is_still_an_install(self):
|
||||||
|
"""The reinstall's pre-delete may tolerate 'not found', but the
|
||||||
|
outcome is the INSTALL, not 'already absent'."""
|
||||||
|
st = srv._ram_batch_new_state(_plan('reinstall-app'))
|
||||||
|
preset = _preset('8970119000004600098', 'Alfa')
|
||||||
|
|
||||||
|
def fake_run(server, body, plan, scc):
|
||||||
|
return {'success': True,
|
||||||
|
'steps': [{'name': 'DELETE (application)', 'por_sw': '6A88',
|
||||||
|
'delete_not_found': True},
|
||||||
|
{'name': 'INSTALL [for install]', 'por_sw': '9000'}]}
|
||||||
|
|
||||||
|
with mock.patch.object(srv, '_ram_plan_run', fake_run):
|
||||||
|
srv._ram_batch_handle_card(st, _fake_server('8970119000004600098',
|
||||||
|
_FakeStore([preset])))
|
||||||
|
self.assertEqual(st['attempts'][0]['result'], 'installed')
|
||||||
|
self.assertEqual(st['counts']['installed'], 1)
|
||||||
|
self.assertEqual(st['counts']['already_absent'], 0)
|
||||||
|
|
||||||
def test_a_card_already_processed_is_reported_not_reinstalled(self):
|
def test_a_card_already_processed_is_reported_not_reinstalled(self):
|
||||||
st = srv._ram_batch_new_state(_plan())
|
st = srv._ram_batch_new_state(_plan())
|
||||||
preset = _preset('8970119000004600098', 'Alfa')
|
preset = _preset('8970119000004600098', 'Alfa')
|
||||||
|
|||||||
Reference in New Issue
Block a user