Initial public release v1.7.233
This commit is contained in:
@@ -0,0 +1,391 @@
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import crypto from "node:crypto";
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
|
||||
type ReleaseVerification = {
|
||||
publish: {
|
||||
provider: string;
|
||||
owner: string;
|
||||
repo: string;
|
||||
};
|
||||
latestArtifact: string;
|
||||
missingArtifacts: string[];
|
||||
};
|
||||
|
||||
type CommandResult = {
|
||||
status: number | null;
|
||||
stdout?: string;
|
||||
stderr?: string;
|
||||
error?: Error;
|
||||
};
|
||||
|
||||
type ArchiveVerification = {
|
||||
verifiedArchives: string[];
|
||||
};
|
||||
|
||||
const verifierPath = path.resolve("scripts", "verify_public_release.mjs");
|
||||
const verifierUrl = "../scripts/verify_public_release.mjs";
|
||||
const { verifyPublicRelease, verifyReleaseArchives } = await import(verifierUrl) as {
|
||||
verifyPublicRelease: (rootDir: string) => ReleaseVerification;
|
||||
verifyReleaseArchives: (
|
||||
rootDir: string,
|
||||
options: {
|
||||
sevenZipPath: string;
|
||||
runCommand: (command: string, args: string[]) => CommandResult;
|
||||
}
|
||||
) => ArchiveVerification;
|
||||
};
|
||||
const fixtureRoots: string[] = [];
|
||||
const redistributionFiles = [
|
||||
"LICENSE",
|
||||
"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;
|
||||
|
||||
function writeFile(rootDir: string, relativePath: string, content: string | Buffer): void {
|
||||
const filePath = path.join(rootDir, ...relativePath.split("/"));
|
||||
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
||||
fs.writeFileSync(filePath, content);
|
||||
}
|
||||
|
||||
function writeRedistributionFiles(rootDir: string, packaged = false): void {
|
||||
for (const relativePath of redistributionFiles) {
|
||||
const content = fs.readFileSync(path.resolve(...relativePath.split("/")));
|
||||
let targetPath: string = relativePath;
|
||||
if (packaged && relativePath === "LICENSE") {
|
||||
targetPath = "win-unpacked/resources/LICENSE";
|
||||
} else if (packaged) {
|
||||
targetPath = `win-unpacked/resources/app.asar.unpacked/${relativePath}`;
|
||||
}
|
||||
writeFile(rootDir, targetPath, content);
|
||||
}
|
||||
}
|
||||
|
||||
function writeArchivePayload(outputDir: string, omittedName = ""): void {
|
||||
for (const relativePath of redistributionFiles) {
|
||||
if (path.basename(relativePath) === omittedName) {
|
||||
continue;
|
||||
}
|
||||
const content = fs.readFileSync(path.resolve(...relativePath.split("/")));
|
||||
const targetPath = relativePath === "LICENSE"
|
||||
? "resources/LICENSE"
|
||||
: `resources/app.asar.unpacked/${relativePath}`;
|
||||
writeFile(outputDir, targetPath, content);
|
||||
}
|
||||
}
|
||||
|
||||
function createArchiveCommandRunner(omittedName = "") {
|
||||
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);
|
||||
}
|
||||
return { status: command ? 0 : 2, stdout: "ok", stderr: "" };
|
||||
};
|
||||
}
|
||||
|
||||
function createReleaseFixture(): string {
|
||||
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "public-release-metadata-"));
|
||||
fixtureRoots.push(rootDir);
|
||||
const setupPayload = Buffer.from("setup");
|
||||
const setupSha512 = crypto.createHash("sha512").update(setupPayload).digest("base64");
|
||||
|
||||
writeFile(rootDir, "package.json", `${JSON.stringify({
|
||||
name: "real-debrid-downloader",
|
||||
version: "1.7.233",
|
||||
build: {
|
||||
productName: "Real-Debrid-Downloader",
|
||||
publish: {
|
||||
provider: "github",
|
||||
owner: "Sucukdeluxe",
|
||||
repo: "multi-debrid-downloader"
|
||||
},
|
||||
files: [
|
||||
"build/main/**/*",
|
||||
"build/renderer/**/*",
|
||||
"resources/extractor-jvm/**/*",
|
||||
"LICENSE",
|
||||
"package.json"
|
||||
],
|
||||
extraResources: [
|
||||
{
|
||||
from: "LICENSE",
|
||||
to: "LICENSE"
|
||||
}
|
||||
],
|
||||
nsis: {
|
||||
artifactName: "${productName}-Setup-${version}.${ext}",
|
||||
oneClick: false,
|
||||
perMachine: false,
|
||||
allowToChangeInstallationDirectory: true,
|
||||
createDesktopShortcut: true
|
||||
},
|
||||
portable: {
|
||||
artifactName: "${productName}-${version}-portable.${ext}"
|
||||
}
|
||||
}
|
||||
}, null, 2)}\n`);
|
||||
writeFile(
|
||||
rootDir,
|
||||
"latest.yml",
|
||||
`version: 1.7.233\nfiles:\n - url: Real-Debrid-Downloader-Setup-1.7.233.exe\n sha512: ${setupSha512}\n size: ${setupPayload.length}\npath: Real-Debrid-Downloader-Setup-1.7.233.exe\nsha512: ${setupSha512}\n`
|
||||
);
|
||||
writeFile(
|
||||
rootDir,
|
||||
"win-unpacked/resources/app-update.yml",
|
||||
"provider: github\nowner: Sucukdeluxe\nrepo: multi-debrid-downloader\n"
|
||||
);
|
||||
writeFile(rootDir, "Real-Debrid-Downloader-Setup-1.7.233.exe", setupPayload);
|
||||
writeFile(rootDir, "Real-Debrid-Downloader-Setup-1.7.233.exe.blockmap", "blockmap");
|
||||
writeFile(rootDir, "Real-Debrid-Downloader-1.7.233-portable.exe", "portable");
|
||||
writeRedistributionFiles(rootDir);
|
||||
writeRedistributionFiles(rootDir, true);
|
||||
|
||||
return rootDir;
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
for (const rootDir of fixtureRoots.splice(0)) {
|
||||
fs.rmSync(rootDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
describe("public release metadata", () => {
|
||||
it("accepts the canonical GitHub release metadata and artifacts", () => {
|
||||
const rootDir = createReleaseFixture();
|
||||
|
||||
const result = verifyPublicRelease(rootDir);
|
||||
|
||||
expect(result.publish).toEqual({
|
||||
provider: "github",
|
||||
owner: "Sucukdeluxe",
|
||||
repo: "multi-debrid-downloader"
|
||||
});
|
||||
expect(result.latestArtifact).toBe("Real-Debrid-Downloader-Setup-1.7.233.exe");
|
||||
expect(result.missingArtifacts).toEqual([]);
|
||||
});
|
||||
|
||||
it("rejects a package configured for a different GitHub owner", () => {
|
||||
const rootDir = createReleaseFixture();
|
||||
const packagePath = path.join(rootDir, "package.json");
|
||||
const packageJson = JSON.parse(fs.readFileSync(packagePath, "utf8"));
|
||||
packageJson.build.publish.owner = "DifferentOwner";
|
||||
fs.writeFileSync(packagePath, `${JSON.stringify(packageJson, null, 2)}\n`);
|
||||
|
||||
expect(() => verifyPublicRelease(rootDir)).toThrow(/owner/i);
|
||||
});
|
||||
|
||||
it("rejects a latest.yml path whose artifact does not exist", () => {
|
||||
const rootDir = createReleaseFixture();
|
||||
fs.rmSync(path.join(rootDir, "Real-Debrid-Downloader-Setup-1.7.233.exe"));
|
||||
|
||||
expect(() => verifyPublicRelease(rootDir)).toThrow(/Real-Debrid-Downloader-Setup-1\.7\.233\.exe/);
|
||||
});
|
||||
|
||||
it("rejects syntactically invalid latest.yml", () => {
|
||||
const rootDir = createReleaseFixture();
|
||||
fs.writeFileSync(
|
||||
path.join(rootDir, "latest.yml"),
|
||||
"version: 1.7.233\nfiles: [\npath: Real-Debrid-Downloader-Setup-1.7.233.exe\n"
|
||||
);
|
||||
|
||||
expect(() => verifyPublicRelease(rootDir)).toThrow(/latest\.yml|yaml/i);
|
||||
});
|
||||
|
||||
it("rejects a noncanonical files entry in latest.yml", () => {
|
||||
const rootDir = createReleaseFixture();
|
||||
const latestPath = path.join(rootDir, "latest.yml");
|
||||
const latest = fs.readFileSync(latestPath, "utf8").replace(
|
||||
"url: Real-Debrid-Downloader-Setup-1.7.233.exe",
|
||||
"url: Different-Setup-1.7.233.exe"
|
||||
);
|
||||
fs.writeFileSync(latestPath, latest);
|
||||
|
||||
expect(() => verifyPublicRelease(rootDir)).toThrow(/files|url|canonical/i);
|
||||
});
|
||||
|
||||
it("rejects a latest.yml SHA512 digest that does not match the installer", () => {
|
||||
const rootDir = createReleaseFixture();
|
||||
const latestPath = path.join(rootDir, "latest.yml");
|
||||
const latest = fs.readFileSync(latestPath, "utf8");
|
||||
const wrongDigest = Buffer.alloc(64, 0x23).toString("base64");
|
||||
fs.writeFileSync(latestPath, latest.replace(/sha512: [^\n]+/g, `sha512: ${wrongDigest}`));
|
||||
|
||||
expect(() => verifyPublicRelease(rootDir)).toThrow(/sha512|digest|integrity/i);
|
||||
});
|
||||
|
||||
it("rejects a directory in place of an artifact file", () => {
|
||||
const rootDir = createReleaseFixture();
|
||||
const setupPath = path.join(rootDir, "Real-Debrid-Downloader-Setup-1.7.233.exe");
|
||||
fs.rmSync(setupPath);
|
||||
fs.mkdirSync(setupPath);
|
||||
|
||||
expect(() => verifyPublicRelease(rootDir)).toThrow(/artifact|file/i);
|
||||
});
|
||||
|
||||
it("rejects an empty artifact file", () => {
|
||||
const rootDir = createReleaseFixture();
|
||||
fs.writeFileSync(path.join(rootDir, "Real-Debrid-Downloader-1.7.233-portable.exe"), "");
|
||||
|
||||
expect(() => verifyPublicRelease(rootDir)).toThrow(/artifact|empty|file/i);
|
||||
});
|
||||
|
||||
it("rejects a release missing a declared redistribution license", () => {
|
||||
const rootDir = createReleaseFixture();
|
||||
fs.rmSync(path.join(rootDir, "resources", "extractor-jvm", "licenses", "Apache-2.0.txt"));
|
||||
|
||||
expect(() => verifyPublicRelease(rootDir)).toThrow(/Apache-2\.0\.txt/);
|
||||
});
|
||||
|
||||
it("rejects a modified official license text", () => {
|
||||
const rootDir = createReleaseFixture();
|
||||
fs.appendFileSync(
|
||||
path.join(rootDir, "resources", "extractor-jvm", "licenses", "LGPL-2.1.txt"),
|
||||
"modified"
|
||||
);
|
||||
|
||||
expect(() => verifyPublicRelease(rootDir)).toThrow(/LGPL-2\.1\.txt|digest|content/i);
|
||||
});
|
||||
|
||||
it("rejects swapped third-party license assignments", () => {
|
||||
const rootDir = createReleaseFixture();
|
||||
const noticePath = path.join(rootDir, "resources", "extractor-jvm", "THIRD_PARTY_NOTICES.txt");
|
||||
const notice = fs.readFileSync(noticePath, "utf8")
|
||||
.replace("GNU Lesser General Public License 2.1 or later", "Apache License 2.0")
|
||||
.replace("licenses/LGPL-2.1.txt; licenses/7-Zip-license.txt", "licenses/Apache-2.0.txt");
|
||||
fs.writeFileSync(noticePath, notice);
|
||||
|
||||
expect(() => verifyPublicRelease(rootDir)).toThrow(/THIRD_PARTY_NOTICES|notice|digest|mapping/i);
|
||||
});
|
||||
|
||||
it("rejects a release whose unpacked application omits a license", () => {
|
||||
const rootDir = createReleaseFixture();
|
||||
fs.rmSync(path.join(
|
||||
rootDir,
|
||||
"win-unpacked",
|
||||
"resources",
|
||||
"app.asar.unpacked",
|
||||
"resources",
|
||||
"extractor-jvm",
|
||||
"licenses",
|
||||
"Apache-2.0.txt"
|
||||
));
|
||||
|
||||
expect(() => verifyPublicRelease(rootDir)).toThrow(/win-unpacked|Apache-2\.0\.txt|packaged/i);
|
||||
});
|
||||
|
||||
it("rejects a symlink in place of a release artifact", () => {
|
||||
const rootDir = createReleaseFixture();
|
||||
const setupPath = path.join(rootDir, "Real-Debrid-Downloader-Setup-1.7.233.exe");
|
||||
const targetPath = path.join(rootDir, "setup-target.exe");
|
||||
fs.renameSync(setupPath, targetPath);
|
||||
fs.symlinkSync(targetPath, setupPath, "file");
|
||||
|
||||
expect(() => verifyPublicRelease(rootDir)).toThrow(/symbolic|symlink|regular file/i);
|
||||
});
|
||||
|
||||
it("rejects a symlink in place of an official license", () => {
|
||||
const rootDir = createReleaseFixture();
|
||||
const licensePath = path.join(rootDir, "resources", "extractor-jvm", "licenses", "Apache-2.0.txt");
|
||||
const targetPath = path.join(rootDir, "Apache-target.txt");
|
||||
fs.renameSync(licensePath, targetPath);
|
||||
fs.symlinkSync(targetPath, licensePath, "file");
|
||||
|
||||
expect(() => verifyPublicRelease(rootDir)).toThrow(/symbolic|symlink|regular file/i);
|
||||
});
|
||||
|
||||
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/);
|
||||
});
|
||||
|
||||
it("rejects build metadata that does not copy the project license into resources", () => {
|
||||
const rootDir = createReleaseFixture();
|
||||
const packagePath = path.join(rootDir, "package.json");
|
||||
const packageJson = JSON.parse(fs.readFileSync(packagePath, "utf8"));
|
||||
delete packageJson.build.extraResources;
|
||||
fs.writeFileSync(packagePath, `${JSON.stringify(packageJson, null, 2)}\n`);
|
||||
|
||||
expect(() => verifyPublicRelease(rootDir)).toThrow(/extraResources|LICENSE/);
|
||||
});
|
||||
|
||||
it("rejects incomplete third-party redistribution notices", () => {
|
||||
const rootDir = createReleaseFixture();
|
||||
fs.writeFileSync(
|
||||
path.join(rootDir, "resources", "extractor-jvm", "THIRD_PARTY_NOTICES.txt"),
|
||||
"net.sf.sevenzipjbinding:sevenzipjbinding:16.02-2.01 LGPL-2.1.txt\n"
|
||||
);
|
||||
|
||||
expect(() => verifyPublicRelease(rootDir)).toThrow(/THIRD_PARTY_NOTICES|notice|content/i);
|
||||
});
|
||||
|
||||
it("returns a nonzero CLI status for invalid release metadata", () => {
|
||||
const rootDir = createReleaseFixture();
|
||||
const packagePath = path.join(rootDir, "package.json");
|
||||
const packageJson = JSON.parse(fs.readFileSync(packagePath, "utf8"));
|
||||
packageJson.build.publish.owner = "DifferentOwner";
|
||||
fs.writeFileSync(packagePath, `${JSON.stringify(packageJson, null, 2)}\n`);
|
||||
|
||||
const result = spawnSync(process.execPath, [verifierPath, rootDir], {
|
||||
encoding: "utf8"
|
||||
});
|
||||
|
||||
expect(result.status).not.toBe(0);
|
||||
expect(result.stderr).toMatch(/owner/i);
|
||||
});
|
||||
|
||||
it("recursively verifies redistribution files inside setup and portable archives", () => {
|
||||
const rootDir = createReleaseFixture();
|
||||
const result = verifyReleaseArchives(rootDir, {
|
||||
sevenZipPath: "C:\\Tools\\7-Zip\\7z.exe",
|
||||
runCommand: createArchiveCommandRunner()
|
||||
});
|
||||
|
||||
expect(result.verifiedArchives).toEqual([
|
||||
"Real-Debrid-Downloader-Setup-1.7.233.exe",
|
||||
"Real-Debrid-Downloader-1.7.233-portable.exe"
|
||||
]);
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
it("exposes archive verification as a nonzero CLI gate", () => {
|
||||
const rootDir = createReleaseFixture();
|
||||
const result = spawnSync(process.execPath, [
|
||||
verifierPath,
|
||||
rootDir,
|
||||
"--verify-archives",
|
||||
"--seven-zip",
|
||||
path.join(rootDir, "missing-7z.exe")
|
||||
], {
|
||||
encoding: "utf8"
|
||||
});
|
||||
|
||||
expect(result.status).not.toBe(0);
|
||||
expect(result.stderr).toMatch(/7-Zip|command|spawn/i);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user