fix: preflight complete archives before mutation
Plan and validate every internal ZIP, Zip4j, and SevenZipJBinding entry and final conflict target before any archive output mutates the filesystem. Preserve raw RAR list entry whitespace for strict validation and keep any non-matching package owner marker immutable while direct scoped outputs continue without marker reissuance.
This commit is contained in:
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
@@ -262,6 +262,20 @@ public final class JBindExtractorMain {
|
|||||||
ProgressTracker progress = new ProgressTracker(totalUnits);
|
ProgressTracker progress = new ProgressTracker(totalUnits);
|
||||||
progress.emitStart();
|
progress.emitStart();
|
||||||
|
|
||||||
|
Set<String> preflightReserved = new HashSet<String>();
|
||||||
|
for (FileHeader header : fileHeaders) {
|
||||||
|
if (header == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
String entryName = normalizeEntryName(header.getFileName(), "file");
|
||||||
|
if (header.isDirectory()) {
|
||||||
|
File dir = resolveDirectory(request.targetDir, entryName);
|
||||||
|
preflightReserved.add(pathKey(dir));
|
||||||
|
} else {
|
||||||
|
resolveOutputFile(request.targetDir, entryName, request.conflictMode, preflightReserved);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Set<String> reserved = new HashSet<String>();
|
Set<String> reserved = new HashSet<String>();
|
||||||
for (FileHeader header : fileHeaders) {
|
for (FileHeader header : fileHeaders) {
|
||||||
if (header == null) {
|
if (header == null) {
|
||||||
@@ -382,6 +396,7 @@ public final class JBindExtractorMain {
|
|||||||
List<Long> fileSizes = new ArrayList<Long>();
|
List<Long> fileSizes = new ArrayList<Long>();
|
||||||
List<String> entryNames = new ArrayList<String>();
|
List<String> entryNames = new ArrayList<String>();
|
||||||
List<String> dispositions = new ArrayList<String>();
|
List<String> dispositions = new ArrayList<String>();
|
||||||
|
List<File> outputDirectories = new ArrayList<File>();
|
||||||
Set<String> reserved = new HashSet<String>();
|
Set<String> reserved = new HashSet<String>();
|
||||||
|
|
||||||
for (int i = 0; i < itemCount; i++) {
|
for (int i = 0; i < itemCount; i++) {
|
||||||
@@ -391,8 +406,7 @@ public final class JBindExtractorMain {
|
|||||||
|
|
||||||
if (Boolean.TRUE.equals(isFolder)) {
|
if (Boolean.TRUE.equals(isFolder)) {
|
||||||
File dir = resolveDirectory(request.targetDir, entryName);
|
File dir = resolveDirectory(request.targetDir, entryName);
|
||||||
ensureDirectory(dir);
|
outputDirectories.add(dir);
|
||||||
rejectLinkedPath(request.targetDir, dir);
|
|
||||||
reserved.add(pathKey(dir));
|
reserved.add(pathKey(dir));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -420,6 +434,11 @@ public final class JBindExtractorMain {
|
|||||||
dispositions.add(outputTarget.disposition);
|
dispositions.add(outputTarget.disposition);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (File directory : outputDirectories) {
|
||||||
|
ensureDirectory(directory);
|
||||||
|
rejectLinkedPath(request.targetDir, directory);
|
||||||
|
}
|
||||||
|
|
||||||
if (fileIndices.isEmpty()) {
|
if (fileIndices.isEmpty()) {
|
||||||
|
|
||||||
ProgressTracker progress = new ProgressTracker(1);
|
ProgressTracker progress = new ProgressTracker(1);
|
||||||
@@ -605,7 +624,7 @@ public final class JBindExtractorMain {
|
|||||||
|
|
||||||
if (conflictMode == ConflictMode.OVERWRITE) {
|
if (conflictMode == ConflictMode.OVERWRITE) {
|
||||||
if (base.exists()) {
|
if (base.exists()) {
|
||||||
if (!base.isFile() || !base.delete()) {
|
if (!base.isFile()) {
|
||||||
throw new IOException("Konnte Datei nicht uberschreiben: " + base.getAbsolutePath());
|
throw new IOException("Konnte Datei nicht uberschreiben: " + base.getAbsolutePath());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4517,17 +4517,13 @@ export class DownloadManager extends EventEmitter {
|
|||||||
} catch {
|
} catch {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const rawExistingMarker = await this.readPackageOutputOwnerMarker(pkg, false);
|
|
||||||
const nonMarkerEntries = entries.filter((entry) => entry.name !== PACKAGE_OUTPUT_OWNER_MARKER);
|
const nonMarkerEntries = entries.filter((entry) => entry.name !== PACKAGE_OUTPUT_OWNER_MARKER);
|
||||||
if (nonMarkerEntries.length > 0) {
|
if (nonMarkerEntries.length > 0) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (entries.some((entry) => entry.name === PACKAGE_OUTPUT_OWNER_MARKER)) {
|
if (entries.some((entry) => entry.name === PACKAGE_OUTPUT_OWNER_MARKER)) {
|
||||||
if (!rawExistingMarker || rawExistingMarker.packageId !== pkg.id) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
await fs.promises.rm(this.packageOutputOwnerMarkerPath(pkg), { force: true });
|
|
||||||
}
|
|
||||||
const marker: PackageOutputOwnerMarker = {
|
const marker: PackageOutputOwnerMarker = {
|
||||||
version: 1,
|
version: 1,
|
||||||
packageId: pkg.id,
|
packageId: pkg.id,
|
||||||
|
|||||||
+78
-35
@@ -2257,7 +2257,7 @@ export function buildExternalListArgs(command: string, archivePath: string, pass
|
|||||||
export function parseNativeArchiveEntryList(command: string, output: string): string[] {
|
export function parseNativeArchiveEntryList(command: string, output: string): string[] {
|
||||||
const lines = String(output || "").split(/\r?\n/);
|
const lines = String(output || "").split(/\r?\n/);
|
||||||
if (isRarNativeCommand(command)) {
|
if (isRarNativeCommand(command)) {
|
||||||
return lines.map((line) => line.trim()).filter(Boolean);
|
return lines.filter((line) => line.length > 0);
|
||||||
}
|
}
|
||||||
const entries: string[] = [];
|
const entries: string[] = [];
|
||||||
let inEntries = false;
|
let inEntries = false;
|
||||||
@@ -2988,8 +2988,18 @@ async function extractZipArchive(
|
|||||||
const zip = new AdmZip(archivePath);
|
const zip = new AdmZip(archivePath);
|
||||||
const entries = zip.getEntries();
|
const entries = zip.getEntries();
|
||||||
const resolvedTarget = path.resolve(targetDir);
|
const resolvedTarget = path.resolve(targetDir);
|
||||||
const usedOutputs = new Set<string>();
|
const plannedOutputs = new Set<string>();
|
||||||
const renameCounters = new Map<string, number>();
|
const renameCounters = new Map<string, number>();
|
||||||
|
const directoryPlans: Array<{ entryPath: string; outputPath: string }> = [];
|
||||||
|
const filePlans: Array<{
|
||||||
|
entry: (typeof entries)[number];
|
||||||
|
entryPath: string;
|
||||||
|
outputPath: string;
|
||||||
|
outputKey: string;
|
||||||
|
disposition: ExtractOutputEvent["disposition"];
|
||||||
|
uncompressedSize: number;
|
||||||
|
compressedSize: number;
|
||||||
|
}> = [];
|
||||||
|
|
||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
if (signal?.aborted) {
|
if (signal?.aborted) {
|
||||||
@@ -2997,13 +3007,12 @@ async function extractZipArchive(
|
|||||||
}
|
}
|
||||||
const baseOutputPath = path.resolve(targetDir, entry.entryName);
|
const baseOutputPath = path.resolve(targetDir, entry.entryName);
|
||||||
if (!baseOutputPath.startsWith(resolvedTarget + path.sep) && baseOutputPath !== resolvedTarget) {
|
if (!baseOutputPath.startsWith(resolvedTarget + path.sep) && baseOutputPath !== resolvedTarget) {
|
||||||
logger.warn(`ZIP-Eintrag übersprungen (Path Traversal): ${entry.entryName}`);
|
throw new Error(`ZIP-Eintrag Path Traversal blockiert: ${entry.entryName}`);
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
if (entry.isDirectory) {
|
if (entry.isDirectory) {
|
||||||
validateTarget?.(entry.entryName.replace(/\\/g, "/").replace(/\/$/, "") || "directory", baseOutputPath);
|
const entryPath = entry.entryName.replace(/\\/g, "/").replace(/\/$/, "") || "directory";
|
||||||
await fs.promises.mkdir(baseOutputPath, { recursive: true });
|
validateTarget?.(entryPath, baseOutputPath);
|
||||||
validateTarget?.(entry.entryName.replace(/\\/g, "/").replace(/\/$/, "") || "directory", baseOutputPath);
|
directoryPlans.push({ entryPath, outputPath: baseOutputPath });
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3044,16 +3053,19 @@ async function extractZipArchive(
|
|||||||
let outputKey = pathSetKey(outputPath);
|
let outputKey = pathSetKey(outputPath);
|
||||||
let disposition: ExtractOutputEvent["disposition"] = "written";
|
let disposition: ExtractOutputEvent["disposition"] = "written";
|
||||||
|
|
||||||
const outputExists = usedOutputs.has(outputKey) || await fs.promises.access(outputPath).then(() => true, () => false);
|
const outputExists = plannedOutputs.has(outputKey) || await fs.promises.access(outputPath).then(() => true, () => false);
|
||||||
if (outputExists) {
|
if (outputExists) {
|
||||||
if (mode === "skip") {
|
if (mode === "skip") {
|
||||||
onOutput?.({
|
const entryPath = entry.entryName.replace(/\\/g, "/");
|
||||||
version: 1,
|
validateTarget?.(entryPath, baseOutputPath);
|
||||||
archivePath: path.resolve(archivePath),
|
filePlans.push({
|
||||||
entryPath: entry.entryName.replace(/\\/g, "/"),
|
entry,
|
||||||
|
entryPath,
|
||||||
outputPath: baseOutputPath,
|
outputPath: baseOutputPath,
|
||||||
state: "complete",
|
outputKey,
|
||||||
disposition: "skipped"
|
disposition: "skipped",
|
||||||
|
uncompressedSize,
|
||||||
|
compressedSize
|
||||||
});
|
});
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -3066,7 +3078,7 @@ async function extractZipArchive(
|
|||||||
while (n <= 10000) {
|
while (n <= 10000) {
|
||||||
candidate = path.join(parsed.dir, `${parsed.name} (${n})${parsed.ext}`);
|
candidate = path.join(parsed.dir, `${parsed.name} (${n})${parsed.ext}`);
|
||||||
candidateKey = pathSetKey(candidate);
|
candidateKey = pathSetKey(candidate);
|
||||||
if (!usedOutputs.has(candidateKey) && !(await fs.promises.access(candidate).then(() => true, () => false))) {
|
if (!plannedOutputs.has(candidateKey) && !(await fs.promises.access(candidate).then(() => true, () => false))) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
n += 1;
|
n += 1;
|
||||||
@@ -3086,43 +3098,74 @@ async function extractZipArchive(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const normalizedEntryPath = entry.entryName.replace(/\\/g, "/");
|
||||||
|
validateTarget?.(normalizedEntryPath, outputPath);
|
||||||
|
plannedOutputs.add(outputKey);
|
||||||
|
filePlans.push({
|
||||||
|
entry,
|
||||||
|
entryPath: normalizedEntryPath,
|
||||||
|
outputPath,
|
||||||
|
outputKey,
|
||||||
|
disposition,
|
||||||
|
uncompressedSize,
|
||||||
|
compressedSize
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const directoryPlan of directoryPlans) {
|
||||||
if (signal?.aborted) {
|
if (signal?.aborted) {
|
||||||
throw new Error("aborted:extract");
|
throw new Error("aborted:extract");
|
||||||
}
|
}
|
||||||
const normalizedEntryPath = entry.entryName.replace(/\\/g, "/");
|
await fs.promises.mkdir(directoryPlan.outputPath, { recursive: true });
|
||||||
validateTarget?.(normalizedEntryPath, outputPath);
|
validateTarget?.(directoryPlan.entryPath, directoryPlan.outputPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const plan of filePlans) {
|
||||||
|
if (signal?.aborted) {
|
||||||
|
throw new Error("aborted:extract");
|
||||||
|
}
|
||||||
|
if (plan.disposition === "skipped") {
|
||||||
onOutput?.({
|
onOutput?.({
|
||||||
version: 1,
|
version: 1,
|
||||||
archivePath: path.resolve(archivePath),
|
archivePath: path.resolve(archivePath),
|
||||||
entryPath: normalizedEntryPath,
|
entryPath: plan.entryPath,
|
||||||
outputPath,
|
outputPath: plan.outputPath,
|
||||||
state: "opened",
|
state: "complete",
|
||||||
disposition
|
disposition: "skipped"
|
||||||
});
|
});
|
||||||
await fs.promises.mkdir(path.dirname(outputPath), { recursive: true });
|
continue;
|
||||||
validateTarget?.(normalizedEntryPath, outputPath);
|
}
|
||||||
const data = entry.getData();
|
onOutput?.({
|
||||||
|
version: 1,
|
||||||
|
archivePath: path.resolve(archivePath),
|
||||||
|
entryPath: plan.entryPath,
|
||||||
|
outputPath: plan.outputPath,
|
||||||
|
state: "opened",
|
||||||
|
disposition: plan.disposition
|
||||||
|
});
|
||||||
|
await fs.promises.mkdir(path.dirname(plan.outputPath), { recursive: true });
|
||||||
|
validateTarget?.(plan.entryPath, plan.outputPath);
|
||||||
|
const data = plan.entry.getData();
|
||||||
if (data.length > memoryLimitBytes) {
|
if (data.length > memoryLimitBytes) {
|
||||||
const entryMb = Math.ceil(data.length / (1024 * 1024));
|
const entryMb = Math.ceil(data.length / (1024 * 1024));
|
||||||
const limitMb = Math.ceil(memoryLimitBytes / (1024 * 1024));
|
const limitMb = Math.ceil(memoryLimitBytes / (1024 * 1024));
|
||||||
throw new Error(`ZIP-Eintrag zu groß für internen Entpacker (${entryMb} MB > ${limitMb} MB)`);
|
throw new Error(`ZIP-Eintrag zu groß für internen Entpacker (${entryMb} MB > ${limitMb} MB)`);
|
||||||
}
|
}
|
||||||
const maxDeclaredSize = Math.max(uncompressedSize, compressedSize);
|
const maxDeclaredSize = Math.max(plan.uncompressedSize, plan.compressedSize);
|
||||||
if (maxDeclaredSize > 0 && data.length > maxDeclaredSize * 20) {
|
if (maxDeclaredSize > 0 && data.length > maxDeclaredSize * 20) {
|
||||||
throw new Error(`ZIP-Eintrag verdächtig groß nach Entpacken (${entry.entryName})`);
|
throw new Error(`ZIP-Eintrag verdächtig groß nach Entpacken (${plan.entry.entryName})`);
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await fs.promises.writeFile(outputPath, data);
|
await fs.promises.writeFile(plan.outputPath, data);
|
||||||
usedOutputs.add(outputKey);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (await fs.promises.access(outputPath).then(() => true, () => false)) {
|
if (await fs.promises.access(plan.outputPath).then(() => true, () => false)) {
|
||||||
onOutput?.({
|
onOutput?.({
|
||||||
version: 1,
|
version: 1,
|
||||||
archivePath: path.resolve(archivePath),
|
archivePath: path.resolve(archivePath),
|
||||||
entryPath: entry.entryName.replace(/\\/g, "/"),
|
entryPath: plan.entryPath,
|
||||||
outputPath,
|
outputPath: plan.outputPath,
|
||||||
state: "partial",
|
state: "partial",
|
||||||
disposition
|
disposition: plan.disposition
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
throw error;
|
throw error;
|
||||||
@@ -3130,10 +3173,10 @@ async function extractZipArchive(
|
|||||||
onOutput?.({
|
onOutput?.({
|
||||||
version: 1,
|
version: 1,
|
||||||
archivePath: path.resolve(archivePath),
|
archivePath: path.resolve(archivePath),
|
||||||
entryPath: normalizedEntryPath,
|
entryPath: plan.entryPath,
|
||||||
outputPath,
|
outputPath: plan.outputPath,
|
||||||
state: "complete",
|
state: "complete",
|
||||||
disposition
|
disposition: plan.disposition
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13581,6 +13581,62 @@ describe("download manager", () => {
|
|||||||
expect(session.packages[packageId].outputRecords).toEqual([]);
|
expect(session.packages[packageId].outputRecords).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
["stale-generation", 2, 1, "11111111-1111-4111-8111-111111111111", "11111111-1111-4111-8111-111111111111"],
|
||||||
|
["stale-owner", 1, 1, "22222222-2222-4222-8222-222222222222", "33333333-3333-4333-8333-333333333333"]
|
||||||
|
] as const)("keeps a %s marker unchanged while direct scoped output continues", async (_label, currentGeneration, markerGeneration, currentOwner, markerOwner) => {
|
||||||
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-owner-stale-empty-"));
|
||||||
|
tempDirs.push(root);
|
||||||
|
const packageName = "stale-empty-package";
|
||||||
|
const extractDir = path.join(root, "extract", packageName);
|
||||||
|
fs.mkdirSync(extractDir, { recursive: true });
|
||||||
|
const packageId = "stale-empty-package-id";
|
||||||
|
const markerPath = path.join(extractDir, ".rd-package-output-owner-v1.json");
|
||||||
|
const markerContent = JSON.stringify({
|
||||||
|
version: 1,
|
||||||
|
packageId,
|
||||||
|
generation: markerGeneration,
|
||||||
|
ownerId: markerOwner
|
||||||
|
});
|
||||||
|
fs.writeFileSync(markerPath, markerContent);
|
||||||
|
const session = emptySession();
|
||||||
|
const pkg: PackageEntry = {
|
||||||
|
id: packageId,
|
||||||
|
name: packageName,
|
||||||
|
outputDir: path.join(root, "downloads", packageName),
|
||||||
|
extractDir,
|
||||||
|
status: "completed",
|
||||||
|
itemIds: [],
|
||||||
|
cancelled: false,
|
||||||
|
enabled: true,
|
||||||
|
outputOwnerId: currentOwner,
|
||||||
|
outputOwnerGeneration: currentGeneration,
|
||||||
|
resultGeneration: currentGeneration,
|
||||||
|
createdAt: 1_000,
|
||||||
|
updatedAt: 1_000
|
||||||
|
};
|
||||||
|
session.packageOrder = [packageId];
|
||||||
|
session.packages[packageId] = pkg;
|
||||||
|
const manager = new DownloadManager(defaultSettings(), session, createStoragePaths(path.join(root, "state")));
|
||||||
|
|
||||||
|
await (manager as any).runWithPackageOutputProvenance(pkg, async (targetDir: string, scope: any) => {
|
||||||
|
const outputPath = path.join(targetDir, "owned.mkv");
|
||||||
|
fs.writeFileSync(outputPath, "owned");
|
||||||
|
scope.add({
|
||||||
|
version: 1,
|
||||||
|
archivePath: path.join(pkg.outputDir, "archive.rar"),
|
||||||
|
entryPath: "owned.mkv",
|
||||||
|
outputPath,
|
||||||
|
state: "complete",
|
||||||
|
disposition: "written"
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(fs.readFileSync(markerPath, "utf8")).toBe(markerContent);
|
||||||
|
expect(fs.readFileSync(path.join(extractDir, "owned.mkv"), "utf8")).toBe("owned");
|
||||||
|
expect(pkg.outputRecords).toEqual([expect.objectContaining({ entryPath: "owned.mkv" })]);
|
||||||
|
});
|
||||||
|
|
||||||
it("does NOT move bonus files from Extras subdirectory to flat library", async () => {
|
it("does NOT move bonus files from Extras subdirectory to flat library", async () => {
|
||||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-dm-"));
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-dm-"));
|
||||||
tempDirs.push(root);
|
tempDirs.push(root);
|
||||||
|
|||||||
@@ -361,6 +361,47 @@ describe.skipIf(!hasJavaRuntime() || !hasJvmExtractorRuntime())("extractor jvm b
|
|||||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||||
}, 10000);
|
}, 10000);
|
||||||
|
|
||||||
|
it.each(["7zjbinding", "zip4j"])("preflights every %s entry before overwriting an earlier safe target", (backend) => {
|
||||||
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), `rd-jvm-full-preflight-${backend}-`));
|
||||||
|
tempDirs.push(root);
|
||||||
|
const targetDir = path.join(root, "out");
|
||||||
|
fs.mkdirSync(targetDir, { recursive: true });
|
||||||
|
const safePath = path.join(targetDir, "00-safe.txt");
|
||||||
|
const aliasPath = path.join(targetDir, "zz-name");
|
||||||
|
fs.writeFileSync(safePath, "foreign-safe");
|
||||||
|
fs.writeFileSync(aliasPath, "foreign-alias");
|
||||||
|
const zipPath = path.join(root, "preflight.zip");
|
||||||
|
const zip = new AdmZip();
|
||||||
|
zip.addFile("00-safe.txt", Buffer.from("package-safe"));
|
||||||
|
zip.addFile("zz-name.", Buffer.from("package-invalid"));
|
||||||
|
zip.writeZip(zipPath);
|
||||||
|
const runtimeRoot = path.join(process.cwd(), "resources", "extractor-jvm");
|
||||||
|
const classPath = [
|
||||||
|
path.join(runtimeRoot, "classes"),
|
||||||
|
path.join(runtimeRoot, "lib", "sevenzipjbinding.jar"),
|
||||||
|
path.join(runtimeRoot, "lib", "sevenzipjbinding-all-platforms.jar"),
|
||||||
|
path.join(runtimeRoot, "lib", "zip4j.jar")
|
||||||
|
].join(path.delimiter);
|
||||||
|
|
||||||
|
const run = spawnSync("java", [
|
||||||
|
"-cp",
|
||||||
|
classPath,
|
||||||
|
"com.sucukdeluxe.extractor.JBindExtractorMain",
|
||||||
|
"--archive",
|
||||||
|
zipPath,
|
||||||
|
"--target",
|
||||||
|
targetDir,
|
||||||
|
"--conflict",
|
||||||
|
"overwrite",
|
||||||
|
"--backend",
|
||||||
|
backend
|
||||||
|
], { encoding: "utf8" });
|
||||||
|
|
||||||
|
expect(run.status).not.toBe(0);
|
||||||
|
expect(fs.readFileSync(safePath, "utf8")).toBe("foreign-safe");
|
||||||
|
expect(fs.readFileSync(aliasPath, "utf8")).toBe("foreign-alias");
|
||||||
|
});
|
||||||
|
|
||||||
it("emits progress callbacks with archiveName and percent", async () => {
|
it("emits progress callbacks with archiveName and percent", async () => {
|
||||||
process.env.RD_EXTRACT_BACKEND = "jvm";
|
process.env.RD_EXTRACT_BACKEND = "jvm";
|
||||||
|
|
||||||
|
|||||||
@@ -1661,5 +1661,44 @@ describe("extractor", () => {
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("preflights every internal ZIP entry before overwriting an earlier safe target", async () => {
|
||||||
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-zip-full-preflight-"));
|
||||||
|
tempDirs.push(root);
|
||||||
|
const packageDir = path.join(root, "pkg");
|
||||||
|
const targetDir = path.join(root, "out");
|
||||||
|
fs.mkdirSync(packageDir, { recursive: true });
|
||||||
|
fs.mkdirSync(targetDir, { recursive: true });
|
||||||
|
const safePath = path.join(targetDir, "00-safe.txt");
|
||||||
|
const aliasPath = path.join(targetDir, "zz-name");
|
||||||
|
fs.writeFileSync(safePath, "foreign-safe");
|
||||||
|
fs.writeFileSync(aliasPath, "foreign-alias");
|
||||||
|
const zip = new AdmZip();
|
||||||
|
zip.addFile("00-safe.txt", Buffer.from("package-safe"));
|
||||||
|
zip.addFile("zz-name.", Buffer.from("package-invalid"));
|
||||||
|
zip.writeZip(path.join(packageDir, "release.zip"));
|
||||||
|
|
||||||
|
const result = await extractPackageArchives({
|
||||||
|
packageDir,
|
||||||
|
targetDir,
|
||||||
|
cleanupMode: "none",
|
||||||
|
conflictMode: "overwrite",
|
||||||
|
removeLinks: false,
|
||||||
|
removeSamples: false
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.extracted).toBe(0);
|
||||||
|
expect(result.failed).toBe(1);
|
||||||
|
expect(fs.readFileSync(safePath, "utf8")).toBe("foreign-safe");
|
||||||
|
expect(fs.readFileSync(aliasPath, "utf8")).toBe("foreign-alias");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("preserves raw RAR list trailing whitespace and dots for validation", () => {
|
||||||
|
const entries = parseNativeArchiveEntryList("UnRAR.exe", "safe.mkv\r\nname \r\nname.\r\n");
|
||||||
|
expect(entries).toEqual(["safe.mkv", "name ", "name."]);
|
||||||
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-native-raw-list-"));
|
||||||
|
tempDirs.push(root);
|
||||||
|
expect(() => validateNativeArchiveEntryCandidates(entries, root)).toThrow(/Ausgabepfad|entry/i);
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user