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
+22 -2
View File
@@ -119,11 +119,12 @@ test('capMemHtml renders the grouped report: package, requires, memory, componen
assert.ok(req.includes('>\u2265 1.0</td>'), req);
assert.ok(req.includes('>Oracle JavaCard API</td>'), req);
assert.ok(req.includes('>6</td>'), req);
// package identity + compiled-against hint derived from the framework version
// package identity + platform requirement derived from the framework version
assert.ok(out.includes('A0000000620101') && out.includes('>v1.0</td>'), out);
assert.ok(out.includes('applet package'), out);
assert.ok(out.includes('(javacard.framework)</span>'), out);
assert.ok(out.includes('Java Card 2.1.1/2.1.2 \u00b7 CAP format 2.1'), out);
assert.ok(out.includes('>Platform</td>'), out);
assert.ok(out.includes('Java Card \u2265 2.1.1/2.1.2 \u00b7 CAP format 2.1'), out);
// components in load-file order with sizes, shares and the total row
assert.ok(out.includes('>Header</td>') && out.includes('>20 B</td>') && out.includes('>1%</td>'), out);
assert.ok(out.includes('>Method</td>') && out.includes('>2.7 kB</td>') && out.includes('>93%</td>'), out);
@@ -134,6 +135,25 @@ test('capMemHtml renders the grouped report: package, requires, memory, componen
assert.strictEqual(capMemHtml(null), '');
});
test('capMemHtml shows the server platform requirement and int support', () => {
const mem = memFixture();
mem.requires_java_card = '2.2.2';
mem.requires_framework = '1.3';
mem.needs_int = true;
const out = capMemHtml(mem);
assert.ok(out.includes('Java Card \u2265 2.2.2 \u00b7 CAP format 2.1 \u00b7 int support'), out);
// the server value wins over the local fallback table (fixture import 1.0)
assert.ok(!out.includes('2.1.1/2.1.2'), out);
});
test('capMemHtml falls back to the raw framework version outside the corpus', () => {
const mem = memFixture();
mem.imports = [{ aid: 'A0000000620101', minor: 0, major: 7, refs: 6 }];
const out = capMemHtml(mem);
assert.ok(out.includes('javacard.framework \u2265 7.0 \u00b7 CAP format 2.1'), out);
assert.ok(!out.includes('Java Card'), out);
});
test('capMemHtml skips the import section on a response without imports', () => {
const mem = memFixture();
delete mem.imports;
+21 -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', 'ramGetStatusApdu', 'ramDeleteApdu',
const FNS = ['berLenStr', 'buildApdu', 'escHtml', 'esc', 'chainInit', 'chainRamBuildRowHex', 'ramFmtLifecycle', 'ramFmtPrivileges', 'ramRenderExploreHtml', 'ramStepLine', '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,26 @@ test('ramStepLine shows the PoR verdict and the remote status word', () => {
assert.ok(noPor.startsWith('\u2705') && noPor.includes('no PoR'), noPor);
});
test('ramInstallFailHint names the CAP import requirement for a rejected LOAD', () => {
globalThis.t = s => s;
const failedLoad = {
success: false, failed_step: 3,
steps: [{ name: 'INSTALL [for load]' }, { name: 'LOAD (1/240)' }, { name: 'LOAD (2/240)', por_sw: '6985' }],
};
const hint = ramInstallFailHint(failedLoad, { requires_java_card: '2.2.2' });
assert.ok(hint.includes('every import version under Requires'), hint);
assert.ok(hint.includes('(Java Card \u2265 2.2.2)'), hint);
assert.ok(hint.includes('smaller LOAD block'), hint);
// without a CAP analysis (INSTALL [for install] ops) the level is omitted
const noMem = ramInstallFailHint(failedLoad, null);
assert.ok(noMem.includes('every import version under Requires'), noMem);
assert.ok(!noMem.includes('Java Card'), noMem);
// only a load-related step gets the hint
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), '');
});
test('ramOpChanged clears the executed status only on a real op change', () => {
const els = fakeRamDocument(['ram-op', 'ram-install-params', 'ram-result', 'ram-explorer', 'ram-steps', 'ram-progress']);
_ramOpLast = null;