mirror of
https://github.com/morluto/rea
synced 2026-10-26 14:25:49 -04:00
5278835b53
* 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.
241 lines
8.1 KiB
TypeScript
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,
|
|
});
|
|
});
|
|
});
|