Skip to main content

rippy_cli/
self_protect.rs

1//! Self-protection: prevent AI tools from modifying rippy's own config files.
2//!
3//! These checks run **before** any user-configurable rules and cannot be
4//! overridden by config. The only escape hatch is `set self-protect off`
5//! (which requires manual editing of the config file).
6
7use std::path::Path;
8
9/// Filenames that are always protected (matched against basename).
10const PROTECTED_BASENAMES: &[&str] = &[".rippy", ".rippy.toml", ".dippy"];
11
12/// Subdirectory paths that are always protected (matched against suffix).
13const PROTECTED_SUFFIXES: &[&str] = &[
14    ".rippy/config",
15    ".rippy/config.toml",
16    ".rippy/trusted.json",
17    ".dippy/config",
18];
19
20/// Message returned when a protected file is denied.
21pub const PROTECTION_MESSAGE: &str = "rippy configuration files are protected from modification. To disable self-protection, manually add `set self-protect off` to your config.";
22
23/// Check if a file path targets a protected rippy configuration file.
24///
25/// Matches against:
26/// - Exact basename: `.rippy`, `.rippy.toml`, `.dippy`
27/// - Path suffixes: `.rippy/config`, `.rippy/config.toml`, `.dippy/config`
28#[must_use]
29pub fn is_protected_path(path: &str) -> bool {
30    let path = Path::new(path);
31
32    if path
33        .file_name()
34        .and_then(|n| n.to_str())
35        .is_some_and(|name| PROTECTED_BASENAMES.contains(&name))
36    {
37        return true;
38    }
39
40    // Check if the path ends with a protected suffix.
41    let path_str = path.to_string_lossy();
42    PROTECTED_SUFFIXES
43        .iter()
44        .any(|suffix| path_str.ends_with(suffix))
45}
46
47#[cfg(test)]
48#[allow(clippy::unwrap_used)]
49mod tests {
50    use super::*;
51
52    #[test]
53    fn protects_rippy_config() {
54        assert!(is_protected_path(".rippy"));
55        assert!(is_protected_path(".rippy.toml"));
56        assert!(is_protected_path(".dippy"));
57    }
58
59    #[test]
60    fn protects_with_directory_prefix() {
61        assert!(is_protected_path("/home/user/project/.rippy"));
62        assert!(is_protected_path("some/path/.rippy.toml"));
63        assert!(is_protected_path("/tmp/.dippy"));
64    }
65
66    #[test]
67    fn protects_global_config() {
68        assert!(is_protected_path("/home/user/.rippy/config"));
69        assert!(is_protected_path("/home/user/.rippy/config.toml"));
70        assert!(is_protected_path("/home/user/.dippy/config"));
71    }
72
73    #[test]
74    fn protects_trust_database() {
75        assert!(is_protected_path("/home/user/.rippy/trusted.json"));
76        assert!(is_protected_path(".rippy/trusted.json"));
77    }
78
79    #[test]
80    fn does_not_protect_unrelated_files() {
81        assert!(!is_protected_path("main.rs"));
82        assert!(!is_protected_path("/tmp/output.txt"));
83        assert!(!is_protected_path(".env"));
84        assert!(!is_protected_path("config.toml"));
85        assert!(!is_protected_path("rippy.rs"));
86    }
87
88    #[test]
89    fn does_not_protect_partial_matches() {
90        assert!(!is_protected_path(".rippy_backup"));
91        assert!(!is_protected_path("not.rippy"));
92        // .rippy.toml is protected (exact basename match)
93        assert!(is_protected_path(".rippy.toml"));
94    }
95}