Change make_path_safe to is_path_safe and update tests to reflect this

This commit is contained in:
Beth Probert 2026-03-09 15:45:32 +00:00
parent 3776828699
commit 6d66dd0ed8
3 changed files with 38 additions and 62 deletions

View file

@ -18,7 +18,7 @@ from pydantic import BaseModel
import labthings_fastapi as lt import labthings_fastapi as lt
from openflexure_microscope_server.utilities import make_path_safe from openflexure_microscope_server.utilities import is_path_safe
IS_WINDOWS = os.name == "nt" IS_WINDOWS = os.name == "nt"
@ -68,7 +68,7 @@ def validate_command(cmd: list[str]) -> None:
"""Validate that the command only contains characters that are allowed in a path. """Validate that the command only contains characters that are allowed in a path.
The values in the commands should be numbers, commandline flags, paths, and The values in the commands should be numbers, commandline flags, paths, and
executables. All of these should be allowed by ``make_path_safe``. executables. All of these should be allowed by ``is_path_safe``.
This also checks against a blacklist of forbidden commands for defense-in-depth. This also checks against a blacklist of forbidden commands for defense-in-depth.
@ -82,8 +82,7 @@ def validate_command(cmd: list[str]) -> None:
) )
# Ensure characters are safe for a path component # Ensure characters are safe for a path component
_, error_raised = make_path_safe(element) if not is_path_safe(element):
if error_raised:
raise StitcherValidationError( raise StitcherValidationError(
f"Invalid stitching command: Element '{element}' contains unsafe characters." f"Invalid stitching command: Element '{element}' contains unsafe characters."
) )
@ -155,8 +154,7 @@ class BaseStitcher:
:raises RuntimeError: if inputs are unsafe. :raises RuntimeError: if inputs are unsafe.
""" """
_, error_raised = make_path_safe(self.images_dir) if not is_path_safe(self.images_dir):
if error_raised:
raise StitcherValidationError( raise StitcherValidationError(
"Invalid directory path: Contains unsafe characters." "Invalid directory path: Contains unsafe characters."
) )

View file

@ -110,7 +110,7 @@ _WINDOWS_RESERVED_NAMES = {
} }
def make_path_safe(unsafe_path_string: str) -> tuple[str, bool]: def is_path_safe(unsafe_path_string: str) -> bool:
"""Check if a file path has any unsafe elements in it. """Check if a file path has any unsafe elements in it.
The path is not coerced into a safe form because if we ask The path is not coerced into a safe form because if we ask
@ -119,8 +119,7 @@ def make_path_safe(unsafe_path_string: str) -> tuple[str, bool]:
:param unsafe_path_string: The original path string to sanitise. :param unsafe_path_string: The original path string to sanitise.
:returns: The input string unchanged and a boolean indicating if :returns: A boolean indicating if any unsafe features were found.
any unsafe features were found.
""" """
# Split by separators first to sanitise components independently # Split by separators first to sanitise components independently
@ -195,14 +194,13 @@ def make_path_safe(unsafe_path_string: str) -> tuple[str, bool]:
) )
reserved_word = True reserved_word = True
warning_raised = ( return not (
unsafe_relative_path unsafe_relative_path
or trailing_dots or trailing_dots
or trailing_whitespace or trailing_whitespace
or unsafe_characters or unsafe_characters
or reserved_word or reserved_word
) )
return unsafe_path_string, warning_raised
def make_name_safe(unsafe_name_string: str) -> str: def make_name_safe(unsafe_name_string: str) -> str:

View file

@ -7,12 +7,14 @@ import pytest
from openflexure_microscope_server.utilities import ( from openflexure_microscope_server.utilities import (
_WINDOWS_RESERVED_NAMES, _WINDOWS_RESERVED_NAMES,
is_path_safe,
make_name_safe, make_name_safe,
make_path_safe,
) )
PATHS_WITH_DOTS = {"a./b/c.", "path./to/file.", "./path./to/file."} PATHS_WITH_DOTS = {"a./b/c.", "path./to/file.", "./path./to/file."}
PATHS_WITH_SPACES = {"path /to /file ", "path/to /file", "path/a long path name /file"}
RESERVED_NAME_PATHS = {"path/CON/file", "path/NUL.txt/file", "C:/AUX/test"} RESERVED_NAME_PATHS = {"path/CON/file", "path/NUL.txt/file", "C:/AUX/test"}
if sys.platform.startswith("win"): if sys.platform.startswith("win"):
@ -76,32 +78,26 @@ def test_make_name_safe_reserved_names_false_positives():
assert make_name_safe("CHILLI CON CARNE") == "chilli_con_carne" assert make_name_safe("CHILLI CON CARNE") == "chilli_con_carne"
def test_make_path_safe_basic(caplog): def test_is_path_safe_basic(caplog):
"""Test basic functionality of make_path_safe.""" """Test basic functionality of is_path_safe."""
# Note: behavior depends on platform for separators # Note: behavior depends on platform for separators
with caplog.at_level(logging.WARNING): with caplog.at_level(logging.WARNING):
if sys.platform.startswith("win"): if sys.platform.startswith("win"):
assert make_path_safe("C:\\path\\to/file")[0] == "C:\\path\\to/file" assert is_path_safe("C:\\path\\to/file")
assert ( assert is_path_safe("C:\\a very long path\\to/file")
make_path_safe("C:\\a very long path\\to/file")[0]
== "C:\\a very long path\\to/file"
)
else: else:
assert make_path_safe("path/to/file")[0] == "path/to/file" assert is_path_safe("path/to/file")
assert ( assert is_path_safe("a very long path/to/file")
make_path_safe("a very long path/to/file")[0]
== "a very long path/to/file"
)
# No errors should be raised. These paths are safe. # No errors should be raised. These paths are safe.
assert len(caplog.records) == 0 assert len(caplog.records) == 0
@pytest.mark.parametrize("path", UNSAFE_CHARACTER_PATHS) @pytest.mark.parametrize("path", UNSAFE_CHARACTER_PATHS)
def test_make_path_safe_unsafe_characters_in_components(caplog, path): def test_is_path_safe_unsafe_characters_in_components(caplog, path):
"""Test unsafe characters within path components.""" """Test unsafe characters within path components."""
with caplog.at_level(logging.WARNING): with caplog.at_level(logging.WARNING):
assert make_path_safe(path)[0] == path assert not is_path_safe(path)
assert ( assert (
f"{path} contains characters that may be unsafe on this platform." f"{path} contains characters that may be unsafe on this platform."
in caplog.messages in caplog.messages
@ -109,68 +105,52 @@ def test_make_path_safe_unsafe_characters_in_components(caplog, path):
@pytest.mark.parametrize("path", RESERVED_NAME_PATHS) @pytest.mark.parametrize("path", RESERVED_NAME_PATHS)
def test_make_path_safe_reserved_in_components(caplog, path): def test_is_path_safe_reserved_in_components(caplog, path):
"""Test reserved names within path components.""" """Test reserved names within path components."""
with caplog.at_level(logging.WARNING): with caplog.at_level(logging.WARNING):
assert make_path_safe(path)[0] == path assert not is_path_safe(path)
assert ( assert (
f"{path} contains a reserved system name and may cause issues." f"{path} contains a reserved system name and may cause issues."
in caplog.messages in caplog.messages
) )
def test_make_path_safe_reserved_name_components(caplog): def test_is_path_safe_reserved_name_components(caplog):
"""Test reserved names are okay as part of a larger dir name.""" """Test reserved names are okay as part of a larger dir name."""
with caplog.at_level(logging.WARNING): with caplog.at_level(logging.WARNING):
assert make_path_safe("chilli con carne/recipe")[0] == "chilli con carne/recipe" assert is_path_safe("chilli con carne/recipe")
assert len(caplog.records) == 0 assert len(caplog.records) == 0
def test_make_path_safe_trailing_space_in_components(caplog): @pytest.mark.parametrize("path", PATHS_WITH_SPACES)
def test_is_path_safe_trailing_space_in_components(caplog, path):
"""Test trailing spaces in path components.""" """Test trailing spaces in path components."""
with caplog.at_level(logging.WARNING): with caplog.at_level(logging.WARNING):
# Test Windows - cannot have trailingspaces # Test Windows - cannot have trailing spaces
if sys.platform.startswith("win"): if sys.platform.startswith("win"):
assert make_path_safe("path /to /file ")[0] == "path /to /file " assert not is_path_safe(path)
assert make_path_safe("path/to /file")[0] == "path/to /file" assert len(caplog.records) == 1
# Check directories can have spaces in them but not at the end assert f"{path} contains unsafe trailing spaces." in caplog.messages
assert (
make_path_safe("path/a long path name /file")[0]
== "path/a long path name /file"
)
assert len(caplog.records) == 3
assert "path /to /file contains unsafe trailing spaces." in caplog.messages
assert "path/to /file contains unsafe trailing spaces." in caplog.messages
assert (
"path/a long path name /file contains unsafe trailing spaces."
in caplog.messages
)
else: else:
# Trailing spaces allows in POSIX systems # Trailing spaces allows in POSIX systems
assert make_path_safe("path /to /file ")[0] == "path /to /file " assert is_path_safe(path)
assert make_path_safe("path/to /file")[0] == "path/to /file"
# Check directories can have spaces in them but not at the end
assert (
make_path_safe("path/a long path name /file")[0]
== "path/a long path name /file"
)
# No warnings should be raised # No warnings should be raised
assert len(caplog.records) == 0 assert len(caplog.records) == 0
def test_make_path_safe_relative(caplog): def test_is_path_safe_relative(caplog):
"""Test that relative path components are preserved.""" """Test that relative path components are preserved."""
# Relative imports ./foo, ../foo or ../../foo are allowed. # Relative imports ./foo, ../foo or ../../foo are allowed.
with caplog.at_level(logging.WARNING): with caplog.at_level(logging.WARNING):
assert make_path_safe("./openflexure/data/")[0] == "./openflexure/data/" assert is_path_safe("./openflexure/data/")
assert make_path_safe("../openflexure/data/")[0] == "../openflexure/data/" assert is_path_safe("../openflexure/data/")
assert make_path_safe("../../openflexure/data/")[0] == "../../openflexure/data/" assert is_path_safe("../../openflexure/data/")
assert make_path_safe(".")[0] == "." assert is_path_safe(".")
assert make_path_safe("..")[0] == ".." assert is_path_safe("..")
assert len(caplog.records) == 0 assert len(caplog.records) == 0
assert make_path_safe("a_bad/../relative_path")[0] == "a_bad/../relative_path" assert not is_path_safe("a_bad/../relative_path")
assert len(caplog.records) == 1 assert len(caplog.records) == 1
assert ( assert (
"File path a_bad/../relative_path may be unsafe due to unexpected relative navigation." "File path a_bad/../relative_path may be unsafe due to unexpected relative navigation."
@ -179,10 +159,10 @@ def test_make_path_safe_relative(caplog):
@pytest.mark.parametrize("filepath", PATHS_WITH_DOTS) @pytest.mark.parametrize("filepath", PATHS_WITH_DOTS)
def test_make_path_safe_dots_warning(caplog, filepath): def test_is_path_safe_dots_warning(caplog, filepath):
"""Test that a user is warned about trailing dots in their file paths. The paths should remain unchanged.""" """Test that a user is warned about trailing dots in their file paths. The paths should remain unchanged."""
with caplog.at_level(logging.WARNING): with caplog.at_level(logging.WARNING):
assert make_path_safe(filepath)[0] == filepath assert not is_path_safe(filepath)
assert ( assert (
f"File path {filepath} may be unsafe due to trailing dots." f"File path {filepath} may be unsafe due to trailing dots."
in caplog.messages in caplog.messages