Compare commits

...
Author SHA1 Message Date
morluto 909e34c416 fix(adb): reject unrepresentable activity arguments 2026-11-10 17:57:56 -05:00
3 changed files with 73 additions and 21 deletions

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, {});
+16 -13
View File
@@ -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" },
+22 -8
View File
@@ -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()