epub2md/tests/test_epub_reader.py

139 lines
5.5 KiB
Python
Raw Permalink Normal View History

import zipfile
import yaml
from conftest import PNG, Book, Doc, build_epub
def test_minimal_epub2_with_ncx(convert):
book = Book(version="2.0", ncx=True, docs=[Doc("ch1.xhtml", "<h1>Erstes Kapitel</h1><p>Hallo Welt.</p>", "Eins")])
run = convert(book)
assert run.rc == 0
text = run.chapter("001")
assert "# Erstes Kapitel" in text
assert "Hallo Welt." in text
assert run.report["epub"]["version"] == "2.0"
assert run.report["epub"]["ncx"] == "OEBPS/toc.ncx"
def test_minimal_epub3_with_nav_and_metadata(convert):
book = Book(
authors=["Anna A.", "Bert B."],
docs=[Doc("a.xhtml", "<h1>A</h1><p>x</p>"), Doc("b.xhtml", "<h1>B</h1><p>y</p>")],
)
run = convert(book)
assert run.rc == 0
meta = yaml.safe_load((run.out / "metadata.yaml").read_text(encoding="utf-8"))
assert meta["title"] == "Testbuch"
assert meta["authors"] == ["Anna A.", "Bert B."]
assert meta["language"] == "de"
assert meta["identifier"] == "urn:isbn:9783161484100"
assert meta["isbn"] == "9783161484100"
assert meta["epub_version"] == "3.0"
assert len(meta["source_sha256"]) == 64
assert "subtitle" not in meta # nothing invented
assert run.report["epub"]["nav"] == "OEBPS/nav.xhtml"
def test_spine_order_is_respected_not_file_names(convert):
docs = [Doc("z.xhtml", "<h1>Zuerst</h1><p>1</p>"), Doc("a.xhtml", "<h1>Danach</h1><p>2</p>")]
run = convert(Book(docs=docs))
names = [p.name for p in run.chapters()]
assert names == ["001-zuerst.md", "002-danach.md"]
def test_cover_image_is_extracted_first(convert):
book = Book(docs=[Doc("a.xhtml", "<h1>A</h1><p>x</p>")], media={"img/cover.png": PNG}, cover="img/cover.png")
run = convert(book)
meta = yaml.safe_load((run.out / "metadata.yaml").read_text(encoding="utf-8"))
assert meta["cover_image"] == "media/images/0001-cover.png"
assert (run.out / meta["cover_image"]).read_bytes() == PNG
def test_missing_container_returns_exit_code_2(tmp_path, convert):
path = tmp_path / "broken.epub"
with zipfile.ZipFile(path, "w") as z:
z.writestr("mimetype", "application/epub+zip")
z.writestr("OEBPS/x.xhtml", "<html/>")
run = convert(path)
assert run.rc == 2
assert "container.xml" in run.stderr
assert not run.out.exists()
def test_not_a_zip_returns_exit_code_2(tmp_path, convert):
path = tmp_path / "kaputt.epub"
path.write_bytes(b"this is not a zip")
run = convert(path)
assert run.rc == 2
def test_broken_opf_spine_reference(convert):
book = Book(docs=[Doc("a.xhtml", "<h1>A</h1><p>x</p>")], spine_extra='<itemref idref="does-not-exist"/>')
run = convert(book)
assert run.rc == 0
assert any(e["code"] == "spine-idref-missing" for e in run.report["errors"])
strict = convert(book, "--strict")
assert strict.rc == 3
assert not strict.out.exists()
def test_drm_encrypted_epub_is_rejected(tmp_path, convert):
book = Book(docs=[Doc("a.xhtml", "<h1>A</h1><p>x</p>")],
extra_entries={"META-INF/encryption.xml": b"""<encryption xmlns="urn:oasis:names:tc:opendocument:xmlns:container"
xmlns:enc="http://www.w3.org/2001/04/xmlenc#"><enc:EncryptedData><enc:EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#aes128-cbc"/>
<enc:CipherData><enc:CipherReference URI="OEBPS/a.xhtml"/></enc:CipherData></enc:EncryptedData></encryption>"""})
run = convert(book)
assert run.rc == 2
assert "DRM" in run.stderr
def test_unsafe_zip_paths_are_never_written(tmp_path, convert):
book = Book(
docs=[Doc("a.xhtml", '<h1>A</h1><p>x <a href="../../outside.txt">raus</a></p>')],
extra_entries={"../../outside.txt": b"evil", "/absolute/path.txt": b"evil", "OEBPS/../../escape.xhtml": b"evil"},
)
run = convert(book)
assert run.rc == 0
assert any(e["code"] == "unsafe-zip-path" for e in run.report["errors"])
assert not (tmp_path / "outside.txt").exists()
assert not (tmp_path.parent / "outside.txt").exists()
for f in run.out.rglob("*"):
assert run.out.resolve() in f.resolve().parents
assert convert(book, "--strict").rc == 2
def test_xml_external_entities_are_not_resolved(tmp_path, convert):
secret = tmp_path / "secret.txt"
secret.write_text("GEHEIM")
body = "<h1>A</h1><p>&xxe;</p>"
book = Book(docs=[Doc("a.xhtml", body)])
path = build_epub(tmp_path / "xxe.epub", book)
# inject a DOCTYPE with an external entity into the chapter
with zipfile.ZipFile(path) as z:
entries = {n: z.read(n) for n in z.namelist()}
entries["OEBPS/a.xhtml"] = (
f'<?xml version="1.0"?><!DOCTYPE html [<!ENTITY xxe SYSTEM "file://{secret}">]>'
'<html xmlns="http://www.w3.org/1999/xhtml"><head><title>a</title></head><body><h1>A</h1><p>&xxe;</p></body></html>'
).encode()
with zipfile.ZipFile(path, "w") as z:
for n, d in entries.items():
z.writestr(n, d)
run = convert(path)
assert run.rc == 0
assert "GEHEIM" not in run.all_md()
def test_invalid_utf8_is_reported(tmp_path, convert):
book = Book(docs=[Doc("a.xhtml", "<h1>A</h1><p>ok</p>")])
path = build_epub(tmp_path / "enc.epub", book)
with zipfile.ZipFile(path) as z:
entries = {n: z.read(n) for n in z.namelist()}
entries["OEBPS/a.xhtml"] = entries["OEBPS/a.xhtml"].replace(b"ok", b"kaputt \xff\xfe ende")
with zipfile.ZipFile(path, "w") as z:
for n, d in entries.items():
z.writestr(n, d)
run = convert(path)
assert run.rc == 0
assert any(w["code"] == "decode-error" for w in run.report["warnings"])
assert "ende" in run.all_md()