fix(epub): import books whose OPF has an unescaped ampersand (#4640)
Some EPUBs ship an OPF that isn't well-formed XML — a bare, unescaped `&` in a hand-built manifest id, e.g.: <item id="Chapter_1213_Search_&_Rescue_153" .../> A strict XML parser rejects it (`EntityRef: expecting ';'`) and the book fails to import on every platform: the web/desktop/Android reader-open path (foliate `EPUB.#loadXML`) and the Android/desktop native-import bridge (`parseEpubMetadataFromXML`, which parsed unsanitized). Bump foliate-js to escape any `&` that doesn't begin a valid character or entity reference, applied at both parse sites (readest/foliate-js#26). Valid and numeric references are preserved. Verified end-to-end on the real "Shadow Slave - Vol. 6" EPUB: imports in the web app (Chrome) and on a physical Xiaomi device via the native path, both of which previously failed. New unit test covers both `parseEpubMetadataFromXML` cases. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
// Regression test for an import failure on EPUBs whose OPF contains a raw,
|
||||
// unescaped ampersand.
|
||||
//
|
||||
// "Shadow Slave - Vol. 6 - All The Devils Are Here.epub" ships an OPF with a
|
||||
// hand-built manifest id that was never XML-escaped:
|
||||
//
|
||||
// <item id="Chapter_1213_Search_&_Rescue_153" .../>
|
||||
//
|
||||
// The bare `&` makes the OPF non-well-formed XML. A strict parser (DOMParser in
|
||||
// Chrome/jsdom, and the same parser foliate-js uses on every platform) reads the
|
||||
// `&`, then the name `_Rescue_153`, then hits `"` where it expected `;` and
|
||||
// reports `EntityRef: expecting ';'`. Import then dies with
|
||||
// "Failed to open the book file: XML parsing error: ...".
|
||||
//
|
||||
// foliate-js already mapped a handful of named HTML entities (` ` …) to
|
||||
// numeric refs, but it never escaped stray ampersands that aren't part of any
|
||||
// entity reference. This test pins the fix: such an OPF must still parse and
|
||||
// yield its metadata.
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { parseEpubMetadataFromXML } from 'foliate-js/epub.js';
|
||||
|
||||
const NBSP = String.fromCharCode(160);
|
||||
|
||||
const opf = (title: string, manifest: string) => `<?xml version='1.0' encoding='utf-8'?>
|
||||
<package xmlns="http://www.idpf.org/2007/opf" xmlns:dc="http://purl.org/dc/elements/1.1/" unique-identifier="BookId" version="3.0">
|
||||
<metadata>
|
||||
<dc:title>${title}</dc:title>
|
||||
<dc:creator>Guiltythree</dc:creator>
|
||||
<dc:identifier id="BookId">1</dc:identifier>
|
||||
<dc:language>en</dc:language>
|
||||
</metadata>
|
||||
<manifest>
|
||||
${manifest}
|
||||
</manifest>
|
||||
<spine>
|
||||
<itemref idref="cover"/>
|
||||
</spine>
|
||||
</package>`;
|
||||
|
||||
const COVER = `<item id="cover" href="cover.xhtml" media-type="application/xhtml+xml"/>`;
|
||||
|
||||
describe('OPF XML entity sanitization', () => {
|
||||
it('imports an OPF with a raw & in a manifest item id (the reported bug)', () => {
|
||||
const title = 'Shadow Slave - Vol. 6 - All The Devils Are Here';
|
||||
const xml = opf(
|
||||
title,
|
||||
`${COVER}
|
||||
<item id="Chapter_1213_Search_&_Rescue_153" href="content/c1.xhtml" media-type="application/xhtml+xml"/>`,
|
||||
);
|
||||
const { metadata } = parseEpubMetadataFromXML(xml);
|
||||
expect(metadata.title).toBe(title);
|
||||
});
|
||||
|
||||
it('escapes a bare & in element text content', () => {
|
||||
const xml = opf('Search & Rescue & More', COVER);
|
||||
const { metadata } = parseEpubMetadataFromXML(xml);
|
||||
expect(metadata.title).toBe('Search & Rescue & More');
|
||||
});
|
||||
|
||||
it('preserves valid entities and numeric/named references (no double-escaping)', () => {
|
||||
// & -> "&",   -> U+00A0, -> U+00A0 (mapped from HTML).
|
||||
const xml = opf('R&D Press Ltd', COVER);
|
||||
const { metadata } = parseEpubMetadataFromXML(xml);
|
||||
expect(metadata.title).toBe(`R&D${NBSP}Press${NBSP}Ltd`);
|
||||
});
|
||||
});
|
||||
+1
-1
Submodule packages/foliate-js updated: 9c34e835c7...4aa4efe3bb
Reference in New Issue
Block a user