Files
rea/src/adb/AdbHostFileName.ts
T
morluto 55b61485e9 fix(adb): preserve device paths and prevent destination collisions
Interpret device paths once as POSIX paths and apply only the selected host's filename constraints. Preserve original PM paths and diagnostics, reject duplicate destinations before pulls, and use exact-name filesystem reservations to detect case and Unicode aliases without guessing volume rules.

Consolidate failed-package cleanup ownership and strengthen no-output assertions. Validated 44 provider fixture tests, 47 filename/parser tests, six compiled CLI tests, static/docs checks, and package checks. No Android hardware was used.
2026-10-12 05:57:35 +08:00

54 lines
1.9 KiB
TypeScript

/** Why a device-reported path cannot be used as a host file name. */
export type AdbHostFileNameReason =
| "empty_file_name"
| "not_absolute_path"
| "dot_segment"
| "unsupported_character"
| "reserved_name"
| "trailing_dot_or_space";
export type AdbHostFileNameResult =
| { readonly ok: true; readonly fileName: string }
| { readonly ok: false; readonly reason: AdbHostFileNameReason };
const WINDOWS_RESERVED_BASENAME =
/^(?:con|prn|aux|nul|com[1-9¹²³]|lpt[1-9¹²³])(?:\.|$)/iu;
/**
* Derive the final component of a device POSIX path without normalizing the
* source. A trailing slash has no filename and must not silently become the
* preceding component as `basename` would make it.
*/
export const adbHostFileName = (
devicePath: string,
platform: NodeJS.Platform = process.platform,
): AdbHostFileNameResult => {
if (devicePath === "" || devicePath.endsWith("/"))
return { ok: false, reason: "empty_file_name" };
if (devicePath.includes("\0"))
return { ok: false, reason: "unsupported_character" };
if (!devicePath.startsWith("/"))
return { ok: false, reason: "not_absolute_path" };
const fileName = devicePath.slice(devicePath.lastIndexOf("/") + 1);
if (fileName === "." || fileName === "..")
return { ok: false, reason: "dot_segment" };
if (platform === "win32") {
if (/[\\:*?"<>|]/u.test(fileName) || hasWindowsControl(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 };
};
const hasWindowsControl = (fileName: string): boolean => {
for (const character of fileName) {
const code = character.codePointAt(0)!;
if (code > 0 && code < 0x20) return true;
}
return false;
};