mirror of
https://github.com/morluto/rea.git
synced 2026-11-14 10:43:30 +00:00
fix(adb): reject unrepresentable activity arguments (#9918)
* fix(adb): reject unrepresentable activity arguments * test(adb): lock complete activity component boundaries The CLI and advertised schema still admitted a slash-free 512-character component after admission required a nonempty package and class. Keep that length sample rejected, admit a pattern-valid 512-character component, and assert the typed field error. * style(docs): format NTFS verification tables oxfmt rejects the tables merged from main. The alignment is formatting only, so static checks can run on this branch. * test(adb): use a well-formed component for the length constraint An over-long run of letters also fails the package/activity pattern, so the selector check reported both issues. The length fixture now has a valid component shape. --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com>
This commit is contained in:
1 parent
13abfdadff
commit
eff195d6aa
5 files changed
+130
-25
No files matched your search
@@ -50,6 +50,41 @@ it.each([
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
[{ component: "example.app" }, ["component"]],
|
||||
[{ component: "example.app/" }, ["component"]],
|
||||
[
|
||||
{ extras: [{ key: "count", type: "int", value: "2147483648" }] },
|
||||
["extras", 0, "value"],
|
||||
],
|
||||
[
|
||||
{ extras: [{ key: "count", type: "int", value: "-2147483649" }] },
|
||||
["extras", 0, "value"],
|
||||
],
|
||||
[
|
||||
{ extras: [{ key: "count", type: "long", value: "9223372036854775808" }] },
|
||||
["extras", 0, "value"],
|
||||
],
|
||||
[
|
||||
{ extras: [{ key: "count", type: "long", value: "-9223372036854775809" }] },
|
||||
["extras", 0, "value"],
|
||||
],
|
||||
] as const)(
|
||||
"rejects an unrepresentable activity argument before any effect: %j",
|
||||
async (input, path) => {
|
||||
const service = new AdbDeviceAnalysisService(refusedPort);
|
||||
const result = await service.execute("start_adb_activity", {
|
||||
serial: "fixture",
|
||||
action: "example.TEST",
|
||||
...input,
|
||||
});
|
||||
expect(result).toMatchObject({
|
||||
ok: false,
|
||||
error: { _tag: "AnalysisInputError", issues: [{ path }] },
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it("rejects an unknown operation before any effect", async () => {
|
||||
const service = new AdbDeviceAnalysisService(refusedPort);
|
||||
const result = await service.execute("unknown_adb_operation" as never, {});
|
||||
|
||||
@@ -2,11 +2,11 @@ import { expect, it } from "vitest";
|
||||
|
||||
import { adbInputSchemas } from "./adbDeviceAnalysis.js";
|
||||
|
||||
it("preserves ADB key, action and component admission when making advertised patterns portable", () => {
|
||||
it("matches portable patterns for ADB keys, actions and complete components", () => {
|
||||
const serial = "emulator-5554";
|
||||
const variants = [
|
||||
{
|
||||
previous: /^[A-Za-z0-9._-]+$/u,
|
||||
expectedPattern: /^[A-Za-z0-9._-]+$/u,
|
||||
max: 256,
|
||||
accepts: (key: string) =>
|
||||
adbInputSchemas.read_adb_setting.safeParse({
|
||||
@@ -16,7 +16,7 @@ it("preserves ADB key, action and component admission when making advertised pat
|
||||
}).success,
|
||||
},
|
||||
{
|
||||
previous: /^[A-Za-z0-9._-]+$/u,
|
||||
expectedPattern: /^[A-Za-z0-9._-]+$/u,
|
||||
max: 256,
|
||||
accepts: (key: string) =>
|
||||
adbInputSchemas.start_adb_activity.safeParse({
|
||||
@@ -26,14 +26,14 @@ it("preserves ADB key, action and component admission when making advertised pat
|
||||
}).success,
|
||||
},
|
||||
{
|
||||
previous: /^[A-Za-z][A-Za-z0-9._-]*$/u,
|
||||
expectedPattern: /^[A-Za-z][A-Za-z0-9._-]*$/u,
|
||||
max: 256,
|
||||
accepts: (action: string) =>
|
||||
adbInputSchemas.start_adb_activity.safeParse({ serial, action })
|
||||
.success,
|
||||
},
|
||||
{
|
||||
previous: /^[A-Za-z0-9._-]+(?:\/[A-Za-z0-9._$-]*)?$/u,
|
||||
expectedPattern: /^[A-Za-z0-9._-]+\/[A-Za-z0-9._$-]+$/u,
|
||||
max: 512,
|
||||
accepts: (component: string) =>
|
||||
adbInputSchemas.start_adb_activity.safeParse({
|
||||
@@ -53,6 +53,7 @@ it("preserves ADB key, action and component admission when making advertised pat
|
||||
"android.intent.action.VIEW",
|
||||
"com.example/.MainActivity",
|
||||
"com.example/.Outer$InnerActivity",
|
||||
"com.example/com.example.Outer$InnerActivity",
|
||||
"com.example.Outer$InnerActivity",
|
||||
"com.example/",
|
||||
"com.example/a/b",
|
||||
@@ -75,11 +76,11 @@ it("preserves ADB key, action and component admission when making advertised pat
|
||||
for (const sample of samples)
|
||||
expect(
|
||||
variant.accepts(sample),
|
||||
JSON.stringify({ pattern: variant.previous.source, sample }),
|
||||
JSON.stringify({ pattern: variant.expectedPattern.source, sample }),
|
||||
).toBe(
|
||||
sample.length > 0 &&
|
||||
sample.length <= variant.max &&
|
||||
variant.previous.test(sample),
|
||||
variant.expectedPattern.test(sample),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -97,6 +98,9 @@ it("admits empty string extras and validates typed extra text at domain admissio
|
||||
const accepted = [
|
||||
{ type: "boolean", value: "false" },
|
||||
{ type: "int", value: "-2147483648" },
|
||||
{ type: "int", value: "0002147483647" },
|
||||
{ type: "long", value: "-0009223372036854775808" },
|
||||
{ type: "long", value: "9223372036854775807" },
|
||||
{ type: "long", value: "9007199254740993" },
|
||||
{ type: "float", value: "-1.25" },
|
||||
{ type: "float", value: "1e-3" },
|
||||
@@ -106,14 +110,13 @@ it("admits empty string extras and validates typed extra text at domain admissio
|
||||
{ type: "float", value: "0x1.8p1" },
|
||||
{ type: "float", value: "NaN" },
|
||||
] as const;
|
||||
for (const { type, value } of accepted)
|
||||
for (const { type, value } of accepted) {
|
||||
const extras = [{ key: "value", type, value }];
|
||||
expect(
|
||||
adbInputSchemas.start_adb_activity.safeParse({
|
||||
...input,
|
||||
extras: [{ key: "value", type, value }],
|
||||
}).success,
|
||||
adbInputSchemas.start_adb_activity.parse({ ...input, extras }).extras,
|
||||
`${type}: ${value}`,
|
||||
).toBe(true);
|
||||
).toEqual(extras);
|
||||
}
|
||||
const rejected = [
|
||||
{ type: "boolean", value: "True" },
|
||||
{ type: "int", value: "1.5" },
|
||||
|
||||
@@ -54,11 +54,29 @@ const keyPattern = /^[A-Za-z0-9._\x2d]+$/u;
|
||||
const activityExtraKeySchema = keyStringSchema
|
||||
.regex(keyPattern, "Extra keys are identifiers")
|
||||
.describe("Extra key");
|
||||
const MAX_ADB_ACTIVITY_EXTRA_VALUE_LENGTH = 2048;
|
||||
const activityExtraStringValueSchema = z
|
||||
.string()
|
||||
.max(2048)
|
||||
.max(MAX_ADB_ACTIVITY_EXTRA_VALUE_LENGTH)
|
||||
.regex(/^[^\x00]*$/u, "Extra values cannot contain NUL");
|
||||
const activityExtraNonEmptyValueSchema = activityExtraStringValueSchema.min(1);
|
||||
/** Check Java int/long range without rounding or changing the caller's text. */
|
||||
const javaDecimalExtraValueSchema = (bits: 32 | 64) => {
|
||||
const upperBound = 1n << BigInt(bits - 1);
|
||||
const range = `signed ${String(bits)}-bit`;
|
||||
return z
|
||||
.string()
|
||||
.max(MAX_ADB_ACTIVITY_EXTRA_VALUE_LENGTH, { abort: true })
|
||||
.regex(/^-?\d+$/u, {
|
||||
message: "Integer extra values must be signed decimal text",
|
||||
abort: true,
|
||||
})
|
||||
.refine((value) => {
|
||||
const decimal = BigInt(value);
|
||||
return decimal >= -upperBound && decimal < upperBound;
|
||||
}, `Integer extra value is outside the ${range} range`)
|
||||
.describe(`Integer extra value as ${range} decimal text`);
|
||||
};
|
||||
/** Maximum typed extras admitted for one Android activity request. */
|
||||
export const MAX_ADB_ACTIVITY_EXTRAS = 16;
|
||||
// Android's `am start --ef` parses this text with Float.valueOf. Keep the
|
||||
@@ -83,16 +101,12 @@ const activityExtraSchema = z.discriminatedUnion("type", [
|
||||
z.strictObject({
|
||||
key: activityExtraKeySchema,
|
||||
type: z.literal("int").describe("Integer extra value type"),
|
||||
value: activityExtraNonEmptyValueSchema
|
||||
.regex(/^-?\d+$/u, "Integer extra values must be an integer")
|
||||
.describe("Integer extra value as signed decimal text"),
|
||||
value: javaDecimalExtraValueSchema(32),
|
||||
}),
|
||||
z.strictObject({
|
||||
key: activityExtraKeySchema,
|
||||
type: z.literal("long").describe("Long extra value type"),
|
||||
value: activityExtraNonEmptyValueSchema
|
||||
.regex(/^-?\d+$/u, "Long extra values must be an integer")
|
||||
.describe("Long extra value as signed decimal text"),
|
||||
value: javaDecimalExtraValueSchema(64),
|
||||
}),
|
||||
z.strictObject({
|
||||
key: activityExtraKeySchema,
|
||||
@@ -330,7 +344,7 @@ export const adbInputSchemas = {
|
||||
.min(1)
|
||||
.max(512)
|
||||
.regex(
|
||||
/^[A-Za-z0-9._\x2d]+(?:\/[A-Za-z0-9._$\x2d]*)?$/u,
|
||||
/^[A-Za-z0-9._\x2d]+\/[A-Za-z0-9._$\x2d]+$/u,
|
||||
"Component must be package/activity form",
|
||||
)
|
||||
.optional()
|
||||
|
||||
@@ -397,7 +397,13 @@ it("retains safe structured constraints for ADB selectors", async () => {
|
||||
expected: "regex",
|
||||
},
|
||||
{
|
||||
argv: ["start-adb-activity", "S", "x", "--component", "a".repeat(513)],
|
||||
argv: [
|
||||
"start-adb-activity",
|
||||
"S",
|
||||
"x",
|
||||
"--component",
|
||||
`${"a".repeat(256)}/${"b".repeat(256)}`,
|
||||
],
|
||||
path: "component",
|
||||
maximum: 512,
|
||||
},
|
||||
|
||||
@@ -485,11 +485,14 @@ describe("ADB selector input schemas: package and activity", () => {
|
||||
undefined,
|
||||
"",
|
||||
"bad component!",
|
||||
"example.app",
|
||||
"example.app/",
|
||||
"com.example/.Main$Inner",
|
||||
"a".repeat(512),
|
||||
"a".repeat(513),
|
||||
`${"a".repeat(255)}/${"b".repeat(256)}`,
|
||||
`${"a".repeat(256)}/${"b".repeat(256)}`,
|
||||
],
|
||||
valid: [true, false, false, true, true, false],
|
||||
valid: [true, false, false, false, false, true, false, true, false],
|
||||
},
|
||||
];
|
||||
for (const selection of selections) {
|
||||
@@ -512,7 +515,7 @@ describe("ADB selector input schemas: package and activity", () => {
|
||||
const expected = selection.valid[index];
|
||||
expect(
|
||||
selection.canonical.safeParse(value).success,
|
||||
`${command} owner`,
|
||||
`${command} ${selection.key} owner ${String(index)}`,
|
||||
).toBe(expected);
|
||||
const base = selection.group === "args" ? { serial: "S" } : {};
|
||||
const input =
|
||||
@@ -564,6 +567,50 @@ describe("ADB selector input schemas: dispatch", () => {
|
||||
code: "VALIDATION_ERROR",
|
||||
});
|
||||
}
|
||||
const incompleteComponents = [
|
||||
"example.app",
|
||||
"example.app/",
|
||||
"a".repeat(512),
|
||||
];
|
||||
for (const component of incompleteComponents) {
|
||||
const result = await serve(cli, [
|
||||
"start-adb-activity",
|
||||
"S",
|
||||
"x",
|
||||
"--component",
|
||||
component,
|
||||
"--json",
|
||||
]);
|
||||
expect(result.exitCode, component).not.toBe(0);
|
||||
expect(JSON.parse(result.stdout), component).toMatchObject({
|
||||
code: "VALIDATION_ERROR",
|
||||
fieldErrors: [
|
||||
expect.objectContaining({
|
||||
path: "component",
|
||||
code: "invalid_format",
|
||||
}),
|
||||
],
|
||||
});
|
||||
}
|
||||
const overlongComponent = `${"a".repeat(256)}/${"b".repeat(256)}`;
|
||||
const overlong = await serve(cli, [
|
||||
"start-adb-activity",
|
||||
"S",
|
||||
"x",
|
||||
"--component",
|
||||
overlongComponent,
|
||||
"--json",
|
||||
]);
|
||||
expect(overlong.exitCode).not.toBe(0);
|
||||
expect(JSON.parse(overlong.stdout)).toMatchObject({
|
||||
code: "VALIDATION_ERROR",
|
||||
fieldErrors: [
|
||||
expect.objectContaining({
|
||||
path: "component",
|
||||
code: "too_big",
|
||||
}),
|
||||
],
|
||||
});
|
||||
expect(provider.inspectAvailability).not.toHaveBeenCalled();
|
||||
expect(provider.execute).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user