From 5355a62d88b7315d171af54536504d98490dac3d 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 21:49:51 +0300 Subject: [PATCH] profiler: apply 'ignore contents' to real card files (KcGPRS FID fix + name fallback) The 'Profile from card' ignore checkboxes matched only by FID, but the list had EF.KcGPRS at 4F52 (TS 31.102 DF.GSM-ACCESS) while the live card exposes it at 6F52 (TS 51.011 DF.GSM, verified in TS 51.011 v4.15.0 10.3.32 and the DF.GSM allocation table). The miss made profilerBuildFileRule capture full contents (Exact) for a file the user had checked to ignore. - Correct EF.KcGPRS FID to 6F52 (spec-verified) - Match ignores by FID OR by pySim name: each checkbox now carries data-ignore-name, profilerScanStart builds an ignoreNames set, and profilerBuildFileRule checks both. Covers FID variants (6F52 vs 4F52) and future constant typos; ignoreNames is optional for back-compat. - 6 new tests: ignore-list FID/name sanity (incl. KcGPRS=6F52), duplicates, ignored-by-FID, ignored-by-name-only regression case, non-ignored control, back-compat when ignoreNames is omitted. SW cache v50 -> v51. --- frontend/index.html | 23 +++++---- frontend/sw.js | 2 +- frontend/tests/profiler.test.js | 88 ++++++++++++++++++++++++++++++++- 3 files changed, 101 insertions(+), 12 deletions(-) diff --git a/frontend/index.html b/frontend/index.html index 2817ba7..3686c31 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -6946,7 +6946,7 @@ const PROFILER_IGNORE_FILES = [ { fid: '6F09', name: 'EF.KeysPS' }, { fid: '6F3C', name: 'EF.SMS' }, { fid: '6F20', name: 'EF.Kc' }, - { fid: '4F52', name: 'EF.KcGPRS' }, + { fid: '6F52', name: 'EF.KcGPRS' }, { fid: '6F53', name: 'EF.LOCIGPRS' }, { fid: '6F48', name: 'EF.CBMID' }, { fid: '6F43', name: 'EF.SMSS' }, @@ -7086,7 +7086,7 @@ function profilerFromCard() { let html = ''; for (const f of PROFILER_IGNORE_FILES) { html += ''; } ignore.innerHTML = html; @@ -7107,14 +7107,18 @@ async function profilerScanStart() { return; } const ignoreFids = new Set(); + const ignoreNames = new Set(); document.querySelectorAll('#profiler-scan-ignore input[data-ignore-fid]').forEach(cb => { - if (cb.checked) ignoreFids.add(cb.getAttribute('data-ignore-fid').toUpperCase()); + if (cb.checked) { + ignoreFids.add(cb.getAttribute('data-ignore-fid').toUpperCase()); + ignoreNames.add((cb.getAttribute('data-ignore-name') || '').toUpperCase()); + } }); const btn = document.getElementById('profiler-scan-btn'); btn.disabled = true; btn.textContent = t('Scanning...'); try { - const rules = await profilerScanCard(ignoreFids); + const rules = await profilerScanCard(ignoreFids, ignoreNames); const profile = { id: profilerNewId(), name: name, created: new Date().toISOString(), rules: rules }; profiles.push(profile); profilerSave(); @@ -7130,7 +7134,7 @@ async function profilerScanStart() { } // Recursively walk the card filesystem and build rules for files that exist. -async function profilerScanCard(ignoreFids) { +async function profilerScanCard(ignoreFids, ignoreNames) { const rules = []; const seen = new Set(); @@ -7157,7 +7161,7 @@ async function profilerScanCard(ignoreFids) { const childPath = dir.pathPrefix.concat(c.fid ? c.fid.toUpperCase() : c.name).join('/'); if (seen.has(childPath)) continue; seen.add(childPath); - const rule = await profilerBuildFileRule(childPath, c, ignoreFids); + const rule = await profilerBuildFileRule(childPath, c, ignoreFids, ignoreNames); if (rule) rules.push(rule); } } @@ -7176,13 +7180,13 @@ async function profilerScanCard(ignoreFids) { const path = segs.join('/'); if (seen.has(path)) continue; seen.add(path); - const rule = await profilerBuildFileRule(path, { fid: cf.fid, name: cf.name }, ignoreFids); + const rule = await profilerBuildFileRule(path, { fid: cf.fid, name: cf.name }, ignoreFids, ignoreNames); if (rule) rules.push(rule); } return rules; } -async function profilerBuildFileRule(path, c, ignoreFids) { +async function profilerBuildFileRule(path, c, ignoreFids, ignoreNames) { let sel; try { sel = await pysimFetch('/api/select', { path: path }); @@ -7199,7 +7203,8 @@ async function profilerBuildFileRule(path, c, ignoreFids) { content: null, }; const fid = (c.fid || sel.fid || '').toUpperCase(); - if (ignoreFids.has(fid)) return rule; + const name = (c.name || sel.name || '').toUpperCase(); + if (ignoreFids.has(fid) || (ignoreNames && ignoreNames.has(name))) return rule; try { const rd = await pysimFetch('/api/read', { path: path, mode: 'raw' }); if (rd && rd.success) { diff --git a/frontend/sw.js b/frontend/sw.js index 90de8c1..685d973 100644 --- a/frontend/sw.js +++ b/frontend/sw.js @@ -1,4 +1,4 @@ -const CACHE = 'otaman-v50'; +const CACHE = 'otaman-v51'; const URLS = [ 'index.html', 'help.html', diff --git a/frontend/tests/profiler.test.js b/frontend/tests/profiler.test.js index 5a633b8..9162bdb 100644 --- a/frontend/tests/profiler.test.js +++ b/frontend/tests/profiler.test.js @@ -5,7 +5,7 @@ const path = require('node:path'); const html = fs.readFileSync(path.join(__dirname, '..', 'index.html'), 'utf8'); -function extractFunc(src, name) { +function extractFunc(src, name, asyncFn) { const re = new RegExp('function\\s+' + name + '\\s*\\([^)]*\\)\\s*\\{'); const m = re.exec(src); if (!m) throw new Error('function ' + name + ' not found'); @@ -18,12 +18,14 @@ function extractFunc(src, name) { if (depth === 0) break; } } - return src.slice(m.index, i + 1); + return (asyncFn ? 'async ' : '') + src.slice(m.index, i + 1); } const FNS = ['profilerNormHex', 'profilerMatch', 'profilerMatchMin', 'profilerMaskPrefix4', 'profilerFileFields', 'profilerContentKindForFileType', 'profilerEmptyRecordContent', 'profilerValidateProfile']; let code = ''; for (const f of FNS) code += extractFunc(html, f) + '\n'; +code += extractFunc(html, 'profilerBuildFileRule', true) + '\n'; +code += html.match(/const PROFILER_MASK_PREFIX4_FIDS = \{[\s\S]*?\n\};/)[0] + '\n'; eval(code); test('profilerNormHex uppercases and strips non-hex', () => { @@ -107,3 +109,85 @@ test('profile validation', () => { assert.strictEqual(profilerValidateProfile({ name: 'x', rules: [{ type: 'file' }] }), 'Rule missing path'); assert.strictEqual(profilerValidateProfile({ name: 'x', rules: [{ type: 'file', path: 'MF/7F10/6F3A' }] }), null); }); + +// --- "Profile from card" ignore list --- + +function parseIgnoreFiles() { + const raw = html.match(/const PROFILER_IGNORE_FILES = \[([\s\S]*?)\n\];/)[1]; + return [...raw.matchAll(/\{ fid: '([^']*)', name: '([^']*)' \}/g)].map(m => ({ fid: m[1], name: m[2] })); +} + +test('ignore list FIDs are well-formed and KcGPRS uses the TS 51.011 FID (6F52)', () => { + const files = parseIgnoreFiles(); + assert.ok(files.length >= 12); + for (const f of files) { + assert.match(f.fid, /^[0-9A-F]{4}$/, f.name + ' has a malformed FID'); + assert.ok(f.name.startsWith('EF.'), f.fid + ' has a malformed name'); + } + assert.strictEqual(files.find(f => f.name === 'EF.KcGPRS').fid, '6F52'); +}); + +test('ignore list has no duplicate FIDs or names', () => { + const files = parseIgnoreFiles(); + assert.strictEqual(new Set(files.map(f => f.fid)).size, files.length); + assert.strictEqual(new Set(files.map(f => f.name)).size, files.length); +}); + +function mockFetch(handlers) { + const calls = []; + global.pysimFetch = async (path, body) => { + calls.push(path); + if (handlers[path]) return handlers[path](body); + throw new Error('unexpected fetch: ' + path); + }; + return calls; +} + +const KCGPRS_SELECT = { + name: 'EF.KcGPRS', fid: '6F52', file_type: 'transparent', + file_size: 9, record_len: null, num_of_rec: null, exists: true, +}; + +test('profilerBuildFileRule skips contents for an ignored FID', async () => { + const calls = mockFetch({ '/api/select': () => KCGPRS_SELECT }); + const rule = await profilerBuildFileRule('MF/7F20/6F52', + { fid: '6f52', name: 'EF.KcGPRS' }, new Set(['6F52']), new Set(['EF.KCGPRS'])); + assert.strictEqual(rule.content, null); + assert.strictEqual(rule.path, 'MF/7F20/6F52'); + assert.strictEqual(rule.fileType, 'transparent'); + assert.strictEqual(rule.fileSize, 9); + assert.ok(!calls.includes('/api/read'), 'contents must not be read for ignored files'); +}); + +test('profilerBuildFileRule skips contents when the name matches but the FID differs', async () => { + // Regression: a KcGPRS copy at the TS 31.102 DF.GSM-ACCESS FID (4F52) must + // still be ignored when the user checked EF.KcGPRS in the ignore list. + const calls = mockFetch({ + '/api/select': () => ({ name: 'EF.KcGPRS', fid: '4F52', file_type: 'transparent', file_size: 9, record_len: null, num_of_rec: null, exists: true }), + }); + const rule = await profilerBuildFileRule('MF/5F3B/4F52', + { fid: '4f52', name: 'EF.KcGPRS' }, new Set(['6F52']), new Set(['EF.KCGPRS'])); + assert.strictEqual(rule.content, null); + assert.ok(!calls.includes('/api/read'), 'contents must not be read for name-matched files'); +}); + +test('profilerBuildFileRule captures contents for non-ignored files', async () => { + const calls = mockFetch({ + '/api/select': () => ({ name: 'EF.ADN', fid: '6F3A', file_type: 'transparent', file_size: 4, record_len: null, num_of_rec: null, exists: true }), + '/api/read': () => ({ success: true, data: 'AABBCCDD' }), + }); + const rule = await profilerBuildFileRule('MF/7F20/6F3A', + { fid: '6f3a', name: 'EF.ADN' }, new Set(['6F52']), new Set(['EF.KCGPRS'])); + assert.ok(rule.content); + assert.strictEqual(rule.content.mode, 'exact'); + assert.strictEqual(rule.content.expected, 'AABBCCDD'); + assert.ok(calls.includes('/api/read')); +}); + +test('profilerBuildFileRule still ignores when ignoreNames is omitted (back-compat)', async () => { + const calls = mockFetch({ '/api/select': () => KCGPRS_SELECT }); + const rule = await profilerBuildFileRule('MF/7F20/6F52', + { fid: '6F52', name: 'EF.KcGPRS' }, new Set(['6F52'])); + assert.strictEqual(rule.content, null); + assert.ok(!calls.includes('/api/read')); +});