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.)
This commit is contained in:
+11
-6
@@ -300,14 +300,19 @@ de-allocates.
|
||||
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**: many services declare a DF or EF (ADN -> EF.ADN 6F3A,
|
||||
SPN -> EF.SPN 6F46, GSM Access -> DF.GSM, GPRS -> EF.KcGPRS, …); a second
|
||||
curated table maps a feature to the file(s) it requires. A **Check** action
|
||||
probes the equipped card for those files (reusing the file-manager presence
|
||||
state / a targeted SELECT) and flags mismatches — a declared service whose
|
||||
- **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 FID(s) its `Filesystem` cell declares.
|
||||
A **Check filesystem** action writes the services, 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. Services
|
||||
that map to a capability rather than a file (CHV1 disable, Proactive SIM,
|
||||
…) carry no file check. The result is cached per `card_session`.
|
||||
…) carry no file check. This first pass matches **FID-level**, not
|
||||
(DF, FID)-exact as finding 6 asks: the documented FID collisions are not
|
||||
disambiguated and the report says so (`docs/services-editor.md` §B.0). The
|
||||
last report is kept in memory and dropped with the cached reads on a
|
||||
card-session change.
|
||||
|
||||
### 6.1 Candidates for the same cross-application treatment
|
||||
|
||||
|
||||
+48
-4
@@ -9,8 +9,9 @@
|
||||
>
|
||||
> 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`. §B (the
|
||||
> filesystem check) is **not** implemented yet.
|
||||
> 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 FID-level 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,
|
||||
@@ -132,7 +133,7 @@ Names are canonicalised; wording aliases are listed in Notes §1.
|
||||
| 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 6F3A) |
|
||||
| 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 |
|
||||
@@ -226,6 +227,49 @@ The FS column in §A is deliverable B, merged for readability. Legend:
|
||||
- `?` — 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 **every FID the cell mentions**, in the cell's order:
|
||||
|
||||
- `EF.FDN 6F3B (+ EF.EXT2 6F4B)` → `['6F3B', '6F4B']`
|
||||
- `DF.GRAPHICS 5F50 → EF.IMG 4F20` → `['5F50', '4F20']` (a DF's FID counts:
|
||||
the DF itself is the file the service gates)
|
||||
- `EF.NIA 6F51 (SIM) / 6FD3 (USIM)` → both trees' FIDs
|
||||
- `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 **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), and compares the two sides:
|
||||
|
||||
- `missing` — a **declared** feature (any of its service bits set in a present
|
||||
file; EF.SST only at *allocated + activated*, the state the editor shows)
|
||||
whose mapped FIDs are absent from the card. In §A order.
|
||||
- `extra` — a **present** FID that no declared feature maps. Sorted by FID,
|
||||
naming every feature that maps it (all of them undeclared). A FID **any**
|
||||
declared feature maps counts as explained and is not reported: several
|
||||
features share one file (`4F01` under the ACDC/MCS/5GS/V2X/5G ProSe/5MBS/A2X
|
||||
DFs, `6F4A`/`6F4B` across EXT1/EXT2 and BDN) and the rule cannot tell which
|
||||
DF the present copy sits in.
|
||||
|
||||
The check is **FID-level, not (DF, FID)-exact** (this first pass; finding 6 in
|
||||
`docs/decoded-editing.md` asks for DF-scoped resolution). The FID collisions
|
||||
of Notes §3 — `6F4E` EXT4/EXT5, `6F4F` ECCP/CCP2, `6F07` IMSI/IST, `6F38`
|
||||
SST/UST, `6FD7` MSK/GBANL, `5F3B` DF.GSM-ACCESS/DF.MULTIMEDIA — are therefore
|
||||
not disambiguated, and the report says so. `EF.PBR 4F30` also exists in
|
||||
several DFs, so a present `4F30` explains `ADN`/`SoLSA` regardless of which
|
||||
copy it is.
|
||||
|
||||
The pure pieces (`servicesFsDeclared`, `servicesFsPresentFids`,
|
||||
`servicesFsReport`, `servicesFsReportHtml`) are DOM-free and unit-tested; the
|
||||
last report is kept in memory 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
|
||||
@@ -251,7 +295,7 @@ The three initially-unresolved mappings were settled from the spec text
|
||||
- **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 6F3A (the eCall procedures).
|
||||
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,
|
||||
|
||||
@@ -1176,6 +1176,13 @@
|
||||
<div id="pysim-services-note" class="mb-2 text-xs text-amber-600 dark:text-amber-400"></div>
|
||||
<div id="pysim-services-grid" class="overflow-x-auto"></div>
|
||||
<div id="pysim-services-save" class="flex flex-wrap gap-x-4 gap-y-2 mt-3"></div>
|
||||
<div class="mt-4 border-t border-gray-200 dark:border-slate-700 pt-3">
|
||||
<div class="flex items-center gap-2">
|
||||
<button id="pysim-services-fs-btn" data-needs="card" onclick="pysimServicesFsCheck()" class="px-2 py-1 text-xs rounded border border-gray-300 dark:border-slate-600 hover:bg-gray-200 dark:hover:bg-slate-700 text-gray-700 dark:text-slate-300 disabled:opacity-40 disabled:cursor-not-allowed" data-l10n="Check filesystem">Check filesystem</button>
|
||||
<span id="pysim-services-fs-status" class="text-xs font-mono text-gray-500 dark:text-slate-400" style="min-height:1.2em"></span>
|
||||
</div>
|
||||
<div id="pysim-services-fs-report" class="mt-2"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@@ -16068,6 +16075,7 @@ function pysimFsSetSort(mode) {
|
||||
async function pysimFsRefresh() {
|
||||
const detailEl = document.getElementById('pysim-fs-detail');
|
||||
detailEl.classList.add('hidden');
|
||||
_pysimFsProbed = false;
|
||||
const data = await pysimFetch('/api/tree', { name: 'MF' });
|
||||
pysimFsTreeRoot = {
|
||||
name: data.name || 'MF',
|
||||
@@ -16121,6 +16129,10 @@ async function pysimFsLoadChildren(node) {
|
||||
}
|
||||
|
||||
let _pysimFsProbe = null;
|
||||
// Whether the tree's presence state comes from a completed probe (a refresh
|
||||
// resets it): /api/tree lists the model's files without testing them, so a
|
||||
// loaded tree alone says nothing about what the card actually has.
|
||||
let _pysimFsProbed = false;
|
||||
|
||||
function pysimFsSelectBody(node) {
|
||||
const body = { name: node.name, fid: node.fid, parent_sel: getParentSel(node), parent_path: getParentPath(node) };
|
||||
@@ -16196,6 +16208,7 @@ async function pysimFsProbeAll() {
|
||||
+ probe.present + ' ' + t('present') + ', ' + probe.absent + ' ' + t('absent') + ' (' + secs + 's)';
|
||||
if (error) summary = t('Error') + ': ' + error + ' — ' + summary;
|
||||
else if (probe.stop) summary = t('Stopped') + ' — ' + summary;
|
||||
_pysimFsProbed = !probe.stop && !error;
|
||||
_pysimFsProbe = null;
|
||||
pysimFsProbeUi(summary, false);
|
||||
}
|
||||
@@ -23288,6 +23301,192 @@ const SERVICES_FILES = [
|
||||
|
||||
const SERVICES_TABLES = { ust: EF_UST_SERVICES, est: EF_EST_SERVICES, ist: EF_IST_SERVICES };
|
||||
|
||||
// Feature -> the FID(s) its Filesystem cell declares, transcribed from
|
||||
// docs/services-editor.md §A (the merged "Filesystem" column, legend in §B):
|
||||
// every FID the cell mentions, in order - `EF.FDN 6F3B (+ EF.EXT2 6F4B)`
|
||||
// -> ['6F3B', '6F4B'], `DF.GRAPHICS 5F50 -> EF.IMG 4F20` -> ['5F50', '4F20']
|
||||
// (a DF's FID counts: the DF itself is the file the service gates),
|
||||
// `EF.NIA 6F51 (SIM) / 6FD3 (USIM)` -> both trees' FIDs.
|
||||
// `capability` cells (procedural/terminal-side features with no file of their
|
||||
// own) map to [] - their parenthetical references (a gate, a title, a URI
|
||||
// set) are informational, not a file the service declares - and so do the
|
||||
// §A.4 RFU/reserved rows, which are not features and therefore absent here.
|
||||
// The map is keyed by feature name because that is the cross-reference's key
|
||||
// (the editor's row identity); SERVICES_FILES above is the four *service
|
||||
// table* descriptors, so the name is taken. Coverage is pinned by
|
||||
// frontend/tests/services.test.js.
|
||||
//
|
||||
// The check is deliberately FID-level, not (DF, FID)-exact (docs/decoded-
|
||||
// editing.md finding 6 asks for DF-scoped resolution; this first pass accepts
|
||||
// the coarser rule, visible to the user as the report's note): the known
|
||||
// collisions - 6F4E EXT4/EXT5, 6F4F ECCP/CCP2, 6F07 IMSI/IST, 6F38 SST/UST,
|
||||
// 6FD7 MSK/GBANL, 5F3B DF.GSM-ACCESS/DF.MULTIMEDIA - are matched by FID
|
||||
// anywhere in the card's tree, so a file carrying a colliding FID can explain
|
||||
// a service it does not actually gate.
|
||||
const SERVICES_FS_MAP = {
|
||||
'CHV1 disable function': [],
|
||||
'Local Phone Book (ADN)': ['6F3A', '5F3A', '4F30'],
|
||||
'Fixed Dialling Numbers (FDN)': ['6F3B', '6F4B'],
|
||||
'Short Message Storage (SMS)': ['6F3C'],
|
||||
'Advice of Charge (AoC)': ['6F39', '6F37', '6F41'],
|
||||
'Capability Configuration Parameters (CCP)': ['6F3D'],
|
||||
'Capability Configuration Parameters 2 (CCP2)': ['6F4F'],
|
||||
'PLMN selector': ['6F30'],
|
||||
'MSISDN': ['6F40'],
|
||||
'Extension 1': ['6F4A'],
|
||||
'Extension 2': ['6F4B'],
|
||||
'SMS Parameters (SMSP)': ['6F42'],
|
||||
'Last Number Dialled (LND)': ['6F44'],
|
||||
'Cell Broadcast Message Identifier (CBMI)': ['6F45'],
|
||||
'Group Identifier Level 1 (GID1)': ['6F3E'],
|
||||
'Group Identifier Level 2 (GID2)': ['6F3F'],
|
||||
'Service Provider Name (SPN)': ['6F46'],
|
||||
'Service Dialling Numbers (SDN)': ['6F49', '6F4C'],
|
||||
'Extension 3': ['6F4C'],
|
||||
'VGCS Group Identifier List': ['6FB1', '6FB2'],
|
||||
'VBS Group Identifier List': ['6FB3', '6FB4'],
|
||||
'enhanced Multi-Level Precedence and Pre-emption (eMLPP)': ['6FB5'],
|
||||
'Automatic Answer for eMLPP (AAeM)': ['6FB6'],
|
||||
'Data download via SMS-CB': [],
|
||||
'Data download via SMS-PP': [],
|
||||
'Menu selection': [],
|
||||
'Call control (by SIM/USIM)': [],
|
||||
'Proactive SIM': [],
|
||||
'Cell Broadcast Message Identifier Ranges (CBMIR)': ['6F50'],
|
||||
'Barred Dialling Numbers (BDN)': ['6F4D', '6F4E', '6F55'],
|
||||
'Extension 4': ['6F4E', '6F55'],
|
||||
'De-personalisation Control Keys (DCK)': ['6F2C'],
|
||||
'Co-operative Network List (CNL)': ['6F32'],
|
||||
'Short Message Status Reports (SMSR)': ['6F47'],
|
||||
'Network\'s indication of alerting in the MS (NIA)': ['6F51', '6FD3'],
|
||||
'MO-SMS control (by SIM/USIM)': [],
|
||||
'GPRS': ['6F52', '7F20'],
|
||||
'Image (IMG)': ['5F50', '4F20'],
|
||||
'SoLSA': ['5F70', '4F30', '4F31'],
|
||||
'USSD string data object supported in Call Control': [],
|
||||
'RUN AT COMMAND command': [],
|
||||
'User controlled PLMN selector with Access Technology': ['6F60'],
|
||||
'Operator controlled PLMN selector with Access Technology': ['6F61'],
|
||||
'HPLMN selector with Access Technology': ['6F62'],
|
||||
'CPBCCH Information': ['6F63', '4F63'],
|
||||
'Investigation Scan': ['6F64', '4F64'],
|
||||
'Extended Capability Configuration Parameters (ECCP)': ['6F4F'],
|
||||
'MExE': ['5F3C', '4F40'],
|
||||
'PLMN Network Name (PNN)': ['6FC5'],
|
||||
'Operator PLMN List (OPL)': ['6FC6'],
|
||||
'Mailbox Dialling Numbers (MBDN)': ['6FC7', '6FC8'],
|
||||
'Message Waiting Indication Status (MWIS)': ['6FCA', '6FC9'],
|
||||
'Call Forwarding Indication Status (CFIS)': ['6FCB', '6FCC'],
|
||||
'Service Provider Display Information (SPDI)': ['6FCD'],
|
||||
'Multimedia Messaging Service (MMS)': ['6FCE'],
|
||||
'Extension 8': ['6FCF'],
|
||||
'MMS User Connectivity Parameters': ['6FD0', '6FD2', '6FD1'],
|
||||
'Outgoing Call Information (OCI and OCT)': ['6F81', '6F83'],
|
||||
'Incoming Call Information (ICI and ICT)': ['6F80', '6F82'],
|
||||
'GSM Access': ['5F3B'],
|
||||
'Enabled Services Table': ['6F56'],
|
||||
'APN Control List (ACL)': ['6F57'],
|
||||
'GSM security context': ['4F20', '4F52', '5F3B'],
|
||||
'Extension 5': ['6F4E'],
|
||||
'Call control on GPRS by USIM': [],
|
||||
'Pseudonym': ['4F41', '5F40'],
|
||||
'User Controlled PLMN selector for I-WLAN access': ['4F42'],
|
||||
'Operator Controlled PLMN selector for I-WLAN access': ['4F43'],
|
||||
'User controlled WSID list': ['4F44'],
|
||||
'Operator controlled WSID list': ['4F45'],
|
||||
'VGCS security': ['6FD4'],
|
||||
'VBS security': ['6FD5'],
|
||||
'WLAN Reauthentication Identity': ['4F46'],
|
||||
'Multimedia Messages Storage': ['4F47', '4F48', '5F3B'],
|
||||
'Generic Bootstrapping Architecture (GBA)': ['6FD6', '6FD5', '6FDA', '6FD7', '6FDD'],
|
||||
'MBMS security': ['6FD7', '6FD8'],
|
||||
'Data download via USSD and USSD application mode': [],
|
||||
'Equivalent HPLMN': ['6FD9'],
|
||||
'Additional TERMINAL PROFILE after UICC activation': [],
|
||||
'Equivalent HPLMN Presentation Indication': ['6FDB'],
|
||||
'Last RPLMN Selection Indication': ['6FDC'],
|
||||
'OMA BCAST Smart Card Profile': ['5F80'],
|
||||
'GBA-based Local Key Establishment Mechanism': [],
|
||||
'Terminal Applications': [],
|
||||
'Service Provider Name Icon': ['6FDE'],
|
||||
'PLMN Network Name Icon': ['6FDF'],
|
||||
'Connectivity Parameters for USIM IP connections': ['6FE2'],
|
||||
'Home I-WLAN Specific Identifier List': ['4F47'],
|
||||
'I-WLAN Equivalent HPLMN Presentation Indication': ['4F48'],
|
||||
'I-WLAN HPLMN Priority Indication': ['4F49'],
|
||||
'I-WLAN Last Registered PLMN': ['4F4A'],
|
||||
'EPS Mobility Management Information': ['6FE3', '6FE4'],
|
||||
'Allowed CSG Lists and corresponding indications': ['4F81', '4F82', '4F83'],
|
||||
'Call control on EPS PDN connection by USIM': [],
|
||||
'HPLMN Direct Access': ['4F4B'],
|
||||
'eCall Data': ['6F49', '6F3B'],
|
||||
'Operator CSG Lists and corresponding indications': ['4F84', '4F85', '4F86'],
|
||||
'Support for SM-over-IP': [],
|
||||
'Support of CSG Display Control': [],
|
||||
'Communication Control for IMS': [],
|
||||
'Extended Terminal Applications': [],
|
||||
'Support of UICC access to IMS': [],
|
||||
'Non-Access Stratum configuration by USIM': ['6FE8'],
|
||||
'PWS configuration by USIM': ['6FEC'],
|
||||
'URI support by UICC': [],
|
||||
'Extended EARFCN support': [],
|
||||
'ProSe': ['5F90', '4F10'],
|
||||
'USAT Application Pairing': ['6FF1', '6FF0', '6FF2'],
|
||||
'Media Type support': [],
|
||||
'IMS call disconnection cause': [],
|
||||
'URI support for MO SHORT MESSAGE CONTROL': [],
|
||||
'ePDG configuration Information support': ['6FF4'],
|
||||
'ePDG configuration Information configured': ['6FF3'],
|
||||
'ACDC support': ['5FA0', '4F01'],
|
||||
'MCPTT': ['5F3D', '4F01'],
|
||||
'ePDG configuration Information for Emergency Service support': ['6FF6'],
|
||||
'ePDG configuration Information for Emergency Service configured': ['6FF5'],
|
||||
'eCall Data over IMS': ['6FEF', '6FED'],
|
||||
'URI support for SMS-PP DOWNLOAD': [],
|
||||
'From Preferred': ['6FF7'],
|
||||
'IMS configuration data': ['6FF8'],
|
||||
'TV configuration': ['6FFB', '5FB0'],
|
||||
'3GPP PS Data Off': ['6FF9'],
|
||||
'3GPP PS Data Off Service List': ['6FFA'],
|
||||
'V2X': ['5F3E', '4F01', '4F02'],
|
||||
'XCAP Configuration Data': ['6FFC'],
|
||||
'EARFCN list for MTC/NB-IOT UEs': ['6FFD'],
|
||||
'5GS Mobility Management Information': ['5FC0', '4F01', '4F02'],
|
||||
'5G Security Parameters': ['4F05', '4F03', '4F04'],
|
||||
'Subscription identifier privacy support': [],
|
||||
'SUCI calculation by the USIM': ['4F07'],
|
||||
'UAC Access Identities support': ['4F06'],
|
||||
'Expect control plane-based Steering of Roaming information during initial registration in VPLMN': [],
|
||||
'Call control on PDU Session by USIM': [],
|
||||
'5GS Operator PLMN List': ['4F08'],
|
||||
'Support for SUPI of type NSI or GLI or GCI': ['4F09'],
|
||||
'3GPP PS Data Off separate Home and Roaming lists': [],
|
||||
'Support for URSP by USIM': ['4F0B'],
|
||||
'5G Security Parameters extended': [],
|
||||
'MuD and MiD configuration data': ['6FFE'],
|
||||
'Support for Trusted non-3GPP access networks by USIM': [],
|
||||
'Support for multiple records of NAS security context storage for multiple registration': ['4F03', '4F04'],
|
||||
'Pre-configured CAG information list': ['4F0D'],
|
||||
'SOR-CMCI storage in USIM': ['4F0E'],
|
||||
'5G ProSe': ['5FF0', '4F01'],
|
||||
'Storage of disaster roaming information in USIM': ['4F0F'],
|
||||
'Pre-configured eDRX parameters': ['4F10'],
|
||||
'5G NSWO support': ['4F11'],
|
||||
'PWS configuration for SNPN in USIM': ['4F01', '5FE0'],
|
||||
'Multiplier Coefficient for Higher Priority PLMN search via NG-RAN satellite access': ['4F15'],
|
||||
'K_AUSF derivation configuration': ['4F16'],
|
||||
'Network Identifier for SNPN (NID)': ['4F02', '5FE0'],
|
||||
'5MBS UE pre-configuration': ['5FF1', '4F01'],
|
||||
'Operator Controlled Signal Threshold per Access Technology': ['6F02'],
|
||||
'A2X': ['5F3F', '4F01'],
|
||||
'IMS Data Channel Indication': ['6F0B'],
|
||||
'P-CSCF address': ['6F09'],
|
||||
'HTTP Digest': [],
|
||||
'Support of P-CSCF discovery for IMS Local Break Out': [],
|
||||
'WebRTC URI': ['6FFA'],
|
||||
'IMS Data Channel indication': ['6F0B'],
|
||||
};
|
||||
|
||||
function servicesDecode(key, bytes) {
|
||||
if (key === 'sst') return efDecSst(bytes);
|
||||
return efDecServiceTable(bytes, SERVICES_TABLES[key]);
|
||||
@@ -23406,11 +23605,143 @@ function servicesFileBytes(key, values, files) {
|
||||
return efEncServiceTable(servicesTableValues(values, key, f.hex), f.hex, table);
|
||||
}
|
||||
|
||||
// --- filesystem check (docs/services-editor.md §B) ---
|
||||
// Three pure pieces: which features the card declares (from the same decoded
|
||||
// service tables the grid renders), which FIDs the card actually has (from
|
||||
// the file manager's probed tree) and the mismatch report between the two.
|
||||
|
||||
// Feature names whose service bit is set in at least one of the present
|
||||
// service files - the "declared" side of the check. A file that is absent
|
||||
// from the card leaves its column null (servicesFeatureState), so it declares
|
||||
// nothing; EF.SST counts only at 'on' (allocated + activated), as the editor
|
||||
// renders it.
|
||||
function servicesFsDeclared(files) {
|
||||
const dec = key => (files && files[key] && files[key].present) ? files[key].dec : null;
|
||||
const sstDec = dec('sst'), ustDec = dec('ust'), estDec = dec('est'), istDec = dec('ist');
|
||||
const out = [];
|
||||
for (const f of SERVICES_CROSS_REF) {
|
||||
const st = servicesFeatureState(f, sstDec, ustDec, estDec, istDec);
|
||||
if (st.sst === 'on' || st.ust === true || st.est === true || st.ist === true) out.push(f.f);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// FIDs present on the card: any node of the file-manager tree that the probe
|
||||
// confirmed (exists === true), DFs included - a service gating a whole DF
|
||||
// declares the DF's FID. A node that was never probed (exists === null) is
|
||||
// not counted, so an unprobed tree reports nothing as present, and a DF
|
||||
// confirmed absent is not descended into.
|
||||
function servicesFsPresentFids(root) {
|
||||
const out = [];
|
||||
const seen = {};
|
||||
const walk = node => {
|
||||
for (const ch of ((node && node.children) || [])) {
|
||||
const fid = String(ch.fid || '').toUpperCase();
|
||||
if (ch.exists === true && /^[0-9A-F]{4}$/.test(fid) && !seen[fid]) {
|
||||
seen[fid] = true;
|
||||
out.push(fid);
|
||||
}
|
||||
if (ch.isDir && ch.exists !== false) walk(ch);
|
||||
}
|
||||
};
|
||||
walk(root);
|
||||
return out;
|
||||
}
|
||||
|
||||
// File names per FID from the same tree, for the report: a FID may carry one
|
||||
// name per DF (the collisions the note names), so the value is a list.
|
||||
function servicesFsFidNames(root) {
|
||||
const names = {};
|
||||
const walk = node => {
|
||||
for (const ch of ((node && node.children) || [])) {
|
||||
const fid = String(ch.fid || '').toUpperCase();
|
||||
if (/^[0-9A-F]{4}$/.test(fid) && ch.name) {
|
||||
if (!names[fid]) names[fid] = [];
|
||||
if (names[fid].indexOf(ch.name) < 0) names[fid].push(ch.name);
|
||||
}
|
||||
if (ch.isDir && ch.exists !== false) walk(ch);
|
||||
}
|
||||
};
|
||||
walk(root);
|
||||
return names;
|
||||
}
|
||||
|
||||
// The mismatch report, deterministic and DOM-free:
|
||||
// missing: [{ feature, fids }] - declared features with absent files, in
|
||||
// cross-reference order, `fids` in the map's (doc) order;
|
||||
// extra: [{ fid, features }] - present FIDs no declared feature explains,
|
||||
// sorted by FID, `features` = every feature mapping that FID (all
|
||||
// of them undeclared). A FID that any declared feature maps is
|
||||
// explained and not reported: several features legitimately share
|
||||
// one file (4F01 under the 5G/ACDC/MCPTT DFs, 6F4A/6F4B across
|
||||
// EXT1/EXT2 and BDN), and the FID-level rule cannot tell which DF
|
||||
// the present file sits in.
|
||||
function servicesFsReport(declared, presentFids) {
|
||||
const declaredSet = {};
|
||||
for (const name of (declared || [])) declaredSet[name] = true;
|
||||
const presentSet = {};
|
||||
for (const fid of (presentFids || [])) presentSet[String(fid).toUpperCase()] = true;
|
||||
const missing = [];
|
||||
const byFid = {};
|
||||
for (const f of SERVICES_CROSS_REF) {
|
||||
const fids = SERVICES_FS_MAP[f.f] || [];
|
||||
for (const fid of fids) {
|
||||
if (!byFid[fid]) byFid[fid] = [];
|
||||
byFid[fid].push(f.f);
|
||||
}
|
||||
if (!declaredSet[f.f]) continue;
|
||||
const absent = fids.filter(fid => !presentSet[fid]);
|
||||
if (absent.length) missing.push({ feature: f.f, fids: absent });
|
||||
}
|
||||
const extra = Object.keys(byFid).sort()
|
||||
.filter(fid => presentSet[fid] && !byFid[fid].some(name => declaredSet[name]))
|
||||
.map(fid => ({ fid: fid, features: byFid[fid] }));
|
||||
return { missing: missing, extra: extra };
|
||||
}
|
||||
|
||||
function servicesFsReportHtml(report, names) {
|
||||
const note = '<div class="text-xs text-gray-500 dark:text-slate-400 mt-1">' +
|
||||
esc(t('The check is FID-level: a FID is counted anywhere in the card\'s tree, so the collisions across DFs (6F4E EXT4/EXT5, 6F4F ECCP/CCP2, 6F07 IMSI/IST, 6F38 SST/UST) are not disambiguated.')) + '</div>';
|
||||
const head = (text, n) => '<div class="text-xs font-medium mt-2">' + esc(text) + ' (' + n + ')</div>';
|
||||
const item = text => '<li>' + text + '</li>';
|
||||
// the tree's own name(s) for the FID, when the check loaded it
|
||||
const fidLabel = fid => {
|
||||
const list = names && names[fid];
|
||||
return '<span class="font-mono">' + esc(fid) + '</span>' +
|
||||
((list && list.length) ? ' (' + esc(list.join(' / ')) + ')' : '');
|
||||
};
|
||||
const fidList = fids => fids.map(fidLabel).join(', ');
|
||||
if (!report || (!report.missing.length && !report.extra.length)) {
|
||||
return '<div class="text-xs text-emerald-600 dark:text-emerald-400">' +
|
||||
esc(t('The filesystem matches the declared services')) + '</div>' + note;
|
||||
}
|
||||
let html = '';
|
||||
if (report.missing.length) {
|
||||
html += head(t('Declared services whose files are missing'), report.missing.length);
|
||||
html += '<ul class="list-disc ml-4">';
|
||||
for (const m of report.missing) html += item(esc(m.feature) + ': ' + fidList(m.fids));
|
||||
html += '</ul>';
|
||||
}
|
||||
if (report.extra.length) {
|
||||
html += head(t('Present files whose service is not declared'), report.extra.length);
|
||||
html += '<ul class="list-disc ml-4">';
|
||||
for (const e of report.extra) html += item(fidLabel(e.fid) + ': ' + esc(e.features.join(', ')));
|
||||
html += '</ul>';
|
||||
}
|
||||
return html + note;
|
||||
}
|
||||
|
||||
// Presence + contents cached per card session, like the EF.ARR copies
|
||||
// (pysimArrEnsure): the four files are read on open, again on Refresh and
|
||||
// after a card-session change.
|
||||
let _pysimServicesCache = null;
|
||||
let _pysimServicesBusy = false;
|
||||
// The last filesystem-check report, kept so a re-render (language switch) can
|
||||
// redraw it; cleared with the cache on a card-session change. The names map
|
||||
// annotates the report's FIDs with the tree's file names.
|
||||
let _pysimServicesFsReport = null;
|
||||
let _pysimServicesFsNames = null;
|
||||
let _pysimServicesFsBusy = false;
|
||||
|
||||
async function pysimServicesReadFile(spec) {
|
||||
try {
|
||||
@@ -23454,10 +23785,59 @@ async function pysimServicesOpen(force) {
|
||||
// A card-session change drops the cached reads and the rendered grid.
|
||||
function pysimServicesReset() {
|
||||
_pysimServicesCache = null;
|
||||
_pysimServicesFsReport = null;
|
||||
_pysimServicesFsNames = null;
|
||||
pysimServicesRender();
|
||||
if (isViewVisible('pysim-sub-services')) pysimServicesOpen();
|
||||
}
|
||||
|
||||
function pysimServicesFsRender() {
|
||||
const el = document.getElementById('pysim-services-fs-report');
|
||||
if (!el) return;
|
||||
el.innerHTML = _pysimServicesFsReport ? servicesFsReportHtml(_pysimServicesFsReport, _pysimServicesFsNames) : '';
|
||||
}
|
||||
|
||||
// "Check filesystem": compare the declared services with the card's probed
|
||||
// tree. The file manager loads its tree lazily and only probes presence on
|
||||
// demand, so the check runs the full probe once per tree (a loaded but
|
||||
// unprobed tree claims every listed file as present - /api/tree does not test
|
||||
// it).
|
||||
async function pysimServicesFsCheck() {
|
||||
const statusEl = document.getElementById('pysim-services-fs-status');
|
||||
const reportEl = document.getElementById('pysim-services-fs-report');
|
||||
if (_pysimServicesFsBusy || !reportEl) return;
|
||||
const setStatus = text => { if (statusEl) statusEl.textContent = text; };
|
||||
if (_pysimFsProbe) { setStatus(t('A file probe is already running')); return; }
|
||||
_pysimServicesFsBusy = true;
|
||||
_pysimServicesFsReport = null;
|
||||
_pysimServicesFsNames = null;
|
||||
reportEl.innerHTML = '';
|
||||
setStatus(t('Loading...'));
|
||||
try {
|
||||
const cache = await pysimServicesEnsure();
|
||||
if (!cache || !cache.files || !pysimServicesColumns(cache.files).length) {
|
||||
reportEl.innerHTML = '<div class="text-xs text-gray-500 dark:text-slate-400">' +
|
||||
esc(t('No service table found on this card')) + '</div>';
|
||||
return;
|
||||
}
|
||||
if (!pysimFsTreeRoot) await pysimFsRefresh();
|
||||
if (!_pysimFsProbed) {
|
||||
setStatus(t('Probing:'));
|
||||
await pysimFsProbeAll();
|
||||
}
|
||||
_pysimServicesFsReport = servicesFsReport(
|
||||
servicesFsDeclared(cache.files), servicesFsPresentFids(pysimFsTreeRoot));
|
||||
_pysimServicesFsNames = servicesFsFidNames(pysimFsTreeRoot);
|
||||
pysimServicesFsRender();
|
||||
} catch (e) {
|
||||
reportEl.innerHTML = '<div class="text-xs text-amber-600 dark:text-amber-400">' +
|
||||
esc(t('Error') + ': ' + ((e && e.message) ? e.message : String(e))) + '</div>';
|
||||
} finally {
|
||||
setStatus('');
|
||||
_pysimServicesFsBusy = false;
|
||||
}
|
||||
}
|
||||
|
||||
// The form's per-row state: { <feature index>: { sst: 'on'|'off', ust: bool, ... } }
|
||||
function pysimServicesCollect() {
|
||||
const values = {};
|
||||
@@ -23547,6 +23927,7 @@ function pysimServicesRender() {
|
||||
grid.innerHTML = '';
|
||||
if (noteEl) noteEl.innerHTML = '';
|
||||
if (saveEl) saveEl.innerHTML = '';
|
||||
pysimServicesFsRender();
|
||||
return;
|
||||
}
|
||||
const notes = SERVICES_FILES.filter(spec => !(cache.files[spec.key] && cache.files[spec.key].present))
|
||||
@@ -23557,6 +23938,7 @@ function pysimServicesRender() {
|
||||
if (noteEl) noteEl.innerHTML = notes.join('');
|
||||
grid.innerHTML = pysimServicesGridHtml(cache.files, current);
|
||||
if (saveEl) saveEl.innerHTML = pysimServicesSaveRowHtml(cache.files);
|
||||
pysimServicesFsRender();
|
||||
pysimApplyAvailability();
|
||||
}
|
||||
|
||||
@@ -27227,6 +27609,12 @@ const LANG_RU = {
|
||||
'Save to': 'Сохранить в',
|
||||
'No change': 'Без изменений',
|
||||
'Not found on the card': 'не найдено на карте',
|
||||
'Check filesystem': 'Проверить файловую систему',
|
||||
'Declared services whose files are missing': 'Заявленные сервисы, файлы которых отсутствуют',
|
||||
'Present files whose service is not declared': 'Присутствующие файлы, сервис которых не заявлен',
|
||||
'The filesystem matches the declared services': 'Файловая система соответствует заявленным сервисам',
|
||||
'A file probe is already running': 'Опрос файлов уже выполняется',
|
||||
'The check is FID-level: a FID is counted anywhere in the card\'s tree, so the collisions across DFs (6F4E EXT4/EXT5, 6F4F ECCP/CCP2, 6F07 IMSI/IST, 6F38 SST/UST) are not disambiguated.': 'Проверка выполняется на уровне FID: FID учитывается в любом месте дерева карты, поэтому совпадения FID в разных DF (6F4E EXT4/EXT5, 6F4F ECCP/CCP2, 6F07 IMSI/IST, 6F38 SST/UST) не различаются.',
|
||||
'No service table found on this card': 'На этой карте не найдено ни одной таблицы сервисов',
|
||||
'Stop': 'Остановить',
|
||||
'Probing:': 'Проверка:',
|
||||
|
||||
@@ -45,12 +45,14 @@ const FNS = [
|
||||
'servicesSstAllocated', 'servicesSstPreserved', 'servicesBitsPreserved', 'servicesSstValues',
|
||||
'servicesTableValues', 'servicesFileBytes', 'pysimServicesColumns', 'pysimServicesGridHtml',
|
||||
'pysimServicesSaveRowHtml', 'pysimServicesCollect',
|
||||
'servicesFsDeclared', 'servicesFsPresentFids', 'servicesFsFidNames', 'servicesFsReport',
|
||||
'servicesFsReportHtml',
|
||||
];
|
||||
|
||||
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_TABLES']) {
|
||||
'SERVICES_CROSS_REF', 'SERVICES_FILES', 'SERVICES_FS_MAP', 'SERVICES_TABLES']) {
|
||||
code += extractConst(html, c);
|
||||
}
|
||||
code += 'globalThis.esc = s => String(s);\nglobalThis.t = k => k;\n';
|
||||
@@ -302,3 +304,228 @@ test('an absent file yields no bytes', () => {
|
||||
assert.strictEqual(servicesFileBytes('sst', {}, { sst: { present: false } }), null);
|
||||
assert.strictEqual(servicesFileBytes('ust', {}, {}), null);
|
||||
});
|
||||
|
||||
// ---- filesystem check: the feature -> FID table (docs/services-editor.md §B) ----
|
||||
|
||||
const UICC_FILES = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'uicc_files.json'), 'utf8'));
|
||||
|
||||
test('SERVICES_FS_MAP covers every feature exactly once and carries 4-hex FIDs', () => {
|
||||
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, fids] of Object.entries(SERVICES_FS_MAP)) {
|
||||
assert.ok(Array.isArray(fids), 'not an array: ' + name);
|
||||
assert.strictEqual(new Set(fids).size, fids.length, 'duplicate FID: ' + name);
|
||||
for (const fid of fids) assert.match(fid, /^[0-9A-F]{4}$/, name + ': ' + fid);
|
||||
if (fids.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 files map to their documented FIDs', () => {
|
||||
assert.deepStrictEqual(SERVICES_FS_MAP['Fixed Dialling Numbers (FDN)'], ['6F3B', '6F4B']);
|
||||
assert.deepStrictEqual(SERVICES_FS_MAP['Service Provider Name (SPN)'], ['6F46']);
|
||||
assert.deepStrictEqual(SERVICES_FS_MAP['GSM Access'], ['5F3B']);
|
||||
// ADN: the EF plus the USIM's DF.PHONEBOOK and EF.PBR
|
||||
assert.ok(SERVICES_FS_MAP['Local Phone Book (ADN)'].includes('6F3A'));
|
||||
assert.ok(SERVICES_FS_MAP['Short Message Storage (SMS)'].includes('6F3C'));
|
||||
assert.deepStrictEqual(SERVICES_FS_MAP['Image (IMG)'], ['5F50', '4F20']);
|
||||
assert.deepStrictEqual(SERVICES_FS_MAP['Advice of Charge (AoC)'], ['6F39', '6F37', '6F41']);
|
||||
assert.deepStrictEqual(SERVICES_FS_MAP['Extension 4'], ['6F4E', '6F55'],
|
||||
'both trees\' FIDs, per the doc\'s "(SIM) / 6F55 (USIM)"');
|
||||
});
|
||||
|
||||
test('every mapped FID is a known file (pySim\'s cardless list or the documented gap)', () => {
|
||||
const known = new Set(UICC_FILES.files.map(e => e.fid));
|
||||
const mapped = new Set();
|
||||
for (const fids of Object.values(SERVICES_FS_MAP)) for (const fid of fids) mapped.add(fid);
|
||||
// docs/services-editor.md §5: files UICC_FILES.md documents but pySim's
|
||||
// model does not carry (the FS check probes the card, so they are not a
|
||||
// blocker there). 6FDC/6FF0/6FF2 are also outside pySim's profile.
|
||||
const gap = new Set(['4F31', '4F40', '5F3C', '5F3F', '5F70', '5F80', '5FA0', '5FB0',
|
||||
'6FDC', '6FF0', '6FF2', '6FF9', '6FFB']);
|
||||
assert.deepStrictEqual(Array.from(mapped).filter(fid => !known.has(fid)).sort(),
|
||||
Array.from(gap).sort(), 'mapped FIDs outside the cardless list');
|
||||
assert.ok(mapped.size >= 150, 'distinct mapped FIDs: ' + mapped.size);
|
||||
});
|
||||
|
||||
// ---- 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 },
|
||||
});
|
||||
|
||||
test('servicesFsPresentFids collects the probed nodes\' FIDs, recursively', () => {
|
||||
const root = {
|
||||
children: [
|
||||
{ name: 'MF/7F20', fid: '7f20', isDir: true, exists: true, children: [
|
||||
{ name: 'EF.SPN', fid: '6f46', exists: true },
|
||||
{ name: 'EF.LND', fid: '6F44', exists: false },
|
||||
{ name: 'EF.ADN', fid: '6F3A', exists: true },
|
||||
] },
|
||||
{ name: 'EF.ICCID', fid: '2FE2', exists: null }, // never probed
|
||||
{ name: 'DF.GSM-ACCESS', fid: '5F3B', isDir: false, exists: true },
|
||||
{ name: 'no fid', exists: true },
|
||||
{ name: 'MF/2F06', fid: '2F06', isDir: true, exists: false, children: [
|
||||
{ name: 'EF.SST', fid: '6F38', exists: true }, // under an absent DF
|
||||
] },
|
||||
],
|
||||
};
|
||||
assert.deepStrictEqual(servicesFsPresentFids(root), ['7F20', '6F46', '6F3A', '5F3B']);
|
||||
assert.deepStrictEqual(servicesFsPresentFids(null), []);
|
||||
// the same walk supplies the report's file names, one list per collided FID
|
||||
assert.deepStrictEqual(servicesFsFidNames(root), {
|
||||
'7F20': ['MF/7F20'], '6F46': ['EF.SPN'], '6F44': ['EF.LND'], '6F3A': ['EF.ADN'],
|
||||
'2FE2': ['EF.ICCID'], '5F3B': ['DF.GSM-ACCESS'], '2F06': ['MF/2F06'],
|
||||
});
|
||||
assert.deepStrictEqual(servicesFsFidNames(null), {});
|
||||
});
|
||||
|
||||
test('servicesFsDeclared reads the union of the present service files', () => {
|
||||
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 declared = servicesFsDeclared(files);
|
||||
assert.ok(declared.includes('Fixed Dialling Numbers (FDN)'), declared.join(' | '));
|
||||
assert.ok(declared.includes('Short Message Storage (SMS)'), declared.join(' | '));
|
||||
// an allocated-off SST service declares nothing on its own
|
||||
const off = servicesFsDeclared(svcFiles(sstDec({ 4: [1, 0] }), null, null, null));
|
||||
assert.deepStrictEqual(off, []);
|
||||
// no files at all: nothing declared
|
||||
assert.deepStrictEqual(servicesFsDeclared(svcFiles(null, null, null, null)), []);
|
||||
});
|
||||
|
||||
test('servicesFsReport: a declared feature with an absent file', () => {
|
||||
const rep = servicesFsReport(['Fixed Dialling Numbers (FDN)'], ['6F3B']);
|
||||
assert.deepStrictEqual(rep.missing, [{ feature: 'Fixed Dialling Numbers (FDN)', fids: ['6F4B'] }]);
|
||||
assert.deepStrictEqual(rep.extra, []);
|
||||
// both files gone
|
||||
assert.deepStrictEqual(servicesFsReport(['Fixed Dialling Numbers (FDN)'], []).missing,
|
||||
[{ feature: 'Fixed Dialling Numbers (FDN)', fids: ['6F3B', '6F4B'] }]);
|
||||
});
|
||||
|
||||
test('servicesFsReport: a present file whose service is undeclared', () => {
|
||||
const rep = servicesFsReport([], ['6F3C']);
|
||||
assert.deepStrictEqual(rep.missing, []);
|
||||
assert.deepStrictEqual(rep.extra, [{ fid: '6F3C', features: ['Short Message Storage (SMS)'] }]);
|
||||
// a FID several features map lists all of them, in table order
|
||||
assert.deepStrictEqual(servicesFsReport([], ['4F02']).extra,
|
||||
[{ fid: '4F02', features: ['V2X', '5GS Mobility Management Information', 'Network Identifier for SNPN (NID)'] }]);
|
||||
});
|
||||
|
||||
test('servicesFsReport: an empty pair reports no mismatch', () => {
|
||||
assert.deepStrictEqual(servicesFsReport([], []), { missing: [], extra: [] });
|
||||
// a declared feature whose files are all present is not a mismatch either
|
||||
assert.deepStrictEqual(servicesFsReport(['Service Provider Name (SPN)'], ['6F46']),
|
||||
{ missing: [], extra: [] });
|
||||
});
|
||||
|
||||
test('servicesFsReport: a capability feature is never flagged', () => {
|
||||
const rep = servicesFsReport(['CHV1 disable function', 'Proactive SIM', 'Menu selection'], []);
|
||||
assert.deepStrictEqual(rep, { missing: [], extra: [] });
|
||||
});
|
||||
|
||||
test('servicesFsReport: a shared feature flags on its own mapped FIDs', () => {
|
||||
// SMS is declared and present; ADN is declared, nothing of it is on the card
|
||||
const rep = servicesFsReport(['Local Phone Book (ADN)', 'Short Message Storage (SMS)'], ['6F3C']);
|
||||
assert.deepStrictEqual(rep.missing, [{ feature: 'Local Phone Book (ADN)', fids: ['6F3A', '5F3A', '4F30'] }]);
|
||||
assert.deepStrictEqual(rep.extra, [], 'the present SMS file is explained by its declared service');
|
||||
// a present FID any declared feature maps is explained by it
|
||||
assert.deepStrictEqual(servicesFsReport(['MCPTT'], ['4F01']),
|
||||
{ missing: [{ feature: 'MCPTT', fids: ['5F3D'] }], extra: [] });
|
||||
// 4F01 present while nothing that maps it is declared: one extra entry
|
||||
assert.deepStrictEqual(servicesFsReport([], ['4F01']).extra[0].fid, '4F01');
|
||||
assert.ok(servicesFsReport([], ['4F01']).extra[0].features.includes('ACDC support'));
|
||||
});
|
||||
|
||||
test('servicesFsReport is deterministic and order-stable', () => {
|
||||
const declared = ['Service Provider Name (SPN)', 'Fixed Dialling Numbers (FDN)'];
|
||||
const present = ['6f3b', '6F46'];
|
||||
const a = servicesFsReport(declared, present);
|
||||
const b = servicesFsReport(declared.slice().reverse(), present);
|
||||
assert.deepStrictEqual(a, b, 'cross-reference order, not input order');
|
||||
assert.deepStrictEqual(a.missing, [{ feature: 'Fixed Dialling Numbers (FDN)', fids: ['6F4B'] }]);
|
||||
// the lower-case present FIDs matched
|
||||
assert.deepStrictEqual(a.extra, []);
|
||||
// extra is sorted by FID
|
||||
const extra = servicesFsReport(['Service Provider Name (SPN)'], ['6F81', '6F3C', '6F45']).extra;
|
||||
assert.deepStrictEqual(extra.map(e => e.fid), ['6F3C', '6F45', '6F81']);
|
||||
});
|
||||
|
||||
test('servicesFsReportHtml renders the two headings, the match line and the note', () => {
|
||||
const note = 'FID-level';
|
||||
const empty = servicesFsReportHtml(servicesFsReport([], []));
|
||||
assert.ok(empty.includes('The filesystem matches the declared services'), empty);
|
||||
assert.ok(empty.includes(note), empty);
|
||||
const rep = servicesFsReport(['Fixed Dialling Numbers (FDN)'], ['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('6F4B'), html);
|
||||
assert.ok(html.includes('Present files whose service is not declared'), html);
|
||||
assert.ok(html.includes('6F3C'), html);
|
||||
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(servicesFsReport(['Fixed Dialling Numbers (FDN)'], ['6F3B']));
|
||||
assert.ok(one.includes('Declared services whose files are missing'), one);
|
||||
assert.ok(!one.includes('Present files whose service is not declared'), one);
|
||||
// the tree's names annotate the FIDs when the check passes its names map
|
||||
const named = servicesFsReportHtml(rep, { '6F3C': ['EF.SMS'], '6F4B': ['EF.EXT2'] });
|
||||
assert.ok(named.includes('6F4B') && named.includes('(EF.EXT2)'), named);
|
||||
assert.ok(named.includes('6F3C') && named.includes('(EF.SMS)'), named);
|
||||
// ... and a FID the map lacks stays bare
|
||||
assert.ok(!named.includes('6F3A ('), named);
|
||||
});
|
||||
|
||||
// ---- 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-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
|
||||
const fn = extractFunc(html, 'pysimServicesFsCheck');
|
||||
assert.ok(fn.indexOf('await pysimFsRefresh()') > 0, fn);
|
||||
assert.ok(fn.indexOf('await pysimFsProbeAll()') > 0, fn);
|
||||
assert.ok(fn.indexOf('servicesFsReport(') > 0, fn);
|
||||
// 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;/);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user