1use std::fs;
2use std::path::{Path, PathBuf};
3use tempfile::TempDir;
4
5use crate::constants;
6
7fn build_fallback_dir(base_dir: &Path) -> PathBuf {
11 constants::FALLBACK_TEMP_DIR
12 .split('/')
13 .fold(base_dir.to_path_buf(), |acc, component| acc.join(component))
14}
15
16pub fn get_temp_dir() -> std::io::Result<PathBuf> {
20 let tmp_path = TempDir::with_prefix("protonup-rs-").map(|dir| dir.keep());
22
23 if let Ok(temp_dir) = tmp_path {
24 let available_space = fs4::available_space(&temp_dir).unwrap_or(0);
25
26 if available_space >= constants::MIN_TEMP_SPACE_BYTES {
27 return Ok(temp_dir);
29 }
30 }
31
32 let fallback_dir = match dirs::state_dir() {
34 Some(state_dir) => build_fallback_dir(&state_dir),
35 None => {
36 let home_dir = dirs::home_dir().unwrap_or_else(|| PathBuf::from("."));
38 build_fallback_dir(&home_dir)
39 }
40 };
41
42 fs::create_dir_all(&fallback_dir)?;
44
45 Ok(fallback_dir)
46}
47
48pub fn create_download_temp_dir(version: &str, download_url: &str) -> std::io::Result<PathBuf> {
52 let temp_dir = get_temp_dir()?;
53
54 let download_dir = temp_dir.join(format!("{}.{}", version, "download"));
55
56 fs::create_dir_all(&download_dir)?;
58
59 let ext = crate::files::check_supported_extension(download_url)
61 .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidInput, e.to_string()))?;
62
63 let download_path = download_dir.join(format!("{}.{}", version, ext));
64
65 Ok(download_path)
66}
67
68pub fn cleanup_fallback_temp_dir() -> std::io::Result<()> {
71 let fallback_dir = match dirs::state_dir() {
72 Some(state_dir) => build_fallback_dir(&state_dir),
73 None => {
74 let home_dir = dirs::home_dir().unwrap_or_else(|| PathBuf::from("."));
75 build_fallback_dir(&home_dir)
76 }
77 };
78
79 if fallback_dir.exists() {
80 for entry in fs::read_dir(&fallback_dir)? {
82 let entry = entry?;
83 let path = entry.path();
84 if path.is_dir() {
85 fs::remove_dir_all(&path)?;
86 } else {
87 fs::remove_file(&path)?;
88 }
89 }
90 }
91
92 Ok(())
93}
94
95pub fn expand_tilde<P: AsRef<Path>>(path_user_input: P) -> Option<PathBuf> {
96 let p = path_user_input.as_ref();
97 if !p.starts_with("~") {
98 return Some(p.to_path_buf());
99 }
100 if p == Path::new("~") {
101 return dirs::home_dir();
102 }
103 dirs::home_dir().map(|mut h| {
104 if h == Path::new("/") {
105 p.strip_prefix("~").unwrap().to_path_buf()
108 } else {
109 h.push(p.strip_prefix("~/").unwrap());
110 h
111 }
112 })
113}
114
115pub fn match_version(version_str: &str, tag_name: &str) -> bool {
123 if version_str.is_empty() {
125 return false;
126 }
127
128 if tag_name == version_str {
130 return true;
131 }
132
133 if tag_name.starts_with(version_str) {
135 return true;
136 }
137
138 let user_components: Vec<&str> = version_str
140 .split(|c: char| !c.is_ascii_digit())
141 .filter(|s| !s.is_empty())
142 .collect();
143
144 if user_components.is_empty() {
145 return false;
146 }
147
148 let tag_components: Vec<&str> = tag_name
150 .split(|c: char| !c.is_ascii_digit())
151 .filter(|s| !s.is_empty())
152 .collect();
153
154 if tag_components
157 .windows(user_components.len())
158 .any(|window| window == user_components.as_slice())
159 {
160 return true;
161 }
162
163 if user_components.len() == 1 {
166 return tag_components.contains(&user_components[0]);
167 }
168
169 false
170}
171
172#[cfg(test)]
173mod tests {
174 use super::*;
175
176 const MATCH_TEST_CASES: &[(&str, &str, bool)] = &[
179 ("v2.7.1", "v2.7.1", true),
181 ("v2.14", "v2.14", true),
182 ("v0.5.4", "v0.5.4", true),
183 ("10.0", "cachyos-10.0-20260228-slr", true),
186 ("10", "cachyos-10.0-20260228-slr", true),
187 ("20260228", "cachyos-10.0-20260228-slr", true),
188 ("10.1", "cachyos-10.0-20260228-slr", false),
189 ("10.26", "GE-Proton10-26-rtsp20", true),
192 ("10", "GE-Proton10-26-rtsp20", true),
193 ("26", "GE-Proton10-26-rtsp20", true),
194 ("20", "GE-Proton10-26-rtsp20", true), ("10.27", "GE-Proton10-26-rtsp20", false),
196 ("8.26", "GE-Proton8-26", true),
199 ("8", "GE-Proton8-26", true),
200 ("26", "GE-Proton8-26", true),
201 ("10.32", "GE-Proton10-32", true),
202 ("10", "GE-Proton10-32", true),
203 ("32", "GE-Proton10-32", true),
204 ("9.26", "GE-Proton8-26", false),
205 ("2.7.1", "v2.7.1", true),
208 ("2.7", "v2.7.1", true), ("2", "v2.7.1", true),
210 ("2.14", "v2.14", true),
211 ("2", "v2.14", true),
212 ("0.5.4", "v0.5.4", true),
213 ("0.5", "v0.5.4", true),
214 ("3.0", "v3.0b", true), ("3", "v3.0b", true),
216 ("76.2.0", "v76.2.0", true),
217 ("76.2", "v76.2.0", true),
218 ("76", "v76.2.0", true),
219 ("", "v2.7.1", false), ("abc", "v2.7.1", false), ("2.7.1", "v2.7.2", false), ("2.8", "v2.7.1", false), ("3", "v2.7.1", false), ("v2", "v2.7.1", true),
229 ("GE-Proton8", "GE-Proton8-26", true),
230 ("cachyos", "cachyos-10.0-20260228-slr", true),
231 ];
232
233 #[test]
234 fn test_match_version() {
235 for (user_input, tag_name, expected) in MATCH_TEST_CASES {
236 let result = match_version(user_input, tag_name);
237 assert_eq!(
238 result, *expected,
239 "match_version(\"{}\", \"{}\") - expected {}, got {}",
240 user_input, tag_name, expected, result
241 );
242 }
243 }
244
245 #[test]
246 fn test_match_version_geproton_variants() {
247 assert!(match_version("8-26", "GE-Proton8-26"));
249 assert!(match_version("10-32", "GE-Proton10-32"));
250 assert!(!match_version("8-27", "GE-Proton8-26"));
251 assert!(match_version("8-27", "GE-Proton8-27-LoL"));
254 assert!(match_version("8.27", "GE-Proton8-27-LoL"));
255 }
256
257 #[test]
258 fn test_match_version_component_extraction() {
259 assert!(match_version("1.2.3", "tool-1.2.3-release"));
261 assert!(match_version("1.2", "tool-1.2.3-release"));
262 assert!(match_version("1", "tool-1.2.3-release"));
263 assert!(!match_version("2.3.4", "tool-1.2.3-release")); assert!(!match_version("4.5.6", "tool-1.2.3-release")); }
266
267 #[test]
268 fn test_match_version_prefix_fallback() {
269 assert!(match_version("GE-Proton", "GE-Proton8-26"));
271 assert!(match_version("v2", "v2.7.1"));
272 assert!(match_version("cachyos-", "cachyos-10.0-20260228-slr"));
273 }
274
275 #[test]
276 fn test_match_version_consecutive_components() {
277 assert!(match_version("10.0", "cachyos-10.0-20260228-slr"));
279 assert!(match_version("10.26", "GE-Proton10-26-rtsp20"));
280 assert!(!match_version("10.27", "GE-Proton10-26-rtsp20"));
281 assert!(match_version("26.20", "GE-Proton10-26-rtsp20"));
286 }
287}