17 Commits

Author SHA1 Message Date
catarrh 851ff65af6 changelog: note the README Telegram channel link
release / release (push) Waiting to run
2026-10-23 21:46:10 +03:00
catarrh 68a22e854b readme: link the project news Telegram channel
Add @youeyeseesee (releases and updates for my GitHub projects) near the
top of both READMEs.
2026-10-23 21:44:45 +03:00
catarrh 39d384636e batch waiting cue: move the contact pad to the bottom edge
The card's contact pad sat mid-card; it now sits near the bottom edge,
away from the top-right cut corner (the cut side is the card's top).
No other geometry or animation changes.

1024 Python + 1063 frontend; simple-v473.
2026-10-23 21:41:30 +03:00
catarrh 14d15b1fe8 batch waiting cue: use the app's SIM silhouette for the card
Redraw the inline card-into-reader SVG after sim.svg: a rounded card
with the top-right cut corner and the 3x2 contact pad, sliding behind
the reader bar.  The viewBox grew to 72x48 and the resting position is
0, so the card is no longer clipped at the top; the insert slide is
20px.  The currentColor/Tailwind theming and the reduced-motion
fallback are unchanged.

1024 Python + 1063 frontend; simple-v472.
2026-10-23 21:37:15 +03:00
catarrh 110dc1f1ad batch delete: report an already-absent object as such
A delete run whose DELETE answered 6A88/6A82 is still a success (the
object is gone, the goal is met) but it is no longer reported as a plain
"OK": the attempt result is 'already_absent', the counts gain that
bucket ("Deleted: N - Already absent: M"), the verdict reads "Already
absent" and the notice says the card was already absent.  A reinstall's
tolerated pre-delete keeps the install outcome (the install is the
point).

1024 Python + 1063 frontend; simple-v471.
2026-10-23 21:23:55 +03:00
catarrh fb65644328 v3.36.0: a deliberate "insert the next card" cue for batch runs
The waiting phase is the only moment the operator must act, so it now:
- shows a card-into-reader SVG animation in the status modal (inline
  SVG, CSS keyframes in src/style.css; reduced-motion users get the
  static card-above-the-slot picture);
- plays a short Web Audio chime once per card, edge-triggered in the
  2 s poll so it never repeats (the AudioContext is unlocked by the
  Start click);
- sets the tab title to "Insert next card" while the window is hidden;
- offers a speaker toggle next to the status (persisted as
  simple_ram_batch_sound, on by default).

The prebuilt style.css was rebuilt: it had gone stale and lacked
utilities the UI already used (bg-blue-50 on the batch notice,
inline-flex, mx-2, max-h-48, w-44, min-w-24).

1022 Python + 1062 frontend green; simple-v470.
2026-10-23 21:14:32 +03:00
catarrh 194b88e7d1 changelog: fold the unreleased 3.34.0 entry into 3.35.0
release / release (push) Waiting to run
v3.34.0 was committed but never tagged; its changes ship in the v3.35.0
release, so the changelog names only released versions (the same rule
applied to 3.32.3 -> 3.32.4 and 3.32.6/3.32.7 -> 3.33.0).
2026-10-23 20:11:59 +03:00
catarrh d5e67360fe file manager: an enabled inactive read pill is outlined, not grey
The Read decoded pill used the same grey fill as a disabled control, so
an enabled pill looked unavailable (reported live).  The inactive-but-
enabled state is now an outlined button (blue on white/slate) and the
grey fill plus dimming are reserved for the actually disabled state.
The active pill keeps a border of the same width so the row does not
shift.

Pinned in fs_edit.test.js.  1022 Python + 1058 frontend; simple-v469.
2026-10-23 17:06:50 +03:00
catarrh 87bf4acbbc v3.35.0: EF.SMSP decoded editor (the full SMS-parameter form)
TS 31.102 4.2.27 scope: alpha; TP-DA and TS-SCA with their own length
conventions (DA = digits, SCA = octets, 0 = default SMSC) and a TON
select; TP-PID; TP-DCS with TS 23.038 hints; the relative TP-VP with the
TS 23.040 ladder; a per-parameter "absent (the ME default applies)"
state.  Unchanged slots are copied verbatim (the live fixture's
DCS-absent 00 and present VP FF survive), alphanumeric TON-5 addresses
stay read-only, and every checked record is validated before any write.

Decoder fixes: the parameter-indicator bits were shifted (SCA on 5,
PID on 2, DCS on 3, VP on 4; the spec is DA/SCA/PID/DCS/VP on bits 1-5
with 6-8 reserved, 1 = absent), and the TP-DA length was read as octets
instead of digits (TS 23.040 9.1.2.5).

Live fixture: ffffffffffffffffffffffffffe9ffffffffffffffffffffffff07919733824009f0ffffffff0000ff
(SCA +79332804900, PID 00, DCS absent/00, VP FF) round-trips
byte-identically.

1022 Python + 1058 frontend green; simple-v468.
2026-10-23 16:54:12 +03:00
catarrh 180dd50c06 v3.34.0: Edit decoded wave 2 — service tables, scalars, PLMN lists, BCD, CB lists
New client-side editors: the service tables EF.UST/IST/EST (a filterable
bit grid; unmodelled bits and the file length preserved), EF.ACC,
EF.PHASE, EF.HPPLMN, EF.AD (op-mode enum, additional info, MNC length
incl. 0 and the 3-byte SIM form), the PLMN-only lists
EF.PLMNsel/FPLMN/EHPLMN, EF.ICCID/EF.IMSI behind a confirmation, and the
cell-broadcast lists EF.CBMI/CBMID/CBMIR.

Framework: a bits form kind, a per-entry model(dec) hook, a per-entry
confirm prompt and hex fields.

Decoder fixes: EF.ACC class numbering (byte1 b8..b1 = 15..8, byte2 =
7..0), EF.AD operation mode (whole byte) and its MNC length rules,
EF.SST decoded with the SIM 2-bit allocated/activated layout (editing
stays disabled until that UI exists), and the EF.IMSI parity nibble
(9 odd / 1 even).  A no-edit save of the new list files no longer
writes all-FF (the collected rows and the encoders disagreed on the
value shape).

Decoded editing for EF.DIR/EF.ARR/EF.SMS/EF.PBR is postponed by
decision (read-only viewing is enough); the survey records it.

1022 Python + 1033 frontend green; simple-v467.
2026-10-23 14:21:00 +03:00
catarrh 101ef21e9f changelog: fold the unreleased 3.32.6/3.32.7 entries into 3.33.0
release / release (push) Waiting to run
Both versions were committed but never tagged; their changes ship in the
v3.33.0 release, so the changelog names only released versions (the same
rule applied to 3.32.3 -> 3.32.4).
2026-10-23 12:18:35 +03:00
catarrh d036ef79a1 v3.33.0: Edit decoded — SPN, PLMNwAcT family, ADN family, PNN
The file manager gains a client-side decoded editor.  EF_ENCODERS pairs
each editable EF with a pure encoder and a form schema (fields / list /
items); the file is read raw, the decoder fills the form, the browser
re-encodes it and the existing update path writes it.

Shapes: SPN (display-condition bits + Annex A text), PLMNwAcT/
OPLMNwAcT/HPLMNwAcT (rows with PLMN + ACT, add/remove),
ADN/FDN/SDN/MSISDN/LND/BDN (one form per record with a checkbox that
auto-checks on change; BCD, length and EXT handling; BDN's own X+15
layout) and PNN (per-record TLV items 43/45/80, add/remove, unknown
TLVs preserved).

Unchanged fields are copied verbatim (SSC/extended-BCD numbers, PLMN
rows including unmodelled satellite ACT bits, PNN codings/AddCI bits,
all-FF files, the BDN comparison-method byte), every checked record is
validated and encoded before anything is written, and edits that would
break a reference are refused.

Also: docs/decoded-editing.md (the spec survey behind the design and
the release waves), the efPlmnAct EC-GSM-IoT-only 0x88 fix, the PNN
spare-bit decode fix, and the /api/write|read cmd2-exception
false-success fix.

1022 Python + 1013 frontend green; simple-v466.
2026-10-23 11:55:31 +03:00
catarrh 9411fd775d v3.32.7: drop the pySim JSON disclosure from the file manager
The decoded view is the client-side EF decoder's table over the raw
read's bytes.  The collapsed "pySim JSON (server)" block was an
unrelated second decode - opening it ran another /api/read with mode
'decoded' (pySim's read_binary_decoded/read_records_decoded on the
card) - and it existed only when our decoder covered the file, so it
could not serve as a cross-check where one would matter.

/api/read keeps mode 'decoded' for API callers.  Help, READMEs and the
project notes now state the table is built in the browser from the same
raw bytes; ef_decode.test.js pins the view's output and that no
mode 'decoded' read remains in the PWA.

1016 Python + 955 frontend green; simple-v465.
2026-10-23 09:50:09 +03:00
catarrh ded63c94e4 v3.32.6: the empty preset banner no longer flashes on the RAM view
ramOpChanged() (v3.28.0) un-hid #ram-preset-warning by the operation
kind, but the banner's text and visibility belong to
spPresetWarningRender() (which already suppresses it for the batch
ops).  With no warning text the empty amber box stayed visible until
the next status poll re-hid it - a flash on every RAM view / operation
switch.  The visibility is content-driven again: the setHidden line is
gone and the renderer is called.

Pinned in frontend/tests/ram.test.js with the real renderer (the
pre-fix code fails the test).  1016 Python + 954 frontend green;
simple-v464.
2026-10-23 09:31:06 +03:00
catarrh 1174a340db changelog: fold the unreleased 3.32.3 entry into 3.32.4
The v3.32.3 changes (the traceback tail and the link-release wording)
were committed together with v3.32.4 and never tagged separately, so
the changelog named a version no release exists for.  The entry is
merged into [3.32.4] and the published v3.32.4 release body matches it.
2026-10-21 15:05:55 +03:00
catarrh f92eb8f131 v3.32.5: a card removed during the startup init leaves it cardless
release / release (push) Has been cancelled
Live log: a card was pulled while the server started and the TERMINAL
PROFILE send raised CardConnectionException 0x80100016
(SCARD_E_NOT_TRANSACTED).  The init's except caught it (no crash), but
the code then continued with the card object it had already built:
card_present and _CARD_CONNECTED became True - a phantom equipped card -
and the auto-equip's _CARD_CONNECTED early return then skipped every
later insertion, leaving only a manual Equip or a restart.

A PC/SC error in the startup init now prints one line (a traceback is
noise for a normal removal), clears the locals that feed those flags and
calls _clear_app_card_state(app); the presence monitor equips the next
card.  Non-PC/SC errors keep their traceback.

Pinned in tests/test_startup_cardless.py (the branch and the flag
sources).  1016 Python + 953 frontend green; simple-v463.
2026-10-21 14:52:12 +03:00
catarrh 7e8c483eb1 v3.32.4: rebuild the link on card insertion (the first attempt)
release / release (push) Has been cancelled
v3.32.2 fixed the endless re-arm, but on Windows the first attempt
still failed on the old link (seen in the confirmation log: the
"self._con.connect()" traceback) and only the retry recovered - a failed
init, a 2-4 s delay and a traceback in the log.

The monitor's insert path now marks the transport stale under
_CARD_LOCK, so the first equip attempt rebuilds the PC/SC link.  It also
covers a link created at a cardless startup (never connected).  The
rebuild still happens in the equip worker, never in the pyscard monitor
thread, and _ensure_transport releases the old link first.

Docs: the READMEs' troubleshooting bullet.
Tests: an insertion marks the transport for the rebuild.
1014 Python + 953 frontend green; simple-v462.
2026-10-21 14:44:34 +03:00
25 changed files with 5290 additions and 124 deletions
+154
View File
@@ -9,6 +9,160 @@ every change carries a descriptive commit message.
## [Unreleased]
## [3.36.0] — 2026-10-23
### Added
- The batch **"waiting for the next card"** state is now a deliberate cue: the status
modal shows a card-into-reader animation next to the prompt, a short chime sounds
**once per card** when the run enters the waiting phase (Web Audio — no audio asset;
edge-triggered in the 2 s poll, so it does not repeat), and while the tab is hidden the
title hints "Insert next card". The speaker button next to the status mutes the chime
and the choice is remembered (`simple_ram_batch_sound`); the chime is unlocked by the
Start click (browser autoplay policy); reduced-motion users get the static card-above-
the-slot picture. The card follows the app's SIM silhouette (`sim.svg`): a cut corner
at the top and the contact pad near the bottom edge, sliding behind the reader bar.
### Changed
- A **batch delete that finds the object already gone** is still a success — the goal is met
and the run continues — but it is no longer reported as a plain "OK": the attempt's verdict
reads **Already absent**, the delete run's counts separate real deletions from no-ops
(`Deleted: N · Already absent: M`), and the notice says the card was already absent. The
counts gain an `already_absent` bucket (the attempt result is `already_absent`); a
reinstall's tolerated pre-delete is *not* affected — its outcome is still the install.
- The READMEs link the project's news channel: [Telegram — @youeyeseesee](https://t.me/youeyeseesee).
### Fixed
- The prebuilt `style.css` had gone stale: a rebuild restored utility classes the UI
already used but the output lacked (e.g. `bg-blue-50` on the batch notice, `inline-flex`,
`mx-2`, `max-h-48`, `w-44`, `min-w-24`).
## [3.35.0] — 2026-10-23
### Added
- **EF.SMSP decoded editor** — the full SMS-parameter form (TS 31.102 §4.2.27): alpha,
TP-Destination Address and TS-Service Centre Address (each with its own length
convention and a TON select; an empty SCA means the default SMSC), TP-PID, TP-DCS and
the relative TP-VP — every parameter with an *absent (the ME default applies)* toggle.
Live hints decode the DCS (TS 23.038 §4: coding group, alphabet, class, compression,
message-waiting) and the relative validity period (TS 23.040 §9.2.3.12.1). Unchanged
parameters keep their exact bytes (a card quirk such as a DCS marked absent with byte
`00` survives), alphanumeric TON addresses are preserved read-only, and all checked
records are validated before anything is written.
- **Edit decoded, wave 2** — new editors for the service tables **EF.UST / EF.IST /
EF.EST** (a filterable bit grid preserving services the table does not model), the
scalars **EF.ACC**, **EF.PHASE**, **EF.HPPLMN** and **EF.AD**, the PLMN lists
**EF.PLMNsel / EF.FPLMN / EF.EHPLMN**, the identities **EF.ICCID / EF.IMSI** (behind a
confirmation — writing them changes the card identity) and the cell-broadcast lists
**EF.CBMI / EF.CBMID / EF.CBMIR**. **EF.SST** is deliberately left read-only: its
SIM layout is 2 bits per service (allocated + activated), which needs its own editor.
- The file-format survey (`docs/decoded-editing.md`) now records the shipped waves and
marks **EF.DIR / EF.ARR / EF.SMS / EF.PBR** as postponed by decision (read-only).
### Fixed
- `efDecSmsp` mapped the parameter indicators shifted (SCA on bit 5, PID on bit 2, DCS
on bit 3, VP on bit 4); the spec order is DA/SCA/PID/DCS/VP on bits 1–5 with bits 6–8
reserved — a record with a mixed indicator (e.g. `E9`) showed DCS and hid VP.
- The read-only EF.SMSP view decoded the TP-DA length as octets, although it is the digit
count (TS 23.040 §9.1.2.5); a slot padded with `00` gained phantom digits.
- The **Read decoded** pill no longer looks disabled when it is enabled: an inactive-but-
enabled pill is now an outlined button (blue on white/slate), and the grey fill and
dimming are left to the actually disabled state.
- **EF.ACC** class numbering (TS 31.102 4.2.15): byte 1 b8…b1 codes classes 15…8 and
byte 2 classes 7…0 — the decoder counted class 0 at the MSB (an all-FF ACC read as
class 0 instead of classes 0–15).
- **EF.AD** (TS 31.102 4.2.18): the operation mode is a whole-byte enum
(00/80/01/81/02/04), not bits 0-1 plus a test bit; the MNC length may be 0 (service
n°130 available) or absent (3-byte SIM form) — the editor previously refused both.
- **EF.SST** (TS 51.011 10.3.7): the SIM service table is decoded with its 2-bits-per-
service layout (allocated + activated) instead of the UST 1-bit codec.
- **EF.IMSI**: the parity/type nibble is `9` for an odd digit count and `1` for an even
one (TS 24.008 10.5.1.4); the encoder always wrote `9`.
- Saving a PLMN/CB list with no edits no longer rewrites it as all-FF (the collected rows
and the list encoders disagreed on the value shape).
## [3.33.0] — 2026-10-23
### Added
- **Edit decoded** in the file manager: a file is read raw, decoded into its own editable
form, re-encoded in the browser and written through the normal update path. The first
wave covers **EF.SPN**, **EF.PLMNwAcT / EF.OPLMNwAcT / EF.HPLMNwAcT**, the dialling-number
files **EF.ADN / EF.FDN / EF.SDN / EF.MSISDN / EF.LND / EF.BDN** and **EF.PNN**. Shapes:
a flat transparent form (SPN), a transparent list editor with add/remove rows (PLMN+ACT),
per-record forms with a selection checkbox that is auto-checked when a field changes
(dialling numbers, PNN) and per-record TLV item rows with add/remove/edit (PNN's full and
short names plus the PLMN additional information). All checked records are encoded and
validated before anything is written; unchanged fields, record lengths, extension
pointers (EF.EXT1–EXT5), the BDN comparison-method byte and unknown PNN TLVs are
preserved, and an edit that would break a reference (a number continued in an extension
file, a dropped mandatory PNN full name) is refused.
- `docs/decoded-editing.md`: the file-format survey behind the design (every known EF
classified by structure/coding into the editor's shapes S0–S10, with the release waves).
### Removed
- The file manager's decoded view no longer carries the collapsed **pySim JSON (server)**
disclosure — the view is the client-side EF decoder's table only. The disclosure was an
unrelated *second* decode: opening it issued another `/api/read` with `mode: 'decoded'`
to run pySim's `read_binary_decoded`/`read_records_decoded`, and it was reachable only
when our decoder covered the file (`Read decoded` is decoder-gated), so the one case
where a second opinion would matter — a file our decoders do not cover — never offered
it. `/api/read` keeps `mode: 'decoded'` for API callers; the help, READMEs and project
notes say the decoded table is built in the browser from the same raw bytes.
### Fixed
- **EF.BDN** decodes and encodes with its own `X+15` layout (the trailing Comparison Method
Pointer); the shared ADN codec had treated it as `X+14`.
- `efPlmnAct` decoded the EC-GSM-IoT-only ACT (`b8=1,b4=1,b3=0`, `0x0088`) as nothing while
checking the impossible `0x0086` (TS 31.102 Table 4.2.5.2).
- PNN network names: the decoder now trusts a non-zero spare-bit count before the
"all bytes < 0x80 → uncompressed" heuristic, so a valid packed name can no longer be
misread (and later written back wrong).
- `/api/write` and `/api/read` report a cmd2 exception in the captured output as a failure
instead of `success: true` with `SW: 9000`.
- The empty amber **preset-mismatch banner** no longer flashes on the SCP80/RAM view.
`ramOpChanged()` (v3.28.0) un-hid `#ram-preset-warning` by the operation kind, but the
banner's text *and* visibility belong to `spPresetWarningRender()` (which also handles
the batch suppression): with no warning text the empty bordered box stayed visible until
the next status poll re-hid it — on every RAM view and operation switch. The visibility
is content-driven again; `frontend/tests/ram.test.js` pins it (the pre-fix code fails
the test).
## [3.32.5] — 2026-10-21
### Fixed
- A card removed during the **startup initialization** no longer leaves the server
reporting a phantom equipped card. The init caught the PC/SC error (and printed a
traceback) but kept the card object it had already built, so `card_present` and
`_CARD_CONNECTED` stayed True — and the auto-equip then skipped every later insertion
(its `_CARD_CONNECTED` early return), leaving only a manual Equip or a restart. A
PC/SC error now prints one line (`INIT: card removed during initialization …`), drops
the half-initialized session and lets the presence monitor equip the next card.
## [3.32.4] — 2026-10-21
### Fixed
- The auto-equip's captured traceback now always includes the **exception line**: cmd2
renders a rich-box traceback longer than the logged head, so the actual error was cut
off. The tail is appended after a `...` marker; the "releasing the old link failed"
message now says it continues with a fresh link (a removed card's dead PC/SC handle
always fails to disconnect).
- A card insertion now marks the PC/SC link for a rebuild, so the **first** auto-equip
attempt already uses a fresh link. On Windows the old link cannot connect after a
swap — v3.32.2 recovered it, but only on the second attempt, after a failed init, a
2–4 s delay and a traceback in the log. A link created at a cardless startup has
never connected either, which the same mark covers. The rebuild still runs in the
equip worker (never in the pyscard monitor thread) and `_ensure_transport` releases
the old link first.
## [3.32.2] — 2026-10-21
### Fixed
+6 -3
View File
@@ -2,6 +2,8 @@
> Formerly known as OTAMan — renamed to SIMple on 2026-09-19.
📢 Project news and updates: [Telegram channel](https://t.me/youeyeseesee)
SIMple is a workbench for UICC/SIM card specialists: an offline HTML/JS PWA plus a local card server ([`pysim-simple-server`](pysim_simple_server/), a pySim/PC-SC bridge that also serves the PWA). It covers the full lab cycle:
- **APDU workbench** — SIM/USIM RFM and Expanded Script builders (TS 102 226), GlobalPlatform RAM commands, a C-APDU/R-APDU parser and a response decoder (ISO 7816 / TS 102 221 / GP);
@@ -593,8 +595,9 @@ Connects to the bundled [`pysim-simple-server`](pysim_simple_server/) for live c
Browse the UICC filesystem in a tree view. Files are shown with names, FIDs, and AIDs (for ADFs). Selecting a file shows its detail pane: FID, file type, size / record layout and the decoded FCI (in a bordered block labelled with the file's symbolic name) above the content pane. When the FCI references its access rules through an EF.ARR record (security attribute `8B`), the referenced record's decoded rules are shown as indented lines under that row — resolved from the card's cached EF.ARR copies (MF, DF.TELECOM/DF.GSM and ADF.USIM, following the file's DF chain; a snapshot resolves them from its own captured copy).
- Entries are grouped with DFs above EFs and sorted by **FID** or symbolic **Name** (pills pinned above the tree together with **Probe all files**, remembered in `localStorage`)
- **Read raw** / **Read decoded** — read the selected file as a hex dump or as a decoded field table (client-side decoders for IMSI, ICCID, SPN, PLMN lists, LOCI/EPSLOCI, ADN/MSISDN, service tables, SUME, …); the highlighted pill is the current view and clicking either pill (re-)reads the file. The server-side pySim JSON of the same read stays behind a collapsed *pySim JSON (server)* disclosure; **Read decoded** is disabled when the client-side decoders do not cover the file (resolved by name/FID before reading)
- **Edit raw** — edit the raw hex data and **Save** to write back (or **Cancel**); transparent files use a textarea, record files one input per record with selection checkboxes. The decoded view is read-only — **Edit raw** switches to the hex view first and reads the file if needed
- **Read raw** / **Read decoded** — read the selected file as a hex dump or as a decoded field table (client-side decoders for IMSI, ICCID, SPN, PLMN lists, LOCI/EPSLOCI, ADN/MSISDN, service tables, SUME, …); the highlighted pill is the current view and clicking either pill (re-)reads the file. The decoded table is built in the browser from the same raw bytes (no server-side decode); **Read decoded** is disabled when the client-side decoders do not cover the file (resolved by name/FID before reading)
- **Edit raw** — edit the raw hex data and **Save** to write back (or **Cancel**); transparent files use a textarea, record files one input per record with selection checkboxes. **Edit raw** switches to the hex view first and reads the file if needed
- **Edit decoded** — edit through the file's own decoded form (client-side encoders; written via the normal update path): **EF.SPN**, **EF.PNN**, the PLMN lists **PLMNsel / FPLMN / EHPLMN** and **PLMNwAcT / OPLMNwAcT / HPLMNwAcT**, the dialling-number files **ADN / FDN / SDN / MSISDN / LND / BDN**, the service tables **UST / IST / EST** (**SST** stays read-only until the SIM allocated/activated editor exists), **ACC**, **PHASE**, **HPPLMN**, **AD**, **ICCID / IMSI** (behind a confirmation), the cell-broadcast lists **CBMI / CBMID / CBMIR**, and **EF.SMSP** (the full SMS-parameter form: alpha, TP-DA/TS-SCA with their addressing rules and TON, TP-PID, TP-DCS and relative TP-VP, each with an absent/default toggle and live DCS/validity hints). Transparent files get one form (PLMN rows and PNN items with add/remove); record files get one form per record with a checkbox that is auto-checked when a field changes, so only edited records are written. **Save** validates every selected record before writing; extension pointers (EF.EXT1–EF.EXT5) and the BDN comparison-method byte are preserved, and editing a number continued in an extension file is refused. The shape survey and the planned coverage are in `docs/decoded-editing.md`
- **Probe all files** — walks the whole tree (incl. custom files), marks every entry present/absent with *N / total* progress, stoppable, and ends with a summary; browsing itself stays lazy. Missing files are shown in red (✗); a present but empty DF shows `(empty)`
- **ADM** — files that need the administrator PIN fail with `6982`/`9804`; when the matching card preset (same ICCID) carries an ADM key, a **Verify ADM** button appears next to the error and the header badge (`ADM ✓/✗ ⚿`) becomes clickable. Every wrong key consumes an attempt (the remaining attempts are shown and a retry asks for confirmation); a blocked ADM needs the card's unblock key
@@ -799,7 +802,7 @@ its MSVC build chain) is not pulled in.
- **"Failed to establish context: Access denied"** — `pcscd` isn't running or the user lacks permission: `sudo systemctl enable --now pcscd && sudo usermod -a -G pcscd $USER`.
- **"device file /dev/ttyUSB0 does not exist"** — no serial reader; connect a USB reader or pass `-d` explicitly. The server still starts without a reader.
- **"Service not available" (0x8010001D) / reader gone after a `pcscd` restart** — the server recovers by itself: a watchdog revives pyscard's presence monitor and the next equip (automatic after re-insertion, or the **Equip** button) recreates the PC/SC transport (releasing the old link first). No server restart needed.
- **"Card was removed" errors / auto-equip fails after a card swap** — the auto-equip retries up to 3 times, and **every failed attempt rebuilds the PC/SC transport** before the next one: after a card swap on Windows the old link can stay unable to connect, which used to fail every retry and re-arm forever. The swallowed init error (with its traceback) is now printed as `AUTO-EQUIP: init output:`, and a card that is present but produced no card is a failure, not a silent "card gone". The watchdog re-arms with a doubling delay (5 s up to 60 s) while a card is present; a removed card clears the card state completely, so handlers answer "no card" instead of transmitting over the dead card.
- **"Card was removed" errors / auto-equip fails after a card swap** — a card insertion marks the PC/SC link for a rebuild (so the **first** attempt already uses a fresh link — on Windows the old one cannot connect after a swap), the auto-equip retries up to 3 times, and **every failed attempt rebuilds the transport** before the next one. A card removed while the server initializes at startup leaves it cardless with one line (no traceback), and the next insertion equips normally. The swallowed init error (with its traceback) is now printed as `AUTO-EQUIP: init output:`, and a card that is present but produced no card is a failure, not a silent "card gone". The watchdog re-arms with a doubling delay (5 s up to 60 s) while a card is present; a removed card clears the card state completely, so handlers answer "no card" instead of transmitting over the dead card.
### API reference
+6 -3
View File
@@ -2,6 +2,8 @@
> Прежнее название — OTAMan; переименован в SIMple 2026-09-19.
📢 Новости и обновления проекта: [Telegram-канал](https://t.me/youeyeseesee)
SIMple — инструментарий для специалистов по UICC/SIM-картам: автономный HTML/JS PWA и локальный сервер карт ([`pysim-simple-server`](pysim_simple_server/) — мост к pySim/PC-SC, он же раздаёт PWA). Закрывает весь цикл лабораторной работы:
- **APDU-верстак** — конструкторы SIM/USIM RFM и Expanded Script (TS 102 226), команды GlobalPlatform RAM, парсер C-APDU/R-APDU и декодер ответов (ISO 7816 / TS 102 221 / GP);
@@ -568,8 +570,9 @@ Delivery PoR (SPI2 `01`) проще — карта возвращает PoR на
Дерево файлов UICC. Отображаются имена, FID и AID (для ADF). При выборе файла открывается панель деталей: FID, тип файла, размер / структура записей и декодированный FCI (в блоке с рамкой и именем файла) над панелью содержимого. Когда FCI ссылается на свои правила доступа через запись EF.ARR (атрибут безопасности `8B`), правила указанной записи показываются отступом под этой строкой — они берутся из кэшированных копий EF.ARR карты (MF, DF.TELECOM/DF.GSM и ADF.USIM, по цепочке DF файла; снимок разрешает их из собственной захваченной копии).
- Элементы сгруппированы (DF выше EF) и отсортированы по **FID** или символьному **имени** (пиллы закреплены над деревом вместе с **«Проверить все файлы»**, выбор сохраняется в `localStorage`)
- **Read raw** / **Read decoded** — чтение выбранного файла как hex-дампа или как таблицы декодированных полей (клиентские декодеры: IMSI, ICCID, SPN, списки PLMN, LOCI/EPSLOCI, ADN/MSISDN, таблицы сервисов, SUME, …); подсвеченная пилла — текущий вид, клик по любой из них перечитывает файл. Серверный pySim JSON того же чтения остаётся в свёрнутом блоке *pySim JSON (server)*; **Read decoded** отключается, когда клиентские декодеры не покрывают файл (определяется по имени/FID до чтения)
- **Edit raw** — редактирование сырых hex-данных и **Save** для записи (или **Cancel**); для transparent-файлов — textarea, для record-файлов — по полю на запись с флажками выбора. Декодированный вид только для чтения — **Edit raw** сначала переключается на hex и при необходимости читает файл
- **Read raw** / **Read decoded** — чтение выбранного файла как hex-дампа или как таблицы декодированных полей (клиентские декодеры: IMSI, ICCID, SPN, списки PLMN, LOCI/EPSLOCI, ADN/MSISDN, таблицы сервисов, SUME, …); подсвеченная пилла — текущий вид, клик по любой из них перечитывает файл. таблица декодируется в браузере из тех же сырых байт (без серверного декодирования); **Read decoded** отключается, когда клиентские декодеры не покрывают файл (определяется по имени/FID до чтения)
- **Edit raw** — редактирование сырых hex-данных и **Save** для записи (или **Cancel**); для transparent-файлов — textarea, для record-файлов — по полю на запись с флажками выбора. **Edit raw** сначала переключается на hex и при необходимости читает файл
- **Edit decoded** — редактирование в собственной декодированной форме файла (клиентские кодировщики; запись обычным путём): **EF.SPN**, **EF.PNN**, списки PLMN **PLMNsel / FPLMN / EHPLMN** и **PLMNwAcT / OPLMNwAcT / HPLMNwAcT**, файлы номеров **ADN / FDN / SDN / MSISDN / LND / BDN**, таблицы сервисов **UST / IST / EST** (**SST** только для чтения до появления редактора allocated/activated), **ACC**, **PHASE**, **HPPLMN**, **AD**, **ICCID / IMSI** (с подтверждением) списки cell broadcast **CBMI / CBMID / CBMIR** и **EF.SMSP** (полная форма SMS-параметров: alpha, TP-DA/TS-SCA с их правилами адресации и TON, TP-PID, TP-DCS и относительный TP-VP; у каждого — флажок «отсутствует/по умолчанию» и живые подсказки по DCS и сроку действия). Для transparent-файла — одна форма (строки PLMN и элементы PNN с добавлением/удалением); для record-файла — форма на запись с флажком, который ставится автоматически при изменении поля, поэтому записываются только изменённые записи. **Save** проверяет все выбранные записи до записи; указатели расширений (EF.EXT1–EF.EXT5) и байт метода сравнения BDN сохраняются; редактирование номера, продолжение которого лежит в файле расширения, отклоняется. Обзор форматов и план охвата — в `docs/decoded-editing.md`
- **Проверить все файлы** — обход всего дерева (включая пользовательские) с пометкой «есть/нет», прогрессом *N / всего*, возможностью остановки и сводкой в конце; сам просмотр остаётся ленивым. Отсутствующие файлы показаны красным (✗); существующий пустой DF — `(пусто)`
- **ADM** — файлы, требующие администраторский PIN, возвращают `6982`/`9804`; если в подходящем пресете карты (тот же ICCID) есть ключ ADM, рядом с ошибкой появляется кнопка **«Проверить ADM»**, а значок в заголовке (`ADM ✓/✗ ⚿`) становится кликабельным. Каждый неверный ключ расходует попытку (остаток показывается, повторная попытка требует подтверждения); заблокированный ADM требует ключа разблокировки карты
@@ -783,7 +786,7 @@ pysim-simple-server --http-port 8080
- **"Failed to establish context: Access denied"** — `pcscd` не запущен или нет прав: `sudo systemctl enable --now pcscd && sudo usermod -a -G pcscd $USER`.
- **"device file /dev/ttyUSB0 does not exist"** — нет serial-ридера; подключите USB-ридер или укажите `-d`. Сервер всё равно стартует без ридера.
- **"Service not available" (0x8010001D) / ридер пропал после перезапуска `pcscd`** — сервер восстанавливается сам: сторож перезапускает монитор присутствия pyscard, а следующий equip (автоматически после повторной вставки карты или кнопкой **Equip**) пересоздаёт транспорт PC/SC (сначала освобождая старый линк). Перезапуск сервера не нужен.
- **Ошибки "Card was removed" / equip не срабатывает после смены карты** — авто-equip повторяется до 3 раз, и **каждая неудачная попытка пересоздаёт транспорт PC/SC** перед следующей: после смены карты в Windows старое соединение может остаться неспособным подключиться, из-за чего раньше все повторы падали, а сторож перезапускал попытки бесконечно. Проглоченная ошибка инициализации (с трассировкой) теперь печатается как `AUTO-EQUIP: init output:`, а «карта есть, но карта не создана» — это ошибка, а не тихое «карта пропала». Сторож повторяет с удвоением задержки (5 с … 60 с), пока карта в ридере; снятие карты полностью очищает состояние, поэтому обработчики отвечают «нет карты», а не шлют APDU мёртвой карте.
- **Ошибки "Card was removed" / equip не срабатывает после смены карты** — вставка карты помечает соединение PC/SC для пересоздания (поэтому **первая** попытка уже использует свежее соединение — в Windows старое после смены карты подключиться не может), авто-equip повторяется до 3 раз, и **каждая неудачная попытка пересоздаёт транспорт** перед следующей. Карта, снятая во время инициализации при старте сервера, оставляет его без карты с одной строкой (без трассировки), а следующая вставка инициализируется обычным образом. Проглоченная ошибка инициализации (с трассировкой) теперь печатается как `AUTO-EQUIP: init output:`, а «карта есть, но карта не создана» — это ошибка, а не тихое «карта пропала». Сторож повторяет с удвоением задержки (5 с … 60 с), пока карта в ридере; снятие карты полностью очищает состояние, поэтому обработчики отвечают «нет карты», а не шлют APDU мёртвой карте.
### Справочник API
+224
View File
@@ -0,0 +1,224 @@
# Decoded editing — file-format survey and design
Status: design note for the **Edit decoded** feature. Waves 1–3 shipped in
v3.33.0 (SPN/PLMN/ADN/PNN), v3.34.0 (service tables/scalars/PLMN lists/BCD/CB)
and v3.35.0 (EF.SMSP); decoded editing for EF.DIR/EF.ARR/EF.SMS/EF.PBR is
**postponed by decision** (their decoded view is enough). It classifies
the EFs the workbench knows (the client-side decoder registry) by the spec's
own structure and content coding, assigns each an editing *shape*, and sets the
release waves. The reference is the pinned specs (`projects/docs/uicc/`):
3GPP TS 31.102 v18.10 (USIM), TS 51.011 v4.15 (SIM), TS 24.008 v18.8 (network
name / PLMN / address coding), TS 102 221 (file structure / Annex A text).
pySim is a cross-check only.
The editor is client-side, like the decoders: the file is read raw, decoded
into its schema's values, edited, re-encoded and written with `/api/write`.
Only the files listed with a shape have an encoder; everything else keeps the
raw editor.
## 1. Editing shapes
Each shape is one renderer + one encoder contract. `fields` are the flat form
values, `list` is a transparent file holding N items, `items` is a record
holding a TLV list; the record-scoped variants reuse the same field/item
renderers inside a per-record block with a "write this record" checkbox
(auto-checked when an input changes).
| Shape | File / record content | Editor |
|---|---|---|
| **S0** read-only | card- or network-managed values (location, keys, counters) | no editor; the button says why |
| **S1** scalar/flag TR | one or a few bytes / flags | one form |
| **S2** text TR | Annex A (TS 102 221) or NAI text | one form, text field |
| **S3** BCD TR | ICCID / IMSI (nibble-swapped BCD, parity/type nibble) | one form, digits |
| **S4** PLMN list TR | n × 3-byte PLMN (TS 24.008) | item rows, add/remove |
| **S5** PLMN+ACT list TR | n × (3-byte PLMN + 2-byte ACT) | item rows, add/remove, tech multi-select |
| **S6** bit/service list TR | service or identifier bit lists | item list with labels (checkbox per service/id) |
| **S7** fixed-record fields | alpha + BCD address + ids (ADN family), LAI+id (OPL), code+alpha+category (ECC), TP parameters (SMSP) | per-record form + checkbox |
| **S8** record TLV list | PNN (name TLVs), DIR (application templates), ARR (access rules), PBR (A8/A9/AA containers) | per-record item rows, add/remove/edit; unknown TLVs preserved |
| **S9** nested structures | PBR containers, DIR templates, EXT chains | later: a tree, or read-only with a structured view |
| **S10** dedicated builders | SMS (TPDU), SMSP (TPDU parameter set), key material | later: a dedicated form, not a generic encoder |
Rules that apply to every encoder (and are what the tests pin):
- encode **only the edited values**, from the original record/file bytes:
length, padding and anything the schema does not expose are preserved;
- never change the file/record length; refuse when the new value does not fit;
- preserve the original text coding when the text is unchanged, and choose a
spec-legal coding when it changed (Annex A: unpacked GSM 7-bit / UCS-2; PNN
network name: packed GSM 7-bit with the spare-bit count / UCS-2);
- refuse (not silently truncate) when an edit would invalidate a cross-file
reference (`EF.EXT1` chain, PBR/IAP, OPL→PNN record id, CMI pointer).
## 2. Survey of the known files
`Wave` is the release that adds the encoder. Files we do not decode yet are
listed only where they matter for the plan; the spec's ~120 USIM EFs include
many specialized ones (MMS, VGCS/VBS, ePDG, IMS configuration, …) that are not
editing candidates.
### 2.1 Transparent files
| EF | FID | Spec | Size | Content | Shape | Wave |
|---|---|---|---|---|---|---|
| EF.ICCID | 2FE2 | TS 51.011 10.3.1 | 10 B | BCD (nibble-swapped), F padding; UPDATE is NEV per TS 102 221 | S3 | **3.34** |
| EF.IMSI | 6F07 | TS 31.102 4.2.2 | 9 B | length byte + parity/type nibble ('9' odd / '1' even) + BCD | S3 | **3.34** |
| EF.IST | 6F07 | TS 31.102 4.2.2 (SAIP) | 1–2 B | service bits | S6 | **3.34** |
| EF.UST | 6F38 | TS 31.102 4.2.8 | X ≥ 1 | service bits | S6 | **3.34** |
| EF.SST | 6F38 | TS 51.011 10.3.7 | X ≥ 1 | **2 bits/service** (allocated + activated) | S6 (pending) | decoder fixed; editor later |
| EF.EST | 6F56 | TS 31.102 4.2.47 | X ≥ 1 | enabled-service bits (3 services) | S6 | **3.34** |
| EF.LI | 6F05 | TS 31.102 4.2.1 | 2n | ISO 639 letters (2 B each) | S6 | 3.35 |
| EF.LP / EF.PL | 6F05 / 2F05 | TS 51.011 10.3.1 / 10.1.2 | 1–n | **1 byte per language** (DCS language coding) | S6 | 3.35 |
| EF.SPN | 6F46 | TS 31.102 4.2.12 | 17 B | condition byte + Annex A text | S2 | **3.33** |
| EF.PLMNsel | 6F30 | TS 51.011 10.3.4 | 3n (n ≥ 8) | PLMN list | S4 | **3.34** |
| EF.FPLMN | 6F7B | TS 31.102 4.2.16 | 3n (n ≥ 4) | PLMN list (FFFFFF holes anywhere) | S4 | **3.34** |
| EF.EHPLMN | 6FD9 | TS 31.102 4.2.84 | 3n | PLMN list | S4 | **3.34** |
| EF.PLMNwAcT | 6F60 | TS 31.102 4.2.5 | 5n (n ≥ 8) | PLMN + ACT | S5 | **3.33** |
| EF.OPLMNwAcT | 6F61 | TS 31.102 4.2.53 | 5n (n ≥ 8) | PLMN + ACT | S5 | **3.33** |
| EF.HPLMNwAcT | 6F62 | TS 31.102 4.2.54 | 5n (n ≥ 1) | PLMN + ACT | S5 | **3.33** |
| EF.SPDI | 6FCD | TS 31.102 4.2.66 | x | TLV: A3 > 80 > PLMN list | S4 | 3.35 |
| EF.GID1 / GID2 | 6F3E / 6F3F | TS 31.102 4.2.10/4.2.11 | n | identifier bytes (coding not stated in the clause) | S2 | 3.35 (verify TS 31.101) |
| EF.SUUME | 6F54 | TS 51.011 10.3.34 | X+Y | alpha (Annex A) | S2 | 3.35 |
| EF.IMPI/DOMAIN/IMPU | 6F02/6F03/6F04 | TS 31.102 (ISIM) | X | NAI text | S2 | 3.35 |
| EF.ACC | 6F78 | TS 31.102 4.2.15 | 2 B | class bits (b8…b1 = 15…8 / 7…0; ACC 10 written 0) | S6 | **3.34** |
| EF.PHASE | 6FAE | TS 51.011 10.3.19 | 1 B | phase id (00/02/03) | S1 | **3.34** |
| EF.HPPLMN | 6F31 | TS 31.102 4.2.6 | 1 B | search-period multiplier | S1 | **3.34** |
| EF.AD | 6FAD | TS 31.102 4.2.18 | 4+X | op-mode byte enum, info, MNC length (may be 0 or absent) | S1 | **3.34** |
| EF.CBMI | 6F45 | TS 31.102 4.2.14 | 2n | CB message ids | S6 | **3.34** |
| EF.CBMID | 6F48 | TS 31.102 4.2.20 | 2n | CB message ids | S6 | **3.34** |
| EF.CBMIR | 6F50 | TS 31.102 4.2.22 | 4n | CB id ranges | S6 | **3.34** |
| EF.ACL | 6F57 | TS 31.102 4.2.48 | X | version byte + TLV list (DD APNs) | S8-TR | 3.35 |
| EF.LOCI | 6F7E | TS 31.102 4.2.17 | 11 B | TMSI/LAI/status | **S0** | — |
| EF.PSLOCI | 6F73 | TS 31.102 4.2.23 | 14 B | P-TMSI/RAI/status | **S0** | — |
| EF.EPSLOCI | 6FE3 | TS 31.102 4.2.91 | 18 B | GUTI/TAI/status | **S0** | — |
| EF.LOCIGPRS | 6F53 | TS 51.011 10.3.33 | 14 B | GPRS location | **S0** | — |
| EF.Kc / KcGPRS | 6F20 / 6F52 | TS 31.102 4.4.3.1/4.4.3.2 | 9 B | ciphering keys | **S0** | — |
| EF.Keys / KeysPS | 6F08 / 6F09 | TS 31.102 4.2.3/4.2.4 | 33 B | keys + status | **S0** | — |
### 2.2 Linear fixed / cyclic files
| EF | FID | Spec | Record | Content | Shape | Wave |
|---|---|---|---|---|---|---|
| EF.ADN | 6F3A | TS 31.102 4.4.2.3 | X+14 | alpha + len + TON/NPI + 10 B BCD + CCP + EXT1 | S7 | **3.33** |
| EF.FDN | 6F3B | TS 31.102 4.2.24 | X+14 | ADN layout (EXT2) | S7 | **3.33** |
| EF.SDN | 6F49 | TS 31.102 4.2.29 | X+14 | ADN layout (EXT3) | S7 | **3.33** |
| EF.MSISDN | 6F40 | TS 31.102 4.2.26 | X+14 | ADN layout (EXT5) | S7 | **3.33** |
| EF.LND | 6F44 | TS 51.011 10.5.8 | X+14 | ADN layout, **cyclic** | S7 | **3.33** |
| EF.BDN | 6F4D | TS 31.102 4.2.44 | **X+15** | ADN layout + **Comparison Method Pointer** | S7 | **3.33** (fix) |
| EF.PNN | 6FC5 | TS 31.102 4.2.58 | X ≥ 3 | TLVs: 43 full / 45 short / 80 PLMN info | S8 | **3.33** |
| EF.OPL | 6FC6 | TS 31.102 4.2.59 | 8 | LAI (PLMN+LAC) + PNN record id | S7 | 3.35 |
| EF.ECC (SIM) | 6FB7 | TS 51.011 10.3.27 | 3n | emergency codes (TR) | S6 | 3.35 |
| EF.ECC (USIM) | 6FB7 | TS 31.102 4.2.21 | X+4 | code(3) + alpha(X) + category(1) | S7 | 3.35 |
| EF.EXT1–EXT5 | 6F4A–6F55 | TS 31.102 4.4.2.4 / 4.2.30/31/45/37 | 13 | type-dependent: additional data / subaddress | S9 | 3.36 |
| EF.SMSP | 6F42 | TS 31.102 4.2.27 | 28+Y | alpha + parameter indicators + TP-DA/SCA/PID/DCS/VP | S10 | **3.35** |
| EF.SMS | 6F3C | TS 31.102 4.2.25 | 176 | status byte + TPDU | S10 | **postponed (read-only)** |
| EF.ARR | 2F06/6F06 | TS 31.102 4.2.55 | X | access-rule TLVs (ISO 7816-4) | S8/S9 | **postponed (read-only)** |
| EF.DIR | 2F00 | TS 51.011 10.3.3 | X | application template TLVs (61 > 4F/50/73…) | S8/S9 | **postponed (read-only)** |
| EF.PBR | 4F30/4FXX | TS 31.102 4.4.2.1 | X | BER-TLV: A8/A9/AA containers, C0–CC links | S9 | **postponed (read-only)** |
| EF.EPSNSC | 6FE4 | TS 31.102 4.2.92 | — | NAS security context | **S0** | — |
| EF.ICI/OCI (call logs) | 6F80/6F81 | TS 31.102 4.2.33/4.2.34 | X+28/X+27 | ADN-like + time/date → cyclic, ME-written | **S0** (plan) | — |
### 2.3 Coverage vs the spec
The decoder registry covers ~45 EFs. The spec defines many more; the
remaining ones are either specialized protocols (MMS, VGCS/VBS, ePDG, IMS
configuration, GBA, 5G slice configuration) or administrative/key files that
must not be edited from a workbench without a dedicated flow (Keys, DCK,
network parameters, START-HFN). Nothing in the survey forces a new shape
beyond S9/S10.
## 3. Findings (defects and constraints the survey exposed)
1. **EF.BDN is X+15, not X+14.** The shared ADN codec assumes the last two
bytes are CCP1/EXT1; BDN adds a trailing **Comparison Method Pointer**
(`X+15`, TS 31.102 4.2.44). The read-only decoded view currently
mis-parses BDN records, and the v3.33.0 encoder would corrupt them. The
codec must take the layout (ADN vs BDN) and preserve the CMI pointer.
2. **EF.ECC has two layouts.** SIM: transparent 3n (TS 51.011 10.3.27);
USIM: linear fixed X+4 (TS 31.102 4.2.21). The decoder implements the SIM
layout only; on a USIM the alpha bytes are read as more codes. A
structure-aware decode is needed before an ECC editor.
3. **S8 is not one shape.** PNN (flat tag+length+value TLVs) is the simple
case. DIR/ARR/PBR nest TLVs and cross-reference other files; they need
either a tree editor or a structured read-only view first.
4. **References are part of the model.** EXT1–5 chains (ADN/MSISDN/BDN),
OPL→PNN record ids, PBR→(ADN/PBC/EXT/IAP) by FID/SFI. An editor that
ignores them can leave a card in a contradictory state: encoders must
preserve untouched references and refuse edits that would break one.
5. **Cyclic files edit like linear ones** for our purposes (write by record
number), but the card owns the order (LND/ICI/OCI/ACM); the editor notes
it and the write is still a plain UPDATE.
6. **FID collisions across DFs** (e.g. 6F02 = EF.IMPI in ISIM and EF.OCST in
USIM) make the fid-only decoder fallback unsafe; the name-based lookup is
the primary one and encoders should not be fid-only where a collision is
possible.
7. **Text coding is per file.** Annex A (TS 102 221) for SPN/ADN/SUUME;
the Network Name IE (TS 24.008 10.5.3.5a) for PNN, where the first byte is
`0x80 | spare` for packed GSM 7-bit and `0x90` for UCS-2 — the real-card
vector `43 0A 82 D4 32 BB BC 7E B7 5D E4 32 …` confirms it.
8. **EF.SST is a 2-bit table** (TS 51.011 10.3.7): 4 services per byte, first
bit of the pair = allocated, second = activated. The shared 1-bit
UST/EST codec was used for it, so SIM service tables were both misread and
miswritten; the decoder is fixed in v3.34.0, editing waits for an
allocated/activated UI.
9. **EF.ACC class numbering** (TS 31.102 4.2.15): byte 1 b8…b1 = classes 15…8
and byte 2 = 7…0 (class 10 is written as 0); the old decoder counted from
class 0 at the MSB. Fixed in v3.34.0.
10. **EF.AD's operation mode is a whole byte** (00/80/01/81/02/04), not bits
0-1 plus a test bit; the MNC-length byte may be 0 (service n°130 available)
or absent (3-byte SIM form). The decoder and encoder follow this now.
11. **The IMSI parity/type nibble** (TS 24.008 10.5.1.4) is 9 for an odd digit
count and 1 for an even one; the first encoder always wrote 9.
12. **SIM EF.LP/EF.PL use the 1-byte DCS language coding**, unlike the USIM
EF.LI's 2-byte ISO 639 letters — the shared decoder is wrong for the SIM
files (queued with the language work).
13. **EF.SMSP's parameter indicators are a five-bit presence map with inverted
meaning** (TS 31.102 §4.2.27: bit 1 DA, 2 SCA, 3 PID, 4 DCS, 5 VP; bit = 1
means *absent*; bits 6–8 reserved = 1). The decoder had the bits shifted
(SCA on 5, PID on 2, DCS on 3, VP on 4), so mixed indicators were misread.
14. **The two address slots use different length conventions**: TP-DA's length
is the digit count (TS 23.040 §9.1.2.5), TS-SCA's is the octet count of
TON/NPI + digits with 0 = default SMSC (TS 24.011 §8.2.5.2); the decoder
read the DA slot as octets. Both are handled explicitly now.
## 4. Release waves
- **v3.33.0 (shipped)** — framework: encoder registry, field/list/items
schemas, record checkboxes + auto-check, save/validation; shapes S2, S5,
S7, S8 for SPN, PLMNwAcT/OPLMNwAcT/HPLMNwAcT, ADN/FDN/SDN/MSISDN/LND,
PNN; the BDN layout fix; the `efPlmnAct` EC-GSM-IoT-only (`0x88`) decode fix.
- **v3.34.0 (shipped)** — the S1/S3/S4/S6 wave: UST/IST/EST service tables
(filterable bit grid), ACC, PHASE, HPPLMN, AD, the PLMN-only lists
(PLMNsel/FPLMN/EHPLMN), ICCID/IMSI (behind a confirmation) and
CBMI/CBMID/CBMIR. Decoder fixes: ACC class numbering, the AD operation
mode / MNC-length rules, the SST 2-bit layout (read-only for now) and the
IMSI parity nibble.
- **v3.35.0 (shipped)** — EF.SMSP as the first S10 dedicated builder: the full
parameter form (alpha; TP-DA and TS-SCA with their own length conventions
and TON; TP-PID; TP-DCS with TS 23.038 hints; the relative TP-VP with the
TS 23.040 ladder), a per-parameter *absent (the ME default applies)* state,
verbatim preservation of unchanged slots (including alphanumeric TON
addresses) and the parameter-indicator and TP-DA-length decoder fixes.
- **v3.36.0** — LI/LP/PL (the SIM files need the 1-byte DCS language coding
and a name-dependent decoder), GID1/GID2 (the clause states no coding —
verify TS 31.101 first), SPDI (the A3 > 80 wrapper), OPL (PNN record
select), ECC (SIM and USIM — needs a structure-aware decode), SUUME,
IMPI/DOMAIN/IMPU, ACL; EXT1–EXT5 as a read-only structured view.
- **Postponed by decision (read-only; do not implement without confirmation)**
— EF.DIR, EF.ARR, EF.SMS, EF.PBR: they rarely need manual changes and the
decoded view is enough.
- **Read-only forever (S0)** unless a dedicated flow appears: LOCI/PSLOCI/
EPSLOCI/EPSNSC/LOCIGPRS, Kc/KcGPRS, Keys/KeysPS, ACM, START-HFN/THRESHOLD,
call logs (ME-written), DCK/NETPAR.
## 5. Testing strategy
- **Round-trip**: for every encoder, `encode(decode(original), original)`
must reproduce the original bytes for spec-canonical records (the vectors
come from the spec's own examples, pySim's test data and real-card dumps).
- **Edit-one-field**: changing a field changes only that field's bytes
(padding, coding of untouched fields, ids and extension pointers stay).
- **Refusals**: over-capacity values, broken references, unsupported layouts
(BDN with the CMI pointer, EXT1 chains) throw an actionable error.
- **No capability by accident**: a file without an encoder must never show
the editor (gating test), and a schema/label drift guard keeps the editors
and decoders aligned.
File diff suppressed because one or more lines are too long
+5 -4
View File
@@ -324,13 +324,13 @@
<tr><td class="py-1 px-2">Install Package (.cap file)</td><td class="py-1 px-2">Sends a <code class="font-mono text-sm">.cap</code> file to the card via the server: INSTALL[for load] &rarr; LOAD &times;N &rarr; INSTALL[for install (+make selectable)]. The load file is split into LOAD APDUs of the <strong>LOAD block size</strong> (1&ndash;240 bytes of payload, 240 by default); each secured packet is delivered as a single SMS or, when larger, as a concatenated SMS-PP download of up to 5 segments, so a large <code class="font-mono text-sm">.cap</code> simply takes several SMS. As soon as the file is selected it is validated and its <strong>memory requirements are estimated</strong> in the form: the <strong>NVRAM requirement</strong> (the code image + persistent data, the GlobalPlatform Card Spec C6+C8 style total) and the RAM estimate. 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) &mdash; 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 &rarr; 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 &mdash; 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 &mdash; 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>) &mdash; 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 &mdash; 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 &mdash; 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 &ldquo;already processed&rdquo; &mdash; 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 &mdash; 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">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 <strong>Already absent</strong> and counted separately from real deletions &mdash; it is still a success (the goal is met) and the run continues; 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 &mdash; the point where the JCRE verifies the import list &mdash; 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 &ge; that version), and <strong>Set all 0.0</strong> drops every requirement. Two-step use: press Set all 0.0 &mdash; 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 &mdash; 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">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' &mdash; 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>
<p class="text-sm mb-3">A batch operation shows its status in a modal window: the live progress (or &ldquo;Insert next card. Waiting for a card...&rdquo;) on top and the processed cards below, with <strong>Stop</strong> and <strong>Clear</strong> inside. It opens on Start and on a page load while a run is active; <strong>Close</strong> stays disabled until the run ends &mdash; the run itself is server-side and survives a reload. After a finished run the panel&rsquo;s <strong>Show status</strong> button reopens the report.</p>
<p class="text-sm mb-3">A batch operation shows its status in a modal window: the live progress (or &ldquo;Insert next card. Waiting for a card...&rdquo;) on top and the processed cards below, with <strong>Stop</strong> and <strong>Clear</strong> inside. It opens on Start and on a page load while a run is active; <strong>Close</strong> stays disabled until the run ends &mdash; the run itself is server-side and survives a reload. After a finished run the panel&rsquo;s <strong>Show status</strong> button reopens the report. While the run waits for a card the modal shows a card-into-reader animation and plays a short chime once per card (the speaker button next to the status mutes it and the choice is remembered); when the window is in the background the tab title asks for the next card.</p>
<h4 id="ram-explorer" class="font-medium mb-1">Explorer View</h4>
<p class="text-sm mb-2">After &ldquo;Explore Card&rdquo; runs, the explorer displays:</p>
@@ -406,8 +406,9 @@
<h3 id="file-manager" class="text-lg font-medium mb-2">6.1 File manager</h3>
<p class="text-sm mb-2">The file system tree is displayed on the left; selecting a file opens its detail pane on the right. Entries are grouped with DFs above EFs and sorted by <strong>FID</strong> or symbolic <strong>Name</strong> (pills pinned above the scrolling tree together with <strong>Probe all files</strong>; the choice is remembered in <code class="font-mono text-sm">localStorage</code>). Selecting a file also shows its FID, file type, size / record layout and the decoded FCI — in a bordered block labelled with the file&rsquo;s symbolic name — above the content pane. When the FCI references its access rules through an <code class="font-mono text-sm">EF.ARR</code> record (security attribute <code class="font-mono text-sm">8B</code>), the rules of the referenced record are shown as indented lines under that row: they are resolved from the card&rsquo;s <code class="font-mono text-sm">EF.ARR</code> copies &mdash; the MF copy (<code class="font-mono text-sm">2F06</code>), the DF.TELECOM/DF.GSM copies (<code class="font-mono text-sm">6F06</code>) and the ADF.USIM copy (<code class="font-mono text-sm">6F06</code>): read once per card session at equip, refreshed on reset/equip and after an <code class="font-mono text-sm">EF.ARR</code> write; a snapshot resolves the same references from its own captured copy. The copy of the file&rsquo;s own DF wins, walking up to MF; a reference with no copy in that chain stays unresolved unless a single candidate exists.</p>
<ul class="list-disc list-inside text-sm space-y-1 mb-3">
<li><strong>Read raw / Read decoded</strong> — read the selected file as a hex dump or as a decoded field table (client-side EF decoders: IMSI, ICCID, SPN, PLMN lists, LOCI/PSLOCI/EPSLOCI, ADN/MSISDN, service tables, SUME, …); the server-side pySim JSON of the same read stays available in the collapsed <em>pySim JSON (server)</em> disclosure. The highlighted pill is the current view; clicking either pill (re-)reads the file. <strong>Read decoded</strong> is disabled when the client-side decoders do not cover the file (name/FID check, before reading).</li>
<li><strong>Edit raw</strong> — edit the raw hex data, <strong>Save</strong> to write back (or <strong>Cancel</strong>); transparent files use a textarea, record files one input per record with selection checkboxes. The decoded view is read-only — <strong>Edit raw</strong> switches to the hex view first and reads the file if needed.</li>
<li><strong>Read raw / Read decoded</strong> — read the selected file as a hex dump or as a decoded field table (client-side EF decoders: IMSI, ICCID, SPN, PLMN lists, LOCI/PSLOCI/EPSLOCI, ADN/MSISDN, service tables, SUME, …); the decoded table is built in the browser from the same raw bytes (no server-side decode). The highlighted pill is the current view; clicking either pill (re-)reads the file. <strong>Read decoded</strong> is disabled when the client-side decoders do not cover the file (name/FID check, before reading).</li>
<li><strong>Edit raw</strong> — edit the raw hex data, <strong>Save</strong> to write back (or <strong>Cancel</strong>); transparent files use a textarea, record files one input per record with selection checkboxes. <strong>Edit raw</strong> switches to the hex view first and reads the file if needed.</li>
<li><strong>Edit decoded</strong> — edit the file through its own decoded form, encoded in the browser and written with the normal update path. The encoders cover <strong>EF.SPN</strong>, <strong>EF.PNN</strong>, the PLMN lists (<strong>PLMNsel / FPLMN / EHPLMN</strong> and the access-technology lists <strong>PLMNwAcT / OPLMNwAcT / HPLMNwAcT</strong>), the dialling-number files (<strong>ADN / FDN / SDN / MSISDN / LND / BDN</strong>), the service tables (<strong>UST / IST / EST</strong>; <strong>SST</strong> stays read-only until the SIM allocated/activated editor exists), <strong>ACC</strong>, <strong>PHASE</strong>, <strong>HPPLMN</strong>, <strong>AD</strong>, <strong>ICCID / IMSI</strong> (behind a confirmation — writing them changes the card identity) the cell-broadcast lists (<strong>CBMI / CBMID / CBMIR</strong>) and <strong>EF.SMSP</strong> (the full SMS-parameter form: alpha, TP-DA and TS-SCA with their addressing rules and TON, TP-PID, TP-DCS and the relative TP-VP, each with an <em>absent (the ME default applies)</em> toggle and live DCS/validity hints). A transparent file gets one form (SPN fields, PLMN rows with add/remove, PNN items with add/remove); a record file gets one form per record with a selection checkbox that is auto-checked when a field changes, so only the edited records are written. <strong>Save</strong> validates every selected record before writing anything; files without an encoder keep the button disabled with a tooltip. Extension pointers (EF.EXT1–EF.EXT5) and the BDN comparison-method byte are preserved, and editing a number that continues in an extension file is refused.</li>
<li><strong>Probe all files</strong> — walks the whole tree (including custom files) and marks every entry present (normal) or absent (red ✗, no expand arrow); empty-but-present DFs show <code class="font-mono text-sm">(empty)</code>. Shows progress <em>N / total</em>, can be stopped, and finishes with a present/absent summary. Files are only verified when expanded or probed — browsing stays lazy.</li>
<li><strong>ADM</strong> — files that need the administrator PIN fail with <code class="font-mono text-sm">6982</code>/<code class="font-mono text-sm">9804</code>; if the matching card preset (same ICCID) carries an ADM key, a <strong>Verify ADM</strong> button appears next to the error and the header badge (<code class="font-mono text-sm">ADM ✓/✗ ⚿</code>) becomes clickable. Every wrong key consumes an attempt: the remaining attempts are shown and a retry asks for confirmation. A blocked ADM cannot be recovered here — it needs the card&rsquo;s unblock key.</li>
</ul>
+2049 -73
View File
File diff suppressed because one or more lines are too long
+25 -1
View File
@@ -4,4 +4,28 @@
.dark .text-heading {
color: #f1f5f9;
}
}
/* RAM batch: the "insert the next card" cue (v3.36.0).
The card slides into the reader slot and back; the slot glows while the
run waits. The static transform is the reduced-motion fallback: the card
rests above the slot. */
.ram-insert-card {
/* the resting position is the card just above the slot (0); the insert
slide takes it 20px down, behind the reader front */
animation: ram-insert 2.8s ease-in-out infinite;
}
@keyframes ram-insert {
0%, 12% { transform: translateY(0); }
45%, 55% { transform: translateY(20px); }
88%, 100% { transform: translateY(0); }
}
.ram-insert-glow {
animation: ram-insert-glow 1.4s ease-in-out infinite;
}
@keyframes ram-insert-glow {
0%, 100% { opacity: 0.35; }
50% { opacity: 0.95; }
}
@media (prefers-reduced-motion: reduce) {
.ram-insert-card, .ram-insert-glow { animation: none; }
}
+97 -10
View File
@@ -616,6 +616,10 @@ video {
pointer-events: none;
}
.\!visible {
visibility: visible !important;
}
.visible {
visibility: visible;
}
@@ -684,6 +688,11 @@ video {
grid-column: span 4 / span 4;
}
.mx-2 {
margin-left: 0.5rem;
margin-right: 0.5rem;
}
.mx-3 {
margin-left: 0.75rem;
margin-right: 0.75rem;
@@ -807,6 +816,10 @@ video {
display: flex;
}
.inline-flex {
display: inline-flex;
}
.table {
display: table;
}
@@ -843,6 +856,10 @@ video {
max-height: 8rem;
}
.max-h-48 {
max-height: 12rem;
}
.max-h-56 {
max-height: 14rem;
}
@@ -935,6 +952,10 @@ video {
width: 10rem;
}
.w-44 {
width: 11rem;
}
.w-48 {
width: 12rem;
}
@@ -980,6 +1001,10 @@ video {
min-width: 0px;
}
.min-w-24 {
min-width: 6rem;
}
.min-w-\[12rem\] {
min-width: 12rem;
}
@@ -1395,6 +1420,11 @@ video {
background-color: rgb(219 234 254 / var(--tw-bg-opacity, 1));
}
.bg-blue-50 {
--tw-bg-opacity: 1;
background-color: rgb(239 246 255 / var(--tw-bg-opacity, 1));
}
.bg-blue-50\/50 {
background-color: rgb(239 246 255 / 0.5);
}
@@ -1796,6 +1826,11 @@ video {
color: rgb(31 41 55 / var(--tw-text-opacity, 1));
}
.text-gray-900 {
--tw-text-opacity: 1;
color: rgb(17 24 39 / var(--tw-text-opacity, 1));
}
.text-green-600 {
--tw-text-opacity: 1;
color: rgb(22 163 74 / var(--tw-text-opacity, 1));
@@ -1893,10 +1928,61 @@ video {
filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow);
}
.transition {
transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 150ms;
}
.dark .text-heading {
color: #f1f5f9;
}
/* RAM batch: the "insert the next card" cue (v3.36.0).
The card slides into the reader slot and back; the slot glows while the
run waits. The static transform is the reduced-motion fallback: the card
rests above the slot. */
.ram-insert-card {
/* the resting position is the card just above the slot (0); the insert
slide takes it 20px down, behind the reader front */
animation: ram-insert 2.8s ease-in-out infinite;
}
@keyframes ram-insert {
0%, 12% {
transform: translateY(0);
}
45%, 55% {
transform: translateY(20px);
}
88%, 100% {
transform: translateY(0);
}
}
.ram-insert-glow {
animation: ram-insert-glow 1.4s ease-in-out infinite;
}
@keyframes ram-insert-glow {
0%, 100% {
opacity: 0.35;
}
50% {
opacity: 0.95;
}
}
@media (prefers-reduced-motion: reduce) {
.ram-insert-card, .ram-insert-glow {
animation: none;
}
}
.hover\:bg-amber-100:hover {
--tw-bg-opacity: 1;
background-color: rgb(254 243 199 / var(--tw-bg-opacity, 1));
@@ -1956,11 +2042,6 @@ video {
background-color: rgb(55 65 81 / var(--tw-bg-opacity, 1));
}
.hover\:bg-red-100:hover {
--tw-bg-opacity: 1;
background-color: rgb(254 226 226 / var(--tw-bg-opacity, 1));
}
.hover\:bg-red-200:hover {
--tw-bg-opacity: 1;
background-color: rgb(254 202 202 / var(--tw-bg-opacity, 1));
@@ -2093,6 +2174,10 @@ video {
background-color: rgb(30 58 138 / 0.1);
}
.dark\:bg-blue-900\/20:is(.dark *) {
background-color: rgb(30 58 138 / 0.2);
}
.dark\:bg-blue-900\/40:is(.dark *) {
background-color: rgb(30 58 138 / 0.4);
}
@@ -2258,10 +2343,6 @@ video {
background-color: rgb(6 78 59 / 0.4);
}
.dark\:hover\:bg-red-900\/40:hover:is(.dark *) {
background-color: rgb(127 29 29 / 0.4);
}
.dark\:hover\:bg-red-900\/50:hover:is(.dark *) {
background-color: rgb(127 29 29 / 0.5);
}
@@ -2289,6 +2370,11 @@ video {
color: rgb(209 213 219 / var(--tw-text-opacity, 1));
}
.dark\:hover\:text-slate-200:hover:is(.dark *) {
--tw-text-opacity: 1;
color: rgb(226 232 240 / var(--tw-text-opacity, 1));
}
.dark\:hover\:text-slate-300:hover:is(.dark *) {
--tw-text-opacity: 1;
color: rgb(203 213 225 / var(--tw-text-opacity, 1));
@@ -2328,7 +2414,8 @@ video {
.lg\:flex-row {
flex-direction: row;
}
}/* ===== Theme contrast adjustments ===== */
}
/* ===== Theme contrast adjustments ===== */
/* Light theme: darken softer (muted) text one step for higher contrast */
.text-gray-400 {
+1 -1
View File
@@ -1,4 +1,4 @@
const CACHE = 'simple-v460';
const CACHE = 'simple-v473';
const URLS = [
'index.html',
'help.html',
+3 -2
View File
@@ -57,9 +57,10 @@ test('pysimAdmSecuritySw flags only the access-condition SWs', () => {
});
test('file-manager security failures route through the ADM hint', () => {
// no raw SW error rendering is left (all three sites use the hint helper)
// no raw SW error rendering is left (every read/write site, the raw and
// the decoded edit paths, uses the hint helper)
assert.ok(!/statusEl\.textContent = 'SW: ' \+ \(data\.sw/.test(html), 'raw SW error rendering is gone');
assert.strictEqual((html.match(/pysimFsShowError\(statusEl, data\.sw/g) || []).length, 3);
assert.strictEqual((html.match(/pysimFsShowError\(statusEl, data\.sw/g) || []).length, 6);
assert.ok(html.includes("btn.textContent = t('Verify ADM')"));
assert.ok(html.includes("el.setAttribute('onclick', 'pysimVerifyAdm()')"));
});
+98 -8
View File
@@ -24,10 +24,10 @@ function extractFunc(src, name) {
const FNS = [
'efBytes', 'efHex', 'efSwapNibbles', 'efAllFf', 'efRstripFf', 'efTlv', 'efUcs2Be',
'efGsm7Octets', 'efAnnexA', 'efBcdAddress', 'efBcdAddressLen', 'efPlmn', 'efPlmnAct', 'efPnnText',
'efDecIccid', 'efDecImsi', 'efDecLi', 'efDecServiceTable', 'efDecPlmnList', 'efDecFplmn', 'efDecPlmnWact',
'efDecIccid', 'efDecImsi', 'efDecLi', 'efDecServiceTable', 'efDecSst', 'efDecPlmnList', 'efDecFplmn', 'efDecPlmnWact',
'efDecSpn', 'efDecLoci', 'efDecPsLoci', 'efDecEpsLoci', 'efDecEpsNsc', 'efDecKc', 'efDecAcc',
'efDecPhase', 'efDecCbmi', 'efDecCbmir', 'efDecEcc', 'efDecOpl', 'efDecAd', 'efDecAcl',
'efDecSmsp', 'efDecSpdi', 'efDecNai', 'efDecDir', 'efDecArr', 'efDecPnn', 'efDecAdn',
'efDecPhase', 'efDecCbmi', 'efDecCbmir', 'efDecHpplmn', 'efDecEcc', 'efDecOpl', 'efDecAd', 'efDecAcl',
'efSmspDaNumber', 'efDecSmsp', 'efDecSpdi', 'efDecNai', 'efDecDir', 'efDecArr', 'efDecPnn', 'efDecAdn',
'pinKeyRefName', 'efArrAmLabel', 'efArrSc', 'efArrAmDo',
'efDecExt1', 'efDecSms', 'efDecSume', 'efPbrHex', 'efDecPbr', 'efFindDecoder', 'efFidFromPath', 'efDecodeBytes',
'efDecodeFile', 'efDecodeHex', 'efIsRawOnly', 'efFieldLabel', 'efPrimitive', 'efFlatten',
@@ -42,7 +42,7 @@ for (const f of FNS) code += extractFunc(html, f) + '\n';
for (const c of ['GSM7_ALPHABET', 'GSM7_EXT_MAP', 'FCP_ARR_AM_BITS']) {
code += html.match(new RegExp('const ' + c + ' = [\\s\\S]*?\\n\\];'))[0].replace('const ', 'var ') + '\n';
}
for (const c of ['EF_UST_SERVICES', 'EF_SST_SERVICES', 'EF_IST_SERVICES', 'EF_PHASE_NAMES', 'EF_PBR_KINDS']) {
for (const c of ['EF_UST_SERVICES', 'EF_SST_SERVICES', 'EF_IST_SERVICES', 'EF_EST_SERVICES', 'EF_PHASE_NAMES', 'EF_PBR_KINDS']) {
code += html.match(new RegExp('const ' + c + ' = \\{.*\\};'))[0].replace('const ', 'var ') + '\n';
}
code += html.match(/const EF_FIELD_LABELS = \{[\s\S]*?\n\};/)[0].replace('const ', 'var ') + '\n';
@@ -103,7 +103,9 @@ test('EF decoders match the pinned spec / pySim test vectors', () => {
assert.deepStrictEqual(efDecLi(efBytes('FFFF')), { languages: [] });
assert.deepStrictEqual(efDecSpn(efBytes('0147534d2d52204348ffffffffffffffff')), { show_in_hplmn: true, hide_in_oplmn: false, name: 'GSM-R CH' });
assert.deepStrictEqual(efDecAcc(efBytes('0000')), { access_classes: [] });
assert.deepStrictEqual(efDecAcc(efBytes('8000')), { access_classes: [0] });
assert.deepStrictEqual(efDecAcc(efBytes('8000')), { access_classes: [15] });
assert.deepStrictEqual(efDecAcc(efBytes('0001')), { access_classes: [0] });
assert.deepStrictEqual(efDecAcc(efBytes('8001')), { access_classes: [15, 0] });
assert.deepStrictEqual(efDecEcc(efBytes('19f101')), { number: '911', esc: '0x01' });
assert.deepStrictEqual(efDecKc(efBytes('837d783609a3858f05')), { kc: '837D783609A3858F', ksi: 0 });
assert.deepStrictEqual(efDecPlmnList(efBytes('22F860')), { plmns: [{ mcc: '228', mnc: '06' }] });
@@ -115,7 +117,7 @@ test('EF decoders match the pinned spec / pySim test vectors', () => {
assert.strictEqual(efFindDecoder('EF.FPLMN', null).fn, efDecFplmn);
assert.deepStrictEqual(efDecCbmi(efBytes('0010FFFF0020')), { message_ids: [16, 32] });
assert.deepStrictEqual(efDecCbmir(efBytes('0000FFFEFFFF')), { ranges: [['0000', 'FFFE']] });
assert.deepStrictEqual(efDecPhase(efBytes('03')), { phase: 'phase 2 and higher' });
assert.deepStrictEqual(efDecPhase(efBytes('03')), { phase: 'phase 2 and PROFILE DOWNLOAD required' });
assert.deepStrictEqual(efDecNai(efBytes('8021696d732e6d6e633030302e6d63633733382e336770706e6574776f726b2e6f7267')),
{ text: 'ims.mnc000.mcc738.3gppnetwork.org' });
assert.deepStrictEqual(efDecImsi(efBytes('FFFF')), { empty: true });
@@ -276,6 +278,9 @@ test('EF record decoders: SMS, OPL, DIR, ARR, IMPI/SMSP', () => {
assert.strictEqual(smsp.sca, '+4915790109999');
assert.strictEqual(smsp.tp_pid, '0x00');
assert.strictEqual(smsp.tp_dcs, '0x00');
assert.strictEqual(smsp.tp_vp, '0xA9');
assert.strictEqual(smsp.indicators, '0xE1');
assert.strictEqual(smsp.tp_da, undefined);
// EF.SMS status byte + skip SMSC + TPDU
const sms = efDecSms(efBytes('030891945197109099f90000'));
assert.strictEqual(sms.direction, 'MT');
@@ -283,6 +288,31 @@ test('EF record decoders: SMS, OPL, DIR, ARR, IMPI/SMSP', () => {
assert.strictEqual(sms.tpdu, '0000');
});
test('EF.SMSP decodes the indicator bits (TS 31.102 4.2.27)', () => {
// live card record: DA absent (0x01), SCA present, PID present, DCS absent
// (0x08), VP present; bits 6..8 reserved = 1 -> 0xE9
const live = efDecSmsp(efBytes('ffffffffffffffffffffffffffe9ffffffffffffffffffffffff07919733824009f0ffffffff0000ff'));
assert.deepStrictEqual(live, {
indicators: '0xE9',
sca: '+79332804900',
tp_pid: '0x00',
tp_vp: '0xFF',
});
assert.strictEqual(live.tp_da, undefined);
assert.strictEqual(live.tp_dcs, undefined); // DCS absent, its 00 slot kept raw for the editor
});
test('EF.SMSP reads the TP-DA length as a digit count (00 padding is not ABS)', () => {
// alpha FF x13 | indicator FE (DA present, rest absent) |
// DA 04 81 21 43 00 00 FF... ("1234" padded with 00, not FF)
const hex = 'FF'.repeat(13) + 'FE' + '048121430000FFFFFFFFFFFF'
+ 'FF'.repeat(12) + 'FF' + 'FF' + 'FF';
assert.deepStrictEqual(efDecSmsp(efBytes(hex)), {
indicators: '0xFE',
tp_da: '1234',
});
});
test('service tables: UST service bits and IST fallback for 6F07', () => {
const ust = efDecServiceTable(efBytes('00000010'), EF_UST_SERVICES);
assert.deepStrictEqual(ust.services, [{ n: 29, name: 'Data download via SMS-CB' }]);
@@ -290,6 +320,29 @@ test('service tables: UST service bits and IST fallback for 6F07', () => {
assert.deepStrictEqual(ist.services, [{ n: 1, name: 'P-CSCF address' }]);
});
test('EF.SST is the SIM 2-bit service table (allocated/activated)', () => {
// TS 51.011 10.3.7: service 2 ADN -> byte 0, pair start bit 2 (0x04
// allocated, 0x08 activated)
assert.deepStrictEqual(efDecSst(efBytes('0C')), { services: [
{ n: 2, name: 'Abbreviated Dialling Numbers (ADN)', allocated: true, activated: true },
] });
assert.deepStrictEqual(efDecSst(efBytes('04')), { services: [
{ n: 2, name: 'Abbreviated Dialling Numbers (ADN)', allocated: true, activated: false },
] });
assert.deepStrictEqual(efDecSst(efBytes('08')), { services: [
{ n: 2, name: 'Abbreviated Dialling Numbers (ADN)', allocated: false, activated: true },
] });
// byte 1 pair 3 (bits 6/7) = service 4 SMS; byte 0 pair 1 = service 2
assert.deepStrictEqual(efDecSst(efBytes('C0')), { services: [
{ n: 4, name: 'Short Message Storage (SMS)', allocated: true, activated: true },
] });
assert.deepStrictEqual(efDecSst(efBytes('00')), { services: [] });
assert.deepStrictEqual(efFindDecoder('EF.SST', null).fn(efBytes('0C')).services[0].activated, true);
// the read-only view shows both bits
assert.ok(efFlatten(efDecSst(efBytes('0C'))).some(r =>
r[0] === 'Service 2' && r[1].includes('allocated') && r[1].includes('activated')));
});
test('decoder registry resolves by name first, then FID; unknown returns null', () => {
assert.strictEqual(efFindDecoder('EF.IMSI', null).fid, '6f07');
assert.strictEqual(efFindDecoder(null, '2fe2').name, 'EF.ICCID');
@@ -325,7 +378,7 @@ test('efFlatten renders fields, services and PLMNs for display', () => {
[['Service 5', 'Call Control by USIM']]);
assert.deepStrictEqual(efFlatten({ plmns: [{ mcc: '262', mnc: '01', access_tech: 'GSM' }] }),
[['PLMN 1', '262-01 (GSM)']]);
assert.deepStrictEqual(efFlatten({ show_in_hplmn: true }), [['Show in HPLMN', 'yes']]);
assert.deepStrictEqual(efFlatten({ show_in_hplmn: true }), [['Display the network name in HPLMN / an SPDI PLMN', 'yes']]);
assert.deepStrictEqual(efFlatten({ title: 'Menu!', icon: '0123' }), [['Title', 'Menu!'], ['Icon', '0123']]);
});
@@ -376,7 +429,10 @@ test('file manager decoded view renders fields, not JSON', () => {
const out = pysimFsDecodedHtml({ name: 'EF.IMSI', fid: null }, { success: true, data: '082982608200002080' });
assert.match(out, /IMSI/);
assert.match(out, /228062800000208/);
assert.match(out, /pySim JSON \(server\)/);
// The view is the client-side decoder's table only (v3.32.7): the
// server-side pySim JSON disclosure is gone - it was an unrelated second
// read of the card, not an input to this decode.
assert.doesNotMatch(out, /pySim JSON/);
assert.doesNotMatch(out, /\{"imsi"/);
// the label column is content-sized, not a fixed 10rem block
assert.match(out, /grid-template-columns:max-content 1fr/);
@@ -387,6 +443,15 @@ test('file manager decoded view renders fields, not JSON', () => {
assert.match(raw, /0011/);
});
test('the file manager never requests the server-side decode', () => {
// The decoded table is client-side (the raw read's bytes); the pySim JSON
// lookup - a second /api/read with mode 'decoded' - is gone (v3.32.7).
assert.ok(!html.includes("mode = 'decoded'"));
assert.ok(!html.includes('pysimFsLoadPySimJson'));
assert.ok(!html.includes('pysim-fs-pysim-json'));
assert.ok(!html.includes('pySim JSON (server)'));
});
test('efRenderFieldsHtml sizes the label column to its content', () => {
const out = efRenderFieldsHtml([['ICCID', '8988'], ['Very long label', 'x']]);
assert.match(out, /style="display:grid;grid-template-columns:max-content 1fr/);
@@ -426,3 +491,28 @@ test('profilerRenderReport shows raw and decoded comparisons for content mismatc
assert.match(same, /Decoded values match/);
assert.doesNotMatch(same, /Decoded comparison/);
});
test('EF.BDN uses the 15-byte ADN layout (TS 31.102 4.2.44)', () => {
// alpha 'TEST' | len 06 | TON/NPI 91 | BCD 21 43 65 87 09 + FF padding |
// CCP FF | EXT4 FF | EF.CMI comparison-method pointer 03
const hex = '54455354' + '06' + '91' + '2143658709' + 'FF'.repeat(7) + '03';
const bdn = efBytes(hex);
const want = { name: 'TEST', number: '+1234567890', cmi: 3 };
assert.strictEqual(bdn.length, 19);
assert.deepStrictEqual(efDecAdn(bdn, 15), want);
assert.deepStrictEqual(efFindDecoder('EF.BDN').fn(bdn), want);
// the 14-byte default and the EF.ADN registry entry are unchanged
const adn = efBytes('42204841203120536963FFFFFFFFFFFF06810628560810FFFFFFFFFFFFFF');
const adnWant = { name: 'B HA 1 Sic', number: '6082658001' };
assert.deepStrictEqual(efDecAdn(adn), adnWant);
assert.deepStrictEqual(efFindDecoder('EF.ADN').fn(adn), adnWant);
assert.notDeepStrictEqual(efDecAdn(adn, 15), adnWant);
});
test('the BDN extension and CMI fields carry readable labels', () => {
assert.deepStrictEqual(efFlatten({ cmi: 3 }), [['Comparison method pointer', '3']]);
assert.deepStrictEqual(efFlatten({ ext1: 2 }), [['Extension record', '2']]);
const bdn = efDecAdn(efBytes('54455354' + '06' + '91' + '2143658709' +
'FF'.repeat(7) + '03'), 15);
assert.ok(efFlatten(bdn).some(r => r[0] === 'Comparison method pointer' && r[1] === '3'));
});
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+12
View File
@@ -24,6 +24,8 @@ function extractFunc(src, name) {
let code = 'var pysimFsDecodedMode = false;\n'
+ 'var pysimFsEditMode = false;\n'
+ 'var pysimFsEditData = null;\n'
+ 'var pysimFsDecEditMode = false;\n'
+ 'var pysimFsDecModel = null;\n'
+ 'var pysimFsSelected = null;\n'
+ 'var pysimFsTreeRoot = null;\n';
for (const fn of ['pysimFsSetMode', 'pysimFsPillsEnabled', 'pysimFsEdit',
@@ -115,12 +117,19 @@ test('Read raw / Read decoded pills switch the view and read the file', () => {
assert.strictEqual(h.reads(), 0, 'skipRead updates the pills only');
assert.ok(h.pill('raw').classList.contains('bg-blue-600'));
assert.ok(!h.pill('dec').classList.contains('bg-blue-600'));
// an enabled inactive pill is outlined, not grey: the grey fill reads as
// "disabled" although the pill is clickable (reported live)
assert.ok(h.pill('dec').classList.contains('bg-white'));
assert.ok(h.pill('dec').classList.contains('text-blue-600'));
assert.ok(!h.pill('dec').classList.contains('bg-gray-200'));
pysimFsSetMode('dec');
assert.strictEqual(pysimFsDecodedMode, true);
assert.strictEqual(h.reads(), 1, 'a pill click reads the selected file');
assert.ok(h.pill('dec').classList.contains('bg-blue-600'));
assert.ok(!h.pill('raw').classList.contains('bg-blue-600'));
assert.ok(h.pill('raw').classList.contains('bg-white'));
assert.ok(!h.pill('raw').classList.contains('bg-gray-200'));
});
test('pill clicks are ignored while editing', () => {
@@ -219,4 +228,7 @@ test('the file manager row exposes the read pills and Edit raw', () => {
assert.ok(html.includes('id="pysim-fs-read-raw-btn"') && html.includes('data-needs="card"'));
const dec = /<button[^>]*id="pysim-fs-read-dec-btn"[^>]*>/.exec(html);
assert.ok(dec && dec[0].includes('data-needs-decoder'), 'the decoded pill is decoder-gated');
// the enabled-inactive look is an outline; the grey fill is for disabled
assert.ok(dec[0].includes('bg-white') && dec[0].includes('text-blue-600'));
assert.ok(!dec[0].includes('bg-gray-200'));
});
+53
View File
@@ -553,6 +553,59 @@ test('ramOpChanged clears the executed status only on a real op change', () => {
assert.ok(!els['ram-install-params'].classList.contains('hidden'));
});
test('the RAM preset banner is content-driven (no empty amber flash)', () => {
// A view / op switch used to un-hide #ram-preset-warning by the op kind
// (v3.28.0): with no warning text the empty bordered box stayed visible
// until the next status poll rendered it away - the amber block flashing
// on every SCP80/RAM view change. Its visibility belongs to the renderer.
const els = fakeRamDocument(['ram-op', 'ram-card-sel', 'ram-preset-warning',
'ram-install-params', 'ram-cap-rows', 'ram-compat-rows', 'ram-app-rows',
'ram-delete-rows', 'ram-del-package-row', 'ram-preset-cell', 'ram-exec-btn',
'ram-batch-panel', 'ram-result', 'ram-explorer', 'ram-steps', 'ram-progress',
'sp-preset-warning', 'tar-preset-warning']);
els['ram-preset-warning'].classList.add('hidden');
const prev = {
cards: globalThis.cards,
t: globalThis.t,
iccid: globalThis._pysimCardIccid,
batchFetch: globalThis.ramBatchFetch,
};
globalThis.cards = [];
globalThis._pysimCardIccid = null; // no card session: no warning text
globalThis.t = s => s;
globalThis.ramBatchFetch = () => {};
// The real renderer, defined where the extracted ramOpChanged finds it
// (a plain function body sees only the globals).
const src = ['cardsNormIccid', 'spPresetWarning', 'ramIsBatchOp', 'ramPresetIdx',
'spPresetIdx', 'tarPresetIdx', 'spPresetWarningRender']
.map(f => extractFunc(html, f)).join('\n');
const real = new Function(src + '\nreturn spPresetWarningRender;')();
let calls = 0;
globalThis.spPresetWarningRender = () => { calls++; return real(); };
try {
_ramOpLast = 'explore';
els['ram-op'].value = 'explore';
ramOpChanged();
assert.strictEqual(calls, 1, 'the content-driven renderer must own the banner');
assert.ok(els['ram-preset-warning'].classList.contains('hidden'),
'an empty banner must not be un-hidden by the op dispatch');
// a batch op keeps it hidden as well (the preset resolves per card)
els['ram-op'].value = 'batch';
ramOpChanged();
assert.ok(els['ram-preset-warning'].classList.contains('hidden'));
// the visibility is never toggled here by the op kind again
assert.ok(!extractFunc(html, 'ramOpChanged').includes("'ram-preset-warning'"));
} finally {
globalThis.spPresetWarningRender = () => {};
if (prev.cards === undefined) delete globalThis.cards; else globalThis.cards = prev.cards;
if (prev.t === undefined) delete globalThis.t; else globalThis.t = prev.t;
if (prev.iccid === undefined) delete globalThis._pysimCardIccid;
else globalThis._pysimCardIccid = prev.iccid;
if (prev.batchFetch === undefined) delete globalThis.ramBatchFetch;
else globalThis.ramBatchFetch = prev.batchFetch;
}
});
test('ramRender keeps the selected card preset across rebuilds', () => {
const els = fakeRamDocument(['ram-card-sel', 'ram-op', 'ram-install-params', 'ram-result', 'ram-explorer', 'ram-steps', 'ram-progress']);
globalThis.cards = [{ name: 'A' }, { name: 'B' }, { name: 'C' }];
+126 -4
View File
@@ -28,7 +28,10 @@ for (const fn of ['ramIsBatchOp', 'ramBatchPanelVisible', 'ramBatchMergedState',
'ramBatchStepsText',
'ramBatchAttemptsHtml', 'ramBatchRender', 'ramBatchApplyStatus', 'ramBatchFetch',
'ramBatchStart', 'ramBatchStop', 'ramBatchClear', 'ramOpProgressText',
'ramOpModalClose', 'esc']) {
'ramOpModalClose', 'esc',
'ramBatchSoundEnabled', 'ramBatchUpdateSoundButton', 'ramBatchToggleSound',
'ramBatchAudioUnlock', 'ramBatchChime', 'ramBatchTitleHint',
'ramBatchWaitingCue']) {
code += extractFunc(html, fn, ASYNC_FNS.indexOf(fn) >= 0) + '\n';
}
@@ -39,7 +42,9 @@ const els = {};
function setEl(id, value) {
els[id] = {
id: id, value: value === undefined ? '' : value, textContent: '', innerHTML: '',
disabled: false, style: {},
disabled: false, style: {}, title: '', attrs: {},
setAttribute(k, v) { els[id].attrs[k] = String(v); },
getAttribute(k) { return els[id].attrs[k]; },
classes: {},
classList: {
add(c) { els[id].classes[c] = true; },
@@ -64,7 +69,8 @@ function clearEls() {
function setModalEls() {
for (const id of ['ram-op-modal', 'ram-op-title', 'ram-op-status', 'ram-op-bar',
'ram-op-steps', 'ram-op-spin', 'ram-op-warn', 'ram-op-close',
'ram-op-batch-info', 'ram-op-batch-stop', 'ram-op-batch-clear']) setEl(id, '');
'ram-op-batch-info', 'ram-op-batch-stop', 'ram-op-batch-clear',
'ram-op-insert', 'ram-op-sound']) setEl(id, '');
}
function setPanelEls() {
for (const id of ['ram-op', 'ram-batch-panel', 'ram-batch-start', 'ram-batch-stop',
@@ -75,7 +81,17 @@ function setPanelEls() {
const app = new Function('els', `
var sandbox = { fetched: [], replies: {}, fetchError: null, alerts: [], visibility: true };
var document = { getElementById: id => els[id] || null };
var document = {
getElementById: id => els[id] || null,
hidden: false, title: 'SIMple',
addEventListener: () => {},
};
var _ls = {};
var localStorage = {
getItem: k => (k in _ls ? _ls[k] : null),
setItem: (k, v) => { _ls[k] = String(v); },
removeItem: k => { delete _ls[k]; },
};
var t = k => k;
var isViewVisible = () => sandbox.visibility;
var alert = m => sandbox.alerts.push(String(m));
@@ -109,6 +125,9 @@ const app = new Function('els', `
var _ramBatchFetchAt = 0;
var _ramBatchSummary = null;
var _ramBatchProgress = null;
var _ramBatchWaiting = false;
var _ramBatchAudio = null;
var _ramBatchTitleBase = '';
var RAM_BATCH_FETCH_STALE_MS = ${RAM_BATCH_FETCH_STALE_MS};
${code}
sandbox.getLast = () => _ramBatchLast;
@@ -134,6 +153,16 @@ const app = new Function('els', `
sandbox.ramBatchStart = ramBatchStart;
sandbox.ramBatchStop = ramBatchStop;
sandbox.ramBatchClear = ramBatchClear;
sandbox.ramBatchWaitingCue = ramBatchWaitingCue;
sandbox.ramBatchTitleHint = ramBatchTitleHint;
sandbox.ramBatchSoundEnabled = ramBatchSoundEnabled;
sandbox.ramBatchUpdateSoundButton = ramBatchUpdateSoundButton;
sandbox.ramBatchToggleSound = ramBatchToggleSound;
sandbox.setHidden = v => { document.hidden = !!v; };
sandbox.getTitle = () => document.title;
sandbox.resetWaiting = () => { _ramBatchWaiting = false; };
sandbox.storage = _ls;
sandbox.resetStorage = () => { for (const k of Object.keys(_ls)) delete _ls[k]; };
return sandbox;
`)(els);
@@ -639,3 +668,96 @@ test('the stop and clear calls hit their endpoints', async () => {
await new Promise(r => setImmediate(r));
assert.deepStrictEqual(app.fetched, ['/api/ram-batch/stop', '/api/ram-batch/clear', '/api/ram-batch/status']);
});
test('the waiting cue fires once per card and drives the insert animation (v3.36.0)', () => {
clearEls();
setPanelEls();
setModalEls();
app.resetWaiting();
assert.strictEqual(app.ramBatchWaitingCue(true), true, 'entering waiting cues');
assert.strictEqual(app.ramBatchWaitingCue(true), false, 'still waiting: no repeat');
assert.strictEqual(app.ramBatchWaitingCue(false), false, 'installing resets');
assert.strictEqual(app.ramBatchWaitingCue(true), true, 'the next card cues again');
assert.ok(els['ram-op-insert'].classes['flex'] === true);
assert.ok(!els['ram-op-insert'].classes['hidden']);
app.ramBatchWaitingCue(false);
assert.ok(els['ram-op-insert'].classes['hidden']);
// the render drives it from the phase
app.setLast({ active: true, phase: 'waiting_card', attempts: [], counts: {}, attempts_total: 0, notice: '' });
app.ramBatchRender();
assert.ok(!els['ram-op-insert'].classes['hidden']);
app.setLast({ active: true, phase: 'installing', current: { iccid: '8970119000004600098' },
attempts: [], counts: {}, attempts_total: 1 });
app.ramBatchRender();
assert.ok(els['ram-op-insert'].classes['hidden']);
});
test('the sound toggle persists and updates the button (v3.36.0)', () => {
clearEls();
setModalEls();
app.resetStorage();
assert.strictEqual(app.ramBatchSoundEnabled(), true, 'sound is on by default');
app.ramBatchUpdateSoundButton();
assert.strictEqual(els['ram-op-sound'].textContent, '\u{1F50A}');
assert.strictEqual(els['ram-op-sound'].attrs['aria-pressed'], 'true');
app.ramBatchToggleSound();
assert.strictEqual(app.ramBatchSoundEnabled(), false);
assert.strictEqual(app.storage['simple_ram_batch_sound'], '0');
assert.strictEqual(els['ram-op-sound'].textContent, '\u{1F507}');
assert.strictEqual(els['ram-op-sound'].attrs['aria-pressed'], 'false');
app.ramBatchToggleSound();
assert.strictEqual(app.ramBatchSoundEnabled(), true);
});
test('the tab title hints while the run waits and the tab is hidden (v3.36.0)', () => {
app.setHidden(false);
app.ramBatchTitleHint(true);
assert.strictEqual(app.getTitle(), 'SIMple');
app.setHidden(true);
app.ramBatchTitleHint(true);
assert.strictEqual(app.getTitle(), 'Insert next card \u2014 SIMple');
app.setHidden(false);
app.ramBatchTitleHint(true);
assert.strictEqual(app.getTitle(), 'SIMple');
});
test('the waiting cue is wired into the poll render, the Start gesture and the CSS (v3.36.0)', () => {
const render = extractFunc(html, 'ramBatchRender');
assert.match(render, /const waiting = active && phase !== 'installing' && phase !== 'stopping';/);
assert.match(render, /if \(ramBatchWaitingCue\(waiting\)\) ramBatchChime\(\);/);
assert.match(extractFunc(html, 'ramBatchStart'), /ramBatchAudioUnlock\(\)/);
assert.match(html, /id="ram-op-insert"/);
assert.match(html, /id="ram-op-sound"/);
const css = fs.readFileSync(path.join(__dirname, '..', 'style.css'), 'utf8');
assert.match(css, /\.ram-insert-card/);
assert.match(css, /@keyframes ram-insert/);
assert.match(css, /\.ram-insert-glow/);
assert.match(css, /prefers-reduced-motion: reduce/);
});
test('an already-absent delete is reported as such, not as a deletion (v3.36.0)', () => {
clearEls();
setPanelEls();
setModalEls();
app.resetBatchModal();
app.setLast({ active: false, phase: 'finished', kind: 'delete',
attempts: [{
iccid: '8970119000000115026', preset: 'Alfa', result: 'already_absent',
steps: [{ name: 'DELETE (application)', sw: '6A88', ok: true, not_found: true }],
steps_total: 1, duration_ms: 1200,
}],
counts: { installed: 2, already_absent: 1, failed: 0, skipped: 0 },
attempts_total: 3, notice: 'Card 8970119000000115026 was already absent.' });
app.ramBatchRender();
// the counts separate a real deletion from a no-op
assert.match(els['ram-batch-counts'].textContent, /Deleted: 2/);
assert.match(els['ram-batch-counts'].textContent, /Already absent: 1/);
// the attempt's verdict says it - not a plain OK - and keeps the step line
assert.match(els['ram-batch-attempts'].innerHTML, /Already absent/);
assert.match(els['ram-batch-attempts'].innerHTML, /6A88 \(not found\)/);
// the modal carries the same counts
app.ramBatchModalOpen();
assert.match(els['ram-op-batch-info'].textContent, /Deleted: 2/);
assert.match(els['ram-op-batch-info'].textContent, /Already absent: 1/);
});
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "pysim-simple-server"
version = "3.32.2"
version = "3.36.0"
description = "HTTP REST server wrapping pysim for the SIMple PWA"
license = { text = "GPL-2.0-or-later" }
requires-python = ">=3.9"
+21 -3
View File
@@ -18,7 +18,7 @@ from . import presets
from . import test_scripts
from . import test_suites
from . import testscript
from .server import PysimHandler, StderrApduTracer, _LoggingApduTracer, VERSION, _send_terminal_profile, _DefaultProactiveHandler, _handle_proactive_chain, _send_status, _init_proactive_session, _timing_on, _tlog, _set_menu_timeout, start_card_monitor, set_auto_equip, _read_iccid, _read_arr, _read_pbr, _netstate_read, _netstate_install, _LineFilter, _test_command_type, migrate_scripts_to_suites
from .server import PysimHandler, StderrApduTracer, _LoggingApduTracer, VERSION, _send_terminal_profile, _DefaultProactiveHandler, _handle_proactive_chain, _send_status, _init_proactive_session, _timing_on, _tlog, _set_menu_timeout, start_card_monitor, set_auto_equip, _read_iccid, _read_arr, _read_pbr, _netstate_read, _netstate_install, _LineFilter, _test_command_type, migrate_scripts_to_suites, _is_pcsc_error, _clear_app_card_state
_server_start = 0
@@ -295,8 +295,26 @@ def main():
break
_handle_proactive_chain(scc, st_sw)
_tlog('terminal_profile_drain: %.0fms' % ((time.time() - t_phase) * 1000))
except Exception:
traceback.print_exc(file=sys.stderr)
except Exception as e:
if _is_pcsc_error(e):
# The card (or the reader) went away during the startup init -
# a normal state, not a crash. Drop the half-initialized
# session: keeping the card object would leave the server
# reporting a phantom equipped card, and the auto-equip would
# then skip every later insertion (_CARD_CONNECTED is True).
# The presence monitor equips the next card (live 2026-10-21).
sys.stderr.write('INIT: card removed during initialization - server ready; '
'insert a card or press Equip\n')
iccid = None
netstate_files = None
arr_files = None
pbr_files = None
card = None
rs = None
scc = None
_clear_app_card_state(app)
else:
traceback.print_exc(file=sys.stderr)
if app is not None and tracer is not None:
if opts.apdu_trace:
# PysimApp.__init__ routes PySimLogger through app.poutput() (app.stdout)
+65 -6
View File
@@ -37,7 +37,7 @@ from osmocom.tlv import BER_TLV_IE
from osmocom.utils import rpad
VERSION = '3.32.2'
VERSION = '3.36.0'
MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE
@@ -249,6 +249,24 @@ def _strip_ansi(text):
return re.sub(r'\x1b\[[0-9;]*[a-zA-Z]', '', text)
def _cmd_exception(output):
"""The first non-empty line of a cmd2/pySim exception captured from a
command's output, or None.
cmd2 catches a command's exception, prints ``EXCEPTION of type ...`` and
(in debug mode) a traceback instead of re-raising, so the HTTP handlers
that only look for ``got (XXXX)`` reported success while nothing was
written. Both markers are command-level failure evidence.
"""
text = output or ''
if 'EXCEPTION of type' not in text and 'Traceback (most recent call last)' not in text:
return None
for line in text.splitlines():
if line.strip():
return line.strip()
return 'command failed'
def _parse_help_text(text):
result = {'usage': '', 'description': '', 'args': []}
lines = text.split('\n')
@@ -2896,7 +2914,7 @@ def _ram_batch_new_state(plan):
return {'active': True, 'phase': 'waiting_card', 'stop': False,
'started': time.time(), 'stopped': None, 'plan': plan,
'session': None, 'seen': {}, 'attempts': [], 'attempts_total': 0,
'counts': {'installed': 0, 'failed': 0, 'skipped': 0},
'counts': {'installed': 0, 'already_absent': 0, 'failed': 0, 'skipped': 0},
'current': None, 'notice': ''}
@@ -2921,6 +2939,8 @@ def _ram_batch_record(state, rec):
verb = {'delete': 'deleted',
'reinstall-app': 'reinstalled'}.get(kind, 'installed')
state['notice'] = 'Card %s %s.' % (who, verb)
elif result == 'already_absent':
state['notice'] = 'Card %s was already absent.' % who
elif result == 'failed':
step = rec.get('failed_step') or ''
sw = rec.get('por_sw') or rec.get('sw') or ''
@@ -2929,8 +2949,9 @@ def _ram_batch_record(state, rec):
else:
state['notice'] = 'Card %s skipped: %s.' % (who, rec.get('reason') or '')
detail = rec.get('reason') or rec.get('failed_step') or ''
label = {'already_absent': 'already absent'}.get(result, result)
sys.stderr.write('RAM-BATCH: %s%s%s\n' % (
result, (' ' + who) if rec.get('iccid') else '', (' - ' + detail) if detail else ''))
label, (' ' + who) if rec.get('iccid') else '', (' - ' + detail) if detail else ''))
def _ram_batch_failure_fields(result):
@@ -3026,6 +3047,15 @@ def _ram_batch_handle_card(state, server):
'module_aid': result.get('module_aid'),
'nv_delta': result.get('nv_delta')}
rec.update(_ram_batch_steps_fields(result))
# A delete run whose DELETE answered 'not found' is a success (the
# object is gone, the goal is met) but not a deletion: report it as
# "already absent" and count it separately (v3.36.0). Only the delete
# kind: a reinstall's tolerated pre-delete is not the outcome.
if (state.get('plan') or {}).get('kind') == 'delete' and any(
s.get('delete_not_found') for s in (result.get('steps') or [])):
outcome = 'already_absent'
rec['result'] = outcome
rec['not_found'] = True
elif result is not None:
outcome = 'failed'
rec = {'iccid': iccid, 'preset': name, 'result': outcome,
@@ -3160,7 +3190,7 @@ def _ram_batch_status():
if _RAM_BATCH is None:
return {'active': False, 'phase': 'idle', 'attempts': [],
'attempts_total': 0, 'current': None, 'notice': '',
'counts': {'installed': 0, 'failed': 0, 'skipped': 0},
'counts': {'installed': 0, 'already_absent': 0, 'failed': 0, 'skipped': 0},
'started': None, 'stopped': None, 'progress': None,
'kind': None}
return {'active': bool(_RAM_BATCH.get('active')),
@@ -5426,7 +5456,8 @@ def _ensure_transport(server):
try:
old.disconnect()
except Exception as e:
sys.stderr.write('TRANSPORT: releasing the old link failed: %s\n' % e)
sys.stderr.write('TRANSPORT: releasing the old link failed '
'(continuing with a fresh one): %s\n' % e)
try:
sl = factory()
except Exception as e:
@@ -5744,8 +5775,15 @@ def _auto_equip_attempt(server):
lines = captured.splitlines()
start = next(i for i, l in enumerate(lines)
if 'Traceback (most recent call last)' in l)
body = lines[start:start + 10]
tail = [l for l in lines[start:] if l.strip()][-3:]
if tail and tail[-1] not in body:
# The exception itself is the last line of the traceback;
# a rich-box traceback can be longer than the head slice.
body.append('...')
body.extend(tail)
sys.stderr.write('AUTO-EQUIP: init output:\n%s\n'
% '\n'.join(lines[start:start + 12]))
% '\n'.join(body))
# ANY failed attempt leaves the PC/SC card handle suspect: after a
# card swap on Windows the link can stay unable to connect, so
# every retry failed and the watchdog re-armed forever (live
@@ -5819,6 +5857,15 @@ class _CardPresenceObserver(CardObserver):
with _CARD_LOCK:
if _server_ref:
_server_ref.card_present = True
# A freshly inserted card gets a fresh PC/SC link: after a
# swap the old link can stay unable to connect (seen on
# Windows: the first init failed and only the retry's
# rebuild recovered it), and a link created at a cardless
# startup has never connected. Marking it here (under the
# lock) makes the FIRST attempt rebuild - the rebuild
# itself runs in the equip worker, never in this thread.
global _TRANSPORT_STALE
_TRANSPORT_STALE = True
trigger_auto = True
if trigger_auto and _AUTO_EQUIP:
_auto_equip_trigger()
@@ -9826,6 +9873,12 @@ class PysimHandler(BaseHTTPRequestHandler):
sys.stderr = old_stderr
finally:
apdu_times = _end_apdu_time_collection()
exc = _cmd_exception(output)
if exc:
resp = {'success': False, 'error': exc}
self._send_json(resp)
self._log_resp(resp)
return
sw_match = re.search(r'SW:\s*(\w+)', output)
err_match = re.search(r'got (\w+)', output)
if err_match:
@@ -9914,6 +9967,12 @@ class PysimHandler(BaseHTTPRequestHandler):
finally:
app.stdout = old_stdout
sys.stderr = old_stderr
exc = _cmd_exception(output)
if exc:
resp = {'success': False, 'error': exc}
self._send_json(resp)
self._log_resp(resp)
return
sw_match = re.search(r'SW:\s*(\w+)', output)
err_match = re.search(r'got (\w+)', output)
if err_match:
+13
View File
@@ -35,8 +35,11 @@ class TestCardPresenceObserver(unittest.TestCase):
self.trigger_mock = self.trigger.start()
self.saved_auto = S._AUTO_EQUIP
S._AUTO_EQUIP = True
self.saved_stale = S._TRANSPORT_STALE
S._TRANSPORT_STALE = False
def tearDown(self):
S._TRANSPORT_STALE = self.saved_stale
S._AUTO_EQUIP = self.saved_auto
self.trigger.stop()
self.patcher.stop()
@@ -61,6 +64,16 @@ class TestCardPresenceObserver(unittest.TestCase):
self.assertEqual(self.disconnects, [])
self.trigger_mock.assert_called_once()
def test_insertion_marks_the_transport_for_a_rebuild(self):
# The first equip attempt must already use a fresh link: after a swap
# the old one cannot connect on Windows, and a cardless-startup link
# never connected (v3.32.4).
S._TRANSPORT_STALE = False
self.server.card_present = False
self.observer.update(None, ([FakeCard('Test Reader 00 00')], []))
self.assertTrue(S._TRANSPORT_STALE)
self.trigger_mock.assert_called_once()
def test_insertion_does_not_trigger_when_disabled(self):
S._AUTO_EQUIP = False
self.server.card_present = False
+22
View File
@@ -444,6 +444,28 @@ class AutoEquipTests(unittest.TestCase):
self.assertIn('AUTO-EQUIP: init output:', out)
self.assertIn('ProtocolError: Connection failed', out)
def test_a_long_traceback_keeps_its_exception_line(self):
# cmd2 renders a rich-box traceback; a fixed head slice cut the
# exception itself off (seen live on Windows 2026-10-21), so the tail
# is always included.
srv = self.make_server(None)
def onecmd(cmd):
srv.app.card = SimpleNamespace(_scc='scc')
sys.stderr.write('Traceback (most recent call last):\n')
for i in range(30):
sys.stderr.write(' File "f%d", line %d, in step%d\n' % (i, i, i))
sys.stderr.write('CardConnectionException: No card is inserted\n')
srv.app.onecmd_plus_hooks = onecmd
with mock.patch.object(server, '_ensure_transport', lambda s: True), \
mock.patch.object(server, '_apply_equipped_card', lambda s: None), \
mock.patch('sys.stderr', new=StringIO()) as err:
self.assertFalse(server._auto_equip_attempt(srv))
out = err.getvalue()
self.assertIn('AUTO-EQUIP: init output:', out)
self.assertIn('CardConnectionException: No card is inserted', out)
def test_failed_attempt_unequips_the_half_initialized_shell(self):
calls = []
srv = self.make_server(None)
+118
View File
@@ -0,0 +1,118 @@
"""cmd2/pySim exception output must fail /api/write and /api/read.
`onecmd_plus_hooks()` catches a command's exception and prints
``EXCEPTION of type ...`` (plus a traceback in debug mode) instead of
re-raising, so the handlers that only looked for ``got (XXXX)`` reported a
successful write while nothing was written. The handlers now treat both
markers as failure; the SW-based success/failure semantics stay as before.
"""
import types
import unittest
from unittest import mock
from pysim_simple_server import server as srv
from pysim_simple_server.server import PysimHandler, _cmd_exception
class FakeApp:
def __init__(self, output):
self.output = output
self.stdout = None
self.rs = types.SimpleNamespace(lchan=[types.SimpleNamespace(selected_file=None)])
def onecmd_plus_hooks(self, cmd):
# pySim writes into the app's stdout, which the handler redirects
self.stdout.write(self.output)
def _fake_handler(path, body, app):
h = object.__new__(PysimHandler)
h.path = path
h.command = 'POST'
h.headers = {}
h.server = types.SimpleNamespace(app=app, log_requests=False, pbr=None)
h._read_body = lambda: body
h._log_req = lambda *a, **k: None
h._log_resp = lambda *a, **k: None
sent = []
h._send_json = lambda data, status=200: sent.append((data, status))
return h, sent
class CmdExceptionTests(unittest.TestCase):
def test_the_helper_detects_both_markers_and_returns_the_first_line(self):
self.assertIsNone(_cmd_exception(''))
self.assertIsNone(_cmd_exception('SW: 9000\n'))
exc = ("EXCEPTION of type 'ValueError' occurred with message: 'boom'\n"
"Traceback (most recent call last):\n File x\nValueError: boom\n")
self.assertEqual(_cmd_exception(exc),
"EXCEPTION of type 'ValueError' occurred with message: 'boom'")
tb = 'Traceback (most recent call last):\n File x\nRuntimeError: boom\n'
self.assertEqual(_cmd_exception(tb), 'Traceback (most recent call last):')
class WriteExceptionTests(unittest.TestCase):
def _write(self, output):
app = FakeApp(output)
h, sent = _fake_handler('/api/write', {
'name': 'EF.ADN', 'data': 'AA' * 30, 'record_nr': 1, 'allow_probe': True,
}, app)
body = {'name': 'EF.ADN', 'data': 'AA' * 30, 'record_nr': 1, 'allow_probe': True}
h._read_body = lambda: body
with mock.patch.object(srv, '_select_with_parent', lambda *a, **k: (None, None)), \
mock.patch.object(srv, '_get_file_type', lambda *a: 'linear_fixed'), \
mock.patch.object(srv, '_arr_refresh_after_write', lambda *a, **k: None):
PysimHandler._do_POST(h)
self.assertEqual(len(sent), 1, sent)
return sent[0][0]
def test_a_cmd2_exception_is_not_a_successful_write(self):
resp = self._write("EXCEPTION of type 'Exception' occurred with message: 'no card'\n")
self.assertFalse(resp['success'])
self.assertIn('EXCEPTION of type', resp['error'])
self.assertNotIn('sw', resp)
def test_a_traceback_is_not_a_successful_write(self):
resp = self._write('Traceback (most recent call last):\n File x\nValueError: boom\n')
self.assertFalse(resp['success'])
self.assertEqual(resp['error'], 'Traceback (most recent call last):')
def test_the_sw_semantics_are_unchanged(self):
self.assertEqual(self._write('SW: 9000\n'), {'success': True, 'sw': '9000'})
self.assertEqual(self._write('SW: 6982 - Security status not satisfied\n'
'got 6982'),
{'success': False, 'sw': '6982',
'error': 'Security status not satisfied'})
class ReadExceptionTests(unittest.TestCase):
def _read(self, output, mode='raw'):
app = FakeApp(output)
body = {'name': 'EF.ADN', 'mode': mode, 'allow_probe': True, 'record_nr': 1}
h, sent = _fake_handler('/api/read', body, app)
with mock.patch.object(srv, '_select_with_parent', lambda *a, **k: (None, None)), \
mock.patch.object(srv, '_get_file_type', lambda *a: 'transparent'), \
mock.patch.object(srv, '_collect_apdu_times', lambda: None), \
mock.patch.object(srv, '_end_apdu_time_collection', lambda: []):
PysimHandler._do_POST(h)
self.assertEqual(len(sent), 1, sent)
return sent[0][0]
def test_a_cmd2_exception_is_not_a_successful_read(self):
resp = self._read("EXCEPTION of type 'Exception' occurred with message: 'no card'\n")
self.assertFalse(resp['success'])
self.assertIn('EXCEPTION of type', resp['error'])
resp = self._read('Traceback (most recent call last):\n File x\n', mode='decoded')
self.assertFalse(resp['success'])
self.assertEqual(resp['error'], 'Traceback (most recent call last):')
def test_a_normal_read_keeps_the_sw_parse(self):
resp = self._read('AABB\nSW: 9000\n')
self.assertTrue(resp['success'])
self.assertEqual(resp['sw'], '9000')
self.assertEqual(resp['data'], 'AABB')
if __name__ == '__main__':
unittest.main()
+45 -1
View File
@@ -88,7 +88,8 @@ class BatchStateTests(unittest.TestCase):
'failed_step': 'LOAD (3/7)', 'por_sw': '6A80'})
srv._ram_batch_record(st, {'iccid': '8970119000004600114', 'result': 'skipped',
'reason': 'no preset matches this ICCID'})
self.assertEqual(st['counts'], {'installed': 1, 'failed': 1, 'skipped': 1})
self.assertEqual(st['counts'], {'installed': 1, 'already_absent': 0,
'failed': 1, 'skipped': 1})
self.assertEqual(st['attempts_total'], 3)
# the notice names the last attempt's outcome
self.assertIn('skipped: no preset matches this ICCID', st['notice'])
@@ -230,6 +231,49 @@ class BatchCardTests(unittest.TestCase):
self.assertEqual(seen['current']['iccid'], '8970119000004600098')
self.assertIn('installed', st['notice']) # ...and set for the new outcome
def test_a_delete_that_finds_nothing_is_reported_already_absent(self):
"""A delete run whose DELETE answered 6A88 is a success (the object is
gone, the goal is met) but not a deletion: it is counted in its own
bucket and the notice says so (v3.36.0)."""
st = srv._ram_batch_new_state(_plan('delete'))
preset = _preset('8970119000004600098', 'Alfa')
def fake_run(server, body, plan, scc):
return {'success': True, 'final_cntr': '0000000002',
'steps': [{'name': 'DELETE (application)', 'sw': '9000',
'por_status': 'por_ok', 'por_sw': '6A88',
'delete_not_found': True}]}
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]
self.assertEqual(rec['result'], 'already_absent')
self.assertTrue(rec['not_found'])
self.assertEqual(st['counts']['already_absent'], 1)
self.assertEqual(st['counts']['installed'], 0)
self.assertIn('already absent', st['notice'])
self.assertEqual(st['seen']['8970119000004600098'], 'already_absent')
def test_a_reinstall_with_a_tolerated_pre_delete_is_still_an_install(self):
"""The reinstall's pre-delete may tolerate 'not found', but the
outcome is the INSTALL, not 'already absent'."""
st = srv._ram_batch_new_state(_plan('reinstall-app'))
preset = _preset('8970119000004600098', 'Alfa')
def fake_run(server, body, plan, scc):
return {'success': True,
'steps': [{'name': 'DELETE (application)', 'por_sw': '6A88',
'delete_not_found': True},
{'name': 'INSTALL [for install]', 'por_sw': '9000'}]}
with mock.patch.object(srv, '_ram_plan_run', fake_run):
srv._ram_batch_handle_card(st, _fake_server('8970119000004600098',
_FakeStore([preset])))
self.assertEqual(st['attempts'][0]['result'], 'installed')
self.assertEqual(st['counts']['installed'], 1)
self.assertEqual(st['counts']['already_absent'], 0)
def test_a_card_already_processed_is_reported_not_reinstalled(self):
st = srv._ram_batch_new_state(_plan())
preset = _preset('8970119000004600098', 'Alfa')
+26
View File
@@ -8,6 +8,7 @@ line and no traceback, and the pySim-internal 'Waiting for card...' /
failure keeps the stock-pysim fallback and its traceback.
"""
import inspect
import io
import sys
import unittest
@@ -158,3 +159,28 @@ def _parser():
if __name__ == '__main__':
unittest.main()
class StartupCardLostTests(unittest.TestCase):
"""A card removed during the startup init leaves the server cardless.
The startup code used to keep the card object it had already built:
``card_present`` and ``_CARD_CONNECTED`` stayed True, so the server
reported a phantom equipped card and the auto-equip skipped every later
insertion (its ``_CARD_CONNECTED`` early return). The PC/SC error must
drop the half-initialized session instead - and not print a traceback for
a normal removal."""
def test_a_pcsc_interruption_drops_the_session(self):
src = inspect.getsource(srv_main.main)
self.assertIn('_is_pcsc_error(e)', src)
self.assertIn('card removed during initialization', src)
self.assertIn('_clear_app_card_state(app)', src)
# the locals that feed card_present / _CARD_CONNECTED are cleared
for line in ('iccid = None', 'card = None', 'rs = None', 'scc = None'):
self.assertIn(line, src)
def test_the_session_state_is_derived_from_the_cleared_card(self):
# the assignments that would otherwise report a phantom card
src = inspect.getsource(srv_main.main)
self.assertIn('server.card_present = card is not None', src)
self.assertIn('_CARD_CONNECTED = card is not None', src)