fix: late-PoR handler on every path, one poll per packet, LOAD-step components (v3.6.37)

_send_envelope defined its SEND SHORT MESSAGE capture handler inside the
91xx branch, but the 'STATUS poll (PoR not captured)' path used it: a
packet whose ENVELOPE answered 9000 and whose late PoR arrived on the
STATUS poll raised UnboundLocalError, returned 500 and aborted the RAM
operation mid-packet with the card's command left pending (live
2026-09-29, 120-byte LOAD blocks).  The handler is now defined for every
path, and the late-PoR poll runs only after a packet's last segment
(poll_status from _send_secured_packet) - a concatenated download makes
no spurious mid-packet STATUS exchanges.

The PWA annotates every LOAD step with the CAP components its block
covers (ramStepComponents: 'completes Import' / 'inside Method' /
'Header...Import') and ramInstallFailHint names the component the failing
block completed - the live import-verification rejection lands in the
block completing Import (6438/6985 at both 240 B and 64 B blocks).

sw cache simple-v310; tests: 509 Python / 672 Node.
This commit is contained in:
2026-09-29 01:14:37 +03:00
parent f007d5c9be
commit ddf5cbb8e7
8 changed files with 194 additions and 32 deletions
+1 -1
View File
@@ -321,7 +321,7 @@
<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">Описание</th></tr></thead>
<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 (расширенные ресурсы карты: число установленных приложений и свободная энергонезависимая/энергозависимая память). Если карта отвергает компактный формат листинга (<code class="font-mono text-sm">P2.b2=0</code>, устаревший по GP), запросы повторяются в расширенном формате <code class="font-mono text-sm">E3</code>; флажок <strong>Запрашивать расширенные данные реестра (E3)</strong> дополнительно запрашивает их и объединяет расширенные поля (привилегии, связи ELF/SD, списки модулей). Результаты отображаются в обзоре с кнопками <strong>Удалить</strong> для каждого элемента и определённым форматом команд RAM.</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+): JCRE проверяет каждый импорт по своим пакетам при верификации load-файла (JC VM spec 4.5.2), поэтому карта со старой платформой отклоняет LOAD сразу после завершения компонента <strong>Import</strong> (в строке шага виден remote SW карты) — меньший размер блока LOAD помогает только если блок превышает буфер карты. Повреждённый файл не проходит анализ, и <strong>Execute</strong> остаётся недоступной до успешного анализа. Оценка носит справочный характер и никогда не используется при установке: NVRAM — это образ кода плюс постоянные данные (сумма в стиле C6+C8 по GP Table 11-48; управление памятью карты, округление и запись в реестре не учитываются, а образ статических полей входит в обе части); рекомендуемый <strong>C6</strong> следует за load-файлом, а рекомендуемые квоты — это десятичные значения в байтах (шестнадцатеричное значение в скобках — это кодирование параметров установки C7/C8: C6 — энергонезависимый код, C7 — энергозависимая RAM, C8 — энергонезависимые данные).</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+): 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>
</tbody>
</table>
+1 -1
View File
@@ -320,7 +320,7 @@
<thead><tr class="border-b border-gray-300 dark:border-slate-700"><th class="text-left py-1 px-2">Operation</th><th class="text-left py-1 px-2">Description</th></tr></thead>
<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). If a card rejects the compact listing (<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; the <strong>Query expanded registry details (E3)</strong> checkbox also requests it as an extra pass and merges the expanded-only fields (privileges, ELF/SD associations, module lists). Results appear in an explorer view with per-item <strong>Delete</strong> buttons and the detected RAM command format.</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+): 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) &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><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+): 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>
</tbody>
</table>
+48 -5
View File
@@ -1732,7 +1732,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.36';
const SIMPLE_VERSION = '3.6.37';
document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION;
// ===== Tab switching =====
@@ -9358,12 +9358,42 @@ async function ramDeleteFromExplorer(aid, withCascade) {
// consumed was saved above.
}
// Which CAP components a LOAD step's payload covers (pure, diagnostic). The
// server sends the component list in load-file order with sizes that include
// each component's 3-byte header (capmem), so the ranges accumulate directly.
// `completed` lists the components whose last byte falls inside the block -
// the card has just seen them whole, which is where a verifier rejection
// lands (a CAP whose Import list the card cannot satisfy is refused exactly
// in the block completing Import).
function ramStepComponents(name, components, blockSize) {
const m = /^LOAD \((\d+)\/(\d+)\)$/.exec(String(name || '').trim());
const comps = components || [];
const size = Number(blockSize) || 0;
if (!m || !comps.length || size <= 0) return null;
const start = (Number(m[1]) - 1) * size;
const end = Number(m[1]) * size;
let off = 0;
const names = [];
const completed = [];
for (const c of comps) {
const cstart = off, cend = off + (Number(c.size) || 0);
off = cend;
if (cend <= start) continue; // entirely before this block
if (cstart >= end) break; // entirely after it
names.push(c.name);
if (cend > start && cend <= end) completed.push(c.name);
}
if (!names.length) return null;
return { names: names, completed: completed };
}
// One RAM install step line: the PoR verdict plus the remote command's
// status word (9000/61xx/62xx/63xx/CAFE are success; anything else is the
// card's error) - the details that used to be lost behind a bare
// "por_error_" label. The envelope/GET RESPONSE SW is only shown when it is
// not a plain 9000.
function ramStepLine(s, idx) {
// not a plain 9000. With the CAP analysis at hand the line also names the
// component the block completed (see ramStepComponents).
function ramStepLine(s, idx, comps, blockSize) {
const ok = !s.por_error && (s.por_status === 'por_ok' || s.por_status === 'no_por');
let line = (ok ? '\u2705' : '\u274c') + ' ' + t('Step') + ' ' + (idx + 1) + ': ' +
s.name + ' \u2014 ';
@@ -9382,6 +9412,12 @@ function ramStepLine(s, idx) {
if (s.bytes && s.segments) {
line += ' \u00b7 ' + s.bytes + ' ' + t('bytes') + ' / ' + s.segments + ' SMS';
}
const sc = ramStepComponents(s.name, comps, blockSize);
if (sc) {
if (sc.completed.length) line += ' \u00b7 ' + t('completes') + ' ' + sc.completed[sc.completed.length - 1];
else if (sc.names.length === 1) line += ' \u00b7 ' + t('inside') + ' ' + sc.names[0];
else line += ' \u00b7 ' + sc.names[0] + '\u2026' + sc.names[sc.names.length - 1];
}
return line;
}
@@ -9398,7 +9434,10 @@ function ramInstallFailHint(data, mem) {
const step = steps[(Number(data.failed_step) || 1) - 1] || null;
const name = step ? String(step.name || '') : '';
if (!/^(LOAD |INSTALL \[for load\])/.test(name)) return '';
let hint = t('a LOAD rejected here means the card refused the CAP: every import version under Requires must be present on the card');
let hint = '';
const sc = ramStepComponents(name, mem && mem.components, data.load_block_size);
if (sc && sc.completed.length) hint += t('the failed step completes') + ' ' + sc.completed[sc.completed.length - 1] + '. ';
hint += t('a LOAD rejected here means the card refused the CAP: every import version under Requires must be present on the card');
if (mem && mem.requires_java_card) hint += ' (Java Card \u2265 ' + mem.requires_java_card + ')';
hint += t('. If the CAP needs only versions this card has, retry with a smaller LOAD block (e.g. 64 B)');
return hint;
@@ -9507,8 +9546,9 @@ function ramShowInstallResult(data, okText, capMem) {
const resultEl = document.getElementById('ram-result');
const stepsEl = document.getElementById('ram-steps');
stepsEl.classList.remove('hidden');
const comps = capMem && capMem.components;
let txt = '';
(data.steps || []).forEach((s, idx) => { txt += ramStepLine(s, idx) + '\n'; });
(data.steps || []).forEach((s, idx) => { txt += ramStepLine(s, idx, comps, data.load_block_size) + '\n'; });
stepsEl.textContent = txt;
// Persist the counter the card actually consumed (accepted packets only):
// even a failed install leaves the accepted steps behind, and replaying
@@ -17176,6 +17216,9 @@ const LANG_RU = {
'Install OK': 'Установка OK',
'Install FAILED at step:': 'Установка не удалась на шаге:',
'Hint:': 'Подсказка:',
'completes': 'завершает',
'inside': 'внутри',
'the failed step completes': 'неудавшийся шаг завершает',
'a LOAD rejected here means the card refused the CAP: every import version under Requires must be present on the card': 'отклонение LOAD здесь означает, что карта отклонила CAP: все версии импортов из «Требуются» должны присутствовать на карте',
'. If the CAP needs only versions this card has, retry with a smaller LOAD block (e.g. 64 B)': '. Если CAP требует только доступные этой карте версии, повторите с меньшим размером блока LOAD (например, 64 Б)',
'Profile from snapshot': 'Профиль из снимка',
+1 -1
View File
@@ -1,4 +1,4 @@
const CACHE = 'simple-v309';
const CACHE = 'simple-v310';
const URLS = [
'index.html',
'help.html',
+49 -1
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', 'ramFmtPrivileges', 'ramRenderExploreHtml', 'ramStepLine', 'ramInstallFailHint', 'ramGetStatusApdu', 'ramDeleteApdu',
const FNS = ['berLenStr', 'buildApdu', 'escHtml', 'esc', 'chainInit', 'chainRamBuildRowHex', 'ramFmtLifecycle', 'ramFmtPrivileges', 'ramRenderExploreHtml', 'ramStepLine', 'ramStepComponents', 'ramInstallFailHint', 'ramGetStatusApdu', 'ramDeleteApdu',
'stkParamsBuild', 'ramRemoteSwOk', 'spPorAccepted', 'ramIncrementCntr', 'ramDeleteFromExplorer', 'ramListingSpi2', 'ramRemoveFromExplorer', 'ramHasInstance', 'ramExpandedQueryApdu',
'_parseRawElfEntry', '_parseRawAppEntry', 'ramParseElfStatus', 'ramParseAppStatus', 'parseTLV', '_parseE3Entry',
'ramCardIdxAfterRemove', 'ramClearResults', 'ramHideProgress', 'ramOpChanged', 'ramRender', 'ramApplyCard', 'ramExecute', 'decodePrivileges', 'ramActionBtn', 'ramCapToolkitMode', 'ramOpProgressText',
@@ -390,6 +390,46 @@ test('ramStepLine shows the PoR verdict and the remote status word', () => {
assert.ok(noPor.startsWith('\u2705') && noPor.includes('no PoR'), noPor);
});
test('ramStepComponents maps a LOAD step to the CAP components it covers', () => {
// RemMobileID-like layout: sizes include each component's 3-byte header
const comps = [
{ name: 'Header', size: 20 }, { name: 'Directory', size: 34 },
{ name: 'Applet', size: 15 }, { name: 'Import', size: 101 },
{ name: 'ConstantPool', size: 1121 }, { name: 'Method', size: 8506 },
];
// 240 B block 1 (0..239) completes the Import component (its end is 170)
let sc = ramStepComponents('LOAD (1/64)', comps, 240);
assert.deepStrictEqual(sc.completed, ['Header', 'Directory', 'Applet', 'Import']);
assert.deepStrictEqual(sc.names.slice(0, 2), ['Header', 'Directory']);
// 64 B blocks: 1 completes Header/Directory, 3 completes Import
assert.deepStrictEqual(ramStepComponents('LOAD (1/240)', comps, 64).completed, ['Header', 'Directory']);
sc = ramStepComponents('LOAD (2/240)', comps, 64);
assert.deepStrictEqual(sc.names, ['Applet', 'Import']);
assert.deepStrictEqual(sc.completed, ['Applet']);
sc = ramStepComponents('LOAD (3/240)', comps, 64);
assert.deepStrictEqual(sc.names, ['Import', 'ConstantPool']);
assert.deepStrictEqual(sc.completed, ['Import']);
// other step names and missing data produce nothing
assert.strictEqual(ramStepComponents('INSTALL [for load]', comps, 64), null);
assert.strictEqual(ramStepComponents('LOAD (1/64)', comps, 0), null);
assert.strictEqual(ramStepComponents('LOAD (1/64)', null, 64), null);
});
test('ramStepLine annotates a LOAD step with the completing component', () => {
globalThis.t = s => s;
globalThis.lookupSw = () => '';
const comps = [{ name: 'Header', size: 20 }, { name: 'Import', size: 101 },
{ name: 'Method', size: 8506 }];
const line = ramStepLine({ name: 'LOAD (1/64)', por_status: 'por_ok', por_sw: '9000' }, 0, comps, 240);
assert.ok(line.includes('completes Import'), line);
// block 2 of 40 B blocks (40..79) lies strictly inside Import
const inside = ramStepLine({ name: 'LOAD (2/64)', por_status: 'por_ok', por_sw: '9000' }, 1, comps, 40);
assert.ok(inside.includes('inside Import'), inside);
// without a component list the line is unchanged
const plain = ramStepLine({ name: 'LOAD (1/64)', por_status: 'por_ok', por_sw: '9000' }, 0);
assert.ok(!plain.includes('completes') && !plain.includes('inside'), plain);
});
test('ramInstallFailHint names the CAP import requirement for a rejected LOAD', () => {
globalThis.t = s => s;
const failedLoad = {
@@ -408,6 +448,14 @@ test('ramInstallFailHint names the CAP import requirement for a rejected LOAD',
assert.strictEqual(ramInstallFailHint(
{ success: false, failed_step: 2, steps: [{ name: 'INSTALL [for load]' }, { name: 'INSTALL [for install]' }] }, null), '');
assert.strictEqual(ramInstallFailHint({ success: true, failed_step: 1, steps: [] }, null), '');
// with the CAP analysis the completing component of the failing block is
// named (the card check lands on the component boundary)
const comps = [{ name: 'Header', size: 20 }, { name: 'Import', size: 101 }];
const withBoundary = ramInstallFailHint(
{ success: false, failed_step: 2, load_block_size: 64,
steps: [{ name: 'INSTALL [for load]' }, { name: 'LOAD (2/240)', por_sw: '6438' }] },
{ requires_java_card: '2.2.2', components: comps });
assert.ok(withBoundary.startsWith('the failed step completes Import. '), withBoundary);
});
test('ramOpChanged clears the executed status only on a real op change', () => {