Files
Martin Vogel 24593cdb9d pdf: forms read again are bounded per document
A Form XObject that drew the next one many times, level under level,
was read (draws ^ depth) times: a few kilobytes asked for an unbounded
reading, and the page text grew with every leaf (D2 of the document
layer security review). The forms' text cannot be read faithfully; it is
exponential in the file.

The first reading of each form in a document stays whole. Reading one
again (the same content drawn elsewhere: a logo, a header on every page)
is counted in decoded bytes; beyond 256 MiB of such re-readings the
document reads no form again, and each skip is counted
(form_rereads_cut), as a decoded stream above 512 MiB already is. Real
documents re-read small forms: the 749-PDF dev corpus extracts
byte-identically as before (720 of 720 documents, 1,492 of 1,492 pages;
flattened copies too). Reading each form only once per page was tried
first and changed one real page, so it was not taken.

Test: pdf_extract_form_ladder_bounded: seven levels of forms drawing the
next four times each, 64 KiB per form: re-readings are cut and the page
keeps its glyphs bounded (with the bound reverted nothing is cut).

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-10-09 02:55:23 +02:00

64 lines
2.6 KiB
C

/*
* pdf.h — the text layer of a PDF, page by page.
*
* Reads what a PDF says in text (content-stream text operators decoded through
* the document's fonts) and nothing else: no rendering, no images, no OCR, no
* JavaScript, actions or URIs, nothing fetched, nothing executed. Input is
* untrusted: every read is bounds-checked, every recursion is depth-limited,
* every decoder is guarded against decompression bombs.
*
* Not read: encrypted documents (the result says so). Scanned pages have no
* text layer and come back empty.
*/
#ifndef CBM_PDF_H
#define CBM_PDF_H
#include "../arena.h"
#include <stdbool.h>
#include <stddef.h>
typedef enum {
CBM_PDF_OK = 0,
CBM_PDF_NOT_PDF, /* no "%PDF-" within the first 1024 bytes */
CBM_PDF_PARSE_FAILURE, /* no page could be found */
CBM_PDF_ENCRYPTED, /* an Encrypt dictionary: the text is not read */
CBM_PDF_NOMEM,
} cbm_pdf_status_t;
typedef struct {
const char *text; /* UTF-8, NUL-terminated, no NUL inside */
size_t len;
int glyphs; /* glyphs shown on the page */
int unmapped; /* glyphs without a Unicode value (U+FFFD in the text) */
} cbm_pdf_page_t;
/* How the document was read: counts of the repairs and limits that applied. */
typedef struct {
int xref_reconstructed; /* cross-reference rebuilt from a scan of the file */
int xref_offset_fixed; /* an xref section found near a wrong offset */
int stream_length_recovered; /* a stream's /Length was wrong */
int objstm_nested; /* opening an object stream needed an object of one */
int xref_streams; /* cross-reference streams decoded */
int form_rereads_cut; /* a form not read again: re-readings past 256 MiB */
int flate_recovered; /* a damaged Flate stream decoded in part */
int stream_guard_hits; /* a decoded stream cut at 512 MiB */
int filters_unsupported; /* a stream with a filter this reader does not decode */
int op_errors; /* content operators that could not be applied */
int page_errors; /* pages whose content stopped early */
} cbm_pdf_counts_t;
typedef struct {
cbm_pdf_status_t status;
cbm_pdf_page_t *pages;
int npages;
cbm_pdf_counts_t counts;
} cbm_pdf_result_t;
/* Read the text layer of data[0..len). Everything the result points to is
* allocated from `out` and lives as long as it. Returns false only when memory
* ran out (status CBM_PDF_NOMEM). */
bool cbm_pdf_extract(const unsigned char *data, size_t len, CBMArena *out, cbm_pdf_result_t *res);
#endif /* CBM_PDF_H */