use std::ffi::{CStr, CString, c_char, c_int, c_void};
use std::sync::Mutex;
use asdf_core::ErrorCode;
const ERROR_FORMATS: &[Option<&CStr>] = &[
None, Some(c"unknown parser state"), Some(c"failed to initialize stream"), Some(c"cannot write to a read-only stream or file"), Some(c"invalid ASDF header"), Some(c"unexpected end of file"), Some(c"invalid block header"), Some(c"block magic mismatch"), Some(c"YAML parser initialization failed"), Some(c"YAML parsing failed"), Some(c"out of memory"), None, Some(c"invalid argument for %s: %s"), Some(c"unknown compression type: %s"), Some(c"compression error: %s"), Some(c"no serializer registered for the %s extension"), Some(c"over limit: %s"), ];
const ERROR_LOG_LEVELS: &[LogLevel] = &[
LogLevel::None, LogLevel::Error, LogLevel::Error, LogLevel::Error, LogLevel::Error, LogLevel::Error, LogLevel::Error, LogLevel::Error, LogLevel::Fatal, LogLevel::Error, LogLevel::Fatal, LogLevel::Error, LogLevel::Error, LogLevel::Error, LogLevel::Error, LogLevel::Warn, LogLevel::Error, ];
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug, Default)]
#[repr(i32)]
pub enum LogLevel {
#[default]
None = 0,
Trace,
Debug,
Info,
Warn,
Error,
Fatal,
}
impl LogLevel {
pub fn from_name(name: &str) -> Option<Self> {
match name.to_ascii_uppercase().as_str() {
"NONE" => Some(LogLevel::None),
"TRACE" => Some(LogLevel::Trace),
"DEBUG" => Some(LogLevel::Debug),
"INFO" => Some(LogLevel::Info),
"WARN" => Some(LogLevel::Warn),
"ERROR" => Some(LogLevel::Error),
"FATAL" => Some(LogLevel::Fatal),
_ => None,
}
}
pub fn as_str(self) -> &'static str {
match self {
LogLevel::None => "NONE",
LogLevel::Trace => "TRACE",
LogLevel::Debug => "DEBUG",
LogLevel::Info => "INFO",
LogLevel::Warn => "WARN",
LogLevel::Error => "ERROR",
LogLevel::Fatal => "FATAL",
}
}
fn from_i32(v: i32) -> Option<Self> {
match v {
0 => Some(LogLevel::None),
1 => Some(LogLevel::Trace),
2 => Some(LogLevel::Debug),
3 => Some(LogLevel::Info),
4 => Some(LogLevel::Warn),
5 => Some(LogLevel::Error),
6 => Some(LogLevel::Fatal),
_ => None,
}
}
}
#[derive(Default, Debug)]
pub struct ErrorState {
inner: Mutex<ErrorStateInner>,
}
#[derive(Default, Debug)]
struct ErrorStateInner {
code: i32,
errno: i32,
message: Option<CString>,
}
impl ErrorState {
pub fn set(&self, code: i32, message: impl Into<Vec<u8>>) {
let mut inner = self.inner.lock().unwrap_or_else(|e| e.into_inner());
inner.code = code;
inner.errno = 0;
inner.message = CString::new(message).ok();
}
pub fn set_system(&self, errnum: i32) {
let mut inner = self.inner.lock().unwrap_or_else(|e| e.into_inner());
inner.code = ErrorCode::System as i32;
inner.errno = errnum;
inner.message = CString::new(strerror(errnum)).ok();
}
pub fn set_error(&self, err: &asdf_core::Error) {
match err.errno() {
Some(n) => {
self.set_system(n);
let mut inner = self.inner.lock().unwrap_or_else(|e| e.into_inner());
inner.message = CString::new(err.message()).ok();
}
None => self.set(err.code() as i32, err.message()),
}
}
pub fn clear(&self) {
let mut inner = self.inner.lock().unwrap_or_else(|e| e.into_inner());
*inner = ErrorStateInner::default();
}
pub fn code(&self) -> i32 {
self.inner.lock().unwrap_or_else(|e| e.into_inner()).code
}
pub fn errno(&self) -> i32 {
self.inner.lock().unwrap_or_else(|e| e.into_inner()).errno
}
pub fn message_ptr(&self) -> *const c_char {
let inner = self.inner.lock().unwrap_or_else(|e| e.into_inner());
match &inner.message {
Some(s) => s.as_ptr(),
None => std::ptr::null(),
}
}
}
#[cfg(unix)]
fn strerror(errnum: i32) -> String {
let mut buffer = [0 as c_char; 256];
let rc = unsafe { libc::strerror_r(errnum, buffer.as_mut_ptr(), buffer.len()) };
if rc != 0 {
return format!("errno {errnum}");
}
unsafe { crate::ffi::c_string_lossy(buffer.as_ptr()) }.unwrap_or_default()
}
#[cfg(not(unix))]
fn strerror(errnum: i32) -> String {
unsafe { crate::ffi::c_string_lossy(libc::strerror(errnum)) }
.unwrap_or_else(|| format!("errno {errnum}"))
}
#[unsafe(no_mangle)]
pub extern "C" fn asdf_shim_error_format(code: c_int) -> *const c_char {
let idx = match usize::try_from(code) {
Ok(i) => i,
Err(_) => return std::ptr::null(),
};
match ERROR_FORMATS.get(idx) {
Some(Some(s)) => s.as_ptr(),
_ => std::ptr::null(),
}
}
pub fn error_log_level(code: i32) -> LogLevel {
usize::try_from(code)
.ok()
.and_then(|i| ERROR_LOG_LEVELS.get(i).copied())
.unwrap_or(LogLevel::Error)
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn asdf_file_error_oom(
file: *mut crate::file_ffi::AsdfFile,
src_file: *const c_char,
lineno: c_int,
) {
unsafe { asdf_shim_error_set(file.cast(), 0, OOM_CODE, src_file, lineno, OOM_MESSAGE.as_ptr()) }
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn asdf_value_error_oom(
value: *mut crate::file_ffi::AsdfValue,
src_file: *const c_char,
lineno: c_int,
) {
unsafe {
asdf_shim_error_set(value.cast(), 1, OOM_CODE, src_file, lineno, OOM_MESSAGE.as_ptr())
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn asdf_file_error_system(
file: *mut crate::file_ffi::AsdfFile,
errnum: c_int,
src_file: *const c_char,
lineno: c_int,
) {
unsafe { asdf_shim_error_set_system(file.cast(), 0, errnum, src_file, lineno) }
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn asdf_value_error_system(
value: *mut crate::file_ffi::AsdfValue,
errnum: c_int,
src_file: *const c_char,
lineno: c_int,
) {
unsafe { asdf_shim_error_set_system(value.cast(), 1, errnum, src_file, lineno) }
}
const OOM_CODE: c_int = ErrorCode::OutOfMemory as c_int;
const OOM_MESSAGE: &CStr = c"out of memory";
#[unsafe(no_mangle)]
pub unsafe extern "C" fn asdf_shim_error_set(
obj: *mut c_void,
is_value: c_int,
code: c_int,
src_file: *const c_char,
lineno: c_int,
msg: *const c_char,
) {
crate::panic::guard("asdf_shim_error_set", (), || {
let text = unsafe { crate::ffi::c_string_lossy(msg) }.unwrap_or_default();
let text = if text.is_empty() {
usize::try_from(code)
.ok()
.and_then(|i| ERROR_FORMATS.get(i).copied().flatten())
.map(|s| s.to_string_lossy().into_owned())
.unwrap_or_else(|| "unknown error".to_string())
} else {
text
};
if let Some(state) = unsafe { state_for(obj, is_value) } {
state.set(code, text.clone());
}
unsafe { emit_log(obj, is_value, error_log_level(code), src_file, lineno, &text) };
});
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn asdf_shim_error_set_system(
obj: *mut c_void,
is_value: c_int,
errnum: c_int,
src_file: *const c_char,
lineno: c_int,
) {
crate::panic::guard("asdf_shim_error_set_system", (), || {
let text = strerror(errnum);
if let Some(state) = unsafe { state_for(obj, is_value) } {
state.set_system(errnum);
}
unsafe { emit_log(obj, is_value, LogLevel::Error, src_file, lineno, &text) };
});
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn asdf_shim_log_message(
file: *const c_void,
level: c_int,
src_file: *const c_char,
lineno: c_int,
msg: *const c_char,
) {
crate::panic::guard("asdf_shim_log_message", (), || {
let Some(level) = LogLevel::from_i32(level) else { return };
let text = unsafe { crate::ffi::c_string_lossy(msg) }.unwrap_or_default();
unsafe { emit_log(file.cast_mut(), 0, level, src_file, lineno, &text) };
});
}
unsafe fn state_for(obj: *mut c_void, is_value: c_int) -> Option<&'static ErrorState> {
if obj.is_null() {
return None;
}
let file = if is_value != 0 {
crate::file_ffi::value_file(obj.cast::<crate::file_ffi::AsdfValue>())?
} else {
obj.cast::<crate::file_ffi::AsdfFile>()
};
crate::file_ffi::error_state(file)
}
unsafe fn emit_log(
_obj: *mut c_void,
_is_value: c_int,
level: LogLevel,
src_file: *const c_char,
lineno: c_int,
msg: &str,
) {
if level == LogLevel::None || level < default_log_level() {
return;
}
let src = unsafe { crate::ffi::c_string_lossy(src_file) }.unwrap_or_else(|| "?".into());
eprintln!("{} libasdf {}:{}: {}", level.as_str(), src, lineno, msg);
}
pub(crate) fn log_to_file(file: *mut crate::file_ffi::AsdfFile, level: LogLevel, msg: &str) {
let config = crate::file_ffi::file_config(file).unwrap_or_default();
let threshold =
if config.log_level == LogLevel::None { default_log_level() } else { config.log_level };
if level == LogLevel::None || level < threshold {
return;
}
let line = format!("{} libasdf: {msg}\n", level.as_str());
if config.log_stream.is_null() {
eprint!("{line}");
return;
}
unsafe {
libc::fwrite(
line.as_ptr().cast::<c_void>(),
1,
line.len(),
config.log_stream.cast::<libc::FILE>(),
);
libc::fflush(config.log_stream.cast::<libc::FILE>());
}
}
pub fn default_log_level() -> LogLevel {
static CACHED: std::sync::OnceLock<LogLevel> = std::sync::OnceLock::new();
*CACHED.get_or_init(|| {
std::env::var("ASDF_LOG_LEVEL")
.ok()
.and_then(|v| LogLevel::from_name(&v))
.unwrap_or(LogLevel::Warn)
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn every_error_code_has_a_table_entry() {
assert_eq!(ERROR_FORMATS.len(), 17);
assert_eq!(ERROR_LOG_LEVELS.len(), 17);
assert_eq!(ERROR_FORMATS.len(), ERROR_LOG_LEVELS.len());
}
#[test]
fn format_strings_match_upstream_conversions() {
let invalid = ERROR_FORMATS[ErrorCode::InvalidArgument as usize].unwrap();
assert_eq!(invalid.to_str().unwrap().matches("%s").count(), 2);
for code in [
ErrorCode::UnknownCompression,
ErrorCode::CompressionFailed,
ErrorCode::ExtensionNotFound,
ErrorCode::OverLimit,
] {
let f = ERROR_FORMATS[code as usize].unwrap();
assert_eq!(f.to_str().unwrap().matches("%s").count(), 1, "{code:?}");
}
}
#[test]
fn system_and_none_have_no_format() {
assert!(ERROR_FORMATS[ErrorCode::None as usize].is_none());
assert!(ERROR_FORMATS[ErrorCode::System as usize].is_none());
}
#[test]
fn error_format_lookup_is_bounds_safe() {
assert!(!asdf_shim_error_format(ErrorCode::UnexpectedEof as i32).is_null());
assert!(asdf_shim_error_format(ErrorCode::None as i32).is_null());
assert!(asdf_shim_error_format(9999).is_null());
assert!(asdf_shim_error_format(-1).is_null());
}
#[test]
fn log_levels_match_upstream() {
assert_eq!(error_log_level(ErrorCode::OutOfMemory as i32), LogLevel::Fatal);
assert_eq!(error_log_level(ErrorCode::YamlParserInitFailed as i32), LogLevel::Fatal);
assert_eq!(error_log_level(ErrorCode::ExtensionNotFound as i32), LogLevel::Warn);
assert_eq!(error_log_level(ErrorCode::UnexpectedEof as i32), LogLevel::Error);
}
#[test]
fn error_state_round_trips() {
let s = ErrorState::default();
assert_eq!(s.code(), 0);
assert!(s.message_ptr().is_null());
s.set(ErrorCode::UnexpectedEof as i32, "truncated");
assert_eq!(s.code(), ErrorCode::UnexpectedEof as i32);
let msg = unsafe { CStr::from_ptr(s.message_ptr()) };
assert_eq!(msg.to_str().unwrap(), "truncated");
s.clear();
assert_eq!(s.code(), 0);
}
#[test]
fn system_errors_carry_errno() {
let s = ErrorState::default();
s.set_system(2);
assert_eq!(s.code(), ErrorCode::System as i32);
assert_eq!(s.errno(), 2);
assert!(!s.message_ptr().is_null());
}
#[test]
fn messages_with_interior_nul_do_not_panic() {
let s = ErrorState::default();
s.set(1, "bad\0message");
assert!(s.message_ptr().is_null());
}
#[test]
fn log_level_names_round_trip() {
for level in [
LogLevel::None,
LogLevel::Trace,
LogLevel::Debug,
LogLevel::Info,
LogLevel::Warn,
LogLevel::Error,
LogLevel::Fatal,
] {
assert_eq!(LogLevel::from_name(level.as_str()), Some(level));
assert_eq!(LogLevel::from_i32(level as i32), Some(level));
}
assert_eq!(LogLevel::from_name("warn"), Some(LogLevel::Warn));
assert_eq!(LogLevel::from_name("nonsense"), None);
}
}