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:
co-authored by
Claude Opus 5
parent
2ee18727cd
commit
4d246b17b8
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user