File size: 7,239 Bytes
b729ea0 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 | """Tests for the binary-document write guard (port of nearai/ironclaw#7109).
A plain-text write can never produce a valid OOXML/OLE/ODF container, so
write_file/patch must refuse to write text into .docx/.xlsx/.pptx (and
friends), and must refuse to OVERWRITE an existing .pdf — while still
allowing new-.pdf creation (raw PDF syntax is text-authorable).
"""
import json
import zipfile
from pathlib import Path
from tools.binary_extensions import (
has_opaque_document_extension,
is_pdf_path,
)
from tools.file_tools import patch_tool, write_file_tool
from tools.file_tools_write_guards import _check_binary_document_write
def _make_minimal_docx(path: Path) -> None:
with zipfile.ZipFile(path, "w") as z:
z.writestr(
"[Content_Types].xml",
'<?xml version="1.0"?><Types xmlns="http://schemas.openxmlformats.org/'
'package/2006/content-types"><Default Extension="xml" '
'ContentType="application/xml"/></Types>',
)
z.writestr(
"word/document.xml",
'<?xml version="1.0"?><w:document xmlns:w="http://schemas.'
'openxmlformats.org/wordprocessingml/2006/main"><w:body><w:p><w:r>'
"<w:t>Quarterly numbers look good.</w:t></w:r></w:p></w:body>"
"</w:document>",
)
class TestExtensionHelpers:
def test_opaque_document_extensions(self):
for p in ("a.docx", "b.XLSX", "c.pptx", "d.doc", "e.odt", "f.ods", "g.odp",
"h.docm", "i.xlsm", "j.xlsb", "k.pptm", "l.ppsx", "m.ppsm",
"n.pps", "o.pot", "p.rtf", "q.epub"):
assert has_opaque_document_extension(p) is True, f"{p} should be opaque"
def test_non_opaque_paths(self):
for p in ("a.txt", "b.py", "c.pdf", "d.md", "noext", "e.csv"):
assert has_opaque_document_extension(p) is False
def test_is_pdf_path(self):
assert is_pdf_path("report.pdf") is True
assert is_pdf_path("report.PDF") is True
assert is_pdf_path("report.txt") is False
class TestCheckBinaryDocumentWrite:
def test_docx_always_rejected(self, tmp_path: Path):
# Even a NON-existing docx is rejected — text can't be a valid container.
err = _check_binary_document_write(str(tmp_path / "new.docx"))
assert err is not None
assert ".docx" in err
def test_existing_pdf_rejected(self, tmp_path: Path):
pdf = tmp_path / "doc.pdf"
pdf.write_bytes(b"%PDF-1.4\n%%EOF\n")
err = _check_binary_document_write(str(pdf))
assert err is not None
assert "overwrite" in err.lower()
def test_new_pdf_allowed(self, tmp_path: Path):
assert _check_binary_document_write(str(tmp_path / "fresh.pdf")) is None
def test_plain_text_allowed(self, tmp_path: Path):
assert _check_binary_document_write(str(tmp_path / "notes.txt")) is None
class TestWriteFileToolGuard:
def test_write_file_rejects_existing_docx(self, tmp_path: Path):
docx = tmp_path / "report.docx"
_make_minimal_docx(docx)
original = docx.read_bytes()
result = json.loads(write_file_tool(str(docx), "edited text"))
assert result.get("error"), "text write into .docx must be refused"
assert docx.read_bytes() == original, "document bytes must be untouched"
assert zipfile.is_zipfile(docx), "document must remain a valid container"
def test_write_file_rejects_docm(self, tmp_path: Path):
"""Regression: .docm is extractable by read_file (anydoc) but was
missing from OPAQUE_DOCUMENT_EXTENSIONS in the original PR #82818.
Flagged by @egilewski — proven live: text write corrupted the zip."""
docm = tmp_path / "macro.docm"
_make_minimal_docx(docm) # same OOXML zip structure
original = docm.read_bytes()
result = json.loads(write_file_tool(str(docm), "edited text"))
assert result.get("error"), "text write into .docm must be refused"
assert docm.read_bytes() == original, "document bytes must be untouched"
assert zipfile.is_zipfile(docm), "document must remain a valid container"
def test_write_file_rejects_new_docx(self, tmp_path: Path):
result = json.loads(write_file_tool(str(tmp_path / "new.docx"), "hello"))
assert result.get("error")
assert not (tmp_path / "new.docx").exists()
def test_write_file_rejects_existing_pdf_overwrite(self, tmp_path: Path):
pdf = tmp_path / "doc.pdf"
pdf.write_bytes(b"%PDF-1.4\n1 0 obj\nendobj\n%%EOF\n")
original = pdf.read_bytes()
result = json.loads(write_file_tool(str(pdf), "replacement text"))
assert result.get("error")
assert pdf.read_bytes() == original
def test_write_file_allows_new_pdf_creation(self, tmp_path: Path):
pdf = tmp_path / "generated.pdf"
result = json.loads(write_file_tool(str(pdf), "%PDF-1.4\n%%EOF\n"))
assert not result.get("error")
assert pdf.exists()
def test_write_file_plain_text_unaffected(self, tmp_path: Path):
target = tmp_path / "notes.txt"
result = json.loads(write_file_tool(str(target), "hello world"))
assert not result.get("error")
assert target.read_text() == "hello world"
class TestPatchToolGuard:
def test_patch_replace_rejects_docx(self, tmp_path: Path):
docx = tmp_path / "report.docx"
_make_minimal_docx(docx)
original = docx.read_bytes()
result = json.loads(
patch_tool(mode="replace", path=str(docx),
old_string="good", new_string="great")
)
assert result.get("error")
assert docx.read_bytes() == original
def test_patch_v4a_update_rejects_docx(self, tmp_path: Path):
docx = tmp_path / "report.docx"
_make_minimal_docx(docx)
original = docx.read_bytes()
v4a = (
"*** Begin Patch\n"
f"*** Update File: {docx}\n"
"@@\n"
"-good\n"
"+great\n"
"*** End Patch"
)
result = json.loads(patch_tool(mode="patch", patch=v4a))
assert result.get("error")
assert docx.read_bytes() == original
def test_patch_v4a_delete_of_docx_not_blocked_by_guard(self, tmp_path: Path):
# Delete doesn't write text content — the binary-document guard must
# not fire for it (delete may still fail/succeed for other reasons).
docx = tmp_path / "old.docx"
_make_minimal_docx(docx)
v4a = (
"*** Begin Patch\n"
f"*** Delete File: {docx}\n"
"*** End Patch"
)
result = json.loads(patch_tool(mode="patch", patch=v4a))
err = result.get("error") or ""
assert "binary document" not in err.lower()
def test_patch_replace_plain_text_unaffected(self, tmp_path: Path):
target = tmp_path / "notes.txt"
target.write_text("hello world")
result = json.loads(
patch_tool(mode="replace", path=str(target),
old_string="world", new_string="there")
)
assert not result.get("error")
assert target.read_text() == "hello there"
|