fix(document): accept zips whose magic bytes are mangled but have valid EOCD (#4321)

Some EPUBs (e.g. downloaded from Baidu Netdisk) have their first few bytes
corrupted while the rest of the archive is still well-formed. The zip spec
locates archives via the End-of-Central-Directory record at the file tail,
so a corrupted local file header signature does not actually invalidate the
file. When the leading magic check fails, fall back to scanning the last
~64 KiB for the EOCD signature (PK\x05\x06); if present, treat the file
as a zip and let zip.js read it normally.
This commit is contained in:
loveheaven
2026-05-27 17:26:51 +08:00
committed by GitHub
parent 647343eab3
commit b5d898a48e
+33 -1
View File
@@ -129,7 +129,39 @@ export class DocumentLoader {
// EPUB writers emit malformed bytes (e.g., PK\x03\x02) on the first entry.
// The archive is still readable via the central directory, so don't gate on
// the 4th byte. PK\x03 alone is enough to identify a local file header.
return arr[0] === 0x50 && arr[1] === 0x4b && arr[2] === 0x03;
if (arr[0] === 0x50 && arr[1] === 0x4b && arr[2] === 0x03) {
return true;
}
// Some files have their first few bytes corrupted (e.g. Baidu Netdisk
// mangles the leading PK\x03\x04 into garbage on certain epubs). The zip
// format is officially located by walking the End-of-Central-Directory
// record at the *tail* of the file -- everything before it is allowed to
// be arbitrary data (self-extracting executables rely on this). So when
// the magic bytes look wrong, fall back to searching for the EOCD
// signature (PK\x05\x06) in the last 64 KiB of the file. If found, the
// file is still a usable zip and we should let zip.js try to read it.
return await this.hasEOCD();
}
private async hasEOCD(): Promise<boolean> {
// EOCD record is at least 22 bytes (sig + 16 + comment length); the
// trailing comment can be up to 64 KiB, so search the last 64 KiB + 22.
const maxEOCDSearch = 1024 * 64 + 22;
const sliceSize = Math.min(maxEOCDSearch, this.file.size);
if (sliceSize < 22) return false;
const tail = await this.file.slice(this.file.size - sliceSize, this.file.size).arrayBuffer();
const bytes = new Uint8Array(tail);
for (let i = bytes.length - 22; i >= 0; i--) {
if (
bytes[i] === 0x50 &&
bytes[i + 1] === 0x4b &&
bytes[i + 2] === 0x05 &&
bytes[i + 3] === 0x06
) {
return true;
}
}
return false;
}
private async isPDF(): Promise<boolean> {