codex-mobile-bridge 0.3.10

Remote bridge and service manager for codex-mobile.
Documentation
use std::fs;
use std::path::{Path, PathBuf};

use anyhow::{Context, Result, bail};

pub fn canonicalize_directory(path: &Path) -> Result<PathBuf> {
    let canonical = fs::canonicalize(path)
        .with_context(|| format!("目录不存在或不可访问: {}", path.display()))?;

    if !canonical.is_dir() {
        bail!("目标必须是目录: {}", canonical.display());
    }

    Ok(trim_trailing_separator(&canonical))
}

pub fn normalize_absolute_directory(path: &Path) -> Result<PathBuf> {
    if !path.is_absolute() {
        bail!("目录路径必须为绝对路径: {}", path.display());
    }

    Ok(trim_trailing_separator(path))
}

pub fn default_display_name(path: &Path) -> String {
    path.file_name()
        .and_then(|name| name.to_str())
        .map(ToOwned::to_owned)
        .filter(|value| !value.is_empty())
        .unwrap_or_else(|| "directory".to_string())
}

pub fn directory_contains(base: &Path, candidate: &Path) -> bool {
    candidate.starts_with(base)
}

pub fn parent_directory(path: &Path) -> Option<PathBuf> {
    path.parent()
        .filter(|parent| parent != &path)
        .map(trim_trailing_separator)
}

fn trim_trailing_separator(path: &Path) -> PathBuf {
    if path == Path::new("/") {
        return PathBuf::from("/");
    }

    let display = path.to_string_lossy();
    let trimmed = display.trim_end_matches('/');
    if trimmed.is_empty() {
        PathBuf::from("/")
    } else {
        PathBuf::from(trimmed)
    }
}

#[cfg(test)]
mod tests {
    use std::fs;

    use super::{canonicalize_directory, directory_contains, parent_directory};

    #[test]
    fn canonicalize_directory_accepts_existing_dir() {
        let temp = tempfile_dir("directory-ok");
        fs::create_dir_all(&temp).unwrap();

        let result = canonicalize_directory(&temp).unwrap();
        assert!(result.is_dir());
    }

    #[test]
    fn directory_contains_respects_path_boundaries() {
        assert!(directory_contains(
            std::path::Path::new("/srv/work"),
            std::path::Path::new("/srv/work/project"),
        ));
        assert!(!directory_contains(
            std::path::Path::new("/srv/work"),
            std::path::Path::new("/srv/workspace"),
        ));
    }

    #[test]
    fn parent_directory_returns_none_for_root() {
        assert!(parent_directory(std::path::Path::new("/")).is_none());
    }

    fn tempfile_dir(name: &str) -> std::path::PathBuf {
        std::env::temp_dir().join(format!("{}-{}", name, uuid::Uuid::new_v4().simple()))
    }
}