v3.35.0: EF.SMSP decoded editor (the full SMS-parameter form)

TS 31.102 4.2.27 scope: alpha; TP-DA and TS-SCA with their own length
conventions (DA = digits, SCA = octets, 0 = default SMSC) and a TON
select; 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.  Unchanged slots are copied verbatim (the live fixture's
DCS-absent 00 and present VP FF survive), alphanumeric TON-5 addresses
stay read-only, and every checked record is validated before any write.

Decoder fixes: the parameter-indicator bits were shifted (SCA on 5,
PID on 2, DCS on 3, VP on 4; the spec is DA/SCA/PID/DCS/VP on bits 1-5
with 6-8 reserved, 1 = absent), and the TP-DA length was read as octets
instead of digits (TS 23.040 9.1.2.5).

Live fixture: ffffffffffffffffffffffffffe9ffffffffffffffffffffffff07919733824009f0ffffffff0000ff
(SCA +79332804900, PID 00, DCS absent/00, VP FF) round-trips
byte-identically.

1022 Python + 1058 frontend green; simple-v468.
This commit is contained in:
2026-10-23 16:54:12 +03:00
parent 180dd50c06
commit 87bf4acbbc
13 changed files with 944 additions and 41 deletions
+22
View File
@@ -9,6 +9,28 @@ every change carries a descriptive commit message.
## [Unreleased]
## [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.
### 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.
## [3.34.0] — 2026-10-23
### Added
+1 -1
View File
@@ -595,7 +595,7 @@ Browse the UICC filesystem in a tree view. Files are shown with names, FIDs, and
- Entries are grouped with DFs above EFs and sorted by **FID** or symbolic **Name** (pills pinned above the tree together with **Probe all files**, remembered in `localStorage`)
- **Read raw** / **Read decoded** — read the selected file as a hex dump or as a decoded field table (client-side decoders for IMSI, ICCID, SPN, PLMN lists, LOCI/EPSLOCI, ADN/MSISDN, service tables, SUME, …); the highlighted pill is the current view and clicking either pill (re-)reads the file. The decoded table is built in the browser from the same raw bytes (no server-side decode); **Read decoded** is disabled when the client-side decoders do not cover the file (resolved by name/FID before reading)
- **Edit raw** — edit the raw hex data and **Save** to write back (or **Cancel**); transparent files use a textarea, record files one input per record with selection checkboxes. **Edit raw** switches to the hex view first and reads the file if needed
- **Edit decoded** — edit through the file's own decoded form (client-side encoders; written via the normal update path): **EF.SPN**, **EF.PNN**, the PLMN lists **PLMNsel / FPLMN / EHPLMN** and **PLMNwAcT / OPLMNwAcT / HPLMNwAcT**, the dialling-number files **ADN / FDN / SDN / MSISDN / LND / BDN**, the service tables **UST / IST / EST** (**SST** stays read-only until the SIM allocated/activated editor exists), **ACC**, **PHASE**, **HPPLMN**, **AD**, **ICCID / IMSI** (behind a confirmation), and the cell-broadcast lists **CBMI / CBMID / CBMIR**. Transparent files get one form (PLMN rows and PNN items with add/remove); record files get one form per record with a checkbox that is auto-checked when a field changes, so only edited records are written. **Save** validates every selected record before writing; extension pointers (EF.EXT1–EF.EXT5) and the BDN comparison-method byte are preserved, and editing a number continued in an extension file is refused. The shape survey and the planned coverage are in `docs/decoded-editing.md`
- **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)`
- **ADM** — files that need the administrator PIN fail with `6982`/`9804`; when the matching card preset (same ICCID) carries an ADM key, a **Verify ADM** button appears next to the error and the header badge (`ADM ✓/✗ ⚿`) becomes clickable. Every wrong key consumes an attempt (the remaining attempts are shown and a retry asks for confirmation); a blocked ADM needs the card's unblock key
+1 -1
View File
@@ -570,7 +570,7 @@ Delivery PoR (SPI2 `01`) проще — карта возвращает PoR на
- Элементы сгруппированы (DF выше EF) и отсортированы по **FID** или символьному **имени** (пиллы закреплены над деревом вместе с **«Проверить все файлы»**, выбор сохраняется в `localStorage`)
- **Read raw** / **Read decoded** — чтение выбранного файла как hex-дампа или как таблицы декодированных полей (клиентские декодеры: IMSI, ICCID, SPN, списки PLMN, LOCI/EPSLOCI, ADN/MSISDN, таблицы сервисов, SUME, …); подсвеченная пилла — текущий вид, клик по любой из них перечитывает файл. таблица декодируется в браузере из тех же сырых байт (без серверного декодирования); **Read decoded** отключается, когда клиентские декодеры не покрывают файл (определяется по имени/FID до чтения)
- **Edit raw** — редактирование сырых hex-данных и **Save** для записи (или **Cancel**); для transparent-файлов — textarea, для record-файлов — по полю на запись с флажками выбора. **Edit raw** сначала переключается на hex и при необходимости читает файл
- **Edit decoded** — редактирование в собственной декодированной форме файла (клиентские кодировщики; запись обычным путём): **EF.SPN**, **EF.PNN**, списки PLMN **PLMNsel / FPLMN / EHPLMN** и **PLMNwAcT / OPLMNwAcT / HPLMNwAcT**, файлы номеров **ADN / FDN / SDN / MSISDN / LND / BDN**, таблицы сервисов **UST / IST / EST** (**SST** только для чтения до появления редактора allocated/activated), **ACC**, **PHASE**, **HPPLMN**, **AD**, **ICCID / IMSI** (с подтверждением) и списки cell broadcast **CBMI / CBMID / CBMIR**. Для transparent-файла — одна форма (строки PLMN и элементы PNN с добавлением/удалением); для record-файла — форма на запись с флажком, который ставится автоматически при изменении поля, поэтому записываются только изменённые записи. **Save** проверяет все выбранные записи до записи; указатели расширений (EF.EXT1–EF.EXT5) и байт метода сравнения BDN сохраняются; редактирование номера, продолжение которого лежит в файле расширения, отклоняется. Обзор форматов и план охвата — в `docs/decoded-editing.md`
- **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 — `(пусто)`
- **ADM** — файлы, требующие администраторский PIN, возвращают `6982`/`9804`; если в подходящем пресете карты (тот же ICCID) есть ключ ADM, рядом с ошибкой появляется кнопка **«Проверить ADM»**, а значок в заголовке (`ADM ✓/✗ ⚿`) становится кликабельным. Каждый неверный ключ расходует попытку (остаток показывается, повторная попытка требует подтверждения); заблокированный ADM требует ключа разблокировки карты
+20 -7
View File
@@ -1,7 +1,8 @@
# Decoded editing — file-format survey and design
Status: design note for the **Edit decoded** feature. Waves 1 and 2 shipped in
v3.33.0 and v3.34.0; decoded editing for EF.DIR/EF.ARR/EF.SMS/EF.PBR is
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
own structure and content coding, assigns each an editing *shape*, and sets the
@@ -108,7 +109,7 @@ editing candidates.
| EF.ECC (SIM) | 6FB7 | TS 51.011 10.3.27 | 3n | emergency codes (TR) | S6 | 3.35 |
| EF.ECC (USIM) | 6FB7 | TS 31.102 4.2.21 | X+4 | code(3) + alpha(X) + category(1) | S7 | 3.35 |
| EF.EXT1–EXT5 | 6F4A–6F55 | TS 31.102 4.4.2.4 / 4.2.30/31/45/37 | 13 | type-dependent: additional data / subaddress | S9 | 3.36 |
| EF.SMSP | 6F42 | TS 31.102 4.2.27 | 28+Y | alpha + parameter indicators + TP-DA/SCA/PID/DCS/VP | S10 | on request only |
| 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 | **postponed (read-only)** |
| EF.ARR | 2F06/6F06 | TS 31.102 4.2.55 | X | access-rule TLVs (ISO 7816-4) | S8/S9 | **postponed (read-only)** |
| EF.DIR | 2F00 | TS 51.011 10.3.3 | X | application template TLVs (61 > 4F/50/73…) | S8/S9 | **postponed (read-only)** |
@@ -169,7 +170,15 @@ beyond S9/S10.
count and 1 for an even one; the first encoder always wrote 9.
12. **SIM EF.LP/EF.PL use the 1-byte DCS language coding**, unlike the USIM
EF.LI's 2-byte ISO 639 letters — the shared decoder is wrong for the SIM
files (queued with the 3.35 language work).
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
@@ -183,7 +192,13 @@ beyond S9/S10.
CBMI/CBMID/CBMIR. Decoder fixes: ACC class numbering, the AD operation
mode / MNC-length rules, the SST 2-bit layout (read-only for now) and the
IMSI parity nibble.
- **v3.35.0** — LI/LP/PL (the SIM files need the 1-byte DCS language coding
- **v3.35.0 (shipped)** — EF.SMSP as the first S10 dedicated builder: the full
parameter form (alpha; TP-DA and TS-SCA with their own length conventions
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,
@@ -191,8 +206,6 @@ beyond S9/S10.
- **Postponed by decision (read-only; do not implement without confirmation)**
— EF.DIR, EF.ARR, EF.SMS, EF.PBR: they rarely need manual changes and the
decoded view is enough.
- **On request only** — EF.SMSP (a parameter form; no TPDU/parameter builder
exists in the PWA today).
- **Read-only forever (S0)** unless a dedicated flow appears: LOCI/PSLOCI/
EPSLOCI/EPSNSC/LOCIGPRS, Kc/KcGPRS, Keys/KeysPS, ACM, START-HFN/THRESHOLD,
call logs (ME-written), DCK/NETPAR.
+1 -1
View File
@@ -409,7 +409,7 @@
<ul class="list-disc list-inside text-sm space-y-1 mb-3">
<li><strong>Читать сырые данные / Декодировать</strong> — чтение выбранного файла в виде hex-дампа или таблицы декодированных полей (клиентские декодеры EF: IMSI, ICCID, SPN, списки PLMN, LOCI/PSLOCI/EPSLOCI, ADN/MSISDN, таблицы сервисов, SUME, …); таблица декодируется в браузере из тех же сырых байт (без серверного декодирования). Подсвеченная кнопка — текущий вид; нажатие любой из них (пере)читывает файл. «Декодировать» недоступна, если для файла нет клиентского декодера (проверка по имени/FID до чтения).</li>
<li><strong>Редактировать сырые данные</strong> — изменение hex-данных, <strong>Сохранить</strong> для записи (или <strong>Отмена</strong>); transparent-файлы редактируются в textarea, record-файлы — по одному полю на запись с флажками выбора. «Редактировать сырые данные» сначала переключает на hex-вид и при необходимости читает файл.</li>
<li><strong>Редактировать декодированные</strong> — редактирование файла в его собственной декодированной форме; кодирование выполняется в браузере, запись идёт обычным путём обновления. Кодировщики покрывают <strong>EF.SPN</strong>, <strong>EF.PNN</strong>, списки PLMN (<strong>PLMNsel / FPLMN / EHPLMN</strong> и списки с технологиями доступа <strong>PLMNwAcT / OPLMNwAcT / HPLMNwAcT</strong>), файлы номеров (<strong>ADN / FDN / SDN / MSISDN / LND / BDN</strong>), таблицы сервисов (<strong>UST / IST / EST</strong>; <strong>SST</strong> остаётся только для чтения до появления редактора allocated/activated), <strong>ACC</strong>, <strong>PHASE</strong>, <strong>HPPLMN</strong>, <strong>AD</strong>, <strong>ICCID / IMSI</strong> (с подтверждением — запись меняет идентичность карты) и списки cell broadcast (<strong>CBMI / CBMID / CBMIR</strong>). Для transparent-файла — одна форма (поля SPN, строки PLMN с добавлением/удалением, элементы PNN с добавлением/удалением); для record-файла — по форме на запись с флажком выбора, который ставится автоматически при изменении поля, поэтому записываются только изменённые записи. <strong>Сохранить</strong> проверяет все выбранные записи до записи; для файлов без кодировщика кнопка недоступна с подсказкой. Указатели расширений (EF.EXT1–EF.EXT5) и байт метода сравнения BDN сохраняются; редактирование номера, продолжение которого лежит в файле расширения, отклоняется.</li>
<li><strong>Редактировать декодированные</strong> — редактирование файла в его собственной декодированной форме; кодирование выполняется в браузере, запись идёт обычным путём обновления. Кодировщики покрывают <strong>EF.SPN</strong>, <strong>EF.PNN</strong>, списки PLMN (<strong>PLMNsel / FPLMN / EHPLMN</strong> и списки с технологиями доступа <strong>PLMNwAcT / OPLMNwAcT / HPLMNwAcT</strong>), файлы номеров (<strong>ADN / FDN / SDN / MSISDN / LND / BDN</strong>), таблицы сервисов (<strong>UST / IST / EST</strong>; <strong>SST</strong> остаётся только для чтения до появления редактора allocated/activated), <strong>ACC</strong>, <strong>PHASE</strong>, <strong>HPPLMN</strong>, <strong>AD</strong>, <strong>ICCID / IMSI</strong> (с подтверждением — запись меняет идентичность карты) списки cell broadcast (<strong>CBMI / CBMID / CBMIR</strong>) и <strong>EF.SMSP</strong> (полная форма SMS-параметров: alpha, TP-DA и TS-SCA с их правилами адресации и TON, TP-PID, TP-DCS и относительный TP-VP; у каждого параметра — флажок <em>«отсутствует (действует значение ME по умолчанию)»</em> и живые подсказки по DCS и сроку действия). Для transparent-файла — одна форма (поля SPN, строки PLMN с добавлением/удалением, элементы PNN с добавлением/удалением); для record-файла — по форме на запись с флажком выбора, который ставится автоматически при изменении поля, поэтому записываются только изменённые записи. <strong>Сохранить</strong> проверяет все выбранные записи до записи; для файлов без кодировщика кнопка недоступна с подсказкой. Указатели расширений (EF.EXT1–EF.EXT5) и байт метода сравнения BDN сохраняются; редактирование номера, продолжение которого лежит в файле расширения, отклоняется.</li>
<li><strong>Проверить все файлы</strong> — обход всего дерева (включая пользовательские файлы) с пометкой каждого элемента: есть (обычный вид) или нет (красный ✗, без стрелки разворачивания); существующие пустые DF показывают <code class="font-mono text-sm">(пусто)</code>. Отображается прогресс <em>N / всего</em>, обход можно остановить; в конце — сводка «есть/нет». Файлы проверяются только при разворачивании или проверке — просмотр остаётся ленивым.</li>
<li><strong>ADM</strong> — файлы, требующие администраторский PIN, возвращают <code class="font-mono text-sm">6982</code>/<code class="font-mono text-sm">9804</code>; если в подходящем пресете карты (тот же ICCID) есть ключ ADM, рядом с ошибкой появляется кнопка <strong>«Проверить ADM»</strong>, а значок в заголовке (<code class="font-mono text-sm">ADM ✓/✗ ⚿</code>) становится кликабельным. Каждый неверный ключ расходует попытку: остаток показывается, а повторная попытка требует подтверждения. Заблокированный ADM здесь не восстановить — нужен ключ разблокировки карты.</li>
</ul>
+1 -1
View File
@@ -408,7 +408,7 @@
<ul class="list-disc list-inside text-sm space-y-1 mb-3">
<li><strong>Read raw / Read decoded</strong> — read the selected file as a hex dump or as a decoded field table (client-side EF decoders: IMSI, ICCID, SPN, PLMN lists, LOCI/PSLOCI/EPSLOCI, ADN/MSISDN, service tables, SUME, …); the decoded table is built in the browser from the same raw bytes (no server-side decode). The highlighted pill is the current view; clicking either pill (re-)reads the file. <strong>Read decoded</strong> is disabled when the client-side decoders do not cover the file (name/FID check, before reading).</li>
<li><strong>Edit raw</strong> — edit the raw hex data, <strong>Save</strong> to write back (or <strong>Cancel</strong>); transparent files use a textarea, record files one input per record with selection checkboxes. <strong>Edit raw</strong> switches to the hex view first and reads the file if needed.</li>
<li><strong>Edit decoded</strong> — edit the file through its own decoded form, encoded in the browser and written with the normal update path. The encoders cover <strong>EF.SPN</strong>, <strong>EF.PNN</strong>, the PLMN lists (<strong>PLMNsel / FPLMN / EHPLMN</strong> and the access-technology lists <strong>PLMNwAcT / OPLMNwAcT / HPLMNwAcT</strong>), the dialling-number files (<strong>ADN / FDN / SDN / MSISDN / LND / BDN</strong>), the service tables (<strong>UST / IST / EST</strong>; <strong>SST</strong> stays read-only until the SIM allocated/activated editor exists), <strong>ACC</strong>, <strong>PHASE</strong>, <strong>HPPLMN</strong>, <strong>AD</strong>, <strong>ICCID / IMSI</strong> (behind a confirmation — writing them changes the card identity) and the cell-broadcast lists (<strong>CBMI / CBMID / CBMIR</strong>). A transparent file gets one form (SPN fields, PLMN rows with add/remove, PNN items with add/remove); a record file gets one form per record with a selection checkbox that is auto-checked when a field changes, so only the edited records are written. <strong>Save</strong> validates every selected record before writing anything; files without an encoder keep the button disabled with a tooltip. Extension pointers (EF.EXT1–EF.EXT5) and the BDN comparison-method byte are preserved, and editing a number that continues in an extension file is refused.</li>
<li><strong>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>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&rsquo;s unblock key.</li>
</ul>
+421 -19
View File
@@ -1944,7 +1944,7 @@
// ===== Version =====
// Single source of truth for the PWA version: shown in the header and used
// by the server version check in pysimConnect().
const SIMPLE_VERSION = '3.34.0';
const SIMPLE_VERSION = '3.35.0';
document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION;
// ===== view history (the browser Back/Forward buttons) =====
@@ -16329,9 +16329,10 @@ async function pysimFsDecSave(node, rawName, statusEl, out, arrTouched) {
try {
if (model.mode === 'tr') {
const orig = root ? (root.getAttribute('data-orig') || '') : '';
const values = entry.list
? efDecCollectRows(root, entry.list.itemFields)
: efDecCollectValues(root, entry.fields);
const values = entry.custom
? entry.custom.collect(root)
: (entry.list ? efDecCollectRows(root, entry.list.itemFields)
: efDecCollectValues(root, entry.fields));
const body = pysimFsSelectBody(node || { name: rawName, fid: null });
body.data = entry.enc(values, orig);
// identity writes ask once, and only when something actually changes
@@ -16358,9 +16359,9 @@ async function pysimFsDecSave(node, rawName, statusEl, out, arrTouched) {
const num = block.getAttribute('data-rec');
if (!num) continue;
const body = pysimFsSelectBody(node || { name: rawName, fid: null });
const collected = entry.items
? efDecCollectItems(block)
: efDecCollectValues(block, entry.fields);
const collected = entry.custom
? entry.custom.collect(block)
: (entry.items ? efDecCollectItems(block) : efDecCollectValues(block, entry.fields));
try {
body.data = entry.enc(collected, block.getAttribute('data-orig') || '');
} catch (e) {
@@ -22753,7 +22754,7 @@ const EF_FIELD_LABELS = {
op_mode: 'Operation mode', test_mode: 'Test mode', additional_info: 'Additional info',
mnc_length: 'MNC length', services: 'Services', plmns: 'PLMNs', applications: 'Applications',
aid: 'AID', label: 'Label', eap: 'EAP', message_ids: 'Message IDs', ranges: 'Ranges',
esc: 'Emergency category', param_indicators: 'Parameter indicators', tp_da: 'TP-DA',
esc: 'Emergency category', indicators: 'Parameter indicators', tp_da: 'TP-DA',
sca: 'SCA', tp_pid: 'TP-PID', tp_dcs: 'TP-DCS', tp_vp: 'TP-VP', alpha: 'Alpha',
direction: 'Direction', status: 'Status', tpdu: 'TPDU', display_condition: 'Display condition',
rfu: 'RFU', ccp: 'CCP', ext: 'Extension', tag: 'Tag', hex: 'Hex', lac_range: 'LAC range',
@@ -23248,6 +23249,17 @@ function efDecAcl(b) {
return apns.length ? { apns: apns } : { raw: efHex(b) };
}
// TS 23.040 §9.1.2.5: the TP-DA length is the DIGIT count, so the BCD octets
// are ceil(len/2) (the SCA length counts the TON/NPI + BCD octets instead).
function efSmspDaNumber(slot) {
const n = slot.length ? slot[0] : 0xFF;
if (n === 0xFF || n === 0) return '';
const ton = (slot[1] >> 4) & 0x07;
const octets = Math.min(10, Math.ceil(n / 2));
const digits = efSwapNibbles(efHex(slot.subarray(2, 2 + octets))).replace(/[fF]+$/, '');
return digits ? (ton === 1 ? '+' + digits : digits) : '';
}
function efDecSmsp(b) {
if (efAllFf(b)) return { empty: true };
if (b.length < 28) return { raw: efHex(b) };
@@ -23255,14 +23267,15 @@ function efDecSmsp(b) {
const out = {};
const alpha = efAnnexA(efRstripFf(b.subarray(0, y)));
if (alpha) out.alpha = alpha;
// TS 31.102 4.2.27: bits 1..5 = DA, SCA, PID, DCS, VP (1 = absent), bits
// 6..8 reserved (1).
const pi = b[y];
const flags = [];
out.param_indicators = '0x' + pi.toString(16).toUpperCase().padStart(2, '0');
if (!(pi & 0x01)) out.tp_da = efBcdAddressLen(b.subarray(y + 1, y + 13));
if (!(pi & 0x10)) out.sca = efBcdAddressLen(b.subarray(y + 13, y + 25));
if (!(pi & 0x02)) out.tp_pid = '0x' + b[y + 25].toString(16).toUpperCase().padStart(2, '0');
if (!(pi & 0x04)) out.tp_dcs = '0x' + b[y + 26].toString(16).toUpperCase().padStart(2, '0');
if (!(pi & 0x08)) out.tp_vp = '0x' + b[y + 27].toString(16).toUpperCase().padStart(2, '0');
out.indicators = '0x' + pi.toString(16).toUpperCase().padStart(2, '0');
if (!(pi & 0x01)) out.tp_da = efSmspDaNumber(b.subarray(y + 1, y + 13));
if (!(pi & 0x02)) out.sca = efBcdAddressLen(b.subarray(y + 13, y + 25));
if (!(pi & 0x04)) out.tp_pid = '0x' + b[y + 25].toString(16).toUpperCase().padStart(2, '0');
if (!(pi & 0x08)) out.tp_dcs = '0x' + b[y + 26].toString(16).toUpperCase().padStart(2, '0');
if (!(pi & 0x10)) out.tp_vp = '0x' + b[y + 27].toString(16).toUpperCase().padStart(2, '0');
return out;
}
@@ -24290,6 +24303,343 @@ function efEncCbmir(values, origHex) {
return efHex(out);
}
// ===== EF.SMSP (TS 31.102 4.2.27) =====
// A linear-fixed record: alpha (Y = len-28 octets), the parameter indicator
// byte (bits 1..5 = DA, SCA, PID, DCS, VP; 1 = absent; bits 6..8 = 1), the
// 12-octet DA slot, the 12-octet SCA slot, then the PID, DCS and VP bytes.
const EF_SMSP_PARAMS = [
{ key: 'da', label: 'TP-DA', kind: 'address', bit: 0x01, off: 1 },
{ key: 'sca', label: 'TS-SCA', kind: 'address', bit: 0x02, off: 13 },
{ key: 'pid', label: 'TP-PID', kind: 'hex', bit: 0x04, off: 25 },
{ key: 'dcs', label: 'TP-DCS', kind: 'hex', bit: 0x08, off: 26 },
{ key: 'vp', label: 'TP-VP', kind: 'hex', bit: 0x10, off: 27 },
];
// Hint text goes through t() when the runtime has it (the pure test harness
// extracts these without the dictionary and gets the English text).
function smspT(s) {
return typeof t === 'function' ? t(s) : s;
}
// TS 23.038 §4 coding-group hint.
function smspDcsHint(hex) {
const s = String(hex === undefined || hex === null ? '' : hex).replace(/^0x/i, '').trim();
if (!/^[0-9a-fA-F]{2}$/.test(s)) return '';
const d = parseInt(s, 16);
const top = d >> 4;
const classes = ['class 0', 'class 1', 'class 2', 'class 3'];
if (top <= 7) {
const parts = [
smspT(top >= 4 ? 'automatic deletion' : 'general data coding'),
smspT(['GSM 7-bit', '8-bit data', 'UCS2', 'reserved'][(d >> 2) & 3]),
smspT((d & 0x20) ? 'compressed' : 'uncompressed'),
];
if (d & 0x10) parts.push(smspT(classes[d & 3]));
return parts.join(', ');
}
if (top <= 11) return smspT('reserved coding group');
if (top <= 14) {
return [
smspT(top === 12 ? 'MWI discard' : 'MWI store'),
smspT(top === 14 ? 'UCS2' : 'GSM 7-bit'),
smspT((d & 0x08) ? 'active' : 'inactive'),
smspT(['voicemail', 'fax', 'email', 'other'][d & 3]),
].join(', ');
}
return [
smspT('data coding'),
smspT((d & 0x04) ? '8-bit data' : 'GSM 7-bit'),
smspT(classes[d & 3]),
].join(', ');
}
function smspDurationText(secs) {
const h = Math.floor(secs / 3600);
const m = Math.round((secs % 3600) / 60);
if (!h) return m + ' ' + smspT('min');
return h + ' ' + smspT('h') + (m ? ' ' + m + ' ' + smspT('min') : '');
}
// Russian needs three plural forms (1 / 2-4 / 5+, and the 11-14 exception);
// English needs the singular/plural pair. `few` is only used by the RU form.
function smspPluralIndex(n) {
n = Math.abs(n) % 100;
if (n >= 11 && n <= 14) return 2;
const d = n % 10;
if (d === 1) return 0;
if (d >= 2 && d <= 4) return 1;
return 2;
}
function smspCount(n, one, few, many) {
const ru = typeof currentLang !== 'undefined' && currentLang === 'ru';
if (ru) {
const idx = smspPluralIndex(n);
return n + ' ' + smspT(idx === 0 ? one : (idx === 1 ? few : many));
}
return n + ' ' + smspT(n === 1 ? one : many);
}
// TS 23.040 §9.2.3.12.1 relative validity-period hint: minutes for 0-143,
// 30-minute steps for 144-167, DAYS for 168-196 and WEEKS for 197-255.
function smspVpHint(hex) {
const s = String(hex === undefined || hex === null ? '' : hex).replace(/^0x/i, '').trim();
if (!/^[0-9a-fA-F]{2}$/.test(s)) return '';
const v = parseInt(s, 16);
if (v <= 143) return smspDurationText((v + 1) * 300);
if (v <= 167) return smspDurationText(43200 + (v - 143) * 1800);
if (v <= 196) return smspCount(v - 166, 'day', 'days-few', 'days');
return smspCount(v - 192, 'week', 'weeks-few', 'weeks');
}
// A DA/SCA slot decoded for the form: the length byte is the digit count for
// DA and the TON/NPI + BCD octet count for SCA (0 = the default SMSC). A
// TON 5 (alphanumeric) address is not BCD text - it is shown raw and kept
// verbatim by the encoder.
function efSmspAddressModel(slot, present, isSca) {
const orig = efHex(slot);
const n = slot.length ? slot[0] : 0xFF;
let ton = 1, number = '';
if (n !== 0xFF && n !== 0) {
ton = (slot[1] >> 4) & 0x07;
const octets = Math.max(0, Math.min(10, isSca ? n - 1 : Math.ceil(n / 2)));
const digits = ton === 5 ? ''
: efSwapNibbles(efHex(slot.subarray(2, 2 + octets))).replace(/[fF]+$/, '');
number = digits ? (ton === 1 ? '+' + digits : digits) : '';
}
return { present: present, orig: orig, number: number, ton: ton, alnum: ton === 5 };
}
// The per-record form model: alpha + each parameter's presence, original slot
// bytes and editable value.
function efSmspModel(hex) {
const b = efBytes(hex || '');
const y = Math.max(0, b.length - 28);
const pi = b.length >= 28 ? b[y] : 0xFF;
const alpha = efAnnexA(efRstripFf(b.subarray(0, Math.min(y, b.length))));
const slot = (at, len) => b.subarray(at, Math.min(b.length, at + len));
const byte = (at, bit) => {
const v = b.length > at ? b[at] : 0xFF;
return { present: !(pi & bit), orig: efHex([v]),
value: '0x' + v.toString(16).toUpperCase().padStart(2, '0') };
};
return {
alpha: alpha,
params: {
da: efSmspAddressModel(slot(y + 1, 12), !(pi & 0x01), false),
sca: efSmspAddressModel(slot(y + 13, 12), !(pi & 0x02), true),
pid: byte(y + 25, 0x04),
dcs: byte(y + 26, 0x08),
vp: byte(y + 27, 0x10),
},
};
}
function efSmspTonOptions(ton) {
const opts = [{ v: 0, l: 'Unknown' }, { v: 1, l: 'International' }, { v: 2, l: 'National' }];
if (!opts.some(o => o.v === ton)) opts.push({ v: ton, l: 'Current', current: true });
return opts.map(o => '<option value="' + o.v + '"' + (o.v === ton ? ' selected' : '') + '>' +
esc(o.current ? t('Current') + ' ' + o.v : t(o.l)) + '</option>').join('');
}
function efSmspParamRow(key, label, slot) {
const absent = !slot.present;
const dis = absent ? ' disabled' : '';
const raw = slot.alnum && slot.present;
let body;
if ((key === 'da' || key === 'sca') && raw) {
// TS 23.040 9.1.2.5 TON 5: GSM 7-bit alphanumeric contents, not BCD -
// keep the slot verbatim, only its presence is editable here.
body = '<span class="text-gray-500 dark:text-slate-400">' + esc(t(label)) + '</span>' +
'<span class="font-mono break-all">' + esc(slot.orig || '') + '</span>' +
'<span class="text-xs text-gray-500 dark:text-slate-400">' +
esc(t('alphanumeric address — not editable here; it is preserved as is')) + '</span>';
} else if (key === 'da' || key === 'sca') {
body = '<span class="text-gray-500 dark:text-slate-400">' + esc(t(label)) + '</span>' +
'<input type="text" data-smsp="' + key + '.number" value="' + esc(slot.number || '') + '"' + dis +
' oninput="pysimFsDecRowCheck(this)" class="flex-1 border border-gray-300 dark:border-slate-600 rounded px-1 bg-white dark:bg-slate-800 text-gray-800 dark:text-slate-200">' +
'<select data-smsp="' + key + '.ton"' + dis + ' onchange="pysimFsDecRowCheck(this)" class="border border-gray-300 dark:border-slate-600 rounded px-1 bg-white dark:bg-slate-800 text-gray-800 dark:text-slate-200">' +
efSmspTonOptions(slot.ton) + '</select>';
if (key === 'sca') {
body += '<span class="text-xs text-gray-500 dark:text-slate-400">' + esc(t('empty = default SMSC')) + '</span>';
}
} else {
const hint = absent ? t('absent — the ME default applies')
: (key === 'dcs' ? smspDcsHint(slot.value) : (key === 'vp' ? smspVpHint(slot.value) : ''));
body = '<span class="text-gray-500 dark:text-slate-400">' + esc(t(label)) + '</span>' +
'<input type="text" data-smsp="' + key + '.value" value="' + esc(slot.value || '') + '" maxlength="4"' + dis +
' oninput="pysimFsSmspHexInput(this)" class="border border-gray-300 dark:border-slate-600 rounded px-1 bg-white dark:bg-slate-800 text-gray-800 dark:text-slate-200">';
body += key === 'pid'
? '<span class="text-xs text-gray-500 dark:text-slate-400">' + esc(t('TS 23.040 TP-PID (raw); 0x00 = short message type 0')) + '</span>'
: '<span class="text-xs text-gray-500 dark:text-slate-400" data-smsp-hint>' + esc(hint) + '</span>';
}
return '<div class="pysim-dec-item flex flex-wrap items-center gap-2" data-smsp-orig="' + key + '">' +
'<label class="flex items-center gap-1"><input type="checkbox" data-smsp-absent="' + key + '"' +
(absent ? ' checked' : '') + ' onchange="pysimFsSmspToggle(this)">' + esc(t('Absent (the ME default applies)')) + '</label>' +
body + '</div>';
}
function efSmspHtml(values) {
const v = values || {};
const p = v.params || {};
let html = '<div class="pysim-dec-items space-y-1">' +
'<div class="pysim-dec-item flex flex-wrap items-center gap-2">' +
'<span class="text-gray-500 dark:text-slate-400">' + esc(t('Alpha')) + '</span>' +
'<input type="text" data-smsp="alpha" value="' + esc(v.alpha || '') + '" oninput="pysimFsDecRowCheck(this)" ' +
'class="flex-1 border border-gray-300 dark:border-slate-600 rounded px-1 bg-white dark:bg-slate-800 text-gray-800 dark:text-slate-200">' +
'</div>';
for (const spec of EF_SMSP_PARAMS) html += efSmspParamRow(spec.key, spec.label, p[spec.key] || {});
return html + '</div>';
}
function efSmspCollect(root) {
const pick = key => root.querySelector('[data-smsp="' + key + '"]');
const out = { alpha: '', params: {} };
const alpha = pick('alpha');
if (alpha) out.alpha = alpha.value;
for (const spec of EF_SMSP_PARAMS) {
const row = root.querySelector('[data-smsp-orig="' + spec.key + '"]');
const absent = row ? row.querySelector('[data-smsp-absent="' + spec.key + '"]') : null;
const present = !(absent && absent.checked);
if (spec.kind === 'address') {
const num = pick(spec.key + '.number');
const ton = pick(spec.key + '.ton');
out.params[spec.key] = { present: present,
number: num ? num.value : '', ton: ton ? parseInt(ton.value, 10) : 1 };
} else {
const val = pick(spec.key + '.value');
out.params[spec.key] = { present: present, value: val ? val.value : '' };
}
}
return out;
}
function pysimFsSmspToggle(cb) {
const row = cb && cb.closest ? cb.closest('[data-smsp-orig]') : null;
if (!row) return;
const absent = !!cb.checked;
row.querySelectorAll('[data-smsp]').forEach(el => { el.disabled = absent; });
const key = row.getAttribute('data-smsp-orig');
const hint = row.querySelector('[data-smsp-hint]');
if (hint) {
const input = row.querySelector('[data-smsp="' + key + '.value"]');
hint.textContent = absent ? t('absent — the ME default applies')
: (key === 'dcs' ? smspDcsHint(input && input.value) : smspVpHint(input && input.value));
}
pysimFsDecRowCheck(cb);
}
function pysimFsSmspHexInput(input) {
const row = input && input.closest ? input.closest('[data-smsp-orig]') : null;
if (row) {
const key = row.getAttribute('data-smsp-orig');
const cb = row.querySelector('[data-smsp-absent]');
const hint = row.querySelector('[data-smsp-hint]');
if (hint) {
hint.textContent = (cb && cb.checked) ? t('absent — the ME default applies')
: (key === 'dcs' ? smspDcsHint(input.value) : smspVpHint(input.value));
}
}
pysimFsDecRowCheck(input);
}
function efSmspHexByte(value) {
const s = String(value === undefined || value === null ? '' : value).replace(/^0x/i, '').trim();
if (!/^[0-9a-fA-F]{2}$/.test(s)) throw efEncError('The parameter value must be 2 hex digits');
return parseInt(s, 16);
}
// A DA/SCA slot: [length][TON/NPI][10 BCD bytes], FF beyond the digits. DA's
// length is the digit count, SCA's the TON/NPI+BCD octet count (0 = default).
// A leading '+' forces TON 1 (international) whatever the select says.
function efSmspAddressBytes(key, val) {
const num = String(val && val.number !== undefined && val.number !== null ? val.number : '').replace(/\s+/g, '');
const plus = num.charAt(0) === '+';
const digits = num.replace(/^\+/, '');
if (digits !== '' && !/^\d+$/.test(digits)) throw efEncError('The number must be digits with an optional leading +');
if (digits.length > 20) throw efEncError('The number is too long for SMSP (max 20 digits)');
if (key === 'da' && digits.length === 0) throw efEncError('The TP-DA number is required when present');
const ton = plus ? 1 : Number(val && val.ton);
const out = new Uint8Array(12);
out.fill(0xFF);
if (key === 'da') out[0] = digits.length;
else out[0] = digits.length ? 1 + Math.ceil(digits.length / 2) : 0;
if (!digits.length) return out;
out[1] = 0x80 | ((Number.isInteger(ton) ? ton : 1) << 4) | 0x01; // extension + TON + NPI(ISDN)
for (let i = 0; i < 10; i++) {
const lo = i * 2 < digits.length ? digits[i * 2] : 'F';
const hi = i * 2 + 1 < digits.length ? digits[i * 2 + 1] : 'F';
out[2 + i] = parseInt(hi + lo, 16);
}
return out;
}
function efSmspAddrUnchanged(slot, key, val) {
const model = efSmspAddressModel(slot, true, key === 'sca');
const num = String(val && val.number !== undefined && val.number !== null ? val.number : '').replace(/\s+/g, '');
const ton = val && val.ton !== undefined && val.ton !== null ? Number(val.ton) : model.ton;
return num === model.number && ton === model.ton;
}
function efSmspByteUnchanged(origByte, value) {
const s = String(value === undefined || value === null ? '' : value).replace(/^0x/i, '').trim();
if (origByte === undefined || !/^[0-9a-fA-F]{2}$/.test(s)) return false;
return parseInt(s, 16) === origByte;
}
// EF.SMSP: the alpha, the five parameters' slots and the indicator byte. An
// unchanged slot is copied verbatim (the fixture's absent DCS 00 and VP FF
// included); the indicator byte is kept when all five presence flags match.
function efEncSmsp(values, origHex) {
const orig = efBytes(origHex || '');
if (orig.length < 28) throw efEncError('The record is too short for the SMSP layout');
const y = orig.length - 28;
const origPi = orig[y];
const out = new Uint8Array(orig.length);
out.fill(0xFF);
const alpha = String(values && values.alpha !== undefined && values.alpha !== null ? values.alpha : '');
const origAlpha = efAnnexA(efRstripFf(orig.subarray(0, y)));
if (alpha === origAlpha) out.set(orig.subarray(0, y), 0);
else out.set(efEncTextBytes(alpha, y, 'The alpha identifier is too long for this record'), 0);
const params = (values && values.params) || {};
const presence = {};
for (const spec of EF_SMSP_PARAMS) {
const v = params[spec.key] || {};
const origPresent = !(origPi & spec.bit);
const present = v.present === undefined ? origPresent : !!v.present;
presence[spec.key] = present;
const at = y + spec.off;
const samePresence = present === origPresent;
if (spec.kind === 'address') {
const slot = orig.subarray(at, at + 12);
const origTon = slot.length > 1 ? (slot[1] >> 4) & 0x07 : 0;
// a present alphanumeric (TON 5) address is not editable here: it
// stays verbatim, and only its presence can change
if (origPresent && origTon === 5) {
if (present) out.set(slot, at);
else for (let i = 0; i < 12; i++) out[at + i] = 0xFF;
} else if (samePresence && efSmspAddrUnchanged(slot, spec.key, v)) out.set(slot, at);
else if (!present) for (let i = 0; i < 12; i++) out[at + i] = 0xFF;
else out.set(efSmspAddressBytes(spec.key, v), at);
} else {
const origByte = orig.length > at ? orig[at] : undefined;
if (samePresence && efSmspByteUnchanged(origByte, v.value)) out[at] = origByte;
else if (!present) out[at] = 0xFF;
else out[at] = efSmspHexByte(v.value);
}
}
const allSame = EF_SMSP_PARAMS.every(spec => presence[spec.key] === !(origPi & spec.bit));
if (allSame) out[y] = origPi;
else {
let pi = 0xE0;
for (const spec of EF_SMSP_PARAMS) if (!presence[spec.key]) pi |= spec.bit;
out[y] = pi;
}
return efHex(out);
}
const EF_SMSP_CUSTOM = { model: efSmspModel, html: efSmspHtml, collect: efSmspCollect };
// The decoded editor's field schemas: one per file family, the keys matching
// the decoder output. `kind` is text, digits, bool, select or techs (the
// access technology checkboxes, the value being the decoder's ', '-joined
@@ -24423,6 +24773,7 @@ const EF_ENCODERS = [
{ name: 'EF.CBMI', fid: '6f45', enc: efEncCbmi, model: efCbmiModel, list: EF_CBMI_LIST },
{ name: 'EF.CBMID', fid: '6f48', enc: efEncCbmi, model: efCbmiModel, list: EF_CBMI_LIST },
{ name: 'EF.CBMIR', fid: '6f50', enc: efEncCbmir, model: efCbmirModel, list: EF_CBMIR_LIST },
{ name: 'EF.SMSP', fid: '6f42', enc: efEncSmsp, custom: EF_SMSP_CUSTOM },
{ name: 'EF.ADN', fid: '6f3a', enc: efEncAdnExt(14, 'EXT1'), ext: 'EXT1', fields: EF_ADN_FIELDS },
{ name: 'EF.FDN', fid: '6f3b', enc: efEncAdnExt(14, 'EXT2'), ext: 'EXT2', fields: EF_ADN_FIELDS },
{ name: 'EF.SDN', fid: '6f49', enc: efEncAdnExt(14, 'EXT3'), ext: 'EXT3', fields: EF_ADN_FIELDS },
@@ -24652,16 +25003,19 @@ function efDecEditModel(name, fid, content) {
const entry = efFindEncoder(name, fid);
if (!entry) return null;
const dec = efFindDecoder(name, fid);
const modelValues = hex => entry.custom
? entry.custom.model(hex)
: efDecModelFor(entry, dec, hex);
if (content && content.kind === 'record') {
return {
entry: entry,
mode: 'records',
records: (content.records || []).map(r => entry.items
? { num: r.num, orig: r.data || '', items: efDecItemsFromHex(entry, r.data || '') }
: { num: r.num, orig: r.data || '', values: efDecModelFor(entry, dec, r.data || '') }),
: { num: r.num, orig: r.data || '', values: modelValues(r.data || '') }),
};
}
return { entry: entry, mode: 'tr', values: efDecModelFor(entry, dec, (content && content.data) || '') };
return { entry: entry, mode: 'tr', values: modelValues((content && content.data) || '') };
}
// The value of a techs field (the decoder's ', '-joined string) as a list.
@@ -24793,7 +25147,8 @@ function efDecRecordsHtml(entry, records) {
'<label class="flex items-center gap-1 text-xs text-gray-500 dark:text-slate-400 mb-0.5">' +
'<input type="checkbox" class="pysim-fs-record-cb shrink-0 hidden">' +
esc(t('Record')) + ' ' + esc(String(r.num)) + (empty ? ' — ' + esc(t('empty')) : '') + '</label>' +
'<div class="pl-5">' + (entry.items ? efDecItemsHtml(entry, r) : efDecGridHtml(entry.fields, r.values)) + '</div></div>';
'<div class="pl-5">' + (entry.items ? efDecItemsHtml(entry, r)
: (entry.custom ? entry.custom.html(r.values) : efDecGridHtml(entry.fields, r.values))) + '</div></div>';
}
return html;
}
@@ -24816,7 +25171,7 @@ function efDecEditHtml(model, origHex) {
return '<div class="pysim-dec-edit" data-mode="tr" data-orig="' + esc(origHex || '') + '">' + title + html + '</div>';
}
return '<div class="pysim-dec-edit" data-mode="tr" data-orig="' + esc(origHex || '') + '">' + title +
efDecGridHtml(entry.fields, model.values) + '</div>';
(entry.custom ? entry.custom.html(model.values) : efDecGridHtml(entry.fields, model.values)) + '</div>';
}
// Collect the form values of one scope (the whole TR form or one record
@@ -25671,6 +26026,53 @@ const LANG_RU = {
'Write cancelled': 'Запись отменена',
'Writing the ICCID changes the card identity and may break network registration. Continue?': 'Запись ICCID меняет идентичность карты и может нарушить регистрацию в сети. Продолжить?',
'Writing the IMSI changes the card identity and may break network registration. Continue?': 'Запись IMSI меняет идентичность карты и может нарушить регистрацию в сети. Продолжить?',
'Unknown': 'Неизвестно',
'International': 'Международный',
'National': 'Национальный',
'Absent (the ME default applies)': 'Отсутствует (действует значение ME по умолчанию)',
'absent — the ME default applies': 'отсутствует — действует значение ME по умолчанию',
'alphanumeric address — not editable here; it is preserved as is': 'алфавитно-цифровой адрес — здесь не редактируется; сохраняется как есть',
'day': 'день',
'days-few': 'дня',
'weeks-few': 'недели',
'TP-DA': 'TP-DA',
'TS-SCA': 'TS-SCA',
'TP-PID': 'TP-PID',
'TP-DCS': 'TP-DCS',
'TP-VP': 'TP-VP',
'empty = default SMSC': 'пусто = SMSC по умолчанию',
'TS 23.040 TP-PID (raw); 0x00 = short message type 0': 'TS 23.040 TP-PID (raw); 0x00 — короткое сообщение типа 0',
'general data coding': 'общая индикация кодирования',
'automatic deletion': 'автоматическое удаление',
'GSM 7-bit': 'GSM 7-бит',
'8-bit data': '8-битные данные',
'UCS2': 'UCS2',
'reserved': 'резерв',
'compressed': 'сжатый',
'uncompressed': 'несжатый',
'class 0': 'класс 0',
'class 1': 'класс 1',
'class 2': 'класс 2',
'class 3': 'класс 3',
'reserved coding group': 'резервная группа кодирования',
'MWI discard': 'MWI: удалить',
'MWI store': 'MWI: сохранить',
'inactive': 'неактивно',
'active': 'активно',
'voicemail': 'голосовая почта',
'fax': 'факс',
'email': 'эл. почта',
'other': 'другое',
'data coding': 'кодирование данных',
'h': 'ч',
'week': 'неделя',
'weeks': 'недель',
'days': 'дней',
'The record is too short for the SMSP layout': 'Запись слишком короткая для формата SMSP',
'The alpha identifier is too long for this record': 'Альфа-идентификатор слишком длинный для этой записи',
'The number is too long for SMSP (max 20 digits)': 'Номер слишком длинный для SMSP (максимум 20 цифр)',
'The TP-DA number is required when present': 'Номер TP-DA обязателен, если параметр присутствует',
'The parameter value must be 2 hex digits': 'Значение параметра должно состоять из 2 hex-цифр',
'Refresh': 'Обновить',
'Last refresh': 'Последнее обновление',
'Save': 'Сохранить',
+1 -1
View File
@@ -1,4 +1,4 @@
const CACHE = 'simple-v467';
const CACHE = 'simple-v468';
const URLS = [
'index.html',
'help.html',
+29 -1
View File
@@ -27,7 +27,7 @@ const FNS = [
'efDecIccid', 'efDecImsi', 'efDecLi', 'efDecServiceTable', 'efDecSst', 'efDecPlmnList', 'efDecFplmn', 'efDecPlmnWact',
'efDecSpn', 'efDecLoci', 'efDecPsLoci', 'efDecEpsLoci', 'efDecEpsNsc', 'efDecKc', 'efDecAcc',
'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',
'efDecExt1', 'efDecSms', 'efDecSume', 'efPbrHex', 'efDecPbr', 'efFindDecoder', 'efFidFromPath', 'efDecodeBytes',
'efDecodeFile', 'efDecodeHex', 'efIsRawOnly', 'efFieldLabel', 'efPrimitive', 'efFlatten',
@@ -278,6 +278,9 @@ test('EF record decoders: SMS, OPL, DIR, ARR, IMPI/SMSP', () => {
assert.strictEqual(smsp.sca, '+4915790109999');
assert.strictEqual(smsp.tp_pid, '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
const sms = efDecSms(efBytes('030891945197109099f90000'));
assert.strictEqual(sms.direction, 'MT');
@@ -285,6 +288,31 @@ test('EF record decoders: SMS, OPL, DIR, ARR, IMPI/SMSP', () => {
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', () => {
const ust = efDecServiceTable(efBytes('00000010'), EF_UST_SERVICES);
assert.deepStrictEqual(ust.services, [{ n: 29, name: 'Data download via SMS-CB' }]);
+241 -4
View File
@@ -44,20 +44,20 @@ function extractConst(src, name) {
const FNS = [
'efBytes', 'efHex', 'efSwapNibbles', 'efAllFf', 'efRstripFf', 'efUcs2Be',
'efGsm7Octets', 'efAnnexA', 'efBcdAddress', 'efPlmn', 'efPlmnAct',
'efDecSpn', 'efDecPlmnWact', 'efDecAdn', 'efDecPnn', 'efFindDecoder',
'efGsm7Octets', 'efAnnexA', 'efBcdAddress', 'efBcdAddressLen', 'efPlmn', 'efPlmnAct',
'efDecSpn', 'efDecPlmnWact', 'efDecAdn', 'efDecPnn', 'efDecSmsp', 'efFindDecoder',
'efDecodeBytes', 'efTlv', 'efPnnText', 'efDecItemsFromHex',
'efGsm7PlainText', 'efEncTextBytes', 'efPlmnEnc', 'efActEnc', 'efEncSpn',
'efEncPlmnList', 'efListRows', 'efPlmnRowOrig', 'efEncAdn', 'efEncAdnExt', 'efPnnNameBytes', 'efPnnHexBytes', 'efPnnOrigTlv', 'efEncPnn',
'efFindEncoder', 'efHasEncoder', 'gsm7Decode', 'gsm7TextToSeptets',
'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',
'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 = '';
for (const f of FNS) code += extractFunc(html, f) + '\n';
for (const c of ['GSM7_ALPHABET', 'GSM7_EXT_MAP', 'EF_ACT_TECHS', 'EF_SPN_FIELDS',
'EF_ADN_FIELDS', 'EF_PLMN_LIST', 'EF_PNN_ITEMS', 'EF_UST_SERVICES', 'EF_SST_SERVICES', 'EF_IST_SERVICES', 'EF_EST_SERVICES', 'EF_PHASE_NAMES', 'EF_ACC_CLASSES', 'EF_UST_FIELDS', 'EF_IST_FIELDS', 'EF_EST_FIELDS', 'EF_ACC_FIELDS', 'EF_PHASE_FIELDS', 'EF_HPPLMN_FIELDS', 'EF_AD_OP_MODES', 'EF_AD_FIELDS', 'EF_PLMN_ONLY_LIST', 'EF_ICCID_FIELDS', 'EF_IMSI_FIELDS', 'EF_CBMI_LIST', 'EF_CBMIR_LIST', 'EF_READONLY_NAMES', 'EF_ENCODERS']) {
'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);
}
eval(code);
@@ -456,6 +456,7 @@ test('unchanged content re-encodes byte-identically (all fixtures)', () => {
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 = {};
@@ -510,6 +511,8 @@ test('unchanged content re-encodes byte-identically (all fixtures)', () => {
{ 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) {
@@ -768,3 +771,237 @@ test('CBMI / CBMIR: message ids and ranges round-trip with FF padding', () => {
{ 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');
});
+204 -3
View File
@@ -40,7 +40,7 @@ function extractConst(src, name) {
}
const FNS = [
'efBytes', 'efHex', 'efAllFf', 'efRstripFf', 'efUcs2Be', 'efGsm7Octets',
'efBytes', 'efHex', 'efSwapNibbles', 'efAllFf', 'efRstripFf', 'efUcs2Be', 'efGsm7Octets',
'efAnnexA', 'efBcdAddress', 'efPlmn', 'efPlmnAct', 'efDecSpn', 'efDecPlmnWact',
'efDecAdn', 'efFindDecoder', 'efDecodeBytes', 'efIsRawOnly', 'efTlv',
'efPnnText', 'efDecItemsFromHex',
@@ -56,14 +56,14 @@ const FNS = [
'pysimFsHasDecoder', 'pysimFsHasEncoder', 'pysimFsDecSave', 'pysimFsDecEdit',
'pysimFsPillsEnabled', 'pysimFsSyncContentVisibility', 'gsm7Decode',
'gsm7TextToSeptets', 'gsm7Encode',
'efDecServiceTable', 'efDecSst', 'efDecAcc', 'efDecPhase', 'efDecHpplmn', 'efDecAd', 'efDecCbmi', 'efDecCbmir', 'efDecIccid', 'efDecImsi', 'efDecPlmnList', 'efDecFplmn', 'efEncServiceTable', 'efEncAcc', 'efPhaseValue', 'efPhaseModel', 'efEncPhase', 'efEncHpplmn', 'efAdOpValue', 'efAdModel', 'efEncAd', 'efEncPlmnOnly', 'efEncIccid', 'efEncImsi', 'efCbmiModel', 'efEncCbmi', 'efCbmirModel', 'efEncCbmir',
'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'
+ 'var pysimFsDecEditMode = false;\nvar pysimFsEditMode = false;\n';
for (const f of FNS) code += extractFunc(html, f) + '\n';
for (const c of ['GSM7_ALPHABET', 'GSM7_EXT_MAP', 'EF_ACT_TECHS', 'EF_SPN_FIELDS',
'EF_ADN_FIELDS', 'EF_PLMN_LIST', 'EF_PNN_ITEMS', 'EF_UST_SERVICES', 'EF_SST_SERVICES', 'EF_IST_SERVICES', 'EF_EST_SERVICES', 'EF_PHASE_NAMES', 'EF_ACC_CLASSES', 'EF_UST_FIELDS', 'EF_IST_FIELDS', 'EF_EST_FIELDS', 'EF_ACC_FIELDS', 'EF_PHASE_FIELDS', 'EF_HPPLMN_FIELDS', 'EF_AD_OP_MODES', 'EF_AD_FIELDS', 'EF_PLMN_ONLY_LIST', 'EF_ICCID_FIELDS', 'EF_IMSI_FIELDS', 'EF_CBMI_LIST', 'EF_CBMIR_LIST', 'EF_READONLY_NAMES', 'EF_ENCODERS']) {
'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);
}
eval(code);
@@ -905,3 +905,204 @@ test('F5: an untouched all-FF AD saves FF through the collected form value', asy
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');
});
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "pysim-simple-server"
version = "3.34.0"
version = "3.35.0"
description = "HTTP REST server wrapping pysim for the SIMple PWA"
license = { text = "GPL-2.0-or-later" }
requires-python = ">=3.9"
+1 -1
View File
@@ -37,7 +37,7 @@ from osmocom.tlv import BER_TLV_IE
from osmocom.utils import rpad
VERSION = '3.34.0'
VERSION = '3.35.0'
MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE