diff --git a/docs/decoded-editing.md b/docs/decoded-editing.md
index a72e8a2..68ff209 100644
--- a/docs/decoded-editing.md
+++ b/docs/decoded-editing.md
@@ -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.IST | 6F07 | TS 31.102 4.2.2 (SAIP) | 1–2 B | service bits | S6 | **3.34** |
| EF.UST | 6F38 | TS 31.102 4.2.8 | X ≥ 1 | service bits | S6 | **3.34** |
-| EF.SST | 6F38 | TS 51.011 10.3.7 | X ≥ 1 | **2 bits/service** (allocated + activated) | S6 (pending) | decoder fixed; editor later |
+| EF.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.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 |
@@ -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
bit of the pair = allocated, second = activated. The shared 1-bit
UST/EST codec was used for it, so SIM service tables were both misread and
- miswritten; the decoder is fixed in v3.34.0, editing waits for an
- allocated/activated UI.
+ miswritten; the decoder is fixed in v3.34.0, and the tri-state editor
+ (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
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.
@@ -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
(PLMNsel/FPLMN/EHPLMN), ICCID/IMSI (behind a confirmation) and
CBMI/CBMID/CBMIR. Decoder fixes: ACC class numbering, the AD operation
- mode / MNC-length rules, the SST 2-bit layout (read-only for now) and the
+ mode / MNC-length rules, the SST 2-bit layout and the
IMSI parity nibble.
- **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
@@ -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
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
-allocated/activated layout no longer needs its own per-file grid (SST stays
-read-only in the per-file editor).
+service tables side by side and writes them back consistently. EF.SST also
+has its own per-file tri-state editor (deleted / allocated-off / on); the
+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
(ADF.USIM, 1 bit), EF.EST (ADF.USIM, a 3-bit *enabled* subset of UST:
diff --git a/frontend/help-ru.html b/frontend/help-ru.html
index 7144b09..faa58a9 100644
--- a/frontend/help-ru.html
+++ b/frontend/help-ru.html
@@ -409,7 +409,7 @@
Читать сырые данные / Декодировать — чтение выбранного файла в виде hex-дампа или таблицы декодированных полей (клиентские декодеры EF: IMSI, ICCID, SPN, списки PLMN, LOCI/PSLOCI/EPSLOCI, ADN/MSISDN, таблицы сервисов, SUME, …); таблица декодируется в браузере из тех же сырых байт (без серверного декодирования). Подсвеченная кнопка — текущий вид; нажатие любой из них (пере)читывает файл. «Декодировать» недоступна, если для файла нет клиентского декодера (проверка по имени/FID до чтения).
Редактировать сырые данные — изменение hex-данных, Сохранить для записи (или Отмена); transparent-файлы редактируются в textarea, record-файлы — по одному полю на запись с флажками выбора. «Редактировать сырые данные» сначала переключает на hex-вид и при необходимости читает файл.
-
Редактировать декодированные — редактирование файла в его собственной декодированной форме; кодирование выполняется в браузере, запись идёт обычным путём обновления. Кодировщики покрывают 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; у каждого параметра — флажок «отсутствует (действует значение ME по умолчанию)» и живые подсказки по DCS и сроку действия). Для transparent-файла — одна форма (поля SPN, строки PLMN с добавлением/удалением, элементы PNN с добавлением/удалением); для record-файла — по форме на запись с флажком выбора, который ставится автоматически при изменении поля, поэтому записываются только изменённые записи. Сохранить проверяет все выбранные записи до записи; для файлов без кодировщика кнопка недоступна с подсказкой. Указатели расширений (EF.EXT1–EF.EXT5) и байт метода сравнения BDN сохраняются; редактирование номера, продолжение которого лежит в файле расширения, отклоняется.
+
Редактировать декодированные — редактирование файла в его собственной декодированной форме; кодирование выполняется в браузере, запись идёт обычным путём обновления. Кодировщики покрывают EF.SPN, EF.PNN, списки PLMN (PLMNsel / FPLMN / EHPLMN и списки с технологиями доступа PLMNwAcT / OPLMNwAcT / HPLMNwAcT), файлы номеров (ADN / FDN / SDN / MSISDN / LND / BDN), таблицы сервисов (UST / IST / EST и SST — SIM-таблица, редактируется по каждой службе как удалена / выделена-выключена / включена), 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; у каждого параметра — флажок «отсутствует (действует значение ME по умолчанию)» и живые подсказки по DCS и сроку действия). Для transparent-файла — одна форма (поля SPN, строки PLMN с добавлением/удалением, элементы PNN с добавлением/удалением); для record-файла — по форме на запись с флажком выбора, который ставится автоматически при изменении поля, поэтому записываются только изменённые записи. Сохранить проверяет все выбранные записи до записи; для файлов без кодировщика кнопка недоступна с подсказкой. Указатели расширений (EF.EXT1–EF.EXT5) и байт метода сравнения BDN сохраняются; редактирование номера, продолжение которого лежит в файле расширения, отклоняется.
Проверить все файлы — обход всего дерева (включая пользовательские файлы) с пометкой каждого элемента: есть (обычный вид) или нет (красный ✗, без стрелки разворачивания); существующие пустые DF показывают (пусто). Отображается прогресс N / всего, обход можно остановить; в конце — сводка «есть/нет». Файлы проверяются только при разворачивании или проверке — просмотр остаётся ленивым.
ADM — файлы, требующие администраторский PIN, возвращают 6982/9804; если в подходящем пресете карты (тот же ICCID) есть ключ ADM, рядом с ошибкой появляется кнопка «Проверить ADM», а значок в заголовке (ADM ✓/✗ ⚿) становится кликабельным. Каждый неверный ключ расходует попытку: остаток показывается, а повторная попытка требует подтверждения. Заблокированный ADM здесь не восстановить — нужен ключ разблокировки карты.
Read raw / Read decoded — 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. Read decoded is disabled when the client-side decoders do not cover the file (name/FID check, before reading).
Edit raw — edit the raw hex data, 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 the file through its own decoded form, encoded in the browser and written with the normal update path. The encoders cover EF.SPN, EF.PNN, the PLMN lists (PLMNsel / FPLMN / EHPLMN and the access-technology lists 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 — writing them changes the card identity) the cell-broadcast lists (CBMI / CBMID / CBMIR) and EF.SMSP (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 absent (the ME default applies) 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. Save 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.
+
Edit decoded — edit the file through its own decoded form, encoded in the browser and written with the normal update path. The encoders cover EF.SPN, EF.PNN, the PLMN lists (PLMNsel / FPLMN / EHPLMN and the access-technology lists PLMNwAcT / OPLMNwAcT / HPLMNwAcT), the dialling-number files (ADN / FDN / SDN / MSISDN / LND / BDN), the service tables (UST / IST / EST and SST — the SIM table, edited per service as deleted / allocated-off / on), ACC, PHASE, HPPLMN, AD, ICCID / IMSI (behind a confirmation — writing them changes the card identity) the cell-broadcast lists (CBMI / CBMID / CBMIR) and EF.SMSP (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 absent (the ME default applies) 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. Save 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.
Probe all files — walks the whole tree (including custom files) and marks every entry present (normal) or absent (red ✗, no expand arrow); empty-but-present DFs show (empty). Shows progress N / total, can be stopped, and finishes with a present/absent summary. Files are only verified when expanded or probed — browsing stays lazy.
ADM — files that need the administrator PIN fail with 6982/9804; if 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 cannot be recovered here — it needs the card’s unblock key.
diff --git a/frontend/index.html b/frontend/index.html
index 4b0e0a7..043649b 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -23988,7 +23988,7 @@ const EF_DECODERS = [
{ name: 'EF.IMSI', fid: '6f07', fn: efDecImsi },
{ name: 'EF.IST', fid: '6f07', fn: b => efDecServiceTable(b, EF_IST_SERVICES) },
{ name: 'EF.UST', fid: '6f38', fn: b => efDecServiceTable(b, EF_UST_SERVICES) },
- { name: 'EF.SST', fid: '6f38', fn: efDecSst },
+ { name: 'EF.SST', df: '7F20', fid: '6f38', fn: efDecSst },
{ name: 'EF.EST', fid: '6f56', fn: b => efDecServiceTable(b, EF_EST_SERVICES) },
{ name: 'EF.HPPLMN', fid: '6f31', fn: efDecHpplmn },
{ name: 'EF.LI', fid: '6f05', fn: efDecLi },
@@ -24467,6 +24467,51 @@ function efEncServiceTable(values, origHex, table) {
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
// the MSB); ACC 10 is not used and is written as 0.
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_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 }];
+// 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 }];
@@ -25186,6 +25238,7 @@ const EF_ENCODERS = [
confirm: 'Writing the IMSI changes the card identity and may break network registration. Continue?' },
{ name: 'EF.IST', fid: '6f07', enc: (v, o) => efEncServiceTable(v, o, EF_IST_SERVICES), fields: EF_IST_FIELDS },
{ name: 'EF.UST', fid: '6f38', enc: (v, o) => efEncServiceTable(v, o, EF_UST_SERVICES), fields: EF_UST_FIELDS },
+ { name: 'EF.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.ACC', fid: '6f78', enc: efEncAcc, fields: EF_ACC_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
-// must not pick the wrong layout (EF.SST is not EF.UST on 6F38 - its 2-bit
-// SIM layout has no editor yet).
-const EF_READONLY_NAMES = ['EF.SST'];
+// must not pick the wrong layout. (The list is empty now - EF.SST got its
+// own 2-bit editor on 6F38 - but the guard stays for a future decoder-only
+// collision.)
+const EF_READONLY_NAMES = [];
function efFindEncoder(name, fid, df) {
let n = (name || '').toUpperCase();
@@ -25500,6 +25554,31 @@ function efDecFieldHtml(f, value) {
'' + n + '' +
'' + esc(t(name)) + '';
}).join('') + '';
+ } 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 = '