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:
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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.
|
||||
|
||||
@@ -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
@@ -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’s unblock key.</li>
|
||||
</ul>
|
||||
|
||||
+421
-19
@@ -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
@@ -1,4 +1,4 @@
|
||||
const CACHE = 'simple-v467';
|
||||
const CACHE = 'simple-v468';
|
||||
const URLS = [
|
||||
'index.html',
|
||||
'help.html',
|
||||
|
||||
@@ -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' }]);
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
|
||||
@@ -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
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user