mirror of
https://github.com/yume-chan/ya-webadb.git
synced 2025-10-03 09:49:24 +02:00
fix(bin): properly escape arguments (#788)
This commit is contained in:
parent
f3f5be0b1a
commit
2f57a86a80
8 changed files with 106 additions and 79 deletions
|
@ -13,7 +13,8 @@ export function escapeArg(s: string) {
|
|||
break;
|
||||
}
|
||||
result += s.substring(base, found);
|
||||
// a'b becomes a'\'b (the backslash is not a escape character)
|
||||
// a'b becomes 'a'\'b', which is 'a' + \' + 'b'
|
||||
// (quoted string 'a', escaped single quote, and quoted string 'b')
|
||||
result += String.raw`'\''`;
|
||||
base = found + 1;
|
||||
}
|
||||
|
@ -22,23 +23,32 @@ export function escapeArg(s: string) {
|
|||
return result;
|
||||
}
|
||||
|
||||
export function splitCommand(command: string): string[] {
|
||||
/**
|
||||
* Split the command.
|
||||
*
|
||||
* Quotes and escaped characters are supported, and will be returned as-is.
|
||||
* @param input The input command
|
||||
* @returns An array of string containing the arguments
|
||||
*/
|
||||
export function splitCommand(input: string): string[] {
|
||||
const result: string[] = [];
|
||||
let quote: string | undefined;
|
||||
let isEscaped = false;
|
||||
let start = 0;
|
||||
|
||||
for (let i = 0, len = command.length; i < len; i += 1) {
|
||||
for (let i = 0, len = input.length; i < len; i += 1) {
|
||||
if (isEscaped) {
|
||||
isEscaped = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
const char = command.charAt(i);
|
||||
const char = input.charAt(i);
|
||||
switch (char) {
|
||||
case " ":
|
||||
if (!quote && i !== start) {
|
||||
result.push(command.substring(start, i));
|
||||
if (!quote) {
|
||||
if (i !== start) {
|
||||
result.push(input.substring(start, i));
|
||||
}
|
||||
start = i + 1;
|
||||
}
|
||||
break;
|
||||
|
@ -56,8 +66,8 @@ export function splitCommand(command: string): string[] {
|
|||
}
|
||||
}
|
||||
|
||||
if (start < command.length) {
|
||||
result.push(command.substring(start));
|
||||
if (start < input.length) {
|
||||
result.push(input.substring(start));
|
||||
}
|
||||
|
||||
return result;
|
||||
|
|
|
@ -5,7 +5,7 @@ import { SplitStringStream, TextDecoderStream } from "@yume-chan/stream-extra";
|
|||
import { Cmd } from "./cmd/index.js";
|
||||
import type { IntentBuilder } from "./intent.js";
|
||||
import type { SingleUser } from "./utils.js";
|
||||
import { buildArguments } from "./utils.js";
|
||||
import { buildCommand } from "./utils.js";
|
||||
|
||||
export interface ActivityManagerStartActivityOptions {
|
||||
displayId?: number;
|
||||
|
@ -43,21 +43,25 @@ export class ActivityManager extends AdbServiceBase {
|
|||
): Promise<void> {
|
||||
// `am start` and `am start-activity` are the same,
|
||||
// but `am start-activity` was added in Android 8.
|
||||
let args = buildArguments(
|
||||
const command = buildCommand(
|
||||
[ActivityManager.ServiceName, "start", "-W"],
|
||||
options,
|
||||
START_ACTIVITY_OPTIONS_MAP,
|
||||
);
|
||||
|
||||
args = args.concat(options.intent.build().map(escapeArg));
|
||||
for (const arg of options.intent.build()) {
|
||||
command.push(arg);
|
||||
}
|
||||
|
||||
// `cmd activity` doesn't support `start` command on Android 7.
|
||||
let process: AdbNoneProtocolProcess;
|
||||
if (this.#apiLevel <= 25) {
|
||||
args[0] = ActivityManager.CommandName;
|
||||
process = await this.adb.subprocess.noneProtocol.spawn(args);
|
||||
command[0] = ActivityManager.CommandName;
|
||||
process = await this.adb.subprocess.noneProtocol.spawn(
|
||||
command.map(escapeArg),
|
||||
);
|
||||
} else {
|
||||
process = await this.#cmd.spawn(args);
|
||||
process = await this.#cmd.spawn(command);
|
||||
}
|
||||
|
||||
const lines = process.output
|
||||
|
|
|
@ -3,6 +3,7 @@ import {
|
|||
AdbFeature,
|
||||
AdbNoneProtocolProcessImpl,
|
||||
adbNoneProtocolSpawner,
|
||||
escapeArg,
|
||||
} from "@yume-chan/adb";
|
||||
|
||||
import { Cmd } from "./service.js";
|
||||
|
@ -37,7 +38,7 @@ export function createNoneProtocol(
|
|||
spawn: adbNoneProtocolSpawner(async (command, signal) => {
|
||||
checkCommand(command);
|
||||
|
||||
const newCommand = command.slice();
|
||||
const newCommand = command.map(escapeArg);
|
||||
newCommand.unshift("cmd");
|
||||
return adb.subprocess.noneProtocol.spawn(newCommand, signal);
|
||||
}),
|
||||
|
@ -50,7 +51,7 @@ export function createNoneProtocol(
|
|||
spawn: adbNoneProtocolSpawner(async (command, signal) => {
|
||||
checkCommand(command);
|
||||
|
||||
const newCommand = command.slice();
|
||||
const newCommand = command.map(escapeArg);
|
||||
newCommand[0] = resolveFallback(fallback, command[0]!);
|
||||
return adb.subprocess.noneProtocol.spawn(newCommand, signal);
|
||||
}),
|
||||
|
|
|
@ -3,6 +3,7 @@ import {
|
|||
AdbFeature,
|
||||
AdbShellProtocolProcessImpl,
|
||||
adbShellProtocolSpawner,
|
||||
escapeArg,
|
||||
} from "@yume-chan/adb";
|
||||
|
||||
import { Cmd } from "./service.js";
|
||||
|
@ -42,7 +43,7 @@ export function createShellProtocol(
|
|||
spawn: adbShellProtocolSpawner(async (command, signal) => {
|
||||
checkCommand(command);
|
||||
|
||||
const newCommand = command.slice();
|
||||
const newCommand = command.map(escapeArg);
|
||||
newCommand.unshift("cmd");
|
||||
return shellProtocolService.spawn(newCommand, signal);
|
||||
}),
|
||||
|
@ -55,7 +56,7 @@ export function createShellProtocol(
|
|||
spawn: adbShellProtocolSpawner(async (command, signal) => {
|
||||
checkCommand(command);
|
||||
|
||||
const newCommand = command.slice();
|
||||
const newCommand = command.map(escapeArg);
|
||||
newCommand[0] = resolveFallback(fallback, command[0]!);
|
||||
return shellProtocolService.spawn(newCommand, signal);
|
||||
}),
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
import { splitCommand } from "@yume-chan/adb";
|
||||
|
||||
import type { Cmd } from "./service.js";
|
||||
|
||||
export function resolveFallback(
|
||||
|
@ -31,13 +29,7 @@ export function serializeAbbService(
|
|||
): string {
|
||||
checkCommand(command);
|
||||
|
||||
// `abb` mode doesn't use `sh -c` to execute to command,
|
||||
// so it doesn't accept escaped arguments.
|
||||
// `splitCommand` can be used to remove the escaping,
|
||||
// each item in `command` must be a single argument.
|
||||
const newCommand = command.map((arg) => splitCommand(arg)[0]!);
|
||||
|
||||
// `abb` mode uses `\0` as the separator, allowing space in arguments.
|
||||
// The last `\0` is required for older versions of `adb`.
|
||||
return `${prefix}:${newCommand.join("\0")}\0`;
|
||||
return `${prefix}:${command.join("\0")}\0`;
|
||||
}
|
||||
|
|
|
@ -16,7 +16,7 @@ import {
|
|||
import { Cmd } from "./cmd/index.js";
|
||||
import type { IntentBuilder } from "./intent.js";
|
||||
import type { Optional, SingleUserOrAll } from "./utils.js";
|
||||
import { buildArguments } from "./utils.js";
|
||||
import { buildCommand } from "./utils.js";
|
||||
|
||||
export enum PackageManagerInstallLocation {
|
||||
Auto,
|
||||
|
@ -65,7 +65,7 @@ export const PackageManagerInstallOptions = {
|
|||
restrictPermissions: option<boolean>("--restrict-permissions", 29),
|
||||
doNotKill: option<boolean>("--dont-kill"),
|
||||
originatingUri: option<string>("--originating-uri"),
|
||||
refererUri: option<string>("--referrer"),
|
||||
referrerUri: option<string>("--referrer"),
|
||||
inheritFrom: option<string>("-p", 24),
|
||||
packageName: option<string>("--pkg", 28),
|
||||
abi: option<string>("--abi", 21),
|
||||
|
@ -187,7 +187,7 @@ const PACKAGE_MANAGER_RESOLVE_ACTIVITY_OPTIONS_MAP: Partial<
|
|||
user: "--user",
|
||||
};
|
||||
|
||||
function buildInstallArguments(
|
||||
function buildInstallCommand(
|
||||
command: string,
|
||||
options: PackageManagerInstallOptions | undefined,
|
||||
apiLevel: number | undefined,
|
||||
|
@ -252,7 +252,7 @@ function buildInstallArguments(
|
|||
args.push(option.name, value.toString());
|
||||
break;
|
||||
case "string":
|
||||
args.push(option.name, escapeArg(value));
|
||||
args.push(option.name, value);
|
||||
break;
|
||||
default:
|
||||
throw new Error(
|
||||
|
@ -287,22 +287,26 @@ export class PackageManager extends AdbServiceBase {
|
|||
apks: readonly string[],
|
||||
options?: PackageManagerInstallOptions,
|
||||
): Promise<void> {
|
||||
const args = buildInstallArguments("install", options, this.#apiLevel);
|
||||
args[0] = PackageManager.CommandName;
|
||||
const command = buildInstallCommand("install", options, this.#apiLevel);
|
||||
|
||||
command[0] = PackageManager.CommandName;
|
||||
|
||||
// WIP: old version of pm doesn't support multiple apks
|
||||
args.push(...apks.map(escapeArg));
|
||||
for (const apk of apks) {
|
||||
command.push(apk);
|
||||
}
|
||||
|
||||
// Starting from Android 7, `pm` becomes a wrapper to `cmd package`.
|
||||
// The benefit of `cmd package` is it starts faster than the old `pm`,
|
||||
// because it connects to the already running `system` process,
|
||||
// instead of initializing all system components from scratch.
|
||||
//
|
||||
// But launching `cmd package` directly causes it to not be able to
|
||||
// read files in `/data/local/tmp` (and many other places) due to SELinux policies,
|
||||
// so installing files must still use `pm`.
|
||||
// But `cmd` executable can't read files in `/data/local/tmp`
|
||||
// (and many other places) due to SELinux policies,
|
||||
// so installing from files must still use `pm`.
|
||||
// (the starting executable file decides which SELinux policies to apply)
|
||||
const output = await this.adb.subprocess.noneProtocol
|
||||
.spawn(args)
|
||||
.spawn(command.map(escapeArg))
|
||||
.wait()
|
||||
.toString()
|
||||
.then((output) => output.trim());
|
||||
|
@ -352,9 +356,9 @@ export class PackageManager extends AdbServiceBase {
|
|||
return;
|
||||
}
|
||||
|
||||
const args = buildInstallArguments("install", options, this.#apiLevel);
|
||||
args.push("-S", size.toString());
|
||||
const process = await this.#cmd.spawn(args);
|
||||
const command = buildInstallCommand("install", options, this.#apiLevel);
|
||||
command.push("-S", size.toString());
|
||||
const process = await this.#cmd.spawn(command);
|
||||
|
||||
const output = process.output
|
||||
.pipeThrough(new TextDecoderStream())
|
||||
|
@ -429,17 +433,17 @@ export class PackageManager extends AdbServiceBase {
|
|||
async *listPackages(
|
||||
options?: Optional<PackageManagerListPackagesOptions>,
|
||||
): AsyncGenerator<PackageManagerListPackagesResult, void, void> {
|
||||
const args = buildArguments(
|
||||
const command = buildCommand(
|
||||
["package", "list", "packages"],
|
||||
options,
|
||||
PACKAGE_MANAGER_LIST_PACKAGES_OPTIONS_MAP,
|
||||
);
|
||||
// `PACKAGE_MANAGER_LIST_PACKAGES_OPTIONS_MAP` doesn't have `filter`
|
||||
|
||||
if (options?.filter) {
|
||||
args.push(escapeArg(options.filter));
|
||||
command.push(options.filter);
|
||||
}
|
||||
|
||||
const process = await this.#cmd.spawn(args);
|
||||
const process = await this.#cmd.spawn(command);
|
||||
|
||||
const output = process.output
|
||||
.pipeThrough(new TextDecoderStream())
|
||||
|
@ -473,19 +477,23 @@ export class PackageManager extends AdbServiceBase {
|
|||
): Promise<string[]> {
|
||||
// `pm path` and `pm -p` are the same,
|
||||
// but `pm path` allows an optional `--user` option.
|
||||
const args = [PackageManager.ServiceName, "path"];
|
||||
const command = [PackageManager.ServiceName, "path"];
|
||||
|
||||
if (options?.user !== undefined) {
|
||||
args.push("--user", options.user.toString());
|
||||
command.push("--user", options.user.toString());
|
||||
}
|
||||
args.push(escapeArg(packageName));
|
||||
|
||||
command.push(packageName);
|
||||
|
||||
// `cmd package` doesn't support `path` command on Android 7 and 8.
|
||||
let process: AdbNoneProtocolProcess;
|
||||
if (this.#apiLevel !== undefined && this.#apiLevel <= 27) {
|
||||
args[0] = PackageManager.CommandName;
|
||||
process = await this.adb.subprocess.noneProtocol.spawn(args);
|
||||
command[0] = PackageManager.CommandName;
|
||||
process = await this.adb.subprocess.noneProtocol.spawn(
|
||||
command.map(escapeArg),
|
||||
);
|
||||
} else {
|
||||
process = await this.#cmd.spawn(args);
|
||||
process = await this.#cmd.spawn(command);
|
||||
}
|
||||
|
||||
const lines = process.output
|
||||
|
@ -510,18 +518,22 @@ export class PackageManager extends AdbServiceBase {
|
|||
packageName: string,
|
||||
options?: Optional<PackageManagerUninstallOptions>,
|
||||
): Promise<void> {
|
||||
let args = buildArguments(
|
||||
const command = buildCommand(
|
||||
[PackageManager.ServiceName, "uninstall"],
|
||||
options,
|
||||
PACKAGE_MANAGER_UNINSTALL_OPTIONS_MAP,
|
||||
);
|
||||
args.push(escapeArg(packageName));
|
||||
|
||||
command.push(packageName);
|
||||
|
||||
if (options?.splitNames) {
|
||||
args = args.concat(options.splitNames.map(escapeArg));
|
||||
for (const splitName of options.splitNames) {
|
||||
command.push(splitName);
|
||||
}
|
||||
}
|
||||
|
||||
const output = await this.#cmd
|
||||
.spawn(args)
|
||||
.spawn(command)
|
||||
.wait()
|
||||
.toString()
|
||||
.then((output) => output.trim());
|
||||
|
@ -533,16 +545,18 @@ export class PackageManager extends AdbServiceBase {
|
|||
async resolveActivity(
|
||||
options: PackageManagerResolveActivityOptions,
|
||||
): Promise<string | undefined> {
|
||||
let args = buildArguments(
|
||||
const command = buildCommand(
|
||||
[PackageManager.ServiceName, "resolve-activity", "--components"],
|
||||
options,
|
||||
PACKAGE_MANAGER_RESOLVE_ACTIVITY_OPTIONS_MAP,
|
||||
);
|
||||
|
||||
args = args.concat(options.intent.build().map(escapeArg));
|
||||
for (const arg of options.intent.build()) {
|
||||
command.push(arg);
|
||||
}
|
||||
|
||||
const output = await this.#cmd
|
||||
.spawn(args)
|
||||
.spawn(command)
|
||||
.wait()
|
||||
.toString()
|
||||
.then((output) => output.trim());
|
||||
|
@ -567,14 +581,14 @@ export class PackageManager extends AdbServiceBase {
|
|||
async sessionCreate(
|
||||
options?: PackageManagerInstallOptions,
|
||||
): Promise<number> {
|
||||
const args = buildInstallArguments(
|
||||
const command = buildInstallCommand(
|
||||
"install-create",
|
||||
options,
|
||||
this.#apiLevel,
|
||||
);
|
||||
|
||||
const output = await this.#cmd
|
||||
.spawn(args)
|
||||
.spawn(command)
|
||||
.wait()
|
||||
.toString()
|
||||
.then((output) => output.trim());
|
||||
|
@ -605,16 +619,18 @@ export class PackageManager extends AdbServiceBase {
|
|||
splitName: string,
|
||||
path: string,
|
||||
): Promise<void> {
|
||||
const args: string[] = [
|
||||
const command: string[] = [
|
||||
PackageManager.CommandName,
|
||||
"install-write",
|
||||
sessionId.toString(),
|
||||
escapeArg(splitName),
|
||||
escapeArg(path),
|
||||
splitName,
|
||||
path,
|
||||
];
|
||||
|
||||
// Similar to `install`, must use `adb.subprocess` so it can read `path`
|
||||
const process = await this.adb.subprocess.noneProtocol.spawn(args);
|
||||
const process = await this.adb.subprocess.noneProtocol.spawn(
|
||||
command.map(escapeArg),
|
||||
);
|
||||
await this.checkResult(process.output);
|
||||
}
|
||||
|
||||
|
@ -624,17 +640,17 @@ export class PackageManager extends AdbServiceBase {
|
|||
size: number,
|
||||
stream: ReadableStream<MaybeConsumable<Uint8Array>>,
|
||||
): Promise<void> {
|
||||
const args: string[] = [
|
||||
const command: string[] = [
|
||||
PackageManager.ServiceName,
|
||||
"install-write",
|
||||
"-S",
|
||||
size.toString(),
|
||||
sessionId.toString(),
|
||||
escapeArg(splitName),
|
||||
splitName,
|
||||
"-",
|
||||
];
|
||||
|
||||
const process = await this.#cmd.spawn(args);
|
||||
const process = await this.#cmd.spawn(command);
|
||||
await Promise.all([
|
||||
stream.pipeTo(process.stdin),
|
||||
this.checkResult(process.output),
|
||||
|
@ -647,7 +663,7 @@ export class PackageManager extends AdbServiceBase {
|
|||
* @returns A `Promise` that resolves when the session is committed
|
||||
*/
|
||||
async sessionCommit(sessionId: number): Promise<void> {
|
||||
const args: string[] = [
|
||||
const command: string[] = [
|
||||
PackageManager.ServiceName,
|
||||
"install-commit",
|
||||
sessionId.toString(),
|
||||
|
@ -658,22 +674,24 @@ export class PackageManager extends AdbServiceBase {
|
|||
// causing this function to fail with an empty message.
|
||||
let process: AdbNoneProtocolProcess;
|
||||
if (this.#apiLevel !== undefined && this.#apiLevel <= 25) {
|
||||
args[0] = PackageManager.CommandName;
|
||||
process = await this.adb.subprocess.noneProtocol.spawn(args);
|
||||
command[0] = PackageManager.CommandName;
|
||||
process = await this.adb.subprocess.noneProtocol.spawn(
|
||||
command.map(escapeArg),
|
||||
);
|
||||
} else {
|
||||
process = await this.#cmd.spawn(args);
|
||||
process = await this.#cmd.spawn(command);
|
||||
}
|
||||
|
||||
await this.checkResult(process.output);
|
||||
}
|
||||
|
||||
async sessionAbandon(sessionId: number): Promise<void> {
|
||||
const args: string[] = [
|
||||
const command: string[] = [
|
||||
PackageManager.ServiceName,
|
||||
"install-abandon",
|
||||
sessionId.toString(),
|
||||
];
|
||||
const process = await this.#cmd.spawn(args);
|
||||
const process = await this.#cmd.spawn(command);
|
||||
await this.checkResult(process.output);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -42,14 +42,17 @@ export class Settings extends AdbServiceBase {
|
|||
options: SettingsOptions | undefined,
|
||||
...args: string[]
|
||||
): Promise<string> {
|
||||
let command = [Settings.ServiceName];
|
||||
const command = [Settings.ServiceName];
|
||||
|
||||
if (options?.user !== undefined) {
|
||||
command.push("--user", options.user.toString());
|
||||
}
|
||||
|
||||
command.push(verb, namespace);
|
||||
command = command.concat(args);
|
||||
|
||||
for (const arg of args) {
|
||||
command.push(arg);
|
||||
}
|
||||
|
||||
return this.#cmd.spawn(command).wait().toString();
|
||||
}
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
import { escapeArg } from "@yume-chan/adb";
|
||||
|
||||
export function buildArguments<T>(
|
||||
export function buildCommand<T>(
|
||||
commands: readonly string[],
|
||||
options: Partial<T> | undefined,
|
||||
map: Partial<Record<keyof T, string>>,
|
||||
|
@ -30,7 +28,7 @@ export function buildArguments<T>(
|
|||
args.push(option, value.toString());
|
||||
break;
|
||||
case "string":
|
||||
args.push(option, escapeArg(value));
|
||||
args.push(option, value);
|
||||
break;
|
||||
default:
|
||||
throw new Error(
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue