mirror of
https://github.com/usestrix/strix.git
synced 2026-09-30 01:52:18 +00:00
fix(interface): handle Windows drive-letter paths in --workspace-file
A Windows drive letter (`C:\path\file`) has its only colon right after the
letter. `resolve_workspace_files` and `_workspace_file_dest` each called
`spec.rpartition(":")` independently and treated any colon as the
`PATH:DEST` separator, so a bare Windows path like `C:\temp\wordlist.txt`
was split into source "C" and destination "\temp\wordlist.txt" -- argument
parsing then rejected it because no file named "C" exists.
Add a shared `_split_workspace_spec` helper used by both call sites. It
detects the drive-letter case (a single-letter `raw` immediately followed
by `\` or `/` in what would be `dest`) and treats the whole spec as the
path instead, leaving the explicit `PATH:DEST` form -- including one where
PATH itself starts with a drive letter -- unaffected, since rpartition's
rightmost split there lands on the real destination separator, not the
drive letter's colon.
Fixes #1257
This commit is contained in:
parent
52b1923347
commit
0bf5c09f80
2 changed files with 51 additions and 5 deletions
|
|
@ -1715,10 +1715,26 @@ def validate_config_file(config_path: str) -> Path:
|
|||
# sources, so a large file makes session bring-up slower.
|
||||
|
||||
|
||||
def _split_workspace_spec(spec: str) -> tuple[str, str | None]:
|
||||
"""Split ``PATH[:DEST]`` into ``(path, dest)``, ``dest`` is ``None`` if absent.
|
||||
|
||||
A Windows drive letter's colon (``C:\\path\\file``) is the only colon in a
|
||||
bare path. ``rpartition`` still finds it, leaving a single-letter ``raw``
|
||||
with the rest of the path in what would be ``dest`` -- that split is
|
||||
detected here and rejected, so the whole spec is returned as the path.
|
||||
"""
|
||||
raw, sep, dest = spec.rpartition(":")
|
||||
if not sep or not dest.strip():
|
||||
return spec, None
|
||||
if re.fullmatch(r"[A-Za-z]", raw) and dest[:1] in ("\\", "/"):
|
||||
return spec, None
|
||||
return raw, dest
|
||||
|
||||
|
||||
def _workspace_file_dest(spec: str, source: Path) -> str:
|
||||
"""Return the workspace-relative destination declared by ``spec``."""
|
||||
_, sep, dest = spec.rpartition(":")
|
||||
candidate = dest.strip() if sep and dest.strip() else source.name
|
||||
_, dest = _split_workspace_spec(spec)
|
||||
candidate = dest.strip() if dest and dest.strip() else source.name
|
||||
if candidate.startswith("/") or Path(candidate).is_absolute():
|
||||
if not candidate.startswith("/workspace/"):
|
||||
raise ValueError(
|
||||
|
|
@ -1748,8 +1764,7 @@ def resolve_workspace_files(specs: list[str] | None) -> list[dict[str, str]]:
|
|||
resolved: list[dict[str, str]] = []
|
||||
seen: dict[str, str] = {}
|
||||
for spec in specs or []:
|
||||
raw, sep, dest = spec.rpartition(":")
|
||||
source_text = raw if sep and dest.strip() else spec
|
||||
source_text, _ = _split_workspace_spec(spec)
|
||||
source = Path(source_text.strip()).expanduser()
|
||||
if not source.is_file():
|
||||
raise ValueError(f"'{source}' is not an existing file")
|
||||
|
|
|
|||
|
|
@ -2,12 +2,17 @@
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import pytest
|
||||
|
||||
from strix.core.inputs import build_root_task
|
||||
from strix.interface.utils import read_workspace_files, resolve_workspace_files
|
||||
from strix.interface.utils import (
|
||||
_split_workspace_spec,
|
||||
read_workspace_files,
|
||||
resolve_workspace_files,
|
||||
)
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
|
@ -40,6 +45,32 @@ def test_a_declared_destination_is_taken_relative_to_the_workspace(
|
|||
assert resolved[0]["workspace_path"] == "/workspace/specs/openapi.yaml"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("spec", "expected"),
|
||||
[
|
||||
(r"C:\temp\wordlist.txt", (r"C:\temp\wordlist.txt", None)),
|
||||
("C:/temp/wordlist.txt", ("C:/temp/wordlist.txt", None)),
|
||||
(r"C:\temp\wordlist.txt:dest/file", (r"C:\temp\wordlist.txt", "dest/file")),
|
||||
],
|
||||
)
|
||||
def test_a_windows_drive_letter_is_not_mistaken_for_a_dest_separator(
|
||||
spec: str, expected: tuple[str, str | None]
|
||||
) -> None:
|
||||
assert _split_workspace_spec(spec) == expected
|
||||
|
||||
|
||||
@pytest.mark.skipif(sys.platform != "win32", reason="tmp_path is only drive-letter-shaped here")
|
||||
def test_a_bare_windows_path_resolves_end_to_end(tmp_path: Path) -> None:
|
||||
source = tmp_path / "wordlist.txt"
|
||||
source.write_text("admin\n", encoding="utf-8")
|
||||
|
||||
resolved = resolve_workspace_files([str(source)])
|
||||
|
||||
assert resolved == [
|
||||
{"source_path": str(source.resolve()), "workspace_path": "/workspace/wordlist.txt"}
|
||||
]
|
||||
|
||||
|
||||
def test_a_missing_file_is_rejected(tmp_path: Path) -> None:
|
||||
with pytest.raises(ValueError, match="not an existing file"):
|
||||
resolve_workspace_files([str(tmp_path / "nope.txt")])
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue