#![allow(non_snake_case)]
pub mod delegate;
pub mod cookies;
pub use cookies::FoundationCookieStorage;
pub mod websocket;
pub use websocket::{FoundationWebSocket, FoundationWebSocketBuilder};
pub mod error;
use crate::backend::types::{BackendRequest, BackendResponse};
use crate::{Error, Result};
use block2::StackBlock;
use objc2::rc::Retained;
use objc2::runtime::AnyObject;
use objc2::runtime::Bool;
use objc2::runtime::ProtocolObject;
use objc2_foundation::{NSHTTPURLResponse, NSMutableURLRequest, NSString, NSURL, NSURLSession};
use std::ptr::NonNull;
use tokio::sync::mpsc;
use url::Url;
#[derive(Clone)]
pub struct FoundationBackend {
session: Retained<NSURLSession>,
delegate: Retained<delegate::DataTaskDelegate>,
cookie_jar: Option<crate::CookieJar>,
default_headers: Option<http::HeaderMap>,
}
impl FoundationBackend {
pub fn new() -> Result<Self> {
let delegate = delegate::DataTaskDelegate::new();
let session = unsafe {
NSURLSession::sessionWithConfiguration_delegate_delegateQueue(
&objc2_foundation::NSURLSessionConfiguration::defaultSessionConfiguration(),
Some(ProtocolObject::from_ref(&*delegate)),
None,
)
};
Ok(Self {
session,
delegate,
cookie_jar: None,
default_headers: None,
})
}
pub fn with_session(
session: Retained<NSURLSession>,
delegate: Retained<delegate::DataTaskDelegate>,
) -> Self {
Self {
session,
delegate,
cookie_jar: None,
default_headers: None,
}
}
pub fn with_config(config: crate::backend::BackendConfig) -> Result<Self> {
let delegate = delegate::DataTaskDelegate::new();
let session_config =
unsafe { objc2_foundation::NSURLSessionConfiguration::defaultSessionConfiguration() };
if let Some(timeout) = config.timeout {
unsafe {
session_config.setTimeoutIntervalForRequest(timeout.as_secs_f64());
}
}
let session = unsafe {
NSURLSession::sessionWithConfiguration_delegate_delegateQueue(
&session_config,
Some(ProtocolObject::from_ref(&*delegate)),
None,
)
};
Ok(Self {
session,
delegate,
cookie_jar: config.cookie_jar,
default_headers: config.default_headers,
})
}
pub fn session(&self) -> &Retained<NSURLSession> {
&self.session
}
pub async fn execute(&self, request: BackendRequest) -> Result<BackendResponse> {
let nsurl = unsafe {
NSURL::URLWithString(&NSString::from_str(&request.url.as_str()))
.ok_or(Error::InvalidUrl)?
};
let scheme = unsafe { nsurl.scheme() };
if let Some(scheme_str) = scheme {
let scheme_string = scheme_str.to_string();
match scheme_string.as_str() {
"http" | "https" => {}
_ => {
return Err(Error::InvalidUrl);
}
}
} else {
return Err(Error::InvalidUrl);
}
let nsrequest = unsafe {
let req = NSMutableURLRequest::requestWithURL(&nsurl);
req.setHTTPMethod(&NSString::from_str(request.method.as_str()));
if let Some(ref default_headers) = self.default_headers {
for (name, value) in default_headers {
req.setValue_forHTTPHeaderField(
Some(&NSString::from_str(
value.to_str().expect("Invalid default header value"),
)),
&NSString::from_str(name.as_str()),
);
}
}
for (name, value) in &request.headers {
req.setValue_forHTTPHeaderField(
Some(&NSString::from_str(
value.to_str().expect("Invalid header value"),
)),
&NSString::from_str(name.as_str()),
);
}
if let Some(body) = &request.body {
match body {
crate::body::Body::Empty => {}
crate::body::Body::Bytes {
content,
content_type,
} => {
req.setValue_forHTTPHeaderField(
Some(&NSString::from_str(content_type)),
&NSString::from_str("Content-Type"),
);
let nsdata = objc2_foundation::NSData::from_vec(content.to_vec());
req.setHTTPBody(Some(&nsdata));
}
crate::body::Body::Form { fields } => {
req.setValue_forHTTPHeaderField(
Some(&NSString::from_str("application/x-www-form-urlencoded")),
&NSString::from_str("Content-Type"),
);
let encoded = encode_form_fields(fields);
let nsdata = objc2_foundation::NSData::from_vec(encoded.into_bytes());
req.setHTTPBody(Some(&nsdata));
}
#[cfg(feature = "json")]
crate::body::Body::Json { value } => {
req.setValue_forHTTPHeaderField(
Some(&NSString::from_str("application/json")),
&NSString::from_str("Content-Type"),
);
let json_bytes = serde_json::to_vec(value)?;
let nsdata = objc2_foundation::NSData::from_vec(json_bytes);
req.setHTTPBody(Some(&nsdata));
}
#[cfg(feature = "multipart")]
crate::body::Body::Multipart { parts } => {
let boundary = generate_boundary();
let content_type = format!("multipart/form-data; boundary={}", boundary);
req.setValue_forHTTPHeaderField(
Some(&NSString::from_str(&content_type)),
&NSString::from_str("Content-Type"),
);
let multipart_data = encode_multipart_data(&boundary, parts)?;
let nsdata = objc2_foundation::NSData::from_vec(multipart_data);
req.setHTTPBody(Some(&nsdata));
}
}
}
req
};
let task_context = if let Some(progress_callback) = request.progress_callback {
std::sync::Arc::new(
delegate::shared_context::TaskSharedContext::with_progress_callback(
progress_callback,
),
)
} else {
std::sync::Arc::new(delegate::shared_context::TaskSharedContext::new())
};
let data_task = unsafe { self.session.dataTaskWithRequest(&nsrequest) };
let task_id = unsafe { data_task.taskIdentifier() } as usize;
self.delegate.register_task(task_id, task_context.clone());
let (tx, rx) = mpsc::channel(32);
unsafe {
data_task.resume();
}
while !task_context.is_completed() && task_context.response.load_full().is_none() {
tokio::task::yield_now().await;
}
if let Some(error) = task_context.error.load_full() {
return Err(Error::from_ns_error(&*error));
}
let response = task_context
.response
.load_full()
.ok_or_else(|| Error::Internal("No response received".to_string()))?;
let status = unsafe {
let http_response: Option<&NSHTTPURLResponse> = response.as_ref().downcast_ref();
if let Some(http_response) = http_response {
http_response.statusCode() as u16
} else {
200
}
};
let status_code = http::StatusCode::from_u16(status as u16).unwrap_or(http::StatusCode::OK);
let mut headers = http::HeaderMap::new();
unsafe {
if let Some(http_response) = response.as_ref().downcast_ref::<NSHTTPURLResponse>() {
let all_headers = http_response.allHeaderFields();
use std::cell::RefCell;
use std::sync::Arc;
let headers_cell = Arc::new(RefCell::new(http::HeaderMap::new()));
let headers_cell_clone = headers_cell.clone();
let closure = move |key: NonNull<AnyObject>,
value: NonNull<AnyObject>,
_stop: NonNull<Bool>| {
if let Some(key_nsstring) = key.as_ref().downcast_ref::<NSString>() {
if let Some(value_nsstring) = value.as_ref().downcast_ref::<NSString>() {
let key_str = key_nsstring.to_string();
let value_str = value_nsstring.to_string();
if let (Ok(header_name), Ok(header_value)) = (
http::HeaderName::from_bytes(key_str.as_bytes()),
http::HeaderValue::from_str(&value_str),
) {
headers_cell_clone
.borrow_mut()
.insert(header_name, header_value);
}
}
}
};
let block = StackBlock::new(
move |key: NonNull<AnyObject>,
value: NonNull<AnyObject>,
stop: NonNull<Bool>| { closure(key, value, stop) },
);
let _: () =
objc2::msg_send![&*all_headers, enumerateKeysAndObjectsUsingBlock: &*block];
drop(block);
headers = Arc::try_unwrap(headers_cell)
.unwrap_or_else(|_| panic!("Headers RefCell still has references"))
.into_inner();
}
}
let body_context = task_context.clone();
tokio::spawn(async move {
while !body_context.is_completed() {
let data = body_context.response_buffer.lock().await.clone();
if !data.is_empty() {
let bytes = bytes::Bytes::from(data);
if tx.send(Ok(bytes)).await.is_err() {
break;
}
body_context.response_buffer.lock().await.clear();
}
tokio::task::yield_now().await;
}
let final_data = body_context.response_buffer.lock().await.clone();
if !final_data.is_empty() {
let bytes = bytes::Bytes::from(final_data);
let _ = tx.send(Ok(bytes)).await;
}
});
Ok(BackendResponse {
status: status_code,
headers,
body_receiver: rx,
})
}
pub async fn execute_background_download(
&self,
url: Url,
file_path: std::path::PathBuf,
session_identifier: Option<String>,
progress_callback: Option<Box<dyn Fn(u64, Option<u64>) + Send + Sync + 'static>>,
) -> Result<crate::client::download::DownloadResponse> {
use delegate::background_session::BackgroundSessionDelegate;
use delegate::shared_context::TaskSharedContext;
use objc2_foundation::NSURLSessionConfiguration;
use std::sync::Arc;
let session_id = session_identifier.unwrap_or_else(|| {
format!(
"frakt-bg-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs()
)
});
let session_config = unsafe {
NSURLSessionConfiguration::backgroundSessionConfigurationWithIdentifier(
&NSString::from_str(&session_id),
)
};
let delegate = BackgroundSessionDelegate::new();
let session = unsafe {
NSURLSession::sessionWithConfiguration_delegate_delegateQueue(
&session_config,
Some(ProtocolObject::from_ref(&*delegate)),
None,
)
};
let nsurl = unsafe {
NSURL::URLWithString(&NSString::from_str(url.as_str())).ok_or(Error::InvalidUrl)?
};
let download_task = unsafe { session.downloadTaskWithURL(&nsurl) };
let task_id = unsafe { download_task.taskIdentifier() } as usize;
let mut task_context = if let Some(callback) = progress_callback {
TaskSharedContext::with_progress_callback(Arc::new(callback))
} else {
TaskSharedContext::new()
};
task_context.download_context = Some(Arc::new(
delegate::shared_context::DownloadContext::new(Some(file_path.clone())),
));
let task_context = Arc::new(task_context);
delegate.register_task(task_id, task_context.clone());
unsafe {
download_task.resume();
}
while !task_context.is_completed() {
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
}
if let Some(error) = task_context.error.load_full() {
return Err(Error::from_ns_error(&*error));
}
let bytes_downloaded = task_context
.bytes_downloaded
.load(std::sync::atomic::Ordering::Relaxed);
Ok(crate::client::download::DownloadResponse {
file_path,
bytes_downloaded,
})
}
pub fn cookie_jar(&self) -> Option<&crate::CookieJar> {
self.cookie_jar.as_ref()
}
}
fn encode_form_fields(
fields: &[(
std::borrow::Cow<'static, str>,
std::borrow::Cow<'static, str>,
)],
) -> String {
fields
.iter()
.map(|(k, v)| format!("{}={}", urlencoding::encode(k), urlencoding::encode(v)))
.collect::<Vec<_>>()
.join("&")
}
#[cfg(feature = "multipart")]
fn generate_boundary() -> String {
use std::time::{SystemTime, UNIX_EPOCH};
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_nanos();
format!("----formdata-frakt-{}", timestamp)
}
#[cfg(feature = "multipart")]
fn encode_multipart_data(boundary: &str, parts: &[crate::body::MultipartPart]) -> Result<Vec<u8>> {
let mut data = Vec::new();
for part in parts {
data.extend_from_slice(format!("\r\n--{}\r\n", boundary).as_bytes());
let mut disposition = format!("Content-Disposition: form-data; name=\"{}\"", part.name);
if let Some(filename) = &part.filename {
disposition.push_str(&format!("; filename=\"{}\"", filename));
}
data.extend_from_slice(disposition.as_bytes());
data.extend_from_slice(b"\r\n");
if let Some(content_type) = &part.content_type {
data.extend_from_slice(format!("Content-Type: {}\r\n", content_type).as_bytes());
}
data.extend_from_slice(b"\r\n");
data.extend_from_slice(&part.content);
}
data.extend_from_slice(format!("\r\n--{}--\r\n", boundary).as_bytes());
Ok(data)
}