v3.25.0 review: the structured access-rule editor

The access-rules block was read-only and its Check radio was disabled
without captured lines, so a hand-made rule could not define rules and
captured ones could not be adjusted.  It now edits the decoded rule's
structure:

- fcpArrRuleParse/fcpArrRuleEncode parse a canonical decoded line into
  the AM (bit mask, RESIZE FILE, INCREASE) + SC condition model and
  re-encode it byte-identically (round-trip tested); the template forms
  (OR/AND/NOT, SM CRTs, command descriptions) stay raw and removable.
- The editor renders each rule as operation checkboxes (or the command
  form) and condition selects (always / never / a key reference with
  its usage), with a preview line of the canonical string the check
  compares; + rule adds READ always; + condition / x adjust the rows.
- The None/Check radio no longer disables; a new rule can be built by
  hand and captured rules adjusted.  The stored format is unchanged
  (the canonical strings), so capture, the check, the set comparison
  and validation are untouched.

Tests: parse/encode round-trips (incl. raw forms), the mutators, the
editor render, plus the existing check tests.  882 frontend + 858
Python; sw cache simple-v424.
This commit is contained in:
2026-10-10 23:55:07 +03:00
parent a37266f865
commit c69a779ca4
5 changed files with 399 additions and 29 deletions
+1 -1
View File
@@ -466,7 +466,7 @@
<ul class="list-disc list-inside text-sm space-y-1 mb-3">
<li><strong>Путь</strong> — начинается с <code class="font-mono text-sm">MF</code> (например, <code class="font-mono text-sm">MF/7F10/6F3A</code>) или с AID ADF (например, <code class="font-mono text-sm">A0000000871002/6F07</code>).</li>
<li><strong>Проверка FCP/FCI</strong> — какая часть информации об управлении файлом проверяется: <strong>Только тип файла (FCP)</strong> (существование + тип файла), <strong>Тип файла + размер (FCP)</strong> (добавляются размер файла либо длина/число записей для record-файлов) или <strong>Полный FCI</strong> (добавляется побайтовое сравнение сырого ответа SELECT — шаблона FCP <code class="font-mono text-sm">'62'</code> — выявляет изменения FID/AID, жизненного цикла, атрибутов безопасности и проприетарных параметров).</li>
<li><strong>Правила доступа</strong> &mdash; проверяет условия доступа файла как <em>декодированные правила</em>, независимо от того, на какую запись <code class="font-mono text-sm">EF.ARR</code> указывает ссылка атрибута безопасности (<code class="font-mono text-sm">'8B'</code>) в FCI: правила указанной записи разрешаются из <code class="font-mono text-sm">EF.ARR</code> карты (или снимка) и сравниваются как множество с захваченными строками. Захватываются автоматически при создании профиля с карты или из снимка для каждого файла, чей FCI содержит ссылку (собственное содержимое EF.ARR по умолчанию пропускается &mdash; значимая проверка это правила); в редакторе захваченные строки показываются только для чтения с переключателем «Нет/Проверять». Файл, чей FCI потерял ссылку, не проходит проверку; ссылка, которую не удалось разрешить (нет кэша <code class="font-mono text-sm">EF.ARR</code>, запись вне диапазона), помечается как непроверяемая. При <strong>Полном FCI</strong> необязательный флажок <em>«Игнорировать ссылку на EF.ARR в точном FCI»</em> сравнивает FCI без значения <code class="font-mono text-sm">'8B'</code>: другой номер записи больше не приводит к несовпадению, а все остальные DO по-прежнему сравниваются побайтово (смысл ссылки проверяет эта проверка).</li>
<li><strong>Правила доступа</strong> &mdash; проверяет условия доступа файла как <em>декодированные правила</em>, независимо от того, на какую запись <code class="font-mono text-sm">EF.ARR</code> указывает ссылка атрибута безопасности (<code class="font-mono text-sm">'8B'</code>) в FCI: правила указанной записи разрешаются из <code class="font-mono text-sm">EF.ARR</code> карты (или снимка) и сравниваются как множество с захваченными строками. Захватываются автоматически при создании профиля с карты или из снимка для каждого файла, чей FCI содержит ссылку (собственное содержимое EF.ARR по умолчанию пропускается &mdash; значимая проверка это правила); в редакторе захваченные правила показываются структурированными строками &mdash; операция в виде флажков битов (или командной формы RESIZE FILE / INCREASE), а каждое условие безопасности — селектором (always, never или ссылка на ключ с её использованием), с предпросмотром канонической декодированной строки, которую сравнивает проверка; «+ правило» создаёт новое (READ always), а шаблонные формы вне этого набора остаются нередактируемыми строками с возможностью удаления. Переключатель «Нет/Проверять» отключает проверку, не теряя правила. Файл, чей FCI потерял ссылку, не проходит проверку; ссылка, которую не удалось разрешить (нет кэша <code class="font-mono text-sm">EF.ARR</code>, запись вне диапазона), помечается как непроверяемая. При <strong>Полном FCI</strong> необязательный флажок <em>«Игнорировать ссылку на EF.ARR в точном FCI»</em> сравнивает FCI без значения <code class="font-mono text-sm">'8B'</code>: другой номер записи больше не приводит к несовпадению, а все остальные DO по-прежнему сравниваются побайтово (смысл ссылки проверяет эта проверка).</li>
<li><strong>Атрибуты файла</strong> — тип файла, размер, длина записи и число записей из FCP-шаблона (любой можно оставить незаданным).</li>
<li><strong>Проверить содержимое</strong> (опционально) — <strong>Точное</strong> (точное совпадение hex) или <strong>Маска</strong>, где <code class="font-mono text-sm">?</code> — шаблон на один полубайт (маска без <code class="font-mono text-sm">?</code> — совпадение префикса, например <code class="font-mono text-sm">0891</code> для MCC/MNC из IMSI). Для record-файлов хранится список по записям.</li>
</ul>
+1 -1
View File
@@ -465,7 +465,7 @@
<ul class="list-disc list-inside text-sm space-y-1 mb-3">
<li><strong>Path</strong> — starts with <code class="font-mono text-sm">MF</code> (e.g. <code class="font-mono text-sm">MF/7F10/6F3A</code>) or an ADF AID (e.g. <code class="font-mono text-sm">A0000000871002/6F07</code>).</li>
<li><strong>FCP/FCI check</strong> — how much of the file control information to verify: <strong>Filetype only (FCP)</strong> (existence + file type), <strong>Filetype + size (FCP)</strong> (adds file size, or record length/count for record files), or <strong>Exact FCI</strong> (adds a byte-for-byte comparison of the raw SELECT response — the FCP template <code class="font-mono text-sm">'62'</code> — catching FID/AID, life-cycle status, security-attribute and proprietary-parameter changes).</li>
<li><strong>Access rules</strong> &mdash; verifies a file&rsquo;s access conditions as <em>decoded rules</em>, independent of which <code class="font-mono text-sm">EF.ARR</code> record the FCI&rsquo;s security-attribute reference (<code class="font-mono text-sm">'8B'</code>) points at: the referenced record&rsquo;s rules are resolved from the card&rsquo;s (or the snapshot&rsquo;s) <code class="font-mono text-sm">EF.ARR</code> and compared as a set with the captured lines. Captured automatically when a profile is created from a card or a snapshot for every file whose FCI carries a reference (EF.ARR&rsquo;s own contents are skipped by default &mdash; the rules are the meaningful check); the editor shows the captured lines read-only with a None/Check toggle. A file whose FCI lost the reference fails; a reference that cannot be resolved (no cached <code class="font-mono text-sm">EF.ARR</code>, record out of range) is reported as unverifiable. With <strong>Exact FCI</strong>, the optional <em>Ignore the EF.ARR reference in the exact FCI</em> checkbox compares the FCI without the <code class="font-mono text-sm">'8B'</code> value: a different record number no longer fails while every other DO is still compared byte-for-byte (the reference&rsquo;s meaning is verified by this check).</li>
<li><strong>Access rules</strong> &mdash; verifies a file&rsquo;s access conditions as <em>decoded rules</em>, independent of which <code class="font-mono text-sm">EF.ARR</code> record the FCI&rsquo;s security-attribute reference (<code class="font-mono text-sm">'8B'</code>) points at: the referenced record&rsquo;s rules are resolved from the card&rsquo;s (or the snapshot&rsquo;s) <code class="font-mono text-sm">EF.ARR</code> and compared as a set with the captured lines. Captured automatically when a profile is created from a card or a snapshot for every file whose FCI carries a reference (EF.ARR&rsquo;s own contents are skipped by default &mdash; the rules are the meaningful check); the editor shows the captured rules as structured rows &mdash; the operation as bit checkboxes (or the RESIZE FILE / INCREASE command form) and each security condition as a select (always, never, or a key reference with its usage), with a preview line of the canonical decoded string the check compares; <em>+ rule</em> builds a new one (READ always), and the template forms outside this subset stay raw and removable. The None/Check toggle disables the check without losing the rules. A file whose FCI lost the reference fails; a reference that cannot be resolved (no cached <code class="font-mono text-sm">EF.ARR</code>, record out of range) is reported as unverifiable. With <strong>Exact FCI</strong>, the optional <em>Ignore the EF.ARR reference in the exact FCI</em> checkbox compares the FCI without the <code class="font-mono text-sm">'8B'</code> value: a different record number no longer fails while every other DO is still compared byte-for-byte (the reference&rsquo;s meaning is verified by this check).</li>
<li><strong>File attributes</strong> — file type, size, record length and record count, taken from the FCP template (any may be left unset).</li>
<li><strong>Check contents</strong> (optional) — <strong>Exact</strong> hex equality, or <strong>Mask</strong> where <code class="font-mono text-sm">?</code> is a per-nibble wildcard (a mask with no <code class="font-mono text-sm">?</code> is a prefix match, e.g. <code class="font-mono text-sm">0891</code> for the IMSI MCC/MNC). Record files store a per-record list.</li>
</ul>
+312 -18
View File
@@ -19807,33 +19807,166 @@ function profilerRenderRule(i, r) {
return html;
}
// Access-rules check editor (v3.25.0): the captured rule lines are read-only
// (capture fills them from the card/snapshot); the radio toggles the check.
// Access-rules check editor (v3.25.0): captured rules open as structured rows
// (operation bits / command + security conditions) and hand-made rules can be
// built the same way; every change re-encodes the canonical decoded line the
// check compares. Forms outside the supported subset stay raw (read-only,
// removable). Capture fills the lines when a profile is created from a card
// or a snapshot; the radio toggles the check.
function profilerRenderAccessRulesEditor(i, r) {
const ar = r.accessRules || null;
const lines = (ar && ar.expected) || [];
const on = !!(ar && ar.enabled !== false && lines.length);
const on = !!(ar && ar.enabled !== false);
let html = '<div class="flex items-center justify-between mb-1 mt-1">';
html += '<span class="text-xs text-gray-600 dark:text-slate-400">' + esc(t('Check access rules')) + '</span>';
html += '<div class="flex items-center gap-3">';
html += '<label class="text-xs flex items-center gap-1"><input type="radio" name="profiler-ar-' + i + '" value="none"' + (!on ? ' checked' : '') + ' onchange="profilerSetAccessRules(' + i + ',false)"> ' + esc(t('None')) + '</label>';
html += '<label class="text-xs flex items-center gap-1' + (lines.length ? '' : ' opacity-50') + '"><input type="radio" name="profiler-ar-' + i + '" value="check"' + (on ? ' checked' : '') + (lines.length ? '' : ' disabled') + ' onchange="profilerSetAccessRules(' + i + ',true)"> ' + esc(t('Check')) + '</label>';
html += '<label class="text-xs flex items-center gap-1"><input type="radio" name="profiler-ar-' + i + '" value="check"' + (on ? ' checked' : '') + ' onchange="profilerSetAccessRules(' + i + ',true)"> ' + esc(t('Check')) + '</label>';
html += '</div></div>';
if (r.fciMode === 'exact') {
html += '<label class="text-xs flex items-center gap-1 mb-1' + (on ? '' : ' opacity-50') + '"><input type="checkbox"' + (ar && ar.ignoreFciRef ? ' checked' : '') + (on ? '' : ' disabled') + ' onchange="profilerSetAccessRulesIgnore(' + i + ',this.checked)"> ' + esc(t('Ignore the EF.ARR reference in the exact FCI')) + '</label>';
}
if (lines.length) {
html += '<div class="mb-2 pl-2 space-y-0.5">';
for (const x of lines) {
html += '<div class="text-xs font-mono text-gray-600 dark:text-slate-400">' + esc('\u21b3 ' + x) + '</div>';
for (let ri = 0; ri < lines.length; ri++) {
html += profilerArrRuleHtml(i, ri, lines[ri]);
}
html += '</div>';
} else {
html += '<div class="mb-2 text-xs text-gray-400 dark:text-slate-500">' + esc(t('Captured when the profile is created from a card or a snapshot')) + '</div>';
if (!lines.length) {
html += '<div class="mb-1 text-xs text-gray-400 dark:text-slate-500">' + esc(t('Captured when the profile is created from a card or a snapshot')) + '</div>';
}
html += '<button onclick="profilerArrAddRule(' + i + ')" class="mb-2 text-xs px-2 py-0.5 rounded bg-gray-200 dark:bg-slate-700 hover:bg-gray-300 dark:hover:bg-slate-600 text-gray-700 dark:text-slate-300">+ ' + esc(t('rule')) + '</button>';
return html;
}
// One expected rule row: the operation form + bit checkboxes (or the RESIZE /
// INCREASE command), the condition rows and the canonical preview line.
function profilerArrRuleHtml(i, ri, line) {
const m = fcpArrRuleParse(line);
if (m.raw !== undefined) {
return '<div class="flex items-start gap-2 mb-1 pl-2">' +
'<span class="flex-1 text-xs font-mono text-gray-500 dark:text-slate-400 break-all">' + esc(t('raw') + ': ' + m.raw) + '</span>' +
'<button onclick="profilerArrRemoveRule(' + i + ',' + ri + ')" class="text-red-400 hover:text-red-600 text-xs px-1">\u2715</button></div>';
}
let html = '<div class="mb-1 pl-2 border-l-2 border-gray-200 dark:border-slate-700">';
html += '<div class="flex flex-wrap items-center gap-x-3 gap-y-1">';
html += '<select onchange="profilerArrSetKind(' + i + ',' + ri + ',this.value)" class="text-xs border border-gray-300 dark:border-slate-600 rounded px-1 py-0.5 dark:bg-slate-800">' +
'<option value="bits"' + (m.kind === 'bits' ? ' selected' : '') + '>' + esc(t('bit mask')) + '</option>' +
'<option value="resize"' + (m.kind === 'resize' ? ' selected' : '') + '>RESIZE FILE</option>' +
'<option value="increase"' + (m.kind === 'increase' ? ' selected' : '') + '>INCREASE</option></select>';
if (m.kind === 'bits') {
for (const bit of FCP_ARR_AM_BITS.slice().reverse()) {
html += '<label class="text-xs flex items-center gap-1"><input type="checkbox"' + (m.am & bit[0] ? ' checked' : '') +
' onchange="profilerArrToggleBit(' + i + ',' + ri + ',' + bit[0] + ',this.checked)"> ' + esc(bit[1]) + '</label>';
}
}
html += '<button onclick="profilerArrRemoveRule(' + i + ',' + ri + ')" class="ml-auto text-red-400 hover:text-red-600 text-xs px-1">\u2715</button>';
html += '</div>';
for (let si = 0; si < m.scs.length; si++) {
html += '<div class="flex flex-wrap items-center gap-x-2 gap-y-1 mt-0.5">';
html += profilerArrScSelect(i, ri, si, m.scs[si]);
if (m.scs[si].kind === 'key') html += profilerArrUqSelect(i, ri, si, m.scs[si].uq);
html += '<button onclick="profilerArrRemoveSc(' + i + ',' + ri + ',' + si + ')" class="text-red-400 hover:text-red-600 text-xs px-1">\u2715</button>';
html += '</div>';
}
html += '<button onclick="profilerArrAddSc(' + i + ',' + ri + ')" class="mt-0.5 text-xs px-1.5 py-0.5 rounded bg-gray-200 dark:bg-slate-700 hover:bg-gray-300 dark:hover:bg-slate-600 text-gray-700 dark:text-slate-300">+ ' + esc(t('condition')) + '</button>';
html += '<div class="mt-0.5 text-xs font-mono text-gray-400 dark:text-slate-500 break-all">' + esc(fcpArrRuleEncode(m)) + '</div>';
html += '</div>';
return html;
}
// The condition select: always / never / the named key references.
function profilerArrScSelect(i, ri, si, sc) {
let opts = '<option value="always"' + (sc.kind === 'alw' ? ' selected' : '') + '>always</option>';
opts += '<option value="never"' + (sc.kind === 'nev' ? ' selected' : '') + '>never</option>';
for (const k of fcpArrKeyRefOptions()) {
opts += '<option value="key:' + k.ref + '"' + (sc.kind === 'key' && sc.ref === k.ref ? ' selected' : '') + '>' + esc(k.name) + '</option>';
}
return '<select onchange="profilerArrSetSc(' + i + ',' + ri + ',' + si + ',this.value)" class="text-xs border border-gray-300 dark:border-slate-600 rounded px-1 py-0.5 dark:bg-slate-800">' + opts + '</select>';
}
// The usage-qualifier select (keys only): verify / verification not used, plus
// the current odd value when a captured rule carried one.
function profilerArrUqSelect(i, ri, si, uq) {
const verify = uq !== 0x00 && (uq & 0x08) === 0x08;
const custom = uq !== 0x00 && (uq & 0x08) === 0;
let opts = '<option value="verify"' + (verify ? ' selected' : '') + '>verify</option>';
opts += '<option value="not-used"' + (uq === 0x00 ? ' selected' : '') + '>verification not used</option>';
if (custom) {
opts += '<option value="usage:' + uq + '" selected>usage 0x' + uq.toString(16).toUpperCase().padStart(2, '0') + '</option>';
}
return '<select onchange="profilerArrSetUq(' + i + ',' + ri + ',' + si + ',this.value)" class="text-xs border border-gray-300 dark:border-slate-600 rounded px-1 py-0.5 dark:bg-slate-800">' + opts + '</select>';
}
// The stored line of one expected rule (undefined when the row is gone).
function profilerArrLine(i, ri) {
if (!profilerDraft) return undefined;
const r = profilerDraft.rules[i];
return (((r && r.accessRules) || {}).expected || [])[ri];
}
// Parse the stored line, mutate the model, re-encode into the draft and
// re-render (the check compares the stored strings).
function profilerArrUpdate(i, ri, model) {
if (!profilerDraft) return;
const r = profilerDraft.rules[i];
if (!r.accessRules) r.accessRules = { expected: [] };
r.accessRules.expected[ri] = fcpArrRuleEncode(model);
profilerRenderEditor();
}
function profilerArrToggleBit(i, ri, bit, on) {
const line = profilerArrLine(i, ri);
if (line === undefined) return;
profilerArrUpdate(i, ri, fcpArrRuleToggleBit(fcpArrRuleParse(line), bit, on));
}
function profilerArrSetKind(i, ri, kind) {
const line = profilerArrLine(i, ri);
if (line === undefined) return;
profilerArrUpdate(i, ri, fcpArrRuleSetKind(fcpArrRuleParse(line), kind));
}
function profilerArrSetSc(i, ri, si, value) {
const line = profilerArrLine(i, ri);
if (line === undefined) return;
profilerArrUpdate(i, ri, fcpArrRuleSetSc(fcpArrRuleParse(line), si, value));
}
function profilerArrSetUq(i, ri, si, value) {
const line = profilerArrLine(i, ri);
if (line === undefined) return;
const uq = value === 'verify' ? 0x08
: (value === 'not-used' ? 0x00 : parseInt(String(value).replace('usage:', ''), 10));
profilerArrUpdate(i, ri, fcpArrRuleSetUq(fcpArrRuleParse(line), si, uq));
}
function profilerArrAddSc(i, ri) {
const line = profilerArrLine(i, ri);
if (line === undefined) return;
profilerArrUpdate(i, ri, fcpArrRuleAddSc(fcpArrRuleParse(line)));
}
function profilerArrRemoveSc(i, ri, si) {
const line = profilerArrLine(i, ri);
if (line === undefined) return;
profilerArrUpdate(i, ri, fcpArrRuleRemoveSc(fcpArrRuleParse(line), si));
}
// A new rule starts from READ always (the common baseline).
function profilerArrAddRule(i) {
if (!profilerDraft) return;
const r = profilerDraft.rules[i];
if (!r.accessRules) r.accessRules = { expected: [] };
r.accessRules.expected.push(fcpArrRuleEncode({ kind: 'bits', am: 0x01, scs: [{ kind: 'alw' }] }));
profilerRenderEditor();
}
function profilerArrRemoveRule(i, ri) {
if (!profilerDraft) return;
const r = profilerDraft.rules[i];
if (!r.accessRules) return;
r.accessRules.expected.splice(ri, 1);
profilerRenderEditor();
}
function profilerSetAccessRules(i, on) {
if (!profilerDraft) return;
const r = profilerDraft.rules[i];
@@ -20600,6 +20733,160 @@ function fcpArrRules(ref, arrs, path) {
return { found: found, rules: out };
}
// ---- structured access-rule editing (v3.25.0 review) -----------------------
// The editor edits a decoded rule's structure (AM byte / command + SC
// conditions) and stores the canonical decoded strings the check compares.
// fcpArrRuleParse returns {raw} for the forms outside the supported subset
// (OR/AND/NOT templates, SM CRTs, command descriptions) - shown read-only.
// Split on a separator at parenthesis depth 0 (the templates nest).
function fcpArrSplitTop(s, sep) {
const out = [];
let depth = 0, start = 0;
for (let i = 0; i < s.length; i++) {
if (s[i] === '(') depth++;
else if (s[i] === ')') depth = Math.max(0, depth - 1);
else if (depth === 0 && s.startsWith(sep, i)) {
out.push(s.slice(start, i));
i += sep.length - 1;
start = i + 1;
}
}
out.push(s.slice(start));
return out;
}
// The key reference whose pinKeyRefName matches a decoded name (null for the
// 'key ref 0xNN' default and unknown names).
function fcpArrKeyRefFromName(name) {
for (let ref = 0; ref <= 0xFF; ref++) {
const n = pinKeyRefName(ref);
if (n === name && !/^key ref /.test(n)) return ref;
}
return null;
}
// {ref, name} options for the condition select (the named key references).
function fcpArrKeyRefOptions() {
const out = [];
for (let ref = 0; ref <= 0xFF; ref++) {
const n = pinKeyRefName(ref);
if (!/^key ref /.test(n)) out.push({ ref: ref, name: n });
}
return out;
}
// One SC (security condition) part of a decoded rule -> model, or null.
function fcpArrScParse(text) {
if (text === 'always') return { kind: 'alw' };
if (text === 'never') return { kind: 'nev' };
const m = /^(.*) \((verify|verification not used|usage 0x([0-9A-F]{2}))\)$/.exec(text);
if (m) {
const ref = fcpArrKeyRefFromName(m[1]);
if (ref !== null) {
const uq = m[2] === 'verify' ? 0x08
: (m[2] === 'verification not used' ? 0x00 : parseInt(m[3], 16));
return { kind: 'key', ref: ref, uq: uq };
}
}
return null;
}
// Model -> the decoded string efArrSc would print (the check compares these).
function fcpArrScEncode(sc) {
if (sc.kind === 'alw') return 'always';
if (sc.kind === 'nev') return 'never';
if (sc.kind === 'key') {
let s = pinKeyRefName(sc.ref);
const uq = sc.uq;
if (uq === null || uq === undefined || (uq & 0x08)) s += ' (verify)';
else if (uq === 0x00) s += ' (verification not used)';
else s += ' (usage 0x' + uq.toString(16).toUpperCase().padStart(2, '0') + ')';
return s;
}
return sc.text || '';
}
// A decoded rule line -> editable model, or {raw} when outside the subset.
function fcpArrRuleParse(line) {
const s = String(line || '');
const idx = s.indexOf(': ');
const amPart = idx < 0 ? s : s.slice(0, idx);
const scPart = idx < 0 ? '' : s.slice(idx + 2);
let kind = null, am = 0;
if (amPart === 'RESIZE FILE (AM_DO 0x84, INS 0xD4)') kind = 'resize';
else if (amPart === 'INCREASE (AM_DO 0x84, INS 0x32)') kind = 'increase';
else {
const m = /^AM 0x([0-9A-F]{2}) \(/.exec(amPart);
if (m) { kind = 'bits'; am = parseInt(m[1], 16); }
}
if (!kind) return { raw: s };
const scs = [];
if (scPart) {
for (const part of fcpArrSplitTop(scPart, ' AND ')) {
const sc = fcpArrScParse(part);
if (!sc) return { raw: s };
scs.push(sc);
}
}
return { kind: kind, am: am, scs: scs };
}
// Model -> the canonical decoded line.
function fcpArrRuleEncode(rule) {
if (rule.raw !== undefined) return rule.raw;
let am;
if (rule.kind === 'resize') am = 'RESIZE FILE (AM_DO 0x84, INS 0xD4)';
else if (rule.kind === 'increase') am = 'INCREASE (AM_DO 0x84, INS 0x32)';
else am = 'AM 0x' + (rule.am & 0xFF).toString(16).toUpperCase().padStart(2, '0') +
' (' + efArrAmLabel(rule.am) + ')';
const scs = (rule.scs || []).map(fcpArrScEncode);
return am + (scs.length ? ': ' + scs.join(' AND ') : '');
}
// The editor mutates the parsed model and re-encodes; a raw model is left as
// it is (its row is read-only).
function fcpArrRuleToggleBit(rule, bit, on) {
if (rule.raw !== undefined) return rule;
const am = on ? (rule.am | bit) : (rule.am & ~bit);
return { kind: rule.kind, am: am & 0xFF, scs: rule.scs.slice() };
}
function fcpArrRuleSetKind(rule, kind) {
if (rule.raw !== undefined) return rule;
return { kind: kind, am: kind === 'bits' ? (rule.am || 0x01) : rule.am, scs: rule.scs.slice() };
}
function fcpArrRuleSetSc(rule, si, value) {
if (rule.raw !== undefined) return rule;
const scs = rule.scs.slice();
if (value === 'always') scs[si] = { kind: 'alw' };
else if (value === 'never') scs[si] = { kind: 'nev' };
else if (String(value).startsWith('key:')) {
const prev = scs[si];
scs[si] = { kind: 'key', ref: parseInt(String(value).slice(4), 10),
uq: (prev && prev.kind === 'key') ? prev.uq : 0x08 };
}
return { kind: rule.kind, am: rule.am, scs: scs };
}
function fcpArrRuleSetUq(rule, si, uq) {
if (rule.raw !== undefined) return rule;
const scs = rule.scs.slice();
if (scs[si] && scs[si].kind === 'key') scs[si] = { kind: 'key', ref: scs[si].ref, uq: uq };
return { kind: rule.kind, am: rule.am, scs: scs };
}
function fcpArrRuleAddSc(rule) {
if (rule.raw !== undefined) return rule;
return { kind: rule.kind, am: rule.am, scs: rule.scs.concat([{ kind: 'alw' }]) };
}
function fcpArrRuleRemoveSc(rule, si) {
if (rule.raw !== undefined) return rule;
return { kind: rule.kind, am: rule.am, scs: rule.scs.filter((_, i) => i !== si) };
}
function fcpDo(key, value) {
if (/^[0-9A-F]+$/.test(value) === false) value = value || '';
switch (key) {
@@ -21556,10 +21843,11 @@ function pinKeyRefName(v) {
return 'key ref 0x' + v.toString(16).toUpperCase().padStart(2, '0');
}
// ISO 7816-4 Tables 16/17: AM byte bit meanings. A record does not store
// whether it protects an EF or a DF, so both mappings are shown.
function efArrAmLabel(am) {
const bits = [
// ISO 7816-4 Tables 16/17: AM byte bit meanings, highest bit first (the
// decoder's label order). A record does not store whether it protects an EF
// or a DF, so both mappings are shown; the structured access-rule editor
// renders the same table low-to-high as operation checkboxes (v3.25.0 review).
const FCP_ARR_AM_BITS = [
[0x40, 'DELETE (self)'],
[0x20, 'TERMINATE EF / TERMINATE DF'],
[0x10, 'ACTIVATE'],
@@ -21567,12 +21855,14 @@ function efArrAmLabel(am) {
[0x04, 'WRITE/APPEND (EF) / CREATE FILE DF (DF)'],
[0x02, 'UPDATE/ERASE (EF) / CREATE FILE EF (DF)'],
[0x01, 'READ/SEARCH (EF) / DELETE FILE child (DF)'],
];
];
function efArrAmLabel(am) {
if (am & 0x80) {
const low = bits.filter(b => (b[0] & 0x07) && (am & b[0])).map(b => b[1]);
const low = FCP_ARR_AM_BITS.filter(b => (b[0] & 0x07) && (am & b[0])).map(b => b[1]);
return 'bits 7-4 proprietary' + (low.length ? ' + ' + low.join(' + ') : '');
}
const ops = bits.filter(b => am & b[0]).map(b => b[1]);
const ops = FCP_ARR_AM_BITS.filter(b => am & b[0]).map(b => b[1]);
return ops.length ? ops.join(' + ') : 'no operations';
}
@@ -23121,6 +23411,10 @@ const LANG_RU = {
'the EF.ARR reference could not be resolved (no cached EF.ARR or the record is out of range)': 'ссылку на EF.ARR не удалось разрешить (нет кэша EF.ARR или запись вне диапазона)',
'Captured when the profile is created from a card or a snapshot': 'Заполняется при создании профиля с карты или из снимка',
'EF.ARR reference ignored (verified by the access-rules check)': 'Ссылка на EF.ARR игнорируется (проверяется по правилам доступа)',
'rule': 'правило',
'condition': 'условие',
'raw': 'как есть',
'bit mask': 'битовая маска',
'Mask': 'Маска',
'Checking': 'Проверка',
'passed': 'пройдено',
+1 -1
View File
@@ -39,7 +39,7 @@ const FNS = [
let code = '';
for (const f of FNS) code += extractFunc(html, f) + '\n';
for (const c of ['GSM7_ALPHABET', 'GSM7_EXT_MAP']) {
for (const c of ['GSM7_ALPHABET', 'GSM7_EXT_MAP', 'FCP_ARR_AM_BITS']) {
code += html.match(new RegExp('const ' + c + ' = [\\s\\S]*?\\n\\];'))[0].replace('const ', 'var ') + '\n';
}
for (const c of ['EF_UST_SERVICES', 'EF_SST_SERVICES', 'EF_IST_SERVICES', 'EF_PHASE_NAMES']) {
+77 -1
View File
@@ -22,7 +22,7 @@ function extractFunc(src, name, asyncFn) {
}
const FNS = ['profilerNormHex', 'profilerNormHexStrict', 'profilerMatch', 'profilerMatchMin', 'profilerMaskPrefix4', 'profilerFileFields', 'profilerContentKindForFileType', 'profilerEmptyRecordContent', 'profilerValidateProfile', 'profilerCustomNameForPath', 'pysimCustomNormPath', 'profilerUpdateRulePath', 'profilerResultAspects', 'profilerAspectSummary', 'profilerNumRanges', 'profilerMatchedRecordsText', 'profilerCloneName', 'profilerClone', 'profilerNewId', 'esc', 'escHtml', 'profilerRawDataCheck', 'profilerRenderReport', 'parseBerLen', 'parseTlvList', 'fcpInt', 'fcpParseTlvs', 'fcpFileDescriptor', 'fcpLifeCycle', 'fcpSfi', 'fcpDo', 'fcpDecode',
'fcpArrRef', 'fcpSeidName', 'fcpArrRules', 'fcpArrRefFromFci', 'fcpWithoutArrRef', 'profilerAccessRulesDiffHtml', 'profilerSnapshotArr', 'efBytes', 'efHex', 'efTlv', 'efDecArr', 'efArrAmLabel', 'efArrSc', 'efArrAmDo', 'efFidFromPath',
'fcpArrRef', 'fcpSeidName', 'fcpArrRules', 'fcpArrRefFromFci', 'fcpWithoutArrRef', 'fcpArrSplitTop', 'fcpArrKeyRefFromName', 'fcpArrKeyRefOptions', 'fcpArrScParse', 'fcpArrScEncode', 'fcpArrRuleParse', 'fcpArrRuleEncode', 'fcpArrRuleToggleBit', 'fcpArrRuleSetKind', 'fcpArrRuleSetSc', 'fcpArrRuleSetUq', 'fcpArrRuleAddSc', 'fcpArrRuleRemoveSc', 'profilerRenderAccessRulesEditor', 'profilerArrRuleHtml', 'profilerArrScSelect', 'profilerArrUqSelect', 'profilerAccessRulesDiffHtml', 'profilerSnapshotArr', 'efBytes', 'efHex', 'efTlv', 'efDecArr', 'efArrAmLabel', 'efArrSc', 'efArrAmDo', 'efFidFromPath',
'pinKeyRefName', 'fcpPsTemplate', 'fcpDiffHtml', 'profilerFciPreviewItems', 'profilerUpdateFciPreview', 'profilerUpdateRule', 'profilerFciInput', 'profilerScanToggleAll', 'profilerScanIgnoreAllState', 'swapNibbles', 'decIccid', 'profilerSnapshotIccid', 'profilerValidateSnapshot', 'profilerListSwitch', 'profilerScanRefreshOptions', 'profilerLiveSource', 'profilerSnapshotSource', 'profilerVisibleResults', 'profilerRulesFromSnapshot', 'profilerExtraFileResults', 'profilerScanNameKeydown', 'profilerTimingStats', 'profilerTimingAccumulator', 'profilerFormatMs', 'profilerRenderSnapshotSummary', 'profilerSnapshotCountLabel', 'pysimFsInfoHtml', 'profilerLabelText', 'profilerResultsHeaderText', 'profilerRenderResultsView', 'profilerBuildFileRuleFromSnapshot', 'profilerSnapshotPickListHtml', 'profilerScanSetTarget'];
let code = '';
for (const f of FNS) code += extractFunc(html, f) + '\n';
@@ -40,6 +40,7 @@ code += "var _pysimArrCache = null;\nvar _pysimCardSession = null;\n";
// the view-history hooks (the router lives in the page; this pure test stubs it)
code += 'globalThis.navRecord = function() {};\nglobalThis.navReplace = function() {};\n';
code += html.match(/const PROFILER_MASK_PREFIX4_FIDS = \{[\s\S]*?\n\};/)[0] + '\n';
code += html.match(/const FCP_ARR_AM_BITS = \[[\s\S]*?\n\];/)[0] + '\n';
eval(code);
test('profilerNormHex uppercases and strips non-hex', () => {
@@ -1058,6 +1059,81 @@ test('the implicit form prefers the file DF\'s own ARR copy', () => {
delete global.t;
});
test('fcpArrRuleParse/Encode round-trips the decoded rule forms', () => {
const lines = [
'AM 0x01 (READ/SEARCH (EF) / DELETE FILE child (DF)): always',
'AM 0x1A (ACTIVATE + DEACTIVATE + UPDATE/ERASE (EF) / CREATE FILE EF (DF)): ADM1 (verify)',
'AM 0x02 (UPDATE/ERASE (EF) / CREATE FILE EF (DF)): PIN app1 (verify) AND ADM1 (verify)',
'RESIZE FILE (AM_DO 0x84, INS 0xD4): never',
'INCREASE (AM_DO 0x84, INS 0x32): ADM1 (verify)',
'AM 0x01 (READ/SEARCH (EF) / DELETE FILE child (DF))',
];
for (const line of lines) {
const m = fcpArrRuleParse(line);
assert.strictEqual(m.raw, undefined, line);
assert.strictEqual(fcpArrRuleEncode(m), line, line);
}
// template forms and unknown conditions stay raw and unchanged
for (const raw of [
'AM 0x02 (UPDATE/ERASE (EF) / CREATE FILE EF (DF)): (PIN app1 (verify) OR PIN app2 (verify))',
'AM 0x01 (READ/SEARCH (EF) / DELETE FILE child (DF)): SM CRT 0xB4',
'command description 0x84 (D4)',
]) {
const rm = fcpArrRuleParse(raw);
assert.strictEqual(rm.raw, raw, raw);
assert.strictEqual(fcpArrRuleEncode(rm), raw, raw);
}
});
test('fcpArrRule mutators re-encode the canonical line', () => {
let m = fcpArrRuleParse('AM 0x01 (READ/SEARCH (EF) / DELETE FILE child (DF)): always');
m = fcpArrRuleToggleBit(m, 0x02, true);
assert.strictEqual(fcpArrRuleEncode(m),
'AM 0x03 (UPDATE/ERASE (EF) / CREATE FILE EF (DF) + READ/SEARCH (EF) / DELETE FILE child (DF)): always');
m = fcpArrRuleSetSc(m, 0, 'key:10'); // 10 = 0x0A = ADM1
assert.ok(fcpArrRuleEncode(m).endsWith(': ADM1 (verify)'), fcpArrRuleEncode(m));
m = fcpArrRuleSetUq(m, 0, 0x00);
assert.ok(fcpArrRuleEncode(m).endsWith(': ADM1 (verification not used)'), fcpArrRuleEncode(m));
m = fcpArrRuleAddSc(m);
assert.ok(fcpArrRuleEncode(m).endsWith(': ADM1 (verification not used) AND always'), fcpArrRuleEncode(m));
m = fcpArrRuleRemoveSc(m, 1);
assert.ok(fcpArrRuleEncode(m).endsWith(': ADM1 (verification not used)'), fcpArrRuleEncode(m));
m = fcpArrRuleSetKind(m, 'resize');
assert.strictEqual(fcpArrRuleEncode(m), 'RESIZE FILE (AM_DO 0x84, INS 0xD4): ADM1 (verification not used)');
m = fcpArrRuleSetSc(m, 0, 'never');
assert.strictEqual(fcpArrRuleEncode(m), 'RESIZE FILE (AM_DO 0x84, INS 0xD4): never');
// a raw model is never modified
const raw = fcpArrRuleParse('command description 0x84 (D4)');
assert.strictEqual(fcpArrRuleEncode(fcpArrRuleToggleBit(raw, 0x01, true)), 'command description 0x84 (D4)');
});
test('the access-rules editor renders structured rows', () => {
global.t = s => s;
const r = { fciMode: 'exact', accessRules: { expected: [
'AM 0x01 (READ/SEARCH (EF) / DELETE FILE child (DF)): always',
'RESIZE FILE (AM_DO 0x84, INS 0xD4): never',
'AM 0x02 (UPDATE/ERASE (EF) / CREATE FILE EF (DF)): (PIN app1 (verify) OR PIN app2 (verify))',
] } };
const html = profilerRenderAccessRulesEditor(3, r);
// the READ bit is checked; the condition select shows always
assert.ok(html.includes('type="checkbox" checked onchange="profilerArrToggleBit(3,0,1,this.checked)"'), html);
assert.ok(html.includes('<option value="always" selected>always</option>'), html);
assert.ok(html.includes('<option value="key:10">ADM1</option>'), html);
// the RESIZE row uses the command form, not bits
assert.ok(html.includes('<option value="resize" selected>RESIZE FILE</option>'), html);
// the template rule is raw and not editable
assert.ok(html.includes('raw: AM 0x02'), html);
assert.ok(!html.includes('profilerArrToggleBit(3,2'), html);
// the preview lines and the + rule button are present
assert.ok(html.includes('RESIZE FILE (AM_DO 0x84, INS 0xD4): never'), html);
assert.ok(html.includes('profilerArrAddRule(3)'), html);
// an empty rule shows the capture hint with no disabled controls
const empty = profilerRenderAccessRulesEditor(4, { fciMode: 'type_size', accessRules: null });
assert.ok(empty.includes('profilerArrAddRule(4)'), empty);
assert.ok(!empty.includes(' disabled'), empty);
delete global.t;
});
test('profilerResultAspects and the summary include Access rules', () => {
const res = { checks: [
{ label: 'exists', ok: true },