Skip to content
Merged
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
239 changes: 58 additions & 181 deletions bad_path/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ class DangerousPathError(PermissionError):
"""Exception raised when a dangerous path is detected."""



# Module-level list of user-defined dangerous paths
_user_defined_paths: list[str] = []

Expand Down Expand Up @@ -112,11 +111,17 @@ def get_dangerous_paths() -> list[str]:
"""
match platform.system():
case "Windows":
from .platforms.windows import system_paths
from .platforms.windows.paths import (
system_paths,
) # pylint: disable=import-outside-toplevel
case "Darwin":
from .platforms.darwin import system_paths
from .platforms.darwin.paths import (
system_paths,
) # pylint: disable=import-outside-toplevel
case _: # Linux and other Unix-like systems
from .platforms.posix import system_paths
from .platforms.posix.paths import (
system_paths,
) # pylint: disable=import-outside-toplevel

# Merge system paths and user-defined paths using sets to avoid duplicates
all_paths = set(system_paths) | set(_user_defined_paths)
Expand Down Expand Up @@ -461,7 +466,9 @@ def _check_cwd_traversal(self, path_obj: Path | None = None) -> bool:
# If other resolution fails, treat as dangerous
return True

def _check_against_paths(self, paths: list[str], path_obj: Path | None = None) -> bool:
def _check_against_paths(
self, paths: list[str], path_obj: Path | None = None
) -> bool:
"""Check if a path matches any in the given list.

Args:
Expand Down Expand Up @@ -517,7 +524,9 @@ def _check_invalid_chars(self, path_str: str | None = None) -> bool:

return False

def __call__(self, path: str | Path | None = None, raise_error: bool = False) -> bool:
def __call__(
self, path: str | Path | None = None, raise_error: bool = False
) -> bool:
"""Check a path for danger, with optional path reload.

Note: Unlike the boolean context (which returns True for safe paths),
Expand Down Expand Up @@ -585,7 +594,9 @@ def __call__(self, path: str | Path | None = None, raise_error: bool = False) ->
is_dangerous = True

if is_dangerous and raise_error:
raise DangerousPathError(f"Path '{path}' points to a dangerous location")
raise DangerousPathError(
f"Path '{path}' points to a dangerous location"
)

return is_dangerous
else:
Expand All @@ -594,7 +605,9 @@ def __call__(self, path: str | Path | None = None, raise_error: bool = False) ->
is_dangerous = self._is_dangerous()

if is_dangerous and raise_error:
raise DangerousPathError(f"Path '{self._path}' points to a dangerous location")
raise DangerousPathError(
f"Path '{self._path}' points to a dangerous location"
)

return is_dangerous

Expand Down Expand Up @@ -739,176 +752,10 @@ def __repr__(self) -> str:
# Platform Classes
# ============================================================================


class WindowsPathChecker(BasePathChecker):
"""Windows-specific PathChecker implementation.

Handles Windows-specific path validation including drive letters, reserved names,
and Windows-specific invalid characters.
"""

def _load_invalid_chars(self) -> None:
"""Load Windows-specific invalid characters and reserved names."""
from .platforms.windows import invalid_chars, reserved_names
self._invalid_chars = invalid_chars
self._reserved_names = reserved_names

def _load_and_check_paths(self) -> None:
"""Load system and user paths, then check the current path against them."""
from .platforms.windows import system_paths
self._system_paths = system_paths
self._user_paths = get_user_paths()

# Check both types
self._is_system_path = self._check_against_paths(self._system_paths)
self._is_user_path = self._check_against_paths(self._user_paths)

def _check_cwd_traversal(self, path_obj: Path | None = None) -> bool:
"""Check if a path traverses outside the current working directory.

Windows-specific implementation with case-insensitive comparison.

Keyword Parameters:
path_obj (Path | None):
Optional Path object to check. If not provided, uses self._path_obj.
Defaults to None.

Returns:
(bool):
True if the path is outside CWD (dangerous), False otherwise.
"""
if path_obj is None:
path_obj = self._path_obj
try:
cwd = Path.cwd().resolve()

# Check if path equals CWD (handles "." case)
# Use case-insensitive string comparison for Windows
if str(path_obj).lower() == str(cwd).lower():
return False # Path is CWD itself (safe)

# Also try samefile() if paths exist (handles symlinks, etc.)
try:
if path_obj.exists() and cwd.exists() and path_obj.samefile(cwd):
return False # Same file/directory (safe)
except (OSError, ValueError, AttributeError):
# samefile() not available or failed, continue with relative_to
pass

# Try to express path_obj relative to cwd
# If this succeeds, the path is within CWD
path_obj.relative_to(cwd)
return False # Path is within CWD (safe)
except ValueError:
# relative_to raised ValueError, so path is outside CWD
return True # Path is outside CWD (dangerous)
except (OSError, RuntimeError):
# If other resolution fails, treat as dangerous
return True

def _check_invalid_chars(self, path_str: str | None = None) -> bool:
"""Check for Windows-specific invalid characters.

Includes special handling for:
- Drive letter colons (C:, D:, etc.)
- Reserved names (CON, PRN, AUX, etc.)
- Paths ending with space or period

Keyword Parameters:
path_str (str | None):
Optional path string to check. If not provided, uses self._path.
Defaults to None.

Returns:
(bool):
True if the path contains invalid characters, False otherwise.
"""
if path_str is None:
path_str = str(self._path_obj)

# Check for invalid characters
for char in self._invalid_chars:
if char in path_str:
# Special handling for colon on Windows (valid in drive letters like C:)
if char == ":":
# Check if colon is part of a drive letter (e.g., C:, D:)
# Valid pattern: single letter followed by colon at start of path
if len(path_str) >= 2 and path_str[1] == ":" and path_str[0].isalpha():
# This is a valid drive letter if it's the only colon
if path_str.count(":") == 1:
continue # This is a valid drive letter colon
return True

# Check for reserved names (case-insensitive)
# Extract the filename from the path using string operations
# to avoid Path() issues with invalid characters
# Split by both forward slash and backslash
path_parts = path_str.replace("\\", "/").split("/")
if path_parts:
filename = path_parts[-1]

# Extract name without extension
if "." in filename:
name_without_ext = filename.rsplit(".", 1)[0].upper()
else:
name_without_ext = filename.upper()

# Check if the name (without extension) is a reserved name
if name_without_ext in self._reserved_names:
return True

# Check if filename ends with space or period (invalid in Windows)
if filename and (filename.endswith(" ") or filename.endswith(".")):
return True

return False


class DarwinPathChecker(BasePathChecker):
"""Darwin (macOS)-specific PathChecker implementation.

Handles macOS-specific path validation including restrictions on colons
in file names and macOS system directories.
"""

def _load_invalid_chars(self) -> None:
"""Load Darwin-specific invalid characters."""
from .platforms.darwin import invalid_chars
self._invalid_chars = invalid_chars
self._reserved_names = []

def _load_and_check_paths(self) -> None:
"""Load system and user paths, then check the current path against them."""
from .platforms.darwin import system_paths
self._system_paths = system_paths
self._user_paths = get_user_paths()

# Check both types
self._is_system_path = self._check_against_paths(self._system_paths)
self._is_user_path = self._check_against_paths(self._user_paths)


class PosixPathChecker(BasePathChecker):
"""POSIX (Linux and Unix)-specific PathChecker implementation.

Handles POSIX-compliant path validation for Linux and other Unix-like systems.
"""

def _load_invalid_chars(self) -> None:
"""Load POSIX-specific invalid characters."""
from .platforms.posix import invalid_chars
self._invalid_chars = invalid_chars
self._reserved_names = []

def _load_and_check_paths(self) -> None:
"""Load system and user paths, then check the current path against them."""
from .platforms.posix import system_paths
self._system_paths = system_paths
self._user_paths = get_user_paths()

# Check both types
self._is_system_path = self._check_against_paths(self._system_paths)
self._is_user_path = self._check_against_paths(self._user_paths)
# Platform-specific checker implementations have been moved to:
# - bad_path.platforms.checkers.windows (WindowsPathChecker)
# - bad_path.platforms.checkers.darwin (DarwinPathChecker)
# - bad_path.platforms.checkers.posix (PosixPathChecker)


# ============================================================================
Expand Down Expand Up @@ -964,16 +811,46 @@ def _create_path_checker(
"""
match platform.system():
case "Windows":
from .platforms.windows.checker import ( # pylint: disable=import-outside-toplevel
WindowsPathChecker,
)

return WindowsPathChecker(
path, raise_error, mode, system_ok, user_paths_ok, not_writeable, cwd_only
path,
raise_error,
mode,
system_ok,
user_paths_ok,
not_writeable,
cwd_only,
)
case "Darwin":
from .platforms.darwin.checker import ( # pylint: disable=import-outside-toplevel
DarwinPathChecker,
)

return DarwinPathChecker(
path, raise_error, mode, system_ok, user_paths_ok, not_writeable, cwd_only
path,
raise_error,
mode,
system_ok,
user_paths_ok,
not_writeable,
cwd_only,
)
case _: # Linux and other Unix-like systems
from .platforms.posix.checker import ( # pylint: disable=import-outside-toplevel
PosixPathChecker,
)

return PosixPathChecker(
path, raise_error, mode, system_ok, user_paths_ok, not_writeable, cwd_only
path,
raise_error,
mode,
system_ok,
user_paths_ok,
not_writeable,
cwd_only,
)


Expand Down
5 changes: 5 additions & 0 deletions bad_path/platforms/darwin/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
"""Darwin (macOS) platform-specific code.

This module contains macOS-specific path validation logic including
system paths, invalid characters, and the DarwinPathChecker class.
"""
36 changes: 36 additions & 0 deletions bad_path/platforms/darwin/checker.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
"""Darwin (macOS)-specific path checker implementation.

This module provides the DarwinPathChecker class for validating paths on macOS systems.
"""

from ...checker import BasePathChecker, get_user_paths


class DarwinPathChecker(BasePathChecker):
"""Darwin (macOS)-specific PathChecker implementation.

Handles macOS-specific path validation including restrictions on colons
in file names and macOS system directories.
"""

def _load_invalid_chars(self) -> None:
"""Load Darwin-specific invalid characters."""
from .paths import ( # pylint: disable=import-outside-toplevel
invalid_chars,
)

self._invalid_chars = invalid_chars
self._reserved_names = []

def _load_and_check_paths(self) -> None:
"""Load system and user paths, then check the current path against them."""
from .paths import ( # pylint: disable=import-outside-toplevel
system_paths,
)

self._system_paths = system_paths
self._user_paths = get_user_paths()

# Check both types
self._is_system_path = self._check_against_paths(self._system_paths)
self._is_user_path = self._check_against_paths(self._user_paths)
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@
"/System",
"/Library",
"/private/etc", # System configuration (don't use /private to allow /private/tmp)
# /private/var subdirectories (don't use /private/var to allow /private/var/folders for temp files)
# /private/var subdirectories (don't use /private/var to allow
# /private/var/folders for temp files)
"/private/var/root", # Root user's home directory
"/private/var/db", # System databases
"/private/var/log", # System logs
Expand All @@ -43,5 +44,5 @@
# The null byte (\0) and colon (:) are forbidden in file names.
invalid_chars = [
"\0", # Null byte - strictly forbidden in POSIX
":", # Colon - problematic in macOS (was path separator in legacy Mac OS)
":", # Colon - problematic in macOS (was path separator in legacy Mac OS)
]
5 changes: 5 additions & 0 deletions bad_path/platforms/posix/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
"""POSIX (Linux and Unix) platform-specific code.

This module contains POSIX-specific path validation logic including
system paths, invalid characters, and the PosixPathChecker class.
"""
Loading
Loading