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 read_json<T: serde::de::DeserializeOwned>(
51 path: &std::path::Path,
52) -> Result<T, crate::exceptions::AicoError> {
53 let file = std::fs::File::open(path)?;
54 let reader = std::io::BufReader::new(file);
55 let data = serde_json::from_reader(reader)?;
56 Ok(data)
57}
58
59pub fn validate_input_paths(
61 session_root: &Path,
62 file_paths: &[PathBuf],
63 require_exists: bool,
64) -> (Vec<String>, bool) {
65 let mut valid_rels = Vec::new();
66 let mut has_errors = false;
67
68 let root_canon = match fs::canonicalize(session_root) {
69 Ok(p) => p,
70 Err(_) => return (vec![], true),
71 };
72
73 let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
74
75 for path in file_paths {
76 let logical_abs_path = normalize_path(&cwd.join(path));
78
79 if require_exists {
81 if !logical_abs_path.exists() {
82 eprintln!("Error: File not found: {}", path.display());
83 has_errors = true;
84 continue;
85 }
86 if logical_abs_path.is_dir() {
87 eprintln!("Error: Cannot add a directory: {}", path.display());
88 has_errors = true;
89 continue;
90 }
91 }
92
93 let physical_target = match fs::canonicalize(&logical_abs_path) {
96 Ok(p) => p,
97 Err(_) if !require_exists => logical_abs_path.clone(), Err(_) => {
99 eprintln!("Error: Could not resolve path: {}", path.display());
100 has_errors = true;
101 continue;
102 }
103 };
104
105 if !physical_target.starts_with(&root_canon) {
106 eprintln!(
107 "Error: File '{}' is outside the session root",
108 path.display()
109 );
110 has_errors = true;
111 continue;
112 }
113
114 match logical_abs_path.strip_prefix(session_root) {
116 Ok(rel) => {
117 let rel_str = rel.to_string_lossy().to_string();
118 valid_rels.push(rel_str);
119 }
120 Err(_) => {
121 if let Ok(rel) = physical_target.strip_prefix(&root_canon) {
124 let rel_str = rel.to_string_lossy().to_string();
125 valid_rels.push(rel_str);
126 } else {
127 eprintln!(
128 "Error: File '{}' is logically outside the session root",
129 path.display()
130 );
131 has_errors = true;
132 }
133 }
134 }
135 }
136
137 (valid_rels, has_errors)
138}
139
140fn normalize_path(path: &Path) -> PathBuf {
141 path.components()
142 .fold(PathBuf::new(), |mut acc, component| {
143 match component {
144 Component::ParentDir => {
146 acc.pop();
147 }
148 Component::CurDir => {}
150 c => acc.push(c.as_os_str()),
152 };
153 acc
154 })
155}