feat: CAP compatibility test as its own operation + append-only imports (v3.6.48)

The import probe no longer lives in 'Install Package (.cap file)': a
diagnostic that can leave a LOADED-only package does not belong in the
operation meant to produce a working applet.  /api/ram-install no longer
takes probe_imports; the install form's probe fieldset is gone.

New RAM operation CAP compatibility test (POST /api/cap-compat): the
server runs the format check, INSTALL [for load] and the LOAD blocks only
up to the block that completes the Import component (_cap_import_end +
_cap_compat_blocks, computed from the load file and the C4 TLV framing),
then stops - no last-block flag, no INSTALL [for install], so nothing is
committed and nothing needs deleting.  The verdict (imports_ok,
boundary_block/total_blocks) states it covers the LOAD/import gate only;
the link gate stays the real install's business.  Verified on the real
RemMobileID CAP: the boundary is LOAD 1/64, 2/128, 3/240 for 240/120/64 B
blocks - exactly the blocks where the live cards rejected the load.

_probe_import_versions(load_file, overrides, additions) now supports
append-only synthetic imports: +AID=version appends a new entry, the
Import component's size field and the Directory's Import size are patched,
and existing entries keep their positions (constant-pool import indices
stay valid); an AID that is already imported becomes a version override.
This is the card-capability query ('does this card export package X at
>= Y?') without building a probe applet.

PWA: new op section (CAP file + analysis + the version list + block size),
ramCompatTest with inline results and ramCompatVerdict, ramProbeParse
understands +AID=version; help EN/RU documents the operation, the two-step
bisect and the gate scope.

sw cache simple-v321; tests: 538 Python / 686 Node.
This commit is contained in:
2026-09-30 07:57:11 +03:00
parent ef949e6a17
commit 0dc79a3ba4
9 changed files with 490 additions and 121 deletions
+26 -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', 'ramStepComponents', 'ramInstallFailHint', 'ramProbeParse', 'ramExpandedDetailsInit', 'ramExpandedDetailsChanged', 'ramGetStatusApdu', 'ramDeleteApdu',
const FNS = ['berLenStr', 'buildApdu', 'escHtml', 'esc', 'chainInit', 'chainRamBuildRowHex', 'ramFmtLifecycle', 'ramFmtPrivileges', 'ramRenderExploreHtml', 'ramStepLine', 'ramStepComponents', 'ramInstallFailHint', 'ramProbeParse', 'ramCompatVerdict', 'ramExpandedDetailsInit', 'ramExpandedDetailsChanged', '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',
@@ -789,6 +789,31 @@ test('ramProbeParse reads AID=version lines', () => {
assert.ok(ramProbeParse('A0000000620101=x.y').error);
assert.ok(ramProbeParse('').error);
assert.ok(ramProbeParse(' \n# only comments\n').error);
// '+AID=version' appends a synthetic import (a card-capability query)
r = ramProbeParse('+A0000000090005FFFFFFFF8912000000=1.11');
assert.deepStrictEqual(r.map, {});
assert.deepStrictEqual(r.additions,
[{ aid: 'A0000000090005FFFFFFFF8912000000', version: '1.11' }]);
r = ramProbeParse('A0000000620101=0.0\n+0102030405=1.0');
assert.deepStrictEqual(r.map, { 'A0000000620101': '0.0' });
assert.strictEqual(r.additions.length, 1);
assert.ok(ramProbeParse('+all=1.0').error);
});
test('ramCompatVerdict reports the import-gate verdict', () => {
globalThis.t = s => s;
const ok = ramCompatVerdict({ imports_ok: true, boundary_block: 3, total_blocks: 240 });
assert.strictEqual(ok, 'Import list accepted \u2014 tested through LOAD 3/240');
const bad = ramCompatVerdict({ imports_ok: false, boundary_block: 3, total_blocks: 240,
error: 'remote SW 6438' });
assert.ok(bad.startsWith('Import list rejected \u2014 tested through LOAD 3/240'), bad);
assert.ok(bad.includes('remote SW 6438'), bad);
assert.strictEqual(ramCompatVerdict(null), '');
});
test('ramExecute dispatches the compatibility test op', () => {
const src = extractFunc(html, 'ramExecute');
assert.ok(/op === 'compat'/.test(src), 'the compat op must be dispatched');
});
test('ramRenderExploreHtml counts apps and ELFs in the section headings', () => {