Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion src/specify_cli/commands/init.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,17 @@ def _install_extension_during_init(project_path: Path, ext_spec: str, speckit_ve
manager = ExtensionManager(project_path)

# --- URL ---
parsed = urlparse(ext_spec)
# A malformed authority (e.g. an unterminated IPv6 bracket
# "https://[not-an-ip]/x.zip") makes urlparse raise ValueError. This
# function's contract is to raise a clean ValueError the caller can
# display as a tracker error; without this guard, the raw urllib message
# (e.g. "'not-an-ip' does not appear to be an IPv4 or IPv6 address")
# leaked through instead. Mirrors the guard every other URL-accepting
# extension/preset/workflow entry point already has (#3435 lineage).
try:
parsed = urlparse(ext_spec)
except ValueError as exc:
raise ValueError(f"Malformed extension URL: {ext_spec}") from exc
if parsed.scheme in ("http", "https"):
try:
manifest = install_extension_from_url(
Expand Down
26 changes: 25 additions & 1 deletion tests/test_init_output_markup.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,10 @@
from typer.testing import CliRunner

from specify_cli import app
from specify_cli.commands.init import _shell_quote_arg
from specify_cli.commands.init import (
_install_extension_during_init,
_shell_quote_arg,
)

from tests.conftest import requires_bash

Expand Down Expand Up @@ -174,3 +177,24 @@ def test_shell_quote_arg_is_host_appropriate():
assert quoted == '"my project"'
else:
assert quoted == "'my project'"


def test_install_extension_during_init_reports_malformed_url_cleanly(tmp_path: Path):
"""A malformed extension URL must raise a clean ValueError, not leak the
raw urllib message.

An unterminated/invalid bracketed IPv6 authority (e.g.
"https://[not-an-ip]/x.zip") makes ``urlparse()`` itself raise
``ValueError`` (this became eager in Python 3.14; it was previously lazy,
raised only on ``.hostname`` access). ``_install_extension_during_init``
parsed the spec unguarded, so `specify init --extension <bad-url>` showed
"failed: 'not-an-ip' does not appear to be an IPv4 or IPv6 address"
instead of an actionable message. Every sibling URL entry point
(extensions/__init__.py, presets/__init__.py, workflows/catalog.py,
extensions/_commands.py) already guards this exact case.
"""
(tmp_path / ".specify").mkdir()
with pytest.raises(ValueError, match="Malformed extension URL"):
_install_extension_during_init(
tmp_path, "https://[not-an-ip]/ext.zip", "1.0.0"
)