Files
openclaw/src/infra/archive-path.ts
2026-02-18 16:48:35 +00:00

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;
}