fix(task-4): refuse LF/CR in XDATA strings before any write

encode_values() accepted strings containing "\n" or "\r". ezdxf writes
1000-tag values verbatim, so the separator split the tag pair: the saved
DXF failed to reopen (DXFStructureError) and, because every undo and
transaction snapshot is also a DXF save, transaction_rollback broke after
a single such write. Both engines route through encode_values, so the
refusal (ValueError naming values[i]) now happens there, before either
backend touches the document. Only LF and CR split a DXF tag; tabs and
the other Unicode separators round-trip and stay allowed.

Tests: pure encoder refusal for \n, \r, \r\n; other control characters
still typed; ezdxf refuses-then-rollback-and-reopen round trip; COM fake
sees no SetXData / RegisteredApplications call.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
This commit is contained in:
Umut CAN
2026-09-15 14:33:00 +03:00
co-authored by Claude Opus 5
parent cda8b69dba
commit 41d6d3505a
2 changed files with 61 additions and 1 deletions
+11 -1
View File
@@ -3,7 +3,11 @@
DXF XDATA is a list of (group code, value) pairs under a registered APPID.
This module is the one place that decides how a JSON value becomes a group
code and enforces the two limits AutoCAD enforces silently: 255 characters
per string and 16 KB per entity.
per string and 16 KB per entity. It also refuses the one thing neither engine
can store: a line separator inside a string. DXF is a text format of
alternating "group code" / "value" lines, and ezdxf writes a 1000 tag verbatim,
so a LF or CR in the value splits the tag pair and the saved file -- and every
undo/transaction snapshot, which is also a DXF save -- becomes unreadable.
"""
from __future__ import annotations
@@ -15,6 +19,7 @@ RESERVED_APPS = {"ACAD"}
MAX_STRING = 255
MAX_BYTES = 16 * 1024
INT_MIN, INT_MAX = -(2**31), 2**31 - 1
LINE_SEPARATORS = ("\n", "\r")
def validate_app_name(app) -> str:
@@ -40,6 +45,11 @@ def encode_values(values) -> list[tuple[int, object]]:
raise ValueError(
f"xdata: {where} is {len(value)} characters; the limit is {MAX_STRING}"
)
if any(sep in value for sep in LINE_SEPARATORS):
raise ValueError(
f"xdata: {where} contains a newline; "
"line separators are not allowed in XDATA strings"
)
tags.append((1000, value))
size += len(value.encode("utf-8")) + 2
elif isinstance(value, int):