Compare commits
14 Commits
30969f8f04
...
dbf9e8559b
| Author | SHA1 | Date | |
|---|---|---|---|
| dbf9e8559b | |||
| ad543fd731 | |||
| 16c183f79c | |||
| d7204b3e96 | |||
| 8f24200537 | |||
| 0ecfd5f720 | |||
| d9b2cb7db2 | |||
| 486f57eb30 | |||
| 69b4fbfb42 | |||
| 5355a62d88 | |||
| 7614067b8f | |||
| 181544b360 | |||
| c31885d40e | |||
| 3ca014991a |
+25
-5
@@ -42,7 +42,7 @@
|
||||
|
||||
<section class="mb-10">
|
||||
<h2 id="c-apdu" class="text-xl font-semibold mb-3 border-b border-gray-300 dark:border-slate-700 pb-1">2. Вкладка C-APDU</h2>
|
||||
<p class="mb-3">Построение командных APDU (C-APDU). Пять подвкладок охватывают разные поколения карт и наборы команд: <strong>SIM RFM</strong>, <strong>USIM RFM</strong>, <strong>Expanded Script</strong>, <strong>RAM/GP</strong> и <strong>C-APDU Parser</strong>.</p>
|
||||
<p class="mb-3">Построение командных APDU (C-APDU). Шесть подвкладок охватывают разные поколения карт и наборы команд: <strong>SIM RFM</strong>, <strong>USIM RFM</strong>, <strong>Expanded Script</strong>, <strong>RAM/GP</strong>, <strong>HTTP OTA</strong> и <strong>C-APDU Parser</strong>.</p>
|
||||
|
||||
<h3 id="sim-rfm" class="text-lg font-medium mb-2">2.1 SIM RFM</h3>
|
||||
<p class="mb-2">CLA = <code class="font-mono text-sm">A0</code> (GSM 11.11 / TS 151 011, ISO 7816-4). Удалённое управление файлами классических SIM-карт.</p>
|
||||
@@ -204,7 +204,26 @@
|
||||
</ul>
|
||||
|
||||
<h3 id="c-apdu-parser" class="text-lg font-medium mb-2">2.6 C-APDU Parser</h3>
|
||||
<p class="text-sm mb-3">Вставка raw APDU hex и отображение сворачиваемого дерева. Автоматически определяет контейнер: <strong>Expanded Script</strong> (начало <code class="font-mono text-sm">AA</code> или <code class="font-mono text-sm">AE80</code>, декодируется по ETSI TS 102 226 §5.2.1) или <strong>Compact C-APDU chain</strong> (последовательность C-APDU ISO 7816). Каждый узел показывает метку, hex и краткое описание; родительские узлы раскрываются в подэлементы.
|
||||
<p class="text-sm mb-3">Вставка raw APDU hex и отображение сворачиваемого дерева. Автоматически определяет контейнер: <strong>Expanded Script</strong> (начало <code class="font-mono text-sm">AA</code> или <code class="font-mono text-sm">AE80</code>, декодируется по ETSI TS 102 226 §5.2.1) или <strong>Compact C-APDU chain</strong> (последовательность C-APDU ISO 7816). Каждый узел показывает метку, hex и краткое описание; родительские узлы раскрываются в подэлементы.</p>
|
||||
|
||||
<h3 id="http-ota" class="text-lg font-medium mb-2">2.7 HTTP OTA</h3>
|
||||
<p class="text-sm mb-3">Сборка payload’ов Remote Application Management over HTTP по GlobalPlatform <strong>GPC v2.2 Amendment B v1.1</strong> (§4.7). Два режима:</p>
|
||||
<ul class="list-disc list-inside text-sm space-y-1 mb-3">
|
||||
<li><strong>Trigger (Push SMS)</strong> — параметры запуска административной сессии (<code class="font-mono text-sm">81 > 83 > 84/[85]/[86]/89</code>, таблица 4-3). Сообщение, которое просит Security Domain карты исходящим запросом начать HTTP-сессию.</li>
|
||||
<li><strong>Store (SD admin params)</strong> — записывает те же параметры как данные карты (параметры Security Domain) командой <strong>STORE DATA в TLV-режиме</strong> (<code class="font-mono text-sm">80 E2 90 00</code>, P1=90 = последний блок + BER-TLV по GP v2.2 Amendment B v1.1.3), обёрнутые в тег <code class="font-mono text-sm">85</code> (или <code class="font-mono text-sm">A5</code>) по таблице 4-4.</li>
|
||||
</ul>
|
||||
<p class="text-sm mb-2">Разделы соответствуют таблицам спецификации:</p>
|
||||
<table class="w-full text-sm mb-3 border-collapse">
|
||||
<thead><tr class="border-b border-gray-300 dark:border-slate-700"><th class="text-left py-1 px-2">Раздел</th><th class="text-left py-1 px-2">Tag</th><th class="text-left py-1 px-2">Содержимое</th></tr></thead>
|
||||
<tbody>
|
||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">Параметры соединения</td><td class="py-1 px-2 font-mono">84</td><td class="py-1 px-2">Любые COMPREHENSION-TLV для открытия TCP-соединения (OPEN CHANNEL по TS 102 223): Device Identities <code class="font-mono text-sm">02</code>, Alpha <code class="font-mono text-sm">80</code>, Bearer <code class="font-mono text-sm">01</code>, вендорские TLV. Редактор строк + пресеты, редактируемый hex.</td></tr>
|
||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">Параметры безопасности</td><td class="py-1 px-2 font-mono">85</td><td class="py-1 px-2">Таблица 4-6: LV PSK Identity (текст), LV Key version/KID. Идентифицирует ключ PSK TLS (RFC 4279).</td></tr>
|
||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">Политика повтора</td><td class="py-1 px-2 font-mono">86</td><td class="py-1 px-2">Таблица 4-7: счётчик повторов (2 байта, напр. <code class="font-mono text-sm">B000</code>), задержка повторной попытки как timer TLV из TS 102 223 (<code class="font-mono text-sm">25 03 HH MM SS</code>), опциональный вендорский TLV отчёта о сбое.</td></tr>
|
||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">HTTP POST</td><td class="py-1 px-2 font-mono">89</td><td class="py-1 px-2">Таблицы 4-8/9/10: Host-заголовок (<code class="font-mono text-sm">8A</code>), X-Admin-From agent ID (<code class="font-mono text-sm">8B</code>), URI (<code class="font-mono text-sm">8C</code>) — текст преобразуется в октеты.</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p class="text-sm mb-2"><strong>Command Scripting template</strong> (флажок) оборачивает всю команду <code class="font-mono text-sm">81</code> в формат Expanded Remote Application data с определённой длиной (<code class="font-mono text-sm">AA</code>, ETSI TS 102 226 §5.2.1) для TAR-ов, обрабатывающих расширенный формат (RAM-over-HTTP §4.7).</p>
|
||||
<p class="text-sm mb-2"><strong>Pack into Secured packet</strong> отправляет готовый payload на вкладку SCP80 для заполнения SPI/счётчика — там укажите TAR, который слушает SD (обычно TAR OTASD).</p>
|
||||
|
||||
|
||||
<section class="mb-10">
|
||||
@@ -356,12 +375,13 @@
|
||||
<p class="text-sm mb-2">Правила выполняются последовательно. Правило файловой системы задаётся:</p>
|
||||
<ul class="list-disc list-inside text-sm space-y-1 mb-3">
|
||||
<li><strong>Путь</strong> — начинается с <code class="font-mono text-sm">MF</code> (например, <code class="font-mono text-sm">MF/7F10/6F3A</code>) или с AID ADF (например, <code class="font-mono text-sm">A0000000871002/6F07</code>).</li>
|
||||
<li><strong>Атрибуты файла</strong> — тип файла, размер, длина записи и число записей из FCI-шаблона (любой можно оставить незаданным).</li>
|
||||
<li><strong>Проверка FCP/FCI</strong> — какая часть информации об управлении файлом проверяется: <strong>Filetype only (FCP)</strong> (существование + тип файла), <strong>Filetype + size (FCP)</strong> (добавляются размер файла либо длина/число записей для record-файлов) или <strong>Exact FCI</strong> (добавляется побайтовое сравнение сырого ответа SELECT — шаблона FCP <code class="font-mono text-sm">'62'</code> — выявляет изменения FID/AID, жизненного цикла, атрибутов безопасности и проприетарных параметров).</li>
|
||||
<li><strong>Атрибуты файла</strong> — тип файла, размер, длина записи и число записей из FCP-шаблона (любой можно оставить незаданным).</li>
|
||||
<li><strong>Содержимое</strong> (опционально) — <strong>Exact</strong> (точное совпадение hex) или <strong>Mask</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>Check</strong> выполняет каждое правило на подключённой карте и показывает строку прогресса и отчёт прохождения (существование, каждый атрибут FCI и совпадение содержимого).</p>
|
||||
<p class="text-sm mb-3"><strong>Check</strong> выполняет каждое правило на подключённой карте и показывает строку прогресса и отчёт прохождения. Рядом с путём файла указывается, что именно проверялось (например, <em>тип файла и размер, содержимое</em> или <em>точный FCI</em>); если часть проверок прошла, а часть нет — каждый аспект помечается (<em>тип файла ✓, размер ✗, содержимое ✓</em>), а расхождения расписываются ниже. Для record-файлов при расхождении содержимого добавляется пометка <em>совпадающие записи: 1-5, 7-10</em> со списком записей, которые совпали.</p>
|
||||
<h4 class="font-medium mb-1">Опции сканирования «Profile from card»</h4>
|
||||
<p class="text-sm mb-2">Диалог сканирования запрашивает имя профиля и предлагает список <strong>«Ignore contents of»</strong> (все отмечены по умолчанию) часто перезаписываемых файлов, содержимое которых пропускается: <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>. Правила создаются только для файлов, которые реально существуют на карте (возвращён FCI-шаблон); отсутствующие файлы пропускаются. Пользовательские файлы из подвкладки <strong>Custom files</strong> включаются с той же проверкой существования.</p>
|
||||
<p class="text-sm mb-2">Диалог сканирования запрашивает имя профиля и предлагает селектор <strong>«FCP/FCI check»</strong> (те же три режима, по умолчанию <strong>Filetype + size</strong>), применяемый ко всем создаваемым правилам, а также список <strong>«Ignore contents of»</strong> (все отмечены по умолчанию) часто перезаписываемых файлов, содержимое которых пропускается: <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>. Ещё две отмеченные по умолчанию опции <strong>«Match first 4 bytes for»</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>Custom files</strong> включаются с той же проверкой существования.</p>
|
||||
|
||||
<h3 id="usage-scenarios" class="text-lg font-medium mb-2">5.7 Сценарии использования</h3>
|
||||
|
||||
|
||||
+25
-5
@@ -42,7 +42,7 @@
|
||||
|
||||
<section class="mb-10">
|
||||
<h2 id="c-apdu" class="text-xl font-semibold mb-3 border-b border-gray-300 dark:border-slate-700 pb-1">2. C-APDU tab</h2>
|
||||
<p class="mb-3">Builds command APDUs (C-APDUs). Five sub-tabs cover different card generations and command sets: <strong>SIM RFM</strong>, <strong>USIM RFM</strong>, <strong>Expanded Script</strong>, <strong>RAM/GP</strong>, and <strong>C-APDU Parser</strong>.</p>
|
||||
<p class="mb-3">Builds command APDUs (C-APDUs). Six sub-tabs cover different card generations and command sets: <strong>SIM RFM</strong>, <strong>USIM RFM</strong>, <strong>Expanded Script</strong>, <strong>RAM/GP</strong>, <strong>HTTP OTA</strong>, and <strong>C-APDU Parser</strong>.</p>
|
||||
|
||||
<h3 id="sim-rfm" class="text-lg font-medium mb-2">2.1 SIM RFM</h3>
|
||||
<p class="mb-2">CLA = <code class="font-mono text-sm">A0</code> (GSM 11.11 / TS 151 011, ISO 7816-4). Remote File Management for classic SIM cards.</p>
|
||||
@@ -204,7 +204,26 @@
|
||||
</ul>
|
||||
|
||||
<h3 id="c-apdu-parser" class="text-lg font-medium mb-2">2.6 C-APDU Parser</h3>
|
||||
<p class="text-sm mb-3">Pastes raw APDU hex and renders a collapsible tree. It auto-detects the container: an <strong>Expanded Script</strong> (leading <code class="font-mono text-sm">AA</code> or <code class="font-mono text-sm">AE80</code>, decoded per ETSI TS 102 226 §5.2.1) or a <strong>Compact C-APDU chain</strong> (a sequence of ISO 7816 C-APDUs). Each node shows its label, hex and a short description; parent nodes expand to reveal their sub-elements.
|
||||
<p class="text-sm mb-3">Pastes raw APDU hex and renders a collapsible tree. It auto-detects the container: an <strong>Expanded Script</strong> (leading <code class="font-mono text-sm">AA</code> or <code class="font-mono text-sm">AE80</code>, decoded per ETSI TS 102 226 §5.2.1) or a <strong>Compact C-APDU chain</strong> (a sequence of ISO 7816 C-APDUs). Each node shows its label, hex and a short description; parent nodes expand to reveal their sub-elements.</p>
|
||||
|
||||
<h3 id="http-ota" class="text-lg font-medium mb-2">2.7 HTTP OTA</h3>
|
||||
<p class="text-sm mb-3">Builds the Remote Application Management over HTTP payloads defined in GlobalPlatform <strong>GPC v2.2 Amendment B v1.1</strong> (§4.7). Two modes:</p>
|
||||
<ul class="list-disc list-inside text-sm space-y-1 mb-3">
|
||||
<li><strong>Trigger (Push SMS)</strong> — administration session triggering parameters (<code class="font-mono text-sm">81 > 83 > 84/[85]/[86]/89</code>, Table 4-3). This is the message that asks the card's Security Domain to dial out and start an HTTP session.</li>
|
||||
<li><strong>Store (SD admin params)</strong> — writes the same parameters as card (Security Domain) data via <strong>STORE DATA in TLV mode</strong> (<code class="font-mono text-sm">80 E2 90 00</code>, P1=90 = last block + BER-TLV per GP v2.2 Amendment B v1.1.3), wrapped in tag <code class="font-mono text-sm">85</code> (or <code class="font-mono text-sm">A5</code>) per Table 4-4.</li>
|
||||
</ul>
|
||||
<p class="text-sm mb-2">Sections mirror the spec tables:</p>
|
||||
<table class="w-full text-sm mb-3 border-collapse">
|
||||
<thead><tr class="border-b border-gray-300 dark:border-slate-700"><th class="text-left py-1 px-2">Section</th><th class="text-left py-1 px-2">Tag</th><th class="text-left py-1 px-2">Contents</th></tr></thead>
|
||||
<tbody>
|
||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">Connection parameters</td><td class="py-1 px-2 font-mono">84</td><td class="py-1 px-2">Any COMPREHENSION-TLV needed to open the TCP connection (OPEN CHANNEL per TS 102 223): Device Identities <code class="font-mono text-sm">02</code>, Alpha <code class="font-mono text-sm">80</code>, Bearer <code class="font-mono text-sm">01</code>, vendor TLVs. Row editor + presets, editable hex.</td></tr>
|
||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">Security parameters</td><td class="py-1 px-2 font-mono">85</td><td class="py-1 px-2">Table 4-6: LV PSK Identity (text), LV Key version/KID. Identifies the PSK TLS key (RFC 4279).</td></tr>
|
||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">Retry policy</td><td class="py-1 px-2 font-mono">86</td><td class="py-1 px-2">Table 4-7: retry counter (2 bytes, e.g. <code class="font-mono text-sm">B000</code>), retry waiting delay as the TS 102 223 timer TLV (<code class="font-mono text-sm">25 03 HH MM SS</code>), optional vendor-specific report-failure TLV.</td></tr>
|
||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">HTTP POST</td><td class="py-1 px-2 font-mono">89</td><td class="py-1 px-2">Tables 4-8/9/10: Host header (<code class="font-mono text-sm">8A</code>), X-Admin-From agent ID (<code class="font-mono text-sm">8B</code>), URI (<code class="font-mono text-sm">8C</code>) — text converted to octets.</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p class="text-sm mb-2"><strong>Command Scripting template</strong> (checkbox) wraps the whole <code class="font-mono text-sm">81</code> triggering command in the definite-length Expanded Remote Application data format (<code class="font-mono text-sm">AA</code>, ETSI TS 102 226 §5.2.1) for TARs that process the expanded format (RAM-over-HTTP §4.7).</p>
|
||||
<p class="text-sm mb-2"><strong>Pack into Secured packet</strong> sends the built payload to the SCP80 tab for SPI/counter filling — insert the TAR the SD listens on (typically the OTASD TAR) there.</p>
|
||||
|
||||
|
||||
<section class="mb-10">
|
||||
@@ -356,12 +375,13 @@
|
||||
<p class="text-sm mb-2">Rules run sequentially. A filesystem rule is defined by:</p>
|
||||
<ul class="list-disc list-inside text-sm space-y-1 mb-3">
|
||||
<li><strong>Path</strong> — starts with <code class="font-mono text-sm">MF</code> (e.g. <code class="font-mono text-sm">MF/7F10/6F3A</code>) or an ADF AID (e.g. <code class="font-mono text-sm">A0000000871002/6F07</code>).</li>
|
||||
<li><strong>File attributes</strong> — file type, size, record length and record count, taken from the FCI template (any may be left unset).</li>
|
||||
<li><strong>FCP/FCI check</strong> — how much of the file control information to verify: <strong>Filetype only (FCP)</strong> (existence + file type), <strong>Filetype + size (FCP)</strong> (adds file size, or record length/count for record files), or <strong>Exact FCI</strong> (adds a byte-for-byte comparison of the raw SELECT response — the FCP template <code class="font-mono text-sm">'62'</code> — catching FID/AID, life-cycle status, security-attribute and proprietary-parameter changes).</li>
|
||||
<li><strong>File attributes</strong> — file type, size, record length and record count, taken from the FCP template (any may be left unset).</li>
|
||||
<li><strong>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</strong> runs every rule against the equipped card and shows a live progress line plus a pass/fail report (existence, each FCI attribute, and the content match).</p>
|
||||
<p class="text-sm mb-3"><strong>Check</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. For record files with a contents mismatch, a <em>matching records: 1-5, 7-10</em> note lists the records that did match.</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 an <strong>“Ignore contents of”</strong> checklist (all checked by default) 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>. Rules are created only for files that actually exist on the card (an FCI template is returned); missing files are skipped. Custom files from the <strong>Custom files</strong> sub-tab are included under the same existence check.</p>
|
||||
<p class="text-sm mb-2">The scan dialog asks for a profile name and offers a <strong>“FCP/FCI check”</strong> selector (the same three modes above, default <strong>Filetype + size</strong>) applied to every generated rule, plus an <strong>“Ignore contents of”</strong> checklist (all checked by default) 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>. Two further checked-by-default options <strong>“Match first 4 bytes for”</strong> <code class="font-mono text-sm">EF.IMSI</code> and <code class="font-mono text-sm">EF.ICCID</code> capture those files’ contents as a mask of only the first 4 bytes (uncheck for exact matching). A progress line shows <em>N / total files</em> with the current file path while scanning; during the scan the options are hidden and the buttons are locked. Rules are created only for files that actually exist on the card (a FCP template is returned); missing files are skipped. Custom files from the <strong>Custom files</strong> sub-tab are included under the same existence check.</p>
|
||||
|
||||
<h3 id="usage-scenarios" class="text-lg font-medium mb-2">5.7 Usage scenarios</h3>
|
||||
|
||||
|
||||
+617
-93
@@ -18,7 +18,7 @@
|
||||
<div class="max-w-7xl mx-auto px-6 py-2">
|
||||
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<h1 class="text-2xl font-bold text-heading">OTAMan <span id="slogan" class="text-sm font-normal text-gray-500 dark:text-slate-400 ml-2" data-l10n="SIM OTA with a Human Face">SIM OTA with a Human Face</span> <span class="text-xs text-gray-400 dark:text-slate-500 ml-1">v1.9.26</span></h1>
|
||||
<h1 class="text-2xl font-bold text-heading">OTAMan <span id="slogan" class="text-sm font-normal text-gray-500 dark:text-slate-400 ml-2" data-l10n="SIM OTA with a Human Face">SIM OTA with a Human Face</span> <span class="text-xs text-gray-400 dark:text-slate-500 ml-1">v1.9.28</span></h1>
|
||||
<div class="flex items-center gap-4">
|
||||
<button id="install-btn" class="px-2 py-1 text-xs rounded border border-gray-300 dark:border-slate-600 hover:bg-gray-200 dark:hover:bg-slate-700" style="display:none">INSTALL PWA [for offline use]</button>
|
||||
<a href="https://github.com/anttro/otaman" target="_blank" class="text-xs text-gray-400 hover:text-gray-600 dark:text-slate-500 dark:hover:text-slate-300">github</a>
|
||||
@@ -43,25 +43,26 @@
|
||||
<button class="c-apdu-subtab px-4 py-1.5 text-sm rounded-full bg-gray-200 dark:bg-slate-700 text-gray-700 dark:text-slate-300 hover:bg-gray-300 dark:hover:bg-slate-600" data-sub="usim" onclick="cApduSwitchSubtab('usim')">USIM RFM</button>
|
||||
<button class="c-apdu-subtab px-4 py-1.5 text-sm rounded-full bg-gray-200 dark:bg-slate-700 text-gray-700 dark:text-slate-300 hover:bg-gray-300 dark:hover:bg-slate-600" data-sub="ber" onclick="cApduSwitchSubtab('ber')">Expanded Script</button>
|
||||
<button class="c-apdu-subtab px-4 py-1.5 text-sm rounded-full bg-gray-200 dark:bg-slate-700 text-gray-700 dark:text-slate-300 hover:bg-gray-300 dark:hover:bg-slate-600" data-sub="ram" onclick="cApduSwitchSubtab('ram')">RAM/GP</button>
|
||||
<button class="c-apdu-subtab px-4 py-1.5 text-sm rounded-full bg-gray-200 dark:bg-slate-700 text-gray-700 dark:text-slate-300 hover:bg-gray-300 dark:hover:bg-slate-600" data-sub="parse" onclick="cApduSwitchSubtab('parse')" data-l10n="C-APDU Parser">C-APDU Parser</button>
|
||||
<button class="c-apdu-subtab px-4 py-1.5 text-sm rounded-full bg-gray-200 dark:bg-slate-700 text-gray-700 dark:text-slate-300 hover:bg-gray-300 dark:hover:bg-slate-600" data-sub="httpota" onclick="cApduSwitchSubtab('httpota')">HTTP OTA</button>
|
||||
<button class="c-apdu-subtab px-4 py-1.5 text-sm rounded-full bg-gray-200 dark:bg-slate-700 text-gray-700 dark:text-slate-300 hover:bg-gray-300 dark:hover:bg-slate-600" data-sub="parse" onclick="cApduSwitchSubtab('parse')" data-l10n="C-APDU Parser">C-APDU Parser</button>
|
||||
</div>
|
||||
<div id="c-apdu-sub-sim">
|
||||
<div id="chain-sim-rows"></div>
|
||||
<div class="flex flex-wrap gap-1 mb-3">
|
||||
<button onclick="chainAddRow('chain-sim','select')" class="px-2 py-1 text-xs bg-blue-100 dark:bg-blue-900 text-blue-700 dark:text-blue-300 rounded hover:bg-blue-200 dark:hover:bg-blue-800">+ SELECT</button>
|
||||
<button onclick="chainAddRow('chain-sim','read-record')" class="px-2 py-1 text-xs bg-blue-100 dark:bg-blue-900 text-blue-700 dark:text-blue-300 rounded hover:bg-blue-200 dark:hover:bg-blue-800">+ READ RECORD</button>
|
||||
<button onclick="chainAddRow('chain-sim','read-binary')" class="px-2 py-1 text-xs bg-blue-100 dark:bg-blue-900 text-blue-700 dark:text-blue-300 rounded hover:bg-blue-200 dark:hover:bg-blue-800">+ READ BINARY</button>
|
||||
<button onclick="chainAddRow('chain-sim','update-record')" class="px-2 py-1 text-xs bg-blue-100 dark:bg-blue-900 text-blue-700 dark:text-blue-300 rounded hover:bg-blue-200 dark:hover:bg-blue-800">+ UPDATE RECORD</button>
|
||||
<button onclick="chainAddRow('chain-sim','update-binary')" class="px-2 py-1 text-xs bg-blue-100 dark:bg-blue-900 text-blue-700 dark:text-blue-300 rounded hover:bg-blue-200 dark:hover:bg-blue-800">+ UPDATE BINARY</button>
|
||||
<button onclick="chainAddRow('chain-sim','erase-binary')" class="px-2 py-1 text-xs bg-blue-100 dark:bg-blue-900 text-blue-700 dark:text-blue-300 rounded hover:bg-blue-200 dark:hover:bg-blue-800">+ ERASE BINARY</button>
|
||||
<button onclick="chainAddRow('chain-sim','activate-file')" class="px-2 py-1 text-xs bg-blue-100 dark:bg-blue-900 text-blue-700 dark:text-blue-300 rounded hover:bg-blue-200 dark:hover:bg-blue-800">+ ACTIVATE</button>
|
||||
<button onclick="chainAddRow('chain-sim','deactivate-file')" class="px-2 py-1 text-xs bg-blue-100 dark:bg-blue-900 text-blue-700 dark:text-blue-300 rounded hover:bg-blue-200 dark:hover:bg-blue-800">+ DEACTIVATE</button>
|
||||
<button onclick="chainAddRow('chain-sim','verify')" class="px-2 py-1 text-xs bg-blue-100 dark:bg-blue-900 text-blue-700 dark:text-blue-300 rounded hover:bg-blue-200 dark:hover:bg-blue-800">+ VERIFY PIN</button>
|
||||
<button onclick="chainAddRow('chain-sim','change')" class="px-2 py-1 text-xs bg-blue-100 dark:bg-blue-900 text-blue-700 dark:text-blue-300 rounded hover:bg-blue-200 dark:hover:bg-blue-800">+ CHANGE PIN</button>
|
||||
<button onclick="chainAddRow('chain-sim','disable')" class="px-2 py-1 text-xs bg-blue-100 dark:bg-blue-900 text-blue-700 dark:text-blue-300 rounded hover:bg-blue-200 dark:hover:bg-blue-800">+ DISABLE PIN</button>
|
||||
<button onclick="chainAddRow('chain-sim','enable')" class="px-2 py-1 text-xs bg-blue-100 dark:bg-blue-900 text-blue-700 dark:text-blue-300 rounded hover:bg-blue-200 dark:hover:bg-blue-800">+ ENABLE PIN</button>
|
||||
<button onclick="chainAddRow('chain-sim','unblock')" class="px-2 py-1 text-xs bg-blue-100 dark:bg-blue-900 text-blue-700 dark:text-blue-300 rounded hover:bg-blue-200 dark:hover:bg-blue-800">+ UNBLOCK PIN</button>
|
||||
<button onclick="chainAddRow('chain-sim','get-response')" class="px-2 py-1 text-xs bg-gray-200 dark:bg-gray-700 text-gray-600 dark:text-gray-400 rounded hover:bg-gray-300 dark:hover:bg-gray-600">+ GET RESPONSE</button>
|
||||
<button onclick="chainAddRow('chain-sim','select')" class="text-xs px-2 py-1 bg-blue-600 text-white rounded hover:bg-blue-700">+ SELECT</button>
|
||||
<button onclick="chainAddRow('chain-sim','read-record')" class="text-xs px-2 py-1 bg-blue-600 text-white rounded hover:bg-blue-700">+ READ RECORD</button>
|
||||
<button onclick="chainAddRow('chain-sim','read-binary')" class="text-xs px-2 py-1 bg-blue-600 text-white rounded hover:bg-blue-700">+ READ BINARY</button>
|
||||
<button onclick="chainAddRow('chain-sim','update-record')" class="text-xs px-2 py-1 bg-blue-600 text-white rounded hover:bg-blue-700">+ UPDATE RECORD</button>
|
||||
<button onclick="chainAddRow('chain-sim','update-binary')" class="text-xs px-2 py-1 bg-blue-600 text-white rounded hover:bg-blue-700">+ UPDATE BINARY</button>
|
||||
<button onclick="chainAddRow('chain-sim','erase-binary')" class="text-xs px-2 py-1 bg-blue-600 text-white rounded hover:bg-blue-700">+ ERASE BINARY</button>
|
||||
<button onclick="chainAddRow('chain-sim','activate-file')" class="text-xs px-2 py-1 bg-blue-600 text-white rounded hover:bg-blue-700">+ ACTIVATE</button>
|
||||
<button onclick="chainAddRow('chain-sim','deactivate-file')" class="text-xs px-2 py-1 bg-blue-600 text-white rounded hover:bg-blue-700">+ DEACTIVATE</button>
|
||||
<button onclick="chainAddRow('chain-sim','verify')" class="text-xs px-2 py-1 bg-blue-600 text-white rounded hover:bg-blue-700">+ VERIFY PIN</button>
|
||||
<button onclick="chainAddRow('chain-sim','change')" class="text-xs px-2 py-1 bg-blue-600 text-white rounded hover:bg-blue-700">+ CHANGE PIN</button>
|
||||
<button onclick="chainAddRow('chain-sim','disable')" class="text-xs px-2 py-1 bg-blue-600 text-white rounded hover:bg-blue-700">+ DISABLE PIN</button>
|
||||
<button onclick="chainAddRow('chain-sim','enable')" class="text-xs px-2 py-1 bg-blue-600 text-white rounded hover:bg-blue-700">+ ENABLE PIN</button>
|
||||
<button onclick="chainAddRow('chain-sim','unblock')" class="text-xs px-2 py-1 bg-blue-600 text-white rounded hover:bg-blue-700">+ UNBLOCK PIN</button>
|
||||
<button onclick="chainAddRow('chain-sim','get-response')" class="text-xs px-2 py-1 bg-emerald-600 text-white rounded hover:bg-emerald-700">+ GET RESPONSE</button>
|
||||
</div>
|
||||
<textarea id="chain-sim-preview" rows="3" readonly class="w-full font-mono border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 bg-gray-100 dark:bg-slate-800" placeholder="Chain preview — add commands above"></textarea>
|
||||
<button onclick="packToSp('chain-sim-preview')" id="chain-sim-pack-btn" disabled class="mb-3 px-5 py-2.5 bg-emerald-600 text-white text-sm font-medium rounded hover:bg-emerald-700 disabled:opacity-40 disabled:cursor-not-allowed" data-l10n="Pack into Secured packet">Pack into Secured packet</button>
|
||||
@@ -70,19 +71,19 @@
|
||||
<div id="c-apdu-sub-usim" class="hidden">
|
||||
<div id="chain-usim-rows"></div>
|
||||
<div class="flex flex-wrap gap-1 mb-3">
|
||||
<button onclick="chainAddRow('chain-usim','select')" class="px-2 py-1 text-xs bg-blue-100 dark:bg-blue-900 text-blue-700 dark:text-blue-300 rounded hover:bg-blue-200 dark:hover:bg-blue-800">+ SELECT</button>
|
||||
<button onclick="chainAddRow('chain-usim','read-record')" class="px-2 py-1 text-xs bg-blue-100 dark:bg-blue-900 text-blue-700 dark:text-blue-300 rounded hover:bg-blue-200 dark:hover:bg-blue-800">+ READ RECORD</button>
|
||||
<button onclick="chainAddRow('chain-usim','read-binary')" class="px-2 py-1 text-xs bg-blue-100 dark:bg-blue-900 text-blue-700 dark:text-blue-300 rounded hover:bg-blue-200 dark:hover:bg-blue-800">+ READ BINARY</button>
|
||||
<button onclick="chainAddRow('chain-usim','update-record')" class="px-2 py-1 text-xs bg-blue-100 dark:bg-blue-900 text-blue-700 dark:text-blue-300 rounded hover:bg-blue-200 dark:hover:bg-blue-800">+ UPDATE RECORD</button>
|
||||
<button onclick="chainAddRow('chain-usim','update-binary')" class="px-2 py-1 text-xs bg-blue-100 dark:bg-blue-900 text-blue-700 dark:text-blue-300 rounded hover:bg-blue-200 dark:hover:bg-blue-800">+ UPDATE BINARY</button>
|
||||
<button onclick="chainAddRow('chain-usim','activate-file')" class="px-2 py-1 text-xs bg-blue-100 dark:bg-blue-900 text-blue-700 dark:text-blue-300 rounded hover:bg-blue-200 dark:hover:bg-blue-800">+ ACTIVATE</button>
|
||||
<button onclick="chainAddRow('chain-usim','deactivate-file')" class="px-2 py-1 text-xs bg-blue-100 dark:bg-blue-900 text-blue-700 dark:text-blue-300 rounded hover:bg-blue-200 dark:hover:bg-blue-800">+ DEACTIVATE</button>
|
||||
<button onclick="chainAddRow('chain-usim','verify')" class="px-2 py-1 text-xs bg-blue-100 dark:bg-blue-900 text-blue-700 dark:text-blue-300 rounded hover:bg-blue-200 dark:hover:bg-blue-800">+ VERIFY PIN</button>
|
||||
<button onclick="chainAddRow('chain-usim','change')" class="px-2 py-1 text-xs bg-blue-100 dark:bg-blue-900 text-blue-700 dark:text-blue-300 rounded hover:bg-blue-200 dark:hover:bg-blue-800">+ CHANGE PIN</button>
|
||||
<button onclick="chainAddRow('chain-usim','disable')" class="px-2 py-1 text-xs bg-blue-100 dark:bg-blue-900 text-blue-700 dark:text-blue-300 rounded hover:bg-blue-200 dark:hover:bg-blue-800">+ DISABLE PIN</button>
|
||||
<button onclick="chainAddRow('chain-usim','enable')" class="px-2 py-1 text-xs bg-blue-100 dark:bg-blue-900 text-blue-700 dark:text-blue-300 rounded hover:bg-blue-200 dark:hover:bg-blue-800">+ ENABLE PIN</button>
|
||||
<button onclick="chainAddRow('chain-usim','unblock')" class="px-2 py-1 text-xs bg-blue-100 dark:bg-blue-900 text-blue-700 dark:text-blue-300 rounded hover:bg-blue-200 dark:hover:bg-blue-800">+ UNBLOCK PIN</button>
|
||||
<button onclick="chainAddRow('chain-usim','get-response')" class="px-2 py-1 text-xs bg-gray-200 dark:bg-gray-700 text-gray-600 dark:text-gray-400 rounded hover:bg-gray-300 dark:hover:bg-gray-600">+ GET RESPONSE</button>
|
||||
<button onclick="chainAddRow('chain-usim','select')" class="text-xs px-2 py-1 bg-blue-600 text-white rounded hover:bg-blue-700">+ SELECT</button>
|
||||
<button onclick="chainAddRow('chain-usim','read-record')" class="text-xs px-2 py-1 bg-blue-600 text-white rounded hover:bg-blue-700">+ READ RECORD</button>
|
||||
<button onclick="chainAddRow('chain-usim','read-binary')" class="text-xs px-2 py-1 bg-blue-600 text-white rounded hover:bg-blue-700">+ READ BINARY</button>
|
||||
<button onclick="chainAddRow('chain-usim','update-record')" class="text-xs px-2 py-1 bg-blue-600 text-white rounded hover:bg-blue-700">+ UPDATE RECORD</button>
|
||||
<button onclick="chainAddRow('chain-usim','update-binary')" class="text-xs px-2 py-1 bg-blue-600 text-white rounded hover:bg-blue-700">+ UPDATE BINARY</button>
|
||||
<button onclick="chainAddRow('chain-usim','activate-file')" class="text-xs px-2 py-1 bg-blue-600 text-white rounded hover:bg-blue-700">+ ACTIVATE</button>
|
||||
<button onclick="chainAddRow('chain-usim','deactivate-file')" class="text-xs px-2 py-1 bg-blue-600 text-white rounded hover:bg-blue-700">+ DEACTIVATE</button>
|
||||
<button onclick="chainAddRow('chain-usim','verify')" class="text-xs px-2 py-1 bg-blue-600 text-white rounded hover:bg-blue-700">+ VERIFY PIN</button>
|
||||
<button onclick="chainAddRow('chain-usim','change')" class="text-xs px-2 py-1 bg-blue-600 text-white rounded hover:bg-blue-700">+ CHANGE PIN</button>
|
||||
<button onclick="chainAddRow('chain-usim','disable')" class="text-xs px-2 py-1 bg-blue-600 text-white rounded hover:bg-blue-700">+ DISABLE PIN</button>
|
||||
<button onclick="chainAddRow('chain-usim','enable')" class="text-xs px-2 py-1 bg-blue-600 text-white rounded hover:bg-blue-700">+ ENABLE PIN</button>
|
||||
<button onclick="chainAddRow('chain-usim','unblock')" class="text-xs px-2 py-1 bg-blue-600 text-white rounded hover:bg-blue-700">+ UNBLOCK PIN</button>
|
||||
<button onclick="chainAddRow('chain-usim','get-response')" class="text-xs px-2 py-1 bg-emerald-600 text-white rounded hover:bg-emerald-700">+ GET RESPONSE</button>
|
||||
</div>
|
||||
<textarea id="chain-usim-preview" rows="3" readonly class="w-full font-mono border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 bg-gray-100 dark:bg-slate-800" placeholder="Chain preview — add commands above"></textarea>
|
||||
<button onclick="packToSp('chain-usim-preview')" id="chain-usim-pack-btn" disabled class="mb-3 px-5 py-2.5 bg-emerald-600 text-white text-sm font-medium rounded hover:bg-emerald-700 disabled:opacity-40 disabled:cursor-not-allowed" data-l10n="Pack into Secured packet">Pack into Secured packet</button>
|
||||
@@ -116,20 +117,20 @@
|
||||
<div id="c-apdu-sub-ram" class="hidden">
|
||||
<div id="chain-ram-rows"></div>
|
||||
<div class="flex flex-wrap gap-1 mb-3">
|
||||
<button onclick="chainAddRow('chain-ram','install-load')" class="px-2 py-1 text-xs bg-blue-100 dark:bg-blue-900 text-blue-700 dark:text-blue-300 rounded hover:bg-blue-200 dark:hover:bg-blue-800">+ INSTALL [for load]</button>
|
||||
<button onclick="chainAddRow('chain-ram','install-install')" class="px-2 py-1 text-xs bg-blue-100 dark:bg-blue-900 text-blue-700 dark:text-blue-300 rounded hover:bg-blue-200 dark:hover:bg-blue-800">+ INSTALL [for install]</button>
|
||||
<button onclick="chainAddRow('chain-ram','install-make-sel')" class="px-2 py-1 text-xs bg-blue-100 dark:bg-blue-900 text-blue-700 dark:text-blue-300 rounded hover:bg-blue-200 dark:hover:bg-blue-800">+ INSTALL [for make selectable]</button>
|
||||
<button onclick="chainAddRow('chain-ram','install-reg-update')" class="px-2 py-1 text-xs bg-blue-100 dark:bg-blue-900 text-blue-700 dark:text-blue-300 rounded hover:bg-blue-200 dark:hover:bg-blue-800">+ INSTALL [for registry update]</button>
|
||||
<button onclick="chainAddRow('chain-ram','install-extradition')" class="px-2 py-1 text-xs bg-blue-100 dark:bg-blue-900 text-blue-700 dark:text-blue-300 rounded hover:bg-blue-200 dark:hover:bg-blue-800">+ INSTALL [for extradition]</button>
|
||||
<button onclick="chainAddRow('chain-ram','load')" class="px-2 py-1 text-xs bg-blue-100 dark:bg-blue-900 text-blue-700 dark:text-blue-300 rounded hover:bg-blue-200 dark:hover:bg-blue-800">+ LOAD</button>
|
||||
<button onclick="chainAddRow('chain-ram','delete')" class="px-2 py-1 text-xs bg-blue-100 dark:bg-blue-900 text-blue-700 dark:text-blue-300 rounded hover:bg-blue-200 dark:hover:bg-blue-800">+ DELETE</button>
|
||||
<button onclick="chainAddRow('chain-ram','get-status')" class="px-2 py-1 text-xs bg-blue-100 dark:bg-blue-900 text-blue-700 dark:text-blue-300 rounded hover:bg-blue-200 dark:hover:bg-blue-800">+ GET STATUS</button>
|
||||
<button onclick="chainAddRow('chain-ram','get-data')" class="px-2 py-1 text-xs bg-blue-100 dark:bg-blue-900 text-blue-700 dark:text-blue-300 rounded hover:bg-blue-200 dark:hover:bg-blue-800">+ GET DATA</button>
|
||||
<button onclick="chainAddRow('chain-ram','store-data')" class="px-2 py-1 text-xs bg-blue-100 dark:bg-blue-900 text-blue-700 dark:text-blue-300 rounded hover:bg-blue-200 dark:hover:bg-blue-800">+ STORE DATA</button>
|
||||
<button onclick="chainAddRow('chain-ram','set-status')" class="px-2 py-1 text-xs bg-blue-100 dark:bg-blue-900 text-blue-700 dark:text-blue-300 rounded hover:bg-blue-200 dark:hover:bg-blue-800">+ SET STATUS</button>
|
||||
<button onclick="chainAddRow('chain-ram','ext-auth')" class="px-2 py-1 text-xs bg-blue-100 dark:bg-blue-900 text-blue-700 dark:text-blue-300 rounded hover:bg-blue-200 dark:hover:bg-blue-800">+ EXTERNAL AUTH</button>
|
||||
<button onclick="chainAddRow('chain-ram','int-auth')" class="px-2 py-1 text-xs bg-blue-100 dark:bg-blue-900 text-blue-700 dark:text-blue-300 rounded hover:bg-blue-200 dark:hover:bg-blue-800">+ INTERNAL AUTH</button>
|
||||
<button onclick="chainAddRow('chain-ram','get-response')" class="px-2 py-1 text-xs bg-blue-100 dark:bg-blue-900 text-blue-700 dark:text-blue-300 rounded hover:bg-blue-200 dark:hover:bg-blue-800">+ GET RESPONSE</button>
|
||||
<button onclick="chainAddRow('chain-ram','install-load')" class="text-xs px-2 py-1 bg-blue-600 text-white rounded hover:bg-blue-700">+ INSTALL [for load]</button>
|
||||
<button onclick="chainAddRow('chain-ram','install-install')" class="text-xs px-2 py-1 bg-blue-600 text-white rounded hover:bg-blue-700">+ INSTALL [for install]</button>
|
||||
<button onclick="chainAddRow('chain-ram','install-make-sel')" class="text-xs px-2 py-1 bg-blue-600 text-white rounded hover:bg-blue-700">+ INSTALL [for make selectable]</button>
|
||||
<button onclick="chainAddRow('chain-ram','install-reg-update')" class="text-xs px-2 py-1 bg-blue-600 text-white rounded hover:bg-blue-700">+ INSTALL [for registry update]</button>
|
||||
<button onclick="chainAddRow('chain-ram','install-extradition')" class="text-xs px-2 py-1 bg-blue-600 text-white rounded hover:bg-blue-700">+ INSTALL [for extradition]</button>
|
||||
<button onclick="chainAddRow('chain-ram','load')" class="text-xs px-2 py-1 bg-blue-600 text-white rounded hover:bg-blue-700">+ LOAD</button>
|
||||
<button onclick="chainAddRow('chain-ram','delete')" class="text-xs px-2 py-1 bg-blue-600 text-white rounded hover:bg-blue-700">+ DELETE</button>
|
||||
<button onclick="chainAddRow('chain-ram','get-status')" class="text-xs px-2 py-1 bg-blue-600 text-white rounded hover:bg-blue-700">+ GET STATUS</button>
|
||||
<button onclick="chainAddRow('chain-ram','get-data')" class="text-xs px-2 py-1 bg-blue-600 text-white rounded hover:bg-blue-700">+ GET DATA</button>
|
||||
<button onclick="chainAddRow('chain-ram','store-data')" class="text-xs px-2 py-1 bg-blue-600 text-white rounded hover:bg-blue-700">+ STORE DATA</button>
|
||||
<button onclick="chainAddRow('chain-ram','set-status')" class="text-xs px-2 py-1 bg-blue-600 text-white rounded hover:bg-blue-700">+ SET STATUS</button>
|
||||
<button onclick="chainAddRow('chain-ram','ext-auth')" class="text-xs px-2 py-1 bg-blue-600 text-white rounded hover:bg-blue-700">+ EXTERNAL AUTH</button>
|
||||
<button onclick="chainAddRow('chain-ram','int-auth')" class="text-xs px-2 py-1 bg-blue-600 text-white rounded hover:bg-blue-700">+ INTERNAL AUTH</button>
|
||||
<button onclick="chainAddRow('chain-ram','get-response')" class="text-xs px-2 py-1 bg-emerald-600 text-white rounded hover:bg-emerald-700">+ GET RESPONSE</button>
|
||||
</div>
|
||||
<textarea id="chain-ram-preview" rows="3" readonly class="w-full font-mono border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 bg-gray-100 dark:bg-slate-800" placeholder="Chain preview — add commands above"></textarea>
|
||||
<button onclick="packToSp('chain-ram-preview')" id="chain-ram-pack-btn" disabled class="mb-3 px-5 py-2.5 bg-emerald-600 text-white text-sm font-medium rounded hover:bg-emerald-700 disabled:opacity-40 disabled:cursor-not-allowed" data-l10n="Pack into Secured packet">Pack into Secured packet</button>
|
||||
@@ -142,6 +143,119 @@
|
||||
<button onclick="parseHex()" class="mb-3 px-5 py-2.5 bg-blue-600 text-white text-sm font-medium rounded hover:bg-blue-700" data-l10n="Decode">Decode</button>
|
||||
<div id="parse-output"></div>
|
||||
</div>
|
||||
|
||||
<div id="c-apdu-sub-httpota" class="hidden">
|
||||
<div class="flex gap-2 mb-3">
|
||||
<button id="hota-mode-trigger" onclick="hotaSetMode('trigger')" class="px-4 py-1.5 text-sm rounded-full bg-blue-600 text-white" data-l10n="Trigger (Push SMS)">Trigger (Push SMS)</button>
|
||||
<button id="hota-mode-store" onclick="hotaSetMode('store')" class="px-4 py-1.5 text-sm rounded-full bg-gray-200 dark:bg-slate-700 text-gray-700 dark:text-slate-300 hover:bg-gray-300 dark:hover:bg-slate-600" data-l10n="Store (SD admin params)">Store (SD admin params)</button>
|
||||
</div>
|
||||
|
||||
<div id="hota-store-tag-row" class="mb-3 hidden">
|
||||
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300" data-l10n="SD params tag">SD params tag</label>
|
||||
<select id="hota-store-tag" onchange="hotaUpdatePreview()" class="w-full border border-gray-300 dark:border-slate-600 text-sm rounded px-2 py-1.5 dark:bg-slate-800">
|
||||
<option value="85">'85' — GP v2.2 Amendment B v1.1 (Table 4-4)</option>
|
||||
<option value="A5">'A5' — GP v2.2 Amendment B v1.1.3</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="border border-gray-300 dark:border-slate-600 rounded p-3 bg-gray-50 dark:bg-slate-800 mb-3">
|
||||
<label class="flex items-center gap-2 text-sm font-medium text-gray-700 dark:text-slate-300 mb-2">
|
||||
<input type="checkbox" id="hota-include-conn" checked onchange="hotaUpdatePreview()" class="rounded">
|
||||
<span data-l10n="Connection parameters ('84')">Connection parameters ('84')</span>
|
||||
</label>
|
||||
<div id="hota-conn-tlvs" class="hidden"></div>
|
||||
<div class="flex flex-wrap gap-1 mt-2">
|
||||
<button onclick="hotaAddTlv()" class="text-xs px-2 py-1 bg-blue-600 text-white rounded hover:bg-blue-700" data-l10n="+ TLV">+ TLV</button>
|
||||
<button onclick="hotaAddPreset('devid')" class="text-xs px-2 py-1 bg-blue-600 text-white rounded hover:bg-blue-700" data-l10n="+ Device Identities">+ Device Identities</button>
|
||||
<button onclick="hotaAddPreset('alpha')" class="text-xs px-2 py-1 bg-blue-600 text-white rounded hover:bg-blue-700" data-l10n="+ Alpha (empty)">+ Alpha (empty)</button>
|
||||
<button onclick="hotaAddPreset('bearer')" class="text-xs px-2 py-1 bg-blue-600 text-white rounded hover:bg-blue-700" data-l10n="+ Bearer (GPRS)">+ Bearer (GPRS)</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="border border-gray-300 dark:border-slate-600 rounded p-3 bg-gray-50 dark:bg-slate-800 mb-3">
|
||||
<label class="flex items-center gap-2 text-sm font-medium text-gray-700 dark:text-slate-300 mb-2">
|
||||
<input type="checkbox" id="hota-include-sec" onchange="hotaUpdatePreview()" class="rounded">
|
||||
<span data-l10n="Security parameters ('85')">Security parameters ('85')</span>
|
||||
</label>
|
||||
<div id="hota-sec-fields" class="hidden space-y-2">
|
||||
<div class="flex gap-2">
|
||||
<div class="flex-1"><label class="block text-xs text-gray-600 dark:text-slate-400" data-l10n="PSK Identity (text)">PSK Identity (text)</label>
|
||||
<input id="hota-psk-text" class="font-mono w-full border border-gray-300 dark:border-slate-600 text-sm rounded px-2 py-1.5 dark:bg-slate-800" placeholder="Test123">
|
||||
</div>
|
||||
<div class="w-16"><label class="block text-xs text-gray-600 dark:text-slate-400">KVN</label>
|
||||
<input id="hota-kvn" maxlength="2" value="01" class="font-mono w-full border border-gray-300 dark:border-slate-600 text-sm rounded px-2 py-1.5 dark:bg-slate-800">
|
||||
</div>
|
||||
<div class="w-16"><label class="block text-xs text-gray-600 dark:text-slate-400">KID</label>
|
||||
<input id="hota-kid" maxlength="2" value="01" class="font-mono w-full border border-gray-300 dark:border-slate-600 text-sm rounded px-2 py-1.5 dark:bg-slate-800">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="border border-gray-300 dark:border-slate-600 rounded p-3 bg-gray-50 dark:bg-slate-800 mb-3">
|
||||
<label class="flex items-center gap-2 text-sm font-medium text-gray-700 dark:text-slate-300 mb-2">
|
||||
<input type="checkbox" id="hota-include-retry" onchange="hotaUpdatePreview()" class="rounded">
|
||||
<span data-l10n="Retry policy ('86')">Retry policy ('86')</span>
|
||||
</label>
|
||||
<div id="hota-retry-fields" class="hidden space-y-2">
|
||||
<div class="flex gap-2">
|
||||
<div class="flex-1"><label class="block text-xs text-gray-600 dark:text-slate-400" data-l10n="Retry counter (B0,00=unlimited)">Retry counter (B0,00=unlimited)</label>
|
||||
<input id="hota-retry-counter" maxlength="4" class="font-mono w-full border border-gray-300 dark:border-slate-600 text-sm rounded px-2 py-1.5 dark:bg-slate-800" placeholder="B000">
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<div class="w-16"><label class="block text-xs text-gray-600 dark:text-slate-400" data-l10n="Hours">Hours</label>
|
||||
<input id="hota-delay-h" type="number" min="0" max="255" class="font-mono w-full border border-gray-300 dark:border-slate-600 text-sm rounded px-2 py-1.5 dark:bg-slate-800" placeholder="0">
|
||||
</div>
|
||||
<div class="w-16"><label class="block text-xs text-gray-600 dark:text-slate-400" data-l10n="Minutes">Minutes</label>
|
||||
<input id="hota-delay-m" type="number" min="0" max="59" class="font-mono w-full border border-gray-300 dark:border-slate-600 text-sm rounded px-2 py-1.5 dark:bg-slate-800" placeholder="0">
|
||||
</div>
|
||||
<div class="w-16"><label class="block text-xs text-gray-600 dark:text-slate-400" data-l10n="Seconds">Seconds</label>
|
||||
<input id="hota-delay-s" type="number" min="0" max="59" class="font-mono w-full border border-gray-300 dark:border-slate-600 text-sm rounded px-2 py-1.5 dark:bg-slate-800" placeholder="0">
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-2 items-end">
|
||||
<div class="flex-1"><label class="block text-xs text-gray-600 dark:text-slate-400" data-l10n="Report failure (vendor-specific TLV)">Report failure (vendor-specific TLV)</label>
|
||||
<input id="hota-retry-report" maxlength="40" class="font-mono w-full border border-gray-300 dark:border-slate-600 text-sm rounded px-2 py-1.5 dark:bg-slate-800" placeholder="0A08...">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="border border-gray-300 dark:border-slate-600 rounded p-3 bg-gray-50 dark:bg-slate-800 mb-3">
|
||||
<label class="flex items-center gap-2 text-sm font-medium text-gray-700 dark:text-slate-300 mb-2">
|
||||
<input type="checkbox" id="hota-include-http" onchange="hotaUpdatePreview()" class="rounded">
|
||||
<span data-l10n="HTTP POST ('89', 8A=host,8B=agent,8C=URI)">HTTP POST ('89', 8A=host,8B=agent,8C=URI)</span>
|
||||
</label>
|
||||
<div id="hota-http-fields" class="hidden space-y-2">
|
||||
<div class="flex gap-2">
|
||||
<div class="flex-1"><label class="block text-xs text-gray-600 dark:text-slate-400" data-l10n="Host (text→hex)">Host (text→hex)</label>
|
||||
<input id="hota-host" class="font-mono w-full border border-gray-300 dark:border-slate-600 text-sm rounded px-2 py-1.5 dark:bg-slate-800" placeholder="megafon.ru">
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<div class="flex-1"><label class="block text-xs text-gray-600 dark:text-slate-400" data-l10n="HTTP agent ID (text, X-Admin-From)">HTTP agent ID (text, X-Admin-From)</label>
|
||||
<input id="hota-agent" class="font-mono w-full border border-gray-300 dark:border-slate-600 text-sm rounded px-2 py-1.5 dark:bg-slate-800" placeholder="v1.0">
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<div class="flex-1"><label class="block text-xs text-gray-600 dark:text-slate-400" data-l10n="URI (text→hex)">URI (text→hex)</label>
|
||||
<input id="hota-uri" class="font-mono w-full border border-gray-300 dark:border-slate-600 text-sm rounded px-2 py-1.5 dark:bg-slate-800" placeholder="http://megafon.ru/sdp">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="border border-gray-300 dark:border-slate-600 rounded p-3 bg-gray-50 dark:bg-slate-800 mb-3 hidden" id="hota-expanded-row">
|
||||
<label class="flex items-center gap-2 text-sm font-medium text-gray-700 dark:text-slate-300">
|
||||
<input type="checkbox" id="hota-expanded" onchange="hotaUpdatePreview()" class="rounded">
|
||||
<span data-l10n="Wrapped in Command Scripting template ('AA')">Wrapped in Command Scripting template ('AA')</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<textarea id="hota-preview" rows="5" readonly oninput="hotaPreviewEdited()" class="w-full font-mono border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 bg-gray-100 dark:bg-slate-800 mb-3" placeholder="RAM-over-HTTP payload will appear here..."></textarea>
|
||||
<button onclick="packToSp('hota-preview')" id="hota-pack-btn" disabled class="mb-3 px-5 py-2.5 bg-emerald-600 text-white text-sm font-medium rounded hover:bg-emerald-700 disabled:opacity-40 disabled:cursor-not-allowed" data-l10n="Pack into Secured packet">Pack into Secured packet</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-1/4">
|
||||
<div class="border border-gray-300 dark:border-slate-600 rounded p-3 bg-gray-50 dark:bg-slate-800">
|
||||
@@ -798,12 +912,22 @@
|
||||
<h3 class="text-lg font-semibold mb-4 text-gray-800 dark:text-slate-200" data-l10n="Profile from card">Profile from card</h3>
|
||||
<label class="block mb-1 text-xs font-medium text-gray-500 dark:text-slate-400" data-l10n="Profile name">Profile name</label>
|
||||
<input id="profiler-scan-name" class="w-full font-mono border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-2 dark:bg-slate-800 mb-3" placeholder="Profile name">
|
||||
<div class="mb-1 text-xs font-medium text-gray-500 dark:text-slate-400" data-l10n="Ignore contents of:">Ignore contents of:</div>
|
||||
<div id="profiler-scan-ignore" class="space-y-1 mb-3 max-h-56 overflow-auto"></div>
|
||||
<div id="profiler-scan-options">
|
||||
<div class="mb-1 text-xs font-medium text-gray-500 dark:text-slate-400" data-l10n="Ignore contents of:">Ignore contents of:</div>
|
||||
<div id="profiler-scan-ignore" class="space-y-1 mb-3 max-h-56 overflow-auto"></div>
|
||||
<div id="profiler-scan-mask" class="space-y-1 mb-3"></div>
|
||||
<label class="block mb-1 text-xs font-medium text-gray-500 dark:text-slate-400" data-l10n="FCP/FCI check">FCP/FCI check</label>
|
||||
<select id="profiler-scan-fci-mode" class="w-full border border-gray-300 dark:border-slate-600 text-sm rounded px-2 py-1.5 dark:bg-slate-800 mb-3">
|
||||
<option value="type" data-l10n="Filetype only (FCP)">Filetype only (FCP)</option>
|
||||
<option value="type_size" selected data-l10n="Filetype + size (FCP)">Filetype + size (FCP)</option>
|
||||
<option value="exact" data-l10n="Exact FCI">Exact FCI</option>
|
||||
</select>
|
||||
</div>
|
||||
<div id="profiler-scan-progress" class="text-xs text-gray-500 dark:text-slate-400 mb-2 hidden"></div>
|
||||
<div id="profiler-scan-error" class="text-xs text-red-500 hidden mb-2"></div>
|
||||
<div class="flex gap-2 justify-end">
|
||||
<button onclick="profilerScanCancel()" class="px-3 py-1.5 text-xs rounded border border-gray-300 dark:border-slate-600 hover:bg-gray-100 dark:hover:bg-slate-700 text-gray-700 dark:text-slate-300" data-l10n="Cancel">Cancel</button>
|
||||
<button id="profiler-scan-btn" onclick="profilerScanStart()" class="px-4 py-1.5 text-xs rounded bg-blue-600 text-white hover:bg-blue-700" data-l10n="Scan">Scan</button>
|
||||
<button id="profiler-scan-cancel-btn" onclick="profilerScanCancel()" class="px-3 py-1.5 text-xs rounded border border-gray-300 dark:border-slate-600 hover:bg-gray-100 dark:hover:bg-slate-700 text-gray-700 dark:text-slate-300 disabled:opacity-40 disabled:cursor-not-allowed" data-l10n="Cancel">Cancel</button>
|
||||
<button id="profiler-scan-btn" onclick="profilerScanStart()" class="px-4 py-1.5 text-xs rounded bg-blue-600 text-white hover:bg-blue-700 disabled:opacity-40 disabled:cursor-not-allowed" data-l10n="Scan">Scan</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -874,12 +998,178 @@ function cApduSwitchSubtab(name) {
|
||||
btn.classList.toggle('text-gray-700', !active);
|
||||
btn.classList.toggle('dark:text-slate-300', !active);
|
||||
});
|
||||
['sim', 'usim', 'ber', 'ram', 'parse'].forEach(s => {
|
||||
['sim', 'usim', 'ber', 'ram', 'parse', 'httpota'].forEach(s => {
|
||||
const el = document.getElementById('c-apdu-sub-' + s);
|
||||
if (el) el.classList.toggle('hidden', s !== name);
|
||||
});
|
||||
setHelpAnchor({sim:'sim-rfm', usim:'usim-rfm', ber:'ber-tlv', ram:'ram-gp', parse:'c-apdu'}[name] || 'c-apdu');
|
||||
setHelpAnchor({sim:'sim-rfm', usim:'usim-rfm', ber:'ber-tlv', ram:'ram-gp', parse:'c-apdu', httpota:'http-ota'}[name] || 'c-apdu');
|
||||
}
|
||||
|
||||
// ===== HTTP OTA constructor =====
|
||||
let _hotaMode = 'trigger';
|
||||
function _hotaHex(s) { return (s || '').replace(/[^0-9a-fA-F]/g, '').toUpperCase(); }
|
||||
function _hotaAsciiHex(s) { return (s || '').split('').map(c => c.charCodeAt(0).toString(16).padStart(2, '0')).join('').toUpperCase(); }
|
||||
function _hotaBerLen(n) {
|
||||
if (n < 128) return n.toString(16).padStart(2, '0').toUpperCase();
|
||||
if (n < 256) return '81' + n.toString(16).padStart(2, '0').toUpperCase();
|
||||
return '82' + n.toString(16).padStart(4, '0').toUpperCase();
|
||||
}
|
||||
function _hotaTlv(tag8, val8) { const v = _hotaHex(val8); return tag8.toUpperCase() + _hotaBerLen(v.length / 2) + v; }
|
||||
|
||||
function hotaTlv(tag, value) { return _hotaTlv(tag, value); }
|
||||
function hotaBuildConn(tlvs) {
|
||||
let content = '';
|
||||
for (const t of tlvs) {
|
||||
const v = _hotaHex(t.value);
|
||||
if (!t.tag) continue;
|
||||
content += t.tag.toUpperCase() + _hotaBerLen(v.length / 2) + v;
|
||||
}
|
||||
return '84' + _hotaBerLen(content.length / 2) + content;
|
||||
}
|
||||
function hotaBuildSec(s) {
|
||||
const psk = _hotaAsciiHex(s.pskIdentity);
|
||||
const kvn = _hotaHex(s.kvn).padStart(2, '0');
|
||||
const kid = _hotaHex(s.kid).padStart(2, '0');
|
||||
const data = (psk.length / 2).toString(16).padStart(2, '0').toUpperCase() + psk + '02' + kvn + kid;
|
||||
return '85' + _hotaBerLen(data.length / 2) + data;
|
||||
}
|
||||
function hotaBuildRetry(r) {
|
||||
const counter = _hotaHex(r.counter);
|
||||
const hh = (parseInt(r.delayH) || 0).toString(16).padStart(2, '0').toUpperCase();
|
||||
const mm = (parseInt(r.delayM) || 0).toString(16).padStart(2, '0').toUpperCase();
|
||||
const ss = (parseInt(r.delayS) || 0).toString(16).padStart(2, '0').toUpperCase();
|
||||
const timerTlv = '2503' + hh + mm + ss;
|
||||
const report = _hotaHex(r.reportFailure);
|
||||
const content = counter + timerTlv + (report && report.length % 2 === 0 ? report : '');
|
||||
return '86' + _hotaBerLen(content.length / 2) + content;
|
||||
}
|
||||
function hotaBuildHttpPost(h) {
|
||||
const host = _hotaAsciiHex(h.host);
|
||||
const agent = _hotaAsciiHex(h.agent);
|
||||
const uri = _hotaAsciiHex(h.uri);
|
||||
const content = _hotaTlv('8A', host) + _hotaTlv('8B', agent) + _hotaTlv('8C', uri);
|
||||
return '89' + _hotaBerLen(content.length / 2) + content;
|
||||
}
|
||||
function hotaBuildTrigger(conn, sec, retry, httpPost, expanded) {
|
||||
let content = conn;
|
||||
if (sec) content += sec;
|
||||
if (retry) content += retry;
|
||||
if (httpPost) content += httpPost;
|
||||
const sd = '83' + _hotaBerLen(content.length / 2) + content;
|
||||
const trig = '81' + _hotaBerLen(sd.length / 2) + sd;
|
||||
if (expanded) return 'AA' + _hotaBerLen(trig.length / 2) + trig;
|
||||
return trig;
|
||||
}
|
||||
function hotaBuildStore(conn, sec, retry, httpPost, storeTag) {
|
||||
const inner = _hotaTlv(storeTag, conn + (sec || '') + (retry || '') + (httpPost || ''));
|
||||
return '80E29000' + _hotaBerLen(inner.length / 2) + inner;
|
||||
}
|
||||
function hotaBuild(mode, conn, sec, retry, httpPost, storeTag, expanded) {
|
||||
return mode === 'store'
|
||||
? hotaBuildStore(conn, sec, retry, httpPost, storeTag)
|
||||
: hotaBuildTrigger(conn, sec, retry, httpPost, expanded);
|
||||
}
|
||||
|
||||
function hotaSetMode(mode) {
|
||||
_hotaMode = mode;
|
||||
document.getElementById('hota-mode-trigger').className = 'px-4 py-1.5 text-sm rounded-full ' + (mode === 'trigger' ? 'bg-blue-600 text-white' : 'bg-gray-200 dark:bg-slate-700 text-gray-700 dark:text-slate-300 hover:bg-gray-300 dark:hover:bg-slate-600');
|
||||
document.getElementById('hota-mode-store').className = 'px-4 py-1.5 text-sm rounded-full ' + (mode === 'store' ? 'bg-blue-600 text-white' : 'bg-gray-200 dark:bg-slate-700 text-gray-700 dark:text-slate-300 hover:bg-gray-300 dark:hover:bg-slate-600');
|
||||
document.getElementById('hota-store-tag-row').classList.toggle('hidden', mode !== 'store');
|
||||
document.getElementById('hota-expanded-row').classList.toggle('hidden', mode !== 'trigger');
|
||||
hotaUpdatePreview();
|
||||
}
|
||||
|
||||
function hotaToggleConn() {
|
||||
const on = document.getElementById('hota-include-conn').checked;
|
||||
document.getElementById('hota-conn-tlvs').classList.toggle('hidden', !on);
|
||||
hotaUpdatePreview();
|
||||
}
|
||||
document.addEventListener('change', function(e) {
|
||||
if (e.target && e.target.id === 'hota-include-conn') hotaToggleConn();
|
||||
else if (e.target && e.target.id === 'hota-include-sec') document.getElementById('hota-sec-fields').classList.toggle('hidden', !e.target.checked);
|
||||
else if (e.target && e.target.id === 'hota-include-retry') document.getElementById('hota-retry-fields').classList.toggle('hidden', !e.target.checked);
|
||||
else if (e.target && e.target.id === 'hota-include-http') document.getElementById('hota-http-fields').classList.toggle('hidden', !e.target.checked);
|
||||
});
|
||||
hotaToggleConn();
|
||||
|
||||
function hotaAddTlv() {
|
||||
const container = document.getElementById('hota-conn-tlvs');
|
||||
const row = document.createElement('div');
|
||||
row.className = 'flex gap-1 items-center mb-1';
|
||||
row.innerHTML =
|
||||
'<input maxlength="2" oninput="hotaUpdatePreview()" class="font-mono w-14 border border-gray-300 dark:border-slate-600 text-sm rounded px-1.5 py-1 dark:bg-slate-800" placeholder="Tag">' +
|
||||
'<input oninput="hotaUpdatePreview()" class="font-mono flex-1 border border-gray-300 dark:border-slate-600 text-sm rounded px-1.5 py-1 dark:bg-slate-800" placeholder="Value">' +
|
||||
'<button onclick="this.parentElement.remove();hotaUpdatePreview()" class="text-gray-400 hover:text-red-500 px-1" title="Delete">×</button>';
|
||||
container.appendChild(row);
|
||||
hotaUpdatePreview();
|
||||
}
|
||||
function hotaAddPreset(name) {
|
||||
const container = document.getElementById('hota-conn-tlvs');
|
||||
const row = document.createElement('div');
|
||||
row.className = 'flex gap-1 items-center mb-1';
|
||||
const presets = {
|
||||
devid: ['02', '8182'],
|
||||
alpha: ['80', ''],
|
||||
bearer: ['01', '014001'],
|
||||
};
|
||||
const [tag, value] = presets[name] || ['', ''];
|
||||
row.innerHTML =
|
||||
'<input maxlength="2" value="' + tag + '" oninput="hotaUpdatePreview()" class="font-mono w-14 border border-gray-300 dark:border-slate-600 text-sm rounded px-1.5 py-1 dark:bg-slate-800" placeholder="Tag">' +
|
||||
'<input value="' + value + '" oninput="hotaUpdatePreview()" class="font-mono flex-1 border border-gray-300 dark:border-slate-600 text-sm rounded px-1.5 py-1 dark:bg-slate-800" placeholder="Value">' +
|
||||
'<button onclick="this.parentElement.remove();hotaUpdatePreview()" class="text-gray-400 hover:text-red-500 px-1" title="Delete">×</button>';
|
||||
container.appendChild(row);
|
||||
hotaUpdatePreview();
|
||||
}
|
||||
|
||||
function hotaReadConnTlvs() {
|
||||
const tlvs = [];
|
||||
document.querySelectorAll('#hota-conn-tlvs .flex').forEach(row => {
|
||||
const inputs = row.querySelectorAll('input');
|
||||
if (inputs.length >= 2) tlvs.push({ tag: _hotaHex(inputs[0].value), value: inputs[1].value });
|
||||
});
|
||||
return tlvs;
|
||||
}
|
||||
|
||||
function hotaUpdatePreview() {
|
||||
const connOn = document.getElementById('hota-include-conn').checked;
|
||||
const secOn = document.getElementById('hota-include-sec').checked;
|
||||
const retryOn = document.getElementById('hota-include-retry').checked;
|
||||
const httpOn = document.getElementById('hota-include-http').checked;
|
||||
const expanded = document.getElementById('hota-expanded').checked;
|
||||
const storeTag = document.getElementById('hota-store-tag').value;
|
||||
const conn = connOn ? hotaBuildConn(hotaReadConnTlvs()) : '';
|
||||
const sec = secOn ? hotaBuildSec({
|
||||
pskIdentity: document.getElementById('hota-psk-text').value,
|
||||
kvn: document.getElementById('hota-kvn').value,
|
||||
kid: document.getElementById('hota-kid').value,
|
||||
}) : '';
|
||||
const retry = retryOn ? hotaBuildRetry({
|
||||
counter: document.getElementById('hota-retry-counter').value,
|
||||
delayH: document.getElementById('hota-delay-h').value,
|
||||
delayM: document.getElementById('hota-delay-m').value,
|
||||
delayS: document.getElementById('hota-delay-s').value,
|
||||
reportFailure: document.getElementById('hota-retry-report').value,
|
||||
}) : '';
|
||||
const http = httpOn ? hotaBuildHttpPost({
|
||||
host: document.getElementById('hota-host').value,
|
||||
agent: document.getElementById('hota-agent').value,
|
||||
uri: document.getElementById('hota-uri').value,
|
||||
}) : '';
|
||||
const hex = hotaBuild(_hotaMode, conn, sec, retry, http, storeTag, expanded);
|
||||
const preview = document.getElementById('hota-preview');
|
||||
preview.value = hex;
|
||||
document.getElementById('hota-pack-btn').disabled = !hex;
|
||||
}
|
||||
|
||||
function hotaPreviewEdited() {
|
||||
const hex = document.getElementById('hota-preview').value;
|
||||
document.getElementById('hota-pack-btn').disabled = !hex;
|
||||
}
|
||||
document.querySelectorAll('#c-apdu-sub-httpota input, #c-apdu-sub-httpota select').forEach(el => {
|
||||
if (el.type === 'checkbox') return;
|
||||
el.addEventListener('input', hotaUpdatePreview);
|
||||
});
|
||||
|
||||
document.querySelectorAll('.tab-btn').forEach(btn => {
|
||||
btn.addEventListener('click', () => switchTab(btn.dataset.tab));
|
||||
});
|
||||
@@ -1223,8 +1513,8 @@ function chainRenderRow(chainId, idx, row) {
|
||||
const hex = chainBuildRowHex(chainId, idx);
|
||||
return '<div class="flex items-center gap-2 mb-2">' +
|
||||
'<select onchange="chainUpdateCmd(\'' + chainId + '\',' + idx + ',this.value)" class="flex-shrink-0 w-52 border border-gray-300 dark:border-slate-600 text-sm rounded px-2 py-1 dark:bg-slate-800">' + cmdOpts + '</select>' +
|
||||
'<button onclick="chainDeleteRow(\'' + chainId + '\',' + idx + ')" class="text-red-500 hover:text-red-700 text-lg font-bold px-1" title="Delete">×</button>' +
|
||||
'<input id="' + chainId + '-row-' + idx + '-hex" readonly value="' + escHtml(hex) + '" class="flex-1 font-mono text-xs border border-gray-300 dark:border-slate-600 rounded px-2 py-1 bg-gray-50 dark:bg-slate-900">' +
|
||||
'<button onclick="chainDeleteRow(\'' + chainId + '\',' + idx + ')" class="text-red-500 hover:text-red-700 text-lg font-bold px-1" title="Delete">✕</button>' +
|
||||
'</div>' +
|
||||
'<div class="pl-4">' + fieldsHtml + '</div>';
|
||||
}
|
||||
@@ -6666,7 +6956,7 @@ const PROFILER_IGNORE_FILES = [
|
||||
{ fid: '6F09', name: 'EF.KeysPS' },
|
||||
{ fid: '6F3C', name: 'EF.SMS' },
|
||||
{ fid: '6F20', name: 'EF.Kc' },
|
||||
{ fid: '4F52', name: 'EF.KcGPRS' },
|
||||
{ fid: '6F52', name: 'EF.KcGPRS' },
|
||||
{ fid: '6F53', name: 'EF.LOCIGPRS' },
|
||||
{ fid: '6F48', name: 'EF.CBMID' },
|
||||
{ fid: '6F43', name: 'EF.SMSS' },
|
||||
@@ -6698,6 +6988,11 @@ function profilerNormHex(s) {
|
||||
return (s || '').toUpperCase().replace(/[^0-9A-F?]/g, '');
|
||||
}
|
||||
|
||||
// Strict hex normalization for byte-exact comparisons (no '?' wildcards).
|
||||
function profilerNormHexStrict(s) {
|
||||
return (s || '').toUpperCase().replace(/[^0-9A-F]/g, '');
|
||||
}
|
||||
|
||||
// Match an expected hex (may contain '?' nibble wildcards) against actual hex.
|
||||
function profilerMatch(mode, expected, actual) {
|
||||
const e = profilerNormHex(expected);
|
||||
@@ -6757,6 +7052,7 @@ function profilerValidateProfile(obj) {
|
||||
if (!r || typeof r !== 'object') return 'Invalid rule';
|
||||
if (r.type !== 'file') return 'Unsupported rule type: ' + r.type;
|
||||
if (!r.path) return 'Rule missing path';
|
||||
if (r.fciMode && !['type', 'type_size', 'exact'].includes(r.fciMode)) return 'Invalid FCP/FCI mode: ' + r.fciMode;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -6802,15 +7098,30 @@ function profilerNew() {
|
||||
|
||||
function profilerFromCard() {
|
||||
document.getElementById('profiler-scan-name').value = '';
|
||||
document.getElementById('profiler-scan-name').readOnly = false;
|
||||
document.getElementById('profiler-scan-name').classList.remove('opacity-50');
|
||||
const ignore = document.getElementById('profiler-scan-ignore');
|
||||
let html = '';
|
||||
for (const f of PROFILER_IGNORE_FILES) {
|
||||
html += '<label class="flex items-center gap-2 text-sm">' +
|
||||
'<input type="checkbox" data-ignore-fid="' + f.fid + '" checked>' +
|
||||
'<input type="checkbox" data-ignore-fid="' + f.fid + '" data-ignore-name="' + f.name + '" checked>' +
|
||||
'<span>' + esc(t('Ignore contents of')) + ' ' + esc(f.name) + '</span></label>';
|
||||
}
|
||||
ignore.innerHTML = html;
|
||||
const mask = document.getElementById('profiler-scan-mask');
|
||||
let mhtml = '';
|
||||
for (const fid in PROFILER_MASK_PREFIX4_FIDS) {
|
||||
mhtml += '<label class="flex items-center gap-2 text-sm">' +
|
||||
'<input type="checkbox" data-mask-fid="' + fid + '" checked>' +
|
||||
'<span>' + esc(t('Match first 4 bytes for')) + ' ' + esc(PROFILER_MASK_PREFIX4_FIDS[fid]) + '</span></label>';
|
||||
}
|
||||
mask.innerHTML = mhtml;
|
||||
document.getElementById('profiler-scan-options').classList.remove('hidden');
|
||||
document.getElementById('profiler-scan-progress').classList.add('hidden');
|
||||
document.getElementById('profiler-scan-error').classList.add('hidden');
|
||||
document.getElementById('profiler-scan-cancel-btn').disabled = false;
|
||||
document.getElementById('profiler-scan-btn').disabled = false;
|
||||
document.getElementById('profiler-scan-btn').textContent = t('Scan');
|
||||
document.getElementById('profiler-scan-modal').classList.remove('hidden');
|
||||
}
|
||||
|
||||
@@ -6827,14 +7138,34 @@ async function profilerScanStart() {
|
||||
return;
|
||||
}
|
||||
const ignoreFids = new Set();
|
||||
const ignoreNames = new Set();
|
||||
document.querySelectorAll('#profiler-scan-ignore input[data-ignore-fid]').forEach(cb => {
|
||||
if (cb.checked) ignoreFids.add(cb.getAttribute('data-ignore-fid').toUpperCase());
|
||||
if (cb.checked) {
|
||||
ignoreFids.add(cb.getAttribute('data-ignore-fid').toUpperCase());
|
||||
ignoreNames.add((cb.getAttribute('data-ignore-name') || '').toUpperCase());
|
||||
}
|
||||
});
|
||||
const maskFids = new Set();
|
||||
document.querySelectorAll('#profiler-scan-mask input[data-mask-fid]').forEach(cb => {
|
||||
if (cb.checked) maskFids.add(cb.getAttribute('data-mask-fid').toUpperCase());
|
||||
});
|
||||
const fciMode = document.getElementById('profiler-scan-fci-mode').value;
|
||||
const btn = document.getElementById('profiler-scan-btn');
|
||||
const cancelBtn = document.getElementById('profiler-scan-cancel-btn');
|
||||
const nameEl = document.getElementById('profiler-scan-name');
|
||||
const progEl = document.getElementById('profiler-scan-progress');
|
||||
document.getElementById('profiler-scan-options').classList.add('hidden');
|
||||
cancelBtn.disabled = true;
|
||||
nameEl.readOnly = true;
|
||||
nameEl.classList.add('opacity-50');
|
||||
progEl.classList.remove('hidden');
|
||||
progEl.textContent = t('Discovering...');
|
||||
btn.disabled = true;
|
||||
btn.textContent = t('Scanning...');
|
||||
try {
|
||||
const rules = await profilerScanCard(ignoreFids);
|
||||
const rules = await profilerScanCard(ignoreFids, ignoreNames, fciMode, (done, total, path) => {
|
||||
progEl.textContent = done + ' / ' + total + ' ' + t('files') + (path ? ' — ' + path : '');
|
||||
}, maskFids);
|
||||
const profile = { id: profilerNewId(), name: name, created: new Date().toISOString(), rules: rules };
|
||||
profiles.push(profile);
|
||||
profilerSave();
|
||||
@@ -6844,14 +7175,23 @@ async function profilerScanStart() {
|
||||
errEl.textContent = e.message;
|
||||
errEl.classList.remove('hidden');
|
||||
} finally {
|
||||
document.getElementById('profiler-scan-options').classList.remove('hidden');
|
||||
cancelBtn.disabled = false;
|
||||
nameEl.readOnly = false;
|
||||
nameEl.classList.remove('opacity-50');
|
||||
progEl.classList.add('hidden');
|
||||
btn.disabled = false;
|
||||
btn.textContent = t('Scan');
|
||||
}
|
||||
}
|
||||
|
||||
// Recursively walk the card filesystem and build rules for files that exist.
|
||||
async function profilerScanCard(ignoreFids) {
|
||||
// Phase 1 discovers every file (tree calls only) to know the total; phase 2
|
||||
// builds a rule per file, reporting progress via the optional onProgress
|
||||
// callback as onProgress(done, total, path).
|
||||
async function profilerScanCard(ignoreFids, ignoreNames, fciMode, onProgress, maskFids) {
|
||||
const rules = [];
|
||||
const files = [];
|
||||
const seen = new Set();
|
||||
|
||||
// dir: { name, fid, parentSel (to select this dir), pathPrefix (rule-path segs for its children) }
|
||||
@@ -6877,8 +7217,7 @@ async function profilerScanCard(ignoreFids) {
|
||||
const childPath = dir.pathPrefix.concat(c.fid ? c.fid.toUpperCase() : c.name).join('/');
|
||||
if (seen.has(childPath)) continue;
|
||||
seen.add(childPath);
|
||||
const rule = await profilerBuildFileRule(childPath, c, ignoreFids);
|
||||
if (rule) rules.push(rule);
|
||||
files.push({ path: childPath, child: c });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6896,13 +7235,21 @@ async function profilerScanCard(ignoreFids) {
|
||||
const path = segs.join('/');
|
||||
if (seen.has(path)) continue;
|
||||
seen.add(path);
|
||||
const rule = await profilerBuildFileRule(path, { fid: cf.fid, name: cf.name }, ignoreFids);
|
||||
files.push({ path: path, child: { fid: cf.fid, name: cf.name } });
|
||||
}
|
||||
|
||||
const total = files.length;
|
||||
if (onProgress) onProgress(0, total, '');
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
const entry = files[i];
|
||||
if (onProgress) onProgress(i + 1, total, entry.path);
|
||||
const rule = await profilerBuildFileRule(entry.path, entry.child, ignoreFids, ignoreNames, fciMode, maskFids);
|
||||
if (rule) rules.push(rule);
|
||||
}
|
||||
return rules;
|
||||
}
|
||||
|
||||
async function profilerBuildFileRule(path, c, ignoreFids) {
|
||||
async function profilerBuildFileRule(path, c, ignoreFids, ignoreNames, fciMode, maskFids) {
|
||||
let sel;
|
||||
try {
|
||||
sel = await pysimFetch('/api/select', { path: path });
|
||||
@@ -6912,14 +7259,18 @@ async function profilerBuildFileRule(path, c, ignoreFids) {
|
||||
const rule = {
|
||||
type: 'file',
|
||||
path: path,
|
||||
name: c.name || sel.name || null,
|
||||
fileType: sel.file_type || null,
|
||||
fileSize: isRecord ? null : ((sel.file_size === null || sel.file_size === undefined) ? null : sel.file_size),
|
||||
recordLen: (sel.record_len === null || sel.record_len === undefined) ? null : sel.record_len,
|
||||
numRecords: (sel.num_of_rec === null || sel.num_of_rec === undefined) ? null : sel.num_of_rec,
|
||||
fciMode: fciMode || 'type_size',
|
||||
fciHex: sel.fci_hex || null,
|
||||
content: null,
|
||||
};
|
||||
const fid = (c.fid || sel.fid || '').toUpperCase();
|
||||
if (ignoreFids.has(fid)) return rule;
|
||||
const name = (c.name || sel.name || '').toUpperCase();
|
||||
if (ignoreFids.has(fid) || (ignoreNames && ignoreNames.has(name))) return rule;
|
||||
try {
|
||||
const rd = await pysimFetch('/api/read', { path: path, mode: 'raw' });
|
||||
if (rd && rd.success) {
|
||||
@@ -6929,7 +7280,8 @@ async function profilerBuildFileRule(path, c, ignoreFids) {
|
||||
records: rd.records.map(r => ({ num: r.num, data: r.data })),
|
||||
};
|
||||
} else if (rd.data) {
|
||||
if (PROFILER_MASK_PREFIX4_FIDS[fid]) {
|
||||
const useMask = maskFids ? maskFids.has(fid) : !!PROFILER_MASK_PREFIX4_FIDS[fid];
|
||||
if (useMask) {
|
||||
rule.content = { mode: 'mask', kind: 'transparent', expected: profilerMaskPrefix4(rd.data) };
|
||||
} else {
|
||||
rule.content = { mode: 'exact', kind: 'transparent', expected: rd.data };
|
||||
@@ -6992,7 +7344,7 @@ function profilerBackToList() {
|
||||
|
||||
function profilerAddRule() {
|
||||
if (!profilerDraft) return;
|
||||
profilerDraft.rules.push({ type: 'file', path: '', fileType: null, fileSize: null, recordLen: null, numRecords: null, content: null });
|
||||
profilerDraft.rules.push({ type: 'file', path: '', name: null, fileType: null, fileSize: null, recordLen: null, numRecords: null, fciMode: 'type_size', fciHex: null, content: null });
|
||||
profilerRenderEditor();
|
||||
}
|
||||
|
||||
@@ -7024,30 +7376,45 @@ function profilerRenderRule(i, r) {
|
||||
ftOpts += '<option value="' + k + '"' + (r.fileType === k ? ' selected' : '') + '>' + fts[k] + '</option>';
|
||||
}
|
||||
const cm = r.content ? r.content.mode : 'exact';
|
||||
const fciMode = r.fciMode || 'type_size';
|
||||
let html = '<div class="mb-2 p-3 border border-gray-200 dark:border-slate-600 rounded">';
|
||||
html += '<div class="flex items-center gap-2 mb-2">';
|
||||
html += '<span class="text-xs font-semibold text-gray-500 dark:text-slate-400">#' + (i + 1) + '</span>';
|
||||
html += '<button onclick="profilerDeleteRule(' + i + ')" class="ml-auto px-2 py-0.5 text-xs rounded bg-red-600 text-white hover:bg-red-700">' + esc(t('Delete')) + '</button>';
|
||||
html += '</div>';
|
||||
html += '<div class="grid grid-cols-1 gap-2 mb-2">';
|
||||
html += '<div><label class="block text-xs text-gray-600 dark:text-slate-400 mb-0.5">' + esc(t('Path')) + '</label>' +
|
||||
'<input value="' + escHtml(r.path || '') + '" oninput="profilerUpdateRule(' + i + ',\'path\',this.value)" class="w-full font-mono text-xs border border-gray-300 dark:border-slate-600 rounded px-2 py-1 dark:bg-slate-800" placeholder="MF/7F10/6F3A"></div>';
|
||||
const sname = profilerCustomNameForPath(r.path) || r.name || '';
|
||||
html += '<div class="flex gap-2 mb-2">';
|
||||
html += '<div class="w-72 shrink-0"><label class="block text-xs text-gray-600 dark:text-slate-400 mb-0.5">' + esc(t('Path')) +
|
||||
' <span class="profiler-rule-name font-normal text-gray-400 dark:text-slate-500">' + esc(sname) + '</span></label>' +
|
||||
'<input value="' + escHtml(r.path || '') + '" oninput="profilerUpdateRulePath(' + i + ',this.value,this)" class="w-full font-mono text-xs border border-gray-300 dark:border-slate-600 rounded px-2 py-1 dark:bg-slate-800" placeholder="MF/7F10/6F3A"></div>';
|
||||
html += '<div class="flex-1"><label class="block text-xs text-gray-600 dark:text-slate-400 mb-0.5">' + esc(t('FCP/FCI check')) + '</label>' +
|
||||
'<select onchange="profilerUpdateRuleFciMode(' + i + ',this.value)" class="w-full border border-gray-300 dark:border-slate-600 text-xs rounded px-2 py-1 dark:bg-slate-800">' +
|
||||
'<option value="type"' + (fciMode === 'type' ? ' selected' : '') + '>' + esc(t('Filetype only (FCP)')) + '</option>' +
|
||||
'<option value="type_size"' + (fciMode === 'type_size' ? ' selected' : '') + '>' + esc(t('Filetype + size (FCP)')) + '</option>' +
|
||||
'<option value="exact"' + (fciMode === 'exact' ? ' selected' : '') + '>' + esc(t('Exact FCI')) + '</option>' +
|
||||
'</select></div>';
|
||||
html += '</div>';
|
||||
const vis = profilerFileFields(r.fileType);
|
||||
html += '<div class="grid grid-cols-2 md:grid-cols-4 gap-2 mb-2">';
|
||||
html += '<div><label class="block text-xs text-gray-600 dark:text-slate-400 mb-0.5">' + esc(t('File type')) + '</label>' +
|
||||
const showSize = vis.size && fciMode !== 'type';
|
||||
const showRecords = vis.records && fciMode !== 'type';
|
||||
html += '<div class="flex gap-2 mb-2">';
|
||||
html += '<div class="flex-1"><label class="block text-xs text-gray-600 dark:text-slate-400 mb-0.5">' + esc(t('File type')) + '</label>' +
|
||||
'<select onchange="profilerUpdateRuleFileType(' + i + ',this.value||null)" class="w-full border border-gray-300 dark:border-slate-600 text-xs rounded px-2 py-1 dark:bg-slate-800">' + ftOpts + '</select></div>';
|
||||
if (vis.size) {
|
||||
html += '<div><label class="block text-xs text-gray-600 dark:text-slate-400 mb-0.5">' + esc(t('Size')) + '</label>' +
|
||||
if (showSize) {
|
||||
html += '<div class="flex-1"><label class="block text-xs text-gray-600 dark:text-slate-400 mb-0.5">' + esc(t('Size')) + '</label>' +
|
||||
'<input type="number" value="' + (r.fileSize === null ? '' : r.fileSize) + '" oninput="profilerUpdateRule(' + i + ',\'fileSize\',this.value===\'\'?null:parseInt(this.value))" class="w-full font-mono text-xs border border-gray-300 dark:border-slate-600 rounded px-2 py-1 dark:bg-slate-800" placeholder="—"></div>';
|
||||
}
|
||||
if (vis.records) {
|
||||
html += '<div><label class="block text-xs text-gray-600 dark:text-slate-400 mb-0.5">' + esc(t('Record length')) + '</label>' +
|
||||
if (showRecords) {
|
||||
html += '<div class="flex-1"><label class="block text-xs text-gray-600 dark:text-slate-400 mb-0.5">' + esc(t('Record length')) + '</label>' +
|
||||
'<input type="number" value="' + (r.recordLen === null ? '' : r.recordLen) + '" oninput="profilerUpdateRule(' + i + ',\'recordLen\',this.value===\'\'?null:parseInt(this.value))" class="w-full font-mono text-xs border border-gray-300 dark:border-slate-600 rounded px-2 py-1 dark:bg-slate-800" placeholder="—"></div>';
|
||||
html += '<div><label class="block text-xs text-gray-600 dark:text-slate-400 mb-0.5">' + esc(t('Record count')) + '</label>' +
|
||||
html += '<div class="flex-1"><label class="block text-xs text-gray-600 dark:text-slate-400 mb-0.5">' + esc(t('Record count')) + '</label>' +
|
||||
'<input type="number" value="' + (r.numRecords === null ? '' : r.numRecords) + '" oninput="profilerUpdateRule(' + i + ',\'numRecords\',this.value===\'\'?null:parseInt(this.value))" class="w-full font-mono text-xs border border-gray-300 dark:border-slate-600 rounded px-2 py-1 dark:bg-slate-800" placeholder="—"></div>';
|
||||
}
|
||||
html += '</div>';
|
||||
if (fciMode === 'exact') {
|
||||
html += '<div class="mb-2"><label class="block text-xs text-gray-600 dark:text-slate-400 mb-0.5">' + esc(t('FCI hex (raw SELECT response)')) + '</label>' +
|
||||
'<textarea oninput="profilerUpdateRule(' + i + ',\'fciHex\',this.value)" rows="2" class="w-full font-mono text-xs border border-gray-300 dark:border-slate-600 rounded px-2 py-1 dark:bg-slate-800" placeholder="62...">' + escHtml(r.fciHex || '') + '</textarea></div>';
|
||||
}
|
||||
if (r.fileType !== 'df') {
|
||||
html += profilerRenderContentEditor(i, r);
|
||||
}
|
||||
@@ -7058,12 +7425,13 @@ function profilerRenderRule(i, r) {
|
||||
function profilerRenderContentEditor(i, r) {
|
||||
const c = r.content;
|
||||
const mode = c ? c.mode : 'exact';
|
||||
let html = '<div class="mb-1 text-xs text-gray-600 dark:text-slate-400">' + esc(t('Contents')) + '</div>';
|
||||
html += '<div class="flex items-center gap-3 mb-1">';
|
||||
let html = '<div class="flex items-center justify-between mb-1">';
|
||||
html += '<span class="text-xs text-gray-600 dark:text-slate-400">' + esc(t('Check contents')) + '</span>';
|
||||
html += '<div class="flex items-center gap-3">';
|
||||
html += '<label class="text-xs flex items-center gap-1"><input type="radio" name="profiler-cm-' + i + '" value="none"' + (!c ? ' checked' : '') + ' onchange="profilerSetContent(' + i + ',null)"> ' + esc(t('None')) + '</label>';
|
||||
html += '<label class="text-xs flex items-center gap-1"><input type="radio" name="profiler-cm-' + i + '" value="exact"' + (c && mode === 'exact' ? ' checked' : '') + ' onchange="profilerSetContent(' + i + ',\'exact\')"> ' + esc(t('Exact')) + '</label>';
|
||||
html += '<label class="text-xs flex items-center gap-1"><input type="radio" name="profiler-cm-' + i + '" value="mask"' + (c && mode === 'mask' ? ' checked' : '') + ' onchange="profilerSetContent(' + i + ',\'mask\')"> ' + esc(t('Mask')) + '</label>';
|
||||
html += '</div>';
|
||||
html += '</div></div>';
|
||||
if (c) {
|
||||
if (c.kind === 'record') {
|
||||
html += '<div class="space-y-1">';
|
||||
@@ -7090,6 +7458,17 @@ function profilerUpdateRule(i, field, value) {
|
||||
profilerDraft.rules[i][field] = value;
|
||||
}
|
||||
|
||||
// Path edits drop the scan-time symbolic name (it no longer corresponds);
|
||||
// a custom-file lookup still supplies a display name for known paths.
|
||||
function profilerUpdateRulePath(i, value, input) {
|
||||
if (!profilerDraft) return;
|
||||
const r = profilerDraft.rules[i];
|
||||
r.path = value;
|
||||
r.name = null;
|
||||
const span = input && input.parentElement ? input.parentElement.querySelector('.profiler-rule-name') : null;
|
||||
if (span) span.textContent = profilerCustomNameForPath(value) || '';
|
||||
}
|
||||
|
||||
function profilerUpdateRuleFileType(i, value) {
|
||||
if (!profilerDraft) return;
|
||||
const r = profilerDraft.rules[i];
|
||||
@@ -7108,6 +7487,12 @@ function profilerUpdateRuleFileType(i, value) {
|
||||
profilerRenderEditor();
|
||||
}
|
||||
|
||||
function profilerUpdateRuleFciMode(i, value) {
|
||||
if (!profilerDraft) return;
|
||||
profilerDraft.rules[i].fciMode = value;
|
||||
profilerRenderEditor();
|
||||
}
|
||||
|
||||
function profilerSetContent(i, mode) {
|
||||
if (!profilerDraft) return;
|
||||
const r = profilerDraft.rules[i];
|
||||
@@ -7214,20 +7599,30 @@ async function profilerRunRule(r) {
|
||||
res.checks.push({ label: 'fileType', expected: r.fileType, actual: sel.file_type, ok: ok });
|
||||
}
|
||||
const isRecord = profilerContentKindForFileType(r.fileType) === 'record';
|
||||
if (!isRecord && r.fileSize !== null && r.fileSize !== undefined) {
|
||||
const ok = sel.file_size === r.fileSize;
|
||||
if (!ok) { res.status = 'fail'; sizeMismatch = true; }
|
||||
res.checks.push({ label: 'fileSize', expected: r.fileSize, actual: sel.file_size, ok: ok });
|
||||
const fciMode = r.fciMode || 'type_size';
|
||||
if (fciMode !== 'type') {
|
||||
if (!isRecord && r.fileSize !== null && r.fileSize !== undefined) {
|
||||
const ok = sel.file_size === r.fileSize;
|
||||
if (!ok) { res.status = 'fail'; sizeMismatch = true; }
|
||||
res.checks.push({ label: 'fileSize', expected: r.fileSize, actual: sel.file_size, ok: ok });
|
||||
}
|
||||
if (r.recordLen !== null && r.recordLen !== undefined) {
|
||||
const ok = sel.record_len === r.recordLen;
|
||||
if (!ok) { res.status = 'fail'; sizeMismatch = true; }
|
||||
res.checks.push({ label: 'recordLen', expected: r.recordLen, actual: sel.record_len, ok: ok });
|
||||
}
|
||||
if (r.numRecords !== null && r.numRecords !== undefined) {
|
||||
const ok = sel.num_of_rec === r.numRecords;
|
||||
if (!ok) { res.status = 'fail'; sizeMismatch = true; }
|
||||
res.checks.push({ label: 'numRecords', expected: r.numRecords, actual: sel.num_of_rec, ok: ok });
|
||||
}
|
||||
}
|
||||
if (r.recordLen !== null && r.recordLen !== undefined) {
|
||||
const ok = sel.record_len === r.recordLen;
|
||||
if (!ok) { res.status = 'fail'; sizeMismatch = true; }
|
||||
res.checks.push({ label: 'recordLen', expected: r.recordLen, actual: sel.record_len, ok: ok });
|
||||
}
|
||||
if (r.numRecords !== null && r.numRecords !== undefined) {
|
||||
const ok = sel.num_of_rec === r.numRecords;
|
||||
if (!ok) { res.status = 'fail'; sizeMismatch = true; }
|
||||
res.checks.push({ label: 'numRecords', expected: r.numRecords, actual: sel.num_of_rec, ok: ok });
|
||||
if (fciMode === 'exact' && r.fciHex) {
|
||||
const expected = profilerNormHexStrict(r.fciHex);
|
||||
const actual = profilerNormHexStrict(sel.fci_hex);
|
||||
const ok = !!expected && expected === actual;
|
||||
if (!ok) res.status = 'fail';
|
||||
res.checks.push({ label: 'fci', expected: r.fciHex, actual: sel.fci_hex || '', ok: ok });
|
||||
}
|
||||
if (r.content) {
|
||||
let rd;
|
||||
@@ -7247,14 +7642,19 @@ async function profilerRunRule(r) {
|
||||
const n = Math.min(exp.length, act.length);
|
||||
if (exp.length !== act.length) {
|
||||
res.status = 'fail';
|
||||
res.checks.push({ label: 'content.records', expected: exp.length + ' records', actual: act.length + ' records', ok: false });
|
||||
const numRec = res.checks.find(c => c.label === 'numRecords');
|
||||
if (!numRec || numRec.ok) {
|
||||
res.checks.push({ label: 'content.records', expected: exp.length + ' records', actual: act.length + ' records', ok: false });
|
||||
}
|
||||
}
|
||||
const matched = [];
|
||||
for (let j = 0; j < n; j++) {
|
||||
const dataOk = sizeMismatch ? profilerMatchMin(r.content.mode, exp[j].data, act[j].data) : profilerMatch(r.content.mode, exp[j].data, act[j].data);
|
||||
const ok = exp[j].num === act[j].num && dataOk;
|
||||
if (!ok) res.status = 'fail';
|
||||
if (!ok) res.status = 'fail'; else matched.push(exp[j].num);
|
||||
res.checks.push({ label: 'content.rec' + exp[j].num, expected: exp[j].data, actual: act[j].data, ok: ok });
|
||||
}
|
||||
if (matched.length) res.recordsMatched = matched;
|
||||
} else {
|
||||
const ok = sizeMismatch ? profilerMatchMin(r.content.mode, r.content.expected, rd.data) : profilerMatch(r.content.mode, r.content.expected, rd.data);
|
||||
if (!ok) res.status = 'fail';
|
||||
@@ -7278,6 +7678,73 @@ function profilerCustomNameForPath(path) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Group a rule's result checks into display aspects, derived from the checks
|
||||
// that actually ran. 'Exact FCI' subsumes filetype/size/records; 'exists' is
|
||||
// implicit and omitted.
|
||||
function profilerResultAspects(res) {
|
||||
const groups = { filetype: [], size: [], records: [], 'Exact FCI': [], contents: [] };
|
||||
for (const c of (res.checks || [])) {
|
||||
const l = c.label;
|
||||
if (l === 'fileType') groups.filetype.push(c);
|
||||
else if (l === 'fileSize') groups.size.push(c);
|
||||
else if (l === 'recordLen' || l === 'numRecords') groups.records.push(c);
|
||||
else if (l === 'fci') groups['Exact FCI'].push(c);
|
||||
else if (l === 'content' || l.startsWith('content.')) groups.contents.push(c);
|
||||
}
|
||||
const out = [];
|
||||
if (groups['Exact FCI'].length) {
|
||||
out.push({ key: 'Exact FCI', ok: groups['Exact FCI'].every(c => c.ok) });
|
||||
} else {
|
||||
if (groups.filetype.length) out.push({ key: 'filetype', ok: groups.filetype.every(c => c.ok) });
|
||||
if (groups.size.length) out.push({ key: 'size', ok: groups.size.every(c => c.ok) });
|
||||
if (groups.records.length) out.push({ key: 'records', ok: groups.records.every(c => c.ok) });
|
||||
}
|
||||
if (groups.contents.length) out.push({ key: 'contents', ok: groups.contents.every(c => c.ok) });
|
||||
return out;
|
||||
}
|
||||
|
||||
function profilerAspectSummary(aspects, marked, tr) {
|
||||
tr = tr || (s => s);
|
||||
if (!aspects || !aspects.length) return '';
|
||||
if (marked) return aspects.map(a => tr(a.key) + ' ' + (a.ok ? '✓' : '✗')).join(', ');
|
||||
const has = k => aspects.some(a => a.key === k);
|
||||
let fcp = '';
|
||||
if (has('Exact FCI')) fcp = tr('Exact FCI');
|
||||
else if (has('filetype') && has('records')) fcp = tr('filetype and records');
|
||||
else if (has('filetype') && has('size')) fcp = tr('filetype and size');
|
||||
else if (has('filetype')) fcp = tr('filetype');
|
||||
else if (has('records')) fcp = tr('records');
|
||||
else if (has('size')) fcp = tr('size');
|
||||
const parts = [];
|
||||
if (fcp) parts.push(fcp);
|
||||
if (has('contents')) parts.push(tr('contents'));
|
||||
return parts.join(', ');
|
||||
}
|
||||
|
||||
// Compress a list of record numbers into ranges: [1,2,3,5,7] -> "1-3, 5, 7".
|
||||
function profilerNumRanges(nums) {
|
||||
if (!nums || !nums.length) return '';
|
||||
const sorted = nums.slice().sort((a, b) => a - b);
|
||||
const parts = [];
|
||||
let start = null, prev = null;
|
||||
for (const n of sorted) {
|
||||
if (start === null) { start = prev = n; continue; }
|
||||
if (n === prev + 1) { prev = n; continue; }
|
||||
parts.push(start === prev ? String(start) : start + '-' + prev);
|
||||
start = prev = n;
|
||||
}
|
||||
if (start !== null) parts.push(start === prev ? String(start) : start + '-' + prev);
|
||||
return parts.join(', ');
|
||||
}
|
||||
|
||||
// Checks whose expected/actual are raw data (hex), rendered as aligned fields.
|
||||
function profilerRawDataCheck(c) {
|
||||
if (c.label === 'fci') return true;
|
||||
if (/^content\.rec\d+$/.test(c.label)) return true;
|
||||
if (c.label === 'content') return c.expected !== 'readable';
|
||||
return false;
|
||||
}
|
||||
|
||||
function profilerRenderReport(results) {
|
||||
let html = '';
|
||||
for (const r of results) {
|
||||
@@ -7287,12 +7754,32 @@ function profilerRenderReport(results) {
|
||||
const title = name
|
||||
? '<span>' + esc(name) + '</span> <span class="text-gray-400 dark:text-slate-500">(' + esc(r.path) + ')</span>'
|
||||
: '<span>' + esc(r.path) + '</span>';
|
||||
const aspects = profilerResultAspects(r);
|
||||
const marked = aspects.some(a => !a.ok);
|
||||
const summary = profilerAspectSummary(aspects, marked, t);
|
||||
html += '<div class="mb-2 p-2 border border-gray-200 dark:border-slate-600 rounded">';
|
||||
html += '<div class="flex items-center gap-2"><span class="' + color + ' font-bold">' + icon + '</span><span class="font-mono text-sm">' + title + '</span></div>';
|
||||
html += '<div class="flex items-center gap-2"><span class="' + color + ' font-bold">' + icon + '</span><span class="font-mono text-sm">' + title + '</span>' +
|
||||
(summary ? '<span class="text-xs text-gray-500 dark:text-slate-400">: ' + esc(summary) + '</span>' : '') + '</div>';
|
||||
if (r.error) html += '<div class="text-xs text-yellow-600 mt-1">' + esc(r.error) + '</div>';
|
||||
for (const c of r.checks) {
|
||||
if (c.ok) continue;
|
||||
html += '<div class="text-xs text-red-600 mt-1">' + esc(c.label) + ': ' + esc(t('expected')) + ' <b>' + esc(String(c.expected)) + '</b>, ' + esc(t('actual')) + ' <b>' + esc(String(c.actual)) + '</b></div>';
|
||||
if (profilerRawDataCheck(c)) {
|
||||
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="text-xs text-gray-500 dark:text-slate-400 w-24 text-right shrink-0">' + esc(t('expected')) + '</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="text-xs text-gray-500 dark:text-slate-400 w-24 text-right shrink-0">' + esc(t('actual')) + '</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>';
|
||||
html += '</div>';
|
||||
} else {
|
||||
html += '<div class="text-xs text-red-600 mt-1">' + esc(c.label) + ': ' + esc(t('expected')) + ' <b>' + esc(String(c.expected)) + '</b>, ' + esc(t('actual')) + ' <b>' + esc(String(c.actual)) + '</b></div>';
|
||||
}
|
||||
}
|
||||
const recFailed = (r.checks || []).some(c => /^content\.rec\d+$/.test(c.label) && !c.ok);
|
||||
if (recFailed && r.recordsMatched && r.recordsMatched.length) {
|
||||
html += '<div class="text-xs text-emerald-600 mt-1">' + esc(t('matching records')) + ': ' + esc(profilerNumRanges(r.recordsMatched)) + '</div>';
|
||||
}
|
||||
html += '</div>';
|
||||
}
|
||||
@@ -7504,8 +7991,16 @@ const LANG_RU = {
|
||||
'Back to list': 'Назад к списку',
|
||||
'Add rule': 'Добавить правило',
|
||||
'Scan': 'Сканировать',
|
||||
'Discovering...': 'Поиск файлов...',
|
||||
'files': 'файлов',
|
||||
'Ignore contents of:': 'Игнорировать содержимое:',
|
||||
'Ignore contents of': 'Игнорировать содержимое',
|
||||
'Match first 4 bytes for': 'Сравнивать первые 4 байта для',
|
||||
'FCP/FCI check': 'Проверка FCP/FCI',
|
||||
'Filetype only (FCP)': 'Только тип файла (FCP)',
|
||||
'Filetype + size (FCP)': 'Тип файла + размер (FCP)',
|
||||
'Exact FCI': 'Точный FCI',
|
||||
'FCI hex (raw SELECT response)': 'FCI hex (сырой ответ SELECT)',
|
||||
'No profiles defined.': 'Профили не заданы.',
|
||||
'rules': 'правил',
|
||||
'Check': 'Проверить',
|
||||
@@ -7520,7 +8015,14 @@ const LANG_RU = {
|
||||
'Size': 'Размер',
|
||||
'Record length': 'Длина записи',
|
||||
'Record count': 'Кол-во записей',
|
||||
'Contents': 'Содержимое',
|
||||
'Check contents': 'Проверить содержимое',
|
||||
'filetype': 'тип файла',
|
||||
'size': 'размер',
|
||||
'records': 'записи',
|
||||
'contents': 'содержимое',
|
||||
'filetype and size': 'тип файла и размер',
|
||||
'filetype and records': 'тип файла и записи',
|
||||
'matching records': 'совпадающие записи',
|
||||
'None': 'Нет',
|
||||
'Exact': 'Точное',
|
||||
'Mask': 'Маска',
|
||||
@@ -7530,6 +8032,28 @@ const LANG_RU = {
|
||||
'errors': 'ошибок',
|
||||
'expected': 'ожидалось',
|
||||
'actual': 'фактически',
|
||||
'HTTP OTA': 'HTTP OTA',
|
||||
'Trigger (Push SMS)': 'Триггер (Push SMS)',
|
||||
'Store (SD admin params)': 'Store (параметры SD)',
|
||||
'SD params tag': 'Tag параметров SD',
|
||||
"Connection parameters ('84')": "Параметры соединения ('84')",
|
||||
'+ TLV': '+ TLV',
|
||||
'+ Device Identities': '+ Device Identities',
|
||||
'+ Alpha (empty)': '+ Alpha (пустой)',
|
||||
'+ Bearer (GPRS)': '+ Bearer (GPRS)',
|
||||
"Security parameters ('85')": "Параметры безопасности ('85')",
|
||||
'PSK Identity (text)': 'PSK Identity (текст)',
|
||||
"Retry policy ('86')": "Политика повтора ('86')",
|
||||
'Retry counter (B0,00=unlimited)': 'Счётчик повторов (B0,00=бесконечно)',
|
||||
'Hours': 'Часы',
|
||||
'Minutes': 'Минуты',
|
||||
'Seconds': 'Секунды',
|
||||
"Report failure (vendor-specific TLV)": "Отчёт о сбое (специфичный TLV)",
|
||||
"HTTP POST ('89', 8A=host,8B=agent,8C=URI)": "HTTP POST ('89', 8A=host,8B=agent,8C=URI)",
|
||||
'Host (text→hex)': 'Хост (текст→hex)',
|
||||
"HTTP agent ID (text, X-Admin-From)": "ID HTTP агента (текст, X-Admin-From)",
|
||||
'URI (text→hex)': 'URI (текст→hex)',
|
||||
"Wrapped in Command Scripting template ('AA')": "Обёртка в Command Scripting template ('AA')",
|
||||
};
|
||||
let currentLang = 'en';
|
||||
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
const CACHE = 'otaman-v47';
|
||||
const CACHE = 'otaman-v60';
|
||||
const URLS = [
|
||||
'index.html',
|
||||
'help.html',
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
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 = ['_hotaHex', '_hotaAsciiHex', '_hotaBerLen', '_hotaTlv',
|
||||
'hotaTlv', 'hotaBuildConn', 'hotaBuildSec', 'hotaBuildRetry',
|
||||
'hotaBuildHttpPost', 'hotaBuildTrigger', 'hotaBuildStore', 'hotaBuild'];
|
||||
let code = '';
|
||||
for (const f of FNS) code += extractFunc(html, f) + '\n';
|
||||
|
||||
eval(code);
|
||||
|
||||
test('_hotaHex strips non-hex and uppercases', () => {
|
||||
assert.strictEqual(_hotaHex(' 81 82 '), '8182');
|
||||
assert.strictEqual(_hotaHex('b0-00'), 'B000');
|
||||
assert.strictEqual(_hotaHex(''), '');
|
||||
assert.strictEqual(_hotaHex(undefined), '');
|
||||
});
|
||||
|
||||
test('_hotaAsciiHex encodes text as ASCII bytes', () => {
|
||||
assert.strictEqual(_hotaAsciiHex('megafon.ru'), '6D656761666F6E2E7275');
|
||||
assert.strictEqual(_hotaAsciiHex('v1.0'), '76312E30');
|
||||
assert.strictEqual(_hotaAsciiHex('/sd'), '2F7364');
|
||||
assert.strictEqual(_hotaAsciiHex(''), '');
|
||||
});
|
||||
|
||||
test('_hotaBerLen encodes definite lengths', () => {
|
||||
assert.strictEqual(_hotaBerLen(0), '00');
|
||||
assert.strictEqual(_hotaBerLen(11), '0B');
|
||||
assert.strictEqual(_hotaBerLen(127), '7F');
|
||||
assert.strictEqual(_hotaBerLen(128), '8180');
|
||||
assert.strictEqual(_hotaBerLen(255), '81FF');
|
||||
assert.strictEqual(_hotaBerLen(256), '820100');
|
||||
});
|
||||
|
||||
test('hotaTlv wraps tag + definite length + value', () => {
|
||||
assert.strictEqual(hotaTlv('02', '8182'), '02028182');
|
||||
assert.strictEqual(hotaTlv('05', ''), '0500');
|
||||
assert.strictEqual(hotaTlv('8A', '6D656761666F6E2E7275'), '8A0A6D656761666F6E2E7275');
|
||||
});
|
||||
|
||||
test('hotaBuildConn produces a full 84 TLV', () => {
|
||||
assert.strictEqual(hotaBuildConn([{ tag: '02', value: '8182' }]), '840402028182');
|
||||
assert.strictEqual(
|
||||
hotaBuildConn([
|
||||
{ tag: '01', value: '014001' },
|
||||
{ tag: '02', value: '81 82' },
|
||||
{ tag: '05', value: '' },
|
||||
]),
|
||||
'840B0103014001020281820500');
|
||||
assert.strictEqual(hotaBuildConn([{ tag: '02', value: '8182' }, { tag: '', value: 'FF' }]), '840402028182');
|
||||
assert.strictEqual(hotaBuildConn([]), '8400');
|
||||
});
|
||||
|
||||
test('hotaBuildSec produces a full 85 TLV per Table 4-6', () => {
|
||||
assert.strictEqual(
|
||||
hotaBuildSec({ pskIdentity: 'Test123', kvn: '01', kid: '01' }),
|
||||
'850B0754657374313233020101');
|
||||
assert.strictEqual(
|
||||
hotaBuildSec({ pskIdentity: '', kvn: '01', kid: '01' }),
|
||||
'850400020101');
|
||||
});
|
||||
|
||||
test('hotaBuildRetry uses TS 102 223 timer TLV (25 03) and wraps in 86', () => {
|
||||
assert.strictEqual(
|
||||
hotaBuildRetry({ counter: 'B000', delayH: 1, delayM: 2, delayS: 3, reportFailure: '' }),
|
||||
'8607B0002503010203');
|
||||
assert.strictEqual(
|
||||
hotaBuildRetry({ counter: 'b0 00', delayH: '1', delayM: '2', delayS: '3', reportFailure: '0A080102030405060708' }),
|
||||
'8611B00025030102030A080102030405060708');
|
||||
assert.strictEqual(
|
||||
hotaBuildRetry({ counter: '0000', delayH: 0, delayM: 0, delayS: 0, reportFailure: '0A0' }),
|
||||
'860700002503000000');
|
||||
});
|
||||
|
||||
test('hotaBuildHttpPost wraps 8A/8B/8C in a full 89 TLV', () => {
|
||||
assert.strictEqual(
|
||||
hotaBuildHttpPost({ host: '', agent: '', uri: '' }),
|
||||
'89068A008B008C00');
|
||||
assert.strictEqual(
|
||||
hotaBuildHttpPost({ host: 'megafon.ru', agent: 'v1.0', uri: '/sd' }),
|
||||
'89178A0A6D656761666F6E2E72758B0476312E308C032F7364');
|
||||
});
|
||||
|
||||
test('hotaBuildTrigger wraps 81 > 83 > (84/85/86/89)', () => {
|
||||
const conn = '840402028182';
|
||||
const sec = '850400020101';
|
||||
const retry = '8607B0002503010203';
|
||||
const httpPost = '89068A008B008C00';
|
||||
assert.strictEqual(
|
||||
hotaBuildTrigger(conn, sec, retry, httpPost, false),
|
||||
'811F831D8404020281828504000201018607B000250301020389068A008B008C00');
|
||||
});
|
||||
|
||||
test('hotaBuildTrigger expanded wraps the 81 command in Command Scripting template AA', () => {
|
||||
const conn = '840402028182';
|
||||
const plain = hotaBuildTrigger(conn, '', '', '', false);
|
||||
const expanded = hotaBuildTrigger(conn, '', '', '', true);
|
||||
const byteLen = plain.length / 2;
|
||||
assert.strictEqual(expanded, 'AA' + _hotaBerLen(byteLen) + plain);
|
||||
assert.ok(expanded.startsWith('AA'));
|
||||
});
|
||||
|
||||
test('hotaBuildStore emits STORE DATA TLV-mode APDU (80 E2 90 00)', () => {
|
||||
assert.strictEqual(
|
||||
hotaBuildStore('840402028182', '', '', '', '85'),
|
||||
'80E29000088506840402028182');
|
||||
assert.strictEqual(
|
||||
hotaBuildStore('840402028182', '', '', '', 'A5'),
|
||||
'80E2900008A506840402028182');
|
||||
});
|
||||
|
||||
test('hotaBuild dispatches on mode', () => {
|
||||
assert.strictEqual(
|
||||
hotaBuild('trigger', '840402028182', '', '', '', '85', false),
|
||||
hotaBuildTrigger('840402028182', '', '', '', false));
|
||||
assert.strictEqual(
|
||||
hotaBuild('store', '840402028182', '', '', '', '85', false),
|
||||
hotaBuildStore('840402028182', '', '', '', '85'));
|
||||
assert.strictEqual(
|
||||
hotaBuild('store', '840402028182', '', '8607B0002503010203', '', '85', false),
|
||||
hotaBuildStore('840402028182', '', '8607B0002503010203', '', '85'));
|
||||
});
|
||||
@@ -5,7 +5,7 @@ const path = require('node:path');
|
||||
|
||||
const html = fs.readFileSync(path.join(__dirname, '..', 'index.html'), 'utf8');
|
||||
|
||||
function extractFunc(src, name) {
|
||||
function extractFunc(src, name, asyncFn) {
|
||||
const re = new RegExp('function\\s+' + name + '\\s*\\([^)]*\\)\\s*\\{');
|
||||
const m = re.exec(src);
|
||||
if (!m) throw new Error('function ' + name + ' not found');
|
||||
@@ -18,12 +18,16 @@ function extractFunc(src, name) {
|
||||
if (depth === 0) break;
|
||||
}
|
||||
}
|
||||
return src.slice(m.index, i + 1);
|
||||
return (asyncFn ? 'async ' : '') + src.slice(m.index, i + 1);
|
||||
}
|
||||
|
||||
const FNS = ['profilerNormHex', 'profilerMatch', 'profilerMatchMin', 'profilerMaskPrefix4', 'profilerFileFields', 'profilerContentKindForFileType', 'profilerEmptyRecordContent', 'profilerValidateProfile'];
|
||||
const FNS = ['profilerNormHex', 'profilerNormHexStrict', 'profilerMatch', 'profilerMatchMin', 'profilerMaskPrefix4', 'profilerFileFields', 'profilerContentKindForFileType', 'profilerEmptyRecordContent', 'profilerValidateProfile', 'profilerCustomNameForPath', 'profilerUpdateRulePath', 'profilerResultAspects', 'profilerAspectSummary', 'profilerNumRanges', 'esc', 'escHtml', 'profilerRawDataCheck', 'profilerRenderReport'];
|
||||
let code = '';
|
||||
for (const f of FNS) code += extractFunc(html, f) + '\n';
|
||||
code += extractFunc(html, 'profilerBuildFileRule', true) + '\n';
|
||||
code += extractFunc(html, 'profilerRunRule', true) + '\n';
|
||||
code += extractFunc(html, 'profilerScanCard', true) + '\n';
|
||||
code += html.match(/const PROFILER_MASK_PREFIX4_FIDS = \{[\s\S]*?\n\};/)[0] + '\n';
|
||||
eval(code);
|
||||
|
||||
test('profilerNormHex uppercases and strips non-hex', () => {
|
||||
@@ -107,3 +111,460 @@ test('profile validation', () => {
|
||||
assert.strictEqual(profilerValidateProfile({ name: 'x', rules: [{ type: 'file' }] }), 'Rule missing path');
|
||||
assert.strictEqual(profilerValidateProfile({ name: 'x', rules: [{ type: 'file', path: 'MF/7F10/6F3A' }] }), null);
|
||||
});
|
||||
|
||||
// --- "Profile from card" ignore list ---
|
||||
|
||||
function parseIgnoreFiles() {
|
||||
const raw = html.match(/const PROFILER_IGNORE_FILES = \[([\s\S]*?)\n\];/)[1];
|
||||
return [...raw.matchAll(/\{ fid: '([^']*)', name: '([^']*)' \}/g)].map(m => ({ fid: m[1], name: m[2] }));
|
||||
}
|
||||
|
||||
test('ignore list FIDs are well-formed and KcGPRS uses the TS 51.011 FID (6F52)', () => {
|
||||
const files = parseIgnoreFiles();
|
||||
assert.ok(files.length >= 12);
|
||||
for (const f of files) {
|
||||
assert.match(f.fid, /^[0-9A-F]{4}$/, f.name + ' has a malformed FID');
|
||||
assert.ok(f.name.startsWith('EF.'), f.fid + ' has a malformed name');
|
||||
}
|
||||
assert.strictEqual(files.find(f => f.name === 'EF.KcGPRS').fid, '6F52');
|
||||
});
|
||||
|
||||
test('ignore list has no duplicate FIDs or names', () => {
|
||||
const files = parseIgnoreFiles();
|
||||
assert.strictEqual(new Set(files.map(f => f.fid)).size, files.length);
|
||||
assert.strictEqual(new Set(files.map(f => f.name)).size, files.length);
|
||||
});
|
||||
|
||||
function mockFetch(handlers) {
|
||||
const calls = [];
|
||||
global.pysimFetch = async (path, body) => {
|
||||
calls.push(path);
|
||||
if (handlers[path]) return handlers[path](body);
|
||||
throw new Error('unexpected fetch: ' + path);
|
||||
};
|
||||
return calls;
|
||||
}
|
||||
|
||||
const KCGPRS_SELECT = {
|
||||
name: 'EF.KcGPRS', fid: '6F52', file_type: 'transparent',
|
||||
file_size: 9, record_len: null, num_of_rec: null, exists: true,
|
||||
};
|
||||
|
||||
test('profilerBuildFileRule skips contents for an ignored FID', async () => {
|
||||
const calls = mockFetch({ '/api/select': () => KCGPRS_SELECT });
|
||||
const rule = await profilerBuildFileRule('MF/7F20/6F52',
|
||||
{ fid: '6f52', name: 'EF.KcGPRS' }, new Set(['6F52']), new Set(['EF.KCGPRS']));
|
||||
assert.strictEqual(rule.content, null);
|
||||
assert.strictEqual(rule.path, 'MF/7F20/6F52');
|
||||
assert.strictEqual(rule.fileType, 'transparent');
|
||||
assert.strictEqual(rule.fileSize, 9);
|
||||
assert.strictEqual(rule.name, 'EF.KcGPRS');
|
||||
assert.ok(!calls.includes('/api/read'), 'contents must not be read for ignored files');
|
||||
});
|
||||
|
||||
test('profilerBuildFileRule skips contents when the name matches but the FID differs', async () => {
|
||||
// Regression: a KcGPRS copy at the TS 31.102 DF.GSM-ACCESS FID (4F52) must
|
||||
// still be ignored when the user checked EF.KcGPRS in the ignore list.
|
||||
const calls = mockFetch({
|
||||
'/api/select': () => ({ name: 'EF.KcGPRS', fid: '4F52', file_type: 'transparent', file_size: 9, record_len: null, num_of_rec: null, exists: true }),
|
||||
});
|
||||
const rule = await profilerBuildFileRule('MF/5F3B/4F52',
|
||||
{ fid: '4f52', name: 'EF.KcGPRS' }, new Set(['6F52']), new Set(['EF.KCGPRS']));
|
||||
assert.strictEqual(rule.content, null);
|
||||
assert.ok(!calls.includes('/api/read'), 'contents must not be read for name-matched files');
|
||||
});
|
||||
|
||||
test('profilerBuildFileRule captures contents for non-ignored files', async () => {
|
||||
const calls = mockFetch({
|
||||
'/api/select': () => ({ name: 'EF.ADN', fid: '6F3A', file_type: 'transparent', file_size: 4, record_len: null, num_of_rec: null, exists: true }),
|
||||
'/api/read': () => ({ success: true, data: 'AABBCCDD' }),
|
||||
});
|
||||
const rule = await profilerBuildFileRule('MF/7F20/6F3A',
|
||||
{ fid: '6f3a', name: 'EF.ADN' }, new Set(['6F52']), new Set(['EF.KCGPRS']));
|
||||
assert.ok(rule.content);
|
||||
assert.strictEqual(rule.content.mode, 'exact');
|
||||
assert.strictEqual(rule.content.expected, 'AABBCCDD');
|
||||
assert.strictEqual(rule.name, 'EF.ADN');
|
||||
assert.ok(calls.includes('/api/read'));
|
||||
});
|
||||
|
||||
test('profilerBuildFileRule falls back to the select name when the child has none', async () => {
|
||||
mockFetch({ '/api/select': () => KCGPRS_SELECT });
|
||||
const rule = await profilerBuildFileRule('MF/7F20/6F52', { fid: '6F52' }, new Set(), new Set());
|
||||
assert.strictEqual(rule.name, 'EF.KcGPRS');
|
||||
});
|
||||
|
||||
test('profilerBuildFileRule stores null when no symbolic name is known', async () => {
|
||||
mockFetch({ '/api/select': () => ({ fid: '6F3A', file_type: 'transparent', file_size: 0, exists: true }) });
|
||||
const rule = await profilerBuildFileRule('MF/7F20/6F3A', { fid: '6F3A' }, new Set(), new Set());
|
||||
assert.strictEqual(rule.name, null);
|
||||
});
|
||||
|
||||
test('profilerCustomNameForPath resolves saved custom file names', () => {
|
||||
global.pysimCustomFiles = [
|
||||
{ path: '3F00/7F10/6F3A', fid: '6F3A', name: 'My ADN' },
|
||||
{ path: 'MF/7F20/6F7E', fid: '6F7E', name: 'My LOCI' },
|
||||
];
|
||||
assert.strictEqual(profilerCustomNameForPath('MF/7F10/6F3A'), 'My ADN');
|
||||
assert.strictEqual(profilerCustomNameForPath('MF/7F20/6F7E'), 'My LOCI');
|
||||
assert.strictEqual(profilerCustomNameForPath('MF/7F20/6F3A'), null);
|
||||
assert.strictEqual(profilerCustomNameForPath(''), null);
|
||||
assert.strictEqual(profilerCustomNameForPath(null), null);
|
||||
});
|
||||
|
||||
test('profilerUpdateRulePath clears the scan-time name and refreshes the label', () => {
|
||||
global.pysimCustomFiles = [{ path: 'MF/7F10/6F3A', fid: '6F3A', name: 'My ADN' }];
|
||||
global.profilerDraft = { rules: [{ path: 'MF/7F20/6F3A', name: 'EF.ADN' }] };
|
||||
const span = { textContent: '' };
|
||||
const input = { parentElement: { querySelector: () => span } };
|
||||
profilerUpdateRulePath(0, 'MF/7F10/6F3A', input);
|
||||
assert.strictEqual(global.profilerDraft.rules[0].path, 'MF/7F10/6F3A');
|
||||
assert.strictEqual(global.profilerDraft.rules[0].name, null);
|
||||
assert.strictEqual(span.textContent, 'My ADN');
|
||||
profilerUpdateRulePath(0, 'MF/7F10/6FB1', input);
|
||||
assert.strictEqual(span.textContent, '');
|
||||
global.profilerDraft = null;
|
||||
profilerUpdateRulePath(0, 'x', input);
|
||||
});
|
||||
|
||||
test('profilerBuildFileRule still ignores when ignoreNames is omitted (back-compat)', async () => {
|
||||
const calls = mockFetch({ '/api/select': () => KCGPRS_SELECT });
|
||||
const rule = await profilerBuildFileRule('MF/7F20/6F52',
|
||||
{ fid: '6F52', name: 'EF.KcGPRS' }, new Set(['6F52']));
|
||||
assert.strictEqual(rule.content, null);
|
||||
assert.ok(!calls.includes('/api/read'));
|
||||
});
|
||||
|
||||
// --- FCP/FCI verification modes ---
|
||||
|
||||
test('profilerNormHexStrict strips non-hex and drops wildcards', () => {
|
||||
assert.strictEqual(profilerNormHexStrict('62 10 82 02 40 21'), '621082024021');
|
||||
assert.strictEqual(profilerNormHexStrict('aabb?cc'), 'AABBCC');
|
||||
assert.strictEqual(profilerNormHexStrict(''), '');
|
||||
});
|
||||
|
||||
test('profilerBuildFileRule stores fciMode and fciHex from the select response', async () => {
|
||||
mockFetch({ '/api/select': () => ({ ...KCGPRS_SELECT, fci_hex: '62 10 82 02 40 21' }) });
|
||||
const rule = await profilerBuildFileRule('MF/7F20/6F52',
|
||||
{ fid: '6F52', name: 'EF.KcGPRS' }, new Set(['6F52']), new Set(['EF.KCGPRS']), 'exact');
|
||||
assert.strictEqual(rule.fciMode, 'exact');
|
||||
assert.strictEqual(rule.fciHex, '62 10 82 02 40 21');
|
||||
});
|
||||
|
||||
test('profilerBuildFileRule defaults fciMode to type_size and fciHex to null when unavailable', async () => {
|
||||
mockFetch({ '/api/select': () => KCGPRS_SELECT });
|
||||
const rule = await profilerBuildFileRule('MF/7F20/6F52',
|
||||
{ fid: '6F52', name: 'EF.KcGPRS' }, new Set(), new Set());
|
||||
assert.strictEqual(rule.fciMode, 'type_size');
|
||||
assert.strictEqual(rule.fciHex, null);
|
||||
});
|
||||
|
||||
function runSelect(extra) {
|
||||
return { name: 'EF.ADN', fid: '6F3A', file_type: 'transparent', file_size: 4, record_len: null, num_of_rec: null, exists: true, ...(extra || {}) };
|
||||
}
|
||||
|
||||
test('profilerRunRule in type mode skips size checks', async () => {
|
||||
mockFetch({ '/api/select': () => runSelect({ file_size: 99 }) });
|
||||
const res = await profilerRunRule({ path: 'MF/7F20/6F3A', fileType: 'transparent', fileSize: 4, fciMode: 'type' });
|
||||
assert.strictEqual(res.status, 'pass');
|
||||
assert.ok(!res.checks.some(c => c.label === 'fileSize'));
|
||||
});
|
||||
|
||||
test('profilerRunRule in type_size mode checks size', async () => {
|
||||
mockFetch({ '/api/select': () => runSelect({ file_size: 99 }) });
|
||||
const res = await profilerRunRule({ path: 'MF/7F20/6F3A', fileType: 'transparent', fileSize: 4, fciMode: 'type_size' });
|
||||
assert.strictEqual(res.status, 'fail');
|
||||
assert.ok(res.checks.some(c => c.label === 'fileSize' && c.ok === false));
|
||||
});
|
||||
|
||||
test('profilerRunRule legacy rule (no fciMode) still checks size', async () => {
|
||||
mockFetch({ '/api/select': () => runSelect({ file_size: 4 }) });
|
||||
const res = await profilerRunRule({ path: 'MF/7F20/6F3A', fileType: 'transparent', fileSize: 4 });
|
||||
assert.strictEqual(res.status, 'pass');
|
||||
assert.ok(res.checks.some(c => c.label === 'fileSize' && c.ok === true));
|
||||
});
|
||||
|
||||
test('profilerRunRule exact FCI passes on byte-identical FCI', async () => {
|
||||
mockFetch({ '/api/select': () => runSelect({ fci_hex: '621082024021' }) });
|
||||
const res = await profilerRunRule({ path: 'MF/7F20/6F3A', fileType: 'transparent', fileSize: 4, fciMode: 'exact', fciHex: '62 10 82 02 40 21' });
|
||||
assert.strictEqual(res.status, 'pass');
|
||||
assert.ok(res.checks.some(c => c.label === 'fci' && c.ok === true));
|
||||
});
|
||||
|
||||
test('profilerRunRule exact FCI fails on byte mismatch', async () => {
|
||||
mockFetch({ '/api/select': () => runSelect({ fci_hex: '621082024022' }) });
|
||||
const res = await profilerRunRule({ path: 'MF/7F20/6F3A', fileType: 'transparent', fileSize: 4, fciMode: 'exact', fciHex: '621082024021' });
|
||||
assert.strictEqual(res.status, 'fail');
|
||||
assert.ok(res.checks.some(c => c.label === 'fci' && c.ok === false));
|
||||
});
|
||||
|
||||
test('profilerRunRule exact FCI fails when the live FCI is missing', async () => {
|
||||
mockFetch({ '/api/select': () => runSelect({ fci_hex: null }) });
|
||||
const res = await profilerRunRule({ path: 'MF/7F20/6F3A', fileType: 'transparent', fileSize: 4, fciMode: 'exact', fciHex: '621082024021' });
|
||||
assert.strictEqual(res.status, 'fail');
|
||||
assert.ok(res.checks.some(c => c.label === 'fci' && c.ok === false));
|
||||
});
|
||||
|
||||
test('profilerValidateProfile accepts valid fciMode and rejects unknown', () => {
|
||||
assert.strictEqual(profilerValidateProfile({ name: 'x', rules: [{ type: 'file', path: 'MF/6F07', fciMode: 'type' }] }), null);
|
||||
assert.strictEqual(profilerValidateProfile({ name: 'x', rules: [{ type: 'file', path: 'MF/6F07', fciMode: 'type_size' }] }), null);
|
||||
assert.strictEqual(profilerValidateProfile({ name: 'x', rules: [{ type: 'file', path: 'MF/6F07', fciMode: 'exact' }] }), null);
|
||||
assert.strictEqual(profilerValidateProfile({ name: 'x', rules: [{ type: 'file', path: 'MF/6F07', fciMode: 'bogus' }] }), 'Invalid FCP/FCI mode: bogus');
|
||||
});
|
||||
|
||||
// --- scan progress ---
|
||||
|
||||
test('profilerScanCard reports discovery total then per-file progress', async () => {
|
||||
const SEL = (fid, name) => ({ name, fid, file_type: 'transparent', file_size: 4, record_len: null, num_of_rec: null, exists: true });
|
||||
const tree = {
|
||||
MF: { exists: true, name: 'MF', children: [
|
||||
{ name: 'DF.GSM', fid: '7f20', isDir: true },
|
||||
{ name: 'EF.IMSI', fid: '6f07', isDir: false },
|
||||
] },
|
||||
'DF.GSM': { exists: true, name: 'DF.GSM', children: [
|
||||
{ name: 'EF.ADN', fid: '6f3a', isDir: false },
|
||||
{ name: 'EF.LOCI', fid: '6f7e', isDir: false },
|
||||
] },
|
||||
};
|
||||
global.pysimCustomFiles = [{ path: 'MF/7F10/6F3A', fid: '6F3A', name: 'My ADN' }];
|
||||
global.pysimFetch = async (path, body) => {
|
||||
if (path === '/api/tree') {
|
||||
const key = body.name === 'DF.GSM' ? 'DF.GSM' : 'MF';
|
||||
return tree[key];
|
||||
}
|
||||
if (path === '/api/select') {
|
||||
const fid = body.path.split('/').pop();
|
||||
return SEL(fid, 'EF.' + fid);
|
||||
}
|
||||
throw new Error('unexpected fetch: ' + path);
|
||||
};
|
||||
|
||||
const progress = [];
|
||||
const rules = await profilerScanCard(new Set(), new Set(), 'type', (done, total, path) => {
|
||||
progress.push([done, total, path]);
|
||||
});
|
||||
|
||||
assert.strictEqual(rules.length, 4);
|
||||
// initial 0/total report
|
||||
assert.deepStrictEqual(progress[0], [0, 4, '']);
|
||||
// then 1..4 with the file path, in scan order
|
||||
assert.strictEqual(progress.length, 5);
|
||||
assert.strictEqual(progress[4][0], 4);
|
||||
assert.strictEqual(progress[4][1], 4);
|
||||
const paths = progress.slice(1).map(p => p[2]);
|
||||
assert.deepStrictEqual(paths, ['MF/7F20/6F3A', 'MF/7F20/6F7E', 'MF/6F07', 'MF/7F10/6F3A']);
|
||||
});
|
||||
|
||||
test('profilerScanCard without onProgress still works (back-compat)', async () => {
|
||||
global.pysimCustomFiles = [];
|
||||
global.pysimFetch = async (path, body) => {
|
||||
if (path === '/api/tree') return { exists: true, name: 'MF', children: [{ name: 'EF.IMSI', fid: '6f07', isDir: false }] };
|
||||
if (path === '/api/select') return { name: 'EF.IMSI', fid: '6F07', file_type: 'transparent', file_size: 9, record_len: null, num_of_rec: null, exists: true };
|
||||
throw new Error('unexpected fetch: ' + path);
|
||||
};
|
||||
const rules = await profilerScanCard(new Set(), new Set(), 'type_size');
|
||||
assert.strictEqual(rules.length, 1);
|
||||
});
|
||||
|
||||
// --- mask first 4 bytes (EF.IMSI / EF.ICCID) ---
|
||||
|
||||
const IMSI_SELECT = { name: 'EF.IMSI', fid: '6F07', file_type: 'transparent', file_size: 9, record_len: null, num_of_rec: null, exists: true };
|
||||
const IMSI_DATA = '082905911234567890';
|
||||
|
||||
test('profilerBuildFileRule masks first 4 bytes when the FID is in maskFids', async () => {
|
||||
mockFetch({ '/api/select': () => IMSI_SELECT, '/api/read': () => ({ success: true, data: IMSI_DATA }) });
|
||||
const rule = await profilerBuildFileRule('MF/6F07', { fid: '6F07', name: 'EF.IMSI' }, new Set(), new Set(), 'type_size', new Set(['6F07']));
|
||||
assert.strictEqual(rule.content.mode, 'mask');
|
||||
assert.strictEqual(rule.content.expected, '08290591??????????');
|
||||
});
|
||||
|
||||
test('profilerBuildFileRule uses exact contents when maskFids does not contain the FID', async () => {
|
||||
mockFetch({ '/api/select': () => IMSI_SELECT, '/api/read': () => ({ success: true, data: IMSI_DATA }) });
|
||||
const rule = await profilerBuildFileRule('MF/6F07', { fid: '6F07', name: 'EF.IMSI' }, new Set(), new Set(), 'type_size', new Set());
|
||||
assert.strictEqual(rule.content.mode, 'exact');
|
||||
assert.strictEqual(rule.content.expected, IMSI_DATA);
|
||||
});
|
||||
|
||||
test('profilerBuildFileRule keeps the legacy mask default when maskFids is omitted', async () => {
|
||||
mockFetch({ '/api/select': () => IMSI_SELECT, '/api/read': () => ({ success: true, data: IMSI_DATA }) });
|
||||
const rule = await profilerBuildFileRule('MF/6F07', { fid: '6F07', name: 'EF.IMSI' }, new Set(), new Set(), 'type_size');
|
||||
assert.strictEqual(rule.content.mode, 'mask');
|
||||
assert.strictEqual(rule.content.expected, '08290591??????????');
|
||||
});
|
||||
|
||||
test('profilerScanCard threads maskFids through to rule building', async () => {
|
||||
global.pysimCustomFiles = [];
|
||||
global.pysimFetch = async (path, body) => {
|
||||
if (path === '/api/tree') return { exists: true, name: 'MF', children: [{ name: 'EF.IMSI', fid: '6f07', isDir: false }] };
|
||||
if (path === '/api/select') return IMSI_SELECT;
|
||||
if (path === '/api/read') return { success: true, data: IMSI_DATA };
|
||||
throw new Error('unexpected fetch: ' + path);
|
||||
};
|
||||
const rules = await profilerScanCard(new Set(), new Set(), 'type_size', undefined, new Set());
|
||||
assert.strictEqual(rules.length, 1);
|
||||
assert.strictEqual(rules[0].content.mode, 'exact');
|
||||
assert.strictEqual(rules[0].content.expected, IMSI_DATA);
|
||||
});
|
||||
|
||||
// --- result report (aspects, summary, matching records) ---
|
||||
|
||||
test('profilerNumRanges compresses consecutive record numbers', () => {
|
||||
assert.strictEqual(profilerNumRanges([]), '');
|
||||
assert.strictEqual(profilerNumRanges([1]), '1');
|
||||
assert.strictEqual(profilerNumRanges([1, 2, 3, 4, 5, 7, 8, 9, 10]), '1-5, 7-10');
|
||||
assert.strictEqual(profilerNumRanges([1, 3, 5]), '1, 3, 5');
|
||||
assert.strictEqual(profilerNumRanges([7, 8, 1, 2]), '1-2, 7-8');
|
||||
assert.strictEqual(profilerNumRanges([6]), '6');
|
||||
});
|
||||
|
||||
test('profilerResultAspects groups checks and lets Exact FCI subsume type/size', () => {
|
||||
assert.deepStrictEqual(
|
||||
profilerResultAspects({ checks: [
|
||||
{ label: 'fileType', ok: true }, { label: 'fileSize', ok: true }, { label: 'content', ok: true },
|
||||
] }),
|
||||
[{ key: 'filetype', ok: true }, { key: 'size', ok: true }, { key: 'contents', ok: true }]);
|
||||
assert.deepStrictEqual(
|
||||
profilerResultAspects({ checks: [
|
||||
{ label: 'fileType', ok: true }, { label: 'fileSize', ok: true }, { label: 'fci', ok: false }, { label: 'content', ok: true },
|
||||
] }),
|
||||
[{ key: 'Exact FCI', ok: false }, { key: 'contents', ok: true }]);
|
||||
assert.deepStrictEqual(
|
||||
profilerResultAspects({ checks: [
|
||||
{ label: 'recordLen', ok: true }, { label: 'numRecords', ok: false },
|
||||
] }),
|
||||
[{ key: 'records', ok: false }]);
|
||||
assert.deepStrictEqual(profilerResultAspects({ checks: [{ label: 'exists', ok: true }] }), []);
|
||||
});
|
||||
|
||||
test('profilerAspectSummary renders plain list when nothing failed', () => {
|
||||
const tr = s => s;
|
||||
assert.strictEqual(
|
||||
profilerAspectSummary([{ key: 'filetype', ok: true }, { key: 'size', ok: true }, { key: 'contents', ok: true }], false, tr),
|
||||
'filetype and size, contents');
|
||||
assert.strictEqual(
|
||||
profilerAspectSummary([{ key: 'filetype', ok: true }, { key: 'records', ok: true }, { key: 'contents', ok: true }], false, tr),
|
||||
'filetype and records, contents');
|
||||
assert.strictEqual(
|
||||
profilerAspectSummary([{ key: 'Exact FCI', ok: true }, { key: 'contents', ok: true }], false, tr),
|
||||
'Exact FCI, contents');
|
||||
assert.strictEqual(profilerAspectSummary([{ key: 'filetype', ok: true }], false, tr), 'filetype');
|
||||
assert.strictEqual(profilerAspectSummary([], false, tr), '');
|
||||
});
|
||||
|
||||
test('profilerAspectSummary marks per-aspect when mixed', () => {
|
||||
const tr = s => s;
|
||||
assert.strictEqual(
|
||||
profilerAspectSummary([{ key: 'filetype', ok: true }, { key: 'size', ok: false }, { key: 'contents', ok: true }], true, tr),
|
||||
'filetype ✓, size ✗, contents ✓');
|
||||
assert.strictEqual(
|
||||
profilerAspectSummary([{ key: 'Exact FCI', ok: false }, { key: 'contents', ok: true }], true, tr),
|
||||
'Exact FCI ✗, contents ✓');
|
||||
});
|
||||
|
||||
test('profilerRunRule records which records matched on a record mismatch', async () => {
|
||||
mockFetch({
|
||||
'/api/select': () => ({ name: 'EF.X', fid: '6F3A', file_type: 'linear_fixed', file_size: null, record_len: 2, num_of_rec: 3, exists: true }),
|
||||
'/api/read': () => ({ success: true, records: [{ num: 1, data: 'AA' }, { num: 2, data: 'XX' }, { num: 3, data: 'CC' }] }),
|
||||
});
|
||||
const res = await profilerRunRule({
|
||||
path: 'MF/7F20/6F3A', fileType: 'linear_fixed', recordLen: 2, numRecords: 3, fciMode: 'type_size',
|
||||
content: { mode: 'exact', kind: 'record', records: [{ num: 1, data: 'AA' }, { num: 2, data: 'BB' }, { num: 3, data: 'CC' }] },
|
||||
});
|
||||
assert.strictEqual(res.status, 'fail');
|
||||
assert.deepStrictEqual(res.recordsMatched, [1, 3]);
|
||||
});
|
||||
|
||||
const REC_SELECT_10 = { name: 'EF.X', fid: '6F3A', file_type: 'linear_fixed', file_size: null, record_len: 2, num_of_rec: 10, exists: true };
|
||||
const REC_EXP_30 = (() => { const r = []; for (let i = 1; i <= 30; i++) r.push({ num: i, data: 'AB' }); return r; })();
|
||||
const REC_ACT_10 = (() => { const r = []; for (let i = 1; i <= 10; i++) r.push({ num: i, data: 'AB' }); return r; })();
|
||||
|
||||
test('record count mismatch is reported once when numRecords is checked', async () => {
|
||||
mockFetch({ '/api/select': () => REC_SELECT_10, '/api/read': () => ({ success: true, records: REC_ACT_10 }) });
|
||||
const res = await profilerRunRule({
|
||||
path: 'MF/7F20/6F3A', fileType: 'linear_fixed', recordLen: 2, numRecords: 30, fciMode: 'type_size',
|
||||
content: { mode: 'exact', kind: 'record', records: REC_EXP_30 },
|
||||
});
|
||||
assert.strictEqual(res.status, 'fail');
|
||||
assert.ok(res.checks.some(c => c.label === 'numRecords' && c.ok === false));
|
||||
assert.ok(!res.checks.some(c => c.label === 'content.records'));
|
||||
});
|
||||
|
||||
test('record count mismatch keeps content.records when numRecords is not checked (type mode)', async () => {
|
||||
mockFetch({ '/api/select': () => REC_SELECT_10, '/api/read': () => ({ success: true, records: REC_ACT_10 }) });
|
||||
const res = await profilerRunRule({
|
||||
path: 'MF/7F20/6F3A', fileType: 'linear_fixed', recordLen: 2, numRecords: 30, fciMode: 'type',
|
||||
content: { mode: 'exact', kind: 'record', records: REC_EXP_30 },
|
||||
});
|
||||
assert.strictEqual(res.status, 'fail');
|
||||
assert.ok(res.checks.some(c => c.label === 'content.records' && c.ok === false));
|
||||
assert.ok(!res.checks.some(c => c.label === 'numRecords'));
|
||||
});
|
||||
|
||||
test('record count mismatch keeps content.records when numRecords passes but read differs', async () => {
|
||||
mockFetch({
|
||||
'/api/select': () => ({ ...REC_SELECT_10, num_of_rec: 3 }),
|
||||
'/api/read': () => ({ success: true, records: REC_ACT_10.slice(0, 2) }),
|
||||
});
|
||||
const res = await profilerRunRule({
|
||||
path: 'MF/7F20/6F3A', fileType: 'linear_fixed', recordLen: 2, numRecords: 3, fciMode: 'type_size',
|
||||
content: { mode: 'exact', kind: 'record', records: [{ num: 1, data: 'AB' }, { num: 2, data: 'AB' }, { num: 3, data: 'AB' }] },
|
||||
});
|
||||
assert.strictEqual(res.status, 'fail');
|
||||
assert.ok(res.checks.some(c => c.label === 'numRecords' && c.ok === true));
|
||||
assert.ok(res.checks.some(c => c.label === 'content.records' && c.ok === false));
|
||||
});
|
||||
|
||||
test('profilerRenderReport includes the checked-aspects summary and matching-record note', () => {
|
||||
global.t = s => s;
|
||||
global.pysimCustomFiles = [];
|
||||
const html = profilerRenderReport([
|
||||
{ path: 'MF/7F20/6F3F', name: 'EF.GID2', status: 'pass', checks: [
|
||||
{ label: 'fileType', ok: true }, { label: 'fileSize', ok: true }, { label: 'content', ok: true },
|
||||
] },
|
||||
{ path: 'MF/7F20/6F3A', name: 'EF.X', status: 'fail', checks: [
|
||||
{ label: 'fileType', ok: true }, { label: 'fileSize', ok: false, expected: 4, actual: 99 }, { label: 'content', ok: true },
|
||||
] },
|
||||
{ path: 'MF/7F20/6F4E', name: 'EF.Y', status: 'fail', checks: [
|
||||
{ label: 'content.rec6', ok: false, expected: 'BB', actual: 'XX' },
|
||||
{ label: 'content.rec1', ok: true }, { label: 'content.rec2', ok: true }, { label: 'content.rec5', ok: true },
|
||||
], recordsMatched: [1, 2, 5] },
|
||||
]);
|
||||
assert.ok(html.includes('filetype and size, contents'));
|
||||
assert.ok(html.includes('filetype ✓, size ✗, contents ✓'));
|
||||
assert.ok(html.includes('matching records'));
|
||||
assert.ok(html.includes('1-2, 5'));
|
||||
delete global.t;
|
||||
});
|
||||
|
||||
test('profilerRawDataCheck identifies raw-data checks', () => {
|
||||
assert.strictEqual(profilerRawDataCheck({ label: 'fci' }), true);
|
||||
assert.strictEqual(profilerRawDataCheck({ label: 'content', expected: 'AABBCC' }), true);
|
||||
assert.strictEqual(profilerRawDataCheck({ label: 'content.rec3' }), true);
|
||||
assert.strictEqual(profilerRawDataCheck({ label: 'content', expected: 'readable' }), false);
|
||||
assert.strictEqual(profilerRawDataCheck({ label: 'content.records' }), false);
|
||||
assert.strictEqual(profilerRawDataCheck({ label: 'fileSize' }), false);
|
||||
});
|
||||
|
||||
test('profilerRenderReport renders raw-data mismatches as aligned readonly fields', () => {
|
||||
global.t = s => s;
|
||||
global.pysimCustomFiles = [];
|
||||
const html = profilerRenderReport([
|
||||
{ path: 'MF/6F07', name: 'EF.IMSI', status: 'fail', checks: [
|
||||
{ label: 'fci', ok: false, expected: '621082024021', actual: '621082024022' },
|
||||
] },
|
||||
{ path: 'MF/7F20/6F3A', name: 'EF.X', status: 'fail', checks: [
|
||||
{ label: 'content.records', ok: false, expected: '30 records', actual: '10 records' },
|
||||
] },
|
||||
]);
|
||||
const fciBlock = html.split('</div>').find(s => s.includes('621082024021'));
|
||||
assert.ok(html.includes('readonly'));
|
||||
assert.ok((html.match(/readonly/g) || []).length >= 2);
|
||||
assert.ok(html.includes('font-mono'));
|
||||
assert.ok(html.includes('value="621082024021"'));
|
||||
assert.ok(html.includes('value="621082024022"'));
|
||||
assert.ok(html.includes('w-24 text-right'));
|
||||
// non-raw check stays inline
|
||||
assert.ok(html.includes('content.records: expected'));
|
||||
assert.ok(!fciBlock.includes(': expected'));
|
||||
delete global.t;
|
||||
});
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "pysim-otaman-server"
|
||||
version = "1.9.26"
|
||||
version = "1.9.28"
|
||||
description = "HTTP REST server wrapping pysim for the OTAMan PWA"
|
||||
requires-python = ">=3.8"
|
||||
# pysim is a git-only dependency installed explicitly by setup.bat/setup.sh.
|
||||
|
||||
@@ -18,7 +18,7 @@ from osmocom.construct import GsmOrUcs2Adapter
|
||||
from osmocom.tlv import BER_TLV_IE
|
||||
|
||||
|
||||
VERSION = '1.9.26'
|
||||
VERSION = '1.9.28'
|
||||
|
||||
MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE
|
||||
|
||||
@@ -1642,6 +1642,7 @@ class PysimHandler(BaseHTTPRequestHandler):
|
||||
'file_size': lchan.selected_file_size() if lchan else None,
|
||||
'record_len': lchan.selected_file_record_len() if lchan else None,
|
||||
'num_of_rec': lchan.selected_file_num_of_rec() if lchan else None,
|
||||
'fci_hex': (lchan.selected_file_fcp_hex or '').upper() if lchan and lchan.selected_file_fcp_hex else None,
|
||||
'exists': True,
|
||||
}
|
||||
self._send_json(data)
|
||||
|
||||
Reference in New Issue
Block a user