v3.25.0 review: the access-rules open-state lifecycle

- profilerEdit resets the expanded-block map, so opening another profile
  (or a freshly scanned one) starts with every access-rules block
  collapsed instead of inheriting the previous profile's state.
- Deleting a rule re-keys the map (profilerArrShiftOpen): the entries
  below stay, the ones above shift down, so the expanded blocks stay
  with their rules.
- The declaration sits before the render function that reads it.
- Test fixture: a real command-description tag (0x85) instead of the
  RESIZE branch's 0x84.

Tests: the shift helper, the profilerEdit reset pin, the existing
editor tests.  883 frontend + 858 Python; sw cache simple-v424.
This commit is contained in:
2026-10-11 00:17:36 +03:00
parent b00d533f03
commit db78696865
2 changed files with 32 additions and 8 deletions
+20 -4
View File
@@ -19545,6 +19545,8 @@ function profilerEdit(i) {
const p = profiles[i]; const p = profiles[i];
profilerEditId = p.id; profilerEditId = p.id;
profilerDraft = JSON.parse(JSON.stringify(p)); profilerDraft = JSON.parse(JSON.stringify(p));
// a fresh draft starts with every access-rules block collapsed
_profilerArrOpen = {};
profilerSetView('editor'); profilerSetView('editor');
document.getElementById('profiler-name').value = profilerDraft.name; document.getElementById('profiler-name').value = profilerDraft.name;
profilerRenderEditor(); profilerRenderEditor();
@@ -19733,6 +19735,7 @@ function profilerAddRule() {
function profilerDeleteRule(i) { function profilerDeleteRule(i) {
if (!profilerDraft) return; if (!profilerDraft) return;
profilerDraft.rules.splice(i, 1); profilerDraft.rules.splice(i, 1);
_profilerArrOpen = profilerArrShiftOpen(_profilerArrOpen, i);
profilerRenderEditor(); profilerRenderEditor();
} }
@@ -19807,6 +19810,11 @@ function profilerRenderRule(i, r) {
return html; return html;
} }
// 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).
let _profilerArrOpen = {};
// Access-rules check editor (v3.25.0): captured rules open as structured rows // Access-rules check editor (v3.25.0): captured rules open as structured rows
// (operation bits / command + security conditions) and hand-made rules can be // (operation bits / command + security conditions) and hand-made rules can be
// built the same way; every change re-encodes the canonical decoded line the // built the same way; every change re-encodes the canonical decoded line the
@@ -19844,15 +19852,23 @@ function profilerRenderAccessRulesEditor(i, r) {
return html; return html;
} }
// The expanded access-rule blocks of the profile editor (transient UI state,
// kept across the editor re-renders; None hides the block entirely).
let _profilerArrOpen = {};
function profilerArrToggleOpen(i) { function profilerArrToggleOpen(i) {
_profilerArrOpen[i] = !_profilerArrOpen[i]; _profilerArrOpen[i] = !_profilerArrOpen[i];
profilerRenderEditor(); profilerRenderEditor();
} }
// Re-key the open-state map after a rule at `removed` was deleted: the
// entries below stay, the ones above shift down by one.
function profilerArrShiftOpen(open, removed) {
const next = {};
for (const k of Object.keys(open)) {
const n = Number(k);
if (n < removed) next[n] = open[k];
else if (n > removed) next[n - 1] = open[k];
}
return next;
}
// One expected rule row: the operation form + bit checkboxes (or the RESIZE / // One expected rule row: the operation form + bit checkboxes (or the RESIZE /
// INCREASE command), the condition rows and the canonical preview line. // INCREASE command), the condition rows and the canonical preview line.
function profilerArrRuleHtml(i, ri, line) { function profilerArrRuleHtml(i, ri, line) {
+12 -4
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', 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', 'fcpArrRuleToggleBit', 'fcpArrRuleSetKind', 'fcpArrRuleSetSc', 'fcpArrRuleSetUq', 'fcpArrRuleAddSc', 'fcpArrRuleRemoveSc', 'profilerRenderAccessRulesEditor', 'profilerArrRuleHtml', 'profilerArrScSelect', 'profilerArrUqSelect', 'profilerArrToggleOpen', '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', 'profilerArrToggleOpen', 'profilerArrShiftOpen', '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']; '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 = ''; let code = '';
for (const f of FNS) code += extractFunc(html, f) + '\n'; for (const f of FNS) code += extractFunc(html, f) + '\n';
@@ -1078,7 +1078,7 @@ test('fcpArrRuleParse/Encode round-trips the decoded rule forms', () => {
for (const raw of [ for (const raw of [
'AM 0x02 (UPDATE/ERASE (EF) / CREATE FILE EF (DF)): (PIN app1 (verify) OR PIN app2 (verify))', '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', 'AM 0x01 (READ/SEARCH (EF) / DELETE FILE child (DF)): SM CRT 0xB4',
'command description 0x84 (D4)', 'command description 0x85 (D4)',
]) { ]) {
const rm = fcpArrRuleParse(raw); const rm = fcpArrRuleParse(raw);
assert.strictEqual(rm.raw, raw, raw); assert.strictEqual(rm.raw, raw, raw);
@@ -1104,8 +1104,8 @@ test('fcpArrRule mutators re-encode the canonical line', () => {
m = fcpArrRuleSetSc(m, 0, 'never'); m = fcpArrRuleSetSc(m, 0, 'never');
assert.strictEqual(fcpArrRuleEncode(m), 'RESIZE FILE (AM_DO 0x84, INS 0xD4): never'); assert.strictEqual(fcpArrRuleEncode(m), 'RESIZE FILE (AM_DO 0x84, INS 0xD4): never');
// a raw model is never modified // a raw model is never modified
const raw = fcpArrRuleParse('command description 0x84 (D4)'); const raw = fcpArrRuleParse('command description 0x85 (D4)');
assert.strictEqual(fcpArrRuleEncode(fcpArrRuleToggleBit(raw, 0x01, true)), 'command description 0x84 (D4)'); assert.strictEqual(fcpArrRuleEncode(fcpArrRuleToggleBit(raw, 0x01, true)), 'command description 0x85 (D4)');
}); });
test('the access-rules editor renders structured rows', () => { test('the access-rules editor renders structured rows', () => {
@@ -1154,9 +1154,17 @@ test('the access-rules editor renders structured rows', () => {
_profilerArrOpen = {}; _profilerArrOpen = {};
// Check opens the block, None hides it // Check opens the block, None hides it
assert.ok(html.includes('_profilerArrOpen[i] = on;'), 'the radio drives the visibility'); assert.ok(html.includes('_profilerArrOpen[i] = on;'), 'the radio drives the visibility');
// a fresh draft resets the open blocks
assert.ok(html.includes("_profilerArrOpen = {};\n\tprofilerSetView('editor');"), 'profilerEdit resets the blocks');
delete global.t; delete global.t;
}); });
test('the access-rule open state survives rule deletion', () => {
assert.deepStrictEqual(profilerArrShiftOpen({ 0: true, 2: true, 3: true }, 1), { 0: true, 1: true, 2: true });
assert.deepStrictEqual(profilerArrShiftOpen({ 0: true, 2: true, 3: true }, 2), { 0: true, 2: true });
assert.deepStrictEqual(profilerArrShiftOpen({}, 0), {});
});
test('profilerResultAspects and the summary include Access rules', () => { test('profilerResultAspects and the summary include Access rules', () => {
const res = { checks: [ const res = { checks: [
{ label: 'exists', ok: true }, { label: 'exists', ok: true },