v3.25.8: the custom (per-data-object) FCI check in profiles
fciMode 'custom' + fciDos + fciExtra ('ignore' default / 'fail'): each
row is a data-object key (the decoder's keys - 2/4-hex tags and the
nested A5/xx/C6/xx children), a mode (value / present / absent) and, for
value, hex with the contents vocabulary (no '?' = prefix - 8B + 2F06 =
any EF.ARR record of that FID, the record-number-agnostic form; ? =
fixed-length nibble mask). Only the listed objects are checked; 'fail'
also rejects any unlisted object (order-insensitive strictness).
The scan dialog's FCP/FCI selector gained the mode ("Custom (per data
object)" - the acronym spelled out) and seeds every rule created from a
card or a snapshot with that file's captured FCI objects; a manually
added rule starts empty, and the editor offers "load from the captured
FCI" when the list is empty and the rule carries an FCI. The report
reuses the access-rules diff marks; the aspect is 'FCI (custom)',
separate from 'Exact FCI' (the filetype/size/record checks still run).
Docs: help EN/RU (the check bullet + the scan dialog paragraph).
Tests: the check (exact/prefix/mask, present/absent, duplicates, nested
keys, extras ignore/fail, the empty list, the parse error, the
size/record checks still running), the seeding, the editor
rows/datalist/load link, the report, the aspects, the validation.
893 frontend + 880 Python; sw cache simple-v432.
This commit is contained in:
@@ -468,14 +468,14 @@
|
||||
<p class="text-sm mb-2">Правила выполняются последовательно. Редактор показывает символьное имя файла pySim (если известно) рядом с путём правила; <strong>Добавить правило</strong> добавляет правило, <strong>Сохранить</strong> сохраняет изменения. Правило файловой системы задаётся:</p>
|
||||
<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>Проверка 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, жизненного цикла, атрибутов безопасности и проприетарных параметров) или <strong>По объектам данных</strong> — список ожиданий по объектам: каждая строка задаёт объект данных (стандартные теги с именами, включая вложенные <code class="font-mono text-sm">A5/xx</code>/<code class="font-mono text-sm">C6/xx</code>, либо произвольный тег), режим — <em>значение</em> (без <code class="font-mono text-sm">?</code> совпадение по префиксу, <code class="font-mono text-sm">?</code> маскирует полубайт: <code class="font-mono text-sm">8B</code> + <code class="font-mono text-sm">2F06</code> принимает любую запись <code class="font-mono text-sm">EF.ARR</code> с таким FID, <code class="font-mono text-sm">2F06??</code> — 3-байтовую форму), <em>присутствует</em> (объект обязан быть) или <em>отсутствует</em> (его быть не должно) — и для значений hex для сравнения. Проверяются только перечисленные объекты; <strong>Лишние объекты данных: игнорировать/не допускать</strong> дополнительно запрещает всё неперечисленное (без учёта порядка, в отличие от побайтового режима). Профили, созданные с этим режимом с карты или из снимка, заполняются объектами записанного FCI; правило с записанным FCI и пустым списком можно заполнить из него (<em>заполнить из записанного FCI</em>).</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 по умолчанию пропускается — значимая проверка это правила); в редакторе захваченные правила показываются структурированными строками — операция в виде флажков битов (или командной формы RESIZE FILE / INCREASE), а каждое условие безопасности — селектором (always, never или ссылка на ключ с её использованием), с предпросмотром канонической декодированной строки, которую сравнивает проверка; «+ правило» создаёт новое (READ always), а шаблонные формы вне этого набора остаются нередактируемыми строками с возможностью удаления. Селектор <strong>«Сопоставление»</strong> выбирает сравнение: <strong>«Точное»</strong> (по умолчанию) требует полного совпадения правил карты с перечисленными строками; <strong>«Содержит»</strong> требует, чтобы каждое перечисленное правило было разрешено картой, — сравнение <em>по операциям</em> (каждый бит AM / командная запись, поэтому другое объединение битов в байте AM при той же политике тоже совпадёт), а не перечисленное правило не проверяется. Переключатель «Нет/Проверять» отключает проверку, не теряя правила; блок сворачивается в заголовок со счётчиком правил, а «Нет» скрывает его полностью. Файл, чей 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>Результаты проверки профиля: <профиль> → <ICCID карты></em>. Опция <strong>«Только расхождения»</strong> в заголовке отчёта скрывает все совпавшие файлы и оставляет только несовпадения и ошибки.</p>
|
||||
<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> заполняет каждое правило объектами записанного FCI — их можно ослабить в редакторе), применяемый ко всем создаваемым правилам, а также список <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>
|
||||
|
||||
<h4 id="card-snapshots" class="font-medium mb-1">Снимки карт</h4>
|
||||
<p class="text-sm mb-2">Представление списка имеет три вкладки — <strong>«Профили»</strong>, <strong>«Снимки карт»</strong> и <strong>«Пользовательские файлы»</strong> (см. ниже). Снимок карты — неизменяемая фиксация файловой системы карты: для каждого существующего файла сохраняются путь, символьное имя, тип, размер (или длина/число записей), сырой FCI из ответа SELECT и содержимое, если файл читается (без списка игнорирования и без масок). ICCID декодируется из EF.ICCID и показывается рядом с именем снимка. Захваченное содержимое показывается вместе с декодированным представлением, где есть декодер — таблица полей для transparent-файлов и однострочная сводка по каждой записи для record-файлов. При сканировании измеряется время каждой команды карты (SELECT, READ BINARY, READ RECORD) от отправки до ответа; снимок хранит min/сред/max по каждому типу команд и общее время сканирования, а в представлении эти значения показываются в сводке под заголовком, время select/read — для каждого файла и время чтения — для каждой записи. Время носит информационный характер и не используется при проверках и сравнении.</p>
|
||||
|
||||
+2
-2
@@ -467,14 +467,14 @@
|
||||
<p class="text-sm mb-2">Rules run sequentially. The editor shows the symbolic pySim name (when known) next to each rule’s path; use <strong>Add rule</strong> to append one and <strong>Save</strong> to keep the changes. A filesystem rule is defined by:</p>
|
||||
<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>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), <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), or <strong>Custom (per data object)</strong> — a list of per-object expectations: each row names a data object (the standard tags with their names, the nested <code class="font-mono text-sm">A5/xx</code>/<code class="font-mono text-sm">C6/xx</code> children included, or a free tag), the mode — <em>value</em> (no <code class="font-mono text-sm">?</code> matches as a prefix, <code class="font-mono text-sm">?</code> masks a nibble: <code class="font-mono text-sm">8B</code> + <code class="font-mono text-sm">2F06</code> accepts any <code class="font-mono text-sm">EF.ARR</code> record of that FID, <code class="font-mono text-sm">2F06??</code> the 3-byte form), <em>present</em> (the object must exist) or <em>absent</em> (it must not) — and, for values, the hex to compare. Only the listed objects are checked; <strong>Extra data objects: ignore/fail</strong> additionally forbids anything unlisted (order-insensitive, unlike the byte-exact mode). Profiles created from a card or a snapshot with this mode are seeded with the captured FCI’s objects; a rule with a captured FCI and an empty list can be filled from it (<em>load from the captured FCI</em>).</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 rules as structured rows — the operation as bit checkboxes (or the RESIZE FILE / INCREASE command form) and each security condition as a select (always, never, or a key reference with its usage), with a preview line of the canonical decoded string the check compares; <em>+ rule</em> builds a new one (READ always), and the template forms outside this subset stay raw and removable. The <strong>Match</strong> selector picks the comparison: <strong>Exact</strong> (default) requires the card’s rules to equal the listed lines; <strong>Contains</strong> requires every listed rule to be granted by the card, matched <em>per operation</em> (each AM bit / command entry, so a different AM grouping of the same policy still matches), and a rule not listed is not checked. The None/Check toggle disables the check without losing the rules; the block collapses to a header with the rule count and None hides it entirely. 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>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…). 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>
|
||||
<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 four modes above, default <strong>Filetype + size</strong>; choosing <strong>Custom (per data object)</strong> seeds every rule with the file’s captured data objects, ready to relax in the editor) 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>
|
||||
|
||||
<h4 id="card-snapshots" class="font-medium mb-1">Card snapshots</h4>
|
||||
<p class="text-sm mb-2">The list view has three tabs — <strong>Profiles</strong>, <strong>Card snapshots</strong> and <strong>Custom files</strong> (see below). A card snapshot is an immutable capture of the card filesystem: for every existing file it stores the path, symbolic name, file type, size (or record length/count), the raw FCI from the SELECT response, and the contents whenever the file is readable (no ignore list, no masking). The ICCID is decoded from EF.ICCID and shown next to the snapshot name. Captured contents are shown with their decoded form where a decoder exists — a field table for transparent files and a one-line summary per record for record files. The scan also measures each card command (SELECT, READ BINARY, READ RECORD) from command to response; the snapshot stores min/avg/max per command type and the total scan time, and the view shows these in the summary under the title plus the select/read times per file and the read time per record. Timings are informational only and are not used by checks or comparisons.</p>
|
||||
|
||||
+245
-5
@@ -1833,6 +1833,7 @@
|
||||
<option value="type" data-l10n="Filetype only (FCP)">Filetype only (FCP)</option>
|
||||
<option value="type_size" selected data-l10n="Filetype + size (FCP)">Filetype + size (FCP)</option>
|
||||
<option value="exact" data-l10n="Exact FCI">Exact FCI</option>
|
||||
<option value="custom" data-l10n="Custom (per data object)">Custom (per data object)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div id="profiler-scan-progress" class="text-xs text-gray-500 dark:text-slate-400 mb-2 hidden"></div>
|
||||
@@ -1906,7 +1907,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.25.7';
|
||||
const SIMPLE_VERSION = '3.25.8';
|
||||
document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION;
|
||||
|
||||
// ===== view history (the browser Back/Forward buttons) =====
|
||||
@@ -18956,7 +18957,19 @@ function profilerValidateProfile(obj) {
|
||||
if (!r || typeof r !== 'object') return 'Invalid rule';
|
||||
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.fciMode && !['type', 'type_size', 'exact', 'custom'].includes(r.fciMode)) return 'Invalid FCP/FCI mode: ' + r.fciMode;
|
||||
if (r.fciDos !== undefined && r.fciDos !== null) {
|
||||
if (!Array.isArray(r.fciDos)) return 'Invalid FCI data objects: ' + r.path;
|
||||
for (const d of r.fciDos) {
|
||||
if (!d || typeof d !== 'object') return 'Invalid FCI data object: ' + r.path;
|
||||
if (!/^[0-9A-F]{2}(?:[0-9A-F]{2})?(?:\/[0-9A-F]{2}(?:[0-9A-F]{2})?)?$/.test(String(d.key || '').toUpperCase()))
|
||||
return 'Invalid FCI data object tag: ' + r.path;
|
||||
if (!['value', 'present', 'absent'].includes(d.mode)) return 'Invalid FCI data object mode: ' + r.path;
|
||||
if (d.mode === 'value' && !/^[0-9A-F?]{2,}$/.test(String(d.value || '').toUpperCase()))
|
||||
return 'Invalid FCI data object value: ' + r.path;
|
||||
}
|
||||
}
|
||||
if (r.fciExtra !== undefined && !['ignore', 'fail'].includes(r.fciExtra)) return 'Invalid FCI extra data object policy: ' + r.path;
|
||||
if (r.accessRules !== undefined && r.accessRules !== null) {
|
||||
const ar = r.accessRules;
|
||||
if (typeof ar !== 'object' || !Array.isArray(ar.expected) ||
|
||||
@@ -19325,6 +19338,12 @@ function profilerBuildFileRuleFromSnapshot(f, ignoreFids, ignoreNames, fciMode,
|
||||
fciHex: f.fciHex || null,
|
||||
content: null,
|
||||
};
|
||||
// Custom FCI (v3.25.8): seed the per-data-object expectations from the
|
||||
// captured FCI - value-exact at first, relaxed in the editor.
|
||||
if (rule.fciMode === 'custom') {
|
||||
const dos = fcpDosFromFci(rule.fciHex);
|
||||
if (dos.length) rule.fciDos = dos;
|
||||
}
|
||||
// Access rules from the snapshot's own EF.ARR copies (v3.25.0)
|
||||
const ref = fcpArrRefFromFci(rule.fciHex);
|
||||
if (ref) {
|
||||
@@ -19385,6 +19404,12 @@ async function profilerBuildFileRule(path, c, ignoreFids, ignoreNames, fciMode,
|
||||
fciHex: sel.fci_hex || null,
|
||||
content: null,
|
||||
};
|
||||
// Custom FCI (v3.25.8): seed the per-data-object expectations from the
|
||||
// captured FCI - value-exact at first, relaxed in the editor.
|
||||
if (rule.fciMode === 'custom') {
|
||||
const dos = fcpDosFromFci(rule.fciHex);
|
||||
if (dos.length) rule.fciDos = dos;
|
||||
}
|
||||
// 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).
|
||||
@@ -19799,6 +19824,7 @@ function profilerRenderRule(i, r) {
|
||||
'<option value="type"' + (fciMode === 'type' ? ' selected' : '') + '>' + esc(t('Filetype only (FCP)')) + '</option>' +
|
||||
'<option value="type_size"' + (fciMode === 'type_size' ? ' selected' : '') + '>' + esc(t('Filetype + size (FCP)')) + '</option>' +
|
||||
'<option value="exact"' + (fciMode === 'exact' ? ' selected' : '') + '>' + esc(t('Exact FCI')) + '</option>' +
|
||||
'<option value="custom"' + (fciMode === 'custom' ? ' selected' : '') + '>' + esc(t('Custom (per data object)')) + '</option>' +
|
||||
'</select></div>';
|
||||
html += '<div class="flex-1"><label class="block text-xs text-gray-600 dark:text-slate-400 mb-0.5">' + esc(t('File type')) + '</label>' +
|
||||
'<select onchange="profilerUpdateRuleFileType(' + i + ',this.value||null)" class="w-full border border-gray-300 dark:border-slate-600 text-xs rounded px-2 py-1 dark:bg-slate-800">' + ftOpts + '</select></div>';
|
||||
@@ -19813,7 +19839,7 @@ function profilerRenderRule(i, r) {
|
||||
'<input type="number" value="' + (r.numRecords === null ? '' : r.numRecords) + '" oninput="profilerUpdateRule(' + i + ',\'numRecords\',this.value===\'\'?null:parseInt(this.value))" class="w-full font-mono text-xs border border-gray-300 dark:border-slate-600 rounded px-2 py-1 dark:bg-slate-800" placeholder="—"></div>';
|
||||
}
|
||||
html += '</div>';
|
||||
if (fciMode === 'exact') {
|
||||
if (fciMode === 'exact' || fciMode === 'custom') {
|
||||
html += '<div class="mb-2 flex gap-3 items-start">';
|
||||
html += '<div class="flex-1 min-w-0"><label class="block text-xs text-gray-600 dark:text-slate-400 mb-0.5">' + esc(t('FCI hex (raw SELECT response)')) + '</label>' +
|
||||
'<textarea oninput="profilerFciInput(' + i + ',this.value)" rows="4" class="w-full font-mono text-xs border border-gray-300 dark:border-slate-600 rounded px-2 py-1 dark:bg-slate-800" placeholder="62...">' + escHtml(r.fciHex || '') + '</textarea></div>';
|
||||
@@ -19821,6 +19847,9 @@ function profilerRenderRule(i, r) {
|
||||
'<div id="profiler-fci-preview-' + i + '" class="text-xs font-mono leading-relaxed text-gray-600 dark:text-slate-400 break-all">' + profilerFciPreviewItems(r.fciHex || '') + '</div></div>';
|
||||
html += '</div>';
|
||||
}
|
||||
if (fciMode === 'custom') {
|
||||
html += profilerRenderFciDosEditor(i, r);
|
||||
}
|
||||
if (r.fileType !== 'df') {
|
||||
html += profilerRenderContentEditor(i, r);
|
||||
}
|
||||
@@ -19829,6 +19858,114 @@ function profilerRenderRule(i, r) {
|
||||
return html;
|
||||
}
|
||||
|
||||
// Custom-FCI editor (v3.25.8): a per-data-object expectation list. Profiles
|
||||
// created from a card or a snapshot with fciMode 'custom' are seeded with the
|
||||
// captured FCI's objects (all 'value', the captured bytes); a rule with a
|
||||
// captured FCI but an empty list can be filled from it here. Only the listed
|
||||
// objects are checked; `present`/`absent` demand or forbid one, and a value
|
||||
// without '?' matches as a prefix ('?' masks a nibble).
|
||||
function profilerRenderFciDosEditor(i, r) {
|
||||
const dos = r.fciDos || [];
|
||||
let html = '<div class="mb-2 p-2 border border-gray-200 dark:border-slate-600 rounded">';
|
||||
html += '<div class="flex flex-wrap items-center gap-2 mb-1">';
|
||||
html += '<span class="text-xs font-semibold text-gray-500 dark:text-slate-400">' + esc(t('Data objects (FCI)')) + (dos.length ? ' (' + dos.length + ')' : '') + '</span>';
|
||||
if (!dos.length && r.fciHex) {
|
||||
html += '<button onclick="profilerFciDosLoad(' + i + ')" class="text-xs px-2 py-0.5 rounded bg-gray-200 dark:bg-slate-700 hover:bg-gray-300 dark:hover:bg-slate-600 text-gray-700 dark:text-slate-300">' + esc(t('load from the captured FCI')) + '</button>';
|
||||
}
|
||||
html += '<label class="ml-auto text-xs flex items-center gap-1">' + esc(t('Extra data objects')) +
|
||||
' <select onchange="profilerSetFciExtra(' + i + ',this.value)" class="text-xs border border-gray-300 dark:border-slate-600 rounded px-1 py-0.5 dark:bg-slate-800">' +
|
||||
'<option value="ignore"' + (r.fciExtra !== 'fail' ? ' selected' : '') + '>' + esc(t('ignore')) + '</option>' +
|
||||
'<option value="fail"' + (r.fciExtra === 'fail' ? ' selected' : '') + '>' + esc(t('fail')) + '</option></select></label>';
|
||||
html += '</div>';
|
||||
html += '<datalist id="profiler-fci-do-keys-' + i + '">' +
|
||||
FCP_DO_KEYS.map(k => '<option value="' + k + '">' + esc(fcpDoName(k)) + '</option>').join('') + '</datalist>';
|
||||
for (let di = 0; di < dos.length; di++) html += profilerFciDoRowHtml(i, di, dos[di]);
|
||||
if (!dos.length) {
|
||||
html += '<div class="mb-1 text-xs text-gray-400 dark:text-slate-500">' + esc(t('No data objects listed - the FCI is not checked.')) + '</div>';
|
||||
}
|
||||
html += '<button onclick="profilerFciDosAdd(' + i + ')" class="text-xs px-2 py-0.5 rounded bg-gray-200 dark:bg-slate-700 hover:bg-gray-300 dark:hover:bg-slate-600 text-gray-700 dark:text-slate-300">+ ' + esc(t('data object')) + '</button>';
|
||||
html += '</div>';
|
||||
return html;
|
||||
}
|
||||
|
||||
// One expectation row: the tag (with the known keys suggested), the mode, and
|
||||
// the value for the 'value' mode.
|
||||
function profilerFciDoRowHtml(i, di, d) {
|
||||
const mode = (d.mode === 'present' || d.mode === 'absent') ? d.mode : 'value';
|
||||
let html = '<div class="flex flex-wrap items-center gap-1.5 mb-1 pl-2 border-l-2 border-gray-200 dark:border-slate-700">';
|
||||
html += '<input list="profiler-fci-do-keys-' + i + '" value="' + escHtml(d.key || '') + '" oninput="profilerFciDoSetKey(' + i + ',' + di + ',this.value)" class="w-20 font-mono text-xs border border-gray-300 dark:border-slate-600 rounded px-1.5 py-0.5 dark:bg-slate-800" placeholder="82">';
|
||||
html += '<select onchange="profilerFciDoSetMode(' + i + ',' + di + ',this.value)" class="text-xs border border-gray-300 dark:border-slate-600 rounded px-1 py-0.5 dark:bg-slate-800">' +
|
||||
'<option value="value"' + (mode === 'value' ? ' selected' : '') + '>' + esc(t('value')) + '</option>' +
|
||||
'<option value="present"' + (mode === 'present' ? ' selected' : '') + '>' + esc(t('present')) + '</option>' +
|
||||
'<option value="absent"' + (mode === 'absent' ? ' selected' : '') + '>' + esc(t('absent')) + '</option></select>';
|
||||
if (mode === 'value') {
|
||||
html += '<input value="' + escHtml(d.value || '') + '" oninput="profilerFciDoSetValue(' + i + ',' + di + ',this.value)" class="w-44 font-mono text-xs border border-gray-300 dark:border-slate-600 rounded px-1.5 py-0.5 dark:bg-slate-800" placeholder="' + esc(t('hex; no ? = prefix, ? = any nibble')) + '">';
|
||||
}
|
||||
html += '<span id="profiler-fci-do-name-' + i + '-' + di + '" class="text-xs text-gray-400 dark:text-slate-500 truncate">' + esc(fcpDoName(d.key)) + '</span>';
|
||||
html += '<button onclick="profilerFciDoRemove(' + i + ',' + di + ')" class="ml-auto text-red-400 hover:text-red-600 text-xs px-1">\u2715</button>';
|
||||
html += '</div>';
|
||||
return html;
|
||||
}
|
||||
|
||||
function profilerFciDosAdd(i) {
|
||||
if (!profilerDraft) return;
|
||||
const r = profilerDraft.rules[i];
|
||||
if (!r.fciDos) r.fciDos = [];
|
||||
r.fciDos.push({ key: '82', mode: 'value', value: '' });
|
||||
profilerRenderEditor();
|
||||
}
|
||||
|
||||
function profilerFciDoRemove(i, di) {
|
||||
if (!profilerDraft) return;
|
||||
const r = profilerDraft.rules[i];
|
||||
if (!r.fciDos) return;
|
||||
r.fciDos.splice(di, 1);
|
||||
if (!r.fciDos.length) delete r.fciDos;
|
||||
profilerRenderEditor();
|
||||
}
|
||||
|
||||
function profilerFciDoSetKey(i, di, v) {
|
||||
if (!profilerDraft) return;
|
||||
const r = profilerDraft.rules[i];
|
||||
if (!r.fciDos || !r.fciDos[di]) return;
|
||||
r.fciDos[di].key = String(v || '').trim().toUpperCase();
|
||||
const el = document.getElementById('profiler-fci-do-name-' + i + '-' + di);
|
||||
if (el) el.textContent = fcpDoName(r.fciDos[di].key);
|
||||
}
|
||||
|
||||
function profilerFciDoSetMode(i, di, v) {
|
||||
if (!profilerDraft) return;
|
||||
const r = profilerDraft.rules[i];
|
||||
if (!r.fciDos || !r.fciDos[di]) return;
|
||||
r.fciDos[di].mode = (v === 'present' || v === 'absent') ? v : 'value';
|
||||
profilerRenderEditor();
|
||||
}
|
||||
|
||||
function profilerFciDoSetValue(i, di, v) {
|
||||
if (!profilerDraft) return;
|
||||
const r = profilerDraft.rules[i];
|
||||
if (!r.fciDos || !r.fciDos[di]) return;
|
||||
r.fciDos[di].value = String(v || '').trim().toUpperCase();
|
||||
}
|
||||
|
||||
function profilerSetFciExtra(i, v) {
|
||||
if (!profilerDraft) return;
|
||||
const r = profilerDraft.rules[i];
|
||||
if (v === 'fail') r.fciExtra = 'fail';
|
||||
else delete r.fciExtra;
|
||||
}
|
||||
|
||||
// Fill an empty list from the rule's own captured FCI (never the card - the
|
||||
// rows come from what the profile already stores; v3.25.8).
|
||||
function profilerFciDosLoad(i) {
|
||||
if (!profilerDraft) return;
|
||||
const r = profilerDraft.rules[i];
|
||||
const dos = fcpDosFromFci(r.fciHex);
|
||||
if (!dos.length) return;
|
||||
r.fciDos = dos;
|
||||
profilerRenderEditor();
|
||||
}
|
||||
|
||||
// The expanded access-rule blocks of the profile editor (transient UI state,
|
||||
// kept across the editor re-renders; None hides the block entirely; a fresh
|
||||
// draft resets it and a deleted rule shifts the keys).
|
||||
@@ -20457,6 +20594,32 @@ async function profilerRunRule(r, source) {
|
||||
if (!ok) res.status = 'fail';
|
||||
res.checks.push({ label: 'fci', expected: r.fciHex, actual: sel.fci_hex || '', ok: ok, ignoredRef: ignoredRef });
|
||||
}
|
||||
// Custom FCI (v3.25.8): a per-data-object expectation list - only the
|
||||
// listed objects are checked (not listed = not checked); `value` reuses
|
||||
// the contents vocabulary (no '?' = prefix, '?' = nibble mask), `present`
|
||||
// demands the object, `absent` forbids it, and `fciExtra: 'fail'` also
|
||||
// fails on any object the card carries although it is not listed
|
||||
// (order-insensitive strictness).
|
||||
if (fciMode === 'custom' && (r.fciDos || []).length) {
|
||||
const dec = fcpDecode(sel.fci_hex || '');
|
||||
const dos = [];
|
||||
for (const d of r.fciDos) {
|
||||
const hits = dec.items.filter(it => it.key === d.key);
|
||||
let ok;
|
||||
if (d.mode === 'absent') ok = hits.length === 0;
|
||||
else if (d.mode === 'present') ok = hits.length > 0;
|
||||
else ok = hits.some(it => profilerMatch('mask', d.value || '', it.value));
|
||||
dos.push({ key: d.key, name: fcpDoName(d.key), mode: d.mode, value: d.value || '',
|
||||
ok: ok, actual: hits.map(it => it.value) });
|
||||
}
|
||||
const listed = {};
|
||||
r.fciDos.forEach(d => { listed[d.key] = true; });
|
||||
const extra = r.fciExtra === 'fail' ? dec.items.filter(it => !listed[it.key]) : [];
|
||||
const ok = dos.every(d => d.ok) && !extra.length && !dec.error;
|
||||
if (!ok) res.status = 'fail';
|
||||
res.checks.push({ label: 'fci', mode: 'custom', dos: dos, extra: extra,
|
||||
parseError: dec.error || '', expected: r.fciDos, actual: dec.items, ok: ok });
|
||||
}
|
||||
// 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.
|
||||
@@ -20556,13 +20719,13 @@ 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: [], accessRules: [] };
|
||||
const groups = { filetype: [], size: [], records: [], 'Exact FCI': [], 'FCI (custom)': [], contents: [], accessRules: [] };
|
||||
for (const c of (res.checks || [])) {
|
||||
const l = c.label;
|
||||
if (l === 'fileType') groups.filetype.push(c);
|
||||
else if (l === 'fileSize') groups.size.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[c.mode === 'custom' ? 'FCI (custom)' : 'Exact FCI'].push(c);
|
||||
else if (l === 'content' || l.startsWith('content.')) groups.contents.push(c);
|
||||
else if (l === 'accessRules') groups.accessRules.push(c);
|
||||
}
|
||||
@@ -20574,6 +20737,7 @@ function profilerResultAspects(res) {
|
||||
if (groups.size.length) out.push({ key: 'size', ok: groups.size.every(c => c.ok) });
|
||||
if (groups.records.length) out.push({ key: 'records', ok: groups.records.every(c => c.ok) });
|
||||
}
|
||||
if (groups['FCI (custom)'].length) out.push({ key: 'FCI (custom)', ok: groups['FCI (custom)'].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;
|
||||
@@ -21178,6 +21342,32 @@ function fcpDecode(hex) {
|
||||
return { ok: !error && items.length > 0, template, items, error };
|
||||
}
|
||||
|
||||
// The data object keys the FCI decoder names (TS 102 221 11.1.1.4 + the
|
||||
// A5/C6 children) - the custom-FCI editor's tag suggestions (v3.25.8).
|
||||
const FCP_DO_KEYS = [
|
||||
'80', '81', '82', '83', '84', '85', '86', '87', '88', '8A', '8B', '8C',
|
||||
'8D', '8E', 'A0', 'A5', 'AB', 'C6',
|
||||
'A5/80', 'A5/81', 'A5/82', 'A5/83', 'A5/84', 'A5/85', 'A5/86', 'A5/87',
|
||||
'A5/88', 'A5/89', 'A5/8A', 'A5/8B', 'A5/C0', 'A5/C1', 'A5/C2',
|
||||
'C6/90', 'C6/95', 'C6/83',
|
||||
];
|
||||
|
||||
// A data object's display name ('' for an empty or unknown key).
|
||||
function fcpDoName(key) {
|
||||
const k = String(key || '').toUpperCase();
|
||||
if (!k) return '';
|
||||
const n = fcpDo(k, '').name || '';
|
||||
return /^Unknown TLV/.test(n) ? '' : n;
|
||||
}
|
||||
|
||||
// The decoded data objects of an FCI as custom-check rows: one 'value'
|
||||
// expectation per object, the captured bytes - profiles created from a card
|
||||
// or a snapshot with fciMode 'custom' are seeded with these (v3.25.8).
|
||||
function fcpDosFromFci(hex) {
|
||||
const dec = fcpDecode(hex || '');
|
||||
return dec.items.map(it => ({ key: it.key, mode: 'value', value: it.value }));
|
||||
}
|
||||
|
||||
function profilerLabelText(labels, which) {
|
||||
if (!labels || !labels[which]) return t(which);
|
||||
return labels.prefix ? t(which) + ' (' + labels[which] + ')' : labels[which];
|
||||
@@ -21320,6 +21510,38 @@ function profilerAccessRulesDiffHtml(c) {
|
||||
return html + '</div>';
|
||||
}
|
||||
|
||||
// The custom FCI's expectation rows in the report (v3.25.8): the same marks
|
||||
// as the access-rules diff - '−' a failed expectation, '+' an object the card
|
||||
// carries although fciExtra forbids it.
|
||||
function profilerFciDosDiffHtml(c) {
|
||||
let html = '<div class="mt-1 pl-2">';
|
||||
html += '<div class="text-xs text-red-600 font-mono">' + esc(t('FCI (custom)')) + '</div>';
|
||||
if (c.parseError) {
|
||||
html += '<div class="text-xs text-yellow-600 mt-0.5">' +
|
||||
esc(t('the FCI could not be parsed') + ': ' + c.parseError) + '</div>';
|
||||
}
|
||||
const line = (mark, cls, text) => '<div class="text-xs font-mono mt-0.5 ' + cls + '">' + esc(mark + ' ' + text) + '</div>';
|
||||
for (const d of (c.dos || [])) {
|
||||
const desc = d.key + (d.name ? ' (' + d.name + ')' : '');
|
||||
const mark = d.ok ? '\u2713' : '\u2212';
|
||||
const cls = d.ok ? 'text-gray-500 dark:text-slate-400' : 'text-red-600';
|
||||
if (d.mode === 'absent') {
|
||||
html += line(mark, cls, desc + ': ' + t('must be absent') +
|
||||
(d.ok ? '' : ' \u2014 ' + t('the card has') + ': ' + d.actual.join(', ')));
|
||||
} else if (d.mode === 'present') {
|
||||
html += line(mark, cls, desc + ': ' + t('must be present') +
|
||||
(d.ok ? '' : ' \u2014 ' + t('not found')));
|
||||
} else {
|
||||
html += line(mark, cls, desc + ' = ' + d.value +
|
||||
(d.ok ? '' : ' \u2014 ' + t('the card has') + ': ' + (d.actual.join(', ') || t('not found'))));
|
||||
}
|
||||
}
|
||||
for (const it of (c.extra || [])) {
|
||||
html += line('+', 'text-red-600', it.key + (it.name ? ' (' + it.name + ')' : '') + ' = ' + it.value);
|
||||
}
|
||||
return html + '</div>';
|
||||
}
|
||||
|
||||
function profilerRenderReport(results, labels) {
|
||||
const expLabel = profilerLabelText(labels, 'expected');
|
||||
const actLabel = profilerLabelText(labels, 'actual');
|
||||
@@ -21345,6 +21567,8 @@ function profilerRenderReport(results, labels) {
|
||||
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 (c.label === 'fci' && c.mode === 'custom') {
|
||||
html += profilerFciDosDiffHtml(c);
|
||||
} else if (profilerRawDataCheck(c)) {
|
||||
const isContent = c.label === 'content' || /^content\.rec\d+$/.test(c.label);
|
||||
const dd = isContent && typeof efContentDiff === 'function'
|
||||
@@ -23503,6 +23727,22 @@ const LANG_RU = {
|
||||
'Filetype only (FCP)': 'Только тип файла (FCP)',
|
||||
'Filetype + size (FCP)': 'Тип файла + размер (FCP)',
|
||||
'Exact FCI': 'Полный FCI',
|
||||
'Custom (per data object)': 'По объектам данных',
|
||||
'Data objects (FCI)': 'Объекты данных (FCI)',
|
||||
'data object': 'объект данных',
|
||||
'Extra data objects': 'Лишние объекты данных',
|
||||
'ignore': 'игнорировать',
|
||||
'fail': 'не допускать',
|
||||
'load from the captured FCI': 'заполнить из записанного FCI',
|
||||
'No data objects listed - the FCI is not checked.': 'Объекты данных не перечислены — FCI не проверяется.',
|
||||
'value': 'значение',
|
||||
'hex; no ? = prefix, ? = any nibble': 'hex; без ? — префикс, ? — любой полубайт',
|
||||
'FCI (custom)': 'FCI (по объектам)',
|
||||
'the FCI could not be parsed': 'FCI не удалось разобрать',
|
||||
'must be present': 'должен присутствовать',
|
||||
'must be absent': 'должен отсутствовать',
|
||||
'the card has': 'на карте',
|
||||
'not found': 'не найдено',
|
||||
'FCI hex (raw SELECT response)': 'FCI hex (сырой ответ SELECT)',
|
||||
'FCI parameters': 'Параметры FCI',
|
||||
'Parameter': 'Параметр',
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
const CACHE = 'simple-v431';
|
||||
const CACHE = 'simple-v432';
|
||||
const URLS = [
|
||||
'index.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',
|
||||
'fcpArrRef', 'fcpSeidName', 'fcpArrRules', 'fcpArrRefFromFci', 'fcpWithoutArrRef', 'fcpArrSplitTop', 'fcpArrKeyRefFromName', 'fcpArrKeyRefOptions', 'fcpArrScParse', 'fcpArrScEncode', 'fcpArrRuleParse', 'fcpArrRuleEncode', 'fcpArrRuleOps', 'fcpArrRuleOpCovered', 'fcpArrRuleSatisfied', 'fcpArrRuleToggleBit', 'fcpArrRuleSetKind', 'fcpArrRuleSetSc', 'fcpArrRuleSetUq', 'fcpArrRuleAddSc', 'fcpArrRuleRemoveSc', 'profilerRenderAccessRulesEditor', 'profilerArrRuleHtml', 'profilerArrScSelect', 'profilerArrUqSelect', 'profilerArrToggleOpen', 'profilerArrShiftOpen', 'profilerAccessRulesDiffHtml', 'profilerSnapshotArr', 'efBytes', 'efHex', 'efTlv', 'efDecArr', 'efArrAmLabel', 'efArrSc', 'efArrAmDo', 'efFidFromPath',
|
||||
'fcpArrRef', 'fcpSeidName', 'fcpArrRules', 'fcpArrRefFromFci', 'fcpWithoutArrRef', 'fcpArrSplitTop', 'fcpArrKeyRefFromName', 'fcpArrKeyRefOptions', 'fcpArrScParse', 'fcpArrScEncode', 'fcpArrRuleParse', 'fcpArrRuleEncode', 'fcpArrRuleOps', 'fcpArrRuleOpCovered', 'fcpArrRuleSatisfied', 'fcpArrRuleToggleBit', 'fcpArrRuleSetKind', 'fcpArrRuleSetSc', 'fcpArrRuleSetUq', 'fcpArrRuleAddSc', 'fcpArrRuleRemoveSc', 'profilerRenderAccessRulesEditor', 'profilerArrRuleHtml', 'profilerArrScSelect', 'profilerArrUqSelect', 'profilerArrToggleOpen', 'profilerArrShiftOpen', 'profilerAccessRulesDiffHtml', 'fcpDosFromFci', 'fcpDoName', 'profilerRenderFciDosEditor', 'profilerFciDoRowHtml', 'profilerFciDosDiffHtml', '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';
|
||||
@@ -42,6 +42,7 @@ code += "var _profilerArrOpen = {};\n";
|
||||
code += 'globalThis.navRecord = function() {};\nglobalThis.navReplace = function() {};\n';
|
||||
code += html.match(/const PROFILER_MASK_PREFIX4_FIDS = \{[\s\S]*?\n\};/)[0] + '\n';
|
||||
code += html.match(/const FCP_ARR_AM_BITS = \[[\s\S]*?\n\];/)[0] + '\n';
|
||||
code += html.match(/const FCP_DO_KEYS = \[[\s\S]*?\n\];/)[0] + '\n';
|
||||
eval(code);
|
||||
|
||||
test('profilerNormHex uppercases and strips non-hex', () => {
|
||||
@@ -1309,6 +1310,168 @@ test('the access-rules report block marks missing and extra lines', () => {
|
||||
delete global.t;
|
||||
});
|
||||
|
||||
test('the custom FCI check matches per data object', async () => {
|
||||
const FCI = '621A82054221000F0583026F4F8A01058B032F06028802004B8801B0';
|
||||
const src = {
|
||||
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 }),
|
||||
};
|
||||
const rule = dos => ({ type: 'file', path: 'MF/7F20/6F07', name: 'EF.SAMPLE',
|
||||
fileType: 'linear_fixed', recordLen: 6, numRecords: 2, fciMode: 'custom',
|
||||
fciHex: FCI, fciDos: dos });
|
||||
const run = dos => profilerRunRule(rule(dos), src);
|
||||
// exact value, the prefix form (the record number ignored) and the mask
|
||||
let r = await run([{ key: '82', mode: 'value', value: '4221000F05' },
|
||||
{ key: '8B', mode: 'value', value: '2F0602' }]);
|
||||
assert.strictEqual(r.status, 'pass', JSON.stringify(r.checks));
|
||||
const c0 = r.checks.find(c => c.label === 'fci');
|
||||
assert.strictEqual(c0.mode, 'custom');
|
||||
assert.deepStrictEqual(c0.dos.map(d => d.ok), [true, true]);
|
||||
// the type/size/record checks still run in the custom mode
|
||||
assert.ok(r.checks.some(c => c.label === 'recordLen' && c.ok), JSON.stringify(r.checks));
|
||||
r = await run([{ key: '8B', mode: 'value', value: '2F06' }]);
|
||||
assert.strictEqual(r.status, 'pass', JSON.stringify(r.checks));
|
||||
r = await run([{ key: '8B', mode: 'value', value: '2F06??' }]);
|
||||
assert.strictEqual(r.status, 'pass', JSON.stringify(r.checks));
|
||||
r = await run([{ key: '8B', mode: 'value', value: '2F07' }]);
|
||||
assert.strictEqual(r.status, 'fail');
|
||||
assert.deepStrictEqual(r.checks.find(c => c.label === 'fci').dos[0].actual, ['2F0602']);
|
||||
// present / absent
|
||||
r = await run([{ key: '83', mode: 'present' }, { key: 'A0', mode: 'absent' }]);
|
||||
assert.strictEqual(r.status, 'pass', JSON.stringify(r.checks));
|
||||
r = await run([{ key: 'A0', mode: 'present' }]);
|
||||
assert.strictEqual(r.status, 'fail');
|
||||
assert.deepStrictEqual(r.checks.find(c => c.label === 'fci').dos[0].actual, []);
|
||||
r = await run([{ key: '83', mode: 'absent' }]);
|
||||
assert.strictEqual(r.status, 'fail');
|
||||
// a duplicated tag: any occurrence satisfies, 'absent' needs none
|
||||
r = await run([{ key: '88', mode: 'value', value: '004B' }]);
|
||||
assert.strictEqual(r.status, 'pass', JSON.stringify(r.checks));
|
||||
r = await run([{ key: '88', mode: 'value', value: 'FFFF' }]);
|
||||
assert.strictEqual(r.status, 'fail');
|
||||
r = await run([{ key: '88', mode: 'absent' }]);
|
||||
assert.strictEqual(r.status, 'fail');
|
||||
// extras: ignored by default, failing with fciExtra
|
||||
r = await run([{ key: '82', mode: 'present' }]);
|
||||
assert.strictEqual(r.status, 'pass', JSON.stringify(r.checks));
|
||||
r = await profilerRunRule({ ...rule([{ key: '82', mode: 'present' }]), fciExtra: 'fail' }, src);
|
||||
assert.strictEqual(r.status, 'fail');
|
||||
assert.ok(r.checks.find(c => c.label === 'fci').extra.some(it => it.key === '8B'));
|
||||
// an empty list checks nothing
|
||||
r = await run([]);
|
||||
assert.ok(!r.checks.some(c => c.label === 'fci'), JSON.stringify(r.checks));
|
||||
// a nested A5 child is addressable
|
||||
const FCI_A5 = '621382054221000F0583026F4F8A0105A503800101';
|
||||
const a5src = { ...src, select: async () => ({ name: 'EF.SAMPLE', file_type: 'linear_fixed',
|
||||
record_len: 6, num_of_rec: 2, fci_hex: FCI_A5, exists: true }) };
|
||||
r = await profilerRunRule({ ...rule([{ key: 'A5/80', mode: 'value', value: '01' }]), fciHex: FCI_A5 }, a5src);
|
||||
assert.strictEqual(r.status, 'pass', JSON.stringify(r.checks));
|
||||
// an unparseable FCI fails with the parse error
|
||||
const bad = { ...src, select: async () => ({ name: 'EF.SAMPLE', file_type: 'linear_fixed',
|
||||
record_len: 6, num_of_rec: 2, fci_hex: '62', exists: true }) };
|
||||
r = await profilerRunRule({ ...rule([{ key: '82', mode: 'present' }]), fciHex: '62' }, bad);
|
||||
assert.strictEqual(r.status, 'fail');
|
||||
assert.ok(r.checks.find(c => c.label === 'fci').parseError, JSON.stringify(r.checks));
|
||||
});
|
||||
|
||||
test('a custom-mode profile is seeded with the captured FCI objects', () => {
|
||||
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, 'custom', null, arrs);
|
||||
assert.ok(Array.isArray(rule.fciDos) && rule.fciDos.length >= 5, JSON.stringify(rule.fciDos));
|
||||
assert.deepStrictEqual(rule.fciDos[0], { key: '82', mode: 'value', value: '4221000F05' });
|
||||
assert.ok(rule.fciDos.some(d => d.key === '8B' && d.value === '2F0602'));
|
||||
// the other modes do not seed
|
||||
const exact = profilerBuildFileRuleFromSnapshot(snap.files[1], new Set(), null, 'exact', null, arrs);
|
||||
assert.strictEqual(exact.fciDos, undefined);
|
||||
});
|
||||
|
||||
test('the custom-FCI editor renders the data object rows', () => {
|
||||
global.t = s => s;
|
||||
const r = { fciMode: 'custom', fciHex: '621A82054221000F0583026F4F8A01058B032F06028802004B8801B0',
|
||||
fciDos: [{ key: '82', mode: 'value', value: '4221000F05' },
|
||||
{ key: '88', mode: 'present' }, { key: 'A0', mode: 'absent' }] };
|
||||
const out = profilerRenderFciDosEditor(3, r);
|
||||
assert.ok(out.includes('Data objects (FCI) (3)'), out);
|
||||
assert.ok(out.includes('profilerFciDoSetKey(3,0,this.value)'), out);
|
||||
assert.ok(out.includes('<option value="present" selected>present</option>'), out);
|
||||
assert.ok(out.includes('<option value="absent" selected>absent</option>'), out);
|
||||
assert.ok(out.includes('value="4221000F05"'), out);
|
||||
assert.ok(out.includes('profilerSetFciExtra(3,this.value)'), out);
|
||||
assert.ok(out.includes('<option value="ignore" selected>ignore</option>'), out);
|
||||
assert.ok(out.includes('profilerFciDosAdd(3)'), out);
|
||||
assert.ok(out.includes('profilerFciDoRemove(3,2)'), out);
|
||||
// no value input on the present/absent rows
|
||||
assert.ok(!out.includes('profilerFciDoSetValue(3,1'), out);
|
||||
// the known-key datalist carries the names
|
||||
assert.ok(out.includes('<datalist id="profiler-fci-do-keys-3">'), out);
|
||||
assert.ok(out.includes('<option value="8B">Security attributes (referenced)</option>'), out);
|
||||
const fail = profilerRenderFciDosEditor(3, { ...r, fciExtra: 'fail' });
|
||||
assert.ok(fail.includes('<option value="fail" selected>fail</option>'), fail);
|
||||
// the load link only with a captured FCI and an empty list
|
||||
const empty = profilerRenderFciDosEditor(3, { ...r, fciDos: [] });
|
||||
assert.ok(empty.includes('profilerFciDosLoad(3)'), empty);
|
||||
assert.ok(empty.includes('No data objects listed'), empty);
|
||||
const noHex = profilerRenderFciDosEditor(3, { ...r, fciDos: [], fciHex: null });
|
||||
assert.ok(!noHex.includes('profilerFciDosLoad'), noHex);
|
||||
// the mode selector offers the custom mode (editor + scan dialog)
|
||||
assert.ok(html.includes('value="custom"\' + (fciMode === \'custom\''), 'the editor option');
|
||||
assert.ok(html.includes('data-l10n="Custom (per data object)"'), 'the scan dialog option');
|
||||
delete global.t;
|
||||
});
|
||||
|
||||
test('the custom FCI report block marks the expectations', () => {
|
||||
global.t = s => s;
|
||||
const out = profilerFciDosDiffHtml({ label: 'fci', mode: 'custom', ok: false, parseError: '',
|
||||
dos: [
|
||||
{ key: '82', name: 'File descriptor', mode: 'value', value: '4221000F05', ok: true, actual: ['4221000F05'] },
|
||||
{ key: '88', name: 'Short file identifier', mode: 'present', ok: false, actual: [] },
|
||||
{ key: 'A0', name: '', mode: 'absent', ok: false, actual: ['01'] },
|
||||
],
|
||||
extra: [{ key: '8B', name: 'Security attributes (referenced)', value: '2F0602' }] });
|
||||
assert.ok(out.includes('\u2713 82 (File descriptor) = 4221000F05'), out);
|
||||
assert.ok(out.includes('\u2212 88 (Short file identifier): must be present \u2014 not found'), out);
|
||||
assert.ok(out.includes('\u2212 A0: must be absent \u2014 the card has: 01'), out);
|
||||
assert.ok(out.includes('+ 8B (Security attributes (referenced)) = 2F0602'), out);
|
||||
const perr = profilerFciDosDiffHtml({ label: 'fci', mode: 'custom', ok: false,
|
||||
parseError: 'Truncated at offset 1', dos: [], extra: [] });
|
||||
assert.ok(perr.includes('the FCI could not be parsed: Truncated at offset 1'), perr);
|
||||
delete global.t;
|
||||
});
|
||||
|
||||
test('the custom FCI aspects stay separate from the exact one', () => {
|
||||
const aspects = profilerResultAspects({ checks: [
|
||||
{ label: 'fci', mode: 'custom', ok: false, dos: [], extra: [] },
|
||||
{ label: 'recordLen', ok: true },
|
||||
]});
|
||||
assert.ok(aspects.some(a => a.key === 'FCI (custom)' && !a.ok), JSON.stringify(aspects));
|
||||
assert.ok(aspects.some(a => a.key === 'records'), JSON.stringify(aspects));
|
||||
});
|
||||
|
||||
test('profile validation accepts the custom FCI fields', () => {
|
||||
const base = { name: 'P', rules: [{ type: 'file', path: 'MF/7F20/6F07', fciMode: 'custom',
|
||||
fciDos: [{ key: '82', mode: 'value', value: '4221000F05' },
|
||||
{ key: '88', mode: 'present' }, { key: 'A0', mode: 'absent' },
|
||||
{ key: 'A5/80', mode: 'value', value: '01' }] }] };
|
||||
const withRule = f => ({ ...base, rules: [{ ...base.rules[0], ...f }] });
|
||||
assert.strictEqual(profilerValidateProfile(base), null);
|
||||
assert.strictEqual(profilerValidateProfile(withRule({ fciExtra: 'fail' })), null);
|
||||
assert.strictEqual(profilerValidateProfile(withRule({ fciExtra: 'ignore' })), null);
|
||||
assert.ok(profilerValidateProfile(withRule({ fciMode: 'bogus' })));
|
||||
assert.ok(profilerValidateProfile(withRule({ fciDos: 'nope' })));
|
||||
assert.ok(profilerValidateProfile(withRule({ fciDos: [{ key: 'ZZ', mode: 'value', value: '01' }] })));
|
||||
assert.ok(profilerValidateProfile(withRule({ fciDos: [{ key: '82', mode: 'nope' }] })));
|
||||
assert.ok(profilerValidateProfile(withRule({ fciDos: [{ key: '82', mode: 'value', value: 'Z' }] })));
|
||||
assert.ok(profilerValidateProfile(withRule({ fciExtra: 'nope' })));
|
||||
});
|
||||
|
||||
test('pysimFsInfoHtml embeds the symbolic name in the FCI border', () => {
|
||||
global.t = s => s;
|
||||
const hex = '6212800200098202412183026F078A0105880110';
|
||||
|
||||
Reference in New Issue
Block a user