use core::ffi::{c_char, c_void};
#[cfg(feature = "async")]
use std::ffi::CStr;
use std::ffi::CString;
use std::fmt;
use std::ptr;
#[cfg(feature = "async")]
use std::sync::{Mutex, OnceLock};
#[cfg(feature = "async")]
use doom_fish_utils::stream::{AsyncStreamSender, BoundedAsyncStream};
use serde::{Deserialize, Serialize};
use crate::asset_pack::AssetPackSnapshot;
#[cfg(feature = "async")]
use crate::download::{register_download_manager_delegate, DownloadManagerDelegate, DownloadManagerEventStream};
use crate::download::{ContentRequest, Download, DownloadSnapshot};
use crate::error::BackgroundAssetsError;
use crate::ffi;
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct AppExtensionInfoSnapshot {
pub restricted_download_size_remaining: Option<i64>,
pub restricted_essential_download_size_remaining: Option<i64>,
}
pub struct AppExtensionInfo {
ptr: *mut c_void,
}
impl AppExtensionInfo {
#[cfg(feature = "async")]
pub(crate) fn from_raw(ptr: *mut c_void) -> Option<Self> {
(!ptr.is_null()).then_some(Self { ptr })
}
#[cfg(feature = "async")]
pub(crate) unsafe fn retained_from_borrowed(ptr: *mut c_void) -> Option<Self> {
Self::from_raw(ffi::retained(ptr))
}
pub fn snapshot(&self) -> AppExtensionInfoSnapshot {
let json = unsafe { ffi::owned_string(ffi::ba_app_extension_info_snapshot_json(self.ptr)) };
serde_json::from_str(&json).unwrap_or_default()
}
pub fn restricted_download_size_remaining(&self) -> Option<i64> {
self.snapshot().restricted_download_size_remaining
}
pub fn restricted_essential_download_size_remaining(&self) -> Option<i64> {
self.snapshot().restricted_essential_download_size_remaining
}
}
impl Clone for AppExtensionInfo {
fn clone(&self) -> Self {
Self {
ptr: ffi::retained(self.ptr),
}
}
}
impl Drop for AppExtensionInfo {
fn drop(&mut self) {
if !self.ptr.is_null() {
unsafe { ffi::ba_object_release(self.ptr) };
}
}
}
impl fmt::Debug for AppExtensionInfo {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("AppExtensionInfo")
.field("snapshot", &self.snapshot())
.finish()
}
}
unsafe impl Send for AppExtensionInfo {}
unsafe impl Sync for AppExtensionInfo {}
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Serialize)]
#[repr(i32)]
pub enum ChallengeDisposition {
UseCredential = 0,
#[default]
PerformDefaultHandling = 1,
CancelAuthenticationChallenge = 2,
RejectProtectionSpace = 3,
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct AuthenticationChallenge {
pub host: String,
pub authentication_method: String,
pub previous_failure_count: i64,
pub proposed_credential_user: Option<String>,
pub proposed_credential_has_password: bool,
}
pub trait DownloaderExtensionHandler: Send + 'static {
fn should_download_asset_pack(&mut self, _asset_pack: &AssetPackSnapshot) -> bool {
true
}
fn downloads(
&mut self,
request: ContentRequest,
manifest_url: &str,
extension_info: &AppExtensionInfo,
) -> Result<Vec<Download>, BackgroundAssetsError>;
fn did_receive_challenge(
&mut self,
_download: &Download,
_challenge: &AuthenticationChallenge,
) -> ChallengeDisposition {
ChallengeDisposition::PerformDefaultHandling
}
fn download_failed(&mut self, _download: &Download, _error: &BackgroundAssetsError) {}
fn download_finished(&mut self, _download: &Download, _file_url: &str) {}
fn extension_will_terminate(&mut self) {}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum ExtensionEvent {
ShouldDownloadAssetPack {
asset_pack: AssetPackSnapshot,
should_download: bool,
},
DownloadsRequested {
request: ContentRequest,
manifest_url: String,
extension_info: AppExtensionInfoSnapshot,
planned_downloads: Vec<DownloadSnapshot>,
},
DownloadPlanFailed {
request: ContentRequest,
manifest_url: String,
error: BackgroundAssetsError,
},
ChallengeRequested {
download: DownloadSnapshot,
challenge: AuthenticationChallenge,
disposition: ChallengeDisposition,
},
DownloadFailed {
download: DownloadSnapshot,
error: BackgroundAssetsError,
},
DownloadFinished {
download: DownloadSnapshot,
file_url: String,
},
Terminating,
}
#[cfg(feature = "async")]
pub struct ExtensionEventStream {
inner: BoundedAsyncStream<ExtensionEvent>,
}
#[cfg(feature = "async")]
impl ExtensionEventStream {
pub fn next(&self) -> impl std::future::Future<Output = Option<ExtensionEvent>> + '_ {
self.inner.next()
}
pub fn try_next(&self) -> Option<ExtensionEvent> {
self.inner.try_next()
}
pub fn is_closed(&self) -> bool {
self.inner.is_closed()
}
pub fn buffered_count(&self) -> usize {
self.inner.buffered_count()
}
}
#[cfg(feature = "async")]
impl fmt::Debug for ExtensionEventStream {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ExtensionEventStream")
.field("buffered_count", &self.buffered_count())
.field("is_closed", &self.is_closed())
.finish()
}
}
#[cfg(feature = "async")]
pub struct ManagedDownloaderExtensionConfiguration<D> {
download_manager_delegate: D,
extension_event_capacity: usize,
download_manager_event_capacity: usize,
}
#[cfg(feature = "async")]
impl<D> ManagedDownloaderExtensionConfiguration<D> {
pub fn new(download_manager_delegate: D) -> Self {
Self {
download_manager_delegate,
extension_event_capacity: 16,
download_manager_event_capacity: 16,
}
}
pub fn extension_event_capacity(mut self, capacity: usize) -> Self {
self.extension_event_capacity = capacity.max(1);
self
}
pub fn download_manager_event_capacity(mut self, capacity: usize) -> Self {
self.download_manager_event_capacity = capacity.max(1);
self
}
}
#[cfg(feature = "async")]
impl<D> fmt::Debug for ManagedDownloaderExtensionConfiguration<D> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ManagedDownloaderExtensionConfiguration")
.field("extension_event_capacity", &self.extension_event_capacity)
.field(
"download_manager_event_capacity",
&self.download_manager_event_capacity,
)
.finish_non_exhaustive()
}
}
#[cfg(feature = "async")]
pub struct ManagedDownloaderExtensionRegistration {
extension_events: ExtensionEventStream,
download_manager_events: DownloadManagerEventStream,
}
#[cfg(feature = "async")]
impl ManagedDownloaderExtensionRegistration {
pub fn extension_events(&self) -> &ExtensionEventStream {
&self.extension_events
}
pub fn download_manager_events(&self) -> &DownloadManagerEventStream {
&self.download_manager_events
}
pub fn into_parts(self) -> (ExtensionEventStream, DownloadManagerEventStream) {
(self.extension_events, self.download_manager_events)
}
}
#[cfg(feature = "async")]
impl fmt::Debug for ManagedDownloaderExtensionRegistration {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ManagedDownloaderExtensionRegistration")
.field("extension_events", &self.extension_events)
.field("download_manager_events", &self.download_manager_events)
.finish()
}
}
#[cfg(feature = "async")]
struct ExtensionState {
handler: Box<dyn DownloaderExtensionHandler>,
sender: AsyncStreamSender<ExtensionEvent>,
}
#[cfg(feature = "async")]
fn extension_state() -> &'static Mutex<Option<ExtensionState>> {
static STATE: OnceLock<Mutex<Option<ExtensionState>>> = OnceLock::new();
STATE.get_or_init(|| Mutex::new(None))
}
#[cfg(feature = "async")]
pub fn install_global_downloader_extension<H>(handler: H, capacity: usize) -> ExtensionEventStream
where
H: DownloaderExtensionHandler,
{
let (stream, sender) = BoundedAsyncStream::new(capacity.max(1));
if let Ok(mut state) = extension_state().lock() {
*state = Some(ExtensionState {
handler: Box::new(handler),
sender,
});
}
ExtensionEventStream { inner: stream }
}
#[cfg(feature = "async")]
pub fn install_global_managed_downloader_extension<H, D>(
handler: H,
configuration: ManagedDownloaderExtensionConfiguration<D>,
) -> ManagedDownloaderExtensionRegistration
where
H: DownloaderExtensionHandler,
D: DownloadManagerDelegate,
{
let ManagedDownloaderExtensionConfiguration {
download_manager_delegate,
extension_event_capacity,
download_manager_event_capacity,
} = configuration;
let extension_events = install_global_downloader_extension(handler, extension_event_capacity);
let download_manager_events =
register_download_manager_delegate(download_manager_delegate, download_manager_event_capacity);
ManagedDownloaderExtensionRegistration {
extension_events,
download_manager_events,
}
}
#[cfg(feature = "async")]
fn string_from_ptr(ptr: *const c_char) -> String {
if ptr.is_null() {
String::new()
} else {
unsafe { CStr::from_ptr(ptr) }
.to_string_lossy()
.into_owned()
}
}
fn json_cstring<T: Serialize>(value: &T) -> *mut c_char {
serde_json::to_string(value)
.ok()
.and_then(|json| CString::new(json).ok())
.map_or(ptr::null_mut(), CString::into_raw)
}
#[derive(Deserialize)]
struct AssetPackSnapshotPayload {
id: String,
#[serde(rename = "downloadSize")]
download_size: i64,
version: i64,
description: String,
}
impl From<AssetPackSnapshotPayload> for AssetPackSnapshot {
fn from(value: AssetPackSnapshotPayload) -> Self {
Self {
id: value.id,
download_size: value.download_size,
version: value.version,
description: value.description,
}
}
}
fn asset_pack_snapshot_from_json(json: &str) -> Option<AssetPackSnapshot> {
serde_json::from_str::<AssetPackSnapshotPayload>(json)
.ok()
.map(Into::into)
}
#[no_mangle]
pub unsafe extern "C" fn ba_rust_extension_should_download_asset_pack(
asset_pack_json: *const c_char,
) -> bool {
#[cfg(not(feature = "async"))]
{
let _ = asset_pack_json;
true
}
#[cfg(feature = "async")]
{
let Some(asset_pack) = asset_pack_snapshot_from_json(&string_from_ptr(asset_pack_json)) else {
return true;
};
let Ok(mut state_guard) = extension_state().lock() else {
return true;
};
let Some(state) = state_guard.as_mut() else {
return true;
};
let should_download = state.handler.should_download_asset_pack(&asset_pack);
state.sender.push(ExtensionEvent::ShouldDownloadAssetPack {
asset_pack,
should_download,
});
should_download
}
}
#[no_mangle]
pub unsafe extern "C" fn ba_rust_extension_downloads_for_request(
request: i32,
manifest_url: *const c_char,
extension_info: *mut c_void,
) -> *mut c_char {
#[cfg(not(feature = "async"))]
{
let _ = request;
let _ = manifest_url;
let _ = extension_info;
json_cstring(&Vec::<u64>::new())
}
#[cfg(feature = "async")]
{
let request = ContentRequest::from_raw(request as isize).unwrap_or(ContentRequest::Install);
let manifest_url = string_from_ptr(manifest_url);
let Some(extension_info) =
(unsafe { AppExtensionInfo::retained_from_borrowed(extension_info) })
else {
return json_cstring(&Vec::<u64>::new());
};
let Ok(mut state_guard) = extension_state().lock() else {
return json_cstring(&Vec::<u64>::new());
};
let Some(state) = state_guard.as_mut() else {
return json_cstring(&Vec::<u64>::new());
};
match state
.handler
.downloads(request, &manifest_url, &extension_info)
{
Ok(downloads) => {
state.sender.push(ExtensionEvent::DownloadsRequested {
request,
manifest_url: manifest_url.clone(),
extension_info: extension_info.snapshot(),
planned_downloads: downloads.iter().map(Download::snapshot).collect(),
});
let retained_ptrs: Vec<u64> = downloads
.iter()
.filter_map(|download| {
let ptr = ffi::retained(download.raw_ptr());
(!ptr.is_null()).then_some(ptr as usize as u64)
})
.collect();
json_cstring(&retained_ptrs)
}
Err(error) => {
state.sender.push(ExtensionEvent::DownloadPlanFailed {
request,
manifest_url,
error,
});
json_cstring(&Vec::<u64>::new())
}
}
}
}
#[no_mangle]
pub unsafe extern "C" fn ba_rust_extension_challenge_disposition(
download: *mut c_void,
challenge_json: *const c_char,
) -> i32 {
#[cfg(not(feature = "async"))]
{
let _ = download;
let _ = challenge_json;
ChallengeDisposition::PerformDefaultHandling as i32
}
#[cfg(feature = "async")]
{
let Some(download) = (unsafe { Download::retained_from_borrowed(download) }) else {
return ChallengeDisposition::PerformDefaultHandling as i32;
};
let challenge =
serde_json::from_str::<AuthenticationChallenge>(&string_from_ptr(challenge_json))
.unwrap_or_default();
let Ok(mut state_guard) = extension_state().lock() else {
return ChallengeDisposition::PerformDefaultHandling as i32;
};
let Some(state) = state_guard.as_mut() else {
return ChallengeDisposition::PerformDefaultHandling as i32;
};
let disposition = state.handler.did_receive_challenge(&download, &challenge);
state.sender.push(ExtensionEvent::ChallengeRequested {
download: download.snapshot(),
challenge,
disposition,
});
disposition as i32
}
}
#[no_mangle]
pub unsafe extern "C" fn ba_rust_extension_download_failed(
download: *mut c_void,
error_json: *const c_char,
) {
#[cfg(not(feature = "async"))]
{
let _ = download;
let _ = error_json;
}
#[cfg(feature = "async")]
{
let Some(download) = (unsafe { Download::retained_from_borrowed(download) }) else {
return;
};
let error = BackgroundAssetsError::from_json_str(&string_from_ptr(error_json));
if let Ok(mut state_guard) = extension_state().lock() {
if let Some(state) = state_guard.as_mut() {
state.handler.download_failed(&download, &error);
state.sender.push(ExtensionEvent::DownloadFailed {
download: download.snapshot(),
error,
});
}
}
}
}
#[no_mangle]
pub unsafe extern "C" fn ba_rust_extension_download_finished(
download: *mut c_void,
file_url: *const c_char,
) {
#[cfg(not(feature = "async"))]
{
let _ = download;
let _ = file_url;
}
#[cfg(feature = "async")]
{
let Some(download) = (unsafe { Download::retained_from_borrowed(download) }) else {
return;
};
let file_url = string_from_ptr(file_url);
if let Ok(mut state_guard) = extension_state().lock() {
if let Some(state) = state_guard.as_mut() {
state.handler.download_finished(&download, &file_url);
state.sender.push(ExtensionEvent::DownloadFinished {
download: download.snapshot(),
file_url,
});
}
}
}
}
#[no_mangle]
pub extern "C" fn ba_rust_extension_will_terminate() {
#[cfg(feature = "async")]
{
if let Ok(mut state_guard) = extension_state().lock() {
if let Some(state) = state_guard.as_mut() {
state.handler.extension_will_terminate();
state.sender.push(ExtensionEvent::Terminating);
}
}
}
}