diff --git a/frontend/help-ru.html b/frontend/help-ru.html index 97137bc..f82729b 100644 --- a/frontend/help-ru.html +++ b/frontend/help-ru.html @@ -468,14 +468,14 @@

Правила выполняются последовательно. Редактор показывает символьное имя файла pySim (если известно) рядом с путём правила; Добавить правило добавляет правило, Сохранить сохраняет изменения. Правило файловой системы задаётся:

Проверить карту выполняет каждое правило на подключённой карте и показывает строку прогресса и отчёт прохождения. Рядом с путём файла указывается, что именно проверялось (например, тип файла и размер, содержимое или полный FCI); если часть проверок прошла, а часть нет — каждый аспект помечается (тип файла ✓, размер ✗, содержимое ✓), а расхождения расписываются ниже. Несовпавшие сырые данные (FCI, содержимое, данные записей) показываются как поля только для чтения с моноширинным шрифтом — ожидаемое над фактическим, в одной и той же колонке — для удобного сравнения; расхождения содержимого дополнительно показывают для файлов с декодером декодированное сравнение по полям (только отличающиеся поля, с заголовками колонок «ожидаемое/фактическое» над значениями; если декодированные значения совпадают, вместо таблицы выводится пометка об этом, а сторона, чья запись целиком из FF, показывает в своих ячейках Пусто (только FF)), а для расхождений FCI дополнительно показывается декодированное сравнение по параметрам (размер файла, дескриптор/структура, жизненный цикл, FID, SFI, проприетарные параметры…). Расхождения правил доступа выводят декодированные строки правил (✓ совпадает, − отсутствует, + лишнее). Декодированный просмотр FCI также отображается рядом с полем FCI hex при редактировании правила. Если данные FCI повреждены, показывается всё, что удалось декодировать до места ошибки, вместе с явным сообщением об ошибке декодирования. Для record-файлов при расхождении содержимого добавляется пометка совпадающие записи: 1-5, 7-10 со списком записей, которые совпали. В отчёте поля расхождений и колонки сравнения FCI подписаны ожидалось (имя профиля) и фактически (ICCID карты), а в заголовке отчёта выводится Результаты проверки профиля: <профиль> → <ICCID карты>. Опция «Только расхождения» в заголовке отчёта скрывает все совпавшие файлы и оставляет только несовпадения и ошибки.

Опции сканирования «Профиль с карты»

-

Диалог сканирования запрашивает имя профиля и предлагает селектор «Проверка FCP/FCI» (те же три режима, по умолчанию Тип файла + размер (FCP)), применяемый ко всем создаваемым правилам, а также список «Игнорировать содержимое файлов» (все отмечены по умолчанию, кроме EF.ARR; флажок в заголовке отмечает или снимает весь список) часто перезаписываемых файлов, содержимое которых пропускается: EF.LOCI, EF.PSLOCI, EF.EPSLOCI, EF.5GS3GPPLOCI, EF.Keys, EF.KeysPS, EF.SMS, EF.Kc, EF.KcGPRS, EF.LOCIGPRS, EF.CBMID, EF.SMSS, EF.ACC, EF.EPSNSC, EF.START-HFN, EF.ARR. Ещё две отмеченные по умолчанию опции «Сравнивать первые 4 байта для» EF.IMSI и EF.ICCID захватывают содержимое этих файлов как маску только первых 4 байт (снимите для точного сравнения). Строка прогресса показывает N / всего файлов с текущим путём файла во время сканирования; при сканировании опции скрываются, а кнопки блокируются. Правила создаются только для файлов, которые реально существуют на карте (возвращён FCP-шаблон); отсутствующие файлы пропускаются. Пользовательские файлы из подвкладки «Пользовательские файлы» включаются с той же проверкой существования. Тот же диалог и опции использует «Профиль из снимка»: вместо карты обходятся захваченные файлы выбранного снимка; для файлов, содержимое которых не было захвачено, правило создаётся без проверки содержимого (при последующей проверке профиля они помечаются как непроверяемые).

+

Диалог сканирования запрашивает имя профиля и предлагает селектор «Проверка FCP/FCI» (те же четыре режима, по умолчанию Тип файла + размер (FCP); выбор По объектам данных заполняет каждое правило объектами записанного FCI — их можно ослабить в редакторе), применяемый ко всем создаваемым правилам, а также список «Игнорировать содержимое файлов» (все отмечены по умолчанию, кроме EF.ARR; флажок в заголовке отмечает или снимает весь список) часто перезаписываемых файлов, содержимое которых пропускается: EF.LOCI, EF.PSLOCI, EF.EPSLOCI, EF.5GS3GPPLOCI, EF.Keys, EF.KeysPS, EF.SMS, EF.Kc, EF.KcGPRS, EF.LOCIGPRS, EF.CBMID, EF.SMSS, EF.ACC, EF.EPSNSC, EF.START-HFN, EF.ARR. Ещё две отмеченные по умолчанию опции «Сравнивать первые 4 байта для» EF.IMSI и EF.ICCID захватывают содержимое этих файлов как маску только первых 4 байт (снимите для точного сравнения). Строка прогресса показывает N / всего файлов с текущим путём файла во время сканирования; при сканировании опции скрываются, а кнопки блокируются. Правила создаются только для файлов, которые реально существуют на карте (возвращён FCP-шаблон); отсутствующие файлы пропускаются. Пользовательские файлы из подвкладки «Пользовательские файлы» включаются с той же проверкой существования. Тот же диалог и опции использует «Профиль из снимка»: вместо карты обходятся захваченные файлы выбранного снимка; для файлов, содержимое которых не было захвачено, правило создаётся без проверки содержимого (при последующей проверке профиля они помечаются как непроверяемые).

Снимки карт

Представление списка имеет три вкладки — «Профили», «Снимки карт» и «Пользовательские файлы» (см. ниже). Снимок карты — неизменяемая фиксация файловой системы карты: для каждого существующего файла сохраняются путь, символьное имя, тип, размер (или длина/число записей), сырой FCI из ответа SELECT и содержимое, если файл читается (без списка игнорирования и без масок). ICCID декодируется из EF.ICCID и показывается рядом с именем снимка. Захваченное содержимое показывается вместе с декодированным представлением, где есть декодер — таблица полей для transparent-файлов и однострочная сводка по каждой записи для record-файлов. При сканировании измеряется время каждой команды карты (SELECT, READ BINARY, READ RECORD) от отправки до ответа; снимок хранит min/сред/max по каждому типу команд и общее время сканирования, а в представлении эти значения показываются в сводке под заголовком, время select/read — для каждого файла и время чтения — для каждой записи. Время носит информационный характер и не используется при проверках и сравнении.

diff --git a/frontend/help.html b/frontend/help.html index 23e0fc1..8c5dc5d 100644 --- a/frontend/help.html +++ b/frontend/help.html @@ -467,14 +467,14 @@

Rules run sequentially. The editor shows the symbolic pySim name (when known) next to each rule’s path; use Add rule to append one and Save to keep the changes. A filesystem rule is defined by:

Check card runs every rule against the equipped card and shows a live progress line plus a pass/fail report. Each row states exactly what was verified next to the file path (e.g. filetype and size, contents or exact FCI); when some checks pass and others fail, each aspect is marked (filetype ✓, size ✗, contents ✓) with the mismatches detailed below. Mismatched raw data (FCI, contents, record data) is shown as read-only monospace fields — expected above actual, aligned in the same column — for easy comparison; content mismatches additionally show, for files with a decoder, a decoded per-field comparison (only the differing fields, with expected/actual column headers above the side-by-side values; when the decoded values match, a note says so instead of the table, and a side whose record is all-FF shows Empty (all FF) in its cells), and FCI mismatches additionally show a decoded per-parameter comparison (file size, file descriptor/structure, life cycle, FID, SFI, proprietary parameters…). Access-rule mismatches list the decoded rule lines (✓ matching, − missing, + extra). A decoded FCI preview is also shown beside the FCI hex field while editing a rule. If the FCI data is corrupt, whatever was decoded before the faulty part is shown together with an explicit decode-failure note. For record files with a contents mismatch, a matching records: 1-5, 7-10 note lists the records that did match. In the report the mismatch fields and FCI comparison columns are labelled expected (profile name) and actual (card ICCID), and the results header reads Profile verification results for: <profile> → <card ICCID>. An Only mismatches option in the results header hides all passing files and keeps only failures and errors.

“Profile from card” scan options

-

The scan dialog asks for a profile name and offers a “FCP/FCI check” selector (the same three modes above, default Filetype + size) applied to every generated rule, plus an “Ignore contents of files” checklist (all checked by default except EF.ARR; the header checkbox checks or unchecks the whole list) of frequently-overwritten files whose contents are skipped: EF.LOCI, EF.PSLOCI, EF.EPSLOCI, EF.5GS3GPPLOCI, EF.Keys, EF.KeysPS, EF.SMS, EF.Kc, EF.KcGPRS, EF.LOCIGPRS, EF.CBMID, EF.SMSS, EF.ACC, EF.EPSNSC, EF.START-HFN, EF.ARR. Two further checked-by-default options “Match first 4 bytes for” EF.IMSI and EF.ICCID capture those files’ contents as a mask of only the first 4 bytes (uncheck for exact matching). A progress line shows N / total files with the current file path while scanning; during the scan the options are hidden and the buttons are locked. Rules are created only for files that actually exist on the card (a FCP template is returned); missing files are skipped. Custom files from the Custom files sub-tab are included under the same existence check. The same dialog and options are used by Profile from snapshot, which walks the selected snapshot’s captured files instead of the card; rules for files whose contents were not captured during the scan get no content check (they are reported as unverifiable when the profile is later checked).

+

The scan dialog asks for a profile name and offers a “FCP/FCI check” selector (the same four modes above, default Filetype + size; choosing Custom (per data object) seeds every rule with the file’s captured data objects, ready to relax in the editor) applied to every generated rule, plus an “Ignore contents of files” checklist (all checked by default except EF.ARR; the header checkbox checks or unchecks the whole list) of frequently-overwritten files whose contents are skipped: EF.LOCI, EF.PSLOCI, EF.EPSLOCI, EF.5GS3GPPLOCI, EF.Keys, EF.KeysPS, EF.SMS, EF.Kc, EF.KcGPRS, EF.LOCIGPRS, EF.CBMID, EF.SMSS, EF.ACC, EF.EPSNSC, EF.START-HFN, EF.ARR. Two further checked-by-default options “Match first 4 bytes for” EF.IMSI and EF.ICCID capture those files’ contents as a mask of only the first 4 bytes (uncheck for exact matching). A progress line shows N / total files with the current file path while scanning; during the scan the options are hidden and the buttons are locked. Rules are created only for files that actually exist on the card (a FCP template is returned); missing files are skipped. Custom files from the Custom files sub-tab are included under the same existence check. The same dialog and options are used by Profile from snapshot, which walks the selected snapshot’s captured files instead of the card; rules for files whose contents were not captured during the scan get no content check (they are reported as unverifiable when the profile is later checked).

Card snapshots

The list view has three tabs — Profiles, Card snapshots and Custom files (see below). A card snapshot is an immutable capture of the card filesystem: for every existing file it stores the path, symbolic name, file type, size (or record length/count), the raw FCI from the SELECT response, and the contents whenever the file is readable (no ignore list, no masking). The ICCID is decoded from EF.ICCID and shown next to the snapshot name. Captured contents are shown with their decoded form where a decoder exists — a field table for transparent files and a one-line summary per record for record files. The scan also measures each card command (SELECT, READ BINARY, READ RECORD) from command to response; the snapshot stores min/avg/max per command type and the total scan time, and the view shows these in the summary under the title plus the select/read times per file and the read time per record. Timings are informational only and are not used by checks or comparisons.

diff --git a/frontend/index.html b/frontend/index.html index df602c3..04f27cb 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -1833,6 +1833,7 @@ + @@ -1906,7 +1907,7 @@ // ===== Version ===== // Single source of truth for the PWA version: shown in the header and used // by the server version check in pysimConnect(). -const SIMPLE_VERSION = '3.25.7'; +const SIMPLE_VERSION = '3.25.8'; document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION; // ===== view history (the browser Back/Forward buttons) ===== @@ -18956,7 +18957,19 @@ function profilerValidateProfile(obj) { if (!r || typeof r !== 'object') return 'Invalid rule'; if (r.type !== 'file') return 'Unsupported rule type: ' + r.type; if (!r.path) return 'Rule missing path'; - if (r.fciMode && !['type', 'type_size', 'exact'].includes(r.fciMode)) return 'Invalid FCP/FCI mode: ' + r.fciMode; + if (r.fciMode && !['type', 'type_size', 'exact', 'custom'].includes(r.fciMode)) return 'Invalid FCP/FCI mode: ' + r.fciMode; + if (r.fciDos !== undefined && r.fciDos !== null) { + if (!Array.isArray(r.fciDos)) return 'Invalid FCI data objects: ' + r.path; + for (const d of r.fciDos) { + if (!d || typeof d !== 'object') return 'Invalid FCI data object: ' + r.path; + if (!/^[0-9A-F]{2}(?:[0-9A-F]{2})?(?:\/[0-9A-F]{2}(?:[0-9A-F]{2})?)?$/.test(String(d.key || '').toUpperCase())) + return 'Invalid FCI data object tag: ' + r.path; + if (!['value', 'present', 'absent'].includes(d.mode)) return 'Invalid FCI data object mode: ' + r.path; + if (d.mode === 'value' && !/^[0-9A-F?]{2,}$/.test(String(d.value || '').toUpperCase())) + return 'Invalid FCI data object value: ' + r.path; + } + } + if (r.fciExtra !== undefined && !['ignore', 'fail'].includes(r.fciExtra)) return 'Invalid FCI extra data object policy: ' + r.path; if (r.accessRules !== undefined && r.accessRules !== null) { const ar = r.accessRules; if (typeof ar !== 'object' || !Array.isArray(ar.expected) || @@ -19325,6 +19338,12 @@ function profilerBuildFileRuleFromSnapshot(f, ignoreFids, ignoreNames, fciMode, fciHex: f.fciHex || null, content: null, }; + // Custom FCI (v3.25.8): seed the per-data-object expectations from the + // captured FCI - value-exact at first, relaxed in the editor. + if (rule.fciMode === 'custom') { + const dos = fcpDosFromFci(rule.fciHex); + if (dos.length) rule.fciDos = dos; + } // Access rules from the snapshot's own EF.ARR copies (v3.25.0) const ref = fcpArrRefFromFci(rule.fciHex); if (ref) { @@ -19385,6 +19404,12 @@ async function profilerBuildFileRule(path, c, ignoreFids, ignoreNames, fciMode, fciHex: sel.fci_hex || null, content: null, }; + // Custom FCI (v3.25.8): seed the per-data-object expectations from the + // captured FCI - value-exact at first, relaxed in the editor. + if (rule.fciMode === 'custom') { + const dos = fcpDosFromFci(rule.fciHex); + if (dos.length) rule.fciDos = dos; + } // Access rules (v3.25.0): resolve the FCP's '8B' reference against the // card's cached EF.ARR copies. Captured before the ignore list - the // rules are the meaningful per-file check (contents may be ignored). @@ -19799,6 +19824,7 @@ function profilerRenderRule(i, r) { '' + '' + '' + + '' + ''; html += '
' + '
'; @@ -19813,7 +19839,7 @@ function profilerRenderRule(i, r) { ''; } html += ''; - if (fciMode === 'exact') { + if (fciMode === 'exact' || fciMode === 'custom') { html += '
'; html += '
' + '
'; @@ -19821,6 +19847,9 @@ function profilerRenderRule(i, r) { '
' + profilerFciPreviewItems(r.fciHex || '') + '
'; html += ''; } + if (fciMode === 'custom') { + html += profilerRenderFciDosEditor(i, r); + } if (r.fileType !== 'df') { html += profilerRenderContentEditor(i, r); } @@ -19829,6 +19858,114 @@ function profilerRenderRule(i, r) { return html; } +// Custom-FCI editor (v3.25.8): a per-data-object expectation list. Profiles +// created from a card or a snapshot with fciMode 'custom' are seeded with the +// captured FCI's objects (all 'value', the captured bytes); a rule with a +// captured FCI but an empty list can be filled from it here. Only the listed +// objects are checked; `present`/`absent` demand or forbid one, and a value +// without '?' matches as a prefix ('?' masks a nibble). +function profilerRenderFciDosEditor(i, r) { + const dos = r.fciDos || []; + let html = '
'; + html += '
'; + html += '' + esc(t('Data objects (FCI)')) + (dos.length ? ' (' + dos.length + ')' : '') + ''; + if (!dos.length && r.fciHex) { + html += ''; + } + html += ''; + html += '
'; + html += '' + + FCP_DO_KEYS.map(k => '').join('') + ''; + for (let di = 0; di < dos.length; di++) html += profilerFciDoRowHtml(i, di, dos[di]); + if (!dos.length) { + html += '
' + esc(t('No data objects listed - the FCI is not checked.')) + '
'; + } + html += ''; + html += '
'; + return html; +} + +// One expectation row: the tag (with the known keys suggested), the mode, and +// the value for the 'value' mode. +function profilerFciDoRowHtml(i, di, d) { + const mode = (d.mode === 'present' || d.mode === 'absent') ? d.mode : 'value'; + let html = '
'; + html += ''; + html += ''; + if (mode === 'value') { + html += ''; + } + html += '' + esc(fcpDoName(d.key)) + ''; + html += ''; + html += '
'; + return html; +} + +function profilerFciDosAdd(i) { + if (!profilerDraft) return; + const r = profilerDraft.rules[i]; + if (!r.fciDos) r.fciDos = []; + r.fciDos.push({ key: '82', mode: 'value', value: '' }); + profilerRenderEditor(); +} + +function profilerFciDoRemove(i, di) { + if (!profilerDraft) return; + const r = profilerDraft.rules[i]; + if (!r.fciDos) return; + r.fciDos.splice(di, 1); + if (!r.fciDos.length) delete r.fciDos; + profilerRenderEditor(); +} + +function profilerFciDoSetKey(i, di, v) { + if (!profilerDraft) return; + const r = profilerDraft.rules[i]; + if (!r.fciDos || !r.fciDos[di]) return; + r.fciDos[di].key = String(v || '').trim().toUpperCase(); + const el = document.getElementById('profiler-fci-do-name-' + i + '-' + di); + if (el) el.textContent = fcpDoName(r.fciDos[di].key); +} + +function profilerFciDoSetMode(i, di, v) { + if (!profilerDraft) return; + const r = profilerDraft.rules[i]; + if (!r.fciDos || !r.fciDos[di]) return; + r.fciDos[di].mode = (v === 'present' || v === 'absent') ? v : 'value'; + profilerRenderEditor(); +} + +function profilerFciDoSetValue(i, di, v) { + if (!profilerDraft) return; + const r = profilerDraft.rules[i]; + if (!r.fciDos || !r.fciDos[di]) return; + r.fciDos[di].value = String(v || '').trim().toUpperCase(); +} + +function profilerSetFciExtra(i, v) { + if (!profilerDraft) return; + const r = profilerDraft.rules[i]; + if (v === 'fail') r.fciExtra = 'fail'; + else delete r.fciExtra; +} + +// Fill an empty list from the rule's own captured FCI (never the card - the +// rows come from what the profile already stores; v3.25.8). +function profilerFciDosLoad(i) { + if (!profilerDraft) return; + const r = profilerDraft.rules[i]; + const dos = fcpDosFromFci(r.fciHex); + if (!dos.length) return; + r.fciDos = dos; + profilerRenderEditor(); +} + // The expanded access-rule blocks of the profile editor (transient UI state, // kept across the editor re-renders; None hides the block entirely; a fresh // draft resets it and a deleted rule shifts the keys). @@ -20457,6 +20594,32 @@ async function profilerRunRule(r, source) { if (!ok) res.status = 'fail'; res.checks.push({ label: 'fci', expected: r.fciHex, actual: sel.fci_hex || '', ok: ok, ignoredRef: ignoredRef }); } + // Custom FCI (v3.25.8): a per-data-object expectation list - only the + // listed objects are checked (not listed = not checked); `value` reuses + // the contents vocabulary (no '?' = prefix, '?' = nibble mask), `present` + // demands the object, `absent` forbids it, and `fciExtra: 'fail'` also + // fails on any object the card carries although it is not listed + // (order-insensitive strictness). + if (fciMode === 'custom' && (r.fciDos || []).length) { + const dec = fcpDecode(sel.fci_hex || ''); + const dos = []; + for (const d of r.fciDos) { + const hits = dec.items.filter(it => it.key === d.key); + let ok; + if (d.mode === 'absent') ok = hits.length === 0; + else if (d.mode === 'present') ok = hits.length > 0; + else ok = hits.some(it => profilerMatch('mask', d.value || '', it.value)); + dos.push({ key: d.key, name: fcpDoName(d.key), mode: d.mode, value: d.value || '', + ok: ok, actual: hits.map(it => it.value) }); + } + const listed = {}; + r.fciDos.forEach(d => { listed[d.key] = true; }); + const extra = r.fciExtra === 'fail' ? dec.items.filter(it => !listed[it.key]) : []; + const ok = dos.every(d => d.ok) && !extra.length && !dec.error; + if (!ok) res.status = 'fail'; + res.checks.push({ label: 'fci', mode: 'custom', dos: dos, extra: extra, + parseError: dec.error || '', expected: r.fciDos, actual: dec.items, ok: ok }); + } // Access rules (v3.25.0): the decoded conditions of the EF.ARR record the // FCI's '8B' reference points at - record-number independent, so cards // with a different EF.ARR layout still verify. @@ -20556,13 +20719,13 @@ function profilerCustomNameForPath(path) { // that actually ran. 'Exact FCI' subsumes filetype/size/records; 'exists' is // implicit and omitted. function profilerResultAspects(res) { - const groups = { filetype: [], size: [], records: [], 'Exact FCI': [], contents: [], accessRules: [] }; + const groups = { filetype: [], size: [], records: [], 'Exact FCI': [], 'FCI (custom)': [], contents: [], accessRules: [] }; for (const c of (res.checks || [])) { const l = c.label; if (l === 'fileType') groups.filetype.push(c); else if (l === 'fileSize') groups.size.push(c); else if (l === 'recordLen' || l === 'numRecords') groups.records.push(c); - else if (l === 'fci') groups['Exact FCI'].push(c); + else if (l === 'fci') groups[c.mode === 'custom' ? 'FCI (custom)' : 'Exact FCI'].push(c); else if (l === 'content' || l.startsWith('content.')) groups.contents.push(c); else if (l === 'accessRules') groups.accessRules.push(c); } @@ -20574,6 +20737,7 @@ function profilerResultAspects(res) { if (groups.size.length) out.push({ key: 'size', ok: groups.size.every(c => c.ok) }); if (groups.records.length) out.push({ key: 'records', ok: groups.records.every(c => c.ok) }); } + if (groups['FCI (custom)'].length) out.push({ key: 'FCI (custom)', ok: groups['FCI (custom)'].every(c => c.ok) }); if (groups.contents.length) out.push({ key: 'contents', ok: groups.contents.every(c => c.ok) }); if (groups.accessRules.length) out.push({ key: 'Access rules', ok: groups.accessRules.every(c => c.ok) }); return out; @@ -21178,6 +21342,32 @@ function fcpDecode(hex) { return { ok: !error && items.length > 0, template, items, error }; } +// The data object keys the FCI decoder names (TS 102 221 11.1.1.4 + the +// A5/C6 children) - the custom-FCI editor's tag suggestions (v3.25.8). +const FCP_DO_KEYS = [ + '80', '81', '82', '83', '84', '85', '86', '87', '88', '8A', '8B', '8C', + '8D', '8E', 'A0', 'A5', 'AB', 'C6', + 'A5/80', 'A5/81', 'A5/82', 'A5/83', 'A5/84', 'A5/85', 'A5/86', 'A5/87', + 'A5/88', 'A5/89', 'A5/8A', 'A5/8B', 'A5/C0', 'A5/C1', 'A5/C2', + 'C6/90', 'C6/95', 'C6/83', +]; + +// A data object's display name ('' for an empty or unknown key). +function fcpDoName(key) { + const k = String(key || '').toUpperCase(); + if (!k) return ''; + const n = fcpDo(k, '').name || ''; + return /^Unknown TLV/.test(n) ? '' : n; +} + +// The decoded data objects of an FCI as custom-check rows: one 'value' +// expectation per object, the captured bytes - profiles created from a card +// or a snapshot with fciMode 'custom' are seeded with these (v3.25.8). +function fcpDosFromFci(hex) { + const dec = fcpDecode(hex || ''); + return dec.items.map(it => ({ key: it.key, mode: 'value', value: it.value })); +} + function profilerLabelText(labels, which) { if (!labels || !labels[which]) return t(which); return labels.prefix ? t(which) + ' (' + labels[which] + ')' : labels[which]; @@ -21320,6 +21510,38 @@ function profilerAccessRulesDiffHtml(c) { return html + ''; } +// The custom FCI's expectation rows in the report (v3.25.8): the same marks +// as the access-rules diff - '−' a failed expectation, '+' an object the card +// carries although fciExtra forbids it. +function profilerFciDosDiffHtml(c) { + let html = '
'; + html += '
' + esc(t('FCI (custom)')) + '
'; + if (c.parseError) { + html += '
' + + esc(t('the FCI could not be parsed') + ': ' + c.parseError) + '
'; + } + const line = (mark, cls, text) => '
' + esc(mark + ' ' + text) + '
'; + for (const d of (c.dos || [])) { + const desc = d.key + (d.name ? ' (' + d.name + ')' : ''); + const mark = d.ok ? '\u2713' : '\u2212'; + const cls = d.ok ? 'text-gray-500 dark:text-slate-400' : 'text-red-600'; + if (d.mode === 'absent') { + html += line(mark, cls, desc + ': ' + t('must be absent') + + (d.ok ? '' : ' \u2014 ' + t('the card has') + ': ' + d.actual.join(', '))); + } else if (d.mode === 'present') { + html += line(mark, cls, desc + ': ' + t('must be present') + + (d.ok ? '' : ' \u2014 ' + t('not found'))); + } else { + html += line(mark, cls, desc + ' = ' + d.value + + (d.ok ? '' : ' \u2014 ' + t('the card has') + ': ' + (d.actual.join(', ') || t('not found')))); + } + } + for (const it of (c.extra || [])) { + html += line('+', 'text-red-600', it.key + (it.name ? ' (' + it.name + ')' : '') + ' = ' + it.value); + } + return html + '
'; +} + function profilerRenderReport(results, labels) { const expLabel = profilerLabelText(labels, 'expected'); const actLabel = profilerLabelText(labels, 'actual'); @@ -21345,6 +21567,8 @@ function profilerRenderReport(results, labels) { html += '
' + esc(t('extra file')) + '
'; } else if (c.label === 'accessRules') { html += profilerAccessRulesDiffHtml(c); + } else if (c.label === 'fci' && c.mode === 'custom') { + html += profilerFciDosDiffHtml(c); } else if (profilerRawDataCheck(c)) { const isContent = c.label === 'content' || /^content\.rec\d+$/.test(c.label); const dd = isContent && typeof efContentDiff === 'function' @@ -23503,6 +23727,22 @@ const LANG_RU = { 'Filetype only (FCP)': 'Только тип файла (FCP)', 'Filetype + size (FCP)': 'Тип файла + размер (FCP)', 'Exact FCI': 'Полный FCI', + 'Custom (per data object)': 'По объектам данных', + 'Data objects (FCI)': 'Объекты данных (FCI)', + 'data object': 'объект данных', + 'Extra data objects': 'Лишние объекты данных', + 'ignore': 'игнорировать', + 'fail': 'не допускать', + 'load from the captured FCI': 'заполнить из записанного FCI', + 'No data objects listed - the FCI is not checked.': 'Объекты данных не перечислены — FCI не проверяется.', + 'value': 'значение', + 'hex; no ? = prefix, ? = any nibble': 'hex; без ? — префикс, ? — любой полубайт', + 'FCI (custom)': 'FCI (по объектам)', + 'the FCI could not be parsed': 'FCI не удалось разобрать', + 'must be present': 'должен присутствовать', + 'must be absent': 'должен отсутствовать', + 'the card has': 'на карте', + 'not found': 'не найдено', 'FCI hex (raw SELECT response)': 'FCI hex (сырой ответ SELECT)', 'FCI parameters': 'Параметры FCI', 'Parameter': 'Параметр', diff --git a/frontend/sw.js b/frontend/sw.js index b7f6815..674e31a 100644 --- a/frontend/sw.js +++ b/frontend/sw.js @@ -1,4 +1,4 @@ -const CACHE = 'simple-v431'; +const CACHE = 'simple-v432'; const URLS = [ 'index.html', 'help.html', diff --git a/frontend/tests/profiler.test.js b/frontend/tests/profiler.test.js index 9887d0b..25a6ce1 100644 --- a/frontend/tests/profiler.test.js +++ b/frontend/tests/profiler.test.js @@ -22,7 +22,7 @@ function extractFunc(src, name, asyncFn) { } const FNS = ['profilerNormHex', 'profilerNormHexStrict', 'profilerMatch', 'profilerMatchMin', 'profilerMaskPrefix4', 'profilerFileFields', 'profilerContentKindForFileType', 'profilerEmptyRecordContent', 'profilerValidateProfile', 'profilerCustomNameForPath', 'pysimCustomNormPath', 'profilerUpdateRulePath', 'profilerResultAspects', 'profilerAspectSummary', 'profilerNumRanges', 'profilerMatchedRecordsText', 'profilerCloneName', 'profilerClone', 'profilerNewId', 'esc', 'escHtml', 'profilerRawDataCheck', 'profilerRenderReport', 'parseBerLen', 'parseTlvList', 'fcpInt', 'fcpParseTlvs', 'fcpFileDescriptor', 'fcpLifeCycle', 'fcpSfi', 'fcpDo', 'fcpDecode', - 'fcpArrRef', 'fcpSeidName', 'fcpArrRules', 'fcpArrRefFromFci', 'fcpWithoutArrRef', 'fcpArrSplitTop', 'fcpArrKeyRefFromName', 'fcpArrKeyRefOptions', 'fcpArrScParse', 'fcpArrScEncode', 'fcpArrRuleParse', 'fcpArrRuleEncode', 'fcpArrRuleOps', 'fcpArrRuleOpCovered', 'fcpArrRuleSatisfied', 'fcpArrRuleToggleBit', 'fcpArrRuleSetKind', 'fcpArrRuleSetSc', 'fcpArrRuleSetUq', 'fcpArrRuleAddSc', 'fcpArrRuleRemoveSc', 'profilerRenderAccessRulesEditor', 'profilerArrRuleHtml', 'profilerArrScSelect', 'profilerArrUqSelect', 'profilerArrToggleOpen', 'profilerArrShiftOpen', 'profilerAccessRulesDiffHtml', 'profilerSnapshotArr', 'efBytes', 'efHex', 'efTlv', 'efDecArr', 'efArrAmLabel', 'efArrSc', 'efArrAmDo', 'efFidFromPath', + 'fcpArrRef', 'fcpSeidName', 'fcpArrRules', 'fcpArrRefFromFci', 'fcpWithoutArrRef', 'fcpArrSplitTop', 'fcpArrKeyRefFromName', 'fcpArrKeyRefOptions', 'fcpArrScParse', 'fcpArrScEncode', 'fcpArrRuleParse', 'fcpArrRuleEncode', 'fcpArrRuleOps', 'fcpArrRuleOpCovered', 'fcpArrRuleSatisfied', 'fcpArrRuleToggleBit', 'fcpArrRuleSetKind', 'fcpArrRuleSetSc', 'fcpArrRuleSetUq', 'fcpArrRuleAddSc', 'fcpArrRuleRemoveSc', 'profilerRenderAccessRulesEditor', 'profilerArrRuleHtml', 'profilerArrScSelect', 'profilerArrUqSelect', 'profilerArrToggleOpen', 'profilerArrShiftOpen', 'profilerAccessRulesDiffHtml', 'fcpDosFromFci', 'fcpDoName', 'profilerRenderFciDosEditor', 'profilerFciDoRowHtml', 'profilerFciDosDiffHtml', 'profilerSnapshotArr', 'efBytes', 'efHex', 'efTlv', 'efDecArr', 'efArrAmLabel', 'efArrSc', 'efArrAmDo', 'efFidFromPath', 'pinKeyRefName', 'fcpPsTemplate', 'fcpDiffHtml', 'profilerFciPreviewItems', 'profilerUpdateFciPreview', 'profilerUpdateRule', 'profilerFciInput', 'profilerScanToggleAll', 'profilerScanIgnoreAllState', 'swapNibbles', 'decIccid', 'profilerSnapshotIccid', 'profilerValidateSnapshot', 'profilerListSwitch', 'profilerScanRefreshOptions', 'profilerLiveSource', 'profilerSnapshotSource', 'profilerVisibleResults', 'profilerRulesFromSnapshot', 'profilerExtraFileResults', 'profilerScanNameKeydown', 'profilerTimingStats', 'profilerTimingAccumulator', 'profilerFormatMs', 'profilerRenderSnapshotSummary', 'profilerSnapshotCountLabel', 'pysimFsInfoHtml', 'profilerLabelText', 'profilerResultsHeaderText', 'profilerRenderResultsView', 'profilerBuildFileRuleFromSnapshot', 'profilerSnapshotPickListHtml', 'profilerScanSetTarget']; let code = ''; for (const f of FNS) code += extractFunc(html, f) + '\n'; @@ -42,6 +42,7 @@ code += "var _profilerArrOpen = {};\n"; code += 'globalThis.navRecord = function() {};\nglobalThis.navReplace = function() {};\n'; code += html.match(/const PROFILER_MASK_PREFIX4_FIDS = \{[\s\S]*?\n\};/)[0] + '\n'; code += html.match(/const FCP_ARR_AM_BITS = \[[\s\S]*?\n\];/)[0] + '\n'; +code += html.match(/const FCP_DO_KEYS = \[[\s\S]*?\n\];/)[0] + '\n'; eval(code); test('profilerNormHex uppercases and strips non-hex', () => { @@ -1309,6 +1310,168 @@ test('the access-rules report block marks missing and extra lines', () => { delete global.t; }); +test('the custom FCI check matches per data object', async () => { + const FCI = '621A82054221000F0583026F4F8A01058B032F06028802004B8801B0'; + const src = { + select: async () => ({ name: 'EF.SAMPLE', file_type: 'linear_fixed', file_size: null, + record_len: 6, num_of_rec: 2, fci_hex: FCI, exists: true }), + read: async () => ({ success: false }), + }; + const rule = dos => ({ type: 'file', path: 'MF/7F20/6F07', name: 'EF.SAMPLE', + fileType: 'linear_fixed', recordLen: 6, numRecords: 2, fciMode: 'custom', + fciHex: FCI, fciDos: dos }); + const run = dos => profilerRunRule(rule(dos), src); + // exact value, the prefix form (the record number ignored) and the mask + let r = await run([{ key: '82', mode: 'value', value: '4221000F05' }, + { key: '8B', mode: 'value', value: '2F0602' }]); + assert.strictEqual(r.status, 'pass', JSON.stringify(r.checks)); + const c0 = r.checks.find(c => c.label === 'fci'); + assert.strictEqual(c0.mode, 'custom'); + assert.deepStrictEqual(c0.dos.map(d => d.ok), [true, true]); + // the type/size/record checks still run in the custom mode + assert.ok(r.checks.some(c => c.label === 'recordLen' && c.ok), JSON.stringify(r.checks)); + r = await run([{ key: '8B', mode: 'value', value: '2F06' }]); + assert.strictEqual(r.status, 'pass', JSON.stringify(r.checks)); + r = await run([{ key: '8B', mode: 'value', value: '2F06??' }]); + assert.strictEqual(r.status, 'pass', JSON.stringify(r.checks)); + r = await run([{ key: '8B', mode: 'value', value: '2F07' }]); + assert.strictEqual(r.status, 'fail'); + assert.deepStrictEqual(r.checks.find(c => c.label === 'fci').dos[0].actual, ['2F0602']); + // present / absent + r = await run([{ key: '83', mode: 'present' }, { key: 'A0', mode: 'absent' }]); + assert.strictEqual(r.status, 'pass', JSON.stringify(r.checks)); + r = await run([{ key: 'A0', mode: 'present' }]); + assert.strictEqual(r.status, 'fail'); + assert.deepStrictEqual(r.checks.find(c => c.label === 'fci').dos[0].actual, []); + r = await run([{ key: '83', mode: 'absent' }]); + assert.strictEqual(r.status, 'fail'); + // a duplicated tag: any occurrence satisfies, 'absent' needs none + r = await run([{ key: '88', mode: 'value', value: '004B' }]); + assert.strictEqual(r.status, 'pass', JSON.stringify(r.checks)); + r = await run([{ key: '88', mode: 'value', value: 'FFFF' }]); + assert.strictEqual(r.status, 'fail'); + r = await run([{ key: '88', mode: 'absent' }]); + assert.strictEqual(r.status, 'fail'); + // extras: ignored by default, failing with fciExtra + r = await run([{ key: '82', mode: 'present' }]); + assert.strictEqual(r.status, 'pass', JSON.stringify(r.checks)); + r = await profilerRunRule({ ...rule([{ key: '82', mode: 'present' }]), fciExtra: 'fail' }, src); + assert.strictEqual(r.status, 'fail'); + assert.ok(r.checks.find(c => c.label === 'fci').extra.some(it => it.key === '8B')); + // an empty list checks nothing + r = await run([]); + assert.ok(!r.checks.some(c => c.label === 'fci'), JSON.stringify(r.checks)); + // a nested A5 child is addressable + const FCI_A5 = '621382054221000F0583026F4F8A0105A503800101'; + const a5src = { ...src, select: async () => ({ name: 'EF.SAMPLE', file_type: 'linear_fixed', + record_len: 6, num_of_rec: 2, fci_hex: FCI_A5, exists: true }) }; + r = await profilerRunRule({ ...rule([{ key: 'A5/80', mode: 'value', value: '01' }]), fciHex: FCI_A5 }, a5src); + assert.strictEqual(r.status, 'pass', JSON.stringify(r.checks)); + // an unparseable FCI fails with the parse error + const bad = { ...src, select: async () => ({ name: 'EF.SAMPLE', file_type: 'linear_fixed', + record_len: 6, num_of_rec: 2, fci_hex: '62', exists: true }) }; + r = await profilerRunRule({ ...rule([{ key: '82', mode: 'present' }]), fciHex: '62' }, bad); + assert.strictEqual(r.status, 'fail'); + assert.ok(r.checks.find(c => c.label === 'fci').parseError, JSON.stringify(r.checks)); +}); + +test('a custom-mode profile is seeded with the captured FCI objects', () => { + const snap = { files: [ + { path: 'MF/2F06', name: 'EF.ARR', fileType: 'linear_fixed', recordLen: 6, numRecords: 2, + content: { kind: 'record', records: [{ num: 1, data: 'FFFF' }, + { num: 2, data: '800101A40683010A950108' }] } }, + { path: 'MF/7F20/6F07', name: 'EF.IMSI', + fciHex: '621A82054221000F0583026F4F8A01058B032F06028802004B8801B0' }, + ]}; + const arrs = profilerSnapshotArr(snap); + const rule = profilerBuildFileRuleFromSnapshot(snap.files[1], new Set(), null, 'custom', null, arrs); + assert.ok(Array.isArray(rule.fciDos) && rule.fciDos.length >= 5, JSON.stringify(rule.fciDos)); + assert.deepStrictEqual(rule.fciDos[0], { key: '82', mode: 'value', value: '4221000F05' }); + assert.ok(rule.fciDos.some(d => d.key === '8B' && d.value === '2F0602')); + // the other modes do not seed + const exact = profilerBuildFileRuleFromSnapshot(snap.files[1], new Set(), null, 'exact', null, arrs); + assert.strictEqual(exact.fciDos, undefined); +}); + +test('the custom-FCI editor renders the data object rows', () => { + global.t = s => s; + const r = { fciMode: 'custom', fciHex: '621A82054221000F0583026F4F8A01058B032F06028802004B8801B0', + fciDos: [{ key: '82', mode: 'value', value: '4221000F05' }, + { key: '88', mode: 'present' }, { key: 'A0', mode: 'absent' }] }; + const out = profilerRenderFciDosEditor(3, r); + assert.ok(out.includes('Data objects (FCI) (3)'), out); + assert.ok(out.includes('profilerFciDoSetKey(3,0,this.value)'), out); + assert.ok(out.includes(''), out); + assert.ok(out.includes(''), out); + assert.ok(out.includes('value="4221000F05"'), out); + assert.ok(out.includes('profilerSetFciExtra(3,this.value)'), out); + assert.ok(out.includes(''), out); + assert.ok(out.includes('profilerFciDosAdd(3)'), out); + assert.ok(out.includes('profilerFciDoRemove(3,2)'), out); + // no value input on the present/absent rows + assert.ok(!out.includes('profilerFciDoSetValue(3,1'), out); + // the known-key datalist carries the names + assert.ok(out.includes(''), out); + assert.ok(out.includes(''), out); + const fail = profilerRenderFciDosEditor(3, { ...r, fciExtra: 'fail' }); + assert.ok(fail.includes(''), fail); + // the load link only with a captured FCI and an empty list + const empty = profilerRenderFciDosEditor(3, { ...r, fciDos: [] }); + assert.ok(empty.includes('profilerFciDosLoad(3)'), empty); + assert.ok(empty.includes('No data objects listed'), empty); + const noHex = profilerRenderFciDosEditor(3, { ...r, fciDos: [], fciHex: null }); + assert.ok(!noHex.includes('profilerFciDosLoad'), noHex); + // the mode selector offers the custom mode (editor + scan dialog) + assert.ok(html.includes('value="custom"\' + (fciMode === \'custom\''), 'the editor option'); + assert.ok(html.includes('data-l10n="Custom (per data object)"'), 'the scan dialog option'); + delete global.t; +}); + +test('the custom FCI report block marks the expectations', () => { + global.t = s => s; + const out = profilerFciDosDiffHtml({ label: 'fci', mode: 'custom', ok: false, parseError: '', + dos: [ + { key: '82', name: 'File descriptor', mode: 'value', value: '4221000F05', ok: true, actual: ['4221000F05'] }, + { key: '88', name: 'Short file identifier', mode: 'present', ok: false, actual: [] }, + { key: 'A0', name: '', mode: 'absent', ok: false, actual: ['01'] }, + ], + extra: [{ key: '8B', name: 'Security attributes (referenced)', value: '2F0602' }] }); + assert.ok(out.includes('\u2713 82 (File descriptor) = 4221000F05'), out); + assert.ok(out.includes('\u2212 88 (Short file identifier): must be present \u2014 not found'), out); + assert.ok(out.includes('\u2212 A0: must be absent \u2014 the card has: 01'), out); + assert.ok(out.includes('+ 8B (Security attributes (referenced)) = 2F0602'), out); + const perr = profilerFciDosDiffHtml({ label: 'fci', mode: 'custom', ok: false, + parseError: 'Truncated at offset 1', dos: [], extra: [] }); + assert.ok(perr.includes('the FCI could not be parsed: Truncated at offset 1'), perr); + delete global.t; +}); + +test('the custom FCI aspects stay separate from the exact one', () => { + const aspects = profilerResultAspects({ checks: [ + { label: 'fci', mode: 'custom', ok: false, dos: [], extra: [] }, + { label: 'recordLen', ok: true }, + ]}); + assert.ok(aspects.some(a => a.key === 'FCI (custom)' && !a.ok), JSON.stringify(aspects)); + assert.ok(aspects.some(a => a.key === 'records'), JSON.stringify(aspects)); +}); + +test('profile validation accepts the custom FCI fields', () => { + const base = { name: 'P', rules: [{ type: 'file', path: 'MF/7F20/6F07', fciMode: 'custom', + fciDos: [{ key: '82', mode: 'value', value: '4221000F05' }, + { key: '88', mode: 'present' }, { key: 'A0', mode: 'absent' }, + { key: 'A5/80', mode: 'value', value: '01' }] }] }; + const withRule = f => ({ ...base, rules: [{ ...base.rules[0], ...f }] }); + assert.strictEqual(profilerValidateProfile(base), null); + assert.strictEqual(profilerValidateProfile(withRule({ fciExtra: 'fail' })), null); + assert.strictEqual(profilerValidateProfile(withRule({ fciExtra: 'ignore' })), null); + assert.ok(profilerValidateProfile(withRule({ fciMode: 'bogus' }))); + assert.ok(profilerValidateProfile(withRule({ fciDos: 'nope' }))); + assert.ok(profilerValidateProfile(withRule({ fciDos: [{ key: 'ZZ', mode: 'value', value: '01' }] }))); + assert.ok(profilerValidateProfile(withRule({ fciDos: [{ key: '82', mode: 'nope' }] }))); + assert.ok(profilerValidateProfile(withRule({ fciDos: [{ key: '82', mode: 'value', value: 'Z' }] }))); + assert.ok(profilerValidateProfile(withRule({ fciExtra: 'nope' }))); +}); + test('pysimFsInfoHtml embeds the symbolic name in the FCI border', () => { global.t = s => s; const hex = '6212800200098202412183026F078A0105880110'; diff --git a/pyproject.toml b/pyproject.toml index 6a6dfc5..1534345 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "pysim-simple-server" -version = "3.25.7" +version = "3.25.8" description = "HTTP REST server wrapping pysim for the SIMple PWA" requires-python = ">=3.8" # pysim is a git-only dependency installed explicitly by setup.bat/setup.sh. diff --git a/pysim_simple_server/server.py b/pysim_simple_server/server.py index 684ea38..19a582a 100644 --- a/pysim_simple_server/server.py +++ b/pysim_simple_server/server.py @@ -36,7 +36,7 @@ from osmocom.tlv import BER_TLV_IE from osmocom.utils import rpad -VERSION = '3.25.7' +VERSION = '3.25.8' MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE