Skip to main content

asdf/
file_ffi.rs

1//! `asdf/file.h`: opening, closing and reading values from a file.
2//!
3//! # Handle model
4//!
5//! `asdf_file_t` and `asdf_value_t` are opaque to C, so they are ordinary
6//! Rust types here. The one thing the C contract forces on their design is
7//! string lifetime: `asdf_error` and `asdf_get_string0` hand back a
8//! `const char *` the caller does not own and does not free. The engine's
9//! strings are Rust `String`s, which are not NUL-terminated, so each one
10//! handed out is interned into an arena owned by the file and freed when the
11//! file is closed. That matches libasdf, where such pointers are owned by the
12//! file and invalidated by `asdf_close`.
13
14use alloc::ffi::CString;
15use core::ffi::{CStr, c_char, c_double, c_int, c_void};
16use std::sync::Mutex;
17
18use asdf_core::yaml::{
19    self as asdf_yaml, Document, NodeId, Resolved, ScalarStyle, Schema, Tag, resolve,
20};
21use asdf_core::{PendingBlock, Reader, Writer};
22
23use crate::error_ffi::ErrorState;
24use crate::ffi::{CMallocBuf, write_out};
25use crate::panic::guard;
26use crate::types::{AsdfValueErr, AsdfValueType, asdf_config_t};
27
28/// How a file was opened, mirroring libasdf's `asdf_file_mode_t`.
29#[derive(Clone, Copy, PartialEq, Eq, Debug)]
30enum FileMode {
31    /// Backed by an existing file or buffer, and not writable.
32    ReadOnly,
33    /// Created empty for writing; no input is read.
34    Write,
35    /// Backed by an existing file or buffer *and* writable, which is what
36    /// `asdf_open_mem` and `asdf_open_file(.., "rw")` give.
37    ReadWrite,
38}
39
40impl FileMode {
41    /// Whether values may be placed in this file's tree.
42    fn writable(self) -> bool {
43        self != FileMode::ReadOnly
44    }
45
46    /// The mode named by a `mode` string, or `None` for anything else.
47    ///
48    /// libasdf accepts exactly `r`, `w` and `rw`, case-insensitively, and
49    /// reports anything else as an invalid argument.
50    fn parse(mode: &str) -> Option<Self> {
51        match mode.to_ascii_lowercase().as_str() {
52            "r" => Some(FileMode::ReadOnly),
53            "w" => Some(FileMode::Write),
54            "rw" => Some(FileMode::ReadWrite),
55            _ => None,
56        }
57    }
58}
59
60/// The parts of `asdf_config_t` a file carries with it.
61///
62/// The struct C passes to `asdf_open_*_ex` is the caller's, and may be a
63/// local that goes out of scope the moment the call returns, so the fields
64/// that outlive the call are copied here rather than referenced.
65#[derive(Clone, Copy, Debug, Default)]
66pub(crate) struct FileConfig {
67    /// Where ndarray data is written, overriding each array's own setting.
68    pub array_storage: crate::types::AsdfArrayStorage,
69    /// Element count above which an inline ndarray draws a warning. Zero
70    /// selects the library default; `usize::MAX` suppresses it.
71    pub inline_ndarray_warning_thresh: usize,
72    /// Where log output goes; null means `stderr`.
73    pub log_stream: *mut c_void,
74    /// The minimum severity to emit; `None` selects the default.
75    pub log_level: crate::error_ffi::LogLevel,
76}
77
78/// An open ASDF file. Opaque to C.
79#[derive(Debug)]
80pub struct AsdfFile {
81    reader: Option<Reader>,
82    document: Option<Document>,
83    mode: FileMode,
84    /// What the caller asked for at open time.
85    pub(crate) config: FileConfig,
86    /// Blocks queued for writing.
87    blocks: Vec<PendingBlock>,
88    error: ErrorState,
89    /// C strings handed out to callers, kept alive until the file is closed.
90    ///
91    /// Buffers from `asdf_write_to_mem` are deliberately *not* tracked here:
92    /// that function allocates with `malloc` and the caller frees them, which
93    /// is the contract libasdf documents.
94    interned: Mutex<Vec<CString>>,
95}
96
97/// A handle to one value in a file's tree. Opaque to C.
98#[derive(Debug)]
99pub struct AsdfValue {
100    file: *mut AsdfFile,
101    node: NodeId,
102}
103
104impl AsdfValue {
105    /// Build a handle for a node of `file`.
106    pub(crate) fn new(file: *mut AsdfFile, node: NodeId) -> Self {
107        Self { file, node }
108    }
109}
110
111/// Read the parts of a caller's `asdf_config_t` that a file keeps.
112///
113/// # Safety
114/// `config` must be null or point to a valid `asdf_config_t`.
115pub(crate) unsafe fn read_config(config: *const asdf_config_t) -> FileConfig {
116    if config.is_null() {
117        return FileConfig::default();
118    }
119    let view = unsafe { &*config };
120    FileConfig {
121        array_storage: view.emitter.array_storage,
122        inline_ndarray_warning_thresh: view.emitter.inline_ndarray_warning_thresh,
123        log_stream: view.log.stream,
124        log_level: view.log.level,
125    }
126}
127
128/// A file's configuration, for callers that need it after open.
129pub(crate) fn file_config(file: *const AsdfFile) -> Option<FileConfig> {
130    unsafe { crate::ffi::as_ref(file) }.map(|f| f.config)
131}
132
133/// A handle's error state, for the `ASDF_ERROR_*` macros.
134///
135/// A value's errors are recorded against the file it came from, which is
136/// what `asdf_error_code(file)` reads after `ASDF_ERROR_COMMON(value, ..)`.
137pub(crate) fn error_state(file: *mut AsdfFile) -> Option<&'static ErrorState> {
138    if file.is_null() {
139        return None;
140    }
141    // The C contract has the file outlive every handle taken from it.
142    Some(&unsafe { &*file }.error)
143}
144
145/// The file a value belongs to.
146pub(crate) fn value_file(value: *mut AsdfValue) -> Option<*mut AsdfFile> {
147    if value.is_null() {
148        return None;
149    }
150    let file = unsafe { &*value }.file;
151    (!file.is_null()).then_some(file)
152}
153
154/// The node a value refers to.
155pub(crate) fn value_node(value: *mut AsdfValue) -> Option<NodeId> {
156    if value.is_null() {
157        return None;
158    }
159    Some(unsafe { &*value }.node)
160}
161
162/// The reader backing a file, for the block API.
163pub(crate) fn file_reader(file: *mut AsdfFile) -> Option<&'static Reader> {
164    if file.is_null() {
165        return None;
166    }
167    unsafe { &*file }.reader.as_ref()
168}
169
170/// The queued blocks of a file open for writing.
171pub(crate) fn file_blocks_mut(file: *mut AsdfFile) -> Option<&'static mut Vec<PendingBlock>> {
172    if file.is_null() {
173        return None;
174    }
175    let handle = unsafe { &mut *file };
176    handle.mode.writable().then_some(&mut handle.blocks)
177}
178
179/// A file's tree.
180pub(crate) fn file_document(file: *mut AsdfFile) -> Option<&'static Document> {
181    if file.is_null() {
182        return None;
183    }
184    // The C contract has the file outlive every value taken from it.
185    unsafe { &*file }.document()
186}
187
188/// A file's tree, for mutation, creating it if the file has none yet.
189///
190/// `unsafe { &mut *file }` followed immediately by `document_for_values()`
191/// appeared at a dozen call sites across four modules. Making that judgement
192/// once is the point: the C contract has the file outlive every value taken
193/// from it, which is what licenses handing out a `'static` borrow, and it is
194/// stated here rather than re-asserted at each site.
195pub(crate) fn file_document_mut(file: *mut AsdfFile) -> Option<&'static mut Document> {
196    if file.is_null() {
197        return None;
198    }
199    unsafe { &mut *file }.document_for_values()
200}
201
202/// The document a value belongs to.
203pub(crate) fn value_document(value: *mut AsdfValue) -> Option<&'static Document> {
204    let file = value_file(value)?;
205    // The C contract has the file outlive every value taken from it.
206    unsafe { &*file }.document()
207}
208
209impl AsdfFile {
210    fn new(mode: FileMode) -> Self {
211        Self {
212            reader: None,
213            document: None,
214            mode,
215            config: FileConfig::default(),
216            blocks: Vec::new(),
217            error: ErrorState::default(),
218            interned: Mutex::new(Vec::new()),
219        }
220    }
221
222    /// A file opened for writing, with the empty tree it starts from.
223    ///
224    /// The tree exists from the moment the file is opened rather than
225    /// appearing with the first `asdf_set_*`, because upstream's does:
226    /// `asdf_get_value(asdf_open(NULL), "")` hands back the root, and its
227    /// own tests rely on that.
228    fn new_for_writing() -> Self {
229        let mut file = Self::new(FileMode::Write);
230        file.document_for_write();
231        file
232    }
233
234    /// The tree, creating an empty one if the file does not have it yet.
235    ///
236    /// A file opened for writing starts with no tree; the first `asdf_set_*`
237    /// call brings one into being, as libasdf's own write example expects.
238    fn document_for_write(&mut self) -> Option<&mut Document> {
239        if !self.mode.writable() {
240            return None;
241        }
242        if self.document.is_none() {
243            let mut doc = Document::new_asdf();
244            let root = doc.add(asdf_core::yaml::Node::mapping());
245            // Every ASDF tree's root carries the core/asdf tag.
246            doc.node_mut(root).tag = Some(Tag::parse(ASDF_ROOT_TAG));
247            doc.set_root(root);
248            self.document = Some(doc);
249        }
250        self.document.as_mut()
251    }
252
253    /// Intern a string and return a pointer valid until the file is closed.
254    pub(crate) fn intern(&self, s: &str) -> *const c_char {
255        let Ok(c) = CString::new(s) else {
256            return core::ptr::null();
257        };
258        let mut arena = self.interned.lock().unwrap_or_else(|e| e.into_inner());
259        arena.push(c);
260        arena.last().map_or(core::ptr::null(), |c| c.as_ptr())
261    }
262
263    /// The file's parsed tree, if it has one.
264    fn document(&self) -> Option<&Document> {
265        self.document.as_ref()
266    }
267
268    /// The tree, for allocating nodes into.
269    ///
270    /// Unlike [`AsdfFile::document_for_write`] this does not require the file
271    /// to be open for writing: building a value is allowed on any file, and
272    /// it is *placing* one in the tree at a path that needs write mode. A
273    /// read-only file with no tree gets an empty one, so a caller can still
274    /// construct values against it.
275    pub(crate) fn document_for_values(&mut self) -> Option<&mut Document> {
276        if self.document.is_none() {
277            self.document = Some(Document::new_asdf());
278        }
279        self.document.as_mut()
280    }
281}
282
283/// The tag every ASDF tree's root carries.
284const ASDF_ROOT_TAG: &str = "tag:stsci.edu:asdf/core/asdf-1.1.0";
285
286/// Attach a configuration to a freshly opened handle.
287fn with_config(file: *mut AsdfFile, settings: FileConfig) -> *mut AsdfFile {
288    if !file.is_null() {
289        unsafe { (*file).config = settings };
290    }
291    file
292}
293
294/// Build a file handle around a reader.
295fn open_reader(reader: Reader, mode: FileMode) -> *mut AsdfFile {
296    let mut file = AsdfFile::new(mode);
297    match reader.tree() {
298        Ok(doc) => file.document = doc,
299        Err(e) => {
300            // A tree that will not parse is reported, but the file still
301            // opens so that its blocks remain reachable.
302            file.error.set_error(&e);
303        }
304    }
305    file.reader = Some(reader);
306    Box::into_raw(Box::new(file))
307}
308
309/// Open a file by path.
310///
311/// # Safety
312/// `filename` and `mode` must be valid NUL-terminated strings or null.
313/// `config` must be null or point to a valid `asdf_config_t`. The result must
314/// be released with [`asdf_close`].
315#[unsafe(no_mangle)]
316pub unsafe extern "C" fn asdf_open_file_ex(
317    filename: *const c_char,
318    mode: *const c_char,
319    config: *mut asdf_config_t,
320) -> *mut AsdfFile {
321    guard("asdf_open_file_ex", core::ptr::null_mut(), || {
322        let settings = unsafe { read_config(config) };
323        if mode.is_null() {
324            return core::ptr::null_mut();
325        }
326        let text = unsafe { CStr::from_ptr(mode) }.to_string_lossy().into_owned();
327        let Some(mode) = FileMode::parse(&text) else {
328            return core::ptr::null_mut();
329        };
330        // A write-only open reads nothing, so it does not touch `filename` at
331        // all -- upstream ignores it too, and the destination is named later
332        // by `asdf_write_to`.
333        if mode == FileMode::Write {
334            let mut file = AsdfFile::new_for_writing();
335            file.config = settings;
336            return Box::into_raw(Box::new(file));
337        }
338        if filename.is_null() {
339            return core::ptr::null_mut();
340        }
341        let path = unsafe { CStr::from_ptr(filename) }.to_string_lossy().into_owned();
342        match Reader::open(&path) {
343            Ok(reader) => with_config(open_reader(reader, mode), settings),
344            Err(_) => core::ptr::null_mut(),
345        }
346    })
347}
348
349/// Open a file from an in-memory buffer.
350///
351/// # Safety
352/// `buf` must point to at least `size` readable bytes, or be null with a
353/// `size` of 0. The result must be released with [`asdf_close`].
354#[unsafe(no_mangle)]
355pub unsafe extern "C" fn asdf_open_mem_ex(
356    buf: *const c_void,
357    size: usize,
358    config: *mut asdf_config_t,
359) -> *mut AsdfFile {
360    guard("asdf_open_mem_ex", core::ptr::null_mut(), || {
361        let settings = unsafe { read_config(config) };
362        // `asdf_open(NULL)` expands to `asdf_open_mem(NULL, 0)`, which is how
363        // the C API asks for a new, empty file to write into.
364        if buf.is_null() || size == 0 {
365            let mut file = AsdfFile::new_for_writing();
366            file.config = settings;
367            return Box::into_raw(Box::new(file));
368        }
369        // A buffer-backed file is read-*write* upstream: its tree may be
370        // edited and written out elsewhere.
371        let bytes = unsafe { core::slice::from_raw_parts(buf.cast::<u8>(), size) }.to_vec();
372        match Reader::from_bytes(bytes) {
373            Ok(reader) => with_config(open_reader(reader, FileMode::ReadWrite), settings),
374            Err(_) => core::ptr::null_mut(),
375        }
376    })
377}
378
379/// Open a file from an already-open `FILE *`.
380///
381/// The stream is read to its end; the caller keeps ownership of it.
382///
383/// # Safety
384/// `fp` must be a `FILE *` open for reading, or null. `filename` is only used
385/// in messages and may be null. The result must be released with
386/// [`asdf_close`].
387#[unsafe(no_mangle)]
388pub unsafe extern "C" fn asdf_open_fp_ex(
389    fp: *mut c_void,
390    filename: *const c_char,
391    config: *mut asdf_config_t,
392) -> *mut AsdfFile {
393    guard("asdf_open_fp_ex", core::ptr::null_mut(), || {
394        let _ = filename;
395        let settings = unsafe { read_config(config) };
396        if fp.is_null() {
397            return core::ptr::null_mut();
398        }
399
400        // Read the stream in whole chunks through libc, since the caller owns
401        // the FILE and may have already consumed part of it.
402        let mut bytes = Vec::new();
403        let mut chunk = [0u8; 8192];
404        loop {
405            let read = unsafe {
406                libc::fread(
407                    chunk.as_mut_ptr().cast::<c_void>(),
408                    1,
409                    chunk.len(),
410                    fp.cast::<libc::FILE>(),
411                )
412            };
413            if read == 0 {
414                break;
415            }
416            bytes.extend_from_slice(&chunk[..read]);
417        }
418        if bytes.is_empty() {
419            return core::ptr::null_mut();
420        }
421        match Reader::from_bytes(bytes) {
422            Ok(reader) => with_config(open_reader(reader, FileMode::ReadOnly), settings),
423            Err(_) => core::ptr::null_mut(),
424        }
425    })
426}
427
428/// Close a file and release everything it owns.
429///
430/// Any `const char *` obtained from this file becomes invalid.
431///
432/// # Safety
433/// `file` must be null or have come from one of the openers, and must not be
434/// used afterwards.
435#[unsafe(no_mangle)]
436pub unsafe extern "C" fn asdf_close(file: *mut AsdfFile) {
437    guard("asdf_close", (), || {
438        if !file.is_null() {
439            drop(unsafe { Box::from_raw(file) });
440        }
441    })
442}
443
444/// The most recent error message, or null if there is none.
445///
446/// # Safety
447/// `file` must be null or a valid file handle. The returned pointer is owned
448/// by the file and is invalidated by the next error or by `asdf_close`.
449#[unsafe(no_mangle)]
450pub unsafe extern "C" fn asdf_error(file: *mut AsdfFile) -> *const c_char {
451    guard("asdf_error", core::ptr::null(), || {
452        if file.is_null() {
453            return core::ptr::null();
454        }
455        unsafe { &*file }.error.message_ptr()
456    })
457}
458
459/// The most recent error code.
460///
461/// # Safety
462/// `file` must be null or a valid file handle.
463#[unsafe(no_mangle)]
464pub unsafe extern "C" fn asdf_error_code(file: *mut AsdfFile) -> c_int {
465    guard("asdf_error_code", 0, || {
466        if file.is_null() {
467            return 0;
468        }
469        unsafe { &*file }.error.code()
470    })
471}
472
473/// The OS `errno` behind the most recent error, when it was a system error.
474///
475/// # Safety
476/// `file` must be null or a valid file handle.
477#[unsafe(no_mangle)]
478pub unsafe extern "C" fn asdf_error_errno(file: *mut AsdfFile) -> c_int {
479    guard("asdf_error_errno", 0, || {
480        if file.is_null() {
481            return 0;
482        }
483        unsafe { &*file }.error.errno()
484    })
485}
486
487/// Look up a node by path.
488fn lookup(file: *mut AsdfFile, path: *const c_char) -> Option<(&'static Document, NodeId)> {
489    if file.is_null() {
490        return None;
491    }
492    // The handle outlives every value taken from it, by the C contract.
493    let f: &'static AsdfFile = unsafe { &*file };
494    let doc = f.document()?;
495    let path = if path.is_null() {
496        String::new()
497    } else {
498        unsafe { CStr::from_ptr(path) }.to_string_lossy().into_owned()
499    };
500    let node = doc.lookup_str(&path)?;
501    Some((doc, node))
502}
503
504/// Get a handle to the value at `path`.
505///
506/// # Safety
507/// `file` must be a valid file handle and `path` a valid NUL-terminated
508/// string or null. The result must be released with `asdf_value_destroy`.
509#[unsafe(no_mangle)]
510pub unsafe extern "C" fn asdf_get_value(
511    file: *mut AsdfFile,
512    path: *const c_char,
513) -> *mut AsdfValue {
514    guard("asdf_get_value", core::ptr::null_mut(), || match lookup(file, path) {
515        Some((_, node)) => Box::into_raw(Box::new(AsdfValue { file, node })),
516        None => core::ptr::null_mut(),
517    })
518}
519
520/// Release a value handle.
521///
522/// This does not free anything the value refers to; the file owns that.
523///
524/// # Safety
525/// `value` must be null or have come from a value-producing call, and must
526/// not be used afterwards.
527#[unsafe(no_mangle)]
528pub unsafe extern "C" fn asdf_value_destroy(value: *mut AsdfValue) {
529    guard("asdf_value_destroy", (), || {
530        if !value.is_null() {
531            drop(unsafe { Box::from_raw(value) });
532        }
533    })
534}
535
536/// Borrow a value's document and node.
537fn value_parts(value: *mut AsdfValue) -> Option<(&'static AsdfFile, &'static Document, NodeId)> {
538    if value.is_null() {
539        return None;
540    }
541    let v = unsafe { &*value };
542    if v.file.is_null() {
543        return None;
544    }
545    let f: &'static AsdfFile = unsafe { &*v.file };
546    let doc = f.document()?;
547    Some((f, doc, v.node))
548}
549
550/// The resolved type of a value.
551///
552/// # Safety
553/// `value` must be null or a valid value handle.
554#[unsafe(no_mangle)]
555pub unsafe extern "C" fn asdf_value_get_type(value: *mut AsdfValue) -> AsdfValueType {
556    guard("asdf_value_get_type", AsdfValueType::Unknown, || {
557        let Some((_, doc, node)) = value_parts(value) else {
558            return AsdfValueType::Unknown;
559        };
560        AsdfValueType::from(node_type(doc, node))
561    })
562}
563
564/// The value type of a node, applying libasdf's resolution rules.
565fn node_type(doc: &Document, node: NodeId) -> asdf_yaml::ValueType {
566    use asdf_yaml::{NodeData, ValueType};
567    let resolved = doc.resolved(node);
568    match &resolved.data {
569        NodeData::Mapping { .. } => ValueType::Mapping,
570        NodeData::Sequence { .. } => ValueType::Sequence,
571        NodeData::Scalar { value, style } => {
572            // An explicit YAML common-schema tag short-circuits inference.
573            if let Some(tag) = doc.tag_of(node)
574                && tag.is_yaml_builtin()
575                && let Some(r) =
576                    asdf_yaml::scalar::resolve_tagged(value, tag.suffix(), Schema::Libasdf)
577            {
578                return r.value_type();
579            }
580            resolve(value, *style, Schema::Libasdf).value_type()
581        }
582        NodeData::Alias(_) => ValueType::Unknown,
583    }
584}
585
586/// The tag on a value, or null if it has none.
587///
588/// # Safety
589/// `value` must be null or a valid value handle. The returned pointer is
590/// owned by the file.
591#[unsafe(no_mangle)]
592pub unsafe extern "C" fn asdf_value_tag(value: *mut AsdfValue) -> *const c_char {
593    guard("asdf_value_tag", core::ptr::null(), || {
594        let Some((file, doc, node)) = value_parts(value) else {
595            return core::ptr::null();
596        };
597        match doc.tag_of(node) {
598            Some(tag) => file.intern(&tag.full()),
599            None => core::ptr::null(),
600        }
601    })
602}
603
604/// The name libasdf reports for a value type.
605///
606/// Taken as an `int` rather than the enum because C may pass any value, and
607/// holding one outside the enum's range in a Rust enum is undefined
608/// behaviour. The two are ABI-identical.
609///
610/// # Safety
611/// Always safe; the returned pointer refers to a `'static` string.
612#[unsafe(no_mangle)]
613pub extern "C" fn asdf_value_type_string(value_type: c_int) -> *const c_char {
614    let Some(value_type) = AsdfValueType::from_i32(value_type) else {
615        return c"<unknown>".as_ptr();
616    };
617    // Static NUL-terminated names, so no allocation and no lifetime concern.
618    let s: &'static CStr = match value_type {
619        AsdfValueType::Unknown => c"<unknown>",
620        AsdfValueType::Sequence => c"sequence",
621        AsdfValueType::Mapping => c"mapping",
622        AsdfValueType::Scalar => c"scalar",
623        AsdfValueType::String => c"string",
624        AsdfValueType::Bool => c"bool",
625        AsdfValueType::Null => c"null",
626        AsdfValueType::Int8 => c"int8",
627        AsdfValueType::Int16 => c"int16",
628        AsdfValueType::Int32 => c"int32",
629        AsdfValueType::Int64 => c"int64",
630        AsdfValueType::Uint8 => c"uint8",
631        AsdfValueType::Uint16 => c"uint16",
632        AsdfValueType::Uint32 => c"uint32",
633        AsdfValueType::Uint64 => c"uint64",
634        AsdfValueType::Float => c"float",
635        AsdfValueType::Double => c"double",
636        AsdfValueType::Extension => c"<extension>",
637    };
638    s.as_ptr()
639}
640
641/// Resolve a path to a scalar and its resolution.
642fn resolve_at(file: *mut AsdfFile, path: *const c_char) -> Option<(Resolved, String, ScalarStyle)> {
643    let (doc, node) = lookup(file, path)?;
644    let resolved_node = doc.resolved(node);
645    let (text, style) = match &resolved_node.data {
646        asdf_yaml::NodeData::Scalar { value, style } => (value.clone(), *style),
647        _ => return None,
648    };
649    // An explicit common-schema tag wins over inference.
650    if let Some(tag) = doc.tag_of(node)
651        && tag.is_yaml_builtin()
652        && let Some(r) = asdf_yaml::scalar::resolve_tagged(&text, tag.suffix(), Schema::Libasdf)
653    {
654        return Some((r, text, style));
655    }
656    Some((resolve(&text, style, Schema::Libasdf), text, style))
657}
658
659/// Generate a typed integer getter matching libasdf's semantics.
660macro_rules! int_getter {
661    ($name:ident, $ty:ty) => {
662        /// Read the value at `path` as this integer type.
663        ///
664        /// Returns `Overflow` when the value is numeric but does not fit, and
665        /// `TypeMismatch` when it is not an integer at all.
666        ///
667        /// # Safety
668        /// `file` must be a valid file handle, `path` a valid string or null,
669        /// and `out` a writable pointer or null.
670        #[unsafe(no_mangle)]
671        pub unsafe extern "C" fn $name(
672            file: *mut AsdfFile,
673            path: *const c_char,
674            out: *mut $ty,
675        ) -> AsdfValueErr {
676            guard(stringify!($name), AsdfValueErr::Unknown, || {
677                let Some((resolved, _, _)) = resolve_at(file, path) else {
678                    return AsdfValueErr::NotFound;
679                };
680                // Truncated as a C cast would, written even when it does
681                // not fit; see `asdf_value_as_<type>`, which this mirrors.
682                let (truncated, fits): ($ty, bool) = match resolved {
683                    Resolved::Uint(v, _) => (v as $ty, <$ty>::try_from(v).is_ok()),
684                    Resolved::Int(v, _) => (v as $ty, <$ty>::try_from(v).is_ok()),
685                    // See `asdf_value_as_<type>`.
686                    Resolved::IntOverflow => return AsdfValueErr::Overflow,
687                    _ => return AsdfValueErr::TypeMismatch,
688                };
689                if !out.is_null() {
690                    unsafe { write_out(out, truncated) };
691                }
692                if fits { AsdfValueErr::Ok } else { AsdfValueErr::Overflow }
693            })
694        }
695    };
696}
697
698int_getter!(asdf_get_int8, i8);
699int_getter!(asdf_get_int16, i16);
700int_getter!(asdf_get_int32, i32);
701int_getter!(asdf_get_int64, i64);
702int_getter!(asdf_get_uint8, u8);
703int_getter!(asdf_get_uint16, u16);
704int_getter!(asdf_get_uint32, u32);
705int_getter!(asdf_get_uint64, u64);
706
707/// Read the value at `path` as a `double`.
708///
709/// Integers are accepted, since a whole number is a valid double.
710///
711/// # Safety
712/// See the integer getters.
713#[unsafe(no_mangle)]
714pub unsafe extern "C" fn asdf_get_double(
715    file: *mut AsdfFile,
716    path: *const c_char,
717    out: *mut c_double,
718) -> AsdfValueErr {
719    guard("asdf_get_double", AsdfValueErr::Unknown, || {
720        let Some((resolved, _, _)) = resolve_at(file, path) else {
721            return AsdfValueErr::NotFound;
722        };
723        let value = match resolved {
724            Resolved::Double(d) => d,
725            Resolved::Uint(v, _) => v as f64,
726            Resolved::Int(v, _) => v as f64,
727            _ => return AsdfValueErr::TypeMismatch,
728        };
729        if !out.is_null() {
730            unsafe { write_out(out, value) };
731        }
732        AsdfValueErr::Ok
733    })
734}
735
736/// Read the value at `path` as a `float`.
737///
738/// # Safety
739/// See the integer getters.
740#[unsafe(no_mangle)]
741pub unsafe extern "C" fn asdf_get_float(
742    file: *mut AsdfFile,
743    path: *const c_char,
744    out: *mut f32,
745) -> AsdfValueErr {
746    guard("asdf_get_float", AsdfValueErr::Unknown, || {
747        let Some((resolved, _, _)) = resolve_at(file, path) else {
748            return AsdfValueErr::NotFound;
749        };
750        let value = match resolved {
751            Resolved::Double(d) => d,
752            Resolved::Uint(v, _) => v as f64,
753            Resolved::Int(v, _) => v as f64,
754            _ => return AsdfValueErr::TypeMismatch,
755        };
756        if !out.is_null() {
757            unsafe { write_out(out, value as f32) };
758        }
759        AsdfValueErr::Ok
760    })
761}
762
763/// Read the value at `path` as a boolean.
764///
765/// # Safety
766/// See the integer getters.
767#[unsafe(no_mangle)]
768pub unsafe extern "C" fn asdf_get_bool(
769    file: *mut AsdfFile,
770    path: *const c_char,
771    out: *mut bool,
772) -> AsdfValueErr {
773    guard("asdf_get_bool", AsdfValueErr::Unknown, || {
774        let Some((resolved, text, _)) = resolve_at(file, path) else {
775            return AsdfValueErr::NotFound;
776        };
777        // libasdf resolves integers before booleans, so a bare 0 or 1 arrives
778        // here as an integer; its documented behaviour is to accept those two
779        // as booleans when read as one.
780        let value = match resolved {
781            Resolved::Bool(b) => b,
782            Resolved::Uint(0, _) => false,
783            Resolved::Uint(1, _) => true,
784            _ => {
785                let _ = text;
786                return AsdfValueErr::TypeMismatch;
787            }
788        };
789        if !out.is_null() {
790            unsafe { write_out(out, value) };
791        }
792        AsdfValueErr::Ok
793    })
794}
795
796/// Read the value at `path` as a NUL-terminated string.
797///
798/// # Safety
799/// `file` must be a valid file handle, `path` a valid string or null, and
800/// `out` a writable pointer or null. The string is owned by the file.
801#[unsafe(no_mangle)]
802pub unsafe extern "C" fn asdf_get_string0(
803    file: *mut AsdfFile,
804    path: *const c_char,
805    out: *mut *const c_char,
806) -> AsdfValueErr {
807    guard("asdf_get_string0", AsdfValueErr::Unknown, || {
808        let Some((resolved, text, _)) = resolve_at(file, path) else {
809            return AsdfValueErr::NotFound;
810        };
811        if !matches!(resolved, Resolved::String) {
812            return AsdfValueErr::TypeMismatch;
813        }
814        let ptr = unsafe { &*file }.intern(&text);
815        if ptr.is_null() {
816            return AsdfValueErr::Oom;
817        }
818        if !out.is_null() {
819            unsafe { write_out(out, ptr) };
820        }
821        AsdfValueErr::Ok
822    })
823}
824
825/// Whether the value at `path` is null.
826///
827/// # Safety
828/// See the integer getters.
829#[unsafe(no_mangle)]
830pub unsafe extern "C" fn asdf_is_null(file: *mut AsdfFile, path: *const c_char) -> bool {
831    guard("asdf_is_null", false, || matches!(resolve_at(file, path), Some((Resolved::Null, _, _))))
832}
833
834/// Generate a predicate over a value's resolved type.
835macro_rules! type_predicate {
836    ($name:ident, $variant:ident) => {
837        /// Whether the value at `path` has this type.
838        ///
839        /// # Safety
840        /// See the integer getters.
841        #[unsafe(no_mangle)]
842        pub unsafe extern "C" fn $name(file: *mut AsdfFile, path: *const c_char) -> bool {
843            guard(stringify!($name), false, || match lookup(file, path) {
844                Some((doc, node)) => {
845                    AsdfValueType::from(node_type(doc, node)) == AsdfValueType::$variant
846                }
847                None => false,
848            })
849        }
850    };
851}
852
853type_predicate!(asdf_is_mapping, Mapping);
854type_predicate!(asdf_is_sequence, Sequence);
855type_predicate!(asdf_is_string, String);
856type_predicate!(asdf_is_bool, Bool);
857
858/// The number of blocks in the file.
859///
860/// # Safety
861/// `file` must be null or a valid file handle.
862#[unsafe(no_mangle)]
863pub unsafe extern "C" fn asdf_block_count(file: *mut AsdfFile) -> usize {
864    guard("asdf_block_count", 0, || {
865        if file.is_null() {
866            return 0;
867        }
868        let handle = unsafe { &*file };
869        // A file opened for writing has no reader; its blocks are the ones
870        // queued so far.
871        match &handle.reader {
872            Some(reader) => reader.block_count(),
873            None => handle.blocks.len(),
874        }
875    })
876}
877
878// ---- Writing --------------------------------------------------------
879
880/// Resolve a file handle for mutation.
881fn write_target(file: *mut AsdfFile) -> Option<&'static mut AsdfFile> {
882    if file.is_null() {
883        return None;
884    }
885    Some(unsafe { &mut *file })
886}
887
888/// Set a node at `path`, creating intermediate mappings as needed.
889fn set_node(
890    file: *mut AsdfFile,
891    path: *const c_char,
892    make: impl FnOnce(&mut Document) -> NodeId,
893) -> AsdfValueErr {
894    let Some(handle) = write_target(file) else {
895        return AsdfValueErr::Unknown;
896    };
897    if !handle.mode.writable() {
898        // libasdf reports a write to a read-only file distinctly from a
899        // type problem, so a caller can tell the two apart.
900        return AsdfValueErr::ReadOnly;
901    }
902    let path = if path.is_null() {
903        String::new()
904    } else {
905        unsafe { CStr::from_ptr(path) }.to_string_lossy().into_owned()
906    };
907    let Some(doc) = handle.document_for_write() else {
908        return AsdfValueErr::Unknown;
909    };
910    let node = make(doc);
911    match doc.insert_at_str(&path, node) {
912        Ok(_) => AsdfValueErr::Ok,
913        Err(_) => AsdfValueErr::Unknown,
914    }
915}
916
917/// Attach an existing value's node at `path`.
918///
919/// Used by the extension layer, which builds a value through an extension's
920/// serializer and then places it in the tree.
921///
922/// # Safety
923/// `file` must be a file handle open for writing; `path` a valid
924/// NUL-terminated string or null; `value` a valid value handle.
925pub(crate) unsafe fn set_value_at(
926    file: *mut AsdfFile,
927    path: *const c_char,
928    value: *mut AsdfValue,
929) -> AsdfValueErr {
930    let Some(node) = crate::file_ffi::value_node(value) else {
931        return AsdfValueErr::Unknown;
932    };
933    set_node(file, path, |_| node)
934}
935
936/// Generate a scalar setter.
937macro_rules! scalar_setter {
938    ($name:ident, $ty:ty) => {
939        /// Set the value at `path`.
940        ///
941        /// Intermediate mappings are created as needed, so setting
942        /// `powers/squares` in an empty tree also creates `powers`.
943        ///
944        /// # Safety
945        /// `file` must be a file handle opened for writing and `path` a valid
946        /// NUL-terminated string or null.
947        #[unsafe(no_mangle)]
948        pub unsafe extern "C" fn $name(
949            file: *mut AsdfFile,
950            path: *const c_char,
951            value: $ty,
952        ) -> AsdfValueErr {
953            guard(stringify!($name), AsdfValueErr::Unknown, || {
954                set_node(file, path, |doc| doc.add_scalar(value.to_string()))
955            })
956        }
957    };
958}
959
960scalar_setter!(asdf_set_int8, i8);
961scalar_setter!(asdf_set_int16, i16);
962scalar_setter!(asdf_set_int32, i32);
963scalar_setter!(asdf_set_int64, i64);
964scalar_setter!(asdf_set_uint8, u8);
965scalar_setter!(asdf_set_uint16, u16);
966scalar_setter!(asdf_set_uint32, u32);
967scalar_setter!(asdf_set_uint64, u64);
968
969/// Set a NUL-terminated string at `path`.
970///
971/// The value is written quoted, so a string of digits reads back as a string
972/// rather than as a number.
973///
974/// # Safety
975/// `file` must be a file handle opened for writing; `path` and `value` must
976/// be valid NUL-terminated strings or null.
977#[unsafe(no_mangle)]
978pub unsafe extern "C" fn asdf_set_string0(
979    file: *mut AsdfFile,
980    path: *const c_char,
981    value: *const c_char,
982) -> AsdfValueErr {
983    guard("asdf_set_string0", AsdfValueErr::Unknown, || {
984        if value.is_null() {
985            return AsdfValueErr::Unknown;
986        }
987        let text = unsafe { CStr::from_ptr(value) }.to_string_lossy().into_owned();
988        set_node(file, path, |doc| {
989            // Plain style is fine for text that cannot be mistaken for
990            // another type; anything else is quoted so it stays a string.
991            let style = match asdf_yaml::resolve(&text, ScalarStyle::Plain, Schema::Libasdf) {
992                Resolved::String => ScalarStyle::Plain,
993                _ => ScalarStyle::SingleQuoted,
994            };
995            doc.add_scalar_styled(text, style)
996        })
997    })
998}
999
1000/// Set a boolean at `path`.
1001///
1002/// # Safety
1003/// See the integer setters.
1004#[unsafe(no_mangle)]
1005pub unsafe extern "C" fn asdf_set_bool(
1006    file: *mut AsdfFile,
1007    path: *const c_char,
1008    value: bool,
1009) -> AsdfValueErr {
1010    guard("asdf_set_bool", AsdfValueErr::Unknown, || {
1011        set_node(file, path, |doc| doc.add_scalar(if value { "true" } else { "false" }))
1012    })
1013}
1014
1015/// Set a null at `path`.
1016///
1017/// # Safety
1018/// See the integer setters.
1019#[unsafe(no_mangle)]
1020pub unsafe extern "C" fn asdf_set_null(file: *mut AsdfFile, path: *const c_char) -> AsdfValueErr {
1021    guard("asdf_set_null", AsdfValueErr::Unknown, || {
1022        set_node(file, path, |doc| doc.add_scalar("null"))
1023    })
1024}
1025
1026/// Set a `double` at `path`.
1027///
1028/// # Safety
1029/// See the integer setters.
1030#[unsafe(no_mangle)]
1031pub unsafe extern "C" fn asdf_set_double(
1032    file: *mut AsdfFile,
1033    path: *const c_char,
1034    value: c_double,
1035) -> AsdfValueErr {
1036    guard("asdf_set_double", AsdfValueErr::Unknown, || {
1037        set_node(file, path, |doc| doc.add_scalar(asdf_core::core::elements::format_float(value)))
1038    })
1039}
1040
1041/// Set a `float` at `path`.
1042///
1043/// # Safety
1044/// See the integer setters.
1045#[unsafe(no_mangle)]
1046pub unsafe extern "C" fn asdf_set_float(
1047    file: *mut AsdfFile,
1048    path: *const c_char,
1049    value: f32,
1050) -> AsdfValueErr {
1051    guard("asdf_set_float", AsdfValueErr::Unknown, || {
1052        set_node(file, path, |doc| {
1053            doc.add_scalar(asdf_core::core::elements::format_float(f64::from(value)))
1054        })
1055    })
1056}
1057
1058/// Assemble the file's bytes.
1059fn serialize(handle: &AsdfFile) -> Result<Vec<u8>, asdf_core::Error> {
1060    let mut writer = match &handle.document {
1061        Some(doc) => Writer::from_document(doc.clone()),
1062        None => Writer::new(),
1063    };
1064    for block in &handle.blocks {
1065        writer.add_block(block.clone());
1066    }
1067    writer.to_bytes()
1068}
1069
1070/// Write the file to a filesystem path.
1071///
1072/// # Safety
1073/// `file` must be a valid file handle and `filename` a valid NUL-terminated
1074/// string.
1075#[unsafe(no_mangle)]
1076pub unsafe extern "C" fn asdf_write_to_file(file: *mut AsdfFile, filename: *const c_char) -> c_int {
1077    guard("asdf_write_to_file", -1, || {
1078        if file.is_null() || filename.is_null() {
1079            return -1;
1080        }
1081        let handle = unsafe { &*file };
1082        let path = unsafe { CStr::from_ptr(filename) }.to_string_lossy().into_owned();
1083
1084        match serialize(handle).and_then(|bytes| Ok(std::fs::write(&path, bytes)?)) {
1085            Ok(()) => 0,
1086            Err(e) => {
1087                handle.error.set_error(&e);
1088                -1
1089            }
1090        }
1091    })
1092}
1093
1094/// Write the file to an open `FILE *`.
1095///
1096/// # Safety
1097/// `file` must be a valid file handle and `fp` a `FILE *` open for writing.
1098#[unsafe(no_mangle)]
1099pub unsafe extern "C" fn asdf_write_to_fp(file: *mut AsdfFile, fp: *mut c_void) -> c_int {
1100    guard("asdf_write_to_fp", -1, || {
1101        if file.is_null() || fp.is_null() {
1102            return -1;
1103        }
1104        let handle = unsafe { &*file };
1105        let bytes = match serialize(handle) {
1106            Ok(b) => b,
1107            Err(e) => {
1108                handle.error.set_error(&e);
1109                return -1;
1110            }
1111        };
1112        let written = unsafe {
1113            libc::fwrite(bytes.as_ptr().cast::<c_void>(), 1, bytes.len(), fp.cast::<libc::FILE>())
1114        };
1115        if written == bytes.len() { 0 } else { -1 }
1116    })
1117}
1118
1119/// Write the file into a freshly allocated buffer.
1120///
1121/// The buffer is allocated with `malloc`, so the caller frees it with
1122/// `free`. This matches libasdf, whose callers own the result.
1123///
1124/// # Safety
1125/// `file` must be a valid file handle; `buf` and `size` must be writable.
1126#[unsafe(no_mangle)]
1127pub unsafe extern "C" fn asdf_write_to_mem(
1128    file: *mut AsdfFile,
1129    buf: *mut *mut c_void,
1130    size: *mut usize,
1131) -> c_int {
1132    guard("asdf_write_to_mem", -1, || {
1133        if file.is_null() || buf.is_null() || size.is_null() {
1134            return -1;
1135        }
1136        let handle = unsafe { &*file };
1137        let bytes = match serialize(handle) {
1138            Ok(b) => b,
1139            Err(e) => {
1140                handle.error.set_error(&e);
1141                return -1;
1142            }
1143        };
1144
1145        // The header specifies `malloc` here, because it tells the caller to
1146        // release the buffer with `free`. `CMallocBuf` is the one place the
1147        // crate honours that, and it frees the allocation itself if anything
1148        // between here and `into_raw` returns early.
1149        let Some(allocation) = CMallocBuf::copy_from(&bytes) else {
1150            return -1;
1151        };
1152        unsafe { write_out(size, allocation.len()) };
1153        unsafe { write_out(buf, allocation.into_raw()) };
1154        0
1155    })
1156}
1157
1158// ---- Path-addressed predicates, getters and setters ------------------
1159
1160/// Generate an integer predicate matching the corresponding getter.
1161///
1162/// True exactly when the getter would return `ASDF_VALUE_OK`: the value is an
1163/// integer *and* it fits the requested width.
1164macro_rules! int_predicate {
1165    ($name:ident, $ty:ty) => {
1166        /// Whether the value at `path` is an integer that fits this type.
1167        ///
1168        /// # Safety
1169        /// See the integer getters.
1170        #[unsafe(no_mangle)]
1171        pub unsafe extern "C" fn $name(file: *mut AsdfFile, path: *const c_char) -> bool {
1172            guard(stringify!($name), false, || match resolve_at(file, path) {
1173                Some((Resolved::Uint(v, _), _, _)) => <$ty>::try_from(v).is_ok(),
1174                Some((Resolved::Int(v, _), _, _)) => <$ty>::try_from(v).is_ok(),
1175                _ => false,
1176            })
1177        }
1178    };
1179}
1180
1181int_predicate!(asdf_is_int8, i8);
1182int_predicate!(asdf_is_int16, i16);
1183int_predicate!(asdf_is_int32, i32);
1184int_predicate!(asdf_is_int64, i64);
1185int_predicate!(asdf_is_uint8, u8);
1186int_predicate!(asdf_is_uint16, u16);
1187int_predicate!(asdf_is_uint32, u32);
1188int_predicate!(asdf_is_uint64, u64);
1189
1190/// Whether the value at `path` is an integer of any width.
1191///
1192/// # Safety
1193/// See the integer getters.
1194#[unsafe(no_mangle)]
1195pub unsafe extern "C" fn asdf_is_int(file: *mut AsdfFile, path: *const c_char) -> bool {
1196    guard("asdf_is_int", false, || {
1197        matches!(resolve_at(file, path), Some((Resolved::Int(..) | Resolved::Uint(..), _, _)))
1198    })
1199}
1200
1201/// Whether the value at `path` is a float.
1202///
1203/// libasdf parses every float as a `double`, so this and [`asdf_is_double`]
1204/// agree.
1205///
1206/// # Safety
1207/// See the integer getters.
1208#[unsafe(no_mangle)]
1209pub unsafe extern "C" fn asdf_is_float(file: *mut AsdfFile, path: *const c_char) -> bool {
1210    guard("asdf_is_float", false, || {
1211        matches!(resolve_at(file, path), Some((Resolved::Double(_), _, _)))
1212    })
1213}
1214
1215/// Whether the value at `path` is a double. See [`asdf_is_float`].
1216///
1217/// # Safety
1218/// See the integer getters.
1219#[unsafe(no_mangle)]
1220pub unsafe extern "C" fn asdf_is_double(file: *mut AsdfFile, path: *const c_char) -> bool {
1221    guard("asdf_is_double", false, || {
1222        matches!(resolve_at(file, path), Some((Resolved::Double(_), _, _)))
1223    })
1224}
1225
1226/// Whether the value at `path` is a scalar of any kind.
1227///
1228/// # Safety
1229/// See the integer getters.
1230#[unsafe(no_mangle)]
1231pub unsafe extern "C" fn asdf_is_scalar(file: *mut AsdfFile, path: *const c_char) -> bool {
1232    guard("asdf_is_scalar", false, || match lookup(file, path) {
1233        Some((doc, node)) => doc.resolved(node).is_scalar(),
1234        None => false,
1235    })
1236}
1237
1238/// Read the value at `path` as a counted string.
1239///
1240/// # Safety
1241/// `file` must be a valid file handle, `path` a valid string or null, and
1242/// `out`/`out_len` writable or null. The string is owned by the file.
1243#[unsafe(no_mangle)]
1244pub unsafe extern "C" fn asdf_get_string(
1245    file: *mut AsdfFile,
1246    path: *const c_char,
1247    out: *mut *const c_char,
1248    out_len: *mut usize,
1249) -> AsdfValueErr {
1250    guard("asdf_get_string", AsdfValueErr::Unknown, || {
1251        let Some((resolved, text, _)) = resolve_at(file, path) else {
1252            return AsdfValueErr::NotFound;
1253        };
1254        if !matches!(resolved, Resolved::String) {
1255            return AsdfValueErr::TypeMismatch;
1256        }
1257        intern_at(file, &text, out, out_len)
1258    })
1259}
1260
1261/// Read the raw text of the scalar at `path`, whatever its resolved type.
1262///
1263/// # Safety
1264/// See [`asdf_get_string`].
1265#[unsafe(no_mangle)]
1266pub unsafe extern "C" fn asdf_get_scalar(
1267    file: *mut AsdfFile,
1268    path: *const c_char,
1269    out: *mut *const c_char,
1270    out_len: *mut usize,
1271) -> AsdfValueErr {
1272    guard("asdf_get_scalar", AsdfValueErr::Unknown, || {
1273        let Some((doc, node)) = lookup(file, path) else {
1274            return AsdfValueErr::NotFound;
1275        };
1276        let Some(text) = doc.resolved(node).as_str() else {
1277            return AsdfValueErr::TypeMismatch;
1278        };
1279        let text = text.to_string();
1280        intern_at(file, &text, out, out_len)
1281    })
1282}
1283
1284/// Read the raw text of the scalar at `path` as a NUL-terminated string.
1285///
1286/// # Safety
1287/// See [`asdf_get_string`].
1288#[unsafe(no_mangle)]
1289pub unsafe extern "C" fn asdf_get_scalar0(
1290    file: *mut AsdfFile,
1291    path: *const c_char,
1292    out: *mut *const c_char,
1293) -> AsdfValueErr {
1294    unsafe { asdf_get_scalar(file, path, out, core::ptr::null_mut()) }
1295}
1296
1297/// Intern `text` in the file and hand back pointer and length.
1298fn intern_at(
1299    file: *mut AsdfFile,
1300    text: &str,
1301    out: *mut *const c_char,
1302    out_len: *mut usize,
1303) -> AsdfValueErr {
1304    let ptr = unsafe { &*file }.intern(text);
1305    if ptr.is_null() {
1306        return AsdfValueErr::Oom;
1307    }
1308    if !out.is_null() {
1309        unsafe { write_out(out, ptr) };
1310    }
1311    if !out_len.is_null() {
1312        unsafe { write_out(out_len, text.len()) };
1313    }
1314    AsdfValueErr::Ok
1315}
1316
1317/// Generate a container getter over a path.
1318macro_rules! container_getter {
1319    ($name:ident, $handle:ty, $variant:ident) => {
1320        /// Get a handle to the container at `path`.
1321        ///
1322        /// # Safety
1323        /// `file` must be a valid file handle, `path` a valid string or null,
1324        /// and `out` writable or null. The result must be released with
1325        /// `asdf_value_destroy`.
1326        #[unsafe(no_mangle)]
1327        pub unsafe extern "C" fn $name(
1328            file: *mut AsdfFile,
1329            path: *const c_char,
1330            out: *mut *mut $handle,
1331        ) -> AsdfValueErr {
1332            guard(stringify!($name), AsdfValueErr::Unknown, || {
1333                let Some((doc, node)) = lookup(file, path) else {
1334                    return AsdfValueErr::NotFound;
1335                };
1336                if !doc.resolved(node).$variant() {
1337                    return AsdfValueErr::TypeMismatch;
1338                }
1339                if !out.is_null() {
1340                    let handle = Box::into_raw(Box::new(AsdfValue::new(file, node)));
1341                    if handle.is_null() {
1342                        return AsdfValueErr::Oom;
1343                    }
1344                    unsafe { write_out(out, handle) };
1345                }
1346                AsdfValueErr::Ok
1347            })
1348        }
1349    };
1350}
1351
1352container_getter!(asdf_get_mapping, crate::value_ffi::AsdfMapping, is_mapping);
1353container_getter!(asdf_get_sequence, crate::value_ffi::AsdfSequence, is_sequence);
1354
1355/// Set a counted string at `path`.
1356///
1357/// # Safety
1358/// `file` must be a file handle opened for writing; `str_` must point to at
1359/// least `len` readable bytes.
1360#[unsafe(no_mangle)]
1361pub unsafe extern "C" fn asdf_set_string(
1362    file: *mut AsdfFile,
1363    path: *const c_char,
1364    str_: *const c_char,
1365    len: usize,
1366) -> AsdfValueErr {
1367    guard("asdf_set_string", AsdfValueErr::Unknown, || {
1368        if str_.is_null() {
1369            return AsdfValueErr::Unknown;
1370        }
1371        let bytes = unsafe { core::slice::from_raw_parts(str_.cast::<u8>(), len) };
1372        let text = String::from_utf8_lossy(bytes).into_owned();
1373        set_node(file, path, |doc| {
1374            let style = match asdf_yaml::resolve(&text, ScalarStyle::Plain, Schema::Libasdf) {
1375                Resolved::String => ScalarStyle::Plain,
1376                _ => ScalarStyle::SingleQuoted,
1377            };
1378            doc.add_scalar_styled(text, style)
1379        })
1380    })
1381}
1382
1383/// Attach an existing value at `path`.
1384///
1385/// # Safety
1386/// `file` must be a file handle opened for writing, `path` a valid string or
1387/// null, and `value` a valid value handle belonging to the same file.
1388#[unsafe(no_mangle)]
1389pub unsafe extern "C" fn asdf_set_value(
1390    file: *mut AsdfFile,
1391    path: *const c_char,
1392    value: *mut AsdfValue,
1393) -> AsdfValueErr {
1394    guard("asdf_set_value", AsdfValueErr::Unknown, || unsafe { set_value_at(file, path, value) })
1395}
1396
1397/// Attach a mapping at `path`. See [`asdf_set_value`].
1398///
1399/// # Safety
1400/// See [`asdf_set_value`].
1401#[unsafe(no_mangle)]
1402pub unsafe extern "C" fn asdf_set_mapping(
1403    file: *mut AsdfFile,
1404    path: *const c_char,
1405    mapping: *mut crate::value_ffi::AsdfMapping,
1406) -> AsdfValueErr {
1407    guard("asdf_set_mapping", AsdfValueErr::Unknown, || unsafe {
1408        set_value_at(file, path, mapping)
1409    })
1410}
1411
1412/// Attach a sequence at `path`. See [`asdf_set_value`].
1413///
1414/// # Safety
1415/// See [`asdf_set_value`].
1416#[unsafe(no_mangle)]
1417pub unsafe extern "C" fn asdf_set_sequence(
1418    file: *mut AsdfFile,
1419    path: *const c_char,
1420    sequence: *mut crate::value_ffi::AsdfSequence,
1421) -> AsdfValueErr {
1422    guard("asdf_set_sequence", AsdfValueErr::Unknown, || unsafe {
1423        set_value_at(file, path, sequence)
1424    })
1425}
1426
1427/// Find the first value in the file's tree matching `pred`, breadth-first.
1428///
1429/// Shorthand for starting an [`asdf_value_find`](crate::value_ffi::asdf_value_find)
1430/// at the root, and the `asdf_file_t *` arm of the `asdf_find` macro.
1431///
1432/// # Safety
1433/// `file` must be a valid file handle. The result must be released with
1434/// [`asdf_value_destroy`].
1435#[unsafe(no_mangle)]
1436pub unsafe extern "C" fn asdf_file_find(
1437    file: *mut AsdfFile,
1438    pred: crate::value_ffi::AsdfValuePred,
1439) -> *mut AsdfValue {
1440    guard("asdf_file_find", core::ptr::null_mut(), || file_find_ex(file, pred, false, None, -1))
1441}
1442
1443/// Find from the file's root with control over traversal order and depth.
1444///
1445/// See [`asdf_value_find_ex`](crate::value_ffi::asdf_value_find_ex) for what
1446/// the options mean.
1447///
1448/// # Safety
1449/// See [`asdf_file_find`].
1450#[unsafe(no_mangle)]
1451pub unsafe extern "C" fn asdf_file_find_ex(
1452    file: *mut AsdfFile,
1453    pred: crate::value_ffi::AsdfValuePred,
1454    depth_first: bool,
1455    descend_pred: crate::value_ffi::AsdfValuePred,
1456    max_depth: i64,
1457) -> *mut AsdfValue {
1458    guard("asdf_file_find_ex", core::ptr::null_mut(), || {
1459        file_find_ex(file, pred, depth_first, descend_pred, max_depth)
1460    })
1461}
1462
1463/// Safe internal form of [`asdf_file_find_ex`].
1464///
1465/// The root handle is built and released here rather than handed out, so a
1466/// caller that finds nothing has nothing to destroy -- which is what
1467/// `file.h` promises.
1468fn file_find_ex(
1469    file: *mut AsdfFile,
1470    pred: crate::value_ffi::AsdfValuePred,
1471    depth_first: bool,
1472    descend_pred: crate::value_ffi::AsdfValuePred,
1473    max_depth: i64,
1474) -> *mut AsdfValue {
1475    let Some((_, node)) = lookup(file, core::ptr::null()) else {
1476        return core::ptr::null_mut();
1477    };
1478    let root = Box::into_raw(Box::new(AsdfValue { file, node }));
1479    let found = crate::value_ffi::value_find_ex(root, pred, depth_first, descend_pred, max_depth);
1480    // SAFETY: `root` was boxed just above and never handed to the caller;
1481    // `value_find_ex` returns a handle of its own, never this one.
1482    drop(unsafe { Box::from_raw(root) });
1483    found
1484}
1485
1486#[cfg(test)]
1487mod tests {
1488    use super::*;
1489
1490    fn sample() -> Vec<u8> {
1491        let mut buf = Vec::new();
1492        buf.extend_from_slice(b"#ASDF 1.0.0\n#ASDF_STANDARD 1.6.0\n");
1493        buf.extend_from_slice(b"%YAML 1.1\n%TAG ! tag:stsci.edu:asdf/\n--- !core/asdf-1.1.0\n");
1494        buf.extend_from_slice(
1495            b"name: Dennis Richie\nfoo: 42\nbig: 5000000000\nneg: -7\n\
1496              pi: 3.5\nyes_flag: true\nnothing: null\nquoted: '1'\n\
1497              nested:\n  inner: deep\nlist: [a, b, c]\n",
1498        );
1499        buf.extend_from_slice(b"...\n");
1500        buf
1501    }
1502
1503    struct Handle(*mut AsdfFile);
1504    impl Drop for Handle {
1505        fn drop(&mut self) {
1506            unsafe { asdf_close(self.0) };
1507        }
1508    }
1509
1510    fn open() -> Handle {
1511        let bytes = sample();
1512        let f =
1513            unsafe { asdf_open_mem_ex(bytes.as_ptr().cast(), bytes.len(), core::ptr::null_mut()) };
1514        assert!(!f.is_null());
1515        Handle(f)
1516    }
1517
1518    fn cpath(s: &str) -> CString {
1519        CString::new(s).unwrap()
1520    }
1521
1522    /// A read-only handle over a file whose tree is the given YAML body.
1523    fn handle_with(body: &str) -> Handle {
1524        let mut buf = Vec::new();
1525        buf.extend_from_slice(b"#ASDF 1.0.0\n#ASDF_STANDARD 1.6.0\n");
1526        buf.extend_from_slice(b"%YAML 1.1\n%TAG ! tag:stsci.edu:asdf/\n--- !core/asdf-1.1.0\n");
1527        buf.extend_from_slice(body.as_bytes());
1528        buf.extend_from_slice(b"...\n");
1529        let f = unsafe { asdf_open_mem_ex(buf.as_ptr().cast(), buf.len(), core::ptr::null_mut()) };
1530        assert!(!f.is_null());
1531        Handle(f)
1532    }
1533
1534    /// The predicate the find tests search for: the string `hit`.
1535    unsafe extern "C" fn is_hit(value: *mut AsdfValue) -> bool {
1536        let mut out: *const c_char = core::ptr::null();
1537        let err = unsafe { crate::value_ffi::asdf_value_as_string0(value, &mut out) };
1538        err == AsdfValueErr::Ok
1539            && !out.is_null()
1540            && unsafe { CStr::from_ptr(out) }.to_bytes() == b"hit"
1541    }
1542
1543    #[test]
1544    fn a_file_find_starts_at_the_root_and_owns_nothing_else() {
1545        let h = handle_with("a_nested:\n  deep: hit\nz_top: hit\n");
1546
1547        // Breadth-first reaches the top-level value first, even though the
1548        // nested branch is written above it.
1549        let found = unsafe { asdf_file_find(h.0, Some(is_hit)) };
1550        assert!(!found.is_null());
1551        let path = unsafe { crate::value_ffi::asdf_value_path(found) };
1552        assert_eq!(unsafe { CStr::from_ptr(path) }.to_str().unwrap(), "/z_top");
1553        unsafe { asdf_value_destroy(found) };
1554
1555        // Depth-first reverses that.
1556        let deep = unsafe { asdf_file_find_ex(h.0, Some(is_hit), true, None, -1) };
1557        assert!(!deep.is_null());
1558        let path = unsafe { crate::value_ffi::asdf_value_path(deep) };
1559        assert_eq!(unsafe { CStr::from_ptr(path) }.to_str().unwrap(), "/a_nested/deep");
1560        unsafe { asdf_value_destroy(deep) };
1561    }
1562
1563    #[test]
1564    fn a_file_find_that_matches_nothing_returns_null() {
1565        let h = handle_with("a: 1\nb: 2\n");
1566        assert!(unsafe { asdf_file_find(h.0, Some(is_hit)) }.is_null());
1567        // A null file is a question, not a crash.
1568        assert!(unsafe { asdf_file_find(core::ptr::null_mut(), Some(is_hit)) }.is_null());
1569        assert!(
1570            unsafe { asdf_file_find_ex(core::ptr::null_mut(), Some(is_hit), false, None, -1) }
1571                .is_null()
1572        );
1573    }
1574
1575    #[test]
1576    fn width_predicates_agree_with_the_getters() {
1577        let h = handle_with("small: 200\nbig: 70000\nnegative: -5\ntext: hello\n");
1578        let small = cpath("small");
1579        let big = cpath("big");
1580        let negative = cpath("negative");
1581        let text = cpath("text");
1582
1583        // 200 fits a uint8 but not an int8.
1584        assert!(unsafe { asdf_is_uint8(h.0, small.as_ptr()) });
1585        assert!(!unsafe { asdf_is_int8(h.0, small.as_ptr()) });
1586        assert!(unsafe { asdf_is_int16(h.0, small.as_ptr()) });
1587        assert!(unsafe { asdf_is_int(h.0, small.as_ptr()) });
1588
1589        // 70000 needs more than 16 bits.
1590        assert!(!unsafe { asdf_is_uint16(h.0, big.as_ptr()) });
1591        assert!(unsafe { asdf_is_uint32(h.0, big.as_ptr()) });
1592
1593        // A negative value is never an unsigned one.
1594        assert!(unsafe { asdf_is_int32(h.0, negative.as_ptr()) });
1595        assert!(!unsafe { asdf_is_uint32(h.0, negative.as_ptr()) });
1596
1597        assert!(!unsafe { asdf_is_int(h.0, text.as_ptr()) });
1598        assert!(unsafe { asdf_is_scalar(h.0, text.as_ptr()) });
1599
1600        // Each predicate must agree with the matching getter.
1601        let mut narrow: i8 = 0;
1602        assert_eq!(
1603            unsafe { asdf_get_int8(h.0, small.as_ptr(), &mut narrow) },
1604            AsdfValueErr::Overflow
1605        );
1606        let mut wide: u8 = 0;
1607        assert_eq!(unsafe { asdf_get_uint8(h.0, small.as_ptr(), &mut wide) }, AsdfValueErr::Ok);
1608    }
1609
1610    #[test]
1611    fn float_predicates_and_scalar_getters() {
1612        let h = handle_with("pi: 3.14\nn: 7\ntext: hi\n");
1613        let pi = cpath("pi");
1614        let n = cpath("n");
1615        let text = cpath("text");
1616
1617        assert!(unsafe { asdf_is_float(h.0, pi.as_ptr()) });
1618        assert!(unsafe { asdf_is_double(h.0, pi.as_ptr()) });
1619        assert!(!unsafe { asdf_is_float(h.0, n.as_ptr()) });
1620
1621        // `get_scalar` hands back the raw text whatever the resolved type is;
1622        // `get_string` insists on an actual string.
1623        let mut out = core::ptr::null();
1624        let mut len = 0usize;
1625        assert_eq!(
1626            unsafe { asdf_get_scalar(h.0, pi.as_ptr(), &mut out, &mut len) },
1627            AsdfValueErr::Ok
1628        );
1629        assert_eq!(len, 4);
1630        assert_eq!(unsafe { CStr::from_ptr(out) }, c"3.14");
1631        assert_eq!(
1632            unsafe { asdf_get_string(h.0, pi.as_ptr(), &mut out, &mut len) },
1633            AsdfValueErr::TypeMismatch
1634        );
1635
1636        assert_eq!(
1637            unsafe { asdf_get_string(h.0, text.as_ptr(), &mut out, &mut len) },
1638            AsdfValueErr::Ok
1639        );
1640        assert_eq!(len, 2);
1641
1642        let mut zero_terminated = core::ptr::null();
1643        assert_eq!(
1644            unsafe { asdf_get_scalar0(h.0, n.as_ptr(), &mut zero_terminated) },
1645            AsdfValueErr::Ok
1646        );
1647        assert_eq!(unsafe { CStr::from_ptr(zero_terminated) }, c"7");
1648    }
1649
1650    #[test]
1651    fn container_getters_check_the_type() {
1652        let h = handle_with("m:\n  k: v\nseq: [1, 2]\nscalar: 3\n");
1653        let m = cpath("m");
1654        let seq = cpath("seq");
1655        let scalar = cpath("scalar");
1656
1657        let mut mapping = core::ptr::null_mut();
1658        assert_eq!(unsafe { asdf_get_mapping(h.0, m.as_ptr(), &mut mapping) }, AsdfValueErr::Ok);
1659        assert!(!mapping.is_null());
1660        unsafe { asdf_value_destroy(mapping) };
1661
1662        let mut sequence = core::ptr::null_mut();
1663        assert_eq!(
1664            unsafe { asdf_get_sequence(h.0, seq.as_ptr(), &mut sequence) },
1665            AsdfValueErr::Ok
1666        );
1667        assert!(!sequence.is_null());
1668        unsafe { asdf_value_destroy(sequence) };
1669
1670        // Asking for the wrong shape is a mismatch, not a guess.
1671        assert_eq!(
1672            unsafe { asdf_get_mapping(h.0, seq.as_ptr(), &mut mapping) },
1673            AsdfValueErr::TypeMismatch
1674        );
1675        assert_eq!(
1676            unsafe { asdf_get_sequence(h.0, scalar.as_ptr(), &mut sequence) },
1677            AsdfValueErr::TypeMismatch
1678        );
1679        let missing = cpath("absent");
1680        assert_eq!(
1681            unsafe { asdf_get_mapping(h.0, missing.as_ptr(), &mut mapping) },
1682            AsdfValueErr::NotFound
1683        );
1684    }
1685
1686    #[test]
1687    fn counted_string_setter_round_trips() {
1688        let file = unsafe { asdf_open_mem_ex(core::ptr::null(), 0, core::ptr::null_mut()) };
1689        let h = Handle(file);
1690        let path = cpath("label");
1691        let value = b"embedded";
1692        assert_eq!(
1693            unsafe {
1694                asdf_set_string(h.0, path.as_ptr(), value.as_ptr().cast::<c_char>(), value.len())
1695            },
1696            AsdfValueErr::Ok
1697        );
1698        let mut out = core::ptr::null();
1699        let mut len = 0usize;
1700        assert_eq!(
1701            unsafe { asdf_get_string(h.0, path.as_ptr(), &mut out, &mut len) },
1702            AsdfValueErr::Ok
1703        );
1704        assert_eq!(len, value.len());
1705        assert_eq!(unsafe { CStr::from_ptr(out) }, c"embedded");
1706    }
1707
1708    #[test]
1709    fn set_mapping_attaches_a_built_container() {
1710        let file = unsafe { asdf_open_mem_ex(core::ptr::null(), 0, core::ptr::null_mut()) };
1711        let h = Handle(file);
1712
1713        let mapping = unsafe { crate::value_ffi::asdf_mapping_create(h.0) };
1714        let inner = cpath("inner");
1715        assert_eq!(
1716            unsafe { crate::value_ffi::asdf_mapping_set_int32(mapping, inner.as_ptr(), 5) },
1717            AsdfValueErr::Ok
1718        );
1719
1720        let path = cpath("outer/nested");
1721        assert_eq!(unsafe { asdf_set_mapping(h.0, path.as_ptr(), mapping) }, AsdfValueErr::Ok);
1722        unsafe { asdf_value_destroy(mapping) };
1723
1724        // Intermediate mappings were created along the way.
1725        let full = cpath("outer/nested/inner");
1726        let mut got: i32 = 0;
1727        assert_eq!(unsafe { asdf_get_int32(h.0, full.as_ptr(), &mut got) }, AsdfValueErr::Ok);
1728        assert_eq!(got, 5);
1729    }
1730
1731    #[test]
1732    fn opens_and_closes_a_memory_buffer() {
1733        let h = open();
1734        assert_eq!(unsafe { asdf_error_code(h.0) }, 0);
1735        assert!(unsafe { asdf_error(h.0) }.is_null());
1736    }
1737
1738    #[test]
1739    fn rejects_bad_arguments_without_crashing() {
1740        assert!(
1741            unsafe {
1742                asdf_open_file_ex(core::ptr::null(), core::ptr::null(), core::ptr::null_mut())
1743            }
1744            .is_null()
1745        );
1746        // Closing null must be a no-op, as it is upstream.
1747        unsafe { asdf_close(core::ptr::null_mut()) };
1748        assert_eq!(unsafe { asdf_error_code(core::ptr::null_mut()) }, 0);
1749    }
1750
1751    /// `asdf_open(NULL)` expands to `asdf_open_mem(NULL, 0)`, which the C API
1752    /// defines as "give me a new, empty file to write into" -- not as an
1753    /// error. libasdf's own write example opens a file that way.
1754    #[test]
1755    fn opening_a_null_buffer_creates_a_writable_file() {
1756        let f = unsafe { asdf_open_mem_ex(core::ptr::null(), 0, core::ptr::null_mut()) };
1757        assert!(!f.is_null());
1758        let h = Handle(f);
1759
1760        let path = cpath("foo");
1761        assert_eq!(unsafe { asdf_set_int64(h.0, path.as_ptr(), 42) }, AsdfValueErr::Ok);
1762    }
1763
1764    /// Write the sample to a real file and hand back its path.
1765    fn sample_on_disk(name: &str) -> std::path::PathBuf {
1766        let dir = std::env::temp_dir().join(format!("asdf-file-ffi-{}", std::process::id()));
1767        std::fs::create_dir_all(&dir).unwrap();
1768        let path = dir.join(name);
1769        std::fs::write(&path, sample()).unwrap();
1770        path
1771    }
1772
1773    #[test]
1774    fn writing_to_a_read_only_file_is_refused() {
1775        let path = sample_on_disk("read-only.asdf");
1776        let name = CString::new(path.to_str().unwrap()).unwrap();
1777        let f = unsafe { asdf_open_file_ex(name.as_ptr(), c"r".as_ptr(), core::ptr::null_mut()) };
1778        assert!(!f.is_null());
1779        let h = Handle(f);
1780
1781        let key = cpath("foo");
1782        assert_eq!(unsafe { asdf_set_int64(h.0, key.as_ptr(), 1) }, AsdfValueErr::ReadOnly);
1783    }
1784
1785    /// libasdf gives a buffer-backed file read-*write* mode, so its tree may
1786    /// be edited and written out somewhere else. Only an `"r"` open of a real
1787    /// file is genuinely read-only.
1788    #[test]
1789    fn a_memory_backed_file_is_writable() {
1790        let h = open();
1791        let key = cpath("foo");
1792        assert_eq!(unsafe { asdf_set_int64(h.0, key.as_ptr(), 1) }, AsdfValueErr::Ok);
1793
1794        // The file it was opened over is still there, with the edit applied
1795        // on top rather than replacing it.
1796        let name = cpath("name");
1797        let mut out = core::ptr::null();
1798        assert_eq!(unsafe { asdf_get_string0(h.0, name.as_ptr(), &mut out) }, AsdfValueErr::Ok);
1799        let mut got: i64 = 0;
1800        assert_eq!(unsafe { asdf_get_int64(h.0, key.as_ptr(), &mut got) }, AsdfValueErr::Ok);
1801        assert_eq!(got, 1);
1802    }
1803
1804    #[test]
1805    fn open_modes_are_the_three_libasdf_accepts() {
1806        let path = sample_on_disk("modes.asdf");
1807        let name = CString::new(path.to_str().unwrap()).unwrap();
1808
1809        // `rw` reads the file and permits edits.
1810        let f = unsafe { asdf_open_file_ex(name.as_ptr(), c"rw".as_ptr(), core::ptr::null_mut()) };
1811        assert!(!f.is_null());
1812        let h = Handle(f);
1813        let key = cpath("foo");
1814        assert_eq!(unsafe { asdf_set_int64(h.0, key.as_ptr(), 7) }, AsdfValueErr::Ok);
1815
1816        // `w` reads nothing at all -- not even the filename, which is why
1817        // upstream accepts a null one here.
1818        let w = unsafe { asdf_open_file_ex(name.as_ptr(), c"W".as_ptr(), core::ptr::null_mut()) };
1819        assert!(!w.is_null());
1820        let wh = Handle(w);
1821        assert_eq!(unsafe { asdf_block_count(wh.0) }, 0);
1822        assert_eq!(unsafe { asdf_set_int64(wh.0, key.as_ptr(), 1) }, AsdfValueErr::Ok);
1823
1824        // Anything else is an invalid argument.
1825        for bad in [c"rb", c"a", c"r+", c""] {
1826            let bad_open =
1827                unsafe { asdf_open_file_ex(name.as_ptr(), bad.as_ptr(), core::ptr::null_mut()) };
1828            assert!(bad_open.is_null(), "{bad:?} should not be a valid mode");
1829        }
1830    }
1831
1832    #[test]
1833    fn a_written_file_reads_back() {
1834        let f = unsafe { asdf_open_mem_ex(core::ptr::null(), 0, core::ptr::null_mut()) };
1835        let h = Handle(f);
1836
1837        let name = cpath("name");
1838        let value = CString::new("Dennis Richie").unwrap();
1839        assert_eq!(
1840            unsafe { asdf_set_string0(h.0, name.as_ptr(), value.as_ptr()) },
1841            AsdfValueErr::Ok
1842        );
1843        let foo = cpath("foo");
1844        assert_eq!(unsafe { asdf_set_int64(h.0, foo.as_ptr(), 42) }, AsdfValueErr::Ok);
1845        // Intermediate mappings are materialised.
1846        let nested = cpath("powers/squares");
1847        assert_eq!(unsafe { asdf_set_uint64(h.0, nested.as_ptr(), 1764) }, AsdfValueErr::Ok);
1848
1849        let mut buf: *mut c_void = core::ptr::null_mut();
1850        let mut size: usize = 0;
1851        assert_eq!(unsafe { asdf_write_to_mem(h.0, &mut buf, &mut size) }, 0);
1852        assert!(!buf.is_null() && size > 0);
1853
1854        // Read the bytes back through the same API.
1855        let reopened = unsafe { asdf_open_mem_ex(buf, size, core::ptr::null_mut()) };
1856        assert!(!reopened.is_null());
1857        let r = Handle(reopened);
1858
1859        let mut out: *const c_char = core::ptr::null();
1860        assert_eq!(unsafe { asdf_get_string0(r.0, name.as_ptr(), &mut out) }, AsdfValueErr::Ok);
1861        assert_eq!(unsafe { CStr::from_ptr(out) }.to_str().unwrap(), "Dennis Richie");
1862
1863        let mut v: i64 = 0;
1864        assert_eq!(unsafe { asdf_get_int64(r.0, foo.as_ptr(), &mut v) }, AsdfValueErr::Ok);
1865        assert_eq!(v, 42);
1866
1867        let mut u: u64 = 0;
1868        assert_eq!(unsafe { asdf_get_uint64(r.0, nested.as_ptr(), &mut u) }, AsdfValueErr::Ok);
1869        assert_eq!(u, 1764);
1870
1871        unsafe { libc::free(buf) };
1872    }
1873
1874    /// A string of digits must survive as a string, not become an integer.
1875    #[test]
1876    fn string_setters_preserve_stringness() {
1877        let f = unsafe { asdf_open_mem_ex(core::ptr::null(), 0, core::ptr::null_mut()) };
1878        let h = Handle(f);
1879
1880        let key = cpath("version");
1881        let value = CString::new("42").unwrap();
1882        unsafe { asdf_set_string0(h.0, key.as_ptr(), value.as_ptr()) };
1883
1884        let mut buf: *mut c_void = core::ptr::null_mut();
1885        let mut size: usize = 0;
1886        unsafe { asdf_write_to_mem(h.0, &mut buf, &mut size) };
1887        let reopened = unsafe { asdf_open_mem_ex(buf, size, core::ptr::null_mut()) };
1888        let r = Handle(reopened);
1889
1890        let mut out: *const c_char = core::ptr::null();
1891        assert_eq!(
1892            unsafe { asdf_get_string0(r.0, key.as_ptr(), &mut out) },
1893            AsdfValueErr::Ok,
1894            "a quoted numeric string must read back as a string"
1895        );
1896        assert_eq!(unsafe { CStr::from_ptr(out) }.to_str().unwrap(), "42");
1897        unsafe { libc::free(buf) };
1898    }
1899
1900    #[test]
1901    fn a_missing_file_returns_null() {
1902        let name = cpath("/definitely/not/here.asdf");
1903        let mode = cpath("r");
1904        let f = unsafe { asdf_open_file_ex(name.as_ptr(), mode.as_ptr(), core::ptr::null_mut()) };
1905        assert!(f.is_null());
1906    }
1907
1908    #[test]
1909    fn reads_a_string() {
1910        let h = open();
1911        let mut out: *const c_char = core::ptr::null();
1912        let path = cpath("name");
1913        assert_eq!(unsafe { asdf_get_string0(h.0, path.as_ptr(), &mut out) }, AsdfValueErr::Ok);
1914        assert_eq!(unsafe { CStr::from_ptr(out) }.to_str().unwrap(), "Dennis Richie");
1915    }
1916
1917    #[test]
1918    fn reads_integers_at_every_width() {
1919        let h = open();
1920        let path = cpath("foo");
1921
1922        let mut v8: i8 = 0;
1923        assert_eq!(unsafe { asdf_get_int8(h.0, path.as_ptr(), &mut v8) }, AsdfValueErr::Ok);
1924        assert_eq!(v8, 42);
1925
1926        let mut v64: i64 = 0;
1927        assert_eq!(unsafe { asdf_get_int64(h.0, path.as_ptr(), &mut v64) }, AsdfValueErr::Ok);
1928        assert_eq!(v64, 42);
1929
1930        let mut u8v: u8 = 0;
1931        assert_eq!(unsafe { asdf_get_uint8(h.0, path.as_ptr(), &mut u8v) }, AsdfValueErr::Ok);
1932        assert_eq!(u8v, 42);
1933    }
1934
1935    #[test]
1936    fn too_small_a_type_overflows_rather_than_truncating() {
1937        let h = open();
1938        let path = cpath("big");
1939        let mut v: u8 = 0;
1940        assert_eq!(unsafe { asdf_get_uint8(h.0, path.as_ptr(), &mut v) }, AsdfValueErr::Overflow);
1941        // The wide type still works.
1942        let mut w: u64 = 0;
1943        assert_eq!(unsafe { asdf_get_uint64(h.0, path.as_ptr(), &mut w) }, AsdfValueErr::Ok);
1944        assert_eq!(w, 5_000_000_000);
1945    }
1946
1947    #[test]
1948    fn a_negative_value_does_not_read_as_unsigned() {
1949        let h = open();
1950        let path = cpath("neg");
1951        let mut v: u32 = 0;
1952        assert_eq!(unsafe { asdf_get_uint32(h.0, path.as_ptr(), &mut v) }, AsdfValueErr::Overflow);
1953        let mut s: i32 = 0;
1954        assert_eq!(unsafe { asdf_get_int32(h.0, path.as_ptr(), &mut s) }, AsdfValueErr::Ok);
1955        assert_eq!(s, -7);
1956    }
1957
1958    #[test]
1959    fn reads_floats_and_accepts_integers_as_doubles() {
1960        let h = open();
1961        let mut d: f64 = 0.0;
1962        let pi = cpath("pi");
1963        assert_eq!(unsafe { asdf_get_double(h.0, pi.as_ptr(), &mut d) }, AsdfValueErr::Ok);
1964        assert_eq!(d, 3.5);
1965
1966        let foo = cpath("foo");
1967        assert_eq!(unsafe { asdf_get_double(h.0, foo.as_ptr(), &mut d) }, AsdfValueErr::Ok);
1968        assert_eq!(d, 42.0);
1969    }
1970
1971    #[test]
1972    fn reads_booleans() {
1973        let h = open();
1974        let mut b = false;
1975        let path = cpath("yes_flag");
1976        assert_eq!(unsafe { asdf_get_bool(h.0, path.as_ptr(), &mut b) }, AsdfValueErr::Ok);
1977        assert!(b);
1978    }
1979
1980    #[test]
1981    fn a_quoted_number_is_a_string_not_an_integer() {
1982        let h = open();
1983        let path = cpath("quoted");
1984
1985        let mut v: i64 = 0;
1986        assert_eq!(
1987            unsafe { asdf_get_int64(h.0, path.as_ptr(), &mut v) },
1988            AsdfValueErr::TypeMismatch
1989        );
1990
1991        let mut s: *const c_char = core::ptr::null();
1992        assert_eq!(unsafe { asdf_get_string0(h.0, path.as_ptr(), &mut s) }, AsdfValueErr::Ok);
1993        assert_eq!(unsafe { CStr::from_ptr(s) }.to_str().unwrap(), "1");
1994    }
1995
1996    #[test]
1997    fn a_missing_path_is_not_found() {
1998        let h = open();
1999        let path = cpath("nope");
2000        let mut v: i64 = 0;
2001        assert_eq!(unsafe { asdf_get_int64(h.0, path.as_ptr(), &mut v) }, AsdfValueErr::NotFound);
2002    }
2003
2004    #[test]
2005    fn nested_and_indexed_paths_resolve() {
2006        let h = open();
2007        let mut out: *const c_char = core::ptr::null();
2008        let path = cpath("nested/inner");
2009        assert_eq!(unsafe { asdf_get_string0(h.0, path.as_ptr(), &mut out) }, AsdfValueErr::Ok);
2010        assert_eq!(unsafe { CStr::from_ptr(out) }.to_str().unwrap(), "deep");
2011
2012        let path = cpath("list/1");
2013        assert_eq!(unsafe { asdf_get_string0(h.0, path.as_ptr(), &mut out) }, AsdfValueErr::Ok);
2014        assert_eq!(unsafe { CStr::from_ptr(out) }.to_str().unwrap(), "b");
2015    }
2016
2017    #[test]
2018    fn nulls_and_type_predicates() {
2019        let h = open();
2020        let nothing = cpath("nothing");
2021        assert!(unsafe { asdf_is_null(h.0, nothing.as_ptr()) });
2022
2023        let nested = cpath("nested");
2024        assert!(unsafe { asdf_is_mapping(h.0, nested.as_ptr()) });
2025        assert!(!unsafe { asdf_is_sequence(h.0, nested.as_ptr()) });
2026
2027        let list = cpath("list");
2028        assert!(unsafe { asdf_is_sequence(h.0, list.as_ptr()) });
2029
2030        let name = cpath("name");
2031        assert!(unsafe { asdf_is_string(h.0, name.as_ptr()) });
2032    }
2033
2034    #[test]
2035    fn value_handles_report_type_and_tag() {
2036        let h = open();
2037        let root = cpath("");
2038        let v = unsafe { asdf_get_value(h.0, root.as_ptr()) };
2039        assert!(!v.is_null());
2040        assert_eq!(unsafe { asdf_value_get_type(v) }, AsdfValueType::Mapping);
2041
2042        let tag = unsafe { asdf_value_tag(v) };
2043        assert!(!tag.is_null());
2044        assert_eq!(
2045            unsafe { CStr::from_ptr(tag) }.to_str().unwrap(),
2046            "tag:stsci.edu:asdf/core/asdf-1.1.0"
2047        );
2048        unsafe { asdf_value_destroy(v) };
2049        unsafe { asdf_value_destroy(core::ptr::null_mut()) };
2050    }
2051
2052    #[test]
2053    fn type_names_match_libasdf() {
2054        let name = |t| unsafe { CStr::from_ptr(asdf_value_type_string(t)) }.to_str().unwrap();
2055        assert_eq!(name(AsdfValueType::Uint8 as c_int), "uint8");
2056        assert_eq!(name(AsdfValueType::Mapping as c_int), "mapping");
2057        assert_eq!(name(AsdfValueType::Unknown as c_int), "<unknown>");
2058        assert_eq!(name(AsdfValueType::Extension as c_int), "<extension>");
2059    }
2060
2061    #[test]
2062    fn interned_strings_stay_valid_while_the_file_is_open() {
2063        let h = open();
2064        let mut first: *const c_char = core::ptr::null();
2065        let path = cpath("name");
2066        unsafe { asdf_get_string0(h.0, path.as_ptr(), &mut first) };
2067
2068        // Reading many more strings must not invalidate the first, which the
2069        // C contract guarantees until asdf_close.
2070        for _ in 0..100 {
2071            let mut other: *const c_char = core::ptr::null();
2072            unsafe { asdf_get_string0(h.0, path.as_ptr(), &mut other) };
2073        }
2074        assert_eq!(unsafe { CStr::from_ptr(first) }.to_str().unwrap(), "Dennis Richie");
2075    }
2076
2077    #[test]
2078    fn null_out_pointers_are_accepted() {
2079        let h = open();
2080        let path = cpath("foo");
2081        // A caller may pass NULL to test for existence without reading.
2082        assert_eq!(
2083            unsafe { asdf_get_int64(h.0, path.as_ptr(), core::ptr::null_mut()) },
2084            AsdfValueErr::Ok
2085        );
2086    }
2087
2088    #[test]
2089    fn block_count_is_reported() {
2090        let h = open();
2091        assert_eq!(unsafe { asdf_block_count(h.0) }, 0);
2092        assert_eq!(unsafe { asdf_block_count(core::ptr::null_mut()) }, 0);
2093    }
2094}