From 665eb215bdba6eca68327987ca42f374eccb06d2 Mon Sep 17 00:00:00 2001 From: d9pouces Date: Tue, 6 Oct 2026 20:34:04 +0200 Subject: [PATCH] feat: add repository name sanitization to prevent invalid characters in poetry config Signed-off-by: d9pouces --- id_configurator/modules/python.py | 19 +++++++++++++++-- pyproject.toml | 2 +- tests/test_python.py | 35 +++++++++++++++++++++++++++++++ 3 files changed, 53 insertions(+), 3 deletions(-) diff --git a/id_configurator/modules/python.py b/id_configurator/modules/python.py index 4081805..083cb9b 100644 --- a/id_configurator/modules/python.py +++ b/id_configurator/modules/python.py @@ -25,6 +25,7 @@ from __future__ import annotations import json import logging +import re from urllib.parse import urlparse from id_configurator.config import PythonRepository @@ -38,12 +39,25 @@ _PIP_CONF = "etc/pip.conf" _UV_TOML = "etc/uv/uv.toml" _POETRY_CONFIG = "root/.config/pypoetry/config.toml" _DISTUTILS_GLOBS = ("usr/lib/python3.*", "usr/local/lib/python3.*") +_REPOSITORY_NAME_FORBIDDEN_CHARS = re.compile(r"[^A-Za-z0-9_-]") def _toml_string(value: str) -> str: return json.dumps(value) +def _sanitize_repository_name(name: str) -> str: + """Replace every character of `name` that isn't a plain identifier + character with `_`. + + `name` is written verbatim as a TOML table key (`[repositories.name]`, + `[certificates.name]`) in poetry's config: an unrestricted name could + break out of the table header, or (via a literal `.`) silently create + extra, nested tables instead of naming the one table it is meant to. + """ + return _REPOSITORY_NAME_FORBIDDEN_CHARS.sub("_", name) + + def _trusted_host(repository: PythonRepository) -> str | None: parsed = urlparse(repository.index_url) return None if parsed.scheme.lower() == "https" else parsed.hostname @@ -83,10 +97,11 @@ def _write_poetry_config( ) -> None: lines = [] for repository in repositories: - lines.append(f"[repositories.{repository.name}]") + name = _sanitize_repository_name(repository.name) + lines.append(f"[repositories.{name}]") lines.append(f"url = {_toml_string(repository.index_url)}") lines.append("") - lines.append(f"[certificates.{repository.name}]") + lines.append(f"[certificates.{name}]") lines.append(f"cert = {_toml_string(_CA_BUNDLE)}") lines.append("") content = ("\n".join(lines).rstrip() + "\n").encode("utf-8") diff --git a/pyproject.toml b/pyproject.toml index 983685f..47049f6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,7 +44,7 @@ maintainers = [ name = "id-configurator" readme = "README.md" requires-python = ">=3.13" -version = "2026.10.5" +version = "2026.10.6" [project.scripts] id-configurator = "id_configurator:main" diff --git a/tests/test_python.py b/tests/test_python.py index 0747633..c993c83 100644 --- a/tests/test_python.py +++ b/tests/test_python.py @@ -183,6 +183,41 @@ def test_apply_multiple_repositories(tmp_path) -> None: ) +def test_apply_sanitizes_poetry_repository_name_with_special_characters( + tmp_path, +) -> None: + data = { + "uuid": "12345678-1234-5678-1234-567812345678", + "organisation": "InterDiode", + "configurations": { + "python": { + "repositories": [ + { + "name": "weird.name]here[bad", + "index-url": "https://example.invalid/simple", + } + ] + } + }, + } + config = Configuration.model_validate(data) + dst = tmp_path / "dst" + context = Context(config=config, dst=dst, src=None) + + PythonModule().apply(context) + + poetry_config = (dst / "root/.config/pypoetry/config.toml").read_text( + encoding="utf-8" + ) + assert poetry_config == ( + "[repositories.weird_name_here_bad]\n" + 'url = "https://example.invalid/simple"\n' + "\n" + "[certificates.weird_name_here_bad]\n" + 'cert = "/etc/ssl/certs/ca-certificates.crt"\n' + ) + + def test_apply_writes_poetry_config_for_each_configured_user(tmp_path) -> None: config = Configuration.model_validate(ONE_REPO_CONFIG) dst = tmp_path / "dst"