diff --git a/docs/decoded-editing.md b/docs/decoded-editing.md index 68ff209..63b43f5 100644 --- a/docs/decoded-editing.md +++ b/docs/decoded-editing.md @@ -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 diff --git a/docs/services-editor.md b/docs/services-editor.md index 6d5dcaa..118ca09 100644 --- a/docs/services-editor.md +++ b/docs/services-editor.md @@ -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, diff --git a/frontend/index.html b/frontend/index.html index a26b0ac..ff05eec 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -1176,6 +1176,13 @@
+
+
+ + +
+
+
@@ -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 = '
' + + 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.')) + '
'; + const head = (text, n) => '
' + esc(text) + ' (' + n + ')
'; + const item = text => '
  • ' + text + '
  • '; + // the tree's own name(s) for the FID, when the check loaded it + const fidLabel = fid => { + const list = names && names[fid]; + return '' + esc(fid) + '' + + ((list && list.length) ? ' (' + esc(list.join(' / ')) + ')' : ''); + }; + const fidList = fids => fids.map(fidLabel).join(', '); + if (!report || (!report.missing.length && !report.extra.length)) { + return '
    ' + + esc(t('The filesystem matches the declared services')) + '
    ' + note; + } + let html = ''; + if (report.missing.length) { + html += head(t('Declared services whose files are missing'), report.missing.length); + html += ''; + } + if (report.extra.length) { + html += head(t('Present files whose service is not declared'), report.extra.length); + html += ''; + } + 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 = '
    ' + + esc(t('No service table found on this card')) + '
    '; + 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 = '
    ' + + esc(t('Error') + ': ' + ((e && e.message) ? e.message : String(e))) + '
    '; + } finally { + setStatus(''); + _pysimServicesFsBusy = false; + } +} + // The form's per-row state: { : { 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:': 'Проверка:', diff --git a/frontend/tests/services.test.js b/frontend/tests/services.test.js index f9c9036..7bdac43 100644 --- a/frontend/tests/services.test.js +++ b/frontend/tests/services.test.js @@ -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;/); +});