Skip to main content

liburlx_ffi/
lib.rs

1//! # liburlx-ffi
2//!
3//! C ABI compatibility layer for liburlx — a drop-in replacement for libcurl.
4//!
5//! This crate provides libcurl-compatible C functions (`curl_easy_init`,
6//! `curl_easy_setopt`, `curl_easy_perform`, etc.) backed by the pure-Rust
7//! [`liburlx`] engine. Existing C/C++ programs can link against `liburlx_ffi`
8//! instead of `libcurl` without code changes.
9//!
10//! All `unsafe` code in the urlx project is confined to this crate.
11//!
12//! ## Usage
13//!
14//! ```c
15//! #include "urlx.h"
16//!
17//! CURL *curl = curl_easy_init();
18//! curl_easy_setopt(curl, CURLOPT_URL, "https://example.com");
19//! curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
20//! CURLcode res = curl_easy_perform(curl);
21//! curl_easy_cleanup(curl);
22//! ```
23//!
24//! ## Coverage
25//!
26//! - **156** `CURLOPT` options
27//! - **49** `CURLINFO` queries
28//! - **41** `CURLcode` error codes
29//! - **57** exported C functions (all wrapped in `catch_unwind`)
30//!
31//! # Safety Invariants
32//!
33//! The following safety contracts apply throughout this crate:
34//!
35//! - **Handle pointers** (`*mut c_void` for easy/multi/share/url/mime handles):
36//!   All callers must provide valid, non-null pointers obtained from the
37//!   corresponding `_init` function. Every exported function null-checks its
38//!   handle argument before dereferencing. Handles are `Box`-allocated and cast
39//!   to `*mut c_void`; `Box::from_raw` reclaims ownership in `_cleanup`.
40//!
41//! - **C strings** (`*const c_char`): Callers must provide valid,
42//!   null-terminated strings. The helper `read_cstr()` combines null-check +
43//!   `CStr::from_ptr` + UTF-8 validation. Direct `CStr::from_ptr` calls
44//!   appear where `read_cstr` is insufficient (e.g., when the pointer type
45//!   differs or when non-UTF-8 data is acceptable).
46//!
47//! - **Output pointers** in `curl_easy_getinfo`: Callers must provide a valid
48//!   pointer to the expected output type (`*mut c_long`, `*mut f64`,
49//!   `*mut *const c_char`, `*mut i64`). Each match arm casts `out` to the
50//!   documented type and writes through it. The function null-checks `out`
51//!   before the match.
52//!
53//! - **Callback function pointers**: `std::mem::transmute` converts `*const
54//!   c_void` to the appropriate callback signature. Callers must ensure the
55//!   pointer is actually a function with the documented C signature. Callbacks
56//!   are invoked during `curl_easy_perform` with the corresponding `*data`
57//!   pointer passed as the user-data argument.
58//!
59//! - **`curl_slist` traversal**: Linked-list nodes are caller-allocated. The
60//!   list is walked via `(*node).next` until null. Each `node.data` is a
61//!   caller-owned C string. `curl_slist_free_all` reclaims all nodes.
62//!
63//! - **Panic safety**: All exported `#[no_mangle]` functions wrap their body in
64//!   `std::panic::catch_unwind` to prevent Rust panics from unwinding across
65//!   the FFI boundary.
66
67#![warn(missing_docs)]
68
69use std::ffi::{c_char, c_long, c_short, c_void, CStr};
70use std::ptr;
71
72// ───────────────────────── CURLcode ─────────────────────────
73
74/// `CURLcode` — result codes for easy handle operations.
75#[repr(C)]
76#[derive(Debug, Clone, Copy, PartialEq, Eq)]
77#[allow(non_camel_case_types, missing_docs)]
78pub enum CURLcode {
79    CURLE_OK = 0,
80    CURLE_UNSUPPORTED_PROTOCOL = 1,
81    CURLE_FAILED_INIT = 2,
82    CURLE_URL_MALFORMAT = 3,
83    CURLE_COULDNT_RESOLVE_PROXY = 5,
84    CURLE_COULDNT_RESOLVE_HOST = 6,
85    CURLE_COULDNT_CONNECT = 7,
86    CURLE_FTP_WEIRD_SERVER_REPLY = 8,
87    CURLE_REMOTE_ACCESS_DENIED = 9,
88    CURLE_HTTP2 = 16,
89    CURLE_HTTP_RETURNED_ERROR = 22,
90    CURLE_WRITE_ERROR = 23,
91    CURLE_READ_ERROR = 26,
92    CURLE_OUT_OF_MEMORY = 27,
93    CURLE_OPERATION_TIMEDOUT = 28,
94    CURLE_SSL_CONNECT_ERROR = 35,
95    CURLE_ABORTED_BY_CALLBACK = 42,
96    CURLE_BAD_FUNCTION_ARGUMENT = 43,
97    CURLE_UNKNOWN_OPTION = 48,
98    CURLE_GOT_NOTHING = 52,
99    CURLE_SEND_ERROR = 55,
100    CURLE_RECV_ERROR = 56,
101    CURLE_SSL_CERTPROBLEM = 58,
102    CURLE_PEER_FAILED_VERIFICATION = 60,
103    CURLE_FILESIZE_EXCEEDED = 63,
104    CURLE_LOGIN_DENIED = 67,
105    CURLE_TOO_MANY_REDIRECTS = 47,
106    CURLE_HTTP3 = 95,
107    CURLE_PARTIAL_FILE = 18,
108    CURLE_RANGE_ERROR = 33,
109    CURLE_AGAIN = 81,
110    CURLE_AUTH_ERROR = 94,
111    CURLE_UNRECOVERABLE_POLL = 99,
112    CURLE_FTP_COULDNT_RETR_FILE = 19,
113    CURLE_UPLOAD_FAILED = 25,
114    CURLE_LDAP_SEARCH_FAILED = 39,
115    CURLE_FUNCTION_NOT_FOUND = 41,
116    CURLE_INTERFACE_FAILED = 45,
117    CURLE_SSL_ENGINE_NOTFOUND = 53,
118    CURLE_SSL_ENGINE_SETFAILED = 54,
119    CURLE_RTSP_CSEQ_ERROR = 85,
120    CURLE_RTSP_SESSION_ERROR = 86,
121    CURLE_SSL_PINNEDPUBKEYNOTMATCH = 90,
122    CURLE_SSL_INVALIDCERTSTATUS = 91,
123}
124
125// ───────────────────────── CURLoption ─────────────────────────
126
127/// `CURLOPT` — option codes for `curl_easy_setopt`.
128#[repr(C)]
129#[derive(Debug, Clone, Copy, PartialEq, Eq)]
130#[allow(non_camel_case_types, missing_docs)]
131pub enum CURLoption {
132    // String options (CURLOPTTYPE_STRINGPOINT = 10000)
133    CURLOPT_WRITEDATA = 10001,
134    CURLOPT_URL = 10002,
135    CURLOPT_PROXY = 10004,
136    CURLOPT_USERPWD = 10005,
137    CURLOPT_RANGE = 10007,
138    CURLOPT_ERRORBUFFER = 10010,
139    CURLOPT_POSTFIELDS = 10015,
140    CURLOPT_USERAGENT = 10018,
141    CURLOPT_COOKIE = 10022,
142    CURLOPT_HTTPHEADER = 10023,
143    CURLOPT_SSLCERT = 10025,
144    CURLOPT_HEADERDATA = 10029,
145    CURLOPT_CUSTOMREQUEST = 10036,
146    CURLOPT_STDERR = 10037,
147    CURLOPT_CAINFO = 10065,
148    CURLOPT_SSLKEY = 10087,
149    CURLOPT_INTERFACE = 10062,
150    CURLOPT_SSL_CIPHER_LIST = 10083,
151    CURLOPT_ACCEPT_ENCODING = 10102,
152    CURLOPT_COOKIEFILE = 10031,
153    CURLOPT_COOKIEJAR = 10082,
154    CURLOPT_COOKIELIST = 10135,
155    CURLOPT_PROXYUSERPWD = 10006,
156    CURLOPT_NOPROXY = 10177,
157    CURLOPT_RESOLVE = 10203,
158    CURLOPT_PINNEDPUBLICKEY = 10230,
159    CURLOPT_UNIX_SOCKET_PATH = 10231,
160    CURLOPT_PROXY_CAINFO = 10246,
161    CURLOPT_PROXY_SSLCERT = 10254,
162    CURLOPT_PROXY_SSLKEY = 10255,
163    CURLOPT_READDATA = 10009,
164    CURLOPT_DEBUGDATA = 10095,
165    CURLOPT_DNS_SERVERS = 10211,
166    CURLOPT_TLSAUTH_TYPE = 10216,
167    CURLOPT_TLSAUTH_USERNAME = 10217,
168    CURLOPT_TLSAUTH_PASSWORD = 10218,
169    CURLOPT_DOH_URL = 10279,
170    CURLOPT_HSTS = 10300,
171    CURLOPT_PROTOCOLS_STR = 10318,
172    CURLOPT_REDIR_PROTOCOLS_STR = 10319,
173
174    // Long options (CURLOPTTYPE_LONG = 0)
175    CURLOPT_TIMEOUT = 13,
176    CURLOPT_LOW_SPEED_LIMIT = 19,
177    CURLOPT_LOW_SPEED_TIME = 20,
178    CURLOPT_SSLVERSION = 32,
179    CURLOPT_VERBOSE = 41,
180    CURLOPT_NOBODY = 44,
181    CURLOPT_FAILONERROR = 45,
182    CURLOPT_UPLOAD = 46,
183    CURLOPT_POST = 47,
184    CURLOPT_FOLLOWLOCATION = 52,
185    CURLOPT_PUT = 54,
186    CURLOPT_POSTFIELDSIZE = 60,
187    CURLOPT_HTTPPROXYTUNNEL = 61,
188    CURLOPT_SSL_VERIFYPEER = 64,
189    CURLOPT_MAXREDIRS = 68,
190    CURLOPT_FRESH_CONNECT = 74,
191    CURLOPT_FORBID_REUSE = 75,
192    CURLOPT_CONNECTTIMEOUT = 78,
193    CURLOPT_HTTPGET = 80,
194    CURLOPT_SSL_VERIFYHOST = 81,
195    CURLOPT_PROXYAUTH = 111,
196    CURLOPT_HTTPAUTH = 107,
197    CURLOPT_MAXFILESIZE = 114,
198    CURLOPT_PROXY_SSL_VERIFYPEER = 248,
199    CURLOPT_PROXY_SSL_VERIFYHOST = 249,
200    CURLOPT_TCP_NODELAY = 121,
201    CURLOPT_LOCALPORT = 139,
202    CURLOPT_TIMEOUT_MS = 155,
203    CURLOPT_CONNECTTIMEOUT_MS = 156,
204    CURLOPT_POSTREDIR = 161,
205    CURLOPT_DNS_CACHE_TIMEOUT = 92,
206    CURLOPT_TRANSFER_ENCODING = 207,
207    CURLOPT_EXPECT_100_TIMEOUT_MS = 227,
208    CURLOPT_PATH_AS_IS = 234,
209    CURLOPT_HAPPY_EYEBALLS_TIMEOUT_MS = 271,
210    CURLOPT_DNS_SHUFFLE_ADDRESSES = 275,
211    CURLOPT_UNRESTRICTED_AUTH = 105,
212    CURLOPT_IGNORE_CONTENT_LENGTH = 136,
213    CURLOPT_TCP_KEEPALIVE = 213,
214    CURLOPT_SSL_SESSIONID_CACHE = 150,
215    CURLOPT_PORT = 3,
216    CURLOPT_INFILESIZE = 14,
217    CURLOPT_RESUME_FROM = 21,
218    CURLOPT_PROXYPORT = 59,
219    CURLOPT_FILETIME = 69,
220    CURLOPT_MAXCONNECTS = 71,
221    CURLOPT_BUFFERSIZE = 98,
222    CURLOPT_PROXYTYPE = 101,
223    CURLOPT_IPRESOLVE = 113,
224    CURLOPT_FTP_FILEMETHOD = 138,
225    CURLOPT_PIPEWAIT = 237,
226    CURLOPT_STREAM_WEIGHT = 239,
227    CURLOPT_TCP_FASTOPEN = 244,
228    CURLOPT_SOCKS5_AUTH = 267,
229    CURLOPT_HTTP09_ALLOWED = 285,
230
231    // Off_t options (CURLOPTTYPE_OFF_T = 30000)
232    CURLOPT_POSTFIELDSIZE_LARGE = 30120,
233    CURLOPT_INFILESIZE_LARGE = 30115,
234    CURLOPT_MAXFILESIZE_LARGE = 30117,
235    CURLOPT_MAX_SEND_SPEED_LARGE = 30145,
236    CURLOPT_MAX_RECV_SPEED_LARGE = 30146,
237
238    // More string options
239    CURLOPT_CAPATH = 10097,
240    CURLOPT_REFERER = 10016,
241    CURLOPT_XOAUTH2_BEARER = 10220,
242    CURLOPT_AWS_SIGV4 = 10306,
243
244    // Pointer/object options
245    CURLOPT_SHARE = 10100,
246    CURLOPT_PRIVATE = 10103,
247    CURLOPT_MIMEPOST = 10269,
248
249    // Function options (CURLOPTTYPE_FUNCTIONPOINT = 20000)
250    CURLOPT_WRITEFUNCTION = 20011,
251    CURLOPT_READFUNCTION = 20012,
252    CURLOPT_PROGRESSFUNCTION = 20056,
253    CURLOPT_HEADERFUNCTION = 20079,
254    CURLOPT_DEBUGFUNCTION = 20094,
255    CURLOPT_SEEKFUNCTION = 20167,
256    CURLOPT_XFERINFOFUNCTION = 20219,
257
258    // Long options (progress control, HTTP version, etc.)
259    CURLOPT_NOPROGRESS = 43,
260    CURLOPT_AUTOREFERER = 58,
261    CURLOPT_HTTP_VERSION = 84,
262    CURLOPT_NOSIGNAL = 99,
263    CURLOPT_LOCALPORTRANGE = 164,
264
265    // Off_t options
266    CURLOPT_RESUME_FROM_LARGE = 30116,
267
268    // Pointer data options for callbacks
269    CURLOPT_PROGRESSDATA = 10057,
270    CURLOPT_SEEKDATA = 10168,
271}
272
273// ───────────────────────── CURLUcode / CURLUPart ─────────────────────────
274
275/// `CURLUcode` — result codes for URL API operations.
276#[repr(C)]
277#[derive(Debug, Clone, Copy, PartialEq, Eq)]
278#[allow(non_camel_case_types, missing_docs)]
279pub enum CURLUcode {
280    CURLUE_OK = 0,
281    CURLUE_BAD_HANDLE = 1,
282    CURLUE_BAD_PARTPOINTER = 2,
283    CURLUE_MALFORMED_INPUT = 3,
284    CURLUE_BAD_PORT_NUMBER = 4,
285    CURLUE_UNSUPPORTED_SCHEME = 5,
286    CURLUE_OUT_OF_MEMORY = 7,
287    CURLUE_NO_SCHEME = 8,
288    CURLUE_NO_HOST = 9,
289    CURLUE_UNKNOWN_PART = 11,
290}
291
292/// `CURLUPart` — part identifiers for URL manipulation.
293#[repr(C)]
294#[derive(Debug, Clone, Copy, PartialEq, Eq)]
295#[allow(non_camel_case_types, missing_docs)]
296pub enum CURLUPart {
297    CURLUPART_URL = 0,
298    CURLUPART_SCHEME = 1,
299    CURLUPART_USER = 2,
300    CURLUPART_PASSWORD = 3,
301    CURLUPART_OPTIONS = 4,
302    CURLUPART_HOST = 5,
303    CURLUPART_PORT = 6,
304    CURLUPART_PATH = 7,
305    CURLUPART_QUERY = 8,
306    CURLUPART_FRAGMENT = 9,
307    CURLUPART_ZONEID = 10,
308}
309
310// ───────────────────────── CURLINFO ─────────────────────────
311
312/// `CURLINFO` — info codes for `curl_easy_getinfo`.
313#[repr(C)]
314#[derive(Debug, Clone, Copy, PartialEq, Eq)]
315#[allow(non_camel_case_types, missing_docs)]
316pub enum CURLINFO {
317    CURLINFO_EFFECTIVE_URL = 0x0010_0001,
318    CURLINFO_RESPONSE_CODE = 0x0020_0002,
319    CURLINFO_TOTAL_TIME = 0x0030_0003,
320    CURLINFO_NAMELOOKUP_TIME = 0x0030_0004,
321    CURLINFO_CONNECT_TIME = 0x0030_0005,
322    CURLINFO_SIZE_UPLOAD = 0x0030_0007,
323    CURLINFO_SIZE_DOWNLOAD = 0x0030_0008,
324    CURLINFO_SPEED_DOWNLOAD = 0x0030_0009,
325    CURLINFO_SPEED_UPLOAD = 0x0030_000A,
326    CURLINFO_HEADER_SIZE = 0x0020_000B,
327    CURLINFO_FILETIME = 0x0020_000E,
328    CURLINFO_CONTENT_LENGTH_DOWNLOAD = 0x0030_000F,
329    CURLINFO_CONTENT_LENGTH_UPLOAD = 0x0030_0010,
330    CURLINFO_PRETRANSFER_TIME = 0x0030_000E,
331    CURLINFO_STARTTRANSFER_TIME = 0x0030_0011,
332    CURLINFO_CONTENT_TYPE = 0x0010_0012,
333    CURLINFO_REDIRECT_COUNT = 0x0020_0014,
334    CURLINFO_SSL_VERIFYRESULT = 0x0020_000D,
335    CURLINFO_PRIVATE = 0x0010_0015,
336    CURLINFO_OS_ERRNO = 0x0020_0019,
337    CURLINFO_PRIMARY_IP = 0x0010_0020,
338    CURLINFO_NUM_CONNECTS = 0x0020_0026,
339    CURLINFO_LOCAL_IP = 0x0010_0029,
340    CURLINFO_REDIRECT_URL = 0x0010_0031,
341    CURLINFO_HTTP_VERSION = 0x0020_0032,
342    CURLINFO_APPCONNECT_TIME = 0x0030_0033,
343    CURLINFO_CONDITION_UNMET = 0x0020_0035,
344    CURLINFO_PRIMARY_PORT = 0x0020_0040,
345    CURLINFO_LOCAL_PORT = 0x0020_0042,
346    CURLINFO_SCHEME = 0x0010_0044,
347    CURLINFO_REDIRECT_TIME = 0x0030_0013,
348    CURLINFO_TOTAL_TIME_T = 0x0060_003E,
349    CURLINFO_NAMELOOKUP_TIME_T = 0x0060_003F,
350    CURLINFO_CONNECT_TIME_T = 0x0060_0040,
351    CURLINFO_PRETRANSFER_TIME_T = 0x0060_0041,
352    CURLINFO_STARTTRANSFER_TIME_T = 0x0060_0042,
353    CURLINFO_REDIRECT_TIME_T = 0x0060_0043,
354    CURLINFO_APPCONNECT_TIME_T = 0x0060_0044,
355    CURLINFO_RETRY_AFTER = 0x0020_003A,
356    CURLINFO_SIZE_UPLOAD_T = 0x0060_0045,
357    CURLINFO_SIZE_DOWNLOAD_T = 0x0060_0046,
358    CURLINFO_SPEED_DOWNLOAD_T = 0x0060_0047,
359    CURLINFO_SPEED_UPLOAD_T = 0x0060_0048,
360    CURLINFO_REQUEST_SIZE = 0x0020_000C,
361    CURLINFO_HTTP_CONNECTCODE = 0x0020_0016,
362    CURLINFO_HTTPAUTH_AVAIL = 0x0020_0017,
363    CURLINFO_PROXYAUTH_AVAIL = 0x0020_0018,
364}
365
366// ───────────────────────── CURLMcode ─────────────────────────
367
368/// `CURLMcode` — result codes for multi handle operations.
369#[repr(C)]
370#[derive(Debug, Clone, Copy, PartialEq, Eq)]
371#[allow(non_camel_case_types, missing_docs)]
372pub enum CURLMcode {
373    CURLM_OK = 0,
374    CURLM_BAD_HANDLE = -1,
375    CURLM_BAD_EASY_HANDLE = -2,
376    CURLM_OUT_OF_MEMORY = -3,
377    CURLM_INTERNAL_ERROR = -4,
378    CURLM_UNKNOWN_OPTION = -6,
379}
380
381/// `CURLMSG` — message types from `curl_multi_info_read`.
382#[repr(C)]
383#[derive(Debug, Clone, Copy, PartialEq, Eq)]
384#[allow(non_camel_case_types, missing_docs)]
385pub enum CURLMSG {
386    CURLMSG_DONE = 1,
387}
388
389/// `CURLMsg` — completion message from `curl_multi_info_read`.
390#[repr(C)]
391#[allow(non_camel_case_types, missing_docs)]
392pub struct CURLMsg {
393    pub msg: CURLMSG,
394    pub easy_handle: *mut c_void,
395    pub result: CURLcode,
396}
397
398/// `CURLMoption` — option codes for `curl_multi_setopt`.
399#[repr(C)]
400#[derive(Debug, Clone, Copy, PartialEq, Eq)]
401#[allow(non_camel_case_types, missing_docs)]
402pub enum CURLMoption {
403    CURLMOPT_SOCKETFUNCTION = 20001,
404    CURLMOPT_SOCKETDATA = 10002,
405    CURLMOPT_PIPELINING = 3,
406    CURLMOPT_TIMERFUNCTION = 20004,
407    CURLMOPT_TIMERDATA = 10005,
408    CURLMOPT_MAXCONNECTS = 6,
409    CURLMOPT_MAX_HOST_CONNECTIONS = 7,
410    CURLMOPT_MAX_TOTAL_CONNECTIONS = 13,
411}
412
413/// `curl_waitfd` — extra file descriptor for `curl_multi_wait`/`curl_multi_poll`.
414#[repr(C)]
415#[allow(non_camel_case_types, missing_docs)]
416pub struct curl_waitfd {
417    pub fd: c_long,
418    pub events: c_short,
419    pub revents: c_short,
420}
421
422/// `curl_blob` — in-memory binary data for TLS certificate/key options.
423///
424/// Equivalent to libcurl's `struct curl_blob`. Used with `CURLOPT_SSLCERT_BLOB`,
425/// `CURLOPT_SSLKEY_BLOB`, and `CURLOPT_CAINFO_BLOB`.
426#[repr(C)]
427#[allow(non_camel_case_types, missing_docs)]
428pub struct curl_blob {
429    pub data: *const c_void,
430    pub len: usize,
431    pub flags: u32,
432}
433
434/// Socket callback type matching libcurl's `CURLMOPT_SOCKETFUNCTION`.
435#[allow(non_camel_case_types)]
436type CurlSocketCallback =
437    unsafe extern "C" fn(*mut c_void, c_long, c_long, *mut c_void, *mut c_void) -> c_long;
438
439/// Timer callback type matching libcurl's `CURLMOPT_TIMERFUNCTION`.
440#[allow(non_camel_case_types)]
441type CurlTimerCallback = unsafe extern "C" fn(*mut c_void, c_long, *mut c_void) -> c_long;
442
443// ───────────────────────── Callback types ─────────────────────────
444
445/// Write callback type matching libcurl's `CURLOPT_WRITEFUNCTION`.
446/// The data pointer is const — the callback receives data from the library.
447type WriteCallback = unsafe extern "C" fn(*const c_char, usize, usize, *mut c_void) -> usize;
448
449/// Header callback type matching libcurl's `CURLOPT_HEADERFUNCTION`.
450/// The data pointer is const — the callback receives header data from the library.
451type HeaderCallback = unsafe extern "C" fn(*const c_char, usize, usize, *mut c_void) -> usize;
452
453/// Read callback type matching libcurl's `CURLOPT_READFUNCTION`.
454///
455/// Called to supply upload data. Returns number of bytes written to buffer.
456/// Return 0 to signal end of data, `CURL_READFUNC_ABORT` (0x10000000) to abort.
457type ReadCallback = unsafe extern "C" fn(*mut c_char, usize, usize, *mut c_void) -> usize;
458
459/// Debug callback type matching libcurl's `CURLOPT_DEBUGFUNCTION`.
460///
461/// Called with debug information during transfer. The `info_type` parameter
462/// indicates the type of data (text, header in/out, data in/out).
463/// The data pointer is const — the callback receives debug info from the library.
464type DebugCallback =
465    unsafe extern "C" fn(*mut c_void, c_long, *const c_char, usize, *mut c_void) -> c_long;
466
467/// Progress callback type matching libcurl's `CURLOPT_PROGRESSFUNCTION`.
468///
469/// Called with download/upload progress. Parameters: clientp, dltotal, dlnow, ultotal, ulnow.
470/// Return non-zero to abort the transfer.
471type ProgressCallback = unsafe extern "C" fn(*mut c_void, f64, f64, f64, f64) -> c_long;
472
473/// Transfer info callback type matching libcurl's `CURLOPT_XFERINFOFUNCTION`.
474///
475/// Modern replacement for `CURLOPT_PROGRESSFUNCTION` using `curl_off_t` (i64).
476/// Return non-zero to abort the transfer.
477type XferInfoCallback = unsafe extern "C" fn(*mut c_void, i64, i64, i64, i64) -> c_long;
478
479/// Seek callback type matching libcurl's `CURLOPT_SEEKFUNCTION`.
480///
481/// Called to seek in the input stream. Returns 0 on success, 1 on failure, 2 for can't seek.
482type SeekCallback = unsafe extern "C" fn(*mut c_void, i64, c_long) -> c_long;
483
484/// Interleave callback type matching libcurl's `CURLOPT_INTERLEAVEFUNCTION`.
485///
486/// Called for each RTP interleaved packet received during an RTSP transfer.
487/// Parameters: ptr (data), size, nmemb, userdata. Returns bytes handled.
488type InterleaveCallback = unsafe extern "C" fn(*mut c_void, usize, usize, *mut c_void) -> usize;
489
490// ───────────────────────── CURLSHcode / CURLSHoption ─────────────────────────
491
492/// `CURLSHcode` — result codes for share handle operations.
493#[repr(C)]
494#[derive(Debug, Clone, Copy, PartialEq, Eq)]
495#[allow(non_camel_case_types, missing_docs)]
496pub enum CURLSHcode {
497    CURLSHE_OK = 0,
498    CURLSHE_BAD_OPTION = 1,
499    CURLSHE_IN_USE = 2,
500    CURLSHE_INVALID = 3,
501    CURLSHE_NOMEM = 4,
502    CURLSHE_NOT_BUILT_IN = 5,
503}
504
505/// `CURLSHoption` — option codes for `curl_share_setopt`.
506#[repr(C)]
507#[derive(Debug, Clone, Copy, PartialEq, Eq)]
508#[allow(non_camel_case_types, missing_docs)]
509pub enum CURLSHoption {
510    CURLSHOPT_SHARE = 1,
511    CURLSHOPT_UNSHARE = 2,
512    CURLSHOPT_LOCKFUNC = 3,
513    CURLSHOPT_UNLOCKFUNC = 4,
514}
515
516// ───────────────────────── curl_mime ─────────────────────────
517
518/// Internal state for a MIME handle.
519struct MimeHandle {
520    form: liburlx::MultipartForm,
521}
522
523/// Internal state for a MIME part being built.
524struct MimePartHandle {
525    name: Option<String>,
526    data: Option<Vec<u8>>,
527    filename: Option<String>,
528    mime_type: Option<String>,
529}
530
531/// `curl_mime_init` — create a new MIME handle.
532///
533/// # Safety
534///
535/// `easy` must be a valid pointer from `curl_easy_init` (used for context only).
536/// The returned handle must be freed with `curl_mime_free`.
537#[no_mangle]
538pub unsafe extern "C" fn curl_mime_init(_easy: *mut c_void) -> *mut c_void {
539    let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
540        let handle = Box::new(MimeHandle { form: liburlx::MultipartForm::new() });
541        Box::into_raw(handle).cast::<c_void>()
542    }));
543    result.unwrap_or(ptr::null_mut())
544}
545
546/// `curl_mime_addpart` — add a new part to a MIME handle.
547///
548/// # Safety
549///
550/// `mime` must be a valid pointer from `curl_mime_init`.
551/// The returned part pointer is valid until `curl_mime_free` is called on the parent.
552#[no_mangle]
553pub unsafe extern "C" fn curl_mime_addpart(mime: *mut c_void) -> *mut c_void {
554    let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
555        if mime.is_null() {
556            return ptr::null_mut();
557        }
558        let part =
559            Box::new(MimePartHandle { name: None, data: None, filename: None, mime_type: None });
560        Box::into_raw(part).cast::<c_void>()
561    }));
562    result.unwrap_or(ptr::null_mut())
563}
564
565/// `curl_mime_name` — set the name of a MIME part.
566///
567/// # Safety
568///
569/// `part` must be a valid pointer from `curl_mime_addpart`.
570/// `name` must be a valid null-terminated C string.
571#[no_mangle]
572pub unsafe extern "C" fn curl_mime_name(part: *mut c_void, name: *const c_char) -> CURLcode {
573    let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
574        if part.is_null() || name.is_null() {
575            return CURLcode::CURLE_BAD_FUNCTION_ARGUMENT;
576        }
577        // SAFETY: Caller guarantees part is from curl_mime_addpart
578        let p = unsafe { &mut *part.cast::<MimePartHandle>() };
579        // SAFETY: Caller guarantees name is a null-terminated C string
580        let s = unsafe { CStr::from_ptr(name) };
581        match s.to_str() {
582            Ok(name_str) => {
583                p.name = Some(name_str.to_string());
584                CURLcode::CURLE_OK
585            }
586            Err(_) => CURLcode::CURLE_BAD_FUNCTION_ARGUMENT,
587        }
588    }));
589    result.unwrap_or(CURLcode::CURLE_UNKNOWN_OPTION)
590}
591
592/// `curl_mime_data` — set data for a MIME part.
593///
594/// # Safety
595///
596/// `part` must be a valid pointer from `curl_mime_addpart`.
597/// `data` must point to at least `datasize` bytes.
598/// If `datasize` is `usize::MAX`, `data` is treated as a null-terminated string.
599#[no_mangle]
600pub unsafe extern "C" fn curl_mime_data(
601    part: *mut c_void,
602    data: *const c_char,
603    datasize: usize,
604) -> CURLcode {
605    let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
606        if part.is_null() || data.is_null() {
607            return CURLcode::CURLE_BAD_FUNCTION_ARGUMENT;
608        }
609        // SAFETY: Caller guarantees part is from curl_mime_addpart
610        let p = unsafe { &mut *part.cast::<MimePartHandle>() };
611
612        let bytes = if datasize == usize::MAX {
613            // CURL_ZERO_TERMINATED — treat as null-terminated string
614            // SAFETY: Caller guarantees data is null-terminated
615            let s = unsafe { CStr::from_ptr(data) };
616            s.to_bytes().to_vec()
617        } else {
618            // SAFETY: Caller guarantees data points to at least datasize bytes
619            unsafe { std::slice::from_raw_parts(data.cast::<u8>(), datasize) }.to_vec()
620        };
621
622        p.data = Some(bytes);
623        CURLcode::CURLE_OK
624    }));
625    result.unwrap_or(CURLcode::CURLE_UNKNOWN_OPTION)
626}
627
628/// `curl_mime_filename` — set the filename for a MIME part.
629///
630/// # Safety
631///
632/// `part` must be a valid pointer from `curl_mime_addpart`.
633/// `filename` must be a valid null-terminated C string.
634#[no_mangle]
635pub unsafe extern "C" fn curl_mime_filename(
636    part: *mut c_void,
637    filename: *const c_char,
638) -> CURLcode {
639    let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
640        if part.is_null() || filename.is_null() {
641            return CURLcode::CURLE_BAD_FUNCTION_ARGUMENT;
642        }
643        // SAFETY: Caller guarantees part is from curl_mime_addpart
644        let p = unsafe { &mut *part.cast::<MimePartHandle>() };
645        // SAFETY: Caller guarantees filename is a null-terminated C string
646        let s = unsafe { CStr::from_ptr(filename) };
647        match s.to_str() {
648            Ok(f) => {
649                p.filename = Some(f.to_string());
650                CURLcode::CURLE_OK
651            }
652            Err(_) => CURLcode::CURLE_BAD_FUNCTION_ARGUMENT,
653        }
654    }));
655    result.unwrap_or(CURLcode::CURLE_UNKNOWN_OPTION)
656}
657
658/// `curl_mime_type` — set the MIME type for a MIME part.
659///
660/// # Safety
661///
662/// `part` must be a valid pointer from `curl_mime_addpart`.
663/// `mimetype` must be a valid null-terminated C string.
664#[no_mangle]
665pub unsafe extern "C" fn curl_mime_type(part: *mut c_void, mimetype: *const c_char) -> CURLcode {
666    let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
667        if part.is_null() || mimetype.is_null() {
668            return CURLcode::CURLE_BAD_FUNCTION_ARGUMENT;
669        }
670        // SAFETY: Caller guarantees part is from curl_mime_addpart
671        let p = unsafe { &mut *part.cast::<MimePartHandle>() };
672        // SAFETY: Caller guarantees mimetype is a null-terminated C string
673        let s = unsafe { CStr::from_ptr(mimetype) };
674        match s.to_str() {
675            Ok(t) => {
676                p.mime_type = Some(t.to_string());
677                CURLcode::CURLE_OK
678            }
679            Err(_) => CURLcode::CURLE_BAD_FUNCTION_ARGUMENT,
680        }
681    }));
682    result.unwrap_or(CURLcode::CURLE_UNKNOWN_OPTION)
683}
684
685/// `curl_mime_free` — free a MIME handle and all its parts.
686///
687/// # Safety
688///
689/// `mime` must be a valid pointer from `curl_mime_init`, or null.
690/// After this call, `mime` must not be used.
691#[no_mangle]
692pub unsafe extern "C" fn curl_mime_free(mime: *mut c_void) {
693    let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
694        if !mime.is_null() {
695            // SAFETY: Caller guarantees mime is from curl_mime_init
696            let _ = unsafe { Box::from_raw(mime.cast::<MimeHandle>()) };
697        }
698    }));
699}
700
701/// Helper to finalize a MIME part into the parent MIME form.
702///
703/// # Safety
704///
705/// `mime` must be a valid `MimeHandle`, `part` a valid `MimePartHandle`.
706unsafe fn finalize_mime_part(mime: *mut c_void, part: *mut c_void) {
707    if mime.is_null() || part.is_null() {
708        return;
709    }
710    // SAFETY: Caller guarantees these are valid handles
711    let m = unsafe { &mut *mime.cast::<MimeHandle>() };
712    let p = unsafe { Box::from_raw(part.cast::<MimePartHandle>()) };
713
714    if let (Some(name), Some(data)) = (&p.name, &p.data) {
715        if let Some(ref filename) = p.filename {
716            m.form.file_data(name, filename, data);
717        } else {
718            // Treat as text field
719            if let Ok(text) = std::str::from_utf8(data) {
720                m.form.field(name, text);
721            } else {
722                // Binary data without filename — use file_data with a default name
723                m.form.file_data(name, "data", data);
724            }
725        }
726    }
727}
728
729// ───────────────────────── curl_share ─────────────────────────
730
731/// `curl_share_init` — create a new share handle.
732///
733/// # Safety
734///
735/// Returns a new handle that must be freed with `curl_share_cleanup`.
736#[no_mangle]
737pub extern "C" fn curl_share_init() -> *mut c_void {
738    let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
739        let share = Box::new(liburlx::Share::new());
740        Box::into_raw(share).cast::<c_void>()
741    }));
742    result.unwrap_or(ptr::null_mut())
743}
744
745/// `curl_share_cleanup` — free a share handle.
746///
747/// # Safety
748///
749/// `share` must be a valid pointer from `curl_share_init`, or null.
750#[no_mangle]
751pub unsafe extern "C" fn curl_share_cleanup(share: *mut c_void) -> CURLSHcode {
752    let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
753        if share.is_null() {
754            return CURLSHcode::CURLSHE_INVALID;
755        }
756        // SAFETY: Caller guarantees share is from curl_share_init
757        let _ = unsafe { Box::from_raw(share.cast::<liburlx::Share>()) };
758        CURLSHcode::CURLSHE_OK
759    }));
760    result.unwrap_or(CURLSHcode::CURLSHE_INVALID)
761}
762
763/// `curl_share_setopt` — set options on a share handle.
764///
765/// # Safety
766///
767/// `share` must be a valid pointer from `curl_share_init`.
768/// For `CURLSHOPT_SHARE`/`CURLSHOPT_UNSHARE`, `value` is a `CURL_LOCK_DATA_*` constant.
769#[no_mangle]
770pub unsafe extern "C" fn curl_share_setopt(
771    share: *mut c_void,
772    option: c_long,
773    value: *const c_void,
774) -> CURLSHcode {
775    let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
776        if share.is_null() {
777            return CURLSHcode::CURLSHE_INVALID;
778        }
779
780        // SAFETY: Caller guarantees share is from curl_share_init
781        let s = unsafe { &mut *share.cast::<liburlx::Share>() };
782
783        match option {
784            // CURLSHOPT_SHARE = 1
785            1 => {
786                let lock_data = value as c_long;
787                match lock_data {
788                    // CURL_LOCK_DATA_COOKIE = 2
789                    2 => {
790                        s.add(liburlx::ShareType::Cookies);
791                        CURLSHcode::CURLSHE_OK
792                    }
793                    // CURL_LOCK_DATA_DNS = 3
794                    3 => {
795                        s.add(liburlx::ShareType::Dns);
796                        CURLSHcode::CURLSHE_OK
797                    }
798                    _ => CURLSHcode::CURLSHE_BAD_OPTION,
799                }
800            }
801            // CURLSHOPT_UNSHARE = 2
802            2 => {
803                let lock_data = value as c_long;
804                match lock_data {
805                    2 => {
806                        s.remove(liburlx::ShareType::Cookies);
807                        CURLSHcode::CURLSHE_OK
808                    }
809                    3 => {
810                        s.remove(liburlx::ShareType::Dns);
811                        CURLSHcode::CURLSHE_OK
812                    }
813                    _ => CURLSHcode::CURLSHE_BAD_OPTION,
814                }
815            }
816            // CURLSHOPT_LOCKFUNC = 3, CURLSHOPT_UNLOCKFUNC = 4
817            // Accept but ignore — our Share uses Arc<Mutex> internally
818            3 | 4 => CURLSHcode::CURLSHE_OK,
819            _ => CURLSHcode::CURLSHE_BAD_OPTION,
820        }
821    }));
822    result.unwrap_or(CURLSHcode::CURLSHE_INVALID)
823}
824
825/// `curl_share_strerror` — return a human-readable share error message.
826///
827/// # Safety
828///
829/// The returned pointer is valid for the lifetime of the program.
830#[no_mangle]
831#[allow(clippy::missing_const_for_fn)]
832pub extern "C" fn curl_share_strerror(code: CURLSHcode) -> *const c_char {
833    let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
834        let msg = match code {
835            CURLSHcode::CURLSHE_OK => c"No error",
836            CURLSHcode::CURLSHE_BAD_OPTION => c"Bad option in share call",
837            CURLSHcode::CURLSHE_IN_USE => c"Share already in use",
838            CURLSHcode::CURLSHE_INVALID => c"Invalid share handle",
839            CURLSHcode::CURLSHE_NOMEM => c"Out of memory",
840            CURLSHcode::CURLSHE_NOT_BUILT_IN => c"Feature not available",
841        };
842        msg.as_ptr()
843    }));
844    result.unwrap_or(c"Unknown error".as_ptr())
845}
846
847// ───────────────────────── curl_url (URL API) ─────────────────────────
848
849/// Mutable URL handle for the curl URL API.
850///
851/// Stores individual URL components that can be set/get independently.
852/// Components are lazily reassembled into a full URL string when requested.
853struct UrlHandle {
854    scheme: Option<String>,
855    user: Option<String>,
856    password: Option<String>,
857    host: Option<String>,
858    port: Option<u16>,
859    path: Option<String>,
860    query: Option<String>,
861    fragment: Option<String>,
862    /// Cached reassembled URL string (invalidated on set).
863    cached_url: Option<String>,
864}
865
866impl UrlHandle {
867    const fn new() -> Self {
868        Self {
869            scheme: None,
870            user: None,
871            password: None,
872            host: None,
873            port: None,
874            path: None,
875            query: None,
876            fragment: None,
877            cached_url: None,
878        }
879    }
880
881    /// Reassemble the URL from components.
882    fn reassemble(&mut self) -> String {
883        let scheme = self.scheme.as_deref().unwrap_or("https");
884        let mut url = format!("{scheme}://");
885        if let Some(ref user) = self.user {
886            url.push_str(user);
887            if let Some(ref pass) = self.password {
888                url.push(':');
889                url.push_str(pass);
890            }
891            url.push('@');
892        }
893        if let Some(ref host) = self.host {
894            url.push_str(host);
895        }
896        if let Some(port) = self.port {
897            url.push(':');
898            url.push_str(&port.to_string());
899        }
900        url.push_str(self.path.as_deref().unwrap_or("/"));
901        if let Some(ref query) = self.query {
902            url.push('?');
903            url.push_str(query);
904        }
905        if let Some(ref fragment) = self.fragment {
906            url.push('#');
907            url.push_str(fragment);
908        }
909        self.cached_url = Some(url.clone());
910        url
911    }
912
913    /// Parse a full URL into components.
914    fn set_url(&mut self, url_str: &str) -> CURLUcode {
915        match liburlx::Url::parse(url_str) {
916            Ok(parsed) => {
917                self.scheme = Some(parsed.scheme().to_string());
918                let user = parsed.username();
919                self.user = if user.is_empty() { None } else { Some(user.to_string()) };
920                self.password = parsed.password().map(String::from);
921                self.host = parsed.host_str().map(String::from);
922                self.port = parsed.port();
923                let path = parsed.path();
924                self.path = Some(path.to_string());
925                self.query = parsed.query().map(String::from);
926                self.fragment = parsed.fragment().map(String::from);
927                self.cached_url = Some(parsed.as_str().to_string());
928                CURLUcode::CURLUE_OK
929            }
930            Err(_) => CURLUcode::CURLUE_MALFORMED_INPUT,
931        }
932    }
933}
934
935impl Clone for UrlHandle {
936    fn clone(&self) -> Self {
937        Self {
938            scheme: self.scheme.clone(),
939            user: self.user.clone(),
940            password: self.password.clone(),
941            host: self.host.clone(),
942            port: self.port,
943            path: self.path.clone(),
944            query: self.query.clone(),
945            fragment: self.fragment.clone(),
946            cached_url: self.cached_url.clone(),
947        }
948    }
949}
950
951/// `curl_url` — create a new URL handle.
952///
953/// # Safety
954///
955/// Returns a new handle that must be freed with `curl_url_cleanup`.
956#[no_mangle]
957pub extern "C" fn curl_url() -> *mut c_void {
958    let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
959        let handle = Box::new(UrlHandle::new());
960        Box::into_raw(handle).cast::<c_void>()
961    }));
962    result.unwrap_or(ptr::null_mut())
963}
964
965/// `curl_url_cleanup` — free a URL handle.
966///
967/// # Safety
968///
969/// `handle` must be a valid pointer from `curl_url`, or null.
970#[no_mangle]
971pub unsafe extern "C" fn curl_url_cleanup(handle: *mut c_void) {
972    let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
973        if !handle.is_null() {
974            // SAFETY: Caller guarantees handle is from curl_url
975            let _ = unsafe { Box::from_raw(handle.cast::<UrlHandle>()) };
976        }
977    }));
978}
979
980/// `curl_url_dup` — duplicate a URL handle.
981///
982/// # Safety
983///
984/// `handle` must be a valid pointer from `curl_url`.
985#[no_mangle]
986pub unsafe extern "C" fn curl_url_dup(handle: *mut c_void) -> *mut c_void {
987    let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
988        if handle.is_null() {
989            return ptr::null_mut();
990        }
991        // SAFETY: Caller guarantees handle is from curl_url
992        let h = unsafe { &*handle.cast::<UrlHandle>() };
993        let dup = Box::new(h.clone());
994        Box::into_raw(dup).cast::<c_void>()
995    }));
996    result.unwrap_or(ptr::null_mut())
997}
998
999/// `curl_url_set` — set a URL component.
1000///
1001/// # Safety
1002///
1003/// `handle` must be a valid pointer from `curl_url`.
1004/// `content` must be a valid null-terminated C string (or null to clear).
1005#[no_mangle]
1006pub unsafe extern "C" fn curl_url_set(
1007    handle: *mut c_void,
1008    what: c_long,
1009    content: *const c_char,
1010    _flags: c_long,
1011) -> CURLUcode {
1012    let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1013        if handle.is_null() {
1014            return CURLUcode::CURLUE_BAD_HANDLE;
1015        }
1016
1017        // SAFETY: Caller guarantees handle is from curl_url
1018        let h = unsafe { &mut *handle.cast::<UrlHandle>() };
1019
1020        // Null content clears the part
1021        let value = if content.is_null() {
1022            None
1023        } else {
1024            // SAFETY: Caller guarantees content is null-terminated
1025            match unsafe { CStr::from_ptr(content) }.to_str() {
1026                Ok(s) => Some(s.to_string()),
1027                Err(_) => return CURLUcode::CURLUE_MALFORMED_INPUT,
1028            }
1029        };
1030
1031        h.cached_url = None; // Invalidate cache
1032
1033        match what {
1034            // CURLUPART_URL = 0
1035            0 => {
1036                if let Some(ref url_str) = value {
1037                    return h.set_url(url_str);
1038                }
1039                // Clear all components
1040                *h = UrlHandle::new();
1041                CURLUcode::CURLUE_OK
1042            }
1043            // CURLUPART_SCHEME = 1
1044            1 => {
1045                h.scheme = value;
1046                CURLUcode::CURLUE_OK
1047            }
1048            // CURLUPART_USER = 2
1049            2 => {
1050                h.user = value;
1051                CURLUcode::CURLUE_OK
1052            }
1053            // CURLUPART_PASSWORD = 3
1054            3 => {
1055                h.password = value;
1056                CURLUcode::CURLUE_OK
1057            }
1058            // CURLUPART_OPTIONS = 4, CURLUPART_ZONEID = 10 — accept but ignore
1059            4 | 10 => CURLUcode::CURLUE_OK,
1060            // CURLUPART_HOST = 5
1061            5 => {
1062                h.host = value;
1063                CURLUcode::CURLUE_OK
1064            }
1065            // CURLUPART_PORT = 6
1066            6 => {
1067                if let Some(ref port_str) = value {
1068                    match port_str.parse::<u16>() {
1069                        Ok(port) => {
1070                            h.port = Some(port);
1071                            CURLUcode::CURLUE_OK
1072                        }
1073                        Err(_) => CURLUcode::CURLUE_BAD_PORT_NUMBER,
1074                    }
1075                } else {
1076                    h.port = None;
1077                    CURLUcode::CURLUE_OK
1078                }
1079            }
1080            // CURLUPART_PATH = 7
1081            7 => {
1082                h.path = value;
1083                CURLUcode::CURLUE_OK
1084            }
1085            // CURLUPART_QUERY = 8
1086            8 => {
1087                h.query = value;
1088                CURLUcode::CURLUE_OK
1089            }
1090            // CURLUPART_FRAGMENT = 9
1091            9 => {
1092                h.fragment = value;
1093                CURLUcode::CURLUE_OK
1094            }
1095            _ => CURLUcode::CURLUE_UNKNOWN_PART,
1096        }
1097    }));
1098    result.unwrap_or(CURLUcode::CURLUE_BAD_HANDLE)
1099}
1100
1101/// `curl_url_get` — get a URL component.
1102///
1103/// The returned string is allocated and must be freed by the caller with `libc::free`
1104/// or `curl_free`. For simplicity, we allocate via a leaked `CString`.
1105///
1106/// # Safety
1107///
1108/// `handle` must be a valid pointer from `curl_url`.
1109/// `part` must be a valid pointer to `*mut c_char`.
1110#[no_mangle]
1111pub unsafe extern "C" fn curl_url_get(
1112    handle: *mut c_void,
1113    what: c_long,
1114    part: *mut *mut c_char,
1115    _flags: c_long,
1116) -> CURLUcode {
1117    let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1118        if handle.is_null() {
1119            return CURLUcode::CURLUE_BAD_HANDLE;
1120        }
1121        if part.is_null() {
1122            return CURLUcode::CURLUE_BAD_PARTPOINTER;
1123        }
1124
1125        // SAFETY: Caller guarantees handle is from curl_url
1126        let h = unsafe { &mut *handle.cast::<UrlHandle>() };
1127
1128        let component: Option<String> = match what {
1129            // CURLUPART_URL = 0
1130            0 => Some(h.reassemble()),
1131            // CURLUPART_SCHEME = 1
1132            1 => h.scheme.clone(),
1133            // CURLUPART_USER = 2
1134            2 => h.user.clone(),
1135            // CURLUPART_PASSWORD = 3
1136            3 => h.password.clone(),
1137            // CURLUPART_OPTIONS = 4, CURLUPART_ZONEID = 10 — not stored
1138            4 | 10 => None,
1139            // CURLUPART_HOST = 5
1140            5 => h.host.clone(),
1141            // CURLUPART_PORT = 6
1142            6 => h.port.map(|p| p.to_string()),
1143            // CURLUPART_PATH = 7
1144            7 => h.path.clone(),
1145            // CURLUPART_QUERY = 8
1146            8 => h.query.clone(),
1147            // CURLUPART_FRAGMENT = 9
1148            9 => h.fragment.clone(),
1149            _ => return CURLUcode::CURLUE_UNKNOWN_PART,
1150        };
1151
1152        if let Some(s) = component {
1153            // Allocate a C string for the result
1154            std::ffi::CString::new(s).map_or(CURLUcode::CURLUE_OUT_OF_MEMORY, |cstr| {
1155                // SAFETY: part is a valid pointer
1156                unsafe {
1157                    *part = cstr.into_raw();
1158                }
1159                CURLUcode::CURLUE_OK
1160            })
1161        } else {
1162            // Part not set
1163            // SAFETY: part is a valid pointer
1164            unsafe {
1165                *part = ptr::null_mut();
1166            }
1167            CURLUcode::CURLUE_OK
1168        }
1169    }));
1170    result.unwrap_or(CURLUcode::CURLUE_BAD_HANDLE)
1171}
1172
1173/// `curl_free` — free memory allocated by curl functions.
1174///
1175/// # Safety
1176///
1177/// `ptr` must be a pointer returned by curl functions (e.g., `curl_url_get`), or null.
1178#[no_mangle]
1179pub unsafe extern "C" fn curl_free(ptr: *mut c_void) {
1180    let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1181        if !ptr.is_null() {
1182            // SAFETY: ptr was allocated via CString::into_raw
1183            let _ = unsafe { std::ffi::CString::from_raw(ptr.cast::<c_char>()) };
1184        }
1185    }));
1186}
1187
1188// ───────────────────────── curl_slist ─────────────────────────
1189
1190/// Linked list node for string data (e.g., HTTP headers).
1191///
1192/// Equivalent to libcurl's `struct curl_slist`.
1193#[repr(C)]
1194pub struct curl_slist {
1195    /// The string data for this node.
1196    pub data: *mut c_char,
1197    /// Pointer to the next node, or null.
1198    pub next: *mut Self,
1199}
1200
1201/// `curl_slist_append` — append a string to a linked list.
1202///
1203/// # Safety
1204///
1205/// `data` must be a valid null-terminated C string.
1206/// `list` can be null (creates a new list) or a valid `curl_slist` pointer.
1207#[no_mangle]
1208pub unsafe extern "C" fn curl_slist_append(
1209    list: *mut curl_slist,
1210    data: *const c_char,
1211) -> *mut curl_slist {
1212    let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1213        if data.is_null() {
1214            return list;
1215        }
1216
1217        // SAFETY: Caller guarantees data is a null-terminated C string
1218        let s = unsafe { CStr::from_ptr(data) };
1219        let owned = s.to_bytes().to_vec();
1220        let mut buf = owned;
1221        buf.push(0); // null terminator
1222                     // Convert to boxed slice for exact-length allocation (len == capacity guaranteed)
1223        let boxed = buf.into_boxed_slice();
1224        let data_ptr = Box::into_raw(boxed).cast::<c_char>();
1225
1226        let node = Box::new(curl_slist { data: data_ptr, next: ptr::null_mut() });
1227
1228        let node_ptr = Box::into_raw(node);
1229
1230        if list.is_null() {
1231            node_ptr
1232        } else {
1233            // Walk to end of list
1234            let mut current = list;
1235            // SAFETY: Caller guarantees list is a valid curl_slist chain
1236            while unsafe { !(*current).next.is_null() } {
1237                current = unsafe { (*current).next };
1238            }
1239            // SAFETY: current is a valid node
1240            unsafe {
1241                (*current).next = node_ptr;
1242            }
1243            list
1244        }
1245    }));
1246    result.unwrap_or(ptr::null_mut())
1247}
1248
1249/// `curl_slist_free_all` — free an entire linked list.
1250///
1251/// # Safety
1252///
1253/// `list` must be a valid `curl_slist` pointer from `curl_slist_append`, or null.
1254#[no_mangle]
1255pub unsafe extern "C" fn curl_slist_free_all(list: *mut curl_slist) {
1256    let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1257        let mut current = list;
1258        while !current.is_null() {
1259            // SAFETY: current is a valid node from curl_slist_append
1260            let node = unsafe { Box::from_raw(current) };
1261            let next = node.next;
1262
1263            // Free the string data
1264            if !node.data.is_null() {
1265                // SAFETY: data was allocated via Box::into_raw on a boxed slice
1266                let s = unsafe { CStr::from_ptr(node.data) };
1267                let len = s.to_bytes_with_nul().len();
1268                let raw_slice =
1269                    unsafe { std::slice::from_raw_parts_mut(node.data.cast::<u8>(), len) };
1270                let _ = unsafe { Box::from_raw(raw_slice) };
1271            }
1272
1273            current = next;
1274        }
1275    }));
1276}
1277
1278// ───────────────────────── Easy handle ─────────────────────────
1279
1280/// Internal state for an easy handle.
1281struct EasyHandle {
1282    easy: liburlx::Easy,
1283    last_response: Option<liburlx::Response>,
1284    write_callback: Option<WriteCallback>,
1285    write_data: *mut c_void,
1286    header_callback: Option<HeaderCallback>,
1287    header_data: *mut c_void,
1288    read_callback: Option<ReadCallback>,
1289    read_data: *mut c_void,
1290    debug_callback: Option<DebugCallback>,
1291    debug_data: *mut c_void,
1292    progress_callback: Option<ProgressCallback>,
1293    xferinfo_callback: Option<XferInfoCallback>,
1294    progress_data: *mut c_void,
1295    seek_callback: Option<SeekCallback>,
1296    seek_data: *mut c_void,
1297    noprogress: bool,
1298    postfields: Option<Vec<u8>>,
1299    infilesize: Option<u64>,
1300    private_data: *mut c_void,
1301    /// MIME parts associated with this handle (not yet finalized).
1302    mime_parts: Vec<(*mut c_void, *mut c_void)>,
1303    /// MIME handle for `CURLOPT_MIMEPOST`.
1304    mimepost: *mut c_void,
1305    error_buf: [u8; 256],
1306    interleave_callback: Option<InterleaveCallback>,
1307    interleave_data: *mut c_void,
1308    /// Cached `CString` for RTSP session ID (kept alive for getinfo pointers).
1309    rtsp_session_id_cstr: Option<std::ffi::CString>,
1310    /// When true, include HTTP headers in the body output (`CURLOPT_HEADER`).
1311    include_headers: bool,
1312}
1313
1314// SAFETY: The raw pointers in EasyHandle (write_data, header_data) are
1315// provided by the C caller and are only dereferenced inside callback
1316// invocations during perform, which is single-threaded from the
1317// caller's perspective (matching libcurl's thread-safety model).
1318unsafe impl Send for EasyHandle {}
1319
1320/// `curl_easy_init` — create a new easy handle.
1321///
1322/// # Safety
1323///
1324/// Returns a new handle that must be freed with `curl_easy_cleanup`.
1325#[no_mangle]
1326pub extern "C" fn curl_easy_init() -> *mut c_void {
1327    let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1328        let handle = Box::new(EasyHandle {
1329            easy: liburlx::Easy::new(),
1330            last_response: None,
1331            write_callback: None,
1332            write_data: ptr::null_mut(),
1333            header_callback: None,
1334            header_data: ptr::null_mut(),
1335            read_callback: None,
1336            read_data: ptr::null_mut(),
1337            debug_callback: None,
1338            debug_data: ptr::null_mut(),
1339            progress_callback: None,
1340            xferinfo_callback: None,
1341            progress_data: ptr::null_mut(),
1342            seek_callback: None,
1343            seek_data: ptr::null_mut(),
1344            noprogress: true,
1345            postfields: None,
1346            infilesize: None,
1347            private_data: ptr::null_mut(),
1348            mime_parts: Vec::new(),
1349            mimepost: ptr::null_mut(),
1350            error_buf: [0u8; 256],
1351            interleave_callback: None,
1352            interleave_data: ptr::null_mut(),
1353            rtsp_session_id_cstr: None,
1354            include_headers: false,
1355        });
1356        Box::into_raw(handle).cast::<c_void>()
1357    }));
1358    result.unwrap_or(ptr::null_mut())
1359}
1360
1361/// `curl_easy_cleanup` — free an easy handle.
1362///
1363/// # Safety
1364///
1365/// `handle` must be a valid pointer returned by `curl_easy_init`, or null.
1366/// After this call, `handle` must not be used.
1367#[no_mangle]
1368pub unsafe extern "C" fn curl_easy_cleanup(handle: *mut c_void) {
1369    let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1370        if !handle.is_null() {
1371            // SAFETY: Caller guarantees handle is from curl_easy_init
1372            let _ = unsafe { Box::from_raw(handle.cast::<EasyHandle>()) };
1373        }
1374    }));
1375}
1376
1377/// `curl_easy_duphandle` — clone an easy handle.
1378///
1379/// # Safety
1380///
1381/// `handle` must be a valid pointer from `curl_easy_init`.
1382/// The returned handle must be freed with `curl_easy_cleanup`.
1383#[no_mangle]
1384pub unsafe extern "C" fn curl_easy_duphandle(handle: *mut c_void) -> *mut c_void {
1385    let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1386        if handle.is_null() {
1387            return ptr::null_mut();
1388        }
1389
1390        // SAFETY: Caller guarantees handle is from curl_easy_init
1391        let h = unsafe { &*handle.cast::<EasyHandle>() };
1392        let dup = Box::new(EasyHandle {
1393            easy: h.easy.clone(),
1394            last_response: None,
1395            write_callback: h.write_callback,
1396            write_data: h.write_data,
1397            header_callback: h.header_callback,
1398            header_data: h.header_data,
1399            read_callback: h.read_callback,
1400            read_data: h.read_data,
1401            debug_callback: h.debug_callback,
1402            debug_data: h.debug_data,
1403            progress_callback: h.progress_callback,
1404            xferinfo_callback: h.xferinfo_callback,
1405            progress_data: h.progress_data,
1406            seek_callback: h.seek_callback,
1407            seek_data: h.seek_data,
1408            noprogress: h.noprogress,
1409            postfields: h.postfields.clone(),
1410            infilesize: h.infilesize,
1411            private_data: h.private_data,
1412            mime_parts: Vec::new(),
1413            mimepost: ptr::null_mut(),
1414            error_buf: [0u8; 256],
1415            interleave_callback: h.interleave_callback,
1416            interleave_data: h.interleave_data,
1417            rtsp_session_id_cstr: None,
1418            include_headers: h.include_headers,
1419        });
1420        Box::into_raw(dup).cast::<c_void>()
1421    }));
1422    result.unwrap_or(ptr::null_mut())
1423}
1424
1425/// `curl_easy_reset` — reset an easy handle to initial state.
1426///
1427/// # Safety
1428///
1429/// `handle` must be a valid pointer from `curl_easy_init`.
1430#[no_mangle]
1431pub unsafe extern "C" fn curl_easy_reset(handle: *mut c_void) {
1432    let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1433        if handle.is_null() {
1434            return;
1435        }
1436
1437        // SAFETY: Caller guarantees handle is from curl_easy_init
1438        let h = unsafe { &mut *handle.cast::<EasyHandle>() };
1439        h.easy = liburlx::Easy::new();
1440        h.last_response = None;
1441        h.write_callback = None;
1442        h.write_data = ptr::null_mut();
1443        h.header_callback = None;
1444        h.header_data = ptr::null_mut();
1445        h.read_callback = None;
1446        h.read_data = ptr::null_mut();
1447        h.debug_callback = None;
1448        h.debug_data = ptr::null_mut();
1449        h.progress_callback = None;
1450        h.xferinfo_callback = None;
1451        h.progress_data = ptr::null_mut();
1452        h.seek_callback = None;
1453        h.seek_data = ptr::null_mut();
1454        h.noprogress = true;
1455        h.postfields = None;
1456        h.infilesize = None;
1457        h.private_data = ptr::null_mut();
1458        h.mime_parts.clear();
1459        h.mimepost = ptr::null_mut();
1460        h.error_buf = [0u8; 256];
1461        h.interleave_callback = None;
1462        h.interleave_data = ptr::null_mut();
1463        h.rtsp_session_id_cstr = None;
1464        h.include_headers = false;
1465    }));
1466}
1467
1468/// Helper to read a C string from a `*const c_void`.
1469///
1470/// # Safety
1471///
1472/// `value` must point to a null-terminated C string.
1473unsafe fn read_cstr(value: *const c_void) -> Option<&'static str> {
1474    if value.is_null() {
1475        return None;
1476    }
1477    // SAFETY: Caller guarantees value is a null-terminated C string
1478    let cstr = unsafe { CStr::from_ptr(value.cast::<c_char>()) };
1479    cstr.to_str().ok()
1480}
1481
1482/// `curl_easy_setopt` — set options on an easy handle.
1483///
1484/// # Safety
1485///
1486/// `handle` must be a valid pointer from `curl_easy_init`.
1487/// Variadic arguments must match the expected type for each option.
1488#[no_mangle]
1489#[allow(clippy::too_many_lines)]
1490pub unsafe extern "C" fn curl_easy_setopt(
1491    handle: *mut c_void,
1492    option: c_long,
1493    value: *const c_void,
1494) -> CURLcode {
1495    let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1496        if handle.is_null() {
1497            return CURLcode::CURLE_FAILED_INIT;
1498        }
1499
1500        // SAFETY: Caller guarantees handle is from curl_easy_init
1501        let h = unsafe { &mut *handle.cast::<EasyHandle>() };
1502
1503        match option {
1504            // ─── String options ───
1505
1506            // CURLOPT_URL = 10002
1507            10002 => {
1508                // SAFETY: value must be a null-terminated C string
1509                match unsafe { read_cstr(value) } {
1510                    Some(s) => match h.easy.url(s) {
1511                        Ok(()) => CURLcode::CURLE_OK,
1512                        Err(_) => CURLcode::CURLE_URL_MALFORMAT,
1513                    },
1514                    None => CURLcode::CURLE_URL_MALFORMAT,
1515                }
1516            }
1517
1518            // CURLOPT_WRITEFUNCTION = 20011
1519            20011 => {
1520                // SAFETY: Caller guarantees value is a valid function pointer
1521                h.write_callback =
1522                    Some(unsafe { std::mem::transmute::<*const c_void, WriteCallback>(value) });
1523                CURLcode::CURLE_OK
1524            }
1525
1526            // CURLOPT_WRITEDATA = 10001
1527            10001 => {
1528                h.write_data = value.cast_mut();
1529                CURLcode::CURLE_OK
1530            }
1531
1532            // CURLOPT_HEADERFUNCTION = 20079
1533            20079 => {
1534                // SAFETY: Caller guarantees value is a valid function pointer
1535                h.header_callback =
1536                    Some(unsafe { std::mem::transmute::<*const c_void, HeaderCallback>(value) });
1537                CURLcode::CURLE_OK
1538            }
1539
1540            // CURLOPT_HEADERDATA = 10029
1541            10029 => {
1542                h.header_data = value.cast_mut();
1543                CURLcode::CURLE_OK
1544            }
1545
1546            // CURLOPT_READFUNCTION = 20012
1547            20012 => {
1548                // SAFETY: Caller guarantees value is a valid function pointer
1549                h.read_callback =
1550                    Some(unsafe { std::mem::transmute::<*const c_void, ReadCallback>(value) });
1551                CURLcode::CURLE_OK
1552            }
1553
1554            // CURLOPT_READDATA = 10009
1555            10009 => {
1556                h.read_data = value.cast_mut();
1557                CURLcode::CURLE_OK
1558            }
1559
1560            // CURLOPT_DEBUGFUNCTION = 20094
1561            20094 => {
1562                // SAFETY: Caller guarantees value is a valid function pointer
1563                h.debug_callback =
1564                    Some(unsafe { std::mem::transmute::<*const c_void, DebugCallback>(value) });
1565                CURLcode::CURLE_OK
1566            }
1567
1568            // CURLOPT_DEBUGDATA = 10095
1569            10095 => {
1570                h.debug_data = value.cast_mut();
1571                CURLcode::CURLE_OK
1572            }
1573
1574            // CURLOPT_USERAGENT = 10018
1575            10018 => {
1576                // SAFETY: value must be a null-terminated C string
1577                if let Some(s) = unsafe { read_cstr(value) } {
1578                    h.easy.header("User-Agent", s);
1579                }
1580                CURLcode::CURLE_OK
1581            }
1582
1583            // CURLOPT_POSTFIELDS = 10015
1584            10015 => {
1585                if value.is_null() {
1586                    h.postfields = None;
1587                } else {
1588                    // SAFETY: Caller guarantees value is a null-terminated C string
1589                    let data = unsafe { CStr::from_ptr(value.cast::<c_char>()) };
1590                    h.postfields = Some(data.to_bytes().to_vec());
1591                }
1592                CURLcode::CURLE_OK
1593            }
1594
1595            // CURLOPT_PROXY = 10004
1596            10004 => {
1597                // SAFETY: value must be a null-terminated C string
1598                match unsafe { read_cstr(value) } {
1599                    Some(s) => match h.easy.proxy(s) {
1600                        Ok(()) => CURLcode::CURLE_OK,
1601                        Err(_) => CURLcode::CURLE_URL_MALFORMAT,
1602                    },
1603                    None => CURLcode::CURLE_OK,
1604                }
1605            }
1606
1607            // CURLOPT_NOPROXY = 10177
1608            10177 => {
1609                // SAFETY: value must be a null-terminated C string
1610                if let Some(s) = unsafe { read_cstr(value) } {
1611                    h.easy.noproxy(s);
1612                }
1613                CURLcode::CURLE_OK
1614            }
1615
1616            // CURLOPT_CUSTOMREQUEST = 10036
1617            10036 => {
1618                // SAFETY: value must be a null-terminated C string
1619                if let Some(s) = unsafe { read_cstr(value) } {
1620                    h.easy.method(s);
1621                }
1622                CURLcode::CURLE_OK
1623            }
1624
1625            // CURLOPT_USERPWD = 10005
1626            10005 => {
1627                // SAFETY: value must be a null-terminated C string
1628                if let Some(s) = unsafe { read_cstr(value) } {
1629                    if let Some((user, pass)) = s.split_once(':') {
1630                        h.easy.basic_auth(user, pass);
1631                    } else {
1632                        h.easy.basic_auth(s, "");
1633                    }
1634                }
1635                CURLcode::CURLE_OK
1636            }
1637
1638            // CURLOPT_RANGE = 10007
1639            10007 => {
1640                // SAFETY: value must be a null-terminated C string
1641                if let Some(s) = unsafe { read_cstr(value) } {
1642                    h.easy.range(s);
1643                }
1644                CURLcode::CURLE_OK
1645            }
1646
1647            // CURLOPT_COOKIE = 10022
1648            10022 => {
1649                // SAFETY: value must be a null-terminated C string
1650                if let Some(s) = unsafe { read_cstr(value) } {
1651                    h.easy.header("Cookie", s);
1652                }
1653                CURLcode::CURLE_OK
1654            }
1655
1656            // CURLOPT_SSLCERT = 10025
1657            10025 => {
1658                // SAFETY: value must be a null-terminated C string
1659                if let Some(s) = unsafe { read_cstr(value) } {
1660                    h.easy.ssl_client_cert(std::path::Path::new(s));
1661                }
1662                CURLcode::CURLE_OK
1663            }
1664
1665            // CURLOPT_SSLKEY = 10087
1666            10087 => {
1667                // SAFETY: value must be a null-terminated C string
1668                if let Some(s) = unsafe { read_cstr(value) } {
1669                    h.easy.ssl_client_key(std::path::Path::new(s));
1670                }
1671                CURLcode::CURLE_OK
1672            }
1673
1674            // CURLOPT_CAINFO = 10065
1675            10065 => {
1676                // SAFETY: value must be a null-terminated C string
1677                if let Some(s) = unsafe { read_cstr(value) } {
1678                    h.easy.ssl_ca_cert(std::path::Path::new(s));
1679                }
1680                CURLcode::CURLE_OK
1681            }
1682
1683            // CURLOPT_CAPATH = 10097
1684            10097 => {
1685                // SAFETY: value must be a null-terminated C string
1686                // CA path directory; accepted for compat (we use CA bundle, not path)
1687                CURLcode::CURLE_OK
1688            }
1689
1690            // CURLOPT_ACCEPT_ENCODING = 10102
1691            10102 => {
1692                h.easy.accept_encoding(!value.is_null());
1693                CURLcode::CURLE_OK
1694            }
1695
1696            // CURLOPT_UNIX_SOCKET_PATH = 10231
1697            10231 => {
1698                // SAFETY: value must be a null-terminated C string
1699                if let Some(s) = unsafe { read_cstr(value) } {
1700                    h.easy.unix_socket(s);
1701                }
1702                CURLcode::CURLE_OK
1703            }
1704
1705            // CURLOPT_PINNEDPUBLICKEY = 10230
1706            10230 => {
1707                // SAFETY: value must be a null-terminated C string
1708                if let Some(s) = unsafe { read_cstr(value) } {
1709                    h.easy.ssl_pinned_public_key(s);
1710                }
1711                CURLcode::CURLE_OK
1712            }
1713
1714            // CURLOPT_INTERFACE = 10062
1715            10062 => {
1716                // SAFETY: value must be a null-terminated C string
1717                if let Some(s) = unsafe { read_cstr(value) } {
1718                    h.easy.interface(s);
1719                }
1720                CURLcode::CURLE_OK
1721            }
1722
1723            // CURLOPT_SSL_CIPHER_LIST = 10083
1724            10083 => {
1725                // SAFETY: value must be a null-terminated C string
1726                if let Some(s) = unsafe { read_cstr(value) } {
1727                    h.easy.ssl_cipher_list(s);
1728                }
1729                CURLcode::CURLE_OK
1730            }
1731
1732            // CURLOPT_COOKIEFILE = 10031
1733            10031 => {
1734                // SAFETY: value must be a null-terminated C string
1735                if let Some(s) = unsafe { read_cstr(value) } {
1736                    if h.easy.cookie_file(s).is_err() {
1737                        // Cookie engine enabled even if file doesn't exist
1738                        h.easy.cookie_jar(true);
1739                    }
1740                } else {
1741                    // NULL enables the cookie engine with empty jar
1742                    h.easy.cookie_jar(true);
1743                }
1744                CURLcode::CURLE_OK
1745            }
1746
1747            // CURLOPT_COOKIEJAR = 10082
1748            10082 => {
1749                // SAFETY: value must be a null-terminated C string
1750                if let Some(s) = unsafe { read_cstr(value) } {
1751                    h.easy.cookie_jar_file(s);
1752                }
1753                CURLcode::CURLE_OK
1754            }
1755
1756            // CURLOPT_PROXYUSERPWD = 10006
1757            10006 => {
1758                // SAFETY: value must be a null-terminated C string in "user:password" format
1759                if let Some(s) = unsafe { read_cstr(value) } {
1760                    if let Some((user, pass)) = s.split_once(':') {
1761                        h.easy.proxy_auth(user, pass);
1762                    } else {
1763                        h.easy.proxy_auth(s, "");
1764                    }
1765                }
1766                CURLcode::CURLE_OK
1767            }
1768
1769            // CURLOPT_PROXY_SSLCERT = 10254
1770            10254 => {
1771                // SAFETY: value must be a null-terminated C string
1772                if let Some(s) = unsafe { read_cstr(value) } {
1773                    h.easy.proxy_ssl_client_cert(std::path::Path::new(s));
1774                }
1775                CURLcode::CURLE_OK
1776            }
1777
1778            // CURLOPT_PROXY_SSLKEY = 10255
1779            10255 => {
1780                // SAFETY: value must be a null-terminated C string
1781                if let Some(s) = unsafe { read_cstr(value) } {
1782                    h.easy.proxy_ssl_client_key(std::path::Path::new(s));
1783                }
1784                CURLcode::CURLE_OK
1785            }
1786
1787            // CURLOPT_RESOLVE = 10203
1788            10203 => {
1789                // This expects a curl_slist of "host:port:address" entries
1790                if !value.is_null() {
1791                    let mut current = value.cast::<curl_slist>().cast_mut();
1792                    while !current.is_null() {
1793                        // SAFETY: current is a valid curl_slist node
1794                        let node = unsafe { &*current };
1795                        if !node.data.is_null() {
1796                            // SAFETY: node.data is a null-terminated string
1797                            if let Ok(s) = unsafe { CStr::from_ptr(node.data) }.to_str() {
1798                                // Parse "host:port:address"
1799                                let parts: Vec<&str> = s.splitn(3, ':').collect();
1800                                if parts.len() == 3 {
1801                                    let host_port = format!("{}:{}", parts[0], parts[1]);
1802                                    h.easy.resolve(&host_port, parts[2]);
1803                                }
1804                            }
1805                        }
1806                        current = node.next;
1807                    }
1808                }
1809                CURLcode::CURLE_OK
1810            }
1811
1812            // CURLOPT_HTTPHEADER = 10023
1813            10023 => {
1814                // This expects a curl_slist of "Name: Value" headers
1815                if !value.is_null() {
1816                    let mut current = value.cast::<curl_slist>().cast_mut();
1817                    while !current.is_null() {
1818                        // SAFETY: current is a valid curl_slist node
1819                        let node = unsafe { &*current };
1820                        if !node.data.is_null() {
1821                            // SAFETY: node.data is a null-terminated string
1822                            if let Ok(s) = unsafe { CStr::from_ptr(node.data) }.to_str() {
1823                                if let Some((name, val)) = s.split_once(':') {
1824                                    h.easy.header(name.trim(), val.trim());
1825                                }
1826                            }
1827                        }
1828                        current = node.next;
1829                    }
1830                }
1831                CURLcode::CURLE_OK
1832            }
1833
1834            // ─── Long options ───
1835
1836            // CURLOPT_HEADER = 42 (include headers in body output)
1837            42 => {
1838                h.include_headers = value as c_long != 0;
1839                CURLcode::CURLE_OK
1840            }
1841
1842            // CURLOPT_POST = 47
1843            47 => {
1844                if value as c_long != 0 {
1845                    h.easy.method("POST");
1846                }
1847                CURLcode::CURLE_OK
1848            }
1849
1850            // CURLOPT_NOBODY = 44 (HEAD request)
1851            44 => {
1852                if value as c_long != 0 {
1853                    h.easy.method("HEAD");
1854                }
1855                CURLcode::CURLE_OK
1856            }
1857
1858            // CURLOPT_FAILONERROR = 45
1859            45 => {
1860                h.easy.fail_on_error(value as c_long != 0);
1861                CURLcode::CURLE_OK
1862            }
1863
1864            // CURLOPT_UPLOAD = 46, CURLOPT_PUT = 54
1865            46 | 54 => {
1866                if value as c_long != 0 {
1867                    h.easy.method("PUT");
1868                }
1869                CURLcode::CURLE_OK
1870            }
1871
1872            // CURLOPT_FOLLOWLOCATION = 52
1873            52 => {
1874                h.easy.follow_redirects(value as c_long != 0);
1875                CURLcode::CURLE_OK
1876            }
1877
1878            // CURLOPT_POSTFIELDSIZE = 60
1879            60 => {
1880                // Store size but actual data comes via POSTFIELDS
1881                CURLcode::CURLE_OK
1882            }
1883
1884            // CURLOPT_SSL_VERIFYPEER = 64
1885            64 => {
1886                h.easy.ssl_verify_peer(value as c_long != 0);
1887                CURLcode::CURLE_OK
1888            }
1889
1890            // CURLOPT_MAXREDIRS = 68
1891            68 => {
1892                #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
1893                h.easy.max_redirects(value as u32);
1894                CURLcode::CURLE_OK
1895            }
1896
1897            // CURLOPT_CONNECTTIMEOUT = 78
1898            78 => {
1899                #[allow(clippy::cast_sign_loss)]
1900                let secs = value as u64;
1901                if secs > 0 {
1902                    h.easy.connect_timeout(std::time::Duration::from_secs(secs));
1903                }
1904                CURLcode::CURLE_OK
1905            }
1906
1907            // CURLOPT_HTTPGET = 80
1908            80 => {
1909                if value as c_long != 0 {
1910                    h.easy.method("GET");
1911                }
1912                CURLcode::CURLE_OK
1913            }
1914
1915            // CURLOPT_SSL_VERIFYHOST = 81
1916            81 => {
1917                // libcurl: 0 = don't verify, 2 = verify (1 is deprecated = 2)
1918                h.easy.ssl_verify_host(value as c_long >= 2);
1919                CURLcode::CURLE_OK
1920            }
1921
1922            // CURLOPT_HTTPAUTH = 107
1923            107 => {
1924                // libcurl auth bitmask: 1=Basic, 2=Digest, 4=Negotiate, 8=NTLM
1925                // We accept the value but currently only Basic/Digest work
1926                CURLcode::CURLE_OK
1927            }
1928
1929            // CURLOPT_PROXYAUTH = 111
1930            111 => {
1931                // libcurl proxy auth bitmask: 1=Basic, 2=Digest, 8=NTLM
1932                // Accept the value; actual method selection happens with proxy_auth calls
1933                CURLcode::CURLE_OK
1934            }
1935
1936            // CURLOPT_TCP_NODELAY = 121
1937            121 => {
1938                h.easy.tcp_nodelay(value as c_long != 0);
1939                CURLcode::CURLE_OK
1940            }
1941
1942            // CURLOPT_LOCALPORT = 139
1943            139 => {
1944                #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
1945                h.easy.local_port(value as u16);
1946                CURLcode::CURLE_OK
1947            }
1948
1949            // CURLOPT_TCP_KEEPALIVE = 213
1950            213 => {
1951                if value as c_long != 0 {
1952                    h.easy.tcp_keepalive(std::time::Duration::from_secs(60));
1953                }
1954                CURLcode::CURLE_OK
1955            }
1956
1957            // CURLOPT_TIMEOUT = 13
1958            13 => {
1959                #[allow(clippy::cast_sign_loss)]
1960                let secs = value as u64;
1961                if secs > 0 {
1962                    h.easy.timeout(std::time::Duration::from_secs(secs));
1963                }
1964                CURLcode::CURLE_OK
1965            }
1966
1967            // CURLOPT_VERBOSE = 41
1968            41 => {
1969                h.easy.verbose(value as c_long != 0);
1970                CURLcode::CURLE_OK
1971            }
1972
1973            // CURLOPT_SSLVERSION = 32
1974            32 => {
1975                // libcurl: 0=default, 6=TLSv1.2, 7=TLSv1.3
1976                let version = value as c_long;
1977                if version == 6 {
1978                    h.easy.ssl_min_version(liburlx::TlsVersion::Tls12);
1979                } else if version == 7 {
1980                    h.easy.ssl_min_version(liburlx::TlsVersion::Tls13);
1981                }
1982                CURLcode::CURLE_OK
1983            }
1984
1985            // CURLOPT_LOW_SPEED_LIMIT = 19
1986            19 => {
1987                #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
1988                h.easy.low_speed_limit(value as u32);
1989                CURLcode::CURLE_OK
1990            }
1991
1992            // CURLOPT_LOW_SPEED_TIME = 20
1993            20 => {
1994                #[allow(clippy::cast_sign_loss)]
1995                let secs = value as u64;
1996                h.easy.low_speed_time(std::time::Duration::from_secs(secs));
1997                CURLcode::CURLE_OK
1998            }
1999
2000            // CURLOPT_FRESH_CONNECT = 74
2001            74 => {
2002                h.easy.fresh_connect(value as c_long != 0);
2003                CURLcode::CURLE_OK
2004            }
2005
2006            // CURLOPT_FORBID_REUSE = 75
2007            75 => {
2008                h.easy.forbid_reuse(value as c_long != 0);
2009                CURLcode::CURLE_OK
2010            }
2011
2012            // CURLOPT_TIMEOUT_MS = 155
2013            155 => {
2014                #[allow(clippy::cast_sign_loss)]
2015                let ms = value as u64;
2016                if ms > 0 {
2017                    h.easy.timeout(std::time::Duration::from_millis(ms));
2018                }
2019                CURLcode::CURLE_OK
2020            }
2021
2022            // CURLOPT_CONNECTTIMEOUT_MS = 156
2023            156 => {
2024                #[allow(clippy::cast_sign_loss)]
2025                let ms = value as u64;
2026                if ms > 0 {
2027                    h.easy.connect_timeout(std::time::Duration::from_millis(ms));
2028                }
2029                CURLcode::CURLE_OK
2030            }
2031
2032            // CURLOPT_SSL_SESSIONID_CACHE = 150
2033            150 => {
2034                h.easy.ssl_session_cache(value as c_long != 0);
2035                CURLcode::CURLE_OK
2036            }
2037
2038            // CURLOPT_PROXY_SSL_VERIFYPEER = 248
2039            248 => {
2040                h.easy.proxy_ssl_verify_peer(value as c_long != 0);
2041                CURLcode::CURLE_OK
2042            }
2043
2044            // CURLOPT_INFILESIZE_LARGE = 30115
2045            30115 => {
2046                #[allow(clippy::cast_sign_loss)]
2047                let size = value as u64;
2048                h.infilesize = Some(size);
2049                h.easy.infilesize(size);
2050                CURLcode::CURLE_OK
2051            }
2052
2053            // CURLOPT_DNS_CACHE_TIMEOUT = 92
2054            92 => {
2055                #[allow(clippy::cast_sign_loss)]
2056                let secs = value as u64;
2057                h.easy.dns_cache_timeout(std::time::Duration::from_secs(secs));
2058                CURLcode::CURLE_OK
2059            }
2060
2061            // CURLOPT_HAPPY_EYEBALLS_TIMEOUT_MS = 271
2062            271 => {
2063                #[allow(clippy::cast_sign_loss)]
2064                let ms = value as u64;
2065                h.easy.happy_eyeballs_timeout(std::time::Duration::from_millis(ms));
2066                CURLcode::CURLE_OK
2067            }
2068
2069            // CURLOPT_DNS_SERVERS = 10211
2070            10211 => {
2071                // SAFETY: value must be a null-terminated C string
2072                if let Some(s) = unsafe { read_cstr(value) } {
2073                    match h.easy.dns_servers(s) {
2074                        Ok(()) => CURLcode::CURLE_OK,
2075                        Err(_) => CURLcode::CURLE_BAD_FUNCTION_ARGUMENT,
2076                    }
2077                } else {
2078                    CURLcode::CURLE_OK
2079                }
2080            }
2081
2082            // CURLOPT_DOH_URL = 10279
2083            10279 => {
2084                // SAFETY: value must be a null-terminated C string
2085                if let Some(s) = unsafe { read_cstr(value) } {
2086                    h.easy.doh_url(s);
2087                }
2088                CURLcode::CURLE_OK
2089            }
2090
2091            // CURLOPT_UNRESTRICTED_AUTH = 105
2092            105 => {
2093                h.easy.unrestricted_auth(value as c_long != 0);
2094                CURLcode::CURLE_OK
2095            }
2096
2097            // CURLOPT_IGNORE_CONTENT_LENGTH = 136
2098            136 => {
2099                h.easy.ignore_content_length(value as c_long != 0);
2100                CURLcode::CURLE_OK
2101            }
2102
2103            // CURLOPT_MAX_SEND_SPEED_LARGE = 30145
2104            30145 => {
2105                #[allow(clippy::cast_sign_loss)]
2106                let speed = value as u64;
2107                if speed > 0 {
2108                    h.easy.max_send_speed(speed);
2109                }
2110                CURLcode::CURLE_OK
2111            }
2112
2113            // CURLOPT_MAX_RECV_SPEED_LARGE = 30146
2114            30146 => {
2115                #[allow(clippy::cast_sign_loss)]
2116                let speed = value as u64;
2117                if speed > 0 {
2118                    h.easy.max_recv_speed(speed);
2119                }
2120                CURLcode::CURLE_OK
2121            }
2122
2123            // CURLOPT_NOPROGRESS = 43
2124            43 => {
2125                h.noprogress = value as c_long != 0;
2126                CURLcode::CURLE_OK
2127            }
2128
2129            // CURLOPT_PROGRESSFUNCTION = 20056
2130            20056 => {
2131                // SAFETY: Caller guarantees value is a valid function pointer
2132                h.progress_callback =
2133                    Some(unsafe { std::mem::transmute::<*const c_void, ProgressCallback>(value) });
2134                CURLcode::CURLE_OK
2135            }
2136
2137            // CURLOPT_XFERINFOFUNCTION = 20219
2138            20219 => {
2139                // SAFETY: Caller guarantees value is a valid function pointer
2140                h.xferinfo_callback =
2141                    Some(unsafe { std::mem::transmute::<*const c_void, XferInfoCallback>(value) });
2142                CURLcode::CURLE_OK
2143            }
2144
2145            // CURLOPT_PROGRESSDATA = 10057 (also used as XFERINFODATA)
2146            10057 => {
2147                h.progress_data = value.cast_mut();
2148                CURLcode::CURLE_OK
2149            }
2150
2151            // CURLOPT_SEEKFUNCTION = 20167
2152            20167 => {
2153                // SAFETY: Caller guarantees value is a valid function pointer
2154                h.seek_callback =
2155                    Some(unsafe { std::mem::transmute::<*const c_void, SeekCallback>(value) });
2156                CURLcode::CURLE_OK
2157            }
2158
2159            // CURLOPT_SEEKDATA = 10168
2160            10168 => {
2161                h.seek_data = value.cast_mut();
2162                CURLcode::CURLE_OK
2163            }
2164
2165            // CURLOPT_PRIVATE = 10103
2166            10103 => {
2167                h.private_data = value.cast_mut();
2168                CURLcode::CURLE_OK
2169            }
2170
2171            // CURLOPT_SHARE = 10100
2172            10100 => {
2173                if value.is_null() {
2174                    // Detach share — accepted as no-op (share state persists)
2175                } else {
2176                    // SAFETY: Caller guarantees value is from curl_share_init
2177                    let share = unsafe { &*value.cast::<liburlx::Share>() };
2178                    h.easy.set_share(share.clone());
2179                }
2180                CURLcode::CURLE_OK
2181            }
2182
2183            // CURLOPT_MIMEPOST = 10269
2184            10269 => {
2185                h.mimepost = value.cast_mut();
2186                // Finalize any pending parts
2187                for &(mime, part) in &h.mime_parts {
2188                    // SAFETY: mime_parts contains valid handles
2189                    unsafe { finalize_mime_part(mime, part) };
2190                }
2191                h.mime_parts.clear();
2192                CURLcode::CURLE_OK
2193            }
2194
2195            // CURLOPT_REFERER = 10016
2196            10016 => {
2197                // SAFETY: value must be a null-terminated C string
2198                if let Some(s) = unsafe { read_cstr(value) } {
2199                    h.easy.header("Referer", s);
2200                }
2201                CURLcode::CURLE_OK
2202            }
2203
2204            // CURLOPT_XOAUTH2_BEARER = 10220
2205            10220 => {
2206                // SAFETY: value must be a null-terminated C string
2207                if let Some(s) = unsafe { read_cstr(value) } {
2208                    h.easy.bearer_token(s);
2209                }
2210                CURLcode::CURLE_OK
2211            }
2212
2213            // CURLOPT_TLSAUTH_TYPE = 10216
2214            10216 => {
2215                // SAFETY: value must be a null-terminated C string
2216                // Only "SRP" is supported; other values are silently accepted.
2217                CURLcode::CURLE_OK
2218            }
2219
2220            // CURLOPT_TLSAUTH_USERNAME = 10217
2221            10217 => {
2222                // SAFETY: value must be a null-terminated C string
2223                if let Some(s) = unsafe { read_cstr(value) } {
2224                    h.easy.ssl_srp_user(s);
2225                }
2226                CURLcode::CURLE_OK
2227            }
2228
2229            // CURLOPT_TLSAUTH_PASSWORD = 10218
2230            10218 => {
2231                // SAFETY: value must be a null-terminated C string
2232                if let Some(s) = unsafe { read_cstr(value) } {
2233                    h.easy.ssl_srp_password(s);
2234                }
2235                CURLcode::CURLE_OK
2236            }
2237
2238            // CURLOPT_AWS_SIGV4 = 10306
2239            10306 => {
2240                // SAFETY: value must be a null-terminated C string
2241                // Format: "provider1[:provider2[:region[:service]]]"
2242                // We accept the value but AWS SigV4 auth is wired through aws_credentials
2243                CURLcode::CURLE_OK
2244            }
2245
2246            // CURLOPT_AUTOREFERER = 58
2247            58 => {
2248                // Accept but no-op — auto-referer on redirect not yet implemented
2249                CURLcode::CURLE_OK
2250            }
2251
2252            // CURLOPT_HTTP_VERSION = 84
2253            84 => {
2254                let version = value as c_long;
2255                match version {
2256                    // CURL_HTTP_VERSION_NONE = 0
2257                    0 => h.easy.http_version(liburlx::HttpVersion::None),
2258                    // CURL_HTTP_VERSION_1_0 = 1
2259                    1 => h.easy.http_version(liburlx::HttpVersion::Http10),
2260                    // CURL_HTTP_VERSION_1_1 = 2
2261                    2 => h.easy.http_version(liburlx::HttpVersion::Http11),
2262                    // CURL_HTTP_VERSION_2_0 = 3, CURL_HTTP_VERSION_2TLS = 4
2263                    3 | 4 => h.easy.http_version(liburlx::HttpVersion::Http2),
2264                    // CURL_HTTP_VERSION_2_PRIOR_KNOWLEDGE = 5
2265                    5 => h.easy.http_version(liburlx::HttpVersion::Http2PriorKnowledge),
2266                    _ => {}
2267                }
2268                CURLcode::CURLE_OK
2269            }
2270
2271            // CURLOPT_NOSIGNAL = 99
2272            99 => {
2273                // Accept but no-op — signals are not used in tokio-based architecture
2274                CURLcode::CURLE_OK
2275            }
2276
2277            // CURLOPT_MAXCONNECTS = 71
2278            71 => {
2279                let max = value as usize;
2280                h.easy.max_pool_connections(max);
2281                CURLcode::CURLE_OK
2282            }
2283
2284            // CURLOPT_PIPEWAIT = 237
2285            237 => {
2286                // Accepted for compat; HTTP/2 multiplexing handled automatically
2287                CURLcode::CURLE_OK
2288            }
2289
2290            // CURLOPT_STREAM_WEIGHT = 239
2291            239 => {
2292                // Accepted for compat; stream priority deprecated in RFC 9113
2293                CURLcode::CURLE_OK
2294            }
2295
2296            // CURLOPT_TCP_FASTOPEN = 244
2297            244 => {
2298                // Accepted for compat; TCP Fast Open not yet supported
2299                CURLcode::CURLE_OK
2300            }
2301
2302            // CURLOPT_HTTP09_ALLOWED = 285
2303            285 => {
2304                h.easy.http09_allowed(!value.is_null());
2305                CURLcode::CURLE_OK
2306            }
2307
2308            // CURLOPT_PORT = 3
2309            3 => {
2310                // Set port number to connect to (override URL port)
2311                // Accepted for compat; port parsed from URL
2312                CURLcode::CURLE_OK
2313            }
2314
2315            // CURLOPT_INFILESIZE = 14
2316            14 => {
2317                // Set expected upload size (long version)
2318                // Accepted for compat; upload size auto-detected from data
2319                CURLcode::CURLE_OK
2320            }
2321
2322            // CURLOPT_RESUME_FROM = 21
2323            21 => {
2324                // Set resume offset (long version)
2325                #[allow(clippy::cast_sign_loss)]
2326                let offset = value as u64;
2327                h.easy.resume_from(offset);
2328                CURLcode::CURLE_OK
2329            }
2330
2331            // CURLOPT_IPRESOLVE = 113
2332            113 => {
2333                // 0=whatever, 1=IPv4, 2=IPv6
2334                // Accepted for compat; resolver handles dual-stack via Happy Eyeballs
2335                CURLcode::CURLE_OK
2336            }
2337
2338            // CURLOPT_POSTFIELDSIZE_LARGE = 30120
2339            30120 => {
2340                // Set the size of POST data (off_t version)
2341                // Accepted for compat; POST size auto-detected from data
2342                CURLcode::CURLE_OK
2343            }
2344
2345            // CURLOPT_LOCALPORTRANGE = 164
2346            164 => {
2347                // Accept the range but only use the base port set via LOCALPORT
2348                CURLcode::CURLE_OK
2349            }
2350
2351            // CURLOPT_RESUME_FROM_LARGE = 30116
2352            30116 => {
2353                #[allow(clippy::cast_sign_loss)]
2354                let offset = value as u64;
2355                h.easy.resume_from(offset);
2356                CURLcode::CURLE_OK
2357            }
2358
2359            // CURLOPT_MAXFILESIZE_LARGE = 30117
2360            30117 => {
2361                // Accept the value (same as MAXFILESIZE but for large files)
2362                CURLcode::CURLE_OK
2363            }
2364
2365            // CURLOPT_ERRORBUFFER = 10010
2366            10010 => {
2367                // Accept but we store errors in our own buffer
2368                // The C caller's buffer would need to be written to on error
2369                CURLcode::CURLE_OK
2370            }
2371
2372            // CURLOPT_STDERR = 10037
2373            10037 => {
2374                // Accept but no-op — we don't redirect stderr in Rust
2375                CURLcode::CURLE_OK
2376            }
2377
2378            // CURLOPT_HTTPPROXYTUNNEL = 61
2379            61 => {
2380                // HTTP CONNECT tunnel is automatically used for HTTPS through proxies
2381                CURLcode::CURLE_OK
2382            }
2383
2384            // CURLOPT_MAXFILESIZE = 114
2385            114 => {
2386                // Accept max file size limit
2387                CURLcode::CURLE_OK
2388            }
2389
2390            // CURLOPT_COOKIELIST = 10135
2391            10135 => {
2392                // Cookie engine control commands (ALL, SESS, FLUSH, RELOAD, or cookie string)
2393                // All values accepted — actual cookie manipulation handled internally
2394                // SAFETY: value is a caller-provided C string
2395                let _ = unsafe { read_cstr(value) };
2396                CURLcode::CURLE_OK
2397            }
2398
2399            // CURLOPT_POSTREDIR = 161
2400            161 => {
2401                // Bitmask: 1=CURL_REDIR_POST_301, 2=CURL_REDIR_POST_302, 4=CURL_REDIR_POST_303
2402                let mask = value as c_long;
2403                h.easy.post301(mask & 1 != 0);
2404                h.easy.post302(mask & 2 != 0);
2405                h.easy.post303(mask & 4 != 0);
2406                CURLcode::CURLE_OK
2407            }
2408
2409            // CURLOPT_TRANSFER_ENCODING = 207
2410            207 => {
2411                // Request Transfer-Encoding (chunked) — alias for accept_encoding in our impl
2412                h.easy.accept_encoding(value as c_long != 0);
2413                CURLcode::CURLE_OK
2414            }
2415
2416            // CURLOPT_EXPECT_100_TIMEOUT_MS = 227
2417            227 => {
2418                #[allow(clippy::cast_sign_loss)]
2419                let ms = value as u64;
2420                if ms > 0 {
2421                    h.easy.expect_100_timeout(std::time::Duration::from_millis(ms));
2422                }
2423                CURLcode::CURLE_OK
2424            }
2425
2426            // CURLOPT_PATH_AS_IS = 234
2427            234 => {
2428                h.easy.path_as_is(value as c_long != 0);
2429                CURLcode::CURLE_OK
2430            }
2431
2432            // CURLOPT_PROXY_CAINFO = 10246
2433            10246 => {
2434                // SAFETY: value must be a null-terminated C string
2435                if let Some(s) = unsafe { read_cstr(value) } {
2436                    // Proxy CA cert — accept, though proxy TLS config is set separately
2437                    let _ = s;
2438                }
2439                CURLcode::CURLE_OK
2440            }
2441
2442            // CURLOPT_PROXY_SSL_VERIFYHOST = 249
2443            249 => {
2444                // Accept proxy host verification setting
2445                CURLcode::CURLE_OK
2446            }
2447
2448            // CURLOPT_DNS_SHUFFLE_ADDRESSES = 275
2449            275 => {
2450                h.easy.dns_shuffle(value as c_long != 0);
2451                CURLcode::CURLE_OK
2452            }
2453
2454            // CURLOPT_HSTS = 10300
2455            10300 => {
2456                // SAFETY: value must be a null-terminated C string
2457                if let Some(_path) = unsafe { read_cstr(value) } {
2458                    // Accept HSTS file path — HSTS cache is enabled but file I/O not wired
2459                    h.easy.hsts(true);
2460                }
2461                CURLcode::CURLE_OK
2462            }
2463
2464            // CURLOPT_PROTOCOLS_STR = 10318
2465            10318 => {
2466                // SAFETY: value must be a valid C string pointer
2467                if let Some(s) = unsafe { read_cstr(value) } {
2468                    h.easy.set_protocols_str(s);
2469                }
2470                CURLcode::CURLE_OK
2471            }
2472
2473            // CURLOPT_REDIR_PROTOCOLS_STR = 10319
2474            10319 => {
2475                // SAFETY: value must be a valid C string pointer
2476                if let Some(s) = unsafe { read_cstr(value) } {
2477                    h.easy.set_redir_protocols_str(s);
2478                }
2479                CURLcode::CURLE_OK
2480            }
2481
2482            // CURLOPT_CONNECT_TO = 10243
2483            10243 => {
2484                // SAFETY: value must be a valid curl_slist pointer
2485                if !value.is_null() {
2486                    // Parse the slist: each entry is "HOST:PORT:CONNECT-TO-HOST:CONNECT-TO-PORT"
2487                    let mut entries = Vec::new();
2488                    let mut node = value.cast::<curl_slist>();
2489                    // SAFETY: Caller guarantees value is a valid slist chain
2490                    while !node.is_null() {
2491                        let n = unsafe { &*node };
2492                        if !n.data.is_null() {
2493                            // SAFETY: data is a null-terminated C string
2494                            if let Ok(s) = unsafe { CStr::from_ptr(n.data) }.to_str() {
2495                                entries.push(s.to_string());
2496                            }
2497                        }
2498                        node = n.next;
2499                    }
2500                    for entry in &entries {
2501                        h.easy.connect_to(entry);
2502                    }
2503                }
2504                CURLcode::CURLE_OK
2505            }
2506
2507            // CURLOPT_HAPROXYPROTOCOL = 274
2508            274 => {
2509                h.easy.haproxy_protocol(value as c_long != 0);
2510                CURLcode::CURLE_OK
2511            }
2512
2513            // CURLOPT_HTTPPOST = 10024 (deprecated, return disabled)
2514            10024 => {
2515                // The deprecated HTTPPOST API is not supported; use CURLOPT_MIMEPOST
2516                CURLcode::CURLE_OK
2517            }
2518
2519            // CURLOPT_ABSTRACT_UNIX_SOCKET = 10264
2520            10264 => {
2521                // SAFETY: value must be a null-terminated C string
2522                if let Some(s) = unsafe { read_cstr(value) } {
2523                    h.easy.abstract_unix_socket(s);
2524                }
2525                CURLcode::CURLE_OK
2526            }
2527
2528            // CURLOPT_DOH_SSL_VERIFYPEER = 306
2529            306 => {
2530                let verify = value as c_long != 0;
2531                h.easy.doh_insecure(!verify);
2532                CURLcode::CURLE_OK
2533            }
2534
2535            // CURLOPT_DOH_SSL_VERIFYHOST = 307
2536            307 => {
2537                // Accept DoH host verification (handled along with peer verify)
2538                CURLcode::CURLE_OK
2539            }
2540
2541            // CURLOPT_SSLCERT_BLOB = 40291
2542            40291 => {
2543                if value.is_null() {
2544                    return CURLcode::CURLE_BAD_FUNCTION_ARGUMENT;
2545                }
2546                // SAFETY: Caller guarantees value points to a valid curl_blob
2547                let blob = unsafe { &*value.cast::<curl_blob>() };
2548                if blob.data.is_null() || blob.len == 0 {
2549                    return CURLcode::CURLE_BAD_FUNCTION_ARGUMENT;
2550                }
2551                // SAFETY: Caller guarantees blob.data points to blob.len bytes
2552                let data = unsafe { std::slice::from_raw_parts(blob.data.cast::<u8>(), blob.len) };
2553                h.easy.ssl_client_cert_blob(data.to_vec());
2554                CURLcode::CURLE_OK
2555            }
2556
2557            // CURLOPT_SSLKEY_BLOB = 40292
2558            40292 => {
2559                if value.is_null() {
2560                    return CURLcode::CURLE_BAD_FUNCTION_ARGUMENT;
2561                }
2562                // SAFETY: Caller guarantees value points to a valid curl_blob
2563                let blob = unsafe { &*value.cast::<curl_blob>() };
2564                if blob.data.is_null() || blob.len == 0 {
2565                    return CURLcode::CURLE_BAD_FUNCTION_ARGUMENT;
2566                }
2567                // SAFETY: Caller guarantees blob.data points to blob.len bytes
2568                let data = unsafe { std::slice::from_raw_parts(blob.data.cast::<u8>(), blob.len) };
2569                h.easy.ssl_client_key_blob(data.to_vec());
2570                CURLcode::CURLE_OK
2571            }
2572
2573            // CURLOPT_CAINFO_BLOB = 40309
2574            40309 => {
2575                if value.is_null() {
2576                    // NULL pointer clears the blob setting (matches curl behavior)
2577                    h.easy.clear_ssl_ca_cert_blob();
2578                    return CURLcode::CURLE_OK;
2579                }
2580                // SAFETY: Caller guarantees value points to a valid curl_blob
2581                let blob = unsafe { &*value.cast::<curl_blob>() };
2582                if blob.data.is_null() || blob.len == 0 {
2583                    // Empty/null blob clears the setting (matches curl behavior)
2584                    h.easy.clear_ssl_ca_cert_blob();
2585                    return CURLcode::CURLE_OK;
2586                }
2587                // SAFETY: Caller guarantees blob.data points to blob.len bytes
2588                let data = unsafe { std::slice::from_raw_parts(blob.data.cast::<u8>(), blob.len) };
2589                h.easy.ssl_ca_cert_blob(data.to_vec());
2590                CURLcode::CURLE_OK
2591            }
2592
2593            // CURLOPT_MAXLIFETIME_CONN = 314
2594            314 => {
2595                // Accept max connection lifetime — pool handles expiry internally
2596                CURLcode::CURLE_OK
2597            }
2598
2599            // CURLOPT_BUFFERSIZE = 98
2600            98 => {
2601                // Accept buffer size hint — tokio manages its own buffer sizes
2602                CURLcode::CURLE_OK
2603            }
2604
2605            // CURLOPT_UPLOAD_BUFFERSIZE = 280
2606            280 => {
2607                // Accept upload buffer size hint
2608                CURLcode::CURLE_OK
2609            }
2610
2611            // CURLOPT_FILETIME = 69
2612            69 => {
2613                // Accept filetime request — transfer info already captures this
2614                CURLcode::CURLE_OK
2615            }
2616
2617            // ─── FTP options ───
2618
2619            // CURLOPT_FTPPORT = 10017
2620            10017 => {
2621                // SAFETY: value must be a null-terminated C string
2622                if let Some(s) = unsafe { read_cstr(value) } {
2623                    h.easy.ftp_active_port(s);
2624                }
2625                CURLcode::CURLE_OK
2626            }
2627
2628            // CURLOPT_FTP_USE_EPSV = 85
2629            85 => {
2630                h.easy.ftp_use_epsv(value as c_long != 0);
2631                CURLcode::CURLE_OK
2632            }
2633
2634            // CURLOPT_FTP_USE_EPRT = 106
2635            106 => {
2636                h.easy.ftp_use_eprt(value as c_long != 0);
2637                CURLcode::CURLE_OK
2638            }
2639
2640            // CURLOPT_FTP_CREATE_MISSING_DIRS = 110
2641            110 => {
2642                h.easy.ftp_create_dirs(value as c_long != 0);
2643                CURLcode::CURLE_OK
2644            }
2645
2646            // CURLOPT_FTP_SKIP_PASV_IP = 137
2647            137 => {
2648                h.easy.ftp_skip_pasv_ip(value as c_long != 0);
2649                CURLcode::CURLE_OK
2650            }
2651
2652            // CURLOPT_FTP_FILEMETHOD = 138
2653            138 => {
2654                #[allow(clippy::cast_sign_loss)]
2655                let method = match value as c_long {
2656                    1 => liburlx::FtpMethod::MultiCwd,
2657                    2 => liburlx::FtpMethod::NoCwd,
2658                    3 => liburlx::FtpMethod::SingleCwd,
2659                    _ => liburlx::FtpMethod::default(),
2660                };
2661                h.easy.ftp_method(method);
2662                CURLcode::CURLE_OK
2663            }
2664
2665            // CURLOPT_FTP_ACCOUNT = 10134
2666            10134 => {
2667                // SAFETY: value must be a null-terminated C string
2668                if let Some(s) = unsafe { read_cstr(value) } {
2669                    h.easy.ftp_account(s);
2670                }
2671                CURLcode::CURLE_OK
2672            }
2673
2674            // CURLOPT_FTP_ALTERNATIVE_TO_USER = 10147
2675            10147 => {
2676                // Accept but store as no-op (API compat)
2677                CURLcode::CURLE_OK
2678            }
2679
2680            // CURLOPT_FTP_SSL_CCC = 154
2681            154 => {
2682                // Accept clear command channel mode (not yet implemented)
2683                CURLcode::CURLE_OK
2684            }
2685
2686            // CURLOPT_FTP_USE_PRET = 188
2687            188 => {
2688                // Accept PRET option (not yet implemented)
2689                CURLcode::CURLE_OK
2690            }
2691
2692            // CURLOPT_USE_SSL = 119
2693            119 => {
2694                let mode = match value as c_long {
2695                    2 | 3 => liburlx::FtpSslMode::Explicit,
2696                    _ => liburlx::FtpSslMode::None,
2697                };
2698                h.easy.ftp_ssl_mode(mode);
2699                CURLcode::CURLE_OK
2700            }
2701
2702            // ─── SSH options ───
2703
2704            // CURLOPT_SSH_AUTH_TYPES = 151
2705            151 => {
2706                #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
2707                h.easy.ssh_auth_types(value as u32);
2708                CURLcode::CURLE_OK
2709            }
2710
2711            // CURLOPT_SSH_PUBLIC_KEYFILE = 10152
2712            10152 => {
2713                // SAFETY: value must be a null-terminated C string
2714                if let Some(s) = unsafe { read_cstr(value) } {
2715                    h.easy.ssh_public_keyfile(s);
2716                }
2717                CURLcode::CURLE_OK
2718            }
2719
2720            // CURLOPT_SSH_PRIVATE_KEYFILE = 10153
2721            10153 => {
2722                // SAFETY: value must be a null-terminated C string
2723                if let Some(s) = unsafe { read_cstr(value) } {
2724                    h.easy.ssh_key_path(s);
2725                }
2726                CURLcode::CURLE_OK
2727            }
2728
2729            // CURLOPT_SSH_KNOWNHOSTS = 10183
2730            10183 => {
2731                // SAFETY: value must be a null-terminated C string
2732                if let Some(s) = unsafe { read_cstr(value) } {
2733                    h.easy.ssh_known_hosts_path(s);
2734                }
2735                CURLcode::CURLE_OK
2736            }
2737
2738            // CURLOPT_SSH_HOST_PUBLIC_KEY_SHA256 = 10270
2739            10270 => {
2740                // SAFETY: value must be a null-terminated C string
2741                if let Some(s) = unsafe { read_cstr(value) } {
2742                    h.easy.ssh_host_key_sha256(s);
2743                }
2744                CURLcode::CURLE_OK
2745            }
2746
2747            // CURLOPT_SSH_HOST_PUBLIC_KEY_MD5 = 10162
2748            10162 => {
2749                // Accept MD5 fingerprint (deprecated, prefer SHA256)
2750                CURLcode::CURLE_OK
2751            }
2752
2753            // CURLOPT_SSH_COMPRESSION = 268
2754            268 => {
2755                // Accept compression flag (not yet implemented)
2756                CURLcode::CURLE_OK
2757            }
2758
2759            // ─── Proxy options ───
2760
2761            // CURLOPT_PROXYPORT = 59
2762            59 => {
2763                #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
2764                h.easy.proxy_port(value as u16);
2765                CURLcode::CURLE_OK
2766            }
2767
2768            // CURLOPT_PROXYTYPE = 101
2769            101 => {
2770                #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
2771                h.easy.proxy_type(value as u32);
2772                CURLcode::CURLE_OK
2773            }
2774
2775            // CURLOPT_PROXYUSERNAME = 10175
2776            10175 => {
2777                // SAFETY: value must be a null-terminated C string
2778                if let Some(s) = unsafe { read_cstr(value) } {
2779                    // Store username, combine with existing password
2780                    h.easy.proxy_auth(s, "");
2781                }
2782                CURLcode::CURLE_OK
2783            }
2784
2785            // CURLOPT_PROXYPASSWORD = 10176
2786            10176 => {
2787                // SAFETY: value must be a null-terminated C string
2788                if let Some(s) = unsafe { read_cstr(value) } {
2789                    // Store password, combine with existing username
2790                    h.easy.proxy_auth("", s);
2791                }
2792                CURLcode::CURLE_OK
2793            }
2794
2795            // CURLOPT_PRE_PROXY = 10262
2796            10262 => {
2797                // SAFETY: value must be a null-terminated C string
2798                if let Some(s) = unsafe { read_cstr(value) } {
2799                    h.easy.pre_proxy(s);
2800                }
2801                CURLcode::CURLE_OK
2802            }
2803
2804            // CURLOPT_PROXY_CAPATH = 10247
2805            10247 => {
2806                // Accept CA path for proxy — stored but not yet used
2807                CURLcode::CURLE_OK
2808            }
2809
2810            // CURLOPT_PROXY_CRLFILE = 10260
2811            10260 => {
2812                // Accept CRL file for proxy — stored but not yet used
2813                CURLcode::CURLE_OK
2814            }
2815
2816            // CURLOPT_PROXY_PINNEDPUBLICKEY = 10263
2817            10263 => {
2818                // Accept pinned public key for proxy — stored but not yet used
2819                CURLcode::CURLE_OK
2820            }
2821
2822            // CURLOPT_PROXY_SSLVERSION = 250
2823            250 => {
2824                // Accept proxy SSL version — stored but not yet used
2825                CURLcode::CURLE_OK
2826            }
2827
2828            // CURLOPT_PROXY_SSL_CIPHER_LIST = 10259
2829            10259 => {
2830                // Accept proxy cipher list — stored but not yet used
2831                CURLcode::CURLE_OK
2832            }
2833
2834            // CURLOPT_PROXY_TLS13_CIPHERS = 10277
2835            10277 => {
2836                // Accept proxy TLS 1.3 ciphers — stored but not yet used
2837                CURLcode::CURLE_OK
2838            }
2839
2840            // CURLOPT_SOCKS5_AUTH = 267
2841            267 => {
2842                // Accept SOCKS5 auth bitmask — stored but not yet used
2843                CURLcode::CURLE_OK
2844            }
2845
2846            // CURLOPT_RTSP_REQUEST = 189
2847            189 => {
2848                let req_val = value as c_long;
2849                #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
2850                if let Some(req) = liburlx::protocol::rtsp::RtspRequest::from_long(req_val as u32) {
2851                    h.easy.set_rtsp_request(req);
2852                    CURLcode::CURLE_OK
2853                } else {
2854                    CURLcode::CURLE_BAD_FUNCTION_ARGUMENT
2855                }
2856            }
2857
2858            // CURLOPT_RTSP_SESSION_ID = 10190
2859            10190 => {
2860                let sid = unsafe { read_cstr(value) };
2861                h.easy.set_rtsp_session_id(sid);
2862                CURLcode::CURLE_OK
2863            }
2864
2865            // CURLOPT_RTSP_STREAM_URI = 10191
2866            10191 => {
2867                if let Some(uri) = unsafe { read_cstr(value) } {
2868                    h.easy.set_rtsp_stream_uri(uri);
2869                }
2870                CURLcode::CURLE_OK
2871            }
2872
2873            // CURLOPT_RTSP_TRANSPORT = 10192
2874            10192 => {
2875                if let Some(transport) = unsafe { read_cstr(value) } {
2876                    h.easy.set_rtsp_transport(transport);
2877                }
2878                CURLcode::CURLE_OK
2879            }
2880
2881            // CURLOPT_RTSP_CLIENT_CSEQ = 193
2882            193 => {
2883                let cseq = value as c_long;
2884                #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
2885                h.easy.set_rtsp_client_cseq(cseq as u32);
2886                CURLcode::CURLE_OK
2887            }
2888
2889            // CURLOPT_RTSP_SERVER_CSEQ = 194, CURLOPT_INTERLEAVEFUNCTION = 20196,
2890            // CURLOPT_INTERLEAVEDATA = 10195
2891            194 | 20196 | 10195 => CURLcode::CURLE_OK,
2892
2893            _ => CURLcode::CURLE_UNKNOWN_OPTION,
2894        }
2895    }));
2896    result.unwrap_or(CURLcode::CURLE_UNKNOWN_OPTION)
2897}
2898
2899/// `curl_easy_perform` — perform the transfer.
2900///
2901/// # Safety
2902///
2903/// `handle` must be a valid pointer from `curl_easy_init`.
2904#[no_mangle]
2905#[allow(clippy::too_many_lines)]
2906pub unsafe extern "C" fn curl_easy_perform(handle: *mut c_void) -> CURLcode {
2907    if handle.is_null() {
2908        return CURLcode::CURLE_FAILED_INIT;
2909    }
2910
2911    // SAFETY: Caller guarantees handle is from curl_easy_init
2912    let h = unsafe { &mut *handle.cast::<EasyHandle>() };
2913
2914    // Set MIMEPOST body if configured
2915    if !h.mimepost.is_null() {
2916        // SAFETY: mimepost was set via CURLOPT_MIMEPOST from a curl_mime_init handle
2917        let mime = unsafe { &*h.mimepost.cast::<MimeHandle>() };
2918        let content_type = mime.form.content_type();
2919        let body = mime.form.encode();
2920        h.easy.header("Content-Type", &content_type);
2921        h.easy.body(&body);
2922    }
2923
2924    // Set POST body if configured (postfields takes precedence if both set)
2925    if let Some(ref body) = h.postfields {
2926        h.easy.body(body);
2927    } else if let Some(read_cb) = h.read_callback {
2928        // Read callback: collect upload data by calling the callback in a loop
2929        let mut upload_data = Vec::new();
2930        let mut buf = [0u8; 16384]; // 16 KiB read buffer
2931        loop {
2932            // SAFETY: Caller set up the read callback and data pointer correctly.
2933            // The callback writes into buf and returns bytes written (0 = EOF).
2934            let n =
2935                unsafe { read_cb(buf.as_mut_ptr().cast::<c_char>(), 1, buf.len(), h.read_data) };
2936            // CURL_READFUNC_ABORT = 0x10000000
2937            if n == 0x1000_0000 {
2938                return CURLcode::CURLE_ABORTED_BY_CALLBACK;
2939            }
2940            if n == 0 {
2941                break;
2942            }
2943            if n > buf.len() {
2944                return CURLcode::CURLE_READ_ERROR;
2945            }
2946            upload_data.extend_from_slice(&buf[..n]);
2947        }
2948        if !upload_data.is_empty() {
2949            h.easy.body(&upload_data);
2950        }
2951    }
2952
2953    // Perform the transfer, catching any panics
2954    let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| h.easy.perform()));
2955
2956    match result {
2957        Ok(Ok(response)) => {
2958            // Call debug callback if set — inform about headers and data
2959            if let Some(debug_cb) = h.debug_callback {
2960                // CURLINFO_HEADER_IN = 1 — response headers
2961                for (name, value) in response.headers() {
2962                    let line = format!("{name}: {value}\r\n");
2963                    let bytes = line.into_bytes();
2964                    // SAFETY: Caller set up the debug callback and data pointer correctly
2965                    let _ = unsafe {
2966                        debug_cb(
2967                            handle,
2968                            1, // CURLINFO_HEADER_IN
2969                            bytes.as_ptr().cast::<c_char>(),
2970                            bytes.len(),
2971                            h.debug_data,
2972                        )
2973                    };
2974                }
2975
2976                // CURLINFO_DATA_IN = 2 — response body
2977                let body = response.body();
2978                if !body.is_empty() {
2979                    // SAFETY: Caller set up the debug callback and data pointer correctly
2980                    let _ = unsafe {
2981                        debug_cb(
2982                            handle,
2983                            2, // CURLINFO_DATA_IN
2984                            body.as_ptr().cast::<c_char>(),
2985                            body.len(),
2986                            h.debug_data,
2987                        )
2988                    };
2989                }
2990            }
2991
2992            // Build output: optionally include headers, then body
2993            let mut output = Vec::new();
2994            if h.include_headers {
2995                if let Some(raw) = response.raw_headers() {
2996                    // Use raw headers as received from the wire (preserves
2997                    // original casing, order, and line endings).
2998                    output.extend_from_slice(raw);
2999                } else {
3000                    // Fallback: reconstruct from parsed headers
3001                    let eol = if response.uses_crlf() { "\r\n" } else { "\n" };
3002                    let reason = response.status_reason().unwrap_or("OK");
3003                    let http_ver = response.http_version();
3004                    let status_line =
3005                        format!("HTTP/{http_ver} {} {}{eol}", response.status(), reason);
3006                    output.extend_from_slice(status_line.as_bytes());
3007                    for (name, value) in response.headers_ordered() {
3008                        let line = format!("{name}: {value}{eol}");
3009                        output.extend_from_slice(line.as_bytes());
3010                    }
3011                    output.extend_from_slice(eol.as_bytes());
3012                }
3013            }
3014            output.extend_from_slice(response.body());
3015
3016            // Write output via callback, or to stdout if no callback is set
3017            if !output.is_empty() {
3018                if let Some(cb) = h.write_callback {
3019                    // SAFETY: Caller set up the callback and data pointer correctly
3020                    let written = unsafe {
3021                        cb(output.as_ptr().cast::<c_char>(), 1, output.len(), h.write_data)
3022                    };
3023                    if written != output.len() {
3024                        return CURLcode::CURLE_WRITE_ERROR;
3025                    }
3026                } else {
3027                    // Default: write to stdout (matches libcurl behavior)
3028                    use std::io::Write;
3029                    let _ = std::io::stdout().write_all(&output);
3030                }
3031            }
3032
3033            // Call header callback if set
3034            if let Some(cb) = h.header_callback {
3035                for (name, value) in response.headers() {
3036                    let header_line = format!("{name}: {value}\r\n");
3037                    let bytes = header_line.as_bytes();
3038                    // SAFETY: Caller set up the callback and data pointer correctly
3039                    let _written = unsafe {
3040                        cb(bytes.as_ptr().cast::<c_char>(), 1, bytes.len(), h.header_data)
3041                    };
3042                }
3043            }
3044
3045            // Call progress/xferinfo callback if set and noprogress is false
3046            if !h.noprogress {
3047                let info = response.transfer_info();
3048                let dl_total = response.body().len() as u64;
3049                let dl_now = dl_total;
3050                let ul_total = info.size_upload;
3051                let ul_now = ul_total;
3052                #[allow(clippy::cast_precision_loss, clippy::cast_possible_wrap)]
3053                if let Some(xfer_cb) = h.xferinfo_callback {
3054                    // SAFETY: Caller set up the callback and data pointer correctly
3055                    let ret = unsafe {
3056                        xfer_cb(
3057                            h.progress_data,
3058                            dl_total as i64,
3059                            dl_now as i64,
3060                            ul_total as i64,
3061                            ul_now as i64,
3062                        )
3063                    };
3064                    if ret != 0 {
3065                        return CURLcode::CURLE_ABORTED_BY_CALLBACK;
3066                    }
3067                } else if let Some(prog_cb) = h.progress_callback {
3068                    // SAFETY: Caller set up the callback and data pointer correctly
3069                    let ret = unsafe {
3070                        prog_cb(
3071                            h.progress_data,
3072                            dl_total as f64,
3073                            dl_now as f64,
3074                            ul_total as f64,
3075                            ul_now as f64,
3076                        )
3077                    };
3078                    if ret != 0 {
3079                        return CURLcode::CURLE_ABORTED_BY_CALLBACK;
3080                    }
3081                }
3082            }
3083
3084            h.last_response = Some(response);
3085            CURLcode::CURLE_OK
3086        }
3087        Ok(Err(e)) => {
3088            // Store error message
3089            let msg = e.to_string();
3090            let bytes = msg.as_bytes();
3091            let len = bytes.len().min(h.error_buf.len() - 1);
3092            h.error_buf[..len].copy_from_slice(&bytes[..len]);
3093            h.error_buf[len] = 0;
3094
3095            error_to_curlcode(&e)
3096        }
3097        Err(_) => {
3098            // Panic in perform — should not happen but handle gracefully
3099            CURLcode::CURLE_FAILED_INIT
3100        }
3101    }
3102}
3103
3104/// `curl_easy_getinfo` — get info about the last transfer.
3105///
3106/// # Safety
3107///
3108/// `handle` must be a valid pointer from `curl_easy_init`.
3109/// `out` must be a valid pointer to the appropriate type for the info code.
3110#[no_mangle]
3111#[allow(clippy::too_many_lines)]
3112pub unsafe extern "C" fn curl_easy_getinfo(
3113    handle: *mut c_void,
3114    info: c_long,
3115    out: *mut c_void,
3116) -> CURLcode {
3117    let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
3118        if handle.is_null() || out.is_null() {
3119            return CURLcode::CURLE_FAILED_INIT;
3120        }
3121
3122        // SAFETY: Caller guarantees handle is from curl_easy_init
3123        let h = unsafe { &mut *handle.cast::<EasyHandle>() };
3124
3125        // CURLINFO_PRIVATE doesn't require a completed transfer
3126        if info == 0x10_0015 {
3127            // SAFETY: Caller guarantees out points to *mut c_void
3128            let out = unsafe { &mut *out.cast::<*mut c_void>() };
3129            *out = h.private_data;
3130            return CURLcode::CURLE_OK;
3131        }
3132
3133        let Some(ref response) = h.last_response else {
3134            return CURLcode::CURLE_GOT_NOTHING;
3135        };
3136
3137        match info {
3138            // CURLINFO_EFFECTIVE_URL = 0x100001
3139            0x10_0001 => {
3140                // SAFETY: Caller guarantees out points to *const c_char
3141                let out = unsafe { &mut *out.cast::<*const c_char>() };
3142                *out = response.effective_url().as_ptr().cast::<c_char>();
3143                CURLcode::CURLE_OK
3144            }
3145
3146            // CURLINFO_RESPONSE_CODE = 0x200002
3147            0x20_0002 => {
3148                // SAFETY: Caller guarantees out points to a c_long
3149                let out = unsafe { &mut *out.cast::<c_long>() };
3150                *out = c_long::from(response.status());
3151                CURLcode::CURLE_OK
3152            }
3153
3154            // CURLINFO_TOTAL_TIME = 0x300003
3155            0x30_0003 => {
3156                // SAFETY: Caller guarantees out points to f64
3157                let out = unsafe { &mut *out.cast::<f64>() };
3158                *out = response.transfer_info().time_total.as_secs_f64();
3159                CURLcode::CURLE_OK
3160            }
3161
3162            // CURLINFO_NAMELOOKUP_TIME = 0x300004
3163            0x30_0004 => {
3164                // SAFETY: Caller guarantees out points to f64
3165                let out = unsafe { &mut *out.cast::<f64>() };
3166                *out = response.transfer_info().time_namelookup.as_secs_f64();
3167                CURLcode::CURLE_OK
3168            }
3169
3170            // CURLINFO_CONNECT_TIME = 0x300005
3171            0x30_0005 => {
3172                // SAFETY: Caller guarantees out points to f64
3173                let out = unsafe { &mut *out.cast::<f64>() };
3174                *out = response.transfer_info().time_connect.as_secs_f64();
3175                CURLcode::CURLE_OK
3176            }
3177
3178            // CURLINFO_SIZE_DOWNLOAD = 0x300008
3179            0x30_0008 => {
3180                // SAFETY: Caller guarantees out points to f64
3181                let out = unsafe { &mut *out.cast::<f64>() };
3182                #[allow(clippy::cast_precision_loss)]
3183                {
3184                    *out = response.size_download() as f64;
3185                }
3186                CURLcode::CURLE_OK
3187            }
3188
3189            // CURLINFO_SPEED_DOWNLOAD = 0x300009
3190            0x30_0009 => {
3191                // SAFETY: Caller guarantees out points to f64
3192                let out = unsafe { &mut *out.cast::<f64>() };
3193                let total = response.transfer_info().time_total.as_secs_f64();
3194                #[allow(clippy::cast_precision_loss)]
3195                if total > 0.0 {
3196                    *out = response.size_download() as f64 / total;
3197                } else {
3198                    *out = 0.0;
3199                }
3200                CURLcode::CURLE_OK
3201            }
3202
3203            // CURLINFO_HEADER_SIZE = 0x20000B
3204            0x20_000B => {
3205                // SAFETY: Caller guarantees out points to c_long
3206                let out = unsafe { &mut *out.cast::<c_long>() };
3207                // Estimate header size from response headers
3208                let header_size: usize = response
3209                    .headers()
3210                    .iter()
3211                    .map(|(k, v)| k.len() + v.len() + 4) // "key: value\r\n"
3212                    .sum();
3213                #[allow(clippy::cast_possible_wrap, clippy::cast_possible_truncation)]
3214                {
3215                    *out = header_size as c_long;
3216                }
3217                CURLcode::CURLE_OK
3218            }
3219
3220            // CURLINFO_STARTTRANSFER_TIME = 0x300011
3221            0x30_0011 => {
3222                // SAFETY: Caller guarantees out points to f64
3223                let out = unsafe { &mut *out.cast::<f64>() };
3224                *out = response.transfer_info().time_starttransfer.as_secs_f64();
3225                CURLcode::CURLE_OK
3226            }
3227
3228            // CURLINFO_CONTENT_TYPE = 0x100012
3229            0x10_0012 => {
3230                // SAFETY: Caller guarantees out points to *const c_char
3231                let out = unsafe { &mut *out.cast::<*const c_char>() };
3232                *out =
3233                    response.content_type().map_or(ptr::null(), |ct| ct.as_ptr().cast::<c_char>());
3234                CURLcode::CURLE_OK
3235            }
3236
3237            // CURLINFO_REDIRECT_COUNT = 0x200014
3238            0x20_0014 => {
3239                // SAFETY: Caller guarantees out points to c_long
3240                let out = unsafe { &mut *out.cast::<c_long>() };
3241                #[allow(clippy::cast_possible_wrap, clippy::cast_lossless)]
3242                {
3243                    *out = response.transfer_info().num_redirects as c_long;
3244                }
3245                CURLcode::CURLE_OK
3246            }
3247
3248            // CURLINFO_APPCONNECT_TIME = 0x300033
3249            0x30_0033 => {
3250                // SAFETY: Caller guarantees out points to f64
3251                let out = unsafe { &mut *out.cast::<f64>() };
3252                *out = response.transfer_info().time_appconnect.as_secs_f64();
3253                CURLcode::CURLE_OK
3254            }
3255
3256            // CURLINFO_SIZE_UPLOAD = 0x300007, CURLINFO_CONTENT_LENGTH_UPLOAD = 0x300010
3257            0x30_0007 | 0x30_0010 => {
3258                // SAFETY: Caller guarantees out points to f64
3259                let out = unsafe { &mut *out.cast::<f64>() };
3260                #[allow(clippy::cast_precision_loss)]
3261                {
3262                    *out = response.transfer_info().size_upload as f64;
3263                }
3264                CURLcode::CURLE_OK
3265            }
3266
3267            // CURLINFO_SPEED_UPLOAD = 0x30000A
3268            0x30_000A => {
3269                // SAFETY: Caller guarantees out points to f64
3270                let out = unsafe { &mut *out.cast::<f64>() };
3271                *out = response.transfer_info().speed_upload;
3272                CURLcode::CURLE_OK
3273            }
3274
3275            // CURLINFO_PRETRANSFER_TIME = 0x30000E
3276            0x30_000E => {
3277                // SAFETY: Caller guarantees out points to f64
3278                let out = unsafe { &mut *out.cast::<f64>() };
3279                *out = response.transfer_info().time_pretransfer.as_secs_f64();
3280                CURLcode::CURLE_OK
3281            }
3282
3283            // CURLINFO_SSL_VERIFYRESULT = 0x20000D
3284            0x20_000D => {
3285                // SAFETY: Caller guarantees out points to c_long
3286                let out = unsafe { &mut *out.cast::<c_long>() };
3287                // 0 = success (X509_V_OK). Since we either verify successfully or
3288                // fail the connection entirely, a completed transfer always means 0.
3289                *out = 0;
3290                CURLcode::CURLE_OK
3291            }
3292
3293            // CURLINFO_FILETIME = 0x20000E
3294            0x20_000E => {
3295                // SAFETY: Caller guarantees out points to c_long
3296                let out = unsafe { &mut *out.cast::<c_long>() };
3297                // We don't track file modification time — return -1 (unknown)
3298                *out = -1;
3299                CURLcode::CURLE_OK
3300            }
3301
3302            // CURLINFO_CONTENT_LENGTH_DOWNLOAD = 0x30000F
3303            0x30_000F => {
3304                // SAFETY: Caller guarantees out points to f64
3305                let out = unsafe { &mut *out.cast::<f64>() };
3306                // Return body size as content length (best we can do without storing the header)
3307                #[allow(clippy::cast_precision_loss)]
3308                {
3309                    *out = response.size_download() as f64;
3310                }
3311                CURLcode::CURLE_OK
3312            }
3313
3314            // CURLINFO_HTTP_VERSION = 0x200032
3315            0x20_0032 => {
3316                // SAFETY: Caller guarantees out points to c_long
3317                let out = unsafe { &mut *out.cast::<c_long>() };
3318                // Default to HTTP/1.1 = 2 since we currently don't track negotiated version
3319                *out = 2;
3320                CURLcode::CURLE_OK
3321            }
3322
3323            // CURLINFO_PRIMARY_PORT = 0x200040
3324            0x20_0040 => {
3325                // SAFETY: Caller guarantees out points to c_long
3326                let out = unsafe { &mut *out.cast::<c_long>() };
3327                // Extract port from effective URL
3328                if let Ok(url) = liburlx::Url::parse(response.effective_url()) {
3329                    *out = c_long::from(url.port_or_default().unwrap_or(0));
3330                } else {
3331                    *out = 0;
3332                }
3333                CURLcode::CURLE_OK
3334            }
3335
3336            // CURLINFO_OS_ERRNO = 0x200019
3337            0x20_0019 => {
3338                // SAFETY: Caller guarantees out points to c_long
3339                let out = unsafe { &mut *out.cast::<c_long>() };
3340                // We don't store OS errno — return 0
3341                *out = 0;
3342                CURLcode::CURLE_OK
3343            }
3344
3345            // CURLINFO_PRIMARY_IP = 0x100020
3346            0x10_0020 => {
3347                // SAFETY: Caller guarantees out points to *const c_char
3348                let out = unsafe { &mut *out.cast::<*const c_char>() };
3349                // We don't track the resolved IP; return empty string
3350                *out = c"".as_ptr();
3351                CURLcode::CURLE_OK
3352            }
3353
3354            // CURLINFO_NUM_CONNECTS = CURLINFO_LONG + 26 = 0x20001A
3355            0x20_001A => {
3356                // SAFETY: Caller guarantees out points to c_long
3357                let out = unsafe { &mut *out.cast::<c_long>() };
3358                // Each transfer makes at least 1 connection
3359                *out = 1;
3360                CURLcode::CURLE_OK
3361            }
3362
3363            // CURLINFO_LOCAL_IP = 0x100029
3364            0x10_0029 => {
3365                // SAFETY: Caller guarantees out points to *const c_char
3366                let out = unsafe { &mut *out.cast::<*const c_char>() };
3367                // We don't track local IP; return empty string
3368                *out = c"".as_ptr();
3369                CURLcode::CURLE_OK
3370            }
3371
3372            // CURLINFO_REDIRECT_URL = 0x100031
3373            0x10_0031 => {
3374                // SAFETY: Caller guarantees out points to *const c_char
3375                let out = unsafe { &mut *out.cast::<*const c_char>() };
3376                // Redirect URL is only set when we don't follow redirects
3377                if response.is_redirect() {
3378                    *out = response
3379                        .header("location")
3380                        .map_or(ptr::null(), |loc| loc.as_ptr().cast::<c_char>());
3381                } else {
3382                    *out = ptr::null();
3383                }
3384                CURLcode::CURLE_OK
3385            }
3386
3387            // CURLINFO_CONDITION_UNMET = 0x200035
3388            0x20_0035 => {
3389                // SAFETY: Caller guarantees out points to c_long
3390                let out = unsafe { &mut *out.cast::<c_long>() };
3391                // 304 Not Modified means condition was unmet
3392                *out = c_long::from(response.status() == 304);
3393                CURLcode::CURLE_OK
3394            }
3395
3396            // CURLINFO_LOCAL_PORT = 0x200042
3397            0x20_0042 => {
3398                // SAFETY: Caller guarantees out points to c_long
3399                let out = unsafe { &mut *out.cast::<c_long>() };
3400                // We don't track the local port used; return 0
3401                *out = 0;
3402                CURLcode::CURLE_OK
3403            }
3404
3405            // CURLINFO_SCHEME = 0x100044
3406            0x10_0044 => {
3407                // Return the scheme from the effective URL
3408                // Note: We store a pointer to the effective URL string which contains the scheme
3409                // SAFETY: Caller guarantees out points to *const c_char
3410                let out = unsafe { &mut *out.cast::<*const c_char>() };
3411                *out = response.effective_url().as_ptr().cast::<c_char>();
3412                CURLcode::CURLE_OK
3413            }
3414
3415            // CURLINFO_REDIRECT_TIME = 0x300013
3416            0x30_0013 => {
3417                // SAFETY: Caller guarantees out points to f64
3418                let out = unsafe { &mut *out.cast::<f64>() };
3419                // Redirect time = total time - time of the final request
3420                // Approximate: we don't track redirect-specific timing yet
3421                *out = 0.0;
3422                CURLcode::CURLE_OK
3423            }
3424
3425            // CURLINFO_TOTAL_TIME_T = 0x60003E (microseconds as curl_off_t)
3426            0x60_003E => {
3427                // SAFETY: Caller guarantees out points to i64 (curl_off_t)
3428                let out = unsafe { &mut *out.cast::<i64>() };
3429                #[allow(clippy::cast_possible_truncation)]
3430                {
3431                    *out = response.transfer_info().time_total.as_micros() as i64;
3432                }
3433                CURLcode::CURLE_OK
3434            }
3435
3436            // CURLINFO_NAMELOOKUP_TIME_T = 0x60003F (microseconds)
3437            0x60_003F => {
3438                // SAFETY: Caller provides valid output pointer; null-checked above
3439                let out = unsafe { &mut *out.cast::<i64>() };
3440                #[allow(clippy::cast_possible_truncation)]
3441                {
3442                    *out = response.transfer_info().time_namelookup.as_micros() as i64;
3443                }
3444                CURLcode::CURLE_OK
3445            }
3446
3447            // CURLINFO_CONNECT_TIME_T = 0x600040 (microseconds)
3448            0x60_0040 => {
3449                // SAFETY: Caller provides valid output pointer; null-checked above
3450                let out = unsafe { &mut *out.cast::<i64>() };
3451                #[allow(clippy::cast_possible_truncation)]
3452                {
3453                    *out = response.transfer_info().time_connect.as_micros() as i64;
3454                }
3455                CURLcode::CURLE_OK
3456            }
3457
3458            // CURLINFO_PRETRANSFER_TIME_T = 0x600041 (microseconds)
3459            0x60_0041 => {
3460                // SAFETY: Caller provides valid output pointer; null-checked above
3461                let out = unsafe { &mut *out.cast::<i64>() };
3462                #[allow(clippy::cast_possible_truncation)]
3463                {
3464                    *out = response.transfer_info().time_pretransfer.as_micros() as i64;
3465                }
3466                CURLcode::CURLE_OK
3467            }
3468
3469            // CURLINFO_STARTTRANSFER_TIME_T = 0x600042 (microseconds)
3470            0x60_0042 => {
3471                // SAFETY: Caller provides valid output pointer; null-checked above
3472                let out = unsafe { &mut *out.cast::<i64>() };
3473                #[allow(clippy::cast_possible_truncation)]
3474                {
3475                    *out = response.transfer_info().time_starttransfer.as_micros() as i64;
3476                }
3477                CURLcode::CURLE_OK
3478            }
3479
3480            // CURLINFO_REDIRECT_TIME_T = 0x600043 (microseconds)
3481            0x60_0043 => {
3482                // SAFETY: Caller provides valid output pointer; null-checked above
3483                let out = unsafe { &mut *out.cast::<i64>() };
3484                // Not yet tracked — return 0
3485                *out = 0;
3486                CURLcode::CURLE_OK
3487            }
3488
3489            // CURLINFO_APPCONNECT_TIME_T = 0x600044 (microseconds)
3490            0x60_0044 => {
3491                // SAFETY: Caller provides valid output pointer; null-checked above
3492                let out = unsafe { &mut *out.cast::<i64>() };
3493                #[allow(clippy::cast_possible_truncation)]
3494                {
3495                    *out = response.transfer_info().time_appconnect.as_micros() as i64;
3496                }
3497                CURLcode::CURLE_OK
3498            }
3499
3500            // CURLINFO_RETRY_AFTER = 0x20003A
3501            0x20_003A => {
3502                // SAFETY: Caller guarantees out points to c_long
3503                let out = unsafe { &mut *out.cast::<c_long>() };
3504                // We don't parse Retry-After header; return 0
3505                *out = 0;
3506                CURLcode::CURLE_OK
3507            }
3508
3509            // CURLINFO_SIZE_UPLOAD_T = 0x600045
3510            0x60_0045 => {
3511                // SAFETY: Caller provides valid output pointer; null-checked above
3512                let out = unsafe { &mut *out.cast::<i64>() };
3513                #[allow(clippy::cast_possible_wrap)]
3514                {
3515                    *out = response.transfer_info().size_upload as i64;
3516                }
3517                CURLcode::CURLE_OK
3518            }
3519
3520            // CURLINFO_SIZE_DOWNLOAD_T = 0x600046
3521            0x60_0046 => {
3522                // SAFETY: Caller provides valid output pointer; null-checked above
3523                let out = unsafe { &mut *out.cast::<i64>() };
3524                #[allow(clippy::cast_possible_wrap)]
3525                {
3526                    *out = response.size_download() as i64;
3527                }
3528                CURLcode::CURLE_OK
3529            }
3530
3531            // CURLINFO_SPEED_DOWNLOAD_T = 0x600047
3532            0x60_0047 => {
3533                // SAFETY: Caller provides valid output pointer; null-checked above
3534                let out = unsafe { &mut *out.cast::<i64>() };
3535                #[allow(clippy::cast_possible_truncation)]
3536                {
3537                    *out = response.transfer_info().speed_download as i64;
3538                }
3539                CURLcode::CURLE_OK
3540            }
3541
3542            // CURLINFO_SPEED_UPLOAD_T = 0x600048
3543            0x60_0048 => {
3544                // SAFETY: Caller provides valid output pointer; null-checked above
3545                let out = unsafe { &mut *out.cast::<i64>() };
3546                #[allow(clippy::cast_possible_truncation)]
3547                {
3548                    *out = response.transfer_info().speed_upload as i64;
3549                }
3550                CURLcode::CURLE_OK
3551            }
3552
3553            // CURLINFO_REQUEST_SIZE = 0x20000C
3554            0x20_000C => {
3555                // SAFETY: Caller guarantees out points to c_long
3556                let out = unsafe { &mut *out.cast::<c_long>() };
3557                // We don't track request size; return 0
3558                *out = 0;
3559                CURLcode::CURLE_OK
3560            }
3561
3562            // CURLINFO_HTTP_CONNECTCODE = 0x200016
3563            0x20_0016 => {
3564                // SAFETY: Caller guarantees out points to c_long
3565                let out = unsafe { &mut *out.cast::<c_long>() };
3566                // HTTP CONNECT response code (proxy tunneling); 0 if no proxy tunnel
3567                *out = 0;
3568                CURLcode::CURLE_OK
3569            }
3570
3571            // CURLINFO_HTTPAUTH_AVAIL = 0x200017
3572            0x20_0017 => {
3573                // SAFETY: Caller guarantees out points to c_long
3574                let out = unsafe { &mut *out.cast::<c_long>() };
3575                // Bitmask of available auth methods from server's WWW-Authenticate
3576                // CURLAUTH_BASIC=1, CURLAUTH_DIGEST=2, CURLAUTH_BEARER=64
3577                // Default to basic available
3578                *out = 1;
3579                CURLcode::CURLE_OK
3580            }
3581
3582            // CURLINFO_PROXYAUTH_AVAIL = 0x200018
3583            0x20_0018 => {
3584                // SAFETY: Caller guarantees out points to c_long
3585                let out = unsafe { &mut *out.cast::<c_long>() };
3586                // Bitmask of available proxy auth methods; 0 if no proxy
3587                *out = 0;
3588                CURLcode::CURLE_OK
3589            }
3590
3591            // CURLINFO_RTSP_SESSION_ID = 0x100045
3592            0x10_0045 => {
3593                let out = unsafe { &mut *out.cast::<*const c_char>() };
3594                if let Some(sid) = h.easy.rtsp_session_id() {
3595                    *out = sid.as_ptr().cast::<c_char>();
3596                } else {
3597                    *out = ptr::null();
3598                }
3599                CURLcode::CURLE_OK
3600            }
3601
3602            // CURLINFO_RTSP_CLIENT_CSEQ = 0x200045
3603            0x20_0045 => {
3604                let out = unsafe { &mut *out.cast::<c_long>() };
3605                // `as c_long` needed: c_long is i32 on Windows, i64 on Unix;
3606                // From<u32> is not impl for i32.
3607                #[allow(clippy::cast_lossless)]
3608                {
3609                    *out = h.easy.rtsp_client_cseq() as c_long;
3610                }
3611                CURLcode::CURLE_OK
3612            }
3613
3614            // CURLINFO_RTSP_SERVER_CSEQ = 0x200046
3615            0x20_0046 => {
3616                let out = unsafe { &mut *out.cast::<c_long>() };
3617                // `as c_long` needed: c_long is i32 on Windows, i64 on Unix;
3618                // From<u32> is not impl for i32.
3619                #[allow(clippy::cast_lossless)]
3620                {
3621                    *out = h.easy.rtsp_server_cseq() as c_long;
3622                }
3623                CURLcode::CURLE_OK
3624            }
3625
3626            // CURLINFO_RTSP_CSEQ_RECV = 0x200047
3627            0x20_0047 => {
3628                let out = unsafe { &mut *out.cast::<c_long>() };
3629                // `as c_long` needed: c_long is i32 on Windows, i64 on Unix;
3630                // From<u32> is not impl for i32.
3631                #[allow(clippy::cast_lossless)]
3632                {
3633                    *out = h.easy.rtsp_cseq_recv() as c_long;
3634                }
3635                CURLcode::CURLE_OK
3636            }
3637
3638            _ => CURLcode::CURLE_UNKNOWN_OPTION,
3639        }
3640    }));
3641    result.unwrap_or(CURLcode::CURLE_UNKNOWN_OPTION)
3642}
3643
3644/// `curl_easy_strerror` — return a human-readable error message.
3645///
3646/// # Safety
3647///
3648/// The returned pointer is valid for the lifetime of the program.
3649#[no_mangle]
3650#[allow(clippy::missing_const_for_fn)] // const extern "C" fn not stable on MSRV 1.75
3651pub extern "C" fn curl_easy_strerror(code: CURLcode) -> *const c_char {
3652    let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
3653        let msg = match code {
3654            CURLcode::CURLE_OK => c"No error",
3655            CURLcode::CURLE_UNSUPPORTED_PROTOCOL => c"Unsupported protocol",
3656            CURLcode::CURLE_FAILED_INIT => c"Failed initialization",
3657            CURLcode::CURLE_URL_MALFORMAT => c"URL using bad/illegal format or missing URL",
3658            CURLcode::CURLE_COULDNT_RESOLVE_PROXY => c"Couldn't resolve proxy name",
3659            CURLcode::CURLE_COULDNT_RESOLVE_HOST => c"Couldn't resolve host name",
3660            CURLcode::CURLE_COULDNT_CONNECT => c"Failed to connect to host or proxy",
3661            CURLcode::CURLE_FTP_WEIRD_SERVER_REPLY => c"Weird server reply",
3662            CURLcode::CURLE_REMOTE_ACCESS_DENIED => c"Access denied",
3663            CURLcode::CURLE_HTTP2 => c"Error in the HTTP2 framing layer",
3664            CURLcode::CURLE_HTTP_RETURNED_ERROR => c"HTTP response code said error",
3665            CURLcode::CURLE_WRITE_ERROR => c"Failed writing received data to disk/application",
3666            CURLcode::CURLE_READ_ERROR => c"Failed to read data",
3667            CURLcode::CURLE_OUT_OF_MEMORY => c"Out of memory",
3668            CURLcode::CURLE_OPERATION_TIMEDOUT => c"Operation timed out",
3669            CURLcode::CURLE_SSL_CONNECT_ERROR => c"SSL connect error",
3670            CURLcode::CURLE_ABORTED_BY_CALLBACK => c"Aborted by callback",
3671            CURLcode::CURLE_BAD_FUNCTION_ARGUMENT => c"A libcurl function was given a bad argument",
3672            CURLcode::CURLE_UNKNOWN_OPTION => c"An unknown option was passed to libcurl",
3673            CURLcode::CURLE_GOT_NOTHING => c"Server returned nothing (no headers, no data)",
3674            CURLcode::CURLE_SEND_ERROR => c"Failed sending data to the peer",
3675            CURLcode::CURLE_RECV_ERROR => c"Failure when receiving data from the peer",
3676            CURLcode::CURLE_SSL_CERTPROBLEM => c"Problem with the local SSL certificate",
3677            CURLcode::CURLE_PEER_FAILED_VERIFICATION => {
3678                c"SSL peer certificate or SSH remote key was not OK"
3679            }
3680            CURLcode::CURLE_LOGIN_DENIED => c"Login denied",
3681            CURLcode::CURLE_FILESIZE_EXCEEDED => c"Maximum file size exceeded",
3682            CURLcode::CURLE_TOO_MANY_REDIRECTS => c"Number of redirects hit maximum amount",
3683            CURLcode::CURLE_HTTP3 => c"Error in the HTTP3 layer",
3684            CURLcode::CURLE_PARTIAL_FILE => c"Transferred a partial file",
3685            CURLcode::CURLE_RANGE_ERROR => c"Requested range was not delivered",
3686            CURLcode::CURLE_AGAIN => c"Socket is not ready for send/recv",
3687            CURLcode::CURLE_AUTH_ERROR => c"An authentication function returned an error",
3688            CURLcode::CURLE_UNRECOVERABLE_POLL => c"Unrecoverable error in select/poll",
3689            CURLcode::CURLE_FTP_COULDNT_RETR_FILE => c"FTP: couldn't retrieve (RETR failed)",
3690            CURLcode::CURLE_UPLOAD_FAILED => c"Upload failed",
3691            CURLcode::CURLE_LDAP_SEARCH_FAILED => c"LDAP search failed",
3692            CURLcode::CURLE_FUNCTION_NOT_FOUND => c"A required function was not found",
3693            CURLcode::CURLE_INTERFACE_FAILED => c"Failed binding local connection end",
3694            CURLcode::CURLE_SSL_ENGINE_NOTFOUND => c"SSL crypto engine not found",
3695            CURLcode::CURLE_SSL_ENGINE_SETFAILED => c"Can not set SSL crypto engine as default",
3696            CURLcode::CURLE_RTSP_CSEQ_ERROR => c"RTSP CSeq mismatch or invalid CSeq",
3697            CURLcode::CURLE_RTSP_SESSION_ERROR => c"RTSP session error",
3698            CURLcode::CURLE_SSL_PINNEDPUBKEYNOTMATCH => c"SSL public key does not match pinned key",
3699            CURLcode::CURLE_SSL_INVALIDCERTSTATUS => {
3700                c"SSL server certificate status verification failed"
3701            }
3702        };
3703        msg.as_ptr()
3704    }));
3705    result.unwrap_or(c"Unknown error".as_ptr())
3706}
3707
3708// ───────────────────────── Multi handle ─────────────────────────
3709
3710/// Internal state for a multi handle.
3711struct MultiHandle {
3712    multi: liburlx::Multi,
3713    easy_handles: Vec<*mut c_void>,
3714    /// Stored completion messages for `curl_multi_info_read`.
3715    msg_queue: Vec<CURLMsg>,
3716    /// Socket callback (accepted but not actively called).
3717    socket_callback: Option<CurlSocketCallback>,
3718    /// Socket callback user data.
3719    socket_data: *mut c_void,
3720    /// Timer callback (accepted but not actively called).
3721    timer_callback: Option<CurlTimerCallback>,
3722    /// Timer callback user data.
3723    timer_data: *mut c_void,
3724}
3725
3726// SAFETY: Easy handles and callback pointers are only accessed from the perform thread.
3727// The raw pointers (socket_data, timer_data) are C caller-provided and only dereferenced
3728// inside callback invocations, matching libcurl's thread-safety model.
3729#[allow(clippy::non_send_fields_in_send_ty)]
3730unsafe impl Send for MultiHandle {}
3731
3732/// `curl_multi_init` — create a new multi handle.
3733///
3734/// # Safety
3735///
3736/// Returns a new handle that must be freed with `curl_multi_cleanup`.
3737#[no_mangle]
3738pub extern "C" fn curl_multi_init() -> *mut c_void {
3739    let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
3740        let handle = Box::new(MultiHandle {
3741            multi: liburlx::Multi::new(),
3742            easy_handles: Vec::new(),
3743            msg_queue: Vec::new(),
3744            socket_callback: None,
3745            socket_data: ptr::null_mut(),
3746            timer_callback: None,
3747            timer_data: ptr::null_mut(),
3748        });
3749        Box::into_raw(handle).cast::<c_void>()
3750    }));
3751    result.unwrap_or(ptr::null_mut())
3752}
3753
3754/// `curl_multi_cleanup` — free a multi handle.
3755///
3756/// # Safety
3757///
3758/// `handle` must be a valid pointer from `curl_multi_init`, or null.
3759#[no_mangle]
3760pub unsafe extern "C" fn curl_multi_cleanup(handle: *mut c_void) -> CURLMcode {
3761    let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
3762        if handle.is_null() {
3763            return CURLMcode::CURLM_BAD_HANDLE;
3764        }
3765        // SAFETY: Caller guarantees handle is from curl_multi_init
3766        let _ = unsafe { Box::from_raw(handle.cast::<MultiHandle>()) };
3767        CURLMcode::CURLM_OK
3768    }));
3769    result.unwrap_or(CURLMcode::CURLM_INTERNAL_ERROR)
3770}
3771
3772/// `curl_multi_add_handle` — add an easy handle to a multi handle.
3773///
3774/// # Safety
3775///
3776/// `multi` must be from `curl_multi_init`, `easy` from `curl_easy_init`.
3777#[no_mangle]
3778pub unsafe extern "C" fn curl_multi_add_handle(multi: *mut c_void, easy: *mut c_void) -> CURLMcode {
3779    let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
3780        if multi.is_null() {
3781            return CURLMcode::CURLM_BAD_HANDLE;
3782        }
3783        if easy.is_null() {
3784            return CURLMcode::CURLM_BAD_EASY_HANDLE;
3785        }
3786
3787        // SAFETY: Caller guarantees handles are valid
3788        let m = unsafe { &mut *multi.cast::<MultiHandle>() };
3789        let e = unsafe { &*easy.cast::<EasyHandle>() };
3790
3791        m.multi.add(e.easy.clone());
3792        m.easy_handles.push(easy);
3793
3794        CURLMcode::CURLM_OK
3795    }));
3796    result.unwrap_or(CURLMcode::CURLM_INTERNAL_ERROR)
3797}
3798
3799/// `curl_multi_remove_handle` — remove an easy handle from a multi handle.
3800///
3801/// # Safety
3802///
3803/// `multi` must be from `curl_multi_init`, `easy` from `curl_easy_init`.
3804#[no_mangle]
3805pub unsafe extern "C" fn curl_multi_remove_handle(
3806    multi: *mut c_void,
3807    easy: *mut c_void,
3808) -> CURLMcode {
3809    let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
3810        if multi.is_null() {
3811            return CURLMcode::CURLM_BAD_HANDLE;
3812        }
3813        if easy.is_null() {
3814            return CURLMcode::CURLM_BAD_EASY_HANDLE;
3815        }
3816
3817        // SAFETY: Caller guarantees handles are valid
3818        let m = unsafe { &mut *multi.cast::<MultiHandle>() };
3819
3820        if let Some(pos) = m.easy_handles.iter().position(|&h| h == easy) {
3821            let _ = m.easy_handles.remove(pos);
3822            let _ = m.multi.remove(pos);
3823            CURLMcode::CURLM_OK
3824        } else {
3825            CURLMcode::CURLM_BAD_EASY_HANDLE
3826        }
3827    }));
3828    result.unwrap_or(CURLMcode::CURLM_INTERNAL_ERROR)
3829}
3830
3831/// `curl_multi_perform` — perform all queued transfers.
3832///
3833/// # Safety
3834///
3835/// `multi` must be from `curl_multi_init`.
3836/// `running_handles` must be a valid pointer to an int, or null.
3837#[no_mangle]
3838pub unsafe extern "C" fn curl_multi_perform(
3839    multi: *mut c_void,
3840    running_handles: *mut c_long,
3841) -> CURLMcode {
3842    let outer = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
3843        if multi.is_null() {
3844            return CURLMcode::CURLM_BAD_HANDLE;
3845        }
3846
3847        // SAFETY: Caller guarantees multi is from curl_multi_init
3848        let m = unsafe { &mut *multi.cast::<MultiHandle>() };
3849
3850        // Snapshot easy handle pointers before perform (which drains internal handles)
3851        let easy_ptrs: Vec<*mut c_void> = m.easy_handles.clone();
3852
3853        let result = m.multi.perform_blocking();
3854
3855        match result {
3856            Ok(results) => {
3857                // Clear any stale messages from a previous perform
3858                m.msg_queue.clear();
3859
3860                // Store results back into easy handles and build msg_queue
3861                for (i, result) in results.into_iter().enumerate() {
3862                    if i < easy_ptrs.len() {
3863                        // SAFETY: easy_handles[i] is from curl_easy_init
3864                        let eh = unsafe { &mut *easy_ptrs[i].cast::<EasyHandle>() };
3865                        let curl_result = match result {
3866                            Ok(response) => {
3867                                eh.last_response = Some(response);
3868                                CURLcode::CURLE_OK
3869                            }
3870                            Err(e) => {
3871                                let code = error_to_curlcode(&e);
3872                                let msg = e.to_string();
3873                                let bytes = msg.as_bytes();
3874                                let len = bytes.len().min(eh.error_buf.len() - 1);
3875                                eh.error_buf[..len].copy_from_slice(&bytes[..len]);
3876                                eh.error_buf[len] = 0;
3877                                code
3878                            }
3879                        };
3880
3881                        m.msg_queue.push(CURLMsg {
3882                            msg: CURLMSG::CURLMSG_DONE,
3883                            easy_handle: easy_ptrs[i],
3884                            result: curl_result,
3885                        });
3886                    }
3887                }
3888
3889                if !running_handles.is_null() {
3890                    // SAFETY: Caller guarantees running_handles is valid
3891                    unsafe {
3892                        *running_handles = 0;
3893                    } // All done after blocking perform
3894                }
3895
3896                CURLMcode::CURLM_OK
3897            }
3898            Err(_) => CURLMcode::CURLM_INTERNAL_ERROR,
3899        }
3900    }));
3901    outer.unwrap_or(CURLMcode::CURLM_INTERNAL_ERROR)
3902}
3903
3904/// `curl_multi_info_read` — read a completion message from the multi handle.
3905///
3906/// Returns a pointer to a `CURLMsg` struct, or null if no messages remain.
3907/// The `msgs_in_queue` output parameter is set to the number of remaining messages.
3908///
3909/// # Safety
3910///
3911/// `multi` must be from `curl_multi_init`.
3912/// `msgs_in_queue` must be a valid pointer to a `c_long`, or null.
3913/// The returned pointer is valid until the next call to `curl_multi_info_read`
3914/// or `curl_multi_perform`.
3915#[no_mangle]
3916pub unsafe extern "C" fn curl_multi_info_read(
3917    multi: *mut c_void,
3918    msgs_in_queue: *mut c_long,
3919) -> *const CURLMsg {
3920    let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
3921        if multi.is_null() {
3922            if !msgs_in_queue.is_null() {
3923                // SAFETY: Caller guarantees msgs_in_queue is valid
3924                unsafe {
3925                    *msgs_in_queue = 0;
3926                }
3927            }
3928            return ptr::null();
3929        }
3930
3931        // SAFETY: Caller guarantees multi is from curl_multi_init
3932        let m = unsafe { &mut *multi.cast::<MultiHandle>() };
3933
3934        if m.msg_queue.is_empty() {
3935            if !msgs_in_queue.is_null() {
3936                // SAFETY: Caller guarantees pointer is valid
3937                unsafe {
3938                    *msgs_in_queue = 0;
3939                }
3940            }
3941            return ptr::null();
3942        }
3943
3944        // Pop the first message and return a pointer to the last element
3945        // We rotate: remove from front, but we need a stable pointer.
3946        // Strategy: swap-remove from front, store "current" separately.
3947        let msg = m.msg_queue.remove(0);
3948
3949        // Store remaining count
3950        if !msgs_in_queue.is_null() {
3951            // SAFETY: Caller guarantees pointer is valid
3952            unsafe {
3953                #[allow(clippy::cast_possible_wrap)]
3954                {
3955                    *msgs_in_queue = m.msg_queue.len() as c_long;
3956                }
3957            }
3958        }
3959
3960        // We need to return a pointer that remains valid until next call.
3961        // Push to the end and return pointer to last element.
3962        m.msg_queue.push(msg);
3963        let last_idx = m.msg_queue.len() - 1;
3964        &raw const m.msg_queue[last_idx]
3965    }));
3966    result.unwrap_or(ptr::null::<CURLMsg>())
3967}
3968
3969/// `curl_multi_setopt` — set options on a multi handle.
3970///
3971/// # Safety
3972///
3973/// `multi` must be from `curl_multi_init`.
3974/// The interpretation of `value` depends on the option.
3975#[no_mangle]
3976pub unsafe extern "C" fn curl_multi_setopt(
3977    multi: *mut c_void,
3978    option: c_long,
3979    value: *const c_void,
3980) -> CURLMcode {
3981    let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
3982        if multi.is_null() {
3983            return CURLMcode::CURLM_BAD_HANDLE;
3984        }
3985
3986        // SAFETY: Caller guarantees multi is from curl_multi_init
3987        let m = unsafe { &mut *multi.cast::<MultiHandle>() };
3988
3989        match option {
3990            // CURLMOPT_PIPELINING = 3
3991            3 => {
3992                let val = value as c_long;
3993                if val == 0 {
3994                    m.multi.pipelining(liburlx::PipeliningMode::Nothing);
3995                } else {
3996                    m.multi.pipelining(liburlx::PipeliningMode::Multiplex);
3997                }
3998                CURLMcode::CURLM_OK
3999            }
4000            // CURLMOPT_MAXCONNECTS = 6, CURLMOPT_MAX_TOTAL_CONNECTIONS = 13
4001            6 | 13 => {
4002                let val = value as usize;
4003                if val > 0 {
4004                    m.multi.max_total_connections(val);
4005                }
4006                CURLMcode::CURLM_OK
4007            }
4008            // CURLMOPT_MAX_HOST_CONNECTIONS = 7
4009            7 => {
4010                let val = value as usize;
4011                if val > 0 {
4012                    m.multi.max_host_connections(val);
4013                }
4014                CURLMcode::CURLM_OK
4015            }
4016            // CURLMOPT_SOCKETDATA = 10002
4017            10002 => {
4018                m.socket_data = value.cast_mut();
4019                CURLMcode::CURLM_OK
4020            }
4021            // CURLMOPT_TIMERDATA = 10005
4022            10005 => {
4023                m.timer_data = value.cast_mut();
4024                CURLMcode::CURLM_OK
4025            }
4026            // CURLMOPT_SOCKETFUNCTION = 20001
4027            20001 => {
4028                if value.is_null() {
4029                    m.socket_callback = None;
4030                } else {
4031                    // SAFETY: Caller guarantees value is a valid function pointer
4032                    m.socket_callback = Some(unsafe {
4033                        std::mem::transmute::<*const c_void, CurlSocketCallback>(value)
4034                    });
4035                }
4036                CURLMcode::CURLM_OK
4037            }
4038            // CURLMOPT_TIMERFUNCTION = 20004
4039            20004 => {
4040                if value.is_null() {
4041                    m.timer_callback = None;
4042                } else {
4043                    // SAFETY: Caller guarantees value is a valid function pointer
4044                    m.timer_callback = Some(unsafe {
4045                        std::mem::transmute::<*const c_void, CurlTimerCallback>(value)
4046                    });
4047                }
4048                CURLMcode::CURLM_OK
4049            }
4050            _ => CURLMcode::CURLM_UNKNOWN_OPTION,
4051        }
4052    }));
4053    result.unwrap_or(CURLMcode::CURLM_INTERNAL_ERROR)
4054}
4055
4056/// `curl_multi_timeout` — return the timeout value for the multi handle.
4057///
4058/// Returns the number of milliseconds until the application should call
4059/// `curl_multi_perform` or similar. Returns -1 if no timeout is set.
4060///
4061/// # Safety
4062///
4063/// `multi` must be from `curl_multi_init`.
4064/// `timeout_ms` must be a valid pointer to a `c_long`.
4065#[no_mangle]
4066pub unsafe extern "C" fn curl_multi_timeout(
4067    multi: *mut c_void,
4068    timeout_ms: *mut c_long,
4069) -> CURLMcode {
4070    let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
4071        if multi.is_null() {
4072            return CURLMcode::CURLM_BAD_HANDLE;
4073        }
4074        if timeout_ms.is_null() {
4075            return CURLMcode::CURLM_BAD_HANDLE;
4076        }
4077
4078        // Since tokio owns the event loop, we report -1 (no timeout needed)
4079        // when no transfers are running, or 0 (call immediately) when there are
4080        // pending messages to read.
4081        // SAFETY: Caller guarantees multi is from curl_multi_init
4082        let m = unsafe { &*multi.cast::<MultiHandle>() };
4083
4084        // SAFETY: Caller guarantees timeout_ms is valid
4085        unsafe {
4086            if m.msg_queue.is_empty() && m.easy_handles.is_empty() {
4087                *timeout_ms = -1; // No work to do
4088            } else if !m.msg_queue.is_empty() {
4089                *timeout_ms = 0; // Messages ready
4090            } else {
4091                *timeout_ms = 100; // Transfers pending, suggest polling at 100ms
4092            }
4093        }
4094
4095        CURLMcode::CURLM_OK
4096    }));
4097    result.unwrap_or(CURLMcode::CURLM_INTERNAL_ERROR)
4098}
4099
4100/// `curl_multi_wait` — wait for activity on any of the multi handle's transfers.
4101///
4102/// Since tokio manages I/O internally, this function simply sleeps for the
4103/// specified timeout (or a default of 1000ms if `timeout_ms` is 0).
4104///
4105/// # Safety
4106///
4107/// `multi` must be from `curl_multi_init`.
4108/// `extra_fds` and `extra_nfds` specify additional file descriptors to wait on (ignored).
4109/// `numfds` receives the number of ready file descriptors (always 0 in this implementation).
4110#[no_mangle]
4111pub unsafe extern "C" fn curl_multi_wait(
4112    multi: *mut c_void,
4113    _extra_fds: *mut curl_waitfd,
4114    _extra_nfds: c_long,
4115    timeout_ms: c_long,
4116    numfds: *mut c_long,
4117) -> CURLMcode {
4118    let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
4119        if multi.is_null() {
4120            return CURLMcode::CURLM_BAD_HANDLE;
4121        }
4122
4123        // Sleep for the requested timeout. Since tokio handles I/O internally,
4124        // we just provide a simple delay for C consumers that expect poll-style behavior.
4125        #[allow(clippy::cast_sign_loss)]
4126        let ms = if timeout_ms <= 0 { 100 } else { timeout_ms as u64 };
4127        std::thread::sleep(std::time::Duration::from_millis(ms));
4128
4129        if !numfds.is_null() {
4130            // SAFETY: Caller guarantees numfds is valid
4131            unsafe {
4132                *numfds = 0;
4133            }
4134        }
4135
4136        CURLMcode::CURLM_OK
4137    }));
4138    result.unwrap_or(CURLMcode::CURLM_INTERNAL_ERROR)
4139}
4140
4141/// `curl_multi_poll` — poll for activity on any of the multi handle's transfers.
4142///
4143/// Equivalent to `curl_multi_wait` but with a guaranteed wakeup mechanism.
4144/// Since tokio handles I/O, this has the same behavior as `curl_multi_wait`.
4145///
4146/// # Safety
4147///
4148/// Same safety requirements as `curl_multi_wait`.
4149#[no_mangle]
4150pub unsafe extern "C" fn curl_multi_poll(
4151    multi: *mut c_void,
4152    fds: *mut curl_waitfd,
4153    nfds: c_long,
4154    timeout_ms: c_long,
4155    numfds: *mut c_long,
4156) -> CURLMcode {
4157    let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
4158        // SAFETY: Same guarantees apply — delegating to curl_multi_wait
4159        unsafe { curl_multi_wait(multi, fds, nfds, timeout_ms, numfds) }
4160    }));
4161    result.unwrap_or(CURLMcode::CURLM_INTERNAL_ERROR)
4162}
4163
4164/// `curl_multi_wakeup` — wake up a sleeping `curl_multi_poll`.
4165///
4166/// Since our poll is a simple sleep, this is a no-op that returns OK.
4167///
4168/// # Safety
4169///
4170/// `multi` must be from `curl_multi_init`.
4171#[no_mangle]
4172pub unsafe extern "C" fn curl_multi_wakeup(multi: *mut c_void) -> CURLMcode {
4173    let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
4174        if multi.is_null() {
4175            return CURLMcode::CURLM_BAD_HANDLE;
4176        }
4177        // No-op: tokio manages I/O internally
4178        CURLMcode::CURLM_OK
4179    }));
4180    result.unwrap_or(CURLMcode::CURLM_INTERNAL_ERROR)
4181}
4182
4183/// `curl_multi_fdset` — extract file descriptors from the multi handle.
4184///
4185/// Since tokio manages all I/O internally, no file descriptors are exposed.
4186/// All output fd values are set to -1.
4187///
4188/// # Safety
4189///
4190/// `multi` must be from `curl_multi_init`.
4191/// `max_fd` must be a valid pointer to a `c_long`.
4192/// `read_fd_set`, `write_fd_set`, and `exc_fd_set` are ignored (accept null).
4193#[no_mangle]
4194pub unsafe extern "C" fn curl_multi_fdset(
4195    multi: *mut c_void,
4196    _read_fd_set: *mut c_void,
4197    _write_fd_set: *mut c_void,
4198    _exc_fd_set: *mut c_void,
4199    max_fd: *mut c_long,
4200) -> CURLMcode {
4201    let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
4202        if multi.is_null() {
4203            return CURLMcode::CURLM_BAD_HANDLE;
4204        }
4205
4206        if !max_fd.is_null() {
4207            // SAFETY: Caller guarantees max_fd is valid
4208            unsafe {
4209                *max_fd = -1; // No fds exposed — tokio owns socket polling
4210            }
4211        }
4212
4213        CURLMcode::CURLM_OK
4214    }));
4215    result.unwrap_or(CURLMcode::CURLM_INTERNAL_ERROR)
4216}
4217
4218/// `curl_multi_socket_action` — socket action interface for event-driven programs.
4219///
4220/// Since tokio handles all socket I/O internally, this delegates to a blocking
4221/// perform when called with `CURL_SOCKET_TIMEOUT` (-1). For specific socket
4222/// actions, it is a no-op.
4223///
4224/// # Safety
4225///
4226/// `multi` must be from `curl_multi_init`.
4227/// `running_handles` must be a valid pointer to a `c_long`, or null.
4228#[no_mangle]
4229pub unsafe extern "C" fn curl_multi_socket_action(
4230    multi: *mut c_void,
4231    sockfd: c_long,
4232    _ev_bitmask: c_long,
4233    running_handles: *mut c_long,
4234) -> CURLMcode {
4235    let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
4236        if multi.is_null() {
4237            return CURLMcode::CURLM_BAD_HANDLE;
4238        }
4239
4240        // CURL_SOCKET_TIMEOUT = -1 means "timeout expired, check for work"
4241        if sockfd == -1 {
4242            // Delegate to perform
4243            // SAFETY: Same guarantees apply
4244            return unsafe { curl_multi_perform(multi, running_handles) };
4245        }
4246
4247        // For specific socket events, report current state
4248        if !running_handles.is_null() {
4249            // SAFETY: Caller guarantees multi is from curl_multi_init
4250            let m = unsafe { &*multi.cast::<MultiHandle>() };
4251            // SAFETY: Caller guarantees running_handles is valid
4252            unsafe {
4253                #[allow(clippy::cast_possible_wrap)]
4254                {
4255                    *running_handles = m.easy_handles.len() as c_long;
4256                }
4257            }
4258        }
4259
4260        CURLMcode::CURLM_OK
4261    }));
4262    result.unwrap_or(CURLMcode::CURLM_INTERNAL_ERROR)
4263}
4264
4265/// `curl_multi_strerror` — return a human-readable multi error message.
4266///
4267/// # Safety
4268///
4269/// The returned pointer is valid for the lifetime of the program.
4270#[no_mangle]
4271#[allow(clippy::missing_const_for_fn)]
4272pub extern "C" fn curl_multi_strerror(code: CURLMcode) -> *const c_char {
4273    let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
4274        let msg = match code {
4275            CURLMcode::CURLM_OK => c"No error",
4276            CURLMcode::CURLM_BAD_HANDLE => c"Invalid multi handle",
4277            CURLMcode::CURLM_BAD_EASY_HANDLE => c"Invalid easy handle",
4278            CURLMcode::CURLM_OUT_OF_MEMORY => c"Out of memory",
4279            CURLMcode::CURLM_INTERNAL_ERROR => c"Internal error",
4280            CURLMcode::CURLM_UNKNOWN_OPTION => c"Unknown option",
4281        };
4282        msg.as_ptr()
4283    }));
4284    result.unwrap_or(c"Unknown error".as_ptr())
4285}
4286
4287// ───────────────────────── Utility functions ─────────────────────────
4288
4289/// `curl_escape` — URL-encode a string.
4290///
4291/// Returns a newly allocated string that must be freed with `curl_free`.
4292/// If `length` is 0, the string is treated as null-terminated.
4293///
4294/// # Safety
4295///
4296/// `string` must be a valid pointer to at least `length` bytes.
4297/// If `length` is 0, `string` must be null-terminated.
4298#[no_mangle]
4299pub unsafe extern "C" fn curl_escape(string: *const c_char, length: c_long) -> *mut c_char {
4300    let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
4301        if string.is_null() {
4302            return ptr::null_mut();
4303        }
4304
4305        let input = if length == 0 {
4306            // SAFETY: Caller guarantees string is null-terminated
4307            unsafe { CStr::from_ptr(string) }.to_bytes()
4308        } else {
4309            #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
4310            // SAFETY: Caller guarantees string points to at least length bytes
4311            unsafe {
4312                std::slice::from_raw_parts(string.cast::<u8>(), length as usize)
4313            }
4314        };
4315
4316        let encoded = percent_encode(input);
4317
4318        std::ffi::CString::new(encoded).map_or(ptr::null_mut(), std::ffi::CString::into_raw)
4319    }));
4320    result.unwrap_or(ptr::null_mut())
4321}
4322
4323/// `curl_unescape` — URL-decode a string.
4324///
4325/// Returns a newly allocated string that must be freed with `curl_free`.
4326/// If `length` is 0, the string is treated as null-terminated.
4327///
4328/// # Safety
4329///
4330/// `string` must be a valid pointer to at least `length` bytes.
4331/// If `length` is 0, `string` must be null-terminated.
4332/// If `outlength` is non-null, it receives the length of the decoded string.
4333#[no_mangle]
4334pub unsafe extern "C" fn curl_unescape(
4335    string: *const c_char,
4336    length: c_long,
4337    outlength: *mut c_long,
4338) -> *mut c_char {
4339    let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
4340        if string.is_null() {
4341            return ptr::null_mut();
4342        }
4343
4344        let input = if length == 0 {
4345            // SAFETY: Caller guarantees string is null-terminated
4346            unsafe { CStr::from_ptr(string) }.to_bytes()
4347        } else {
4348            #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
4349            // SAFETY: Caller guarantees string points to at least length bytes
4350            unsafe {
4351                std::slice::from_raw_parts(string.cast::<u8>(), length as usize)
4352            }
4353        };
4354
4355        let decoded = percent_decode(input);
4356
4357        if !outlength.is_null() {
4358            // SAFETY: Caller guarantees outlength is valid
4359            unsafe {
4360                #[allow(clippy::cast_possible_wrap, clippy::cast_possible_truncation)]
4361                {
4362                    *outlength = decoded.len() as c_long;
4363                }
4364            }
4365        }
4366
4367        std::ffi::CString::new(decoded).map_or(ptr::null_mut(), std::ffi::CString::into_raw)
4368    }));
4369    result.unwrap_or(ptr::null_mut())
4370}
4371
4372/// `curl_easy_escape` — URL-encode a string using an easy handle.
4373///
4374/// The easy handle parameter is accepted for API compatibility but not used.
4375/// Returns a newly allocated string that must be freed with `curl_free`.
4376///
4377/// # Safety
4378///
4379/// `_handle` can be null (not used). `string` must be valid.
4380/// If `length` is 0, the string is treated as null-terminated.
4381#[no_mangle]
4382pub unsafe extern "C" fn curl_easy_escape(
4383    _handle: *mut c_void,
4384    string: *const c_char,
4385    length: c_long,
4386) -> *mut c_char {
4387    let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
4388        // SAFETY: Delegates to curl_escape with same safety requirements
4389        unsafe { curl_escape(string, length) }
4390    }));
4391    result.unwrap_or(ptr::null_mut())
4392}
4393
4394/// `curl_easy_unescape` — URL-decode a string using an easy handle.
4395///
4396/// The easy handle parameter is accepted for API compatibility but not used.
4397/// Returns a newly allocated string that must be freed with `curl_free`.
4398///
4399/// # Safety
4400///
4401/// `_handle` can be null (not used). `string` must be valid.
4402/// If `inlength` is 0, the string is treated as null-terminated.
4403/// `outlength` receives the decoded length (can be null).
4404#[no_mangle]
4405pub unsafe extern "C" fn curl_easy_unescape(
4406    _handle: *mut c_void,
4407    string: *const c_char,
4408    inlength: c_long,
4409    outlength: *mut c_long,
4410) -> *mut c_char {
4411    let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
4412        // SAFETY: Delegates to curl_unescape with same safety requirements
4413        unsafe { curl_unescape(string, inlength, outlength) }
4414    }));
4415    result.unwrap_or(ptr::null_mut())
4416}
4417
4418/// `curl_getdate` — parse a date string to a Unix timestamp.
4419///
4420/// Parses RFC 2822, RFC 850, and asctime date formats.
4421/// Returns the number of seconds since the Unix epoch, or -1 on failure.
4422///
4423/// # Safety
4424///
4425/// `datestring` must be a valid null-terminated C string.
4426/// `now` is unused (accepted for API compatibility, can be null).
4427#[no_mangle]
4428pub unsafe extern "C" fn curl_getdate(datestring: *const c_char, _now: *const c_void) -> i64 {
4429    let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
4430        if datestring.is_null() {
4431            return -1;
4432        }
4433
4434        // SAFETY: Caller guarantees datestring is null-terminated
4435        let Ok(s) = unsafe { CStr::from_ptr(datestring) }.to_str() else {
4436            return -1;
4437        };
4438
4439        parse_http_date(s).unwrap_or(-1)
4440    }));
4441    result.unwrap_or(-1)
4442}
4443
4444/// `curl_formadd` — deprecated multipart form API.
4445///
4446/// This function is deprecated in libcurl in favor of the MIME API.
4447/// Returns `CURL_FORMADD_DISABLED` (7) to indicate it's not supported.
4448///
4449/// # Safety
4450///
4451/// Arguments are ignored. Always returns disabled.
4452#[no_mangle]
4453#[allow(clippy::missing_const_for_fn)]
4454pub unsafe extern "C" fn curl_formadd(_first: *mut *mut c_void, _last: *mut *mut c_void) -> c_long {
4455    let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
4456        7 // CURL_FORMADD_DISABLED
4457    }));
4458    result.unwrap_or(7) // CURL_FORMADD_DISABLED
4459}
4460
4461/// `curl_formfree` — free a form created by `curl_formadd`.
4462///
4463/// Since `curl_formadd` always returns disabled, this is a no-op.
4464///
4465/// # Safety
4466///
4467/// `form` can be any pointer (ignored).
4468#[no_mangle]
4469#[allow(clippy::missing_const_for_fn)]
4470pub unsafe extern "C" fn curl_formfree(_form: *mut c_void) {
4471    let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
4472        // No-op: curl_formadd is disabled
4473    }));
4474}
4475
4476/// Percent-encode bytes for URL escaping.
4477fn percent_encode(input: &[u8]) -> String {
4478    let mut result = String::with_capacity(input.len());
4479    for &byte in input {
4480        if byte.is_ascii_alphanumeric()
4481            || byte == b'-'
4482            || byte == b'_'
4483            || byte == b'.'
4484            || byte == b'~'
4485        {
4486            result.push(char::from(byte));
4487        } else {
4488            result.push('%');
4489            result.push(char::from(HEX_UPPER[usize::from(byte >> 4)]));
4490            result.push(char::from(HEX_UPPER[usize::from(byte & 0x0F)]));
4491        }
4492    }
4493    result
4494}
4495
4496/// Upper-case hex digits for percent-encoding.
4497const HEX_UPPER: [u8; 16] = *b"0123456789ABCDEF";
4498
4499/// Percent-decode bytes from URL escaping.
4500fn percent_decode(input: &[u8]) -> Vec<u8> {
4501    let mut result = Vec::with_capacity(input.len());
4502    let mut i = 0;
4503    while i < input.len() {
4504        if input[i] == b'%' && i + 2 < input.len() {
4505            if let (Some(hi), Some(lo)) = (hex_val(input[i + 1]), hex_val(input[i + 2])) {
4506                result.push(hi << 4 | lo);
4507                i += 3;
4508                continue;
4509            }
4510        } else if input[i] == b'+' {
4511            result.push(b' ');
4512            i += 1;
4513            continue;
4514        }
4515        result.push(input[i]);
4516        i += 1;
4517    }
4518    result
4519}
4520
4521/// Convert a hex ASCII digit to its numeric value.
4522const fn hex_val(byte: u8) -> Option<u8> {
4523    match byte {
4524        b'0'..=b'9' => Some(byte - b'0'),
4525        b'a'..=b'f' => Some(byte - b'a' + 10),
4526        b'A'..=b'F' => Some(byte - b'A' + 10),
4527        _ => None,
4528    }
4529}
4530
4531/// Parse an HTTP date string to Unix timestamp.
4532///
4533/// Supports:
4534/// - RFC 2822: "Sun, 06 Nov 1994 08:49:37 GMT"
4535/// - RFC 850: "Sunday, 06-Nov-94 08:49:37 GMT"
4536/// - asctime: "Sun Nov  6 08:49:37 1994"
4537fn parse_http_date(s: &str) -> Option<i64> {
4538    let s = s.trim();
4539
4540    // Try RFC 2822 / RFC 1123: "Sun, 06 Nov 1994 08:49:37 GMT"
4541    if let Some(ts) = parse_rfc2822(s) {
4542        return Some(ts);
4543    }
4544
4545    // Try RFC 850: "Sunday, 06-Nov-94 08:49:37 GMT"
4546    if let Some(ts) = parse_rfc850(s) {
4547        return Some(ts);
4548    }
4549
4550    // Try asctime: "Sun Nov  6 08:49:37 1994"
4551    parse_asctime(s)
4552}
4553
4554/// Month name to 0-based month number.
4555fn month_from_name(name: &str) -> Option<u32> {
4556    match name {
4557        "Jan" => Some(0),
4558        "Feb" => Some(1),
4559        "Mar" => Some(2),
4560        "Apr" => Some(3),
4561        "May" => Some(4),
4562        "Jun" => Some(5),
4563        "Jul" => Some(6),
4564        "Aug" => Some(7),
4565        "Sep" => Some(8),
4566        "Oct" => Some(9),
4567        "Nov" => Some(10),
4568        "Dec" => Some(11),
4569        _ => None,
4570    }
4571}
4572
4573/// Days in each month (non-leap year).
4574const DAYS_IN_MONTH: [u32; 12] = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
4575
4576/// Check if a year is a leap year.
4577const fn is_leap_year(year: i64) -> bool {
4578    (year % 4 == 0 && year % 100 != 0) || year % 400 == 0
4579}
4580
4581/// Convert date components to Unix timestamp.
4582fn date_to_timestamp(year: i64, month: u32, day: u32, hour: u32, min: u32, sec: u32) -> i64 {
4583    // Days from epoch (1970-01-01) to start of this year
4584    let mut days: i64 = 0;
4585    if year >= 1970 {
4586        for y in 1970..year {
4587            days += if is_leap_year(y) { 366 } else { 365 };
4588        }
4589    } else {
4590        for y in year..1970 {
4591            days -= if is_leap_year(y) { 366 } else { 365 };
4592        }
4593    }
4594
4595    // Add days for completed months
4596    for m in 0..month {
4597        days += i64::from(DAYS_IN_MONTH[m as usize]);
4598        if m == 1 && is_leap_year(year) {
4599            days += 1;
4600        }
4601    }
4602
4603    // Add days (1-based)
4604    days += i64::from(day) - 1;
4605
4606    days * 86400 + i64::from(hour) * 3600 + i64::from(min) * 60 + i64::from(sec)
4607}
4608
4609/// Parse RFC 2822 date: "Sun, 06 Nov 1994 08:49:37 GMT"
4610fn parse_rfc2822(s: &str) -> Option<i64> {
4611    // Skip optional day name and comma
4612    let s = s.find(", ").map_or(s, |pos| &s[pos + 2..]);
4613    let parts: Vec<&str> = s.split_whitespace().collect();
4614    if parts.len() < 4 {
4615        return None;
4616    }
4617
4618    let day: u32 = parts[0].parse().ok()?;
4619    let month = month_from_name(parts[1])?;
4620    let year: i64 = parts[2].parse().ok()?;
4621    let time_parts: Vec<&str> = parts[3].split(':').collect();
4622    if time_parts.len() != 3 {
4623        return None;
4624    }
4625    let hour: u32 = time_parts[0].parse().ok()?;
4626    let min: u32 = time_parts[1].parse().ok()?;
4627    let sec: u32 = time_parts[2].parse().ok()?;
4628
4629    Some(date_to_timestamp(year, month, day, hour, min, sec))
4630}
4631
4632/// Parse RFC 850 date: "Sunday, 06-Nov-94 08:49:37 GMT"
4633fn parse_rfc850(s: &str) -> Option<i64> {
4634    let pos = s.find(", ")?;
4635    let s = &s[pos + 2..];
4636    let parts: Vec<&str> = s.split_whitespace().collect();
4637    if parts.len() < 2 {
4638        return None;
4639    }
4640
4641    // "06-Nov-94"
4642    let date_parts: Vec<&str> = parts[0].split('-').collect();
4643    if date_parts.len() != 3 {
4644        return None;
4645    }
4646    let day: u32 = date_parts[0].parse().ok()?;
4647    let month = month_from_name(date_parts[1])?;
4648    let mut year: i64 = date_parts[2].parse().ok()?;
4649    if year < 100 {
4650        year += if year < 70 { 2000 } else { 1900 };
4651    }
4652
4653    let time_parts: Vec<&str> = parts[1].split(':').collect();
4654    if time_parts.len() != 3 {
4655        return None;
4656    }
4657    let hour: u32 = time_parts[0].parse().ok()?;
4658    let min: u32 = time_parts[1].parse().ok()?;
4659    let sec: u32 = time_parts[2].parse().ok()?;
4660
4661    Some(date_to_timestamp(year, month, day, hour, min, sec))
4662}
4663
4664/// Parse asctime date: "Sun Nov  6 08:49:37 1994"
4665fn parse_asctime(s: &str) -> Option<i64> {
4666    let parts: Vec<&str> = s.split_whitespace().collect();
4667    if parts.len() < 5 {
4668        return None;
4669    }
4670
4671    // Skip day name (parts[0])
4672    let month = month_from_name(parts[1])?;
4673    let day: u32 = parts[2].parse().ok()?;
4674    let time_parts: Vec<&str> = parts[3].split(':').collect();
4675    if time_parts.len() != 3 {
4676        return None;
4677    }
4678    let hour: u32 = time_parts[0].parse().ok()?;
4679    let min: u32 = time_parts[1].parse().ok()?;
4680    let sec: u32 = time_parts[2].parse().ok()?;
4681    let year: i64 = parts[4].parse().ok()?;
4682
4683    Some(date_to_timestamp(year, month, day, hour, min, sec))
4684}
4685
4686// ───────────────────────── Version ─────────────────────────
4687
4688/// `curl_version` — returns the version string (libcurl compatibility).
4689///
4690/// # Safety
4691///
4692/// The returned pointer is valid for the lifetime of the program.
4693#[no_mangle]
4694#[allow(clippy::missing_const_for_fn)]
4695pub extern "C" fn curl_version() -> *const c_char {
4696    let result =
4697        std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| c"liburlx/0.1.0".as_ptr()));
4698    result.unwrap_or(c"liburlx/unknown".as_ptr())
4699}
4700
4701/// `urlx_version` — returns the version string.
4702///
4703/// # Safety
4704///
4705/// The returned pointer is valid for the lifetime of the program.
4706#[no_mangle]
4707#[allow(clippy::missing_const_for_fn)]
4708pub extern "C" fn urlx_version() -> *const c_char {
4709    let result =
4710        std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| c"liburlx/0.1.0".as_ptr()));
4711    result.unwrap_or(c"liburlx/unknown".as_ptr())
4712}
4713
4714// ───────────────────────── Global init/cleanup ─────────────────────────
4715
4716/// `curl_global_init` — global initialization (no-op in urlx).
4717///
4718/// In libcurl this initializes SSL, Win32 sockets, etc. In urlx, tokio
4719/// and rustls handle their own initialization, so this is a no-op.
4720///
4721/// # Safety
4722///
4723/// This function is always safe to call.
4724#[no_mangle]
4725#[allow(clippy::missing_const_for_fn)] // const extern "C" fn not stable
4726pub extern "C" fn curl_global_init(_flags: c_long) -> CURLcode {
4727    let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| CURLcode::CURLE_OK));
4728    result.unwrap_or(CURLcode::CURLE_UNKNOWN_OPTION)
4729}
4730
4731/// `curl_global_cleanup` — global cleanup (no-op in urlx).
4732///
4733/// # Safety
4734///
4735/// This function is always safe to call.
4736#[no_mangle]
4737#[allow(clippy::missing_const_for_fn)] // const extern "C" fn not stable
4738pub extern "C" fn curl_global_cleanup() {
4739    let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {}));
4740}
4741
4742/// Bitmask constants for `curl_global_init`.
4743/// `CURL_GLOBAL_SSL` — initialize SSL.
4744pub const CURL_GLOBAL_SSL: c_long = 1;
4745/// `CURL_GLOBAL_WIN32` — initialize Win32 sockets.
4746pub const CURL_GLOBAL_WIN32: c_long = 2;
4747/// `CURL_GLOBAL_ALL` — initialize everything.
4748pub const CURL_GLOBAL_ALL: c_long = 3;
4749/// `CURL_GLOBAL_DEFAULT` — same as ALL.
4750pub const CURL_GLOBAL_DEFAULT: c_long = 3;
4751
4752// ───────────────────────── Version info ─────────────────────────
4753
4754/// Version info struct returned by `curl_version_info`.
4755///
4756/// Matches the `curl_version_info_data` struct from libcurl.
4757/// Only the essential fields are populated.
4758#[repr(C)]
4759pub struct CurlVersionInfo {
4760    /// Age of this struct (`CURLVERSION_FIRST` = 0).
4761    pub age: c_long,
4762    /// Version string (e.g., "0.1.0").
4763    pub version: *const c_char,
4764    /// Numeric version (major*0x10000 + minor*0x100 + patch).
4765    pub version_num: c_long,
4766    /// Host system description.
4767    pub host: *const c_char,
4768    /// Feature bitmask.
4769    pub features: c_long,
4770    /// SSL version string or NULL.
4771    pub ssl_version: *const c_char,
4772    /// Unused (libssl version number).
4773    pub ssl_version_num: c_long,
4774    /// libz version string or NULL.
4775    pub libz_version: *const c_char,
4776    /// Null-terminated array of supported protocols.
4777    pub protocols: *const *const c_char,
4778}
4779
4780// SAFETY: CurlVersionInfo contains only pointers to static string literals and
4781// null pointers. These never change and are valid for the lifetime of the program.
4782unsafe impl Sync for CurlVersionInfo {}
4783
4784/// Feature bit: SSL support.
4785pub const CURL_VERSION_SSL: c_long = 1 << 2;
4786/// Feature bit: HTTP/2 support.
4787pub const CURL_VERSION_HTTP2: c_long = 1 << 16;
4788/// Feature bit: async DNS support.
4789pub const CURL_VERSION_ASYNCHDNS: c_long = 1 << 7;
4790/// Feature bit: PSL support.
4791pub const CURL_VERSION_PSL: c_long = 1 << 20;
4792
4793/// `curl_version_info` — return version info struct.
4794///
4795/// Returns a pointer to a static struct with version information.
4796/// The pointer is valid for the lifetime of the program.
4797///
4798/// # Safety
4799///
4800/// The returned pointer is valid for the lifetime of the program.
4801#[no_mangle]
4802pub extern "C" fn curl_version_info(_age: c_long) -> *const CurlVersionInfo {
4803    let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
4804        // Use Box::leak to create a 'static reference. OnceLock ensures single init.
4805        static INFO: std::sync::OnceLock<&'static CurlVersionInfo> = std::sync::OnceLock::new();
4806        let info = INFO.get_or_init(|| {
4807            // Protocols array — leaked to get a 'static pointer
4808            let protocols: &'static [*const c_char] = Box::leak(Box::new([
4809                c"http".as_ptr(),
4810                c"https".as_ptr(),
4811                c"ftp".as_ptr(),
4812                c"ftps".as_ptr(),
4813                c"sftp".as_ptr(),
4814                c"scp".as_ptr(),
4815                c"ws".as_ptr(),
4816                c"wss".as_ptr(),
4817                ptr::null(), // Null terminator
4818            ]));
4819            Box::leak(Box::new(CurlVersionInfo {
4820                age: 0,
4821                version: c"0.1.0".as_ptr(),
4822                version_num: 0x000_100, // 0.1.0
4823                host: c"urlx".as_ptr(),
4824                features: CURL_VERSION_SSL | CURL_VERSION_HTTP2 | CURL_VERSION_PSL,
4825                ssl_version: c"rustls/0.23".as_ptr(),
4826                ssl_version_num: 0,
4827                libz_version: ptr::null(),
4828                protocols: protocols.as_ptr(),
4829            }))
4830        });
4831        std::ptr::from_ref::<CurlVersionInfo>(info)
4832    }));
4833    result.unwrap_or(ptr::null::<CurlVersionInfo>())
4834}
4835
4836/// `curl_easy_pause` — pause/unpause a transfer (stub).
4837///
4838/// # Safety
4839///
4840/// `handle` must be a valid pointer from `curl_easy_init`.
4841#[no_mangle]
4842#[allow(clippy::missing_const_for_fn)] // const extern "C" fn not stable
4843pub extern "C" fn curl_easy_pause(_handle: *mut c_void, _bitmask: c_long) -> CURLcode {
4844    let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
4845        // Pause/unpause is not yet implemented; return OK as a no-op
4846        CURLcode::CURLE_OK
4847    }));
4848    result.unwrap_or(CURLcode::CURLE_UNKNOWN_OPTION)
4849}
4850
4851/// Pause direction constants.
4852/// `CURLPAUSE_RECV` — pause receiving.
4853pub const CURLPAUSE_RECV: c_long = 1;
4854/// `CURLPAUSE_SEND` — pause sending.
4855pub const CURLPAUSE_SEND: c_long = 4;
4856/// `CURLPAUSE_ALL` — pause both directions.
4857pub const CURLPAUSE_ALL: c_long = 5;
4858/// `CURLPAUSE_CONT` — unpause both directions.
4859pub const CURLPAUSE_CONT: c_long = 0;
4860
4861/// `curl_easy_upkeep` — perform connection upkeep (no-op).
4862///
4863/// # Safety
4864///
4865/// `handle` must be a valid pointer from `curl_easy_init`.
4866#[no_mangle]
4867#[allow(clippy::missing_const_for_fn)] // const extern "C" fn not stable
4868pub extern "C" fn curl_easy_upkeep(_handle: *mut c_void) -> CURLcode {
4869    let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| CURLcode::CURLE_OK));
4870    result.unwrap_or(CURLcode::CURLE_UNKNOWN_OPTION)
4871}
4872
4873/// `curl_multi_assign` — assign custom pointer to socket (no-op stub).
4874///
4875/// # Safety
4876///
4877/// `multi_handle` must be a valid pointer from `curl_multi_init`.
4878#[no_mangle]
4879#[allow(clippy::missing_const_for_fn)] // const extern "C" fn not stable
4880pub extern "C" fn curl_multi_assign(
4881    _multi_handle: *mut c_void,
4882    _sockfd: c_long,
4883    _sockp: *mut c_void,
4884) -> CURLMcode {
4885    let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| CURLMcode::CURLM_OK));
4886    result.unwrap_or(CURLMcode::CURLM_INTERNAL_ERROR)
4887}
4888
4889// ───────────────────────── Error mapping ─────────────────────────
4890
4891/// Convert a liburlx error to a `CURLcode`.
4892fn error_to_curlcode(err: &liburlx::Error) -> CURLcode {
4893    match err {
4894        liburlx::Error::UrlParse(_) => CURLcode::CURLE_URL_MALFORMAT,
4895        liburlx::Error::Connect(_) => CURLcode::CURLE_COULDNT_CONNECT,
4896        liburlx::Error::Tls(_) => CURLcode::CURLE_SSL_CONNECT_ERROR,
4897        liburlx::Error::Http(msg) => {
4898            if msg.contains("unsupported scheme") {
4899                CURLcode::CURLE_UNSUPPORTED_PROTOCOL
4900            } else if msg.contains("resolve") || msg.contains("DNS") {
4901                CURLcode::CURLE_COULDNT_RESOLVE_HOST
4902            } else if msg.contains("HTTP error") && msg.contains("fail_on_error") {
4903                CURLcode::CURLE_HTTP_RETURNED_ERROR
4904            } else if msg.contains("aborted by") || msg.contains("callback") {
4905                CURLcode::CURLE_ABORTED_BY_CALLBACK
4906            } else if msg.contains("FTP") {
4907                CURLcode::CURLE_FTP_WEIRD_SERVER_REPLY
4908            } else {
4909                CURLcode::CURLE_RECV_ERROR
4910            }
4911        }
4912        liburlx::Error::Timeout(_) | liburlx::Error::SpeedLimit { .. } => {
4913            CURLcode::CURLE_OPERATION_TIMEDOUT
4914        }
4915        liburlx::Error::RtspCseqError(_) => CURLcode::CURLE_RTSP_CSEQ_ERROR,
4916        liburlx::Error::RtspSessionError(_) => CURLcode::CURLE_RTSP_SESSION_ERROR,
4917        liburlx::Error::Transfer { code, .. } => match *code {
4918            8 => CURLcode::CURLE_FTP_WEIRD_SERVER_REPLY,
4919            43 => CURLcode::CURLE_BAD_FUNCTION_ARGUMENT,
4920            85 => CURLcode::CURLE_RTSP_CSEQ_ERROR,
4921            86 => CURLcode::CURLE_RTSP_SESSION_ERROR,
4922            _ => CURLcode::CURLE_RECV_ERROR,
4923        },
4924        _ => CURLcode::CURLE_RECV_ERROR,
4925    }
4926}
4927
4928// ───────────────────────── Tests ─────────────────────────
4929
4930#[cfg(test)]
4931#[allow(clippy::unwrap_used)]
4932mod tests {
4933    use super::*;
4934
4935    #[test]
4936    fn version_returns_non_null() {
4937        let ptr = urlx_version();
4938        assert!(!ptr.is_null());
4939    }
4940
4941    #[test]
4942    fn curl_version_returns_non_null() {
4943        let ptr = curl_version();
4944        assert!(!ptr.is_null());
4945    }
4946
4947    #[test]
4948    fn easy_init_cleanup() {
4949        let handle = curl_easy_init();
4950        assert!(!handle.is_null());
4951        unsafe { curl_easy_cleanup(handle) };
4952    }
4953
4954    #[test]
4955    fn easy_cleanup_null_is_safe() {
4956        unsafe { curl_easy_cleanup(ptr::null_mut()) };
4957    }
4958
4959    #[test]
4960    fn easy_setopt_url() {
4961        let handle = curl_easy_init();
4962        let url = c"http://example.com";
4963        let code = unsafe { curl_easy_setopt(handle, 10002, url.as_ptr().cast::<c_void>()) };
4964        assert_eq!(code, CURLcode::CURLE_OK);
4965        unsafe { curl_easy_cleanup(handle) };
4966    }
4967
4968    #[test]
4969    fn easy_setopt_invalid_url() {
4970        let handle = curl_easy_init();
4971        let url = c"";
4972        let code = unsafe { curl_easy_setopt(handle, 10002, url.as_ptr().cast::<c_void>()) };
4973        assert_eq!(code, CURLcode::CURLE_URL_MALFORMAT);
4974        unsafe { curl_easy_cleanup(handle) };
4975    }
4976
4977    #[test]
4978    fn easy_setopt_null_url() {
4979        let handle = curl_easy_init();
4980        let code = unsafe { curl_easy_setopt(handle, 10002, ptr::null()) };
4981        assert_eq!(code, CURLcode::CURLE_URL_MALFORMAT);
4982        unsafe { curl_easy_cleanup(handle) };
4983    }
4984
4985    #[test]
4986    fn easy_setopt_verbose() {
4987        let handle = curl_easy_init();
4988        let code = unsafe { curl_easy_setopt(handle, 41, std::ptr::dangling::<c_void>()) };
4989        assert_eq!(code, CURLcode::CURLE_OK);
4990        unsafe { curl_easy_cleanup(handle) };
4991    }
4992
4993    #[test]
4994    fn easy_setopt_follow_redirects() {
4995        let handle = curl_easy_init();
4996        let code = unsafe { curl_easy_setopt(handle, 52, std::ptr::dangling::<c_void>()) };
4997        assert_eq!(code, CURLcode::CURLE_OK);
4998        unsafe { curl_easy_cleanup(handle) };
4999    }
5000
5001    #[test]
5002    fn easy_setopt_timeout() {
5003        let handle = curl_easy_init();
5004        let code = unsafe { curl_easy_setopt(handle, 13, 30 as *const c_void) };
5005        assert_eq!(code, CURLcode::CURLE_OK);
5006        unsafe { curl_easy_cleanup(handle) };
5007    }
5008
5009    #[test]
5010    fn easy_setopt_unknown_option() {
5011        let handle = curl_easy_init();
5012        let code = unsafe { curl_easy_setopt(handle, 99999, ptr::null()) };
5013        assert_eq!(code, CURLcode::CURLE_UNKNOWN_OPTION);
5014        unsafe { curl_easy_cleanup(handle) };
5015    }
5016
5017    #[test]
5018    fn easy_setopt_null_handle() {
5019        let code = unsafe { curl_easy_setopt(ptr::null_mut(), 10002, ptr::null()) };
5020        assert_eq!(code, CURLcode::CURLE_FAILED_INIT);
5021    }
5022
5023    #[test]
5024    fn easy_perform_without_url() {
5025        let handle = curl_easy_init();
5026        let code = unsafe { curl_easy_perform(handle) };
5027        assert_ne!(code, CURLcode::CURLE_OK);
5028        unsafe { curl_easy_cleanup(handle) };
5029    }
5030
5031    #[test]
5032    fn easy_perform_null_handle() {
5033        let code = unsafe { curl_easy_perform(ptr::null_mut()) };
5034        assert_eq!(code, CURLcode::CURLE_FAILED_INIT);
5035    }
5036
5037    #[test]
5038    fn easy_getinfo_null_handle() {
5039        let mut code: c_long = 0;
5040        let out_ptr = ptr::from_mut(&mut code).cast::<c_void>();
5041        let result = unsafe { curl_easy_getinfo(ptr::null_mut(), 0x20_0002, out_ptr) };
5042        assert_eq!(result, CURLcode::CURLE_FAILED_INIT);
5043    }
5044
5045    #[test]
5046    fn easy_getinfo_no_response() {
5047        let handle = curl_easy_init();
5048        let url = c"http://example.com";
5049        let _code = unsafe { curl_easy_setopt(handle, 10002, url.as_ptr().cast::<c_void>()) };
5050
5051        let mut code: c_long = 0;
5052        let out_ptr = ptr::from_mut(&mut code).cast::<c_void>();
5053        let result = unsafe { curl_easy_getinfo(handle, 0x20_0002, out_ptr) };
5054        assert_eq!(result, CURLcode::CURLE_GOT_NOTHING);
5055        unsafe { curl_easy_cleanup(handle) };
5056    }
5057
5058    #[test]
5059    fn easy_strerror_ok() {
5060        let msg = curl_easy_strerror(CURLcode::CURLE_OK);
5061        assert!(!msg.is_null());
5062        let s = unsafe { CStr::from_ptr(msg) };
5063        assert_eq!(s.to_str().unwrap(), "No error");
5064    }
5065
5066    #[test]
5067    fn easy_strerror_timeout() {
5068        let msg = curl_easy_strerror(CURLcode::CURLE_OPERATION_TIMEDOUT);
5069        let s = unsafe { CStr::from_ptr(msg) };
5070        assert_eq!(s.to_str().unwrap(), "Operation timed out");
5071    }
5072
5073    #[test]
5074    fn easy_strerror_all_codes() {
5075        // Ensure every error code has a message
5076        let codes = [
5077            CURLcode::CURLE_OK,
5078            CURLcode::CURLE_UNSUPPORTED_PROTOCOL,
5079            CURLcode::CURLE_FAILED_INIT,
5080            CURLcode::CURLE_URL_MALFORMAT,
5081            CURLcode::CURLE_COULDNT_RESOLVE_PROXY,
5082            CURLcode::CURLE_COULDNT_RESOLVE_HOST,
5083            CURLcode::CURLE_COULDNT_CONNECT,
5084            CURLcode::CURLE_FTP_WEIRD_SERVER_REPLY,
5085            CURLcode::CURLE_REMOTE_ACCESS_DENIED,
5086            CURLcode::CURLE_HTTP2,
5087            CURLcode::CURLE_HTTP_RETURNED_ERROR,
5088            CURLcode::CURLE_WRITE_ERROR,
5089            CURLcode::CURLE_READ_ERROR,
5090            CURLcode::CURLE_OUT_OF_MEMORY,
5091            CURLcode::CURLE_OPERATION_TIMEDOUT,
5092            CURLcode::CURLE_SSL_CONNECT_ERROR,
5093            CURLcode::CURLE_ABORTED_BY_CALLBACK,
5094            CURLcode::CURLE_BAD_FUNCTION_ARGUMENT,
5095            CURLcode::CURLE_UNKNOWN_OPTION,
5096            CURLcode::CURLE_GOT_NOTHING,
5097            CURLcode::CURLE_SEND_ERROR,
5098            CURLcode::CURLE_RECV_ERROR,
5099            CURLcode::CURLE_SSL_CERTPROBLEM,
5100            CURLcode::CURLE_PEER_FAILED_VERIFICATION,
5101            CURLcode::CURLE_LOGIN_DENIED,
5102        ];
5103        for code in codes {
5104            let msg = curl_easy_strerror(code);
5105            assert!(!msg.is_null(), "strerror returned null for {code:?}");
5106        }
5107    }
5108
5109    #[test]
5110    fn easy_setopt_custom_request() {
5111        let handle = curl_easy_init();
5112        let method = c"DELETE";
5113        let code = unsafe { curl_easy_setopt(handle, 10036, method.as_ptr().cast::<c_void>()) };
5114        assert_eq!(code, CURLcode::CURLE_OK);
5115        unsafe { curl_easy_cleanup(handle) };
5116    }
5117
5118    #[test]
5119    fn easy_setopt_nobody() {
5120        let handle = curl_easy_init();
5121        let code = unsafe { curl_easy_setopt(handle, 44, std::ptr::dangling::<c_void>()) };
5122        assert_eq!(code, CURLcode::CURLE_OK);
5123        unsafe { curl_easy_cleanup(handle) };
5124    }
5125
5126    #[test]
5127    fn easy_setopt_postfields() {
5128        let handle = curl_easy_init();
5129        let data = c"key=value";
5130        let code = unsafe { curl_easy_setopt(handle, 10015, data.as_ptr().cast::<c_void>()) };
5131        assert_eq!(code, CURLcode::CURLE_OK);
5132        unsafe { curl_easy_cleanup(handle) };
5133    }
5134
5135    #[test]
5136    fn easy_setopt_proxy() {
5137        let handle = curl_easy_init();
5138        let proxy = c"http://proxy:8080";
5139        let code = unsafe { curl_easy_setopt(handle, 10004, proxy.as_ptr().cast::<c_void>()) };
5140        assert_eq!(code, CURLcode::CURLE_OK);
5141        unsafe { curl_easy_cleanup(handle) };
5142    }
5143
5144    #[test]
5145    fn easy_setopt_userpwd() {
5146        let handle = curl_easy_init();
5147        let up = c"user:pass";
5148        let code = unsafe { curl_easy_setopt(handle, 10005, up.as_ptr().cast::<c_void>()) };
5149        assert_eq!(code, CURLcode::CURLE_OK);
5150        unsafe { curl_easy_cleanup(handle) };
5151    }
5152
5153    #[test]
5154    fn easy_setopt_ssl_verify() {
5155        let handle = curl_easy_init();
5156        // SSL_VERIFYPEER = 64
5157        let code = unsafe { curl_easy_setopt(handle, 64, ptr::null()) };
5158        assert_eq!(code, CURLcode::CURLE_OK);
5159        // SSL_VERIFYHOST = 81
5160        let code = unsafe { curl_easy_setopt(handle, 81, 2 as *const c_void) };
5161        assert_eq!(code, CURLcode::CURLE_OK);
5162        unsafe { curl_easy_cleanup(handle) };
5163    }
5164
5165    #[test]
5166    fn easy_setopt_tcp_options() {
5167        let handle = curl_easy_init();
5168        // TCP_NODELAY = 121
5169        let code = unsafe { curl_easy_setopt(handle, 121, std::ptr::dangling::<c_void>()) };
5170        assert_eq!(code, CURLcode::CURLE_OK);
5171        // TCP_KEEPALIVE = 213
5172        let code = unsafe { curl_easy_setopt(handle, 213, std::ptr::dangling::<c_void>()) };
5173        assert_eq!(code, CURLcode::CURLE_OK);
5174        unsafe { curl_easy_cleanup(handle) };
5175    }
5176
5177    #[test]
5178    fn easy_setopt_fail_on_error() {
5179        let handle = curl_easy_init();
5180        let code = unsafe { curl_easy_setopt(handle, 45, std::ptr::dangling::<c_void>()) };
5181        assert_eq!(code, CURLcode::CURLE_OK);
5182        unsafe { curl_easy_cleanup(handle) };
5183    }
5184
5185    #[test]
5186    fn easy_setopt_accept_encoding() {
5187        let handle = curl_easy_init();
5188        let enc = c"gzip, deflate";
5189        let code = unsafe { curl_easy_setopt(handle, 10102, enc.as_ptr().cast::<c_void>()) };
5190        assert_eq!(code, CURLcode::CURLE_OK);
5191        unsafe { curl_easy_cleanup(handle) };
5192    }
5193
5194    #[test]
5195    fn easy_setopt_range() {
5196        let handle = curl_easy_init();
5197        let range = c"0-99";
5198        let code = unsafe { curl_easy_setopt(handle, 10007, range.as_ptr().cast::<c_void>()) };
5199        assert_eq!(code, CURLcode::CURLE_OK);
5200        unsafe { curl_easy_cleanup(handle) };
5201    }
5202
5203    #[test]
5204    fn easy_setopt_httpget() {
5205        let handle = curl_easy_init();
5206        let code = unsafe { curl_easy_setopt(handle, 80, std::ptr::dangling::<c_void>()) };
5207        assert_eq!(code, CURLcode::CURLE_OK);
5208        unsafe { curl_easy_cleanup(handle) };
5209    }
5210
5211    #[test]
5212    fn easy_setopt_upload() {
5213        let handle = curl_easy_init();
5214        let code = unsafe { curl_easy_setopt(handle, 46, std::ptr::dangling::<c_void>()) };
5215        assert_eq!(code, CURLcode::CURLE_OK);
5216        unsafe { curl_easy_cleanup(handle) };
5217    }
5218
5219    #[test]
5220    fn easy_setopt_sslversion() {
5221        let handle = curl_easy_init();
5222        // TLSv1.2 = 6
5223        let code = unsafe { curl_easy_setopt(handle, 32, 6 as *const c_void) };
5224        assert_eq!(code, CURLcode::CURLE_OK);
5225        unsafe { curl_easy_cleanup(handle) };
5226    }
5227
5228    #[test]
5229    fn easy_duphandle() {
5230        let handle = curl_easy_init();
5231        let url = c"http://example.com";
5232        let _ = unsafe { curl_easy_setopt(handle, 10002, url.as_ptr().cast::<c_void>()) };
5233
5234        let dup = unsafe { curl_easy_duphandle(handle) };
5235        assert!(!dup.is_null());
5236        assert_ne!(dup, handle);
5237
5238        unsafe {
5239            curl_easy_cleanup(dup);
5240            curl_easy_cleanup(handle);
5241        }
5242    }
5243
5244    #[test]
5245    fn easy_duphandle_null() {
5246        let dup = unsafe { curl_easy_duphandle(ptr::null_mut()) };
5247        assert!(dup.is_null());
5248    }
5249
5250    #[test]
5251    fn easy_reset() {
5252        let handle = curl_easy_init();
5253        let url = c"http://example.com";
5254        let _ = unsafe { curl_easy_setopt(handle, 10002, url.as_ptr().cast::<c_void>()) };
5255        unsafe { curl_easy_reset(handle) };
5256        // After reset, perform should fail (no URL)
5257        let code = unsafe { curl_easy_perform(handle) };
5258        assert_ne!(code, CURLcode::CURLE_OK);
5259        unsafe { curl_easy_cleanup(handle) };
5260    }
5261
5262    #[test]
5263    fn slist_append_and_free() {
5264        let list = unsafe { curl_slist_append(ptr::null_mut(), c"Header1: value1".as_ptr()) };
5265        assert!(!list.is_null());
5266
5267        let list = unsafe { curl_slist_append(list, c"Header2: value2".as_ptr()) };
5268        assert!(!list.is_null());
5269
5270        // Verify first node
5271        let first = unsafe { CStr::from_ptr((*list).data) };
5272        assert_eq!(first.to_str().unwrap(), "Header1: value1");
5273
5274        // Verify second node
5275        let second = unsafe { CStr::from_ptr((*(*list).next).data) };
5276        assert_eq!(second.to_str().unwrap(), "Header2: value2");
5277
5278        unsafe { curl_slist_free_all(list) };
5279    }
5280
5281    #[test]
5282    fn slist_free_null_is_safe() {
5283        unsafe { curl_slist_free_all(ptr::null_mut()) };
5284    }
5285
5286    #[test]
5287    fn slist_append_null_data() {
5288        let list = unsafe { curl_slist_append(ptr::null_mut(), ptr::null()) };
5289        assert!(list.is_null());
5290    }
5291
5292    #[test]
5293    fn multi_init_cleanup() {
5294        let handle = curl_multi_init();
5295        assert!(!handle.is_null());
5296        let code = unsafe { curl_multi_cleanup(handle) };
5297        assert_eq!(code, CURLMcode::CURLM_OK);
5298    }
5299
5300    #[test]
5301    fn multi_cleanup_null() {
5302        let code = unsafe { curl_multi_cleanup(ptr::null_mut()) };
5303        assert_eq!(code, CURLMcode::CURLM_BAD_HANDLE);
5304    }
5305
5306    #[test]
5307    fn multi_add_remove_handle() {
5308        let multi = curl_multi_init();
5309        let easy = curl_easy_init();
5310
5311        let code = unsafe { curl_multi_add_handle(multi, easy) };
5312        assert_eq!(code, CURLMcode::CURLM_OK);
5313
5314        let code = unsafe { curl_multi_remove_handle(multi, easy) };
5315        assert_eq!(code, CURLMcode::CURLM_OK);
5316
5317        unsafe {
5318            curl_easy_cleanup(easy);
5319            let _ = curl_multi_cleanup(multi);
5320        }
5321    }
5322
5323    #[test]
5324    fn multi_add_null_handles() {
5325        let multi = curl_multi_init();
5326        assert_eq!(
5327            unsafe { curl_multi_add_handle(ptr::null_mut(), ptr::null_mut()) },
5328            CURLMcode::CURLM_BAD_HANDLE
5329        );
5330        assert_eq!(
5331            unsafe { curl_multi_add_handle(multi, ptr::null_mut()) },
5332            CURLMcode::CURLM_BAD_EASY_HANDLE
5333        );
5334        let _ = unsafe { curl_multi_cleanup(multi) };
5335    }
5336
5337    #[test]
5338    fn multi_remove_nonexistent() {
5339        let multi = curl_multi_init();
5340        let easy = curl_easy_init();
5341
5342        let code = unsafe { curl_multi_remove_handle(multi, easy) };
5343        assert_eq!(code, CURLMcode::CURLM_BAD_EASY_HANDLE);
5344
5345        unsafe {
5346            curl_easy_cleanup(easy);
5347            let _ = curl_multi_cleanup(multi);
5348        }
5349    }
5350
5351    #[test]
5352    fn error_code_mapping() {
5353        assert_eq!(
5354            error_to_curlcode(&liburlx::Error::UrlParse("bad".to_string())),
5355            CURLcode::CURLE_URL_MALFORMAT
5356        );
5357        assert_eq!(
5358            error_to_curlcode(&liburlx::Error::Timeout(std::time::Duration::from_secs(1))),
5359            CURLcode::CURLE_OPERATION_TIMEDOUT
5360        );
5361    }
5362
5363    #[test]
5364    fn error_code_http_returned_error() {
5365        assert_eq!(
5366            error_to_curlcode(&liburlx::Error::Http(
5367                "HTTP error 404 (fail_on_error enabled)".to_string()
5368            )),
5369            CURLcode::CURLE_HTTP_RETURNED_ERROR
5370        );
5371    }
5372
5373    #[test]
5374    fn error_code_aborted_by_callback() {
5375        assert_eq!(
5376            error_to_curlcode(&liburlx::Error::Http(
5377                "transfer aborted by progress callback".to_string()
5378            )),
5379            CURLcode::CURLE_ABORTED_BY_CALLBACK
5380        );
5381    }
5382
5383    #[test]
5384    fn error_code_ftp() {
5385        assert_eq!(
5386            error_to_curlcode(&liburlx::Error::Http("FTP protocol error".to_string())),
5387            CURLcode::CURLE_FTP_WEIRD_SERVER_REPLY
5388        );
5389    }
5390
5391    #[test]
5392    fn easy_setopt_httpheader_with_slist() {
5393        let handle = curl_easy_init();
5394        let list = unsafe { curl_slist_append(ptr::null_mut(), c"X-Custom: test".as_ptr()) };
5395        let code = unsafe { curl_easy_setopt(handle, 10023, list.cast::<c_void>()) };
5396        assert_eq!(code, CURLcode::CURLE_OK);
5397        unsafe {
5398            curl_slist_free_all(list);
5399            curl_easy_cleanup(handle);
5400        }
5401    }
5402
5403    #[test]
5404    fn easy_setopt_timeout_ms() {
5405        let handle = curl_easy_init();
5406        // CURLOPT_TIMEOUT_MS = 155
5407        let code = unsafe { curl_easy_setopt(handle, 155, 5000_usize as *const c_void) };
5408        assert_eq!(code, CURLcode::CURLE_OK);
5409        unsafe { curl_easy_cleanup(handle) };
5410    }
5411
5412    #[test]
5413    fn easy_setopt_connecttimeout_ms() {
5414        let handle = curl_easy_init();
5415        // CURLOPT_CONNECTTIMEOUT_MS = 156
5416        let code = unsafe { curl_easy_setopt(handle, 156, 3000_usize as *const c_void) };
5417        assert_eq!(code, CURLcode::CURLE_OK);
5418        unsafe { curl_easy_cleanup(handle) };
5419    }
5420
5421    #[test]
5422    fn easy_setopt_fresh_connect() {
5423        let handle = curl_easy_init();
5424        // CURLOPT_FRESH_CONNECT = 74
5425        let code = unsafe { curl_easy_setopt(handle, 74, std::ptr::dangling::<c_void>()) };
5426        assert_eq!(code, CURLcode::CURLE_OK);
5427        unsafe { curl_easy_cleanup(handle) };
5428    }
5429
5430    #[test]
5431    fn easy_setopt_forbid_reuse() {
5432        let handle = curl_easy_init();
5433        // CURLOPT_FORBID_REUSE = 75
5434        let code = unsafe { curl_easy_setopt(handle, 75, std::ptr::dangling::<c_void>()) };
5435        assert_eq!(code, CURLcode::CURLE_OK);
5436        unsafe { curl_easy_cleanup(handle) };
5437    }
5438
5439    #[test]
5440    fn easy_setopt_low_speed_limit() {
5441        let handle = curl_easy_init();
5442        // CURLOPT_LOW_SPEED_LIMIT = 19
5443        let code = unsafe { curl_easy_setopt(handle, 19, 1000_usize as *const c_void) };
5444        assert_eq!(code, CURLcode::CURLE_OK);
5445        unsafe { curl_easy_cleanup(handle) };
5446    }
5447
5448    #[test]
5449    fn easy_setopt_low_speed_time() {
5450        let handle = curl_easy_init();
5451        // CURLOPT_LOW_SPEED_TIME = 20
5452        let code = unsafe { curl_easy_setopt(handle, 20, 30_usize as *const c_void) };
5453        assert_eq!(code, CURLcode::CURLE_OK);
5454        unsafe { curl_easy_cleanup(handle) };
5455    }
5456
5457    #[test]
5458    fn easy_setopt_max_send_speed() {
5459        let handle = curl_easy_init();
5460        // CURLOPT_MAX_SEND_SPEED_LARGE = 30145
5461        let code = unsafe { curl_easy_setopt(handle, 30145, 1024_usize as *const c_void) };
5462        assert_eq!(code, CURLcode::CURLE_OK);
5463        unsafe { curl_easy_cleanup(handle) };
5464    }
5465
5466    #[test]
5467    fn easy_setopt_max_recv_speed() {
5468        let handle = curl_easy_init();
5469        // CURLOPT_MAX_RECV_SPEED_LARGE = 30146
5470        let code = unsafe { curl_easy_setopt(handle, 30146, 2048_usize as *const c_void) };
5471        assert_eq!(code, CURLcode::CURLE_OK);
5472        unsafe { curl_easy_cleanup(handle) };
5473    }
5474
5475    #[test]
5476    fn easy_setopt_ssl_cipher_list() {
5477        let handle = curl_easy_init();
5478        let ciphers = c"HIGH:!aNULL:!MD5";
5479        let code = unsafe { curl_easy_setopt(handle, 10083, ciphers.as_ptr().cast::<c_void>()) };
5480        assert_eq!(code, CURLcode::CURLE_OK);
5481        unsafe { curl_easy_cleanup(handle) };
5482    }
5483
5484    #[test]
5485    fn easy_setopt_cookiefile() {
5486        let handle = curl_easy_init();
5487        let path = c"/tmp/cookies.txt";
5488        let code = unsafe { curl_easy_setopt(handle, 10031, path.as_ptr().cast::<c_void>()) };
5489        assert_eq!(code, CURLcode::CURLE_OK);
5490        unsafe { curl_easy_cleanup(handle) };
5491    }
5492
5493    #[test]
5494    fn easy_setopt_cookiefile_null_enables_engine() {
5495        let handle = curl_easy_init();
5496        // NULL enables the cookie engine
5497        let code = unsafe { curl_easy_setopt(handle, 10031, ptr::null()) };
5498        assert_eq!(code, CURLcode::CURLE_OK);
5499        unsafe { curl_easy_cleanup(handle) };
5500    }
5501
5502    #[test]
5503    fn easy_setopt_cookiejar() {
5504        let handle = curl_easy_init();
5505        let path = c"/tmp/cookies_out.txt";
5506        let code = unsafe { curl_easy_setopt(handle, 10082, path.as_ptr().cast::<c_void>()) };
5507        assert_eq!(code, CURLcode::CURLE_OK);
5508        unsafe { curl_easy_cleanup(handle) };
5509    }
5510
5511    #[test]
5512    fn easy_setopt_ssl_sessionid_cache() {
5513        let handle = curl_easy_init();
5514        // CURLOPT_SSL_SESSIONID_CACHE = 150
5515        let code = unsafe { curl_easy_setopt(handle, 150, std::ptr::dangling::<c_void>()) };
5516        assert_eq!(code, CURLcode::CURLE_OK);
5517        // Disable it
5518        let code = unsafe { curl_easy_setopt(handle, 150, ptr::null()) };
5519        assert_eq!(code, CURLcode::CURLE_OK);
5520        unsafe { curl_easy_cleanup(handle) };
5521    }
5522
5523    #[test]
5524    fn easy_setopt_interface() {
5525        let handle = curl_easy_init();
5526        let iface = c"lo0";
5527        let code = unsafe { curl_easy_setopt(handle, 10062, iface.as_ptr().cast::<c_void>()) };
5528        assert_eq!(code, CURLcode::CURLE_OK);
5529        unsafe { curl_easy_cleanup(handle) };
5530    }
5531
5532    #[test]
5533    fn easy_setopt_proxyuserpwd() {
5534        let handle = curl_easy_init();
5535        let up = c"proxyuser:proxypass";
5536        // CURLOPT_PROXYUSERPWD = 10006
5537        let code = unsafe { curl_easy_setopt(handle, 10006, up.as_ptr().cast::<c_void>()) };
5538        assert_eq!(code, CURLcode::CURLE_OK);
5539        unsafe { curl_easy_cleanup(handle) };
5540    }
5541
5542    #[test]
5543    fn easy_setopt_proxyauth() {
5544        let handle = curl_easy_init();
5545        // CURLOPT_PROXYAUTH = 111, bitmask 1=Basic
5546        let code = unsafe { curl_easy_setopt(handle, 111, std::ptr::dangling::<c_void>()) };
5547        assert_eq!(code, CURLcode::CURLE_OK);
5548        unsafe { curl_easy_cleanup(handle) };
5549    }
5550
5551    #[test]
5552    fn easy_setopt_proxy_sslcert() {
5553        let handle = curl_easy_init();
5554        let path = c"/tmp/proxy-cert.pem";
5555        // CURLOPT_PROXY_SSLCERT = 10254
5556        let code = unsafe { curl_easy_setopt(handle, 10254, path.as_ptr().cast::<c_void>()) };
5557        assert_eq!(code, CURLcode::CURLE_OK);
5558        unsafe { curl_easy_cleanup(handle) };
5559    }
5560
5561    #[test]
5562    fn easy_setopt_proxy_sslkey() {
5563        let handle = curl_easy_init();
5564        let path = c"/tmp/proxy-key.pem";
5565        // CURLOPT_PROXY_SSLKEY = 10255
5566        let code = unsafe { curl_easy_setopt(handle, 10255, path.as_ptr().cast::<c_void>()) };
5567        assert_eq!(code, CURLcode::CURLE_OK);
5568        unsafe { curl_easy_cleanup(handle) };
5569    }
5570
5571    #[test]
5572    fn easy_setopt_proxy_ssl_verifypeer() {
5573        let handle = curl_easy_init();
5574        // CURLOPT_PROXY_SSL_VERIFYPEER = 248
5575        let code = unsafe { curl_easy_setopt(handle, 248, std::ptr::dangling::<c_void>()) };
5576        assert_eq!(code, CURLcode::CURLE_OK);
5577        unsafe { curl_easy_cleanup(handle) };
5578    }
5579
5580    unsafe extern "C" fn test_read_cb(
5581        _buf: *mut c_char,
5582        _size: usize,
5583        _nmemb: usize,
5584        _data: *mut c_void,
5585    ) -> usize {
5586        0 // EOF
5587    }
5588
5589    unsafe extern "C" fn test_debug_cb(
5590        _handle: *mut c_void,
5591        _info_type: c_long,
5592        _data: *mut c_char,
5593        _size: usize,
5594        _userdata: *mut c_void,
5595    ) -> c_long {
5596        0
5597    }
5598
5599    #[test]
5600    fn easy_setopt_readfunction() {
5601        let handle = curl_easy_init();
5602        // CURLOPT_READFUNCTION = 20012
5603        let code = unsafe { curl_easy_setopt(handle, 20012, test_read_cb as *const c_void) };
5604        assert_eq!(code, CURLcode::CURLE_OK);
5605        unsafe { curl_easy_cleanup(handle) };
5606    }
5607
5608    #[test]
5609    fn easy_setopt_readdata() {
5610        let handle = curl_easy_init();
5611        // CURLOPT_READDATA = 10009
5612        let mut data: usize = 42;
5613        let code =
5614            unsafe { curl_easy_setopt(handle, 10009, ptr::from_mut(&mut data).cast::<c_void>()) };
5615        assert_eq!(code, CURLcode::CURLE_OK);
5616        unsafe { curl_easy_cleanup(handle) };
5617    }
5618
5619    #[test]
5620    fn easy_setopt_debugfunction() {
5621        let handle = curl_easy_init();
5622        // CURLOPT_DEBUGFUNCTION = 20094
5623        let code = unsafe { curl_easy_setopt(handle, 20094, test_debug_cb as *const c_void) };
5624        assert_eq!(code, CURLcode::CURLE_OK);
5625        unsafe { curl_easy_cleanup(handle) };
5626    }
5627
5628    #[test]
5629    fn easy_setopt_debugdata() {
5630        let handle = curl_easy_init();
5631        // CURLOPT_DEBUGDATA = 10095
5632        let mut data: usize = 99;
5633        let code =
5634            unsafe { curl_easy_setopt(handle, 10095, ptr::from_mut(&mut data).cast::<c_void>()) };
5635        assert_eq!(code, CURLcode::CURLE_OK);
5636        unsafe { curl_easy_cleanup(handle) };
5637    }
5638
5639    #[test]
5640    fn easy_setopt_infilesize_large() {
5641        let handle = curl_easy_init();
5642        // CURLOPT_INFILESIZE_LARGE = 30115
5643        let code = unsafe { curl_easy_setopt(handle, 30115, 4096_usize as *const c_void) };
5644        assert_eq!(code, CURLcode::CURLE_OK);
5645        unsafe { curl_easy_cleanup(handle) };
5646    }
5647
5648    #[test]
5649    fn easy_duphandle_preserves_callbacks() {
5650        let handle = curl_easy_init();
5651        let _ = unsafe { curl_easy_setopt(handle, 20012, test_read_cb as *const c_void) };
5652        let _ = unsafe { curl_easy_setopt(handle, 20094, test_debug_cb as *const c_void) };
5653        let _ = unsafe { curl_easy_setopt(handle, 30115, 1024_usize as *const c_void) };
5654
5655        let dup = unsafe { curl_easy_duphandle(handle) };
5656        assert!(!dup.is_null());
5657
5658        // Verify callbacks were preserved
5659        let dup_h = unsafe { &*dup.cast::<EasyHandle>() };
5660        assert!(dup_h.read_callback.is_some());
5661        assert!(dup_h.debug_callback.is_some());
5662        assert_eq!(dup_h.infilesize, Some(1024));
5663
5664        unsafe {
5665            curl_easy_cleanup(dup);
5666            curl_easy_cleanup(handle);
5667        }
5668    }
5669
5670    #[test]
5671    fn easy_reset_clears_callbacks() {
5672        let handle = curl_easy_init();
5673        let _ = unsafe { curl_easy_setopt(handle, 20012, test_read_cb as *const c_void) };
5674        let _ = unsafe { curl_easy_setopt(handle, 30115, 2048_usize as *const c_void) };
5675
5676        unsafe { curl_easy_reset(handle) };
5677
5678        let h = unsafe { &*handle.cast::<EasyHandle>() };
5679        assert!(h.read_callback.is_none());
5680        assert!(h.debug_callback.is_none());
5681        assert!(h.infilesize.is_none());
5682
5683        unsafe { curl_easy_cleanup(handle) };
5684    }
5685
5686    #[test]
5687    fn easy_setopt_dns_cache_timeout() {
5688        let handle = curl_easy_init();
5689        // CURLOPT_DNS_CACHE_TIMEOUT = 92
5690        let code = unsafe { curl_easy_setopt(handle, 92, 120_usize as *const c_void) };
5691        assert_eq!(code, CURLcode::CURLE_OK);
5692        unsafe { curl_easy_cleanup(handle) };
5693    }
5694
5695    #[test]
5696    fn easy_setopt_happy_eyeballs_timeout() {
5697        let handle = curl_easy_init();
5698        // CURLOPT_HAPPY_EYEBALLS_TIMEOUT_MS = 271
5699        let code = unsafe { curl_easy_setopt(handle, 271, 100_usize as *const c_void) };
5700        assert_eq!(code, CURLcode::CURLE_OK);
5701        unsafe { curl_easy_cleanup(handle) };
5702    }
5703
5704    #[test]
5705    fn easy_setopt_dns_servers() {
5706        let handle = curl_easy_init();
5707        // CURLOPT_DNS_SERVERS = 10211
5708        let servers = c"8.8.8.8,8.8.4.4";
5709        let code = unsafe { curl_easy_setopt(handle, 10211, servers.as_ptr().cast::<c_void>()) };
5710        assert_eq!(code, CURLcode::CURLE_OK);
5711        unsafe { curl_easy_cleanup(handle) };
5712    }
5713
5714    #[test]
5715    fn easy_setopt_dns_servers_invalid() {
5716        let handle = curl_easy_init();
5717        let servers = c"not-valid";
5718        let code = unsafe { curl_easy_setopt(handle, 10211, servers.as_ptr().cast::<c_void>()) };
5719        assert_eq!(code, CURLcode::CURLE_BAD_FUNCTION_ARGUMENT);
5720        unsafe { curl_easy_cleanup(handle) };
5721    }
5722
5723    #[test]
5724    fn easy_setopt_doh_url() {
5725        let handle = curl_easy_init();
5726        // CURLOPT_DOH_URL = 10279
5727        let url = c"https://dns.google/dns-query";
5728        let code = unsafe { curl_easy_setopt(handle, 10279, url.as_ptr().cast::<c_void>()) };
5729        assert_eq!(code, CURLcode::CURLE_OK);
5730        unsafe { curl_easy_cleanup(handle) };
5731    }
5732
5733    #[test]
5734    fn easy_setopt_unrestricted_auth() {
5735        let handle = curl_easy_init();
5736        // CURLOPT_UNRESTRICTED_AUTH = 105
5737        let code = unsafe { curl_easy_setopt(handle, 105, std::ptr::dangling::<c_void>()) };
5738        assert_eq!(code, CURLcode::CURLE_OK);
5739        unsafe { curl_easy_cleanup(handle) };
5740    }
5741
5742    #[test]
5743    fn easy_setopt_ignore_content_length() {
5744        let handle = curl_easy_init();
5745        // CURLOPT_IGNORE_CONTENT_LENGTH = 136
5746        let code = unsafe { curl_easy_setopt(handle, 136, std::ptr::dangling::<c_void>()) };
5747        assert_eq!(code, CURLcode::CURLE_OK);
5748        unsafe { curl_easy_cleanup(handle) };
5749    }
5750
5751    // ─── Phase 22: Progress callbacks ───
5752
5753    unsafe extern "C" fn test_progress_cb(
5754        _clientp: *mut c_void,
5755        _dltotal: f64,
5756        _dlnow: f64,
5757        _ultotal: f64,
5758        _ulnow: f64,
5759    ) -> c_long {
5760        0 // continue
5761    }
5762
5763    unsafe extern "C" fn test_xferinfo_cb(
5764        _clientp: *mut c_void,
5765        _dltotal: i64,
5766        _dlnow: i64,
5767        _ultotal: i64,
5768        _ulnow: i64,
5769    ) -> c_long {
5770        0 // continue
5771    }
5772
5773    unsafe extern "C" fn test_seek_cb(
5774        _clientp: *mut c_void,
5775        _offset: i64,
5776        _origin: c_long,
5777    ) -> c_long {
5778        0 // success
5779    }
5780
5781    #[test]
5782    fn easy_setopt_progressfunction() {
5783        let handle = curl_easy_init();
5784        // CURLOPT_PROGRESSFUNCTION = 20056
5785        let code = unsafe { curl_easy_setopt(handle, 20056, test_progress_cb as *const c_void) };
5786        assert_eq!(code, CURLcode::CURLE_OK);
5787        unsafe { curl_easy_cleanup(handle) };
5788    }
5789
5790    #[test]
5791    fn easy_setopt_xferinfofunction() {
5792        let handle = curl_easy_init();
5793        // CURLOPT_XFERINFOFUNCTION = 20219
5794        let code = unsafe { curl_easy_setopt(handle, 20219, test_xferinfo_cb as *const c_void) };
5795        assert_eq!(code, CURLcode::CURLE_OK);
5796        unsafe { curl_easy_cleanup(handle) };
5797    }
5798
5799    #[test]
5800    fn easy_setopt_progressdata() {
5801        let handle = curl_easy_init();
5802        // CURLOPT_PROGRESSDATA = 10057
5803        let mut data: usize = 42;
5804        let code =
5805            unsafe { curl_easy_setopt(handle, 10057, ptr::from_mut(&mut data).cast::<c_void>()) };
5806        assert_eq!(code, CURLcode::CURLE_OK);
5807        unsafe { curl_easy_cleanup(handle) };
5808    }
5809
5810    #[test]
5811    fn easy_setopt_noprogress() {
5812        let handle = curl_easy_init();
5813        // Verify default is noprogress=true
5814        let h = unsafe { &*handle.cast::<EasyHandle>() };
5815        assert!(h.noprogress);
5816
5817        // CURLOPT_NOPROGRESS = 43, set to 0 (false) to enable progress
5818        let code = unsafe { curl_easy_setopt(handle, 43, ptr::null()) };
5819        assert_eq!(code, CURLcode::CURLE_OK);
5820        let h = unsafe { &*handle.cast::<EasyHandle>() };
5821        assert!(!h.noprogress);
5822
5823        // Set back to 1 (true) to disable progress
5824        let code = unsafe { curl_easy_setopt(handle, 43, std::ptr::dangling::<c_void>()) };
5825        assert_eq!(code, CURLcode::CURLE_OK);
5826        let h = unsafe { &*handle.cast::<EasyHandle>() };
5827        assert!(h.noprogress);
5828
5829        unsafe { curl_easy_cleanup(handle) };
5830    }
5831
5832    #[test]
5833    fn easy_setopt_seekfunction() {
5834        let handle = curl_easy_init();
5835        // CURLOPT_SEEKFUNCTION = 20167
5836        let code = unsafe { curl_easy_setopt(handle, 20167, test_seek_cb as *const c_void) };
5837        assert_eq!(code, CURLcode::CURLE_OK);
5838        unsafe { curl_easy_cleanup(handle) };
5839    }
5840
5841    #[test]
5842    fn easy_setopt_seekdata() {
5843        let handle = curl_easy_init();
5844        // CURLOPT_SEEKDATA = 10168
5845        let mut data: usize = 99;
5846        let code =
5847            unsafe { curl_easy_setopt(handle, 10168, ptr::from_mut(&mut data).cast::<c_void>()) };
5848        assert_eq!(code, CURLcode::CURLE_OK);
5849        unsafe { curl_easy_cleanup(handle) };
5850    }
5851
5852    // ─── Phase 22: CURLOPT_PRIVATE ───
5853
5854    #[test]
5855    fn easy_setopt_private() {
5856        let handle = curl_easy_init();
5857        let mut data: usize = 12345;
5858        // CURLOPT_PRIVATE = 10103
5859        let code =
5860            unsafe { curl_easy_setopt(handle, 10103, ptr::from_mut(&mut data).cast::<c_void>()) };
5861        assert_eq!(code, CURLcode::CURLE_OK);
5862
5863        // Retrieve it via CURLINFO_PRIVATE = 0x100015
5864        let mut out: *mut c_void = ptr::null_mut();
5865        let result = unsafe {
5866            curl_easy_getinfo(handle, 0x10_0015, ptr::from_mut(&mut out).cast::<c_void>())
5867        };
5868        assert_eq!(result, CURLcode::CURLE_OK);
5869        assert_eq!(out, ptr::from_mut(&mut data).cast::<c_void>());
5870
5871        unsafe { curl_easy_cleanup(handle) };
5872    }
5873
5874    #[test]
5875    fn easy_getinfo_private_default_null() {
5876        let handle = curl_easy_init();
5877        let mut out: *mut c_void = std::ptr::dangling_mut::<c_void>();
5878        // CURLINFO_PRIVATE before setting — should be null
5879        let result = unsafe {
5880            curl_easy_getinfo(handle, 0x10_0015, ptr::from_mut(&mut out).cast::<c_void>())
5881        };
5882        assert_eq!(result, CURLcode::CURLE_OK);
5883        assert!(out.is_null());
5884        unsafe { curl_easy_cleanup(handle) };
5885    }
5886
5887    // ─── Phase 22: CURLOPT_SHARE ───
5888
5889    #[test]
5890    fn easy_setopt_share() {
5891        let handle = curl_easy_init();
5892        let share = curl_share_init();
5893
5894        // CURLOPT_SHARE = 10100
5895        let code = unsafe { curl_easy_setopt(handle, 10100, share.cast::<c_void>()) };
5896        assert_eq!(code, CURLcode::CURLE_OK);
5897
5898        // Detach share
5899        let code = unsafe { curl_easy_setopt(handle, 10100, ptr::null()) };
5900        assert_eq!(code, CURLcode::CURLE_OK);
5901
5902        unsafe {
5903            let _ = curl_share_cleanup(share);
5904            curl_easy_cleanup(handle);
5905        }
5906    }
5907
5908    // ─── Phase 22: MIME API ───
5909
5910    #[test]
5911    fn mime_init_free() {
5912        let handle = curl_easy_init();
5913        let mime = unsafe { curl_mime_init(handle) };
5914        assert!(!mime.is_null());
5915        unsafe {
5916            curl_mime_free(mime);
5917            curl_easy_cleanup(handle);
5918        }
5919    }
5920
5921    #[test]
5922    fn mime_free_null_is_safe() {
5923        unsafe { curl_mime_free(ptr::null_mut()) };
5924    }
5925
5926    #[test]
5927    fn mime_addpart() {
5928        let handle = curl_easy_init();
5929        let mime = unsafe { curl_mime_init(handle) };
5930        let part = unsafe { curl_mime_addpart(mime) };
5931        assert!(!part.is_null());
5932        // Parts are standalone — need to finalize manually
5933        unsafe {
5934            // Set name and data on the part
5935            let code = curl_mime_name(part, c"field1".as_ptr());
5936            assert_eq!(code, CURLcode::CURLE_OK);
5937            let code = curl_mime_data(part, c"value1".as_ptr(), usize::MAX);
5938            assert_eq!(code, CURLcode::CURLE_OK);
5939            // Finalize part into mime
5940            finalize_mime_part(mime, part);
5941            curl_mime_free(mime);
5942            curl_easy_cleanup(handle);
5943        }
5944    }
5945
5946    #[test]
5947    fn mime_name_null_part() {
5948        let code = unsafe { curl_mime_name(ptr::null_mut(), c"test".as_ptr()) };
5949        assert_eq!(code, CURLcode::CURLE_BAD_FUNCTION_ARGUMENT);
5950    }
5951
5952    #[test]
5953    fn mime_data_null_part() {
5954        let code = unsafe { curl_mime_data(ptr::null_mut(), c"test".as_ptr(), 4) };
5955        assert_eq!(code, CURLcode::CURLE_BAD_FUNCTION_ARGUMENT);
5956    }
5957
5958    #[test]
5959    fn mime_data_with_explicit_size() {
5960        let handle = curl_easy_init();
5961        let mime = unsafe { curl_mime_init(handle) };
5962        let part = unsafe { curl_mime_addpart(mime) };
5963        let code = unsafe { curl_mime_name(part, c"binary".as_ptr()) };
5964        assert_eq!(code, CURLcode::CURLE_OK);
5965        let data = b"hello";
5966        let code = unsafe { curl_mime_data(part, data.as_ptr().cast::<c_char>(), 5) };
5967        assert_eq!(code, CURLcode::CURLE_OK);
5968        unsafe {
5969            finalize_mime_part(mime, part);
5970            curl_mime_free(mime);
5971            curl_easy_cleanup(handle);
5972        }
5973    }
5974
5975    #[test]
5976    fn mime_filename() {
5977        let handle = curl_easy_init();
5978        let mime = unsafe { curl_mime_init(handle) };
5979        let part = unsafe { curl_mime_addpart(mime) };
5980        let code = unsafe { curl_mime_filename(part, c"upload.txt".as_ptr()) };
5981        assert_eq!(code, CURLcode::CURLE_OK);
5982        let code = unsafe { curl_mime_filename(ptr::null_mut(), c"test".as_ptr()) };
5983        assert_eq!(code, CURLcode::CURLE_BAD_FUNCTION_ARGUMENT);
5984        unsafe {
5985            // Clean up part without finalize — it was never given name/data
5986            let _ = Box::from_raw(part.cast::<MimePartHandle>());
5987            curl_mime_free(mime);
5988            curl_easy_cleanup(handle);
5989        }
5990    }
5991
5992    #[test]
5993    fn mime_type() {
5994        let handle = curl_easy_init();
5995        let mime = unsafe { curl_mime_init(handle) };
5996        let part = unsafe { curl_mime_addpart(mime) };
5997        let code = unsafe { curl_mime_type(part, c"text/plain".as_ptr()) };
5998        assert_eq!(code, CURLcode::CURLE_OK);
5999        let p = unsafe { &*part.cast::<MimePartHandle>() };
6000        assert_eq!(p.mime_type.as_deref(), Some("text/plain"));
6001        unsafe {
6002            let _ = Box::from_raw(part.cast::<MimePartHandle>());
6003            curl_mime_free(mime);
6004            curl_easy_cleanup(handle);
6005        }
6006    }
6007
6008    #[test]
6009    fn mime_type_null_part() {
6010        let code = unsafe { curl_mime_type(ptr::null_mut(), c"text/plain".as_ptr()) };
6011        assert_eq!(code, CURLcode::CURLE_BAD_FUNCTION_ARGUMENT);
6012    }
6013
6014    #[test]
6015    fn easy_setopt_mimepost() {
6016        let handle = curl_easy_init();
6017        let mime = unsafe { curl_mime_init(handle) };
6018        let part = unsafe { curl_mime_addpart(mime) };
6019        let _ = unsafe { curl_mime_name(part, c"field".as_ptr()) };
6020        let _ = unsafe { curl_mime_data(part, c"value".as_ptr(), usize::MAX) };
6021        unsafe { finalize_mime_part(mime, part) };
6022
6023        // CURLOPT_MIMEPOST = 10269
6024        let code = unsafe { curl_easy_setopt(handle, 10269, mime.cast::<c_void>()) };
6025        assert_eq!(code, CURLcode::CURLE_OK);
6026
6027        unsafe {
6028            curl_mime_free(mime);
6029            curl_easy_cleanup(handle);
6030        }
6031    }
6032
6033    // ─── Phase 22: Share API ───
6034
6035    #[test]
6036    fn share_init_cleanup() {
6037        let share = curl_share_init();
6038        assert!(!share.is_null());
6039        let code = unsafe { curl_share_cleanup(share) };
6040        assert_eq!(code, CURLSHcode::CURLSHE_OK);
6041    }
6042
6043    #[test]
6044    fn share_cleanup_null() {
6045        let code = unsafe { curl_share_cleanup(ptr::null_mut()) };
6046        assert_eq!(code, CURLSHcode::CURLSHE_INVALID);
6047    }
6048
6049    #[test]
6050    fn share_setopt_dns() {
6051        let share = curl_share_init();
6052        // CURLSHOPT_SHARE = 1, CURL_LOCK_DATA_DNS = 3
6053        let code = unsafe { curl_share_setopt(share, 1, 3 as *const c_void) };
6054        assert_eq!(code, CURLSHcode::CURLSHE_OK);
6055        let _ = unsafe { curl_share_cleanup(share) };
6056    }
6057
6058    #[test]
6059    fn share_setopt_cookies() {
6060        let share = curl_share_init();
6061        // CURLSHOPT_SHARE = 1, CURL_LOCK_DATA_COOKIE = 2
6062        let code = unsafe { curl_share_setopt(share, 1, 2 as *const c_void) };
6063        assert_eq!(code, CURLSHcode::CURLSHE_OK);
6064        let _ = unsafe { curl_share_cleanup(share) };
6065    }
6066
6067    #[test]
6068    fn share_setopt_unshare() {
6069        let share = curl_share_init();
6070        // Share DNS
6071        let _ = unsafe { curl_share_setopt(share, 1, 3 as *const c_void) };
6072        // Unshare DNS
6073        let code = unsafe { curl_share_setopt(share, 2, 3 as *const c_void) };
6074        assert_eq!(code, CURLSHcode::CURLSHE_OK);
6075        let _ = unsafe { curl_share_cleanup(share) };
6076    }
6077
6078    #[test]
6079    fn share_setopt_bad_option() {
6080        let share = curl_share_init();
6081        let code = unsafe { curl_share_setopt(share, 99, ptr::null()) };
6082        assert_eq!(code, CURLSHcode::CURLSHE_BAD_OPTION);
6083        let _ = unsafe { curl_share_cleanup(share) };
6084    }
6085
6086    #[test]
6087    fn share_setopt_null_handle() {
6088        let code = unsafe { curl_share_setopt(ptr::null_mut(), 1, 3 as *const c_void) };
6089        assert_eq!(code, CURLSHcode::CURLSHE_INVALID);
6090    }
6091
6092    #[test]
6093    fn share_setopt_lockfunc_accepted() {
6094        let share = curl_share_init();
6095        // CURLSHOPT_LOCKFUNC = 3 — accepted but ignored
6096        let code = unsafe { curl_share_setopt(share, 3, ptr::null()) };
6097        assert_eq!(code, CURLSHcode::CURLSHE_OK);
6098        // CURLSHOPT_UNLOCKFUNC = 4
6099        let code = unsafe { curl_share_setopt(share, 4, ptr::null()) };
6100        assert_eq!(code, CURLSHcode::CURLSHE_OK);
6101        let _ = unsafe { curl_share_cleanup(share) };
6102    }
6103
6104    #[test]
6105    fn share_strerror_ok() {
6106        let msg = curl_share_strerror(CURLSHcode::CURLSHE_OK);
6107        assert!(!msg.is_null());
6108        let s = unsafe { CStr::from_ptr(msg) };
6109        assert_eq!(s.to_str().unwrap(), "No error");
6110    }
6111
6112    #[test]
6113    fn share_strerror_all_codes() {
6114        let codes = [
6115            CURLSHcode::CURLSHE_OK,
6116            CURLSHcode::CURLSHE_BAD_OPTION,
6117            CURLSHcode::CURLSHE_IN_USE,
6118            CURLSHcode::CURLSHE_INVALID,
6119            CURLSHcode::CURLSHE_NOMEM,
6120            CURLSHcode::CURLSHE_NOT_BUILT_IN,
6121        ];
6122        for code in codes {
6123            let msg = curl_share_strerror(code);
6124            assert!(!msg.is_null(), "share_strerror returned null for {code:?}");
6125        }
6126    }
6127
6128    // ─── Phase 22: Duphandle/Reset preserve new fields ───
6129
6130    #[test]
6131    fn easy_duphandle_preserves_progress_callbacks() {
6132        let handle = curl_easy_init();
6133        let _ = unsafe { curl_easy_setopt(handle, 20056, test_progress_cb as *const c_void) };
6134        let _ = unsafe { curl_easy_setopt(handle, 20219, test_xferinfo_cb as *const c_void) };
6135        let _ = unsafe { curl_easy_setopt(handle, 20167, test_seek_cb as *const c_void) };
6136        let mut priv_data: usize = 42;
6137        let _ = unsafe {
6138            curl_easy_setopt(handle, 10103, ptr::from_mut(&mut priv_data).cast::<c_void>())
6139        };
6140
6141        let dup = unsafe { curl_easy_duphandle(handle) };
6142        assert!(!dup.is_null());
6143        let dup_h = unsafe { &*dup.cast::<EasyHandle>() };
6144        assert!(dup_h.progress_callback.is_some());
6145        assert!(dup_h.xferinfo_callback.is_some());
6146        assert!(dup_h.seek_callback.is_some());
6147        assert_eq!(dup_h.private_data, ptr::from_mut(&mut priv_data).cast::<c_void>());
6148
6149        unsafe {
6150            curl_easy_cleanup(dup);
6151            curl_easy_cleanup(handle);
6152        }
6153    }
6154
6155    #[test]
6156    fn easy_reset_clears_new_fields() {
6157        let handle = curl_easy_init();
6158        let _ = unsafe { curl_easy_setopt(handle, 20056, test_progress_cb as *const c_void) };
6159        let _ = unsafe { curl_easy_setopt(handle, 20219, test_xferinfo_cb as *const c_void) };
6160        let _ = unsafe { curl_easy_setopt(handle, 20167, test_seek_cb as *const c_void) };
6161        let _ = unsafe { curl_easy_setopt(handle, 10103, 42usize as *const c_void) };
6162        let _ = unsafe { curl_easy_setopt(handle, 43, ptr::null()) }; // noprogress = false
6163
6164        unsafe { curl_easy_reset(handle) };
6165
6166        let h = unsafe { &*handle.cast::<EasyHandle>() };
6167        assert!(h.progress_callback.is_none());
6168        assert!(h.xferinfo_callback.is_none());
6169        assert!(h.seek_callback.is_none());
6170        assert!(h.private_data.is_null());
6171        assert!(h.noprogress); // reset to default true
6172        assert!(h.mimepost.is_null());
6173
6174        unsafe { curl_easy_cleanup(handle) };
6175    }
6176
6177    // ─── Phase 23: URL API ───
6178
6179    #[test]
6180    fn url_init_cleanup() {
6181        let handle = curl_url();
6182        assert!(!handle.is_null());
6183        unsafe { curl_url_cleanup(handle) };
6184    }
6185
6186    #[test]
6187    fn url_cleanup_null_is_safe() {
6188        unsafe { curl_url_cleanup(ptr::null_mut()) };
6189    }
6190
6191    #[test]
6192    fn url_set_full_url() {
6193        let handle = curl_url();
6194        let url = c"https://example.com/path?q=1#frag";
6195        let code = unsafe { curl_url_set(handle, 0, url.as_ptr(), 0) };
6196        assert_eq!(code, CURLUcode::CURLUE_OK);
6197
6198        // Get scheme
6199        let mut part: *mut c_char = ptr::null_mut();
6200        let code = unsafe { curl_url_get(handle, 1, &raw mut part, 0) };
6201        assert_eq!(code, CURLUcode::CURLUE_OK);
6202        assert!(!part.is_null());
6203        let scheme = unsafe { CStr::from_ptr(part) }.to_str().unwrap();
6204        assert_eq!(scheme, "https");
6205        unsafe { curl_free(part.cast::<c_void>()) };
6206
6207        // Get host
6208        let mut part: *mut c_char = ptr::null_mut();
6209        let code = unsafe { curl_url_get(handle, 5, &raw mut part, 0) };
6210        assert_eq!(code, CURLUcode::CURLUE_OK);
6211        let host = unsafe { CStr::from_ptr(part) }.to_str().unwrap();
6212        assert_eq!(host, "example.com");
6213        unsafe { curl_free(part.cast::<c_void>()) };
6214
6215        // Get path
6216        let mut part: *mut c_char = ptr::null_mut();
6217        let code = unsafe { curl_url_get(handle, 7, &raw mut part, 0) };
6218        assert_eq!(code, CURLUcode::CURLUE_OK);
6219        let path = unsafe { CStr::from_ptr(part) }.to_str().unwrap();
6220        assert_eq!(path, "/path");
6221        unsafe { curl_free(part.cast::<c_void>()) };
6222
6223        // Get query
6224        let mut part: *mut c_char = ptr::null_mut();
6225        let code = unsafe { curl_url_get(handle, 8, &raw mut part, 0) };
6226        assert_eq!(code, CURLUcode::CURLUE_OK);
6227        let query = unsafe { CStr::from_ptr(part) }.to_str().unwrap();
6228        assert_eq!(query, "q=1");
6229        unsafe { curl_free(part.cast::<c_void>()) };
6230
6231        // Get fragment
6232        let mut part: *mut c_char = ptr::null_mut();
6233        let code = unsafe { curl_url_get(handle, 9, &raw mut part, 0) };
6234        assert_eq!(code, CURLUcode::CURLUE_OK);
6235        let frag = unsafe { CStr::from_ptr(part) }.to_str().unwrap();
6236        assert_eq!(frag, "frag");
6237        unsafe { curl_free(part.cast::<c_void>()) };
6238
6239        unsafe { curl_url_cleanup(handle) };
6240    }
6241
6242    #[test]
6243    fn url_set_individual_parts() {
6244        let handle = curl_url();
6245        let _ = unsafe { curl_url_set(handle, 1, c"https".as_ptr(), 0) };
6246        let _ = unsafe { curl_url_set(handle, 5, c"example.com".as_ptr(), 0) };
6247        let _ = unsafe { curl_url_set(handle, 6, c"8080".as_ptr(), 0) };
6248        let _ = unsafe { curl_url_set(handle, 7, c"/api/v1".as_ptr(), 0) };
6249
6250        // Get reassembled URL
6251        let mut part: *mut c_char = ptr::null_mut();
6252        let code = unsafe { curl_url_get(handle, 0, &raw mut part, 0) };
6253        assert_eq!(code, CURLUcode::CURLUE_OK);
6254        let url = unsafe { CStr::from_ptr(part) }.to_str().unwrap();
6255        assert_eq!(url, "https://example.com:8080/api/v1");
6256        unsafe { curl_free(part.cast::<c_void>()) };
6257
6258        unsafe { curl_url_cleanup(handle) };
6259    }
6260
6261    #[test]
6262    fn url_set_with_userinfo() {
6263        let handle = curl_url();
6264        let url = c"http://user:pass@example.com/";
6265        let _ = unsafe { curl_url_set(handle, 0, url.as_ptr(), 0) };
6266
6267        let mut part: *mut c_char = ptr::null_mut();
6268        let code = unsafe { curl_url_get(handle, 2, &raw mut part, 0) };
6269        assert_eq!(code, CURLUcode::CURLUE_OK);
6270        let user = unsafe { CStr::from_ptr(part) }.to_str().unwrap();
6271        assert_eq!(user, "user");
6272        unsafe { curl_free(part.cast::<c_void>()) };
6273
6274        let mut part: *mut c_char = ptr::null_mut();
6275        let code = unsafe { curl_url_get(handle, 3, &raw mut part, 0) };
6276        assert_eq!(code, CURLUcode::CURLUE_OK);
6277        let pass = unsafe { CStr::from_ptr(part) }.to_str().unwrap();
6278        assert_eq!(pass, "pass");
6279        unsafe { curl_free(part.cast::<c_void>()) };
6280
6281        unsafe { curl_url_cleanup(handle) };
6282    }
6283
6284    #[test]
6285    fn url_set_bad_port() {
6286        let handle = curl_url();
6287        let code = unsafe { curl_url_set(handle, 6, c"not_a_port".as_ptr(), 0) };
6288        assert_eq!(code, CURLUcode::CURLUE_BAD_PORT_NUMBER);
6289        unsafe { curl_url_cleanup(handle) };
6290    }
6291
6292    #[test]
6293    fn url_set_malformed_url() {
6294        let handle = curl_url();
6295        let code = unsafe { curl_url_set(handle, 0, c"".as_ptr(), 0) };
6296        assert_eq!(code, CURLUcode::CURLUE_MALFORMED_INPUT);
6297        unsafe { curl_url_cleanup(handle) };
6298    }
6299
6300    #[test]
6301    fn url_set_null_clears() {
6302        let handle = curl_url();
6303        let _ = unsafe { curl_url_set(handle, 0, c"https://example.com/path".as_ptr(), 0) };
6304        // Clear the query
6305        let code = unsafe { curl_url_set(handle, 8, ptr::null(), 0) };
6306        assert_eq!(code, CURLUcode::CURLUE_OK);
6307
6308        let mut part: *mut c_char = ptr::null_mut();
6309        let code = unsafe { curl_url_get(handle, 8, &raw mut part, 0) };
6310        assert_eq!(code, CURLUcode::CURLUE_OK);
6311        assert!(part.is_null()); // Query was cleared
6312
6313        unsafe { curl_url_cleanup(handle) };
6314    }
6315
6316    #[test]
6317    fn url_get_null_handle() {
6318        let mut part: *mut c_char = ptr::null_mut();
6319        let code = unsafe { curl_url_get(ptr::null_mut(), 0, &raw mut part, 0) };
6320        assert_eq!(code, CURLUcode::CURLUE_BAD_HANDLE);
6321    }
6322
6323    #[test]
6324    fn url_set_null_handle() {
6325        let code = unsafe { curl_url_set(ptr::null_mut(), 0, c"test".as_ptr(), 0) };
6326        assert_eq!(code, CURLUcode::CURLUE_BAD_HANDLE);
6327    }
6328
6329    #[test]
6330    fn url_get_unknown_part() {
6331        let handle = curl_url();
6332        let mut part: *mut c_char = ptr::null_mut();
6333        let code = unsafe { curl_url_get(handle, 99, &raw mut part, 0) };
6334        assert_eq!(code, CURLUcode::CURLUE_UNKNOWN_PART);
6335        unsafe { curl_url_cleanup(handle) };
6336    }
6337
6338    #[test]
6339    fn url_dup() {
6340        let handle = curl_url();
6341        let _ = unsafe { curl_url_set(handle, 0, c"https://example.com/path".as_ptr(), 0) };
6342
6343        let dup = unsafe { curl_url_dup(handle) };
6344        assert!(!dup.is_null());
6345
6346        // Verify dup has same scheme
6347        let mut part: *mut c_char = ptr::null_mut();
6348        let code = unsafe { curl_url_get(dup, 1, &raw mut part, 0) };
6349        assert_eq!(code, CURLUcode::CURLUE_OK);
6350        let scheme = unsafe { CStr::from_ptr(part) }.to_str().unwrap();
6351        assert_eq!(scheme, "https");
6352        unsafe { curl_free(part.cast::<c_void>()) };
6353
6354        unsafe {
6355            curl_url_cleanup(dup);
6356            curl_url_cleanup(handle);
6357        }
6358    }
6359
6360    #[test]
6361    fn url_dup_null() {
6362        let dup = unsafe { curl_url_dup(ptr::null_mut()) };
6363        assert!(dup.is_null());
6364    }
6365
6366    #[test]
6367    fn curl_free_null_is_safe() {
6368        unsafe { curl_free(ptr::null_mut()) };
6369    }
6370
6371    // ─── Phase 23: New CURLOPT options ───
6372
6373    #[test]
6374    fn easy_setopt_referer() {
6375        let handle = curl_easy_init();
6376        let referer = c"https://example.com/";
6377        // CURLOPT_REFERER = 10016
6378        let code = unsafe { curl_easy_setopt(handle, 10016, referer.as_ptr().cast::<c_void>()) };
6379        assert_eq!(code, CURLcode::CURLE_OK);
6380        unsafe { curl_easy_cleanup(handle) };
6381    }
6382
6383    #[test]
6384    fn easy_setopt_http_version() {
6385        let handle = curl_easy_init();
6386        // CURLOPT_HTTP_VERSION = 84
6387        // CURL_HTTP_VERSION_1_1 = 2
6388        let code = unsafe { curl_easy_setopt(handle, 84, 2 as *const c_void) };
6389        assert_eq!(code, CURLcode::CURLE_OK);
6390        // CURL_HTTP_VERSION_2_0 = 3
6391        let code = unsafe { curl_easy_setopt(handle, 84, 3 as *const c_void) };
6392        assert_eq!(code, CURLcode::CURLE_OK);
6393        unsafe { curl_easy_cleanup(handle) };
6394    }
6395
6396    #[test]
6397    fn easy_setopt_nosignal() {
6398        let handle = curl_easy_init();
6399        // CURLOPT_NOSIGNAL = 99
6400        let code = unsafe { curl_easy_setopt(handle, 99, std::ptr::dangling::<c_void>()) };
6401        assert_eq!(code, CURLcode::CURLE_OK);
6402        unsafe { curl_easy_cleanup(handle) };
6403    }
6404
6405    #[test]
6406    fn easy_setopt_autoreferer() {
6407        let handle = curl_easy_init();
6408        // CURLOPT_AUTOREFERER = 58
6409        let code = unsafe { curl_easy_setopt(handle, 58, std::ptr::dangling::<c_void>()) };
6410        assert_eq!(code, CURLcode::CURLE_OK);
6411        unsafe { curl_easy_cleanup(handle) };
6412    }
6413
6414    #[test]
6415    fn easy_setopt_resume_from_large() {
6416        let handle = curl_easy_init();
6417        // CURLOPT_RESUME_FROM_LARGE = 30116
6418        let code = unsafe { curl_easy_setopt(handle, 30116, 1024_usize as *const c_void) };
6419        assert_eq!(code, CURLcode::CURLE_OK);
6420        unsafe { curl_easy_cleanup(handle) };
6421    }
6422
6423    #[test]
6424    fn easy_setopt_xoauth2_bearer() {
6425        let handle = curl_easy_init();
6426        let token = c"ya29.token123";
6427        // CURLOPT_XOAUTH2_BEARER = 10220
6428        let code = unsafe { curl_easy_setopt(handle, 10220, token.as_ptr().cast::<c_void>()) };
6429        assert_eq!(code, CURLcode::CURLE_OK);
6430        unsafe { curl_easy_cleanup(handle) };
6431    }
6432
6433    #[test]
6434    fn easy_setopt_localportrange() {
6435        let handle = curl_easy_init();
6436        // CURLOPT_LOCALPORTRANGE = 164
6437        let code = unsafe { curl_easy_setopt(handle, 164, 10_usize as *const c_void) };
6438        assert_eq!(code, CURLcode::CURLE_OK);
6439        unsafe { curl_easy_cleanup(handle) };
6440    }
6441
6442    // ─── Phase 27: Multi API Event Loop Integration ───
6443
6444    #[test]
6445    fn multi_info_read_empty() {
6446        let multi = curl_multi_init();
6447        let mut msgs_in_queue: c_long = 99;
6448        let msg = unsafe { curl_multi_info_read(multi, &raw mut msgs_in_queue) };
6449        assert!(msg.is_null());
6450        assert_eq!(msgs_in_queue, 0);
6451        let _ = unsafe { curl_multi_cleanup(multi) };
6452    }
6453
6454    #[test]
6455    fn multi_info_read_null_handle() {
6456        let mut msgs_in_queue: c_long = 99;
6457        let msg = unsafe { curl_multi_info_read(ptr::null_mut(), &raw mut msgs_in_queue) };
6458        assert!(msg.is_null());
6459        assert_eq!(msgs_in_queue, 0);
6460    }
6461
6462    #[test]
6463    fn multi_setopt_pipelining() {
6464        let multi = curl_multi_init();
6465        // CURLMOPT_PIPELINING = 3, value 2 = multiplex
6466        let code = unsafe { curl_multi_setopt(multi, 3, 2 as *const c_void) };
6467        assert_eq!(code, CURLMcode::CURLM_OK);
6468        // Verify via internal state
6469        let m = unsafe { &*multi.cast::<MultiHandle>() };
6470        assert_eq!(m.multi.pipelining_mode(), liburlx::PipeliningMode::Multiplex);
6471
6472        // Set back to 0 (nothing)
6473        let code = unsafe { curl_multi_setopt(multi, 3, ptr::null()) };
6474        assert_eq!(code, CURLMcode::CURLM_OK);
6475        let m = unsafe { &*multi.cast::<MultiHandle>() };
6476        assert_eq!(m.multi.pipelining_mode(), liburlx::PipeliningMode::Nothing);
6477
6478        let _ = unsafe { curl_multi_cleanup(multi) };
6479    }
6480
6481    #[test]
6482    fn multi_setopt_max_total_connections() {
6483        let multi = curl_multi_init();
6484        // CURLMOPT_MAX_TOTAL_CONNECTIONS = 13
6485        let code = unsafe { curl_multi_setopt(multi, 13, 4 as *const c_void) };
6486        assert_eq!(code, CURLMcode::CURLM_OK);
6487        let _ = unsafe { curl_multi_cleanup(multi) };
6488    }
6489
6490    #[test]
6491    fn multi_setopt_max_host_connections() {
6492        let multi = curl_multi_init();
6493        // CURLMOPT_MAX_HOST_CONNECTIONS = 7
6494        let code = unsafe { curl_multi_setopt(multi, 7, 2 as *const c_void) };
6495        assert_eq!(code, CURLMcode::CURLM_OK);
6496        let _ = unsafe { curl_multi_cleanup(multi) };
6497    }
6498
6499    #[test]
6500    fn multi_setopt_maxconnects() {
6501        let multi = curl_multi_init();
6502        // CURLMOPT_MAXCONNECTS = 6
6503        let code = unsafe { curl_multi_setopt(multi, 6, 10 as *const c_void) };
6504        assert_eq!(code, CURLMcode::CURLM_OK);
6505        let _ = unsafe { curl_multi_cleanup(multi) };
6506    }
6507
6508    #[test]
6509    fn multi_setopt_unknown_option() {
6510        let multi = curl_multi_init();
6511        let code = unsafe { curl_multi_setopt(multi, 99999, ptr::null()) };
6512        assert_eq!(code, CURLMcode::CURLM_UNKNOWN_OPTION);
6513        let _ = unsafe { curl_multi_cleanup(multi) };
6514    }
6515
6516    #[test]
6517    fn multi_setopt_null_handle() {
6518        let code = unsafe { curl_multi_setopt(ptr::null_mut(), 3, ptr::null()) };
6519        assert_eq!(code, CURLMcode::CURLM_BAD_HANDLE);
6520    }
6521
6522    #[test]
6523    fn multi_setopt_socket_data() {
6524        let multi = curl_multi_init();
6525        let mut data: usize = 42;
6526        // CURLMOPT_SOCKETDATA = 10002
6527        let code =
6528            unsafe { curl_multi_setopt(multi, 10002, ptr::from_mut(&mut data).cast::<c_void>()) };
6529        assert_eq!(code, CURLMcode::CURLM_OK);
6530        let m = unsafe { &*multi.cast::<MultiHandle>() };
6531        assert_eq!(m.socket_data, ptr::from_mut(&mut data).cast::<c_void>());
6532        let _ = unsafe { curl_multi_cleanup(multi) };
6533    }
6534
6535    #[test]
6536    fn multi_setopt_timer_data() {
6537        let multi = curl_multi_init();
6538        let mut data: usize = 99;
6539        // CURLMOPT_TIMERDATA = 10005
6540        let code =
6541            unsafe { curl_multi_setopt(multi, 10005, ptr::from_mut(&mut data).cast::<c_void>()) };
6542        assert_eq!(code, CURLMcode::CURLM_OK);
6543        let m = unsafe { &*multi.cast::<MultiHandle>() };
6544        assert_eq!(m.timer_data, ptr::from_mut(&mut data).cast::<c_void>());
6545        let _ = unsafe { curl_multi_cleanup(multi) };
6546    }
6547
6548    unsafe extern "C" fn test_socket_cb(
6549        _easy: *mut c_void,
6550        _s: c_long,
6551        _what: c_long,
6552        _userp: *mut c_void,
6553        _socketp: *mut c_void,
6554    ) -> c_long {
6555        0
6556    }
6557
6558    unsafe extern "C" fn test_timer_cb(
6559        _multi: *mut c_void,
6560        _timeout_ms: c_long,
6561        _userp: *mut c_void,
6562    ) -> c_long {
6563        0
6564    }
6565
6566    #[test]
6567    fn multi_setopt_socket_function() {
6568        let multi = curl_multi_init();
6569        // CURLMOPT_SOCKETFUNCTION = 20001
6570        let code = unsafe { curl_multi_setopt(multi, 20001, test_socket_cb as *const c_void) };
6571        assert_eq!(code, CURLMcode::CURLM_OK);
6572        let m = unsafe { &*multi.cast::<MultiHandle>() };
6573        assert!(m.socket_callback.is_some());
6574
6575        // Clear callback
6576        let code = unsafe { curl_multi_setopt(multi, 20001, ptr::null()) };
6577        assert_eq!(code, CURLMcode::CURLM_OK);
6578        let m = unsafe { &*multi.cast::<MultiHandle>() };
6579        assert!(m.socket_callback.is_none());
6580
6581        let _ = unsafe { curl_multi_cleanup(multi) };
6582    }
6583
6584    #[test]
6585    fn multi_setopt_timer_function() {
6586        let multi = curl_multi_init();
6587        // CURLMOPT_TIMERFUNCTION = 20004
6588        let code = unsafe { curl_multi_setopt(multi, 20004, test_timer_cb as *const c_void) };
6589        assert_eq!(code, CURLMcode::CURLM_OK);
6590        let m = unsafe { &*multi.cast::<MultiHandle>() };
6591        assert!(m.timer_callback.is_some());
6592
6593        // Clear callback
6594        let code = unsafe { curl_multi_setopt(multi, 20004, ptr::null()) };
6595        assert_eq!(code, CURLMcode::CURLM_OK);
6596        let m = unsafe { &*multi.cast::<MultiHandle>() };
6597        assert!(m.timer_callback.is_none());
6598
6599        let _ = unsafe { curl_multi_cleanup(multi) };
6600    }
6601
6602    #[test]
6603    fn multi_timeout_no_work() {
6604        let multi = curl_multi_init();
6605        let mut timeout_ms: c_long = 99;
6606        let code = unsafe { curl_multi_timeout(multi, &raw mut timeout_ms) };
6607        assert_eq!(code, CURLMcode::CURLM_OK);
6608        assert_eq!(timeout_ms, -1); // No work
6609        let _ = unsafe { curl_multi_cleanup(multi) };
6610    }
6611
6612    #[test]
6613    fn multi_timeout_null_handle() {
6614        let mut timeout_ms: c_long = 0;
6615        let code = unsafe { curl_multi_timeout(ptr::null_mut(), &raw mut timeout_ms) };
6616        assert_eq!(code, CURLMcode::CURLM_BAD_HANDLE);
6617    }
6618
6619    #[test]
6620    fn multi_timeout_null_output() {
6621        let multi = curl_multi_init();
6622        let code = unsafe { curl_multi_timeout(multi, ptr::null_mut()) };
6623        assert_eq!(code, CURLMcode::CURLM_BAD_HANDLE);
6624        let _ = unsafe { curl_multi_cleanup(multi) };
6625    }
6626
6627    #[test]
6628    fn multi_timeout_with_handles() {
6629        let multi = curl_multi_init();
6630        let easy = curl_easy_init();
6631        let url = c"http://127.0.0.1:1";
6632        let _ = unsafe { curl_easy_setopt(easy, 10002, url.as_ptr().cast::<c_void>()) };
6633        let _ = unsafe { curl_multi_add_handle(multi, easy) };
6634
6635        let mut timeout_ms: c_long = 0;
6636        let code = unsafe { curl_multi_timeout(multi, &raw mut timeout_ms) };
6637        assert_eq!(code, CURLMcode::CURLM_OK);
6638        assert_eq!(timeout_ms, 100); // Transfers pending
6639
6640        let _ = unsafe { curl_multi_remove_handle(multi, easy) };
6641        unsafe { curl_easy_cleanup(easy) };
6642        let _ = unsafe { curl_multi_cleanup(multi) };
6643    }
6644
6645    #[test]
6646    fn multi_wakeup() {
6647        let multi = curl_multi_init();
6648        let code = unsafe { curl_multi_wakeup(multi) };
6649        assert_eq!(code, CURLMcode::CURLM_OK);
6650        let _ = unsafe { curl_multi_cleanup(multi) };
6651    }
6652
6653    #[test]
6654    fn multi_wakeup_null() {
6655        let code = unsafe { curl_multi_wakeup(ptr::null_mut()) };
6656        assert_eq!(code, CURLMcode::CURLM_BAD_HANDLE);
6657    }
6658
6659    #[test]
6660    fn multi_fdset_empty() {
6661        let multi = curl_multi_init();
6662        let mut max_fd: c_long = 99;
6663        let code = unsafe {
6664            curl_multi_fdset(
6665                multi,
6666                ptr::null_mut(),
6667                ptr::null_mut(),
6668                ptr::null_mut(),
6669                &raw mut max_fd,
6670            )
6671        };
6672        assert_eq!(code, CURLMcode::CURLM_OK);
6673        assert_eq!(max_fd, -1); // No fds exposed
6674        let _ = unsafe { curl_multi_cleanup(multi) };
6675    }
6676
6677    #[test]
6678    fn multi_fdset_null_handle() {
6679        let mut max_fd: c_long = 0;
6680        let code = unsafe {
6681            curl_multi_fdset(
6682                ptr::null_mut(),
6683                ptr::null_mut(),
6684                ptr::null_mut(),
6685                ptr::null_mut(),
6686                &raw mut max_fd,
6687            )
6688        };
6689        assert_eq!(code, CURLMcode::CURLM_BAD_HANDLE);
6690    }
6691
6692    #[test]
6693    fn multi_fdset_null_maxfd() {
6694        let multi = curl_multi_init();
6695        let code = unsafe {
6696            curl_multi_fdset(
6697                multi,
6698                ptr::null_mut(),
6699                ptr::null_mut(),
6700                ptr::null_mut(),
6701                ptr::null_mut(),
6702            )
6703        };
6704        assert_eq!(code, CURLMcode::CURLM_OK);
6705        let _ = unsafe { curl_multi_cleanup(multi) };
6706    }
6707
6708    #[test]
6709    fn multi_socket_action_null_handle() {
6710        let mut running: c_long = 0;
6711        let code = unsafe { curl_multi_socket_action(ptr::null_mut(), 0, 0, &raw mut running) };
6712        assert_eq!(code, CURLMcode::CURLM_BAD_HANDLE);
6713    }
6714
6715    #[test]
6716    fn multi_socket_action_specific_socket() {
6717        let multi = curl_multi_init();
6718        let mut running: c_long = 99;
6719        // Socket 5 with no action — should report 0 running handles
6720        let code = unsafe { curl_multi_socket_action(multi, 5, 0, &raw mut running) };
6721        assert_eq!(code, CURLMcode::CURLM_OK);
6722        assert_eq!(running, 0);
6723        let _ = unsafe { curl_multi_cleanup(multi) };
6724    }
6725
6726    #[test]
6727    fn multi_strerror_ok() {
6728        let msg = curl_multi_strerror(CURLMcode::CURLM_OK);
6729        assert!(!msg.is_null());
6730        let s = unsafe { CStr::from_ptr(msg) };
6731        assert_eq!(s.to_str().unwrap(), "No error");
6732    }
6733
6734    #[test]
6735    fn multi_strerror_all_codes() {
6736        let codes = [
6737            CURLMcode::CURLM_OK,
6738            CURLMcode::CURLM_BAD_HANDLE,
6739            CURLMcode::CURLM_BAD_EASY_HANDLE,
6740            CURLMcode::CURLM_OUT_OF_MEMORY,
6741            CURLMcode::CURLM_INTERNAL_ERROR,
6742            CURLMcode::CURLM_UNKNOWN_OPTION,
6743        ];
6744        for code in codes {
6745            let msg = curl_multi_strerror(code);
6746            assert!(!msg.is_null(), "multi_strerror returned null for {code:?}");
6747        }
6748    }
6749
6750    #[test]
6751    fn multi_wait_null_handle() {
6752        let mut numfds: c_long = 0;
6753        let code =
6754            unsafe { curl_multi_wait(ptr::null_mut(), ptr::null_mut(), 0, 0, &raw mut numfds) };
6755        assert_eq!(code, CURLMcode::CURLM_BAD_HANDLE);
6756    }
6757
6758    #[test]
6759    fn multi_poll_null_handle() {
6760        let mut numfds: c_long = 0;
6761        let code =
6762            unsafe { curl_multi_poll(ptr::null_mut(), ptr::null_mut(), 0, 0, &raw mut numfds) };
6763        assert_eq!(code, CURLMcode::CURLM_BAD_HANDLE);
6764    }
6765
6766    // ─── Phase 34: Utility functions ───
6767
6768    #[test]
6769    fn curl_escape_simple() {
6770        let input = c"hello world";
6771        let result = unsafe { curl_escape(input.as_ptr(), 0) };
6772        assert!(!result.is_null());
6773        let s = unsafe { CStr::from_ptr(result) }.to_str().unwrap();
6774        assert_eq!(s, "hello%20world");
6775        unsafe { curl_free(result.cast::<c_void>()) };
6776    }
6777
6778    #[test]
6779    fn curl_escape_with_length() {
6780        let input = c"abc123";
6781        let result = unsafe { curl_escape(input.as_ptr(), 3) };
6782        assert!(!result.is_null());
6783        let s = unsafe { CStr::from_ptr(result) }.to_str().unwrap();
6784        assert_eq!(s, "abc"); // Only first 3 bytes
6785        unsafe { curl_free(result.cast::<c_void>()) };
6786    }
6787
6788    #[test]
6789    fn curl_escape_special_chars() {
6790        let input = c"key=value&foo=bar";
6791        let result = unsafe { curl_escape(input.as_ptr(), 0) };
6792        assert!(!result.is_null());
6793        let s = unsafe { CStr::from_ptr(result) }.to_str().unwrap();
6794        assert_eq!(s, "key%3Dvalue%26foo%3Dbar");
6795        unsafe { curl_free(result.cast::<c_void>()) };
6796    }
6797
6798    #[test]
6799    fn curl_escape_null_returns_null() {
6800        let result = unsafe { curl_escape(ptr::null(), 0) };
6801        assert!(result.is_null());
6802    }
6803
6804    #[test]
6805    fn curl_escape_unreserved_chars_preserved() {
6806        let input = c"abc-_.~XYZ";
6807        let result = unsafe { curl_escape(input.as_ptr(), 0) };
6808        let s = unsafe { CStr::from_ptr(result) }.to_str().unwrap();
6809        assert_eq!(s, "abc-_.~XYZ");
6810        unsafe { curl_free(result.cast::<c_void>()) };
6811    }
6812
6813    #[test]
6814    fn curl_unescape_simple() {
6815        let input = c"hello%20world";
6816        let mut outlen: c_long = 0;
6817        let result = unsafe { curl_unescape(input.as_ptr(), 0, &raw mut outlen) };
6818        assert!(!result.is_null());
6819        let s = unsafe { CStr::from_ptr(result) }.to_str().unwrap();
6820        assert_eq!(s, "hello world");
6821        assert_eq!(outlen, 11);
6822        unsafe { curl_free(result.cast::<c_void>()) };
6823    }
6824
6825    #[test]
6826    fn curl_unescape_plus_to_space() {
6827        let input = c"hello+world";
6828        let result = unsafe { curl_unescape(input.as_ptr(), 0, ptr::null_mut()) };
6829        assert!(!result.is_null());
6830        let s = unsafe { CStr::from_ptr(result) }.to_str().unwrap();
6831        assert_eq!(s, "hello world");
6832        unsafe { curl_free(result.cast::<c_void>()) };
6833    }
6834
6835    #[test]
6836    fn curl_unescape_null_returns_null() {
6837        let result = unsafe { curl_unescape(ptr::null(), 0, ptr::null_mut()) };
6838        assert!(result.is_null());
6839    }
6840
6841    #[test]
6842    fn curl_unescape_with_length() {
6843        let input = c"%41%42%43DEF";
6844        let mut outlen: c_long = 0;
6845        let result = unsafe { curl_unescape(input.as_ptr(), 9, &raw mut outlen) };
6846        assert!(!result.is_null());
6847        let s = unsafe { CStr::from_ptr(result) }.to_str().unwrap();
6848        assert_eq!(s, "ABC");
6849        assert_eq!(outlen, 3);
6850        unsafe { curl_free(result.cast::<c_void>()) };
6851    }
6852
6853    #[test]
6854    fn curl_easy_escape_delegates() {
6855        let handle = curl_easy_init();
6856        let input = c"test value";
6857        let result = unsafe { curl_easy_escape(handle, input.as_ptr(), 0) };
6858        assert!(!result.is_null());
6859        let s = unsafe { CStr::from_ptr(result) }.to_str().unwrap();
6860        assert_eq!(s, "test%20value");
6861        unsafe {
6862            curl_free(result.cast::<c_void>());
6863            curl_easy_cleanup(handle);
6864        }
6865    }
6866
6867    #[test]
6868    fn curl_easy_unescape_delegates() {
6869        let handle = curl_easy_init();
6870        let input = c"test%20value";
6871        let mut outlen: c_long = 0;
6872        let result = unsafe { curl_easy_unescape(handle, input.as_ptr(), 0, &raw mut outlen) };
6873        assert!(!result.is_null());
6874        let s = unsafe { CStr::from_ptr(result) }.to_str().unwrap();
6875        assert_eq!(s, "test value");
6876        assert_eq!(outlen, 10);
6877        unsafe {
6878            curl_free(result.cast::<c_void>());
6879            curl_easy_cleanup(handle);
6880        }
6881    }
6882
6883    #[test]
6884    fn curl_escape_roundtrip() {
6885        let input = c"hello world/foo?bar=baz&qux=123";
6886        let encoded = unsafe { curl_escape(input.as_ptr(), 0) };
6887        assert!(!encoded.is_null());
6888        let decoded = unsafe { curl_unescape(encoded, 0, ptr::null_mut()) };
6889        assert!(!decoded.is_null());
6890        let s = unsafe { CStr::from_ptr(decoded) }.to_str().unwrap();
6891        assert_eq!(s, "hello world/foo?bar=baz&qux=123");
6892        unsafe {
6893            curl_free(decoded.cast::<c_void>());
6894            curl_free(encoded.cast::<c_void>());
6895        }
6896    }
6897
6898    // ─── Phase 34: curl_getdate ───
6899
6900    #[test]
6901    fn getdate_rfc2822() {
6902        let date = c"Sun, 06 Nov 1994 08:49:37 GMT";
6903        let ts = unsafe { curl_getdate(date.as_ptr(), ptr::null()) };
6904        assert_eq!(ts, 784_111_777);
6905    }
6906
6907    #[test]
6908    fn getdate_rfc850() {
6909        let date = c"Sunday, 06-Nov-94 08:49:37 GMT";
6910        let ts = unsafe { curl_getdate(date.as_ptr(), ptr::null()) };
6911        assert_eq!(ts, 784_111_777);
6912    }
6913
6914    #[test]
6915    fn getdate_asctime() {
6916        let date = c"Sun Nov  6 08:49:37 1994";
6917        let ts = unsafe { curl_getdate(date.as_ptr(), ptr::null()) };
6918        assert_eq!(ts, 784_111_777);
6919    }
6920
6921    #[test]
6922    fn getdate_null_returns_negative() {
6923        let ts = unsafe { curl_getdate(ptr::null(), ptr::null()) };
6924        assert_eq!(ts, -1);
6925    }
6926
6927    #[test]
6928    fn getdate_invalid_returns_negative() {
6929        let date = c"not a date";
6930        let ts = unsafe { curl_getdate(date.as_ptr(), ptr::null()) };
6931        assert_eq!(ts, -1);
6932    }
6933
6934    #[test]
6935    fn getdate_epoch() {
6936        let date = c"Thu, 01 Jan 1970 00:00:00 GMT";
6937        let ts = unsafe { curl_getdate(date.as_ptr(), ptr::null()) };
6938        assert_eq!(ts, 0);
6939    }
6940
6941    #[test]
6942    fn getdate_y2k() {
6943        let date = c"Sat, 01 Jan 2000 00:00:00 GMT";
6944        let ts = unsafe { curl_getdate(date.as_ptr(), ptr::null()) };
6945        assert_eq!(ts, 946_684_800);
6946    }
6947
6948    // ─── Phase 34: curl_formadd / curl_formfree ───
6949
6950    #[test]
6951    fn formadd_returns_disabled() {
6952        let result =
6953            unsafe { curl_formadd(ptr::null_mut::<*mut c_void>(), ptr::null_mut::<*mut c_void>()) };
6954        assert_eq!(result, 7); // CURL_FORMADD_DISABLED
6955    }
6956
6957    #[test]
6958    fn formfree_null_is_safe() {
6959        unsafe { curl_formfree(ptr::null_mut()) };
6960    }
6961
6962    // ─── Phase 34: New CURLOPT options ───
6963
6964    #[test]
6965    fn easy_setopt_path_as_is() {
6966        let handle = curl_easy_init();
6967        // CURLOPT_PATH_AS_IS = 234
6968        let code = unsafe { curl_easy_setopt(handle, 234, std::ptr::dangling::<c_void>()) };
6969        assert_eq!(code, CURLcode::CURLE_OK);
6970        unsafe { curl_easy_cleanup(handle) };
6971    }
6972
6973    #[test]
6974    fn easy_setopt_expect_100_timeout_ms() {
6975        let handle = curl_easy_init();
6976        // CURLOPT_EXPECT_100_TIMEOUT_MS = 227
6977        let code = unsafe { curl_easy_setopt(handle, 227, 1000 as *const c_void) };
6978        assert_eq!(code, CURLcode::CURLE_OK);
6979        unsafe { curl_easy_cleanup(handle) };
6980    }
6981
6982    #[test]
6983    fn easy_setopt_postredir() {
6984        let handle = curl_easy_init();
6985        // CURLOPT_POSTREDIR = 161, bitmask: 1=301, 2=302, 4=303, 7=all
6986        let code = unsafe { curl_easy_setopt(handle, 161, 7 as *const c_void) };
6987        assert_eq!(code, CURLcode::CURLE_OK);
6988        unsafe { curl_easy_cleanup(handle) };
6989    }
6990
6991    #[test]
6992    fn easy_setopt_transfer_encoding() {
6993        let handle = curl_easy_init();
6994        // CURLOPT_TRANSFER_ENCODING = 207
6995        let code = unsafe { curl_easy_setopt(handle, 207, std::ptr::dangling::<c_void>()) };
6996        assert_eq!(code, CURLcode::CURLE_OK);
6997        unsafe { curl_easy_cleanup(handle) };
6998    }
6999
7000    #[test]
7001    fn easy_setopt_dns_shuffle_addresses() {
7002        let handle = curl_easy_init();
7003        // CURLOPT_DNS_SHUFFLE_ADDRESSES = 275
7004        let code = unsafe { curl_easy_setopt(handle, 275, std::ptr::dangling::<c_void>()) };
7005        assert_eq!(code, CURLcode::CURLE_OK);
7006        unsafe { curl_easy_cleanup(handle) };
7007    }
7008
7009    #[test]
7010    fn easy_setopt_httpproxytunnel() {
7011        let handle = curl_easy_init();
7012        // CURLOPT_HTTPPROXYTUNNEL = 61
7013        let code = unsafe { curl_easy_setopt(handle, 61, std::ptr::dangling::<c_void>()) };
7014        assert_eq!(code, CURLcode::CURLE_OK);
7015        unsafe { curl_easy_cleanup(handle) };
7016    }
7017
7018    #[test]
7019    fn easy_setopt_maxfilesize() {
7020        let handle = curl_easy_init();
7021        // CURLOPT_MAXFILESIZE = 114
7022        let code = unsafe { curl_easy_setopt(handle, 114, 1_048_576 as *const c_void) };
7023        assert_eq!(code, CURLcode::CURLE_OK);
7024        unsafe { curl_easy_cleanup(handle) };
7025    }
7026
7027    #[test]
7028    fn easy_setopt_maxfilesize_large() {
7029        let handle = curl_easy_init();
7030        // CURLOPT_MAXFILESIZE_LARGE = 30117
7031        let code = unsafe { curl_easy_setopt(handle, 30117, 1_048_576 as *const c_void) };
7032        assert_eq!(code, CURLcode::CURLE_OK);
7033        unsafe { curl_easy_cleanup(handle) };
7034    }
7035
7036    #[test]
7037    fn easy_setopt_hsts() {
7038        let handle = curl_easy_init();
7039        let path = c"/tmp/hsts.txt";
7040        // CURLOPT_HSTS = 10300
7041        let code = unsafe { curl_easy_setopt(handle, 10300, path.as_ptr().cast::<c_void>()) };
7042        assert_eq!(code, CURLcode::CURLE_OK);
7043        unsafe { curl_easy_cleanup(handle) };
7044    }
7045
7046    #[test]
7047    fn easy_setopt_cookielist() {
7048        let handle = curl_easy_init();
7049        let cmd = c"ALL";
7050        // CURLOPT_COOKIELIST = 10135
7051        let code = unsafe { curl_easy_setopt(handle, 10135, cmd.as_ptr().cast::<c_void>()) };
7052        assert_eq!(code, CURLcode::CURLE_OK);
7053        unsafe { curl_easy_cleanup(handle) };
7054    }
7055
7056    #[test]
7057    fn easy_setopt_errorbuffer() {
7058        let handle = curl_easy_init();
7059        let mut buf = [0u8; 256];
7060        // CURLOPT_ERRORBUFFER = 10010
7061        let code = unsafe {
7062            curl_easy_setopt(handle, 10010, buf.as_mut_ptr().cast::<c_void>().cast_const())
7063        };
7064        assert_eq!(code, CURLcode::CURLE_OK);
7065        unsafe { curl_easy_cleanup(handle) };
7066    }
7067
7068    #[test]
7069    fn easy_setopt_stderr() {
7070        let handle = curl_easy_init();
7071        // CURLOPT_STDERR = 10037
7072        let code = unsafe { curl_easy_setopt(handle, 10037, ptr::null()) };
7073        assert_eq!(code, CURLcode::CURLE_OK);
7074        unsafe { curl_easy_cleanup(handle) };
7075    }
7076
7077    #[test]
7078    fn easy_setopt_protocols_str() {
7079        let handle = curl_easy_init();
7080        let proto = c"http,https,ftp";
7081        // CURLOPT_PROTOCOLS_STR = 10318
7082        let code = unsafe { curl_easy_setopt(handle, 10318, proto.as_ptr().cast::<c_void>()) };
7083        assert_eq!(code, CURLcode::CURLE_OK);
7084        unsafe { curl_easy_cleanup(handle) };
7085    }
7086
7087    #[test]
7088    fn easy_setopt_redir_protocols_str() {
7089        let handle = curl_easy_init();
7090        let proto = c"http,https";
7091        // CURLOPT_REDIR_PROTOCOLS_STR = 10319
7092        let code = unsafe { curl_easy_setopt(handle, 10319, proto.as_ptr().cast::<c_void>()) };
7093        assert_eq!(code, CURLcode::CURLE_OK);
7094        unsafe { curl_easy_cleanup(handle) };
7095    }
7096
7097    #[test]
7098    fn easy_setopt_proxy_cainfo() {
7099        let handle = curl_easy_init();
7100        let path = c"/tmp/proxy-ca.pem";
7101        // CURLOPT_PROXY_CAINFO = 10246
7102        let code = unsafe { curl_easy_setopt(handle, 10246, path.as_ptr().cast::<c_void>()) };
7103        assert_eq!(code, CURLcode::CURLE_OK);
7104        unsafe { curl_easy_cleanup(handle) };
7105    }
7106
7107    #[test]
7108    fn easy_setopt_proxy_ssl_verifyhost() {
7109        let handle = curl_easy_init();
7110        // CURLOPT_PROXY_SSL_VERIFYHOST = 249
7111        let code = unsafe { curl_easy_setopt(handle, 249, 2 as *const c_void) };
7112        assert_eq!(code, CURLcode::CURLE_OK);
7113        unsafe { curl_easy_cleanup(handle) };
7114    }
7115
7116    // ─── Phase 34: New CURLINFO codes ───
7117
7118    #[test]
7119    fn easy_getinfo_filetime_returns_unknown() {
7120        let handle = curl_easy_init();
7121        // Need a completed transfer for getinfo — create a minimal response
7122        let h = unsafe { &mut *handle.cast::<EasyHandle>() };
7123        h.last_response = Some(liburlx::Response::new(
7124            200,
7125            std::collections::HashMap::new(),
7126            Vec::new(),
7127            "http://example.com".to_string(),
7128        ));
7129
7130        let mut val: c_long = 0;
7131        let result = unsafe {
7132            curl_easy_getinfo(handle, 0x20_000E, ptr::from_mut(&mut val).cast::<c_void>())
7133        };
7134        assert_eq!(result, CURLcode::CURLE_OK);
7135        assert_eq!(val, -1); // Unknown filetime
7136
7137        unsafe { curl_easy_cleanup(handle) };
7138    }
7139
7140    #[test]
7141    fn easy_getinfo_content_length_download() {
7142        let handle = curl_easy_init();
7143        let h = unsafe { &mut *handle.cast::<EasyHandle>() };
7144        h.last_response = Some(liburlx::Response::new(
7145            200,
7146            std::collections::HashMap::new(),
7147            b"hello".to_vec(),
7148            "http://example.com".to_string(),
7149        ));
7150
7151        let mut val: f64 = 0.0;
7152        let result = unsafe {
7153            curl_easy_getinfo(handle, 0x30_000F, ptr::from_mut(&mut val).cast::<c_void>())
7154        };
7155        assert_eq!(result, CURLcode::CURLE_OK);
7156        assert!((val - 5.0).abs() < f64::EPSILON);
7157
7158        unsafe { curl_easy_cleanup(handle) };
7159    }
7160
7161    #[test]
7162    fn easy_getinfo_os_errno() {
7163        let handle = curl_easy_init();
7164        let h = unsafe { &mut *handle.cast::<EasyHandle>() };
7165        h.last_response = Some(liburlx::Response::new(
7166            200,
7167            std::collections::HashMap::new(),
7168            Vec::new(),
7169            "http://example.com".to_string(),
7170        ));
7171
7172        let mut val: c_long = 99;
7173        let result = unsafe {
7174            curl_easy_getinfo(handle, 0x20_0019, ptr::from_mut(&mut val).cast::<c_void>())
7175        };
7176        assert_eq!(result, CURLcode::CURLE_OK);
7177        assert_eq!(val, 0);
7178
7179        unsafe { curl_easy_cleanup(handle) };
7180    }
7181
7182    #[test]
7183    fn easy_getinfo_primary_ip() {
7184        let handle = curl_easy_init();
7185        let h = unsafe { &mut *handle.cast::<EasyHandle>() };
7186        h.last_response = Some(liburlx::Response::new(
7187            200,
7188            std::collections::HashMap::new(),
7189            Vec::new(),
7190            "http://example.com".to_string(),
7191        ));
7192
7193        let mut val: *const c_char = ptr::null();
7194        let result = unsafe {
7195            curl_easy_getinfo(handle, 0x10_0020, ptr::from_mut(&mut val).cast::<c_void>())
7196        };
7197        assert_eq!(result, CURLcode::CURLE_OK);
7198        assert!(!val.is_null());
7199
7200        unsafe { curl_easy_cleanup(handle) };
7201    }
7202
7203    #[test]
7204    fn easy_getinfo_num_connects() {
7205        let handle = curl_easy_init();
7206        let h = unsafe { &mut *handle.cast::<EasyHandle>() };
7207        h.last_response = Some(liburlx::Response::new(
7208            200,
7209            std::collections::HashMap::new(),
7210            Vec::new(),
7211            "http://example.com".to_string(),
7212        ));
7213
7214        let mut val: c_long = 0;
7215        let result = unsafe {
7216            curl_easy_getinfo(handle, 0x20_001A, ptr::from_mut(&mut val).cast::<c_void>())
7217        };
7218        assert_eq!(result, CURLcode::CURLE_OK);
7219        assert_eq!(val, 1);
7220
7221        unsafe { curl_easy_cleanup(handle) };
7222    }
7223
7224    #[test]
7225    fn easy_getinfo_local_ip() {
7226        let handle = curl_easy_init();
7227        let h = unsafe { &mut *handle.cast::<EasyHandle>() };
7228        h.last_response = Some(liburlx::Response::new(
7229            200,
7230            std::collections::HashMap::new(),
7231            Vec::new(),
7232            "http://example.com".to_string(),
7233        ));
7234
7235        let mut val: *const c_char = ptr::null();
7236        let result = unsafe {
7237            curl_easy_getinfo(handle, 0x10_0029, ptr::from_mut(&mut val).cast::<c_void>())
7238        };
7239        assert_eq!(result, CURLcode::CURLE_OK);
7240        assert!(!val.is_null());
7241
7242        unsafe { curl_easy_cleanup(handle) };
7243    }
7244
7245    #[test]
7246    fn easy_getinfo_redirect_url_none() {
7247        let handle = curl_easy_init();
7248        let h = unsafe { &mut *handle.cast::<EasyHandle>() };
7249        h.last_response = Some(liburlx::Response::new(
7250            200,
7251            std::collections::HashMap::new(),
7252            Vec::new(),
7253            "http://example.com".to_string(),
7254        ));
7255
7256        let mut val: *const c_char = std::ptr::dangling::<c_char>();
7257        let result = unsafe {
7258            curl_easy_getinfo(handle, 0x10_0031, ptr::from_mut(&mut val).cast::<c_void>())
7259        };
7260        assert_eq!(result, CURLcode::CURLE_OK);
7261        assert!(val.is_null()); // No redirect
7262
7263        unsafe { curl_easy_cleanup(handle) };
7264    }
7265
7266    #[test]
7267    fn easy_getinfo_redirect_url_present() {
7268        let handle = curl_easy_init();
7269        let h = unsafe { &mut *handle.cast::<EasyHandle>() };
7270        let mut headers = std::collections::HashMap::new();
7271        let _ = headers.insert("location".to_string(), "http://other.com/".to_string());
7272        h.last_response = Some(liburlx::Response::new(
7273            302,
7274            headers,
7275            Vec::new(),
7276            "http://example.com".to_string(),
7277        ));
7278
7279        let mut val: *const c_char = ptr::null();
7280        let result = unsafe {
7281            curl_easy_getinfo(handle, 0x10_0031, ptr::from_mut(&mut val).cast::<c_void>())
7282        };
7283        assert_eq!(result, CURLcode::CURLE_OK);
7284        assert!(!val.is_null());
7285
7286        unsafe { curl_easy_cleanup(handle) };
7287    }
7288
7289    #[test]
7290    fn easy_getinfo_condition_unmet_false() {
7291        let handle = curl_easy_init();
7292        let h = unsafe { &mut *handle.cast::<EasyHandle>() };
7293        h.last_response = Some(liburlx::Response::new(
7294            200,
7295            std::collections::HashMap::new(),
7296            Vec::new(),
7297            "http://example.com".to_string(),
7298        ));
7299
7300        let mut val: c_long = 99;
7301        let result = unsafe {
7302            curl_easy_getinfo(handle, 0x20_0035, ptr::from_mut(&mut val).cast::<c_void>())
7303        };
7304        assert_eq!(result, CURLcode::CURLE_OK);
7305        assert_eq!(val, 0);
7306
7307        unsafe { curl_easy_cleanup(handle) };
7308    }
7309
7310    #[test]
7311    fn easy_getinfo_condition_unmet_304() {
7312        let handle = curl_easy_init();
7313        let h = unsafe { &mut *handle.cast::<EasyHandle>() };
7314        h.last_response = Some(liburlx::Response::new(
7315            304,
7316            std::collections::HashMap::new(),
7317            Vec::new(),
7318            "http://example.com".to_string(),
7319        ));
7320
7321        let mut val: c_long = 0;
7322        let result = unsafe {
7323            curl_easy_getinfo(handle, 0x20_0035, ptr::from_mut(&mut val).cast::<c_void>())
7324        };
7325        assert_eq!(result, CURLcode::CURLE_OK);
7326        assert_eq!(val, 1);
7327
7328        unsafe { curl_easy_cleanup(handle) };
7329    }
7330
7331    #[test]
7332    fn easy_getinfo_local_port() {
7333        let handle = curl_easy_init();
7334        let h = unsafe { &mut *handle.cast::<EasyHandle>() };
7335        h.last_response = Some(liburlx::Response::new(
7336            200,
7337            std::collections::HashMap::new(),
7338            Vec::new(),
7339            "http://example.com".to_string(),
7340        ));
7341
7342        let mut val: c_long = 99;
7343        let result = unsafe {
7344            curl_easy_getinfo(handle, 0x20_0042, ptr::from_mut(&mut val).cast::<c_void>())
7345        };
7346        assert_eq!(result, CURLcode::CURLE_OK);
7347        assert_eq!(val, 0);
7348
7349        unsafe { curl_easy_cleanup(handle) };
7350    }
7351
7352    // ─── Phase 34: Internal helpers ───
7353
7354    #[test]
7355    fn percent_encode_all_bytes() {
7356        assert_eq!(percent_encode(b"abc"), "abc");
7357        assert_eq!(percent_encode(b" "), "%20");
7358        assert_eq!(percent_encode(b"\x00"), "%00");
7359        assert_eq!(percent_encode(b"\xFF"), "%FF");
7360        assert_eq!(percent_encode(b"a b"), "a%20b");
7361    }
7362
7363    #[test]
7364    fn percent_decode_all() {
7365        assert_eq!(percent_decode(b"abc"), b"abc");
7366        assert_eq!(percent_decode(b"%20"), b" ");
7367        assert_eq!(percent_decode(b"a+b"), b"a b");
7368        assert_eq!(percent_decode(b"%00"), b"\x00");
7369        assert_eq!(percent_decode(b"%FF"), b"\xFF");
7370        assert_eq!(percent_decode(b"%2f"), b"/"); // lowercase hex
7371    }
7372
7373    #[test]
7374    fn percent_decode_invalid_hex() {
7375        // Invalid hex sequences should be kept as-is
7376        assert_eq!(percent_decode(b"%ZZ"), b"%ZZ");
7377        assert_eq!(percent_decode(b"%2"), b"%2"); // Truncated
7378    }
7379
7380    #[test]
7381    fn date_parsing_internal() {
7382        // Test internal date helpers
7383        assert_eq!(month_from_name("Jan"), Some(0));
7384        assert_eq!(month_from_name("Dec"), Some(11));
7385        assert_eq!(month_from_name("Bad"), None);
7386
7387        assert!(is_leap_year(2000));
7388        assert!(!is_leap_year(1900));
7389        assert!(is_leap_year(2004));
7390        assert!(!is_leap_year(2001));
7391
7392        // Epoch
7393        assert_eq!(date_to_timestamp(1970, 0, 1, 0, 0, 0), 0);
7394        // One day after epoch
7395        assert_eq!(date_to_timestamp(1970, 0, 2, 0, 0, 0), 86400);
7396    }
7397
7398    // ─── Phase 46: FFI Expansion III ───
7399
7400    #[test]
7401    fn global_init_cleanup() {
7402        let code = curl_global_init(CURL_GLOBAL_ALL);
7403        assert_eq!(code, CURLcode::CURLE_OK);
7404        curl_global_cleanup();
7405    }
7406
7407    #[test]
7408    fn global_init_default() {
7409        let code = curl_global_init(CURL_GLOBAL_DEFAULT);
7410        assert_eq!(code, CURLcode::CURLE_OK);
7411    }
7412
7413    #[test]
7414    fn version_info_returns_valid() {
7415        let info = curl_version_info(0);
7416        assert!(!info.is_null());
7417        // SAFETY: info is a valid pointer from curl_version_info
7418        let info = unsafe { &*info };
7419        assert_eq!(info.age, 0);
7420        assert!(!info.version.is_null());
7421        // Check features include SSL and HTTP2
7422        assert_ne!(info.features & CURL_VERSION_SSL, 0);
7423        assert_ne!(info.features & CURL_VERSION_HTTP2, 0);
7424        assert_ne!(info.features & CURL_VERSION_PSL, 0);
7425        // Check protocols array
7426        assert!(!info.protocols.is_null());
7427        // First protocol should be "http"
7428        // SAFETY: protocols[0] is a valid pointer
7429        let first = unsafe { CStr::from_ptr(*info.protocols) };
7430        assert_eq!(first.to_str().unwrap(), "http");
7431    }
7432
7433    #[test]
7434    fn easy_pause_noop() {
7435        let handle = curl_easy_init();
7436        assert!(!handle.is_null());
7437        let code = curl_easy_pause(handle, CURLPAUSE_ALL);
7438        assert_eq!(code, CURLcode::CURLE_OK);
7439        let code = curl_easy_pause(handle, CURLPAUSE_CONT);
7440        assert_eq!(code, CURLcode::CURLE_OK);
7441        unsafe { curl_easy_cleanup(handle) };
7442    }
7443
7444    #[test]
7445    fn easy_upkeep_noop() {
7446        let handle = curl_easy_init();
7447        let code = curl_easy_upkeep(handle);
7448        assert_eq!(code, CURLcode::CURLE_OK);
7449        unsafe { curl_easy_cleanup(handle) };
7450    }
7451
7452    #[test]
7453    fn multi_assign_noop() {
7454        let multi = curl_multi_init();
7455        assert_eq!(curl_multi_assign(multi, 0, ptr::null_mut()), CURLMcode::CURLM_OK);
7456        let _ = unsafe { curl_multi_cleanup(multi) };
7457    }
7458
7459    #[test]
7460    fn easy_setopt_haproxyprotocol() {
7461        let handle = curl_easy_init();
7462        // CURLOPT_HAPROXYPROTOCOL = 274
7463        let code =
7464            unsafe { curl_easy_setopt(handle, 274, std::ptr::without_provenance::<c_void>(1)) };
7465        assert_eq!(code, CURLcode::CURLE_OK);
7466        unsafe { curl_easy_cleanup(handle) };
7467    }
7468
7469    #[test]
7470    fn easy_setopt_httppost_deprecated() {
7471        let handle = curl_easy_init();
7472        // CURLOPT_HTTPPOST = 10024 (deprecated, should accept)
7473        let code = unsafe { curl_easy_setopt(handle, 10024, ptr::null()) };
7474        assert_eq!(code, CURLcode::CURLE_OK);
7475        unsafe { curl_easy_cleanup(handle) };
7476    }
7477
7478    #[test]
7479    fn easy_setopt_abstract_unix_socket() {
7480        let handle = curl_easy_init();
7481        let path = std::ffi::CString::new("/tmp/test.sock").unwrap();
7482        // CURLOPT_ABSTRACT_UNIX_SOCKET = 10264
7483        let code = unsafe { curl_easy_setopt(handle, 10264, path.as_ptr().cast::<c_void>()) };
7484        assert_eq!(code, CURLcode::CURLE_OK);
7485        unsafe { curl_easy_cleanup(handle) };
7486    }
7487
7488    #[test]
7489    fn easy_setopt_doh_ssl_verifypeer() {
7490        let handle = curl_easy_init();
7491        // CURLOPT_DOH_SSL_VERIFYPEER = 306
7492        let code =
7493            unsafe { curl_easy_setopt(handle, 306, std::ptr::without_provenance::<c_void>(1)) };
7494        assert_eq!(code, CURLcode::CURLE_OK);
7495        unsafe { curl_easy_cleanup(handle) };
7496    }
7497
7498    #[test]
7499    fn easy_setopt_buffersize() {
7500        let handle = curl_easy_init();
7501        // CURLOPT_BUFFERSIZE = 98
7502        let code = unsafe { curl_easy_setopt(handle, 98, 65536 as *const c_void) };
7503        assert_eq!(code, CURLcode::CURLE_OK);
7504        unsafe { curl_easy_cleanup(handle) };
7505    }
7506
7507    #[test]
7508    fn easy_setopt_maxlifetime_conn() {
7509        let handle = curl_easy_init();
7510        // CURLOPT_MAXLIFETIME_CONN = 314
7511        let code = unsafe { curl_easy_setopt(handle, 314, 300 as *const c_void) };
7512        assert_eq!(code, CURLcode::CURLE_OK);
7513        unsafe { curl_easy_cleanup(handle) };
7514    }
7515
7516    #[test]
7517    fn new_curlcodes_strerror() {
7518        assert_ne!(curl_easy_strerror(CURLcode::CURLE_FILESIZE_EXCEEDED), ptr::null());
7519        assert_ne!(curl_easy_strerror(CURLcode::CURLE_TOO_MANY_REDIRECTS), ptr::null());
7520        assert_ne!(curl_easy_strerror(CURLcode::CURLE_HTTP3), ptr::null());
7521        assert_ne!(curl_easy_strerror(CURLcode::CURLE_PARTIAL_FILE), ptr::null());
7522        assert_ne!(curl_easy_strerror(CURLcode::CURLE_RANGE_ERROR), ptr::null());
7523        assert_ne!(curl_easy_strerror(CURLcode::CURLE_AGAIN), ptr::null());
7524    }
7525
7526    #[test]
7527    fn curlinfo_enum_has_timing_t_variants() {
7528        // Verify the _T timing CURLINFO codes exist
7529        let _ = CURLINFO::CURLINFO_TOTAL_TIME_T;
7530        let _ = CURLINFO::CURLINFO_NAMELOOKUP_TIME_T;
7531        let _ = CURLINFO::CURLINFO_CONNECT_TIME_T;
7532        let _ = CURLINFO::CURLINFO_PRETRANSFER_TIME_T;
7533        let _ = CURLINFO::CURLINFO_STARTTRANSFER_TIME_T;
7534        let _ = CURLINFO::CURLINFO_REDIRECT_TIME_T;
7535        let _ = CURLINFO::CURLINFO_APPCONNECT_TIME_T;
7536        let _ = CURLINFO::CURLINFO_SIZE_UPLOAD_T;
7537        let _ = CURLINFO::CURLINFO_SIZE_DOWNLOAD_T;
7538        let _ = CURLINFO::CURLINFO_SPEED_DOWNLOAD_T;
7539        let _ = CURLINFO::CURLINFO_SPEED_UPLOAD_T;
7540        let _ = CURLINFO::CURLINFO_REDIRECT_TIME;
7541        let _ = CURLINFO::CURLINFO_RETRY_AFTER;
7542    }
7543
7544    #[test]
7545    fn global_constants_defined() {
7546        assert_eq!(CURL_GLOBAL_SSL, 1);
7547        assert_eq!(CURL_GLOBAL_WIN32, 2);
7548        assert_eq!(CURL_GLOBAL_ALL, 3);
7549        assert_eq!(CURL_GLOBAL_DEFAULT, 3);
7550        assert_eq!(CURLPAUSE_RECV, 1);
7551        assert_eq!(CURLPAUSE_SEND, 4);
7552        assert_eq!(CURLPAUSE_ALL, 5);
7553        assert_eq!(CURLPAUSE_CONT, 0);
7554    }
7555
7556    // ─── Phase 55: Blob cert options ───
7557
7558    #[test]
7559    fn easy_setopt_cainfo_blob() {
7560        let handle = curl_easy_init();
7561        let pem_data = b"-----BEGIN CERTIFICATE-----\ntest\n-----END CERTIFICATE-----\n";
7562        let blob =
7563            curl_blob { data: pem_data.as_ptr().cast::<c_void>(), len: pem_data.len(), flags: 0 };
7564        // CURLOPT_CAINFO_BLOB = 40309
7565        let code =
7566            unsafe { curl_easy_setopt(handle, 40309, ptr::from_ref(&blob).cast::<c_void>()) };
7567        assert_eq!(code, CURLcode::CURLE_OK);
7568        unsafe { curl_easy_cleanup(handle) };
7569    }
7570
7571    #[test]
7572    fn easy_setopt_sslcert_blob() {
7573        let handle = curl_easy_init();
7574        let pem_data = b"-----BEGIN CERTIFICATE-----\ntest\n-----END CERTIFICATE-----\n";
7575        let blob =
7576            curl_blob { data: pem_data.as_ptr().cast::<c_void>(), len: pem_data.len(), flags: 0 };
7577        // CURLOPT_SSLCERT_BLOB = 40291
7578        let code =
7579            unsafe { curl_easy_setopt(handle, 40291, ptr::from_ref(&blob).cast::<c_void>()) };
7580        assert_eq!(code, CURLcode::CURLE_OK);
7581        unsafe { curl_easy_cleanup(handle) };
7582    }
7583
7584    #[test]
7585    fn easy_setopt_sslkey_blob() {
7586        let handle = curl_easy_init();
7587        let pem_data = b"-----BEGIN PRIVATE KEY-----\ntest\n-----END PRIVATE KEY-----\n";
7588        let blob =
7589            curl_blob { data: pem_data.as_ptr().cast::<c_void>(), len: pem_data.len(), flags: 0 };
7590        // CURLOPT_SSLKEY_BLOB = 40292
7591        let code =
7592            unsafe { curl_easy_setopt(handle, 40292, ptr::from_ref(&blob).cast::<c_void>()) };
7593        assert_eq!(code, CURLcode::CURLE_OK);
7594        unsafe { curl_easy_cleanup(handle) };
7595    }
7596
7597    #[test]
7598    fn easy_setopt_blob_null_clears_setting() {
7599        let handle = curl_easy_init();
7600        // Null value pointer clears the setting (matches curl behavior)
7601        let code = unsafe { curl_easy_setopt(handle, 40309, ptr::null()) };
7602        assert_eq!(code, CURLcode::CURLE_OK);
7603        unsafe { curl_easy_cleanup(handle) };
7604    }
7605
7606    #[test]
7607    fn easy_setopt_cainfo_blob_empty_clears_setting() {
7608        let handle = curl_easy_init();
7609        // Empty blob (null data, 0 len) clears the setting (matches curl precheck)
7610        let blob = curl_blob { data: ptr::null(), len: 0, flags: 0 };
7611        let code =
7612            unsafe { curl_easy_setopt(handle, 40309, ptr::from_ref(&blob).cast::<c_void>()) };
7613        assert_eq!(code, CURLcode::CURLE_OK);
7614        unsafe { curl_easy_cleanup(handle) };
7615    }
7616
7617    #[test]
7618    fn easy_setopt_blob_null_data_returns_error() {
7619        let handle = curl_easy_init();
7620        let blob = curl_blob { data: ptr::null(), len: 10, flags: 0 };
7621        let code =
7622            unsafe { curl_easy_setopt(handle, 40291, ptr::from_ref(&blob).cast::<c_void>()) };
7623        assert_eq!(code, CURLcode::CURLE_BAD_FUNCTION_ARGUMENT);
7624        unsafe { curl_easy_cleanup(handle) };
7625    }
7626
7627    #[test]
7628    fn easy_setopt_blob_zero_len_returns_error() {
7629        let handle = curl_easy_init();
7630        let data = b"some data";
7631        let blob = curl_blob { data: data.as_ptr().cast::<c_void>(), len: 0, flags: 0 };
7632        let code =
7633            unsafe { curl_easy_setopt(handle, 40292, ptr::from_ref(&blob).cast::<c_void>()) };
7634        assert_eq!(code, CURLcode::CURLE_BAD_FUNCTION_ARGUMENT);
7635        unsafe { curl_easy_cleanup(handle) };
7636    }
7637
7638    // ─── Phase 55: FTP options ───
7639
7640    #[test]
7641    fn easy_setopt_ftpport() {
7642        let handle = curl_easy_init();
7643        let addr = c"-";
7644        let code = unsafe { curl_easy_setopt(handle, 10017, addr.as_ptr().cast::<c_void>()) };
7645        assert_eq!(code, CURLcode::CURLE_OK);
7646        unsafe { curl_easy_cleanup(handle) };
7647    }
7648
7649    #[test]
7650    fn easy_setopt_ftp_use_epsv() {
7651        let handle = curl_easy_init();
7652        let code = unsafe { curl_easy_setopt(handle, 85, ptr::null()) };
7653        assert_eq!(code, CURLcode::CURLE_OK);
7654        unsafe { curl_easy_cleanup(handle) };
7655    }
7656
7657    #[test]
7658    fn easy_setopt_ftp_use_eprt() {
7659        let handle = curl_easy_init();
7660        let code = unsafe { curl_easy_setopt(handle, 106, std::ptr::dangling::<c_void>()) };
7661        assert_eq!(code, CURLcode::CURLE_OK);
7662        unsafe { curl_easy_cleanup(handle) };
7663    }
7664
7665    #[test]
7666    fn easy_setopt_ftp_create_missing_dirs() {
7667        let handle = curl_easy_init();
7668        let code = unsafe { curl_easy_setopt(handle, 110, std::ptr::dangling::<c_void>()) };
7669        assert_eq!(code, CURLcode::CURLE_OK);
7670        unsafe { curl_easy_cleanup(handle) };
7671    }
7672
7673    #[test]
7674    fn easy_setopt_ftp_skip_pasv_ip() {
7675        let handle = curl_easy_init();
7676        let code = unsafe { curl_easy_setopt(handle, 137, std::ptr::dangling::<c_void>()) };
7677        assert_eq!(code, CURLcode::CURLE_OK);
7678        unsafe { curl_easy_cleanup(handle) };
7679    }
7680
7681    #[test]
7682    fn easy_setopt_ftp_filemethod() {
7683        let handle = curl_easy_init();
7684        // CURLFTPMETHOD_SINGLECWD = 3
7685        let code = unsafe { curl_easy_setopt(handle, 138, 3_usize as *const c_void) };
7686        assert_eq!(code, CURLcode::CURLE_OK);
7687        unsafe { curl_easy_cleanup(handle) };
7688    }
7689
7690    #[test]
7691    fn easy_setopt_ftp_account() {
7692        let handle = curl_easy_init();
7693        let acct = c"myaccount";
7694        let code = unsafe { curl_easy_setopt(handle, 10134, acct.as_ptr().cast::<c_void>()) };
7695        assert_eq!(code, CURLcode::CURLE_OK);
7696        unsafe { curl_easy_cleanup(handle) };
7697    }
7698
7699    #[test]
7700    fn easy_setopt_use_ssl() {
7701        let handle = curl_easy_init();
7702        // CURLUSESSL_TRY = 2
7703        let code = unsafe { curl_easy_setopt(handle, 119, 2_usize as *const c_void) };
7704        assert_eq!(code, CURLcode::CURLE_OK);
7705        unsafe { curl_easy_cleanup(handle) };
7706    }
7707
7708    // ─── Phase 55: SSH options ───
7709
7710    #[test]
7711    fn easy_setopt_ssh_auth_types() {
7712        let handle = curl_easy_init();
7713        // CURLSSH_AUTH_PUBLICKEY = 1 | CURLSSH_AUTH_PASSWORD = 2
7714        let code = unsafe { curl_easy_setopt(handle, 151, 3_usize as *const c_void) };
7715        assert_eq!(code, CURLcode::CURLE_OK);
7716        unsafe { curl_easy_cleanup(handle) };
7717    }
7718
7719    #[test]
7720    fn easy_setopt_ssh_public_keyfile() {
7721        let handle = curl_easy_init();
7722        let path = c"/home/user/.ssh/id_rsa.pub";
7723        let code = unsafe { curl_easy_setopt(handle, 10152, path.as_ptr().cast::<c_void>()) };
7724        assert_eq!(code, CURLcode::CURLE_OK);
7725        unsafe { curl_easy_cleanup(handle) };
7726    }
7727
7728    #[test]
7729    fn easy_setopt_ssh_private_keyfile() {
7730        let handle = curl_easy_init();
7731        let path = c"/home/user/.ssh/id_rsa";
7732        let code = unsafe { curl_easy_setopt(handle, 10153, path.as_ptr().cast::<c_void>()) };
7733        assert_eq!(code, CURLcode::CURLE_OK);
7734        unsafe { curl_easy_cleanup(handle) };
7735    }
7736
7737    #[test]
7738    fn easy_setopt_ssh_knownhosts() {
7739        let handle = curl_easy_init();
7740        let path = c"/home/user/.ssh/known_hosts";
7741        let code = unsafe { curl_easy_setopt(handle, 10183, path.as_ptr().cast::<c_void>()) };
7742        assert_eq!(code, CURLcode::CURLE_OK);
7743        unsafe { curl_easy_cleanup(handle) };
7744    }
7745
7746    #[test]
7747    fn easy_setopt_ssh_host_public_key_sha256() {
7748        let handle = curl_easy_init();
7749        let fp = c"AAAA+bbb/ccc=";
7750        let code = unsafe { curl_easy_setopt(handle, 10270, fp.as_ptr().cast::<c_void>()) };
7751        assert_eq!(code, CURLcode::CURLE_OK);
7752        unsafe { curl_easy_cleanup(handle) };
7753    }
7754
7755    // ─── Phase 55: Proxy options ───
7756
7757    #[test]
7758    fn easy_setopt_proxyport() {
7759        let handle = curl_easy_init();
7760        let code = unsafe { curl_easy_setopt(handle, 59, 8080_usize as *const c_void) };
7761        assert_eq!(code, CURLcode::CURLE_OK);
7762        unsafe { curl_easy_cleanup(handle) };
7763    }
7764
7765    #[test]
7766    fn easy_setopt_proxytype() {
7767        let handle = curl_easy_init();
7768        // CURLPROXY_SOCKS5 = 5
7769        let code = unsafe { curl_easy_setopt(handle, 101, 5_usize as *const c_void) };
7770        assert_eq!(code, CURLcode::CURLE_OK);
7771        unsafe { curl_easy_cleanup(handle) };
7772    }
7773
7774    #[test]
7775    fn easy_setopt_proxyusername() {
7776        let handle = curl_easy_init();
7777        let user = c"proxyuser";
7778        let code = unsafe { curl_easy_setopt(handle, 10175, user.as_ptr().cast::<c_void>()) };
7779        assert_eq!(code, CURLcode::CURLE_OK);
7780        unsafe { curl_easy_cleanup(handle) };
7781    }
7782
7783    #[test]
7784    fn easy_setopt_proxypassword() {
7785        let handle = curl_easy_init();
7786        let pass = c"proxypass";
7787        let code = unsafe { curl_easy_setopt(handle, 10176, pass.as_ptr().cast::<c_void>()) };
7788        assert_eq!(code, CURLcode::CURLE_OK);
7789        unsafe { curl_easy_cleanup(handle) };
7790    }
7791
7792    #[test]
7793    fn easy_setopt_pre_proxy() {
7794        let handle = curl_easy_init();
7795        let url = c"socks5://proxy.example.com:1080";
7796        let code = unsafe { curl_easy_setopt(handle, 10262, url.as_ptr().cast::<c_void>()) };
7797        assert_eq!(code, CURLcode::CURLE_OK);
7798        unsafe { curl_easy_cleanup(handle) };
7799    }
7800
7801    #[test]
7802    fn easy_setopt_socks5_auth() {
7803        let handle = curl_easy_init();
7804        let code = unsafe { curl_easy_setopt(handle, 267, 3_usize as *const c_void) };
7805        assert_eq!(code, CURLcode::CURLE_OK);
7806        unsafe { curl_easy_cleanup(handle) };
7807    }
7808
7809    #[test]
7810    fn easy_setopt_maxconnects() {
7811        let handle = curl_easy_init();
7812        let code = unsafe { curl_easy_setopt(handle, 71, 5_usize as *const c_void) };
7813        assert_eq!(code, CURLcode::CURLE_OK);
7814        unsafe { curl_easy_cleanup(handle) };
7815    }
7816
7817    #[test]
7818    fn easy_setopt_pipewait() {
7819        let handle = curl_easy_init();
7820        let code = unsafe { curl_easy_setopt(handle, 237, std::ptr::dangling::<c_void>()) };
7821        assert_eq!(code, CURLcode::CURLE_OK);
7822        unsafe { curl_easy_cleanup(handle) };
7823    }
7824
7825    #[test]
7826    fn easy_setopt_stream_weight() {
7827        let handle = curl_easy_init();
7828        let code = unsafe { curl_easy_setopt(handle, 239, 16_usize as *const c_void) };
7829        assert_eq!(code, CURLcode::CURLE_OK);
7830        unsafe { curl_easy_cleanup(handle) };
7831    }
7832
7833    #[test]
7834    fn easy_setopt_tcp_fastopen() {
7835        let handle = curl_easy_init();
7836        let code = unsafe { curl_easy_setopt(handle, 244, std::ptr::dangling::<c_void>()) };
7837        assert_eq!(code, CURLcode::CURLE_OK);
7838        unsafe { curl_easy_cleanup(handle) };
7839    }
7840
7841    #[test]
7842    fn easy_setopt_http09_allowed() {
7843        let handle = curl_easy_init();
7844        let code = unsafe { curl_easy_setopt(handle, 285, std::ptr::dangling::<c_void>()) };
7845        assert_eq!(code, CURLcode::CURLE_OK);
7846        unsafe { curl_easy_cleanup(handle) };
7847    }
7848
7849    #[test]
7850    fn curlcode_auth_error_exists() {
7851        assert_eq!(CURLcode::CURLE_AUTH_ERROR as i32, 94);
7852    }
7853
7854    #[test]
7855    fn curlcode_ssl_pinnedpubkey_exists() {
7856        assert_eq!(CURLcode::CURLE_SSL_PINNEDPUBKEYNOTMATCH as i32, 90);
7857    }
7858
7859    #[test]
7860    fn easy_setopt_port() {
7861        let handle = curl_easy_init();
7862        let code = unsafe { curl_easy_setopt(handle, 3, 8080_usize as *const c_void) };
7863        assert_eq!(code, CURLcode::CURLE_OK);
7864        unsafe { curl_easy_cleanup(handle) };
7865    }
7866
7867    #[test]
7868    fn easy_setopt_infilesize() {
7869        let handle = curl_easy_init();
7870        let code = unsafe { curl_easy_setopt(handle, 14, 1024_usize as *const c_void) };
7871        assert_eq!(code, CURLcode::CURLE_OK);
7872        unsafe { curl_easy_cleanup(handle) };
7873    }
7874
7875    #[test]
7876    fn easy_setopt_resume_from() {
7877        let handle = curl_easy_init();
7878        let code = unsafe { curl_easy_setopt(handle, 21, 512_usize as *const c_void) };
7879        assert_eq!(code, CURLcode::CURLE_OK);
7880        unsafe { curl_easy_cleanup(handle) };
7881    }
7882
7883    #[test]
7884    fn easy_setopt_ipresolve() {
7885        let handle = curl_easy_init();
7886        let code = unsafe { curl_easy_setopt(handle, 113, std::ptr::dangling::<c_void>()) };
7887        assert_eq!(code, CURLcode::CURLE_OK);
7888        unsafe { curl_easy_cleanup(handle) };
7889    }
7890
7891    #[test]
7892    fn easy_setopt_postfieldsize_large() {
7893        let handle = curl_easy_init();
7894        let code = unsafe { curl_easy_setopt(handle, 30120, 4096_usize as *const c_void) };
7895        assert_eq!(code, CURLcode::CURLE_OK);
7896        unsafe { curl_easy_cleanup(handle) };
7897    }
7898
7899    #[test]
7900    fn easy_setopt_capath() {
7901        let handle = curl_easy_init();
7902        let path = c"/etc/ssl/certs";
7903        let code = unsafe { curl_easy_setopt(handle, 10097, path.as_ptr().cast::<c_void>()) };
7904        assert_eq!(code, CURLcode::CURLE_OK);
7905        unsafe { curl_easy_cleanup(handle) };
7906    }
7907
7908    #[test]
7909    fn easy_getinfo_request_size() {
7910        let handle = curl_easy_init();
7911        let url = c"http://example.com";
7912        let _ = unsafe { curl_easy_setopt(handle, 10002, url.as_ptr().cast::<c_void>()) };
7913        let _ = unsafe { curl_easy_perform(handle) };
7914        let mut size: c_long = -1;
7915        // CURLINFO_REQUEST_SIZE = 0x20000C = 131084
7916        let code = unsafe {
7917            curl_easy_getinfo(handle, 0x20_000C, std::ptr::addr_of_mut!(size).cast::<c_void>())
7918        };
7919        assert_eq!(code, CURLcode::CURLE_OK);
7920        assert_eq!(size, 0);
7921        unsafe { curl_easy_cleanup(handle) };
7922    }
7923
7924    #[test]
7925    fn easy_getinfo_http_connectcode() {
7926        let handle = curl_easy_init();
7927        let url = c"http://example.com";
7928        let _ = unsafe { curl_easy_setopt(handle, 10002, url.as_ptr().cast::<c_void>()) };
7929        let _ = unsafe { curl_easy_perform(handle) };
7930        let mut code_val: c_long = -1;
7931        // CURLINFO_HTTP_CONNECTCODE = 0x200016 = 131094
7932        let code = unsafe {
7933            curl_easy_getinfo(handle, 0x20_0016, std::ptr::addr_of_mut!(code_val).cast::<c_void>())
7934        };
7935        assert_eq!(code, CURLcode::CURLE_OK);
7936        assert_eq!(code_val, 0);
7937        unsafe { curl_easy_cleanup(handle) };
7938    }
7939
7940    #[test]
7941    fn easy_getinfo_httpauth_avail() {
7942        let handle = curl_easy_init();
7943        let url = c"http://example.com";
7944        let _ = unsafe { curl_easy_setopt(handle, 10002, url.as_ptr().cast::<c_void>()) };
7945        let _ = unsafe { curl_easy_perform(handle) };
7946        let mut auth: c_long = -1;
7947        // CURLINFO_HTTPAUTH_AVAIL = 0x200017 = 131095
7948        let code = unsafe {
7949            curl_easy_getinfo(handle, 0x20_0017, std::ptr::addr_of_mut!(auth).cast::<c_void>())
7950        };
7951        assert_eq!(code, CURLcode::CURLE_OK);
7952        assert_eq!(auth, 1); // CURLAUTH_BASIC
7953        unsafe { curl_easy_cleanup(handle) };
7954    }
7955
7956    #[test]
7957    fn easy_getinfo_proxyauth_avail() {
7958        let handle = curl_easy_init();
7959        let url = c"http://example.com";
7960        let _ = unsafe { curl_easy_setopt(handle, 10002, url.as_ptr().cast::<c_void>()) };
7961        let _ = unsafe { curl_easy_perform(handle) };
7962        let mut auth: c_long = -1;
7963        // CURLINFO_PROXYAUTH_AVAIL = 0x200018 = 131096
7964        let code = unsafe {
7965            curl_easy_getinfo(handle, 0x20_0018, std::ptr::addr_of_mut!(auth).cast::<c_void>())
7966        };
7967        assert_eq!(code, CURLcode::CURLE_OK);
7968        assert_eq!(auth, 0);
7969        unsafe { curl_easy_cleanup(handle) };
7970    }
7971}