27 Commits
Author SHA1 Message Date
catarrh 465a43eef2 docs: link the project website alongside the Telegram channel
release / release (push) Waiting to run
2026-10-26 22:11:38 +03:00
catarrh fdbff77188 release: 3.40.2 — file-filter reset (✕) button
A red ✕ next to the filter selector appears whenever the filter is not
"All" and resets it to "All" in one click (pysimFsFilterClear).  Bump
VERSION/pyproject/PWA header to 3.40.2 and the SW cache to simple-v487.
2026-10-26 21:01:29 +03:00
catarrh 1f8fa54c49 release: 3.40.1 — filter uses per-spec service names
release / release (push) Waiting to run
Drop the invented "SMS-related" / "CB-related" filter categories: those
files are service-declared and are already reached through their
per-service options (per-spec service names).  Bump VERSION/pyproject/
PWA header to 3.40.1 and the SW cache to simple-v486.
2026-10-26 19:41:32 +03:00
catarrh 98926833f7 release: 3.40.0 — file-tree filter
release / release (push) Canceled after 0s
Bump VERSION/pyproject/PWA header to 3.40.0 and the SW cache to
simple-v485 (index.html changed).  CHANGELOG [3.40.0] documents the
file-tree filter (checkbox multiselect, path-exact DF hiding, expand/
collapse all, auto-expand, and the "To EF filter" cross-link).
2026-10-26 15:20:08 +03:00
catarrh 4fd2efe77c file manager: style expand/collapse as buttons like the All filter
Give Expand all / Collapse all the same bordered button style as the
filter button in its default 'All' state.  Frontend 1163 tests green.
2026-10-26 15:10:22 +03:00
catarrh 4a4232de5c file manager: expand/collapse all + auto-expand on filter
- "Expand all" / "Collapse all" links in the sort row: expand-all walks
  the directory tree loading children on demand (skipping DFs the probe
  found absent), collapse-all resets every directory.
- Selecting any filter but "All" auto-expands the tree so the matched
  files are visible (pysimFsAutoExpandForFilter, wired into the two
  filter setters).
- i18n EN/RU for the two links.

Frontend 1163 tests green (+2).  (Version/sw bump still deferred.)
2026-10-26 15:05:49 +03:00
catarrh cc7c23a9c7 file manager: blue filter button when a filter is active
A non-'All' selection turns the collapsed filter button blue (like the
active sort pill) so it stands out; 'All' keeps the default style.
Frontend 1161 tests green.
2026-10-26 14:53:36 +03:00
catarrh 8fdf1ddff9 file manager: hide directories outside the filter's file paths
When a filter is active (not "All"), only the matching files, their
ancestor directories and the MF root stay; every other directory is
hidden.  The ancestors come from the standard file list (name -> paths):
pysimFsFilterMatchingPaths unions the registry paths of every matching
name, and pysimFsFilterShow keeps a directory iff it matches itself
(e.g. DF.GSM for svc:GPRS) or its canonical path prefixes a matching
path.  Custom files contribute their own paths; the not-loaded window
keeps directories rather than hiding everything.

Frontend 1160 tests green (+8).  (Version/sw bump still deferred.)
2026-10-26 14:49:44 +03:00
catarrh e2d5d720f7 file manager: move the filter to its own row under the sort pills
Filter names (the per-service options) are long, so the label + dropdown
now live on a dedicated row below Sort.  Frontend 1152 tests green.
2026-10-26 14:37:37 +03:00
catarrh 627198e57b service editors: compact 'To EF filter' glyph with tooltip
The per-service cross-link is now a single compact glyph (U+25BE) with
'To EF filter' as the title/aria-label, instead of a text button that
shifted the grid.  Frontend 1152 tests green.
2026-10-26 14:17:18 +03:00
catarrh 77fbed80ce service editors: "To EF filter" per-service cross-link
In the SST/UST/IST/EST decoded editors, each service row backed by files
gains a small "To EF filter" action that sets the file-tree filter to
exactly that service's option:

- The four service-table fields carry a `table` key (sst/ust/est/ist);
  servicesFeatureByNumber() inverts SERVICES_CROSS_REF into per-table
  number->feature maps (lazy, cached).
- pysimFsFilterBtnHtml(table, n) renders the button only for file-backed
  features; pysimFsSetFilterToService(table, n) sets pysimFsFilter to
  ['svc:<feature>'], persists, and re-renders the dropdown + tree.
- i18n: "To EF filter" (EN/RU).  No CSS rebuild needed.

Frontend 1152 tests green (+6).  (Version/sw bump still deferred.)
2026-10-26 14:06:06 +03:00
catarrh d1067e3e72 file manager: filter as a collapsed checkbox dropdown
Replace the <select multiple size=4> filter with a single-row button that
expands into a scrollable checkbox menu (All + Custom files, then the
Identifiers/PLMN/Services headings with the categories and per-service
options), mirroring simtrace-analyser's command filter.

- pysimFsFilterSummary/Label, pysimFsFilterToggle, pysimFsFilterSetEntry
  (all-exclusive, empty -> all) and a rewritten pysimFsFilterRender;
  pysimFsSetFilter is gone.
- Closes on outside click and Escape.  i18n: "Selected: {n}".
- No CSS rebuild needed (swapped to utility classes already in the
  prebuilt style.css).

Frontend 1146 tests green.  (Version/sw bump still deferred.)
2026-10-26 13:55:48 +03:00
catarrh 01e50d841f file manager: implement the tree filter (categories + per-service)
Fill in the scaffolded file-tree filter:

- FS_FILTER_GROUPS: Identifiers, PLMN selection, Provider names,
  WLAN/I-WLAN, SMS-related and CB-related (cell broadcast), each with
  its exact file list; plus one option per file-backed feature of
  SERVICES_FS_MAP (svc:<feature>).
- "All" is the default and exclusive; a top-level "Custom files"
  option matches the injected custom-file nodes by their flag.
- fsFilterIndexBuild inverts name -> criterion set; pysimFsFilterMatch
  matches by name (and custom by flag); pysimFsFilterShow keeps a
  directory while it has a matching (loaded) descendant, and keeps
  never-expanded directories.  The dropdown renders All / Custom files
  and three optgroups (Identifiers / PLMN / Services).
- i18n EN/RU for the new labels.

Frontend 1145 tests green (fs_filter.test.js 19).
2026-10-26 13:34:54 +03:00
catarrh 3878156646 file manager: scaffold the tree filter (multiselect)
Add a Filter multiselect next to the sort pills in the file tree toolbar.
The option list is large and undecided, so FS_FILTER_OPTIONS is empty and
the per-node match is a no-op for now; the mechanism is wired end to end:

- FS_FILTER_OPTIONS const (empty), pysimFsFilter state persisted to
  simple_fs_filter, pysimFsSetFilter / pysimFsFilterMatch /
  pysimFsFilterRender, and the render loop consults pysimFsFilterMatch.
- The dropdown re-renders on language switch; "Filter:" and
  "(no filters yet)" are translated (EN/RU).

Frontend 1132 tests green (6 new scaffold pins).
2026-10-26 13:03:22 +03:00
catarrh 105efe62da release: 3.39.0 — decoded-editing batch
release / release (push) Canceled after 0s
Bump VERSION/pyproject/PWA header to 3.39.0 and the SW cache to
simple-v484 (index.html + help changed).  CHANGELOG [3.39.0] folds the
unreleased 3.38.2 start-script fix and documents the decoded-editing
batch: DF-context resolution, EF.ECC/EF.AD split, path-based selection,
EF.SST tri-state editor, the Services editor + path-exact FS check, and
the UST 147-150 completion.
2026-10-26 00:11:26 +03:00
catarrh 4835bc8585 Services editor: throbber while the filesystem check runs
Show the established animate-spin throbber next to the Check filesystem
button while the scan (tree load + probe-all + report) is in flight, and
hide it when it settles.  Frontend 1126 tests green.
2026-10-26 00:07:54 +03:00
catarrh b173e73437 Services editor: path-exact filesystem check
Rework the FS check from FID-level to path-exact:

- SERVICES_FS_MAP now maps a feature to symbolic roles (registry names),
  not FIDs; the (name, root) -> path index resolves each role to its
  unambiguous canonical path (same-FID files in other DFs - EF.SAI vs
  EF.PBR, EF.AST - resolve separately).
- Per-tree declaration: SST -> MF, UST/EST -> ADF.USIM, IST -> ADF.ISIM,
  with a DF.TELECOM fallback for USIM/ISIM roles whose DFs live under
  DF.TELECOM (MMS storage, MCPTT, V2X, A2X), except the GSM-application
  mapped copies (ADN/EXT1/ECCP).
- The report carries the path (and declaring tree) for missing and
  present-but-undeclared, plus an honest "unchecked" bucket for roles
  with no path yet (DF.GRAPHICS/IMG, SIM DF.SoLSA/MExE copies,
  EF.3GPPPSDATAOFFservicelist).
- efDfFromPath maps A000000087100B -> ADF.USIM-NON-IMSI.

Frontend 1126 + Python 1091 tests green.  (Version/sw bump still
deferred for the decoded-editing batch.)
2026-10-26 00:01:23 +03:00
catarrh a1892094f4 uicc_files: supplement optional files missing from pySim's model
Add a supplement to the cardless registry generator for the 13 files the
Services editor's FS check references but pySim does not emit (optional,
service-gated): EF.LRPLMNSI/IAL/IPD/3GPPPSDATAOFF/TVCONFIG, DF.MExE +
EF.MExE-ST/ORPK/ARPK/TPRPK, DF.SoLSA + EF.SAI/EF.SLL, DF.BCAST,
DF.ACDC + EF.ACDC_LIST, DF.TV, and DF.A2X + EF.AST/EF.A2X_CONFIG (under
DF.TELECOM).  Emitted with distinct (name, path) so same-FID files in
other DFs (EF.SAI vs EF.PBR, EF.AST) stay separate; ADF.USIM rows are
mirrored to ADF.USIM-NON-IMSI.  490 -> 527 entries.

Regenerated the asset; test_uicc_files.py pins the 20 new paths, the
FID-collision cases and (name, root) uniqueness.  1091 Python tests.
2026-10-26 00:01:23 +03:00
catarrh 12ff552df8 Services editor: filesystem check
Add a "Check filesystem" action to the Services pill:

- SERVICES_FS_MAP: the feature -> FID list transcribed from
  docs/services-editor.md §A (127 file-backed + 34 capability features,
  157 distinct FIDs; a completeness/data test pins it).
- servicesFsReport: pure two-direction check - declared features whose
  mapped files are absent, and present files whose features are
  undeclared.  FID-level (not DF-exact) for this first pass; documented.
- The Check runs the tree probe (once per session) and renders the
  missing/extra lists or the "matches" line, with an FID-level note.

Also fix a doc typo (eCall Data's EF.FDN was written as 6F3A = EF.ADN).
Frontend 1120 + Python 1090 tests green.  (Version/sw bump still
deferred for the decoded-editing batch.)
2026-10-25 22:22:33 +03:00
catarrh e10228c37c Services editor (core): cross-file service tables
Add a Services pill to the Card reader tab that shows the card's service
tables side by side:

- SERVICES_CROSS_REF: the 161-row feature cross-reference (SST/UST/EST/IST
  numbers + class) transcribed from docs/services-editor.md §A; a
  completeness test pins every non-RFU service number to exactly one row.
- Reads the four files (DF.GSM/6F38, ADF.USIM/6F38, ADF.USIM/6F56,
  ADF.ISIM/6F07) via /api/read, cached per card session; a column renders
  only for a present file (no ISIM -> no IST column).
- SST cell is on/allocated-off only (never de-allocates); UST/EST/IST are
  checkboxes; per-file Save buttons write via /api/write, preserving
  unmodelled bits and reporting each result.

Frontend 1105 + Python 1090 tests green.  (Version/sw bump deferred;
FS-check is the next step.)
2026-10-25 22:06:31 +03:00
catarrh fc27feba8e EF.SST: tri-state editor (deleted / allocated-off / on)
Give EF.SST its own per-file editor for the 2-bit SIM service table
(TS 51.011 10.3.7: allocated + activated per service):

- efEncSst encodes the pair layout, preserving unmodelled pairs and the
  file length; validates service number, fit and state.
- A new `bits2` field kind renders a three-state select per service and
  collects {n, name, allocated, activated}; registered as EF_SST_FIELDS
  with the encoder (df '7F20'), and SST leaves EF_READONLY_NAMES.
- i18n: Deleted/Off/On + the state-error and hint strings (EN/RU).

Docs/help updated: SST is no longer read-only.  Frontend 1093 + Python
1090 tests green.  (Version/sw bump still deferred for the unreleased
decoded-editing batch.)
2026-10-25 21:43:07 +03:00
catarrh 5ebcd0773d file manager: path-based selection + ADF AID normalization
Make the DF context from the resolver actually reach the live views:

- pysimFsSelected now stores the canonical path (pysimFsNodePath), not
  the node name; pysimFsRenderNode emits the path in the click/toggle
  handlers; every consumer resolves via pysimFsFindNodeByPath.  The
  name-based pysimFsFindNode is removed - two same-named files under
  different DFs (EF.ECC/EF.AD, DF.PHONEBOOK copies) are now distinct.
- efDfFromPath maps ADF-rooted parent segments by RID (A0000000871002
  -> ADF.USIM, A0000000871004 -> ADF.ISIM), so snapshot/profile paths
  rooted by AID resolve to the same DF key as the named form.
- efDecEcc shares efEccCategory with efDecEccUsim (TS 24.008
  Table 10.5.135d), so the SIM and USIM category displays can't drift.

Frontend 1087 + Python 1090 tests green.  (Version/sw bump deferred -
  this is part of the unreleased decoded-editing batch.)
2026-10-25 21:01:11 +03:00
catarrh 8e0a97f025 EF resolver: resolve by (DF, FID); split EF.ECC and EF.AD per DF
The decoder/encoder lookup now takes an optional DF context and resolves
DF-first, then name, then FID (the spec keys files by FID within their
DF; the name is a display label).  efDfFromPath derives the parent DF
segment of a canonical path.

EF.ECC is split into its two DF-specific decoders: DF.GSM (7F20)
transparent 3n (existing) and ADF.USIM linear-fixed X+4 - the new
efDecEccUsim reads the 3-byte Emergency Call Code, the Annex A alpha and
the Emergency Service Category (decoded per TS 24.008 Table 10.5.135d).
EF.AD is registered as DF-keyed entries (7F20 / ADF.USIM / ADF.ISIM),
reusing the existing encoder.

The df is threaded through the file-manager decoded-view/edit/save and
the snapshot/compare decode.  No ECC editor yet (read-only).

Frontend 1081 + Python 1090 tests green.
2026-10-25 12:40:51 +03:00
catarrh 6d762b9d9c ust: complete the USIM service table (147-150) + Services editor data fixes
The PWA's EF_UST_SERVICES stopped at 146; TS 31.102 §4.2.8 pins 147-150
(5MBS UE pre-configuration, Operator Controlled Signal Threshold per
Access Technology, A2X, IMS Data Channel Indication).  Add them and fix
the two test fixtures that assumed the 146-service table.

Resolve the three Services-editor FS mappings against the spec:
- eCall Data (89) -> EF.SDN 6F49 (+ EF.FDN 6F3A)
- eCall Data over IMS (112) -> EF.SDNURI 6FEF (+ EF.FDNURI 6FED)
- EARFCN list for MTC/NB-IOT (121) -> EF.EARFCNList 6FFD; "Extended
  EARFCN support" (100) is a capability, not a file.

Record the FID-first rule: the resolver and FS check key by (DF, FID),
the symbolic name is only a label; and clarify that the FS check is
card-only (independent of the cardless uicc_files.json list).
2026-10-25 12:10:14 +03:00
catarrh 1d5a049ea1 docs: Services editor data tables (draft)
Feature cross-reference (SST/UST/IST/EST) and feature->filesystem
mapping for the planned cross-file Services editor.  Reconciliation
exact: 160 feature rows, every service number exactly once.

Flags for review: UST list is 4 short of the spec (147-150 missing
from EF_UST_SERVICES), GSM Access gates DF.GSM-ACCESS 5F3B (not DF.GSM),
CCP/CCP2/ECCP equivalence, and three unresolved file mappings.
2026-10-25 11:52:28 +03:00
catarrh 504bf1d66e docs: SIM/USIM format divergence + cross-file Services editor design
- correct finding 12 (EF.LP is 1-byte; EF.PL is 2-byte like EF.LI)
- name-collision class now names EF.ECC and EF.AD
- split EF.LP/EF.PL and EF.AD (SIM/USIM) survey rows
- add §2.4 SIM/USIM divergence table (groups A/B/C)
- add §6 Services editor + §6.1 cross-application candidates
2026-10-25 11:36:12 +03:00
catarrh 62c5b4e1a9 start: name the requested --http-host/--http-port in the opening line
The first line was hardcoded to http://127.0.0.1:8080 while the flags
were passed through, so ./start.sh --http-host 10.0.28.161 printed
127.0.0.1 although the server bound 10.0.28.161 (its own listening banner
named it).  Both start scripts now parse --http-host/--http-port (space
and = forms, the last value wins like argparse) and echo the effective
address; start.bat parses with shift (%* is unaffected, so the server
still receives every argument).

Bump 3.38.2 (server, pyproject, PWA header, sw simple-v483) and add the
CHANGELOG entry.

Tests: tests/test_pcsc_startup.py pins the parsing/echo (2 new).
1090 Python + 1075 frontend.
2026-10-25 03:25:22 +03:00
27 changed files with 4885 additions and 159 deletions

No files matched your search

+90
View File
@@ -9,6 +9,96 @@ every change carries a descriptive commit message.
## [Unreleased] ## [Unreleased]
## [3.40.2] — 2026-10-26
### Added
- **File filter: reset (✕) button.** A red ✕ to the right of the filter
selector appears whenever the filter is not "All" and resets it to "All"
in one click.
## [3.40.1] — 2026-10-26
### Changed
- **File filter: drop the "SMS-related" / "CB-related" categories.** Those
files are service-declared and already reachable through their per-service
filter options, which carry the per-spec service names ("Short Message
Storage (SMS)", "SMS Parameters (SMSP)", "Short Message Status Reports
(SMSR)", "Cell Broadcast Message Identifier (CBMI)", "Cell Broadcast
Message Identifier Ranges (CBMIR)").
## [3.40.0] — 2026-10-26
### Added
- **File-tree filter** (the file manager gains a **Filter** dropdown, next to
Sort). A collapsed checkbox multiselect with: **All** (default), **Custom
files**, **Identifiers** (ICCID/IMSI/MSISDN/IMPI/IMPU/SUPI_NAI/GID1/GID2),
**PLMN selection** / **Provider names** / **WLAN/I-WLAN** / **SMS-related** /
**CB-related** (cell broadcast), and one option per file-backed service of
the cross-reference. When a filter is active, directories that are not on
the path of the selected files are hidden; the button turns blue to stand
out.
- **Expand all / Collapse all** buttons beside the sort pills; **Expand all**
walks the tree loading children on demand and skips directories the probe
found absent, and selecting any filter other than "All" auto-expands the
tree so the matched files are visible.
- **"To EF filter"** cross-link: each file-backed service row in the
SST/UST/IST/EST decoded editors carries a compact glyph that narrows the
file-tree filter to exactly that service's files.
## [3.39.0] — 2026-10-26
### Added
- **EF.SST tri-state editor.** The SIM service table (TS 51.011 10.3.7, two
bits per service — allocated + activated) now edits per service as
*deleted / allocated-off / on*, preserving unmodelled pairs and the file
length; it stops being read-only.
- **Cross-file Services editor** (a new Card-reader pill): SST / UST / EST /
IST shown side by side from a curated 161-row feature cross-reference, a
column rendered only for a file present on the card, per-file Save buttons,
and a **Check filesystem** action (with a throbber) that verifies the
declared services against the card's actual files.
- **Path-exact filesystem check.** Each feature's files are resolved by
(symbolic name, root) → canonical path through `uicc_files.json`, probed at
that exact path, and reported in both directions (declared-but-missing,
present-but-undeclared) plus an "unchecked" bucket for files not yet in the
registry.
### Changed
- **EF resolution is DF-context aware.** `efDfFromPath` derives the parent DF
and `efFindDecoder`/`efFindEncoder` resolve DF → name → FID, so same-name/
same-FID files in different DFs are distinct; EF.ECC gained a USIM
linear-fixed decoder beside the SIM transparent one, and EF.AD is registered
per DF.
- **The file manager selects files by canonical path** (not node name), so two
same-named files under different DFs (EF.ECC/EF.AD, the DF.PHONEBOOK copies)
are distinguishable, and snapshot/profile paths root ADFs by AID are
normalized (`A0000000871002`→USIM, `…1004`→ISIM, `…100B`→USIM-non-IMSI).
- **The USIM service table is complete**: UST 147–150 (5MBS UE pre-configuration,
Operator Controlled Signal Threshold per Access Technology, A2X, IMS Data
Channel Indication) were missing.
- **`uicc_files.json`** gained the 13 optional files pySim does not model
(DF.MExE, DF.SoLSA, DF.BCAST, DF.ACDC, DF.TV, DF.A2X and their children),
emitted with distinct (name, path) so same-FID files in other DFs stay
separate.
### Fixed
- **The start scripts name the address they were asked to bind.** The opening
`Starting pysim-simple-server on http://127.0.0.1:8080` line was hardcoded
while `--http-host`/`--http-port` were passed through to the server:
`./start.sh --http-host 10.0.28.161` bound correctly (the server's own
`listening on` banner named the address), but the first line still said
127.0.0.1. `start.sh` and `start.bat` now parse both flags (space and `=`
forms, the last value wins like argparse) and echo the effective address;
the defaults are unchanged.
- eCall Data's `EF.FDN` reference was a typo (`6F3A` is EF.ADN); corrected to
EF.SDN 6F49 / EF.FDN 6F3B.
## [3.38.1] — 2026-10-25 ## [3.38.1] — 2026-10-25
### Performance ### Performance
+1 -1
View File
@@ -2,7 +2,7 @@
> Formerly known as OTAMan — renamed to SIMple on 2026-09-19. > Formerly known as OTAMan — renamed to SIMple on 2026-09-19.
📢 Project news and updates: [Telegram channel](https://t.me/youeyeseesee) 📢 Project news and updates: [website](https://atroshin.ru) · [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: 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:
+1 -1
View File
@@ -2,7 +2,7 @@
> Прежнее название — OTAMan; переименован в SIMple 2026-09-19. > Прежнее название — OTAMan; переименован в SIMple 2026-09-19.
📢 Новости и обновления проекта: [Telegram-канал](https://t.me/youeyeseesee) 📢 Новости и обновления проекта: [сайт](https://atroshin.ru) · [Telegram-канал](https://t.me/youeyeseesee)
SIMple — инструментарий для специалистов по UICC/SIM-картам: автономный HTML/JS PWA и локальный сервер карт ([`pysim-simple-server`](pysim_simple_server/) — мост к pySim/PC-SC, он же раздаёт PWA). Закрывает весь цикл лабораторной работы: SIMple — инструментарий для специалистов по UICC/SIM-картам: автономный HTML/JS PWA и локальный сервер карт ([`pysim-simple-server`](pysim_simple_server/) — мост к pySim/PC-SC, он же раздаёт PWA). Закрывает весь цикл лабораторной работы:
+133 -16
View File
@@ -64,10 +64,11 @@ editing candidates.
| 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.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.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.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.SST | 6F38 | TS 51.011 10.3.7 | X ≥ 1 | **2 bits/service** (allocated + activated) | S6 | **shipped** — tri-state editor (deleted / allocated-off / on) |
| EF.EST | 6F56 | TS 31.102 4.2.47 | X ≥ 1 | enabled-service bits (3 services) | S6 | **3.34** | | 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.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.LP | 6F05 | TS 51.011 10.3.1 | n | **1 byte per language** (SMS 7-bit) | S6 | 3.35 |
| EF.PL | 2F05 | TS 102 221 13.3 | 2n | ISO 639 letters (2 B each, like LI) | S6 | 3.35 |
| EF.SPN | 6F46 | TS 31.102 4.2.12 | 17 B | condition byte + Annex A text | S2 | **3.33** | | 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.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.FPLMN | 6F7B | TS 31.102 4.2.16 | 3n (n ≥ 4) | PLMN list (FFFFFF holes anywhere) | S4 | **3.34** |
@@ -82,7 +83,8 @@ editing candidates.
| 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.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.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.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.AD (SIM) | 6FAD | TS 51.011 10.3.18 | 3+X | op-mode byte + additional info (no MNC length) | S1 | **3.34** |
| EF.AD (USIM) | 6FAD | TS 31.102 4.2.18 | 4+X | op-mode byte + info + MNC length (may be 0) | S1 | **3.34** |
| EF.CBMI | 6F45 | TS 31.102 4.2.14 | 2n | CB message ids | S6 | **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.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.CBMIR | 6F50 | TS 31.102 4.2.22 | 4n | CB id ranges | S6 | **3.34** |
@@ -126,6 +128,28 @@ 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 network parameters, START-HFN). Nothing in the survey forces a new shape
beyond S9/S10. beyond S9/S10.
### 2.4 SIM ↔ USIM format divergence
Some features are represented by a different file (or the same FID with a
different structure) in the SIM (DF.GSM, TS 51.011) and USIM (ADF.USIM,
TS 31.102) applications. Three groups:
| group | SIM | USIM | purpose |
|---|---|---|---|
| A — same FID + name, different format | EF.ECC 6FB7 transparent 3n (code+ESC) | EF.ECC 6FB7 linear X+4 (code+alpha+ESC) | emergency codes |
| A | EF.AD 6FAD 3+X (no MNC length) | EF.AD 6FAD 4+X (+ MNC length) | administrative data |
| B — same FID, different name + format | EF.SST 6F38 (2-bit) | EF.UST 6F38 (1-bit) | service table |
| B | EF.LP 6F05 (1 B/lang) | EF.LI 6F05 (2 B/lang) | language preference |
| C — different FID + name + format | EF.PLMNsel 6F30 (3n, no AcT) | EF.PLMNwAcT 6F60 (5n, +AcT) | user PLMN selector |
| C | EF.Kc/KcGPRS 6F20/6F52 (9 B) | EF.Keys/KeysPS 6F08/6F09 (33 B) | ciphering keys |
| C | EF.CCP 6F3D (14 B) | EF.CCP2 6F4F (X≥15) | bearer capability |
Group A is the only one where the *name and FID both* collide, so those two
(EF.ECC, EF.AD) are what the DF-context resolver (finding 6) is for; groups B
and C are already disambiguated by name. EF.HPLMNwAcT (a list) vs the SIM
EF.HPPLMN (a 1-byte search period) is a related-but-different pair, and
OPLMNwAcT/OPL/PNN/EHPLMN/FPLMN/SPDI/IMG are USIM-only (no SIM counterpart).
## 3. Findings (defects and constraints the survey exposed) ## 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 1. **EF.BDN is X+15, not X+14.** The shared ADN codec assumes the last two
@@ -133,10 +157,16 @@ beyond S9/S10.
(`X+15`, TS 31.102 4.2.44). The read-only decoded view currently (`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 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. 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); 2. **EF.ECC is two different files, not one file with two layouts.** They
USIM: linear fixed X+4 (TS 31.102 4.2.21). The decoder implements the SIM share the FID `6FB7` **and the name `EF.ECC`**, but live in different DFs
layout only; on a USIM the alpha bytes are read as more codes. A under different specs: DF.GSM — transparent 3n, code(2 BCD) + category
structure-aware decode is needed before an ECC editor. (TS 51.011 10.3.27); ADF.USIM — linear fixed X+4, code(3 BCD) + alpha
(Annex A) + category (TS 31.102 4.2.21). The decoder implements the SIM
file only; on a USIM the alpha bytes are read as more codes. They also
need **different editors** (a plain code list vs a record form with a text
alpha), so they are two registry entries, resolved by the DF context —
not a name/FID lookup, which is ambiguous here (the name *and* the FID
collide).
3. **S8 is not one shape.** PNN (flat tag+length+value TLVs) is the simple 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 case. DIR/ARR/PBR nest TLVs and cross-reference other files; they need
either a tree editor or a structured read-only view first. either a tree editor or a structured read-only view first.
@@ -150,7 +180,13 @@ beyond S9/S10.
6. **FID collisions across DFs** (e.g. 6F02 = EF.IMPI in ISIM and EF.OCST in 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 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 the primary one and encoders should not be fid-only where a collision is
possible. possible. **Name collisions across DFs** also exist — EF.ECC and EF.AD
(DF.GSM vs ADF.USIM) — where both the name *and* the FID collide. The
specs key files by **FID within their DF**, not by symbolic name (names are
a derived display label), so the resolver must be **(DF, FID)-first**: the
parent DF + FID is the authoritative key, and the name is only a label.
The FS check in the Services editor follows the same rule — it probes a
(DF, FID), never a name.
7. **Text coding is per file.** Annex A (TS 102 221) for SPN/ADN/SUUME; 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 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 `0x80 | spare` for packed GSM 7-bit and `0x90` for UCS-2 — the real-card
@@ -158,8 +194,8 @@ beyond S9/S10.
8. **EF.SST is a 2-bit table** (TS 51.011 10.3.7): 4 services per byte, first 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 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 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 miswritten; the decoder is fixed in v3.34.0, and the tri-state editor
allocated/activated UI. (deleted / allocated-off / on) shipped with the DF-context work.
9. **EF.ACC class numbering** (TS 31.102 4.2.15): byte 1 b8…b1 = classes 15…8 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 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. class 0 at the MSB. Fixed in v3.34.0.
@@ -168,9 +204,11 @@ beyond S9/S10.
or absent (3-byte SIM form). The decoder and encoder follow this now. 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 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. 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 12. **The SIM/USIM language files differ in width.** EF.LP (SIM, DF.GSM,
EF.LI's 2-byte ISO 639 letters — the shared decoder is wrong for the SIM 6F05) is 1 byte per language (SMS 7-bit); EF.LI (USIM, ADF.USIM, 6F05) is
files (queued with the language work). the 2-byte ISO 639 pair; EF.PL (MF, 2F05) is 2 bytes, like LI — not 1
byte. The shared decoder is wrong for EF.LP only (queued with the
language work).
13. **EF.SMSP's parameter indicators are a five-bit presence map with inverted 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 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 means *absent*; bits 6–8 reserved = 1). The decoder had the bits shifted
@@ -190,7 +228,7 @@ beyond S9/S10.
(filterable bit grid), ACC, PHASE, HPPLMN, AD, the PLMN-only lists (filterable bit grid), ACC, PHASE, HPPLMN, AD, the PLMN-only lists
(PLMNsel/FPLMN/EHPLMN), ICCID/IMSI (behind a confirmation) and (PLMNsel/FPLMN/EHPLMN), ICCID/IMSI (behind a confirmation) and
CBMI/CBMID/CBMIR. Decoder fixes: ACC class numbering, the AD operation 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 mode / MNC-length rules, the SST 2-bit layout and the
IMSI parity nibble. IMSI parity nibble.
- **v3.35.0 (shipped)** — EF.SMSP as the first S10 dedicated builder: the full - **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 parameter form (alpha; TP-DA and TS-SCA with their own length conventions
@@ -201,8 +239,10 @@ beyond S9/S10.
- **v3.36.0** — LI/LP/PL (the SIM files need the 1-byte DCS language coding - **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 — 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 verify TS 31.101 first), SPDI (the A3 > 80 wrapper), OPL (PNN record
select), ECC (SIM and USIM — needs a structure-aware decode), SUUME, select), ECC (**two files, two editors** — DF.GSM transparent code list vs
IMPI/DOMAIN/IMPU, ACL; EXT1–EXT5 as a read-only structured view. ADF.USIM record form with an alpha; needs the DF-context resolver from
finding 6), SUUME, IMPI/DOMAIN/IMPU, ACL; EXT1–EXT5 as a read-only
structured view.
- **Postponed by decision (read-only; do not implement without confirmation)** - **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 — EF.DIR, EF.ARR, EF.SMS, EF.PBR: they rarely need manual changes and the
decoded view is enough. decoded view is enough.
@@ -222,3 +262,80 @@ beyond S9/S10.
- **No capability by accident**: a file without an encoder must never show - **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 the editor (gating test), and a schema/label drift guard keeps the editors
and decoders aligned. and decoders aligned.
## 6. Cross-file "Services" editor (proposal)
A file-agnostic service editor for the Card reader tab, in addition to the
per-file decoded editors (which stay independent). It presents the four
service tables side by side and writes them back consistently. EF.SST also
has its own per-file tri-state editor (deleted / allocated-off / on); the
Services editor's SIM column only toggles on / allocated-off and never
de-allocates.
- **Scope**: EF.SST (DF.GSM, 2 bits/service: allocated + activated), EF.UST
(ADF.USIM, 1 bit), EF.EST (ADF.USIM, a 3-bit *enabled* subset of UST:
FDN/BDN/ACL = UST 2/6/35) and EF.IST (ADF.ISIM, 1 bit, IMS services).
- **The numbering does not align across files**: SST and UST (and EST/IST)
use different service-number -> name maps, so the editor is driven by a
curated **feature cross-reference** (by name), not by service number. Each
feature row belongs to *SIM-only*, *USIM-only*, *ISIM-only* or *shared*
(e.g. FDN = SST 3 / UST 2 / EST 1; SMS = SST 4 / UST 10). The table is
spec-derived and pinned by a test, like the event/TR vectors.
- **Card-aware and probed**: on open, the editor resolves the four files on
the current card (the file-manager presence state, or a targeted probe) and
renders a column per file that exists; an absent file (a card without ISIM
has no EF.IST) hides that column and its feature rows entirely. Presence is
cached per `card_session`, like the EF.ARR copies.
- **Semantics**: "enabled" = UST bit set = SST *allocated* **and** *activated*.
SST's intermediate "allocated but not activated" state has no UST
equivalent, so the SIM column is a per-feature toggle between *on*
(allocated + activated) and *off* (allocated but not activated) — the
Services editor never de-allocates, so a card-wide change keeps the SIM
able to re-enable the service. Explicit *deletion* (clearing the allocation
bit) is the per-file EF.SST editor's job: its grid is a tri-state per
service (deleted / allocated-off / on).
- **Write, per file**: the merged form carries the intended bytes for every
visible file, but writing is explicit and separate — one button per file
("Save to EF.SST", "Save to EF.UST", "Save to EF.EST", "Save to EF.IST").
Each button writes only that file via `/api/write`; an unchanged file is
skipped and the panel reports each write's result (there is no atomic
cross-file write).
- **Filesystem check** (implemented, step 4b): many services declare a DF or EF
(ADN -> EF.ADN 6F3A, SPN -> EF.SPN 6F46, GSM Access -> DF.GSM-ACCESS 5F3B,
GPRS -> EF.KcGPRS, …); a second curated table (`SERVICES_FS_MAP`, keyed by
feature name) maps a feature to the **symbolic files** its `Filesystem` cell
declares, resolved through the standard file list's `(name, root) -> path`
index — path-exact, so the same FID under another DF (EF.PBR/EF.SAI both
4F30, EF.SST/EF.UST both 6F38) no longer explains a service. A **Check
filesystem** action probes the file-manager tree once
(`pysimFsProbeAll`) and flags mismatches — a declared service whose file is
absent, or a present file whose service is not declared in any checked tree;
a declared feature the standard list cannot resolve is reported separately as
*unchecked*. Services that map to a capability rather than a file (CHV1
disable, Proactive SIM, …) carry no file check. The declarations are read
per application (SST -> MF, UST/EST -> ADF.USIM, IST -> ADF.ISIM) and a
USIM/ISIM role may resolve into the common DF.TELECOM subtree (the MF tree),
except the GSM application's mapped copies (EF.ADN/EF.EXT1/EF.ECCP, TS 31.102
4.5). The follow-ups are the `ADF.USIM-NON-IMSI` root alias and the files
missing from the standard list (`DF.GRAPHICS`/`EF.IMG`, the DF.TELECOM
DF.SoLSA/DF.MExE, EF.3GPPPSDATAOFFservicelist) — `docs/services-editor.md`
§B.0. The last report is kept in memory (it carries its resolved paths) and
dropped with the cached reads on a card-session change.
### 6.1 Candidates for the same cross-application treatment
The cross-file pattern of the Services editor generalises to the other
SIM/USIM feature pairs (group C in §2.4), where one operator action should
update both applications consistently:
- **PLMN selectors** — EF.PLMNsel (SIM, no AcT) ↔ EF.PLMNwAcT (USIM, +AcT):
a shared PLMN list, the SIM side dropping the AcT and the USIM side
re-applying it (AcT is not expressible on the SIM side).
- **Language preferences** — EF.LP (SIM, 1 B/lang) ↔ EF.LI (USIM, 2 B/lang)
↔ EF.PL (MF, 2 B/lang): a shared ordered language list re-encoded per file.
- **Bearer capabilities** — EF.CCP (SIM) ↔ EF.CCP2 (USIM): a shared
bearer-capability record re-encoded per file's layout.
None of these are committed; they are recorded here so the DF-context resolver
and the per-file encoders are shaped with them in mind (group A/C files must
not be fid-only).
+447
View File
@@ -0,0 +1,447 @@
# Services editor — data tables (draft)
> **Status: DRAFT for human review — data only.** No code or UI has been written
> against these tables yet. They are a semantic merge of the four service lists
> from `frontend/index.html` (`EF_SST_SERVICES`, `EF_UST_SERVICES`,
> `EF_IST_SERVICES`, `EF_EST_SERVICES`) cross-referenced with the filesystem
> registry `docs/uicc/UICC_FILES.md` and the standard file tree
> `frontend/uicc_files.json`.
>
> Step 4a (the cross-file Services editor: §A's cross-reference, read, render,
> per-file save) is implemented against §A; the counts below were verified
> against the PWA's constants by `frontend/tests/services.test.js`. Step 4b
> (§B, the filesystem check) is implemented as well — see §B.0 for the
> representation and the path-exact `(name, root)` rule it uses.
**Counts:** SST **59** / UST **150** / IST **22** / EST **3** services →
**164 feature rows** (10 SIM-only, 85 USIM-only, 5 ISIM-only, 61 shared,
3 RFU/reserved). Reconciliation is exact; see Notes §4.
---
## A. Feature cross-reference
Columns: **SST #** (SIM, TS 51.011 §10.3.7, 2 bits/service: allocated + activated),
**UST #** (USIM, TS 31.102 §4.2.8, 1 bit), **IST #** (ISIM, TS 31.103 §4.2.7, 1 bit),
**EST #** (USIM Enabled Services Table, TS 31.102 §4.2.47). Class ∈
`SIM-only · USIM-only · ISIM-only · shared · RFU`. The **Filesystem** column is
deliverable B merged in (see §B for the legend and the full `capability`/`?` list).
Names are canonicalised; wording aliases are listed in Notes §1.
### A.1 Features present in the SIM list (SST), and their USIM/ISIM counterparts
| # | Feature | SST # | UST # | IST # | EST # | Class | Filesystem |
|---|---|---|---|---|---|---|---|
| 1 | CHV1 disable function | 1 | | | | SIM-only | capability |
| 2 | Local Phone Book (ADN) | 2 | 1 | | | shared | EF.ADN 6F3A (USIM: DF.PHONEBOOK 5F3A → EF.PBR 4F30) |
| 3 | Fixed Dialling Numbers (FDN) | 3 | 2 | | 1 | shared | EF.FDN 6F3B (+ EF.EXT2 6F4B) |
| 4 | Short Message Storage (SMS) | 4 | 10 | 6 | | shared | EF.SMS 6F3C |
| 5 | Advice of Charge (AoC) | 5 | 13 | | | shared | EF.ACM 6F39 · EF.ACMmax 6F37 · EF.PUCT 6F41 |
| 6 | Capability Configuration Parameters (CCP) | 6 | | | | SIM-only | EF.CCP 6F3D |
| 7 | Capability Configuration Parameters 2 (CCP2) | | 14 | | | USIM-only | EF.CCP2 6F4F |
| 8 | PLMN selector | 7 | | | | SIM-only | EF.PLMNsel 6F30 |
| 9 | MSISDN | 9 | 21 | | | shared | EF.MSISDN 6F40 |
| 10 | Extension 1 | 10 | | | | SIM-only | EF.EXT1 6F4A |
| 11 | Extension 2 | 11 | 3 | | | shared | EF.EXT2 6F4B |
| 12 | SMS Parameters (SMSP) | 12 | 12 | | | shared | EF.SMSP 6F42 |
| 13 | Last Number Dialled (LND) | 13 | | | | SIM-only | EF.LND 6F44 |
| 14 | Cell Broadcast Message Identifier (CBMI) | 14 | 15 | | | shared | EF.CBMI 6F45 |
| 15 | Group Identifier Level 1 (GID1) | 15 | 17 | | | shared | EF.GID1 6F3E |
| 16 | Group Identifier Level 2 (GID2) | 16 | 18 | | | shared | EF.GID2 6F3F |
| 17 | Service Provider Name (SPN) | 17 | 19 | | | shared | EF.SPN 6F46 |
| 18 | Service Dialling Numbers (SDN) | 18 | 4 | | | shared | EF.SDN 6F49 (+ EF.EXT3 6F4C) |
| 19 | Extension 3 | 19 | 5 | | | shared | EF.EXT3 6F4C |
| 20 | VGCS Group Identifier List | 21 | 57 | | | shared | EF.VGCS 6FB1 · EF.VGCSS 6FB2 |
| 21 | VBS Group Identifier List | 22 | 58 | | | shared | EF.VBS 6FB3 · EF.VBSS 6FB4 |
| 22 | enhanced Multi-Level Precedence and Pre-emption (eMLPP) | 23 | 24 | | | shared | EF.eMLPP 6FB5 |
| 23 | Automatic Answer for eMLPP (AAeM) | 24 | 25 | | | shared | EF.AAeM 6FB6 |
| 24 | Data download via SMS-CB | 25 | 29 | | | shared | capability (gate: EF.CBMID 6F48) |
| 25 | Data download via SMS-PP | 26 | 28 | | | shared | capability |
| 26 | Menu selection | 27 | | | | SIM-only | capability (menu title: EF.SUME 6F54) |
| 27 | Call control (by SIM/USIM) | 28 | 30 | | | shared | capability |
| 28 | Proactive SIM | 29 | | | | SIM-only | capability |
| 29 | Cell Broadcast Message Identifier Ranges (CBMIR) | 30 | 16 | | | shared | EF.CBMIR 6F50 |
| 30 | Barred Dialling Numbers (BDN) | 31 | 6 | | 2 | shared | EF.BDN 6F4D (+ EF.EXT4 6F4E/6F55) |
| 31 | Extension 4 | 32 | 7 | | | shared | EF.EXT4 6F4E (SIM) / 6F55 (USIM) |
| 32 | De-personalisation Control Keys (DCK) | 33 | 36 | | | shared | EF.DCK 6F2C |
| 33 | Co-operative Network List (CNL) | 34 | 37 | | | shared | EF.CNL 6F32 |
| 34 | Short Message Status Reports (SMSR) | 35 | 11 | 7 | | shared | EF.SMSR 6F47 |
| 35 | Network's indication of alerting in the MS (NIA) | 36 | 56 | | | shared | EF.NIA 6F51 (SIM) / 6FD3 (USIM) |
| 36 | MO-SMS control (by SIM/USIM) | 37 | 31 | | | shared | capability |
| 37 | GPRS | 38 | | | | SIM-only | EF.KcGPRS 6F52 (+ DF.GSM 7F20) |
| 38 | Image (IMG) | 39 | 22 | | | shared | DF.GRAPHICS 5F50 → EF.IMG 4F20 |
| 39 | SoLSA | 40 | 23 | | | shared | DF.SoLSA 5F70 → EF.SAI 4F30 · EF.SLL 4F31 |
| 40 | USSD string data object supported in Call Control | 41 | | | | SIM-only | capability |
| 41 | RUN AT COMMAND command | 42 | 32 | | | shared | capability |
| 42 | User controlled PLMN selector with Access Technology | 43 | 20 | | | shared | EF.PLMNwAcT 6F60 |
| 43 | Operator controlled PLMN selector with Access Technology | 44 | 42 | | | shared | EF.OPLMNwAcT 6F61 |
| 44 | HPLMN selector with Access Technology | 45 | 43 | | | shared | EF.HPLMNwAcT 6F62 |
| 45 | CPBCCH Information | 46 | 39 | | | shared | EF.CPBCCH 6F63 (SIM) / 4F63 (USIM) |
| 46 | Investigation Scan | 47 | 40 | | | shared | EF.InvScan 6F64 (SIM) / 4F64 (USIM) |
| 47 | Extended Capability Configuration Parameters (ECCP) | 48 | | | | SIM-only | EF.ECCP 6F4F |
| 48 | MExE | 49 | 41 | | | shared | DF.MExE 5F3C → EF.MExE-ST 4F40 |
| 49 | PLMN Network Name (PNN) | 51 | 45 | | | shared | EF.PNN 6FC5 |
| 50 | Operator PLMN List (OPL) | 52 | 46 | | | shared | EF.OPL 6FC6 |
| 51 | Mailbox Dialling Numbers (MBDN) | 53 | 47 | | | shared | EF.MBDN 6FC7 (+ EF.EXT6 6FC8) |
| 52 | Message Waiting Indication Status (MWIS) | 54 | 48 | | | shared | EF.MWIS 6FCA (+ EF.MBI 6FC9) |
| 53 | Call Forwarding Indication Status (CFIS) | 55 | 49 | | | shared | EF.CFIS 6FCB (+ EF.EXT7 6FCC) |
| 54 | Service Provider Display Information (SPDI) | 56 | 51 | | | shared | EF.SPDI 6FCD |
| 55 | Multimedia Messaging Service (MMS) | 57 | 52 | | | shared | EF.MMSN 6FCE (+ MMS set: EXT8/MMSICP/MMSUP/MMSUCP) |
| 56 | Extension 8 | 58 | 53 | | | shared | EF.EXT8 6FCF |
| 57 | MMS User Connectivity Parameters | 59 | 55 | | | shared | EF.MMSICP 6FD0 · EF.MMSUCP 6FD2 · EF.MMSUP 6FD1 |
### A.2 USIM-only, and USIM+ISIM shared features (no SIM entry)
| # | Feature | SST # | UST # | IST # | EST # | Class | Filesystem |
|---|---|---|---|---|---|---|---|
| 58 | Outgoing Call Information (OCI and OCT) | | 8 | | | USIM-only | EF.OCI 6F81 · EF.OCT 6F83 |
| 59 | Incoming Call Information (ICI and ICT) | | 9 | | | USIM-only | EF.ICI 6F80 · EF.ICT 6F82 |
| 60 | GSM Access | | 27 | | | USIM-only | DF.GSM-ACCESS 5F3B |
| 61 | Enabled Services Table | | 34 | | | USIM-only | EF.EST 6F56 |
| 62 | APN Control List (ACL) | | 35 | | 3 | USIM-only | EF.ACL 6F57 |
| 63 | GSM security context | | 38 | | | USIM-only | EF.Kc 4F20 · EF.KcGPRS 4F52 (DF.GSM-ACCESS 5F3B) |
| 64 | Extension 5 | | 44 | | | USIM-only | EF.EXT5 6F4E (USIM) |
| 65 | Call control on GPRS by USIM | | 54 | | | USIM-only | capability |
| 66 | Pseudonym | | 59 | | | USIM-only | EF.Pseudo 4F41 (DF.WLAN 5F40) |
| 67 | User Controlled PLMN selector for I-WLAN access | | 60 | | | USIM-only | EF.UPLMNWLAN 4F42 |
| 68 | Operator Controlled PLMN selector for I-WLAN access | | 61 | | | USIM-only | EF.OPLMNWLAN 4F43 |
| 69 | User controlled WSID list | | 62 | | | USIM-only | EF.UWSIDL 4F44 |
| 70 | Operator controlled WSID list | | 63 | | | USIM-only | EF.OWSIDL 4F45 |
| 71 | VGCS security | | 64 | | | USIM-only | EF.VGCSCA 6FD4 |
| 72 | VBS security | | 65 | | | USIM-only | EF.VBSCA 6FD5 |
| 73 | WLAN Reauthentication Identity | | 66 | | | USIM-only | EF.WRI 4F46 |
| 74 | Multimedia Messages Storage | | 67 | | | USIM-only | EF.MML 4F47 · EF.MMDF 4F48 (DF.MULTIMEDIA 5F3B) |
| 75 | Generic Bootstrapping Architecture (GBA) | | 68 | 2 | | shared | EF.GBABP 6FD6 (USIM) / 6FD5 (ISIM) · EF.GBANL 6FDA/6FD7 · EF.NAFKCA 6FDD |
| 76 | MBMS security | | 69 | | | USIM-only | EF.MSK 6FD7 · EF.MUK 6FD8 |
| 77 | Data download via USSD and USSD application mode | | 70 | | | USIM-only | capability |
| 78 | Equivalent HPLMN | | 71 | | | USIM-only | EF.EHPLMN 6FD9 |
| 79 | Additional TERMINAL PROFILE after UICC activation | | 72 | | | USIM-only | capability |
| 80 | Equivalent HPLMN Presentation Indication | | 73 | | | USIM-only | EF.EHPLMNPI 6FDB |
| 81 | Last RPLMN Selection Indication | | 74 | | | USIM-only | EF.LRPLMNSI 6FDC |
| 82 | OMA BCAST Smart Card Profile | | 75 | | | USIM-only | DF.BCAST 5F80 |
| 83 | GBA-based Local Key Establishment Mechanism | | 76 | 4 | | shared | capability |
| 84 | Terminal Applications | | 77 | | | USIM-only | capability |
| 85 | Service Provider Name Icon | | 78 | | | USIM-only | EF.SPNI 6FDE |
| 86 | PLMN Network Name Icon | | 79 | | | USIM-only | EF.PNNI 6FDF |
| 87 | Connectivity Parameters for USIM IP connections | | 80 | | | USIM-only | EF.NCP-IP 6FE2 |
| 88 | Home I-WLAN Specific Identifier List | | 81 | | | USIM-only | EF.HWSIDL 4F47 |
| 89 | I-WLAN Equivalent HPLMN Presentation Indication | | 82 | | | USIM-only | EF.WEHPLMNPI 4F48 |
| 90 | I-WLAN HPLMN Priority Indication | | 83 | | | USIM-only | EF.WHPI 4F49 |
| 91 | I-WLAN Last Registered PLMN | | 84 | | | USIM-only | EF.WLRPLMN 4F4A |
| 92 | EPS Mobility Management Information | | 85 | | | USIM-only | EF.EPSLOCI 6FE3 · EF.EPSNSC 6FE4 |
| 93 | Allowed CSG Lists and corresponding indications | | 86 | | | USIM-only | EF.ACSGL 4F81 · EF.CSGT 4F82 · EF.HNBN 4F83 |
| 94 | Call control on EPS PDN connection by USIM | | 87 | | | USIM-only | capability |
| 95 | HPLMN Direct Access | | 88 | | | USIM-only | EF.HPLMNDAI 4F4B |
| 96 | eCall Data | | 89 | | | USIM-only | EF.SDN 6F49 (+ EF.FDN 6F3B) |
| 97 | Operator CSG Lists and corresponding indications | | 90 | | | USIM-only | EF.OCSGL 4F84 · EF.OCSGT 4F85 · EF.OHNBN 4F86 |
| 98 | Support for SM-over-IP | | 91 | 8 | | shared | capability (ISIM-related: EF.PSISMSC 6FE5) |
| 99 | Support of CSG Display Control | | 92 | | | USIM-only | capability |
| 100 | Communication Control for IMS | | 93 | 9 | | shared | capability |
| 101 | Extended Terminal Applications | | 94 | | | USIM-only | capability |
| 102 | Support of UICC access to IMS | | 95 | 10 | | shared | capability |
| 103 | Non-Access Stratum configuration by USIM | | 96 | | | USIM-only | EF.NASCONFIG 6FE8 |
| 104 | PWS configuration by USIM | | 97 | | | USIM-only | EF.PWS 6FEC |
| 105 | URI support by UICC | | 99 | 11 | | shared | capability (URI files: EF.FDNURI 6FED · EF.BDNURI 6FEE · EF.SDNURI 6FEF) |
| 106 | Extended EARFCN support | | 100 | | | USIM-only | capability |
| 107 | ProSe | | 101 | | | USIM-only | DF.ProSe 5F90 → EF.PST 4F10 |
| 108 | USAT Application Pairing | | 102 | | | USIM-only | EF.IPS 6FF1 (+ EF.IAL 6FF0 · EF.IPD 6FF2) |
| 109 | Media Type support | | 103 | 12 | | shared | capability |
| 110 | IMS call disconnection cause | | 104 | 13 | | shared | capability |
| 111 | URI support for MO SHORT MESSAGE CONTROL | | 105 | 14 | | shared | capability |
| 112 | ePDG configuration Information support | | 106 | | | USIM-only | EF.ePDGSelection 6FF4 |
| 113 | ePDG configuration Information configured | | 107 | | | USIM-only | EF.ePDGId 6FF3 |
| 114 | ACDC support | | 108 | | | USIM-only | DF.ACDC 5FA0 → EF.ACDC_LIST 4F01 |
| 115 | MCPTT | | 109 | 15 | | shared | DF.MCS 5F3D → EF.MST 4F01 (USIM) |
| 116 | ePDG configuration Information for Emergency Service support | | 110 | | | USIM-only | EF.ePDGSelectionEm 6FF6 |
| 117 | ePDG configuration Information for Emergency Service configured | | 111 | | | USIM-only | EF.ePDGIdEm 6FF5 |
| 118 | eCall Data over IMS | | 112 | | | USIM-only | EF.SDNURI 6FEF (+ EF.FDNURI 6FED) |
| 119 | URI support for SMS-PP DOWNLOAD | | 113 | 16 | | shared | capability |
| 120 | From Preferred | | 114 | 17 | | shared | EF.FromPreferred 6FF7 |
| 121 | IMS configuration data | | 115 | 18 | | shared | EF.IMSConfigData 6FF8 |
| 122 | TV configuration | | 116 | | | USIM-only | EF.TVCONFIG 6FFB (+ DF.TV 5FB0 → EF.TVUSD) |
| 123 | 3GPP PS Data Off | | 117 | | | USIM-only | EF.3GPPPSDATAOFF 6FF9 |
| 124 | 3GPP PS Data Off Service List | | 118 | | | USIM-only | EF.3GPPPSDATAOFFservicelist 6FFA |
| 125 | V2X | | 119 | | | USIM-only | DF.V2X 5F3E → EF.VST 4F01 · EF.V2X_CONFIG 4F02 |
| 126 | XCAP Configuration Data | | 120 | 19 | | shared | EF.XCAPConfigData 6FFC |
| 127 | EARFCN list for MTC/NB-IOT UEs | | 121 | | | USIM-only | EF.EARFCNList 6FFD |
| 128 | 5GS Mobility Management Information | | 122 | | | USIM-only | DF.5GS 5FC0 → EF.5GS3GPPLOCI 4F01 · EF.5GSN3GPPLOCI 4F02 |
| 129 | 5G Security Parameters | | 123 | | | USIM-only | EF.5GAUTHKEYS 4F05 (+ EF.5GS3GPPNSC 4F03 · EF.5GSN3GPPNSC 4F04) |
| 130 | Subscription identifier privacy support | | 124 | | | USIM-only | capability (SUCI data: EF.SUCI_Calc_Info 4F07, gated by 125) |
| 131 | SUCI calculation by the USIM | | 125 | | | USIM-only | EF.SUCI_Calc_Info 4F07 |
| 132 | UAC Access Identities support | | 126 | | | USIM-only | EF.UAC_AIC 4F06 |
| 133 | Expect control plane-based Steering of Roaming information during initial registration in VPLMN | | 127 | | | USIM-only | capability (SOR data: EF.SOR-CMCI 4F0E, gated by 138) |
| 134 | Call control on PDU Session by USIM | | 128 | | | USIM-only | capability |
| 135 | 5GS Operator PLMN List | | 129 | | | USIM-only | EF.OPL5G 4F08 |
| 136 | Support for SUPI of type NSI or GLI or GCI | | 130 | | | USIM-only | EF.SUPI_NAI 4F09 |
| 137 | 3GPP PS Data Off separate Home and Roaming lists | | 131 | | | USIM-only | capability (extends EF.3GPPPSDATAOFFservicelist 6FFA) |
| 138 | Support for URSP by USIM | | 132 | | | USIM-only | EF.URSP 4F0B |
| 139 | 5G Security Parameters extended | | 133 | | | USIM-only | capability (extends EF.5GAUTHKEYS 4F05) |
| 140 | MuD and MiD configuration data | | 134 | 21 | | shared | EF.MuDMiDConfigData 6FFE |
| 141 | Support for Trusted non-3GPP access networks by USIM | | 135 | | | USIM-only | capability |
| 142 | Support for multiple records of NAS security context storage for multiple registration | | 136 | | | USIM-only | EF.5GS3GPPNSC 4F03 · EF.5GSN3GPPNSC 4F04 (2nd record) |
| 143 | Pre-configured CAG information list | | 137 | | | USIM-only | EF.CAG 4F0D |
| 144 | SOR-CMCI storage in USIM | | 138 | | | USIM-only | EF.SOR-CMCI 4F0E |
| 145 | 5G ProSe | | 139 | | | USIM-only | DF.5G_ProSe 5FF0 → EF.5G_PROSE_ST 4F01 |
| 146 | Storage of disaster roaming information in USIM | | 140 | | | USIM-only | EF.DRI 4F0F |
| 147 | Pre-configured eDRX parameters | | 141 | | | USIM-only | EF.5GSEDRX 4F10 |
| 148 | 5G NSWO support | | 142 | | | USIM-only | EF.5GNSWO_CONF 4F11 |
| 149 | PWS configuration for SNPN in USIM | | 143 | | | USIM-only | EF.PWS_SNPN 4F01 (DF.SNPN 5FE0) |
| 150 | Multiplier Coefficient for Higher Priority PLMN search via NG-RAN satellite access | | 144 | | | USIM-only | EF.MCHPPLMN 4F15 |
| 151 | K_AUSF derivation configuration | | 145 | | | USIM-only | EF.KAUSF_DERIVATION 4F16 |
| 152 | Network Identifier for SNPN (NID) | | 146 | | | USIM-only | EF.NID 4F02 (DF.SNPN 5FE0) |
| 153 | 5MBS UE pre-configuration | | 147 | | | USIM-only | DF.5MBSUECONFIG 5FF1 → EF.5MBSUECONFIG 4F01 |
| 154 | Operator Controlled Signal Threshold per Access Technology | | 148 | | | USIM-only | EF.OCST 6F02 |
| 155 | A2X | | 149 | | | USIM-only | DF.A2X 5F3F → EF.AST 4F01 |
| 156 | IMS Data Channel Indication | | 150 | | | USIM-only | EF.IMSDCI 6F0B |
### A.3 ISIM-only features
| # | Feature | SST # | UST # | IST # | EST # | Class | Filesystem |
|---|---|---|---|---|---|---|---|
| 153 | P-CSCF address | | | 1 | | ISIM-only | EF.P-CSCF 6F09 |
| 154 | HTTP Digest | | | 3 | | ISIM-only | capability |
| 155 | Support of P-CSCF discovery for IMS Local Break Out | | | 5 | | ISIM-only | capability (address store: EF.P-CSCF 6F09) |
| 156 | WebRTC URI | | | 20 | | ISIM-only | EF.WebRTCURI 6FFA |
| 157 | IMS Data Channel indication | | | 22 | | ISIM-only | EF.IMSDCI 6F0B |
### A.4 RFU / reserved (non-editable)
| # | Feature | SST # | UST # | IST # | EST # | Class | Filesystem |
|---|---|---|---|---|---|---|---|
| 158 | RFU | 8, 20 | 26, 98 | | | RFU | — |
| 159 | Reserved and shall be ignored | 50 | 50 | | | RFU | — |
| 160 | shall be set to 1 | | 33 | | | RFU | — |
---
## B. Feature → filesystem (FS check)
The FS column in §A is deliverable B, merged for readability. Legend:
- `EF.<name> <FID>` — the file the service declares (FID from `uicc_files.json`
or `UICC_FILES.md`; both agree unless noted).
- `DF.<name> <FID> → EF.<name> <FID>` — the service gates a whole DF; the
principal EF is shown. Companion files after `(+ …)` or `·`.
- `capability` — a procedural / terminal-side feature with no single file.
- `?` — no clear file found in either source; needs a human decision.
- `—` — RFU/reserved.
### B.0 Implemented check (`SERVICES_FS_MAP` + the Check filesystem action)
`frontend/index.html` carries the column as `SERVICES_FS_MAP`, a map keyed by
the §A feature name (the cross-reference's row identity) whose value is the
list of **symbolic file names the cell mentions**, in the cell's order:
- `EF.FDN 6F3B (+ EF.EXT2 6F4B)` → `['EF.FDN', 'EF.EXT2']`
- `DF.GRAPHICS 5F50 → EF.IMG 4F20` → `['DF.GRAPHICS', 'EF.IMG']` (a DF's own
name counts: the DF is the file the service gates)
- `EF.NIA 6F51 (SIM) / 6FD3 (USIM)` → `['EF.NIA']` — one name, a different
path per tree
- `capability` → `[]`; the parenthetical references a capability cell carries
(`gate: EF.CBMID 6F48`, `menu title: EF.SUME 6F54`, the URI set, `SUCI data`)
are informational — the service does not declare a file of its own. The
§A.4 RFU/reserved rows are not features and therefore have no entry.
The names are the standard file list's (`frontend/uicc_files.json`, built from
pySim by `pysim_simple_server/uicc_files.py`) `name` values, so the check can
resolve them **path-exactly**: a FID alone is ambiguous across applications
(`4F30` is EF.PBR *and* EF.SAI, `6F38` EF.SST *and* EF.UST, `6F4E` EF.EXT4
under DF.TELECOM *and* EF.EXT5 under ADF.USIM). `servicesPathIndexBuild()`
turns the list into a `NAME|ROOT → path` index (uppercased both sides; the one
duplicate — pySim models `EF.SUCI_Calc_Info` twice under ADF.USIM, in DF.5GS
`4F07` and in the eSIM profile package's DF.SAIP `4F01` — takes the list's
first entry, the DF.5GS one). `servicesPathIndex()` loads it once per page
through `uiccStdFilesLoad()`.
The **Check filesystem** button (Services pill, needs a card) reads the four
service tables, probes the file manager's tree once (`pysimFsProbeAll`; the
`/api/tree` model lists files without testing them, so a loaded-but-unprobed
tree proves nothing), collects the tree's **present paths** (every node with
`exists === true`, DFs included, as canonical `pysimFsNodePath` paths) and
compares the two sides:
- **declared per tree** — `servicesFsDeclaredTrees()`: EF.SST declares in the
**MF** tree, EF.UST/EF.EST in **ADF.USIM**, EF.IST in **ADF.ISIM** (the state
the editor shows: EF.SST only at *allocated + activated*). A tree whose
service table is not on the card declares nothing there (its declarations
are unknown, so it is not checked at all).
- `missing` — a declared feature whose resolved paths are absent, in §A order
then MF / ADF.USIM / ADF.ISIM; each entry carries the declaring `tree` and
the absent `paths` (in the cell's order).
- `extra` — a present path whose mapped features are declared in **none** of
the checked trees, sorted by path, with the path's own `tree`, its `role`
name and every mapping feature. A path **any** declared feature maps is
explained: several features share one file (DF.GSM-ACCESS across "GSM
Access"/"GSM security context", EXT2 across FDN/Extension 2, the 5G DFs'
`4F01`/`4F02`) and a declaration in one application may gate a file of
another (a UST service gating a DF.TELECOM DF — see below).
- `unchecked` — a declared feature with **no** path at all: the standard list
is missing the file, so its presence cannot be verified. Today:
`Image (IMG)` (`DF.GRAPHICS`/`EF.IMG` — pySim does not model DF.GRAPHICS),
`SoLSA` and `MExE` in the MF tree (the DF.TELECOM copies of DF.SoLSA/DF.MExE
are not in the list) and `3GPP PS Data Off Service List`
(EF.3GPPPSDATAOFFservicelist). Reported instead of guessed — the follow-up
is to add those files to `SUPPLEMENT` in `pysim_simple_server/uicc_files.py`.
**Resolution per declaring tree.** A role resolves in the declaring tree's
root first, then — for a USIM/ISIM declaration — in the **MF** tree, because
several UST-gated DFs are children of the common DF.TELECOM (TS 31.102 4.6.0:
DF.MULTIMEDIA `5F3B`, DF.MCS `5F3D`, DF.V2X `5F3E`, DF.A2X `5F3F`,
DF.GRAPHICS `5F50`), i.e. `MF/7F10/…` paths. The exceptions
(`SERVICES_FS_GSM_MAPPED`) are the files TS 31.102 4.5 lists as *mappings* of a
DF.PHONEBOOK file into DF.TELECOM "in case of a present GSM application" —
EF.ADN `6F3A`, EF.EXT1 `6F4A`, EF.ECCP `6F4F`: a USIM declaration's phonebook
is DF.PHONEBOOK + EF.PBR (ADR's `EF.ADN` is the SIM's file), so those are
never expected in a USIM/ISIM tree. A role the list knows for another root
only is skipped, not a failure — a feature's files are the union of its
applications' files (ADN = EF.ADN for the SIM, DF.PHONEBOOK + EF.PBR for the
USIM).
**Not covered (follow-ups):** `ADF.USIM-NON-IMSI` — the list mirrors the
ADF.USIM files under that root, but a card's tree names the application
`ADF.USIM-non-IMSI`, so a non-IMSI USIM card's UST/EST paths are not matched
yet (resolve it as an alias of ADF.USIM); and the registry gaps listed above.
The pure pieces (`servicesFsDeclaredTrees`, `servicesFsPresentPaths`,
`servicesPathIndexBuild`, `servicesFsRolePath`, `servicesFsReport`,
`servicesFsReportHtml`) are DOM/fetch-free and unit-tested; the last report is
kept in memory (it carries its resolved paths) and redrawn on a language
switch, and it is dropped with the cached reads on a card-session change.
### B.1 `capability` entries (no single file)
CHV1 disable · Data download via SMS-PP · Data download via SMS-CB (gate
EF.CBMID 6F48) · Menu selection (title EF.SUME 6F54) · Call control · Proactive
SIM · MO-SMS control · USSD string in Call Control · RUN AT COMMAND · Call
control on GPRS · Data download via USSD · Additional TERMINAL PROFILE ·
GBA-based Local Key Establishment · Terminal Applications · SM-over-IP (related
EF.PSISMSC 6FE5, ISIM) · CSG Display Control · Communication Control for IMS ·
Extended Terminal Applications · UICC access to IMS · URI support by UICC (URI
files EF.FDNURI/BDNURI/SDNURI) · Media Type support · IMS call disconnection
cause · URI for MO SHORT MESSAGE CONTROL · URI for SMS-PP DOWNLOAD · Call
control on EPS PDN · Call control on PDU Session · Subscription identifier
privacy (SUCI data in EF.SUCI_Calc_Info 4F07) · SOR expectation (data in
EF.SOR-CMCI 4F0E) · PS Data Off separate Home/Roaming (EF.3GPPPSDATAOFFservicelist
6FFA) · 5G Security Parameters extended (EF.5GAUTHKEYS 4F05) · Trusted non-3GPP
access · HTTP Digest · P-CSCF discovery Local Break Out (EF.P-CSCF 6F09).
### B.2 Resolved against TS 31.102 v18.10.0
The three initially-unresolved mappings were settled from the spec text
(`docs/uicc/ts_131102*.txt`):
- **eCall Data (89)** — no dedicated EF: the eCall test/reconfiguration
numbers live in **EF.SDN 6F49** ("If service n° 4 and/or n° 89 is available
this file shall be present … eCall test and reconfiguration numbers") and
EF.FDN 6F3B (the eCall procedures).
- **eCall Data over IMS (112)** — the eCall test/reconfiguration **URIs** live
in **EF.SDNURI 6FEF** (and EF.FDNURI 6FED).
- **EARFCN list for MTC/NB-IOT UEs (121)** — **EF.EARFCNList 6FFD** (§4.2.112,
whose clause title is literally "EARFCN list for MTC/NB-IOT UEs").
"Extended EARFCN support" (100) is a **capability** with no file.
---
## Notes / open questions
### 1. Aliases used (case-insensitive exact name did not join these; merged manually)
| Canonical | SST wording | UST wording | IST wording |
|---|---|---|---|
| Local Phone Book (ADN) | "Abbreviated Dialling Numbers (ADN)" (2) | "Local Phone Book" (1) | |
| SMS Parameters (SMSP) | "SMS Parameters" (12) | "Short Message Service Parameters (SMSP)" (12) | |
| Short Message Status Reports (SMSR) | "Short Message Status Reports" (35) | "…Status Reports (SMSR)" (11) | "…Status Reports (SMSR)" (7) |
| Network's indication of alerting in the MS (NIA) | "Network's indication of alerting in the MS" (36) | "…in the MS (NIA)" (56) | |
| Call control | "Call control" (28) | "Call Control by USIM" (30) | |
| MO-SMS control | "Mobile Originated Short Message control by SIM" (37) | "MO-SMS Control by USIM" (31) | |
| De-personalisation Control Keys (DCK) | "De-personalization Control Keys" (33) | "Depersonalisation Control Keys" (36) | |
| SoLSA | "SoLSA (Support of Local Service Area)" (40) | "Support of Localised Service Areas (SoLSA)" (23) | |
| eMLPP / MExE | "enhanced Multi-Level…" (23), "MExE" (49) | "Enhanced Multi-Level…" (24), "MexE" (41) | |
| PLMN selector w/ Access Technology (×3) | "…PLMN Selector with Access Technology" (43–45) | "…PLMN selector with Access Technology" (20/42/43) | |
| Extension 2 | "Extension2" (11) | "Extension 2" (3) | |
| SM-over-IP | | "Support for SM-over-IP" (91) | "Support for SM-over-IP including data download via SMS-PP as defined in TS 31.111 [31]" (8) |
| Communication Control for IMS | | "…by USIM" (93) | "…by ISIM" (9) |
| URI support for SMS-PP DOWNLOAD | | "…TS 31.111 [12]" (113) | "…TS 31.111 [31]" (16) |
The other shared rows joined on a case-insensitive exact name (e.g. "Service
Provider Name" SST 17 / UST 19, "MSISDN" SST 9 / UST 21, "Short Message Storage
(SMS)" SST 4 / UST 10 / IST 6).
### 2. Kept separate, flagged for review
- **CCP / CCP2 / ECCP.** SST carries two bearer-capability services — "Capability
Configuration Parameters (CCP)" (6, file EF.CCP 6F3D) and "Extended Capability
Configuration Parameters (ECCP)" (48, EF.ECCP 6F4F); UST carries one —
"Capability Configuration Parameters 2 (CCP2)" (14, EF.CCP2 6F4F). These were
kept as three rows. Note the structural mismatch: the USIM's CCP2 shares FID
**6F4F** with the SIM's **ECCP**, not with the SIM's CCP (6F3D), so a
CCP↔CCP2 merge is *not* obviously correct — decide whether CCP2 ≙ CCP or
CCP2 ≙ ECCP (or neither) before writing code.
### 3. FID / tree collisions to respect in the editor
- `6F4E` = EF.EXT4 in the GSM tree, EF.EXT5 in the USIM tree (UICC_FILES.md §2.3).
- `6F4F` = EF.ECCP (GSM) vs EF.CCP2 (USIM) — see §2.
- `6F07` = EF.IMSI (DF.GSM / ADF.USIM) vs EF.IST (ADF.ISIM).
- `6F38` = EF.SST (DF.GSM) vs EF.UST (ADF.USIM).
- `6FD7` = EF.MSK (USIM) vs EF.GBANL (ISIM).
- `EF.NIA` has different FIDs per tree: 6F51 (SIM) vs 6FD3 (USIM).
- `DF.GSM-ACCESS 5F3B` (ADF.USIM) vs `DF.MULTIMEDIA 5F3B` (DF.TELECOM) — same
FID, different parents; the child-DF summary (UICC_FILES.md §4.13) is the
authority for which DF a service gates.
The filesystem column already resolves these per-tree (e.g. EF.EXT4 shows both
FIDs); the editor's DF-context selection must do the same.
### 4. Reconciliation (completeness invariant)
- **SST 59:** 56 non-RFU service numbers (SST 1–59 minus {8,20,50}) spread over
the 57 rows of §A.1 (one of those rows, CCP2, carries no SST number) + 3
RFU/reserved slots (8, 20, 50) = 59. ✓
- **UST 150:** 46 shared-with-SIM + 15 shared-with-ISIM + 85 USIM-only (1 in
§A.1 = CCP2, 84 in §A.2) + 4 RFU/reserved (26, 33, 50, 98) = 150. ✓
- **IST 22:** 2 shared with SIM (SMS 6, SMSR 7) + 15 shared with USIM
(2,4,8,9,10,11,12,13,14,15,16,17,18,19,21) + 5 ISIM-only (1,3,5,20,22) = 22. ✓
- **EST 3:** FDN (1), BDN (2), ACL (3) — all present. ✓
The PWA's `EF_UST_SERVICES` constant previously stopped at **146**; it now
carries **147–150** (5MBS UE pre-configuration / Operator Controlled Signal
Threshold per Access Technology / A2X / IMS Data Channel Indication), matching
`UICC_FILES.md` §6.1 and TS 31.102 §4.2.8, so the tables above are complete.
### 5. Source-coverage gaps (the standard file list)
The Services editor is **card-only**: it probes the equipped card's service
tables by path and writes via `/api/write`. The **FS check** additionally
needs the *cardless* standard file list (`frontend/uicc_files.json`) to resolve
a feature's symbolic files to paths (`(name, root) -> path`, §B.0) — the card's
own tree supplies presence, the list supplies the paths. Part 1 of step 4b
added the service-gated files pySim's model omits to `SUPPLEMENT`
(`pysim_simple_server/uicc_files.py`): EF.LRPLMNSI, EF.IAL, EF.IPD,
EF.TVCONFIG, EF.3GPPPSDATAOFF, DF.MExE + its files, DF.SoLSA + EF.SAI/EF.SLL,
DF.BCAST, DF.ACDC + EF.ACDC_LIST, DF.TV, DF.A2X + EF.AST/EF.A2X_CONFIG.
Still missing from the list, and therefore reported as *unchecked* by the
check (a declared feature whose files have no path) rather than guessed:
- **DF.GRAPHICS 5F50 → EF.IMG 4F20** — the DF.TELECOM child (TS 31.102 4.6.0);
pySim's SIM model does not carry DF.GRAPHICS at all.
- **DF.SoLSA 5F70 / DF.MExE 5F3C under DF.TELECOM** — the list carries them
under ADF.USIM only, so an *SST* declaration of SoLSA (service 40) or MExE
(49) cannot be checked in the MF tree.
- **EF.3GPPPSDATAOFFservicelist 6FFA** — a pySim TODO.
A non-IMSI USIM (`ADF.USIM-NON-IMSI`) is mirrored in the list, but a card's
tree names the application `ADF.USIM-non-IMSI`; resolving the two as one root
is the follow-up (§B.0).
### 6. Deliberately unmapped
- "GSM Access" (UST 27) maps to **DF.GSM-ACCESS 5F3B** (UICC_FILES.md §4.13c
"DF_GSM-ACCESS — UST n°27"), not to DF.GSM 7F20 — DF.GSM is the classic SIM
tree and is not UST-gated. This corrects the DF.GSM hint in the drafting brief.
- EF.RPLMNAcT 6F65 (legacy, "deleted in Rel-6") has no current UST/SST service
and is intentionally absent.
- EF.ACC 6F78 (access-control class) and EF.Phase 6FAE are not service-gated in
either list, so they do not appear.
+1 -1
View File
@@ -409,7 +409,7 @@
<ul class="list-disc list-inside text-sm space-y-1 mb-3"> <ul class="list-disc list-inside text-sm space-y-1 mb-3">
<li><strong>Читать сырые данные / Декодировать</strong> — чтение выбранного файла в виде hex-дампа или таблицы декодированных полей (клиентские декодеры EF: IMSI, ICCID, SPN, списки PLMN, LOCI/PSLOCI/EPSLOCI, ADN/MSISDN, таблицы сервисов, SUME, …); таблица декодируется в браузере из тех же сырых байт (без серверного декодирования). Подсвеченная кнопка — текущий вид; нажатие любой из них (пере)читывает файл. «Декодировать» недоступна, если для файла нет клиентского декодера (проверка по имени/FID до чтения).</li> <li><strong>Читать сырые данные / Декодировать</strong> — чтение выбранного файла в виде hex-дампа или таблицы декодированных полей (клиентские декодеры EF: IMSI, ICCID, SPN, списки PLMN, LOCI/PSLOCI/EPSLOCI, ADN/MSISDN, таблицы сервисов, SUME, …); таблица декодируется в браузере из тех же сырых байт (без серверного декодирования). Подсвеченная кнопка — текущий вид; нажатие любой из них (пере)читывает файл. «Декодировать» недоступна, если для файла нет клиентского декодера (проверка по имени/FID до чтения).</li>
<li><strong>Редактировать сырые данные</strong> — изменение hex-данных, <strong>Сохранить</strong> для записи (или <strong>Отмена</strong>); transparent-файлы редактируются в textarea, record-файлы — по одному полю на запись с флажками выбора. «Редактировать сырые данные» сначала переключает на hex-вид и при необходимости читает файл.</li> <li><strong>Редактировать сырые данные</strong> — изменение hex-данных, <strong>Сохранить</strong> для записи (или <strong>Отмена</strong>); transparent-файлы редактируются в textarea, record-файлы — по одному полю на запись с флажками выбора. «Редактировать сырые данные» сначала переключает на hex-вид и при необходимости читает файл.</li>
<li><strong>Редактировать декодированные</strong> — редактирование файла в его собственной декодированной форме; кодирование выполняется в браузере, запись идёт обычным путём обновления. Кодировщики покрывают <strong>EF.SPN</strong>, <strong>EF.PNN</strong>, списки PLMN (<strong>PLMNsel / FPLMN / EHPLMN</strong> и списки с технологиями доступа <strong>PLMNwAcT / OPLMNwAcT / HPLMNwAcT</strong>), файлы номеров (<strong>ADN / FDN / SDN / MSISDN / LND / BDN</strong>), таблицы сервисов (<strong>UST / IST / EST</strong>; <strong>SST</strong> остаётся только для чтения до появления редактора allocated/activated), <strong>ACC</strong>, <strong>PHASE</strong>, <strong>HPPLMN</strong>, <strong>AD</strong>, <strong>ICCID / IMSI</strong> (с подтверждением — запись меняет идентичность карты) списки cell broadcast (<strong>CBMI / CBMID / CBMIR</strong>) и <strong>EF.SMSP</strong> (полная форма SMS-параметров: alpha, TP-DA и TS-SCA с их правилами адресации и TON, TP-PID, TP-DCS и относительный TP-VP; у каждого параметра — флажок <em>«отсутствует (действует значение ME по умолчанию)»</em> и живые подсказки по DCS и сроку действия). Для transparent-файла — одна форма (поля SPN, строки PLMN с добавлением/удалением, элементы PNN с добавлением/удалением); для record-файла — по форме на запись с флажком выбора, который ставится автоматически при изменении поля, поэтому записываются только изменённые записи. <strong>Сохранить</strong> проверяет все выбранные записи до записи; для файлов без кодировщика кнопка недоступна с подсказкой. Указатели расширений (EF.EXT1–EF.EXT5) и байт метода сравнения BDN сохраняются; редактирование номера, продолжение которого лежит в файле расширения, отклоняется.</li> <li><strong>Редактировать декодированные</strong> — редактирование файла в его собственной декодированной форме; кодирование выполняется в браузере, запись идёт обычным путём обновления. Кодировщики покрывают <strong>EF.SPN</strong>, <strong>EF.PNN</strong>, списки PLMN (<strong>PLMNsel / FPLMN / EHPLMN</strong> и списки с технологиями доступа <strong>PLMNwAcT / OPLMNwAcT / HPLMNwAcT</strong>), файлы номеров (<strong>ADN / FDN / SDN / MSISDN / LND / BDN</strong>), таблицы сервисов (<strong>UST / IST / EST</strong> и <strong>SST</strong> — SIM-таблица, редактируется по каждой службе как удалена / выделена-выключена / включена), <strong>ACC</strong>, <strong>PHASE</strong>, <strong>HPPLMN</strong>, <strong>AD</strong>, <strong>ICCID / IMSI</strong> (с подтверждением — запись меняет идентичность карты) списки cell broadcast (<strong>CBMI / CBMID / CBMIR</strong>) и <strong>EF.SMSP</strong> (полная форма SMS-параметров: alpha, TP-DA и TS-SCA с их правилами адресации и TON, TP-PID, TP-DCS и относительный TP-VP; у каждого параметра — флажок <em>«отсутствует (действует значение ME по умолчанию)»</em> и живые подсказки по DCS и сроку действия). Для transparent-файла — одна форма (поля SPN, строки PLMN с добавлением/удалением, элементы PNN с добавлением/удалением); для record-файла — по форме на запись с флажком выбора, который ставится автоматически при изменении поля, поэтому записываются только изменённые записи. <strong>Сохранить</strong> проверяет все выбранные записи до записи; для файлов без кодировщика кнопка недоступна с подсказкой. Указатели расширений (EF.EXT1–EF.EXT5) и байт метода сравнения BDN сохраняются; редактирование номера, продолжение которого лежит в файле расширения, отклоняется.</li>
<li><strong>Проверить все файлы</strong> — обход всего дерева (включая пользовательские файлы) с пометкой каждого элемента: есть (обычный вид) или нет (красный ✗, без стрелки разворачивания); существующие пустые DF показывают <code class="font-mono text-sm">(пусто)</code>. Отображается прогресс <em>N / всего</em>, обход можно остановить; в конце — сводка «есть/нет». Файлы проверяются только при разворачивании или проверке — просмотр остаётся ленивым.</li> <li><strong>Проверить все файлы</strong> — обход всего дерева (включая пользовательские файлы) с пометкой каждого элемента: есть (обычный вид) или нет (красный ✗, без стрелки разворачивания); существующие пустые DF показывают <code class="font-mono text-sm">(пусто)</code>. Отображается прогресс <em>N / всего</em>, обход можно остановить; в конце — сводка «есть/нет». Файлы проверяются только при разворачивании или проверке — просмотр остаётся ленивым.</li>
<li><strong>ADM</strong> — файлы, требующие администраторский PIN, возвращают <code class="font-mono text-sm">6982</code>/<code class="font-mono text-sm">9804</code>; если в подходящем пресете карты (тот же ICCID) есть ключ ADM, рядом с ошибкой появляется кнопка <strong>«Проверить ADM»</strong>, а значок в заголовке (<code class="font-mono text-sm">ADM ✓/✗ ⚿</code>) становится кликабельным. Каждый неверный ключ расходует попытку: остаток показывается, а повторная попытка требует подтверждения. Заблокированный ADM здесь не восстановить — нужен ключ разблокировки карты.</li> <li><strong>ADM</strong> — файлы, требующие администраторский PIN, возвращают <code class="font-mono text-sm">6982</code>/<code class="font-mono text-sm">9804</code>; если в подходящем пресете карты (тот же ICCID) есть ключ ADM, рядом с ошибкой появляется кнопка <strong>«Проверить ADM»</strong>, а значок в заголовке (<code class="font-mono text-sm">ADM ✓/✗ ⚿</code>) становится кликабельным. Каждый неверный ключ расходует попытку: остаток показывается, а повторная попытка требует подтверждения. Заблокированный ADM здесь не восстановить — нужен ключ разблокировки карты.</li>
</ul> </ul>
+1 -1
View File
@@ -408,7 +408,7 @@
<ul class="list-disc list-inside text-sm space-y-1 mb-3"> <ul class="list-disc list-inside text-sm space-y-1 mb-3">
<li><strong>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>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 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>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> and <strong>SST</strong> — the SIM table, edited per service as deleted / allocated-off / on), <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>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> <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> </ul>
+1759 -90
View File
File diff suppressed because it is too large. Load diff
+1 -1
View File
@@ -1,4 +1,4 @@
const CACHE = 'simple-v482'; const CACHE = 'simple-v487';
const URLS = [ const URLS = [
'index.html', 'index.html',
'help.html', 'help.html',
+159 -5
View File
@@ -26,13 +26,13 @@ const FNS = [
'efGsm7Octets', 'efAnnexA', 'efBcdAddress', 'efBcdAddressLen', 'efPlmn', 'efPlmnAct', 'efPnnText', 'efGsm7Octets', 'efAnnexA', 'efBcdAddress', 'efBcdAddressLen', 'efPlmn', 'efPlmnAct', 'efPnnText',
'efDecIccid', 'efDecImsi', 'efDecLi', 'efDecServiceTable', 'efDecSst', 'efDecPlmnList', 'efDecFplmn', 'efDecPlmnWact', 'efDecIccid', 'efDecImsi', 'efDecLi', 'efDecServiceTable', 'efDecSst', 'efDecPlmnList', 'efDecFplmn', 'efDecPlmnWact',
'efDecSpn', 'efDecLoci', 'efDecPsLoci', 'efDecEpsLoci', 'efDecEpsNsc', 'efDecKc', 'efDecAcc', 'efDecSpn', 'efDecLoci', 'efDecPsLoci', 'efDecEpsLoci', 'efDecEpsNsc', 'efDecKc', 'efDecAcc',
'efDecPhase', 'efDecCbmi', 'efDecCbmir', 'efDecHpplmn', 'efDecEcc', 'efDecOpl', 'efDecAd', 'efDecAcl', 'efDecPhase', 'efDecCbmi', 'efDecCbmir', 'efDecHpplmn', 'efDecEcc', 'efDecEccUsim', 'efEccCategory', 'efDecOpl', 'efDecAd', 'efDecAcl',
'efSmspDaNumber', 'efDecSmsp', 'efDecSpdi', 'efDecNai', 'efDecDir', 'efDecArr', 'efDecPnn', 'efDecAdn', 'efSmspDaNumber', 'efDecSmsp', 'efDecSpdi', 'efDecNai', 'efDecDir', 'efDecArr', 'efDecPnn', 'efDecAdn',
'pinKeyRefName', 'efArrAmLabel', 'efArrSc', 'efArrAmDo', 'pinKeyRefName', 'efArrAmLabel', 'efArrSc', 'efArrAmDo',
'efDecExt1', 'efDecSms', 'efDecSume', 'efPbrHex', 'efDecPbr', 'efFindDecoder', 'efFidFromPath', 'efDecodeBytes', 'efDecExt1', 'efDecSms', 'efDecSume', 'efPbrHex', 'efDecPbr', 'efFindDecoder', 'efFidFromPath', 'efDfFromPath', 'efDecodeBytes',
'efDecodeFile', 'efDecodeHex', 'efIsRawOnly', 'efFieldLabel', 'efPrimitive', 'efFlatten', 'efDecodeFile', 'efDecodeHex', 'efIsRawOnly', 'efFieldLabel', 'efPrimitive', 'efFlatten',
'efDiffData', 'efDataSummary', 'efContentDiff', 'gsm7Decode', 'efRenderFieldsHtml', 'efDiffData', 'efDataSummary', 'efContentDiff', 'gsm7Decode', 'efRenderFieldsHtml',
'pysimFsDecodedHtml', 'profilerRenderReport', 'profilerLabelText', 'profilerResultAspects', 'pysimFsDecodedHtml', 'pysimFsNodePath', 'pysimFsFindNodeByPath', 'profilerRenderReport', 'profilerLabelText', 'profilerResultAspects',
'profilerAspectSummary', 'profilerMatchedRecordsText', 'profilerRawDataCheck', 'profilerCustomNameForPath', 'profilerAspectSummary', 'profilerMatchedRecordsText', 'profilerRawDataCheck', 'profilerCustomNameForPath',
'pysimCustomNormPath', 'pysimCustomNormPath',
]; ];
@@ -45,9 +45,11 @@ for (const c of ['GSM7_ALPHABET', 'GSM7_EXT_MAP', 'FCP_ARR_AM_BITS']) {
for (const c of ['EF_UST_SERVICES', 'EF_SST_SERVICES', 'EF_IST_SERVICES', 'EF_EST_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(new RegExp('const ' + c + ' = \\{.*\\};'))[0].replace('const ', 'var ') + '\n';
} }
code += html.match(/const EF_ECC_CATEGORIES = \[[\s\S]*?\n\];/)[0].replace('const ', 'var ') + '\n';
code += html.match(/const EF_ADF_RIDS = \[[\s\S]*?\n\];/)[0].replace('const ', 'var ') + '\n';
code += html.match(/const EF_FIELD_LABELS = \{[\s\S]*?\n\};/)[0].replace('const ', 'var ') + '\n'; code += html.match(/const EF_FIELD_LABELS = \{[\s\S]*?\n\};/)[0].replace('const ', 'var ') + '\n';
code += html.match(/const EF_DECODERS = \[[\s\S]*?\n\];/)[0].replace('const ', 'var ') + '\n'; code += html.match(/const EF_DECODERS = \[[\s\S]*?\n\];/)[0].replace('const ', 'var ') + '\n';
code += 'var pysimCustomFiles = [];\nvar pysimFsSelected = \'EF.IMSI\';\n'; code += 'var pysimCustomFiles = [];\nvar pysimFsSelected = \'EF.IMSI\';\nvar pysimFsTreeRoot = null;\n';
code += 'function esc(s){return String(s);}\nfunction escHtml(s){return String(s);}\nfunction t(k){return k;}\n'; code += 'function esc(s){return String(s);}\nfunction escHtml(s){return String(s);}\nfunction t(k){return k;}\n';
eval(code); eval(code);
@@ -106,7 +108,7 @@ test('EF decoders match the pinned spec / pySim test vectors', () => {
assert.deepStrictEqual(efDecAcc(efBytes('8000')), { access_classes: [15] }); assert.deepStrictEqual(efDecAcc(efBytes('8000')), { access_classes: [15] });
assert.deepStrictEqual(efDecAcc(efBytes('0001')), { access_classes: [0] }); assert.deepStrictEqual(efDecAcc(efBytes('0001')), { access_classes: [0] });
assert.deepStrictEqual(efDecAcc(efBytes('8001')), { access_classes: [15, 0] }); assert.deepStrictEqual(efDecAcc(efBytes('8001')), { access_classes: [15, 0] });
assert.deepStrictEqual(efDecEcc(efBytes('19f101')), { number: '911', esc: '0x01' }); assert.deepStrictEqual(efDecEcc(efBytes('19f101')), { number: '911', esc: 'police' });
assert.deepStrictEqual(efDecKc(efBytes('837d783609a3858f05')), { kc: '837D783609A3858F', ksi: 0 }); assert.deepStrictEqual(efDecKc(efBytes('837d783609a3858f05')), { kc: '837D783609A3858F', ksi: 0 });
assert.deepStrictEqual(efDecPlmnList(efBytes('22F860')), { plmns: [{ mcc: '228', mnc: '06' }] }); assert.deepStrictEqual(efDecPlmnList(efBytes('22F860')), { plmns: [{ mcc: '228', mnc: '06' }] });
// EF.FPLMN: a FFFFFF gap in any position is skipped, not a terminator // EF.FPLMN: a FFFFFF gap in any position is skipped, not a terminator
@@ -357,6 +359,106 @@ test('decoder registry resolves by name first, then FID; unknown returns null',
assert.strictEqual(efFidFromPath('ADF.USIM'), null); assert.strictEqual(efFidFromPath('ADF.USIM'), null);
}); });
test('EF.ECC (DF.GSM) decodes the Emergency Service Category like the USIM file', () => {
// TS 51.011 10.3.27: the category byte is the TS 24.008 10.5.135d coding,
// the same table the ADF.USIM file uses (bit n = service n)
assert.deepStrictEqual(efDecEcc(efBytes('19F104')), { number: '911', esc: 'fire brigade' });
assert.deepStrictEqual(efDecEcc(efBytes('19F102')), { number: '911', esc: 'ambulance' });
assert.deepStrictEqual(efDecEcc(efBytes('19F109')), { number: '911', esc: 'police, marine guard' });
// a byte without a known bit keeps its raw hex value
assert.deepStrictEqual(efDecEcc(efBytes('19F180')), { number: '911', esc: '0x80' });
// a record whose code is FF is still decoded (only an all-FF record is empty)
assert.deepStrictEqual(efDecEcc(efBytes('FFFF00')), { number: null, esc: '0x00' });
// several 3-byte records
assert.deepStrictEqual(efDecEcc(efBytes('19F10411F220')), {
numbers: [{ number: '911', esc: 'fire brigade' }, { number: '112', esc: 'manual eCall' }],
});
assert.deepStrictEqual(efDecEcc(efBytes('FFFFFFFF')), { empty: true });
});
test('efDfFromPath reads the parent DF of a canonical path', () => {
assert.strictEqual(efDfFromPath('MF/7F20/6FAD'), '7F20');
assert.strictEqual(efDfFromPath('ADF.USIM/6FAD'), 'ADF.USIM');
assert.strictEqual(efDfFromPath('MF/ADF.USIM/6FAD'), 'ADF.USIM');
assert.strictEqual(efDfFromPath('mf/7f20/6fad'), '7F20');
assert.strictEqual(efDfFromPath('MF/7F10/5F3A/4F30'), '5F3A');
// a bare FID (and a bare root) carries no DF context
assert.strictEqual(efDfFromPath('6FAD'), '');
assert.strictEqual(efDfFromPath('MF'), '');
assert.strictEqual(efDfFromPath('ADF.USIM'), '');
assert.strictEqual(efDfFromPath(''), '');
assert.strictEqual(efDfFromPath(null), '');
// the two halves of the spec identity come from the same path
assert.strictEqual(efFidFromPath('MF/7F20/6FAD'), '6FAD');
});
test('efDfFromPath folds an ADF AID root onto its symbolic ADF name', () => {
// a snapshot/profile path roots an ADF by the DF name (the AID) the card
// reports, not by "ADF.USIM" - the RID decides, whatever the PIX is
assert.strictEqual(efDfFromPath('A0000000871002/6F07'), 'ADF.USIM');
assert.strictEqual(efDfFromPath('A0000000871002FF49FF0589/6F07'), 'ADF.USIM');
assert.strictEqual(efDfFromPath('a0000000871002ff49ff0589/6fad'), 'ADF.USIM');
assert.strictEqual(efDfFromPath('A0000000871004/6F07'), 'ADF.ISIM');
assert.strictEqual(efDfFromPath('A0000000871004FF49FF0589/6F07'), 'ADF.ISIM');
// the non-IMSI USIM (TS 31.102 Annex N) repeats the ADF.USIM file set
assert.strictEqual(efDfFromPath('A000000087100B/6F38'), 'ADF.USIM-NON-IMSI');
assert.strictEqual(efDfFromPath('a000000087100bff49ff0589/6f38'), 'ADF.USIM-NON-IMSI');
// an unrelated AID is not an ADF name: it passes through as itself
assert.strictEqual(efDfFromPath('A0000000090003FFFF/6F07'), 'A0000000090003FFFF');
assert.strictEqual(efDfFromPath('F0414C46416001/6F07'), 'F0414C46416001');
// the AID root reads the same as the symbolic name and the plain FID do
assert.strictEqual(efFindDecoder('EF.ECC', '6FB7', efDfFromPath('A0000000871002/6FB7')).fn, efDecEccUsim);
assert.strictEqual(efFindDecoder('EF.ECC', '6FB7', efDfFromPath('MF/7F20/6FB7')).fn, efDecEcc);
});
test('the decoder resolver is DF-first: EF.ECC is the SIM or the USIM file', () => {
// EF.ECC collides in name AND FID: only the parent DF separates the
// TS 51.011 transparent form from the TS 31.102 linear-fixed one
assert.strictEqual(efFindDecoder('EF.ECC', '6fb7', '7F20').fn, efDecEcc);
assert.strictEqual(efFindDecoder('EF.ECC', '6fb7', 'ADF.USIM').fn, efDecEccUsim);
assert.strictEqual(efFindDecoder('EF.ECC', '6fb7', 'adf.usim').fn, efDecEccUsim);
assert.strictEqual(efFindDecoder(null, '6FB7', 'ADF.USIM').fn, efDecEccUsim);
// without a DF the historical name-then-FID lookup answers (the first
// entry, the DF.GSM form, is unchanged behaviour)
assert.strictEqual(efFindDecoder('EF.ECC', '6fb7').fn, efDecEcc);
assert.strictEqual(efFindDecoder('EF.ECC', null).fn, efDecEcc);
// a DF carrying no entry of its own falls back to the generic lookup
assert.strictEqual(efFindDecoder('EF.ECC', '6fb7', 'ADF.ISIM').fn, efDecEcc);
// the FID fallback stays inside the DF
assert.strictEqual(efFindDecoder('EF.NOPE', '6FB7', 'ADF.USIM').fn, efDecEccUsim);
assert.strictEqual(efFindDecoder('EF.NOPE', 'abcd', 'ADF.USIM'), null);
// every EF.AD form has its own DF-keyed entry over one decoder
assert.strictEqual(efFindDecoder('EF.AD', '6fad', '7F20').fn, efDecAd);
assert.strictEqual(efFindDecoder('EF.AD', '6fad', 'ADF.USIM').fn, efDecAd);
assert.strictEqual(efFindDecoder('EF.AD', '6fad', 'ADF.ISIM').fn, efDecAd);
assert.strictEqual(efFindDecoder('EF.AD', '6fad', 'ADF.ISIM').df, 'ADF.ISIM');
// files without a collision are unaffected by the DF context
assert.strictEqual(efFindDecoder('EF.IMSI', '6f07', 'ADF.USIM').name, 'EF.IMSI');
assert.strictEqual(efFindDecoder(null, '2fe2', 'MF').name, 'EF.ICCID');
});
test('EF.ECC (USIM) decodes a X+4 record: code, alpha and service category', () => {
// pySim's EF_ECC vectors (TS 31.102 4.2.21): 3 BCD code bytes, the
// optional annex A alpha, then the category byte
assert.deepStrictEqual(efDecEccUsim(efBytes('19F1FF01')), { code: '911', category: 'police' });
assert.deepStrictEqual(efDecEccUsim(efBytes('19F3FF02')), { code: '913', category: 'ambulance' });
// code 112 = 11 F2 FF (odd digit count, F padded)
assert.deepStrictEqual(efDecEccUsim(efBytes('11F2FF40')), { code: '112', category: 'automatic eCall' });
// with the optional alpha identifier (X = 3, GSM 7-bit octets)
assert.deepStrictEqual(efDecEccUsim(efBytes('19F1FF424F4204')),
{ code: '911', category: 'fire brigade', alpha: 'BOB' });
// a UCS-2 (0x80-prefixed) alpha, and two category bits at once
assert.deepStrictEqual(efDecEccUsim(efBytes('19F1FF800042004F004205')),
{ code: '911', category: 'police, fire brigade', alpha: 'BOB' });
// an unmodelled category byte stays visible as its hex value
assert.deepStrictEqual(efDecEccUsim(efBytes('19F1FF80')), { code: '911', category: '0x80' });
// unused records: all FF, or the mandatory code all FF
assert.deepStrictEqual(efDecEccUsim(efBytes('FFFFFFFFFFFF')), { empty: true });
assert.deepStrictEqual(efDecEccUsim(efBytes('FFFFFF02')), { empty: true });
// shorter than the mandatory fields: raw fallback, never a throw
assert.deepStrictEqual(efDecEccUsim(efBytes('19F1FF')), { raw: '19F1FF' });
});
test('efDecodeFile handles transparent and record content', () => { test('efDecodeFile handles transparent and record content', () => {
const t = efDecodeFile('EF.IMSI', null, { kind: 'transparent', data: '082982608200002080' }); const t = efDecodeFile('EF.IMSI', null, { kind: 'transparent', data: '082982608200002080' });
assert.strictEqual(typeof t.fn, 'function'); assert.strictEqual(typeof t.fn, 'function');
@@ -398,6 +500,58 @@ test('efFlatten skips the all-FF empty marker', () => {
assert.deepStrictEqual(efFlatten({ empty: true }), []); assert.deepStrictEqual(efFlatten({ empty: true }), []);
}); });
test('the file manager resolves two same-named files by their parent DF', () => {
// EF.ECC exists in DF.GSM and ADF.USIM with the same name and FID; the
// selection key is the canonical path, so each resolves to its own node
const root = { name: 'MF', fid: '3F00', children: [], parent: null };
const gsm = { name: 'DF.GSM', fid: '7F20', children: [], parent: root };
const usim = { name: 'ADF.USIM', fid: '7FFF', children: [], parent: root };
const gsmEcc = { name: 'EF.ECC', fid: '6FB7', children: null, parent: gsm };
const usimEcc = { name: 'EF.ECC', fid: '6FB7', children: null, parent: usim };
root.children = [gsm, usim];
gsm.children = [gsmEcc];
usim.children = [usimEcc];
pysimFsTreeRoot = root;
assert.strictEqual(pysimFsFindNodeByPath('MF/7F20/6FB7'), gsmEcc);
assert.strictEqual(pysimFsFindNodeByPath('ADF.USIM/6FB7'), usimEcc);
// a path that is not loaded resolves to nothing (the callers fall back to
// the path's own last segment / FID)
assert.strictEqual(pysimFsFindNodeByPath('MF/7F10/6FB7'), null);
// the decoded view's DF follows the resolved node: the USIM file is the
// linear-fixed X+4 form, the DF.GSM copy the SIM transparent one
pysimFsSelected = 'ADF.USIM/6FB7';
const usimHtml = pysimFsDecodedHtml(pysimFsFindNodeByPath(pysimFsSelected),
{ success: true, records: [{ num: 1, data: '19F1FF04' }] });
assert.ok(usimHtml.includes('Emergency call code'));
assert.ok(usimHtml.includes('fire brigade'));
pysimFsSelected = 'MF/7F20/6FB7';
const gsmHtml = pysimFsDecodedHtml(pysimFsFindNodeByPath(pysimFsSelected),
{ success: true, data: '19F104' });
assert.ok(gsmHtml.includes('Emergency category'));
assert.ok(gsmHtml.includes('fire brigade'));
assert.ok(!gsmHtml.includes('Emergency call code'));
});
test('pysimFsDecodedHtml decodes EF.ECC by the selected node\'s parent DF', () => {
// ADF.USIM's EF.ECC is the linear-fixed X+4 form (code/alpha/category)
const usim = { name: 'ADF.USIM', children: [] };
const usimEcc = { name: 'EF.ECC', fid: '6FB7', parent: usim };
usim.children.push(usimEcc);
const usimHtml = pysimFsDecodedHtml(usimEcc, { success: true, records: [{ num: 1, data: '19F1FF01' }] });
assert.ok(usimHtml.includes('Emergency call code'));
assert.ok(usimHtml.includes('911'));
assert.ok(usimHtml.includes('police'));
// DF.GSM's EF.ECC keeps the transparent code + category decode
const gsm = { name: 'DF.GSM', fid: '7F20', children: [] };
const gsmEcc = { name: 'EF.ECC', fid: '6FB7', parent: gsm };
gsm.children.push(gsmEcc);
const gsmHtml = pysimFsDecodedHtml(gsmEcc, { success: true, data: '19F101' });
assert.ok(gsmHtml.includes('Number'));
assert.ok(gsmHtml.includes('911'));
assert.ok(gsmHtml.includes('Emergency category'));
assert.ok(!gsmHtml.includes('Emergency call code'));
});
test('efDiffData names an all-FF side Empty (all FF)', () => { test('efDiffData names an all-FF side Empty (all FF)', () => {
assert.deepStrictEqual(efDiffData({ empty: true }, { full: 'Miran' }), assert.deepStrictEqual(efDiffData({ empty: true }, { full: 'Miran' }),
[['Full name', 'Empty (all FF)', 'Miran']]); [['Full name', 'Empty (all FF)', 'Miran']]);
+106 -8
View File
@@ -51,13 +51,13 @@ const FNS = [
'efEncPlmnList', 'efListRows', 'efPlmnRowOrig', 'efEncAdn', 'efEncAdnExt', 'efPnnNameBytes', 'efPnnHexBytes', 'efPnnOrigTlv', 'efEncPnn', 'efEncPlmnList', 'efListRows', 'efPlmnRowOrig', 'efEncAdn', 'efEncAdnExt', 'efPnnNameBytes', 'efPnnHexBytes', 'efPnnOrigTlv', 'efEncPnn',
'efFindEncoder', 'efHasEncoder', 'gsm7Decode', 'gsm7TextToSeptets', 'efFindEncoder', 'efHasEncoder', 'gsm7Decode', 'gsm7TextToSeptets',
'gsm7Encode', 'efEncError', 'efDecModelValues', 'gsm7Encode', 'efEncError', 'efDecModelValues',
'efDecServiceTable', 'efDecSst', 'efDecAcc', 'efDecPhase', 'efDecHpplmn', 'efDecAd', 'efDecCbmi', 'efDecCbmir', 'efDecIccid', 'efDecImsi', 'efDecPlmnList', 'efDecFplmn', 'efEncServiceTable', 'efEncAcc', 'efPhaseValue', 'efPhaseModel', 'efEncPhase', 'efEncHpplmn', 'efAdOpValue', 'efAdModel', 'efEncAd', 'efEncPlmnOnly', 'efEncIccid', 'efEncImsi', 'efCbmiModel', 'efEncCbmi', 'efCbmirModel', 'efEncCbmir', 'efSmspAddressModel', 'efSmspModel', 'efSmspTonOptions', 'efSmspParamRow', 'efSmspHtml', 'efSmspCollect', 'pysimFsSmspToggle', 'pysimFsSmspHexInput', 'efSmspHexByte', 'efSmspAddressBytes', 'efSmspAddrUnchanged', 'efSmspByteUnchanged', 'efEncSmsp', 'smspT', 'smspDcsHint', 'smspVpHint', 'smspDurationText', 'smspPluralIndex', 'smspCount', 'efSmspDaNumber', 'efDecServiceTable', 'efDecSst', 'efDecAcc', 'efDecPhase', 'efDecHpplmn', 'efDecAd', 'efDecCbmi', 'efDecCbmir', 'efDecIccid', 'efDecImsi', 'efDecPlmnList', 'efDecFplmn', 'efEncServiceTable', 'efEncSst', 'efEncAcc', 'efPhaseValue', 'efPhaseModel', 'efEncPhase', 'efEncHpplmn', 'efAdOpValue', 'efAdModel', 'efEncAd', 'efEncPlmnOnly', 'efEncIccid', 'efEncImsi', 'efCbmiModel', 'efEncCbmi', 'efCbmirModel', 'efEncCbmir', 'efSmspAddressModel', 'efSmspModel', 'efSmspTonOptions', 'efSmspParamRow', 'efSmspHtml', 'efSmspCollect', 'pysimFsSmspToggle', 'pysimFsSmspHexInput', 'efSmspHexByte', 'efSmspAddressBytes', 'efSmspAddrUnchanged', 'efSmspByteUnchanged', 'efEncSmsp', 'smspT', 'smspDcsHint', 'smspVpHint', 'smspDurationText', 'smspPluralIndex', 'smspCount', 'efSmspDaNumber',
]; ];
let code = ''; let code = '';
for (const f of FNS) code += extractFunc(html, f) + '\n'; for (const f of FNS) code += extractFunc(html, f) + '\n';
for (const c of ['GSM7_ALPHABET', 'GSM7_EXT_MAP', 'EF_ACT_TECHS', 'EF_SPN_FIELDS', for (const c of ['GSM7_ALPHABET', 'GSM7_EXT_MAP', 'EF_ACT_TECHS', 'EF_SPN_FIELDS',
'EF_ADN_FIELDS', 'EF_PLMN_LIST', 'EF_PNN_ITEMS', 'EF_UST_SERVICES', 'EF_SST_SERVICES', 'EF_IST_SERVICES', 'EF_EST_SERVICES', 'EF_PHASE_NAMES', 'EF_ACC_CLASSES', 'EF_UST_FIELDS', 'EF_IST_FIELDS', 'EF_EST_FIELDS', 'EF_ACC_FIELDS', 'EF_PHASE_FIELDS', 'EF_HPPLMN_FIELDS', 'EF_AD_OP_MODES', 'EF_AD_FIELDS', 'EF_PLMN_ONLY_LIST', 'EF_ICCID_FIELDS', 'EF_IMSI_FIELDS', 'EF_CBMI_LIST', 'EF_CBMIR_LIST', 'EF_READONLY_NAMES', 'EF_SMSP_PARAMS', 'EF_SMSP_CUSTOM', 'EF_ENCODERS']) { 'EF_ADN_FIELDS', 'EF_PLMN_LIST', 'EF_PNN_ITEMS', 'EF_UST_SERVICES', 'EF_SST_SERVICES', 'EF_IST_SERVICES', 'EF_EST_SERVICES', 'EF_PHASE_NAMES', 'EF_ACC_CLASSES', 'EF_UST_FIELDS', 'EF_IST_FIELDS', 'EF_EST_FIELDS', 'EF_SST_FIELDS', 'EF_ACC_FIELDS', 'EF_PHASE_FIELDS', 'EF_HPPLMN_FIELDS', 'EF_AD_OP_MODES', 'EF_AD_FIELDS', 'EF_PLMN_ONLY_LIST', 'EF_ICCID_FIELDS', 'EF_IMSI_FIELDS', 'EF_CBMI_LIST', 'EF_CBMIR_LIST', 'EF_READONLY_NAMES', 'EF_SMSP_PARAMS', 'EF_SMSP_CUSTOM', 'EF_ENCODERS']) {
code += extractConst(html, c); code += extractConst(html, c);
} }
eval(code); eval(code);
@@ -218,6 +218,43 @@ test('EF.SPN and the ADN family share the encoder registry with the PLMN list',
assert.strictEqual(efFindEncoder('EF.PLMNwAcT', null).list, efFindEncoder('EF.HPLMNwAcT', null).list); assert.strictEqual(efFindEncoder('EF.PLMNwAcT', null).list, efFindEncoder('EF.HPLMNwAcT', null).list);
}); });
test('the encoder resolver is DF-first: every EF.AD form shares one encoder', () => {
// EF.AD collides in name AND FID: one entry per DF (DF.GSM 3+X,
// ADF.USIM 4+X, ADF.ISIM 3+X), all over the same length-preserving codec
const sim = efFindEncoder('EF.AD', '6fad', '7F20');
const usim = efFindEncoder('EF.AD', '6fad', 'ADF.USIM');
const isim = efFindEncoder('EF.AD', '6fad', 'ADF.ISIM');
for (const e of [sim, usim, isim]) {
assert.strictEqual(e.enc, efEncAd);
assert.strictEqual(e.model, efAdModel);
assert.strictEqual(e.fields, EF_AD_FIELDS);
}
assert.notStrictEqual(sim, isim);
// without a DF the historical name-first lookup answers (DF.GSM's entry)
assert.strictEqual(efFindEncoder('EF.AD', '6fad'), sim);
assert.strictEqual(efFindEncoder('EF.AD', '6fad', 'ADF.NOPE'), sim);
// EF.ECC stays read-only in either DF; the shared 6F38 FID resolves to
// EF.SST under DF.GSM and to EF.UST under ADF.USIM (the FID fallback
// without a DF keeps the historical UST answer - SST's entry is DF-scoped)
assert.strictEqual(efHasEncoder('EF.ECC', '6fb7', '7F20'), false);
assert.strictEqual(efHasEncoder('EF.ECC', '6fb7', 'ADF.USIM'), false);
assert.strictEqual(efHasEncoder('EF.SST', '6f38', '7F20'), true);
assert.strictEqual(efHasEncoder('EF.UST', '6f38', 'ADF.USIM'), true);
assert.strictEqual(efFindEncoder('EF.SST', '6f38', '7F20').enc, efEncSst);
assert.strictEqual(efFindEncoder('EF.UST', '6f38', 'ADF.USIM').enc, efFindEncoder('EF.UST', '6f38').enc);
assert.strictEqual(efFindEncoder(null, '6f38', '7F20').name, 'EF.SST');
assert.strictEqual(efFindEncoder(null, '6f38', 'ADF.USIM').name, 'EF.UST');
assert.strictEqual(efFindEncoder(null, '6f38').name, 'EF.UST');
assert.strictEqual(efFindEncoder(null, '6f38', 'ADF.USIM').enc !== efEncSst, true);
assert.strictEqual(efFindEncoder('EF.NOPE', 'abcd', 'ADF.USIM'), null);
// the SIM 3+X and USIM 4+X forms keep their length on an untouched save
for (const hex of ['00' + '0000', '00' + '0000' + '02' + 'AABB', AD_HEX, 'FF'.repeat(6)]) {
for (const e of [sim, usim]) {
assert.strictEqual(e.enc(e.model(efDecAd(efBytes(hex))), hex), hex.toUpperCase(), hex);
}
}
});
test('encoder validation errors go through t() when the runtime provides it', () => { test('encoder validation errors go through t() when the runtime provides it', () => {
globalThis.t = k => (k === 'Select at least one access technology' ? 'Выберите технологию' : k); globalThis.t = k => (k === 'Select at least one access technology' ? 'Выберите технологию' : k);
try { try {
@@ -573,7 +610,7 @@ test('F8: the ADN-family entries name their extension EF and use it in refusals'
// Wave 2 fixtures. // Wave 2 fixtures.
const UST_ONE = '00000010'; // service 29 const UST_ONE = '00000010'; // service 29
const UST_MULTI = '010204'; // services 1, 10, 19 const UST_MULTI = '010204'; // services 1, 10, 19
const UST_UNMODELLED = '000000' + '10' + '00'.repeat(14) + 'FC'; // 29 + bits 147-152 const UST_UNMODELLED = '000000' + '10' + '00'.repeat(14) + 'C0'; // 29 + bits 151-152 (above the 150-service table)
const EST_FF = 'FFFF'; const EST_FF = 'FFFF';
const ACC_ONE = '8001'; // classes 15 and 0 const ACC_ONE = '8001'; // classes 15 and 0
const AD_HEX = '00' + '0000' + '03' + 'AABB'; // op normal, info 0000, MNC 3, RFU const AD_HEX = '00' + '0000' + '03' + 'AABB'; // op normal, info 0000, MNC 3, RFU
@@ -596,10 +633,13 @@ test('service tables: encode/decode round-trips and unmodelled bits', () => {
const services = efDecServiceTable(efBytes(hex), table).services; const services = efDecServiceTable(efBytes(hex), table).services;
assert.strictEqual(entry.enc({ services: services }, hex), hex, name + ' ' + hex); assert.strictEqual(entry.enc({ services: services }, hex), hex, name + ' ' + hex);
} }
// EF.SST has no editor (its 2-bit SIM layout needs a dedicated UI) // EF.SST has its own 2-bit editor on the shared 6F38 FID (the DF-scoped
assert.strictEqual(efFindEncoder('EF.SST', '6f38'), null); // entry keeps the UST answer for a bare FID / ADF context)
assert.strictEqual(efHasEncoder('EF.SST', '6f38'), false); assert.strictEqual(efFindEncoder('EF.SST', '6f38').enc, efEncSst);
assert.strictEqual(efHasEncoder('EF.SST', '6f38'), true);
assert.strictEqual(efFindEncoder('EF.UST', '6f38').name, 'EF.UST'); assert.strictEqual(efFindEncoder('EF.UST', '6f38').name, 'EF.UST');
assert.strictEqual(efFindEncoder(null, '6f38').name, 'EF.UST');
assert.strictEqual(efFindEncoder(null, '6f38', '7F20').name, 'EF.SST');
// EST defines services 1-3; all-FF keeps the other bits too // EST defines services 1-3; all-FF keeps the other bits too
assert.deepStrictEqual(efDecServiceTable(efBytes('07'), EF_EST_SERVICES).services, assert.deepStrictEqual(efDecServiceTable(efBytes('07'), EF_EST_SERVICES).services,
[{ n: 1, name: 'Fixed Dialling Numbers (FDN)' }, [{ n: 1, name: 'Fixed Dialling Numbers (FDN)' },
@@ -615,20 +655,78 @@ test('service tables: encode/decode round-trips and unmodelled bits', () => {
assert.strictEqual(entry.enc({ services: unmodelled }, UST_UNMODELLED), UST_UNMODELLED); assert.strictEqual(entry.enc({ services: unmodelled }, UST_UNMODELLED), UST_UNMODELLED);
// unchecking everything clears the table bits but keeps the unmodelled byte // unchecking everything clears the table bits but keeps the unmodelled byte
assert.strictEqual(entry.enc({ services: [] }, UST_UNMODELLED), assert.strictEqual(entry.enc({ services: [] }, UST_UNMODELLED),
'000000' + '00' + '00'.repeat(14) + 'FC'); '000000' + '00' + '00'.repeat(14) + 'C0');
// length is kept // length is kept
assert.strictEqual(entry.enc({ services: efDecServiceTable(efBytes('00000010' + '00'), EF_UST_SERVICES).services }, assert.strictEqual(entry.enc({ services: efDecServiceTable(efBytes('00000010' + '00'), EF_UST_SERVICES).services },
'0000001000'), '0000001000'); '0000001000'), '0000001000');
}); });
test('service tables: unknown services and a too-short table are refused', () => { test('service tables: unknown services and a too-short table are refused', () => {
assert.throws(() => efEncServiceTable({ services: [{ n: 147, name: 'RFU' }] }, assert.throws(() => efEncServiceTable({ services: [{ n: 151, name: 'RFU' }] },
'FF'.repeat(19), EF_UST_SERVICES), /Unknown service/); 'FF'.repeat(19), EF_UST_SERVICES), /Unknown service/);
assert.throws(() => efEncServiceTable({ services: [{ n: 9, name: 'MSISDN' }] }, assert.throws(() => efEncServiceTable({ services: [{ n: 9, name: 'MSISDN' }] },
'00', EF_UST_SERVICES), /too short for service/); '00', EF_UST_SERVICES), /too short for service/);
assert.throws(() => efEncServiceTable({ services: [{ n: 0 }] }, '00', EF_UST_SERVICES), /Unknown service/); assert.throws(() => efEncServiceTable({ services: [{ n: 0 }] }, '00', EF_UST_SERVICES), /Unknown service/);
}); });
// EF.SST (TS 51.011 10.3.7): two bits per service, four per byte. 15 bytes =
// services 1-60; C7/1C/C1 carry on (11), off (10) and deleted (00) pairs and
// the last byte's bits 7-8 are service 60, above the 59-entry table.
const SST_MIX = 'C71CC1' + '00'.repeat(11) + 'C0';
test('EF.SST: the 2-bit encoder round-trips the decoded file', () => {
const dec = efDecSst(efBytes(SST_MIX));
assert.deepStrictEqual(dec.services.map(s => [s.n, s.allocated, s.activated]), [
[1, true, true], [2, true, false], [4, true, true], [6, true, true],
[7, true, false], [9, true, false], [12, true, true], [60, true, true],
]);
// a deleted (00) pair is omitted by the decoder, so decode->encode is exact
assert.strictEqual(efEncSst(dec, SST_MIX), SST_MIX);
// every state on: all-FF, unmodelled service 60 included
assert.strictEqual(efEncSst(efDecSst(efBytes('FF'.repeat(15))), 'FF'.repeat(15)), 'FF'.repeat(15));
// no service allocated at all
assert.strictEqual(efEncSst(efDecSst(efBytes('00'.repeat(15))), '00'.repeat(15)), '00'.repeat(15));
// clearing every modelled service keeps service 60's pair (byte 14 C0)
assert.strictEqual(efEncSst({ services: [] }, SST_MIX), '000000' + '00'.repeat(11) + 'C0');
// the file length is kept
assert.strictEqual(efEncSst(efDecSst(efBytes('0C')), '0C'), '0C');
assert.strictEqual(efEncSst(efDecSst(efBytes('0C00')), '0C00'), '0C00');
});
test('EF.SST: deleted clears the pair, off sets 10 and on sets 11', () => {
const on = n => ({ n: n, allocated: true, activated: true });
const off = n => ({ n: n, allocated: true, activated: false });
const del = n => ({ n: n, allocated: false, activated: false });
// service 2 (byte 0, pair 2)
assert.strictEqual(efEncSst({ services: [on(2)] }, '00'), '0C');
assert.strictEqual(efEncSst({ services: [off(2)] }, '00'), '04');
assert.strictEqual(efEncSst({ services: [del(2)] }, '0C'), '00');
// every modelled pair is rebuilt from the list, the rest of the file stays
assert.strictEqual(efEncSst({ services: [on(2)] }, 'FF'.repeat(15)),
'0C' + '00'.repeat(13) + 'C0');
assert.strictEqual(efEncSst({ services: [on(2), off(4)] }, SST_MIX).slice(0, 2), '4C');
// a pair above the table is written like any other (a longer file)
const long = '00'.repeat(16);
assert.strictEqual(efEncSst({ services: [on(62)] }, long), '00'.repeat(15) + '0C');
// the decoder's illegal '08' pair (activated, not allocated) is refused
assert.deepStrictEqual(efDecSst(efBytes('08')), { services: [
{ n: 2, name: 'Abbreviated Dialling Numbers (ADN)', allocated: false, activated: true },
] });
assert.throws(() => efEncSst(efDecSst(efBytes('08')), '08'), /must be deleted, off or on/);
});
test('EF.SST: unknown services, an invalid state and a too-short file are refused', () => {
assert.throws(() => efEncSst({ services: [{ n: 5, allocated: true, activated: true }] }, '00'),
/too short for service/);
assert.throws(() => efEncSst({ services: [{ n: 0, allocated: true, activated: true }] }, '00'),
/Unknown service/);
assert.throws(() => efEncSst({ services: [{ n: 'x' }] }, '00'), /Unknown service/);
assert.throws(() => efEncSst({ services: [{ n: 2, allocated: false, activated: true }] }, '00'),
/must be deleted, off or on/);
// a bare service number carries no state
assert.throws(() => efEncSst({ services: [2] }, '00'), /must be deleted, off or on/);
});
test('F-ACC: the class numbering and the ACC10/encoder rules', () => { test('F-ACC: the class numbering and the ACC10/encoder rules', () => {
assert.deepStrictEqual(efDecAcc(efBytes('8000')), { access_classes: [15] }); assert.deepStrictEqual(efDecAcc(efBytes('8000')), { access_classes: [15] });
assert.deepStrictEqual(efDecAcc(efBytes('0001')), { access_classes: [0] }); assert.deepStrictEqual(efDecAcc(efBytes('0001')), { access_classes: [0] });
+147 -19
View File
@@ -48,22 +48,24 @@ const FNS = [
'efEncPlmnList', 'efListRows', 'efPlmnRowOrig', 'efEncAdn', 'efEncAdnExt', 'efPnnNameBytes', 'efPnnHexBytes', 'efPnnOrigTlv', 'efEncPnn', 'efEncPlmnList', 'efListRows', 'efPlmnRowOrig', 'efEncAdn', 'efEncAdnExt', 'efPnnNameBytes', 'efPnnHexBytes', 'efPnnOrigTlv', 'efEncPnn',
'efFindEncoder', 'efHasEncoder', 'efEncError', 'efFindEncoder', 'efHasEncoder', 'efEncError',
'efDecModelValues', 'efDecModelFor', 'efDecEditModel', 'efActTechList', 'efDecFieldHtml', 'efDecModelValues', 'efDecModelFor', 'efDecEditModel', 'efActTechList', 'efDecFieldHtml',
'servicesFeatureByNumber', 'pysimFsFilterBtnHtml',
'efDecRowHtml', 'efDecGridHtml', 'efDecItemRowHtml', 'efDecItemsHtml', 'efDecRowHtml', 'efDecGridHtml', 'efDecItemRowHtml', 'efDecItemsHtml',
'efDecRecordsHtml', 'efDecEditHtml', 'efDecRecordsHtml', 'efDecEditHtml',
'efDecCollectValues', 'efDecCollectRows', 'efDecCollectItems', 'efDecCollectValues', 'efDecCollectRows', 'efDecCollectItems',
'efBitsSelected', 'pysimFsDecBitsFilter', 'pysimFsDecRowCheck', 'pysimFsDecItemTag', 'pysimFsDecAddItem', 'efBitsSelected', 'pysimFsDecBitsFilter', 'pysimFsDecBits2Filter', 'pysimFsDecRowCheck', 'pysimFsDecItemTag', 'pysimFsDecAddItem',
'pysimFsDecRemoveItem', 'pysimFsFindNode', 'pysimFsDecRemoveItem', 'pysimFsFindNodeByPath', 'pysimCustomNormPath', 'pysimFsNodePath', 'pysimFsFallbackNode', 'efFidFromPath', 'efDfFromPath',
'pysimFsHasDecoder', 'pysimFsHasEncoder', 'pysimFsDecSave', 'pysimFsDecEdit', 'pysimFsHasDecoder', 'pysimFsHasEncoder', 'pysimFsDecSave', 'pysimFsDecEdit',
'pysimFsPillsEnabled', 'pysimFsSyncContentVisibility', 'gsm7Decode', 'pysimFsPillsEnabled', 'pysimFsSyncContentVisibility', 'gsm7Decode',
'gsm7TextToSeptets', 'gsm7Encode', 'gsm7TextToSeptets', 'gsm7Encode',
'efDecServiceTable', 'efDecSst', 'efDecAcc', 'efDecPhase', 'efDecHpplmn', 'efDecAd', 'efDecCbmi', 'efDecCbmir', 'efDecIccid', 'efDecImsi', 'efDecPlmnList', 'efDecFplmn', 'efEncServiceTable', 'efEncAcc', 'efPhaseValue', 'efPhaseModel', 'efEncPhase', 'efEncHpplmn', 'efAdOpValue', 'efAdModel', 'efEncAd', 'efEncPlmnOnly', 'efEncIccid', 'efEncImsi', 'efCbmiModel', 'efEncCbmi', 'efCbmirModel', 'efEncCbmir', 'efSmspAddressModel', 'efSmspModel', 'efSmspTonOptions', 'efSmspParamRow', 'efSmspHtml', 'efSmspCollect', 'pysimFsSmspToggle', 'pysimFsSmspHexInput', 'efSmspHexByte', 'efSmspAddressBytes', 'efSmspAddrUnchanged', 'efSmspByteUnchanged', 'efEncSmsp', 'smspT', 'smspDcsHint', 'smspVpHint', 'smspDurationText', 'smspPluralIndex', 'smspCount', 'efSmspDaNumber', 'efDecServiceTable', 'efDecSst', 'efDecAcc', 'efDecPhase', 'efDecHpplmn', 'efDecAd', 'efDecCbmi', 'efDecCbmir', 'efDecIccid', 'efDecImsi', 'efDecPlmnList', 'efDecFplmn', 'efEncServiceTable', 'efEncSst', 'efEncAcc', 'efPhaseValue', 'efPhaseModel', 'efEncPhase', 'efEncHpplmn', 'efAdOpValue', 'efAdModel', 'efEncAd', 'efEncPlmnOnly', 'efEncIccid', 'efEncImsi', 'efCbmiModel', 'efEncCbmi', 'efCbmirModel', 'efEncCbmir', 'efSmspAddressModel', 'efSmspModel', 'efSmspTonOptions', 'efSmspParamRow', 'efSmspHtml', 'efSmspCollect', 'pysimFsSmspToggle', 'pysimFsSmspHexInput', 'efSmspHexByte', 'efSmspAddressBytes', 'efSmspAddrUnchanged', 'efSmspByteUnchanged', 'efEncSmsp', 'smspT', 'smspDcsHint', 'smspVpHint', 'smspDurationText', 'smspPluralIndex', 'smspCount', 'efSmspDaNumber',
]; ];
let code = 'var pysimFsSelected = null;\nvar pysimFsTreeRoot = null;\nvar pysimFsDecModel = null;\n' let code = 'var pysimFsSelected = null;\nvar pysimFsTreeRoot = null;\nvar pysimFsDecModel = null;\n'
+ 'var pysimFsDecEditMode = false;\nvar pysimFsEditMode = false;\n'; + 'var pysimFsDecEditMode = false;\nvar pysimFsEditMode = false;\nvar _servicesFeatureByNumber = null;\n';
for (const f of FNS) code += extractFunc(html, f) + '\n'; for (const f of FNS) code += extractFunc(html, f) + '\n';
for (const c of ['GSM7_ALPHABET', 'GSM7_EXT_MAP', 'EF_ACT_TECHS', 'EF_SPN_FIELDS', for (const c of ['GSM7_ALPHABET', 'GSM7_EXT_MAP', 'EF_ACT_TECHS', 'EF_SPN_FIELDS',
'EF_ADN_FIELDS', 'EF_PLMN_LIST', 'EF_PNN_ITEMS', 'EF_UST_SERVICES', 'EF_SST_SERVICES', 'EF_IST_SERVICES', 'EF_EST_SERVICES', 'EF_PHASE_NAMES', 'EF_ACC_CLASSES', 'EF_UST_FIELDS', 'EF_IST_FIELDS', 'EF_EST_FIELDS', 'EF_ACC_FIELDS', 'EF_PHASE_FIELDS', 'EF_HPPLMN_FIELDS', 'EF_AD_OP_MODES', 'EF_AD_FIELDS', 'EF_PLMN_ONLY_LIST', 'EF_ICCID_FIELDS', 'EF_IMSI_FIELDS', 'EF_CBMI_LIST', 'EF_CBMIR_LIST', 'EF_READONLY_NAMES', 'EF_SMSP_PARAMS', 'EF_SMSP_CUSTOM', 'EF_ENCODERS']) { 'EF_ADN_FIELDS', 'EF_PLMN_LIST', 'EF_PNN_ITEMS', 'EF_UST_SERVICES', 'EF_SST_SERVICES', 'EF_IST_SERVICES', 'EF_EST_SERVICES', 'EF_PHASE_NAMES', 'EF_ACC_CLASSES', 'EF_UST_FIELDS', 'EF_IST_FIELDS', 'EF_EST_FIELDS', 'EF_SST_FIELDS', 'EF_ACC_FIELDS', 'EF_PHASE_FIELDS', 'EF_HPPLMN_FIELDS', 'EF_AD_OP_MODES', 'EF_AD_FIELDS', 'EF_PLMN_ONLY_LIST', 'EF_ICCID_FIELDS', 'EF_IMSI_FIELDS', 'EF_CBMI_LIST', 'EF_CBMIR_LIST', 'EF_READONLY_NAMES', 'EF_SMSP_PARAMS', 'EF_SMSP_CUSTOM', 'EF_ENCODERS', 'EF_ADF_RIDS',
'SERVICES_CROSS_REF', 'SERVICES_FS_MAP']) {
code += extractConst(html, c); code += extractConst(html, c);
} }
eval(code); eval(code);
@@ -253,24 +255,50 @@ test('changing a record input checks its block', () => {
assert.strictEqual(cb.checked, true); assert.strictEqual(cb.checked, true);
}); });
test('pysimFsHasEncoder resolves the selected file by name or FID', () => { test('pysimFsHasEncoder resolves the selected file by its path or FID', () => {
pysimFsTreeRoot = { name: 'MF', children: [{ name: 'EF.SPN', fid: '6f46' }, { name: 'EF.ALIAS', fid: '6f3a' }] }; pysimFsTreeRoot = { name: 'MF', children: [] };
pysimFsSelected = 'EF.SPN'; const add = (name, fid) => {
const n = { name: name, fid: fid, parent: pysimFsTreeRoot };
pysimFsTreeRoot.children.push(n);
return n;
};
add('EF.SPN', '6f46');
add('EF.ALIAS', '6f3a');
pysimFsSelected = 'MF/6F46';
assert.strictEqual(pysimFsHasEncoder(), true); assert.strictEqual(pysimFsHasEncoder(), true);
pysimFsSelected = 'EF.ALIAS'; pysimFsSelected = 'MF/6F3A';
assert.strictEqual(pysimFsHasEncoder(), true); assert.strictEqual(pysimFsHasEncoder(), true);
// a bare FID selection still resolves through the FID fallback
pysimFsSelected = '6F46'; pysimFsSelected = '6F46';
assert.strictEqual(pysimFsHasEncoder(), true); assert.strictEqual(pysimFsHasEncoder(), true);
pysimFsSelected = 'EF.LOCI'; pysimFsSelected = 'MF/6F7E';
assert.strictEqual(pysimFsHasEncoder(), false);
// the SIM service table shares 6F38 with UST but has no editor
pysimFsTreeRoot = { name: 'MF', children: [{ name: 'EF.SST', fid: '6f38' }] };
pysimFsSelected = 'EF.SST';
assert.strictEqual(pysimFsHasEncoder(), false); assert.strictEqual(pysimFsHasEncoder(), false);
// the SIM service table shares 6F38 with UST but has its own 2-bit editor
pysimFsTreeRoot = { name: 'MF', children: [] };
const gsm = { name: 'DF.GSM', fid: '7F20', parent: pysimFsTreeRoot, children: [] };
pysimFsTreeRoot.children.push(gsm);
const sst = { name: 'EF.SST', fid: '6f38', parent: gsm };
gsm.children.push(sst);
pysimFsSelected = 'MF/7F20/6F38';
assert.strictEqual(pysimFsHasEncoder(), true);
pysimFsSelected = null; pysimFsSelected = null;
assert.strictEqual(pysimFsHasEncoder(), false); assert.strictEqual(pysimFsHasEncoder(), false);
}); });
test('the file-manager resolver call sites pass the parent DF', () => {
// the DF context comes from the selected node's canonical path (or the
// selected path itself when the node is not loaded); these sites
// gate/read/edit a selected file and must resolve the same entry
assert.ok(html.includes('return !!efFindDecoder(node && node.name, fid, df);'));
assert.ok(html.includes('return !!efFindEncoder(node && node.name, fid, df);'));
assert.ok(html.includes("const df = node ? efDfFromPath(pysimFsNodePath(node)) : efDfFromPath(pysimFsSelected);"));
assert.ok(html.includes('const model = efDecEditModel(node && node.name, fid, content,'));
for (const fn of ['pysimFsDecSave', 'pysimFsDecAddRow', 'pysimFsDecAddItem']) {
assert.ok(/efFindEncoder\([^;]*efDfFromPath\(pysimFsNodePath\(node\)\)/s.test(extractFunc(html, fn)),
fn + ' resolves the entry with the node DF');
}
});
test('the Edit decoded button is encoder-gated next to Edit raw', () => { test('the Edit decoded button is encoder-gated next to Edit raw', () => {
const btn = /<button[^>]*id="pysim-fs-edit-dec-btn"[^>]*>/.exec(html); const btn = /<button[^>]*id="pysim-fs-edit-dec-btn"[^>]*>/.exec(html);
assert.ok(btn, 'Edit decoded button'); assert.ok(btn, 'Edit decoded button');
@@ -634,8 +662,9 @@ test('F2: untouched PNN items keep their TLV bytes through the DOM and the save'
}); });
test('F12: a selection change during the read abandons the decoded edit', async () => { test('F12: a selection change during the read abandons the decoded edit', async () => {
pysimFsSelected = 'EF.SPN'; pysimFsSelected = 'MF/6F46';
pysimFsTreeRoot = { name: 'MF', children: [{ name: 'EF.SPN', fid: '6f46' }] }; pysimFsTreeRoot = { name: 'MF', children: [] };
pysimFsTreeRoot.children.push({ name: 'EF.SPN', fid: '6f46', parent: pysimFsTreeRoot });
const fakeEl = () => { const fakeEl = () => {
const classes = new Set(); const classes = new Set();
return { return {
@@ -668,7 +697,7 @@ test('F12: a selection change during the read abandons the decoded edit', async
let resets = 0; let resets = 0;
globalThis.pysimFsResetEdit = () => { resets++; }; globalThis.pysimFsResetEdit = () => { resets++; };
const pending = pysimFsDecEdit(); const pending = pysimFsDecEdit();
pysimFsSelected = 'EF.ADN'; pysimFsSelected = 'MF/6F3A';
release(); release();
await pending; await pending;
assert.strictEqual(resets, 1); assert.strictEqual(resets, 1);
@@ -689,8 +718,8 @@ test('the bits field renders a filtered checkbox grid and collects {n,name}', ()
assert.match(out, /class="pysim-dec-bits /); assert.match(out, /class="pysim-dec-bits /);
assert.match(out, /pysim-dec-bits-filter/); assert.match(out, /pysim-dec-bits-filter/);
assert.match(out, /data-bit="29" data-bit-name="Data download via SMS-CB" checked/); assert.match(out, /data-bit="29" data-bit-name="Data download via SMS-CB" checked/);
// the whole UST table is rendered (146 services) // the whole UST table is rendered (150 services)
assert.strictEqual((out.match(/data-bit="/g) || []).length, 146); assert.strictEqual((out.match(/data-bit="/g) || []).length, 150);
// ACC: 15 editable classes (10 is always written 0), 15 and 0 checked // ACC: 15 editable classes (10 is always written 0), 15 and 0 checked
const acc = efDecEditHtml(efDecEditModel('EF.ACC', '6f78', { kind: 'transparent', data: ACC_ONE }), ACC_ONE); const acc = efDecEditHtml(efDecEditModel('EF.ACC', '6f78', { kind: 'transparent', data: ACC_ONE }), ACC_ONE);
assert.strictEqual((acc.match(/data-bit="/g) || []).length, 15); assert.strictEqual((acc.match(/data-bit="/g) || []).length, 15);
@@ -712,6 +741,20 @@ test('the bits field renders a filtered checkbox grid and collects {n,name}', ()
] }); ] });
}); });
test('the service grids link file-backed rows to the file tree filter', () => {
const ust = efDecEditHtml(efDecEditModel('EF.UST', '6f38', { kind: 'transparent', data: UST_ONE }), UST_ONE);
// service 2 = FDN (files), service 99 = URI support by UICC (capability)
assert.match(ust, /pysimFsSetFilterToService\('ust', 2\)/);
assert.ok(!/pysimFsSetFilterToService\('ust', 99\)/.test(ust));
const sst = efDecEditHtml(efDecEditModel('EF.SST', '6f38', { kind: 'transparent', data: '4C' }, '7F20'), '4C');
// service 4 = SMS (files), service 1 = CHV1 disable function (capability)
assert.match(sst, /pysimFsSetFilterToService\('sst', 4\)/);
assert.ok(!/pysimFsSetFilterToService\('sst', 1\)/.test(sst));
// EF.ACC's bits grid is not a service table - no cross-link at all
const acc = efDecEditHtml(efDecEditModel('EF.ACC', '6f78', { kind: 'transparent', data: ACC_ONE }), ACC_ONE);
assert.ok(!/pysimFsSetFilterToService/.test(acc));
});
test('the bits filter hides the non-matching rows', () => { test('the bits filter hides the non-matching rows', () => {
const rows = [ const rows = [
{ textContent: '1 Local Phone Book', style: {} }, { textContent: '1 Local Phone Book', style: {} },
@@ -724,6 +767,91 @@ test('the bits filter hides the non-matching rows', () => {
assert.strictEqual(rows[1].style.display, ''); assert.strictEqual(rows[1].style.display, '');
}); });
test('the EF.SST grid renders one tri-state select per service', () => {
// 4C: service 2 on (11), service 4 off (10), the rest of the pairs deleted
const model = efDecEditModel('EF.SST', '6f38', { kind: 'transparent', data: '4C' }, '7F20');
assert.deepStrictEqual(model.values.services, [
{ n: 2, name: 'Abbreviated Dialling Numbers (ADN)', allocated: true, activated: true },
{ n: 4, name: 'Short Message Storage (SMS)', allocated: true, activated: false },
]);
const out = efDecEditHtml(model, '4C');
assert.match(out, /class="pysim-dec-bits /);
assert.match(out, /pysim-dec-bits-filter/);
assert.match(out, /data-bit2="2" data-bit2-name="Abbreviated Dialling Numbers \(ADN\)"/);
// the whole SST table is rendered (59 services, RFU 8/20/50 included)
assert.strictEqual((out.match(/data-bit2="/g) || []).length, 59);
assert.match(out, /data-bit2="8" data-bit2-name="RFU"/);
assert.match(out, /data-bit2="20" data-bit2-name="RFU"/);
assert.match(out, /data-bit2="50" data-bit2-name="Reserved and shall be ignored"/);
// each select is preselected from the decoded state, an absent pair is deleted
const sel = n => {
const m = new RegExp('<select data-bit2="' + n + '"[^>]*>([\\s\\S]*?)</select>').exec(out);
assert.ok(m, 'select for service ' + n);
return m[1];
};
assert.match(sel(2), /<option value="on" selected>On<\/option>/);
assert.match(sel(4), /<option value="off" selected>Off<\/option>/);
assert.match(sel(1), /<option value="deleted" selected>Deleted<\/option>/);
assert.match(sel(59), /<option value="deleted" selected>Deleted<\/option>/);
for (const n of [2, 4, 1, 59]) {
assert.match(sel(n), />Deleted<\/option>/);
assert.match(sel(n), />Off<\/option>/);
assert.match(sel(n), />On<\/option>/);
}
// an all-FF file preselects every modelled service 'on'
const ff = efDecEditHtml(efDecEditModel('EF.SST', '6f38',
{ kind: 'transparent', data: 'FF'.repeat(15) }, '7F20'), 'FF'.repeat(15));
assert.strictEqual((ff.match(/<option value="on" selected>/g) || []).length, 59);
assert.ok(!/<option value="(deleted|off)" selected>/.test(ff));
});
test('the EF.SST collector yields {n, name, allocated, activated}', () => {
const row = (n, name, state) => ({
value: state,
getAttribute: k => k === 'data-bit2' ? String(n) : name,
});
const root = {
querySelectorAll: sel => sel === '[data-bit2]' ? [
row(2, 'Abbreviated Dialling Numbers (ADN)', 'on'),
row(1, 'CHV1 disable function', 'deleted'),
row(4, 'Short Message Storage (SMS)', 'off'),
] : [],
};
assert.deepStrictEqual(efDecCollectValues(root, EF_SST_FIELDS), { services: [
{ n: 2, name: 'Abbreviated Dialling Numbers (ADN)', allocated: true, activated: true },
{ n: 1, name: 'CHV1 disable function', allocated: false, activated: false },
{ n: 4, name: 'Short Message Storage (SMS)', allocated: true, activated: false },
] });
// an untouched form re-encodes to the original bytes
const dec = efDecSst(efBytes('4C'));
const values = efDecCollectValues({
querySelectorAll: () => [2, 4].map(n => row(n, EF_SST_SERVICES[n],
dec.services.find(s => s.n === n).activated ? 'on' : 'off')),
}, EF_SST_FIELDS);
assert.strictEqual(efFindEncoder('EF.SST', '6f38', '7F20').enc(values, '4C'), '4C');
// the EF.SST field is the tri-state kind, the 1-bit tables are untouched
assert.strictEqual(EF_SST_FIELDS[0].kind, 'bits2');
assert.strictEqual(EF_UST_FIELDS[0].kind, 'bits');
assert.strictEqual(EF_EST_FIELDS[0].kind, 'bits');
});
test('the tri-state grid filter matches the row label, not the select options', () => {
const rows = [
{ getAttribute: k => k === 'data-label' ? '1 CHV1 disable function' : '', style: {} },
{ getAttribute: k => k === 'data-label' ? '4 Short Message Storage (SMS)' : '', style: {} },
];
const box = { querySelectorAll: sel => sel === '.pysim-dec-bit2-row' ? rows : [] };
const input = { value: 'sms', closest: sel => sel === '.pysim-dec-bits' ? box : null };
pysimFsDecBits2Filter(input);
assert.strictEqual(rows[0].style.display, 'none');
assert.strictEqual(rows[1].style.display, '');
// 'off' is a state option of every row, never part of a service name
pysimFsDecBits2Filter({ value: 'off', closest: () => box });
assert.strictEqual(rows[0].style.display, 'none');
assert.strictEqual(rows[1].style.display, 'none');
pysimFsDecBits2Filter(null);
});
test('the select keeps an unknown current value as an option', () => { test('the select keeps an unknown current value as an option', () => {
const model = efDecEditModel('EF.PHASE', '6fae', { kind: 'transparent', data: '0F' }); const model = efDecEditModel('EF.PHASE', '6fae', { kind: 'transparent', data: '0F' });
assert.deepStrictEqual(model.values, { phase_hex: '0F' }); assert.deepStrictEqual(model.values, { phase_hex: '0F' });
+12 -5
View File
@@ -29,10 +29,12 @@ let code = 'var pysimFsDecodedMode = false;\n'
+ 'var pysimFsSelected = null;\n' + 'var pysimFsSelected = null;\n'
+ 'var pysimFsTreeRoot = null;\n'; + 'var pysimFsTreeRoot = null;\n';
for (const fn of ['pysimFsSetMode', 'pysimFsPillsEnabled', 'pysimFsEdit', for (const fn of ['pysimFsSetMode', 'pysimFsPillsEnabled', 'pysimFsEdit',
'pysimFsCancel', 'pysimFsResetEdit', 'pysimFsFindNode', 'pysimFsHasDecoder', 'pysimFsCancel', 'pysimFsResetEdit', 'pysimFsFindNodeByPath', 'pysimCustomNormPath',
'pysimFsNodePath', 'efFidFromPath', 'efDfFromPath', 'pysimFsHasDecoder',
'pysimFsSyncContentVisibility']) { 'pysimFsSyncContentVisibility']) {
code += extractFunc(html, fn) + '\n'; code += extractFunc(html, fn) + '\n';
} }
code += html.match(/const EF_ADF_RIDS = \[[\s\S]*?\n\];/)[0].replace('const ', 'var ') + '\n';
eval(code); eval(code);
let events = []; let events = [];
@@ -83,8 +85,9 @@ function setup(opts) {
pysimFsDecodedMode = !!opts.decoded; pysimFsDecodedMode = !!opts.decoded;
pysimFsEditMode = false; pysimFsEditMode = false;
pysimFsEditData = null; pysimFsEditData = null;
pysimFsSelected = 'EF.IMSI'; pysimFsSelected = 'MF/6F07';
pysimFsTreeRoot = { name: 'MF', fid: '3f00', children: [{ name: 'EF.IMSI', fid: '6f07' }] }; pysimFsTreeRoot = { name: 'MF', fid: '3f00', children: [] };
pysimFsTreeRoot.children.push({ name: 'EF.IMSI', fid: '6f07', parent: pysimFsTreeRoot });
globalThis.efFindDecoder = (name, fid) => { globalThis.efFindDecoder = (name, fid) => {
const n = (name || '').toUpperCase(); const n = (name || '').toUpperCase();
const f = (fid || '').toLowerCase(); const f = (fid || '').toLowerCase();
@@ -205,12 +208,16 @@ test('Read decoded is ignored when the file has no decoder', () => {
assert.ok(!h.pill('dec').classList.contains('bg-blue-600')); assert.ok(!h.pill('dec').classList.contains('bg-blue-600'));
}); });
test('pysimFsHasDecoder resolves the selected file by name or FID', () => { test('pysimFsHasDecoder resolves the selected file by its path or FID', () => {
setup(); setup();
assert.strictEqual(pysimFsHasDecoder(), true, 'EF.IMSI is in the tree'); assert.strictEqual(pysimFsHasDecoder(), true, 'EF.IMSI is in the tree');
// alias name: the decoder is matched by the FID fallback // alias name: the decoder is matched by the FID fallback
pysimFsTreeRoot = { name: 'MF', children: [{ name: 'EF.ALIAS', fid: '6f07' }] }; pysimFsTreeRoot = { name: 'MF', children: [{ name: 'EF.ALIAS', fid: '6f07' }] };
pysimFsSelected = 'EF.ALIAS'; pysimFsTreeRoot.children[0].parent = pysimFsTreeRoot;
pysimFsSelected = 'MF/6F07';
assert.strictEqual(pysimFsHasDecoder(), true);
// a bare FID selection (an API caller) resolves by the FID fallback
pysimFsSelected = '6F07';
assert.strictEqual(pysimFsHasDecoder(), true); assert.strictEqual(pysimFsHasDecoder(), true);
// unknown file -> no decoder // unknown file -> no decoder
globalThis.efFindDecoder = () => null; globalThis.efFindDecoder = () => null;
+665
View File
@@ -0,0 +1,665 @@
const { test } = require('node:test');
const assert = require('node:assert');
const fs = require('node:fs');
const path = require('node:path');
const html = fs.readFileSync(path.join(__dirname, '..', 'index.html'), 'utf8');
function extractFunc(src, name) {
const re = new RegExp('(?:async\\s+)?function\\s+' + name + '\\s*\\([^)]*\\)\\s*\\{');
const m = re.exec(src);
if (!m) throw new Error('function ' + name + ' not found');
let i = m.index + m[0].length - 1;
let depth = 0;
for (; i < src.length; i++) {
if (src[i] === '{') depth++;
else if (src[i] === '}') {
depth--;
if (depth === 0) break;
}
}
return src.slice(m.index, i + 1);
}
function extractConst(src, name) {
const m = new RegExp('const ' + name + ' = ').exec(src);
if (!m) throw new Error('const ' + name + ' not found');
const begin = m.index + m[0].length;
const opener = src[begin];
const closer = opener === '[' ? ']' : (opener === '{' ? '}' : null);
if (!closer) throw new Error('unsupported const ' + name);
let depth = 0, i = begin;
for (; i < src.length; i++) {
if (src[i] === opener) depth++;
else if (src[i] === closer) {
depth--;
if (depth === 0) { i++; break; }
}
}
return 'var ' + name + ' = ' + src.slice(begin, i) + ';\n';
}
function extractStrConst(src, name) {
const m = new RegExp("const " + name + " = '([^']*)';").exec(src);
if (!m) throw new Error('const ' + name + ' not found');
return 'var ' + name + " = '" + m[1] + "';\n";
}
// module state the extracted functions close over
let pysimFsFilter = new Set(['all']);
let _fsFilterIndex = null;
// The shipped standard list stands in for the fetched one (uicc_files.json is
// what uiccStdFilesLoad caches; the filter reads its entries synchronously).
const stdAsset = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'uicc_files.json'), 'utf8'));
let _uiccStdFiles = stdAsset.files;
let _fsFilterPathIndex = null;
let _fsFilterPathIndexSrc = null;
let _fsFilterPaths = null;
let _fsFilterPathsKey = null;
let pysimCustomFiles = [];
const _store = {};
let _treeRenders = 0;
let pysimFsTreeRoot = null; // expand-all walks this; null = nothing to expand
let pysimFsLoadChildren = async node => { if (!node.children) node.children = []; };
globalThis.localStorage = {
getItem: k => (Object.prototype.hasOwnProperty.call(_store, k) ? _store[k] : null),
setItem: (k, v) => { _store[k] = String(v); },
removeItem: k => { delete _store[k]; },
};
globalThis.esc = s => String(s);
globalThis.t = k => k;
globalThis.pysimFsRenderTree = () => { _treeRenders++; };
// The filter's two DOM elements: the collapsed row's button and the hidden
// menu it expands into. The menu only needs a settable innerHTML (the
// render never reads the DOM back) and a classList for the open/closed state.
function fakeBtn() {
const classes = new Set();
return {
textContent: '',
attrs: {},
classes,
classList: {
contains: c => classes.has(c),
toggle(c, force) {
const on = force === undefined ? !classes.has(c) : !!force;
if (on) classes.add(c); else classes.delete(c);
return on;
},
},
setAttribute(k, v) { this.attrs[k] = String(v); },
getAttribute(k) { return this.attrs[k]; },
};
}
function fakeMenu() {
const classes = new Set(['hidden']);
return {
_html: '',
classes,
classList: {
contains: c => classes.has(c),
toggle(c, force) {
const on = force === undefined ? !classes.has(c) : !!force;
if (on) classes.add(c); else classes.delete(c);
return on;
},
},
get innerHTML() { return this._html; },
set innerHTML(v) { this._html = v; },
};
}
const dom = { menu: null, btn: null, clear: null };
globalThis.document = {
getElementById: id => (id === 'pysim-fs-filter-menu' ? dom.menu
: (id === 'pysim-fs-filter-btn' ? dom.btn
: (id === 'pysim-fs-filter-clear' ? dom.clear : null))),
};
let code = '';
for (const f of ['fsFilterServiceOptions', 'fsFilterIndexBuild', 'fsFilterIndex',
'fsFilterPathIndexBuild', 'fsFilterPathIndex', 'fsFilterCustomPaths',
'fsFilterPathsKeyOf', 'pysimFsFilterPathsInvalidate', 'pysimFsFilterMatchingPaths',
'pysimFsFilterMatch', 'pysimFsFilterShow', 'pysimFsFilterLabel',
'pysimFsFilterSummary', 'pysimFsFilterRender', 'pysimFsFilterToggle',
'pysimFsFilterSetEntry', 'pysimFsFilterClear', 'pysimFsFilterRestore', 'pysimFsNodePath',
'pysimFsExpandAll', 'pysimFsCollapseAll', 'pysimFsAutoExpandForFilter']) {
code += extractFunc(html, f) + '\n';
}
for (const c of ['FS_FILTER_GROUPS', 'FS_FILTER_GROUP_ORDER', 'SERVICES_FS_MAP']) code += extractConst(html, c);
code += extractStrConst(html, 'FS_FILTER_ALL');
code += extractStrConst(html, 'FS_FILTER_CUSTOM');
eval(code);
function withFilter(ids, fn) {
const saved = pysimFsFilter;
pysimFsFilter = new Set(ids);
try { return fn(); } finally { pysimFsFilter = saved; }
}
// The checkbox rows the render wrote: [{id, checked}] in document order.
function renderedBoxes() {
const out = [];
for (const m of (dom.menu ? dom.menu.innerHTML : '').matchAll(
/<input type="checkbox" data-filter="([^"]*)"( checked)?/g)) {
out.push({ id: m[1], checked: !!m[2] });
}
return out;
}
const box = id => renderedBoxes().find(b => b.id === id);
const fileBackedFeatures = () => Object.keys(SERVICES_FS_MAP).filter(n => SERVICES_FS_MAP[n].length);
test('the file tree has a collapsed filter dropdown next to the sort pills', () => {
assert.match(html, /id="pysim-fs-filter-btn"[^>]*aria-haspopup="true"[^>]*aria-expanded="false"[^>]*onclick="pysimFsFilterToggle\(\)"/);
assert.match(html, /id="pysim-fs-filter-menu"[^>]*role="group"[^>]*class="hidden absolute/);
assert.match(html, /data-l10n="Filter:"/);
// the multiselect (and its onchange handler) is gone
assert.ok(!/id="pysim-fs-filter"/.test(html));
assert.ok(!/pysimFsSetFilter\(/.test(html));
});
test('the ✕ next to the selector resets a non-All filter to All', () => {
// markup: a hidden red ✕ that clears the filter
assert.match(html, /id="pysim-fs-filter-clear"[^>]*onclick="pysimFsFilterClear\(\)"/);
assert.match(html, /id="pysim-fs-filter-clear"[^>]*data-l10n-title="Reset filter"/);
// the clear handler resets to All
const fn = extractFunc(html, 'pysimFsFilterClear');
assert.ok(fn.indexOf('pysimFsFilterSetEntry(FS_FILTER_ALL, true)') > 0, fn);
// the render shows it only when a non-All filter is active
dom.menu = fakeMenu();
dom.btn = fakeBtn();
dom.clear = fakeBtn();
withFilter(['plmn-selection'], () => {
pysimFsFilterRender();
assert.strictEqual(dom.clear.classes.has('hidden'), false);
assert.strictEqual(dom.clear.attrs['aria-label'], 'Reset filter');
});
withFilter(['all'], () => {
pysimFsFilterRender();
assert.strictEqual(dom.clear.classes.has('hidden'), true);
});
// the Russian entry exists
const dict = html.match(/const LANG_RU = \{([\s\S]*?)\n\};/)[1];
assert.ok(dict.includes("'Reset filter':"), 'missing Russian entry');
});
test('an outside click or Escape closes the menu', () => {
const click = html.match(/document\.addEventListener\('click', e => \{([\s\S]{0,600}?)\n\}\);/);
assert.ok(click, 'no document click listener');
assert.match(click[1], /#pysim-fs-filter-btn/);
assert.match(click[1], /#pysim-fs-filter-menu/);
assert.match(click[1], /pysimFsFilterToggle\(false\)/);
const key = html.match(/document\.addEventListener\('keydown', e => \{([\s\S]{0,300}?)\n\}\);/);
assert.ok(key, 'no document keydown listener');
assert.match(key[1], /Escape/);
assert.match(key[1], /pysimFsFilterToggle\(false\)/);
});
test('the four category groups name their files', () => {
assert.deepStrictEqual(FS_FILTER_GROUPS.map(g => g.id),
['identifiers', 'plmn-selection', 'provider-names', 'wlan']);
const files = id => new Set(FS_FILTER_GROUPS.find(g => g.id === id).files);
assert.deepStrictEqual([...files('identifiers')],
['EF.ICCID', 'EF.IMSI', 'EF.MSISDN', 'EF.IMPI', 'EF.IMPU', 'EF.SUPI_NAI', 'EF.GID1', 'EF.GID2']);
assert.deepStrictEqual([...files('plmn-selection')],
['EF.PLMNsel', 'EF.PLMNwAcT', 'EF.OPLMNwAcT', 'EF.HPLMNwAcT', 'EF.EHPLMN', 'EF.FPLMN',
'EF.HPPLMN', 'EF.LRPLMNSI', 'EF.EHPLMNPI', 'EF.MCHPPLMN']);
assert.deepStrictEqual([...files('provider-names')],
['EF.SPN', 'EF.PNN', 'EF.OPL', 'EF.OPL5G', 'EF.SPDI', 'EF.SPNI', 'EF.PNNI', 'EF.CNL']);
assert.deepStrictEqual([...files('wlan')],
['EF.UPLMNWLAN', 'EF.OPLMNWLAN', 'EF.WRI', 'EF.HWSIDL', 'EF.WEHPLMNPI', 'EF.WHPI',
'EF.WLRPLMN', 'EF.HPLMNDAI']);
assert.strictEqual(FS_FILTER_ALL, 'all');
assert.strictEqual(FS_FILTER_CUSTOM, 'custom');
});
test('the per-service options come from the file-backed features only', () => {
const services = fsFilterServiceOptions();
const features = fileBackedFeatures();
assert.ok(features.length > 50, 'expected the cross-reference to be available');
assert.strictEqual(services.length, features.length);
assert.ok(services.every(o => o.id === 'svc:' + o.label));
const spn = services.find(o => o.id === 'svc:Service Provider Name (SPN)');
assert.deepStrictEqual(spn.files, ['EF.SPN']);
// capability/RFU rows carry no files, so they are not offered
assert.ok(!services.some(o => o.id === 'svc:Proactive SIM' || o.id === 'svc:CHV1 disable function'));
});
test('the inverted index keys the uppercased names and merges memberships', () => {
const idx = fsFilterIndexBuild(FS_FILTER_GROUPS, fsFilterServiceOptions());
const ids = name => Array.from(idx.get(name.toUpperCase()) || []);
assert.ok(ids('EF.ICCID').includes('identifiers'));
assert.ok(ids('EF.PLMNwAcT').includes('plmn-selection'));
assert.ok(ids('EF.SPN').includes('provider-names'));
assert.ok(ids('EF.SPN').includes('svc:Service Provider Name (SPN)'));
// SMS/CB files are covered by their per-service options, not a category
assert.ok(ids('EF.SMS').includes('svc:Short Message Storage (SMS)'));
assert.ok(ids('EF.CBMI').includes('svc:Cell Broadcast Message Identifier (CBMI)'));
// EF.PSISMSC is neither a category nor a file-backed service, so nothing
// maps it (pySim models no such file, and no service lists it either)
assert.ok(!idx.has('EF.PSISMSC'));
assert.ok(!fileBackedFeatures().some(n => (SERVICES_FS_MAP[n] || []).includes('EF.PSISMSC')));
});
test('a file matches the selected categories, case-insensitively', () => {
withFilter(['identifiers'], () => {
assert.strictEqual(pysimFsFilterMatch({ name: 'EF.ICCID', fid: '2FE2' }), true);
assert.strictEqual(pysimFsFilterMatch({ name: 'ef.iccid', fid: '2FE2' }), true);
assert.strictEqual(pysimFsFilterMatch({ name: 'EF.SPN', fid: '6F46' }), false);
assert.strictEqual(pysimFsFilterMatch({ name: 'DF.GSM', fid: '7F20', isDir: true }), false);
});
withFilter(['svc:Service Provider Name (SPN)'], () => {
assert.strictEqual(pysimFsFilterMatch({ name: 'EF.SPN' }), true);
assert.strictEqual(pysimFsFilterMatch({ name: 'EF.PNN' }), false);
});
withFilter(['identifiers', 'svc:Short Message Status Reports (SMSR)'], () => {
assert.strictEqual(pysimFsFilterMatch({ name: 'EF.IMSI' }), true);
assert.strictEqual(pysimFsFilterMatch({ name: 'EF.SMSR' }), true);
assert.strictEqual(pysimFsFilterMatch({ name: 'EF.SPN' }), false);
});
withFilter(['svc:Short Message Storage (SMS)'], () => {
assert.strictEqual(pysimFsFilterMatch({ name: 'EF.ECC' }), false);
});
});
test('an empty filter and "all" show everything', () => {
withFilter([], () => {
assert.strictEqual(pysimFsFilterMatch({ name: 'EF.ADN' }), true);
assert.strictEqual(pysimFsFilterMatch({ name: 'DF.GSM', isDir: true }), true);
assert.strictEqual(pysimFsFilterMatch({ name: undefined }), true);
});
withFilter(['all'], () => {
assert.strictEqual(pysimFsFilterMatch({ name: 'EF.ADN' }), true);
assert.strictEqual(pysimFsFilterMatch({ name: 'EF.ZZZ' }), true);
});
// a stale multi-selection that still names all also shows everything
withFilter(['all', 'identifiers'], () => {
assert.strictEqual(pysimFsFilterMatch({ name: 'EF.ECC' }), true);
});
});
test('a capability-only service selection matches nothing', () => {
withFilter(['svc:Proactive SIM'], () => {
assert.strictEqual(pysimFsFilterMatch({ name: 'EF.ADN' }), false);
assert.strictEqual(pysimFsFilterMatch({ name: 'EF.UST' }), false);
});
});
test('"custom files" matches only the injected custom nodes', () => {
withFilter(['custom'], () => {
assert.strictEqual(pysimFsFilterMatch({ name: 'EF.ADN', custom: false }), false);
assert.strictEqual(pysimFsFilterMatch({ name: 'MyCustomEF', custom: true }), true);
assert.strictEqual(pysimFsFilterMatch({ name: 'EF.ICCID', custom: true }), true);
});
withFilter(['custom', 'identifiers'], () => {
assert.strictEqual(pysimFsFilterMatch({ name: 'EF.ICCID', custom: false }), true);
assert.strictEqual(pysimFsFilterMatch({ name: 'MyCustomEF', custom: true }), true);
assert.strictEqual(pysimFsFilterMatch({ name: 'EF.SPN', custom: false }), false);
});
});
// A small tree with real parent links (pysimFsNodePath walks them): MF with
// DF.TELECOM (EF.MSISDN), DF.GSM (EF.SPN, EF.ECC) and ADF.USIM (DF.WLAN).
function fsTree() {
const root = { name: 'MF', fid: '3F00', isDir: true, children: [], parent: null, expanded: true };
const add = (parent, name, fid, isDir) => {
const n = { name, fid, isDir: !!isDir, children: isDir ? [] : null, parent, expanded: true };
parent.children.push(n);
return n;
};
const telecom = add(root, 'DF.TELECOM', '7F10', true);
const msisdn = add(telecom, 'EF.MSISDN', '6F40', false);
const gsm = add(root, 'DF.GSM', '7F20', true);
const spn = add(gsm, 'EF.SPN', '6F46', false);
const ecc = add(gsm, 'EF.ECC', '6FB7', false);
const usim = add(root, 'ADF.USIM', '7FFF', true);
const wlan = add(usim, 'DF.WLAN', '5F40', true);
return { root, telecom, msisdn, gsm, spn, ecc, usim, wlan };
}
test('a directory shows only on the path of a file the filter selects', () => {
const t = fsTree();
withFilter(['provider-names'], () => {
// the root always stays; DF.GSM holds EF.SPN (MF/7F20/6F46)
assert.strictEqual(pysimFsFilterShow(t.root), true);
assert.strictEqual(pysimFsFilterShow(t.gsm), true);
// ... and ADF.USIM holds its own EF.SPN copy
assert.strictEqual(pysimFsFilterShow(t.usim), true);
// neither DF.TELECOM nor DF.WLAN holds a provider-name file
assert.strictEqual(pysimFsFilterShow(t.telecom), false);
assert.strictEqual(pysimFsFilterShow(t.wlan), false);
// files keep their name rule
assert.strictEqual(pysimFsFilterShow(t.spn), true);
assert.strictEqual(pysimFsFilterShow(t.ecc), false);
});
withFilter(['identifiers'], () => {
// EF.MSISDN is in DF.TELECOM, EF.IMSI/EF.GID1/2 in DF.GSM
assert.strictEqual(pysimFsFilterShow(t.telecom), true);
assert.strictEqual(pysimFsFilterShow(t.msisdn), true);
assert.strictEqual(pysimFsFilterShow(t.gsm), true);
assert.strictEqual(pysimFsFilterShow(t.usim), true); // EF.IMSI/EF.MSISDN copies
assert.strictEqual(pysimFsFilterShow(t.wlan), false);
});
withFilter(['svc:Cell Broadcast Message Identifier (CBMI)'], () => {
assert.strictEqual(pysimFsFilterShow(t.gsm), true); // EF.CBMI
assert.strictEqual(pysimFsFilterShow(t.telecom), false);
assert.strictEqual(pysimFsFilterShow(t.wlan), false);
});
});
test('a directory named by the selection itself shows', () => {
const t = fsTree();
// the GPRS feature names DF.GSM (its SST bit covers the DF, not a file)
withFilter(['svc:GPRS'], () => {
assert.strictEqual(pysimFsFilterMatch(t.gsm), true);
assert.strictEqual(pysimFsFilterShow(t.gsm), true);
assert.strictEqual(pysimFsFilterShow(t.telecom), false);
});
});
test('"all" and an empty selection show every node', () => {
const t = fsTree();
for (const ids of [['all'], []]) {
withFilter(ids, () => {
assert.strictEqual(pysimFsFilterShow(t.root), true);
assert.strictEqual(pysimFsFilterShow(t.telecom), true);
assert.strictEqual(pysimFsFilterShow(t.wlan), true);
assert.strictEqual(pysimFsFilterShow(t.ecc), true);
});
}
});
test('directories are kept until the standard list is available', () => {
const t = fsTree();
const saved = _uiccStdFiles;
_uiccStdFiles = null;
try {
withFilter(['provider-names'], () => {
assert.strictEqual(pysimFsFilterMatchingPaths(), null);
// the ancestors cannot be judged, so nothing vanishes
assert.strictEqual(pysimFsFilterShow(t.telecom), true);
assert.strictEqual(pysimFsFilterShow(t.wlan), true);
// files still obey the name rule
assert.strictEqual(pysimFsFilterShow(t.ecc), false);
});
} finally {
_uiccStdFiles = saved;
}
});
test('the standard list gives a name every tree it lives in', () => {
const idx = fsFilterPathIndexBuild(_uiccStdFiles);
assert.deepStrictEqual(idx.get('EF.SPN').slice().sort(),
['ADF.USIM-NON-IMSI/6F46', 'ADF.USIM/6F46', 'MF/7F20/6F46']);
assert.deepStrictEqual(idx.get('EF.ICCID'), ['MF/2FE2']);
// the index is cached for the loaded list
assert.strictEqual(fsFilterPathIndex(), fsFilterPathIndex());
});
test('the selection resolves to the paths of its files', () => {
withFilter(['svc:Cell Broadcast Message Identifier (CBMI)'], () => {
const paths = pysimFsFilterMatchingPaths();
assert.ok(paths instanceof Set, 'expected a Set');
assert.deepStrictEqual(Array.from(paths).sort(), [
'ADF.USIM-NON-IMSI/6F45', 'ADF.USIM/6F45', 'MF/7F20/6F45',
]);
// cached for the same selection
assert.strictEqual(pysimFsFilterMatchingPaths(), paths);
});
withFilter(['custom'], () => {
assert.deepStrictEqual(Array.from(pysimFsFilterMatchingPaths()), []);
});
});
test('"custom files" keeps the directories that hold them', () => {
const t = fsTree();
const custom = { name: 'EF.MYFILE', fid: '6F99', isDir: false, children: null, parent: t.gsm, custom: true };
t.gsm.children.push(custom);
pysimCustomFiles = [{ path: 'MF/7F20/6F99', name: 'EF.MYFILE', kind: 'ef' }];
try {
withFilter(['custom'], () => {
// custom files are outside the standard list: their own paths are
// the ancestry, so the holding DF stays
assert.strictEqual(pysimFsFilterShow(custom), true);
assert.strictEqual(pysimFsFilterShow(t.gsm), true);
assert.strictEqual(pysimFsFilterShow(t.root), true);
assert.strictEqual(pysimFsFilterShow(t.telecom), false);
});
} finally {
pysimCustomFiles = [];
}
});
test('the menu lists all, custom, the three headings and the services', () => {
pysimFsFilter = new Set(['all']);
dom.menu = fakeMenu();
dom.btn = fakeBtn();
pysimFsFilterRender();
const out = dom.menu.innerHTML;
assert.match(out, /^<label[^>]*><input type="checkbox" data-filter="all" checked/);
assert.match(out, /data-filter="custom"/);
assert.match(out, /<div class="[^"]*">Identifiers<\/div>/);
assert.match(out, /<div class="[^"]*">PLMN<\/div>/);
assert.match(out, /<div class="[^"]*">Services<\/div>/);
// the headings come in the declared order
assert.ok(out.indexOf('>Identifiers</div>') < out.indexOf('>PLMN</div>'));
assert.ok(out.indexOf('>PLMN</div>') < out.indexOf('>Services</div>'));
// the categories sit under their headings
assert.ok(out.indexOf('data-filter="provider-names"') < out.indexOf('>PLMN</div>'));
assert.ok(out.indexOf('data-filter="plmn-selection"') > out.indexOf('>PLMN</div>'));
// the per-service options sit under the Services heading
assert.ok(out.indexOf('data-filter="svc:') > out.indexOf('>Services</div>'));
const boxes = renderedBoxes();
assert.strictEqual(boxes.length, 2 + FS_FILTER_GROUPS.length + fsFilterServiceOptions().length);
assert.strictEqual(boxes[0].id, 'all');
assert.strictEqual(boxes[0].checked, true);
assert.strictEqual(box('custom').checked, false);
assert.ok(boxes.some(b => b.id === 'svc:Service Provider Name (SPN)'));
assert.ok(!boxes.some(b => b.id === 'svc:Proactive SIM'));
// the row shows the summary, and the checkbox calls the setter directly
assert.strictEqual(dom.btn.textContent, 'All');
assert.match(out, /onchange="pysimFsFilterSetEntry\(this\.dataset\.filter, this\.checked\)"/);
});
test('the button turns blue when a filter is active and back for all', () => {
dom.menu = fakeMenu();
dom.btn = fakeBtn();
pysimFsFilter = new Set(['identifiers']);
pysimFsFilterRender();
assert.strictEqual(dom.btn.classes.has('bg-blue-600'), true);
assert.strictEqual(dom.btn.classes.has('text-white'), true);
assert.strictEqual(dom.btn.classes.has('text-gray-700'), false);
pysimFsFilter = new Set(['all']);
pysimFsFilterRender();
assert.strictEqual(dom.btn.classes.has('bg-blue-600'), false);
assert.strictEqual(dom.btn.classes.has('text-white'), false);
assert.strictEqual(dom.btn.classes.has('text-gray-700'), true);
});
test('expand all and collapse all walk the directory tree, skipping absent DFs', async () => {
const telecom = { name: 'DF.TELECOM', fid: '7F10', isDir: true, exists: true, expanded: false,
parent: null, children: [
{ name: 'DF.PHONEBOOK', fid: '5F3A', isDir: true, exists: true, expanded: false, parent: null, children: null },
] };
const gsm = { name: 'DF.GSM', fid: '7F20', isDir: true, exists: false, expanded: false, parent: null, children: null };
pysimFsTreeRoot = { name: 'MF', fid: '3F00', isDir: true, exists: true, expanded: true, parent: null,
children: [gsm, telecom] };
let loads = 0;
pysimFsLoadChildren = async node => { if (!node.children) { node.children = []; } loads++; };
try {
_treeRenders = 0;
await pysimFsExpandAll();
assert.strictEqual(pysimFsTreeRoot.expanded, true);
assert.strictEqual(gsm.expanded, false, 'an absent DF is not expanded');
assert.strictEqual(telecom.expanded, true);
assert.strictEqual(telecom.children[0].expanded, true);
assert.ok(loads > 0, 'lazy children are loaded on demand');
assert.strictEqual(_treeRenders, 1);
pysimFsCollapseAll();
assert.strictEqual(pysimFsTreeRoot.expanded, false);
assert.strictEqual(telecom.expanded, false);
assert.strictEqual(telecom.children[0].expanded, false);
} finally {
pysimFsTreeRoot = null;
}
});
test('auto-expand fires only for a non-"all" filter', () => {
// expand-all (with a null tree) still re-renders once; pin the branch
pysimFsFilter = new Set(['all']);
_treeRenders = 0;
pysimFsAutoExpandForFilter();
assert.strictEqual(_treeRenders, 0, '"all" does not expand');
pysimFsFilter = new Set(['identifiers']);
pysimFsAutoExpandForFilter();
assert.strictEqual(_treeRenders, 1, 'a category expands the tree');
});
test('the menu marks the saved selection', () => {
pysimFsFilter = new Set(['svc:Short Message Storage (SMS)', 'identifiers']);
dom.menu = fakeMenu();
dom.btn = fakeBtn();
pysimFsFilterRender();
assert.deepStrictEqual(renderedBoxes().filter(b => b.checked).map(b => b.id),
['identifiers', 'svc:Short Message Storage (SMS)']);
assert.strictEqual(box('all').checked, false);
assert.strictEqual(dom.btn.textContent, 'Selected: 2');
});
test('the button summary names the single entry or counts the selection', () => {
const summary = ids => withFilter(ids, () => pysimFsFilterSummary());
assert.strictEqual(summary(['all']), 'All');
// an empty selection also means everything
assert.strictEqual(summary([]), 'All');
assert.strictEqual(summary(['all', 'plmn-selection']), 'All');
assert.strictEqual(summary(['custom']), 'Custom files');
assert.strictEqual(summary(['plmn-selection']), 'PLMN selection');
assert.strictEqual(summary(['svc:Service Provider Name (SPN)']), 'Service Provider Name (SPN)');
assert.strictEqual(summary(['plmn-selection', 'wlan']), 'Selected: 2');
// an unknown id falls back to itself
assert.strictEqual(summary(['providers']), 'providers');
});
test('the menu toggles open and closed', () => {
dom.menu = fakeMenu();
dom.btn = fakeBtn();
assert.strictEqual(dom.menu.classList.contains('hidden'), true);
pysimFsFilterToggle();
assert.strictEqual(dom.menu.classList.contains('hidden'), false);
assert.strictEqual(dom.btn.getAttribute('aria-expanded'), 'true');
pysimFsFilterToggle();
assert.strictEqual(dom.menu.classList.contains('hidden'), true);
assert.strictEqual(dom.btn.getAttribute('aria-expanded'), 'false');
// the forced forms (the outside click / Escape handlers pass false)
pysimFsFilterToggle(true);
assert.strictEqual(dom.menu.classList.contains('hidden'), false);
pysimFsFilterToggle(false);
assert.strictEqual(dom.menu.classList.contains('hidden'), true);
});
test('the checkbox rows keep "all" exclusive and persist the selection', () => {
dom.menu = fakeMenu();
dom.btn = fakeBtn();
delete _store.simple_fs_filter;
_treeRenders = 0;
pysimFsFilter = new Set(['all']);
pysimFsFilterSetEntry('all', true);
assert.deepStrictEqual([...pysimFsFilter], ['all']);
assert.strictEqual(_store.simple_fs_filter, '["all"]');
assert.strictEqual(_treeRenders, 1);
// choosing a category drops "all"
pysimFsFilterSetEntry('provider-names', true);
assert.deepStrictEqual([...pysimFsFilter], ['provider-names']);
assert.strictEqual(_store.simple_fs_filter, '["provider-names"]');
assert.strictEqual(_treeRenders, 3); // render + the auto-expand's render
assert.strictEqual(box('provider-names').checked, true);
assert.strictEqual(box('all').checked, false);
assert.strictEqual(dom.btn.textContent, 'Provider names');
// several categories stay selected
pysimFsFilterSetEntry('identifiers', true);
assert.deepStrictEqual([...pysimFsFilter], ['provider-names', 'identifiers']);
// unchecking one keeps the other
pysimFsFilterSetEntry('provider-names', false);
assert.deepStrictEqual([...pysimFsFilter], ['identifiers']);
// unchecking the last one restores the default
pysimFsFilterSetEntry('identifiers', false);
assert.deepStrictEqual([...pysimFsFilter], ['all']);
assert.strictEqual(_store.simple_fs_filter, '["all"]');
assert.strictEqual(box('all').checked, true);
// "all" is exclusive even when another entry is selected
pysimFsFilter = new Set(['provider-names']);
pysimFsFilterSetEntry('all', true);
assert.deepStrictEqual([...pysimFsFilter], ['all']);
});
test('the saved selection is restored, defaulting to "all"', () => {
delete _store.simple_fs_filter;
pysimFsFilterRestore();
assert.deepStrictEqual([...pysimFsFilter], ['all']);
_store.simple_fs_filter = JSON.stringify(['providers', 'svc:Service Provider Name (SPN)']);
pysimFsFilterRestore();
assert.deepStrictEqual([...pysimFsFilter], ['providers', 'svc:Service Provider Name (SPN)']);
// `all` is exclusive; an empty or broken save falls back to it
_store.simple_fs_filter = JSON.stringify(['all', 'plmn-selection']);
pysimFsFilterRestore();
assert.deepStrictEqual([...pysimFsFilter], ['all']);
_store.simple_fs_filter = '[]';
pysimFsFilterRestore();
assert.deepStrictEqual([...pysimFsFilter], ['all']);
_store.simple_fs_filter = '{not json';
pysimFsFilterRestore();
assert.deepStrictEqual([...pysimFsFilter], ['all']);
});
test('the render consults the filter when walking children', () => {
assert.match(html, /if \(!pysimFsFilterShow\(child\)\) continue;/);
assert.ok(!/pysimFsFilterMatch\(child\)/.test(html));
});
test('the directory rule uses the registry paths, not the loaded children', () => {
const fn = extractFunc(html, 'pysimFsFilterShow');
assert.ok(!fn.includes('node.children'), fn);
assert.ok(fn.indexOf('pysimFsFilterMatchingPaths()') > 0, fn);
assert.ok(fn.indexOf("path === 'MF'") > 0, fn);
// the standard list is read synchronously from the cached array
assert.match(html, /let _uiccStdFiles = null;/);
assert.ok(extractFunc(html, 'pysimFsFilterMatchingPaths').indexOf('_uiccStdFiles') > 0);
});
test('every filter change drops the path cache before the tree redraws', () => {
for (const name of ['pysimFsFilterSetEntry', 'pysimFsSetFilterToService', 'pysimFsFilterRestore']) {
const fn = extractFunc(html, name);
const at = fn.indexOf('pysimFsFilterPathsInvalidate()');
assert.ok(at > 0, name + ' does not invalidate the path cache');
const render = fn.indexOf('pysimFsRenderTree()');
if (render > 0) assert.ok(at < render, name + ': the cache is dropped before the redraw');
}
// the list arriving redraws a tree that was filtered while it loaded
const load = extractFunc(html, 'uiccStdFilesLoad');
assert.ok(load.indexOf('pysimFsRenderTree()') > 0, load);
});
test('the filter state is persisted under simple_fs_filter', () => {
const fn = extractFunc(html, 'pysimFsFilterSetEntry');
assert.ok(fn.indexOf("localStorage.setItem('simple_fs_filter'") > 0, fn);
assert.ok(fn.indexOf('pysimFsRenderTree()') > 0, fn);
assert.ok(fn.indexOf('pysimFsFilterRender()') > 0, fn);
});
test('the dropdown is re-rendered on language switch', () => {
const fn = extractFunc(html, 'refreshDynamicI18n');
assert.ok(fn.indexOf('pysimFsFilterRender()') > 0, fn);
});
test('the filter labels have Russian entries', () => {
const dict = html.match(/const LANG_RU = \{([\s\S]*?)\n\};/)[1];
for (const key of ['All', 'Custom files', 'Identifiers', 'PLMN', 'Services', 'PLMN selection',
'Provider names', 'WLAN / I-WLAN', 'Selected: {n}']) {
assert.ok(dict.includes("'" + key + "':"), 'missing Russian entry: ' + key);
}
});
+16 -2
View File
@@ -24,6 +24,9 @@ function extractFunc(src, name) {
let code = 'var pysimFsSelected = null;\n' let code = 'var pysimFsSelected = null;\n'
+ 'var pysimFsDecodedMode = false;\n' + 'var pysimFsDecodedMode = false;\n'
+ 'var pysimFsTreeRoot = null;\n'; + 'var pysimFsTreeRoot = null;\n';
code += extractFunc(html, 'efFidFromPath') + '\n';
code += extractFunc(html, 'efDfFromPath') + '\n';
code += extractFunc(html, 'pysimFsFallbackNode') + '\n';
code += extractFunc(html, 'pysimFsSyncContentVisibility') + '\n'; code += extractFunc(html, 'pysimFsSyncContentVisibility') + '\n';
code += extractFunc(html, 'pysimFsRead') + '\n'; code += extractFunc(html, 'pysimFsRead') + '\n';
eval(code); eval(code);
@@ -53,7 +56,7 @@ function setup() {
}; };
globalThis.t = s => s; globalThis.t = s => s;
globalThis.esc = s => s; globalThis.esc = s => s;
globalThis.pysimFsFindNode = () => ({ name: 'EF.IMSI', fid: '6f07' }); globalThis.pysimFsFindNodeByPath = () => ({ name: 'EF.IMSI', fid: '6f07' });
globalThis.pysimFsSelectBody = () => ({}); globalThis.pysimFsSelectBody = () => ({});
globalThis.pysimFsShowError = (el, sw, err) => { globalThis.pysimFsShowError = (el, sw, err) => {
errorShown = { sw, err }; errorShown = { sw, err };
@@ -63,7 +66,7 @@ function setup() {
if (fetchResult instanceof Error) throw fetchResult; if (fetchResult instanceof Error) throw fetchResult;
return fetchResult; return fetchResult;
}; };
pysimFsSelected = 'EF.IMSI'; pysimFsSelected = 'ADF.USIM/6F07';
pysimFsDecodedMode = false; pysimFsDecodedMode = false;
pysimFsTreeRoot = { name: 'MF' }; pysimFsTreeRoot = { name: 'MF' };
fetchResult = { success: true, sw: '9000', data: 'AABB' }; fetchResult = { success: true, sw: '9000', data: 'AABB' };
@@ -92,6 +95,17 @@ test('a successful read fills and shows the content pane', async () => {
assert.strictEqual(status.textContent, 'SW: 9000 OK'); assert.strictEqual(status.textContent, 'SW: 9000 OK');
}); });
test('a path whose node is not loaded selects by its last segment and FID', async () => {
setup();
globalThis.pysimFsFindNodeByPath = () => null;
let body = null;
globalThis.pysimFsSelectBody = node => { body = node; return {}; };
pysimFsSelected = 'MF/7F20/6FB7';
const ok = await pysimFsRead();
assert.strictEqual(ok, true);
assert.deepStrictEqual(body, { name: '6FB7', fid: '6FB7' });
});
test('a failed read keeps the content pane hidden', async () => { test('a failed read keeps the content pane hidden', async () => {
setup(); setup();
fetchResult = { success: false, sw: '6982', error: 'Security status not satisfied' }; fetchResult = { success: false, sw: '6982', error: 'Security status not satisfied' };
+28
View File
@@ -22,7 +22,11 @@ function extractFunc(src, name) {
} }
let code = 'var pysimFsSort = "fid";\n'; let code = 'var pysimFsSort = "fid";\n';
code += 'var pysimFsFilter = new Set();\n';
code += extractFunc(html, 'pysimFsSortChildren') + '\n'; code += extractFunc(html, 'pysimFsSortChildren') + '\n';
code += extractFunc(html, 'pysimFsNodePath') + '\n';
code += extractFunc(html, 'pysimFsFilterMatch') + '\n';
code += extractFunc(html, 'pysimFsFilterShow') + '\n';
code += extractFunc(html, 'pysimFsRenderNode') + '\n'; code += extractFunc(html, 'pysimFsRenderNode') + '\n';
code += 'globalThis.esc = s => s;\nglobalThis.t = s => s;\n'; code += 'globalThis.esc = s => s;\nglobalThis.t = s => s;\n';
eval(code); eval(code);
@@ -65,3 +69,27 @@ test('non-existing directory hides previously loaded children', () => {
assert.ok(!out.includes('(empty)'), out); assert.ok(!out.includes('(empty)'), out);
assert.ok(!out.includes('pysimFsToggleDir'), out); assert.ok(!out.includes('pysimFsToggleDir'), out);
}); });
test('the row handlers carry the canonical path, so same-named files differ', () => {
// EF.ECC lives in DF.GSM and ADF.USIM with the same name AND FID: the
// onclick argument must be the path, never the ambiguous display name
const root = { name: 'MF', fid: '3F00', isDir: true, exists: true, children: [], expanded: true, parent: null };
const gsm = { name: 'DF.GSM', fid: '7F20', isDir: true, exists: true, children: [], expanded: true, parent: root };
const usim = { name: 'ADF.USIM', fid: '7FFF', isDir: true, exists: true, children: [], expanded: true, parent: root };
const gsmEcc = { name: 'EF.ECC', fid: '6FB7', isDir: false, exists: true, children: null, expanded: false, parent: gsm };
const usimEcc = { name: 'EF.ECC', fid: '6FB7', isDir: false, exists: true, children: null, expanded: false, parent: usim };
root.children = [gsm, usim];
gsm.children = [gsmEcc];
usim.children = [usimEcc];
assert.ok(pysimFsRenderNode(gsmEcc, 0).includes("pysimFsClickFile('MF/7F20/6FB7')"));
assert.ok(pysimFsRenderNode(usimEcc, 0).includes("pysimFsClickFile('ADF.USIM/6FB7')"));
assert.ok(pysimFsRenderNode(gsm, 0).includes("pysimFsToggleDir('MF/7F20')"));
assert.ok(pysimFsRenderNode(usim, 0).includes("pysimFsToggleDir('ADF.USIM')"));
});
test('pysimFsRenderNode passes pysimFsNodePath(node) to the click/toggle handlers', () => {
const body = extractFunc(html, 'pysimFsRenderNode');
assert.ok(body.includes('const nodePath = pysimFsNodePath(node);'));
assert.match(body, /pysimFsToggleDir\(\\'' \+ esc\(nodePath\)/);
assert.match(body, /pysimFsClickFile\(\\'' \+ esc\(nodePath\)/);
});
+134
View File
@@ -0,0 +1,134 @@
// The service-table rows' cross-link to the file-tree filter: a table row
// that is backed by files gets a "To EF filter" button that narrows the tree
// to exactly that service's files.
const { test } = require('node:test');
const assert = require('node:assert');
const fs = require('node:fs');
const path = require('node:path');
const html = fs.readFileSync(path.join(__dirname, '..', 'index.html'), 'utf8');
function extractFunc(src, name) {
const re = new RegExp('(?:async\\s+)?function\\s+' + name + '\\s*\\([^)]*\\)\\s*\\{');
const m = re.exec(src);
if (!m) throw new Error('function ' + name + ' not found');
let i = m.index + m[0].length - 1;
let depth = 0;
for (; i < src.length; i++) {
if (src[i] === '{') depth++;
else if (src[i] === '}') {
depth--;
if (depth === 0) break;
}
}
return src.slice(m.index, i + 1);
}
function extractConst(src, name) {
const m = new RegExp('const ' + name + ' = ').exec(src);
if (!m) throw new Error('const ' + name + ' not found');
const begin = m.index + m[0].length;
const opener = src[begin];
const closer = opener === '[' ? ']' : (opener === '{' ? '}' : null);
if (!closer) throw new Error('unsupported const ' + name);
let depth = 0, i = begin;
for (; i < src.length; i++) {
if (src[i] === opener) depth++;
else if (src[i] === closer) {
depth--;
if (depth === 0) { i++; break; }
}
}
return 'var ' + name + ' = ' + src.slice(begin, i) + ';\n';
}
// module state the extracted functions close over
let pysimFsFilter = new Set(['all']);
let _servicesFeatureByNumber = null;
// the tree filter's matching-path cache (pysimFsSetFilterToService drops it)
let _fsFilterPaths = null;
let _fsFilterPathsKey = null;
const _store = {};
let _renders = 0;
let _treeRenders = 0;
let pysimFsTreeRoot = null; // expand-all walks this; null = nothing to expand
globalThis.localStorage = {
getItem: k => (Object.prototype.hasOwnProperty.call(_store, k) ? _store[k] : null),
setItem: (k, v) => { _store[k] = String(v); },
removeItem: k => { delete _store[k]; },
};
globalThis.esc = s => String(s);
globalThis.t = k => k;
globalThis.pysimFsFilterRender = () => { _renders++; };
globalThis.pysimFsRenderTree = () => { _treeRenders++; };
let code = '';
for (const f of ['servicesFeatureByNumber', 'pysimFsFilterBtnHtml', 'pysimFsSetFilterToService',
'pysimFsFilterPathsInvalidate', 'pysimFsExpandAll', 'pysimFsCollapseAll',
'pysimFsAutoExpandForFilter']) {
code += extractFunc(html, f) + '\n';
}
for (const c of ['SERVICES_CROSS_REF', 'SERVICES_FS_MAP']) code += extractConst(html, c);
code += "var FS_FILTER_ALL = 'all';\n";
eval(code);
test('servicesFeatureByNumber inverts the cross-reference per file table', () => {
const map = servicesFeatureByNumber();
assert.strictEqual(map.ust[2], 'Fixed Dialling Numbers (FDN)');
assert.strictEqual(map.sst[2], 'Local Phone Book (ADN)');
assert.strictEqual(map.est[1], 'Fixed Dialling Numbers (FDN)');
assert.strictEqual(map.ist[1], 'P-CSCF address');
// the same number names a different feature in another table
assert.strictEqual(map.ust[1], 'Local Phone Book (ADN)');
assert.strictEqual(map.sst[1], 'CHV1 disable function');
// a capability-only number maps to a feature too
assert.strictEqual(map.ust[99], 'URI support by UICC');
// the map is built once
assert.strictEqual(servicesFeatureByNumber(), map);
});
test('pysimFsFilterBtnHtml links a file-backed service row only', () => {
const out = pysimFsFilterBtnHtml('ust', 2);
assert.match(out, /^<button type="button" class="ml-1 px-1 text-xs text-blue-600 dark:text-blue-400 hover:underline"/);
assert.ok(out.indexOf("pysimFsSetFilterToService('ust', 2)") > 0, out);
assert.ok(out.indexOf('To EF filter') > 0, out);
// OCI is file-backed
assert.notStrictEqual(pysimFsFilterBtnHtml('ust', 8), '');
// a capability-only service, a missing table and an unmodelled number
assert.strictEqual(pysimFsFilterBtnHtml('ust', 99), '');
assert.strictEqual(pysimFsFilterBtnHtml(undefined, 1), '');
assert.strictEqual(pysimFsFilterBtnHtml('sst', 99), '');
});
test('pysimFsSetFilterToService narrows the tree to that service', () => {
delete _store.simple_fs_filter;
_renders = 0;
_treeRenders = 0;
pysimFsFilter = new Set(['all']);
pysimFsSetFilterToService('ust', 2);
assert.deepStrictEqual([...pysimFsFilter], ['svc:Fixed Dialling Numbers (FDN)']);
assert.strictEqual(_store.simple_fs_filter, JSON.stringify(['svc:Fixed Dialling Numbers (FDN)']));
assert.strictEqual(_renders, 1);
assert.strictEqual(_treeRenders, 2); // render + the auto-expand's render
// a capability-only service changes nothing
pysimFsSetFilterToService('ust', 99);
assert.deepStrictEqual([...pysimFsFilter], ['svc:Fixed Dialling Numbers (FDN)']);
assert.strictEqual(_renders, 1);
assert.strictEqual(_treeRenders, 2);
});
test('the four service-table fields carry their table, EF.ACC carries none', () => {
assert.match(html, /const EF_UST_FIELDS = \[\{[^\n]*table: 'ust'/);
assert.match(html, /const EF_IST_FIELDS = \[\{[^\n]*table: 'ist'/);
assert.match(html, /const EF_EST_FIELDS = \[\{[^\n]*table: 'est'/);
assert.match(html, /const EF_SST_FIELDS = \[\{[\s\S]{0,200}?table: 'sst'/);
const acc = /const EF_ACC_FIELDS = [^\n]*/.exec(html);
assert.ok(acc, 'EF_ACC_FIELDS not found');
assert.ok(acc[0].indexOf('table') < 0, acc[0]);
});
test('both service-grid renderers append the cross-link after their control', () => {
const fn = extractFunc(html, 'efDecFieldHtml');
assert.strictEqual((fn.match(/pysimFsFilterBtnHtml\(f\.table, n\)/g) || []).length, 2, fn);
});
+2 -1
View File
@@ -446,7 +446,8 @@ test('file manager shows FCI info and keeps the selection in state, not the DOM'
assert.ok(html.includes('pysimFsInfoHtml(sel, name, { arr: arrs, path: pysimFsNodePath(node) })')); assert.ok(html.includes('pysimFsInfoHtml(sel, name, { arr: arrs, path: pysimFsNodePath(node) })'));
assert.ok(!html.includes('pysim-fs-filename')); assert.ok(!html.includes('pysim-fs-filename'));
assert.ok(html.includes('let pysimFsSelected = null;')); assert.ok(html.includes('let pysimFsSelected = null;'));
assert.ok(html.includes('pysimFsSelected = name;')); assert.ok(html.includes('pysimFsSelected = node ? pysimFsNodePath(node) : path;'),
'the selection is keyed by the canonical path, not the display name');
assert.ok(!html.includes('pysimFsSelect()')); assert.ok(!html.includes('pysimFsSelect()'));
}); });
+702
View File
@@ -0,0 +1,702 @@
const { test } = require('node:test');
const assert = require('node:assert');
const fs = require('node:fs');
const path = require('node:path');
const html = fs.readFileSync(path.join(__dirname, '..', 'index.html'), 'utf8');
function extractFunc(src, name) {
const re = new RegExp('(?:async\\s+)?function\\s+' + name + '\\s*\\([^)]*\\)\\s*\\{');
const m = re.exec(src);
if (!m) throw new Error('function ' + name + ' not found');
let i = m.index + m[0].length - 1;
let depth = 0;
for (; i < src.length; i++) {
if (src[i] === '{') depth++;
else if (src[i] === '}') {
depth--;
if (depth === 0) break;
}
}
return src.slice(m.index, i + 1);
}
function extractConst(src, name) {
const m = new RegExp('const ' + name + ' = ').exec(src);
if (!m) throw new Error('const ' + name + ' not found');
const begin = m.index + m[0].length;
const opener = src[begin];
const closer = opener === '[' ? ']' : (opener === '{' ? '}' : null);
if (!closer) throw new Error('unsupported const ' + name);
let depth = 0, i = begin;
for (; i < src.length; i++) {
if (src[i] === opener) depth++;
else if (src[i] === closer) {
depth--;
if (depth === 0) { i++; break; }
}
}
return 'var ' + name + ' = ' + src.slice(begin, i) + ';\n';
}
const FNS = [
'efBytes', 'efHex', 'efDecServiceTable', 'efDecSst', 'efEncServiceTable', 'efEncSst', 'efEncError',
'servicesDecode', 'servicesUnmodelled', 'servicesSstState', 'servicesBitState', 'servicesFeatureState',
'servicesSstAllocated', 'servicesSstPreserved', 'servicesBitsPreserved', 'servicesSstValues',
'servicesTableValues', 'servicesFileBytes', 'pysimServicesColumns', 'pysimServicesGridHtml',
'pysimServicesSaveRowHtml', 'pysimServicesCollect',
'servicesFsDeclaredTrees', 'servicesFsPresentPaths', 'servicesFsReport',
'servicesFsReportHtml', 'servicesFsPathKey', 'servicesFsNormPath', 'servicesFsRolePath',
'servicesPathIndexBuild', 'pysimFsNodePath',
];
let code = 'var _pysimServicesCache = null;\nvar _pysimServicesBusy = false;\n';
for (const f of FNS) code += extractFunc(html, f) + '\n';
for (const c of ['EF_UST_SERVICES', 'EF_SST_SERVICES', 'EF_IST_SERVICES', 'EF_EST_SERVICES',
'SERVICES_CROSS_REF', 'SERVICES_FILES', 'SERVICES_FS_MAP', 'SERVICES_FS_TREES',
'SERVICES_FS_TREE_ORDER', 'SERVICES_FS_GSM_MAPPED', 'SERVICES_TABLES']) {
code += extractConst(html, c);
}
code += 'globalThis.esc = s => String(s);\nglobalThis.t = k => k;\n';
eval(code);
// ---- data ----
test('SERVICES_CROSS_REF covers every non-RFU service of the four files exactly once', () => {
const tables = { sst: EF_SST_SERVICES, ust: EF_UST_SERVICES, est: EF_EST_SERVICES, ist: EF_IST_SERVICES };
// The doc's §A.4 RFU/reserved slots: SST 8/20/50, UST 26/33/50/98.
const rfu = { sst: [8, 20, 50], ust: [26, 33, 50, 98], est: [], ist: [] };
const seen = { sst: [], ust: [], est: [], ist: [] };
for (const f of SERVICES_CROSS_REF) {
for (const k of Object.keys(seen)) {
if (f[k] === null || f[k] === undefined) continue;
assert.ok(Number.isInteger(f[k]), 'non-integer ' + k + ' in ' + f.f);
seen[k].push(f[k]);
}
}
for (const k of Object.keys(seen)) {
assert.strictEqual(new Set(seen[k]).size, seen[k].length, 'duplicate ' + k + ' numbers');
const expected = Object.keys(tables[k]).map(Number).filter(n => rfu[k].indexOf(n) < 0).sort((a, b) => a - b);
assert.deepStrictEqual(seen[k].slice().sort((a, b) => a - b), expected, k + ' coverage');
// the numbers a save copies from the file instead of editing
assert.deepStrictEqual(servicesUnmodelled(k), rfu[k], k + ' unmodelled slots');
}
});
test('SERVICES_CROSS_REF has the doc\'s totals and no empty row', () => {
const counts = { sim: 0, usim: 0, isim: 0, shared: 0 };
for (const f of SERVICES_CROSS_REF) {
assert.ok(typeof f.f === 'string' && f.f.length > 0, 'feature name');
assert.ok(Object.prototype.hasOwnProperty.call(counts, f.cls), 'class: ' + f.cls);
counts[f.cls]++;
const nums = ['sst', 'ust', 'est', 'ist'].filter(k => f[k] !== null && f[k] !== undefined);
assert.ok(nums.length > 0, 'row without a service number: ' + f.f);
// the class says which applications carry the feature
if (f.cls === 'sim') assert.deepStrictEqual(nums, ['sst'], f.f);
else if (f.cls === 'isim') assert.deepStrictEqual(nums, ['ist'], f.f);
else if (f.cls === 'usim') assert.ok(nums.indexOf('ust') >= 0 && nums.indexOf('sst') < 0 && nums.indexOf('ist') < 0, f.f);
else assert.ok(nums.length > 1, 'shared row with one file: ' + f.f);
}
// docs/services-editor.md: 164 table rows = 161 features + 3 RFU rows
// (10 SIM-only, 85 USIM-only, 5 ISIM-only, 61 shared).
assert.deepStrictEqual(counts, { sim: 10, usim: 85, isim: 5, shared: 61 });
assert.strictEqual(SERVICES_CROSS_REF.length, 161);
});
test('the four files are addressed by their canonical (DF, FID) path', () => {
assert.deepStrictEqual(SERVICES_FILES.map(f => f.key), ['sst', 'ust', 'est', 'ist']);
const paths = {};
for (const f of SERVICES_FILES) paths[f.key] = f.path;
assert.deepStrictEqual(paths, {
sst: 'DF.GSM/6F38', ust: 'ADF.USIM/6F38', est: 'ADF.USIM/6F56', ist: 'ADF.ISIM/6F07',
});
});
// ---- the edit-state helper ----
const dec = obj => obj;
function sstDec(pairs) {
// pairs: { <n>: [allocated, activated] } - a deleted service is absent
const services = [];
for (const k of Object.keys(pairs)) {
const n = Number(k);
if (!pairs[k][0] && !pairs[k][1]) continue;
services.push({ n, name: '', allocated: !!pairs[k][0], activated: !!pairs[k][1] });
}
return { services };
}
function bitDec(nums) {
return { services: nums.map(n => ({ n, name: '' })) };
}
test('servicesFeatureState: shared feature on/off/deleted', () => {
const adn = SERVICES_CROSS_REF.find(f => f.f === 'Local Phone Book (ADN)');
assert.strictEqual(adn.sst, 2);
assert.strictEqual(adn.ust, 1);
// allocated + activated, UST bit set
let st = servicesFeatureState(adn, sstDec({ 2: [1, 1] }), bitDec([1, 3]), null, null);
assert.deepStrictEqual(st, { sst: 'on', ust: true, est: null, ist: null });
// allocated but not activated -> 'off' (the editor never de-allocates)
st = servicesFeatureState(adn, sstDec({ 2: [1, 0] }), bitDec([]), null, null);
assert.deepStrictEqual(st, { sst: 'off', ust: false, est: null, ist: null });
// deleted on the SIM (the decoder drops the service entirely) -> 'off'
st = servicesFeatureState(adn, sstDec({ 3: [1, 1] }), bitDec([]), null, null);
assert.strictEqual(st.sst, 'off');
});
test('servicesFeatureState: sim-only, usim-only, isim-only and EST', () => {
const chv1 = SERVICES_CROSS_REF.find(f => f.f === 'CHV1 disable function');
assert.strictEqual(chv1.cls, 'sim');
assert.deepStrictEqual(servicesFeatureState(chv1, sstDec({ 1: [1, 1] }), null, null, null),
{ sst: 'on', ust: null, est: null, ist: null });
const oci = SERVICES_CROSS_REF.find(f => f.f === 'Outgoing Call Information (OCI and OCT)');
assert.deepStrictEqual(servicesFeatureState(oci, null, bitDec([8]), null, null),
{ sst: null, ust: true, est: null, ist: null });
const pcscf = SERVICES_CROSS_REF.find(f => f.f === 'P-CSCF address');
assert.deepStrictEqual(servicesFeatureState(pcscf, null, null, null, bitDec([1])),
{ sst: null, ust: null, est: null, ist: true });
const fdn = SERVICES_CROSS_REF.find(f => f.f === 'Fixed Dialling Numbers (FDN)');
assert.strictEqual(fdn.est, 1);
assert.deepStrictEqual(servicesFeatureState(fdn, sstDec({ 3: [1, 1] }), bitDec([2]), bitDec([]), null),
{ sst: 'on', ust: true, est: false, ist: null });
// a feature without a number in a file is null even when the file is there
const ccp2 = SERVICES_CROSS_REF.find(f => f.f === 'Capability Configuration Parameters 2 (CCP2)');
assert.deepStrictEqual(servicesFeatureState(ccp2, sstDec({ 1: [1, 1] }), bitDec([14]), null, null).sst, null);
// an absent file leaves its column null
const adn = SERVICES_CROSS_REF.find(f => f.f === 'Local Phone Book (ADN)');
assert.strictEqual(servicesFeatureState(adn, null, bitDec([1]), null, null).sst, null);
});
// ---- render ----
test('the grid renders a column per present file and the right cell states', () => {
const adn = SERVICES_CROSS_REF.findIndex(f => f.f === 'Local Phone Book (ADN)');
const files = {
sst: { present: true, hex: '00300000', dec: sstDec({ 2: [1, 1] }) },
ust: { present: true, hex: '02000000', dec: bitDec([1]) },
est: { present: false },
ist: { present: false },
};
const out = pysimServicesGridHtml(files, {});
assert.ok(out.includes('>SST<'), out);
assert.ok(out.includes('>UST<'), out);
assert.ok(!out.includes('>EST<'), out);
assert.ok(!out.includes('>IST<'), out);
// the ADN row: SST select on 'on', UST checkbox checked
assert.ok(out.includes('data-svc="sst" data-row="' + adn + '"'));
assert.match(out, new RegExp('<option value="on" selected>[^<]*</option>'));
const adnRow = out.split('<tr').find(r => r.includes('Local Phone Book (ADN)'));
assert.ok(adnRow.includes('checked'), adnRow);
assert.ok(adnRow.includes('<select data-svc="sst"'), adnRow);
// an ISIM-only feature is hidden while EF.IST is absent
assert.ok(!out.includes('P-CSCF address'), out);
// and a UST-only feature is hidden while EF.UST is absent
const sstOnly = pysimServicesGridHtml({ sst: files.sst, ust: { present: false } }, {});
assert.ok(sstOnly.includes('>SST<'), sstOnly);
assert.ok(!sstOnly.includes('>UST<'), sstOnly);
assert.ok(!sstOnly.includes('Outgoing Call Information'), sstOnly);
assert.ok(sstOnly.includes('Local Phone Book (ADN)'), sstOnly);
// a feature without a number in a present file shows an empty cell, no input
const plmnRow = out.split('<tr').find(r => r.includes('>PLMN selector<'));
assert.ok(plmnRow, 'the PLMN selector row renders');
assert.ok(plmnRow.includes('data-svc="sst"'), plmnRow);
assert.ok(!plmnRow.includes('data-svc="ust"'), plmnRow);
assert.ok(plmnRow.includes('\u2014'), plmnRow);
// an EST column renders a checkbox per EST-numbered feature (FDN = 1)
const withEst = pysimServicesGridHtml({ ust: files.ust, est: { present: true, dec: bitDec([]) } }, {});
assert.ok(withEst.includes('>EST<'), withEst);
const fdnRow = withEst.split('<tr').find(r => r.includes('>Fixed Dialling Numbers (FDN)<'));
assert.ok(fdnRow.includes('data-svc="est"'), fdnRow);
assert.ok(!fdnRow.includes('data-svc="est" data-row="2" checked'), fdnRow);
// ... and is checked from the card's decode
const estOn = pysimServicesGridHtml({ ust: files.ust, est: { present: true, dec: bitDec([1]) } }, {});
const fdnOn = estOn.split('<tr').find(r => r.includes('>Fixed Dialling Numbers (FDN)<'));
assert.match(fdnOn, /data-svc="est" data-row="\d+" checked/);
});
test('no present file reports why, and the save row lists only present files', () => {
const out = pysimServicesGridHtml({}, {});
assert.ok(out.includes('No service table found on this card'), out);
const row = pysimServicesSaveRowHtml({
sst: { present: true }, ust: { present: true }, est: { present: false }, ist: { present: false },
});
assert.ok(row.includes('pysimServicesSave(\'sst\')'), row);
assert.ok(row.includes('pysimServicesSave(\'ust\')'), row);
assert.ok(!row.includes('pysimServicesSave(\'est\')'), row);
assert.ok(!row.includes('pysimServicesSave(\'ist\')'), row);
assert.ok(row.includes('Save to EF.SST'), row);
assert.ok(row.includes('Save to EF.UST'), row);
// the per-button status spans the writes report into
assert.ok(row.includes('id="pysim-services-save-status-sst"'), row);
});
test('pysimServicesCollect reads the form controls into per-row state', () => {
const grid = {
querySelectorAll: () => [
{ getAttribute: k => ({ 'data-svc': 'sst', 'data-row': '3' })[k], type: 'select-one', value: 'off' },
{ getAttribute: k => ({ 'data-svc': 'ust', 'data-row': '3' })[k], type: 'checkbox', checked: true },
{ getAttribute: k => ({ 'data-svc': 'ust', 'data-row': '4' })[k], type: 'checkbox', checked: false },
],
};
globalThis.document = { getElementById: id => (id === 'pysim-services-grid' ? grid : null) };
assert.deepStrictEqual(pysimServicesCollect(), { 3: { sst: 'off', ust: true }, 4: { ust: false } });
globalThis.document = { getElementById: () => null };
assert.deepStrictEqual(pysimServicesCollect(), {});
});
// ---- save ----
test('SST bytes: on allocates and activates, off never de-allocates', () => {
// service 3 (FDN) allocated but not activated -> 0x10 in byte 0
const files = { sst: { present: true, hex: '10000000' } };
const fdn = SERVICES_CROSS_REF.findIndex(f => f.f === 'Fixed Dialling Numbers (FDN)');
assert.strictEqual(efBytes('10000000')[0] & 0x30, 0x10); // sanity: pair 1
// "off" on an allocated-off service keeps it allocated (pair 1, not 0)
let out = servicesFileBytes('sst', { [fdn]: { sst: 'off' } }, files);
assert.strictEqual(out, '10000000');
// "on" allocates and activates (pair 3)
out = servicesFileBytes('sst', { [fdn]: { sst: 'on' } }, files);
assert.strictEqual(out, '30000000');
// switching an allocated+activated service to "off" de-activates but keeps
// the allocation (pair 1)
out = servicesFileBytes('sst', { [fdn]: { sst: 'off' } }, { sst: { present: true, hex: '30000000' } });
assert.strictEqual(out, '10000000');
// a deleted service stays deleted when the form renders it as "off"
out = servicesFileBytes('sst', { [fdn]: { sst: 'off' } }, { sst: { present: true, hex: '00000000' } });
assert.strictEqual(out, '00000000');
// ... and is allocated by an explicit "on"
out = servicesFileBytes('sst', { [fdn]: { sst: 'on' } }, { sst: { present: true, hex: '00000000' } });
assert.strictEqual(out, '30000000');
});
test('SST bytes preserve the unmodelled RFU slots and the file length', () => {
// service 8 = RFU, in byte 1 (services 5-8), pair 4 -> bits 6+7 = 0xC0
const files = { sst: { present: true, hex: '00C000' } };
assert.deepStrictEqual(servicesUnmodelled('sst'), [8, 20, 50]);
assert.strictEqual(servicesFileBytes('sst', {}, files), '00C000',
'the RFU pair and the trailing byte survive');
});
test('UST/EST/IST bytes come from efEncServiceTable over the original bytes', () => {
// UST service 1 (ADN) set in byte 0; service 26 (RFU) in byte 3 bit 1
const files = { ust: { present: true, hex: '01000002' } };
const adn = SERVICES_CROSS_REF.findIndex(f => f.f === 'Local Phone Book (ADN)');
const sms = SERVICES_CROSS_REF.findIndex(f => f.f === 'Short Message Storage (SMS)');
let out = servicesFileBytes('ust', { [adn]: { ust: true }, [sms]: { ust: true } }, files);
// service 10 (SMS) -> byte 1 bit 1 = 0x02
assert.strictEqual(out, '01020002');
out = servicesFileBytes('ust', { [adn]: { ust: false }, [sms]: { ust: false } }, files);
assert.strictEqual(out, '00000002', 'the RFU bit is preserved, the modelled bits cleared');
// EST has its own numbering: FDN = 1
const estFiles = { est: { present: true, hex: '00' } };
const fdn = SERVICES_CROSS_REF.findIndex(f => f.f === 'Fixed Dialling Numbers (FDN)');
assert.strictEqual(servicesFileBytes('est', { [fdn]: { est: true } }, estFiles), '01');
assert.strictEqual(servicesFileBytes('est', { [fdn]: { est: false } }, estFiles), '00');
// ISIM IST: P-CSCF address = 1
const istFiles = { ist: { present: true, hex: '00' } };
const pcscf = SERVICES_CROSS_REF.findIndex(f => f.f === 'P-CSCF address');
assert.strictEqual(servicesFileBytes('ist', { [pcscf]: { ist: true } }, istFiles), '01');
});
test('an absent file yields no bytes', () => {
assert.strictEqual(servicesFileBytes('sst', {}, { sst: { present: false } }), null);
assert.strictEqual(servicesFileBytes('ust', {}, {}), null);
});
// ---- filesystem check: the feature -> symbolic files table (docs/services-editor.md §B) ----
const UICC_FILES = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'uicc_files.json'), 'utf8'));
const FS_INDEX = servicesPathIndexBuild(UICC_FILES.files);
const fsResolve = (role, root) => FS_INDEX.get(servicesFsPathKey(role, root));
test('SERVICES_FS_MAP covers every feature exactly once and carries symbolic file names', () => {
const names = SERVICES_CROSS_REF.map(f => f.f);
assert.deepStrictEqual(Object.keys(SERVICES_FS_MAP), names,
'one entry per cross-reference row, in table order');
let backed = 0, capability = 0;
for (const [name, roles] of Object.entries(SERVICES_FS_MAP)) {
assert.ok(Array.isArray(roles), 'not an array: ' + name);
assert.strictEqual(new Set(roles).size, roles.length, 'duplicate role: ' + name);
// the standard file list's own names, never FIDs: a FID is ambiguous
// across applications (4F30 = EF.PBR and EF.SAI)
for (const role of roles) assert.match(role, /^(EF|DF)\.[A-Za-z0-9_+.-]+$/, name + ': ' + role);
if (roles.length) backed++; else capability++;
}
// docs/services-editor.md §B.1 lists 34 `capability` cells; the remaining
// 127 rows declare at least one file.
assert.strictEqual(capability, 34);
assert.strictEqual(backed, 127);
assert.strictEqual(SERVICES_CROSS_REF.length, backed + capability);
});
test('the capability features carry no file check', () => {
for (const name of [
'CHV1 disable function', 'Data download via SMS-CB', 'Data download via SMS-PP',
'Menu selection', 'Call control (by SIM/USIM)', 'Proactive SIM',
'MO-SMS control (by SIM/USIM)', 'RUN AT COMMAND command', 'Call control on GPRS by USIM',
'Additional TERMINAL PROFILE after UICC activation', 'Terminal Applications',
'URI support by UICC', 'HTTP Digest', 'Support of P-CSCF discovery for IMS Local Break Out',
]) {
assert.deepStrictEqual(SERVICES_FS_MAP[name], [], name);
}
});
test('the well-known features map to their documented files', () => {
assert.deepStrictEqual(SERVICES_FS_MAP['Fixed Dialling Numbers (FDN)'], ['EF.FDN', 'EF.EXT2']);
assert.deepStrictEqual(SERVICES_FS_MAP['Service Provider Name (SPN)'], ['EF.SPN']);
assert.deepStrictEqual(SERVICES_FS_MAP['GSM Access'], ['DF.GSM-ACCESS']);
assert.deepStrictEqual(SERVICES_FS_MAP['Local Phone Book (ADN)'], ['EF.ADN', 'DF.PHONEBOOK', 'EF.PBR']);
assert.deepStrictEqual(SERVICES_FS_MAP['SoLSA'], ['DF.SoLSA', 'EF.SAI', 'EF.SLL']);
assert.deepStrictEqual(SERVICES_FS_MAP['MExE'], ['DF.MExE', 'EF.MExE-ST']);
assert.deepStrictEqual(SERVICES_FS_MAP['A2X'], ['DF.A2X', 'EF.AST', 'EF.A2X_CONFIG']);
assert.deepStrictEqual(SERVICES_FS_MAP['Advice of Charge (AoC)'], ['EF.ACM', 'EF.ACMmax', 'EF.PUCT']);
assert.deepStrictEqual(SERVICES_FS_MAP['Extension 4'], ['EF.EXT4'],
'one name: the SIM and the USIM copy have different FIDs but the same name');
});
test('a role outside its feature\'s declaring tree lives in the shared DF.TELECOM subtree', () => {
// A UST/IST-gated DF may be a child of DF.TELECOM (TS 31.102 4.6.0), i.e.
// an MF path - the resolver's fallback. Anything else would be a mapping
// error, and the GSM application's mapped copies (EF.ADN/EF.EXT1/EF.ECCP)
// are deliberately not expected there.
const features = new Set();
for (const f of SERVICES_CROSS_REF) {
const trees = [];
if (f.sst !== null) trees.push('MF');
if (f.ust !== null || f.est !== null) trees.push('ADF.USIM');
if (f.ist !== null) trees.push('ADF.ISIM');
for (const role of (SERVICES_FS_MAP[f.f] || [])) {
if (trees.some(root => fsResolve(role, root))) continue;
const path = fsResolve(role, 'MF');
if (!path) continue; // a registry gap, pinned above
assert.match(path, /^MF\/7F10\//, f.f + ': ' + role + ' -> ' + path);
assert.ok(SERVICES_FS_GSM_MAPPED.indexOf(role) < 0, f.f + ': ' + role);
features.add(f.f);
}
}
assert.deepStrictEqual(Array.from(features),
['Multimedia Messages Storage', 'MCPTT', 'V2X', 'A2X']);
});
test('every mapped role is a standard file, except the documented registry gaps', () => { // The names that resolve in none of the checked trees: files the standard
// list does not carry yet (pySim omits DF.GRAPHICS and the DF.TELECOM
// copies of DF.SoLSA/DF.MExE; EF.3GPPPSDATAOFFservicelist is a pySim TODO).
// The report lists these as `unchecked` instead of guessing a path.
const gaps = {
'Image (IMG)': ['DF.GRAPHICS', 'EF.IMG'],
'3GPP PS Data Off Service List': ['EF.3GPPPSDATAOFFservicelist'],
};
const unresolved = {};
for (const [name, roles] of Object.entries(SERVICES_FS_MAP)) {
for (const role of roles) {
if (SERVICES_FS_TREE_ORDER.some(root => fsResolve(role, root))) continue;
(unresolved[name] = unresolved[name] || []).push(role);
}
}
assert.deepStrictEqual(unresolved, gaps);
// a mapped role that does resolve resolves through exactly one list entry
for (const roles of Object.values(SERVICES_FS_MAP)) {
for (const role of roles) {
for (const root of SERVICES_FS_TREE_ORDER) {
const path = fsResolve(role, root);
if (!path) continue;
const entries = UICC_FILES.files.filter(f => f.name === role
&& f.root.toUpperCase() === root && f.path.toUpperCase() === path);
assert.strictEqual(entries.length, 1, role + ' ' + root + ' -> ' + path);
}
}
}
});
// ---- filesystem check: the (name, root) -> path index ----
test('servicesPathIndexBuild keys the standard list by (name, root)', () => {
assert.ok(FS_INDEX.size >= 200, 'indexed files: ' + FS_INDEX.size);
// every value is one of the list's paths, and the list's roots are the keys
for (const [key, path] of FS_INDEX) {
const [name, root] = key.split('|');
assert.ok(UICC_FILES.files.some(f => f.name.toUpperCase() === name && f.root.toUpperCase() === root
&& f.path.toUpperCase() === path), key + ' -> ' + path);
}
assert.deepStrictEqual(servicesPathIndexBuild(null), new Map());
assert.deepStrictEqual(servicesPathIndexBuild([null, {}, { name: 'EF.X' }]), new Map());
// case-insensitive on both sides
assert.strictEqual(FS_INDEX.get(servicesFsPathKey('ef.spn', 'mf')), 'MF/7F20/6F46');
});
test('the same FID under different names resolves to its own path', () => {
// EF.PBR and EF.SAI are both 4F30
assert.strictEqual(fsResolve('EF.PBR', 'ADF.USIM'), 'ADF.USIM/5F3A/4F30');
assert.strictEqual(fsResolve('EF.SAI', 'ADF.USIM'), 'ADF.USIM/5F70/4F30');
// EF.PBR exists in both trees, under different parents
assert.strictEqual(fsResolve('EF.PBR', 'MF'), 'MF/7F10/5F3A/4F30');
// a DF.TELECOM child resolves in the MF tree
assert.strictEqual(fsResolve('DF.A2X', 'MF'), 'MF/7F10/5F3F');
assert.strictEqual(fsResolve('EF.A2X_CONFIG', 'MF'), 'MF/7F10/5F3F/4F02');
// EF.SST and EF.UST are both 6F38
assert.strictEqual(fsResolve('EF.UST', 'ADF.USIM'), 'ADF.USIM/6F38');
assert.strictEqual(fsResolve('EF.IST', 'ADF.ISIM'), 'ADF.ISIM/6F07');
// the one duplicate (name, root): pySim models EF.SUCI_Calc_Info twice
// under ADF.USIM (DF.5GS/4F07 - the UST 125 file - and the eSIM profile
// package's DF.SAIP/4F01); the list's first entry, DF.5GS, wins
const suci = UICC_FILES.files.filter(f => f.name === 'EF.SUCI_Calc_Info' && f.root === 'ADF.USIM');
assert.strictEqual(suci.length, 2);
assert.strictEqual(fsResolve('EF.SUCI_Calc_Info', 'ADF.USIM'), 'ADF.USIM/5FC0/4F07');
});
test('servicesFsRolePath falls back to the shared DF.TELECOM subtree', () => {
// DF.MCS is a child of DF.TELECOM (TS 31.102 4.6.0): a UST declaration
// resolves it in the MF tree
assert.strictEqual(servicesFsRolePath(FS_INDEX, 'DF.MCS', 'ADF.USIM'), 'MF/7F10/5F3D');
assert.strictEqual(servicesFsRolePath(FS_INDEX, 'EF.MST', 'ADF.USIM'), 'MF/7F10/5F3D/4F01');
// an own-tree path wins over the fallback
assert.strictEqual(servicesFsRolePath(FS_INDEX, 'EF.FDN', 'ADF.USIM'), 'ADF.USIM/6F3B');
assert.strictEqual(servicesFsRolePath(FS_INDEX, 'EF.FDN', 'MF'), 'MF/7F10/6F3B');
// the GSM application's mapped copies are not expected by a USIM
// declaration (TS 31.102 4.5) ...
assert.strictEqual(servicesFsRolePath(FS_INDEX, 'EF.ADN', 'ADF.USIM'), undefined);
assert.ok(SERVICES_FS_GSM_MAPPED.indexOf('EF.ADN') >= 0);
// ... and a SIM (MF) declaration never falls back to an application
assert.strictEqual(servicesFsRolePath(FS_INDEX, 'DF.SoLSA', 'MF'), undefined);
assert.strictEqual(servicesFsRolePath(null, 'EF.SPN', 'MF'), undefined);
});
// ---- filesystem check: the report ----
const svcFiles = (sst, ust, est, ist) => ({
sst: sst ? { present: true, dec: sst } : { present: false },
ust: ust ? { present: true, dec: ust } : { present: false },
est: est ? { present: true, dec: est } : { present: false },
ist: ist ? { present: true, dec: ist } : { present: false },
});
// A file-manager tree with parent links (pysimFsNodePath walks up them).
function fsTree(children) {
const mk = (spec, parent) => {
const node = {
name: spec.name, fid: spec.fid, isDir: !!spec.isDir, exists: spec.exists,
children: null, parent: parent,
};
if (spec.children) node.children = spec.children.map(c => mk(c, node));
return node;
};
const root = { name: 'MF', path: 'MF', fid: '3F00', isDir: true, exists: true, parent: null, children: null };
root.children = (children || []).map(c => mk(c, root));
return root;
}
test('servicesFsPresentPaths collects the probed nodes\' canonical paths', () => {
const root = fsTree([
{ name: 'DF.TELECOM', fid: '7F10', isDir: true, exists: true, children: [
{ name: 'EF.SMS', fid: '6f3c', exists: true },
{ name: 'EF.LND', fid: '6F44', exists: false },
] },
{ name: 'EF.ICCID', fid: '2FE2', exists: null }, // never probed
{ name: 'ADF.USIM', isDir: true, exists: true, children: [
{ name: 'EF.UST', fid: '6F38', exists: true },
] },
{ name: 'DF.ABSENT', fid: '5F80', isDir: true, exists: false, children: [
{ name: 'EF.BCAST', fid: '4F01', exists: true }, // under an absent DF
] },
]);
assert.deepStrictEqual(servicesFsPresentPaths(root),
['MF/7F10', 'MF/7F10/6F3C', 'ADF.USIM', 'ADF.USIM/6F38']);
assert.deepStrictEqual(servicesFsPresentPaths(null), []);
});
test('servicesFsDeclaredTrees groups the declared features by declaring application', () => {
const files = svcFiles(
sstDec({ 3: [1, 1], 4: [1, 0] }), // FDN on, SMS allocated-off
bitDec([2, 10]), // UST FDN + SMS
null, // no EF.EST on the card
bitDec([6])); // IST SMS
const trees = servicesFsDeclaredTrees(files);
assert.deepStrictEqual(Object.keys(trees), ['MF', 'ADF.USIM', 'ADF.ISIM']);
assert.ok(trees.MF.includes('Fixed Dialling Numbers (FDN)'), trees.MF.join(' | '));
assert.ok(!trees.MF.includes('Short Message Storage (SMS)'), 'an allocated-off SST service declares nothing');
assert.ok(trees['ADF.USIM'].includes('Fixed Dialling Numbers (FDN)'));
assert.ok(trees['ADF.USIM'].includes('Short Message Storage (SMS)'));
assert.ok(trees['ADF.ISIM'].includes('Short Message Storage (SMS)'));
// the tree's own order: cross-reference order, deduplicated
assert.deepStrictEqual(trees['ADF.USIM'], ['Fixed Dialling Numbers (FDN)', 'Short Message Storage (SMS)']);
// a tree whose service table is absent is not checked at all
assert.deepStrictEqual(servicesFsDeclaredTrees(svcFiles(null, bitDec([2]), null, null)),
{ 'ADF.USIM': ['Fixed Dialling Numbers (FDN)'] });
assert.deepStrictEqual(servicesFsDeclaredTrees(svcFiles(null, null, null, null)), {});
});
const fsReport = (declaredByTree, present) => servicesFsReport(declaredByTree, FS_INDEX, present);
test('servicesFsReport: a declared feature with an absent path', () => {
const rep = fsReport({ MF: ['Fixed Dialling Numbers (FDN)'] }, ['MF/7F10/6F3B']);
assert.deepStrictEqual(rep.missing,
[{ feature: 'Fixed Dialling Numbers (FDN)', tree: 'MF', paths: ['MF/7F10/6F4B'] }]);
assert.deepStrictEqual(rep.extra, []);
assert.deepStrictEqual(rep.unchecked, []);
// both files gone, in the map's (doc) role order
assert.deepStrictEqual(fsReport({ MF: ['Fixed Dialling Numbers (FDN)'] }, []).missing,
[{ feature: 'Fixed Dialling Numbers (FDN)', tree: 'MF', paths: ['MF/7F10/6F3B', 'MF/7F10/6F4B'] }]);
});
test('servicesFsReport: the same FID in another DF no longer explains a service', () => {
// EF.PBR and EF.SAI are both 4F30: a card holding the phonebook copy but
// no DF.SoLSA leaves SoLSA's paths missing
const rep = fsReport({ 'ADF.USIM': ['SoLSA'] }, ['ADF.USIM/5F3A', 'ADF.USIM/5F3A/4F30']);
assert.deepStrictEqual(rep.missing, [{ feature: 'SoLSA', tree: 'ADF.USIM',
paths: ['ADF.USIM/5F70', 'ADF.USIM/5F70/4F30', 'ADF.USIM/5F70/4F31'] }]);
// the phonebook paths are present but ADN is not declared
assert.deepStrictEqual(rep.extra.map(e => e.path), ['ADF.USIM/5F3A', 'ADF.USIM/5F3A/4F30']);
assert.deepStrictEqual(rep.extra[1],
{ path: 'ADF.USIM/5F3A/4F30', role: 'EF.PBR', tree: 'ADF.USIM', features: ['Local Phone Book (ADN)'] });
// with the SoLSA paths present the report is clean
const ok = fsReport({ 'ADF.USIM': ['SoLSA', 'Local Phone Book (ADN)'] },
['ADF.USIM/5F70', 'ADF.USIM/5F70/4F30', 'ADF.USIM/5F70/4F31', 'ADF.USIM/5F3A', 'ADF.USIM/5F3A/4F30']);
assert.deepStrictEqual(ok, { missing: [], extra: [], unchecked: [] });
});
test('servicesFsReport: ADN\'s USIM files are the phonebook pair, not the GSM copy', () => {
// TS 31.102 4.5: EF.ADN under DF.TELECOM is the GSM application's mapping
const usim = fsReport({ 'ADF.USIM': ['Local Phone Book (ADN)'] }, ['ADF.USIM/5F3A', 'ADF.USIM/5F3A/4F30']);
assert.deepStrictEqual(usim, { missing: [], extra: [], unchecked: [] });
// ... so a USIM card without the mapped 6F3A is not missing anything
assert.deepStrictEqual(fsReport({ 'ADF.USIM': ['Local Phone Book (ADN)'] }, ['ADF.USIM/5F3A/4F30']).missing,
[{ feature: 'Local Phone Book (ADN)', tree: 'ADF.USIM', paths: ['ADF.USIM/5F3A'] }]);
// the SIM declaration does expect it
const sim = fsReport({ MF: ['Local Phone Book (ADN)'] }, ['MF/7F10/5F3A', 'MF/7F10/5F3A/4F30']);
assert.deepStrictEqual(sim.missing,
[{ feature: 'Local Phone Book (ADN)', tree: 'MF', paths: ['MF/7F10/6F3A'] }]);
assert.deepStrictEqual(sim.extra, []);
});
test('servicesFsReport: a UST-declared DF.TELECOM file resolves in the MF tree', () => {
// MCPTT's DF.MCS is a child of DF.TELECOM (TS 31.102 4.6.0)
assert.deepStrictEqual(fsReport({ 'ADF.USIM': ['MCPTT'] }, ['MF/7F10/5F3D', 'MF/7F10/5F3D/4F01']),
{ missing: [], extra: [], unchecked: [] });
assert.deepStrictEqual(fsReport({ 'ADF.USIM': ['MCPTT'] }, []).missing,
[{ feature: 'MCPTT', tree: 'ADF.USIM', paths: ['MF/7F10/5F3D', 'MF/7F10/5F3D/4F01'] }]);
});
test('servicesFsReport: a declared feature the standard list cannot resolve is unchecked', () => {
const rep = fsReport({ 'ADF.USIM': ['Image (IMG)'] }, []);
assert.deepStrictEqual(rep, { missing: [], extra: [], unchecked: [
{ feature: 'Image (IMG)', tree: 'ADF.USIM', roles: ['DF.GRAPHICS', 'EF.IMG'] }] });
// a capability row has no files and is never unchecked
assert.deepStrictEqual(fsReport({ MF: ['CHV1 disable function'] }, []),
{ missing: [], extra: [], unchecked: [] });
// a tree the card does not have declares nothing there
assert.deepStrictEqual(fsReport({}, []), { missing: [], extra: [], unchecked: [] });
});
test('servicesFsReport: a shared file is explained by any declared feature', () => {
// DF.GSM-ACCESS is gated by both "GSM Access" and "GSM security context";
// declaring either one explains the present path
const present = ['ADF.USIM/5F3B', 'ADF.USIM/5F3B/4F20', 'ADF.USIM/5F3B/4F52'];
assert.deepStrictEqual(fsReport({ 'ADF.USIM': ['GSM security context'] }, present),
{ missing: [], extra: [], unchecked: [] });
// with neither declared the path is an extra, naming every mapping feature
assert.deepStrictEqual(fsReport({ 'ADF.USIM': [] }, ['ADF.USIM/5F3B']).extra,
[{ path: 'ADF.USIM/5F3B', role: 'DF.GSM-ACCESS', tree: 'ADF.USIM',
features: ['GSM Access', 'GSM security context'] }]);
// a lower-case present path matches the list's upper-case one
assert.deepStrictEqual(fsReport({ MF: ['Service Provider Name (SPN)'] }, ['mf/7f20/6f46']),
{ missing: [], extra: [], unchecked: [] });
});
test('servicesFsReport: a present path whose service is undeclared', () => {
const rep = fsReport({ MF: [] }, ['MF/7F10/6F3C']);
assert.deepStrictEqual(rep.missing, []);
assert.deepStrictEqual(rep.extra,
[{ path: 'MF/7F10/6F3C', role: 'EF.SMS', tree: 'MF', features: ['Short Message Storage (SMS)'] }]);
// extra is sorted by path, and a path without a mapped feature is ignored
const extra = fsReport({ MF: ['Service Provider Name (SPN)'] },
['MF/7F20/6F45', 'MF/7F10/6F44', 'MF/7F10/6F3C', 'MF/7F20/6F46', 'MF/2F06']).extra;
assert.deepStrictEqual(extra.map(e => e.path), ['MF/7F10/6F3C', 'MF/7F10/6F44', 'MF/7F20/6F45']);
assert.deepStrictEqual(extra[2].features, ['Cell Broadcast Message Identifier (CBMI)']);
});
test('servicesFsReport is deterministic and order-stable', () => {
const present = ['ADF.USIM/6F3B'];
const a = fsReport({ 'ADF.USIM': ['Service Provider Name (SPN)', 'Fixed Dialling Numbers (FDN)'] }, present);
const b = fsReport({ 'ADF.USIM': ['Fixed Dialling Numbers (FDN)', 'Service Provider Name (SPN)'] }, present);
assert.deepStrictEqual(a, b, 'cross-reference order, not input order');
assert.deepStrictEqual(a.missing, [
{ feature: 'Fixed Dialling Numbers (FDN)', tree: 'ADF.USIM', paths: ['ADF.USIM/6F4B'] },
{ feature: 'Service Provider Name (SPN)', tree: 'ADF.USIM', paths: ['ADF.USIM/6F46'] },
]);
});
test('servicesFsReportHtml renders the paths and the notes', () => {
const note = 'path-exact';
const empty = servicesFsReportHtml(fsReport({ MF: [] }, []));
assert.ok(empty.includes('The filesystem matches the declared services'), empty);
assert.ok(empty.includes(note), empty);
assert.ok(servicesFsReportHtml(null).includes('The filesystem matches the declared services'));
const rep = fsReport({ MF: ['Fixed Dialling Numbers (FDN)'] }, ['MF/7F10/6F3C']);
const html = servicesFsReportHtml(rep);
assert.ok(html.includes('Declared services whose files are missing'), html);
assert.ok(html.includes('Fixed Dialling Numbers (FDN)'), html);
assert.ok(html.includes('MF/7F10/6F4B'), 'the missing path is shown');
assert.ok(html.includes('Present files whose service is not declared'), html);
assert.ok(html.includes('MF/7F10/6F3C'), 'the extra path is shown');
assert.ok(html.includes('EF.SMS'), 'the extra path names its role');
assert.ok(html.includes('Short Message Storage (SMS)'), html);
assert.ok(html.includes(note), html);
assert.ok(!html.includes('The filesystem matches the declared services'), html);
// a single-sided report renders only its own heading
const one = servicesFsReportHtml(fsReport({ MF: ['Fixed Dialling Numbers (FDN)'] }, ['MF/7F10/6F3B']));
assert.ok(one.includes('Declared services whose files are missing'), one);
assert.ok(!one.includes('Present files whose service is not declared'), one);
assert.ok(!one.includes('Declared services that cannot be checked'), one);
// the unchecked bucket names the roles and explains itself
const unc = servicesFsReportHtml(fsReport({ 'ADF.USIM': ['Image (IMG)'] }, []));
assert.ok(unc.includes('Declared services that cannot be checked'), unc);
assert.ok(unc.includes('Image (IMG)') && unc.includes('DF.GRAPHICS') && unc.includes('EF.IMG'), unc);
assert.ok(unc.includes('cannot be verified'), unc);
});
// ---- filesystem check: the wiring ----
test('the Services pill carries the Check filesystem action and its report panel', () => {
const start = html.indexOf('id="pysim-sub-services"');
const pill = html.slice(start, html.indexOf('id="tab-phone"', start));
assert.match(pill, /id="pysim-services-fs-btn"[^>]*data-needs="card"[^>]*onclick="pysimServicesFsCheck\(\)"/);
assert.match(pill, /data-l10n="Check filesystem"/);
assert.match(pill, /id="pysim-services-fs-status"/);
assert.match(pill, /id="pysim-services-fs-spin"[^>]*class="hidden animate-spin/);
assert.match(pill, /id="pysim-services-fs-report"/);
});
test('the check probes the tree once per loaded session and keeps the report across renders', () => {
// /api/tree lists files without testing them: only a completed probe sets
// the presence flag, and a refresh (card change) clears it
assert.match(html, /_pysimFsProbed = !probe\.stop && !error;/);
assert.match(html, /async function pysimFsRefresh\(\) \{[\s\S]{0,200}_pysimFsProbed = false;/);
// the check loads the tree if missing, probes when unprobed, and renders
// the pure report from the per-tree declarations, the standard list's path
// index and the tree's present paths
const fn = extractFunc(html, 'pysimServicesFsCheck');
assert.ok(fn.indexOf('await pysimFsRefresh()') > 0, fn);
assert.ok(fn.indexOf('await pysimFsProbeAll()') > 0, fn);
// a throbber shows while the scan runs and hides when it settles
assert.ok(fn.indexOf('pysim-services-fs-spin') > 0, fn);
assert.ok(fn.indexOf("spinEl.classList.remove('hidden')") > 0, fn);
assert.ok(fn.indexOf("spinEl.classList.add('hidden')") > 0, fn);
assert.ok(fn.indexOf('servicesFsDeclaredTrees(cache.files)') > 0, fn);
assert.ok(fn.indexOf('await servicesPathIndex()') > 0, fn);
assert.ok(fn.indexOf('index.size') > 0, 'an unloadable list is an error, not a wall of unchecked rows');
assert.ok(fn.indexOf('servicesFsPresentPaths(pysimFsTreeRoot)') > 0, fn);
assert.ok(fn.indexOf('servicesFsReport(') > 0, fn);
// the path index is loaded once from the standard file list
const loader = extractFunc(html, 'servicesPathIndex');
assert.ok(loader.indexOf('uiccStdFilesLoad()') > 0, loader);
assert.ok(loader.indexOf('servicesPathIndexBuild(') > 0, loader);
// the grid's render redraws the kept report (language switch)
const render = extractFunc(html, 'pysimServicesRender');
assert.ok(render.indexOf('pysimServicesFsRender()') > 0, render);
// and a card-session change drops it with the cache
assert.match(html, /function pysimServicesReset\(\) \{\s*_pysimServicesCache = null;\s*_pysimServicesFsReport = null;/);
});
+297 -1
View File
@@ -1,5 +1,5 @@
{ {
"generated": "2026-09-23", "generated": "2026-10-25",
"files": [ "files": [
{ {
"path": "MF/2F00", "path": "MF/2F00",
@@ -161,6 +161,30 @@
"kind": "ef", "kind": "ef",
"root": "MF" "root": "MF"
}, },
{
"path": "MF/7F10/5F3F",
"name": "DF.A2X",
"sym": "MF/DF.TELECOM/DF.A2X",
"fid": "5F3F",
"kind": "df",
"root": "MF"
},
{
"path": "MF/7F10/5F3F/4F01",
"name": "EF.AST",
"sym": "MF/DF.TELECOM/DF.A2X/EF.AST",
"fid": "4F01",
"kind": "ef",
"root": "MF"
},
{
"path": "MF/7F10/5F3F/4F02",
"name": "EF.A2X_CONFIG",
"sym": "MF/DF.TELECOM/DF.A2X/EF.A2X_CONFIG",
"fid": "4F02",
"kind": "ef",
"root": "MF"
},
{ {
"path": "MF/7F10/6F3A", "path": "MF/7F10/6F3A",
"name": "EF.ADN", "name": "EF.ADN",
@@ -1030,6 +1054,46 @@
"kind": "ef", "kind": "ef",
"root": "ADF.USIM" "root": "ADF.USIM"
}, },
{
"path": "ADF.USIM/5F3C",
"name": "DF.MExE",
"sym": "ADF.USIM/DF.MExE",
"fid": "5F3C",
"kind": "df",
"root": "ADF.USIM"
},
{
"path": "ADF.USIM/5F3C/4F40",
"name": "EF.MExE-ST",
"sym": "ADF.USIM/DF.MExE/EF.MExE-ST",
"fid": "4F40",
"kind": "ef",
"root": "ADF.USIM"
},
{
"path": "ADF.USIM/5F3C/4F41",
"name": "EF.ORPK",
"sym": "ADF.USIM/DF.MExE/EF.ORPK",
"fid": "4F41",
"kind": "ef",
"root": "ADF.USIM"
},
{
"path": "ADF.USIM/5F3C/4F42",
"name": "EF.ARPK",
"sym": "ADF.USIM/DF.MExE/EF.ARPK",
"fid": "4F42",
"kind": "ef",
"root": "ADF.USIM"
},
{
"path": "ADF.USIM/5F3C/4F43",
"name": "EF.TPRPK",
"sym": "ADF.USIM/DF.MExE/EF.TPRPK",
"fid": "4F43",
"kind": "ef",
"root": "ADF.USIM"
},
{ {
"path": "ADF.USIM/5F40", "path": "ADF.USIM/5F40",
"name": "DF.WLAN", "name": "DF.WLAN",
@@ -1182,6 +1246,38 @@
"kind": "ef", "kind": "ef",
"root": "ADF.USIM" "root": "ADF.USIM"
}, },
{
"path": "ADF.USIM/5F70",
"name": "DF.SoLSA",
"sym": "ADF.USIM/DF.SoLSA",
"fid": "5F70",
"kind": "df",
"root": "ADF.USIM"
},
{
"path": "ADF.USIM/5F70/4F30",
"name": "EF.SAI",
"sym": "ADF.USIM/DF.SoLSA/EF.SAI",
"fid": "4F30",
"kind": "ef",
"root": "ADF.USIM"
},
{
"path": "ADF.USIM/5F70/4F31",
"name": "EF.SLL",
"sym": "ADF.USIM/DF.SoLSA/EF.SLL",
"fid": "4F31",
"kind": "ef",
"root": "ADF.USIM"
},
{
"path": "ADF.USIM/5F80",
"name": "DF.BCAST",
"sym": "ADF.USIM/DF.BCAST",
"fid": "5F80",
"kind": "df",
"root": "ADF.USIM"
},
{ {
"path": "ADF.USIM/5F90", "path": "ADF.USIM/5F90",
"name": "DF.ProSe", "name": "DF.ProSe",
@@ -1302,6 +1398,30 @@
"kind": "ef", "kind": "ef",
"root": "ADF.USIM" "root": "ADF.USIM"
}, },
{
"path": "ADF.USIM/5FA0",
"name": "DF.ACDC",
"sym": "ADF.USIM/DF.ACDC",
"fid": "5FA0",
"kind": "df",
"root": "ADF.USIM"
},
{
"path": "ADF.USIM/5FA0/4F01",
"name": "EF.ACDC_LIST",
"sym": "ADF.USIM/DF.ACDC/EF.ACDC_LIST",
"fid": "4F01",
"kind": "ef",
"root": "ADF.USIM"
},
{
"path": "ADF.USIM/5FB0",
"name": "DF.TV",
"sym": "ADF.USIM/DF.TV",
"fid": "5FB0",
"kind": "df",
"root": "ADF.USIM"
},
{ {
"path": "ADF.USIM/5FC0", "path": "ADF.USIM/5FC0",
"name": "DF.5GS", "name": "DF.5GS",
@@ -2262,6 +2382,14 @@
"kind": "ef", "kind": "ef",
"root": "ADF.USIM" "root": "ADF.USIM"
}, },
{
"path": "ADF.USIM/6FDC",
"name": "EF.LRPLMNSI",
"sym": "ADF.USIM/EF.LRPLMNSI",
"fid": "6FDC",
"kind": "ef",
"root": "ADF.USIM"
},
{ {
"path": "ADF.USIM/6FDD", "path": "ADF.USIM/6FDD",
"name": "EF.NAFKCA", "name": "EF.NAFKCA",
@@ -2358,6 +2486,14 @@
"kind": "ef", "kind": "ef",
"root": "ADF.USIM" "root": "ADF.USIM"
}, },
{
"path": "ADF.USIM/6FF0",
"name": "EF.IAL",
"sym": "ADF.USIM/EF.IAL",
"fid": "6FF0",
"kind": "ef",
"root": "ADF.USIM"
},
{ {
"path": "ADF.USIM/6FF1", "path": "ADF.USIM/6FF1",
"name": "EF.IPS", "name": "EF.IPS",
@@ -2366,6 +2502,14 @@
"kind": "ef", "kind": "ef",
"root": "ADF.USIM" "root": "ADF.USIM"
}, },
{
"path": "ADF.USIM/6FF2",
"name": "EF.IPD",
"sym": "ADF.USIM/EF.IPD",
"fid": "6FF2",
"kind": "ef",
"root": "ADF.USIM"
},
{ {
"path": "ADF.USIM/6FF3", "path": "ADF.USIM/6FF3",
"name": "EF.ePDGId", "name": "EF.ePDGId",
@@ -2414,6 +2558,22 @@
"kind": "ef", "kind": "ef",
"root": "ADF.USIM" "root": "ADF.USIM"
}, },
{
"path": "ADF.USIM/6FF9",
"name": "EF.3GPPPSDATAOFF",
"sym": "ADF.USIM/EF.3GPPPSDATAOFF",
"fid": "6FF9",
"kind": "ef",
"root": "ADF.USIM"
},
{
"path": "ADF.USIM/6FFB",
"name": "EF.TVCONFIG",
"sym": "ADF.USIM/EF.TVCONFIG",
"fid": "6FFB",
"kind": "ef",
"root": "ADF.USIM"
},
{ {
"path": "ADF.USIM/6FFC", "path": "ADF.USIM/6FFC",
"name": "EF.XCAPConfigData", "name": "EF.XCAPConfigData",
@@ -2527,6 +2687,46 @@
"kind": "ef", "kind": "ef",
"root": "ADF.USIM-NON-IMSI" "root": "ADF.USIM-NON-IMSI"
}, },
{
"path": "ADF.USIM-NON-IMSI/5F3C",
"name": "DF.MExE",
"sym": "ADF.USIM-non-IMSI/DF.MExE",
"fid": "5F3C",
"kind": "df",
"root": "ADF.USIM-NON-IMSI"
},
{
"path": "ADF.USIM-NON-IMSI/5F3C/4F40",
"name": "EF.MExE-ST",
"sym": "ADF.USIM-non-IMSI/DF.MExE/EF.MExE-ST",
"fid": "4F40",
"kind": "ef",
"root": "ADF.USIM-NON-IMSI"
},
{
"path": "ADF.USIM-NON-IMSI/5F3C/4F41",
"name": "EF.ORPK",
"sym": "ADF.USIM-non-IMSI/DF.MExE/EF.ORPK",
"fid": "4F41",
"kind": "ef",
"root": "ADF.USIM-NON-IMSI"
},
{
"path": "ADF.USIM-NON-IMSI/5F3C/4F42",
"name": "EF.ARPK",
"sym": "ADF.USIM-non-IMSI/DF.MExE/EF.ARPK",
"fid": "4F42",
"kind": "ef",
"root": "ADF.USIM-NON-IMSI"
},
{
"path": "ADF.USIM-NON-IMSI/5F3C/4F43",
"name": "EF.TPRPK",
"sym": "ADF.USIM-non-IMSI/DF.MExE/EF.TPRPK",
"fid": "4F43",
"kind": "ef",
"root": "ADF.USIM-NON-IMSI"
},
{ {
"path": "ADF.USIM-NON-IMSI/5F40", "path": "ADF.USIM-NON-IMSI/5F40",
"name": "DF.WLAN", "name": "DF.WLAN",
@@ -2679,6 +2879,38 @@
"kind": "ef", "kind": "ef",
"root": "ADF.USIM-NON-IMSI" "root": "ADF.USIM-NON-IMSI"
}, },
{
"path": "ADF.USIM-NON-IMSI/5F70",
"name": "DF.SoLSA",
"sym": "ADF.USIM-non-IMSI/DF.SoLSA",
"fid": "5F70",
"kind": "df",
"root": "ADF.USIM-NON-IMSI"
},
{
"path": "ADF.USIM-NON-IMSI/5F70/4F30",
"name": "EF.SAI",
"sym": "ADF.USIM-non-IMSI/DF.SoLSA/EF.SAI",
"fid": "4F30",
"kind": "ef",
"root": "ADF.USIM-NON-IMSI"
},
{
"path": "ADF.USIM-NON-IMSI/5F70/4F31",
"name": "EF.SLL",
"sym": "ADF.USIM-non-IMSI/DF.SoLSA/EF.SLL",
"fid": "4F31",
"kind": "ef",
"root": "ADF.USIM-NON-IMSI"
},
{
"path": "ADF.USIM-NON-IMSI/5F80",
"name": "DF.BCAST",
"sym": "ADF.USIM-non-IMSI/DF.BCAST",
"fid": "5F80",
"kind": "df",
"root": "ADF.USIM-NON-IMSI"
},
{ {
"path": "ADF.USIM-NON-IMSI/5F90", "path": "ADF.USIM-NON-IMSI/5F90",
"name": "DF.ProSe", "name": "DF.ProSe",
@@ -2799,6 +3031,30 @@
"kind": "ef", "kind": "ef",
"root": "ADF.USIM-NON-IMSI" "root": "ADF.USIM-NON-IMSI"
}, },
{
"path": "ADF.USIM-NON-IMSI/5FA0",
"name": "DF.ACDC",
"sym": "ADF.USIM-non-IMSI/DF.ACDC",
"fid": "5FA0",
"kind": "df",
"root": "ADF.USIM-NON-IMSI"
},
{
"path": "ADF.USIM-NON-IMSI/5FA0/4F01",
"name": "EF.ACDC_LIST",
"sym": "ADF.USIM-non-IMSI/DF.ACDC/EF.ACDC_LIST",
"fid": "4F01",
"kind": "ef",
"root": "ADF.USIM-NON-IMSI"
},
{
"path": "ADF.USIM-NON-IMSI/5FB0",
"name": "DF.TV",
"sym": "ADF.USIM-non-IMSI/DF.TV",
"fid": "5FB0",
"kind": "df",
"root": "ADF.USIM-NON-IMSI"
},
{ {
"path": "ADF.USIM-NON-IMSI/5FC0", "path": "ADF.USIM-NON-IMSI/5FC0",
"name": "DF.5GS", "name": "DF.5GS",
@@ -3751,6 +4007,14 @@
"kind": "ef", "kind": "ef",
"root": "ADF.USIM-NON-IMSI" "root": "ADF.USIM-NON-IMSI"
}, },
{
"path": "ADF.USIM-NON-IMSI/6FDC",
"name": "EF.LRPLMNSI",
"sym": "ADF.USIM-non-IMSI/EF.LRPLMNSI",
"fid": "6FDC",
"kind": "ef",
"root": "ADF.USIM-NON-IMSI"
},
{ {
"path": "ADF.USIM-NON-IMSI/6FDD", "path": "ADF.USIM-NON-IMSI/6FDD",
"name": "EF.NAFKCA", "name": "EF.NAFKCA",
@@ -3847,6 +4111,14 @@
"kind": "ef", "kind": "ef",
"root": "ADF.USIM-NON-IMSI" "root": "ADF.USIM-NON-IMSI"
}, },
{
"path": "ADF.USIM-NON-IMSI/6FF0",
"name": "EF.IAL",
"sym": "ADF.USIM-non-IMSI/EF.IAL",
"fid": "6FF0",
"kind": "ef",
"root": "ADF.USIM-NON-IMSI"
},
{ {
"path": "ADF.USIM-NON-IMSI/6FF1", "path": "ADF.USIM-NON-IMSI/6FF1",
"name": "EF.IPS", "name": "EF.IPS",
@@ -3855,6 +4127,14 @@
"kind": "ef", "kind": "ef",
"root": "ADF.USIM-NON-IMSI" "root": "ADF.USIM-NON-IMSI"
}, },
{
"path": "ADF.USIM-NON-IMSI/6FF2",
"name": "EF.IPD",
"sym": "ADF.USIM-non-IMSI/EF.IPD",
"fid": "6FF2",
"kind": "ef",
"root": "ADF.USIM-NON-IMSI"
},
{ {
"path": "ADF.USIM-NON-IMSI/6FF3", "path": "ADF.USIM-NON-IMSI/6FF3",
"name": "EF.ePDGId", "name": "EF.ePDGId",
@@ -3903,6 +4183,22 @@
"kind": "ef", "kind": "ef",
"root": "ADF.USIM-NON-IMSI" "root": "ADF.USIM-NON-IMSI"
}, },
{
"path": "ADF.USIM-NON-IMSI/6FF9",
"name": "EF.3GPPPSDATAOFF",
"sym": "ADF.USIM-non-IMSI/EF.3GPPPSDATAOFF",
"fid": "6FF9",
"kind": "ef",
"root": "ADF.USIM-NON-IMSI"
},
{
"path": "ADF.USIM-NON-IMSI/6FFB",
"name": "EF.TVCONFIG",
"sym": "ADF.USIM-non-IMSI/EF.TVCONFIG",
"fid": "6FFB",
"kind": "ef",
"root": "ADF.USIM-NON-IMSI"
},
{ {
"path": "ADF.USIM-NON-IMSI/6FFC", "path": "ADF.USIM-NON-IMSI/6FFC",
"name": "EF.XCAPConfigData", "name": "EF.XCAPConfigData",
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "pysim-simple-server" name = "pysim-simple-server"
version = "3.38.1" version = "3.40.2"
description = "HTTP REST server wrapping pysim for the SIMple PWA" description = "HTTP REST server wrapping pysim for the SIMple PWA"
license = { text = "GPL-2.0-or-later" } license = { text = "GPL-2.0-or-later" }
requires-python = ">=3.9" requires-python = ">=3.9"
+1 -1
View File
@@ -37,7 +37,7 @@ from osmocom.tlv import BER_TLV_IE
from osmocom.utils import rpad from osmocom.utils import rpad
VERSION = '3.38.1' VERSION = '3.40.2'
MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE
+63
View File
@@ -11,6 +11,11 @@ classes - the same model pySim assembles at equip time:
It ships as ``frontend/uicc_files.json`` so the SIM/USIM RFM SELECT picker has It ships as ``frontend/uicc_files.json`` so the SIM/USIM RFM SELECT picker has
a file list offline (no card, no server); when a card is equipped the picker a file list offline (no card, no server); when a card is equipped the picker
merges the live tree over it and hides probed-absent files. merges the live tree over it and hides probed-absent files.
The optional / service-gated files pySim's model does not carry (the SoLSA,
MExE, BCAST, ACDC, TV and A2X DFs, EF.LRPLMNSI, EF.IAL, EF.IPD, EF.TVCONFIG,
EF.3GPPPSDATAOFF) are appended from ``SUPPLEMENT`` with their spec locations -
the decoded-editing filesystem check resolves a file by its (name, root), so a
same-FID file under another DF must stay a distinct entry.
``tests/test_uicc_files.py`` regenerates the list and fails when pySim adds or ``tests/test_uicc_files.py`` regenerates the list and fails when pySim adds or
renames files, so the asset cannot silently drift. renames files, so the asset cannot silently drift.
@@ -55,6 +60,62 @@ def _entry(fid_path, sym_path, name, fid, kind, root, aid=None):
return out return out
# Files the pySim model does not carry (optional / service-gated), with their
# spec locations: TS 31.102 4.2.86 (EF.LRPLMNSI), 4.2.100 (EF.IAL), 4.2.102
# (EF.IPD), 4.2.108 (EF.TVCONFIG), 4.2.109 (EF.3GPPPSDATAOFF), 4.4.1
# (DF.SoLSA), 4.4.4 (DF.MExE), 4.4.7 (DF.BCAST - its files are OMA BCAST, so
# the DF is listed alone), 4.4.9 (DF.ACDC), 4.4.10 (DF.TV - EF.TVUSD is a
# variable 4Fxx, omitted) and 4.6.6 (DF.A2X); the FID map is
# docs/uicc/UICC_FILES.md 4.13. Entries are (root, parent FID chain, fid,
# name); a DF is a 'DF.' name and precedes its children so the symbolic chain
# can be resolved.
SUPPLEMENT = (
('ADF.USIM', (), '6FDC', 'EF.LRPLMNSI'),
('ADF.USIM', (), '6FF0', 'EF.IAL'),
('ADF.USIM', (), '6FF2', 'EF.IPD'),
('ADF.USIM', (), '6FF9', 'EF.3GPPPSDATAOFF'),
('ADF.USIM', (), '6FFB', 'EF.TVCONFIG'),
('ADF.USIM', (), '5F3C', 'DF.MExE'),
('ADF.USIM', ('5F3C',), '4F40', 'EF.MExE-ST'),
('ADF.USIM', ('5F3C',), '4F41', 'EF.ORPK'),
('ADF.USIM', ('5F3C',), '4F42', 'EF.ARPK'),
('ADF.USIM', ('5F3C',), '4F43', 'EF.TPRPK'),
('ADF.USIM', (), '5F70', 'DF.SoLSA'),
('ADF.USIM', ('5F70',), '4F30', 'EF.SAI'),
('ADF.USIM', ('5F70',), '4F31', 'EF.SLL'),
('ADF.USIM', (), '5F80', 'DF.BCAST'),
('ADF.USIM', (), '5FA0', 'DF.ACDC'),
('ADF.USIM', ('5FA0',), '4F01', 'EF.ACDC_LIST'),
('ADF.USIM', (), '5FB0', 'DF.TV'),
('MF', ('7F10',), '5F3F', 'DF.A2X'),
('MF', ('7F10', '5F3F'), '4F01', 'EF.AST'),
('MF', ('7F10', '5F3F'), '4F02', 'EF.A2X_CONFIG'),
)
# The non-IMSI USIM application repeats the ADF.USIM files under its own root
# (the walk emits it as a separate ADF with the same file set).
USIM_ROOT = 'ADF.USIM'
USIM_MIRROR = 'ADF.USIM-NON-IMSI'
def _add_supplement(out, seen, sym_by_path):
"""Append SUPPLEMENT, mirroring the ADF.USIM files under the non-IMSI root."""
for root, parents, fid, name in SUPPLEMENT:
roots = [root]
if root == USIM_ROOT and USIM_MIRROR in seen:
roots.append(USIM_MIRROR)
for target in roots:
key = '/'.join((target,) + tuple(parents) + (fid,)).upper()
if key in seen:
continue
parent_key = '/'.join((target,) + tuple(parents)).upper()
sym = '%s/%s' % (sym_by_path.get(parent_key, parent_key), name)
seen.add(key)
out.append(_entry(key, sym, name, fid,
'df' if name.startswith('DF.') else 'ef', target))
sym_by_path[key] = sym
def _walk(node, fid_path, sym_path, root, out, seen): def _walk(node, fid_path, sym_path, root, out, seen):
"""Depth-first walk of a DF/ADF subtree (children is a fid -> CardFile dict).""" """Depth-first walk of a DF/ADF subtree (children is a fid -> CardFile dict)."""
for fid, child in sorted(node.children.items()): for fid, child in sorted(node.children.items()):
@@ -116,6 +177,8 @@ def build():
seen.add(key) seen.add(key)
out.append(_entry(key, adf.name, adf.name, '', 'adf', adf.name, app.aid)) out.append(_entry(key, adf.name, adf.name, '', 'adf', adf.name, app.aid))
_walk(adf, key, adf.name, adf.name, out, seen) _walk(adf, key, adf.name, adf.name, out, seen)
# The optional / service-gated files pySim's model does not carry.
_add_supplement(out, seen, {e['path']: e['sym'] for e in out})
# Stable order: MF first, then the ADFs alphabetically; DFs before their EFs # Stable order: MF first, then the ADFs alphabetically; DFs before their EFs
# is implicit in the walk, paths sort consistently inside a root. # is implicit in the walk, paths sort consistently inside a root.
out.sort(key=lambda e: (0 if e['root'] == 'MF' else 1, e['root'], e['path'])) out.sort(key=lambda e: (0 if e['root'] == 'MF' else 1, e['root'], e['path']))
+29 -3
View File
@@ -12,15 +12,41 @@ set VENV_DIR=%~dp0.venv
set LOG=%~dp0simple_lastrun.log set LOG=%~dp0simple_lastrun.log
set PYTHONUNBUFFERED=1 set PYTHONUNBUFFERED=1
REM The address the opening line names (v3.38.2). The server parses the same
REM flags; this mirrors its last-one-wins rule for the echo. %* is not
REM affected by SHIFT, so the server still receives every argument.
set "HTTP_HOST=127.0.0.1"
set "HTTP_PORT=8080"
:parse_http_args
if "%~1"=="" goto http_args_done
if /i "%~1"=="--http-host" (
set "HTTP_HOST=%~2"
shift
shift
goto parse_http_args
)
if /i "%~1"=="--http-port" (
set "HTTP_PORT=%~2"
shift
shift
goto parse_http_args
)
set "ARG=%~1"
if /i "%ARG:~0,12%"=="--http-host=" set "HTTP_HOST=%ARG:~12%"
if /i "%ARG:~0,12%"=="--http-port=" set "HTTP_PORT=%ARG:~12%"
shift
goto parse_http_args
:http_args_done
if exist "%VENV_DIR%\Scripts\pysim-simple-server.exe" ( if exist "%VENV_DIR%\Scripts\pysim-simple-server.exe" (
echo Starting pysim-simple-server from venv on http://127.0.0.1:8080 echo Starting pysim-simple-server from venv on http://%HTTP_HOST%:%HTTP_PORT%
echo Log: simple_lastrun.log (overwritten on each start^). Press Ctrl+C to stop. echo Log: simple_lastrun.log (overwritten on each start^). Press Ctrl+C to stop.
"%VENV_DIR%\Scripts\pysim-simple-server.exe" --http-port 8080 -p 0 %* 2>&1 | powershell -NoProfile -Command "$input | Tee-Object -FilePath '%LOG%'" "%VENV_DIR%\Scripts\pysim-simple-server.exe" --http-port 8080 -p 0 %* 2>&1 | powershell -NoProfile -Command "$input | Tee-Object -FilePath '%LOG%'"
goto :eof goto :eof
) )
if exist "%~dp0pysim_simple_server\__main__.py" ( if exist "%~dp0pysim_simple_server\__main__.py" (
echo Starting pysim-simple-server from source on http://127.0.0.1:8080 echo Starting pysim-simple-server from source on http://%HTTP_HOST%:%HTTP_PORT%
echo Log: simple_lastrun.log (overwritten on each start^). Press Ctrl+C to stop. echo Log: simple_lastrun.log (overwritten on each start^). Press Ctrl+C to stop.
python -m pysim_simple_server --http-port 8080 -p 0 %* 2>&1 | powershell -NoProfile -Command "$input | Tee-Object -FilePath '%LOG%'" python -m pysim_simple_server --http-port 8080 -p 0 %* 2>&1 | powershell -NoProfile -Command "$input | Tee-Object -FilePath '%LOG%'"
goto :eof goto :eof
@@ -28,7 +54,7 @@ if exist "%~dp0pysim_simple_server\__main__.py" (
where pysim-simple-server >nul 2>&1 where pysim-simple-server >nul 2>&1
if %errorlevel% equ 0 ( if %errorlevel% equ 0 (
echo Starting pysim-simple-server on http://127.0.0.1:8080 echo Starting pysim-simple-server on http://%HTTP_HOST%:%HTTP_PORT%
echo Log: simple_lastrun.log (overwritten on each start^). Press Ctrl+C to stop. echo Log: simple_lastrun.log (overwritten on each start^). Press Ctrl+C to stop.
pysim-simple-server --http-port 8080 -p 0 %* 2>&1 | powershell -NoProfile -Command "$input | Tee-Object -FilePath '%LOG%'" pysim-simple-server --http-port 8080 -p 0 %* 2>&1 | powershell -NoProfile -Command "$input | Tee-Object -FilePath '%LOG%'"
goto :eof goto :eof
+20 -2
View File
@@ -10,6 +10,24 @@ set -e
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
VENV_DIR="$SCRIPT_DIR/.venv" VENV_DIR="$SCRIPT_DIR/.venv"
# The address the opening line names (v3.38.2). The server parses the same
# flags; start.sh passes its own --http-port 8080 before "$@", so a later user
# value wins (argparse keeps the last one) - mirror that here.
HTTP_HOST=127.0.0.1
HTTP_PORT=8080
PREV_ARG=""
for arg in "$@"; do
case "$PREV_ARG" in
--http-host) HTTP_HOST="$arg" ;;
--http-port) HTTP_PORT="$arg" ;;
esac
case "$arg" in
--http-host=*) HTTP_HOST="${arg#*=}" ;;
--http-port=*) HTTP_PORT="${arg#*=}" ;;
esac
PREV_ARG="$arg"
done
# Console + log file. The log contains card keys and request payloads (test # Console + log file. The log contains card keys and request payloads (test
# cards) - it is gitignored and must not be committed. # cards) - it is gitignored and must not be committed.
LOG_FILE="$SCRIPT_DIR/simple_lastrun.log" LOG_FILE="$SCRIPT_DIR/simple_lastrun.log"
@@ -73,7 +91,7 @@ if [ -f "$VENV_DIR/bin/pysim-simple-server" ]; then
elif command -v pysim-simple-server &> /dev/null; then elif command -v pysim-simple-server &> /dev/null; then
SERVER="pysim-simple-server" SERVER="pysim-simple-server"
elif [ -f "$SCRIPT_DIR/pysim_simple_server/__main__.py" ]; then elif [ -f "$SCRIPT_DIR/pysim_simple_server/__main__.py" ]; then
echo "Starting pysim-simple-server from source on http://127.0.0.1:8080" echo "Starting pysim-simple-server from source on http://$HTTP_HOST:$HTTP_PORT"
echo "Log: simple_lastrun.log (overwritten on each start)." echo "Log: simple_lastrun.log (overwritten on each start)."
cd "$SCRIPT_DIR" && python3 -m pysim_simple_server --http-port 8080 $READER_ARGS "$@" cd "$SCRIPT_DIR" && python3 -m pysim_simple_server --http-port 8080 $READER_ARGS "$@"
exit $? exit $?
@@ -84,7 +102,7 @@ else
exit 1 exit 1
fi fi
echo "Starting pysim-simple-server on http://127.0.0.1:8080" echo "Starting pysim-simple-server on http://$HTTP_HOST:$HTTP_PORT"
echo "Log: simple_lastrun.log (overwritten on each start)." echo "Log: simple_lastrun.log (overwritten on each start)."
echo "Press Ctrl+C to stop." echo "Press Ctrl+C to stop."
echo "Extra arguments are passed to the server (e.g. ./start.sh --gsmtap)." echo "Extra arguments are passed to the server (e.g. ./start.sh --gsmtap)."
+15
View File
@@ -150,6 +150,21 @@ class SourcePins(unittest.TestCase):
# the old "is pcscd running" heuristic is gone # the old "is pcscd running" heuristic is gone
self.assertNotIn('pgrep -x pcscd', src) self.assertNotIn('pgrep -x pcscd', src)
def test_the_start_message_names_the_requested_address(self):
src = (Path(m.__file__).parent.parent / 'start.sh').read_text()
self.assertIn('--http-host=*) HTTP_HOST="${arg#*=}"', src)
self.assertIn('--http-port=*) HTTP_PORT="${arg#*=}"', src)
self.assertIn('http://$HTTP_HOST:$HTTP_PORT', src)
# the old hardcoded address is gone (the defaults are named separately)
self.assertNotIn('http://127.0.0.1:8080', src)
def test_the_start_bat_message_names_the_requested_address(self):
src = (Path(m.__file__).parent.parent / 'start.bat').read_text()
self.assertIn('--http-host=', src)
self.assertIn('--http-port=', src)
self.assertIn('%HTTP_HOST%:%HTTP_PORT%', src)
self.assertNotIn('http://127.0.0.1:8080', src)
if __name__ == '__main__': if __name__ == '__main__':
unittest.main() unittest.main()
+54
View File
@@ -72,6 +72,60 @@ class TestUiccFilesAsset(unittest.TestCase):
self.assertRegex(f['fid'], r'^[0-9A-F]{4}$') self.assertRegex(f['fid'], r'^[0-9A-F]{4}$')
self.assertTrue(f['name'].startswith(('EF.', 'DF.'))) self.assertTrue(f['name'].startswith(('EF.', 'DF.')))
def test_optional_files_missing_from_the_pysim_model(self):
"""The service-gated files pySim does not model (the SUPPLEMENT)."""
files = _load()['files']
by_path = {f['path']: f for f in files}
expected = {
# TS 31.102 4.2.86 / 4.2.100 / 4.2.102 / 4.2.108 / 4.2.109
'ADF.USIM/6FDC': 'EF.LRPLMNSI',
'ADF.USIM/6FF0': 'EF.IAL',
'ADF.USIM/6FF2': 'EF.IPD',
'ADF.USIM/6FF9': 'EF.3GPPPSDATAOFF',
'ADF.USIM/6FFB': 'EF.TVCONFIG',
# 4.4.4 DF.MExE
'ADF.USIM/5F3C': 'DF.MExE',
'ADF.USIM/5F3C/4F40': 'EF.MExE-ST',
'ADF.USIM/5F3C/4F41': 'EF.ORPK',
'ADF.USIM/5F3C/4F42': 'EF.ARPK',
'ADF.USIM/5F3C/4F43': 'EF.TPRPK',
# 4.4.1 DF.SoLSA
'ADF.USIM/5F70': 'DF.SoLSA',
'ADF.USIM/5F70/4F30': 'EF.SAI',
'ADF.USIM/5F70/4F31': 'EF.SLL',
# 4.4.7 / 4.4.9 / 4.4.10
'ADF.USIM/5F80': 'DF.BCAST',
'ADF.USIM/5FA0': 'DF.ACDC',
'ADF.USIM/5FA0/4F01': 'EF.ACDC_LIST',
'ADF.USIM/5FB0': 'DF.TV',
# 4.6.6 DF.A2X (DF.TELECOM)
'MF/7F10/5F3F': 'DF.A2X',
'MF/7F10/5F3F/4F01': 'EF.AST',
'MF/7F10/5F3F/4F02': 'EF.A2X_CONFIG',
}
for path, name in expected.items():
self.assertIn(path, by_path)
self.assertEqual(by_path[path]['name'], name)
self.assertEqual(by_path[path]['root'], path.split('/')[0])
self.assertEqual(by_path[path]['kind'],
'df' if name.startswith('DF.') else 'ef')
# The same FID under another DF stays a distinct entry (name, path).
self.assertEqual(by_path['ADF.USIM/5F3A/4F30']['name'], 'EF.PBR')
self.assertEqual(by_path['MF/7F10/5F3A/4F30']['name'], 'EF.PBR')
# The ADF.USIM files are mirrored under the non-IMSI USIM root.
for path, name in expected.items():
if not path.startswith('ADF.USIM/'):
continue
mirror = 'ADF.USIM-NON-IMSI' + path[len('ADF.USIM'):]
self.assertIn(mirror, by_path)
self.assertEqual(by_path[mirror]['name'], name)
self.assertEqual(by_path[mirror]['root'], 'ADF.USIM-NON-IMSI')
# (name, root) resolves each SUPPLEMENT file to exactly one path.
for path, name in expected.items():
root = path.split('/')[0]
self.assertEqual([f['path'] for f in files
if f['name'] == name and f['root'] == root], [path])
if __name__ == '__main__': if __name__ == '__main__':
unittest.main() unittest.main()