mirror of
https://github.com/tdurieux/anonymous_github.git
synced 2026-10-19 14:17:20 +02:00
fix: avoid timeouts for safe ZIP anonymization (#873)
This commit is contained in:
1 parent
40c369641b
commit
4fd31a59c8
7 files changed
+159
-12
No files matched your search
@@ -39,4 +39,31 @@ describe("literal term presence checks", function () {
|
||||
expect(optimized.wasAnonymized).to.equal(reference.wasAnonymized);
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps required-prefix absence checks conservative for arbitrary regex syntax", function () {
|
||||
const cases = [
|
||||
{ terms: ["Researcher[0-9]+"], text: "absent name Researcher12 Résearcher42" },
|
||||
{ terms: ["foo?bar"], text: "foobar fobar" },
|
||||
{ terms: ["foo*bar"], text: "foobar fobar foobarbar" },
|
||||
{ terms: ["foo{0,2}bar"], text: "fobar foobar fooobar" },
|
||||
{ terms: ["foo+bar"], text: "foobar fooobar" },
|
||||
{ terms: ["a?", "[ab]*", "[ab]{0,2}"], text: "x a ab" },
|
||||
{ terms: ["foo[a]{0}bar", "[ab]?foo"], text: "foobar foo afoo" },
|
||||
{ terms: ["😀?bar"], text: "bar 😀bar" },
|
||||
{ terms: ["foo|bar", "foo(bar|baz)"], text: "foo bar foobar foobaz" },
|
||||
{ terms: ["foo\\|bar", "foo\\.bar"], text: "foo|bar foo.bar" },
|
||||
{ terms: ["^foo", "(?:foo)?bar", "[A-Z]+foo"], text: "foo bar XXfoo" },
|
||||
{ terms: ["k[0-9]+", "s[0-9]+"], text: "k12 K12 s34 ſ34 ék12" },
|
||||
{ terms: ["😀[0-9]+", "研究[0-9]+"], text: "😀12 研究34" },
|
||||
{ terms: ["Alice=>Researcher12", "Researcher[0-9]+=>Hidden"], text: "Alice" },
|
||||
{ terms: ["Researcher[0-9]+"], text: "https://host/Researcher12/path" },
|
||||
];
|
||||
for (const { terms, text } of cases) {
|
||||
const reference = new ContentAnonimizer({ terms });
|
||||
for (const term of reference.compiledTerms) delete term.requiredPrefix;
|
||||
const optimized = new ContentAnonimizer({ terms });
|
||||
expect(optimized.anonymize(text), JSON.stringify(terms)).to.equal(reference.anonymize(text));
|
||||
expect(optimized.wasAnonymized).to.equal(reference.wasAnonymized);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -160,6 +160,19 @@ describe("large notebook downloads", function () {
|
||||
expect(JSON.parse(entries["example.ipynb"].toString()).cells[0].source[0]).to.equal("# XXXX-1's experiment");
|
||||
});
|
||||
|
||||
it("completes large safe regex entries and continues to the next ZIP entry", async function () {
|
||||
const payload = "123,456,78.91234,-45.123456\n".repeat(280000);
|
||||
upstream = Readable.from([await zip({ "root/large.csv": `Alice\n${payload}Alice`, "root/after.txt": "Alice" })]);
|
||||
const response = new PassThrough(), output = collect(response);
|
||||
await streamAnonymizedZip({ ...options, anonymizerOptions: {
|
||||
terms: ["Alice", ...Array.from({ length: 8 }, (_, i) => `Researcher${i}[0-9]+`)],
|
||||
} }, response);
|
||||
const entries = await unzip(await output);
|
||||
expect(entries["large.csv"].toString()).to.equal(`XXXX-1\n${payload}XXXX-1`);
|
||||
expect(entries["after.txt"].toString()).to.equal("XXXX-1");
|
||||
expect(response.writableFinished).to.equal(true);
|
||||
});
|
||||
|
||||
it("aborts the response when an entry cannot be anonymized", async function () {
|
||||
upstream = Readable.from([await zip({ "root/large.txt": "Alice".repeat(1000) })]);
|
||||
config.MAX_FILE_SIZE = 100;
|
||||
|
||||
@@ -59,6 +59,44 @@ describe("large anonymization resources", function () {
|
||||
const options = { filePath: `recovery-${randomUUID()}.txt`, terms: ["Alice=>Hidden"] };
|
||||
expect(await transform(text, options)).to.equal(new ContentAnonimizer(options).anonymize(text));
|
||||
});
|
||||
it("finishes large files containing many safe regex rules", async () => {
|
||||
const payload = "123,456,78.91234,-45.123456\n".repeat(280000);
|
||||
const terms = ["Alice", ...Array.from({ length: 8 }, (_, i) => `Researcher${i}[0-9]+`)];
|
||||
const output = await transform(`Alice\n${payload}Alice`, {
|
||||
filePath: `safe-regex-${randomUUID()}.csv`, terms,
|
||||
});
|
||||
expect(output).to.equal(`XXXX-1\n${payload}XXXX-1`);
|
||||
});
|
||||
|
||||
it("uses the worker budget for dense Unicode replacements", async () => {
|
||||
const input = "Álice,123,45.678,-23.456\n".repeat(800000);
|
||||
expect(await transform(input, { filePath: `dense-unicode-${randomUUID()}.csv`, terms: ["Alice"] }))
|
||||
.to.equal("XXXX-1,123,45.678,-23.456\n".repeat(800000));
|
||||
});
|
||||
|
||||
it("retains the short deadline for backtracking regexes on a worker and recovers", async () => {
|
||||
const root = await fs.mkdtemp(join(tmpdir(), "worker-regex-deadline-"));
|
||||
const input = join(root, "input"), output = join(root, "output");
|
||||
const controller = new AbortController();
|
||||
try {
|
||||
const text = "a".repeat(250);
|
||||
await fs.writeFile(input, text);
|
||||
let failure;
|
||||
const started = Date.now();
|
||||
try {
|
||||
await pool.anonymizeOnWorker({ input, output, options: { terms: ["a+a+a+a+a+a+b(?=x)"] },
|
||||
maxOutput: 1000000, context: { mask: "XXXX", hostname: "localhost" } }, text.length, controller.signal);
|
||||
} catch (error) { failure = error; }
|
||||
expect(failure).to.be.instanceOf(Error);
|
||||
expect(failure.message).to.match(/timed out after 1000ms/);
|
||||
expect(Date.now() - started).to.be.lessThan(8000);
|
||||
let exists = true;
|
||||
try { await fs.access(output); } catch (error) { if (error.code === "ENOENT") exists = false; else throw error; }
|
||||
expect(exists).to.equal(false);
|
||||
expect(await transform("Alice ".repeat(50000), { filePath: `regex-recovery-${randomUUID()}.txt`, terms: ["Alice"] }))
|
||||
.to.equal("XXXX-1 ".repeat(50000));
|
||||
} finally { controller.abort(); await fs.rm(root, { force: true, recursive: true }); }
|
||||
});
|
||||
it("cleans crash leftovers while preserving recent spools and unrelated files", async () => {
|
||||
const root = await fs.mkdtemp(join(tmpdir(), "anonymous-spool-test-"));
|
||||
try {
|
||||
|
||||
Reference in new issue
Block a user