diff --git a/docs/api.md b/docs/api.md index 688e69f..79fd1d7 100644 --- a/docs/api.md +++ b/docs/api.md @@ -348,6 +348,11 @@ used for installation. "memory": { "package_aid": "AA1902BC226001", "applet_count": 1, "applets": ["AA1902BC226001"], + "imports": [{"aid": "A0000000620101", "minor": 0, "major": 1, "refs": 6}, + {"aid": "A0000000090005FFFFFFFF8912000000", "minor": 11, "major": 1, "refs": 7}], + "flags": {"raw": 4, "int": false, "export": false, "applet": true}, + "package_name": null, + "components": [{"name": "Header", "size": 20}, {"name": "Method", "size": 433}], "class_count": 3, "method_count": 12, "code": {"method_component": 850, "load_file": 1234}, "nvram": {"static_image": 12, "array_init": 4, "install_objects": 100, @@ -360,6 +365,18 @@ used for installation. } ``` +`imports` are the libraries the CAP is linked against (JC VM spec §6.6, +the Import component), each with the export-file version recorded in the CAP +and the number of distinct constant-pool references to it (§6.7; a linked but +unreferenced package is 0). A card resolves an import only when the resident +package has the **same major** version and a **minor ≥** the recorded one +(§4.5.2), hence the `name >= version` display. `flags` decodes the Header +package flags (Table 6-4: `0x01` uses `int`, `0x02` exports an API, `0x04` +applet package); `package_name` is only present in JC 2.2+ headers (CAP 2.1 +files have none); `components` lists the archive entries in load-file order +(each size includes the component's tag and size header, so the sizes sum to +`load_file_bytes`). + `code.load_file` is the concatenated load file (all components) - the package image the card stores, our proxy for the GlobalPlatform Card Spec v2.3.1 Table 11-48 "non-volatile code" minimum memory requirement; the diff --git a/frontend/help-ru.html b/frontend/help-ru.html index 1961618..40b9541 100644 --- a/frontend/help-ru.html +++ b/frontend/help-ru.html @@ -319,7 +319,7 @@ ОперацияОписание Обзор карты (все данные GP)Запрос GET STATUS для ISD, приложений, ELF и модулей ELF, а также GET DATA FF21 для информации о памяти. Результаты отображаются в обзоре с кнопками Удалить для каждого элемента. - Установка пакета (.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; повреждённый файл не проходит анализ, и Execute остаётся недоступной до успешного анализа. + Установка пакета (.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, в виде имя ≥ версия — имена стандартных библиотек подставляются, а где известно, даётся подсказка о версии Java Card), идентификаторы пакета и апплетов и разбивка по компонентам. Повреждённый файл не проходит анализ, и Execute остаётся недоступной до успешного анализа. @@ -376,7 +376,7 @@

Таблица показывает имя, тип, число APDU и время создания каждого скрипта, а также кнопки Редактировать и Удалить; в редакторе есть поле имени и текстовая область APDU. В ходе сессии сервер отдаёт по одному C-APDU на каждый POST карты и отслеживает выполнение: карта сообщает статус в следующем POST (X-Admin-Script-Status), оборвавшаяся сессия досылает только невыполненные APDU (X-Admin-Resume продолжает, новый диалог начинает заново), а завершённый скрипт закрывается ответом 204 No Content. Конструктор RAM/GP вкладки Remote APDU может отправить цепочку команд прямо в прогон кнопкой «В очередь SCP81».

diff --git a/frontend/help.html b/frontend/help.html index e6ff790..4117bb8 100644 --- a/frontend/help.html +++ b/frontend/help.html @@ -318,7 +318,7 @@ OperationDescription Explore Card (all GP data)Queries GET STATUS for ISD, Applications, ELFs, and ELF Modules, plus GET DATA FF21 for memory info. Results appear in an explorer view with per-item Delete buttons. - 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; a corrupt or wrong-format file fails the analysis, and Execute stays disabled until a successful analysis. + 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 lists the libraries the CAP is linked against (its Import component, shown as name ≥ version with standard library names resolved and a Java Card SDK hint where known) plus the package/applet identity and the component breakdown. A corrupt or wrong-format file fails the analysis, and Execute stays disabled until a successful analysis. @@ -375,7 +375,7 @@

The table lists each script with its kind, APDU count and creation time, plus Edit and Delete; the editor has a name field and the APDU textarea. Over a session the server serves one C-APDU per card POST and tracks execution: the card reports status in its next POST (X-Admin-Script-Status), a session that dies resends only the unexecuted APDUs (X-Admin-Resume continues, a fresh dialog restarts), and a completed script is closed with 204 No Content. The Remote APDU tab's RAM/GP builder can feed a command chain straight into the run with Queue in SCP81.

diff --git a/frontend/index.html b/frontend/index.html index 81870cf..2cbe277 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -1616,7 +1616,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.5.15'; +const SIMPLE_VERSION = '3.5.16'; document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION; // ===== Tab switching ===== @@ -8031,7 +8031,17 @@ function capMemHtml(mem) { const nvramData = Number(nv.total) || 0; const requirement = Number(nv.requirement) || (loadFile + nvramData); const otherComponents = Math.max(0, loadFile - methodBytes); + const imports = mem.imports || []; let html = '
' + esc(t('CAP requirements (estimate)')) + '
'; + // the libraries the CAP is linked against (JC VM spec 6.6), shown as + // ">=" requirements: same major, minor >= the export version (4.5.2) + if (imports.length) { + const req = imports.map(im => { + const name = jcAidName(im.aid) || im.aid; + return name + ' \u2265 ' + (Number(im.major) || 0) + '.' + (Number(im.minor) || 0); + }).join(' \u00b7 '); + html += '
' + esc(t('Requires:')) + ' ' + esc(req) + '
'; + } // the NVRAM requirement is a C6+C8-style total (GP Card Spec v2.3.1 // Table 11-48): the package image (load file) plus the persistent data html += '
' + esc(t('NVRAM requirement ≈')) + ' ' + esc(capMemBytes(requirement)) + @@ -8039,6 +8049,48 @@ function capMemHtml(mem) { ' + ' + esc(t('data')) + ' ' + esc(capMemBytes(nvramData)) + ')' + ' · ' + esc(t('RAM (volatile):')) + ' ' + esc(capMemBytes(ram.total)) + '
'; html += '
' + esc(t('Details')) + ''; + // compiled-against hint from the javacard.framework version + CAP format + 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 : ''; + if (mem.cap_version) against += (against ? ' · ' : '') + t('CAP format') + ' ' + mem.cap_version; + if (against) html += '
' + esc(t('Compiled against:')) + ' ' + esc(against) + '
'; + if (imports.length) { + imports.forEach(im => { + const name = jcAidName(im.aid); + const fam = jcAidFamily(im.aid); + let line = (name || im.aid) + ' ' + (Number(im.major) || 0) + '.' + (Number(im.minor) || 0); + if (fam) line += ' — ' + fam; + line += ' — ' + (Number(im.refs) || 0) + ' ' + t('refs'); + if (name) line += ' — ' + im.aid; + html += '
' + esc(line) + '
'; + }); + } + // package identity + header flags (Table 6-4) + applets + let pkg = esc(mem.package_aid || '?') + (mem.package_version ? ' v' + esc(mem.package_version) : ''); + const fl = mem.flags || {}; + const fls = []; + if (fl.applet) fls.push(t('applet package')); + if (fl.export) fls.push(t('exports an API')); + if (fl.int) fls.push(t('uses int')); + if (fls.length) pkg += ' (' + fls.join(', ') + ')'; + if (mem.package_name) pkg += ' — ' + esc(mem.package_name); + html += '
' + esc(t('Package:')) + ' ' + pkg + '
'; + if ((mem.applets || []).length) { + html += '
' + esc(t('Applets:')) + ' ' + esc((mem.applets || []).map(a => { + const n = jcAidName(a); + return n ? a + ' (' + n + ')' : a; + }).join(', ')) + '
'; + } + const comps = mem.components || []; + if (comps.length) { + const ctotal = loadFile || comps.reduce((sum, c) => sum + (Number(c.size) || 0), 0); + html += '
' + esc(t('Components:')) + ' ' + comps.map(c => { + const sz = Number(c.size) || 0; + const pct = ctotal ? ' (' + Math.round(sz * 100 / ctotal) + '%)' : ''; + return esc(c.name + ' ' + capMemBytes(sz) + pct); + }).join(' · ') + '
'; + } html += '
' + esc(t('Code image (load file):')) + ' ' + esc(capMemBytes(loadFile)) + ' · ' + esc(t('bytecode (Method.cap):')) + ' ' + esc(capMemBytes(methodBytes)) + ' · ' + esc(t('other components:')) + ' ' + esc(capMemBytes(otherComponents)) + '
'; @@ -8237,11 +8289,18 @@ const JC_AID_RIDS = { 'A000000151': 'GlobalPlatform RID', }; -// Resolve an AID (any hex case/spacing) to a standard package name, a -// well-known AID name or a RID owner hint; '' when nothing is known. -function jcAidName(aid) { +// Normalise an AID (strip spacing, uppercase) and reject malformed input; +// '' when it cannot be an AID (5-16 bytes). +function jcAidNorm(aid) { const s = (aid || '').replace(/[^0-9a-fA-F]/g, '').toUpperCase(); - if (s.length < 10 || s.length > 32 || s.length % 2) return ''; + return (s.length >= 10 && s.length <= 32 && s.length % 2 === 0) ? s : ''; +} + +// Resolve an AID to a standard package name, a well-known AID name or a RID +// owner hint; '' when nothing is known. +function jcAidName(aid) { + const s = jcAidNorm(aid); + if (!s) return ''; if (JC_AID_NAMES[s]) return JC_AID_NAMES[s]; for (const rid in JC_AID_RIDS) { if (s.startsWith(rid)) return JC_AID_RIDS[rid]; @@ -8249,6 +8308,28 @@ function jcAidName(aid) { return ''; } +// Library family of a standard package AID (JAVACARD.md section 6); '' for +// applet/vendor AIDs so the UI never guesses. +function jcAidFamily(aid) { + const s = jcAidNorm(aid); + if (!s) return ''; + if (s.startsWith('A000000062')) return 'Oracle JavaCard API'; + if (s.startsWith('A000000009')) { + return s.indexOf('0003FFFFFFFF') >= 0 ? 'ETSI SIM (2G) API' : 'ETSI UICC API'; + } + if (s.startsWith('A000000087')) return '3GPP USIM/ISIM API'; + if (s.startsWith('A000000151')) return 'GlobalPlatform API'; + return ''; +} + +// javacard.framework version -> Oracle SDK release family. Derived from the +// local Oracle SDK kit corpus (jc211_kit .. jc305u4_kit exports, 2026-09-27); +// versions not present there get no hint rather than a guess. +const JC_FRAMEWORK_SDK = { + '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', +}; + // ' (name)' for plain-text listings; '' when the AID is unknown. function jcAidSuffix(aid) { const name = jcAidName(aid); @@ -16028,6 +16109,16 @@ const LANG_RU = { 'Analyze the CAP file first': 'Сначала проанализируйте CAP-файл', 'CAP requirements (estimate)': 'Требования CAP (оценка)', 'NVRAM requirement ≈': 'Требование NVRAM ≈', + 'Requires:': 'Требуются:', + 'Compiled against:': 'Собрано под:', + 'CAP format': 'формат CAP', + 'Package:': 'Пакет:', + 'Applets:': 'Апплеты:', + 'Components:': 'Компоненты:', + 'applet package': 'пакет-апплет', + 'exports an API': 'экспортирует API', + 'uses int': 'использует int', + 'refs': 'ссылок', 'code image': 'образ кода', 'data': 'данные', 'Code image (load file):': 'Образ кода (load-файл):', diff --git a/frontend/sw.js b/frontend/sw.js index dca5e1b..0683fd5 100644 --- a/frontend/sw.js +++ b/frontend/sw.js @@ -1,4 +1,4 @@ -const CACHE = 'simple-v268'; +const CACHE = 'simple-v269'; const URLS = [ 'index.html', 'help.html', diff --git a/frontend/tests/aid_names.test.js b/frontend/tests/aid_names.test.js index 8a0ae3e..4319302 100644 --- a/frontend/tests/aid_names.test.js +++ b/frontend/tests/aid_names.test.js @@ -31,8 +31,10 @@ function extractFunc(src, name) { // Rewrite top-level const -> var so the tables leak out of sloppy-mode eval. eval(extractBlock('const JC_AID_NAMES = {', 'const JC_AID_RIDS = {').replace(/^const /gm, 'var ')); eval(extractBlock('const JC_AID_RIDS = {', 'function jcAidName').replace(/^const /gm, 'var ')); +eval(extractFunc(html, 'jcAidNorm')); eval(extractFunc(html, 'jcAidName')); eval(extractFunc(html, 'jcAidSuffix')); +eval(extractFunc(html, 'jcAidFamily')); eval(extractFunc(html, 'jcAidHtml')); globalThis.esc = s => s; @@ -81,6 +83,24 @@ test('the plain-text and HTML suffixes name known AIDs only', () => { globalThis.esc = s => s; }); +test('the AID family follows the RID groups (JAVACARD.md section 6)', () => { + assert.strictEqual(jcAidFamily('A0000000620101'), 'Oracle JavaCard API'); + assert.strictEqual(jcAidFamily('A0000000090003FFFFFFFF8910710002'), 'ETSI SIM (2G) API'); + assert.strictEqual(jcAidFamily('A0000000090005FFFFFFFF8912000000'), 'ETSI UICC API'); + assert.strictEqual(jcAidFamily('A0000000871005FFFFFFFF8913200000'), '3GPP USIM/ISIM API'); + assert.strictEqual(jcAidFamily('A00000015100'), 'GlobalPlatform API'); + // vendor/applet AIDs stay unlabelled + assert.strictEqual(jcAidFamily('D276000005AAFFCAFE0010'), ''); + assert.strictEqual(jcAidFamily(''), ''); +}); + +test('jcAidNorm validates and normalises', () => { + assert.strictEqual(jcAidNorm('a0 00 00 00 62 01 01'), 'A0000000620101'); + assert.strictEqual(jcAidNorm('AB'), ''); + assert.strictEqual(jcAidNorm('A000000062010'), ''); // odd hex length + assert.strictEqual(jcAidNorm(null), ''); +}); + test('the package table is well-formed', () => { const keys = Object.keys(JC_AID_NAMES); assert.ok(keys.length >= 45, 'entries: ' + keys.length); diff --git a/frontend/tests/capmem.test.js b/frontend/tests/capmem.test.js index 3a26f64..02139c0 100644 --- a/frontend/tests/capmem.test.js +++ b/frontend/tests/capmem.test.js @@ -32,6 +32,12 @@ function extractFunc(src, name, asyncFn) { eval(extractBlock('const _capAnalysis = {', 'function capFileKey').replace(/^const /gm, 'var ')); eval(extractFunc(html, 'capFileKey')); eval(extractFunc(html, 'capGateOk')); +eval(extractBlock('const JC_AID_NAMES = {', 'const JC_AID_RIDS = {').replace(/^const /gm, 'var ')); +eval(extractBlock('const JC_AID_RIDS = {', '// Normalise an AID').replace(/^const /gm, 'var ')); +eval(extractBlock('const JC_FRAMEWORK_SDK = {', '// ===== TLV parsers for GET STATUS').replace(/^const /gm, 'var ')); +eval(extractFunc(html, 'jcAidNorm')); +eval(extractFunc(html, 'jcAidName')); +eval(extractFunc(html, 'jcAidFamily')); eval(extractFunc(html, 'capMemBytes')); eval(extractFunc(html, 'capMemHtml')); eval(extractFunc(html, 'capAnalyzeFile', true)); @@ -48,6 +54,15 @@ function resetState() { function memFixture() { return { + cap_version: '2.1', + package_aid: 'A0000000620101', + package_version: '1.0', + package_name: null, + flags: { raw: 4, int: false, export: false, applet: true }, + applets: ['A0000000620101'], + imports: [{ aid: 'A0000000620101', minor: 0, major: 1, refs: 6 }], + components: [{ name: 'Header', size: 20 }, { name: 'Method', size: 2800 }, + { name: 'ConstantPool', size: 180 }], code: { method_component: 2048, load_file: 3000 }, nvram: { static_image: 12, array_init: 4, install_objects: 100, header_overhead: 12, ref_storage: 8, total: 136, runtime: 0, requirement: 3136 }, ram: { transient_arrays: 16, runtime_transient: 0, peak_frame: 8, total: 24 }, @@ -83,9 +98,36 @@ test('capMemHtml renders the NVRAM requirement, breakdown and warnings', () => { assert.ok(out.includes('Table 11-48'), out); assert.ok(out.includes('Estimate only'), out); assert.ok(out.includes('unknown opcode 0xAA'), out); + // the required libraries with the family, reference count and the + // compiled-against hint derived from the framework version + assert.ok(out.includes('Requires: javacard.framework \u2265 1.0'), out); + assert.ok(out.includes('Compiled against: Java Card 2.1.1/2.1.2 \u00b7 CAP format 2.1'), out); + assert.ok(out.includes('javacard.framework 1.0 — Oracle JavaCard API — 6 refs — A0000000620101'), out); + assert.ok(out.includes('Package: A0000000620101 v1.0 (applet package)'), out); + assert.ok(out.includes('Applets: A0000000620101 (javacard.framework)'), out); + assert.ok(out.includes('Components: Header 20 B (1%) \u00b7 Method 2.7 kB (93%) \u00b7 ConstantPool 180 B (6%)'), out); assert.strictEqual(capMemHtml(null), ''); }); +test('capMemHtml skips the import section on a response without imports', () => { + const mem = memFixture(); + delete mem.imports; + mem.code = { method_component: 2048 }; + delete mem.nvram.requirement; + const out = capMemHtml(mem); + assert.ok(!out.includes('Requires:'), out); + assert.ok(out.includes('NVRAM requirement \u2248 2.1 kB'), out); +}); + +test('capMemHtml keeps unknown import AIDs bare and labels the family only when known', () => { + const mem = memFixture(); + mem.imports = [{ aid: 'A1130001180001', minor: 0, major: 1, refs: 2 }]; + const out = capMemHtml(mem); + assert.ok(out.includes('Requires: A1130001180001 \u2265 1.0'), out); + assert.ok(out.includes('A1130001180001 1.0 — 2 refs'), out); + assert.ok(!out.includes('Compiled against: Java Card'), out); // no framework import +}); + test('capMemHtml falls back to the bytecode size on older server responses', () => { const mem = memFixture(); delete mem.code.load_file; diff --git a/frontend/tests/ram.test.js b/frontend/tests/ram.test.js index 772028f..69eb545 100644 --- a/frontend/tests/ram.test.js +++ b/frontend/tests/ram.test.js @@ -24,7 +24,7 @@ function extractFunc(src, name) { // Extract chain builder functions and dependencies const FNS = ['berLenStr', 'buildApdu', 'escHtml', 'esc', 'chainInit', 'chainRamBuildRowHex', 'ramFmtLifecycle', 'ramFmtPrivileges', 'ramRenderExploreHtml', 'ramCardIdxAfterRemove', 'ramClearResults', 'ramHideProgress', 'ramOpChanged', 'ramRender', 'ramApplyCard', 'ramExecute', - 'jcAidName', 'jcAidSuffix', 'jcAidHtml']; + 'jcAidNorm', 'jcAidName', 'jcAidSuffix', 'jcAidHtml']; let code = ''; for (const f of FNS) { code += extractFunc(html, f) + '\n'; diff --git a/frontend/tests/scp81.test.js b/frontend/tests/scp81.test.js index d7ad565..6a27c0b 100644 --- a/frontend/tests/scp81.test.js +++ b/frontend/tests/scp81.test.js @@ -60,6 +60,7 @@ test('scp81LogLine renders script entries', () => { eval(html.match(/const JC_AID_NAMES = \{[\s\S]*?\n\};/)[0].replace(/^const /, 'var ')); eval(html.match(/const JC_AID_RIDS = \{[\s\S]*?\n\};/)[0].replace(/^const /, 'var ')); +eval(extractFunc(html, 'jcAidNorm')); eval(extractFunc(html, 'jcAidName')); eval(extractFunc(html, 'jcAidSuffix')); eval(extractFunc(html, 'scp81DecodeGetStatus')); diff --git a/pyproject.toml b/pyproject.toml index 4c4ffd5..7b4175c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "pysim-simple-server" -version = "3.5.15" +version = "3.5.16" 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 41e95a6..0cf1f36 100755 --- a/pysim_simple_server/capmem.py +++ b/pysim_simple_server/capmem.py @@ -291,6 +291,18 @@ class Header(CapComponent): self.package_major = self.stream.read_u1() aid_len = self.stream.read_u1() self.aid = self.stream.read_bytes(aid_len) + # JC VM spec 2.2.2 6.3: JC 2.2+ headers carry the package name as + # package_name_info { u1 name_length; u1 name[name_length] } (UTF-8, + # internal form, e.g. "javacard/framework"). CAP 2.1 files have no + # bytes left after the AID. + self.package_name = None + if self.stream.available() >= 1: + name_len = self.stream.read_u1() + if 0 < name_len <= self.stream.available(): + try: + self.package_name = self.stream.read_bytes(name_len).decode('utf-8') + except UnicodeDecodeError: + self.package_name = None def package_version_str(self): return f"{self.package_major}.{self.package_minor}" @@ -763,14 +775,17 @@ class CAP: """`data` is the .cap archive (a ZIP) as bytes.""" self.data = data self.components = {} + self.files = [] # [(component name, byte size)] in ZIP order self._read_cap() def _read_cap(self): # ZIP archive of nested *.cap components with zipfile.ZipFile(io.BytesIO(self.data), 'r') as z: for name in z.namelist(): - if name.endswith('.cap'): - self._add_component(z.read(name)) + if name.endswith('.cap') and not name.lower().endswith('.capx'): + data = z.read(name) + self.files.append((name.split('/')[-1][:-4], len(data))) + self._add_component(data) def _add_component(self, data): tag = data[0] @@ -1407,6 +1422,28 @@ def scan_method_bytecode(ms, cap, resolver, warnings=None): # Main Analysis # ═══════════════════════════════════════════════════════════════════ +def _external_ref_counts(cap): + """Distinct constant-pool references per imported package token (6.7): + ClassRef/MethodRef/FieldRef/StaticRef entries whose package token points + into the Import table. A package linked against but never referenced + stays at 0.""" + counts = {} + cp = cap.components.get('constant_pool') + if not cp: + return counts + for e in cp.entries: + ref = None + if isinstance(e, (CPClassRef, CPMethodOrFieldRef)): + ref = e.class_ref + elif isinstance(e, CPStaticFieldRef): + ref = e.static_field_ref + elif isinstance(e, CPStaticMethodRef): + ref = e.static_method_ref + if ref is not None and not ref.is_internal: + counts[ref.package_token] = counts.get(ref.package_token, 0) + 1 + return counts + + def analyze_bytes(cap_bytes, verbose=False): """Analyze a CAP archive (bytes) and return (report, memory).""" cap = CAP(cap_bytes) @@ -1419,16 +1456,25 @@ def analyze_bytes(cap_bytes, verbose=False): report['package_version'] = h.package_version_str() report['package_aid'] = h.aid.hex().upper() - # Import packages + # Import packages (JC VM spec 6.6): the libraries the package is linked + # against, with the export-file versions recorded in the CAP if cap.has('import'): pkgs = cap.components['import'].packages + refs = _external_ref_counts(cap) report['import_count'] = len(pkgs) - report['packages'] = [] - for p in pkgs: - report['packages'].append({ - 'version': f'{p.major}.{p.minor}', - 'aid': p.aid_hex, - }) + report['imports'] = [ + {'aid': p.aid_hex, 'minor': p.minor, 'major': p.major, + 'refs': refs.get(0x80 + i, 0)} + for i, p in enumerate(pkgs)] + + # Header package flags (Table 6-4) and the optional package name + if cap.has('header'): + h = cap.components['header'] + report['flags'] = h.flags + report['package_name'] = h.package_name + + # Component sizes (the load file order) for the breakdown display + report['components'] = [{'name': name, 'size': size} for name, size in cap.files] # Applets if cap.has('applet'): @@ -1653,6 +1699,18 @@ def memory_json(report, memory, load_file_bytes=None): 'package_aid': report.get('package_aid'), 'applet_count': report.get('applet_count', 0), 'applets': [a.get('aid') for a in report.get('applets', [])], + # libraries the CAP is linked against (JC VM spec 6.6) with the + # distinct constant-pool reference counts (6.7) + 'imports': report.get('imports', []), + # header package flags (Table 6-4: 0x01 int, 0x02 exports, 0x04 applet) + 'flags': { + 'raw': report.get('flags', 0), + 'int': bool(report.get('flags', 0) & 0x01), + 'export': bool(report.get('flags', 0) & 0x02), + 'applet': bool(report.get('flags', 0) & 0x04), + }, + 'package_name': report.get('package_name'), + 'components': report.get('components', []), 'class_count': report.get('class_count', 0), 'method_count': report.get('method_count', 0), 'code': { diff --git a/pysim_simple_server/server.py b/pysim_simple_server/server.py index cb617cd..fad44bc 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.5.15' +VERSION = '3.5.16' 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 31e99c3..dc4ad59 100644 --- a/tests/test_cap_memory.py +++ b/tests/test_cap_memory.py @@ -27,9 +27,13 @@ def _component(tag, payload): return bytes([tag]) + _u2(len(payload) + 3) + payload -def _header(aid): +def _header(aid, flags=0, name=None): # magic, cap minor/major, flags, package minor/major, aid (LV) - return _component(0x01, _u4(0xDECAFFED) + bytes([1, 2, 0, 1, 2]) + _u1(len(aid)) + aid) + payload = _u4(0xDECAFFED) + bytes([1, 2, flags, 1, 2]) + _u1(len(aid)) + aid + if name is not None: # JC 2.2+ package_name_info + raw = name.encode('utf-8') + payload += _u1(len(raw)) + raw + return _component(0x01, payload) def _applet(aid, install_offset): @@ -107,8 +111,9 @@ def _static_field(image_size, ref_count, array_inits=()): def build_cap(header_aid=b'\x01\x02\x03\x04\x05', applet_aid=b'\x01\x02\x03\x04\x05', imports=(), cp_entries=(), classes=(), descriptor=None, method_bytecode=None, - static=None): - components = {'Header': _header(header_aid), 'Applet': _applet(applet_aid, 1)} + static=None, header_flags=0, header_name=None): + components = {'Header': _header(header_aid, header_flags, header_name), + 'Applet': _applet(applet_aid, 1)} if imports: components['Import'] = _import(imports) if cp_entries: @@ -145,7 +150,7 @@ def rich_cap(with_static=True): b'\x7a') # return bytecode = _method_header(2, 0, 0) + body return build_cap( - imports=[(1, 2, JAVACARD_FRAMEWORK)], + imports=[(0, 1, JAVACARD_FRAMEWORK)], cp_entries=[_cp_class_ref_internal(1), _cp_static_method_external(0x80, 8, 13)], classes=[_class_record(instance_size=6, ref_count=2)], descriptor=_descriptor(0, 1, [(0, 0, 1, len(body))]), @@ -202,6 +207,26 @@ class TestCapAnalyzer(unittest.TestCase): # C6+C8-style total is reported self.assertEqual(info['code']['load_file'], 0) self.assertIsNone(info['nvram']['requirement']) + # the imported library (with the distinct CP reference count), the + # header flags and the component list (ZIP order, load file order) + self.assertEqual(info['imports'], + [{'aid': 'A0000000620101', 'minor': 0, 'major': 1, 'refs': 1}]) + self.assertEqual(info['flags'], {'raw': 0, 'int': False, 'export': False, 'applet': False}) + self.assertIsNone(info['package_name']) + names = [c['name'] for c in info['components']] + self.assertIn('Header', names) + self.assertIn('Import', names) + self.assertIn('Method', names) + + def test_header_flags_and_package_name(self): + cap = build_cap(header_flags=0x05, header_name='com/example/applet') + report, _ = capmem.analyze_bytes(cap) + info = capmem.memory_json(report, capmem.compute_memory(report)) + self.assertEqual(info['flags'], {'raw': 5, 'int': True, 'export': False, 'applet': True}) + self.assertEqual(info['package_name'], 'com/example/applet') + # a CAP 2.1 header (no name bytes) reports no name + report, _ = capmem.analyze_bytes(build_cap()) + self.assertIsNone(report['package_name']) def test_memory_json_nvram_requirement_uses_the_load_file(self): report, memory = capmem.analyze_bytes(rich_cap()) @@ -242,6 +267,9 @@ class TestCapInfoBody(unittest.TestCase): self.assertEqual(resp['memory']['suggested']['c6'], resp['load_file_bytes']) self.assertEqual(resp['memory']['nvram']['requirement'], resp['load_file_bytes'] + resp['memory']['nvram']['total']) + # the component sizes are the load file parts + self.assertEqual(sum(c['size'] for c in resp['memory']['components']), + resp['load_file_bytes']) def test_bad_hex_and_corrupt_archives_are_rejected(self): self.assertFalse(_cap_info_body({})['ok'])