EF content decoders: decoded fields in the file manager, snapshots, checks and comparisons (v2.4.0)
Client-side, offline decoder layer (ported from simtrace-analyser, verified against the pinned specs and pySim test vectors): - file manager Decoded pill: decoded field table instead of the raw pySim JSON dump (per-record fields for record files); the server's read_*_decoded JSON stays behind a 'pySim JSON (server)' disclosure; switching Raw/Decoded re-reads the open file - snapshot view: decoded field block for transparent files and a per-record decoded summary for record files - profile check / snapshot comparison report: content mismatches of files with a decoder render as a per-field decoded diff (only the differing fields, expected | actual); the raw expected/actual pair is kept when the decoded values match - coverage: ICCID, IMSI/IST, UST/SST, LI/PL, SPN, PLMNsel/PLMNwAcT/ OPLMNwAcT/HPLMNwAcT, OPL, SPDI, PNN, LOCI/PSLOCI/EPSLOCI/EPSNSC/LOCIGPRS, Kc/KcGPRS, ACC, PHASE, CBMI/CBMID/CBMIR, ECC, AD, ACL, SMSP, SMS, ARR, DIR, IMPI/DOMAIN/IMPU, ADN/FDN/SDN/MSISDN/LND/BDN, EXT1-5, SUME (TS 51.011 §10.3.34); unknown files fall back to raw - new frontend/tests/ef_decode.test.js (14 tests); help EN/RU + READMEs document the decoded views; version 2.4.0, SW cache simple-v191 - also fix the README/RUS snapshot-comparison text: comparison is always exact (no content masking)
This commit is contained in:
1 parent
4f6e48f0e0
commit
eb83238ae7
9 files changed
+1161
-28
No files matched your search
@@ -500,7 +500,7 @@ Browse the UICC filesystem in a tree view. Files are shown with names, FIDs, and
|
||||
- Entries are grouped with DFs above EFs and sorted by **FID** or symbolic **Name** (pills above the tree, remembered in `localStorage`)
|
||||
- **Read** — reads the selected file (auto-detects transparent vs record files)
|
||||
- **Edit** — switch to edit mode, modify hex data, click **Save** to write back
|
||||
- **Raw / Decoded** — toggle between hex dump and pysim-decoded JSON view
|
||||
- **Raw / Decoded** — toggle between the hex dump and a decoded field table (client-side decoders for IMSI, ICCID, SPN, PLMN lists, LOCI/EPSLOCI, ADN/MSISDN, service tables, SUME, …); the server-side pySim JSON of the same read stays behind a collapsed disclosure
|
||||
- Selecting a file shows its FID, file type, size / record layout and the decoded FCI above the contents
|
||||
- Missing files are shown in red (✗); a present but empty DF shows `(empty)`
|
||||
- **Probe all files** — walks the whole tree (incl. custom files), marks every entry present/absent with *N / total* progress, stoppable, and ends with a summary; browsing itself stays lazy
|
||||
@@ -525,7 +525,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*. 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.
|
||||
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, shows content mismatches of files with a decoder as a decoded per-field comparison (only the differing fields, expected and actual side by side; the raw pair is kept when the decoded values match), 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
|
||||
|
||||
@@ -533,12 +533,12 @@ The scan dialog asks for a profile name and offers the FCP/FCI mode described ab
|
||||
|
||||
#### Card snapshots
|
||||
|
||||
The list view has two tabs — **Profiles** and **Card snapshots**. A 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 every card command (SELECT / READ BINARY / READ RECORD) from command to response and stores min/avg/max per type plus the total scan time; the snapshot view shows these in the summary and the select/read time per file (read time per record). Timings are display-only and ignored by checks/comparisons.
|
||||
The list view has two tabs — **Profiles** and **Card snapshots**. A 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 every card command (SELECT / READ BINARY / READ RECORD) from command to response and stores min/avg/max per type plus the total scan time; the snapshot view shows these in the summary and the select/read time per file (read time per record). Timings are display-only and ignored by checks/comparisons.
|
||||
|
||||
- **New snapshot** scans the card; **Import snapshot** loads JSON.
|
||||
- Each snapshot row has **Open** (all captured data read-only, raw FCI with decoded FCI and contents; only the name is editable), **Export**, and **Delete**.
|
||||
- **Check card snapshot** on a profile row runs the profile rules against a snapshot picked from the list, without a card reader. Files whose contents were not captured are reported as unverifiable errors.
|
||||
- **Compare snapshots** compares two snapshots offline exactly like a profile check: pick the *master* snapshot and the *snapshot to check*, optionally masking the first 4 bytes of EF.IMSI/EF.ICCID (on by default), and get the same report. Every file must match exactly (exact FCI, contents); files present only in the checked snapshot are reported as extra files. In the comparison report the mismatch fields and FCI comparison columns are labeled with the master/checked snapshot names instead of expected/actual.
|
||||
- **Compare snapshots** compares two snapshots offline exactly like a profile check: pick the *master* snapshot and the *snapshot to check* and get the same report — snapshot comparison is always exact (no content masking). Files present only in the checked snapshot are reported as extra files. In the comparison report the mismatch fields and FCI comparison columns are labeled with the master/checked snapshot names instead of expected/actual.
|
||||
|
||||
---
|
||||
|
||||
|
||||
+4
-4
@@ -474,7 +474,7 @@ Delivery PoR (SPI2 `01`) проще — карта возвращает PoR на
|
||||
- Элементы сгруппированы (DF выше EF) и отсортированы по **FID** или символьному **имени** (пиллы над деревом, выбор сохраняется в `localStorage`)
|
||||
- **Read** — чтение файла (автоопределение transparent/record)
|
||||
- **Edit** — режим редактирования, измените hex-данные и нажмите **Save** для записи
|
||||
- **Raw / Decoded** — переключение между hex-дампом и декодированным JSON
|
||||
- **Raw / Decoded** — переключение между hex-дампом и таблицей декодированных полей (клиентские декодеры: IMSI, ICCID, SPN, списки PLMN, LOCI/EPSLOCI, ADN/MSISDN, таблицы сервисов, SUME, …); серверный pySim JSON того же чтения остаётся в свёрнутом блоке
|
||||
- При выборе файла над содержимым показываются FID, тип файла, размер / структура записей и декодированный FCI
|
||||
- Отсутствующие файлы показаны красным (✗); существующий пустой DF — `(пусто)`
|
||||
- **Проверить все файлы** — обход всего дерева (включая пользовательские) с пометкой «есть/нет», прогрессом *N / всего*, возможностью остановки и сводкой в конце; сам просмотр остаётся ленивым
|
||||
@@ -499,7 +499,7 @@ Delivery PoR (SPI2 `01`) проще — карта возвращает PoR на
|
||||
- **Атрибуты файла** — тип, размер, длина и число записей из шаблона FCP (любое можно не задавать).
|
||||
- **Проверка содержимого** (опционально) — **Exact** (точное равенство hex) или **Mask**, где `?` — пониббловый джокер (маска без `?` — префиксное совпадение, напр. `0891` для MCC/MNC IMSI). Для record-файлов хранится список записей.
|
||||
|
||||
Отчёт проверки помечает каждый аспект (напр. *тип файла ✓, размер ✗, содержимое ✓*), показывает расхождения как поля только для чтения (ожидаемое/фактическое в одной колонке) и декодированное сравнение параметров FCI для расхождений FCI. Повреждённые FCI показывают всё, что удалось декодировать, плюс явное сообщение об ошибке; для записей указываются *совпадающие записи*. В отчёте поля расхождений и колонки сравнения FCI подписаны `ожидалось (имя профиля)` и `фактически (ICCID карты)` для проверки карты либо `фактически (имя снимка)` для проверки снимка; в заголовке отчёта — `Результаты проверки профиля: <профиль> → <ICCID карты>` (или `… → <имя снимка>`; для сравнения снимков — `Результаты сравнения снимков: <эталон> → <проверяемый>`). Опция **«Только расхождения»** скрывает все совпавшие файлы, оставляя несовпадения и ошибки.
|
||||
Отчёт проверки помечает каждый аспект (напр. *тип файла ✓, размер ✗, содержимое ✓*), показывает расхождения как поля только для чтения (ожидаемое/фактическое в одной колонке), показывает расхождения содержимого файлов с декодером декодированным сравнением по полям (только отличающиеся поля, ожидаемое и фактическое рядом; сырая пара остаётся, когда декодированные значения совпадают) и декодированное сравнение параметров FCI для расхождений FCI. Повреждённые FCI показывают всё, что удалось декодировать, плюс явное сообщение об ошибке; для записей указываются *совпадающие записи*. В отчёте поля расхождений и колонки сравнения FCI подписаны `ожидалось (имя профиля)` и `фактически (ICCID карты)` для проверки карты либо `фактически (имя снимка)` для проверки снимка; в заголовке отчёта — `Результаты проверки профиля: <профиль> → <ICCID карты>` (или `… → <имя снимка>`; для сравнения снимков — `Результаты сравнения снимков: <эталон> → <проверяемый>`). Опция **«Только расхождения»** скрывает все совпавшие файлы, оставляя несовпадения и ошибки.
|
||||
|
||||
#### Опции сканирования «Профиль с карты»
|
||||
|
||||
@@ -507,12 +507,12 @@ Delivery PoR (SPI2 `01`) проще — карта возвращает PoR на
|
||||
|
||||
#### Снимки карт
|
||||
|
||||
Представление списка имеет две вкладки — **«Профили»** и **«Снимки карт»**. Снимок — неизменяемая фиксация файловой системы: путь, символьное имя, тип, размер (или длина/число записей), сырой FCI и содержимое (если читается) каждого существующего файла. ICCID декодируется из EF.ICCID и показывается рядом с именем. При сканировании также измеряется время каждой команды карты (SELECT / READ BINARY / READ RECORD) от отправки до ответа; сохраняются min/сред/max по типам и общее время сканирования — они показываются в сводке снимка и по файлам/записям. Время носит информационный характер и не используется при проверках и сравнении.
|
||||
Представление списка имеет две вкладки — **«Профили»** и **«Снимки карт»**. Снимок — неизменяемая фиксация файловой системы: путь, символьное имя, тип, размер (или длина/число записей), сырой FCI и содержимое (если читается) каждого существующего файла. ICCID декодируется из EF.ICCID и показывается рядом с именем. Захваченное содержимое показывается вместе с декодированным представлением, где есть декодер — таблица полей для transparent-файлов и однострочная сводка по каждой записи для record-файлов. При сканировании также измеряется время каждой команды карты (SELECT / READ BINARY / READ RECORD) от отправки до ответа; сохраняются min/сред/max по типам и общее время сканирования — они показываются в сводке снимка и по файлам/записям. Время носит информационный характер и не используется при проверках и сравнении.
|
||||
|
||||
- **Новый снимок** сканирует карту; **Импорт снимка** загружает JSON.
|
||||
- В строке снимка: **Открыть** (все данные только для чтения, сырой FCI с декодированным и содержимое; редактируется только имя), **Экспорт**, **Удалить**.
|
||||
- **Проверить снимок карты** в строке профиля выполняет правила профиля на выбранном снимке без картридера. Файлы без захваченного содержимого помечаются как непроверяемые ошибки.
|
||||
- **Сравнить снимки** сравнивает два снимка offline так же, как проверка профиля: выберите *эталонный* снимок и *снимок для проверки*, при необходимости включите маску первых 4 байт EF.IMSI/EF.ICCID (включена по умолчанию). Всё должно совпадать точно (FCI, содержимое); файлы только в проверяемом снимке помечаются как лишние. В отчёте поля расхождений и колонки сравнения FCI подписаны именами эталонного и проверяемого снимков вместо expected/actual.
|
||||
- **Сравнить снимки** сравнивает два снимка offline так же, как проверка профиля: выберите *эталонный* снимок и *снимок для проверки* и получите тот же отчёт — сравнение снимков всегда точное (без масок содержимого). Файлы только в проверяемом снимке помечаются как лишние. В отчёте поля расхождений и колонки сравнения FCI подписаны именами эталонного и проверяемого снимков вместо expected/actual.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -350,7 +350,7 @@
|
||||
<ul class="list-disc list-inside text-sm space-y-1 mb-3">
|
||||
<li><strong>Прочитать</strong> — чтение файла (автоопределение transparent/record)</li>
|
||||
<li><strong>Редактировать</strong> — изменение hex-данных, <strong>Сохранить</strong> для записи (или <strong>Отмена</strong>)</li>
|
||||
<li><strong>Данные как на карте / Декодированные данные</strong> — переключение между hex-дампом и декодированным JSON</li>
|
||||
<li><strong>Данные как на карте / Декодированные данные</strong> — переключение между hex-дампом и таблицей декодированных полей (клиентские декодеры EF: IMSI, ICCID, SPN, списки PLMN, LOCI/PSLOCI/EPSLOCI, ADN/MSISDN, таблицы сервисов, SUME, …); серверный pySim JSON того же чтения остаётся доступен в свёрнутом блоке <em>pySim JSON (сервер)</em></li>
|
||||
<li><strong>Проверить все файлы</strong> — обход всего дерева (включая пользовательские файлы) с пометкой каждого элемента: есть (обычный вид) или нет (красный ✗, без стрелки разворачивания); существующие пустые DF показывают <code class="font-mono text-sm">(пусто)</code>. Отображается прогресс <em>N / всего</em>, обход можно остановить; в конце — сводка «есть/нет». Файлы проверяются только при разворачивании или проверке — просмотр остаётся ленивым.</li>
|
||||
</ul>
|
||||
|
||||
@@ -417,12 +417,12 @@
|
||||
<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> со списком записей, которые совпали. В отчёте поля расхождений и колонки сравнения FCI подписаны <em>ожидалось (имя профиля)</em> и <em>фактически (ICCID карты)</em>, а в заголовке отчёта выводится <em>Результаты проверки профиля: <профиль> → <ICCID карты></em>. Опция <strong>«Только расхождения»</strong> в заголовке отчёта скрывает все совпавшие файлы и оставляет только несовпадения и ошибки.</p>
|
||||
<p class="text-sm mb-3"><strong>Проверить карту</strong> выполняет каждое правило на подключённой карте и показывает строку прогресса и отчёт прохождения. Рядом с путём файла указывается, что именно проверялось (например, <em>тип файла и размер, содержимое</em> или <em>полный FCI</em>); если часть проверок прошла, а часть нет — каждый аспект помечается (<em>тип файла ✓, размер ✗, содержимое ✓</em>), а расхождения расписываются ниже. Несовпавшие сырые данные (FCI, содержимое, данные записей) показываются как поля только для чтения с моноширинным шрифтом — ожидаемое над фактическим, в одной и той же колонке — для удобного сравнения; расхождения содержимого файлов с декодером показываются декодированным сравнением по полям (только отличающиеся поля, ожидаемое и фактическое рядом; сырая пара остаётся, когда декодированные значения совпадают), а для расхождений 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>
|
||||
|
||||
<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>
|
||||
<p class="text-sm mb-2">Представление списка имеет две вкладки — <strong>«Профили»</strong> и <strong>«Снимки карт»</strong>. Снимок карты — неизменяемая фиксация файловой системы карты: для каждого существующего файла сохраняются путь, символьное имя, тип, размер (или длина/число записей), сырой FCI из ответа SELECT и содержимое, если файл читается (без списка игнорирования и без масок). ICCID декодируется из EF.ICCID и показывается рядом с именем снимка. Захваченное содержимое показывается вместе с декодированным представлением, где есть декодер — таблица полей для transparent-файлов и однострочная сводка по каждой записи для record-файлов. При сканировании измеряется время каждой команды карты (SELECT, READ BINARY, READ RECORD) от отправки до ответа; снимок хранит min/сред/max по каждому типу команд и общее время сканирования, а в представлении эти значения показываются в сводке под заголовком, время select/read — для каждого файла и время чтения — для каждой записи. Время носит информационный характер и не используется при проверках и сравнении.</p>
|
||||
<ul class="list-disc list-inside text-sm space-y-1 mb-3">
|
||||
<li><strong>Новый снимок</strong> — запрашивает имя и сканирует карту, затем возвращает к списку.</li>
|
||||
<li><strong>Импорт снимка</strong> — загружает снимок из JSON-файла.</li>
|
||||
|
||||
+3
-3
@@ -350,7 +350,7 @@
|
||||
<ul class="list-disc list-inside text-sm space-y-1 mb-3">
|
||||
<li><strong>Read</strong> — reads the selected file (auto-detects transparent vs record files)</li>
|
||||
<li><strong>Edit</strong> — modify hex data, <strong>Save</strong> to write back (or <strong>Cancel</strong>)</li>
|
||||
<li><strong>Raw / Decoded</strong> — toggle between hex dump and pySim-decoded JSON</li>
|
||||
<li><strong>Raw / Decoded</strong> — toggle between the hex dump and a decoded field table (client-side EF decoders: IMSI, ICCID, SPN, PLMN lists, LOCI/PSLOCI/EPSLOCI, ADN/MSISDN, service tables, SUME, …); the server-side pySim JSON of the same read stays available in the collapsed <em>pySim JSON (server)</em> disclosure</li>
|
||||
<li><strong>Probe all files</strong> — walks the whole tree (including custom files) and marks every entry present (normal) or absent (red ✗, no expand arrow); empty-but-present DFs show <code class="font-mono text-sm">(empty)</code>. Shows progress <em>N / total</em>, can be stopped, and finishes with a present/absent summary. Files are only verified when expanded or probed — browsing stays lazy.</li>
|
||||
</ul>
|
||||
|
||||
@@ -417,12 +417,12 @@
|
||||
<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. In the report the mismatch fields and FCI comparison columns are labelled <em>expected (profile name)</em> and <em>actual (card ICCID)</em>, and the results header reads <em>Profile verification results for: <profile> → <card ICCID></em>. An <strong>Only mismatches</strong> option in the results header hides all passing files and keeps only failures and errors.</p>
|
||||
<p class="text-sm mb-3"><strong>Check card</strong> runs every rule against the equipped card and shows a live progress line plus a pass/fail report. Each row states exactly what was verified next to the file path (e.g. <em>filetype and size, contents</em> or <em>exact FCI</em>); when some checks pass and others fail, each aspect is marked (<em>filetype ✓, size ✗, contents ✓</em>) with the mismatches detailed below. Mismatched raw data (FCI, contents, record data) is shown as read-only monospace fields — expected above actual, aligned in the same column — for easy comparison; content mismatches of files with a decoder are shown as a decoded per-field comparison instead (only the differing fields, expected and actual side by side; the raw pair is kept when the decoded values match), and FCI mismatches additionally show a decoded per-parameter comparison (file size, file descriptor/structure, life cycle, FID, SFI, proprietary parameters…). A decoded FCI preview is also shown beside the FCI hex field while editing a rule. If the FCI data is corrupt, whatever was decoded before the faulty part is shown together with an explicit decode-failure note. For record files with a contents mismatch, a <em>matching records: 1-5, 7-10</em> note lists the records that did match. In the report the mismatch fields and FCI comparison columns are labelled <em>expected (profile name)</em> and <em>actual (card ICCID)</em>, and the results header reads <em>Profile verification results for: <profile> → <card ICCID></em>. An <strong>Only mismatches</strong> option in the results header hides all passing files and keeps only failures and errors.</p>
|
||||
<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>
|
||||
|
||||
<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>
|
||||
<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. 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>
|
||||
<ul class="list-disc list-inside text-sm space-y-1 mb-3">
|
||||
<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>
|
||||
|
||||
+912
-11
@@ -1305,7 +1305,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 = '2.3.1';
|
||||
const SIMPLE_VERSION = '2.4.0';
|
||||
document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION;
|
||||
|
||||
// ===== Tab switching =====
|
||||
@@ -6838,19 +6838,19 @@ async function pysimFsRead() {
|
||||
const out = document.getElementById('pysim-fs-content');
|
||||
const statusEl = document.getElementById('pysim-fs-status');
|
||||
out.innerHTML = '';
|
||||
statusEl.textContent = 'Loading...';
|
||||
statusEl.textContent = t('Loading...');
|
||||
try {
|
||||
const node = pysimFsFindNode(rawName, pysimFsTreeRoot);
|
||||
const body = pysimFsSelectBody(node || { name: rawName, fid: null });
|
||||
body.mode = pysimFsDecodedMode ? 'decoded' : 'raw';
|
||||
body.mode = 'raw'; // decoding is client-side (works offline in snapshots too)
|
||||
const data = await pysimFetch('/api/read', body);
|
||||
if (!data.success) {
|
||||
statusEl.textContent = 'SW: ' + (data.sw || '?') + ' — ' + (data.error || 'Error');
|
||||
return;
|
||||
}
|
||||
statusEl.textContent = 'SW: ' + data.sw + ' OK';
|
||||
if (data.decoded) {
|
||||
out.innerHTML = '<pre class="text-xs leading-relaxed">' + esc(JSON.stringify(data.decoded, null, 2)) + '</pre>';
|
||||
if (pysimFsDecodedMode) {
|
||||
out.innerHTML = pysimFsDecodedHtml(node || { name: rawName, fid: null }, data);
|
||||
} else if (data.records) {
|
||||
let html = '';
|
||||
for (const r of data.records) {
|
||||
@@ -6869,6 +6869,59 @@ async function pysimFsRead() {
|
||||
}
|
||||
}
|
||||
|
||||
// Decoded view of a read result: label/value fields from the client-side EF
|
||||
// decoders, with the server-side pySim JSON behind a disclosure.
|
||||
function pysimFsDecodedHtml(node, data) {
|
||||
const name = (node && node.name) || null;
|
||||
const fid = (node && node.fid) || (/^[0-9a-fA-F]{4}$/.test(pysimFsSelected || '') ? pysimFsSelected : null);
|
||||
const content = data.records ? { kind: 'record', records: data.records } : { kind: 'transparent', data: data.data };
|
||||
const dec = efDecodeFile(node && node.name, fid, content);
|
||||
let html = '';
|
||||
if (!dec || !dec.fn) {
|
||||
html += '<div class="text-xs text-yellow-600 dark:text-yellow-500 mb-1">' + esc(t('No decoder for this file')) + ' — ' + esc(t('showing raw data')) + '</div>';
|
||||
if (data.records) {
|
||||
for (const r of data.records) {
|
||||
html += '<div class="flex items-center gap-1 mb-1"><span class="text-xs w-8 text-gray-500 dark:text-slate-400 shrink-0">' + r.num + '</span>' +
|
||||
'<input class="flex-1 font-mono text-xs border border-gray-300 dark:border-slate-600 rounded px-2 py-1 bg-white dark:bg-slate-800" value="' + esc(r.data) + '" readonly></div>';
|
||||
}
|
||||
} else {
|
||||
html += '<textarea rows="8" readonly class="w-full font-mono text-xs border-0 bg-transparent resize-none">' + esc(data.data || '') + '</textarea>';
|
||||
}
|
||||
return html;
|
||||
}
|
||||
if (dec.records) {
|
||||
for (const r of dec.records) {
|
||||
html += '<div class="mb-2">' +
|
||||
'<div class="text-xs text-gray-500 dark:text-slate-400">' + esc(t('Record')) + ' ' + r.num + (r.empty ? ' — ' + esc(t('empty')) : '') + '</div>' +
|
||||
efRenderFieldsHtml(efFlatten(r.data)) + '</div>';
|
||||
}
|
||||
} else {
|
||||
if (dec.empty) html += '<div class="text-xs text-gray-500 dark:text-slate-400 mb-1">' + esc(t('empty')) + '</div>';
|
||||
html += efRenderFieldsHtml(efFlatten(dec.data));
|
||||
}
|
||||
html += '<details class="mt-2" ontoggle="if (this.open) pysimFsLoadPySimJson()"><summary class="text-xs text-gray-400 dark:text-slate-500 cursor-pointer">' + esc(t('pySim JSON (server)')) + '</summary>' +
|
||||
'<div id="pysim-fs-pysim-json" class="text-xs font-mono mt-1 text-gray-600 dark:text-slate-400 break-all"></div></details>';
|
||||
return html;
|
||||
}
|
||||
|
||||
async function pysimFsLoadPySimJson() {
|
||||
const el = document.getElementById('pysim-fs-pysim-json');
|
||||
if (!el) return;
|
||||
el.textContent = t('Loading...');
|
||||
try {
|
||||
const rawName = pysimFsSelected;
|
||||
const node = pysimFsFindNode(rawName, pysimFsTreeRoot);
|
||||
const body = pysimFsSelectBody(node || { name: rawName, fid: null });
|
||||
body.mode = 'decoded';
|
||||
const data = await pysimFetch('/api/read', body);
|
||||
if (data && data.decoded) el.textContent = JSON.stringify(data.decoded, null, 1);
|
||||
else if (data && data.data) el.textContent = data.data;
|
||||
else el.textContent = (data && data.error) || t('No response yet');
|
||||
} catch (e) {
|
||||
el.textContent = 'Error: ' + e.message;
|
||||
}
|
||||
}
|
||||
|
||||
let pysimFsEditMode = false;
|
||||
let pysimFsEditData = null;
|
||||
|
||||
@@ -6966,6 +7019,9 @@ function pysimFsSetMode(mode) {
|
||||
el.classList.toggle('text-gray-700', !active);
|
||||
el.classList.toggle('dark:text-slate-300', !active);
|
||||
});
|
||||
// Re-render the open file in the new mode (not while editing: that would
|
||||
// discard unsaved changes).
|
||||
if (pysimFsSelected && !pysimFsEditMode) pysimFsRead();
|
||||
}
|
||||
|
||||
function pysimFsResetEdit() {
|
||||
@@ -10387,6 +10443,7 @@ function profilerRenderSnapshotFiles(s) {
|
||||
'<div class="text-xs font-mono leading-relaxed text-gray-600 dark:text-slate-400 break-all">' + profilerFciPreviewItems(f.fciHex) + '</div></div>';
|
||||
html += '</div>';
|
||||
}
|
||||
const dec = f.content ? efDecodeFile(f.name, efFidFromPath(f.path), f.content) : null;
|
||||
html += '<div class="text-xs text-gray-500 dark:text-slate-400 mt-1 mb-0.5">' + esc(t('Contents')) + '</div>';
|
||||
if (!f.content) {
|
||||
html += '<div class="text-xs text-gray-400 dark:text-slate-500">' + esc(t('Not captured')) + '</div>';
|
||||
@@ -10397,10 +10454,16 @@ function profilerRenderSnapshotFiles(s) {
|
||||
'<input readonly value="' + escHtml(rec.data) + '" class="flex-1 min-w-0 font-mono text-xs border border-gray-300 dark:border-slate-600 rounded px-2 py-1 bg-gray-100 dark:bg-slate-800">' +
|
||||
(rec.ms !== undefined ? '<span class="text-xs text-gray-400 w-16 text-right shrink-0">' + esc(profilerFormatMs(rec.ms)) + '</span>' : '') +
|
||||
'</div>';
|
||||
const dr = dec && dec.records ? dec.records.find(x => x.num === rec.num) : null;
|
||||
const sum = dr && !dr.empty ? efDataSummary(dr.data) : '';
|
||||
if (sum) html += '<div class="text-xs text-gray-500 dark:text-slate-400 ml-10">' + esc(sum) + '</div>';
|
||||
}
|
||||
html += '</div>';
|
||||
} else {
|
||||
html += '<input readonly value="' + escHtml(f.content.data || '') + '" class="w-full min-w-0 font-mono text-xs border border-gray-300 dark:border-slate-600 rounded px-2 py-1 bg-gray-100 dark:bg-slate-800">';
|
||||
if (dec && dec.data && !dec.empty) {
|
||||
html += '<div class="mt-1"><div class="text-xs text-gray-500 dark:text-slate-400 mb-0.5">' + esc(t('Decoded')) + '</div>' + efRenderFieldsHtml(efFlatten(dec.data)) + '</div>';
|
||||
}
|
||||
}
|
||||
html += '</div>';
|
||||
}
|
||||
@@ -11309,14 +11372,29 @@ function profilerRenderReport(results, labels) {
|
||||
if (c.label === 'extra file') {
|
||||
html += '<div class="text-xs text-red-600 mt-1">' + esc(t('extra file')) + '</div>';
|
||||
} else if (profilerRawDataCheck(c)) {
|
||||
const isContent = c.label === 'content' || /^content\.rec\d+$/.test(c.label);
|
||||
const dd = isContent && typeof efContentDiff === 'function'
|
||||
? efContentDiff(r.name, efFidFromPath(r.path), String(c.expected), String(c.actual)) : null;
|
||||
html += '<div class="mt-1">';
|
||||
html += '<div class="text-xs text-red-600 font-mono pl-2">' + esc(c.label) + '</div>';
|
||||
html += '<div class="flex items-center gap-2 mt-0.5 pl-2">' +
|
||||
'<span class="' + labelCls + '">' + esc(expLabel) + '</span>' +
|
||||
'<input readonly title="' + escHtml(String(c.expected)) + '" value="' + escHtml(String(c.expected)) + '" class="flex-1 min-w-0 font-mono text-xs border border-gray-300 dark:border-slate-600 rounded px-2 py-1 bg-gray-100 dark:bg-slate-800"></div>';
|
||||
html += '<div class="flex items-center gap-2 mt-0.5 pl-2">' +
|
||||
'<span class="' + labelCls + '">' + esc(actLabel) + '</span>' +
|
||||
'<input readonly title="' + escHtml(String(c.actual)) + '" value="' + escHtml(String(c.actual)) + '" class="flex-1 min-w-0 font-mono text-xs border border-gray-300 dark:border-slate-600 rounded px-2 py-1 bg-gray-100 dark:bg-slate-800"></div>';
|
||||
if (dd && dd.rows.length) {
|
||||
html += '<div class="text-xs text-gray-500 dark:text-slate-400 pl-2 mt-0.5">' + esc(t('Decoded comparison')) + ' — ' + esc(expLabel) + ' | ' + esc(actLabel) + '</div>';
|
||||
for (const [label, ev, av] of dd.rows) {
|
||||
html += '<div class="flex items-center gap-2 mt-0.5 pl-2">' +
|
||||
'<span class="' + labelCls + '">' + esc(label) + '</span>' +
|
||||
'<input readonly title="' + escHtml(expLabel) + '" value="' + escHtml(ev) + '" class="flex-1 min-w-0 font-mono text-xs border border-gray-300 dark:border-slate-600 rounded px-2 py-1 bg-gray-100 dark:bg-slate-800">' +
|
||||
'<input readonly title="' + escHtml(actLabel) + '" value="' + escHtml(av) + '" class="flex-1 min-w-0 font-mono text-xs border border-gray-300 dark:border-slate-600 rounded px-2 py-1 bg-gray-100 dark:bg-slate-800">' +
|
||||
'</div>';
|
||||
}
|
||||
} else {
|
||||
if (dd && dd.sameDecoded) html += '<div class="text-xs text-gray-500 dark:text-slate-400 pl-2">' + esc(t('Decoded values match; raw bytes differ')) + '</div>';
|
||||
html += '<div class="flex items-center gap-2 mt-0.5 pl-2">' +
|
||||
'<span class="' + labelCls + '">' + esc(expLabel) + '</span>' +
|
||||
'<input readonly title="' + escHtml(String(c.expected)) + '" value="' + escHtml(String(c.expected)) + '" class="flex-1 min-w-0 font-mono text-xs border border-gray-300 dark:border-slate-600 rounded px-2 py-1 bg-gray-100 dark:bg-slate-800"></div>';
|
||||
html += '<div class="flex items-center gap-2 mt-0.5 pl-2">' +
|
||||
'<span class="' + labelCls + '">' + esc(actLabel) + '</span>' +
|
||||
'<input readonly title="' + escHtml(String(c.actual)) + '" value="' + escHtml(String(c.actual)) + '" class="flex-1 min-w-0 font-mono text-xs border border-gray-300 dark:border-slate-600 rounded px-2 py-1 bg-gray-100 dark:bg-slate-800"></div>';
|
||||
}
|
||||
if (c.label === 'fci') html += fcpDiffHtml(String(c.expected), String(c.actual), labels);
|
||||
html += '</div>';
|
||||
} else {
|
||||
@@ -11343,6 +11421,820 @@ document.querySelectorAll('.pysim-subtab').forEach(btn => {
|
||||
});
|
||||
window.addEventListener('resize', pysimFsFitTree);
|
||||
|
||||
const EF_UST_SERVICES = {1: "Local Phone Book", 2: "Fixed Dialling Numbers (FDN)", 3: "Extension 2", 4: "Service Dialling Numbers (SDN)", 5: "Extension3", 6: "Barred Dialling Numbers (BDN)", 7: "Extension4", 8: "Outgoing Call Information (OCI and OCT)", 9: "Incoming Call Information (ICI and ICT)", 10: "Short Message Storage (SMS)", 11: "Short Message Status Reports (SMSR)", 12: "Short Message Service Parameters (SMSP)", 13: "Advice of Charge (AoC)", 14: "Capability Configuration Parameters 2 (CCP2)", 15: "Cell Broadcast Message Identifier", 16: "Cell Broadcast Message Identifier Ranges", 17: "Group Identifier Level 1", 18: "Group Identifier Level 2", 19: "Service Provider Name", 20: "User controlled PLMN selector with Access Technology", 21: "MSISDN", 22: "Image (IMG)", 23: "Support of Localised Service Areas (SoLSA)", 24: "Enhanced Multi-Level Precedence and Pre-emption Service", 25: "Automatic Answer for eMLPP", 26: "RFU", 27: "GSM Access", 28: "Data download via SMS-PP", 29: "Data download via SMS-CB", 30: "Call Control by USIM", 31: "MO-SMS Control by USIM", 32: "RUN AT COMMAND command", 33: "shall be set to 1", 34: "Enabled Services Table", 35: "APN Control List (ACL)", 36: "Depersonalisation Control Keys", 37: "Co-operative Network List", 38: "GSM security context", 39: "CPBCCH Information", 40: "Investigation Scan", 41: "MexE", 42: "Operator controlled PLMN selector with Access Technology", 43: "HPLMN selector with Access Technology", 44: "Extension 5", 45: "PLMN Network Name", 46: "Operator PLMN List", 47: "Mailbox Dialling Numbers", 48: "Message Waiting Indication Status", 49: "Call Forwarding Indication Status", 50: "Reserved and shall be ignored", 51: "Service Provider Display Information", 52: "Multimedia Messaging Service (MMS)", 53: "Extension 8", 54: "Call control on GPRS by USIM", 55: "MMS User Connectivity Parameters", 56: "Network's indication of alerting in the MS (NIA)", 57: "VGCS Group Identifier List (EFVGCS and EFVGCSS)", 58: "VBS Group Identifier List (EFVBS and EFVBSS)", 59: "Pseudonym", 60: "User Controlled PLMN selector for I-WLAN access", 61: "Operator Controlled PLMN selector for I-WLAN access", 62: "User controlled WSID list", 63: "Operator controlled WSID list", 64: "VGCS security", 65: "VBS security", 66: "WLAN Reauthentication Identity", 67: "Multimedia Messages Storage", 68: "Generic Bootstrapping Architecture (GBA)", 69: "MBMS security", 70: "Data download via USSD and USSD application mode", 71: "Equivalent HPLMN", 72: "Additional TERMINAL PROFILE after UICC activation", 73: "Equivalent HPLMN Presentation Indication", 74: "Last RPLMN Selection Indication", 75: "OMA BCAST Smart Card Profile", 76: "GBA-based Local Key Establishment Mechanism", 77: "Terminal Applications", 78: "Service Provider Name Icon", 79: "PLMN Network Name Icon", 80: "Connectivity Parameters for USIM IP connections", 81: "Home I-WLAN Specific Identifier List", 82: "I-WLAN Equivalent HPLMN Presentation Indication", 83: "I-WLAN HPLMN Priority Indication", 84: "I-WLAN Last Registered PLMN", 85: "EPS Mobility Management Information", 86: "Allowed CSG Lists and corresponding indications", 87: "Call control on EPS PDN connection by USIM", 88: "HPLMN Direct Access", 89: "eCall Data", 90: "Operator CSG Lists and corresponding indications", 91: "Support for SM-over-IP", 92: "Support of CSG Display Control", 93: "Communication Control for IMS by USIM", 94: "Extended Terminal Applications", 95: "Support of UICC access to IMS", 96: "Non-Access Stratum configuration by USIM", 97: "PWS configuration by USIM", 98: "RFU", 99: "URI support by UICC", 100: "Extended EARFCN support", 101: "ProSe", 102: "USAT Application Pairing", 103: "Media Type support", 104: "IMS call disconnection cause", 105: "URI support for MO SHORT MESSAGE CONTROL", 106: "ePDG configuration Information support", 107: "ePDG configuration Information configured", 108: "ACDC support", 109: "MCPTT", 110: "ePDG configuration Information for Emergency Service support", 111: "ePDG configuration Information for Emergency Service configured", 112: "eCall Data over IMS", 113: "URI support for SMS-PP DOWNLOAD as defined in 3GPP TS 31.111 [12]", 114: "From Preferred", 115: "IMS configuration data", 116: "TV configuration", 117: "3GPP PS Data Off", 118: "3GPP PS Data Off Service List", 119: "V2X", 120: "XCAP Configuration Data", 121: "EARFCN list for MTC/NB-IOT UEs", 122: "5GS Mobility Management Information", 123: "5G Security Parameters", 124: "Subscription identifier privacy support", 125: "SUCI calculation by the USIM", 126: "UAC Access Identities support", 127: "Expect control plane-based Steering of Roaming information during initial registration in VPLMN", 128: "Call control on PDU Session by USIM", 129: "5GS Operator PLMN List", 130: "Support for SUPI of type NSI or GLI or GCI", 131: "3GPP PS Data Off separate Home and Roaming lists", 132: "Support for URSP by USIM", 133: "5G Security Parameters extended", 134: "MuD and MiD configuration data", 135: Line truncated
|
||||
const EF_SST_SERVICES = {1: "CHV1 disable function", 2: "Abbreviated Dialling Numbers (ADN)", 3: "Fixed Dialling Numbers (FDN)", 4: "Short Message Storage (SMS)", 5: "Advice of Charge (AoC)", 6: "Capability Configuration Parameters (CCP)", 7: "PLMN selector", 8: "RFU", 9: "MSISDN", 10: "Extension1", 11: "Extension2", 12: "SMS Parameters", 13: "Last Number Dialled (LND)", 14: "Cell Broadcast Message Identifier", 15: "Group Identifier Level 1", 16: "Group Identifier Level 2", 17: "Service Provider Name", 18: "Service Dialling Numbers (SDN)", 19: "Extension3", 20: "RFU", 21: "VGCS Group Identifier List (EFVGCS and EFVGCSS)", 22: "VBS Group Identifier List (EFVBS and EFVBSS)", 23: "enhanced Multi-Level Precedence and Pre-emption Service", 24: "Automatic Answer for eMLPP", 25: "Data download via SMS-CB", 26: "Data download via SMS-PP", 27: "Menu selection", 28: "Call control", 29: "Proactive SIM", 30: "Cell Broadcast Message Identifier Ranges", 31: "Barred Dialling Numbers (BDN)", 32: "Extension4", 33: "De-personalization Control Keys", 34: "Co-operative Network List", 35: "Short Message Status Reports", 36: "Network's indication of alerting in the MS", 37: "Mobile Originated Short Message control by SIM", 38: "GPRS", 39: "Image (IMG)", 40: "SoLSA (Support of Local Service Area)", 41: "USSD string data object supported in Call Control", 42: "RUN AT COMMAND command", 43: "User controlled PLMN Selector with Access Technology", 44: "Operator controlled PLMN Selector with Access Technology", 45: "HPLMN Selector with Access Technology", 46: "CPBCCH Information", 47: "Investigation Scan", 48: "Extended Capability Configuration Parameters", 49: "MExE", 50: "Reserved and shall be ignored", 51: "PLMN Network Name", 52: "Operator PLMN List", 53: "Mailbox Dialling Numbers", 54: "Message Waiting Indication Status", 55: "Call Forwarding Indication Status", 56: "Service Provider Display Information", 57: "Multimedia Messaging Service (MMS)", 58: "Extension 8", 59: "MMS User Connectivity Parameters"};
|
||||
const EF_IST_SERVICES = {1: "P-CSCF address", 2: "Generic Bootstrapping Architecture (GBA)", 3: "HTTP Digest", 4: "GBA-based Local Key Establishment Mechanism", 5: "Support of P-CSCF discovery for IMS Local Break Out", 6: "Short Message Storage (SMS)", 7: "Short Message Status Reports (SMSR)", 8: "Support for SM-over-IP including data download via SMS-PP as defined in TS 31.111 [31]", 9: "Communication Control for IMS by ISIM", 10: "Support of UICC access to IMS", 11: "URI support by UICC", 12: "Media Type support", 13: "IMS call disconnection cause", 14: "URI support for MO SHORT MESSAGE CONTROL", 15: "MCPTT", 16: "URI support for SMS-PP DOWNLOAD as defined in 3GPP TS 31.111 [31]", 17: "From Preferred", 18: "IMS configuration data", 19: "XCAP Configuration Data", 20: "WebRTC URI", 21: "MuD and MiD configuration data", 22: "IMS Data Channel indication"};
|
||||
const EF_PHASE_NAMES = {0: "phase 1", 2: "phase 2", 3: "phase 2 and higher"};
|
||||
|
||||
// ===== EF content decoders (client-side, offline) =====
|
||||
// Decode the contents of common EFs for the file manager, the snapshot view,
|
||||
// profile-check results and snapshot comparison. Ported/adapted from
|
||||
// simtrace-analyser, which sourced the formats from the pinned specs
|
||||
// (TS 51.011, TS 31.102, TS 31.103, TS 102 221 Annex A, TS 24.008) and pySim;
|
||||
// pySim's server-side read_*_decoded stays available as a cross-check.
|
||||
// Decoders are pure functions over byte arrays returning plain objects;
|
||||
// efFlatten() turns a decoded object into [label, value] rows for display and
|
||||
// efDiffData() into per-field diff rows for the report.
|
||||
|
||||
const EF_FIELD_LABELS = {
|
||||
imsi: 'IMSI', iccid: 'ICCID', languages: 'Languages', name: 'Name', number: 'Number',
|
||||
text: 'Text', title: 'Title', icon: 'Icon', mcc: 'MCC', mnc: 'MNC', lac: 'LAC',
|
||||
tac: 'TAC', rac: 'RAC', tmsi: 'TMSI', p_tmsi: 'P-TMSI', guti: 'GUTI', ksi: 'KSI',
|
||||
kc: 'Kc', acm_max: 'ACM max', apns: 'APNs', access_classes: 'Access classes',
|
||||
op_mode: 'Operation mode', test_mode: 'Test mode', additional_info: 'Additional info',
|
||||
mnc_length: 'MNC length', services: 'Services', plmns: 'PLMNs', applications: 'Applications',
|
||||
aid: 'AID', label: 'Label', eap: 'EAP', message_ids: 'Message IDs', ranges: 'Ranges',
|
||||
esc: 'Emergency category', param_indicators: 'Parameter indicators', tp_da: 'TP-DA',
|
||||
sca: 'SCA', tp_pid: 'TP-PID', tp_dcs: 'TP-DCS', tp_vp: 'TP-VP', alpha: 'Alpha',
|
||||
direction: 'Direction', status: 'Status', tpdu: 'TPDU', display_condition: 'Display condition',
|
||||
rfu: 'RFU', ccp: 'CCP', ext: 'Extension', tag: 'Tag', hex: 'Hex', lac_range: 'LAC range',
|
||||
pnn_record: 'PNN record', pnn_record_id: 'PNN record', mcc_mnc: 'MCC/MNC', full: 'Full name',
|
||||
short: 'Short name', eap_types: 'EAP types', dfs: 'DFs', uplink_nas_count: 'Uplink NAS count',
|
||||
downlink_nas_count: 'Downlink NAS count', nas_algorithms: 'NAS algorithms', k_asme: 'K-ASME',
|
||||
ksi_asme: 'KSI-ASME', tmsi_time: 'TMSI time', lac_range_low: 'LAC range low',
|
||||
lac_range_high: 'LAC range high', show_in_hplmn: 'Show in HPLMN', hide_in_oplmn: 'Hide in OPLMN',
|
||||
location_update_status: 'Location update status', eps_update_status: 'EPS update status',
|
||||
update_status: 'Update status', access_tech: 'Access technology', p_tmsi_signature: 'P-TMSI signature',
|
||||
};
|
||||
|
||||
const EF_GSM7_EXT = { 0x0A: '\n', 0x14: '^', 0x28: '{', 0x29: '}', 0x2F: '\\', 0x3C: '[', 0x3D: '~', 0x3E: ']', 0x40: '|', 0x65: '\u20ac' };
|
||||
|
||||
function efBytes(hex) {
|
||||
const n = (hex || '').length >> 1;
|
||||
const out = new Uint8Array(n);
|
||||
for (let i = 0; i < n; i++) out[i] = parseInt(hex.substr(i * 2, 2), 16);
|
||||
return out;
|
||||
}
|
||||
|
||||
function efHex(b) {
|
||||
let s = '';
|
||||
for (let i = 0; i < b.length; i++) s += b[i].toString(16).padStart(2, '0').toUpperCase();
|
||||
return s;
|
||||
}
|
||||
|
||||
function efSwapNibbles(hex) {
|
||||
let s = '';
|
||||
for (let i = 0; i + 1 < hex.length; i += 2) s += hex[i + 1] + hex[i];
|
||||
return s;
|
||||
}
|
||||
|
||||
function efAllFf(b) {
|
||||
if (!b.length) return true;
|
||||
for (let i = 0; i < b.length; i++) if (b[i] !== 0xFF) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
function efRstripFf(b) {
|
||||
let end = b.length;
|
||||
while (end > 0 && b[end - 1] === 0xFF) end--;
|
||||
return b.subarray(0, end);
|
||||
}
|
||||
|
||||
function efTlv(data) {
|
||||
const out = [];
|
||||
let i = 0;
|
||||
while (i + 2 <= data.length) {
|
||||
const tag = data[i]; i += 1;
|
||||
let len = data[i]; i += 1;
|
||||
if (len & 0x80) {
|
||||
const n = len & 0x7F;
|
||||
if (n === 0 || i + n > data.length) break;
|
||||
len = 0;
|
||||
for (let k = 0; k < n; k++) len = (len << 8) | data[i++];
|
||||
}
|
||||
if (i + len > data.length) break;
|
||||
out.push({ tag: tag, value: data.slice(i, i + len) });
|
||||
i += len;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function efUcs2Be(b) {
|
||||
let end = b.length - (b.length % 2);
|
||||
let s = '';
|
||||
for (let i = 0; i + 1 < end; i += 2) {
|
||||
const u = (b[i] << 8) | b[i + 1];
|
||||
if (u === 0) break;
|
||||
s += String.fromCharCode(u);
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
function efGsm7Octets(b) {
|
||||
let s = '';
|
||||
for (let i = 0; i < b.length; i++) {
|
||||
const v = b[i];
|
||||
if (v === 0xFF) continue;
|
||||
s += v < GSM7_ALPHABET.length ? GSM7_ALPHABET[v] : '?';
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
// TS 102 221 Annex A / TS 24.008 text string (GsmOrUcs2 variants).
|
||||
function efAnnexA(b) {
|
||||
if (!b.length || efAllFf(b)) return '';
|
||||
const first = b[0];
|
||||
if (first === 0x80) return efUcs2Be(b.subarray(1));
|
||||
if (first === 0x90) {
|
||||
let d = b.subarray(1);
|
||||
for (let i = 0; i + 1 < d.length; i += 2) {
|
||||
if (d[i] === 0 && d[i + 1] === 0) { d = d.subarray(0, i); break; }
|
||||
}
|
||||
return efUcs2Be(d);
|
||||
}
|
||||
if (first === 0x81 && b.length >= 3) {
|
||||
const num = b[1], base = b[2] << 7;
|
||||
let s = '';
|
||||
for (let i = 0; i < num && 3 + i < b.length; i++) {
|
||||
const ch = b[3 + i];
|
||||
s += (ch & 0x80) ? String.fromCharCode((ch & 0x7F) + base)
|
||||
: (ch < GSM7_ALPHABET.length ? GSM7_ALPHABET[ch] : '?');
|
||||
}
|
||||
return s;
|
||||
}
|
||||
if (first === 0x82 && b.length >= 4) {
|
||||
const num = b[1], base = (b[2] << 8) | b[3];
|
||||
let s = '';
|
||||
for (let i = 0; i < num && 4 + i < b.length; i++) {
|
||||
const ch = b[4 + i];
|
||||
s += (ch & 0x80) ? String.fromCharCode((ch & 0x7F) + base)
|
||||
: (ch < GSM7_ALPHABET.length ? GSM7_ALPHABET[ch] : '?');
|
||||
}
|
||||
return s;
|
||||
}
|
||||
if (first >= 0x83 && first <= 0x87) {
|
||||
const d = efRstripFf(b.subarray(1));
|
||||
return d.length ? gsm7Decode(d) : '';
|
||||
}
|
||||
if (first & 0x80) {
|
||||
const u = b.slice();
|
||||
u[0] &= 0x7F;
|
||||
let end = u.length;
|
||||
while (end >= 2 && u[end - 1] === 0xFF && u[end - 2] === 0xFF) end -= 2;
|
||||
return efUcs2Be(u.subarray(0, end));
|
||||
}
|
||||
const d = efRstripFf(b);
|
||||
let allLow = d.length > 0;
|
||||
for (let i = 0; i < d.length; i++) if (d[i] >= 0x80) { allLow = false; break; }
|
||||
return allLow ? efGsm7Octets(d) : gsm7Decode(d);
|
||||
}
|
||||
|
||||
// [TON/NPI byte + BCD digits] -> dialling string.
|
||||
function efBcdAddress(data) {
|
||||
if (!data.length) return '';
|
||||
const ton = (data[0] >> 4) & 0x07;
|
||||
const digits = [];
|
||||
for (let i = 1; i < data.length; i++) {
|
||||
const lo = data[i] & 0x0F, hi = (data[i] >> 4) & 0x0F;
|
||||
if (lo <= 9) digits.push(String(lo)); else if (lo === 0x0F) break;
|
||||
if (hi <= 9) digits.push(String(hi)); else if (hi === 0x0F) break;
|
||||
}
|
||||
const num = digits.join('');
|
||||
return ton === 1 ? '+' + num : num;
|
||||
}
|
||||
|
||||
// [length octet (counts the following octets) + TON/NPI + BCD digits],
|
||||
// as used by the SMSP TP-DA/SCA fields and TPDU addresses (TS 24.008).
|
||||
function efBcdAddressLen(data) {
|
||||
if (data.length < 2) return '';
|
||||
const n = data[0];
|
||||
const ton = (data[1] >> 4) & 0x07;
|
||||
const bcd = data.subarray(2, Math.min(data.length, 1 + n));
|
||||
const digits = efSwapNibbles(efHex(bcd)).replace(/[fF]+$/, '');
|
||||
return ton === 1 ? '+' + digits : digits;
|
||||
}
|
||||
|
||||
// 3-byte PLMN (TS 24.008) -> {mcc, mnc}
|
||||
function efPlmn(b) {
|
||||
if (b.length < 3) return null;
|
||||
const h = efHex(b.subarray(0, 3));
|
||||
const mcc = (h[1] + h[0] + h[3]).replace(/F+$/, '') || '0';
|
||||
const mnc = (h[5] + h[4] + h[2]).replace(/F+$/, '') || '0';
|
||||
return { mcc: mcc, mnc: mnc };
|
||||
}
|
||||
|
||||
function efPlmnAct(u16) {
|
||||
const techs = new Set();
|
||||
if (u16 & 0x8000) techs.add('UTRAN');
|
||||
if (u16 & 0x0800) techs.add('NG-RAN');
|
||||
if (u16 & 0x0040) techs.add('GSM COMPACT');
|
||||
if (u16 & 0x0020) techs.add('cdma2000 HRPD');
|
||||
if (u16 & 0x0010) techs.add('cdma2000 1xRTT');
|
||||
const e = u16 & 0x7000;
|
||||
if (e === 0x4000 || e === 0x7000) { techs.add('E-UTRAN WB-S1'); techs.add('E-UTRAN NB-S1'); }
|
||||
else if (e === 0x5000) techs.add('E-UTRAN NB-S1');
|
||||
else if (e === 0x6000) techs.add('E-UTRAN WB-S1');
|
||||
const g = u16 & 0x008C;
|
||||
if (g === 0x0080 || g === 0x008C) { techs.add('GSM'); techs.add('EC-GSM-IoT'); }
|
||||
else if (g === 0x0084) techs.add('GSM');
|
||||
else if (g === 0x0086) techs.add('EC-GSM-IoT');
|
||||
return [...techs].sort();
|
||||
}
|
||||
|
||||
// TS 23.038 Network Name (DCS byte + text) from EF_PNN.
|
||||
function efPnnText(value) {
|
||||
if (value.length < 2) return '';
|
||||
const dcs = value[0];
|
||||
const coding = (dcs >> 4) & 0x07;
|
||||
let data = efRstripFf(value.subarray(1));
|
||||
if (coding === 1) {
|
||||
for (let i = 0; i + 1 < data.length; i += 2) {
|
||||
if (data[i] === 0 && data[i + 1] === 0) { data = data.subarray(0, i); break; }
|
||||
}
|
||||
return efUcs2Be(data);
|
||||
}
|
||||
if (coding === 0) {
|
||||
let allLow = data.length > 0;
|
||||
for (let i = 0; i < data.length; i++) if (data[i] >= 0x80) { allLow = false; break; }
|
||||
return allLow ? efGsm7Octets(data) : gsm7Decode(data);
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
// ---- decoders ----
|
||||
|
||||
function efDecIccid(b) {
|
||||
if (efAllFf(b)) return { empty: true };
|
||||
const digits = efSwapNibbles(efHex(b)).replace(/[fF]+$/, '');
|
||||
return { iccid: digits || null };
|
||||
}
|
||||
|
||||
function efDecImsi(b) {
|
||||
if (efAllFf(b)) return { empty: true };
|
||||
if (b.length <= 2) return efDecServiceTable(b, EF_IST_SERVICES); // 6F07 is also EF.IST
|
||||
const digits = efSwapNibbles(efHex(b.subarray(1))).replace(/[fF]+$/, '');
|
||||
// First nibble is the mobile-identity type + parity indicator (TS 24.008).
|
||||
return { imsi: digits.slice(1) || null };
|
||||
}
|
||||
|
||||
function efDecLi(b) {
|
||||
const codes = [];
|
||||
for (let i = 0; i + 1 < b.length; i += 2) {
|
||||
if (b[i] === 0xFF && b[i + 1] === 0xFF) continue;
|
||||
try {
|
||||
const c = String.fromCharCode(b[i]) + String.fromCharCode(b[i + 1]);
|
||||
if (/^[A-Za-z]{2}$/.test(c)) codes.push(c);
|
||||
} catch (e) {}
|
||||
}
|
||||
return { languages: codes };
|
||||
}
|
||||
|
||||
function efDecServiceTable(b, table) {
|
||||
const services = [];
|
||||
for (let i = 0; i < b.length; i++) {
|
||||
for (let bit = 0; bit < 8; bit++) {
|
||||
if (b[i] & (1 << bit)) {
|
||||
const n = i * 8 + bit + 1;
|
||||
services.push({ n: n, name: table[n] || 'RFU' });
|
||||
}
|
||||
}
|
||||
}
|
||||
return { services: services };
|
||||
}
|
||||
|
||||
function efDecPlmnList(b) {
|
||||
const plmns = [];
|
||||
for (let i = 0; i + 2 < b.length; i += 3) {
|
||||
const chunk = b.subarray(i, i + 3);
|
||||
if (efAllFf(chunk)) break;
|
||||
const p = efPlmn(chunk);
|
||||
if (p) plmns.push(p);
|
||||
}
|
||||
return { plmns: plmns };
|
||||
}
|
||||
|
||||
function efDecPlmnWact(b) {
|
||||
const plmns = [];
|
||||
for (let i = 0; i + 4 < b.length; i += 5) {
|
||||
const chunk = b.subarray(i, i + 5);
|
||||
if (efAllFf(chunk.subarray(0, 3))) break;
|
||||
const p = efPlmn(chunk.subarray(0, 3));
|
||||
if (!p) continue;
|
||||
const act = (chunk[3] << 8) | chunk[4];
|
||||
const techs = efPlmnAct(act);
|
||||
p.access_tech = techs.length ? techs.join(', ') : '0x' + act.toString(16).toUpperCase().padStart(4, '0');
|
||||
plmns.push(p);
|
||||
}
|
||||
return { plmns: plmns };
|
||||
}
|
||||
|
||||
function efDecSpn(b) {
|
||||
if (b.length < 2) return { raw: efHex(b) };
|
||||
const cond = b[0];
|
||||
const out = { show_in_hplmn: !!(cond & 0x01), hide_in_oplmn: !!(cond & 0x02) };
|
||||
const text = efAnnexA(efRstripFf(b.subarray(1)));
|
||||
if (text) out.name = text;
|
||||
return out;
|
||||
}
|
||||
|
||||
function efDecLoci(b) {
|
||||
if (b.length < 11) return { raw: efHex(b) };
|
||||
const tmsi = b.subarray(0, 4), lai = b.subarray(4, 9);
|
||||
const p = efPlmn(lai.subarray(0, 3)) || {};
|
||||
return {
|
||||
tmsi: efAllFf(tmsi) ? null : efHex(tmsi),
|
||||
mcc: p.mcc, mnc: p.mnc,
|
||||
lac: '0x' + efHex(lai.subarray(3, 5)),
|
||||
tmsi_time: '0x' + efHex(b.subarray(9, 10)),
|
||||
location_update_status: '0x' + efHex(b.subarray(10, 11)),
|
||||
};
|
||||
}
|
||||
|
||||
function efDecPsLoci(b) {
|
||||
if (efAllFf(b)) return { empty: true };
|
||||
if (b.length < 14) return { raw: efHex(b) };
|
||||
const rai = b.subarray(7, 13);
|
||||
const p = efPlmn(rai.subarray(0, 3)) || {};
|
||||
return {
|
||||
p_tmsi: efAllFf(b.subarray(0, 4)) ? null : efHex(b.subarray(0, 4)),
|
||||
p_tmsi_signature: efHex(b.subarray(4, 7)),
|
||||
mcc: p.mcc, mnc: p.mnc,
|
||||
lac: '0x' + efHex(rai.subarray(3, 5)),
|
||||
rac: '0x' + efHex(rai.subarray(5, 6)),
|
||||
update_status: '0x' + efHex(b.subarray(13, 14)),
|
||||
};
|
||||
}
|
||||
|
||||
function efDecEpsLoci(b) {
|
||||
if (efAllFf(b)) return { empty: true };
|
||||
if (b.length < 18) return { raw: efHex(b) };
|
||||
const tai = b.subarray(12, 17);
|
||||
const p = efPlmn(tai.subarray(0, 3)) || {};
|
||||
return {
|
||||
guti: efAllFf(b.subarray(0, 12)) ? null : efHex(b.subarray(0, 12)),
|
||||
mcc: p.mcc, mnc: p.mnc,
|
||||
tac: '0x' + efHex(tai.subarray(3, 5)),
|
||||
eps_update_status: '0x' + efHex(b.subarray(17, 18)),
|
||||
};
|
||||
}
|
||||
|
||||
function efDecEpsNsc(b) {
|
||||
if (!b.length || efAllFf(b)) return { empty: true };
|
||||
let inner = b;
|
||||
for (const t of efTlv(b)) if (t.tag === 0xA0) { inner = t.value; break; }
|
||||
const out = {};
|
||||
for (const t of efTlv(inner)) {
|
||||
if (t.tag === 0x80) out.ksi_asme = efHex(t.value);
|
||||
else if (t.tag === 0x81) out.k_asme = efHex(t.value);
|
||||
else if (t.tag === 0x82) out.uplink_nas_count = parseInt(efHex(t.value) || '0', 16);
|
||||
else if (t.tag === 0x83) out.downlink_nas_count = parseInt(efHex(t.value) || '0', 16);
|
||||
else if (t.tag === 0x84) out.nas_algorithms = efHex(t.value);
|
||||
}
|
||||
return Object.keys(out).length ? out : { raw: efHex(b) };
|
||||
}
|
||||
|
||||
function efDecKc(b) {
|
||||
if (efAllFf(b)) return { empty: true };
|
||||
const out = {};
|
||||
if (b.length >= 8) out.kc = efHex(b.subarray(0, 8));
|
||||
if (b.length >= 9) out.ksi = (b[8] >> 4) & 0x0F;
|
||||
return Object.keys(out).length ? out : { raw: efHex(b) };
|
||||
}
|
||||
|
||||
function efDecAcc(b) {
|
||||
if (b.length < 2) return { raw: efHex(b) };
|
||||
const val = (b[0] << 8) | b[1];
|
||||
const classes = [];
|
||||
for (let i = 0; i < 16; i++) if (val & (0x8000 >> i)) classes.push(i);
|
||||
return { access_classes: classes };
|
||||
}
|
||||
|
||||
function efDecPhase(b) {
|
||||
const v = b.length ? b[0] : 0;
|
||||
return { phase: EF_PHASE_NAMES[v] || '0x' + v.toString(16).toUpperCase().padStart(2, '0') };
|
||||
}
|
||||
|
||||
function efDecCbmi(b) {
|
||||
const ids = [];
|
||||
for (let i = 0; i + 1 < b.length; i += 2) {
|
||||
const v = (b[i] << 8) | b[i + 1];
|
||||
if (v !== 0xFFFF) ids.push(v);
|
||||
}
|
||||
return { message_ids: ids };
|
||||
}
|
||||
|
||||
function efDecCbmir(b) {
|
||||
const ranges = [];
|
||||
for (let i = 0; i + 3 < b.length; i += 4) {
|
||||
const rec = b.subarray(i, i + 4);
|
||||
if (efAllFf(rec)) continue;
|
||||
ranges.push([efHex(rec.subarray(0, 2)), efHex(rec.subarray(2, 4))]);
|
||||
}
|
||||
return ranges.length ? { ranges: ranges } : { raw: efHex(b) };
|
||||
}
|
||||
|
||||
function efDecEcc(b) {
|
||||
if (efAllFf(b)) return { empty: true };
|
||||
const recs = [];
|
||||
for (let i = 0; i + 2 < b.length; i += 3) {
|
||||
const rec = b.subarray(i, i + 3);
|
||||
if (efAllFf(rec)) continue;
|
||||
const digits = efSwapNibbles(efHex(rec.subarray(0, 2))).replace(/[fF]+$/, '');
|
||||
recs.push({ number: digits || null, esc: '0x' + efHex(rec.subarray(2, 3)) });
|
||||
}
|
||||
if (!recs.length) return { raw: efHex(b) };
|
||||
return recs.length === 1 ? recs[0] : { numbers: recs };
|
||||
}
|
||||
|
||||
function efDecOpl(b) {
|
||||
if (efAllFf(b)) return { empty: true };
|
||||
const recs = [];
|
||||
for (let i = 0; i + 7 < b.length; i += 8) {
|
||||
const rec = b.subarray(i, i + 8);
|
||||
if (efAllFf(rec)) continue;
|
||||
const p = efPlmn(rec.subarray(0, 3)) || {};
|
||||
recs.push({
|
||||
mcc: p.mcc, mnc: p.mnc,
|
||||
lac_range: [efHex(rec.subarray(3, 5)), efHex(rec.subarray(5, 7))],
|
||||
pnn_record: rec[7],
|
||||
});
|
||||
}
|
||||
if (!recs.length) return { raw: efHex(b) };
|
||||
return recs.length === 1 ? recs[0] : { records: recs };
|
||||
}
|
||||
|
||||
function efDecAd(b) {
|
||||
if (efAllFf(b)) return { empty: true };
|
||||
const op = b[0] & 0x03;
|
||||
const out = {
|
||||
op_mode: { 0: 'normal operation', 1: 'specific features' }[op] || '0x' + op.toString(16).padStart(2, '0'),
|
||||
test_mode: !!(b[0] & 0x04),
|
||||
};
|
||||
if (b.length >= 3) out.additional_info = efHex(b.subarray(1, 3));
|
||||
if (b.length >= 4 && b[3] !== 0xFF) out.mnc_length = b[3];
|
||||
return out;
|
||||
}
|
||||
|
||||
function efDecAcl(b) {
|
||||
if (efAllFf(b)) return { empty: true };
|
||||
const apns = [];
|
||||
for (const t of efTlv(b.subarray(1))) {
|
||||
if (t.tag === 0xDD) apns.push(t.value.length ? String.fromCharCode.apply(null, t.value) : '(network provided)');
|
||||
}
|
||||
return apns.length ? { apns: apns } : { raw: efHex(b) };
|
||||
}
|
||||
|
||||
function efDecSmsp(b) {
|
||||
if (efAllFf(b)) return { empty: true };
|
||||
if (b.length < 28) return { raw: efHex(b) };
|
||||
const y = b.length - 28;
|
||||
const out = {};
|
||||
const alpha = efAnnexA(efRstripFf(b.subarray(0, y)));
|
||||
if (alpha) out.alpha = alpha;
|
||||
const pi = b[y];
|
||||
const flags = [];
|
||||
out.param_indicators = '0x' + pi.toString(16).toUpperCase().padStart(2, '0');
|
||||
if (!(pi & 0x01)) out.tp_da = efBcdAddressLen(b.subarray(y + 1, y + 13));
|
||||
if (!(pi & 0x10)) out.sca = efBcdAddressLen(b.subarray(y + 13, y + 25));
|
||||
if (!(pi & 0x02)) out.tp_pid = '0x' + b[y + 25].toString(16).toUpperCase().padStart(2, '0');
|
||||
if (!(pi & 0x04)) out.tp_dcs = '0x' + b[y + 26].toString(16).toUpperCase().padStart(2, '0');
|
||||
if (!(pi & 0x08)) out.tp_vp = '0x' + b[y + 27].toString(16).toUpperCase().padStart(2, '0');
|
||||
return out;
|
||||
}
|
||||
|
||||
function efDecSpdi(b) {
|
||||
if (efAllFf(b)) return { empty: true };
|
||||
const plmns = [];
|
||||
const collect = value => {
|
||||
for (let i = 0; i + 2 < value.length; i += 3) {
|
||||
const p = efPlmn(value.subarray(i, i + 3));
|
||||
if (p && !plmns.some(x => x.mcc === p.mcc && x.mnc === p.mnc)) plmns.push(p);
|
||||
}
|
||||
};
|
||||
for (const t of efTlv(b)) {
|
||||
if (t.tag === 0xA3) {
|
||||
let got = false;
|
||||
for (const t2 of efTlv(t.value)) if (t2.tag === 0x80) { collect(t2.value); got = true; }
|
||||
if (!got) collect(t.value);
|
||||
} else if (t.tag === 0x80) collect(t.value);
|
||||
}
|
||||
return plmns.length ? { plmns: plmns } : { raw: efHex(b) };
|
||||
}
|
||||
|
||||
function efDecNai(b) {
|
||||
if (!b.length || efAllFf(b)) return { empty: true };
|
||||
const data = efRstripFf(b);
|
||||
if (!data.length || ((data[0] === 0x80 || data[0] === 0x81) && data.length <= 2)) return { empty: true };
|
||||
const texts = [];
|
||||
for (const t of efTlv(data)) {
|
||||
if ((t.tag === 0x80 || t.tag === 0x81) && t.value.length) {
|
||||
try { texts.push(new TextDecoder('utf-8').decode(t.value)); } catch (e) {}
|
||||
}
|
||||
}
|
||||
return texts.length ? { text: texts.length === 1 ? texts[0] : texts.join(', ') } : { raw: efHex(b) };
|
||||
}
|
||||
|
||||
function efDecDir(b) {
|
||||
const apps = [];
|
||||
for (const t of efTlv(b)) {
|
||||
if (t.tag !== 0x61) continue;
|
||||
const app = {};
|
||||
for (const t2 of efTlv(t.value)) {
|
||||
if (t2.tag === 0x4F) app.aid = efHex(t2.value);
|
||||
else if (t2.tag === 0x50) { const l = efAnnexA(t2.value); if (l) app.label = l; }
|
||||
else if (t2.tag === 0x73) {
|
||||
for (const t3 of efTlv(t2.value)) {
|
||||
if (t3.tag !== 0xA0) continue;
|
||||
const eap = {};
|
||||
for (const t4 of efTlv(t3.value)) {
|
||||
if (t4.tag === 0x80) eap.eap_types = Array.prototype.map.call(t4.value, x => ({ 18: 'EAP-SIM', 23: 'EAP-AKA', 50: 'EAP-AKA\'' }[x] || x));
|
||||
else if (t4.tag === 0x81) { eap.dfs = []; for (let i = 0; i + 1 < t4.value.length; i += 2) eap.dfs.push(efHex(t4.value.subarray(i, i + 2))); }
|
||||
else if (t4.tag === 0x82) eap.label = String.fromCharCode.apply(null, t4.value);
|
||||
}
|
||||
app.eap = eap;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (Object.keys(app).length) apps.push(app);
|
||||
}
|
||||
return apps.length ? { applications: apps } : { raw: efHex(b) };
|
||||
}
|
||||
|
||||
function efDecArr(b) {
|
||||
const rules = [];
|
||||
for (const t of efTlv(b)) rules.push({ tag: '0x' + t.tag.toString(16).toUpperCase().padStart(2, '0'), hex: efHex(t.value) });
|
||||
return rules.length ? { rules: rules } : { raw: efHex(b) };
|
||||
}
|
||||
|
||||
function efDecPnn(b) {
|
||||
if (efAllFf(b)) return { empty: true };
|
||||
const out = {};
|
||||
for (const t of efTlv(b)) {
|
||||
if (t.tag === 0x43) out.full = efPnnText(t.value);
|
||||
else if (t.tag === 0x45) out.short = efPnnText(t.value);
|
||||
}
|
||||
return Object.keys(out).length ? out : { raw: efHex(b) };
|
||||
}
|
||||
|
||||
// ADN-format record: alpha + len(1) + TON/NPI(1) + BCD(10) + CCP(1) + EXT1(1).
|
||||
function efDecAdn(b) {
|
||||
if (efAllFf(b)) return { empty: true };
|
||||
if (b.length < 14) return { raw: efHex(b) };
|
||||
const alpha = efRstripFf(b.subarray(0, b.length - 14));
|
||||
const name = efAnnexA(alpha);
|
||||
const number = efBcdAddress(b.subarray(b.length - 13, b.length - 2));
|
||||
const out = { name: name, number: number };
|
||||
if (b[b.length - 2] !== 0xFF) out.ccp = b[b.length - 2];
|
||||
if (b[b.length - 1] !== 0xFF) out.ext1 = b[b.length - 1];
|
||||
return out;
|
||||
}
|
||||
|
||||
// TS 51.011 §10.5.10 EF_EXT1: record type(1) + extension data(11) + identifier(1).
|
||||
// Additional data (type bit 2): count byte + BCD remainder; called party
|
||||
// subaddress (type bit 1): TS 24.008 subaddress data.
|
||||
function efDecExt1(b) {
|
||||
if (efAllFf(b)) return { empty: true };
|
||||
if (b.length < 13) return { raw: efHex(b) };
|
||||
const rt = b[0];
|
||||
const out = {};
|
||||
if (rt & 0x02) {
|
||||
const n = b[1];
|
||||
if (n > 0) {
|
||||
const digits = efSwapNibbles(efHex(b.subarray(2, 2 + Math.min(n, 10)))).replace(/[fF]+$/, '');
|
||||
out.number = digits || null;
|
||||
}
|
||||
}
|
||||
if (rt & 0x01) out.subaddress = efHex(b.subarray(1, 12));
|
||||
if (b[12] !== 0xFF) out.next_record = b[12];
|
||||
return Object.keys(out).length ? out : { raw: efHex(b) };
|
||||
}
|
||||
|
||||
function efDecSms(b) {
|
||||
if (efAllFf(b)) return { empty: true };
|
||||
const s = b[0];
|
||||
let direction = null, status;
|
||||
if ((s & 0x01) === 0x00) { direction = null; status = 'free space'; }
|
||||
else if ((s & 0x07) === 0x01) { direction = 'MT'; status = 'message read'; }
|
||||
else if ((s & 0x07) === 0x03) { direction = 'MT'; status = 'message to be read'; }
|
||||
else if ((s & 0x07) === 0x07) { direction = 'MO'; status = 'message to be sent'; }
|
||||
else if ((s & 0x1F) === 0x05) { direction = 'MO'; status = 'sent (status not requested)'; }
|
||||
else if ((s & 0x1F) === 0x0D) { direction = 'MO'; status = 'sent (status requested, not received)'; }
|
||||
else if ((s & 0x1F) === 0x15) { direction = 'MO'; status = 'sent (status received, not stored)'; }
|
||||
else if ((s & 0x1F) === 0x1D) { direction = 'MO'; status = 'sent (status received, stored)'; }
|
||||
else { direction = null; status = 'RFU'; }
|
||||
const out = { status: status };
|
||||
if (direction) out.direction = direction;
|
||||
const rem = b.subarray(1);
|
||||
if (rem.length) {
|
||||
const scLen = rem[0];
|
||||
const tpdu = efRstripFf(rem.subarray(1 + scLen));
|
||||
if (tpdu.length) out.tpdu = efHex(tpdu);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// TS 51.011 §10.3.34 EF_SUME: Simple TLVs for the SET UP MENU title.
|
||||
function efDecSume(b) {
|
||||
if (efAllFf(b)) return { empty: true };
|
||||
const out = {};
|
||||
for (const t of efTlv(b)) {
|
||||
if (t.tag === 0x05) { const s = efAnnexA(t.value); if (s) out.title = s; }
|
||||
else if (t.tag === 0x1E) out.icon = efHex(t.value);
|
||||
}
|
||||
return Object.keys(out).length ? out : { raw: efHex(b) };
|
||||
}
|
||||
|
||||
// ---- registry ----
|
||||
|
||||
const EF_DECODERS = [
|
||||
{ name: 'EF.ICCID', fid: '2fe2', fn: efDecIccid },
|
||||
{ name: 'EF.IMSI', fid: '6f07', fn: efDecImsi },
|
||||
{ name: 'EF.IST', fid: '6f07', fn: b => efDecServiceTable(b, EF_IST_SERVICES) },
|
||||
{ name: 'EF.UST', fid: '6f38', fn: b => efDecServiceTable(b, EF_UST_SERVICES) },
|
||||
{ name: 'EF.SST', fid: '6f38', fn: b => efDecServiceTable(b, EF_SST_SERVICES) },
|
||||
{ name: 'EF.LI', fid: '6f05', fn: efDecLi },
|
||||
{ name: 'EF.LP', fid: '6f05', fn: efDecLi },
|
||||
{ name: 'EF.PL', fid: '2f05', fn: efDecLi },
|
||||
{ name: 'EF.SPN', fid: '6f46', fn: efDecSpn },
|
||||
{ name: 'EF.PLMNsel', fid: '6f30', fn: efDecPlmnList },
|
||||
{ name: 'EF.PLMNwAcT', fid: '6f60', fn: efDecPlmnWact },
|
||||
{ name: 'EF.OPLMNwAcT', fid: '6f61', fn: efDecPlmnWact },
|
||||
{ name: 'EF.HPLMNwAcT', fid: '6f62', fn: efDecPlmnWact },
|
||||
{ name: 'EF.OPL', fid: '6fc6', fn: efDecOpl },
|
||||
{ name: 'EF.SPDI', fid: '6fcd', fn: efDecSpdi },
|
||||
{ name: 'EF.PNN', fid: '6fc5', fn: efDecPnn },
|
||||
{ name: 'EF.LOCI', fid: '6f7e', fn: efDecLoci },
|
||||
{ name: 'EF.PSLOCI', fid: '6f73', fn: efDecPsLoci },
|
||||
{ name: 'EF.EPSLOCI', fid: '6fe3', fn: efDecEpsLoci },
|
||||
{ name: 'EF.EPSNSC', fid: '6fe4', fn: efDecEpsNsc },
|
||||
{ name: 'EF.LOCIGPRS', fid: '6f53', fn: efDecPsLoci },
|
||||
{ name: 'EF.Kc', fid: '6f20', fn: efDecKc },
|
||||
{ name: 'EF.KcGPRS', fid: '6f52', fn: efDecKc },
|
||||
{ name: 'EF.ACC', fid: '6f78', fn: efDecAcc },
|
||||
{ name: 'EF.PHASE', fid: '6fae', fn: efDecPhase },
|
||||
{ name: 'EF.CBMI', fid: '6f45', fn: efDecCbmi },
|
||||
{ name: 'EF.CBMID', fid: '6f48', fn: efDecCbmi },
|
||||
{ name: 'EF.CBMIR', fid: '6f50', fn: efDecCbmir },
|
||||
{ name: 'EF.ECC', fid: '6fb7', fn: efDecEcc },
|
||||
{ name: 'EF.AD', fid: '6fad', fn: efDecAd },
|
||||
{ name: 'EF.ACL', fid: '6f57', fn: efDecAcl },
|
||||
{ name: 'EF.SMSP', fid: '6f42', fn: efDecSmsp },
|
||||
{ name: 'EF.SMS', fid: '6f3c', fn: efDecSms },
|
||||
{ name: 'EF.ARR', fid: '2f06', fn: efDecArr },
|
||||
{ name: 'EF.ARR', fid: '6f06', fn: efDecArr },
|
||||
{ name: 'EF.DIR', fid: '2f00', fn: efDecDir },
|
||||
{ name: 'EF.IMPI', fid: '6f02', fn: efDecNai },
|
||||
{ name: 'EF.DOMAIN', fid: '6f03', fn: efDecNai },
|
||||
{ name: 'EF.IMPU', fid: '6f04', fn: efDecNai },
|
||||
{ name: 'EF.ADN', fid: '6f3a', fn: efDecAdn },
|
||||
{ name: 'EF.FDN', fid: '6f3b', fn: efDecAdn },
|
||||
{ name: 'EF.SDN', fid: '6f49', fn: efDecAdn },
|
||||
{ name: 'EF.MSISDN', fid: '6f40', fn: efDecAdn },
|
||||
{ name: 'EF.LND', fid: '6f44', fn: efDecAdn },
|
||||
{ name: 'EF.BDN', fid: '6f4d', fn: efDecAdn },
|
||||
{ name: 'EF.EXT1', fid: '6f4a', fn: efDecExt1 },
|
||||
{ name: 'EF.EXT2', fid: '6f4b', fn: efDecExt1 },
|
||||
{ name: 'EF.EXT3', fid: '6f4c', fn: efDecExt1 },
|
||||
{ name: 'EF.EXT4', fid: '6f4e', fn: efDecExt1 },
|
||||
{ name: 'EF.EXT5', fid: '6f55', fn: efDecExt1 },
|
||||
{ name: 'EF.SUME', fid: '6f54', fn: efDecSume },
|
||||
];
|
||||
|
||||
function efFindDecoder(name, fid) {
|
||||
let n = (name || '').toUpperCase();
|
||||
if (n.includes('/')) n = n.split('/').pop();
|
||||
for (const d of EF_DECODERS) if (d.name === n) return d;
|
||||
const f = (fid || '').toLowerCase();
|
||||
if (f) for (const d of EF_DECODERS) if (d.fid === f) return d;
|
||||
return null;
|
||||
}
|
||||
|
||||
function efFidFromPath(path) {
|
||||
const seg = (path || '').split('/').pop();
|
||||
return /^[0-9a-fA-F]{4}$/.test(seg) ? seg.toUpperCase() : null;
|
||||
}
|
||||
|
||||
// ---- content decoding, flattening and diffs ----
|
||||
|
||||
function efDecodeBytes(fn, bytes) {
|
||||
try { return fn(bytes); } catch (e) { return { raw: efHex(bytes) }; }
|
||||
}
|
||||
|
||||
// content: {kind:'transparent', data:'HEX'} | {kind:'record', records:[{num,data}]}
|
||||
function efDecodeFile(name, fid, content) {
|
||||
const dec = efFindDecoder(name, fid);
|
||||
if (!dec) return null;
|
||||
const out = { ef: dec.name, fn: dec.fn, data: null, records: null, empty: false };
|
||||
if (!content) return out;
|
||||
if (content.kind === 'record') {
|
||||
out.records = (content.records || []).map(r => {
|
||||
const d = efDecodeBytes(dec.fn, efBytes(r.data || ''));
|
||||
return { num: r.num, data: d, empty: !!(d && d.empty) };
|
||||
});
|
||||
} else {
|
||||
out.data = efDecodeBytes(dec.fn, efBytes(content.data || ''));
|
||||
out.empty = !!(out.data && out.data.empty);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function efDecodeHex(name, fid, hex) {
|
||||
const dec = efFindDecoder(name, fid);
|
||||
if (!dec || !dec.fn) return null;
|
||||
return efDecodeBytes(dec.fn, efBytes(hex || ''));
|
||||
}
|
||||
|
||||
function efIsRawOnly(data) {
|
||||
if (!data || typeof data !== 'object') return true;
|
||||
const keys = Object.keys(data);
|
||||
return keys.length === 0 || (keys.length === 1 && keys[0] === 'raw');
|
||||
}
|
||||
|
||||
function efFieldLabel(k) {
|
||||
if (EF_FIELD_LABELS[k]) return EF_FIELD_LABELS[k];
|
||||
return k.replace(/_/g, ' ').replace(/^./, c => c.toUpperCase());
|
||||
}
|
||||
|
||||
function efPrimitive(v) {
|
||||
return typeof v === 'boolean' ? (v ? 'yes' : 'no') : String(v);
|
||||
}
|
||||
|
||||
// Flatten a decoded object into [label, value] rows (display and diff).
|
||||
function efFlatten(data) {
|
||||
const rows = [];
|
||||
if (data === null || data === undefined) return rows;
|
||||
if (typeof data !== 'object') { rows.push(['', efPrimitive(data)]); return rows; }
|
||||
const arrayRows = (label, arr) => {
|
||||
if (!arr.length) return;
|
||||
if (typeof arr[0] !== 'object' || arr[0] === null) {
|
||||
rows.push([label, arr.map(efPrimitive).join(', ')]);
|
||||
return;
|
||||
}
|
||||
arr.forEach((it, i) => {
|
||||
if (Array.isArray(it)) rows.push([label + ' ' + (i + 1), it.map(efPrimitive).join('\u2013')]);
|
||||
else if (it && it.n !== undefined && it.name !== undefined) rows.push(['Service ' + it.n, String(it.name)]);
|
||||
else if (it && it.mcc !== undefined) rows.push(['PLMN ' + (i + 1), it.mcc + '-' + it.mnc + (it.access_tech ? ' (' + it.access_tech + ')' : '')]);
|
||||
else if (it && it.aid !== undefined) rows.push(['Application ' + (i + 1), (it.label ? it.label + ' \u2014 ' : '') + it.aid + (it.eap ? ' [' + (it.eap.eap_types || []).join(', ') + ']' : '')]);
|
||||
else rows.push([label + ' ' + (i + 1), JSON.stringify(it)]);
|
||||
});
|
||||
};
|
||||
for (const k of Object.keys(data)) {
|
||||
const v = data[k];
|
||||
if (v === null || v === undefined) continue;
|
||||
if (Array.isArray(v)) arrayRows(efFieldLabel(k), v);
|
||||
else if (typeof v === 'object') {
|
||||
for (const [k2, v2] of efFlatten(v)) rows.push([efFieldLabel(k) + (k2 ? ' / ' + k2 : ''), v2]);
|
||||
} else {
|
||||
rows.push([efFieldLabel(k), efPrimitive(v)]);
|
||||
}
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
// Per-field diff of two decoded objects: only differing fields.
|
||||
function efDiffData(expData, actData) {
|
||||
const e = {}, a = {};
|
||||
for (const [k, v] of efFlatten(expData)) e[k] = v;
|
||||
for (const [k, v] of efFlatten(actData)) a[k] = v;
|
||||
const labels = [...new Set([...Object.keys(e), ...Object.keys(a)])];
|
||||
const rows = [];
|
||||
for (const l of labels) {
|
||||
const ev = e[l], av = a[l];
|
||||
if (ev !== av) rows.push([l, ev === undefined ? '' : ev, av === undefined ? '' : av]);
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
function efDataSummary(data) {
|
||||
if (!data || typeof data !== 'object') return '';
|
||||
if (data.imsi) return 'IMSI ' + data.imsi;
|
||||
if (data.iccid) return 'ICCID ' + data.iccid;
|
||||
if (data.title) return data.title;
|
||||
if (data.name !== undefined && data.number !== undefined) return (data.name + ' ' + data.number).trim();
|
||||
if (data.number) return data.number;
|
||||
if (data.text) return data.text;
|
||||
if (data.languages && data.languages.length) return data.languages.join(' ');
|
||||
if (data.services) return data.services.length + ' services';
|
||||
if (data.plmns) return data.plmns.map(p => p.mcc + '-' + p.mnc).join(', ');
|
||||
if (data.applications) return data.applications.map(x => x.label || x.aid || '').join('; ');
|
||||
if (data.status) return data.status + (data.direction ? ' (' + data.direction + ')' : '');
|
||||
if (data.full) return data.full;
|
||||
if (data.mcc !== undefined && data.mnc !== undefined) return data.mcc + '-' + data.mnc;
|
||||
if (data.kc) return 'Kc ' + data.kc;
|
||||
return '';
|
||||
}
|
||||
|
||||
function efRenderFieldsHtml(rows) {
|
||||
if (!rows.length) return '<span class="text-gray-400">' + esc(t('No decodable fields')) + '</span>';
|
||||
let html = '<div class="text-xs">';
|
||||
for (const [k, v] of rows) {
|
||||
html += '<div class="flex gap-2 mb-0.5">' +
|
||||
'<span class="w-40 shrink-0 text-right text-gray-500 dark:text-slate-400 break-all">' + esc(k) + '</span>' +
|
||||
'<span class="font-mono break-all">' + esc(v) + '</span></div>';
|
||||
}
|
||||
return html + '</div>';
|
||||
}
|
||||
|
||||
// Mismatching decoded fields for a content check; null when the file has no
|
||||
// decoder or either side fails to decode.
|
||||
function efContentDiff(name, fid, expectedHex, actualHex) {
|
||||
const exp = efDecodeHex(name, fid, expectedHex);
|
||||
const act = efDecodeHex(name, fid, actualHex);
|
||||
if (exp === null || act === null || efIsRawOnly(exp) || efIsRawOnly(act)) return null;
|
||||
const rows = efDiffData(exp, act);
|
||||
return { rows: rows, sameDecoded: rows.length === 0 };
|
||||
}
|
||||
|
||||
// ===== i18n =====
|
||||
const LANG_RU = {
|
||||
'Generate APDU': 'Сгенерировать APDU',
|
||||
@@ -11663,6 +12555,15 @@ const LANG_RU = {
|
||||
'Open': 'Открыть',
|
||||
'Contents': 'Содержимое',
|
||||
'Not captured': 'Не прочитано',
|
||||
'Decoded': 'Декодировано',
|
||||
'Record': 'Запись',
|
||||
'empty': 'пусто',
|
||||
'No decoder for this file': 'Нет декодера для этого файла',
|
||||
'showing raw data': 'показаны сырые данные',
|
||||
'No decodable fields': 'Нет декодируемых полей',
|
||||
'pySim JSON (server)': 'pySim JSON (сервер)',
|
||||
'Decoded comparison': 'Декодированное сравнение',
|
||||
'Decoded values match; raw bytes differ': 'Декодированные значения совпадают; отличаются сырые байты',
|
||||
'Back to list': 'Назад к списку',
|
||||
'Add rule': 'Добавить правило',
|
||||
'Scan': 'Сканировать',
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
const CACHE = 'simple-v190';
|
||||
const CACHE = 'simple-v191';
|
||||
const URLS = [
|
||||
'index.html',
|
||||
'help.html',
|
||||
|
||||
@@ -0,0 +1,232 @@
|
||||
const { test } = require('node:test');
|
||||
const assert = require('node:assert');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
|
||||
const html = fs.readFileSync(path.join(__dirname, '..', 'index.html'), 'utf8');
|
||||
|
||||
function extractFunc(src, name) {
|
||||
const re = new RegExp('function\\s+' + name + '\\s*\\([^)]*\\)\\s*\\{');
|
||||
const m = re.exec(src);
|
||||
if (!m) throw new Error('function ' + name + ' not found');
|
||||
let i = m.index + m[0].length - 1;
|
||||
let depth = 0;
|
||||
for (; i < src.length; i++) {
|
||||
if (src[i] === '{') depth++;
|
||||
else if (src[i] === '}') {
|
||||
depth--;
|
||||
if (depth === 0) break;
|
||||
}
|
||||
}
|
||||
return src.slice(m.index, i + 1);
|
||||
}
|
||||
|
||||
const FNS = [
|
||||
'efBytes', 'efHex', 'efSwapNibbles', 'efAllFf', 'efRstripFf', 'efTlv', 'efUcs2Be',
|
||||
'efGsm7Octets', 'efAnnexA', 'efBcdAddress', 'efBcdAddressLen', 'efPlmn', 'efPlmnAct', 'efPnnText',
|
||||
'efDecIccid', 'efDecImsi', 'efDecLi', 'efDecServiceTable', 'efDecPlmnList', 'efDecPlmnWact',
|
||||
'efDecSpn', 'efDecLoci', 'efDecPsLoci', 'efDecEpsLoci', 'efDecEpsNsc', 'efDecKc', 'efDecAcc',
|
||||
'efDecPhase', 'efDecCbmi', 'efDecCbmir', 'efDecEcc', 'efDecOpl', 'efDecAd', 'efDecAcl',
|
||||
'efDecSmsp', 'efDecSpdi', 'efDecNai', 'efDecDir', 'efDecArr', 'efDecPnn', 'efDecAdn',
|
||||
'efDecExt1', 'efDecSms', 'efDecSume', 'efFindDecoder', 'efFidFromPath', 'efDecodeBytes',
|
||||
'efDecodeFile', 'efDecodeHex', 'efIsRawOnly', 'efFieldLabel', 'efPrimitive', 'efFlatten',
|
||||
'efDiffData', 'efDataSummary', 'efContentDiff', 'gsm7Decode', 'efRenderFieldsHtml',
|
||||
'pysimFsDecodedHtml', 'profilerRenderReport', 'profilerLabelText', 'profilerResultAspects',
|
||||
'profilerAspectSummary', 'profilerMatchedRecordsText', 'profilerRawDataCheck', 'profilerCustomNameForPath',
|
||||
'pysimCustomNormPath',
|
||||
];
|
||||
|
||||
let code = '';
|
||||
for (const f of FNS) code += extractFunc(html, f) + '\n';
|
||||
for (const c of ['GSM7_ALPHABET', 'GSM7_EXT_MAP']) {
|
||||
code += html.match(new RegExp('const ' + c + ' = [\\s\\S]*?\\n\\];'))[0].replace('const ', 'var ') + '\n';
|
||||
}
|
||||
for (const c of ['EF_UST_SERVICES', 'EF_SST_SERVICES', 'EF_IST_SERVICES', 'EF_PHASE_NAMES']) {
|
||||
code += html.match(new RegExp('const ' + c + ' = \\{.*\\};'))[0].replace('const ', 'var ') + '\n';
|
||||
}
|
||||
code += html.match(/const EF_FIELD_LABELS = \{[\s\S]*?\n\};/)[0].replace('const ', 'var ') + '\n';
|
||||
code += html.match(/const EF_DECODERS = \[[\s\S]*?\n\];/)[0].replace('const ', 'var ') + '\n';
|
||||
code += 'var pysimCustomFiles = [];\nvar pysimFsSelected = \'EF.IMSI\';\n';
|
||||
code += 'function esc(s){return String(s);}\nfunction escHtml(s){return String(s);}\nfunction t(k){return k;}\n';
|
||||
eval(code);
|
||||
|
||||
test('ef helpers: bytes, hex, swap, TLV, PLMN', () => {
|
||||
assert.strictEqual(efHex(efBytes('a0ff01')), 'A0FF01');
|
||||
assert.strictEqual(efSwapNibbles('1234'), '2143');
|
||||
assert.strictEqual(efPlmn(efBytes('22F860')).mcc, '228');
|
||||
assert.strictEqual(efPlmn(efBytes('22F860')).mnc, '06');
|
||||
assert.ok(efAllFf(efBytes('FFFF')));
|
||||
assert.ok(!efAllFf(efBytes('FF00')));
|
||||
// 05 03 AA BB CC + 1E 01 DD
|
||||
const tlvs = efTlv(efBytes('0503AABBCC1E01DD'));
|
||||
assert.deepStrictEqual(tlvs.map(t => t.tag), [0x05, 0x1E]);
|
||||
assert.strictEqual(efHex(tlvs[0].value), 'AABBCC');
|
||||
assert.strictEqual(efHex(tlvs[1].value), 'DD');
|
||||
// long-form length
|
||||
assert.strictEqual(efTlv(efBytes('058105' + '00'.repeat(5)))[0].value.length, 5);
|
||||
});
|
||||
|
||||
test('efAnnexA decodes GSM7 octets, UCS2 and packed variants', () => {
|
||||
assert.strictEqual(efAnnexA(efBytes('47534D2D52204348')), 'GSM-R CH'); // one octet per char
|
||||
assert.strictEqual(efAnnexA(efBytes('8000470053004D')), 'GSM'); // UCS2 prefix
|
||||
assert.strictEqual(efAnnexA(efBytes('')), '');
|
||||
assert.strictEqual(efAnnexA(efBytes('FFFFFFFF')), '');
|
||||
});
|
||||
|
||||
test('EF decoders match the pinned spec / pySim test vectors', () => {
|
||||
assert.deepStrictEqual(efDecIccid(efBytes('988812010000400310f0')), { iccid: '8988211000000430010' });
|
||||
assert.deepStrictEqual(efDecImsi(efBytes('082982608200002080')), { imsi: '228062800000208' });
|
||||
assert.deepStrictEqual(efDecLi(efBytes('6465')), { languages: ['de'] });
|
||||
assert.deepStrictEqual(efDecLi(efBytes('FFFF')), { languages: [] });
|
||||
assert.deepStrictEqual(efDecSpn(efBytes('0147534d2d52204348ffffffffffffffff')), { show_in_hplmn: true, hide_in_oplmn: false, name: 'GSM-R CH' });
|
||||
assert.deepStrictEqual(efDecAcc(efBytes('0000')), { access_classes: [] });
|
||||
assert.deepStrictEqual(efDecAcc(efBytes('8000')), { access_classes: [0] });
|
||||
assert.deepStrictEqual(efDecEcc(efBytes('19f101')), { number: '911', esc: '0x01' });
|
||||
assert.deepStrictEqual(efDecKc(efBytes('837d783609a3858f05')), { kc: '837D783609A3858F', ksi: 0 });
|
||||
assert.deepStrictEqual(efDecPlmnList(efBytes('22F860')), { plmns: [{ mcc: '228', mnc: '06' }] });
|
||||
assert.deepStrictEqual(efDecCbmi(efBytes('0010FFFF0020')), { message_ids: [16, 32] });
|
||||
assert.deepStrictEqual(efDecCbmir(efBytes('0000FFFEFFFF')), { ranges: [['0000', 'FFFE']] });
|
||||
assert.deepStrictEqual(efDecPhase(efBytes('03')), { phase: 'phase 2 and higher' });
|
||||
assert.deepStrictEqual(efDecNai(efBytes('8021696d732e6d6e633030302e6d63633733382e336770706e6574776f726b2e6f7267')),
|
||||
{ text: 'ims.mnc000.mcc738.3gppnetwork.org' });
|
||||
assert.deepStrictEqual(efDecImsi(efBytes('FFFF')), { empty: true });
|
||||
assert.deepStrictEqual(efDecIccid(efBytes('FFFFFFFFFFFF')), { empty: true });
|
||||
});
|
||||
|
||||
test('EF.SUME decodes the SET UP MENU title and icon TLVs (TS 51.011 10.3.34)', () => {
|
||||
// 05 05 "Menu!" + 1E 02 01 23 + FF padding
|
||||
const d = efDecSume(efBytes('05054D656E75211E020123FFFFFFFF'));
|
||||
assert.deepStrictEqual(d, { title: 'Menu!', icon: '0123' });
|
||||
assert.deepStrictEqual(efDecSume(efBytes('FFFFFFFF')), { empty: true });
|
||||
// UCS2 title: 05 09 80 00 4D 00 65 00 6E 00 75
|
||||
assert.deepStrictEqual(efDecSume(efBytes('050980004D0065006E0075FFFFFFFF')),
|
||||
{ title: 'Menu' });
|
||||
});
|
||||
|
||||
test('ADN-format records: alpha + BCD number + CCP/EXT1', () => {
|
||||
const d = efDecAdn(efBytes('42204841203120536963FFFFFFFFFFFF06810628560810FFFFFFFFFFFFFF'));
|
||||
assert.strictEqual(d.name, 'B HA 1 Sic');
|
||||
assert.strictEqual(d.number, '6082658001');
|
||||
assert.strictEqual(d.ccp, undefined);
|
||||
assert.strictEqual(d.ext1, undefined);
|
||||
// international number in EF.MSISDN
|
||||
const m = efDecAdn(efBytes('ffffffffffffffffffffffffffffffffffffffff04b12143f5ffffffffffffffffff'));
|
||||
assert.strictEqual(m.name, '');
|
||||
assert.strictEqual(m.number, '12345');
|
||||
// EXT1 per TS 51.011 10.5.10: type 02 (additional data) + count 03 + BCD
|
||||
assert.deepStrictEqual(efDecExt1(efBytes('02032143F5' + 'FF'.repeat(8))), { number: '12345' });
|
||||
});
|
||||
|
||||
test('EF record decoders: SMS, OPL, DIR, ARR, IMPI/SMSP', () => {
|
||||
const opl = efDecOpl(efBytes('62f2100000fffe01'));
|
||||
assert.deepStrictEqual(opl, { mcc: '262', mnc: '01', lac_range: ['0000', 'FFFE'], pnn_record: 1 });
|
||||
const dir = efDecDir(efBytes('61294f10a0000000871002ffffffff890709000050055553696d31730ea00c80011781025f608203454150'));
|
||||
assert.strictEqual(dir.applications.length, 1);
|
||||
assert.strictEqual(dir.applications[0].aid, 'A0000000871002FFFFFFFF8907090000');
|
||||
assert.strictEqual(dir.applications[0].label, 'USim1');
|
||||
const arr = efDecArr(efBytes('800101a40683010a950108'));
|
||||
assert.deepStrictEqual(arr.rules, [{ tag: '0x80', hex: '01' }, { tag: '0xA4', hex: '83010A950108' }]);
|
||||
const impi = efDecNai(efBytes('803137333830303630303030303031303140696d732e6d6e633030302e6d63633733382e336770706e6574776f726b2e6f7267'));
|
||||
assert.strictEqual(impi.text, '738006000000101@ims.mnc000.mcc738.3gppnetwork.org');
|
||||
const smsp = efDecSmsp(efBytes('534d5343ffffffffffffffffffffffffe1ffffffffffffffffffffffff0891945197109099f9ffffff0000a9'));
|
||||
assert.strictEqual(smsp.alpha, 'SMSC');
|
||||
assert.strictEqual(smsp.sca, '+4915790109999');
|
||||
assert.strictEqual(smsp.tp_pid, '0x00');
|
||||
assert.strictEqual(smsp.tp_dcs, '0x00');
|
||||
// EF.SMS status byte + skip SMSC + TPDU
|
||||
const sms = efDecSms(efBytes('030891945197109099f90000'));
|
||||
assert.strictEqual(sms.direction, 'MT');
|
||||
assert.strictEqual(sms.status, 'message to be read');
|
||||
assert.strictEqual(sms.tpdu, '0000');
|
||||
});
|
||||
|
||||
test('service tables: UST service bits and IST fallback for 6F07', () => {
|
||||
const ust = efDecServiceTable(efBytes('00000010'), EF_UST_SERVICES);
|
||||
assert.deepStrictEqual(ust.services, [{ n: 29, name: 'Data download via SMS-CB' }]);
|
||||
const ist = efDecImsi(efBytes('01'));
|
||||
assert.deepStrictEqual(ist.services, [{ n: 1, name: 'P-CSCF address' }]);
|
||||
});
|
||||
|
||||
test('decoder registry resolves by name first, then FID; unknown returns null', () => {
|
||||
assert.strictEqual(efFindDecoder('EF.IMSI', null).fid, '6f07');
|
||||
assert.strictEqual(efFindDecoder(null, '2fe2').name, 'EF.ICCID');
|
||||
assert.strictEqual(efFindDecoder('ADF.USIM/EF.LOCI', null).name, 'EF.LOCI');
|
||||
assert.strictEqual(efFindDecoder('EF.SUME', null).fid, '6f54');
|
||||
assert.strictEqual(efFindDecoder('EF.NOPE', 'ABCD'), null);
|
||||
assert.strictEqual(efFidFromPath('ADF.USIM/6F07'), '6F07');
|
||||
assert.strictEqual(efFidFromPath('MF/7F10/6F3A'), '6F3A');
|
||||
assert.strictEqual(efFidFromPath('ADF.USIM'), null);
|
||||
});
|
||||
|
||||
test('efDecodeFile handles transparent and record content', () => {
|
||||
const t = efDecodeFile('EF.IMSI', null, { kind: 'transparent', data: '082982608200002080' });
|
||||
assert.strictEqual(typeof t.fn, 'function');
|
||||
assert.strictEqual(t.data.imsi, '228062800000208');
|
||||
const r = efDecodeFile('EF.ADN', '6f3a', { kind: 'record', records: [
|
||||
{ num: 1, data: '42204841203120536963FFFFFFFFFFFF06810628560810FFFFFFFFFFFFFF' },
|
||||
{ num: 2, data: 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF' },
|
||||
] });
|
||||
assert.strictEqual(r.records.length, 2);
|
||||
assert.strictEqual(r.records[0].data.name, 'B HA 1 Sic');
|
||||
assert.strictEqual(r.records[0].empty, false);
|
||||
assert.strictEqual(r.records[1].empty, true);
|
||||
assert.strictEqual(efDecodeFile('EF.NOPE', null, { kind: 'transparent', data: '00' }), null);
|
||||
});
|
||||
|
||||
test('efFlatten renders fields, services and PLMNs for display', () => {
|
||||
assert.deepStrictEqual(efFlatten({ imsi: '123' }), [['IMSI', '123']]);
|
||||
assert.deepStrictEqual(efFlatten({ services: [{ n: 5, name: 'Call Control by USIM' }] }),
|
||||
[['Service 5', 'Call Control by USIM']]);
|
||||
assert.deepStrictEqual(efFlatten({ plmns: [{ mcc: '262', mnc: '01', access_tech: 'GSM' }] }),
|
||||
[['PLMN 1', '262-01 (GSM)']]);
|
||||
assert.deepStrictEqual(efFlatten({ show_in_hplmn: true }), [['Show in HPLMN', 'yes']]);
|
||||
assert.deepStrictEqual(efFlatten({ title: 'Menu!', icon: '0123' }), [['Title', 'Menu!'], ['Icon', '0123']]);
|
||||
});
|
||||
|
||||
test('efDiffData reports only differing fields; efContentDiff decodes both sides', () => {
|
||||
assert.deepStrictEqual(efDiffData({ imsi: '1', kc: 'AA' }, { imsi: '2', kc: 'AA' }), [['IMSI', '1', '2']]);
|
||||
assert.deepStrictEqual(efDiffData({ imsi: '1' }, { imsi: '1' }), []);
|
||||
const dd = efContentDiff('EF.IMSI', null, '082982608200002080', '082982608200002081');
|
||||
assert.ok(dd && dd.rows.length === 1);
|
||||
assert.strictEqual(dd.rows[0][0], 'IMSI');
|
||||
assert.strictEqual(dd.rows[0][1], '228062800000208');
|
||||
const same = efContentDiff('EF.IMSI', null, '082982608200002080', '092982608200002080');
|
||||
assert.ok(same && same.sameDecoded);
|
||||
assert.strictEqual(efContentDiff('EF.NOPE', null, '00', '01'), null);
|
||||
});
|
||||
|
||||
test('efDataSummary produces one-line summaries', () => {
|
||||
assert.strictEqual(efDataSummary(efDecImsi(efBytes('082982608200002080'))), 'IMSI 228062800000208');
|
||||
assert.strictEqual(efDataSummary(efDecIccid(efBytes('988812010000400310f0'))), 'ICCID 8988211000000430010');
|
||||
assert.strictEqual(efDataSummary(efDecSume(efBytes('05054D656E7521FFFFFFFF'))), 'Menu!');
|
||||
assert.strictEqual(efDataSummary({ plmns: [{ mcc: '262', mnc: '01' }] }), '262-01');
|
||||
});
|
||||
|
||||
test('file manager decoded view renders fields, not JSON', () => {
|
||||
const out = pysimFsDecodedHtml({ name: 'EF.IMSI', fid: null }, { success: true, data: '082982608200002080' });
|
||||
assert.match(out, /IMSI/);
|
||||
assert.match(out, /228062800000208/);
|
||||
assert.match(out, /pySim JSON \(server\)/);
|
||||
assert.doesNotMatch(out, /\{"imsi"/);
|
||||
// no decoder -> raw fallback with a note
|
||||
const raw = pysimFsDecodedHtml({ name: 'EF.NOPE', fid: 'abcd' }, { success: true, data: '0011' });
|
||||
assert.match(raw, /No decoder for this file/);
|
||||
assert.match(raw, /0011/);
|
||||
});
|
||||
|
||||
test('profilerRenderReport shows decoded field diffs and the raw-only note', () => {
|
||||
const res = {
|
||||
path: 'ADF.USIM/6F07', name: 'EF.IMSI', status: 'fail',
|
||||
checks: [{ label: 'content', expected: '082982608200002080', actual: '082982608200002081', ok: false }],
|
||||
};
|
||||
const out = profilerRenderReport([res], null);
|
||||
assert.match(out, /Decoded comparison/);
|
||||
assert.match(out, /IMSI/);
|
||||
assert.match(out, /228062800000208/);
|
||||
assert.match(out, /228062800000218/);
|
||||
const same = profilerRenderReport([{
|
||||
path: 'ADF.USIM/6F07', name: 'EF.IMSI', status: 'fail',
|
||||
checks: [{ label: 'content', expected: '082982608200002080', actual: '092982608200002080', ok: false }],
|
||||
}], null);
|
||||
assert.match(same, /Decoded values match/);
|
||||
});
|
||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "pysim-simple-server"
|
||||
version = "2.3.1"
|
||||
version = "2.4.0"
|
||||
description = "HTTP REST server wrapping pysim for the SIMple PWA"
|
||||
requires-python = ">=3.8"
|
||||
# pysim is a git-only dependency installed explicitly by setup.bat/setup.sh.
|
||||
|
||||
@@ -21,7 +21,7 @@ from osmocom.construct import GsmOrUcs2Adapter
|
||||
from osmocom.tlv import BER_TLV_IE
|
||||
|
||||
|
||||
VERSION = '2.3.1'
|
||||
VERSION = '2.4.0'
|
||||
|
||||
MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE
|
||||
|
||||
|
||||
Reference in new issue
Block a user