rippy_cli/config/
loader.rs1use std::path::{Path, PathBuf};
2
3use crate::error::RippyError;
4use crate::verdict::Decision;
5
6use super::Config;
7use super::parser::{parse_action_word, parse_rule};
8use super::types::{ConfigDirective, Rule};
9
10pub(super) fn apply_setting(config: &mut Config, key: &str, value: &str) {
11 match key {
12 "default" => config.default_action = parse_action_word(value),
13 "log" => config.log_file = Some(PathBuf::from(value)),
14 "log-full" => config.log_full = true,
15 "tracking" => {
16 config.tracking_db = Some(if value == "on" || value.is_empty() {
17 home_dir().map_or_else(
18 || PathBuf::from(".rippy/tracking.db"),
19 |h| h.join(".rippy/tracking.db"),
20 )
21 } else {
22 PathBuf::from(value)
23 });
24 }
25 "trust-project-configs" => {
26 config.trust_project_configs = value != "off" && value != "false";
27 }
28 "self-protect" => {
29 config.self_protect = value != "off";
30 }
31 _ => {}
33 }
34}
35
36pub(super) fn detect_dangerous_setting(key: &str, value: &str, notes: &mut Vec<String>) {
38 if key == "default" && value == "allow" {
39 notes.push("sets default action to allow (all unknown commands auto-approved)".to_string());
40 }
41 if key == "self-protect" && value == "off" {
42 notes.push("disables self-protection (AI tools can modify rippy config)".to_string());
43 }
44}
45
46pub(super) fn detect_broad_allow(rule: &Rule, notes: &mut Vec<String>) {
48 if rule.decision != Decision::Allow {
49 return;
50 }
51 let raw = rule.pattern.raw();
52 if raw == "*" || raw == "**" || raw == "*|" {
53 notes.push(format!("allows all commands with pattern \"{raw}\""));
54 }
55}
56
57pub(super) fn build_weakening_suffix(notes: &[String]) -> String {
61 if notes.is_empty() {
62 return String::new();
63 }
64 let mut suffix = String::from(" | NOTE: project config ");
65 for (i, note) in notes.iter().enumerate() {
66 if i > 0 {
67 suffix.push_str(", ");
68 }
69 suffix.push_str(note);
70 }
71 suffix
72}
73
74pub(super) fn load_first_existing(
80 paths: &[PathBuf],
81 directives: &mut Vec<ConfigDirective>,
82) -> Result<(), RippyError> {
83 for path in paths {
84 if path.is_file() {
85 return load_file(path, directives);
86 }
87 }
88 Ok(())
89}
90
91pub fn load_file(path: &Path, directives: &mut Vec<ConfigDirective>) -> Result<(), RippyError> {
97 let content = std::fs::read_to_string(path).map_err(|e| RippyError::Config {
98 path: path.to_owned(),
99 line: 0,
100 message: format!("could not read: {e}"),
101 })?;
102
103 load_file_from_content(&content, path, directives)
104}
105
106pub(super) fn load_file_from_content(
108 content: &str,
109 path: &Path,
110 directives: &mut Vec<ConfigDirective>,
111) -> Result<(), RippyError> {
112 if path.extension().is_some_and(|ext| ext == "toml") {
113 let parsed = crate::toml_config::parse_toml_config(content, path)?;
114 directives.extend(parsed);
115 return Ok(());
116 }
117
118 for (line_num, line) in content.lines().enumerate() {
119 let line = line.trim();
120 if line.is_empty() || line.starts_with('#') {
121 continue;
122 }
123 let directive = parse_rule(line).map_err(|msg| RippyError::Config {
124 path: path.to_owned(),
125 line: line_num + 1,
126 message: msg,
127 })?;
128 directives.push(directive);
129 }
130
131 Ok(())
132}
133
134pub(super) fn has_trust_setting(directives: &[ConfigDirective]) -> bool {
136 directives.iter().rev().any(|d| {
137 matches!(
138 d,
139 ConfigDirective::Set { key, value }
140 if key == "trust-project-configs"
141 && value != "off"
142 && value != "false"
143 )
144 })
145}
146
147pub(super) fn load_project_config_if_trusted(
153 path: &Path,
154 trust_all: bool,
155 directives: &mut Vec<ConfigDirective>,
156) -> Result<(), RippyError> {
157 let content = std::fs::read_to_string(path).map_err(|e| RippyError::Config {
158 path: path.to_owned(),
159 line: 0,
160 message: format!("could not read: {e}"),
161 })?;
162
163 if trust_all {
164 return load_file_from_content(&content, path, directives);
165 }
166
167 let db = crate::trust::TrustDb::load();
168 match db.check(path, &content) {
169 crate::trust::TrustStatus::Trusted => load_file_from_content(&content, path, directives),
170 crate::trust::TrustStatus::Untrusted => {
171 eprintln!(
172 "[rippy] untrusted project config: {} — run `rippy trust` to review and enable",
173 path.display()
174 );
175 Ok(())
176 }
177 crate::trust::TrustStatus::Modified { .. } => {
178 eprintln!(
179 "[rippy] project config modified since last trust: {} — \
180 run `rippy trust` to re-approve",
181 path.display()
182 );
183 Ok(())
184 }
185 }
186}
187
188pub fn home_dir() -> Option<PathBuf> {
189 std::env::var_os("HOME").map(PathBuf::from)
190}
191
192pub(super) fn extract_package_setting(path: &Path) -> Option<String> {
198 let content = std::fs::read_to_string(path).ok()?;
199 if path.extension().is_some_and(|ext| ext == "toml") {
200 let config: crate::toml_config::TomlConfig = toml::from_str(&content).ok()?;
201 config.settings?.package
202 } else {
203 for line in content.lines() {
205 let line = line.trim();
206 if let Some(rest) = line.strip_prefix("set package ") {
207 let value = rest.trim().trim_matches('"');
208 if !value.is_empty() {
209 return Some(value.to_string());
210 }
211 }
212 }
213 None
214 }
215}