test(coding-agent): covered docs.rs gunzip cap via extracted seam
- Extracted gunzipRustdocJson() with overridable maxOutputLength so the cap contract is testable with real gzip payloads instead of the banned mock.module().
This commit is contained in:
@@ -278,6 +278,17 @@ function findItemInModule(mod_: RustdocItem, name: string, index: Record<string,
|
||||
|
||||
const DOCS_RS_CACHE_FILENAME = "rustdoc.json";
|
||||
|
||||
/** Hard ceiling for decompressed rustdoc JSON: a 50 MB compressed payload can
|
||||
* expand far enough to block the event loop or OOM without a cap. Exceeding it
|
||||
* throws (RangeError), which the fetch path converts to a `null` result. */
|
||||
export const MAX_RUSTDOC_GUNZIP_BYTES = 256 * 1024 * 1024;
|
||||
|
||||
/** Decompress a docs.rs rustdoc gzip payload with the output-size cap applied.
|
||||
* `maxOutputLength` is overridable only for tests exercising the cap contract. */
|
||||
export function gunzipRustdocJson(compressed: Buffer, maxOutputLength: number = MAX_RUSTDOC_GUNZIP_BYTES): string {
|
||||
return gunzipSync(compressed, { maxOutputLength }).toString("utf-8");
|
||||
}
|
||||
|
||||
function sanitizeCacheSegment(value: string): string {
|
||||
return value.replace(/[^A-Za-z0-9._-]+/g, "_");
|
||||
}
|
||||
@@ -399,7 +410,7 @@ export const handleDocsRs: SpecialHandler = async (
|
||||
}
|
||||
|
||||
const compressed = Buffer.concat(chunks);
|
||||
const jsonStr = gunzipSync(compressed, { maxOutputLength: 256 * 1024 * 1024 }).toString("utf-8");
|
||||
const jsonStr = gunzipRustdocJson(compressed);
|
||||
crate_ = tryParseJson<RustdocCrate>(jsonStr);
|
||||
if (crate_?.index) {
|
||||
await writeCachedRustdocCrate(target, jsonStr);
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { gzipSync } from "node:zlib";
|
||||
import { gunzipRustdocJson, MAX_RUSTDOC_GUNZIP_BYTES } from "../../src/web/scrapers/docs-rs";
|
||||
|
||||
describe("docs.rs rustdoc gunzip cap", () => {
|
||||
test("decompresses payloads under the cap", () => {
|
||||
const json = JSON.stringify({ root: "0", index: { "0": { name: "demo" } } });
|
||||
expect(gunzipRustdocJson(gzipSync(json))).toBe(json);
|
||||
});
|
||||
|
||||
test("rejects payloads whose decompressed size exceeds the cap", () => {
|
||||
// A tiny compressed body expanding past the (test-scaled) cap must throw,
|
||||
// which handleDocsRs converts into a null result instead of parsing.
|
||||
const oversized = gzipSync("x".repeat(4096));
|
||||
expect(() => gunzipRustdocJson(oversized, 1024)).toThrow(RangeError);
|
||||
});
|
||||
|
||||
test("default cap is 256 MiB", () => {
|
||||
expect(MAX_RUSTDOC_GUNZIP_BYTES).toBe(256 * 1024 * 1024);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user