feat: show the CAP's required libraries, package identity and components (v3.5.16)
The Import component (JC VM spec 6.6) is now exposed by /api/cap-info and rendered in the CAP analysis box: - imports: the libraries the CAP is linked against with the export-file versions and the number of distinct constant-pool references (6.7), displayed as "name >= version" (a card resolves an import only with the same major and a minor >= the recorded one, 4.5.2). Standard names come from the AID table; each gets a family label (Oracle JavaCard / ETSI SIM 2G / ETSI UICC / 3GPP USIM-ISIM / GlobalPlatform) and, for javacard.framework, a Java Card SDK release hint derived from the local Oracle SDK kit corpus (jc211..jc305u4 exports; unknown versions stay unhinted). Vendor/applet AIDs stay bare. - Header package flags (Table 6-4: int / exports / applet package) and the optional JC 2.2 package_name (absent in all CAP 2.1 files). - components: every archive entry in load-file order with its size and share of the load file (including the Directory/Export entries capmem does not parse); the sizes sum to load_file_bytes. - The PWA box leads with "Requires: ..." when imports exist, keeps the compiled-against line (Java Card hint + CAP format), the package/applet identity, the import details (family, refs, AID) and the component breakdown in Details; a memory-only response still renders. Tests: Python +2 (imports/flags/name/components; header flags + package name) with the synthetic CAP builder extended; frontend +2 renderer cases (unknown AIDs, no-import responses) plus jcAidNorm/jcAidFamily tests; the jcAidNorm extraction added to the ram/scp81 harnesses. Help EN/RU, docs/api.md, AGENTS. 591 frontend / 473 Python green; version 3.5.16; sw simple-v269.
This commit is contained in:
+17
@@ -348,6 +348,11 @@ used for installation.
|
|||||||
"memory": {
|
"memory": {
|
||||||
"package_aid": "AA1902BC226001",
|
"package_aid": "AA1902BC226001",
|
||||||
"applet_count": 1, "applets": ["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,
|
"class_count": 3, "method_count": 12,
|
||||||
"code": {"method_component": 850, "load_file": 1234},
|
"code": {"method_component": 850, "load_file": 1234},
|
||||||
"nvram": {"static_image": 12, "array_init": 4, "install_objects": 100,
|
"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
|
`code.load_file` is the concatenated load file (all components) - the
|
||||||
package image the card stores, our proxy for the GlobalPlatform Card Spec
|
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
|
v2.3.1 Table 11-48 "non-volatile code" minimum memory requirement; the
|
||||||
|
|||||||
@@ -319,7 +319,7 @@
|
|||||||
<thead><tr class="border-b border-gray-300 dark:border-slate-700"><th class="text-left py-1 px-2">Операция</th><th class="text-left py-1 px-2">Описание</th></tr></thead>
|
<thead><tr class="border-b border-gray-300 dark:border-slate-700"><th class="text-left py-1 px-2">Операция</th><th class="text-left py-1 px-2">Описание</th></tr></thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">Обзор карты (все данные GP)</td><td class="py-1 px-2">Запрос GET STATUS для ISD, приложений, ELF и модулей ELF, а также GET DATA FF21 для информации о памяти. Результаты отображаются в обзоре с кнопками <strong>Удалить</strong> для каждого элемента.</td></tr>
|
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">Обзор карты (все данные GP)</td><td class="py-1 px-2">Запрос GET STATUS для ISD, приложений, ELF и модулей ELF, а также GET DATA FF21 для информации о памяти. Результаты отображаются в обзоре с кнопками <strong>Удалить</strong> для каждого элемента.</td></tr>
|
||||||
<tr><td class="py-1 px-2">Установка пакета (.cap файл)</td><td class="py-1 px-2">Отправка <code class="font-mono text-sm">.cap</code> файла на карту через сервер: INSTALL[for load] → LOAD ×N → INSTALL[for install (+make selectable)]. Load-файл делится на LOAD APDU размера <strong>размер блока LOAD</strong> (1–240 байт полезной нагрузки, по умолчанию 240); каждый защищённый пакет доставляется одним SMS или, если он больше, конкатенированной SMS-PP загрузкой до 5 сегментов, так что большой <code>.cap</code> просто занимает несколько SMS. Сразу после выбора файл проверяется и в форме показывается <strong>оценка требований к памяти</strong>: <strong>требование NVRAM</strong> (образ кода + постоянные данные, сумма в стиле C6+C8 из GP Card Spec) и оценка RAM; повреждённый файл не проходит анализ, и <strong>Execute</strong> остаётся недоступной до успешного анализа.</td></tr>
|
<tr><td class="py-1 px-2">Установка пакета (.cap файл)</td><td class="py-1 px-2">Отправка <code class="font-mono text-sm">.cap</code> файла на карту через сервер: INSTALL[for load] → LOAD ×N → INSTALL[for install (+make selectable)]. Load-файл делится на LOAD APDU размера <strong>размер блока LOAD</strong> (1–240 байт полезной нагрузки, по умолчанию 240); каждый защищённый пакет доставляется одним SMS или, если он больше, конкатенированной SMS-PP загрузкой до 5 сегментов, так что большой <code>.cap</code> просто занимает несколько SMS. Сразу после выбора файл проверяется и в форме показывается <strong>оценка требований к памяти</strong>: <strong>требование NVRAM</strong> (образ кода + постоянные данные, сумма в стиле C6+C8 из GP Card Spec) и оценка RAM. Там же перечислены <strong>библиотеки, с которыми слинкован CAP</strong> (его компонент Import, в виде <code class="font-mono text-sm">имя ≥ версия</code> — имена стандартных библиотек подставляются, а где известно, даётся подсказка о версии Java Card), идентификаторы пакета и апплетов и разбивка по компонентам. Повреждённый файл не проходит анализ, и <strong>Execute</strong> остаётся недоступной до успешного анализа.</td></tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
@@ -376,7 +376,7 @@
|
|||||||
<ul class="list-disc list-inside text-sm space-y-1 mb-3">
|
<ul class="list-disc list-inside text-sm space-y-1 mb-3">
|
||||||
<li><strong>Empty</strong> — начать с пустого списка.</li>
|
<li><strong>Empty</strong> — начать с пустого списка.</li>
|
||||||
<li><strong>Explore ISD</strong> — эталонная административная последовательность (<code class="font-mono text-sm">GET DATA FF21</code>, листинги GET STATUS ISD/ELF/приложений, <code class="font-mono text-sm">GET DATA 0085</code>); длинные листинги автоматически продолжаются страницами <code class="font-mono text-sm">SW 6310/CAFE</code>.</li>
|
<li><strong>Explore ISD</strong> — эталонная административная последовательность (<code class="font-mono text-sm">GET DATA FF21</code>, листинги GET STATUS ISD/ELF/приложений, <code class="font-mono text-sm">GET DATA 0085</code>); длинные листинги автоматически продолжаются страницами <code class="font-mono text-sm">SW 6310/CAFE</code>.</li>
|
||||||
<li><strong>Install from .cap</strong> — выберите <code class="font-mono text-sm">.cap</code>, при необходимости SD AID, параметры install/STK и <em>Make selectable</em>; кнопка <strong>Сгенерировать</strong> строит INSTALL [for load] → LOAD ×N → INSTALL [for install]. Файл используется только для генерации APDU — он не сохраняется, как и его имя. При выборе файл проверяется и показывается оценка требований к памяти (требование NVRAM как сумма образа кода и постоянных данных, плюс RAM); <strong>Сгенерировать</strong> недоступна до успешного анализа.</li>
|
<li><strong>Install from .cap</strong> — выберите <code class="font-mono text-sm">.cap</code>, при необходимости SD AID, параметры install/STK и <em>Make selectable</em>; кнопка <strong>Сгенерировать</strong> строит INSTALL [for load] → LOAD ×N → INSTALL [for install]. Файл используется только для генерации APDU — он не сохраняется, как и его имя. При выборе файл проверяется и показывается оценка требований к памяти (требование NVRAM как сумма образа кода и постоянных данных, плюс RAM); также перечисляются требуемые библиотеки, идентификаторы пакета и разбивка по компонентам. <strong>Сгенерировать</strong> недоступна до успешного анализа.</li>
|
||||||
<li><strong>Delete AID</strong> — список AID (по одному в строке) и P2 (<em>object only</em> / <em>object and related objects</em>) → APDU DELETE.</li>
|
<li><strong>Delete AID</strong> — список AID (по одному в строке) и P2 (<em>object only</em> / <em>object and related objects</em>) → APDU DELETE.</li>
|
||||||
</ul>
|
</ul>
|
||||||
<p class="text-sm mb-3">Таблица показывает имя, тип, число APDU и время создания каждого скрипта, а также кнопки <strong>Редактировать</strong> и <strong>Удалить</strong>; в редакторе есть поле имени и текстовая область APDU. В ходе сессии сервер отдаёт по одному C-APDU на каждый POST карты и отслеживает выполнение: карта сообщает статус в следующем POST (<code class="font-mono text-sm">X-Admin-Script-Status</code>), оборвавшаяся сессия досылает только невыполненные APDU (<code class="font-mono text-sm">X-Admin-Resume</code> продолжает, новый диалог начинает заново), а завершённый скрипт закрывается ответом <code class="font-mono text-sm">204 No Content</code>. Конструктор RAM/GP вкладки Remote APDU может отправить цепочку команд прямо в прогон кнопкой <strong>«В очередь SCP81»</strong>.</p>
|
<p class="text-sm mb-3">Таблица показывает имя, тип, число APDU и время создания каждого скрипта, а также кнопки <strong>Редактировать</strong> и <strong>Удалить</strong>; в редакторе есть поле имени и текстовая область APDU. В ходе сессии сервер отдаёт по одному C-APDU на каждый POST карты и отслеживает выполнение: карта сообщает статус в следующем POST (<code class="font-mono text-sm">X-Admin-Script-Status</code>), оборвавшаяся сессия досылает только невыполненные APDU (<code class="font-mono text-sm">X-Admin-Resume</code> продолжает, новый диалог начинает заново), а завершённый скрипт закрывается ответом <code class="font-mono text-sm">204 No Content</code>. Конструктор RAM/GP вкладки Remote APDU может отправить цепочку команд прямо в прогон кнопкой <strong>«В очередь SCP81»</strong>.</p>
|
||||||
|
|||||||
+2
-2
@@ -318,7 +318,7 @@
|
|||||||
<thead><tr class="border-b border-gray-300 dark:border-slate-700"><th class="text-left py-1 px-2">Operation</th><th class="text-left py-1 px-2">Description</th></tr></thead>
|
<thead><tr class="border-b border-gray-300 dark:border-slate-700"><th class="text-left py-1 px-2">Operation</th><th class="text-left py-1 px-2">Description</th></tr></thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">Explore Card (all GP data)</td><td class="py-1 px-2">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 <strong>Delete</strong> buttons.</td></tr>
|
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">Explore Card (all GP data)</td><td class="py-1 px-2">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 <strong>Delete</strong> buttons.</td></tr>
|
||||||
<tr><td class="py-1 px-2">Install Package (.cap file)</td><td class="py-1 px-2">Sends a <code class="font-mono text-sm">.cap</code> 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 <strong>LOAD block size</strong> (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 <code class="font-mono text-sm">.cap</code> simply takes several SMS. As soon as the file is selected it is validated and its <strong>memory requirements are estimated</strong> in the form: the <strong>NVRAM requirement</strong> (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 <strong>Execute</strong> stays disabled until a successful analysis.</td></tr>
|
<tr><td class="py-1 px-2">Install Package (.cap file)</td><td class="py-1 px-2">Sends a <code class="font-mono text-sm">.cap</code> 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 <strong>LOAD block size</strong> (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 <code class="font-mono text-sm">.cap</code> simply takes several SMS. As soon as the file is selected it is validated and its <strong>memory requirements are estimated</strong> in the form: the <strong>NVRAM requirement</strong> (the code image + persistent data, the GlobalPlatform Card Spec C6+C8 style total) and the RAM estimate. The same box lists the <strong>libraries the CAP is linked against</strong> (its Import component, shown as <code class="font-mono text-sm">name ≥ version</code> 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 <strong>Execute</strong> stays disabled until a successful analysis.</td></tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
@@ -375,7 +375,7 @@
|
|||||||
<ul class="list-disc list-inside text-sm space-y-1 mb-3">
|
<ul class="list-disc list-inside text-sm space-y-1 mb-3">
|
||||||
<li><strong>Empty</strong> — start from scratch.</li>
|
<li><strong>Empty</strong> — start from scratch.</li>
|
||||||
<li><strong>Explore ISD</strong> — the reference administration sequence (<code class="font-mono text-sm">GET DATA FF21</code>, GET STATUS ISD/ELF/application listings, <code class="font-mono text-sm">GET DATA 0085</code>); long listings auto-continue through the <code class="font-mono text-sm">SW 6310/CAFE</code> pages.</li>
|
<li><strong>Explore ISD</strong> — the reference administration sequence (<code class="font-mono text-sm">GET DATA FF21</code>, GET STATUS ISD/ELF/application listings, <code class="font-mono text-sm">GET DATA 0085</code>); long listings auto-continue through the <code class="font-mono text-sm">SW 6310/CAFE</code> pages.</li>
|
||||||
<li><strong>Install from .cap</strong> — pick a <code class="font-mono text-sm">.cap</code> plus optional SD AID, install/STK parameters and <em>make selectable</em>; <strong>Generate</strong> builds INSTALL [for load] → LOAD ×N → INSTALL [for install]. The file is only used to generate the APDUs — it is not stored, not even its name. On selection it is validated and its memory requirements are estimated (the NVRAM requirement as the code-image + persistent-data total, plus RAM); <strong>Generate</strong> stays disabled until the analysis succeeds.</li>
|
<li><strong>Install from .cap</strong> — pick a <code class="font-mono text-sm">.cap</code> plus optional SD AID, install/STK parameters and <em>make selectable</em>; <strong>Generate</strong> builds INSTALL [for load] → LOAD ×N → INSTALL [for install]. The file is only used to generate the APDUs — it is not stored, not even its name. On selection it is validated and its memory requirements are estimated (the NVRAM requirement as the code-image + persistent-data total, plus RAM); the required libraries, package identity and component breakdown are listed too. <strong>Generate</strong> stays disabled until the analysis succeeds.</li>
|
||||||
<li><strong>Delete AID</strong> — AID list (one per line) and P2 (<em>object only</em> / <em>object and related objects</em>) → DELETE APDUs.</li>
|
<li><strong>Delete AID</strong> — AID list (one per line) and P2 (<em>object only</em> / <em>object and related objects</em>) → DELETE APDUs.</li>
|
||||||
</ul>
|
</ul>
|
||||||
<p class="text-sm mb-3">The table lists each script with its kind, APDU count and creation time, plus <strong>Edit</strong> and <strong>Delete</strong>; 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 (<code class="font-mono text-sm">X-Admin-Script-Status</code>), a session that dies resends only the unexecuted APDUs (<code class="font-mono text-sm">X-Admin-Resume</code> continues, a fresh dialog restarts), and a completed script is closed with <code class="font-mono text-sm">204 No Content</code>. The Remote APDU tab's RAM/GP builder can feed a command chain straight into the run with <strong>Queue in SCP81</strong>.</p>
|
<p class="text-sm mb-3">The table lists each script with its kind, APDU count and creation time, plus <strong>Edit</strong> and <strong>Delete</strong>; 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 (<code class="font-mono text-sm">X-Admin-Script-Status</code>), a session that dies resends only the unexecuted APDUs (<code class="font-mono text-sm">X-Admin-Resume</code> continues, a fresh dialog restarts), and a completed script is closed with <code class="font-mono text-sm">204 No Content</code>. The Remote APDU tab's RAM/GP builder can feed a command chain straight into the run with <strong>Queue in SCP81</strong>.</p>
|
||||||
|
|||||||
+96
-5
@@ -1616,7 +1616,7 @@
|
|||||||
// ===== Version =====
|
// ===== Version =====
|
||||||
// Single source of truth for the PWA version: shown in the header and used
|
// Single source of truth for the PWA version: shown in the header and used
|
||||||
// by the server version check in pysimConnect().
|
// 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;
|
document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION;
|
||||||
|
|
||||||
// ===== Tab switching =====
|
// ===== Tab switching =====
|
||||||
@@ -8031,7 +8031,17 @@ function capMemHtml(mem) {
|
|||||||
const nvramData = Number(nv.total) || 0;
|
const nvramData = Number(nv.total) || 0;
|
||||||
const requirement = Number(nv.requirement) || (loadFile + nvramData);
|
const requirement = Number(nv.requirement) || (loadFile + nvramData);
|
||||||
const otherComponents = Math.max(0, loadFile - methodBytes);
|
const otherComponents = Math.max(0, loadFile - methodBytes);
|
||||||
|
const imports = mem.imports || [];
|
||||||
let html = '<div class="font-medium text-gray-700 dark:text-slate-300">' + esc(t('CAP requirements (estimate)')) + '</div>';
|
let html = '<div class="font-medium text-gray-700 dark:text-slate-300">' + esc(t('CAP requirements (estimate)')) + '</div>';
|
||||||
|
// 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 += '<div>' + esc(t('Requires:')) + ' ' + esc(req) + '</div>';
|
||||||
|
}
|
||||||
// the NVRAM requirement is a C6+C8-style total (GP Card Spec v2.3.1
|
// 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
|
// Table 11-48): the package image (load file) plus the persistent data
|
||||||
html += '<div>' + esc(t('NVRAM requirement ≈')) + ' ' + esc(capMemBytes(requirement)) +
|
html += '<div>' + esc(t('NVRAM requirement ≈')) + ' ' + esc(capMemBytes(requirement)) +
|
||||||
@@ -8039,6 +8049,48 @@ function capMemHtml(mem) {
|
|||||||
' + ' + esc(t('data')) + ' ' + esc(capMemBytes(nvramData)) + ')' +
|
' + ' + esc(t('data')) + ' ' + esc(capMemBytes(nvramData)) + ')' +
|
||||||
' · ' + esc(t('RAM (volatile):')) + ' ' + esc(capMemBytes(ram.total)) + '</div>';
|
' · ' + esc(t('RAM (volatile):')) + ' ' + esc(capMemBytes(ram.total)) + '</div>';
|
||||||
html += '<details class="mt-1"><summary class="cursor-pointer text-gray-400">' + esc(t('Details')) + '</summary>';
|
html += '<details class="mt-1"><summary class="cursor-pointer text-gray-400">' + esc(t('Details')) + '</summary>';
|
||||||
|
// 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 += '<div>' + esc(t('Compiled against:')) + ' ' + esc(against) + '</div>';
|
||||||
|
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 += '<div>' + esc(line) + '</div>';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// 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 += '<div>' + esc(t('Package:')) + ' ' + pkg + '</div>';
|
||||||
|
if ((mem.applets || []).length) {
|
||||||
|
html += '<div>' + esc(t('Applets:')) + ' ' + esc((mem.applets || []).map(a => {
|
||||||
|
const n = jcAidName(a);
|
||||||
|
return n ? a + ' (' + n + ')' : a;
|
||||||
|
}).join(', ')) + '</div>';
|
||||||
|
}
|
||||||
|
const comps = mem.components || [];
|
||||||
|
if (comps.length) {
|
||||||
|
const ctotal = loadFile || comps.reduce((sum, c) => sum + (Number(c.size) || 0), 0);
|
||||||
|
html += '<div>' + 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(' · ') + '</div>';
|
||||||
|
}
|
||||||
html += '<div>' + esc(t('Code image (load file):')) + ' ' + esc(capMemBytes(loadFile)) +
|
html += '<div>' + esc(t('Code image (load file):')) + ' ' + esc(capMemBytes(loadFile)) +
|
||||||
' · ' + esc(t('bytecode (Method.cap):')) + ' ' + esc(capMemBytes(methodBytes)) +
|
' · ' + esc(t('bytecode (Method.cap):')) + ' ' + esc(capMemBytes(methodBytes)) +
|
||||||
' · ' + esc(t('other components:')) + ' ' + esc(capMemBytes(otherComponents)) + '</div>';
|
' · ' + esc(t('other components:')) + ' ' + esc(capMemBytes(otherComponents)) + '</div>';
|
||||||
@@ -8237,11 +8289,18 @@ const JC_AID_RIDS = {
|
|||||||
'A000000151': 'GlobalPlatform RID',
|
'A000000151': 'GlobalPlatform RID',
|
||||||
};
|
};
|
||||||
|
|
||||||
// Resolve an AID (any hex case/spacing) to a standard package name, a
|
// Normalise an AID (strip spacing, uppercase) and reject malformed input;
|
||||||
// well-known AID name or a RID owner hint; '' when nothing is known.
|
// '' when it cannot be an AID (5-16 bytes).
|
||||||
function jcAidName(aid) {
|
function jcAidNorm(aid) {
|
||||||
const s = (aid || '').replace(/[^0-9a-fA-F]/g, '').toUpperCase();
|
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];
|
if (JC_AID_NAMES[s]) return JC_AID_NAMES[s];
|
||||||
for (const rid in JC_AID_RIDS) {
|
for (const rid in JC_AID_RIDS) {
|
||||||
if (s.startsWith(rid)) return JC_AID_RIDS[rid];
|
if (s.startsWith(rid)) return JC_AID_RIDS[rid];
|
||||||
@@ -8249,6 +8308,28 @@ function jcAidName(aid) {
|
|||||||
return '';
|
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.
|
// ' (name)' for plain-text listings; '' when the AID is unknown.
|
||||||
function jcAidSuffix(aid) {
|
function jcAidSuffix(aid) {
|
||||||
const name = jcAidName(aid);
|
const name = jcAidName(aid);
|
||||||
@@ -16028,6 +16109,16 @@ const LANG_RU = {
|
|||||||
'Analyze the CAP file first': 'Сначала проанализируйте CAP-файл',
|
'Analyze the CAP file first': 'Сначала проанализируйте CAP-файл',
|
||||||
'CAP requirements (estimate)': 'Требования CAP (оценка)',
|
'CAP requirements (estimate)': 'Требования CAP (оценка)',
|
||||||
'NVRAM requirement ≈': 'Требование NVRAM ≈',
|
'NVRAM requirement ≈': 'Требование NVRAM ≈',
|
||||||
|
'Requires:': 'Требуются:',
|
||||||
|
'Compiled against:': 'Собрано под:',
|
||||||
|
'CAP format': 'формат CAP',
|
||||||
|
'Package:': 'Пакет:',
|
||||||
|
'Applets:': 'Апплеты:',
|
||||||
|
'Components:': 'Компоненты:',
|
||||||
|
'applet package': 'пакет-апплет',
|
||||||
|
'exports an API': 'экспортирует API',
|
||||||
|
'uses int': 'использует int',
|
||||||
|
'refs': 'ссылок',
|
||||||
'code image': 'образ кода',
|
'code image': 'образ кода',
|
||||||
'data': 'данные',
|
'data': 'данные',
|
||||||
'Code image (load file):': 'Образ кода (load-файл):',
|
'Code image (load file):': 'Образ кода (load-файл):',
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
const CACHE = 'simple-v268';
|
const CACHE = 'simple-v269';
|
||||||
const URLS = [
|
const URLS = [
|
||||||
'index.html',
|
'index.html',
|
||||||
'help.html',
|
'help.html',
|
||||||
|
|||||||
@@ -31,8 +31,10 @@ function extractFunc(src, name) {
|
|||||||
// Rewrite top-level const -> var so the tables leak out of sloppy-mode eval.
|
// 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_NAMES = {', 'const JC_AID_RIDS = {').replace(/^const /gm, 'var '));
|
||||||
eval(extractBlock('const JC_AID_RIDS = {', 'function jcAidName').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, 'jcAidName'));
|
||||||
eval(extractFunc(html, 'jcAidSuffix'));
|
eval(extractFunc(html, 'jcAidSuffix'));
|
||||||
|
eval(extractFunc(html, 'jcAidFamily'));
|
||||||
eval(extractFunc(html, 'jcAidHtml'));
|
eval(extractFunc(html, 'jcAidHtml'));
|
||||||
globalThis.esc = s => s;
|
globalThis.esc = s => s;
|
||||||
|
|
||||||
@@ -81,6 +83,24 @@ test('the plain-text and HTML suffixes name known AIDs only', () => {
|
|||||||
globalThis.esc = s => s;
|
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', () => {
|
test('the package table is well-formed', () => {
|
||||||
const keys = Object.keys(JC_AID_NAMES);
|
const keys = Object.keys(JC_AID_NAMES);
|
||||||
assert.ok(keys.length >= 45, 'entries: ' + keys.length);
|
assert.ok(keys.length >= 45, 'entries: ' + keys.length);
|
||||||
|
|||||||
@@ -32,6 +32,12 @@ function extractFunc(src, name, asyncFn) {
|
|||||||
eval(extractBlock('const _capAnalysis = {', 'function capFileKey').replace(/^const /gm, 'var '));
|
eval(extractBlock('const _capAnalysis = {', 'function capFileKey').replace(/^const /gm, 'var '));
|
||||||
eval(extractFunc(html, 'capFileKey'));
|
eval(extractFunc(html, 'capFileKey'));
|
||||||
eval(extractFunc(html, 'capGateOk'));
|
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, 'capMemBytes'));
|
||||||
eval(extractFunc(html, 'capMemHtml'));
|
eval(extractFunc(html, 'capMemHtml'));
|
||||||
eval(extractFunc(html, 'capAnalyzeFile', true));
|
eval(extractFunc(html, 'capAnalyzeFile', true));
|
||||||
@@ -48,6 +54,15 @@ function resetState() {
|
|||||||
|
|
||||||
function memFixture() {
|
function memFixture() {
|
||||||
return {
|
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 },
|
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 },
|
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 },
|
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('Table 11-48'), out);
|
||||||
assert.ok(out.includes('Estimate only'), out);
|
assert.ok(out.includes('Estimate only'), out);
|
||||||
assert.ok(out.includes('unknown opcode 0xAA'), 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), '');
|
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', () => {
|
test('capMemHtml falls back to the bytecode size on older server responses', () => {
|
||||||
const mem = memFixture();
|
const mem = memFixture();
|
||||||
delete mem.code.load_file;
|
delete mem.code.load_file;
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ function extractFunc(src, name) {
|
|||||||
// Extract chain builder functions and dependencies
|
// Extract chain builder functions and dependencies
|
||||||
const FNS = ['berLenStr', 'buildApdu', 'escHtml', 'esc', 'chainInit', 'chainRamBuildRowHex', 'ramFmtLifecycle', 'ramFmtPrivileges', 'ramRenderExploreHtml',
|
const FNS = ['berLenStr', 'buildApdu', 'escHtml', 'esc', 'chainInit', 'chainRamBuildRowHex', 'ramFmtLifecycle', 'ramFmtPrivileges', 'ramRenderExploreHtml',
|
||||||
'ramCardIdxAfterRemove', 'ramClearResults', 'ramHideProgress', 'ramOpChanged', 'ramRender', 'ramApplyCard', 'ramExecute',
|
'ramCardIdxAfterRemove', 'ramClearResults', 'ramHideProgress', 'ramOpChanged', 'ramRender', 'ramApplyCard', 'ramExecute',
|
||||||
'jcAidName', 'jcAidSuffix', 'jcAidHtml'];
|
'jcAidNorm', 'jcAidName', 'jcAidSuffix', 'jcAidHtml'];
|
||||||
let code = '';
|
let code = '';
|
||||||
for (const f of FNS) {
|
for (const f of FNS) {
|
||||||
code += extractFunc(html, f) + '\n';
|
code += extractFunc(html, f) + '\n';
|
||||||
|
|||||||
@@ -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_NAMES = \{[\s\S]*?\n\};/)[0].replace(/^const /, 'var '));
|
||||||
eval(html.match(/const JC_AID_RIDS = \{[\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, 'jcAidName'));
|
||||||
eval(extractFunc(html, 'jcAidSuffix'));
|
eval(extractFunc(html, 'jcAidSuffix'));
|
||||||
eval(extractFunc(html, 'scp81DecodeGetStatus'));
|
eval(extractFunc(html, 'scp81DecodeGetStatus'));
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "pysim-simple-server"
|
name = "pysim-simple-server"
|
||||||
version = "3.5.15"
|
version = "3.5.16"
|
||||||
description = "HTTP REST server wrapping pysim for the SIMple PWA"
|
description = "HTTP REST server wrapping pysim for the SIMple PWA"
|
||||||
requires-python = ">=3.8"
|
requires-python = ">=3.8"
|
||||||
# pysim is a git-only dependency installed explicitly by setup.bat/setup.sh.
|
# pysim is a git-only dependency installed explicitly by setup.bat/setup.sh.
|
||||||
|
|||||||
@@ -291,6 +291,18 @@ class Header(CapComponent):
|
|||||||
self.package_major = self.stream.read_u1()
|
self.package_major = self.stream.read_u1()
|
||||||
aid_len = self.stream.read_u1()
|
aid_len = self.stream.read_u1()
|
||||||
self.aid = self.stream.read_bytes(aid_len)
|
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):
|
def package_version_str(self):
|
||||||
return f"{self.package_major}.{self.package_minor}"
|
return f"{self.package_major}.{self.package_minor}"
|
||||||
@@ -763,14 +775,17 @@ class CAP:
|
|||||||
"""`data` is the .cap archive (a ZIP) as bytes."""
|
"""`data` is the .cap archive (a ZIP) as bytes."""
|
||||||
self.data = data
|
self.data = data
|
||||||
self.components = {}
|
self.components = {}
|
||||||
|
self.files = [] # [(component name, byte size)] in ZIP order
|
||||||
self._read_cap()
|
self._read_cap()
|
||||||
|
|
||||||
def _read_cap(self):
|
def _read_cap(self):
|
||||||
# ZIP archive of nested *.cap components
|
# ZIP archive of nested *.cap components
|
||||||
with zipfile.ZipFile(io.BytesIO(self.data), 'r') as z:
|
with zipfile.ZipFile(io.BytesIO(self.data), 'r') as z:
|
||||||
for name in z.namelist():
|
for name in z.namelist():
|
||||||
if name.endswith('.cap'):
|
if name.endswith('.cap') and not name.lower().endswith('.capx'):
|
||||||
self._add_component(z.read(name))
|
data = z.read(name)
|
||||||
|
self.files.append((name.split('/')[-1][:-4], len(data)))
|
||||||
|
self._add_component(data)
|
||||||
|
|
||||||
def _add_component(self, data):
|
def _add_component(self, data):
|
||||||
tag = data[0]
|
tag = data[0]
|
||||||
@@ -1407,6 +1422,28 @@ def scan_method_bytecode(ms, cap, resolver, warnings=None):
|
|||||||
# Main Analysis
|
# 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):
|
def analyze_bytes(cap_bytes, verbose=False):
|
||||||
"""Analyze a CAP archive (bytes) and return (report, memory)."""
|
"""Analyze a CAP archive (bytes) and return (report, memory)."""
|
||||||
cap = CAP(cap_bytes)
|
cap = CAP(cap_bytes)
|
||||||
@@ -1419,16 +1456,25 @@ def analyze_bytes(cap_bytes, verbose=False):
|
|||||||
report['package_version'] = h.package_version_str()
|
report['package_version'] = h.package_version_str()
|
||||||
report['package_aid'] = h.aid.hex().upper()
|
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'):
|
if cap.has('import'):
|
||||||
pkgs = cap.components['import'].packages
|
pkgs = cap.components['import'].packages
|
||||||
|
refs = _external_ref_counts(cap)
|
||||||
report['import_count'] = len(pkgs)
|
report['import_count'] = len(pkgs)
|
||||||
report['packages'] = []
|
report['imports'] = [
|
||||||
for p in pkgs:
|
{'aid': p.aid_hex, 'minor': p.minor, 'major': p.major,
|
||||||
report['packages'].append({
|
'refs': refs.get(0x80 + i, 0)}
|
||||||
'version': f'{p.major}.{p.minor}',
|
for i, p in enumerate(pkgs)]
|
||||||
'aid': p.aid_hex,
|
|
||||||
})
|
# 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
|
# Applets
|
||||||
if cap.has('applet'):
|
if cap.has('applet'):
|
||||||
@@ -1653,6 +1699,18 @@ def memory_json(report, memory, load_file_bytes=None):
|
|||||||
'package_aid': report.get('package_aid'),
|
'package_aid': report.get('package_aid'),
|
||||||
'applet_count': report.get('applet_count', 0),
|
'applet_count': report.get('applet_count', 0),
|
||||||
'applets': [a.get('aid') for a in report.get('applets', [])],
|
'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),
|
'class_count': report.get('class_count', 0),
|
||||||
'method_count': report.get('method_count', 0),
|
'method_count': report.get('method_count', 0),
|
||||||
'code': {
|
'code': {
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ from osmocom.tlv import BER_TLV_IE
|
|||||||
from osmocom.utils import rpad
|
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
|
MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE
|
||||||
|
|
||||||
|
|||||||
@@ -27,9 +27,13 @@ def _component(tag, payload):
|
|||||||
return bytes([tag]) + _u2(len(payload) + 3) + 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)
|
# 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):
|
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',
|
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,
|
imports=(), cp_entries=(), classes=(), descriptor=None, method_bytecode=None,
|
||||||
static=None):
|
static=None, header_flags=0, header_name=None):
|
||||||
components = {'Header': _header(header_aid), 'Applet': _applet(applet_aid, 1)}
|
components = {'Header': _header(header_aid, header_flags, header_name),
|
||||||
|
'Applet': _applet(applet_aid, 1)}
|
||||||
if imports:
|
if imports:
|
||||||
components['Import'] = _import(imports)
|
components['Import'] = _import(imports)
|
||||||
if cp_entries:
|
if cp_entries:
|
||||||
@@ -145,7 +150,7 @@ def rich_cap(with_static=True):
|
|||||||
b'\x7a') # return
|
b'\x7a') # return
|
||||||
bytecode = _method_header(2, 0, 0) + body
|
bytecode = _method_header(2, 0, 0) + body
|
||||||
return build_cap(
|
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)],
|
cp_entries=[_cp_class_ref_internal(1), _cp_static_method_external(0x80, 8, 13)],
|
||||||
classes=[_class_record(instance_size=6, ref_count=2)],
|
classes=[_class_record(instance_size=6, ref_count=2)],
|
||||||
descriptor=_descriptor(0, 1, [(0, 0, 1, len(body))]),
|
descriptor=_descriptor(0, 1, [(0, 0, 1, len(body))]),
|
||||||
@@ -202,6 +207,26 @@ class TestCapAnalyzer(unittest.TestCase):
|
|||||||
# C6+C8-style total is reported
|
# C6+C8-style total is reported
|
||||||
self.assertEqual(info['code']['load_file'], 0)
|
self.assertEqual(info['code']['load_file'], 0)
|
||||||
self.assertIsNone(info['nvram']['requirement'])
|
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):
|
def test_memory_json_nvram_requirement_uses_the_load_file(self):
|
||||||
report, memory = capmem.analyze_bytes(rich_cap())
|
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']['suggested']['c6'], resp['load_file_bytes'])
|
||||||
self.assertEqual(resp['memory']['nvram']['requirement'],
|
self.assertEqual(resp['memory']['nvram']['requirement'],
|
||||||
resp['load_file_bytes'] + resp['memory']['nvram']['total'])
|
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):
|
def test_bad_hex_and_corrupt_archives_are_rejected(self):
|
||||||
self.assertFalse(_cap_info_body({})['ok'])
|
self.assertFalse(_cap_info_body({})['ok'])
|
||||||
|
|||||||
Reference in New Issue
Block a user