Skip to main content

impcurl_sys/
lib.rs

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    // Keep the dynamic library loaded for process lifetime. Unloading can crash
166    // with libcurl-impersonate on process teardown in some environments.
167    _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    /// # Safety
216    /// Caller must ensure the loaded library is ABI-compatible with the symbols used.
217    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
421// CurlApi only contains function pointers and a ManuallyDrop<Library> (never unloaded).
422// All function pointers are process-global symbols — safe to share across threads.
423unsafe impl Send for CurlApi {}
424unsafe impl Sync for CurlApi {}
425
426static SHARED_API: OnceLock<Arc<CurlApi>> = OnceLock::new();
427
428/// Get or initialize the process-wide shared CurlApi instance.
429/// First call loads the library; subsequent calls return the cached Arc.
430pub 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    // Race is fine — loser's CurlApi uses ManuallyDrop so no resource leak.
437    let _ = SHARED_API.set(Arc::clone(&arc));
438    Ok(SHARED_API.get().map(Arc::clone).unwrap_or(arc))
439}
440
441impl CurlMessage {
442    /// # Safety
443    /// Caller must only use this for messages where `msg == CURLMSG_DONE`.
444    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}