Skip to main content

asdf/
error_ffi.rs

1//! Error and log plumbing shared with `shim.c`.
2//!
3//! The `asdf_shim_*` functions below are the Rust side of that split. They
4//! are exported because `shim.c` resolves them by name, and they sit inside
5//! the `asdf_` namespace so the symbol-leakage gate accepts them. They are
6//! internal plumbing, not public API -- upstream does the same for the
7//! `asdf_file_error_common` family, which its headers call out as "exported
8//! for the macros, but intentionally left undocumented".
9//!
10//! The C API reports errors as a code plus a message string owned by the
11//! handle they were recorded against, so the handle keeps the `CString`
12//! alive until the next error replaces it.
13
14use alloc::ffi::CString;
15use core::ffi::{CStr, c_char, c_int, c_void};
16use std::sync::Mutex;
17
18use asdf_core::ErrorCode;
19
20/// The per-code `printf` format strings, matching `src/error.c` upstream.
21///
22/// The parameter counts are part of the documented contract for
23/// `ASDF_ERROR_COMMON`, so these strings must keep their conversions exactly.
24const ERROR_FORMATS: &[Option<&CStr>] = &[
25    None,                                                   // NONE
26    Some(c"unknown parser state"),                          // UNKNOWN_STATE
27    Some(c"failed to initialize stream"),                   // STREAM_INIT_FAILED
28    Some(c"cannot write to a read-only stream or file"),    // STREAM_READ_ONLY
29    Some(c"invalid ASDF header"),                           // INVALID_ASDF_HEADER
30    Some(c"unexpected end of file"),                        // UNEXPECTED_EOF
31    Some(c"invalid block header"),                          // INVALID_BLOCK_HEADER
32    Some(c"block magic mismatch"),                          // BLOCK_MAGIC_MISMATCH
33    Some(c"YAML parser initialization failed"),             // YAML_PARSER_INIT_FAILED
34    Some(c"YAML parsing failed"),                           // YAML_PARSE_FAILED
35    Some(c"out of memory"),                                 // OUT_OF_MEMORY
36    None,                                                   // SYSTEM (from strerror)
37    Some(c"invalid argument for %s: %s"),                   // INVALID_ARGUMENT
38    Some(c"unknown compression type: %s"),                  // UNKNOWN_COMPRESSION
39    Some(c"compression error: %s"),                         // COMPRESSION_FAILED
40    Some(c"no serializer registered for the %s extension"), // EXTENSION_NOT_FOUND
41    Some(c"over limit: %s"),                                // OVER_LIMIT
42];
43
44/// The severity each error code is logged at, matching upstream.
45const ERROR_LOG_LEVELS: &[LogLevel] = &[
46    LogLevel::None,  // NONE
47    LogLevel::Error, // UNKNOWN_STATE
48    LogLevel::Error, // STREAM_INIT_FAILED
49    LogLevel::Error, // STREAM_READ_ONLY
50    LogLevel::Error, // INVALID_ASDF_HEADER
51    LogLevel::Error, // UNEXPECTED_EOF
52    LogLevel::Error, // INVALID_BLOCK_HEADER
53    LogLevel::Error, // BLOCK_MAGIC_MISMATCH
54    LogLevel::Fatal, // YAML_PARSER_INIT_FAILED
55    LogLevel::Error, // YAML_PARSE_FAILED
56    LogLevel::Fatal, // OUT_OF_MEMORY
57    LogLevel::Error, // SYSTEM
58    LogLevel::Error, // INVALID_ARGUMENT
59    LogLevel::Error, // UNKNOWN_COMPRESSION
60    LogLevel::Error, // COMPRESSION_FAILED
61    LogLevel::Warn,  // EXTENSION_NOT_FOUND
62    LogLevel::Error, // OVER_LIMIT
63];
64
65/// Severity levels, matching `asdf_log_level_t`.
66///
67/// Zero is `None`, which is what a caller's zeroed `asdf_log_cfg_t` carries
68/// and what upstream reads as "unset, use the default".
69#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug, Default)]
70#[repr(i32)]
71pub enum LogLevel {
72    /// Emit nothing, and what an unset configuration field holds.
73    #[default]
74    None = 0,
75    /// Fine-grained tracing.
76    Trace,
77    /// Debugging messages.
78    Debug,
79    /// Informational messages.
80    Info,
81    /// Recoverable problems.
82    Warn,
83    /// Errors.
84    Error,
85    /// Unrecoverable errors.
86    Fatal,
87}
88
89impl LogLevel {
90    /// Parse the `ASDF_LOG_LEVEL` environment variable's spelling.
91    pub fn from_name(name: &str) -> Option<Self> {
92        match name.to_ascii_uppercase().as_str() {
93            "NONE" => Some(LogLevel::None),
94            "TRACE" => Some(LogLevel::Trace),
95            "DEBUG" => Some(LogLevel::Debug),
96            "INFO" => Some(LogLevel::Info),
97            "WARN" => Some(LogLevel::Warn),
98            "ERROR" => Some(LogLevel::Error),
99            "FATAL" => Some(LogLevel::Fatal),
100            _ => None,
101        }
102    }
103
104    /// The name used in log output.
105    pub fn as_str(self) -> &'static str {
106        match self {
107            LogLevel::None => "NONE",
108            LogLevel::Trace => "TRACE",
109            LogLevel::Debug => "DEBUG",
110            LogLevel::Info => "INFO",
111            LogLevel::Warn => "WARN",
112            LogLevel::Error => "ERROR",
113            LogLevel::Fatal => "FATAL",
114        }
115    }
116
117    fn from_i32(v: i32) -> Option<Self> {
118        match v {
119            0 => Some(LogLevel::None),
120            1 => Some(LogLevel::Trace),
121            2 => Some(LogLevel::Debug),
122            3 => Some(LogLevel::Info),
123            4 => Some(LogLevel::Warn),
124            5 => Some(LogLevel::Error),
125            6 => Some(LogLevel::Fatal),
126            _ => None,
127        }
128    }
129}
130
131/// The error state a file or value handle carries.
132///
133/// `asdf_error` hands out a borrowed pointer into `message`, so the string
134/// must outlive the call and stay put until the next error is recorded.
135#[derive(Default, Debug)]
136pub struct ErrorState {
137    inner: Mutex<ErrorStateInner>,
138}
139
140#[derive(Default, Debug)]
141struct ErrorStateInner {
142    code: i32,
143    errno: i32,
144    message: Option<CString>,
145}
146
147impl ErrorState {
148    /// Record an error.
149    pub fn set(&self, code: i32, message: impl Into<Vec<u8>>) {
150        let mut inner = self.inner.lock().unwrap_or_else(|e| e.into_inner());
151        inner.code = code;
152        inner.errno = 0;
153        inner.message = CString::new(message).ok();
154    }
155
156    /// Record an OS-level error, deriving the message from `strerror`.
157    pub fn set_system(&self, errnum: i32) {
158        let mut inner = self.inner.lock().unwrap_or_else(|e| e.into_inner());
159        inner.code = ErrorCode::System as i32;
160        inner.errno = errnum;
161        inner.message = CString::new(strerror(errnum)).ok();
162    }
163
164    /// Record an engine error.
165    pub fn set_error(&self, err: &asdf_core::Error) {
166        match err.errno() {
167            Some(n) => {
168                self.set_system(n);
169                // Keep the engine's richer message rather than bare strerror.
170                let mut inner = self.inner.lock().unwrap_or_else(|e| e.into_inner());
171                inner.message = CString::new(err.message()).ok();
172            }
173            None => self.set(err.code() as i32, err.message()),
174        }
175    }
176
177    /// Clear any recorded error.
178    pub fn clear(&self) {
179        let mut inner = self.inner.lock().unwrap_or_else(|e| e.into_inner());
180        *inner = ErrorStateInner::default();
181    }
182
183    /// The recorded code.
184    pub fn code(&self) -> i32 {
185        self.inner.lock().unwrap_or_else(|e| e.into_inner()).code
186    }
187
188    /// The recorded `errno`, meaningful only for [`ErrorCode::System`].
189    pub fn errno(&self) -> i32 {
190        self.inner.lock().unwrap_or_else(|e| e.into_inner()).errno
191    }
192
193    /// A pointer to the recorded message, valid until the next error is set.
194    ///
195    /// # Safety
196    /// The returned pointer borrows from `self` and is invalidated by any
197    /// later `set`/`clear` on the same state.
198    pub fn message_ptr(&self) -> *const c_char {
199        let inner = self.inner.lock().unwrap_or_else(|e| e.into_inner());
200        match &inner.message {
201            Some(s) => s.as_ptr(),
202            None => core::ptr::null(),
203        }
204    }
205}
206
207/// The message C's `strerror` gives for an `errno`.
208///
209/// Rust's own `io::Error` display appends " (os error N)", which is helpful
210/// in a Rust message and wrong here: `asdf_error` hands this to a C caller
211/// that expects exactly what `strerror` would have said, and upstream's own
212/// tests compare it against that string.
213///
214/// `strerror_r` rather than `strerror`, since the message must not be
215/// clobbered by another thread between formatting and use.
216#[cfg(unix)]
217fn strerror(errnum: i32) -> String {
218    let mut buffer = [0 as c_char; 256];
219    // SAFETY: the buffer is ours and its length is passed correctly.
220    let rc = unsafe { libc::strerror_r(errnum, buffer.as_mut_ptr(), buffer.len()) };
221    if rc != 0 {
222        // The XSI form failed; fall back to something truthful.
223        return format!("errno {errnum}");
224    }
225    unsafe { crate::ffi::c_string_lossy(buffer.as_ptr()) }.unwrap_or_default()
226}
227
228/// The Windows CRT has no `strerror_r`; its thread-safe spelling is
229/// `strerror_s`, which the `libc` crate does not bind either. `strerror`
230/// itself is what remains, and on the Windows CRT it returns a pointer into
231/// per-thread storage rather than a shared static, so the race the POSIX
232/// branch avoids does not arise here.
233#[cfg(not(unix))]
234fn strerror(errnum: i32) -> String {
235    // SAFETY: `strerror` never returns null, and on this CRT the storage is
236    // per-thread, so it is stable until this thread calls `strerror` again.
237    unsafe { crate::ffi::c_string_lossy(libc::strerror(errnum)) }
238        .unwrap_or_else(|| format!("errno {errnum}"))
239}
240
241/// The format string for an error code, or null.
242///
243/// Called by `shim.c` to drive `vsnprintf` over the caller's varargs.
244///
245/// # Safety
246/// The returned pointer refers to a `'static` string and is always valid.
247#[unsafe(no_mangle)]
248pub extern "C" fn asdf_shim_error_format(code: c_int) -> *const c_char {
249    let idx = match usize::try_from(code) {
250        Ok(i) => i,
251        Err(_) => return core::ptr::null(),
252    };
253    match ERROR_FORMATS.get(idx) {
254        Some(Some(s)) => s.as_ptr(),
255        _ => core::ptr::null(),
256    }
257}
258
259/// The severity a given error code is logged at.
260pub fn error_log_level(code: i32) -> LogLevel {
261    usize::try_from(code)
262        .ok()
263        .and_then(|i| ERROR_LOG_LEVELS.get(i).copied())
264        .unwrap_or(LogLevel::Error)
265}
266
267// ---- The non-variadic error entry points -----------------------------
268//
269// These four were in `shim.c` because they sit beside the variadic ones, not
270// because they had to be. Being C cost them their export: rustc gives a
271// cdylib its own symbol list, naming only Rust `#[no_mangle]` symbols, and a
272// C symbol not in that list is localised. On Linux the version script this
273// crate's `build.rs` emits puts them back; Mach-O has no version script, so
274// on macOS they simply were not in the dylib, and a C caller using
275// `ASDF_ERROR_OOM` failed to link. Defining them in Rust makes rustc export
276// them on every platform, which is the fix rather than a workaround.
277
278/// Record an out-of-memory error against a file handle.
279///
280/// # Safety
281/// `file` must be null or a valid file handle; `src_file` a C string or null.
282#[unsafe(no_mangle)]
283pub unsafe extern "C" fn asdf_file_error_oom(
284    file: *mut crate::file_ffi::AsdfFile,
285    src_file: *const c_char,
286    lineno: c_int,
287) {
288    unsafe { asdf_shim_error_set(file.cast(), 0, OOM_CODE, src_file, lineno, OOM_MESSAGE.as_ptr()) }
289}
290
291/// Record an out-of-memory error against a value handle.
292///
293/// # Safety
294/// As [`asdf_file_error_oom`], for a value handle.
295#[unsafe(no_mangle)]
296pub unsafe extern "C" fn asdf_value_error_oom(
297    value: *mut crate::file_ffi::AsdfValue,
298    src_file: *const c_char,
299    lineno: c_int,
300) {
301    unsafe {
302        asdf_shim_error_set(value.cast(), 1, OOM_CODE, src_file, lineno, OOM_MESSAGE.as_ptr())
303    }
304}
305
306/// Record an OS-level error against a file handle.
307///
308/// # Safety
309/// As [`asdf_file_error_oom`].
310#[unsafe(no_mangle)]
311pub unsafe extern "C" fn asdf_file_error_system(
312    file: *mut crate::file_ffi::AsdfFile,
313    errnum: c_int,
314    src_file: *const c_char,
315    lineno: c_int,
316) {
317    unsafe { asdf_shim_error_set_system(file.cast(), 0, errnum, src_file, lineno) }
318}
319
320/// Record an OS-level error against a value handle.
321///
322/// # Safety
323/// As [`asdf_file_error_oom`], for a value handle.
324#[unsafe(no_mangle)]
325pub unsafe extern "C" fn asdf_value_error_system(
326    value: *mut crate::file_ffi::AsdfValue,
327    errnum: c_int,
328    src_file: *const c_char,
329    lineno: c_int,
330) {
331    unsafe { asdf_shim_error_set_system(value.cast(), 1, errnum, src_file, lineno) }
332}
333
334/// `ASDF_ERR_OUT_OF_MEMORY`, and the message upstream's shim passed with it.
335const OOM_CODE: c_int = ErrorCode::OutOfMemory as c_int;
336const OOM_MESSAGE: &CStr = c"out of memory";
337
338/// Record an already-formatted error against a handle.
339///
340/// # Safety
341/// `obj` must be null, or a valid `asdf_file_t *` when `is_value` is 0, or a
342/// valid `asdf_value_t *` when it is 1. `msg` must be a valid NUL-terminated
343/// string.
344#[unsafe(no_mangle)]
345pub unsafe extern "C" fn asdf_shim_error_set(
346    obj: *mut c_void,
347    is_value: c_int,
348    code: c_int,
349    src_file: *const c_char,
350    lineno: c_int,
351    msg: *const c_char,
352) {
353    crate::panic::guard("asdf_shim_error_set", (), || {
354        let text = unsafe { crate::ffi::c_string_lossy(msg) }.unwrap_or_default();
355        let text = if text.is_empty() {
356            usize::try_from(code)
357                .ok()
358                .and_then(|i| ERROR_FORMATS.get(i).copied().flatten())
359                .map(|s| s.to_string_lossy().into_owned())
360                .unwrap_or_else(|| "unknown error".to_string())
361        } else {
362            text
363        };
364
365        if let Some(state) = unsafe { state_for(obj, is_value) } {
366            state.set(code, text.clone());
367        }
368        unsafe { emit_log(obj, is_value, error_log_level(code), src_file, lineno, &text) };
369    });
370}
371
372/// Record an OS-level error against a handle.
373///
374/// # Safety
375/// Same requirements as [`asdf_shim_error_set`].
376#[unsafe(no_mangle)]
377pub unsafe extern "C" fn asdf_shim_error_set_system(
378    obj: *mut c_void,
379    is_value: c_int,
380    errnum: c_int,
381    src_file: *const c_char,
382    lineno: c_int,
383) {
384    crate::panic::guard("asdf_shim_error_set_system", (), || {
385        let text = strerror(errnum);
386        if let Some(state) = unsafe { state_for(obj, is_value) } {
387            state.set_system(errnum);
388        }
389        unsafe { emit_log(obj, is_value, LogLevel::Error, src_file, lineno, &text) };
390    });
391}
392
393/// Emit an already-formatted log message.
394///
395/// # Safety
396/// `file` must be null or a valid `asdf_file_t *`; the string arguments must
397/// be valid NUL-terminated strings or null.
398#[unsafe(no_mangle)]
399pub unsafe extern "C" fn asdf_shim_log_message(
400    file: *const c_void,
401    level: c_int,
402    src_file: *const c_char,
403    lineno: c_int,
404    msg: *const c_char,
405) {
406    crate::panic::guard("asdf_shim_log_message", (), || {
407        let Some(level) = LogLevel::from_i32(level) else { return };
408        let text = unsafe { crate::ffi::c_string_lossy(msg) }.unwrap_or_default();
409        unsafe { emit_log(file.cast_mut(), 0, level, src_file, lineno, &text) };
410    });
411}
412
413/// Resolve a handle to its error state.
414///
415/// # Safety
416/// See [`asdf_shim_error_set`].
417unsafe fn state_for(obj: *mut c_void, is_value: c_int) -> Option<&'static ErrorState> {
418    if obj.is_null() {
419        return None;
420    }
421    // A value's errors belong to its file: `ASDF_ERROR_COMMON(value, ..)`
422    // followed by `asdf_error_code(file)` is how upstream's own tests read
423    // them back.
424    let file = if is_value != 0 {
425        crate::file_ffi::value_file(obj.cast::<crate::file_ffi::AsdfValue>())?
426    } else {
427        obj.cast::<crate::file_ffi::AsdfFile>()
428    };
429    crate::file_ffi::error_state(file)
430}
431
432/// Write a log line if it meets the active threshold.
433///
434/// # Safety
435/// See [`asdf_shim_log_message`].
436unsafe fn emit_log(
437    _obj: *mut c_void,
438    _is_value: c_int,
439    level: LogLevel,
440    src_file: *const c_char,
441    lineno: c_int,
442    msg: &str,
443) {
444    if level == LogLevel::None || level < default_log_level() {
445        return;
446    }
447    // A null `src_file` is what the shim passes when it has no source
448    // location to report, and upstream prints `?` for it.
449    let src = unsafe { crate::ffi::c_string_lossy(src_file) }.unwrap_or_else(|| "?".into());
450    eprintln!("{} libasdf {}:{}: {}", level.as_str(), src, lineno, msg);
451}
452
453/// Emit a log line against a file, honouring its own log configuration.
454///
455/// A file opened with an `asdf_config_t` may name its own stream and level,
456/// which is how a caller captures warnings; without one this falls back to
457/// the process-wide default.
458pub(crate) fn log_to_file(file: *mut crate::file_ffi::AsdfFile, level: LogLevel, msg: &str) {
459    let config = crate::file_ffi::file_config(file).unwrap_or_default();
460    let threshold =
461        if config.log_level == LogLevel::None { default_log_level() } else { config.log_level };
462    if level == LogLevel::None || level < threshold {
463        return;
464    }
465
466    let line = format!("{} libasdf: {msg}\n", level.as_str());
467    if config.log_stream.is_null() {
468        eprint!("{line}");
469        return;
470    }
471    // SAFETY: the caller's `asdf_config_t` named this stream and, by the C
472    // contract, keeps it open for the file's lifetime.
473    unsafe {
474        libc::fwrite(
475            line.as_ptr().cast::<c_void>(),
476            1,
477            line.len(),
478            config.log_stream.cast::<libc::FILE>(),
479        );
480        libc::fflush(config.log_stream.cast::<libc::FILE>());
481    }
482}
483
484/// The threshold used when a file carries no explicit configuration.
485///
486/// Taken from `ASDF_LOG_LEVEL`, defaulting to `WARN`, as upstream documents.
487pub fn default_log_level() -> LogLevel {
488    static CACHED: std::sync::OnceLock<LogLevel> = std::sync::OnceLock::new();
489    *CACHED.get_or_init(|| {
490        std::env::var("ASDF_LOG_LEVEL")
491            .ok()
492            .and_then(|v| LogLevel::from_name(&v))
493            .unwrap_or(LogLevel::Warn)
494    })
495}
496
497#[cfg(test)]
498mod tests {
499    use super::*;
500
501    #[test]
502    fn every_error_code_has_a_table_entry() {
503        // The tables are indexed by code, so a gap would be a silent
504        // out-of-bounds read on the C side.
505        assert_eq!(ERROR_FORMATS.len(), 17);
506        assert_eq!(ERROR_LOG_LEVELS.len(), 17);
507        assert_eq!(ERROR_FORMATS.len(), ERROR_LOG_LEVELS.len());
508    }
509
510    #[test]
511    fn format_strings_match_upstream_conversions() {
512        // The number of %s conversions is the documented contract for
513        // ASDF_ERROR_COMMON's variadic arguments.
514        let invalid = ERROR_FORMATS[ErrorCode::InvalidArgument as usize].unwrap();
515        assert_eq!(invalid.to_str().unwrap().matches("%s").count(), 2);
516
517        for code in [
518            ErrorCode::UnknownCompression,
519            ErrorCode::CompressionFailed,
520            ErrorCode::ExtensionNotFound,
521            ErrorCode::OverLimit,
522        ] {
523            let f = ERROR_FORMATS[code as usize].unwrap();
524            assert_eq!(f.to_str().unwrap().matches("%s").count(), 1, "{code:?}");
525        }
526    }
527
528    #[test]
529    fn system_and_none_have_no_format() {
530        assert!(ERROR_FORMATS[ErrorCode::None as usize].is_none());
531        assert!(ERROR_FORMATS[ErrorCode::System as usize].is_none());
532    }
533
534    #[test]
535    fn error_format_lookup_is_bounds_safe() {
536        assert!(!asdf_shim_error_format(ErrorCode::UnexpectedEof as i32).is_null());
537        assert!(asdf_shim_error_format(ErrorCode::None as i32).is_null());
538        assert!(asdf_shim_error_format(9999).is_null());
539        assert!(asdf_shim_error_format(-1).is_null());
540    }
541
542    #[test]
543    fn log_levels_match_upstream() {
544        assert_eq!(error_log_level(ErrorCode::OutOfMemory as i32), LogLevel::Fatal);
545        assert_eq!(error_log_level(ErrorCode::YamlParserInitFailed as i32), LogLevel::Fatal);
546        assert_eq!(error_log_level(ErrorCode::ExtensionNotFound as i32), LogLevel::Warn);
547        assert_eq!(error_log_level(ErrorCode::UnexpectedEof as i32), LogLevel::Error);
548    }
549
550    #[test]
551    fn error_state_round_trips() {
552        let s = ErrorState::default();
553        assert_eq!(s.code(), 0);
554        assert!(s.message_ptr().is_null());
555
556        s.set(ErrorCode::UnexpectedEof as i32, "truncated");
557        assert_eq!(s.code(), ErrorCode::UnexpectedEof as i32);
558        let msg = unsafe { CStr::from_ptr(s.message_ptr()) };
559        assert_eq!(msg.to_str().unwrap(), "truncated");
560
561        s.clear();
562        assert_eq!(s.code(), 0);
563    }
564
565    #[test]
566    fn system_errors_carry_errno() {
567        let s = ErrorState::default();
568        s.set_system(2);
569        assert_eq!(s.code(), ErrorCode::System as i32);
570        assert_eq!(s.errno(), 2);
571        assert!(!s.message_ptr().is_null());
572    }
573
574    #[test]
575    fn messages_with_interior_nul_do_not_panic() {
576        let s = ErrorState::default();
577        s.set(1, "bad\0message");
578        // CString rejects it; the state simply reports no message.
579        assert!(s.message_ptr().is_null());
580    }
581
582    #[test]
583    fn log_level_names_round_trip() {
584        for level in [
585            LogLevel::None,
586            LogLevel::Trace,
587            LogLevel::Debug,
588            LogLevel::Info,
589            LogLevel::Warn,
590            LogLevel::Error,
591            LogLevel::Fatal,
592        ] {
593            assert_eq!(LogLevel::from_name(level.as_str()), Some(level));
594            assert_eq!(LogLevel::from_i32(level as i32), Some(level));
595        }
596        // The environment variable is documented as case-insensitive.
597        assert_eq!(LogLevel::from_name("warn"), Some(LogLevel::Warn));
598        assert_eq!(LogLevel::from_name("nonsense"), None);
599    }
600}