From 403be32d5acd05049227ce3d1b9026403c2c9f8a Mon Sep 17 00:00:00 2001 From: Huang Xin Date: Thu, 18 Jun 2026 14:22:44 +0800 Subject: [PATCH] fix(epub): import books whose OPF has an unescaped ampersand (#4640) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Some EPUBs ship an OPF that isn't well-formed XML — a bare, unescaped `&` in a hand-built manifest id, e.g.: 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) --- .../foliate-opf-entity-sanitize.test.ts | 67 +++++++++++++++++++ packages/foliate-js | 2 +- 2 files changed, 68 insertions(+), 1 deletion(-) create mode 100644 apps/readest-app/src/__tests__/foliate-opf-entity-sanitize.test.ts diff --git a/apps/readest-app/src/__tests__/foliate-opf-entity-sanitize.test.ts b/apps/readest-app/src/__tests__/foliate-opf-entity-sanitize.test.ts new file mode 100644 index 00000000..1374d0ed --- /dev/null +++ b/apps/readest-app/src/__tests__/foliate-opf-entity-sanitize.test.ts @@ -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: +// +// +// +// 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) => ` + + + ${title} + Guiltythree + 1 + en + + + ${manifest} + + + + +`; + +const COVER = ``; + +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} + `, + ); + 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`); + }); +}); diff --git a/packages/foliate-js b/packages/foliate-js index 9c34e835..4aa4efe3 160000 --- a/packages/foliate-js +++ b/packages/foliate-js @@ -1 +1 @@ -Subproject commit 9c34e835c7a58e2c09197e77ca0d678c48202b17 +Subproject commit 4aa4efe3bb372fca472df6e9c0b47d63e0103789