use std::ffi::{CStr, c_char, c_int, c_void};
use std::sync::Mutex;
use crate::panic::guard;
use crate::types::AsdfValueType;
use crate::version_ffi::asdf_version_t;
#[repr(C)]
#[derive(Debug)]
pub struct asdf_tag_t {
pub name: *const c_char,
pub version: *const asdf_version_t,
}
#[repr(C)]
#[derive(Debug)]
pub struct asdf_software_t {
pub name: *const c_char,
pub version: *const asdf_version_t,
pub author: *const c_char,
pub homepage: *const c_char,
}
pub type AsdfExtensionSerialize = Option<
unsafe extern "C" fn(
file: *mut crate::file_ffi::AsdfFile,
obj: *const c_void,
userdata: *const c_void,
) -> *mut crate::file_ffi::AsdfValue,
>;
pub type AsdfExtensionDeserialize = Option<
unsafe extern "C" fn(
value: *mut crate::file_ffi::AsdfValue,
userdata: *const c_void,
out: *mut *mut c_void,
) -> crate::types::AsdfValueErr,
>;
pub type AsdfExtensionCopy = Option<
unsafe extern "C" fn(
file: *mut crate::file_ffi::AsdfFile,
src: *const c_void,
dst: *mut c_void,
) -> bool,
>;
pub type AsdfExtensionDeinit = Option<unsafe extern "C" fn(obj: *mut c_void)>;
pub type AsdfExtensionMethod = Option<unsafe extern "C" fn()>;
pub const ASDF_EXTENSION_VTAB_MAX_METHODS: usize = 8;
pub const ASDF_EXTENSION_VTAB_METHODS: usize = 4;
#[repr(C)]
#[derive(Debug)]
pub struct asdf_extension_vtab_t {
pub serialize: AsdfExtensionSerialize,
pub deserialize: AsdfExtensionDeserialize,
pub copy: AsdfExtensionCopy,
pub deinit: AsdfExtensionDeinit,
pub _reserved:
[AsdfExtensionMethod; ASDF_EXTENSION_VTAB_MAX_METHODS - ASDF_EXTENSION_VTAB_METHODS],
}
#[repr(C)]
#[derive(Debug)]
pub struct asdf_extension_t {
pub tags: *const *const c_char,
pub software: *mut asdf_software_t,
pub vtab: *const asdf_extension_vtab_t,
pub size: usize,
pub userdata: *mut c_void,
}
#[derive(Clone, Copy)]
struct Registration {
extension: *const asdf_extension_t,
}
unsafe impl Send for Registration {}
static REGISTRY: Mutex<Vec<Registration>> = Mutex::new(Vec::new());
#[unsafe(no_mangle)]
pub unsafe extern "C" fn asdf_extension_register(ext: *mut asdf_extension_t) {
guard("asdf_extension_register", (), || {
if ext.is_null() {
return;
}
let mut registry = REGISTRY.lock().unwrap_or_else(|e| e.into_inner());
if registry.iter().any(|r| std::ptr::eq(r.extension, ext)) {
return;
}
registry.push(Registration { extension: ext });
})
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn asdf_extension_get(
file: *mut crate::file_ffi::AsdfFile,
tag: *const c_char,
) -> *const asdf_extension_t {
guard("asdf_extension_get", std::ptr::null(), || extension_get(file, tag))
}
pub(crate) fn extension_get(
file: *mut crate::file_ffi::AsdfFile,
tag: *const c_char,
) -> *const asdf_extension_t {
let _ = file;
if tag.is_null() {
return std::ptr::null();
}
let wanted = unsafe { crate::ffi::c_string_lossy(tag) }.unwrap_or_default();
let registry = REGISTRY.lock().unwrap_or_else(|e| e.into_inner());
for entry in registry.iter() {
let extension = unsafe { &*entry.extension };
if extension.tags.is_null() {
continue;
}
let mut index = 0isize;
loop {
let tag_ptr = unsafe { *extension.tags.offset(index) };
if tag_ptr.is_null() {
break;
}
let declared = unsafe { CStr::from_ptr(tag_ptr) }.to_string_lossy();
if tags_match(&declared, &wanted) {
return entry.extension;
}
index += 1;
}
}
std::ptr::null()
}
#[cfg(test)]
pub(crate) fn registrations_of(ext: *const asdf_extension_t) -> usize {
REGISTRY
.lock()
.unwrap_or_else(|e| e.into_inner())
.iter()
.filter(|r| std::ptr::eq(r.extension, ext))
.count()
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn asdf_tag_parse(tag: *const c_char) -> *mut asdf_tag_t {
use std::ffi::CString;
guard("asdf_tag_parse", std::ptr::null_mut(), || {
if tag.is_null() {
return std::ptr::null_mut();
}
let text = unsafe { crate::ffi::c_string_lossy(tag) }.unwrap_or_default();
let (name, version) = asdf_core::yaml::tag::split_tag_version(&text);
let Ok(name) = CString::new(name) else {
return std::ptr::null_mut();
};
let version_ptr = match version {
Some(v) => {
let Ok(v) = CString::new(v) else {
return std::ptr::null_mut();
};
let parsed = unsafe { crate::version_ffi::asdf_version_parse(v.as_ptr()) };
if parsed.is_null() {
return std::ptr::null_mut();
}
parsed.cast_const()
}
None => std::ptr::null(),
};
Box::into_raw(Box::new(asdf_tag_t {
name: name.into_raw().cast_const(),
version: version_ptr,
}))
})
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn asdf_tag_destroy(tag: *mut asdf_tag_t) {
use std::ffi::CString;
guard("asdf_tag_destroy", (), || {
if tag.is_null() {
return;
}
let boxed = unsafe { Box::from_raw(tag) };
if !boxed.name.is_null() {
drop(unsafe { CString::from_raw(boxed.name.cast_mut()) });
}
if !boxed.version.is_null() {
unsafe { crate::version_ffi::asdf_version_destroy(boxed.version.cast_mut()) };
}
})
}
fn tags_match(left: &str, right: &str) -> bool {
fn bare(tag: &str) -> &str {
tag.strip_prefix("tag:").unwrap_or(tag)
}
bare(left) == bare(right)
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn asdf_value_is_extension_type(
value: *mut crate::file_ffi::AsdfValue,
ext: *const asdf_extension_t,
) -> bool {
guard("asdf_value_is_extension_type", false, || value_is_extension_type(value, ext))
}
pub(crate) fn value_is_extension_type(
value: *mut crate::file_ffi::AsdfValue,
ext: *const asdf_extension_t,
) -> bool {
use crate::file_ffi::{value_document, value_node};
if ext.is_null() {
return false;
}
let (Some(doc), Some(node)) = (value_document(value), value_node(value)) else {
return false;
};
let Some(tag) = doc.tag_of(node) else {
return false;
};
let full = tag.full();
let extension = unsafe { &*ext };
if extension.tags.is_null() {
return false;
}
let mut index = 0isize;
loop {
let tag_ptr = unsafe { *extension.tags.offset(index) };
if tag_ptr.is_null() {
return false;
}
if tags_match(&unsafe { CStr::from_ptr(tag_ptr) }.to_string_lossy(), &full) {
return true;
}
index += 1;
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn asdf_is_extension_type(
file: *mut crate::file_ffi::AsdfFile,
path: *const c_char,
ext: *mut asdf_extension_t,
) -> bool {
guard("asdf_is_extension_type", false, || {
let value = unsafe { crate::file_ffi::asdf_get_value(file, path) };
if value.is_null() {
return false;
}
let matched = value_is_extension_type(value, ext);
unsafe { crate::file_ffi::asdf_value_destroy(value) };
matched
})
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn asdf_value_as_extension_type(
value: *mut crate::file_ffi::AsdfValue,
ext: *const asdf_extension_t,
out: *mut *mut c_void,
) -> crate::types::AsdfValueErr {
use crate::types::AsdfValueErr;
guard("asdf_value_as_extension_type", AsdfValueErr::Unknown, || {
if ext.is_null() || out.is_null() {
return AsdfValueErr::Unknown;
}
if !value_is_extension_type(value, ext) {
return AsdfValueErr::TypeMismatch;
}
let extension = unsafe { &*ext };
if extension.vtab.is_null() {
return AsdfValueErr::Unknown;
}
let Some(deserialize) = (unsafe { &*extension.vtab }).deserialize else {
return AsdfValueErr::Unknown;
};
unsafe { deserialize(value, extension.userdata.cast_const(), out) }
})
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn asdf_get_extension_type(
file: *mut crate::file_ffi::AsdfFile,
path: *const c_char,
ext: *const asdf_extension_t,
out: *mut *mut c_void,
) -> crate::types::AsdfValueErr {
use crate::types::AsdfValueErr;
guard("asdf_get_extension_type", AsdfValueErr::Unknown, || {
let value = unsafe { crate::file_ffi::asdf_get_value(file, path) };
if value.is_null() {
return AsdfValueErr::NotFound;
}
let result = unsafe { asdf_value_as_extension_type(value, ext, out) };
unsafe { crate::file_ffi::asdf_value_destroy(value) };
result
})
}
#[repr(transparent)]
#[derive(Debug)]
pub struct Identity<T>(pub T);
unsafe impl<T> Sync for Identity<T> {}
#[unsafe(no_mangle)]
pub static libasdf_version: Identity<asdf_version_t> = Identity(asdf_version_t {
version: c"0.1.0".as_ptr(),
major: 0,
minor: 1,
patch: 0,
extra: std::ptr::null(),
});
#[unsafe(no_mangle)]
pub static libasdf_software: Identity<asdf_software_t> = Identity(asdf_software_t {
name: c"libasdf-rs".as_ptr(),
version: (&raw const libasdf_version).cast::<asdf_version_t>(),
author: c"The libasdf-rs Developers".as_ptr(),
homepage: c"https://github.com/cruzzil/asdf".as_ptr(),
});
#[unsafe(no_mangle)]
pub unsafe extern "C" fn asdf_value_of_extension_type(
file: *mut crate::file_ffi::AsdfFile,
obj: *const c_void,
ext: *const asdf_extension_t,
) -> *mut crate::file_ffi::AsdfValue {
guard("asdf_value_of_extension_type", std::ptr::null_mut(), || {
value_of_extension_type(file, obj, ext)
})
}
pub(crate) fn value_of_extension_type(
file: *mut crate::file_ffi::AsdfFile,
obj: *const c_void,
ext: *const asdf_extension_t,
) -> *mut crate::file_ffi::AsdfValue {
if ext.is_null() {
return std::ptr::null_mut();
}
let extension = unsafe { &*ext };
if extension.vtab.is_null() {
return std::ptr::null_mut();
}
let Some(serialize) = (unsafe { &*extension.vtab }).serialize else {
return std::ptr::null_mut();
};
if extension.tags.is_null() {
return std::ptr::null_mut();
}
let first = unsafe { *extension.tags };
if first.is_null() {
return std::ptr::null_mut();
}
let tag = unsafe { CStr::from_ptr(first) }.to_string_lossy().into_owned();
let value = unsafe { serialize(file, obj, extension.userdata.cast_const()) };
if value.is_null() {
return value;
}
let (Some(owner), Some(node)) =
(crate::file_ffi::value_file(value), crate::file_ffi::value_node(value))
else {
return value;
};
let full = if tag.starts_with("tag:") { tag } else { format!("tag:{tag}") };
if let Some(doc) = unsafe { &mut *owner }.document_for_values() {
doc.node_mut(node).tag = Some(asdf_core::yaml::Tag::parse(&full));
}
value
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn asdf_set_extension_type(
file: *mut crate::file_ffi::AsdfFile,
path: *const c_char,
obj: *const c_void,
ext: *const asdf_extension_t,
) -> crate::types::AsdfValueErr {
use crate::types::AsdfValueErr;
guard("asdf_set_extension_type", AsdfValueErr::Unknown, || {
let value = value_of_extension_type(file, obj, ext);
if value.is_null() {
return AsdfValueErr::EmitFailure;
}
let result = unsafe { crate::file_ffi::set_value_at(file, path, value) };
unsafe { crate::file_ffi::asdf_value_destroy(value) };
result
})
}
fn is_equivalent_type(found: AsdfValueType, wanted: AsdfValueType) -> bool {
use AsdfValueType as T;
match wanted {
T::Uint64 => matches!(found, T::Uint64 | T::Uint32 | T::Uint16 | T::Uint8),
T::Uint32 => matches!(found, T::Uint32 | T::Uint16 | T::Uint8),
T::Uint16 => matches!(found, T::Uint16 | T::Uint8),
T::Uint8 => found == T::Uint8,
T::Int64 => matches!(
found,
T::Int64 | T::Uint32 | T::Int32 | T::Uint16 | T::Int16 | T::Uint8 | T::Int8
),
T::Int32 => matches!(found, T::Int32 | T::Uint16 | T::Int16 | T::Uint8 | T::Int8),
T::Int16 => matches!(found, T::Int16 | T::Uint8 | T::Int8),
T::Int8 => found == T::Int8,
T::Double => matches!(
found,
T::Double
| T::Float
| T::Int64
| T::Int32
| T::Int16
| T::Int8
| T::Uint64
| T::Uint32
| T::Uint16
| T::Uint8
),
other => found == other,
}
}
unsafe fn get_property(
mapping: *mut crate::value_ffi::AsdfMapping,
name: *const c_char,
value_type: c_int,
tag: *const c_char,
out: *mut c_void,
) -> crate::types::AsdfValueErr {
use crate::types::AsdfValueErr;
let prop = unsafe { crate::value_ffi::asdf_mapping_get(mapping, name) };
if prop.is_null() {
return AsdfValueErr::NotFound;
}
let release = |value| unsafe { crate::file_ffi::asdf_value_destroy(value) };
let Some(wanted) = AsdfValueType::from_i32(value_type) else {
release(prop);
return AsdfValueErr::TypeMismatch;
};
if wanted == AsdfValueType::Extension && !tag.is_null() {
let file = crate::file_ffi::value_file(mapping).unwrap_or(std::ptr::null_mut());
let ext = extension_get(file, tag);
if ext.is_null() || !value_is_extension_type(prop, ext) {
release(prop);
return AsdfValueErr::TypeMismatch;
}
let err = unsafe { asdf_value_as_extension_type(prop, ext, out.cast()) };
release(prop);
return err;
}
if wanted != AsdfValueType::Unknown && wanted != AsdfValueType::Extension {
let found = unsafe { crate::file_ffi::asdf_value_get_type(prop) };
if !is_equivalent_type(found, wanted) {
release(prop);
return AsdfValueErr::TypeMismatch;
}
}
let err = unsafe { crate::value_ffi::asdf_value_as_type(prop, value_type, out) };
release(prop);
err
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn asdf_get_required_property(
mapping: *mut crate::value_ffi::AsdfMapping,
name: *const c_char,
value_type: c_int,
tag: *const c_char,
out: *mut c_void,
) -> crate::types::AsdfValueErr {
guard("asdf_get_required_property", crate::types::AsdfValueErr::Unknown, || unsafe {
get_property(mapping, name, value_type, tag, out)
})
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn asdf_get_optional_property(
mapping: *mut crate::value_ffi::AsdfMapping,
name: *const c_char,
value_type: c_int,
tag: *const c_char,
out: *mut c_void,
) -> crate::types::AsdfValueErr {
guard("asdf_get_optional_property", crate::types::AsdfValueErr::Unknown, || unsafe {
get_property(mapping, name, value_type, tag, out)
})
}
#[cfg(test)]
mod tests {
use super::*;
use std::ffi::CString;
#[test]
fn parses_a_versioned_tag() {
let tag = CString::new("tag:stsci.edu:asdf/core/ndarray-1.1.0").unwrap();
let parsed = unsafe { asdf_tag_parse(tag.as_ptr()) };
assert!(!parsed.is_null());
let view = unsafe { &*parsed };
assert_eq!(
unsafe { CStr::from_ptr(view.name) }.to_str().unwrap(),
"tag:stsci.edu:asdf/core/ndarray"
);
assert!(!view.version.is_null());
let version = unsafe { &*view.version };
assert_eq!((version.major, version.minor, version.patch), (1, 1, 0));
unsafe { asdf_tag_destroy(parsed) };
}
#[test]
fn a_tag_without_a_version_has_a_null_version() {
let tag = CString::new("tag:example.com:plain").unwrap();
let parsed = unsafe { asdf_tag_parse(tag.as_ptr()) };
let view = unsafe { &*parsed };
assert_eq!(unsafe { CStr::from_ptr(view.name) }.to_str().unwrap(), "tag:example.com:plain");
assert!(view.version.is_null());
unsafe { asdf_tag_destroy(parsed) };
}
#[test]
fn tag_parsing_tolerates_null() {
assert!(unsafe { asdf_tag_parse(std::ptr::null()) }.is_null());
unsafe { asdf_tag_destroy(std::ptr::null_mut()) };
}
fn make_extension(tags: &[&str]) -> &'static mut asdf_extension_t {
let names: Vec<CString> = tags.iter().map(|t| CString::new(*t).unwrap()).collect();
let mut array: Vec<*const c_char> = names.iter().map(|n| n.as_ptr()).collect();
array.push(std::ptr::null());
let names: &'static [CString] = Vec::leak(names);
let _ = names;
let array: &'static [*const c_char] = Vec::leak(array);
Box::leak(Box::new(asdf_extension_t {
tags: array.as_ptr(),
software: std::ptr::null_mut(),
vtab: std::ptr::null(),
size: 0,
userdata: std::ptr::null_mut(),
}))
}
#[test]
fn registers_and_looks_up_by_tag() {
let ext = make_extension(&["tag:example.com:thing-1.0.0"]);
assert_eq!(registrations_of(ext), 0);
unsafe { asdf_extension_register(ext) };
assert_eq!(registrations_of(ext), 1);
let wanted = CString::new("tag:example.com:thing-1.0.0").unwrap();
let found = unsafe { asdf_extension_get(std::ptr::null_mut(), wanted.as_ptr()) };
assert!(std::ptr::eq(found, ext));
let missing = CString::new("tag:example.com:other-1.0.0").unwrap();
assert!(unsafe { asdf_extension_get(std::ptr::null_mut(), missing.as_ptr()) }.is_null());
}
#[test]
fn one_extension_can_serve_several_tag_versions() {
let ext = make_extension(&["tag:example.com:multi-1.1.0", "tag:example.com:multi-1.0.0"]);
unsafe { asdf_extension_register(ext) };
for tag in ["tag:example.com:multi-1.1.0", "tag:example.com:multi-1.0.0"] {
let c = CString::new(tag).unwrap();
let found = unsafe { asdf_extension_get(std::ptr::null_mut(), c.as_ptr()) };
assert!(std::ptr::eq(found, ext), "{tag}");
}
}
#[test]
fn registering_twice_keeps_one_entry() {
let ext = make_extension(&["tag:example.com:dup-1.0.0"]);
unsafe { asdf_extension_register(ext) };
unsafe { asdf_extension_register(ext) };
assert_eq!(registrations_of(ext), 1, "a repeated registration must not add a second entry");
}
#[test]
fn registration_tolerates_null() {
unsafe { asdf_extension_register(std::ptr::null_mut()) };
assert_eq!(registrations_of(std::ptr::null()), 0);
assert!(unsafe { asdf_extension_get(std::ptr::null_mut(), std::ptr::null()) }.is_null());
}
#[test]
fn value_type_matching_uses_the_full_tag() {
use crate::file_ffi::{asdf_close, asdf_get_value, asdf_open_mem_ex, asdf_value_destroy};
let mut buf = Vec::new();
buf.extend_from_slice(b"#ASDF 1.0.0\n#ASDF_STANDARD 1.6.0\n");
buf.extend_from_slice(b"%YAML 1.1\n%TAG ! tag:stsci.edu:asdf/\n--- !core/asdf-1.1.0\n");
buf.extend_from_slice(b"d: !core/ndarray-1.1.0\n source: 0\n...\n");
let file =
unsafe { asdf_open_mem_ex(buf.as_ptr().cast(), buf.len(), std::ptr::null_mut()) };
assert!(!file.is_null());
let path = CString::new("d").unwrap();
let value = unsafe { asdf_get_value(file, path.as_ptr()) };
assert!(!value.is_null());
let matching = make_extension(&["tag:stsci.edu:asdf/core/ndarray-1.1.0"]);
assert!(unsafe { asdf_value_is_extension_type(value, matching) });
let other = make_extension(&["tag:stsci.edu:asdf/core/ndarray-1.0.0"]);
assert!(!unsafe { asdf_value_is_extension_type(value, other) });
assert!(!unsafe { asdf_value_is_extension_type(value, std::ptr::null()) });
unsafe { asdf_value_destroy(value) };
unsafe { asdf_close(file) };
}
#[test]
fn deserializing_without_a_vtable_is_an_error_not_a_crash() {
use crate::types::AsdfValueErr;
let ext = make_extension(&["tag:example.com:novtab-1.0.0"]);
let mut out: *mut c_void = std::ptr::null_mut();
assert_eq!(
unsafe { asdf_value_as_extension_type(std::ptr::null_mut(), ext, &mut out) },
AsdfValueErr::TypeMismatch
);
}
#[test]
fn the_vtable_keeps_its_reserved_width() {
use std::mem::size_of;
assert_eq!(
size_of::<asdf_extension_vtab_t>(),
ASDF_EXTENSION_VTAB_MAX_METHODS * size_of::<AsdfExtensionMethod>()
);
}
#[test]
fn the_library_reports_its_own_version() {
assert_eq!(unsafe { CStr::from_ptr(libasdf_version.0.version) }.to_str().unwrap(), "0.1.0");
assert_eq!(
unsafe { CStr::from_ptr(libasdf_software.0.name) }.to_str().unwrap(),
"libasdf-rs"
);
assert!(std::ptr::eq(
libasdf_software.0.version,
(&raw const libasdf_version).cast::<asdf_version_t>()
));
}
}