mirror of
https://github.com/morluto/rea.git
synced 2026-10-12 02:19:24 +00:00
fix: order remaining outputs independently of host locale (#1867)
* fix: order remaining outputs independently of host locale
String.prototype.localeCompare without a pinned collation follows the
host's ICU default locale, so the same input could be ordered differently
on different machines. 614fb768 moved canonical domain projections to
compareUnicodeCodePoints; provider, application, catalog, and setup
orderings still used locale collation.
Order every remaining source site by Unicode code point, including Mach-O
symbols, firmware extraction paths, ASAR entries (previously pinned to
"en" collation), evidence ID sets, browser and Electron target, script,
and resource inventories, reconstruction obligations, capability and
provider listings, and setup environment details.
The current tool and prompt catalog names order identically under both
comparators, so the catalog identity digest is unchanged.
* fix(artifacts): order ASAR entries by their portable path
@electron/asar lists host-spelled paths, so code-point order over the raw
spelling still differed by host: "a\b" sorts after "a0" on Windows while
"a/b" sorts before it on POSIX. Pair each listing with its portable
archive path once, order by that path, and keep the host spelling for
@electron/asar calls and adapter keys.
This commit is contained in:
@@ -20,6 +20,7 @@ import {
|
||||
clientRequirementsFor,
|
||||
NO_CLIENT_FEATURES,
|
||||
} from "./CapabilityClientRequirements.js";
|
||||
import { compareUnicodeCodePoints } from "../domain/unicodeCodePointOrder.js";
|
||||
|
||||
const statusSchema = z.object({
|
||||
open: z.boolean(),
|
||||
@@ -210,7 +211,7 @@ export const buildCapabilityInventory = (
|
||||
reason: availability.reason,
|
||||
remediation: availability.remediation,
|
||||
};
|
||||
}).sort((left, right) => left.name.localeCompare(right.name));
|
||||
}).sort((left, right) => compareUnicodeCodePoints(left.name, right.name));
|
||||
};
|
||||
|
||||
const availabilityFor = (context: AvailabilityContext): Availability => {
|
||||
|
||||
@@ -18,6 +18,7 @@ import { z } from "zod";
|
||||
|
||||
import { PRODUCT_IDENTITY } from "../identity.js";
|
||||
import type { SetupClient } from "./SupportedClients.js";
|
||||
import { compareUnicodeCodePoints } from "../domain/unicodeCodePointOrder.js";
|
||||
|
||||
export type ClientConfigurationFormat = NonNullable<SetupClient["format"]>;
|
||||
export type ClientServersKey = "mcp_servers" | "mcpServers" | "mcp" | "servers";
|
||||
@@ -63,7 +64,7 @@ const normalizeConfigurationValue = (value: unknown): unknown => {
|
||||
return value;
|
||||
return Object.fromEntries(
|
||||
Object.entries(value)
|
||||
.sort(([left], [right]) => left.localeCompare(right))
|
||||
.sort(([left], [right]) => compareUnicodeCodePoints(left, right))
|
||||
.map(([key, nested]) => [key, normalizeConfigurationValue(nested)]),
|
||||
);
|
||||
};
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
type ClientConfigurationDocument,
|
||||
} from "./ClientConfigurationDocument.js";
|
||||
import { PRODUCT_IDENTITY } from "../identity.js";
|
||||
import { compareUnicodeCodePoints } from "../domain/unicodeCodePointOrder.js";
|
||||
|
||||
/** Read explicit REA overrides without dropping peers when one value is invalid. */
|
||||
export const clientRegistrationEnvironment = (
|
||||
@@ -47,7 +48,7 @@ export const clientRegistrationEnvironment = (
|
||||
return ok(
|
||||
Object.fromEntries(
|
||||
Object.entries(environment).sort(([left], [right]) =>
|
||||
left.localeCompare(right),
|
||||
compareUnicodeCodePoints(left, right),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
supportedClients,
|
||||
} from "./SupportedClients.js";
|
||||
import type { SetupClient } from "./SupportedClients.js";
|
||||
import { compareUnicodeCodePoints } from "../domain/unicodeCodePointOrder.js";
|
||||
|
||||
interface ClientRegistrationStatusBase {
|
||||
readonly client: string;
|
||||
@@ -184,7 +185,7 @@ export const readClientRegistrationStatuses = async (
|
||||
}
|
||||
}
|
||||
return statuses.sort((left, right) =>
|
||||
left.client.localeCompare(right.client),
|
||||
compareUnicodeCodePoints(left.client, right.client),
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
semanticObligationPolicy,
|
||||
type ReconstructionObligationCandidatePolicy,
|
||||
} from "./ReconstructionObligationPolicies.js";
|
||||
import { compareUnicodeCodePoints } from "../domain/unicodeCodePointOrder.js";
|
||||
|
||||
type CandidateFields = Omit<
|
||||
ReconstructionObligation,
|
||||
@@ -54,7 +55,7 @@ export const deriveReconstructionObligationCandidates = (
|
||||
addCandidate(candidates, reviewedCandidate(obligation, records));
|
||||
return {
|
||||
candidates: [...candidates.values()].sort((left, right) =>
|
||||
left.obligation_id.localeCompare(right.obligation_id),
|
||||
compareUnicodeCodePoints(left.obligation_id, right.obligation_id),
|
||||
),
|
||||
limitations: [...limitations].sort(),
|
||||
};
|
||||
@@ -430,7 +431,7 @@ const uniqueBy = <Value>(
|
||||
key: (value: Value) => string,
|
||||
): Value[] =>
|
||||
[...new Map(values.map((value) => [key(value), value])).values()].sort(
|
||||
(left, right) => key(left).localeCompare(key(right)),
|
||||
(left, right) => compareUnicodeCodePoints(key(left), key(right)),
|
||||
);
|
||||
|
||||
const obligationId = (key: string): string =>
|
||||
|
||||
@@ -3,6 +3,7 @@ import type {
|
||||
ReconstructionObligationManifest,
|
||||
} from "../domain/reconstructionObligationLedgerSchemas.js";
|
||||
import type { Evidence } from "../domain/evidence.js";
|
||||
import { compareUnicodeCodePoints } from "../domain/unicodeCodePointOrder.js";
|
||||
|
||||
type Diagnostic = ReconstructionObligation["diagnostics"][number];
|
||||
type Binding = NonNullable<ReconstructionObligation["binding"]>;
|
||||
@@ -50,7 +51,8 @@ export const uniqueObligationDiagnostics = (
|
||||
]),
|
||||
).values(),
|
||||
].sort((left, right) =>
|
||||
`${left.code}:${left.detail}`.localeCompare(
|
||||
compareUnicodeCodePoints(
|
||||
`${left.code}:${left.detail}`,
|
||||
`${right.code}:${right.detail}`,
|
||||
),
|
||||
);
|
||||
|
||||
@@ -36,6 +36,7 @@ import {
|
||||
obligationReports,
|
||||
summarizeObligations,
|
||||
} from "./ReconstructionObligationLedgerProjection.js";
|
||||
import { compareUnicodeCodePoints } from "../domain/unicodeCodePointOrder.js";
|
||||
|
||||
type Diagnostic = ReconstructionObligation["diagnostics"][number];
|
||||
type Binding = NonNullable<ReconstructionObligation["binding"]>;
|
||||
@@ -60,7 +61,7 @@ export const evaluateReconstructionObligationLedger = ({
|
||||
}): ReconstructionObligationLedger => {
|
||||
const bundle = parseEvidenceBundle(bundleInput);
|
||||
const allCandidates = [...candidatesInput].sort((left, right) =>
|
||||
left.obligation_id.localeCompare(right.obligation_id),
|
||||
compareUnicodeCodePoints(left.obligation_id, right.obligation_id),
|
||||
);
|
||||
const orphanManifestIds = findOrphanManifestIds(allCandidates, manifest);
|
||||
const candidates = allCandidates;
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { Evidence } from "../domain/evidence.js";
|
||||
import type { ReconstructionObligation } from "../domain/reconstructionObligationLedgerSchemas.js";
|
||||
import { evidenceSupportsOriginalAuthority } from "./ReconstructionObligationAuthority.js";
|
||||
import type { ReconstructionObligationCandidate } from "./ReconstructionObligationCandidates.js";
|
||||
import { compareUnicodeCodePoints } from "../domain/unicodeCodePointOrder.js";
|
||||
|
||||
type Binding = NonNullable<ReconstructionObligation["binding"]>;
|
||||
type Diagnostic = ReconstructionObligation["diagnostics"][number];
|
||||
@@ -21,7 +22,8 @@ export const originalCasesFor = (
|
||||
),
|
||||
).values(),
|
||||
].sort((left, right) =>
|
||||
`${left.kind}:${left.evidence_id}:${left.location}`.localeCompare(
|
||||
compareUnicodeCodePoints(
|
||||
`${left.kind}:${left.evidence_id}:${left.location}`,
|
||||
`${right.kind}:${right.evidence_id}:${right.location}`,
|
||||
),
|
||||
);
|
||||
|
||||
@@ -15,6 +15,7 @@ import type {
|
||||
} from "./SetupTypes.js";
|
||||
import type { SetupClient } from "./SupportedClients.js";
|
||||
import type { DoctorReport } from "./Doctor.js";
|
||||
import { compareUnicodeCodePoints } from "../domain/unicodeCodePointOrder.js";
|
||||
|
||||
/** Read-only setup facts shared by client selection and preflight. */
|
||||
export interface SetupDiscovery {
|
||||
@@ -308,7 +309,7 @@ const clientConfigurationDetail = (
|
||||
installHopper: boolean,
|
||||
): string => {
|
||||
const entries = Object.entries(environment)
|
||||
.sort(([left], [right]) => left.localeCompare(right))
|
||||
.sort(([left], [right]) => compareUnicodeCodePoints(left, right))
|
||||
.map(([name, value]) => `${name}=${value}`);
|
||||
const environmentDetail =
|
||||
entries.length === 0
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { DoctorProviderInspection } from "./Doctor.js";
|
||||
import { compareUnicodeCodePoints } from "../domain/unicodeCodePointOrder.js";
|
||||
|
||||
/** Preserve adapter-published settings independently of runtime readiness. */
|
||||
export const providerRegistrationEnvironment = (
|
||||
@@ -9,5 +10,5 @@ export const providerRegistrationEnvironment = (
|
||||
.flatMap(({ registrationEnvironment }) =>
|
||||
Object.entries(registrationEnvironment),
|
||||
)
|
||||
.sort(([left], [right]) => left.localeCompare(right)),
|
||||
.sort(([left], [right]) => compareUnicodeCodePoints(left, right)),
|
||||
);
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
HermesClientPathError,
|
||||
resolveHermesClientHome,
|
||||
} from "./HermesClientHome.js";
|
||||
import { compareUnicodeCodePoints } from "../domain/unicodeCodePointOrder.js";
|
||||
|
||||
/** One supported client configuration location. */
|
||||
export interface SetupClient {
|
||||
@@ -642,6 +643,6 @@ export const clientSkillDirectories = (
|
||||
});
|
||||
}
|
||||
return [...destinations.values()].sort((left, right) =>
|
||||
left.directory.localeCompare(right.directory),
|
||||
compareUnicodeCodePoints(left.directory, right.directory),
|
||||
);
|
||||
};
|
||||
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
evaluateAnalysisProviderCandidate,
|
||||
type AnalysisProviderCandidateEvaluation,
|
||||
} from "./AnalysisProviderEvaluation.js";
|
||||
import { compareUnicodeCodePoints } from "../../domain/unicodeCodePointOrder.js";
|
||||
|
||||
/** Deterministic discovery truth for one configured deep provider. */
|
||||
export interface AnalysisProviderCandidateStatus {
|
||||
@@ -109,12 +110,14 @@ export class AnalysisProviderRegistry {
|
||||
}
|
||||
this.#providers = Object.freeze(
|
||||
[...providers].sort((left, right) =>
|
||||
left.identity().id.localeCompare(right.identity().id),
|
||||
compareUnicodeCodePoints(left.identity().id, right.identity().id),
|
||||
),
|
||||
);
|
||||
this.#byId = byId;
|
||||
this.#declaredOperations = Object.freeze(
|
||||
[...declaredOperations].sort((left, right) => left.localeCompare(right)),
|
||||
[...declaredOperations].sort((left, right) =>
|
||||
compareUnicodeCodePoints(left, right),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import type {
|
||||
SessionProviderRoute,
|
||||
SessionProviderRouter,
|
||||
} from "./SessionProviderRouter.js";
|
||||
import { compareUnicodeCodePoints } from "../../domain/unicodeCodePointOrder.js";
|
||||
|
||||
interface BinarySessionStatusInput {
|
||||
readonly target: BinaryTarget | undefined;
|
||||
@@ -39,7 +40,9 @@ export const binarySessionStatus = ({
|
||||
}: BinarySessionStatusInput): JsonValue => {
|
||||
const capabilities = [...route.capabilities.values()]
|
||||
.filter(({ operation }) => operation !== "inventory_artifact")
|
||||
.sort((left, right) => left.operation.localeCompare(right.operation))
|
||||
.sort((left, right) =>
|
||||
compareUnicodeCodePoints(left.operation, right.operation),
|
||||
)
|
||||
.map((descriptor) => {
|
||||
const unavailable = runtimeUnavailability.get(descriptor.operation);
|
||||
return unavailable === undefined
|
||||
@@ -69,7 +72,9 @@ export const binarySessionStatus = ({
|
||||
capabilities: candidate.capabilities
|
||||
.slice()
|
||||
.filter(({ operation }) => operation !== "inventory_artifact")
|
||||
.sort((left, right) => left.operation.localeCompare(right.operation))
|
||||
.sort((left, right) =>
|
||||
compareUnicodeCodePoints(left.operation, right.operation),
|
||||
)
|
||||
.map(capabilityStatus),
|
||||
}));
|
||||
const common = {
|
||||
@@ -102,7 +107,9 @@ const requestActivityStatus = (
|
||||
) => {
|
||||
const providers = snapshots
|
||||
.slice()
|
||||
.sort((left, right) => left.provider.id.localeCompare(right.provider.id))
|
||||
.sort((left, right) =>
|
||||
compareUnicodeCodePoints(left.provider.id, right.provider.id),
|
||||
)
|
||||
.map(({ provider, active, queuedRequests }) => ({
|
||||
provider: providerSummary(provider),
|
||||
active:
|
||||
@@ -150,7 +157,7 @@ const processLineageStatus = (
|
||||
snapshots: snapshots
|
||||
.slice()
|
||||
.sort((left, right) =>
|
||||
left.provider.id.localeCompare(right.provider.id),
|
||||
compareUnicodeCodePoints(left.provider.id, right.provider.id),
|
||||
)
|
||||
.map(({ provider, observation }) => ({
|
||||
provider: providerSummary(provider),
|
||||
|
||||
@@ -10,6 +10,7 @@ import type {
|
||||
ProviderIdentity,
|
||||
} from "../AnalysisProvider.js";
|
||||
import { closeAnalysisClient } from "./AnalysisClientCleanup.js";
|
||||
import { compareUnicodeCodePoints } from "../../domain/unicodeCodePointOrder.js";
|
||||
|
||||
/** Synthetic identity for a deterministic provider set. */
|
||||
const compositeProviderIdentity = (
|
||||
@@ -53,8 +54,8 @@ export class CompositeProvider implements AnalysisProvider {
|
||||
this.#capabilities = Object.freeze(
|
||||
capabilities.sort(
|
||||
(left, right) =>
|
||||
left.operation.localeCompare(right.operation) ||
|
||||
left.provider.id.localeCompare(right.provider.id),
|
||||
compareUnicodeCodePoints(left.operation, right.operation) ||
|
||||
compareUnicodeCodePoints(left.provider.id, right.provider.id),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -122,13 +123,13 @@ export class CompositeProvider implements AnalysisProvider {
|
||||
[...clients.values()]
|
||||
.flatMap((client) => client.runtimeLineageSnapshots?.() ?? [])
|
||||
.sort((left, right) =>
|
||||
left.provider.id.localeCompare(right.provider.id),
|
||||
compareUnicodeCodePoints(left.provider.id, right.provider.id),
|
||||
),
|
||||
requestActivitySnapshots: () =>
|
||||
[...clients.values()]
|
||||
.flatMap((client) => client.requestActivitySnapshots?.() ?? [])
|
||||
.sort((left, right) =>
|
||||
left.provider.id.localeCompare(right.provider.id),
|
||||
compareUnicodeCodePoints(left.provider.id, right.provider.id),
|
||||
),
|
||||
operationHealthSnapshot: () => {
|
||||
for (const client of clients.values()) {
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
type UpdateUnknownInput,
|
||||
} from "../../domain/residualUnknown.js";
|
||||
import { err, ok, type Result } from "../../domain/result.js";
|
||||
import { compareUnicodeCodePoints } from "../../domain/unicodeCodePointOrder.js";
|
||||
|
||||
type EvidenceLedgerFailure = EvidenceIntegrityError;
|
||||
type RecordResult = Result<"added" | "duplicate", EvidenceLedgerFailure>;
|
||||
@@ -321,7 +322,9 @@ export class EvidenceLedger {
|
||||
unknown.severity === filters.severity) &&
|
||||
(filters.domain === undefined || unknown.domain === filters.domain),
|
||||
)
|
||||
.sort((left, right) => left.unknown_id.localeCompare(right.unknown_id))
|
||||
.sort((left, right) =>
|
||||
compareUnicodeCodePoints(left.unknown_id, right.unknown_id),
|
||||
)
|
||||
.map((unknown) => structuredClone(unknown));
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { orderAsarListing } from "./AsarArtifactReader.js";
|
||||
|
||||
describe("orderAsarListing", () => {
|
||||
it("orders host-spelled listings by their portable archive path", () => {
|
||||
const posix = orderAsarListing(["/a0", "/a", "/a/b"], "/");
|
||||
const windows = orderAsarListing(["\\a0", "\\a", "\\a\\b"], "\\");
|
||||
|
||||
// "/" (0x2F) precedes "0" (0x30); "\" (0x5C) would follow it.
|
||||
expect(posix.map(({ path }) => path)).toEqual(["a", "a/b", "a0"]);
|
||||
expect(windows.map(({ path }) => path)).toEqual(["a", "a/b", "a0"]);
|
||||
expect(windows.map(({ listed }) => listed)).toEqual([
|
||||
"\\a",
|
||||
"\\a\\b",
|
||||
"\\a0",
|
||||
]);
|
||||
});
|
||||
|
||||
it("keeps POSIX backslashes as literal filename characters", () => {
|
||||
expect(orderAsarListing(["/a\\b"], "/")).toEqual([
|
||||
{ listed: "/a\\b", path: "a\\b" },
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -32,6 +32,7 @@ import { closeAsarHandle, readValidatedAsarEntry } from "./AsarEntryStream.js";
|
||||
import { admitAsarHeader } from "./AsarHeader.js";
|
||||
import { OwnedFileHandle } from "../filesystem/OwnedFileHandle.js";
|
||||
import { readFileHandleChunks } from "../filesystem/readFileHandleChunks.js";
|
||||
import { compareUnicodeCodePoints } from "../domain/unicodeCodePointOrder.js";
|
||||
|
||||
/**
|
||||
* Official Electron ASAR adapter with range-streamed member reads.
|
||||
@@ -64,7 +65,7 @@ export class AsarArtifactReader implements ArtifactReader {
|
||||
) {}
|
||||
|
||||
async *entries(signal?: AbortSignal): AsyncIterable<ArtifactEntry> {
|
||||
let paths: string[];
|
||||
let paths: readonly AsarListedPath[];
|
||||
try {
|
||||
await this.prepareContainer(undefined, signal);
|
||||
const snapshotPath = this.#snapshotPath();
|
||||
@@ -82,9 +83,7 @@ export class AsarArtifactReader implements ArtifactReader {
|
||||
await headerHandle.close();
|
||||
}
|
||||
abortIfNeeded(signal);
|
||||
paths = listPackage(snapshotPath, { isPack: false }).sort((left, right) =>
|
||||
left.localeCompare(right, "en"),
|
||||
);
|
||||
paths = orderAsarListing(listPackage(snapshotPath, { isPack: false }));
|
||||
const archiveMetadata = await lstat(snapshotPath);
|
||||
if (!archiveMetadata.isFile() || archiveMetadata.isSymbolicLink())
|
||||
throw new ArtifactReaderFailure(
|
||||
@@ -95,13 +94,8 @@ export class AsarArtifactReader implements ArtifactReader {
|
||||
} catch (cause: unknown) {
|
||||
throw asarFailure(this.path, "inventory", cause);
|
||||
}
|
||||
for (const listed of paths) {
|
||||
for (const { listed, path } of paths) {
|
||||
abortIfNeeded(signal);
|
||||
// @electron/asar returns paths assembled with the host path module.
|
||||
// Keep that spelling for its API calls, but expose archive paths with
|
||||
// portable separators. On POSIX, backslashes can be literal filename
|
||||
// characters and must remain untouched.
|
||||
const path = toArtifactPath(listed);
|
||||
if (path.length === 0) continue;
|
||||
const providerPath = listed.startsWith(sep)
|
||||
? listed.slice(sep.length)
|
||||
@@ -577,8 +571,30 @@ const waitForOperation = async (
|
||||
});
|
||||
};
|
||||
|
||||
const toArtifactPath = (listed: string): string => {
|
||||
const portable = sep === "\\" ? listed.replaceAll("\\", "/") : listed;
|
||||
interface AsarListedPath {
|
||||
readonly listed: string;
|
||||
readonly path: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pair each host-spelled listing with its portable archive path, ordered by
|
||||
* the portable path so Windows and POSIX hosts agree.
|
||||
*
|
||||
* @electron/asar returns paths assembled with the host path module. Keep that
|
||||
* spelling for its API calls, but expose archive paths with portable
|
||||
* separators. On POSIX, backslashes can be literal filename characters and
|
||||
* must remain untouched.
|
||||
*/
|
||||
export const orderAsarListing = (
|
||||
listed: readonly string[],
|
||||
separator: string = sep,
|
||||
): AsarListedPath[] =>
|
||||
listed
|
||||
.map((entry) => ({ listed: entry, path: toArtifactPath(entry, separator) }))
|
||||
.sort((left, right) => compareUnicodeCodePoints(left.path, right.path));
|
||||
|
||||
const toArtifactPath = (listed: string, separator: string): string => {
|
||||
const portable = separator === "\\" ? listed.replaceAll("\\", "/") : listed;
|
||||
return portable.replace(/^\/+|\/+$/gu, "");
|
||||
};
|
||||
|
||||
|
||||
@@ -68,6 +68,7 @@ import { captureCdpScreenshot } from "./CdpScreenshot.js";
|
||||
import { comparePngScreenshots } from "./PngVisualDiff.js";
|
||||
|
||||
import { CDP_BROWSER_PROVIDER_IDENTITY } from "./providerIdentities.js";
|
||||
import { compareUnicodeCodePoints } from "../domain/unicodeCodePointOrder.js";
|
||||
const CLEANUP_DOMAINS = ["Network", "Debugger", "Runtime", "Page"] as const;
|
||||
|
||||
/** Passive browser observation through a selected user-owned CDP endpoint. */
|
||||
@@ -408,7 +409,9 @@ const selectTargets = (
|
||||
attached: target.attached,
|
||||
});
|
||||
}
|
||||
allowed.sort((left, right) => left.target_id.localeCompare(right.target_id));
|
||||
allowed.sort((left, right) =>
|
||||
compareUnicodeCodePoints(left.target_id, right.target_id),
|
||||
);
|
||||
return {
|
||||
allowed,
|
||||
excluded: {
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
import { exclusionReasonForUrl } from "./CdpCaptureEventHelpers.js";
|
||||
import type { CdpCaptureCompleteness } from "./CdpCaptureCompleteness.js";
|
||||
import { createDocumentSvgAncestry, domSvgLink } from "./CdpCaptureDomSvg.js";
|
||||
import { compareUnicodeCodePoints } from "../domain/unicodeCodePointOrder.js";
|
||||
|
||||
export type CapturedResource = Omit<
|
||||
WebPageInspection["resources"][number],
|
||||
@@ -346,7 +347,7 @@ const accessibilityStates = (
|
||||
if (state !== undefined) states.set(name, state);
|
||||
}
|
||||
return [...states]
|
||||
.sort(([left], [right]) => left.localeCompare(right))
|
||||
.sort(([left], [right]) => compareUnicodeCodePoints(left, right))
|
||||
.map(([name, state]) => ({ name, value: state }));
|
||||
};
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
import { captureElectronScripts } from "./CdpElectronScripts.js";
|
||||
import { captureElectronWorkers } from "./CdpElectronWorkers.js";
|
||||
import { authorizedElectronFile } from "./ElectronFileScope.js";
|
||||
import { compareUnicodeCodePoints } from "../domain/unicodeCodePointOrder.js";
|
||||
|
||||
interface ElectronContext {
|
||||
readonly connection: CdpConnection;
|
||||
@@ -300,7 +301,7 @@ const captureResources = async (
|
||||
pending.push(...recordsValue(tree.childFrames));
|
||||
}
|
||||
return resources.sort((left, right) =>
|
||||
left.resource_key.localeCompare(right.resource_key),
|
||||
compareUnicodeCodePoints(left.resource_key, right.resource_key),
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ import { inspectCdpElectronPage } from "./CdpElectronInspection.js";
|
||||
import { authorizedElectronFile } from "./ElectronFileScope.js";
|
||||
|
||||
import { CDP_ELECTRON_PROVIDER_IDENTITY } from "./providerIdentities.js";
|
||||
import { compareUnicodeCodePoints } from "../domain/unicodeCodePointOrder.js";
|
||||
/** Passive Electron provider for local file pages exposed by loopback CDP. */
|
||||
export class CdpElectronProvider implements ElectronObservationPort {
|
||||
identity(): ProviderIdentity {
|
||||
@@ -73,7 +74,7 @@ export class CdpElectronProvider implements ElectronObservationPort {
|
||||
});
|
||||
}
|
||||
allowed.sort((left, right) =>
|
||||
left.target_id.localeCompare(right.target_id),
|
||||
compareUnicodeCodePoints(left.target_id, right.target_id),
|
||||
);
|
||||
return ok(
|
||||
electronTargetListSchema.parse({
|
||||
|
||||
@@ -9,6 +9,7 @@ import { CdpCaptureCompleteness } from "./CdpCaptureCompleteness.js";
|
||||
import { requiredScriptSource } from "./CdpCaptureValues.js";
|
||||
import type { ElectronScriptDraft } from "./CdpElectronScriptEvents.js";
|
||||
import { authorizedElectronFile } from "./ElectronFileScope.js";
|
||||
import { compareUnicodeCodePoints } from "../domain/unicodeCodePointOrder.js";
|
||||
|
||||
interface ElectronScriptCaptureInput {
|
||||
readonly connection: CdpConnection;
|
||||
@@ -65,7 +66,7 @@ export const captureElectronScripts = async (
|
||||
return {
|
||||
total,
|
||||
items: items.sort((left, right) =>
|
||||
left.script_key.localeCompare(right.script_key),
|
||||
compareUnicodeCodePoints(left.script_key, right.script_key),
|
||||
),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
import type { CdpEndpointTarget } from "./CdpEndpoint.js";
|
||||
import { optionalCdpCommand } from "./CdpOptionalCommand.js";
|
||||
import { authorizedElectronFile } from "./ElectronFileScope.js";
|
||||
import { compareUnicodeCodePoints } from "../domain/unicodeCodePointOrder.js";
|
||||
|
||||
/** Inventory local worker targets without attaching to them. */
|
||||
export const captureElectronWorkers = async (input: {
|
||||
@@ -70,7 +71,7 @@ export const captureElectronWorkers = async (input: {
|
||||
});
|
||||
}
|
||||
return workers.sort((left, right) =>
|
||||
left.target_id.localeCompare(right.target_id),
|
||||
compareUnicodeCodePoints(left.target_id, right.target_id),
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
} from "./CdpCaptureValues.js";
|
||||
import type { CaptureContext } from "./CdpPageCapture.js";
|
||||
import type { WebSourceMapRequest } from "./WebSourceMapFetcher.js";
|
||||
import { compareUnicodeCodePoints } from "../domain/unicodeCodePointOrder.js";
|
||||
|
||||
export interface CapturedScripts {
|
||||
readonly inventory: WebPageInspection["scripts"];
|
||||
@@ -131,11 +132,15 @@ const keyAndBuildInventory = (
|
||||
}));
|
||||
keyed.sort(
|
||||
(left, right) =>
|
||||
left.base.localeCompare(right.base) ||
|
||||
sourceDigest(left.item.source).localeCompare(
|
||||
compareUnicodeCodePoints(left.base, right.base) ||
|
||||
compareUnicodeCodePoints(
|
||||
sourceDigest(left.item.source),
|
||||
sourceDigest(right.item.source),
|
||||
) ||
|
||||
(left.item.frame_id ?? "").localeCompare(right.item.frame_id ?? ""),
|
||||
compareUnicodeCodePoints(
|
||||
left.item.frame_id ?? "",
|
||||
right.item.frame_id ?? "",
|
||||
),
|
||||
);
|
||||
const totals = new Map<string, number>();
|
||||
for (const { base } of keyed) totals.set(base, (totals.get(base) ?? 0) + 1);
|
||||
|
||||
@@ -7,7 +7,10 @@ import type {
|
||||
} from "../domain/webMcpDiscovery.js";
|
||||
import { jsonValueSchema } from "../domain/jsonValue.js";
|
||||
import { digestCanonicalValue } from "../domain/canonicalDigest.js";
|
||||
import { compositeKey } from "../domain/unicodeCodePointOrder.js";
|
||||
import {
|
||||
compareUnicodeCodePoints,
|
||||
compositeKey,
|
||||
} from "../domain/unicodeCodePointOrder.js";
|
||||
import type { CdpEndpointDiscovery, CdpEndpointTarget } from "./CdpEndpoint.js";
|
||||
import type { CdpConnection, CdpEvent } from "./CdpConnection.js";
|
||||
import { CdpCaptureCompleteness } from "./CdpCaptureCompleteness.js";
|
||||
@@ -521,7 +524,7 @@ const buildWebMcpResult = (options: WebMcpResultOptions): WebMcpDiscovery => {
|
||||
tools: {
|
||||
total: tools.size + completeness.droppedTotal,
|
||||
items: [...tools.values()].sort((left, right) =>
|
||||
left.tool_key.localeCompare(right.tool_key),
|
||||
compareUnicodeCodePoints(left.tool_key, right.tool_key),
|
||||
),
|
||||
},
|
||||
completeness: completeness.snapshot(),
|
||||
|
||||
@@ -6,9 +6,10 @@ import { z } from "zod";
|
||||
import { PROMPT_CONTRACTS } from "./contracts/promptContracts.js";
|
||||
import { TOOL_CONTRACTS } from "./contracts/toolContracts.js";
|
||||
import { CLI_COMMAND_NAMES } from "./cliCommandNames.js";
|
||||
import { compareUnicodeCodePoints } from "./domain/unicodeCodePointOrder.js";
|
||||
|
||||
const sortedToolContracts = [...TOOL_CONTRACTS].sort((left, right) =>
|
||||
left.name.localeCompare(right.name),
|
||||
compareUnicodeCodePoints(left.name, right.name),
|
||||
);
|
||||
|
||||
const createToolCatalog = () =>
|
||||
@@ -39,7 +40,7 @@ const promptCatalog = PROMPT_CONTRACTS.map((contract) => ({
|
||||
description: contract.description,
|
||||
arguments: contract.arguments,
|
||||
steps: contract.steps,
|
||||
})).sort((left, right) => left.name.localeCompare(right.name));
|
||||
})).sort((left, right) => compareUnicodeCodePoints(left.name, right.name));
|
||||
|
||||
const digest = (value: unknown): string => {
|
||||
const encoded = canonicalize(value);
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
import type { FirmwareRequest } from "../domain/firmware/firmwareAnalysis.js";
|
||||
import { readBoundedFileBytes } from "../process/BoundedFileBytes.js";
|
||||
import { FIRMWARE_LIMITS } from "./FirmwareRelease.js";
|
||||
import { compareUnicodeCodePoints } from "../domain/unicodeCodePointOrder.js";
|
||||
|
||||
/** One regular file or unpublished filesystem entry in a private extraction. */
|
||||
export interface FirmwareEntry {
|
||||
@@ -261,7 +262,9 @@ export const inventoryFirmwareOutput = async (
|
||||
});
|
||||
}
|
||||
}
|
||||
return entries.sort((a, b) => a.relativePath.localeCompare(b.relativePath));
|
||||
return entries.sort((a, b) =>
|
||||
compareUnicodeCodePoints(a.relativePath, b.relativePath),
|
||||
);
|
||||
};
|
||||
|
||||
/** Bind producer paths to the selected input or private extraction tree. */
|
||||
|
||||
@@ -6,6 +6,7 @@ import { readWindowsPrivateRuntimeFile } from "../windows/WindowsPrivateRuntime.
|
||||
|
||||
import { err, ok, type Result } from "../domain/result.js";
|
||||
import { GhidraSessionError } from "./GhidraSessionError.js";
|
||||
import { compareUnicodeCodePoints } from "../domain/unicodeCodePointOrder.js";
|
||||
|
||||
/** Local bridge transports used by the current Ghidra session. */
|
||||
export type GhidraTransportKind = "unix-socket" | "authenticated-loopback-tcp";
|
||||
@@ -147,7 +148,7 @@ const hasExactKeys = (
|
||||
expected: readonly string[],
|
||||
): value is Record<string, unknown> => {
|
||||
const keys = Object.keys(value).sort((left, right) =>
|
||||
left.localeCompare(right),
|
||||
compareUnicodeCodePoints(left, right),
|
||||
);
|
||||
return (
|
||||
keys.length === expected.length &&
|
||||
|
||||
@@ -13,6 +13,7 @@ import { parseDyldSymbols } from "./parsers/dyldInfo.js";
|
||||
import { parseLipoArchitectures } from "./parsers/lipo.js";
|
||||
import { withoutEchoedPathHeader } from "./parsers/echoedPath.js";
|
||||
import { parseOtoolLoadCommands } from "./parsers/otool.js";
|
||||
import { compareUnicodeCodePoints } from "../domain/unicodeCodePointOrder.js";
|
||||
|
||||
interface NativeMachoObservation {
|
||||
readonly result: JsonValue;
|
||||
@@ -270,7 +271,7 @@ const uniqueSymbols = <Value extends { readonly name: string }>(
|
||||
for (const item of items)
|
||||
if (!unique.has(item.name)) unique.set(item.name, item);
|
||||
return [...unique.values()].sort((left, right) =>
|
||||
left.name.localeCompare(right.name),
|
||||
compareUnicodeCodePoints(left.name, right.name),
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
import { launcherIdentityFailure } from "./ProcessOwnershipIdentity.js";
|
||||
import { descendantsOf, liveProcesses } from "./ProcessOwnershipProcessTree.js";
|
||||
import { readProcessRunTokens } from "./ProcessRunTokenObservations.js";
|
||||
import { compareUnicodeCodePoints } from "../domain/unicodeCodePointOrder.js";
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
|
||||
@@ -613,7 +614,7 @@ const cleanupValidationFailure = (
|
||||
counts.set(category, (counts.get(category) ?? 0) + 1);
|
||||
}
|
||||
const breakdown = [...counts]
|
||||
.sort(([left], [right]) => left.localeCompare(right))
|
||||
.sort(([left], [right]) => compareUnicodeCodePoints(left, right))
|
||||
.map(([category, count]) => `${category}=${String(count)}`)
|
||||
.join(", ");
|
||||
const candidates = unreadable
|
||||
@@ -621,7 +622,7 @@ const cleanupValidationFailure = (
|
||||
({ pid, diagnostic }) =>
|
||||
`${String(pid)}=${sanitizedTokenReadFailure(diagnostic)}`,
|
||||
)
|
||||
.sort((left, right) => left.localeCompare(right));
|
||||
.sort((left, right) => compareUnicodeCodePoints(left, right));
|
||||
return {
|
||||
cleaned: false,
|
||||
reason: `process ownership token could not be read for ${String(unreadable.length)} live process(es): ${breakdown}; live candidates ${candidates.join(", ")}`,
|
||||
|
||||
@@ -5,6 +5,7 @@ import type { AnalysisError } from "../../domain/analysisErrorBase.js";
|
||||
import { err, ok, type Result } from "../../domain/result.js";
|
||||
import { recordDerivedEvidence } from "../recordDerivedEvidence.js";
|
||||
import type { WorkflowUnknownInput } from "./types.js";
|
||||
import { compareUnicodeCodePoints } from "../../domain/unicodeCodePointOrder.js";
|
||||
|
||||
export const isIncomplete = (status: string): boolean =>
|
||||
status === "unknown" || status === "truncated";
|
||||
@@ -21,7 +22,9 @@ export const functionEvidenceIds = (records: readonly Evidence[]): string[] =>
|
||||
uniqueIds(records.map(({ evidence_id }) => evidence_id));
|
||||
|
||||
const uniqueIds = (ids: readonly string[]): string[] =>
|
||||
[...new Set(ids)].sort((left, right) => left.localeCompare(right));
|
||||
[...new Set(ids)].sort((left, right) =>
|
||||
compareUnicodeCodePoints(left, right),
|
||||
);
|
||||
|
||||
export const evidenceClosure = (
|
||||
session: BinarySessionPort,
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import { mkdir, writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { expect, it } from "vitest";
|
||||
|
||||
import { readFirmwareReport } from "../../../src/firmware/FirmwareFiles.js";
|
||||
import {
|
||||
inventoryFirmwareOutput,
|
||||
readFirmwareReport,
|
||||
} from "../../../src/firmware/FirmwareFiles.js";
|
||||
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
|
||||
it.each([
|
||||
@@ -41,3 +44,24 @@ it("preserves valid Unicode and a literal replacement character in producer JSON
|
||||
report,
|
||||
);
|
||||
});
|
||||
|
||||
it("orders extracted firmware paths by code point independently of host locale", async () => {
|
||||
const root = await createTestTempDirectory("rea-firmware-inventory-");
|
||||
await mkdir(join(root, "Lib"));
|
||||
for (const path of ["a.bin", "z.bin", "\u00e4.bin", "B.bin", "Lib/x.bin"])
|
||||
await writeFile(join(root, path), "");
|
||||
const entries = await inventoryFirmwareOutput(
|
||||
root,
|
||||
1024,
|
||||
16,
|
||||
"inspect_firmware_regions",
|
||||
);
|
||||
// Locale collation would interleave case and place "ä" by language rules.
|
||||
expect(entries.map(({ relativePath }) => relativePath)).toEqual([
|
||||
"B.bin",
|
||||
"Lib/x.bin",
|
||||
"a.bin",
|
||||
"z.bin",
|
||||
"\u00e4.bin",
|
||||
]);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user