From c69a779ca4f138bea37a4d2cc674254cee2a4d3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BD=D1=82=D0=BE=D0=BD=20=D0=A2=D1=80=D0=BE=D1=88?= =?UTF-8?q?=D0=B8=D0=BD?= Date: Sat, 10 Oct 2026 23:55:07 +0300 Subject: [PATCH] v3.25.0 review: the structured access-rule editor The access-rules block was read-only and its Check radio was disabled without captured lines, so a hand-made rule could not define rules and captured ones could not be adjusted. It now edits the decoded rule's structure: - fcpArrRuleParse/fcpArrRuleEncode parse a canonical decoded line into the AM (bit mask, RESIZE FILE, INCREASE) + SC condition model and re-encode it byte-identically (round-trip tested); the template forms (OR/AND/NOT, SM CRTs, command descriptions) stay raw and removable. - The editor renders each rule as operation checkboxes (or the command form) and condition selects (always / never / a key reference with its usage), with a preview line of the canonical string the check compares; + rule adds READ always; + condition / x adjust the rows. - The None/Check radio no longer disables; a new rule can be built by hand and captured rules adjusted. The stored format is unchanged (the canonical strings), so capture, the check, the set comparison and validation are untouched. Tests: parse/encode round-trips (incl. raw forms), the mutators, the editor render, plus the existing check tests. 882 frontend + 858 Python; sw cache simple-v424. --- frontend/help-ru.html | 2 +- frontend/help.html | 2 +- frontend/index.html | 344 ++++++++++++++++++++++++++++--- frontend/tests/ef_decode.test.js | 2 +- frontend/tests/profiler.test.js | 78 ++++++- 5 files changed, 399 insertions(+), 29 deletions(-) diff --git a/frontend/help-ru.html b/frontend/help-ru.html index d261f94..d840ca5 100644 --- a/frontend/help-ru.html +++ b/frontend/help-ru.html @@ -466,7 +466,7 @@ diff --git a/frontend/help.html b/frontend/help.html index 0c03b6b..fae53d9 100644 --- a/frontend/help.html +++ b/frontend/help.html @@ -465,7 +465,7 @@ diff --git a/frontend/index.html b/frontend/index.html index 11294fd..4d2326c 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -19807,33 +19807,166 @@ function profilerRenderRule(i, r) { return html; } -// Access-rules check editor (v3.25.0): the captured rule lines are read-only -// (capture fills them from the card/snapshot); the radio toggles the check. +// Access-rules check editor (v3.25.0): captured rules open as structured rows +// (operation bits / command + security conditions) and hand-made rules can be +// built the same way; every change re-encodes the canonical decoded line the +// check compares. Forms outside the supported subset stay raw (read-only, +// removable). Capture fills the lines when a profile is created from a card +// or a snapshot; the radio toggles the check. function profilerRenderAccessRulesEditor(i, r) { const ar = r.accessRules || null; const lines = (ar && ar.expected) || []; - const on = !!(ar && ar.enabled !== false && lines.length); + const on = !!(ar && ar.enabled !== false); let html = '
'; html += '' + esc(t('Check access rules')) + ''; html += '
'; html += ''; - html += ''; + html += ''; html += '
'; if (r.fciMode === 'exact') { html += ''; } - if (lines.length) { - html += '
'; - for (const x of lines) { - html += '
' + esc('\u21b3 ' + x) + '
'; - } - html += '
'; - } else { - html += '
' + esc(t('Captured when the profile is created from a card or a snapshot')) + '
'; + for (let ri = 0; ri < lines.length; ri++) { + html += profilerArrRuleHtml(i, ri, lines[ri]); } + if (!lines.length) { + html += '
' + esc(t('Captured when the profile is created from a card or a snapshot')) + '
'; + } + html += ''; return html; } +// One expected rule row: the operation form + bit checkboxes (or the RESIZE / +// INCREASE command), the condition rows and the canonical preview line. +function profilerArrRuleHtml(i, ri, line) { + const m = fcpArrRuleParse(line); + if (m.raw !== undefined) { + return '
' + + '' + esc(t('raw') + ': ' + m.raw) + '' + + '
'; + } + let html = '
'; + html += '
'; + html += ''; + if (m.kind === 'bits') { + for (const bit of FCP_ARR_AM_BITS.slice().reverse()) { + html += ''; + } + } + html += ''; + html += '
'; + for (let si = 0; si < m.scs.length; si++) { + html += '
'; + html += profilerArrScSelect(i, ri, si, m.scs[si]); + if (m.scs[si].kind === 'key') html += profilerArrUqSelect(i, ri, si, m.scs[si].uq); + html += ''; + html += '
'; + } + html += ''; + html += '
' + esc(fcpArrRuleEncode(m)) + '
'; + html += '
'; + return html; +} + +// The condition select: always / never / the named key references. +function profilerArrScSelect(i, ri, si, sc) { + let opts = ''; + opts += ''; + for (const k of fcpArrKeyRefOptions()) { + opts += ''; + } + return ''; +} + +// The usage-qualifier select (keys only): verify / verification not used, plus +// the current odd value when a captured rule carried one. +function profilerArrUqSelect(i, ri, si, uq) { + const verify = uq !== 0x00 && (uq & 0x08) === 0x08; + const custom = uq !== 0x00 && (uq & 0x08) === 0; + let opts = ''; + opts += ''; + if (custom) { + opts += ''; + } + return ''; +} + +// The stored line of one expected rule (undefined when the row is gone). +function profilerArrLine(i, ri) { + if (!profilerDraft) return undefined; + const r = profilerDraft.rules[i]; + return (((r && r.accessRules) || {}).expected || [])[ri]; +} + +// Parse the stored line, mutate the model, re-encode into the draft and +// re-render (the check compares the stored strings). +function profilerArrUpdate(i, ri, model) { + if (!profilerDraft) return; + const r = profilerDraft.rules[i]; + if (!r.accessRules) r.accessRules = { expected: [] }; + r.accessRules.expected[ri] = fcpArrRuleEncode(model); + profilerRenderEditor(); +} + +function profilerArrToggleBit(i, ri, bit, on) { + const line = profilerArrLine(i, ri); + if (line === undefined) return; + profilerArrUpdate(i, ri, fcpArrRuleToggleBit(fcpArrRuleParse(line), bit, on)); +} + +function profilerArrSetKind(i, ri, kind) { + const line = profilerArrLine(i, ri); + if (line === undefined) return; + profilerArrUpdate(i, ri, fcpArrRuleSetKind(fcpArrRuleParse(line), kind)); +} + +function profilerArrSetSc(i, ri, si, value) { + const line = profilerArrLine(i, ri); + if (line === undefined) return; + profilerArrUpdate(i, ri, fcpArrRuleSetSc(fcpArrRuleParse(line), si, value)); +} + +function profilerArrSetUq(i, ri, si, value) { + const line = profilerArrLine(i, ri); + if (line === undefined) return; + const uq = value === 'verify' ? 0x08 + : (value === 'not-used' ? 0x00 : parseInt(String(value).replace('usage:', ''), 10)); + profilerArrUpdate(i, ri, fcpArrRuleSetUq(fcpArrRuleParse(line), si, uq)); +} + +function profilerArrAddSc(i, ri) { + const line = profilerArrLine(i, ri); + if (line === undefined) return; + profilerArrUpdate(i, ri, fcpArrRuleAddSc(fcpArrRuleParse(line))); +} + +function profilerArrRemoveSc(i, ri, si) { + const line = profilerArrLine(i, ri); + if (line === undefined) return; + profilerArrUpdate(i, ri, fcpArrRuleRemoveSc(fcpArrRuleParse(line), si)); +} + +// A new rule starts from READ always (the common baseline). +function profilerArrAddRule(i) { + if (!profilerDraft) return; + const r = profilerDraft.rules[i]; + if (!r.accessRules) r.accessRules = { expected: [] }; + r.accessRules.expected.push(fcpArrRuleEncode({ kind: 'bits', am: 0x01, scs: [{ kind: 'alw' }] })); + profilerRenderEditor(); +} + +function profilerArrRemoveRule(i, ri) { + if (!profilerDraft) return; + const r = profilerDraft.rules[i]; + if (!r.accessRules) return; + r.accessRules.expected.splice(ri, 1); + profilerRenderEditor(); +} + function profilerSetAccessRules(i, on) { if (!profilerDraft) return; const r = profilerDraft.rules[i]; @@ -20600,6 +20733,160 @@ function fcpArrRules(ref, arrs, path) { return { found: found, rules: out }; } +// ---- structured access-rule editing (v3.25.0 review) ----------------------- +// The editor edits a decoded rule's structure (AM byte / command + SC +// conditions) and stores the canonical decoded strings the check compares. +// fcpArrRuleParse returns {raw} for the forms outside the supported subset +// (OR/AND/NOT templates, SM CRTs, command descriptions) - shown read-only. + +// Split on a separator at parenthesis depth 0 (the templates nest). +function fcpArrSplitTop(s, sep) { + const out = []; + let depth = 0, start = 0; + for (let i = 0; i < s.length; i++) { + if (s[i] === '(') depth++; + else if (s[i] === ')') depth = Math.max(0, depth - 1); + else if (depth === 0 && s.startsWith(sep, i)) { + out.push(s.slice(start, i)); + i += sep.length - 1; + start = i + 1; + } + } + out.push(s.slice(start)); + return out; +} + +// The key reference whose pinKeyRefName matches a decoded name (null for the +// 'key ref 0xNN' default and unknown names). +function fcpArrKeyRefFromName(name) { + for (let ref = 0; ref <= 0xFF; ref++) { + const n = pinKeyRefName(ref); + if (n === name && !/^key ref /.test(n)) return ref; + } + return null; +} + +// {ref, name} options for the condition select (the named key references). +function fcpArrKeyRefOptions() { + const out = []; + for (let ref = 0; ref <= 0xFF; ref++) { + const n = pinKeyRefName(ref); + if (!/^key ref /.test(n)) out.push({ ref: ref, name: n }); + } + return out; +} + +// One SC (security condition) part of a decoded rule -> model, or null. +function fcpArrScParse(text) { + if (text === 'always') return { kind: 'alw' }; + if (text === 'never') return { kind: 'nev' }; + const m = /^(.*) \((verify|verification not used|usage 0x([0-9A-F]{2}))\)$/.exec(text); + if (m) { + const ref = fcpArrKeyRefFromName(m[1]); + if (ref !== null) { + const uq = m[2] === 'verify' ? 0x08 + : (m[2] === 'verification not used' ? 0x00 : parseInt(m[3], 16)); + return { kind: 'key', ref: ref, uq: uq }; + } + } + return null; +} + +// Model -> the decoded string efArrSc would print (the check compares these). +function fcpArrScEncode(sc) { + if (sc.kind === 'alw') return 'always'; + if (sc.kind === 'nev') return 'never'; + if (sc.kind === 'key') { + let s = pinKeyRefName(sc.ref); + const uq = sc.uq; + if (uq === null || uq === undefined || (uq & 0x08)) s += ' (verify)'; + else if (uq === 0x00) s += ' (verification not used)'; + else s += ' (usage 0x' + uq.toString(16).toUpperCase().padStart(2, '0') + ')'; + return s; + } + return sc.text || ''; +} + +// A decoded rule line -> editable model, or {raw} when outside the subset. +function fcpArrRuleParse(line) { + const s = String(line || ''); + const idx = s.indexOf(': '); + const amPart = idx < 0 ? s : s.slice(0, idx); + const scPart = idx < 0 ? '' : s.slice(idx + 2); + let kind = null, am = 0; + if (amPart === 'RESIZE FILE (AM_DO 0x84, INS 0xD4)') kind = 'resize'; + else if (amPart === 'INCREASE (AM_DO 0x84, INS 0x32)') kind = 'increase'; + else { + const m = /^AM 0x([0-9A-F]{2}) \(/.exec(amPart); + if (m) { kind = 'bits'; am = parseInt(m[1], 16); } + } + if (!kind) return { raw: s }; + const scs = []; + if (scPart) { + for (const part of fcpArrSplitTop(scPart, ' AND ')) { + const sc = fcpArrScParse(part); + if (!sc) return { raw: s }; + scs.push(sc); + } + } + return { kind: kind, am: am, scs: scs }; +} + +// Model -> the canonical decoded line. +function fcpArrRuleEncode(rule) { + if (rule.raw !== undefined) return rule.raw; + let am; + if (rule.kind === 'resize') am = 'RESIZE FILE (AM_DO 0x84, INS 0xD4)'; + else if (rule.kind === 'increase') am = 'INCREASE (AM_DO 0x84, INS 0x32)'; + else am = 'AM 0x' + (rule.am & 0xFF).toString(16).toUpperCase().padStart(2, '0') + + ' (' + efArrAmLabel(rule.am) + ')'; + const scs = (rule.scs || []).map(fcpArrScEncode); + return am + (scs.length ? ': ' + scs.join(' AND ') : ''); +} + +// The editor mutates the parsed model and re-encodes; a raw model is left as +// it is (its row is read-only). +function fcpArrRuleToggleBit(rule, bit, on) { + if (rule.raw !== undefined) return rule; + const am = on ? (rule.am | bit) : (rule.am & ~bit); + return { kind: rule.kind, am: am & 0xFF, scs: rule.scs.slice() }; +} + +function fcpArrRuleSetKind(rule, kind) { + if (rule.raw !== undefined) return rule; + return { kind: kind, am: kind === 'bits' ? (rule.am || 0x01) : rule.am, scs: rule.scs.slice() }; +} + +function fcpArrRuleSetSc(rule, si, value) { + if (rule.raw !== undefined) return rule; + const scs = rule.scs.slice(); + if (value === 'always') scs[si] = { kind: 'alw' }; + else if (value === 'never') scs[si] = { kind: 'nev' }; + else if (String(value).startsWith('key:')) { + const prev = scs[si]; + scs[si] = { kind: 'key', ref: parseInt(String(value).slice(4), 10), + uq: (prev && prev.kind === 'key') ? prev.uq : 0x08 }; + } + return { kind: rule.kind, am: rule.am, scs: scs }; +} + +function fcpArrRuleSetUq(rule, si, uq) { + if (rule.raw !== undefined) return rule; + const scs = rule.scs.slice(); + if (scs[si] && scs[si].kind === 'key') scs[si] = { kind: 'key', ref: scs[si].ref, uq: uq }; + return { kind: rule.kind, am: rule.am, scs: scs }; +} + +function fcpArrRuleAddSc(rule) { + if (rule.raw !== undefined) return rule; + return { kind: rule.kind, am: rule.am, scs: rule.scs.concat([{ kind: 'alw' }]) }; +} + +function fcpArrRuleRemoveSc(rule, si) { + if (rule.raw !== undefined) return rule; + return { kind: rule.kind, am: rule.am, scs: rule.scs.filter((_, i) => i !== si) }; +} + function fcpDo(key, value) { if (/^[0-9A-F]+$/.test(value) === false) value = value || ''; switch (key) { @@ -21556,23 +21843,26 @@ function pinKeyRefName(v) { return 'key ref 0x' + v.toString(16).toUpperCase().padStart(2, '0'); } -// ISO 7816-4 Tables 16/17: AM byte bit meanings. A record does not store -// whether it protects an EF or a DF, so both mappings are shown. +// ISO 7816-4 Tables 16/17: AM byte bit meanings, highest bit first (the +// decoder's label order). A record does not store whether it protects an EF +// or a DF, so both mappings are shown; the structured access-rule editor +// renders the same table low-to-high as operation checkboxes (v3.25.0 review). +const FCP_ARR_AM_BITS = [ + [0x40, 'DELETE (self)'], + [0x20, 'TERMINATE EF / TERMINATE DF'], + [0x10, 'ACTIVATE'], + [0x08, 'DEACTIVATE'], + [0x04, 'WRITE/APPEND (EF) / CREATE FILE DF (DF)'], + [0x02, 'UPDATE/ERASE (EF) / CREATE FILE EF (DF)'], + [0x01, 'READ/SEARCH (EF) / DELETE FILE child (DF)'], +]; + function efArrAmLabel(am) { - const bits = [ - [0x40, 'DELETE (self)'], - [0x20, 'TERMINATE EF / TERMINATE DF'], - [0x10, 'ACTIVATE'], - [0x08, 'DEACTIVATE'], - [0x04, 'WRITE/APPEND (EF) / CREATE FILE DF (DF)'], - [0x02, 'UPDATE/ERASE (EF) / CREATE FILE EF (DF)'], - [0x01, 'READ/SEARCH (EF) / DELETE FILE child (DF)'], - ]; if (am & 0x80) { - const low = bits.filter(b => (b[0] & 0x07) && (am & b[0])).map(b => b[1]); + const low = FCP_ARR_AM_BITS.filter(b => (b[0] & 0x07) && (am & b[0])).map(b => b[1]); return 'bits 7-4 proprietary' + (low.length ? ' + ' + low.join(' + ') : ''); } - const ops = bits.filter(b => am & b[0]).map(b => b[1]); + const ops = FCP_ARR_AM_BITS.filter(b => am & b[0]).map(b => b[1]); return ops.length ? ops.join(' + ') : 'no operations'; } @@ -23121,6 +23411,10 @@ const LANG_RU = { 'the EF.ARR reference could not be resolved (no cached EF.ARR or the record is out of range)': 'ссылку на EF.ARR не удалось разрешить (нет кэша EF.ARR или запись вне диапазона)', 'Captured when the profile is created from a card or a snapshot': 'Заполняется при создании профиля с карты или из снимка', 'EF.ARR reference ignored (verified by the access-rules check)': 'Ссылка на EF.ARR игнорируется (проверяется по правилам доступа)', + 'rule': 'правило', + 'condition': 'условие', + 'raw': 'как есть', + 'bit mask': 'битовая маска', 'Mask': 'Маска', 'Checking': 'Проверка', 'passed': 'пройдено', diff --git a/frontend/tests/ef_decode.test.js b/frontend/tests/ef_decode.test.js index bac7571..0d6bf8e 100644 --- a/frontend/tests/ef_decode.test.js +++ b/frontend/tests/ef_decode.test.js @@ -39,7 +39,7 @@ const FNS = [ let code = ''; for (const f of FNS) code += extractFunc(html, f) + '\n'; -for (const c of ['GSM7_ALPHABET', 'GSM7_EXT_MAP']) { +for (const c of ['GSM7_ALPHABET', 'GSM7_EXT_MAP', 'FCP_ARR_AM_BITS']) { code += html.match(new RegExp('const ' + c + ' = [\\s\\S]*?\\n\\];'))[0].replace('const ', 'var ') + '\n'; } for (const c of ['EF_UST_SERVICES', 'EF_SST_SERVICES', 'EF_IST_SERVICES', 'EF_PHASE_NAMES']) { diff --git a/frontend/tests/profiler.test.js b/frontend/tests/profiler.test.js index 682e145..02999bd 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', 'profilerAccessRulesDiffHtml', 'profilerSnapshotArr', 'efBytes', 'efHex', 'efTlv', 'efDecArr', 'efArrAmLabel', 'efArrSc', 'efArrAmDo', 'efFidFromPath', + 'fcpArrRef', 'fcpSeidName', 'fcpArrRules', 'fcpArrRefFromFci', 'fcpWithoutArrRef', 'fcpArrSplitTop', 'fcpArrKeyRefFromName', 'fcpArrKeyRefOptions', 'fcpArrScParse', 'fcpArrScEncode', 'fcpArrRuleParse', 'fcpArrRuleEncode', 'fcpArrRuleToggleBit', 'fcpArrRuleSetKind', 'fcpArrRuleSetSc', 'fcpArrRuleSetUq', 'fcpArrRuleAddSc', 'fcpArrRuleRemoveSc', 'profilerRenderAccessRulesEditor', 'profilerArrRuleHtml', 'profilerArrScSelect', 'profilerArrUqSelect', 'profilerAccessRulesDiffHtml', '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'; @@ -40,6 +40,7 @@ code += "var _pysimArrCache = null;\nvar _pysimCardSession = null;\n"; // the view-history hooks (the router lives in the page; this pure test stubs it) 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'; eval(code); test('profilerNormHex uppercases and strips non-hex', () => { @@ -1058,6 +1059,81 @@ test('the implicit form prefers the file DF\'s own ARR copy', () => { delete global.t; }); +test('fcpArrRuleParse/Encode round-trips the decoded rule forms', () => { + const lines = [ + 'AM 0x01 (READ/SEARCH (EF) / DELETE FILE child (DF)): always', + 'AM 0x1A (ACTIVATE + DEACTIVATE + UPDATE/ERASE (EF) / CREATE FILE EF (DF)): ADM1 (verify)', + 'AM 0x02 (UPDATE/ERASE (EF) / CREATE FILE EF (DF)): PIN app1 (verify) AND ADM1 (verify)', + 'RESIZE FILE (AM_DO 0x84, INS 0xD4): never', + 'INCREASE (AM_DO 0x84, INS 0x32): ADM1 (verify)', + 'AM 0x01 (READ/SEARCH (EF) / DELETE FILE child (DF))', + ]; + for (const line of lines) { + const m = fcpArrRuleParse(line); + assert.strictEqual(m.raw, undefined, line); + assert.strictEqual(fcpArrRuleEncode(m), line, line); + } + // template forms and unknown conditions stay raw and unchanged + for (const raw of [ + 'AM 0x02 (UPDATE/ERASE (EF) / CREATE FILE EF (DF)): (PIN app1 (verify) OR PIN app2 (verify))', + 'AM 0x01 (READ/SEARCH (EF) / DELETE FILE child (DF)): SM CRT 0xB4', + 'command description 0x84 (D4)', + ]) { + const rm = fcpArrRuleParse(raw); + assert.strictEqual(rm.raw, raw, raw); + assert.strictEqual(fcpArrRuleEncode(rm), raw, raw); + } +}); + +test('fcpArrRule mutators re-encode the canonical line', () => { + let m = fcpArrRuleParse('AM 0x01 (READ/SEARCH (EF) / DELETE FILE child (DF)): always'); + m = fcpArrRuleToggleBit(m, 0x02, true); + assert.strictEqual(fcpArrRuleEncode(m), + 'AM 0x03 (UPDATE/ERASE (EF) / CREATE FILE EF (DF) + READ/SEARCH (EF) / DELETE FILE child (DF)): always'); + m = fcpArrRuleSetSc(m, 0, 'key:10'); // 10 = 0x0A = ADM1 + assert.ok(fcpArrRuleEncode(m).endsWith(': ADM1 (verify)'), fcpArrRuleEncode(m)); + m = fcpArrRuleSetUq(m, 0, 0x00); + assert.ok(fcpArrRuleEncode(m).endsWith(': ADM1 (verification not used)'), fcpArrRuleEncode(m)); + m = fcpArrRuleAddSc(m); + assert.ok(fcpArrRuleEncode(m).endsWith(': ADM1 (verification not used) AND always'), fcpArrRuleEncode(m)); + m = fcpArrRuleRemoveSc(m, 1); + assert.ok(fcpArrRuleEncode(m).endsWith(': ADM1 (verification not used)'), fcpArrRuleEncode(m)); + m = fcpArrRuleSetKind(m, 'resize'); + assert.strictEqual(fcpArrRuleEncode(m), 'RESIZE FILE (AM_DO 0x84, INS 0xD4): ADM1 (verification not used)'); + m = fcpArrRuleSetSc(m, 0, 'never'); + assert.strictEqual(fcpArrRuleEncode(m), 'RESIZE FILE (AM_DO 0x84, INS 0xD4): never'); + // a raw model is never modified + const raw = fcpArrRuleParse('command description 0x84 (D4)'); + assert.strictEqual(fcpArrRuleEncode(fcpArrRuleToggleBit(raw, 0x01, true)), 'command description 0x84 (D4)'); +}); + +test('the access-rules editor renders structured rows', () => { + global.t = s => s; + const r = { fciMode: 'exact', accessRules: { expected: [ + 'AM 0x01 (READ/SEARCH (EF) / DELETE FILE child (DF)): always', + 'RESIZE FILE (AM_DO 0x84, INS 0xD4): never', + 'AM 0x02 (UPDATE/ERASE (EF) / CREATE FILE EF (DF)): (PIN app1 (verify) OR PIN app2 (verify))', + ] } }; + const html = profilerRenderAccessRulesEditor(3, r); + // the READ bit is checked; the condition select shows always + assert.ok(html.includes('type="checkbox" checked onchange="profilerArrToggleBit(3,0,1,this.checked)"'), html); + assert.ok(html.includes(''), html); + assert.ok(html.includes(''), html); + // the RESIZE row uses the command form, not bits + assert.ok(html.includes(''), html); + // the template rule is raw and not editable + assert.ok(html.includes('raw: AM 0x02'), html); + assert.ok(!html.includes('profilerArrToggleBit(3,2'), html); + // the preview lines and the + rule button are present + assert.ok(html.includes('RESIZE FILE (AM_DO 0x84, INS 0xD4): never'), html); + assert.ok(html.includes('profilerArrAddRule(3)'), html); + // an empty rule shows the capture hint with no disabled controls + const empty = profilerRenderAccessRulesEditor(4, { fciMode: 'type_size', accessRules: null }); + assert.ok(empty.includes('profilerArrAddRule(4)'), empty); + assert.ok(!empty.includes(' disabled'), empty); + delete global.t; +}); + test('profilerResultAspects and the summary include Access rules', () => { const res = { checks: [ { label: 'exists', ok: true },