From f6556ad2bc102d80223f5b434136f546dbd2bbab Mon Sep 17 00:00:00 2001 From: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com> Date: Mon, 12 Oct 2026 23:44:09 -0700 Subject: [PATCH] fix(pcap): order evidence ties by code point instead of host locale (#2509) The pcap summary sorted equal-count endpoints and protocol stacks with String#localeCompare, and Flutter build inspection sorted APK ABI directory names the same way. localeCompare follows the host's ICU locale, so the same capture produced differently ordered Evidence on different hosts: under da_DK "aarp" sorts after "zz", under lt_LT "syslog" sorts before "ssdp", and every locale puts "fe80::1" before "fe80:1::1". Use the repository's compareUnicodeCodePoints, as the other Evidence producers already do. Co-authored-by: Claude Opus 5.5 --- src/flutter/FlutterBuildProvider.ts | 5 +++-- src/pcap/PcapOutput.test.ts | 18 ++++++++++++++++++ src/pcap/PcapOutput.ts | 5 +++-- 3 files changed, 24 insertions(+), 4 deletions(-) diff --git a/src/flutter/FlutterBuildProvider.ts b/src/flutter/FlutterBuildProvider.ts index 2c088b2c9..9a6e6fe50 100644 --- a/src/flutter/FlutterBuildProvider.ts +++ b/src/flutter/FlutterBuildProvider.ts @@ -23,6 +23,7 @@ import { AnalysisUnsupportedTargetError, } from "../domain/analysisErrorCore.js"; import { AnalysisProtocolError } from "../domain/analysisErrorCore.js"; +import { compareUnicodeCodePoints } from "../domain/unicodeCodePointOrder.js"; import { AnalysisError } from "../domain/analysisErrorBase.js"; import { err, ok, type Result } from "../domain/result.js"; import { @@ -759,7 +760,7 @@ export class FlutterBuildProvider { toolchain_capped: value.libflutter.toolchainCapped, }, })) - .sort((left, right) => left.abi.localeCompare(right.abi)); + .sort((left, right) => compareUnicodeCodePoints(left.abi, right.abi)); const flutterDetected = abis.some((entry) => entry.libapp.present); const partial = abis.some( @@ -871,7 +872,7 @@ export class FlutterBuildProvider { (left, right) => (preference.indexOf(left.abi) + 1 || preference.length + 1) - (preference.indexOf(right.abi) + 1 || preference.length + 1) || - left.abi.localeCompare(right.abi), + compareUnicodeCodePoints(left.abi, right.abi), )[0]! : candidates.find((candidate) => candidate.abi === abi); if (selected === undefined) diff --git a/src/pcap/PcapOutput.test.ts b/src/pcap/PcapOutput.test.ts index 6cfa6b0d3..c0e158438 100644 --- a/src/pcap/PcapOutput.test.ts +++ b/src/pcap/PcapOutput.test.ts @@ -83,6 +83,24 @@ it("counts IPv6 endpoints once per packet and preserves every examined fact", as expect(result.partial).toBe(false); }); +it("orders equal-count endpoints and protocols by code point, not host locale", async () => { + // Locale collation puts "fe80::1" first and, under da-DK, "aarp" after "zz". + const result = await parseTsharkJsonOutput( + capture([ + { "ipv6.src": ["fe80::1"], "frame.protocols": ["eth:llc:zz"] }, + { "ipv6.src": ["fe80:1::1"], "frame.protocols": ["eth:llc:aarp"] }, + ]), + ); + expect(result.endpoints.map(({ address }) => address)).toEqual([ + "fe80:1::1", + "fe80::1", + ]); + expect(result.protocolDistribution.map(({ protocol }) => protocol)).toEqual([ + "eth:llc:aarp", + "eth:llc:zz", + ]); +}); + it("preserves all DNS addresses and their exact examined count", async () => { const result = await parseTsharkJsonOutput( capture( diff --git a/src/pcap/PcapOutput.ts b/src/pcap/PcapOutput.ts index 95cb81926..b14d7ba3c 100644 --- a/src/pcap/PcapOutput.ts +++ b/src/pcap/PcapOutput.ts @@ -4,6 +4,7 @@ import { AnalysisResourceConstraintError, } from "../domain/analysisErrorCore.js"; import { parseRecordedTransportUrl } from "../domain/recordedTransportUrl.js"; +import { compareUnicodeCodePoints } from "../domain/unicodeCodePointOrder.js"; const OPERATION = "inspect_pcap_capture"; /** Bounds JSON decoding plus its expanded object graph before JSON.parse. */ @@ -284,7 +285,7 @@ export const parseTsharkJsonOutput = async ( signal?.throwIfAborted(); return { endpoints: [...endpoints] - .sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])) + .sort((a, b) => b[1] - a[1] || compareUnicodeCodePoints(a[0], b[0])) .map(([address, packet_count]) => ({ address, port: null, @@ -300,7 +301,7 @@ export const parseTsharkJsonOutput = async ( tlsSniValues: [...sni], tlsSniCount: sni.size, protocolDistribution: [...protocols] - .sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])) + .sort((a, b) => b[1] - a[1] || compareUnicodeCodePoints(a[0], b[0])) .map(([protocol, packet_count]) => ({ protocol, packet_count })), protocolCount: protocols.size, httpRequestUris: [...uris],