mirror of
https://github.com/github/spec-kit.git
synced 2026-07-10 09:44:41 +08:00
fix(toml): escape control characters so generated command files parse (#3341)
* fix(toml): escape control characters so generated command files parse both toml renderers (TomlIntegration._render_toml_string for gemini/tabnine and CommandRegistrar.render_toml_command for extension/preset commands) wrote control characters raw into a multiline or basic string. toml forbids literal control chars (U+0000-U+001F except tab/newline, and U+007F) in every string form, and a bare CR that is not part of a CRLF pair, so a description or body containing one produced a .toml file that fails to parse. route any value with such a character to a fully-escaped basic string that emits the leftover control chars as \uXXXX. added regression tests that round-trip through tomllib. * refactor(toml): centralize control-char escaping in one shared helper the control-char detection and basic-string escaping added for both toml renderers were copy-pasted into agents.py and integrations/base.py. move the two functions into specify_cli/_toml_string.py and have both renderers delegate to it, so the escaping rules can't drift apart later. no behavior change; both renderers now reference the same implementation.
This commit is contained in:
60
src/specify_cli/_toml_string.py
Normal file
60
src/specify_cli/_toml_string.py
Normal file
@@ -0,0 +1,60 @@
|
||||
"""Shared TOML string-escaping helpers.
|
||||
|
||||
Both TOML command renderers — ``TomlIntegration`` (gemini, tabnine) in
|
||||
``specify_cli.integrations.base`` and ``CommandRegistrar.render_toml_command``
|
||||
(extension/preset commands) in ``specify_cli.agents`` — need the same rules for
|
||||
detecting characters TOML forbids literally and for emitting a fully-escaped
|
||||
basic string. Keeping one implementation here avoids the two drifting apart if
|
||||
the escaping rules change again.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
def has_illegal_toml_control(value: str) -> bool:
|
||||
"""True when *value* contains a character TOML forbids literally.
|
||||
|
||||
TOML basic/literal strings (single- or multi-line) allow tab and, in the
|
||||
multiline forms, newlines — but every other control character
|
||||
(``U+0000``–``U+001F`` and ``U+007F``) must be ``\\u``-escaped, which only a
|
||||
basic string can do. A bare carriage return counts too: a multiline basic
|
||||
string treats ``\\r`` as a newline only when paired into ``\\r\\n``; a lone
|
||||
``\\r`` is an illegal control character.
|
||||
"""
|
||||
length = len(value)
|
||||
for i, ch in enumerate(value):
|
||||
code = ord(ch)
|
||||
if ch == "\r":
|
||||
# Only a CR that is part of a CRLF newline is allowed literally.
|
||||
if i + 1 < length and value[i + 1] == "\n":
|
||||
continue
|
||||
return True
|
||||
if (code < 0x20 and ch not in ("\t", "\n")) or code == 0x7F:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def escape_toml_basic(value: str) -> str:
|
||||
"""Render *value* as a single-line basic string, escaping everything.
|
||||
|
||||
Always valid TOML: backslash/quote are escaped, the common control chars
|
||||
use their short escapes, and any remaining control character is emitted as
|
||||
a ``\\uXXXX`` sequence.
|
||||
"""
|
||||
out: list[str] = []
|
||||
for ch in value:
|
||||
code = ord(ch)
|
||||
if ch == "\\":
|
||||
out.append("\\\\")
|
||||
elif ch == '"':
|
||||
out.append('\\"')
|
||||
elif ch == "\n":
|
||||
out.append("\\n")
|
||||
elif ch == "\r":
|
||||
out.append("\\r")
|
||||
elif ch == "\t":
|
||||
out.append("\\t")
|
||||
elif code < 0x20 or code == 0x7F:
|
||||
out.append(f"\\u{code:04x}")
|
||||
else:
|
||||
out.append(ch)
|
||||
return '"' + "".join(out) + '"'
|
||||
Reference in New Issue
Block a user