Skip to main content

asdf/
extension_ffi.rs

1//! `asdf/extension.h`: the extension mechanism.
2//!
3//! # Registration happens before `main`
4//!
5//! `ASDF_REGISTER_EXTENSION` generates a function marked
6//! `__attribute__((constructor))`, so a third-party extension such as
7//! libasdf-gwcs calls [`asdf_extension_register`] while the dynamic loader is
8//! still bringing the process up — before `main`, and before anything in this
9//! library has had a chance to initialise.
10//!
11//! The registry is therefore a `static Mutex<Vec<..>>` built by a `const`
12//! constructor: it needs no lazy initialisation, allocates nothing until the
13//! first registration, and has no ordering dependency on anything else in the
14//! library. Reaching for a `OnceLock` with an initialiser, or anything that
15//! runs its own setup first, would reintroduce exactly the ordering problem
16//! this avoids.
17
18use core::ffi::{CStr, c_char, c_int, c_void};
19use std::sync::Mutex;
20
21use crate::panic::guard;
22use crate::types::AsdfValueType;
23use crate::version_ffi::asdf_version_t;
24
25/// Mirror of `asdf_tag_t`.
26#[repr(C)]
27#[derive(Debug)]
28pub struct asdf_tag_t {
29    /// The tag's name, with any version suffix removed.
30    pub name: *const c_char,
31    /// The parsed version, or null when the tag carried none.
32    pub version: *const asdf_version_t,
33}
34
35/// Mirror of `asdf_software_t`.
36///
37/// Defined in `asdf/extension.h` rather than `core/software.h`, because the
38/// two headers would otherwise be circular.
39#[repr(C)]
40#[derive(Debug)]
41pub struct asdf_software_t {
42    /// The software's name.
43    pub name: *const c_char,
44    /// Its version.
45    pub version: *const asdf_version_t,
46    /// Optional author.
47    pub author: *const c_char,
48    /// Optional homepage.
49    pub homepage: *const c_char,
50}
51
52/// Serialize a native object into a value.
53pub type AsdfExtensionSerialize = Option<
54    unsafe extern "C" fn(
55        file: *mut crate::file_ffi::AsdfFile,
56        obj: *const c_void,
57        userdata: *const c_void,
58    ) -> *mut crate::file_ffi::AsdfValue,
59>;
60
61/// Deserialize a value into a native object.
62pub type AsdfExtensionDeserialize = Option<
63    unsafe extern "C" fn(
64        value: *mut crate::file_ffi::AsdfValue,
65        userdata: *const c_void,
66        out: *mut *mut c_void,
67    ) -> crate::types::AsdfValueErr,
68>;
69
70/// Deep-copy a native object into caller-provided storage.
71pub type AsdfExtensionCopy = Option<
72    unsafe extern "C" fn(
73        file: *mut crate::file_ffi::AsdfFile,
74        src: *const c_void,
75        dst: *mut c_void,
76    ) -> bool,
77>;
78
79/// De-initialise a native object's fields, without freeing the object.
80pub type AsdfExtensionDeinit = Option<unsafe extern "C" fn(obj: *mut c_void)>;
81
82/// A generic method pointer, for the vtable's reserved slots.
83pub type AsdfExtensionMethod = Option<unsafe extern "C" fn()>;
84
85/// Total method slots in the vtable, used and reserved.
86pub const ASDF_EXTENSION_VTAB_MAX_METHODS: usize = 8;
87/// Method slots currently defined.
88pub const ASDF_EXTENSION_VTAB_METHODS: usize = 4;
89
90/// Mirror of `asdf_extension_vtab_t`.
91///
92/// The reserved slots are what let upstream add methods without breaking the
93/// ABI, so the total width must stay at
94/// [`ASDF_EXTENSION_VTAB_MAX_METHODS`] pointers.
95#[repr(C)]
96#[derive(Debug)]
97pub struct asdf_extension_vtab_t {
98    /// Serializer, or null if the type cannot be written.
99    pub serialize: AsdfExtensionSerialize,
100    /// Deserializer.
101    pub deserialize: AsdfExtensionDeserialize,
102    /// Deep-copy method, or null for a shallow copy.
103    pub copy: AsdfExtensionCopy,
104    /// De-initialiser for objects the deserializer produced.
105    pub deinit: AsdfExtensionDeinit,
106    /// Reserved, keeping the ABI stable as methods are added.
107    pub _reserved:
108        [AsdfExtensionMethod; ASDF_EXTENSION_VTAB_MAX_METHODS - ASDF_EXTENSION_VTAB_METHODS],
109}
110
111/// Mirror of `asdf_extension_t`.
112#[repr(C)]
113#[derive(Debug)]
114pub struct asdf_extension_t {
115    /// A null-terminated array of the full YAML tags this handles.
116    ///
117    /// `tags[0]` is written when serializing; any listed tag is recognised
118    /// when reading, so one extension can serve several schema versions.
119    pub tags: *const *const c_char,
120    /// The software implementing the extension.
121    pub software: *mut asdf_software_t,
122    /// The extension's methods.
123    pub vtab: *const asdf_extension_vtab_t,
124    /// Size of the extension's objects, for allocation.
125    pub size: usize,
126    /// Opaque data passed through to the methods.
127    pub userdata: *mut c_void,
128}
129
130/// One registration.
131///
132/// The pointer is stored rather than the struct: `ASDF_REGISTER_EXTENSION`
133/// makes the `asdf_extension_t` a file-scope `static`, so it outlives the
134/// process's use of it.
135#[derive(Clone, Copy)]
136struct Registration {
137    extension: *const asdf_extension_t,
138}
139
140// SAFETY: the registered struct is a C `static` and is only ever read after
141// registration, so sharing the pointer across threads is sound. The C API
142// makes the same assumption.
143unsafe impl Send for Registration {}
144
145/// The registry.
146///
147/// `Mutex::new` is `const`, so this needs no lazy initialisation and is safe
148/// to touch from a pre-`main` constructor. See the module comment.
149static REGISTRY: Mutex<Vec<Registration>> = Mutex::new(Vec::new());
150
151/// Register an extension.
152///
153/// Normally called by the constructor `ASDF_REGISTER_EXTENSION` generates,
154/// which runs before `main`.
155///
156/// # Safety
157/// `ext` must point to an `asdf_extension_t` that outlives the process's use
158/// of the library — in practice a file-scope `static`, which is what the
159/// registration macro produces.
160#[unsafe(no_mangle)]
161pub unsafe extern "C" fn asdf_extension_register(ext: *mut asdf_extension_t) {
162    guard("asdf_extension_register", (), || {
163        if ext.is_null() {
164            return;
165        }
166        let mut registry = REGISTRY.lock().unwrap_or_else(|e| e.into_inner());
167        // Registering the same extension twice is harmless and can happen
168        // when a library is loaded more than once; keep one entry.
169        if registry.iter().any(|r| core::ptr::eq(r.extension, ext)) {
170            return;
171        }
172        registry.push(Registration { extension: ext });
173    })
174}
175
176/// Look up the extension registered for a tag.
177///
178/// The tag is matched in full, so `core/ndarray-1.0.0` and
179/// `core/ndarray-1.1.0` each match only if the extension listed them. The
180/// `tag:` prefix is optional on both sides: upstream canonicalizes with
181/// `asdf_yaml_tag_canonicalize`, and its own tests register and look up
182/// tags without it.
183///
184/// # Safety
185/// `tag` must be a valid NUL-terminated string or null. `file` is accepted
186/// for signature compatibility and may be null.
187#[unsafe(no_mangle)]
188pub unsafe extern "C" fn asdf_extension_get(
189    file: *mut crate::file_ffi::AsdfFile,
190    tag: *const c_char,
191) -> *const asdf_extension_t {
192    guard("asdf_extension_get", core::ptr::null(), || extension_get(file, tag))
193}
194
195/// Safe internal form of [`asdf_extension_get`].
196///
197/// The exported entry point is `unsafe extern "C"`, so calling it from
198/// inside the crate would need an `unsafe` block at every site to assert a
199/// contract the crate itself is upholding. Callers use this instead.
200pub(crate) fn extension_get(
201    file: *mut crate::file_ffi::AsdfFile,
202    tag: *const c_char,
203) -> *const asdf_extension_t {
204    let _ = file;
205    if tag.is_null() {
206        return core::ptr::null();
207    }
208    let wanted = unsafe { crate::ffi::c_string_lossy(tag) }.unwrap_or_default();
209    let registry = REGISTRY.lock().unwrap_or_else(|e| e.into_inner());
210
211    for entry in registry.iter() {
212        // SAFETY: registration promised this outlives our use of it.
213        let extension = unsafe { &*entry.extension };
214        if extension.tags.is_null() {
215            continue;
216        }
217        let mut index = 0isize;
218        loop {
219            let tag_ptr = unsafe { *extension.tags.offset(index) };
220            if tag_ptr.is_null() {
221                break;
222            }
223            let declared = unsafe { CStr::from_ptr(tag_ptr) }.to_string_lossy();
224            if tags_match(&declared, &wanted) {
225                return entry.extension;
226            }
227            index += 1;
228        }
229    }
230    core::ptr::null()
231}
232
233/// How many times a particular extension is registered.
234///
235/// Not part of libasdf's API; used by this crate's own tests. It counts one
236/// specific pointer rather than the whole registry, because the registry is
237/// process-global and tests run in parallel — a total would be racy.
238#[cfg(test)]
239pub(crate) fn registrations_of(ext: *const asdf_extension_t) -> usize {
240    REGISTRY
241        .lock()
242        .unwrap_or_else(|e| e.into_inner())
243        .iter()
244        .filter(|r| core::ptr::eq(r.extension, ext))
245        .count()
246}
247
248/// Parse a tag into its name and version.
249///
250/// `core/ndarray-1.1.0` yields the name `core/ndarray` and version `1.1.0`.
251/// A tag with no parseable trailing version yields the whole string and a
252/// null version.
253///
254/// # Safety
255/// `tag` must be a valid NUL-terminated string or null. The result must be
256/// freed with [`asdf_tag_destroy`].
257#[unsafe(no_mangle)]
258pub unsafe extern "C" fn asdf_tag_parse(tag: *const c_char) -> *mut asdf_tag_t {
259    use alloc::ffi::CString;
260
261    guard("asdf_tag_parse", core::ptr::null_mut(), || {
262        if tag.is_null() {
263            return core::ptr::null_mut();
264        }
265        let text = unsafe { crate::ffi::c_string_lossy(tag) }.unwrap_or_default();
266        let (name, version) = asdf_core::yaml::tag::split_tag_version(&text);
267
268        let Ok(name) = CString::new(name) else {
269            return core::ptr::null_mut();
270        };
271        let version_ptr = match version {
272            Some(v) => {
273                let Ok(v) = CString::new(v) else {
274                    return core::ptr::null_mut();
275                };
276                let parsed = unsafe { crate::version_ffi::asdf_version_parse(v.as_ptr()) };
277                if parsed.is_null() {
278                    return core::ptr::null_mut();
279                }
280                parsed.cast_const()
281            }
282            None => core::ptr::null(),
283        };
284
285        Box::into_raw(Box::new(asdf_tag_t {
286            name: name.into_raw().cast_const(),
287            version: version_ptr,
288        }))
289    })
290}
291
292/// Free a tag from [`asdf_tag_parse`].
293///
294/// # Safety
295/// `tag` must be null or have come from [`asdf_tag_parse`], and must not be
296/// used afterwards.
297#[unsafe(no_mangle)]
298pub unsafe extern "C" fn asdf_tag_destroy(tag: *mut asdf_tag_t) {
299    use alloc::ffi::CString;
300
301    guard("asdf_tag_destroy", (), || {
302        if tag.is_null() {
303            return;
304        }
305        let boxed = unsafe { Box::from_raw(tag) };
306        if !boxed.name.is_null() {
307            drop(unsafe { CString::from_raw(boxed.name.cast_mut()) });
308        }
309        if !boxed.version.is_null() {
310            unsafe { crate::version_ffi::asdf_version_destroy(boxed.version.cast_mut()) };
311        }
312    })
313}
314
315/// Whether two tags name the same thing, with or without the `tag:` prefix.
316///
317/// An extension may register `stsci.edu:asdf/tests/foo-1.1.0` and a caller
318/// may look it up the same way, while the tag on a value is always the full
319/// `tag:stsci.edu:asdf/tests/foo-1.1.0`. Upstream reconciles the two by
320/// canonicalizing both with `asdf_yaml_tag_canonicalize`; comparing without
321/// the prefix is the same relation and allocates nothing.
322fn tags_match(left: &str, right: &str) -> bool {
323    fn bare(tag: &str) -> &str {
324        tag.strip_prefix("tag:").unwrap_or(tag)
325    }
326    bare(left) == bare(right)
327}
328
329/// Whether a value's tag is one this extension handles.
330///
331/// # Safety
332/// `value` must be null or a valid value handle; `ext` null or a registered
333/// extension.
334#[unsafe(no_mangle)]
335pub unsafe extern "C" fn asdf_value_is_extension_type(
336    value: *mut crate::file_ffi::AsdfValue,
337    ext: *const asdf_extension_t,
338) -> bool {
339    guard("asdf_value_is_extension_type", false, || value_is_extension_type(value, ext))
340}
341
342/// Safe internal form of [`asdf_value_is_extension_type`].
343///
344/// The exported entry point is `unsafe extern "C"`, so calling it from
345/// inside the crate would need an `unsafe` block at every site to assert a
346/// contract the crate itself is upholding. Callers use this instead.
347pub(crate) fn value_is_extension_type(
348    value: *mut crate::file_ffi::AsdfValue,
349    ext: *const asdf_extension_t,
350) -> bool {
351    use crate::file_ffi::{value_document, value_node};
352
353    if ext.is_null() {
354        return false;
355    }
356    let (Some(doc), Some(node)) = (value_document(value), value_node(value)) else {
357        return false;
358    };
359    let Some(tag) = doc.tag_of(node) else {
360        return false;
361    };
362    let full = tag.full();
363
364    let extension = unsafe { &*ext };
365    if extension.tags.is_null() {
366        return false;
367    }
368    let mut index = 0isize;
369    loop {
370        let tag_ptr = unsafe { *extension.tags.offset(index) };
371        if tag_ptr.is_null() {
372            return false;
373        }
374        if tags_match(&unsafe { CStr::from_ptr(tag_ptr) }.to_string_lossy(), &full) {
375            return true;
376        }
377        index += 1;
378    }
379}
380
381/// Whether the value at `path` is of this extension's type.
382///
383/// # Safety
384/// `file` must be a valid file handle; `path` a valid string or null.
385#[unsafe(no_mangle)]
386pub unsafe extern "C" fn asdf_is_extension_type(
387    file: *mut crate::file_ffi::AsdfFile,
388    path: *const c_char,
389    ext: *mut asdf_extension_t,
390) -> bool {
391    guard("asdf_is_extension_type", false, || {
392        let value = unsafe { crate::file_ffi::asdf_get_value(file, path) };
393        if value.is_null() {
394            return false;
395        }
396        let matched = value_is_extension_type(value, ext);
397        unsafe { crate::file_ffi::asdf_value_destroy(value) };
398        matched
399    })
400}
401
402/// Deserialize a value through an extension.
403///
404/// # Safety
405/// `value` must be a valid value handle, `ext` a registered extension whose
406/// vtable has a deserializer, and `out` writable.
407#[unsafe(no_mangle)]
408pub unsafe extern "C" fn asdf_value_as_extension_type(
409    value: *mut crate::file_ffi::AsdfValue,
410    ext: *const asdf_extension_t,
411    out: *mut *mut c_void,
412) -> crate::types::AsdfValueErr {
413    use crate::types::AsdfValueErr;
414
415    guard("asdf_value_as_extension_type", AsdfValueErr::Unknown, || {
416        if ext.is_null() || out.is_null() {
417            return AsdfValueErr::Unknown;
418        }
419        if !value_is_extension_type(value, ext) {
420            return AsdfValueErr::TypeMismatch;
421        }
422        let extension = unsafe { &*ext };
423        if extension.vtab.is_null() {
424            return AsdfValueErr::Unknown;
425        }
426        let Some(deserialize) = (unsafe { &*extension.vtab }).deserialize else {
427            return AsdfValueErr::Unknown;
428        };
429        // The extension owns what it produces; the generated
430        // `asdf_<ext>_destroy` releases it.
431        unsafe { deserialize(value, extension.userdata.cast_const(), out) }
432    })
433}
434
435/// Read the value at `path` through an extension.
436///
437/// # Safety
438/// See [`asdf_value_as_extension_type`].
439#[unsafe(no_mangle)]
440pub unsafe extern "C" fn asdf_get_extension_type(
441    file: *mut crate::file_ffi::AsdfFile,
442    path: *const c_char,
443    ext: *const asdf_extension_t,
444    out: *mut *mut c_void,
445) -> crate::types::AsdfValueErr {
446    use crate::types::AsdfValueErr;
447
448    guard("asdf_get_extension_type", AsdfValueErr::Unknown, || {
449        let value = unsafe { crate::file_ffi::asdf_get_value(file, path) };
450        if value.is_null() {
451            return AsdfValueErr::NotFound;
452        }
453        let result = unsafe { asdf_value_as_extension_type(value, ext, out) };
454        unsafe { crate::file_ffi::asdf_value_destroy(value) };
455        result
456    })
457}
458
459/// A wrapper making one of the identity statics shareable.
460///
461/// `asdf_version_t` and `asdf_software_t` hold raw pointers, so they are not
462/// `Sync` in general — a heap-allocated one from `asdf_version_parse` is the
463/// caller's to manage. The two statics below are different: every pointer in
464/// them refers to a string literal, and nothing ever writes to them, so
465/// sharing them across threads is sound. `repr(transparent)` keeps the
466/// exported symbol byte-identical to the bare struct.
467#[repr(transparent)]
468#[derive(Debug)]
469pub struct Identity<T>(pub T);
470
471// SAFETY: only used for the two statics below, which are built entirely from
472// `'static` string literals and are never mutated.
473unsafe impl<T> Sync for Identity<T> {}
474
475/// The library's own version, exported as `libasdf_version`.
476///
477/// A data symbol, not a function: callers read it directly.
478///
479/// Immutable, though the header declares it as plain (non-`const`) extern
480/// data. It is the library's identity and nothing should write to it; making
481/// it a shared `static` lets it live in read-only memory, so an errant write
482/// faults rather than silently corrupting the value every later reader sees.
483#[unsafe(no_mangle)]
484pub static libasdf_version: Identity<asdf_version_t> = Identity(asdf_version_t {
485    version: c"0.2.0".as_ptr(),
486    major: 0,
487    minor: 2,
488    patch: 0,
489    extra: core::ptr::null(),
490});
491
492/// The library's own `core/software` metadata, exported as
493/// `libasdf_software`.
494///
495/// Recorded in the `asdf_library` field of files this library writes.
496/// Immutable for the same reason as [`libasdf_version`].
497#[unsafe(no_mangle)]
498pub static libasdf_software: Identity<asdf_software_t> = Identity(asdf_software_t {
499    name: c"libasdf-rs".as_ptr(),
500    version: (&raw const libasdf_version).cast::<asdf_version_t>(),
501    author: c"The libasdf-rs Developers".as_ptr(),
502    homepage: c"https://github.com/cruzzil/asdf".as_ptr(),
503});
504
505/// Serialize a native object through an extension.
506///
507/// # Safety
508/// `file` must be a valid file handle, `obj` a valid object of the
509/// extension's type, and `ext` a registered extension whose vtable has a
510/// serializer. The result must be released with `asdf_value_destroy`.
511#[unsafe(no_mangle)]
512pub unsafe extern "C" fn asdf_value_of_extension_type(
513    file: *mut crate::file_ffi::AsdfFile,
514    obj: *const c_void,
515    ext: *const asdf_extension_t,
516) -> *mut crate::file_ffi::AsdfValue {
517    guard("asdf_value_of_extension_type", core::ptr::null_mut(), || {
518        value_of_extension_type(file, obj, ext)
519    })
520}
521
522/// Safe internal form of [`asdf_value_of_extension_type`].
523///
524/// The exported entry point is `unsafe extern "C"`, so calling it from
525/// inside the crate would need an `unsafe` block at every site to assert a
526/// contract the crate itself is upholding. Callers use this instead.
527pub(crate) fn value_of_extension_type(
528    file: *mut crate::file_ffi::AsdfFile,
529    obj: *const c_void,
530    ext: *const asdf_extension_t,
531) -> *mut crate::file_ffi::AsdfValue {
532    if ext.is_null() {
533        return core::ptr::null_mut();
534    }
535    let extension = unsafe { &*ext };
536    if extension.vtab.is_null() {
537        return core::ptr::null_mut();
538    }
539    let Some(serialize) = (unsafe { &*extension.vtab }).serialize else {
540        // The header allows a null serializer, meaning the type cannot
541        // be written.
542        return core::ptr::null_mut();
543    };
544
545    // The first tag an extension registers is the one written for a
546    // newly serialized object; without it the value goes into the tree
547    // untagged and nothing can read it back as this type.
548    if extension.tags.is_null() {
549        return core::ptr::null_mut();
550    }
551    let first = unsafe { *extension.tags };
552    if first.is_null() {
553        return core::ptr::null_mut();
554    }
555    let tag = unsafe { CStr::from_ptr(first) }.to_string_lossy().into_owned();
556
557    let value = unsafe { serialize(file, obj, extension.userdata.cast_const()) };
558    if value.is_null() {
559        return value;
560    }
561
562    let (Some(owner), Some(node)) =
563        (crate::file_ffi::value_file(value), crate::file_ffi::value_node(value))
564    else {
565        return value;
566    };
567    // The tag may be registered without its `tag:` prefix; the tree
568    // always carries the full form.
569    let full = if tag.starts_with("tag:") { tag } else { format!("tag:{tag}") };
570    // SAFETY: the file outlives every value taken from it, by the C
571    // contract, and the serializer has already finished with the tree.
572    if let Some(doc) = unsafe { &mut *owner }.document_for_values() {
573        doc.node_mut(node).tag = Some(asdf_core::yaml::Tag::parse(&full));
574    }
575    value
576}
577
578/// Write a native object at `path` through an extension.
579///
580/// # Safety
581/// See [`asdf_value_of_extension_type`]; `path` must be a valid
582/// NUL-terminated string or null.
583#[unsafe(no_mangle)]
584pub unsafe extern "C" fn asdf_set_extension_type(
585    file: *mut crate::file_ffi::AsdfFile,
586    path: *const c_char,
587    obj: *const c_void,
588    ext: *const asdf_extension_t,
589) -> crate::types::AsdfValueErr {
590    use crate::types::AsdfValueErr;
591
592    guard("asdf_set_extension_type", AsdfValueErr::Unknown, || {
593        let value = value_of_extension_type(file, obj, ext);
594        if value.is_null() {
595            // No serializer, or the serializer failed.
596            return AsdfValueErr::EmitFailure;
597        }
598        let result = unsafe { crate::file_ffi::set_value_at(file, path, value) };
599        unsafe { crate::file_ffi::asdf_value_destroy(value) };
600        result
601    })
602}
603
604// ---- Schema property helpers -----------------------------------------
605//
606// `asdf/extension_util.h`. These are what an extension's deserializer uses
607// to pull a schema's properties out of a mapping with the type checking the
608// schema calls for, rather than repeating it at each call site.
609
610/// Whether a value of `found` can be read as `wanted`.
611///
612/// Integer widths widen, and every numeric type reads as a `double`. The
613/// relation is deliberately one-way: a `uint16` satisfies a request for an
614/// `int32`, but not the reverse, because the reverse can overflow.
615fn is_equivalent_type(found: AsdfValueType, wanted: AsdfValueType) -> bool {
616    use AsdfValueType as T;
617    match wanted {
618        T::Uint64 => matches!(found, T::Uint64 | T::Uint32 | T::Uint16 | T::Uint8),
619        T::Uint32 => matches!(found, T::Uint32 | T::Uint16 | T::Uint8),
620        T::Uint16 => matches!(found, T::Uint16 | T::Uint8),
621        T::Uint8 => found == T::Uint8,
622        T::Int64 => matches!(
623            found,
624            T::Int64 | T::Uint32 | T::Int32 | T::Uint16 | T::Int16 | T::Uint8 | T::Int8
625        ),
626        T::Int32 => matches!(found, T::Int32 | T::Uint16 | T::Int16 | T::Uint8 | T::Int8),
627        T::Int16 => matches!(found, T::Int16 | T::Uint8 | T::Int8),
628        T::Int8 => found == T::Int8,
629        T::Double => matches!(
630            found,
631            T::Double
632                | T::Float
633                | T::Int64
634                | T::Int32
635                | T::Int16
636                | T::Int8
637                | T::Uint64
638                | T::Uint32
639                | T::Uint16
640                | T::Uint8
641        ),
642        other => found == other,
643    }
644}
645
646/// Look up a mapping's property and read it as `value_type`.
647///
648/// # Safety
649/// `mapping` must be a valid handle, `name` a valid string, `tag` a valid
650/// string or null, and `out` storage of the C type matching `value_type`.
651unsafe fn get_property(
652    mapping: *mut crate::value_ffi::AsdfMapping,
653    name: *const c_char,
654    value_type: c_int,
655    tag: *const c_char,
656    out: *mut c_void,
657) -> crate::types::AsdfValueErr {
658    use crate::types::AsdfValueErr;
659
660    let prop = unsafe { crate::value_ffi::asdf_mapping_get(mapping, name) };
661    if prop.is_null() {
662        return AsdfValueErr::NotFound;
663    }
664    let release = |value| unsafe { crate::file_ffi::asdf_value_destroy(value) };
665
666    let Some(wanted) = AsdfValueType::from_i32(value_type) else {
667        release(prop);
668        return AsdfValueErr::TypeMismatch;
669    };
670
671    // An extension type is matched by tag rather than by shape.
672    if wanted == AsdfValueType::Extension && !tag.is_null() {
673        let file = crate::file_ffi::value_file(mapping).unwrap_or(core::ptr::null_mut());
674        let ext = extension_get(file, tag);
675        if ext.is_null() || !value_is_extension_type(prop, ext) {
676            release(prop);
677            return AsdfValueErr::TypeMismatch;
678        }
679        let err = unsafe { asdf_value_as_extension_type(prop, ext, out.cast()) };
680        release(prop);
681        return err;
682    }
683
684    if wanted != AsdfValueType::Unknown && wanted != AsdfValueType::Extension {
685        let found = unsafe { crate::file_ffi::asdf_value_get_type(prop) };
686        if !is_equivalent_type(found, wanted) {
687            release(prop);
688            return AsdfValueErr::TypeMismatch;
689        }
690    }
691
692    let err = unsafe { crate::value_ffi::asdf_value_as_type(prop, value_type, out) };
693
694    // `Mapping` and `Sequence` are views: what lands in `*out` *is* `prop`,
695    // not a copy of it. Releasing it here would hand the caller a dangling
696    // handle -- and libasdf-gwcs, which reads every optional `inputs` and
697    // `bounding_box` this way, dereferences it immediately. Ownership passes
698    // to the caller instead, who destroys it as the header says. Every other
699    // type copies out of the value, so `prop` stays ours to free.
700    let out_aliases_prop = matches!(wanted, AsdfValueType::Mapping | AsdfValueType::Sequence)
701        && err == AsdfValueErr::Ok;
702    if !out_aliases_prop {
703        release(prop);
704    }
705    err
706}
707
708/// Read a property the schema requires.
709///
710/// # Safety
711/// See [`asdf_get_optional_property`].
712#[unsafe(no_mangle)]
713pub unsafe extern "C" fn asdf_get_required_property(
714    mapping: *mut crate::value_ffi::AsdfMapping,
715    name: *const c_char,
716    value_type: c_int,
717    tag: *const c_char,
718    out: *mut c_void,
719) -> crate::types::AsdfValueErr {
720    guard("asdf_get_required_property", crate::types::AsdfValueErr::Unknown, || unsafe {
721        get_property(mapping, name, value_type, tag, out)
722    })
723}
724
725/// Read a property the schema allows but does not require.
726///
727/// Identical to [`asdf_get_required_property`] except in how loudly an
728/// absent property is reported; both return `ASDF_VALUE_ERR_NOT_FOUND`.
729///
730/// # Safety
731/// `mapping` must be a valid handle, `name` a valid string, `tag` a valid
732/// string or null, and `out` storage of the C type matching `value_type`.
733#[unsafe(no_mangle)]
734pub unsafe extern "C" fn asdf_get_optional_property(
735    mapping: *mut crate::value_ffi::AsdfMapping,
736    name: *const c_char,
737    value_type: c_int,
738    tag: *const c_char,
739    out: *mut c_void,
740) -> crate::types::AsdfValueErr {
741    guard("asdf_get_optional_property", crate::types::AsdfValueErr::Unknown, || unsafe {
742        get_property(mapping, name, value_type, tag, out)
743    })
744}
745
746#[cfg(test)]
747mod tests {
748    use super::*;
749    use alloc::ffi::CString;
750
751    #[test]
752    fn parses_a_versioned_tag() {
753        let tag = CString::new("tag:stsci.edu:asdf/core/ndarray-1.1.0").unwrap();
754        let parsed = unsafe { asdf_tag_parse(tag.as_ptr()) };
755        assert!(!parsed.is_null());
756
757        let view = unsafe { &*parsed };
758        assert_eq!(
759            unsafe { CStr::from_ptr(view.name) }.to_str().unwrap(),
760            "tag:stsci.edu:asdf/core/ndarray"
761        );
762        assert!(!view.version.is_null());
763        let version = unsafe { &*view.version };
764        assert_eq!((version.major, version.minor, version.patch), (1, 1, 0));
765
766        unsafe { asdf_tag_destroy(parsed) };
767    }
768
769    #[test]
770    fn a_tag_without_a_version_has_a_null_version() {
771        let tag = CString::new("tag:example.com:plain").unwrap();
772        let parsed = unsafe { asdf_tag_parse(tag.as_ptr()) };
773        let view = unsafe { &*parsed };
774        assert_eq!(unsafe { CStr::from_ptr(view.name) }.to_str().unwrap(), "tag:example.com:plain");
775        assert!(view.version.is_null());
776        unsafe { asdf_tag_destroy(parsed) };
777    }
778
779    #[test]
780    fn tag_parsing_tolerates_null() {
781        assert!(unsafe { asdf_tag_parse(core::ptr::null()) }.is_null());
782        unsafe { asdf_tag_destroy(core::ptr::null_mut()) };
783    }
784
785    /// Build a registration the way `ASDF_REGISTER_EXTENSION` does.
786    ///
787    /// Everything is **leaked on purpose**. The macro makes the
788    /// `asdf_extension_t` and its tag array file-scope statics, and the
789    /// registry stores the pointer rather than a copy, so a registered
790    /// extension must genuinely live for the process. Allocating one in a
791    /// `Box` and dropping it leaves the registry holding a dangling pointer
792    /// — and, since the allocator reuses addresses, a later extension can
793    /// appear to be registered already.
794    fn make_extension(tags: &[&str]) -> &'static mut asdf_extension_t {
795        let names: Vec<CString> = tags.iter().map(|t| CString::new(*t).unwrap()).collect();
796        let mut array: Vec<*const c_char> = names.iter().map(|n| n.as_ptr()).collect();
797        array.push(core::ptr::null());
798
799        // Leak the names first so their pointers stay valid.
800        let names: &'static [CString] = Vec::leak(names);
801        let _ = names;
802        let array: &'static [*const c_char] = Vec::leak(array);
803
804        Box::leak(Box::new(asdf_extension_t {
805            tags: array.as_ptr(),
806            software: core::ptr::null_mut(),
807            vtab: core::ptr::null(),
808            size: 0,
809            userdata: core::ptr::null_mut(),
810        }))
811    }
812
813    #[test]
814    fn registers_and_looks_up_by_tag() {
815        let ext = make_extension(&["tag:example.com:thing-1.0.0"]);
816        assert_eq!(registrations_of(ext), 0);
817        unsafe { asdf_extension_register(ext) };
818        assert_eq!(registrations_of(ext), 1);
819
820        let wanted = CString::new("tag:example.com:thing-1.0.0").unwrap();
821        let found = unsafe { asdf_extension_get(core::ptr::null_mut(), wanted.as_ptr()) };
822        assert!(core::ptr::eq(found, ext));
823
824        let missing = CString::new("tag:example.com:other-1.0.0").unwrap();
825        assert!(unsafe { asdf_extension_get(core::ptr::null_mut(), missing.as_ptr()) }.is_null());
826    }
827
828    #[test]
829    fn one_extension_can_serve_several_tag_versions() {
830        // The documented use: an extension lists every version it reads, and
831        // writes with the first.
832        let ext = make_extension(&["tag:example.com:multi-1.1.0", "tag:example.com:multi-1.0.0"]);
833        unsafe { asdf_extension_register(ext) };
834
835        for tag in ["tag:example.com:multi-1.1.0", "tag:example.com:multi-1.0.0"] {
836            let c = CString::new(tag).unwrap();
837            let found = unsafe { asdf_extension_get(core::ptr::null_mut(), c.as_ptr()) };
838            assert!(core::ptr::eq(found, ext), "{tag}");
839        }
840    }
841
842    #[test]
843    fn registering_twice_keeps_one_entry() {
844        let ext = make_extension(&["tag:example.com:dup-1.0.0"]);
845        unsafe { asdf_extension_register(ext) };
846        unsafe { asdf_extension_register(ext) };
847        assert_eq!(registrations_of(ext), 1, "a repeated registration must not add a second entry");
848    }
849
850    #[test]
851    fn registration_tolerates_null() {
852        unsafe { asdf_extension_register(core::ptr::null_mut()) };
853        assert_eq!(registrations_of(core::ptr::null()), 0);
854        assert!(unsafe { asdf_extension_get(core::ptr::null_mut(), core::ptr::null()) }.is_null());
855    }
856
857    #[test]
858    fn value_type_matching_uses_the_full_tag() {
859        use crate::file_ffi::{asdf_close, asdf_get_value, asdf_open_mem_ex, asdf_value_destroy};
860
861        let mut buf = Vec::new();
862        buf.extend_from_slice(b"#ASDF 1.0.0\n#ASDF_STANDARD 1.6.0\n");
863        buf.extend_from_slice(b"%YAML 1.1\n%TAG ! tag:stsci.edu:asdf/\n--- !core/asdf-1.1.0\n");
864        buf.extend_from_slice(b"d: !core/ndarray-1.1.0\n  source: 0\n...\n");
865
866        let file =
867            unsafe { asdf_open_mem_ex(buf.as_ptr().cast(), buf.len(), core::ptr::null_mut()) };
868        assert!(!file.is_null());
869
870        let path = CString::new("d").unwrap();
871        let value = unsafe { asdf_get_value(file, path.as_ptr()) };
872        assert!(!value.is_null());
873
874        let matching = make_extension(&["tag:stsci.edu:asdf/core/ndarray-1.1.0"]);
875        assert!(unsafe { asdf_value_is_extension_type(value, matching) });
876
877        // A different *version* of the same schema must not match, since an
878        // extension declares each version it handles.
879        let other = make_extension(&["tag:stsci.edu:asdf/core/ndarray-1.0.0"]);
880        assert!(!unsafe { asdf_value_is_extension_type(value, other) });
881
882        assert!(!unsafe { asdf_value_is_extension_type(value, core::ptr::null()) });
883
884        unsafe { asdf_value_destroy(value) };
885        unsafe { asdf_close(file) };
886    }
887
888    #[test]
889    fn deserializing_without_a_vtable_is_an_error_not_a_crash() {
890        use crate::types::AsdfValueErr;
891
892        let ext = make_extension(&["tag:example.com:novtab-1.0.0"]);
893        let mut out: *mut c_void = core::ptr::null_mut();
894        // A null value handle first.
895        assert_eq!(
896            unsafe { asdf_value_as_extension_type(core::ptr::null_mut(), ext, &mut out) },
897            AsdfValueErr::TypeMismatch
898        );
899    }
900
901    #[test]
902    fn the_vtable_keeps_its_reserved_width() {
903        // The reserved slots are what let upstream add methods without an
904        // ABI break, so the total width is part of the contract.
905        use core::mem::size_of;
906        assert_eq!(
907            size_of::<asdf_extension_vtab_t>(),
908            ASDF_EXTENSION_VTAB_MAX_METHODS * size_of::<AsdfExtensionMethod>()
909        );
910    }
911
912    #[test]
913    fn the_library_reports_its_own_version() {
914        assert_eq!(unsafe { CStr::from_ptr(libasdf_version.0.version) }.to_str().unwrap(), "0.2.0");
915        assert_eq!(
916            unsafe { CStr::from_ptr(libasdf_software.0.name) }.to_str().unwrap(),
917            "libasdf-rs"
918        );
919        // The software's version must point at the exported version symbol,
920        // not a copy of it.
921        assert!(core::ptr::eq(
922            libasdf_software.0.version,
923            (&raw const libasdf_version).cast::<asdf_version_t>()
924        ));
925    }
926    /// A container property must outlive the call that produced it.
927    ///
928    /// `asdf_get_*_property` used to destroy the value it looked up before
929    /// returning, which is right for a scalar -- the C type is copied out --
930    /// but wrong for a mapping or a sequence, where what lands in `*out` *is*
931    /// that value. Callers got `ASDF_VALUE_OK` and a freed handle.
932    ///
933    /// libasdf-gwcs hits this on the first thing it does: every transform's
934    /// deserializer reads optional `inputs`, `outputs` and `bounding_box`
935    /// this way and dereferences the result immediately.
936    #[test]
937    fn a_container_property_is_usable_after_the_call() {
938        use crate::file_ffi::{AsdfFile, asdf_close, asdf_open_mem_ex};
939        use crate::types::{AsdfValueErr, AsdfValueType};
940
941        let doc = b"#ASDF 1.0.0\n#ASDF_STANDARD 1.5.0\n%YAML 1.1\n\
942--- !<tag:stsci.edu:asdf/core/asdf-1.1.0>\n\
943transform:\n  inputs: [x, y]\n  meta: {unit: deg}\n...\n";
944
945        let file: *mut AsdfFile =
946            unsafe { asdf_open_mem_ex(doc.as_ptr().cast(), doc.len(), core::ptr::null_mut()) };
947        assert!(!file.is_null());
948
949        let path = CString::new("transform").unwrap();
950        let transform = unsafe { crate::file_ffi::asdf_get_value(file, path.as_ptr()) };
951        assert!(!transform.is_null());
952
953        let mut map: *mut crate::value_ffi::AsdfMapping = core::ptr::null_mut();
954        assert_eq!(
955            unsafe { crate::value_ffi::asdf_value_as_mapping(transform, &mut map) },
956            AsdfValueErr::Ok
957        );
958
959        // A sequence property, as gwcs reads `inputs`.
960        let key = CString::new("inputs").unwrap();
961        let mut seq: *mut crate::value_ffi::AsdfSequence = core::ptr::null_mut();
962        assert_eq!(
963            unsafe {
964                asdf_get_optional_property(
965                    map,
966                    key.as_ptr(),
967                    AsdfValueType::Sequence as c_int,
968                    core::ptr::null(),
969                    (&raw mut seq).cast(),
970                )
971            },
972            AsdfValueErr::Ok
973        );
974        assert!(!seq.is_null());
975        assert_eq!(unsafe { crate::value_ffi::asdf_sequence_size(seq) }, 2);
976        unsafe { crate::file_ffi::asdf_value_destroy(seq) };
977
978        // And a mapping property, which reached the same fate.
979        let key = CString::new("meta").unwrap();
980        let mut inner: *mut crate::value_ffi::AsdfMapping = core::ptr::null_mut();
981        assert_eq!(
982            unsafe {
983                asdf_get_optional_property(
984                    map,
985                    key.as_ptr(),
986                    AsdfValueType::Mapping as c_int,
987                    core::ptr::null(),
988                    (&raw mut inner).cast(),
989                )
990            },
991            AsdfValueErr::Ok
992        );
993        assert!(!inner.is_null());
994        assert_eq!(unsafe { crate::value_ffi::asdf_mapping_size(inner) }, 1);
995        unsafe { crate::file_ffi::asdf_value_destroy(inner) };
996
997        unsafe { crate::file_ffi::asdf_value_destroy(transform) };
998        unsafe { asdf_close(file) };
999    }
1000
1001    /// A missing optional property must leave `*out` alone.
1002    ///
1003    /// gwcs relies on this: it pre-sets the handle to NULL and skips the key
1004    /// when it comes back NULL, so writing a stale pointer would be as bad as
1005    /// returning a freed one.
1006    #[test]
1007    fn an_absent_container_property_leaves_out_untouched() {
1008        use crate::file_ffi::{AsdfFile, asdf_close, asdf_open_mem_ex};
1009        use crate::types::{AsdfValueErr, AsdfValueType};
1010
1011        let doc = b"#ASDF 1.0.0\n#ASDF_STANDARD 1.5.0\n%YAML 1.1\n\
1012--- !<tag:stsci.edu:asdf/core/asdf-1.1.0>\n\
1013transform:\n  name: shifty\n...\n";
1014
1015        let file: *mut AsdfFile =
1016            unsafe { asdf_open_mem_ex(doc.as_ptr().cast(), doc.len(), core::ptr::null_mut()) };
1017        let path = CString::new("transform").unwrap();
1018        let transform = unsafe { crate::file_ffi::asdf_get_value(file, path.as_ptr()) };
1019        let mut map: *mut crate::value_ffi::AsdfMapping = core::ptr::null_mut();
1020        unsafe { crate::value_ffi::asdf_value_as_mapping(transform, &mut map) };
1021
1022        let key = CString::new("inputs").unwrap();
1023        let mut seq: *mut crate::value_ffi::AsdfSequence = core::ptr::null_mut();
1024        let err = unsafe {
1025            asdf_get_optional_property(
1026                map,
1027                key.as_ptr(),
1028                AsdfValueType::Sequence as c_int,
1029                core::ptr::null(),
1030                (&raw mut seq).cast(),
1031            )
1032        };
1033        assert_eq!(err, AsdfValueErr::NotFound);
1034        assert!(seq.is_null(), "an absent property must not write to `out`");
1035
1036        unsafe { crate::file_ffi::asdf_value_destroy(transform) };
1037        unsafe { asdf_close(file) };
1038    }
1039}