From fc27feba8e4ecf8c1d608ef83cbf2ebdc449d746 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BD=D1=82=D0=BE=D0=BD=20=D0=A2=D1=80=D0=BE=D1=88?= =?UTF-8?q?=D0=B8=D0=BD?= Date: Sun, 25 Oct 2026 21:43:07 +0300 Subject: [PATCH] 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.) --- docs/decoded-editing.md | 15 ++-- frontend/help-ru.html | 2 +- frontend/help.html | 2 +- frontend/index.html | 120 +++++++++++++++++++++++++++-- frontend/tests/ef_encode.test.js | 84 ++++++++++++++++++-- frontend/tests/fs_dec_edit.test.js | 103 +++++++++++++++++++++++-- 6 files changed, 296 insertions(+), 30 deletions(-) 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 @@ diff --git a/frontend/help.html b/frontend/help.html index 46d04c7..636c0a8 100644 --- a/frontend/help.html +++ b/frontend/help.html @@ -408,7 +408,7 @@ 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 = '
' + + '' + + '
' + + nums.map(n => { + const name = String(f.items[n]); + const cur = states[String(n)] || 'deleted'; + return ''; + }).join('') + '
'; } else if (f.kind === 'select') { const cur = String(value === undefined || value === null ? '' : value); 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">'; } const body = '' + esc(t(f.label)) + '' + input; - const wide = f.kind === 'techs' || f.kind === 'bits'; + const wide = f.kind === 'techs' || f.kind === 'bits' || f.kind === 'bits2'; if (!f.hint) { return wide ? '
' + body + '
' @@ -25634,6 +25713,20 @@ function efDecCollectValues(root, fields) { name: cb.getAttribute('data-bit-name') || '' }); }); 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 { const el = root.querySelector('[data-key="' + f.key + '"]'); 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() { if (!pysimFsSelected) return; const node = pysimFsFindNodeByPath(pysimFsSelected); @@ -26424,6 +26530,10 @@ const LANG_RU = { 'The number continues in EF.EXT5 - editing it is not supported': 'Номер продолжается в EF.EXT5 — редактирование не поддерживается', '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': 'Классы доступа', 'Phase': 'Фаза', 'Interval': 'Интервал', diff --git a/frontend/tests/ef_encode.test.js b/frontend/tests/ef_encode.test.js index 683c037..71d0673 100644 --- a/frontend/tests/ef_encode.test.js +++ b/frontend/tests/ef_encode.test.js @@ -51,13 +51,13 @@ const FNS = [ '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', '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 = ''; 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_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); } 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) assert.strictEqual(efFindEncoder('EF.AD', '6fad'), 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 - // shared 6F38 FID must not hand SST the UST encoder + // EF.ECC stays read-only in either DF; the shared 6F38 FID resolves to + // 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', '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(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); // 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)]) { @@ -626,10 +633,13 @@ test('service tables: encode/decode round-trips and unmodelled bits', () => { const services = efDecServiceTable(efBytes(hex), table).services; assert.strictEqual(entry.enc({ services: services }, hex), hex, name + ' ' + hex); } - // EF.SST has no editor (its 2-bit SIM layout needs a dedicated UI) - assert.strictEqual(efFindEncoder('EF.SST', '6f38'), null); - assert.strictEqual(efHasEncoder('EF.SST', '6f38'), false); + // EF.SST has its own 2-bit editor on the shared 6F38 FID (the DF-scoped + // entry keeps the UST answer for a bare FID / ADF context) + 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(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 assert.deepStrictEqual(efDecServiceTable(efBytes('07'), EF_EST_SERVICES).services, [{ 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/); }); +// 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', () => { assert.deepStrictEqual(efDecAcc(efBytes('8000')), { access_classes: [15] }); assert.deepStrictEqual(efDecAcc(efBytes('0001')), { access_classes: [0] }); diff --git a/frontend/tests/fs_dec_edit.test.js b/frontend/tests/fs_dec_edit.test.js index 1063768..9ddf43a 100644 --- a/frontend/tests/fs_dec_edit.test.js +++ b/frontend/tests/fs_dec_edit.test.js @@ -51,19 +51,19 @@ const FNS = [ 'efDecRowHtml', 'efDecGridHtml', 'efDecItemRowHtml', 'efDecItemsHtml', 'efDecRecordsHtml', 'efDecEditHtml', 'efDecCollectValues', 'efDecCollectRows', 'efDecCollectItems', - 'efBitsSelected', 'pysimFsDecBitsFilter', 'pysimFsDecRowCheck', 'pysimFsDecItemTag', 'pysimFsDecAddItem', + 'efBitsSelected', 'pysimFsDecBitsFilter', 'pysimFsDecBits2Filter', 'pysimFsDecRowCheck', 'pysimFsDecItemTag', 'pysimFsDecAddItem', 'pysimFsDecRemoveItem', 'pysimFsFindNodeByPath', 'pysimCustomNormPath', 'pysimFsNodePath', 'pysimFsFallbackNode', 'efFidFromPath', 'efDfFromPath', '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', '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' + '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_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); } eval(code); @@ -271,12 +271,14 @@ test('pysimFsHasEncoder resolves the selected file by its path or FID', () => { assert.strictEqual(pysimFsHasEncoder(), true); pysimFsSelected = 'MF/6F7E'; 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: [] }; - const sst = { name: 'EF.SST', fid: '6f38', parent: pysimFsTreeRoot }; - pysimFsTreeRoot.children.push(sst); - pysimFsSelected = 'MF/6F38'; - assert.strictEqual(pysimFsHasEncoder(), false); + const gsm = { name: 'DF.GSM', fid: '7F20', parent: pysimFsTreeRoot, children: [] }; + pysimFsTreeRoot.children.push(gsm); + const sst = { name: 'EF.SST', fid: '6f38', parent: gsm }; + gsm.children.push(sst); + pysimFsSelected = 'MF/7F20/6F38'; + assert.strictEqual(pysimFsHasEncoder(), true); pysimFsSelected = null; assert.strictEqual(pysimFsHasEncoder(), false); }); @@ -749,6 +751,91 @@ test('the bits filter hides the non-matching rows', () => { 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('').exec(out); + assert.ok(m, 'select for service ' + n); + return m[1]; + }; + assert.match(sel(2), /