use std::{
borrow::Cow,
ffi::{CStr, c_char, c_void},
ptr,
};
use log::Level;
use open62541_sys::{
UA_LogCategory, UA_LogLevel, UA_Logger, va_list_, vsnprintf_va_copy, vsnprintf_va_end,
};
use crate::ua;
const LOG_TARGET: &str = "open62541_sys";
pub(crate) fn logger() -> ua::Logger {
unsafe extern "C" fn log_c(
_log_context: *mut c_void,
level: UA_LogLevel,
category: UA_LogCategory,
msg: *const c_char,
args: va_list_,
) {
let level = match level {
UA_LogLevel::UA_LOGLEVEL_FATAL | UA_LogLevel::UA_LOGLEVEL_ERROR => Level::Error,
UA_LogLevel::UA_LOGLEVEL_WARNING => Level::Warn,
UA_LogLevel::UA_LOGLEVEL_INFO => Level::Info,
UA_LogLevel::UA_LOGLEVEL_DEBUG => Level::Debug,
UA_LogLevel::UA_LOGLEVEL_TRACE => Level::Trace,
#[expect(clippy::match_same_arms, reason = "distinction of cases")]
_ => Level::Error,
};
if !log::log_enabled!(target: LOG_TARGET, level) {
return;
}
let msg = format_message(msg, args);
let msg = match msg {
Some(ref msg) => CStr::from_bytes_with_nul(msg)
.unwrap_or(c"Invalid log message")
.to_string_lossy(),
None => Cow::Borrowed("Unknown log message"),
};
let category = log_category(&category);
log::log!(target: LOG_TARGET, level, "({category}) {msg}");
}
unsafe extern "C" fn clear_c(logger: *mut UA_Logger) {
log::debug!("Clearing `log` logger");
let logger = unsafe { Box::from_raw(logger) };
#[expect(unpredictable_function_pointer_comparisons, reason = "extern 'C'")]
{
debug_assert!(logger.log.is_some_and(|log| log == log_c));
debug_assert!(logger.clear.is_some_and(|clear| clear == clear_c));
}
debug_assert!(logger.context.is_null());
drop(logger);
}
log::debug!("Creating `log` logger");
let logger = Box::leak(Box::new(UA_Logger {
log: Some(log_c),
context: ptr::null_mut(),
clear: Some(clear_c),
}));
unsafe { ua::Logger::from_raw(logger) }
}
const FORMAT_MESSAGE_DEFAULT_BUFFER_LEN: usize = 128;
const FORMAT_MESSAGE_MAXIMUM_BUFFER_LEN: usize = 65536;
fn format_message(msg: *const c_char, args: va_list_) -> Option<Vec<u8>> {
let mut msg_buffer: Vec<u8> = vec![0; FORMAT_MESSAGE_DEFAULT_BUFFER_LEN];
loop {
let result = unsafe {
vsnprintf_va_copy(
msg_buffer.as_mut_ptr().cast::<c_char>(),
msg_buffer.len(),
msg,
args,
)
};
let Ok(msg_len) = usize::try_from(result) else {
debug_assert!(result < 0);
unsafe { vsnprintf_va_end(args) }
return None;
};
let buffer_len = msg_len + 1;
if buffer_len > msg_buffer.len() {
debug_assert_eq!(msg_buffer.last(), Some(&0));
if msg_buffer.len() < FORMAT_MESSAGE_MAXIMUM_BUFFER_LEN {
msg_buffer.resize(FORMAT_MESSAGE_MAXIMUM_BUFFER_LEN, 0);
continue;
}
for char in msg_buffer.iter_mut().rev().skip(1).take(3) {
*char = b'.';
}
} else {
msg_buffer.truncate(buffer_len);
}
break;
}
unsafe { vsnprintf_va_end(args) }
debug_assert_eq!(msg_buffer.last(), Some(&0));
Some(msg_buffer)
}
const LOG_CATEGORY_NETWORK: &str = "network";
const LOG_CATEGORY_SECURECHANNEL: &str = "channel";
const LOG_CATEGORY_SESSION: &str = "session";
const LOG_CATEGORY_SERVER: &str = "server";
const LOG_CATEGORY_CLIENT: &str = "client";
const LOG_CATEGORY_USERLAND: &str = "userland";
const LOG_CATEGORY_SECURITYPOLICY: &str = "security";
const LOG_CATEGORY_EVENTLOOP: &str = "eventloop";
const LOG_CATEGORY_PUBSUB: &str = "pubsub";
const LOG_CATEGORY_DISCOVERY: &str = "discovery";
const LOG_CATEGORY_UNKNOWN: &str = "unknown";
const fn log_category(category: &UA_LogCategory) -> &'static str {
match *category {
UA_LogCategory::UA_LOGCATEGORY_NETWORK => LOG_CATEGORY_NETWORK,
UA_LogCategory::UA_LOGCATEGORY_SECURECHANNEL => LOG_CATEGORY_SECURECHANNEL,
UA_LogCategory::UA_LOGCATEGORY_SESSION => LOG_CATEGORY_SESSION,
UA_LogCategory::UA_LOGCATEGORY_SERVER => LOG_CATEGORY_SERVER,
UA_LogCategory::UA_LOGCATEGORY_CLIENT => LOG_CATEGORY_CLIENT,
UA_LogCategory::UA_LOGCATEGORY_USERLAND => LOG_CATEGORY_USERLAND,
UA_LogCategory::UA_LOGCATEGORY_SECURITYPOLICY => LOG_CATEGORY_SECURITYPOLICY,
UA_LogCategory::UA_LOGCATEGORY_EVENTLOOP => LOG_CATEGORY_EVENTLOOP,
UA_LogCategory::UA_LOGCATEGORY_PUBSUB => LOG_CATEGORY_PUBSUB,
UA_LogCategory::UA_LOGCATEGORY_DISCOVERY => LOG_CATEGORY_DISCOVERY,
_ => LOG_CATEGORY_UNKNOWN,
}
}