1use std::path::{Path, PathBuf};
4use std::process::ExitCode;
5
6use crate::cli::MigrateArgs;
7use crate::config::ConfigDirective;
8use crate::error::RippyError;
9use crate::toml_config::rules_to_toml;
10
11pub fn run(args: &MigrateArgs) -> Result<ExitCode, RippyError> {
18 let input_path = resolve_input_path(args)?;
19 let rules = parse_legacy_file(&input_path)?;
20 let toml_output = rules_to_toml(&rules);
21
22 if args.stdout {
23 print!("{toml_output}");
24 } else {
25 let output_path = derive_output_path(&input_path);
26 std::fs::write(&output_path, &toml_output).map_err(|e| {
27 RippyError::Setup(format!("could not write {}: {e}", output_path.display()))
28 })?;
29 crate::trust::TrustGuard::for_new_file(&output_path).commit();
30 eprintln!(
31 "[rippy] Converted {} -> {}",
32 input_path.display(),
33 output_path.display()
34 );
35 }
36
37 Ok(ExitCode::SUCCESS)
38}
39
40fn resolve_input_path(args: &MigrateArgs) -> Result<PathBuf, RippyError> {
42 if let Some(path) = &args.path {
43 return Ok(path.clone());
44 }
45
46 let cwd = std::env::current_dir().map_err(|e| RippyError::Setup(format!("no cwd: {e}")))?;
48 let mut dir = cwd.as_path();
49 loop {
50 let rippy = dir.join(".rippy");
51 if rippy.is_file() {
52 return Ok(rippy);
53 }
54 let dippy = dir.join(".dippy");
55 if dippy.is_file() {
56 return Ok(dippy);
57 }
58 dir = dir
59 .parent()
60 .ok_or_else(|| RippyError::Setup("no .rippy or .dippy config found".to_string()))?;
61 }
62}
63
64fn derive_output_path(input: &Path) -> PathBuf {
66 input
67 .parent()
68 .unwrap_or_else(|| Path::new("."))
69 .join(".rippy.toml")
70}
71
72fn parse_legacy_file(path: &Path) -> Result<Vec<ConfigDirective>, RippyError> {
74 let content = std::fs::read_to_string(path).map_err(|e| RippyError::Config {
75 path: path.to_owned(),
76 line: 0,
77 message: format!("could not read: {e}"),
78 })?;
79
80 let mut directives = Vec::new();
81 for (line_num, line) in content.lines().enumerate() {
82 let line = line.trim();
83 if line.is_empty() || line.starts_with('#') {
84 continue;
85 }
86 let directive = crate::config::parse_rule(line).map_err(|msg| RippyError::Config {
87 path: path.to_owned(),
88 line: line_num + 1,
89 message: msg,
90 })?;
91 directives.push(directive);
92 }
93
94 Ok(directives)
95}
96
97#[cfg(test)]
98#[allow(clippy::unwrap_used)]
99mod tests {
100 use super::*;
101
102 #[test]
103 fn migrate_basic_config() {
104 let dir = tempfile::TempDir::new().unwrap();
105 let input = dir.path().join(".rippy");
106 std::fs::write(
107 &input,
108 "allow git status\ndeny rm -rf \"use trash instead\"\nset default ask\n",
109 )
110 .unwrap();
111
112 let directives = parse_legacy_file(&input).unwrap();
113 let toml = rules_to_toml(&directives);
114
115 let re_parsed =
117 crate::toml_config::parse_toml_config(&toml, Path::new("test.toml")).unwrap();
118 let config = crate::config::Config::from_directives(re_parsed);
119 assert_eq!(
120 config.match_command("git status", None).unwrap().decision,
121 crate::verdict::Decision::Allow,
122 );
123 assert_eq!(
124 config.match_command("rm -rf /tmp", None).unwrap().decision,
125 crate::verdict::Decision::Deny,
126 );
127 assert_eq!(config.default_action, Some(crate::verdict::Decision::Ask));
128 }
129
130 #[test]
131 fn derive_output_path_sibling() {
132 let out = derive_output_path(Path::new("/home/user/project/.rippy"));
133 assert_eq!(out, PathBuf::from("/home/user/project/.rippy.toml"));
134 }
135
136 #[test]
137 fn derive_output_path_bare() {
138 let out = derive_output_path(Path::new(".rippy"));
139 assert_eq!(out, PathBuf::from(".rippy.toml"));
140 }
141}