coil-storage 0.1.1

Object storage primitives for the Coil framework.
Documentation
use super::StoragePolicyError;

pub(crate) fn normalize_relative_path(input: &str) -> Result<String, StoragePolicyError> {
    let trimmed = input.trim();

    if trimmed.is_empty() || trimmed.starts_with('/') {
        return Err(StoragePolicyError::InvalidRelativePath {
            path: input.to_string(),
        });
    }

    let mut segments = Vec::new();
    for segment in trimmed.split('/') {
        match segment {
            "" | "." => continue,
            ".." => {
                return Err(StoragePolicyError::ParentTraversal {
                    path: input.to_string(),
                });
            }
            _ => segments.push(segment),
        }
    }

    if segments.is_empty() {
        return Err(StoragePolicyError::InvalidRelativePath {
            path: input.to_string(),
        });
    }

    Ok(segments.join("/"))
}

pub(crate) fn normalize_rule_prefix(input: &str) -> Result<String, StoragePolicyError> {
    let trimmed = input.trim();
    if trimmed.is_empty() {
        return Ok(String::new());
    }

    normalize_relative_path(trimmed)
}

pub(crate) fn join_relative(prefix: Option<&str>, logical_path: &str) -> String {
    let mut parts = Vec::new();

    if let Some(prefix) = prefix {
        let normalized = prefix.trim().trim_matches('/');
        if !normalized.is_empty() {
            parts.push(normalized);
        }
    }

    let normalized_path = logical_path.trim().trim_matches('/');
    if !normalized_path.is_empty() {
        parts.push(normalized_path);
    }

    parts.join("/")
}

pub(crate) fn join_local_path(root: &str, subdir: Option<&str>, logical_path: &str) -> String {
    let root = root.trim_end_matches('/');
    let root_is_absolute = root.starts_with('/');
    let mut parts = Vec::new();

    if !root.is_empty() {
        parts.push(if root_is_absolute {
            root.trim_start_matches('/').to_string()
        } else {
            root.to_string()
        });
    }

    if let Some(subdir) = subdir {
        let normalized = subdir.trim().trim_matches('/');
        if !normalized.is_empty() {
            parts.push(normalized.to_string());
        }
    }

    let normalized_path = logical_path.trim().trim_matches('/');
    if !normalized_path.is_empty() {
        parts.push(normalized_path.to_string());
    }

    let joined = parts.join("/");
    if root_is_absolute {
        format!("/{joined}")
    } else {
        joined
    }
}

pub(crate) fn trim_trailing_separator(input: &str) -> String {
    let trimmed = input.trim_end_matches('/');
    if trimmed.is_empty() {
        input.to_string()
    } else {
        trimmed.to_string()
    }
}