Source code for sqlspec.storage._paths

"""Pure storage path helpers safe for mypyc compilation."""

import re
from pathlib import Path, PurePosixPath, PureWindowsPath
from typing import Final

from sqlspec.exceptions import StoragePathTraversalError

__all__ = (
    "FILE_PROTOCOL",
    "FILE_SCHEME_PREFIX",
    "ensure_path_within_root",
    "extract_glob_static_prefix",
    "glob_to_regex",
    "is_file_destination",
    "reject_parent_traversal",
    "resolve_storage_path",
    "strip_windows_drive_prefix",
)


FILE_PROTOCOL: Final[str] = "file"
FILE_SCHEME_PREFIX: Final[str] = "file://"


def reject_parent_traversal(path: "str | Path") -> None:
    """Raise if any segment of ``path`` is a parent reference.

    Both separators are considered, so a Windows-style key cannot smuggle a
    parent reference past a POSIX-only split.

    Args:
        path: Caller-supplied storage path.

    Raises:
        StoragePathTraversalError: If a ``..`` segment is present.
    """
    path_str = str(path)
    posix_parts = PurePosixPath(path_str).parts
    windows_parts = PureWindowsPath(path_str).parts
    if ".." in posix_parts or ".." in windows_parts:
        raise StoragePathTraversalError(path_str)


def ensure_path_within_root(path: "str | Path", root: "str | Path") -> str:
    """Return ``path`` as a ``root``-relative POSIX path, refusing anything outside ``root``.

    A relative path is joined to ``root``; an absolute path must already be inside
    it. Parent references are rejected before any filesystem call, and the
    resolved result is re-checked so a symlink cannot escape.

    Args:
        path: Caller-supplied storage path.
        root: Directory the path must stay within.

    Returns:
        The path relative to ``root``, or ``""`` when it is ``root`` itself.

    Raises:
        StoragePathTraversalError: If the path escapes ``root``.
    """
    reject_parent_traversal(path)

    root_obj = Path(str(root)).resolve()
    path_obj = Path(str(path))
    candidate = path_obj if path_obj.is_absolute() else root_obj / path_obj
    resolved = candidate.resolve()

    if resolved != root_obj and root_obj not in resolved.parents:
        raise StoragePathTraversalError(str(path), str(root_obj))

    if resolved == root_obj:
        return ""
    return resolved.relative_to(root_obj).as_posix()


_GLOB_MAGIC: Final = re.compile(r"[*?\[]")


def extract_glob_static_prefix(pattern: str) -> str:
    """Return the literal directory prefix of a glob pattern.

    The result is either ``""`` or a string ending in ``/`` made of whole path
    segments that contain no glob metacharacters. A pattern with no wildcards
    yields its directory portion, never the full key.

    Args:
        pattern: Glob pattern in POSIX form.

    Returns:
        Static directory prefix suitable for a listing API.
    """
    segments = pattern.lstrip("/").split("/")
    static: list[str] = []
    for segment in segments[:-1]:
        if not segment or _GLOB_MAGIC.search(segment):
            break
        static.append(segment)
    return "/".join(static) + "/" if static else ""


def glob_to_regex(pattern: str) -> "re.Pattern[str]":
    """Compile a glob pattern to an anchored regex with pathlib semantics.

    ``*`` and ``?`` match within a single path segment. A ``**`` segment matches
    zero or more whole segments. Matching is anchored at both ends, so a pattern
    describes the entire object key rather than a suffix of it.

    The honored magic characters are ``*``, ``?`` and ``[``. There is no brace
    expansion and no escape syntax, matching what the local and fsspec backends
    accept. Compiled patterns are cached by :mod:`re` itself, so repeated calls
    with the same pattern do not recompile.

    Args:
        pattern: Glob pattern.

    Returns:
        A compiled, anchored regex. An empty pattern matches nothing.
    """
    if not pattern:
        return re.compile(r"(?!)")

    parts = pattern.split("/")
    pieces: list[str] = []
    last_index = len(parts) - 1
    for index, part in enumerate(parts):
        if part == "**":
            pieces.append(".*" if index == last_index else "(?:[^/]+/)*")
            continue
        pieces.append(_glob_segment_regex(part))
        if index != last_index:
            pieces.append("/")

    return re.compile(f"(?s:{''.join(pieces)})\\Z")


def strip_windows_drive_prefix(path: str) -> str:
    """Drop a leading slash from a urlparse'd Windows drive path (``/C:/x`` -> ``C:/x``)."""
    if path and len(path) > 2 and path[2] == ":":  # noqa: PLR2004
        return path[1:]
    return path


def is_file_destination(path: "str | Path") -> bool:
    """Classify a local path as a file (vs directory) destination.

    Resolves the file-vs-directory ambiguity for paths that may not exist yet, so
    writes and reads agree. A trailing separator or an existing directory is a
    directory; a path with a filename suffix is a file; otherwise it is a directory.
    """
    path_str = str(path)
    if path_str.endswith(("/", "\\")):
        return False
    path_obj = Path(path_str)
    if path_obj.is_dir():
        return False
    return bool(path_obj.suffix)


[docs] def resolve_storage_path( path: "str | Path", base_path: str = "", protocol: str = FILE_PROTOCOL, strip_file_scheme: bool = True ) -> str: """Resolve path relative to base_path with protocol-specific handling. Args: path: Path to resolve. base_path: Base path to prepend if path is relative. protocol: Storage protocol. strip_file_scheme: Whether to strip ``file://`` prefixes. Returns: Resolved path string suitable for the storage backend. Raises: StoragePathTraversalError: If the path contains a parent reference. """ reject_parent_traversal(path) path_str = str(path) if strip_file_scheme and path_str.startswith(FILE_SCHEME_PREFIX): path_str = path_str.removeprefix(FILE_SCHEME_PREFIX) if protocol == FILE_PROTOCOL: path_obj = Path(path_str) if path_obj.is_absolute(): if base_path: base_obj = Path(base_path) try: relative = path_obj.relative_to(base_obj) if str(relative) == ".": return base_path return f"{base_path.rstrip('/')}/{relative}" except ValueError: return path_str.lstrip("/") return path_str.lstrip("/") if base_path: return f"{base_path.rstrip('/')}/{path_str}" return path_str if not base_path: return path_str clean_base = base_path.rstrip("/") clean_path = path_str.lstrip("/") return f"{clean_base}/{clean_path}"
def _glob_segment_regex(segment: str) -> str: """Translate one glob path segment to regex source that never crosses ``/``.""" out: list[str] = [] index = 0 length = len(segment) while index < length: char = segment[index] if char == "*": out.append("[^/]*") index += 1 elif char == "?": out.append("[^/]") index += 1 elif char == "[": close = segment.find("]", index + 1) body = segment[index + 1 : close] if close != -1 else "" negated = body.startswith(("!", "^")) if negated: body = body[1:] if close == -1 or not body: out.append(re.escape(char)) index += 1 continue escaped = body.replace("\\", "\\\\").replace("[", "\\[") prefix = "^" if negated else "" out.append(f"[{prefix}{escaped}]") index = close + 1 else: out.append(re.escape(char)) index += 1 return "".join(out)