feat: CAP platform requirement + LOAD rejection hint (v3.6.36)

capmem derives the minimum platform from the CAP's Import component
(javacard.framework -> Java Card level via the SDK-kit corpus, int flag)
and /api/cap-info exposes it; the CAP details show it as a Platform row
('Java Card >= 2.2.2 - CAP format 2.1 - int support').

A failed RAM install at a LOAD / INSTALL [for load] step now appends a
hint naming the import requirement and the smaller-block fallback
(ramInstallFailHint, pure): JC VM spec 4.5.2 makes every import a
'>= version' demand on the card's own packages checked while the load
file is verified - live 2026-09-29, a CAP requiring Java Card 2.2.2 was
rejected exactly at the block containing the end of Import (6438 at
240 B blocks, 6985 at 64 B), not at a step count.

sw cache simple-v309; tests: 507 Python / 670 Node.
This commit is contained in:
2026-09-29 00:59:17 +03:00
parent bd5dc69c4e
commit f007d5c9be
10 changed files with 169 additions and 16 deletions
+43 -8
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.35';
const SIMPLE_VERSION = '3.6.36';
document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION;
// ===== Tab switching =====
@@ -8472,11 +8472,21 @@ function capMemHtml(mem) {
return esc(a) + (n ? ' <span class="text-gray-400">(' + esc(n) + ')</span>' : '');
}).join('<br>'), 'pl-4 font-mono break-all'));
}
// ── minimum platform: the card must provide every import version (JC VM
// spec 4.5.2 - same major, minor >= the export version) and the JCRE
// checks that while the load file is verified, so the framework import
// names the minimum Java Card level; the header int flag needs int
// support (Java Card 2.2+). capmem computes the level
// (requires_java_card); the local table is the fallback for an older
// server response.
const fw = imports.find(im => (im.aid || '').toUpperCase() === 'A0000000620101');
const sdk = fw ? JC_FRAMEWORK_SDK[(Number(fw.major) || 0) + '.' + (Number(fw.minor) || 0)] : '';
let against = sdk ? 'Java Card ' + sdk : '';
const sdk = mem.requires_java_card ||
(fw ? JC_FRAMEWORK_SDK[(Number(fw.major) || 0) + '.' + (Number(fw.minor) || 0)] : '');
let against = sdk ? 'Java Card \u2265 ' + sdk
: (fw ? 'javacard.framework \u2265 ' + (Number(fw.major) || 0) + '.' + (Number(fw.minor) || 0) : '');
if (mem.cap_version) against += (against ? ' \u00b7 ' : '') + t('CAP format') + ' ' + mem.cap_version;
if (against) pkgRows += row(label(esc(t('Compiled'))) + cell(esc(against), 'pl-4'));
if (mem.needs_int) against += (against ? ' \u00b7 ' : '') + t('int support');
if (against) pkgRows += row(label(esc(t('Platform'))) + cell(esc(against), 'pl-4'));
if (pkgRows) html += section(t('Package')) + table(pkgRows);
// ── the libraries the CAP is linked against (JC VM spec 6.6), shown as
@@ -9375,6 +9385,25 @@ function ramStepLine(s, idx) {
return line;
}
// Hint after a failed RAM install (pure). A rejected LOAD / INSTALL [for
// load] step has two known causes: the card's platform is older than the
// CAP requires (the JCRE checks every import while it verifies the load file,
// JC VM spec 4.5.2, and refuses the step as soon as the Import component is
// complete - observed remote SW 6438/6985 exactly at that block boundary),
// or the LOAD block exceeds the card's buffer. `mem` is the CAP analysis
// (capmem memory_json) when the install came from a .cap.
function ramInstallFailHint(data, mem) {
if (!data || data.success) 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 '';
let 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;
}
async function ramInstallCap(sp) {
const fileInput = document.getElementById('ram-cap-file');
const file = fileInput.files[0];
@@ -9426,7 +9455,7 @@ async function ramInstallCap(sp) {
if (data.load_block_size_auto) sizeInfo += ' (' + t('default') + ')';
}
ramShowInstallResult(data, t('Install OK') + ' — load_file_aid=' + data.load_file_aid
+ ' module_aid=' + data.module_aid + sizeInfo);
+ ' module_aid=' + data.module_aid + sizeInfo, _capAnalysis.ram.memory);
}
// INSTALL [for install] / [for make selectable] for an already loaded
@@ -9474,7 +9503,7 @@ async function ramInstallApp(sp, op) {
// Shared result rendering for /api/ram-install and /api/ram-install-app: the
// per-step lines, the counter the card consumed, and the verdict.
function ramShowInstallResult(data, okText) {
function ramShowInstallResult(data, okText, capMem) {
const resultEl = document.getElementById('ram-result');
const stepsEl = document.getElementById('ram-steps');
stepsEl.classList.remove('hidden');
@@ -9489,7 +9518,9 @@ function ramShowInstallResult(data, okText) {
resultEl.textContent = okText || t('OK');
resultEl.classList.remove('hidden', 'text-red-600'); resultEl.classList.add('text-green-600');
} else {
resultEl.textContent = t('Install FAILED at step:') + ' ' + data.failed_step + (data.error ? ' (' + data.error + ')' : '');
const hint = ramInstallFailHint(data, capMem || null);
resultEl.textContent = t('Install FAILED at step:') + ' ' + data.failed_step + (data.error ? ' (' + data.error + ')' : '') +
(hint ? ' \u2014 ' + t('Hint:') + ' ' + hint : '');
resultEl.classList.remove('hidden', 'text-green-600'); resultEl.classList.add('text-red-600');
}
ramOpModalFinish(resultEl.textContent, !!data.success, txt);
@@ -17116,7 +17147,8 @@ const LANG_RU = {
'Version': 'Версия',
'Type': 'Тип',
'Applets': 'Апплеты',
'Compiled': 'Собрано',
'Platform': 'Платформа',
'int support': 'поддержка int',
'Requires': 'Требуются',
'Family': 'Семейство',
'AID': 'AID',
@@ -17143,6 +17175,9 @@ const LANG_RU = {
'Step': 'Шаг',
'Install OK': 'Установка OK',
'Install FAILED at step:': 'Установка не удалась на шаге:',
'Hint:': 'Подсказка:',
'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': 'Профиль из снимка',
'Profile verification results for:': 'Результаты проверки профиля:',
'Snapshot comparison results:': 'Результаты сравнения снимков:',