Skip to main content

asdf/
ndarray_ffi.rs

1//! `asdf/core/ndarray.h` and `asdf/core/datatype.h`.
2//!
3//! # Why the layouts matter here
4//!
5//! `asdf_ndarray_t` and `asdf_datatype_t` are **not** opaque. Callers read
6//! `array->ndim` and `array->shape[0]` directly, and libasdf's own write
7//! example builds an `asdf_ndarray_t` as a stack literal. So both layouts are
8//! reproduced field for field, and the trailing `_reserved` pointer is where
9//! this implementation keeps the state it needs.
10
11use crate::file_ffi::file_document_mut;
12use alloc::ffi::CString;
13use core::ffi::{CStr, c_char, c_int, c_void};
14
15use asdf_core::compression::Compression;
16use asdf_core::core::datatype::{Datatype, ScalarType};
17use asdf_core::core::elements::{Element, decode_all};
18use asdf_core::core::ndarray::{Ndarray, Source};
19
20use crate::ffi::{CMallocBuf, write_out};
21use crate::panic::guard;
22use crate::types::AsdfArrayStorage;
23
24/// Error codes matching `asdf_ndarray_err_t`.
25#[derive(Clone, Copy, PartialEq, Eq, Debug)]
26#[repr(i32)]
27pub enum NdarrayErr {
28    /// Read successfully.
29    Ok = 0,
30    /// Read beyond the bounds of the array.
31    OutOfBounds,
32    /// Allocation failure.
33    Oom,
34    /// An argument was invalid.
35    Inval,
36    /// A value did not fit the requested type.
37    Overflow,
38    /// An element could not be converted to the requested type.
39    Conversion,
40}
41
42/// Mirror of `asdf_datatype_t`.
43///
44/// Field order and widths must match `include/asdf/core/datatype.h`.
45#[repr(C)]
46#[derive(Debug)]
47pub struct asdf_datatype_t {
48    /// The scalar type, or `STRUCTURED` for a compound type.
49    pub type_: ScalarTypeAbi,
50    /// Element size in bytes. May be left 0 for numeric types.
51    pub size: u64,
52    /// Optional field name, for a compound type's member.
53    pub name: *const c_char,
54    /// Byte order of the elements.
55    pub byteorder: ByteOrderAbi,
56    /// Number of sub-array dimensions, 0 for a scalar.
57    pub ndim: u32,
58    /// The sub-array shape, `ndim` entries.
59    pub shape: *const u64,
60    /// Number of fields, for a compound type.
61    pub nfields: u32,
62    /// The fields, `nfields` entries.
63    pub fields: *const asdf_datatype_t,
64}
65
66/// `asdf_scalar_datatype_t` as it crosses the boundary.
67pub type ScalarTypeAbi = i32;
68/// `asdf_byteorder_t` as it crosses the boundary.
69pub type ByteOrderAbi = i32;
70
71/// Mirror of `asdf_ndarray_t`.
72///
73/// The header notes that these fields are public "for now" and may not stay
74/// ABI-stable; reproducing them exactly is what makes this a drop-in today.
75#[repr(C)]
76#[derive(Debug)]
77pub struct asdf_ndarray_t {
78    /// Index of the block holding the data.
79    pub source: usize,
80    /// Number of dimensions.
81    pub ndim: u32,
82    /// The shape, `ndim` entries.
83    pub shape: *const u64,
84    /// The element type.
85    pub datatype: asdf_datatype_t,
86    /// Byte order of the array data.
87    pub byteorder: ByteOrderAbi,
88    /// Offset into the block where the data starts.
89    pub offset: u64,
90    /// Strides in bytes, `ndim` entries, or null for C-contiguous.
91    pub strides: *const i64,
92    /// Reserved for the implementation. This is where our state lives.
93    pub _reserved: *mut c_void,
94}
95
96/// Sixteen bytes, aligned to sixteen. See [`AlignedBuf`].
97///
98/// The field is never named again: it exists to give the allocation its size
99/// and its alignment, and the bytes are reached through the buffer's slice
100/// accessors. `#[repr(align)]` rather than `u128` because `u128`'s alignment
101/// is 16 only on some targets.
102#[repr(align(16))]
103#[derive(Clone, Copy, Debug)]
104struct Chunk(#[allow(dead_code)] [u8; 16]);
105
106/// A byte buffer aligned the way `malloc` aligns.
107///
108/// `asdf_ndarray_data` returns its pointer straight to C, and every caller
109/// casts it before dereferencing:
110///
111/// ```c
112/// int32_t *values = asdf_ndarray_data(array, &size);
113/// printf("%d\n", values[0]);
114/// ```
115///
116/// A `Vec<u8>` is aligned to 1, so that cast is undefined behaviour, and on a
117/// strict-alignment target it is a bus error rather than a theoretical one.
118/// Upstream libasdf gets this right without trying, because `malloc` is
119/// specified to return storage aligned for any fundamental type; we had
120/// quietly given up that guarantee by holding the bytes in a `Vec<u8>`.
121///
122/// Backing the storage with a 16-byte-aligned element restores it in safe
123/// code: `Vec<T>` is always aligned to `align_of::<T>()`. Sixteen covers
124/// every ASDF datatype -- the widest is `complex128`, a pair of doubles --
125/// and matches `max_align_t` on the platforms libasdf targets.
126#[derive(Clone, Debug)]
127pub(crate) struct AlignedBuf {
128    chunks: Vec<Chunk>,
129    /// The requested length. The backing store rounds up to a whole chunk.
130    len: usize,
131}
132
133impl AlignedBuf {
134    /// A zeroed buffer of `len` bytes.
135    fn zeroed(len: usize) -> Self {
136        Self { chunks: vec![Chunk([0; 16]); len.div_ceil(16)], len }
137    }
138
139    /// A buffer holding a copy of `bytes`.
140    fn from_slice(bytes: &[u8]) -> Self {
141        let mut buf = Self::zeroed(bytes.len());
142        buf.as_mut_slice()[..bytes.len()].copy_from_slice(bytes);
143        buf
144    }
145
146    fn as_slice(&self) -> &[u8] {
147        // Reading a `[Chunk]` as bytes is always valid: `Chunk` is a byte
148        // array under a stricter alignment, so it has no padding and no
149        // invalid bit patterns.
150        let bytes: &[u8] = unsafe {
151            core::slice::from_raw_parts(self.chunks.as_ptr().cast::<u8>(), self.chunks.len() * 16)
152        };
153        &bytes[..self.len]
154    }
155
156    fn as_mut_slice(&mut self) -> &mut [u8] {
157        // SAFETY: as `as_slice`, and the borrow is exclusive.
158        let bytes: &mut [u8] = unsafe {
159            core::slice::from_raw_parts_mut(
160                self.chunks.as_mut_ptr().cast::<u8>(),
161                self.chunks.len() * 16,
162            )
163        };
164        &mut bytes[..self.len]
165    }
166
167    /// The aligned pointer handed to C.
168    fn as_mut_ptr(&mut self) -> *mut u8 {
169        self.chunks.as_mut_ptr().cast::<u8>()
170    }
171}
172
173impl core::ops::Deref for AlignedBuf {
174    type Target = [u8];
175
176    fn deref(&self) -> &[u8] {
177        self.as_slice()
178    }
179}
180
181/// The state hanging off `_reserved`.
182///
183/// It owns every buffer the public struct points at, so the pointers stay
184/// valid for as long as the array does and are freed exactly once.
185struct NdarrayState {
186    shape: Vec<u64>,
187    strides: Option<Vec<i64>>,
188    /// Field descriptors for a compound datatype, kept alive for `fields`.
189    fields: Vec<asdf_datatype_t>,
190    /// Field names, kept alive for each field's `name`.
191    field_names: Vec<CString>,
192    /// Per-field sub-array shapes.
193    field_shapes: Vec<Vec<u64>>,
194    /// The engine's own view of the array.
195    parsed: Ndarray,
196    /// Data read from the file, cached for `asdf_ndarray_data`.
197    data: Option<AlignedBuf>,
198    /// A buffer from `asdf_ndarray_data_alloc`, owned until dealloc.
199    allocated: Option<AlignedBuf>,
200    /// Compression to use when the array is written.
201    compression: Compression,
202    /// Where the data will be written.
203    storage: AsdfArrayStorage,
204    /// The file the array was read from, for `asdf_ndarray_block`.
205    file: *mut crate::file_ffi::AsdfFile,
206    /// The index of the block holding the data, when it is not inline.
207    block_index: Option<usize>,
208    /// The block view handed out by `asdf_ndarray_block`, owned here.
209    block: *mut crate::block_ffi::AsdfBlock,
210}
211
212fn scalar_abi(t: ScalarType) -> ScalarTypeAbi {
213    t as i32
214}
215
216/// The scalar type for an ABI discriminant, for other modules.
217pub(crate) fn scalar_from_abi_public(v: ScalarTypeAbi) -> ScalarType {
218    scalar_from_abi(v)
219}
220
221fn scalar_from_abi(v: ScalarTypeAbi) -> ScalarType {
222    match v {
223        1 => ScalarType::Int8,
224        2 => ScalarType::Uint8,
225        3 => ScalarType::Int16,
226        4 => ScalarType::Uint16,
227        5 => ScalarType::Int32,
228        6 => ScalarType::Uint32,
229        7 => ScalarType::Int64,
230        8 => ScalarType::Uint64,
231        9 => ScalarType::Float16,
232        10 => ScalarType::Float32,
233        11 => ScalarType::Float64,
234        12 => ScalarType::Complex64,
235        13 => ScalarType::Complex128,
236        14 => ScalarType::Bool8,
237        15 => ScalarType::Ascii,
238        16 => ScalarType::Ucs4,
239        17 => ScalarType::Structured,
240        _ => ScalarType::Unknown,
241    }
242}
243
244/// Build the C datatype view for `datatype`, parking owned storage in `state`.
245fn build_datatype(
246    datatype: &Datatype,
247    array_order: asdf_core::core::datatype::ByteOrder,
248    state: &mut NdarrayState,
249) -> asdf_datatype_t {
250    use asdf_core::core::datatype::ByteOrder;
251
252    // A datatype states its own byte order only inside a compound field; a
253    // plain one takes the array's, which is where the schema puts it.
254    let effective = |own: ByteOrder| if own == ByteOrder::Default { array_order } else { own };
255
256    for field in &datatype.fields {
257        let name = field.name.as_deref().and_then(|n| CString::new(n).ok()).unwrap_or_default();
258        state.field_names.push(name);
259        state.field_shapes.push(field.datatype.shape.clone());
260    }
261
262    let base = state.fields.len();
263    for (index, field) in datatype.fields.iter().enumerate() {
264        let name_ptr = state.field_names[base + index].as_ptr();
265        let shape = &state.field_shapes[base + index];
266        state.fields.push(asdf_datatype_t {
267            type_: scalar_abi(field.datatype.scalar),
268            size: field.datatype.item_size(),
269            name: name_ptr,
270            byteorder: effective(field.datatype.byteorder) as i32,
271            ndim: u32::try_from(shape.len()).unwrap_or(0),
272            shape: if shape.is_empty() { core::ptr::null() } else { shape.as_ptr() },
273            nfields: 0,
274            fields: core::ptr::null(),
275        });
276    }
277
278    asdf_datatype_t {
279        type_: scalar_abi(datatype.scalar),
280        size: datatype.item_size(),
281        name: core::ptr::null(),
282        byteorder: effective(datatype.byteorder) as i32,
283        ndim: 0,
284        shape: core::ptr::null(),
285        nfields: u32::try_from(datatype.fields.len()).unwrap_or(0),
286        fields: if state.fields.is_empty() {
287            core::ptr::null()
288        } else {
289            state.fields[base..].as_ptr()
290        },
291    }
292}
293
294/// Build a public ndarray handle from the engine's parsed form.
295pub(crate) fn make_ndarray(parsed: Ndarray, shape: Vec<u64>) -> *mut asdf_ndarray_t {
296    let mut state = Box::new(NdarrayState {
297        shape,
298        strides: parsed.strides.clone(),
299        fields: Vec::new(),
300        field_names: Vec::new(),
301        field_shapes: Vec::new(),
302        parsed: parsed.clone(),
303        data: None,
304        allocated: None,
305        compression: Compression::None,
306        // Where the data was found is where it will go back, unless the
307        // caller says otherwise with `asdf_ndarray_storage_set`.
308        storage: match parsed.source {
309            Source::Inline(_) => AsdfArrayStorage::Inline,
310            Source::External(_) => AsdfArrayStorage::External,
311            _ => AsdfArrayStorage::Internal,
312        },
313        file: core::ptr::null_mut(),
314        block_index: None,
315        block: core::ptr::null_mut(),
316    });
317
318    // The compound-field storage must be sized before any pointer into it is
319    // taken, or a later push would reallocate and dangle it.
320    state.fields.reserve(parsed.datatype.fields.len());
321    state.field_names.reserve(parsed.datatype.fields.len());
322    state.field_shapes.reserve(parsed.datatype.fields.len());
323    let datatype = build_datatype(&parsed.datatype, parsed.byteorder, &mut state);
324
325    let source = match parsed.source {
326        Source::Block(index) => index,
327        _ => 0,
328    };
329
330    let shape_ptr = if state.shape.is_empty() { core::ptr::null() } else { state.shape.as_ptr() };
331    let strides_ptr = state.strides.as_ref().map_or(core::ptr::null(), |s| s.as_ptr());
332
333    let array = Box::new(asdf_ndarray_t {
334        source,
335        ndim: u32::try_from(state.shape.len()).unwrap_or(0),
336        shape: shape_ptr,
337        datatype,
338        byteorder: parsed.byteorder as i32,
339        offset: parsed.offset,
340        strides: strides_ptr,
341        _reserved: Box::into_raw(state).cast::<c_void>(),
342    });
343    Box::into_raw(array)
344}
345
346fn state_of<'a>(array: *mut asdf_ndarray_t) -> Option<&'a mut NdarrayState> {
347    if array.is_null() {
348        return None;
349    }
350    let reserved = unsafe { &*array }._reserved;
351    unsafe { crate::ffi::as_mut(reserved.cast::<NdarrayState>()) }
352}
353
354/// The state behind an array, creating it from the public fields if absent.
355///
356/// A caller may build an `asdf_ndarray_t` as a stack literal with
357/// `_reserved` left zero -- libasdf's own README write example does exactly
358/// that, then calls `asdf_ndarray_data_alloc` on it. So anything needing
359/// state has to bring it into being rather than assume it is already there.
360///
361/// The caller then owns that allocation, and releases it with
362/// `asdf_ndarray_deinit` or `asdf_ndarray_destroy`.
363fn ensure_state<'a>(array: *mut asdf_ndarray_t) -> Option<&'a mut NdarrayState> {
364    if array.is_null() {
365        return None;
366    }
367    if state_of(array).is_some() {
368        return state_of(array);
369    }
370
371    let view = unsafe { &*array };
372    let shape: Vec<u64> = if view.shape.is_null() || view.ndim == 0 {
373        Vec::new()
374    } else {
375        unsafe { core::slice::from_raw_parts(view.shape, view.ndim as usize) }.to_vec()
376    };
377    let strides: Option<Vec<i64>> = (!view.strides.is_null() && view.ndim > 0)
378        .then(|| unsafe { core::slice::from_raw_parts(view.strides, view.ndim as usize) }.to_vec());
379
380    // Rebuild the engine's view from what the caller filled in.
381    let scalar = scalar_from_abi(view.datatype.type_);
382    let mut datatype = Datatype::scalar(scalar);
383    if view.datatype.size != 0 {
384        datatype.size = view.datatype.size;
385    }
386    let parsed = Ndarray {
387        source: Source::Block(view.source),
388        shape: shape.iter().map(|d| Some(*d)).collect(),
389        datatype,
390        byteorder: match view.byteorder {
391            62 => asdf_core::core::datatype::ByteOrder::Big,
392            60 => asdf_core::core::datatype::ByteOrder::Little,
393            _ => asdf_core::core::datatype::ByteOrder::native(),
394        },
395        offset: view.offset,
396        strides: strides.clone(),
397        mask: None,
398    };
399
400    let state = Box::new(NdarrayState {
401        shape,
402        strides,
403        fields: Vec::new(),
404        field_names: Vec::new(),
405        field_shapes: Vec::new(),
406        parsed,
407        data: None,
408        allocated: None,
409        compression: Compression::None,
410        storage: AsdfArrayStorage::Internal,
411        file: core::ptr::null_mut(),
412        block_index: None,
413        block: core::ptr::null_mut(),
414    });
415    unsafe { (*array)._reserved = Box::into_raw(state).cast::<c_void>() };
416    state_of(array)
417}
418
419/// The number of elements.
420///
421/// # Safety
422/// `ndarray` must be null or a valid `asdf_ndarray_t`.
423#[unsafe(no_mangle)]
424pub unsafe extern "C" fn asdf_ndarray_size(ndarray: *const asdf_ndarray_t) -> u64 {
425    guard("asdf_ndarray_size", 0, || ndarray_size(ndarray))
426}
427
428/// Safe internal form of [`asdf_ndarray_size`].
429///
430/// The exported entry point is `unsafe extern "C"`, so calling it from
431/// inside the crate would need an `unsafe` block at every site to assert a
432/// contract the crate itself is upholding. Callers use this instead.
433pub(crate) fn ndarray_size(ndarray: *const asdf_ndarray_t) -> u64 {
434    if ndarray.is_null() {
435        return 0;
436    }
437    let array = unsafe { &*ndarray };
438    if array.shape.is_null() || array.ndim == 0 {
439        return 0;
440    }
441    let shape = unsafe { core::slice::from_raw_parts(array.shape, array.ndim as usize) };
442    // Saturating, not wrapping. The shape can come from a file, and these
443    // two entry points return a bare `uint64_t` with no error channel, so
444    // the only honest answer for a shape that does not fit is the largest
445    // one there is: a caller's `if (nbytes > available)` then rejects it,
446    // where a wrapped small value would sail through. Upstream multiplies
447    // these unchecked and a caller that trusts the result overruns its own
448    // buffer.
449    shape.iter().try_fold(1u64, |acc, dim| acc.checked_mul(*dim)).unwrap_or(u64::MAX)
450}
451
452/// The number of bytes the elements occupy.
453///
454/// # Safety
455/// `ndarray` must be null or a valid `asdf_ndarray_t`.
456#[unsafe(no_mangle)]
457pub unsafe extern "C" fn asdf_ndarray_nbytes(ndarray: *const asdf_ndarray_t) -> u64 {
458    guard("asdf_ndarray_nbytes", 0, || ndarray_nbytes(ndarray))
459}
460
461/// Safe internal form of [`asdf_ndarray_nbytes`].
462///
463/// The exported entry point is `unsafe extern "C"`, so calling it from
464/// inside the crate would need an `unsafe` block at every site to assert a
465/// contract the crate itself is upholding. Callers use this instead.
466pub(crate) fn ndarray_nbytes(ndarray: *const asdf_ndarray_t) -> u64 {
467    if ndarray.is_null() {
468        return 0;
469    }
470    let count = ndarray_size(ndarray);
471    // Only the field projection needs the unsafe; the size call does not.
472    let datatype = unsafe { &raw const (*ndarray).datatype };
473    count.saturating_mul(datatype_size(datatype.cast_mut()))
474}
475
476/// The size of one element of a datatype, computing it when left at zero.
477///
478/// # Safety
479/// `datatype` must be null or a valid `asdf_datatype_t`.
480#[unsafe(no_mangle)]
481pub unsafe extern "C" fn asdf_datatype_size(datatype: *mut asdf_datatype_t) -> u64 {
482    guard("asdf_datatype_size", 0, || datatype_size(datatype))
483}
484
485/// Safe internal form of [`asdf_datatype_size`].
486///
487/// The exported entry point is `unsafe extern "C"`, so calling it from
488/// inside the crate would need an `unsafe` block at every site to assert a
489/// contract the crate itself is upholding. Callers use this instead.
490pub(crate) fn datatype_size(datatype: *mut asdf_datatype_t) -> u64 {
491    if datatype.is_null() {
492        return 0;
493    }
494    let dt = unsafe { &mut *datatype };
495    if dt.size != 0 {
496        return dt.size;
497    }
498    // A string type must carry its own size; zero there means an empty
499    // string, as the header documents. Numeric types are computed and
500    // written back.
501    let scalar = scalar_from_abi(dt.type_);
502    let computed = scalar.size();
503    dt.size = computed;
504    computed
505}
506
507/// The scalar type named by a string, or `UNKNOWN`.
508///
509/// # Safety
510/// `name` must be a valid NUL-terminated string or null.
511#[unsafe(no_mangle)]
512pub unsafe extern "C" fn asdf_scalar_datatype_from_string(name: *const c_char) -> ScalarTypeAbi {
513    guard("asdf_scalar_datatype_from_string", 0, || {
514        if name.is_null() {
515            return 0;
516        }
517        let text = unsafe { CStr::from_ptr(name) }.to_string_lossy().into_owned();
518        scalar_abi(ScalarType::from_name(&text))
519    })
520}
521
522/// The string naming a scalar type.
523///
524/// # Safety
525/// The returned pointer refers to a `'static` string.
526#[unsafe(no_mangle)]
527pub extern "C" fn asdf_scalar_datatype_to_string(datatype: ScalarTypeAbi) -> *const c_char {
528    // Static names, so no allocation and no lifetime question.
529    let name: &'static CStr = match scalar_from_abi(datatype) {
530        ScalarType::Int8 => c"int8",
531        ScalarType::Uint8 => c"uint8",
532        ScalarType::Int16 => c"int16",
533        ScalarType::Uint16 => c"uint16",
534        ScalarType::Int32 => c"int32",
535        ScalarType::Uint32 => c"uint32",
536        ScalarType::Int64 => c"int64",
537        ScalarType::Uint64 => c"uint64",
538        ScalarType::Float16 => c"float16",
539        ScalarType::Float32 => c"float32",
540        ScalarType::Float64 => c"float64",
541        ScalarType::Complex64 => c"complex64",
542        ScalarType::Complex128 => c"complex128",
543        ScalarType::Bool8 => c"bool8",
544        ScalarType::Ascii => c"ascii",
545        ScalarType::Ucs4 => c"ucs4",
546        ScalarType::Structured => c"structured",
547        ScalarType::Unknown => c"unknown",
548    };
549    name.as_ptr()
550}
551
552/// Allocate a data buffer sized for the array.
553///
554/// Repeated calls return the same buffer. It is released by
555/// [`asdf_ndarray_data_dealloc`], not automatically.
556///
557/// # Safety
558/// `ndarray` must be null or a valid `asdf_ndarray_t`.
559#[unsafe(no_mangle)]
560pub unsafe extern "C" fn asdf_ndarray_data_alloc(ndarray: *mut asdf_ndarray_t) -> *mut c_void {
561    guard("asdf_ndarray_data_alloc", core::ptr::null_mut(), || ndarray_data_alloc(ndarray))
562}
563
564/// Safe internal form of [`asdf_ndarray_data_alloc`].
565///
566/// The exported entry point is `unsafe extern "C"`, so calling it from
567/// inside the crate would need an `unsafe` block at every site to assert a
568/// contract the crate itself is upholding. Callers use this instead.
569pub(crate) fn ndarray_data_alloc(ndarray: *mut asdf_ndarray_t) -> *mut c_void {
570    let nbytes = ndarray_nbytes(ndarray);
571    let Some(state) = ensure_state(ndarray) else {
572        return core::ptr::null_mut();
573    };
574    let Ok(len) = usize::try_from(nbytes) else {
575        return core::ptr::null_mut();
576    };
577    if state.allocated.is_none() {
578        state.allocated = Some(AlignedBuf::zeroed(len));
579    }
580    state.allocated.as_mut().map_or(core::ptr::null_mut(), |b| b.as_mut_ptr().cast::<c_void>())
581}
582
583/// Free a buffer from [`asdf_ndarray_data_alloc`].
584///
585/// Calling this without a prior allocation is a no-op.
586///
587/// # Safety
588/// `ndarray` must be null or a valid `asdf_ndarray_t`.
589#[unsafe(no_mangle)]
590pub unsafe extern "C" fn asdf_ndarray_data_dealloc(ndarray: *mut asdf_ndarray_t) {
591    guard("asdf_ndarray_data_dealloc", (), || {
592        if let Some(state) = state_of(ndarray) {
593            state.allocated = None;
594        }
595    })
596}
597
598/// Allocate the array's buffer and copy `src` into it.
599///
600/// # Safety
601/// `src` must point to at least `asdf_ndarray_nbytes` readable bytes.
602#[unsafe(no_mangle)]
603pub unsafe extern "C" fn asdf_ndarray_data_copy(
604    ndarray: *mut asdf_ndarray_t,
605    src: *const c_void,
606) -> NdarrayErr {
607    guard("asdf_ndarray_data_copy", NdarrayErr::Inval, || {
608        if ndarray.is_null() || src.is_null() {
609            return NdarrayErr::Inval;
610        }
611        let nbytes = ndarray_nbytes(ndarray);
612        let Ok(len) = usize::try_from(nbytes) else {
613            return NdarrayErr::Inval;
614        };
615        let destination = ndarray_data_alloc(ndarray);
616        if destination.is_null() {
617            return NdarrayErr::Oom;
618        }
619        unsafe {
620            core::ptr::copy_nonoverlapping(src.cast::<u8>(), destination.cast::<u8>(), len);
621        }
622        NdarrayErr::Ok
623    })
624}
625
626/// The array's data, decompressed if needed.
627///
628/// # Safety
629/// `ndarray` must be null or a valid `asdf_ndarray_t`; `size` writable or
630/// null. The pointer is owned by the array.
631#[unsafe(no_mangle)]
632pub unsafe extern "C" fn asdf_ndarray_data(
633    ndarray: *mut asdf_ndarray_t,
634    size: *mut usize,
635) -> *const c_void {
636    guard("asdf_ndarray_data", core::ptr::null(), || ndarray_data(ndarray, size))
637}
638
639/// Safe internal form of [`asdf_ndarray_data`].
640///
641/// The exported entry point is `unsafe extern "C"`, so calling it from
642/// inside the crate would need an `unsafe` block at every site to assert a
643/// contract the crate itself is upholding. Callers use this instead.
644pub(crate) fn ndarray_data(ndarray: *mut asdf_ndarray_t, size: *mut usize) -> *const c_void {
645    let Some(state) = state_of(ndarray) else {
646        if !size.is_null() {
647            unsafe { write_out(size, 0) };
648        }
649        return core::ptr::null();
650    };
651    // A buffer the caller built takes precedence: it is the array's data.
652    let bytes = match (&state.allocated, &state.data) {
653        (Some(buffer), _) => buffer,
654        (None, Some(data)) => data,
655        (None, None) => {
656            if !size.is_null() {
657                unsafe { write_out(size, 0) };
658            }
659            return core::ptr::null();
660        }
661    };
662    if !size.is_null() {
663        unsafe { write_out(size, bytes.len()) };
664    }
665    bytes.as_ptr().cast::<c_void>()
666}
667
668/// The array's data as stored, without decompressing.
669///
670/// # Safety
671/// See [`asdf_ndarray_data`].
672#[unsafe(no_mangle)]
673pub unsafe extern "C" fn asdf_ndarray_data_raw(
674    ndarray: *mut asdf_ndarray_t,
675    size: *mut usize,
676) -> *const c_void {
677    // The engine decompresses on read, so the two coincide for arrays we
678    // hand out; a caller wanting the stored form uses the block API.
679    ndarray_data(ndarray, size)
680}
681
682/// Set the compression used when the array is written.
683///
684/// # Safety
685/// `compression` must be a valid NUL-terminated string or null.
686#[unsafe(no_mangle)]
687pub unsafe extern "C" fn asdf_ndarray_compression_set(
688    ndarray: *mut asdf_ndarray_t,
689    compression: *const c_char,
690) -> c_int {
691    guard("asdf_ndarray_compression_set", -1, || {
692        let Some(state) = ensure_state(ndarray) else { return -1 };
693        let name = if compression.is_null() {
694            String::new()
695        } else {
696            unsafe { CStr::from_ptr(compression) }.to_string_lossy().into_owned()
697        };
698        let Ok(method) = Compression::from_name(&name) else {
699            return -1;
700        };
701        if !method.is_available() {
702            return -1;
703        }
704        state.compression = method;
705        0
706    })
707}
708
709/// Where the array's data will be written.
710///
711/// # Safety
712/// `ndarray` must be null or a valid `asdf_ndarray_t`.
713#[unsafe(no_mangle)]
714pub unsafe extern "C" fn asdf_ndarray_storage(ndarray: *mut asdf_ndarray_t) -> AsdfArrayStorage {
715    guard("asdf_ndarray_storage", AsdfArrayStorage::Default, || {
716        state_of(ndarray).map_or(AsdfArrayStorage::Default, |s| s.storage)
717    })
718}
719
720/// Set where the array's data will be written.
721///
722/// `External` is not yet supported and leaves the setting unchanged, as
723/// upstream does.
724///
725/// # Safety
726/// `ndarray` must be null or a valid `asdf_ndarray_t`.
727#[unsafe(no_mangle)]
728pub unsafe extern "C" fn asdf_ndarray_storage_set(
729    ndarray: *mut asdf_ndarray_t,
730    storage: AsdfArrayStorage,
731) {
732    guard("asdf_ndarray_storage_set", (), || {
733        if storage == AsdfArrayStorage::External {
734            return;
735        }
736        if let Some(state) = ensure_state(ndarray) {
737            state.storage = storage;
738        }
739    })
740}
741
742/// Convert one element to a destination scalar type, writing it into `out`.
743///
744/// A value outside the destination's range **saturates** rather than
745/// wrapping or being refused: to the destination's minimum or maximum for an
746/// integer, and to an infinity for a float. The call still reports
747/// [`NdarrayErr::Overflow`], and the caller is expected to keep the
748/// converted buffer -- that is what upstream does, and what a reader
749/// converting a whole array needs, since one bad element should not cost the
750/// rest.
751///
752/// Losing *precision* is not overflow: `int32::MAX` as an `f32` rounds, and
753/// that is an ordinary conversion. Only leaving the representable range is.
754fn write_converted(element: &Element, target: ScalarType, out: &mut [u8]) -> NdarrayErr {
755    /// Write a value's native-endian bytes, if they fit.
756    macro_rules! emit {
757        ($bytes:expr) => {{
758            let bytes = $bytes;
759            if out.len() < bytes.len() {
760                return NdarrayErr::Inval;
761            }
762            out[..bytes.len()].copy_from_slice(&bytes);
763        }};
764    }
765
766    macro_rules! put_int {
767        ($ty:ty) => {{
768            let (wide, saturated): (i128, bool) = match element {
769                Element::Int(v) => (i128::from(*v), false),
770                Element::Uint(v) => (i128::from(*v), false),
771                Element::Bool(v) => (i128::from(*v), false),
772                Element::Float(v) => {
773                    if v.is_nan() {
774                        // Converting a NaN to an integer has no defined
775                        // answer; zero is as good as any, and upstream
776                        // makes no promise either.
777                        (0, false)
778                    } else if v.is_infinite() {
779                        (
780                            if *v > 0.0 { i128::from(<$ty>::MAX) } else { i128::from(<$ty>::MIN) },
781                            true,
782                        )
783                    } else {
784                        // Truncates toward zero, as a C cast does.
785                        (v.trunc() as i128, false)
786                    }
787                }
788                _ => return NdarrayErr::Conversion,
789            };
790
791            let clamped = wide.clamp(i128::from(<$ty>::MIN), i128::from(<$ty>::MAX));
792            emit!((clamped as $ty).to_ne_bytes());
793            if saturated || clamped != wide { NdarrayErr::Overflow } else { NdarrayErr::Ok }
794        }};
795    }
796
797    /// Convert to a float type, reporting only a finite value turning
798    /// infinite.
799    macro_rules! put_float {
800        ($convert:expr, $bytes:expr) => {{
801            let source: f64 = match element {
802                Element::Float(v) => *v,
803                Element::Int(v) => *v as f64,
804                Element::Uint(v) => *v as f64,
805                Element::Bool(v) => f64::from(*v),
806                _ => return NdarrayErr::Conversion,
807            };
808            #[allow(clippy::redundant_closure_call)]
809            let value = ($convert)(source);
810            #[allow(clippy::redundant_closure_call)]
811            let bytes = ($bytes)(value);
812            emit!(bytes);
813            if source.is_finite() && !is_finite_result(value.into()) {
814                NdarrayErr::Overflow
815            } else {
816                NdarrayErr::Ok
817            }
818        }};
819    }
820
821    match target {
822        ScalarType::Int8 => put_int!(i8),
823        ScalarType::Int16 => put_int!(i16),
824        ScalarType::Int32 => put_int!(i32),
825        ScalarType::Int64 => put_int!(i64),
826        ScalarType::Uint8 => put_int!(u8),
827        ScalarType::Uint16 => put_int!(u16),
828        ScalarType::Uint32 => put_int!(u32),
829        ScalarType::Uint64 => put_int!(u64),
830        ScalarType::Bool8 => {
831            let value = match element {
832                Element::Bool(v) => u8::from(*v),
833                Element::Int(v) => u8::from(*v != 0),
834                Element::Uint(v) => u8::from(*v != 0),
835                _ => return NdarrayErr::Conversion,
836            };
837            emit!(value.to_ne_bytes());
838            NdarrayErr::Ok
839        }
840        ScalarType::Float32 => put_float!(|v: f64| v as f32, |v: f32| v.to_ne_bytes()),
841        ScalarType::Float64 => put_float!(|v: f64| v, |v: f64| v.to_ne_bytes()),
842        ScalarType::Float16 => {
843            put_float!(half::f16::from_f64, |v: half::f16| v.to_bits().to_ne_bytes())
844        }
845        _ => NdarrayErr::Conversion,
846    }
847}
848
849/// Whether a converted float stayed finite.
850fn is_finite_result(value: f64) -> bool {
851    value.is_finite()
852}
853
854/// Decode the array's elements, using the cached data.
855fn elements_of(state: &NdarrayState) -> Option<Vec<Element>> {
856    let data = state.allocated.as_ref().or(state.data.as_ref())?;
857    decode_all(&state.parsed, &state.shape, data).ok()
858}
859
860/// Read the whole array, converting to `dst_t`.
861///
862/// With `dst` pointing at a null pointer, a buffer is allocated and the
863/// caller frees it with `free`. Passing `ASDF_DATATYPE_SOURCE` (which is
864/// `UNKNOWN`) keeps the array's own type.
865///
866/// # Safety
867/// `ndarray` must be a valid `asdf_ndarray_t`; `dst` must be writable.
868#[unsafe(no_mangle)]
869pub unsafe extern "C" fn asdf_ndarray_read_all(
870    ndarray: *mut asdf_ndarray_t,
871    dst_t: ScalarTypeAbi,
872    dst: *mut *mut c_void,
873) -> NdarrayErr {
874    guard("asdf_ndarray_read_all", NdarrayErr::Inval, || {
875        let Some(state) = state_of(ndarray) else {
876            return NdarrayErr::Inval;
877        };
878        let Some(elements) = elements_of(state) else {
879            return NdarrayErr::Inval;
880        };
881
882        let target = match scalar_from_abi(dst_t) {
883            // ASDF_DATATYPE_SOURCE is an alias for UNKNOWN and means
884            // "keep the source type".
885            ScalarType::Unknown => state.parsed.datatype.scalar,
886            other => other,
887        };
888        let width = target.size();
889        if width == 0 {
890            return NdarrayErr::Conversion;
891        }
892        let Ok(width) = usize::try_from(width) else {
893            return NdarrayErr::Inval;
894        };
895
896        let Some(total) = elements.len().checked_mul(width) else {
897            return NdarrayErr::Inval;
898        };
899        let mut buffer = vec![0u8; total];
900        // An overflowing element saturates and the read continues: the
901        // caller gets the whole converted array *and* the report that
902        // something did not fit. Anything else is a hard failure.
903        let mut overflowed = false;
904        for (index, element) in elements.iter().enumerate() {
905            let slot = &mut buffer[index * width..(index + 1) * width];
906            match write_converted(element, target, slot) {
907                NdarrayErr::Ok => {}
908                NdarrayErr::Overflow => overflowed = true,
909                err => return err,
910            }
911        }
912
913        let delivered = deliver(&buffer, dst);
914        if delivered != NdarrayErr::Ok {
915            return delivered;
916        }
917        if overflowed { NdarrayErr::Overflow } else { NdarrayErr::Ok }
918    })
919}
920
921/// The flat offset of an element, or `None` if the indices are out of range.
922fn flat_index(shape: &[u64], indices: &[u64]) -> Option<usize> {
923    if shape.len() != indices.len() {
924        return None;
925    }
926    let mut flat = 0u64;
927    for (dim, index) in shape.iter().zip(indices.iter()) {
928        if index >= dim {
929            return None;
930        }
931        flat = flat.checked_mul(*dim)?.checked_add(*index)?;
932    }
933    usize::try_from(flat).ok()
934}
935
936/// Generate a typed single-element accessor.
937macro_rules! read_at {
938    ($name:ident, $ty:ty, $convert:expr) => {
939        /// Read one element, converted to this type.
940        ///
941        /// # Safety
942        /// `ndarray` must be a valid `asdf_ndarray_t`; `indices` must point
943        /// to `ndim` values; `err` writable or null.
944        #[unsafe(no_mangle)]
945        pub unsafe extern "C" fn $name(
946            ndarray: *mut asdf_ndarray_t,
947            indices: *const u64,
948            err: *mut c_int,
949        ) -> $ty {
950            guard(stringify!($name), <$ty>::default(), || {
951                let set = |code: NdarrayErr| {
952                    if !err.is_null() {
953                        unsafe { write_out(err, code as c_int) };
954                    }
955                };
956                let Some(state) = state_of(ndarray) else {
957                    set(NdarrayErr::Inval);
958                    return <$ty>::default();
959                };
960                if indices.is_null() {
961                    set(NdarrayErr::Inval);
962                    return <$ty>::default();
963                }
964                let idx = unsafe { core::slice::from_raw_parts(indices, state.shape.len()) };
965                let Some(flat) = flat_index(&state.shape, idx) else {
966                    set(NdarrayErr::OutOfBounds);
967                    return <$ty>::default();
968                };
969                let Some(elements) = elements_of(state) else {
970                    set(NdarrayErr::Inval);
971                    return <$ty>::default();
972                };
973                let Some(element) = elements.get(flat) else {
974                    set(NdarrayErr::OutOfBounds);
975                    return <$ty>::default();
976                };
977                #[allow(clippy::redundant_closure_call)]
978                match ($convert)(element) {
979                    Some(v) => {
980                        set(NdarrayErr::Ok);
981                        v
982                    }
983                    None => {
984                        set(NdarrayErr::Conversion);
985                        <$ty>::default()
986                    }
987                }
988            })
989        }
990    };
991}
992
993/// Convert an element to an integer type, or `None`.
994macro_rules! to_int {
995    ($ty:ty) => {
996        |element: &Element| -> Option<$ty> {
997            let wide: i128 = match element {
998                Element::Int(v) => i128::from(*v),
999                Element::Uint(v) => i128::from(*v),
1000                Element::Bool(v) => i128::from(*v),
1001                Element::Float(v) if v.fract() == 0.0 => *v as i128,
1002                _ => return None,
1003            };
1004            <$ty>::try_from(wide).ok()
1005        }
1006    };
1007}
1008
1009read_at!(asdf_ndarray_read_int8_at, i8, to_int!(i8));
1010read_at!(asdf_ndarray_read_int16_at, i16, to_int!(i16));
1011read_at!(asdf_ndarray_read_int32_at, i32, to_int!(i32));
1012read_at!(asdf_ndarray_read_int64_at, i64, to_int!(i64));
1013read_at!(asdf_ndarray_read_uint8_at, u8, to_int!(u8));
1014read_at!(asdf_ndarray_read_uint16_at, u16, to_int!(u16));
1015read_at!(asdf_ndarray_read_uint32_at, u32, to_int!(u32));
1016read_at!(asdf_ndarray_read_uint64_at, u64, to_int!(u64));
1017
1018read_at!(asdf_ndarray_read_float32_at, f32, |element: &Element| {
1019    match element {
1020        Element::Float(v) => Some(*v as f32),
1021        Element::Int(v) => Some(*v as f32),
1022        Element::Uint(v) => Some(*v as f32),
1023        _ => None,
1024    }
1025});
1026read_at!(asdf_ndarray_read_float64_at, f64, |element: &Element| {
1027    match element {
1028        Element::Float(v) => Some(*v),
1029        Element::Int(v) => Some(*v as f64),
1030        Element::Uint(v) => Some(*v as f64),
1031        _ => None,
1032    }
1033});
1034
1035/// Read one `float16` element, returning its raw bit pattern.
1036///
1037/// Called only by `shim.c`, which reinterprets the bits as `_Float16`. The
1038/// conversion has to happen on the C side: `_Float16` and `uint16_t` do not
1039/// share a return ABI -- on x86-64 SysV one returns in `xmm0`, the other in
1040/// `rax` -- so returning bits from Rust and reinterpreting in C is the only
1041/// way to place the value correctly without unstable Rust.
1042///
1043/// # Safety
1044/// See the other `read_*_at` accessors.
1045#[unsafe(no_mangle)]
1046pub unsafe extern "C" fn asdf_shim_ndarray_read_float16_bits_at(
1047    ndarray: *mut c_void,
1048    indices: *const u64,
1049    err: *mut c_int,
1050) -> u16 {
1051    guard("asdf_shim_ndarray_read_float16_bits_at", 0u16, || {
1052        let value =
1053            unsafe { asdf_ndarray_read_float64_at(ndarray.cast::<asdf_ndarray_t>(), indices, err) };
1054        half::f16::from_f64(value).to_bits()
1055    })
1056}
1057
1058/// Free an ndarray handle and everything it owns.
1059///
1060/// # Safety
1061/// `ndarray` must be null or have come from the library, and must not be used
1062/// afterwards.
1063#[unsafe(no_mangle)]
1064pub unsafe extern "C" fn asdf_ndarray_destroy(ndarray: *mut asdf_ndarray_t) {
1065    guard("asdf_ndarray_destroy", (), || ndarray_destroy(ndarray))
1066}
1067
1068/// Safe internal form of [`asdf_ndarray_destroy`].
1069///
1070/// The exported entry point is `unsafe extern "C"`, so calling it from
1071/// inside the crate would need an `unsafe` block at every site to assert a
1072/// contract the crate itself is upholding. Callers use this instead.
1073pub(crate) fn ndarray_destroy(ndarray: *mut asdf_ndarray_t) {
1074    if ndarray.is_null() {
1075        return;
1076    }
1077    let boxed = unsafe { Box::from_raw(ndarray) };
1078    if !boxed._reserved.is_null() {
1079        drop(unsafe { Box::from_raw(boxed._reserved.cast::<NdarrayState>()) });
1080    }
1081}
1082
1083/// Attach data read from a file to an array handle.
1084pub(crate) fn set_data(array: *mut asdf_ndarray_t, data: &[u8]) {
1085    if let Some(state) = state_of(array) {
1086        state.data = Some(AlignedBuf::from_slice(data));
1087    }
1088}
1089
1090/// Build an ndarray handle for a value, reading its block data.
1091fn ndarray_from_value(value: *mut crate::file_ffi::AsdfValue) -> *mut asdf_ndarray_t {
1092    use crate::file_ffi::{file_reader, value_document, value_file, value_node};
1093
1094    let (Some(doc), Some(node)) = (value_document(value), value_node(value)) else {
1095        return core::ptr::null_mut();
1096    };
1097    let Ok(parsed) = Ndarray::parse(doc, node) else {
1098        return core::ptr::null_mut();
1099    };
1100
1101    // Read the block up front, as libasdf does, so the array's data pointer
1102    // is usable for as long as the handle is.
1103    let mut data: Option<Vec<u8>> = None;
1104    let mut block_len = None;
1105    let mut block_index = None;
1106    if let Some(file) = value_file(value)
1107        && let Some(reader) = file_reader(file)
1108    {
1109        let index = match parsed.source {
1110            Source::Block(index) => Some(index),
1111            Source::LastBlock => reader.block_count().checked_sub(1),
1112            // An external `source` names another file. `asdf_core` resolves
1113            // those, but upstream libasdf does not -- it logs a warning and
1114            // hands back nothing -- so the C surface does not either. The
1115            // idiomatic API is where exploded form is available.
1116            _ => None,
1117        };
1118        block_index = index;
1119        if let Some(index) = index
1120            && let Ok(bytes) = reader.block_data(index)
1121        {
1122            block_len = Some(bytes.len() as u64);
1123            data = Some(bytes.into_owned());
1124        }
1125    }
1126
1127    let Ok(shape) = parsed.resolved_shape(block_len) else {
1128        return core::ptr::null_mut();
1129    };
1130
1131    // An inline array's elements are already in the tree. `asdf_ndarray_data`
1132    // still hands out bytes, so they are encoded into the array's own scalar
1133    // type in this machine's order -- there is no stored layout to preserve.
1134    if data.is_none() && matches!(parsed.source, Source::Inline(_)) {
1135        data = encode_inline(doc, &parsed, &shape);
1136    }
1137
1138    let array = make_ndarray(parsed, shape);
1139    if let Some(bytes) = &data {
1140        set_data(array, bytes);
1141    }
1142    // Remember where the data came from so `asdf_ndarray_block` can hand
1143    // back a view of the underlying block.
1144    if let Some(state) = state_of(array) {
1145        state.file = value_file(value).unwrap_or(core::ptr::null_mut());
1146        state.block_index = block_index;
1147    }
1148    array
1149}
1150
1151/// Encode an inline array's elements as bytes of its own scalar type.
1152///
1153/// Returns `None` for a compound datatype, whose record layout is not a
1154/// simple sequence of scalars; such an array is still described correctly,
1155/// it just has no flat buffer to hand out.
1156fn encode_inline(
1157    doc: &asdf_core::yaml::Document,
1158    parsed: &Ndarray,
1159    shape: &[u64],
1160) -> Option<Vec<u8>> {
1161    let scalar = parsed.datatype.scalar;
1162    if !parsed.datatype.fields.is_empty() {
1163        return None;
1164    }
1165    let width = usize::try_from(scalar.size()).ok()?;
1166    if width == 0 {
1167        return None;
1168    }
1169
1170    let elements = asdf_core::core::decode_inline(doc, parsed, shape).ok()?;
1171    let mut out = vec![0u8; elements.len().checked_mul(width)?];
1172    for (index, element) in elements.iter().enumerate() {
1173        let slot = &mut out[index * width..(index + 1) * width];
1174        if write_converted(element, scalar, slot) != NdarrayErr::Ok {
1175            return None;
1176        }
1177    }
1178    Some(out)
1179}
1180
1181/// Read the array at `path`.
1182///
1183/// # Safety
1184/// `file` must be a valid file handle, `path` a valid NUL-terminated string
1185/// or null, and `out` writable. The result must be released with
1186/// [`asdf_ndarray_destroy`].
1187#[unsafe(no_mangle)]
1188pub unsafe extern "C" fn asdf_get_ndarray(
1189    file: *mut crate::file_ffi::AsdfFile,
1190    path: *const c_char,
1191    out: *mut *mut asdf_ndarray_t,
1192) -> crate::types::AsdfValueErr {
1193    use crate::types::AsdfValueErr;
1194
1195    guard("asdf_get_ndarray", AsdfValueErr::Unknown, || {
1196        let value = unsafe { crate::file_ffi::asdf_get_value(file, path) };
1197        if value.is_null() {
1198            return AsdfValueErr::NotFound;
1199        }
1200        let array = ndarray_from_value(value);
1201        unsafe { crate::file_ffi::asdf_value_destroy(value) };
1202
1203        if array.is_null() {
1204            return AsdfValueErr::TypeMismatch;
1205        }
1206        if !out.is_null() {
1207            unsafe { write_out(out, array) };
1208        } else {
1209            ndarray_destroy(array);
1210        }
1211        AsdfValueErr::Ok
1212    })
1213}
1214
1215/// Interpret a value as an array.
1216///
1217/// # Safety
1218/// `value` must be a valid value handle and `out` writable. The result must
1219/// be released with [`asdf_ndarray_destroy`].
1220#[unsafe(no_mangle)]
1221pub unsafe extern "C" fn asdf_value_as_ndarray(
1222    value: *mut crate::file_ffi::AsdfValue,
1223    out: *mut *mut asdf_ndarray_t,
1224) -> crate::types::AsdfValueErr {
1225    use crate::types::AsdfValueErr;
1226
1227    guard("asdf_value_as_ndarray", AsdfValueErr::Unknown, || {
1228        let array = ndarray_from_value(value);
1229        if array.is_null() {
1230            return AsdfValueErr::TypeMismatch;
1231        }
1232        if !out.is_null() {
1233            unsafe { write_out(out, array) };
1234        } else {
1235            ndarray_destroy(array);
1236        }
1237        AsdfValueErr::Ok
1238    })
1239}
1240
1241/// Whether the value at `path` is an ndarray.
1242///
1243/// # Safety
1244/// `file` must be a valid file handle and `path` a valid string or null.
1245#[unsafe(no_mangle)]
1246pub unsafe extern "C" fn asdf_is_ndarray(
1247    file: *mut crate::file_ffi::AsdfFile,
1248    path: *const c_char,
1249) -> bool {
1250    guard("asdf_is_ndarray", false, || {
1251        let value = unsafe { crate::file_ffi::asdf_get_value(file, path) };
1252        if value.is_null() {
1253            return false;
1254        }
1255        let is_array = value_is_ndarray(value);
1256        unsafe { crate::file_ffi::asdf_value_destroy(value) };
1257        is_array
1258    })
1259}
1260
1261/// Whether a value is an ndarray, by its tag.
1262///
1263/// # Safety
1264/// `value` must be null or a valid value handle.
1265#[unsafe(no_mangle)]
1266pub unsafe extern "C" fn asdf_value_is_ndarray(value: *mut crate::file_ffi::AsdfValue) -> bool {
1267    guard("asdf_value_is_ndarray", false, || value_is_ndarray(value))
1268}
1269
1270/// Safe internal form of [`asdf_value_is_ndarray`].
1271///
1272/// The exported entry point is `unsafe extern "C"`, so calling it from
1273/// inside the crate would need an `unsafe` block at every site to assert a
1274/// contract the crate itself is upholding. Callers use this instead.
1275pub(crate) fn value_is_ndarray(value: *mut crate::file_ffi::AsdfValue) -> bool {
1276    use crate::file_ffi::{value_document, value_node};
1277    let (Some(doc), Some(node)) = (value_document(value), value_node(value)) else {
1278        return false;
1279    };
1280    doc.tag_of(node).is_some_and(|t| t.split_version().0 == "core/ndarray")
1281}
1282
1283// ---- The rest of the generated extension family ----------------------
1284
1285/// Free an ndarray's fields without freeing the struct.
1286///
1287/// # Safety
1288/// `ndarray` must be null or a valid `asdf_ndarray_t`; safe on a zeroed one.
1289#[unsafe(no_mangle)]
1290pub unsafe extern "C" fn asdf_ndarray_deinit(ndarray: *mut asdf_ndarray_t) {
1291    guard("asdf_ndarray_deinit", (), || ndarray_deinit(ndarray))
1292}
1293
1294/// Safe internal form of [`asdf_ndarray_deinit`].
1295///
1296/// The exported entry point is `unsafe extern "C"`, so calling it from
1297/// inside the crate would need an `unsafe` block at every site to assert a
1298/// contract the crate itself is upholding. Callers use this instead.
1299pub(crate) fn ndarray_deinit(ndarray: *mut asdf_ndarray_t) {
1300    if ndarray.is_null() {
1301        return;
1302    }
1303    let array = unsafe { &mut *ndarray };
1304    if !array._reserved.is_null() {
1305        let state = unsafe { Box::from_raw(array._reserved.cast::<NdarrayState>()) };
1306        if !state.block.is_null() {
1307            unsafe { crate::block_ffi::asdf_block_close(state.block) };
1308        }
1309        drop(state);
1310        array._reserved = core::ptr::null_mut();
1311    }
1312    // The public pointers all borrowed from the state that just went.
1313    array.shape = core::ptr::null();
1314    array.strides = core::ptr::null();
1315    array.datatype.fields = core::ptr::null();
1316    array.datatype.nfields = 0;
1317    array.ndim = 0;
1318}
1319
1320/// Deep-copy an ndarray into caller-provided storage.
1321///
1322/// The copy owns its own data, so it may outlive the original and be written
1323/// to a different file.
1324///
1325/// # Safety
1326/// `src` and `dst` must be valid `asdf_ndarray_t` values.
1327#[unsafe(no_mangle)]
1328pub unsafe extern "C" fn asdf_ndarray_copy_into(
1329    file: *mut crate::file_ffi::AsdfFile,
1330    src: *const asdf_ndarray_t,
1331    dst: *mut asdf_ndarray_t,
1332) -> bool {
1333    guard("asdf_ndarray_copy_into", false, || ndarray_copy_into(file, src, dst))
1334}
1335
1336/// Safe internal form of [`asdf_ndarray_copy_into`].
1337///
1338/// The exported entry point is `unsafe extern "C"`, so calling it from
1339/// inside the crate would need an `unsafe` block at every site to assert a
1340/// contract the crate itself is upholding. Callers use this instead.
1341pub(crate) fn ndarray_copy_into(
1342    file: *mut crate::file_ffi::AsdfFile,
1343    src: *const asdf_ndarray_t,
1344    dst: *mut asdf_ndarray_t,
1345) -> bool {
1346    let _ = file;
1347    if src.is_null() || dst.is_null() {
1348        return false;
1349    }
1350    let Some(state) = state_of(src.cast_mut()) else {
1351        return false;
1352    };
1353
1354    // Rebuild from the engine's own view, so every buffer is fresh.
1355    let rebuilt = make_ndarray(state.parsed.clone(), state.shape.clone());
1356    if rebuilt.is_null() {
1357        return false;
1358    }
1359    if let Some(data) = state.allocated.as_ref().or(state.data.as_ref()) {
1360        set_data(rebuilt, data);
1361    }
1362    if let Some(fresh) = state_of(rebuilt) {
1363        fresh.compression = state.compression;
1364        fresh.storage = state.storage;
1365    }
1366
1367    // Move the rebuilt value into the caller's storage.
1368    let boxed = unsafe { Box::from_raw(rebuilt) };
1369    unsafe { core::ptr::write(dst, *boxed) };
1370    true
1371}
1372
1373/// Deep-copy an ndarray into fresh storage.
1374///
1375/// # Safety
1376/// `src` must be a valid `asdf_ndarray_t`. The result must be released with
1377/// [`asdf_ndarray_destroy`].
1378#[unsafe(no_mangle)]
1379pub unsafe extern "C" fn asdf_ndarray_copy(
1380    file: *mut crate::file_ffi::AsdfFile,
1381    src: *const asdf_ndarray_t,
1382) -> *mut asdf_ndarray_t {
1383    guard("asdf_ndarray_copy", core::ptr::null_mut(), || ndarray_copy(file, src))
1384}
1385
1386/// Safe internal form of [`asdf_ndarray_copy`].
1387///
1388/// The exported entry point is `unsafe extern "C"`, so calling it from
1389/// inside the crate would need an `unsafe` block at every site to assert a
1390/// contract the crate itself is upholding. Callers use this instead.
1391pub(crate) fn ndarray_copy(
1392    file: *mut crate::file_ffi::AsdfFile,
1393    src: *const asdf_ndarray_t,
1394) -> *mut asdf_ndarray_t {
1395    if src.is_null() {
1396        return core::ptr::null_mut();
1397    }
1398    let raw = Box::into_raw(Box::new(asdf_ndarray_t {
1399        source: 0,
1400        ndim: 0,
1401        shape: core::ptr::null(),
1402        datatype: asdf_datatype_t {
1403            type_: 0,
1404            size: 0,
1405            name: core::ptr::null(),
1406            byteorder: 0,
1407            ndim: 0,
1408            shape: core::ptr::null(),
1409            nfields: 0,
1410            fields: core::ptr::null(),
1411        },
1412        byteorder: 0,
1413        offset: 0,
1414        strides: core::ptr::null(),
1415        _reserved: core::ptr::null_mut(),
1416    }));
1417    if ndarray_copy_into(file, src, raw) {
1418        raw
1419    } else {
1420        drop(unsafe { Box::from_raw(raw) });
1421        core::ptr::null_mut()
1422    }
1423}
1424
1425/// Deep-copy a null-terminated array of ndarrays.
1426///
1427/// # Safety
1428/// `src` must be a null-terminated array of valid `asdf_ndarray_t` pointers.
1429#[unsafe(no_mangle)]
1430pub unsafe extern "C" fn asdf_ndarray_array_copy(
1431    file: *mut crate::file_ffi::AsdfFile,
1432    src: *mut *const asdf_ndarray_t,
1433) -> *mut *mut asdf_ndarray_t {
1434    guard("asdf_ndarray_array_copy", core::ptr::null_mut(), || {
1435        if src.is_null() {
1436            return core::ptr::null_mut();
1437        }
1438        let mut count = 0isize;
1439        while !unsafe { *src.offset(count) }.is_null() {
1440            count += 1;
1441        }
1442
1443        let mut copies: Vec<*mut asdf_ndarray_t> = Vec::with_capacity(count as usize + 1);
1444        for index in 0..count {
1445            // Reading the caller's NULL-terminated list is the unsafe part.
1446            let entry = unsafe { *src.offset(index) };
1447            let copy = ndarray_copy(file, entry);
1448            if copy.is_null() {
1449                // Unwind rather than leak the copies already made.
1450                for made in copies {
1451                    ndarray_destroy(made);
1452                }
1453                return core::ptr::null_mut();
1454            }
1455            copies.push(copy);
1456        }
1457        copies.push(core::ptr::null_mut());
1458        Box::into_raw(copies.into_boxed_slice()).cast::<*mut asdf_ndarray_t>()
1459    })
1460}
1461
1462/// Build a value for an ndarray, writing its data into a new block.
1463///
1464/// The array's `source` in the tree is the index of the block appended to
1465/// `file`, so the value is only meaningful once written with that file.
1466///
1467/// # Safety
1468/// `file` must be a file handle open for writing and `obj` a valid
1469/// `asdf_ndarray_t`. The result must be released with `asdf_value_destroy`.
1470#[unsafe(no_mangle)]
1471pub unsafe extern "C" fn asdf_value_of_ndarray(
1472    file: *mut crate::file_ffi::AsdfFile,
1473    obj: *const asdf_ndarray_t,
1474) -> *mut crate::file_ffi::AsdfValue {
1475    guard("asdf_value_of_ndarray", core::ptr::null_mut(), || {
1476        if file.is_null() || obj.is_null() {
1477            return core::ptr::null_mut();
1478        }
1479        let value = value_of_ndarray_inner(file, obj);
1480        // `ndarray.h`: "assigning the ndarray this way transfers ownership of
1481        // its data to ``file``". The payload has been copied into the file
1482        // above, so the state hanging off `_reserved` is spent -- and callers
1483        // that build an `asdf_ndarray_t` as a stack literal, which is what
1484        // libasdf's own write example and every libasdf-gwcs serializer do,
1485        // have nowhere to call `asdf_ndarray_deinit` from. Only the state we
1486        // allocated goes; the caller's own `shape`/`strides` are left alone.
1487        if !value.is_null() {
1488            release_ndarray_state(obj.cast_mut());
1489        }
1490        value
1491    })
1492}
1493
1494/// Free the `_reserved` state, leaving the caller's public fields intact.
1495///
1496/// Unlike [`ndarray_deinit`] this does not null `shape`, `strides` or `ndim`:
1497/// on the write path those point at storage the caller owns.
1498fn release_ndarray_state(ndarray: *mut asdf_ndarray_t) {
1499    if ndarray.is_null() {
1500        return;
1501    }
1502    // Reached through the raw pointer rather than a `&mut`, because the
1503    // public signature takes a `*const` and a reference retag here would be
1504    // a Stacked Borrows violation for any caller that had one. `ensure_state`
1505    // writes the same field the same way.
1506    let slot = unsafe { &raw mut (*ndarray)._reserved };
1507    let reserved = unsafe { slot.read() };
1508    if reserved.is_null() {
1509        return;
1510    }
1511    let state = unsafe { Box::from_raw(reserved.cast::<NdarrayState>()) };
1512    if !state.block.is_null() {
1513        unsafe { crate::block_ffi::asdf_block_close(state.block) };
1514    }
1515    drop(state);
1516    unsafe { slot.write(core::ptr::null_mut()) };
1517}
1518
1519fn value_of_ndarray_inner(
1520    file: *mut crate::file_ffi::AsdfFile,
1521    obj: *const asdf_ndarray_t,
1522) -> *mut crate::file_ffi::AsdfValue {
1523    use asdf_core::yaml::{CollectionStyle, NodeData, Tag};
1524
1525    {
1526        let array = unsafe { &*obj };
1527
1528        // The shape and datatype come from the public fields, so an array
1529        // built as a C stack literal works -- which is what libasdf's own
1530        // write example does.
1531        let shape: Vec<u64> = if array.shape.is_null() || array.ndim == 0 {
1532            Vec::new()
1533        } else {
1534            unsafe { core::slice::from_raw_parts(array.shape, array.ndim as usize) }.to_vec()
1535        };
1536        let scalar = scalar_from_abi(array.datatype.type_);
1537        let item_size = if array.datatype.size != 0 { array.datatype.size } else { scalar.size() };
1538        if item_size == 0 {
1539            return core::ptr::null_mut();
1540        }
1541
1542        // The data is whatever the caller allocated or we read. The shape
1543        // is the caller's, but a wrapped product would size a buffer the
1544        // following copy then overruns, so it is checked like any other.
1545        let Ok(element_count) = asdf_core::core::ndarray::element_count(&shape) else {
1546            return core::ptr::null_mut();
1547        };
1548        let Some(expected) =
1549            element_count.max(1).checked_mul(item_size).and_then(|n| usize::try_from(n).ok())
1550        else {
1551            return core::ptr::null_mut();
1552        };
1553        let payload: Vec<u8> = match ensure_state(obj.cast_mut()) {
1554            Some(state) => state
1555                .allocated
1556                .as_ref()
1557                .or(state.data.as_ref())
1558                .map(|b| b.as_slice().to_vec())
1559                .unwrap_or_else(|| vec![0u8; expected]),
1560            None => vec![0u8; expected],
1561        };
1562        let (compression, array_storage) = state_of(obj.cast_mut())
1563            .map(|s| (s.compression, s.storage))
1564            .unwrap_or((Compression::None, AsdfArrayStorage::Default));
1565
1566        // The file's own `emitter.array_storage` decides for every array it
1567        // holds; each array's setting applies only where the file has none.
1568        let config = crate::file_ffi::file_config(file).unwrap_or_default();
1569        let storage = if config.array_storage == AsdfArrayStorage::Default {
1570            array_storage
1571        } else {
1572            config.array_storage
1573        };
1574
1575        // Inline storage writes the values into the tree instead of a block,
1576        // which is what `asdf_ndarray_storage_set(.., INLINE)` asks for.
1577        if storage == AsdfArrayStorage::Inline {
1578            let count: u64 = shape.iter().product::<u64>().max(1);
1579            warn_if_inline_is_large(file, count, config.inline_ndarray_warning_thresh);
1580            return inline_value_of_ndarray(file, array, &shape, &payload);
1581        }
1582
1583        // Append the block, then reference it by index.
1584        let Some(blocks) = crate::file_ffi::file_blocks_mut(file) else {
1585            return core::ptr::null_mut();
1586        };
1587        blocks.push(asdf_core::PendingBlock::compressed(payload, compression));
1588        let index = blocks.len() - 1;
1589
1590        let Some(doc) = file_document_mut(file) else {
1591            return core::ptr::null_mut();
1592        };
1593
1594        let source = doc.add_scalar(index.to_string());
1595        let datatype = doc.add_scalar(scalar.name());
1596        let order = match array.byteorder {
1597            62 => "big",
1598            60 => "little",
1599            // An unspecified order means this machine's.
1600            _ => ByteOrderNative,
1601        };
1602        let byteorder = doc.add_scalar(order);
1603
1604        let dims: Vec<_> = shape.iter().map(|d| doc.add_scalar(d.to_string())).collect();
1605        let shape_node = doc.add_sequence(dims);
1606        if let NodeData::Sequence { style, .. } = &mut doc.node_mut(shape_node).data {
1607            *style = CollectionStyle::Flow;
1608        }
1609
1610        let keys: Vec<_> = ["source", "datatype", "byteorder", "shape"]
1611            .iter()
1612            .map(|k| doc.add_scalar(*k))
1613            .collect();
1614        let mut pairs = vec![
1615            (keys[0], source),
1616            (keys[1], datatype),
1617            (keys[2], byteorder),
1618            (keys[3], shape_node),
1619        ];
1620
1621        // `offset` and `strides` say where the elements sit inside the
1622        // block, so an array that has them is unreadable without them. Both
1623        // are omitted at their defaults, as every other writer omits them.
1624        if array.offset != 0 {
1625            let key = doc.add_scalar("offset");
1626            let value = doc.add_scalar(array.offset.to_string());
1627            pairs.push((key, value));
1628        }
1629        if !array.strides.is_null() && array.ndim > 0 {
1630            let strides =
1631                unsafe { core::slice::from_raw_parts(array.strides, array.ndim as usize) };
1632            let items: Vec<_> = strides.iter().map(|s| doc.add_scalar(s.to_string())).collect();
1633            let node = doc.add_sequence(items);
1634            if let NodeData::Sequence { style, .. } = &mut doc.node_mut(node).data {
1635                *style = CollectionStyle::Flow;
1636            }
1637            let key = doc.add_scalar("strides");
1638            pairs.push((key, node));
1639        }
1640
1641        let node = doc.add_mapping(pairs);
1642        doc.node_mut(node).tag = Some(Tag::parse("tag:stsci.edu:asdf/core/ndarray-1.1.0"));
1643
1644        Box::into_raw(Box::new(crate::file_ffi::AsdfValue::new(file, node)))
1645    }
1646}
1647
1648/// Warn when an inline array is larger than the file's threshold.
1649///
1650/// Inline data is text, so a large array bloats the tree and slows every
1651/// reader that parses it. A threshold of zero means the caller set none.
1652fn warn_if_inline_is_large(file: *mut crate::file_ffi::AsdfFile, elements: u64, threshold: usize) {
1653    if threshold == 0 || elements <= threshold as u64 {
1654        return;
1655    }
1656    crate::error_ffi::log_to_file(
1657        file,
1658        crate::error_ffi::LogLevel::Warn,
1659        &format!(
1660            "inline ndarray has {elements} elements, exceeding the threshold of {threshold}; \
1661             consider using binary block storage instead"
1662        ),
1663    );
1664}
1665
1666/// Build the tree value for an array whose data goes inline.
1667///
1668/// The elements are decoded from the caller's buffer and nested to the
1669/// array's shape, so the file carries `data: [[..], ..]` and no block at
1670/// all. `byteorder` is left out: it describes bytes in a block, and there
1671/// are none.
1672fn inline_value_of_ndarray(
1673    file: *mut crate::file_ffi::AsdfFile,
1674    array: &asdf_ndarray_t,
1675    shape: &[u64],
1676    payload: &[u8],
1677) -> *mut crate::file_ffi::AsdfValue {
1678    use asdf_core::core::datatype::Datatype;
1679    use asdf_core::core::ndarray::Ndarray;
1680    use asdf_core::yaml::{CollectionStyle, NodeData, Tag};
1681
1682    let scalar = scalar_from_abi(array.datatype.type_);
1683    let mut datatype = Datatype::scalar(scalar);
1684    if array.datatype.size != 0 {
1685        datatype.size = array.datatype.size;
1686    }
1687    let parsed = Ndarray {
1688        source: Source::Block(0),
1689        shape: shape.iter().map(|d| Some(*d)).collect(),
1690        datatype,
1691        byteorder: match array.byteorder {
1692            62 => asdf_core::core::datatype::ByteOrder::Big,
1693            60 => asdf_core::core::datatype::ByteOrder::Little,
1694            _ => asdf_core::core::datatype::ByteOrder::native(),
1695        },
1696        offset: array.offset,
1697        strides: None,
1698        mask: None,
1699    };
1700
1701    // A zero-dimensional array holds nothing -- `asdf_ndarray_size` says so
1702    // and `asdf_ndarray_data_alloc` allocates nothing -- so there is no data
1703    // to decode, and `data: []` is what goes in the tree.
1704    let elements = if shape.is_empty() {
1705        Vec::new()
1706    } else {
1707        match asdf_core::core::decode_all(&parsed, shape, payload) {
1708            Ok(elements) => elements,
1709            Err(_) => return core::ptr::null_mut(),
1710        }
1711    };
1712
1713    let Some(doc) = file_document_mut(file) else {
1714        return core::ptr::null_mut();
1715    };
1716
1717    let data = if elements.is_empty() {
1718        let node = doc.add_sequence(Vec::new());
1719        if let NodeData::Sequence { style, .. } = &mut doc.node_mut(node).data {
1720            *style = CollectionStyle::Flow;
1721        }
1722        node
1723    } else {
1724        asdf_core::core::elements::nest(doc, &elements, shape)
1725    };
1726    let datatype_node = doc.add_scalar(scalar.name());
1727
1728    let dims: Vec<_> = shape.iter().map(|d| doc.add_scalar(d.to_string())).collect();
1729    let shape_node = doc.add_sequence(dims);
1730    if let NodeData::Sequence { style, .. } = &mut doc.node_mut(shape_node).data {
1731        *style = CollectionStyle::Flow;
1732    }
1733
1734    let keys: Vec<_> = ["datatype", "data", "shape"].iter().map(|k| doc.add_scalar(*k)).collect();
1735    let node =
1736        doc.add_mapping(vec![(keys[0], datatype_node), (keys[1], data), (keys[2], shape_node)]);
1737    doc.node_mut(node).tag = Some(Tag::parse("tag:stsci.edu:asdf/core/ndarray-1.1.0"));
1738
1739    Box::into_raw(Box::new(crate::file_ffi::AsdfValue::new(file, node)))
1740}
1741
1742/// This machine's byte order, as the schema spells it.
1743#[allow(non_upper_case_globals)]
1744const ByteOrderNative: &str = if cfg!(target_endian = "big") { "big" } else { "little" };
1745
1746/// Write an ndarray at `path`, appending its data as a new block.
1747///
1748/// # Safety
1749/// See [`asdf_value_of_ndarray`]; `path` must be a valid string or null.
1750#[unsafe(no_mangle)]
1751pub unsafe extern "C" fn asdf_set_ndarray(
1752    file: *mut crate::file_ffi::AsdfFile,
1753    path: *const c_char,
1754    obj: *const asdf_ndarray_t,
1755) -> crate::types::AsdfValueErr {
1756    use crate::types::AsdfValueErr;
1757
1758    guard("asdf_set_ndarray", AsdfValueErr::Unknown, || {
1759        let value = unsafe { asdf_value_of_ndarray(file, obj) };
1760        if value.is_null() {
1761            return AsdfValueErr::EmitFailure;
1762        }
1763        let result = unsafe { crate::file_ffi::set_value_at(file, path, value) };
1764        unsafe { crate::file_ffi::asdf_value_destroy(value) };
1765        result
1766    })
1767}
1768
1769// ---- Blocks and tiles ------------------------------------------------
1770
1771/// The block underlying an array, or null when its data is inline.
1772///
1773/// The view is opened on first use and owned by the array, so it must not be
1774/// closed by the caller; it is released with the array.
1775///
1776/// # Safety
1777/// `ndarray` must be null or a valid `asdf_ndarray_t`.
1778#[unsafe(no_mangle)]
1779pub unsafe extern "C" fn asdf_ndarray_block(
1780    ndarray: *mut asdf_ndarray_t,
1781) -> *mut crate::block_ffi::AsdfBlock {
1782    guard("asdf_ndarray_block", core::ptr::null_mut(), || {
1783        let Some(state) = state_of(ndarray) else {
1784            return core::ptr::null_mut();
1785        };
1786        if !state.block.is_null() {
1787            return state.block;
1788        }
1789        let (Some(index), false) = (state.block_index, state.file.is_null()) else {
1790            return core::ptr::null_mut();
1791        };
1792        state.block = unsafe { crate::block_ffi::asdf_block_open(state.file, index) };
1793        state.block
1794    })
1795}
1796
1797/// Copy `count` elements starting at `flat` into `dst`, converting to `target`.
1798fn write_elements(
1799    elements: &[Element],
1800    flat: usize,
1801    count: usize,
1802    target: ScalarType,
1803    width: usize,
1804    dst: &mut [u8],
1805) -> NdarrayErr {
1806    let mut overflowed = false;
1807    for step in 0..count {
1808        let Some(element) = elements.get(flat + step) else {
1809            return NdarrayErr::OutOfBounds;
1810        };
1811        let slot = &mut dst[step * width..(step + 1) * width];
1812        match write_converted(element, target, slot) {
1813            NdarrayErr::Ok => {}
1814            // Saturating is reported but does not stop the copy; see
1815            // `write_converted`.
1816            NdarrayErr::Overflow => overflowed = true,
1817            err => return err,
1818        }
1819    }
1820    if overflowed { NdarrayErr::Overflow } else { NdarrayErr::Ok }
1821}
1822
1823/// Hand a buffer back through `dst`, allocating with `malloc` if asked.
1824///
1825/// libasdf lets the caller either supply storage or take a fresh allocation
1826/// by pointing `dst` at a null pointer, in which case the caller frees it
1827/// with `free` -- so `malloc` rather than Rust's allocator.
1828fn deliver(buffer: &[u8], dst: *mut *mut c_void) -> NdarrayErr {
1829    if dst.is_null() {
1830        return NdarrayErr::Inval;
1831    }
1832    let existing = unsafe { *dst };
1833    if existing.is_null() {
1834        // A null destination means "allocate one for me, I will `free` it",
1835        // so this must come from `malloc` and not Rust's allocator.
1836        let Some(allocation) = CMallocBuf::copy_from(buffer) else {
1837            return NdarrayErr::Oom;
1838        };
1839        unsafe { write_out(dst, allocation.into_raw()) };
1840    } else {
1841        unsafe {
1842            core::ptr::copy_nonoverlapping(buffer.as_ptr(), existing.cast::<u8>(), buffer.len());
1843        }
1844    }
1845    NdarrayErr::Ok
1846}
1847
1848/// Read one element, converting to `dst_t`.
1849///
1850/// # Safety
1851/// `ndarray` must be a valid `asdf_ndarray_t`; `indices` must point to `ndim`
1852/// values; `dst` must have room for one value of `dst_t`. `dst` need not be
1853/// aligned.
1854#[unsafe(no_mangle)]
1855pub unsafe extern "C" fn asdf_ndarray_read_at(
1856    ndarray: *mut asdf_ndarray_t,
1857    indices: *const u64,
1858    dst_t: ScalarTypeAbi,
1859    dst: *mut c_void,
1860) -> NdarrayErr {
1861    guard("asdf_ndarray_read_at", NdarrayErr::Inval, || {
1862        let Some(state) = state_of(ndarray) else {
1863            return NdarrayErr::Inval;
1864        };
1865        if indices.is_null() || dst.is_null() {
1866            return NdarrayErr::Inval;
1867        }
1868        let idx = unsafe { core::slice::from_raw_parts(indices, state.shape.len()) };
1869        let Some(flat) = flat_index(&state.shape, idx) else {
1870            return NdarrayErr::OutOfBounds;
1871        };
1872        let Some(elements) = elements_of(state) else {
1873            return NdarrayErr::Inval;
1874        };
1875        let Some(element) = elements.get(flat) else {
1876            return NdarrayErr::OutOfBounds;
1877        };
1878
1879        let target = match scalar_from_abi(dst_t) {
1880            ScalarType::Unknown => state.parsed.datatype.scalar,
1881            other => other,
1882        };
1883        let Ok(width) = usize::try_from(target.size()) else {
1884            return NdarrayErr::Inval;
1885        };
1886        if width == 0 {
1887            return NdarrayErr::Conversion;
1888        }
1889        // Written into a local first because `dst` carries no alignment
1890        // guarantee, then copied out byte by byte.
1891        let mut scratch = vec![0u8; width];
1892        let err = write_converted(element, target, &mut scratch);
1893        if err != NdarrayErr::Ok {
1894            return err;
1895        }
1896        unsafe { core::ptr::copy_nonoverlapping(scratch.as_ptr(), dst.cast::<u8>(), width) };
1897        NdarrayErr::Ok
1898    })
1899}
1900
1901/// Read an N-dimensional tile, converting to `dst_t`.
1902///
1903/// # Safety
1904/// `origin` and `shape` must each point to `ndim` values; `dst` must be
1905/// writable, pointing either at storage large enough for the tile or at a
1906/// null pointer, in which case a buffer is allocated for the caller to
1907/// `free`.
1908#[unsafe(no_mangle)]
1909pub unsafe extern "C" fn asdf_ndarray_read_tile_ndim(
1910    ndarray: *mut asdf_ndarray_t,
1911    origin: *const u64,
1912    shape: *const u64,
1913    dst_t: ScalarTypeAbi,
1914    dst: *mut *mut c_void,
1915) -> NdarrayErr {
1916    guard("asdf_ndarray_read_tile_ndim", NdarrayErr::Inval, || {
1917        ndarray_read_tile_ndim(ndarray, origin, shape, dst_t, dst)
1918    })
1919}
1920
1921/// Safe internal form of [`asdf_ndarray_read_tile_ndim`].
1922///
1923/// The exported entry point is `unsafe extern "C"`, so calling it from
1924/// inside the crate would need an `unsafe` block at every site to assert a
1925/// contract the crate itself is upholding. Callers use this instead.
1926pub(crate) fn ndarray_read_tile_ndim(
1927    ndarray: *mut asdf_ndarray_t,
1928    origin: *const u64,
1929    shape: *const u64,
1930    dst_t: ScalarTypeAbi,
1931    dst: *mut *mut c_void,
1932) -> NdarrayErr {
1933    let Some(state) = state_of(ndarray) else {
1934        return NdarrayErr::Inval;
1935    };
1936    if origin.is_null() || shape.is_null() {
1937        return NdarrayErr::Inval;
1938    }
1939    let ndim = state.shape.len();
1940    if ndim == 0 {
1941        return NdarrayErr::Inval;
1942    }
1943    let origin = unsafe { core::slice::from_raw_parts(origin, ndim) }.to_vec();
1944    let tile = unsafe { core::slice::from_raw_parts(shape, ndim) }.to_vec();
1945
1946    // Every corner of the tile has to land inside the array.
1947    for axis in 0..ndim {
1948        let Some(end) = origin[axis].checked_add(tile[axis]) else {
1949            return NdarrayErr::OutOfBounds;
1950        };
1951        if end > state.shape[axis] {
1952            return NdarrayErr::OutOfBounds;
1953        }
1954    }
1955
1956    let target = match scalar_from_abi(dst_t) {
1957        ScalarType::Unknown => state.parsed.datatype.scalar,
1958        other => other,
1959    };
1960    let Ok(width) = usize::try_from(target.size()) else {
1961        return NdarrayErr::Inval;
1962    };
1963    if width == 0 {
1964        return NdarrayErr::Conversion;
1965    }
1966
1967    let mut count: u64 = 1;
1968    for extent in &tile {
1969        let Some(next) = count.checked_mul(*extent) else {
1970            return NdarrayErr::Inval;
1971        };
1972        count = next;
1973    }
1974    let Ok(count) = usize::try_from(count) else {
1975        return NdarrayErr::Inval;
1976    };
1977    if count == 0 {
1978        return deliver(&[], dst);
1979    }
1980
1981    let Some(elements) = elements_of(state) else {
1982        return NdarrayErr::Inval;
1983    };
1984
1985    // The tile is contiguous along the last axis only, so copy it one
1986    // run at a time and step the outer indices by hand.
1987    let run = tile[ndim - 1] as usize;
1988    let Some(total) = count.checked_mul(width) else {
1989        return NdarrayErr::Inval;
1990    };
1991    let mut buffer = vec![0u8; total];
1992    let mut cursor = origin.clone();
1993    let mut written = 0usize;
1994    let mut overflowed = false;
1995    loop {
1996        let Some(flat) = flat_index(&state.shape, &cursor) else {
1997            return NdarrayErr::OutOfBounds;
1998        };
1999        let slice = &mut buffer[written * width..(written + run) * width];
2000        match write_elements(&elements, flat, run, target, width, slice) {
2001            NdarrayErr::Ok => {}
2002            NdarrayErr::Overflow => overflowed = true,
2003            err => return err,
2004        }
2005        written += run;
2006
2007        // Advance the outer axes odometer-style; the last is the run.
2008        let mut axis = ndim as isize - 2;
2009        loop {
2010            if axis < 0 {
2011                let delivered = deliver(&buffer, dst);
2012                if delivered != NdarrayErr::Ok {
2013                    return delivered;
2014                }
2015                return if overflowed { NdarrayErr::Overflow } else { NdarrayErr::Ok };
2016            }
2017            let a = axis as usize;
2018            cursor[a] += 1;
2019            if cursor[a] < origin[a] + tile[a] {
2020                break;
2021            }
2022            cursor[a] = origin[a];
2023            axis -= 1;
2024        }
2025    }
2026}
2027
2028/// Read a 2-D tile, converting to `dst_t`.
2029///
2030/// For an array of more than two dimensions, `plane_origin` gives the
2031/// `ndim - 2` outer coordinates; null selects the first plane. `x`/`width`
2032/// index the last axis and `y`/`height` the one before it.
2033///
2034/// # Safety
2035/// See [`asdf_ndarray_read_tile_ndim`]; `plane_origin`, when not null, must
2036/// point to `ndim - 2` values.
2037#[unsafe(no_mangle)]
2038#[allow(clippy::too_many_arguments)]
2039pub unsafe extern "C" fn asdf_ndarray_read_tile_2d(
2040    ndarray: *mut asdf_ndarray_t,
2041    x: u64,
2042    y: u64,
2043    width: u64,
2044    height: u64,
2045    plane_origin: *const u64,
2046    dst_t: ScalarTypeAbi,
2047    dst: *mut *mut c_void,
2048) -> NdarrayErr {
2049    guard("asdf_ndarray_read_tile_2d", NdarrayErr::Inval, || {
2050        let Some(state) = state_of(ndarray) else {
2051            return NdarrayErr::Inval;
2052        };
2053        let ndim = state.shape.len();
2054        if ndim < 2 {
2055            return NdarrayErr::Inval;
2056        }
2057        let planes = ndim - 2;
2058
2059        let mut origin = vec![0u64; ndim];
2060        let mut tile = vec![1u64; ndim];
2061        if planes > 0 && !plane_origin.is_null() {
2062            let outer = unsafe { core::slice::from_raw_parts(plane_origin, planes) };
2063            origin[..planes].copy_from_slice(outer);
2064        }
2065        origin[ndim - 2] = y;
2066        origin[ndim - 1] = x;
2067        tile[ndim - 2] = height;
2068        tile[ndim - 1] = width;
2069
2070        ndarray_read_tile_ndim(ndarray, origin.as_ptr(), tile.as_ptr(), dst_t, dst)
2071    })
2072}
2073
2074// ---- Registry entry --------------------------------------------------
2075//
2076// See the matching section in `core_ext.rs`: generic callers reach an
2077// extension only through `asdf_extension_get` and
2078// `asdf_value_as_extension_type`, so the typed functions above are not
2079// enough on their own. This family is written out rather than generated
2080// because the ndarray extension's functions are hand-written too.
2081
2082/// Deserialize through the registry's generic entry point.
2083///
2084/// # Safety
2085/// `value` must be a valid value handle and `out` writable.
2086unsafe extern "C" fn ndarray_ext_deserialize(
2087    value: *mut crate::file_ffi::AsdfValue,
2088    _userdata: *const c_void,
2089    out: *mut *mut c_void,
2090) -> crate::types::AsdfValueErr {
2091    let mut typed: *mut asdf_ndarray_t = core::ptr::null_mut();
2092    let err = unsafe { asdf_value_as_ndarray(value, &mut typed) };
2093    if err == crate::types::AsdfValueErr::Ok && !out.is_null() {
2094        unsafe { write_out(out, typed.cast::<c_void>()) };
2095    }
2096    err
2097}
2098
2099/// Serialize through the registry's generic entry point.
2100///
2101/// # Safety
2102/// `obj` must be a valid `asdf_ndarray_t`.
2103unsafe extern "C" fn ndarray_ext_serialize(
2104    file: *mut crate::file_ffi::AsdfFile,
2105    obj: *const c_void,
2106    _userdata: *const c_void,
2107) -> *mut crate::file_ffi::AsdfValue {
2108    unsafe { asdf_value_of_ndarray(file, obj.cast::<asdf_ndarray_t>()) }
2109}
2110
2111/// Deep-copy through the registry's generic entry point.
2112///
2113/// # Safety
2114/// `src` and `dst` must be valid `asdf_ndarray_t` values.
2115unsafe extern "C" fn ndarray_ext_copy(
2116    file: *mut crate::file_ffi::AsdfFile,
2117    src: *const c_void,
2118    dst: *mut c_void,
2119) -> bool {
2120    unsafe {
2121        asdf_ndarray_copy_into(file, src.cast::<asdf_ndarray_t>(), dst.cast::<asdf_ndarray_t>())
2122    }
2123}
2124
2125/// De-initialise through the registry's generic entry point.
2126///
2127/// # Safety
2128/// `obj` must be a valid `asdf_ndarray_t`.
2129unsafe extern "C" fn ndarray_ext_deinit(obj: *mut c_void) {
2130    ndarray_deinit(obj.cast::<asdf_ndarray_t>());
2131}
2132
2133/// Build the ndarray extension's registry entry.
2134///
2135/// Upstream registers both `ndarray-1.1.0` and `ndarray-1.0.0`: the newer
2136/// schema adds `float16` and requires one of `source`/`data`, but the same
2137/// deserializer reads both.
2138pub(crate) fn build_ndarray_extension() -> *mut crate::extension_ffi::asdf_extension_t {
2139    use crate::extension_ffi::{
2140        asdf_extension_t, asdf_extension_vtab_t, asdf_software_t, libasdf_software,
2141    };
2142
2143    let tags: Vec<*const c_char> = vec![
2144        c"tag:stsci.edu:asdf/core/ndarray-1.1.0".as_ptr(),
2145        c"tag:stsci.edu:asdf/core/ndarray-1.0.0".as_ptr(),
2146        core::ptr::null(),
2147    ];
2148    let tags = Box::leak(tags.into_boxed_slice());
2149
2150    let vtab = Box::leak(Box::new(asdf_extension_vtab_t {
2151        serialize: Some(ndarray_ext_serialize),
2152        deserialize: Some(ndarray_ext_deserialize),
2153        copy: Some(ndarray_ext_copy),
2154        deinit: Some(ndarray_ext_deinit),
2155        _reserved: [None; 4],
2156    }));
2157
2158    Box::leak(Box::new(asdf_extension_t {
2159        tags: tags.as_ptr(),
2160        software: (&raw const libasdf_software).cast::<asdf_software_t>().cast_mut(),
2161        vtab: core::ptr::from_ref(vtab),
2162        size: core::mem::size_of::<asdf_ndarray_t>(),
2163        userdata: core::ptr::null_mut(),
2164    }))
2165}
2166
2167#[cfg(test)]
2168mod tests {
2169    use super::*;
2170    use asdf_core::core::datatype::ByteOrder;
2171    use asdf_core::core::ndarray::Ndarray as CoreNdarray;
2172    use asdf_core::yaml::parse_document;
2173
2174    /// Build a handle over a little-endian int32 array of the given values.
2175    fn int32_array(values: &[i32]) -> *mut asdf_ndarray_t {
2176        let doc = parse_document(&format!(
2177            "a:\n  source: 0\n  shape: [{}]\n  datatype: int32\n  byteorder: little\n",
2178            values.len()
2179        ))
2180        .unwrap();
2181        let root = doc.root().unwrap();
2182        let parsed = CoreNdarray::parse(&doc, doc.mapping_get(root, "a").unwrap()).unwrap();
2183
2184        let array = make_ndarray(parsed, vec![values.len() as u64]);
2185        let bytes: Vec<u8> = values.iter().flat_map(|v| v.to_le_bytes()).collect();
2186        set_data(array, &bytes);
2187        array
2188    }
2189
2190    /// A 3x4 little-endian int32 array holding 0..12 in row-major order.
2191    fn int32_grid() -> *mut asdf_ndarray_t {
2192        let doc = parse_document(
2193            "a:\n  source: 0\n  shape: [3, 4]\n  datatype: int32\n  byteorder: little\n",
2194        )
2195        .unwrap();
2196        let root = doc.root().unwrap();
2197        let parsed = CoreNdarray::parse(&doc, doc.mapping_get(root, "a").unwrap()).unwrap();
2198        let array = make_ndarray(parsed, vec![3, 4]);
2199        let bytes: Vec<u8> = (0i32..12).flat_map(i32::to_le_bytes).collect();
2200        set_data(array, &bytes);
2201        array
2202    }
2203
2204    /// A value outside the destination's range saturates and reports
2205    /// overflow, rather than wrapping or refusing the whole read.
2206    #[test]
2207    fn out_of_range_values_saturate_and_report_overflow() {
2208        let mut out = [0u8; 8];
2209
2210        // A negative integer into an unsigned type clamps at zero.
2211        assert_eq!(
2212            write_converted(&Element::Int(-5), ScalarType::Uint8, &mut out[..1]),
2213            NdarrayErr::Overflow
2214        );
2215        assert_eq!(out[0], 0);
2216
2217        // Too large for the destination clamps at its maximum.
2218        assert_eq!(
2219            write_converted(&Element::Int(70_000), ScalarType::Uint16, &mut out[..2]),
2220            NdarrayErr::Overflow
2221        );
2222        assert_eq!(u16::from_ne_bytes([out[0], out[1]]), u16::MAX);
2223
2224        // And at its minimum going the other way.
2225        assert_eq!(
2226            write_converted(&Element::Int(-70_000), ScalarType::Int16, &mut out[..2]),
2227            NdarrayErr::Overflow
2228        );
2229        assert_eq!(i16::from_ne_bytes([out[0], out[1]]), i16::MIN);
2230
2231        // A value that fits is not an overflow.
2232        assert_eq!(
2233            write_converted(&Element::Int(42), ScalarType::Uint8, &mut out[..1]),
2234            NdarrayErr::Ok
2235        );
2236        assert_eq!(out[0], 42);
2237    }
2238
2239    /// Losing precision is not overflow; leaving the representable range is.
2240    #[test]
2241    fn only_leaving_the_range_counts_as_overflow() {
2242        let mut out = [0u8; 8];
2243
2244        // `i32::MAX` rounds when it becomes an `f32`, which is ordinary.
2245        assert_eq!(
2246            write_converted(&Element::Int(i64::from(i32::MAX)), ScalarType::Float32, &mut out[..4]),
2247            NdarrayErr::Ok
2248        );
2249
2250        // `f64::MAX` has no `f32`, so it becomes an infinity, which is not.
2251        assert_eq!(
2252            write_converted(&Element::Float(f64::MAX), ScalarType::Float32, &mut out[..4]),
2253            NdarrayErr::Overflow
2254        );
2255        assert!(f32::from_ne_bytes([out[0], out[1], out[2], out[3]]).is_infinite());
2256
2257        // An infinity that was already infinite stays one, and is not an
2258        // overflow: nothing was lost.
2259        assert_eq!(
2260            write_converted(&Element::Float(f64::INFINITY), ScalarType::Float32, &mut out[..4]),
2261            NdarrayErr::Ok
2262        );
2263    }
2264
2265    /// A float into an integer truncates toward zero, and the non-finites
2266    /// saturate.
2267    #[test]
2268    fn floats_convert_to_integers_the_way_a_c_cast_does() {
2269        let mut out = [0u8; 8];
2270
2271        assert_eq!(
2272            write_converted(&Element::Float(3.9), ScalarType::Int32, &mut out[..4]),
2273            NdarrayErr::Ok
2274        );
2275        assert_eq!(i32::from_ne_bytes([out[0], out[1], out[2], out[3]]), 3);
2276
2277        assert_eq!(
2278            write_converted(&Element::Float(-3.9), ScalarType::Int32, &mut out[..4]),
2279            NdarrayErr::Ok
2280        );
2281        assert_eq!(i32::from_ne_bytes([out[0], out[1], out[2], out[3]]), -3);
2282
2283        assert_eq!(
2284            write_converted(&Element::Float(f64::INFINITY), ScalarType::Int32, &mut out[..4]),
2285            NdarrayErr::Overflow
2286        );
2287        assert_eq!(i32::from_ne_bytes([out[0], out[1], out[2], out[3]]), i32::MAX);
2288
2289        assert_eq!(
2290            write_converted(&Element::Float(f64::NEG_INFINITY), ScalarType::Int32, &mut out[..4]),
2291            NdarrayErr::Overflow
2292        );
2293        assert_eq!(i32::from_ne_bytes([out[0], out[1], out[2], out[3]]), i32::MIN);
2294
2295        // A NaN has no integer, so any answer will do -- but it must not be
2296        // reported as an overflow, which would be a claim about magnitude.
2297        assert_eq!(
2298            write_converted(&Element::Float(f64::NAN), ScalarType::Int32, &mut out[..4]),
2299            NdarrayErr::Ok
2300        );
2301    }
2302
2303    /// The whole array still comes back when one element overflows.
2304    #[test]
2305    fn an_overflow_does_not_abandon_the_read() {
2306        let array = int32_array(&[1, -1, 300]);
2307        let mut dst: *mut c_void = core::ptr::null_mut();
2308        assert_eq!(
2309            unsafe { asdf_ndarray_read_all(array, ScalarType::Uint8 as ScalarTypeAbi, &mut dst) },
2310            NdarrayErr::Overflow
2311        );
2312        assert!(!dst.is_null(), "the converted buffer is still delivered");
2313
2314        let got = unsafe { core::slice::from_raw_parts(dst.cast::<u8>(), 3) };
2315        assert_eq!(got, [1, 0, 255], "each element saturates on its own");
2316
2317        unsafe { libc::free(dst) };
2318        unsafe { asdf_ndarray_destroy(array) };
2319    }
2320
2321    #[test]
2322    fn reads_one_element_at_indices() {
2323        let array = int32_grid();
2324        let indices: [u64; 2] = [2, 1];
2325        let mut out: i32 = 0;
2326        assert_eq!(
2327            unsafe {
2328                asdf_ndarray_read_at(
2329                    array,
2330                    indices.as_ptr(),
2331                    ScalarType::Int32 as ScalarTypeAbi,
2332                    core::ptr::from_mut(&mut out).cast(),
2333                )
2334            },
2335            NdarrayErr::Ok
2336        );
2337        assert_eq!(out, 9, "row 2, column 1 of a 3x4 array holding 0..12");
2338
2339        // Converting on the way out is allowed.
2340        let mut wide: f64 = 0.0;
2341        assert_eq!(
2342            unsafe {
2343                asdf_ndarray_read_at(
2344                    array,
2345                    indices.as_ptr(),
2346                    ScalarType::Float64 as ScalarTypeAbi,
2347                    core::ptr::from_mut(&mut wide).cast(),
2348                )
2349            },
2350            NdarrayErr::Ok
2351        );
2352        assert!((wide - 9.0).abs() < f64::EPSILON);
2353
2354        let past_end: [u64; 2] = [3, 0];
2355        assert_eq!(
2356            unsafe {
2357                asdf_ndarray_read_at(
2358                    array,
2359                    past_end.as_ptr(),
2360                    ScalarType::Int32 as ScalarTypeAbi,
2361                    core::ptr::from_mut(&mut out).cast(),
2362                )
2363            },
2364            NdarrayErr::OutOfBounds
2365        );
2366        unsafe { asdf_ndarray_destroy(array) };
2367    }
2368
2369    #[test]
2370    fn reads_a_2d_tile() {
2371        let array = int32_grid();
2372        // A 2x2 tile whose origin is (x=1, y=1): rows 1-2, columns 1-2.
2373        let mut buffer = [0i32; 4];
2374        let mut dst = buffer.as_mut_ptr().cast::<c_void>();
2375        assert_eq!(
2376            unsafe {
2377                asdf_ndarray_read_tile_2d(
2378                    array,
2379                    1,
2380                    1,
2381                    2,
2382                    2,
2383                    core::ptr::null(),
2384                    ScalarType::Int32 as ScalarTypeAbi,
2385                    &mut dst,
2386                )
2387            },
2388            NdarrayErr::Ok
2389        );
2390        assert_eq!(buffer, [5, 6, 9, 10]);
2391        unsafe { asdf_ndarray_destroy(array) };
2392    }
2393
2394    #[test]
2395    fn a_tile_may_not_run_past_the_edge() {
2396        let array = int32_grid();
2397        let mut buffer = [0i32; 4];
2398        let mut dst = buffer.as_mut_ptr().cast::<c_void>();
2399        assert_eq!(
2400            unsafe {
2401                asdf_ndarray_read_tile_2d(
2402                    array,
2403                    3,
2404                    1,
2405                    2,
2406                    2,
2407                    core::ptr::null(),
2408                    ScalarType::Int32 as ScalarTypeAbi,
2409                    &mut dst,
2410                )
2411            },
2412            NdarrayErr::OutOfBounds
2413        );
2414        unsafe { asdf_ndarray_destroy(array) };
2415    }
2416
2417    #[test]
2418    fn an_ndim_tile_can_allocate_its_own_buffer() {
2419        let array = int32_grid();
2420        let origin: [u64; 2] = [0, 2];
2421        let shape: [u64; 2] = [3, 2];
2422        // A null destination asks the library to allocate.
2423        let mut dst: *mut c_void = core::ptr::null_mut();
2424        assert_eq!(
2425            unsafe {
2426                asdf_ndarray_read_tile_ndim(
2427                    array,
2428                    origin.as_ptr(),
2429                    shape.as_ptr(),
2430                    ScalarType::Int32 as ScalarTypeAbi,
2431                    &mut dst,
2432                )
2433            },
2434            NdarrayErr::Ok
2435        );
2436        assert!(!dst.is_null());
2437        let got = unsafe { core::slice::from_raw_parts(dst.cast::<i32>(), 6) };
2438        assert_eq!(got, [2, 3, 6, 7, 10, 11]);
2439        unsafe { libc::free(dst) };
2440        unsafe { asdf_ndarray_destroy(array) };
2441    }
2442
2443    #[test]
2444    fn an_inline_array_has_no_block() {
2445        let array = int32_grid();
2446        // Built without a file behind it, so there is no block to hand back.
2447        assert!(unsafe { asdf_ndarray_block(array) }.is_null());
2448        unsafe { asdf_ndarray_destroy(array) };
2449    }
2450
2451    #[test]
2452    fn reports_shape_and_sizes_through_the_public_fields() {
2453        let array = int32_array(&[1, 2, 3, 4]);
2454        let view = unsafe { &*array };
2455
2456        assert_eq!(view.ndim, 1);
2457        assert_eq!(view.source, 0);
2458        assert!(!view.shape.is_null());
2459        assert_eq!(unsafe { *view.shape }, 4);
2460        assert_eq!(view.byteorder, ByteOrder::Little as i32);
2461
2462        assert_eq!(unsafe { asdf_ndarray_size(array) }, 4);
2463        assert_eq!(unsafe { asdf_ndarray_nbytes(array) }, 16);
2464
2465        unsafe { asdf_ndarray_destroy(array) };
2466    }
2467
2468    #[test]
2469    fn datatype_size_is_computed_when_left_zero() {
2470        let mut dt = asdf_datatype_t {
2471            type_: scalar_abi(ScalarType::Float64),
2472            size: 0,
2473            name: core::ptr::null(),
2474            byteorder: 0,
2475            ndim: 0,
2476            shape: core::ptr::null(),
2477            nfields: 0,
2478            fields: core::ptr::null(),
2479        };
2480        assert_eq!(unsafe { asdf_datatype_size(&mut dt) }, 8);
2481        // ...and written back, as the header documents.
2482        assert_eq!(dt.size, 8);
2483    }
2484
2485    #[test]
2486    fn scalar_type_names_round_trip() {
2487        for name in ["int8", "uint64", "float32", "complex128", "bool8", "ascii", "ucs4"] {
2488            let c = CString::new(name).unwrap();
2489            let code = unsafe { asdf_scalar_datatype_from_string(c.as_ptr()) };
2490            assert_ne!(code, 0, "{name}");
2491            let back = unsafe { CStr::from_ptr(asdf_scalar_datatype_to_string(code)) };
2492            assert_eq!(back.to_str().unwrap(), name);
2493        }
2494        // An unknown name is UNKNOWN, not a crash.
2495        let bogus = CString::new("float128").unwrap();
2496        assert_eq!(unsafe { asdf_scalar_datatype_from_string(bogus.as_ptr()) }, 0);
2497    }
2498
2499    #[test]
2500    fn reads_the_whole_array_in_its_own_type() {
2501        let array = int32_array(&[10, -20, 30]);
2502        let mut dst: *mut c_void = core::ptr::null_mut();
2503        // ASDF_DATATYPE_SOURCE is UNKNOWN, meaning "keep the source type".
2504        assert_eq!(unsafe { asdf_ndarray_read_all(array, 0, &mut dst) }, NdarrayErr::Ok);
2505        assert!(!dst.is_null());
2506        let values = unsafe { core::slice::from_raw_parts(dst.cast::<i32>(), 3) };
2507        assert_eq!(values, [10, -20, 30]);
2508
2509        unsafe { libc::free(dst) };
2510        unsafe { asdf_ndarray_destroy(array) };
2511    }
2512
2513    #[test]
2514    fn reads_the_whole_array_converted() {
2515        let array = int32_array(&[1, 2, 3]);
2516        let mut dst: *mut c_void = core::ptr::null_mut();
2517        assert_eq!(
2518            unsafe { asdf_ndarray_read_all(array, scalar_abi(ScalarType::Float64), &mut dst) },
2519            NdarrayErr::Ok
2520        );
2521        let values = unsafe { core::slice::from_raw_parts(dst.cast::<f64>(), 3) };
2522        assert_eq!(values, [1.0, 2.0, 3.0]);
2523        unsafe { libc::free(dst) };
2524        unsafe { asdf_ndarray_destroy(array) };
2525    }
2526
2527    #[test]
2528    fn read_all_fills_a_caller_supplied_buffer() {
2529        let array = int32_array(&[7, 8]);
2530        let mut buffer = [0i32; 2];
2531        let mut dst: *mut c_void = buffer.as_mut_ptr().cast();
2532        assert_eq!(unsafe { asdf_ndarray_read_all(array, 0, &mut dst) }, NdarrayErr::Ok);
2533        assert_eq!(buffer, [7, 8]);
2534        unsafe { asdf_ndarray_destroy(array) };
2535    }
2536
2537    #[test]
2538    fn converting_a_value_that_does_not_fit_overflows() {
2539        let array = int32_array(&[1000]);
2540        let mut dst: *mut c_void = core::ptr::null_mut();
2541        assert_eq!(
2542            unsafe { asdf_ndarray_read_all(array, scalar_abi(ScalarType::Int8), &mut dst) },
2543            NdarrayErr::Overflow
2544        );
2545
2546        // Overflow still delivers the buffer -- the caller gets the saturated
2547        // value *and* the report that it did not fit -- so `dst` owns an
2548        // allocation the caller frees, exactly as the header says.
2549        assert!(!dst.is_null());
2550        assert_eq!(unsafe { *dst.cast::<i8>() }, i8::MAX);
2551        unsafe { libc::free(dst) };
2552
2553        unsafe { asdf_ndarray_destroy(array) };
2554    }
2555
2556    #[test]
2557    fn reads_single_elements_by_index() {
2558        let array = int32_array(&[5, 6, 7]);
2559
2560        for (index, expected) in [(0u64, 5i64), (1, 6), (2, 7)] {
2561            let mut err: c_int = -1;
2562            let value = unsafe { asdf_ndarray_read_int64_at(array, &index, &mut err) };
2563            assert_eq!(err, NdarrayErr::Ok as c_int);
2564            assert_eq!(value, expected);
2565        }
2566        unsafe { asdf_ndarray_destroy(array) };
2567    }
2568
2569    #[test]
2570    fn out_of_bounds_reads_are_reported() {
2571        let array = int32_array(&[1, 2]);
2572        let index = 5u64;
2573        let mut err: c_int = -1;
2574        let value = unsafe { asdf_ndarray_read_int64_at(array, &index, &mut err) };
2575        assert_eq!(err, NdarrayErr::OutOfBounds as c_int);
2576        assert_eq!(value, 0, "a failed read returns a zero value");
2577        unsafe { asdf_ndarray_destroy(array) };
2578    }
2579
2580    #[test]
2581    fn single_element_reads_convert_and_overflow() {
2582        let array = int32_array(&[300]);
2583        let index = 0u64;
2584
2585        let mut err: c_int = -1;
2586        let wide = unsafe { asdf_ndarray_read_float64_at(array, &index, &mut err) };
2587        assert_eq!(err, NdarrayErr::Ok as c_int);
2588        assert_eq!(wide, 300.0);
2589
2590        let mut err: c_int = -1;
2591        let narrow = unsafe { asdf_ndarray_read_uint8_at(array, &index, &mut err) };
2592        assert_eq!(err, NdarrayErr::Conversion as c_int);
2593        assert_eq!(narrow, 0);
2594
2595        unsafe { asdf_ndarray_destroy(array) };
2596    }
2597
2598    #[test]
2599    fn float16_reads_go_through_the_bit_pattern() {
2600        let array = int32_array(&[3]);
2601        let index = 0u64;
2602        let mut err: c_int = -1;
2603        let bits =
2604            unsafe { asdf_shim_ndarray_read_float16_bits_at(array.cast(), &index, &mut err) };
2605        assert_eq!(err, NdarrayErr::Ok as c_int);
2606        assert_eq!(half::f16::from_bits(bits).to_f64(), 3.0);
2607        unsafe { asdf_ndarray_destroy(array) };
2608    }
2609
2610    #[test]
2611    fn data_alloc_is_idempotent_and_freeable() {
2612        let array = int32_array(&[0; 4]);
2613        let first = unsafe { asdf_ndarray_data_alloc(array) };
2614        let second = unsafe { asdf_ndarray_data_alloc(array) };
2615        assert!(!first.is_null());
2616        assert_eq!(first, second, "repeated calls return the same buffer");
2617
2618        unsafe { asdf_ndarray_data_dealloc(array) };
2619        // Deallocating twice must be a no-op, as the header documents.
2620        unsafe { asdf_ndarray_data_dealloc(array) };
2621        unsafe { asdf_ndarray_destroy(array) };
2622    }
2623
2624    #[test]
2625    fn data_copy_fills_the_allocated_buffer() {
2626        let array = int32_array(&[0; 3]);
2627        let source: Vec<i32> = vec![11, 22, 33];
2628        assert_eq!(
2629            unsafe { asdf_ndarray_data_copy(array, source.as_ptr().cast()) },
2630            NdarrayErr::Ok
2631        );
2632
2633        let mut size = 0usize;
2634        let data = unsafe { asdf_ndarray_data(array, &mut size) };
2635        assert_eq!(size, 12);
2636        let values = unsafe { core::slice::from_raw_parts(data.cast::<i32>(), 3) };
2637        assert_eq!(values, [11, 22, 33]);
2638
2639        unsafe { asdf_ndarray_data_dealloc(array) };
2640        unsafe { asdf_ndarray_destroy(array) };
2641    }
2642
2643    /// C casts the data pointer to the element type before dereferencing, so
2644    /// an under-aligned buffer is undefined behaviour and, on a
2645    /// strict-alignment target, a bus error. `malloc` gives upstream this for
2646    /// free; holding the bytes in a `Vec<u8>` had quietly given it up. Found
2647    /// by Miri, which rejected `data.cast::<i32>()` over a 2-aligned buffer.
2648    ///
2649    /// Be aware of what this test does and does not prove. Rust's global
2650    /// allocator forwards to `malloc` for any alignment it already satisfies,
2651    /// so on glibc a `Vec<u8>` comes back 16-aligned anyway and this test
2652    /// passed even *with* the bug present. It is a tripwire for an allocator
2653    /// that honours the requested alignment of 1, and a statement of the
2654    /// contract. The gate that actually catches a regression here is Miri,
2655    /// which models the guarantee rather than the platform.
2656    #[test]
2657    fn the_data_pointer_is_aligned_for_any_element_type() {
2658        // Small sizes are the dangerous ones: a large allocation tends to be
2659        // aligned by luck, so a test using only those would pass regardless.
2660        for len in [1usize, 2, 3, 4, 6, 12, 20, 36] {
2661            let array = int32_array(&vec![0; len]);
2662
2663            let allocated = unsafe { asdf_ndarray_data_alloc(array) };
2664            assert!(!allocated.is_null());
2665            assert_eq!(
2666                allocated as usize % 16,
2667                0,
2668                "data_alloc returned a {}-byte buffer aligned to {}",
2669                len * 4,
2670                1 << (allocated as usize).trailing_zeros().min(4)
2671            );
2672
2673            let mut size = 0usize;
2674            let data = unsafe { asdf_ndarray_data(array, &mut size) };
2675            assert_eq!(data as usize % 16, 0, "asdf_ndarray_data returned an unaligned pointer");
2676            assert_eq!(size, len * 4);
2677
2678            unsafe { asdf_ndarray_data_dealloc(array) };
2679
2680            // The other buffer that escapes to C: data read from a file
2681            // rather than allocated by the caller.
2682            let bytes: Vec<u8> = (0..len as i32).flat_map(i32::to_le_bytes).collect();
2683            set_data(array, &bytes);
2684            let read = unsafe { asdf_ndarray_data(array, &mut size) };
2685            assert_eq!(read as usize % 16, 0, "file data was handed out unaligned");
2686            assert_eq!(unsafe { core::slice::from_raw_parts(read.cast::<u8>(), size) }, &bytes[..]);
2687
2688            unsafe { asdf_ndarray_destroy(array) };
2689        }
2690    }
2691
2692    #[test]
2693    fn compression_and_storage_settings() {
2694        let array = int32_array(&[1]);
2695
2696        let zlib = CString::new("zlib").unwrap();
2697        assert_eq!(unsafe { asdf_ndarray_compression_set(array, zlib.as_ptr()) }, 0);
2698        let bogus = CString::new("zstd").unwrap();
2699        assert_eq!(unsafe { asdf_ndarray_compression_set(array, bogus.as_ptr()) }, -1);
2700
2701        // Internal is the default when nothing has been set.
2702        assert_eq!(unsafe { asdf_ndarray_storage(array) }, AsdfArrayStorage::Internal);
2703        unsafe { asdf_ndarray_storage_set(array, AsdfArrayStorage::Inline) };
2704        assert_eq!(unsafe { asdf_ndarray_storage(array) }, AsdfArrayStorage::Inline);
2705
2706        // External is not supported and must leave the setting alone.
2707        unsafe { asdf_ndarray_storage_set(array, AsdfArrayStorage::External) };
2708        assert_eq!(unsafe { asdf_ndarray_storage(array) }, AsdfArrayStorage::Inline);
2709
2710        unsafe { asdf_ndarray_destroy(array) };
2711    }
2712
2713    #[test]
2714    fn a_compound_datatype_exposes_its_fields() {
2715        let doc = parse_document(
2716            "a:\n  source: 0\n  shape: [2]\n  byteorder: little\n  \
2717             datatype:\n    - name: x\n      datatype: float64\n    \
2718             - name: y\n      datatype: int32\n",
2719        )
2720        .unwrap();
2721        let root = doc.root().unwrap();
2722        let parsed = CoreNdarray::parse(&doc, doc.mapping_get(root, "a").unwrap()).unwrap();
2723        let array = make_ndarray(parsed, vec![2]);
2724
2725        let view = unsafe { &*array };
2726        assert_eq!(view.datatype.nfields, 2);
2727        assert!(!view.datatype.fields.is_null());
2728
2729        let fields = unsafe { core::slice::from_raw_parts(view.datatype.fields, 2) };
2730        assert_eq!(fields[0].type_, scalar_abi(ScalarType::Float64));
2731        assert_eq!(fields[0].size, 8);
2732        assert_eq!(unsafe { CStr::from_ptr(fields[0].name) }.to_str().unwrap(), "x");
2733        assert_eq!(fields[1].type_, scalar_abi(ScalarType::Int32));
2734        assert_eq!(unsafe { CStr::from_ptr(fields[1].name) }.to_str().unwrap(), "y");
2735
2736        unsafe { asdf_ndarray_destroy(array) };
2737    }
2738
2739    #[test]
2740    fn multi_dimensional_indexing() {
2741        let doc = parse_document(
2742            "a:\n  source: 0\n  shape: [2, 3]\n  datatype: uint8\n  byteorder: little\n",
2743        )
2744        .unwrap();
2745        let root = doc.root().unwrap();
2746        let parsed = CoreNdarray::parse(&doc, doc.mapping_get(root, "a").unwrap()).unwrap();
2747        let array = make_ndarray(parsed, vec![2, 3]);
2748        set_data(array, &[1, 2, 3, 4, 5, 6]);
2749
2750        // Row-major: [1][2] is the sixth element.
2751        let indices = [1u64, 2];
2752        let mut err: c_int = -1;
2753        let value = unsafe { asdf_ndarray_read_uint8_at(array, indices.as_ptr(), &mut err) };
2754        assert_eq!(err, NdarrayErr::Ok as c_int);
2755        assert_eq!(value, 6);
2756
2757        // Out of range in the second dimension only.
2758        let bad = [0u64, 3];
2759        let mut err: c_int = -1;
2760        unsafe { asdf_ndarray_read_uint8_at(array, bad.as_ptr(), &mut err) };
2761        assert_eq!(err, NdarrayErr::OutOfBounds as c_int);
2762
2763        unsafe { asdf_ndarray_destroy(array) };
2764    }
2765
2766    #[test]
2767    fn err_discriminants_match_the_c_abi() {
2768        assert_eq!(NdarrayErr::Ok as i32, 0);
2769        assert_eq!(NdarrayErr::OutOfBounds as i32, 1);
2770        assert_eq!(NdarrayErr::Oom as i32, 2);
2771        assert_eq!(NdarrayErr::Inval as i32, 3);
2772        assert_eq!(NdarrayErr::Overflow as i32, 4);
2773        assert_eq!(NdarrayErr::Conversion as i32, 5);
2774    }
2775
2776    #[test]
2777    fn null_handles_are_tolerated() {
2778        let null: *mut asdf_ndarray_t = core::ptr::null_mut();
2779        assert_eq!(unsafe { asdf_ndarray_size(null) }, 0);
2780        assert_eq!(unsafe { asdf_ndarray_nbytes(null) }, 0);
2781        assert!(unsafe { asdf_ndarray_data_alloc(null) }.is_null());
2782        unsafe { asdf_ndarray_data_dealloc(null) };
2783        assert_eq!(unsafe { asdf_ndarray_data_copy(null, core::ptr::null()) }, NdarrayErr::Inval);
2784        assert_eq!(unsafe { asdf_datatype_size(core::ptr::null_mut()) }, 0);
2785        unsafe { asdf_ndarray_destroy(null) };
2786
2787        let mut size = 99usize;
2788        assert!(unsafe { asdf_ndarray_data(null, &mut size) }.is_null());
2789        assert_eq!(size, 0);
2790    }
2791    /// Serializing an array must not strand the state we hung off it.
2792    ///
2793    /// `ndarray.h` says assigning an ndarray this way "transfers ownership of
2794    /// its data to ``file``", and callers build the struct as a stack literal
2795    /// -- libasdf's own write example and every libasdf-gwcs serializer do --
2796    /// so there is nowhere for them to call `asdf_ndarray_deinit` from. The
2797    /// payload is copied into the file, so the `_reserved` state is spent and
2798    /// has to go with it; leaving it behind leaked ~1.2 KB per array written.
2799    #[test]
2800    fn serializing_an_array_reclaims_its_internal_state() {
2801        use crate::file_ffi::{asdf_close, asdf_open_mem_ex};
2802
2803        let file = unsafe { asdf_open_mem_ex(core::ptr::null(), 0, core::ptr::null_mut()) };
2804        assert!(!file.is_null());
2805
2806        let shape = [2u64, 2];
2807        let mut array = asdf_ndarray_t {
2808            source: 0,
2809            ndim: 2,
2810            shape: shape.as_ptr(),
2811            // 11 is `ASDF_DATATYPE_FLOAT64`; 60 is `ASDF_BYTEORDER_LITTLE`.
2812            datatype: asdf_datatype_t {
2813                type_: 11,
2814                size: 0,
2815                name: core::ptr::null(),
2816                byteorder: 60,
2817                ndim: 0,
2818                shape: core::ptr::null(),
2819                nfields: 0,
2820                fields: core::ptr::null(),
2821            },
2822            byteorder: 60,
2823            offset: 0,
2824            strides: core::ptr::null(),
2825            _reserved: core::ptr::null_mut(),
2826        };
2827
2828        // One raw pointer throughout, as a C caller has: the entry point takes
2829        // a `*const` but transfers ownership, so it writes through it.
2830        let array_ptr: *mut asdf_ndarray_t = &raw mut array;
2831
2832        // What a gwcs serializer does: pick storage, copy the data in, then
2833        // hand the array to the file.
2834        unsafe { asdf_ndarray_storage_set(array_ptr, AsdfArrayStorage::Inline) };
2835        let data = [1.0f64, 2.0, 3.0, 4.0];
2836        assert_eq!(
2837            unsafe { asdf_ndarray_data_copy(array_ptr, data.as_ptr().cast()) },
2838            NdarrayErr::Ok
2839        );
2840        assert!(
2841            !unsafe { (*array_ptr)._reserved }.is_null(),
2842            "the copy should have attached state"
2843        );
2844
2845        let value = unsafe { asdf_value_of_ndarray(file, array_ptr) };
2846        assert!(!value.is_null());
2847        assert!(
2848            unsafe { (*array_ptr)._reserved }.is_null(),
2849            "the file owns the data now, so the array's state must be released"
2850        );
2851
2852        // The caller's own fields are untouched: they point at storage the
2853        // caller owns, unlike `asdf_ndarray_deinit`, which clears them.
2854        assert_eq!(unsafe { (*array_ptr).ndim }, 2);
2855        assert!(core::ptr::eq(unsafe { (*array_ptr).shape }, shape.as_ptr()));
2856
2857        unsafe { crate::file_ffi::asdf_value_destroy(value) };
2858        unsafe { asdf_close(file) };
2859    }
2860}