Bug#1143581: trixie-pu: package calibre/8.5.0+ds-1+deb13u4

YOKOTA Hiroshi <[email protected]> Tue, 04 Aug 2026 21:53:46 +0900
Newsgroups gmane.linux.debian.devel.release
Message-ID <178584802670.497691.9669251083012201823.reportbug__24482.3211150774$1785848174$gmane$org@loadstone.darkstar.local>
This is a multi-part MIME message sent by reportbug.


--===============4685015674154026920==
Content-Type: text/plain; charset="us-ascii"
MIME-Version: 1.0
Content-Transfer-Encoding: 7bit
Content-Disposition: inline

Package: release.debian.org
Severity: normal
Tags: trixie
X-Debbugs-Cc: [email protected], [email protected]
Control: affects -1 + src:calibre
User: [email protected]
Usertags: pu

[ Reason ]
Fix Debian bug 1143484
https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1143484

[ Impact ]
Failed to convert ePub file with obfuscated fonts

[ Tests ]
Build time unit test was successful.

[ Risks ]
I don't have ePub file with obfuscated fonts.
So I can't test specific file conversions.

[ Checklist ]
  [x] *all* changes are documented in the d/changelog
  [x] I reviewed all changes and I approve them
  [x] attach debdiff against the package in (old)stable
  [x] the issue is verified as fixed in unstable

[ Changes ]
Add fixup to CVE-2026-25636 fix that in 8.5.0+ds-1+deb13u2.

[ Other info ]
debdiff is too big because it includes file rename.
I also add minimized diff file that drops renamed files.

You can examine from online:
https://github.com/debian-
calibre/calibre/compare/debian/8.5.0+ds-1+deb13u3...debian/trixie

--===============4685015674154026920==
Content-Type: text/plain; charset="us-ascii"
MIME-Version: 1.0
Content-Transfer-Encoding: 7bit
Content-Disposition: attachment; filename="calibre_8.5.0+ds-1+deb13u4.debdiff"

diff -Nru calibre-8.5.0+ds/debian/changelog calibre-8.5.0+ds/debian/changelog
--- calibre-8.5.0+ds/debian/changelog	2026-05-25 08:19:53.000000000 +0900
+++ calibre-8.5.0+ds/debian/changelog	2026-08-04 16:13:08.000000000 +0900
@@ -1,3 +1,15 @@
+calibre (8.5.0+ds-1+deb13u4) trixie; urgency=medium
+
+  * This update adds fixup for CVE-2026-25636 patch in 8.5.0+ds-1+deb13u2
+
+  * Set default Debian branch name for trixie
+  * CVE-2026-25636 (1/2): EPUB Input: Ensure font obfuscation processing
+    happens only on files from the EPUB (Closes: #1143484)
+  * Rename a CVE-2026-25636 patch in 8.5.0+ds-1+deb13u2
+  * Renumber patches
+
+ -- YOKOTA Hiroshi <[email protected]>  Tue, 04 Aug 2026 16:13:08 +0900
+
 calibre (8.5.0+ds-1+deb13u3) trixie; urgency=medium
 
   * Fix security vulnerabilities and code quality issues (Closes: #1135543)
diff -Nru calibre-8.5.0+ds/debian/gbp.conf calibre-8.5.0+ds/debian/gbp.conf
--- calibre-8.5.0+ds/debian/gbp.conf	2026-05-25 08:19:53.000000000 +0900
+++ calibre-8.5.0+ds/debian/gbp.conf	2026-08-04 16:13:08.000000000 +0900
@@ -1,5 +1,6 @@
 [DEFAULT]
 pristine-tar = True
+debian-branch = debian/trixie
 
 [dch]
 full = True
diff -Nru calibre-8.5.0+ds/debian/patches/series calibre-8.5.0+ds/debian/patches/series
--- calibre-8.5.0+ds/debian/patches/series	2026-05-25 08:19:53.000000000 +0900
+++ calibre-8.5.0+ds/debian/patches/series	2026-08-04 16:13:08.000000000 +0900
@@ -81,14 +81,15 @@
 0081-Revert-Update-7zip-wrapper-code-for-removal-of-read-.patch
 upstream/0082-Fix-CVE-2025-64486.patch
 upstream/0083-CVE-2026-25635-CHM-Input-Ignore-internal-files-that-.patch
-upstream/0084-CVE-2026-25636-DRYer.patch
-upstream/0085-CVE-2026-25731-ZIP-Output-Change-the-template-engine.patch
-upstream/0086-CVE-2026-26064-ODT-Input-Ensure-images-are-extracted.patch
-upstream/0087-CVE-2026-26065-PDB-Input-Ensure-extracted-images-are.patch
-upstream/0088-CVE-2026-27810-Content-server-Sanitize-content-dispo.patch
-upstream/0089-CVE-2026-27824-Content-server-When-banning-IPs-for-r.patch
-upstream/0090-Fix-security-vulnerabilities-and-code-quality-issues.patch
-upstream/0091-CVE-2026-30853-RB-Input-Ensure-files-are-extracted-w.patch
-upstream/0092-CVE-2026-33205-1-2-E-book-viewer-prevent-reading-bac.patch
-upstream/0093-CVE-2026-33205-2-2-E-book-viewer-Disallow-background.patch
-upstream/0094-CVE-2026-33206-TXT-Input-Ensure-resource-files-are-r.patch
+upstream/0084-CVE-2026-25636-1-2-EPUB-Input-Ensure-font-obfuscatio.patch
+upstream/0085-CVE-2026-25636-2-2-DRYer.patch
+upstream/0086-CVE-2026-25731-ZIP-Output-Change-the-template-engine.patch
+upstream/0087-CVE-2026-26064-ODT-Input-Ensure-images-are-extracted.patch
+upstream/0088-CVE-2026-26065-PDB-Input-Ensure-extracted-images-are.patch
+upstream/0089-CVE-2026-27810-Content-server-Sanitize-content-dispo.patch
+upstream/0090-CVE-2026-27824-Content-server-When-banning-IPs-for-r.patch
+upstream/0091-Fix-security-vulnerabilities-and-code-quality-issues.patch
+upstream/0092-CVE-2026-30853-RB-Input-Ensure-files-are-extracted-w.patch
+upstream/0093-CVE-2026-33205-1-2-E-book-viewer-prevent-reading-bac.patch
+upstream/0094-CVE-2026-33205-2-2-E-book-viewer-Disallow-background.patch
+upstream/0095-CVE-2026-33206-TXT-Input-Ensure-resource-files-are-r.patch
diff -Nru calibre-8.5.0+ds/debian/patches/upstream/0084-CVE-2026-25636-1-2-EPUB-Input-Ensure-font-obfuscatio.patch calibre-8.5.0+ds/debian/patches/upstream/0084-CVE-2026-25636-1-2-EPUB-Input-Ensure-font-obfuscatio.patch
--- calibre-8.5.0+ds/debian/patches/upstream/0084-CVE-2026-25636-1-2-EPUB-Input-Ensure-font-obfuscatio.patch	1970-01-01 09:00:00.000000000 +0900
+++ calibre-8.5.0+ds/debian/patches/upstream/0084-CVE-2026-25636-1-2-EPUB-Input-Ensure-font-obfuscatio.patch	2026-08-04 16:13:08.000000000 +0900
@@ -0,0 +1,63 @@
+From: Kovid Goyal <[email protected]>
+Date: Mon, 2 Feb 2026 09:51:18 +0530
+Subject: CVE-2026-25636 (1/2): EPUB Input: Ensure font obfuscation processing
+ happens only on files from the EPUB
+
+Forwarded: not-needed
+Bug: https://github.com/kovidgoyal/calibre/security/advisories/GHSA-8r26-m7j5-hm29
+Origin: backport, https://github.com/kovidgoyal/calibre/commit/267bfd34020a4f297c2de9cc0cde50ebe5d024d4
+Bug-Debian: https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1143484
+
+Signed-off-by: YOKOTA Hiroshi <[email protected]>
+---
+ src/calibre/ebooks/conversion/plugins/epub_input.py |  4 +++-
+ src/calibre/utils/filenames.py                      | 14 ++++++++++++++
+ 2 files changed, 17 insertions(+), 1 deletion(-)
+
+diff --git a/src/calibre/ebooks/conversion/plugins/epub_input.py b/src/calibre/ebooks/conversion/plugins/epub_input.py
+index 2505169..f62fed1 100644
+--- a/src/calibre/ebooks/conversion/plugins/epub_input.py
++++ b/src/calibre/ebooks/conversion/plugins/epub_input.py
+@@ -44,6 +44,8 @@ class EPUBInput(InputFormatPlugin):
+         import uuid
+ 
+         from lxml import etree
++
++        from calibre.utils.filenames import is_existing_subpath
+         idpf_key = opf.raw_unique_identifier
+         if idpf_key:
+             idpf_key = re.sub(r'[ \t\r\n]', '', idpf_key)
+@@ -74,7 +76,7 @@ class EPUBInput(InputFormatPlugin):
+                 uri = cr.get('URI')
+                 path = os.path.abspath(os.path.join(os.path.dirname(encfile), '..', *uri.split('/')))
+                 tkey = (key if algorithm == ADOBE_OBFUSCATION else idpf_key)
+-                if (tkey and os.path.exists(path)):
++                if (tkey and is_existing_subpath(path, os.getcwd())):
+                     self._encrypted_font_uris.append(uri)
+                     decrypt_font(tkey, path, algorithm)
+             return True
+diff --git a/src/calibre/utils/filenames.py b/src/calibre/utils/filenames.py
+index 5182f75..1cb07f4 100644
+--- a/src/calibre/utils/filenames.py
++++ b/src/calibre/utils/filenames.py
+@@ -644,6 +644,20 @@ def copytree_using_links(path, dest, dest_is_parent=True, filecopyfunc=copyfile)
+                 filecopyfunc(src, df)
+ 
+ 
++def is_existing_subpath(child: str, parent: str) -> bool:
++    ' Check if child is under parent. If either child or parent dont exist, returns False. '
++    try:
++        parent = os.path.realpath(parent, strict=True)  # resolve symlinks
++        child = os.path.realpath(child, strict=True)
++    except OSError:
++        return False
++    parent = os.path.abspath(parent)
++    child = os.path.abspath(child)
++    if not parent.endswith(os.sep):
++        parent += os.sep
++    return child.startswith(parent)
++
++
+ rmtree = shutil.rmtree
+ 
+ 
diff -Nru calibre-8.5.0+ds/debian/patches/upstream/0084-CVE-2026-25636-DRYer.patch calibre-8.5.0+ds/debian/patches/upstream/0084-CVE-2026-25636-DRYer.patch
--- calibre-8.5.0+ds/debian/patches/upstream/0084-CVE-2026-25636-DRYer.patch	2026-05-25 08:19:53.000000000 +0900
+++ calibre-8.5.0+ds/debian/patches/upstream/0084-CVE-2026-25636-DRYer.patch	1970-01-01 09:00:00.000000000 +0900
@@ -1,37 +0,0 @@
-From: Kovid Goyal <[email protected]>
-Date: Mon, 2 Feb 2026 11:25:09 +0530
-Subject: CVE-2026-25636: DRYer
-
-Forwarded: not-needed
-Bug: https://github.com/kovidgoyal/calibre/security/advisories/GHSA-8r26-m7j5-hm29
-Origin: backport, https://github.com/kovidgoyal/calibre/commit/9484ea82c6ab226c18e6ca5aa000fa16de598726
-
-Signed-off-by: YOKOTA Hiroshi <[email protected]>
----
- src/calibre/ebooks/conversion/plugins/epub_input.py | 6 ++++--
- 1 file changed, 4 insertions(+), 2 deletions(-)
-
-diff --git a/src/calibre/ebooks/conversion/plugins/epub_input.py b/src/calibre/ebooks/conversion/plugins/epub_input.py
-index 2505169..edfebee 100644
---- a/src/calibre/ebooks/conversion/plugins/epub_input.py
-+++ b/src/calibre/ebooks/conversion/plugins/epub_input.py
-@@ -66,15 +66,17 @@ class EPUBInput(InputFormatPlugin):
- 
-         try:
-             root = etree.parse(encfile)
-+            base = os.path.dirname(encfile)
-+            container_base = os.path.dirname(base)
-             for em in root.xpath('descendant::*[contains(name(), "EncryptionMethod")]'):
-                 algorithm = em.get('Algorithm', '')
-                 if algorithm not in {ADOBE_OBFUSCATION, IDPF_OBFUSCATION}:
-                     return False
-                 cr = em.getparent().xpath('descendant::*[contains(name(), "CipherReference")]')[0]
-                 uri = cr.get('URI')
--                path = os.path.abspath(os.path.join(os.path.dirname(encfile), '..', *uri.split('/')))
-+                path = os.path.abspath(os.path.join(base, '..', *uri.split('/')))
-                 tkey = (key if algorithm == ADOBE_OBFUSCATION else idpf_key)
--                if (tkey and os.path.exists(path)):
-+                if (tkey and is_existing_subpath(path, container_base)):
-                     self._encrypted_font_uris.append(uri)
-                     decrypt_font(tkey, path, algorithm)
-             return True
diff -Nru calibre-8.5.0+ds/debian/patches/upstream/0085-CVE-2026-25636-2-2-DRYer.patch calibre-8.5.0+ds/debian/patches/upstream/0085-CVE-2026-25636-2-2-DRYer.patch
--- calibre-8.5.0+ds/debian/patches/upstream/0085-CVE-2026-25636-2-2-DRYer.patch	1970-01-01 09:00:00.000000000 +0900
+++ calibre-8.5.0+ds/debian/patches/upstream/0085-CVE-2026-25636-2-2-DRYer.patch	2026-08-04 16:13:08.000000000 +0900
@@ -0,0 +1,37 @@
+From: Kovid Goyal <[email protected]>
+Date: Mon, 2 Feb 2026 11:25:09 +0530
+Subject: CVE-2026-25636 (2/2): DRYer
+
+Forwarded: not-needed
+Bug: https://github.com/kovidgoyal/calibre/security/advisories/GHSA-8r26-m7j5-hm29
+Origin: backport, https://github.com/kovidgoyal/calibre/commit/9484ea82c6ab226c18e6ca5aa000fa16de598726
+
+Signed-off-by: YOKOTA Hiroshi <[email protected]>
+---
+ src/calibre/ebooks/conversion/plugins/epub_input.py | 6 ++++--
+ 1 file changed, 4 insertions(+), 2 deletions(-)
+
+diff --git a/src/calibre/ebooks/conversion/plugins/epub_input.py b/src/calibre/ebooks/conversion/plugins/epub_input.py
+index f62fed1..6b81949 100644
+--- a/src/calibre/ebooks/conversion/plugins/epub_input.py
++++ b/src/calibre/ebooks/conversion/plugins/epub_input.py
+@@ -68,15 +68,17 @@ class EPUBInput(InputFormatPlugin):
+ 
+         try:
+             root = etree.parse(encfile)
++            base = os.path.dirname(encfile)
++            container_base = os.path.dirname(base)
+             for em in root.xpath('descendant::*[contains(name(), "EncryptionMethod")]'):
+                 algorithm = em.get('Algorithm', '')
+                 if algorithm not in {ADOBE_OBFUSCATION, IDPF_OBFUSCATION}:
+                     return False
+                 cr = em.getparent().xpath('descendant::*[contains(name(), "CipherReference")]')[0]
+                 uri = cr.get('URI')
+-                path = os.path.abspath(os.path.join(os.path.dirname(encfile), '..', *uri.split('/')))
++                path = os.path.abspath(os.path.join(base, '..', *uri.split('/')))
+                 tkey = (key if algorithm == ADOBE_OBFUSCATION else idpf_key)
+-                if (tkey and is_existing_subpath(path, os.getcwd())):
++                if (tkey and is_existing_subpath(path, container_base)):
+                     self._encrypted_font_uris.append(uri)
+                     decrypt_font(tkey, path, algorithm)
+             return True
diff -Nru calibre-8.5.0+ds/debian/patches/upstream/0085-CVE-2026-25731-ZIP-Output-Change-the-template-engine.patch calibre-8.5.0+ds/debian/patches/upstream/0085-CVE-2026-25731-ZIP-Output-Change-the-template-engine.patch
--- calibre-8.5.0+ds/debian/patches/upstream/0085-CVE-2026-25731-ZIP-Output-Change-the-template-engine.patch	2026-05-25 08:19:53.000000000 +0900
+++ calibre-8.5.0+ds/debian/patches/upstream/0085-CVE-2026-25731-ZIP-Output-Change-the-template-engine.patch	1970-01-01 09:00:00.000000000 +0900
@@ -1,588 +0,0 @@
-From: Kovid Goyal <[email protected]>
-Date: Thu, 5 Feb 2026 14:21:25 +0530
-Subject: CVE-2026-25731: ZIP Output: Change the template engine used for HTML
- templating from templite to Mustache,
- for greater safety and performance. Note that this is a breaking change if
- you use custom templates with ZIP output.
-
-Forwarded: not-needed
-Bug: https://github.com/kovidgoyal/calibre/security/advisories/GHSA-xrh9-w7qx-3gcc
-Origin: backport, https://github.com/kovidgoyal/calibre/commit/f0649b27512e987b95fcab2e1e0a3bcdafc23379
-
-Signed-off-by: YOKOTA Hiroshi <[email protected]>
----
- COPYRIGHT                                          |  6 --
- pyproject.toml                                     |  5 +-
- resources/templates/html_export_default.mustache   | 70 ++++++++++++++++
- resources/templates/html_export_default.tmpl       | 74 -----------------
- .../templates/html_export_default_index.mustache   | 55 +++++++++++++
- resources/templates/html_export_default_index.tmpl | 61 --------------
- .../ebooks/conversion/plugins/html_output.py       | 61 ++++++++------
- src/templite/__init__.py                           | 96 ----------------------
- 8 files changed, 164 insertions(+), 264 deletions(-)
- create mode 100644 resources/templates/html_export_default.mustache
- delete mode 100644 resources/templates/html_export_default.tmpl
- create mode 100644 resources/templates/html_export_default_index.mustache
- delete mode 100644 resources/templates/html_export_default_index.tmpl
- delete mode 100644 src/templite/__init__.py
-
-diff --git a/COPYRIGHT b/COPYRIGHT
-index a44d756..09e1308 100644
---- a/COPYRIGHT
-+++ b/COPYRIGHT
-@@ -12,12 +12,6 @@ Files: resources/rapydscript/*
- Copyright: Various
- License: BSD
- 
--Files: src/templite/*
--Copyright: Copyright (c) 2009 joonis new media, Thimo Kraemer
--License: GPL-2+
-- The full text of the GPL is distributed as in
-- /usr/share/common-licenses/GPL-2 on Debian systems.
--
- Files: src/calibre/devices/bambook/*
- Copyright: 2010, Li Fanxi
- License: GPL-3
-diff --git a/pyproject.toml b/pyproject.toml
-index b3697bd..f7ef5cb 100644
---- a/pyproject.toml
-+++ b/pyproject.toml
-@@ -26,7 +26,6 @@ exclude = [
-     "setup/linux-installer.py",
-     "src/css_selectors/*",
-     "src/polyglot/*",
--    "src/templite/*",
-     "src/tinycss/*",
- ]
- preview = true
-@@ -71,7 +70,7 @@ unfixable = ['PIE794', 'ISC001']
- detect-same-package = true
- extra-standard-library = ["aes", "elementmaker", "encodings"]
- known-first-party = ["calibre_extensions", "calibre_plugins", "polyglot"]
--known-third-party = ["odf", "qt", "templite", "tinycss", "css_selectors"]
-+known-third-party = ["odf", "qt", "tinycss", "css_selectors"]
- relative-imports-order = "closest-to-furthest"
- split-on-trailing-comma = false
- section-order = ['__python__', "future", "standard-library", "third-party", "first-party", "local-folder"]
-@@ -189,7 +188,6 @@ skip = [
-     "./setup/linux-installer.py",
-     "./src/css_selectors/*",
-     "./src/polyglot/*",
--    "./src/templite/*",
-     "./src/tinycss/*",
-     "./src/unicode_names/*",
- ]
-@@ -205,7 +203,6 @@ exclude = [
-     "src/calibre/gui2/store/stores/",
-     "src/css_selectors/",
-     "src/polyglot/",
--    "src/templite/",
-     "src/tinycss/",
- ]
- 
-diff --git a/resources/templates/html_export_default.mustache b/resources/templates/html_export_default.mustache
-new file mode 100644
-index 0000000..1c8691a
---- /dev/null
-+++ b/resources/templates/html_export_default.mustache
-@@ -0,0 +1,70 @@
-+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
-+<html xmlns="http://www.w3.org/1999/xhtml">
-+<head>
-+{{{head_content}}}
-+
-+<link href="{{css_link}}" type="text/css" rel="stylesheet" />
-+
-+</head>
-+<body>
-+
-+<div class="calibreMeta">
-+  <div class="calibreMetaTitle">
-+  {{#meta.titles}}
-+	{{#is_first}}
-+		<h1><a href="{{toc_url}}">{{title}}</a> </h1>
-+	{{/is_first}}
-+	{{^is_first}}
-+		<div class="calibreMetaSubtitle">{{title}}</div>
-+	{{/is_first}}
-+  {{/meta.titles}}
-+  </div>
-+  <div class="calibreMetaAuthor">{{meta.creators}}</div>
-+</div>
-+
-+<div class="calibreMain">
-+
-+  <div class="calibreEbookContent">
-+    {{#has_link}}
-+      <div class="calibreEbNavTop">
-+	    {{#prev_link}}
-+          <a href="{{prev_link}}" class="calibreAPrev">{{prev_page}}</a>
-+		{{/prev_link}}
-+        {{^prev_link}}
-+          <a href="{{toc_url}}" class="calibreAPrev">{{prev_page}}</a>
-+		{{/prev_link}}
-+		{{#next_link}}
-+          <a href="{{next_link}}" class="calibreANext">{{next_page}}</a>
-+		{{/next_link}}
-+      </div>
-+	{{/has_link}}
-+
-+	{{{ebook_content}}}
-+  </div>
-+
-+  {{#has_toc}}
-+  <div class="calibreToc">
-+    <h2><a href="{{toc_url}}">{{table_of_contents}}</a></h2>
-+	{{{toc}}}
-+  </div>
-+  {{/has_toc}}
-+
-+  <div class="calibreEbNav">
-+	{{#prev_link}}
-+		<a href="{{prev_link}}" class="calibreAPrev">{{prev_page}}</a>
-+	{{/prev_link}}
-+	{{^prev_link}}
-+		<a href="{{toc_url}}" class="calibreAPrev">{{prev_page}}</a>
-+	{{/prev_link}}
-+
-+    <a href="{{toc_url}}" class="calibreAHome">{{start}}</a>
-+
-+	{{#next_link}}
-+		<a href="{{next_link}}" class="calibreANext">{{next_page}}</a>
-+	{{/next_link}}
-+  </div>
-+
-+</div>
-+
-+</body>
-+</html>
-diff --git a/resources/templates/html_export_default.tmpl b/resources/templates/html_export_default.tmpl
-deleted file mode 100644
-index 7aac247..0000000
---- a/resources/templates/html_export_default.tmpl
-+++ /dev/null
-@@ -1,74 +0,0 @@
--<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
--<html xmlns="http://www.w3.org/1999/xhtml">
--<head>
--${head_content}$
--
--<link href="${cssLink}$" type="text/css" rel="stylesheet" />
--
--</head>
--<body>
--
--<div class="calibreMeta">
--  <div class="calibreMetaTitle">
--  ${pos1=1}$
--  ${for title in meta.titles():}$
--    ${if pos1:}$
--    <h1>
--      <a href="${tocUrl}$">${print(title)}$</a>
--    </h1>
--    ${:else:}$
--    <div class="calibreMetaSubtitle">${print(title)}$</div>
--    ${:endif}$
--    ${pos1=0}$
--  ${:endfor}$
--  </div>
--  <div class="calibreMetaAuthor">
--    ${print(', '.join(meta.creators()))}$
--  </div>
--</div>
--
--<div class="calibreMain">
--
--  <div class="calibreEbookContent">
--    ${if prevLink or nextLink:}$
--      <div class="calibreEbNavTop">
--        ${if prevLink:}$
--          <a href="${prevLink}$" class="calibreAPrev">${print(_('previous page'))}$</a>
--        ${:else:}$
--          <a href="${tocUrl}$" class="calibreAPrev">${print(_('previous page'))}$</a>
--        ${:endif}$
--
--        ${if nextLink:}$
--          <a href="${nextLink}$" class="calibreANext">${print(_('next page'))}$</a>
--        ${:endif}$
--      </div>
--    ${:endif}$
--
--    ${ebookContent}$
--  </div>
--
--  ${if has_toc:}$
--  <div class="calibreToc">
--    <h2><a href="${tocUrl}$">${print( _('Table of contents'))}$</a></h2>
--    ${print(toc())}$
--  </div>
--  ${:endif}$
--
--  <div class="calibreEbNav">
--    ${if prevLink:}$
--      <a href="${prevLink}$" class="calibreAPrev">${print(_('previous page'))}$</a>
--    ${:else:}$
--      <a href="${tocUrl}$" class="calibreAPrev">${print(_('previous page'))}$</a>
--    ${:endif}$
--
--    <a href="${tocUrl}$" class="calibreAHome">${print(_('start'))}$</a>
--
--    ${if nextLink:}$
--      <a href="${nextLink}$" class="calibreANext">${print(_('next page'))}$</a>
--    ${:endif}$
--  </div>
--
--</div>
--
--</body>
--</html>
-diff --git a/resources/templates/html_export_default_index.mustache b/resources/templates/html_export_default_index.mustache
-new file mode 100644
-index 0000000..aa1bc4d
---- /dev/null
-+++ b/resources/templates/html_export_default_index.mustache
-@@ -0,0 +1,55 @@
-+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
-+<html xmlns="http://www.w3.org/1999/xhtml">
-+<head>
-+<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
-+
-+<link rel="schema.DC" href="http://purl.org/dc/elements/1.1/" />
-+<link rel="schema.DCTERMS" href="http://purl.org/dc/terms/" />
-+
-+<title>{{meta.creators}} - {{meta.first_title}}</title>
-+
-+{{#meta.items}}
-+  <meta name="DC.{{name}}" content="{{value}}" />
-+{{/meta.items}}
-+
-+<link href="{{css_link}}" type="text/css" rel="stylesheet" />
-+</head>
-+<body>
-+
-+<div class="calibreMeta">
-+  <div class="calibreMetaTitle">
-+  {{#meta.titles}}
-+	{{#is_first}}
-+		<h1><a href="{{toc_url}}">{{title}}</a> </h1>
-+	{{/is_first}}
-+	{{^is_first}}
-+		<div class="calibreMetaSubtitle">{{title}}</div>
-+	{{/is_first}}
-+  {{/meta.titles}}
-+  </div>
-+  <div class="calibreMetaAuthor">{{meta.creators}}</div>
-+</div>
-+
-+<div class="calibreMain">
-+  <div class="calibreEbookContent">
-+    {{#has_toc}}
-+      <div class="calibreTocIndex">
-+        <h2>{{table_of_contents}}</h2>
-+		{{{toc}}}
-+      </div>
-+    {{/has_toc}}
-+    {{^has_toc}}
-+        <h2>{{no_toc}}</h2>
-+        <div><strong><a href="{{next_link}}">{{begin_to_read}}</a></strong></div>
-+    {{/has_toc}}
-+  </div>
-+
-+  <div class="calibreEbNav">
-+    {{#next_link}}
-+      <a href="{{next_link}}" class="calibreANext">{{next_page}}</a>
-+    {{/next_link}}
-+  </div>
-+</div>
-+
-+</body>
-+</html>
-diff --git a/resources/templates/html_export_default_index.tmpl b/resources/templates/html_export_default_index.tmpl
-deleted file mode 100644
-index f0665ad..0000000
---- a/resources/templates/html_export_default_index.tmpl
-+++ /dev/null
-@@ -1,61 +0,0 @@
--<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
--<html xmlns="http://www.w3.org/1999/xhtml">
--<head>
--<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
--
--<link rel="schema.DC" href="http://purl.org/dc/elements/1.1/" />
--<link rel="schema.DCTERMS" href="http://purl.org/dc/terms/" />
--
--<title>${print(', '.join(meta.creators()))}$ - ${print(next(meta.titles())); print(meta.titles().close())}$</title>
--
--${for item in meta:}$
--  <meta ${print('name="DC.'+item['name']+'"')}$ ${print('content="'+item['value']+'"')}$ />
--${:endfor}$
--
--<link href="${cssLink}$" type="text/css" rel="stylesheet" />
--</head>
--<body>
--
--<div class="calibreMeta">
--  <div class="calibreMetaTitle">
--  ${pos1=1}$
--  ${for title in meta.titles():}$
--    ${if pos1:}$
--    <h1>
--      <a href="${tocUrl}$">${print(title)}$</a>
--    </h1>
--    ${:else:}$
--    <div class="calibreMetaSubtitle">${print(title)}$</div>
--    ${:endif}$
--    ${pos1=0}$
--  ${:endfor}$
--  </div>
--  <div class="calibreMetaAuthor">
--    ${print(', '.join(meta.creators()))}$
--  </div>
--</div>
--
--<div class="calibreMain">
--  <div class="calibreEbookContent">
--
--    ${if has_toc:}$
--      <div class="calibreTocIndex">
--        <h2>${print(_('Table of contents'))}$</h2>
--        ${toc}$
--      </div>
--    ${:else:}$
--        <h2>${print(_('No table of contents present'))}$</h2>
--        <div><strong><a href="${nextLink}$">${print(_('begin to read'))}$</a></strong></div>
--    ${:endif}$
--
--  </div>
--
--  <div class="calibreEbNav">
--    ${if nextLink:}$
--      <a href="${nextLink}$" class="calibreANext">${print(_('next page'))}$</a>
--    ${:endif}$
--  </div>
--</div>
--
--</body>
--</html>
-diff --git a/src/calibre/ebooks/conversion/plugins/html_output.py b/src/calibre/ebooks/conversion/plugins/html_output.py
-index ea64c70..5573d04 100644
---- a/src/calibre/ebooks/conversion/plugins/html_output.py
-+++ b/src/calibre/ebooks/conversion/plugins/html_output.py
-@@ -27,13 +27,13 @@ class HTMLOutput(OutputFormatPlugin):
- 
-     options = {
-         OptionRecommendation(name='template_css',
--            help=_('CSS file used for the output instead of the default file')),
-+            help=_('CSS file used for the output instead of the default CSS.')),
- 
-         OptionRecommendation(name='template_html_index',
--            help=_('Template used for generation of the HTML index file instead of the default file')),
-+            help=_('Template used for generation of the HTML index file instead of the default template. In Mustache format.')),
- 
-         OptionRecommendation(name='template_html',
--            help=_('Template used for the generation of the HTML contents of the book instead of the default file')),
-+            help=_('Template used for the generation of the HTML contents of the book instead of the default template. In Mustache format.')),
- 
-         OptionRecommendation(name='extract_to',
-             help=_('Extract the contents of the generated ZIP file to the '
-@@ -85,8 +85,8 @@ class HTMLOutput(OutputFormatPlugin):
-                 xml_declaration=False)
- 
-     def convert(self, oeb_book, output_path, input_plugin, opts, log):
-+        import pystache
-         from lxml import etree
--        from templite import Templite
- 
-         from calibre.ebooks.html.meta import EasyMeta
-         from calibre.utils import zipfile
-@@ -97,7 +97,7 @@ class HTMLOutput(OutputFormatPlugin):
-             with open(opts.template_html_index, 'rb') as f:
-                 template_html_index_data = f.read()
-         else:
--            template_html_index_data = P('templates/html_export_default_index.tmpl', data=True)
-+            template_html_data = P('templates/html_export_default.mustache', data=True)
- 
-         if opts.template_html is not None:
-             with open(opts.template_html, 'rb') as f:
-@@ -111,9 +111,10 @@ class HTMLOutput(OutputFormatPlugin):
-         else:
-             template_css_data = P('templates/html_export_default.css', data=True)
- 
--        template_html_index_data = template_html_index_data.decode('utf-8')
--        template_html_data = template_html_data.decode('utf-8')
-+        template_html_index = pystache.parse(template_html_index_data.decode('utf-8'))
-+        template_html = pystache.parse(template_html_data.decode('utf-8'))
-         template_css_data = template_css_data.decode('utf-8')
-+        has_toc = bool(oeb_book.toc.count())
- 
-         self.log  = log
-         self.opts = opts
-@@ -130,18 +131,31 @@ class HTMLOutput(OutputFormatPlugin):
-         css_path = output_dir+os.sep+'calibreHtmlOutBasicCss.css'
-         with open(css_path, 'wb') as f:
-             f.write(template_css_data.encode('utf-8'))
-+        meta_dict = {
-+            'titles': [{'title': x, 'is_first': i == 0} for i, x in enumerate(meta.titles())],
-+            'creators': authors_to_string(tuple(meta.creators())),
-+            'items': list(meta),
-+        }
-+        meta_dict['first_title'] = meta_dict['titles'][0]['title'] if meta_dict['titles'] else ''
-+        basic_template_vars = {
-+                'meta': meta_dict, 'has_toc': has_toc,
-+                'table_of_contents':  _('Table of contents'), 'no_toc': _('No table of contents present'),
-+                'begin_to_read': _('begin to read'), 'start': _('start'),
-+                'prev_page': _('previous page'), 'next_page': _('next page'),
-+        }
- 
-         with open(output_file, 'wb') as f:
--            html_toc = self.generate_html_toc(oeb_book, output_file, output_dir)
--            templite = Templite(template_html_index_data)
-             nextLink = oeb_book.spine[0].href
-             nextLink = relpath(output_dir+os.sep+nextLink, dirname(output_file))
-             cssLink = relpath(abspath(css_path), dirname(output_file))
-             tocUrl = relpath(output_file, dirname(output_file))
--            t = templite.render(has_toc=bool(oeb_book.toc.count()),
--                    toc=html_toc, meta=meta, nextLink=nextLink,
--                    tocUrl=tocUrl, cssLink=cssLink,
--                    firstContentPageLink=nextLink)
-+            toc_as_html = self.generate_html_toc(oeb_book, output_file, output_dir) if has_toc else ''
-+            v = basic_template_vars.copy()
-+            v.update({
-+                'toc': toc_as_html, 'css_link': cssLink, 'toc_url': tocUrl, 'next_link': nextLink,
-+                'first_content_page_link': nextLink,
-+            })
-+            t = pystache.render(template_html_index, v)
-             if isinstance(t, str):
-                 t = t.encode('utf-8')
-             f.write(t)
-@@ -197,17 +211,18 @@ class HTMLOutput(OutputFormatPlugin):
-                 firstContentPageLink = oeb_book.spine[0].href
- 
-                 # render template
--                templite = Templite(template_html_data)
--
-                 def toc():
--                    return self.generate_html_toc(oeb_book, path, output_dir)
--                t = templite.render(ebookContent=ebook_content,
--                        prevLink=prevLink, nextLink=nextLink,
--                        has_toc=bool(oeb_book.toc.count()), toc=toc,
--                        tocUrl=tocUrl, head_content=head_content,
--                        meta=meta, cssLink=cssLink,
--                        firstContentPageLink=firstContentPageLink)
--
-+                    return
-+                toc_as_html = self.generate_html_toc(oeb_book, path, output_dir) if has_toc else ''
-+                v = basic_template_vars.copy()
-+                v.update({
-+                    'has_link': prevLink or nextLink,
-+                    'prev_link': prevLink, 'next_link': nextLink, 'toc_url': tocUrl,
-+                    'head_content': head_content, 'ebook_content': ebook_content,
-+                    'css_link': cssLink, 'toc': toc_as_html,
-+                    'first_content_page_link': firstContentPageLink,
-+                })
-+                t = pystache.render(template_html, v)
-                 # write html to file
-                 with open(path, 'wb') as f:
-                     f.write(t.encode('utf-8'))
-diff --git a/src/templite/__init__.py b/src/templite/__init__.py
-deleted file mode 100644
-index 8723d0d..0000000
---- a/src/templite/__init__.py
-+++ /dev/null
-@@ -1,96 +0,0 @@
--#!/usr/bin/env python
--#
--#       Templite+
--#       A light-weight, fully functional, general purpose templating engine
--#
--#       Copyright (c) 2009 joonis new media
--#       Author: Thimo Kraemer <[email protected]>
--#
--#       Based on Templite - Tomer Filiba
--#       http://code.activestate.com/recipes/496702/
--#
--#       This program is free software; you can redistribute it and/or modify
--#       it under the terms of the GNU General Public License as published by
--#       the Free Software Foundation; either version 2 of the License, or
--#       (at your option) any later version.
--#
--#       This program is distributed in the hope that it will be useful,
--#       but WITHOUT ANY WARRANTY; without even the implied warranty of
--#       MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
--#       GNU General Public License for more details.
--#
--#       You should have received a copy of the GNU General Public License
--#       along with this program; if not, write to the Free Software
--#       Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
--#       MA 02110-1301, USA.
--#
--
--import re
--import sys
--
--from polyglot.builtins import unicode_type
--
--
--class Templite:
--    auto_emit = re.compile(r'''(^['"])|(^[a-zA-Z0-9_[\]'"]+$)''')
--
--    def __init__(self, template, start='${', end='}$'):
--        if len(start) != 2 or len(end) != 2:
--            raise ValueError('each delimiter must be two characters long')
--        delimiter = re.compile('%s(.*?)%s' % (re.escape(start), re.escape(end)), re.DOTALL)
--        offset = 0
--        tokens = []
--        for i, part in enumerate(delimiter.split(template)):
--            part = part.replace('\\'.join(list(start)), start)
--            part = part.replace('\\'.join(list(end)), end)
--            if i % 2 == 0:
--                if not part:
--                    continue
--                part = part.replace('\\', '\\\\').replace('"', '\\"')
--                part = '\t' * offset + 'emit("""%s""")' % part
--            else:
--                part = part.rstrip()
--                if not part:
--                    continue
--                if part.lstrip().startswith(':'):
--                    if not offset:
--                        raise SyntaxError('no block statement to terminate: ${%s}$' % part)
--                    offset -= 1
--                    part = part.lstrip()[1:]
--                    if not part.endswith(':'):
--                        continue
--                elif self.auto_emit.match(part.lstrip()):
--                    part = 'emit(%s)' % part.lstrip()
--                lines = part.splitlines()
--                margin = min(len(l) - len(l.lstrip()) for l in lines if l.strip())
--                part = '\n'.join('\t' * offset + l[margin:] for l in lines)
--                if part.endswith(':'):
--                    offset += 1
--            tokens.append(part)
--        if offset:
--            raise SyntaxError('%i block statement(s) not terminated' % offset)
--        self.__code = compile('\n'.join(tokens), '<templite %r>' % template[:20], 'exec')
--
--    def render(self, __namespace=None, **kw):
--        """
--        renders the template according to the given namespace.
--        __namespace - a dictionary serving as a namespace for evaluation
--        **kw - keyword arguments which are added to the namespace
--        """
--        namespace = {}
--        if __namespace:
--            namespace.update(__namespace)
--        if kw:
--            namespace.update(kw)
--        namespace['emit'] = self.write
--
--        __stdout = sys.stdout
--        sys.stdout = self
--        self.__output = []
--        eval(self.__code, namespace)
--        sys.stdout = __stdout
--        return ''.join(self.__output)
--
--    def write(self, *args):
--        for a in args:
--            self.__output.append(unicode_type(a))
diff -Nru calibre-8.5.0+ds/debian/patches/upstream/0086-CVE-2026-25731-ZIP-Output-Change-the-template-engine.patch calibre-8.5.0+ds/debian/patches/upstream/0086-CVE-2026-25731-ZIP-Output-Change-the-template-engine.patch
--- calibre-8.5.0+ds/debian/patches/upstream/0086-CVE-2026-25731-ZIP-Output-Change-the-template-engine.patch	1970-01-01 09:00:00.000000000 +0900
+++ calibre-8.5.0+ds/debian/patches/upstream/0086-CVE-2026-25731-ZIP-Output-Change-the-template-engine.patch	2026-08-04 16:13:08.000000000 +0900
@@ -0,0 +1,588 @@
+From: Kovid Goyal <[email protected]>
+Date: Thu, 5 Feb 2026 14:21:25 +0530
+Subject: CVE-2026-25731: ZIP Output: Change the template engine used for HTML
+ templating from templite to Mustache,
+ for greater safety and performance. Note that this is a breaking change if
+ you use custom templates with ZIP output.
+
+Forwarded: not-needed
+Bug: https://github.com/kovidgoyal/calibre/security/advisories/GHSA-xrh9-w7qx-3gcc
+Origin: backport, https://github.com/kovidgoyal/calibre/commit/f0649b27512e987b95fcab2e1e0a3bcdafc23379
+
+Signed-off-by: YOKOTA Hiroshi <[email protected]>
+---
+ COPYRIGHT                                          |  6 --
+ pyproject.toml                                     |  5 +-
+ resources/templates/html_export_default.mustache   | 70 ++++++++++++++++
+ resources/templates/html_export_default.tmpl       | 74 -----------------
+ .../templates/html_export_default_index.mustache   | 55 +++++++++++++
+ resources/templates/html_export_default_index.tmpl | 61 --------------
+ .../ebooks/conversion/plugins/html_output.py       | 61 ++++++++------
+ src/templite/__init__.py                           | 96 ----------------------
+ 8 files changed, 164 insertions(+), 264 deletions(-)
+ create mode 100644 resources/templates/html_export_default.mustache
+ delete mode 100644 resources/templates/html_export_default.tmpl
+ create mode 100644 resources/templates/html_export_default_index.mustache
+ delete mode 100644 resources/templates/html_export_default_index.tmpl
+ delete mode 100644 src/templite/__init__.py
+
+diff --git a/COPYRIGHT b/COPYRIGHT
+index a44d756..09e1308 100644
+--- a/COPYRIGHT
++++ b/COPYRIGHT
+@@ -12,12 +12,6 @@ Files: resources/rapydscript/*
+ Copyright: Various
+ License: BSD
+ 
+-Files: src/templite/*
+-Copyright: Copyright (c) 2009 joonis new media, Thimo Kraemer
+-License: GPL-2+
+- The full text of the GPL is distributed as in
+- /usr/share/common-licenses/GPL-2 on Debian systems.
+-
+ Files: src/calibre/devices/bambook/*
+ Copyright: 2010, Li Fanxi
+ License: GPL-3
+diff --git a/pyproject.toml b/pyproject.toml
+index b3697bd..f7ef5cb 100644
+--- a/pyproject.toml
++++ b/pyproject.toml
+@@ -26,7 +26,6 @@ exclude = [
+     "setup/linux-installer.py",
+     "src/css_selectors/*",
+     "src/polyglot/*",
+-    "src/templite/*",
+     "src/tinycss/*",
+ ]
+ preview = true
+@@ -71,7 +70,7 @@ unfixable = ['PIE794', 'ISC001']
+ detect-same-package = true
+ extra-standard-library = ["aes", "elementmaker", "encodings"]
+ known-first-party = ["calibre_extensions", "calibre_plugins", "polyglot"]
+-known-third-party = ["odf", "qt", "templite", "tinycss", "css_selectors"]
++known-third-party = ["odf", "qt", "tinycss", "css_selectors"]
+ relative-imports-order = "closest-to-furthest"
+ split-on-trailing-comma = false
+ section-order = ['__python__', "future", "standard-library", "third-party", "first-party", "local-folder"]
+@@ -189,7 +188,6 @@ skip = [
+     "./setup/linux-installer.py",
+     "./src/css_selectors/*",
+     "./src/polyglot/*",
+-    "./src/templite/*",
+     "./src/tinycss/*",
+     "./src/unicode_names/*",
+ ]
+@@ -205,7 +203,6 @@ exclude = [
+     "src/calibre/gui2/store/stores/",
+     "src/css_selectors/",
+     "src/polyglot/",
+-    "src/templite/",
+     "src/tinycss/",
+ ]
+ 
+diff --git a/resources/templates/html_export_default.mustache b/resources/templates/html_export_default.mustache
+new file mode 100644
+index 0000000..1c8691a
+--- /dev/null
++++ b/resources/templates/html_export_default.mustache
+@@ -0,0 +1,70 @@
++<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
++<html xmlns="http://www.w3.org/1999/xhtml">
++<head>
++{{{head_content}}}
++
++<link href="{{css_link}}" type="text/css" rel="stylesheet" />
++
++</head>
++<body>
++
++<div class="calibreMeta">
++  <div class="calibreMetaTitle">
++  {{#meta.titles}}
++	{{#is_first}}
++		<h1><a href="{{toc_url}}">{{title}}</a> </h1>
++	{{/is_first}}
++	{{^is_first}}
++		<div class="calibreMetaSubtitle">{{title}}</div>
++	{{/is_first}}
++  {{/meta.titles}}
++  </div>
++  <div class="calibreMetaAuthor">{{meta.creators}}</div>
++</div>
++
++<div class="calibreMain">
++
++  <div class="calibreEbookContent">
++    {{#has_link}}
++      <div class="calibreEbNavTop">
++	    {{#prev_link}}
++          <a href="{{prev_link}}" class="calibreAPrev">{{prev_page}}</a>
++		{{/prev_link}}
++        {{^prev_link}}
++          <a href="{{toc_url}}" class="calibreAPrev">{{prev_page}}</a>
++		{{/prev_link}}
++		{{#next_link}}
++          <a href="{{next_link}}" class="calibreANext">{{next_page}}</a>
++		{{/next_link}}
++      </div>
++	{{/has_link}}
++
++	{{{ebook_content}}}
++  </div>
++
++  {{#has_toc}}
++  <div class="calibreToc">
++    <h2><a href="{{toc_url}}">{{table_of_contents}}</a></h2>
++	{{{toc}}}
++  </div>
++  {{/has_toc}}
++
++  <div class="calibreEbNav">
++	{{#prev_link}}
++		<a href="{{prev_link}}" class="calibreAPrev">{{prev_page}}</a>
++	{{/prev_link}}
++	{{^prev_link}}
++		<a href="{{toc_url}}" class="calibreAPrev">{{prev_page}}</a>
++	{{/prev_link}}
++
++    <a href="{{toc_url}}" class="calibreAHome">{{start}}</a>
++
++	{{#next_link}}
++		<a href="{{next_link}}" class="calibreANext">{{next_page}}</a>
++	{{/next_link}}
++  </div>
++
++</div>
++
++</body>
++</html>
+diff --git a/resources/templates/html_export_default.tmpl b/resources/templates/html_export_default.tmpl
+deleted file mode 100644
+index 7aac247..0000000
+--- a/resources/templates/html_export_default.tmpl
++++ /dev/null
+@@ -1,74 +0,0 @@
+-<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+-<html xmlns="http://www.w3.org/1999/xhtml">
+-<head>
+-${head_content}$
+-
+-<link href="${cssLink}$" type="text/css" rel="stylesheet" />
+-
+-</head>
+-<body>
+-
+-<div class="calibreMeta">
+-  <div class="calibreMetaTitle">
+-  ${pos1=1}$
+-  ${for title in meta.titles():}$
+-    ${if pos1:}$
+-    <h1>
+-      <a href="${tocUrl}$">${print(title)}$</a>
+-    </h1>
+-    ${:else:}$
+-    <div class="calibreMetaSubtitle">${print(title)}$</div>
+-    ${:endif}$
+-    ${pos1=0}$
+-  ${:endfor}$
+-  </div>
+-  <div class="calibreMetaAuthor">
+-    ${print(', '.join(meta.creators()))}$
+-  </div>
+-</div>
+-
+-<div class="calibreMain">
+-
+-  <div class="calibreEbookContent">
+-    ${if prevLink or nextLink:}$
+-      <div class="calibreEbNavTop">
+-        ${if prevLink:}$
+-          <a href="${prevLink}$" class="calibreAPrev">${print(_('previous page'))}$</a>
+-        ${:else:}$
+-          <a href="${tocUrl}$" class="calibreAPrev">${print(_('previous page'))}$</a>
+-        ${:endif}$
+-
+-        ${if nextLink:}$
+-          <a href="${nextLink}$" class="calibreANext">${print(_('next page'))}$</a>
+-        ${:endif}$
+-      </div>
+-    ${:endif}$
+-
+-    ${ebookContent}$
+-  </div>
+-
+-  ${if has_toc:}$
+-  <div class="calibreToc">
+-    <h2><a href="${tocUrl}$">${print( _('Table of contents'))}$</a></h2>
+-    ${print(toc())}$
+-  </div>
+-  ${:endif}$
+-
+-  <div class="calibreEbNav">
+-    ${if prevLink:}$
+-      <a href="${prevLink}$" class="calibreAPrev">${print(_('previous page'))}$</a>
+-    ${:else:}$
+-      <a href="${tocUrl}$" class="calibreAPrev">${print(_('previous page'))}$</a>
+-    ${:endif}$
+-
+-    <a href="${tocUrl}$" class="calibreAHome">${print(_('start'))}$</a>
+-
+-    ${if nextLink:}$
+-      <a href="${nextLink}$" class="calibreANext">${print(_('next page'))}$</a>
+-    ${:endif}$
+-  </div>
+-
+-</div>
+-
+-</body>
+-</html>
+diff --git a/resources/templates/html_export_default_index.mustache b/resources/templates/html_export_default_index.mustache
+new file mode 100644
+index 0000000..aa1bc4d
+--- /dev/null
++++ b/resources/templates/html_export_default_index.mustache
+@@ -0,0 +1,55 @@
++<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
++<html xmlns="http://www.w3.org/1999/xhtml">
++<head>
++<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
++
++<link rel="schema.DC" href="http://purl.org/dc/elements/1.1/" />
++<link rel="schema.DCTERMS" href="http://purl.org/dc/terms/" />
++
++<title>{{meta.creators}} - {{meta.first_title}}</title>
++
++{{#meta.items}}
++  <meta name="DC.{{name}}" content="{{value}}" />
++{{/meta.items}}
++
++<link href="{{css_link}}" type="text/css" rel="stylesheet" />
++</head>
++<body>
++
++<div class="calibreMeta">
++  <div class="calibreMetaTitle">
++  {{#meta.titles}}
++	{{#is_first}}
++		<h1><a href="{{toc_url}}">{{title}}</a> </h1>
++	{{/is_first}}
++	{{^is_first}}
++		<div class="calibreMetaSubtitle">{{title}}</div>
++	{{/is_first}}
++  {{/meta.titles}}
++  </div>
++  <div class="calibreMetaAuthor">{{meta.creators}}</div>
++</div>
++
++<div class="calibreMain">
++  <div class="calibreEbookContent">
++    {{#has_toc}}
++      <div class="calibreTocIndex">
++        <h2>{{table_of_contents}}</h2>
++		{{{toc}}}
++      </div>
++    {{/has_toc}}
++    {{^has_toc}}
++        <h2>{{no_toc}}</h2>
++        <div><strong><a href="{{next_link}}">{{begin_to_read}}</a></strong></div>
++    {{/has_toc}}
++  </div>
++
++  <div class="calibreEbNav">
++    {{#next_link}}
++      <a href="{{next_link}}" class="calibreANext">{{next_page}}</a>
++    {{/next_link}}
++  </div>
++</div>
++
++</body>
++</html>
+diff --git a/resources/templates/html_export_default_index.tmpl b/resources/templates/html_export_default_index.tmpl
+deleted file mode 100644
+index f0665ad..0000000
+--- a/resources/templates/html_export_default_index.tmpl
++++ /dev/null
+@@ -1,61 +0,0 @@
+-<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+-<html xmlns="http://www.w3.org/1999/xhtml">
+-<head>
+-<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
+-
+-<link rel="schema.DC" href="http://purl.org/dc/elements/1.1/" />
+-<link rel="schema.DCTERMS" href="http://purl.org/dc/terms/" />
+-
+-<title>${print(', '.join(meta.creators()))}$ - ${print(next(meta.titles())); print(meta.titles().close())}$</title>
+-
+-${for item in meta:}$
+-  <meta ${print('name="DC.'+item['name']+'"')}$ ${print('content="'+item['value']+'"')}$ />
+-${:endfor}$
+-
+-<link href="${cssLink}$" type="text/css" rel="stylesheet" />
+-</head>
+-<body>
+-
+-<div class="calibreMeta">
+-  <div class="calibreMetaTitle">
+-  ${pos1=1}$
+-  ${for title in meta.titles():}$
+-    ${if pos1:}$
+-    <h1>
+-      <a href="${tocUrl}$">${print(title)}$</a>
+-    </h1>
+-    ${:else:}$
+-    <div class="calibreMetaSubtitle">${print(title)}$</div>
+-    ${:endif}$
+-    ${pos1=0}$
+-  ${:endfor}$
+-  </div>
+-  <div class="calibreMetaAuthor">
+-    ${print(', '.join(meta.creators()))}$
+-  </div>
+-</div>
+-
+-<div class="calibreMain">
+-  <div class="calibreEbookContent">
+-
+-    ${if has_toc:}$
+-      <div class="calibreTocIndex">
+-        <h2>${print(_('Table of contents'))}$</h2>
+-        ${toc}$
+-      </div>
+-    ${:else:}$
+-        <h2>${print(_('No table of contents present'))}$</h2>
+-        <div><strong><a href="${nextLink}$">${print(_('begin to read'))}$</a></strong></div>
+-    ${:endif}$
+-
+-  </div>
+-
+-  <div class="calibreEbNav">
+-    ${if nextLink:}$
+-      <a href="${nextLink}$" class="calibreANext">${print(_('next page'))}$</a>
+-    ${:endif}$
+-  </div>
+-</div>
+-
+-</body>
+-</html>
+diff --git a/src/calibre/ebooks/conversion/plugins/html_output.py b/src/calibre/ebooks/conversion/plugins/html_output.py
+index ea64c70..5573d04 100644
+--- a/src/calibre/ebooks/conversion/plugins/html_output.py
++++ b/src/calibre/ebooks/conversion/plugins/html_output.py
+@@ -27,13 +27,13 @@ class HTMLOutput(OutputFormatPlugin):
+ 
+     options = {
+         OptionRecommendation(name='template_css',
+-            help=_('CSS file used for the output instead of the default file')),
++            help=_('CSS file used for the output instead of the default CSS.')),
+ 
+         OptionRecommendation(name='template_html_index',
+-            help=_('Template used for generation of the HTML index file instead of the default file')),
++            help=_('Template used for generation of the HTML index file instead of the default template. In Mustache format.')),
+ 
+         OptionRecommendation(name='template_html',
+-            help=_('Template used for the generation of the HTML contents of the book instead of the default file')),
++            help=_('Template used for the generation of the HTML contents of the book instead of the default template. In Mustache format.')),
+ 
+         OptionRecommendation(name='extract_to',
+             help=_('Extract the contents of the generated ZIP file to the '
+@@ -85,8 +85,8 @@ class HTMLOutput(OutputFormatPlugin):
+                 xml_declaration=False)
+ 
+     def convert(self, oeb_book, output_path, input_plugin, opts, log):
++        import pystache
+         from lxml import etree
+-        from templite import Templite
+ 
+         from calibre.ebooks.html.meta import EasyMeta
+         from calibre.utils import zipfile
+@@ -97,7 +97,7 @@ class HTMLOutput(OutputFormatPlugin):
+             with open(opts.template_html_index, 'rb') as f:
+                 template_html_index_data = f.read()
+         else:
+-            template_html_index_data = P('templates/html_export_default_index.tmpl', data=True)
++            template_html_data = P('templates/html_export_default.mustache', data=True)
+ 
+         if opts.template_html is not None:
+             with open(opts.template_html, 'rb') as f:
+@@ -111,9 +111,10 @@ class HTMLOutput(OutputFormatPlugin):
+         else:
+             template_css_data = P('templates/html_export_default.css', data=True)
+ 
+-        template_html_index_data = template_html_index_data.decode('utf-8')
+-        template_html_data = template_html_data.decode('utf-8')
++        template_html_index = pystache.parse(template_html_index_data.decode('utf-8'))
++        template_html = pystache.parse(template_html_data.decode('utf-8'))
+         template_css_data = template_css_data.decode('utf-8')
++        has_toc = bool(oeb_book.toc.count())
+ 
+         self.log  = log
+         self.opts = opts
+@@ -130,18 +131,31 @@ class HTMLOutput(OutputFormatPlugin):
+         css_path = output_dir+os.sep+'calibreHtmlOutBasicCss.css'
+         with open(css_path, 'wb') as f:
+             f.write(template_css_data.encode('utf-8'))
++        meta_dict = {
++            'titles': [{'title': x, 'is_first': i == 0} for i, x in enumerate(meta.titles())],
++            'creators': authors_to_string(tuple(meta.creators())),
++            'items': list(meta),
++        }
++        meta_dict['first_title'] = meta_dict['titles'][0]['title'] if meta_dict['titles'] else ''
++        basic_template_vars = {
++                'meta': meta_dict, 'has_toc': has_toc,
++                'table_of_contents':  _('Table of contents'), 'no_toc': _('No table of contents present'),
++                'begin_to_read': _('begin to read'), 'start': _('start'),
++                'prev_page': _('previous page'), 'next_page': _('next page'),
++        }
+ 
+         with open(output_file, 'wb') as f:
+-            html_toc = self.generate_html_toc(oeb_book, output_file, output_dir)
+-            templite = Templite(template_html_index_data)
+             nextLink = oeb_book.spine[0].href
+             nextLink = relpath(output_dir+os.sep+nextLink, dirname(output_file))
+             cssLink = relpath(abspath(css_path), dirname(output_file))
+             tocUrl = relpath(output_file, dirname(output_file))
+-            t = templite.render(has_toc=bool(oeb_book.toc.count()),
+-                    toc=html_toc, meta=meta, nextLink=nextLink,
+-                    tocUrl=tocUrl, cssLink=cssLink,
+-                    firstContentPageLink=nextLink)
++            toc_as_html = self.generate_html_toc(oeb_book, output_file, output_dir) if has_toc else ''
++            v = basic_template_vars.copy()
++            v.update({
++                'toc': toc_as_html, 'css_link': cssLink, 'toc_url': tocUrl, 'next_link': nextLink,
++                'first_content_page_link': nextLink,
++            })
++            t = pystache.render(template_html_index, v)
+             if isinstance(t, str):
+                 t = t.encode('utf-8')
+             f.write(t)
+@@ -197,17 +211,18 @@ class HTMLOutput(OutputFormatPlugin):
+                 firstContentPageLink = oeb_book.spine[0].href
+ 
+                 # render template
+-                templite = Templite(template_html_data)
+-
+                 def toc():
+-                    return self.generate_html_toc(oeb_book, path, output_dir)
+-                t = templite.render(ebookContent=ebook_content,
+-                        prevLink=prevLink, nextLink=nextLink,
+-                        has_toc=bool(oeb_book.toc.count()), toc=toc,
+-                        tocUrl=tocUrl, head_content=head_content,
+-                        meta=meta, cssLink=cssLink,
+-                        firstContentPageLink=firstContentPageLink)
+-
++                    return
++                toc_as_html = self.generate_html_toc(oeb_book, path, output_dir) if has_toc else ''
++                v = basic_template_vars.copy()
++                v.update({
++                    'has_link': prevLink or nextLink,
++                    'prev_link': prevLink, 'next_link': nextLink, 'toc_url': tocUrl,
++                    'head_content': head_content, 'ebook_content': ebook_content,
++                    'css_link': cssLink, 'toc': toc_as_html,
++                    'first_content_page_link': firstContentPageLink,
++                })
++                t = pystache.render(template_html, v)
+                 # write html to file
+                 with open(path, 'wb') as f:
+                     f.write(t.encode('utf-8'))
+diff --git a/src/templite/__init__.py b/src/templite/__init__.py
+deleted file mode 100644
+index 8723d0d..0000000
+--- a/src/templite/__init__.py
++++ /dev/null
+@@ -1,96 +0,0 @@
+-#!/usr/bin/env python
+-#
+-#       Templite+
+-#       A light-weight, fully functional, general purpose templating engine
+-#
+-#       Copyright (c) 2009 joonis new media
+-#       Author: Thimo Kraemer <[email protected]>
+-#
+-#       Based on Templite - Tomer Filiba
+-#       http://code.activestate.com/recipes/496702/
+-#
+-#       This program is free software; you can redistribute it and/or modify
+-#       it under the terms of the GNU General Public License as published by
+-#       the Free Software Foundation; either version 2 of the License, or
+-#       (at your option) any later version.
+-#
+-#       This program is distributed in the hope that it will be useful,
+-#       but WITHOUT ANY WARRANTY; without even the implied warranty of
+-#       MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+-#       GNU General Public License for more details.
+-#
+-#       You should have received a copy of the GNU General Public License
+-#       along with this program; if not, write to the Free Software
+-#       Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+-#       MA 02110-1301, USA.
+-#
+-
+-import re
+-import sys
+-
+-from polyglot.builtins import unicode_type
+-
+-
+-class Templite:
+-    auto_emit = re.compile(r'''(^['"])|(^[a-zA-Z0-9_[\]'"]+$)''')
+-
+-    def __init__(self, template, start='${', end='}$'):
+-        if len(start) != 2 or len(end) != 2:
+-            raise ValueError('each delimiter must be two characters long')
+-        delimiter = re.compile('%s(.*?)%s' % (re.escape(start), re.escape(end)), re.DOTALL)
+-        offset = 0
+-        tokens = []
+-        for i, part in enumerate(delimiter.split(template)):
+-            part = part.replace('\\'.join(list(start)), start)
+-            part = part.replace('\\'.join(list(end)), end)
+-            if i % 2 == 0:
+-                if not part:
+-                    continue
+-                part = part.replace('\\', '\\\\').replace('"', '\\"')
+-                part = '\t' * offset + 'emit("""%s""")' % part
+-            else:
+-                part = part.rstrip()
+-                if not part:
+-                    continue
+-                if part.lstrip().startswith(':'):
+-                    if not offset:
+-                        raise SyntaxError('no block statement to terminate: ${%s}$' % part)
+-                    offset -= 1
+-                    part = part.lstrip()[1:]
+-                    if not part.endswith(':'):
+-                        continue
+-                elif self.auto_emit.match(part.lstrip()):
+-                    part = 'emit(%s)' % part.lstrip()
+-                lines = part.splitlines()
+-                margin = min(len(l) - len(l.lstrip()) for l in lines if l.strip())
+-                part = '\n'.join('\t' * offset + l[margin:] for l in lines)
+-                if part.endswith(':'):
+-                    offset += 1
+-            tokens.append(part)
+-        if offset:
+-            raise SyntaxError('%i block statement(s) not terminated' % offset)
+-        self.__code = compile('\n'.join(tokens), '<templite %r>' % template[:20], 'exec')
+-
+-    def render(self, __namespace=None, **kw):
+-        """
+-        renders the template according to the given namespace.
+-        __namespace - a dictionary serving as a namespace for evaluation
+-        **kw - keyword arguments which are added to the namespace
+-        """
+-        namespace = {}
+-        if __namespace:
+-            namespace.update(__namespace)
+-        if kw:
+-            namespace.update(kw)
+-        namespace['emit'] = self.write
+-
+-        __stdout = sys.stdout
+-        sys.stdout = self
+-        self.__output = []
+-        eval(self.__code, namespace)
+-        sys.stdout = __stdout
+-        return ''.join(self.__output)
+-
+-    def write(self, *args):
+-        for a in args:
+-            self.__output.append(unicode_type(a))
diff -Nru calibre-8.5.0+ds/debian/patches/upstream/0086-CVE-2026-26064-ODT-Input-Ensure-images-are-extracted.patch calibre-8.5.0+ds/debian/patches/upstream/0086-CVE-2026-26064-ODT-Input-Ensure-images-are-extracted.patch
--- calibre-8.5.0+ds/debian/patches/upstream/0086-CVE-2026-26064-ODT-Input-Ensure-images-are-extracted.patch	2026-05-25 08:19:53.000000000 +0900
+++ calibre-8.5.0+ds/debian/patches/upstream/0086-CVE-2026-26064-ODT-Input-Ensure-images-are-extracted.patch	1970-01-01 09:00:00.000000000 +0900
@@ -1,36 +0,0 @@
-From: Kovid Goyal <[email protected]>
-Date: Thu, 12 Feb 2026 10:15:57 +0530
-Subject: CVE-2026-26064: ODT Input: Ensure images are extracted within
- container
-
-Forwarded: not-needed
-Bug: https://github.com/kovidgoyal/calibre/security/advisories/GHSA-72ch-3hqc-pgmp
-Origin: https://github.com/kovidgoyal/calibre/commit/e1b5f9b45a5e8fa96c136963ad9a1d35e6adac62
-
-Signed-off-by: YOKOTA Hiroshi <[email protected]>
----
- src/calibre/ebooks/odt/input.py | 8 +++++++-
- 1 file changed, 7 insertions(+), 1 deletion(-)
-
-diff --git a/src/calibre/ebooks/odt/input.py b/src/calibre/ebooks/odt/input.py
-index d2da2e1..0daa298 100644
---- a/src/calibre/ebooks/odt/input.py
-+++ b/src/calibre/ebooks/odt/input.py
-@@ -28,10 +28,16 @@ class Extract(ODF2XHTML):
-     def extract_pictures(self, zf):
-         if not os.path.exists('Pictures'):
-             os.makedirs('Pictures')
-+        base = os.path.abspath(os.getcwd())
-+        if not base.endswith(os.sep):
-+            base += os.sep
-         for name in zf.namelist():
-             if name.startswith('Pictures') and name not in {'Pictures', 'Pictures/'}:
-+                dest = os.path.abspath(os.path.join(base, name))
-+                if os.path.commonprefix([base, dest]) != dest:
-+                    continue
-                 data = zf.read(name)
--                with open(name, 'wb') as f:
-+                with open(dest, 'wb') as f:
-                     f.write(data)
- 
-     def apply_list_starts(self, root, log):
diff -Nru calibre-8.5.0+ds/debian/patches/upstream/0087-CVE-2026-26064-ODT-Input-Ensure-images-are-extracted.patch calibre-8.5.0+ds/debian/patches/upstream/0087-CVE-2026-26064-ODT-Input-Ensure-images-are-extracted.patch
--- calibre-8.5.0+ds/debian/patches/upstream/0087-CVE-2026-26064-ODT-Input-Ensure-images-are-extracted.patch	1970-01-01 09:00:00.000000000 +0900
+++ calibre-8.5.0+ds/debian/patches/upstream/0087-CVE-2026-26064-ODT-Input-Ensure-images-are-extracted.patch	2026-08-04 16:13:08.000000000 +0900
@@ -0,0 +1,36 @@
+From: Kovid Goyal <[email protected]>
+Date: Thu, 12 Feb 2026 10:15:57 +0530
+Subject: CVE-2026-26064: ODT Input: Ensure images are extracted within
+ container
+
+Forwarded: not-needed
+Bug: https://github.com/kovidgoyal/calibre/security/advisories/GHSA-72ch-3hqc-pgmp
+Origin: https://github.com/kovidgoyal/calibre/commit/e1b5f9b45a5e8fa96c136963ad9a1d35e6adac62
+
+Signed-off-by: YOKOTA Hiroshi <[email protected]>
+---
+ src/calibre/ebooks/odt/input.py | 8 +++++++-
+ 1 file changed, 7 insertions(+), 1 deletion(-)
+
+diff --git a/src/calibre/ebooks/odt/input.py b/src/calibre/ebooks/odt/input.py
+index d2da2e1..0daa298 100644
+--- a/src/calibre/ebooks/odt/input.py
++++ b/src/calibre/ebooks/odt/input.py
+@@ -28,10 +28,16 @@ class Extract(ODF2XHTML):
+     def extract_pictures(self, zf):
+         if not os.path.exists('Pictures'):
+             os.makedirs('Pictures')
++        base = os.path.abspath(os.getcwd())
++        if not base.endswith(os.sep):
++            base += os.sep
+         for name in zf.namelist():
+             if name.startswith('Pictures') and name not in {'Pictures', 'Pictures/'}:
++                dest = os.path.abspath(os.path.join(base, name))
++                if os.path.commonprefix([base, dest]) != dest:
++                    continue
+                 data = zf.read(name)
+-                with open(name, 'wb') as f:
++                with open(dest, 'wb') as f:
+                     f.write(data)
+ 
+     def apply_list_starts(self, root, log):
diff -Nru calibre-8.5.0+ds/debian/patches/upstream/0087-CVE-2026-26065-PDB-Input-Ensure-extracted-images-are.patch calibre-8.5.0+ds/debian/patches/upstream/0087-CVE-2026-26065-PDB-Input-Ensure-extracted-images-are.patch
--- calibre-8.5.0+ds/debian/patches/upstream/0087-CVE-2026-26065-PDB-Input-Ensure-extracted-images-are.patch	2026-05-25 08:19:53.000000000 +0900
+++ calibre-8.5.0+ds/debian/patches/upstream/0087-CVE-2026-26065-PDB-Input-Ensure-extracted-images-are.patch	1970-01-01 09:00:00.000000000 +0900
@@ -1,120 +0,0 @@
-From: Kovid Goyal <[email protected]>
-Date: Thu, 12 Feb 2026 10:31:53 +0530
-Subject: CVE-2026-26065: PDB Input: Ensure extracted images are within the
- container
-
-Forwarded: not-needed
-Bug: https://github.com/kovidgoyal/calibre/security/advisories/GHSA-vmfh-7mr7-pp2w
-Origin: https://github.com/kovidgoyal/calibre/commit/b6da1c3878c06eb1356cb0ec1106cb66e0e9bfb8
-
-Signed-off-by: YOKOTA Hiroshi <[email protected]>
----
- src/calibre/ebooks/pdb/ereader/reader132.py | 27 +++++++++++++++++++--------
- src/calibre/ebooks/pdb/ereader/reader202.py | 25 ++++++++++++++++++-------
- 2 files changed, 37 insertions(+), 15 deletions(-)
-
-diff --git a/src/calibre/ebooks/pdb/ereader/reader132.py b/src/calibre/ebooks/pdb/ereader/reader132.py
-index a92fa3d..f8c9284 100644
---- a/src/calibre/ebooks/pdb/ereader/reader132.py
-+++ b/src/calibre/ebooks/pdb/ereader/reader132.py
-@@ -90,6 +90,15 @@ class Reader132(FormatReader):
-         img = data[62:]
-         return name, img
- 
-+    def image_dest(self, name, cwd):
-+        base = os.path.abspath(cwd)
-+        if not base.endswith(os.sep):
-+            base += os.sep
-+        ans = os.path.abspath(os.path.join(base, name))
-+        if os.path.commonprefix([ans, base]) != base:
-+            ans = ''
-+        return ans
-+
-     def get_text_page(self, number):
-         '''
-         Only palmdoc and zlib compressed are supported. The text is
-@@ -156,13 +165,14 @@ class Reader132(FormatReader):
-         if not os.path.exists(os.path.join(output_dir, 'images/')):
-             os.makedirs(os.path.join(output_dir, 'images/'))
-         images = []
--        with CurrentDir(os.path.join(output_dir, 'images/')):
-+        with CurrentDir(os.path.join(output_dir, 'images/')) as cwd:
-             for i in range(self.header_record.num_image_pages):
-                 name, img = self.get_image(self.header_record.image_data_offset + i)
--                images.append(name)
--                with open(name, 'wb') as imgf:
--                    self.log.debug(f'Writing image {name} to images/')
--                    imgf.write(img)
-+                if dest := self.image_dest(name, cwd):
-+                    images.append(name)
-+                    with open(dest, 'wb') as imgf:
-+                        self.log.debug(f'Writing image {name} to images/')
-+                        imgf.write(img)
- 
-         opf_path = self.create_opf(output_dir, images, toc)
- 
-@@ -209,8 +219,9 @@ class Reader132(FormatReader):
-         if not os.path.exists(output_dir):
-             os.makedirs(output_dir)
- 
--        with CurrentDir(output_dir):
-+        with CurrentDir(output_dir) as cwd:
-             for i in range(self.header_record.num_image_pages):
-                 name, img = self.get_image(self.header_record.image_data_offset + i)
--                with open(name, 'wb') as imgf:
--                    imgf.write(img)
-+                if dest := self.image_dest(name, cwd):
-+                    with open(dest, 'wb') as imgf:
-+                        imgf.write(img)
-diff --git a/src/calibre/ebooks/pdb/ereader/reader202.py b/src/calibre/ebooks/pdb/ereader/reader202.py
-index 259bfe0..d233072 100644
---- a/src/calibre/ebooks/pdb/ereader/reader202.py
-+++ b/src/calibre/ebooks/pdb/ereader/reader202.py
-@@ -108,13 +108,13 @@ class Reader202(FormatReader):
-         if not os.path.exists(os.path.join(output_dir, 'images/')):
-             os.makedirs(os.path.join(output_dir, 'images/'))
-         images = []
--        with CurrentDir(os.path.join(output_dir, 'images/')):
-+        with CurrentDir(os.path.join(output_dir, 'images/')) as cwd:
-             for i in range(self.header_record.non_text_offset, len(self.sections)):
-                 name, img = self.get_image(i)
--                if name:
--                    name = as_unicode(name)
-+                name = as_unicode(name or b'')
-+                if name and (dest := self.image_dest(name, cwd)):
-                     images.append(name)
--                    with open(name, 'wb') as imgf:
-+                    with open(dest, 'wb') as imgf:
-                         self.log.debug(f'Writing image {name} to images/')
-                         imgf.write(img)
- 
-@@ -150,6 +150,15 @@ class Reader202(FormatReader):
- 
-         return pml
- 
-+    def image_dest(self, name, cwd):
-+        base = os.path.abspath(cwd)
-+        if not base.endswith(os.sep):
-+            base += os.sep
-+        ans = os.path.abspath(os.path.join(base, name))
-+        if os.path.commonprefix([ans, base]) != base:
-+            ans = ''
-+        return ans
-+
-     def dump_images(self, output_dir):
-         '''
-         This is primarily used for debugging and 3rd party tools to
-@@ -158,8 +167,10 @@ class Reader202(FormatReader):
-         if not os.path.exists(output_dir):
-             os.makedirs(output_dir)
- 
--        with CurrentDir(output_dir):
-+        with CurrentDir(output_dir) as cwd:
-             for i in range(self.header_record.num_image_pages):
-                 name, img = self.get_image(self.header_record.image_data_offset + i)
--                with open(name, 'wb') as imgf:
--                    imgf.write(img)
-+                name = as_unicode(name or b'')
-+                if name and (dest := self.image_dest(name, cwd)):
-+                    with open(dest, 'wb') as imgf:
-+                        imgf.write(img)
diff -Nru calibre-8.5.0+ds/debian/patches/upstream/0088-CVE-2026-26065-PDB-Input-Ensure-extracted-images-are.patch calibre-8.5.0+ds/debian/patches/upstream/0088-CVE-2026-26065-PDB-Input-Ensure-extracted-images-are.patch
--- calibre-8.5.0+ds/debian/patches/upstream/0088-CVE-2026-26065-PDB-Input-Ensure-extracted-images-are.patch	1970-01-01 09:00:00.000000000 +0900
+++ calibre-8.5.0+ds/debian/patches/upstream/0088-CVE-2026-26065-PDB-Input-Ensure-extracted-images-are.patch	2026-08-04 16:13:08.000000000 +0900
@@ -0,0 +1,120 @@
+From: Kovid Goyal <[email protected]>
+Date: Thu, 12 Feb 2026 10:31:53 +0530
+Subject: CVE-2026-26065: PDB Input: Ensure extracted images are within the
+ container
+
+Forwarded: not-needed
+Bug: https://github.com/kovidgoyal/calibre/security/advisories/GHSA-vmfh-7mr7-pp2w
+Origin: https://github.com/kovidgoyal/calibre/commit/b6da1c3878c06eb1356cb0ec1106cb66e0e9bfb8
+
+Signed-off-by: YOKOTA Hiroshi <[email protected]>
+---
+ src/calibre/ebooks/pdb/ereader/reader132.py | 27 +++++++++++++++++++--------
+ src/calibre/ebooks/pdb/ereader/reader202.py | 25 ++++++++++++++++++-------
+ 2 files changed, 37 insertions(+), 15 deletions(-)
+
+diff --git a/src/calibre/ebooks/pdb/ereader/reader132.py b/src/calibre/ebooks/pdb/ereader/reader132.py
+index a92fa3d..f8c9284 100644
+--- a/src/calibre/ebooks/pdb/ereader/reader132.py
++++ b/src/calibre/ebooks/pdb/ereader/reader132.py
+@@ -90,6 +90,15 @@ class Reader132(FormatReader):
+         img = data[62:]
+         return name, img
+ 
++    def image_dest(self, name, cwd):
++        base = os.path.abspath(cwd)
++        if not base.endswith(os.sep):
++            base += os.sep
++        ans = os.path.abspath(os.path.join(base, name))
++        if os.path.commonprefix([ans, base]) != base:
++            ans = ''
++        return ans
++
+     def get_text_page(self, number):
+         '''
+         Only palmdoc and zlib compressed are supported. The text is
+@@ -156,13 +165,14 @@ class Reader132(FormatReader):
+         if not os.path.exists(os.path.join(output_dir, 'images/')):
+             os.makedirs(os.path.join(output_dir, 'images/'))
+         images = []
+-        with CurrentDir(os.path.join(output_dir, 'images/')):
++        with CurrentDir(os.path.join(output_dir, 'images/')) as cwd:
+             for i in range(self.header_record.num_image_pages):
+                 name, img = self.get_image(self.header_record.image_data_offset + i)
+-                images.append(name)
+-                with open(name, 'wb') as imgf:
+-                    self.log.debug(f'Writing image {name} to images/')
+-                    imgf.write(img)
++                if dest := self.image_dest(name, cwd):
++                    images.append(name)
++                    with open(dest, 'wb') as imgf:
++                        self.log.debug(f'Writing image {name} to images/')
++                        imgf.write(img)
+ 
+         opf_path = self.create_opf(output_dir, images, toc)
+ 
+@@ -209,8 +219,9 @@ class Reader132(FormatReader):
+         if not os.path.exists(output_dir):
+             os.makedirs(output_dir)
+ 
+-        with CurrentDir(output_dir):
++        with CurrentDir(output_dir) as cwd:
+             for i in range(self.header_record.num_image_pages):
+                 name, img = self.get_image(self.header_record.image_data_offset + i)
+-                with open(name, 'wb') as imgf:
+-                    imgf.write(img)
++                if dest := self.image_dest(name, cwd):
++                    with open(dest, 'wb') as imgf:
++                        imgf.write(img)
+diff --git a/src/calibre/ebooks/pdb/ereader/reader202.py b/src/calibre/ebooks/pdb/ereader/reader202.py
+index 259bfe0..d233072 100644
+--- a/src/calibre/ebooks/pdb/ereader/reader202.py
++++ b/src/calibre/ebooks/pdb/ereader/reader202.py
+@@ -108,13 +108,13 @@ class Reader202(FormatReader):
+         if not os.path.exists(os.path.join(output_dir, 'images/')):
+             os.makedirs(os.path.join(output_dir, 'images/'))
+         images = []
+-        with CurrentDir(os.path.join(output_dir, 'images/')):
++        with CurrentDir(os.path.join(output_dir, 'images/')) as cwd:
+             for i in range(self.header_record.non_text_offset, len(self.sections)):
+                 name, img = self.get_image(i)
+-                if name:
+-                    name = as_unicode(name)
++                name = as_unicode(name or b'')
++                if name and (dest := self.image_dest(name, cwd)):
+                     images.append(name)
+-                    with open(name, 'wb') as imgf:
++                    with open(dest, 'wb') as imgf:
+                         self.log.debug(f'Writing image {name} to images/')
+                         imgf.write(img)
+ 
+@@ -150,6 +150,15 @@ class Reader202(FormatReader):
+ 
+         return pml
+ 
++    def image_dest(self, name, cwd):
++        base = os.path.abspath(cwd)
++        if not base.endswith(os.sep):
++            base += os.sep
++        ans = os.path.abspath(os.path.join(base, name))
++        if os.path.commonprefix([ans, base]) != base:
++            ans = ''
++        return ans
++
+     def dump_images(self, output_dir):
+         '''
+         This is primarily used for debugging and 3rd party tools to
+@@ -158,8 +167,10 @@ class Reader202(FormatReader):
+         if not os.path.exists(output_dir):
+             os.makedirs(output_dir)
+ 
+-        with CurrentDir(output_dir):
++        with CurrentDir(output_dir) as cwd:
+             for i in range(self.header_record.num_image_pages):
+                 name, img = self.get_image(self.header_record.image_data_offset + i)
+-                with open(name, 'wb') as imgf:
+-                    imgf.write(img)
++                name = as_unicode(name or b'')
++                if name and (dest := self.image_dest(name, cwd)):
++                    with open(dest, 'wb') as imgf:
++                        imgf.write(img)
diff -Nru calibre-8.5.0+ds/debian/patches/upstream/0088-CVE-2026-27810-Content-server-Sanitize-content-dispo.patch calibre-8.5.0+ds/debian/patches/upstream/0088-CVE-2026-27810-Content-server-Sanitize-content-dispo.patch
--- calibre-8.5.0+ds/debian/patches/upstream/0088-CVE-2026-27810-Content-server-Sanitize-content-dispo.patch	2026-05-25 08:19:53.000000000 +0900
+++ calibre-8.5.0+ds/debian/patches/upstream/0088-CVE-2026-27810-Content-server-Sanitize-content-dispo.patch	1970-01-01 09:00:00.000000000 +0900
@@ -1,41 +0,0 @@
-From: Kovid Goyal <[email protected]>
-Date: Tue, 24 Feb 2026 09:06:11 +0530
-Subject: CVE-2026-27810: Content server: Sanitize content disposition
- received as query parameter
-
-Forwarded: not-needed
-Bug: https://github.com/kovidgoyal/calibre/security/advisories/GHSA-5fpj-fxw7-8grw
-Origin: https://github.com/kovidgoyal/calibre/commit/a468ce0f268032eea1f7431853248148ffa2e06a
-
-Signed-off-by: YOKOTA Hiroshi <[email protected]>
----
- src/calibre/srv/content.py | 8 ++++++--
- 1 file changed, 6 insertions(+), 2 deletions(-)
-
-diff --git a/src/calibre/srv/content.py b/src/calibre/srv/content.py
-index a7e4e89..0094580 100644
---- a/src/calibre/srv/content.py
-+++ b/src/calibre/srv/content.py
-@@ -221,7 +221,7 @@ def book_fmt(ctx, rd, library_id, db, book_id, fmt):
-             set_metadata(dest, mi, fmt)
-             dest.seek(0)
- 
--    cd = rd.query.get('content_disposition', 'attachment')
-+    cd = sanitize_content_disposition(rd.query.get('content_disposition', 'attachment'))
-     rd.outheaders['Content-Disposition'] = (
-         f'''{cd}; filename="{book_filename(rd, book_id, mi, fmt)}"; filename*=utf-8''{book_filename(rd, book_id, mi, fmt, as_encoded_unicode=True)}''')
- 
-@@ -522,8 +522,12 @@ def set_note(ctx, rd, field, item_id, library_id):
-     return srv_html
- 
- 
-+def sanitize_content_disposition(x: str) -> str:
-+    return re.sub(r'[^a-zA-Z0-9./-]', '-', x)
-+
-+
- def data_file(rd, fname, path, stat_result):
--    cd = rd.query.get('content_disposition', 'attachment')
-+    cd = sanitize_content_disposition(rd.query.get('content_disposition', 'attachment'))
-     rd.outheaders['Content-Disposition'] = (
-         f'''{cd}; filename="{fname_for_content_disposition(fname)}"; filename*=utf-8''{fname_for_content_disposition(fname, as_encoded_unicode=True)}''')
-     return rd.filesystem_file_with_custom_etag(share_open(path, 'rb'), stat_result.st_dev, stat_result.st_ino, stat_result.st_size, stat_result.st_mtime)
diff -Nru calibre-8.5.0+ds/debian/patches/upstream/0089-CVE-2026-27810-Content-server-Sanitize-content-dispo.patch calibre-8.5.0+ds/debian/patches/upstream/0089-CVE-2026-27810-Content-server-Sanitize-content-dispo.patch
--- calibre-8.5.0+ds/debian/patches/upstream/0089-CVE-2026-27810-Content-server-Sanitize-content-dispo.patch	1970-01-01 09:00:00.000000000 +0900
+++ calibre-8.5.0+ds/debian/patches/upstream/0089-CVE-2026-27810-Content-server-Sanitize-content-dispo.patch	2026-08-04 16:13:08.000000000 +0900
@@ -0,0 +1,41 @@
+From: Kovid Goyal <[email protected]>
+Date: Tue, 24 Feb 2026 09:06:11 +0530
+Subject: CVE-2026-27810: Content server: Sanitize content disposition
+ received as query parameter
+
+Forwarded: not-needed
+Bug: https://github.com/kovidgoyal/calibre/security/advisories/GHSA-5fpj-fxw7-8grw
+Origin: https://github.com/kovidgoyal/calibre/commit/a468ce0f268032eea1f7431853248148ffa2e06a
+
+Signed-off-by: YOKOTA Hiroshi <[email protected]>
+---
+ src/calibre/srv/content.py | 8 ++++++--
+ 1 file changed, 6 insertions(+), 2 deletions(-)
+
+diff --git a/src/calibre/srv/content.py b/src/calibre/srv/content.py
+index a7e4e89..0094580 100644
+--- a/src/calibre/srv/content.py
++++ b/src/calibre/srv/content.py
+@@ -221,7 +221,7 @@ def book_fmt(ctx, rd, library_id, db, book_id, fmt):
+             set_metadata(dest, mi, fmt)
+             dest.seek(0)
+ 
+-    cd = rd.query.get('content_disposition', 'attachment')
++    cd = sanitize_content_disposition(rd.query.get('content_disposition', 'attachment'))
+     rd.outheaders['Content-Disposition'] = (
+         f'''{cd}; filename="{book_filename(rd, book_id, mi, fmt)}"; filename*=utf-8''{book_filename(rd, book_id, mi, fmt, as_encoded_unicode=True)}''')
+ 
+@@ -522,8 +522,12 @@ def set_note(ctx, rd, field, item_id, library_id):
+     return srv_html
+ 
+ 
++def sanitize_content_disposition(x: str) -> str:
++    return re.sub(r'[^a-zA-Z0-9./-]', '-', x)
++
++
+ def data_file(rd, fname, path, stat_result):
+-    cd = rd.query.get('content_disposition', 'attachment')
++    cd = sanitize_content_disposition(rd.query.get('content_disposition', 'attachment'))
+     rd.outheaders['Content-Disposition'] = (
+         f'''{cd}; filename="{fname_for_content_disposition(fname)}"; filename*=utf-8''{fname_for_content_disposition(fname, as_encoded_unicode=True)}''')
+     return rd.filesystem_file_with_custom_etag(share_open(path, 'rb'), stat_result.st_dev, stat_result.st_ino, stat_result.st_size, stat_result.st_mtime)
diff -Nru calibre-8.5.0+ds/debian/patches/upstream/0089-CVE-2026-27824-Content-server-When-banning-IPs-for-r.patch calibre-8.5.0+ds/debian/patches/upstream/0089-CVE-2026-27824-Content-server-When-banning-IPs-for-r.patch
--- calibre-8.5.0+ds/debian/patches/upstream/0089-CVE-2026-27824-Content-server-When-banning-IPs-for-r.patch	2026-05-25 08:19:53.000000000 +0900
+++ calibre-8.5.0+ds/debian/patches/upstream/0089-CVE-2026-27824-Content-server-When-banning-IPs-for-r.patch	1970-01-01 09:00:00.000000000 +0900
@@ -1,34 +0,0 @@
-From: Kovid Goyal <[email protected]>
-Date: Tue, 24 Feb 2026 16:53:46 +0530
-Subject: CVE-2026-27824: Content server: When banning IPs for repeated login
- is enabled, only use the IP address not any HTTP headers as the ban key
-
-Forwarded: not-needed
-Bug: https://github.com/kovidgoyal/calibre/security/advisories/GHSA-vhxc-r7v8-2xrw
-Origin: https://github.com/kovidgoyal/calibre/commit/2f273444460d06f72f7a8f390f5f9ff325d1f836
-
-This means banning is ineffective behind a proxy, but cant be helped
-since we have no way of authenticating that a header comes from the
-proxy.
-
-Signed-off-by: YOKOTA Hiroshi <[email protected]>
----
- src/calibre/srv/auth.py | 4 ++--
- 1 file changed, 2 insertions(+), 2 deletions(-)
-
-diff --git a/src/calibre/srv/auth.py b/src/calibre/srv/auth.py
-index a5b5a0c..e2a4e64 100644
---- a/src/calibre/srv/auth.py
-+++ b/src/calibre/srv/auth.py
-@@ -269,9 +269,9 @@ class AuthController:
-         return cookie and validate_nonce(self.key_order, cookie, path, self.secret) and not is_nonce_stale(cookie, self.max_age_seconds)
- 
-     def do_http_auth(self, data, endpoint):
--        ban_key = data.remote_addr, data.forwarded_for
-+        ban_key = data.remote_addr
-         if self.ban_list.is_banned(ban_key):
--            raise HTTPForbidden('Too many login attempts', log=f'Too many login attempts from: {ban_key if data.forwarded_for else data.remote_addr}')
-+            raise HTTPForbidden('Too many login attempts', log=f'Too many login attempts from: {data.remote_addr}')
-         auth = data.inheaders.get('Authorization')
-         nonce_is_stale = False
-         log_msg = None
diff -Nru calibre-8.5.0+ds/debian/patches/upstream/0090-CVE-2026-27824-Content-server-When-banning-IPs-for-r.patch calibre-8.5.0+ds/debian/patches/upstream/0090-CVE-2026-27824-Content-server-When-banning-IPs-for-r.patch
--- calibre-8.5.0+ds/debian/patches/upstream/0090-CVE-2026-27824-Content-server-When-banning-IPs-for-r.patch	1970-01-01 09:00:00.000000000 +0900
+++ calibre-8.5.0+ds/debian/patches/upstream/0090-CVE-2026-27824-Content-server-When-banning-IPs-for-r.patch	2026-08-04 16:13:08.000000000 +0900
@@ -0,0 +1,34 @@
+From: Kovid Goyal <[email protected]>
+Date: Tue, 24 Feb 2026 16:53:46 +0530
+Subject: CVE-2026-27824: Content server: When banning IPs for repeated login
+ is enabled, only use the IP address not any HTTP headers as the ban key
+
+Forwarded: not-needed
+Bug: https://github.com/kovidgoyal/calibre/security/advisories/GHSA-vhxc-r7v8-2xrw
+Origin: https://github.com/kovidgoyal/calibre/commit/2f273444460d06f72f7a8f390f5f9ff325d1f836
+
+This means banning is ineffective behind a proxy, but cant be helped
+since we have no way of authenticating that a header comes from the
+proxy.
+
+Signed-off-by: YOKOTA Hiroshi <[email protected]>
+---
+ src/calibre/srv/auth.py | 4 ++--
+ 1 file changed, 2 insertions(+), 2 deletions(-)
+
+diff --git a/src/calibre/srv/auth.py b/src/calibre/srv/auth.py
+index a5b5a0c..e2a4e64 100644
+--- a/src/calibre/srv/auth.py
++++ b/src/calibre/srv/auth.py
+@@ -269,9 +269,9 @@ class AuthController:
+         return cookie and validate_nonce(self.key_order, cookie, path, self.secret) and not is_nonce_stale(cookie, self.max_age_seconds)
+ 
+     def do_http_auth(self, data, endpoint):
+-        ban_key = data.remote_addr, data.forwarded_for
++        ban_key = data.remote_addr
+         if self.ban_list.is_banned(ban_key):
+-            raise HTTPForbidden('Too many login attempts', log=f'Too many login attempts from: {ban_key if data.forwarded_for else data.remote_addr}')
++            raise HTTPForbidden('Too many login attempts', log=f'Too many login attempts from: {data.remote_addr}')
+         auth = data.inheaders.get('Authorization')
+         nonce_is_stale = False
+         log_msg = None
diff -Nru calibre-8.5.0+ds/debian/patches/upstream/0090-Fix-security-vulnerabilities-and-code-quality-issues.patch calibre-8.5.0+ds/debian/patches/upstream/0090-Fix-security-vulnerabilities-and-code-quality-issues.patch
--- calibre-8.5.0+ds/debian/patches/upstream/0090-Fix-security-vulnerabilities-and-code-quality-issues.patch	2026-05-25 08:19:53.000000000 +0900
+++ calibre-8.5.0+ds/debian/patches/upstream/0090-Fix-security-vulnerabilities-and-code-quality-issues.patch	1970-01-01 09:00:00.000000000 +0900
@@ -1,163 +0,0 @@
-From: ECB <[email protected]>
-Date: Tue, 21 Apr 2026 12:48:42 +0200
-Subject: Fix security vulnerabilities and code quality issues (Closes:
- #1135543)
-
-Forwarded: not-needed
-Bug: https://github.com/kovidgoyal/calibre/pull/3101
-Origin: backport, https://github.com/kovidgoyal/calibre/commit/b0c4ba19686232d5bff99d58ce6019546ef4d166
-
-High severity:
-- Fix typo normapth -> normpath in srv/content.py (broken endpoint)
-- Replace eval() with ast.literal_eval() in catalogs/epub_mobi.py
-- Log exceptions in FunctionDispatcher.dispatch instead of swallowing
-
-Medium severity:
-- Add path traversal protection to DirContainer read/write/exists
-- Fix XPath injection in comments_editor.py merge_contiguous_links
-- Use parameterized SQL queries in database2.py library_id setter
-- Add safety comment to pickle_loads in utils/serialize.py
----
- src/calibre/ebooks/oeb/base.py            | 12 +++++++++---
- src/calibre/gui2/__init__.py              |  2 ++
- src/calibre/gui2/comments_editor.py       |  2 +-
- src/calibre/library/catalogs/epub_mobi.py | 10 ++++++----
- src/calibre/library/database2.py          |  6 ++----
- src/calibre/srv/content.py                |  2 +-
- src/calibre/utils/serialize.py            |  2 +-
- 7 files changed, 22 insertions(+), 14 deletions(-)
-
-diff --git a/src/calibre/ebooks/oeb/base.py b/src/calibre/ebooks/oeb/base.py
-index 1192c87..44abb94 100644
---- a/src/calibre/ebooks/oeb/base.py
-+++ b/src/calibre/ebooks/oeb/base.py
-@@ -597,12 +597,16 @@ class DirContainer:
-     def read(self, path):
-         if path is None:
-             path = self.opfname
--        path = os.path.join(self.rootdir, self._unquote(path))
-+        path = os.path.abspath(os.path.join(self.rootdir, self._unquote(path)))
-+        if not path.startswith(os.path.abspath(self.rootdir)):
-+            raise ValueError(f'Path {path!r} is not inside {self.rootdir!r}')
-         with open(path, 'rb') as f:
-             return f.read()
- 
-     def write(self, path, data):
--        path = os.path.join(self.rootdir, self._unquote(path))
-+        path = os.path.abspath(os.path.join(self.rootdir, self._unquote(path)))
-+        if not path.startswith(os.path.abspath(self.rootdir)):
-+            raise ValueError(f'Path {path!r} is not inside {self.rootdir!r}')
-         dir = os.path.dirname(path)
-         if not os.path.isdir(dir):
-             os.makedirs(dir)
-@@ -613,9 +617,11 @@ class DirContainer:
-         if not path:
-             return False
-         try:
--            path = os.path.join(self.rootdir, self._unquote(path))
-+            path = os.path.abspath(os.path.join(self.rootdir, self._unquote(path)))
-         except ValueError:  # Happens if path contains quoted special chars
-             return False
-+        if not path.startswith(os.path.abspath(self.rootdir)):
-+            return False
-         try:
-             return os.path.isfile(path)
-         except UnicodeEncodeError:
-diff --git a/src/calibre/gui2/__init__.py b/src/calibre/gui2/__init__.py
-index a5893d2..aca7df6 100644
---- a/src/calibre/gui2/__init__.py
-+++ b/src/calibre/gui2/__init__.py
-@@ -845,6 +845,8 @@ class FunctionDispatcher(QObject):
-         try:
-             res = self.func(*args, **kwargs)
-         except:
-+            import traceback
-+            traceback.print_exc()
-             res = None
-         q.put(res)
- 
-diff --git a/src/calibre/gui2/comments_editor.py b/src/calibre/gui2/comments_editor.py
-index 8fb2919..6017c57 100644
---- a/src/calibre/gui2/comments_editor.py
-+++ b/src/calibre/gui2/comments_editor.py
-@@ -198,7 +198,7 @@ def use_implicit_styling_for_a(a, style_map):
- def merge_contiguous_links(root):
-     all_hrefs = set(root.xpath('//a/@href'))
-     for href in all_hrefs:
--        tags = root.xpath(f'//a[@href="{href}"]')
-+        tags = root.xpath('//a[@href=$h]', h=href)
-         processed = set()
- 
-         def insert_tag(parent, child):
-diff --git a/src/calibre/library/catalogs/epub_mobi.py b/src/calibre/library/catalogs/epub_mobi.py
-index 3e56266..e6408f6 100644
---- a/src/calibre/library/catalogs/epub_mobi.py
-+++ b/src/calibre/library/catalogs/epub_mobi.py
-@@ -351,10 +351,11 @@ class EPUB_MOBI(CatalogPlugin):
-             log.error(f"coercing thumb_width from '{opts.thumb_width}' to '{self.THUMB_SMALLEST}'")
-             opts.thumb_width = '1.0'
- 
--        # eval prefix_rules if passed from command line
-+        # parse prefix_rules if passed from command line
-         if type(opts.prefix_rules) is not tuple:
-             try:
--                opts.prefix_rules = eval(opts.prefix_rules)
-+                import ast
-+                opts.prefix_rules = ast.literal_eval(opts.prefix_rules)
-             except:
-                 log.error(f'malformed --prefix-rules: {opts.prefix_rules}')
-                 raise
-@@ -362,10 +363,11 @@ class EPUB_MOBI(CatalogPlugin):
-                 if len(rule) != 4:
-                     log.error(f'incorrect number of args for --prefix-rules: {rule!r}')
- 
--        # eval exclusion_rules if passed from command line
-+        # parse exclusion_rules if passed from command line
-         if type(opts.exclusion_rules) is not tuple:
-             try:
--                opts.exclusion_rules = eval(opts.exclusion_rules)
-+                import ast
-+                opts.exclusion_rules = ast.literal_eval(opts.exclusion_rules)
-             except:
-                 log.error(f'malformed --exclusion-rules: {opts.exclusion_rules}')
-                 raise
-diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py
-index ff0d30e..646ad15 100644
---- a/src/calibre/library/database2.py
-+++ b/src/calibre/library/database2.py
-@@ -108,10 +108,8 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
-     @library_id.setter
-     def library_id(self, val):
-         self._library_id_ = str(val)
--        self.conn.executescript(f'''
--                DELETE FROM library_id;
--                INSERT INTO library_id (uuid) VALUES ("{self._library_id_}");
--                ''')
-+        self.conn.execute('DELETE FROM library_id')
-+        self.conn.execute('INSERT INTO library_id (uuid) VALUES (?)', (self._library_id_,))
-         self.conn.commit()
- 
-     def connect(self):
-diff --git a/src/calibre/srv/content.py b/src/calibre/srv/content.py
-index 0094580..2b1a0e5 100644
---- a/src/calibre/srv/content.py
-+++ b/src/calibre/srv/content.py
-@@ -306,7 +306,7 @@ def icon(ctx, rd, which):
- 
- @endpoint('/reader-background/{encoded_fname}', android_workaround=True)
- def reader_background(ctx, rd, encoded_fname):
--    base = os.path.abspath(os.path.normapth(os.path.join(config_dir, 'viewer', 'background-images')))
-+    base = os.path.abspath(os.path.normpath(os.path.join(config_dir, 'viewer', 'background-images')))
-     fname = bytes.fromhex(encoded_fname)
-     q = os.path.abspath(os.path.normpath(os.path.join(base, fname)))
-     if not q.startswith(base):
-diff --git a/src/calibre/utils/serialize.py b/src/calibre/utils/serialize.py
-index 81e187f..75f3370 100644
---- a/src/calibre/utils/serialize.py
-+++ b/src/calibre/utils/serialize.py
-@@ -119,4 +119,4 @@ def pickle_dumps(data):
- 
- def pickle_loads(dump):
-     import pickle
--    return pickle.loads(dump, encoding='utf-8')
-+    return pickle.loads(dump, encoding='utf-8')  # nosec: only used for calibre's own serialized data
diff -Nru calibre-8.5.0+ds/debian/patches/upstream/0091-CVE-2026-30853-RB-Input-Ensure-files-are-extracted-w.patch calibre-8.5.0+ds/debian/patches/upstream/0091-CVE-2026-30853-RB-Input-Ensure-files-are-extracted-w.patch
--- calibre-8.5.0+ds/debian/patches/upstream/0091-CVE-2026-30853-RB-Input-Ensure-files-are-extracted-w.patch	2026-05-25 08:19:53.000000000 +0900
+++ calibre-8.5.0+ds/debian/patches/upstream/0091-CVE-2026-30853-RB-Input-Ensure-files-are-extracted-w.patch	1970-01-01 09:00:00.000000000 +0900
@@ -1,58 +0,0 @@
-From: Kovid Goyal <[email protected]>
-Date: Fri, 6 Mar 2026 07:39:44 +0530
-Subject: CVE-2026-30853: RB Input: Ensure files are extracted within
- container dir
-
-Forwarded: not-needed
-Bug: https://github.com/kovidgoyal/calibre/security/advisories/GHSA-7mp7-rfrg-542x
-Origin: https://github.com/kovidgoyal/calibre/commit/0f8dc639337d9ace67201e15ca12d5906d05f4c8
-
-Signed-off-by: YOKOTA Hiroshi <[email protected]>
----
- src/calibre/ebooks/rb/reader.py | 19 +++++++++++++++----
- 1 file changed, 15 insertions(+), 4 deletions(-)
-
-diff --git a/src/calibre/ebooks/rb/reader.py b/src/calibre/ebooks/rb/reader.py
-index c1f77dd..b13066a 100644
---- a/src/calibre/ebooks/rb/reader.py
-+++ b/src/calibre/ebooks/rb/reader.py
-@@ -67,6 +67,15 @@ class Reader:
- 
-         return toc
- 
-+    def get_safe_path(self, output_dir, name):
-+        base = os.path.abspath(output_dir)
-+        if not base.endswith(os.sep):
-+            base += os.sep
-+        ans = os.path.abspath(os.path.join(base, name))
-+        if os.path.commonprefix([ans, base]) != base:
-+            ans = ''
-+        return ans
-+
-     def get_text(self, toc_item, output_dir):
-         if toc_item.flags in (1, 2):
-             return
-@@ -87,8 +96,9 @@ class Reader:
-         else:
-             output += self.stream.read(toc_item.size).decode('cp1252' if self.encoding is None else self.encoding, 'replace')
- 
--        with open(os.path.join(output_dir, toc_item.name.decode('utf-8')), 'wb') as html:
--            html.write(output.replace('<TITLE>', '<TITLE> ').encode('utf-8'))
-+        if path := self.get_safe_path(output_dir, toc_item.name.decode('utf-8')):
-+            with open(path, 'wb') as html:
-+                html.write(output.replace('<TITLE>', '<TITLE> ').encode('utf-8'))
- 
-     def get_image(self, toc_item, output_dir):
-         if toc_item.flags != 0:
-@@ -97,8 +107,9 @@ class Reader:
-         self.stream.seek(toc_item.offset)
-         data = self.stream.read(toc_item.size)
- 
--        with open(os.path.join(output_dir, toc_item.name.decode('utf-8')), 'wb') as img:
--            img.write(data)
-+        if path := self.get_safe_path(output_dir, toc_item.name.decode('utf-8')):
-+            with open(path, 'wb') as img:
-+                img.write(data)
- 
-     def extract_content(self, output_dir):
-         self.log.debug('Extracting content from file...')
diff -Nru calibre-8.5.0+ds/debian/patches/upstream/0091-Fix-security-vulnerabilities-and-code-quality-issues.patch calibre-8.5.0+ds/debian/patches/upstream/0091-Fix-security-vulnerabilities-and-code-quality-issues.patch
--- calibre-8.5.0+ds/debian/patches/upstream/0091-Fix-security-vulnerabilities-and-code-quality-issues.patch	1970-01-01 09:00:00.000000000 +0900
+++ calibre-8.5.0+ds/debian/patches/upstream/0091-Fix-security-vulnerabilities-and-code-quality-issues.patch	2026-08-04 16:13:08.000000000 +0900
@@ -0,0 +1,163 @@
+From: ECB <[email protected]>
+Date: Tue, 21 Apr 2026 12:48:42 +0200
+Subject: Fix security vulnerabilities and code quality issues (Closes:
+ #1135543)
+
+Forwarded: not-needed
+Bug: https://github.com/kovidgoyal/calibre/pull/3101
+Origin: backport, https://github.com/kovidgoyal/calibre/commit/b0c4ba19686232d5bff99d58ce6019546ef4d166
+
+High severity:
+- Fix typo normapth -> normpath in srv/content.py (broken endpoint)
+- Replace eval() with ast.literal_eval() in catalogs/epub_mobi.py
+- Log exceptions in FunctionDispatcher.dispatch instead of swallowing
+
+Medium severity:
+- Add path traversal protection to DirContainer read/write/exists
+- Fix XPath injection in comments_editor.py merge_contiguous_links
+- Use parameterized SQL queries in database2.py library_id setter
+- Add safety comment to pickle_loads in utils/serialize.py
+---
+ src/calibre/ebooks/oeb/base.py            | 12 +++++++++---
+ src/calibre/gui2/__init__.py              |  2 ++
+ src/calibre/gui2/comments_editor.py       |  2 +-
+ src/calibre/library/catalogs/epub_mobi.py | 10 ++++++----
+ src/calibre/library/database2.py          |  6 ++----
+ src/calibre/srv/content.py                |  2 +-
+ src/calibre/utils/serialize.py            |  2 +-
+ 7 files changed, 22 insertions(+), 14 deletions(-)
+
+diff --git a/src/calibre/ebooks/oeb/base.py b/src/calibre/ebooks/oeb/base.py
+index 1192c87..44abb94 100644
+--- a/src/calibre/ebooks/oeb/base.py
++++ b/src/calibre/ebooks/oeb/base.py
+@@ -597,12 +597,16 @@ class DirContainer:
+     def read(self, path):
+         if path is None:
+             path = self.opfname
+-        path = os.path.join(self.rootdir, self._unquote(path))
++        path = os.path.abspath(os.path.join(self.rootdir, self._unquote(path)))
++        if not path.startswith(os.path.abspath(self.rootdir)):
++            raise ValueError(f'Path {path!r} is not inside {self.rootdir!r}')
+         with open(path, 'rb') as f:
+             return f.read()
+ 
+     def write(self, path, data):
+-        path = os.path.join(self.rootdir, self._unquote(path))
++        path = os.path.abspath(os.path.join(self.rootdir, self._unquote(path)))
++        if not path.startswith(os.path.abspath(self.rootdir)):
++            raise ValueError(f'Path {path!r} is not inside {self.rootdir!r}')
+         dir = os.path.dirname(path)
+         if not os.path.isdir(dir):
+             os.makedirs(dir)
+@@ -613,9 +617,11 @@ class DirContainer:
+         if not path:
+             return False
+         try:
+-            path = os.path.join(self.rootdir, self._unquote(path))
++            path = os.path.abspath(os.path.join(self.rootdir, self._unquote(path)))
+         except ValueError:  # Happens if path contains quoted special chars
+             return False
++        if not path.startswith(os.path.abspath(self.rootdir)):
++            return False
+         try:
+             return os.path.isfile(path)
+         except UnicodeEncodeError:
+diff --git a/src/calibre/gui2/__init__.py b/src/calibre/gui2/__init__.py
+index a5893d2..aca7df6 100644
+--- a/src/calibre/gui2/__init__.py
++++ b/src/calibre/gui2/__init__.py
+@@ -845,6 +845,8 @@ class FunctionDispatcher(QObject):
+         try:
+             res = self.func(*args, **kwargs)
+         except:
++            import traceback
++            traceback.print_exc()
+             res = None
+         q.put(res)
+ 
+diff --git a/src/calibre/gui2/comments_editor.py b/src/calibre/gui2/comments_editor.py
+index 8fb2919..6017c57 100644
+--- a/src/calibre/gui2/comments_editor.py
++++ b/src/calibre/gui2/comments_editor.py
+@@ -198,7 +198,7 @@ def use_implicit_styling_for_a(a, style_map):
+ def merge_contiguous_links(root):
+     all_hrefs = set(root.xpath('//a/@href'))
+     for href in all_hrefs:
+-        tags = root.xpath(f'//a[@href="{href}"]')
++        tags = root.xpath('//a[@href=$h]', h=href)
+         processed = set()
+ 
+         def insert_tag(parent, child):
+diff --git a/src/calibre/library/catalogs/epub_mobi.py b/src/calibre/library/catalogs/epub_mobi.py
+index 3e56266..e6408f6 100644
+--- a/src/calibre/library/catalogs/epub_mobi.py
++++ b/src/calibre/library/catalogs/epub_mobi.py
+@@ -351,10 +351,11 @@ class EPUB_MOBI(CatalogPlugin):
+             log.error(f"coercing thumb_width from '{opts.thumb_width}' to '{self.THUMB_SMALLEST}'")
+             opts.thumb_width = '1.0'
+ 
+-        # eval prefix_rules if passed from command line
++        # parse prefix_rules if passed from command line
+         if type(opts.prefix_rules) is not tuple:
+             try:
+-                opts.prefix_rules = eval(opts.prefix_rules)
++                import ast
++                opts.prefix_rules = ast.literal_eval(opts.prefix_rules)
+             except:
+                 log.error(f'malformed --prefix-rules: {opts.prefix_rules}')
+                 raise
+@@ -362,10 +363,11 @@ class EPUB_MOBI(CatalogPlugin):
+                 if len(rule) != 4:
+                     log.error(f'incorrect number of args for --prefix-rules: {rule!r}')
+ 
+-        # eval exclusion_rules if passed from command line
++        # parse exclusion_rules if passed from command line
+         if type(opts.exclusion_rules) is not tuple:
+             try:
+-                opts.exclusion_rules = eval(opts.exclusion_rules)
++                import ast
++                opts.exclusion_rules = ast.literal_eval(opts.exclusion_rules)
+             except:
+                 log.error(f'malformed --exclusion-rules: {opts.exclusion_rules}')
+                 raise
+diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py
+index ff0d30e..646ad15 100644
+--- a/src/calibre/library/database2.py
++++ b/src/calibre/library/database2.py
+@@ -108,10 +108,8 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
+     @library_id.setter
+     def library_id(self, val):
+         self._library_id_ = str(val)
+-        self.conn.executescript(f'''
+-                DELETE FROM library_id;
+-                INSERT INTO library_id (uuid) VALUES ("{self._library_id_}");
+-                ''')
++        self.conn.execute('DELETE FROM library_id')
++        self.conn.execute('INSERT INTO library_id (uuid) VALUES (?)', (self._library_id_,))
+         self.conn.commit()
+ 
+     def connect(self):
+diff --git a/src/calibre/srv/content.py b/src/calibre/srv/content.py
+index 0094580..2b1a0e5 100644
+--- a/src/calibre/srv/content.py
++++ b/src/calibre/srv/content.py
+@@ -306,7 +306,7 @@ def icon(ctx, rd, which):
+ 
+ @endpoint('/reader-background/{encoded_fname}', android_workaround=True)
+ def reader_background(ctx, rd, encoded_fname):
+-    base = os.path.abspath(os.path.normapth(os.path.join(config_dir, 'viewer', 'background-images')))
++    base = os.path.abspath(os.path.normpath(os.path.join(config_dir, 'viewer', 'background-images')))
+     fname = bytes.fromhex(encoded_fname)
+     q = os.path.abspath(os.path.normpath(os.path.join(base, fname)))
+     if not q.startswith(base):
+diff --git a/src/calibre/utils/serialize.py b/src/calibre/utils/serialize.py
+index 81e187f..75f3370 100644
+--- a/src/calibre/utils/serialize.py
++++ b/src/calibre/utils/serialize.py
+@@ -119,4 +119,4 @@ def pickle_dumps(data):
+ 
+ def pickle_loads(dump):
+     import pickle
+-    return pickle.loads(dump, encoding='utf-8')
++    return pickle.loads(dump, encoding='utf-8')  # nosec: only used for calibre's own serialized data
diff -Nru calibre-8.5.0+ds/debian/patches/upstream/0092-CVE-2026-30853-RB-Input-Ensure-files-are-extracted-w.patch calibre-8.5.0+ds/debian/patches/upstream/0092-CVE-2026-30853-RB-Input-Ensure-files-are-extracted-w.patch
--- calibre-8.5.0+ds/debian/patches/upstream/0092-CVE-2026-30853-RB-Input-Ensure-files-are-extracted-w.patch	1970-01-01 09:00:00.000000000 +0900
+++ calibre-8.5.0+ds/debian/patches/upstream/0092-CVE-2026-30853-RB-Input-Ensure-files-are-extracted-w.patch	2026-08-04 16:13:08.000000000 +0900
@@ -0,0 +1,58 @@
+From: Kovid Goyal <[email protected]>
+Date: Fri, 6 Mar 2026 07:39:44 +0530
+Subject: CVE-2026-30853: RB Input: Ensure files are extracted within
+ container dir
+
+Forwarded: not-needed
+Bug: https://github.com/kovidgoyal/calibre/security/advisories/GHSA-7mp7-rfrg-542x
+Origin: https://github.com/kovidgoyal/calibre/commit/0f8dc639337d9ace67201e15ca12d5906d05f4c8
+
+Signed-off-by: YOKOTA Hiroshi <[email protected]>
+---
+ src/calibre/ebooks/rb/reader.py | 19 +++++++++++++++----
+ 1 file changed, 15 insertions(+), 4 deletions(-)
+
+diff --git a/src/calibre/ebooks/rb/reader.py b/src/calibre/ebooks/rb/reader.py
+index c1f77dd..b13066a 100644
+--- a/src/calibre/ebooks/rb/reader.py
++++ b/src/calibre/ebooks/rb/reader.py
+@@ -67,6 +67,15 @@ class Reader:
+ 
+         return toc
+ 
++    def get_safe_path(self, output_dir, name):
++        base = os.path.abspath(output_dir)
++        if not base.endswith(os.sep):
++            base += os.sep
++        ans = os.path.abspath(os.path.join(base, name))
++        if os.path.commonprefix([ans, base]) != base:
++            ans = ''
++        return ans
++
+     def get_text(self, toc_item, output_dir):
+         if toc_item.flags in (1, 2):
+             return
+@@ -87,8 +96,9 @@ class Reader:
+         else:
+             output += self.stream.read(toc_item.size).decode('cp1252' if self.encoding is None else self.encoding, 'replace')
+ 
+-        with open(os.path.join(output_dir, toc_item.name.decode('utf-8')), 'wb') as html:
+-            html.write(output.replace('<TITLE>', '<TITLE> ').encode('utf-8'))
++        if path := self.get_safe_path(output_dir, toc_item.name.decode('utf-8')):
++            with open(path, 'wb') as html:
++                html.write(output.replace('<TITLE>', '<TITLE> ').encode('utf-8'))
+ 
+     def get_image(self, toc_item, output_dir):
+         if toc_item.flags != 0:
+@@ -97,8 +107,9 @@ class Reader:
+         self.stream.seek(toc_item.offset)
+         data = self.stream.read(toc_item.size)
+ 
+-        with open(os.path.join(output_dir, toc_item.name.decode('utf-8')), 'wb') as img:
+-            img.write(data)
++        if path := self.get_safe_path(output_dir, toc_item.name.decode('utf-8')):
++            with open(path, 'wb') as img:
++                img.write(data)
+ 
+     def extract_content(self, output_dir):
+         self.log.debug('Extracting content from file...')
diff -Nru calibre-8.5.0+ds/debian/patches/upstream/0092-CVE-2026-33205-1-2-E-book-viewer-prevent-reading-bac.patch calibre-8.5.0+ds/debian/patches/upstream/0092-CVE-2026-33205-1-2-E-book-viewer-prevent-reading-bac.patch
--- calibre-8.5.0+ds/debian/patches/upstream/0092-CVE-2026-33205-1-2-E-book-viewer-prevent-reading-bac.patch	2026-05-25 08:19:53.000000000 +0900
+++ calibre-8.5.0+ds/debian/patches/upstream/0092-CVE-2026-33205-1-2-E-book-viewer-prevent-reading-bac.patch	1970-01-01 09:00:00.000000000 +0900
@@ -1,31 +0,0 @@
-From: Kovid Goyal <[email protected]>
-Date: Mon, 16 Mar 2026 08:50:19 +0530
-Subject: CVE-2026-33205 (1/2): E-book viewer: prevent reading background
- images from outside the config dir
-
-Forwarded: not-needed
-Bug: https://github.com/kovidgoyal/calibre/security/advisories/GHSA-4926-v9px-wv7v
-Origin: https://github.com/kovidgoyal/calibre/commit/6eb7b5458f183c8a037e9d7dac428122a77204e4
-
-Signed-off-by: YOKOTA Hiroshi <[email protected]>
----
- src/calibre/gui2/viewer/web_view.py | 5 ++++-
- 1 file changed, 4 insertions(+), 1 deletion(-)
-
-diff --git a/src/calibre/gui2/viewer/web_view.py b/src/calibre/gui2/viewer/web_view.py
-index d2116bb..e09688c 100644
---- a/src/calibre/gui2/viewer/web_view.py
-+++ b/src/calibre/gui2/viewer/web_view.py
-@@ -106,8 +106,11 @@ def background_image(encoded_fname=''):
-         except FileNotFoundError:
-             return 'image/jpeg', b''
-     fname = bytes.fromhex(encoded_fname).decode()
--    img_path = os.path.join(viewer_config_dir, 'background-images', fname)
-+    base = os.path.abspath(os.path.join(viewer_config_dir, 'background-images')) + os.sep
-+    img_path = os.path.abspath(os.path.join(base, fname))
-     mt = guess_type(fname)[0] or 'image/jpeg'
-+    if not img_path.startswith(base):
-+        return mt, b''
-     try:
-         with open(make_long_path_useable(img_path), 'rb') as f:
-             return mt, f.read()
diff -Nru calibre-8.5.0+ds/debian/patches/upstream/0093-CVE-2026-33205-1-2-E-book-viewer-prevent-reading-bac.patch calibre-8.5.0+ds/debian/patches/upstream/0093-CVE-2026-33205-1-2-E-book-viewer-prevent-reading-bac.patch
--- calibre-8.5.0+ds/debian/patches/upstream/0093-CVE-2026-33205-1-2-E-book-viewer-prevent-reading-bac.patch	1970-01-01 09:00:00.000000000 +0900
+++ calibre-8.5.0+ds/debian/patches/upstream/0093-CVE-2026-33205-1-2-E-book-viewer-prevent-reading-bac.patch	2026-08-04 16:13:08.000000000 +0900
@@ -0,0 +1,31 @@
+From: Kovid Goyal <[email protected]>
+Date: Mon, 16 Mar 2026 08:50:19 +0530
+Subject: CVE-2026-33205 (1/2): E-book viewer: prevent reading background
+ images from outside the config dir
+
+Forwarded: not-needed
+Bug: https://github.com/kovidgoyal/calibre/security/advisories/GHSA-4926-v9px-wv7v
+Origin: https://github.com/kovidgoyal/calibre/commit/6eb7b5458f183c8a037e9d7dac428122a77204e4
+
+Signed-off-by: YOKOTA Hiroshi <[email protected]>
+---
+ src/calibre/gui2/viewer/web_view.py | 5 ++++-
+ 1 file changed, 4 insertions(+), 1 deletion(-)
+
+diff --git a/src/calibre/gui2/viewer/web_view.py b/src/calibre/gui2/viewer/web_view.py
+index d2116bb..e09688c 100644
+--- a/src/calibre/gui2/viewer/web_view.py
++++ b/src/calibre/gui2/viewer/web_view.py
+@@ -106,8 +106,11 @@ def background_image(encoded_fname=''):
+         except FileNotFoundError:
+             return 'image/jpeg', b''
+     fname = bytes.fromhex(encoded_fname).decode()
+-    img_path = os.path.join(viewer_config_dir, 'background-images', fname)
++    base = os.path.abspath(os.path.join(viewer_config_dir, 'background-images')) + os.sep
++    img_path = os.path.abspath(os.path.join(base, fname))
+     mt = guess_type(fname)[0] or 'image/jpeg'
++    if not img_path.startswith(base):
++        return mt, b''
+     try:
+         with open(make_long_path_useable(img_path), 'rb') as f:
+             return mt, f.read()
diff -Nru calibre-8.5.0+ds/debian/patches/upstream/0093-CVE-2026-33205-2-2-E-book-viewer-Disallow-background.patch calibre-8.5.0+ds/debian/patches/upstream/0093-CVE-2026-33205-2-2-E-book-viewer-Disallow-background.patch
--- calibre-8.5.0+ds/debian/patches/upstream/0093-CVE-2026-33205-2-2-E-book-viewer-Disallow-background.patch	2026-05-25 08:19:53.000000000 +0900
+++ calibre-8.5.0+ds/debian/patches/upstream/0093-CVE-2026-33205-2-2-E-book-viewer-Disallow-background.patch	1970-01-01 09:00:00.000000000 +0900
@@ -1,36 +0,0 @@
-From: Kovid Goyal <[email protected]>
-Date: Mon, 16 Mar 2026 08:58:25 +0530
-Subject: CVE-2026-33205 (2/2): E-book viewer: Disallow background images from
- the internet. This was an unused feature anyway
-
-Forwarded: not-needed
-Bug: https://github.com/kovidgoyal/calibre/security/advisories/GHSA-4926-v9px-wv7v
-Origin: https://github.com/kovidgoyal/calibre/commit/b1ef6a8142b8dadeb7e72c250c65d42b36ee7118
-
-Signed-off-by: YOKOTA Hiroshi <[email protected]>
----
- src/calibre/gui2/viewer/web_view.py | 11 -----------
- 1 file changed, 11 deletions(-)
-
-diff --git a/src/calibre/gui2/viewer/web_view.py b/src/calibre/gui2/viewer/web_view.py
-index e09688c..9c8e237 100644
---- a/src/calibre/gui2/viewer/web_view.py
-+++ b/src/calibre/gui2/viewer/web_view.py
-@@ -115,17 +115,6 @@ def background_image(encoded_fname=''):
-         with open(make_long_path_useable(img_path), 'rb') as f:
-             return mt, f.read()
-     except FileNotFoundError:
--        if fname.startswith(('https://', 'http://')):
--            from calibre import browser
--            br = browser()
--            try:
--                with br.open(fname) as src:
--                    data = src.read()
--            except Exception:
--                return mt, b''
--            with open(make_long_path_useable(img_path), 'wb') as dest:
--                dest.write(data)
--            return mt, data
-         return mt, b''
- 
- 
diff -Nru calibre-8.5.0+ds/debian/patches/upstream/0094-CVE-2026-33205-2-2-E-book-viewer-Disallow-background.patch calibre-8.5.0+ds/debian/patches/upstream/0094-CVE-2026-33205-2-2-E-book-viewer-Disallow-background.patch
--- calibre-8.5.0+ds/debian/patches/upstream/0094-CVE-2026-33205-2-2-E-book-viewer-Disallow-background.patch	1970-01-01 09:00:00.000000000 +0900
+++ calibre-8.5.0+ds/debian/patches/upstream/0094-CVE-2026-33205-2-2-E-book-viewer-Disallow-background.patch	2026-08-04 16:13:08.000000000 +0900
@@ -0,0 +1,36 @@
+From: Kovid Goyal <[email protected]>
+Date: Mon, 16 Mar 2026 08:58:25 +0530
+Subject: CVE-2026-33205 (2/2): E-book viewer: Disallow background images from
+ the internet. This was an unused feature anyway
+
+Forwarded: not-needed
+Bug: https://github.com/kovidgoyal/calibre/security/advisories/GHSA-4926-v9px-wv7v
+Origin: https://github.com/kovidgoyal/calibre/commit/b1ef6a8142b8dadeb7e72c250c65d42b36ee7118
+
+Signed-off-by: YOKOTA Hiroshi <[email protected]>
+---
+ src/calibre/gui2/viewer/web_view.py | 11 -----------
+ 1 file changed, 11 deletions(-)
+
+diff --git a/src/calibre/gui2/viewer/web_view.py b/src/calibre/gui2/viewer/web_view.py
+index e09688c..9c8e237 100644
+--- a/src/calibre/gui2/viewer/web_view.py
++++ b/src/calibre/gui2/viewer/web_view.py
+@@ -115,17 +115,6 @@ def background_image(encoded_fname=''):
+         with open(make_long_path_useable(img_path), 'rb') as f:
+             return mt, f.read()
+     except FileNotFoundError:
+-        if fname.startswith(('https://', 'http://')):
+-            from calibre import browser
+-            br = browser()
+-            try:
+-                with br.open(fname) as src:
+-                    data = src.read()
+-            except Exception:
+-                return mt, b''
+-            with open(make_long_path_useable(img_path), 'wb') as dest:
+-                dest.write(data)
+-            return mt, data
+         return mt, b''
+ 
+ 
diff -Nru calibre-8.5.0+ds/debian/patches/upstream/0094-CVE-2026-33206-TXT-Input-Ensure-resource-files-are-r.patch calibre-8.5.0+ds/debian/patches/upstream/0094-CVE-2026-33206-TXT-Input-Ensure-resource-files-are-r.patch
--- calibre-8.5.0+ds/debian/patches/upstream/0094-CVE-2026-33206-TXT-Input-Ensure-resource-files-are-r.patch	2026-05-25 08:19:53.000000000 +0900
+++ calibre-8.5.0+ds/debian/patches/upstream/0094-CVE-2026-33206-TXT-Input-Ensure-resource-files-are-r.patch	1970-01-01 09:00:00.000000000 +0900
@@ -1,27 +0,0 @@
-From: Kovid Goyal <[email protected]>
-Date: Mon, 16 Mar 2026 08:37:16 +0530
-Subject: CVE-2026-33206: TXT Input: Ensure resource files are read only from
- book contents
-
-Forwarded: not-needed
-Bug: https://github.com/kovidgoyal/calibre/security/advisories/GHSA-h3p4-m74f-43g6
-Origin: https://github.com/kovidgoyal/calibre/commit/c43f347837dbc00d9a7b5ff15a228b6f6081e290
-
-Signed-off-by: YOKOTA Hiroshi <[email protected]>
----
- src/calibre/ebooks/conversion/plugins/txt_input.py | 2 +-
- 1 file changed, 1 insertion(+), 1 deletion(-)
-
-diff --git a/src/calibre/ebooks/conversion/plugins/txt_input.py b/src/calibre/ebooks/conversion/plugins/txt_input.py
-index d6d43a6..6bc8289 100644
---- a/src/calibre/ebooks/conversion/plugins/txt_input.py
-+++ b/src/calibre/ebooks/conversion/plugins/txt_input.py
-@@ -112,7 +112,7 @@ class TXTInput(InputFormatPlugin):
-             src = img.get('src')
-             prefix = src.split(':', 1)[0].lower()
-             if src and prefix not in ('file', 'http', 'https', 'ftp') and not os.path.isabs(src):
--                src = os.path.join(base_dir, src)
-+                src = os.path.abspath(os.path.join(base_dir, src))
-                 if os.path.normcase(src).startswith(base_dir) and os.path.isfile(src) and os.access(src, os.R_OK):
-                     with open(src, 'rb') as f:
-                         data = f.read()
diff -Nru calibre-8.5.0+ds/debian/patches/upstream/0095-CVE-2026-33206-TXT-Input-Ensure-resource-files-are-r.patch calibre-8.5.0+ds/debian/patches/upstream/0095-CVE-2026-33206-TXT-Input-Ensure-resource-files-are-r.patch
--- calibre-8.5.0+ds/debian/patches/upstream/0095-CVE-2026-33206-TXT-Input-Ensure-resource-files-are-r.patch	1970-01-01 09:00:00.000000000 +0900
+++ calibre-8.5.0+ds/debian/patches/upstream/0095-CVE-2026-33206-TXT-Input-Ensure-resource-files-are-r.patch	2026-08-04 16:13:08.000000000 +0900
@@ -0,0 +1,27 @@
+From: Kovid Goyal <[email protected]>
+Date: Mon, 16 Mar 2026 08:37:16 +0530
+Subject: CVE-2026-33206: TXT Input: Ensure resource files are read only from
+ book contents
+
+Forwarded: not-needed
+Bug: https://github.com/kovidgoyal/calibre/security/advisories/GHSA-h3p4-m74f-43g6
+Origin: https://github.com/kovidgoyal/calibre/commit/c43f347837dbc00d9a7b5ff15a228b6f6081e290
+
+Signed-off-by: YOKOTA Hiroshi <[email protected]>
+---
+ src/calibre/ebooks/conversion/plugins/txt_input.py | 2 +-
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+diff --git a/src/calibre/ebooks/conversion/plugins/txt_input.py b/src/calibre/ebooks/conversion/plugins/txt_input.py
+index d6d43a6..6bc8289 100644
+--- a/src/calibre/ebooks/conversion/plugins/txt_input.py
++++ b/src/calibre/ebooks/conversion/plugins/txt_input.py
+@@ -112,7 +112,7 @@ class TXTInput(InputFormatPlugin):
+             src = img.get('src')
+             prefix = src.split(':', 1)[0].lower()
+             if src and prefix not in ('file', 'http', 'https', 'ftp') and not os.path.isabs(src):
+-                src = os.path.join(base_dir, src)
++                src = os.path.abspath(os.path.join(base_dir, src))
+                 if os.path.normcase(src).startswith(base_dir) and os.path.isfile(src) and os.access(src, os.R_OK):
+                     with open(src, 'rb') as f:
+                         data = f.read()

--===============4685015674154026920==
Content-Type: text/plain; charset="us-ascii"
MIME-Version: 1.0
Content-Transfer-Encoding: 7bit
Content-Disposition: attachment; filename="calibre_8.5.0+ds-1+deb13u4-minimize.diff"

diff --git a/debian/changelog b/debian/changelog
index 2c68c9e613..13caa1e364 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,15 @@
+calibre (8.5.0+ds-1+deb13u4) trixie; urgency=medium
+
+  * This update adds fixup for CVE-2026-25636 patch in 8.5.0+ds-1+deb13u2
+
+  * Set default Debian branch name for trixie
+  * CVE-2026-25636 (1/2): EPUB Input: Ensure font obfuscation processing
+    happens only on files from the EPUB (Closes: #1143484)
+  * Rename a CVE-2026-25636 patch in 8.5.0+ds-1+deb13u2
+  * Renumber patches
+
+ -- YOKOTA Hiroshi <[email protected]>  Tue, 04 Aug 2026 16:13:08 +0900
+
 calibre (8.5.0+ds-1+deb13u3) trixie; urgency=medium
 
   * Fix security vulnerabilities and code quality issues (Closes: #1135543)
diff --git a/debian/gbp.conf b/debian/gbp.conf
index 6b7cdfe43c..4cc8345f8e 100644
--- a/debian/gbp.conf
+++ b/debian/gbp.conf
@@ -1,5 +1,6 @@
 [DEFAULT]
 pristine-tar = True
+debian-branch = debian/trixie
 
 [dch]
 full = True
diff --git a/debian/patches/series b/debian/patches/series
index da92a1e411..8ee25d4e3d 100644
--- a/debian/patches/series
+++ b/debian/patches/series
@@ -81,14 +81,15 @@ pykakasi/0080-Revert-Fix-a-regression-that-caused-incorrect-Englis.patch
 0081-Revert-Update-7zip-wrapper-code-for-removal-of-read-.patch
 upstream/0082-Fix-CVE-2025-64486.patch
 upstream/0083-CVE-2026-25635-CHM-Input-Ignore-internal-files-that-.patch
-upstream/0084-CVE-2026-25636-DRYer.patch
-upstream/0085-CVE-2026-25731-ZIP-Output-Change-the-template-engine.patch
-upstream/0086-CVE-2026-26064-ODT-Input-Ensure-images-are-extracted.patch
-upstream/0087-CVE-2026-26065-PDB-Input-Ensure-extracted-images-are.patch
-upstream/0088-CVE-2026-27810-Content-server-Sanitize-content-dispo.patch
-upstream/0089-CVE-2026-27824-Content-server-When-banning-IPs-for-r.patch
-upstream/0090-Fix-security-vulnerabilities-and-code-quality-issues.patch
-upstream/0091-CVE-2026-30853-RB-Input-Ensure-files-are-extracted-w.patch
-upstream/0092-CVE-2026-33205-1-2-E-book-viewer-prevent-reading-bac.patch
-upstream/0093-CVE-2026-33205-2-2-E-book-viewer-Disallow-background.patch
-upstream/0094-CVE-2026-33206-TXT-Input-Ensure-resource-files-are-r.patch
+upstream/0084-CVE-2026-25636-1-2-EPUB-Input-Ensure-font-obfuscatio.patch
+upstream/0085-CVE-2026-25636-2-2-DRYer.patch
+upstream/0086-CVE-2026-25731-ZIP-Output-Change-the-template-engine.patch
+upstream/0087-CVE-2026-26064-ODT-Input-Ensure-images-are-extracted.patch
+upstream/0088-CVE-2026-26065-PDB-Input-Ensure-extracted-images-are.patch
+upstream/0089-CVE-2026-27810-Content-server-Sanitize-content-dispo.patch
+upstream/0090-CVE-2026-27824-Content-server-When-banning-IPs-for-r.patch
+upstream/0091-Fix-security-vulnerabilities-and-code-quality-issues.patch
+upstream/0092-CVE-2026-30853-RB-Input-Ensure-files-are-extracted-w.patch
+upstream/0093-CVE-2026-33205-1-2-E-book-viewer-prevent-reading-bac.patch
+upstream/0094-CVE-2026-33205-2-2-E-book-viewer-Disallow-background.patch
+upstream/0095-CVE-2026-33206-TXT-Input-Ensure-resource-files-are-r.patch
diff --git a/debian/patches/upstream/0084-CVE-2026-25636-1-2-EPUB-Input-Ensure-font-obfuscatio.patch b/debian/patches/upstream/0084-CVE-2026-25636-1-2-EPUB-Input-Ensure-font-obfuscatio.patch
new file mode 100644
index 0000000000..9f906d768a
--- /dev/null
+++ b/debian/patches/upstream/0084-CVE-2026-25636-1-2-EPUB-Input-Ensure-font-obfuscatio.patch
@@ -0,0 +1,63 @@
+From: Kovid Goyal <[email protected]>
+Date: Mon, 2 Feb 2026 09:51:18 +0530
+Subject: CVE-2026-25636 (1/2): EPUB Input: Ensure font obfuscation processing
+ happens only on files from the EPUB
+
+Forwarded: not-needed
+Bug: https://github.com/kovidgoyal/calibre/security/advisories/GHSA-8r26-m7j5-hm29
+Origin: backport, https://github.com/kovidgoyal/calibre/commit/267bfd34020a4f297c2de9cc0cde50ebe5d024d4
+Bug-Debian: https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1143484
+
+Signed-off-by: YOKOTA Hiroshi <[email protected]>
+---
+ src/calibre/ebooks/conversion/plugins/epub_input.py |  4 +++-
+ src/calibre/utils/filenames.py                      | 14 ++++++++++++++
+ 2 files changed, 17 insertions(+), 1 deletion(-)
+
+diff --git a/src/calibre/ebooks/conversion/plugins/epub_input.py b/src/calibre/ebooks/conversion/plugins/epub_input.py
+index 2505169..f62fed1 100644
+--- a/src/calibre/ebooks/conversion/plugins/epub_input.py
++++ b/src/calibre/ebooks/conversion/plugins/epub_input.py
+@@ -44,6 +44,8 @@ class EPUBInput(InputFormatPlugin):
+         import uuid
+ 
+         from lxml import etree
++
++        from calibre.utils.filenames import is_existing_subpath
+         idpf_key = opf.raw_unique_identifier
+         if idpf_key:
+             idpf_key = re.sub(r'[ \t\r\n]', '', idpf_key)
+@@ -74,7 +76,7 @@ class EPUBInput(InputFormatPlugin):
+                 uri = cr.get('URI')
+                 path = os.path.abspath(os.path.join(os.path.dirname(encfile), '..', *uri.split('/')))
+                 tkey = (key if algorithm == ADOBE_OBFUSCATION else idpf_key)
+-                if (tkey and os.path.exists(path)):
++                if (tkey and is_existing_subpath(path, os.getcwd())):
+                     self._encrypted_font_uris.append(uri)
+                     decrypt_font(tkey, path, algorithm)
+             return True
+diff --git a/src/calibre/utils/filenames.py b/src/calibre/utils/filenames.py
+index 5182f75..1cb07f4 100644
+--- a/src/calibre/utils/filenames.py
++++ b/src/calibre/utils/filenames.py
+@@ -644,6 +644,20 @@ def copytree_using_links(path, dest, dest_is_parent=True, filecopyfunc=copyfile)
+                 filecopyfunc(src, df)
+ 
+ 
++def is_existing_subpath(child: str, parent: str) -> bool:
++    ' Check if child is under parent. If either child or parent dont exist, returns False. '
++    try:
++        parent = os.path.realpath(parent, strict=True)  # resolve symlinks
++        child = os.path.realpath(child, strict=True)
++    except OSError:
++        return False
++    parent = os.path.abspath(parent)
++    child = os.path.abspath(child)
++    if not parent.endswith(os.sep):
++        parent += os.sep
++    return child.startswith(parent)
++
++
+ rmtree = shutil.rmtree
+ 
+ 
diff --git a/debian/patches/upstream/0084-CVE-2026-25636-DRYer.patch b/debian/patches/upstream/0085-CVE-2026-25636-2-2-DRYer.patch
similarity index 89%
rename from debian/patches/upstream/0084-CVE-2026-25636-DRYer.patch
rename to debian/patches/upstream/0085-CVE-2026-25636-2-2-DRYer.patch
index c4d08af444..e057ec930c 100644
--- a/debian/patches/upstream/0084-CVE-2026-25636-DRYer.patch
+++ b/debian/patches/upstream/0085-CVE-2026-25636-2-2-DRYer.patch
@@ -1,6 +1,6 @@
 From: Kovid Goyal <[email protected]>
 Date: Mon, 2 Feb 2026 11:25:09 +0530
-Subject: CVE-2026-25636: DRYer
+Subject: CVE-2026-25636 (2/2): DRYer
 
 Forwarded: not-needed
 Bug: https://github.com/kovidgoyal/calibre/security/advisories/GHSA-8r26-m7j5-hm29
@@ -12,10 +12,10 @@ Signed-off-by: YOKOTA Hiroshi <[email protected]>
  1 file changed, 4 insertions(+), 2 deletions(-)
 
 diff --git a/src/calibre/ebooks/conversion/plugins/epub_input.py b/src/calibre/ebooks/conversion/plugins/epub_input.py
-index 2505169..edfebee 100644
+index f62fed1..6b81949 100644
 --- a/src/calibre/ebooks/conversion/plugins/epub_input.py
 +++ b/src/calibre/ebooks/conversion/plugins/epub_input.py
-@@ -66,15 +66,17 @@ class EPUBInput(InputFormatPlugin):
+@@ -68,15 +68,17 @@ class EPUBInput(InputFormatPlugin):
  
          try:
              root = etree.parse(encfile)
@@ -30,7 +30,7 @@ index 2505169..edfebee 100644
 -                path = os.path.abspath(os.path.join(os.path.dirname(encfile), '..', *uri.split('/')))
 +                path = os.path.abspath(os.path.join(base, '..', *uri.split('/')))
                  tkey = (key if algorithm == ADOBE_OBFUSCATION else idpf_key)
--                if (tkey and os.path.exists(path)):
+-                if (tkey and is_existing_subpath(path, os.getcwd())):
 +                if (tkey and is_existing_subpath(path, container_base)):
                      self._encrypted_font_uris.append(uri)
                      decrypt_font(tkey, path, algorithm)
diff --git a/debian/patches/upstream/0085-CVE-2026-25731-ZIP-Output-Change-the-template-engine.patch b/debian/patches/upstream/0086-CVE-2026-25731-ZIP-Output-Change-the-template-engine.patch
similarity index 100%
rename from debian/patches/upstream/0085-CVE-2026-25731-ZIP-Output-Change-the-template-engine.patch
rename to debian/patches/upstream/0086-CVE-2026-25731-ZIP-Output-Change-the-template-engine.patch
diff --git a/debian/patches/upstream/0086-CVE-2026-26064-ODT-Input-Ensure-images-are-extracted.patch b/debian/patches/upstream/0087-CVE-2026-26064-ODT-Input-Ensure-images-are-extracted.patch
similarity index 100%
rename from debian/patches/upstream/0086-CVE-2026-26064-ODT-Input-Ensure-images-are-extracted.patch
rename to debian/patches/upstream/0087-CVE-2026-26064-ODT-Input-Ensure-images-are-extracted.patch
diff --git a/debian/patches/upstream/0087-CVE-2026-26065-PDB-Input-Ensure-extracted-images-are.patch b/debian/patches/upstream/0088-CVE-2026-26065-PDB-Input-Ensure-extracted-images-are.patch
similarity index 100%
rename from debian/patches/upstream/0087-CVE-2026-26065-PDB-Input-Ensure-extracted-images-are.patch
rename to debian/patches/upstream/0088-CVE-2026-26065-PDB-Input-Ensure-extracted-images-are.patch
diff --git a/debian/patches/upstream/0088-CVE-2026-27810-Content-server-Sanitize-content-dispo.patch b/debian/patches/upstream/0089-CVE-2026-27810-Content-server-Sanitize-content-dispo.patch
similarity index 100%
rename from debian/patches/upstream/0088-CVE-2026-27810-Content-server-Sanitize-content-dispo.patch
rename to debian/patches/upstream/0089-CVE-2026-27810-Content-server-Sanitize-content-dispo.patch
diff --git a/debian/patches/upstream/0089-CVE-2026-27824-Content-server-When-banning-IPs-for-r.patch b/debian/patches/upstream/0090-CVE-2026-27824-Content-server-When-banning-IPs-for-r.patch
similarity index 100%
rename from debian/patches/upstream/0089-CVE-2026-27824-Content-server-When-banning-IPs-for-r.patch
rename to debian/patches/upstream/0090-CVE-2026-27824-Content-server-When-banning-IPs-for-r.patch
diff --git a/debian/patches/upstream/0090-Fix-security-vulnerabilities-and-code-quality-issues.patch b/debian/patches/upstream/0091-Fix-security-vulnerabilities-and-code-quality-issues.patch
similarity index 100%
rename from debian/patches/upstream/0090-Fix-security-vulnerabilities-and-code-quality-issues.patch
rename to debian/patches/upstream/0091-Fix-security-vulnerabilities-and-code-quality-issues.patch
diff --git a/debian/patches/upstream/0091-CVE-2026-30853-RB-Input-Ensure-files-are-extracted-w.patch b/debian/patches/upstream/0092-CVE-2026-30853-RB-Input-Ensure-files-are-extracted-w.patch
similarity index 100%
rename from debian/patches/upstream/0091-CVE-2026-30853-RB-Input-Ensure-files-are-extracted-w.patch
rename to debian/patches/upstream/0092-CVE-2026-30853-RB-Input-Ensure-files-are-extracted-w.patch
diff --git a/debian/patches/upstream/0092-CVE-2026-33205-1-2-E-book-viewer-prevent-reading-bac.patch b/debian/patches/upstream/0093-CVE-2026-33205-1-2-E-book-viewer-prevent-reading-bac.patch
similarity index 100%
rename from debian/patches/upstream/0092-CVE-2026-33205-1-2-E-book-viewer-prevent-reading-bac.patch
rename to debian/patches/upstream/0093-CVE-2026-33205-1-2-E-book-viewer-prevent-reading-bac.patch
diff --git a/debian/patches/upstream/0093-CVE-2026-33205-2-2-E-book-viewer-Disallow-background.patch b/debian/patches/upstream/0094-CVE-2026-33205-2-2-E-book-viewer-Disallow-background.patch
similarity index 100%
rename from debian/patches/upstream/0093-CVE-2026-33205-2-2-E-book-viewer-Disallow-background.patch
rename to debian/patches/upstream/0094-CVE-2026-33205-2-2-E-book-viewer-Disallow-background.patch
diff --git a/debian/patches/upstream/0094-CVE-2026-33206-TXT-Input-Ensure-resource-files-are-r.patch b/debian/patches/upstream/0095-CVE-2026-33206-TXT-Input-Ensure-resource-files-are-r.patch
similarity index 100%
rename from debian/patches/upstream/0094-CVE-2026-33206-TXT-Input-Ensure-resource-files-are-r.patch
rename to debian/patches/upstream/0095-CVE-2026-33206-TXT-Input-Ensure-resource-files-are-r.patch

--===============4685015674154026920==--