use once_cell::sync::Lazy;
use std::ffi::{CStr, CString};
use std::os::raw::c_char;
use std::ptr;
use tokio::runtime::Runtime;
static RUNTIME: Lazy<Runtime> =
Lazy::new(|| Runtime::new().expect("Failed to create tokio runtime for FFI"));
#[repr(C)]
#[derive(Debug, Clone, Copy)]
pub enum ButterflyResult {
Success = 0,
InvalidParameter = 1,
NetworkError = 2,
IoError = 3,
UnknownError = 4,
}
pub type ProgressCallback =
extern "C" fn(downloaded: u64, total: u64, user_data: *mut std::ffi::c_void);
fn convert_error(result: crate::Result<()>) -> ButterflyResult {
match result {
Ok(()) => ButterflyResult::Success,
Err(crate::Error::SourceNotFound(_)) | Err(crate::Error::InvalidInput(_)) => {
ButterflyResult::InvalidParameter
}
Err(crate::Error::NetworkError(_)) | Err(crate::Error::HttpError(_)) => {
ButterflyResult::NetworkError
}
Err(crate::Error::IoError(_)) => ButterflyResult::IoError,
_ => ButterflyResult::UnknownError,
}
}
#[no_mangle]
pub unsafe extern "C" fn butterfly_download(
source: *const c_char,
dest_path: *const c_char,
) -> ButterflyResult {
if source.is_null() {
return ButterflyResult::InvalidParameter;
}
let source_str = match unsafe { CStr::from_ptr(source) }.to_str() {
Ok(s) => s,
Err(_) => return ButterflyResult::InvalidParameter,
};
let dest_str = if dest_path.is_null() {
None
} else {
match unsafe { CStr::from_ptr(dest_path) }.to_str() {
Ok(s) => Some(s),
Err(_) => return ButterflyResult::InvalidParameter,
}
};
let result = RUNTIME.block_on(async { crate::get(source_str, dest_str).await });
convert_error(result)
}
#[no_mangle]
pub unsafe extern "C" fn butterfly_download_with_progress(
source: *const c_char,
dest_path: *const c_char,
progress_callback: Option<ProgressCallback>,
user_data: *mut std::ffi::c_void,
) -> ButterflyResult {
if source.is_null() {
return ButterflyResult::InvalidParameter;
}
let source_str = match unsafe { CStr::from_ptr(source) }.to_str() {
Ok(s) => s,
Err(_) => return ButterflyResult::InvalidParameter,
};
let dest_str = if dest_path.is_null() {
None
} else {
match unsafe { CStr::from_ptr(dest_path) }.to_str() {
Ok(s) => Some(s),
Err(_) => return ButterflyResult::InvalidParameter,
}
};
let result = if let Some(callback) = progress_callback {
let user_data_addr = user_data as usize;
RUNTIME.block_on(async move {
crate::get_with_progress(source_str, dest_str, move |downloaded, total| {
let user_data_ptr = user_data_addr as *mut std::ffi::c_void;
callback(downloaded, total, user_data_ptr);
})
.await
})
} else {
RUNTIME.block_on(async { crate::get(source_str, dest_str).await })
};
convert_error(result)
}
#[no_mangle]
pub unsafe extern "C" fn butterfly_get_filename(source: *const c_char) -> *mut c_char {
if source.is_null() {
return ptr::null_mut();
}
let source_str = match unsafe { CStr::from_ptr(source) }.to_str() {
Ok(s) => s,
Err(_) => return ptr::null_mut(),
};
let filename = crate::core::resolve_output_filename(source_str);
match CString::new(filename) {
Ok(c_string) => c_string.into_raw(),
Err(_) => ptr::null_mut(),
}
}
#[no_mangle]
pub unsafe extern "C" fn butterfly_free_string(ptr: *mut c_char) {
if !ptr.is_null() {
unsafe {
drop(CString::from_raw(ptr));
}
}
}
#[no_mangle]
pub extern "C" fn butterfly_version() -> *const c_char {
use std::sync::OnceLock;
static VERSION_STRING: OnceLock<std::ffi::CString> = OnceLock::new();
VERSION_STRING
.get_or_init(|| {
std::ffi::CString::new(format!("butterfly-dl {}", env!("BUTTERFLY_VERSION")))
.expect("Version string contains null byte")
})
.as_ptr()
}
#[no_mangle]
pub extern "C" fn butterfly_init() -> ButterflyResult {
Lazy::force(&RUNTIME);
ButterflyResult::Success
}
#[cfg(test)]
mod tests {
use super::*;
use std::ffi::CString;
#[test]
fn test_butterfly_version() {
let version = butterfly_version();
let version_str = unsafe { CStr::from_ptr(version) }.to_str().unwrap();
assert!(version_str.contains("butterfly-dl"));
assert!(version_str.contains(env!("BUTTERFLY_VERSION")));
}
#[test]
fn test_butterfly_get_filename() {
let source = CString::new("europe/belgium").unwrap();
let filename_ptr = unsafe { butterfly_get_filename(source.as_ptr()) };
assert!(!filename_ptr.is_null());
let filename = unsafe { CStr::from_ptr(filename_ptr) }.to_str().unwrap();
assert_eq!(filename, "belgium-latest.osm.pbf");
unsafe { butterfly_free_string(filename_ptr) };
}
#[test]
fn test_butterfly_init() {
let result = butterfly_init();
assert_eq!(result as u32, ButterflyResult::Success as u32);
}
#[test]
fn test_invalid_parameters() {
let result = unsafe { butterfly_download(std::ptr::null(), std::ptr::null()) };
assert_eq!(result as u32, ButterflyResult::InvalidParameter as u32);
}
}