fix(task-9): DIMDSEP refuses control characters and code points above the 16-bit DXF group

check_dim_value's char branch only checked len == 1, so '\n', '\t', '\x00'
and non-BMP characters passed and the headless writer stored ord(value)
unguarded: '\n' split the dimension text over two lines, NUL read back as a
comma, and U+1F600 reached $DIMDSEP / group 278 as 128512, outside the 16-bit
range and refused by AutoCAD as "not a single character". ezdxf's audit
caught none of it. The same hole 827719f closed for names and fonts, left
open for the one variable stored as a character code.

The gate now refuses code < 32, DEL, and code > DIMDSEP_MAX_CODE (0x7FFF,
the signed short DWG stores) with a ValueError naming DIMDSEP, so nothing
reaches a backend. Pinned by a parametrized refusal test (newline, tab, NUL,
DEL, U+1F600, U+8000, U+FFFF), a positive test that the middle dot, the
Arabic decimal separator and U+7FFF still pass, and the ezdxf refusal test
now proves dimstyle_create / dimstyle_modify write nothing for these.

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-17 14:52:57 +03:00
co-authored by Claude Opus 5
parent 2ee18727cd
commit 4d246b17b8
3 changed files with 63 additions and 3 deletions
+26 -3
View File
@@ -12,9 +12,13 @@ Every value in ``PRESETS`` is pinned by name in
lettering, .125 in arrowheads) in metric values: 3 mm text and arrowheads,
text centred in a gap and horizontal, decimal point.
``DIMDSEP`` is a one-character *string* here. DXF stores the character code and
ezdxf raises on a string in the header, so the translation belongs to the
backends, not to the data.
``DIMDSEP`` is a one-character *string* here. DXF stores the character code
(group 278, a 16-bit value) and ezdxf raises on a string in the header, so the
translation belongs to the backends, not to the data — but the data is where
the refusal starts: a control character (``\\n`` splits the dimension text over
two lines, NUL reads back as a comma) or a code point above ``0x7FFF``
(``ezdxf`` writes it, AutoCAD refuses it as "not a single character") is
refused here, before ``ord()`` runs in a backend.
Pure data plus validation: no I/O, no ezdxf, no COM, so the backends, the
tools and the provenance tests all read one copy.
@@ -270,6 +274,15 @@ def ezdxf_arrowhead(canonical: str) -> str:
return canonical
DIMDSEP_MAX_CODE = 0x7FFF
"""The largest character code ``DIMDSEP`` accepts.
DXF group 278 is a 16-bit integer and the DWG field a signed short; AutoCAD
refuses anything above it as "not a single character", while ezdxf would
write it verbatim (measured: ``'\\U0001F600'`` reached the file as 128512).
"""
def _number(key: str, value: Any) -> float:
if isinstance(value, bool) or not isinstance(value, (int, float)):
raise TypeError(f"{key}: expected a number, got {type(value).__name__}")
@@ -324,6 +337,16 @@ def check_dim_value(name: str, value: Any) -> Any:
raise TypeError(f"{key}: expected a string, got {type(value).__name__}")
if len(value) != 1: # "char"
raise ValueError(f"{key}: {value!r} must be exactly one character")
code = ord(value)
if code < 32 or code == 127:
raise ValueError(
f"{key}: {value!r} is a control character, which a DXF cannot hold as a separator"
)
if code > DIMDSEP_MAX_CODE:
raise ValueError(
f"{key}: {value!r} (U+{code:04X}) is outside the 16-bit character code DXF group "
f"278 stores (at most U+{DIMDSEP_MAX_CODE:04X})"
)
return value
+29
View File
@@ -200,6 +200,35 @@ def test_unknown_preset_is_refused():
resolve_dimstyle("iso-25", [("DIMTXT", 2.5)])
@pytest.mark.parametrize(
"separator",
["\n", "\t", "\x00", "\x7f", chr(0x1F600), chr(0x8000), chr(0xFFFF)],
ids=["newline", "tab", "nul", "del", "non-bmp", "first-above-signed-short", "u-ffff"],
)
def test_dimdsep_refuses_what_a_dxf_character_code_cannot_hold(separator):
"""DIMDSEP is the one variable stored as a character code (group 278, a
16-bit value); ``len == 1`` alone let these through, and the headless
engine then wrote ``ord()`` unguarded — measured: ``'\\n'`` split the
dimension text over two lines, NUL read back as a comma, and the emoji
reached the file as 128512, which AutoCAD refuses as "not a single
character". ezdxf's audit reports none of it."""
with pytest.raises(ValueError, match="DIMDSEP"):
check_dim_value("DIMDSEP", separator)
with pytest.raises(ValueError, match="DIMDSEP"):
resolve_dimstyle("iso-25", {"DIMDSEP": separator})
@pytest.mark.parametrize(
"separator",
[",", ".", " ", chr(0xB7), chr(0x66B), chr(0x7FFF)],
ids=["comma", "point", "space", "middle-dot", "arabic-decimal", "last-signed-short"],
)
def test_dimdsep_takes_any_printable_separator_a_dxf_can_hold(separator):
"""The comma, the point, the middle dot, the Arabic decimal separator, and
the last code a signed short holds all pass; the guard is not an ASCII gate."""
assert check_dim_value("DIMDSEP", separator) == separator
def test_validate_overrides_alone_is_what_modify_uses():
assert validate_overrides(None) == {}
assert validate_overrides({"dimgap": -1}) == {"DIMGAP": -1.0}, "negative gap boxes the text"
+8
View File
@@ -134,6 +134,14 @@ async def test_refusals_happen_before_anything_is_written(backend):
with pytest.raises(ValueError, match="does not exist"):
await backend.dimstyle_set_current("NOPE")
assert backend._doc.dimstyles.get("ISO-25").dxf.dimdec == 2
# DIMDSEP is stored as ``ord(value)``: a control character or a code point
# above the 16-bit group 278 must stop here, not reach the table.
with pytest.raises(ValueError, match="DIMDSEP"):
await backend.dimstyle_create("Z", {"DIMDSEP": "\n"})
assert not backend._doc.dimstyles.has_entry("Z")
with pytest.raises(ValueError, match="DIMDSEP"):
await backend.dimstyle_modify("ISO-25", {"DIMDSEP": "\U0001f600"})
assert backend._doc.dimstyles.get("ISO-25").dxf.dimdsep == 44
async def test_builtin_arrowheads_render_save_and_report_one_spelling(backend, tmp_path):