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')); +});