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