mirror of
https://github.com/github/spec-kit.git
synced 2026-08-03 06:26:30 +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:
@@ -16,6 +16,8 @@ from typing import Any, Dict, List, Optional
|
||||
import yaml
|
||||
|
||||
from ._init_options import is_ai_skills_enabled, load_init_options
|
||||
from ._toml_string import escape_toml_basic as _escape_toml_basic
|
||||
from ._toml_string import has_illegal_toml_control as _has_illegal_toml_control
|
||||
from ._utils import relative_extension_path_violation
|
||||
|
||||
|
||||
@@ -258,7 +260,12 @@ class CommandRegistrar:
|
||||
# ``C:\\Users\\...`` whose ``\\U`` reads as an invalid unicode escape) would
|
||||
# produce unparseable TOML — route those to the *literal* form ('''...'''),
|
||||
# which does not process escapes, or to the escaped basic string.
|
||||
if '"""' not in body and "\\" not in body:
|
||||
# Control characters (U+0000–U+001F except tab/newline, U+007F) and a bare
|
||||
# CR are illegal in every TOML string form, so a body containing them must
|
||||
# go to the escaped basic string regardless of which delimiters it uses.
|
||||
if self._has_illegal_toml_control(body):
|
||||
toml_lines.append(f"prompt = {self._render_basic_toml_string(body)}")
|
||||
elif '"""' not in body and "\\" not in body:
|
||||
toml_lines.append('prompt = """')
|
||||
toml_lines.append(body)
|
||||
toml_lines.append('"""')
|
||||
@@ -271,17 +278,11 @@ class CommandRegistrar:
|
||||
|
||||
return "\n".join(toml_lines)
|
||||
|
||||
@staticmethod
|
||||
def _render_basic_toml_string(value: str) -> str:
|
||||
"""Render *value* as a TOML basic string literal."""
|
||||
escaped = (
|
||||
value.replace("\\", "\\\\")
|
||||
.replace('"', '\\"')
|
||||
.replace("\n", "\\n")
|
||||
.replace("\r", "\\r")
|
||||
.replace("\t", "\\t")
|
||||
)
|
||||
return f'"{escaped}"'
|
||||
# Control-char detection and basic-string escaping are shared with the
|
||||
# gemini/tabnine renderer in ``specify_cli.integrations.base`` via
|
||||
# ``specify_cli._toml_string`` so the two never drift apart.
|
||||
_has_illegal_toml_control = staticmethod(_has_illegal_toml_control)
|
||||
_render_basic_toml_string = staticmethod(_escape_toml_basic)
|
||||
|
||||
def render_yaml_command(
|
||||
self,
|
||||
|
||||
Reference in New Issue
Block a user