This commit is contained in:
1 parent
4db3f45164
commit
d25c04cdd2
175 files changed
+56243
-15
No files matched your search
@@ -0,0 +1,113 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from gewu.adapters.pdf_extractor import PdfTextExtractor
|
||||
|
||||
|
||||
def _pdf(pages: tuple[str, ...]) -> bytes:
|
||||
objects: list[bytes] = []
|
||||
page_ids = [4 + 2 * index for index in range(len(pages))]
|
||||
objects.extend(
|
||||
[
|
||||
b"<< /Type /Catalog /Pages 2 0 R >>",
|
||||
(
|
||||
f"<< /Type /Pages /Kids [{' '.join(f'{page_id} 0 R' for page_id in page_ids)}] "
|
||||
f"/Count {len(pages)} >>"
|
||||
).encode(),
|
||||
b"<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>",
|
||||
]
|
||||
)
|
||||
for page_id, text in zip(page_ids, pages, strict=True):
|
||||
content_id = page_id + 1
|
||||
escaped = text.replace("\\", "\\\\").replace("(", "\\(").replace(")", "\\)")
|
||||
stream = f"BT /F1 12 Tf 72 720 Td ({escaped}) Tj ET".encode()
|
||||
page = (
|
||||
f"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] "
|
||||
f"/Resources << /Font << /F1 3 0 R >> >> /Contents {content_id} 0 R >>"
|
||||
)
|
||||
objects.append(page.encode())
|
||||
objects.append(
|
||||
b"<< /Length " + str(len(stream)).encode() + b" >>\nstream\n" + stream + b"\nendstream"
|
||||
)
|
||||
output = bytearray(b"%PDF-1.4\n")
|
||||
offsets = [0]
|
||||
for number, obj in enumerate(objects, 1):
|
||||
offsets.append(len(output))
|
||||
output.extend(f"{number} 0 obj\n".encode() + obj + b"\nendobj\n")
|
||||
xref = len(output)
|
||||
output.extend(f"xref\n0 {len(offsets)}\n0000000000 65535 f \n".encode())
|
||||
for offset in offsets[1:]:
|
||||
output.extend(f"{offset:010d} 00000 n \n".encode())
|
||||
output.extend(
|
||||
f"trailer\n<< /Size {len(offsets)} /Root 1 0 R >>\nstartxref\n{xref}\n%%EOF\n".encode()
|
||||
)
|
||||
return bytes(output)
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not all(shutil.which(item) for item in ("bwrap", "pdftotext", "prlimit")),
|
||||
reason="Linux PDF sandbox tools unavailable",
|
||||
)
|
||||
def test_pdf_extraction_preserves_page_locations_and_parser_identity() -> None:
|
||||
record = PdfTextExtractor().extract(
|
||||
"project:source:r1",
|
||||
_pdf(("First page evidence.", "Second page evidence.")),
|
||||
"application/pdf",
|
||||
)
|
||||
assert record.status == "complete"
|
||||
assert record.parser_name == "gewu-poppler-pdftotext"
|
||||
assert [block.page for block in record.blocks] == [1, 2]
|
||||
assert [block.original_text.strip() for block in record.blocks] == [
|
||||
"First page evidence.",
|
||||
"Second page evidence.",
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not all(shutil.which(item) for item in ("bwrap", "pdftotext", "prlimit")),
|
||||
reason="Linux PDF sandbox tools unavailable",
|
||||
)
|
||||
def test_empty_pdf_page_is_reported_as_partial() -> None:
|
||||
record = PdfTextExtractor().extract(
|
||||
"project:source:r1", _pdf(("Text page.", "")), "application/pdf"
|
||||
)
|
||||
assert record.status == "partial"
|
||||
assert record.blocks[0].page == 1
|
||||
assert record.warnings
|
||||
|
||||
|
||||
def test_pdf_parser_rejects_bad_signature_without_launching_parser() -> None:
|
||||
with pytest.raises(ValueError, match="signature"):
|
||||
PdfTextExtractor().extract("project:source:r1", b"not a pdf", "application/pdf")
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not shutil.which("bwrap") or not shutil.which("prlimit"), reason="bubblewrap unavailable"
|
||||
)
|
||||
def test_sandbox_cannot_read_host_temporary_file(tmp_path: Path) -> None:
|
||||
sentinel = tmp_path / "gewu-host-secret-fixture"
|
||||
sentinel.write_text("must-not-be-visible", encoding="utf-8")
|
||||
extractor = PdfTextExtractor()
|
||||
if not extractor._pdftotext:
|
||||
pytest.skip("pdftotext unavailable")
|
||||
command = extractor._command()
|
||||
command[-6:] = ["/usr/bin/cat", str(sentinel)]
|
||||
with tempfile.TemporaryFile() as output, tempfile.TemporaryFile() as errors:
|
||||
result = subprocess.run(
|
||||
command,
|
||||
stdin=subprocess.DEVNULL,
|
||||
stdout=output,
|
||||
stderr=errors,
|
||||
env={},
|
||||
timeout=10,
|
||||
check=False,
|
||||
)
|
||||
assert result.returncode != 0
|
||||
output.seek(0)
|
||||
assert b"must-not-be-visible" not in output.read()
|
||||
Reference in new issue
Block a user