1use crate::exceptions::AicoError;
2use std::fs;
3use std::io::Write;
4use std::path::Component;
5use std::path::Path;
6use std::path::PathBuf;
7use tempfile::NamedTempFile;
8
9pub fn atomic_write_text<P: AsRef<Path>>(path: P, text: &str) -> Result<(), AicoError> {
11 let path = path.as_ref();
12 let dir = path.parent().unwrap_or_else(|| Path::new("."));
13 fs::create_dir_all(dir)?;
14
15 let mut temp_file = NamedTempFile::new_in(dir)?;
17
18 temp_file.write_all(text.as_bytes())?;
19
20 temp_file
22 .persist(path)
23 .map_err(|e| AicoError::Io(e.error))?;
24
25 Ok(())
26}
27
28pub fn atomic_write_json<T: serde::Serialize>(
29 path: &std::path::Path,
30 data: &T,
31) -> Result<(), crate::exceptions::AicoError> {
32 let dir = path.parent().unwrap_or_else(|| std::path::Path::new("."));
33 std::fs::create_dir_all(dir)?;
34
35 let mut temp_file = tempfile::NamedTempFile::new_in(dir)?;
36
37 {
39 let mut writer = std::io::BufWriter::new(&mut temp_file);
40 serde_json::to_writer(&mut writer, data)?;
41 writer.flush()?;
42 }
43
44 temp_file
45 .persist(path)
46 .map_err(|e| crate::exceptions::AicoError::Io(e.error))?;
47 Ok(())
48}
49
50pub fn validate_input_paths(
52 session_root: &Path,
53 file_paths: &[PathBuf],
54 require_exists: bool,
55) -> (Vec<String>, bool) {
56 let mut valid_rels = Vec::new();
57 let mut has_errors = false;
58
59 let root_canon = match fs::canonicalize(session_root) {
60 Ok(p) => p,
61 Err(_) => return (vec![], true),
62 };
63
64 let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
65
66 for path in file_paths {
67 let logical_abs_path = normalize_path(&cwd.join(path));
69
70 if require_exists {
72 if !logical_abs_path.exists() {
73 eprintln!("Error: File not found: {}", path.display());
74 has_errors = true;
75 continue;
76 }
77 if logical_abs_path.is_dir() {
78 eprintln!("Error: Cannot add a directory: {}", path.display());
79 has_errors = true;
80 continue;
81 }
82 }
83
84 let physical_target = match fs::canonicalize(&logical_abs_path) {
87 Ok(p) => p,
88 Err(_) if !require_exists => logical_abs_path.clone(), Err(_) => {
90 eprintln!("Error: Could not resolve path: {}", path.display());
91 has_errors = true;
92 continue;
93 }
94 };
95
96 if !physical_target.starts_with(&root_canon) {
97 eprintln!(
98 "Error: File '{}' is outside the session root",
99 path.display()
100 );
101 has_errors = true;
102 continue;
103 }
104
105 match logical_abs_path.strip_prefix(session_root) {
107 Ok(rel) => {
108 let rel_str = rel.to_string_lossy().replace('\\', "/");
109 valid_rels.push(rel_str);
110 }
111 Err(_) => {
112 if let Ok(rel) = physical_target.strip_prefix(&root_canon) {
115 let rel_str = rel.to_string_lossy().replace('\\', "/");
116 valid_rels.push(rel_str);
117 } else {
118 eprintln!(
119 "Error: File '{}' is logically outside the session root",
120 path.display()
121 );
122 has_errors = true;
123 }
124 }
125 }
126 }
127
128 (valid_rels, has_errors)
129}
130
131fn normalize_path(path: &Path) -> PathBuf {
132 path.components()
133 .fold(PathBuf::new(), |mut acc, component| {
134 match component {
135 Component::ParentDir => {
137 acc.pop();
138 }
139 Component::CurDir => {}
141 c => acc.push(c.as_os_str()),
143 };
144 acc
145 })
146}