Files
rea/src/adb/AdbHostFileName.ts
T
Dery Ferdikaandmorluto 1d4c4eb030 fix(adb): reject unsafe host file names when pulling one device file (#1962)
pull_adb_file derived its host file name from the last path segment of a
caller-supplied device path and joined it to the output directory without
checking that the name is usable on the host. A device path ending in a
separator, a dot segment, a control character, a Windows reserved device
name, a trailing dot or space, or a ':' alternate-data-stream marker could
resolve to an unexpected host path or fail opaquely after the transfer.

The single-file path now derives the name with POSIX semantics and refuses
any non-portable segment as invalid_request before creating a directory or
running adb pull. This matches the package path's pending host-name policy
and never renames a device-reported file.

Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
2026-10-12 05:33:40 +08:00

62 lines
2.5 KiB
TypeScript

import { posix } from "node:path";
/** Why a device-reported path cannot be used as a host file name. */
export type AdbHostFileNameReason =
| "empty_file_name"
| "dot_segment"
| "path_separator"
| "control_character"
| "unsupported_character"
| "reserved_name"
| "trailing_dot_or_space"
| "alternate_data_stream";
/** A portable host file name, or the exact reason the device name was refused. */
export type AdbHostFileNameResult =
| { readonly ok: true; readonly fileName: string }
| { readonly ok: false; readonly reason: AdbHostFileNameReason };
/**
* Win32 resolves these basenames to a device regardless of case and of any
* extension (`NUL`, `nul.txt`), so a portable host file name must avoid them on
* every host. `(?:\.|$)` keeps ordinary names such as `console.txt` admissible.
*/
const WINDOWS_RESERVED_BASENAME =
/^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/iu;
/** Characters that are not portable in a host file name on Windows or POSIX. */
const UNSUPPORTED_CHARACTER = /[*?"<>|]/u;
/** Control characters (`Cc`), including C0 controls and DEL. */
const CONTROL_CHARACTER = /\p{Cc}/u;
/**
* Derive a portable host file name from a device-reported path.
*
* The device path is a POSIX path on the device filesystem, so its last
* `/`-separated segment is taken with POSIX semantics rather than the host
* `basename` (which would vary between POSIX and Windows hosts). A name is
* accepted only when every host can store it and address it back unambiguously:
* no separator, control, or reserved-device characters, no trailing dot or
* space, no `:` alternate-data-stream marker, and no `.`/`..` segment.
*/
export const adbHostFileName = (devicePath: string): AdbHostFileNameResult => {
const fileName = posix.basename(devicePath);
if (fileName === "") return { ok: false, reason: "empty_file_name" };
if (fileName === "." || fileName === "..")
return { ok: false, reason: "dot_segment" };
if (fileName.includes("\\") || fileName.includes("/"))
return { ok: false, reason: "path_separator" };
if (CONTROL_CHARACTER.test(fileName))
return { ok: false, reason: "control_character" };
if (fileName.includes(":"))
return { ok: false, reason: "alternate_data_stream" };
if (UNSUPPORTED_CHARACTER.test(fileName))
return { ok: false, reason: "unsupported_character" };
if (WINDOWS_RESERVED_BASENAME.test(fileName))
return { ok: false, reason: "reserved_name" };
if (/[. ]$/u.test(fileName))
return { ok: false, reason: "trailing_dot_or_space" };
return { ok: true, fileName };
};