"""Test the pypdf._reader module."""
import io
import struct
import sys
import time
from io import BytesIO
from pathlib import Path
from typing import Union
from unittest import mock

import pytest

from pypdf import PdfReader, PdfWriter
from pypdf._crypt_providers import crypt_provider
from pypdf._reader import convert_to_int
from pypdf.constants import ImageAttributes
from pypdf.constants import PageAttributes as PG
from pypdf.constants import UserAccessPermissions as UAP
from pypdf.errors import (
    DeprecationError,
    EmptyFileError,
    FileNotDecryptedError,
    LimitReachedError,
    PdfReadError,
    PdfStreamError,
    WrongPasswordError,
)
from pypdf.filters import FlateDecode
from pypdf.generic import (
    ArrayObject,
    Destination,
    DictionaryObject,
    IndirectObject,
    NameObject,
    NumberObject,
    TextStringObject,
)

from . import RESOURCE_ROOT, SAMPLE_ROOT, get_data_from_url, normalize_warnings

HAS_AES = crypt_provider[0] in ["pycryptodome", "cryptography"]


NestedList = Union[int, None, list["NestedList"]]


@pytest.mark.parametrize(
    ("src", "num_pages"),
    [("selenium-pypdf-issue-177.pdf", 1), ("pdflatex-outline.pdf", 4)],
)
def test_get_num_pages(src, num_pages):
    src = RESOURCE_ROOT / src
    with PdfReader(src) as reader:
        assert len(reader.pages) == num_pages
        # from #1911
        assert "/Size" in reader.trailer


@pytest.mark.parametrize(
    ("pdf_path", "expected"),
    [
        (
            RESOURCE_ROOT / "crazyones.pdf",
            {
                "/CreationDate": "D:20150604133406-06'00'",
                "/Creator": " XeTeX output 2015.06.04:1334",
                "/Producer": "xdvipdfmx (20140317)",
            },
        ),
        (
            RESOURCE_ROOT / "metadata.pdf",
            {
                "/CreationDate": "D:20220415093243+02'00'",
                "/ModDate": "D:20220415093243+02'00'",
                "/Creator": "pdflatex, or other tool",
                "/Producer": "Latex with hyperref, or other system",
                "/Author": "Martin Thoma",
                "/Keywords": "Some Keywords, other keywords; more keywords",
                "/Subject": "The Subject",
                "/Title": "The Title",
                "/Trapped": "/False",
                "/PTEX.Fullbanner": (
                    "This is pdfTeX, Version "
                    "3.141592653-2.6-1.40.23 (TeX Live 2021) "
                    "kpathsea version 6.3.3"
                ),
            },
        ),
    ],
    ids=["crazyones", "metadata"],
)
def test_read_metadata(pdf_path, expected):
    with open(pdf_path, "rb") as inputfile:
        reader = PdfReader(inputfile)
        docinfo = reader.metadata
        assert docinfo is not None
        metadict = dict(docinfo)
        assert metadict == expected
        docinfo.title
        docinfo.title_raw
        docinfo.author
        docinfo.author_raw
        docinfo.creator
        docinfo.creator_raw
        docinfo.producer
        docinfo.producer_raw
        docinfo.subject
        docinfo.subject_raw
        docinfo.creation_date
        docinfo.creation_date_raw
        docinfo.modification_date
        docinfo.modification_date_raw
        docinfo.keywords
        docinfo.keywords_raw
        if "/Title" in metadict:
            assert isinstance(docinfo.title, str)
            assert metadict["/Title"] == docinfo.title


def test_read_metadata_title_is_utf8():
    with open(RESOURCE_ROOT / "bytes.pdf", "rb") as inputfile:
        reader = PdfReader(inputfile)
        title = reader.metadata.title
        # Should be a str.
        assert title == "Microsoft Word - トランスバース社買収電話会議英語Final.docx"


def test_iss1943():
    with PdfReader(RESOURCE_ROOT / "crazyones.pdf") as reader:
        docinfo = reader.metadata
        docinfo.update(
            {
                NameObject("/CreationDate"): TextStringObject(
                    "D:20230705005151Z00'00'"
                ),
                NameObject("/ModDate"): TextStringObject("D:20230705005151Z00'00'"),
            }
        )
        docinfo.creation_date
        docinfo.creation_date_raw
        docinfo.modification_date
        docinfo.modification_date_raw
        docinfo.update({NameObject("/CreationDate"): NumberObject(1)})
        assert docinfo.creation_date is None


@pytest.mark.samples
@pytest.mark.parametrize(
    "pdf_path", [SAMPLE_ROOT / "017-unreadable-meta-data/unreadablemetadata.pdf"]
)
def test_broken_meta_data(pdf_path):
    with open(pdf_path, "rb") as f:
        reader = PdfReader(f)
        assert reader.metadata is None

    with open(RESOURCE_ROOT / "crazyones.pdf", "rb") as f:
        b = f.read(-1)
    reader = PdfReader(BytesIO(b.replace(b"/Info 2 0 R", b"/Info 2    ")))
    with pytest.raises(PdfReadError) as exc:
        reader.metadata
    assert "does not point to a document information dictionary" in repr(exc)


@pytest.mark.parametrize(
    "src",
    [
        RESOURCE_ROOT / "crazyones.pdf",
        RESOURCE_ROOT / "commented.pdf",
    ],
)
def test_get_annotations(src):
    with PdfReader(src) as reader:
        for page in reader.pages:
            if PG.ANNOTS in page:
                for annot in page[PG.ANNOTS]:
                    subtype = annot.get_object()[ImageAttributes.SUBTYPE]
                    if subtype == "/Text":
                        annot.get_object()[PG.CONTENTS]


@pytest.mark.parametrize(
    ("src", "nb_attachments"),
    [
        (RESOURCE_ROOT / "attachment.pdf", 1),
        (RESOURCE_ROOT / "crazyones.pdf", 0),
    ],
)
def test_get_attachments(src, nb_attachments):
    reader = PdfReader(src)

    attachments = {}
    for page in reader.pages:
        if PG.ANNOTS in page:
            for annotation in page[PG.ANNOTS]:
                annotobj = annotation.get_object()
                if annotobj[ImageAttributes.SUBTYPE] == "/FileAttachment":
                    fileobj = annotobj["/FS"]
                    attachments[fileobj["/F"]] = fileobj["/EF"]["/F"].get_data()
    assert len(attachments) == nb_attachments


@pytest.mark.parametrize(
    ("src", "outline_elements"),
    [
        (RESOURCE_ROOT / "pdflatex-outline.pdf", 9),
        (RESOURCE_ROOT / "crazyones.pdf", 0),
    ],
)
def test_get_outline(src, outline_elements):
    reader = PdfReader(src)
    outline = reader.outline
    assert len(outline) == outline_elements


@pytest.mark.samples
@pytest.mark.parametrize(
    ("src", "expected_images"),
    [
        ("pdflatex-outline.pdf", []),
        ("crazyones.pdf", []),
        ("git.pdf", ["Image9.png"]),
        pytest.param(
            "imagemagick-lzw.pdf",
            ["Im0.png"],
            marks=pytest.mark.xfail(reason="broken image extraction"),
        ),
        pytest.param(
            "imagemagick-ASCII85Decode.pdf",
            ["Im0.png"],
            # marks=pytest.mark.xfail(reason="broken image extraction"),
        ),
        ("imagemagick-CCITTFaxDecode.pdf", ["Im0.tiff"]),
        (SAMPLE_ROOT / "019-grayscale-image/grayscale-image.pdf", ["X0.png"]),
    ],
)
def test_get_images(src, expected_images):
    from PIL import Image  # noqa: PLC0415

    src_abs = RESOURCE_ROOT / src
    reader = PdfReader(src_abs)
    page = reader.pages[0]
    images_extracted = page.images

    assert len(images_extracted) == len(expected_images)
    for image, expected_image in zip(images_extracted, expected_images):
        assert image.name == expected_image
        assert (
            image.name.split(".")[-1].upper()
            == Image.open(io.BytesIO(image.data)).format
        )


@pytest.mark.parametrize(
    ("strict", "with_prev_0", "startx_correction", "should_fail", "warning_msgs"),
    [
        (
            True,
            False,
            -1,
            False,
            [
                "startxref on same line as offset",
                (
                    "Xref table not zero-indexed. "
                    "ID numbers for objects will be corrected."
                ),
            ],
        ),  # all nominal => no fail
        (True, True, -1, True, ""),  # Prev=0 => fail expected
        (
            False,
            False,
            -1,
            False,
            [
                "startxref on same line as offset",
            ],
        ),
        (
            False,
            True,
            -1,
            False,
            [
                "startxref on same line as offset",
                "/Prev=0 in the trailer - assuming there is no previous xref table",
            ],
        ),  # Prev =0 => no strict so tolerant
        (True, False, 0, True, ""),  # error on startxref, in strict => fail expected
        (True, True, 0, True, ""),
        (
            False,
            False,
            0,
            False,
            [
                "startxref on same line as offset",
                "incorrect startxref pointer(1)",
                "parsing for Object Streams",
            ],
        ),  # error on startxref, but no strict => xref rebuilt,no fail
        (
            False,
            True,
            0,
            False,
            [
                "startxref on same line as offset",
                "incorrect startxref pointer(1)",
                "parsing for Object Streams",
            ],
        ),
    ],
)
def test_get_images_raw(
    caplog, strict, with_prev_0, startx_correction, should_fail, warning_msgs
):
    pdf_data = (
        b"%%PDF-1.7\n"
        b"1 0 obj << /Count 1 /Kids [4 0 R] /Type /Pages >> endobj\n"
        b"2 0 obj << >> endobj\n"
        b"3 0 obj << >> endobj\n"
        b"4 0 obj << /Contents 3 0 R /CropBox [0.0 0.0 2550.0 3508.0]"
        b" /MediaBox [0.0 0.0 2550.0 3508.0] /Parent 1 0 R"
        b" /Resources << /Font << >> >>"
        b" /Rotate 0 /Type /Page >> endobj\n"
        b"5 0 obj << /Pages 1 0 R /Type /Catalog >> endobj\n"
        b"xref 1 5\n"
        b"%010d 00000 n\n"
        b"%010d 00000 n\n"
        b"%010d 00000 n\n"
        b"%010d 00000 n\n"
        b"%010d 00000 n\n"
        b"trailer << %s/Root 5 0 R /Size 6 >>\n"
        b"startxref %d\n"
        b"%%%%EOF"
    )
    pdf_data = pdf_data % (
        # - 1 below in the find because of the double %
        pdf_data.find(b"1 0 obj") - 1,
        pdf_data.find(b"2 0 obj") - 1,
        pdf_data.find(b"3 0 obj") - 1,
        pdf_data.find(b"4 0 obj") - 1,
        pdf_data.find(b"5 0 obj") - 1,
        b"/Prev 0 " if with_prev_0 else b"",
        # startx_correction should be -1 due to double % at the beginning
        # inducing an error on startxref computation
        pdf_data.find(b"xref") + startx_correction,
    )
    pdf_stream = io.BytesIO(pdf_data)
    if should_fail:
        with pytest.raises(PdfReadError) as exc:
            PdfReader(pdf_stream, strict=strict)
        assert exc.type == PdfReadError
        if startx_correction == -1:
            assert (
                exc.value.args[0]
                == "/Prev=0 in the trailer (try opening with strict=False)"
            )
    else:
        PdfReader(pdf_stream, strict=strict)
        assert normalize_warnings(caplog.text) == warning_msgs


def test_issue297(caplog):
    path = RESOURCE_ROOT / "issue-297.pdf"
    with pytest.raises(PdfReadError) as exc:
        reader = PdfReader(path, strict=True)
    assert caplog.text == ""
    assert "Broken xref table" in exc.value.args[0]
    reader = PdfReader(path, strict=False)
    assert normalize_warnings(caplog.text) == [
        "incorrect startxref pointer(1)",
        "parsing for Object Streams",
    ]
    reader.pages[0]


def test_startxref_corrupt_trailing_pointer(caplog):
    """
    A corrupt trailing startxref keyword (e.g. ``tartxref``) must not prevent
    reading when an earlier, intact startxref from a previous revision is still
    present further up the file. See #3238.
    """
    pdf_data = (
        b"%%PDF-1.7\n"
        b"1 0 obj << /Count 1 /Kids [4 0 R] /Type /Pages >> endobj\n"
        b"2 0 obj << >> endobj\n"
        b"3 0 obj << >> endobj\n"
        b"4 0 obj << /Contents 3 0 R /CropBox [0.0 0.0 2550.0 3508.0]"
        b" /MediaBox [0.0 0.0 2550.0 3508.0] /Parent 1 0 R"
        b" /Resources << /Font << >> >>"
        b" /Rotate 0 /Type /Page >> endobj\n"
        b"5 0 obj << /Pages 1 0 R /Type /Catalog >> endobj\n"
        b"xref 1 5\n"
        b"%010d 00000 n\n"
        b"%010d 00000 n\n"
        b"%010d 00000 n\n"
        b"%010d 00000 n\n"
        b"%010d 00000 n\n"
        b"trailer << /Root 5 0 R /Size 6 >>\n"
        b"startxref\n"
        b"%d\n"
        b"%%%%EOF\n"
        # A corrupt, trailing cross-reference pointer left behind by a broken
        # incremental update: the keyword has lost its leading 's'.
        b"tartxref\n"
        b"99999\n"
        b"%%%%EOF"
    )
    pdf_data = pdf_data % (
        # - 1 below in the find because of the double % at the beginning
        pdf_data.find(b"1 0 obj") - 1,
        pdf_data.find(b"2 0 obj") - 1,
        pdf_data.find(b"3 0 obj") - 1,
        pdf_data.find(b"4 0 obj") - 1,
        pdf_data.find(b"5 0 obj") - 1,
        pdf_data.find(b"xref") - 1,
    )
    reader = PdfReader(io.BytesIO(pdf_data))
    assert len(reader.pages) == 1
    assert reader.pages[0].mediabox.width == 2550
    assert normalize_warnings(caplog.text) == [
        "found startxref pointing to a previous revision after a corrupt one",
    ]

    # Recovering from the corrupt pointer violates the standard, so it must
    # only happen in non-strict mode; strict mode raises instead.
    with pytest.raises(PdfReadError, match="startxref not found"):
        PdfReader(io.BytesIO(pdf_data), strict=True)


def test_find_previous_startxref_pos_recovery_variants():
    """Cover the backward-scan branches of _find_previous_startxref_pos (#3238)."""

    def scan(data: bytes) -> int:
        stream = io.BytesIO(data)
        stream.seek(0, 2)
        return PdfReader._find_previous_startxref_pos(stream)

    # Offset on the line below the keyword (the usual layout)
    assert scan(b"startxref\n12345\ngarbage\n") == 12345

    # Keyword and offset share a line
    assert scan(b"startxref 6789\ngarbage\n") == 6789

    # A startxref keyword with no numeric offset above it cannot be recovered
    with pytest.raises(PdfReadError, match="startxref not found"):
        scan(b"startxref\nnot-a-number\n")

    # No startxref keyword at all: the scan exhausts the stream and gives up
    with pytest.raises(PdfReadError, match="startxref not found"):
        scan(b"only some bytes\nand more\n")

    # The backward scan is bounded: a long stream without a recoverable
    # pointer hits the line cap and gives up instead of scanning forever
    with pytest.raises(PdfReadError, match="startxref not found"):
        scan(b"x\n" * (PdfReader._MAX_STARTXREF_RECOVERY_LINES + 100))


@pytest.mark.parametrize(
    ("pdffile", "password", "should_fail"),
    [
        ("encrypted-file.pdf", "test", False),
        ("encrypted-file.pdf", b"test", False),
        ("encrypted-file.pdf", "qwerty", True),
        ("encrypted-file.pdf", b"qwerty", True),
    ],
)
def test_get_page_of_encrypted_file(pdffile, password, should_fail):
    """
    Check if we can read a page of an encrypted file.

    This is a regression test for issue 327:
    IndexError for get_page() of decrypted file
    """
    path = RESOURCE_ROOT / pdffile
    if should_fail:
        with pytest.raises(PdfReadError):
            PdfReader(path, password=password)
    else:
        PdfReader(path, password=password).pages[0]


@pytest.mark.parametrize(
    ("src", "expected", "expected_get_fields"),
    [
        (
            "form.pdf",
            {"foo": ""},
            {"foo": {"/DV": "", "/FT": "/Tx", "/T": "foo", "/V": ""}},
        ),
        (
            "form_acrobatReader.pdf",
            {"foo": "Bar"},
            {"foo": {"/DV": "", "/FT": "/Tx", "/T": "foo", "/V": "Bar"}},
        ),
        (
            "form_evince.pdf",
            {"foo": "bar"},
            {"foo": {"/DV": "", "/FT": "/Tx", "/T": "foo", "/V": "bar"}},
        ),
        (
            "crazyones.pdf",
            {},
            None,
        )
    ],
)
def test_get_form(src, expected, expected_get_fields, txt_file_path):
    """Check if we can read out form data."""
    src = RESOURCE_ROOT / src
    reader = PdfReader(src)
    fields = reader.get_form_text_fields()
    assert fields == expected

    with open(txt_file_path, "w") as f:
        fields = reader.get_fields(fileobj=f)
    assert fields == expected_get_fields
    if fields:
        for field in fields.values():
            # Just access the attributes
            [
                field.field_type,
                field.parent,
                field.kids,
                field.name,
                field.alternate_name,
                field.mapping_name,
                field.flags,
                field.value,
                field.default_value,
                field.additional_actions,
            ]


@pytest.mark.enable_socket
def test_reading_choice_field_without_opt_key():
    """Tests reading a choice field in a PDF without an /Opt key."""
    url = "https://github.com/user-attachments/files/23853677/Musterservicevertrag-HNRAGB_Okt2022-Blanko.pdf"
    reader = PdfReader(BytesIO(get_data_from_url(url=url, name="Musterservicevertrag-HNRAGB_Okt2022-Blanko.pdf")))
    fields = reader.get_fields()

    tn_anrede = fields.get("TN_Anrede")
    assert tn_anrede is not None

    # Ensure that parsing of a choice field without /Opt key worked
    tn_anrede_opt = tn_anrede.get("/Opt")
    assert tn_anrede_opt is None


@pytest.mark.parametrize(
    ("src", "page_number"),
    [
        ("form.pdf", 0),
        ("pdflatex-outline.pdf", 2),
    ],
)
def test_get_page_number(src, page_number):
    src = RESOURCE_ROOT / src
    reader = PdfReader(src)
    reader.get_page(0)
    page = reader.pages[page_number]
    assert reader.get_page_number(page) == page_number


@pytest.mark.parametrize(
    ("src", "expected"),
    [("form.pdf", None), ("AutoCad_Simple.pdf", "/SinglePage")],
)
def test_get_page_layout(src, expected):
    src = RESOURCE_ROOT / src
    reader = PdfReader(src)
    assert reader.page_layout == expected


@pytest.mark.parametrize(
    ("src", "expected"),
    [
        ("form.pdf", "/UseNone"),
        ("crazyones.pdf", None),
    ],
)
def test_get_page_mode(src, expected):
    src = RESOURCE_ROOT / src
    reader = PdfReader(src)
    assert reader.page_mode == expected


def test_read_empty():
    with pytest.raises(EmptyFileError) as exc:
        PdfReader(io.BytesIO())
    assert exc.value.args[0] == "Cannot read an empty file"


def test_read_malformed_header(caplog):
    with pytest.raises(PdfReadError) as exc:
        PdfReader(io.BytesIO(b"foo"), strict=True)
    assert exc.value.args[0] == "PDF starts with 'foo', but '%PDF-' expected"
    caplog.clear()
    try:
        PdfReader(io.BytesIO(b"foo"), strict=False)
    except Exception:
        pass
    assert caplog.messages[0].startswith("invalid pdf header")


def test_read_malformed_body():
    with pytest.raises(PdfReadError) as exc:
        PdfReader(io.BytesIO(b"%PDF-"), strict=True)
    assert (
        exc.value.args[0] == "EOF marker not found"
    )  # used to be:STREAM_TRUNCATED_PREMATURELY


def test_read_prev_0_trailer():
    pdf_data = (
        b"%%PDF-1.7\n"
        b"1 0 obj << /Count 1 /Kids [4 0 R] /Type /Pages >> endobj\n"
        b"2 0 obj << >> endobj\n"
        b"3 0 obj << >> endobj\n"
        b"4 0 obj << /Contents 3 0 R /CropBox [0.0 0.0 2550.0 3508.0]"
        b" /MediaBox [0.0 0.0 2550.0 3508.0] /Parent 1 0 R"
        b" /Resources << /Font << >> >>"
        b" /Rotate 0 /Type /Page >> endobj\n"
        b"5 0 obj << /Pages 1 0 R /Type /Catalog >> endobj\n"
        b"xref 1 5\n"
        b"%010d 00000 n\n"
        b"%010d 00000 n\n"
        b"%010d 00000 n\n"
        b"%010d 00000 n\n"
        b"%010d 00000 n\n"
        b"trailer << %s/Root 5 0 R /Size 6 >>\n"
        b"startxref %d\n"
        b"%%%%EOF"
    )
    with_prev_0 = True
    pdf_data = pdf_data % (
        pdf_data.find(b"1 0 obj"),
        pdf_data.find(b"2 0 obj"),
        pdf_data.find(b"3 0 obj"),
        pdf_data.find(b"4 0 obj"),
        pdf_data.find(b"5 0 obj"),
        b"/Prev 0 " if with_prev_0 else b"",
        pdf_data.find(b"xref") - 1,
    )
    pdf_stream = io.BytesIO(pdf_data)
    with pytest.raises(PdfReadError) as exc:
        PdfReader(pdf_stream, strict=True)
    assert exc.value.args[0] == "/Prev=0 in the trailer (try opening with strict=False)"


def test_circular_xref_prev_reference(caplog):
    """Circular /Prev in trailer should be detected, not loop forever (#3654)."""
    pdf_data = (
        b"%%PDF-1.7\n"
        b"1 0 obj << /Count 1 /Kids [4 0 R] /Type /Pages >> endobj\n"
        b"2 0 obj << >> endobj\n"
        b"3 0 obj << >> endobj\n"
        b"4 0 obj << /Contents 3 0 R /CropBox [0.0 0.0 2550.0 3508.0]"
        b" /MediaBox [0.0 0.0 2550.0 3508.0] /Parent 1 0 R"
        b" /Resources << /Font << >> >>"
        b" /Rotate 0 /Type /Page >> endobj\n"
        b"5 0 obj << /Pages 1 0 R /Type /Catalog >> endobj\n"
        b"xref 1 5\n"
        b"%010d 00000 n\n"
        b"%010d 00000 n\n"
        b"%010d 00000 n\n"
        b"%010d 00000 n\n"
        b"%010d 00000 n\n"
        b"trailer << /Prev %d /Root 5 0 R /Size 6 >>\n"
        b"startxref %d\n"
        b"%%%%EOF"
    )
    xref_offset = pdf_data.find(b"xref") - 1
    pdf_data = pdf_data % (
        pdf_data.find(b"1 0 obj"),
        pdf_data.find(b"2 0 obj"),
        pdf_data.find(b"3 0 obj"),
        pdf_data.find(b"4 0 obj"),
        pdf_data.find(b"5 0 obj"),
        xref_offset,  # /Prev points to same xref = circular
        xref_offset,  # startxref
    )
    PdfReader(io.BytesIO(pdf_data))
    assert "Circular xref chain detected" in caplog.text


def test_cyclic_pages_tree():
    """Circular /Pages reference (multi-hop cycle) must raise PdfReadError, not recurse infinitely (#3847)."""
    # Page tree: root /Pages (obj 2) -> /Pages obj 3 -> /Pages obj 4 -> /Pages obj 5 -> obj 3 (cycle)
    # obj 6 is the only real /Page (reachable directly from root).
    pdf_data = b"""%PDF-1.7

1 0 obj
  << /Type /Catalog
     /Pages 2 0 R
  >>
endobj

2 0 obj
  << /Type /Pages
     /Kids [6 0 R 3 0 R]
     /Count 2
     /MediaBox [0 0 595 842]
  >>
endobj

3 0 obj
  << /Type /Pages
     /Kids [4 0 R]
     /Count 1
     /MediaBox [0 0 595 842]
  >>
endobj

4 0 obj
  << /Type /Pages
     /Kids [5 0 R]
     /Count 1
     /MediaBox [0 0 595 842]
  >>
endobj

5 0 obj
  << /Type /Pages
     /Kids [3 0 R]
     /Count 1
     /MediaBox [0 0 595 842]
  >>
endobj

6 0 obj
  << /Type /Page
     /Parent 2 0 R
     /Resources << /Font << /F1 << /Type /Font /Subtype /Type1 /BaseFont /Courier >> >> >>
     /Contents [7 0 R]
  >>
endobj

7 0 obj
  << /Length 44 >>
stream
  BT /F1 22 Tf 30 800 Td (Test) Tj ET
endstream
endobj

xref
0 8
0000000000 65535 f\x20
0000000010 00000 n\x20
0000000069 00000 n\x20
0000000176 00000 n\x20
0000000277 00000 n\x20
0000000378 00000 n\x20
0000000479 00000 n\x20
0000000744 00000 n\x20
trailer
  << /Root 1 0 R
     /Size 8
  >>
startxref
841
%%EOF
"""
    with pytest.raises(PdfReadError, match=r"^Detected cyclic page references\.$"):
        reader = PdfReader(io.BytesIO(pdf_data), strict=False)
        len(reader.pages)


def test_read_missing_startxref():
    pdf_data = (
        b"%%PDF-1.7\n"
        b"1 0 obj << /Count 1 /Kids [4 0 R] /Type /Pages >> endobj\n"
        b"2 0 obj << >> endobj\n"
        b"3 0 obj << >> endobj\n"
        b"4 0 obj << /Contents 3 0 R /CropBox [0.0 0.0 2550.0 3508.0]"
        b" /MediaBox [0.0 0.0 2550.0 3508.0] /Parent 1 0 R"
        b" /Resources << /Font << >> >>"
        b" /Rotate 0 /Type /Page >> endobj\n"
        b"5 0 obj << /Pages 1 0 R /Type /Catalog >> endobj\n"
        b"xref 1 5\n"
        b"%010d 00000 n\n"
        b"%010d 00000 n\n"
        b"%010d 00000 n\n"
        b"%010d 00000 n\n"
        b"%010d 00000 n\n"
        b"trailer << /Root 5 0 R /Size 6 >>\n"
        # Removed for this test: b"startxref %d\n"
        b"%%%%EOF"
    )
    pdf_data = pdf_data % (
        pdf_data.find(b"1 0 obj"),
        pdf_data.find(b"2 0 obj"),
        pdf_data.find(b"3 0 obj"),
        pdf_data.find(b"4 0 obj"),
        pdf_data.find(b"5 0 obj"),
        # Removed for this test: pdf_data.find(b"xref") - 1,
    )
    pdf_stream = io.BytesIO(pdf_data)
    with pytest.raises(PdfReadError) as exc:
        PdfReader(pdf_stream, strict=True)
    assert exc.value.args[0] == "startxref not found"


def test_read_unknown_zero_pages(caplog):
    pdf_data = (
        b"%%PDF-1.7\n"
        b"1 0 obj << /Count 1 /Kids [4 0 R] /Type /Pages >> endobj\n"
        b"2 0 obj << >> endobj\n"
        b"3 0 obj << >> endobj\n"
        b"4 0 obj << /Contents 3 0 R /CropBox [0.0 0.0 2550.0 3508.0]"
        b" /MediaBox [0.0 0.0 2550.0 3508.0] /Parent 1 0 R"
        b" /Resources << /Font << >> >>"
        b" /Rotate 0 /Type /Page >> endobj\n"
        # Pages 0 0 is the key point:
        b"5 0 obj << /Pages 0 0 R /Type /Catalog >> endobj\n"
        b"xref 1 5\n"
        b"%010d 00000 n\n"
        b"%010d 00000 n\n"
        b"%010d 00000 n\n"
        b"%010d 00000 n\n"
        b"%010d 00000 n\n"
        b"trailer << /Root 5 1 R /Size 6 >>\n"
        b"startxref %d\n"
        b"%%%%EOF"
    )
    pdf_data = pdf_data % (
        pdf_data.find(b"1 0 obj") - 1,
        pdf_data.find(b"2 0 obj") - 1,
        pdf_data.find(b"3 0 obj") - 1,
        pdf_data.find(b"4 0 obj") - 1,
        pdf_data.find(b"5 0 obj") - 1,
        pdf_data.find(b"xref") - 1,
    )
    pdf_stream = io.BytesIO(pdf_data)
    reader = PdfReader(pdf_stream, strict=True)
    warnings = [
        "startxref on same line as offset",
        "Xref table not zero-indexed. ID numbers for objects will be corrected.",
    ]
    assert normalize_warnings(caplog.text) == warnings
    with pytest.raises(PdfReadError) as exc:
        len(reader.pages)

    assert exc.value.args[0] == "Could not find object."
    reader = PdfReader(pdf_stream, strict=False)
    warnings += [
        "Object 5 1 not defined.",
        "startxref on same line as offset",
    ]
    assert normalize_warnings(caplog.text) == warnings
    with pytest.raises(PdfReadError) as exc:
        len(reader.pages)
    assert exc.value.args[0] == "Invalid object in /Pages"


def test_read_encrypted_without_decryption():
    src = RESOURCE_ROOT / "libreoffice-writer-password.pdf"
    reader = PdfReader(src)
    with pytest.raises(FileNotDecryptedError) as exc:
        len(reader.pages)
    assert exc.value.args[0] == "File has not been decrypted"


def test_get_destination_page_number():
    src = RESOURCE_ROOT / "pdflatex-outline.pdf"
    reader = PdfReader(src)
    outline = reader.outline
    for outline_item in outline:
        if not isinstance(outline_item, list):
            reader.get_destination_page_number(outline_item)


def test_do_not_get_stuck_on_large_files_without_start_xref():
    """
    Tests for the absence of a DoS bug, where a large file without an
    startxref mark would cause the library to hang for minutes to hours.
    """
    start_time = time.time()
    broken_stream = BytesIO(b"\0" * 5 * 1000 * 1000)
    with pytest.raises(PdfReadError):
        PdfReader(broken_stream)
    parse_duration = time.time() - start_time
    # parsing is expected take less than a second on a modern cpu, but include
    # a large tolerance to account for busy or slow systems
    assert parse_duration < 60


@pytest.mark.enable_socket
def test_decrypt_when_no_id():
    """
    Decrypt an encrypted file that's missing the 'ID' value in its trailer.

    https://github.com/py-pdf/pypdf/issues/608
    """
    with open(RESOURCE_ROOT / "encrypted_doc_no_id.pdf", "rb") as inputfile:
        ipdf = PdfReader(inputfile)
        ipdf.decrypt("")
        assert ipdf.metadata == {"/Producer": "European Patent Office"}


def test_reader_properties():
    reader = PdfReader(RESOURCE_ROOT / "crazyones.pdf")
    assert reader.outline == []
    assert len(reader.pages) == 1
    assert reader.page_layout is None
    assert reader.page_mode is None
    assert reader.is_encrypted is False


@pytest.mark.parametrize(
    "strict",
    [True, False],
)
def test_issue604(caplog, strict):
    """Test with invalid destinations."""
    with open(RESOURCE_ROOT / "issue-604.pdf", "rb") as f:
        pdf = None
        outline = None
        if strict:
            pdf = PdfReader(f, strict=strict)
            with pytest.raises(PdfReadError) as exc:
                outline = pdf.outline
            if "Unknown Destination" not in exc.value.args[0]:
                raise Exception("Expected exception not raised")
            return  # outline is not correct
        pdf = PdfReader(f, strict=strict)
        outline = pdf.outline
        msg = [
            "Unknown destination: 'ms_Thyroid_2_2020_071520_watermarked.pdf' [0, 1]"
        ]
        assert normalize_warnings(caplog.text) == msg

        def get_dest_pages(x) -> NestedList:
            if isinstance(x, list):
                return [get_dest_pages(y) for y in x]
            destination_page_number = pdf.get_destination_page_number(x)
            if destination_page_number is None:
                return destination_page_number
            return destination_page_number + 1

        out = []

        # oi can be destination or a list:preferred to just print them
        for oi in outline:
            out.append(get_dest_pages(oi))  # noqa: PERF401


def test_decode_permissions():
    reader = PdfReader(RESOURCE_ROOT / "crazyones.pdf")
    base = {
        "accessability": False,  # Do not fix typo, as part of official, but deprecated API.
        "annotations": False,
        "assemble": False,
        "copy": False,
        "forms": False,
        "modify": False,
        "print_high_quality": False,
        "print": False,
    }

    print_ = base.copy()
    print_["print"] = True
    with pytest.raises(
        DeprecationError,
            match=(
                r"decode_permissions is deprecated and was removed in pypdf 5\.0\.0\. "
                r"Use user_access_permissions instead"
            ),
    ):
        assert reader.decode_permissions(4) == print_

    modify = base.copy()
    modify["modify"] = True
    with pytest.raises(
        DeprecationError,
        match=(
            r"decode_permissions is deprecated and was removed in pypdf 5\.0\.0\. "
            r"Use user_access_permissions instead"
        ),
    ):
        assert reader.decode_permissions(8) == modify


@pytest.mark.skipif(not HAS_AES, reason="No AES implementation")
def test_user_access_permissions():
    # Not encrypted.
    reader = PdfReader(RESOURCE_ROOT / "crazyones.pdf")
    assert reader.user_access_permissions is None

    # Encrypted.
    reader = PdfReader(RESOURCE_ROOT / "encryption" / "r6-owner-password.pdf")
    assert reader.user_access_permissions == UAP.all()

    # Custom writer permissions.
    writer = PdfWriter(clone_from=RESOURCE_ROOT / "crazyones.pdf")
    writer.encrypt(
        user_password="",
        owner_password="abc",
        permissions_flag=UAP.PRINT | UAP.FILL_FORM_FIELDS,
    )
    output = BytesIO()
    writer.write(output)
    reader = PdfReader(output)
    assert reader.user_access_permissions == (UAP.PRINT | UAP.FILL_FORM_FIELDS)

    # All writer permissions.
    writer = PdfWriter(clone_from=RESOURCE_ROOT / "crazyones.pdf")
    writer.encrypt(
        user_password="",
        owner_password="abc",
        permissions_flag=UAP.all(),
    )
    output = BytesIO()
    writer.write(output)
    reader = PdfReader(output)
    assert reader.user_access_permissions == UAP.all()


def test_pages_attribute():
    pdf_path = RESOURCE_ROOT / "crazyones.pdf"
    reader = PdfReader(pdf_path)

    # Test if getting as slice throws an error
    assert len(reader.pages[:]) == 1

    with pytest.raises(IndexError) as exc:
        reader.pages[-1000]

    assert exc.value.args[0] == "Sequence index out of range"

    with pytest.raises(IndexError):
        reader.pages[1000]

    assert exc.value.args[0] == "Sequence index out of range"


@pytest.mark.parametrize(
    ("d", "size", "expected"),
    [
        (b"\x01", 8, 1),
        # Cross-reference stream entry fields are >= 0 (ISO 32000-2, Table 18),
        # so a wide field with the high bit set must not decode to a negative value.
        (b"\x80\x00\x00\x00\x00\x00\x00\x01", 8, 0x8000000000000001),
    ],
)
def test_convert_to_int(d, size, expected):
    assert convert_to_int(d, size) == expected


def test_convert_to_int_error():
    with pytest.raises(PdfReadError) as exc:
        convert_to_int(b"256", 16)
    assert exc.value.args[0] == "Invalid size in convert_to_int"


@pytest.mark.enable_socket
def test_iss925():
    url = "https://github.com/py-pdf/pypdf/files/8796328/1.pdf"
    reader = PdfReader(BytesIO(get_data_from_url(url=url, name="iss925.pdf")))

    for page_sliced in reader.pages:
        page_object = page_sliced.get_object()
        # Extracts the PDF's Annots (Annotations and Commenting):
        annots = page_object.get("/Annots")
        if annots is not None:
            for annot in annots:
                annot.get_object()


def test_get_object():
    reader = PdfReader(RESOURCE_ROOT / "hello-world.pdf")
    assert reader.get_object(22)["/Type"] == "/Catalog"
    assert reader._get_indirect_object(22, 0)["/Type"] == "/Catalog"


def test_extract_text_hello_world():
    reader = PdfReader(RESOURCE_ROOT / "hello-world.pdf")
    text = reader.pages[0].extract_text().split("\n")
    assert text == [
        "English:",
        "Hello World",
        "Arabic:",
        "مرحبا بالعالم",
        "Russian:",
        "Привет, мир",
        "Chinese (traditional):",
        "你好世界",
        "Thai:",
        "สวัสดีชาวโลก",
        "Japanese:",
        "こんにちは世界",
    ]


def test_read_path():
    path = Path(RESOURCE_ROOT, "crazyones.pdf")
    reader = PdfReader(path)
    assert len(reader.pages) == 1


def test_read_not_binary_mode(caplog):
    with open(RESOURCE_ROOT / "crazyones.pdf") as f:
        msg = (
            "PdfReader stream/file object is not in binary mode. "
            "It may not be read correctly."
        )
        with pytest.raises(io.UnsupportedOperation):
            PdfReader(f)
    assert normalize_warnings(caplog.text) == [msg]


@pytest.mark.enable_socket
@pytest.mark.skipif(not HAS_AES, reason="No AES algorithm available")
def test_read_form_416():
    url = (
        "https://www.fda.gov/downloads/AboutFDA/ReportsManualsForms/Forms/UCM074728.pdf"
    )
    reader = PdfReader(BytesIO(get_data_from_url(url=url, name="issue_416.pdf")))
    fields = reader.get_form_text_fields()
    assert len(fields) > 0


def test_form_topname_with_and_without_acroform(caplog):
    r = PdfReader(RESOURCE_ROOT / "crazyones.pdf")
    r.add_form_topname("no")
    r.rename_form_topname("renamed")
    assert "/AcroForm" not in r.trailer["/Root"]
    r.trailer["/Root"][NameObject("/AcroForm")] = DictionaryObject()
    r.add_form_topname("toto")
    r.rename_form_topname("renamed")
    assert len(r.get_fields()) == 0

    r = PdfReader(RESOURCE_ROOT / "form.pdf")
    r.add_form_topname("top")
    flds = r.get_fields()
    assert "top" in flds
    assert "top.foo" in flds
    r.rename_form_topname("renamed")
    flds = r.get_fields()
    assert "renamed" in flds
    assert "renamed.foo" in flds

    r = PdfReader(RESOURCE_ROOT / "form.pdf")
    r.get_fields()["foo"].indirect_reference.get_object()[
        NameObject("/Parent")
    ] = DictionaryObject()
    r.add_form_topname("top")
    assert "has a non-expected parent" in caplog.text


@pytest.mark.enable_socket
def test_extract_text_xref_issue_2(caplog):
    # pdf/0264cf510015b2a4b395a15cb23c001e.pdf
    url = "https://github.com/user-attachments/files/18381758/tika-981961.pdf"
    msg = [
        "incorrect startxref pointer(2)",
        "parsing for Object Streams",
    ]
    reader = PdfReader(BytesIO(get_data_from_url(url=url, name="tika-981961.pdf")))
    for page in reader.pages:
        page.extract_text()
    assert normalize_warnings(caplog.text) == msg


@pytest.mark.enable_socket
@pytest.mark.slow
def test_extract_text_xref_issue_3(caplog):
    # pdf/0264cf510015b2a4b395a15cb23c001e.pdf
    url = "https://github.com/user-attachments/files/18381755/tika-977774.pdf"
    msg = [
        "incorrect startxref pointer(3)",
    ]
    reader = PdfReader(BytesIO(get_data_from_url(url=url, name="tika-977774.pdf")))
    for page in reader.pages:
        page.extract_text()
    assert normalize_warnings(caplog.text) == msg


@pytest.mark.enable_socket
def test_extract_text_pdf15():
    # pdf/0264cf510015b2a4b395a15cb23c001e.pdf
    url = "https://github.com/user-attachments/files/18381751/tika-976030.pdf"
    reader = PdfReader(BytesIO(get_data_from_url(url=url, name="tika-976030.pdf")))
    for page in reader.pages:
        page.extract_text()


@pytest.mark.enable_socket
def test_extract_text_xref_table_21_bytes_clrf():
    # pdf/0264cf510015b2a4b395a15cb23c001e.pdf
    url = "https://github.com/user-attachments/files/18381723/tika-956939.pdf"
    reader = PdfReader(BytesIO(get_data_from_url(url=url, name="tika-956939.pdf")))
    for page in reader.pages:
        page.extract_text()


@pytest.mark.enable_socket
def test_get_fields():
    url = "https://github.com/user-attachments/files/18381747/tika-972486.pdf"
    name = "tika-972486.pdf"
    reader = PdfReader(BytesIO(get_data_from_url(url=url, name=name)))
    fields = reader.get_fields()
    assert fields is not None
    assert "c1-1" in fields
    assert dict(fields["c1-1"]) == (
        {"/FT": "/Btn", "/T": "c1-1", "/_States_": ["/On", "/Off"]}
    )


@pytest.mark.enable_socket
def test_get_full_qualified_fields():
    url = "https://github.com/py-pdf/pypdf/files/10142389/fields_with_dots.pdf"
    name = "fields_with_dots.pdf"
    reader = PdfReader(BytesIO(get_data_from_url(url=url, name=name)))
    fields = reader.get_form_text_fields(True)
    assert fields is not None
    assert "customer.name" in fields

    fields = reader.get_form_text_fields(False)
    assert fields is not None
    assert "customer.name" not in fields
    assert "name" in fields

    fields = reader.get_fields(True)
    assert fields is not None
    assert "customer.name" in fields
    assert fields["customer.name"]["/T"] == "name"


@pytest.mark.enable_socket
@pytest.mark.filterwarnings("ignore::pypdf.errors.PdfReadWarning")
def test_get_fields_read_else_block():
    # covers also issue 1089
    url = "https://github.com/user-attachments/files/18381705/tika-934771.pdf"
    name = "tika-934771.pdf"
    PdfReader(BytesIO(get_data_from_url(url=url, name=name)))


@pytest.mark.enable_socket
def test_get_fields_read_else_block2():
    url = "https://github.com/user-attachments/files/18381689/tika-914902.pdf"
    name = "tika-914902.pdf"
    reader = PdfReader(BytesIO(get_data_from_url(url=url, name=name)))
    fields = reader.get_fields()
    assert fields is None


@pytest.mark.enable_socket
@pytest.mark.filterwarnings("ignore::pypdf.errors.PdfReadWarning")
def test_get_fields_read_else_block3():
    url = "https://github.com/user-attachments/files/18381726/tika-957721.pdf"
    name = "tika-957721.pdf"
    PdfReader(BytesIO(get_data_from_url(url=url, name=name)))


@pytest.mark.enable_socket
def test_metadata_is_none():
    url = "https://github.com/user-attachments/files/18381735/tika-963692.pdf"
    name = "tika-963692.pdf"
    reader = PdfReader(BytesIO(get_data_from_url(url=url, name=name)))
    assert reader.metadata is None


@pytest.mark.enable_socket
def test_get_fields_read_write_report(txt_file_path):
    url = "https://github.com/user-attachments/files/18381683/tika-909655.pdf"
    name = "tika-909655.pdf"
    reader = PdfReader(BytesIO(get_data_from_url(url=url, name=name)))
    with open(txt_file_path, "w") as fp:
        fields = reader.get_fields(fileobj=fp)
    assert fields


@pytest.mark.parametrize(
    "src",
    [
        RESOURCE_ROOT / "crazyones.pdf",
        RESOURCE_ROOT / "commented.pdf",
    ],
)
def test_xfa(src):
    reader = PdfReader(src)
    assert reader.xfa is None


@pytest.mark.enable_socket
def test_xfa_non_empty():
    url = "https://github.com/user-attachments/files/18381713/tika-942050.pdf"
    name = "tika-942050.pdf"
    reader = PdfReader(BytesIO(get_data_from_url(url=url, name=name)))
    assert list(reader.xfa.keys()) == [
        "preamble",
        "config",
        "template",
        "PDFSecurity",
        "datasets",
        "postamble",
    ]


@pytest.mark.parametrize(
    ("src", "pdf_header"),
    [
        (RESOURCE_ROOT / "attachment.pdf", "%PDF-1.5"),
        (RESOURCE_ROOT / "crazyones.pdf", "%PDF-1.5"),
    ],
)
def test_header(src, pdf_header):
    reader = PdfReader(src)

    assert reader.pdf_header == pdf_header


@pytest.mark.enable_socket
def test_outline_color():
    reader = PdfReader(BytesIO(get_data_from_url(name="tika-924546.pdf")))
    assert reader.outline[0].color == [0, 0, 1]


@pytest.mark.enable_socket
def test_outline_font_format():
    reader = PdfReader(BytesIO(get_data_from_url(name="tika-924546.pdf")))
    assert reader.outline[0].font_format == 2


def get_outline_property(outline, attribute_name: str):
    results = []
    if isinstance(outline, list):
        for outline_item in outline:
            if isinstance(outline_item, Destination):
                results.append(getattr(outline_item, attribute_name))
            else:
                results.append(get_outline_property(outline_item, attribute_name))
    else:
        raise ValueError(f"got {type(outline)}")
    return results


@pytest.mark.samples
def test_outline_title_issue_1121():
    reader = PdfReader(SAMPLE_ROOT / "014-outlines/mistitled_outlines_example.pdf")

    assert get_outline_property(reader.outline, "title") == [
        "First",
        [
            "Second",
            "Third",
            "Fourth",
            [
                "Fifth",
                "Sixth",
            ],
            "Seventh",
            [
                "Eighth",
                "Ninth",
            ],
        ],
        "Tenth",
        [
            "Eleventh",
            "Twelfth",
            "Thirteenth",
            "Fourteenth",
        ],
        "Fifteenth",
        [
            "Sixteenth",
            "Seventeenth",
        ],
        "Eighteenth",
        "Nineteenth",
        [
            "Twentieth",
            "Twenty-first",
            "Twenty-second",
            "Twenty-third",
            "Twenty-fourth",
            "Twenty-fifth",
            "Twenty-sixth",
            "Twenty-seventh",
        ],
    ]


@pytest.mark.samples
def test_outline_count():
    reader = PdfReader(SAMPLE_ROOT / "014-outlines/mistitled_outlines_example.pdf")

    assert get_outline_property(reader.outline, "outline_count") == [
        5,
        [
            None,
            None,
            2,
            [
                None,
                None,
            ],
            -2,
            [
                None,
                None,
            ],
        ],
        4,
        [
            None,
            None,
            None,
            None,
        ],
        -2,
        [
            None,
            None,
        ],
        None,
        8,
        [
            None,
            None,
            None,
            None,
            None,
            None,
            None,
            None,
        ],
    ]


def test_outline_missing_title(caplog):
    # Strict
    reader = PdfReader(RESOURCE_ROOT / "outline-without-title.pdf", strict=True)
    with pytest.raises(PdfReadError) as exc:
        reader.outline
    assert exc.value.args[0].startswith("Outline Entry Missing /Title attribute:")

    # Non-strict : no errors
    reader = PdfReader(RESOURCE_ROOT / "outline-without-title.pdf", strict=False)
    assert reader.outline[0]["/Title"] == ""


@pytest.mark.enable_socket
@pytest.mark.parametrize(
    ("url", "name"),
    [
        # 1st case : the named_dest are stored directly as a dictionary, PDF 1.1 style
        (
            "https://github.com/py-pdf/pypdf/files/9197028/lorem_ipsum.pdf",
            "lorem_ipsum.pdf",
        ),
        # 2nd case : Dest below names and with Kids...
        (
            "https://github.com/py-pdf/pypdf/files/11714214/PDF32000_2008.pdf",
            "PDF32000_2008.pdf",
        )
        # 3rd case : Dests with Name tree (TODO: Add this case)
    ],
    ids=["stored_directly", "dest_below_names_with_kids"],
)
def test_named_destination(url, name):
    reader = PdfReader(BytesIO(get_data_from_url(url=url, name=name)))
    assert len(reader.named_destinations) > 0


@pytest.mark.enable_socket
def test_outline_with_missing_named_destination():
    url = "https://github.com/user-attachments/files/18381686/tika-913678.pdf"
    name = "tika-913678.pdf"
    reader = PdfReader(BytesIO(get_data_from_url(url=url, name=name)))
    # outline items in document reference a named destination that is not defined
    assert reader.outline[1][0].title.startswith("Report for 2002AZ3B: Microbial")


@pytest.mark.enable_socket
def test_outline_with_empty_action():
    url = "https://github.com/user-attachments/files/18381697/tika-924546.pdf"
    name = "tika-924546.pdf"
    reader = PdfReader(BytesIO(get_data_from_url(url=url, name=name)))
    # outline items (entitled Tables and Figures) utilize an empty action (/A)
    # that has no type or destination
    assert reader.outline[-4].title == "Tables"


def test_outline_with_invalid_destinations():
    reader = PdfReader(RESOURCE_ROOT / "outlines-with-invalid-destinations.pdf")
    # contains 9 outline items, 6 with invalid destinations
    # caused by different malformations
    assert len(reader.outline) == 9


@pytest.mark.enable_socket
def test_pdfreader_multiple_definitions(caplog):
    """iss325"""
    url = "https://github.com/py-pdf/pypdf/files/9176644/multipledefs.pdf"
    name = "multipledefs.pdf"
    reader = PdfReader(BytesIO(get_data_from_url(url=url, name=name)))
    reader.pages[0].extract_text()
    assert normalize_warnings(caplog.text) == [
        "Multiple definitions in dictionary at byte 0xb5 for key /Group"
    ]


def test_wrong_password_error():
    encrypted_pdf_path = RESOURCE_ROOT / "encrypted-file.pdf"
    with pytest.raises(WrongPasswordError):
        PdfReader(
            encrypted_pdf_path,
            password="definitely_the_wrong_password!",
        )


def test_get_page_number_by_indirect():
    reader = PdfReader(RESOURCE_ROOT / "crazyones.pdf")
    reader._get_page_number_by_indirect(1)


@pytest.mark.enable_socket
def test_corrupted_xref_table():
    # issue #1292
    url = "https://github.com/py-pdf/pypdf/files/9444747/BreezeManual.orig.pdf"
    name = "BreezeMan1.pdf"
    reader = PdfReader(BytesIO(get_data_from_url(url=url, name=name)))
    reader.pages[0].extract_text()
    url = "https://github.com/py-pdf/pypdf/files/9444748/BreezeManual.failed.pdf"
    name = "BreezeMan2.pdf"
    reader = PdfReader(BytesIO(get_data_from_url(url=url, name=name)))
    reader.pages[0].extract_text()


@pytest.mark.enable_socket
def test_reader(caplog):
    # iss #1273
    url = "https://github.com/py-pdf/pypdf/files/9464742/shiv_resume.pdf"
    name = "shiv_resume.pdf"
    reader = PdfReader(BytesIO(get_data_from_url(url=url, name=name)))
    assert "Previous trailer cannot be read" in caplog.text
    caplog.clear()
    # first call requires some reparations...
    reader.pages[0].extract_text()
    caplog.clear()
    # ...and now no more required
    reader.pages[0].extract_text()
    assert caplog.text == ""


@pytest.mark.enable_socket
def test_zeroing_xref():
    # iss #328
    url = (
        "https://github.com/py-pdf/pypdf/files/9066120/"
        "UTA_OSHA_3115_Fall_Protection_Training_09162021_.pdf"
    )
    name = "UTA_OSHA.pdf"
    reader = PdfReader(BytesIO(get_data_from_url(url=url, name=name)))
    len(reader.pages)


@pytest.mark.enable_socket
def test_thread():
    url = (
        "https://github.com/py-pdf/pypdf/files/9066120/"
        "UTA_OSHA_3115_Fall_Protection_Training_09162021_.pdf"
    )
    name = "UTA_OSHA.pdf"
    reader = PdfReader(BytesIO(get_data_from_url(url=url, name=name)))
    assert reader.threads is None
    url = "https://github.com/user-attachments/files/18381699/tika-924666.pdf"
    name = "tika-924666.pdf"
    reader = PdfReader(BytesIO(get_data_from_url(url=url, name=name)))
    assert isinstance(reader.threads, ArrayObject)
    assert len(reader.threads) >= 1


@pytest.mark.enable_socket
def test_build_outline_item(caplog):
    url = "https://github.com/py-pdf/pypdf/files/9464742/shiv_resume.pdf"
    name = "shiv_resume.pdf"
    reader = PdfReader(BytesIO(get_data_from_url(url=url, name=name)))
    outline = reader._build_outline_item(
        DictionaryObject(
            {
                NameObject("/Title"): TextStringObject("Toto"),
                NameObject("/Dest"): NumberObject(2),
            }
        )
    )
    assert "Removed unexpected destination 2 from destination" in caplog.text
    assert outline["/Title"] == "Toto"
    reader.strict = True
    with pytest.raises(PdfReadError) as exc:
        reader._build_outline_item(
            DictionaryObject(
                {
                    NameObject("/Title"): TextStringObject("Toto"),
                    NameObject("/Dest"): NumberObject(2),
                }
            )
        )
    assert "Unexpected destination 2" in exc.value.args[0]


@pytest.mark.samples
@pytest.mark.parametrize(
    ("src", "page_labels"),
    [
        (RESOURCE_ROOT / "selenium-pypdf-issue-177.pdf", ["1"]),
        (RESOURCE_ROOT / "encrypted_doc_no_id.pdf", ["1", "2", "3"]),
        (RESOURCE_ROOT / "pdflatex-outline.pdf", ["1", "2", "3", "4"]),
        (
            SAMPLE_ROOT / "009-pdflatex-geotopo/GeoTopo.pdf",
            ["i", "ii", "iii", "1", "2", "3"],
        ),
    ],
    ids=[
        "selenium-pypdf-issue-177.pdf",
        "encrypted_doc_no_id.pdf",
        "pdflatex-outline.pdf",
        "GeoTopo.pdf",
    ],
)
def test_page_labels(src, page_labels):
    max_indices = 6
    assert PdfReader(src).page_labels[:max_indices] == page_labels[:max_indices]


@pytest.mark.enable_socket
def test_iss1559():
    url = "https://github.com/py-pdf/pypdf/files/10441992/default.pdf"
    name = "iss1559.pdf"
    reader = PdfReader(BytesIO(get_data_from_url(url=url, name=name)))
    for p in reader.pages:
        p.extract_text()


@pytest.mark.enable_socket
def test_iss1652():
    # test of an annotation(link) directly stored in the /Annots in the page
    url = "https://github.com/py-pdf/pypdf/files/10818844/tt.pdf"
    name = "invalidNamesDest.pdf"
    reader = PdfReader(BytesIO(get_data_from_url(url=url, name=name)))
    reader.named_destinations


@pytest.mark.enable_socket
def test_iss1689():
    url = "https://github.com/py-pdf/pypdf/files/10948283/error_file_without_data.pdf"
    name = "iss1689.pdf"
    reader = PdfReader(BytesIO(get_data_from_url(url=url, name=name)))
    reader.pages[0]


@pytest.mark.enable_socket
def test_iss1710():
    url = "https://github.com/py-pdf/pypdf/files/15234776/irbookonlinereading.pdf"
    name = "irbookonlinereading.pdf"
    reader = PdfReader(BytesIO(get_data_from_url(url=url, name=name)))
    reader.outline


def test_broken_file_header():
    pdf_data = (
        b"%%PDF-\xa0sd\n"
        b"1 0 obj << /Count 1 /Kids [4 0 R] /Type /Pages >> endobj\n"
        b"2 0 obj << >> endobj\n"
        b"3 0 obj << >> endobj\n"
        b"4 0 obj << /Contents 3 0 R /CropBox [0.0 0.0 2550.0 3508.0]"
        b" /MediaBox [0.0 0.0 2550.0 3508.0] /Parent 1 0 R"
        b" /Resources << /Font << >> >>"
        b" /Rotate 0 /Type /Page >> endobj\n"
        b"5 0 obj << /Pages 1 0 R /Type /Catalog >> endobj\n"
        b"xref 1 5\n"
        b"%010d 00000 n\n"
        b"%010d 00000 n\n"
        b"%010d 00000 n\n"
        b"%010d 00000 n\n"
        b"%010d 00000 n\n"
        b"trailer << %s/Root 5 0 R /Size 6 >>\n"
        b"startxref %d\n"
        b"%%%%EOF"
    )
    with_prev_0 = True
    pdf_data = pdf_data % (
        pdf_data.find(b"1 0 obj"),
        pdf_data.find(b"2 0 obj"),
        pdf_data.find(b"3 0 obj"),
        pdf_data.find(b"4 0 obj"),
        pdf_data.find(b"5 0 obj"),
        b"/Prev 0 " if with_prev_0 else b"",
        pdf_data.find(b"xref") - 1,
    )
    PdfReader(io.BytesIO(pdf_data))


@pytest.mark.enable_socket
def test_iss1756():
    url = "https://github.com/py-pdf/pypdf/files/11105591/641-Attachment-B-Pediatric-Cardiac-Arrest-8-1-2019.pdf"
    name = "iss1756.pdf"
    reader = PdfReader(BytesIO(get_data_from_url(url=url, name=name)))
    reader.trailer["/ID"]
    # removed to cope with missing cryptodome during commit check : len(reader.pages)


@pytest.mark.enable_socket
@pytest.mark.timeout(30)
def test_iss1825():
    url = "https://github.com/py-pdf/pypdf/files/11367871/MiFO_LFO_FEIS_NOA_Published.3.pdf"
    name = "iss1825.pdf"
    reader = PdfReader(BytesIO(get_data_from_url(url=url, name=name)))
    page = reader.pages[0]
    page.extract_text()


@pytest.mark.enable_socket
def test_iss2082():
    url = "https://github.com/py-pdf/pypdf/files/12317939/test.pdf"
    name = "iss2082.pdf"
    b = get_data_from_url(url=url, name=name)
    reader = PdfReader(BytesIO(b))
    reader.pages[0].extract_text()

    bb = bytearray(b)
    bb[b.find(b"xref") + 2] = ord(b"E")
    with pytest.raises(PdfReadError):
        reader = PdfReader(BytesIO(bb))


@pytest.mark.enable_socket
def test_issue_140():
    url = "https://github.com/py-pdf/pypdf/files/12168578/bad_pdf_example.pdf"
    name = "issue-140.pdf"
    b = get_data_from_url(url=url, name=name)
    reader = PdfReader(BytesIO(b))
    assert len(reader.pages) == 54


@pytest.mark.enable_socket
def test_xyz_with_missing_param():
    """Cf #2236"""
    url = "https://github.com/py-pdf/pypdf/files/12795356/tt1.pdf"
    name = "issue2236.pdf"
    reader = PdfReader(BytesIO(get_data_from_url(url=url, name=name)))
    assert reader.outline[0]["/Left"] == 820
    assert reader.outline[0]["/Top"] == 0
    assert reader.outline[1]["/Left"] == 0
    assert reader.outline[0]["/Top"] == 0


@pytest.mark.enable_socket
def test_corrupted_xref():
    url = "https://github.com/py-pdf/pypdf/files/14628314/iss2516.pdf"
    name = "iss2516.pdf"
    reader = PdfReader(BytesIO(get_data_from_url(url=url, name=name)))
    assert reader.root_object["/Type"] == "/Catalog"


@pytest.mark.enable_socket
def test_truncated_xref(caplog):
    url = "https://github.com/py-pdf/pypdf/files/14843553/002-trivial-libre-office-writer-broken.pdf"
    name = "iss2575.pdf"
    PdfReader(BytesIO(get_data_from_url(url=url, name=name)))
    assert "Invalid/Truncated xref table. Rebuilding it." in caplog.text


@pytest.mark.enable_socket
def test_damaged_pdf():
    url = "https://github.com/py-pdf/pypdf/files/15186107/malformed_pdf.pdf"
    name = "malformed_pdf.pdf"
    reader = PdfReader(BytesIO(get_data_from_url(url=url, name=name)), strict=False)
    len(reader.pages)
    strict_reader = PdfReader(BytesIO(get_data_from_url(url=url, name=name)), strict=True)
    with pytest.raises(PdfReadError) as exc:
        len(strict_reader.pages)
    assert (
        exc.value.args[0] == "Expected object ID (21 0) does not match actual (-1 -1)."
    )


@pytest.mark.enable_socket
@pytest.mark.timeout(10)
def test_looping_form(caplog):
    """Cf iss 2643"""
    url = "https://github.com/py-pdf/pypdf/files/15306053/inheritance.pdf"
    name = "iss2643.pdf"
    reader = PdfReader(BytesIO(get_data_from_url(url=url, name=name)), strict=False)
    flds = reader.get_fields()
    assert all(
        x in flds
        for x in (
            "Text10",
            "Text10.0.0.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1",
            "amt1.0",
            "amt1.1",
            "DSS#3pg3#0hgu7",
        )
    )
    writer = PdfWriter(reader)
    writer.root_object["/AcroForm"]["/Fields"][5]["/Kids"].append(
        writer.root_object["/AcroForm"]["/Fields"][5]["/Kids"][0]
    )
    flds2 = writer.get_fields()
    assert "Text68.0 already parsed" in caplog.text
    assert list(flds.keys()) == list(flds2.keys())


def test_context_manager_with_stream():
    pdf_data = (
        b"%%PDF-1.7\n"
        b"1 0 obj << /Count 1 /Kids [4 0 R] /Type /Pages >> endobj\n"
        b"2 0 obj << >> endobj\n"
        b"3 0 obj << >> endobj\n"
        b"4 0 obj << /Contents 3 0 R /CropBox [0.0 0.0 2550.0 3508.0]"
        b" /MediaBox [0.0 0.0 2550.0 3508.0] /Parent 1 0 R"
        b" /Resources << /Font << >> >>"
        b" /Rotate 0 /Type /Page >> endobj\n"
        b"5 0 obj << /Pages 1 0 R /Type /Catalog >> endobj\n"
        b"xref 1 5\n"
        b"%010d 00000 n\n"
        b"%010d 00000 n\n"
        b"%010d 00000 n\n"
        b"%010d 00000 n\n"
        b"%010d 00000 n\n"
        b"trailer << /Root 5 0 R /Size 6 >>\n"
        b"startxref %d\n"
        b"%%%%EOF"
    )
    pdf_data = pdf_data % (
        pdf_data.find(b"1 0 obj"),
        pdf_data.find(b"2 0 obj"),
        pdf_data.find(b"3 0 obj"),
        pdf_data.find(b"4 0 obj"),
        pdf_data.find(b"5 0 obj"),
        pdf_data.find(b"xref") - 1,
    )
    pdf_stream = io.BytesIO(pdf_data)
    with PdfReader(pdf_stream) as reader:
        assert not reader.stream.closed
    assert not pdf_stream.closed


@pytest.mark.enable_socket
@pytest.mark.timeout(10)
def test_iss2761():
    url = "https://github.com/user-attachments/files/16312198/crash-b26d05712a29b241ac6f9dc7fff57428ba2d1a04.pdf"
    name = "iss2761.pdf"
    reader = PdfReader(BytesIO(get_data_from_url(url=url, name=name)), strict=False)
    with pytest.raises(PdfReadError):
        reader.pages[0].extract_text()


@pytest.mark.enable_socket
def test_iss2817():
    """Test for rebuilding Xref_ObjStm"""
    url = "https://github.com/user-attachments/files/16764070/crash-7e1356f1179b4198337f282304cb611aea26a199.pdf"
    name = "iss2817.pdf"
    reader = PdfReader(BytesIO(get_data_from_url(url=url, name=name)))
    assert (
        reader.pages[0]["/Annots"][0].get_object()["/Contents"]
        == "A\xa0\xa0\xa0\xa0\xa0\xa0\xa0\xa0\xa0 B"
    )


@pytest.mark.enable_socket
def test_truncated_files(caplog):
    """Cf #2853"""
    url = "https://github.com/user-attachments/files/16796095/f5471sm-2.pdf"
    name = "iss2780.pdf"  # reused
    b = get_data_from_url(url=url, name=name)
    reader = PdfReader(BytesIO(b))
    assert caplog.text == ""
    # remove \n at end of file : invisible
    reader = PdfReader(BytesIO(b[:-1]))
    assert caplog.text == ""
    # truncate but still detectable
    for i in range(-2, -6, -1):
        caplog.clear()
        reader = PdfReader(BytesIO(b[:i]))
        assert "EOF marker seems truncated" in caplog.text
        assert reader._startxref == 100993
    # remove completely EOF : we will not read last section
    caplog.clear()
    reader = PdfReader(BytesIO(b[:-6]))
    assert "CAUTION: startxref found while searching for %%EOF" in caplog.text
    assert reader._startxref < 100993


@pytest.mark.enable_socket
def test_comments_in_array(caplog):
    """Cf #2843: this deals with comments"""
    url = "https://github.com/user-attachments/files/16992416/crash-2347912aa2a6f0fab5df4ebc8a424735d5d0d128.pdf"
    name = "iss2843.pdf"  # reused
    b = get_data_from_url(url=url, name=name)
    reader = PdfReader(BytesIO(b))
    reader.pages[0]
    assert caplog.text == ""
    reader = PdfReader(BytesIO(b))
    reader.stream = BytesIO(b[:1149])
    with pytest.raises(PdfStreamError):
        reader.pages[0]


@pytest.mark.enable_socket
def test_space_in_names_to_continue_processing(caplog):
    """
    This deals with space not encoded in names inducing errors.
    Also covers case where NameObject not met for key.
    """
    url = "https://github.com/user-attachments/files/17095516/crash-e108c4f677040b61e12fa9f1cfde025d704c9b0d.pdf"
    name = "iss2866.pdf"  # reused
    b = get_data_from_url(url=url, name=name)
    reader = PdfReader(BytesIO(b))
    obj = reader.get_object(70)
    assert all(
        x in obj
        for x in (
            "/BaseFont",
            "/DescendantFonts",
            "/Encoding",
            "/Subtype",
            "/ToUnicode",
            "/Type",
        )
    )
    assert obj["/BaseFont"] == "/AASGAA+Arial,Unicode"  # MS is missing to meet spec
    assert 'PdfReadError("Invalid Elementary Object starting with' in caplog.text

    caplog.clear()

    b = b[:264] + b"(Inv) /d " + b[273:]
    reader = PdfReader(BytesIO(b))
    obj = reader.get_object(70)
    assert all(
        x in obj
        for x in ["/DescendantFonts", "/Encoding", "/Subtype", "/ToUnicode", "/Type"]
    )
    assert all(
        x in caplog.text
        for x in (
            "Expecting a NameObject for key but",
            'PdfReadError("Invalid Elementary Object starting with',
        )
    )
    reader = PdfReader(BytesIO(b), strict=True)
    with pytest.raises(PdfReadError):
        obj = reader.get_object(70)


@pytest.mark.enable_socket
def test_unbalanced_brackets_in_dictionary_object(caplog):
    """Cf #2877"""
    url = "https://github.com/user-attachments/files/17162634/7f40cb209fb97d1782bffcefc5e7be40.pdf"
    name = "iss2877.pdf"  # reused
    reader = PdfReader(BytesIO(get_data_from_url(url=url, name=name)))
    assert len(reader.pages) == 43  # note:  /Count = 46 but 3 kids are None


@pytest.mark.enable_socket
def test_repair_root(caplog):
    """Cf #2877"""
    url = "https://github.com/user-attachments/files/17162216/crash-6620e8b1abfe3da639b654595da859b87f985748.pdf"
    name = "iss2875.pdf"

    b = get_data_from_url(url=url, name=name)
    reader = PdfReader(BytesIO(b))
    assert len(reader.pages) == 1
    assert all(
        msg in caplog.text
        for msg in (
            "Invalid Root object",
            'Searching object with "/Catalog" key',
            "Root found at IndirectObject(2, 0,",
        )
    )

    # no /Root Entry
    reader = PdfReader(BytesIO(b.replace(b"/Root", b"/Roo ")))
    caplog.clear()
    assert len(reader.pages) == 1
    assert all(
        msg in caplog.text
        for msg in (
            'Cannot find "/Root" key in trailer',
            'Searching object with "/Catalog" key',
            "Root found at IndirectObject(2, 0,",
        )
    )

    # Invalid /Root Entry
    caplog.clear()
    reader = PdfReader(
        BytesIO(
            b.replace(b"/Root 1 0 R", b"/Root 2 0 R").replace(b"/Catalog/Pages 3 0 R", b"/Catalo ")
        )
    )
    with pytest.raises(PdfReadError):
        len(reader.pages)
    assert all(
        msg in caplog.text
        for msg in (
            "Invalid Root object in trailer",
            'Searching object with "/Catalog" key',
        )
    )

    # Invalid /Root Entry + error in get_object
    caplog.clear()
    data = b.replace(b"/Root 1 0 R", b"/Root 2 0 R").replace(b"/Catalog/Pages 3 0 R", b"/Catalo ")
    data = data[:5124] + b"A" + data[5125:]
    reader = PdfReader(BytesIO(data))
    with pytest.raises(PdfReadError):
        len(reader.pages)
    assert all(
        msg in caplog.text
        for msg in (
            "Invalid Root object in trailer",
            'Searching object with "/Catalog" key',
        )
    )

    # Invalid /Root Entry without /Type, but /Pages.
    caplog.clear()
    reader = PdfReader(
        BytesIO(
            b.replace(b"/Root 1 0 R", b"/Root 2 0 R").replace(b"/Catalog", b"/Catalo ")
        )
    )
    assert len(reader.pages) == 1
    assert all(
        msg in caplog.text
        for msg in (
            "Invalid Root object in trailer",
            'Searching object with "/Catalog" key',
            f"Possible root found at IndirectObject(2, 0, {id(reader)}), but missing /Catalog key"
        )
    )


@pytest.mark.enable_socket
def test_issue3151(caplog):
    """Tests for #3151"""
    url = "https://github.com/user-attachments/files/18941494/bible.pdf"
    name = "issue3151.pdf"
    reader = PdfReader(BytesIO(get_data_from_url(url=url, name=name)))
    assert len(reader.pages) == 742


@pytest.mark.enable_socket
def test_issue2886(caplog):
    """Tests for #2886"""
    url = "https://github.com/user-attachments/files/17187711/crash-e8a85d82de01cab5eb44e7993304d8b9d1544970.pdf"
    name = "issue2886.pdf"

    # Actual: 100_067
    with mock.patch("pypdf.generic._base.NumberObject._LENGTH_LIMIT", 200_000), \
            pytest.raises(PdfReadError, match=r"Unexpected empty line in Xref table\."):
        _ = PdfReader(BytesIO(get_data_from_url(url=url, name=name)))


@pytest.mark.enable_socket
def test_infinite_loop_for_length_value():
    """Tests for #3112"""
    url = "https://github.com/user-attachments/files/19106009/Special.n.15.du.jeudi.22.fevrier.2024.pdf"
    name = "issue3112.pdf"

    reader = PdfReader(BytesIO(get_data_from_url(url=url, name=name)))
    writer = PdfWriter()
    with pytest.raises(
            LimitReachedError, match=r"^Detected loop with self reference for IndirectObject\(165, 0, \d+\)\.$"
    ):
        writer.add_page(reader.pages[0])


def test_trailer_cannot_be_read():
    path = RESOURCE_ROOT / "crazyones.pdf"
    data = path.read_bytes().replace(b"/Type/XRef", b"/Type/Invalid")
    with pytest.raises(PdfReadError, match=r"^Trailer cannot be read: Unexpected type '/Invalid'$"):
        reader = PdfReader(BytesIO(data))
        list(reader.pages)


@pytest.mark.enable_socket
def test_read_pdf15_xref_stream():
    data = get_data_from_url(name="issue-3429.pdf")

    with pytest.raises(PdfReadError, match=r"^Trailer cannot be read: Size missing from XRef stream {"):
        PdfReader(BytesIO(data))

    data_modified = data.replace(b"/XRef/", b"/XRef/Size/2/")
    with pytest.raises(
            PdfReadError,
            match=r"^Trailer cannot be read: Limit reached while decompressing\. 1545392 bytes remaining\.$"
    ):
        PdfReader(BytesIO(data_modified))


@pytest.mark.enable_socket
def test_read_standard_xref_table__two_whitespace_characters_between_offset_and_generation():
    """Tests for #3482"""
    url = "https://github.com/user-attachments/files/22591813/helloworld.pdf"
    name = "issue3482.pdf"

    reader = PdfReader(BytesIO(get_data_from_url(url=url, name=name)))
    assert len(reader.pages) == 1
    assert reader.pages[0].extract_text() == "Hello World!"


def test_read_standard_xref_table__entry_invalid_but_object_found(caplog):
    """Tests for #3841"""
    body = (
        b"%PDF-1.7\n"
        b"1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n"
        b"2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n"
        b"3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >>\nendobj\n"
    )
    xref_offset = len(body)
    corrupt_entry = b"xxxxxxxxxxxxxxxx 0\r\n"
    assert len(corrupt_entry) == 20
    data = body + (
        b"xref\n1 3\n"
        + corrupt_entry
        + b"%010d 00000 n\r\n" % body.index(b"2 0 obj")
        + b"%010d 00000 n\r\n" % body.index(b"3 0 obj")
        + b"trailer\n<< /Size 4 /Root 1 0 R >>\nstartxref\n"
        + b"%d" % xref_offset
        + b"\n%%EOF"
    )

    reader = PdfReader(BytesIO(data))
    assert len(reader.pages) == 1
    assert "entry 1 in Xref table invalid but object found" in caplog.text


@pytest.mark.enable_socket
def test_root_object_recovery_limit(caplog):
    url = "https://github.com/user-attachments/files/24525509/root_object_recovery_limit.pdf"
    name = "root_object_recovery_limit.pdf"
    data = get_data_from_url(url=url, name=name)

    # Default limit.
    reader = PdfReader(BytesIO(data))
    with pytest.raises(
            expected_exception=LimitReachedError, match=r"^Maximum Root object recovery limit reached\.$"
    ):
        _ = list(reader.pages)
    message_numbers = {
        int(message.split(" ", maxsplit=2)[1])
        for message in caplog.messages
        if message.startswith("Object ") and message.endswith(" 0 not defined.")
    }
    assert sorted(message_numbers) == list(range(5, 10001))

    # Custom limit.
    caplog.clear()
    reader = PdfReader(BytesIO(data), root_object_recovery_limit=42)
    with pytest.raises(
            expected_exception=LimitReachedError, match=r"^Maximum Root object recovery limit reached\.$"
    ):
        _ = list(reader.pages)
    message_numbers = {
        int(message.split(" ", maxsplit=2)[1])
        for message in caplog.messages
        if message.startswith("Object ") and message.endswith(" 0 not defined.")
    }
    assert sorted(message_numbers) == list(range(5, 43))

    # No limit. Do not run actual process for speed reasons.
    reader = PdfReader(BytesIO(data), root_object_recovery_limit=None)
    assert reader._root_object_recovery_limit == sys.maxsize

    # Strict mode.
    with pytest.raises(expected_exception=PdfReadError, match=r"^Broken xref table$"):
        reader = PdfReader(BytesIO(data), strict=True)
        _ = list(reader.pages)


@pytest.mark.timeout(10)
def test_rebuild_xref_table__speed():
    total_len = 2_000_790
    middle = b"\nstartxref   1\n % "
    leading_len = 0x55E  # 1374
    leading = b" " * leading_len
    trailing = b" " * (total_len - leading_len - len(middle))
    data = leading + middle + trailing

    reader = PdfReader(BytesIO(data))
    with pytest.raises(expected_exception=PdfReadError, match=r"^Cannot find Root object in pdf$"):
        _ = list(reader.pages)


def test_find_pdf_objects():
    data = (
        b"     \n"
        b"  11 0 obj\n"
        b"  12 0 obj\n"
        b"13  1  obj\n"
        b"ob\n"
        b"ab obj\n"
        b"42 1337 obj \n"
        b"\n"
    )

    result = list(PdfReader._find_pdf_objects(data))
    assert result == [(11, 0, 8), (12, 0, 19), (13, 1, 28), (42, 1337, 49)]


@pytest.mark.parametrize(
    ("data", "expected"),
    [
        (b"\n\ntrailer", []),
        (b"\n\ntrailer abc", []),
        (b"\n\ntrailer <<", [10]),
        (b"\n\ntrailer << /Key null >>\n\n  trailer << /Key 42 >>\n", [10, 37])
    ]
)
def test_find_pdf_trailers(data: bytes, expected: list[int]):
    result = list(PdfReader._find_pdf_trailers(data))
    assert result == expected


def test_cache_indirect_object_strict_overwrite_error():
    reader = PdfReader(RESOURCE_ROOT / "crazyones.pdf", strict=True)
    reader.resolved_objects[(99, 12345)] = None

    with pytest.raises(PdfReadError, match=r"^Overwriting cache for 99 12345$"):
        reader.cache_indirect_object(99, 12345, None)


def test_objstm_batch_parse_caches_all_objects():
    """Resolving one ObjStm object should batch-cache all siblings."""
    reader = PdfReader(RESOURCE_ROOT / "crazyones.pdf")
    assert len(reader.xref_objStm) > 0

    obj_ids = list(reader.xref_objStm.keys())
    first_obj = reader.get_object(obj_ids[0])
    assert first_obj is not None

    for idnum in obj_ids[1:]:
        cached = reader.cache_get_indirect_object(0, idnum)
        assert cached is not None, f"Object {idnum} was not batch-cached"


def test_objstm_cache_hit_returns_target():
    """Second call to _get_object_from_stream should return cached objects."""
    reader = PdfReader(RESOURCE_ROOT / "crazyones.pdf")
    obj_ids = list(reader.xref_objStm.keys())

    # Trigger batch parse
    reader.get_object(obj_ids[0])

    # Call again — all objects are already cached
    second_id = obj_ids[1]
    ref = IndirectObject(second_id, 0, reader)
    result = reader._get_object_from_stream(ref)
    assert result is reader.cache_get_indirect_object(0, second_id)


def test_objstm_skips_cache_for_overridden_objects():
    """Objects removed from xref_objStm should not be cached during batch parse."""
    reader = PdfReader(RESOURCE_ROOT / "crazyones.pdf")
    obj_ids = list(reader.xref_objStm.keys())
    assert len(obj_ids) >= 2

    # Simulate an incremental update overriding one object
    removed_id = obj_ids[-1]
    saved_entry = reader.xref_objStm.pop(removed_id)
    reader.resolved_objects.clear()

    result = reader.get_object(obj_ids[0])
    assert result is not None
    assert reader.cache_get_indirect_object(0, removed_id) is None
    assert reader.cache_get_indirect_object(0, obj_ids[0]) is not None

    reader.xref_objStm[removed_id] = saved_entry


def test_objstm_does_not_cache_stale_objects_from_non_authoritative_stream():
    """Decompressing a non-authoritative stream must not cache stale object copies."""

    def _write_obj(buf: io.BytesIO, objnum: int, data: Union[str, bytes]) -> int:
        offset = buf.tell()
        buf.write(f"{objnum} 0 obj\n".encode())
        buf.write(data if isinstance(data, bytes) else data.encode())
        buf.write(b"\nendobj\n")
        return offset

    def _write_objstm(buf: io.BytesIO, objnum: int, obj_contents: list[tuple[int, bytes]]) -> int:
        header_parts, data_parts, cur = [], [], 0
        for oid, content in obj_contents:
            header_parts.append(f"{oid} {cur}")
            data_parts.append(content)
            cur += len(content) + 1
        header = " ".join(header_parts) + " "
        data = b" ".join(data_parts)
        stream = header.encode() + data
        offset = buf.tell()
        buf.write(f"{objnum} 0 obj\n".encode())
        buf.write(
            f"<< /Type /ObjStm /N {len(obj_contents)} "
            f"/First {len(header)} /Length {len(stream)} >>\n".encode()
        )
        buf.write(b"stream\n")
        buf.write(stream)
        buf.write(b"\nendstream\nendobj\n")
        return offset

    buf = io.BytesIO()
    buf.write(b"%PDF-1.5\n")

    offsets = {}
    offsets[1] = _write_obj(buf, 1, "<< /Type /Catalog /Pages 2 0 R /AcroForm 5 0 R >>")
    offsets[2] = _write_obj(buf, 2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>")
    offsets[3] = _write_obj(
        buf, 3,
        "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Annots [6 0 R] >>",
    )
    # Old object stream: AcroForm (obj 5) + field without /V (obj 6)
    offsets[4] = _write_objstm(buf, 4, [
        (5, b"<< /Fields [6 0 R] >>"),
        (6, b"<< /Type /Annot /Subtype /Widget /FT /Tx /T (amount) >>"),
    ])
    # New object stream: field with /V (obj 6, updated)
    offsets[7] = _write_objstm(buf, 7, [
        (6, b"<< /Type /Annot /Subtype /Widget /FT /Tx /T (amount) /V (42) >>"),
    ])

    # Cross-reference stream
    xref_offset = buf.tell()
    raw_entries = [
        (0, 0, 65535),          # obj 0: free
        (1, offsets[1], 0),     # obj 1: catalog
        (1, offsets[2], 0),     # obj 2: pages
        (1, offsets[3], 0),     # obj 3: page
        (1, offsets[4], 0),     # obj 4: old objstm
        (2, 4, 0),              # obj 5: in objstm 4, index 0
        (2, 7, 0),              # obj 6: in objstm 7, index 0 (authoritative)
        (1, offsets[7], 0),     # obj 7: new objstm
        (1, xref_offset, 0),    # obj 8: this xref stream
    ]
    stream_data = bytearray()
    for typ, f1, f2 in raw_entries:
        stream_data.append(typ)
        stream_data.extend(f1.to_bytes(4, "big"))
        stream_data.extend(f2.to_bytes(2, "big"))
    buf.write(
        f"8 0 obj\n<< /Type /XRef /Size 9 /W [1 4 2] "
        f"/Root 1 0 R /Length {len(stream_data)} /Index [0 9] >>".encode()
    )
    buf.write(b"\nstream\n")
    buf.write(bytes(stream_data))
    buf.write(b"\nendstream\nendobj\n")
    buf.write(f"startxref\n{xref_offset}\n%%EOF\n".encode())

    reader = PdfReader(BytesIO(buf.getvalue()))

    # Resolve AcroForm - this decompresses stream 4, which contains
    # the stale copy of obj 6 (without /V).
    acroform = reader.trailer["/Root"].get_object()["/AcroForm"].get_object()
    assert "/Fields" in acroform

    # obj 6 must reflect the authoritative version from stream 7.
    field = reader.get_object(6)
    assert field["/V"] == "42"


def test_xref_table_with_comments_before_trailer():
    """Comments between xref entries and trailer are legal per PDF spec §7.2.3.

    Some PDF producers (e.g. Vectorizer.AI) insert human-readable comments
    between the last xref entry and the ``trailer`` keyword.  pypdf must skip
    these instead of crashing with ``PdfReadError: Could not read Boolean
    object``.
    """
    pdf_data = (
        b"%%PDF-1.4\n"
        b"1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n"
        b"2 0 obj\n<< /Type /Pages /Count 1 /Kids [3 0 R] >>\nendobj\n"
        b"3 0 obj\n<< /Type /Page /MediaBox [0 0 100 100] /Parent 2 0 R >>\nendobj\n"
        b"xref\n"
        b"0 4\n"
        b"0000000000 65535 f \n"
        b"%010d 00000 n \n"
        b"%010d 00000 n \n"
        b"%010d 00000 n \n"
        b"%% This is a legal PDF comment\n"
        b"%% And another one\n"
        b"trailer\n<< /Size 4 /Root 1 0 R >>\n"
        b"startxref\n%d\n"
        b"%%%%EOF\n"
    )
    pdf_data = pdf_data % (
        pdf_data.find(b"1 0 obj") - 1,
        pdf_data.find(b"2 0 obj") - 1,
        pdf_data.find(b"3 0 obj") - 1,
        pdf_data.find(b"xref") - 1,
    )
    reader = PdfReader(BytesIO(pdf_data))
    assert len(reader.pages) == 1


@pytest.mark.timeout(10)
def test_read_pdf15_xref_stream__w_0_0_0(caplog):
    pdf = b"%PDF-1.7\n"
    pdf += b"1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n"
    pdf += b"2 0 obj\n<< /Type /Pages /Kids [] /Count 0 >>\nendobj\n"
    startxref = len(pdf)
    encoded = FlateDecode.encode(b"")
    pdf += (
        f"3 0 obj\n<< /Type /XRef /Size 50000000 /W [0 0 0] /Root 1 0 R /Filter /FlateDecode /Length {len(encoded)} >>"
        f"\nstream\n"
    ).encode()
    pdf += encoded + b"\nendstream\nendobj\n"
    pdf += f"startxref\n{startxref}\n%%EOF\n".encode()

    with pytest.raises(
            PdfReadError,
            match=r"^Trailer cannot be read: Cross\-reference stream encodes no entry data\.$"
    ):
        _ = PdfReader(BytesIO(pdf), strict=True)
    assert caplog.messages == []

    _ = PdfReader(BytesIO(pdf), strict=False)
    assert caplog.messages == [
        "Cross-reference stream encodes no entry data.",
    ]


@pytest.mark.timeout(10)
def test_read_pdf15_xref_stream__size_limit(caplog):
    pdf = b"%PDF-1.7\n"
    pdf += b"1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n"
    pdf += b"2 0 obj\n<< /Type /Pages /Kids [] /Count 0 >>\nendobj\n"
    startxref = len(pdf)
    encoded = FlateDecode.encode(b"")
    pdf += (
        f"3 0 obj\n<< /Type /XRef /Size 50000000 /W [1 0 0] /Root 1 0 R /Filter /FlateDecode /Length {len(encoded)} >>"
        f"\nstream\n"
    ).encode()
    pdf += encoded + b"\nendstream\nendobj\n"
    pdf += f"startxref\n{startxref}\n%%EOF\n".encode()

    with pytest.raises(
            PdfReadError,
            match=r"^Trailer cannot be read: Total XRef entries 50000000 exceed maximum allowed value 1\.$"
    ):
        _ = PdfReader(BytesIO(pdf), strict=True)
    assert caplog.messages == []

    _ = PdfReader(BytesIO(pdf), strict=False)
    assert caplog.messages == [
        "Clamping XRef count from 50000000 to 1 to fit stream size.",
    ]


@pytest.mark.timeout(10)
def test_get_object_from_stream__size_limit(caplog):
    obj_stm_encoded = FlateDecode.encode(b"4 0\nnull")
    pdf = b"%PDF-1.7\n"
    header_length = len(pdf)
    pdf += b"1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n"
    catalog_length = len(pdf)
    pdf += b"2 0 obj\n<< /Type /Pages /Kids [4 0 R] /Count 1 >>\nendobj\n"
    pages_length = len(pdf)
    pdf += (
        f"3 0 obj\n<< /Type /ObjStm /N 5000000 /First 4 /Filter /FlateDecode /Length {len(obj_stm_encoded)} >>\n"
        f"stream\n"
    ).encode()
    pdf += obj_stm_encoded + b"\nendstream\nendobj\n"
    xref = bytearray()
    for xref_type, value, generation in [
            (0, 0, 0),
            (1, header_length, 0),
            (1, catalog_length, 0),
            (1, pages_length, 0),
            (2, 3, 0)
    ]:
        # xref type: 0 = free object, 1 = in-use object (points to byte offset), 2 = compressed (stored in stream)
        # value: type 1 = byte offset in the file, type 2 = object stream number
        # >B = 1 byte (unsigned char) for type and generation
        # >H = 2 bytes (unsigned short) for value
        xref += struct.pack(">B", xref_type) + struct.pack(">H", value) + struct.pack(">B", generation)
    xref_encoded = FlateDecode.encode(bytes(xref))
    startxref = len(pdf)
    pdf += (
        f"5 0 obj\n<< /Type /XRef /Size 6 /W [1 2 1] /Root 1 0 R /Filter /FlateDecode "
        f"/Length {len(xref_encoded)} >>\nstream\n"
    ).encode()
    pdf += xref_encoded + b"\nendstream\nendobj\n"
    pdf += f"startxref\n{startxref}\n%%EOF\n".encode()

    with pytest.raises(LimitReachedError, match=r"^Value /N 5000000 for object 3 exceeds maximum allowed value 2\.$"):
        reader = PdfReader(BytesIO(pdf), strict=True)
        _ = reader.pages[0]
    assert caplog.messages == []

    with pytest.raises(PdfReadError, match=r"cyclic page references|Maximum recursion depth"):
        reader = PdfReader(BytesIO(pdf), strict=False)
        _ = reader.pages[0]
    assert caplog.messages == [
        "Value /N 5000000 for object 3 exceeds maximum allowed value 2. Limiting to 2.",
        "NumberObject(b'') invalid; use 0 instead",
        "NumberObject(b'') invalid; use 0 instead",
    ]


def test_named_destinations_cache():
    reader = PdfReader(RESOURCE_ROOT / "crazyones.pdf")

    with mock.patch("pypdf._doc_common.PdfDocCommon._get_named_destinations") as get_mock:
        for _ix in range(20):
            reader.named_destinations.get("foo")

        get_mock.assert_called_once()


@pytest.mark.timeout(10)
def test_read_standard_xref_table__malformed__speed():
    body = (
        b"%PDF-1.7\n"
        b"1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n"
        b"2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n"
        b"3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >>\nendobj\n"
    )
    xref_offset = len(body)
    corrupt_entry = b"xxxxxxxxxxxxxxxx 0\r\n"
    assert len(corrupt_entry) == 20
    entry_count = 20_000
    data = (
        body
        + f"xref\n1 {entry_count}\n".encode("ascii")
        + corrupt_entry * entry_count
        + b"trailer\n<< /Size "
        + str(entry_count + 1).encode("ascii")
        + b" /Root 1 0 R >>\nstartxref\n"
        + str(xref_offset).encode("ascii")
        + b"\n%%EOF\n"
    )

    reader = PdfReader(BytesIO(data))
    assert len(reader.pages) == 1


def test_load_recovery_cache():
    reader = PdfReader(RESOURCE_ROOT / "crazyones.pdf")

    pdf_objects = [
        (1, 0, 10),
        (1, 1, 20),
        (2, 0, 30),
        (3, 1, 42),
    ]
    with mock.patch.object(reader, "_find_pdf_objects", return_value=iter(pdf_objects)):
        assert reader._load_recovery_cache(b"") == {1: (10, 0), 2: (30, 0), 3: (42, 1)}
