fix: add a missing description to Office files instead of silently writing nothing

This commit is contained in:
randogoth 2026-10-24 22:19:13 +03:00
parent 93661d664a
commit c1bc07ffeb
3 changed files with 70 additions and 4 deletions

View file

@ -5,6 +5,7 @@ import os
import shutil
from pathlib import Path
from zipfile import ZipFile
from orvanitag.errors import UnsupportedFile
def rewrite(path, changes: dict = None, comment: bytes = None):
@ -21,6 +22,9 @@ def rewrite(path, changes: dict = None, comment: bytes = None):
# Copying every ZipInfo keeps entry order and compression, which ODF and EPUB
# require for their leading, uncompressed 'mimetype' entry
with ZipFile(path) as source, ZipFile(tmp, 'w') as target:
missing = set(changes) - set(source.namelist())
if missing:
raise UnsupportedFile(f'{path} has no {", ".join(sorted(missing))}')
for info in source.infolist():
data = source.read(info)
if info.filename in changes:

View file

@ -7,7 +7,10 @@ from orvanitag.errors import UnsupportedFile
NAME = 'office'
TYPES = ['.doc', '.xls', '.ppt', '.docx', '.xlsx', '.pptx', '.odt', '.ods', '.odp']
XML_REGEX = re.compile(r"(?P<pre><dc:description>)(?P<body>.*?)(?P<post><\/dc:description>)", re.S)
DC = 'http://purl.org/dc/elements/1.1/'
DESCRIPTION = re.compile(r'<dc:description\s*/>|<dc:description>(?P<body>.*?)</dc:description>', re.S)
# The end of the metadata block a missing description is added to
METADATA_END = re.compile(r'</(?:[\w-]+:)?(?:meta|coreProperties)>')
# Detected by suffix because mimetypes doesn't know the ODF/OOXML types on every system
META_FILES = {
@ -28,7 +31,7 @@ def read(path, config):
if suffix in META_FILES:
with ZipFile(path) as archive:
meta = archive.read(META_FILES[suffix]).decode('utf-8')
found = XML_REGEX.search(meta)
found = DESCRIPTION.search(meta)
if found and found.group('body'):
return Entry.parse(unescape(found.group('body')).split())
@ -48,5 +51,21 @@ def write(path, entry, config):
if suffix not in META_FILES:
raise UnsupportedFile(f'writing metadata to {suffix} files is not supported')
string = escape(str(entry))
rewrite(path, {META_FILES[suffix]: lambda meta: XML_REGEX.sub(lambda m: m['pre'] + string + m['post'], meta)})
rewrite(path, {META_FILES[suffix]: lambda meta: describe(meta, escape(str(entry)))})
def describe(meta: str, string: str) -> str:
"""Sets the text of the description element, adding the element if there is none"""
found = DESCRIPTION.search(meta)
if found and found.group('body') is not None:
return meta[:found.start('body')] + string + meta[found.end('body'):]
start, end = (found.start(), found.end()) if found else (None, None)
if not found:
closing = METADATA_END.search(meta)
if not closing:
raise UnsupportedFile('the document properties have no metadata block')
start = end = closing.start()
rest = meta[:start] + meta[end:]
declare = '' if 'xmlns:dc=' in rest else f' xmlns:dc="{DC}"'
return meta[:start] + f'<dc:description{declare}>{string}</dc:description>' + meta[end:]

View file

@ -186,3 +186,46 @@ def test_cmd_messages_show_square_brackets_in_file_names(cwd, monkeypatch, capsy
monkeypatch.setattr(sys, 'argv', ['otag', 'a', 'a [demo].mp3', '#lofi'])
main()
assert 'a [demo].mp3' in capsys.readouterr().out
def make_zip(path, members):
with zipfile.ZipFile(path, 'w') as z:
for name, data in members.items():
z.writestr(name, data)
def core_xml(cwd, name):
with zipfile.ZipFile(cwd / name) as z:
return z.read('docProps/core.xml').decode()
def test_docx_without_description_gets_one(cwd):
make_zip(cwd / 'a.docx', {'docProps/core.xml': DOCX_CORE.replace('<dc:description></dc:description>', '')})
roundtrip(cwd, 'a.docx')
assert '<dc:title>T</dc:title><dc:description>+proj #lofi R&amp;B C:\\1</dc:description></cp:coreProperties>' in core_xml(cwd, 'a.docx')
def test_self_closing_description_is_replaced(cwd):
make_zip(cwd / 'a.docx', {'docProps/core.xml': DOCX_CORE.replace('<dc:description></dc:description>', '<dc:description/>')})
roundtrip(cwd, 'a.docx')
assert core_xml(cwd, 'a.docx').count('dc:description') == 2
def test_inserted_description_declares_its_namespace(cwd):
import xml.etree.ElementTree as ET
core = '<cp:coreProperties xmlns:cp="http://schemas.openxmlformats.org/package/2006/metadata/core-properties"></cp:coreProperties>'
make_zip(cwd / 'a.docx', {'docProps/core.xml': core})
OrvaniTag().add('a.docx', '#lofi')
OrvaniTag().write_metadata('a.docx', 'office')
root = ET.fromstring(core_xml(cwd, 'a.docx'))
assert root.find('{http://purl.org/dc/elements/1.1/}description').text == '#lofi'
def test_docx_without_core_properties_is_refused(cwd):
from orvanitag.errors import UnsupportedFile
make_zip(cwd / 'a.docx', {'[Content_Types].xml': '<Types/>'})
before = (cwd / 'a.docx').read_bytes()
OrvaniTag().add('a.docx', '#lofi')
with pytest.raises(UnsupportedFile):
OrvaniTag().write_metadata('a.docx', 'office')
assert (cwd / 'a.docx').read_bytes() == before