Skip to main content

impcurl_sys/
lib.rs

1use libloading::Library;
2use std::ffi::CStr;
3use std::fs::{self};
4use std::io;
5use std::mem::ManuallyDrop;
6use std::os::raw::{c_char, c_int, c_long, c_short, c_uint, c_ulong, c_void};
7use std::path::{Path, PathBuf};
8use std::process::Command;
9use std::sync::{Arc, OnceLock};
10use tracing::{debug, info};
11
12pub type CurlCode = c_int;
13pub type CurlOption = c_uint;
14pub type CurlMCode = c_int;
15pub type CurlMOption = c_int;
16
17#[cfg(windows)]
18pub type CurlSocket = usize;
19#[cfg(not(windows))]
20pub type CurlSocket = c_int;
21
22pub type CurlMultiSocketCallback =
23    unsafe extern "C" fn(*mut Curl, CurlSocket, c_int, *mut c_void, *mut c_void) -> c_int;
24pub type CurlMultiTimerCallback =
25    unsafe extern "C" fn(*mut CurlMulti, c_long, *mut c_void) -> c_int;
26
27#[repr(C)]
28pub struct Curl {
29    _private: [u8; 0],
30}
31
32#[repr(C)]
33pub struct CurlMulti {
34    _private: [u8; 0],
35}
36
37#[repr(C)]
38pub struct CurlSlist {
39    pub data: *mut c_char,
40    pub next: *mut CurlSlist,
41}
42
43#[repr(C)]
44pub struct CurlWsFrame {
45    pub age: c_int,
46    pub flags: c_int,
47    pub offset: i64,
48    pub bytesleft: i64,
49    pub len: usize,
50}
51
52#[repr(C)]
53pub union CurlMessageData {
54    pub whatever: *mut c_void,
55    pub result: CurlCode,
56}
57
58#[repr(C)]
59pub struct CurlMessage {
60    pub msg: c_int,
61    pub easy_handle: *mut Curl,
62    pub data: CurlMessageData,
63}
64
65#[repr(C)]
66pub struct CurlWaitFd {
67    pub fd: CurlSocket,
68    pub events: c_short,
69    pub revents: c_short,
70}
71
72pub const CURLE_OK: CurlCode = 0;
73pub const CURLE_AGAIN: CurlCode = 81;
74pub const CURL_GLOBAL_DEFAULT: c_ulong = 3;
75pub const CURLM_OK: CurlMCode = 0;
76pub const CURLMSG_DONE: c_int = 1;
77
78#[cfg(windows)]
79pub const CURL_SOCKET_TIMEOUT: CurlSocket = usize::MAX;
80#[cfg(not(windows))]
81pub const CURL_SOCKET_TIMEOUT: CurlSocket = -1;
82
83pub const CURL_CSELECT_IN: c_int = 0x01;
84pub const CURL_CSELECT_OUT: c_int = 0x02;
85pub const CURL_CSELECT_ERR: c_int = 0x04;
86
87pub const CURL_POLL_NONE: c_int = 0;
88pub const CURL_POLL_IN: c_int = 1;
89pub const CURL_POLL_OUT: c_int = 2;
90pub const CURL_POLL_INOUT: c_int = 3;
91pub const CURL_POLL_REMOVE: c_int = 4;
92
93pub const CURLMOPT_SOCKETFUNCTION: CurlMOption = 20001;
94pub const CURLMOPT_SOCKETDATA: CurlMOption = 10002;
95pub const CURLMOPT_TIMERFUNCTION: CurlMOption = 20004;
96pub const CURLMOPT_TIMERDATA: CurlMOption = 10005;
97
98pub const CURLOPT_URL: CurlOption = 10002;
99pub const CURLOPT_HTTPHEADER: CurlOption = 10023;
100pub const CURLOPT_HTTP_VERSION: CurlOption = 84;
101pub const CURLOPT_CONNECT_ONLY: CurlOption = 141;
102pub const CURLOPT_VERBOSE: CurlOption = 41;
103pub const CURLOPT_PROXY: CurlOption = 10004;
104pub const CURLOPT_CAINFO: CurlOption = 10065;
105pub const CURLINFO_RESPONSE_CODE: c_uint = 0x200002;
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("I/O error during auto-fetch: {0}")]
148    AutoFetchIo(#[from] io::Error),
149    #[error("no standalone libcurl-impersonate shared library was found in {cache_dir}")]
150    AutoFetchNoStandaloneRuntime { cache_dir: PathBuf },
151    #[error("no libcurl-impersonate asset naming rule for target: {0}")]
152    AutoFetchRuntimeUnsupportedTarget(String),
153}
154
155pub struct CurlApi {
156    // Keep the dynamic library loaded for process lifetime. Unloading can crash
157    // with libcurl-impersonate on process teardown in some environments.
158    _lib: ManuallyDrop<Library>,
159    pub global_init: unsafe extern "C" fn(c_ulong) -> CurlCode,
160    pub global_cleanup: unsafe extern "C" fn(),
161    pub easy_init: unsafe extern "C" fn() -> *mut Curl,
162    pub easy_cleanup: unsafe extern "C" fn(*mut Curl),
163    pub easy_perform: unsafe extern "C" fn(*mut Curl) -> CurlCode,
164    pub easy_setopt: unsafe extern "C" fn(*mut Curl, CurlOption, ...) -> CurlCode,
165    pub easy_getinfo: unsafe extern "C" fn(*mut Curl, c_uint, ...) -> CurlCode,
166    pub easy_strerror: unsafe extern "C" fn(CurlCode) -> *const c_char,
167    pub easy_impersonate: unsafe extern "C" fn(*mut Curl, *const c_char, c_int) -> CurlCode,
168    pub slist_append: unsafe extern "C" fn(*mut CurlSlist, *const c_char) -> *mut CurlSlist,
169    pub slist_free_all: unsafe extern "C" fn(*mut CurlSlist),
170    pub ws_send:
171        unsafe extern "C" fn(*mut Curl, *const c_void, usize, *mut usize, i64, c_uint) -> CurlCode,
172    pub ws_recv: unsafe extern "C" fn(
173        *mut Curl,
174        *mut c_void,
175        usize,
176        *mut usize,
177        *mut *const CurlWsFrame,
178    ) -> CurlCode,
179    pub multi_init: unsafe extern "C" fn() -> *mut CurlMulti,
180    pub multi_cleanup: unsafe extern "C" fn(*mut CurlMulti) -> CurlMCode,
181    pub multi_setopt: unsafe extern "C" fn(*mut CurlMulti, CurlMOption, ...) -> CurlMCode,
182    pub multi_add_handle: unsafe extern "C" fn(*mut CurlMulti, *mut Curl) -> CurlMCode,
183    pub multi_remove_handle: unsafe extern "C" fn(*mut CurlMulti, *mut Curl) -> CurlMCode,
184    pub multi_fdset: unsafe extern "C" fn(
185        *mut CurlMulti,
186        *mut c_void,
187        *mut c_void,
188        *mut c_void,
189        *mut c_int,
190    ) -> CurlMCode,
191    pub multi_timeout: unsafe extern "C" fn(*mut CurlMulti, *mut c_long) -> CurlMCode,
192    pub multi_perform: unsafe extern "C" fn(*mut CurlMulti, *mut c_int) -> CurlMCode,
193    pub multi_poll: unsafe extern "C" fn(
194        *mut CurlMulti,
195        *mut CurlWaitFd,
196        c_uint,
197        c_int,
198        *mut c_int,
199    ) -> CurlMCode,
200    pub multi_socket_action:
201        unsafe extern "C" fn(*mut CurlMulti, CurlSocket, c_int, *mut c_int) -> CurlMCode,
202    pub multi_info_read: unsafe extern "C" fn(*mut CurlMulti, *mut c_int) -> *mut CurlMessage,
203    pub multi_strerror: unsafe extern "C" fn(CurlMCode) -> *const c_char,
204}
205
206impl CurlApi {
207    /// # Safety
208    /// Caller must ensure the loaded library is ABI-compatible with the symbols used.
209    pub unsafe fn load(path: &Path) -> Result<Self, SysError> {
210        debug!(path = %path.display(), "loading curl-impersonate library");
211        let lib = unsafe { Library::new(path) }.map_err(|source| SysError::LoadLibrary {
212            path: path.to_path_buf(),
213            source,
214        })?;
215
216        let global_init = unsafe {
217            load_symbol::<unsafe extern "C" fn(c_ulong) -> CurlCode>(&lib, b"curl_global_init\0")?
218        };
219        let global_cleanup =
220            unsafe { load_symbol::<unsafe extern "C" fn()>(&lib, b"curl_global_cleanup\0")? };
221        let easy_init = unsafe {
222            load_symbol::<unsafe extern "C" fn() -> *mut Curl>(&lib, b"curl_easy_init\0")?
223        };
224        let easy_cleanup = unsafe {
225            load_symbol::<unsafe extern "C" fn(*mut Curl)>(&lib, b"curl_easy_cleanup\0")?
226        };
227        let easy_perform = unsafe {
228            load_symbol::<unsafe extern "C" fn(*mut Curl) -> CurlCode>(
229                &lib,
230                b"curl_easy_perform\0",
231            )?
232        };
233        let easy_setopt = unsafe {
234            load_symbol::<unsafe extern "C" fn(*mut Curl, CurlOption, ...) -> CurlCode>(
235                &lib,
236                b"curl_easy_setopt\0",
237            )?
238        };
239        let easy_getinfo = unsafe {
240            load_symbol::<unsafe extern "C" fn(*mut Curl, c_uint, ...) -> CurlCode>(
241                &lib,
242                b"curl_easy_getinfo\0",
243            )?
244        };
245        let easy_strerror = unsafe {
246            load_symbol::<unsafe extern "C" fn(CurlCode) -> *const c_char>(
247                &lib,
248                b"curl_easy_strerror\0",
249            )?
250        };
251        let easy_impersonate = unsafe {
252            load_symbol::<unsafe extern "C" fn(*mut Curl, *const c_char, c_int) -> CurlCode>(
253                &lib,
254                b"curl_easy_impersonate\0",
255            )?
256        };
257        let slist_append = unsafe {
258            load_symbol::<unsafe extern "C" fn(*mut CurlSlist, *const c_char) -> *mut CurlSlist>(
259                &lib,
260                b"curl_slist_append\0",
261            )?
262        };
263        let slist_free_all = unsafe {
264            load_symbol::<unsafe extern "C" fn(*mut CurlSlist)>(&lib, b"curl_slist_free_all\0")?
265        };
266        let ws_send = unsafe {
267            load_symbol::<
268                unsafe extern "C" fn(
269                    *mut Curl,
270                    *const c_void,
271                    usize,
272                    *mut usize,
273                    i64,
274                    c_uint,
275                ) -> CurlCode,
276            >(&lib, b"curl_ws_send\0")?
277        };
278        let ws_recv = unsafe {
279            load_symbol::<
280                unsafe extern "C" fn(
281                    *mut Curl,
282                    *mut c_void,
283                    usize,
284                    *mut usize,
285                    *mut *const CurlWsFrame,
286                ) -> CurlCode,
287            >(&lib, b"curl_ws_recv\0")?
288        };
289        let multi_init = unsafe {
290            load_symbol::<unsafe extern "C" fn() -> *mut CurlMulti>(&lib, b"curl_multi_init\0")?
291        };
292        let multi_cleanup = unsafe {
293            load_symbol::<unsafe extern "C" fn(*mut CurlMulti) -> CurlMCode>(
294                &lib,
295                b"curl_multi_cleanup\0",
296            )?
297        };
298        let multi_setopt = unsafe {
299            load_symbol::<unsafe extern "C" fn(*mut CurlMulti, CurlMOption, ...) -> CurlMCode>(
300                &lib,
301                b"curl_multi_setopt\0",
302            )?
303        };
304        let multi_add_handle = unsafe {
305            load_symbol::<unsafe extern "C" fn(*mut CurlMulti, *mut Curl) -> CurlMCode>(
306                &lib,
307                b"curl_multi_add_handle\0",
308            )?
309        };
310        let multi_remove_handle = unsafe {
311            load_symbol::<unsafe extern "C" fn(*mut CurlMulti, *mut Curl) -> CurlMCode>(
312                &lib,
313                b"curl_multi_remove_handle\0",
314            )?
315        };
316        let multi_fdset = unsafe {
317            load_symbol::<
318                unsafe extern "C" fn(
319                    *mut CurlMulti,
320                    *mut c_void,
321                    *mut c_void,
322                    *mut c_void,
323                    *mut c_int,
324                ) -> CurlMCode,
325            >(&lib, b"curl_multi_fdset\0")?
326        };
327        let multi_timeout = unsafe {
328            load_symbol::<unsafe extern "C" fn(*mut CurlMulti, *mut c_long) -> CurlMCode>(
329                &lib,
330                b"curl_multi_timeout\0",
331            )?
332        };
333        let multi_perform = unsafe {
334            load_symbol::<unsafe extern "C" fn(*mut CurlMulti, *mut c_int) -> CurlMCode>(
335                &lib,
336                b"curl_multi_perform\0",
337            )?
338        };
339        let multi_poll = unsafe {
340            load_symbol::<
341                unsafe extern "C" fn(
342                    *mut CurlMulti,
343                    *mut CurlWaitFd,
344                    c_uint,
345                    c_int,
346                    *mut c_int,
347                ) -> CurlMCode,
348            >(&lib, b"curl_multi_poll\0")?
349        };
350        let multi_socket_action = unsafe {
351            load_symbol::<
352                unsafe extern "C" fn(*mut CurlMulti, CurlSocket, c_int, *mut c_int) -> CurlMCode,
353            >(&lib, b"curl_multi_socket_action\0")?
354        };
355        let multi_info_read = unsafe {
356            load_symbol::<unsafe extern "C" fn(*mut CurlMulti, *mut c_int) -> *mut CurlMessage>(
357                &lib,
358                b"curl_multi_info_read\0",
359            )?
360        };
361        let multi_strerror = unsafe {
362            load_symbol::<unsafe extern "C" fn(CurlMCode) -> *const c_char>(
363                &lib,
364                b"curl_multi_strerror\0",
365            )?
366        };
367
368        info!(path = %path.display(), "curl-impersonate library loaded");
369        Ok(Self {
370            _lib: ManuallyDrop::new(lib),
371            global_init,
372            global_cleanup,
373            easy_init,
374            easy_cleanup,
375            easy_perform,
376            easy_setopt,
377            easy_getinfo,
378            easy_strerror,
379            easy_impersonate,
380            slist_append,
381            slist_free_all,
382            ws_send,
383            ws_recv,
384            multi_init,
385            multi_cleanup,
386            multi_setopt,
387            multi_add_handle,
388            multi_remove_handle,
389            multi_fdset,
390            multi_timeout,
391            multi_perform,
392            multi_poll,
393            multi_socket_action,
394            multi_info_read,
395            multi_strerror,
396        })
397    }
398
399    pub fn error_text(&self, code: CurlCode) -> String {
400        unsafe {
401            let ptr = (self.easy_strerror)(code);
402            if ptr.is_null() {
403                return format!("CURLcode {}", code);
404            }
405            CStr::from_ptr(ptr).to_string_lossy().into_owned()
406        }
407    }
408
409    pub fn multi_error_text(&self, code: CurlMCode) -> String {
410        unsafe {
411            let ptr = (self.multi_strerror)(code);
412            if ptr.is_null() {
413                return format!("CURLMcode {}", code);
414            }
415            CStr::from_ptr(ptr).to_string_lossy().into_owned()
416        }
417    }
418}
419
420// CurlApi only contains function pointers and a ManuallyDrop<Library> (never unloaded).
421// All function pointers are process-global symbols — safe to share across threads.
422unsafe impl Send for CurlApi {}
423unsafe impl Sync for CurlApi {}
424
425static SHARED_API: OnceLock<Arc<CurlApi>> = OnceLock::new();
426
427/// Get or initialize the process-wide shared CurlApi instance.
428/// First call loads the library; subsequent calls return the cached Arc.
429pub fn shared_curl_api(lib_path: &Path) -> Result<Arc<CurlApi>, SysError> {
430    if let Some(api) = SHARED_API.get() {
431        return Ok(Arc::clone(api));
432    }
433    let api = unsafe { CurlApi::load(lib_path) }?;
434    let arc = Arc::new(api);
435    // Race is fine — loser's CurlApi uses ManuallyDrop so no resource leak.
436    let _ = SHARED_API.set(Arc::clone(&arc));
437    Ok(SHARED_API.get().map(Arc::clone).unwrap_or(arc))
438}
439
440impl CurlMessage {
441    /// # Safety
442    /// Caller must only use this for messages where `msg == CURLMSG_DONE`.
443    pub unsafe fn done_result(&self) -> CurlCode {
444        unsafe { self.data.result }
445    }
446}
447
448unsafe fn load_symbol<T: Copy>(lib: &Library, name: &[u8]) -> Result<T, SysError> {
449    let symbol = unsafe { lib.get::<T>(name) }.map_err(|source| SysError::MissingSymbol {
450        name: String::from_utf8_lossy(name)
451            .trim_end_matches('\0')
452            .to_owned(),
453        source,
454    })?;
455    Ok(*symbol)
456}
457
458pub fn platform_library_names() -> &'static [&'static str] {
459    if cfg!(target_os = "macos") {
460        &["libcurl-impersonate.4.dylib", "libcurl-impersonate.dylib"]
461    } else if cfg!(target_os = "linux") {
462        &["libcurl-impersonate.so.4", "libcurl-impersonate.so"]
463    } else if cfg!(target_os = "windows") {
464        &[
465            "curl-impersonate.dll",
466            "libcurl-impersonate.dll",
467            "libcurl.dll",
468        ]
469    } else {
470        &[
471            "libcurl-impersonate.4.dylib",
472            "libcurl-impersonate.so.4",
473            "curl-impersonate.dll",
474        ]
475    }
476}
477
478pub fn find_near_executable() -> Option<PathBuf> {
479    let exe = std::env::current_exe().ok()?;
480    let exe_dir = exe.parent()?;
481    for name in platform_library_names() {
482        let in_lib = exe_dir.join("..").join("lib").join(name);
483        if in_lib.exists() {
484            return Some(in_lib);
485        }
486        let side_by_side = exe_dir.join(name);
487        if side_by_side.exists() {
488            return Some(side_by_side);
489        }
490    }
491    None
492}
493
494fn probe_library_dir(dir: &Path, searched: &mut Vec<PathBuf>) -> Option<PathBuf> {
495    for name in platform_library_names() {
496        let candidate = dir.join(name);
497        searched.push(candidate.clone());
498        if candidate.exists() {
499            return Some(candidate);
500        }
501    }
502    None
503}
504
505fn default_library_search_roots() -> Vec<PathBuf> {
506    let mut roots = Vec::new();
507
508    if let Ok(dir) = std::env::var("IMPCURL_LIB_DIR") {
509        roots.push(PathBuf::from(dir));
510    }
511
512    if let Ok(home) = std::env::var("HOME") {
513        roots.push(PathBuf::from(&home).join(".impcurl/lib"));
514        roots.push(PathBuf::from(home).join(".cuimp/binaries"));
515    }
516
517    roots
518}
519
520/// Resolve a usable CA bundle file path for TLS verification.
521///
522/// Resolution order:
523/// 1. `CURL_CA_BUNDLE`
524/// 2. `SSL_CERT_FILE`
525/// 3. platform defaults (`/etc/ssl/certs/ca-certificates.crt` first on Linux)
526pub fn resolve_ca_bundle_path() -> Option<PathBuf> {
527    for key in ["CURL_CA_BUNDLE", "SSL_CERT_FILE"] {
528        if let Ok(value) = std::env::var(key) {
529            let candidate = PathBuf::from(value);
530            if candidate.is_file() {
531                debug!(path = %candidate.display(), env = key, "resolved CA bundle from env");
532                return Some(candidate);
533            }
534        }
535    }
536
537    for candidate in default_ca_bundle_candidates() {
538        if candidate.is_file() {
539            debug!(path = %candidate.display(), "resolved CA bundle from platform defaults");
540            return Some(candidate);
541        }
542    }
543
544    None
545}
546
547fn default_ca_bundle_candidates() -> Vec<PathBuf> {
548    if cfg!(target_os = "linux") {
549        vec![
550            PathBuf::from("/etc/ssl/certs/ca-certificates.crt"),
551            PathBuf::from("/etc/pki/tls/certs/ca-bundle.crt"),
552            PathBuf::from("/etc/ssl/cert.pem"),
553            PathBuf::from("/etc/pki/tls/cert.pem"),
554            PathBuf::from("/etc/ssl/ca-bundle.pem"),
555        ]
556    } else if cfg!(target_os = "macos") {
557        vec![PathBuf::from("/etc/ssl/cert.pem")]
558    } else {
559        Vec::new()
560    }
561}
562
563fn auto_fetch_enabled() -> bool {
564    match std::env::var("IMPCURL_AUTO_FETCH") {
565        Ok(value) => {
566            let normalized = value.trim().to_ascii_lowercase();
567            !matches!(normalized.as_str(), "0" | "false" | "no" | "off")
568        }
569        Err(_) => true,
570    }
571}
572
573fn auto_fetch_cache_dir() -> Result<PathBuf, SysError> {
574    if let Ok(dir) = std::env::var("IMPCURL_AUTO_FETCH_CACHE_DIR") {
575        return Ok(PathBuf::from(dir));
576    }
577    if let Ok(dir) = std::env::var("IMPCURL_LIB_DIR") {
578        return Ok(PathBuf::from(dir));
579    }
580    if let Ok(home) = std::env::var("HOME") {
581        return Ok(PathBuf::from(home).join(".impcurl/lib"));
582    }
583    Err(SysError::AutoFetchCacheDirUnavailable)
584}
585
586fn current_target_triple() -> &'static str {
587    if cfg!(all(target_os = "macos", target_arch = "aarch64")) {
588        "aarch64-apple-darwin"
589    } else if cfg!(all(target_os = "macos", target_arch = "x86_64")) {
590        "x86_64-apple-darwin"
591    } else if cfg!(all(
592        target_os = "linux",
593        target_arch = "x86_64",
594        target_env = "gnu"
595    )) {
596        "x86_64-unknown-linux-gnu"
597    } else if cfg!(all(
598        target_os = "linux",
599        target_arch = "x86",
600        target_env = "gnu"
601    )) {
602        "i686-unknown-linux-gnu"
603    } else if cfg!(all(
604        target_os = "linux",
605        target_arch = "aarch64",
606        target_env = "gnu"
607    )) {
608        "aarch64-unknown-linux-gnu"
609    } else if cfg!(all(
610        target_os = "linux",
611        target_arch = "x86_64",
612        target_env = "musl"
613    )) {
614        "x86_64-unknown-linux-musl"
615    } else if cfg!(all(
616        target_os = "linux",
617        target_arch = "aarch64",
618        target_env = "musl"
619    )) {
620        "aarch64-unknown-linux-musl"
621    } else if cfg!(all(target_os = "windows", target_arch = "x86_64")) {
622        "x86_64-pc-windows-msvc"
623    } else if cfg!(all(target_os = "windows", target_arch = "x86")) {
624        "i686-pc-windows-msvc"
625    } else if cfg!(all(target_os = "windows", target_arch = "aarch64")) {
626        "aarch64-pc-windows-msvc"
627    } else {
628        "unknown"
629    }
630}
631
632fn asset_target_for_triple(target: &str) -> Option<&'static str> {
633    match target {
634        "x86_64-apple-darwin" => Some("x86_64-apple-darwin"),
635        "aarch64-apple-darwin" => Some("aarch64-apple-darwin"),
636        "x86_64-unknown-linux-gnu" => Some("x86_64-unknown-linux-gnu"),
637        "aarch64-unknown-linux-gnu" => Some("aarch64-unknown-linux-gnu"),
638        "x86_64-unknown-linux-musl" => Some("x86_64-unknown-linux-musl"),
639        "aarch64-unknown-linux-musl" => Some("aarch64-unknown-linux-musl"),
640        _ => None,
641    }
642}
643
644fn asset_version() -> String {
645    std::env::var("IMPCURL_LIBCURL_VERSION")
646        .unwrap_or_else(|_| env!("CARGO_PKG_VERSION").to_owned())
647}
648
649fn asset_repo() -> String {
650    std::env::var("IMPCURL_LIBCURL_REPO").unwrap_or_else(|_| "tuchg/impcurl".to_owned())
651}
652
653fn asset_release_tag(version: &str) -> String {
654    format!("impcurl-libcurl-impersonate-v{version}")
655}
656
657fn asset_name(version: &str, target: &str) -> String {
658    format!("impcurl-libcurl-impersonate-v{version}-{target}.tar.gz")
659}
660
661fn asset_cache_dir(base: &Path, version: &str, target: &str) -> PathBuf {
662    base.join("libcurl-impersonate-assets")
663        .join(version)
664        .join(target)
665}
666
667fn asset_url(repo: &str, tag: &str, asset_name: &str) -> String {
668    format!("https://github.com/{repo}/releases/download/{tag}/{asset_name}")
669}
670
671fn run_download_command(command: &mut Command, command_label: &str) -> Result<Vec<u8>, SysError> {
672    let output = command
673        .output()
674        .map_err(|source| SysError::AutoFetchCommandSpawn {
675            command: command_label.to_owned(),
676            source,
677        })?;
678
679    if output.status.success() {
680        return Ok(output.stdout);
681    }
682
683    let stderr = String::from_utf8_lossy(&output.stderr).to_string();
684    Err(SysError::AutoFetchCommandFailed {
685        command: command_label.to_owned(),
686        status: output.status.code(),
687        stderr,
688    })
689}
690
691fn fetch_url_to_file(url: &str, output_path: &Path) -> Result<(), SysError> {
692    if let Some(parent) = output_path.parent() {
693        fs::create_dir_all(parent)?;
694    }
695
696    let output_str = output_path.to_string_lossy().to_string();
697
698    let mut curl_cmd = Command::new("curl");
699    curl_cmd
700        .arg("-fL")
701        .arg("-o")
702        .arg(&output_str)
703        .arg("-H")
704        .arg("User-Agent: impcurl-sys")
705        .arg(url);
706    match run_download_command(&mut curl_cmd, "curl") {
707        Ok(_) => return Ok(()),
708        Err(SysError::AutoFetchCommandSpawn { .. }) => {}
709        Err(err) => return Err(err),
710    }
711
712    let mut wget_cmd = Command::new("wget");
713    wget_cmd.arg("-O").arg(&output_str).arg(url);
714    run_download_command(&mut wget_cmd, "wget")?;
715    Ok(())
716}
717
718fn extract_tar_gz_archive(archive_path: &Path, output_dir: &Path) -> Result<(), SysError> {
719    fs::create_dir_all(output_dir)?;
720    let mut tar_cmd = Command::new("tar");
721    tar_cmd
722        .arg("-xzf")
723        .arg(archive_path)
724        .arg("-C")
725        .arg(output_dir);
726    let _ = run_download_command(&mut tar_cmd, "tar")?;
727    Ok(())
728}
729
730fn auto_fetch_from_assets(cache_dir: &Path) -> Result<PathBuf, SysError> {
731    let version = asset_version();
732    let target_triple = current_target_triple().to_owned();
733    let target = asset_target_for_triple(&target_triple)
734        .ok_or_else(|| SysError::AutoFetchRuntimeUnsupportedTarget(target_triple.clone()))?;
735    let repo = asset_repo();
736    let output_dir = asset_cache_dir(cache_dir, &version, target);
737    let tag = asset_release_tag(&version);
738    let asset = asset_name(&version, target);
739    let url = asset_url(&repo, &tag, &asset);
740    let archive_path = cache_dir.join(format!(
741        ".libcurl-impersonate-asset-{version}-{target}-{}.tar.gz",
742        std::process::id()
743    ));
744
745    info!(
746        cache_dir = %cache_dir.display(),
747        output_dir = %output_dir.display(),
748        url = %url,
749        "auto-fetching libcurl-impersonate from asset release"
750    );
751
752    fetch_url_to_file(&url, &archive_path)?;
753    extract_tar_gz_archive(&archive_path, &output_dir)?;
754    let _ = fs::remove_file(&archive_path);
755
756    let mut searched = Vec::new();
757    probe_library_dir(&output_dir, &mut searched).ok_or_else(|| {
758        SysError::AutoFetchNoStandaloneRuntime {
759            cache_dir: output_dir,
760        }
761    })
762}
763
764pub fn resolve_impersonate_lib_path(extra_search_roots: &[PathBuf]) -> Result<PathBuf, SysError> {
765    if let Ok(path) = std::env::var("CURL_IMPERSONATE_LIB") {
766        let resolved = PathBuf::from(path);
767        if resolved.exists() {
768            debug!(path = %resolved.display(), "found via CURL_IMPERSONATE_LIB");
769            return Ok(resolved);
770        }
771        return Err(SysError::MissingEnvPath(resolved));
772    }
773
774    if let Some(packaged) = find_near_executable() {
775        debug!(path = %packaged.display(), "found near executable");
776        return Ok(packaged);
777    }
778
779    let mut searched = Vec::new();
780    for root in extra_search_roots {
781        if let Some(found) = probe_library_dir(root, &mut searched) {
782            return Ok(found);
783        }
784    }
785
786    for root in default_library_search_roots() {
787        if let Some(found) = probe_library_dir(&root, &mut searched) {
788            return Ok(found);
789        }
790    }
791
792    if auto_fetch_enabled() {
793        let auto_fetch_result = (|| -> Result<PathBuf, SysError> {
794            let cache_dir = auto_fetch_cache_dir()?;
795            let target_triple = current_target_triple().to_owned();
796            let target = asset_target_for_triple(&target_triple).ok_or_else(|| {
797                SysError::AutoFetchRuntimeUnsupportedTarget(target_triple.clone())
798            })?;
799            let version = asset_version();
800            let asset_dir = asset_cache_dir(&cache_dir, &version, target);
801            if let Some(found) = probe_library_dir(&asset_dir, &mut searched) {
802                return Ok(found);
803            }
804            if let Some(found) = probe_library_dir(&cache_dir, &mut searched) {
805                return Ok(found);
806            }
807
808            auto_fetch_from_assets(&cache_dir)?;
809            if let Some(found) = probe_library_dir(&asset_dir, &mut searched) {
810                return Ok(found);
811            }
812            probe_library_dir(&cache_dir, &mut searched).ok_or_else(|| {
813                SysError::AutoFetchNoStandaloneRuntime {
814                    cache_dir: cache_dir.to_path_buf(),
815                }
816            })
817        })();
818
819        return match auto_fetch_result {
820            Ok(found) => Ok(found),
821            Err(err) => Err(SysError::LibraryNotFoundAfterAutoFetch {
822                searched,
823                auto_fetch_error: err.to_string(),
824            }),
825        };
826    }
827
828    Err(SysError::LibraryNotFound(searched))
829}
830
831#[cfg(test)]
832mod tests {
833    use super::{resolve_ca_bundle_path, asset_name, asset_release_tag};
834    use std::{env, ffi::OsString};
835
836    #[test]
837    fn prefers_curl_ca_bundle_env_when_file_exists() {
838        let fixture = env::current_exe().expect("current executable path should exist");
839        let _guard_ssl = EnvGuard::set("SSL_CERT_FILE", None);
840        let _guard_curl = EnvGuard::set("CURL_CA_BUNDLE", Some(fixture.as_os_str()));
841
842        let resolved = resolve_ca_bundle_path().expect("expected env CA bundle to resolve");
843        assert_eq!(resolved, fixture);
844    }
845
846    #[test]
847    fn asset_naming_is_versioned() {
848        assert_eq!(
849            asset_release_tag("1.2.3"),
850            "impcurl-libcurl-impersonate-v1.2.3"
851        );
852        assert_eq!(
853            asset_name("1.2.3", "x86_64-unknown-linux-gnu"),
854            "impcurl-libcurl-impersonate-v1.2.3-x86_64-unknown-linux-gnu.tar.gz"
855        );
856    }
857
858    struct EnvGuard {
859        key: &'static str,
860        old: Option<OsString>,
861    }
862
863    impl EnvGuard {
864        fn set(key: &'static str, new: Option<&std::ffi::OsStr>) -> Self {
865            let old = env::var_os(key);
866            unsafe {
867                match new {
868                    Some(value) => env::set_var(key, value),
869                    None => env::remove_var(key),
870                }
871            }
872            Self { key, old }
873        }
874    }
875
876    impl Drop for EnvGuard {
877        fn drop(&mut self) {
878            unsafe {
879                match self.old.as_ref() {
880                    Some(value) => env::set_var(self.key, value),
881                    None => env::remove_var(self.key),
882                }
883            }
884        }
885    }
886}