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