v3.34.0: Edit decoded wave 2 — service tables, scalars, PLMN lists, BCD, CB lists
New client-side editors: the service tables EF.UST/IST/EST (a filterable bit grid; unmodelled bits and the file length preserved), EF.ACC, EF.PHASE, EF.HPPLMN, EF.AD (op-mode enum, additional info, MNC length incl. 0 and the 3-byte SIM form), the PLMN-only lists EF.PLMNsel/FPLMN/EHPLMN, EF.ICCID/EF.IMSI behind a confirmation, and the cell-broadcast lists EF.CBMI/CBMID/CBMIR. Framework: a bits form kind, a per-entry model(dec) hook, a per-entry confirm prompt and hex fields. Decoder fixes: EF.ACC class numbering (byte1 b8..b1 = 15..8, byte2 = 7..0), EF.AD operation mode (whole byte) and its MNC length rules, EF.SST decoded with the SIM 2-bit allocated/activated layout (editing stays disabled until that UI exists), and the EF.IMSI parity nibble (9 odd / 1 even). A no-edit save of the new list files no longer writes all-FF (the collected rows and the encoders disagreed on the value shape). Decoded editing for EF.DIR/EF.ARR/EF.SMS/EF.PBR is postponed by decision (read-only viewing is enough); the survey records it. 1022 Python + 1033 frontend green; simple-v467.
This commit is contained in:
@@ -409,7 +409,7 @@
|
||||
<ul class="list-disc list-inside text-sm space-y-1 mb-3">
|
||||
<li><strong>Читать сырые данные / Декодировать</strong> — чтение выбранного файла в виде hex-дампа или таблицы декодированных полей (клиентские декодеры EF: IMSI, ICCID, SPN, списки PLMN, LOCI/PSLOCI/EPSLOCI, ADN/MSISDN, таблицы сервисов, SUME, …); таблица декодируется в браузере из тех же сырых байт (без серверного декодирования). Подсвеченная кнопка — текущий вид; нажатие любой из них (пере)читывает файл. «Декодировать» недоступна, если для файла нет клиентского декодера (проверка по имени/FID до чтения).</li>
|
||||
<li><strong>Редактировать сырые данные</strong> — изменение hex-данных, <strong>Сохранить</strong> для записи (или <strong>Отмена</strong>); transparent-файлы редактируются в textarea, record-файлы — по одному полю на запись с флажками выбора. «Редактировать сырые данные» сначала переключает на hex-вид и при необходимости читает файл.</li>
|
||||
<li><strong>Редактировать декодированные</strong> — редактирование файла в его собственной декодированной форме; кодирование выполняется в браузере, запись идёт обычным путём обновления. Кодировщики покрывают <strong>EF.SPN</strong>, списки PLMN с технологиями доступа (<strong>PLMNwAcT / OPLMNwAcT / HPLMNwAcT</strong>), файлы номеров (<strong>ADN / FDN / SDN / MSISDN / LND / BDN</strong>) и <strong>EF.PNN</strong>. Для transparent-файла — одна форма (поля SPN, строки PLMN с добавлением/удалением, элементы PNN с добавлением/удалением); для record-файла — по форме на запись с флажком выбора, который ставится автоматически при изменении поля, поэтому записываются только изменённые записи. <strong>Сохранить</strong> проверяет все выбранные записи до записи; для файлов без кодировщика кнопка недоступна с подсказкой. Указатели расширений (EF.EXT1–EF.EXT5) и байт метода сравнения BDN сохраняются; редактирование номера, продолжение которого лежит в файле расширения, отклоняется.</li>
|
||||
<li><strong>Редактировать декодированные</strong> — редактирование файла в его собственной декодированной форме; кодирование выполняется в браузере, запись идёт обычным путём обновления. Кодировщики покрывают <strong>EF.SPN</strong>, <strong>EF.PNN</strong>, списки PLMN (<strong>PLMNsel / FPLMN / EHPLMN</strong> и списки с технологиями доступа <strong>PLMNwAcT / OPLMNwAcT / HPLMNwAcT</strong>), файлы номеров (<strong>ADN / FDN / SDN / MSISDN / LND / BDN</strong>), таблицы сервисов (<strong>UST / IST / EST</strong>; <strong>SST</strong> остаётся только для чтения до появления редактора allocated/activated), <strong>ACC</strong>, <strong>PHASE</strong>, <strong>HPPLMN</strong>, <strong>AD</strong>, <strong>ICCID / IMSI</strong> (с подтверждением — запись меняет идентичность карты) и списки cell broadcast (<strong>CBMI / CBMID / CBMIR</strong>). Для transparent-файла — одна форма (поля SPN, строки PLMN с добавлением/удалением, элементы PNN с добавлением/удалением); для record-файла — по форме на запись с флажком выбора, который ставится автоматически при изменении поля, поэтому записываются только изменённые записи. <strong>Сохранить</strong> проверяет все выбранные записи до записи; для файлов без кодировщика кнопка недоступна с подсказкой. Указатели расширений (EF.EXT1–EF.EXT5) и байт метода сравнения BDN сохраняются; редактирование номера, продолжение которого лежит в файле расширения, отклоняется.</li>
|
||||
<li><strong>Проверить все файлы</strong> — обход всего дерева (включая пользовательские файлы) с пометкой каждого элемента: есть (обычный вид) или нет (красный ✗, без стрелки разворачивания); существующие пустые DF показывают <code class="font-mono text-sm">(пусто)</code>. Отображается прогресс <em>N / всего</em>, обход можно остановить; в конце — сводка «есть/нет». Файлы проверяются только при разворачивании или проверке — просмотр остаётся ленивым.</li>
|
||||
<li><strong>ADM</strong> — файлы, требующие администраторский PIN, возвращают <code class="font-mono text-sm">6982</code>/<code class="font-mono text-sm">9804</code>; если в подходящем пресете карты (тот же ICCID) есть ключ ADM, рядом с ошибкой появляется кнопка <strong>«Проверить ADM»</strong>, а значок в заголовке (<code class="font-mono text-sm">ADM ✓/✗ ⚿</code>) становится кликабельным. Каждый неверный ключ расходует попытку: остаток показывается, а повторная попытка требует подтверждения. Заблокированный ADM здесь не восстановить — нужен ключ разблокировки карты.</li>
|
||||
</ul>
|
||||
|
||||
+1
-1
@@ -408,7 +408,7 @@
|
||||
<ul class="list-disc list-inside text-sm space-y-1 mb-3">
|
||||
<li><strong>Read raw / Read decoded</strong> — read the selected file as a hex dump or as a decoded field table (client-side EF decoders: IMSI, ICCID, SPN, PLMN lists, LOCI/PSLOCI/EPSLOCI, ADN/MSISDN, service tables, SUME, …); the decoded table is built in the browser from the same raw bytes (no server-side decode). The highlighted pill is the current view; clicking either pill (re-)reads the file. <strong>Read decoded</strong> is disabled when the client-side decoders do not cover the file (name/FID check, before reading).</li>
|
||||
<li><strong>Edit raw</strong> — edit the raw hex data, <strong>Save</strong> to write back (or <strong>Cancel</strong>); transparent files use a textarea, record files one input per record with selection checkboxes. <strong>Edit raw</strong> switches to the hex view first and reads the file if needed.</li>
|
||||
<li><strong>Edit decoded</strong> — edit the file through its own decoded form, encoded in the browser and written with the normal update path. The encoders cover <strong>EF.SPN</strong>, the PLMN lists with access technology (<strong>PLMNwAcT / OPLMNwAcT / HPLMNwAcT</strong>), the dialling-number files (<strong>ADN / FDN / SDN / MSISDN / LND / BDN</strong>) and <strong>EF.PNN</strong>. A transparent file gets one form (SPN fields, PLMN rows with add/remove, PNN items with add/remove); a record file gets one form per record with a selection checkbox that is auto-checked when a field changes, so only the edited records are written. <strong>Save</strong> validates every selected record before writing anything; files without an encoder keep the button disabled with a tooltip. Extension pointers (EF.EXT1–EF.EXT5) and the BDN comparison-method byte are preserved, and editing a number that continues in an extension file is refused.</li>
|
||||
<li><strong>Edit decoded</strong> — edit the file through its own decoded form, encoded in the browser and written with the normal update path. The encoders cover <strong>EF.SPN</strong>, <strong>EF.PNN</strong>, the PLMN lists (<strong>PLMNsel / FPLMN / EHPLMN</strong> and the access-technology lists <strong>PLMNwAcT / OPLMNwAcT / HPLMNwAcT</strong>), the dialling-number files (<strong>ADN / FDN / SDN / MSISDN / LND / BDN</strong>), the service tables (<strong>UST / IST / EST</strong>; <strong>SST</strong> stays read-only until the SIM allocated/activated editor exists), <strong>ACC</strong>, <strong>PHASE</strong>, <strong>HPPLMN</strong>, <strong>AD</strong>, <strong>ICCID / IMSI</strong> (behind a confirmation — writing them changes the card identity) and the cell-broadcast lists (<strong>CBMI / CBMID / CBMIR</strong>). A transparent file gets one form (SPN fields, PLMN rows with add/remove, PNN items with add/remove); a record file gets one form per record with a selection checkbox that is auto-checked when a field changes, so only the edited records are written. <strong>Save</strong> validates every selected record before writing anything; files without an encoder keep the button disabled with a tooltip. Extension pointers (EF.EXT1–EF.EXT5) and the BDN comparison-method byte are preserved, and editing a number that continues in an extension file is refused.</li>
|
||||
<li><strong>Probe all files</strong> — walks the whole tree (including custom files) and marks every entry present (normal) or absent (red ✗, no expand arrow); empty-but-present DFs show <code class="font-mono text-sm">(empty)</code>. Shows progress <em>N / total</em>, can be stopped, and finishes with a present/absent summary. Files are only verified when expanded or probed — browsing stays lazy.</li>
|
||||
<li><strong>ADM</strong> — files that need the administrator PIN fail with <code class="font-mono text-sm">6982</code>/<code class="font-mono text-sm">9804</code>; if the matching card preset (same ICCID) carries an ADM key, a <strong>Verify ADM</strong> button appears next to the error and the header badge (<code class="font-mono text-sm">ADM ✓/✗ ⚿</code>) becomes clickable. Every wrong key consumes an attempt: the remaining attempts are shown and a retry asks for confirmation. A blocked ADM cannot be recovered here — it needs the card’s unblock key.</li>
|
||||
</ul>
|
||||
|
||||
+564
-26
File diff suppressed because one or more lines are too long
+1
-1
@@ -1,4 +1,4 @@
|
||||
const CACHE = 'simple-v466';
|
||||
const CACHE = 'simple-v467';
|
||||
const URLS = [
|
||||
'index.html',
|
||||
'help.html',
|
||||
|
||||
@@ -24,9 +24,9 @@ function extractFunc(src, name) {
|
||||
const FNS = [
|
||||
'efBytes', 'efHex', 'efSwapNibbles', 'efAllFf', 'efRstripFf', 'efTlv', 'efUcs2Be',
|
||||
'efGsm7Octets', 'efAnnexA', 'efBcdAddress', 'efBcdAddressLen', 'efPlmn', 'efPlmnAct', 'efPnnText',
|
||||
'efDecIccid', 'efDecImsi', 'efDecLi', 'efDecServiceTable', 'efDecPlmnList', 'efDecFplmn', 'efDecPlmnWact',
|
||||
'efDecIccid', 'efDecImsi', 'efDecLi', 'efDecServiceTable', 'efDecSst', 'efDecPlmnList', 'efDecFplmn', 'efDecPlmnWact',
|
||||
'efDecSpn', 'efDecLoci', 'efDecPsLoci', 'efDecEpsLoci', 'efDecEpsNsc', 'efDecKc', 'efDecAcc',
|
||||
'efDecPhase', 'efDecCbmi', 'efDecCbmir', 'efDecEcc', 'efDecOpl', 'efDecAd', 'efDecAcl',
|
||||
'efDecPhase', 'efDecCbmi', 'efDecCbmir', 'efDecHpplmn', 'efDecEcc', 'efDecOpl', 'efDecAd', 'efDecAcl',
|
||||
'efDecSmsp', 'efDecSpdi', 'efDecNai', 'efDecDir', 'efDecArr', 'efDecPnn', 'efDecAdn',
|
||||
'pinKeyRefName', 'efArrAmLabel', 'efArrSc', 'efArrAmDo',
|
||||
'efDecExt1', 'efDecSms', 'efDecSume', 'efPbrHex', 'efDecPbr', 'efFindDecoder', 'efFidFromPath', 'efDecodeBytes',
|
||||
@@ -42,7 +42,7 @@ for (const f of FNS) code += extractFunc(html, f) + '\n';
|
||||
for (const c of ['GSM7_ALPHABET', 'GSM7_EXT_MAP', 'FCP_ARR_AM_BITS']) {
|
||||
code += html.match(new RegExp('const ' + c + ' = [\\s\\S]*?\\n\\];'))[0].replace('const ', 'var ') + '\n';
|
||||
}
|
||||
for (const c of ['EF_UST_SERVICES', 'EF_SST_SERVICES', 'EF_IST_SERVICES', 'EF_PHASE_NAMES', 'EF_PBR_KINDS']) {
|
||||
for (const c of ['EF_UST_SERVICES', 'EF_SST_SERVICES', 'EF_IST_SERVICES', 'EF_EST_SERVICES', 'EF_PHASE_NAMES', 'EF_PBR_KINDS']) {
|
||||
code += html.match(new RegExp('const ' + c + ' = \\{.*\\};'))[0].replace('const ', 'var ') + '\n';
|
||||
}
|
||||
code += html.match(/const EF_FIELD_LABELS = \{[\s\S]*?\n\};/)[0].replace('const ', 'var ') + '\n';
|
||||
@@ -103,7 +103,9 @@ test('EF decoders match the pinned spec / pySim test vectors', () => {
|
||||
assert.deepStrictEqual(efDecLi(efBytes('FFFF')), { languages: [] });
|
||||
assert.deepStrictEqual(efDecSpn(efBytes('0147534d2d52204348ffffffffffffffff')), { show_in_hplmn: true, hide_in_oplmn: false, name: 'GSM-R CH' });
|
||||
assert.deepStrictEqual(efDecAcc(efBytes('0000')), { access_classes: [] });
|
||||
assert.deepStrictEqual(efDecAcc(efBytes('8000')), { access_classes: [0] });
|
||||
assert.deepStrictEqual(efDecAcc(efBytes('8000')), { access_classes: [15] });
|
||||
assert.deepStrictEqual(efDecAcc(efBytes('0001')), { access_classes: [0] });
|
||||
assert.deepStrictEqual(efDecAcc(efBytes('8001')), { access_classes: [15, 0] });
|
||||
assert.deepStrictEqual(efDecEcc(efBytes('19f101')), { number: '911', esc: '0x01' });
|
||||
assert.deepStrictEqual(efDecKc(efBytes('837d783609a3858f05')), { kc: '837D783609A3858F', ksi: 0 });
|
||||
assert.deepStrictEqual(efDecPlmnList(efBytes('22F860')), { plmns: [{ mcc: '228', mnc: '06' }] });
|
||||
@@ -115,7 +117,7 @@ test('EF decoders match the pinned spec / pySim test vectors', () => {
|
||||
assert.strictEqual(efFindDecoder('EF.FPLMN', null).fn, efDecFplmn);
|
||||
assert.deepStrictEqual(efDecCbmi(efBytes('0010FFFF0020')), { message_ids: [16, 32] });
|
||||
assert.deepStrictEqual(efDecCbmir(efBytes('0000FFFEFFFF')), { ranges: [['0000', 'FFFE']] });
|
||||
assert.deepStrictEqual(efDecPhase(efBytes('03')), { phase: 'phase 2 and higher' });
|
||||
assert.deepStrictEqual(efDecPhase(efBytes('03')), { phase: 'phase 2 and PROFILE DOWNLOAD required' });
|
||||
assert.deepStrictEqual(efDecNai(efBytes('8021696d732e6d6e633030302e6d63633733382e336770706e6574776f726b2e6f7267')),
|
||||
{ text: 'ims.mnc000.mcc738.3gppnetwork.org' });
|
||||
assert.deepStrictEqual(efDecImsi(efBytes('FFFF')), { empty: true });
|
||||
@@ -290,6 +292,29 @@ test('service tables: UST service bits and IST fallback for 6F07', () => {
|
||||
assert.deepStrictEqual(ist.services, [{ n: 1, name: 'P-CSCF address' }]);
|
||||
});
|
||||
|
||||
test('EF.SST is the SIM 2-bit service table (allocated/activated)', () => {
|
||||
// TS 51.011 10.3.7: service 2 ADN -> byte 0, pair start bit 2 (0x04
|
||||
// allocated, 0x08 activated)
|
||||
assert.deepStrictEqual(efDecSst(efBytes('0C')), { services: [
|
||||
{ n: 2, name: 'Abbreviated Dialling Numbers (ADN)', allocated: true, activated: true },
|
||||
] });
|
||||
assert.deepStrictEqual(efDecSst(efBytes('04')), { services: [
|
||||
{ n: 2, name: 'Abbreviated Dialling Numbers (ADN)', allocated: true, activated: false },
|
||||
] });
|
||||
assert.deepStrictEqual(efDecSst(efBytes('08')), { services: [
|
||||
{ n: 2, name: 'Abbreviated Dialling Numbers (ADN)', allocated: false, activated: true },
|
||||
] });
|
||||
// byte 1 pair 3 (bits 6/7) = service 4 SMS; byte 0 pair 1 = service 2
|
||||
assert.deepStrictEqual(efDecSst(efBytes('C0')), { services: [
|
||||
{ n: 4, name: 'Short Message Storage (SMS)', allocated: true, activated: true },
|
||||
] });
|
||||
assert.deepStrictEqual(efDecSst(efBytes('00')), { services: [] });
|
||||
assert.deepStrictEqual(efFindDecoder('EF.SST', null).fn(efBytes('0C')).services[0].activated, true);
|
||||
// the read-only view shows both bits
|
||||
assert.ok(efFlatten(efDecSst(efBytes('0C'))).some(r =>
|
||||
r[0] === 'Service 2' && r[1].includes('allocated') && r[1].includes('activated')));
|
||||
});
|
||||
|
||||
test('decoder registry resolves by name first, then FID; unknown returns null', () => {
|
||||
assert.strictEqual(efFindDecoder('EF.IMSI', null).fid, '6f07');
|
||||
assert.strictEqual(efFindDecoder(null, '2fe2').name, 'EF.ICCID');
|
||||
|
||||
@@ -48,15 +48,16 @@ const FNS = [
|
||||
'efDecSpn', 'efDecPlmnWact', 'efDecAdn', 'efDecPnn', 'efFindDecoder',
|
||||
'efDecodeBytes', 'efTlv', 'efPnnText', 'efDecItemsFromHex',
|
||||
'efGsm7PlainText', 'efEncTextBytes', 'efPlmnEnc', 'efActEnc', 'efEncSpn',
|
||||
'efEncPlmnList', 'efPlmnRowOrig', 'efEncAdn', 'efEncAdnExt', 'efPnnNameBytes', 'efPnnHexBytes', 'efPnnOrigTlv', 'efEncPnn',
|
||||
'efEncPlmnList', 'efListRows', 'efPlmnRowOrig', 'efEncAdn', 'efEncAdnExt', 'efPnnNameBytes', 'efPnnHexBytes', 'efPnnOrigTlv', 'efEncPnn',
|
||||
'efFindEncoder', 'efHasEncoder', 'gsm7Decode', 'gsm7TextToSeptets',
|
||||
'gsm7Encode', 'efEncError',
|
||||
'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',
|
||||
];
|
||||
|
||||
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_ENCODERS']) {
|
||||
'EF_ADN_FIELDS', 'EF_PLMN_LIST', 'EF_PNN_ITEMS', 'EF_UST_SERVICES', 'EF_SST_SERVICES', 'EF_IST_SERVICES', 'EF_EST_SERVICES', 'EF_PHASE_NAMES', 'EF_ACC_CLASSES', 'EF_UST_FIELDS', 'EF_IST_FIELDS', 'EF_EST_FIELDS', 'EF_ACC_FIELDS', 'EF_PHASE_FIELDS', 'EF_HPPLMN_FIELDS', 'EF_AD_OP_MODES', 'EF_AD_FIELDS', 'EF_PLMN_ONLY_LIST', 'EF_ICCID_FIELDS', 'EF_IMSI_FIELDS', 'EF_CBMI_LIST', 'EF_CBMIR_LIST', 'EF_READONLY_NAMES', 'EF_ENCODERS']) {
|
||||
code += extractConst(html, c);
|
||||
}
|
||||
eval(code);
|
||||
@@ -211,7 +212,7 @@ test('EF.SPN and the ADN family share the encoder registry with the PLMN list',
|
||||
assert.strictEqual(efFindEncoder('EF.ALIAS', '6f60').enc, efEncPlmnList);
|
||||
assert.strictEqual(efFindEncoder('EF.NOPE', 'abcd'), null);
|
||||
assert.strictEqual(efHasEncoder('EF.ADN', null), true);
|
||||
assert.strictEqual(efHasEncoder('EF.IMSI', null), false);
|
||||
assert.strictEqual(efHasEncoder('EF.LOCI', null), false);
|
||||
// one shared schema for the ADN family and one for the three PLMN files
|
||||
assert.strictEqual(efFindEncoder('EF.ADN', null).fields, efFindEncoder('EF.BDN', null).fields);
|
||||
assert.strictEqual(efFindEncoder('EF.PLMNwAcT', null).list, efFindEncoder('EF.HPLMNwAcT', null).list);
|
||||
@@ -429,14 +430,36 @@ test('F14: the encoder messages go through t() (unknown technology, too many row
|
||||
});
|
||||
|
||||
test('unchanged content re-encodes byte-identically (all fixtures)', () => {
|
||||
const tables = { 'EF.UST': EF_UST_SERVICES, 'EF.IST': EF_IST_SERVICES,
|
||||
'EF.EST': EF_EST_SERVICES };
|
||||
const decode = (name, hex) => {
|
||||
const entry = efFindEncoder(name, null);
|
||||
const bytes = efBytes(hex);
|
||||
if (name === 'EF.SPN') return efDecSpn(bytes);
|
||||
if (name === 'EF.PLMNwAcT') return efDecPlmnWact(bytes).plmns;
|
||||
if (name === 'EF.BDN') return efDecAdn(bytes, 15);
|
||||
if (name === 'EF.ADN') return efDecAdn(bytes);
|
||||
if (name === 'EF.PNN') return efDecItemsFromHex(efFindEncoder('EF.PNN', null), hex);
|
||||
throw new Error('no decoder for ' + name);
|
||||
let dec;
|
||||
if (name === 'EF.SPN') dec = efDecSpn(bytes);
|
||||
else if (tables[name]) {
|
||||
// the form renders only the table's services; other set bits stay
|
||||
// in the original bytes
|
||||
dec = { services: efDecServiceTable(bytes, tables[name]).services
|
||||
.filter(s => Object.prototype.hasOwnProperty.call(tables[name], s.n)) };
|
||||
} else if (name === 'EF.ACC') dec = efDecAcc(bytes);
|
||||
else if (name === 'EF.PHASE') dec = efDecPhase(bytes);
|
||||
else if (name === 'EF.HPPLMN') dec = efDecHpplmn(bytes);
|
||||
else if (name === 'EF.AD') dec = efDecAd(bytes);
|
||||
else if (name === 'EF.PLMNsel' || name === 'EF.EHPLMN') dec = efDecPlmnList(bytes);
|
||||
else if (name === 'EF.FPLMN') dec = efDecFplmn(bytes);
|
||||
else if (name === 'EF.ICCID') dec = efDecIccid(bytes);
|
||||
else if (name === 'EF.IMSI') dec = efDecImsi(bytes);
|
||||
else if (name === 'EF.CBMI' || name === 'EF.CBMID') dec = efDecCbmi(bytes);
|
||||
else if (name === 'EF.CBMIR') dec = efDecCbmir(bytes);
|
||||
else if (name === 'EF.PLMNwAcT') dec = efDecPlmnWact(bytes).plmns;
|
||||
else if (name === 'EF.BDN') dec = efDecAdn(bytes, 15);
|
||||
else if (name === 'EF.ADN') dec = efDecAdn(bytes);
|
||||
else if (name === 'EF.PNN') dec = efDecItemsFromHex(efFindEncoder('EF.PNN', null), hex);
|
||||
else throw new Error('no decoder for ' + name);
|
||||
// the model's empty/raw marker yields the form's empty value set
|
||||
if (dec && (dec.empty === true || (dec.raw !== undefined && Object.keys(dec).length === 1))) dec = {};
|
||||
return entry.model ? entry.model(dec) : dec;
|
||||
};
|
||||
const cases = [
|
||||
{ name: 'EF.SPN', fid: '6f46', hex: SPN_ORIG },
|
||||
@@ -456,6 +479,38 @@ test('unchanged content re-encodes byte-identically (all fixtures)', () => {
|
||||
{ name: 'EF.PNN', fid: '6fc5', hex: PNN_2 },
|
||||
{ name: 'EF.PNN', fid: '6fc5', hex: '800400410042' + 'FF'.repeat(16) },
|
||||
{ name: 'EF.PNN', fid: '6fc5', hex: '430a82d432bbbc7eb75de4325a02aabb' + 'ff'.repeat(14) },
|
||||
// wave 2
|
||||
{ name: 'EF.UST', fid: '6f38', hex: UST_ONE },
|
||||
{ name: 'EF.UST', fid: '6f38', hex: UST_MULTI },
|
||||
{ name: 'EF.UST', fid: '6f38', hex: UST_UNMODELLED },
|
||||
{ name: 'EF.UST', fid: '6f38', hex: 'FF'.repeat(19) },
|
||||
{ name: 'EF.IST', fid: '6f07', hex: '01' },
|
||||
{ name: 'EF.EST', fid: '6f56', hex: '07' },
|
||||
{ name: 'EF.EST', fid: '6f56', hex: EST_FF },
|
||||
{ name: 'EF.ACC', fid: '6f78', hex: ACC_ONE },
|
||||
{ name: 'EF.PHASE', fid: '6fae', hex: '00' },
|
||||
{ name: 'EF.PHASE', fid: '6fae', hex: '02' },
|
||||
{ name: 'EF.PHASE', fid: '6fae', hex: '03' },
|
||||
{ name: 'EF.PHASE', fid: '6fae', hex: '0F' },
|
||||
{ name: 'EF.HPPLMN', fid: '6f31', hex: '00' },
|
||||
{ name: 'EF.HPPLMN', fid: '6f31', hex: '2A' },
|
||||
{ name: 'EF.AD', fid: '6fad', hex: AD_HEX },
|
||||
{ name: 'EF.AD', fid: '6fad', hex: '55' + '0000' + '03' + 'AABB' },
|
||||
{ name: 'EF.AD', fid: '6fad', hex: 'FF'.repeat(6) },
|
||||
{ name: 'EF.AD', fid: '6fad', hex: '00' + '0000' },
|
||||
{ name: 'EF.AD', fid: '6fad', hex: '00' + '0000' + '00' + 'AABB' },
|
||||
{ name: 'EF.PLMNsel', fid: '6f30', hex: PLMN_ONLY },
|
||||
{ name: 'EF.FPLMN', fid: '6f7b', hex: FPLMN_HOLES },
|
||||
{ name: 'EF.EHPLMN', fid: '6fd9', hex: PLMN_ONLY },
|
||||
{ name: 'EF.ICCID', fid: '2fe2', hex: ICCID_HEX },
|
||||
{ name: 'EF.ICCID', fid: '2fe2', hex: 'FF'.repeat(10) },
|
||||
{ name: 'EF.IMSI', fid: '6f07', hex: IMSI_HEX },
|
||||
{ name: 'EF.IMSI', fid: '6f07', hex: '0811325476981032F4' },
|
||||
{ name: 'EF.IMSI', fid: '6f07', hex: 'FF'.repeat(9) },
|
||||
{ name: 'EF.CBMI', fid: '6f45', hex: CBMI_HEX },
|
||||
{ name: 'EF.CBMID', fid: '6f48', hex: CBMI_HEX },
|
||||
{ name: 'EF.CBMIR', fid: '6f50', hex: CBMIR_HEX },
|
||||
{ name: 'EF.CBMIR', fid: '6f50', hex: 'FF'.repeat(4) },
|
||||
];
|
||||
for (const c of cases) {
|
||||
const entry = efFindEncoder(c.name, c.fid);
|
||||
@@ -511,3 +566,205 @@ test('F8: the ADN-family entries name their extension EF and use it in refusals'
|
||||
// the unchanged number still copies the extension slots verbatim
|
||||
assert.strictEqual(efEncAdn(efDecAdn(efBytes(fdn)), fdn, 14, 'EXT2'), fdn.toUpperCase());
|
||||
});
|
||||
|
||||
// Wave 2 fixtures.
|
||||
const UST_ONE = '00000010'; // service 29
|
||||
const UST_MULTI = '010204'; // services 1, 10, 19
|
||||
const UST_UNMODELLED = '000000' + '10' + '00'.repeat(14) + 'FC'; // 29 + bits 147-152
|
||||
const EST_FF = 'FFFF';
|
||||
const ACC_ONE = '8001'; // classes 15 and 0
|
||||
const AD_HEX = '00' + '0000' + '03' + 'AABB'; // op normal, info 0000, MNC 3, RFU
|
||||
const PLMN_ONLY = '62F21022F860';
|
||||
const FPLMN_HOLES = '22F860FFFFFF62F210';
|
||||
const ICCID_HEX = '988812010000400310f0';
|
||||
const IMSI_HEX = '082982608200002080';
|
||||
const CBMI_HEX = '0010FFFF0020';
|
||||
const CBMIR_HEX = '0000FFFE';
|
||||
|
||||
test('service tables: encode/decode round-trips and unmodelled bits', () => {
|
||||
const label = n => EF_UST_SERVICES[n] || 'RFU';
|
||||
assert.deepStrictEqual(efDecServiceTable(efBytes(UST_MULTI), EF_UST_SERVICES).services,
|
||||
[{ n: 1, name: label(1) }, { n: 10, name: label(10) }, { n: 19, name: label(19) }]);
|
||||
for (const [name, hex] of [['EF.UST', UST_ONE], ['EF.UST', UST_MULTI],
|
||||
['EF.IST', '01'], ['EF.EST', '07']]) {
|
||||
const entry = efFindEncoder(name, null);
|
||||
const table = name === 'EF.UST' ? EF_UST_SERVICES
|
||||
: (name === 'EF.IST' ? EF_IST_SERVICES : EF_EST_SERVICES);
|
||||
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);
|
||||
assert.strictEqual(efFindEncoder('EF.UST', '6f38').name, 'EF.UST');
|
||||
// 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)' },
|
||||
{ n: 2, name: 'Barred Dialling Numbers (BDN)' },
|
||||
{ n: 3, name: 'APN Control List (ACL)' }]);
|
||||
assert.strictEqual(efFindEncoder('EF.EST', null).enc(
|
||||
{ services: efDecServiceTable(efBytes(EST_FF), EF_EST_SERVICES).services
|
||||
.filter(s => Object.prototype.hasOwnProperty.call(EF_EST_SERVICES, s.n)) }, EST_FF), EST_FF);
|
||||
// service bits outside the table are preserved from the original bytes
|
||||
const entry = efFindEncoder('EF.UST', null);
|
||||
const unmodelled = efDecServiceTable(efBytes(UST_UNMODELLED), EF_UST_SERVICES).services
|
||||
.filter(s => Object.prototype.hasOwnProperty.call(EF_UST_SERVICES, s.n));
|
||||
assert.strictEqual(entry.enc({ services: unmodelled }, UST_UNMODELLED), UST_UNMODELLED);
|
||||
// unchecking everything clears the table bits but keeps the unmodelled byte
|
||||
assert.strictEqual(entry.enc({ services: [] }, UST_UNMODELLED),
|
||||
'000000' + '00' + '00'.repeat(14) + 'FC');
|
||||
// length is kept
|
||||
assert.strictEqual(entry.enc({ services: efDecServiceTable(efBytes('00000010' + '00'), EF_UST_SERVICES).services },
|
||||
'0000001000'), '0000001000');
|
||||
});
|
||||
|
||||
test('service tables: unknown services and a too-short table are refused', () => {
|
||||
assert.throws(() => efEncServiceTable({ services: [{ n: 147, name: 'RFU' }] },
|
||||
'FF'.repeat(19), EF_UST_SERVICES), /Unknown service/);
|
||||
assert.throws(() => efEncServiceTable({ services: [{ n: 9, name: 'MSISDN' }] },
|
||||
'00', EF_UST_SERVICES), /too short for service/);
|
||||
assert.throws(() => efEncServiceTable({ services: [{ n: 0 }] }, '00', EF_UST_SERVICES), /Unknown service/);
|
||||
});
|
||||
|
||||
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] });
|
||||
assert.deepStrictEqual(efDecAcc(efBytes('8001')), { access_classes: [15, 0] });
|
||||
assert.strictEqual(efEncAcc({ access_classes: [{ n: 15 }] }, '0000'), '8000');
|
||||
assert.strictEqual(efEncAcc({ access_classes: [{ n: 0 }] }, '0000'), '0001');
|
||||
assert.strictEqual(efEncAcc({ access_classes: efDecAcc(efBytes(ACC_ONE)).access_classes }, ACC_ONE), ACC_ONE);
|
||||
// ACC 10 is not editable but the encoder always forces it to 0
|
||||
assert.strictEqual(efEncAcc({ access_classes: [{ n: 10 }] }, '0000'), '0000');
|
||||
assert.strictEqual(efEncAcc({ access_classes: [{ n: 15 }, { n: 10 }] }, '0000'), '8000');
|
||||
assert.throws(() => efEncAcc({ access_classes: [{ n: 16 }] }, '0000'), /Unknown access class/);
|
||||
assert.throws(() => efEncAcc({ access_classes: [] }, '00'), /too short/);
|
||||
});
|
||||
|
||||
test('PHASE: known and unknown values round-trip through the model', () => {
|
||||
for (const hex of ['00', '02', '03', '0F']) {
|
||||
const model = efPhaseModel(efDecPhase(efBytes(hex)));
|
||||
assert.strictEqual(efEncPhase(model, hex), hex, hex);
|
||||
}
|
||||
assert.deepStrictEqual(efPhaseModel(efDecPhase(efBytes('03'))), { phase_hex: '03' });
|
||||
assert.deepStrictEqual(efPhaseModel(efDecPhase(efBytes('0F'))), { phase_hex: '0F' });
|
||||
assert.strictEqual(efDecPhase(efBytes('0F')).phase, '0x0F');
|
||||
assert.throws(() => efEncPhase({ phase_hex: 'ZZ' }, '00'), /hex byte/);
|
||||
});
|
||||
|
||||
test('HPPLMN: the raw interval byte round-trips', () => {
|
||||
assert.deepStrictEqual(efDecHpplmn(efBytes('2A')), { interval: '0x2A' });
|
||||
for (const hex of ['00', '2A', 'FF']) {
|
||||
assert.strictEqual(efEncHpplmn(efDecHpplmn(efBytes(hex)), hex), hex, hex);
|
||||
}
|
||||
assert.strictEqual(efEncHpplmn({ interval: '2A' }, '2A'), '2A');
|
||||
assert.throws(() => efEncHpplmn({ interval: 'XYZ' }, '00'), /hex byte/);
|
||||
});
|
||||
|
||||
test('AD: the whole-byte operation mode, additional info, MNC length and RFU tail', () => {
|
||||
const dec = efDecAd(efBytes(AD_HEX));
|
||||
assert.deepStrictEqual(dec, { op_mode: 'normal operation', additional_info: '0000', mnc_length: 3 });
|
||||
assert.deepStrictEqual(efAdModel(dec), { op_mode: '00', additional_info: '0000', mnc_length: '3' });
|
||||
assert.strictEqual(efEncAd(efAdModel(dec), AD_HEX), AD_HEX);
|
||||
for (const [op, name] of [['80', 'type approval'], ['01', 'normal operation + specific facilities'],
|
||||
['81', 'type approval + specific facilities'], ['02', 'maintenance (off line)'],
|
||||
['04', 'cell test operation']]) {
|
||||
const hex = op + '0000' + '03';
|
||||
assert.strictEqual(efDecAd(efBytes(hex)).op_mode, name, op);
|
||||
assert.strictEqual(efEncAd(efAdModel(efDecAd(efBytes(hex))), hex), hex, op);
|
||||
}
|
||||
// an edited op mode / info / MNC keeps the RFU tail
|
||||
assert.strictEqual(efEncAd({ op_mode: '80', additional_info: '1234', mnc_length: '2' }, AD_HEX),
|
||||
'80123402AABB');
|
||||
// the SIM 3-byte form (no MNC-length byte) and the valid MNC length 0
|
||||
assert.strictEqual(efEncAd({ op_mode: '00', additional_info: '1234' }, '00' + '0000'), '001234');
|
||||
assert.strictEqual(efEncAd({ op_mode: '00', additional_info: '1234', mnc_length: '' }, '00' + '0000'), '001234');
|
||||
assert.throws(() => efEncAd({ op_mode: '00', additional_info: '1234', mnc_length: '2' }, '00' + '0000'),
|
||||
/no MNC length byte/);
|
||||
assert.strictEqual(efEncAd({ op_mode: '00', additional_info: '0000', mnc_length: '0' }, AD_HEX),
|
||||
'000000' + '00' + 'AABB');
|
||||
// an unknown op byte is shown and written back
|
||||
const unk = '55' + '0000' + '03' + 'AABB';
|
||||
assert.strictEqual(efDecAd(efBytes(unk)).op_mode, '0x55');
|
||||
assert.strictEqual(efAdModel(efDecAd(efBytes(unk))).op_mode, '55');
|
||||
assert.strictEqual(efEncAd(efAdModel(efDecAd(efBytes(unk))), unk), unk);
|
||||
// an all-FF record stays all-FF (the model is empty)
|
||||
assert.strictEqual(efEncAd(efAdModel({}), 'FF'.repeat(6)), 'FF'.repeat(6));
|
||||
assert.throws(() => efEncAd({ op_mode: 'ZZ' }, AD_HEX), /hex byte/);
|
||||
assert.throws(() => efEncAd({ op_mode: '00', additional_info: '123' }, AD_HEX), /4 hex digits/);
|
||||
assert.throws(() => efEncAd({ op_mode: '00', mnc_length: '4' }, AD_HEX), /0, 2 or 3/);
|
||||
assert.throws(() => efEncAd({ op_mode: '00' }, '00'), /too short/);
|
||||
});
|
||||
|
||||
test('PLMN-only lists: round-trip, holes, add/remove and capacity', () => {
|
||||
assert.deepStrictEqual(efDecPlmnList(efBytes(PLMN_ONLY)).plmns,
|
||||
[{ mcc: '262', mnc: '01' }, { mcc: '228', mnc: '06' }]);
|
||||
assert.strictEqual(efEncPlmnOnly({ plmns: efDecPlmnList(efBytes(PLMN_ONLY)).plmns }, PLMN_ONLY), PLMN_ONLY);
|
||||
// FPLMN FFFFFF holes anywhere are kept for an unchanged list
|
||||
assert.deepStrictEqual(efDecFplmn(efBytes(FPLMN_HOLES)).plmns,
|
||||
[{ mcc: '228', mnc: '06' }, { mcc: '262', mnc: '01' }]);
|
||||
assert.strictEqual(efEncPlmnOnly({ plmns: efDecFplmn(efBytes(FPLMN_HOLES)).plmns }, FPLMN_HOLES), FPLMN_HOLES);
|
||||
// an edit compacts and FF-pads
|
||||
assert.strictEqual(efEncPlmnOnly({ plmns: [{ mcc: '262', mnc: '01' }] }, FPLMN_HOLES),
|
||||
'62F210' + 'FF'.repeat(6));
|
||||
assert.strictEqual(efEncPlmnOnly({ plmns: [
|
||||
{ mcc: '262', mnc: '01' }, { mcc: '228', mnc: '06' }, { mcc: '250', mnc: '99' },
|
||||
] }, '62F21022F860' + 'FFFFFF'), '62F21022F86052F099');
|
||||
assert.throws(() => efEncPlmnOnly({ plmns: [{ mcc: '262', mnc: '01' }, { mcc: '228', mnc: '06' }] },
|
||||
'62F210'), /Too many entries/);
|
||||
assert.throws(() => efEncPlmnOnly({ plmns: [{ mcc: '26', mnc: '01' }] }, '62F210'), /MCC/);
|
||||
});
|
||||
|
||||
test('ICCID / IMSI: BCD round-trips, padding and refusals', () => {
|
||||
assert.deepStrictEqual(efDecIccid(efBytes(ICCID_HEX)), { iccid: '8988211000000430010' });
|
||||
assert.strictEqual(efEncIccid({ iccid: '8988211000000430010' }, ICCID_HEX), ICCID_HEX.toUpperCase());
|
||||
// odd digit count -> trailing F nibble
|
||||
assert.strictEqual(efEncIccid({ iccid: '12345' }, 'FF'.repeat(10)), '2143F5' + 'FF'.repeat(7));
|
||||
assert.strictEqual(efEncIccid({}, 'FF'.repeat(10)), 'FF'.repeat(10));
|
||||
assert.throws(() => efEncIccid({ iccid: '12A' }, 'FF'.repeat(10)), /digits/);
|
||||
assert.throws(() => efEncIccid({ iccid: '1'.repeat(21) }, 'FF'.repeat(10)), /too long/);
|
||||
assert.throws(() => efEncIccid({ iccid: '1'.repeat(20) }, 'FF'.repeat(9)), /does not fit/);
|
||||
|
||||
assert.deepStrictEqual(efDecImsi(efBytes(IMSI_HEX)), { imsi: '228062800000208' });
|
||||
assert.strictEqual(efEncImsi({ imsi: '228062800000208' }, IMSI_HEX), IMSI_HEX);
|
||||
// 14 digits (even): the type/parity nibble is 1 and the body is F-padded
|
||||
assert.strictEqual(efEncImsi({ imsi: '12345678901234' }, 'FF'.repeat(9)), '0811325476981032F4');
|
||||
assert.deepStrictEqual(efDecImsi(efBytes('0811325476981032F4')), { imsi: '12345678901234' });
|
||||
assert.strictEqual(efEncImsi({ imsi: '123456789012345' }, 'FF'.repeat(9)), '081932547698103254');
|
||||
assert.deepStrictEqual(efDecImsi(efBytes('081932547698103254')), { imsi: '123456789012345' });
|
||||
// 6F07 is also EF.IST: a short file is refused, not overwritten
|
||||
assert.throws(() => efEncImsi({ imsi: '228062800000208' }, '01'), /service table, not an IMSI/);
|
||||
assert.throws(() => efEncImsi({}, '01'), /service table, not an IMSI/);
|
||||
assert.strictEqual(efEncImsi({}, 'FF'.repeat(9)), 'FF'.repeat(9));
|
||||
assert.throws(() => efEncImsi({ imsi: '12345' }, 'FF'.repeat(9)), /6-15/);
|
||||
assert.throws(() => efEncImsi({ imsi: '1'.repeat(16) }, 'FF'.repeat(9)), /6-15/);
|
||||
assert.throws(() => efEncImsi({ imsi: '228062800000208' }, 'FF'.repeat(8)), /does not fit/);
|
||||
});
|
||||
|
||||
test('CBMI / CBMIR: message ids and ranges round-trip with FF padding', () => {
|
||||
assert.deepStrictEqual(efDecCbmi(efBytes(CBMI_HEX)), { message_ids: [16, 32] });
|
||||
const cbmi = efCbmiModel(efDecCbmi(efBytes(CBMI_HEX)));
|
||||
assert.deepStrictEqual(cbmi, { message_ids: [{ id: '0010' }, { id: '0020' }] });
|
||||
assert.strictEqual(efEncCbmi(cbmi, CBMI_HEX), CBMI_HEX); // the FFFF hole is kept
|
||||
assert.strictEqual(efEncCbmi({ message_ids: [{ id: '0010' }, { id: '0020' }, { id: '0044' }] },
|
||||
CBMI_HEX), '001000200044');
|
||||
assert.strictEqual(efEncCbmi({ message_ids: [{ id: '0010' }] }, '0010FFFFFFFF'), '0010FFFFFFFF');
|
||||
assert.strictEqual(efEncCbmi({ message_ids: [{ id: '0010' }, { id: '20' }] }, '0010FFFFFFFF'),
|
||||
'00100020FFFF');
|
||||
assert.throws(() => efEncCbmi({ message_ids: [{ id: 'XYZ' }] }, CBMI_HEX), /1-4 hex digits/);
|
||||
assert.throws(() => efEncCbmi({ message_ids: [{ id: '0010' }, { id: '0020' }, { id: '0030' }, { id: '0040' }] },
|
||||
'0010FFFF'), /Too many entries/);
|
||||
|
||||
assert.deepStrictEqual(efDecCbmir(efBytes(CBMIR_HEX)), { ranges: [['0000', 'FFFE']] });
|
||||
const cbmir = efCbmirModel(efDecCbmir(efBytes(CBMIR_HEX)));
|
||||
assert.deepStrictEqual(cbmir, { ranges: [{ low: '0000', high: 'FFFE' }] });
|
||||
assert.strictEqual(efEncCbmir(cbmir, CBMIR_HEX), CBMIR_HEX);
|
||||
assert.strictEqual(efEncCbmir({ ranges: [
|
||||
{ low: '0000', high: 'FFFE' }, { low: '0100', high: '02FF' },
|
||||
] }, CBMIR_HEX + 'FFFFFFFF'), '0000FFFE010002FF');
|
||||
assert.strictEqual(efEncCbmir({ ranges: [{ low: '0000', high: 'FFFE' }] },
|
||||
CBMIR_HEX + 'FFFFFFFF'), CBMIR_HEX + 'FFFFFFFF');
|
||||
assert.throws(() => efEncCbmir({ ranges: [{ low: '0000', high: 'XYZ' }] }, CBMIR_HEX), /1-4 hex digits/);
|
||||
assert.throws(() => efEncCbmir({ ranges: [
|
||||
{ low: '0000', high: 'FFFE' }, { low: '0100', high: '02FF' },
|
||||
] }, CBMIR_HEX), /Too many entries/);
|
||||
});
|
||||
|
||||
@@ -45,24 +45,25 @@ const FNS = [
|
||||
'efDecAdn', 'efFindDecoder', 'efDecodeBytes', 'efIsRawOnly', 'efTlv',
|
||||
'efPnnText', 'efDecItemsFromHex',
|
||||
'efGsm7PlainText', 'efEncTextBytes', 'efPlmnEnc', 'efActEnc', 'efEncSpn',
|
||||
'efEncPlmnList', 'efPlmnRowOrig', 'efEncAdn', 'efEncAdnExt', 'efPnnNameBytes', 'efPnnHexBytes', 'efPnnOrigTlv', 'efEncPnn',
|
||||
'efEncPlmnList', 'efListRows', 'efPlmnRowOrig', 'efEncAdn', 'efEncAdnExt', 'efPnnNameBytes', 'efPnnHexBytes', 'efPnnOrigTlv', 'efEncPnn',
|
||||
'efFindEncoder', 'efHasEncoder', 'efEncError',
|
||||
'efDecModelValues', 'efDecEditModel', 'efActTechList', 'efDecFieldHtml',
|
||||
'efDecModelValues', 'efDecModelFor', 'efDecEditModel', 'efActTechList', 'efDecFieldHtml',
|
||||
'efDecRowHtml', 'efDecGridHtml', 'efDecItemRowHtml', 'efDecItemsHtml',
|
||||
'efDecRecordsHtml', 'efDecEditHtml',
|
||||
'efDecCollectValues', 'efDecCollectRows', 'efDecCollectItems',
|
||||
'pysimFsDecRowCheck', 'pysimFsDecItemTag', 'pysimFsDecAddItem',
|
||||
'efBitsSelected', 'pysimFsDecBitsFilter', 'pysimFsDecRowCheck', 'pysimFsDecItemTag', 'pysimFsDecAddItem',
|
||||
'pysimFsDecRemoveItem', 'pysimFsFindNode',
|
||||
'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',
|
||||
];
|
||||
|
||||
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_ENCODERS']) {
|
||||
'EF_ADN_FIELDS', 'EF_PLMN_LIST', 'EF_PNN_ITEMS', 'EF_UST_SERVICES', 'EF_SST_SERVICES', 'EF_IST_SERVICES', 'EF_EST_SERVICES', 'EF_PHASE_NAMES', 'EF_ACC_CLASSES', 'EF_UST_FIELDS', 'EF_IST_FIELDS', 'EF_EST_FIELDS', 'EF_ACC_FIELDS', 'EF_PHASE_FIELDS', 'EF_HPPLMN_FIELDS', 'EF_AD_OP_MODES', 'EF_AD_FIELDS', 'EF_PLMN_ONLY_LIST', 'EF_ICCID_FIELDS', 'EF_IMSI_FIELDS', 'EF_CBMI_LIST', 'EF_CBMIR_LIST', 'EF_READONLY_NAMES', 'EF_ENCODERS']) {
|
||||
code += extractConst(html, c);
|
||||
}
|
||||
eval(code);
|
||||
@@ -70,6 +71,22 @@ eval(code);
|
||||
// The decoder entries these files resolve to (the full EF_DECODERS registry is
|
||||
// pinned by ef_decode.test.js; only the model's lookup path is exercised here).
|
||||
var EF_DECODERS = [
|
||||
{ name: 'EF.ICCID', fid: '2fe2', fn: efDecIccid },
|
||||
{ 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.EST', fid: '6f56', fn: b => efDecServiceTable(b, EF_EST_SERVICES) },
|
||||
{ name: 'EF.HPPLMN', fid: '6f31', fn: efDecHpplmn },
|
||||
{ name: 'EF.ACC', fid: '6f78', fn: efDecAcc },
|
||||
{ name: 'EF.PHASE', fid: '6fae', fn: efDecPhase },
|
||||
{ name: 'EF.AD', fid: '6fad', fn: efDecAd },
|
||||
{ name: 'EF.PLMNsel', fid: '6f30', fn: efDecPlmnList },
|
||||
{ name: 'EF.FPLMN', fid: '6f7b', fn: efDecFplmn },
|
||||
{ name: 'EF.EHPLMN', fid: '6fd9', fn: efDecPlmnList },
|
||||
{ name: 'EF.CBMI', fid: '6f45', fn: efDecCbmi },
|
||||
{ name: 'EF.CBMID', fid: '6f48', fn: efDecCbmi },
|
||||
{ name: 'EF.CBMIR', fid: '6f50', fn: efDecCbmir },
|
||||
{ name: 'EF.SPN', fid: '6f46', fn: efDecSpn },
|
||||
{ name: 'EF.PLMNwAcT', fid: '6f60', fn: efDecPlmnWact },
|
||||
{ name: 'EF.OPLMNwAcT', fid: '6f61', fn: efDecPlmnWact },
|
||||
@@ -95,10 +112,10 @@ test('efFindEncoder / efHasEncoder resolve by name and FID, like the decoders',
|
||||
assert.strictEqual(efFindEncoder('ef.plmnwact', null).name, 'EF.PLMNwAcT');
|
||||
assert.strictEqual(efFindEncoder('MF/7F10/6F3A/EF.ADN', null).name, 'EF.ADN');
|
||||
assert.strictEqual(efFindEncoder('EF.ALIAS', '6f40').name, 'EF.MSISDN');
|
||||
assert.strictEqual(efFindEncoder('EF.IMSI', '6f07'), null);
|
||||
assert.strictEqual(efFindEncoder('EF.LOCI', '6f7e'), null);
|
||||
assert.strictEqual(efFindEncoder(null, null), null);
|
||||
assert.strictEqual(efHasEncoder('EF.SDN', null), true);
|
||||
assert.strictEqual(efHasEncoder('EF.IMSI', null), false);
|
||||
assert.strictEqual(efHasEncoder('EF.LOCI', null), false);
|
||||
});
|
||||
|
||||
test('the model is a TR value set for SPN / PLMNwAcT and record rows for ADN', () => {
|
||||
@@ -126,7 +143,7 @@ test('the model is a TR value set for SPN / PLMNwAcT and record rows for ADN', (
|
||||
assert.deepStrictEqual(adn.records[1].values, {});
|
||||
assert.strictEqual(adn.records[1].orig, ADN_FF);
|
||||
// no encoder -> no model, even when a decoder exists
|
||||
assert.strictEqual(efDecEditModel('EF.IMSI', null, { kind: 'transparent', data: '082982608200002080' }), null);
|
||||
assert.strictEqual(efDecEditModel('EF.LOCI', null, { kind: 'transparent', data: '082982608200002080' }), null);
|
||||
assert.strictEqual(efDecEditModel('EF.NOPE', 'abcd', { kind: 'transparent', data: '00' }), null);
|
||||
});
|
||||
|
||||
@@ -244,7 +261,11 @@ test('pysimFsHasEncoder resolves the selected file by name or FID', () => {
|
||||
assert.strictEqual(pysimFsHasEncoder(), true);
|
||||
pysimFsSelected = '6F46';
|
||||
assert.strictEqual(pysimFsHasEncoder(), true);
|
||||
pysimFsSelected = 'EF.IMSI';
|
||||
pysimFsSelected = 'EF.LOCI';
|
||||
assert.strictEqual(pysimFsHasEncoder(), false);
|
||||
// the SIM service table shares 6F38 with UST but has no editor
|
||||
pysimFsTreeRoot = { name: 'MF', children: [{ name: 'EF.SST', fid: '6f38' }] };
|
||||
pysimFsSelected = 'EF.SST';
|
||||
assert.strictEqual(pysimFsHasEncoder(), false);
|
||||
pysimFsSelected = null;
|
||||
assert.strictEqual(pysimFsHasEncoder(), false);
|
||||
@@ -653,3 +674,234 @@ test('F12: a selection change during the read abandons the decoded edit', async
|
||||
assert.strictEqual(resets, 1);
|
||||
assert.strictEqual(content.innerHTML, '', 'the old file must not render over the new selection');
|
||||
});
|
||||
|
||||
// Wave 2 fixtures.
|
||||
const UST_ONE = '00000010'; // service 29
|
||||
const ACC_ONE = '8001'; // classes 15 and 0
|
||||
const CBMI_HEX = '0010FFFF0020';
|
||||
const CBMIR_HEX = '0000FFFE';
|
||||
const ICCID_HEX = '988812010000400310f0';
|
||||
|
||||
test('the bits field renders a filtered checkbox grid and collects {n,name}', () => {
|
||||
const model = efDecEditModel('EF.UST', '6f38', { kind: 'transparent', data: UST_ONE });
|
||||
assert.deepStrictEqual(model.values.services, [{ n: 29, name: 'Data download via SMS-CB' }]);
|
||||
const out = efDecEditHtml(model, UST_ONE);
|
||||
assert.match(out, /class="pysim-dec-bits /);
|
||||
assert.match(out, /pysim-dec-bits-filter/);
|
||||
assert.match(out, /data-bit="29" data-bit-name="Data download via SMS-CB" checked/);
|
||||
// the whole UST table is rendered (146 services)
|
||||
assert.strictEqual((out.match(/data-bit="/g) || []).length, 146);
|
||||
// ACC: 15 editable classes (10 is always written 0), 15 and 0 checked
|
||||
const acc = efDecEditHtml(efDecEditModel('EF.ACC', '6f78', { kind: 'transparent', data: ACC_ONE }), ACC_ONE);
|
||||
assert.strictEqual((acc.match(/data-bit="/g) || []).length, 15);
|
||||
assert.match(acc, /data-bit="15" data-bit-name="Class 15" checked/);
|
||||
assert.match(acc, /data-bit="0" data-bit-name="Class 0" checked/);
|
||||
assert.doesNotMatch(acc, /data-bit="10"/);
|
||||
assert.match(acc, /data-bit="11" data-bit-name="Class 11"/);
|
||||
// the collection yields the checked {n,name} list
|
||||
const root = {
|
||||
querySelectorAll: sel => sel === '[data-bit]' ? [
|
||||
{ checked: true, getAttribute: k => k === 'data-bit' ? '1' : 'Fixed Dialling Numbers (FDN)' },
|
||||
{ checked: false, getAttribute: () => '2' },
|
||||
{ checked: true, getAttribute: k => k === 'data-bit' ? '3' : 'APN Control List (ACL)' },
|
||||
] : [],
|
||||
};
|
||||
assert.deepStrictEqual(efDecCollectValues(root, EF_EST_FIELDS), { services: [
|
||||
{ n: 1, name: 'Fixed Dialling Numbers (FDN)' },
|
||||
{ n: 3, name: 'APN Control List (ACL)' },
|
||||
] });
|
||||
});
|
||||
|
||||
test('the bits filter hides the non-matching rows', () => {
|
||||
const rows = [
|
||||
{ textContent: '1 Local Phone Book', style: {} },
|
||||
{ textContent: '2 Fixed Dialling Numbers', style: {} },
|
||||
];
|
||||
const box = { querySelectorAll: sel => sel === '.pysim-dec-bit-row' ? rows : [] };
|
||||
const input = { value: 'fixed', closest: sel => sel === '.pysim-dec-bits' ? box : null };
|
||||
pysimFsDecBitsFilter(input);
|
||||
assert.strictEqual(rows[0].style.display, 'none');
|
||||
assert.strictEqual(rows[1].style.display, '');
|
||||
});
|
||||
|
||||
test('the select keeps an unknown current value as an option', () => {
|
||||
const model = efDecEditModel('EF.PHASE', '6fae', { kind: 'transparent', data: '0F' });
|
||||
assert.deepStrictEqual(model.values, { phase_hex: '0F' });
|
||||
const out = efDecEditHtml(model, '0F');
|
||||
assert.match(out, /<option value="0F" selected>Current 0F<\/option>/);
|
||||
const known = efDecEditHtml(efDecEditModel('EF.PHASE', '6fae', { kind: 'transparent', data: '03' }), '03');
|
||||
assert.match(known, /<option value="03" selected>phase 2 and PROFILE DOWNLOAD required<\/option>/);
|
||||
// AD unknown op mode
|
||||
const ad = efDecEditHtml(efDecEditModel('EF.AD', '6fad',
|
||||
{ kind: 'transparent', data: '55' + '0000' + '03' + 'AABB' }), '55' + '0000' + '03' + 'AABB');
|
||||
assert.match(ad, /<option value="55" selected>Current 55<\/option>/);
|
||||
assert.match(ad, /data-key="additional_info" data-hex="1" value="0000"/);
|
||||
assert.match(ad, /data-key="mnc_length" value="3"/);
|
||||
});
|
||||
|
||||
test('the hint fields render their help text', () => {
|
||||
const out = efDecEditHtml(efDecEditModel('EF.HPPLMN', '6f31', { kind: 'transparent', data: '2A' }), '2A');
|
||||
assert.match(out, /data-key="interval" data-hex="1" value="0x2A" maxlength="4"/);
|
||||
assert.match(out, /00 = no higher-priority PLMN search/);
|
||||
assert.match(out, /NB-IoT\/EC-GSM-IoT\/eMTC-M1 read the same byte in hours/);
|
||||
});
|
||||
|
||||
test('the wave-2 model hooks shape PHASE/AD/CBMI/CBMIR for the form', () => {
|
||||
assert.deepStrictEqual(efDecEditModel('EF.PHASE', '6fae', { kind: 'transparent', data: '02' }).values,
|
||||
{ phase_hex: '02' });
|
||||
assert.deepStrictEqual(efDecEditModel('EF.AD', '6fad', { kind: 'transparent', data: '80' + '1234' + '02' }).values,
|
||||
{ op_mode: '80', additional_info: '1234', mnc_length: '2' });
|
||||
const cbmi = efDecEditModel('EF.CBMI', '6f45', { kind: 'transparent', data: CBMI_HEX });
|
||||
assert.deepStrictEqual(cbmi.values, { message_ids: [{ id: '0010' }, { id: '0020' }] });
|
||||
const cbmir = efDecEditModel('EF.CBMIR', '6f50', { kind: 'transparent', data: CBMIR_HEX });
|
||||
assert.deepStrictEqual(cbmir.values, { ranges: [{ low: '0000', high: 'FFFE' }] });
|
||||
});
|
||||
|
||||
test('the CB list forms use the row editor with hex ids/bounds', () => {
|
||||
const cbmi = efDecEditModel('EF.CBMI', '6f45', { kind: 'transparent', data: CBMI_HEX });
|
||||
const out = efDecEditHtml(cbmi, CBMI_HEX);
|
||||
assert.strictEqual((out.match(/class="pysim-dec-row /g) || []).length, 2);
|
||||
assert.match(out, /data-key="id" data-hex="1" value="0010"/);
|
||||
assert.match(out, /data-hex="1"/);
|
||||
assert.match(out, />Add message ID</);
|
||||
const cbmir = efDecEditModel('EF.CBMIR', '6f50', { kind: 'transparent', data: CBMIR_HEX });
|
||||
const out2 = efDecEditHtml(cbmir, CBMIR_HEX);
|
||||
assert.strictEqual((out2.match(/class="pysim-dec-row /g) || []).length, 1);
|
||||
assert.match(out2, /data-key="low" data-hex="1" value="0000"/);
|
||||
assert.match(out2, /data-key="high" data-hex="1" value="FFFE"/);
|
||||
assert.match(out2, />Add range</);
|
||||
});
|
||||
|
||||
test('the confirm entries ask once and only for a changed identity', async () => {
|
||||
const decSave = extractFunc(html, 'pysimFsDecSave');
|
||||
assert.ok(decSave.includes('entry.confirm'));
|
||||
assert.ok(decSave.includes('pysimConfirm(t(entry.confirm))'));
|
||||
const entry = efFindEncoder('EF.ICCID', null);
|
||||
pysimFsDecModel = { entry: entry, mode: 'tr' };
|
||||
const makeOut = value => {
|
||||
const root = {
|
||||
getAttribute: k => k === 'data-orig' ? ICCID_HEX : null,
|
||||
querySelector: sel => sel === '[data-key="iccid"]' ? { value: value } : null,
|
||||
};
|
||||
return { querySelector: sel => sel === '.pysim-dec-edit' ? root : null, querySelectorAll: () => [] };
|
||||
};
|
||||
// unchanged -> no dialog
|
||||
let asked = 0;
|
||||
globalThis.pysimConfirm = async () => { asked++; return true; };
|
||||
let stubs = saveStubs({ success: true, sw: '9000' });
|
||||
let status = { textContent: '' };
|
||||
await pysimFsDecSave(null, 'EF.ICCID', status, makeOut('8988211000000430010'), false);
|
||||
assert.strictEqual(asked, 0);
|
||||
assert.strictEqual(stubs.bodies.length, 1);
|
||||
// changed + cancel -> nothing written
|
||||
globalThis.pysimConfirm = async () => { asked++; return false; };
|
||||
stubs = saveStubs({ success: true, sw: '9000' });
|
||||
status = { textContent: '' };
|
||||
await pysimFsDecSave(null, 'EF.ICCID', status, makeOut('8988211000000430011'), false);
|
||||
assert.strictEqual(asked, 1);
|
||||
assert.strictEqual(stubs.bodies.length, 0);
|
||||
assert.match(status.textContent, /Write cancelled/);
|
||||
// changed + accept -> written
|
||||
globalThis.pysimConfirm = async () => { asked++; return true; };
|
||||
stubs = saveStubs({ success: true, sw: '9000' });
|
||||
status = { textContent: '' };
|
||||
await pysimFsDecSave(null, 'EF.ICCID', status, makeOut('8988211000000430011'), false);
|
||||
assert.strictEqual(asked, 2);
|
||||
assert.strictEqual(stubs.bodies.length, 1);
|
||||
assert.strictEqual(stubs.bodies[0].body.data, '988812010000400310F1');
|
||||
});
|
||||
|
||||
// Finding 1: the list save passes the collected row ARRAY; the encoders must
|
||||
// not wipe the file when nothing was edited.
|
||||
const PLMN_ONLY = '62F21022F860';
|
||||
const FPLMN_HOLES = '22F860FFFFFF62F210';
|
||||
|
||||
function listSaveRoot(orig, rows) {
|
||||
const rowEls = rows.map(vals => ({
|
||||
querySelector: sel => {
|
||||
const m = /^\[data-key="(.+)"\]$/.exec(sel);
|
||||
return m && vals[m[1]] !== undefined ? { value: vals[m[1]] } : null;
|
||||
},
|
||||
querySelectorAll: () => [],
|
||||
}));
|
||||
return {
|
||||
getAttribute: k => k === 'data-orig' ? orig : null,
|
||||
querySelectorAll: sel => sel === '.pysim-dec-row' ? rowEls : [],
|
||||
};
|
||||
}
|
||||
|
||||
test('F1: a list save with no edits keeps the original bytes', async () => {
|
||||
const cases = [
|
||||
{ name: 'EF.PLMNsel', fid: '6f30', orig: PLMN_ONLY,
|
||||
rows: [{ mcc: '262', mnc: '01' }, { mcc: '228', mnc: '06' }] },
|
||||
{ name: 'EF.FPLMN', fid: '6f7b', orig: FPLMN_HOLES,
|
||||
rows: [{ mcc: '228', mnc: '06' }, { mcc: '262', mnc: '01' }] },
|
||||
{ name: 'EF.EHPLMN', fid: '6fd9', orig: PLMN_ONLY,
|
||||
rows: [{ mcc: '262', mnc: '01' }, { mcc: '228', mnc: '06' }] },
|
||||
{ name: 'EF.CBMI', fid: '6f45', orig: CBMI_HEX, rows: [{ id: '0010' }, { id: '0020' }] },
|
||||
{ name: 'EF.CBMID', fid: '6f48', orig: CBMI_HEX, rows: [{ id: '0010' }, { id: '0020' }] },
|
||||
{ name: 'EF.CBMIR', fid: '6f50', orig: CBMIR_HEX, rows: [{ low: '0000', high: 'FFFE' }] },
|
||||
];
|
||||
for (const c of cases) {
|
||||
pysimFsDecModel = { entry: efFindEncoder(c.name, c.fid), mode: 'tr' };
|
||||
const out = {
|
||||
querySelector: sel => sel === '.pysim-dec-edit' ? listSaveRoot(c.orig, c.rows) : null,
|
||||
querySelectorAll: () => [],
|
||||
};
|
||||
const stubs = saveStubs({ success: true, sw: '9000' });
|
||||
const status = { textContent: '' };
|
||||
await pysimFsDecSave(null, c.name, status, out, false);
|
||||
assert.strictEqual(stubs.bodies.length, 1, c.name);
|
||||
assert.strictEqual(stubs.bodies[0].body.data, c.orig,
|
||||
c.name + ': a no-edit list save must not wipe the file');
|
||||
}
|
||||
});
|
||||
|
||||
test('F1: an edited list row is written through the same save path', async () => {
|
||||
const cases = [
|
||||
{ name: 'EF.PLMNsel', fid: '6f30', orig: PLMN_ONLY,
|
||||
rows: [{ mcc: '262', mnc: '01' }, { mcc: '228', mnc: '07' }] },
|
||||
{ name: 'EF.CBMI', fid: '6f45', orig: CBMI_HEX,
|
||||
rows: [{ id: '0010' }, { id: '0030' }] },
|
||||
{ name: 'EF.CBMIR', fid: '6f50', orig: CBMIR_HEX,
|
||||
rows: [{ low: '0000', high: 'FF00' }] },
|
||||
];
|
||||
for (const c of cases) {
|
||||
const entry = efFindEncoder(c.name, c.fid);
|
||||
pysimFsDecModel = { entry: entry, mode: 'tr' };
|
||||
const out = {
|
||||
querySelector: sel => sel === '.pysim-dec-edit' ? listSaveRoot(c.orig, c.rows) : null,
|
||||
querySelectorAll: () => [],
|
||||
};
|
||||
const stubs = saveStubs({ success: true, sw: '9000' });
|
||||
const status = { textContent: '' };
|
||||
await pysimFsDecSave(null, c.name, status, out, false);
|
||||
const expected = entry.enc(c.rows, c.orig);
|
||||
assert.notStrictEqual(expected, c.orig, c.name + ': the edit must change the bytes');
|
||||
assert.strictEqual(stubs.bodies.length, 1, c.name);
|
||||
assert.strictEqual(stubs.bodies[0].body.data, expected, c.name);
|
||||
}
|
||||
});
|
||||
|
||||
test('F5: an untouched all-FF AD saves FF through the collected form value', async () => {
|
||||
const entry = efFindEncoder('EF.AD', null);
|
||||
const model = efDecEditModel('EF.AD', null, { kind: 'transparent', data: 'FF'.repeat(6) });
|
||||
assert.deepStrictEqual(model.values, { op_mode: 'FF', additional_info: '', mnc_length: '' });
|
||||
const form = efDecEditHtml(model, 'FF'.repeat(6));
|
||||
assert.match(form, /<option value="FF" selected>Current FF<\/option>/);
|
||||
pysimFsDecModel = model;
|
||||
const vals = { op_mode: 'FF', additional_info: '', mnc_length: '' };
|
||||
const root = {
|
||||
getAttribute: k => k === 'data-orig' ? 'FF'.repeat(6) : null,
|
||||
querySelector: sel => {
|
||||
const m = /^\[data-key="(.+)"\]$/.exec(sel);
|
||||
return m ? { value: vals[m[1]] } : null;
|
||||
},
|
||||
};
|
||||
const out = { querySelector: sel => sel === '.pysim-dec-edit' ? root : null, querySelectorAll: () => [] };
|
||||
const stubs = saveStubs({ success: true, sw: '9000' });
|
||||
const status = { textContent: '' };
|
||||
await pysimFsDecSave(null, 'EF.AD', status, out, false);
|
||||
assert.strictEqual(stubs.bodies.length, 1);
|
||||
assert.strictEqual(stubs.bodies[0].body.data, 'FF'.repeat(6));
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user