pacsea/sources/
pkgbuild.rs1use crate::logic::files::get_pkgbuild_from_cache;
4use crate::state::{PackageItem, Source};
5use crate::util::percent_encode;
6use std::sync::Mutex;
7use std::time::{Duration, Instant};
8
9type Result<T> = super::Result<T>;
11
12static PKGBUILD_RATE_LIMITER: Mutex<Option<Instant>> = Mutex::new(None);
16const PKGBUILD_MIN_INTERVAL_MS: u64 = 200;
20
21pub async fn fetch_pkgbuild_fast(item: &PackageItem) -> Result<String> {
43 let name = item.name.clone();
44
45 if let Some(cached) = tokio::task::spawn_blocking({
47 let name = name.clone();
48 move || get_pkgbuild_from_cache(&name)
49 })
50 .await?
51 {
52 tracing::debug!("Using cached PKGBUILD for {} (offline)", name);
53 return Ok(cached);
54 }
55
56 let delay = {
58 let mut last_request = PKGBUILD_RATE_LIMITER
59 .lock()
60 .expect("PKGBUILD rate limiter mutex poisoned");
61 if let Some(last) = *last_request {
62 let elapsed = last.elapsed();
63 if elapsed < Duration::from_millis(PKGBUILD_MIN_INTERVAL_MS) {
64 let delay = Duration::from_millis(PKGBUILD_MIN_INTERVAL_MS)
65 .checked_sub(elapsed)
66 .expect("elapsed should be less than PKGBUILD_MIN_INTERVAL_MS");
67 tracing::debug!(
68 "Rate limiting PKGBUILD request for {}: waiting {:?}",
69 name,
70 delay
71 );
72 *last_request = Some(Instant::now());
74 Some(delay)
75 } else {
76 *last_request = Some(Instant::now());
77 None
78 }
79 } else {
80 *last_request = Some(Instant::now());
81 None
82 }
83 };
84 if let Some(delay) = delay {
85 tokio::time::sleep(delay).await;
86 }
87
88 match &item.source {
90 Source::Aur => {
91 let url = format!(
92 "https://aur.archlinux.org/cgit/aur.git/plain/PKGBUILD?h={}",
93 percent_encode(&name)
94 );
95 let res = tokio::task::spawn_blocking({
97 let url = url.clone();
98 move || crate::util::curl::curl_text_with_args(&url, &["--max-time", "10"])
99 })
100 .await??;
101 Ok(res)
102 }
103 Source::Official { .. } => {
104 let url_main = format!(
105 "https://gitlab.archlinux.org/archlinux/packaging/packages/{}/-/raw/main/PKGBUILD",
106 percent_encode(&name)
107 );
108 let main_result = tokio::task::spawn_blocking({
109 let u = url_main.clone();
110 move || crate::util::curl::curl_text_with_args(&u, &["--max-time", "10"])
111 })
112 .await;
113 if let Ok(Ok(txt)) = main_result {
114 return Ok(txt);
115 }
116 let url_master = format!(
117 "https://gitlab.archlinux.org/archlinux/packaging/packages/{}/-/raw/master/PKGBUILD",
118 percent_encode(&name)
119 );
120 let txt = tokio::task::spawn_blocking({
121 let u = url_master;
122 move || crate::util::curl::curl_text_with_args(&u, &["--max-time", "10"])
123 })
124 .await??;
125 Ok(txt)
126 }
127 }
128}
129
130#[cfg(not(target_os = "windows"))]
131#[cfg(test)]
132mod tests {
133 use super::*;
134
135 #[tokio::test]
136 #[ignore = "Only run when explicitly mentioned"]
137 #[allow(clippy::await_holding_lock)]
138 async fn pkgbuild_fetches_aur_via_curl_text() {
139 let _guard = crate::sources::test_mutex()
140 .lock()
141 .expect("Test mutex poisoned");
142 let old_path = std::env::var("PATH").unwrap_or_default();
144 let mut root = std::env::temp_dir();
145 root.push(format!(
146 "pacsea_fake_curl_pkgbuild_{}_{}",
147 std::process::id(),
148 std::time::SystemTime::now()
149 .duration_since(std::time::UNIX_EPOCH)
150 .expect("System time is before UNIX epoch")
151 .as_nanos()
152 ));
153 std::fs::create_dir_all(&root).expect("Failed to create test root directory");
154 let mut bin = root.clone();
155 bin.push("bin");
156 std::fs::create_dir_all(&bin).expect("Failed to create test bin directory");
157 let mut curl = bin.clone();
158 curl.push("curl");
159 let script = "#!/bin/sh\necho 'pkgver=1'\n";
160 std::fs::write(&curl, script.as_bytes()).expect("Failed to write test curl script");
161 #[cfg(unix)]
162 {
163 use std::os::unix::fs::PermissionsExt;
164 let mut perm = std::fs::metadata(&curl)
165 .expect("Failed to read test curl script metadata")
166 .permissions();
167 perm.set_mode(0o755);
168 std::fs::set_permissions(&curl, perm)
169 .expect("Failed to set test curl script permissions");
170 }
171 let new_path = format!("{}:{old_path}", bin.to_string_lossy());
172 unsafe { std::env::set_var("PATH", &new_path) };
173
174 let item = PackageItem {
175 name: "yay-bin".into(),
176 version: String::new(),
177 description: String::new(),
178 source: Source::Aur,
179 popularity: None,
180 out_of_date: None,
181 orphaned: false,
182 };
183 let txt = super::fetch_pkgbuild_fast(&item)
184 .await
185 .expect("Failed to fetch PKGBUILD in test");
186 assert!(txt.contains("pkgver=1"));
187
188 unsafe { std::env::set_var("PATH", &old_path) };
189 let _ = std::fs::remove_dir_all(&root);
190 }
191
192 #[test]
193 #[allow(clippy::await_holding_lock)]
194 fn pkgbuild_fetches_official_main_then_master() {
195 let _guard = crate::global_test_mutex_lock();
196 let old_path = std::env::var("PATH").unwrap_or_default();
197 let mut root = std::env::temp_dir();
198 root.push(format!(
199 "pacsea_fake_curl_pkgbuild_official_{}_{}",
200 std::process::id(),
201 std::time::SystemTime::now()
202 .duration_since(std::time::UNIX_EPOCH)
203 .expect("System time is before UNIX epoch")
204 .as_nanos()
205 ));
206 std::fs::create_dir_all(&root).expect("Failed to create test root directory");
207 let mut bin = root.clone();
208 bin.push("bin");
209 std::fs::create_dir_all(&bin).expect("Failed to create test bin directory");
210 let mut curl = bin.clone();
211 curl.push("curl");
212 let script = "#!/bin/sh\nfor arg; do :; done\nurl=\"$arg\"\nif echo \"$url\" | grep -q '/-/raw/main/'; then exit 22; fi\nprintf 'pkgrel=2'\n";
217 std::fs::write(&curl, script.as_bytes()).expect("Failed to write test curl script");
218 #[cfg(unix)]
219 {
220 use std::os::unix::fs::PermissionsExt;
221 let mut perm = std::fs::metadata(&curl)
222 .expect("Failed to read test curl script metadata")
223 .permissions();
224 perm.set_mode(0o755);
225 std::fs::set_permissions(&curl, perm)
226 .expect("Failed to set test curl script permissions");
227 }
228
229 let mut paru = bin.clone();
231 paru.push("paru");
232 std::fs::write(&paru, b"#!/bin/sh\nexit 1\n").expect("Failed to write test paru script");
233 #[cfg(unix)]
234 {
235 use std::os::unix::fs::PermissionsExt;
236 let mut perm = std::fs::metadata(&paru)
237 .expect("Failed to read test paru script metadata")
238 .permissions();
239 perm.set_mode(0o755);
240 std::fs::set_permissions(&paru, perm)
241 .expect("Failed to set test paru script permissions");
242 }
243
244 let mut yay = bin.clone();
245 yay.push("yay");
246 std::fs::write(&yay, b"#!/bin/sh\nexit 1\n").expect("Failed to write test yay script");
247 #[cfg(unix)]
248 {
249 use std::os::unix::fs::PermissionsExt;
250 let mut perm = std::fs::metadata(&yay)
251 .expect("Failed to read test yay script metadata")
252 .permissions();
253 perm.set_mode(0o755);
254 std::fs::set_permissions(&yay, perm)
255 .expect("Failed to set test yay script permissions");
256 }
257 let new_path = format!("{}:{old_path}", bin.to_string_lossy());
258 unsafe { std::env::set_var("PATH", &new_path) };
259 unsafe { std::env::set_var("PACSEA_CURL_PATH", "1") };
261
262 let old_home = std::env::var("HOME").unwrap_or_default();
264 unsafe { std::env::set_var("HOME", root.to_string_lossy().as_ref()) };
265
266 let rt = tokio::runtime::Runtime::new().expect("Failed to create tokio runtime for test");
268 let txt = rt.block_on(async {
269 let item = PackageItem {
270 name: "ripgrep".into(),
271 version: String::new(),
272 description: String::new(),
273 source: Source::Official {
274 repo: "extra".into(),
275 arch: "x86_64".into(),
276 },
277 popularity: None,
278 out_of_date: None,
279 orphaned: false,
280 };
281 super::fetch_pkgbuild_fast(&item)
282 .await
283 .expect("Failed to fetch PKGBUILD in test")
284 });
285
286 assert!(txt.contains("pkgrel=2"));
287
288 unsafe { std::env::set_var("PATH", &old_path) };
289 unsafe { std::env::set_var("HOME", &old_home) };
290 unsafe { std::env::remove_var("PACSEA_CURL_PATH") };
291 let _ = std::fs::remove_dir_all(&root);
292 }
293}