diff --git a/confsync/utils/path_utils.py b/confsync/utils/path_utils.py new file mode 100644 index 0000000..e36cfba --- /dev/null +++ b/confsync/utils/path_utils.py @@ -0,0 +1,80 @@ +"""Path utility functions for ConfSync.""" + +import os +from pathlib import Path +from typing import List + + +def expand_path(path: str) -> str: + """Expand user home directory and environment variables in path.""" + return os.path.expandvars(os.path.expanduser(path)) + + +def normalize_path(path: str) -> str: + """Normalize a path to absolute form.""" + return str(Path(expand_path(path)).resolve()) + + +def get_home_directory() -> str: + """Get the user's home directory.""" + return str(Path.home()) + + +def get_xdg_config_dir() -> str: + """Get the XDG config directory, falling back to ~/.config.""" + xdg_config = os.environ.get('XDG_CONFIG_HOME') + if xdg_config: + return xdg_config + return os.path.join(get_home_directory(), '.config') + + +def get_xdg_data_dir() -> str: + """Get the XDG data directory, falling back to ~/.local/share.""" + xdg_data = os.environ.get('XDG_DATA_HOME') + if xdg_data: + return xdg_data + return os.path.join(get_home_directory(), '.local', 'share') + + +def list_directories(path: str) -> List[str]: + """List all directories in a path.""" + try: + return [d for d in os.listdir(path) if os.path.isdir(os.path.join(path, d))] + except OSError: + return [] + + +def path_components(path: str) -> List[str]: + """Get all components of a path.""" + return list(Path(path).parts) + + +def get_relative_path(path: str, base: str) -> str: + """Get the relative path from base to path.""" + try: + return str(Path(path).relative_to(base)) + except ValueError: + return path + + +def is_subpath(path: str, parent: str) -> bool: + """Check if path is a subpath of parent.""" + try: + path_resolved = Path(expand_path(path)).resolve() + parent_resolved = Path(expand_path(parent)).resolve() + return path_resolved == parent_resolved or parent_resolved in path_resolved.parents + except (OSError, ValueError): + return False + + +def safe_join(base: str, *parts: str) -> str: + """Safely join paths, preventing directory traversal.""" + base_path = Path(expand_path(base)).resolve() + + for part in parts: + if part.startswith('/') or part.startswith('..'): + if '..' in part: + part = part.replace('..', '') + base_path /= part + + return str(base_path)