Files
OCRmyPDF/tests/plugins/stdout_polluter.py
James R. Barlow a13d27bfb5 Protect stdout from corruption when writing PDF to standard output
Writing the output PDF to stdout (ocrmypdf in.pdf -) previously relied on
an honor system: no in-process code -- third-party libraries, plugins, or
stray print() calls -- was supposed to write to stdout, enforced only
indirectly. A single accidental write to fd 1 would silently corrupt the
output PDF.

Enforce this at the OS level. At CLI startup, before plugins load or any
worker process/thread starts, save the real stdout via os.dup() and point
fd 1 at stderr, so stray writes are diverted to stderr while only the final
"produce the PDF" step writes to the preserved descriptor. Exposed as the
opt-in public API function configure_stdout_protection(), mirroring
configure_logging(); it is not enabled inside ocr() so in-process library
users keep their own stdout.

Also fix check_requested_output_file() to test the preserved real stdout
for tty-ness, since after the redirect sys.stdout reports stderr's status.

Fold unreleased v17.7.2 notes into v17.8.0.
2026-06-28 12:02:34 -07:00

43 lines
1.1 KiB
Python

# SPDX-FileCopyrightText: 2024 James R. Barlow
# SPDX-License-Identifier: MIT
"""Test plugin that deliberately writes garbage to stdout.
Used to verify that OCRmyPDF's stdout protection diverts stray writes (from
plugins or libraries) to stderr, so that a PDF written to stdout is never
corrupted. Pollutes at three points: plugin import (main process), the
``validate`` hook (main process), and the ``filter_ocr_image`` hook (worker
process/thread).
"""
from __future__ import annotations
import os
import sys
from ocrmypdf import hookimpl
POLLUTION = b'POLLUTION'
def _pollute(where: bytes) -> None:
# Write to file descriptor 1 directly (as a careless C library might) and
# via Python's sys.stdout (as a stray print() might).
os.write(1, POLLUTION + b'-fd1-' + where + b'\n')
print(POLLUTION.decode() + '-stdout-' + where.decode())
sys.stdout.flush()
# Pollute at import time, which happens while plugins are being loaded.
_pollute(b'import')
@hookimpl
def validate(pdfinfo, options):
_pollute(b'validate')
@hookimpl
def filter_ocr_image(page, image):
_pollute(b'filter_ocr_image')
return image