fix: restore public file streaming and contain ZIP failures

This commit is contained in:
Thomas Durieux
2026-09-14 15:06:29 +00:00
parent ecba15ce2e
commit c9c729c2c5
7 changed files with 226 additions and 22 deletions
+91
View File
@@ -0,0 +1,91 @@
const { expect } = require("chai");
const { Readable, PassThrough } = require("stream");
const { setImmediate } = require("timers");
const { once } = require("events");
const vm = require("vm");
const got = require("got");
const archiver = require("archiver");
require("ts-node/register/transpile-only");
const GitHubDownload = require("../src/core/source/GitHubDownload").default;
const { AnonymizeTransformer } = require("../src/core/anonymize-utils");
const { streamAnonymizedZip } = require("../src/core/zipStream");
async function fixture() {
const zip = archiver("zip");
const chunks = [];
zip.on("data", chunk => chunks.push(chunk));
const finished = once(zip, "end");
zip.append("private source content", { name: "repo/README.md" });
await zip.finalize();
await finished;
return Buffer.concat(chunks);
}
describe("ZIP stream errors", function () {
it("finishes a valid ZIP with anonymized file content", async function () {
const input = await fixture();
const previous = { stream: got.stream, zip: GitHubDownload.prototype.getZipUrl };
const response = new PassThrough();
const parser = require("unzip-stream").Parse();
const files = [];
const entries = [];
parser.on("entry", entry => {
const chunks = [];
entry.on("data", chunk => chunks.push(chunk));
entries.push(once(entry, "end").then(() => files.push({ name: entry.path, content: Buffer.concat(chunks).toString() })));
});
const finished = once(parser, "finish");
response.pipe(parser);
try {
GitHubDownload.prototype.getZipUrl = async () => ({ url: "https://example.test/archive.zip" });
got.stream = () => Readable.from([input]);
await streamAnonymizedZip({
repoId: "test", organization: "owner", repoName: "public", commit: "abc",
getToken: () => "", anonymizerOptions: { terms: ["private"], image: true, link: true },
}, response);
await finished;
await Promise.all(entries);
expect(files).to.deep.equal([{ name: "README.md", content: "XXXX-1 source content" }]);
} finally {
got.stream = previous.stream;
GitHubDownload.prototype.getZipUrl = previous.zip;
response.destroy();
}
});
it("aborts a download on an asynchronous anonymization timeout without crashing", async function () {
const input = await fixture();
const previous = { stream: got.stream, zip: GitHubDownload.prototype.getZipUrl, flush: AnonymizeTransformer.prototype._flush };
const response = new PassThrough();
const errors = [];
const chunks = [];
response.on("error", error => errors.push(error));
response.on("data", chunk => chunks.push(chunk));
const closed = new Promise(resolve => response.once("close", resolve));
const timeout = vm.runInNewContext('Object.assign(new Error("Script execution timed out after 1000ms"), { code: "ERR_SCRIPT_EXECUTION_TIMEOUT" })');
try {
GitHubDownload.prototype.getZipUrl = async () => ({ url: "https://example.test/archive.zip" });
got.stream = () => Readable.from([input]);
// VM timeout errors come from another realm and can arrive after the ZIP
// parser finishes, while archiver is still consuming the entry stream.
AnonymizeTransformer.prototype._flush = function (callback) {
setImmediate(() => callback(timeout));
};
await streamAnonymizedZip({
repoId: "test", organization: "owner", repoName: "public", commit: "abc",
getToken: () => "", anonymizerOptions: { terms: ["private"], image: true, link: true },
}, response);
await closed;
await new Promise(resolve => setImmediate(resolve));
expect(response.destroyed).to.equal(true);
expect(errors).to.have.length(1);
expect(errors[0].code).to.equal("ERR_SCRIPT_EXECUTION_TIMEOUT");
expect(Buffer.concat(chunks).includes(Buffer.from("private source content"))).to.equal(false);
} finally {
got.stream = previous.stream;
GitHubDownload.prototype.getZipUrl = previous.zip;
AnonymizeTransformer.prototype._flush = previous.flush;
response.destroy();
}
});
});