Skip to main content

libxml_rs/abi/
exports_nano.rs

1//! exports_nano — xmlNanoFTP*/xmlNanoHTTP*/xmlIOFTP*/xmlIOHTTP* C ABI family (§11.1-I).
2//!
3//! Implements the legacy network client APIs from upstream `nanoftp.c` /
4//! `nanohttp.c` (nanoftp.h / nanohttp.h) plus the protocol I/O callbacks
5//! from xmlIO.h — 48 exported entry points in total:
6//!
7//! - 22 × `xmlNanoFTP*` (nanoftp.h)
8//! - 17 × `xmlNanoHTTP*` (nanohttp.h)
9//! - 5 × `xmlIOHTTP*` (xmlIO.h)
10//! - 4 × `xmlIOFTP*` (xmlIO.h)
11//!
12//! # Offline design (INTENTIONAL)
13//!
14//! This crate is an offline forensic reimplementation: there is no network
15//! stack, so no real sockets are ever created. The context types
16//! (`xmlNanoFTPCtxt` / `xmlNanoHTTPCtxt`) are opaque to callers; state is
17//! kept in side registries (`FTP_CTXTS` / `HTTP_CTXTS`) keyed by an
18//! allocated handle pointer, mirroring the field layout of the upstream
19//! structs (nanoftp.c / nanohttp.c) as closely as the fake transport
20//! allows.
21//!
22//! The FTP control-plane lifecycle (`NewCtxt → Connect → … → Quit → Close`)
23//! is internally consistent: `xmlNanoFTPConnect` simulates a successful
24//! control connection by allocating a fake fd and `xmlNanoFTPGetConnection`
25//! returns a fake data fd. Everything that requires an actual server
26//! round-trip (list, get, open a fetch, read data, responses) returns the
27//! upstream documented failure value and is *not* faked. The HTTP client
28//! never fabricates success: without a network no context can be returned,
29//! so `xmlNanoHTTPMethodRedir` (and everything built on it) returns NULL
30//! exactly as upstream does when the connect fails.
31
32#![allow(
33    missing_docs,
34    non_snake_case,
35    non_camel_case_types,
36    non_upper_case_globals
37)]
38#![allow(unused_variables)]
39#![allow(clippy::missing_safety_doc)]
40#![allow(clippy::not_unsafe_ptr_arg_deref)]
41
42use core::ffi::c_void;
43use core::ptr;
44use once_cell::sync::Lazy;
45use parking_lot::Mutex;
46use std::collections::HashMap;
47use std::ffi::{CStr, CString};
48use std::os::raw::{c_char, c_int, c_ulong};
49use std::sync::atomic::{AtomicBool, AtomicI32, AtomicU64, Ordering};
50
51/// Upstream `INVALID_SOCKET` (nanoftp.h / nanohttp.h): `(-1)` on POSIX.
52const INVALID_SOCKET: c_int = -1;
53/// Upstream default FTP port (`21`), used by `xmlNanoFTPNewCtxt`.
54const FTP_DEFAULT_PORT: c_int = 21;
55/// Upstream default HTTP port (`80`), used by `xmlNanoHTTPNewCtxt`.
56const HTTP_DEFAULT_PORT: c_int = 80;
57
58/// C `ftpListCallback` (nanoftp.h) — invoked once per entry by
59/// `xmlNanoFTPList`. NULL is legal (upstream only calls it when non-NULL).
60type FtpListCallback = Option<
61    unsafe extern "C" fn(
62        userData: *mut c_void,
63        filename: *const c_char,
64        attrib: *const c_char,
65        owner: *const c_char,
66        group: *const c_char,
67        size: c_ulong,
68        links: c_int,
69        year: c_int,
70        month: *const c_char,
71        day: c_int,
72        hour: c_int,
73        minute: c_int,
74    ),
75>;
76
77/// C `ftpDataCallback` (nanoftp.h) — invoked with each data block by
78/// `xmlNanoFTPGet`. NULL is legal (upstream requires it non-NULL in Get).
79type FtpDataCallback =
80    Option<unsafe extern "C" fn(userData: *mut c_void, data: *const c_char, len: c_int)>;
81
82// ═══════════════════════════════════════════════════════════════════════════════
83// Side registries and shared helpers
84// ═══════════════════════════════════════════════════════════════════════════════
85
86/// Opaque handle allocated for each FTP context; its address is the key in
87/// `FTP_CTXTS`. Non-zero-sized so every allocation is a distinct address.
88#[repr(C)]
89struct FtpHandle(u64);
90
91/// Opaque handle allocated for each HTTP context; its address is the key in
92/// `HTTP_CTXTS`.
93#[repr(C)]
94struct HttpHandle(u64);
95
96/// Side registry for live FTP contexts, keyed by handle address. Mirrors
97/// `struct xmlNanoFTPCtxt` (nanoftp.c); transport fields are faked.
98static FTP_CTXTS: Lazy<Mutex<HashMap<usize, NanoFtpState>>> = Lazy::new(|| Mutex::new(HashMap::new()));
99
100/// Side registry for live HTTP contexts, keyed by handle address. Mirrors
101/// `struct xmlNanoHTTPCtxt` (nanohttp.c); the zlib members are omitted.
102static HTTP_CTXTS: Lazy<Mutex<HashMap<usize, NanoHttpState>>> = Lazy::new(|| Mutex::new(HashMap::new()));
103
104/// Module-level FTP proxy configuration (upstream static `proxy` globals in
105/// nanoftp.c). Inert in the offline build: it is stored for API shape but no
106/// connection ever dials it.
107#[allow(dead_code)]
108#[derive(Default)]
109struct FtpProxyCfg {
110    host: Option<CString>,
111    port: i32,
112    user: Option<CString>,
113    passwd: Option<CString>,
114    kind: i32,
115}
116
117static FTP_PROXY: Lazy<Mutex<FtpProxyCfg>> = Lazy::new(|| Mutex::new(FtpProxyCfg::default()));
118
119/// Module-level HTTP proxy configuration (upstream `proxy`/`proxyPort`
120/// globals in nanohttp.c).
121#[allow(dead_code)]
122#[derive(Default)]
123struct HttpProxyCfg {
124    host: Option<CString>,
125    port: i32,
126}
127
128static HTTP_PROXY: Lazy<Mutex<HttpProxyCfg>> = Lazy::new(|| Mutex::new(HttpProxyCfg::default()));
129
130/// One-time-init flags (upstream `static int initialized`).
131static FTP_INITIALIZED: AtomicBool = AtomicBool::new(false);
132static HTTP_INITIALIZED: AtomicBool = AtomicBool::new(false);
133
134/// Handle allocation counter (any distinct address works).
135static NEXT_KEY: AtomicU64 = AtomicU64::new(1);
136/// Fake fd allocator: small positive ints starting at 3 (past stdio).
137static NEXT_FD: AtomicI32 = AtomicI32::new(3);
138
139/// Mirror of `struct xmlNanoFTPCtxt` (nanoftp.c). Fields follow upstream
140/// layout; `control_fd`/`data_fd` hold *fake* fds and the control buffer is
141/// never filled (no peer ever answers).
142#[allow(dead_code)]
143#[derive(Default)]
144struct NanoFtpState {
145    protocol: Option<CString>,
146    hostname: Option<CString>,
147    port: i32,
148    path: Option<CString>,
149    user: Option<CString>,
150    passwd: Option<CString>,
151    passive: i32,
152    control_fd: i32,
153    data_fd: i32,
154    state: i32,
155    return_value: i32,
156    control_buf_index: i32,
157    control_buf_used: i32,
158    control_buf_answer: i32,
159}
160
161/// Mirror of `struct xmlNanoHTTPCtxt` (nanohttp.c), minus the zlib members.
162/// No HTTP context is ever returned by the offline build, so most fields are
163/// inert but kept for layout parity.
164#[allow(dead_code)]
165#[derive(Default)]
166struct NanoHttpState {
167    protocol: Option<CString>,
168    hostname: Option<CString>,
169    port: i32,
170    path: Option<CString>,
171    query: Option<CString>,
172    fd: i32,
173    state: i32,
174    out: Option<CString>,
175    in_: Option<CString>,
176    content: Option<CString>,
177    inptr: i32,
178    inrptr: i32,
179    inlen: i32,
180    last: i32,
181    return_value: i32,
182    version: i32,
183    content_length: i32,
184    content_type: Option<CString>,
185    location: Option<CString>,
186    auth_header: Option<CString>,
187    encoding: Option<CString>,
188    mime_type: Option<CString>,
189}
190
191fn alloc_ftp_handle() -> *mut c_void {
192    let key = NEXT_KEY.fetch_add(1, Ordering::Relaxed);
193    Box::into_raw(Box::new(FtpHandle(key))) as *mut c_void
194}
195
196fn alloc_http_handle() -> *mut c_void {
197    let key = NEXT_KEY.fetch_add(1, Ordering::Relaxed);
198    Box::into_raw(Box::new(HttpHandle(key))) as *mut c_void
199}
200
201unsafe fn free_ftp_handle(handle: *mut c_void) {
202    if !handle.is_null() {
203        drop(unsafe { Box::from_raw(handle as *mut FtpHandle) });
204    }
205}
206
207unsafe fn free_http_handle(handle: *mut c_void) {
208    if !handle.is_null() {
209        drop(unsafe { Box::from_raw(handle as *mut HttpHandle) });
210    }
211}
212
213/// Drop the FTP context: deregister (freeing the state strings) and release
214/// the handle. Returns false when `ctx` was NULL or not registered.
215fn remove_ftp_ctxt(ctx: *mut c_void) -> bool {
216    if ctx.is_null() {
217        return false;
218    }
219    let removed = FTP_CTXTS.lock().remove(&(ctx as usize)).is_some();
220    if removed {
221        unsafe { free_ftp_handle(ctx) };
222    }
223    removed
224}
225
226/// Drop the HTTP context: deregister (freeing the state strings) and release
227/// the handle. Returns false when `ctx` was NULL or not registered.
228fn remove_http_ctxt(ctx: *mut c_void) -> bool {
229    if ctx.is_null() {
230        return false;
231    }
232    let removed = HTTP_CTXTS.lock().remove(&(ctx as usize)).is_some();
233    if removed {
234        unsafe { free_http_handle(ctx) };
235    }
236    removed
237}
238
239/// Fake fd for the simulated transport. Small positive ints, never reused by
240/// the (fake) socket layer, mirroring what a real `socket(2)` would return.
241fn fake_fd() -> c_int {
242    NEXT_FD.fetch_add(1, Ordering::Relaxed)
243}
244
245/// NULL-aware C string read.
246unsafe fn cstr_to_string(p: *const c_char) -> Option<String> {
247    if p.is_null() {
248        return None;
249    }
250    let s = unsafe { CStr::from_ptr(p) };
251    Some(s.to_string_lossy().into_owned())
252}
253
254/// NULL-aware C string read into an owned `CString` (for registry storage).
255fn opt_cstring(p: *const c_char) -> Option<CString> {
256    unsafe { cstr_to_string(p) }.map(to_cstring)
257}
258
259/// Bytes of a NULL-terminated C string (`&[]` for NULL).
260unsafe fn cstr_bytes<'a>(p: *const c_char) -> &'a [u8] {
261    if p.is_null() {
262        return &[];
263    }
264    let mut len = 0usize;
265    while unsafe { *p.add(len) } != 0 {
266        len += 1;
267    }
268    unsafe { core::slice::from_raw_parts(p as *const u8, len) }
269}
270
271fn to_cstring(s: String) -> CString {
272    CString::new(s).unwrap_or_default()
273}
274
275/// ASCII case-insensitive prefix test (upstream `xmlStrncasecmp` for the
276/// `ftp://`/`http://` scheme checks in xmlIO.c).
277fn starts_with_ci(haystack: &[u8], needle: &[u8]) -> bool {
278    haystack.len() >= needle.len()
279        && haystack[..needle.len()]
280            .iter()
281            .zip(needle.iter())
282            .all(|(h, n)| h.to_ascii_lowercase() == n.to_ascii_lowercase())
283}
284
285/// Result of the minimal URL scan, replicating the fields upstream extracts
286/// with `xmlParseURIRaw` in `xmlNanoFTPScanURL` / `xmlNanoHTTPScanURL`.
287#[derive(Default)]
288struct ParsedUrl {
289    scheme: Option<String>,
290    host: Option<String>,
291    port: Option<i32>,
292    path: Option<String>,
293    query: Option<String>,
294    user: Option<String>,
295    passwd: Option<String>,
296}
297
298/// Minimal `scheme://[user[:pass]@]host[:port][/path][?query]` splitter.
299/// No percent-decoding (upstream calls `xmlURIUnescapeString`); good enough
300/// for the API shape of an offline client.
301fn parse_url(url: &str) -> ParsedUrl {
302    let mut out = ParsedUrl::default();
303    let rest = match url.find("://") {
304        Some(i) => {
305            out.scheme = Some(url[..i].to_string());
306            &url[i + 3..]
307        }
308        None => return out,
309    };
310    let (authority, tail) = match rest.find(|c| c == '/' || c == '?') {
311        Some(i) => rest.split_at(i),
312        None => (rest, ""),
313    };
314    if !tail.is_empty() {
315        if let Some(qi) = tail.find('?') {
316            let (p, q) = tail.split_at(qi);
317            if !p.is_empty() {
318                out.path = Some(p.to_string());
319            }
320            if q.len() > 1 {
321                out.query = Some(q[1..].to_string());
322            }
323        } else {
324            out.path = Some(tail.to_string());
325        }
326    }
327    if out.path.is_none() {
328        out.path = Some("/".to_string());
329    }
330    let (userinfo, hostport) = match authority.rfind('@') {
331        Some(i) => {
332            let (u, hp) = authority.split_at(i);
333            (u, &hp[1..])
334        }
335        None => ("", authority),
336    };
337    if !userinfo.is_empty() {
338        match userinfo.find(':') {
339            Some(ci) => {
340                out.user = Some(userinfo[..ci].to_string());
341                out.passwd = Some(userinfo[ci + 1..].to_string());
342            }
343            None => out.user = Some(userinfo.to_string()),
344        }
345    }
346    match hostport.rfind(':') {
347        Some(ci) => {
348            out.host = Some(hostport[..ci].to_string());
349            out.port = hostport[ci + 1..].parse::<i32>().ok();
350        }
351        None => out.host = Some(hostport.to_string()),
352    }
353    out
354}
355
356// ═══════════════════════════════════════════════════════════════════════════════
357// NanoFTP — legacy FTP client (nanoftp.h / nanoftp.c)
358// ═══════════════════════════════════════════════════════════════════════════════
359
360/// Initialize the FTP protocol layer.
361///
362/// # UPSTREAM-PARITY
363///
364/// ```c
365/// void xmlNanoFTPInit(void);
366/// ```
367///
368/// One-time initialization. Upstream also scans `ftp_proxy`/`FTP_PROXY`
369/// environment variables; with no network stack the proxy settings are
370/// inert, so env scanning is skipped (no-op).
371#[no_mangle]
372pub unsafe extern "C" fn xmlNanoFTPInit() {
373    if FTP_INITIALIZED.load(Ordering::Relaxed) {
374        return;
375    }
376    FTP_INITIALIZED.store(true, Ordering::Relaxed);
377}
378
379/// Cleanup the FTP protocol layer (frees proxy information upstream).
380///
381/// # UPSTREAM-PARITY
382///
383/// ```c
384/// void xmlNanoFTPCleanup(void);
385/// ```
386#[no_mangle]
387pub unsafe extern "C" fn xmlNanoFTPCleanup() {
388    FTP_INITIALIZED.store(false, Ordering::Relaxed);
389    *FTP_PROXY.lock() = FtpProxyCfg::default();
390}
391
392/// Allocate and initialize a new FTP context.
393///
394/// # UPSTREAM-PARITY
395///
396/// ```c
397/// void * xmlNanoFTPNewCtxt(const char *URL);
398/// ```
399///
400/// Returns an opaque handle registered in `FTP_CTXTS`, or NULL on
401/// allocation failure. Defaults mirror `xmlNanoFTPNewCtxt` (nanoftp.c):
402/// port 21, passive mode, `returnValue` 0, `controlFd`/`dataFd` invalid.
403#[no_mangle]
404pub unsafe extern "C" fn xmlNanoFTPNewCtxt(URL: *const c_char) -> *mut c_void {
405    let handle = alloc_ftp_handle();
406    let mut st = NanoFtpState {
407        port: FTP_DEFAULT_PORT,
408        passive: 1,
409        control_fd: INVALID_SOCKET,
410        data_fd: INVALID_SOCKET,
411        control_buf_index: 0,
412        control_buf_used: 0,
413        control_buf_answer: 0,
414        ..NanoFtpState::default()
415    };
416    if !URL.is_null() {
417        let parsed = parse_url(&unsafe { cstr_to_string(URL) }.unwrap_or_default());
418        st.protocol = parsed.scheme.map(to_cstring);
419        st.hostname = parsed.host.map(to_cstring);
420        if let Some(p) = parsed.port {
421            st.port = p;
422        }
423        st.path = parsed.path.map(to_cstring);
424        st.user = parsed.user.map(to_cstring);
425        st.passwd = parsed.passwd.map(to_cstring);
426    }
427    FTP_CTXTS.lock().insert(handle as usize, st);
428    handle
429}
430
431/// Free an FTP context, closing the connection first (upstream).
432///
433/// # UPSTREAM-PARITY
434///
435/// ```c
436/// void xmlNanoFTPFreeCtxt(void * ctx);
437/// ```
438#[no_mangle]
439pub unsafe extern "C" fn xmlNanoFTPFreeCtxt(ctx: *mut c_void) {
440    if ctx.is_null() {
441        return;
442    }
443    // Upstream closes the sockets and frees the URL fields; the fake fds
444    // need no close and the strings die with the removed registry record.
445    remove_ftp_ctxt(ctx);
446}
447
448/// Tries to open a control connection to the given server/port.
449///
450/// # UPSTREAM-PARITY
451///
452/// ```c
453/// void * xmlNanoFTPConnectTo(const char *server, int port);
454/// ```
455///
456/// Returns an FTP context or NULL if it failed. The crate cannot do network
457/// I/O (offline forensic reimplementation), so this returns the documented
458/// failure pointer (NULL). The FTP *state* machine — `xmlNanoFTPConnect`,
459/// `xmlNanoFTPQuit`, `xmlNanoFTPClose`, … — remains internally consistent
460/// via fake fds (see `xmlNanoFTPConnect`).
461#[no_mangle]
462pub unsafe extern "C" fn xmlNanoFTPConnectTo(server: *const c_char, port: c_int) -> *mut c_void {
463    xmlNanoFTPInit();
464    if server.is_null() {
465        return ptr::null_mut();
466    }
467    if port <= 0 {
468        return ptr::null_mut();
469    }
470    // INTENTIONAL (offline): upstream dials server:port and returns the
471    // connected context; no TCP connect can happen here, so NULL is the
472    // documented failure return.
473    ptr::null_mut()
474}
475
476/// Start fetching the given `ftp://` resource.
477///
478/// # UPSTREAM-PARITY
479///
480/// ```c
481/// void * xmlNanoFTPOpen(const char *URL);
482/// ```
483///
484/// Returns an FTP context, or NULL. Upstream opens the control connection
485/// and then initiates the data-channel fetch (`TYPE I` + `RETR`) via
486/// `xmlNanoFTPGetSocket`; the RETR handshake needs server replies, so
487/// `GetSocket` fails offline and Open returns NULL as documented.
488#[no_mangle]
489pub unsafe extern "C" fn xmlNanoFTPOpen(URL: *const c_char) -> *mut c_void {
490    xmlNanoFTPInit();
491    if URL.is_null() {
492        return ptr::null_mut();
493    }
494    if !unsafe { cstr_bytes(URL) }.starts_with(b"ftp://") {
495        return ptr::null_mut();
496    }
497    let ctxt = xmlNanoFTPNewCtxt(URL);
498    if ctxt.is_null() {
499        return ptr::null_mut();
500    }
501    if xmlNanoFTPConnect(ctxt) < 0 {
502        xmlNanoFTPFreeCtxt(ctxt);
503        return ptr::null_mut();
504    }
505    // Upstream passes ctxt->path here; our GetSocket can never succeed, so
506    // mirror the upstream failure path exactly.
507    let path_ptr = ftp_path_ptr(ctxt);
508    if xmlNanoFTPGetSocket(ctxt, path_ptr) == INVALID_SOCKET {
509        xmlNanoFTPFreeCtxt(ctxt);
510        return ptr::null_mut();
511    }
512    ctxt
513}
514
515/// Pointer to the current path of an FTP context (NULL if unset).
516fn ftp_path_ptr(ctx: *mut c_void) -> *const c_char {
517    let reg = FTP_CTXTS.lock();
518    match reg.get(&(ctx as usize)).and_then(|st| st.path.as_ref()) {
519        Some(p) => p.as_ptr(),
520        None => ptr::null(),
521    }
522}
523
524/// Tries to open a control connection.
525///
526/// # UPSTREAM-PARITY
527///
528/// ```c
529/// int xmlNanoFTPConnect(void *ctx);
530/// ```
531///
532/// Returns -1 in case of error, 0 otherwise. INTENTIONAL (offline): the
533/// socket connect is simulated by allocating a fake fd, so the FTP lifecycle
534/// (`NewCtxt → Connect → GetConnection → … → Quit → Close`) is internally
535/// consistent. No bytes can ever flow on the fake channel.
536#[no_mangle]
537pub unsafe extern "C" fn xmlNanoFTPConnect(ctx: *mut c_void) -> c_int {
538    if ctx.is_null() {
539        return -1;
540    }
541    let mut reg = FTP_CTXTS.lock();
542    let st = match reg.get_mut(&(ctx as usize)) {
543        Some(st) => st,
544        None => return -1,
545    };
546    if st.hostname.is_none() {
547        return -1;
548    }
549    st.control_fd = fake_fd();
550    0
551}
552
553/// Close the connection and free both control and data channels.
554///
555/// # UPSTREAM-PARITY
556///
557/// ```c
558/// int xmlNanoFTPClose(void *ctx);
559/// ```
560///
561/// Returns -1 in case of error, 0 otherwise.
562#[no_mangle]
563pub unsafe extern "C" fn xmlNanoFTPClose(ctx: *mut c_void) -> c_int {
564    if ctx.is_null() {
565        return -1;
566    }
567    {
568        let mut reg = FTP_CTXTS.lock();
569        let st = match reg.get_mut(&(ctx as usize)) {
570            Some(st) => st,
571            None => return -1,
572        };
573        // Upstream sends QUIT (see xmlNanoFTPQuit) then closes both sockets;
574        // the fake channel has no peer, so the QUIT is a no-op.
575        st.data_fd = INVALID_SOCKET;
576        st.control_fd = INVALID_SOCKET;
577    }
578    remove_ftp_ctxt(ctx);
579    0
580}
581
582/// Send a QUIT command to the server.
583///
584/// # UPSTREAM-PARITY
585///
586/// ```c
587/// int xmlNanoFTPQuit(void *ctx);
588/// ```
589///
590/// Returns -1 in case of error, 0 otherwise. On the simulated control
591/// channel the command is considered delivered (upstream returns 0 on a
592/// successful send).
593#[no_mangle]
594pub unsafe extern "C" fn xmlNanoFTPQuit(ctx: *mut c_void) -> c_int {
595    if ctx.is_null() {
596        return -1;
597    }
598    let reg = FTP_CTXTS.lock();
599    let st = match reg.get(&(ctx as usize)) {
600        Some(st) => st,
601        None => return -1,
602    };
603    if st.control_fd == INVALID_SOCKET {
604        return -1;
605    }
606    0
607}
608
609/// (Re)Initialize the FTP proxy context from a proxy URL.
610///
611/// # UPSTREAM-PARITY
612///
613/// ```c
614/// void xmlNanoFTPScanProxy(const char *URL);
615/// ```
616///
617/// `ftp://host/` or `ftp://host:port/`; a NULL URL clears the proxy info.
618/// The stored proxy is inert (no network stack ever dials it).
619#[no_mangle]
620pub unsafe extern "C" fn xmlNanoFTPScanProxy(URL: *const c_char) {
621    let mut proxy = FTP_PROXY.lock();
622    *proxy = FtpProxyCfg { port: 0, ..FtpProxyCfg::default() };
623    if URL.is_null() {
624        return;
625    }
626    let parsed = parse_url(&unsafe { cstr_to_string(URL) }.unwrap_or_default());
627    if parsed.scheme.as_deref() != Some("ftp") || parsed.host.is_none() {
628        // Upstream raises XML_FTP_URL_SYNTAX here; proxy stays cleared.
629        return;
630    }
631    proxy.host = parsed.host.map(to_cstring);
632    if let Some(p) = parsed.port {
633        proxy.port = p;
634    }
635}
636
637/// Setup the FTP proxy information.
638///
639/// # UPSTREAM-PARITY
640///
641/// ```c
642/// void xmlNanoFTPProxy(const char *host, int port,
643///                      const char *user, const char *passwd, int type);
644/// ```
645///
646/// `type` is 1 for using SITE, 2 for USER a@b. Stored for API shape; inert
647/// in the offline build.
648#[no_mangle]
649pub unsafe extern "C" fn xmlNanoFTPProxy(
650    host: *const c_char,
651    port: c_int,
652    user: *const c_char,
653    passwd: *const c_char,
654    kind: c_int,
655) {
656    let mut proxy = FTP_PROXY.lock();
657    proxy.host = opt_cstring(host);
658    proxy.user = opt_cstring(user);
659    proxy.passwd = opt_cstring(passwd);
660    proxy.port = port;
661    proxy.kind = kind;
662}
663
664/// Update an FTP context by parsing the URL and finding a new path.
665///
666/// # UPSTREAM-PARITY
667///
668/// ```c
669/// int xmlNanoFTPUpdateURL(void *ctx, const char *URL);
670/// ```
671///
672/// Returns 0 if Ok, -1 in case of error (other host/scheme/port, or a NULL
673/// context/URL, or the context was never initialized with a protocol).
674#[no_mangle]
675pub unsafe extern "C" fn xmlNanoFTPUpdateURL(ctx: *mut c_void, URL: *const c_char) -> c_int {
676    if URL.is_null() || ctx.is_null() {
677        return -1;
678    }
679    let parsed = parse_url(&unsafe { cstr_to_string(URL) }.unwrap_or_default());
680    if parsed.scheme.is_none() || parsed.host.is_none() {
681        return -1;
682    }
683    let mut reg = FTP_CTXTS.lock();
684    let st = match reg.get_mut(&(ctx as usize)) {
685        Some(st) => st,
686        None => return -1,
687    };
688    if st.protocol.is_none() || st.hostname.is_none() {
689        return -1;
690    }
691    let scheme_mismatch = match (st.protocol.as_ref(), parsed.scheme.as_deref()) {
692        (Some(a), Some(b)) => a.as_bytes() != b.as_bytes(),
693        _ => true,
694    };
695    let host_mismatch = match (st.hostname.as_ref(), parsed.host.as_deref()) {
696        (Some(a), Some(b)) => a.as_bytes() != b.as_bytes(),
697        _ => true,
698    };
699    if scheme_mismatch || host_mismatch {
700        return -1;
701    }
702    if let Some(p) = parsed.port {
703        if p != st.port {
704            return -1;
705        }
706        st.port = p;
707    }
708    st.path = parsed.path.map(to_cstring);
709    0
710}
711
712/// Get the response from the FTP server after a command.
713///
714/// # UPSTREAM-PARITY
715///
716/// ```c
717/// int xmlNanoFTPGetResponse(void *ctx);
718/// ```
719///
720/// Returns the code number, -1 on error. The fake control channel never
721/// receives a reply, so the documented error return is produced.
722#[no_mangle]
723pub unsafe extern "C" fn xmlNanoFTPGetResponse(ctx: *mut c_void) -> c_int {
724    if ctx.is_null() {
725        return -1;
726    }
727    let reg = FTP_CTXTS.lock();
728    let st = match reg.get(&(ctx as usize)) {
729        Some(st) => st,
730        None => return -1,
731    };
732    if st.control_fd == INVALID_SOCKET {
733        return -1;
734    }
735    // Upstream blocks reading a 3-digit reply from the server; there is no
736    // peer on the fake control channel, so the read fails as documented.
737    -1
738}
739
740/// Check if there is a response from the FTP server after a command.
741///
742/// # UPSTREAM-PARITY
743///
744/// ```c
745/// int xmlNanoFTPCheckResponse(void *ctx);
746/// ```
747///
748/// Returns the code number, or 0 when nothing is pending. A `select()` on
749/// the fake fd never reports readable data → 0.
750#[no_mangle]
751pub unsafe extern "C" fn xmlNanoFTPCheckResponse(ctx: *mut c_void) -> c_int {
752    if ctx.is_null() {
753        return -1;
754    }
755    let reg = FTP_CTXTS.lock();
756    let st = match reg.get(&(ctx as usize)) {
757        Some(st) => st,
758        None => return -1,
759    };
760    if st.control_fd == INVALID_SOCKET {
761        return -1;
762    }
763    0
764}
765
766/// Tries to change the remote directory.
767///
768/// # UPSTREAM-PARITY
769///
770/// ```c
771/// int xmlNanoFTPCwd(void *ctx, const char *directory);
772/// ```
773///
774/// Returns -1 in case of error, 1 if CWD worked, 0 if it failed. The fake
775/// channel never delivers the required 250 reply, so the command "fails"
776/// (0) exactly as upstream does for any non-2xx response.
777#[no_mangle]
778pub unsafe extern "C" fn xmlNanoFTPCwd(ctx: *mut c_void, directory: *const c_char) -> c_int {
779    if ctx.is_null() {
780        return -1;
781    }
782    let reg = FTP_CTXTS.lock();
783    let st = match reg.get(&(ctx as usize)) {
784        Some(st) => st,
785        None => return -1,
786    };
787    if st.control_fd == INVALID_SOCKET {
788        return -1;
789    }
790    if directory.is_null() {
791        return 0;
792    }
793    0
794}
795
796/// Tries to delete an item (file or directory) from the server.
797///
798/// # UPSTREAM-PARITY
799///
800/// ```c
801/// int xmlNanoFTPDele(void *ctx, const char *file);
802/// ```
803///
804/// Returns -1 in case of error, 1 if DELE worked, 0 if it failed. The fake
805/// channel never delivers the required 250 reply → 0.
806#[no_mangle]
807pub unsafe extern "C" fn xmlNanoFTPDele(ctx: *mut c_void, file: *const c_char) -> c_int {
808    if ctx.is_null() {
809        return -1;
810    }
811    let reg = FTP_CTXTS.lock();
812    let st = match reg.get(&(ctx as usize)) {
813        Some(st) => st,
814        None => return -1,
815    };
816    if st.control_fd == INVALID_SOCKET || file.is_null() {
817        return -1;
818    }
819    0
820}
821
822/// Try to open a data connection to the server (passive mode only).
823///
824/// # UPSTREAM-PARITY
825///
826/// ```c
827/// SOCKET xmlNanoFTPGetConnection(void *ctx);
828/// ```
829///
830/// Returns -1 in case of error, 0 otherwise (SOCKET == int on POSIX).
831/// INTENTIONAL (offline): the PASV/EPSV data channel is simulated by a fake
832/// fd, keeping the lifecycle (`GetConnection → Read → CloseConnection`)
833/// internally consistent. No bytes can ever flow on it.
834#[no_mangle]
835pub unsafe extern "C" fn xmlNanoFTPGetConnection(ctx: *mut c_void) -> c_int {
836    if ctx.is_null() {
837        return INVALID_SOCKET;
838    }
839    let mut reg = FTP_CTXTS.lock();
840    let st = match reg.get_mut(&(ctx as usize)) {
841        Some(st) => st,
842        None => return INVALID_SOCKET,
843    };
844    st.data_fd = fake_fd();
845    st.data_fd
846}
847
848/// Close the data connection from the server.
849///
850/// # UPSTREAM-PARITY
851///
852/// ```c
853/// int xmlNanoFTPCloseConnection(void *ctx);
854/// ```
855///
856/// Returns -1 in case of error, 0 otherwise.
857#[no_mangle]
858pub unsafe extern "C" fn xmlNanoFTPCloseConnection(ctx: *mut c_void) -> c_int {
859    if ctx.is_null() {
860        return -1;
861    }
862    let mut reg = FTP_CTXTS.lock();
863    let st = match reg.get_mut(&(ctx as usize)) {
864        Some(st) => st,
865        None => return -1,
866    };
867    if st.control_fd == INVALID_SOCKET {
868        return -1;
869    }
870    st.data_fd = INVALID_SOCKET;
871    0
872}
873
874/// Do a listing on the server; entries go to the callback.
875///
876/// # UPSTREAM-PARITY
877///
878/// ```c
879/// int xmlNanoFTPList(void *ctx, ftpListCallback callback,
880///                    void *userData, const char *filename);
881/// ```
882///
883/// Returns -1 in case of error, 0 otherwise. INTENTIONAL (offline): a real
884/// listing requires a data connection and server data, so the documented
885/// error return (-1) is produced.
886#[no_mangle]
887pub unsafe extern "C" fn xmlNanoFTPList(
888    ctx: *mut c_void,
889    callback: FtpListCallback,
890    userData: *mut c_void,
891    filename: *const c_char,
892) -> c_int {
893    if ctx.is_null() {
894        return -1;
895    }
896    let _ = (callback, userData, filename);
897    -1
898}
899
900/// Initiate fetch of the given file from the server.
901///
902/// # UPSTREAM-PARITY
903///
904/// ```c
905/// SOCKET xmlNanoFTPGetSocket(void *ctx, const char *filename);
906/// ```
907///
908/// Returns the socket for the data connection, or <0 in case of error.
909/// Upstream opens the data channel then drives `TYPE I` + `RETR`
910/// handshakes; both need server replies, so the fetch can never be
911/// initiated offline → INVALID_SOCKET.
912#[no_mangle]
913pub unsafe extern "C" fn xmlNanoFTPGetSocket(ctx: *mut c_void, filename: *const c_char) -> c_int {
914    if ctx.is_null() {
915        return INVALID_SOCKET;
916    }
917    let reg = FTP_CTXTS.lock();
918    let st = match reg.get(&(ctx as usize)) {
919        Some(st) => st,
920        None => return INVALID_SOCKET,
921    };
922    if filename.is_null() && st.path.is_none() {
923        return INVALID_SOCKET;
924    }
925    INVALID_SOCKET
926}
927
928/// Fetch the given file from the server; data goes to the callback.
929///
930/// # UPSTREAM-PARITY
931///
932/// ```c
933/// int xmlNanoFTPGet(void *ctx, ftpDataCallback callback,
934///                   void *userData, const char *filename);
935/// ```
936///
937/// Returns -1 in case of error, 0 otherwise. The transfer needs
938/// `xmlNanoFTPGetSocket`, which can never succeed offline → -1.
939#[no_mangle]
940pub unsafe extern "C" fn xmlNanoFTPGet(
941    ctx: *mut c_void,
942    callback: FtpDataCallback,
943    userData: *mut c_void,
944    filename: *const c_char,
945) -> c_int {
946    if ctx.is_null() {
947        return -1;
948    }
949    {
950        let reg = FTP_CTXTS.lock();
951        let st = match reg.get(&(ctx as usize)) {
952            Some(st) => st,
953            None => return -1,
954        };
955        if filename.is_null() && st.path.is_none() {
956            return -1;
957        }
958    }
959    if callback.is_none() {
960        return -1;
961    }
962    if xmlNanoFTPGetSocket(ctx, filename) == INVALID_SOCKET {
963        return -1;
964    }
965    let _ = userData;
966    -1
967}
968
969/// Read @len bytes from the existing FTP data connection.
970///
971/// # UPSTREAM-PARITY
972///
973/// ```c
974/// int xmlNanoFTPRead(void *ctx, void *dest, int len);
975/// ```
976///
977/// Returns the number of bytes read. 0 indicates end of connection, -1 a
978/// parameter error. The fake data channel reports EOF immediately, so after
979/// the parameter checks this returns 0 and closes the data connection,
980/// exactly as upstream does at end-of-connection.
981#[no_mangle]
982pub unsafe extern "C" fn xmlNanoFTPRead(ctx: *mut c_void, dest: *mut c_void, len: c_int) -> c_int {
983    if ctx.is_null() {
984        return -1;
985    }
986    let mut reg = FTP_CTXTS.lock();
987    let st = match reg.get_mut(&(ctx as usize)) {
988        Some(st) => st,
989        None => return -1,
990    };
991    if st.data_fd == INVALID_SOCKET {
992        return 0;
993    }
994    if dest.is_null() {
995        return -1;
996    }
997    if len <= 0 {
998        return 0;
999    }
1000    // Simulated recv(): EOF with zero bytes; upstream then closes the data
1001    // connection and returns 0.
1002    st.data_fd = INVALID_SOCKET;
1003    0
1004}
1005
1006// ═══════════════════════════════════════════════════════════════════════════════
1007// NanoHTTP — legacy HTTP client (nanohttp.h / nanohttp.c)
1008// ═══════════════════════════════════════════════════════════════════════════════
1009
1010/// Initialize the HTTP protocol layer.
1011///
1012/// # UPSTREAM-PARITY
1013///
1014/// ```c
1015/// void xmlNanoHTTPInit(void);
1016/// ```
1017///
1018/// One-time initialization; upstream also scans `http_proxy`/`HTTP_PROXY`
1019/// environment variables, skipped here (proxy settings are inert offline).
1020#[no_mangle]
1021pub unsafe extern "C" fn xmlNanoHTTPInit() {
1022    if HTTP_INITIALIZED.load(Ordering::Relaxed) {
1023        return;
1024    }
1025    HTTP_INITIALIZED.store(true, Ordering::Relaxed);
1026}
1027
1028/// Cleanup the HTTP protocol layer.
1029///
1030/// # UPSTREAM-PARITY
1031///
1032/// ```c
1033/// void xmlNanoHTTPCleanup(void);
1034/// ```
1035#[no_mangle]
1036pub unsafe extern "C" fn xmlNanoHTTPCleanup() {
1037    HTTP_INITIALIZED.store(false, Ordering::Relaxed);
1038    *HTTP_PROXY.lock() = HttpProxyCfg::default();
1039}
1040
1041/// (Re)Initialize the HTTP proxy context from a proxy URL.
1042///
1043/// # UPSTREAM-PARITY
1044///
1045/// ```c
1046/// void xmlNanoHTTPScanProxy(const char *URL);
1047/// ```
1048///
1049/// `http://myproxy/` or `http://myproxy:3128/`; a NULL URL clears the proxy
1050/// info. Inert in the offline build.
1051#[no_mangle]
1052pub unsafe extern "C" fn xmlNanoHTTPScanProxy(URL: *const c_char) {
1053    let mut proxy = HTTP_PROXY.lock();
1054    *proxy = HttpProxyCfg { port: 0, ..HttpProxyCfg::default() };
1055    if URL.is_null() {
1056        return;
1057    }
1058    let parsed = parse_url(&unsafe { cstr_to_string(URL) }.unwrap_or_default());
1059    if parsed.scheme.as_deref() != Some("http") || parsed.host.is_none() {
1060        // Upstream raises XML_HTTP_URL_SYNTAX here; proxy stays cleared.
1061        return;
1062    }
1063    proxy.host = parsed.host.map(to_cstring);
1064    if let Some(p) = parsed.port {
1065        proxy.port = p;
1066    }
1067}
1068
1069/// Open a connection to the indicated resource via HTTP GET.
1070///
1071/// # UPSTREAM-PARITY
1072///
1073/// ```c
1074/// void * xmlNanoHTTPOpen(const char *URL, char **contentType);
1075/// ```
1076///
1077/// Returns NULL in case of failure, otherwise a request handler. The
1078/// contentType is set to NULL first (upstream). Since no HTTP context can
1079/// be created offline, this always returns NULL.
1080#[no_mangle]
1081pub unsafe extern "C" fn xmlNanoHTTPOpen(URL: *const c_char, contentType: *mut *mut c_char) -> *mut c_void {
1082    if !contentType.is_null() {
1083        unsafe { *contentType = ptr::null_mut() };
1084    }
1085    xmlNanoHTTPMethod(URL, ptr::null(), ptr::null(), contentType, ptr::null(), 0)
1086}
1087
1088/// Open a connection to the indicated resource via HTTP GET, tracking
1089/// redirects.
1090///
1091/// # UPSTREAM-PARITY
1092///
1093/// ```c
1094/// void * xmlNanoHTTPOpenRedir(const char *URL, char **contentType, char **redir);
1095/// ```
1096///
1097/// Returns NULL in case of failure; `contentType`/`redir` are cleared first.
1098#[no_mangle]
1099pub unsafe extern "C" fn xmlNanoHTTPOpenRedir(
1100    URL: *const c_char,
1101    contentType: *mut *mut c_char,
1102    redir: *mut *mut c_char,
1103) -> *mut c_void {
1104    if !contentType.is_null() {
1105        unsafe { *contentType = ptr::null_mut() };
1106    }
1107    if !redir.is_null() {
1108        unsafe { *redir = ptr::null_mut() };
1109    }
1110    xmlNanoHTTPMethodRedir(URL, ptr::null(), ptr::null(), contentType, redir, ptr::null(), 0)
1111}
1112
1113/// Open a connection via HTTP using the given method, headers and input.
1114///
1115/// # UPSTREAM-PARITY
1116///
1117/// ```c
1118/// void * xmlNanoHTTPMethod(const char *URL, const char *method,
1119///                          const char *input, char **contentType,
1120///                          const char *headers, int ilen);
1121/// ```
1122///
1123/// Returns NULL in case of failure (always, offline — see
1124/// `xmlNanoHTTPMethodRedir`).
1125#[no_mangle]
1126pub unsafe extern "C" fn xmlNanoHTTPMethod(
1127    URL: *const c_char,
1128    method: *const c_char,
1129    input: *const c_char,
1130    contentType: *mut *mut c_char,
1131    headers: *const c_char,
1132    ilen: c_int,
1133) -> *mut c_void {
1134    xmlNanoHTTPMethodRedir(URL, method, input, contentType, ptr::null_mut(), headers, ilen)
1135}
1136
1137/// Open a connection via HTTP using the given method, tracking redirects.
1138///
1139/// # UPSTREAM-PARITY
1140///
1141/// ```c
1142/// void * xmlNanoHTTPMethodRedir(const char *URL, const char *method,
1143///                               const char *input, char **contentType,
1144///                               char **redir, const char *headers, int ilen);
1145/// ```
1146///
1147/// Returns NULL in case of failure. Upstream allocates the context, checks
1148/// the scheme/host, opens a TCP connection (or proxy) and exchanges the
1149/// request/response headers here. This crate has no network stack (offline
1150/// forensic reimplementation), so the connect fails and the documented
1151/// failure return (NULL) is produced — never fake success. The context is
1152/// registered during validation and freed again before returning.
1153#[no_mangle]
1154pub unsafe extern "C" fn xmlNanoHTTPMethodRedir(
1155    URL: *const c_char,
1156    method: *const c_char,
1157    input: *const c_char,
1158    contentType: *mut *mut c_char,
1159    redir: *mut *mut c_char,
1160    headers: *const c_char,
1161    ilen: c_int,
1162) -> *mut c_void {
1163    let _ = (method, input, contentType, redir, headers, ilen);
1164    if URL.is_null() {
1165        return ptr::null_mut();
1166    }
1167    xmlNanoHTTPInit();
1168
1169    let handle = alloc_http_handle();
1170    let mut st = NanoHttpState {
1171        port: HTTP_DEFAULT_PORT,
1172        fd: INVALID_SOCKET,
1173        content_length: -1,
1174        ..NanoHttpState::default()
1175    };
1176    // Upstream xmlNanoHTTPScanURL (nanohttp.c).
1177    let parsed = parse_url(&unsafe { cstr_to_string(URL) }.unwrap_or_default());
1178    st.protocol = parsed.scheme.map(to_cstring);
1179    st.hostname = parsed.host.map(to_cstring);
1180    if let Some(p) = parsed.port {
1181        st.port = p;
1182    }
1183    st.path = parsed.path.map(to_cstring);
1184    st.query = parsed.query.map(to_cstring);
1185    HTTP_CTXTS.lock().insert(handle as usize, st);
1186
1187    let valid = {
1188        let reg = HTTP_CTXTS.lock();
1189        match reg.get(&(handle as usize)) {
1190            Some(st) => {
1191                let proto_ok = match st.protocol.as_ref() {
1192                    Some(p) => p.as_bytes() == "http".as_bytes(),
1193                    None => false,
1194                };
1195                proto_ok && st.hostname.is_some()
1196            }
1197            None => false,
1198        }
1199    };
1200    if !valid {
1201        remove_http_ctxt(handle);
1202        return ptr::null_mut();
1203    }
1204    // INTENTIONAL (offline): the TCP connect (to host or proxy) cannot
1205    // happen, so upstream's documented failure return is produced.
1206    remove_http_ctxt(handle);
1207    ptr::null_mut()
1208}
1209
1210/// Read @len bytes from the existing HTTP connection.
1211///
1212/// # UPSTREAM-PARITY
1213///
1214/// ```c
1215/// int xmlNanoHTTPRead(void *ctx, void *dest, int len);
1216/// ```
1217///
1218/// Returns the number of bytes read; 0 is end of connection, -1 a parameter
1219/// error. No HTTP context can exist offline, so any real call hits the
1220/// NULL/unknown-context error path → -1.
1221#[no_mangle]
1222pub unsafe extern "C" fn xmlNanoHTTPRead(ctx: *mut c_void, dest: *mut c_void, len: c_int) -> c_int {
1223    if ctx.is_null() {
1224        return -1;
1225    }
1226    if dest.is_null() {
1227        return -1;
1228    }
1229    if len <= 0 {
1230        return 0;
1231    }
1232    if HTTP_CTXTS.lock().get(&(ctx as usize)).is_none() {
1233        return -1;
1234    }
1235    // A registered context would report end-of-connection (upstream recv()
1236    // at EOF returns 0); unreachable with the current offline flow.
1237    0
1238}
1239
1240/// Close an HTTP context, ending the connection and freeing all data.
1241///
1242/// # UPSTREAM-PARITY
1243///
1244/// ```c
1245/// void xmlNanoHTTPClose(void *ctx);
1246/// ```
1247#[no_mangle]
1248pub unsafe extern "C" fn xmlNanoHTTPClose(ctx: *mut c_void) {
1249    if ctx.is_null() {
1250        return;
1251    }
1252    remove_http_ctxt(ctx);
1253}
1254
1255/// Get the latest HTTP return code received.
1256///
1257/// # UPSTREAM-PARITY
1258///
1259/// ```c
1260/// int xmlNanoHTTPReturnCode(void *ctx);
1261/// ```
1262#[no_mangle]
1263pub unsafe extern "C" fn xmlNanoHTTPReturnCode(ctx: *mut c_void) -> c_int {
1264    let reg = HTTP_CTXTS.lock();
1265    match reg.get(&(ctx as usize)) {
1266        Some(st) => st.return_value,
1267        None => -1,
1268    }
1269}
1270
1271/// Get the stashed WWW-Authenticate / Proxy-Authenticate header.
1272///
1273/// # UPSTREAM-PARITY
1274///
1275/// ```c
1276/// const char * xmlNanoHTTPAuthHeader(void *ctx);
1277/// ```
1278#[no_mangle]
1279pub unsafe extern "C" fn xmlNanoHTTPAuthHeader(ctx: *mut c_void) -> *const c_char {
1280    let reg = HTTP_CTXTS.lock();
1281    match reg.get(&(ctx as usize)).and_then(|st| st.auth_header.as_ref()) {
1282        Some(h) => h.as_ptr(),
1283        None => ptr::null(),
1284    }
1285}
1286
1287/// The specified content length from the HTTP header (-1 if absent).
1288///
1289/// # UPSTREAM-PARITY
1290///
1291/// ```c
1292/// int xmlNanoHTTPContentLength(void *ctx);
1293/// ```
1294#[no_mangle]
1295pub unsafe extern "C" fn xmlNanoHTTPContentLength(ctx: *mut c_void) -> c_int {
1296    let reg = HTTP_CTXTS.lock();
1297    match reg.get(&(ctx as usize)) {
1298        Some(st) => st.content_length,
1299        None => -1,
1300    }
1301}
1302
1303/// The redirection URL from the HTTP header, or NULL.
1304///
1305/// # UPSTREAM-PARITY
1306///
1307/// ```c
1308/// const char * xmlNanoHTTPRedir(void *ctx);
1309/// ```
1310#[no_mangle]
1311pub unsafe extern "C" fn xmlNanoHTTPRedir(ctx: *mut c_void) -> *const c_char {
1312    let reg = HTTP_CTXTS.lock();
1313    match reg.get(&(ctx as usize)).and_then(|st| st.location.as_ref()) {
1314        Some(l) => l.as_ptr(),
1315        None => ptr::null(),
1316    }
1317}
1318
1319/// The encoding specified in the HTTP headers, or NULL.
1320///
1321/// # UPSTREAM-PARITY
1322///
1323/// ```c
1324/// const char * xmlNanoHTTPEncoding(void *ctx);
1325/// ```
1326#[no_mangle]
1327pub unsafe extern "C" fn xmlNanoHTTPEncoding(ctx: *mut c_void) -> *const c_char {
1328    let reg = HTTP_CTXTS.lock();
1329    match reg.get(&(ctx as usize)).and_then(|st| st.encoding.as_ref()) {
1330        Some(e) => e.as_ptr(),
1331        None => ptr::null(),
1332    }
1333}
1334
1335/// The Mime-Type specified in the HTTP headers, or NULL.
1336///
1337/// # UPSTREAM-PARITY
1338///
1339/// ```c
1340/// const char * xmlNanoHTTPMimeType(void *ctx);
1341/// ```
1342#[no_mangle]
1343pub unsafe extern "C" fn xmlNanoHTTPMimeType(ctx: *mut c_void) -> *const c_char {
1344    let reg = HTTP_CTXTS.lock();
1345    match reg.get(&(ctx as usize)).and_then(|st| st.mime_type.as_ref()) {
1346        Some(m) => m.as_ptr(),
1347        None => ptr::null(),
1348    }
1349}
1350
1351/// Fetch the indicated resource via HTTP GET and save it to a file.
1352///
1353/// # UPSTREAM-PARITY
1354///
1355/// ```c
1356/// int xmlNanoHTTPFetch(const char *URL, const char *filename, char **contentType);
1357/// ```
1358///
1359/// Returns -1 in case of failure, 0 in case of success. `Open` can never
1360/// succeed offline → -1.
1361#[no_mangle]
1362pub unsafe extern "C" fn xmlNanoHTTPFetch(
1363    URL: *const c_char,
1364    filename: *const c_char,
1365    contentType: *mut *mut c_char,
1366) -> c_int {
1367    if filename.is_null() {
1368        return -1;
1369    }
1370    let ctxt = xmlNanoHTTPOpen(URL, contentType);
1371    if ctxt.is_null() {
1372        return -1;
1373    }
1374    // Unreachable offline (Open never succeeds); upstream would stream the
1375    // body into `filename` here.
1376    xmlNanoHTTPClose(ctxt);
1377    -1
1378}
1379
1380/// Save the output of the HTTP transaction to a file.
1381///
1382/// # UPSTREAM-PARITY
1383///
1384/// ```c
1385/// int xmlNanoHTTPSave(void *ctxt, const char *filename);
1386/// ```
1387///
1388/// Returns -1 in case of failure, 0 in case of success. No HTTP context can
1389/// exist offline → -1.
1390#[no_mangle]
1391pub unsafe extern "C" fn xmlNanoHTTPSave(ctxt: *mut c_void, filename: *const c_char) -> c_int {
1392    if ctxt.is_null() || filename.is_null() {
1393        return -1;
1394    }
1395    if HTTP_CTXTS.lock().get(&(ctxt as usize)).is_none() {
1396        return -1;
1397    }
1398    // Unreachable offline: no context and no fetched content exist.
1399    -1
1400}
1401
1402// ═══════════════════════════════════════════════════════════════════════════════
1403// xmlIO.c protocol I/O callbacks (xmlIO.h)
1404// ═══════════════════════════════════════════════════════════════════════════════
1405
1406/// Default `http://` protocol callback: URI matcher.
1407///
1408/// # UPSTREAM-PARITY
1409///
1410/// ```c
1411/// int xmlIOHTTPMatch(const char *filename);
1412/// ```
1413///
1414/// Returns 1 if the filename starts with `http://` (case-insensitive, like
1415/// upstream `xmlStrncasecmp`), 0 otherwise.
1416#[no_mangle]
1417pub unsafe extern "C" fn xmlIOHTTPMatch(filename: *const c_char) -> c_int {
1418    if starts_with_ci(unsafe { cstr_bytes(filename) }, b"http://") {
1419        1
1420    } else {
1421        0
1422    }
1423}
1424
1425/// Default `http://` protocol callback: open an HTTP I/O channel.
1426///
1427/// # UPSTREAM-PARITY
1428///
1429/// ```c
1430/// void * xmlIOHTTPOpen(const char *filename);
1431/// ```
1432#[no_mangle]
1433pub unsafe extern "C" fn xmlIOHTTPOpen(filename: *const c_char) -> *mut c_void {
1434    xmlNanoHTTPOpen(filename, ptr::null_mut())
1435}
1436
1437/// Default `http://` protocol callback: open an HTTP I/O channel for POST.
1438///
1439/// # UPSTREAM-PARITY
1440///
1441/// ```c
1442/// void * xmlIOHTTPOpenW(const char *post_uri, int compression);
1443/// ```
1444///
1445/// Upstream 2.13+: "Support for HTTP POST has been removed. Returns NULL."
1446#[no_mangle]
1447pub unsafe extern "C" fn xmlIOHTTPOpenW(post_uri: *const c_char, compression: c_int) -> *mut c_void {
1448    let _ = (post_uri, compression);
1449    ptr::null_mut()
1450}
1451
1452/// Default `http://` protocol callback: read from the HTTP I/O channel.
1453///
1454/// # UPSTREAM-PARITY
1455///
1456/// ```c
1457/// int xmlIOHTTPRead(void *context, char *buffer, int len);
1458/// ```
1459#[no_mangle]
1460pub unsafe extern "C" fn xmlIOHTTPRead(
1461    context: *mut c_void,
1462    buffer: *mut c_char,
1463    len: c_int,
1464) -> c_int {
1465    if buffer.is_null() || len < 0 {
1466        return -1;
1467    }
1468    xmlNanoHTTPRead(context, buffer as *mut c_void, len)
1469}
1470
1471/// Default `http://` protocol callback: close the HTTP I/O channel.
1472///
1473/// # UPSTREAM-PARITY
1474///
1475/// ```c
1476/// int xmlIOHTTPClose(void *context);
1477/// ```
1478///
1479/// Returns 0 (upstream).
1480#[no_mangle]
1481pub unsafe extern "C" fn xmlIOHTTPClose(context: *mut c_void) -> c_int {
1482    xmlNanoHTTPClose(context);
1483    0
1484}
1485
1486/// Default `ftp://` protocol callback: URI matcher.
1487///
1488/// # UPSTREAM-PARITY
1489///
1490/// ```c
1491/// int xmlIOFTPMatch(const char *filename);
1492/// ```
1493///
1494/// Returns 1 if the filename starts with `ftp://` (case-insensitive), 0
1495/// otherwise.
1496#[no_mangle]
1497pub unsafe extern "C" fn xmlIOFTPMatch(filename: *const c_char) -> c_int {
1498    if starts_with_ci(unsafe { cstr_bytes(filename) }, b"ftp://") {
1499        1
1500    } else {
1501        0
1502    }
1503}
1504
1505/// Default `ftp://` protocol callback: open an FTP I/O channel.
1506///
1507/// # UPSTREAM-PARITY
1508///
1509/// ```c
1510/// void * xmlIOFTPOpen(const char *filename);
1511/// ```
1512#[no_mangle]
1513pub unsafe extern "C" fn xmlIOFTPOpen(filename: *const c_char) -> *mut c_void {
1514    xmlNanoFTPOpen(filename)
1515}
1516
1517/// Default `ftp://` protocol callback: read from the FTP I/O channel.
1518///
1519/// # UPSTREAM-PARITY
1520///
1521/// ```c
1522/// int xmlIOFTPRead(void *context, char *buffer, int len);
1523/// ```
1524#[no_mangle]
1525pub unsafe extern "C" fn xmlIOFTPRead(
1526    context: *mut c_void,
1527    buffer: *mut c_char,
1528    len: c_int,
1529) -> c_int {
1530    if buffer.is_null() || len < 0 {
1531        return -1;
1532    }
1533    xmlNanoFTPRead(context, buffer as *mut c_void, len)
1534}
1535
1536/// Default `ftp://` protocol callback: close the FTP I/O channel.
1537///
1538/// # UPSTREAM-PARITY
1539///
1540/// ```c
1541/// int xmlIOFTPClose(void *context);
1542/// ```
1543#[no_mangle]
1544pub unsafe extern "C" fn xmlIOFTPClose(context: *mut c_void) -> c_int {
1545    xmlNanoFTPClose(context)
1546}