magi-code 0.96.1

Repository-aware CLI coding agent for terminal work
Documentation
use super::*;

pub(super) const REDACTED_PATH: &str = "[redacted]";

fn secret_like_path_case_insensitive(path: &Path) -> bool {
    path.file_name()
        .and_then(OsStr::to_str)
        .is_some_and(|name| {
            name.eq_ignore_ascii_case("auth.json")
                || name.eq_ignore_ascii_case("id_rsa")
                || name.eq_ignore_ascii_case("id_ed25519")
                || name
                    .get(..4)
                    .is_some_and(|prefix| prefix.eq_ignore_ascii_case(".env"))
                || name
                    .get(name.len().saturating_sub(4)..)
                    .is_some_and(|suffix| suffix.eq_ignore_ascii_case(".pem"))
                || name
                    .get(name.len().saturating_sub(4)..)
                    .is_some_and(|suffix| suffix.eq_ignore_ascii_case(".key"))
        })
}

fn secret_path_for_rewind(path: &Path) -> bool {
    secret_like_path(path) || secret_like_path_case_insensitive(path)
}

fn ascii_case_insensitive_component_eq(left: &OsStr, right: &OsStr) -> bool {
    match (left.to_str(), right.to_str()) {
        (Some(left), Some(right)) => left.eq_ignore_ascii_case(right),
        // Comparison-only fallback: never turn an arbitrary OS component into
        // a lossy path used for filesystem access.
        _ => left == right,
    }
}

fn path_starts_with_ascii_case_insensitive(path: &Path, prefix: &Path) -> bool {
    let mut path_components = path.components();
    for prefix_component in prefix.components() {
        let Some(path_component) = path_components.next() else {
            return false;
        };
        if !ascii_case_insensitive_component_eq(
            path_component.as_os_str(),
            prefix_component.as_os_str(),
        ) {
            return false;
        }
    }
    true
}
pub(super) fn redacted_relative_path(
    reason: &SnapshotCaptureStatus,
    relative_path: PathBuf,
) -> PathBuf {
    if matches!(
        reason,
        SnapshotCaptureStatus::Excluded {
            reason: super::SnapshotExclusionReason::SecretPath
        }
    ) || secret_path_for_rewind(&relative_path)
    {
        PathBuf::from(REDACTED_PATH)
    } else {
        relative_path
    }
}

pub(crate) fn redacted_path_for_display(path: &Path) -> String {
    safe_provider_path(&path.to_string_lossy())
}

pub(super) fn safe_rewind_target_path(
    store: &CheckpointStore,
    cwd: &Path,
    relative_path: &Path,
) -> Result<(), &'static str> {
    if relative_path.is_absolute() || relative_path.as_os_str().is_empty() {
        return Err("denied path");
    }
    let mut has_normal_component = false;
    for component in relative_path.components() {
        match component {
            Component::Normal(name) => {
                has_normal_component = true;
                if name
                    .to_str()
                    .is_some_and(|name| name.eq_ignore_ascii_case(".git"))
                {
                    return Err("denied path");
                }
            }
            Component::CurDir => {}
            Component::ParentDir | Component::Prefix(_) | Component::RootDir => {
                return Err("denied path");
            }
        }
    }
    if !has_normal_component || secret_path_for_rewind(relative_path) {
        return Err("denied path");
    }

    let root = lexical_normalize(cwd);
    let path = lexical_normalize(&root.join(relative_path));
    if !path_starts_with_ascii_case_insensitive(&path, &root) || path == root {
        return Err("denied path");
    }

    let checkpoint_root = store
        .root
        .canonicalize()
        .unwrap_or_else(|_| lexical_normalize(&store.root));
    if path_starts_with_ascii_case_insensitive(&path, &checkpoint_root) {
        return Err("denied path");
    }
    if let Some(mc_root) = checkpoint_root.parent() {
        let session_root = lexical_normalize(&mc_root.join("sessions"));
        if path_starts_with_ascii_case_insensitive(&path, &session_root) {
            return Err("denied path");
        }
    }

    Ok(())
}

pub(super) fn safe_provider_path(path: &str) -> String {
    let sanitized = redact_sensitive_text(path);
    if sanitized.contains("..")
        || sanitized.starts_with('/')
        || secret_path_for_rewind(Path::new(&sanitized))
    {
        "<redacted>".to_string()
    } else {
        sanitized
    }
}