diff --git a/docs/api.md b/docs/api.md index 9ea387f..688e69f 100644 --- a/docs/api.md +++ b/docs/api.md @@ -349,9 +349,10 @@ used for installation. "package_aid": "AA1902BC226001", "applet_count": 1, "applets": ["AA1902BC226001"], "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, - "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}, "suggested": {"c6": 850, "c7": 272, "c8": 148}, "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`): `{"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 diff --git a/frontend/help-ru.html b/frontend/help-ru.html index 1cb37f4..1961618 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 / 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; повреждённый файл не проходит анализ, и 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 a2ac31e..e6ff790 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 (code / NVRAM / RAM) in the form; 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; 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 7e41e8b..81870cf 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.14'; +const SIMPLE_VERSION = '3.5.15'; document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION; // ===== Tab switching ===== @@ -8025,21 +8025,36 @@ function capMemHtml(mem) { if (!mem) return ''; 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 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 = '
' + esc(t('CAP requirements (estimate)')) + '
'; - html += '
' + esc(t('Code (Method.cap):')) + ' ' + esc(capMemBytes(code.method_component)) + - ' · ' + esc(t('Persistent (NVRAM):')) + ' ' + esc(capMemBytes(nv.total)) + + // 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)) + + ' (' + esc(t('code image')) + ' ' + esc(capMemBytes(loadFile)) + + ' + ' + esc(t('data')) + ' ' + esc(capMemBytes(nvramData)) + ')' + ' · ' + esc(t('RAM (volatile):')) + ' ' + esc(capMemBytes(ram.total)) + '
'; html += '
' + esc(t('Details')) + ''; - html += '
' + esc(t('Static image:')) + ' ' + esc(capMemBytes(nv.static_image)) + + 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)) + '
'; + html += '
' + 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('Install-time objects:')) + ' ' + esc(capMemBytes(nv.install_objects)) + ' · ' + esc(t('Object headers:')) + ' ' + esc(capMemBytes(nv.header_overhead)) + ' · ' + esc(t('Reference storage:')) + ' ' + esc(capMemBytes(nv.ref_storage)) + '
'; - html += '
' + esc(t('Transient arrays:')) + ' ' + esc(capMemBytes(ram.transient_arrays)) + + html += '
' + 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('Peak method frame:')) + ' ' + esc(capMemBytes(ram.peak_frame)) + '
'; html += '
' + 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)) + '
'; + html += '
' + 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.')) + '
'; html += '
' + esc(t('Estimate only — memory values are not used for installation')) + '
'; html += '
'; (mem.warnings || []).forEach(w => { @@ -16012,6 +16027,14 @@ const LANG_RU = { 'Retry': 'Повторить', 'Analyze the CAP file first': 'Сначала проанализируйте 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):', 'Persistent (NVRAM):': 'Энергонезависимая память (NVRAM):', 'RAM (volatile):': 'ОЗУ (volatile):', diff --git a/frontend/sw.js b/frontend/sw.js index 6462e17..dca5e1b 100644 --- a/frontend/sw.js +++ b/frontend/sw.js @@ -1,4 +1,4 @@ -const CACHE = 'simple-v267'; +const CACHE = 'simple-v268'; const URLS = [ 'index.html', 'help.html', diff --git a/frontend/tests/capmem.test.js b/frontend/tests/capmem.test.js index eabee96..3a26f64 100644 --- a/frontend/tests/capmem.test.js +++ b/frontend/tests/capmem.test.js @@ -48,10 +48,10 @@ function resetState() { function memFixture() { return { - code: { method_component: 2048 }, - nvram: { static_image: 12, array_init: 4, install_objects: 100, header_overhead: 12, ref_storage: 8, total: 136, runtime: 0 }, + 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 }, - suggested: { c6: 2048, c7: 272, c8: 136 }, + suggested: { c6: 3000, c7: 272, c8: 136 }, warnings: [], }; } @@ -64,23 +64,36 @@ test('capMemBytes formats bytes and kilobytes', () => { 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(); mem.warnings = ['method class[0].token[1]: unknown opcode 0xAA, scan stopped']; const out = capMemHtml(mem); assert.ok(out.includes('CAP requirements (estimate)'), out); - assert.ok(out.includes('Code (Method.cap): 2.0 kB'), out); - assert.ok(out.includes('Persistent (NVRAM): 136 B'), out); + // C6+C8-style total: load file + persistent data + 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('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('Reference storage: 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('unknown opcode 0xAA'), out); 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', () => { resetState(); globalThis.document = { getElementById: id => (id === 'ram-op' ? { value: 'install-cap' } : null) }; diff --git a/pyproject.toml b/pyproject.toml index 59e1fdf..4c4ffd5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "pysim-simple-server" -version = "3.5.14" +version = "3.5.15" 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 fe6f608..41e95a6 100755 --- a/pysim_simple_server/capmem.py +++ b/pysim_simple_server/capmem.py @@ -1629,11 +1629,24 @@ def compute_memory(report): return result -def memory_json(report, memory): - """Compact, stable JSON shape for the API/PWA (all byte counts).""" +def memory_json(report, memory, load_file_bytes=None): + """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_runtime = memory.get('runtime_transient_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 { 'cap_version': report.get('cap_version'), 'package_version': report.get('package_version'), @@ -1643,7 +1656,8 @@ def memory_json(report, memory): 'class_count': report.get('class_count', 0), 'method_count': report.get('method_count', 0), 'code': { - 'method_component': report.get('method_component_size', 0), + 'method_component': method_component, + 'load_file': load_file, }, 'nvram': { '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), 'header_overhead': memory.get('nvram_object_header_overhead', 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), + # 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': { 'transient_arrays': ram_transient, @@ -1661,9 +1678,9 @@ def memory_json(report, memory): 'total': ram_transient + ram_runtime + ram_frame, }, 'suggested': { - 'c6': report.get('method_component_size', 0), + 'c6': load_file or method_component, 'c7': ram_transient + 256, - 'c8': memory.get('nvram_with_ref_storage', 0), + 'c8': nvram_total, }, 'warnings': list(report.get('warnings', [])), } diff --git a/pysim_simple_server/server.py b/pysim_simple_server/server.py index 6cd1e6c..cb617cd 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.14' +VERSION = '3.5.15' 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} try: 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: return {'ok': False, 'error': 'cap analysis failed: %s' % e} return {'ok': True, 'load_file_aid': loadfile_aid, 'module_aid': module_aid, diff --git a/tests/test_cap_memory.py b/tests/test_cap_memory.py index 23b4b30..31e99c3 100644 --- a/tests/test_cap_memory.py +++ b/tests/test_cap_memory.py @@ -198,6 +198,18 @@ class TestCapAnalyzer(unittest.TestCase): self.assertEqual(info['suggested']['c8'], memory['nvram_with_ref_storage']) self.assertEqual(info['warnings'], []) 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): 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.assertEqual(resp['memory']['package_aid'], '0102030405') 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): self.assertFalse(_cap_info_body({})['ok'])