v3.25.0: the access-rules check in profiles

A profile file rule may carry accessRules {expected, enabled?, ignoreFciRef?}:
the check resolves the file FCI's '8B' security-attribute reference against
the card's (or the snapshot's) EF.ARR copies and compares the decoded rules as
a set - record-number independent, so cards with a different EF.ARR layout
still verify.  Missing/extra lines are listed in the report; a file whose FCI
lost the reference fails; a reference that cannot be resolved (no cached
EF.ARR / record out of range) is an unverifiable error.

Capture fills the lines when a profile is created from a card
(pysimArrEnsure) or a snapshot (profilerSnapshotArr), before the contents
ignore list - the rules are the meaningful per-file check, and EF.ARR's own
contents stay ignored by default as before.  The editor shows the captured
lines read-only with a None/Check toggle; with exact FCI an optional "Ignore
the EF.ARR reference in the exact FCI" checkbox compares the FCI without the
'8B' value (every other DO stays byte-exact; fcpWithoutArrRef), so a different
record number no longer fails while the rules check owns its meaning.

The snapshot comparison is unchanged (byte-exact EF.ARR + FCI catch record
renumbering).  New 'Access rules' aspect in the report/summary with a
matching/missing/extra line diff.

Tests: capture, the four check outcomes, the ignore-FCI path, the
aspect/report, validation.  877 frontend + 857 Python; sw cache simple-v424.
This commit is contained in:
2026-10-10 23:09:47 +03:00
parent 4d87094a98
commit 3bf005619e
7 changed files with 331 additions and 23 deletions
+2 -1
View File
@@ -466,10 +466,11 @@
<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> — тип файла, размер, длина записи и число записей из 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>
<p class="text-sm mb-3"><strong>Проверить карту</strong> выполняет каждое правило на подключённой карте и показывает строку прогресса и отчёт прохождения. Рядом с путём файла указывается, что именно проверялось (например, <em>тип файла и размер, содержимое</em> или <em>полный FCI</em>); если часть проверок прошла, а часть нет — каждый аспект помечается (<em>тип файла ✓, размер ✗, содержимое ✓</em>), а расхождения расписываются ниже. Несовпавшие сырые данные (FCI, содержимое, данные записей) показываются как поля только для чтения с моноширинным шрифтом — ожидаемое над фактическим, в одной и той же колонке — для удобного сравнения; расхождения содержимого дополнительно показывают для файлов с декодером декодированное сравнение по полям (только отличающиеся поля, с заголовками колонок «ожидаемое/фактическое» над значениями; если декодированные значения совпадают, вместо таблицы выводится пометка об этом, а сторона, чья запись целиком из FF, показывает в своих ячейках <em>Пусто (только FF)</em>), а для расхождений FCI дополнительно показывается декодированное сравнение по параметрам (размер файла, дескриптор/структура, жизненный цикл, FID, SFI, проприетарные параметры…). Декодированный просмотр FCI также отображается рядом с полем FCI hex при редактировании правила. Если данные FCI повреждены, показывается всё, что удалось декодировать до места ошибки, вместе с явным сообщением об ошибке декодирования. Для record-файлов при расхождении содержимого добавляется пометка <em>совпадающие записи: 1-5, 7-10</em> со списком записей, которые совпали. В отчёте поля расхождений и колонки сравнения FCI подписаны <em>ожидалось (имя профиля)</em> и <em>фактически (ICCID карты)</em>, а в заголовке отчёта выводится <em>Результаты проверки профиля: &lt;профиль&gt; &rarr; &lt;ICCID карты&gt;</em>. Опция <strong>«Только расхождения»</strong> в заголовке отчёта скрывает все совпавшие файлы и оставляет только несовпадения и ошибки.</p>
<p class="text-sm mb-3"><strong>Проверить карту</strong> выполняет каждое правило на подключённой карте и показывает строку прогресса и отчёт прохождения. Рядом с путём файла указывается, что именно проверялось (например, <em>тип файла и размер, содержимое</em> или <em>полный FCI</em>); если часть проверок прошла, а часть нет — каждый аспект помечается (<em>тип файла ✓, размер ✗, содержимое ✓</em>), а расхождения расписываются ниже. Несовпавшие сырые данные (FCI, содержимое, данные записей) показываются как поля только для чтения с моноширинным шрифтом — ожидаемое над фактическим, в одной и той же колонке — для удобного сравнения; расхождения содержимого дополнительно показывают для файлов с декодером декодированное сравнение по полям (только отличающиеся поля, с заголовками колонок «ожидаемое/фактическое» над значениями; если декодированные значения совпадают, вместо таблицы выводится пометка об этом, а сторона, чья запись целиком из FF, показывает в своих ячейках <em>Пусто (только FF)</em>), а для расхождений FCI дополнительно показывается декодированное сравнение по параметрам (размер файла, дескриптор/структура, жизненный цикл, FID, SFI, проприетарные параметры…). Расхождения правил доступа выводят декодированные строки правил (&#10003; совпадает, &minus; отсутствует, + лишнее). Декодированный просмотр FCI также отображается рядом с полем FCI hex при редактировании правила. Если данные FCI повреждены, показывается всё, что удалось декодировать до места ошибки, вместе с явным сообщением об ошибке декодирования. Для record-файлов при расхождении содержимого добавляется пометка <em>совпадающие записи: 1-5, 7-10</em> со списком записей, которые совпали. В отчёте поля расхождений и колонки сравнения FCI подписаны <em>ожидалось (имя профиля)</em> и <em>фактически (ICCID карты)</em>, а в заголовке отчёта выводится <em>Результаты проверки профиля: &lt;профиль&gt; &rarr; &lt;ICCID карты&gt;</em>. Опция <strong>«Только расхождения»</strong> в заголовке отчёта скрывает все совпавшие файлы и оставляет только несовпадения и ошибки.</p>
<h4 class="font-medium mb-1">Опции сканирования &laquo;Профиль с карты&raquo;</h4>
<p class="text-sm mb-2">Диалог сканирования запрашивает имя профиля и предлагает селектор <strong>&laquo;Проверка FCP/FCI&raquo;</strong> (те же три режима, по умолчанию <strong>Тип файла + размер (FCP)</strong>), применяемый ко всем создаваемым правилам, а также список <strong>&laquo;Игнорировать содержимое файлов&raquo;</strong> (все отмечены по умолчанию, кроме <code class="font-mono text-sm">EF.ARR</code>; флажок в заголовке отмечает или снимает весь список) часто перезаписываемых файлов, содержимое которых пропускается: <code class="font-mono text-sm">EF.LOCI</code>, <code class="font-mono text-sm">EF.PSLOCI</code>, <code class="font-mono text-sm">EF.EPSLOCI</code>, <code class="font-mono text-sm">EF.5GS3GPPLOCI</code>, <code class="font-mono text-sm">EF.Keys</code>, <code class="font-mono text-sm">EF.KeysPS</code>, <code class="font-mono text-sm">EF.SMS</code>, <code class="font-mono text-sm">EF.Kc</code>, <code class="font-mono text-sm">EF.KcGPRS</code>, <code class="font-mono text-sm">EF.LOCIGPRS</code>, <code class="font-mono text-sm">EF.CBMID</code>, <code class="font-mono text-sm">EF.SMSS</code>, <code class="font-mono text-sm">EF.ACC</code>, <code class="font-mono text-sm">EF.EPSNSC</code>, <code class="font-mono text-sm">EF.START-HFN</code>, <code class="font-mono text-sm">EF.ARR</code>. Ещё две отмеченные по умолчанию опции <strong>&laquo;Сравнивать первые 4 байта для&raquo;</strong> <code class="font-mono text-sm">EF.IMSI</code> и <code class="font-mono text-sm">EF.ICCID</code> захватывают содержимое этих файлов как маску только первых 4 байт (снимите для точного сравнения). Строка прогресса показывает <em>N / всего файлов</em> с текущим путём файла во время сканирования; при сканировании опции скрываются, а кнопки блокируются. Правила создаются только для файлов, которые реально существуют на карте (возвращён FCP-шаблон); отсутствующие файлы пропускаются. Пользовательские файлы из подвкладки <strong>&laquo;Пользовательские файлы&raquo;</strong> включаются с той же проверкой существования. Тот же диалог и опции использует <strong>&laquo;Профиль из снимка&raquo;</strong>: вместо карты обходятся захваченные файлы выбранного снимка; для файлов, содержимое которых не было захвачено, правило создаётся без проверки содержимого (при последующей проверке профиля они помечаются как непроверяемые).</p>
+2 -1
View File
@@ -465,10 +465,11 @@
<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>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>
<p class="text-sm mb-3"><strong>Check card</strong> runs every rule against the equipped card and shows a live progress line plus a pass/fail report. Each row states exactly what was verified next to the file path (e.g. <em>filetype and size, contents</em> or <em>exact FCI</em>); when some checks pass and others fail, each aspect is marked (<em>filetype ✓, size ✗, contents ✓</em>) with the mismatches detailed below. Mismatched raw data (FCI, contents, record data) is shown as read-only monospace fields — expected above actual, aligned in the same column — for easy comparison; content mismatches additionally show, for files with a decoder, a decoded per-field comparison (only the differing fields, with expected/actual column headers above the side-by-side values; when the decoded values match, a note says so instead of the table, and a side whose record is all-FF shows <em>Empty (all FF)</em> in its cells), and FCI mismatches additionally show a decoded per-parameter comparison (file size, file descriptor/structure, life cycle, FID, SFI, proprietary parameters…). A decoded FCI preview is also shown beside the FCI hex field while editing a rule. If the FCI data is corrupt, whatever was decoded before the faulty part is shown together with an explicit decode-failure note. For record files with a contents mismatch, a <em>matching records: 1-5, 7-10</em> note lists the records that did match. In the report the mismatch fields and FCI comparison columns are labelled <em>expected (profile name)</em> and <em>actual (card ICCID)</em>, and the results header reads <em>Profile verification results for: &lt;profile&gt; &rarr; &lt;card ICCID&gt;</em>. An <strong>Only mismatches</strong> option in the results header hides all passing files and keeps only failures and errors.</p>
<p class="text-sm mb-3"><strong>Check card</strong> runs every rule against the equipped card and shows a live progress line plus a pass/fail report. Each row states exactly what was verified next to the file path (e.g. <em>filetype and size, contents</em> or <em>exact FCI</em>); when some checks pass and others fail, each aspect is marked (<em>filetype ✓, size ✗, contents ✓</em>) with the mismatches detailed below. Mismatched raw data (FCI, contents, record data) is shown as read-only monospace fields — expected above actual, aligned in the same column — for easy comparison; content mismatches additionally show, for files with a decoder, a decoded per-field comparison (only the differing fields, with expected/actual column headers above the side-by-side values; when the decoded values match, a note says so instead of the table, and a side whose record is all-FF shows <em>Empty (all FF)</em> in its cells), and FCI mismatches additionally show a decoded per-parameter comparison (file size, file descriptor/structure, life cycle, FID, SFI, proprietary parameters…). Access-rule mismatches list the decoded rule lines (&#10003; matching, &minus; missing, + extra). A decoded FCI preview is also shown beside the FCI hex field while editing a rule. If the FCI data is corrupt, whatever was decoded before the faulty part is shown together with an explicit decode-failure note. For record files with a contents mismatch, a <em>matching records: 1-5, 7-10</em> note lists the records that did match. In the report the mismatch fields and FCI comparison columns are labelled <em>expected (profile name)</em> and <em>actual (card ICCID)</em>, and the results header reads <em>Profile verification results for: &lt;profile&gt; &rarr; &lt;card ICCID&gt;</em>. An <strong>Only mismatches</strong> option in the results header hides all passing files and keeps only failures and errors.</p>
<h4 class="font-medium mb-1">&ldquo;Profile from card&rdquo; scan options</h4>
<p class="text-sm mb-2">The scan dialog asks for a profile name and offers a <strong>&ldquo;FCP/FCI check&rdquo;</strong> selector (the same three modes above, default <strong>Filetype + size</strong>) applied to every generated rule, plus an <strong>&ldquo;Ignore contents of files&rdquo;</strong> checklist (all checked by default except <code class="font-mono text-sm">EF.ARR</code>; the header checkbox checks or unchecks the whole list) of frequently-overwritten files whose contents are skipped: <code class="font-mono text-sm">EF.LOCI</code>, <code class="font-mono text-sm">EF.PSLOCI</code>, <code class="font-mono text-sm">EF.EPSLOCI</code>, <code class="font-mono text-sm">EF.5GS3GPPLOCI</code>, <code class="font-mono text-sm">EF.Keys</code>, <code class="font-mono text-sm">EF.KeysPS</code>, <code class="font-mono text-sm">EF.SMS</code>, <code class="font-mono text-sm">EF.Kc</code>, <code class="font-mono text-sm">EF.KcGPRS</code>, <code class="font-mono text-sm">EF.LOCIGPRS</code>, <code class="font-mono text-sm">EF.CBMID</code>, <code class="font-mono text-sm">EF.SMSS</code>, <code class="font-mono text-sm">EF.ACC</code>, <code class="font-mono text-sm">EF.EPSNSC</code>, <code class="font-mono text-sm">EF.START-HFN</code>, <code class="font-mono text-sm">EF.ARR</code>. Two further checked-by-default options <strong>&ldquo;Match first 4 bytes for&rdquo;</strong> <code class="font-mono text-sm">EF.IMSI</code> and <code class="font-mono text-sm">EF.ICCID</code> capture those files&rsquo; contents as a mask of only the first 4 bytes (uncheck for exact matching). A progress line shows <em>N / total files</em> with the current file path while scanning; during the scan the options are hidden and the buttons are locked. Rules are created only for files that actually exist on the card (a FCP template is returned); missing files are skipped. Custom files from the <strong>Custom files</strong> sub-tab are included under the same existence check. The same dialog and options are used by <strong>Profile from snapshot</strong>, which walks the selected snapshot&rsquo;s captured files instead of the card; rules for files whose contents were not captured during the scan get no content check (they are reported as unverifiable when the profile is later checked).</p>
+193 -17
View File
@@ -1905,7 +1905,7 @@
// ===== Version =====
// Single source of truth for the PWA version: shown in the header and used
// by the server version check in pysimConnect().
const SIMPLE_VERSION = '3.24.0';
const SIMPLE_VERSION = '3.25.0';
document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION;
// ===== view history (the browser Back/Forward buttons) =====
@@ -18939,6 +18939,11 @@ function profilerValidateProfile(obj) {
if (r.type !== 'file') return 'Unsupported rule type: ' + r.type;
if (!r.path) return 'Rule missing path';
if (r.fciMode && !['type', 'type_size', 'exact'].includes(r.fciMode)) return 'Invalid FCP/FCI mode: ' + r.fciMode;
if (r.accessRules !== undefined && r.accessRules !== null) {
const ar = r.accessRules;
if (typeof ar !== 'object' || !Array.isArray(ar.expected) ||
ar.expected.some(x => typeof x !== 'string')) return 'Invalid access rules: ' + r.path;
}
}
return null;
}
@@ -19287,7 +19292,7 @@ async function profilerScanCard(ignoreFids, ignoreNames, fciMode, onProgress, ma
let _scanSnapshot = null;
function profilerBuildFileRuleFromSnapshot(f, ignoreFids, ignoreNames, fciMode, maskFids) {
function profilerBuildFileRuleFromSnapshot(f, ignoreFids, ignoreNames, fciMode, maskFids, arrs) {
const isRecord = profilerContentKindForFileType(f.fileType) === 'record';
const rule = {
type: 'file',
@@ -19301,6 +19306,12 @@ function profilerBuildFileRuleFromSnapshot(f, ignoreFids, ignoreNames, fciMode,
fciHex: f.fciHex || null,
content: null,
};
// Access rules from the snapshot's own EF.ARR copies (v3.25.0)
const ref = fcpArrRefFromFci(rule.fciHex);
if (ref) {
const got = fcpArrRules(ref, arrs || [], f.path);
if (got.rules.length) rule.accessRules = { expected: got.rules };
}
const seg = (f.path || '').toUpperCase().split('/').pop();
const fid = /^[0-9A-F]{4}$/.test(seg) ? seg : '';
const name = (f.name || '').toUpperCase();
@@ -19324,11 +19335,13 @@ function profilerBuildFileRuleFromSnapshot(f, ignoreFids, ignoreNames, fciMode,
async function profilerScanSnapshot(snapshot, ignoreFids, ignoreNames, fciMode, maskFids, onProgress) {
const files = (snapshot && snapshot.files) ? snapshot.files : [];
const total = files.length;
// the snapshot's own EF.ARR copies feed every rule's access-rules capture
const arrs = profilerSnapshotArr(snapshot);
if (onProgress) onProgress(0, total, '');
const rules = [];
for (let i = 0; i < files.length; i++) {
if (onProgress) onProgress(i + 1, total, files[i].path);
rules.push(profilerBuildFileRuleFromSnapshot(files[i], ignoreFids, ignoreNames, fciMode, maskFids));
rules.push(profilerBuildFileRuleFromSnapshot(files[i], ignoreFids, ignoreNames, fciMode, maskFids, arrs));
if (i % 20 === 19) await new Promise(r => setTimeout(r, 0));
}
return rules;
@@ -19353,6 +19366,16 @@ async function profilerBuildFileRule(path, c, ignoreFids, ignoreNames, fciMode,
fciHex: sel.fci_hex || null,
content: null,
};
// Access rules (v3.25.0): resolve the FCP's '8B' reference against the
// card's cached EF.ARR copies. Captured before the ignore list - the
// rules are the meaningful per-file check (contents may be ignored).
try {
const ref = fcpArrRefFromFci(rule.fciHex);
if (ref) {
const got = fcpArrRules(ref, await pysimArrEnsure(), path);
if (got.rules.length) rule.accessRules = { expected: got.rules };
}
} catch (e) {}
const fid = (c.fid || sel.fid || '').toUpperCase();
const name = (c.name || sel.name || '').toUpperCase();
if (ignoreFids.has(fid) || (ignoreNames && ignoreNames.has(name))) return rule;
@@ -19779,10 +19802,53 @@ function profilerRenderRule(i, r) {
if (r.fileType !== 'df') {
html += profilerRenderContentEditor(i, r);
}
html += profilerRenderAccessRulesEditor(i, r);
html += '</div>';
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.
function profilerRenderAccessRulesEditor(i, r) {
const ar = r.accessRules || null;
const lines = (ar && ar.expected) || [];
const on = !!(ar && ar.enabled !== false && lines.length);
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 += '</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>';
}
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>';
}
return html;
}
function profilerSetAccessRules(i, on) {
if (!profilerDraft) return;
const r = profilerDraft.rules[i];
if (!r.accessRules) r.accessRules = { expected: [] };
r.accessRules.enabled = on;
profilerRenderEditor();
}
function profilerSetAccessRulesIgnore(i, on) {
if (!profilerDraft) return;
const r = profilerDraft.rules[i];
if (!r.accessRules) r.accessRules = { expected: [] };
r.accessRules.ignoreFciRef = on;
}
function profilerRenderContentEditor(i, r) {
const c = r.content;
const mode = c ? c.mode : 'exact';
@@ -20091,11 +20157,15 @@ function profilerLiveSource() {
return {
select: path => pysimFetch('/api/select', { path: path }),
read: path => pysimFetch('/api/read', { path: path, mode: 'raw' }),
// the card's ARR cache (read once per session at equip) - fetched
// lazily, only when a rule checks access rules
arrs: () => pysimArrEnsure(),
};
}
function profilerSnapshotSource(snapshot) {
const find = path => (snapshot.files || []).find(f => (f.path || '').toUpperCase() === (path || '').toUpperCase());
let arrCache = null;
return {
select: async path => {
const f = find(path);
@@ -20117,6 +20187,8 @@ function profilerSnapshotSource(snapshot) {
if (f.content.kind === 'record') return { success: true, records: f.content.records };
return { success: true, data: f.content.data };
},
// the snapshot's own EF.ARR copies (memoized per source)
arrs: () => arrCache || (arrCache = profilerSnapshotArr(snapshot)),
};
}
@@ -20164,11 +20236,50 @@ async function profilerRunRule(r, source) {
}
}
if (fciMode === 'exact' && r.fciHex) {
const expected = profilerNormHexStrict(r.fciHex);
const actual = profilerNormHexStrict(sel.fci_hex);
let expected = profilerNormHexStrict(r.fciHex);
let actual = profilerNormHexStrict(sel.fci_hex);
let ignoredRef = false;
// the EF.ARR reference's record number may differ across cards while
// the access rules match - the rules check owns its meaning then
if (r.accessRules && r.accessRules.enabled !== false && r.accessRules.ignoreFciRef &&
(r.accessRules.expected || []).length) {
const e2 = fcpWithoutArrRef(expected);
const a2 = fcpWithoutArrRef(actual);
if (e2 !== null && a2 !== null) { expected = e2; actual = a2; ignoredRef = true; }
}
const ok = !!expected && expected === actual;
if (!ok) res.status = 'fail';
res.checks.push({ label: 'fci', expected: r.fciHex, actual: sel.fci_hex || '', ok: ok });
res.checks.push({ label: 'fci', expected: r.fciHex, actual: sel.fci_hex || '', ok: ok, ignoredRef: ignoredRef });
}
// Access rules (v3.25.0): the decoded conditions of the EF.ARR record the
// FCI's '8B' reference points at - record-number independent, so cards
// with a different EF.ARR layout still verify.
if (r.accessRules && r.accessRules.enabled !== false && (r.accessRules.expected || []).length) {
const ref = fcpArrRefFromFci(sel.fci_hex);
if (!ref) {
res.status = 'fail';
res.checks.push({ label: 'accessRules', expected: r.accessRules.expected, actual: null, ok: false,
missing: r.accessRules.expected.slice(), extra: [] });
} else {
let arrs = [];
try {
arrs = !source.arrs ? [] : (typeof source.arrs === 'function' ? await source.arrs() : source.arrs);
} catch (e) { arrs = []; }
const got = fcpArrRules(ref, arrs, r.path);
if (!got.found) {
if (res.status === 'pass') res.status = 'error';
res.checks.push({ label: 'accessRules', expected: r.accessRules.expected, actual: null, ok: false,
unresolved: true, missing: [], extra: [] });
} else {
const exp = r.accessRules.expected;
const missing = exp.filter(x => !got.rules.includes(x));
const extra = got.rules.filter(x => !exp.includes(x));
const ok = !missing.length && !extra.length;
if (!ok) res.status = 'fail';
res.checks.push({ label: 'accessRules', expected: exp, actual: got.rules, ok: ok,
missing: missing, extra: extra });
}
}
}
if (r.content) {
let rd;
@@ -20233,7 +20344,7 @@ function profilerCustomNameForPath(path) {
// that actually ran. 'Exact FCI' subsumes filetype/size/records; 'exists' is
// implicit and omitted.
function profilerResultAspects(res) {
const groups = { filetype: [], size: [], records: [], 'Exact FCI': [], contents: [] };
const groups = { filetype: [], size: [], records: [], 'Exact FCI': [], contents: [], accessRules: [] };
for (const c of (res.checks || [])) {
const l = c.label;
if (l === 'fileType') groups.filetype.push(c);
@@ -20241,6 +20352,7 @@ function profilerResultAspects(res) {
else if (l === 'recordLen' || l === 'numRecords') groups.records.push(c);
else if (l === 'fci') groups['Exact FCI'].push(c);
else if (l === 'content' || l.startsWith('content.')) groups.contents.push(c);
else if (l === 'accessRules') groups.accessRules.push(c);
}
const out = [];
if (groups['Exact FCI'].length) {
@@ -20251,6 +20363,7 @@ function profilerResultAspects(res) {
if (groups.records.length) out.push({ key: 'records', ok: groups.records.every(c => c.ok) });
}
if (groups.contents.length) out.push({ key: 'contents', ok: groups.contents.every(c => c.ok) });
if (groups.accessRules.length) out.push({ key: 'Access rules', ok: groups.accessRules.every(c => c.ok) });
return out;
}
@@ -20269,6 +20382,7 @@ function profilerAspectSummary(aspects, marked, tr) {
const parts = [];
if (fcp) parts.push(fcp);
if (has('contents')) parts.push(tr('contents'));
if (has('Access rules')) parts.push(tr('Access rules'));
return parts.join(', ');
}
@@ -20409,14 +20523,35 @@ function fcpSeidName(seid) {
return 'SE' + (Number(seid) || 0).toString(16).toUpperCase().padStart(2, '0');
}
// The FCP's '8B' referencing DO of an FCI hex, parsed (or null).
function fcpArrRefFromFci(hex) {
const item = fcpDecode(hex).items.find(i => i.key === '8B');
return item ? fcpArrRef(item.value) : null;
}
// The FCI without its top-level '8B' referencing DO: the exact-FCI comparison
// with `ignoreFciRef` compares these, so a different EF.ARR record number no
// longer fails while every other DO is still compared byte-exactly (the
// reference's meaning is verified by the access-rules check). Returns null
// when the FCI does not parse - the caller falls back to the strict compare.
function fcpWithoutArrRef(hex) {
const d = fcpDecode(hex);
if (!d.items.length || d.error) return null;
const parts = d.items.filter(i => i.key.indexOf('/') < 0 && i.key !== '8B').map(i => i.raw || '');
if (parts.some(p => !p)) return null;
return (d.template || '') + ':' + parts.join('');
}
// Access rules of the record(s) an FCP '8B' reference points at, resolved
// against a set of EF.ARR copies ({fid, path, records[]} - the server's
// /api/arr cache or a snapshot's own EF.ARR files). `path` is the tree path
// of the file whose FCI is shown: it picks the ARR copy of that DF (the
// ISO 7816-4 5.4.3.3 lookup order - ADF files use the ADF's 6F06 copy).
// Returns [] when nothing resolves - the caller stays silent (v3.24.0).
// Returns {found, rules}: found = at least one referenced record exists;
// rules = the decoded rule lines (empty when nothing resolves - the caller
// stays silent, v3.24.0).
function fcpArrRules(ref, arrs, path) {
if (!ref || !arrs || !arrs.length) return [];
if (!ref || !arrs || !arrs.length) return { found: false, rules: [] };
const df = (((path || '').split('/')[0]) || 'MF').toUpperCase();
const pick = fid => {
const hits = arrs.filter(a => String(a.fid || '').toUpperCase() === fid);
@@ -20424,10 +20559,12 @@ function fcpArrRules(ref, arrs, path) {
return hits.find(a => String(a.path || '').split('/')[0].toUpperCase() === df) || hits[0];
};
const out = [];
let found = false;
const add = (label, fid, record) => {
const arr = pick(fid);
const recs = (arr && arr.records) || [];
if (!(record >= 1) || record > recs.length) return;
found = true;
const dec = efDecArr(efBytes(recs[record - 1] || ''));
for (const rule of ((dec && dec.rules) || [])) {
out.push((label ? label + ': ' : '') + rule);
@@ -20440,7 +20577,7 @@ function fcpArrRules(ref, arrs, path) {
} else {
add('', ref.fid, ref.record);
}
return out;
return { found: found, rules: out };
}
function fcpDo(key, value) {
@@ -20598,13 +20735,13 @@ function fcpDecode(hex) {
const items = [];
for (const t of parsed.tlvs) {
const d = fcpDo(t.tag, t.value);
items.push({ key: t.tag, tag: t.tag, name: d.name, value: t.value, decoded: d.decoded });
items.push({ key: t.tag, tag: t.tag, name: d.name, value: t.value, decoded: d.decoded, raw: t.raw });
if (t.tag === 'A5' || t.tag === 'C6') {
const sub = fcpParseTlvs(t.value);
if (!error && sub.error) error = 'In ' + t.tag + ': ' + sub.error;
for (const s of sub.tlvs) {
const sd = fcpDo(t.tag + '/' + s.tag, s.value);
items.push({ key: t.tag + '/' + s.tag, tag: s.tag, name: sd.name, value: s.value, decoded: sd.decoded });
items.push({ key: t.tag + '/' + s.tag, tag: s.tag, name: sd.name, value: s.value, decoded: sd.decoded, raw: s.raw });
}
}
}
@@ -20616,18 +20753,19 @@ function profilerLabelText(labels, which) {
return labels.prefix ? t(which) + ' (' + labels[which] + ')' : labels[which];
}
function fcpDiffHtml(expectedHex, actualHex, labels) {
function fcpDiffHtml(expectedHex, actualHex, labels, opts) {
const expLabel = profilerLabelText(labels, 'expected');
const actLabel = profilerLabelText(labels, 'actual');
const e = fcpDecode(expectedHex);
const a = fcpDecode(actualHex);
const eBad = !!e.error, aBad = !!a.error;
if (!eBad && !aBad && (!e.items.length || !a.items.length)) return '';
const skipKey = k => !!(opts && opts.ignoreArrRef && k === '8B');
const eMap = {}, aMap = {};
e.items.forEach(it => { eMap[it.key] = it; });
a.items.forEach(it => { aMap[it.key] = it; });
const keys = e.items.map(it => it.key);
for (const it of a.items) if (!eMap[it.key]) keys.push(it.key);
const keys = e.items.map(it => it.key).filter(k => !skipKey(k));
for (const it of a.items) if (!eMap[it.key] && !skipKey(it.key)) keys.push(it.key);
let html = '';
if (keys.length) {
html += '<div class="mt-1 text-xs text-gray-500 dark:text-slate-400">' + esc(t('FCI parameters')) + '</div>';
@@ -20651,6 +20789,10 @@ function fcpDiffHtml(expectedHex, actualHex, labels) {
}
if (eBad) html += '<div class="text-xs text-red-600 mt-0.5 pl-2">' + esc(expLabel) + ': ' + esc(e.error) + '</div>';
if (aBad) html += '<div class="text-xs text-red-600 mt-0.5 pl-2">' + esc(actLabel) + ': ' + esc(a.error) + '</div>';
if (opts && opts.ignoreArrRef) {
html += '<div class="text-xs text-gray-500 dark:text-slate-400 mt-0.5 pl-2">' +
esc(t('EF.ARR reference ignored (verified by the access-rules check)')) + '</div>';
}
return html;
}
@@ -20689,7 +20831,7 @@ function profilerFciPreviewItems(hex, opts) {
// ARR content (the file manager's live cache, or a snapshot's copy)
const ref = it.key === '8B' ? fcpArrRef(it.value) : null;
rows.push({ key: it.key, name: it.name, text: it.decoded || it.value,
sub: ref ? fcpArrRules(ref, opts.arr, opts.path) : [] });
sub: ref ? fcpArrRules(ref, opts.arr, opts.path).rules : [] });
}
const fi = rows.find(r => r.key === '83');
const sfi = rows.find(r => r.key === '88');
@@ -20720,6 +20862,30 @@ function profilerFciInput(i, value) {
profilerUpdateFciPreview(i, value);
}
// A failed access-rules check: the decoded rules of the referenced EF.ARR
// record, expected vs actual (set diff; v3.25.0).
function profilerAccessRulesDiffHtml(c) {
let html = '<div class="mt-1 pl-2">';
html += '<div class="text-xs text-red-600 font-mono">' + esc(t('Access rules')) + '</div>';
if (c.unresolved) {
html += '<div class="text-xs text-yellow-600 mt-0.5">' +
esc(t('the EF.ARR reference could not be resolved (no cached EF.ARR or the record is out of range)')) + '</div>';
return html + '</div>';
}
if (!c.actual) {
html += '<div class="text-xs text-red-600 mt-0.5">' + esc(t('the FCI has no EF.ARR reference')) + '</div>';
}
const line = (mark, cls, text) => '<div class="text-xs font-mono mt-0.5 ' + cls + '">' + esc(mark + ' ' + text) + '</div>';
for (const x of (c.expected || [])) {
const miss = (c.missing || []).includes(x);
html += line(miss ? '\u2212' : '\u2713', miss ? 'text-red-600' : 'text-gray-500 dark:text-slate-400', x);
}
for (const x of (c.extra || [])) {
html += line('+', 'text-red-600', x);
}
return html + '</div>';
}
function profilerRenderReport(results, labels) {
const expLabel = profilerLabelText(labels, 'expected');
const actLabel = profilerLabelText(labels, 'actual');
@@ -20743,6 +20909,8 @@ function profilerRenderReport(results, labels) {
if (c.ok) continue;
if (c.label === 'extra file') {
html += '<div class="text-xs text-red-600 mt-1">' + esc(t('extra file')) + '</div>';
} else if (c.label === 'accessRules') {
html += profilerAccessRulesDiffHtml(c);
} else if (profilerRawDataCheck(c)) {
const isContent = c.label === 'content' || /^content\.rec\d+$/.test(c.label);
const dd = isContent && typeof efContentDiff === 'function'
@@ -20776,7 +20944,7 @@ function profilerRenderReport(results, labels) {
} else if (dd && dd.sameDecoded) {
html += '<div class="text-xs text-gray-500 dark:text-slate-400 pl-2 mt-0.5">' + esc(t('Decoded values match; raw bytes differ')) + '</div>';
}
if (c.label === 'fci') html += fcpDiffHtml(String(c.expected), String(c.actual), labels);
if (c.label === 'fci') html += fcpDiffHtml(String(c.expected), String(c.actual), labels, c.ignoredRef ? { ignoreArrRef: true } : null);
html += '</div>';
} else {
html += '<div class="text-xs text-red-600 mt-1">' + esc(c.label) + ': ' + esc(expLabel) + ' <b>' + esc(String(c.expected)) + '</b>, ' + esc(actLabel) + ' <b>' + esc(String(c.actual)) + '</b></div>';
@@ -22925,6 +23093,14 @@ const LANG_RU = {
'of': 'из',
'None': 'Нет',
'Exact': 'Точное',
'Check': 'Проверять',
'Access rules': 'Правила доступа',
'Check access rules': 'Проверять правила доступа',
'Ignore the EF.ARR reference in the exact FCI': 'Игнорировать ссылку на EF.ARR в точном FCI',
'the FCI has no EF.ARR reference': 'в FCI нет ссылки на EF.ARR',
'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 игнорируется (проверяется по правилам доступа)',
'Mask': 'Маска',
'Checking': 'Проверка',
'passed': 'пройдено',
+1 -1
View File
@@ -1,4 +1,4 @@
const CACHE = 'simple-v423';
const CACHE = 'simple-v424';
const URLS = [
'index.html',
'help.html',
+131 -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', 'profilerSnapshotArr', 'efBytes', 'efHex', 'efTlv', 'efDecArr', 'efArrAmLabel', 'efArrSc', 'efArrAmDo', 'efFidFromPath',
'fcpArrRef', 'fcpSeidName', 'fcpArrRules', 'fcpArrRefFromFci', 'fcpWithoutArrRef', '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';
@@ -930,6 +930,136 @@ test('pysimArrEnsure does not cache a transient failure', async () => {
_pysimCardSession = null;
});
test('fcpWithoutArrRef drops only the 8B referencing DO', () => {
const a = '621A82054221000F0583026F4F8A01058B032F06028802004B8801B0';
const b = '621A82054221000F0583026F4F8A01058B032F06098802004B8801B0';
const wa = fcpWithoutArrRef(a), wb = fcpWithoutArrRef(b);
assert.ok(wa && wb, 'parseable');
assert.strictEqual(wa, wb); // only the record number differed
assert.ok(!wa.includes('8B03'), wa); // the reference is gone
assert.ok(wa.includes('82054221000F05'), wa); // every other DO is kept
assert.strictEqual(fcpWithoutArrRef('garbage'), null);
});
test('fcpArrRefFromFci reads the 8B reference of an FCI', () => {
assert.deepStrictEqual(fcpArrRefFromFci('621A82054221000F0583026F4F8A01058B032F06028802004B8801B0'),
{ fid: '2F06', record: 2 });
assert.strictEqual(fcpArrRefFromFci('621582054221000F0583026F4F8A01058002004B8801B0'), null);
});
test('access rules are captured from a snapshot rule', () => {
const snap = { files: [
{ path: 'MF/2F06', name: 'EF.ARR', fileType: 'linear_fixed', recordLen: 6, numRecords: 2,
content: { kind: 'record', records: [{ num: 1, data: 'FFFF' },
{ num: 2, data: '800101A40683010A950108' }] } },
{ path: 'MF/7F20/6F07', name: 'EF.IMSI',
fciHex: '621A82054221000F0583026F4F8A01058B032F06028802004B8801B0' },
]};
const arrs = profilerSnapshotArr(snap);
const rule = profilerBuildFileRuleFromSnapshot(snap.files[1], new Set(), null, 'type_size', null, arrs);
assert.deepStrictEqual(rule.accessRules,
{ expected: ['AM 0x01 (READ/SEARCH (EF) / DELETE FILE child (DF)): ADM1 (verify)'] });
// without the ARR copies nothing is captured
const bare = profilerBuildFileRuleFromSnapshot(snap.files[1], new Set(), null, 'type_size', null, []);
assert.strictEqual(bare.accessRules, undefined);
});
test('profilerRunRule checks the access rules against the ARR copies', async () => {
const FCI2 = '621A82054221000F0583026F4F8A01058B032F06028802004B8801B0';
const ADM = 'AM 0x01 (READ/SEARCH (EF) / DELETE FILE child (DF)): ADM1 (verify)';
const ALW = 'AM 0x01 (READ/SEARCH (EF) / DELETE FILE child (DF)): always';
const arrs = [{ fid: '2F06', path: 'MF/2F06', records: ['FFFF', '800101A40683010A950108'] }];
const src = (fci, a) => ({
select: async () => ({ name: 'EF.SAMPLE', file_type: 'linear_fixed', file_size: null,
record_len: 6, num_of_rec: 2, fci_hex: fci, exists: true }),
read: async () => ({ success: false }),
arrs: () => a,
});
const rule = fciHex => ({ type: 'file', path: 'MF/7F20/6F07', name: 'EF.SAMPLE',
fileType: 'linear_fixed', fciMode: 'type_size', fciHex: fciHex, accessRules: { expected: [ADM] } });
// pass
const ok = await profilerRunRule(rule(FCI2), src(FCI2, arrs));
assert.strictEqual(ok.status, 'pass', JSON.stringify(ok.checks));
// mismatch: missing + extra
const bad = await profilerRunRule(
{ ...rule(FCI2), accessRules: { expected: [ALW] } }, src(FCI2, arrs));
assert.strictEqual(bad.status, 'fail');
const c1 = bad.checks.find(c => c.label === 'accessRules');
assert.deepStrictEqual(c1.missing, [ALW]);
assert.deepStrictEqual(c1.extra, [ADM]);
// no EF.ARR reference on the card
const FCI0 = '621582054221000F0583026F4F8A01058002004B8801B0';
const noref = await profilerRunRule(rule(FCI2), src(FCI0, arrs));
assert.strictEqual(noref.status, 'fail');
assert.strictEqual(noref.checks.find(c => c.label === 'accessRules').actual, null);
// unresolvable (no cached ARR / record out of range)
const unres = await profilerRunRule(rule(FCI2), src(FCI2, []));
assert.strictEqual(unres.status, 'error');
assert.ok(unres.checks.find(c => c.label === 'accessRules').unresolved);
});
test('ignoreFciRef compares the FCI without the EF.ARR reference', async () => {
const FCI2 = '621A82054221000F0583026F4F8A01058B032F06028802004B8801B0';
const FCI9 = '621A82054221000F0583026F4F8A01058B032F06098802004B8801B0';
const ALW = 'AM 0x01 (READ/SEARCH (EF) / DELETE FILE child (DF)): always';
// record 2 and record 9 carry the same rule
const arrs = [{ fid: '2F06', path: 'MF/2F06',
records: ['', '8001019000', '', '', '', '', '', '', '8001019000'] }];
const src = fci => ({
select: async () => ({ name: 'EF.SAMPLE', file_type: 'linear_fixed', fci_hex: fci, exists: true }),
read: async () => ({ success: false }),
arrs: () => arrs,
});
const rule = { type: 'file', path: 'MF/7F20/6F07', fileType: 'linear_fixed', fciMode: 'exact',
fciHex: FCI2, accessRules: { expected: [ALW], ignoreFciRef: true } };
const res = await profilerRunRule(rule, src(FCI9));
assert.strictEqual(res.status, 'pass', JSON.stringify(res.checks));
assert.strictEqual(res.checks.find(c => c.label === 'fci').ignoredRef, true);
// without the flag the same setup fails on the FCI
const strict = await profilerRunRule({ ...rule, accessRules: { expected: [ALW] } }, src(FCI9));
assert.strictEqual(strict.status, 'fail');
assert.strictEqual(strict.checks.find(c => c.label === 'fci').ok, false);
});
test('profilerResultAspects and the summary include Access rules', () => {
const res = { checks: [
{ label: 'exists', ok: true },
{ label: 'fileType', ok: true },
{ label: 'accessRules', ok: false, expected: ['x'], actual: ['y'], missing: ['x'], extra: ['y'] },
]};
const aspects = profilerResultAspects(res);
assert.ok(aspects.some(a => a.key === 'Access rules' && !a.ok), JSON.stringify(aspects));
const sum = profilerAspectSummary(aspects, true, s => s);
assert.ok(sum.includes('Access rules'), sum);
});
test('profile validation accepts the access-rules field', () => {
const base = { name: 'P', rules: [{ type: 'file', path: 'MF/7F20/6F07' }] };
assert.strictEqual(profilerValidateProfile(base), null);
assert.strictEqual(profilerValidateProfile({ ...base, rules: [{ type: 'file', path: 'MF/7F20/6F07',
accessRules: { expected: ['AM 0x01: always'] } }] }), null);
assert.ok(profilerValidateProfile({ ...base, rules: [{ type: 'file', path: 'MF/7F20/6F07',
accessRules: { expected: 'nope' } }] }));
assert.ok(profilerValidateProfile({ ...base, rules: [{ type: 'file', path: 'MF/7F20/6F07',
accessRules: { expected: [1] } }] }));
});
test('the access-rules report block marks missing and extra lines', () => {
global.t = s => s;
const html = profilerAccessRulesDiffHtml({ label: 'accessRules', ok: false,
expected: ['A', 'B'], actual: ['B', 'C'], missing: ['A'], extra: ['C'] });
assert.ok(html.includes('\u2212 A'), html);
assert.ok(html.includes('\u2713 B'), html);
assert.ok(html.includes('+ C'), html);
const unres = profilerAccessRulesDiffHtml({ label: 'accessRules', ok: false, unresolved: true,
expected: ['A'], actual: null, missing: [], extra: [] });
assert.ok(unres.includes('could not be resolved'), unres);
const noref = profilerAccessRulesDiffHtml({ label: 'accessRules', ok: false,
expected: ['A'], actual: null, missing: ['A'], extra: [] });
assert.ok(noref.includes('no EF.ARR reference'), noref);
delete global.t;
});
test('pysimFsInfoHtml embeds the symbolic name in the FCI border', () => {
global.t = s => s;
const hex = '6212800200098202412183026F078A0105880110';