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