Files
rea/src/flutter/FlutterPayloadScan.test.ts
T
Dragolone 5278835b53 fix(flutter): find snapshot headers that a read chunk boundary splits (#2804)
* fix(flutter): find snapshot headers that a read chunk boundary splits

Within one chunk the snapshot hash scanner resumes four bytes after each
magic, but across chunks it resumed after the whole 52-byte record and
dropped the carried bytes. A magic starting 49-51 bytes after a rejected
(non-hex) record was therefore lost when its four bytes straddled a read
boundary, so the result depended on chunking and a conflicting hash could
be omitted while coverage stayed complete. Resume after the magic in both
cases.

* refactor(flutter): drop the snapshot scanner cursor the fix superseded

After a complete record the carried tail is shorter than a magic, and a
pending magic has not been processed, so every push can rescan its carried
bytes from the start without counting a record twice. Remove the absolute
cursor and base offset, and cover sections 49-51 bytes after a rejected
record with two-chunk and byte-at-a-time feeds.
2026-10-13 22:14:07 +08:00

241 lines
8.1 KiB
TypeScript

import { describe, expect, it } from "vitest";
import {
LabeledStringScanner,
SnapshotHashScanner,
} from "./FlutterPayloadScan.js";
/** Real payload prefix from Flutter Gallery 2.9.2's arm64 libapp.so. */
const realSnapshotSection = (hash: string): Uint8Array =>
Uint8Array.from([
0xf5,
0xf5,
0xdc,
0xdc,
0x49,
0x0d,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x03,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
...Array.from(hash, (character) => character.charCodeAt(0)),
]);
describe("SnapshotHashScanner", () => {
it("reads the hash from real vm and isolate sections that agree", () => {
const scanner = new SnapshotHashScanner();
scanner.push(realSnapshotSection("65817c30a78bb44c3dc3771876b6010a"));
scanner.push(Uint8Array.from([0, 0, 0]));
scanner.push(realSnapshotSection("65817c30a78bb44c3dc3771876b6010a"));
expect(scanner.result()).toEqual({
capped: false,
sources: 2,
candidates: ["65817c30a78bb44c3dc3771876b6010a"],
});
});
it("keeps both hashes when sections disagree", () => {
const scanner = new SnapshotHashScanner();
scanner.push(realSnapshotSection("11111111111111111111111111111111"));
scanner.push(realSnapshotSection("22222222222222222222222222222222"));
expect(scanner.result()).toEqual({
capped: false,
sources: 2,
candidates: [
"11111111111111111111111111111111",
"22222222222222222222222222222222",
],
});
});
it("survives chunk boundaries that split the magic and hash", () => {
const scanner = new SnapshotHashScanner();
const payload = realSnapshotSection("65817c30a78bb44c3dc3771876b6010a");
for (let offset = 0; offset < payload.length; offset += 7)
scanner.push(payload.subarray(offset, offset + 7));
expect(scanner.result()).toEqual({
capped: false,
sources: 1,
candidates: ["65817c30a78bb44c3dc3771876b6010a"],
});
});
it.each([49, 50, 51])(
"finds a section %i bytes after a rejected record at any chunking",
(start) => {
// A stray magic whose hash field is not hex, then a real section that
// starts inside the stray record's magic-to-hash span.
const payload = new Uint8Array(start + 52);
payload.set([0xf5, 0xf5, 0xdc, 0xdc], 0);
payload.set(
realSnapshotSection("65817c30a78bb44c3dc3771876b6010a"),
start,
);
const whole = new SnapshotHashScanner();
whole.push(payload);
expect(whole.result()).toEqual({
capped: false,
sources: 1,
candidates: ["65817c30a78bb44c3dc3771876b6010a"],
});
for (let split = 1; split < payload.length; split += 1) {
const scanner = new SnapshotHashScanner();
scanner.push(payload.subarray(0, split));
scanner.push(payload.subarray(split));
expect(scanner.result(), `split at ${split}`).toEqual(whole.result());
}
const bytewise = new SnapshotHashScanner();
for (let offset = 0; offset < payload.length; offset += 1)
bytewise.push(payload.subarray(offset, offset + 1));
expect(bytewise.result()).toEqual(whole.result());
},
);
it("ignores a magic without a following hash", () => {
const scanner = new SnapshotHashScanner();
scanner.push(Uint8Array.from([0xf5, 0xf5, 0xdc, 0xdc, 0xff, 0xff, 0xff]));
expect(scanner.result()).toEqual({
capped: false,
sources: 0,
candidates: [],
});
});
});
it("distinguishes a full candidate set from omitted conflicts", () => {
const scanner = new SnapshotHashScanner();
for (let index = 0; index < 64; index += 1)
scanner.push(realSnapshotSection(index.toString(16).padStart(32, "0")));
expect(scanner.result().capped).toBe(false);
scanner.push(realSnapshotSection("0".repeat(32)));
expect(scanner.result().capped).toBe(false);
scanner.push(realSnapshotSection("f".repeat(32)));
expect(scanner.result()).toMatchObject({ sources: 66, capped: true });
expect(scanner.result().candidates).toHaveLength(64);
});
describe("LabeledStringScanner", () => {
const assertSingleToolchainLine = (line: string): void => {
const scanner = new LabeledStringScanner();
scanner.push(
Uint8Array.from(
Array.from(`\x00${line}\x00`, (character) => character.charCodeAt(0)),
),
);
expect(scanner.result().toolchainLines).toEqual([line]);
};
it("collects a full clang line within the 500-character bound", () => {
assertSingleToolchainLine(
"Android (5900059 based on r365631c1) clang version 9.0.8 (https://android.googlesource.com/toolchain/llvm-project 207d7abc1a2abf3ef8d4301736d6a7ebc224a290) (based on LLVM 9.0.8svn)",
);
});
it("reads a labeled Dart VM version string when a build carries one", () => {
const scanner = new LabeledStringScanner();
scanner.push(
Uint8Array.from(
Array.from(
"\x00Dart VM version: 3.1.0 (stable) somewhere\x00",
(character) => character.charCodeAt(0),
),
),
);
expect(scanner.result().dartVersion).toBe(
"Dart VM version: 3.1.0 (stable) somewhere",
);
});
it("collects the labeled Android clang line", () => {
assertSingleToolchainLine(
"Android (5900059 based on r365631c) clang version 9.0.8 (https://android.googlesource.com/toolchain/llvm-project 207d7abc) (based on LLVM 9.0.8svn)",
);
});
it("does not claim a toolchain cap for unrelated overlong strings", () => {
const scanner = new LabeledStringScanner();
const license = `Apache License Version 2.0 ${"x".repeat(700)}`;
scanner.push(
Uint8Array.from(
Array.from(`\x00${license}\x00`, (character) =>
character.charCodeAt(0),
),
),
);
const result = scanner.result();
expect(result.toolchainCapped).toBe(false);
expect(result.toolchainLines).toEqual([]);
});
it("claims the cap only for an overlong run shaped like a toolchain line", () => {
const overlongToolchain = `Android (${"r".repeat(700)}) clang version 9`;
for (const [text, toolchainCapped, dartVersionCapped] of [
[overlongToolchain, true, false],
[`Dart VM version: ${"3".repeat(700)}`, false, true],
[`based on LLVM 9 ${"x".repeat(700)}`, true, false],
] as const) {
const scanner = new LabeledStringScanner();
scanner.push(
Uint8Array.from(
Array.from(`\x00${text}\x00`, (character) => character.charCodeAt(0)),
),
);
expect(scanner.result()).toMatchObject({
toolchainCapped,
dartVersionCapped,
dartVersion: null,
toolchainLines: [],
});
}
});
it.each([
{ label: "Dart", line: `Dart VM version:${"x".repeat(200)}`, dart: true },
{
label: "Android clang",
line: `Android (${"r".repeat(80)}) clang version ${"x".repeat(500)}`,
dart: false,
},
{ label: "LLVM", line: `based on LLVM 9${"x".repeat(80)}`, dart: false },
])(
"requires the complete $label run at its own grammar boundary",
({ line, dart }) => {
for (const suffix of ["", "x", "x".repeat(1000)]) {
const payload = Buffer.from(`${line}${suffix}\x00`);
for (const chunkSize of [1, 7, payload.length]) {
const scanner = new LabeledStringScanner();
for (let offset = 0; offset < payload.length; offset += chunkSize)
scanner.push(payload.subarray(offset, offset + chunkSize));
const valid = suffix === "";
expect(scanner.result()).toEqual({
dartVersion: dart && valid ? line : null,
dartVersionCapped: dart && !valid,
toolchainLines: !dart && valid ? [line] : [],
toolchainCapped: !dart && !valid,
});
}
}
},
);
it("preserves whitespace and separates distinct Dart-label omissions", () => {
assertSingleToolchainLine("based on LLVM 9 ");
const scanner = new LabeledStringScanner();
scanner.push(Buffer.from("Dart VM version: 3.1\0Dart VM version: 3.2\0"));
expect(scanner.result()).toMatchObject({
dartVersion: "Dart VM version: 3.1",
dartVersionCapped: true,
toolchainCapped: false,
});
});
});