diff --git a/codex/codex_patcher.py b/codex/codex_patcher.py index dd93071..80f68dd 100755 --- a/codex/codex_patcher.py +++ b/codex/codex_patcher.py @@ -221,25 +221,56 @@ def read_toml_raw(path): return f.read() +def _toml_str(s): + """Escape a Python string as a TOML basic-string literal. + + Backslashes (Windows paths!) and double-quotes must be escaped per + TOML spec — otherwise the parser sees `C:\\Windows` as `C:Windows` + or fails with `Unescaped '\\' in a string`. + """ + escaped = s.replace("\\", "\\\\").replace('"', '\\"') + return f'"{escaped}"' + + def toml_value(v): - """Format a Python value as TOML.""" + """Format a Python value as TOML. + + Dicts → inline table `{ key = "val", k2 = "v2" }` (NOT Python str(dict) + which uses single quotes + colons and breaks `tomllib.load`). + Strings → basic-string with backslash/quote escapes (Windows paths). + """ if isinstance(v, bool): return "true" if v else "false" if isinstance(v, str): - return f'"{v}"' + return _toml_str(v) if isinstance(v, (int, float)): return str(v) if isinstance(v, list): items = ", ".join(toml_value(i) for i in v) return f"[{items}]" - return str(v) + if isinstance(v, dict): + # TOML inline table: keys may need quoting (special chars / dots), + # values recurse through toml_value. + items = ", ".join( + f"{toml_key(k)} = {toml_value(val)}" for k, val in v.items() + ) + return f"{{ {items} }}" if items else "{}" + return _toml_str(str(v)) def toml_key(k): - """Format a TOML key, quoting if it contains dots or special chars.""" - if "." in k or " " in k: - return f'"{k}"' - return k + """Format a TOML key, quoting if it contains anything other than the + bare-key charset (A-Z a-z 0-9 _ -). Examples that MUST be quoted: + "PROGRAMFILES(X86)" — parens + "key.with.dots" — would parse as nested + "key with spaces" + """ + import re as _re + if _re.fullmatch(r"[A-Za-z0-9_-]+", k): + return k + # Quote, escape backslashes and quotes + escaped = k.replace("\\", "\\\\").replace('"', '\\"') + return f'"{escaped}"' def generate_config_toml(existing, config, home_dir=None):