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.
This commit is contained in:
2026-10-10 23:55:07 +03:00
parent a37266f865
commit c69a779ca4
5 changed files with 399 additions and 29 deletions
+1 -1
View File
@@ -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']) {
+77 -1
View File
@@ -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('<option value="always" selected>always</option>'), html);
assert.ok(html.includes('<option value="key:10">ADM1</option>'), html);
// the RESIZE row uses the command form, not bits
assert.ok(html.includes('<option value="resize" selected>RESIZE FILE</option>'), 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 },