Skip to content

Commit e594832

Browse files
ikreymertw4l
andauthored
Fixes off-by-1 error in WACZ compressed cdx index creation. (#1122)
Fixes off-by-1 error where the index.idx actually references the second line in the index.cdx.gz block, and the last line was getting added to the next block. Adding tests to ensure the compressed CDXJ is working as expected now. Fixes #1121 --------- Co-authored-by: Tessa Walsh <tessa@bitarchivist.net>
1 parent b64dd00 commit e594832

2 files changed

Lines changed: 104 additions & 6 deletions

File tree

src/util/wacz.ts

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -326,6 +326,12 @@ export async function mergeCDXJ(
326326
indexesDir: string,
327327
zipped: boolean | null = null,
328328
) {
329+
// added as env vars as really just used for testing
330+
const zipLinesPerBlock =
331+
parseInt(process.env.ZIP_LINES_PER_BLOCK || "") || LINES_PER_BLOCK;
332+
const zipCdxMinSize =
333+
parseInt(process.env.ZIP_CDX_MIN_SIZE || "") || ZIP_CDX_MIN_SIZE;
334+
329335
async function* readLinesFrom(stdout: Readable): AsyncGenerator<string> {
330336
for await (const line of readline.createInterface({ input: stdout })) {
331337
yield line + "\n";
@@ -335,6 +341,7 @@ export async function mergeCDXJ(
335341
async function* generateCompressed(
336342
reader: AsyncGenerator<string>,
337343
idxFile: Writable,
344+
zipLinesPerBlock: number,
338345
) {
339346
let offset = 0;
340347

@@ -345,7 +352,6 @@ export async function mergeCDXJ(
345352
let cdxLines: string[] = [];
346353

347354
let key = "";
348-
let count = 0;
349355

350356
idxFile.write(
351357
`!meta 0 ${JSON.stringify({
@@ -374,7 +380,6 @@ export async function mergeCDXJ(
374380

375381
offset += length;
376382

377-
count = 1;
378383
key = "";
379384
cdxLines = [];
380385

@@ -386,10 +391,11 @@ export async function mergeCDXJ(
386391
key = cdx.split(" {", 1)[0];
387392
}
388393

389-
if (++count === LINES_PER_BLOCK) {
394+
cdxLines.push(cdx);
395+
396+
if (cdxLines.length === zipLinesPerBlock) {
390397
yield await finishChunk();
391398
}
392-
cdxLines.push(cdx);
393399
}
394400

395401
if (key) {
@@ -418,7 +424,7 @@ export async function mergeCDXJ(
418424
const tempCdxSize = await getDirSize(warcCdxDir);
419425

420426
// if CDX size is at least this size, use compressed version
421-
zipped = tempCdxSize >= ZIP_CDX_MIN_SIZE;
427+
zipped = tempCdxSize >= zipCdxMinSize;
422428
}
423429

424430
const proc = child_process.spawn("sort", cdxFiles, {
@@ -440,7 +446,13 @@ export async function mergeCDXJ(
440446
});
441447

442448
await pipeline(
443-
Readable.from(generateCompressed(readLinesFrom(proc.stdout), outputIdx)),
449+
Readable.from(
450+
generateCompressed(
451+
readLinesFrom(proc.stdout),
452+
outputIdx,
453+
zipLinesPerBlock,
454+
),
455+
),
444456
output,
445457
);
446458

tests/zipped-cdx-index.test.ts

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import { execSync } from "child_process";
2+
import fs from "fs";
3+
import { gunzipSync } from "zlib";
4+
5+
const LINES_PER_BLOCK = 16;
6+
7+
test("ensure basic crawl run with docker run passes", async () => {
8+
execSync(
9+
`docker run -e ZIP_LINES_PER_BLOCK=${LINES_PER_BLOCK} -e ZIP_CDX_MIN_SIZE=10000 -v $PWD/test-crawls:/crawls webrecorder/browsertrix-crawler crawl --url https://old.webrecorder.net/ --limit 6 --collection zipped-cdx-merge --generateWACZ`,
10+
);
11+
});
12+
13+
test("check that indexes/index.cdx.gz and indexes/index.idx exist", () => {
14+
expect(
15+
fs.existsSync("test-crawls/collections/zipped-cdx-merge/indexes/index.idx"),
16+
).toBe(true);
17+
18+
expect(
19+
fs.existsSync(
20+
"test-crawls/collections/zipped-cdx-merge/indexes/index.cdx.gz",
21+
),
22+
).toBe(true);
23+
});
24+
25+
test("verify that index entries are as expected", async () => {
26+
const indexBuff = fs.readFileSync(
27+
"test-crawls/collections/zipped-cdx-merge/indexes/index.idx",
28+
{ encoding: "utf-8" },
29+
);
30+
31+
const gzipBuff = new Uint8Array(
32+
fs.readFileSync(
33+
"test-crawls/collections/zipped-cdx-merge/indexes/index.cdx.gz",
34+
),
35+
);
36+
37+
const blockLineCounts = [];
38+
39+
let allCdxLines: string[] = [];
40+
41+
for (const line of indexBuff.trim().split("\n")) {
42+
if (line.startsWith("!meta 0")) {
43+
continue;
44+
}
45+
// parse index line in format <surt> <ts> <json>, e.g.
46+
// com,example)/ 20261226010203 {...}
47+
const prefixIndex = line.indexOf(" ");
48+
// surt key prefix
49+
const prefix = line.slice(0, prefixIndex);
50+
const tsIndex = line.indexOf(" ", prefixIndex + 1);
51+
// timestamp
52+
const timestamp = line.slice(prefixIndex + 1, tsIndex);
53+
54+
const { offset, length } = JSON.parse(line.slice(tsIndex + 1));
55+
56+
const cdxLineBuff = new TextDecoder().decode(
57+
gunzipSync(gzipBuff.slice(offset, offset + length)),
58+
);
59+
const cdxLines = cdxLineBuff.trim().split("\n");
60+
61+
// surt and timestamp of first CDX line match index surt and timestamp
62+
const parts = cdxLines[0].split(" ");
63+
expect(parts[0]).toBe(prefix);
64+
expect(parts[1]).toBe(timestamp);
65+
66+
allCdxLines = [...allCdxLines, ...cdxLines];
67+
68+
// add to line counts
69+
blockLineCounts.push(cdxLines.length);
70+
}
71+
72+
// each line count is equal to number of lines per block, except last one
73+
for (let i = 0; i < blockLineCounts.length - 1; i++) {
74+
expect(blockLineCounts[i]).toBe(LINES_PER_BLOCK);
75+
}
76+
77+
const remainder = allCdxLines.length % LINES_PER_BLOCK;
78+
79+
// should have the remainder
80+
expect(blockLineCounts[blockLineCounts.length - 1]).toBe(remainder);
81+
82+
// ensure full sort of all cdx lines
83+
for (let i = 0; i < allCdxLines.length - 1; i++) {
84+
expect(allCdxLines[i] <= allCdxLines[i + 1]).toBe(true);
85+
}
86+
});

0 commit comments

Comments
 (0)