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:
@@ -466,10 +466,11 @@
|
|||||||
<ul class="list-disc list-inside text-sm space-y-1 mb-3">
|
<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>Путь</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>Проверка 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> — проверяет условия доступа файла как <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 по умолчанию пропускается — значимая проверка это правила); в редакторе захваченные строки показываются только для чтения с переключателем «Нет/Проверять». Файл, чей 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> — тип файла, размер, длина записи и число записей из 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>
|
<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>
|
</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>Результаты проверки профиля: <профиль> → <ICCID карты></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, проприетарные параметры…). Расхождения правил доступа выводят декодированные строки правил (✓ совпадает, − отсутствует, + лишнее). Декодированный просмотр FCI также отображается рядом с полем FCI hex при редактировании правила. Если данные FCI повреждены, показывается всё, что удалось декодировать до места ошибки, вместе с явным сообщением об ошибке декодирования. Для record-файлов при расхождении содержимого добавляется пометка <em>совпадающие записи: 1-5, 7-10</em> со списком записей, которые совпали. В отчёте поля расхождений и колонки сравнения FCI подписаны <em>ожидалось (имя профиля)</em> и <em>фактически (ICCID карты)</em>, а в заголовке отчёта выводится <em>Результаты проверки профиля: <профиль> → <ICCID карты></em>. Опция <strong>«Только расхождения»</strong> в заголовке отчёта скрывает все совпавшие файлы и оставляет только несовпадения и ошибки.</p>
|
||||||
<h4 class="font-medium mb-1">Опции сканирования «Профиль с карты»</h4>
|
<h4 class="font-medium mb-1">Опции сканирования «Профиль с карты»</h4>
|
||||||
<p class="text-sm mb-2">Диалог сканирования запрашивает имя профиля и предлагает селектор <strong>«Проверка FCP/FCI»</strong> (те же три режима, по умолчанию <strong>Тип файла + размер (FCP)</strong>), применяемый ко всем создаваемым правилам, а также список <strong>«Игнорировать содержимое файлов»</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>«Сравнивать первые 4 байта для»</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>«Пользовательские файлы»</strong> включаются с той же проверкой существования. Тот же диалог и опции использует <strong>«Профиль из снимка»</strong>: вместо карты обходятся захваченные файлы выбранного снимка; для файлов, содержимое которых не было захвачено, правило создаётся без проверки содержимого (при последующей проверке профиля они помечаются как непроверяемые).</p>
|
<p class="text-sm mb-2">Диалог сканирования запрашивает имя профиля и предлагает селектор <strong>«Проверка FCP/FCI»</strong> (те же три режима, по умолчанию <strong>Тип файла + размер (FCP)</strong>), применяемый ко всем создаваемым правилам, а также список <strong>«Игнорировать содержимое файлов»</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>«Сравнивать первые 4 байта для»</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>«Пользовательские файлы»</strong> включаются с той же проверкой существования. Тот же диалог и опции использует <strong>«Профиль из снимка»</strong>: вместо карты обходятся захваченные файлы выбранного снимка; для файлов, содержимое которых не было захвачено, правило создаётся без проверки содержимого (при последующей проверке профиля они помечаются как непроверяемые).</p>
|
||||||
|
|
||||||
|
|||||||
+2
-1
@@ -465,10 +465,11 @@
|
|||||||
<ul class="list-disc list-inside text-sm space-y-1 mb-3">
|
<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>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>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> — verifies a file’s access conditions as <em>decoded rules</em>, independent of which <code class="font-mono text-sm">EF.ARR</code> record the FCI’s security-attribute reference (<code class="font-mono text-sm">'8B'</code>) points at: the referenced record’s rules are resolved from the card’s (or the snapshot’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’s own contents are skipped by default — 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’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>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>
|
<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>
|
</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: <profile> → <card ICCID></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 (✓ matching, − 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: <profile> → <card ICCID></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">“Profile from card” scan options</h4>
|
<h4 class="font-medium mb-1">“Profile from card” scan options</h4>
|
||||||
<p class="text-sm mb-2">The scan dialog asks for a profile name and offers a <strong>“FCP/FCI check”</strong> selector (the same three modes above, default <strong>Filetype + size</strong>) applied to every generated rule, plus an <strong>“Ignore contents of files”</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>“Match first 4 bytes for”</strong> <code class="font-mono text-sm">EF.IMSI</code> and <code class="font-mono text-sm">EF.ICCID</code> capture those files’ 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’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>
|
<p class="text-sm mb-2">The scan dialog asks for a profile name and offers a <strong>“FCP/FCI check”</strong> selector (the same three modes above, default <strong>Filetype + size</strong>) applied to every generated rule, plus an <strong>“Ignore contents of files”</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>“Match first 4 bytes for”</strong> <code class="font-mono text-sm">EF.IMSI</code> and <code class="font-mono text-sm">EF.ICCID</code> capture those files’ 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’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
@@ -1905,7 +1905,7 @@
|
|||||||
// ===== Version =====
|
// ===== Version =====
|
||||||
// Single source of truth for the PWA version: shown in the header and used
|
// Single source of truth for the PWA version: shown in the header and used
|
||||||
// by the server version check in pysimConnect().
|
// 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;
|
document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION;
|
||||||
|
|
||||||
// ===== view history (the browser Back/Forward buttons) =====
|
// ===== 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.type !== 'file') return 'Unsupported rule type: ' + r.type;
|
||||||
if (!r.path) return 'Rule missing path';
|
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.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;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -19287,7 +19292,7 @@ async function profilerScanCard(ignoreFids, ignoreNames, fciMode, onProgress, ma
|
|||||||
|
|
||||||
let _scanSnapshot = null;
|
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 isRecord = profilerContentKindForFileType(f.fileType) === 'record';
|
||||||
const rule = {
|
const rule = {
|
||||||
type: 'file',
|
type: 'file',
|
||||||
@@ -19301,6 +19306,12 @@ function profilerBuildFileRuleFromSnapshot(f, ignoreFids, ignoreNames, fciMode,
|
|||||||
fciHex: f.fciHex || null,
|
fciHex: f.fciHex || null,
|
||||||
content: 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 seg = (f.path || '').toUpperCase().split('/').pop();
|
||||||
const fid = /^[0-9A-F]{4}$/.test(seg) ? seg : '';
|
const fid = /^[0-9A-F]{4}$/.test(seg) ? seg : '';
|
||||||
const name = (f.name || '').toUpperCase();
|
const name = (f.name || '').toUpperCase();
|
||||||
@@ -19324,11 +19335,13 @@ function profilerBuildFileRuleFromSnapshot(f, ignoreFids, ignoreNames, fciMode,
|
|||||||
async function profilerScanSnapshot(snapshot, ignoreFids, ignoreNames, fciMode, maskFids, onProgress) {
|
async function profilerScanSnapshot(snapshot, ignoreFids, ignoreNames, fciMode, maskFids, onProgress) {
|
||||||
const files = (snapshot && snapshot.files) ? snapshot.files : [];
|
const files = (snapshot && snapshot.files) ? snapshot.files : [];
|
||||||
const total = files.length;
|
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, '');
|
if (onProgress) onProgress(0, total, '');
|
||||||
const rules = [];
|
const rules = [];
|
||||||
for (let i = 0; i < files.length; i++) {
|
for (let i = 0; i < files.length; i++) {
|
||||||
if (onProgress) onProgress(i + 1, total, files[i].path);
|
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));
|
if (i % 20 === 19) await new Promise(r => setTimeout(r, 0));
|
||||||
}
|
}
|
||||||
return rules;
|
return rules;
|
||||||
@@ -19353,6 +19366,16 @@ async function profilerBuildFileRule(path, c, ignoreFids, ignoreNames, fciMode,
|
|||||||
fciHex: sel.fci_hex || null,
|
fciHex: sel.fci_hex || null,
|
||||||
content: 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 fid = (c.fid || sel.fid || '').toUpperCase();
|
||||||
const name = (c.name || sel.name || '').toUpperCase();
|
const name = (c.name || sel.name || '').toUpperCase();
|
||||||
if (ignoreFids.has(fid) || (ignoreNames && ignoreNames.has(name))) return rule;
|
if (ignoreFids.has(fid) || (ignoreNames && ignoreNames.has(name))) return rule;
|
||||||
@@ -19779,10 +19802,53 @@ function profilerRenderRule(i, r) {
|
|||||||
if (r.fileType !== 'df') {
|
if (r.fileType !== 'df') {
|
||||||
html += profilerRenderContentEditor(i, r);
|
html += profilerRenderContentEditor(i, r);
|
||||||
}
|
}
|
||||||
|
html += profilerRenderAccessRulesEditor(i, r);
|
||||||
html += '</div>';
|
html += '</div>';
|
||||||
return html;
|
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) {
|
function profilerRenderContentEditor(i, r) {
|
||||||
const c = r.content;
|
const c = r.content;
|
||||||
const mode = c ? c.mode : 'exact';
|
const mode = c ? c.mode : 'exact';
|
||||||
@@ -20091,11 +20157,15 @@ function profilerLiveSource() {
|
|||||||
return {
|
return {
|
||||||
select: path => pysimFetch('/api/select', { path: path }),
|
select: path => pysimFetch('/api/select', { path: path }),
|
||||||
read: path => pysimFetch('/api/read', { path: path, mode: 'raw' }),
|
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) {
|
function profilerSnapshotSource(snapshot) {
|
||||||
const find = path => (snapshot.files || []).find(f => (f.path || '').toUpperCase() === (path || '').toUpperCase());
|
const find = path => (snapshot.files || []).find(f => (f.path || '').toUpperCase() === (path || '').toUpperCase());
|
||||||
|
let arrCache = null;
|
||||||
return {
|
return {
|
||||||
select: async path => {
|
select: async path => {
|
||||||
const f = find(path);
|
const f = find(path);
|
||||||
@@ -20117,6 +20187,8 @@ function profilerSnapshotSource(snapshot) {
|
|||||||
if (f.content.kind === 'record') return { success: true, records: f.content.records };
|
if (f.content.kind === 'record') return { success: true, records: f.content.records };
|
||||||
return { success: true, data: f.content.data };
|
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) {
|
if (fciMode === 'exact' && r.fciHex) {
|
||||||
const expected = profilerNormHexStrict(r.fciHex);
|
let expected = profilerNormHexStrict(r.fciHex);
|
||||||
const actual = profilerNormHexStrict(sel.fci_hex);
|
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;
|
const ok = !!expected && expected === actual;
|
||||||
if (!ok) res.status = 'fail';
|
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) {
|
if (r.content) {
|
||||||
let rd;
|
let rd;
|
||||||
@@ -20233,7 +20344,7 @@ function profilerCustomNameForPath(path) {
|
|||||||
// that actually ran. 'Exact FCI' subsumes filetype/size/records; 'exists' is
|
// that actually ran. 'Exact FCI' subsumes filetype/size/records; 'exists' is
|
||||||
// implicit and omitted.
|
// implicit and omitted.
|
||||||
function profilerResultAspects(res) {
|
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 || [])) {
|
for (const c of (res.checks || [])) {
|
||||||
const l = c.label;
|
const l = c.label;
|
||||||
if (l === 'fileType') groups.filetype.push(c);
|
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 === 'recordLen' || l === 'numRecords') groups.records.push(c);
|
||||||
else if (l === 'fci') groups['Exact FCI'].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 === 'content' || l.startsWith('content.')) groups.contents.push(c);
|
||||||
|
else if (l === 'accessRules') groups.accessRules.push(c);
|
||||||
}
|
}
|
||||||
const out = [];
|
const out = [];
|
||||||
if (groups['Exact FCI'].length) {
|
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.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.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;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -20269,6 +20382,7 @@ function profilerAspectSummary(aspects, marked, tr) {
|
|||||||
const parts = [];
|
const parts = [];
|
||||||
if (fcp) parts.push(fcp);
|
if (fcp) parts.push(fcp);
|
||||||
if (has('contents')) parts.push(tr('contents'));
|
if (has('contents')) parts.push(tr('contents'));
|
||||||
|
if (has('Access rules')) parts.push(tr('Access rules'));
|
||||||
return parts.join(', ');
|
return parts.join(', ');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -20409,14 +20523,35 @@ function fcpSeidName(seid) {
|
|||||||
return 'SE' + (Number(seid) || 0).toString(16).toUpperCase().padStart(2, '0');
|
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
|
// 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
|
// 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
|
// /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
|
// 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).
|
// 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) {
|
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 df = (((path || '').split('/')[0]) || 'MF').toUpperCase();
|
||||||
const pick = fid => {
|
const pick = fid => {
|
||||||
const hits = arrs.filter(a => String(a.fid || '').toUpperCase() === 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];
|
return hits.find(a => String(a.path || '').split('/')[0].toUpperCase() === df) || hits[0];
|
||||||
};
|
};
|
||||||
const out = [];
|
const out = [];
|
||||||
|
let found = false;
|
||||||
const add = (label, fid, record) => {
|
const add = (label, fid, record) => {
|
||||||
const arr = pick(fid);
|
const arr = pick(fid);
|
||||||
const recs = (arr && arr.records) || [];
|
const recs = (arr && arr.records) || [];
|
||||||
if (!(record >= 1) || record > recs.length) return;
|
if (!(record >= 1) || record > recs.length) return;
|
||||||
|
found = true;
|
||||||
const dec = efDecArr(efBytes(recs[record - 1] || ''));
|
const dec = efDecArr(efBytes(recs[record - 1] || ''));
|
||||||
for (const rule of ((dec && dec.rules) || [])) {
|
for (const rule of ((dec && dec.rules) || [])) {
|
||||||
out.push((label ? label + ': ' : '') + rule);
|
out.push((label ? label + ': ' : '') + rule);
|
||||||
@@ -20440,7 +20577,7 @@ function fcpArrRules(ref, arrs, path) {
|
|||||||
} else {
|
} else {
|
||||||
add('', ref.fid, ref.record);
|
add('', ref.fid, ref.record);
|
||||||
}
|
}
|
||||||
return out;
|
return { found: found, rules: out };
|
||||||
}
|
}
|
||||||
|
|
||||||
function fcpDo(key, value) {
|
function fcpDo(key, value) {
|
||||||
@@ -20598,13 +20735,13 @@ function fcpDecode(hex) {
|
|||||||
const items = [];
|
const items = [];
|
||||||
for (const t of parsed.tlvs) {
|
for (const t of parsed.tlvs) {
|
||||||
const d = fcpDo(t.tag, t.value);
|
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') {
|
if (t.tag === 'A5' || t.tag === 'C6') {
|
||||||
const sub = fcpParseTlvs(t.value);
|
const sub = fcpParseTlvs(t.value);
|
||||||
if (!error && sub.error) error = 'In ' + t.tag + ': ' + sub.error;
|
if (!error && sub.error) error = 'In ' + t.tag + ': ' + sub.error;
|
||||||
for (const s of sub.tlvs) {
|
for (const s of sub.tlvs) {
|
||||||
const sd = fcpDo(t.tag + '/' + s.tag, s.value);
|
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];
|
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 expLabel = profilerLabelText(labels, 'expected');
|
||||||
const actLabel = profilerLabelText(labels, 'actual');
|
const actLabel = profilerLabelText(labels, 'actual');
|
||||||
const e = fcpDecode(expectedHex);
|
const e = fcpDecode(expectedHex);
|
||||||
const a = fcpDecode(actualHex);
|
const a = fcpDecode(actualHex);
|
||||||
const eBad = !!e.error, aBad = !!a.error;
|
const eBad = !!e.error, aBad = !!a.error;
|
||||||
if (!eBad && !aBad && (!e.items.length || !a.items.length)) return '';
|
if (!eBad && !aBad && (!e.items.length || !a.items.length)) return '';
|
||||||
|
const skipKey = k => !!(opts && opts.ignoreArrRef && k === '8B');
|
||||||
const eMap = {}, aMap = {};
|
const eMap = {}, aMap = {};
|
||||||
e.items.forEach(it => { eMap[it.key] = it; });
|
e.items.forEach(it => { eMap[it.key] = it; });
|
||||||
a.items.forEach(it => { aMap[it.key] = it; });
|
a.items.forEach(it => { aMap[it.key] = it; });
|
||||||
const keys = e.items.map(it => it.key);
|
const keys = e.items.map(it => it.key).filter(k => !skipKey(k));
|
||||||
for (const it of a.items) if (!eMap[it.key]) keys.push(it.key);
|
for (const it of a.items) if (!eMap[it.key] && !skipKey(it.key)) keys.push(it.key);
|
||||||
let html = '';
|
let html = '';
|
||||||
if (keys.length) {
|
if (keys.length) {
|
||||||
html += '<div class="mt-1 text-xs text-gray-500 dark:text-slate-400">' + esc(t('FCI parameters')) + '</div>';
|
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 (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 (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;
|
return html;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -20689,7 +20831,7 @@ function profilerFciPreviewItems(hex, opts) {
|
|||||||
// ARR content (the file manager's live cache, or a snapshot's copy)
|
// ARR content (the file manager's live cache, or a snapshot's copy)
|
||||||
const ref = it.key === '8B' ? fcpArrRef(it.value) : null;
|
const ref = it.key === '8B' ? fcpArrRef(it.value) : null;
|
||||||
rows.push({ key: it.key, name: it.name, text: it.decoded || it.value,
|
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 fi = rows.find(r => r.key === '83');
|
||||||
const sfi = rows.find(r => r.key === '88');
|
const sfi = rows.find(r => r.key === '88');
|
||||||
@@ -20720,6 +20862,30 @@ function profilerFciInput(i, value) {
|
|||||||
profilerUpdateFciPreview(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) {
|
function profilerRenderReport(results, labels) {
|
||||||
const expLabel = profilerLabelText(labels, 'expected');
|
const expLabel = profilerLabelText(labels, 'expected');
|
||||||
const actLabel = profilerLabelText(labels, 'actual');
|
const actLabel = profilerLabelText(labels, 'actual');
|
||||||
@@ -20743,6 +20909,8 @@ function profilerRenderReport(results, labels) {
|
|||||||
if (c.ok) continue;
|
if (c.ok) continue;
|
||||||
if (c.label === 'extra file') {
|
if (c.label === 'extra file') {
|
||||||
html += '<div class="text-xs text-red-600 mt-1">' + esc(t('extra file')) + '</div>';
|
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)) {
|
} else if (profilerRawDataCheck(c)) {
|
||||||
const isContent = c.label === 'content' || /^content\.rec\d+$/.test(c.label);
|
const isContent = c.label === 'content' || /^content\.rec\d+$/.test(c.label);
|
||||||
const dd = isContent && typeof efContentDiff === 'function'
|
const dd = isContent && typeof efContentDiff === 'function'
|
||||||
@@ -20776,7 +20944,7 @@ function profilerRenderReport(results, labels) {
|
|||||||
} else if (dd && dd.sameDecoded) {
|
} 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>';
|
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>';
|
html += '</div>';
|
||||||
} else {
|
} 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>';
|
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': 'из',
|
'of': 'из',
|
||||||
'None': 'Нет',
|
'None': 'Нет',
|
||||||
'Exact': 'Точное',
|
'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': 'Маска',
|
'Mask': 'Маска',
|
||||||
'Checking': 'Проверка',
|
'Checking': 'Проверка',
|
||||||
'passed': 'пройдено',
|
'passed': 'пройдено',
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
const CACHE = 'simple-v423';
|
const CACHE = 'simple-v424';
|
||||||
const URLS = [
|
const URLS = [
|
||||||
'index.html',
|
'index.html',
|
||||||
'help.html',
|
'help.html',
|
||||||
|
|||||||
@@ -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',
|
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'];
|
'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 = '';
|
let code = '';
|
||||||
for (const f of FNS) code += extractFunc(html, f) + '\n';
|
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;
|
_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', () => {
|
test('pysimFsInfoHtml embeds the symbolic name in the FCI border', () => {
|
||||||
global.t = s => s;
|
global.t = s => s;
|
||||||
const hex = '6212800200098202412183026F078A0105880110';
|
const hex = '6212800200098202412183026F078A0105880110';
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "pysim-simple-server"
|
name = "pysim-simple-server"
|
||||||
version = "3.24.0"
|
version = "3.25.0"
|
||||||
description = "HTTP REST server wrapping pysim for the SIMple PWA"
|
description = "HTTP REST server wrapping pysim for the SIMple PWA"
|
||||||
requires-python = ">=3.8"
|
requires-python = ">=3.8"
|
||||||
# pysim is a git-only dependency installed explicitly by setup.bat/setup.sh.
|
# pysim is a git-only dependency installed explicitly by setup.bat/setup.sh.
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ from osmocom.tlv import BER_TLV_IE
|
|||||||
from osmocom.utils import rpad
|
from osmocom.utils import rpad
|
||||||
|
|
||||||
|
|
||||||
VERSION = '3.24.0'
|
VERSION = '3.25.0'
|
||||||
|
|
||||||
MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE
|
MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user