From 6a99cac578272efff360afa056f0be1a5faa822a Mon Sep 17 00:00:00 2001 From: Julian Smith Date: Sun, 6 Sep 2026 16:26:42 +0100 Subject: [PATCH 1/3] tests/: added test_4994() - text/search with TEXT_DEHYPHENATE. --- tests/resources/test_4994.pdf | Bin 0 -> 997 bytes tests/test_textextract.py | 23 +++++++++++++++++++++++ 2 files changed, 23 insertions(+) create mode 100644 tests/resources/test_4994.pdf diff --git a/tests/resources/test_4994.pdf b/tests/resources/test_4994.pdf new file mode 100644 index 0000000000000000000000000000000000000000..b54ce5a6fbb504e214dcb3ade9c43f449a4658c2 GIT binary patch literal 997 zcmZuwJ!lj`6b5Z%6tu7~-B=vN!keAho!LD+4{!f4nqa&f1Uyr0?&dCQF1u!TP7({T z5JA$3V4)&vA=rqB+E^qZ3b7Ca{(z>pv@;+9ow@vN4u@r7-p}{G_ue4Z2>%sc@ALk-5`mq4+Is+)JQw?M{O_S_!0@J{*cJ-q{ z3{4_G2CGwGV||#wY7^FqtiiBpFiJG0GmBxynJI6)G*&$Wyec>k{!@4+;Z2}H3uxO1 zGD>v{n{eA^$R(-+qiq`|QU$#>$*k8#uv9}&oG#h-+6*iu&=W_k3?2|k5Vm=up3g{l zl|xHPFP2(YQbGf6t>}<4arCl5JxUFOGDo*vhuDO9)WVMGmx{Jorr2={4#TePTG;al eCLXq^sksHeOiD_UsR$C$9gC^yAeZyb`QRVy Date: Sun, 6 Sep 2026 16:27:32 +0100 Subject: [PATCH 2/3] src/: fix test_4994(), need to handle mupdf.FZ_STEXT_LINE_FLAGS_JOINED in line.flags. --- src/__init__.py | 14 +++++++++----- src/extra.i | 28 ++++++++++++++++++++++------ 2 files changed, 31 insertions(+), 11 deletions(-) diff --git a/src/__init__.py b/src/__init__.py index 97890ed45..f4a0f0d12 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -21544,13 +21544,18 @@ def JM_new_buffer_from_stext_page(page): for block in page: if block.m_internal.type == mupdf.FZ_STEXT_BLOCK_TEXT: for line in block: + break_line = True for ch in line: if (not JM_rects_overlap(rect, JM_char_bbox(line, ch)) and not mupdf.fz_is_infinite_rect(rect) ): continue - mupdf.fz_append_rune(buf, ch.m_internal.c) - mupdf.fz_append_byte(buf, ord('\n')) + if not ch.m_internal.next and (line.m_internal.flags & mupdf.FZ_STEXT_LINE_FLAGS_JOINED): + break_line = 0 + else: + mupdf.fz_append_rune(buf, ch.m_internal.c) + if break_line: + mupdf.fz_append_byte(buf, ord('\n')) mupdf.fz_append_byte(buf, ord('\n')) return buf @@ -22031,9 +22036,8 @@ def __str__(self): break haystack += 1 #next_char:; - assert haystack_string[haystack] == '\n', \ - f'{haystack=} {haystack_string[haystack]=}' - haystack += 1 + if haystack_string[haystack] == '\n': + haystack += 1 assert haystack_string[haystack] == '\n', \ f'{haystack=} {haystack_string[haystack]=}' haystack += 1 diff --git a/src/extra.i b/src/extra.i index dd1670a2e..d461095ce 100644 --- a/src/extra.i +++ b/src/extra.i @@ -2301,16 +2301,23 @@ void _as_text(fz_stext_block *block, mupdf::FzBuffer& res, mupdf::FzStextPage& p last_char = 0; for (line = block->u.t.first_line; line; line = line->next) { + int break_line = 1; for (ch = line->first_char; ch; ch = ch->next) { fz_rect chbbox = JM_char_bbox( line, ch); - if (mupdf::ll_fz_is_infinite_rect(rect) || JM_rects_overlap(rect, chbbox)) + if (!ch->next && (line->flags & FZ_STEXT_LINE_FLAGS_JOINED)) { + break_line = 0; + } + else if (mupdf::ll_fz_is_infinite_rect(rect) || JM_rects_overlap(rect, chbbox)) { last_char = ch->c; JM_append_rune(res.m_internal, last_char); } } - if (last_char != 10 && last_char > 0) + if (!break_line) + { + } + else if (last_char != 10 && last_char > 0) { mupdf::ll_fz_append_string(res.m_internal, "\n"); last_char = 10; @@ -4013,14 +4020,22 @@ JM_new_buffer_from_stext_page(fz_stext_page *page) for (block = page->first_block; block; block = block->next) { if (block->type == FZ_STEXT_BLOCK_TEXT) { for (line = block->u.t.first_line; line; line = line->next) { + int break_line = 1; for (ch = line->first_char; ch; ch = ch->next) { if (!JM_rects_overlap(rect, JM_char_bbox(line, ch)) && !fz_is_infinite_rect(rect)) { continue; } - fz_append_rune(ctx, buf, ch->c); + if (!ch->next && (line->flags & FZ_STEXT_LINE_FLAGS_JOINED)) { + break_line = 0; + } + else { + fz_append_rune(ctx, buf, ch->c); + } + } + if (break_line) { + fz_append_byte(ctx, buf, '\n'); } - fz_append_byte(ctx, buf, '\n'); } fz_append_byte(ctx, buf, '\n'); } @@ -4261,8 +4276,9 @@ try_new_match: haystack += fz_chartorune(&c, haystack); next_char:; } - assert(*haystack == '\n'); - ++haystack; + if (*haystack == '\n') { + ++haystack; + } } assert(*haystack == '\n'); ++haystack; From 0d17caaff7ea1fa9358c326c9cff57a2e581415f Mon Sep 17 00:00:00 2001 From: Julian Smith Date: Mon, 7 Sep 2026 17:13:40 +0100 Subject: [PATCH 3/3] src/extra.i tests/test_annots.py: fix potential segv/remote code execution in JM_set_annot_callout_line(). Fixes #5112. Also fixed similar problem in set_pixel() and ll_JM_color_count() if pixmap->n is out of range. --- src/extra.i | 15 +++++++++++---- tests/test_annots.py | 18 ++++++++++++++++++ 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/src/extra.i b/src/extra.i index d461095ce..f7da7bf19 100644 --- a/src/extra.i +++ b/src/extra.i @@ -788,6 +788,10 @@ static void JM_set_annot_callout_line(mupdf::PdfAnnot& annot, PyObject *callout, { fz_point points[3]; mupdf::FzPoint p; + if (count > sizeof(points)/sizeof(points[0])) + { + throw std::runtime_error(MSG_BAD_ARG_POINTS); + } for (int i = 0; i < count; i++) { p = JM_point_from_py(PyTuple_GetItem(callout, (Py_ssize_t) i)); @@ -3980,11 +3984,11 @@ PyObject *set_pixel(fz_pixmap* pm, int x, int y, PyObject *color) throw std::range_error( MSG_PIXEL_OUTSIDE); } int n = pm->n; - if (!PySequence_Check(color) || PySequence_Size(color) != n) { - throw std::range_error(MSG_BAD_COLOR_SEQ); - } int i, j; unsigned char c[5]; + if (!PySequence_Check(color) || PySequence_Size(color) != n || n > sizeof(c)) { + throw std::range_error(MSG_BAD_COLOR_SEQ); + } for (j = 0; j < n; j++) { if (JM_INT_ITEM(color, j, &i) == 1) { throw std::range_error(MSG_BAD_COLOR_SEQ); @@ -4363,7 +4367,10 @@ PyObject* ll_JM_color_count(fz_pixmap *pm, PyObject *clip) unsigned char* s = pm->samples + stride * (irect.y0 - pm->y) + n * (irect.x0 - pm->x); // Cache previous pixel. char oldpix[10]; - assert(n <= sizeof(oldpix)); + if (n > sizeof(oldpix)) + { + throw std::range_error(MSG_PIXEL_OUTSIDE); + } memcpy(oldpix, s, n); long cnt = 0; for (size_t i = 0; i < height; i++) diff --git a/tests/test_annots.py b/tests/test_annots.py index d5ebdcb3d..f6d1dd561 100644 --- a/tests/test_annots.py +++ b/tests/test_annots.py @@ -794,3 +794,21 @@ def test_4936(): drawings = page.get_drawings() print(f'{len(drawings)=}') assert len(drawings) == 0 + + +def test_5112(): + print() + with pymupdf.open() as document: + page = document.new_page() + rect = pymupdf.Rect(50, 50, 200, 150) + callout = [(10, 10)] * 100 + try: + print(f'test_5112(): calling page.add_freetext_annot().', flush=1) + e = page.add_freetext_annot(rect, "test", callout=callout) + except Exception as e: + print(f'Received exception: {e}') + assert isinstance(e, RuntimeError) + assert str(e) == 'bad seq of points' + else: + assert 0, f'Expected exception from page.add_freetext_annot().' +