64 lines
2.2 KiB
TypeScript
64 lines
2.2 KiB
TypeScript
import path from "node:path";
|
|
import { resolveSafeBaseDir } from "./path-safety.js";
|
|
|
|
export function isWindowsDrivePath(value: string): boolean {
|
|
return /^[a-zA-Z]:[\\/]/.test(value);
|
|
}
|
|
|
|
export function normalizeArchiveEntryPath(raw: string): string {
|
|
return raw.replaceAll("\\", "/");
|
|
}
|
|
|
|
export function validateArchiveEntryPath(
|
|
entryPath: string,
|
|
params?: { escapeLabel?: string },
|
|
): void {
|
|
if (!entryPath || entryPath === "." || entryPath === "./") {
|
|
return;
|
|
}
|
|
if (isWindowsDrivePath(entryPath)) {
|
|
throw new Error(`archive entry uses a drive path: ${entryPath}`);
|
|
}
|
|
const normalized = path.posix.normalize(normalizeArchiveEntryPath(entryPath));
|
|
const escapeLabel = params?.escapeLabel ?? "destination";
|
|
if (normalized === ".." || normalized.startsWith("../")) {
|
|
throw new Error(`archive entry escapes ${escapeLabel}: ${entryPath}`);
|
|
}
|
|
if (path.posix.isAbsolute(normalized) || normalized.startsWith("//")) {
|
|
throw new Error(`archive entry is absolute: ${entryPath}`);
|
|
}
|
|
}
|
|
|
|
export function stripArchivePath(entryPath: string, stripComponents: number): string | null {
|
|
const raw = normalizeArchiveEntryPath(entryPath);
|
|
if (!raw || raw === "." || raw === "./") {
|
|
return null;
|
|
}
|
|
|
|
// Mimic tar --strip-components semantics (raw segments before normalization)
|
|
// so strip-induced escapes like "a/../b" are visible to validators.
|
|
const parts = raw.split("/").filter((part) => part.length > 0 && part !== ".");
|
|
const strip = Math.max(0, Math.floor(stripComponents));
|
|
const stripped = strip === 0 ? parts.join("/") : parts.slice(strip).join("/");
|
|
const result = path.posix.normalize(stripped);
|
|
if (!result || result === "." || result === "./") {
|
|
return null;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
export function resolveArchiveOutputPath(params: {
|
|
rootDir: string;
|
|
relPath: string;
|
|
originalPath: string;
|
|
escapeLabel?: string;
|
|
}): string {
|
|
const safeBase = resolveSafeBaseDir(params.rootDir);
|
|
const outPath = path.resolve(params.rootDir, params.relPath);
|
|
const escapeLabel = params.escapeLabel ?? "destination";
|
|
if (!outPath.startsWith(safeBase)) {
|
|
throw new Error(`archive entry escapes ${escapeLabel}: ${params.originalPath}`);
|
|
}
|
|
return outPath;
|
|
}
|