mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-04 10:48:20 +00:00
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>
This commit is contained in:
1 parent
2af58b0a33
commit
24593cdb9d
4 files changed
+89
No files matched your search
@@ -40,6 +40,7 @@ typedef struct {
|
||||
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 */
|
||||
|
||||
@@ -71,6 +71,13 @@ enum {
|
||||
* no text layer, and real documents stay far below it. */
|
||||
#define PDF_MAX_STREAM_OUT ((size_t)512 << 20)
|
||||
|
||||
/* A Form XObject's first reading in a document is always whole. Reading one
|
||||
* again (the same content drawn elsewhere: a logo, a header) is counted, and
|
||||
* beyond this many decoded bytes of such re-readings the document reads no
|
||||
* form again (counted): forms drawing the next one many times, level under
|
||||
* level, asked for (draws ^ depth) readings from a few kilobytes. */
|
||||
#define PDF_MAX_FORM_REREAD ((size_t)256 << 20)
|
||||
|
||||
/* ── Integers from the file: saturated, never undefined ──────────── */
|
||||
|
||||
static inline int64_t pdf_sat_add(int64_t a, int64_t b) {
|
||||
|
||||
@@ -359,6 +359,8 @@ typedef struct {
|
||||
pdf_out_t out; /* the page being read */
|
||||
pdf_formkey_t forms[PDF_MAX_FORM_DEPTH + 2];
|
||||
int nforms;
|
||||
pdf_map_t forms_read; /* the forms the document has read once */
|
||||
size_t reread; /* decoded bytes of forms read again */
|
||||
pdf_glyph_out_t *gbuf;
|
||||
size_t gcap;
|
||||
} pdf_interp_t;
|
||||
@@ -484,6 +486,17 @@ static bool do_xobject(pdf_interp_t *it, pdf_val_t *res, const pdf_val_t *name,
|
||||
if (!data || !dlen) {
|
||||
return true;
|
||||
}
|
||||
uint64_t once = key.key << 1 | (key.is_ref ? 1U : 0U);
|
||||
if (pdf_map_get(&it->forms_read, once) >= 0) {
|
||||
if (dlen > PDF_MAX_FORM_REREAD - it->reread) {
|
||||
d->counts->form_rereads_cut++;
|
||||
return true;
|
||||
}
|
||||
it->reread += dlen;
|
||||
} else if (!pdf_map_put(&it->forms_read, once, 1)) {
|
||||
d->nomem = true;
|
||||
return false;
|
||||
}
|
||||
pdf_val_t *mat = pdf_dget(d, xo->stream->dict, "Matrix");
|
||||
double m[6];
|
||||
memcpy(m, IDENT, sizeof(m));
|
||||
@@ -1025,6 +1038,7 @@ bool cbm_pdf_extract(const unsigned char *data, size_t len, CBMArena *out, cbm_p
|
||||
res->npages = i + 1;
|
||||
}
|
||||
cbm_free(CBM_MEM_CLASS_EXTRACT, it.gbuf);
|
||||
pdf_map_free(&it.forms_read);
|
||||
cbm_free(CBM_MEM_CLASS_EXTRACT, pl.v);
|
||||
bool nomem = d.nomem;
|
||||
pdf_doc_close(&d);
|
||||
|
||||
@@ -707,6 +707,72 @@ static unsigned char *tp_xrefstm_sections(int sections, size_t *out_len) {
|
||||
return b.p;
|
||||
}
|
||||
|
||||
/* A page drawing form 1, form k drawing form k+1 `fan` times, `depth` forms,
|
||||
* each padded with a 64 KiB comment; the last one shows "x". */
|
||||
static unsigned char *tp_form_ladder(int depth, int fan, size_t *out_len) {
|
||||
int n = 5 + depth;
|
||||
tp_obj_t *o = (tp_obj_t *)calloc((size_t)n, sizeof(tp_obj_t));
|
||||
char **dicts = (char **)calloc((size_t)n, sizeof(char *));
|
||||
tp_buf_t *bodies = (tp_buf_t *)calloc((size_t)n, sizeof(tp_buf_t));
|
||||
o[0] = (tp_obj_t){TP_CATALOG, NULL, 0};
|
||||
o[1] = (tp_obj_t){"<< /Type /Pages /Kids [3 0 R] /Count 1 >>", NULL, 0};
|
||||
o[2] = (tp_obj_t){"<< /Type /Page /Parent 2 0 R /Resources << /XObject << /F 6 0 R >> >> "
|
||||
"/Contents 5 0 R >>",
|
||||
NULL, 0};
|
||||
o[3] = (tp_obj_t){TP_HELV, NULL, 0};
|
||||
o[4] = (tp_obj_t){"", "q /F Do Q", 9};
|
||||
char *pad = (char *)malloc(64 * 1024 + 3);
|
||||
pad[0] = '%';
|
||||
memset(pad + 1, 'x', 64 * 1024);
|
||||
pad[64 * 1024 + 1] = '\n';
|
||||
pad[64 * 1024 + 2] = 0;
|
||||
for (int k = 0; k < depth; k++) {
|
||||
int i = 5 + k; /* object i + 1 */
|
||||
dicts[i] = (char *)malloc(256);
|
||||
snprintf(dicts[i], 256,
|
||||
"/Type /XObject /Subtype /Form /BBox [0 0 612 792] /Resources << /XObject << /F "
|
||||
"%d 0 R >> /Font << /F1 4 0 R >> >>",
|
||||
i + 2);
|
||||
tp_puts(&bodies[i], pad);
|
||||
if (k + 1 < depth) {
|
||||
for (int f = 0; f < fan; f++) {
|
||||
tp_puts(&bodies[i], "q /F Do Q\n");
|
||||
}
|
||||
} else {
|
||||
tp_puts(&bodies[i], "BT /F1 12 Tf 72 700 Td (x) Tj ET");
|
||||
}
|
||||
o[i] = (tp_obj_t){dicts[i], bodies[i].p, bodies[i].n};
|
||||
}
|
||||
unsigned char *pdf = tp_pdf(o, n, NULL, out_len);
|
||||
for (int i = 0; i < n; i++) {
|
||||
free(dicts[i]);
|
||||
free(bodies[i].p);
|
||||
}
|
||||
free(pad);
|
||||
free(bodies);
|
||||
free(dicts);
|
||||
free(o);
|
||||
return pdf;
|
||||
}
|
||||
|
||||
/* Forms drawing the next one four times, seven levels: the first reading of
|
||||
* each form is whole, readings again stop at PDF_MAX_FORM_REREAD (about 1.3
|
||||
* GB of form content read again before, and draws ^ depth in general). */
|
||||
TEST(pdf_extract_form_ladder_bounded) {
|
||||
size_t len;
|
||||
tp_text_t t;
|
||||
unsigned char *pdf = tp_form_ladder(7, 4, &len);
|
||||
ASSERT_TRUE(tp_extract(pdf, len, &t));
|
||||
ASSERT_EQ(t.r.status, CBM_PDF_OK);
|
||||
ASSERT_EQ(t.r.npages, 1);
|
||||
ASSERT_TRUE(t.r.counts.form_rereads_cut > 0);
|
||||
ASSERT_TRUE(t.r.pages[0].glyphs >= 1);
|
||||
ASSERT_TRUE((size_t)t.r.pages[0].glyphs <= (256U << 20) / (64U * 1024U) + 1);
|
||||
cbm_arena_destroy(&t.arena);
|
||||
free(pdf);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* tp_simple's page, then `n` objects that each open a string and never close
|
||||
* it, and no cross-reference: the reader rebuilds it from the object headers. */
|
||||
static unsigned char *tp_open_strings(int n, size_t *out_len) {
|
||||
@@ -1117,6 +1183,7 @@ SUITE(doc_links_pdf) {
|
||||
RUN_TEST(pdf_extract_object_chains);
|
||||
RUN_TEST(pdf_extract_xrefstm_once);
|
||||
RUN_TEST(pdf_extract_open_strings_linear);
|
||||
RUN_TEST(pdf_extract_form_ladder_bounded);
|
||||
RUN_TEST(pdf_scan_mentions);
|
||||
RUN_TEST(pdf_links_pipeline);
|
||||
RUN_TEST(pdf_snippet_is_page_text);
|
||||
|
||||
Reference in new issue
Block a user