1use libloading::Library;
2use tracing::{debug, info};
3use serde_json::Value;
4use std::ffi::CStr;
5use std::fs::{self, File};
6use std::io;
7use std::mem::ManuallyDrop;
8use std::os::raw::{c_char, c_int, c_long, c_short, c_uint, c_ulong, c_void};
9use std::path::{Path, PathBuf};
10use std::process::Command;
11use std::sync::{Arc, OnceLock};
12use zip::ZipArchive;
13
14pub type CurlCode = c_int;
15pub type CurlOption = c_uint;
16pub type CurlMCode = c_int;
17pub type CurlMOption = c_int;
18
19#[cfg(windows)]
20pub type CurlSocket = usize;
21#[cfg(not(windows))]
22pub type CurlSocket = c_int;
23
24pub type CurlMultiSocketCallback =
25 unsafe extern "C" fn(*mut Curl, CurlSocket, c_int, *mut c_void, *mut c_void) -> c_int;
26pub type CurlMultiTimerCallback =
27 unsafe extern "C" fn(*mut CurlMulti, c_long, *mut c_void) -> c_int;
28
29#[repr(C)]
30pub struct Curl {
31 _private: [u8; 0],
32}
33
34#[repr(C)]
35pub struct CurlMulti {
36 _private: [u8; 0],
37}
38
39#[repr(C)]
40pub struct CurlSlist {
41 pub data: *mut c_char,
42 pub next: *mut CurlSlist,
43}
44
45#[repr(C)]
46pub struct CurlWsFrame {
47 pub age: c_int,
48 pub flags: c_int,
49 pub offset: i64,
50 pub bytesleft: i64,
51 pub len: usize,
52}
53
54#[repr(C)]
55pub union CurlMessageData {
56 pub whatever: *mut c_void,
57 pub result: CurlCode,
58}
59
60#[repr(C)]
61pub struct CurlMessage {
62 pub msg: c_int,
63 pub easy_handle: *mut Curl,
64 pub data: CurlMessageData,
65}
66
67#[repr(C)]
68pub struct CurlWaitFd {
69 pub fd: CurlSocket,
70 pub events: c_short,
71 pub revents: c_short,
72}
73
74pub const CURLE_OK: CurlCode = 0;
75pub const CURLE_AGAIN: CurlCode = 81;
76pub const CURL_GLOBAL_DEFAULT: c_ulong = 3;
77pub const CURLM_OK: CurlMCode = 0;
78pub const CURLMSG_DONE: c_int = 1;
79
80#[cfg(windows)]
81pub const CURL_SOCKET_TIMEOUT: CurlSocket = usize::MAX;
82#[cfg(not(windows))]
83pub const CURL_SOCKET_TIMEOUT: CurlSocket = -1;
84
85pub const CURL_CSELECT_IN: c_int = 0x01;
86pub const CURL_CSELECT_OUT: c_int = 0x02;
87pub const CURL_CSELECT_ERR: c_int = 0x04;
88
89pub const CURL_POLL_NONE: c_int = 0;
90pub const CURL_POLL_IN: c_int = 1;
91pub const CURL_POLL_OUT: c_int = 2;
92pub const CURL_POLL_INOUT: c_int = 3;
93pub const CURL_POLL_REMOVE: c_int = 4;
94
95pub const CURLMOPT_SOCKETFUNCTION: CurlMOption = 20001;
96pub const CURLMOPT_SOCKETDATA: CurlMOption = 10002;
97pub const CURLMOPT_TIMERFUNCTION: CurlMOption = 20004;
98pub const CURLMOPT_TIMERDATA: CurlMOption = 10005;
99
100pub const CURLOPT_URL: CurlOption = 10002;
101pub const CURLOPT_HTTPHEADER: CurlOption = 10023;
102pub const CURLOPT_HTTP_VERSION: CurlOption = 84;
103pub const CURLOPT_CONNECT_ONLY: CurlOption = 141;
104pub const CURLOPT_VERBOSE: CurlOption = 41;
105pub const CURLOPT_PROXY: CurlOption = 10004;
106
107pub const CURL_HTTP_VERSION_1_1: c_long = 2;
108pub const CURLWS_TEXT: c_uint = 1;
109
110#[derive(Debug, thiserror::Error)]
111pub enum SysError {
112 #[error("failed to load dynamic library {path}: {source}")]
113 LoadLibrary {
114 path: PathBuf,
115 #[source]
116 source: libloading::Error,
117 },
118 #[error("missing symbol {name}: {source}")]
119 MissingSymbol {
120 name: String,
121 #[source]
122 source: libloading::Error,
123 },
124 #[error("CURL_IMPERSONATE_LIB points to a missing file: {0}")]
125 MissingEnvPath(PathBuf),
126 #[error("failed to locate libcurl-impersonate. searched: {0:?}")]
127 LibraryNotFound(Vec<PathBuf>),
128 #[error(
129 "failed to locate libcurl-impersonate after auto-fetch attempt. searched: {searched:?}; auto-fetch error: {auto_fetch_error}"
130 )]
131 LibraryNotFoundAfterAutoFetch {
132 searched: Vec<PathBuf>,
133 auto_fetch_error: String,
134 },
135 #[error("auto-fetch is not supported on target: {0}")]
136 AutoFetchUnsupportedTarget(String),
137 #[error("auto-fetch needs cache directory but HOME and IMPCURL_LIB_DIR are not set")]
138 AutoFetchCacheDirUnavailable,
139 #[error("failed to run downloader command {command}: {source}")]
140 AutoFetchCommandSpawn { command: String, source: io::Error },
141 #[error("downloader command {command} failed with status {status:?}: {stderr}")]
142 AutoFetchCommandFailed {
143 command: String,
144 status: Option<i32>,
145 stderr: String,
146 },
147 #[error("failed to parse GitHub release JSON: {0}")]
148 AutoFetchJson(#[from] serde_json::Error),
149 #[error("I/O error during auto-fetch: {0}")]
150 AutoFetchIo(#[from] io::Error),
151 #[error("failed to extract wheel archive: {0}")]
152 AutoFetchWheel(#[from] zip::result::ZipError),
153 #[error("no matching curl_cffi wheel asset for version={version}, platform_tag={platform_tag}")]
154 AutoFetchWheelAssetNotFound {
155 version: String,
156 platform_tag: String,
157 },
158 #[error(
159 "wheel extracted shared objects into {cache_dir}, but no standalone libcurl-impersonate runtime was found"
160 )]
161 AutoFetchNoStandaloneRuntime { cache_dir: PathBuf },
162}
163
164pub struct CurlApi {
165 _lib: ManuallyDrop<Library>,
168 pub global_init: unsafe extern "C" fn(c_ulong) -> CurlCode,
169 pub global_cleanup: unsafe extern "C" fn(),
170 pub easy_init: unsafe extern "C" fn() -> *mut Curl,
171 pub easy_cleanup: unsafe extern "C" fn(*mut Curl),
172 pub easy_perform: unsafe extern "C" fn(*mut Curl) -> CurlCode,
173 pub easy_setopt: unsafe extern "C" fn(*mut Curl, CurlOption, ...) -> CurlCode,
174 pub easy_strerror: unsafe extern "C" fn(CurlCode) -> *const c_char,
175 pub easy_impersonate: unsafe extern "C" fn(*mut Curl, *const c_char, c_int) -> CurlCode,
176 pub slist_append: unsafe extern "C" fn(*mut CurlSlist, *const c_char) -> *mut CurlSlist,
177 pub slist_free_all: unsafe extern "C" fn(*mut CurlSlist),
178 pub ws_send:
179 unsafe extern "C" fn(*mut Curl, *const c_void, usize, *mut usize, i64, c_uint) -> CurlCode,
180 pub ws_recv: unsafe extern "C" fn(
181 *mut Curl,
182 *mut c_void,
183 usize,
184 *mut usize,
185 *mut *const CurlWsFrame,
186 ) -> CurlCode,
187 pub multi_init: unsafe extern "C" fn() -> *mut CurlMulti,
188 pub multi_cleanup: unsafe extern "C" fn(*mut CurlMulti) -> CurlMCode,
189 pub multi_setopt: unsafe extern "C" fn(*mut CurlMulti, CurlMOption, ...) -> CurlMCode,
190 pub multi_add_handle: unsafe extern "C" fn(*mut CurlMulti, *mut Curl) -> CurlMCode,
191 pub multi_remove_handle: unsafe extern "C" fn(*mut CurlMulti, *mut Curl) -> CurlMCode,
192 pub multi_fdset: unsafe extern "C" fn(
193 *mut CurlMulti,
194 *mut c_void,
195 *mut c_void,
196 *mut c_void,
197 *mut c_int,
198 ) -> CurlMCode,
199 pub multi_timeout: unsafe extern "C" fn(*mut CurlMulti, *mut c_long) -> CurlMCode,
200 pub multi_perform: unsafe extern "C" fn(*mut CurlMulti, *mut c_int) -> CurlMCode,
201 pub multi_poll: unsafe extern "C" fn(
202 *mut CurlMulti,
203 *mut CurlWaitFd,
204 c_uint,
205 c_int,
206 *mut c_int,
207 ) -> CurlMCode,
208 pub multi_socket_action:
209 unsafe extern "C" fn(*mut CurlMulti, CurlSocket, c_int, *mut c_int) -> CurlMCode,
210 pub multi_info_read: unsafe extern "C" fn(*mut CurlMulti, *mut c_int) -> *mut CurlMessage,
211 pub multi_strerror: unsafe extern "C" fn(CurlMCode) -> *const c_char,
212}
213
214impl CurlApi {
215 pub unsafe fn load(path: &Path) -> Result<Self, SysError> {
218 debug!(path = %path.display(), "loading curl-impersonate library");
219 let lib = unsafe { Library::new(path) }.map_err(|source| SysError::LoadLibrary {
220 path: path.to_path_buf(),
221 source,
222 })?;
223
224 let global_init = unsafe {
225 load_symbol::<unsafe extern "C" fn(c_ulong) -> CurlCode>(&lib, b"curl_global_init\0")?
226 };
227 let global_cleanup =
228 unsafe { load_symbol::<unsafe extern "C" fn()>(&lib, b"curl_global_cleanup\0")? };
229 let easy_init = unsafe {
230 load_symbol::<unsafe extern "C" fn() -> *mut Curl>(&lib, b"curl_easy_init\0")?
231 };
232 let easy_cleanup = unsafe {
233 load_symbol::<unsafe extern "C" fn(*mut Curl)>(&lib, b"curl_easy_cleanup\0")?
234 };
235 let easy_perform = unsafe {
236 load_symbol::<unsafe extern "C" fn(*mut Curl) -> CurlCode>(
237 &lib,
238 b"curl_easy_perform\0",
239 )?
240 };
241 let easy_setopt = unsafe {
242 load_symbol::<unsafe extern "C" fn(*mut Curl, CurlOption, ...) -> CurlCode>(
243 &lib,
244 b"curl_easy_setopt\0",
245 )?
246 };
247 let easy_strerror = unsafe {
248 load_symbol::<unsafe extern "C" fn(CurlCode) -> *const c_char>(
249 &lib,
250 b"curl_easy_strerror\0",
251 )?
252 };
253 let easy_impersonate = unsafe {
254 load_symbol::<unsafe extern "C" fn(*mut Curl, *const c_char, c_int) -> CurlCode>(
255 &lib,
256 b"curl_easy_impersonate\0",
257 )?
258 };
259 let slist_append = unsafe {
260 load_symbol::<unsafe extern "C" fn(*mut CurlSlist, *const c_char) -> *mut CurlSlist>(
261 &lib,
262 b"curl_slist_append\0",
263 )?
264 };
265 let slist_free_all = unsafe {
266 load_symbol::<unsafe extern "C" fn(*mut CurlSlist)>(&lib, b"curl_slist_free_all\0")?
267 };
268 let ws_send = unsafe {
269 load_symbol::<
270 unsafe extern "C" fn(
271 *mut Curl,
272 *const c_void,
273 usize,
274 *mut usize,
275 i64,
276 c_uint,
277 ) -> CurlCode,
278 >(&lib, b"curl_ws_send\0")?
279 };
280 let ws_recv = unsafe {
281 load_symbol::<
282 unsafe extern "C" fn(
283 *mut Curl,
284 *mut c_void,
285 usize,
286 *mut usize,
287 *mut *const CurlWsFrame,
288 ) -> CurlCode,
289 >(&lib, b"curl_ws_recv\0")?
290 };
291 let multi_init = unsafe {
292 load_symbol::<unsafe extern "C" fn() -> *mut CurlMulti>(&lib, b"curl_multi_init\0")?
293 };
294 let multi_cleanup = unsafe {
295 load_symbol::<unsafe extern "C" fn(*mut CurlMulti) -> CurlMCode>(
296 &lib,
297 b"curl_multi_cleanup\0",
298 )?
299 };
300 let multi_setopt = unsafe {
301 load_symbol::<unsafe extern "C" fn(*mut CurlMulti, CurlMOption, ...) -> CurlMCode>(
302 &lib,
303 b"curl_multi_setopt\0",
304 )?
305 };
306 let multi_add_handle = unsafe {
307 load_symbol::<unsafe extern "C" fn(*mut CurlMulti, *mut Curl) -> CurlMCode>(
308 &lib,
309 b"curl_multi_add_handle\0",
310 )?
311 };
312 let multi_remove_handle = unsafe {
313 load_symbol::<unsafe extern "C" fn(*mut CurlMulti, *mut Curl) -> CurlMCode>(
314 &lib,
315 b"curl_multi_remove_handle\0",
316 )?
317 };
318 let multi_fdset = unsafe {
319 load_symbol::<
320 unsafe extern "C" fn(
321 *mut CurlMulti,
322 *mut c_void,
323 *mut c_void,
324 *mut c_void,
325 *mut c_int,
326 ) -> CurlMCode,
327 >(&lib, b"curl_multi_fdset\0")?
328 };
329 let multi_timeout = unsafe {
330 load_symbol::<unsafe extern "C" fn(*mut CurlMulti, *mut c_long) -> CurlMCode>(
331 &lib,
332 b"curl_multi_timeout\0",
333 )?
334 };
335 let multi_perform = unsafe {
336 load_symbol::<unsafe extern "C" fn(*mut CurlMulti, *mut c_int) -> CurlMCode>(
337 &lib,
338 b"curl_multi_perform\0",
339 )?
340 };
341 let multi_poll = unsafe {
342 load_symbol::<
343 unsafe extern "C" fn(
344 *mut CurlMulti,
345 *mut CurlWaitFd,
346 c_uint,
347 c_int,
348 *mut c_int,
349 ) -> CurlMCode,
350 >(&lib, b"curl_multi_poll\0")?
351 };
352 let multi_socket_action = unsafe {
353 load_symbol::<
354 unsafe extern "C" fn(*mut CurlMulti, CurlSocket, c_int, *mut c_int) -> CurlMCode,
355 >(&lib, b"curl_multi_socket_action\0")?
356 };
357 let multi_info_read = unsafe {
358 load_symbol::<unsafe extern "C" fn(*mut CurlMulti, *mut c_int) -> *mut CurlMessage>(
359 &lib,
360 b"curl_multi_info_read\0",
361 )?
362 };
363 let multi_strerror = unsafe {
364 load_symbol::<unsafe extern "C" fn(CurlMCode) -> *const c_char>(
365 &lib,
366 b"curl_multi_strerror\0",
367 )?
368 };
369
370 info!(path = %path.display(), "curl-impersonate library loaded");
371 Ok(Self {
372 _lib: ManuallyDrop::new(lib),
373 global_init,
374 global_cleanup,
375 easy_init,
376 easy_cleanup,
377 easy_perform,
378 easy_setopt,
379 easy_strerror,
380 easy_impersonate,
381 slist_append,
382 slist_free_all,
383 ws_send,
384 ws_recv,
385 multi_init,
386 multi_cleanup,
387 multi_setopt,
388 multi_add_handle,
389 multi_remove_handle,
390 multi_fdset,
391 multi_timeout,
392 multi_perform,
393 multi_poll,
394 multi_socket_action,
395 multi_info_read,
396 multi_strerror,
397 })
398 }
399
400 pub fn error_text(&self, code: CurlCode) -> String {
401 unsafe {
402 let ptr = (self.easy_strerror)(code);
403 if ptr.is_null() {
404 return format!("CURLcode {}", code);
405 }
406 CStr::from_ptr(ptr).to_string_lossy().into_owned()
407 }
408 }
409
410 pub fn multi_error_text(&self, code: CurlMCode) -> String {
411 unsafe {
412 let ptr = (self.multi_strerror)(code);
413 if ptr.is_null() {
414 return format!("CURLMcode {}", code);
415 }
416 CStr::from_ptr(ptr).to_string_lossy().into_owned()
417 }
418 }
419}
420
421unsafe impl Send for CurlApi {}
424unsafe impl Sync for CurlApi {}
425
426static SHARED_API: OnceLock<Arc<CurlApi>> = OnceLock::new();
427
428pub fn shared_curl_api(lib_path: &Path) -> Result<Arc<CurlApi>, SysError> {
431 if let Some(api) = SHARED_API.get() {
432 return Ok(Arc::clone(api));
433 }
434 let api = unsafe { CurlApi::load(lib_path) }?;
435 let arc = Arc::new(api);
436 let _ = SHARED_API.set(Arc::clone(&arc));
438 Ok(SHARED_API.get().map(Arc::clone).unwrap_or(arc))
439}
440
441impl CurlMessage {
442 pub unsafe fn done_result(&self) -> CurlCode {
445 unsafe { self.data.result }
446 }
447}
448
449unsafe fn load_symbol<T: Copy>(lib: &Library, name: &[u8]) -> Result<T, SysError> {
450 let symbol = unsafe { lib.get::<T>(name) }.map_err(|source| SysError::MissingSymbol {
451 name: String::from_utf8_lossy(name)
452 .trim_end_matches('\0')
453 .to_owned(),
454 source,
455 })?;
456 Ok(*symbol)
457}
458
459pub fn platform_library_names() -> &'static [&'static str] {
460 if cfg!(target_os = "macos") {
461 &["libcurl-impersonate.4.dylib", "libcurl-impersonate.dylib"]
462 } else if cfg!(target_os = "linux") {
463 &["libcurl-impersonate.so.4", "libcurl-impersonate.so"]
464 } else if cfg!(target_os = "windows") {
465 &[
466 "curl-impersonate.dll",
467 "libcurl-impersonate.dll",
468 "libcurl.dll",
469 ]
470 } else {
471 &[
472 "libcurl-impersonate.4.dylib",
473 "libcurl-impersonate.so.4",
474 "curl-impersonate.dll",
475 ]
476 }
477}
478
479pub fn find_near_executable() -> Option<PathBuf> {
480 let exe = std::env::current_exe().ok()?;
481 let exe_dir = exe.parent()?;
482 for name in platform_library_names() {
483 let in_lib = exe_dir.join("..").join("lib").join(name);
484 if in_lib.exists() {
485 return Some(in_lib);
486 }
487 let side_by_side = exe_dir.join(name);
488 if side_by_side.exists() {
489 return Some(side_by_side);
490 }
491 }
492 None
493}
494
495fn probe_library_dir(dir: &Path, searched: &mut Vec<PathBuf>) -> Option<PathBuf> {
496 for name in platform_library_names() {
497 let candidate = dir.join(name);
498 searched.push(candidate.clone());
499 if candidate.exists() {
500 return Some(candidate);
501 }
502 }
503 None
504}
505
506fn default_runtime_search_roots() -> Vec<PathBuf> {
507 let mut roots = Vec::new();
508
509 if let Ok(dir) = std::env::var("IMPCURL_LIB_DIR") {
510 roots.push(PathBuf::from(dir));
511 }
512
513 if let Ok(home) = std::env::var("HOME") {
514 roots.push(PathBuf::from(&home).join(".impcurl/lib"));
515 roots.push(PathBuf::from(home).join(".cuimp/binaries"));
516 }
517
518 roots
519}
520
521fn auto_fetch_enabled() -> bool {
522 match std::env::var("IMPCURL_AUTO_FETCH") {
523 Ok(value) => {
524 let normalized = value.trim().to_ascii_lowercase();
525 !matches!(normalized.as_str(), "0" | "false" | "no" | "off")
526 }
527 Err(_) => true,
528 }
529}
530
531fn auto_fetch_cache_dir() -> Result<PathBuf, SysError> {
532 if let Ok(dir) = std::env::var("IMPCURL_AUTO_FETCH_CACHE_DIR") {
533 return Ok(PathBuf::from(dir));
534 }
535 if let Ok(dir) = std::env::var("IMPCURL_LIB_DIR") {
536 return Ok(PathBuf::from(dir));
537 }
538 if let Ok(home) = std::env::var("HOME") {
539 return Ok(PathBuf::from(home).join(".impcurl/lib"));
540 }
541 Err(SysError::AutoFetchCacheDirUnavailable)
542}
543
544fn current_target_triple() -> &'static str {
545 if cfg!(all(target_os = "macos", target_arch = "aarch64")) {
546 "aarch64-apple-darwin"
547 } else if cfg!(all(target_os = "macos", target_arch = "x86_64")) {
548 "x86_64-apple-darwin"
549 } else if cfg!(all(
550 target_os = "linux",
551 target_arch = "x86_64",
552 target_env = "gnu"
553 )) {
554 "x86_64-unknown-linux-gnu"
555 } else if cfg!(all(
556 target_os = "linux",
557 target_arch = "x86",
558 target_env = "gnu"
559 )) {
560 "i686-unknown-linux-gnu"
561 } else if cfg!(all(
562 target_os = "linux",
563 target_arch = "aarch64",
564 target_env = "gnu"
565 )) {
566 "aarch64-unknown-linux-gnu"
567 } else if cfg!(all(
568 target_os = "linux",
569 target_arch = "x86_64",
570 target_env = "musl"
571 )) {
572 "x86_64-unknown-linux-musl"
573 } else if cfg!(all(
574 target_os = "linux",
575 target_arch = "aarch64",
576 target_env = "musl"
577 )) {
578 "aarch64-unknown-linux-musl"
579 } else if cfg!(all(target_os = "windows", target_arch = "x86_64")) {
580 "x86_64-pc-windows-msvc"
581 } else if cfg!(all(target_os = "windows", target_arch = "x86")) {
582 "i686-pc-windows-msvc"
583 } else if cfg!(all(target_os = "windows", target_arch = "aarch64")) {
584 "aarch64-pc-windows-msvc"
585 } else {
586 "unknown"
587 }
588}
589
590fn wheel_platform_tag_for_target(target: &str) -> Option<&'static str> {
591 match target {
592 "x86_64-apple-darwin" => Some("macosx_10_9_x86_64"),
593 "aarch64-apple-darwin" => Some("macosx_11_0_arm64"),
594 "x86_64-unknown-linux-gnu" => Some("manylinux_2_17_x86_64.manylinux2014_x86_64"),
595 "aarch64-unknown-linux-gnu" => Some("manylinux_2_17_aarch64.manylinux2014_aarch64"),
596 "i686-unknown-linux-gnu" => Some("manylinux_2_17_i686.manylinux2014_i686"),
597 "x86_64-unknown-linux-musl" => Some("musllinux_1_1_x86_64"),
598 "aarch64-unknown-linux-musl" => Some("musllinux_1_1_aarch64"),
599 "x86_64-pc-windows-msvc" | "x86_64-pc-windows-gnu" => Some("win_amd64"),
600 "i686-pc-windows-msvc" | "i686-pc-windows-gnu" => Some("win32"),
601 "aarch64-pc-windows-msvc" => Some("win_arm64"),
602 _ => None,
603 }
604}
605
606fn is_shared_library_name(file_name: &str) -> bool {
607 if cfg!(target_os = "macos") {
608 file_name.ends_with(".dylib")
609 } else if cfg!(target_os = "linux") {
610 file_name.contains(".so")
611 } else if cfg!(target_os = "windows") {
612 file_name.to_ascii_lowercase().ends_with(".dll")
613 } else {
614 file_name.ends_with(".dylib")
615 || file_name.contains(".so")
616 || file_name.to_ascii_lowercase().ends_with(".dll")
617 }
618}
619
620fn run_download_command(command: &mut Command, command_label: &str) -> Result<Vec<u8>, SysError> {
621 let output = command
622 .output()
623 .map_err(|source| SysError::AutoFetchCommandSpawn {
624 command: command_label.to_owned(),
625 source,
626 })?;
627
628 if output.status.success() {
629 return Ok(output.stdout);
630 }
631
632 let stderr = String::from_utf8_lossy(&output.stderr).to_string();
633 Err(SysError::AutoFetchCommandFailed {
634 command: command_label.to_owned(),
635 status: output.status.code(),
636 stderr,
637 })
638}
639
640fn fetch_url_to_string(url: &str) -> Result<String, SysError> {
641 let mut curl_cmd = Command::new("curl");
642 curl_cmd
643 .arg("-fsSL")
644 .arg("-H")
645 .arg("User-Agent: impcurl-sys")
646 .arg(url);
647 match run_download_command(&mut curl_cmd, "curl") {
648 Ok(stdout) => {
649 let body = String::from_utf8_lossy(&stdout).to_string();
650 return Ok(body);
651 }
652 Err(SysError::AutoFetchCommandSpawn { .. }) => {}
653 Err(err) => return Err(err),
654 }
655
656 let mut wget_cmd = Command::new("wget");
657 wget_cmd.arg("-qO-").arg(url);
658 let stdout = run_download_command(&mut wget_cmd, "wget")?;
659 Ok(String::from_utf8_lossy(&stdout).to_string())
660}
661
662fn fetch_url_to_file(url: &str, output_path: &Path) -> Result<(), SysError> {
663 if let Some(parent) = output_path.parent() {
664 fs::create_dir_all(parent)?;
665 }
666
667 let output_str = output_path.to_string_lossy().to_string();
668
669 let mut curl_cmd = Command::new("curl");
670 curl_cmd
671 .arg("-fL")
672 .arg("-o")
673 .arg(&output_str)
674 .arg("-H")
675 .arg("User-Agent: impcurl-sys")
676 .arg(url);
677 match run_download_command(&mut curl_cmd, "curl") {
678 Ok(_) => return Ok(()),
679 Err(SysError::AutoFetchCommandSpawn { .. }) => {}
680 Err(err) => return Err(err),
681 }
682
683 let mut wget_cmd = Command::new("wget");
684 wget_cmd.arg("-O").arg(&output_str).arg(url);
685 run_download_command(&mut wget_cmd, "wget")?;
686 Ok(())
687}
688
689fn select_curl_cffi_wheel_download_url(
690 release_json: &str,
691 version: &str,
692 platform_tag: &str,
693) -> Option<String> {
694 let parsed: Value = serde_json::from_str(release_json).ok()?;
695 let assets = parsed.get("assets")?.as_array()?;
696 for asset in assets {
697 let name = asset.get("name")?.as_str()?;
698 if !name.starts_with(&format!("curl_cffi-{version}-")) {
699 continue;
700 }
701 if !name.contains("-abi3-") {
702 continue;
703 }
704 if !name.ends_with(&format!("-{platform_tag}.whl")) {
705 continue;
706 }
707 let url = asset.get("browser_download_url")?.as_str()?;
708 return Some(url.to_owned());
709 }
710 None
711}
712
713fn extract_shared_libraries_from_wheel(
714 wheel_path: &Path,
715 output_dir: &Path,
716) -> Result<Vec<PathBuf>, SysError> {
717 let wheel_file = File::open(wheel_path)?;
718 let mut archive = ZipArchive::new(wheel_file)?;
719 let mut copied = Vec::new();
720
721 for index in 0..archive.len() {
722 let mut file = archive.by_index(index)?;
723 if !file.is_file() {
724 continue;
725 }
726 let file_name = match Path::new(file.name()).file_name().and_then(|s| s.to_str()) {
727 Some(name) => name,
728 None => continue,
729 };
730 if !is_shared_library_name(file_name) {
731 continue;
732 }
733 let dst = output_dir.join(file_name);
734 let mut out = File::create(&dst)?;
735 io::copy(&mut file, &mut out)?;
736 copied.push(dst);
737 }
738
739 Ok(copied)
740}
741
742fn auto_fetch_from_curl_cffi(cache_dir: &Path) -> Result<(), SysError> {
743 info!(cache_dir = %cache_dir.display(), "auto-fetching curl-impersonate from curl_cffi wheel");
744 fs::create_dir_all(cache_dir)?;
745
746 let cffi_version =
747 std::env::var("IMPCURL_CURL_CFFI_VERSION").unwrap_or_else(|_| "0.11.3".to_owned());
748 let target = current_target_triple();
749 let platform_tag = wheel_platform_tag_for_target(target)
750 .ok_or_else(|| SysError::AutoFetchUnsupportedTarget(target.to_owned()))?;
751
752 let release_api_url =
753 format!("https://api.github.com/repos/lexiforest/curl_cffi/releases/tags/v{cffi_version}");
754 let release_json = fetch_url_to_string(&release_api_url)?;
755 let wheel_url = select_curl_cffi_wheel_download_url(&release_json, &cffi_version, platform_tag)
756 .ok_or_else(|| SysError::AutoFetchWheelAssetNotFound {
757 version: cffi_version.clone(),
758 platform_tag: platform_tag.to_owned(),
759 })?;
760
761 let wheel_path = cache_dir.join(format!(
762 ".curl_cffi-{cffi_version}-{platform_tag}-{}.whl",
763 std::process::id()
764 ));
765
766 fetch_url_to_file(&wheel_url, &wheel_path)?;
767 let _ = extract_shared_libraries_from_wheel(&wheel_path, cache_dir)?;
768 let _ = fs::remove_file(&wheel_path);
769
770 let mut searched = Vec::new();
771 if probe_library_dir(cache_dir, &mut searched).is_none() {
772 return Err(SysError::AutoFetchNoStandaloneRuntime {
773 cache_dir: cache_dir.to_path_buf(),
774 });
775 }
776
777 Ok(())
778}
779
780pub fn resolve_impersonate_lib_path(extra_search_roots: &[PathBuf]) -> Result<PathBuf, SysError> {
781 if let Ok(path) = std::env::var("CURL_IMPERSONATE_LIB") {
782 let resolved = PathBuf::from(path);
783 if resolved.exists() {
784 debug!(path = %resolved.display(), "found via CURL_IMPERSONATE_LIB");
785 return Ok(resolved);
786 }
787 return Err(SysError::MissingEnvPath(resolved));
788 }
789
790 if let Some(packaged) = find_near_executable() {
791 debug!(path = %packaged.display(), "found near executable");
792 return Ok(packaged);
793 }
794
795 let mut searched = Vec::new();
796 for root in extra_search_roots {
797 if let Some(found) = probe_library_dir(root, &mut searched) {
798 return Ok(found);
799 }
800 }
801
802 for root in default_runtime_search_roots() {
803 if let Some(found) = probe_library_dir(&root, &mut searched) {
804 return Ok(found);
805 }
806 }
807
808 if auto_fetch_enabled() {
809 let auto_fetch_result = (|| -> Result<PathBuf, SysError> {
810 let cache_dir = auto_fetch_cache_dir()?;
811 if let Some(found) = probe_library_dir(&cache_dir, &mut searched) {
812 return Ok(found);
813 }
814 auto_fetch_from_curl_cffi(&cache_dir)?;
815 probe_library_dir(&cache_dir, &mut searched).ok_or_else(|| {
816 SysError::AutoFetchNoStandaloneRuntime {
817 cache_dir: cache_dir.to_path_buf(),
818 }
819 })
820 })();
821
822 return match auto_fetch_result {
823 Ok(found) => Ok(found),
824 Err(err) => Err(SysError::LibraryNotFoundAfterAutoFetch {
825 searched,
826 auto_fetch_error: err.to_string(),
827 }),
828 };
829 }
830
831 Err(SysError::LibraryNotFound(searched))
832}
833
834#[cfg(test)]
835mod tests {
836 use super::select_curl_cffi_wheel_download_url;
837
838 #[test]
839 fn picks_matching_wheel_asset_url() {
840 let release_json = r#"
841 {
842 "assets": [
843 {
844 "name": "curl_cffi-0.11.3-cp39-abi3-macosx_10_9_x86_64.whl",
845 "browser_download_url": "https://example.test/x86_64.whl"
846 },
847 {
848 "name": "curl_cffi-0.11.3-cp39-abi3-macosx_11_0_arm64.whl",
849 "browser_download_url": "https://example.test/arm64.whl"
850 }
851 ]
852 }
853 "#;
854
855 let url = select_curl_cffi_wheel_download_url(release_json, "0.11.3", "macosx_11_0_arm64")
856 .expect("expected matching wheel URL");
857
858 assert_eq!(url, "https://example.test/arm64.whl");
859 }
860}