@@ -1419,6 +1442,11 @@ function encIccid(iccid) {
return swapNibbles(iccid);
}
+// EF.ICCID (2FE2): nibble-swapped E.118 digits, 19-20 digits, 'F' pad (TS 102 221 13.2).
+function decIccid(hex) {
+ return swapNibbles((hex || '').replace(/[^0-9a-fA-F]/g, '')).replace(/F+$/, '');
+}
+
function buildApdu(cla, ins, p1, p2, p3, data) {
let s = cla.toString(16).padStart(2, '0').toUpperCase() +
ins.toString(16).padStart(2, '0').toUpperCase() +
@@ -6950,6 +6978,9 @@ let profilerView = 'list';
let profilerEditId = null;
let profilerDraft = null;
let profilerResults = null;
+let snapshots = [];
+let snapshotViewId = null;
+let _scanTarget = 'profile';
// Files whose contents are ignored (contents check omitted) when scanning.
// Keyed by FID; display name is the pySim name.
@@ -6990,6 +7021,21 @@ function profilerSave() {
localStorage.setItem('otaman_profiles', JSON.stringify(profiles));
}
+function snapshotsLoad() {
+ try {
+ const d = localStorage.getItem('otaman_snapshots');
+ snapshots = d ? JSON.parse(d) : [];
+ } catch (e) { snapshots = []; }
+}
+
+function snapshotsSave() {
+ try {
+ localStorage.setItem('otaman_snapshots', JSON.stringify(snapshots));
+ } catch (e) {
+ alert('Snapshot save failed: ' + e.message);
+ }
+}
+
function profilerNewId() {
return Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 8);
}
@@ -7095,7 +7141,31 @@ function profilerSetView(view) {
document.getElementById('profiler-list').classList.toggle('hidden', view !== 'list');
document.getElementById('profiler-editor').classList.toggle('hidden', view !== 'editor');
document.getElementById('profiler-results').classList.toggle('hidden', view !== 'results');
- if (view === 'list') profilerRenderList();
+ document.getElementById('profiler-snapshot').classList.toggle('hidden', view !== 'snapshot');
+ if (view === 'list') { profilerRenderList(); profilerRenderSnapshotList(); }
+}
+
+function profilerRenderSnapshotList() {
+ const el = document.getElementById('snapshot-list-body');
+ if (!snapshots.length) {
+ el.innerHTML = '
' + t('No snapshots defined.') + ' ';
+ return;
+ }
+ let html = '';
+ for (let i = 0; i < snapshots.length; i++) {
+ const s = snapshots[i];
+ html += '
';
+ html += '' + esc(s.name) + ' ';
+ if (s.iccid) html += '' + esc(s.iccid) + ' ';
+ html += '' + esc(new Date(s.created).toLocaleString()) + ' ';
+ html += '(' + s.files.length + ' ' + esc(t('files')) + ') ';
+ html += '';
+ html += '' + esc(t('Open')) + ' ';
+ html += '' + esc(t('Export')) + ' ';
+ html += '' + esc(t('Delete')) + ' ';
+ html += '
';
+ }
+ el.innerHTML = html;
}
function profilerNew() {
@@ -7120,7 +7190,21 @@ function profilerScanIgnoreAllState() {
all.indeterminate = checked > 0 && checked < boxes.length;
}
+function profilerScanSetTarget(target) {
+ _scanTarget = target;
+ const title = document.getElementById('profiler-scan-title');
+ const label = document.getElementById('profiler-scan-name-label');
+ const titleKey = target === 'snapshot' ? 'New snapshot' : 'Profile from card';
+ const labelKey = target === 'snapshot' ? 'Snapshot name' : 'Profile name';
+ title.setAttribute('data-l10n', titleKey);
+ title.textContent = t(titleKey);
+ label.setAttribute('data-l10n', labelKey);
+ label.textContent = t(labelKey);
+ document.getElementById('profiler-scan-options').classList.toggle('hidden', target === 'snapshot');
+}
+
function profilerFromCard() {
+ profilerScanSetTarget('profile');
document.getElementById('profiler-scan-name').value = '';
document.getElementById('profiler-scan-name').readOnly = false;
document.getElementById('profiler-scan-name').classList.remove('opacity-50');
@@ -7154,11 +7238,25 @@ function profilerScanCancel() {
document.getElementById('profiler-scan-modal').classList.add('hidden');
}
+function snapshotNew() {
+ profilerScanSetTarget('snapshot');
+ const nameEl = document.getElementById('profiler-scan-name');
+ nameEl.value = '';
+ nameEl.readOnly = false;
+ nameEl.classList.remove('opacity-50');
+ 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');
+}
+
async function profilerScanStart() {
const name = document.getElementById('profiler-scan-name').value.trim();
const errEl = document.getElementById('profiler-scan-error');
if (!name) {
- errEl.textContent = t('Please enter a profile name');
+ errEl.textContent = t(_scanTarget === 'snapshot' ? 'Please enter a snapshot name' : 'Please enter a profile name');
errEl.classList.remove('hidden');
return;
}
@@ -7188,19 +7286,29 @@ async function profilerScanStart() {
btn.disabled = true;
btn.textContent = t('Scanning...');
try {
- const rules = await profilerScanCard(ignoreFids, ignoreNames, fciMode, (done, total, path) => {
+ const onProgress = (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();
- profilerScanCancel();
- profilerEdit(profiles.length - 1);
+ };
+ if (_scanTarget === 'snapshot') {
+ const files = await profilerScanCard(new Set(), new Set(), 'exact', onProgress, new Set(), 'snapshot');
+ const snapshot = { id: profilerNewId(), name: name, created: new Date().toISOString(), iccid: profilerSnapshotIccid(files), files: files };
+ snapshots.push(snapshot);
+ snapshotsSave();
+ profilerScanCancel();
+ profilerSetView('list');
+ } else {
+ const rules = await profilerScanCard(ignoreFids, ignoreNames, fciMode, onProgress, maskFids);
+ const profile = { id: profilerNewId(), name: name, created: new Date().toISOString(), rules: rules };
+ profiles.push(profile);
+ profilerSave();
+ profilerScanCancel();
+ profilerEdit(profiles.length - 1);
+ }
} catch (e) {
errEl.textContent = e.message;
errEl.classList.remove('hidden');
} finally {
- document.getElementById('profiler-scan-options').classList.remove('hidden');
+ profilerScanSetTarget(_scanTarget);
cancelBtn.disabled = false;
nameEl.readOnly = false;
nameEl.classList.remove('opacity-50');
@@ -7214,7 +7322,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, maskFids) {
+async function profilerScanCard(ignoreFids, ignoreNames, fciMode, onProgress, maskFids, mode) {
const rules = [];
const files = [];
const seen = new Set();
@@ -7268,7 +7376,9 @@ async function profilerScanCard(ignoreFids, ignoreNames, fciMode, onProgress, ma
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, maskFids);
+ const rule = mode === 'snapshot'
+ ? await profilerBuildSnapshotFile(entry.path, entry.child)
+ : await profilerBuildFileRule(entry.path, entry.child, ignoreFids, ignoreNames, fciMode, maskFids);
if (rule) rules.push(rule);
}
return rules;
@@ -7317,6 +7427,45 @@ async function profilerBuildFileRule(path, c, ignoreFids, ignoreNames, fciMode,
return rule;
}
+// Card snapshot entry: metadata + raw FCI + exact contents for every readable
+// file (no ignore list, no masking, no FCP/FCI check mode).
+async function profilerBuildSnapshotFile(path, c) {
+ let sel;
+ try {
+ sel = await pysimFetch('/api/select', { path: path });
+ } catch (e) { return null; }
+ if (!sel || sel.exists !== true) return null;
+ const isRecord = profilerContentKindForFileType(sel.file_type) === 'record';
+ const file = {
+ path: path,
+ name: c.name || sel.name || null,
+ fileType: sel.file_type || null,
+ fileSize: isRecord ? null : ((sel.file_size === null || sel.file_size === undefined) ? null : sel.file_size),
+ recordLen: (sel.record_len === null || sel.record_len === undefined) ? null : sel.record_len,
+ numRecords: (sel.num_of_rec === null || sel.num_of_rec === undefined) ? null : sel.num_of_rec,
+ fciHex: sel.fci_hex || null,
+ content: null,
+ };
+ try {
+ const rd = await pysimFetch('/api/read', { path: path, mode: 'raw' });
+ if (rd && rd.success) {
+ if (rd.records) {
+ file.content = { kind: 'record', records: rd.records.map(r => ({ num: r.num, data: r.data })) };
+ } else if (rd.data) {
+ file.content = { kind: 'transparent', data: rd.data };
+ }
+ }
+ } catch (e) {}
+ return file;
+}
+
+// Decode the ICCID from the captured EF.ICCID (2FE2) entry, if readable.
+function profilerSnapshotIccid(files) {
+ const f = (files || []).find(x => x.name === 'EF.ICCID' || (x.path || '').toUpperCase().endsWith('/2FE2'));
+ if (!f || !f.content || f.content.kind !== 'transparent' || !f.content.data) return null;
+ return decIccid(f.content.data) || null;
+}
+
function profilerImportFile(input) {
const file = input.files && input.files[0];
input.value = '';
@@ -7364,9 +7513,118 @@ function profilerEdit(i) {
function profilerBackToList() {
profilerDraft = null;
profilerEditId = null;
+ snapshotViewId = null;
profilerSetView('list');
}
+function profilerValidateSnapshot(obj) {
+ if (!obj || typeof obj !== 'object') return 'Not an object';
+ if (!obj.name) return 'Missing name';
+ if (!Array.isArray(obj.files)) return 'Missing files array';
+ for (const f of obj.files) {
+ if (!f || typeof f !== 'object') return 'Invalid file entry';
+ if (!f.path) return 'File entry missing path';
+ }
+ return null;
+}
+
+function snapshotImportFile(input) {
+ const file = input.files && input.files[0];
+ input.value = '';
+ if (!file) return;
+ const reader = new FileReader();
+ reader.onload = () => {
+ try {
+ const obj = JSON.parse(reader.result);
+ const err = profilerValidateSnapshot(obj);
+ if (err) throw new Error(err);
+ obj.id = profilerNewId();
+ obj.created = obj.created || new Date().toISOString();
+ obj.iccid = obj.iccid || profilerSnapshotIccid(obj.files);
+ snapshots.push(obj);
+ snapshotsSave();
+ profilerRenderSnapshotList();
+ } catch (e) {
+ alert('Import error: ' + e.message);
+ }
+ };
+ reader.readAsText(file);
+}
+
+function snapshotExport(i) {
+ const s = snapshots[i];
+ downloadJson((s.name || 'snapshot').replace(/[^a-zA-Z0-9_-]+/g, '_') + '.json', s);
+}
+
+function snapshotDelete(i) {
+ const s = snapshots[i];
+ if (!confirm(t('Delete snapshot') + ' "' + s.name + '"?') ) return;
+ snapshots.splice(i, 1);
+ snapshotsSave();
+ profilerRenderSnapshotList();
+}
+
+function snapshotOpen(i) {
+ const s = snapshots[i];
+ snapshotViewId = s.id;
+ profilerSetView('snapshot');
+ document.getElementById('snapshot-name').value = s.name;
+ document.getElementById('snapshot-iccid').textContent = s.iccid || '';
+ document.getElementById('snapshot-files').innerHTML = profilerRenderSnapshotFiles(s);
+}
+
+function snapshotSaveName() {
+ const s = snapshots.find(x => x.id === snapshotViewId);
+ if (!s) return;
+ const name = document.getElementById('snapshot-name').value.trim();
+ if (!name) return;
+ s.name = name;
+ snapshotsSave();
+ document.getElementById('snapshot-name').value = s.name;
+}
+
+function profilerRenderSnapshotFiles(s) {
+ if (!s.files.length) return '
' + t('No files captured.') + ' ';
+ let html = '';
+ for (const f of s.files) {
+ const name = profilerCustomNameForPath(f.path) || f.name;
+ const title = name
+ ? '
' + esc(name) + ' (' + esc(f.path) + ') '
+ : '
' + esc(f.path) + ' ';
+ html += '
';
+ html += '
' + title + '
';
+ const attrs = [];
+ if (f.fileType) attrs.push(esc(t('File type')) + ': ' + esc(f.fileType));
+ if (f.fileSize !== null && f.fileSize !== undefined) attrs.push(esc(t('Size')) + ': ' + esc(String(f.fileSize)));
+ if (f.recordLen !== null && f.recordLen !== undefined) attrs.push(esc(t('Record length')) + ': ' + esc(String(f.recordLen)));
+ if (f.numRecords !== null && f.numRecords !== undefined) attrs.push(esc(t('Record count')) + ': ' + esc(String(f.numRecords)));
+ if (attrs.length) html += '
' + attrs.join(' · ') + '
';
+ if (f.fciHex) {
+ html += '
';
+ html += '
' + esc(t('FCI hex (raw SELECT response)')) + '
' +
+ '
';
+ html += '
' + esc(t('Decoded FCI')) + '
' +
+ '
' + profilerFciPreviewItems(f.fciHex) + '
';
+ html += '
';
+ }
+ html += '
' + esc(t('Contents')) + '
';
+ if (!f.content) {
+ html += '
' + esc(t('Not captured')) + '
';
+ } else if (f.content.kind === 'record') {
+ html += '
';
+ for (const rec of (f.content.records || [])) {
+ html += '
' + rec.num + ' ' +
+ '
';
+ }
+ html += '
';
+ } else {
+ html += '
';
+ }
+ html += '
';
+ }
+ return html;
+}
+
function profilerAddRule() {
if (!profilerDraft) return;
profilerDraft.rules.push({ type: 'file', path: '', name: null, fileType: null, fileSize: null, recordLen: null, numRecords: null, fciMode: 'type_size', fciHex: null, content: null });
@@ -8082,6 +8340,7 @@ function profilerRenderReport(results) {
}
profilerLoad();
+snapshotsLoad();
// Init sub-tab pills
document.querySelectorAll('.pysim-subtab').forEach(btn => {
@@ -8283,6 +8542,18 @@ const LANG_RU = {
'New profile': 'Новый профиль',
'Profile from card': 'Профиль с карты',
'Import profile': 'Импорт профиля',
+ 'Profiles': 'Профили',
+ 'Card snapshots': 'Снимки карты',
+ 'New snapshot': 'Новый снимок',
+ 'Import snapshot': 'Импорт снимка',
+ 'Snapshot name': 'Имя снимка',
+ 'Delete snapshot': 'Удалить снимок',
+ 'No snapshots defined.': 'Снимки не заданы.',
+ 'No files captured.': 'Файлы не захвачены.',
+ 'Please enter a snapshot name': 'Введите имя снимка',
+ 'Open': 'Открыть',
+ 'Contents': 'Содержимое',
+ 'Not captured': 'Не прочитано',
'Back to list': 'Назад к списку',
'Add rule': 'Добавить правило',
'Scan': 'Сканировать',
diff --git a/frontend/sw.js b/frontend/sw.js
index 3fb80fa..d0065eb 100644
--- a/frontend/sw.js
+++ b/frontend/sw.js
@@ -1,4 +1,4 @@
-const CACHE = 'otaman-v79';
+const CACHE = 'otaman-v80';
const URLS = [
'index.html',
'help.html',
diff --git a/frontend/tests/profiler.test.js b/frontend/tests/profiler.test.js
index d74290f..41eede8 100644
--- a/frontend/tests/profiler.test.js
+++ b/frontend/tests/profiler.test.js
@@ -21,12 +21,13 @@ function extractFunc(src, name, asyncFn) {
return (asyncFn ? 'async ' : '') + src.slice(m.index, i + 1);
}
-const FNS = ['profilerNormHex', 'profilerNormHexStrict', 'profilerMatch', 'profilerMatchMin', 'profilerMaskPrefix4', 'profilerFileFields', 'profilerContentKindForFileType', 'profilerEmptyRecordContent', 'profilerValidateProfile', 'profilerCustomNameForPath', 'profilerUpdateRulePath', 'profilerResultAspects', 'profilerAspectSummary', 'profilerNumRanges', 'esc', 'escHtml', 'profilerRawDataCheck', 'profilerRenderReport', 'parseBerLen', 'parseTlvList', 'fcpInt', 'fcpParseTlvs', 'fcpFileDescriptor', 'fcpLifeCycle', 'fcpSfi', 'fcpDo', 'fcpDecode', 'fcpDiffHtml', 'profilerFciPreviewItems', 'profilerUpdateFciPreview', 'profilerUpdateRule', 'profilerFciInput', 'profilerScanToggleAll', 'profilerScanIgnoreAllState'];
+const FNS = ['profilerNormHex', 'profilerNormHexStrict', 'profilerMatch', 'profilerMatchMin', 'profilerMaskPrefix4', 'profilerFileFields', 'profilerContentKindForFileType', 'profilerEmptyRecordContent', 'profilerValidateProfile', 'profilerCustomNameForPath', 'profilerUpdateRulePath', 'profilerResultAspects', 'profilerAspectSummary', 'profilerNumRanges', 'esc', 'escHtml', 'profilerRawDataCheck', 'profilerRenderReport', 'parseBerLen', 'parseTlvList', 'fcpInt', 'fcpParseTlvs', 'fcpFileDescriptor', 'fcpLifeCycle', 'fcpSfi', 'fcpDo', 'fcpDecode', 'fcpDiffHtml', 'profilerFciPreviewItems', 'profilerUpdateFciPreview', 'profilerUpdateRule', 'profilerFciInput', 'profilerScanToggleAll', 'profilerScanIgnoreAllState', 'swapNibbles', 'decIccid', 'profilerSnapshotIccid', 'profilerValidateSnapshot'];
let code = '';
for (const f of FNS) code += extractFunc(html, f) + '\n';
code += extractFunc(html, 'profilerBuildFileRule', true) + '\n';
code += extractFunc(html, 'profilerRunRule', true) + '\n';
code += extractFunc(html, 'profilerScanCard', true) + '\n';
+code += extractFunc(html, 'profilerBuildSnapshotFile', true) + '\n';
code += html.match(/const PROFILER_MASK_PREFIX4_FIDS = \{[\s\S]*?\n\};/)[0] + '\n';
eval(code);
@@ -819,3 +820,97 @@ test('profilerScanToggleAll / profilerScanIgnoreAllState manage the ignore check
delete global.document;
});
+
+// --- card snapshots ---
+
+test('decIccid decodes nibble-swapped EF.ICCID digits and strips F padding', () => {
+ assert.strictEqual(decIccid('980711090000640090F8'), '8970119000004600098');
+ assert.strictEqual(decIccid('98103254769810325476'), '89012345678901234567');
+ assert.strictEqual(decIccid('98 07 11 09 00 00 64 00 90 f8'), '8970119000004600098');
+ assert.strictEqual(decIccid(''), '');
+});
+
+test('profilerSnapshotIccid finds EF.ICCID by name or FID and handles null cases', () => {
+ const hex = '980711090000640090F8';
+ assert.strictEqual(
+ profilerSnapshotIccid([{ path: 'MF/2FE2', name: 'EF.ICCID', content: { kind: 'transparent', data: hex } }]),
+ '8970119000004600098');
+ assert.strictEqual(
+ profilerSnapshotIccid([{ path: 'MF/6F07', name: 'x', content: { kind: 'transparent', data: hex } }]),
+ null);
+ assert.strictEqual(
+ profilerSnapshotIccid([{ path: 'MF/2FE2', name: 'EF.ICCID', content: null }]),
+ null);
+ assert.strictEqual(profilerSnapshotIccid([]), null);
+});
+
+test('profilerValidateSnapshot checks name and files array', () => {
+ assert.strictEqual(profilerValidateSnapshot(null), 'Not an object');
+ assert.strictEqual(profilerValidateSnapshot({ name: 'x' }), 'Missing files array');
+ assert.strictEqual(profilerValidateSnapshot({ name: 'x', files: 'nope' }), 'Missing files array');
+ assert.strictEqual(profilerValidateSnapshot({ files: [] }), 'Missing name');
+ assert.strictEqual(profilerValidateSnapshot({ name: 'x', files: [null] }), 'Invalid file entry');
+ assert.strictEqual(profilerValidateSnapshot({ name: 'x', files: [{}] }), 'File entry missing path');
+ assert.strictEqual(profilerValidateSnapshot({ name: 'x', files: [{ path: 'MF/6F07' }] }), null);
+});
+
+test('profilerBuildSnapshotFile captures exact contents for everything readable', async () => {
+ // transparent file (IMSI) is captured exactly, no mask
+ mockFetch({
+ '/api/select': () => ({ name: 'EF.IMSI', fid: '6F07', file_type: 'transparent', file_size: 9, record_len: null, num_of_rec: null, fci_hex: '6212', exists: true }),
+ '/api/read': () => ({ success: true, data: '082905911234567890' }),
+ });
+ const f = await profilerBuildSnapshotFile('MF/6F07', { fid: '6f07', name: 'EF.IMSI' });
+ assert.strictEqual(f.name, 'EF.IMSI');
+ assert.strictEqual(f.fileType, 'transparent');
+ assert.strictEqual(f.fileSize, 9);
+ assert.strictEqual(f.fciHex, '6212');
+ assert.deepStrictEqual(f.content, { kind: 'transparent', data: '082905911234567890' });
+
+ // record file
+ mockFetch({
+ '/api/select': () => ({ name: 'EF.ADN', fid: '6F3A', file_type: 'linear_fixed', file_size: null, record_len: 2, num_of_rec: 2, fci_hex: '620E', exists: true }),
+ '/api/read': () => ({ success: true, records: [{ num: 1, data: 'AA' }, { num: 2, data: 'BB' }] }),
+ });
+ const r = await profilerBuildSnapshotFile('MF/7F20/6F3A', { fid: '6F3A', name: 'EF.ADN' });
+ assert.strictEqual(r.recordLen, 2);
+ assert.strictEqual(r.numRecords, 2);
+ assert.deepStrictEqual(r.content, { kind: 'record', records: [{ num: 1, data: 'AA' }, { num: 2, data: 'BB' }] });
+
+ // unreadable -> content null, metadata kept
+ mockFetch({
+ '/api/select': () => ({ name: 'EF.Kc', fid: '6F20', file_type: 'transparent', file_size: 9, record_len: null, num_of_rec: null, fci_hex: '620A', exists: true }),
+ '/api/read': () => ({ success: false, sw: '6982' }),
+ });
+ const u = await profilerBuildSnapshotFile('MF/7F20/6F20', { fid: '6F20', name: 'EF.Kc' });
+ assert.strictEqual(u.content, null);
+ assert.strictEqual(u.fileSize, 9);
+});
+
+test('profilerScanCard snapshot mode builds snapshot entries with ICCID', async () => {
+ global.pysimCustomFiles = [];
+ global.pysimFetch = async (path, body) => {
+ if (path === '/api/tree') return { exists: true, name: 'MF', children: [
+ { name: 'EF.ICCID', fid: '2fe2', isDir: false },
+ { name: 'EF.IMSI', fid: '6f07', isDir: false },
+ ] };
+ if (path === '/api/select') {
+ const fid = body.path.split('/').pop();
+ if (fid === '2FE2') return { name: 'EF.ICCID', fid: '2FE2', file_type: 'transparent', file_size: 10, record_len: null, num_of_rec: null, fci_hex: '620E', exists: true };
+ return { name: 'EF.IMSI', fid: '6F07', file_type: 'transparent', file_size: 9, record_len: null, num_of_rec: null, fci_hex: '6212', exists: true };
+ }
+ if (path === '/api/read') {
+ return body.path.endsWith('2FE2')
+ ? { success: true, data: '980711090000640090F8' }
+ : { success: true, data: '082905911234567890' };
+ }
+ throw new Error('unexpected ' + path);
+ };
+ const files = await profilerScanCard(new Set(), new Set(), 'exact', undefined, new Set(), 'snapshot');
+ assert.strictEqual(files.length, 2);
+ assert.ok(!files[0].fciMode, 'snapshot entries carry no FCI mode');
+ assert.strictEqual(profilerSnapshotIccid(files), '8970119000004600098');
+ // IMSI captured exactly (no mask)
+ assert.strictEqual(files.find(f => f.name === 'EF.IMSI').content.data, '082905911234567890');
+ delete global.pysimCustomFiles;
+});