fix: tolerate non-UTF-8 soffice output in doc/ppt conversion - #4465
fix: tolerate non-UTF-8 soffice output in doc/ppt conversion#4465huuufu wants to merge 1 commit into
Conversation
convert_office_doc() decoded soffice stdout and stderr with a strict UTF-8 decode, used only for logging and to check whether stdout was empty. LibreOffice echoes the input path using the console encoding, which on Windows is the locale codepage, so a document whose name or path contains multi-byte characters raised UnicodeDecodeError and aborted a conversion that had already succeeded -- the traceback in Unstructured-IO#3652 is on the first stdout decode. The stderr site is reachable and broken the same way when a conversion fails. All three sites now go through one helper using errors="backslashreplace", which keeps the message pure ASCII: still loggable by a handler whose stream uses the locale codepage (it cannot encode U+FFFD), and showing the offending bytes. Resolves Unstructured-IO#3652
|
Prior art: #3830 patched the first of these three sites in 2024; the open question there was a 1-page |
Summary
convert_office_doc()— shared bypartition_doc()andpartition_ppt()— decodedsofficestdout and stderr with a strict UTF-8.decode()at three sites, used only for logging and to check whether stdout was empty (which drives the retry loop and the final failure check). LibreOffice echoes the input path in the console encoding, which on Windows is the locale codepage rather than UTF-8, so a name or path with multi-byte characters makes the decode raise and aborts a conversion that had already succeeded:The traceback in #3652 is on the first stdout decode. The stderr site is reachable and broken the same way when a conversion fails, and sits outside the surrounding
tryentirely.Fixing the child process's output encoding is not an option — @Snowman-s confirmed on the issue that
PYTHONIOENCODING=utf-8:surrogateescapeandsys.stdout.reconfigure()both leave the exception unchanged.Fix
All three sites go through one
_decode_soffice_output()helper decoding witherrors="backslashreplace".@scanny weighed three options on the issue and voted for option 1,
try/except UnicodeDecodeErrorfalling back tostr(bytes). A codec error handler gets the same tolerance in one expression with no rarely-exercised second branch, and escapes only the offending bytes rather than turning a perfectly good line into arepr. Option 2 — detect Windows and decode with the locale encoding — would render the filename correctly when the guess is right, but the console output codepage need not matchlocale.getpreferredencoding()— anything that ranchcpupstream moves one and not the other — and a wrong guess is silent mojibake rather than a visible escape. It layers cleanly on top of this later if you want it.backslashreplacerather thanreplacebecause U+FFFD is not encodable in the very codepages this bug is about: alogging.FileHandlerwhose stream uses cp932 or cp936 cannot encode it and drops the whole record, soreplacewould stop the abort and then throw away the diagnostic.backslashreplacekeeps the line pure ASCII and shows the actual bytes — closer to what option 1 was reaching for.One behavior change worth flagging: stdout consisting entirely of undecodable bytes now yields a non-empty string, so with
returncode == 0it no longer trips the empty-stdout heuristic the code uses to detect a silent failure. Realsofficeoutput carries ASCII scaffolding around the path, so this needs a degenerate payload, but it is a real narrowing of that heuristic — one option 1 shares, sincestr()of non-empty bytes is never empty either. Theb""that the heuristic actually watches for still decodes to"".Testing
No LibreOffice needed — stub
subprocess.runwith the bytessofficeemits on a Japanese console:Raises
UnicodeDecodeErroronmain; returns normally here.test_convert_office_doc_survives_non_utf8_soffice_outputis parametrized over stdout and stderr so both decode paths are covered, and asserts the escaped bytes reach the log and that the record is ASCII-encodable — it fails onmain, and also fails if the helper is switched toerrors="replace".test_common.py: 63 passed / 2 failed here, 61 passed / 4 failed with the same file onmain— the extra two are the new test's stdout and stderr cases. The other two fail identically on both (FileNotFoundError: soffice command was not found; no LibreOffice on this machine).ruff check .passes repo-wide andruff format --checkpasses on both changed files (repo-wide it exits non-zero on theexample-docs/umlauts-non-utf8.mdfixture, identically onmain).Not verified against a real
soffice: the root cause is from @Snowman-s's and @scanny's analysis on the issue, and the CP932 payload is my own reconstruction of it.Resolves #3652