#![allow(non_camel_case_types)]
use std::ffi::{c_char, c_int, c_long, CStr};
use std::panic::{self, AssertUnwindSafe};
use std::ptr;
use crate::http::{Request, Response};
fn ffi_guard<T>(default: T, f: impl FnOnce() -> T) -> T {
panic::catch_unwind(AssertUnwindSafe(f)).unwrap_or(default)
}
pub enum RSURL {}
#[repr(C)]
pub enum RsurlOpt {
Url = 1,
CustomRequest = 2,
Header = 3,
PostFieldsString = 4,
ConnectTimeout = 5,
Timeout = 6,
UserAgent = 7,
}
#[repr(C)]
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum RsurlCode {
Ok = 0,
InvalidHandle = 1,
UnknownOption = 2,
InvalidArg = 3,
NoResponse = 4,
Network = 5,
BadResponse = 6,
Unsupported = 7,
}
struct Handle {
url: Option<String>,
method: Option<String>,
user_agent: Option<String>,
headers: Vec<(String, String)>,
body: Option<Vec<u8>>,
connect_timeout_secs: Option<u64>,
timeout_secs: Option<u64>,
last_response: Option<Response>,
header_buf: Vec<Vec<u8>>,
}
impl Handle {
fn new() -> Self {
Handle {
url: None,
method: None,
user_agent: None,
headers: Vec::new(),
body: None,
connect_timeout_secs: None,
timeout_secs: None,
last_response: None,
header_buf: Vec::new(),
}
}
}
fn handle_mut<'a>(h: *mut RSURL) -> Option<&'a mut Handle> {
if h.is_null() {
return None;
}
Some(unsafe { &mut *(h as *mut Handle) })
}
fn handle_ref<'a>(h: *const RSURL) -> Option<&'a Handle> {
if h.is_null() {
return None;
}
Some(unsafe { &*(h as *const Handle) })
}
#[no_mangle]
pub extern "C" fn rsurl_easy_init() -> *mut RSURL {
ffi_guard(ptr::null_mut(), || {
let boxed = Box::new(Handle::new());
Box::into_raw(boxed) as *mut RSURL
})
}
#[no_mangle]
pub extern "C" fn rsurl_easy_cleanup(handle: *mut RSURL) {
ffi_guard((), || {
if handle.is_null() {
return;
}
unsafe {
drop(Box::from_raw(handle as *mut Handle));
}
})
}
#[no_mangle]
pub extern "C" fn rsurl_easy_reset(handle: *mut RSURL) -> RsurlCode {
ffi_guard(RsurlCode::Network, || {
let Some(h) = handle_mut(handle) else {
return RsurlCode::InvalidHandle;
};
*h = Handle::new();
RsurlCode::Ok
})
}
#[no_mangle]
pub unsafe extern "C" fn rsurl_easy_setopt_str(
handle: *mut RSURL,
option: c_int,
value: *const c_char,
) -> RsurlCode {
ffi_guard(RsurlCode::Network, || {
let Some(h) = handle_mut(handle) else {
return RsurlCode::InvalidHandle;
};
let s = if value.is_null() {
None
} else {
match unsafe { CStr::from_ptr(value) }.to_str() {
Ok(s) => Some(s.to_string()),
Err(_) => return RsurlCode::InvalidArg,
}
};
let Some(opt) = opt_from_int(option) else {
return RsurlCode::UnknownOption;
};
match opt {
RsurlOpt::Url => h.url = s,
RsurlOpt::CustomRequest => h.method = s,
RsurlOpt::UserAgent => h.user_agent = s,
RsurlOpt::Header => match s {
Some(line) => {
let Some((k, v)) = line.split_once(':') else {
return RsurlCode::InvalidArg;
};
h.headers.push((k.trim().to_string(), v.trim().to_string()));
}
None => h.headers.clear(),
},
RsurlOpt::PostFieldsString => h.body = s.map(|s| s.into_bytes()),
RsurlOpt::ConnectTimeout | RsurlOpt::Timeout => return RsurlCode::InvalidArg,
}
RsurlCode::Ok
})
}
#[no_mangle]
pub extern "C" fn rsurl_easy_setopt_long(
handle: *mut RSURL,
option: c_int,
value: c_long,
) -> RsurlCode {
ffi_guard(RsurlCode::Network, || {
let Some(h) = handle_mut(handle) else {
return RsurlCode::InvalidHandle;
};
let Some(opt) = opt_from_int(option) else {
return RsurlCode::UnknownOption;
};
let secs = if value <= 0 { None } else { Some(value as u64) };
match opt {
RsurlOpt::ConnectTimeout => h.connect_timeout_secs = secs,
RsurlOpt::Timeout => h.timeout_secs = secs,
_ => return RsurlCode::InvalidArg,
}
RsurlCode::Ok
})
}
fn opt_from_int(v: c_int) -> Option<RsurlOpt> {
Some(match v {
1 => RsurlOpt::Url,
2 => RsurlOpt::CustomRequest,
3 => RsurlOpt::Header,
4 => RsurlOpt::PostFieldsString,
5 => RsurlOpt::ConnectTimeout,
6 => RsurlOpt::Timeout,
7 => RsurlOpt::UserAgent,
_ => return None,
})
}
#[no_mangle]
pub extern "C" fn rsurl_easy_perform(handle: *mut RSURL) -> RsurlCode {
ffi_guard(RsurlCode::Network, || {
let Some(h) = handle_mut(handle) else {
return RsurlCode::InvalidHandle;
};
let Some(url) = h.url.as_deref() else {
return RsurlCode::InvalidArg;
};
let method = h.method.clone().unwrap_or_else(|| {
if h.body.is_some() {
"POST".to_string()
} else {
"GET".to_string()
}
});
let mut req = match Request::new(&method, url) {
Ok(r) => r,
Err(crate::Error::UnsupportedScheme(_)) => return RsurlCode::Unsupported,
Err(_) => return RsurlCode::InvalidArg,
};
for (k, v) in &h.headers {
req = req.header(k, v);
}
if let Some(ua) = &h.user_agent {
req = req.header("User-Agent", ua);
}
if let Some(body) = h.body.clone() {
req = req.body(body);
}
match req.send() {
Ok(resp) => {
h.header_buf = resp
.headers
.iter()
.map(|(k, v)| {
let mut s = format!("{k}: {v}").into_bytes();
s.push(0);
s
})
.collect();
h.last_response = Some(resp);
RsurlCode::Ok
}
Err(crate::Error::UnsupportedScheme(_)) => RsurlCode::Unsupported,
Err(crate::Error::Io(_)) | Err(crate::Error::UnexpectedEof) => RsurlCode::Network,
Err(crate::Error::BadResponse(_)) | Err(crate::Error::H2NotNegotiated) => {
RsurlCode::BadResponse
}
Err(crate::Error::InvalidUrl(_)) => RsurlCode::InvalidArg,
}
})
}
#[no_mangle]
pub unsafe extern "C" fn rsurl_easy_response_body(
handle: *const RSURL,
out_ptr: *mut *const u8,
out_len: *mut usize,
) -> RsurlCode {
ffi_guard(RsurlCode::Network, || {
let Some(h) = handle_ref(handle) else {
return RsurlCode::InvalidHandle;
};
if out_ptr.is_null() || out_len.is_null() {
return RsurlCode::InvalidArg;
}
match &h.last_response {
Some(resp) => unsafe {
*out_ptr = resp.body.as_ptr();
*out_len = resp.body.len();
},
None => unsafe {
*out_ptr = ptr::null();
*out_len = 0;
},
}
RsurlCode::Ok
})
}
#[no_mangle]
pub extern "C" fn rsurl_easy_response_status(handle: *const RSURL) -> c_long {
ffi_guard(0, || {
handle_ref(handle)
.and_then(|h| h.last_response.as_ref())
.map(|r| r.status as c_long)
.unwrap_or(0)
})
}
#[no_mangle]
pub extern "C" fn rsurl_easy_response_header(handle: *const RSURL, index: usize) -> *const c_char {
ffi_guard(ptr::null(), || {
let Some(h) = handle_ref(handle) else {
return ptr::null();
};
h.header_buf
.get(index)
.map(|b| b.as_ptr() as *const c_char)
.unwrap_or(ptr::null())
})
}
#[no_mangle]
pub extern "C" fn rsurl_easy_response_header_count(handle: *const RSURL) -> usize {
ffi_guard(0, || {
handle_ref(handle).map(|h| h.header_buf.len()).unwrap_or(0)
})
}
#[no_mangle]
pub extern "C" fn rsurl_strerror(code: c_int) -> *const c_char {
ffi_guard(ptr::null(), || {
let s: &'static [u8] = match code {
0 => b"ok\0",
1 => b"invalid handle\0",
2 => b"unknown option\0",
3 => b"invalid argument\0",
4 => b"no response available\0",
5 => b"network error\0",
6 => b"bad response\0",
7 => b"unsupported scheme or feature\0",
_ => b"unknown error\0",
};
s.as_ptr() as *const c_char
})
}
#[no_mangle]
pub extern "C" fn rsurl_version() -> *const c_char {
ffi_guard(ptr::null(), || {
concat!("rsurl/", env!("CARGO_PKG_VERSION"), "\0").as_ptr() as *const c_char
})
}