mirror of
https://github.com/morluto/rea.git
synced 2026-10-26 17:35:11 +00:00
fix(flutter): preserve short GNU build IDs through public workflows
Reuse the reader correction from PR #6705, and share the byte projection budget with output validation. Verify actual APK parsing through compiled CLI and stdio MCP at 0, 1, 2, 3, 64 and 65 descriptor bytes. Co-authored-by: Ezra <64943463+ezrabrilliant@users.noreply.github.com> Fixes #6697
This commit is contained in:
1 parent
866c72c907
commit
678c4cfa68
4 files changed
+57
-6
No files matched your search
@@ -108,6 +108,8 @@ server; older servers keep the complete request after schema discovery.
|
||||
`build_id_candidates` preserves distinct IDs. `build_id` is populated only
|
||||
when all declared note ranges were examined and one ID was found;
|
||||
conflicting IDs or malformed/bounded note metadata make coverage partial.
|
||||
Nonempty GNU build-ID descriptors up to 64 bytes are retained as lowercase
|
||||
hexadecimal, including IDs shorter than four bytes.
|
||||
Matching bytes outside declared note ranges cannot supply a build ID.
|
||||
- Labeled strings must fit their complete grammar. `dart_version_capped`
|
||||
and `toolchain_capped` independently report labels omitted by grammar or
|
||||
|
||||
@@ -5,6 +5,14 @@ import { digestSchema } from "../digests.js";
|
||||
/** Full logical ZIP path budget, enforced by Flutter library inventory. */
|
||||
export const MAX_FLUTTER_LIBRARY_PATH_BYTES = 1024;
|
||||
|
||||
/** Retained GNU build-ID descriptor budget, in bytes. */
|
||||
export const MAX_FLUTTER_BUILD_ID_BYTES = 64;
|
||||
const gnuBuildIdSchema = z
|
||||
.string()
|
||||
.min(2)
|
||||
.max(MAX_FLUTTER_BUILD_ID_BYTES * 2)
|
||||
.regex(/^(?:[0-9a-f]{2})+$/u);
|
||||
|
||||
/**
|
||||
* A libapp label can occupy every remaining byte of an admitted path. Its
|
||||
* Unicode character count cannot exceed that byte count. The query permits
|
||||
@@ -118,15 +126,13 @@ export const flutterResultSchemas = {
|
||||
present: z.boolean(),
|
||||
bytes: z.int().nonnegative().nullable(),
|
||||
sha256: digestSchema.nullable(),
|
||||
build_id: z
|
||||
.string()
|
||||
.regex(/^[0-9a-f]{8,128}$/u)
|
||||
build_id: gnuBuildIdSchema
|
||||
.nullable()
|
||||
.describe(
|
||||
"The single GNU build ID when ELF note coverage is complete and unambiguous",
|
||||
),
|
||||
build_id_candidates: z
|
||||
.array(z.string().regex(/^[0-9a-f]{8,128}$/u))
|
||||
.array(gnuBuildIdSchema)
|
||||
.max(64)
|
||||
.describe(
|
||||
"Distinct GNU build IDs observed in ELF-owned note ranges; conflicting IDs remain explicit",
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { MAX_FLUTTER_BUILD_ID_BYTES } from "../domain/flutter/flutterBuildAnalysis.js";
|
||||
import type { FileHandle } from "node:fs/promises";
|
||||
import { setImmediate as yieldToEventLoop } from "node:timers/promises";
|
||||
import {
|
||||
@@ -167,9 +168,9 @@ export const readElfBuildIds = async (
|
||||
type === 3 &&
|
||||
notes.subarray(at + 12, at + 16).equals(Buffer.from("GNU\0"))
|
||||
) {
|
||||
if (descsz < 4 || descsz > 64)
|
||||
if (descsz < 1 || descsz > MAX_FLUTTER_BUILD_ID_BYTES)
|
||||
return failure(
|
||||
"GNU build-id descriptor exceeds the 4–64 byte projection",
|
||||
`GNU build-id descriptor is outside the 1–${MAX_FLUTTER_BUILD_ID_BYTES} byte projection`,
|
||||
);
|
||||
const candidate = notes.subarray(descriptor, end).toString("hex");
|
||||
if (!candidates.has(candidate) && candidates.size === MAX_BUILD_IDS)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { gnuBuildIdNote } from "../../src/native/elf/ElfNotes.fixture.js";
|
||||
import { createHash } from "node:crypto";
|
||||
import { expect } from "vitest";
|
||||
import { toolContract } from "../../src/contracts/toolContracts.js";
|
||||
@@ -245,3 +246,44 @@ cliTest(
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
cliTest(
|
||||
"CLI and stdio MCP preserve byte-sized GNU build IDs and descriptor coverage",
|
||||
async ({ cli }) => {
|
||||
const { call } = await connectLocalToolsMcp();
|
||||
for (const bytes of [0, 1, 2, 3, 64, 65]) {
|
||||
const hex = "ab".repeat(bytes);
|
||||
const path = await createFlutterApk(`engine-${bytes}.apk`, [
|
||||
{ path: "lib/arm64-v8a/libapp.so", bytes: aotFixtureImage() },
|
||||
{
|
||||
path: "lib/arm64-v8a/libflutter.so",
|
||||
bytes: engineFixtureImage({ notes: gnuBuildIdNote(hex) }),
|
||||
},
|
||||
]);
|
||||
const output = await cli.run({
|
||||
arguments: ["identify-flutter-build", path, "--json"],
|
||||
});
|
||||
expect(output.exitCode, output.stderr).toBe(0);
|
||||
const cliEvidence = parseEvidence(output.json);
|
||||
const response = await call("identify_flutter_build", { path });
|
||||
expect(response.isError, JSON.stringify(response)).not.toBe(true);
|
||||
const mcpEvidence = parseEvidence(response.structuredContent);
|
||||
expect(mcpEvidence.normalized_result).toEqual(
|
||||
cliEvidence.normalized_result,
|
||||
);
|
||||
const result = flutterResultSchemas.identify_flutter_build.parse(
|
||||
cliEvidence.normalized_result,
|
||||
);
|
||||
const admitted = bytes > 0 && bytes <= 64;
|
||||
expect(result.abis[0]?.libflutter).toMatchObject({
|
||||
build_id: admitted ? hex : null,
|
||||
build_id_candidates: admitted ? [hex] : [],
|
||||
});
|
||||
expect(result.coverage).toBe(admitted ? "complete" : "partial");
|
||||
if (!admitted)
|
||||
expect(cliEvidence.limitations.join("\n")).toContain(
|
||||
"1–64 byte projection",
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
Reference in new issue
Block a user