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 <noreply@anthropic.com>
This commit is contained in:
Kaoru0822 2026-10-12 23:44:09 -07:00 committed by GitHub
parent d73ed6fce2
commit f6556ad2bc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 24 additions and 4 deletions

View File

@ -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)

View File

@ -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(

View File

@ -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],