Skip to main content

asdf/
core_ext.rs

1//! The core-schema extensions.
2//!
3//! `ASDF_REGISTER_EXTENSION` generates eleven public functions for each
4//! extension type. C gets them from the macro; here they are produced by
5//! `declare_extension`, which mirrors the macro's naming and semantics
6//! exactly so a caller cannot tell the difference:
7//!
8//! `asdf_get_<name>`, `asdf_set_<name>`, `asdf_is_<name>`,
9//! `asdf_value_is_<name>`, `asdf_value_as_<name>`, `asdf_value_of_<name>`,
10//! `asdf_<name>_copy`, `asdf_<name>_copy_into`, `asdf_<name>_array_copy`,
11//! `asdf_<name>_deinit` and `asdf_<name>_destroy`.
12//!
13//! The C types they work with are plain structs of borrowed pointers, so
14//! each has a `deinit` that frees the fields without freeing the struct —
15//! the split libasdf's headers call out, because an object may be embedded,
16//! an array element, or static.
17
18use alloc::ffi::CString;
19use core::ffi::{CStr, c_char, c_int};
20
21use asdf_core::yaml::{Document, NodeId, Tag};
22
23use crate::extension_ffi::{asdf_software_t, asdf_tag_t};
24use crate::file_ffi::{AsdfFile, AsdfValue, value_document, value_file, value_node};
25use crate::panic::guard;
26use crate::types::AsdfValueErr;
27use crate::version_ffi::{asdf_version_parse, asdf_version_t};
28
29/// Allocate a C string, or null when the text contains an interior NUL.
30fn to_c_string(text: &str) -> *const c_char {
31    CString::new(text).map_or(core::ptr::null(), |c| c.into_raw().cast_const())
32}
33
34/// Free a string produced by [`to_c_string`].
35unsafe fn free_c_string(ptr: *const c_char) {
36    if !ptr.is_null() {
37        drop(unsafe { CString::from_raw(ptr.cast_mut()) });
38    }
39}
40
41/// Copy a C string, or null.
42unsafe fn clone_c_string(ptr: *const c_char) -> *const c_char {
43    let Some(text) = (unsafe { crate::ffi::c_str(ptr) }) else {
44        return core::ptr::null();
45    };
46    CString::new(text.to_bytes()).map_or(core::ptr::null(), |c| c.into_raw().cast_const())
47}
48
49/// Read a mapping's string entry.
50fn string_field(doc: &Document, node: NodeId, key: &str) -> *const c_char {
51    doc.mapping_get(node, key)
52        .and_then(|id| doc.resolved(id).as_str().map(to_c_string))
53        .unwrap_or(core::ptr::null())
54}
55
56/// Read a mapping's entry as a parsed version.
57fn version_field(doc: &Document, node: NodeId, key: &str) -> *const asdf_version_t {
58    let Some(text) =
59        doc.mapping_get(node, key).and_then(|id| doc.resolved(id).as_str().map(str::to_string))
60    else {
61        return core::ptr::null();
62    };
63    let Ok(c) = CString::new(text) else {
64        return core::ptr::null();
65    };
66    unsafe { asdf_version_parse(c.as_ptr()) }.cast_const()
67}
68
69/// Generate the eleven functions `ASDF_REGISTER_EXTENSION` produces.
70///
71/// `$deserialize` fills a zeroed object from a value; `$serialize` builds a
72/// value from an object; `$deinit` frees the object's fields; `$copy`
73/// deep-copies into pre-zeroed storage.
74macro_rules! declare_extension {
75    (
76        name: $name:ident,
77        ty: $ty:ty,
78        tag: $tag:expr,
79        deserialize: $deserialize:path,
80        serialize: $serialize:path,
81        deinit: $deinit:path,
82        copy: $copy:path,
83        is_fn: $is_fn:ident,
84        value_is_fn: $value_is_fn:ident,
85        value_as_fn: $value_as_fn:ident,
86        value_of_fn: $value_of_fn:ident,
87        get_fn: $get_fn:ident,
88        set_fn: $set_fn:ident,
89        copy_fn: $copy_fn:ident,
90        copy_into_fn: $copy_into_fn:ident,
91        array_copy_fn: $array_copy_fn:ident,
92        deinit_fn: $deinit_fn:ident,
93        destroy_fn: $destroy_fn:ident,
94        tags: $tags:expr,
95        ext_build_fn: $ext_build_fn:ident,
96        ext_deserialize_fn: $ext_deserialize_fn:ident,
97        ext_serialize_fn: $ext_serialize_fn:ident,
98        ext_copy_fn: $ext_copy_fn:ident,
99        ext_deinit_fn: $ext_deinit_fn:ident,
100    ) => {
101        /// Whether a value carries one of this extension's tags.
102        ///
103        /// Every schema version the extension declares counts, not just the
104        /// newest: `core/asdf-1.0.0` and `-1.1.0` share a deserializer, and
105        /// `time/time` has five versions behind one. Matching only the
106        /// newest would leave most of the reference corpus unreadable.
107        ///
108        /// # Safety
109        /// `value` must be null or a valid value handle.
110        #[unsafe(no_mangle)]
111        pub unsafe extern "C" fn $value_is_fn(value: *mut AsdfValue) -> bool {
112            guard(stringify!($value_is_fn), false, || {
113                let (Some(doc), Some(node)) = (value_document(value), value_node(value)) else {
114                    return false;
115                };
116                doc.tag_of(node).is_some_and(|found| {
117                    let found = found.full();
118                    let tags: &[&CStr] = $tags;
119                    tags.iter().any(|t| t.to_bytes() == found.as_bytes())
120                })
121            })
122        }
123
124        /// Whether the value at `path` carries this extension's tag.
125        ///
126        /// # Safety
127        /// `file` must be a valid file handle and `path` a valid string or
128        /// null.
129        #[unsafe(no_mangle)]
130        pub unsafe extern "C" fn $is_fn(file: *mut AsdfFile, path: *const c_char) -> bool {
131            guard(stringify!($is_fn), false, || {
132                let value = unsafe { crate::file_ffi::asdf_get_value(file, path) };
133                if value.is_null() {
134                    return false;
135                }
136                let matched = unsafe { $value_is_fn(value) };
137                unsafe { crate::file_ffi::asdf_value_destroy(value) };
138                matched
139            })
140        }
141
142        /// Read a value as this extension's type.
143        ///
144        /// # Safety
145        /// `value` must be a valid value handle and `out` writable. The
146        /// result must be released with the matching `destroy`.
147        #[unsafe(no_mangle)]
148        pub unsafe extern "C" fn $value_as_fn(
149            value: *mut AsdfValue,
150            out: *mut *mut $ty,
151        ) -> AsdfValueErr {
152            guard(stringify!($value_as_fn), AsdfValueErr::Unknown, || {
153                if out.is_null() {
154                    return AsdfValueErr::Unknown;
155                }
156                if !unsafe { $value_is_fn(value) } {
157                    return AsdfValueErr::TypeMismatch;
158                }
159                let (Some(doc), Some(node)) = (value_document(value), value_node(value)) else {
160                    return AsdfValueErr::Unknown;
161                };
162                // The file goes through too: an extension whose object holds
163                // a value -- `extension_metadata`'s spare properties, say --
164                // needs one to build a handle against.
165                let file = value_file(value).unwrap_or(core::ptr::null_mut());
166                let boxed: Box<$ty> = Box::new(<$ty>::zeroed());
167                let raw = Box::into_raw(boxed);
168                match $deserialize(doc, node, file, raw) {
169                    AsdfValueErr::Ok => {
170                        unsafe { write_out(out, raw) };
171                        AsdfValueErr::Ok
172                    }
173                    err => {
174                        unsafe { $destroy_fn(raw) };
175                        err
176                    }
177                }
178            })
179        }
180
181        /// Read the value at `path` as this extension's type.
182        ///
183        /// # Safety
184        /// See the value-level reader.
185        #[unsafe(no_mangle)]
186        pub unsafe extern "C" fn $get_fn(
187            file: *mut AsdfFile,
188            path: *const c_char,
189            out: *mut *mut $ty,
190        ) -> AsdfValueErr {
191            guard(stringify!($get_fn), AsdfValueErr::Unknown, || {
192                let value = unsafe { crate::file_ffi::asdf_get_value(file, path) };
193                if value.is_null() {
194                    return AsdfValueErr::NotFound;
195                }
196                let result = unsafe { $value_as_fn(value, out) };
197                unsafe { crate::file_ffi::asdf_value_destroy(value) };
198                result
199            })
200        }
201
202        /// Build a value from an object of this extension's type.
203        ///
204        /// # Safety
205        /// `file` must be a valid file handle and `obj` a valid object. The
206        /// result must be released with `asdf_value_destroy`.
207        #[unsafe(no_mangle)]
208        pub unsafe extern "C" fn $value_of_fn(
209            file: *mut AsdfFile,
210            obj: *const $ty,
211        ) -> *mut AsdfValue {
212            guard(stringify!($value_of_fn), core::ptr::null_mut(), || {
213                if file.is_null() || obj.is_null() {
214                    return core::ptr::null_mut();
215                }
216                let Some(doc) = $crate::file_ffi::file_document_mut(file) else {
217                    return core::ptr::null_mut();
218                };
219                let Some(node) = $serialize(doc, unsafe { &*obj }) else {
220                    return core::ptr::null_mut();
221                };
222                doc.node_mut(node).tag = Some(Tag::parse($tag));
223                Box::into_raw(Box::new(AsdfValue::new(file, node)))
224            })
225        }
226
227        /// Write an object of this extension's type at `path`.
228        ///
229        /// # Safety
230        /// See the value constructor; `path` must be a valid string or null.
231        #[unsafe(no_mangle)]
232        pub unsafe extern "C" fn $set_fn(
233            file: *mut AsdfFile,
234            path: *const c_char,
235            obj: *const $ty,
236        ) -> AsdfValueErr {
237            guard(stringify!($set_fn), AsdfValueErr::Unknown, || {
238                let value = unsafe { $value_of_fn(file, obj) };
239                if value.is_null() {
240                    return AsdfValueErr::EmitFailure;
241                }
242                let result = unsafe { crate::file_ffi::set_value_at(file, path, value) };
243                unsafe { crate::file_ffi::asdf_value_destroy(value) };
244                result
245            })
246        }
247
248        /// Free the object's fields without freeing the object itself.
249        ///
250        /// The split matters because an object may be embedded, an array
251        /// element, or static, so its own storage is not always ours to free.
252        ///
253        /// # Safety
254        /// `obj` must be null or a valid object of this type; it must be safe
255        /// to call on a zeroed or partially-initialised one.
256        #[unsafe(no_mangle)]
257        pub unsafe extern "C" fn $deinit_fn(obj: *mut $ty) {
258            guard(stringify!($deinit_fn), (), || {
259                if !obj.is_null() {
260                    unsafe { $deinit(obj) };
261                }
262            })
263        }
264
265        /// De-initialise and free an object.
266        ///
267        /// # Safety
268        /// `obj` must be null or have come from this extension, and must not
269        /// be used afterwards.
270        #[unsafe(no_mangle)]
271        pub unsafe extern "C" fn $destroy_fn(obj: *mut $ty) {
272            guard(stringify!($destroy_fn), (), || {
273                if obj.is_null() {
274                    return;
275                }
276                unsafe { $deinit(obj) };
277                drop(unsafe { Box::from_raw(obj) });
278            })
279        }
280
281        /// Deep-copy an object into caller-provided storage.
282        ///
283        /// `dst` is zeroed first, and de-initialised on failure, matching the
284        /// generated wrapper's contract.
285        ///
286        /// # Safety
287        /// `src` and `dst` must be valid objects of this type.
288        #[unsafe(no_mangle)]
289        pub unsafe extern "C" fn $copy_into_fn(
290            file: *mut AsdfFile,
291            src: *const $ty,
292            dst: *mut $ty,
293        ) -> bool {
294            guard(stringify!($copy_into_fn), false, || {
295                let _ = file;
296                if src.is_null() || dst.is_null() {
297                    return false;
298                }
299                unsafe { core::ptr::write(dst, <$ty>::zeroed()) };
300                if unsafe { $copy(&*src, dst) } {
301                    true
302                } else {
303                    unsafe { $deinit(dst) };
304                    false
305                }
306            })
307        }
308
309        /// Deep-copy an object into fresh storage.
310        ///
311        /// # Safety
312        /// `src` must be a valid object of this type. The result must be
313        /// released with the matching `destroy`.
314        #[unsafe(no_mangle)]
315        pub unsafe extern "C" fn $copy_fn(file: *mut AsdfFile, src: *const $ty) -> *mut $ty {
316            guard(stringify!($copy_fn), core::ptr::null_mut(), || {
317                if src.is_null() {
318                    return core::ptr::null_mut();
319                }
320                let raw = Box::into_raw(Box::new(<$ty>::zeroed()));
321                if unsafe { $copy_into_fn(file, src, raw) } {
322                    raw
323                } else {
324                    drop(unsafe { Box::from_raw(raw) });
325                    core::ptr::null_mut()
326                }
327            })
328        }
329
330        /// Deep-copy a null-terminated array of objects.
331        ///
332        /// # Safety
333        /// `src` must be a null-terminated array of valid objects.
334        #[unsafe(no_mangle)]
335        pub unsafe extern "C" fn $array_copy_fn(
336            file: *mut AsdfFile,
337            src: *mut *const $ty,
338        ) -> *mut *mut $ty {
339            guard(stringify!($array_copy_fn), core::ptr::null_mut(), || {
340                if src.is_null() {
341                    return core::ptr::null_mut();
342                }
343                let mut count = 0isize;
344                while !unsafe { *src.offset(count) }.is_null() {
345                    count += 1;
346                }
347
348                let mut copies: Vec<*mut $ty> = Vec::with_capacity(count as usize + 1);
349                for index in 0..count {
350                    let element = unsafe { *src.offset(index) };
351                    let copy = unsafe { $copy_fn(file, element) };
352                    if copy.is_null() {
353                        // Unwind the copies made so far rather than leaking.
354                        for made in copies {
355                            unsafe { $destroy_fn(made) };
356                        }
357                        return core::ptr::null_mut();
358                    }
359                    copies.push(copy);
360                }
361                copies.push(core::ptr::null_mut());
362                copies.shrink_to_fit();
363                let boxed = copies.into_boxed_slice();
364                Box::into_raw(boxed).cast::<*mut $ty>()
365            })
366        }
367
368        // ---- Registry entry ------------------------------------------
369        //
370        // `ASDF_REGISTER_EXTENSION` puts each core extension in the
371        // process-wide registry, and generic code reaches them only that
372        // way: `asdf_extension_get(file, tag)` followed by
373        // `asdf_value_as_extension_type`. Upstream's own
374        // `test-reference-files` drives every tagged value in the corpus
375        // through exactly that path, so the typed functions above are not
376        // enough on their own.
377
378        /// Deserialize through the registry's generic entry point.
379        ///
380        /// Deliberately *not* routed through the typed
381        /// `asdf_value_as_<name>`: that one checks the tag first, and a
382        /// vtable method must not. The tag check belongs to
383        /// `asdf_value_as_extension_type`, which is what lets a caller
384        /// deserialize an untagged value -- an ndarray's `datatype`, say --
385        /// by reaching for the vtable directly, as upstream's own test does.
386        ///
387        /// # Safety
388        /// `value` must be a valid value handle and `out` writable.
389        unsafe extern "C" fn $ext_deserialize_fn(
390            value: *mut AsdfValue,
391            _userdata: *const core::ffi::c_void,
392            out: *mut *mut core::ffi::c_void,
393        ) -> AsdfValueErr {
394            guard(stringify!($ext_deserialize_fn), AsdfValueErr::Unknown, || {
395                if out.is_null() {
396                    return AsdfValueErr::Unknown;
397                }
398                let (Some(doc), Some(node)) = (value_document(value), value_node(value)) else {
399                    return AsdfValueErr::Unknown;
400                };
401                let file = value_file(value).unwrap_or(core::ptr::null_mut());
402                let raw = Box::into_raw(Box::new(<$ty>::zeroed()));
403                match $deserialize(doc, node, file, raw) {
404                    AsdfValueErr::Ok => {
405                        unsafe { write_out(out, raw.cast::<core::ffi::c_void>()) };
406                        AsdfValueErr::Ok
407                    }
408                    err => {
409                        unsafe { $destroy_fn(raw) };
410                        err
411                    }
412                }
413            })
414        }
415
416        /// Serialize through the registry's generic entry point.
417        ///
418        /// # Safety
419        /// `obj` must be a valid object of this extension's type.
420        unsafe extern "C" fn $ext_serialize_fn(
421            file: *mut AsdfFile,
422            obj: *const core::ffi::c_void,
423            _userdata: *const core::ffi::c_void,
424        ) -> *mut AsdfValue {
425            unsafe { $value_of_fn(file, obj.cast::<$ty>()) }
426        }
427
428        /// Deep-copy through the registry's generic entry point.
429        ///
430        /// # Safety
431        /// `src` and `dst` must be valid objects of this extension's type.
432        unsafe extern "C" fn $ext_copy_fn(
433            file: *mut AsdfFile,
434            src: *const core::ffi::c_void,
435            dst: *mut core::ffi::c_void,
436        ) -> bool {
437            unsafe { $copy_into_fn(file, src.cast::<$ty>(), dst.cast::<$ty>()) }
438        }
439
440        /// De-initialise through the registry's generic entry point.
441        ///
442        /// # Safety
443        /// `obj` must be a valid object of this extension's type.
444        unsafe extern "C" fn $ext_deinit_fn(obj: *mut core::ffi::c_void) {
445            unsafe { $deinit_fn(obj.cast::<$ty>()) };
446        }
447
448        /// Build this extension's registry entry.
449        ///
450        /// The parts are leaked deliberately: `ASDF_REGISTER_EXTENSION`
451        /// makes them file-scope `static`s, and the registry stores the
452        /// pointer rather than a copy, so they must outlive every use. Seven
453        /// of these exist for the life of the process.
454        fn $ext_build_fn() -> *mut crate::extension_ffi::asdf_extension_t {
455            use crate::extension_ffi::{asdf_extension_t, asdf_extension_vtab_t, libasdf_software};
456
457            let mut tags: Vec<*const c_char> = $tags.iter().map(|t: &&CStr| t.as_ptr()).collect();
458            tags.push(core::ptr::null());
459            let tags = Box::leak(tags.into_boxed_slice());
460
461            let vtab = Box::leak(Box::new(asdf_extension_vtab_t {
462                serialize: Some($ext_serialize_fn),
463                deserialize: Some($ext_deserialize_fn),
464                copy: Some($ext_copy_fn),
465                deinit: Some($ext_deinit_fn),
466                _reserved: [None; 4],
467            }));
468
469            Box::leak(Box::new(asdf_extension_t {
470                tags: tags.as_ptr(),
471                software: (&raw const libasdf_software)
472                    .cast::<crate::extension_ffi::asdf_software_t>()
473                    .cast_mut(),
474                vtab: core::ptr::from_ref(vtab),
475                size: core::mem::size_of::<$ty>(),
476                userdata: core::ptr::null_mut(),
477            }))
478        }
479    };
480}
481
482// ---- core/software ---------------------------------------------------
483
484/// The tag for `core/software`.
485pub const SOFTWARE_TAG: &str = "tag:stsci.edu:asdf/core/software-1.0.0";
486
487impl asdf_software_t {
488    /// A zeroed instance, matching what the generated wrappers assume.
489    fn zeroed() -> Self {
490        Self {
491            name: core::ptr::null(),
492            version: core::ptr::null(),
493            author: core::ptr::null(),
494            homepage: core::ptr::null(),
495        }
496    }
497}
498
499fn software_deserialize(
500    doc: &Document,
501    node: NodeId,
502    _file: *mut AsdfFile,
503    out: *mut asdf_software_t,
504) -> AsdfValueErr {
505    // `name` and `version` are required by the schema.
506    let name = string_field(doc, node, "name");
507    let version = version_field(doc, node, "version");
508    if name.is_null() || version.is_null() {
509        unsafe { free_c_string(name) };
510        if !version.is_null() {
511            unsafe { crate::version_ffi::asdf_version_destroy(version.cast_mut()) };
512        }
513        return AsdfValueErr::ParseFailure;
514    }
515
516    unsafe {
517        (*out).name = name;
518        (*out).version = version;
519        (*out).author = string_field(doc, node, "author");
520        (*out).homepage = string_field(doc, node, "homepage");
521    }
522    AsdfValueErr::Ok
523}
524
525fn software_serialize(doc: &mut Document, obj: &asdf_software_t) -> Option<NodeId> {
526    let mut pairs = Vec::new();
527
528    let mut put = |doc: &mut Document, key: &str, ptr: *const c_char| {
529        if ptr.is_null() {
530            return;
531        }
532        let text = unsafe { CStr::from_ptr(ptr) }.to_string_lossy().into_owned();
533        let k = doc.add_scalar(key);
534        let v = doc.add_scalar_styled(text, asdf_core::yaml::ScalarStyle::SingleQuoted);
535        pairs.push((k, v));
536    };
537
538    put(doc, "name", obj.name);
539    if !obj.version.is_null() {
540        let version = unsafe { &*obj.version };
541        put(doc, "version", version.version);
542    }
543    put(doc, "author", obj.author);
544    put(doc, "homepage", obj.homepage);
545
546    (!pairs.is_empty()).then(|| doc.add_mapping(pairs))
547}
548
549unsafe fn software_deinit(obj: *mut asdf_software_t) {
550    let software = unsafe { &mut *obj };
551    unsafe { free_c_string(software.name) };
552    unsafe { free_c_string(software.author) };
553    unsafe { free_c_string(software.homepage) };
554    if !software.version.is_null() {
555        unsafe { crate::version_ffi::asdf_version_destroy(software.version.cast_mut()) };
556    }
557    *software = asdf_software_t::zeroed();
558}
559
560unsafe fn software_copy(src: &asdf_software_t, dst: *mut asdf_software_t) -> bool {
561    let out = unsafe { &mut *dst };
562    out.name = unsafe { clone_c_string(src.name) };
563    out.author = unsafe { clone_c_string(src.author) };
564    out.homepage = unsafe { clone_c_string(src.homepage) };
565    out.version = if src.version.is_null() {
566        core::ptr::null()
567    } else {
568        unsafe { crate::version_ffi::asdf_version_copy(src.version) }.cast_const()
569    };
570    // Only a failed allocation of a field that was present is a failure.
571    !(out.name.is_null() && !src.name.is_null())
572}
573
574declare_extension! {
575    name: software,
576    ty: asdf_software_t,
577    tag: SOFTWARE_TAG,
578    deserialize: software_deserialize,
579    serialize: software_serialize,
580    deinit: software_deinit,
581    copy: software_copy,
582    is_fn: asdf_is_software,
583    value_is_fn: asdf_value_is_software,
584    value_as_fn: asdf_value_as_software,
585    value_of_fn: asdf_value_of_software,
586    get_fn: asdf_get_software,
587    set_fn: asdf_set_software,
588    copy_fn: asdf_software_copy,
589    copy_into_fn: asdf_software_copy_into,
590    array_copy_fn: asdf_software_array_copy,
591    deinit_fn: asdf_software_deinit,
592    destroy_fn: asdf_software_destroy,
593    // The tag list upstream's `ASDF_REGISTER_EXTENSION` declares.
594    tags: &[c"tag:stsci.edu:asdf/core/software-1.0.0"],
595    ext_build_fn: build_software_extension,
596    ext_deserialize_fn: software_ext_deserialize,
597    ext_serialize_fn: software_ext_serialize,
598    ext_copy_fn: software_ext_copy,
599    ext_deinit_fn: software_ext_deinit,
600}
601
602// ---- core/extension_metadata ----------------------------------------
603
604/// The tag for `core/extension_metadata`.
605pub const EXTENSION_METADATA_TAG: &str = "tag:stsci.edu:asdf/core/extension_metadata-1.0.0";
606
607/// Mirror of `asdf_extension_metadata_t`.
608#[repr(C)]
609#[derive(Debug)]
610pub struct asdf_extension_metadata_t {
611    /// The extension class that wrote the file.
612    pub extension_class: *const c_char,
613    /// The package providing it.
614    pub package: *const asdf_software_t,
615    /// Any further metadata, as a mapping value.
616    pub metadata: *mut AsdfValue,
617}
618
619impl asdf_extension_metadata_t {
620    fn zeroed() -> Self {
621        Self {
622            extension_class: core::ptr::null(),
623            package: core::ptr::null(),
624            metadata: core::ptr::null_mut(),
625        }
626    }
627}
628
629fn extension_metadata_deserialize(
630    doc: &Document,
631    node: NodeId,
632    file: *mut AsdfFile,
633    out: *mut asdf_extension_metadata_t,
634) -> AsdfValueErr {
635    let class = string_field(doc, node, "extension_class");
636    if class.is_null() {
637        return AsdfValueErr::ParseFailure;
638    }
639
640    // Only `package`. The schema also has `software` and
641    // `manifest_software` keys, but those are not the package -- reading
642    // either into `package` makes a file that has no package look as though
643    // it has one, which is exactly what upstream's own test checks against.
644    let package = doc
645        .mapping_get(node, "package")
646        .map(|id| {
647            let raw = Box::into_raw(Box::new(asdf_software_t::zeroed()));
648            if software_deserialize(doc, id, file, raw) == AsdfValueErr::Ok {
649                raw.cast_const()
650            } else {
651                drop(unsafe { Box::from_raw(raw) });
652                core::ptr::null()
653            }
654        })
655        .unwrap_or(core::ptr::null());
656
657    // `metadata` is the whole mapping, so a caller can reach the properties
658    // the struct has no field for -- `extension_uri`, `manifest_software`.
659    let metadata = if file.is_null() {
660        core::ptr::null_mut()
661    } else {
662        crate::value_ffi::make_value(file, node)
663    };
664
665    unsafe {
666        (*out).extension_class = class;
667        (*out).package = package;
668        (*out).metadata = metadata;
669    }
670    AsdfValueErr::Ok
671}
672
673fn extension_metadata_serialize(
674    doc: &mut Document,
675    obj: &asdf_extension_metadata_t,
676) -> Option<NodeId> {
677    let mut pairs = Vec::new();
678    if !obj.extension_class.is_null() {
679        let text = unsafe { CStr::from_ptr(obj.extension_class) }.to_string_lossy().into_owned();
680        let k = doc.add_scalar("extension_class");
681        let v = doc.add_scalar_styled(text, asdf_core::yaml::ScalarStyle::SingleQuoted);
682        pairs.push((k, v));
683    }
684    if !obj.package.is_null()
685        && let Some(node) = software_serialize(doc, unsafe { &*obj.package })
686    {
687        doc.node_mut(node).tag = Some(Tag::parse(SOFTWARE_TAG));
688        // `package`, matching the key the deserializer reads. Writing it as
689        // `software` would make the value fail to round-trip through its own
690        // reader, which is what upstream's serialize test catches.
691        let k = doc.add_scalar("package");
692        pairs.push((k, node));
693    }
694
695    // Any further properties the struct has no field for -- `extension_uri`,
696    // `manifest_software` -- ride along in `metadata`. The two keys with
697    // fields of their own are skipped so they cannot be written twice.
698    if let Some(node) = value_node(obj.metadata)
699        && let Some(entries) = doc.mapping_entries(doc.resolve(node)).map(<[_]>::to_vec)
700    {
701        for entry in entries {
702            let key = doc.resolved(entry.key).as_str().map(str::to_string);
703            if matches!(key.as_deref(), Some("extension_class" | "package")) {
704                continue;
705            }
706            pairs.push((entry.key, entry.value));
707        }
708    }
709
710    (!pairs.is_empty()).then(|| doc.add_mapping(pairs))
711}
712
713unsafe fn extension_metadata_deinit(obj: *mut asdf_extension_metadata_t) {
714    let metadata = unsafe { &mut *obj };
715    unsafe { free_c_string(metadata.extension_class) };
716    if !metadata.package.is_null() {
717        unsafe { asdf_software_destroy(metadata.package.cast_mut()) };
718    }
719    if !metadata.metadata.is_null() {
720        unsafe { crate::file_ffi::asdf_value_destroy(metadata.metadata) };
721    }
722    *metadata = asdf_extension_metadata_t::zeroed();
723}
724
725unsafe fn extension_metadata_copy(
726    src: &asdf_extension_metadata_t,
727    dst: *mut asdf_extension_metadata_t,
728) -> bool {
729    let out = unsafe { &mut *dst };
730    out.extension_class = unsafe { clone_c_string(src.extension_class) };
731    out.package = if src.package.is_null() {
732        core::ptr::null()
733    } else {
734        unsafe { asdf_software_copy(core::ptr::null_mut(), src.package) }.cast_const()
735    };
736    out.metadata = core::ptr::null_mut();
737    true
738}
739
740declare_extension! {
741    name: extension_metadata,
742    ty: asdf_extension_metadata_t,
743    tag: EXTENSION_METADATA_TAG,
744    deserialize: extension_metadata_deserialize,
745    serialize: extension_metadata_serialize,
746    deinit: extension_metadata_deinit,
747    copy: extension_metadata_copy,
748    is_fn: asdf_is_extension_metadata,
749    value_is_fn: asdf_value_is_extension_metadata,
750    value_as_fn: asdf_value_as_extension_metadata,
751    value_of_fn: asdf_value_of_extension_metadata,
752    get_fn: asdf_get_extension_metadata,
753    set_fn: asdf_set_extension_metadata,
754    copy_fn: asdf_extension_metadata_copy,
755    copy_into_fn: asdf_extension_metadata_copy_into,
756    array_copy_fn: asdf_extension_metadata_array_copy,
757    deinit_fn: asdf_extension_metadata_deinit,
758    destroy_fn: asdf_extension_metadata_destroy,
759    // The tag list upstream's `ASDF_REGISTER_EXTENSION` declares.
760    tags: &[c"tag:stsci.edu:asdf/core/extension_metadata-1.0.0"],
761    ext_build_fn: build_extension_metadata_extension,
762    ext_deserialize_fn: extension_metadata_ext_deserialize,
763    ext_serialize_fn: extension_metadata_ext_serialize,
764    ext_copy_fn: extension_metadata_ext_copy,
765    ext_deinit_fn: extension_metadata_ext_deinit,
766}
767
768/// Override the `asdf_library` metadata written to a file.
769///
770/// # Safety
771/// `file` must be a valid file handle and `software` a valid object, which is
772/// copied.
773#[unsafe(no_mangle)]
774pub unsafe extern "C" fn asdf_library_set(file: *mut AsdfFile, software: *const asdf_software_t) {
775    guard("asdf_library_set", (), || {
776        if file.is_null() || software.is_null() {
777            return;
778        }
779        let path = c"asdf_library";
780        unsafe { asdf_set_software(file, path.as_ptr(), software) };
781    })
782}
783
784/// Override only the version of the `asdf_library` metadata.
785///
786/// # Safety
787/// `file` must be a valid file handle and `version` a valid NUL-terminated
788/// string.
789#[unsafe(no_mangle)]
790pub unsafe extern "C" fn asdf_library_set_version(file: *mut AsdfFile, version: *const c_char) {
791    guard("asdf_library_set_version", (), || {
792        if file.is_null() || version.is_null() {
793            return;
794        }
795        let path = c"asdf_library/version";
796        let value = unsafe { crate::value_ffi::asdf_value_of_string0(file, version) };
797        if value.is_null() {
798            return;
799        }
800        unsafe { crate::file_ffi::set_value_at(file, path.as_ptr(), value) };
801        unsafe { crate::file_ffi::asdf_value_destroy(value) };
802    })
803}
804
805/// Parse a tag string, exposed for extension authors.
806///
807/// # Safety
808/// See [`crate::extension_ffi::asdf_tag_parse`], which this forwards to.
809pub unsafe fn parse_tag(tag: *const c_char) -> *mut asdf_tag_t {
810    unsafe { crate::extension_ffi::asdf_tag_parse(tag) }
811}
812
813#[cfg(test)]
814mod tests {
815    use super::*;
816    use crate::file_ffi::{asdf_close, asdf_open_mem_ex, asdf_write_to_mem};
817
818    struct Handle(*mut AsdfFile);
819    impl Drop for Handle {
820        fn drop(&mut self) {
821            unsafe { asdf_close(self.0) };
822        }
823    }
824
825    fn sample() -> Handle {
826        let mut buf = Vec::new();
827        buf.extend_from_slice(b"#ASDF 1.0.0\n#ASDF_STANDARD 1.6.0\n");
828        buf.extend_from_slice(b"%YAML 1.1\n%TAG ! tag:stsci.edu:asdf/\n--- !core/asdf-1.1.0\n");
829        buf.extend_from_slice(
830            b"asdf_library: !core/software-1.0.0\n  \
831              author: The ASDF Developers\n  \
832              homepage: 'http://github.com/asdf-format/asdf'\n  \
833              name: asdf\n  version: 4.1.0\n\
834              history:\n  extensions:\n  - !core/extension_metadata-1.0.0\n    \
835              extension_class: asdf.extension._manifest.ManifestExtension\n    \
836              extension_uri: asdf://asdf-format.org/core/extensions/core-1.6.0\n    \
837              package: !core/software-1.0.0 {name: asdf_standard, version: 1.1.1}\n    \
838              software: !core/software-1.0.0 {name: asdf, version: 4.1.0}\n  \
839              - !core/extension_metadata-1.0.0\n    \
840              extension_class: some.other.Extension\n    \
841              software: !core/software-1.0.0 {name: asdf, version: 4.1.0}\n",
842        );
843        buf.extend_from_slice(b"...\n");
844        let f = unsafe { asdf_open_mem_ex(buf.as_ptr().cast(), buf.len(), core::ptr::null_mut()) };
845        assert!(!f.is_null());
846        Handle(f)
847    }
848
849    #[test]
850    fn reads_the_asdf_library_software() {
851        let h = sample();
852        let path = c"asdf_library";
853        assert!(unsafe { asdf_is_software(h.0, path.as_ptr()) });
854
855        let mut software: *mut asdf_software_t = core::ptr::null_mut();
856        assert_eq!(
857            unsafe { asdf_get_software(h.0, path.as_ptr(), &mut software) },
858            AsdfValueErr::Ok
859        );
860        assert!(!software.is_null());
861
862        let view = unsafe { &*software };
863        assert_eq!(unsafe { CStr::from_ptr(view.name) }.to_str().unwrap(), "asdf");
864        assert!(!view.version.is_null());
865        let version = unsafe { &*view.version };
866        assert_eq!((version.major, version.minor, version.patch), (4, 1, 0));
867        assert_eq!(unsafe { CStr::from_ptr(view.author) }.to_str().unwrap(), "The ASDF Developers");
868
869        unsafe { asdf_software_destroy(software) };
870    }
871
872    #[test]
873    fn a_wrong_tag_is_a_mismatch() {
874        let h = sample();
875        let path = c"history";
876        assert!(!unsafe { asdf_is_software(h.0, path.as_ptr()) });
877
878        let mut software: *mut asdf_software_t = core::ptr::null_mut();
879        assert_eq!(
880            unsafe { asdf_get_software(h.0, path.as_ptr(), &mut software) },
881            AsdfValueErr::TypeMismatch
882        );
883        assert!(software.is_null());
884    }
885
886    #[test]
887    fn a_missing_path_is_not_found() {
888        let h = sample();
889        let path = c"nope";
890        let mut software: *mut asdf_software_t = core::ptr::null_mut();
891        assert_eq!(
892            unsafe { asdf_get_software(h.0, path.as_ptr(), &mut software) },
893            AsdfValueErr::NotFound
894        );
895    }
896
897    #[test]
898    fn software_round_trips_through_a_written_file() {
899        let f = unsafe { asdf_open_mem_ex(core::ptr::null(), 0, core::ptr::null_mut()) };
900        let h = Handle(f);
901
902        let version = unsafe { asdf_version_parse(c"2.3.4".as_ptr()) };
903        let software = asdf_software_t {
904            name: c"my-writer".as_ptr(),
905            version: version.cast_const(),
906            author: c"Someone".as_ptr(),
907            homepage: c"https://example.com".as_ptr(),
908        };
909
910        let path = c"asdf_library";
911        assert_eq!(unsafe { asdf_set_software(h.0, path.as_ptr(), &software) }, AsdfValueErr::Ok);
912
913        let mut buf: *mut core::ffi::c_void = core::ptr::null_mut();
914        let mut size = 0usize;
915        assert_eq!(unsafe { asdf_write_to_mem(h.0, &mut buf, &mut size) }, 0);
916
917        let reopened = unsafe { asdf_open_mem_ex(buf, size, core::ptr::null_mut()) };
918        let r = Handle(reopened);
919
920        let mut read_back: *mut asdf_software_t = core::ptr::null_mut();
921        assert_eq!(
922            unsafe { asdf_get_software(r.0, path.as_ptr(), &mut read_back) },
923            AsdfValueErr::Ok
924        );
925        let view = unsafe { &*read_back };
926        assert_eq!(unsafe { CStr::from_ptr(view.name) }.to_str().unwrap(), "my-writer");
927        assert_eq!(unsafe { &*view.version }.minor, 3);
928
929        unsafe { asdf_software_destroy(read_back) };
930        unsafe { libc::free(buf) };
931        unsafe { crate::version_ffi::asdf_version_destroy(version) };
932    }
933
934    #[test]
935    fn copies_are_independent() {
936        let h = sample();
937        let path = c"asdf_library";
938        let mut software: *mut asdf_software_t = core::ptr::null_mut();
939        unsafe { asdf_get_software(h.0, path.as_ptr(), &mut software) };
940
941        let copy = unsafe { asdf_software_copy(h.0, software) };
942        assert!(!copy.is_null());
943        // Distinct allocations for every owned field.
944        unsafe {
945            assert_ne!((*copy).name, (*software).name);
946            assert_ne!((*copy).version, (*software).version);
947        }
948
949        // Freeing the original must leave the copy intact.
950        unsafe { asdf_software_destroy(software) };
951        assert_eq!(unsafe { CStr::from_ptr((*copy).name) }.to_str().unwrap(), "asdf");
952        unsafe { asdf_software_destroy(copy) };
953    }
954
955    #[test]
956    fn deinit_is_safe_on_a_zeroed_object() {
957        // The header requires this: the generated copy wrapper zeroes `dst`
958        // and de-initialises it on failure.
959        let mut zeroed = asdf_software_t::zeroed();
960        unsafe { asdf_software_deinit(&mut zeroed) };
961        unsafe { asdf_software_deinit(&mut zeroed) };
962        unsafe { asdf_software_deinit(core::ptr::null_mut()) };
963    }
964
965    #[test]
966    fn copy_into_zeroes_the_destination_first() {
967        let h = sample();
968        let path = c"asdf_library";
969        let mut software: *mut asdf_software_t = core::ptr::null_mut();
970        unsafe { asdf_get_software(h.0, path.as_ptr(), &mut software) };
971
972        let mut destination = asdf_software_t::zeroed();
973        assert!(unsafe { asdf_software_copy_into(h.0, software, &mut destination) });
974        assert_eq!(unsafe { CStr::from_ptr(destination.name) }.to_str().unwrap(), "asdf");
975
976        unsafe { asdf_software_deinit(&mut destination) };
977        unsafe { asdf_software_destroy(software) };
978    }
979
980    #[test]
981    fn arrays_of_objects_copy() {
982        let h = sample();
983        let path = c"asdf_library";
984        let mut software: *mut asdf_software_t = core::ptr::null_mut();
985        unsafe { asdf_get_software(h.0, path.as_ptr(), &mut software) };
986
987        let mut array: [*const asdf_software_t; 2] = [software, core::ptr::null()];
988        let copies = unsafe { asdf_software_array_copy(h.0, array.as_mut_ptr()) };
989        assert!(!copies.is_null());
990
991        let first = unsafe { *copies };
992        assert!(!first.is_null());
993        assert_eq!(unsafe { CStr::from_ptr((*first).name) }.to_str().unwrap(), "asdf");
994        // Null-terminated, as the C helper produces.
995        assert!(unsafe { *copies.offset(1) }.is_null());
996
997        unsafe { asdf_software_destroy(first) };
998        drop(unsafe { Box::from_raw(core::ptr::slice_from_raw_parts_mut(copies, 2)) });
999        unsafe { asdf_software_destroy(software) };
1000    }
1001
1002    #[test]
1003    fn reads_extension_metadata() {
1004        let h = sample();
1005        let path = c"history/extensions/0";
1006        assert!(unsafe { asdf_is_extension_metadata(h.0, path.as_ptr()) });
1007
1008        let mut metadata: *mut asdf_extension_metadata_t = core::ptr::null_mut();
1009        assert_eq!(
1010            unsafe { asdf_get_extension_metadata(h.0, path.as_ptr(), &mut metadata) },
1011            AsdfValueErr::Ok
1012        );
1013        let view = unsafe { &*metadata };
1014        assert_eq!(
1015            unsafe { CStr::from_ptr(view.extension_class) }.to_str().unwrap(),
1016            "asdf.extension._manifest.ManifestExtension"
1017        );
1018        // `package` is the `package` key alone. `software` and
1019        // `manifest_software` are different things, and reading either into
1020        // `package` would make a file that has none look as though it does.
1021        assert!(!view.package.is_null());
1022        assert_eq!(
1023            unsafe { CStr::from_ptr((*view.package).name) }.to_str().unwrap(),
1024            "asdf_standard"
1025        );
1026
1027        // Everything the struct has no field for stays reachable through
1028        // `metadata`, which is the whole mapping.
1029        assert!(!view.metadata.is_null());
1030        let uri = c"extension_uri";
1031        let entry = unsafe { crate::value_ffi::asdf_mapping_get(view.metadata, uri.as_ptr()) };
1032        assert!(!entry.is_null());
1033        let mut text = core::ptr::null();
1034        assert_eq!(
1035            unsafe { crate::value_ffi::asdf_value_as_string0(entry, &mut text) },
1036            AsdfValueErr::Ok
1037        );
1038        assert_eq!(
1039            unsafe { CStr::from_ptr(text) }.to_str().unwrap(),
1040            "asdf://asdf-format.org/core/extensions/core-1.6.0"
1041        );
1042        unsafe { crate::file_ffi::asdf_value_destroy(entry) };
1043        unsafe { asdf_extension_metadata_destroy(metadata) };
1044    }
1045
1046    /// An entry with no `package` must report none, rather than borrowing
1047    /// the `software` beside it.
1048    #[test]
1049    fn extension_metadata_without_a_package_reports_none() {
1050        let h = sample();
1051        let path = c"history/extensions/1";
1052        let mut metadata: *mut asdf_extension_metadata_t = core::ptr::null_mut();
1053        assert_eq!(
1054            unsafe { asdf_get_extension_metadata(h.0, path.as_ptr(), &mut metadata) },
1055            AsdfValueErr::Ok
1056        );
1057        let view = unsafe { &*metadata };
1058        assert!(view.package.is_null());
1059        assert!(!view.metadata.is_null(), "the mapping is still reachable");
1060        unsafe { asdf_extension_metadata_destroy(metadata) };
1061    }
1062
1063    #[test]
1064    fn null_handles_are_tolerated() {
1065        let mut out: *mut asdf_software_t = core::ptr::null_mut();
1066        assert_eq!(
1067            unsafe { asdf_value_as_software(core::ptr::null_mut(), &mut out) },
1068            AsdfValueErr::TypeMismatch
1069        );
1070        assert!(
1071            unsafe { asdf_value_of_software(core::ptr::null_mut(), core::ptr::null()) }.is_null()
1072        );
1073        assert!(unsafe { asdf_software_copy(core::ptr::null_mut(), core::ptr::null()) }.is_null());
1074        assert!(!unsafe {
1075            asdf_software_copy_into(core::ptr::null_mut(), core::ptr::null(), core::ptr::null_mut())
1076        });
1077        unsafe { asdf_software_destroy(core::ptr::null_mut()) };
1078        unsafe { asdf_library_set(core::ptr::null_mut(), core::ptr::null()) };
1079        unsafe { asdf_library_set_version(core::ptr::null_mut(), core::ptr::null()) };
1080    }
1081}
1082
1083// ---- time/time -------------------------------------------------------
1084
1085use crate::time_ffi::{
1086    TIME_TAG, asdf_time_t, time_copy, time_deinit, time_deserialize, time_serialize,
1087};
1088
1089declare_extension! {
1090    name: time,
1091    ty: asdf_time_t,
1092    tag: TIME_TAG,
1093    deserialize: time_deserialize,
1094    serialize: time_serialize,
1095    deinit: time_deinit,
1096    copy: time_copy,
1097    is_fn: asdf_is_time,
1098    value_is_fn: asdf_value_is_time,
1099    value_as_fn: asdf_value_as_time,
1100    value_of_fn: asdf_value_of_time,
1101    get_fn: asdf_get_time,
1102    set_fn: asdf_set_time,
1103    copy_fn: asdf_time_copy,
1104    copy_into_fn: asdf_time_copy_into,
1105    array_copy_fn: asdf_time_array_copy,
1106    deinit_fn: asdf_time_deinit,
1107    destroy_fn: asdf_time_destroy,
1108    // The tag list upstream's `ASDF_REGISTER_EXTENSION` declares.
1109    tags: &[
1110        c"tag:stsci.edu:asdf/time/time-1.4.0",
1111        c"tag:stsci.edu:asdf/time/time-1.3.0",
1112        c"tag:stsci.edu:asdf/time/time-1.2.0",
1113        c"tag:stsci.edu:asdf/time/time-1.1.0",
1114        c"tag:stsci.edu:asdf/time/time-1.0.0",
1115    ],
1116    ext_build_fn: build_time_extension,
1117    ext_deserialize_fn: time_ext_deserialize,
1118    ext_serialize_fn: time_ext_serialize,
1119    ext_copy_fn: time_ext_copy,
1120    ext_deinit_fn: time_ext_deinit,
1121}
1122
1123// ---- core/history_entry ----------------------------------------------
1124
1125/// The tag for `core/history_entry`.
1126pub const HISTORY_ENTRY_TAG: &str = "tag:stsci.edu:asdf/core/history_entry-1.0.0";
1127
1128/// Mirror of `asdf_history_entry_t`.
1129#[repr(C)]
1130#[derive(Debug)]
1131pub struct asdf_history_entry_t {
1132    /// What the entry records.
1133    pub description: *const c_char,
1134    /// When it happened, if the entry says.
1135    pub time: *const asdf_time_t,
1136    /// A null-terminated array of the software involved.
1137    pub software: *mut *const asdf_software_t,
1138}
1139
1140impl asdf_history_entry_t {
1141    fn zeroed() -> Self {
1142        Self {
1143            description: core::ptr::null(),
1144            time: core::ptr::null(),
1145            software: core::ptr::null_mut(),
1146        }
1147    }
1148}
1149
1150/// Read a null-terminated software array from a mapping's `software` key.
1151///
1152/// The schema allows either one object or a sequence of them.
1153fn read_software_list(
1154    doc: &Document,
1155    node: NodeId,
1156    file: *mut AsdfFile,
1157) -> *mut *const asdf_software_t {
1158    let Some(entry) = doc.mapping_get(node, "software") else {
1159        return core::ptr::null_mut();
1160    };
1161
1162    let nodes: Vec<NodeId> = match doc.sequence_items(entry) {
1163        Some(items) => items.to_vec(),
1164        // A single object rather than a list.
1165        None => vec![entry],
1166    };
1167
1168    let mut list: Vec<*const asdf_software_t> = Vec::with_capacity(nodes.len() + 1);
1169    for item in nodes {
1170        let raw = Box::into_raw(Box::new(asdf_software_t::zeroed()));
1171        if software_deserialize(doc, item, file, raw) == AsdfValueErr::Ok {
1172            list.push(raw.cast_const());
1173        } else {
1174            drop(unsafe { Box::from_raw(raw) });
1175        }
1176    }
1177    if list.is_empty() {
1178        return core::ptr::null_mut();
1179    }
1180    list.push(core::ptr::null());
1181    list.shrink_to_fit();
1182    Box::into_raw(list.into_boxed_slice()).cast::<*const asdf_software_t>()
1183}
1184
1185/// Free a software array produced by [`read_software_list`].
1186unsafe fn free_software_list(list: *mut *const asdf_software_t) {
1187    if list.is_null() {
1188        return;
1189    }
1190    let mut count = 0isize;
1191    while !unsafe { *list.offset(count) }.is_null() {
1192        unsafe { asdf_software_destroy((*list.offset(count)).cast_mut()) };
1193        count += 1;
1194    }
1195    // The array itself was a boxed slice, including its null terminator.
1196    let slice = core::ptr::slice_from_raw_parts_mut(list, count as usize + 1);
1197    drop(unsafe { Box::from_raw(slice) });
1198}
1199
1200fn history_entry_deserialize(
1201    doc: &Document,
1202    node: NodeId,
1203    file: *mut AsdfFile,
1204    out: *mut asdf_history_entry_t,
1205) -> AsdfValueErr {
1206    let description = string_field(doc, node, "description");
1207
1208    let time = doc
1209        .mapping_get(node, "time")
1210        .map(|id| {
1211            let raw = Box::into_raw(Box::new(asdf_time_t::zeroed()));
1212            if time_deserialize(doc, id, file, raw) == AsdfValueErr::Ok {
1213                raw.cast_const()
1214            } else {
1215                drop(unsafe { Box::from_raw(raw) });
1216                core::ptr::null()
1217            }
1218        })
1219        .unwrap_or(core::ptr::null());
1220
1221    unsafe {
1222        (*out).description = description;
1223        (*out).time = time;
1224        (*out).software = read_software_list(doc, node, file);
1225    }
1226    AsdfValueErr::Ok
1227}
1228
1229fn history_entry_serialize(doc: &mut Document, obj: &asdf_history_entry_t) -> Option<NodeId> {
1230    let mut pairs = Vec::new();
1231
1232    if !obj.description.is_null() {
1233        let text = unsafe { CStr::from_ptr(obj.description) }.to_string_lossy().into_owned();
1234        let key = doc.add_scalar("description");
1235        let value = doc.add_scalar_styled(text, asdf_core::yaml::ScalarStyle::SingleQuoted);
1236        pairs.push((key, value));
1237    }
1238    if !obj.time.is_null()
1239        && let Some(node) = time_serialize(doc, unsafe { &*obj.time })
1240    {
1241        doc.node_mut(node).tag = Some(Tag::parse(TIME_TAG));
1242        let key = doc.add_scalar("time");
1243        pairs.push((key, node));
1244    }
1245    if !obj.software.is_null() {
1246        let mut items = Vec::new();
1247        let mut index = 0isize;
1248        while !unsafe { *obj.software.offset(index) }.is_null() {
1249            let entry = unsafe { *obj.software.offset(index) };
1250            if let Some(node) = software_serialize(doc, unsafe { &*entry }) {
1251                doc.node_mut(node).tag = Some(Tag::parse(SOFTWARE_TAG));
1252                items.push(node);
1253            }
1254            index += 1;
1255        }
1256        if !items.is_empty() {
1257            let list = doc.add_sequence(items);
1258            let key = doc.add_scalar("software");
1259            pairs.push((key, list));
1260        }
1261    }
1262
1263    (!pairs.is_empty()).then(|| doc.add_mapping(pairs))
1264}
1265
1266unsafe fn history_entry_deinit(obj: *mut asdf_history_entry_t) {
1267    let entry = unsafe { &mut *obj };
1268    unsafe { free_c_string(entry.description) };
1269    if !entry.time.is_null() {
1270        unsafe { asdf_time_destroy(entry.time.cast_mut()) };
1271    }
1272    unsafe { free_software_list(entry.software) };
1273    *entry = asdf_history_entry_t::zeroed();
1274}
1275
1276unsafe fn history_entry_copy(src: &asdf_history_entry_t, dst: *mut asdf_history_entry_t) -> bool {
1277    let out = unsafe { &mut *dst };
1278    out.description = unsafe { clone_c_string(src.description) };
1279    out.time = if src.time.is_null() {
1280        core::ptr::null()
1281    } else {
1282        unsafe { asdf_time_copy(core::ptr::null_mut(), src.time) }.cast_const()
1283    };
1284    out.software = if src.software.is_null() {
1285        core::ptr::null_mut()
1286    } else {
1287        unsafe { asdf_software_array_copy(core::ptr::null_mut(), src.software) }
1288            .cast::<*const asdf_software_t>()
1289    };
1290    true
1291}
1292
1293declare_extension! {
1294    name: history_entry,
1295    ty: asdf_history_entry_t,
1296    tag: HISTORY_ENTRY_TAG,
1297    deserialize: history_entry_deserialize,
1298    serialize: history_entry_serialize,
1299    deinit: history_entry_deinit,
1300    copy: history_entry_copy,
1301    is_fn: asdf_is_history_entry,
1302    value_is_fn: asdf_value_is_history_entry,
1303    value_as_fn: asdf_value_as_history_entry,
1304    value_of_fn: asdf_value_of_history_entry,
1305    get_fn: asdf_get_history_entry,
1306    set_fn: asdf_set_history_entry,
1307    copy_fn: asdf_history_entry_copy,
1308    copy_into_fn: asdf_history_entry_copy_into,
1309    array_copy_fn: asdf_history_entry_array_copy,
1310    deinit_fn: asdf_history_entry_deinit,
1311    destroy_fn: asdf_history_entry_destroy,
1312    // The tag list upstream's `ASDF_REGISTER_EXTENSION` declares.
1313    tags: &[c"tag:stsci.edu:asdf/core/history_entry-1.0.0"],
1314    ext_build_fn: build_history_entry_extension,
1315    ext_deserialize_fn: history_entry_ext_deserialize,
1316    ext_serialize_fn: history_entry_ext_serialize,
1317    ext_copy_fn: history_entry_ext_copy,
1318    ext_deinit_fn: history_entry_ext_deinit,
1319}
1320
1321/// Append a history entry to the file.
1322///
1323/// # Safety
1324/// `file` must be a file handle open for writing and `description` a valid
1325/// NUL-terminated string.
1326#[unsafe(no_mangle)]
1327pub unsafe extern "C" fn asdf_history_entry_add(
1328    file: *mut AsdfFile,
1329    description: *const c_char,
1330) -> c_int {
1331    guard("asdf_history_entry_add", -1, || {
1332        if file.is_null() || description.is_null() {
1333            return -1;
1334        }
1335        let entry = asdf_history_entry_t {
1336            description,
1337            time: core::ptr::null(),
1338            software: core::ptr::null_mut(),
1339        };
1340        let value = unsafe { asdf_value_of_history_entry(file, &entry) };
1341        if value.is_null() {
1342            return -1;
1343        }
1344
1345        // Entries accumulate under history/entries, which is created on the
1346        // first call.
1347        let handle = unsafe { &mut *file };
1348        let Some(node) = crate::file_ffi::value_node(value) else {
1349            unsafe { crate::file_ffi::asdf_value_destroy(value) };
1350            return -1;
1351        };
1352        let Some(doc) = handle.document_for_values() else {
1353            unsafe { crate::file_ffi::asdf_value_destroy(value) };
1354            return -1;
1355        };
1356
1357        let existing = doc.lookup_str("history/entries");
1358        let list = match existing {
1359            Some(list) if doc.resolved(list).is_sequence() => doc.resolve(list),
1360            _ => {
1361                let fresh = doc.add(asdf_core::yaml::Node::sequence());
1362                if doc.insert_at_str("history/entries", fresh).is_err() {
1363                    unsafe { crate::file_ffi::asdf_value_destroy(value) };
1364                    return -1;
1365                }
1366                fresh
1367            }
1368        };
1369        if let asdf_core::yaml::NodeData::Sequence { items, .. } = &mut doc.node_mut(list).data {
1370            items.push(node);
1371        }
1372
1373        unsafe { crate::file_ffi::asdf_value_destroy(value) };
1374        0
1375    })
1376}
1377
1378#[cfg(test)]
1379mod history_tests {
1380    use super::*;
1381    use crate::file_ffi::{asdf_close, asdf_open_mem_ex, asdf_write_to_mem};
1382    use crate::time_ffi::asdf_time_t;
1383
1384    struct Handle(*mut AsdfFile);
1385    impl Drop for Handle {
1386        fn drop(&mut self) {
1387            unsafe { asdf_close(self.0) };
1388        }
1389    }
1390
1391    fn open(tree: &str) -> Handle {
1392        let mut buf = Vec::new();
1393        buf.extend_from_slice(b"#ASDF 1.0.0\n#ASDF_STANDARD 1.6.0\n");
1394        buf.extend_from_slice(b"%YAML 1.1\n%TAG ! tag:stsci.edu:asdf/\n--- !core/asdf-1.1.0\n");
1395        buf.extend_from_slice(tree.as_bytes());
1396        buf.extend_from_slice(b"...\n");
1397        let f = unsafe { asdf_open_mem_ex(buf.as_ptr().cast(), buf.len(), core::ptr::null_mut()) };
1398        assert!(!f.is_null());
1399        Handle(f)
1400    }
1401
1402    #[test]
1403    fn reads_a_history_entry() {
1404        let h = open(
1405            "entry: !core/history_entry-1.0.0\n  \
1406             description: 'reprocessed with a new flat'\n  \
1407             time: !time/time-1.4.0 '2026-09-04T12:00:00'\n  \
1408             software:\n  - !core/software-1.0.0 {name: mypipeline, version: 1.2.3}\n",
1409        );
1410
1411        let path = c"entry";
1412        assert!(unsafe { asdf_is_history_entry(h.0, path.as_ptr()) });
1413
1414        let mut entry: *mut asdf_history_entry_t = core::ptr::null_mut();
1415        assert_eq!(
1416            unsafe { asdf_get_history_entry(h.0, path.as_ptr(), &mut entry) },
1417            AsdfValueErr::Ok
1418        );
1419        let view = unsafe { &*entry };
1420        assert_eq!(
1421            unsafe { CStr::from_ptr(view.description) }.to_str().unwrap(),
1422            "reprocessed with a new flat"
1423        );
1424
1425        // The nested time, decoded.
1426        assert!(!view.time.is_null());
1427        let time = unsafe { &*view.time };
1428        assert_eq!(time.info.tm.tm_year, 2026 - 1900);
1429        assert_eq!(time.info.tm.tm_mday, 4);
1430
1431        // The software list, null-terminated.
1432        assert!(!view.software.is_null());
1433        let first = unsafe { *view.software };
1434        assert!(!first.is_null());
1435        assert_eq!(unsafe { CStr::from_ptr((*first).name) }.to_str().unwrap(), "mypipeline");
1436        assert!(unsafe { *view.software.offset(1) }.is_null(), "list must be terminated");
1437
1438        unsafe { asdf_history_entry_destroy(entry) };
1439    }
1440
1441    #[test]
1442    fn a_single_software_object_is_accepted() {
1443        // The schema allows one object where a list would also do.
1444        let h = open(
1445            "entry: !core/history_entry-1.0.0\n  description: 'x'\n  \
1446             software: !core/software-1.0.0 {name: solo, version: 0.1.0}\n",
1447        );
1448        let mut entry: *mut asdf_history_entry_t = core::ptr::null_mut();
1449        unsafe { asdf_get_history_entry(h.0, c"entry".as_ptr(), &mut entry) };
1450        let view = unsafe { &*entry };
1451        assert!(!view.software.is_null());
1452        let first = unsafe { *view.software };
1453        assert_eq!(unsafe { CStr::from_ptr((*first).name) }.to_str().unwrap(), "solo");
1454        unsafe { asdf_history_entry_destroy(entry) };
1455    }
1456
1457    #[test]
1458    fn history_entries_can_be_added_and_read_back() {
1459        let f = unsafe { asdf_open_mem_ex(core::ptr::null(), 0, core::ptr::null_mut()) };
1460        let h = Handle(f);
1461
1462        assert_eq!(unsafe { asdf_history_entry_add(h.0, c"first change".as_ptr()) }, 0);
1463        assert_eq!(unsafe { asdf_history_entry_add(h.0, c"second change".as_ptr()) }, 0);
1464
1465        let mut buf: *mut core::ffi::c_void = core::ptr::null_mut();
1466        let mut size = 0usize;
1467        assert_eq!(unsafe { asdf_write_to_mem(h.0, &mut buf, &mut size) }, 0);
1468
1469        let reopened = unsafe { asdf_open_mem_ex(buf, size, core::ptr::null_mut()) };
1470        let r = Handle(reopened);
1471
1472        // Both entries must be there, in order.
1473        for (index, expected) in [(0, "first change"), (1, "second change")] {
1474            let path = CString::new(format!("history/entries/{index}")).unwrap();
1475            let mut entry: *mut asdf_history_entry_t = core::ptr::null_mut();
1476            assert_eq!(
1477                unsafe { asdf_get_history_entry(r.0, path.as_ptr(), &mut entry) },
1478                AsdfValueErr::Ok,
1479                "entry {index}"
1480            );
1481            assert_eq!(unsafe { CStr::from_ptr((*entry).description) }.to_str().unwrap(), expected);
1482            unsafe { asdf_history_entry_destroy(entry) };
1483        }
1484
1485        unsafe { libc::free(buf) };
1486    }
1487
1488    #[test]
1489    fn a_history_entry_round_trips_through_a_written_file() {
1490        let f = unsafe { asdf_open_mem_ex(core::ptr::null(), 0, core::ptr::null_mut()) };
1491        let h = Handle(f);
1492
1493        let value = CString::new("2026-09-04T12:00:00").unwrap();
1494        let time = asdf_time_t { value: value.as_ptr().cast_mut(), ..asdf_time_t::zeroed() };
1495        let entry = asdf_history_entry_t {
1496            description: c"a described change".as_ptr(),
1497            time: &time,
1498            software: core::ptr::null_mut(),
1499        };
1500
1501        assert_eq!(
1502            unsafe { asdf_set_history_entry(h.0, c"note".as_ptr(), &entry) },
1503            AsdfValueErr::Ok
1504        );
1505
1506        let mut buf: *mut core::ffi::c_void = core::ptr::null_mut();
1507        let mut size = 0usize;
1508        unsafe { asdf_write_to_mem(h.0, &mut buf, &mut size) };
1509        let reopened = unsafe { asdf_open_mem_ex(buf, size, core::ptr::null_mut()) };
1510        let r = Handle(reopened);
1511
1512        let mut read_back: *mut asdf_history_entry_t = core::ptr::null_mut();
1513        assert_eq!(
1514            unsafe { asdf_get_history_entry(r.0, c"note".as_ptr(), &mut read_back) },
1515            AsdfValueErr::Ok
1516        );
1517        let view = unsafe { &*read_back };
1518        assert_eq!(
1519            unsafe { CStr::from_ptr(view.description) }.to_str().unwrap(),
1520            "a described change"
1521        );
1522        assert!(!view.time.is_null(), "the nested time must survive");
1523
1524        unsafe { asdf_history_entry_destroy(read_back) };
1525        unsafe { libc::free(buf) };
1526    }
1527
1528    #[test]
1529    fn a_time_value_round_trips() {
1530        let h = open("t: !time/time-1.4.0 '2026-09-04T12:34:56'\n");
1531        assert!(unsafe { asdf_is_time(h.0, c"t".as_ptr()) });
1532
1533        let mut time: *mut asdf_time_t = core::ptr::null_mut();
1534        assert_eq!(unsafe { asdf_get_time(h.0, c"t".as_ptr(), &mut time) }, AsdfValueErr::Ok);
1535        let view = unsafe { &*time };
1536        assert_eq!(unsafe { CStr::from_ptr(view.value) }.to_str().unwrap(), "2026-09-04T12:34:56");
1537        assert_eq!(view.info.tm.tm_hour, 12);
1538
1539        // The copy must be independent of the original.
1540        let copy = unsafe { asdf_time_copy(h.0, time) };
1541        assert!(!copy.is_null());
1542        unsafe { asdf_time_destroy(time) };
1543        assert_eq!(
1544            unsafe { CStr::from_ptr((*copy).value) }.to_str().unwrap(),
1545            "2026-09-04T12:34:56"
1546        );
1547        unsafe { asdf_time_destroy(copy) };
1548    }
1549
1550    #[test]
1551    fn deinit_is_safe_on_zeroed_objects() {
1552        let mut entry = asdf_history_entry_t::zeroed();
1553        unsafe { asdf_history_entry_deinit(&mut entry) };
1554        unsafe { asdf_history_entry_deinit(&mut entry) };
1555
1556        let mut time = asdf_time_t::zeroed();
1557        unsafe { asdf_time_deinit(&mut time) };
1558        unsafe { asdf_time_deinit(core::ptr::null_mut()) };
1559    }
1560}
1561
1562// ---- core/datatype ---------------------------------------------------
1563
1564use crate::ffi::write_out;
1565use crate::ndarray_ffi::asdf_datatype_t;
1566
1567/// The tag for `core/datatype`.
1568pub const DATATYPE_TAG: &str = "tag:stsci.edu:asdf/core/datatype-1.0.0";
1569
1570impl asdf_datatype_t {
1571    /// A zeroed instance.
1572    pub(crate) fn zeroed() -> Self {
1573        Self {
1574            type_: 0,
1575            size: 0,
1576            name: core::ptr::null(),
1577            byteorder: 0,
1578            ndim: 0,
1579            shape: core::ptr::null(),
1580            nfields: 0,
1581            fields: core::ptr::null(),
1582        }
1583    }
1584}
1585
1586fn datatype_deserialize(
1587    doc: &Document,
1588    node: NodeId,
1589    _file: *mut AsdfFile,
1590    out: *mut asdf_datatype_t,
1591) -> AsdfValueErr {
1592    use asdf_core::core::datatype::Datatype;
1593
1594    let Ok(parsed) = Datatype::parse(doc, node) else {
1595        return AsdfValueErr::ParseFailure;
1596    };
1597
1598    // A datatype read on its own has no enclosing ndarray to inherit a byte
1599    // order from, and the standard does not say what one means alone. Both
1600    // libasdf and Python asdf take it as little-endian; see
1601    // https://github.com/asdf-format/asdf-standard/issues/501
1602    let default_order = asdf_core::core::datatype::ByteOrder::Little;
1603    let order_of = |order: asdf_core::core::datatype::ByteOrder| {
1604        if order == asdf_core::core::datatype::ByteOrder::Default { default_order } else { order }
1605    };
1606
1607    // Field descriptors and their names are leaked into owned allocations
1608    // that `deinit` reclaims, so the pointers stay valid for the object's
1609    // life.
1610    let mut fields: Vec<asdf_datatype_t> = Vec::with_capacity(parsed.fields.len());
1611    for field in &parsed.fields {
1612        let name = field.name.as_deref().map(to_c_string).unwrap_or(core::ptr::null());
1613        let shape: Vec<u64> = field.datatype.shape.clone();
1614        let (shape_ptr, ndim) = if shape.is_empty() {
1615            (core::ptr::null(), 0)
1616        } else {
1617            let boxed = shape.into_boxed_slice();
1618            let len = boxed.len() as u32;
1619            (Box::into_raw(boxed).cast::<u64>().cast_const(), len)
1620        };
1621        fields.push(asdf_datatype_t {
1622            type_: field.datatype.scalar as i32,
1623            size: field.datatype.item_size(),
1624            name,
1625            byteorder: order_of(field.datatype.byteorder) as i32,
1626            ndim,
1627            shape: shape_ptr,
1628            nfields: 0,
1629            fields: core::ptr::null(),
1630        });
1631    }
1632
1633    let (fields_ptr, nfields) = if fields.is_empty() {
1634        (core::ptr::null(), 0)
1635    } else {
1636        let len = fields.len() as u32;
1637        (Box::into_raw(fields.into_boxed_slice()).cast::<asdf_datatype_t>().cast_const(), len)
1638    };
1639
1640    unsafe {
1641        (*out).type_ = parsed.scalar as i32;
1642        (*out).size = parsed.item_size();
1643        (*out).name = core::ptr::null();
1644        (*out).byteorder = order_of(parsed.byteorder) as i32;
1645        (*out).ndim = 0;
1646        (*out).shape = core::ptr::null();
1647        (*out).nfields = nfields;
1648        (*out).fields = fields_ptr;
1649    }
1650    AsdfValueErr::Ok
1651}
1652
1653/// Whether a datatype is a plain scalar that needs no mapping around it.
1654///
1655/// Mirrors upstream's `asdf_datatype_is_simple_scalar`: not structured, no
1656/// name, no shape, no fields, and a byte order that need not be stated.
1657fn is_simple_scalar(obj: &asdf_datatype_t) -> bool {
1658    use asdf_core::core::datatype::ByteOrder;
1659    let scalar = crate::ndarray_ffi::scalar_from_abi_public(obj.type_);
1660    scalar != asdf_core::core::datatype::ScalarType::Structured
1661        && (obj.byteorder == 0 || obj.byteorder == ByteOrder::Little as i32)
1662        && obj.name.is_null()
1663        && obj.ndim == 0
1664        && obj.nfields == 0
1665}
1666
1667/// Render a scalar datatype: its name, or `[kind, length]` for a string.
1668fn datatype_serialize_scalar(doc: &mut Document, obj: &asdf_datatype_t) -> Option<NodeId> {
1669    use asdf_core::core::datatype::ScalarType;
1670
1671    let scalar = crate::ndarray_ffi::scalar_from_abi_public(obj.type_);
1672    if scalar.is_string() {
1673        // A string type is a [kind, length] pair, sized in characters.
1674        let characters = obj.size / scalar.bytes_per_char().max(1);
1675        let kind = doc.add_scalar(scalar.name());
1676        let length = doc.add_scalar(characters.to_string());
1677        let seq = doc.add_sequence(vec![kind, length]);
1678        if let asdf_core::yaml::NodeData::Sequence { style, .. } = &mut doc.node_mut(seq).data {
1679            *style = asdf_core::yaml::CollectionStyle::Flow;
1680        }
1681        return Some(seq);
1682    }
1683    (scalar != ScalarType::Unknown).then(|| doc.add_scalar(scalar.name()))
1684}
1685
1686/// Render one field of a compound datatype as a mapping.
1687///
1688/// A field carries what a bare scalar cannot: its name, its own byte order,
1689/// and a sub-array shape.
1690fn datatype_serialize_field(doc: &mut Document, field: &asdf_datatype_t) -> Option<NodeId> {
1691    use asdf_core::core::datatype::ScalarType;
1692
1693    let scalar = crate::ndarray_ffi::scalar_from_abi_public(field.type_);
1694    let mut pairs = Vec::new();
1695
1696    if !field.name.is_null() {
1697        let text = unsafe { CStr::from_ptr(field.name) }.to_string_lossy().into_owned();
1698        let key = doc.add_scalar("name");
1699        let value = doc.add_scalar_styled(text, asdf_core::yaml::ScalarStyle::Plain);
1700        pairs.push((key, value));
1701    }
1702
1703    let inner = if scalar == ScalarType::Structured {
1704        datatype_serialize_impl(doc, field, false)?
1705    } else {
1706        datatype_serialize_scalar(doc, field)?
1707    };
1708    let key = doc.add_scalar("datatype");
1709    pairs.push((key, inner));
1710
1711    if field.byteorder != 0
1712        && let Some(order) = byteorder_name(field.byteorder)
1713    {
1714        let key = doc.add_scalar("byteorder");
1715        let value = doc.add_scalar(order);
1716        pairs.push((key, value));
1717    }
1718
1719    if field.ndim > 0 && !field.shape.is_null() {
1720        let dims = unsafe { core::slice::from_raw_parts(field.shape, field.ndim as usize) };
1721        let items: Vec<NodeId> = dims.iter().map(|d| doc.add_scalar(d.to_string())).collect();
1722        let seq = doc.add_sequence(items);
1723        if let asdf_core::yaml::NodeData::Sequence { style, .. } = &mut doc.node_mut(seq).data {
1724            *style = asdf_core::yaml::CollectionStyle::Flow;
1725        }
1726        let key = doc.add_scalar("shape");
1727        pairs.push((key, seq));
1728    }
1729
1730    let node = doc.add_mapping(pairs);
1731    // Python asdf writes a plain non-string scalar field inline and anything
1732    // richer in block style; upstream reproduces that, so we do too.
1733    if scalar != ScalarType::Structured
1734        && !scalar.is_string()
1735        && field.ndim == 0
1736        && let asdf_core::yaml::NodeData::Mapping { style, .. } = &mut doc.node_mut(node).data
1737    {
1738        *style = asdf_core::yaml::CollectionStyle::Flow;
1739    }
1740    Some(node)
1741}
1742
1743/// The schema's name for a byte order discriminant.
1744fn byteorder_name(byteorder: i32) -> Option<&'static str> {
1745    use asdf_core::core::datatype::ByteOrder;
1746    if byteorder == ByteOrder::Little as i32 {
1747        Some("little")
1748    } else if byteorder == ByteOrder::Big as i32 {
1749        Some("big")
1750    } else {
1751        None
1752    }
1753}
1754
1755/// Render a datatype, as a field of a compound type or on its own.
1756fn datatype_serialize_impl(
1757    doc: &mut Document,
1758    obj: &asdf_datatype_t,
1759    is_field: bool,
1760) -> Option<NodeId> {
1761    use asdf_core::core::datatype::ScalarType;
1762
1763    let scalar = crate::ndarray_ffi::scalar_from_abi_public(obj.type_);
1764
1765    if is_simple_scalar(obj) {
1766        return datatype_serialize_scalar(doc, obj);
1767    }
1768    if !is_field && scalar != ScalarType::Structured && obj.ndim == 0 {
1769        // A top-level scalar is written as its name even when its byte order
1770        // is not the default: the order belongs to the enclosing ndarray's
1771        // own `byteorder`, not repeated here. As a *field* it would need the
1772        // mapping form, which carries the per-field order.
1773        return datatype_serialize_scalar(doc, obj);
1774    }
1775    if is_field {
1776        return datatype_serialize_field(doc, obj);
1777    }
1778    if scalar == ScalarType::Structured {
1779        let fields = if obj.nfields > 0 && !obj.fields.is_null() {
1780            unsafe { core::slice::from_raw_parts(obj.fields, obj.nfields as usize) }
1781        } else {
1782            &[]
1783        };
1784        let items: Vec<NodeId> = fields
1785            .iter()
1786            .map(|field| datatype_serialize_impl(doc, field, true))
1787            .collect::<Option<Vec<_>>>()?;
1788        return Some(doc.add_sequence(items));
1789    }
1790    None
1791}
1792
1793fn datatype_serialize(doc: &mut Document, obj: &asdf_datatype_t) -> Option<NodeId> {
1794    datatype_serialize_impl(doc, obj, false)
1795}
1796
1797unsafe fn datatype_deinit(obj: *mut asdf_datatype_t) {
1798    let datatype = unsafe { &mut *obj };
1799    unsafe { datatype_free_storage(datatype) };
1800    *datatype = asdf_datatype_t::zeroed();
1801}
1802
1803/// Release everything a datatype owns, without zeroing it.
1804///
1805/// Recursive, because a field may itself be structured; the fields array is
1806/// freed after its members, and each member's name and shape after that.
1807///
1808/// # Safety
1809/// `datatype` must own its `name`, `shape` and `fields`, as one produced by
1810/// the deserializer or by [`datatype_copy`] does.
1811unsafe fn datatype_free_storage(datatype: &mut asdf_datatype_t) {
1812    if !datatype.fields.is_null() && datatype.nfields > 0 {
1813        let count = datatype.nfields as usize;
1814        let slice = core::ptr::slice_from_raw_parts_mut(datatype.fields.cast_mut(), count);
1815        for index in 0..count {
1816            let field = unsafe { &mut *datatype.fields.cast_mut().add(index) };
1817            unsafe { datatype_free_storage(field) };
1818        }
1819        drop(unsafe { Box::from_raw(slice) });
1820        datatype.fields = core::ptr::null();
1821        datatype.nfields = 0;
1822    }
1823    if !datatype.shape.is_null() && datatype.ndim > 0 {
1824        let shape =
1825            core::ptr::slice_from_raw_parts_mut(datatype.shape.cast_mut(), datatype.ndim as usize);
1826        drop(unsafe { Box::from_raw(shape) });
1827        datatype.shape = core::ptr::null();
1828        datatype.ndim = 0;
1829    }
1830    unsafe { free_c_string(datatype.name) };
1831    datatype.name = core::ptr::null();
1832}
1833
1834unsafe fn datatype_copy(src: &asdf_datatype_t, dst: *mut asdf_datatype_t) -> bool {
1835    let out = unsafe { &mut *dst };
1836    out.type_ = src.type_;
1837    out.size = src.size;
1838    out.byteorder = src.byteorder;
1839    out.name = unsafe { clone_c_string(src.name) };
1840
1841    // A field's sub-array shape is its own storage, so the copy gets one
1842    // too: a shallow copy would leave two owners of the same allocation.
1843    if src.ndim > 0 && !src.shape.is_null() {
1844        let dims = unsafe { core::slice::from_raw_parts(src.shape, src.ndim as usize) };
1845        out.ndim = src.ndim;
1846        out.shape = Box::into_raw(dims.to_vec().into_boxed_slice()).cast::<u64>().cast_const();
1847    } else {
1848        out.ndim = 0;
1849        out.shape = core::ptr::null();
1850    }
1851
1852    if src.nfields == 0 || src.fields.is_null() {
1853        out.nfields = 0;
1854        out.fields = core::ptr::null();
1855        return true;
1856    }
1857
1858    let source = unsafe { core::slice::from_raw_parts(src.fields, src.nfields as usize) };
1859    let mut copies: Vec<asdf_datatype_t> = Vec::with_capacity(source.len());
1860    for field in source {
1861        let mut copy = asdf_datatype_t::zeroed();
1862        // Nested fields are one level deep in practice; a deeper nesting
1863        // recurses through this same path.
1864        if !unsafe { datatype_copy(field, &mut copy) } {
1865            return false;
1866        }
1867        copies.push(copy);
1868    }
1869    out.nfields = src.nfields;
1870    out.fields = Box::into_raw(copies.into_boxed_slice()).cast::<asdf_datatype_t>().cast_const();
1871    true
1872}
1873
1874declare_extension! {
1875    name: datatype,
1876    ty: asdf_datatype_t,
1877    tag: DATATYPE_TAG,
1878    deserialize: datatype_deserialize,
1879    serialize: datatype_serialize,
1880    deinit: datatype_deinit,
1881    copy: datatype_copy,
1882    is_fn: asdf_is_datatype,
1883    value_is_fn: asdf_value_is_datatype,
1884    value_as_fn: asdf_value_as_datatype,
1885    value_of_fn: asdf_value_of_datatype,
1886    get_fn: asdf_get_datatype,
1887    set_fn: asdf_set_datatype,
1888    copy_fn: asdf_datatype_copy,
1889    copy_into_fn: asdf_datatype_copy_into,
1890    array_copy_fn: asdf_datatype_array_copy,
1891    deinit_fn: asdf_datatype_deinit,
1892    destroy_fn: asdf_datatype_destroy,
1893    // The tag list upstream's `ASDF_REGISTER_EXTENSION` declares.
1894    tags: &[c"tag:stsci.edu:asdf/core/datatype-1.0.0"],
1895    ext_build_fn: build_datatype_extension,
1896    ext_deserialize_fn: datatype_ext_deserialize,
1897    ext_serialize_fn: datatype_ext_serialize,
1898    ext_copy_fn: datatype_ext_copy,
1899    ext_deinit_fn: datatype_ext_deinit,
1900}
1901
1902// ---- core/asdf (the tree's own metadata) -----------------------------
1903
1904/// The tag for the tree root, `core/asdf`.
1905pub const META_TAG: &str = "tag:stsci.edu:asdf/core/asdf-1.1.0";
1906
1907/// Mirror of `asdf_meta_history_t`.
1908#[repr(C)]
1909#[derive(Debug)]
1910pub struct asdf_meta_history_t {
1911    /// A null-terminated array of the extensions used.
1912    pub extensions: *mut *const asdf_extension_metadata_t,
1913    /// A null-terminated array of history entries.
1914    pub entries: *mut *const asdf_history_entry_t,
1915}
1916
1917/// Mirror of `asdf_meta_t`, the `core/asdf` tree root.
1918#[repr(C)]
1919#[derive(Debug)]
1920pub struct asdf_meta_t {
1921    /// The software that wrote the file.
1922    pub asdf_library: *mut asdf_software_t,
1923    /// The file's history.
1924    pub history: asdf_meta_history_t,
1925}
1926
1927impl asdf_meta_t {
1928    fn zeroed() -> Self {
1929        Self {
1930            asdf_library: core::ptr::null_mut(),
1931            history: asdf_meta_history_t {
1932                extensions: core::ptr::null_mut(),
1933                entries: core::ptr::null_mut(),
1934            },
1935        }
1936    }
1937}
1938
1939/// Read a null-terminated array of objects from a sequence.
1940fn read_list<T>(
1941    doc: &Document,
1942    node: Option<NodeId>,
1943    file: *mut AsdfFile,
1944    zeroed: fn() -> T,
1945    deserialize: fn(&Document, NodeId, *mut AsdfFile, *mut T) -> AsdfValueErr,
1946) -> *mut *const T {
1947    let Some(node) = node else {
1948        return core::ptr::null_mut();
1949    };
1950    let items: Vec<NodeId> = match doc.sequence_items(node) {
1951        Some(items) => items.to_vec(),
1952        None => vec![node],
1953    };
1954
1955    let mut list: Vec<*const T> = Vec::with_capacity(items.len() + 1);
1956    for item in items {
1957        let raw = Box::into_raw(Box::new(zeroed()));
1958        if deserialize(doc, item, file, raw) == AsdfValueErr::Ok {
1959            list.push(raw.cast_const());
1960        } else {
1961            drop(unsafe { Box::from_raw(raw) });
1962        }
1963    }
1964    if list.is_empty() {
1965        return core::ptr::null_mut();
1966    }
1967    list.push(core::ptr::null());
1968    Box::into_raw(list.into_boxed_slice()).cast::<*const T>()
1969}
1970
1971/// Free a list produced by [`read_list`].
1972///
1973/// The destructor is an `extern "C"` function, since these are the same
1974/// generated `destroy` entry points C callers use.
1975unsafe fn free_list<T>(list: *mut *const T, destroy: unsafe extern "C" fn(*mut T)) {
1976    if list.is_null() {
1977        return;
1978    }
1979    let mut count = 0isize;
1980    while !unsafe { *list.offset(count) }.is_null() {
1981        unsafe { destroy((*list.offset(count)).cast_mut()) };
1982        count += 1;
1983    }
1984    let slice = core::ptr::slice_from_raw_parts_mut(list, count as usize + 1);
1985    drop(unsafe { Box::from_raw(slice) });
1986}
1987
1988fn meta_deserialize(
1989    doc: &Document,
1990    node: NodeId,
1991    file: *mut AsdfFile,
1992    out: *mut asdf_meta_t,
1993) -> AsdfValueErr {
1994    let library = doc
1995        .mapping_get(node, "asdf_library")
1996        .map(|id| {
1997            let raw = Box::into_raw(Box::new(asdf_software_t::zeroed()));
1998            if software_deserialize(doc, id, file, raw) == AsdfValueErr::Ok {
1999                raw
2000            } else {
2001                drop(unsafe { Box::from_raw(raw) });
2002                core::ptr::null_mut()
2003            }
2004        })
2005        .unwrap_or(core::ptr::null_mut());
2006
2007    // `history` is a mapping of extensions and entries in the 1.1.0 form,
2008    // and a bare sequence of entries in the older one. Both are accepted.
2009    let history = doc.mapping_get(node, "history");
2010    let (extensions_node, entries_node) = match history {
2011        Some(history) if doc.resolved(history).is_mapping() => {
2012            (doc.mapping_get(history, "extensions"), doc.mapping_get(history, "entries"))
2013        }
2014        Some(history) => (None, Some(history)),
2015        None => (None, None),
2016    };
2017
2018    unsafe {
2019        (*out).asdf_library = library;
2020        (*out).history.extensions = read_list(
2021            doc,
2022            extensions_node,
2023            file,
2024            asdf_extension_metadata_t::zeroed,
2025            extension_metadata_deserialize,
2026        );
2027        (*out).history.entries = read_list(
2028            doc,
2029            entries_node,
2030            file,
2031            asdf_history_entry_t::zeroed,
2032            history_entry_deserialize,
2033        );
2034    }
2035    AsdfValueErr::Ok
2036}
2037
2038fn meta_serialize(doc: &mut Document, obj: &asdf_meta_t) -> Option<NodeId> {
2039    let mut pairs = Vec::new();
2040
2041    if !obj.asdf_library.is_null()
2042        && let Some(node) = software_serialize(doc, unsafe { &*obj.asdf_library })
2043    {
2044        doc.node_mut(node).tag = Some(Tag::parse(SOFTWARE_TAG));
2045        let key = doc.add_scalar("asdf_library");
2046        pairs.push((key, node));
2047    }
2048
2049    let mut history_pairs = Vec::new();
2050    if !obj.history.extensions.is_null() {
2051        let mut items = Vec::new();
2052        let mut index = 0isize;
2053        while !unsafe { *obj.history.extensions.offset(index) }.is_null() {
2054            let entry = unsafe { *obj.history.extensions.offset(index) };
2055            if let Some(node) = extension_metadata_serialize(doc, unsafe { &*entry }) {
2056                doc.node_mut(node).tag = Some(Tag::parse(EXTENSION_METADATA_TAG));
2057                items.push(node);
2058            }
2059            index += 1;
2060        }
2061        if !items.is_empty() {
2062            let list = doc.add_sequence(items);
2063            let key = doc.add_scalar("extensions");
2064            history_pairs.push((key, list));
2065        }
2066    }
2067    if !obj.history.entries.is_null() {
2068        let mut items = Vec::new();
2069        let mut index = 0isize;
2070        while !unsafe { *obj.history.entries.offset(index) }.is_null() {
2071            let entry = unsafe { *obj.history.entries.offset(index) };
2072            if let Some(node) = history_entry_serialize(doc, unsafe { &*entry }) {
2073                doc.node_mut(node).tag = Some(Tag::parse(HISTORY_ENTRY_TAG));
2074                items.push(node);
2075            }
2076            index += 1;
2077        }
2078        if !items.is_empty() {
2079            let list = doc.add_sequence(items);
2080            let key = doc.add_scalar("entries");
2081            history_pairs.push((key, list));
2082        }
2083    }
2084    if !history_pairs.is_empty() {
2085        let history = doc.add_mapping(history_pairs);
2086        let key = doc.add_scalar("history");
2087        pairs.push((key, history));
2088    }
2089
2090    Some(doc.add_mapping(pairs))
2091}
2092
2093unsafe fn meta_deinit(obj: *mut asdf_meta_t) {
2094    let meta = unsafe { &mut *obj };
2095    if !meta.asdf_library.is_null() {
2096        unsafe { asdf_software_destroy(meta.asdf_library) };
2097    }
2098    unsafe { free_list(meta.history.extensions, asdf_extension_metadata_destroy) };
2099    unsafe { free_list(meta.history.entries, asdf_history_entry_destroy) };
2100    *meta = asdf_meta_t::zeroed();
2101}
2102
2103unsafe fn meta_copy(src: &asdf_meta_t, dst: *mut asdf_meta_t) -> bool {
2104    let out = unsafe { &mut *dst };
2105    out.asdf_library = if src.asdf_library.is_null() {
2106        core::ptr::null_mut()
2107    } else {
2108        unsafe { asdf_software_copy(core::ptr::null_mut(), src.asdf_library) }
2109    };
2110    out.history.extensions = if src.history.extensions.is_null() {
2111        core::ptr::null_mut()
2112    } else {
2113        unsafe { asdf_extension_metadata_array_copy(core::ptr::null_mut(), src.history.extensions) }
2114            .cast::<*const asdf_extension_metadata_t>()
2115    };
2116    out.history.entries = if src.history.entries.is_null() {
2117        core::ptr::null_mut()
2118    } else {
2119        unsafe { asdf_history_entry_array_copy(core::ptr::null_mut(), src.history.entries) }
2120            .cast::<*const asdf_history_entry_t>()
2121    };
2122    true
2123}
2124
2125declare_extension! {
2126    name: meta,
2127    ty: asdf_meta_t,
2128    tag: META_TAG,
2129    deserialize: meta_deserialize,
2130    serialize: meta_serialize,
2131    deinit: meta_deinit,
2132    copy: meta_copy,
2133    is_fn: asdf_is_meta,
2134    value_is_fn: asdf_value_is_meta,
2135    value_as_fn: asdf_value_as_meta,
2136    value_of_fn: asdf_value_of_meta,
2137    get_fn: asdf_get_meta,
2138    set_fn: asdf_set_meta,
2139    copy_fn: asdf_meta_copy,
2140    copy_into_fn: asdf_meta_copy_into,
2141    array_copy_fn: asdf_meta_array_copy,
2142    deinit_fn: asdf_meta_deinit,
2143    destroy_fn: asdf_meta_destroy,
2144    // The tag list upstream's `ASDF_REGISTER_EXTENSION` declares.
2145    tags: &[
2146        c"tag:stsci.edu:asdf/core/asdf-1.1.0",
2147        c"tag:stsci.edu:asdf/core/asdf-1.0.0",
2148    ],
2149    ext_build_fn: build_meta_extension,
2150    ext_deserialize_fn: meta_ext_deserialize,
2151    ext_serialize_fn: meta_ext_serialize,
2152    ext_copy_fn: meta_ext_copy,
2153    ext_deinit_fn: meta_ext_deinit,
2154}
2155
2156#[cfg(test)]
2157mod meta_tests {
2158    use super::*;
2159    use crate::file_ffi::{asdf_close, asdf_open_mem_ex};
2160    use crate::ndarray_ffi::asdf_datatype_t;
2161
2162    struct Handle(*mut AsdfFile);
2163    impl Drop for Handle {
2164        fn drop(&mut self) {
2165            unsafe { asdf_close(self.0) };
2166        }
2167    }
2168
2169    /// A tree shaped like a real file's metadata.
2170    fn open_full() -> Handle {
2171        let mut buf = Vec::new();
2172        buf.extend_from_slice(b"#ASDF 1.0.0\n#ASDF_STANDARD 1.6.0\n");
2173        buf.extend_from_slice(b"%YAML 1.1\n%TAG ! tag:stsci.edu:asdf/\n--- !core/asdf-1.1.0\n");
2174        buf.extend_from_slice(
2175            b"asdf_library: !core/software-1.0.0 {name: asdf, version: 4.1.0}\n\
2176              history:\n  extensions:\n  - !core/extension_metadata-1.0.0\n    \
2177              extension_class: asdf.extension._manifest.ManifestExtension\n    \
2178              software: !core/software-1.0.0 {name: asdf_standard, version: 1.1.1}\n  \
2179              entries:\n  - !core/history_entry-1.0.0 {description: 'made it'}\n\
2180              dt: !core/datatype-1.0.0 float64\n\
2181              compound: !core/datatype-1.0.0\n  - name: x\n    datatype: float64\n  \
2182              - name: y\n    datatype: int32\n",
2183        );
2184        buf.extend_from_slice(b"...\n");
2185        let f = unsafe { asdf_open_mem_ex(buf.as_ptr().cast(), buf.len(), core::ptr::null_mut()) };
2186        assert!(!f.is_null());
2187        Handle(f)
2188    }
2189
2190    #[test]
2191    fn reads_the_tree_metadata() {
2192        let h = open_full();
2193        // The root itself carries the core/asdf tag.
2194        assert!(unsafe { asdf_is_meta(h.0, c"".as_ptr()) });
2195
2196        let mut meta: *mut asdf_meta_t = core::ptr::null_mut();
2197        assert_eq!(unsafe { asdf_get_meta(h.0, c"".as_ptr(), &mut meta) }, AsdfValueErr::Ok);
2198        let view = unsafe { &*meta };
2199
2200        assert!(!view.asdf_library.is_null());
2201        assert_eq!(unsafe { CStr::from_ptr((*view.asdf_library).name) }.to_str().unwrap(), "asdf");
2202
2203        // Extensions and entries both decoded, both null-terminated.
2204        assert!(!view.history.extensions.is_null());
2205        let first = unsafe { *view.history.extensions };
2206        assert_eq!(
2207            unsafe { CStr::from_ptr((*first).extension_class) }.to_str().unwrap(),
2208            "asdf.extension._manifest.ManifestExtension"
2209        );
2210        assert!(unsafe { *view.history.extensions.offset(1) }.is_null());
2211
2212        assert!(!view.history.entries.is_null());
2213        let entry = unsafe { *view.history.entries };
2214        assert_eq!(unsafe { CStr::from_ptr((*entry).description) }.to_str().unwrap(), "made it");
2215
2216        unsafe { asdf_meta_destroy(meta) };
2217    }
2218
2219    /// The older schema wrote `history` as a bare sequence of entries rather
2220    /// than a mapping. Both forms must read.
2221    #[test]
2222    fn the_legacy_history_form_is_accepted() {
2223        let mut buf = Vec::new();
2224        buf.extend_from_slice(b"#ASDF 1.0.0\n#ASDF_STANDARD 1.6.0\n");
2225        buf.extend_from_slice(b"%YAML 1.1\n%TAG ! tag:stsci.edu:asdf/\n--- !core/asdf-1.1.0\n");
2226        buf.extend_from_slice(
2227            b"history:\n- !core/history_entry-1.0.0 {description: 'old style'}\n",
2228        );
2229        buf.extend_from_slice(b"...\n");
2230        let f = unsafe { asdf_open_mem_ex(buf.as_ptr().cast(), buf.len(), core::ptr::null_mut()) };
2231        let h = Handle(f);
2232
2233        let mut meta: *mut asdf_meta_t = core::ptr::null_mut();
2234        assert_eq!(unsafe { asdf_get_meta(h.0, c"".as_ptr(), &mut meta) }, AsdfValueErr::Ok);
2235        let view = unsafe { &*meta };
2236        assert!(view.history.extensions.is_null(), "no extensions in the old form");
2237        assert!(!view.history.entries.is_null());
2238        assert_eq!(
2239            unsafe { CStr::from_ptr((**view.history.entries).description) }.to_str().unwrap(),
2240            "old style"
2241        );
2242        unsafe { asdf_meta_destroy(meta) };
2243    }
2244
2245    #[test]
2246    fn metadata_copies_are_independent() {
2247        let h = open_full();
2248        let mut meta: *mut asdf_meta_t = core::ptr::null_mut();
2249        unsafe { asdf_get_meta(h.0, c"".as_ptr(), &mut meta) };
2250
2251        let copy = unsafe { asdf_meta_copy(h.0, meta) };
2252        assert!(!copy.is_null());
2253        unsafe {
2254            assert_ne!((*copy).asdf_library, (*meta).asdf_library);
2255            assert_ne!((*copy).history.entries, (*meta).history.entries);
2256        }
2257
2258        // Freeing the original must leave the copy whole.
2259        unsafe { asdf_meta_destroy(meta) };
2260        assert_eq!(
2261            unsafe { CStr::from_ptr((*(*copy).asdf_library).name) }.to_str().unwrap(),
2262            "asdf"
2263        );
2264        unsafe { asdf_meta_destroy(copy) };
2265    }
2266
2267    #[test]
2268    fn reads_a_scalar_datatype() {
2269        let h = open_full();
2270        assert!(unsafe { asdf_is_datatype(h.0, c"dt".as_ptr()) });
2271
2272        let mut datatype: *mut asdf_datatype_t = core::ptr::null_mut();
2273        assert_eq!(
2274            unsafe { asdf_get_datatype(h.0, c"dt".as_ptr(), &mut datatype) },
2275            AsdfValueErr::Ok
2276        );
2277        let view = unsafe { &*datatype };
2278        // float64 is discriminant 11, eight bytes wide.
2279        assert_eq!(view.type_, 11);
2280        assert_eq!(view.size, 8);
2281        assert_eq!(view.nfields, 0);
2282        unsafe { asdf_datatype_destroy(datatype) };
2283    }
2284
2285    #[test]
2286    fn reads_a_compound_datatype_with_named_fields() {
2287        let h = open_full();
2288        let mut datatype: *mut asdf_datatype_t = core::ptr::null_mut();
2289        assert_eq!(
2290            unsafe { asdf_get_datatype(h.0, c"compound".as_ptr(), &mut datatype) },
2291            AsdfValueErr::Ok
2292        );
2293        let view = unsafe { &*datatype };
2294        assert_eq!(view.nfields, 2);
2295        assert!(!view.fields.is_null());
2296        // A record of float64 plus int32 is twelve bytes.
2297        assert_eq!(view.size, 12);
2298
2299        let fields = unsafe { core::slice::from_raw_parts(view.fields, 2) };
2300        assert_eq!(unsafe { CStr::from_ptr(fields[0].name) }.to_str().unwrap(), "x");
2301        assert_eq!(fields[0].size, 8);
2302        assert_eq!(unsafe { CStr::from_ptr(fields[1].name) }.to_str().unwrap(), "y");
2303        assert_eq!(fields[1].size, 4);
2304
2305        // The copy must duplicate the field array, not share it.
2306        let copy = unsafe { asdf_datatype_copy(h.0, datatype) };
2307        assert!(!copy.is_null());
2308        unsafe { assert_ne!((*copy).fields, view.fields) };
2309        unsafe { asdf_datatype_destroy(datatype) };
2310        assert_eq!(unsafe { (*copy).nfields }, 2);
2311        unsafe { asdf_datatype_destroy(copy) };
2312    }
2313
2314    #[test]
2315    fn deinit_is_safe_on_zeroed_objects() {
2316        let mut meta = asdf_meta_t::zeroed();
2317        unsafe { asdf_meta_deinit(&mut meta) };
2318        unsafe { asdf_meta_deinit(&mut meta) };
2319
2320        let mut datatype = asdf_datatype_t::zeroed();
2321        unsafe { asdf_datatype_deinit(&mut datatype) };
2322        unsafe { asdf_datatype_deinit(core::ptr::null_mut()) };
2323    }
2324}
2325
2326// ---- Registering the core schemas ------------------------------------
2327
2328/// Put the seven core-schema extensions in the process-wide registry.
2329///
2330/// `ASDF_REGISTER_EXTENSION` does this with a `__attribute__((constructor))`
2331/// per extension, so upstream's are in the registry before `main`. Rust has
2332/// no equivalent attribute, so `shim.c` carries one constructor that calls
2333/// this — which also keeps the ordering guarantee, since a third-party
2334/// extension's own constructor may run before or after ours and the registry
2335/// is const-constructed either way.
2336///
2337/// Idempotent: calling it twice registers nothing new.
2338pub fn register_core_extensions() {
2339    use core::sync::atomic::{AtomicBool, Ordering};
2340
2341    static REGISTERED: AtomicBool = AtomicBool::new(false);
2342    if REGISTERED.swap(true, Ordering::SeqCst) {
2343        return;
2344    }
2345
2346    let extensions = [
2347        build_meta_extension(),
2348        build_software_extension(),
2349        build_extension_metadata_extension(),
2350        build_history_entry_extension(),
2351        build_datatype_extension(),
2352        build_time_extension(),
2353        crate::ndarray_ffi::build_ndarray_extension(),
2354    ];
2355    for extension in extensions {
2356        // SAFETY: each was just leaked, so it outlives the process's use of
2357        // the library, which is what registration requires.
2358        unsafe { crate::extension_ffi::asdf_extension_register(extension) };
2359    }
2360}
2361
2362/// The entry point `shim.c`'s constructor calls.
2363///
2364/// # Safety
2365/// Safe to call at any time, including before `main`.
2366#[unsafe(no_mangle)]
2367pub unsafe extern "C" fn asdf_shim_register_core_extensions() {
2368    guard("asdf_shim_register_core_extensions", (), register_core_extensions);
2369}