release: harden archive recovery and lifecycle for v2.0.63
Corroborate CRC failures across extraction backends, retry only implicated multipart volumes, and distinguish corruption, missing volumes, I/O failures, and wrong passwords across native, Zip4j, and JBinding paths. Make disk retries generation-safe, preserve selective run scopes and cooldowns, protect shared output files during cleanup, validate manual extraction batches atomically, and restore interrupted integrity work safely. Prioritize active package operations in the UI, strengthen extraction IPC validation, compile the JVM sidecar before release builds, and verify shipped JVM resources byte-for-byte in every Windows artifact.
This commit is contained in:
@@ -58,14 +58,22 @@ async function createFixtureAsar(version: string): Promise<Buffer> {
|
||||
}
|
||||
const validAppAsar = await createFixtureAsar("1.7.233");
|
||||
const staleAppAsar = await createFixtureAsar("1.7.232");
|
||||
const redistributionFiles = [
|
||||
const redistributionFiles = [
|
||||
"LICENSE",
|
||||
"THIRD_PARTY_NOTICES.md",
|
||||
"resources/extractor-jvm/licenses/LGPL-2.1.txt",
|
||||
"resources/extractor-jvm/licenses/7-Zip-license.txt",
|
||||
"resources/extractor-jvm/licenses/Apache-2.0.txt",
|
||||
"resources/extractor-jvm/THIRD_PARTY_NOTICES.txt"
|
||||
] as const;
|
||||
] as const;
|
||||
const jvmRuntimeFiles = Object.freeze({
|
||||
"resources/extractor-jvm/classes/.source.sha256": "source-digest\n",
|
||||
"resources/extractor-jvm/classes/com/sucukdeluxe/extractor/JBindExtractorMain.class": Buffer.from([0xca, 0xfe, 0xba, 0xbe, 0x01]),
|
||||
"resources/extractor-jvm/classes/com/sucukdeluxe/extractor/JBindExtractorMain$Backend.class": Buffer.from([0xca, 0xfe, 0xba, 0xbe, 0x02]),
|
||||
"resources/extractor-jvm/lib/sevenzipjbinding.jar": Buffer.from("sevenzip-binding"),
|
||||
"resources/extractor-jvm/lib/sevenzipjbinding-all-platforms.jar": Buffer.from("sevenzip-platforms"),
|
||||
"resources/extractor-jvm/lib/zip4j.jar": Buffer.from("zip4j")
|
||||
});
|
||||
|
||||
function writeFile(rootDir: string, relativePath: string, content: string | Buffer): void {
|
||||
const filePath = path.join(rootDir, ...relativePath.split("/"));
|
||||
@@ -73,7 +81,7 @@ function writeFile(rootDir: string, relativePath: string, content: string | Buff
|
||||
fs.writeFileSync(filePath, content);
|
||||
}
|
||||
|
||||
function writeRedistributionFiles(rootDir: string, packaged = false): void {
|
||||
function writeRedistributionFiles(rootDir: string, packaged = false): void {
|
||||
for (const relativePath of redistributionFiles) {
|
||||
const content = fs.readFileSync(path.resolve(...relativePath.split("/")));
|
||||
let targetPath: string = relativePath;
|
||||
@@ -104,21 +112,44 @@ function writeArchivePayload(outputDir: string, omittedName = ""): void {
|
||||
if (omittedName !== "app_icon.ico") {
|
||||
writeFile(outputDir, "resources/assets/app_icon.ico", "application-icon");
|
||||
}
|
||||
for (const [relativePath, content] of Object.entries(jvmRuntimeFiles)) {
|
||||
if (path.basename(relativePath) !== omittedName) {
|
||||
writeFile(outputDir, `resources/app.asar.unpacked/${relativePath}`, content);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function writeJvmRuntimeFiles(rootDir: string, packaged = false): void {
|
||||
for (const [relativePath, content] of Object.entries(jvmRuntimeFiles)) {
|
||||
const targetPath = packaged
|
||||
? `win-unpacked/resources/app.asar.unpacked/${relativePath}`
|
||||
: relativePath;
|
||||
writeFile(rootDir, targetPath, content);
|
||||
}
|
||||
}
|
||||
|
||||
function createArchiveCommandRunner(omittedName = "") {
|
||||
function createArchiveCommandRunner(omittedName = "", corruptArchiveName = "") {
|
||||
let currentArchiveName = "";
|
||||
return (command: string, args: string[]): CommandResult => {
|
||||
const archivePath = args[1] || "";
|
||||
const outputArg = args.find((arg) => arg.startsWith("-o"));
|
||||
if (!outputArg) {
|
||||
return { status: 2, stderr: "missing output directory" };
|
||||
}
|
||||
const outputDir = outputArg.slice(2);
|
||||
if (archivePath.toLowerCase().endsWith(".exe")) {
|
||||
writeFile(outputDir, "payload/app-64.7z", "nested archive");
|
||||
} else if (archivePath.toLowerCase().endsWith(".7z")) {
|
||||
writeArchivePayload(outputDir, omittedName);
|
||||
}
|
||||
const outputDir = outputArg.slice(2);
|
||||
if (archivePath.toLowerCase().endsWith(".exe")) {
|
||||
currentArchiveName = path.basename(archivePath);
|
||||
writeFile(outputDir, "payload/app-64.7z", "nested archive");
|
||||
} else if (archivePath.toLowerCase().endsWith(".7z")) {
|
||||
writeArchivePayload(outputDir, omittedName);
|
||||
if (currentArchiveName === corruptArchiveName) {
|
||||
writeFile(
|
||||
outputDir,
|
||||
"resources/app.asar.unpacked/resources/extractor-jvm/lib/zip4j.jar",
|
||||
"corrupt-zip4j"
|
||||
);
|
||||
}
|
||||
}
|
||||
return { status: command ? 0 : 2, stdout: "ok", stderr: "" };
|
||||
};
|
||||
}
|
||||
@@ -139,15 +170,18 @@ function createReleaseFixture(): string {
|
||||
owner: "Sucukdeluxe",
|
||||
repo: "Multi-Debrid-Downloader"
|
||||
},
|
||||
files: [
|
||||
files: [
|
||||
"build/main/**/*",
|
||||
"build/renderer/**/*",
|
||||
"resources/extractor-jvm/**/*",
|
||||
"LICENSE",
|
||||
"THIRD_PARTY_NOTICES.md",
|
||||
"package.json"
|
||||
],
|
||||
extraResources: [
|
||||
"package.json"
|
||||
],
|
||||
asarUnpack: [
|
||||
"resources/extractor-jvm/**/*"
|
||||
],
|
||||
extraResources: [
|
||||
{
|
||||
from: "LICENSE",
|
||||
to: "LICENSE"
|
||||
@@ -189,6 +223,8 @@ function createReleaseFixture(): string {
|
||||
writeFile(rootDir, "Multi-Debrid-Downloader-1.7.233-portable.exe", "portable");
|
||||
writeRedistributionFiles(rootDir);
|
||||
writeRedistributionFiles(rootDir, true);
|
||||
writeJvmRuntimeFiles(rootDir);
|
||||
writeJvmRuntimeFiles(rootDir, true);
|
||||
writeFile(rootDir, "assets/app_icon.ico", "application-icon");
|
||||
writeFile(rootDir, "win-unpacked/resources/assets/app_icon.ico", "application-icon");
|
||||
writeFile(rootDir, "win-unpacked/resources/app.asar", validAppAsar);
|
||||
@@ -347,15 +383,74 @@ describe("public release metadata", () => {
|
||||
expect(() => verifyPublicRelease(rootDir)).toThrow(/symbolic|symlink|regular file/i);
|
||||
});
|
||||
|
||||
it("rejects build metadata that omits the project license", () => {
|
||||
it("rejects build metadata that omits the project license", () => {
|
||||
const rootDir = createReleaseFixture();
|
||||
const packagePath = path.join(rootDir, "package.json");
|
||||
const packageJson = JSON.parse(fs.readFileSync(packagePath, "utf8"));
|
||||
packageJson.build.files = packageJson.build.files.filter((entry: string) => entry !== "LICENSE");
|
||||
fs.writeFileSync(packagePath, `${JSON.stringify(packageJson, null, 2)}\n`);
|
||||
|
||||
expect(() => verifyPublicRelease(rootDir)).toThrow(/LICENSE/);
|
||||
});
|
||||
expect(() => verifyPublicRelease(rootDir)).toThrow(/LICENSE/);
|
||||
});
|
||||
|
||||
it("rejects build metadata that does not unpack the JVM runtime", () => {
|
||||
const rootDir = createReleaseFixture();
|
||||
const packagePath = path.join(rootDir, "package.json");
|
||||
const packageJson = JSON.parse(fs.readFileSync(packagePath, "utf8"));
|
||||
delete packageJson.build.asarUnpack;
|
||||
fs.writeFileSync(packagePath, `${JSON.stringify(packageJson, null, 2)}\n`);
|
||||
|
||||
expect(() => verifyPublicRelease(rootDir)).toThrow(/asarUnpack|JVM runtime/i);
|
||||
});
|
||||
|
||||
it("rejects a missing JVM class in the unpacked application", () => {
|
||||
const rootDir = createReleaseFixture();
|
||||
fs.rmSync(path.join(
|
||||
rootDir,
|
||||
"win-unpacked",
|
||||
"resources",
|
||||
"app.asar.unpacked",
|
||||
"resources",
|
||||
"extractor-jvm",
|
||||
"classes",
|
||||
"com",
|
||||
"sucukdeluxe",
|
||||
"extractor",
|
||||
"JBindExtractorMain$Backend.class"
|
||||
));
|
||||
|
||||
expect(() => verifyPublicRelease(rootDir)).toThrow(/JVM runtime|Backend\.class|missing/i);
|
||||
});
|
||||
|
||||
it("rejects changed JVM bytecode in the unpacked application", () => {
|
||||
const rootDir = createReleaseFixture();
|
||||
fs.writeFileSync(path.join(
|
||||
rootDir,
|
||||
"win-unpacked",
|
||||
"resources",
|
||||
"app.asar.unpacked",
|
||||
"resources",
|
||||
"extractor-jvm",
|
||||
"classes",
|
||||
"com",
|
||||
"sucukdeluxe",
|
||||
"extractor",
|
||||
"JBindExtractorMain.class"
|
||||
), "stale-bytecode");
|
||||
|
||||
expect(() => verifyPublicRelease(rootDir)).toThrow(/JVM runtime|JBindExtractorMain\.class|SHA-?256|content/i);
|
||||
});
|
||||
|
||||
it("rejects stale extra JVM bytecode in the unpacked application", () => {
|
||||
const rootDir = createReleaseFixture();
|
||||
writeFile(
|
||||
rootDir,
|
||||
"win-unpacked/resources/app.asar.unpacked/resources/extractor-jvm/classes/com/sucukdeluxe/extractor/Stale.class",
|
||||
"stale-bytecode"
|
||||
);
|
||||
|
||||
expect(() => verifyPublicRelease(rootDir)).toThrow(/JVM runtime|Stale\.class|unexpected/i);
|
||||
});
|
||||
|
||||
it("rejects build metadata that does not copy the project license into resources", () => {
|
||||
const rootDir = createReleaseFixture();
|
||||
@@ -429,14 +524,26 @@ describe("public release metadata", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("rejects an archive whose nested application payload omits a license", () => {
|
||||
it("rejects an archive whose nested application payload omits a license", () => {
|
||||
const rootDir = createReleaseFixture();
|
||||
|
||||
expect(() => verifyReleaseArchives(rootDir, {
|
||||
sevenZipPath: "C:\\Tools\\7-Zip\\7z.exe",
|
||||
runCommand: createArchiveCommandRunner("Apache-2.0.txt")
|
||||
})).toThrow(/Apache-2\.0\.txt|missing redistribution file/i);
|
||||
});
|
||||
})).toThrow(/Apache-2\.0\.txt|missing redistribution file/i);
|
||||
});
|
||||
|
||||
it("rejects a portable archive whose JVM runtime differs from the repository", () => {
|
||||
const rootDir = createReleaseFixture();
|
||||
|
||||
expect(() => verifyReleaseArchives(rootDir, {
|
||||
sevenZipPath: "C:\\Tools\\7-Zip\\7z.exe",
|
||||
runCommand: createArchiveCommandRunner(
|
||||
"",
|
||||
"Multi-Debrid-Downloader-1.7.233-portable.exe"
|
||||
)
|
||||
})).toThrow(/portable|JVM runtime|zip4j\.jar|SHA-?256|content/i);
|
||||
});
|
||||
|
||||
it("exposes archive verification as a nonzero CLI gate", () => {
|
||||
const rootDir = createReleaseFixture();
|
||||
|
||||
Reference in New Issue
Block a user