1use std::{
2 env::{
3 self,
4 consts::{ARCH, OS},
5 },
6 fs::{self, File},
7 io::{self, BufReader, Read, Seek},
8 os,
9 path::{Path, PathBuf},
10};
11
12use nix::unistd::{geteuid, User};
13use regex::Regex;
14use tracing::info;
15
16use crate::{
17 config::get_config,
18 error::{ErrorContext, SoarError},
19 SoarResult,
20};
21
22type Result<T> = std::result::Result<T, SoarError>;
23
24fn get_username() -> Result<String> {
25 let uid = geteuid();
26 User::from_uid(uid)?
27 .ok_or_else(|| panic!("Failed to get user"))
28 .map(|user| user.name)
29}
30
31pub fn home_path() -> String {
32 env::var("HOME").unwrap_or_else(|_| {
33 let username = env::var("USER")
34 .or_else(|_| env::var("LOGNAME"))
35 .or_else(|_| get_username().map_err(|_| ()))
36 .unwrap_or_else(|_| panic!("Couldn't determine username. Please fix the system."));
37 format!("/home/{username}")
38 })
39}
40
41pub fn home_config_path() -> String {
42 env::var("XDG_CONFIG_HOME").unwrap_or(format!("{}/.config", home_path()))
43}
44
45pub fn home_cache_path() -> String {
46 env::var("XDG_CACHE_HOME").unwrap_or(format!("{}/.cache", home_path()))
47}
48
49pub fn home_data_path() -> String {
50 env::var("XDG_DATA_HOME").unwrap_or(format!("{}/.local/share", home_path()))
51}
52
53pub fn build_path(path: &str) -> Result<PathBuf> {
55 let mut result = String::new();
56 let mut chars = path.chars().peekable();
57
58 while let Some(c) = chars.next() {
59 if c == '$' {
60 let mut var_name = String::new();
61 while let Some(&c) = chars.peek() {
62 if !c.is_alphanumeric() && c != '_' {
63 break;
64 }
65 var_name.push(chars.next().unwrap());
66 }
67 if !var_name.is_empty() {
68 let expanded = if var_name == "HOME" {
69 home_path()
70 } else {
71 env::var(&var_name)?
72 };
73 result.push_str(&expanded);
74 } else {
75 result.push('$');
76 }
77 } else if c == '~' && result.is_empty() {
78 result.push_str(&home_path())
79 } else {
80 result.push(c);
81 }
82 }
83
84 Ok(PathBuf::from(result))
85}
86
87pub fn format_bytes(bytes: u64) -> String {
88 let kb = 1024u64;
89 let mb = kb * 1024;
90 let gb = mb * 1024;
91
92 match bytes {
93 b if b >= gb => format!("{:.2} GiB", b as f64 / gb as f64),
94 b if b >= mb => format!("{:.2} MiB", b as f64 / mb as f64),
95 b if b >= kb => format!("{:.2} KiB", b as f64 / kb as f64),
96 _ => format!("{bytes} B"),
97 }
98}
99
100pub fn parse_size(size_str: &str) -> Option<u64> {
101 let size_str = size_str.trim();
102 let units = [
103 ("B", 1u64),
104 ("KB", 1000u64),
105 ("MB", 1000u64 * 1000),
106 ("GB", 1000u64 * 1000 * 1000),
107 ("KiB", 1024u64),
108 ("MiB", 1024u64 * 1024),
109 ("GiB", 1024u64 * 1024 * 1024),
110 ];
111
112 for (unit, multiplier) in &units {
113 let size_str = size_str.to_uppercase();
114 if size_str.ends_with(unit) {
115 let number_part = size_str.trim_end_matches(unit).trim();
116 if let Ok(num) = number_part.parse::<f64>() {
117 return Some((num * (*multiplier as f64)) as u64);
118 }
119 }
120 }
121
122 None
123}
124
125pub fn calculate_checksum<P: AsRef<Path>>(file_path: P) -> Result<String> {
126 let file_path = file_path.as_ref();
127 let mut hasher = blake3::Hasher::new();
128 hasher
129 .update_mmap(file_path)
130 .with_context(|| format!("reading {} using memory mapping", file_path.display()))?;
131 Ok(hasher.finalize().to_hex().to_string())
132}
133
134pub fn setup_required_paths() -> Result<()> {
135 let config = get_config();
136 let bin_path = config.get_bin_path()?;
137 if !bin_path.exists() {
138 fs::create_dir_all(&bin_path)
139 .with_context(|| format!("creating bin directory {}", bin_path.display()))?;
140 }
141
142 let db_path = config.get_db_path()?;
143 if !db_path.exists() {
144 fs::create_dir_all(&db_path)
145 .with_context(|| format!("creating database directory {}", db_path.display()))?;
146 }
147
148 for profile in config.profile.values() {
149 let packages_path = profile.get_packages_path()?;
150 if !packages_path.exists() {
151 fs::create_dir_all(&packages_path).with_context(|| {
152 format!("creating packages directory {}", packages_path.display())
153 })?;
154 }
155 }
156
157 Ok(())
158}
159
160pub fn calc_magic_bytes<P: AsRef<Path>>(file_path: P, size: usize) -> Result<Vec<u8>> {
161 let file_path = file_path.as_ref();
162 let file = File::open(file_path).with_context(|| format!("opening {}", file_path.display()))?;
163 let mut file = BufReader::new(file);
164 let mut magic_bytes = vec![0u8; size];
165 file.read_exact(&mut magic_bytes)
166 .with_context(|| format!("reading magic bytes from {}", file_path.display()))?;
167 file.rewind().unwrap();
168 Ok(magic_bytes)
169}
170
171pub fn create_symlink<P: AsRef<Path>>(from: P, to: P) -> SoarResult<()> {
172 let from = from.as_ref();
173 let to = to.as_ref();
174
175 if let Some(parent) = to.parent() {
176 fs::create_dir_all(parent)
177 .with_context(|| format!("creating parent directory {}", parent.display()))?;
178 }
179
180 if to.is_symlink() {
181 fs::remove_file(to).with_context(|| format!("removing symlink {}", to.display()))?;
182 }
183 os::unix::fs::symlink(from, to)
184 .with_context(|| format!("creating symlink {} -> {}", from.display(), to.display()))?;
185 Ok(())
186}
187
188pub fn cleanup_cache() -> Result<()> {
189 let cache_path = get_config().get_cache_path()?;
190 if cache_path.exists() {
191 fs::remove_dir_all(&cache_path)
192 .with_context(|| format!("removing directory {}", cache_path.display()))?;
193 info!("Nuked cache directory: {}", cache_path.display());
194 } else {
195 info!("Cache directory is clean.");
196 }
197
198 Ok(())
199}
200
201pub fn process_dir<P: AsRef<Path>, F>(dir: P, action: &mut F) -> Result<()>
202where
203 F: FnMut(&Path) -> Result<()>,
204{
205 let dir = dir.as_ref();
206 if !dir.is_dir() {
207 return Ok(());
208 }
209
210 for entry in
211 fs::read_dir(dir).with_context(|| format!("reading directory {}", dir.display()))?
212 {
213 let path = entry
214 .with_context(|| format!("reading entry from directory {}", dir.display()))?
215 .path();
216
217 if path.is_dir() {
218 process_dir(&path, action)?;
219 continue;
220 }
221
222 action(&path)?;
223 }
224
225 Ok(())
226}
227
228fn remove_action(path: &Path) -> Result<()> {
229 if !path.exists() {
230 fs::remove_file(path)
231 .with_context(|| format!("removing broken symlink {}", path.display()))?;
232 info!("Removed broken symlink: {}", path.display());
233 }
234 Ok(())
235}
236
237pub fn remove_broken_symlinks() -> Result<()> {
238 let mut soar_files_action = |path: &Path| -> SoarResult<()> {
239 if let Some(filename) = path.file_stem().and_then(|s| s.to_str()) {
240 if filename.ends_with("-soar") {
241 return remove_action(path);
242 }
243 }
244 Ok(())
245 };
246
247 process_dir(&get_config().get_bin_path()?, &mut remove_action)?;
248 process_dir(desktop_dir(), &mut soar_files_action)?;
249 process_dir(icons_dir(), &mut soar_files_action)?;
250
251 Ok(())
252}
253
254pub fn desktop_dir() -> String {
255 format!("{}/applications", home_data_path())
256}
257
258pub fn icons_dir() -> String {
259 format!("{}/icons/hicolor", home_data_path())
260}
261
262pub fn get_platform() -> String {
267 format!("{}-{}{}", ARCH, &OS[..1].to_uppercase(), &OS[1..])
268}
269
270pub fn calculate_dir_size<P: AsRef<Path>>(path: P) -> io::Result<u64> {
271 let mut total_size = 0;
272 let path = path.as_ref();
273
274 if path.is_dir() {
275 for entry in fs::read_dir(path)? {
276 let Ok(entry) = entry else {
277 continue;
278 };
279 let Ok(metadata) = entry.metadata() else {
280 continue;
281 };
282
283 if metadata.is_file() {
284 total_size += metadata.len();
285 } else if metadata.is_dir() {
286 total_size += calculate_dir_size(entry.path())?;
287 }
288 }
289 }
290
291 Ok(total_size)
292}
293
294pub fn parse_duration(input: &str) -> Option<u128> {
295 let re = Regex::new(r"(\d+)([smhd])").ok()?;
296 let mut total: u128 = 0;
297
298 for cap in re.captures_iter(input) {
299 let number: u128 = cap[1].parse().ok()?;
300 let multiplier = match &cap[2] {
301 "s" => 1000,
302 "m" => 60 * 1000,
303 "h" => 60 * 60 * 1000,
304 "d" => 24 * 60 * 60 * 1000,
305 _ => return None,
306 };
307 total += number * multiplier;
308 }
309
310 Some(total)
311}
312
313pub fn default_install_patterns() -> Vec<String> {
314 ["!*.log", "!SBUILD", "!*.json", "!*.version"]
315 .into_iter()
316 .map(String::from)
317 .collect::<Vec<String>>()
318}
319
320pub fn get_extract_dir<P: AsRef<Path>>(base_dir: P) -> PathBuf {
321 let base_dir = base_dir.as_ref();
322 base_dir.join("SOAR_AUTOEXTRACT")
323}
324
325pub fn apply_sig_variants(patterns: Vec<String>) -> Vec<String> {
326 patterns
327 .into_iter()
328 .map(|pat| {
329 let (negate, inner) = if let Some(rest) = pat.strip_prefix('!') {
330 (true, rest)
331 } else {
332 (false, pat.as_str())
333 };
334
335 let sig_variant = format!("{inner}.sig");
336 let brace_pattern = format!("{{{inner},{sig_variant}}}");
337
338 if negate {
339 format!("!{brace_pattern}")
340 } else {
341 brace_pattern
342 }
343 })
344 .collect()
345}