feat: CAP compatibility test in the popup + per-AID failure analysis (v3.6.52)

The compatibility test now runs in the same operation modal as the installs
(live progress from the server's ram_progress, Close disabled until done) and
its result gains a per-AID detail block instead of the comma list:

- the probe summary (probe: 2 imports -> 0.0 . +1 appended);
- the import list as tested (ramCompatImportRows): the CAP's imports with the
  version the card was asked for and the CAP's own version when the probe
  lowered it, appended synthetic entries and unmatched lines - each with the
  resolved library name (jcAidName), so a raw AID list becomes readable rows;
- on a rejection, the failure analysis (ramCompatFailureHtml): which CAP
  section the failing block covers (load-file byte range and component names,
  'completes Import'), what the card checks at that point (JC VM spec 4.5.2)
  or that the import list already passed, the remote status word with its
  name, and what the probe state implies (every import at 0.0 -> a missing
  package or another load-file check is the cause; otherwise lower the
  versions and bisect).

ramCompatVerdict names the remote SW; the inline result keeps the verdict +
short probe note and the modal carries the escaped step lines + the detail
HTML (ramOpModalFinish gained an asHtml mode); the server reports
probe_unmatched as [{'aid', 'version'}] (_probe_unmatched), documented in
docs/api.md.  The same failure block for the real install is the next step.

sw cache simple-v325; tests: 550 Python / 703 Node.
This commit is contained in:
2026-09-30 09:22:22 +03:00
parent 599d84fd66
commit ee747003f2
10 changed files with 434 additions and 42 deletions
+16
View File
@@ -40,6 +40,7 @@ a 3.x PWA).
| `/api/ram-install` | POST | Install a Java Card `.cap` file via SCP80 (INSTALL[for load] → LOAD ×N → INSTALL[for install]) |
| `/api/ram-install-app` | POST | Single INSTALL [for install] / [for make selectable] for an already loaded package (no re-load) |
| `/api/cap-info` | POST | Validate a `.cap` archive and estimate its code/NVRAM/RAM requirements (read-only) |
| `/api/cap-compat` | POST | CAP compatibility test: LOAD only to the block completing the Import component (nothing committed) |
| `/api/test/run` | POST | Start a test script (actions + proactive expectations) |
| `/api/test/status` | GET | Test script run state and per-step results |
| `/api/test/stop` | POST | Request a running test script to stop |
@@ -622,6 +623,21 @@ Run the single **INSTALL [for install]** (or **INSTALL [for make selectable]**)
**Response:** `{"success": bool, "steps": [...], "final_cntr": "...", "ram_format": "...", "error": "...", "failed_step": N}` — the same step records as `/api/ram-install`.
### `POST /api/cap-compat`
Run the **CAP compatibility test**: the format check, `INSTALL [for load]` and the LOAD blocks only up to the block that completes the **Import** component (the point where the JCRE verifies the import list, JC VM spec 4.5.2), then stop — no last-block flag and no `INSTALL [for install]`, so nothing is committed and nothing has to be deleted afterwards. The verdict (`imports_ok`) covers the **LOAD/import gate only**; the link gate is what a real install checks at `INSTALL [for install]`.
**Request body:** as `/api/ram-install` (`cap_hex`, the SCP80 fields, optional `load_block_size` and `ram_format`), plus:
| Field | Req | Description |
|---|---|---|
| `probe_imports` | no | `{"all": "x.y"}` or `{aid_hex: "x.y"}`: rewrite the Import component's requested versions (diagnostic) |
| `probe_additions` | no | `[{"aid": hex, "version": "x.y"}]`: append synthetic imports (card-capability queries; an AID already present becomes a version override) |
**Response:** `{"success": bool, "imports_ok": bool, "steps": [...], "boundary_block": N, "total_blocks": M, "load_file_aid": "...", "module_aid": "...", "ram_format": "...", "probe_imports": [...], "probe_unmatched": [...], "final_cntr": "...", "load_block_size": N, "error": "...", "failed_step": N}`.
`probe_imports` lists the entries changed or appended (`{"aid": "...", "from": "1.3", "to": "0.0"}`; `from: null` = an appended synthetic entry). `probe_unmatched` lists override AIDs that matched no Import entry — the CAP does not import them, so the card is never asked about them — as `[{"aid": "...", "version": "0.0"}]` with the version the probe requested. The PWA renders both as a per-AID list in the result popup, together with the failing block's CAP section and the remote status word's meaning.
### `POST /api/sp-verify`
Cross-check a secured packet against pySim's `OtaDialectSms.encode_cmd`
+1 -1
View File
@@ -322,7 +322,7 @@
<tbody>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">Обзор карты (все данные GP)</td><td class="py-1 px-2">Запрос GET STATUS для ISD, приложений, ELF и модулей ELF, а также GET DATA FF21 (расширенные ресурсы карты: число установленных приложений и свободная энергонезависимая/энергозависимая память). Компактный листинг содержит только AID, состояние жизненного цикла и привилегии (версии в устаревшем формате нет); если карта отвергает его (<code class="font-mono text-sm">P2.b2=0</code>, устаревший по GP), запросы повторяются в расширенном формате <code class="font-mono text-sm">E3</code>, а флажок <strong>Запрашивать расширенные данные реестра (связи load-файла и SD)</strong> дополнительно запрашивает их и объединяет расширенные поля: версию исполняемого load-файла, AID load-файла, AID Security Domain, списки модулей, пункты меню STK апплета (SCP Registry Data, TS 102 226 8.2.1.6) и любые другие объекты реестра, которые вернула карта (показываются как есть, без потерь) &mdash; он расходует дополнительные защищённые пакеты, и не все карты поддерживают расширенный запрос, поэтому по умолчанию выключен и запоминается в браузере. Результаты отображаются в обзоре (строка успеха минимальна, счётчики — в заголовках разделов) с кнопками <strong>Удалить</strong> для каждого элемента и определённым форматом команд RAM (подсказка при наведении объясняет compact и expanded).</td></tr>
<tr><td class="py-1 px-2">Установка пакета (.cap файл)</td><td class="py-1 px-2">Отправка <code class="font-mono text-sm">.cap</code> файла на карту через сервер: INSTALL[for load] &rarr; LOAD &times;N &rarr; INSTALL[for install (+make selectable)]. Load-файл делится на LOAD APDU размера <strong>размер блока LOAD</strong> (1&ndash;240 байт полезной нагрузки, по умолчанию 240); каждый защищённый пакет доставляется одним SMS или, если он больше, конкатенированной SMS-PP загрузкой до 5 сегментов, так что большой <code>.cap</code> просто занимает несколько SMS. Сразу после выбора файл проверяется и в форме показывается <strong>оценка требований к памяти</strong>: <strong>требование NVRAM</strong> (образ кода + постоянные данные, сумма в стиле C6+C8 из GP Card Spec) и оценка RAM. Там же показаны <strong>библиотеки, с которыми слинкован CAP</strong> (его компонент Import: имя библиотеки, AID и минимальная версия <code class="font-mono text-sm">≥ x.y</code> — имена стандартных библиотек и семейства подставляются, а где известно, даётся подсказка о версии Java Card), идентификаторы пакета и апплетов и разбивка по компонентам (размеры и доли в порядке load-файла) — всё сгруппировано по разделам <strong>Package</strong>, <strong>Requires</strong>, <strong>Memory</strong> и <strong>Components</strong>. Строка <strong>Platform</strong> показывает минимальный уровень Java Card, который должна предоставить карта (выводится из импорта <code class="font-mono text-sm">javacard.framework</code>; флаг <code class="font-mono text-sm">int</code> в заголовке дополнительно требует поддержки int — Java Card 2.2+), и <strong>релиз ETSI</strong>, из которого пришли импорты UICC API (версия <code class="font-mono text-sm">uicc.toolkit</code> &rarr; REL-N по корпусу <code class="font-mono text-sm">sim-tools</code> в рабочем каталоге; основание показано в скобках): JCRE проверяет каждый импорт по своим пакетам при верификации load-файла (JC VM spec 4.5.2), поэтому карта со старой платформой отклоняет LOAD сразу после завершения компонента <strong>Import</strong> (в строке шага виден remote SW карты и компонент, который завершил блок, например <em>завершает Import</em>) — меньший размер блока LOAD помогает только если блок превышает буфер карты. Повреждённый файл не проходит анализ, и <strong>Execute</strong> остаётся недоступной до успешного анализа. Оценка носит справочный характер и никогда не используется при установке: NVRAM — это образ кода плюс постоянные данные (сумма в стиле C6+C8 по GP Table 11-48; управление памятью карты, округление и запись в реестре не учитываются, а образ статических полей входит в обе части); рекомендуемый <strong>C6</strong> следует за load-файлом, а рекомендуемые квоты — это десятичные значения в байтах (шестнадцатеричное значение в скобках — это кодирование параметров установки C7/C8: C6 — энергонезависимый код, C7 — энергозависимая RAM, C8 — энергонезависимые данные).</td></tr>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">Проверка совместимости CAP</td><td class="py-1 px-2">Отвечает на один вопрос, ничего не устанавливая: <strong>может ли эта карта удовлетворить список импортов CAP?</strong> Сервер выполняет проверку формата, INSTALL[for load] и блоки LOAD только до блока, завершающего компонент <strong>Import</strong> &mdash; именно там JCRE проверяет список импортов &mdash; и останавливается: без флага последнего блока и без INSTALL[for install], поэтому ничего не фиксируется и удалять после этого ничего не нужно. Список <strong>«Версии импортов для проверки»</strong> предзаполняется из анализа CAP: <code class="font-mono text-sm">AID=version</code> переписывает запрошенную версию импорта, <code class="font-mono text-sm">+AID=version</code> добавляет синтетический импорт (запрос возможностей карты: LOAD проходит, только если карта экспортирует этот пакет версии &ge; указанной), а <strong>«Все 0.0»</strong> снимает все требования. Использование в два шага: нажмите «Все 0.0» &mdash; если проверка проходит, причиной был список импортов; затем возвращайте по одной строке, чтобы найти пакет и версию, которых не хватает карте. Вердикт явно указывает, что проверяется только <strong>этап LOAD/импортов</strong>: этап связывания (наличие реально вызываемых API) проверяет настоящая установка на INSTALL[for install].</td></tr>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">Проверка совместимости CAP</td><td class="py-1 px-2">Отвечает на один вопрос, ничего не устанавливая: <strong>может ли эта карта удовлетворить список импортов CAP?</strong> Сервер выполняет проверку формата, INSTALL[for load] и блоки LOAD только до блока, завершающего компонент <strong>Import</strong> &mdash; именно там JCRE проверяет список импортов &mdash; и останавливается: без флага последнего блока и без INSTALL[for install], поэтому ничего не фиксируется и удалять после этого ничего не нужно. Список <strong>«Версии импортов для проверки»</strong> предзаполняется из анализа CAP: <code class="font-mono text-sm">AID=version</code> переписывает запрошенную версию импорта, <code class="font-mono text-sm">+AID=version</code> добавляет синтетический импорт (запрос возможностей карты: LOAD проходит, только если карта экспортирует этот пакет версии &ge; указанной), а <strong>«Все 0.0»</strong> снимает все требования. Использование в два шага: нажмите «Все 0.0» &mdash; если проверка проходит, причиной был список импортов; затем возвращайте по одной строке, чтобы найти пакет и версию, которых не хватает карте. Вердикт явно указывает, что проверяется только <strong>этап LOAD/импортов</strong>: этап связывания (наличие реально вызываемых API) проверяет настоящая установка на INSTALL[for install]. Запуск и результат показываются в окне (как при установке): строки шагов, вердикт и блок деталей по каждому AID &mdash; при отказе он называет раздел CAP, который покрывает неудачный блок (диапазон байтов load-файла и компоненты, и завершает ли этот блок <strong>Import</strong>), SW команды с его значением, список импортов ровно в том виде, в каком он проверялся (версии CAP, правки пробы, добавленные синтетические записи, строки без совпадений) и что означает состояние пробы; при успехе — список импортов, который прошёл проверку.</td></tr>
</tbody>
</table>
+1 -1
View File
@@ -321,7 +321,7 @@
<tbody>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">Explore Card (all GP data)</td><td class="py-1 px-2">Queries GET STATUS for ISD, Applications, ELFs, and ELF Modules, plus GET DATA FF21 (Extended Card Resources: the number of installed applications and the free non-volatile/volatile memory). The compact listing carries the AID, life cycle and privileges (the legacy format has no version); if a card rejects it (<code class="font-mono text-sm">P2.b2=0</code>, deprecated by GP) the queries are retried in the expanded <code class="font-mono text-sm">E3</code> form, and the <strong>Fetch expanded registry details (load-file / SD associations)</strong> checkbox also requests it as an extra pass and merges the expanded-only fields: the Executable Load File version, Load File AID, Security Domain AID, module lists, the applet STK menu entries (SCP Registry Data, TS 102 226 8.2.1.6) and any other registry data objects the card returns (shown raw, never dropped) &mdash; it costs extra secured packets and some cards reject the expanded query, so it is off by default and remembered per browser. Results appear in an explorer view (the success line stays minimal; the counts are in the section headings) with per-item <strong>Delete</strong> buttons and the detected RAM command format (hover for what compact vs expanded means).</td></tr>
<tr><td class="py-1 px-2">Install Package (.cap file)</td><td class="py-1 px-2">Sends a <code class="font-mono text-sm">.cap</code> file to the card via the server: INSTALL[for load] &rarr; LOAD &times;N &rarr; INSTALL[for install (+make selectable)]. The load file is split into LOAD APDUs of the <strong>LOAD block size</strong> (1&ndash;240 bytes of payload, 240 by default); each secured packet is delivered as a single SMS or, when larger, as a concatenated SMS-PP download of up to 5 segments, so a large <code class="font-mono text-sm">.cap</code> simply takes several SMS. As soon as the file is selected it is validated and its <strong>memory requirements are estimated</strong> in the form: the <strong>NVRAM requirement</strong> (the code image + persistent data, the GlobalPlatform Card Spec C6+C8 style total) and the RAM estimate. The same box reports the <strong>libraries the CAP is linked against</strong> (its Import component: library name, AID and the minimum version <code class="font-mono text-sm">≥ x.y</code>, with standard names and families resolved and a Java Card SDK hint where known), the package/applet identity and the component breakdown (sizes and shares in load-file order) &mdash; grouped under the <strong>Package</strong>, <strong>Requires</strong>, <strong>Memory</strong> and <strong>Components</strong> headings. The <strong>Platform</strong> line names the minimum Java Card level the card must provide (derived from the <code class="font-mono text-sm">javacard.framework</code> import; the header <code class="font-mono text-sm">int</code> flag additionally requires int support, Java Card 2.2+) and the <strong>ETSI release</strong> the UICC API imports come from (<code class="font-mono text-sm">uicc.toolkit</code> version &rarr; REL-N, per the workspace <code class="font-mono text-sm">sim-tools</code> corpus; shown with its basis in brackets): the JCRE checks every import against its own packages when it verifies the load file (JC VM spec 4.5.2), so a card whose platform is older rejects the LOAD as soon as the <strong>Import</strong> component is complete (the step line carries the card's remote SW and names the component the block completed, e.g. <em>completes Import</em>) &mdash; a smaller LOAD block only helps when the block exceeds the card's buffer. A corrupt or wrong-format file fails the analysis, and <strong>Execute</strong> stays disabled until a successful analysis. The estimate is informational only and is never used for installation: NVRAM is the code image plus the persistent data (the GlobalPlatform C6+C8 style total, Table 11-48 &mdash; card memory management, rounding and the registry entry are not included, and the static field image is counted in both parts); the suggested <strong>C6</strong> follows the load file, and the suggested quotas are decimal byte counts (the hex in parentheses is how the C7/C8 install parameters encode them: C6 = non-volatile code, C7 = volatile RAM, C8 = non-volatile data).</td></tr>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">CAP compatibility test</td><td class="py-1 px-2">Answers one question without installing anything: <strong>can this card satisfy the CAP's import list?</strong> The server runs the format check, INSTALL[for load] and the LOAD blocks only up to the block that completes the <strong>Import</strong> component &mdash; the point where the JCRE verifies the import list &mdash; then stops: no last-block flag and no INSTALL[for install], so nothing is committed and nothing has to be deleted afterwards. The <strong>Import versions to test</strong> list is pre-filled from the CAP analysis: <code class="font-mono text-sm">AID=version</code> rewrites an import's requested version, <code class="font-mono text-sm">+AID=version</code> appends a synthetic import (a card-capability query: the LOAD passes only if the card exports that package at &ge; that version), and <strong>Set all 0.0</strong> drops every requirement. Two-step use: press Set all 0.0 &mdash; if the test passes, the import list was the blocker; then restore one line at a time to find the package and version the card lacks. The verdict states it covers the <strong>LOAD/import gate only</strong>: the link gate (whether the applet's actual API calls exist) is what a real install checks at INSTALL[for install].</td></tr>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">CAP compatibility test</td><td class="py-1 px-2">Answers one question without installing anything: <strong>can this card satisfy the CAP's import list?</strong> The server runs the format check, INSTALL[for load] and the LOAD blocks only up to the block that completes the <strong>Import</strong> component &mdash; the point where the JCRE verifies the import list &mdash; then stops: no last-block flag and no INSTALL[for install], so nothing is committed and nothing has to be deleted afterwards. The <strong>Import versions to test</strong> list is pre-filled from the CAP analysis: <code class="font-mono text-sm">AID=version</code> rewrites an import's requested version, <code class="font-mono text-sm">+AID=version</code> appends a synthetic import (a card-capability query: the LOAD passes only if the card exports that package at &ge; that version), and <strong>Set all 0.0</strong> drops every requirement. Two-step use: press Set all 0.0 &mdash; if the test passes, the import list was the blocker; then restore one line at a time to find the package and version the card lacks. The verdict states it covers the <strong>LOAD/import gate only</strong>: the link gate (whether the applet's actual API calls exist) is what a real install checks at INSTALL[for install]. The run and its result appear in a popup (like the install): the step lines, the verdict and a per-AID detail block &mdash; on a rejection it names the CAP section the failing block covers (load-file byte range and components, and whether that block completes <strong>Import</strong>), the remote status word with its meaning, the import list exactly as tested (CAP versions, probe rewrites, appended synthetic entries, unmatched lines) and what the probe state implies; on success it lists the import list that passed.</td></tr>
</tbody>
</table>
+201 -24
View File
@@ -1749,7 +1749,7 @@
// ===== Version =====
// Single source of truth for the PWA version: shown in the header and used
// by the server version check in pysimConnect().
const SIMPLE_VERSION = '3.6.51';
const SIMPLE_VERSION = '3.6.52';
document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION;
// ===== Tab switching =====
@@ -8368,7 +8368,7 @@ function ramOpProgress(p) {
(p.total ? Math.min(100, Math.round(p.step * 100 / p.total)) : 10) + '%';
}
function ramOpModalFinish(summary, ok, stepsText) {
function ramOpModalFinish(summary, ok, content, asHtml) {
_ramOpActive = false;
window.removeEventListener('beforeunload', ramOpBeforeUnload);
document.getElementById('ram-op-spin').classList.add('hidden');
@@ -8376,7 +8376,13 @@ function ramOpModalFinish(summary, ok, stepsText) {
document.getElementById('ram-op-status').textContent = summary || '';
document.getElementById('ram-op-status').className = 'text-sm font-semibold ' +
(ok ? 'text-green-600 dark:text-green-400' : 'text-red-600 dark:text-red-400');
if (stepsText !== undefined) document.getElementById('ram-op-steps').textContent = stepsText || '';
if (content !== undefined) {
// asHtml is only used by views that build escaped HTML (the
// compatibility-test detail); everything else stays plain text.
const el = document.getElementById('ram-op-steps');
if (asHtml) el.innerHTML = content || '';
else el.textContent = content || '';
}
document.getElementById('ram-op-warn').classList.add('hidden');
document.getElementById('ram-op-close').disabled = false;
}
@@ -9674,7 +9680,7 @@ function ramProbeAllZero() {
// the load file; the link gate is what a real install checks later. The
// headline distinguishes where the run actually failed: a request error, the
// format probe (the card not answering), the INSTALL [for load] request, or
// the import list itself.
// the import list itself - and names the remote status word.
function ramCompatVerdict(data) {
if (!data) return '';
const stepOk = s => !s.por_error && (s.por_status === 'por_ok' || s.por_status === 'no_por');
@@ -9688,17 +9694,21 @@ function ramCompatVerdict(data) {
}
const scope = t('tested through LOAD') + ' ' + (data.boundary_block || '?') +
'/' + (data.total_blocks || '?');
if (data.imports_ok) return t('Import list accepted') + ' \u2014 ' + scope;
const failed = data.steps.find(s => !stepOk(s)) || {};
const sw = String(failed.por_sw || '');
const swName = sw ? lookupSw(sw.substr(0, 2), sw.substr(2, 2), 'gp') : '';
const swText = sw ? t('remote SW') + ' ' + sw + (swName ? ' (' + swName + ')' : '')
: (data.error || t('no data'));
if (data.imports_ok) return t('Import list accepted') + ' \u2014 ' + scope;
if (String(failed.name || '') === 'INSTALL [for load]') {
return t('Load request rejected') + ' \u2014 ' + (data.error || t('no data'));
return t('Load request rejected') + ' \u2014 ' + swText;
}
return t('Import list rejected') + ' \u2014 ' + scope +
(data.error ? ' \u00b7 ' + data.error : '');
return t('Import list rejected') + ' \u2014 ' + scope + ' \u00b7 ' + swText;
}
// The applied probe changes of a compatibility test (pure): version rewrites,
// appended synthetic imports and override AIDs that matched nothing.
// The applied probe changes of a compatibility test (pure): version rewrites
// and appended synthetic imports. The unmatched override AIDs are a list of
// their own in the modal detail (ramCompatDetailHtml).
function ramCompatProbeNote(data) {
const pa = (data && data.probe_imports) || [];
const added = pa.filter(p => p.from === null || p.from === undefined).length;
@@ -9710,11 +9720,140 @@ function ramCompatProbeNote(data) {
(targets.length === 1 ? targets[0] : t('mixed')));
}
if (added) parts.push('+' + added + ' ' + t('appended'));
const unmatched = (data && data.probe_unmatched) || [];
if (unmatched.length) parts.push(t('unmatched') + ': ' + unmatched.join(', '));
return parts.length ? t('probe') + ': ' + parts.join(' \u00b7 ') : '';
}
// Import-list rows of a compatibility test (pure): one row per AID the run
// touches - the CAP's imports (with the version the card was asked for, and
// the CAP's own version when the probe lowered it), the appended synthetic
// imports, and the probe override AIDs that matched nothing.
// state: 'cap' | 'lowered' | 'appended' | 'unmatched'.
function ramCompatImportRows(data, mem) {
const applied = (data && data.probe_imports) || [];
const unmatched = (data && data.probe_unmatched) || [];
const byAid = {};
applied.forEach(p => { const a = String(p.aid || '').toUpperCase(); if (a) byAid[a] = p; });
const rows = [];
const seen = {};
const push = r => { if (!r.aid || seen[r.aid]) return; seen[r.aid] = 1; rows.push(r); };
((mem && mem.imports) || []).forEach(im => {
const aid = String(im.aid || '').toUpperCase();
const capVer = (Number(im.major) || 0) + '.' + (Number(im.minor) || 0);
const ap = byAid[aid];
push({ aid: aid, capVersion: capVer, testedVersion: ap ? ap.to : capVer,
state: ap ? 'lowered' : 'cap' });
});
applied.forEach(p => {
const aid = String(p.aid || '').toUpperCase();
const appended = (p.from === null || p.from === undefined);
push({ aid: aid, capVersion: appended ? null : p.from, testedVersion: p.to,
state: appended ? 'appended' : 'lowered' });
});
unmatched.forEach(u => {
const aid = String((u && u.aid) || u || '').toUpperCase();
push({ aid: aid, capVersion: null, testedVersion: (u && u.version) || '',
state: 'unmatched' });
});
return rows;
}
// One row per import: AID (resolved library name) - tested version, with the
// probe state as a muted note.
function ramCompatRowsHtml(rows) {
if (!rows || !rows.length) return '';
return '<div class="mt-1">' + rows.map(r => {
let ver = r.testedVersion ? esc(r.testedVersion) : '';
if (r.state === 'lowered' && r.capVersion) {
ver = esc(r.capVersion) + ' \u2192 ' + esc(r.testedVersion || '?');
}
let note = '';
if (r.state === 'lowered') note = t('probe');
else if (r.state === 'appended') note = t('appended') + ' \u2014 ' + t('not in the CAP');
else if (r.state === 'unmatched') note = t('not in the CAP\'s import list');
return '<div>' + jcAidHtml(r.aid) + (ver ? ' \u2014 ' + ver : '') +
(note ? ' <span class="text-gray-500 dark:text-slate-400">(' + esc(note) + ')</span>' : '') +
'</div>';
}).join('') + '</div>';
}
// Failure analysis of a rejected LOAD (pure HTML): which CAP section the
// failing block covers and what it completes, what the card checks at that
// point (JC VM spec 4.5.2), the remote status word, the import list as tested
// and what the run's probe state implies. Used by the compatibility test
// modal (the install side reuses it in the next step).
function ramCompatFailureHtml(data, mem) {
if (!data || data.success || data.imports_ok) return '';
const steps = data.steps || [];
const step = steps[(Number(data.failed_step) || 1) - 1] || null;
const name = step ? String(step.name || '') : '';
if (!/^(LOAD |INSTALL \[for load\])/.test(name)) return '';
const bs = Number(data.load_block_size) || 0;
const sc = ramStepComponents(name, mem && mem.components, bs);
const loadSize = (mem && mem.code) ? (Number(mem.code.load_file) || 0) : 0;
let html = '<div class="mt-2 pt-2 border-t border-gray-200 dark:border-slate-700">';
// where it failed: the block, its load-file byte range and CAP components
let where = t('Failed in:') + ' ' + name;
const m = /^LOAD \((\d+)\/(\d+)\)$/.exec(name);
if (m && bs) {
const start = (Number(m[1]) - 1) * bs;
const end = loadSize ? Math.min(loadSize, start + bs) - 1 : start + bs - 1;
where += ' \u2014 ' + t('load-file bytes') + ' ' + start + '\u2013' + end +
(loadSize ? ' / ' + loadSize : '');
}
if (sc) {
where += ' \u00b7 ' + t('covers') + ' ' + sc.names.join(', ');
if (sc.completed.length) where += ' \u00b7 ' + t('completes') + ' ' + sc.completed[sc.completed.length - 1];
}
html += '<div>' + esc(where) + '</div>';
// what the card checks at that point (JC VM spec 4.5.2)
const completesImport = !!(sc && sc.completed.indexOf('Import') >= 0);
let importDone = '';
steps.forEach(s => {
const c = ramStepComponents(String(s.name || ''), mem && mem.components, bs);
if (c && c.completed.indexOf('Import') >= 0) importDone = String(s.name || '');
});
if (completesImport) {
html += '<div>' + esc(t('The card verifies the load file\'s import list when the Import component is complete (JC VM spec 4.5.2).')) + '</div>';
} else if (importDone) {
html += '<div>' + esc(t('The import list already passed \u2014 the Import component completed at') + ' ' + importDone + '.') + '</div>';
}
// the status word
const sw = step && step.por_sw ? String(step.por_sw) : '';
if (sw) {
const swName = lookupSw(sw.substr(0, 2), sw.substr(2, 2), 'gp');
html += '<div>' + esc(t('remote SW') + ' ' + sw + (swName ? ' \u2014 ' + swName : '')) + '</div>';
}
// the import list as tested + what the probe state implies
const rows = ramCompatImportRows(data, mem);
if (rows.length) {
html += '<div class="mt-1">' + esc(t('Import list under test') + ' (' + rows.length + '):') + '</div>';
html += ramCompatRowsHtml(rows);
const capRows = rows.filter(r => r.state === 'cap' || r.state === 'lowered');
const atCap = capRows.filter(r => r.state === 'cap').length;
if (capRows.length && !atCap) {
html += '<div>' + esc(t('Every import was tested at 0.0 \u2014 a package the card does not have (or another load-file check, e.g. memory) is the cause.')) + '</div>';
} else if (capRows.length) {
html += '<div>' + esc(t('The card checks the whole list at that point \u2014 lower the versions (Set all 0.0) to test whether the list is the cause, then restore one line at a time to find the culprit.')) + '</div>';
}
}
html += '</div>';
return html;
}
// Modal detail of a compatibility test (pure HTML): the failure analysis when
// the card refused, or the import list that passed.
function ramCompatDetailHtml(data, mem) {
if (!data) return '';
if (data.imports_ok) {
const rows = ramCompatImportRows(data, mem);
if (!rows.length) return '';
return '<div class="mt-2 pt-2 border-t border-gray-200 dark:border-slate-700">' +
'<div>' + esc(t('Import list under test') + ' (' + rows.length + '):') + '</div>' +
ramCompatRowsHtml(rows) + '</div>';
}
return ramCompatFailureHtml(data, mem);
}
// CAP compatibility test: run the load only as far as the Import component's
// end and report whether the card accepted the import list. Nothing is
// installed or committed (the install operation keeps the CAP's own versions).
@@ -9725,36 +9864,63 @@ async function ramCompatTest(sp) {
if (file.size > 48 * 1024) { alert(t('CAP file exceeds 48 kB limit')); return; }
const parsed = ramProbeParse(document.getElementById('ram-probe-versions').value);
if (parsed.error) { alert(parsed.error); return; }
const bsRaw = (document.getElementById('ram-compat-block').value || '').trim();
let blockSize = null;
if (bsRaw !== '') {
const bs = parseInt(bsRaw, 10);
if (!(bs >= 1 && bs <= 240)) { alert(t('LOAD block size must be 1..240')); return; }
blockSize = bs;
}
const mem = (_capAnalysis.ram && _capAnalysis.ram.memory) || null;
const capPkg = (mem && mem.package_aid) || '';
ramClearResults();
ramShowProgress(t('Sending to server...'));
// The run holds the card lock; the modal carries the live progress (the
// server reports ram_progress for cap-compat) and the failure analysis.
ramOpModalBegin(t('CAP compatibility test') + (capPkg ? ' \u2014 ' + capPkg : ''));
ramOpModalStatus(t('Reading CAP file...'));
const body = {
cap_hex: await ramReadFileHex(file),
probe_imports: parsed.map, probe_additions: parsed.additions,
spi1: sp.spi1, spi2: '01', kic: sp.kic, kid: sp.kid,
tar: sp.tar, cntr: sp.cntr, kicKey: sp.kicKey, kidKey: sp.kidKey,
};
const bsRaw = (document.getElementById('ram-compat-block').value || '').trim();
if (bsRaw !== '') {
const bs = parseInt(bsRaw, 10);
if (!(bs >= 1 && bs <= 240)) { alert(t('LOAD block size must be 1..240')); return; }
body.load_block_size = bs;
}
if (blockSize !== null) body.load_block_size = blockSize;
ramOpModalStatus(t('Sending to server...'));
const data = await pysimFetch('/api/cap-compat', body);
ramHideProgress();
let data;
try {
data = await pysimFetch('/api/cap-compat', body);
} catch (e) {
const msg = t('Error') + ': ' + e.message;
const el = document.getElementById('ram-result');
el.textContent = msg;
el.classList.remove('hidden', 'text-green-600');
el.classList.add('text-red-600');
ramOpModalFinish(msg, false, '');
return;
}
if (data.final_cntr) ramSaveCntr(data.final_cntr);
const comps = _capAnalysis.ram.memory && _capAnalysis.ram.memory.components;
// Inline at the bottom of the form: the verdict, the short probe note and
// the step lines; the per-AID detail lives in the modal.
const comps = mem && mem.components;
let txt = '';
(data.steps || []).forEach((s, idx) => { txt += ramStepLine(s, idx, comps, data.load_block_size) + '\n'; });
const stepsEl = document.getElementById('ram-steps');
stepsEl.classList.remove('hidden');
stepsEl.textContent = txt;
const resultEl = document.getElementById('ram-result');
const verdict = ramCompatVerdict(data);
const probeNote = ramCompatProbeNote(data);
resultEl.textContent = ramCompatVerdict(data) + (probeNote ? ' \u2014 ' + probeNote : '');
const resultEl = document.getElementById('ram-result');
resultEl.textContent = verdict + (probeNote ? ' \u2014 ' + probeNote : '');
resultEl.classList.remove('hidden', 'text-green-600', 'text-red-600');
resultEl.classList.add(data.imports_ok ? 'text-green-600' : 'text-red-600');
// Modal: the step lines (escaped) plus the probe summary and the per-AID
// detail (the failure analysis when the card refused).
let modal = esc(txt);
if (probeNote) modal += '<div class="mt-2 text-gray-500 dark:text-slate-400">' + esc(probeNote) + '</div>';
modal += ramCompatDetailHtml(data, mem);
ramOpModalFinish(verdict, !!data.imports_ok, modal, true);
}
async function ramInstallCap(sp) {
@@ -17729,6 +17895,17 @@ const LANG_RU = {
'unmatched': 'без совпадений',
'Load request rejected': 'Запрос загрузки отклонён',
'The card did not answer the format probe': 'Карта не ответила на пробу формата',
'Sending to server...': 'Отправка на сервер...',
'Failed in:': 'Ошибка в:',
'load-file bytes': 'байты load-файла',
'covers': 'охватывает',
'Import list under test': 'Список импортов под проверкой',
'not in the CAP': 'нет в CAP',
"not in the CAP's import list": 'нет в списке импортов CAP',
"The card verifies the load file's import list when the Import component is complete (JC VM spec 4.5.2).": 'Карта проверяет список импортов load-файла, когда компонент Import завершён (JC VM spec 4.5.2).',
'The import list already passed \u2014 the Import component completed at': 'Список импортов уже пройден \u2014 компонент Import завершён на',
'Every import was tested at 0.0 \u2014 a package the card does not have (or another load-file check, e.g. memory) is the cause.': 'Все импорты проверены с версией 0.0 \u2014 причина в пакете, которого нет на карте (или в другой проверке load-файла, например памяти).',
'The card checks the whole list at that point \u2014 lower the versions (Set all 0.0) to test whether the list is the cause, then restore one line at a time to find the culprit.': 'Карта проверяет весь список в этой точке \u2014 понизьте версии («Все 0.0»), чтобы проверить, в списке ли причина, затем возвращайте по одной строке, чтобы найти виновника.',
'completes': 'завершает',
'inside': 'внутри',
'the failed step completes': 'неудавшийся шаг завершает',
+1 -1
View File
@@ -1,4 +1,4 @@
const CACHE = 'simple-v324';
const CACHE = 'simple-v325';
const URLS = [
'index.html',
'help.html',
+150 -7
View File
@@ -22,7 +22,7 @@ function extractFunc(src, name) {
}
// Extract chain builder functions and dependencies
const FNS = ['berLenStr', 'buildApdu', 'escHtml', 'esc', 'chainInit', 'chainRamBuildRowHex', 'ramFmtLifecycle', 'ramFmtVersion', 'ramMenuState', 'ramEntryKind', 'ramMenuHtml', 'ramRawTlvsHtml', 'ramNvLine', 'ramFmtPrivileges', 'ramRenderExploreHtml', 'ramMergeExpanded', 'ramStepLine', 'ramStepComponents', 'ramInstallFailHint', 'ramProbeParse', 'ramCompatVerdict', 'ramCompatProbeNote', 'ramExpandedDetailsInit', 'ramExpandedDetailsChanged', 'ramGetStatusApdu', 'ramDeleteApdu',
const FNS = ['berLenStr', 'buildApdu', 'escHtml', 'esc', 'chainInit', 'chainRamBuildRowHex', 'ramFmtLifecycle', 'ramFmtVersion', 'ramMenuState', 'ramEntryKind', 'ramMenuHtml', 'ramRawTlvsHtml', 'ramNvLine', 'ramFmtPrivileges', 'ramRenderExploreHtml', 'ramMergeExpanded', 'ramStepLine', 'ramStepComponents', 'ramInstallFailHint', 'ramProbeParse', 'ramCompatVerdict', 'ramCompatProbeNote', 'ramCompatImportRows', 'ramCompatRowsHtml', 'ramCompatFailureHtml', 'ramCompatDetailHtml', 'lookupSw', 'ramExpandedDetailsInit', 'ramExpandedDetailsChanged', 'ramGetStatusApdu', 'ramDeleteApdu',
'stkParamsBuild', 'ramRemoteSwOk', 'spPorAccepted', 'ramIncrementCntr', 'ramDeleteFromExplorer', 'ramListingSpi2', 'ramRemoveFromExplorer', 'ramHasInstance', 'ramExpandedQueryApdu',
'_parseRawElfEntry', '_parseRawAppEntry', 'ramParseElfStatus', 'ramParseAppStatus', 'ramParseModuleAids', 'parseTLV', '_parseE3Entry', '_parseMenuEntries',
'ramCardIdxAfterRemove', 'ramClearResults', 'ramHideProgress', 'ramOpChanged', 'ramRender', 'ramApplyCard', 'ramExecute', 'decodePrivileges', 'ramActionBtn', 'ramCapToolkitMode', 'ramOpProgressText',
@@ -33,6 +33,8 @@ for (const f of FNS) {
}
const m = html.match(/const _chains = \{\};/);
if (m) code += m[0].replace(/^const /, 'var ') + '\n';
const swm = html.match(/const SW_MAP = \{[\s\S]*?\n\};/);
if (swm) code += swm[0].replace(/^const /, 'var ') + '\n';
const pn = html.match(/const PRIVILEGE_NAMES = \[[\s\S]*?\n\];/);
if (pn) code += pn[0].replace(/^const /, 'var ') + '\n';
const an = html.match(/const JC_AID_NAMES = \{[\s\S]*?\n\};/);
@@ -960,9 +962,9 @@ test('ramProbeParse reads AID=version lines', () => {
test('ramCompatVerdict distinguishes where the test failed', () => {
globalThis.t = s => s;
const step = (name, ok) => ok
const step = (name, ok, sw) => ok
? { name: name, por_status: 'por_ok', por_sw: '9000' }
: { name: name, por_status: 'por_ok', por_sw: '6438', por_error: 'remote SW 6438' };
: { name: name, por_status: 'por_ok', por_sw: sw || '6438', por_error: 'remote SW ' + (sw || '6438') };
assert.strictEqual(ramCompatVerdict({ imports_ok: true, boundary_block: 3, total_blocks: 240,
steps: [step('FORMAT CHECK (compact)', true), step('INSTALL [for load]', true), step('LOAD (3/240)', true)] }),
'Import list accepted \u2014 tested through LOAD 3/240');
@@ -970,6 +972,11 @@ test('ramCompatVerdict distinguishes where the test failed', () => {
error: 'remote SW 6438',
steps: [step('FORMAT CHECK (compact)', true), step('INSTALL [for load]', true), step('LOAD (3/240)', false)] }),
'Import list rejected \u2014 tested through LOAD 3/240 \u00b7 remote SW 6438');
// the status word name is shown when the SW map knows it (GP 6985)
assert.strictEqual(ramCompatVerdict({ imports_ok: false, boundary_block: 1, total_blocks: 12,
error: 'LOAD (1/12): remote SW 6985',
steps: [step('FORMAT CHECK (compact)', true), step('INSTALL [for load]', true), step('LOAD (1/12)', false, '6985')] }),
'Import list rejected \u2014 tested through LOAD 1/12 \u00b7 remote SW 6985 (Conditions of use not satisfied)');
// the card never answered the format probe
assert.strictEqual(ramCompatVerdict({ imports_ok: false, boundary_block: 1, total_blocks: 240,
steps: [step('FORMAT CHECK (compact)', false), step('FORMAT CHECK (expanded)', false)] }),
@@ -977,8 +984,8 @@ test('ramCompatVerdict distinguishes where the test failed', () => {
// the INSTALL [for load] request itself was refused
assert.strictEqual(ramCompatVerdict({ imports_ok: false, boundary_block: 1, total_blocks: 240,
error: 'remote SW 6985',
steps: [step('FORMAT CHECK (compact)', true), step('INSTALL [for load]', false)] }),
'Load request rejected \u2014 remote SW 6985');
steps: [step('FORMAT CHECK (compact)', true), step('INSTALL [for load]', false, '6985')] }),
'Load request rejected \u2014 remote SW 6985 (Conditions of use not satisfied)');
// a request error without steps
assert.strictEqual(ramCompatVerdict({ error: 'import probe: invalid version' }),
'Error: import probe: invalid version');
@@ -994,11 +1001,147 @@ test('ramCompatProbeNote summarizes the applied probe', () => {
assert.strictEqual(ramCompatProbeNote({ probe_imports: [
{ aid: 'A1', from: '1.3', to: '0.0' }, { aid: 'A2', from: '1.4', to: '1.0' }] }),
'probe: 2 imports \u2192 mixed');
assert.strictEqual(ramCompatProbeNote({ probe_imports: [], probe_unmatched: ['DEADBEEF'] }),
'probe: unmatched: DEADBEEF');
// the unmatched AIDs are a list of their own in the modal detail, not a
// comma string in the summary line
assert.strictEqual(ramCompatProbeNote({ probe_imports: [], probe_unmatched: ['DEADBEEF'] }), '');
assert.strictEqual(ramCompatProbeNote({}), '');
});
test('ramCompatImportRows merges the CAP imports, the probe and the unmatched lines', () => {
const mem = {
imports: [
{ aid: 'A0000000620101', major: 1, minor: 3 },
{ aid: 'A0000000620102', major: 1, minor: 3 },
{ aid: 'A0000000090005FFFFFFFF8911000000', major: 1, minor: 2 },
],
};
const data = {
probe_imports: [
{ aid: 'A0000000620102', from: '1.3', to: '0.0' },
{ aid: 'A0000000620201', from: null, to: '1.0' },
],
probe_unmatched: [{ aid: 'A0000000090005FFFFFFFF8912000000', version: '0.0' }],
};
assert.deepStrictEqual(ramCompatImportRows(data, mem), [
{ aid: 'A0000000620101', capVersion: '1.3', testedVersion: '1.3', state: 'cap' },
{ aid: 'A0000000620102', capVersion: '1.3', testedVersion: '0.0', state: 'lowered' },
{ aid: 'A0000000090005FFFFFFFF8911000000', capVersion: '1.2', testedVersion: '1.2', state: 'cap' },
{ aid: 'A0000000620201', capVersion: null, testedVersion: '1.0', state: 'appended' },
{ aid: 'A0000000090005FFFFFFFF8912000000', capVersion: null, testedVersion: '0.0', state: 'unmatched' },
]);
// without the CAP analysis at hand the probe entries still produce rows
assert.deepStrictEqual(ramCompatImportRows(data, null).map(r => r.aid),
['A0000000620102', 'A0000000620201', 'A0000000090005FFFFFFFF8912000000']);
});
test('ramCompatRowsHtml lists AIDs with names, versions and probe state', () => {
globalThis.t = s => s;
const out = ramCompatRowsHtml([
{ aid: 'A0000000620101', capVersion: '1.3', testedVersion: '1.3', state: 'cap' },
{ aid: 'A0000000620102', capVersion: '1.3', testedVersion: '0.0', state: 'lowered' },
{ aid: 'A0000000620201', capVersion: null, testedVersion: '1.0', state: 'appended' },
{ aid: 'A0000000090005FFFFFFFF8912000000', capVersion: null, testedVersion: '0.0', state: 'unmatched' },
]);
delete globalThis.t;
assert.ok(out.includes('A0000000620101 <span class="text-gray-400 dark:text-slate-500">(javacard.framework)</span> \u2014 1.3'), out);
assert.ok(out.includes('1.3 \u2192 0.0 <span class="text-gray-500 dark:text-slate-400">(probe)</span>'), out);
assert.ok(out.includes('(appended \u2014 not in the CAP)'), out);
assert.ok(out.includes("(not in the CAP's import list)"), out);
assert.ok(out.includes('(uicc.toolkit)'), out);
assert.strictEqual(ramCompatRowsHtml([]), '');
});
test('ramCompatFailureHtml names the failing CAP section and the import list', () => {
globalThis.t = s => s;
const mem = {
imports: [
{ aid: 'A0000000620101', major: 1, minor: 3 },
{ aid: 'A0000000620102', major: 1, minor: 3 },
{ aid: 'A0000000871005FFFFFFFF8913200000', major: 1, minor: 9 },
{ aid: 'A0000000090005FFFFFFFF8911000000', major: 1, minor: 2 },
],
components: [
{ name: 'Header', size: 100 },
{ name: 'Directory', size: 100 },
{ name: 'Import', size: 40 },
{ name: 'Method', size: 200 },
],
code: { load_file: 440 },
};
const data = {
success: false, imports_ok: false, failed_step: 3, boundary_block: 1, total_blocks: 2,
load_block_size: 240, error: 'LOAD (1/2): remote SW 6985',
steps: [
{ name: 'FORMAT CHECK (compact)', por_status: 'por_ok', por_sw: '9000' },
{ name: 'INSTALL [for load]', por_status: 'por_ok', por_sw: '9000' },
{ name: 'LOAD (1/2)', por_status: 'por_ok', por_sw: '6985', por_error: 'remote SW 6985' },
],
probe_imports: [
{ aid: 'A0000000620102', from: '1.3', to: '0.0' },
{ aid: 'A0000000090005FFFFFFFF8911000000', from: '1.2', to: '0.0' },
{ aid: 'A0000000620201', from: null, to: '1.0' },
],
probe_unmatched: [{ aid: 'A0000000090005FFFFFFFF8912000000', version: '0.0' }],
};
const out = ramCompatFailureHtml(data, mem);
assert.ok(out.includes('Failed in: LOAD (1/2) \u2014 load-file bytes 0\u2013239 / 440 \u00b7 covers Header, Directory, Import \u00b7 completes Import'), out);
assert.ok(out.includes("The card verifies the load file's import list when the Import component is complete (JC VM spec 4.5.2)."), out);
assert.ok(out.includes('remote SW 6985 \u2014 Conditions of use not satisfied'), out);
assert.ok(out.includes('Import list under test (6):'), out);
assert.ok(out.includes('(javacard.framework)'), out);
// two imports are still at their CAP versions -> the lower-them hint
assert.ok(out.includes('lower the versions (Set all 0.0)'), out);
assert.ok(!out.includes('Every import was tested at 0.0'), out);
// a run with every import lowered: the list cannot be the cause
const allZero = Object.assign({}, data, {
probe_imports: mem.imports.map(im => ({ aid: im.aid, from: im.major + '.' + im.minor, to: '0.0' })),
probe_unmatched: [],
});
const out2 = ramCompatFailureHtml(allZero, mem);
assert.ok(out2.includes('Every import was tested at 0.0'), out2);
// a failure later in the file: the import list already passed
const later = Object.assign({}, data, {
failed_step: 4,
steps: [
{ name: 'FORMAT CHECK (compact)', por_status: 'por_ok', por_sw: '9000' },
{ name: 'INSTALL [for load]', por_status: 'por_ok', por_sw: '9000' },
{ name: 'LOAD (1/2)', por_status: 'por_ok', por_sw: '9000' },
{ name: 'LOAD (2/2)', por_status: 'por_ok', por_sw: '6581', por_error: 'remote SW 6581' },
],
});
const out3 = ramCompatFailureHtml(later, mem);
assert.ok(out3.includes('The import list already passed \u2014 the Import component completed at LOAD (1/2).'), out3);
assert.ok(out3.includes('remote SW 6581'), out3);
assert.ok(!out3.includes('4.5.2'), out3);
delete globalThis.t;
// not a LOAD failure at all: no block analysis
assert.strictEqual(ramCompatFailureHtml(Object.assign({}, data, {
failed_step: 1, steps: [{ name: 'FORMAT CHECK (compact)', por_status: 'por_error' }] }), mem), '');
assert.strictEqual(ramCompatFailureHtml({ success: true, imports_ok: true }, mem), '');
});
test('ramCompatDetailHtml shows the tested list on success and the analysis on failure', () => {
globalThis.t = s => s;
const mem = { imports: [{ aid: 'A0000000620101', major: 1, minor: 3 }] };
const ok = ramCompatDetailHtml({ imports_ok: true, probe_imports: [] }, mem);
assert.ok(ok.includes('Import list under test (1):'), ok);
assert.ok(ok.includes('(javacard.framework)'), ok);
assert.ok(!ok.includes('Failed in:'), ok);
const bad = ramCompatDetailHtml({ imports_ok: false, failed_step: 1, load_block_size: 240,
steps: [{ name: 'LOAD (1/1)', por_status: 'por_ok', por_sw: '6985', por_error: 'remote SW 6985' }] }, mem);
assert.ok(bad.includes('Failed in: LOAD (1/1)'), bad);
delete globalThis.t;
assert.strictEqual(ramCompatDetailHtml(null, mem), '');
});
test('the compatibility test runs in the modal like the install', () => {
const src = extractFunc(html, 'ramCompatTest');
assert.ok(src.includes('ramOpModalBegin'), 'must open the operation modal');
assert.ok(src.includes('ramOpModalFinish'), 'must finish it with the verdict');
assert.ok(src.includes('ramCompatDetailHtml'), 'the per-AID detail must reach the modal');
assert.ok(!src.includes('ramShowProgress'), 'the inline progress bar is replaced by the modal');
});
test('ramExecute dispatches the compatibility test op', () => {
const src = extractFunc(html, 'ramExecute');
assert.ok(/op === 'compat'/.test(src), 'the compat op must be dispatched');
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "pysim-simple-server"
version = "3.6.51"
version = "3.6.52"
description = "HTTP REST server wrapping pysim for the SIMple PWA"
requires-python = ">=3.8"
# pysim is a git-only dependency installed explicitly by setup.bat/setup.sh.
+20 -6
View File
@@ -31,7 +31,7 @@ from osmocom.tlv import BER_TLV_IE
from osmocom.utils import rpad
VERSION = '3.6.51'
VERSION = '3.6.52'
MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE
@@ -895,6 +895,23 @@ def _cap_import_aids(load_file_hex):
return []
def _probe_unmatched(probe, known):
"""Import-probe override AIDs that matched no Import entry: the CAP does
not import them, so the card is never asked about them. Returns
[{'aid': hex, 'version': 'x.y'}] (the version the probe requested), sorted
by AID; the 'all' override is skipped."""
out = []
for key, version in (probe or {}).items():
k = str(key).replace(' ', '')
if k.lower() == 'all':
continue
if k.upper() in known:
continue
out.append({'aid': k.upper(), 'version': str(version)})
out.sort(key=lambda e: e['aid'])
return out
def _cap_compat_blocks(load_file_hex, block_size):
"""(boundary, total) LOAD block numbers for the compatibility test.
@@ -6990,13 +7007,10 @@ class PysimHandler(BaseHTTPRequestHandler):
known = original_aids | {
str(a.get('aid', '')).replace(' ', '').upper()
for a in additions}
probe_unmatched = sorted(
k for k in probe
if str(k).lower() != 'all'
and str(k).replace(' ', '').upper() not in known)
probe_unmatched = _probe_unmatched(probe, known)
sys.stderr.write('CAP-COMPAT: import probe applied: %s%s\n' % (
json.dumps(probe_applied),
(' (unmatched: %s)' % ', '.join(probe_unmatched))
(' (unmatched: %s)' % ', '.join(e['aid'] for e in probe_unmatched))
if probe_unmatched else ''))
block_size_req = body.get('load_block_size')
+18 -1
View File
@@ -10,7 +10,8 @@ import zipfile
from pysim_simple_server import capmem
from pysim_simple_server.server import (
_ber_len, _cap_apdu_sequence, _cap_compat_blocks, _cap_import_aids,
_cap_import_end, _cap_info_body, _cap_parse, _probe_import_versions)
_cap_import_end, _cap_info_body, _cap_parse, _probe_import_versions,
_probe_unmatched)
# ─── synthetic CAP builder ───────────────────────────────────────────────
@@ -276,6 +277,22 @@ class ImportProbeTests(unittest.TestCase):
self.assertEqual(_cap_import_aids(self._load_file()),
[JAVACARD_FRAMEWORK.hex().upper(), self.OTHER_AID.hex().upper()])
def test_probe_unmatched_reports_the_requested_version(self):
known = set(_cap_import_aids(self._load_file()))
# an override for an AID the CAP does not import: kept, with the
# version the probe asked for (the modal lists it per AID)
self.assertEqual(
_probe_unmatched({JAVACARD_FRAMEWORK.hex().upper(): '0.0',
'DEADBEEF': '1.2', 'all': '0.0'}, known),
[{'aid': 'DEADBEEF', 'version': '1.2'}])
# matched AIDs and the 'all' override are not unmatched
self.assertEqual(_probe_unmatched({'all': '0.0'}, known), [])
self.assertEqual(_probe_unmatched({JAVACARD_FRAMEWORK.hex().lower(): '0.0'}, known), [])
# a lowercase / spaced unmatched AID is normalised for display
self.assertEqual(_probe_unmatched({'dead beef': '1.2'}, known),
[{'aid': 'DEADBEEF', 'version': '1.2'}])
self.assertEqual(_probe_unmatched(None, known), [])
def test_probe_rejects_bad_additions(self):
load = self._load_file()
with self.assertRaises(ValueError):
+25
View File
@@ -222,6 +222,31 @@ class CapCompatHttpTests(unittest.TestCase):
self.assertEqual(status, 400, resp)
self.assertIn('import probe', resp['error'])
def test_cap_compat_reports_unmatched_probe_lines_with_versions(self):
# v3.6.52: probe overrides that match no Import entry come back as a
# per-AID list ({aid, version}) for the modal's detail block.
from pysim_simple_server import server as srv
self.server.scc = object()
patches = [
mock.patch.object(srv, '_ram_detect_format', lambda *a, **k: 'compact'),
mock.patch.object(srv, '_ram_send_gp_apdu', lambda *a, **k: True),
]
for p in patches:
p.start()
try:
status, resp = self._post('/api/cap-compat', {
'cap_hex': _mini_cap_hex(),
'probe_imports': {'A0000000620101': '0.0', 'dead beef': '1.2'},
})
finally:
for p in reversed(patches):
p.stop()
self.assertEqual(status, 200, resp)
self.assertTrue(resp['success'], resp)
self.assertEqual(resp['probe_unmatched'], [{'aid': 'DEADBEEF', 'version': '1.2'}])
self.assertEqual(resp['probe_imports'],
[{'aid': 'A0000000620101', 'from': '1.0', 'to': '0.0'}])
class RamInstallNvFootprintHttpTests(unittest.TestCase):
"""The RAM install response carries the measured NV footprint (v3.6.51):