Compare commits
4 Commits
fb6f80ef1d
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 0370fa58ad | |||
| 2213e25385 | |||
| bf8abbb0fd | |||
| a9a4d03b09 |
@@ -504,7 +504,7 @@ Type a command name in the **pySim command line** input. Usage hints appear as a
|
||||
|
||||
Verifies that a card matches a named **profile** — an ordered set of rules describing the expected file system and, optionally, file contents. Profiles are stored in `localStorage`.
|
||||
|
||||
- **New profile** creates an empty ruleset; **Profile from card** scans the equipped card and generates one rule per existing file; **Import profile** loads a ruleset from JSON (the name is stored inside the file).
|
||||
- **New profile** creates an empty ruleset; **Profile from card** scans the equipped card and generates one rule per existing file; **Profile from snapshot** generates the same ruleset from a saved snapshot (same ignore/mask/FCP-FCI options, no card reader, name prefilled from the snapshot); **Import profile** loads a ruleset from JSON (the name is stored inside the file).
|
||||
- Each profile row has **Check card ▶** (run against the equipped card), **Check card snapshot** (run offline against a saved snapshot), **Edit**, **Export**, and **Delete**.
|
||||
|
||||
A filesystem rule is defined by:
|
||||
@@ -514,7 +514,7 @@ A filesystem rule is defined by:
|
||||
- **File attributes** — file type, size, record length and record count, taken from the FCP template (any may be left unset).
|
||||
- **Check contents** (optional) — **Exact** hex equality, or **Mask** where `?` is a per-nibble wildcard (a mask with no `?` is a prefix match, e.g. `0891` for the IMSI MCC/MNC). Record files store a per-record list.
|
||||
|
||||
The check report marks each verified aspect (e.g. *filetype ✓, size ✗, contents ✓*), lists mismatches as read-only monospace expected/actual fields aligned in one column, and shows a decoded per-parameter FCI comparison for FCI mismatches. Corrupt FCI data shows whatever decoded before the faulty part plus an explicit decode-failure note; record mismatches list the *matching records*. **Only mismatches** in the results header hides all passing files and keeps failures and errors only.
|
||||
The check report marks each verified aspect (e.g. *filetype ✓, size ✗, contents ✓*), lists mismatches as read-only monospace expected/actual fields aligned in one column, and shows a decoded per-parameter FCI comparison for FCI mismatches. Corrupt FCI data shows whatever decoded before the faulty part plus an explicit decode-failure note; record mismatches list the *matching records*. In the report the mismatch fields and FCI comparison columns are labelled `expected (profile name)` and `actual (card ICCID)` for a live check, or `actual (snapshot name)` for a snapshot check; the results header reads `Profile verification results for: <profile> → <card ICCID>` (or `… → <snapshot name>`; snapshot comparison: `Snapshot comparison results: <master> → <checked>`). **Only mismatches** in the results header hides all passing files and keeps failures and errors only.
|
||||
|
||||
#### “Profile from card” scan options
|
||||
|
||||
|
||||
+2
-2
@@ -478,7 +478,7 @@ Delivery PoR (SPI2 `01`) проще — карта возвращает PoR на
|
||||
|
||||
Проверка соответствия карты именованному **профилю** — упорядоченному набору правил, описывающих ожидаемую файловую систему и (опционально) содержимое файлов. Профили хранятся в `localStorage`.
|
||||
|
||||
- **Новый профиль** создаёт пустой набор правил; **Профиль с карты** сканирует подключённую карту и создаёт по правилу на каждый существующий файл; **Импорт профиля** загружает набор из JSON (имя хранится внутри файла).
|
||||
- **Новый профиль** создаёт пустой набор правил; **Профиль с карты** сканирует подключённую карту и создаёт по правилу на каждый существующий файл; **Профиль из снимка** создаёт тот же набор правил из сохранённого снимка (те же опции игнорирования/масок/FCP-FCI, без картридера, имя подставляется из снимка); **Импорт профиля** загружает набор из JSON (имя хранится внутри файла).
|
||||
- В каждой строке профиля: **Проверить карту ▶** (на подключённой карте), **Проверить снимок карты** (offline по сохранённому снимку), **Редактировать**, **Экспорт** и **Удалить**.
|
||||
|
||||
Правило файловой системы задаётся:
|
||||
@@ -488,7 +488,7 @@ Delivery PoR (SPI2 `01`) проще — карта возвращает PoR на
|
||||
- **Атрибуты файла** — тип, размер, длина и число записей из шаблона FCP (любое можно не задавать).
|
||||
- **Проверка содержимого** (опционально) — **Exact** (точное равенство hex) или **Mask**, где `?` — пониббловый джокер (маска без `?` — префиксное совпадение, напр. `0891` для MCC/MNC IMSI). Для record-файлов хранится список записей.
|
||||
|
||||
Отчёт проверки помечает каждый аспект (напр. *тип файла ✓, размер ✗, содержимое ✓*), показывает расхождения как поля только для чтения (ожидаемое/фактическое в одной колонке) и декодированное сравнение параметров FCI для расхождений FCI. Повреждённые FCI показывают всё, что удалось декодировать, плюс явное сообщение об ошибке; для записей указываются *совпадающие записи*. Опция **«Только расхождения»** скрывает все совпавшие файлы, оставляя несовпадения и ошибки.
|
||||
Отчёт проверки помечает каждый аспект (напр. *тип файла ✓, размер ✗, содержимое ✓*), показывает расхождения как поля только для чтения (ожидаемое/фактическое в одной колонке) и декодированное сравнение параметров FCI для расхождений FCI. Повреждённые FCI показывают всё, что удалось декодировать, плюс явное сообщение об ошибке; для записей указываются *совпадающие записи*. В отчёте поля расхождений и колонки сравнения FCI подписаны `ожидалось (имя профиля)` и `фактически (ICCID карты)` для проверки карты либо `фактически (имя снимка)` для проверки снимка; в заголовке отчёта — `Результаты проверки профиля: <профиль> → <ICCID карты>` (или `… → <имя снимка>`; для сравнения снимков — `Результаты сравнения снимков: <эталон> → <проверяемый>`). Опция **«Только расхождения»** скрывает все совпавшие файлы, оставляя несовпадения и ошибки.
|
||||
|
||||
#### Опции сканирования «Профиль с карты»
|
||||
|
||||
|
||||
+1
-1
@@ -55,7 +55,7 @@ Returns server version for compatibility checking.
|
||||
|
||||
**Example response:**
|
||||
```json
|
||||
{"version": "2.1.1"}
|
||||
{"version": "2.1.2"}
|
||||
```
|
||||
|
||||
### `GET /api/status`
|
||||
|
||||
@@ -376,6 +376,7 @@
|
||||
<ul class="list-disc list-inside text-sm space-y-1 mb-3">
|
||||
<li><strong>Новый профиль</strong> — создаёт пустой набор правил, запросив имя.</li>
|
||||
<li><strong>Профиль с карты</strong> — сканирует подключённую карту и создаёт по одному правилу на каждый существующий файл (см. ниже), затем открывает редактор.</li>
|
||||
<li><strong>Профиль из снимка</strong> — выбирает сохранённый снимок карты и создаёт по правилу на каждый захваченный файл с теми же опциями сканирования (см. ниже), без картридера; имя профиля подставляется из имени снимка.</li>
|
||||
<li><strong>Импорт профиля</strong> — загружает набор правил из JSON-файла (имя хранится внутри JSON).</li>
|
||||
<li>В каждой строке профиля показаны имя и время создания, а также действия <strong>Проверить карту ▶</strong>, <strong>Проверить снимок карты</strong>, <strong>Редактировать</strong>, <strong>Экспорт</strong> (скачать JSON) и <strong>Удалить</strong>.</li>
|
||||
</ul>
|
||||
@@ -387,9 +388,9 @@
|
||||
<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, содержимое, данные записей) показываются как поля только для чтения с моноширинным шрифтом — ожидаемое над фактическим, в одной и той же колонке — для удобного сравнения; для расхождений FCI дополнительно показывается декодированное сравнение по параметрам (размер файла, дескриптор/структура, жизненный цикл, FID, SFI, проприетарные параметры…). Декодированный просмотр FCI также отображается рядом с полем FCI hex при редактировании правила. Если данные FCI повреждены, показывается всё, что удалось декодировать до места ошибки, вместе с явным сообщением об ошибке декодирования. Для record-файлов при расхождении содержимого добавляется пометка <em>совпадающие записи: 1-5, 7-10</em> со списком записей, которые совпали. Опция <strong>«Только расхождения»</strong> в заголовке отчёта скрывает все совпавшие файлы и оставляет только несовпадения и ошибки.</p>
|
||||
<p class="text-sm mb-3"><strong>Проверить карту</strong> выполняет каждое правило на подключённой карте и показывает строку прогресса и отчёт прохождения. Рядом с путём файла указывается, что именно проверялось (например, <em>тип файла и размер, содержимое</em> или <em>полный FCI</em>); если часть проверок прошла, а часть нет — каждый аспект помечается (<em>тип файла ✓, размер ✗, содержимое ✓</em>), а расхождения расписываются ниже. Несовпавшие сырые данные (FCI, содержимое, данные записей) показываются как поля только для чтения с моноширинным шрифтом — ожидаемое над фактическим, в одной и той же колонке — для удобного сравнения; для расхождений 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> включаются с той же проверкой существования.</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>
|
||||
|
||||
<h4 id="card-snapshots" class="font-medium mb-1">Снимки карт</h4>
|
||||
<p class="text-sm mb-2">Представление списка имеет две вкладки — <strong>«Профили»</strong> и <strong>«Снимки карт»</strong>. Снимок карты — неизменяемая фиксация файловой системы карты: для каждого существующего файла сохраняются путь, символьное имя, тип, размер (или длина/число записей), сырой FCI из ответа SELECT и содержимое, если файл читается (без списка игнорирования и без масок). ICCID декодируется из EF.ICCID и показывается рядом с именем снимка. При сканировании измеряется время каждой команды карты (SELECT, READ BINARY, READ RECORD) от отправки до ответа; снимок хранит min/сред/max по каждому типу команд и общее время сканирования, а в представлении эти значения показываются в сводке под заголовком, время select/read — для каждого файла и время чтения — для каждой записи. Время носит информационный характер и не используется при проверках и сравнении.</p>
|
||||
@@ -397,8 +398,8 @@
|
||||
<li><strong>Новый снимок</strong> — запрашивает имя и сканирует карту, затем возвращает к списку.</li>
|
||||
<li><strong>Импорт снимка</strong> — загружает снимок из JSON-файла.</li>
|
||||
<li>В каждой строке снимка — <strong>Открыть</strong>, <strong>Экспорт</strong> и <strong>Удалить</strong>. <strong>Открыть</strong> показывает все захваченные данные только для чтения (сырой FCI с декодированным FCI, содержимое); редактируется только имя снимка.</li>
|
||||
<li><strong>Проверить снимок карты</strong> в строке профиля выполняет правила профиля на выбранном из списка снимке, без картридера. Отчёт такой же, как при проверке карты; файлы, содержимое которых не было захвачено при сканировании, помечаются как непроверяемые ошибки.</li>
|
||||
<li><strong>Сравнить снимки</strong> сравнивает два снимка без картридера так же, как проверка профиля: выберите <em>эталонный</em> снимок и <em>снимок для проверки</em>, при необходимости включите маску первых 4 байт EF.IMSI/EF.ICCID (включена по умолчанию) и получите такой же отчёт; в этом отчёте поля расхождений и колонки сравнения FCI подписаны именами эталонного и проверяемого снимков вместо expected/actual. Файлы, которые есть только в проверяемом снимке, помечаются как лишние. «К списку» возвращает на вкладку «Снимки карт».</li>
|
||||
<li><strong>Проверить снимок карты</strong> в строке профиля выполняет правила профиля на выбранном из списка снимке, без картридера. Отчёт такой же, как при проверке карты: фактическая сторона подписана именем снимка (<em>фактически (имя снимка)</em>), а в заголовке — <em>Результаты проверки профиля: <профиль> → <имя снимка></em>; файлы, содержимое которых не было захвачено при сканировании, помечаются как непроверяемые ошибки.</li>
|
||||
<li><strong>Сравнить снимки</strong> сравнивает два снимка без картридера так же, как проверка профиля: выберите <em>эталонный</em> снимок и <em>снимок для проверки</em>, при необходимости включите маску первых 4 байт EF.IMSI/EF.ICCID (включена по умолчанию) и получите такой же отчёт; в этом отчёте заголовок — <em>Результаты сравнения снимков: <эталон> → <проверяемый></em>, а поля расхождений и колонки сравнения FCI подписаны именами эталонного и проверяемого снимков вместо expected/actual. Файлы, которые есть только в проверяемом снимке, помечаются как лишние. «К списку» возвращает на вкладку «Снимки карт».</li>
|
||||
</ul>
|
||||
|
||||
</section>
|
||||
|
||||
+5
-4
@@ -376,6 +376,7 @@
|
||||
<ul class="list-disc list-inside text-sm space-y-1 mb-3">
|
||||
<li><strong>New profile</strong> — creates an empty ruleset after prompting for a name.</li>
|
||||
<li><strong>Profile from card</strong> — scans the equipped card and generates one rule per existing file (see below), then opens the editor.</li>
|
||||
<li><strong>Profile from snapshot</strong> — picks a saved card snapshot and generates one rule per captured file using the same scan options (see below), without a card reader; the profile name is prefilled with the snapshot name.</li>
|
||||
<li><strong>Import profile</strong> — loads a ruleset from a JSON file (the name is stored inside the JSON).</li>
|
||||
<li>Each profile row shows its name and creation time, with <strong>Check card ▶</strong>, <strong>Check card snapshot</strong>, <strong>Edit</strong>, <strong>Export</strong> (download JSON), and <strong>Delete</strong> actions.</li>
|
||||
</ul>
|
||||
@@ -387,9 +388,9 @@
|
||||
<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; 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. 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; 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>
|
||||
<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.</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>
|
||||
|
||||
<h4 id="card-snapshots" class="font-medium mb-1">Card snapshots</h4>
|
||||
<p class="text-sm mb-2">The list view has two tabs — <strong>Profiles</strong> and <strong>Card snapshots</strong>. 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. 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>
|
||||
@@ -397,8 +398,8 @@
|
||||
<li><strong>New snapshot</strong> — asks for a name and scans the card, then returns to the list.</li>
|
||||
<li><strong>Import snapshot</strong> — loads a snapshot from a JSON file.</li>
|
||||
<li>Each snapshot row has <strong>Open</strong>, <strong>Export</strong>, and <strong>Delete</strong>. <strong>Open</strong> shows all captured data read-only (raw FCI with the decoded FCI, contents); only the snapshot name is editable.</li>
|
||||
<li><strong>Check card snapshot</strong> on a profile row runs the profile rules against a snapshot you pick from the list, without a card reader. The report is the same as a live check; files whose contents were not captured during the scan are reported as unverifiable errors.</li>
|
||||
<li><strong>Compare snapshots</strong> compares two snapshots offline, exactly like a profile check: pick the <em>master</em> snapshot and the <em>snapshot to check</em>, optionally masking the first 4 bytes of EF.IMSI/EF.ICCID (on by default), and get the same pass/fail report; in that report the mismatch fields and the FCI comparison columns are labeled with the master and checked snapshot names instead of expected/actual. Files present only in the checked snapshot are reported as extra files. Back to list returns to the Card snapshots tab.</li>
|
||||
<li><strong>Check card snapshot</strong> on a profile row runs the profile rules against a snapshot you pick from the list, without a card reader. The report is the same as a live check: the actual side is labelled with the snapshot name (<em>actual (snapshot name)</em>) and the header reads <em>Profile verification results for: <profile> → <snapshot name></em>; files whose contents were not captured during the scan are reported as unverifiable errors.</li>
|
||||
<li><strong>Compare snapshots</strong> compares two snapshots offline, exactly like a profile check: pick the <em>master</em> snapshot and the <em>snapshot to check</em>, optionally masking the first 4 bytes of EF.IMSI/EF.ICCID (on by default), and get the same pass/fail report; in that report the header reads <em>Snapshot comparison results: <master> → <checked></em> and the mismatch fields and the FCI comparison columns are labeled with the master and checked snapshot names instead of expected/actual. Files present only in the checked snapshot are reported as extra files. Back to list returns to the Card snapshots tab.</li>
|
||||
</ul>
|
||||
|
||||
</section>
|
||||
|
||||
+139
-30
@@ -18,7 +18,7 @@
|
||||
<div class="max-w-7xl mx-auto px-6 py-2">
|
||||
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<h1 class="text-2xl font-bold text-heading">OTAMan <span id="slogan" class="text-sm font-normal text-gray-500 dark:text-slate-400 ml-2" data-l10n="SIM OTA with a Human Face">SIM OTA with a Human Face</span> <span class="text-xs text-gray-400 dark:text-slate-500 ml-1">v2.1.1</span></h1>
|
||||
<h1 class="text-2xl font-bold text-heading">OTAMan <span id="slogan" class="text-sm font-normal text-gray-500 dark:text-slate-400 ml-2" data-l10n="SIM OTA with a Human Face">SIM OTA with a Human Face</span> <span class="text-xs text-gray-400 dark:text-slate-500 ml-1">v2.1.2</span></h1>
|
||||
<div class="flex items-center gap-4">
|
||||
<span id="state-indicator" class="flex items-center select-none" style="cursor:default" title="Connecting...">
|
||||
<span id="state-indicator-dot" class="text-xs text-gray-400" title="Connecting...">●</span>
|
||||
@@ -749,6 +749,7 @@
|
||||
<div class="flex flex-wrap gap-2 mb-3">
|
||||
<button onclick="profilerNew()" class="px-2.5 py-1 text-sm rounded bg-blue-600 text-white hover:bg-blue-700" data-l10n="New profile">New profile</button>
|
||||
<button data-needs="card" onclick="profilerFromCard()" class="px-2.5 py-1 text-sm rounded bg-blue-600 text-white hover:bg-blue-700 disabled:opacity-40 disabled:cursor-not-allowed" data-l10n="Profile from card">Profile from card</button>
|
||||
<button onclick="profilerFromSnapshot()" class="px-2.5 py-1 text-sm rounded bg-blue-600 text-white hover:bg-blue-700" data-l10n="Profile from snapshot">Profile from snapshot</button>
|
||||
<button onclick="document.getElementById('profiler-import-file').click()" class="px-2.5 py-1 text-sm rounded bg-blue-600 text-white hover:bg-blue-700" data-l10n="Import profile">Import profile</button>
|
||||
<input type="file" id="profiler-import-file" accept=".json,application/json" class="hidden" onchange="profilerImportFile(this)">
|
||||
</div>
|
||||
@@ -7404,6 +7405,7 @@ let profilerEditId = null;
|
||||
let profilerDraft = null;
|
||||
let profilerResults = null;
|
||||
let profilerResultsLabels = null;
|
||||
let profilerResultsHeader = null;
|
||||
let profilerMismatchOnly = false;
|
||||
let snapshots = [];
|
||||
let snapshotViewId = null;
|
||||
@@ -7648,7 +7650,7 @@ function profilerScanSetTarget(target) {
|
||||
_scanTarget = target;
|
||||
const title = document.getElementById('profiler-scan-title');
|
||||
const label = document.getElementById('profiler-scan-name-label');
|
||||
const titleKey = target === 'snapshot' ? 'New snapshot' : 'Profile from card';
|
||||
const titleKey = target === 'snapshot' ? 'New snapshot' : (target === 'profile-snapshot' ? 'Profile from snapshot' : 'Profile from card');
|
||||
const labelKey = target === 'snapshot' ? 'Snapshot name' : 'Profile name';
|
||||
title.setAttribute('data-l10n', titleKey);
|
||||
title.textContent = t(titleKey);
|
||||
@@ -7668,10 +7670,15 @@ function profilerScanRefreshOptions() {
|
||||
}
|
||||
|
||||
function profilerFromCard() {
|
||||
profilerScanSetTarget('profile');
|
||||
document.getElementById('profiler-scan-name').value = '';
|
||||
document.getElementById('profiler-scan-name').readOnly = false;
|
||||
document.getElementById('profiler-scan-name').classList.remove('opacity-50');
|
||||
profilerScanOpenForm('profile', '');
|
||||
}
|
||||
|
||||
function profilerScanOpenForm(target, presetName) {
|
||||
profilerScanSetTarget(target);
|
||||
const nameEl = document.getElementById('profiler-scan-name');
|
||||
nameEl.value = presetName || '';
|
||||
nameEl.readOnly = false;
|
||||
nameEl.classList.remove('opacity-50');
|
||||
const ignore = document.getElementById('profiler-scan-ignore');
|
||||
let html = '';
|
||||
for (const f of PROFILER_IGNORE_FILES) {
|
||||
@@ -7696,7 +7703,7 @@ function profilerFromCard() {
|
||||
document.getElementById('profiler-scan-btn').disabled = false;
|
||||
document.getElementById('profiler-scan-btn').textContent = t('Scan');
|
||||
document.getElementById('profiler-scan-modal').classList.remove('hidden');
|
||||
document.getElementById('profiler-scan-name').focus();
|
||||
nameEl.focus();
|
||||
}
|
||||
|
||||
function profilerScanCancel() {
|
||||
@@ -7762,7 +7769,14 @@ async function profilerScanStart() {
|
||||
const onProgress = (done, total, path) => {
|
||||
progEl.textContent = done + ' / ' + total + ' ' + t('files') + (path ? ' — ' + path : '');
|
||||
};
|
||||
if (_scanTarget === 'snapshot') {
|
||||
if (_scanTarget === 'profile-snapshot' && _scanSnapshot) {
|
||||
const rules = await profilerScanSnapshot(_scanSnapshot, ignoreFids, ignoreNames, fciMode, maskFids, onProgress);
|
||||
const profile = { id: profilerNewId(), name: name, created: new Date().toISOString(), rules: rules };
|
||||
profiles.push(profile);
|
||||
profilerSave();
|
||||
profilerScanCancel();
|
||||
profilerEdit(profiles.length - 1);
|
||||
} else if (_scanTarget === 'snapshot') {
|
||||
const timing = profilerTimingAccumulator();
|
||||
const scanStart = performance.now();
|
||||
const files = await profilerScanCard(new Set(), new Set(), 'exact', onProgress, new Set(), 'snapshot', timing);
|
||||
@@ -7786,6 +7800,7 @@ async function profilerScanStart() {
|
||||
errEl.textContent = e.message;
|
||||
errEl.classList.remove('hidden');
|
||||
} finally {
|
||||
_scanSnapshot = null;
|
||||
profilerScanSetTarget(_scanTarget);
|
||||
cancelBtn.disabled = false;
|
||||
nameEl.readOnly = false;
|
||||
@@ -7864,6 +7879,55 @@ async function profilerScanCard(ignoreFids, ignoreNames, fciMode, onProgress, ma
|
||||
return rules;
|
||||
}
|
||||
|
||||
let _scanSnapshot = null;
|
||||
|
||||
function profilerBuildFileRuleFromSnapshot(f, ignoreFids, ignoreNames, fciMode, maskFids) {
|
||||
const isRecord = profilerContentKindForFileType(f.fileType) === 'record';
|
||||
const rule = {
|
||||
type: 'file',
|
||||
path: f.path,
|
||||
name: f.name || null,
|
||||
fileType: f.fileType || null,
|
||||
fileSize: isRecord ? null : ((f.fileSize === null || f.fileSize === undefined) ? null : f.fileSize),
|
||||
recordLen: (f.recordLen === null || f.recordLen === undefined) ? null : f.recordLen,
|
||||
numRecords: (f.numRecords === null || f.numRecords === undefined) ? null : f.numRecords,
|
||||
fciMode: fciMode || 'type_size',
|
||||
fciHex: f.fciHex || null,
|
||||
content: null,
|
||||
};
|
||||
const seg = (f.path || '').toUpperCase().split('/').pop();
|
||||
const fid = /^[0-9A-F]{4}$/.test(seg) ? seg : '';
|
||||
const name = (f.name || '').toUpperCase();
|
||||
if (fid && ignoreFids && ignoreFids.has(fid)) return rule;
|
||||
if (name && ignoreNames && ignoreNames.has(name)) return rule;
|
||||
if (!f.content) return rule;
|
||||
if (f.content.kind === 'record') {
|
||||
rule.content = {
|
||||
mode: 'exact', kind: 'record',
|
||||
records: (f.content.records || []).map(r => ({ num: r.num, data: r.data })),
|
||||
};
|
||||
} else if (typeof f.content.data === 'string') {
|
||||
const useMask = maskFids ? maskFids.has(fid) : !!PROFILER_MASK_PREFIX4_FIDS[fid];
|
||||
rule.content = useMask
|
||||
? { mode: 'mask', kind: 'transparent', expected: profilerMaskPrefix4(f.content.data) }
|
||||
: { mode: 'exact', kind: 'transparent', expected: f.content.data };
|
||||
}
|
||||
return rule;
|
||||
}
|
||||
|
||||
async function profilerScanSnapshot(snapshot, ignoreFids, ignoreNames, fciMode, maskFids, onProgress) {
|
||||
const files = (snapshot && snapshot.files) ? snapshot.files : [];
|
||||
const total = files.length;
|
||||
if (onProgress) onProgress(0, total, '');
|
||||
const rules = [];
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
if (onProgress) onProgress(i + 1, total, files[i].path);
|
||||
rules.push(profilerBuildFileRuleFromSnapshot(files[i], ignoreFids, ignoreNames, fciMode, maskFids));
|
||||
if (i % 20 === 19) await new Promise(r => setTimeout(r, 0));
|
||||
}
|
||||
return rules;
|
||||
}
|
||||
|
||||
async function profilerBuildFileRule(path, c, ignoreFids, ignoreNames, fciMode, maskFids) {
|
||||
let sel;
|
||||
try {
|
||||
@@ -8429,14 +8493,24 @@ function profilerSaveProfile() {
|
||||
profilerSetView('list');
|
||||
}
|
||||
|
||||
async function profilerCheck(i) {
|
||||
const p = profiles[i];
|
||||
await profilerRunProfile(p, null, p.name);
|
||||
async function profilerCardIccid() {
|
||||
try {
|
||||
const rd = await pysimFetch('/api/read', { path: 'MF/2FE2', mode: 'raw' });
|
||||
if (rd && rd.success && rd.data) return decIccid(rd.data) || null;
|
||||
} catch (e) { /* EF.ICCID not readable */ }
|
||||
return null;
|
||||
}
|
||||
|
||||
async function profilerRunProfile(p, source, title, extraResults, labels) {
|
||||
async function profilerCheck(i) {
|
||||
const p = profiles[i];
|
||||
const iccid = await profilerCardIccid();
|
||||
await profilerRunProfile(p, null, { kind: 'profile', from: p.name, to: iccid }, null,
|
||||
{ expected: p.name, actual: iccid, prefix: true });
|
||||
}
|
||||
|
||||
async function profilerRunProfile(p, source, header, extraResults, labels) {
|
||||
profilerSetView('results');
|
||||
document.getElementById('profiler-results-title').textContent = title;
|
||||
profilerResultsHeader = header || null;
|
||||
const prog = document.getElementById('profiler-progress');
|
||||
document.getElementById('profiler-summary').innerHTML = '';
|
||||
document.getElementById('profiler-report').innerHTML = '';
|
||||
@@ -8453,27 +8527,47 @@ async function profilerRunProfile(p, source, title, extraResults, labels) {
|
||||
profilerRenderResultsView();
|
||||
}
|
||||
|
||||
function profilerSnapshotPickListHtml(actionHtml) {
|
||||
let html = '';
|
||||
for (let si = 0; si < snapshots.length; si++) {
|
||||
const s = snapshots[si];
|
||||
html += '<button onclick="' + actionHtml(si) + '" class="w-full text-left px-3 py-2 text-xs rounded border border-gray-300 dark:border-slate-600 hover:bg-gray-100 dark:hover:bg-slate-700">' +
|
||||
'<span class="font-semibold text-gray-800 dark:text-slate-200">' + esc(s.name) + '</span>' +
|
||||
'<span class="block text-gray-500 dark:text-slate-400">' + esc(s.iccid || '') + ' · ' + esc(new Date(s.created).toLocaleString()) + ' · ' + (s.files ? s.files.length : 0) + ' ' + t('files') + '</span></button>';
|
||||
}
|
||||
return html;
|
||||
}
|
||||
|
||||
function profilerCheckSnapshotPick(i) {
|
||||
if (!snapshots.length) {
|
||||
alert(t('No snapshots defined.'));
|
||||
return;
|
||||
}
|
||||
let html = '';
|
||||
for (let si = 0; si < snapshots.length; si++) {
|
||||
const s = snapshots[si];
|
||||
html += '<button onclick="profilerCheckSnapshot(' + i + ',' + si + ')" class="w-full text-left px-3 py-2 text-xs rounded border border-gray-300 dark:border-slate-600 hover:bg-gray-100 dark:hover:bg-slate-700">' +
|
||||
'<span class="font-semibold text-gray-800 dark:text-slate-200">' + esc(s.name) + '</span>' +
|
||||
'<span class="block text-gray-500 dark:text-slate-400">' + esc(s.iccid || '') + ' · ' + esc(new Date(s.created).toLocaleString()) + ' · ' + (s.files ? s.files.length : 0) + ' ' + t('files') + '</span></button>';
|
||||
}
|
||||
document.getElementById('snapshot-pick-list').innerHTML = html;
|
||||
document.getElementById('snapshot-pick-list').innerHTML = profilerSnapshotPickListHtml(si => 'profilerCheckSnapshot(' + i + ',' + si + ')');
|
||||
document.getElementById('snapshot-pick-modal').classList.remove('hidden');
|
||||
}
|
||||
|
||||
function profilerFromSnapshot() {
|
||||
if (!snapshots.length) {
|
||||
alert(t('No snapshots defined.'));
|
||||
return;
|
||||
}
|
||||
document.getElementById('snapshot-pick-list').innerHTML = profilerSnapshotPickListHtml(si => 'profilerScanFromSnapshot(' + si + ')');
|
||||
document.getElementById('snapshot-pick-modal').classList.remove('hidden');
|
||||
}
|
||||
|
||||
function profilerScanFromSnapshot(si) {
|
||||
document.getElementById('snapshot-pick-modal').classList.add('hidden');
|
||||
_scanSnapshot = snapshots[si];
|
||||
profilerScanOpenForm('profile-snapshot', snapshots[si].name || '');
|
||||
}
|
||||
|
||||
async function profilerCheckSnapshot(i, si) {
|
||||
document.getElementById('snapshot-pick-modal').classList.add('hidden');
|
||||
const p = profiles[i];
|
||||
const s = snapshots[si];
|
||||
await profilerRunProfile(p, profilerSnapshotSource(s), p.name + ' — ' + s.name);
|
||||
await profilerRunProfile(p, profilerSnapshotSource(s), { kind: 'profile', from: p.name, to: s.name }, null,
|
||||
{ expected: p.name, actual: s.name, prefix: true });
|
||||
}
|
||||
|
||||
function profilerMaskFidForFile(f, maskFids) {
|
||||
@@ -8560,7 +8654,7 @@ async function snapshotCompareRun() {
|
||||
});
|
||||
document.getElementById('snapshot-compare-modal').classList.add('hidden');
|
||||
const pseudo = { name: master.name + ' → ' + check.name, rules: profilerRulesFromSnapshot(master, maskFids) };
|
||||
await profilerRunProfile(pseudo, profilerSnapshotSource(check), pseudo.name, profilerExtraFileResults(master, check),
|
||||
await profilerRunProfile(pseudo, profilerSnapshotSource(check), { kind: 'snapshot', from: master.name, to: check.name }, profilerExtraFileResults(master, check),
|
||||
{ expected: master.name, actual: check.name });
|
||||
}
|
||||
|
||||
@@ -8573,8 +8667,15 @@ function profilerVisibleResults(results, mismatchOnly) {
|
||||
return mismatchOnly ? results.filter(r => r.status !== 'pass') : results;
|
||||
}
|
||||
|
||||
function profilerResultsHeaderText(header) {
|
||||
if (!header || !header.from) return '';
|
||||
const base = header.kind === 'snapshot' ? t('Snapshot comparison results:') : t('Profile verification results for:');
|
||||
return base + ' ' + header.from + (header.to ? ' \u2192 ' + header.to : '');
|
||||
}
|
||||
|
||||
function profilerRenderResultsView() {
|
||||
if (!profilerResults) return;
|
||||
document.getElementById('profiler-results-title').textContent = profilerResultsHeaderText(profilerResultsHeader);
|
||||
let passed = 0, failed = 0, errors = 0;
|
||||
for (const res of profilerResults) {
|
||||
if (res.status === 'pass') passed++;
|
||||
@@ -9007,9 +9108,14 @@ function fcpDecode(hex) {
|
||||
return { ok: !error && items.length > 0, template, items, error };
|
||||
}
|
||||
|
||||
function profilerLabelText(labels, which) {
|
||||
if (!labels || !labels[which]) return t(which);
|
||||
return labels.prefix ? t(which) + ' (' + labels[which] + ')' : labels[which];
|
||||
}
|
||||
|
||||
function fcpDiffHtml(expectedHex, actualHex, labels) {
|
||||
const expLabel = (labels && labels.expected) ? labels.expected : t('expected');
|
||||
const actLabel = (labels && labels.actual) ? labels.actual : t('actual');
|
||||
const expLabel = profilerLabelText(labels, 'expected');
|
||||
const actLabel = profilerLabelText(labels, 'actual');
|
||||
const e = fcpDecode(expectedHex);
|
||||
const a = fcpDecode(actualHex);
|
||||
const eBad = !!e.error, aBad = !!a.error;
|
||||
@@ -9040,8 +9146,8 @@ function fcpDiffHtml(expectedHex, actualHex, labels) {
|
||||
}
|
||||
html += '</tbody></table>';
|
||||
}
|
||||
if (eBad) html += '<div class="text-xs text-red-600 mt-0.5 pl-2">' + esc(t('expected')) + ': ' + esc(e.error) + '</div>';
|
||||
if (aBad) html += '<div class="text-xs text-red-600 mt-0.5 pl-2">' + esc(t('actual')) + ': ' + esc(a.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>';
|
||||
return html;
|
||||
}
|
||||
|
||||
@@ -9070,9 +9176,9 @@ function profilerFciInput(i, value) {
|
||||
}
|
||||
|
||||
function profilerRenderReport(results, labels) {
|
||||
const expLabel = (labels && labels.expected) ? labels.expected : t('expected');
|
||||
const actLabel = (labels && labels.actual) ? labels.actual : t('actual');
|
||||
const labelCls = 'text-xs text-gray-500 dark:text-slate-400 text-right shrink-0 ' + (labels ? 'w-40 break-all' : 'w-24');
|
||||
const expLabel = profilerLabelText(labels, 'expected');
|
||||
const actLabel = profilerLabelText(labels, 'actual');
|
||||
const labelCls = 'text-xs text-gray-500 dark:text-slate-400 text-right shrink-0 ' + (labels ? ((labels.prefix ? 'w-48' : 'w-40') + ' break-all') : 'w-24');
|
||||
let html = '';
|
||||
for (const r of results) {
|
||||
const color = r.status === 'pass' ? 'text-emerald-600' : (r.status === 'fail' ? 'text-red-600' : 'text-yellow-600');
|
||||
@@ -9260,6 +9366,9 @@ const LANG_RU = {
|
||||
'Step': 'Шаг',
|
||||
'Install OK': 'Установка OK',
|
||||
'Install FAILED at step:': 'Установка не удалась на шаге:',
|
||||
'Profile from snapshot': 'Профиль из снимка',
|
||||
'Profile verification results for:': 'Результаты проверки профиля:',
|
||||
'Snapshot comparison results:': 'Результаты сравнения снимков:',
|
||||
'RAM operations perform atomic GlobalPlatform commands over SCP80. Card keys are taken from the saved preset.': 'RAM-операции выполняют атомарные команды GlobalPlatform через SCP80. Ключи карты берутся из сохранённого пресета.',
|
||||
'No cards defined.': 'Карты не заданы.',
|
||||
'Card presets': 'Выбор карты',
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
const CACHE = 'otaman-v128';
|
||||
const CACHE = 'otaman-v132';
|
||||
const URLS = [
|
||||
'index.html',
|
||||
'help.html',
|
||||
|
||||
@@ -81,3 +81,10 @@ test('file manager shows FCI info and keeps the selection in state, not the DOM'
|
||||
assert.ok(html.includes('pysimFsSelected = name;'));
|
||||
assert.ok(!html.includes('pysimFsSelect()'));
|
||||
});
|
||||
|
||||
test('profile list has a Profile from snapshot button', () => {
|
||||
assert.match(html, /data-l10n="Profile from snapshot">Profile from snapshot</);
|
||||
assert.ok(html.includes('onclick="profilerFromSnapshot()"'));
|
||||
assert.ok(html.includes('function profilerScanFromSnapshot(si)'));
|
||||
assert.ok(html.includes('function profilerBuildFileRuleFromSnapshot('));
|
||||
});
|
||||
|
||||
@@ -21,14 +21,18 @@ function extractFunc(src, name, asyncFn) {
|
||||
return (asyncFn ? 'async ' : '') + src.slice(m.index, i + 1);
|
||||
}
|
||||
|
||||
const FNS = ['profilerNormHex', 'profilerNormHexStrict', 'profilerMatch', 'profilerMatchMin', 'profilerMaskPrefix4', 'profilerFileFields', 'profilerContentKindForFileType', 'profilerEmptyRecordContent', 'profilerValidateProfile', 'profilerCustomNameForPath', 'profilerUpdateRulePath', 'profilerResultAspects', 'profilerAspectSummary', 'profilerNumRanges', 'esc', 'escHtml', 'profilerRawDataCheck', 'profilerRenderReport', 'parseBerLen', 'parseTlvList', 'fcpInt', 'fcpParseTlvs', 'fcpFileDescriptor', 'fcpLifeCycle', 'fcpSfi', 'fcpDo', 'fcpDecode', 'fcpDiffHtml', 'profilerFciPreviewItems', 'profilerUpdateFciPreview', 'profilerUpdateRule', 'profilerFciInput', 'profilerScanToggleAll', 'profilerScanIgnoreAllState', 'swapNibbles', 'decIccid', 'profilerSnapshotIccid', 'profilerValidateSnapshot', 'profilerListSwitch', 'profilerScanRefreshOptions', 'profilerLiveSource', 'profilerSnapshotSource', 'profilerVisibleResults', 'profilerMaskFidForFile', 'profilerRulesFromSnapshot', 'profilerExtraFileResults', 'profilerScanNameKeydown', 'profilerTimingStats', 'profilerTimingAccumulator', 'profilerFormatMs', 'profilerRenderSnapshotSummary', 'profilerSnapshotCountLabel', 'pysimFsInfoHtml'];
|
||||
const FNS = ['profilerNormHex', 'profilerNormHexStrict', 'profilerMatch', 'profilerMatchMin', 'profilerMaskPrefix4', 'profilerFileFields', 'profilerContentKindForFileType', 'profilerEmptyRecordContent', 'profilerValidateProfile', 'profilerCustomNameForPath', 'profilerUpdateRulePath', 'profilerResultAspects', 'profilerAspectSummary', 'profilerNumRanges', 'esc', 'escHtml', 'profilerRawDataCheck', 'profilerRenderReport', 'parseBerLen', 'parseTlvList', 'fcpInt', 'fcpParseTlvs', 'fcpFileDescriptor', 'fcpLifeCycle', 'fcpSfi', 'fcpDo', 'fcpDecode', 'fcpDiffHtml', 'profilerFciPreviewItems', 'profilerUpdateFciPreview', 'profilerUpdateRule', 'profilerFciInput', 'profilerScanToggleAll', 'profilerScanIgnoreAllState', 'swapNibbles', 'decIccid', 'profilerSnapshotIccid', 'profilerValidateSnapshot', 'profilerListSwitch', 'profilerScanRefreshOptions', 'profilerLiveSource', 'profilerSnapshotSource', 'profilerVisibleResults', 'profilerMaskFidForFile', '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';
|
||||
code += extractFunc(html, 'profilerBuildFileRule', true) + '\n';
|
||||
code += extractFunc(html, 'profilerRunRule', true) + '\n';
|
||||
code += extractFunc(html, 'profilerScanCard', true) + '\n';
|
||||
code += extractFunc(html, 'profilerBuildSnapshotFile', true) + '\n';
|
||||
code += "var _scanTarget = 'profile';\n";
|
||||
code += extractFunc(html, 'profilerCardIccid', true) + '\n';
|
||||
code += extractFunc(html, 'profilerCheck', true) + '\n';
|
||||
code += extractFunc(html, 'profilerCheckSnapshot', true) + '\n';
|
||||
code += extractFunc(html, 'profilerScanSnapshot', true) + '\n';
|
||||
code += "var _scanTarget = 'profile';\nvar profilerResults = null;\nvar profilerResultsHeader = null;\nvar profilerMismatchOnly = false;\n";
|
||||
code += html.match(/const PROFILER_MASK_PREFIX4_FIDS = \{[\s\S]*?\n\};/)[0] + '\n';
|
||||
eval(code);
|
||||
|
||||
@@ -1303,3 +1307,176 @@ test('fcpDiffHtml headers use custom labels', () => {
|
||||
assert.ok(diff.includes('>Candidate<'), diff);
|
||||
delete global.t;
|
||||
});
|
||||
|
||||
test('profilerLabelText builds default, verbatim and prefixed labels', () => {
|
||||
global.t = s => 't:' + s;
|
||||
assert.strictEqual(profilerLabelText(null, 'expected'), 't:expected');
|
||||
assert.strictEqual(profilerLabelText({ expected: 'A', actual: 'B' }, 'expected'), 'A');
|
||||
assert.strictEqual(profilerLabelText({ expected: 'A', actual: 'B' }, 'actual'), 'B');
|
||||
assert.strictEqual(profilerLabelText({ expected: 'A', actual: 'B', prefix: true }, 'actual'), 't:actual (B)');
|
||||
assert.strictEqual(profilerLabelText({ expected: '', prefix: true }, 'expected'), 't:expected');
|
||||
delete global.t;
|
||||
});
|
||||
|
||||
test('profilerRenderReport prefixes names in expected/actual labels', () => {
|
||||
global.t = s => s;
|
||||
global.pysimCustomFiles = [];
|
||||
const html = profilerRenderReport([{
|
||||
path: 'MF/6F3A', status: 'fail',
|
||||
checks: [{ label: 'fileSize', expected: 4, actual: 9, ok: false }],
|
||||
}], { expected: 'My profile', actual: 'Snap X', prefix: true });
|
||||
assert.ok(html.includes('expected (My profile)'), html);
|
||||
assert.ok(html.includes('actual (Snap X)'), html);
|
||||
delete global.t;
|
||||
delete global.pysimCustomFiles;
|
||||
});
|
||||
|
||||
test('fcpDiffHtml uses prefixed labels in headers and decode notes', () => {
|
||||
global.t = s => s;
|
||||
const diff = fcpDiffHtml(FCP_TRANSPARENT, '62128002000A8202412183026F078A0105880110', { expected: 'Prof', actual: 'Snap', prefix: true });
|
||||
assert.ok(diff.includes('expected (Prof)'), diff);
|
||||
assert.ok(diff.includes('actual (Snap)'), diff);
|
||||
const bad = fcpDiffHtml('not hex', FCP_TRANSPARENT, { expected: 'Prof', actual: 'Snap', prefix: true });
|
||||
assert.ok(bad.includes('expected (Prof)'), bad);
|
||||
delete global.t;
|
||||
});
|
||||
|
||||
test('profilerCardIccid reads and decodes MF/2FE2, null otherwise', async () => {
|
||||
let body = null;
|
||||
global.pysimFetch = async (path, b) => { body = [path, b]; return { success: true, data: '98103254769810325476' }; };
|
||||
assert.strictEqual(await profilerCardIccid(), '89012345678901234567');
|
||||
assert.deepStrictEqual(body, ['/api/read', { path: 'MF/2FE2', mode: 'raw' }]);
|
||||
global.pysimFetch = async () => ({ success: false });
|
||||
assert.strictEqual(await profilerCardIccid(), null);
|
||||
global.pysimFetch = async () => { throw new Error('offline'); };
|
||||
assert.strictEqual(await profilerCardIccid(), null);
|
||||
delete global.pysimFetch;
|
||||
});
|
||||
|
||||
test('profilerCheck titles with the card ICCID and passes prefixed labels', async () => {
|
||||
global.t = s => s;
|
||||
global.profiles = [{ name: 'MyProfile', rules: [] }];
|
||||
global.pysimFetch = async () => ({ success: true, data: '98103254769810325476' });
|
||||
let captured = null;
|
||||
global.profilerRunProfile = async (...args) => { captured = args; };
|
||||
await profilerCheck(0);
|
||||
assert.deepStrictEqual(captured[2], { kind: 'profile', from: 'MyProfile', to: '89012345678901234567' });
|
||||
assert.deepStrictEqual(captured[4], { expected: 'MyProfile', actual: '89012345678901234567', prefix: true });
|
||||
delete global.profiles; delete global.pysimFetch; delete global.profilerRunProfile; delete global.t;
|
||||
});
|
||||
|
||||
test('profilerCheckSnapshot passes the snapshot name as the actual label', async () => {
|
||||
global.t = s => s;
|
||||
global.profiles = [{ name: 'Prof', rules: [] }];
|
||||
global.snapshots = [{ name: 'SnapX', files: [] }];
|
||||
global.document = { getElementById: () => ({ classList: { add() {} } }) };
|
||||
let captured = null;
|
||||
global.profilerRunProfile = async (...args) => { captured = args; };
|
||||
await profilerCheckSnapshot(0, 0);
|
||||
assert.deepStrictEqual(captured[2], { kind: 'profile', from: 'Prof', to: 'SnapX' });
|
||||
assert.deepStrictEqual(captured[4], { expected: 'Prof', actual: 'SnapX', prefix: true });
|
||||
delete global.profiles; delete global.snapshots; delete global.profilerRunProfile; delete global.t;
|
||||
});
|
||||
|
||||
test('profilerResultsHeaderText builds profile and snapshot headers', () => {
|
||||
global.t = s => 't:' + s;
|
||||
assert.strictEqual(profilerResultsHeaderText({ kind: 'profile', from: 'Prof', to: '8901' }), 't:Profile verification results for: Prof \u2192 8901');
|
||||
assert.strictEqual(profilerResultsHeaderText({ kind: 'snapshot', from: 'Snap1', to: 'Snap2' }), 't:Snapshot comparison results: Snap1 \u2192 Snap2');
|
||||
assert.strictEqual(profilerResultsHeaderText({ kind: 'profile', from: 'Prof', to: null }), 't:Profile verification results for: Prof');
|
||||
assert.strictEqual(profilerResultsHeaderText(null), '');
|
||||
delete global.t;
|
||||
});
|
||||
|
||||
test('profilerRenderResultsView writes the built header into the title', () => {
|
||||
global.t = s => s;
|
||||
profilerResults = [];
|
||||
profilerResultsHeader = { kind: 'profile', from: 'Prof', to: 'Snap' };
|
||||
const els = {};
|
||||
for (const id of ['profiler-summary', 'profiler-report', 'profiler-results-title']) els[id] = { innerHTML: '', textContent: '' };
|
||||
global.document = { getElementById: id => els[id] || null };
|
||||
profilerRenderResultsView();
|
||||
assert.strictEqual(els['profiler-results-title'].textContent, 'Profile verification results for: Prof \u2192 Snap');
|
||||
profilerResultsHeader = { kind: 'snapshot', from: 'A', to: 'B' };
|
||||
profilerRenderResultsView();
|
||||
assert.strictEqual(els['profiler-results-title'].textContent, 'Snapshot comparison results: A \u2192 B');
|
||||
profilerResults = null;
|
||||
profilerResultsHeader = null;
|
||||
delete global.t;
|
||||
});
|
||||
|
||||
test('profilerBuildFileRuleFromSnapshot builds a rule with exact contents', () => {
|
||||
const f = { path: 'MF/7F10/6F3A', name: 'EF.ADN', fileType: 'transparent', fileSize: 4, recordLen: null, numRecords: null, fciHex: '620B', content: { kind: 'transparent', data: 'AABBCCDD' } };
|
||||
const rule = profilerBuildFileRuleFromSnapshot(f, new Set(), new Set(), 'type_size', new Set());
|
||||
assert.strictEqual(rule.path, 'MF/7F10/6F3A');
|
||||
assert.strictEqual(rule.name, 'EF.ADN');
|
||||
assert.strictEqual(rule.fileType, 'transparent');
|
||||
assert.strictEqual(rule.fileSize, 4);
|
||||
assert.strictEqual(rule.fciMode, 'type_size');
|
||||
assert.strictEqual(rule.fciHex, '620B');
|
||||
assert.deepStrictEqual(rule.content, { mode: 'exact', kind: 'transparent', expected: 'AABBCCDD' });
|
||||
});
|
||||
|
||||
test('profilerBuildFileRuleFromSnapshot masks the first 4 bytes on request', () => {
|
||||
const f = { path: 'MF/6F07', name: 'EF.IMSI', fileType: 'transparent', fileSize: 9, content: { kind: 'transparent', data: '0891101234567890' } };
|
||||
assert.deepStrictEqual(profilerBuildFileRuleFromSnapshot(f, new Set(), new Set(), 'type', new Set(['6F07'])).content,
|
||||
{ mode: 'mask', kind: 'transparent', expected: '08911012????????' });
|
||||
assert.strictEqual(profilerBuildFileRuleFromSnapshot(f, new Set(), new Set(), 'type', new Set()).content.mode, 'exact');
|
||||
});
|
||||
|
||||
test('profilerBuildFileRuleFromSnapshot honors the ignore list by FID and name', () => {
|
||||
const f = { path: 'MF/7F20/6F52', name: 'EF.KcGPRS', fileType: 'transparent', fileSize: 9, content: { kind: 'transparent', data: 'AABB' } };
|
||||
assert.strictEqual(profilerBuildFileRuleFromSnapshot(f, new Set(['6F52']), new Set(), 'type_size', new Set()).content, null);
|
||||
assert.strictEqual(profilerBuildFileRuleFromSnapshot(f, new Set(), new Set(['EF.KCGPRS']), 'type_size', new Set()).content, null);
|
||||
});
|
||||
|
||||
test('profilerBuildFileRuleFromSnapshot keeps uncaptured and record contents', () => {
|
||||
const noContent = { path: 'MF/6F07', name: 'EF.IMSI', fileType: 'transparent', fileSize: 9, content: null };
|
||||
assert.strictEqual(profilerBuildFileRuleFromSnapshot(noContent, new Set(), new Set(), 'type_size', new Set()).content, null);
|
||||
const records = { path: 'MF/7F10/6F3A', name: 'EF.ADN', fileType: 'linear_fixed', fileSize: 60, recordLen: 30, numRecords: 2, content: { kind: 'record', records: [{ num: 1, data: 'AA' }, { num: 2, data: 'BB' }] } };
|
||||
const rule = profilerBuildFileRuleFromSnapshot(records, new Set(), new Set(), 'exact', new Set());
|
||||
assert.strictEqual(rule.fileSize, null);
|
||||
assert.strictEqual(rule.recordLen, 30);
|
||||
assert.strictEqual(rule.numRecords, 2);
|
||||
assert.deepStrictEqual(rule.content, { mode: 'exact', kind: 'record', records: [{ num: 1, data: 'AA' }, { num: 2, data: 'BB' }] });
|
||||
});
|
||||
|
||||
test('profilerScanSnapshot walks snapshot files with progress', async () => {
|
||||
const snapshot = { files: [
|
||||
{ path: 'MF/6F07', name: 'EF.IMSI', fileType: 'transparent', content: null },
|
||||
{ path: 'MF/2FE2', name: 'EF.ICCID', fileType: 'transparent', content: { kind: 'transparent', data: '9807' } },
|
||||
] };
|
||||
const progress = [];
|
||||
const rules = await profilerScanSnapshot(snapshot, new Set(), new Set(), 'type_size', new Set(), (d, tt, p) => progress.push([d, tt, p]));
|
||||
assert.strictEqual(rules.length, 2);
|
||||
assert.deepStrictEqual(rules.map(r => r.path), ['MF/6F07', 'MF/2FE2']);
|
||||
assert.deepStrictEqual(progress, [[0, 2, ''], [1, 2, 'MF/6F07'], [2, 2, 'MF/2FE2']]);
|
||||
});
|
||||
|
||||
test('profilerSnapshotPickListHtml wires the chosen action per snapshot', () => {
|
||||
global.t = s => s;
|
||||
global.snapshots = [
|
||||
{ name: 'SnapA', iccid: '8901', created: '2026-01-01T00:00:00Z', files: [{}, {}] },
|
||||
{ name: 'SnapB', iccid: '', created: '2026-01-02T00:00:00Z', files: [] },
|
||||
];
|
||||
const html = profilerSnapshotPickListHtml(si => 'profilerScanFromSnapshot(' + si + ')');
|
||||
assert.ok(html.includes('onclick="profilerScanFromSnapshot(0)"'), html);
|
||||
assert.ok(html.includes('onclick="profilerScanFromSnapshot(1)"'), html);
|
||||
assert.ok(html.includes('SnapA'), html);
|
||||
assert.ok(html.includes('2 files'), html);
|
||||
delete global.snapshots;
|
||||
delete global.t;
|
||||
});
|
||||
|
||||
test('profilerScanSetTarget labels the snapshot-sourced profile form', () => {
|
||||
global.t = s => s;
|
||||
const els = {};
|
||||
for (const id of ['profiler-scan-title', 'profiler-scan-name-label', 'profiler-scan-options']) {
|
||||
els[id] = { textContent: '', attrs: {}, classList: { set: new Set(), toggle(c, on) { if (on) this.set.add(c); else this.set.delete(c); }, contains(c) { return this.set.has(c); } }, setAttribute(k, v) { this.attrs[k] = v; } };
|
||||
}
|
||||
global.document = { getElementById: id => els[id] || null };
|
||||
profilerScanSetTarget('profile-snapshot');
|
||||
assert.strictEqual(els['profiler-scan-title'].textContent, 'Profile from snapshot');
|
||||
assert.strictEqual(els['profiler-scan-name-label'].textContent, 'Profile name');
|
||||
assert.ok(!els['profiler-scan-options'].classList.contains('hidden'));
|
||||
delete global.t;
|
||||
});
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "pysim-otaman-server"
|
||||
version = "2.1.1"
|
||||
version = "2.1.2"
|
||||
description = "HTTP REST server wrapping pysim for the OTAMan PWA"
|
||||
requires-python = ">=3.8"
|
||||
# pysim is a git-only dependency installed explicitly by setup.bat/setup.sh.
|
||||
|
||||
@@ -19,7 +19,7 @@ from osmocom.construct import GsmOrUcs2Adapter
|
||||
from osmocom.tlv import BER_TLV_IE
|
||||
|
||||
|
||||
VERSION = '2.1.1'
|
||||
VERSION = '2.1.2'
|
||||
|
||||
MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE
|
||||
|
||||
|
||||
Reference in New Issue
Block a user