soar_core/
utils.rs

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
53/// Expands the environment variables and user home directory in a given path.
54pub 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
262/// Retrieves the platform string in the format `ARCH-Os`.
263///
264/// This function combines the architecture (e.g., `x86_64`) and the operating
265/// system (e.g., `Linux`) into a single string to identify the platform.
266pub 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}