use std::ffi::{CStr, CString, c_char, c_uint};
use asdf_core::Version;
use crate::panic::guard;
#[repr(C)]
#[derive(Debug)]
pub struct asdf_version_t {
pub version: *const c_char,
pub major: c_uint,
pub minor: c_uint,
pub patch: c_uint,
pub extra: *const c_char,
}
fn into_c_string(s: &str) -> *const c_char {
match CString::new(s) {
Ok(c) => c.into_raw().cast_const(),
Err(_) => std::ptr::null(),
}
}
unsafe fn free_c_string(p: *const c_char) {
if !p.is_null() {
drop(unsafe { CString::from_raw(p.cast_mut()) });
}
}
fn to_ffi(v: &Version) -> *mut asdf_version_t {
let version = into_c_string(&v.version);
if version.is_null() && !v.version.is_empty() {
return std::ptr::null_mut();
}
let extra = match &v.extra {
Some(e) => into_c_string(e),
None => std::ptr::null(),
};
Box::into_raw(Box::new(asdf_version_t {
version,
major: v.major,
minor: v.minor,
patch: v.patch,
extra,
}))
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn asdf_version_parse(version: *const c_char) -> *mut asdf_version_t {
guard("asdf_version_parse", std::ptr::null_mut(), || {
if version.is_null() {
return std::ptr::null_mut();
}
let text = unsafe { CStr::from_ptr(version) }.to_string_lossy().into_owned();
to_ffi(&Version::parse(&text))
})
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn asdf_version_copy(version: *const asdf_version_t) -> *mut asdf_version_t {
guard("asdf_version_copy", std::ptr::null_mut(), || {
if version.is_null() {
return std::ptr::null_mut();
}
let src = unsafe { &*version };
let version_str = if src.version.is_null() {
std::ptr::null()
} else {
let s = unsafe { CStr::from_ptr(src.version) };
match CString::new(s.to_bytes()) {
Ok(c) => c.into_raw().cast_const(),
Err(_) => return std::ptr::null_mut(),
}
};
let extra_str = if src.extra.is_null() {
std::ptr::null()
} else {
let s = unsafe { CStr::from_ptr(src.extra) };
match CString::new(s.to_bytes()) {
Ok(c) => c.into_raw().cast_const(),
Err(_) => {
unsafe { free_c_string(version_str) };
return std::ptr::null_mut();
}
}
};
Box::into_raw(Box::new(asdf_version_t {
version: version_str,
major: src.major,
minor: src.minor,
patch: src.patch,
extra: extra_str,
}))
})
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn asdf_version_destroy(version: *mut asdf_version_t) {
guard("asdf_version_destroy", (), || {
if version.is_null() {
return;
}
let boxed = unsafe { Box::from_raw(version) };
unsafe { free_c_string(boxed.version) };
unsafe { free_c_string(boxed.extra) };
})
}
#[cfg(test)]
mod tests {
use super::*;
fn parse(s: &str) -> *mut asdf_version_t {
let c = CString::new(s).unwrap();
unsafe { asdf_version_parse(c.as_ptr()) }
}
fn version_str(v: *const asdf_version_t) -> Option<String> {
let v = unsafe { &*v };
(!v.version.is_null()).then(|| {
unsafe { crate::ffi::c_str(v.version) }
.map_or_else(String::new, |s| s.to_string_lossy().into_owned())
})
}
fn extra_str(v: *const asdf_version_t) -> Option<String> {
let v = unsafe { &*v };
(!v.extra.is_null()).then(|| {
unsafe { crate::ffi::c_str(v.extra) }
.map_or_else(String::new, |s| s.to_string_lossy().into_owned())
})
}
#[test]
fn parses_and_frees() {
let v = parse("1.6.0");
assert!(!v.is_null());
unsafe {
assert_eq!((*v).major, 1);
assert_eq!((*v).minor, 6);
assert_eq!((*v).patch, 0);
assert!((*v).extra.is_null());
}
assert_eq!(version_str(v).as_deref(), Some("1.6.0"));
unsafe { asdf_version_destroy(v) };
}
#[test]
fn carries_the_extra_field() {
let v = parse("0.1.0.dev4");
assert_eq!(extra_str(v).as_deref(), Some("dev4"));
assert_eq!(version_str(v).as_deref(), Some("0.1.0.dev4"));
unsafe { asdf_version_destroy(v) };
}
#[test]
fn non_semver_is_preserved() {
let v = parse("not-a-version");
unsafe {
assert_eq!((*v).major, 0);
assert_eq!((*v).minor, 0);
assert_eq!((*v).patch, 0);
}
assert_eq!(version_str(v).as_deref(), Some("not-a-version"));
unsafe { asdf_version_destroy(v) };
}
#[test]
fn copy_is_deep() {
let a = parse("1.2.3-rc1");
let b = unsafe { asdf_version_copy(a) };
assert!(!b.is_null());
unsafe {
assert_ne!((*a).version, (*b).version);
assert_ne!((*a).extra, (*b).extra);
}
assert_eq!(version_str(a), version_str(b));
assert_eq!(extra_str(a), extra_str(b));
unsafe { asdf_version_destroy(a) };
assert_eq!(version_str(b).as_deref(), Some("1.2.3-rc1"));
unsafe { asdf_version_destroy(b) };
}
#[test]
fn null_arguments_are_handled() {
assert!(unsafe { asdf_version_parse(std::ptr::null()) }.is_null());
assert!(unsafe { asdf_version_copy(std::ptr::null()) }.is_null());
unsafe { asdf_version_destroy(std::ptr::null_mut()) };
}
#[test]
fn struct_layout_matches_the_header() {
use std::mem::{align_of, offset_of, size_of};
assert_eq!(offset_of!(asdf_version_t, version), 0);
assert_eq!(offset_of!(asdf_version_t, major), size_of::<*const c_char>());
assert_eq!(
offset_of!(asdf_version_t, minor),
size_of::<*const c_char>() + size_of::<c_uint>()
);
assert_eq!(
offset_of!(asdf_version_t, patch),
size_of::<*const c_char>() + 2 * size_of::<c_uint>()
);
assert_eq!(align_of::<asdf_version_t>(), align_of::<*const c_char>());
}
}