Fix RAR native extractor fallback

This commit is contained in:
Sucukdeluxe 2026-03-08 02:48:49 +01:00
parent 53c411f635
commit 935f05e214
2 changed files with 58 additions and 29 deletions

View File

@ -694,11 +694,14 @@ function nativeExtractorCandidates(archivePath = ""): string[] {
} }
const winRarInstalled = [ const winRarInstalled = [
path.join(programFiles, "WinRAR", "Rar.exe"),
path.join(programFilesX86, "WinRAR", "Rar.exe"),
path.join(programFiles, "WinRAR", "UnRAR.exe"), path.join(programFiles, "WinRAR", "UnRAR.exe"),
path.join(programFilesX86, "WinRAR", "UnRAR.exe") path.join(programFilesX86, "WinRAR", "UnRAR.exe")
]; ];
if (localAppData) { if (localAppData) {
winRarInstalled.push(path.join(localAppData, "Programs", "WinRAR", "Rar.exe"));
winRarInstalled.push(path.join(localAppData, "Programs", "WinRAR", "UnRAR.exe")); winRarInstalled.push(path.join(localAppData, "Programs", "WinRAR", "UnRAR.exe"));
} }
@ -711,6 +714,8 @@ function nativeExtractorCandidates(archivePath = ""): string[] {
"7za.exe", "7za.exe",
"7za", "7za",
...winRarInstalled, ...winRarInstalled,
"Rar.exe",
"rar",
"UnRAR.exe", "UnRAR.exe",
"unrar" "unrar"
] ]
@ -721,6 +726,8 @@ function nativeExtractorCandidates(archivePath = ""): string[] {
"7za.exe", "7za.exe",
"7za", "7za",
...winRarInstalled, ...winRarInstalled,
"Rar.exe",
"rar",
"UnRAR.exe", "UnRAR.exe",
"unrar" "unrar"
]; ];
@ -1763,8 +1770,7 @@ export function buildExternalExtractArgs(
flatMode = false flatMode = false
): string[] { ): string[] {
const mode = effectiveConflictMode(conflictMode); const mode = effectiveConflictMode(conflictMode);
const lower = command.toLowerCase(); if (isRarNativeCommand(command)) {
if (lower.includes("unrar") || lower.includes("winrar")) {
// "e" extracts without paths (flat). Used as fallback when the archive stores paths with a // "e" extracts without paths (flat). Used as fallback when the archive stores paths with a
// leading \ (absolute-style), which causes UnRAR to produce invalid \\ double-separators. // leading \ (absolute-style), which causes UnRAR to produce invalid \\ double-separators.
const extractCmd = flatMode ? "e" : "x"; const extractCmd = flatMode ? "e" : "x";
@ -1804,8 +1810,7 @@ async function resolveExtractorCommandInternal(archivePath = ""): Promise<string
if (isAbsoluteCommand(command) && !fs.existsSync(command)) { if (isAbsoluteCommand(command) && !fs.existsSync(command)) {
continue; continue;
} }
const lower = command.toLowerCase(); const probeArgs = extractorProbeArgs(command);
const probeArgs = (lower.includes("winrar") || lower.includes("unrar")) ? ["-?"] : ["?"];
const probe = await runExtractCommand(command, probeArgs, undefined, undefined, EXTRACTOR_PROBE_TIMEOUT_MS); const probe = await runExtractCommand(command, probeArgs, undefined, undefined, EXTRACTOR_PROBE_TIMEOUT_MS);
if (probe.ok || (!probe.missingCommand && !probe.timedOut)) { if (probe.ok || (!probe.missingCommand && !probe.timedOut)) {
resolvedExtractorCommand = command; resolvedExtractorCommand = command;
@ -1845,15 +1850,19 @@ function is7zCommand(command: string): boolean {
return lower.includes("7z") && !lower.includes("unrar") && !lower.includes("winrar"); return lower.includes("7z") && !lower.includes("unrar") && !lower.includes("winrar");
} }
function isUnrarCommand(command: string): boolean { function isRarNativeCommand(command: string): boolean {
const lower = command.toLowerCase(); const base = path.basename(String(command || "")).toLowerCase();
return lower.includes("unrar") || lower.includes("winrar"); return base === "unrar.exe"
|| base === "unrar"
|| base === "winrar.exe"
|| base === "rar.exe"
|| base === "rar";
} }
type ExtractorCommandKind = "rar_native" | "seven_zip" | "other"; type ExtractorCommandKind = "rar_native" | "seven_zip" | "other";
function extractorCommandKind(command: string): ExtractorCommandKind { function extractorCommandKind(command: string): ExtractorCommandKind {
if (isUnrarCommand(command)) { if (isRarNativeCommand(command)) {
return "rar_native"; return "rar_native";
} }
if (is7zCommand(command)) { if (is7zCommand(command)) {
@ -1913,22 +1922,29 @@ export function orderExtractorCandidatesForArchive(
.map((entry) => entry.command); .map((entry) => entry.command);
} }
function extractorProbeArgs(command: string): string[] {
return isRarNativeCommand(command) ? ["-?"] : ["?"];
}
async function findAlternativeExtractor(currentCommand: string, archivePath = ""): Promise<string | null> { async function findAlternativeExtractor(currentCommand: string, archivePath = ""): Promise<string | null> {
const candidates = nativeExtractorCandidates(archivePath); const candidates = nativeExtractorCandidates(archivePath);
const currentIs7z = is7zCommand(currentCommand); const currentKind = extractorCommandKind(currentCommand);
const preferredKinds: ExtractorCommandKind[] = currentKind === "seven_zip"
? ["rar_native"]
: isRarArchivePath(archivePath)
? ["rar_native", "seven_zip"]
: ["seven_zip", "rar_native"];
for (const kind of preferredKinds) {
for (const candidate of candidates) { for (const candidate of candidates) {
if (candidate === currentCommand) continue; if (candidate === currentCommand) continue;
// If current is 7z, look for UnRAR/WinRAR. If current is UnRAR, look for 7z. if (extractorCommandKind(candidate) !== kind) continue;
if (currentIs7z && !isUnrarCommand(candidate)) continue;
if (!currentIs7z && !is7zCommand(candidate)) continue;
if (isAbsoluteCommand(candidate) && !fs.existsSync(candidate)) continue; if (isAbsoluteCommand(candidate) && !fs.existsSync(candidate)) continue;
const lower = candidate.toLowerCase(); const probe = await runExtractCommand(candidate, extractorProbeArgs(candidate), undefined, undefined, EXTRACTOR_PROBE_TIMEOUT_MS);
const probeArgs = (lower.includes("winrar") || lower.includes("unrar")) ? ["-?"] : ["?"];
const probe = await runExtractCommand(candidate, probeArgs, undefined, undefined, EXTRACTOR_PROBE_TIMEOUT_MS);
if (probe.ok || (!probe.missingCommand && !probe.timedOut)) { if (probe.ok || (!probe.missingCommand && !probe.timedOut)) {
return candidate; return candidate;
} }
} }
}
return null; return null;
} }
@ -2194,6 +2210,7 @@ async function runExternalExtractInner(
let bestPercent = 0; let bestPercent = 0;
let passwordAttempt = 0; let passwordAttempt = 0;
let usePerformanceFlags = externalExtractorSupportsPerfFlags && shouldUseExtractorPerformanceFlags(); let usePerformanceFlags = externalExtractorSupportsPerfFlags && shouldUseExtractorPerformanceFlags();
const summarizeResultError = (errorText: string): string => cleanErrorText(errorText).slice(0, 280);
// Skip normal extraction loop if flat mode is already known to be needed for this package // Skip normal extraction loop if flat mode is already known to be needed for this package
if (forceFlatMode) { if (forceFlatMode) {
@ -2275,6 +2292,13 @@ async function runExternalExtractInner(
`ms=${Date.now() - attemptStartedAt}, ok=${result.ok}, timedOut=${result.timedOut}, missingCommand=${result.missingCommand}, bestPercent=${bestPercent}` `ms=${Date.now() - attemptStartedAt}, ok=${result.ok}, timedOut=${result.timedOut}, missingCommand=${result.missingCommand}, bestPercent=${bestPercent}`
); );
onLog?.("INFO", `Legacy-Passwort-Versuch Ergebnis: archive=${path.basename(archivePath)}, attempt=${passwordAttempt}/${passwords.length}, ms=${Date.now() - attemptStartedAt}, ok=${result.ok}, timedOut=${result.timedOut}, missingCommand=${result.missingCommand}, bestPercent=${bestPercent}`); onLog?.("INFO", `Legacy-Passwort-Versuch Ergebnis: archive=${path.basename(archivePath)}, attempt=${passwordAttempt}/${passwords.length}, ms=${Date.now() - attemptStartedAt}, ok=${result.ok}, timedOut=${result.timedOut}, missingCommand=${result.missingCommand}, bestPercent=${bestPercent}`);
if (!result.ok) {
const errorSummary = summarizeResultError(result.errorText);
if (errorSummary) {
logger.info(`Legacy-Passwort-Versuch Fehlertext: archive=${path.basename(archivePath)}, attempt=${passwordAttempt}/${passwords.length}, extractor=${extractorName}, error=${errorSummary}`);
onLog?.("INFO", `Legacy-Passwort-Versuch Fehlertext: archive=${path.basename(archivePath)}, attempt=${passwordAttempt}/${passwords.length}, extractor=${extractorName}, error=${errorSummary}`);
}
}
if (result.ok) { if (result.ok) {
onArchiveProgress?.(100); onArchiveProgress?.(100);

View File

@ -68,6 +68,11 @@ describe("extractor", () => {
expect(unrarRename[2]).toBe("-p-"); expect(unrarRename[2]).toBe("-p-");
expect(unrarRename[3]).toBe("-y"); expect(unrarRename[3]).toBe("-y");
expect(unrarRename[unrarRename.length - 2]).toBe("archive.rar"); expect(unrarRename[unrarRename.length - 2]).toBe("archive.rar");
const rarCliArgs = buildExternalExtractArgs("Rar.exe", "archive.rar", "C:\\target", "overwrite", "serienjunkies.org");
expect(rarCliArgs.slice(0, 4)).toEqual(["x", "-o+", "-pserienjunkies.org", "-y"]);
expect(rarCliArgs[rarCliArgs.length - 2]).toBe("archive.rar");
expect(rarCliArgs[rarCliArgs.length - 1]).toBe("C:\\target\\");
}); });
it("deletes only successfully extracted archives", async () => { it("deletes only successfully extracted archives", async () => {
@ -1174,13 +1179,13 @@ describe("extractor", () => {
}); });
describe("orderExtractorCandidatesForArchive", () => { describe("orderExtractorCandidatesForArchive", () => {
it("prefers WinRAR/UnRAR over 7-Zip for rar archives", () => { it("prefers RAR-native CLIs over 7-Zip for rar archives", () => {
const ordered = orderExtractorCandidatesForArchive( const ordered = orderExtractorCandidatesForArchive(
["7z.exe", "UnRAR.exe", "WinRAR.exe"], ["7z.exe", "Rar.exe", "UnRAR.exe", "WinRAR.exe"],
"C:\\Downloads\\archive.part01.rar" "C:\\Downloads\\archive.part01.rar"
); );
expect(ordered.slice(0, 2)).toEqual(["UnRAR.exe", "WinRAR.exe"]); expect(ordered.slice(0, 3)).toEqual(["Rar.exe", "UnRAR.exe", "WinRAR.exe"]);
expect(ordered[2]).toBe("7z.exe"); expect(ordered[3]).toBe("7z.exe");
}); });
it("keeps 7-Zip first for non-rar archives", () => { it("keeps 7-Zip first for non-rar archives", () => {