profiler: mask-first-4-bytes options + lock scan form during scan

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.
This commit is contained in:
2026-09-10 23:04:17 +03:00
parent d7204b3e96
commit 16c183f79c
5 changed files with 92 additions and 18 deletions
+1 -1
View File
@@ -381,7 +381,7 @@
</ul>
<p class="text-sm mb-3"><strong>Check</strong> выполняет каждое правило на подключённой карте и показывает строку прогресса и отчёт прохождения. Рядом с путём файла указывается, что именно проверялось (например, <em>тип файла и размер, содержимое</em> или <em>точный FCI</em>); если часть проверок прошла, а часть нет — каждый аспект помечается (<em>тип файла ✓, размер ✗, содержимое ✓</em>), а расхождения расписываются ниже. Для record-файлов при расхождении содержимого добавляется пометка <em>совпадающие записи: 1-5, 7-10</em> со списком записей, которые совпали.</p>
<h4 class="font-medium mb-1">Опции сканирования &laquo;Profile from card&raquo;</h4>
<p class="text-sm mb-2">Диалог сканирования запрашивает имя профиля и предлагает селектор <strong>&laquo;FCP/FCI check&raquo;</strong> (те же три режима, по умолчанию <strong>Filetype + size</strong>), применяемый ко всем создаваемым правилам, а также список <strong>&laquo;Ignore contents of&raquo;</strong> (все отмечены по умолчанию) часто перезаписываемых файлов, содержимое которых пропускается: <code class="font-mono text-sm">EF.LOCI</code>, <code class="font-mono text-sm">EF.PSLOCI</code>, <code class="font-mono text-sm">EF.EPSLOCI</code>, <code class="font-mono text-sm">EF.5GS3GPPLOCI</code>, <code class="font-mono text-sm">EF.Keys</code>, <code class="font-mono text-sm">EF.KeysPS</code>, <code class="font-mono text-sm">EF.SMS</code>, <code class="font-mono text-sm">EF.Kc</code>, <code class="font-mono text-sm">EF.KcGPRS</code>, <code class="font-mono text-sm">EF.LOCIGPRS</code>, <code class="font-mono text-sm">EF.CBMID</code>, <code class="font-mono text-sm">EF.SMSS</code>. Строка прогресса показывает <em>N / всего файлов</em> с текущим путём файла во время сканирования. Правила создаются только для файлов, которые реально существуют на карте (возвращён FCP-шаблон); отсутствующие файлы пропускаются. Пользовательские файлы из подвкладки <strong>Custom files</strong> включаются с той же проверкой существования.</p>
<p class="text-sm mb-2">Диалог сканирования запрашивает имя профиля и предлагает селектор <strong>&laquo;FCP/FCI check&raquo;</strong> (те же три режима, по умолчанию <strong>Filetype + size</strong>), применяемый ко всем создаваемым правилам, а также список <strong>&laquo;Ignore contents of&raquo;</strong> (все отмечены по умолчанию) часто перезаписываемых файлов, содержимое которых пропускается: <code class="font-mono text-sm">EF.LOCI</code>, <code class="font-mono text-sm">EF.PSLOCI</code>, <code class="font-mono text-sm">EF.EPSLOCI</code>, <code class="font-mono text-sm">EF.5GS3GPPLOCI</code>, <code class="font-mono text-sm">EF.Keys</code>, <code class="font-mono text-sm">EF.KeysPS</code>, <code class="font-mono text-sm">EF.SMS</code>, <code class="font-mono text-sm">EF.Kc</code>, <code class="font-mono text-sm">EF.KcGPRS</code>, <code class="font-mono text-sm">EF.LOCIGPRS</code>, <code class="font-mono text-sm">EF.CBMID</code>, <code class="font-mono text-sm">EF.SMSS</code>. Ещё две отмеченные по умолчанию опции <strong>&laquo;Match first 4 bytes for&raquo;</strong> <code class="font-mono text-sm">EF.IMSI</code> и <code class="font-mono text-sm">EF.ICCID</code> захватывают содержимое этих файлов как маску только первых 4 байт (снимите для точного сравнения). Строка прогресса показывает <em>N / всего файлов</em> с текущим путём файла во время сканирования; при сканировании опции скрываются, а кнопки блокируются. Правила создаются только для файлов, которые реально существуют на карте (возвращён FCP-шаблон); отсутствующие файлы пропускаются. Пользовательские файлы из подвкладки <strong>Custom files</strong> включаются с той же проверкой существования.</p>
<h3 id="usage-scenarios" class="text-lg font-medium mb-2">5.7 Сценарии использования</h3>
+1 -1
View File
@@ -381,7 +381,7 @@
</ul>
<p class="text-sm mb-3"><strong>Check</strong> 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. <em>filetype and size, contents</em> or <em>exact FCI</em>); when some checks pass and others fail, each aspect is marked (<em>filetype ✓, size ✗, contents ✓</em>) with the mismatches detailed below. For record files with a contents mismatch, a <em>matching records: 1-5, 7-10</em> note lists the records that did match.</p>
<h4 class="font-medium mb-1">&ldquo;Profile from card&rdquo; scan options</h4>
<p class="text-sm mb-2">The scan dialog asks for a profile name and offers a <strong>&ldquo;FCP/FCI check&rdquo;</strong> selector (the same three modes above, default <strong>Filetype + size</strong>) applied to every generated rule, plus an <strong>&ldquo;Ignore contents of&rdquo;</strong> checklist (all checked by default) of frequently-overwritten files whose contents are skipped: <code class="font-mono text-sm">EF.LOCI</code>, <code class="font-mono text-sm">EF.PSLOCI</code>, <code class="font-mono text-sm">EF.EPSLOCI</code>, <code class="font-mono text-sm">EF.5GS3GPPLOCI</code>, <code class="font-mono text-sm">EF.Keys</code>, <code class="font-mono text-sm">EF.KeysPS</code>, <code class="font-mono text-sm">EF.SMS</code>, <code class="font-mono text-sm">EF.Kc</code>, <code class="font-mono text-sm">EF.KcGPRS</code>, <code class="font-mono text-sm">EF.LOCIGPRS</code>, <code class="font-mono text-sm">EF.CBMID</code>, <code class="font-mono text-sm">EF.SMSS</code>. A progress line shows <em>N / total files</em> 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 <strong>Custom files</strong> sub-tab are included under the same existence check.</p>
<p class="text-sm mb-2">The scan dialog asks for a profile name and offers a <strong>&ldquo;FCP/FCI check&rdquo;</strong> selector (the same three modes above, default <strong>Filetype + size</strong>) applied to every generated rule, plus an <strong>&ldquo;Ignore contents of&rdquo;</strong> checklist (all checked by default) of frequently-overwritten files whose contents are skipped: <code class="font-mono text-sm">EF.LOCI</code>, <code class="font-mono text-sm">EF.PSLOCI</code>, <code class="font-mono text-sm">EF.EPSLOCI</code>, <code class="font-mono text-sm">EF.5GS3GPPLOCI</code>, <code class="font-mono text-sm">EF.Keys</code>, <code class="font-mono text-sm">EF.KeysPS</code>, <code class="font-mono text-sm">EF.SMS</code>, <code class="font-mono text-sm">EF.Kc</code>, <code class="font-mono text-sm">EF.KcGPRS</code>, <code class="font-mono text-sm">EF.LOCIGPRS</code>, <code class="font-mono text-sm">EF.CBMID</code>, <code class="font-mono text-sm">EF.SMSS</code>. Two further checked-by-default options <strong>&ldquo;Match first 4 bytes for&rdquo;</strong> <code class="font-mono text-sm">EF.IMSI</code> and <code class="font-mono text-sm">EF.ICCID</code> capture those files&rsquo; contents as a mask of only the first 4 bytes (uncheck for exact matching). A progress line shows <em>N / total files</em> 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 <strong>Custom files</strong> sub-tab are included under the same existence check.</p>
<h3 id="usage-scenarios" class="text-lg font-medium mb-2">5.7 Usage scenarios</h3>
+49 -15
View File
@@ -912,19 +912,22 @@
<h3 class="text-lg font-semibold mb-4 text-gray-800 dark:text-slate-200" data-l10n="Profile from card">Profile from card</h3>
<label class="block mb-1 text-xs font-medium text-gray-500 dark:text-slate-400" data-l10n="Profile name">Profile name</label>
<input id="profiler-scan-name" class="w-full font-mono border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-2 dark:bg-slate-800 mb-3" placeholder="Profile name">
<div class="mb-1 text-xs font-medium text-gray-500 dark:text-slate-400" data-l10n="Ignore contents of:">Ignore contents of:</div>
<div id="profiler-scan-ignore" class="space-y-1 mb-3 max-h-56 overflow-auto"></div>
<label class="block mb-1 text-xs font-medium text-gray-500 dark:text-slate-400" data-l10n="FCP/FCI check">FCP/FCI check</label>
<select id="profiler-scan-fci-mode" class="w-full border border-gray-300 dark:border-slate-600 text-sm rounded px-2 py-1.5 dark:bg-slate-800 mb-3">
<option value="type" data-l10n="Filetype only (FCP)">Filetype only (FCP)</option>
<option value="type_size" selected data-l10n="Filetype + size (FCP)">Filetype + size (FCP)</option>
<option value="exact" data-l10n="Exact FCI">Exact FCI</option>
</select>
<div id="profiler-scan-options">
<div class="mb-1 text-xs font-medium text-gray-500 dark:text-slate-400" data-l10n="Ignore contents of:">Ignore contents of:</div>
<div id="profiler-scan-ignore" class="space-y-1 mb-3 max-h-56 overflow-auto"></div>
<div id="profiler-scan-mask" class="space-y-1 mb-3"></div>
<label class="block mb-1 text-xs font-medium text-gray-500 dark:text-slate-400" data-l10n="FCP/FCI check">FCP/FCI check</label>
<select id="profiler-scan-fci-mode" class="w-full border border-gray-300 dark:border-slate-600 text-sm rounded px-2 py-1.5 dark:bg-slate-800 mb-3">
<option value="type" data-l10n="Filetype only (FCP)">Filetype only (FCP)</option>
<option value="type_size" selected data-l10n="Filetype + size (FCP)">Filetype + size (FCP)</option>
<option value="exact" data-l10n="Exact FCI">Exact FCI</option>
</select>
</div>
<div id="profiler-scan-progress" class="text-xs text-gray-500 dark:text-slate-400 mb-2 hidden"></div>
<div id="profiler-scan-error" class="text-xs text-red-500 hidden mb-2"></div>
<div class="flex gap-2 justify-end">
<button onclick="profilerScanCancel()" class="px-3 py-1.5 text-xs rounded border border-gray-300 dark:border-slate-600 hover:bg-gray-100 dark:hover:bg-slate-700 text-gray-700 dark:text-slate-300" data-l10n="Cancel">Cancel</button>
<button id="profiler-scan-btn" onclick="profilerScanStart()" class="px-4 py-1.5 text-xs rounded bg-blue-600 text-white hover:bg-blue-700" data-l10n="Scan">Scan</button>
<button id="profiler-scan-cancel-btn" onclick="profilerScanCancel()" class="px-3 py-1.5 text-xs rounded border border-gray-300 dark:border-slate-600 hover:bg-gray-100 dark:hover:bg-slate-700 text-gray-700 dark:text-slate-300 disabled:opacity-40 disabled:cursor-not-allowed" data-l10n="Cancel">Cancel</button>
<button id="profiler-scan-btn" onclick="profilerScanStart()" class="px-4 py-1.5 text-xs rounded bg-blue-600 text-white hover:bg-blue-700 disabled:opacity-40 disabled:cursor-not-allowed" data-l10n="Scan">Scan</button>
</div>
</div>
</div>
@@ -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() {
'<span>' + esc(t('Ignore contents of')) + ' ' + esc(f.name) + '</span></label>';
}
ignore.innerHTML = html;
const mask = document.getElementById('profiler-scan-mask');
let mhtml = '';
for (const fid in PROFILER_MASK_PREFIX4_FIDS) {
mhtml += '<label class="flex items-center gap-2 text-sm">' +
'<input type="checkbox" data-mask-fid="' + fid + '" checked>' +
'<span>' + esc(t('Match first 4 bytes for')) + ' ' + esc(PROFILER_MASK_PREFIX4_FIDS[fid]) + '</span></label>';
}
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)',
+1 -1
View File
@@ -1,4 +1,4 @@
const CACHE = 'otaman-v57';
const CACHE = 'otaman-v58';
const URLS = [
'index.html',
'help.html',
+40
View File
@@ -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', () => {