Files
epub_bilingual_translator/tests/test_fonts.py
T

104 lines
7.0 KiB
Python

import hashlib
import uuid
import zipfile
import pytest
from lxml import etree
from src.edition.fonts import ADOBE, IDPF, _key, decoded_fonts, primary_identifier, rebind_fonts
OPF = 'http://www.idpf.org/2007/opf'
def key(identifier, algorithm):
if algorithm == IDPF:
return hashlib.sha1(''.join(identifier.split()).encode()).digest()
return uuid.UUID(identifier.removeprefix('urn:uuid:')).bytes
def xor(data, identifier, algorithm):
k = key(identifier, algorithm); n = 1040 if algorithm == IDPF else 1024
return bytes(b ^ k[i % len(k)] for i, b in enumerate(data[:n])) + data[n:]
def fixture(tmp_path, algorithm):
old = 'urn:uuid:' + str(uuid.UUID('12345678-1234-5678-1234-567812345678'))
new = 'urn:uuid:' + str(uuid.UUID('87654321-4321-8765-4321-876543218765'))
plain = b'\x00\x01\x00\x00' + bytes(range(256)) * 20
encrypted = xor(plain, old, algorithm)
opf = f'''<package xmlns="{OPF}" unique-identifier="uid"><metadata><identifier id="uid">{old}</identifier></metadata><manifest><item id="font" href="fonts/a.ttf" media-type="application/x-font-truetype"/><item id="css" href="styles/main.css" media-type="text/css"/><item id="page" href="text/ch.xhtml" media-type="application/xhtml+xml"/></manifest></package>'''
enc = f'''<encryption xmlns="http://www.w3.org/2001/04/xmlenc#"><EncryptedData><EncryptionMethod Algorithm="{algorithm}"/><CipherData><CipherReference URI="EPUB/fonts/a.ttf"/></CipherData></EncryptedData></encryption>'''
path = tmp_path / 'book.epub'
with zipfile.ZipFile(path, 'w') as z:
z.writestr('EPUB/package.opf', opf)
z.writestr('META-INF/encryption.xml', enc)
z.writestr('EPUB/fonts/a.ttf', encrypted)
z.writestr('EPUB/styles/main.css', b'@font-face {src:url("./../fonts/%61.ttf")} /* url(https://example.org/fonts/a.ttf) */')
z.writestr('EPUB/text/ch.xhtml', '<html xmlns="http://www.w3.org/1999/xhtml"><head><link rel="preload" as="font" href="../fonts/./a.ttf"/><style>@font-face{src:url(../fonts/a.ttf)}</style></head><body><p style="font-family:url(../fonts/a.ttf)">x</p></body></html>')
root = etree.fromstring(opf.encode())
return path, root, old, new, plain, encrypted
@pytest.mark.parametrize('algorithm', [IDPF, ADOBE])
def test_decode_and_rebind_preserves_original_and_rekeys_css_urls(tmp_path, algorithm):
path, root, old, new, plain, encrypted = fixture(tmp_path, algorithm)
with zipfile.ZipFile(path) as z:
assert primary_identifier(root) == old
assert decoded_fonts(z, 'EPUB/package.opf', root) == {'EPUB/fonts/a.ttf': plain}
additions, replacements = {}, {}
records = rebind_fonts(z, 'EPUB/package.opf', root, old, new, additions, replacements)
assert len(records) == 1 and records[0]['algorithm'] == algorithm
assert records[0]['original_sha256'] == hashlib.sha256(encrypted).hexdigest()
assert records[0]['decoded_sha256'] == hashlib.sha256(plain).hexdigest()
assert additions['EPUB/fonts/edition-a.ttf'] == xor(plain, new, algorithm)
assert z.read('EPUB/fonts/a.ttf') == encrypted
assert root.xpath('//*[local-name()="item" and @id="font"]/@href') == ['fonts/edition-a.ttf']
assert root.xpath('//*[local-name()="item" and @id="font-source"]/@media-type') == ['application/octet-stream']
assert b'../fonts/edition-a.ttf' in replacements['EPUB/styles/main.css']
assert b'../fonts/edition-a.ttf' in replacements['EPUB/text/ch.xhtml']
assert b'https://example.org/fonts/a.ttf' in replacements['EPUB/styles/main.css']
assert b'edition-a.ttf' in replacements['EPUB/text/ch.xhtml']
enc = etree.fromstring(replacements['META-INF/encryption.xml'])
assert enc.xpath('//*[local-name()="CipherReference"]/@URI') == ['EPUB/fonts/edition-a.ttf']
# The replacement payload decodes under the new primary identifier.
root.xpath('//*[local-name()="identifier" and @id="uid"]')[0].text = new
replacements['EPUB/package.opf'] = etree.tostring(root)
class ZipView:
def read(self, name):
if name in replacements: return replacements[name]
return additions[name] if name in additions else z.read(name)
def namelist(self): return z.namelist() + list(additions)
view = ZipView()
decoded = decoded_fonts(view, 'EPUB/package.opf', root)
assert decoded['EPUB/fonts/edition-a.ttf'] == plain
def test_rejects_unknown_algorithm_and_non_font_payload(tmp_path):
path, root, old, new, plain, encrypted = fixture(tmp_path, 'urn:unknown')
with zipfile.ZipFile(path) as z, pytest.raises(ValueError, match='Unsupported EPUB encryption algorithm'):
decoded_fonts(z, 'EPUB/package.opf', root)
path, root, old, new, plain, encrypted = fixture(tmp_path, IDPF)
with zipfile.ZipFile(path, 'w') as z:
z.writestr('EPUB/package.opf', f'''<package xmlns="{OPF}" unique-identifier="uid"><metadata><identifier id="uid">{old}</identifier></metadata><manifest><item id="font" href="fonts/a.ttf" media-type="font/ttf"/></manifest></package>''')
z.writestr('META-INF/encryption.xml', f'''<encryption><EncryptedData><EncryptionMethod Algorithm="{IDPF}"/><CipherData><CipherReference URI="EPUB/fonts/a.ttf"/></CipherData></EncryptedData></encryption>''')
z.writestr('EPUB/fonts/a.ttf', xor(b'not-a-font'+bytes(1200), old, IDPF))
root = etree.fromstring(f'''<package xmlns="{OPF}" unique-identifier="uid"><metadata><identifier id="uid">{old}</identifier></metadata><manifest><item id="font" href="fonts/a.ttf" media-type="font/ttf"/></manifest></package>'''.encode())
with zipfile.ZipFile(path) as z, pytest.raises(ValueError, match='recognized font signature'):
decoded_fonts(z, 'EPUB/package.opf', root)
def test_rejects_unsafe_cipher_path(tmp_path):
path, root, old, new, plain, encrypted = fixture(tmp_path, IDPF)
with zipfile.ZipFile(path, 'w') as z:
z.writestr('EPUB/package.opf', f'''<package xmlns="{OPF}" unique-identifier="uid"><metadata><identifier id="uid">{old}</identifier></metadata><manifest><item id="font" href="fonts/a.ttf" media-type="font/ttf"/></manifest></package>''')
z.writestr('META-INF/encryption.xml', f'''<encryption><EncryptedData><EncryptionMethod Algorithm="{IDPF}"/><CipherData><CipherReference URI="../../outside.ttf"/></CipherData></EncryptedData></encryption>''')
root = etree.fromstring(f'''<package xmlns="{OPF}" unique-identifier="uid"><metadata><identifier id="uid">{old}</identifier></metadata><manifest><item id="font" href="fonts/a.ttf" media-type="font/ttf"/></manifest></package>'''.encode())
with zipfile.ZipFile(path) as z, pytest.raises(ValueError, match='escapes EPUB root'):
decoded_fonts(z, 'EPUB/package.opf', root)
def test_idpf_whitespace_removal_is_limited_to_xml_whitespace():
assert _key('ab c\td\re\nf', IDPF) == hashlib.sha1(b'abcdef').digest()
# Unicode whitespace is not part of XML's four whitespace code points.
assert _key('ab\u00a0c', IDPF) == hashlib.sha1('ab\u00a0c'.encode()).digest()