v3.29.0: the batch kinds (delete / reinstall) + the delete-package pre-step
The batch plan is a list of named steps ({name, apdu, delete}) now and
_ram_install_run generalized into _ram_plan_run; POST /api/ram-batch/start
dispatches on kind:
- install-cap (default; delete_package prepends a cascade DELETE on the
package AID - a leftover package blocks INSTALL [for load]),
- reinstall-app (_ram_reinstall_plan: DELETE the applet instance + INSTALL
[for install] from the loaded package - no .cap, no LOAD),
- delete (_ram_delete_plan: one GP DELETE, aid + target app/package ->
P2=00/P2=80).
A delete step tolerates 'not found' (6A88/6A82 - the object is already
gone, the goal is met); any other delete failure stops the chain with the
delete's reason. The policy serves all three kinds and the single
/api/ram-install-app, which now deletes the instance first (make
selectable stays one step); /api/ram-install takes delete_package too.
The status/summary carry the run's kind (the panel labels follow:
installed/deleted/reinstalled) and each attempt carries the last three
steps (steps + steps_total) for the panel's per-step detail. The PWA's
three batch entries are grouped at the end of the operation selector (an
optgroup); ramIsBatchOp() is the one source for "per-card batch".
Docs: help EN/RU, api.md, the repo AGENTS. Tests: the plan builders per
kind, the validations, the delete-package prepend, the not-found
tolerance, the kind dispatch/status/notice, the steps tail, the endpoint
steps, the PWA wiring. 925 frontend + 943 Python; sw cache simple-v447.
This commit is contained in:
+57
-30
@@ -45,12 +45,12 @@ a 3.x PWA).
|
||||
| `/api/esim/profile` | POST | Enable/disable an eSIM profile (card re-initialized after the switch) |
|
||||
| `/api/help` | POST | pySim help for a given command |
|
||||
| `/api/send-ota` | POST | SCP80 OTA secured packet delivery |
|
||||
| `/api/ram-install` | POST | Install a Java Card `.cap` file via SCP80 (INSTALL[for load] → LOAD ×N → INSTALL[for install]) |
|
||||
| `/api/ram-batch/start` | POST | Start the **batch CAP install** (v3.28.0): the server installs the same `.cap` on every card that gets equipped, using each card's preset (matched by ICCID) |
|
||||
| `/api/ram-install` | POST | Install a Java Card `.cap` file via SCP80 (INSTALL[for load] → LOAD ×N → INSTALL[for install]); `delete_package` prepends a cascade DELETE on the package AID |
|
||||
| `/api/ram-batch/start` | POST | Start a **batch operation** (v3.28.0; kinds v3.29.0): `install-cap` (the same `.cap` per equipped card), `reinstall-app` (DELETE + INSTALL [for install]) or `delete` (one GP DELETE), using each card's preset (matched by ICCID) |
|
||||
| `/api/ram-batch/status` | GET | The batch state: phase, current card, counts and the attempts report (card-free) |
|
||||
| `/api/ram-batch/stop` | POST | Stop the batch — a running install finishes first (card-free, so it works while the install holds the card lock) |
|
||||
| `/api/ram-batch/clear` | POST | Drop the finished batch's report (`409` while a batch is active) |
|
||||
| `/api/ram-install-app` | POST | Single INSTALL [for install] / [for make selectable] for an already loaded package (no re-load) |
|
||||
| `/api/ram-install-app` | POST | INSTALL [for install] (deletes the instance first, tolerating 'not found') / INSTALL [for make selectable] for an already loaded package (no re-load) |
|
||||
| `/api/cap-info` | POST | Validate a `.cap` archive and estimate its code/NVRAM/RAM requirements (read-only) |
|
||||
| `/api/cap-compat` | POST | CAP compatibility test: LOAD only to the block completing the Import component (nothing committed) |
|
||||
| `/api/test/run` | POST | Start a test script (actions + proactive expectations; `script` inline or `script_id` from the store) or a test suite (`suite_id`; ordered scripts, one card session) |
|
||||
@@ -1028,7 +1028,7 @@ Clears a finished run report (409 while a run is active).
|
||||
|
||||
### `POST /api/ram-install`
|
||||
|
||||
Install a Java Card `.cap` file on the card via GlobalPlatform commands (INSTALL[for load] → LOAD ×N → INSTALL[for install (+ make selectable)]) wrapped in SCP80 secured packets. Each step is sent via ENVELOPE; the PoR verdict and the remote command's own status word are both checked and the sequence aborts on the first failure (a non-`por_ok` PoR, a remote SW outside the success set, or an undecodable PoR). The counter advances only for a packet the card accepted (PoR `por_ok`); `final_cntr` is returned on success **and** on failure, so the caller keeps the card's consumed counter (a rejected packet leaves it unchanged). A missing `cntr` is seeded from the preset's keyset (v3.20.0); an explicit value wins but never lowers the stored counter (only forward moves are persisted). The RAM command format is detected per operation: a read-only `GET STATUS [ISD]` probe (`FORMAT CHECK (compact)`, then `FORMAT CHECK (expanded)`) decides between the compact C-APDU and the expanded `AA`/`22` form (TS 102 226 §5.2.1), and every INSTALL/LOAD step of the chain then uses the detected format — the response carries `ram_format`. Pass `ram_format` (`compact`/`expanded`) to pin it. The **NV footprint** is measured around the chain: a best-effort `GET DATA FF21` (Extended Card Resources, TS 102 226 §8.2.1.7.2) read before the first step and again after the last one (also on failure) reports the free non-volatile memory and its delta — the read is silent (no step record, no failure) and a card without FF21 simply yields no fields. The `.cap` archive (a ZIP of nested components) is parsed server-side in `_cap_parse`; no external tooling is required.
|
||||
Install a Java Card `.cap` file on the card via GlobalPlatform commands (INSTALL[for load] → LOAD ×N → INSTALL[for install (+ make selectable)]) wrapped in SCP80 secured packets. Each step is sent via ENVELOPE; the PoR verdict and the remote command's own status word are both checked and the sequence aborts on the first failure (a non-`por_ok` PoR, a remote SW outside the success set, or an undecodable PoR). The counter advances only for a packet the card accepted (PoR `por_ok`); `final_cntr` is returned on success **and** on failure, so the caller keeps the card's consumed counter (a rejected packet leaves it unchanged). A missing `cntr` is seeded from the preset's keyset (v3.20.0); an explicit value wins but never lowers the stored counter (only forward moves are persisted). The RAM command format is detected per operation: a read-only `GET STATUS [ISD]` probe (`FORMAT CHECK (compact)`, then `FORMAT CHECK (expanded)`) decides between the compact C-APDU and the expanded `AA`/`22` form (TS 102 226 §5.2.1), and every INSTALL/LOAD step of the chain then uses the detected format — the response carries `ram_format`. Pass `ram_format` (`compact`/`expanded`) to pin it. The **NV footprint** is measured around the chain: a best-effort `GET DATA FF21` (Extended Card Resources, TS 102 226 §8.2.1.7.2) read before the first step and again after the last one (also on failure) reports the free non-volatile memory and its delta — the read is silent (no step record, no failure) and a card without FF21 simply yields no fields. The `.cap` archive (a ZIP of nested components) is parsed server-side in `_cap_parse`; no external tooling is required. `delete_package: true` prepends a **cascade GP DELETE** (`P2=0x80`) on the package AID — a leftover package blocks `INSTALL [for load]`, so a re-install can clear it first; the delete step tolerates 'not found' (`6A88`/`6A82`, a fresh card) and any other delete failure stops the chain with its reason (the same policy as the batch and `/api/ram-install-app`).
|
||||
|
||||
**Request body:**
|
||||
```json
|
||||
@@ -1060,6 +1060,7 @@ Install a Java Card `.cap` file on the card via GlobalPlatform commands (INSTALL
|
||||
| `load_block_size` | no | Bytes of load-file payload per LOAD APDU, 1–240 (default 240 when omitted). SCP80 concatenation carries a secured packet larger than one SMS over up to 5 SMs, so the block size is no longer clamped to fit a single SMS. |
|
||||
| `ram_format` | no | RAM command format: `auto` (default — the read-only probe decides, recorded as `FORMAT CHECK (...)` steps), `compact` or `expanded` to pin it |
|
||||
| `includeCpi` | no | As in `/api/send-ota`: `true` (default) marks the delivered SMS with the OTA CPI IE; `false` drops it (the full 140-octet single-SM budget then applies) |
|
||||
| `delete_package` | no | If true, prepend a cascade DELETE on the package AID (`P2=0x80` — the load file with its modules and installed applications). The delete step tolerates 'not found' (a fresh card proceeds); any other delete failure stops the chain |
|
||||
|
||||
**Install-parameter composition (TS 102 226 §8.2.1.3.2.1; GPD_SPE_013 v1.1
|
||||
Table 6-5).** The memory quotas (`C7`/`C8`) and an EF-form STK part (the CA)
|
||||
@@ -1133,43 +1134,65 @@ The `steps` array contains one entry per GP command. `final_cntr` is the counter
|
||||
|
||||
### `POST /api/ram-batch/start`, `GET /api/ram-batch/status`, `POST /api/ram-batch/stop`, `POST /api/ram-batch/clear`
|
||||
|
||||
The **batch CAP install** (v3.28.0): one `.cap` and one set of install
|
||||
parameters applied to card after card, for bulk provisioning. The loop runs
|
||||
**in the server** (a worker thread), so a page reload or a closed tab does not
|
||||
interrupt it; the state lives in memory and a server restart ends the run.
|
||||
The **batch operations** (v3.28.0; the `reinstall-app`/`delete` kinds
|
||||
v3.29.0): one operation applied to card after card, for bulk provisioning or
|
||||
repair. The loop runs **in the server** (a worker thread), so a page reload or
|
||||
a closed tab does not interrupt it; the state lives in memory and a server
|
||||
restart ends the run.
|
||||
|
||||
- `POST /api/ram-batch/start` — the body is the `/api/ram-install` payload
|
||||
**without** the per-card fields (`preset_id`, `tar`, `spi1`, `cntr`,
|
||||
`kic`/`kid`/`kicKey`/`kidKey`): the plan is card independent and is built
|
||||
once, so a bad `.cap` or an invalid parameter combination is a `400` at
|
||||
Start, not a per-card failure. **No card is needed** — the worker waits for
|
||||
one. `400` for a bad request, `409` when another batch or a test run is
|
||||
active. The response is the status payload below plus `ok: true`.
|
||||
- `POST /api/ram-batch/start` — `kind` selects the operation; the plan is card
|
||||
independent and is built once, so a bad `.cap`, AID or parameter combination
|
||||
is a `400` at Start, not a per-card failure. The body carries the kind's
|
||||
fields **without** the per-card ones (`preset_id`, `tar`, `spi1`, `cntr`,
|
||||
`kic`/`kid`/`kicKey`/`kidKey`):
|
||||
- `kind: "install-cap"` (default) — the `/api/ram-install` payload
|
||||
(`cap_hex`, `sd_aid`, `install_params`, …) plus `delete_package` (a
|
||||
cascade DELETE on the package AID before the chain, see `/api/ram-install`);
|
||||
- `kind: "reinstall-app"` — the `/api/ram-install-app` payload (`loadfile_aid`,
|
||||
`module_aid`, `instance_aid`, `privileges`, `install_params`,
|
||||
`stk_params`, `make_selectable`, `nv_quota`/`volatile_quota`): DELETE the
|
||||
applet instance (tolerating 'not found'), then INSTALL [for install] from
|
||||
the loaded package — no `.cap`, no LOAD;
|
||||
- `kind: "delete"` — `aid` (5-16 bytes of hex) and `target`: `"app"`
|
||||
deletes the object only (an applet instance, `P2=00`), `"package"` the
|
||||
object and its related objects (the load file with its modules and
|
||||
installed applications, `P2=80`).
|
||||
|
||||
**No card is needed** — the worker waits for one. `400` for a bad request
|
||||
or an unknown `kind`, `409` when another batch or a test run is active. The
|
||||
response is the status payload below plus `ok: true`.
|
||||
- `GET /api/ram-batch/status` — **card-free** (the panel polls it while an
|
||||
install holds the card lock):
|
||||
|
||||
```json
|
||||
{"active": true, "phase": "waiting_card",
|
||||
{"active": true, "phase": "waiting_card", "kind": "reinstall-app",
|
||||
"attempts": [{"ts": 1760000000.0, "iccid": "8970119000004600098",
|
||||
"preset": "Batch 0002", "result": "installed",
|
||||
"duration_ms": 41230, "final_cntr": "0000000005",
|
||||
"load_file_aid": "F0414C46416101",
|
||||
"module_aid": "F0414C4641610101", "nv_delta": 3120}],
|
||||
"duration_ms": 4120, "final_cntr": "0000000005",
|
||||
"steps": [{"name": "DELETE (application)", "ok": true, "sw": "6101", "not_found": false},
|
||||
{"name": "INSTALL [for install]", "ok": true, "sw": "6101", "not_found": false}],
|
||||
"steps_total": 2}],
|
||||
"attempts_total": 3,
|
||||
"current": null, "notice": "Card 8970119000004600098 installed.",
|
||||
"current": null, "notice": "Card 8970119000004600098 reinstalled.",
|
||||
"counts": {"installed": 2, "failed": 1, "skipped": 0},
|
||||
"started": 1760000000.0, "stopped": null, "progress": null}
|
||||
```
|
||||
|
||||
`phase` is `idle` (no run), `waiting_card`, `installing`, `stopping`,
|
||||
`stopped` or `finished`. A failed attempt carries `failed_step` (e.g.
|
||||
`LOAD (3/7)`), `por_sw` (the card's own status word), `por_status`,
|
||||
`por_error` and `sw` — or, when the chain died before a verdict (a lost
|
||||
card, a transport error), just `error`; a skipped one carries `reason`
|
||||
(a request/eligibility error: the install never started). `current` names the
|
||||
card being installed (`{iccid, preset, started}`) and `progress` mirrors the
|
||||
RAM progress payload while a chain runs. The report keeps the last 500
|
||||
attempts (`attempts_total` counts them all).
|
||||
`stopped` or `finished`; `kind` names the operation (the panel's labels
|
||||
follow it: installed / deleted / reinstalled). A failed attempt carries
|
||||
`failed_step` (e.g. `LOAD (3/7)`), `por_sw` (the card's own status word),
|
||||
`por_status`, `por_error` and `sw` — or, when the chain died before a verdict
|
||||
(a lost card, a transport error), just `error`; a skipped one carries
|
||||
`reason` (a request/eligibility error: the operation never started). An
|
||||
attempt also carries the last three steps (`steps`: `[{name, ok, sw,
|
||||
not_found}]` plus `steps_total`), so the panel shows the per-step verdicts
|
||||
(`DELETE (application) 6101 · INSTALL [for install] 6101`) for the short
|
||||
delete/reinstall chains; a delete step that answered 'not found' has
|
||||
`not_found: true` and `ok: true`. `current` names the card being processed
|
||||
(`{iccid, preset, started}`) and `progress` mirrors the RAM progress payload
|
||||
while a chain runs. The report keeps the last 500 attempts
|
||||
(`attempts_total` counts them all).
|
||||
- `POST /api/ram-batch/stop` — sets the stop flag: a running install finishes
|
||||
first (an interrupted LOAD chain can leave a half-loaded package), then the
|
||||
run ends. It touches no card state, so it answers while the card lock is
|
||||
@@ -1186,7 +1209,11 @@ as `skipped` with its reason and the batch keeps waiting. A card is attempted
|
||||
**once per run** (any outcome): re-equipping it is recorded as
|
||||
`already processed in this batch (<outcome>)`, never installed again —
|
||||
restart the batch to retry it. Failures never stop the run and are never
|
||||
retried automatically.
|
||||
retried automatically. A **delete step** (the reinstall's instance delete,
|
||||
the delete kind, the `delete_package` pre-step) tolerates 'not found'
|
||||
(`6A88`/`6A82` — the object is already gone, the goal is met); any other
|
||||
delete failure stops that card's chain and the attempt reports the delete's
|
||||
reason.
|
||||
|
||||
`/api/status` carries a small summary in `ram_batch` — the detailed state
|
||||
**minus the attempts list** (`{active, phase, current, notice,
|
||||
@@ -1197,7 +1224,7 @@ attempts report is fetched from `/api/ram-batch/status` only when
|
||||
|
||||
### `POST /api/ram-install-app`
|
||||
|
||||
Run the single **INSTALL [for install]** (or **INSTALL [for make selectable]**) APDU for an *already loaded* package — one APDU, so the install parameters can be iterated without deleting and re-loading the `.cap`. The APDU builders are shared with `/api/ram-install` (`_cap_install_apdu` / `_cap_make_selectable_apdu`; both case 3 — no trailing `Le`, which the card's SCP80 layer counts as a phantom command) and the step verdict/counter logic is the same (`final_cntr` is returned on success **and** on failure).
|
||||
Run **INSTALL [for install]** (or **INSTALL [for make selectable]**) for an *already loaded* package — no `.cap` and no LOAD, so the install parameters can be iterated without re-loading. Since v3.29.0 the install **deletes the applet instance first** (`mode: "install"`; the operation does not delete the package, so the instance is removed for a clean re-install), tolerating 'not found' (`6A88`/`6A82` — a fresh card just installs); any other delete failure stops the chain with its reason. `mode: "make_selectable"` stays a single APDU. The APDU builders are shared with `/api/ram-install` (`_cap_install_apdu` / `_cap_make_selectable_apdu`; both case 3 — no trailing `Le`, which the card's SCP80 layer counts as a phantom command) and the step verdict/counter logic is the same (`final_cntr` is returned on success **and** on failure).
|
||||
|
||||
**Request body:**
|
||||
```json
|
||||
|
||||
File diff suppressed because one or more lines are too long
+5
-3
@@ -321,10 +321,12 @@
|
||||
<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>
|
||||
<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 (Extended Card Resources: the number of installed applications and the free non-volatile/volatile memory). The compact listing carries the AID, life cycle and privileges (the legacy format has no version); if a card rejects it (<code class="font-mono text-sm">P2.b2=0</code>, deprecated by GP) the queries are retried in the expanded <code class="font-mono text-sm">E3</code> form, and the <strong>Fetch expanded registry details (load-file / SD associations)</strong> checkbox also requests it as an extra pass and merges the expanded-only fields: the Executable Load File version, Load File AID, Security Domain AID, module lists, the applet STK menu entries (SCP Registry Data, TS 102 226 8.2.1.6) and any other registry data objects the card returns (shown raw, never dropped) — it costs a few extra secured packets per Explore and is <strong>on by default</strong> (remembered per browser; untick to skip). Cards differ in what they accept, so the expanded query is tried in three forms — the reference tag list (with the TS 102 226 <code class="font-mono text-sm">EA</code>), the same list without <code class="font-mono text-sm">EA</code>, and no tag list at all (the GlobalPlatformPro form) — and the explorer reports which form worked and where the run fell back to the compact listing (package versions are then unavailable). When the ELF rows still carry no version, the tag-list forms are retried for both P1=10 and P1=20, skipping the combinations already refused, and the walk stops as soon as a version appears. Results appear in an explorer view (the success line stays minimal; the counts are in the section headings) with per-item <strong>Delete</strong> buttons and the detected RAM command format (hover for what compact vs expanded means).</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 reports the <strong>libraries the CAP is linked against</strong> (its Import component: library name, AID and the minimum version <code class="font-mono text-sm">≥ x.y</code>, with standard names and families resolved and a Java Card SDK hint where known), the package/applet identity and the component breakdown (sizes and shares in load-file order) — grouped under the <strong>Package</strong>, <strong>Requires</strong>, <strong>Memory</strong> and <strong>Components</strong> headings. The <strong>Platform</strong> line names the minimum Java Card level the card must provide (derived from the <code class="font-mono text-sm">javacard.framework</code> import; the header <code class="font-mono text-sm">int</code> flag additionally requires int support, Java Card 2.2+) and the <strong>ETSI release</strong> the UICC API imports come from (<code class="font-mono text-sm">uicc.toolkit</code> version → REL-N, per the workspace <code class="font-mono text-sm">sim-tools</code> corpus; shown with its basis in brackets): the JCRE checks every import against its own packages when it verifies the load file (JC VM spec 4.5.2), so a card whose platform is older rejects the LOAD as soon as the <strong>Import</strong> component is complete (the step line carries the card's remote SW and names the component the block completed, e.g. <em>completes Import</em>): the failure hint and the popup then point at the <strong>CAP compatibility test</strong> (one click — the same <code class="font-mono text-sm">.cap</code> is still selected) to find the missing package and version; a smaller LOAD block only helps when the block exceeds the card's buffer. A corrupt or wrong-format file fails the analysis, and <strong>Execute</strong> stays disabled until a successful analysis. The estimate is informational only and is never used for installation: NVRAM is the code image plus the persistent data (the GlobalPlatform C6+C8 style total, Table 11-48 — card memory management, rounding and the registry entry are not included, and the static field image is counted in both parts); the suggested <strong>C6</strong> follows the load file, and the suggested quotas are decimal byte counts (the hex in parentheses is how the C7/C8 install parameters encode them: C6 = non-volatile code, C7 = volatile RAM, C8 = non-volatile data).</td></tr>
|
||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">Batch install (.cap file, per card)</td><td class="py-1 px-2">The bulk workflow: one <code class="font-mono text-sm">.cap</code> and one set of install parameters, applied to card after card. <strong>Start batch</strong> needs no card — the server owns the loop: it waits for a card, installs the moment the equip finishes, then waits for the next one. Every card's <strong>preset is matched by ICCID</strong> and supplies the keyset (the ISD role TAR's default, v3.26.0), the keys and the counter; a card with no matching preset or no usable ISD keyset is reported and skipped. Each attempt lands in the list with its time, ICCID, preset, verdict and duration; a failure shows the failing step and the remote status word — the batch never retries and never stops on a failure (failures are yours to manage afterwards). A card is attempted <strong>once per run</strong>: re-equipping it reports “already processed” — restart the batch to retry it. <strong>Stop</strong> finishes the current card first (an interrupted LOAD chain can leave a half-loaded package) and then ends the run; <strong>Clear</strong> drops the finished report. The loop runs in the server, so closing the tab, switching views or reloading the page does not interrupt it; a server restart ends it. A batch and a test run exclude each other.</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 reports the <strong>libraries the CAP is linked against</strong> (its Import component: library name, AID and the minimum version <code class="font-mono text-sm">≥ x.y</code>, with standard names and families resolved and a Java Card SDK hint where known), the package/applet identity and the component breakdown (sizes and shares in load-file order) — grouped under the <strong>Package</strong>, <strong>Requires</strong>, <strong>Memory</strong> and <strong>Components</strong> headings. The <strong>Platform</strong> line names the minimum Java Card level the card must provide (derived from the <code class="font-mono text-sm">javacard.framework</code> import; the header <code class="font-mono text-sm">int</code> flag additionally requires int support, Java Card 2.2+) and the <strong>ETSI release</strong> the UICC API imports come from (<code class="font-mono text-sm">uicc.toolkit</code> version → REL-N, per the workspace <code class="font-mono text-sm">sim-tools</code> corpus; shown with its basis in brackets): the JCRE checks every import against its own packages when it verifies the load file (JC VM spec 4.5.2), so a card whose platform is older rejects the LOAD as soon as the <strong>Import</strong> component is complete (the step line carries the card's remote SW and names the component the block completed, e.g. <em>completes Import</em>): the failure hint and the popup then point at the <strong>CAP compatibility test</strong> (one click — the same <code class="font-mono text-sm">.cap</code> is still selected) to find the missing package and version; a smaller LOAD block only helps when the block exceeds the card's buffer. A corrupt or wrong-format file fails the analysis, and <strong>Execute</strong> stays disabled until a successful analysis. The estimate is informational only and is never used for installation: NVRAM is the code image plus the persistent data (the GlobalPlatform C6+C8 style total, Table 11-48 — card memory management, rounding and the registry entry are not included, and the static field image is counted in both parts); the suggested <strong>C6</strong> follows the load file, and the suggested quotas are decimal byte counts (the hex in parentheses is how the C7/C8 install parameters encode them: C6 = non-volatile code, C7 = volatile RAM, C8 = non-volatile data). The <strong>Delete the package before installing (with its applications)</strong> checkbox prepends a cascade GP <code class="font-mono text-sm">DELETE</code> on the package AID (the load file with its modules and applications, P2=<code class="font-mono text-sm">0x80</code>) — a leftover package blocks INSTALL[for load]; 'not found' (a fresh card) proceeds, any other delete failure stops the chain with its reason.</td></tr>
|
||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">Batch install (.cap file, per card)</td><td class="py-1 px-2">The bulk workflow: one <code class="font-mono text-sm">.cap</code> and one set of install parameters, applied to card after card. <strong>Start batch</strong> needs no card — the server owns the loop: it waits for a card, installs the moment the equip finishes, then waits for the next one. Every card's <strong>preset is matched by ICCID</strong> and supplies the keyset (the ISD role TAR's default, v3.26.0), the keys and the counter; a card with no matching preset or no usable ISD keyset is reported and skipped. Each attempt lands in the list with its time, ICCID, preset, verdict and duration; a failure shows the failing step and the remote status word — the batch never retries and never stops on a failure (failures are yours to manage afterwards). A card is attempted <strong>once per run</strong>: re-equipping it reports “already processed” — restart the batch to retry it. <strong>Stop</strong> finishes the current card first (an interrupted LOAD chain can leave a half-loaded package) and then ends the run; <strong>Clear</strong> drops the finished report. The loop runs in the server, so closing the tab, switching views or reloading the page does not interrupt it; a server restart ends it. A batch and a test run exclude each other. The batch shares the install form, so the <strong>Delete the package before installing</strong> option applies to every card.</td></tr>
|
||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">Batch reinstall app (per card)</td><td class="py-1 px-2">The repair workflow: for every equipped card, delete the applet instance (tolerating 'not found') and run INSTALL [for install] again from the already loaded package — no <code class="font-mono text-sm">.cap</code> and no LOAD. The fields are the single INSTALL [for install] parameters (Load File / Module / Instance AIDs, privileges, the toolkit/STK parameters, <em>make selectable</em>); the preset, keyset and counter come from each card's own preset like the other batch operations, and the attempts list shows both verdicts (delete and install).</td></tr>
|
||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">Batch delete (per card)</td><td class="py-1 px-2">Deletes one object on every equipped card: an application (instance only, P2=<code class="font-mono text-sm">0x00</code>) or a package with its applications (cascade, P2=<code class="font-mono text-sm">0x80</code>). Fields: the AID and the target. A 'not found' result (the object is already gone) is reported as such and counted OK; any other failure is reported with the card's status word.</td></tr>
|
||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">CAP compatibility test</td><td class="py-1 px-2">Answers one question without installing anything: <strong>can this card satisfy the CAP's import list?</strong> The server runs the format check, INSTALL[for load] and the LOAD blocks only up to the block that completes the <strong>Import</strong> component — the point where the JCRE verifies the import list — then stops: no last-block flag and no INSTALL[for install], so nothing is committed and nothing has to be deleted afterwards. The <strong>Import versions to test</strong> list is pre-filled from the CAP analysis: <code class="font-mono text-sm">AID=version</code> rewrites an import's requested version, <code class="font-mono text-sm">+AID=version</code> appends a synthetic import (a card-capability query: the LOAD passes only if the card exports that package at ≥ that version), and <strong>Set all 0.0</strong> drops every requirement. Two-step use: press Set all 0.0 — if the test passes, the import list was the blocker; then restore one line at a time to find the package and version the card lacks. The verdict states it covers the <strong>LOAD/import gate only</strong>: the link gate (whether the applet's actual API calls exist) is what a real install checks at INSTALL[for install]. The run and its result appear in a popup (like the install): the step lines, the verdict and a per-AID detail block — on a rejection it names the CAP section the failing block covers (load-file byte range and components, and whether that block completes <strong>Import</strong>), the remote status word with its meaning, the import list exactly as tested (CAP versions, probe rewrites, appended synthetic entries, unmatched lines) and what the probe state implies; on success it lists the import list that passed.</td></tr>
|
||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">INSTALL [for install] (loaded package)</td><td class="py-1 px-2">One INSTALL [for install] APDU for a package that is already loaded on the card (its Load File AID and Module AID, privileges, install/STK parameters and an optional <em>make selectable</em>) — iterate the install parameters without deleting and re-loading the <code class="font-mono text-sm">.cap</code> every time.</td></tr>
|
||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">INSTALL [for install] (loaded package)</td><td class="py-1 px-2">For a package that is already loaded on the card (its Load File AID and Module AID, privileges, install/STK parameters and an optional <em>make selectable</em>). The applet instance is deleted first (tolerating 'not found' — a fresh card just installs; any other delete failure stops the chain with its reason), so the install parameters can be iterated without re-loading the <code class="font-mono text-sm">.cap</code>.</td></tr>
|
||||
<tr><td class="py-1 px-2">INSTALL [for make selectable]</td><td class="py-1 px-2">One INSTALL [for make selectable] APDU (the instance AID) for an INSTALLED, non-selectable applet instance.</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
+123
-19
@@ -700,10 +700,14 @@
|
||||
<select id="ram-op" onchange="ramOpChanged()" class="w-full border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800">
|
||||
<option value="explore" data-l10n="Explore Card (all GP data)">Explore Card (all GP data)</option>
|
||||
<option value="install-cap" data-l10n="Install Package (.cap file)">Install Package (.cap file)</option>
|
||||
<option value="batch" data-l10n="Batch install (.cap file, per card)">Batch install (.cap file, per card)</option>
|
||||
<option value="compat" data-l10n="CAP compatibility test">CAP compatibility test</option>
|
||||
<option value="install-app" data-l10n="INSTALL [for install] (loaded package)">INSTALL [for install] (loaded package)</option>
|
||||
<option value="make-selectable" data-l10n="INSTALL [for make selectable]">INSTALL [for make selectable]</option>
|
||||
<optgroup label="Batch (per card)" data-l10n="Batch (per card)">
|
||||
<option value="batch" data-l10n="Batch install (.cap file, per card)">Batch install (.cap file, per card)</option>
|
||||
<option value="batch-reinstall" data-l10n="Batch reinstall app (per card)">Batch reinstall app (per card)</option>
|
||||
<option value="batch-delete" data-l10n="Batch delete (per card)">Batch delete (per card)</option>
|
||||
</optgroup>
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
@@ -729,6 +733,10 @@
|
||||
<div id="ram-cap-body"></div>
|
||||
</fieldset>
|
||||
</div>
|
||||
<label id="ram-del-package-row" class="hidden flex items-center gap-2 mb-3 text-sm text-gray-700 dark:text-slate-300">
|
||||
<input type="checkbox" id="ram-del-package" class="rounded border-gray-300 dark:border-slate-600">
|
||||
<span data-l10n="Delete the package before installing (with its applications)">Delete the package before installing (with its applications)</span>
|
||||
</label>
|
||||
<div id="ram-compat-rows" class="hidden mb-3">
|
||||
<fieldset class="border border-gray-200 dark:border-slate-700 rounded p-3" data-l10n-title="Runs the load only as far as the block that completes the CAP's Import component: the card checks the import list there, so its answer says whether the card can satisfy it. Nothing is installed and nothing is committed - the link gate is the real install's business. Lines are AID=version (rewrites an import) or +AID=version (appends a synthetic import, to query the card for a package the CAP does not use).">
|
||||
<legend class="px-1 text-xs font-medium text-gray-500 dark:text-slate-400" data-l10n="Import versions to test">Import versions to test</legend>
|
||||
@@ -742,6 +750,16 @@
|
||||
<label class="block mb-1 mt-2 text-sm font-medium text-gray-700 dark:text-slate-300" data-l10n="LOAD block size (1..240, default 240)">LOAD block size (1..240, default 240)</label>
|
||||
<input id="ram-compat-block" type="number" min="1" max="240" class="w-32 font-mono border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800" placeholder="240">
|
||||
</div>
|
||||
<div id="ram-delete-rows" class="hidden mb-3">
|
||||
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300" data-l10n="AID (hex)">AID (hex)</label>
|
||||
<input id="ram-del-aid" class="w-full font-mono border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800" placeholder="F0414C4641610101" maxlength="32">
|
||||
<label class="block mb-1 mt-2 text-sm font-medium text-gray-700 dark:text-slate-300" data-l10n="Target">Target</label>
|
||||
<select id="ram-del-target" class="w-full border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800">
|
||||
<option value="app" data-l10n="Application (instance only)">Application (instance only)</option>
|
||||
<option value="package" data-l10n="Package (with its applications)">Package (with its applications)</option>
|
||||
</select>
|
||||
<div class="text-xs text-gray-400 dark:text-slate-500 mt-1" data-l10n="A package delete cascades (the load file with its modules and installed applications, P2=80).">A package delete cascades (the load file with its modules and installed applications, P2=80).</div>
|
||||
</div>
|
||||
<div id="ram-install-params" class="hidden mb-3">
|
||||
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300" data-l10n="SD AID (empty = ISD)">SD AID (empty = ISD)</label>
|
||||
<input id="ram-sd-aid" class="w-full font-mono border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800" placeholder="A000000003000000" maxlength="32">
|
||||
@@ -1910,7 +1928,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.28.8';
|
||||
const SIMPLE_VERSION = '3.29.0';
|
||||
document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION;
|
||||
|
||||
// ===== view history (the browser Back/Forward buttons) =====
|
||||
@@ -10110,14 +10128,22 @@ function ramApplyCard(idx) {
|
||||
spPresetWarningRender();
|
||||
}
|
||||
|
||||
// The three per-card batch operations (v3.29.0): the preset, keyset and
|
||||
// counter resolve from each card's own preset, so the preset selector, the
|
||||
// Execute button and the preset-mismatch banner do not apply.
|
||||
function ramIsBatchOp(op) {
|
||||
return op === 'batch' || op === 'batch-reinstall' || op === 'batch-delete';
|
||||
}
|
||||
|
||||
function ramOpChanged() {
|
||||
const op = document.getElementById('ram-op').value;
|
||||
if (_ramOpLast !== null && op !== _ramOpLast) ramClearResults();
|
||||
_ramOpLast = op;
|
||||
const isBatch = op === 'batch';
|
||||
const isCap = op === 'install-cap' || isBatch;
|
||||
const isBatch = ramIsBatchOp(op);
|
||||
const isCap = op === 'install-cap' || op === 'batch';
|
||||
const isCompat = op === 'compat';
|
||||
const isApp = op === 'install-app' || op === 'make-selectable';
|
||||
const isApp = op === 'install-app' || op === 'make-selectable' || op === 'batch-reinstall';
|
||||
const isDelete = op === 'batch-delete';
|
||||
const setHidden = (id, hidden) => {
|
||||
const el = document.getElementById(id);
|
||||
if (el) el.classList.toggle('hidden', hidden);
|
||||
@@ -10126,6 +10152,8 @@ function ramOpChanged() {
|
||||
setHidden('ram-cap-rows', !(isCap || isCompat));
|
||||
setHidden('ram-compat-rows', !isCompat);
|
||||
setHidden('ram-app-rows', !isApp);
|
||||
setHidden('ram-delete-rows', !isDelete);
|
||||
setHidden('ram-del-package-row', !isCap);
|
||||
// The batch resolves the preset per card (by ICCID): its selector and the
|
||||
// single-card Execute button do not apply, and the batch panel takes over.
|
||||
setHidden('ram-preset-cell', isBatch);
|
||||
@@ -10164,7 +10192,7 @@ const RAM_BATCH_FETCH_STALE_MS = 10000;
|
||||
function ramBatchPanelVisible() {
|
||||
const op = document.getElementById('ram-op');
|
||||
return isViewVisible('tab-scp80') && isViewVisible('scp80-sub-ram')
|
||||
&& !!op && op.value === 'batch';
|
||||
&& !!op && ramIsBatchOp(op.value);
|
||||
}
|
||||
|
||||
async function ramBatchFetch() {
|
||||
@@ -10218,6 +10246,13 @@ function ramBatchRender() {
|
||||
_ramBatchLast || {}, _ramBatchSummary || {});
|
||||
const active = !!st.active;
|
||||
const phase = st.phase || (active ? 'waiting_card' : 'idle');
|
||||
// The success labels follow the operation (v3.29.0): the counts bucket is
|
||||
// 'installed' for every kind - "the operation succeeded".
|
||||
const kind = st.kind || 'install-cap';
|
||||
const okLabel = kind === 'delete' ? t('Deleted')
|
||||
: (kind === 'reinstall-app' ? t('Reinstalled') : t('Installed'));
|
||||
const actLabel = kind === 'delete' ? t('Deleting...')
|
||||
: (kind === 'reinstall-app' ? t('Reinstalling...') : t('Installing...'));
|
||||
const startBtn = document.getElementById('ram-batch-start');
|
||||
if (startBtn) startBtn.classList.toggle('hidden', active);
|
||||
const stopBtn = document.getElementById('ram-batch-stop');
|
||||
@@ -10240,7 +10275,7 @@ function ramBatchRender() {
|
||||
: (phase === 'finished' ? t('Batch finished') : '');
|
||||
} else if (phase === 'installing') {
|
||||
const cur = st.current || {};
|
||||
stateEl.textContent = t('Installing...') + ' ' + (cur.iccid || '')
|
||||
stateEl.textContent = actLabel + ' ' + (cur.iccid || '')
|
||||
+ (cur.preset ? ' \u2014 ' + cur.preset : '');
|
||||
} else if (phase === 'stopping') {
|
||||
stateEl.textContent = t('Stopping after the current card...');
|
||||
@@ -10256,7 +10291,7 @@ function ramBatchRender() {
|
||||
const counts = document.getElementById('ram-batch-counts');
|
||||
if (counts) {
|
||||
const c = st.counts || {};
|
||||
let text = t('Installed') + ': ' + (c.installed || 0)
|
||||
let text = okLabel + ': ' + (c.installed || 0)
|
||||
+ ' \u00b7 ' + t('Failed') + ': ' + (c.failed || 0)
|
||||
+ ' \u00b7 ' + t('Skipped') + ': ' + (c.skipped || 0);
|
||||
if (st.attempts_total) text += ' \u00b7 ' + st.attempts_total + ' ' + t('attempts');
|
||||
@@ -10276,6 +10311,17 @@ function ramBatchRender() {
|
||||
}
|
||||
}
|
||||
|
||||
// The per-step verdict tail of an attempt (v3.29.0): e.g.
|
||||
// "DELETE (application) 6101 · INSTALL [for install] 6101". Only short chains
|
||||
// are rendered (the delete/reinstall kinds) - a full LOAD chain's tail would
|
||||
// be noise; anything else keeps the skip reason.
|
||||
function ramBatchStepsText(a) {
|
||||
const steps = (a && a.steps) || [];
|
||||
if (!steps.length || steps.length > 3) return (a && a.reason) || '';
|
||||
return steps.map(s => s.name + (s.sw ? ' ' + s.sw : '') +
|
||||
(s.not_found ? ' (' + t('not found') + ')' : '')).join(' \u00b7 ');
|
||||
}
|
||||
|
||||
function ramBatchAttemptsHtml(attempts) {
|
||||
if (!attempts.length) return '<div class="text-gray-400 dark:text-slate-500">' + esc(t('No cards processed yet.')) + '</div>';
|
||||
let html = '<table class="w-full"><tbody>';
|
||||
@@ -10288,7 +10334,7 @@ function ramBatchAttemptsHtml(attempts) {
|
||||
: (a.result === 'failed' ? 'text-red-600 dark:text-red-400' : 'text-amber-600 dark:text-amber-400');
|
||||
const detail = a.result === 'failed'
|
||||
? [a.failed_step, a.por_sw || a.sw || a.error].filter(Boolean).join(' \u2014 ')
|
||||
: (a.reason || '');
|
||||
: ramBatchStepsText(a);
|
||||
html += '<tr class="border-b border-gray-100 dark:border-slate-800">'
|
||||
+ '<td class="py-0.5 pr-2 text-gray-400 dark:text-slate-500 whitespace-nowrap">' + esc(when) + '</td>'
|
||||
+ '<td class="py-0.5 pr-2 font-mono whitespace-nowrap">' + esc(a.iccid || '') + '</td>'
|
||||
@@ -10321,6 +10367,7 @@ function ramInstallCapFields() {
|
||||
const composed = ramInstallParamsCompose();
|
||||
const body = {
|
||||
sd_aid: (document.getElementById('ram-sd-aid').value || '').replace(/[^0-9a-fA-F]/g, ''),
|
||||
delete_package: !!(document.getElementById('ram-del-package') || {}).checked,
|
||||
privileges: (document.getElementById('rc-priv').value || '00').replace(/[^0-9a-fA-F]/g, ''),
|
||||
install_params: composed.install_params,
|
||||
stk_params: composed.stk_params,
|
||||
@@ -10337,17 +10384,50 @@ function ramInstallCapFields() {
|
||||
}
|
||||
|
||||
async function ramBatchStart() {
|
||||
if (!capGateOk('ram')) { alert(t('Analyze the CAP file first')); return; }
|
||||
const fileInput = document.getElementById('ram-cap-file');
|
||||
const file = fileInput.files[0];
|
||||
if (!file) { alert(t('Select a .cap file')); return; }
|
||||
if (file.size > 48 * 1024) { alert(t('CAP file exceeds 48 kB limit')); return; }
|
||||
const fields = ramInstallCapFields();
|
||||
if (!fields) return;
|
||||
// The operation selects the batch kind (v3.29.0): the .cap install, the
|
||||
// applet reinstall (no .cap - the package is loaded) or a GP delete. The
|
||||
// server builds and validates the plan once at Start.
|
||||
const op = document.getElementById('ram-op').value;
|
||||
const clean = id => (document.getElementById(id).value || '').replace(/[^0-9a-fA-F]/g, '').toUpperCase();
|
||||
let fields;
|
||||
if (op === 'batch-delete') {
|
||||
const aid = clean('ram-del-aid');
|
||||
if (!/^(?:[0-9A-F]{2}){5,16}$/.test(aid)) { alert(t('AID must be 5-16 bytes of hex')); return; }
|
||||
fields = {kind: 'delete', aid: aid,
|
||||
target: document.getElementById('ram-del-target').value || 'app'};
|
||||
} else if (op === 'batch-reinstall') {
|
||||
const loadfile = clean('ram-app-loadfile');
|
||||
const module = clean('ram-app-module');
|
||||
const instance = clean('ram-app-instance');
|
||||
if (!loadfile || !module) { alert(t('Load File AID and Module AID required')); return; }
|
||||
// The privileges and the STK hex are derived from the form, never sent
|
||||
// stale (the same rule the single install-app follows).
|
||||
updateRcPriv();
|
||||
const stkEl = document.getElementById('ram-stk-params');
|
||||
if (document.getElementById('rc-toolkit-enable').checked && !(stkEl.dataset && stkEl.dataset.manual)) {
|
||||
updateStkParamsHex();
|
||||
}
|
||||
const composed = ramInstallParamsCompose();
|
||||
fields = {kind: 'reinstall-app', loadfile_aid: loadfile, module_aid: module,
|
||||
instance_aid: instance,
|
||||
privileges: (document.getElementById('rc-priv').value || '00').replace(/[^0-9a-fA-F]/g, ''),
|
||||
install_params: composed.install_params, stk_params: composed.stk_params,
|
||||
make_selectable: document.getElementById('ram-make-sel').checked};
|
||||
} else {
|
||||
if (!capGateOk('ram')) { alert(t('Analyze the CAP file first')); return; }
|
||||
const fileInput = document.getElementById('ram-cap-file');
|
||||
const file = fileInput.files[0];
|
||||
if (!file) { alert(t('Select a .cap file')); return; }
|
||||
if (file.size > 48 * 1024) { alert(t('CAP file exceeds 48 kB limit')); return; }
|
||||
fields = ramInstallCapFields();
|
||||
if (!fields) return;
|
||||
fields.kind = 'install-cap';
|
||||
fields.cap_hex = await ramReadFileHex(file);
|
||||
}
|
||||
fields.ram_format = (document.getElementById('ram-format') || {}).value || 'auto';
|
||||
const startBtn = document.getElementById('ram-batch-start');
|
||||
if (startBtn) startBtn.disabled = true;
|
||||
try {
|
||||
fields.cap_hex = await ramReadFileHex(file);
|
||||
_ramBatchLast = await pysimFetchOk('/api/ram-batch/start', fields);
|
||||
_ramBatchSummary = null;
|
||||
} catch (e) {
|
||||
@@ -12312,6 +12392,12 @@ function ramStepComponents(name, components, blockSize) {
|
||||
// not a plain 9000. With the CAP analysis at hand the line also names the
|
||||
// component the block completed (see ramStepComponents).
|
||||
function ramStepLine(s, idx, comps, blockSize) {
|
||||
// a delete step that answered 'not found' (6A88/6A82): the object was
|
||||
// already gone - the delete's goal is met, the chain continued (v3.29.0)
|
||||
if (s.delete_not_found) {
|
||||
return '\u2796 ' + t('Step') + ' ' + (idx + 1) + ': ' + s.name + ' \u2014 ' +
|
||||
(s.por_sw ? s.por_sw + ' (' + t('not found') + ')' : t('not found'));
|
||||
}
|
||||
const ok = !s.por_error && (s.por_status === 'por_ok' || s.por_status === 'no_por');
|
||||
let line = (ok ? '\u2705' : '\u274c') + ' ' + t('Step') + ' ' + (idx + 1) + ': ' +
|
||||
s.name + ' \u2014 ';
|
||||
@@ -14052,7 +14138,7 @@ function spPresetWarningRender() {
|
||||
// The batch resolves the preset per card, so the RAM view's warning does
|
||||
// not apply there (v3.28.0).
|
||||
const ramOp = document.getElementById('ram-op');
|
||||
const batchMode = !!ramOp && ramOp.value === 'batch';
|
||||
const batchMode = !!ramOp && ramIsBatchOp(ramOp.value);
|
||||
show('ram-preset-warning', batchMode ? '' : spPresetWarning(iccid, cards, ramPresetIdx()));
|
||||
show('tar-preset-warning', spPresetWarning(iccid, cards, tarPresetIdx()));
|
||||
}
|
||||
@@ -24442,14 +24528,29 @@ const LANG_RU = {
|
||||
'Close': 'Закрыть',
|
||||
'Installing package': 'Установка пакета',
|
||||
'Batch install (.cap file, per card)': 'Пакетная установка (.cap на множество карт)',
|
||||
'Batch (per card)': 'Пакетные операции (по картам)',
|
||||
'Batch reinstall app (per card)': 'Пакетная переустановка приложения (по картам)',
|
||||
'Batch delete (per card)': 'Пакетное удаление (по картам)',
|
||||
'AID (hex)': 'AID (hex)',
|
||||
'Target': 'Цель',
|
||||
'Application (instance only)': 'Приложение (только экземпляр)',
|
||||
'Package (with its applications)': 'Пакет (с его приложениями)',
|
||||
'A package delete cascades (the load file with its modules and installed applications, P2=80).': 'Удаление пакета каскадное (load-файл с его модулями и установленными приложениями, P2=80).',
|
||||
'Delete the package before installing (with its applications)': 'Удалить пакет перед установкой (с его приложениями)',
|
||||
'AID must be 5-16 bytes of hex': 'AID должен быть 5-16 байт hex',
|
||||
'not found': 'не найдено',
|
||||
'Start batch': 'Запустить пакетную установку',
|
||||
'Stopping...': 'Остановка…',
|
||||
'Batch stopped': 'Пакетная установка остановлена',
|
||||
'Batch finished': 'Пакетная установка завершена',
|
||||
'Installing...': 'Установка…',
|
||||
'Deleting...': 'Удаление…',
|
||||
'Reinstalling...': 'Переустановка…',
|
||||
'Stopping after the current card...': 'Остановка после текущей карты…',
|
||||
'Insert next card. Waiting for a card...': 'Вставьте следующую карту. Ожидание карты…',
|
||||
'Installed': 'Установлено',
|
||||
'Deleted': 'Удалено',
|
||||
'Reinstalled': 'Переустановлено',
|
||||
'Skipped': 'Пропущено',
|
||||
'attempts': 'попыток',
|
||||
'No cards processed yet.': 'Карты ещё не обрабатывались.',
|
||||
@@ -24794,7 +24895,10 @@ function setHelpAnchor(anchor) {
|
||||
function translatePage() {
|
||||
document.querySelectorAll('[data-l10n]').forEach(el => {
|
||||
const key = el.getAttribute('data-l10n');
|
||||
el.textContent = t(key);
|
||||
// an optgroup's label is an attribute: setting its textContent would
|
||||
// not change what the select renders
|
||||
if (el.tagName === 'OPTGROUP') el.label = t(key);
|
||||
else el.textContent = t(key);
|
||||
});
|
||||
// static tooltips (the data-l10n-title attribute; the html test checks
|
||||
// those keys against LANG_RU together with the data-l10n ones)
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
const CACHE = 'simple-v446';
|
||||
const CACHE = 'simple-v447';
|
||||
const URLS = [
|
||||
'index.html',
|
||||
'help.html',
|
||||
|
||||
@@ -33,7 +33,7 @@ const FNS = ['berLenStr', 'buildApdu', 'escHtml', 'esc', 'chainInit', 'chainRamB
|
||||
'spTarOptionsHtml', 'spTarRender', 'spSpi1Seed', 'spTarKeysetGuard', 'cardsTarKeysetCell', 'cardsTarKeysetFromForm', 'cardsTarKeysetRefresh', 'cardsTarRowsRender', 'cardsTarRowHtml', 'cardsTarsFromTable', 'cardsFreeTarsFromForm', 'spPresetIdx', 'tarPresetIdx',
|
||||
'cardsKeysetsFromForm', 'spKeysetSync', 'spKeysetApply',
|
||||
'ramPresetIdx', 'spKeysetGuard', 'ramKeysetGuard',
|
||||
'ramCardIdxAfterRemove', 'ramClearResults', 'ramHideProgress', 'ramOpChanged', 'ramRender', 'ramApplyCard', 'ramExecute', 'decodePrivileges', 'ramActionBtn', 'ramCapToolkitMode', 'ramOpProgressText',
|
||||
'ramCardIdxAfterRemove', 'ramClearResults', 'ramHideProgress', 'ramIsBatchOp', 'ramOpChanged', 'ramRender', 'ramApplyCard', 'ramExecute', 'decodePrivileges', 'ramActionBtn', 'ramCapToolkitMode', 'ramOpProgressText',
|
||||
'jcAidNorm', 'jcAidName', 'jcAidSuffix', 'jcAidHtml'];
|
||||
let code = '';
|
||||
for (const f of FNS) {
|
||||
@@ -419,6 +419,17 @@ test('ramStepLine shows the PoR verdict and the remote status word', () => {
|
||||
assert.ok(noPor.startsWith('\u2705') && noPor.includes('no PoR'), noPor);
|
||||
});
|
||||
|
||||
test('ramStepLine marks a tolerated not-found delete step (v3.29.0)', () => {
|
||||
globalThis.t = s => s;
|
||||
// a delete step's 'not found' (6A88/6A82) is not a failure: the object was
|
||||
// already gone, the chain continued - the line says so instead of showing
|
||||
// the failure style
|
||||
const line = ramStepLine({ name: 'DELETE (application)', por_sw: '6A88',
|
||||
por_error: 'remote SW 6A88', delete_not_found: true }, 0);
|
||||
assert.strictEqual(line,
|
||||
'\u2796 Step 1: DELETE (application) \u2014 6A88 (not found)');
|
||||
});
|
||||
|
||||
test('ramStepComponents maps a LOAD step to the CAP components it covers', () => {
|
||||
// RemMobileID-like layout: sizes include each component's 3-byte header
|
||||
const comps = [
|
||||
|
||||
@@ -23,9 +23,9 @@ function extractFunc(src, name, asyncFn) {
|
||||
|
||||
const ASYNC_FNS = ['ramBatchFetch', 'ramBatchStart', 'ramBatchStop', 'ramBatchClear'];
|
||||
let code = '';
|
||||
for (const fn of ['ramBatchPanelVisible', 'ramBatchAttemptsHtml', 'ramBatchRender',
|
||||
'ramBatchApplyStatus', 'ramBatchFetch', 'ramBatchStart', 'ramBatchStop',
|
||||
'ramBatchClear', 'esc']) {
|
||||
for (const fn of ['ramIsBatchOp', 'ramBatchPanelVisible', 'ramBatchStepsText',
|
||||
'ramBatchAttemptsHtml', 'ramBatchRender', 'ramBatchApplyStatus', 'ramBatchFetch',
|
||||
'ramBatchStart', 'ramBatchStop', 'ramBatchClear', 'esc']) {
|
||||
code += extractFunc(html, fn, ASYNC_FNS.indexOf(fn) >= 0) + '\n';
|
||||
}
|
||||
|
||||
@@ -79,6 +79,9 @@ const app = new Function('els', `
|
||||
var capGateOk = () => sandbox.capGate !== false;
|
||||
var ramReadFileHex = async () => 'AABB';
|
||||
var ramInstallCapFields = () => ({ sd_aid: '', install_params: '', ram_format: 'auto' });
|
||||
var ramInstallParamsCompose = () => ({ install_params: 'C900', stk_params: '' });
|
||||
var updateRcPriv = () => {};
|
||||
var updateStkParamsHex = () => {};
|
||||
var pysimApplyAvailability = () => {};
|
||||
var _ramBatchLast = null;
|
||||
var _ramBatchFetching = false;
|
||||
@@ -96,6 +99,7 @@ const app = new Function('els', `
|
||||
sandbox.getProgress = () => _ramBatchProgress;
|
||||
sandbox.ramBatchPanelVisible = ramBatchPanelVisible;
|
||||
sandbox.ramBatchAttemptsHtml = ramBatchAttemptsHtml;
|
||||
sandbox.ramBatchStepsText = ramBatchStepsText;
|
||||
sandbox.ramBatchRender = ramBatchRender;
|
||||
sandbox.ramBatchApplyStatus = ramBatchApplyStatus;
|
||||
sandbox.ramBatchFetch = ramBatchFetch;
|
||||
@@ -119,11 +123,20 @@ test('the RAM view wires the batch operation and the panel', () => {
|
||||
|
||||
test('ramOpChanged hides the preset cell and Execute in batch mode', () => {
|
||||
const src = extractFunc(html, 'ramOpChanged');
|
||||
assert.match(src, /const isBatch = op === 'batch';/);
|
||||
assert.match(src, /const isCap = op === 'install-cap' \|\| isBatch;/);
|
||||
assert.match(src, /const isBatch = ramIsBatchOp\(op\);/);
|
||||
// the per-card batch set lives in one place (the panel and the preset
|
||||
// warning follow it too)
|
||||
const helper = extractFunc(html, 'ramIsBatchOp');
|
||||
assert.match(helper, /op === 'batch' \|\| op === 'batch-reinstall' \|\| op === 'batch-delete'/);
|
||||
assert.match(extractFunc(html, 'ramBatchPanelVisible'), /ramIsBatchOp\(op\.value\)/);
|
||||
assert.match(extractFunc(html, 'spPresetWarningRender'), /ramIsBatchOp\(ramOp\.value\)/);
|
||||
assert.match(src, /const isCap = op === 'install-cap' \|\| op === 'batch';/);
|
||||
assert.match(src, /const isDelete = op === 'batch-delete';/);
|
||||
assert.match(src, /setHidden\('ram-preset-cell', isBatch\)/);
|
||||
assert.match(src, /setHidden\('ram-exec-btn', isBatch\)/);
|
||||
assert.match(src, /setHidden\('ram-batch-panel', !isBatch\)/);
|
||||
assert.match(src, /setHidden\('ram-delete-rows', !isDelete\)/);
|
||||
assert.match(src, /setHidden\('ram-del-package-row', !isCap\)/);
|
||||
assert.match(src, /if \(isBatch\) ramBatchFetch\(\);/);
|
||||
// the preset warning does not apply to the per-card resolution
|
||||
const warn = extractFunc(html, 'spPresetWarningRender');
|
||||
@@ -364,6 +377,104 @@ test('a stale fetch guard is released so the report recovers (v3.28.7)', async (
|
||||
app.resetFetching();
|
||||
});
|
||||
|
||||
test('the batch group carries the three operations (v3.29.0)', () => {
|
||||
assert.match(html, /<optgroup label="Batch \(per card\)" data-l10n="Batch \(per card\)">/);
|
||||
assert.match(html, /<option value="batch" data-l10n="Batch install \(\.cap file, per card\)">/);
|
||||
assert.match(html, /<option value="batch-reinstall" data-l10n="Batch reinstall app \(per card\)">/);
|
||||
assert.match(html, /<option value="batch-delete" data-l10n="Batch delete \(per card\)">/);
|
||||
// the group sits at the end of the operation list
|
||||
const sel = /<select id="ram-op"[\s\S]*?<\/select>/.exec(html)[0];
|
||||
assert.ok(sel.indexOf('value="batch-delete"') > sel.indexOf('value="make-selectable"'));
|
||||
// the batch delete fields and the delete-package option exist
|
||||
assert.match(html, /id="ram-del-aid"/);
|
||||
assert.match(html, /id="ram-del-target"/);
|
||||
assert.match(html, /id="ram-del-package"/);
|
||||
assert.match(html, /'Batch reinstall app \(per card\)': /);
|
||||
assert.match(html, /'Batch delete \(per card\)': /);
|
||||
assert.match(html, /'Deleted': /);
|
||||
assert.match(html, /'Reinstalled': /);
|
||||
});
|
||||
|
||||
test('ramBatchStart sends the kind-specific body (v3.29.0)', async () => {
|
||||
// the delete kind: the AID and the target
|
||||
clearEls();
|
||||
setEl('ram-op', 'batch-delete');
|
||||
setEl('ram-del-aid', 'f0414c4641610101');
|
||||
setEl('ram-del-target', 'package');
|
||||
app.fetched = [];
|
||||
await app.ramBatchStart();
|
||||
assert.deepStrictEqual(app.bodies['/api/ram-batch/start'],
|
||||
{ kind: 'delete', aid: 'F0414C4641610101', target: 'package', ram_format: 'auto' });
|
||||
// a short/odd AID is refused before anything is sent
|
||||
clearEls();
|
||||
setEl('ram-op', 'batch-delete');
|
||||
setEl('ram-del-aid', 'F041');
|
||||
setEl('ram-del-target', 'app');
|
||||
app.fetched = [];
|
||||
app.alerts = [];
|
||||
await app.ramBatchStart();
|
||||
assert.deepStrictEqual(app.fetched, []);
|
||||
assert.match(app.alerts.join(' '), /5-16 bytes/);
|
||||
// the reinstall kind: the install-app fields, no .cap
|
||||
clearEls();
|
||||
setEl('ram-op', 'batch-reinstall');
|
||||
setEl('ram-app-loadfile', 'f0414c46416101');
|
||||
setEl('ram-app-module', 'f0414c4641610101');
|
||||
setEl('ram-app-instance', '');
|
||||
setEl('rc-priv', '00');
|
||||
setEl('rc-toolkit-enable', '');
|
||||
setEl('ram-make-sel', '');
|
||||
app.fetched = [];
|
||||
await app.ramBatchStart();
|
||||
const body = app.bodies['/api/ram-batch/start'];
|
||||
assert.strictEqual(body.kind, 'reinstall-app');
|
||||
assert.strictEqual(body.loadfile_aid, 'F0414C46416101');
|
||||
assert.strictEqual(body.module_aid, 'F0414C4641610101');
|
||||
assert.strictEqual(body.install_params, 'C900');
|
||||
// the cap kind carries the delete-package flag (the shared field builder)
|
||||
assert.match(extractFunc(html, 'ramInstallCapFields'),
|
||||
/delete_package: !!\(document\.getElementById\('ram-del-package'\) \|\| \{\}\)\.checked/);
|
||||
});
|
||||
|
||||
test('the batch panel labels follow the run kind (v3.29.0)', () => {
|
||||
clearEls();
|
||||
setEl('ram-op', 'batch-delete');
|
||||
setEl('ram-batch-panel', '');
|
||||
setEl('ram-batch-start', '');
|
||||
setEl('ram-batch-stop', '');
|
||||
setEl('ram-batch-state', '');
|
||||
setEl('ram-batch-notice', '');
|
||||
setEl('ram-batch-counts', '');
|
||||
setEl('ram-batch-attempts', '');
|
||||
setEl('ram-batch-clear', '');
|
||||
setEl('ram-batch-spin', '');
|
||||
app.setLast({ kind: 'delete', active: true, phase: 'installing', attempts: [],
|
||||
counts: { installed: 2, failed: 0, skipped: 0 }, attempts_total: 2,
|
||||
current: { iccid: '8970119000004600098' } });
|
||||
app.ramBatchRender();
|
||||
assert.match(els['ram-batch-state'].textContent, /Deleting\.\.\. 8970119000004600098/);
|
||||
assert.match(els['ram-batch-counts'].textContent, /Deleted: 2/);
|
||||
app.setLast({ kind: 'reinstall-app', active: true, phase: 'waiting_card',
|
||||
attempts: [], counts: {}, attempts_total: 0 });
|
||||
app.ramBatchRender();
|
||||
assert.match(els['ram-batch-counts'].textContent, /Reinstalled: 0/);
|
||||
});
|
||||
|
||||
test('the attempts render the per-step tail for short chains (v3.29.0)', () => {
|
||||
const out = app.ramBatchAttemptsHtml([
|
||||
{ iccid: '8970119000004600098', result: 'installed',
|
||||
steps: [{ name: 'DELETE (application)', sw: '6101', ok: true },
|
||||
{ name: 'INSTALL [for install]', sw: '6101', ok: true }] },
|
||||
{ iccid: '8970119000004600106', result: 'installed',
|
||||
steps: [{ name: 'DELETE (application)', sw: '6A88', ok: true, not_found: true },
|
||||
{ name: 'INSTALL [for install]', sw: '6101', ok: true }] },
|
||||
]);
|
||||
assert.match(out, /DELETE \(application\) 6101 · INSTALL \[for install\] 6101/);
|
||||
assert.match(out, /DELETE \(application\) 6A88 \(not found\)/);
|
||||
// a long LOAD chain keeps no tail (the failing-step detail covers it)
|
||||
assert.strictEqual(app.ramBatchStepsText({ steps: [{}, {}, {}, {}], reason: '' }), '');
|
||||
});
|
||||
|
||||
test('the stop and clear calls hit their endpoints', async () => {
|
||||
clearEls();
|
||||
app.fetched = [];
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "pysim-simple-server"
|
||||
version = "3.28.8"
|
||||
version = "3.29.0"
|
||||
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.
|
||||
|
||||
+223
-149
@@ -37,7 +37,7 @@ from osmocom.tlv import BER_TLV_IE
|
||||
from osmocom.utils import rpad
|
||||
|
||||
|
||||
VERSION = '3.28.8'
|
||||
VERSION = '3.29.0'
|
||||
|
||||
MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE
|
||||
|
||||
@@ -935,6 +935,21 @@ def _cap_make_selectable_apdu(instance_aid, privileges='00'):
|
||||
return '80E60800%02X%s' % (len(data) // 2, data)
|
||||
|
||||
|
||||
def _ram_delete_apdu(aid, cascade=False):
|
||||
"""GP DELETE (Card Spec v2.3.1 11.2, Tables 11-20/11-23): P1=00 (last/only
|
||||
command), P2.b8 selects 'object' ('00') or 'object and related objects'
|
||||
('80'), the data field carries the mandatory '4F' AID TLV. Case 3 (no
|
||||
trailing Le: on the card's SCP80 layer a trailing byte counts as a phantom
|
||||
second command whose SW masks the real result). The PWA's ramDeleteApdu
|
||||
(the Explore buttons) is the mirror; both suites pin the same bytes."""
|
||||
a = re.sub(r'[^0-9a-fA-F]', '', str(aid or '')).upper()
|
||||
if not a:
|
||||
return ''
|
||||
data = '4F' + _ber_len(len(a) // 2) + a
|
||||
return ('80E400' + ('80' if cascade else '00')
|
||||
+ _ber_len(len(data) // 2) + data).upper()
|
||||
|
||||
|
||||
def _cap_apdu_sequence(loadfile_aid, module_aid, loadfile_data, sd_aid='',
|
||||
privileges='00', install_params='', stk_params='',
|
||||
make_selectable=True, block_size=240, instance_aid=None,
|
||||
@@ -1614,6 +1629,12 @@ def _send_secured_packet(scc, sp_hex, oa_number, sm_sc=None, include_cpi=True,
|
||||
# GlobalPlatform "more data available" (GET STATUS pages).
|
||||
_RAM_REMOTE_SW_OK = ('9000', 'CAFE')
|
||||
|
||||
# A GP DELETE whose object is already gone: the delete's goal is met, so a
|
||||
# delete step (the batch delete, the reinstall's instance delete, the
|
||||
# delete-package pre-step) tolerates these and the chain proceeds (6A88 =
|
||||
# referenced data not found; 6A82 = file/application not found).
|
||||
_RAM_DELETE_NOT_FOUND = ('6A88', '6A82')
|
||||
|
||||
|
||||
def _ram_remote_sw_ok(sw):
|
||||
s = str(sw or '').upper()
|
||||
@@ -2401,9 +2422,11 @@ def _ram_nv_fields(nv_before, nv_after):
|
||||
def _ram_install_plan(body):
|
||||
"""The card-independent part of a CAP install: parse the .cap, build the
|
||||
INSTALL [for load] -> LOAD xN -> INSTALL [for install] chain and read the
|
||||
transport knobs. Nothing here touches the card, so the batch builds the
|
||||
plan once and reuses it for every card. Raises ValueError with the
|
||||
caller's message (the endpoint and the batch answer 400)."""
|
||||
transport knobs. `delete_package` prepends a cascade DELETE on the package
|
||||
AID (a re-install over a leftover package; the delete step tolerates 'not
|
||||
found' - see _ram_plan_run). Nothing here touches the card, so the batch
|
||||
builds the plan once and reuses it for every card. Raises ValueError with
|
||||
the caller's message (the endpoint and the batch answer 400)."""
|
||||
cap_hex = str(body.get('cap_hex') or '').replace(' ', '')
|
||||
if not cap_hex:
|
||||
raise ValueError('No cap_hex provided')
|
||||
@@ -2439,21 +2462,102 @@ def _ram_install_plan(body):
|
||||
block_size=block_size,
|
||||
nv_quota=body.get('nv_quota'),
|
||||
volatile_quota=body.get('volatile_quota'))
|
||||
return {'loadfile_aid': loadfile_aid, 'module_aid': module_aid, 'seq': seq,
|
||||
'block_size': block_size, 'block_size_req': block_size_req,
|
||||
steps = []
|
||||
for i, apdu in enumerate(seq):
|
||||
if i == 0:
|
||||
name = 'INSTALL [for load]'
|
||||
elif i == len(seq) - 1:
|
||||
name = 'INSTALL [for install]'
|
||||
else:
|
||||
name = 'LOAD (%d/%d)' % (i, len(seq) - 2)
|
||||
steps.append({'name': name, 'apdu': apdu})
|
||||
if body.get('delete_package'):
|
||||
# A leftover package blocks INSTALL [for load]: the cascade delete
|
||||
# clears the ELF, its modules and its applications first. It is a
|
||||
# delete step, so 'not found' (a fresh card) lets the chain proceed.
|
||||
steps.insert(0, {'name': 'DELETE (package + applications)',
|
||||
'apdu': _ram_delete_apdu(loadfile_aid, cascade=True),
|
||||
'delete': True})
|
||||
return {'kind': 'install-cap', 'seq': steps,
|
||||
'spi2': str(body.get('spi2') or '01'),
|
||||
'include_cpi': body.get('includeCpi', True),
|
||||
'ram_format': _ram_normalize_format(body.get('ram_format'))}
|
||||
'ram_format': _ram_normalize_format(body.get('ram_format')),
|
||||
'nv': True, 'load_size': block_size,
|
||||
'response': {'load_file_aid': loadfile_aid, 'module_aid': module_aid,
|
||||
'load_block_size': block_size,
|
||||
'load_block_size_requested': block_size_req,
|
||||
'load_block_size_auto': not block_size_req}}
|
||||
|
||||
|
||||
def _ram_install_run(server, body, plan, scc):
|
||||
"""Run one CAP install chain on `scc` - the caller holds the card lock.
|
||||
def _ram_delete_plan(body):
|
||||
"""The batch delete plan: one GP DELETE step. `target` selects the P2
|
||||
semantics (GP Card Spec v2.3.1 11.2): 'app' deletes the object only (an
|
||||
applet instance, P2=00), 'package' the object and its related objects (the
|
||||
load file with its modules and installed applications, P2=80). Raises
|
||||
ValueError (400 at Start)."""
|
||||
aid = re.sub(r'[^0-9a-fA-F]', '', str(body.get('aid') or '')).upper()
|
||||
if not re.fullmatch(r'(?:[0-9A-F]{2}){5,16}', aid):
|
||||
raise ValueError('aid must be 5-16 bytes of hex (an AID)')
|
||||
target = str(body.get('target') or 'app').strip().lower()
|
||||
if target not in ('app', 'package'):
|
||||
raise ValueError("target must be 'app' or 'package'")
|
||||
cascade = target == 'package'
|
||||
return {'kind': 'delete',
|
||||
'seq': [{'name': 'DELETE (package + applications)' if cascade
|
||||
else 'DELETE (application)',
|
||||
'apdu': _ram_delete_apdu(aid, cascade), 'delete': True}],
|
||||
'spi2': str(body.get('spi2') or '01'),
|
||||
'include_cpi': body.get('includeCpi', True),
|
||||
'ram_format': _ram_normalize_format(body.get('ram_format')),
|
||||
'response': {'aid': aid, 'target': target}}
|
||||
|
||||
The per-card part: the preset's ISD TAR supplies the keyset (v3.26.0) and
|
||||
the counter is seeded from the store (v3.20.0). Raises ValueError for a
|
||||
request error (the endpoint and the batch answer 400 / record the skip); a
|
||||
chain failure comes back as ``{'success': False, ...}``. Returns the
|
||||
response dict. Shared by /api/ram-install and the batch worker."""
|
||||
|
||||
def _ram_reinstall_plan(body, kind='reinstall-app'):
|
||||
"""The reinstall plan: DELETE the applet instance (tolerating 'not found' -
|
||||
a fresh card just installs), then INSTALL [for install] from the already
|
||||
loaded package. No .cap and no LOAD: the same parameters the single
|
||||
INSTALL [for install] uses, validated here (400 at Start, never per card).
|
||||
`kind` is 'install-app' for the single operation, 'reinstall-app' for the
|
||||
batch (only the progress/log label differs)."""
|
||||
loadfile = str(body.get('loadfile_aid') or '').replace(' ', '').upper()
|
||||
module = str(body.get('module_aid') or '').replace(' ', '').upper()
|
||||
if not loadfile or not module:
|
||||
raise ValueError('loadfile_aid and module_aid required')
|
||||
instance = (str(body.get('instance_aid') or '').replace(' ', '').upper()
|
||||
or module)
|
||||
install_apdu = _cap_install_apdu(
|
||||
loadfile, module, instance_aid=instance,
|
||||
privileges=str(body.get('privileges') or '').replace(' ', '') or '00',
|
||||
install_params=str(body.get('install_params') or '').replace(' ', ''),
|
||||
stk_params=str(body.get('stk_params') or '').replace(' ', ''),
|
||||
make_selectable=body.get('make_selectable', True),
|
||||
nv_quota=body.get('nv_quota'),
|
||||
volatile_quota=body.get('volatile_quota'))
|
||||
return {'kind': kind,
|
||||
'seq': [{'name': 'DELETE (application)',
|
||||
'apdu': _ram_delete_apdu(instance, cascade=False),
|
||||
'delete': True},
|
||||
{'name': 'INSTALL [for install]', 'apdu': install_apdu}],
|
||||
'spi2': str(body.get('spi2') or '01'),
|
||||
'include_cpi': body.get('includeCpi', True),
|
||||
'ram_format': _ram_normalize_format(body.get('ram_format')),
|
||||
'response': {'load_file_aid': loadfile, 'module_aid': module,
|
||||
'instance_aid': instance}}
|
||||
|
||||
|
||||
def _ram_plan_run(server, body, plan, scc):
|
||||
"""Run one batch plan on `scc` - the caller holds the card lock.
|
||||
|
||||
A plan is a list of named steps (an INSTALL/LOAD chain, GP DELETEs, a
|
||||
single INSTALL [for install]); a step flagged `delete` tolerates the 'not
|
||||
found' status words (6A88/6A82 - the object is already gone, so the
|
||||
delete's goal is met and the chain proceeds), while any other failure stops
|
||||
it. The per-card part: the preset's ISD TAR supplies the keyset (v3.26.0)
|
||||
and the counter is seeded from the store (v3.20.0). Raises ValueError for
|
||||
a request error (the endpoint and the batch answer 400 / record the skip);
|
||||
a chain failure comes back as ``{'success': False, ...}``. Returns the
|
||||
response dict. Shared by /api/ram-install, /api/ram-install-app and the
|
||||
batch worker."""
|
||||
preset = _preset_by_id(server, body.get('preset_id'))
|
||||
tar = str(body.get('tar') or presets.role_tar(preset, 'isd')
|
||||
or '000000').strip().upper()
|
||||
@@ -2475,11 +2579,12 @@ def _ram_install_run(server, body, plan, scc):
|
||||
'tar': tar, 'kic_key': kic_key, 'kid_key': kid_key,
|
||||
'include_cpi': plan['include_cpi']}
|
||||
seq = plan['seq']
|
||||
sys.stderr.write('RAM-INSTALL: LOAD block size %d bytes\n' % plan['block_size'])
|
||||
kind = plan.get('kind') or 'ram'
|
||||
label = 'RAM-%s' % kind.upper().replace('-', ' ')
|
||||
|
||||
# Live progress for the PWA's operation modal (the request holds _CARD_LOCK
|
||||
# until the chain is done; /api/status carries this)
|
||||
_ram_progress_begin('install-cap', len(seq) + 1)
|
||||
_ram_progress_begin(kind, len(seq) + 1)
|
||||
nv_before = None
|
||||
nv_after = None
|
||||
try:
|
||||
@@ -2489,55 +2594,58 @@ def _ram_install_run(server, body, plan, scc):
|
||||
if ram_format == 'auto':
|
||||
_ram_progress_step(0, 'FORMAT CHECK')
|
||||
ram_format = _ram_detect_format(server, scc, sp_state, state)
|
||||
sys.stderr.write('RAM-INSTALL: %d APDUs (INSTALL / %d x LOAD / INSTALL, %s) loadfile_aid=%s\n' % (
|
||||
len(seq), len(seq) - 2, ram_format, plan['loadfile_aid']))
|
||||
if plan.get('load_size'):
|
||||
sys.stderr.write('RAM-INSTALL: LOAD block size %d bytes\n' % plan['load_size'])
|
||||
sys.stderr.write('%s: %d step(s), %s\n' % (label, len(seq), ram_format))
|
||||
# NV footprint: free non-volatile memory before the chain (best effort
|
||||
# - a card without FF21 skips it).
|
||||
nv_before = _ram_read_ff21(server, scc, sp_state, state, ram_format, 'NV before')
|
||||
if plan.get('nv'):
|
||||
nv_before = _ram_read_ff21(server, scc, sp_state, state, ram_format, 'NV before')
|
||||
|
||||
for apdu_idx, gp_apdu in enumerate(seq):
|
||||
if apdu_idx == 0:
|
||||
step_name = 'INSTALL [for load]'
|
||||
elif apdu_idx == len(seq) - 1:
|
||||
step_name = 'INSTALL [for install]'
|
||||
else:
|
||||
step_name = 'LOAD (%d/%d)' % (apdu_idx, len(seq) - 2)
|
||||
_ram_progress_step(apdu_idx + 1, step_name)
|
||||
if not _ram_send_gp_apdu(server, scc, sp_state, state, step_name,
|
||||
_ram_format_apdu(gp_apdu, ram_format)):
|
||||
for step_idx, step in enumerate(seq):
|
||||
step_name = step['name']
|
||||
_ram_progress_step(step_idx + 1, step_name)
|
||||
ok = _ram_send_gp_apdu(server, scc, sp_state, state, step_name,
|
||||
_ram_format_apdu(step['apdu'], ram_format))
|
||||
if not ok and step.get('delete'):
|
||||
# A delete step tolerates 'not found': the object is already
|
||||
# gone (6A88/6A82), which is exactly what the delete wanted -
|
||||
# the chain proceeds. Any other failure stops it.
|
||||
rec = steps[-1] if steps else {}
|
||||
sw = str(rec.get('por_sw') or '').upper()
|
||||
if sw in _RAM_DELETE_NOT_FOUND:
|
||||
rec['delete_not_found'] = True
|
||||
ok = True
|
||||
if not ok:
|
||||
# What the failed attempt consumed (a rejected load may keep or
|
||||
# discard its partial file - the delta tells which).
|
||||
nv_after = _ram_read_ff21(server, scc, sp_state, state,
|
||||
ram_format, 'NV after')
|
||||
if plan.get('nv'):
|
||||
nv_after = _ram_read_ff21(server, scc, sp_state, state,
|
||||
ram_format, 'NV after')
|
||||
resp = {'success': False, 'steps': steps, 'failed_step': len(steps),
|
||||
'error': (state['encode_error'] or state['failure'].get('error')
|
||||
or ('%s failed' % step_name)),
|
||||
'final_cntr': state['cntr'], 'ram_format': ram_format,
|
||||
'load_file_aid': plan['loadfile_aid'],
|
||||
'module_aid': plan['module_aid'],
|
||||
'load_block_size': plan['block_size'],
|
||||
'load_block_size_requested': plan['block_size_req'],
|
||||
'load_block_size_auto': not plan['block_size_req']}
|
||||
'final_cntr': state['cntr'], 'ram_format': ram_format}
|
||||
resp.update(plan.get('response') or {})
|
||||
resp.update(_ram_nv_fields(nv_before, nv_after))
|
||||
failed_rec = steps[-1] if steps else {}
|
||||
resp.update(_cntr_low_fields(failed_rec.get('por_status')))
|
||||
if resp.get('cntr_low'):
|
||||
sys.stderr.write('RAM-INSTALL: low counter - the packet '
|
||||
'counter is not above the card\'s\n')
|
||||
sys.stderr.write('%s: low counter - the packet counter is '
|
||||
'not above the card\'s\n' % label)
|
||||
return resp
|
||||
# NV footprint: what the successful chain consumed.
|
||||
nv_after = _ram_read_ff21(server, scc, sp_state, state, ram_format, 'NV after')
|
||||
if plan.get('nv'):
|
||||
nv_after = _ram_read_ff21(server, scc, sp_state, state, ram_format, 'NV after')
|
||||
finally:
|
||||
_ram_progress_end()
|
||||
|
||||
resp = {'success': True, 'steps': steps, 'load_file_aid': plan['loadfile_aid'],
|
||||
'module_aid': plan['module_aid'], 'final_cntr': state['cntr'],
|
||||
'ram_format': ram_format, 'load_block_size': plan['block_size'],
|
||||
'load_block_size_requested': plan['block_size_req'],
|
||||
'load_block_size_auto': not plan['block_size_req']}
|
||||
resp = {'success': True, 'steps': steps, 'final_cntr': state['cntr'],
|
||||
'ram_format': ram_format}
|
||||
resp.update(plan.get('response') or {})
|
||||
resp.update(_ram_nv_fields(nv_before, nv_after))
|
||||
sys.stderr.write('RAM-INSTALL: Complete — loadfile_aid=%s module_aid=%s cntr=%s\n' % (
|
||||
plan['loadfile_aid'], plan['module_aid'], state['cntr']))
|
||||
sys.stderr.write('%s: Complete (%d step(s), cntr=%s)\n' % (
|
||||
label, len(seq), state['cntr']))
|
||||
return resp
|
||||
|
||||
|
||||
@@ -2587,7 +2695,12 @@ def _ram_batch_record(state, rec):
|
||||
state['counts'][result] += 1
|
||||
who = rec.get('iccid') or 'the card'
|
||||
if result == 'installed':
|
||||
state['notice'] = 'Card %s installed.' % who
|
||||
# the success verb follows the operation (the counts bucket is
|
||||
# 'installed' for every kind - "the operation succeeded")
|
||||
kind = ((state.get('plan') or {}).get('kind')) or 'install-cap'
|
||||
verb = {'delete': 'deleted',
|
||||
'reinstall-app': 'reinstalled'}.get(kind, 'installed')
|
||||
state['notice'] = 'Card %s %s.' % (who, verb)
|
||||
elif result == 'failed':
|
||||
step = rec.get('failed_step') or ''
|
||||
sw = rec.get('por_sw') or rec.get('sw') or ''
|
||||
@@ -2615,6 +2728,20 @@ def _ram_batch_failure_fields(result):
|
||||
'sw': rec.get('sw') or ''}
|
||||
|
||||
|
||||
def _ram_batch_steps_fields(result):
|
||||
"""The attempt's per-step verdict tail for the panel: the last three steps
|
||||
(name, ok, remote SW; a delete step that answered 'not found' is ok) plus
|
||||
the chain length - a full LOAD chain stays short in the report."""
|
||||
steps = result.get('steps') or []
|
||||
out = []
|
||||
for s in steps[-3:]:
|
||||
out.append({'name': s.get('name') or '',
|
||||
'ok': (not s.get('por_error')) or bool(s.get('delete_not_found')),
|
||||
'sw': s.get('por_sw') or '',
|
||||
'not_found': bool(s.get('delete_not_found'))})
|
||||
return {'steps': out, 'steps_total': len(steps)}
|
||||
|
||||
|
||||
def _ram_batch_handle_card(state, server):
|
||||
"""One equipped card: match the preset by ICCID, check its ISD TAR keyset
|
||||
and run the install under the card lock. A card already attempted in this
|
||||
@@ -2655,7 +2782,7 @@ def _ram_batch_handle_card(state, server):
|
||||
request_error = False
|
||||
try:
|
||||
with _CARD_LOCK:
|
||||
result = _ram_install_run(server, body, state['plan'], server.scc)
|
||||
result = _ram_plan_run(server, body, state['plan'], server.scc)
|
||||
except ValueError as e:
|
||||
# A request/eligibility error (the preset's ISD keyset, the plan): the
|
||||
# install never started - the card is skipped.
|
||||
@@ -2678,6 +2805,7 @@ def _ram_batch_handle_card(state, server):
|
||||
'load_file_aid': result.get('load_file_aid'),
|
||||
'module_aid': result.get('module_aid'),
|
||||
'nv_delta': result.get('nv_delta')}
|
||||
rec.update(_ram_batch_steps_fields(result))
|
||||
elif result is not None:
|
||||
outcome = 'failed'
|
||||
rec = {'iccid': iccid, 'preset': name, 'result': outcome,
|
||||
@@ -2685,6 +2813,7 @@ def _ram_batch_handle_card(state, server):
|
||||
'cntr_low': bool(result.get('cntr_low')),
|
||||
'error': result.get('error') or ''}
|
||||
rec.update(_ram_batch_failure_fields(result))
|
||||
rec.update(_ram_batch_steps_fields(result))
|
||||
elif request_error:
|
||||
outcome = 'skipped'
|
||||
rec = {'iccid': iccid, 'preset': name, 'result': outcome,
|
||||
@@ -2750,23 +2879,35 @@ def _ram_batch_worker(state):
|
||||
|
||||
|
||||
def _ram_batch_start(body):
|
||||
"""Validate the CAP/parameters (the plan is card independent, so it is built
|
||||
once) and spawn the worker (the worker reads `_server_ref` itself). Raises ValueError (400) for a bad request and
|
||||
RuntimeError (409) when another batch or a test run is active."""
|
||||
"""Validate the operation's parameters (the plan is card independent, so it
|
||||
is built once) and spawn the worker (the worker reads `_server_ref` itself).
|
||||
`kind` selects the operation: 'install-cap' (the .cap chain, optionally with
|
||||
the delete-package pre-step), 'delete' (one GP DELETE) or 'reinstall-app'
|
||||
(DELETE the instance + INSTALL [for install]). Raises ValueError (400) for
|
||||
a bad request and RuntimeError (409) when another batch or a test run is
|
||||
active."""
|
||||
global _RAM_BATCH
|
||||
with _RAM_BATCH_LOCK:
|
||||
if _RAM_BATCH and _RAM_BATCH.get('active'):
|
||||
raise RuntimeError('a batch is already running - stop it first')
|
||||
if _TEST_RUNNING:
|
||||
raise RuntimeError('a test run is active - stop it first')
|
||||
plan = _ram_install_plan(body)
|
||||
kind = str(body.get('kind') or 'install-cap').strip().lower()
|
||||
if kind == 'install-cap':
|
||||
plan = _ram_install_plan(body)
|
||||
elif kind == 'delete':
|
||||
plan = _ram_delete_plan(body)
|
||||
elif kind == 'reinstall-app':
|
||||
plan = _ram_reinstall_plan(body)
|
||||
else:
|
||||
raise ValueError("kind must be 'install-cap', 'delete' or 'reinstall-app'")
|
||||
state = _ram_batch_new_state(plan)
|
||||
with _RAM_BATCH_LOCK:
|
||||
_RAM_BATCH = state
|
||||
threading.Thread(target=_ram_batch_worker, args=(state,),
|
||||
name='ram-batch', daemon=True).start()
|
||||
sys.stderr.write('RAM-BATCH: started (%d APDUs, loadfile_aid=%s)\n'
|
||||
% (len(plan['seq']), plan['loadfile_aid']))
|
||||
sys.stderr.write('RAM-BATCH: started (%s, %d step(s))\n'
|
||||
% (plan['kind'], len(plan['seq'])))
|
||||
return state
|
||||
|
||||
|
||||
@@ -2800,9 +2941,11 @@ def _ram_batch_status():
|
||||
return {'active': False, 'phase': 'idle', 'attempts': [],
|
||||
'attempts_total': 0, 'current': None, 'notice': '',
|
||||
'counts': {'installed': 0, 'failed': 0, 'skipped': 0},
|
||||
'started': None, 'stopped': None, 'progress': None}
|
||||
'started': None, 'stopped': None, 'progress': None,
|
||||
'kind': None}
|
||||
return {'active': bool(_RAM_BATCH.get('active')),
|
||||
'phase': _RAM_BATCH.get('phase'),
|
||||
'kind': ((_RAM_BATCH.get('plan') or {}).get('kind')) or None,
|
||||
'attempts': list(_RAM_BATCH['attempts']),
|
||||
'attempts_total': _RAM_BATCH['attempts_total'],
|
||||
'current': dict(_RAM_BATCH['current']) if _RAM_BATCH.get('current') else None,
|
||||
@@ -2823,6 +2966,7 @@ def _ram_batch_summary():
|
||||
cur = _RAM_BATCH.get('current')
|
||||
return {'active': bool(_RAM_BATCH.get('active')),
|
||||
'phase': _RAM_BATCH.get('phase'),
|
||||
'kind': ((_RAM_BATCH.get('plan') or {}).get('kind')) or None,
|
||||
'current': dict(cur) if cur else None,
|
||||
'notice': _RAM_BATCH.get('notice') or '',
|
||||
'attempts_total': _RAM_BATCH['attempts_total'],
|
||||
@@ -10040,7 +10184,7 @@ class PysimHandler(BaseHTTPRequestHandler):
|
||||
# then one run on the equipped card - both shared with the
|
||||
# batch worker (v3.28.0).
|
||||
plan = _ram_install_plan(body)
|
||||
resp = _ram_install_run(self.server, body, plan, scc)
|
||||
resp = _ram_plan_run(self.server, body, plan, scc)
|
||||
except ValueError as e:
|
||||
err = {'success': False, 'error': str(e)}
|
||||
self._send_json(err, 400)
|
||||
@@ -10118,107 +10262,37 @@ class PysimHandler(BaseHTTPRequestHandler):
|
||||
return
|
||||
try:
|
||||
mode = (body.get('mode') or 'install').strip()
|
||||
loadfile_aid = (body.get('loadfile_aid') or '').replace(' ', '').upper()
|
||||
module_aid = (body.get('module_aid') or '').replace(' ', '').upper()
|
||||
instance_aid = ((body.get('instance_aid') or '').replace(' ', '').upper()
|
||||
or module_aid)
|
||||
privileges = (body.get('privileges') or '').replace(' ', '') or '00'
|
||||
install_params_hex = (body.get('install_params') or '').replace(' ', '')
|
||||
stk_params_hex = (body.get('stk_params') or '').replace(' ', '')
|
||||
make_selectable = bool(body.get('make_selectable', True))
|
||||
if mode == 'make_selectable':
|
||||
instance_aid = ((body.get('instance_aid') or '')
|
||||
.replace(' ', '').upper())
|
||||
if not instance_aid:
|
||||
err = {'success': False, 'error': 'instance_aid required'}
|
||||
self._send_json(err, 400)
|
||||
self._log_resp(err)
|
||||
return
|
||||
apdu = _cap_make_selectable_apdu(instance_aid, privileges)
|
||||
step_name = 'INSTALL [for make selectable]'
|
||||
privileges = (body.get('privileges') or '').replace(' ', '') or '00'
|
||||
plan = {'kind': 'make-selectable',
|
||||
'seq': [{'name': 'INSTALL [for make selectable]',
|
||||
'apdu': _cap_make_selectable_apdu(instance_aid,
|
||||
privileges)}],
|
||||
'spi2': str(body.get('spi2') or '01'),
|
||||
'include_cpi': body.get('includeCpi', True),
|
||||
'ram_format': _ram_normalize_format(body.get('ram_format'))}
|
||||
elif mode == 'install':
|
||||
# The install deletes the applet instance first (tolerating
|
||||
# 'not found') so re-installing with new parameters works in
|
||||
# one step; the plan builder validates the AIDs and the
|
||||
# parameter composition (400 here, never mid-chain).
|
||||
plan = _ram_reinstall_plan(body, kind='install-app')
|
||||
else:
|
||||
if not loadfile_aid or not module_aid:
|
||||
err = {'success': False,
|
||||
'error': 'loadfile_aid and module_aid required'}
|
||||
self._send_json(err, 400)
|
||||
self._log_resp(err)
|
||||
return
|
||||
try:
|
||||
apdu = _cap_install_apdu(
|
||||
loadfile_aid, module_aid, instance_aid=instance_aid,
|
||||
privileges=privileges, install_params=install_params_hex,
|
||||
stk_params=stk_params_hex, make_selectable=make_selectable,
|
||||
nv_quota=body.get('nv_quota'),
|
||||
volatile_quota=body.get('volatile_quota'))
|
||||
except ValueError as e:
|
||||
err = {'success': False, 'error': str(e)}
|
||||
self._send_json(err, 400)
|
||||
self._log_resp(err)
|
||||
return
|
||||
step_name = 'INSTALL [for install]'
|
||||
# SCP80 params: the SPI1 comes from the MSL of the packet's
|
||||
# TAR (the operation uses the preset's table); a caller-supplied
|
||||
# value is only the fallback for a TAR the preset does not carry.
|
||||
preset = _preset_by_id(self.server, body.get('preset_id'))
|
||||
tar = str(body.get('tar') or presets.role_tar(preset, 'isd')
|
||||
or '000000').strip().upper()
|
||||
try:
|
||||
spi1, _ = _spi1_for_tar(preset, tar, body.get('spi1'),
|
||||
prefer_msl=True)
|
||||
except ValueError as e:
|
||||
err = {'success': False, 'error': str(e)}
|
||||
self._send_json(err, 400)
|
||||
self._log_resp(err)
|
||||
return
|
||||
sp_state = {
|
||||
'spi1': spi1, 'spi2': body.get('spi2', '01'),
|
||||
'tar': tar, 'cntr': body.get('cntr', '00000000'),
|
||||
'include_cpi': body.get('includeCpi', True)}
|
||||
# The keyset: the preset's default for the operation's TAR
|
||||
# (v3.26.0) unless the request names KIc/KID itself - then the
|
||||
# number must be consistent and defined in the preset
|
||||
# (TS 102 225 A.2 / the "keyset must exist" rule) - checked
|
||||
# before the format probe consumes a counter.
|
||||
kvn, kic, kid, keyset_err = self._op_keyset(body, tar)
|
||||
if keyset_err:
|
||||
err = {'success': False, 'error': keyset_err}
|
||||
self._send_json(err, 400)
|
||||
self._log_resp(err)
|
||||
return
|
||||
sp_state['kic'] = kic
|
||||
sp_state['kid'] = kid
|
||||
sp_state['kic_key'] = body.get('kicKey', '')
|
||||
sp_state['kid_key'] = body.get('kidKey', '')
|
||||
# The counter: explicit `cntr` wins, else the preset's value
|
||||
# for this keyset (the server owns the counters, v3.20.0).
|
||||
sp_state['cntr'] = _preset_counter_seed(self.server, body, kvn) or sp_state['cntr']
|
||||
state = {'steps': [], 'encode_error': None, 'failure': {},
|
||||
'cntr': sp_state['cntr'], 'preset_id': body.get('preset_id'),
|
||||
'kvn': kvn}
|
||||
# Live progress for the PWA's operation modal (see /api/ram-install)
|
||||
_ram_progress_begin('install-app', 2)
|
||||
try:
|
||||
# RAM command format: detected per operation (a read-only
|
||||
# probe step) unless the caller pinned compact/expanded.
|
||||
ram_format = _ram_normalize_format(body.get('ram_format'))
|
||||
if ram_format == 'auto':
|
||||
_ram_progress_step(0, 'FORMAT CHECK')
|
||||
ram_format = _ram_detect_format(self.server, scc, sp_state, state)
|
||||
apdu = _ram_format_apdu(apdu, ram_format)
|
||||
_ram_progress_step(1, step_name)
|
||||
sys.stderr.write('RAM-INSTALL-APP: %s (%s, %s) cntr=%s\n' % (
|
||||
apdu, step_name, ram_format, sp_state['cntr']))
|
||||
ok = _ram_send_gp_apdu(self.server, scc, sp_state, state, step_name, apdu)
|
||||
finally:
|
||||
_ram_progress_end()
|
||||
resp = {'success': bool(ok), 'steps': state['steps'],
|
||||
'final_cntr': state['cntr'], 'ram_format': ram_format}
|
||||
if not ok:
|
||||
resp['error'] = (state['encode_error'] or state['failure'].get('error')
|
||||
or ('%s failed' % step_name))
|
||||
resp['failed_step'] = len(state['steps'])
|
||||
failed_rec = state['steps'][-1] if state['steps'] else {}
|
||||
resp.update(_cntr_low_fields(failed_rec.get('por_status')))
|
||||
raise ValueError("mode must be 'install' or 'make_selectable'")
|
||||
resp = _ram_plan_run(self.server, body, plan, scc)
|
||||
self._send_json(resp)
|
||||
self._log_resp(resp)
|
||||
except ValueError as e:
|
||||
err = {'success': False, 'error': str(e)}
|
||||
self._send_json(err, 400)
|
||||
self._log_resp(err)
|
||||
except Exception as e:
|
||||
sys.stderr.write('RAM-INSTALL-APP error: %s\n' % e)
|
||||
err = {'success': False, 'error': str(e)}
|
||||
|
||||
@@ -1095,7 +1095,7 @@ class PresetStoreHttpTests(unittest.TestCase):
|
||||
captured = []
|
||||
|
||||
def fake_step(server, scc, sp, state, name, apdu, silent=False):
|
||||
captured.append((sp['spi1'], sp['tar']))
|
||||
captured.append((name, sp['spi1'], sp['tar']))
|
||||
state['steps'].append({'name': name, 'por_status': 'por_ok', 'por_sw': '9000'})
|
||||
return True
|
||||
|
||||
@@ -1122,7 +1122,12 @@ class PresetStoreHttpTests(unittest.TestCase):
|
||||
finally:
|
||||
for patch in reversed(patches):
|
||||
patch.stop()
|
||||
self.assertEqual(captured, [('1A', '000000'), ('1A', '000000')])
|
||||
# the install deletes the instance first (v3.29.0), then installs;
|
||||
# make selectable stays a single step - all at the TAR's MSL
|
||||
self.assertEqual(captured, [
|
||||
('DELETE (application)', '1A', '000000'),
|
||||
('INSTALL [for install]', '1A', '000000'),
|
||||
('INSTALL [for make selectable]', '1A', '000000')])
|
||||
|
||||
def test_an_install_app_without_an_msl_is_refused_before_the_card(self):
|
||||
# no preset and no spi1: the resolution must refuse with the hint
|
||||
|
||||
+216
-14
@@ -30,10 +30,14 @@ from pysim_simple_server import server as srv
|
||||
from pysim_simple_server.server import PysimHandler, _CARD_LOCK
|
||||
|
||||
|
||||
def _plan():
|
||||
return {'seq': ['80E60200', '80E80000', '80E60000'], 'loadfile_aid': 'F0414C46416101',
|
||||
'module_aid': 'F0414C4641610101', 'block_size': 240, 'block_size_req': None,
|
||||
'spi2': '01', 'include_cpi': True, 'ram_format': 'compact'}
|
||||
def _plan(kind='install-cap'):
|
||||
return {'kind': kind,
|
||||
'seq': [{'name': 'INSTALL [for load]', 'apdu': '80E60200'},
|
||||
{'name': 'LOAD (1/1)', 'apdu': '80E80000'},
|
||||
{'name': 'INSTALL [for install]', 'apdu': '80E60000'}],
|
||||
'spi2': '01', 'include_cpi': True, 'ram_format': 'compact',
|
||||
'response': {'load_file_aid': 'F0414C46416101',
|
||||
'module_aid': 'F0414C4641610101'}}
|
||||
|
||||
|
||||
class _FakeStore:
|
||||
@@ -142,7 +146,7 @@ class BatchCardTests(unittest.TestCase):
|
||||
return {'success': True, 'final_cntr': '0000000002',
|
||||
'load_file_aid': 'F0414C46416101', 'module_aid': 'F0414C4641610101'}
|
||||
|
||||
with mock.patch.object(srv, '_ram_install_run', fake_run):
|
||||
with mock.patch.object(srv, '_ram_plan_run', fake_run):
|
||||
srv._ram_batch_handle_card(st, _fake_server('8970119000004600098',
|
||||
_FakeStore([preset])))
|
||||
self.assertEqual(len(calls), 1)
|
||||
@@ -163,7 +167,7 @@ class BatchCardTests(unittest.TestCase):
|
||||
{'name': 'LOAD (1/1)', 'sw': '9000', 'por_sw': '6A80'}],
|
||||
'final_cntr': '0000000002'}
|
||||
|
||||
with mock.patch.object(srv, '_ram_install_run', fake_run):
|
||||
with mock.patch.object(srv, '_ram_plan_run', fake_run):
|
||||
srv._ram_batch_handle_card(st, _fake_server('8970119000004600098',
|
||||
_FakeStore([preset])))
|
||||
rec = st['attempts'][0]
|
||||
@@ -180,7 +184,7 @@ class BatchCardTests(unittest.TestCase):
|
||||
raise ValueError('TAR 000000 needs a default keyset (its MSL 16 '
|
||||
'requires security)')
|
||||
|
||||
with mock.patch.object(srv, '_ram_install_run', fake_run):
|
||||
with mock.patch.object(srv, '_ram_plan_run', fake_run):
|
||||
srv._ram_batch_handle_card(st, _fake_server('8970119000004600098',
|
||||
_FakeStore([preset])))
|
||||
rec = st['attempts'][0]
|
||||
@@ -195,7 +199,7 @@ class BatchCardTests(unittest.TestCase):
|
||||
def fake_run(*a, **k):
|
||||
raise OSError('card removed')
|
||||
|
||||
with mock.patch.object(srv, '_ram_install_run', fake_run):
|
||||
with mock.patch.object(srv, '_ram_plan_run', fake_run):
|
||||
srv._ram_batch_handle_card(st, _fake_server('8970119000004600098',
|
||||
_FakeStore([preset])))
|
||||
rec = st['attempts'][0]
|
||||
@@ -219,7 +223,7 @@ class BatchCardTests(unittest.TestCase):
|
||||
seen['current'] = st['current']
|
||||
return {'success': True}
|
||||
|
||||
with mock.patch.object(srv, '_ram_install_run', fake_run):
|
||||
with mock.patch.object(srv, '_ram_plan_run', fake_run):
|
||||
srv._ram_batch_handle_card(st, _fake_server('8970119000004600098',
|
||||
_FakeStore([preset])))
|
||||
self.assertEqual(seen['notice'], '') # cleared while installing
|
||||
@@ -230,7 +234,7 @@ class BatchCardTests(unittest.TestCase):
|
||||
st = srv._ram_batch_new_state(_plan())
|
||||
preset = _preset('8970119000004600098', 'Alfa')
|
||||
calls = []
|
||||
with mock.patch.object(srv, '_ram_install_run',
|
||||
with mock.patch.object(srv, '_ram_plan_run',
|
||||
lambda *a, **k: calls.append(1) or {'success': True}):
|
||||
srv._ram_batch_handle_card(st, _fake_server('8970119000004600098',
|
||||
_FakeStore([preset]), session=1))
|
||||
@@ -252,7 +256,7 @@ class BatchCardTests(unittest.TestCase):
|
||||
session=7)
|
||||
calls = []
|
||||
with mock.patch.object(srv, '_CARD_CONNECTED', True), \
|
||||
mock.patch.object(srv, '_ram_install_run',
|
||||
mock.patch.object(srv, '_ram_plan_run',
|
||||
lambda *a, **k: calls.append(1) or {'success': True}):
|
||||
t = threading.Thread(target=srv._ram_batch_worker, args=(st,), daemon=True)
|
||||
t.start()
|
||||
@@ -280,10 +284,13 @@ class RamInstallPlanTests(unittest.TestCase):
|
||||
with mock.patch.object(srv, '_cap_parse', lambda h: ('A1', 'A2', b'\x01')), \
|
||||
mock.patch.object(srv, '_cap_apdu_sequence', lambda *a, **k: ['80E60200']):
|
||||
plan = srv._ram_install_plan({'cap_hex': 'AA', 'load_block_size': '120'})
|
||||
self.assertEqual(plan['block_size'], 120)
|
||||
self.assertEqual(plan['load_size'], 120)
|
||||
self.assertEqual(plan['spi2'], '01')
|
||||
self.assertEqual(plan['ram_format'], 'auto')
|
||||
self.assertEqual(plan['seq'], ['80E60200'])
|
||||
self.assertEqual(plan['kind'], 'install-cap')
|
||||
self.assertEqual(plan['seq'][0]['name'], 'INSTALL [for load]')
|
||||
self.assertEqual(plan['seq'][0]['apdu'], '80E60200')
|
||||
self.assertNotIn('delete', plan['seq'][0])
|
||||
with self.assertRaises(ValueError):
|
||||
srv._ram_install_plan({'cap_hex': 'AA', 'load_block_size': 300})
|
||||
with self.assertRaises(ValueError):
|
||||
@@ -291,6 +298,201 @@ class RamInstallPlanTests(unittest.TestCase):
|
||||
with self.assertRaises(ValueError):
|
||||
srv._ram_install_plan({'cap_hex': ''})
|
||||
|
||||
def test_delete_package_prepends_the_cascade_delete(self):
|
||||
with mock.patch.object(srv, '_cap_parse',
|
||||
lambda h: ('F0414C46416101', 'A2', b'\x01')), \
|
||||
mock.patch.object(srv, '_cap_apdu_sequence',
|
||||
lambda *a, **k: ['80E60200', '80E80000', '80E60000']):
|
||||
plan = srv._ram_install_plan({'cap_hex': 'AA', 'delete_package': True})
|
||||
self.assertEqual(len(plan['seq']), 4)
|
||||
self.assertEqual(plan['seq'][0]['name'], 'DELETE (package + applications)')
|
||||
self.assertEqual(plan['seq'][0]['apdu'], '80E40080094F07F0414C46416101')
|
||||
self.assertTrue(plan['seq'][0]['delete'])
|
||||
self.assertEqual(plan['seq'][1]['name'], 'INSTALL [for load]')
|
||||
self.assertFalse(plan['seq'][1].get('delete', False))
|
||||
|
||||
|
||||
class RamDeletePlanTests(unittest.TestCase):
|
||||
"""The batch delete plan (v3.29.0): one GP DELETE - P2=00 for an applet
|
||||
instance, P2=80 (cascade) for a package with its related objects."""
|
||||
|
||||
def test_the_app_delete_is_object_only(self):
|
||||
plan = srv._ram_delete_plan({'aid': 'F0414C4641610101', 'target': 'app'})
|
||||
self.assertEqual(plan['kind'], 'delete')
|
||||
self.assertEqual(plan['seq'][0]['apdu'], '80E400000A4F08F0414C4641610101')
|
||||
self.assertTrue(plan['seq'][0]['delete'])
|
||||
self.assertIn('application', plan['seq'][0]['name'])
|
||||
self.assertEqual(plan['response'], {'aid': 'F0414C4641610101', 'target': 'app'})
|
||||
|
||||
def test_the_package_delete_cascades(self):
|
||||
plan = srv._ram_delete_plan({'aid': 'F0 41 4C 46 41 61 01',
|
||||
'target': 'package'})
|
||||
self.assertEqual(plan['seq'][0]['apdu'], '80E40080094F07F0414C46416101')
|
||||
self.assertIn('package', plan['seq'][0]['name'])
|
||||
|
||||
def test_the_target_defaults_to_the_application(self):
|
||||
plan = srv._ram_delete_plan({'aid': 'F0414C4641610101'})
|
||||
self.assertEqual(plan['seq'][0]['apdu'], '80E400000A4F08F0414C4641610101')
|
||||
|
||||
def test_the_plan_validates_the_aid_and_the_target(self):
|
||||
for bad in ('', 'F041', 'F0414C464161010', 'ZZZZZZZZZZ'):
|
||||
with self.assertRaises(ValueError):
|
||||
srv._ram_delete_plan({'aid': bad})
|
||||
with self.assertRaises(ValueError):
|
||||
srv._ram_delete_plan({'aid': 'F0414C4641610101', 'target': 'applet'})
|
||||
|
||||
|
||||
class RamReinstallPlanTests(unittest.TestCase):
|
||||
"""The reinstall plan (v3.29.0): DELETE the instance, then INSTALL [for
|
||||
install] from the loaded package - no .cap, no LOAD. The same plan serves
|
||||
the single INSTALL [for install] (kind 'install-app')."""
|
||||
|
||||
def test_the_plan_deletes_the_instance_then_installs(self):
|
||||
plan = srv._ram_reinstall_plan({'loadfile_aid': 'F0414C46416101',
|
||||
'module_aid': 'F0414C4641610101',
|
||||
'privileges': '00'})
|
||||
self.assertEqual(plan['kind'], 'reinstall-app')
|
||||
self.assertEqual(plan['seq'][0]['name'], 'DELETE (application)')
|
||||
self.assertEqual(plan['seq'][0]['apdu'], '80E400000A4F08F0414C4641610101')
|
||||
self.assertTrue(plan['seq'][0]['delete'])
|
||||
self.assertEqual(plan['seq'][1]['name'], 'INSTALL [for install]')
|
||||
self.assertEqual(plan['seq'][1]['apdu'],
|
||||
srv._cap_install_apdu('F0414C46416101', 'F0414C4641610101',
|
||||
instance_aid='F0414C4641610101',
|
||||
privileges='00'))
|
||||
self.assertEqual(srv._ram_reinstall_plan({'loadfile_aid': 'F0414C46416101',
|
||||
'module_aid': 'F0414C4641610101'},
|
||||
kind='install-app')['kind'],
|
||||
'install-app')
|
||||
|
||||
def test_the_plan_validates_the_aids_and_the_parameters(self):
|
||||
with self.assertRaises(ValueError):
|
||||
srv._ram_reinstall_plan({})
|
||||
with self.assertRaises(ValueError):
|
||||
srv._ram_reinstall_plan({'loadfile_aid': 'F0414C46416101'})
|
||||
with self.assertRaises(ValueError):
|
||||
srv._ram_reinstall_plan({'loadfile_aid': 'F0414C46416101',
|
||||
'module_aid': 'F0414C4641610101',
|
||||
'install_params': 'C900', 'nv_quota': 1})
|
||||
|
||||
|
||||
class RamPlanRunTests(unittest.TestCase):
|
||||
"""The runner's delete-step policy (v3.29.0): 'not found' (6A88/6A82)
|
||||
continues the chain - the delete's goal is met; any other delete failure
|
||||
stops it."""
|
||||
|
||||
def _run(self, por_sw):
|
||||
sends = []
|
||||
|
||||
def fake_send(server, scc, sp, state, step_name, apdu, silent=False):
|
||||
sends.append(step_name)
|
||||
if step_name.startswith('DELETE'):
|
||||
state['steps'].append({'name': step_name, 'por_status': 'por_ok',
|
||||
'por_sw': por_sw,
|
||||
'por_error': 'remote SW %s' % por_sw})
|
||||
state['failure']['error'] = '%s: remote SW %s' % (step_name, por_sw)
|
||||
return False
|
||||
state['steps'].append({'name': step_name, 'por_status': 'por_ok',
|
||||
'por_sw': '6101'})
|
||||
return True
|
||||
|
||||
plan = {'kind': 'reinstall-app', 'ram_format': 'compact', 'spi2': '01',
|
||||
'include_cpi': True,
|
||||
'seq': [{'name': 'DELETE (application)', 'apdu': '80E400',
|
||||
'delete': True},
|
||||
{'name': 'INSTALL [for install]', 'apdu': '80E600'}]}
|
||||
with mock.patch.object(srv, '_op_keyset_resolve',
|
||||
lambda server, body, tar: (3, '35', '35', None)), \
|
||||
mock.patch.object(srv, '_preset_counter_seed',
|
||||
lambda server, body, kvn: '0000000010'), \
|
||||
mock.patch.object(srv, '_ram_send_gp_apdu', fake_send):
|
||||
resp = srv._ram_plan_run(None, {'preset_id': 'p', 'spi1': '16',
|
||||
'tar': '000000'}, plan, None)
|
||||
return resp, sends
|
||||
|
||||
def test_a_not_found_delete_proceeds(self):
|
||||
resp, sends = self._run('6A88')
|
||||
self.assertTrue(resp['success'])
|
||||
self.assertEqual(sends, ['DELETE (application)', 'INSTALL [for install]'])
|
||||
self.assertTrue(resp['steps'][0]['delete_not_found'])
|
||||
|
||||
def test_another_delete_failure_stops_the_chain(self):
|
||||
resp, sends = self._run('6A80')
|
||||
self.assertFalse(resp['success'])
|
||||
self.assertEqual(sends, ['DELETE (application)'])
|
||||
self.assertEqual(resp['failed_step'], 1)
|
||||
self.assertIn('6A80', resp['error'])
|
||||
|
||||
|
||||
class BatchKindTests(unittest.TestCase):
|
||||
"""The batch start dispatches by kind (v3.29.0) and the status/summary
|
||||
carry it, so the panel's labels follow the operation."""
|
||||
|
||||
def setUp(self):
|
||||
srv._RAM_BATCH = None
|
||||
|
||||
def tearDown(self):
|
||||
srv._RAM_BATCH = None
|
||||
|
||||
def _start(self, body, plan=None):
|
||||
with mock.patch.object(srv, '_ram_batch_worker', lambda state: None), \
|
||||
mock.patch.object(srv.threading, 'Thread') as th:
|
||||
th.return_value = SimpleNamespace(start=lambda: None)
|
||||
if plan is not None:
|
||||
with mock.patch.object(srv, '_ram_install_plan',
|
||||
lambda b: plan):
|
||||
return srv._ram_batch_start(body)
|
||||
return srv._ram_batch_start(body)
|
||||
|
||||
def test_the_delete_kind(self):
|
||||
st = self._start({'kind': 'delete', 'aid': 'F0414C4641610101'})
|
||||
self.assertEqual(st['plan']['kind'], 'delete')
|
||||
self.assertEqual(srv._ram_batch_status()['kind'], 'delete')
|
||||
self.assertEqual(srv._ram_batch_summary()['kind'], 'delete')
|
||||
|
||||
def test_the_reinstall_kind(self):
|
||||
st = self._start({'kind': 'reinstall-app', 'loadfile_aid': 'F0414C46416101',
|
||||
'module_aid': 'F0414C4641610101'})
|
||||
self.assertEqual(st['plan']['kind'], 'reinstall-app')
|
||||
self.assertEqual(len(st['plan']['seq']), 2)
|
||||
|
||||
def test_the_default_kind_is_the_cap_install(self):
|
||||
st = self._start({'cap_hex': 'AA'}, plan=_plan())
|
||||
self.assertEqual(st['plan']['kind'], 'install-cap')
|
||||
|
||||
def test_an_unknown_kind_is_refused(self):
|
||||
with self.assertRaises(ValueError):
|
||||
self._start({'kind': 'wipe'})
|
||||
|
||||
def test_the_notice_verb_follows_the_kind(self):
|
||||
st = srv._ram_batch_new_state(_plan('delete'))
|
||||
srv._ram_batch_record(st, {'iccid': '8970119000004600098',
|
||||
'result': 'installed'})
|
||||
self.assertIn('deleted', st['notice'])
|
||||
st = srv._ram_batch_new_state(_plan('reinstall-app'))
|
||||
srv._ram_batch_record(st, {'iccid': '8970119000004600098',
|
||||
'result': 'installed'})
|
||||
self.assertIn('reinstalled', st['notice'])
|
||||
st = srv._ram_batch_new_state(_plan())
|
||||
srv._ram_batch_record(st, {'iccid': '8970119000004600098',
|
||||
'result': 'installed'})
|
||||
self.assertIn('installed', st['notice'])
|
||||
|
||||
def test_the_steps_fields_carry_the_tail(self):
|
||||
result = {'steps': [{'name': 'INSTALL [for load]', 'sw': '9000'},
|
||||
{'name': 'LOAD (1/1)', 'por_sw': '9000'},
|
||||
{'name': 'INSTALL [for install]', 'por_sw': '6101'},
|
||||
{'name': 'DELETE (application)', 'por_sw': '6A88',
|
||||
'delete_not_found': True,
|
||||
'por_error': 'remote SW 6A88'}]}
|
||||
fields = srv._ram_batch_steps_fields(result)
|
||||
self.assertEqual(fields['steps_total'], 4)
|
||||
self.assertEqual([x['name'] for x in fields['steps']],
|
||||
['LOAD (1/1)', 'INSTALL [for install]',
|
||||
'DELETE (application)'])
|
||||
self.assertTrue(fields['steps'][-1]['not_found'])
|
||||
self.assertTrue(fields['steps'][-1]['ok'])
|
||||
|
||||
|
||||
class BatchWorkerSourceTests(unittest.TestCase):
|
||||
def test_the_worker_waits_for_a_finished_equip(self):
|
||||
@@ -306,7 +508,7 @@ class BatchWorkerSourceTests(unittest.TestCase):
|
||||
session=9, ready=8)
|
||||
calls = []
|
||||
with mock.patch.object(srv, '_CARD_CONNECTED', True), \
|
||||
mock.patch.object(srv, '_ram_install_run',
|
||||
mock.patch.object(srv, '_ram_plan_run',
|
||||
lambda *a, **k: calls.append(1) or {'success': True}):
|
||||
t = threading.Thread(target=srv._ram_batch_worker, args=(st,), daemon=True)
|
||||
t.start()
|
||||
|
||||
Reference in New Issue
Block a user