custom files: any DF parent path at any depth (v2.2.11)
The parent field was a select over custom DFs only, so a standard DF (e.g. MF/7F20) could not be a parent and canonical entries under one were dropped on import. It is now a free-text path with suggestions from the root, custom DFs and DFs of the loaded tree (any depth); a bare FID chain is completed from the root selector. Validation accepts standard or not-yet-seen parents (flagged in the list), rejects a parent known to be an EF, and import keeps such entries. Custom-file edits re-inject the loaded tree: new nodes appear at once, renamed model nodes are restored and injected nodes removed when their entry goes away. Help updated (the form lives in Profiler, not Card reader). SW cache otaman-v176.
This commit is contained in:
@@ -338,8 +338,8 @@
|
||||
<h4 id="scenario-a" class="font-medium mb-1">Сценарий A — Работа с файлами, не входящими в модель pySim («Пользовательские файлы»)</h4>
|
||||
<ol class="list-decimal list-inside text-sm space-y-1 mb-3">
|
||||
<li>Получите FID целевого файла (документация вендора или анализ ATR/файловой системы; такие файлы часто отсутствуют в открытых спецификациях).</li>
|
||||
<li>Откройте вкладку <strong>«Картридер»</strong> → подвкладку <strong>«Пользовательские файлы»</strong>.</li>
|
||||
<li>Введите полный путь (например, <code class="font-mono text-sm">3F00/7F20/6F46</code>) и псевдоним (например, <code class="font-mono text-sm">EF.SPN</code>).</li>
|
||||
<li>Откройте вкладку <strong>«Профайлер»</strong> → подвкладку <strong>«Пользовательские файлы»</strong>.</li>
|
||||
<li>Выберите корень, введите путь родительского DF (стандартный DF из дерева или пользовательский DF, любой вложенности — при вводе появляются подсказки), 4-hex FID и псевдоним (например, <code class="font-mono text-sm">EF.SPN</code>).</li>
|
||||
<li>Нажмите <strong>Добавить</strong> — файл появится в дереве курсивом (непроверенный).</li>
|
||||
<li>Кликните по файлу для проверки существования; при успехе (<code class="font-mono text-sm">9000</code>) он работает как обычный файл.</li>
|
||||
<li>Читайте, редактируйте и сохраняйте hex-данные; переключайте <strong>Данные как на карте</strong> / <strong>Декодированные данные</strong>.</li>
|
||||
@@ -409,7 +409,7 @@
|
||||
|
||||
|
||||
<h4 id="custom-files" class="font-medium mb-1">Пользовательские файлы</h4>
|
||||
<p class="text-sm mb-3">Добавление файлов, не покрытых моделью pySim. Пользовательский файл всегда имеет полный путь от <strong>MF</strong> или ADF (<strong>ADF.USIM</strong>, <strong>ADF.ISIM</strong>): выберите корень, <strong>родительский DF</strong> (сам корень или DF, уже добавленный здесь — родитель должен существовать до добавления его файлов), 4-hex <strong>FID</strong> и псевдоним (<code class="font-mono text-sm">EF.…</code>/<code class="font-mono text-sm">DF.…</code>; префикс определяет, EF это или DF). Канонический путь убирает прежнюю неоднозначность, когда один и тот же файл можно было описать и относительно, и абсолютно. Добавленные файлы появляются в дереве «Файловый менеджер»; в каждой строке есть <strong>«Редактировать»</strong> (путь + псевдоним; изменение пути DF перепривязывает дочерние записи — кнопка становится <strong>«Сохранить»</strong>, <strong>«Отмена»</strong> отменяет) и <strong>«Удалить»</strong> (удаление DF удаляет и его дочерние записи после подтверждения). Список сохраняется в <code class="font-mono text-sm">localStorage</code>; обмен — <strong>Экспорт в JSON</strong> / <strong>Экспорт в файл</strong> / <strong>Импорт</strong>; старые относительные пути (например, <code class="font-mono text-sm">a153/4954</code>) разрешаются при загрузке, неразрешимые отбрасываются и отмечаются в списке.</p>
|
||||
<p class="text-sm mb-3">Добавление файлов, не покрытых моделью pySim. Пользовательский файл всегда имеет полный путь от <strong>MF</strong> или ADF (<strong>ADF.USIM</strong>, <strong>ADF.ISIM</strong>): выберите корень, введите путь <strong>родительского DF</strong> (сам корень, стандартный DF из дерева файлового менеджера или пользовательский DF — любой вложенности, с подсказками при вводе; родитель, ещё не встречавшийся в дереве, остаётся допустимым и помечается <code class="font-mono text-sm">⚠</code>), 4-hex <strong>FID</strong> и псевдоним (<code class="font-mono text-sm">EF.…</code>/<code class="font-mono text-sm">DF.…</code>; префикс определяет, EF это или DF). Канонический путь убирает прежнюю неоднозначность, когда один и тот же файл можно было описать и относительно, и абсолютно. Добавленные файлы появляются в дереве «Файловый менеджер»; в каждой строке есть <strong>«Редактировать»</strong> (путь + псевдоним; изменение пути DF перепривязывает дочерние записи — кнопка становится <strong>«Сохранить»</strong>, <strong>«Отмена»</strong> отменяет) и <strong>«Удалить»</strong> (удаление DF удаляет и его дочерние записи после подтверждения). Список сохраняется в <code class="font-mono text-sm">localStorage</code>; обмен — <strong>Экспорт в JSON</strong> / <strong>Экспорт в файл</strong> / <strong>Импорт</strong>; старые относительные пути (например, <code class="font-mono text-sm">a153/4954</code>) разрешаются при загрузке, неразрешимые отбрасываются и отмечаются в списке.</p>
|
||||
|
||||
<section class="mb-10">
|
||||
<h2 id="proactive-uicc" class="text-xl font-semibold mb-3 border-b border-gray-300 dark:border-slate-700 pb-1">6. Симулятор телефона</h2>
|
||||
|
||||
+3
-3
@@ -338,8 +338,8 @@
|
||||
<h4 id="scenario-a" class="font-medium mb-1">Scenario A — Working with files not in pySim’s model (Custom files)</h4>
|
||||
<ol class="list-decimal list-inside text-sm space-y-1 mb-3">
|
||||
<li>Obtain the FID of the target file (vendor documentation or ATR/file-system analysis; such files are often not in public specs).</li>
|
||||
<li>Open the <strong>Card reader</strong> tab → <strong>Custom files</strong> sub-tab.</li>
|
||||
<li>Enter the full path (e.g. <code class="font-mono text-sm">3F00/7F20/6F46</code>) and an alias (e.g. <code class="font-mono text-sm">EF.SPN</code>).</li>
|
||||
<li>Open the <strong>Profiler</strong> tab → <strong>Custom files</strong> sub-tab.</li>
|
||||
<li>Pick the root, type the parent DF path (a standard DF from the tree or a custom DF, any depth — suggestions appear while you type), the 4-hex FID and an alias (e.g. <code class="font-mono text-sm">EF.SPN</code>).</li>
|
||||
<li>Click <strong>Add</strong> — the file appears in the tree in italics (unverified).</li>
|
||||
<li>Click the file to verify existence; on success (<code class="font-mono text-sm">9000</code>) it behaves like a normal file.</li>
|
||||
<li>Read, edit and save hex data; toggle Raw/Decoded views.</li>
|
||||
@@ -409,7 +409,7 @@
|
||||
|
||||
|
||||
<h4 id="custom-files" class="font-medium mb-1">Custom files</h4>
|
||||
<p class="text-sm mb-3">Add files that pySim’s model does not cover. A custom file always has a full path rooted at <strong>MF</strong> or an ADF (<strong>ADF.USIM</strong>, <strong>ADF.ISIM</strong>): pick the root, the <strong>parent DF</strong> (the root itself, or a DF that was already added here — a parent must exist before its files can be added), the 4-hex <strong>FID</strong> and an alias (<code class="font-mono text-sm">EF.…</code>/<code class="font-mono text-sm">DF.…</code>; the prefix decides whether the entry is an EF or a DF). The canonical path removes the old ambiguity where the same file could be described both relatively and absolutely. Added files appear in the File manager tree; each row has <strong>Edit</strong> (path + alias; changing a DF’s path re-points its child entries — the button becomes <strong>Save</strong>, <strong>Cancel</strong> aborts) and <strong>Delete</strong> (deleting a DF also deletes its child entries after a confirmation). The list persists in <code class="font-mono text-sm">localStorage</code> and can be shared with <strong>Export as JSON</strong> / <strong>Export to file</strong> / <strong>Import</strong>; legacy relative paths (e.g. <code class="font-mono text-sm">a153/4954</code>) are resolved on load, unresolvable ones are dropped and reported in the list.</p>
|
||||
<p class="text-sm mb-3">Add files that pySim’s model does not cover. A custom file always has a full path rooted at <strong>MF</strong> or an ADF (<strong>ADF.USIM</strong>, <strong>ADF.ISIM</strong>): pick the root, type the <strong>parent DF</strong> path (the root itself, a standard DF known from the file-manager tree, or a custom DF — any depth, with suggestions while you type; a parent that has not been seen in the tree yet stays valid and is marked <code class="font-mono text-sm">⚠</code>), the 4-hex <strong>FID</strong> and an alias (<code class="font-mono text-sm">EF.…</code>/<code class="font-mono text-sm">DF.…</code>; the prefix decides whether the entry is an EF or a DF). The canonical path removes the old ambiguity where the same file could be described both relatively and absolutely. Added files appear in the File manager tree; each row has <strong>Edit</strong> (path + alias; changing a DF’s path re-points its child entries — the button becomes <strong>Save</strong>, <strong>Cancel</strong> aborts) and <strong>Delete</strong> (deleting a DF also deletes its child entries after a confirmation). The list persists in <code class="font-mono text-sm">localStorage</code> and can be shared with <strong>Export as JSON</strong> / <strong>Export to file</strong> / <strong>Import</strong>; legacy relative paths (e.g. <code class="font-mono text-sm">a153/4954</code>) are resolved on load, unresolvable ones are dropped and reported in the list.</p>
|
||||
|
||||
<section class="mb-10">
|
||||
<h2 id="proactive-uicc" class="text-xl font-semibold mb-3 border-b border-gray-300 dark:border-slate-700 pb-1">6. Phone simulator</h2>
|
||||
|
||||
+142
-24
@@ -18,7 +18,7 @@
|
||||
<div class="max-w-7xl mx-auto px-6 py-2">
|
||||
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<h1 class="text-2xl font-bold text-heading">OTAMan <span id="slogan" class="text-sm font-normal text-gray-500 dark:text-slate-400 ml-2" data-l10n="SIM OTA with a Human Face">SIM OTA with a Human Face</span> <span class="text-xs text-gray-400 dark:text-slate-500 ml-1">v2.2.10</span></h1>
|
||||
<h1 class="text-2xl font-bold text-heading">OTAMan <span id="slogan" class="text-sm font-normal text-gray-500 dark:text-slate-400 ml-2" data-l10n="SIM OTA with a Human Face">SIM OTA with a Human Face</span> <span class="text-xs text-gray-400 dark:text-slate-500 ml-1">v2.2.11</span></h1>
|
||||
<div class="flex items-center gap-4">
|
||||
<span id="state-indicator" class="flex items-center select-none" style="cursor:default" title="Connecting...">
|
||||
<span id="state-indicator-dot" class="text-xs text-gray-400" title="Connecting...">●</span>
|
||||
@@ -744,7 +744,8 @@
|
||||
</div>
|
||||
<div>
|
||||
<label class="block mb-1 text-xs font-medium text-gray-600 dark:text-slate-400" data-l10n="Parent DF">Parent DF</label>
|
||||
<select id="pysim-cf-parent" class="font-mono w-56 border border-gray-300 dark:border-slate-600 text-sm rounded px-2 py-2 dark:bg-slate-800"></select>
|
||||
<input id="pysim-cf-parent" list="pysim-cf-parent-list" oninput="pysimCustomParentInput()" onkeydown="if(event.key==='Enter')pysimCustomSubmit()" class="font-mono w-72 border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-2 dark:bg-slate-800" placeholder="MF/7F20 or ADF.USIM/5F3A">
|
||||
<datalist id="pysim-cf-parent-list"></datalist>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block mb-1 text-xs font-medium text-gray-600 dark:text-slate-400" data-l10n="FID (4 hex)">FID (4 hex)</label>
|
||||
@@ -757,7 +758,7 @@
|
||||
<button id="pysim-cf-add-btn" onclick="pysimCustomSubmit()" class="px-3 py-2 bg-blue-600 text-white text-sm font-medium rounded hover:bg-blue-700 whitespace-nowrap" data-l10n="Add">Add</button>
|
||||
<button id="pysim-cf-cancel-btn" onclick="pysimCustomEditCancel()" class="hidden px-3 py-2 bg-gray-600 text-white text-sm font-medium rounded hover:bg-gray-700 whitespace-nowrap" data-l10n="Cancel">Cancel</button>
|
||||
</div>
|
||||
<div class="text-xs text-gray-500 dark:text-slate-400 mb-2" data-l10n="Full path from MF or an ADF; the parent DF must be defined here first.">Full path from MF or an ADF; the parent DF must be defined here first.</div>
|
||||
<div class="text-xs text-gray-500 dark:text-slate-400 mb-2" data-l10n="The parent may be any DF path — a standard DF from the tree or a custom DF, any depth; parents not yet seen in the tree are marked ⚠.">The parent may be any DF path — a standard DF from the tree or a custom DF, any depth; parents not yet seen in the tree are marked ⚠.</div>
|
||||
<div id="pysim-cf-list" class="text-xs font-mono text-gray-600 dark:text-slate-400"></div>
|
||||
<input type="file" id="pysim-cf-file" accept=".json,application/json" class="hidden" onchange="pysimCustomImportFile(this)">
|
||||
<div class="flex flex-wrap gap-2 mt-2">
|
||||
@@ -6317,6 +6318,7 @@ function pysimCustomInject(node) {
|
||||
const fid = pysimCustomFid(c);
|
||||
const existing = (node.children || []).find(ch => (ch.fid || '').toUpperCase() === fid);
|
||||
if (existing) {
|
||||
if (existing.modelName === undefined) existing.modelName = existing.name;
|
||||
existing.name = c.name;
|
||||
existing.kind = c.kind;
|
||||
existing.custom = true;
|
||||
@@ -6338,6 +6340,37 @@ function pysimCustomInject(node) {
|
||||
}
|
||||
}
|
||||
|
||||
// Re-apply the custom-file list to the already-loaded tree: drop injected
|
||||
// nodes whose entry is gone, restore renamed model nodes, inject anew.
|
||||
function pysimCustomRefreshTree() {
|
||||
if (!pysimFsTreeRoot) return;
|
||||
const prune = node => {
|
||||
if (!node.children) return;
|
||||
node.children = node.children.filter(ch => {
|
||||
if (!ch.custom) return true;
|
||||
if (pysimCustomFiles.some(c => c.path === ch.customPath)) return true;
|
||||
if (ch.modelName !== undefined) {
|
||||
ch.name = ch.modelName;
|
||||
delete ch.modelName;
|
||||
delete ch.custom;
|
||||
delete ch.customPath;
|
||||
delete ch.kind;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
};
|
||||
const walk = node => {
|
||||
prune(node);
|
||||
pysimCustomInject(node);
|
||||
for (const ch of (node.children || [])) {
|
||||
if (ch.isDir) walk(ch);
|
||||
}
|
||||
};
|
||||
walk(pysimFsTreeRoot);
|
||||
pysimFsRenderTree();
|
||||
}
|
||||
|
||||
let pysimFsSort = localStorage.getItem('otaman_fs_sort') === 'name' ? 'name' : 'fid';
|
||||
|
||||
function pysimFsSortChildren(children, mode) {
|
||||
@@ -6583,6 +6616,23 @@ function pysimFsFindNode(name, root) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Find a node by its canonical path ("MF/7F20/6F46", "ADF.USIM/6F07"), so
|
||||
// custom-file parents can be resolved against the loaded tree (standard DFs
|
||||
// included, at any depth). Returns null when the path is not loaded yet.
|
||||
function pysimFsFindNodeByPath(path) {
|
||||
const want = pysimCustomNormPath(path);
|
||||
if (!want || !pysimFsTreeRoot) return null;
|
||||
const walk = node => {
|
||||
if (pysimCustomNormPath(pysimFsNodePath(node)) === want) return node;
|
||||
for (const c of (node.children || [])) {
|
||||
const hit = walk(c);
|
||||
if (hit) return hit;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
return walk(pysimFsTreeRoot);
|
||||
}
|
||||
|
||||
async function pysimFsToggleDir(name) {
|
||||
const node = pysimFsFindNode(name, pysimFsTreeRoot);
|
||||
if (!node || !node.isDir) return;
|
||||
@@ -8730,20 +8780,66 @@ function pysimCustomFid(entry) { return entry.path.split('/').pop(); }
|
||||
function pysimCustomParent(entry) { return entry.path.split('/').slice(0, -1).join('/'); }
|
||||
function pysimCustomRoot(entry) { return entry.path.split('/')[0]; }
|
||||
|
||||
// Parent DF suggestions for the current root: the root, custom DFs and every
|
||||
// DF node known from the loaded file-manager tree (standard pySim files),
|
||||
// at any depth.
|
||||
function pysimCustomKnownDfPaths(root) {
|
||||
const out = [];
|
||||
const seen = new Set();
|
||||
const add = p => { if (p && !seen.has(p)) { seen.add(p); out.push(p); } };
|
||||
add(root);
|
||||
for (const c of pysimCustomFiles) {
|
||||
if (c.kind === 'df' && pysimCustomRoot(c) === root) add(c.path);
|
||||
}
|
||||
const walk = node => {
|
||||
for (const ch of (node.children || [])) {
|
||||
if (!ch.isDir) continue;
|
||||
const p = pysimFsNodePath(ch);
|
||||
if (p && p.split('/')[0] === root) add(p);
|
||||
walk(ch);
|
||||
}
|
||||
};
|
||||
if (pysimFsTreeRoot) walk(pysimFsTreeRoot);
|
||||
return out.sort();
|
||||
}
|
||||
|
||||
// Where a parent path comes from: root, a custom DF, a DF of the loaded tree,
|
||||
// a known non-DF (error), or unknown (allowed, flagged as a warning).
|
||||
function pysimCustomParentStatus(parentPath) {
|
||||
const p = pysimCustomNormPath(parentPath);
|
||||
if (!p || p.split('/').length <= 1) return {status: 'root', source: null};
|
||||
const c = pysimCustomFiles.find(e => e.path === p);
|
||||
if (c) return {status: c.kind === 'df' ? 'df' : 'not-df', source: 'custom'};
|
||||
const node = (typeof pysimFsFindNodeByPath === 'function') ? pysimFsFindNodeByPath(p) : null;
|
||||
if (node) return {status: node.isDir ? 'df' : 'not-df', source: 'tree'};
|
||||
return {status: 'unknown', source: null};
|
||||
}
|
||||
|
||||
function pysimCustomValidate(root, parentPath, fid, name, entries, editIndex) {
|
||||
if (!CUSTOM_ROOTS.includes(root)) {
|
||||
parentPath = pysimCustomNormPath(parentPath) || root;
|
||||
// a bare FID chain typed without the root is completed from the selector
|
||||
const first = parentPath.split('/')[0];
|
||||
if (!CUSTOM_ROOTS.includes(first) && /^[0-9A-F]{4}$/.test(first)) {
|
||||
parentPath = pysimCustomNormPath(root + '/' + parentPath);
|
||||
}
|
||||
if (!CUSTOM_ROOTS.includes(parentPath.split('/')[0])) {
|
||||
return {error: t('Root must be MF, ADF.USIM or ADF.ISIM')};
|
||||
}
|
||||
const segs = parentPath.split('/');
|
||||
if (segs.length > 1 && !segs.slice(1).every(s => /^[0-9A-F]{4}$/.test(s))) {
|
||||
return {error: t('Parent must be the root or a chain of 4-hex FIDs (e.g. MF/7F20)')};
|
||||
}
|
||||
fid = (fid || '').trim().toUpperCase();
|
||||
if (!/^[0-9A-F]{4}$/.test(fid)) return {error: t('FID must be 4 hex characters')};
|
||||
const kind = pysimCustomKindForName(name);
|
||||
if (!kind) return {error: t('Alias must start with "EF." or "DF."')};
|
||||
if (parentPath !== root) {
|
||||
const idx = entries.findIndex((e, i) => i !== editIndex && e.path === parentPath);
|
||||
if (idx < 0) return {error: t('Parent DF is not defined as a custom file:') + ' ' + parentPath};
|
||||
if (entries[idx].kind !== 'df') return {error: t('Parent is not a DF:') + ' ' + parentPath};
|
||||
// A parent known here to be an EF is an error; a parent that is a standard
|
||||
// DF (or not seen yet) is fine - the tree resolves it later.
|
||||
const parent = entries.find((e, i) => i !== editIndex && e.path === parentPath);
|
||||
if (parent && parent.kind !== 'df') {
|
||||
return {error: t('Parent is not a DF:') + ' ' + parentPath};
|
||||
}
|
||||
const path = (parentPath === root ? root : parentPath) + '/' + fid;
|
||||
const path = parentPath + '/' + fid;
|
||||
if (entries.some((e, i) => i !== editIndex && e.path === path)) {
|
||||
return {error: t('File already defined:') + ' ' + path};
|
||||
}
|
||||
@@ -8836,25 +8932,36 @@ function pysimCustomRenderRoots() {
|
||||
}
|
||||
|
||||
function pysimCustomRenderParents() {
|
||||
const sel = document.getElementById('pysim-cf-parent');
|
||||
if (!sel) return;
|
||||
const list = document.getElementById('pysim-cf-parent-list');
|
||||
if (!list) return;
|
||||
const root = document.getElementById('pysim-cf-root').value || CUSTOM_ROOTS[0];
|
||||
const prev = sel.value;
|
||||
let html = '<option value="' + esc(root) + '">' + esc(t('— root —')) + '</option>';
|
||||
for (const c of pysimCustomFiles) {
|
||||
if (c.kind !== 'df' || pysimCustomRoot(c) !== root) continue;
|
||||
html += '<option value="' + esc(c.path) + '">' + esc(c.path + ' → ' + c.name) + '</option>';
|
||||
}
|
||||
sel.innerHTML = html;
|
||||
if (prev && (prev === root || pysimCustomFiles.some(c => c.path === prev && c.kind === 'df'))) {
|
||||
sel.value = prev;
|
||||
let html = '';
|
||||
for (const p of pysimCustomKnownDfPaths(root)) {
|
||||
const c = pysimCustomFiles.find(e => e.path === p && e.kind === 'df');
|
||||
html += '<option value="' + esc(p) + '">' + esc(c ? p + ' → ' + c.name : p) + '</option>';
|
||||
}
|
||||
list.innerHTML = html;
|
||||
}
|
||||
|
||||
function pysimCustomRootChanged() {
|
||||
const sel = document.getElementById('pysim-cf-root');
|
||||
const inp = document.getElementById('pysim-cf-parent');
|
||||
const first = pysimCustomNormPath(inp && inp.value).split('/')[0];
|
||||
if (CUSTOM_ROOTS.includes(first) && first !== sel.value) inp.value = sel.value;
|
||||
pysimCustomRenderParents();
|
||||
}
|
||||
|
||||
// Keep the root selector (and its suggestions) in step with a typed parent path.
|
||||
function pysimCustomParentInput() {
|
||||
const sel = document.getElementById('pysim-cf-root');
|
||||
const inp = document.getElementById('pysim-cf-parent');
|
||||
const first = pysimCustomNormPath(inp && inp.value).split('/')[0];
|
||||
if (CUSTOM_ROOTS.includes(first) && sel.value !== first) {
|
||||
sel.value = first;
|
||||
pysimCustomRenderParents();
|
||||
}
|
||||
}
|
||||
|
||||
function pysimCustomSubmit() {
|
||||
const root = document.getElementById('pysim-cf-root').value;
|
||||
const parentPath = document.getElementById('pysim-cf-parent').value || root;
|
||||
@@ -8863,6 +8970,11 @@ function pysimCustomSubmit() {
|
||||
const name = nameEl.value.trim();
|
||||
const res = pysimCustomValidate(root, parentPath, fid, name, pysimCustomFiles, pysimCustomEditIndex);
|
||||
if (res.error) { alert(res.error); return; }
|
||||
const parent = pysimCustomParent({path: res.path});
|
||||
if (pysimCustomParentStatus(parent).status === 'not-df') {
|
||||
alert(t('Parent is not a DF:') + ' ' + parent);
|
||||
return;
|
||||
}
|
||||
if (pysimCustomEditIndex !== null) {
|
||||
const oldPath = pysimCustomFiles[pysimCustomEditIndex].path;
|
||||
if (oldPath !== res.path) pysimCustomRewriteDescendants(pysimCustomFiles, oldPath, res.path);
|
||||
@@ -8873,6 +8985,7 @@ function pysimCustomSubmit() {
|
||||
pysimCustomSave();
|
||||
pysimCustomRender();
|
||||
pysimCustomEditCancel();
|
||||
pysimCustomRefreshTree();
|
||||
}
|
||||
|
||||
function pysimCustomEdit(i) {
|
||||
@@ -8919,6 +9032,7 @@ function pysimCustomRemove(i) {
|
||||
}
|
||||
pysimCustomSave();
|
||||
pysimCustomRender();
|
||||
pysimCustomRefreshTree();
|
||||
}
|
||||
|
||||
function pysimCustomRender() {
|
||||
@@ -8936,8 +9050,11 @@ function pysimCustomRender() {
|
||||
}
|
||||
for (let i = 0; i < pysimCustomFiles.length; i++) {
|
||||
const c = pysimCustomFiles[i];
|
||||
const warn = pysimCustomParentStatus(pysimCustomParent(c)).status === 'unknown'
|
||||
? '<span class="text-amber-600 dark:text-amber-400" title="' + esc(t('Parent DF not seen in the tree yet')) + '">⚠</span> '
|
||||
: '';
|
||||
html += '<div class="flex items-center gap-2 py-1 border-b border-gray-200 dark:border-slate-700">';
|
||||
html += '<span class="text-gray-500">' + esc(c.path) + '</span> <span class="text-blue-600 dark:text-blue-400">→</span> <b>' + esc(c.name) + '</b>';
|
||||
html += '<span class="text-gray-500">' + warn + esc(c.path) + '</span> <span class="text-blue-600 dark:text-blue-400">→</span> <b>' + esc(c.name) + '</b>';
|
||||
html += '<span class="text-gray-400">(' + esc(c.kind.toUpperCase()) + ')</span>';
|
||||
html += '<span class="ml-auto flex gap-2">';
|
||||
html += '<button onclick="pysimCustomEdit(' + i + ')" class="px-2 py-0.5 text-xs rounded bg-blue-600 text-white hover:bg-blue-700">' + esc(t('Edit')) + '</button>';
|
||||
@@ -8969,6 +9086,7 @@ async function pysimCustomImport(text) {
|
||||
pysimCustomFiles = res.files;
|
||||
pysimCustomSave();
|
||||
pysimCustomRender();
|
||||
pysimCustomRefreshTree();
|
||||
const el = document.getElementById('pysim-cf-io');
|
||||
el.value = 'Imported ' + added + ' file(s)' + (res.dropped ? ', dropped ' + res.dropped : '') + '.';
|
||||
setTimeout(() => { el.classList.add('hidden'); }, 2000);
|
||||
@@ -11309,14 +11427,14 @@ const LANG_RU = {
|
||||
'Parent DF': 'Родительский DF',
|
||||
'FID (4 hex)': 'FID (4 hex)',
|
||||
'Alias (EF. / DF.)': 'Псевдоним (EF. / DF.)',
|
||||
'Full path from MF or an ADF; the parent DF must be defined here first.': 'Полный путь от MF или ADF; родительский DF должен быть создан здесь заранее.',
|
||||
'The parent may be any DF path — a standard DF from the tree or a custom DF, any depth; parents not yet seen in the tree are marked ⚠.': 'Родителем может быть любой путь DF — стандартный DF из дерева или пользовательский DF, любой вложенности; DF, ещё не встречавшиеся в дереве, помечаются ⚠.',
|
||||
'Root must be MF, ADF.USIM or ADF.ISIM': 'Корень должен быть MF, ADF.USIM или ADF.ISIM',
|
||||
'FID must be 4 hex characters': 'FID должен состоять из 4 hex-символов',
|
||||
'Alias must start with "EF." or "DF."': 'Псевдоним должен начинаться с «EF.» или «DF.»',
|
||||
'Parent DF is not defined as a custom file:': 'Родительский DF не создан как пользовательский файл:',
|
||||
'Parent must be the root or a chain of 4-hex FIDs (e.g. MF/7F20)': 'Родитель — корень или цепочка 4-hex FID (например, MF/7F20)',
|
||||
'Parent is not a DF:': 'Родитель не является DF:',
|
||||
'Parent DF not seen in the tree yet': 'Родительский DF ещё не встречался в дереве',
|
||||
'File already defined:': 'Файл уже задан:',
|
||||
'— root —': '— корень —',
|
||||
'and': 'и',
|
||||
'child file(s)?': 'дочерних файлов?',
|
||||
'Dropped invalid legacy entries:': 'Отброшено некорректных старых записей:',
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
const CACHE = 'otaman-v175';
|
||||
const CACHE = 'otaman-v176';
|
||||
const URLS = [
|
||||
'index.html',
|
||||
'help.html',
|
||||
|
||||
@@ -21,17 +21,18 @@ function extractFunc(src, name) {
|
||||
return src.slice(m.index, i + 1);
|
||||
}
|
||||
|
||||
let code = 'var pysimCustomFiles = [];\nvar pysimCustomEditIndex = null;\nvar _pysimCustomDropped = 0;\n';
|
||||
let code = 'var pysimCustomFiles = [];\nvar pysimCustomEditIndex = null;\nvar _pysimCustomDropped = 0;\nvar pysimFsTreeRoot = null;\n';
|
||||
for (const fn of ['pysimCustomNormPath', 'pysimCustomKindForName', 'pysimCustomFid',
|
||||
'pysimCustomParent', 'pysimCustomRoot', 'pysimCustomValidate',
|
||||
'pysimCustomRewriteDescendants', 'pysimCustomNormalizeEntries', 'pysimCustomSave',
|
||||
'pysimCustomRenderRoots', 'pysimCustomRenderParents', 'pysimCustomRootChanged',
|
||||
'pysimCustomSubmit', 'pysimCustomEdit', 'pysimCustomEditCancel',
|
||||
'pysimCustomRemove', 'pysimCustomRender']) {
|
||||
'pysimCustomParent', 'pysimCustomRoot', 'pysimCustomKnownDfPaths', 'pysimCustomParentStatus',
|
||||
'pysimCustomValidate', 'pysimCustomRewriteDescendants', 'pysimCustomNormalizeEntries',
|
||||
'pysimCustomSave', 'pysimCustomRenderRoots', 'pysimCustomRenderParents',
|
||||
'pysimCustomRootChanged', 'pysimCustomParentInput', 'pysimCustomSubmit',
|
||||
'pysimCustomEdit', 'pysimCustomEditCancel', 'pysimCustomRemove', 'pysimCustomRender',
|
||||
'pysimFsNodePath', 'pysimFsFindNodeByPath', 'pysimCustomInject', 'pysimCustomRefreshTree']) {
|
||||
code += extractFunc(html, fn) + '\n';
|
||||
}
|
||||
code += html.match(/const CUSTOM_ROOTS = \[[^\]]*\];/)[0].replace('const ', 'var ') + '\n';
|
||||
code += 'globalThis.esc = s => s;\nglobalThis.t = s => s;\n';
|
||||
code += 'globalThis.esc = s => s;\nglobalThis.t = s => s;\nglobalThis.pysimFsRenderTree = () => {};\n';
|
||||
eval(code);
|
||||
|
||||
function fakeEl(id) {
|
||||
@@ -53,6 +54,7 @@ function setup(entries) {
|
||||
const els = {
|
||||
'pysim-cf-root': fakeEl('pysim-cf-root'),
|
||||
'pysim-cf-parent': fakeEl('pysim-cf-parent'),
|
||||
'pysim-cf-parent-list': fakeEl('pysim-cf-parent-list'),
|
||||
'pysim-cf-fid': fakeEl('pysim-cf-fid'),
|
||||
'pysim-cf-name': fakeEl('pysim-cf-name'),
|
||||
'pysim-cf-list': fakeEl('pysim-cf-list'),
|
||||
@@ -73,6 +75,7 @@ function setup(entries) {
|
||||
pysimCustomFiles = (entries || []).map(e => Object.assign({}, e));
|
||||
pysimCustomEditIndex = null;
|
||||
_pysimCustomDropped = 0;
|
||||
pysimFsTreeRoot = null;
|
||||
pysimCustomRenderRoots();
|
||||
pysimCustomRenderParents();
|
||||
return els;
|
||||
@@ -115,12 +118,24 @@ test('migration resolves legacy relative paths and drops the unresolvable', () =
|
||||
assert.strictEqual(res.dropped, 2);
|
||||
});
|
||||
|
||||
test('validation requires a defined parent DF and a valid FID/alias', () => {
|
||||
test('validation accepts standard/unknown parents but rejects known EFs', () => {
|
||||
const files = [{ path: 'MF/A153', name: 'DF.A1', kind: 'df' }];
|
||||
// parent not defined
|
||||
assert.match(pysimCustomValidate('MF', 'MF/A999', '6F46', 'EF.SPN', files, null).error, /not defined/);
|
||||
// a parent that is not a custom entry is accepted: it may be a standard DF
|
||||
// from the card model, or simply not seen in the tree yet
|
||||
const unknown = pysimCustomValidate('MF', 'MF/A999', '6F46', 'EF.SPN', files, null);
|
||||
assert.strictEqual(unknown.error, null);
|
||||
assert.strictEqual(unknown.path, 'MF/A999/6F46');
|
||||
// parent defined -> ok
|
||||
assert.strictEqual(pysimCustomValidate('MF', 'MF/A153', '6F46', 'EF.SPN', files, null).path, 'MF/A153/6F46');
|
||||
// a bare FID chain typed without the root is completed from the selector
|
||||
assert.strictEqual(pysimCustomValidate('MF', 'A153', '6F46', 'EF.SPN', files, null).path, 'MF/A153/6F46');
|
||||
// deep chains are fine
|
||||
assert.strictEqual(pysimCustomValidate('MF', 'MF/7F20/5F01', '6F46', 'EF.DEEP', [], null).path, 'MF/7F20/5F01/6F46');
|
||||
// a parent known here to be an EF is rejected
|
||||
assert.match(pysimCustomValidate('MF', 'MF/A153/6F46', '1234', 'EF.X',
|
||||
files.concat([{ path: 'MF/A153/6F46', name: 'EF.OTHER', kind: 'ef' }]), null).error, /not a DF/);
|
||||
// parent segments must be 4-hex FIDs
|
||||
assert.match(pysimCustomValidate('MF', 'MF/FOO', '6F46', 'EF.SPN', files, null).error, /4-hex/);
|
||||
// bad FID
|
||||
assert.match(pysimCustomValidate('MF', 'MF', '6F4', 'EF.SPN', files, null).error, /4 hex/);
|
||||
// bad alias
|
||||
@@ -135,21 +150,62 @@ test('validation requires a defined parent DF and a valid FID/alias', () => {
|
||||
assert.match(pysimCustomValidate('MFX', 'MFX', '6F46', 'EF.SPN', files, null).error, /Root/);
|
||||
});
|
||||
|
||||
test('root and parent selectors list roots and defined DFs', () => {
|
||||
test('root and parent suggestions list roots, custom DFs and tree DFs', () => {
|
||||
const els = setup([
|
||||
{ path: 'MF/A153', name: 'DF.A1', kind: 'df' },
|
||||
{ path: 'MF/A153/4954', name: 'EF.SPNS', kind: 'ef' },
|
||||
{ path: 'ADF.USIM/6F07', name: 'EF.IMSI', kind: 'ef' },
|
||||
]);
|
||||
assert.deepStrictEqual(els['pysim-cf-root'].options(), ['MF', 'ADF.USIM', 'ADF.ISIM']);
|
||||
const parents = els['pysim-cf-parent'].options();
|
||||
let parents = els['pysim-cf-parent-list'].options();
|
||||
assert.ok(parents.includes('MF'));
|
||||
assert.ok(parents.includes('MF/A153'));
|
||||
assert.ok(!parents.includes('MF/A153/4954'), 'EFs are not parent options');
|
||||
// DFs known from the loaded file tree are suggested, at any depth
|
||||
pysimFsTreeRoot = { name: 'MF', fid: '3F00', isDir: true, parent: null, children: [] };
|
||||
const df = { name: 'DF.TELECOM', fid: '7F10', isDir: true, parent: pysimFsTreeRoot, children: [] };
|
||||
const sub = { name: 'DF.SUB', fid: '5F01', isDir: true, parent: df, children: [] };
|
||||
df.children = [sub];
|
||||
pysimFsTreeRoot.children = [df];
|
||||
pysimCustomRenderParents();
|
||||
parents = els['pysim-cf-parent-list'].options();
|
||||
assert.ok(parents.includes('MF/7F10'));
|
||||
assert.ok(parents.includes('MF/7F10/5F01'));
|
||||
// the ADF root is always a valid parent
|
||||
els['pysim-cf-root'].value = 'ADF.USIM';
|
||||
pysimCustomRootChanged();
|
||||
assert.deepStrictEqual(els['pysim-cf-parent'].options(), ['ADF.USIM']);
|
||||
assert.deepStrictEqual(els['pysim-cf-parent-list'].options(), ['ADF.USIM']);
|
||||
});
|
||||
|
||||
test('parent status classifies root, custom, tree and unknown parents', () => {
|
||||
setup([
|
||||
{ path: 'MF/A153', name: 'DF.A1', kind: 'df' },
|
||||
{ path: 'MF/A153/4954', name: 'EF.SPNS', kind: 'ef' },
|
||||
]);
|
||||
assert.strictEqual(pysimCustomParentStatus('MF').status, 'root');
|
||||
assert.strictEqual(pysimCustomParentStatus('MF/A153').status, 'df');
|
||||
assert.strictEqual(pysimCustomParentStatus('MF/A153/4954').status, 'not-df');
|
||||
assert.strictEqual(pysimCustomParentStatus('MF/FFFF').status, 'unknown');
|
||||
pysimFsTreeRoot = { name: 'MF', fid: '3F00', isDir: true, parent: null, children: [] };
|
||||
const df = { name: 'DF.GSM', fid: '7F20', isDir: true, parent: pysimFsTreeRoot, children: [] };
|
||||
df.children = [{ name: 'EF.SPN', fid: '6F46', isDir: false, parent: df, children: null }];
|
||||
pysimFsTreeRoot.children = [df];
|
||||
assert.strictEqual(pysimCustomParentStatus('MF/7F20').status, 'df');
|
||||
assert.strictEqual(pysimCustomParentStatus('MF/7F20/6F46').status, 'not-df');
|
||||
assert.strictEqual(pysimCustomParentStatus('MF/7F20/9999').status, 'unknown');
|
||||
});
|
||||
|
||||
test('import keeps canonical entries whose parent is outside the custom list', () => {
|
||||
const res = pysimCustomNormalizeEntries([
|
||||
{ path: '3F00/7f20/5f01/6f46', name: 'EF.DEEP' },
|
||||
{ path: 'mf/ffff/6f46', name: 'EF.ORPHAN' },
|
||||
{ path: 'MF/6F46', name: 'NOPE' },
|
||||
], []);
|
||||
assert.deepStrictEqual(res.files, [
|
||||
{ path: 'MF/7F20/5F01/6F46', name: 'EF.DEEP', kind: 'ef' },
|
||||
{ path: 'MF/FFFF/6F46', name: 'EF.ORPHAN', kind: 'ef' },
|
||||
]);
|
||||
assert.strictEqual(res.dropped, 1);
|
||||
});
|
||||
|
||||
test('submit adds files under the root and under a defined DF', () => {
|
||||
@@ -165,13 +221,17 @@ test('submit adds files under the root and under a defined DF', () => {
|
||||
pysimCustomSubmit();
|
||||
assert.deepStrictEqual(pysimCustomFiles.map(c => c.path),
|
||||
['MF/6F46', 'MF/A153', 'MF/A153/4954']);
|
||||
// adding under an undefined parent is rejected
|
||||
fill(els, 'MF', 'MF', '1111', 'EF.X');
|
||||
els['pysim-cf-parent'].value = 'MF/A153';
|
||||
els['pysim-cf-fid'].value = '2222';
|
||||
els['pysim-cf-name'].value = 'BAD';
|
||||
// a parent that is not a custom entry is allowed (it may be a standard DF
|
||||
// the tree has not loaded yet)
|
||||
fill(els, 'MF', 'MF/A153', '2222', 'EF.ORPHAN');
|
||||
pysimCustomSubmit();
|
||||
assert.ok(globalThis.alertCalls.length === 1);
|
||||
assert.deepStrictEqual(pysimCustomFiles.map(c => c.path),
|
||||
['MF/6F46', 'MF/A153', 'MF/A153/4954', 'MF/A153/2222']);
|
||||
assert.strictEqual(globalThis.alertCalls.length, 0);
|
||||
// a parent known here to be an EF is rejected
|
||||
fill(els, 'MF', 'MF/A153/4954', '3333', 'EF.NOPE');
|
||||
pysimCustomSubmit();
|
||||
assert.deepStrictEqual(globalThis.alertCalls, ['Parent is not a DF: MF/A153/4954']);
|
||||
});
|
||||
|
||||
test('editing a DF FID rewrites its descendants', () => {
|
||||
|
||||
@@ -21,10 +21,11 @@ function extractFunc(src, name) {
|
||||
return src.slice(m.index, i + 1);
|
||||
}
|
||||
|
||||
let code = 'var pysimCustomFiles = [];\n';
|
||||
for (const fn of ['pysimFsNodePath', 'pysimCustomInject', 'pysimCustomParent', 'pysimCustomFid']) {
|
||||
let code = 'var pysimCustomFiles = [];\nvar pysimFsTreeRoot = null;\n';
|
||||
for (const fn of ['pysimFsNodePath', 'pysimCustomInject', 'pysimCustomRefreshTree', 'pysimCustomParent', 'pysimCustomFid']) {
|
||||
code += extractFunc(html, fn) + '\n';
|
||||
}
|
||||
code += 'globalThis.pysimFsRenderTree = () => {};\n';
|
||||
eval(code);
|
||||
|
||||
function node(name, fid, parent, children) {
|
||||
@@ -100,3 +101,25 @@ test('injected DFs are directories and can host their own children', () => {
|
||||
assert.strictEqual(df.children.length, 1);
|
||||
assert.strictEqual(df.children[0].name, 'EF.UNDER-NEW');
|
||||
});
|
||||
|
||||
test('refresh restores renamed model nodes and drops injected ones', () => {
|
||||
const t = tree();
|
||||
pysimFsTreeRoot = t.mf;
|
||||
pysimCustomFiles = [{ path: 'MF/5F03/6F46', name: 'EF.RENAMED', kind: 'ef' }];
|
||||
pysimCustomRefreshTree();
|
||||
assert.strictEqual(t.dfC.children[0].name, 'EF.RENAMED');
|
||||
assert.strictEqual(t.dfC.children[0].custom, true);
|
||||
// deleting the entry brings the model name back
|
||||
pysimCustomFiles = [];
|
||||
pysimCustomRefreshTree();
|
||||
assert.strictEqual(t.dfC.children[0].name, 'EF.X');
|
||||
assert.ok(!t.dfC.children[0].custom);
|
||||
assert.strictEqual(t.dfC.children[0].modelName, undefined);
|
||||
// injected nodes disappear together with their entry
|
||||
pysimCustomFiles = [{ path: 'MF/5F10', name: 'DF.NEW', kind: 'df' }];
|
||||
pysimCustomRefreshTree();
|
||||
assert.ok(t.mf.children.some(c => c.fid === '5F10'));
|
||||
pysimCustomFiles = [];
|
||||
pysimCustomRefreshTree();
|
||||
assert.ok(!t.mf.children.some(c => c.fid === '5F10'));
|
||||
});
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "pysim-otaman-server"
|
||||
version = "2.2.10"
|
||||
version = "2.2.11"
|
||||
description = "HTTP REST server wrapping pysim for the OTAMan PWA"
|
||||
requires-python = ">=3.8"
|
||||
# pysim is a git-only dependency installed explicitly by setup.bat/setup.sh.
|
||||
|
||||
@@ -21,7 +21,7 @@ from osmocom.construct import GsmOrUcs2Adapter
|
||||
from osmocom.tlv import BER_TLV_IE
|
||||
|
||||
|
||||
VERSION = '2.2.10'
|
||||
VERSION = '2.2.11'
|
||||
|
||||
MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE
|
||||
|
||||
|
||||
Reference in New Issue
Block a user