ui: show FID, layout and decoded FCI in the file manager detail pane
- selecting an EF now renders an info block between the File: title and the contents: FID, file type, size, record length/count (nulls skipped) followed by the decoded FCI via the existing profilerFciPreviewItems() decoder (no raw hex), matching the snapshot detail view - pysimFsInfoHtml() is pure and reused by pysimFsClickFile(); the [LF] shorthand stays in the title; custom files get the same metadata from the temporary select probe - tests: pysimFsInfoHtml in profiler.test.js (full response, missing fci_hex, skipped nulls) and a structural check for #pysim-fs-info; help EN/RU and READMEs updated; SW cache v119 -> v120.
This commit is contained in:
@@ -490,6 +490,7 @@ Browse the UICC filesystem in a tree view. Files are shown with names, FIDs, and
|
||||
- **Read** — reads the selected file (auto-detects transparent vs record files)
|
||||
- **Edit** — switch to edit mode, modify hex data, click **Save** to write back
|
||||
- **Raw / Decoded** — toggle between hex dump and pysim-decoded JSON view
|
||||
- Selecting a file shows its FID, file type, size / record layout and the decoded FCI above the contents
|
||||
- Missing files are shown in red (✗); a present but empty DF shows `(empty)`
|
||||
- **Probe all files** — walks the whole tree (incl. custom files), marks every entry present/absent with *N / total* progress, stoppable, and ends with a summary; browsing itself stays lazy
|
||||
|
||||
|
||||
@@ -464,6 +464,7 @@ Delivery PoR (SPI2 `01`) проще — карта возвращает PoR на
|
||||
- **Read** — чтение файла (автоопределение transparent/record)
|
||||
- **Edit** — режим редактирования, измените hex-данные и нажмите **Save** для записи
|
||||
- **Raw / Decoded** — переключение между hex-дампом и декодированным JSON
|
||||
- При выборе файла над содержимым показываются FID, тип файла, размер / структура записей и декодированный FCI
|
||||
- Отсутствующие файлы показаны красным (✗); существующий пустой DF — `(пусто)`
|
||||
- **Проверить все файлы** — обход всего дерева (включая пользовательские) с пометкой «есть/нет», прогрессом *N / всего*, возможностью остановки и сводкой в конце; сам просмотр остаётся ленивым
|
||||
|
||||
|
||||
@@ -317,7 +317,7 @@
|
||||
<p class="mb-3">Подключение к локальному <a href="https://github.com/anttro/otaman" class="text-blue-600 dark:text-blue-400 hover:underline">pysim-otaman-server</a> для работы с картой: введите URL сервера (по умолчанию <code class="font-mono text-sm">http://127.0.0.1:8080</code>) и нажмите <strong>Подключиться</strong>. Область статуса показывает состояние ридера/карты, а <strong>Подключить карту</strong> (пере)инициализирует карту после вставки. Подвкладки: <strong>Файловый менеджер</strong>, <strong>Командная строка pySim</strong> и <strong>Отправка APDU</strong>. <strong>«Профайлер»</strong> и <strong>«Симулятор телефона»</strong> — отдельные вкладки верхнего уровня.</p>
|
||||
|
||||
<h3 id="file-manager" class="text-lg font-medium mb-2">4.1 Файловый менеджер</h3>
|
||||
<p class="text-sm mb-2">Дерево файловой системы отображается слева; выбор файла открывает панель деталей справа. Элементы сгруппированы: DF выше EF, сортировка по <strong>FID</strong> или символьному <strong>имени</strong> (пиллы над деревом; выбор сохраняется в <code class="font-mono text-sm">localStorage</code>).</p>
|
||||
<p class="text-sm mb-2">Дерево файловой системы отображается слева; выбор файла открывает панель деталей справа. Элементы сгруппированы: DF выше EF, сортировка по <strong>FID</strong> или символьному <strong>имени</strong> (пиллы над деревом; выбор сохраняется в <code class="font-mono text-sm">localStorage</code>). При выборе файла над содержимым также показываются FID, тип файла, размер / структура записей и декодированный FCI.</p>
|
||||
<ul class="list-disc list-inside text-sm space-y-1 mb-3">
|
||||
<li><strong>Прочитать</strong> — чтение файла (автоопределение transparent/record)</li>
|
||||
<li><strong>Редактировать</strong> — изменение hex-данных, <strong>Сохранить</strong> для записи (или <strong>Отмена</strong>)</li>
|
||||
|
||||
+1
-1
@@ -317,7 +317,7 @@
|
||||
<p class="mb-3">Connects to a local <a href="https://github.com/anttro/otaman" class="text-blue-600 dark:text-blue-400 hover:underline">pysim-otaman-server</a> for live card operations: enter the server URL (default <code class="font-mono text-sm">http://127.0.0.1:8080</code>) and press <strong>Connect</strong>. The status area shows the reader/card state, and <strong>Equip card</strong> (re)initializes the card after insertion. Sub-tabs: <strong>File manager</strong>, <strong>pySim command line</strong>, and <strong>Raw APDU</strong>. The <strong>Profiler</strong> and <strong>Phone simulator</strong> are separate top-level tabs.</p>
|
||||
|
||||
<h3 id="file-manager" class="text-lg font-medium mb-2">4.1 File manager</h3>
|
||||
<p class="text-sm mb-2">The file system tree is displayed on the left; selecting a file opens its detail pane on the right. Entries are grouped with DFs above EFs and sorted by <strong>FID</strong> or symbolic <strong>Name</strong> (pills above the tree; the choice is remembered in <code class="font-mono text-sm">localStorage</code>).</p>
|
||||
<p class="text-sm mb-2">The file system tree is displayed on the left; selecting a file opens its detail pane on the right. Entries are grouped with DFs above EFs and sorted by <strong>FID</strong> or symbolic <strong>Name</strong> (pills above the tree; the choice is remembered in <code class="font-mono text-sm">localStorage</code>). Selecting a file also shows its FID, file type, size / record layout and the decoded FCI above the content pane.</p>
|
||||
<ul class="list-disc list-inside text-sm space-y-1 mb-3">
|
||||
<li><strong>Read</strong> — reads the selected file (auto-detects transparent vs record files)</li>
|
||||
<li><strong>Edit</strong> — modify hex data, <strong>Save</strong> to write back (or <strong>Cancel</strong>)</li>
|
||||
|
||||
@@ -867,6 +867,7 @@
|
||||
<div class="flex-1">
|
||||
<div id="pysim-fs-detail" class="hidden">
|
||||
<div class="text-xs font-mono mb-1"><span data-l10n="File:">File:</span> <b id="pysim-fs-filename"></b></div>
|
||||
<div id="pysim-fs-info" class="mb-1"></div>
|
||||
<div id="pysim-fs-content" class="w-full font-mono text-xs border border-gray-300 dark:border-slate-600 rounded px-2 py-1.5 bg-gray-100 dark:bg-slate-800 min-h-20 overflow-y-auto"></div>
|
||||
<div class="flex gap-2 mt-1 items-center">
|
||||
<button data-needs="card" onclick="pysimFsRead()" id="pysim-fs-read-btn" class="px-2 py-1 text-xs rounded bg-blue-600 text-white hover:bg-blue-700 disabled:opacity-40 disabled:cursor-not-allowed" data-l10n="Read">Read</button>
|
||||
@@ -6124,6 +6125,24 @@ async function pysimFsToggleDir(name) {
|
||||
if (node.expanded) pysimRefresh();
|
||||
}
|
||||
|
||||
function pysimFsInfoHtml(sel) {
|
||||
if (!sel) return '';
|
||||
const num = v => (v === null || v === undefined) ? null : String(v);
|
||||
const attrs = [];
|
||||
if (sel.fid) attrs.push('FID: ' + esc(String(sel.fid).toUpperCase()));
|
||||
if (sel.file_type) attrs.push(esc(t('File type')) + ': ' + esc(sel.file_type));
|
||||
if (num(sel.file_size) !== null) attrs.push(esc(t('Size')) + ': ' + esc(num(sel.file_size)));
|
||||
if (num(sel.record_len) !== null) attrs.push(esc(t('Record length')) + ': ' + esc(num(sel.record_len)));
|
||||
if (num(sel.num_of_rec) !== null) attrs.push(esc(t('Record count')) + ': ' + esc(num(sel.num_of_rec)));
|
||||
let html = '';
|
||||
if (attrs.length) html += '<div class="text-xs font-mono text-gray-500 dark:text-slate-400">' + attrs.join(' · ') + '</div>';
|
||||
if (sel.fci_hex) {
|
||||
html += '<div class="text-xs text-gray-500 dark:text-slate-400 mt-0.5">' + esc(t('Decoded FCI')) + '</div>';
|
||||
html += '<div class="text-xs font-mono leading-relaxed text-gray-600 dark:text-slate-400 break-all">' + profilerFciPreviewItems(sel.fci_hex) + '</div>';
|
||||
}
|
||||
return html;
|
||||
}
|
||||
|
||||
async function pysimFsClickFile(name) {
|
||||
pysimFsResetEdit();
|
||||
pysimFsSetMode('raw');
|
||||
@@ -6139,6 +6158,7 @@ async function pysimFsClickFile(name) {
|
||||
const label = ({ transparent: 'TR', linear_fixed: 'LF', cyclic: 'CY', ber_tlv: 'BT', df: 'DF' })[ft] || '';
|
||||
const prefix = exists ? '' : '✗ ';
|
||||
document.getElementById('pysim-fs-filename').textContent = prefix + name + (label ? ' [' + label + ']' : '');
|
||||
document.getElementById('pysim-fs-info').innerHTML = exists ? pysimFsInfoHtml(sel) : '';
|
||||
document.getElementById('pysim-fs-content').innerHTML = '';
|
||||
document.getElementById('pysim-fs-status').textContent = exists ? '' : '✗ File not found on card';
|
||||
if (exists) {
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
const CACHE = 'otaman-v119';
|
||||
const CACHE = 'otaman-v120';
|
||||
const URLS = [
|
||||
'index.html',
|
||||
'help.html',
|
||||
|
||||
@@ -68,3 +68,12 @@ test('file manager has a probe-all-files button and status line', () => {
|
||||
assert.ok(html.includes('onclick="pysimFsProbeAll()"'));
|
||||
assert.ok(html.includes('id="pysim-fs-probe-status"'));
|
||||
});
|
||||
|
||||
test('file manager shows decoded FCI info under the file name', () => {
|
||||
const name = html.indexOf('id="pysim-fs-filename"');
|
||||
const info = html.indexOf('id="pysim-fs-info"');
|
||||
const content = html.indexOf('id="pysim-fs-content"');
|
||||
assert.ok(name !== -1 && info > name && info < content, 'pysim-fs-info must sit between filename and content');
|
||||
assert.ok(html.includes('function pysimFsInfoHtml'));
|
||||
assert.ok(html.includes("pysimFsInfoHtml(sel)"));
|
||||
});
|
||||
|
||||
@@ -21,7 +21,7 @@ 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', 'swapNibbles', 'decIccid', 'profilerSnapshotIccid', 'profilerValidateSnapshot', 'profilerListSwitch', 'profilerScanRefreshOptions', 'profilerLiveSource', 'profilerSnapshotSource', 'profilerVisibleResults', 'profilerMaskFidForFile', 'profilerRulesFromSnapshot', 'profilerExtraFileResults', 'profilerScanNameKeydown', 'profilerTimingStats', 'profilerTimingAccumulator', 'profilerFormatMs', 'profilerRenderSnapshotSummary', 'profilerSnapshotCountLabel'];
|
||||
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', 'profilerListSwitch', 'profilerScanRefreshOptions', 'profilerLiveSource', 'profilerSnapshotSource', 'profilerVisibleResults', 'profilerMaskFidForFile', 'profilerRulesFromSnapshot', 'profilerExtraFileResults', 'profilerScanNameKeydown', 'profilerTimingStats', 'profilerTimingAccumulator', 'profilerFormatMs', 'profilerRenderSnapshotSummary', 'profilerSnapshotCountLabel', 'pysimFsInfoHtml'];
|
||||
let code = '';
|
||||
for (const f of FNS) code += extractFunc(html, f) + '\n';
|
||||
code += extractFunc(html, 'profilerBuildFileRule', true) + '\n';
|
||||
@@ -698,6 +698,33 @@ test('fcpDiffHtml highlights differing FCI parameters', () => {
|
||||
delete global.t;
|
||||
});
|
||||
|
||||
test('pysimFsInfoHtml shows FID, type, size and the decoded FCI', () => {
|
||||
global.t = s => s;
|
||||
const hex = '621A82054221000F0583026F4F8A01058B036F06098002004B8801B0';
|
||||
const out = pysimFsInfoHtml({ fid: '6f4f', file_type: 'linear_fixed', file_size: 75, record_len: 15, num_of_rec: 5, fci_hex: hex });
|
||||
assert.ok(out.includes('FID: 6F4F'), out);
|
||||
assert.ok(out.includes('File type: linear_fixed'), out);
|
||||
assert.ok(out.includes('Size: 75'), out);
|
||||
assert.ok(out.includes('Record length: 15'), out);
|
||||
assert.ok(out.includes('Record count: 5'), out);
|
||||
assert.ok(out.includes('Decoded FCI'), out);
|
||||
assert.ok(out.includes('File descriptor'), out);
|
||||
assert.ok(out.includes('75 bytes'), out);
|
||||
delete global.t;
|
||||
});
|
||||
|
||||
test('pysimFsInfoHtml omits the FCI block without fci_hex and skips null fields', () => {
|
||||
global.t = s => s;
|
||||
const out = pysimFsInfoHtml({ fid: '6f07', file_type: 'transparent', file_size: null, record_len: null, num_of_rec: undefined, fci_hex: null });
|
||||
assert.ok(out.includes('FID: 6F07'), out);
|
||||
assert.ok(!out.includes('Size:'), out);
|
||||
assert.ok(!out.includes('Record length:'), out);
|
||||
assert.ok(!out.includes('Record count:'), out);
|
||||
assert.ok(!out.includes('Decoded FCI'), out);
|
||||
assert.strictEqual(pysimFsInfoHtml(null), '');
|
||||
delete global.t;
|
||||
});
|
||||
|
||||
test('profilerFciPreviewItems renders decoded items and degrades gracefully', () => {
|
||||
global.t = s => s;
|
||||
assert.ok(profilerFciPreviewItems(FCP_TRANSPARENT).includes('File size: '));
|
||||
|
||||
Reference in New Issue
Block a user