From f007d5c9be2f8173be0bd69bec2b9cbf44a88215 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BD=D1=82=D0=BE=D0=BD=20=D0=A2=D1=80=D0=BE=D1=88?= =?UTF-8?q?=D0=B8=D0=BD?= Date: Tue, 29 Sep 2026 00:59:17 +0300 Subject: [PATCH] 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. --- frontend/help-ru.html | 2 +- frontend/help.html | 2 +- frontend/index.html | 51 +++++++++++++++++++++++++++++------ frontend/sw.js | 2 +- frontend/tests/capmem.test.js | 24 +++++++++++++++-- frontend/tests/ram.test.js | 22 ++++++++++++++- pyproject.toml | 2 +- pysim_simple_server/capmem.py | 44 ++++++++++++++++++++++++++++++ pysim_simple_server/server.py | 2 +- tests/test_cap_memory.py | 34 +++++++++++++++++++++++ 10 files changed, 169 insertions(+), 16 deletions(-) diff --git a/frontend/help-ru.html b/frontend/help-ru.html index a2de934..02f6fba 100644 --- a/frontend/help-ru.html +++ b/frontend/help-ru.html @@ -321,7 +321,7 @@ ОперацияОписание Обзор карты (все данные GP)Запрос GET STATUS для ISD, приложений, ELF и модулей ELF, а также GET DATA FF21 (расширенные ресурсы карты: число установленных приложений и свободная энергонезависимая/энергозависимая память). Если карта отвергает компактный формат листинга (P2.b2=0, устаревший по GP), запросы повторяются в расширенном формате E3; флажок Запрашивать расширенные данные реестра (E3) дополнительно запрашивает их и объединяет расширенные поля (привилегии, связи ELF/SD, списки модулей). Результаты отображаются в обзоре с кнопками Удалить для каждого элемента и определённым форматом команд RAM. - Установка пакета (.cap файл)Отправка .cap файла на карту через сервер: INSTALL[for load] → LOAD ×N → INSTALL[for install (+make selectable)]. Load-файл делится на LOAD APDU размера размер блока LOAD (1–240 байт полезной нагрузки, по умолчанию 240); каждый защищённый пакет доставляется одним SMS или, если он больше, конкатенированной SMS-PP загрузкой до 5 сегментов, так что большой .cap просто занимает несколько SMS. Сразу после выбора файл проверяется и в форме показывается оценка требований к памяти: требование NVRAM (образ кода + постоянные данные, сумма в стиле C6+C8 из GP Card Spec) и оценка RAM. Там же показаны библиотеки, с которыми слинкован CAP (его компонент Import: имя библиотеки, AID и минимальная версия ≥ x.y — имена стандартных библиотек и семейства подставляются, а где известно, даётся подсказка о версии Java Card), идентификаторы пакета и апплетов и разбивка по компонентам (размеры и доли в порядке load-файла) — всё сгруппировано по разделам Package, Requires, Memory и Components. Повреждённый файл не проходит анализ, и Execute остаётся недоступной до успешного анализа. Оценка носит справочный характер и никогда не используется при установке: NVRAM — это образ кода плюс постоянные данные (сумма в стиле C6+C8 по GP Table 11-48; управление памятью карты, округление и запись в реестре не учитываются, а образ статических полей входит в обе части); рекомендуемый C6 следует за load-файлом, а рекомендуемые квоты — это десятичные значения в байтах (шестнадцатеричное значение в скобках — это кодирование параметров установки C7/C8: C6 — энергонезависимый код, C7 — энергозависимая RAM, C8 — энергонезависимые данные). + Установка пакета (.cap файл)Отправка .cap файла на карту через сервер: INSTALL[for load] → LOAD ×N → INSTALL[for install (+make selectable)]. Load-файл делится на LOAD APDU размера размер блока LOAD (1–240 байт полезной нагрузки, по умолчанию 240); каждый защищённый пакет доставляется одним SMS или, если он больше, конкатенированной SMS-PP загрузкой до 5 сегментов, так что большой .cap просто занимает несколько SMS. Сразу после выбора файл проверяется и в форме показывается оценка требований к памяти: требование NVRAM (образ кода + постоянные данные, сумма в стиле C6+C8 из GP Card Spec) и оценка RAM. Там же показаны библиотеки, с которыми слинкован CAP (его компонент Import: имя библиотеки, AID и минимальная версия ≥ x.y — имена стандартных библиотек и семейства подставляются, а где известно, даётся подсказка о версии Java Card), идентификаторы пакета и апплетов и разбивка по компонентам (размеры и доли в порядке load-файла) — всё сгруппировано по разделам Package, Requires, Memory и Components. Строка Platform показывает минимальный уровень Java Card, который должна предоставить карта (выводится из импорта javacard.framework; флаг int в заголовке дополнительно требует поддержки int — Java Card 2.2+): JCRE проверяет каждый импорт по своим пакетам при верификации load-файла (JC VM spec 4.5.2), поэтому карта со старой платформой отклоняет LOAD сразу после завершения компонента Import (в строке шага виден remote SW карты) — меньший размер блока LOAD помогает только если блок превышает буфер карты. Повреждённый файл не проходит анализ, и Execute остаётся недоступной до успешного анализа. Оценка носит справочный характер и никогда не используется при установке: NVRAM — это образ кода плюс постоянные данные (сумма в стиле C6+C8 по GP Table 11-48; управление памятью карты, округление и запись в реестре не учитываются, а образ статических полей входит в обе части); рекомендуемый C6 следует за load-файлом, а рекомендуемые квоты — это десятичные значения в байтах (шестнадцатеричное значение в скобках — это кодирование параметров установки C7/C8: C6 — энергонезависимый код, C7 — энергозависимая RAM, C8 — энергонезависимые данные). diff --git a/frontend/help.html b/frontend/help.html index 53c4d6c..03004b1 100644 --- a/frontend/help.html +++ b/frontend/help.html @@ -320,7 +320,7 @@ OperationDescription Explore Card (all GP data)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 (P2.b2=0, deprecated by GP) the queries are retried in the expanded E3 form; the Query expanded registry details (E3) 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 Delete buttons and the detected RAM command format. - Install Package (.cap file)Sends a .cap file to the card via the server: INSTALL[for load] → LOAD ×N → INSTALL[for install (+make selectable)]. The load file is split into LOAD APDUs of the LOAD block size (1–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 .cap simply takes several SMS. As soon as the file is selected it is validated and its memory requirements are estimated in the form: the NVRAM requirement (the code image + persistent data, the GlobalPlatform Card Spec C6+C8 style total) and the RAM estimate. The same box reports the libraries the CAP is linked against (its Import component: library name, AID and the minimum version ≥ x.y, 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) — grouped under the Package, Requires, Memory and Components headings. A corrupt or wrong-format file fails the analysis, and Execute 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 — card memory management, rounding and the registry entry are not included, and the static field image is counted in both parts); the suggested C6 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). + Install Package (.cap file)Sends a .cap file to the card via the server: INSTALL[for load] → LOAD ×N → INSTALL[for install (+make selectable)]. The load file is split into LOAD APDUs of the LOAD block size (1–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 .cap simply takes several SMS. As soon as the file is selected it is validated and its memory requirements are estimated in the form: the NVRAM requirement (the code image + persistent data, the GlobalPlatform Card Spec C6+C8 style total) and the RAM estimate. The same box reports the libraries the CAP is linked against (its Import component: library name, AID and the minimum version ≥ x.y, 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) — grouped under the Package, Requires, Memory and Components headings. The Platform line names the minimum Java Card level the card must provide (derived from the javacard.framework import; the header int 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 Import component is complete (the step line carries the card's remote SW) — a smaller LOAD block only helps when the block exceeds the card's buffer. A corrupt or wrong-format file fails the analysis, and Execute 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 — card memory management, rounding and the registry entry are not included, and the static field image is counted in both parts); the suggested C6 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). diff --git a/frontend/index.html b/frontend/index.html index 1f37acd..905e212 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -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 ? ' (' + esc(n) + ')' : ''); }).join('
'), '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:': 'Результаты сравнения снимков:', diff --git a/frontend/sw.js b/frontend/sw.js index b64a2bb..0176db5 100644 --- a/frontend/sw.js +++ b/frontend/sw.js @@ -1,4 +1,4 @@ -const CACHE = 'simple-v308'; +const CACHE = 'simple-v309'; const URLS = [ 'index.html', 'help.html', diff --git a/frontend/tests/capmem.test.js b/frontend/tests/capmem.test.js index babe7dc..1d27868 100644 --- a/frontend/tests/capmem.test.js +++ b/frontend/tests/capmem.test.js @@ -119,11 +119,12 @@ test('capMemHtml renders the grouped report: package, requires, memory, componen assert.ok(req.includes('>\u2265 1.0'), req); assert.ok(req.includes('>Oracle JavaCard API'), req); assert.ok(req.includes('>6'), 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'), out); assert.ok(out.includes('applet package'), out); assert.ok(out.includes('(javacard.framework)'), out); - assert.ok(out.includes('Java Card 2.1.1/2.1.2 \u00b7 CAP format 2.1'), out); + assert.ok(out.includes('>Platform'), 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') && out.includes('>20 B') && out.includes('>1%'), out); assert.ok(out.includes('>Method') && out.includes('>2.7 kB') && out.includes('>93%'), 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; diff --git a/frontend/tests/ram.test.js b/frontend/tests/ram.test.js index 26e0cad..947423f 100644 --- a/frontend/tests/ram.test.js +++ b/frontend/tests/ram.test.js @@ -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; diff --git a/pyproject.toml b/pyproject.toml index 9fa8b7b..b95099a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "pysim-simple-server" -version = "3.6.35" +version = "3.6.36" 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. diff --git a/pysim_simple_server/capmem.py b/pysim_simple_server/capmem.py index 0cf1f36..84d2538 100755 --- a/pysim_simple_server/capmem.py +++ b/pysim_simple_server/capmem.py @@ -1444,6 +1444,21 @@ def _external_ref_counts(cap): return counts +# javacard.framework export version -> Java Card release family whose kit +# ships exactly that version. Derived from the local Oracle SDK kit corpus +# (jc211_kit .. jc305u4_kit exports, 2026-09-27; same table as the PWA's +# JC_FRAMEWORK_SDK). A card provides the framework export of its platform, +# and JC VM spec 4.5.2 makes the CAP's import version a ">=" demand on it +# (same major, minor >= the export version), checked by the JCRE while the +# load file is verified - so the framework import names the minimum Java Card +# level a CAP can load on. +JC_FRAMEWORK_AID = 'A0000000620101' +JC_FRAMEWORK_JDK = { + '1.0': '2.1.1/2.1.2', '1.2': '2.2.1', '1.3': '2.2.2', + '1.4': '3.0.3', '1.5': '3.0.4', '1.6': '3.0.5', +} + + def analyze_bytes(cap_bytes, verbose=False): """Analyze a CAP archive (bytes) and return (report, memory).""" cap = CAP(cap_bytes) @@ -1472,6 +1487,21 @@ def analyze_bytes(cap_bytes, verbose=False): h = cap.components['header'] report['flags'] = h.flags report['package_name'] = h.package_name + # flags bit 0 ('int', set by the JC 2.1.2 converter's -i): the loader + # must support int (Java Card 2.2+) + report['needs_int'] = bool(h.flags & 0x01) + + # Minimum platform (JC VM spec 4.5.2): the card must export every + # imported package at >= the version recorded in the CAP, and the JCRE + # performs that check while the load file is verified. The framework + # import names the minimum Java Card level (a card with an older platform + # rejects the LOAD as soon as the Import component is complete). + if cap.has('import'): + fw = next((p for p in cap.components['import'].packages + if p.aid_hex == JC_FRAMEWORK_AID), None) + if fw: + report['requires_framework'] = f'{fw.major}.{fw.minor}' + report['requires_java_card'] = JC_FRAMEWORK_JDK.get(report['requires_framework']) # Component sizes (the load file order) for the breakdown display report['components'] = [{'name': name, 'size': size} for name, size in cap.files] @@ -1710,6 +1740,12 @@ def memory_json(report, memory, load_file_bytes=None): 'applet': bool(report.get('flags', 0) & 0x04), }, 'package_name': report.get('package_name'), + # minimum platform the card must provide for this load file (JC VM + # spec 4.5.2, derived from the imports): the Java Card level named by + # the framework import, and the header int flag's loader requirement + 'requires_framework': report.get('requires_framework'), + 'requires_java_card': report.get('requires_java_card'), + 'needs_int': bool(report.get('needs_int')), 'components': report.get('components', []), 'class_count': report.get('class_count', 0), 'method_count': report.get('method_count', 0), @@ -1756,6 +1792,14 @@ def format_report(report, memory): if 'cap_version' in report: lines.append(f'CAP format: {report["cap_version"]}') + if report.get('requires_java_card') or report.get('requires_framework'): + req = 'Requires Java Card >= %s' % (report.get('requires_java_card') + or report.get('requires_framework')) + if report.get('requires_java_card') and report.get('requires_framework'): + req += ' (javacard.framework %s)' % report['requires_framework'] + if report.get('needs_int'): + req += ', int support' + lines.append(req) if 'package_version' in report: lines.append(f'Package version: {report["package_version"]}') if 'package_aid' in report: diff --git a/pysim_simple_server/server.py b/pysim_simple_server/server.py index 673cbf5..6703d45 100644 --- a/pysim_simple_server/server.py +++ b/pysim_simple_server/server.py @@ -31,7 +31,7 @@ from osmocom.tlv import BER_TLV_IE from osmocom.utils import rpad -VERSION = '3.6.35' +VERSION = '3.6.36' MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE diff --git a/tests/test_cap_memory.py b/tests/test_cap_memory.py index dc4ad59..c58ee8c 100644 --- a/tests/test_cap_memory.py +++ b/tests/test_cap_memory.py @@ -1,6 +1,8 @@ """Tests for the bundled CAP memory analyzer (capmem) and /api/cap-info helper.""" import io +import pathlib +import re import struct import unittest import zipfile @@ -228,6 +230,38 @@ class TestCapAnalyzer(unittest.TestCase): report, _ = capmem.analyze_bytes(build_cap()) self.assertIsNone(report['package_name']) + def test_framework_sdk_table_matches_the_pwa(self): + # capmem.JC_FRAMEWORK_JDK mirrors the PWA's fallback JC_FRAMEWORK_SDK + # (drift guard: the PWA shows the server value when it is present) + html = pathlib.Path(__file__).resolve().parents[1].joinpath( + 'frontend', 'index.html').read_text(encoding='utf-8') + m = re.search(r"const JC_FRAMEWORK_SDK = \{(.*?)\};", html, re.S) + self.assertIsNotNone(m, 'JC_FRAMEWORK_SDK not found in index.html') + pwa = dict(re.findall(r"'([\d.]+)':\s*'([^']+)'", m.group(1))) + self.assertEqual(pwa, capmem.JC_FRAMEWORK_JDK) + + def test_platform_requirement_from_the_framework_import(self): + # java.lang only: no Java Card level named, but the int flag shows + report, _ = capmem.analyze_bytes( + build_cap(imports=[(0, 1, bytes.fromhex('A0000000620001'))], header_flags=0x01)) + info = capmem.memory_json(report, capmem.compute_memory(report)) + self.assertIsNone(info['requires_framework']) + self.assertIsNone(info['requires_java_card']) + self.assertTrue(info['needs_int']) + # javacard.framework 1.3 -> Java Card 2.2.2 (the kit corpus mapping) + report, _ = capmem.analyze_bytes( + build_cap(imports=[(3, 1, JAVACARD_FRAMEWORK)], header_flags=0x05)) + info = capmem.memory_json(report, capmem.compute_memory(report)) + self.assertEqual(info['requires_framework'], '1.3') + self.assertEqual(info['requires_java_card'], '2.2.2') + self.assertTrue(info['needs_int']) + # a version outside the corpus keeps the raw framework version + report, _ = capmem.analyze_bytes( + build_cap(imports=[(0, 2, JAVACARD_FRAMEWORK)])) + info = capmem.memory_json(report, capmem.compute_memory(report)) + self.assertEqual(info['requires_framework'], '2.0') + self.assertIsNone(info['requires_java_card']) + def test_memory_json_nvram_requirement_uses_the_load_file(self): report, memory = capmem.analyze_bytes(rich_cap()) info = capmem.memory_json(report, memory, load_file_bytes=1000)