From 16c183f79cdf31dbcd3231167c6470c272a41154 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: Thu, 10 Sep 2026 23:04:17 +0300 Subject: [PATCH] profiler: mask-first-4-bytes options + lock scan form during scan MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Profile-from-card scan form gains two checked-by-default options 'Match first 4 bytes for EF.IMSI / EF.ICCID', surfacing the previously hardcoded mask behavior. Unchecking captures those files' contents exactly. profilerBuildFileRule now takes a maskFids set (undefined keeps the legacy mask default); scan start collects it and threads it through profilerScanCard. While a scan runs the options form (ignore list, mask options, FCP/FCI selector) is hidden, Cancel and Scan are disabled (with visible disabled styling), and the profile-name input becomes read-only — fixing the latent cancel-mid-scan bug that left the scan running and popped the editor open. profilerFromCard performs a full fresh-open reset. 4 new tests (mask/exact/legacy + scan threading). RU i18n key, help docs synced. SW cache v57 -> v58. --- frontend/help-ru.html | 2 +- frontend/help.html | 2 +- frontend/index.html | 64 +++++++++++++++++++++++++-------- frontend/sw.js | 2 +- frontend/tests/profiler.test.js | 40 +++++++++++++++++++++ 5 files changed, 92 insertions(+), 18 deletions(-) diff --git a/frontend/help-ru.html b/frontend/help-ru.html index 7c4e17e..2ce7251 100644 --- a/frontend/help-ru.html +++ b/frontend/help-ru.html @@ -381,7 +381,7 @@

Check выполняет каждое правило на подключённой карте и показывает строку прогресса и отчёт прохождения. Рядом с путём файла указывается, что именно проверялось (например, тип файла и размер, содержимое или точный FCI); если часть проверок прошла, а часть нет — каждый аспект помечается (тип файла ✓, размер ✗, содержимое ✓), а расхождения расписываются ниже. Для record-файлов при расхождении содержимого добавляется пометка совпадающие записи: 1-5, 7-10 со списком записей, которые совпали.

Опции сканирования «Profile from card»

-

Диалог сканирования запрашивает имя профиля и предлагает селектор «FCP/FCI check» (те же три режима, по умолчанию Filetype + size), применяемый ко всем создаваемым правилам, а также список «Ignore contents of» (все отмечены по умолчанию) часто перезаписываемых файлов, содержимое которых пропускается: EF.LOCI, EF.PSLOCI, EF.EPSLOCI, EF.5GS3GPPLOCI, EF.Keys, EF.KeysPS, EF.SMS, EF.Kc, EF.KcGPRS, EF.LOCIGPRS, EF.CBMID, EF.SMSS. Строка прогресса показывает N / всего файлов с текущим путём файла во время сканирования. Правила создаются только для файлов, которые реально существуют на карте (возвращён FCP-шаблон); отсутствующие файлы пропускаются. Пользовательские файлы из подвкладки Custom files включаются с той же проверкой существования.

+

Диалог сканирования запрашивает имя профиля и предлагает селектор «FCP/FCI check» (те же три режима, по умолчанию Filetype + size), применяемый ко всем создаваемым правилам, а также список «Ignore contents of» (все отмечены по умолчанию) часто перезаписываемых файлов, содержимое которых пропускается: EF.LOCI, EF.PSLOCI, EF.EPSLOCI, EF.5GS3GPPLOCI, EF.Keys, EF.KeysPS, EF.SMS, EF.Kc, EF.KcGPRS, EF.LOCIGPRS, EF.CBMID, EF.SMSS. Ещё две отмеченные по умолчанию опции «Match first 4 bytes for» EF.IMSI и EF.ICCID захватывают содержимое этих файлов как маску только первых 4 байт (снимите для точного сравнения). Строка прогресса показывает N / всего файлов с текущим путём файла во время сканирования; при сканировании опции скрываются, а кнопки блокируются. Правила создаются только для файлов, которые реально существуют на карте (возвращён FCP-шаблон); отсутствующие файлы пропускаются. Пользовательские файлы из подвкладки Custom files включаются с той же проверкой существования.

5.7 Сценарии использования

diff --git a/frontend/help.html b/frontend/help.html index 510910c..886dd3b 100644 --- a/frontend/help.html +++ b/frontend/help.html @@ -381,7 +381,7 @@

Check runs every rule against the equipped card and shows a live progress line plus a pass/fail report. Each row states exactly what was verified next to the file path (e.g. filetype and size, contents or exact FCI); when some checks pass and others fail, each aspect is marked (filetype ✓, size ✗, contents ✓) with the mismatches detailed below. For record files with a contents mismatch, a matching records: 1-5, 7-10 note lists the records that did match.

“Profile from card” scan options

-

The scan dialog asks for a profile name and offers a “FCP/FCI check” selector (the same three modes above, default Filetype + size) applied to every generated rule, plus an “Ignore contents of” checklist (all checked by default) of frequently-overwritten files whose contents are skipped: EF.LOCI, EF.PSLOCI, EF.EPSLOCI, EF.5GS3GPPLOCI, EF.Keys, EF.KeysPS, EF.SMS, EF.Kc, EF.KcGPRS, EF.LOCIGPRS, EF.CBMID, EF.SMSS. A progress line shows N / total files with the current file path while scanning. Rules are created only for files that actually exist on the card (a FCP template is returned); missing files are skipped. Custom files from the Custom files sub-tab are included under the same existence check.

+

The scan dialog asks for a profile name and offers a “FCP/FCI check” selector (the same three modes above, default Filetype + size) applied to every generated rule, plus an “Ignore contents of” checklist (all checked by default) of frequently-overwritten files whose contents are skipped: EF.LOCI, EF.PSLOCI, EF.EPSLOCI, EF.5GS3GPPLOCI, EF.Keys, EF.KeysPS, EF.SMS, EF.Kc, EF.KcGPRS, EF.LOCIGPRS, EF.CBMID, EF.SMSS. Two further checked-by-default options “Match first 4 bytes for” EF.IMSI and EF.ICCID capture those files’ contents as a mask of only the first 4 bytes (uncheck for exact matching). A progress line shows N / total files with the current file path while scanning; during the scan the options are hidden and the buttons are locked. Rules are created only for files that actually exist on the card (a FCP template is returned); missing files are skipped. Custom files from the Custom files sub-tab are included under the same existence check.

5.7 Usage scenarios

diff --git a/frontend/index.html b/frontend/index.html index 04b1b7b..d74ef35 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -912,19 +912,22 @@

Profile from card

-
Ignore contents of:
-
- - +
+
Ignore contents of:
+
+
+ + +
- - + +
@@ -7095,6 +7098,8 @@ function profilerNew() { function profilerFromCard() { document.getElementById('profiler-scan-name').value = ''; + document.getElementById('profiler-scan-name').readOnly = false; + document.getElementById('profiler-scan-name').classList.remove('opacity-50'); const ignore = document.getElementById('profiler-scan-ignore'); let html = ''; for (const f of PROFILER_IGNORE_FILES) { @@ -7103,7 +7108,20 @@ function profilerFromCard() { '' + esc(t('Ignore contents of')) + ' ' + esc(f.name) + ''; } ignore.innerHTML = html; + const mask = document.getElementById('profiler-scan-mask'); + let mhtml = ''; + for (const fid in PROFILER_MASK_PREFIX4_FIDS) { + mhtml += ''; + } + mask.innerHTML = mhtml; + document.getElementById('profiler-scan-options').classList.remove('hidden'); + document.getElementById('profiler-scan-progress').classList.add('hidden'); document.getElementById('profiler-scan-error').classList.add('hidden'); + document.getElementById('profiler-scan-cancel-btn').disabled = false; + document.getElementById('profiler-scan-btn').disabled = false; + document.getElementById('profiler-scan-btn').textContent = t('Scan'); document.getElementById('profiler-scan-modal').classList.remove('hidden'); } @@ -7127,9 +7145,19 @@ async function profilerScanStart() { ignoreNames.add((cb.getAttribute('data-ignore-name') || '').toUpperCase()); } }); + const maskFids = new Set(); + document.querySelectorAll('#profiler-scan-mask input[data-mask-fid]').forEach(cb => { + if (cb.checked) maskFids.add(cb.getAttribute('data-mask-fid').toUpperCase()); + }); const fciMode = document.getElementById('profiler-scan-fci-mode').value; const btn = document.getElementById('profiler-scan-btn'); + const cancelBtn = document.getElementById('profiler-scan-cancel-btn'); + const nameEl = document.getElementById('profiler-scan-name'); const progEl = document.getElementById('profiler-scan-progress'); + document.getElementById('profiler-scan-options').classList.add('hidden'); + cancelBtn.disabled = true; + nameEl.readOnly = true; + nameEl.classList.add('opacity-50'); progEl.classList.remove('hidden'); progEl.textContent = t('Discovering...'); btn.disabled = true; @@ -7137,7 +7165,7 @@ async function profilerScanStart() { try { const rules = await profilerScanCard(ignoreFids, ignoreNames, fciMode, (done, total, path) => { progEl.textContent = done + ' / ' + total + ' ' + t('files') + (path ? ' — ' + path : ''); - }); + }, maskFids); const profile = { id: profilerNewId(), name: name, created: new Date().toISOString(), rules: rules }; profiles.push(profile); profilerSave(); @@ -7147,6 +7175,10 @@ async function profilerScanStart() { errEl.textContent = e.message; errEl.classList.remove('hidden'); } finally { + document.getElementById('profiler-scan-options').classList.remove('hidden'); + cancelBtn.disabled = false; + nameEl.readOnly = false; + nameEl.classList.remove('opacity-50'); progEl.classList.add('hidden'); btn.disabled = false; btn.textContent = t('Scan'); @@ -7157,7 +7189,7 @@ async function profilerScanStart() { // Phase 1 discovers every file (tree calls only) to know the total; phase 2 // builds a rule per file, reporting progress via the optional onProgress // callback as onProgress(done, total, path). -async function profilerScanCard(ignoreFids, ignoreNames, fciMode, onProgress) { +async function profilerScanCard(ignoreFids, ignoreNames, fciMode, onProgress, maskFids) { const rules = []; const files = []; const seen = new Set(); @@ -7211,13 +7243,13 @@ async function profilerScanCard(ignoreFids, ignoreNames, fciMode, onProgress) { for (let i = 0; i < files.length; i++) { const entry = files[i]; if (onProgress) onProgress(i + 1, total, entry.path); - const rule = await profilerBuildFileRule(entry.path, entry.child, ignoreFids, ignoreNames, fciMode); + const rule = await profilerBuildFileRule(entry.path, entry.child, ignoreFids, ignoreNames, fciMode, maskFids); if (rule) rules.push(rule); } return rules; } -async function profilerBuildFileRule(path, c, ignoreFids, ignoreNames, fciMode) { +async function profilerBuildFileRule(path, c, ignoreFids, ignoreNames, fciMode, maskFids) { let sel; try { sel = await pysimFetch('/api/select', { path: path }); @@ -7248,7 +7280,8 @@ async function profilerBuildFileRule(path, c, ignoreFids, ignoreNames, fciMode) records: rd.records.map(r => ({ num: r.num, data: r.data })), }; } else if (rd.data) { - if (PROFILER_MASK_PREFIX4_FIDS[fid]) { + const useMask = maskFids ? maskFids.has(fid) : !!PROFILER_MASK_PREFIX4_FIDS[fid]; + if (useMask) { rule.content = { mode: 'mask', kind: 'transparent', expected: profilerMaskPrefix4(rd.data) }; } else { rule.content = { mode: 'exact', kind: 'transparent', expected: rd.data }; @@ -7939,6 +7972,7 @@ const LANG_RU = { 'files': 'файлов', 'Ignore contents of:': 'Игнорировать содержимое:', 'Ignore contents of': 'Игнорировать содержимое', + 'Match first 4 bytes for': 'Сравнивать первые 4 байта для', 'FCP/FCI check': 'Проверка FCP/FCI', 'Filetype only (FCP)': 'Только тип файла (FCP)', 'Filetype + size (FCP)': 'Тип файла + размер (FCP)', diff --git a/frontend/sw.js b/frontend/sw.js index 2f59ea5..93541b0 100644 --- a/frontend/sw.js +++ b/frontend/sw.js @@ -1,4 +1,4 @@ -const CACHE = 'otaman-v57'; +const CACHE = 'otaman-v58'; const URLS = [ 'index.html', 'help.html', diff --git a/frontend/tests/profiler.test.js b/frontend/tests/profiler.test.js index 3eeee43..6388e66 100644 --- a/frontend/tests/profiler.test.js +++ b/frontend/tests/profiler.test.js @@ -366,6 +366,46 @@ test('profilerScanCard without onProgress still works (back-compat)', async () = assert.strictEqual(rules.length, 1); }); +// --- mask first 4 bytes (EF.IMSI / EF.ICCID) --- + +const IMSI_SELECT = { name: 'EF.IMSI', fid: '6F07', file_type: 'transparent', file_size: 9, record_len: null, num_of_rec: null, exists: true }; +const IMSI_DATA = '082905911234567890'; + +test('profilerBuildFileRule masks first 4 bytes when the FID is in maskFids', async () => { + mockFetch({ '/api/select': () => IMSI_SELECT, '/api/read': () => ({ success: true, data: IMSI_DATA }) }); + const rule = await profilerBuildFileRule('MF/6F07', { fid: '6F07', name: 'EF.IMSI' }, new Set(), new Set(), 'type_size', new Set(['6F07'])); + assert.strictEqual(rule.content.mode, 'mask'); + assert.strictEqual(rule.content.expected, '08290591??????????'); +}); + +test('profilerBuildFileRule uses exact contents when maskFids does not contain the FID', async () => { + mockFetch({ '/api/select': () => IMSI_SELECT, '/api/read': () => ({ success: true, data: IMSI_DATA }) }); + const rule = await profilerBuildFileRule('MF/6F07', { fid: '6F07', name: 'EF.IMSI' }, new Set(), new Set(), 'type_size', new Set()); + assert.strictEqual(rule.content.mode, 'exact'); + assert.strictEqual(rule.content.expected, IMSI_DATA); +}); + +test('profilerBuildFileRule keeps the legacy mask default when maskFids is omitted', async () => { + mockFetch({ '/api/select': () => IMSI_SELECT, '/api/read': () => ({ success: true, data: IMSI_DATA }) }); + const rule = await profilerBuildFileRule('MF/6F07', { fid: '6F07', name: 'EF.IMSI' }, new Set(), new Set(), 'type_size'); + assert.strictEqual(rule.content.mode, 'mask'); + assert.strictEqual(rule.content.expected, '08290591??????????'); +}); + +test('profilerScanCard threads maskFids through to rule building', async () => { + global.pysimCustomFiles = []; + global.pysimFetch = async (path, body) => { + if (path === '/api/tree') return { exists: true, name: 'MF', children: [{ name: 'EF.IMSI', fid: '6f07', isDir: false }] }; + if (path === '/api/select') return IMSI_SELECT; + if (path === '/api/read') return { success: true, data: IMSI_DATA }; + throw new Error('unexpected fetch: ' + path); + }; + const rules = await profilerScanCard(new Set(), new Set(), 'type_size', undefined, new Set()); + assert.strictEqual(rules.length, 1); + assert.strictEqual(rules[0].content.mode, 'exact'); + assert.strictEqual(rules[0].content.expected, IMSI_DATA); +}); + // --- result report (aspects, summary, matching records) --- test('profilerNumRanges compresses consecutive record numbers', () => {