fix(epub): accept EPUBs with malformed first ZIP local file header (#4103)
Some EPUB writers (e.g., "ebookredo") emit a non-standard local file header signature on the first entry — bytes like PK\x03\x02 instead of PK\x03\x04. The archive is still readable via the End of Central Directory record, and @zip.js/zip.js handles it without complaint, but DocumentLoader.isZip() rejected the file at the magic-bytes gate before zip.js ever ran. The user saw "Unsupported or corrupted book file" on a perfectly readable EPUB. Drop the strict 4th-byte equality check. PK\x03 alone identifies a local file header — no other ZIP record signature starts that way — so loosening the check is safe and aligns with what tolerant ZIP parsers already do. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,50 @@
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { readFileSync } from 'fs';
|
||||
import { resolve } from 'path';
|
||||
import { DocumentLoader } from '@/libs/document';
|
||||
|
||||
if (typeof globalThis['CSS'] === 'undefined') {
|
||||
(globalThis as Record<string, unknown>)['CSS'] = {
|
||||
escape: (s: string) => s.replace(/([^\w-])/g, '\\$1'),
|
||||
};
|
||||
}
|
||||
|
||||
if (!customElements.get('foliate-paginator')) {
|
||||
customElements.define(
|
||||
'foliate-paginator',
|
||||
class extends HTMLElement {
|
||||
override setAttribute() {}
|
||||
override addEventListener() {}
|
||||
open() {}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
vi.mock('foliate-js/paginator.js', () => ({}));
|
||||
|
||||
const loadFixtureBytes = (name: string): Uint8Array => {
|
||||
const epubPath = resolve(__dirname, `../fixtures/data/${name}`);
|
||||
return new Uint8Array(readFileSync(epubPath));
|
||||
};
|
||||
|
||||
describe('DocumentLoader.open', () => {
|
||||
it('opens an EPUB whose first local file header has a non-standard signature byte', async () => {
|
||||
// Some EPUB writers in the wild produce a malformed first local file header
|
||||
// signature - PK\x03\x02 instead of the spec-mandated PK\x03\x04.
|
||||
// The archive is otherwise valid: zip.js reads every entry via the central
|
||||
// directory at the end of the file. We must not reject it at the magic-bytes gate.
|
||||
const bytes = loadFixtureBytes('repro-3688.epub');
|
||||
expect([bytes[0], bytes[1], bytes[2], bytes[3]]).toEqual([0x50, 0x4b, 0x03, 0x04]);
|
||||
|
||||
const malformed = bytes.slice();
|
||||
malformed[3] = 0x02;
|
||||
|
||||
const file = new File([malformed], 'malformed-header.epub', {
|
||||
type: 'application/epub+zip',
|
||||
});
|
||||
const result = await new DocumentLoader(file).open();
|
||||
|
||||
expect(result.book).toBeTruthy();
|
||||
expect(result.format).toBe('EPUB');
|
||||
}, 15000);
|
||||
});
|
||||
@@ -125,7 +125,11 @@ export class DocumentLoader {
|
||||
|
||||
private async isZip(): Promise<boolean> {
|
||||
const arr = new Uint8Array(await this.file.slice(0, 4).arrayBuffer());
|
||||
return arr[0] === 0x50 && arr[1] === 0x4b && arr[2] === 0x03 && arr[3] === 0x04;
|
||||
// Standard local file header signature is PK\x03\x04, but some non-conformant
|
||||
// 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;
|
||||
}
|
||||
|
||||
private async isPDF(): Promise<boolean> {
|
||||
|
||||
Reference in New Issue
Block a user