feat: approximate NVRAM requirement from the CAP analysis (v3.5.15)

GlobalPlatform models this explicitly (GP Card Spec v2.3.1 Table 11-48,
load parameters): C6 = non-volatile code, C7 = volatile data, C8 =
non-volatile data, and 11.5.2.3.7 - when the card makes no code/data
distinction the required minimum is C6 + C8.  The analysis now reports it:

- capmem.memory_json() accepts the load file size (all CAP components -
  the package image the card stores) and returns code.load_file,
  nvram.requirement = load_file + persistent data, and sets the suggested
  C6 to the load file (the bytecode-only Method.cap figure stays in
  code.method_component); _cap_info_body passes the size it already
  computed.
- The CAP estimate box (both the RAM installer and the SCP81
  Install-from-.cap form) leads with "NVRAM requirement ≈ code image +
  data" plus the RAM estimate, keeps the bytecode/other-component split and
  the full data breakdown in Details, and carries the GP citation with the
  caveats (card memory management, allocation rounding and the registry
  entry are not included; the static field image appears in both parts).
- An older server response without code.load_file still renders (the
  bytecode size is used as the fallback).

Tests: Python +1 (load-file semantics) with extended cap-info assertions,
frontend +1 (renderer + fallback).  Help EN/RU, docs/api.md, AGENTS.

587 frontend / 472 Python green; version 3.5.15; sw simple-v268.
This commit is contained in:
2026-09-27 02:14:49 +03:00
parent 2d2a40a73c
commit 18e0db75a9
10 changed files with 112 additions and 28 deletions
+13 -2
View File
@@ -349,9 +349,10 @@ used for installation.
"package_aid": "AA1902BC226001", "package_aid": "AA1902BC226001",
"applet_count": 1, "applets": ["AA1902BC226001"], "applet_count": 1, "applets": ["AA1902BC226001"],
"class_count": 3, "method_count": 12, "class_count": 3, "method_count": 12,
"code": {"method_component": 850}, "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,
"header_overhead": 24, "ref_storage": 8, "total": 148, "runtime": 0}, "header_overhead": 24, "ref_storage": 8, "total": 148, "runtime": 0,
"requirement": 1382},
"ram": {"transient_arrays": 16, "runtime_transient": 0, "peak_frame": 8, "total": 24}, "ram": {"transient_arrays": 16, "runtime_transient": 0, "peak_frame": 8, "total": 24},
"suggested": {"c6": 850, "c7": 272, "c8": 148}, "suggested": {"c6": 850, "c7": 272, "c8": 148},
"warnings": [] "warnings": []
@@ -359,6 +360,16 @@ used for installation.
} }
``` ```
`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
tool's bytecode-only figure stays in `code.method_component`. The suggested
`C6` follows the load file, and `nvram.requirement` = `code.load_file` +
`nvram.total` (the C6+C8-style total per §11.5.2.3.7: with no code/data
split in the card's memory the required minimum is the sum of both). The
estimate excludes card-specific memory management, allocation rounding and
the GP registry entry.
**Errors** (HTTP 200 with `ok: false`, like `/api/scp81/gen-install`): **Errors** (HTTP 200 with `ok: false`, like `/api/scp81/gen-install`):
`{"ok": false, "error": "cap parse failed: File is not a zip file"}` for a `{"ok": false, "error": "cap parse failed: File is not a zip file"}` for a
corrupt/wrong archive, or `cap analysis failed: …` when a component passes corrupt/wrong archive, or `cap analysis failed: …` when a component passes
+2 -2
View File
@@ -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] &rarr; LOAD &times;N &rarr; INSTALL[for install (+make selectable)]. Load-файл делится на LOAD APDU размера <strong>размер блока LOAD</strong> (1&ndash;240 байт полезной нагрузки, по умолчанию 240); каждый защищённый пакет доставляется одним SMS или, если он больше, конкатенированной SMS-PP загрузкой до 5 сегментов, так что большой <code>.cap</code> просто занимает несколько SMS. Сразу после выбора файл проверяется и в форме показывается <strong>оценка требований к памяти</strong> (код / NVRAM / 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] &rarr; LOAD &times;N &rarr; INSTALL[for install (+make selectable)]. Load-файл делится на LOAD APDU размера <strong>размер блока LOAD</strong> (1&ndash;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>
</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] &rarr; LOAD &times;N &rarr; INSTALL [for install]. Файл используется только для генерации APDU &mdash; он не сохраняется, как и его имя. При выборе файл проверяется и показывается оценка требований к памяти (код / 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] &rarr; LOAD &times;N &rarr; INSTALL [for install]. Файл используется только для генерации APDU &mdash; он не сохраняется, как и его имя. При выборе файл проверяется и показывается оценка требований к памяти (требование NVRAM как сумма образа кода и постоянных данных, плюс RAM); <strong>Сгенерировать</strong> недоступна до успешного анализа.</li>
<li><strong>Delete AID</strong> — список AID (по одному в строке) и P2 (<em>object only</em> / <em>object and related objects</em>) &rarr; APDU DELETE.</li> <li><strong>Delete AID</strong> — список AID (по одному в строке) и P2 (<em>object only</em> / <em>object and related objects</em>) &rarr; 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>&laquo;В очередь SCP81&raquo;</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>&laquo;В очередь SCP81&raquo;</strong>.</p>
+2 -2
View File
@@ -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] &rarr; LOAD &times;N &rarr; INSTALL[for install (+make selectable)]. The load file is split into LOAD APDUs of the <strong>LOAD block size</strong> (1&ndash;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> (code / NVRAM / RAM) in the form; 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] &rarr; LOAD &times;N &rarr; INSTALL[for install (+make selectable)]. The load file is split into LOAD APDUs of the <strong>LOAD block size</strong> (1&ndash;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>
</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] &rarr; LOAD &times;N &rarr; INSTALL [for install]. The file is only used to generate the APDUs &mdash; it is not stored, not even its name. On selection it is validated and its memory requirements are estimated (code / NVRAM / 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] &rarr; LOAD &times;N &rarr; INSTALL [for install]. The file is only used to generate the APDUs &mdash; 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>Delete AID</strong> — AID list (one per line) and P2 (<em>object only</em> / <em>object and related objects</em>) &rarr; 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>) &rarr; 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>
+28 -5
View File
@@ -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.14'; const SIMPLE_VERSION = '3.5.15';
document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION; document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION;
// ===== Tab switching ===== // ===== Tab switching =====
@@ -8025,21 +8025,36 @@ function capMemHtml(mem) {
if (!mem) return ''; if (!mem) return '';
const nv = mem.nvram || {}, ram = mem.ram || {}, code = mem.code || {}, sug = mem.suggested || {}; const nv = mem.nvram || {}, ram = mem.ram || {}, code = mem.code || {}, sug = mem.suggested || {};
const hex2 = v => (Number(v) || 0).toString(16).toUpperCase().padStart(4, '0'); const hex2 = v => (Number(v) || 0).toString(16).toUpperCase().padStart(4, '0');
const methodBytes = Number(code.method_component) || 0;
// older server responses carry only the bytecode size: fall back to it
const loadFile = Number(code.load_file) || methodBytes;
const nvramData = Number(nv.total) || 0;
const requirement = Number(nv.requirement) || (loadFile + nvramData);
const otherComponents = Math.max(0, loadFile - methodBytes);
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>';
html += '<div>' + esc(t('Code (Method.cap):')) + ' ' + esc(capMemBytes(code.method_component)) + // the NVRAM requirement is a C6+C8-style total (GP Card Spec v2.3.1
' · ' + esc(t('Persistent (NVRAM):')) + ' ' + esc(capMemBytes(nv.total)) + // Table 11-48): the package image (load file) plus the persistent data
html += '<div>' + esc(t('NVRAM requirement ≈')) + ' ' + esc(capMemBytes(requirement)) +
' (' + esc(t('code image')) + ' ' + esc(capMemBytes(loadFile)) +
' + ' + 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>';
html += '<div>' + esc(t('Static image:')) + ' ' + esc(capMemBytes(nv.static_image)) + html += '<div>' + esc(t('Code image (load file):')) + ' ' + esc(capMemBytes(loadFile)) +
' · ' + esc(t('bytecode (Method.cap):')) + ' ' + esc(capMemBytes(methodBytes)) +
' · ' + esc(t('other components:')) + ' ' + esc(capMemBytes(otherComponents)) + '</div>';
html += '<div>' + esc(t('Persistent data (NVRAM):')) + ' ' + esc(capMemBytes(nvramData)) +
' · ' + esc(t('Static image:')) + ' ' + esc(capMemBytes(nv.static_image)) +
' · ' + esc(t('Static array init:')) + ' ' + esc(capMemBytes(nv.array_init)) + ' · ' + esc(t('Static array init:')) + ' ' + esc(capMemBytes(nv.array_init)) +
' · ' + esc(t('Install-time objects:')) + ' ' + esc(capMemBytes(nv.install_objects)) + ' · ' + esc(t('Install-time objects:')) + ' ' + esc(capMemBytes(nv.install_objects)) +
' · ' + esc(t('Object headers:')) + ' ' + esc(capMemBytes(nv.header_overhead)) + ' · ' + esc(t('Object headers:')) + ' ' + esc(capMemBytes(nv.header_overhead)) +
' · ' + esc(t('Reference storage:')) + ' ' + esc(capMemBytes(nv.ref_storage)) + '</div>'; ' · ' + esc(t('Reference storage:')) + ' ' + esc(capMemBytes(nv.ref_storage)) + '</div>';
html += '<div>' + esc(t('Transient arrays:')) + ' ' + esc(capMemBytes(ram.transient_arrays)) + html += '<div>' + esc(t('RAM (volatile):')) + ' ' + esc(capMemBytes(ram.total)) +
' · ' + esc(t('Transient arrays:')) + ' ' + esc(capMemBytes(ram.transient_arrays)) +
' · ' + esc(t('Runtime transient:')) + ' ' + esc(capMemBytes(ram.runtime_transient)) + ' · ' + esc(t('Runtime transient:')) + ' ' + esc(capMemBytes(ram.runtime_transient)) +
' · ' + esc(t('Peak method frame:')) + ' ' + esc(capMemBytes(ram.peak_frame)) + '</div>'; ' · ' + esc(t('Peak method frame:')) + ' ' + esc(capMemBytes(ram.peak_frame)) + '</div>';
html += '<div>' + esc(t('Suggested install quotas (informational):')) + html += '<div>' + esc(t('Suggested install quotas (informational):')) +
' C6=' + esc(String(Number(sug.c6) || 0)) + ' C7=0x' + esc(hex2(sug.c7)) + ' C8=0x' + esc(hex2(sug.c8)) + '</div>'; ' C6=' + esc(String(Number(sug.c6) || 0)) + ' C7=0x' + esc(hex2(sug.c7)) + ' C8=0x' + esc(hex2(sug.c8)) + '</div>';
html += '<div class="text-gray-400">' + esc(t('NVRAM = code image + persistent data (GP Card Spec v2.3.1 Table 11-48, C6+C8 style); card memory management, rounding and the registry entry are not included, and the static field image appears in both parts.')) + '</div>';
html += '<div class="text-gray-400">' + esc(t('Estimate only — memory values are not used for installation')) + '</div>'; html += '<div class="text-gray-400">' + esc(t('Estimate only — memory values are not used for installation')) + '</div>';
html += '</details>'; html += '</details>';
(mem.warnings || []).forEach(w => { (mem.warnings || []).forEach(w => {
@@ -16012,6 +16027,14 @@ const LANG_RU = {
'Retry': 'Повторить', 'Retry': 'Повторить',
'Analyze the CAP file first': 'Сначала проанализируйте CAP-файл', 'Analyze the CAP file first': 'Сначала проанализируйте CAP-файл',
'CAP requirements (estimate)': 'Требования CAP (оценка)', 'CAP requirements (estimate)': 'Требования CAP (оценка)',
'NVRAM requirement ≈': 'Требование NVRAM ≈',
'code image': 'образ кода',
'data': 'данные',
'Code image (load file):': 'Образ кода (load-файл):',
'bytecode (Method.cap):': 'байткод (Method.cap):',
'other components:': 'прочие компоненты:',
'Persistent data (NVRAM):': 'Постоянные данные (NVRAM):',
'NVRAM = code image + persistent data (GP Card Spec v2.3.1 Table 11-48, C6+C8 style); card memory management, rounding and the registry entry are not included, and the static field image appears in both parts.': 'NVRAM = образ кода + постоянные данные (GP Card Spec v2.3.1 Table 11-48, стиль C6+C8); управление памятью карты, округление и запись в реестре не учтены, а образ статических полей входит в обе части.',
'Code (Method.cap):': 'Код (Method.cap):', 'Code (Method.cap):': 'Код (Method.cap):',
'Persistent (NVRAM):': 'Энергонезависимая память (NVRAM):', 'Persistent (NVRAM):': 'Энергонезависимая память (NVRAM):',
'RAM (volatile):': 'ОЗУ (volatile):', 'RAM (volatile):': 'ОЗУ (volatile):',
+1 -1
View File
@@ -1,4 +1,4 @@
const CACHE = 'simple-v267'; const CACHE = 'simple-v268';
const URLS = [ const URLS = [
'index.html', 'index.html',
'help.html', 'help.html',
+20 -7
View File
@@ -48,10 +48,10 @@ function resetState() {
function memFixture() { function memFixture() {
return { return {
code: { method_component: 2048 }, 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 }, 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 },
suggested: { c6: 2048, c7: 272, c8: 136 }, suggested: { c6: 3000, c7: 272, c8: 136 },
warnings: [], warnings: [],
}; };
} }
@@ -64,23 +64,36 @@ test('capMemBytes formats bytes and kilobytes', () => {
assert.strictEqual(capMemBytes(null), '0 B'); assert.strictEqual(capMemBytes(null), '0 B');
}); });
test('capMemHtml renders the summary, breakdown and warnings', () => { test('capMemHtml renders the NVRAM requirement, breakdown and warnings', () => {
const mem = memFixture(); const mem = memFixture();
mem.warnings = ['method class[0].token[1]: unknown opcode 0xAA, scan stopped']; mem.warnings = ['method class[0].token[1]: unknown opcode 0xAA, scan stopped'];
const out = capMemHtml(mem); const out = capMemHtml(mem);
assert.ok(out.includes('CAP requirements (estimate)'), out); assert.ok(out.includes('CAP requirements (estimate)'), out);
assert.ok(out.includes('Code (Method.cap): 2.0 kB'), out); // C6+C8-style total: load file + persistent data
assert.ok(out.includes('Persistent (NVRAM): 136 B'), out); assert.ok(out.includes('NVRAM requirement ≈ 3.1 kB (code image 2.9 kB + data 136 B)'), out);
assert.ok(out.includes('RAM (volatile): 24 B'), out); assert.ok(out.includes('RAM (volatile): 24 B'), out);
assert.ok(out.includes('Code image (load file): 2.9 kB'), out);
assert.ok(out.includes('bytecode (Method.cap): 2.0 kB'), out);
assert.ok(out.includes('other components: 952 B'), out);
assert.ok(out.includes('Persistent data (NVRAM): 136 B'), out);
assert.ok(out.includes('Static image: 12 B'), out); assert.ok(out.includes('Static image: 12 B'), out);
assert.ok(out.includes('Reference storage: 8 B'), out); assert.ok(out.includes('Reference storage: 8 B'), out);
assert.ok(out.includes('Peak method frame: 8 B'), out); assert.ok(out.includes('Peak method frame: 8 B'), out);
assert.ok(out.includes('C6=2048 C7=0x0110 C8=0x0088'), out); assert.ok(out.includes('C6=3000 C7=0x0110 C8=0x0088'), 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);
assert.strictEqual(capMemHtml(null), ''); assert.strictEqual(capMemHtml(null), '');
}); });
test('capMemHtml falls back to the bytecode size on older server responses', () => {
const mem = memFixture();
delete mem.code.load_file;
delete mem.nvram.requirement;
const out = capMemHtml(mem);
assert.ok(out.includes('NVRAM requirement ≈ 2.1 kB (code image 2.0 kB + data 136 B)'), out);
});
test('capGateOk gates the RAM install op and the scripts form', () => { test('capGateOk gates the RAM install op and the scripts form', () => {
resetState(); resetState();
globalThis.document = { getElementById: id => (id === 'ram-op' ? { value: 'install-cap' } : null) }; globalThis.document = { getElementById: id => (id === 'ram-op' ? { value: 'install-cap' } : null) };
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "pysim-simple-server" name = "pysim-simple-server"
version = "3.5.14" version = "3.5.15"
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.
+23 -6
View File
@@ -1629,11 +1629,24 @@ def compute_memory(report):
return result return result
def memory_json(report, memory): def memory_json(report, memory, load_file_bytes=None):
"""Compact, stable JSON shape for the API/PWA (all byte counts).""" """Compact, stable JSON shape for the API/PWA (all byte counts).
``load_file_bytes`` is the concatenated CAP load file (all components) -
the package image the card stores, our proxy for the GP Card Spec v2.3.1
Table 11-48 "non-volatile code" minimum memory requirement. With it the
suggested C6 becomes the load file and ``nvram.requirement`` reports the
C6+C8-style total (code image + persistent data, per 11.5.2.3.7: "If both
tags 'C6' and 'C8' are present and the implementation does not make any
distinction between Non-Volatile Code and Non-Volatile Data Memory then
the required minimum shall be the sum of both values"); without it the
tool's bytecode-only C6 is kept."""
ram_transient = memory.get('ram_transient_arrays', 0) ram_transient = memory.get('ram_transient_arrays', 0)
ram_runtime = memory.get('runtime_transient_bytes', 0) ram_runtime = memory.get('runtime_transient_bytes', 0)
ram_frame = memory.get('ram_frame_bytes', 0) ram_frame = memory.get('ram_frame_bytes', 0)
nvram_total = memory.get('nvram_with_ref_storage', 0)
load_file = max(0, int(load_file_bytes or 0))
method_component = report.get('method_component_size', 0)
return { return {
'cap_version': report.get('cap_version'), 'cap_version': report.get('cap_version'),
'package_version': report.get('package_version'), 'package_version': report.get('package_version'),
@@ -1643,7 +1656,8 @@ def memory_json(report, memory):
'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': {
'method_component': report.get('method_component_size', 0), 'method_component': method_component,
'load_file': load_file,
}, },
'nvram': { 'nvram': {
'static_image': memory.get('nvram_static_image', 0), 'static_image': memory.get('nvram_static_image', 0),
@@ -1651,8 +1665,11 @@ def memory_json(report, memory):
'install_objects': memory.get('nvram_persistent_objects', 0), 'install_objects': memory.get('nvram_persistent_objects', 0),
'header_overhead': memory.get('nvram_object_header_overhead', 0), 'header_overhead': memory.get('nvram_object_header_overhead', 0),
'ref_storage': memory.get('reference_storage', 0), 'ref_storage': memory.get('reference_storage', 0),
'total': memory.get('nvram_with_ref_storage', 0), 'total': nvram_total,
'runtime': memory.get('runtime_persistent_bytes', 0), 'runtime': memory.get('runtime_persistent_bytes', 0),
# C6+C8-style total: package image + persistent data (None when
# the caller did not provide the load file size)
'requirement': (load_file + nvram_total) if load_file else None,
}, },
'ram': { 'ram': {
'transient_arrays': ram_transient, 'transient_arrays': ram_transient,
@@ -1661,9 +1678,9 @@ def memory_json(report, memory):
'total': ram_transient + ram_runtime + ram_frame, 'total': ram_transient + ram_runtime + ram_frame,
}, },
'suggested': { 'suggested': {
'c6': report.get('method_component_size', 0), 'c6': load_file or method_component,
'c7': ram_transient + 256, 'c7': ram_transient + 256,
'c8': memory.get('nvram_with_ref_storage', 0), 'c8': nvram_total,
}, },
'warnings': list(report.get('warnings', [])), 'warnings': list(report.get('warnings', [])),
} }
+4 -2
View File
@@ -31,7 +31,7 @@ from osmocom.tlv import BER_TLV_IE
from osmocom.utils import rpad from osmocom.utils import rpad
VERSION = '3.5.14' VERSION = '3.5.15'
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
@@ -2564,7 +2564,9 @@ def _cap_info_body(body):
return {'ok': False, 'error': 'cap parse failed: %s' % e} return {'ok': False, 'error': 'cap parse failed: %s' % e}
try: try:
report, memory = capmem.analyze_bytes(bytes.fromhex(cap_hex)) report, memory = capmem.analyze_bytes(bytes.fromhex(cap_hex))
info = capmem.memory_json(report, memory) # the load file (all components) is our proxy for the GP "non-volatile
# code" requirement, so the JSON can report the C6+C8-style total
info = capmem.memory_json(report, memory, load_file_bytes=len(loadfile_data) // 2)
except Exception as e: except Exception as e:
return {'ok': False, 'error': 'cap analysis failed: %s' % e} return {'ok': False, 'error': 'cap analysis failed: %s' % e}
return {'ok': True, 'load_file_aid': loadfile_aid, 'module_aid': module_aid, return {'ok': True, 'load_file_aid': loadfile_aid, 'module_aid': module_aid,
+18
View File
@@ -198,6 +198,18 @@ class TestCapAnalyzer(unittest.TestCase):
self.assertEqual(info['suggested']['c8'], memory['nvram_with_ref_storage']) self.assertEqual(info['suggested']['c8'], memory['nvram_with_ref_storage'])
self.assertEqual(info['warnings'], []) self.assertEqual(info['warnings'], [])
self.assertIn('applets', info) self.assertIn('applets', info)
# without the load file the tool's bytecode-only C6 is kept and no
# C6+C8-style total is reported
self.assertEqual(info['code']['load_file'], 0)
self.assertIsNone(info['nvram']['requirement'])
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)
self.assertEqual(info['code']['load_file'], 1000)
self.assertEqual(info['suggested']['c6'], 1000)
self.assertEqual(info['nvram']['requirement'],
1000 + memory['nvram_with_ref_storage'])
def test_unknown_opcode_stops_the_scan_with_a_warning(self): def test_unknown_opcode_stops_the_scan_with_a_warning(self):
bytecode = _method_header(1, 0, 0) + b'\xee' # 0xEE is not a JC 2.1 opcode bytecode = _method_header(1, 0, 0) + b'\xee' # 0xEE is not a JC 2.1 opcode
@@ -224,6 +236,12 @@ class TestCapInfoBody(unittest.TestCase):
self.assertTrue(resp['load_file_bytes'] > 0) self.assertTrue(resp['load_file_bytes'] > 0)
self.assertEqual(resp['memory']['package_aid'], '0102030405') self.assertEqual(resp['memory']['package_aid'], '0102030405')
self.assertTrue(resp['memory']['nvram']['total'] > 0) self.assertTrue(resp['memory']['nvram']['total'] > 0)
# the endpoint feeds the load file size in: C6 = package image and
# nvram.requirement = code image + persistent data (GP C6+C8 style)
self.assertEqual(resp['memory']['code']['load_file'], resp['load_file_bytes'])
self.assertEqual(resp['memory']['suggested']['c6'], resp['load_file_bytes'])
self.assertEqual(resp['memory']['nvram']['requirement'],
resp['load_file_bytes'] + resp['memory']['nvram']['total'])
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'])