EF.SST: tri-state editor (deleted / allocated-off / on)

Give EF.SST its own per-file editor for the 2-bit SIM service table
(TS 51.011 10.3.7: allocated + activated per service):

- efEncSst encodes the pair layout, preserving unmodelled pairs and the
  file length; validates service number, fit and state.
- A new `bits2` field kind renders a three-state select per service and
  collects {n, name, allocated, activated}; registered as EF_SST_FIELDS
  with the encoder (df '7F20'), and SST leaves EF_READONLY_NAMES.
- i18n: Deleted/Off/On + the state-error and hint strings (EN/RU).

Docs/help updated: SST is no longer read-only.  Frontend 1093 + Python
1090 tests green.  (Version/sw bump still deferred for the unreleased
decoded-editing batch.)
This commit is contained in:
catarrh committed 2026-10-25 21:43:07 +03:00
1 parent 5ebcd0773d
commit fc27feba8e
6 files changed
+296 -30

No files matched your search

+8 -7
View File
@@ -64,7 +64,7 @@ editing candidates.
| EF.IMSI | 6F07 | TS 31.102 4.2.2 | 9 B | length byte + parity/type nibble ('9' odd / '1' even) + BCD | S3 | **3.34** | | EF.IMSI | 6F07 | TS 31.102 4.2.2 | 9 B | length byte + parity/type nibble ('9' odd / '1' even) + BCD | S3 | **3.34** |
| EF.IST | 6F07 | TS 31.102 4.2.2 (SAIP) | 1–2 B | service bits | S6 | **3.34** | | EF.IST | 6F07 | TS 31.102 4.2.2 (SAIP) | 1–2 B | service bits | S6 | **3.34** |
| EF.UST | 6F38 | TS 31.102 4.2.8 | X ≥ 1 | service bits | S6 | **3.34** | | EF.UST | 6F38 | TS 31.102 4.2.8 | X ≥ 1 | service bits | S6 | **3.34** |
| EF.SST | 6F38 | TS 51.011 10.3.7 | X ≥ 1 | **2 bits/service** (allocated + activated) | S6 (pending) | decoder fixed; editor later | | EF.SST | 6F38 | TS 51.011 10.3.7 | X ≥ 1 | **2 bits/service** (allocated + activated) | S6 | **shipped** — tri-state editor (deleted / allocated-off / on) |
| EF.EST | 6F56 | TS 31.102 4.2.47 | X ≥ 1 | enabled-service bits (3 services) | S6 | **3.34** | | EF.EST | 6F56 | TS 31.102 4.2.47 | X ≥ 1 | enabled-service bits (3 services) | S6 | **3.34** |
| EF.LI | 6F05 | TS 31.102 4.2.1 | 2n | ISO 639 letters (2 B each) | S6 | 3.35 | | EF.LI | 6F05 | TS 31.102 4.2.1 | 2n | ISO 639 letters (2 B each) | S6 | 3.35 |
| EF.LP | 6F05 | TS 51.011 10.3.1 | n | **1 byte per language** (SMS 7-bit) | S6 | 3.35 | | EF.LP | 6F05 | TS 51.011 10.3.1 | n | **1 byte per language** (SMS 7-bit) | S6 | 3.35 |
@@ -194,8 +194,8 @@ OPLMNwAcT/OPL/PNN/EHPLMN/FPLMN/SPDI/IMG are USIM-only (no SIM counterpart).
8. **EF.SST is a 2-bit table** (TS 51.011 10.3.7): 4 services per byte, first 8. **EF.SST is a 2-bit table** (TS 51.011 10.3.7): 4 services per byte, first
bit of the pair = allocated, second = activated. The shared 1-bit bit of the pair = allocated, second = activated. The shared 1-bit
UST/EST codec was used for it, so SIM service tables were both misread and UST/EST codec was used for it, so SIM service tables were both misread and
miswritten; the decoder is fixed in v3.34.0, editing waits for an miswritten; the decoder is fixed in v3.34.0, and the tri-state editor
allocated/activated UI. (deleted / allocated-off / on) shipped with the DF-context work.
9. **EF.ACC class numbering** (TS 31.102 4.2.15): byte 1 b8…b1 = classes 15…8 9. **EF.ACC class numbering** (TS 31.102 4.2.15): byte 1 b8…b1 = classes 15…8
and byte 2 = 7…0 (class 10 is written as 0); the old decoder counted from and byte 2 = 7…0 (class 10 is written as 0); the old decoder counted from
class 0 at the MSB. Fixed in v3.34.0. class 0 at the MSB. Fixed in v3.34.0.
@@ -228,7 +228,7 @@ OPLMNwAcT/OPL/PNN/EHPLMN/FPLMN/SPDI/IMG are USIM-only (no SIM counterpart).
(filterable bit grid), ACC, PHASE, HPPLMN, AD, the PLMN-only lists (filterable bit grid), ACC, PHASE, HPPLMN, AD, the PLMN-only lists
(PLMNsel/FPLMN/EHPLMN), ICCID/IMSI (behind a confirmation) and (PLMNsel/FPLMN/EHPLMN), ICCID/IMSI (behind a confirmation) and
CBMI/CBMID/CBMIR. Decoder fixes: ACC class numbering, the AD operation CBMI/CBMID/CBMIR. Decoder fixes: ACC class numbering, the AD operation
mode / MNC-length rules, the SST 2-bit layout (read-only for now) and the mode / MNC-length rules, the SST 2-bit layout and the
IMSI parity nibble. IMSI parity nibble.
- **v3.35.0 (shipped)** — EF.SMSP as the first S10 dedicated builder: the full - **v3.35.0 (shipped)** — EF.SMSP as the first S10 dedicated builder: the full
parameter form (alpha; TP-DA and TS-SCA with their own length conventions parameter form (alpha; TP-DA and TS-SCA with their own length conventions
@@ -267,9 +267,10 @@ OPLMNwAcT/OPL/PNN/EHPLMN/FPLMN/SPDI/IMG are USIM-only (no SIM counterpart).
A file-agnostic service editor for the Card reader tab, in addition to the A file-agnostic service editor for the Card reader tab, in addition to the
per-file decoded editors (which stay independent). It presents the four per-file decoded editors (which stay independent). It presents the four
service tables side by side and writes them back consistently, so EF.SST's service tables side by side and writes them back consistently. EF.SST also
allocated/activated layout no longer needs its own per-file grid (SST stays has its own per-file tri-state editor (deleted / allocated-off / on); the
read-only in the per-file editor). Services editor's SIM column only toggles on / allocated-off and never
de-allocates.
- **Scope**: EF.SST (DF.GSM, 2 bits/service: allocated + activated), EF.UST - **Scope**: EF.SST (DF.GSM, 2 bits/service: allocated + activated), EF.UST
(ADF.USIM, 1 bit), EF.EST (ADF.USIM, a 3-bit *enabled* subset of UST: (ADF.USIM, 1 bit), EF.EST (ADF.USIM, a 3-bit *enabled* subset of UST:
+1 -1
View File
@@ -409,7 +409,7 @@
<ul class="list-disc list-inside text-sm space-y-1 mb-3"> <ul class="list-disc list-inside text-sm space-y-1 mb-3">
<li><strong>Читать сырые данные / Декодировать</strong> — чтение выбранного файла в виде hex-дампа или таблицы декодированных полей (клиентские декодеры EF: IMSI, ICCID, SPN, списки PLMN, LOCI/PSLOCI/EPSLOCI, ADN/MSISDN, таблицы сервисов, SUME, …); таблица декодируется в браузере из тех же сырых байт (без серверного декодирования). Подсвеченная кнопка — текущий вид; нажатие любой из них (пере)читывает файл. «Декодировать» недоступна, если для файла нет клиентского декодера (проверка по имени/FID до чтения).</li> <li><strong>Читать сырые данные / Декодировать</strong> — чтение выбранного файла в виде hex-дампа или таблицы декодированных полей (клиентские декодеры EF: IMSI, ICCID, SPN, списки PLMN, LOCI/PSLOCI/EPSLOCI, ADN/MSISDN, таблицы сервисов, SUME, …); таблица декодируется в браузере из тех же сырых байт (без серверного декодирования). Подсвеченная кнопка — текущий вид; нажатие любой из них (пере)читывает файл. «Декодировать» недоступна, если для файла нет клиентского декодера (проверка по имени/FID до чтения).</li>
<li><strong>Редактировать сырые данные</strong> — изменение hex-данных, <strong>Сохранить</strong> для записи (или <strong>Отмена</strong>); transparent-файлы редактируются в textarea, record-файлы — по одному полю на запись с флажками выбора. «Редактировать сырые данные» сначала переключает на hex-вид и при необходимости читает файл.</li> <li><strong>Редактировать сырые данные</strong> — изменение hex-данных, <strong>Сохранить</strong> для записи (или <strong>Отмена</strong>); transparent-файлы редактируются в textarea, record-файлы — по одному полю на запись с флажками выбора. «Редактировать сырые данные» сначала переключает на hex-вид и при необходимости читает файл.</li>
<li><strong>Редактировать декодированные</strong> — редактирование файла в его собственной декодированной форме; кодирование выполняется в браузере, запись идёт обычным путём обновления. Кодировщики покрывают <strong>EF.SPN</strong>, <strong>EF.PNN</strong>, списки PLMN (<strong>PLMNsel / FPLMN / EHPLMN</strong> и списки с технологиями доступа <strong>PLMNwAcT / OPLMNwAcT / HPLMNwAcT</strong>), файлы номеров (<strong>ADN / FDN / SDN / MSISDN / LND / BDN</strong>), таблицы сервисов (<strong>UST / IST / EST</strong>; <strong>SST</strong> остаётся только для чтения до появления редактора allocated/activated), <strong>ACC</strong>, <strong>PHASE</strong>, <strong>HPPLMN</strong>, <strong>AD</strong>, <strong>ICCID / IMSI</strong> (с подтверждением — запись меняет идентичность карты) списки cell broadcast (<strong>CBMI / CBMID / CBMIR</strong>) и <strong>EF.SMSP</strong> (полная форма SMS-параметров: alpha, TP-DA и TS-SCA с их правилами адресации и TON, TP-PID, TP-DCS и относительный TP-VP; у каждого параметра — флажок <em>«отсутствует (действует значение ME по умолчанию)»</em> и живые подсказки по DCS и сроку действия). Для transparent-файла — одна форма (поля SPN, строки PLMN с добавлением/удалением, элементы PNN с добавлением/удалением); для record-файла — по форме на запись с флажком выбора, который ставится автоматически при изменении поля, поэтому записываются только изменённые записи. <strong>Сохранить</strong> проверяет все выбранные записи до записи; для файлов без кодировщика кнопка недоступна с подсказкой. Указатели расширений (EF.EXT1–EF.EXT5) и байт метода сравнения BDN сохраняются; редактирование номера, продолжение которого лежит в файле расширения, отклоняется.</li> <li><strong>Редактировать декодированные</strong> — редактирование файла в его собственной декодированной форме; кодирование выполняется в браузере, запись идёт обычным путём обновления. Кодировщики покрывают <strong>EF.SPN</strong>, <strong>EF.PNN</strong>, списки PLMN (<strong>PLMNsel / FPLMN / EHPLMN</strong> и списки с технологиями доступа <strong>PLMNwAcT / OPLMNwAcT / HPLMNwAcT</strong>), файлы номеров (<strong>ADN / FDN / SDN / MSISDN / LND / BDN</strong>), таблицы сервисов (<strong>UST / IST / EST</strong> и <strong>SST</strong> — SIM-таблица, редактируется по каждой службе как удалена / выделена-выключена / включена), <strong>ACC</strong>, <strong>PHASE</strong>, <strong>HPPLMN</strong>, <strong>AD</strong>, <strong>ICCID / IMSI</strong> (с подтверждением — запись меняет идентичность карты) списки cell broadcast (<strong>CBMI / CBMID / CBMIR</strong>) и <strong>EF.SMSP</strong> (полная форма SMS-параметров: alpha, TP-DA и TS-SCA с их правилами адресации и TON, TP-PID, TP-DCS и относительный TP-VP; у каждого параметра — флажок <em>«отсутствует (действует значение ME по умолчанию)»</em> и живые подсказки по DCS и сроку действия). Для transparent-файла — одна форма (поля SPN, строки PLMN с добавлением/удалением, элементы PNN с добавлением/удалением); для record-файла — по форме на запись с флажком выбора, который ставится автоматически при изменении поля, поэтому записываются только изменённые записи. <strong>Сохранить</strong> проверяет все выбранные записи до записи; для файлов без кодировщика кнопка недоступна с подсказкой. Указатели расширений (EF.EXT1–EF.EXT5) и байт метода сравнения BDN сохраняются; редактирование номера, продолжение которого лежит в файле расширения, отклоняется.</li>
<li><strong>Проверить все файлы</strong> — обход всего дерева (включая пользовательские файлы) с пометкой каждого элемента: есть (обычный вид) или нет (красный ✗, без стрелки разворачивания); существующие пустые DF показывают <code class="font-mono text-sm">(пусто)</code>. Отображается прогресс <em>N / всего</em>, обход можно остановить; в конце — сводка «есть/нет». Файлы проверяются только при разворачивании или проверке — просмотр остаётся ленивым.</li> <li><strong>Проверить все файлы</strong> — обход всего дерева (включая пользовательские файлы) с пометкой каждого элемента: есть (обычный вид) или нет (красный ✗, без стрелки разворачивания); существующие пустые DF показывают <code class="font-mono text-sm">(пусто)</code>. Отображается прогресс <em>N / всего</em>, обход можно остановить; в конце — сводка «есть/нет». Файлы проверяются только при разворачивании или проверке — просмотр остаётся ленивым.</li>
<li><strong>ADM</strong> — файлы, требующие администраторский PIN, возвращают <code class="font-mono text-sm">6982</code>/<code class="font-mono text-sm">9804</code>; если в подходящем пресете карты (тот же ICCID) есть ключ ADM, рядом с ошибкой появляется кнопка <strong>«Проверить ADM»</strong>, а значок в заголовке (<code class="font-mono text-sm">ADM ✓/✗ ⚿</code>) становится кликабельным. Каждый неверный ключ расходует попытку: остаток показывается, а повторная попытка требует подтверждения. Заблокированный ADM здесь не восстановить — нужен ключ разблокировки карты.</li> <li><strong>ADM</strong> — файлы, требующие администраторский PIN, возвращают <code class="font-mono text-sm">6982</code>/<code class="font-mono text-sm">9804</code>; если в подходящем пресете карты (тот же ICCID) есть ключ ADM, рядом с ошибкой появляется кнопка <strong>«Проверить ADM»</strong>, а значок в заголовке (<code class="font-mono text-sm">ADM ✓/✗ ⚿</code>) становится кликабельным. Каждый неверный ключ расходует попытку: остаток показывается, а повторная попытка требует подтверждения. Заблокированный ADM здесь не восстановить — нужен ключ разблокировки карты.</li>
</ul> </ul>
+1 -1
View File
@@ -408,7 +408,7 @@
<ul class="list-disc list-inside text-sm space-y-1 mb-3"> <ul class="list-disc list-inside text-sm space-y-1 mb-3">
<li><strong>Read raw / Read decoded</strong> — read the selected file as a hex dump or as a decoded field table (client-side EF decoders: IMSI, ICCID, SPN, PLMN lists, LOCI/PSLOCI/EPSLOCI, ADN/MSISDN, service tables, SUME, …); the decoded table is built in the browser from the same raw bytes (no server-side decode). The highlighted pill is the current view; clicking either pill (re-)reads the file. <strong>Read decoded</strong> is disabled when the client-side decoders do not cover the file (name/FID check, before reading).</li> <li><strong>Read raw / Read decoded</strong> — read the selected file as a hex dump or as a decoded field table (client-side EF decoders: IMSI, ICCID, SPN, PLMN lists, LOCI/PSLOCI/EPSLOCI, ADN/MSISDN, service tables, SUME, …); the decoded table is built in the browser from the same raw bytes (no server-side decode). The highlighted pill is the current view; clicking either pill (re-)reads the file. <strong>Read decoded</strong> is disabled when the client-side decoders do not cover the file (name/FID check, before reading).</li>
<li><strong>Edit raw</strong> — edit the raw hex data, <strong>Save</strong> to write back (or <strong>Cancel</strong>); transparent files use a textarea, record files one input per record with selection checkboxes. <strong>Edit raw</strong> switches to the hex view first and reads the file if needed.</li> <li><strong>Edit raw</strong> — edit the raw hex data, <strong>Save</strong> to write back (or <strong>Cancel</strong>); transparent files use a textarea, record files one input per record with selection checkboxes. <strong>Edit raw</strong> switches to the hex view first and reads the file if needed.</li>
<li><strong>Edit decoded</strong> — edit the file through its own decoded form, encoded in the browser and written with the normal update path. The encoders cover <strong>EF.SPN</strong>, <strong>EF.PNN</strong>, the PLMN lists (<strong>PLMNsel / FPLMN / EHPLMN</strong> and the access-technology lists <strong>PLMNwAcT / OPLMNwAcT / HPLMNwAcT</strong>), the dialling-number files (<strong>ADN / FDN / SDN / MSISDN / LND / BDN</strong>), the service tables (<strong>UST / IST / EST</strong>; <strong>SST</strong> stays read-only until the SIM allocated/activated editor exists), <strong>ACC</strong>, <strong>PHASE</strong>, <strong>HPPLMN</strong>, <strong>AD</strong>, <strong>ICCID / IMSI</strong> (behind a confirmation — writing them changes the card identity) the cell-broadcast lists (<strong>CBMI / CBMID / CBMIR</strong>) and <strong>EF.SMSP</strong> (the full SMS-parameter form: alpha, TP-DA and TS-SCA with their addressing rules and TON, TP-PID, TP-DCS and the relative TP-VP, each with an <em>absent (the ME default applies)</em> toggle and live DCS/validity hints). A transparent file gets one form (SPN fields, PLMN rows with add/remove, PNN items with add/remove); a record file gets one form per record with a selection checkbox that is auto-checked when a field changes, so only the edited records are written. <strong>Save</strong> validates every selected record before writing anything; files without an encoder keep the button disabled with a tooltip. Extension pointers (EF.EXT1–EF.EXT5) and the BDN comparison-method byte are preserved, and editing a number that continues in an extension file is refused.</li> <li><strong>Edit decoded</strong> — edit the file through its own decoded form, encoded in the browser and written with the normal update path. The encoders cover <strong>EF.SPN</strong>, <strong>EF.PNN</strong>, the PLMN lists (<strong>PLMNsel / FPLMN / EHPLMN</strong> and the access-technology lists <strong>PLMNwAcT / OPLMNwAcT / HPLMNwAcT</strong>), the dialling-number files (<strong>ADN / FDN / SDN / MSISDN / LND / BDN</strong>), the service tables (<strong>UST / IST / EST</strong> and <strong>SST</strong> — the SIM table, edited per service as deleted / allocated-off / on), <strong>ACC</strong>, <strong>PHASE</strong>, <strong>HPPLMN</strong>, <strong>AD</strong>, <strong>ICCID / IMSI</strong> (behind a confirmation — writing them changes the card identity) the cell-broadcast lists (<strong>CBMI / CBMID / CBMIR</strong>) and <strong>EF.SMSP</strong> (the full SMS-parameter form: alpha, TP-DA and TS-SCA with their addressing rules and TON, TP-PID, TP-DCS and the relative TP-VP, each with an <em>absent (the ME default applies)</em> toggle and live DCS/validity hints). A transparent file gets one form (SPN fields, PLMN rows with add/remove, PNN items with add/remove); a record file gets one form per record with a selection checkbox that is auto-checked when a field changes, so only the edited records are written. <strong>Save</strong> validates every selected record before writing anything; files without an encoder keep the button disabled with a tooltip. Extension pointers (EF.EXT1–EF.EXT5) and the BDN comparison-method byte are preserved, and editing a number that continues in an extension file is refused.</li>
<li><strong>Probe all files</strong> — walks the whole tree (including custom files) and marks every entry present (normal) or absent (red ✗, no expand arrow); empty-but-present DFs show <code class="font-mono text-sm">(empty)</code>. Shows progress <em>N / total</em>, can be stopped, and finishes with a present/absent summary. Files are only verified when expanded or probed — browsing stays lazy.</li> <li><strong>Probe all files</strong> — walks the whole tree (including custom files) and marks every entry present (normal) or absent (red ✗, no expand arrow); empty-but-present DFs show <code class="font-mono text-sm">(empty)</code>. Shows progress <em>N / total</em>, can be stopped, and finishes with a present/absent summary. Files are only verified when expanded or probed — browsing stays lazy.</li>
<li><strong>ADM</strong> — files that need the administrator PIN fail with <code class="font-mono text-sm">6982</code>/<code class="font-mono text-sm">9804</code>; if the matching card preset (same ICCID) carries an ADM key, a <strong>Verify ADM</strong> button appears next to the error and the header badge (<code class="font-mono text-sm">ADM ✓/✗ ⚿</code>) becomes clickable. Every wrong key consumes an attempt: the remaining attempts are shown and a retry asks for confirmation. A blocked ADM cannot be recovered here — it needs the card&rsquo;s unblock key.</li> <li><strong>ADM</strong> — files that need the administrator PIN fail with <code class="font-mono text-sm">6982</code>/<code class="font-mono text-sm">9804</code>; if the matching card preset (same ICCID) carries an ADM key, a <strong>Verify ADM</strong> button appears next to the error and the header badge (<code class="font-mono text-sm">ADM ✓/✗ ⚿</code>) becomes clickable. Every wrong key consumes an attempt: the remaining attempts are shown and a retry asks for confirmation. A blocked ADM cannot be recovered here — it needs the card&rsquo;s unblock key.</li>
</ul> </ul>
+115 -5
View File
@@ -23988,7 +23988,7 @@ const EF_DECODERS = [
{ name: 'EF.IMSI', fid: '6f07', fn: efDecImsi }, { name: 'EF.IMSI', fid: '6f07', fn: efDecImsi },
{ name: 'EF.IST', fid: '6f07', fn: b => efDecServiceTable(b, EF_IST_SERVICES) }, { name: 'EF.IST', fid: '6f07', fn: b => efDecServiceTable(b, EF_IST_SERVICES) },
{ name: 'EF.UST', fid: '6f38', fn: b => efDecServiceTable(b, EF_UST_SERVICES) }, { name: 'EF.UST', fid: '6f38', fn: b => efDecServiceTable(b, EF_UST_SERVICES) },
{ name: 'EF.SST', fid: '6f38', fn: efDecSst }, { name: 'EF.SST', df: '7F20', fid: '6f38', fn: efDecSst },
{ name: 'EF.EST', fid: '6f56', fn: b => efDecServiceTable(b, EF_EST_SERVICES) }, { name: 'EF.EST', fid: '6f56', fn: b => efDecServiceTable(b, EF_EST_SERVICES) },
{ name: 'EF.HPPLMN', fid: '6f31', fn: efDecHpplmn }, { name: 'EF.HPPLMN', fid: '6f31', fn: efDecHpplmn },
{ name: 'EF.LI', fid: '6f05', fn: efDecLi }, { name: 'EF.LI', fid: '6f05', fn: efDecLi },
@@ -24467,6 +24467,51 @@ function efEncServiceTable(values, origHex, table) {
return efHex(out); return efHex(out);
} }
// EF.SST (TS 51.011 10.3.7, the SIM service table): two bits per service,
// four services per byte - the first bit of the pair (b1/b3/b5/b7) is
// allocated, the second (b2/b4/b6/b8) activated, so a pair is 00 deleted,
// 10 allocated but not activated or 11 allocated and activated. The original
// bytes are the base: both pair bits of every modelled service are cleared
// and the listed ones set again, so a pair of a service the 59-entry table
// does not name (the decoder reports it as RFU) keeps its original bits and
// the file length survives an edit. An entry is the decoder's own shape
// {n, name, allocated, activated} (the collector produces it), so
// enc(decode(orig), orig) reproduces the file of a spec-canonical card;
// activated without allocated is not a legal pair (10.3.7) and is refused.
function efEncSst(values, origHex) {
const orig = efBytes(origHex || '');
const out = new Uint8Array(orig.length);
out.set(orig, 0);
for (const key of Object.keys(EF_SST_SERVICES)) {
const n = Number(key);
if (n < 1 || (n - 1) >= out.length * 4) continue;
out[(n - 1) >> 2] &= ~(3 << (2 * ((n - 1) & 3)));
}
for (const item of (values && values.services) || []) {
const n = Number(item && typeof item === 'object' ? item.n : item);
// the table names services 1-59; a pair beyond it belongs to a longer
// file (the decoder reports those as RFU too), so only a number that
// cannot be a service at all is unknown
if (!Number.isInteger(n) || n < 1) {
throw efEncError('Unknown service', ': ' + (item && item.n));
}
if ((n - 1) >= out.length * 4) {
throw efEncError('The service table is too short for service', ' ' + n);
}
if (!item || typeof item !== 'object') {
throw efEncError('The service state must be deleted, off or on', ': ' + n);
}
const allocated = !!item.allocated;
const activated = !!item.activated;
if (activated && !allocated) {
throw efEncError('The service state must be deleted, off or on', ': ' + n);
}
const pair = activated ? 3 : (allocated ? 1 : 0);
out[(n - 1) >> 2] |= pair << (2 * ((n - 1) & 3));
}
return efHex(out);
}
// EF.ACC (TS 31.102 4.2.15): class c is bit c of the 16-bit value (class 15 is // EF.ACC (TS 31.102 4.2.15): class c is bit c of the 16-bit value (class 15 is
// the MSB); ACC 10 is not used and is written as 0. // the MSB); ACC 10 is not used and is written as 0.
function efEncAcc(values, origHex) { function efEncAcc(values, origHex) {
@@ -25118,6 +25163,13 @@ const EF_ACC_CLASSES = {
const EF_UST_FIELDS = [{ key: 'services', label: 'Services', kind: 'bits', items: EF_UST_SERVICES }]; const EF_UST_FIELDS = [{ key: 'services', label: 'Services', kind: 'bits', items: EF_UST_SERVICES }];
const EF_IST_FIELDS = [{ key: 'services', label: 'Services', kind: 'bits', items: EF_IST_SERVICES }]; const EF_IST_FIELDS = [{ key: 'services', label: 'Services', kind: 'bits', items: EF_IST_SERVICES }];
const EF_EST_FIELDS = [{ key: 'services', label: 'Services', kind: 'bits', items: EF_EST_SERVICES }]; const EF_EST_FIELDS = [{ key: 'services', label: 'Services', kind: 'bits', items: EF_EST_SERVICES }];
// EF.SST is the SIM table with two bits per service: the tri-state grid
// (deleted 00 / off 10 / on 11) instead of the 1-bit checkbox grid.
const EF_SST_FIELDS = [{
key: 'services', label: 'Services', kind: 'bits2', items: EF_SST_SERVICES,
hint: 'Two bits per service (TS 51.011 10.3.7): deleted = not allocated (00), '
+ 'off = allocated but not activated (10), on = allocated and activated (11).',
}];
const EF_ACC_FIELDS = [{ key: 'access_classes', label: 'Access classes', kind: 'bits', items: EF_ACC_CLASSES }]; const EF_ACC_FIELDS = [{ key: 'access_classes', label: 'Access classes', kind: 'bits', items: EF_ACC_CLASSES }];
@@ -25186,6 +25238,7 @@ const EF_ENCODERS = [
confirm: 'Writing the IMSI changes the card identity and may break network registration. Continue?' }, confirm: 'Writing the IMSI changes the card identity and may break network registration. Continue?' },
{ name: 'EF.IST', fid: '6f07', enc: (v, o) => efEncServiceTable(v, o, EF_IST_SERVICES), fields: EF_IST_FIELDS }, { name: 'EF.IST', fid: '6f07', enc: (v, o) => efEncServiceTable(v, o, EF_IST_SERVICES), fields: EF_IST_FIELDS },
{ name: 'EF.UST', fid: '6f38', enc: (v, o) => efEncServiceTable(v, o, EF_UST_SERVICES), fields: EF_UST_FIELDS }, { name: 'EF.UST', fid: '6f38', enc: (v, o) => efEncServiceTable(v, o, EF_UST_SERVICES), fields: EF_UST_FIELDS },
{ name: 'EF.SST', df: '7F20', fid: '6f38', enc: efEncSst, fields: EF_SST_FIELDS },
{ name: 'EF.EST', fid: '6f56', enc: (v, o) => efEncServiceTable(v, o, EF_EST_SERVICES), fields: EF_EST_FIELDS }, { name: 'EF.EST', fid: '6f56', enc: (v, o) => efEncServiceTable(v, o, EF_EST_SERVICES), fields: EF_EST_FIELDS },
{ name: 'EF.ACC', fid: '6f78', enc: efEncAcc, fields: EF_ACC_FIELDS }, { name: 'EF.ACC', fid: '6f78', enc: efEncAcc, fields: EF_ACC_FIELDS },
{ name: 'EF.PHASE', fid: '6fae', enc: efEncPhase, model: efPhaseModel, fields: EF_PHASE_FIELDS }, { name: 'EF.PHASE', fid: '6fae', enc: efEncPhase, model: efPhaseModel, fields: EF_PHASE_FIELDS },
@@ -25217,9 +25270,10 @@ const EF_ENCODERS = [
]; ];
// Decoder-only names that share a FID with an editable file: the FID fallback // Decoder-only names that share a FID with an editable file: the FID fallback
// must not pick the wrong layout (EF.SST is not EF.UST on 6F38 - its 2-bit // must not pick the wrong layout. (The list is empty now - EF.SST got its
// SIM layout has no editor yet). // own 2-bit editor on 6F38 - but the guard stays for a future decoder-only
const EF_READONLY_NAMES = ['EF.SST']; // collision.)
const EF_READONLY_NAMES = [];
function efFindEncoder(name, fid, df) { function efFindEncoder(name, fid, df) {
let n = (name || '').toUpperCase(); let n = (name || '').toUpperCase();
@@ -25500,6 +25554,31 @@ function efDecFieldHtml(f, value) {
'<span class="font-mono text-gray-500 dark:text-slate-400">' + n + '</span>' + '<span class="font-mono text-gray-500 dark:text-slate-400">' + n + '</span>' +
'<span class="break-all">' + esc(t(name)) + '</span></label>'; '<span class="break-all">' + esc(t(name)) + '</span></label>';
}).join('') + '</div></div>'; }).join('') + '</div></div>';
} else if (f.kind === 'bits2') {
// the tri-state grid (EF.SST): one select per service, the decoder's
// entries preselected (an absent service is a deleted pair)
const states = {};
for (const v of (value || [])) {
if (!v || typeof v !== 'object') continue;
states[String(v.n)] = v.activated && v.allocated ? 'on' : (v.allocated ? 'off' : 'deleted');
}
const nums = Object.keys(f.items || {}).map(Number).sort((a, b) => a - b);
const states3 = [['deleted', t('Deleted')], ['off', t('Off')], ['on', t('On')]];
input = '<div class="pysim-dec-bits border border-gray-200 dark:border-slate-700 rounded p-2">' +
'<input type="text" class="pysim-dec-bits-filter w-full mb-1 border border-gray-300 dark:border-slate-600 rounded px-1 bg-white dark:bg-slate-800 text-gray-800 dark:text-slate-200" placeholder="' +
esc(t('Filter services')) + '" oninput="pysimFsDecBits2Filter(this)">' +
'<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(17rem,1fr));max-height:12rem;overflow-y:auto">' +
nums.map(n => {
const name = String(f.items[n]);
const cur = states[String(n)] || 'deleted';
return '<label class="pysim-dec-bit2-row flex items-center gap-1" data-label="' + esc(n + ' ' + name) + '">' +
'<span class="font-mono text-gray-500 dark:text-slate-400">' + n + '</span>' +
'<span class="break-all flex-1">' + esc(t(name)) + '</span>' +
'<select data-bit2="' + n + '" data-bit2-name="' + esc(name) + '" 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">' +
states3.map(o => '<option value="' + o[0] + '"' + (o[0] === cur ? ' selected' : '') + '>' +
esc(o[1]) + '</option>').join('') +
'</select></label>';
}).join('') + '</div></div>';
} else if (f.kind === 'select') { } else if (f.kind === 'select') {
const cur = String(value === undefined || value === null ? '' : value); const cur = String(value === undefined || value === null ? '' : value);
const opts = (f.options || []).slice(); const opts = (f.options || []).slice();
@@ -25518,7 +25597,7 @@ function efDecFieldHtml(f, value) {
' oninput="pysimFsDecRowCheck(this)" class="border border-gray-300 dark:border-slate-600 rounded px-1 bg-white dark:bg-slate-800 text-gray-800 dark:text-slate-200">'; ' oninput="pysimFsDecRowCheck(this)" class="border border-gray-300 dark:border-slate-600 rounded px-1 bg-white dark:bg-slate-800 text-gray-800 dark:text-slate-200">';
} }
const body = '<span class="text-gray-500 dark:text-slate-400">' + esc(t(f.label)) + '</span>' + input; const body = '<span class="text-gray-500 dark:text-slate-400">' + esc(t(f.label)) + '</span>' + input;
const wide = f.kind === 'techs' || f.kind === 'bits'; const wide = f.kind === 'techs' || f.kind === 'bits' || f.kind === 'bits2';
if (!f.hint) { if (!f.hint) {
return wide return wide
? '<div class="pysim-dec-item flex flex-wrap items-center gap-x-2 gap-y-1">' + body + '</div>' ? '<div class="pysim-dec-item flex flex-wrap items-center gap-x-2 gap-y-1">' + body + '</div>'
@@ -25634,6 +25713,20 @@ function efDecCollectValues(root, fields) {
name: cb.getAttribute('data-bit-name') || '' }); name: cb.getAttribute('data-bit-name') || '' });
}); });
out[f.key] = items; out[f.key] = items;
} else if (f.kind === 'bits2') {
// the tri-state grid: every select is collected, a deleted pair as
// {allocated: false, activated: false} (the encoder clears it anyway)
const items = [];
root.querySelectorAll('[data-bit2]').forEach(sel => {
const state = String(sel.value || '');
items.push({
n: parseInt(sel.getAttribute('data-bit2'), 10),
name: sel.getAttribute('data-bit2-name') || '',
allocated: state === 'off' || state === 'on',
activated: state === 'on',
});
});
out[f.key] = items;
} else { } else {
const el = root.querySelector('[data-key="' + f.key + '"]'); const el = root.querySelector('[data-key="' + f.key + '"]');
if (!el) { out[f.key] = f.kind === 'bool' ? false : ''; continue; } if (!el) { out[f.key] = f.kind === 'bool' ? false : ''; continue; }
@@ -25690,6 +25783,19 @@ function pysimFsDecBitsFilter(input) {
}); });
} }
// The tri-state grid's text filter (EF.SST): hide the rows whose service name
// does not match. The query is matched against the row's label only - a
// select's textContent would carry every one of its three state options.
function pysimFsDecBits2Filter(input) {
const box = input && input.closest ? input.closest('.pysim-dec-bits') : null;
if (!box) return;
const q = String(input.value || '').toLowerCase();
box.querySelectorAll('.pysim-dec-bit2-row').forEach(row => {
const label = String(row.getAttribute('data-label') || '').toLowerCase();
row.style.display = label.indexOf(q) === -1 ? 'none' : '';
});
}
function pysimFsDecAddRow() { function pysimFsDecAddRow() {
if (!pysimFsSelected) return; if (!pysimFsSelected) return;
const node = pysimFsFindNodeByPath(pysimFsSelected); const node = pysimFsFindNodeByPath(pysimFsSelected);
@@ -26424,6 +26530,10 @@ const LANG_RU = {
'The number continues in EF.EXT5 - editing it is not supported': 'Номер продолжается в EF.EXT5 — редактирование не поддерживается', 'The number continues in EF.EXT5 - editing it is not supported': 'Номер продолжается в EF.EXT5 — редактирование не поддерживается',
'Services': 'Сервисы', 'Services': 'Сервисы',
'Filter services': 'Фильтр сервисов', 'Filter services': 'Фильтр сервисов',
'Off': 'Выключен',
'On': 'Включён',
'The service state must be deleted, off or on': 'Состояние сервиса должно быть: удалён, выключен или включён',
'Two bits per service (TS 51.011 10.3.7): deleted = not allocated (00), off = allocated but not activated (10), on = allocated and activated (11).': 'Два бита на сервис (TS 51.011 10.3.7): удалён — не выделен (00), выключен — выделен, но не активирован (10), включён — выделен и активирован (11).',
'Access classes': 'Классы доступа', 'Access classes': 'Классы доступа',
'Phase': 'Фаза', 'Phase': 'Фаза',
'Interval': 'Интервал', 'Interval': 'Интервал',
+76 -8
View File
@@ -51,13 +51,13 @@ const FNS = [
'efEncPlmnList', 'efListRows', 'efPlmnRowOrig', 'efEncAdn', 'efEncAdnExt', 'efPnnNameBytes', 'efPnnHexBytes', 'efPnnOrigTlv', 'efEncPnn', 'efEncPlmnList', 'efListRows', 'efPlmnRowOrig', 'efEncAdn', 'efEncAdnExt', 'efPnnNameBytes', 'efPnnHexBytes', 'efPnnOrigTlv', 'efEncPnn',
'efFindEncoder', 'efHasEncoder', 'gsm7Decode', 'gsm7TextToSeptets', 'efFindEncoder', 'efHasEncoder', 'gsm7Decode', 'gsm7TextToSeptets',
'gsm7Encode', 'efEncError', 'efDecModelValues', 'gsm7Encode', 'efEncError', 'efDecModelValues',
'efDecServiceTable', 'efDecSst', 'efDecAcc', 'efDecPhase', 'efDecHpplmn', 'efDecAd', 'efDecCbmi', 'efDecCbmir', 'efDecIccid', 'efDecImsi', 'efDecPlmnList', 'efDecFplmn', 'efEncServiceTable', 'efEncAcc', 'efPhaseValue', 'efPhaseModel', 'efEncPhase', 'efEncHpplmn', 'efAdOpValue', 'efAdModel', 'efEncAd', 'efEncPlmnOnly', 'efEncIccid', 'efEncImsi', 'efCbmiModel', 'efEncCbmi', 'efCbmirModel', 'efEncCbmir', 'efSmspAddressModel', 'efSmspModel', 'efSmspTonOptions', 'efSmspParamRow', 'efSmspHtml', 'efSmspCollect', 'pysimFsSmspToggle', 'pysimFsSmspHexInput', 'efSmspHexByte', 'efSmspAddressBytes', 'efSmspAddrUnchanged', 'efSmspByteUnchanged', 'efEncSmsp', 'smspT', 'smspDcsHint', 'smspVpHint', 'smspDurationText', 'smspPluralIndex', 'smspCount', 'efSmspDaNumber', 'efDecServiceTable', 'efDecSst', 'efDecAcc', 'efDecPhase', 'efDecHpplmn', 'efDecAd', 'efDecCbmi', 'efDecCbmir', 'efDecIccid', 'efDecImsi', 'efDecPlmnList', 'efDecFplmn', 'efEncServiceTable', 'efEncSst', 'efEncAcc', 'efPhaseValue', 'efPhaseModel', 'efEncPhase', 'efEncHpplmn', 'efAdOpValue', 'efAdModel', 'efEncAd', 'efEncPlmnOnly', 'efEncIccid', 'efEncImsi', 'efCbmiModel', 'efEncCbmi', 'efCbmirModel', 'efEncCbmir', 'efSmspAddressModel', 'efSmspModel', 'efSmspTonOptions', 'efSmspParamRow', 'efSmspHtml', 'efSmspCollect', 'pysimFsSmspToggle', 'pysimFsSmspHexInput', 'efSmspHexByte', 'efSmspAddressBytes', 'efSmspAddrUnchanged', 'efSmspByteUnchanged', 'efEncSmsp', 'smspT', 'smspDcsHint', 'smspVpHint', 'smspDurationText', 'smspPluralIndex', 'smspCount', 'efSmspDaNumber',
]; ];
let code = ''; let code = '';
for (const f of FNS) code += extractFunc(html, f) + '\n'; for (const f of FNS) code += extractFunc(html, f) + '\n';
for (const c of ['GSM7_ALPHABET', 'GSM7_EXT_MAP', 'EF_ACT_TECHS', 'EF_SPN_FIELDS', for (const c of ['GSM7_ALPHABET', 'GSM7_EXT_MAP', 'EF_ACT_TECHS', 'EF_SPN_FIELDS',
'EF_ADN_FIELDS', 'EF_PLMN_LIST', 'EF_PNN_ITEMS', 'EF_UST_SERVICES', 'EF_SST_SERVICES', 'EF_IST_SERVICES', 'EF_EST_SERVICES', 'EF_PHASE_NAMES', 'EF_ACC_CLASSES', 'EF_UST_FIELDS', 'EF_IST_FIELDS', 'EF_EST_FIELDS', 'EF_ACC_FIELDS', 'EF_PHASE_FIELDS', 'EF_HPPLMN_FIELDS', 'EF_AD_OP_MODES', 'EF_AD_FIELDS', 'EF_PLMN_ONLY_LIST', 'EF_ICCID_FIELDS', 'EF_IMSI_FIELDS', 'EF_CBMI_LIST', 'EF_CBMIR_LIST', 'EF_READONLY_NAMES', 'EF_SMSP_PARAMS', 'EF_SMSP_CUSTOM', 'EF_ENCODERS']) { 'EF_ADN_FIELDS', 'EF_PLMN_LIST', 'EF_PNN_ITEMS', 'EF_UST_SERVICES', 'EF_SST_SERVICES', 'EF_IST_SERVICES', 'EF_EST_SERVICES', 'EF_PHASE_NAMES', 'EF_ACC_CLASSES', 'EF_UST_FIELDS', 'EF_IST_FIELDS', 'EF_EST_FIELDS', 'EF_SST_FIELDS', 'EF_ACC_FIELDS', 'EF_PHASE_FIELDS', 'EF_HPPLMN_FIELDS', 'EF_AD_OP_MODES', 'EF_AD_FIELDS', 'EF_PLMN_ONLY_LIST', 'EF_ICCID_FIELDS', 'EF_IMSI_FIELDS', 'EF_CBMI_LIST', 'EF_CBMIR_LIST', 'EF_READONLY_NAMES', 'EF_SMSP_PARAMS', 'EF_SMSP_CUSTOM', 'EF_ENCODERS']) {
code += extractConst(html, c); code += extractConst(html, c);
} }
eval(code); eval(code);
@@ -233,12 +233,19 @@ test('the encoder resolver is DF-first: every EF.AD form shares one encoder', ()
// without a DF the historical name-first lookup answers (DF.GSM's entry) // without a DF the historical name-first lookup answers (DF.GSM's entry)
assert.strictEqual(efFindEncoder('EF.AD', '6fad'), sim); assert.strictEqual(efFindEncoder('EF.AD', '6fad'), sim);
assert.strictEqual(efFindEncoder('EF.AD', '6fad', 'ADF.NOPE'), sim); assert.strictEqual(efFindEncoder('EF.AD', '6fad', 'ADF.NOPE'), sim);
// EF.ECC stays read-only in either DF, EF.SST in the SIM layout, and the // EF.ECC stays read-only in either DF; the shared 6F38 FID resolves to
// shared 6F38 FID must not hand SST the UST encoder // EF.SST under DF.GSM and to EF.UST under ADF.USIM (the FID fallback
// without a DF keeps the historical UST answer - SST's entry is DF-scoped)
assert.strictEqual(efHasEncoder('EF.ECC', '6fb7', '7F20'), false); assert.strictEqual(efHasEncoder('EF.ECC', '6fb7', '7F20'), false);
assert.strictEqual(efHasEncoder('EF.ECC', '6fb7', 'ADF.USIM'), false); assert.strictEqual(efHasEncoder('EF.ECC', '6fb7', 'ADF.USIM'), false);
assert.strictEqual(efHasEncoder('EF.SST', '6f38', '7F20'), false); assert.strictEqual(efHasEncoder('EF.SST', '6f38', '7F20'), true);
assert.strictEqual(efHasEncoder('EF.UST', '6f38', 'ADF.USIM'), true); assert.strictEqual(efHasEncoder('EF.UST', '6f38', 'ADF.USIM'), true);
assert.strictEqual(efFindEncoder('EF.SST', '6f38', '7F20').enc, efEncSst);
assert.strictEqual(efFindEncoder('EF.UST', '6f38', 'ADF.USIM').enc, efFindEncoder('EF.UST', '6f38').enc);
assert.strictEqual(efFindEncoder(null, '6f38', '7F20').name, 'EF.SST');
assert.strictEqual(efFindEncoder(null, '6f38', 'ADF.USIM').name, 'EF.UST');
assert.strictEqual(efFindEncoder(null, '6f38').name, 'EF.UST');
assert.strictEqual(efFindEncoder(null, '6f38', 'ADF.USIM').enc !== efEncSst, true);
assert.strictEqual(efFindEncoder('EF.NOPE', 'abcd', 'ADF.USIM'), null); assert.strictEqual(efFindEncoder('EF.NOPE', 'abcd', 'ADF.USIM'), null);
// the SIM 3+X and USIM 4+X forms keep their length on an untouched save // the SIM 3+X and USIM 4+X forms keep their length on an untouched save
for (const hex of ['00' + '0000', '00' + '0000' + '02' + 'AABB', AD_HEX, 'FF'.repeat(6)]) { for (const hex of ['00' + '0000', '00' + '0000' + '02' + 'AABB', AD_HEX, 'FF'.repeat(6)]) {
@@ -626,10 +633,13 @@ test('service tables: encode/decode round-trips and unmodelled bits', () => {
const services = efDecServiceTable(efBytes(hex), table).services; const services = efDecServiceTable(efBytes(hex), table).services;
assert.strictEqual(entry.enc({ services: services }, hex), hex, name + ' ' + hex); assert.strictEqual(entry.enc({ services: services }, hex), hex, name + ' ' + hex);
} }
// EF.SST has no editor (its 2-bit SIM layout needs a dedicated UI) // EF.SST has its own 2-bit editor on the shared 6F38 FID (the DF-scoped
assert.strictEqual(efFindEncoder('EF.SST', '6f38'), null); // entry keeps the UST answer for a bare FID / ADF context)
assert.strictEqual(efHasEncoder('EF.SST', '6f38'), false); assert.strictEqual(efFindEncoder('EF.SST', '6f38').enc, efEncSst);
assert.strictEqual(efHasEncoder('EF.SST', '6f38'), true);
assert.strictEqual(efFindEncoder('EF.UST', '6f38').name, 'EF.UST'); assert.strictEqual(efFindEncoder('EF.UST', '6f38').name, 'EF.UST');
assert.strictEqual(efFindEncoder(null, '6f38').name, 'EF.UST');
assert.strictEqual(efFindEncoder(null, '6f38', '7F20').name, 'EF.SST');
// EST defines services 1-3; all-FF keeps the other bits too // EST defines services 1-3; all-FF keeps the other bits too
assert.deepStrictEqual(efDecServiceTable(efBytes('07'), EF_EST_SERVICES).services, assert.deepStrictEqual(efDecServiceTable(efBytes('07'), EF_EST_SERVICES).services,
[{ n: 1, name: 'Fixed Dialling Numbers (FDN)' }, [{ n: 1, name: 'Fixed Dialling Numbers (FDN)' },
@@ -659,6 +669,64 @@ test('service tables: unknown services and a too-short table are refused', () =>
assert.throws(() => efEncServiceTable({ services: [{ n: 0 }] }, '00', EF_UST_SERVICES), /Unknown service/); assert.throws(() => efEncServiceTable({ services: [{ n: 0 }] }, '00', EF_UST_SERVICES), /Unknown service/);
}); });
// EF.SST (TS 51.011 10.3.7): two bits per service, four per byte. 15 bytes =
// services 1-60; C7/1C/C1 carry on (11), off (10) and deleted (00) pairs and
// the last byte's bits 7-8 are service 60, above the 59-entry table.
const SST_MIX = 'C71CC1' + '00'.repeat(11) + 'C0';
test('EF.SST: the 2-bit encoder round-trips the decoded file', () => {
const dec = efDecSst(efBytes(SST_MIX));
assert.deepStrictEqual(dec.services.map(s => [s.n, s.allocated, s.activated]), [
[1, true, true], [2, true, false], [4, true, true], [6, true, true],
[7, true, false], [9, true, false], [12, true, true], [60, true, true],
]);
// a deleted (00) pair is omitted by the decoder, so decode->encode is exact
assert.strictEqual(efEncSst(dec, SST_MIX), SST_MIX);
// every state on: all-FF, unmodelled service 60 included
assert.strictEqual(efEncSst(efDecSst(efBytes('FF'.repeat(15))), 'FF'.repeat(15)), 'FF'.repeat(15));
// no service allocated at all
assert.strictEqual(efEncSst(efDecSst(efBytes('00'.repeat(15))), '00'.repeat(15)), '00'.repeat(15));
// clearing every modelled service keeps service 60's pair (byte 14 C0)
assert.strictEqual(efEncSst({ services: [] }, SST_MIX), '000000' + '00'.repeat(11) + 'C0');
// the file length is kept
assert.strictEqual(efEncSst(efDecSst(efBytes('0C')), '0C'), '0C');
assert.strictEqual(efEncSst(efDecSst(efBytes('0C00')), '0C00'), '0C00');
});
test('EF.SST: deleted clears the pair, off sets 10 and on sets 11', () => {
const on = n => ({ n: n, allocated: true, activated: true });
const off = n => ({ n: n, allocated: true, activated: false });
const del = n => ({ n: n, allocated: false, activated: false });
// service 2 (byte 0, pair 2)
assert.strictEqual(efEncSst({ services: [on(2)] }, '00'), '0C');
assert.strictEqual(efEncSst({ services: [off(2)] }, '00'), '04');
assert.strictEqual(efEncSst({ services: [del(2)] }, '0C'), '00');
// every modelled pair is rebuilt from the list, the rest of the file stays
assert.strictEqual(efEncSst({ services: [on(2)] }, 'FF'.repeat(15)),
'0C' + '00'.repeat(13) + 'C0');
assert.strictEqual(efEncSst({ services: [on(2), off(4)] }, SST_MIX).slice(0, 2), '4C');
// a pair above the table is written like any other (a longer file)
const long = '00'.repeat(16);
assert.strictEqual(efEncSst({ services: [on(62)] }, long), '00'.repeat(15) + '0C');
// the decoder's illegal '08' pair (activated, not allocated) is refused
assert.deepStrictEqual(efDecSst(efBytes('08')), { services: [
{ n: 2, name: 'Abbreviated Dialling Numbers (ADN)', allocated: false, activated: true },
] });
assert.throws(() => efEncSst(efDecSst(efBytes('08')), '08'), /must be deleted, off or on/);
});
test('EF.SST: unknown services, an invalid state and a too-short file are refused', () => {
assert.throws(() => efEncSst({ services: [{ n: 5, allocated: true, activated: true }] }, '00'),
/too short for service/);
assert.throws(() => efEncSst({ services: [{ n: 0, allocated: true, activated: true }] }, '00'),
/Unknown service/);
assert.throws(() => efEncSst({ services: [{ n: 'x' }] }, '00'), /Unknown service/);
assert.throws(() => efEncSst({ services: [{ n: 2, allocated: false, activated: true }] }, '00'),
/must be deleted, off or on/);
// a bare service number carries no state
assert.throws(() => efEncSst({ services: [2] }, '00'), /must be deleted, off or on/);
});
test('F-ACC: the class numbering and the ACC10/encoder rules', () => { test('F-ACC: the class numbering and the ACC10/encoder rules', () => {
assert.deepStrictEqual(efDecAcc(efBytes('8000')), { access_classes: [15] }); assert.deepStrictEqual(efDecAcc(efBytes('8000')), { access_classes: [15] });
assert.deepStrictEqual(efDecAcc(efBytes('0001')), { access_classes: [0] }); assert.deepStrictEqual(efDecAcc(efBytes('0001')), { access_classes: [0] });
+95 -8
View File
@@ -51,19 +51,19 @@ const FNS = [
'efDecRowHtml', 'efDecGridHtml', 'efDecItemRowHtml', 'efDecItemsHtml', 'efDecRowHtml', 'efDecGridHtml', 'efDecItemRowHtml', 'efDecItemsHtml',
'efDecRecordsHtml', 'efDecEditHtml', 'efDecRecordsHtml', 'efDecEditHtml',
'efDecCollectValues', 'efDecCollectRows', 'efDecCollectItems', 'efDecCollectValues', 'efDecCollectRows', 'efDecCollectItems',
'efBitsSelected', 'pysimFsDecBitsFilter', 'pysimFsDecRowCheck', 'pysimFsDecItemTag', 'pysimFsDecAddItem', 'efBitsSelected', 'pysimFsDecBitsFilter', 'pysimFsDecBits2Filter', 'pysimFsDecRowCheck', 'pysimFsDecItemTag', 'pysimFsDecAddItem',
'pysimFsDecRemoveItem', 'pysimFsFindNodeByPath', 'pysimCustomNormPath', 'pysimFsNodePath', 'pysimFsFallbackNode', 'efFidFromPath', 'efDfFromPath', 'pysimFsDecRemoveItem', 'pysimFsFindNodeByPath', 'pysimCustomNormPath', 'pysimFsNodePath', 'pysimFsFallbackNode', 'efFidFromPath', 'efDfFromPath',
'pysimFsHasDecoder', 'pysimFsHasEncoder', 'pysimFsDecSave', 'pysimFsDecEdit', 'pysimFsHasDecoder', 'pysimFsHasEncoder', 'pysimFsDecSave', 'pysimFsDecEdit',
'pysimFsPillsEnabled', 'pysimFsSyncContentVisibility', 'gsm7Decode', 'pysimFsPillsEnabled', 'pysimFsSyncContentVisibility', 'gsm7Decode',
'gsm7TextToSeptets', 'gsm7Encode', 'gsm7TextToSeptets', 'gsm7Encode',
'efDecServiceTable', 'efDecSst', 'efDecAcc', 'efDecPhase', 'efDecHpplmn', 'efDecAd', 'efDecCbmi', 'efDecCbmir', 'efDecIccid', 'efDecImsi', 'efDecPlmnList', 'efDecFplmn', 'efEncServiceTable', 'efEncAcc', 'efPhaseValue', 'efPhaseModel', 'efEncPhase', 'efEncHpplmn', 'efAdOpValue', 'efAdModel', 'efEncAd', 'efEncPlmnOnly', 'efEncIccid', 'efEncImsi', 'efCbmiModel', 'efEncCbmi', 'efCbmirModel', 'efEncCbmir', 'efSmspAddressModel', 'efSmspModel', 'efSmspTonOptions', 'efSmspParamRow', 'efSmspHtml', 'efSmspCollect', 'pysimFsSmspToggle', 'pysimFsSmspHexInput', 'efSmspHexByte', 'efSmspAddressBytes', 'efSmspAddrUnchanged', 'efSmspByteUnchanged', 'efEncSmsp', 'smspT', 'smspDcsHint', 'smspVpHint', 'smspDurationText', 'smspPluralIndex', 'smspCount', 'efSmspDaNumber', 'efDecServiceTable', 'efDecSst', 'efDecAcc', 'efDecPhase', 'efDecHpplmn', 'efDecAd', 'efDecCbmi', 'efDecCbmir', 'efDecIccid', 'efDecImsi', 'efDecPlmnList', 'efDecFplmn', 'efEncServiceTable', 'efEncSst', 'efEncAcc', 'efPhaseValue', 'efPhaseModel', 'efEncPhase', 'efEncHpplmn', 'efAdOpValue', 'efAdModel', 'efEncAd', 'efEncPlmnOnly', 'efEncIccid', 'efEncImsi', 'efCbmiModel', 'efEncCbmi', 'efCbmirModel', 'efEncCbmir', 'efSmspAddressModel', 'efSmspModel', 'efSmspTonOptions', 'efSmspParamRow', 'efSmspHtml', 'efSmspCollect', 'pysimFsSmspToggle', 'pysimFsSmspHexInput', 'efSmspHexByte', 'efSmspAddressBytes', 'efSmspAddrUnchanged', 'efSmspByteUnchanged', 'efEncSmsp', 'smspT', 'smspDcsHint', 'smspVpHint', 'smspDurationText', 'smspPluralIndex', 'smspCount', 'efSmspDaNumber',
]; ];
let code = 'var pysimFsSelected = null;\nvar pysimFsTreeRoot = null;\nvar pysimFsDecModel = null;\n' let code = 'var pysimFsSelected = null;\nvar pysimFsTreeRoot = null;\nvar pysimFsDecModel = null;\n'
+ 'var pysimFsDecEditMode = false;\nvar pysimFsEditMode = false;\n'; + 'var pysimFsDecEditMode = false;\nvar pysimFsEditMode = false;\n';
for (const f of FNS) code += extractFunc(html, f) + '\n'; for (const f of FNS) code += extractFunc(html, f) + '\n';
for (const c of ['GSM7_ALPHABET', 'GSM7_EXT_MAP', 'EF_ACT_TECHS', 'EF_SPN_FIELDS', for (const c of ['GSM7_ALPHABET', 'GSM7_EXT_MAP', 'EF_ACT_TECHS', 'EF_SPN_FIELDS',
'EF_ADN_FIELDS', 'EF_PLMN_LIST', 'EF_PNN_ITEMS', 'EF_UST_SERVICES', 'EF_SST_SERVICES', 'EF_IST_SERVICES', 'EF_EST_SERVICES', 'EF_PHASE_NAMES', 'EF_ACC_CLASSES', 'EF_UST_FIELDS', 'EF_IST_FIELDS', 'EF_EST_FIELDS', 'EF_ACC_FIELDS', 'EF_PHASE_FIELDS', 'EF_HPPLMN_FIELDS', 'EF_AD_OP_MODES', 'EF_AD_FIELDS', 'EF_PLMN_ONLY_LIST', 'EF_ICCID_FIELDS', 'EF_IMSI_FIELDS', 'EF_CBMI_LIST', 'EF_CBMIR_LIST', 'EF_READONLY_NAMES', 'EF_SMSP_PARAMS', 'EF_SMSP_CUSTOM', 'EF_ENCODERS', 'EF_ADF_RIDS']) { 'EF_ADN_FIELDS', 'EF_PLMN_LIST', 'EF_PNN_ITEMS', 'EF_UST_SERVICES', 'EF_SST_SERVICES', 'EF_IST_SERVICES', 'EF_EST_SERVICES', 'EF_PHASE_NAMES', 'EF_ACC_CLASSES', 'EF_UST_FIELDS', 'EF_IST_FIELDS', 'EF_EST_FIELDS', 'EF_SST_FIELDS', 'EF_ACC_FIELDS', 'EF_PHASE_FIELDS', 'EF_HPPLMN_FIELDS', 'EF_AD_OP_MODES', 'EF_AD_FIELDS', 'EF_PLMN_ONLY_LIST', 'EF_ICCID_FIELDS', 'EF_IMSI_FIELDS', 'EF_CBMI_LIST', 'EF_CBMIR_LIST', 'EF_READONLY_NAMES', 'EF_SMSP_PARAMS', 'EF_SMSP_CUSTOM', 'EF_ENCODERS', 'EF_ADF_RIDS']) {
code += extractConst(html, c); code += extractConst(html, c);
} }
eval(code); eval(code);
@@ -271,12 +271,14 @@ test('pysimFsHasEncoder resolves the selected file by its path or FID', () => {
assert.strictEqual(pysimFsHasEncoder(), true); assert.strictEqual(pysimFsHasEncoder(), true);
pysimFsSelected = 'MF/6F7E'; pysimFsSelected = 'MF/6F7E';
assert.strictEqual(pysimFsHasEncoder(), false); assert.strictEqual(pysimFsHasEncoder(), false);
// the SIM service table shares 6F38 with UST but has no editor // the SIM service table shares 6F38 with UST but has its own 2-bit editor
pysimFsTreeRoot = { name: 'MF', children: [] }; pysimFsTreeRoot = { name: 'MF', children: [] };
const sst = { name: 'EF.SST', fid: '6f38', parent: pysimFsTreeRoot }; const gsm = { name: 'DF.GSM', fid: '7F20', parent: pysimFsTreeRoot, children: [] };
pysimFsTreeRoot.children.push(sst); pysimFsTreeRoot.children.push(gsm);
pysimFsSelected = 'MF/6F38'; const sst = { name: 'EF.SST', fid: '6f38', parent: gsm };
assert.strictEqual(pysimFsHasEncoder(), false); gsm.children.push(sst);
pysimFsSelected = 'MF/7F20/6F38';
assert.strictEqual(pysimFsHasEncoder(), true);
pysimFsSelected = null; pysimFsSelected = null;
assert.strictEqual(pysimFsHasEncoder(), false); assert.strictEqual(pysimFsHasEncoder(), false);
}); });
@@ -749,6 +751,91 @@ test('the bits filter hides the non-matching rows', () => {
assert.strictEqual(rows[1].style.display, ''); assert.strictEqual(rows[1].style.display, '');
}); });
test('the EF.SST grid renders one tri-state select per service', () => {
// 4C: service 2 on (11), service 4 off (10), the rest of the pairs deleted
const model = efDecEditModel('EF.SST', '6f38', { kind: 'transparent', data: '4C' }, '7F20');
assert.deepStrictEqual(model.values.services, [
{ n: 2, name: 'Abbreviated Dialling Numbers (ADN)', allocated: true, activated: true },
{ n: 4, name: 'Short Message Storage (SMS)', allocated: true, activated: false },
]);
const out = efDecEditHtml(model, '4C');
assert.match(out, /class="pysim-dec-bits /);
assert.match(out, /pysim-dec-bits-filter/);
assert.match(out, /data-bit2="2" data-bit2-name="Abbreviated Dialling Numbers \(ADN\)"/);
// the whole SST table is rendered (59 services, RFU 8/20/50 included)
assert.strictEqual((out.match(/data-bit2="/g) || []).length, 59);
assert.match(out, /data-bit2="8" data-bit2-name="RFU"/);
assert.match(out, /data-bit2="20" data-bit2-name="RFU"/);
assert.match(out, /data-bit2="50" data-bit2-name="Reserved and shall be ignored"/);
// each select is preselected from the decoded state, an absent pair is deleted
const sel = n => {
const m = new RegExp('<select data-bit2="' + n + '"[^>]*>([\\s\\S]*?)</select>').exec(out);
assert.ok(m, 'select for service ' + n);
return m[1];
};
assert.match(sel(2), /<option value="on" selected>On<\/option>/);
assert.match(sel(4), /<option value="off" selected>Off<\/option>/);
assert.match(sel(1), /<option value="deleted" selected>Deleted<\/option>/);
assert.match(sel(59), /<option value="deleted" selected>Deleted<\/option>/);
for (const n of [2, 4, 1, 59]) {
assert.match(sel(n), />Deleted<\/option>/);
assert.match(sel(n), />Off<\/option>/);
assert.match(sel(n), />On<\/option>/);
}
// an all-FF file preselects every modelled service 'on'
const ff = efDecEditHtml(efDecEditModel('EF.SST', '6f38',
{ kind: 'transparent', data: 'FF'.repeat(15) }, '7F20'), 'FF'.repeat(15));
assert.strictEqual((ff.match(/<option value="on" selected>/g) || []).length, 59);
assert.ok(!/<option value="(deleted|off)" selected>/.test(ff));
});
test('the EF.SST collector yields {n, name, allocated, activated}', () => {
const row = (n, name, state) => ({
value: state,
getAttribute: k => k === 'data-bit2' ? String(n) : name,
});
const root = {
querySelectorAll: sel => sel === '[data-bit2]' ? [
row(2, 'Abbreviated Dialling Numbers (ADN)', 'on'),
row(1, 'CHV1 disable function', 'deleted'),
row(4, 'Short Message Storage (SMS)', 'off'),
] : [],
};
assert.deepStrictEqual(efDecCollectValues(root, EF_SST_FIELDS), { services: [
{ n: 2, name: 'Abbreviated Dialling Numbers (ADN)', allocated: true, activated: true },
{ n: 1, name: 'CHV1 disable function', allocated: false, activated: false },
{ n: 4, name: 'Short Message Storage (SMS)', allocated: true, activated: false },
] });
// an untouched form re-encodes to the original bytes
const dec = efDecSst(efBytes('4C'));
const values = efDecCollectValues({
querySelectorAll: () => [2, 4].map(n => row(n, EF_SST_SERVICES[n],
dec.services.find(s => s.n === n).activated ? 'on' : 'off')),
}, EF_SST_FIELDS);
assert.strictEqual(efFindEncoder('EF.SST', '6f38', '7F20').enc(values, '4C'), '4C');
// the EF.SST field is the tri-state kind, the 1-bit tables are untouched
assert.strictEqual(EF_SST_FIELDS[0].kind, 'bits2');
assert.strictEqual(EF_UST_FIELDS[0].kind, 'bits');
assert.strictEqual(EF_EST_FIELDS[0].kind, 'bits');
});
test('the tri-state grid filter matches the row label, not the select options', () => {
const rows = [
{ getAttribute: k => k === 'data-label' ? '1 CHV1 disable function' : '', style: {} },
{ getAttribute: k => k === 'data-label' ? '4 Short Message Storage (SMS)' : '', style: {} },
];
const box = { querySelectorAll: sel => sel === '.pysim-dec-bit2-row' ? rows : [] };
const input = { value: 'sms', closest: sel => sel === '.pysim-dec-bits' ? box : null };
pysimFsDecBits2Filter(input);
assert.strictEqual(rows[0].style.display, 'none');
assert.strictEqual(rows[1].style.display, '');
// 'off' is a state option of every row, never part of a service name
pysimFsDecBits2Filter({ value: 'off', closest: () => box });
assert.strictEqual(rows[0].style.display, 'none');
assert.strictEqual(rows[1].style.display, 'none');
pysimFsDecBits2Filter(null);
});
test('the select keeps an unknown current value as an option', () => { test('the select keeps an unknown current value as an option', () => {
const model = efDecEditModel('EF.PHASE', '6fae', { kind: 'transparent', data: '0F' }); const model = efDecEditModel('EF.PHASE', '6fae', { kind: 'transparent', data: '0F' });
assert.deepStrictEqual(model.values, { phase_hex: '0F' }); assert.deepStrictEqual(model.values, { phase_hex: '0F' });