mirror of
https://github.com/d9pouces/id_configurator.git
synced 2026-11-09 01:47:21 +01:00
feat: add repository name sanitization to prevent invalid characters in poetry config
Signed-off-by: d9pouces <github@19pouces.net>
This commit is contained in:
@@ -25,6 +25,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
import re
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
from id_configurator.config import PythonRepository
|
from id_configurator.config import PythonRepository
|
||||||
@@ -38,12 +39,25 @@ _PIP_CONF = "etc/pip.conf"
|
|||||||
_UV_TOML = "etc/uv/uv.toml"
|
_UV_TOML = "etc/uv/uv.toml"
|
||||||
_POETRY_CONFIG = "root/.config/pypoetry/config.toml"
|
_POETRY_CONFIG = "root/.config/pypoetry/config.toml"
|
||||||
_DISTUTILS_GLOBS = ("usr/lib/python3.*", "usr/local/lib/python3.*")
|
_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:
|
def _toml_string(value: str) -> str:
|
||||||
return json.dumps(value)
|
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:
|
def _trusted_host(repository: PythonRepository) -> str | None:
|
||||||
parsed = urlparse(repository.index_url)
|
parsed = urlparse(repository.index_url)
|
||||||
return None if parsed.scheme.lower() == "https" else parsed.hostname
|
return None if parsed.scheme.lower() == "https" else parsed.hostname
|
||||||
@@ -83,10 +97,11 @@ def _write_poetry_config(
|
|||||||
) -> None:
|
) -> None:
|
||||||
lines = []
|
lines = []
|
||||||
for repository in repositories:
|
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(f"url = {_toml_string(repository.index_url)}")
|
||||||
lines.append("")
|
lines.append("")
|
||||||
lines.append(f"[certificates.{repository.name}]")
|
lines.append(f"[certificates.{name}]")
|
||||||
lines.append(f"cert = {_toml_string(_CA_BUNDLE)}")
|
lines.append(f"cert = {_toml_string(_CA_BUNDLE)}")
|
||||||
lines.append("")
|
lines.append("")
|
||||||
content = ("\n".join(lines).rstrip() + "\n").encode("utf-8")
|
content = ("\n".join(lines).rstrip() + "\n").encode("utf-8")
|
||||||
|
|||||||
+1
-1
@@ -44,7 +44,7 @@ maintainers = [
|
|||||||
name = "id-configurator"
|
name = "id-configurator"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.13"
|
requires-python = ">=3.13"
|
||||||
version = "2026.10.5"
|
version = "2026.10.6"
|
||||||
|
|
||||||
[project.scripts]
|
[project.scripts]
|
||||||
id-configurator = "id_configurator:main"
|
id-configurator = "id_configurator:main"
|
||||||
|
|||||||
@@ -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:
|
def test_apply_writes_poetry_config_for_each_configured_user(tmp_path) -> None:
|
||||||
config = Configuration.model_validate(ONE_REPO_CONFIG)
|
config = Configuration.model_validate(ONE_REPO_CONFIG)
|
||||||
dst = tmp_path / "dst"
|
dst = tmp_path / "dst"
|
||||||
|
|||||||
Reference in New Issue
Block a user