Files
reverse-engineering-anything/src/domain/browserScenarioNormalization.ts
T
Devulapalli Naga Sri Vaishnaviandmorluto 809f3b7b5b fix(browser): ignore scenario storage enumeration order (#5035)
* fix(browser): ignore scenario storage enumeration order

* perf(browser): sort storage entries without serialized copies

* style(browser): format storage normalization comparator

---------

Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
2026-10-23 02:26:41 +08:00

120 lines
4.0 KiB
TypeScript

import { digestCanonicalValue } from "./canonicalDigest.js";
import { compareUnicodeCodePoints } from "./unicodeCodePointOrder.js";
import {
BROWSER_SCENARIO_BUILT_IN_NORMALIZATION_RULES,
type BrowserScenarioArtifactKind,
type BrowserScenarioDiff,
type BrowserScenarioNormalization,
type BrowserScenarioNormalizationRule,
} from "./browserScenarioDiffValues.js";
/** Canonically order and commit a parsed browser scenario normalization policy. */
export const commitBrowserScenarioNormalization = (
input: BrowserScenarioNormalization,
): BrowserScenarioDiff["normalization"] => {
const rules = [...input.rules]
.map((rule) => ({
...rule,
artifacts: [...rule.artifacts].sort(),
}))
.sort((left, right) =>
left.rule_id < right.rule_id ? -1 : left.rule_id > right.rule_id ? 1 : 0,
);
const commitment = {
built_in_rules: [...BROWSER_SCENARIO_BUILT_IN_NORMALIZATION_RULES],
rules,
};
return {
...commitment,
sha256: digestCanonicalValue(commitment, "Browser scenario"),
};
};
/** Digest one artifact after applying committed exact-literal rules, then any artifact ordering. */
export const digestNormalizedScenarioValue = (
value: unknown,
artifact: BrowserScenarioArtifactKind,
rules: readonly BrowserScenarioNormalizationRule[],
order: (normalized: unknown) => unknown = (normalized) => normalized,
): string =>
digestCanonicalValue(
order(normalizeScenarioValue(value, artifact, rules)),
"Browser scenario",
);
export const normalizeScenarioValue = (
value: unknown,
artifact: BrowserScenarioArtifactKind,
rules: readonly BrowserScenarioNormalizationRule[],
): unknown => {
if (artifact === "screenshot") return value;
if (typeof value === "string")
return rules
.filter(({ artifacts }) => artifacts.includes(artifact))
.reduce(
(normalized, rule) =>
normalized.split(rule.match).join(rule.replacement),
value,
);
if (Array.isArray(value)) {
const normalized = value.map((item) =>
normalizeScenarioValue(item, artifact, rules),
);
// Storage arrays are inventories, not ordered events. Sort complete entries
// after literal rules; names alone can collide after redaction/normalization.
// Keep multiplicity and every cookie attribute or value fingerprint.
return artifact === "storage"
? normalized.sort(compareStorageEntries)
: normalized;
}
if (typeof value !== "object" || value === null) return value;
return Object.fromEntries(
Object.entries(value).map(([key, item]) => [
key,
normalizeScenarioValue(item, artifact, rules),
]),
);
};
/** Compare parsed storage records by their fixed fields without serializing them. */
const compareStorageEntries = (left: unknown, right: unknown): number => {
if (!isStorageRecord(left) || !isStorageRecord(right))
return compareUnicodeCodePoints(
digestCanonicalValue(left, "Browser scenario"),
digestCanonicalValue(right, "Browser scenario"),
);
const fields =
Object.hasOwn(left, "domain") || Object.hasOwn(right, "domain")
? [
"domain",
"http_only",
"name",
"path",
"same_site",
"secure",
"value_sha256",
"value_state",
]
: ["name", "value_sha256", "value_state"];
for (const field of fields) {
const compared = compareStorageField(left[field], right[field]);
if (compared !== 0) return compared;
}
return 0;
};
const isStorageRecord = (value: unknown): value is Record<string, unknown> =>
typeof value === "object" && value !== null && !Array.isArray(value);
const compareStorageField = (left: unknown, right: unknown): number => {
if (left === right) return 0;
if (left === null) return -1;
if (right === null) return 1;
if (typeof left === "string" && typeof right === "string")
return compareUnicodeCodePoints(left, right);
if (typeof left === "boolean" && typeof right === "boolean")
return left ? 1 : -1;
return compareUnicodeCodePoints(String(left), String(right));
};