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}