From 48725b50687861082d4662e560700a09e837e5d8 Mon Sep 17 00:00:00 2001 From: Nerijus Arlauskas Date: Tue, 4 Mar 2025 16:51:26 +0200 Subject: [PATCH 1/2] Split things into different files, to optimize future work. --- src/arrays.rs | 48 +++ src/config.rs | 98 +++++++ src/id.rs | 47 +++ src/lib.rs | 581 +------------------------------------ src/platform_log_writer.rs | 311 ++++++++++++++++++++ src/tests.rs | 79 +++++ 6 files changed, 598 insertions(+), 566 deletions(-) create mode 100644 src/arrays.rs create mode 100644 src/config.rs create mode 100644 src/id.rs create mode 100644 src/platform_log_writer.rs create mode 100644 src/tests.rs diff --git a/src/arrays.rs b/src/arrays.rs new file mode 100644 index 0000000..1ee31af --- /dev/null +++ b/src/arrays.rs @@ -0,0 +1,48 @@ +use std::ffi::CStr; +use std::mem::MaybeUninit; +use crate::LOGGING_TAG_MAX_LEN; + +// FIXME: When `maybe_uninit_uninit_array ` is stabilized, use it instead of this helper +pub fn uninit_array() -> [MaybeUninit; N] { + // SAFETY: Array contains MaybeUninit, which is fine to be uninit + unsafe { MaybeUninit::uninit().assume_init() } +} + +// FIXME: Remove when maybe_uninit_slice is stabilized to provide MaybeUninit::slice_assume_init_ref() +pub unsafe fn slice_assume_init_ref(slice: &[MaybeUninit]) -> &[T] { + &*(slice as *const [MaybeUninit] as *const [T]) +} + +/// Fills up `storage` with `tag` and a necessary NUL terminator, optionally ellipsizing the input +/// `tag` if it's too large. +/// +/// Returns a [`CStr`] containing the initialized portion of `storage`, including its NUL +/// terminator. +pub fn fill_tag_bytes<'a>( + storage: &'a mut [MaybeUninit; LOGGING_TAG_MAX_LEN + 1], + tag: &[u8], +) -> &'a CStr { + // FIXME: Simplify when maybe_uninit_fill with MaybeUninit::fill_from() is stabilized + let initialized = if tag.len() > LOGGING_TAG_MAX_LEN { + for (input, output) in tag + .iter() + // Elipsize the last two characters (TODO: use special … character)? + .take(LOGGING_TAG_MAX_LEN - 2) + .chain(b"..\0") + .zip(storage.iter_mut()) + { + output.write(*input); + } + storage.as_slice() + } else { + for (input, output) in tag.iter().chain(b"\0").zip(storage.iter_mut()) { + output.write(*input); + } + &storage[..tag.len() + 1] + }; + + // SAFETY: The above code ensures that `initialized` only refers to a portion of the `array` + // slice that was initialized, thus it is safe to cast those `MaybeUninit`s to `u8`: + let initialized = unsafe { slice_assume_init_ref(initialized) }; + CStr::from_bytes_with_nul(initialized).expect("Unreachable: we wrote a nul terminator") +} \ No newline at end of file diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..e6f9248 --- /dev/null +++ b/src/config.rs @@ -0,0 +1,98 @@ +use std::ffi::CString; +use std::fmt; +use log::{Level, LevelFilter, Record}; +use crate::{FormatFn, LogId}; + +/// Filter for android logger. +#[derive(Default)] +pub struct Config { + pub(crate) log_level: Option, + pub(crate) buf_id: Option, + filter: Option, + pub(crate) tag: Option, + pub(crate) custom_format: Option, +} + +impl fmt::Debug for Config { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Config") + .field("log_level", &self.log_level) + .field("buf_id", &self.buf_id) + .field("filter", &self.filter) + .field("tag", &self.tag) + .field( + "custom_format", + match &self.custom_format { + Some(_) => &"Some(_)", + None => &"None", + }, + ) + .finish() + } +} + +impl Config { + /// Changes the maximum log level. + /// + /// Note, that `Trace` is the maximum level, because it provides the + /// maximum amount of detail in the emitted logs. + /// + /// If `Off` level is provided, then nothing is logged at all. + /// + /// [`log::max_level()`] is considered as the default level. + pub fn with_max_level(mut self, level: LevelFilter) -> Self { + self.log_level = Some(level); + self + } + + /// Changes the Android logging system buffer to be used. + /// + /// By default, logs are sent to the [`Main`] log. Other logging buffers may + /// only be accessible to certain processes. + /// + /// [`Main`]: LogId::Main + pub fn with_log_buffer(mut self, buf_id: LogId) -> Self { + self.buf_id = Some(buf_id); + self + } + + pub(crate) fn filter_matches(&self, record: &Record) -> bool { + if let Some(ref filter) = self.filter { + filter.matches(record) + } else { + true + } + } + + pub(crate) fn is_loggable(&self, level: Level) -> bool { + // todo: consider __android_log_is_loggable. + level <= self.log_level.unwrap_or_else(log::max_level) + } + + pub fn with_filter(mut self, filter: env_filter::Filter) -> Self { + self.filter = Some(filter); + self + } + + pub fn with_tag>>(mut self, tag: S) -> Self { + self.tag = Some(CString::new(tag).expect("Can't convert tag to CString")); + self + } + + /// Sets the format function for formatting the log output. + /// ``` + /// # use android_logger::Config; + /// android_logger::init_once( + /// Config::default() + /// .with_max_level(log::LevelFilter::Trace) + /// .format(|f, record| write!(f, "my_app: {}", record.args())) + /// ) + /// ``` + pub fn format(mut self, format: F) -> Self + where + F: Fn(&mut dyn fmt::Write, &Record) -> fmt::Result + Sync + Send + 'static, + { + self.custom_format = Some(Box::new(format)); + self + } +} \ No newline at end of file diff --git a/src/id.rs b/src/id.rs new file mode 100644 index 0000000..0734439 --- /dev/null +++ b/src/id.rs @@ -0,0 +1,47 @@ +/// Possible identifiers of a specific buffer of Android logging system for +/// logging a message. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum LogId { + /// Main log buffer. + /// + /// This is the only log buffer available to apps. + Main, + + /// Radio log buffer. + Radio, + + /// Event log buffer. + Events, + + /// System log buffer. + System, + + /// Crash log buffer. + Crash, + + /// Kernel log buffer. + Kernel, + + /// Security log buffer. + Security, + + /// Statistics log buffer. + Stats, +} + +#[cfg(target_os = "android")] +impl LogId { + pub(crate) const fn to_native(log_id: Option) -> Option { + match log_id { + Some(Self::Main) => Some(log_ffi::log_id_t::MAIN), + Some(Self::Radio) => Some(log_ffi::log_id_t::RADIO), + Some(Self::Events) => Some(log_ffi::log_id_t::EVENTS), + Some(Self::System) => Some(log_ffi::log_id_t::SYSTEM), + Some(Self::Crash) => Some(log_ffi::log_id_t::CRASH), + Some(Self::Kernel) => Some(log_ffi::log_id_t::KERNEL), + Some(Self::Security) => Some(log_ffi::log_id_t::SECURITY), + Some(Self::Stats) => Some(log_ffi::log_id_t::STATS), + None => None, + } + } +} \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index a6d1eb5..4f89913 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -66,66 +66,26 @@ #[cfg(target_os = "android")] extern crate android_log_sys as log_ffi; -use log::{Level, LevelFilter, Log, Metadata, Record}; -#[cfg(target_os = "android")] -use log_ffi::LogPriority; +use log::{Log, Metadata, Record}; use std::ffi::{CStr, CString}; use std::fmt; -use std::mem::{self, MaybeUninit}; -use std::ptr; +use std::mem::{MaybeUninit}; use std::sync::OnceLock; pub use env_filter::{Builder as FilterBuilder, Filter}; +pub use config::{Config}; +pub use id::{LogId}; +use crate::arrays::{fill_tag_bytes, uninit_array}; +use crate::platform_log_writer::PlatformLogWriter; pub(crate) type FormatFn = Box fmt::Result + Sync + Send>; -/// Possible identifiers of a specific buffer of Android logging system for -/// logging a message. -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub enum LogId { - /// Main log buffer. - /// - /// This is the only log buffer available to apps. - Main, - - /// Radio log buffer. - Radio, - - /// Event log buffer. - Events, - - /// System log buffer. - System, - - /// Crash log buffer. - Crash, - - /// Kernel log buffer. - Kernel, - - /// Security log buffer. - Security, - - /// Statistics log buffer. - Stats, -} - -#[cfg(target_os = "android")] -impl LogId { - const fn to_native(log_id: Option) -> Option { - match log_id { - Some(Self::Main) => Some(log_ffi::log_id_t::MAIN), - Some(Self::Radio) => Some(log_ffi::log_id_t::RADIO), - Some(Self::Events) => Some(log_ffi::log_id_t::EVENTS), - Some(Self::System) => Some(log_ffi::log_id_t::SYSTEM), - Some(Self::Crash) => Some(log_ffi::log_id_t::CRASH), - Some(Self::Kernel) => Some(log_ffi::log_id_t::KERNEL), - Some(Self::Security) => Some(log_ffi::log_id_t::SECURITY), - Some(Self::Stats) => Some(log_ffi::log_id_t::STATS), - None => None, - } - } -} +mod config; +mod id; +#[cfg(test)] +mod tests; +mod platform_log_writer; +mod arrays; /// Outputs log to Android system. #[cfg(target_os = "android")] @@ -157,7 +117,7 @@ fn android_log( /// Dummy output placeholder for tests. #[cfg(not(target_os = "android"))] -fn android_log(_buf_id: Option, _priority: Level, _tag: &CStr, _msg: &CStr) {} +fn android_log(_buf_id: Option, _priority: log::Level, _tag: &CStr, _msg: &CStr) {} /// Underlying android logger backend #[derive(Debug, Default)] @@ -186,9 +146,7 @@ const LOGGING_MSG_MAX_LEN: usize = 4000; impl Log for AndroidLogger { fn enabled(&self, metadata: &Metadata) -> bool { - let config = self.config(); - // todo: consider __android_log_is_loggable. - metadata.level() <= config.log_level.unwrap_or_else(log::max_level) + self.config().is_loggable(metadata.level()) } fn log(&self, record: &Record) { @@ -254,305 +212,6 @@ impl Log for AndroidLogger { fn flush(&self) {} } -/// Fills up `storage` with `tag` and a necessary NUL terminator, optionally ellipsizing the input -/// `tag` if it's too large. -/// -/// Returns a [`CStr`] containing the initialized portion of `storage`, including its NUL -/// terminator. -fn fill_tag_bytes<'a>( - storage: &'a mut [MaybeUninit; LOGGING_TAG_MAX_LEN + 1], - tag: &[u8], -) -> &'a CStr { - // FIXME: Simplify when maybe_uninit_fill with MaybeUninit::fill_from() is stabilized - let initialized = if tag.len() > LOGGING_TAG_MAX_LEN { - for (input, output) in tag - .iter() - // Elipsize the last two characters (TODO: use special … character)? - .take(LOGGING_TAG_MAX_LEN - 2) - .chain(b"..\0") - .zip(storage.iter_mut()) - { - output.write(*input); - } - storage.as_slice() - } else { - for (input, output) in tag.iter().chain(b"\0").zip(storage.iter_mut()) { - output.write(*input); - } - &storage[..tag.len() + 1] - }; - - // SAFETY: The above code ensures that `initialized` only refers to a portion of the `array` - // slice that was initialized, thus it is safe to cast those `MaybeUninit`s to `u8`: - let initialized = unsafe { slice_assume_init_ref(initialized) }; - CStr::from_bytes_with_nul(initialized).expect("Unreachable: we wrote a nul terminator") -} - -/// Filter for android logger. -#[derive(Default)] -pub struct Config { - log_level: Option, - buf_id: Option, - filter: Option, - tag: Option, - custom_format: Option, -} - -impl fmt::Debug for Config { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("Config") - .field("log_level", &self.log_level) - .field("buf_id", &self.buf_id) - .field("filter", &self.filter) - .field("tag", &self.tag) - .field( - "custom_format", - match &self.custom_format { - Some(_) => &"Some(_)", - None => &"None", - }, - ) - .finish() - } -} - -impl Config { - /// Changes the maximum log level. - /// - /// Note, that `Trace` is the maximum level, because it provides the - /// maximum amount of detail in the emitted logs. - /// - /// If `Off` level is provided, then nothing is logged at all. - /// - /// [`log::max_level()`] is considered as the default level. - pub fn with_max_level(mut self, level: LevelFilter) -> Self { - self.log_level = Some(level); - self - } - - /// Changes the Android logging system buffer to be used. - /// - /// By default, logs are sent to the [`Main`] log. Other logging buffers may - /// only be accessible to certain processes. - /// - /// [`Main`]: LogId::Main - pub fn with_log_buffer(mut self, buf_id: LogId) -> Self { - self.buf_id = Some(buf_id); - self - } - - fn filter_matches(&self, record: &Record) -> bool { - if let Some(ref filter) = self.filter { - filter.matches(record) - } else { - true - } - } - - pub fn with_filter(mut self, filter: env_filter::Filter) -> Self { - self.filter = Some(filter); - self - } - - pub fn with_tag>>(mut self, tag: S) -> Self { - self.tag = Some(CString::new(tag).expect("Can't convert tag to CString")); - self - } - - /// Sets the format function for formatting the log output. - /// ``` - /// # use android_logger::Config; - /// android_logger::init_once( - /// Config::default() - /// .with_max_level(log::LevelFilter::Trace) - /// .format(|f, record| write!(f, "my_app: {}", record.args())) - /// ) - /// ``` - pub fn format(mut self, format: F) -> Self - where - F: Fn(&mut dyn fmt::Write, &Record) -> fmt::Result + Sync + Send + 'static, - { - self.custom_format = Some(Box::new(format)); - self - } -} - -pub struct PlatformLogWriter<'a> { - #[cfg(target_os = "android")] - priority: LogPriority, - #[cfg(not(target_os = "android"))] - priority: Level, - #[cfg(target_os = "android")] - buf_id: Option, - #[cfg(not(target_os = "android"))] - buf_id: Option, - len: usize, - last_newline_index: usize, - tag: &'a CStr, - buffer: [MaybeUninit; LOGGING_MSG_MAX_LEN + 1], -} - -impl PlatformLogWriter<'_> { - #[cfg(target_os = "android")] - pub fn new_with_priority( - buf_id: Option, - priority: log_ffi::LogPriority, - tag: &CStr, - ) -> PlatformLogWriter<'_> { - #[allow(deprecated)] // created an issue #35 for this - PlatformLogWriter { - priority, - buf_id: LogId::to_native(buf_id), - len: 0, - last_newline_index: 0, - tag, - buffer: uninit_array(), - } - } - - #[cfg(target_os = "android")] - pub fn new(buf_id: Option, level: Level, tag: &CStr) -> PlatformLogWriter<'_> { - PlatformLogWriter::new_with_priority( - buf_id, - match level { - Level::Warn => LogPriority::WARN, - Level::Info => LogPriority::INFO, - Level::Debug => LogPriority::DEBUG, - Level::Error => LogPriority::ERROR, - Level::Trace => LogPriority::VERBOSE, - }, - tag, - ) - } - - #[cfg(not(target_os = "android"))] - pub fn new(buf_id: Option, level: Level, tag: &CStr) -> PlatformLogWriter<'_> { - #[allow(deprecated)] // created an issue #35 for this - PlatformLogWriter { - priority: level, - buf_id, - len: 0, - last_newline_index: 0, - tag, - buffer: uninit_array(), - } - } - - /// Flush some bytes to android logger. - /// - /// If there is a newline, flush up to it. - /// If ther was no newline, flush all. - /// - /// Not guaranteed to flush everything. - fn temporal_flush(&mut self) { - let total_len = self.len; - - if total_len == 0 { - return; - } - - if self.last_newline_index > 0 { - let copy_from_index = self.last_newline_index; - let remaining_chunk_len = total_len - copy_from_index; - - unsafe { self.output_specified_len(copy_from_index) }; - self.copy_bytes_to_start(copy_from_index, remaining_chunk_len); - self.len = remaining_chunk_len; - } else { - unsafe { self.output_specified_len(total_len) }; - self.len = 0; - } - self.last_newline_index = 0; - } - - /// Flush everything remaining to android logger. - pub fn flush(&mut self) { - let total_len = self.len; - - if total_len == 0 { - return; - } - - unsafe { self.output_specified_len(total_len) }; - self.len = 0; - self.last_newline_index = 0; - } - - /// Output buffer up until the \0 which will be placed at `len` position. - /// - /// # Safety - /// The first `len` bytes of `self.buffer` must be initialized. - unsafe fn output_specified_len(&mut self, len: usize) { - let mut last_byte = MaybeUninit::new(b'\0'); - - mem::swap( - &mut last_byte, - self.buffer.get_mut(len).expect("`len` is out of bounds"), - ); - - let initialized = unsafe { slice_assume_init_ref(&self.buffer[..len + 1]) }; - let msg = CStr::from_bytes_with_nul(initialized) - .expect("Unreachable: nul terminator was placed at `len`"); - android_log(self.buf_id, self.priority, self.tag, msg); - - unsafe { *self.buffer.get_unchecked_mut(len) = last_byte }; - } - - /// Copy `len` bytes from `index` position to starting position. - fn copy_bytes_to_start(&mut self, index: usize, len: usize) { - let dst = self.buffer.as_mut_ptr(); - let src = unsafe { self.buffer.as_ptr().add(index) }; - unsafe { ptr::copy(src, dst, len) }; - } -} - -impl fmt::Write for PlatformLogWriter<'_> { - fn write_str(&mut self, s: &str) -> fmt::Result { - let mut incoming_bytes = s.as_bytes(); - - while !incoming_bytes.is_empty() { - let len = self.len; - - // write everything possible to buffer and mark last \n - let new_len = len + incoming_bytes.len(); - let last_newline = self.buffer[len..LOGGING_MSG_MAX_LEN] - .iter_mut() - .zip(incoming_bytes) - .enumerate() - .fold(None, |acc, (i, (output, input))| { - output.write(*input); - if *input == b'\n' { - Some(i) - } else { - acc - } - }); - - // update last \n index - if let Some(newline) = last_newline { - self.last_newline_index = len + newline; - } - - // calculate how many bytes were written - let written_len = if new_len <= LOGGING_MSG_MAX_LEN { - // if the len was not exceeded - self.len = new_len; - new_len - len // written len - } else { - // if new length was exceeded - self.len = LOGGING_MSG_MAX_LEN; - self.temporal_flush(); - - LOGGING_MSG_MAX_LEN - len // written len - }; - - incoming_bytes = &incoming_bytes[written_len..]; - } - - Ok(()) - } -} - /// Send a log record to Android logging backend. /// /// This action does not require initialization. However, without initialization it @@ -579,214 +238,4 @@ pub fn init_once(config: Config) { } else if let Some(level) = log_level { log::set_max_level(level); } -} - -// FIXME: When `maybe_uninit_uninit_array ` is stabilized, use it instead of this helper -fn uninit_array() -> [MaybeUninit; N] { - // SAFETY: Array contains MaybeUninit, which is fine to be uninit - unsafe { MaybeUninit::uninit().assume_init() } -} - -// FIXME: Remove when maybe_uninit_slice is stabilized to provide MaybeUninit::slice_assume_init_ref() -unsafe fn slice_assume_init_ref(slice: &[MaybeUninit]) -> &[T] { - &*(slice as *const [MaybeUninit] as *const [T]) -} - -#[cfg(test)] -mod tests { - use super::*; - use std::fmt::Write; - use std::sync::atomic::{AtomicBool, Ordering}; - - #[test] - fn check_config_values() { - // Filter is checked in config_filter_match below. - let config = Config::default() - .with_max_level(LevelFilter::Trace) - .with_log_buffer(LogId::System) - .with_tag("my_app"); - - assert_eq!(config.log_level, Some(LevelFilter::Trace)); - assert_eq!(config.buf_id, Some(LogId::System)); - assert_eq!(config.tag, Some(CString::new("my_app").unwrap())); - } - - #[test] - fn log_calls_formatter() { - static FORMAT_FN_WAS_CALLED: AtomicBool = AtomicBool::new(false); - let config = Config::default() - .with_max_level(LevelFilter::Info) - .format(|_, _| { - FORMAT_FN_WAS_CALLED.store(true, Ordering::SeqCst); - Ok(()) - }); - let logger = AndroidLogger::new(config); - - logger.log(&Record::builder().level(Level::Info).build()); - - assert!(FORMAT_FN_WAS_CALLED.load(Ordering::SeqCst)); - } - - #[test] - fn logger_enabled_threshold() { - let logger = AndroidLogger::new(Config::default().with_max_level(LevelFilter::Info)); - - assert!(logger.enabled(&log::MetadataBuilder::new().level(Level::Warn).build())); - assert!(logger.enabled(&log::MetadataBuilder::new().level(Level::Info).build())); - assert!(!logger.enabled(&log::MetadataBuilder::new().level(Level::Debug).build())); - } - - // Test whether the filter gets called correctly. Not meant to be exhaustive for all filter - // options, as these are handled directly by the filter itself. - #[test] - fn config_filter_match() { - let info_record = Record::builder().level(Level::Info).build(); - let debug_record = Record::builder().level(Level::Debug).build(); - - let info_all_filter = env_filter::Builder::new().parse("info").build(); - let info_all_config = Config::default().with_filter(info_all_filter); - - assert!(info_all_config.filter_matches(&info_record)); - assert!(!info_all_config.filter_matches(&debug_record)); - } - - #[test] - fn fill_tag_bytes_truncates_long_tag() { - let too_long_tag = [b'a'; LOGGING_TAG_MAX_LEN + 20]; - - let mut result = uninit_array(); - let tag = fill_tag_bytes(&mut result, &too_long_tag); - - let mut expected_result = vec![b'a'; LOGGING_TAG_MAX_LEN - 2]; - expected_result.extend("..\0".as_bytes()); - assert_eq!(tag.to_bytes_with_nul(), expected_result); - } - - #[test] - fn fill_tag_bytes_keeps_short_tag() { - let short_tag = [b'a'; 3]; - - let mut result = uninit_array(); - let tag = fill_tag_bytes(&mut result, &short_tag); - - let mut expected_result = short_tag.to_vec(); - expected_result.push(0); - assert_eq!(tag.to_bytes_with_nul(), expected_result); - } - - #[test] - fn platform_log_writer_init_values() { - let tag = CStr::from_bytes_with_nul(b"tag\0").unwrap(); - - let writer = PlatformLogWriter::new(None, Level::Warn, tag); - - assert_eq!(writer.tag, tag); - // Android uses LogPriority instead, which doesn't implement equality checks - #[cfg(not(target_os = "android"))] - assert_eq!(writer.priority, Level::Warn); - } - - #[test] - fn temporal_flush() { - let mut writer = get_tag_writer(); - - writer - .write_str("12\n\n567\n90") - .expect("Unable to write to PlatformLogWriter"); - - assert_eq!(writer.len, 10); - writer.temporal_flush(); - // Should have flushed up until the last newline. - assert_eq!(writer.len, 3); - assert_eq!(writer.last_newline_index, 0); - assert_eq!( - unsafe { slice_assume_init_ref(&writer.buffer[..writer.len]) }, - "\n90".as_bytes() - ); - - writer.temporal_flush(); - // Should have flushed all remaining bytes. - assert_eq!(writer.len, 0); - assert_eq!(writer.last_newline_index, 0); - } - - #[test] - fn flush() { - let mut writer = get_tag_writer(); - writer - .write_str("abcdefghij\n\nklm\nnopqr\nstuvwxyz") - .expect("Unable to write to PlatformLogWriter"); - - writer.flush(); - - assert_eq!(writer.last_newline_index, 0); - assert_eq!(writer.len, 0); - } - - #[test] - fn last_newline_index() { - let mut writer = get_tag_writer(); - - writer - .write_str("12\n\n567\n90") - .expect("Unable to write to PlatformLogWriter"); - - assert_eq!(writer.last_newline_index, 7); - } - - #[test] - fn output_specified_len_leaves_buffer_unchanged() { - let mut writer = get_tag_writer(); - let log_string = "abcdefghij\n\nklm\nnopqr\nstuvwxyz"; - writer - .write_str(log_string) - .expect("Unable to write to PlatformLogWriter"); - - unsafe { writer.output_specified_len(5) }; - - assert_eq!( - unsafe { slice_assume_init_ref(&writer.buffer[..log_string.len()]) }, - log_string.as_bytes() - ); - } - - #[test] - fn copy_bytes_to_start() { - let mut writer = get_tag_writer(); - writer - .write_str("0123456789") - .expect("Unable to write to PlatformLogWriter"); - - writer.copy_bytes_to_start(3, 2); - - assert_eq!( - unsafe { slice_assume_init_ref(&writer.buffer[..10]) }, - "3423456789".as_bytes() - ); - } - - #[test] - fn copy_bytes_to_start_nop() { - let test_string = "Test_string_with\n\n\n\nnewlines\n"; - let mut writer = get_tag_writer(); - writer - .write_str(test_string) - .expect("Unable to write to PlatformLogWriter"); - - writer.copy_bytes_to_start(0, 20); - writer.copy_bytes_to_start(10, 0); - - assert_eq!( - unsafe { slice_assume_init_ref(&writer.buffer[..test_string.len()]) }, - test_string.as_bytes() - ); - } - - fn get_tag_writer() -> PlatformLogWriter<'static> { - PlatformLogWriter::new( - None, - Level::Warn, - CStr::from_bytes_with_nul(b"tag\0").unwrap(), - ) - } -} +} \ No newline at end of file diff --git a/src/platform_log_writer.rs b/src/platform_log_writer.rs new file mode 100644 index 0000000..9842b7c --- /dev/null +++ b/src/platform_log_writer.rs @@ -0,0 +1,311 @@ +use std::ffi::CStr; +use std::{fmt, mem, ptr}; +use std::mem::MaybeUninit; +use log::Level; +#[cfg(target_os = "android")] +use log_ffi::LogPriority; +use crate::{android_log, uninit_array, LogId, LOGGING_MSG_MAX_LEN}; +use crate::arrays::slice_assume_init_ref; + +/// The purpose of this "writer" is to split logged messages on whitespace when the log message +/// length exceeds the maximum. Without allocations. +pub struct PlatformLogWriter<'a> { + #[cfg(target_os = "android")] + priority: LogPriority, + #[cfg(not(target_os = "android"))] + priority: Level, + #[cfg(target_os = "android")] + buf_id: Option, + #[cfg(not(target_os = "android"))] + buf_id: Option, + len: usize, + last_newline_index: usize, + tag: &'a CStr, + buffer: [MaybeUninit; LOGGING_MSG_MAX_LEN + 1], +} + +impl PlatformLogWriter<'_> { + #[cfg(target_os = "android")] + pub fn new_with_priority( + buf_id: Option, + priority: log_ffi::LogPriority, + tag: &CStr, + ) -> PlatformLogWriter<'_> { + #[allow(deprecated)] // created an issue #35 for this + PlatformLogWriter { + priority, + buf_id: LogId::to_native(buf_id), + len: 0, + last_newline_index: 0, + tag, + buffer: uninit_array(), + } + } + + #[cfg(target_os = "android")] + pub fn new(buf_id: Option, level: Level, tag: &CStr) -> PlatformLogWriter<'_> { + PlatformLogWriter::new_with_priority( + buf_id, + match level { + Level::Warn => LogPriority::WARN, + Level::Info => LogPriority::INFO, + Level::Debug => LogPriority::DEBUG, + Level::Error => LogPriority::ERROR, + Level::Trace => LogPriority::VERBOSE, + }, + tag, + ) + } + + #[cfg(not(target_os = "android"))] + pub fn new(buf_id: Option, level: Level, tag: &CStr) -> PlatformLogWriter<'_> { + #[allow(deprecated)] // created an issue #35 for this + PlatformLogWriter { + priority: level, + buf_id, + len: 0, + last_newline_index: 0, + tag, + buffer: uninit_array(), + } + } + + /// Flush some bytes to android logger. + /// + /// If there is a newline, flush up to it. + /// If there was no newline, flush all. + /// + /// Not guaranteed to flush everything. + fn temporal_flush(&mut self) { + let total_len = self.len; + + if total_len == 0 { + return; + } + + if self.last_newline_index > 0 { + let copy_from_index = self.last_newline_index; + let remaining_chunk_len = total_len - copy_from_index; + + unsafe { self.output_specified_len(copy_from_index) }; + self.copy_bytes_to_start(copy_from_index, remaining_chunk_len); + self.len = remaining_chunk_len; + } else { + unsafe { self.output_specified_len(total_len) }; + self.len = 0; + } + self.last_newline_index = 0; + } + + /// Flush everything remaining to android logger. + pub fn flush(&mut self) { + let total_len = self.len; + + if total_len == 0 { + return; + } + + unsafe { self.output_specified_len(total_len) }; + self.len = 0; + self.last_newline_index = 0; + } + + /// Output buffer up until the \0 which will be placed at `len` position. + /// + /// # Safety + /// The first `len` bytes of `self.buffer` must be initialized. + unsafe fn output_specified_len(&mut self, len: usize) { + let mut last_byte = MaybeUninit::new(b'\0'); + + mem::swap( + &mut last_byte, + self.buffer.get_mut(len).expect("`len` is out of bounds"), + ); + + let initialized = unsafe { slice_assume_init_ref(&self.buffer[..len + 1]) }; + let msg = CStr::from_bytes_with_nul(initialized) + .expect("Unreachable: nul terminator was placed at `len`"); + android_log(self.buf_id, self.priority, self.tag, msg); + + unsafe { *self.buffer.get_unchecked_mut(len) = last_byte }; + } + + /// Copy `len` bytes from `index` position to starting position. + fn copy_bytes_to_start(&mut self, index: usize, len: usize) { + let dst = self.buffer.as_mut_ptr(); + let src = unsafe { self.buffer.as_ptr().add(index) }; + unsafe { ptr::copy(src, dst, len) }; + } +} + +impl fmt::Write for PlatformLogWriter<'_> { + fn write_str(&mut self, s: &str) -> fmt::Result { + let mut incoming_bytes = s.as_bytes(); + + while !incoming_bytes.is_empty() { + let len = self.len; + + // write everything possible to buffer and mark last \n + let new_len = len + incoming_bytes.len(); + let last_newline = self.buffer[len..LOGGING_MSG_MAX_LEN] + .iter_mut() + .zip(incoming_bytes) + .enumerate() + .fold(None, |acc, (i, (output, input))| { + output.write(*input); + if *input == b'\n' { + Some(i) + } else { + acc + } + }); + + // update last \n index + if let Some(newline) = last_newline { + self.last_newline_index = len + newline; + } + + // calculate how many bytes were written + let written_len = if new_len <= LOGGING_MSG_MAX_LEN { + // if the len was not exceeded + self.len = new_len; + new_len - len // written len + } else { + // if new length was exceeded + self.len = LOGGING_MSG_MAX_LEN; + self.temporal_flush(); + + LOGGING_MSG_MAX_LEN - len // written len + }; + + incoming_bytes = &incoming_bytes[written_len..]; + } + + Ok(()) + } +} + +#[cfg(test)] +pub mod tests { + use std::ffi::CStr; + use std::fmt::Write; + use log::Level; + use crate::arrays::slice_assume_init_ref; + use crate::platform_log_writer::PlatformLogWriter; + + #[test] + fn platform_log_writer_init_values() { + let tag = CStr::from_bytes_with_nul(b"tag\0").unwrap(); + + let writer = PlatformLogWriter::new(None, Level::Warn, tag); + + assert_eq!(writer.tag, tag); + // Android uses LogPriority instead, which doesn't implement equality checks + #[cfg(not(target_os = "android"))] + assert_eq!(writer.priority, Level::Warn); + } + + #[test] + fn temporal_flush() { + let mut writer = get_tag_writer(); + + writer + .write_str("12\n\n567\n90") + .expect("Unable to write to PlatformLogWriter"); + + assert_eq!(writer.len, 10); + writer.temporal_flush(); + // Should have flushed up until the last newline. + assert_eq!(writer.len, 3); + assert_eq!(writer.last_newline_index, 0); + assert_eq!( + unsafe { slice_assume_init_ref(&writer.buffer[..writer.len]) }, + "\n90".as_bytes() + ); + + writer.temporal_flush(); + // Should have flushed all remaining bytes. + assert_eq!(writer.len, 0); + assert_eq!(writer.last_newline_index, 0); + } + + #[test] + fn flush() { + let mut writer = get_tag_writer(); + writer + .write_str("abcdefghij\n\nklm\nnopqr\nstuvwxyz") + .expect("Unable to write to PlatformLogWriter"); + + writer.flush(); + + assert_eq!(writer.last_newline_index, 0); + assert_eq!(writer.len, 0); + } + + #[test] + fn last_newline_index() { + let mut writer = get_tag_writer(); + + writer + .write_str("12\n\n567\n90") + .expect("Unable to write to PlatformLogWriter"); + + assert_eq!(writer.last_newline_index, 7); + } + + #[test] + fn output_specified_len_leaves_buffer_unchanged() { + let mut writer = get_tag_writer(); + let log_string = "abcdefghij\n\nklm\nnopqr\nstuvwxyz"; + writer + .write_str(log_string) + .expect("Unable to write to PlatformLogWriter"); + + unsafe { writer.output_specified_len(5) }; + + assert_eq!( + unsafe { slice_assume_init_ref(&writer.buffer[..log_string.len()]) }, + log_string.as_bytes() + ); + } + + #[test] + fn copy_bytes_to_start() { + let mut writer = get_tag_writer(); + writer + .write_str("0123456789") + .expect("Unable to write to PlatformLogWriter"); + + writer.copy_bytes_to_start(3, 2); + + assert_eq!( + unsafe { slice_assume_init_ref(&writer.buffer[..10]) }, + "3423456789".as_bytes() + ); + } + + #[test] + fn copy_bytes_to_start_nop() { + let test_string = "Test_string_with\n\n\n\nnewlines\n"; + let mut writer = get_tag_writer(); + writer + .write_str(test_string) + .expect("Unable to write to PlatformLogWriter"); + + writer.copy_bytes_to_start(0, 20); + writer.copy_bytes_to_start(10, 0); + + assert_eq!( + unsafe { slice_assume_init_ref(&writer.buffer[..test_string.len()]) }, + test_string.as_bytes() + ); + } + + fn get_tag_writer() -> PlatformLogWriter<'static> { + PlatformLogWriter::new( + None, + Level::Warn, + CStr::from_bytes_with_nul(b"tag\0").unwrap(), + ) + } +} \ No newline at end of file diff --git a/src/tests.rs b/src/tests.rs new file mode 100644 index 0000000..c28e8fb --- /dev/null +++ b/src/tests.rs @@ -0,0 +1,79 @@ +use super::*; +use std::sync::atomic::{AtomicBool, Ordering}; +use log::LevelFilter; + +#[test] +fn check_config_values() { + // Filter is checked in config_filter_match below. + let config = Config::default() + .with_max_level(LevelFilter::Trace) + .with_log_buffer(LogId::System) + .with_tag("my_app"); + + assert_eq!(config.log_level, Some(LevelFilter::Trace)); + assert_eq!(config.buf_id, Some(LogId::System)); + assert_eq!(config.tag, Some(CString::new("my_app").unwrap())); +} + +#[test] +fn log_calls_formatter() { + static FORMAT_FN_WAS_CALLED: AtomicBool = AtomicBool::new(false); + let config = Config::default() + .with_max_level(LevelFilter::Info) + .format(|_, _| { + FORMAT_FN_WAS_CALLED.store(true, Ordering::SeqCst); + Ok(()) + }); + let logger = AndroidLogger::new(config); + + logger.log(&Record::builder().level(log::Level::Info).build()); + + assert!(FORMAT_FN_WAS_CALLED.load(Ordering::SeqCst)); +} + +#[test] +fn logger_enabled_threshold() { + let logger = AndroidLogger::new(Config::default().with_max_level(LevelFilter::Info)); + + assert!(logger.enabled(&log::MetadataBuilder::new().level(log::Level::Warn).build())); + assert!(logger.enabled(&log::MetadataBuilder::new().level(log::Level::Info).build())); + assert!(!logger.enabled(&log::MetadataBuilder::new().level(log::Level::Debug).build())); +} + +// Test whether the filter gets called correctly. Not meant to be exhaustive for all filter +// options, as these are handled directly by the filter itself. +#[test] +fn config_filter_match() { + let info_record = Record::builder().level(log::Level::Info).build(); + let debug_record = Record::builder().level(log::Level::Debug).build(); + + let info_all_filter = env_filter::Builder::new().parse("info").build(); + let info_all_config = Config::default().with_filter(info_all_filter); + + assert!(info_all_config.filter_matches(&info_record)); + assert!(!info_all_config.filter_matches(&debug_record)); +} + +#[test] +fn fill_tag_bytes_truncates_long_tag() { + let too_long_tag = [b'a'; LOGGING_TAG_MAX_LEN + 20]; + + let mut result = uninit_array(); + let tag = fill_tag_bytes(&mut result, &too_long_tag); + + let mut expected_result = vec![b'a'; LOGGING_TAG_MAX_LEN - 2]; + expected_result.extend("..\0".as_bytes()); + assert_eq!(tag.to_bytes_with_nul(), expected_result); +} + +#[test] +fn fill_tag_bytes_keeps_short_tag() { + let short_tag = [b'a'; 3]; + + let mut result = uninit_array(); + let tag = fill_tag_bytes(&mut result, &short_tag); + + let mut expected_result = short_tag.to_vec(); + expected_result.push(0); + assert_eq!(tag.to_bytes_with_nul(), expected_result); +} \ No newline at end of file From 1b5dd782699a6d8a91f40162ee23258f5e4301b7 Mon Sep 17 00:00:00 2001 From: Nerijus Arlauskas Date: Tue, 4 Mar 2025 17:23:11 +0200 Subject: [PATCH 2/2] Run rustfmt --- rustfmt.toml | 1 + src/arrays.rs | 96 +++--- src/config.rs | 193 ++++++------ src/id.rs | 94 +++--- src/lib.rs | 14 +- src/platform_log_writer.rs | 618 ++++++++++++++++++------------------- src/tests.rs | 158 +++++----- 7 files changed, 584 insertions(+), 590 deletions(-) create mode 100644 rustfmt.toml diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..c5cf55d --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1 @@ +style_edition = "2024" \ No newline at end of file diff --git a/src/arrays.rs b/src/arrays.rs index 1ee31af..f67b8e5 100644 --- a/src/arrays.rs +++ b/src/arrays.rs @@ -1,48 +1,48 @@ -use std::ffi::CStr; -use std::mem::MaybeUninit; -use crate::LOGGING_TAG_MAX_LEN; - -// FIXME: When `maybe_uninit_uninit_array ` is stabilized, use it instead of this helper -pub fn uninit_array() -> [MaybeUninit; N] { - // SAFETY: Array contains MaybeUninit, which is fine to be uninit - unsafe { MaybeUninit::uninit().assume_init() } -} - -// FIXME: Remove when maybe_uninit_slice is stabilized to provide MaybeUninit::slice_assume_init_ref() -pub unsafe fn slice_assume_init_ref(slice: &[MaybeUninit]) -> &[T] { - &*(slice as *const [MaybeUninit] as *const [T]) -} - -/// Fills up `storage` with `tag` and a necessary NUL terminator, optionally ellipsizing the input -/// `tag` if it's too large. -/// -/// Returns a [`CStr`] containing the initialized portion of `storage`, including its NUL -/// terminator. -pub fn fill_tag_bytes<'a>( - storage: &'a mut [MaybeUninit; LOGGING_TAG_MAX_LEN + 1], - tag: &[u8], -) -> &'a CStr { - // FIXME: Simplify when maybe_uninit_fill with MaybeUninit::fill_from() is stabilized - let initialized = if tag.len() > LOGGING_TAG_MAX_LEN { - for (input, output) in tag - .iter() - // Elipsize the last two characters (TODO: use special … character)? - .take(LOGGING_TAG_MAX_LEN - 2) - .chain(b"..\0") - .zip(storage.iter_mut()) - { - output.write(*input); - } - storage.as_slice() - } else { - for (input, output) in tag.iter().chain(b"\0").zip(storage.iter_mut()) { - output.write(*input); - } - &storage[..tag.len() + 1] - }; - - // SAFETY: The above code ensures that `initialized` only refers to a portion of the `array` - // slice that was initialized, thus it is safe to cast those `MaybeUninit`s to `u8`: - let initialized = unsafe { slice_assume_init_ref(initialized) }; - CStr::from_bytes_with_nul(initialized).expect("Unreachable: we wrote a nul terminator") -} \ No newline at end of file +use crate::LOGGING_TAG_MAX_LEN; +use std::ffi::CStr; +use std::mem::MaybeUninit; + +// FIXME: When `maybe_uninit_uninit_array` is stabilized, use it instead of this helper +pub fn uninit_array() -> [MaybeUninit; N] { + // SAFETY: Array contains MaybeUninit, which is fine to be uninit + unsafe { MaybeUninit::uninit().assume_init() } +} + +// FIXME: Remove when maybe_uninit_slice is stabilized to provide MaybeUninit::slice_assume_init_ref() +pub unsafe fn slice_assume_init_ref(slice: &[MaybeUninit]) -> &[T] { + &*(slice as *const [MaybeUninit] as *const [T]) +} + +/// Fills up `storage` with `tag` and a necessary NUL terminator, optionally ellipsizing the input +/// `tag` if it's too large. +/// +/// Returns a [`CStr`] containing the initialized portion of `storage`, including its NUL +/// terminator. +pub fn fill_tag_bytes<'a>( + storage: &'a mut [MaybeUninit; LOGGING_TAG_MAX_LEN + 1], + tag: &[u8], +) -> &'a CStr { + // FIXME: Simplify when maybe_uninit_fill with MaybeUninit::fill_from() is stabilized + let initialized = if tag.len() > LOGGING_TAG_MAX_LEN { + for (input, output) in tag + .iter() + // Elipsize the last two characters (TODO: use special … character)? + .take(LOGGING_TAG_MAX_LEN - 2) + .chain(b"..\0") + .zip(storage.iter_mut()) + { + output.write(*input); + } + storage.as_slice() + } else { + for (input, output) in tag.iter().chain(b"\0").zip(storage.iter_mut()) { + output.write(*input); + } + &storage[..tag.len() + 1] + }; + + // SAFETY: The above code ensures that `initialized` only refers to a portion of the `array` + // slice that was initialized, thus it is safe to cast those `MaybeUninit`s to `u8`: + let initialized = unsafe { slice_assume_init_ref(initialized) }; + CStr::from_bytes_with_nul(initialized).expect("Unreachable: we wrote a nul terminator") +} diff --git a/src/config.rs b/src/config.rs index e6f9248..fc8bc53 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,98 +1,95 @@ -use std::ffi::CString; -use std::fmt; -use log::{Level, LevelFilter, Record}; -use crate::{FormatFn, LogId}; - -/// Filter for android logger. -#[derive(Default)] -pub struct Config { - pub(crate) log_level: Option, - pub(crate) buf_id: Option, - filter: Option, - pub(crate) tag: Option, - pub(crate) custom_format: Option, -} - -impl fmt::Debug for Config { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("Config") - .field("log_level", &self.log_level) - .field("buf_id", &self.buf_id) - .field("filter", &self.filter) - .field("tag", &self.tag) - .field( - "custom_format", - match &self.custom_format { - Some(_) => &"Some(_)", - None => &"None", - }, - ) - .finish() - } -} - -impl Config { - /// Changes the maximum log level. - /// - /// Note, that `Trace` is the maximum level, because it provides the - /// maximum amount of detail in the emitted logs. - /// - /// If `Off` level is provided, then nothing is logged at all. - /// - /// [`log::max_level()`] is considered as the default level. - pub fn with_max_level(mut self, level: LevelFilter) -> Self { - self.log_level = Some(level); - self - } - - /// Changes the Android logging system buffer to be used. - /// - /// By default, logs are sent to the [`Main`] log. Other logging buffers may - /// only be accessible to certain processes. - /// - /// [`Main`]: LogId::Main - pub fn with_log_buffer(mut self, buf_id: LogId) -> Self { - self.buf_id = Some(buf_id); - self - } - - pub(crate) fn filter_matches(&self, record: &Record) -> bool { - if let Some(ref filter) = self.filter { - filter.matches(record) - } else { - true - } - } - - pub(crate) fn is_loggable(&self, level: Level) -> bool { - // todo: consider __android_log_is_loggable. - level <= self.log_level.unwrap_or_else(log::max_level) - } - - pub fn with_filter(mut self, filter: env_filter::Filter) -> Self { - self.filter = Some(filter); - self - } - - pub fn with_tag>>(mut self, tag: S) -> Self { - self.tag = Some(CString::new(tag).expect("Can't convert tag to CString")); - self - } - - /// Sets the format function for formatting the log output. - /// ``` - /// # use android_logger::Config; - /// android_logger::init_once( - /// Config::default() - /// .with_max_level(log::LevelFilter::Trace) - /// .format(|f, record| write!(f, "my_app: {}", record.args())) - /// ) - /// ``` - pub fn format(mut self, format: F) -> Self - where - F: Fn(&mut dyn fmt::Write, &Record) -> fmt::Result + Sync + Send + 'static, - { - self.custom_format = Some(Box::new(format)); - self - } -} \ No newline at end of file +use crate::{FormatFn, LogId}; +use log::{Level, LevelFilter, Record}; +use std::ffi::CString; +use std::fmt; + +/// Filter for android logger. +#[derive(Default)] +pub struct Config { + pub(crate) log_level: Option, + pub(crate) buf_id: Option, + filter: Option, + pub(crate) tag: Option, + pub(crate) custom_format: Option, +} + +impl fmt::Debug for Config { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Config") + .field("log_level", &self.log_level) + .field("buf_id", &self.buf_id) + .field("filter", &self.filter) + .field("tag", &self.tag) + .field("custom_format", match &self.custom_format { + Some(_) => &"Some(_)", + None => &"None", + }) + .finish() + } +} + +impl Config { + /// Changes the maximum log level. + /// + /// Note, that `Trace` is the maximum level, because it provides the + /// maximum amount of detail in the emitted logs. + /// + /// If `Off` level is provided, then nothing is logged at all. + /// + /// [`log::max_level()`] is considered as the default level. + pub fn with_max_level(mut self, level: LevelFilter) -> Self { + self.log_level = Some(level); + self + } + + /// Changes the Android logging system buffer to be used. + /// + /// By default, logs are sent to the [`Main`] log. Other logging buffers may + /// only be accessible to certain processes. + /// + /// [`Main`]: LogId::Main + pub fn with_log_buffer(mut self, buf_id: LogId) -> Self { + self.buf_id = Some(buf_id); + self + } + + pub(crate) fn filter_matches(&self, record: &Record) -> bool { + if let Some(ref filter) = self.filter { + filter.matches(record) + } else { + true + } + } + + pub(crate) fn is_loggable(&self, level: Level) -> bool { + // todo: consider __android_log_is_loggable. + level <= self.log_level.unwrap_or_else(log::max_level) + } + + pub fn with_filter(mut self, filter: env_filter::Filter) -> Self { + self.filter = Some(filter); + self + } + + pub fn with_tag>>(mut self, tag: S) -> Self { + self.tag = Some(CString::new(tag).expect("Can't convert tag to CString")); + self + } + + /// Sets the format function for formatting the log output. + /// ``` + /// # use android_logger::Config; + /// android_logger::init_once( + /// Config::default() + /// .with_max_level(log::LevelFilter::Trace) + /// .format(|f, record| write!(f, "my_app: {}", record.args())) + /// ) + /// ``` + pub fn format(mut self, format: F) -> Self + where + F: Fn(&mut dyn fmt::Write, &Record) -> fmt::Result + Sync + Send + 'static, + { + self.custom_format = Some(Box::new(format)); + self + } +} diff --git a/src/id.rs b/src/id.rs index 0734439..0b6597b 100644 --- a/src/id.rs +++ b/src/id.rs @@ -1,47 +1,47 @@ -/// Possible identifiers of a specific buffer of Android logging system for -/// logging a message. -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub enum LogId { - /// Main log buffer. - /// - /// This is the only log buffer available to apps. - Main, - - /// Radio log buffer. - Radio, - - /// Event log buffer. - Events, - - /// System log buffer. - System, - - /// Crash log buffer. - Crash, - - /// Kernel log buffer. - Kernel, - - /// Security log buffer. - Security, - - /// Statistics log buffer. - Stats, -} - -#[cfg(target_os = "android")] -impl LogId { - pub(crate) const fn to_native(log_id: Option) -> Option { - match log_id { - Some(Self::Main) => Some(log_ffi::log_id_t::MAIN), - Some(Self::Radio) => Some(log_ffi::log_id_t::RADIO), - Some(Self::Events) => Some(log_ffi::log_id_t::EVENTS), - Some(Self::System) => Some(log_ffi::log_id_t::SYSTEM), - Some(Self::Crash) => Some(log_ffi::log_id_t::CRASH), - Some(Self::Kernel) => Some(log_ffi::log_id_t::KERNEL), - Some(Self::Security) => Some(log_ffi::log_id_t::SECURITY), - Some(Self::Stats) => Some(log_ffi::log_id_t::STATS), - None => None, - } - } -} \ No newline at end of file +/// Possible identifiers of a specific buffer of Android logging system for +/// logging a message. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum LogId { + /// Main log buffer. + /// + /// This is the only log buffer available to apps. + Main, + + /// Radio log buffer. + Radio, + + /// Event log buffer. + Events, + + /// System log buffer. + System, + + /// Crash log buffer. + Crash, + + /// Kernel log buffer. + Kernel, + + /// Security log buffer. + Security, + + /// Statistics log buffer. + Stats, +} + +#[cfg(target_os = "android")] +impl LogId { + pub(crate) const fn to_native(log_id: Option) -> Option { + match log_id { + Some(Self::Main) => Some(log_ffi::log_id_t::MAIN), + Some(Self::Radio) => Some(log_ffi::log_id_t::RADIO), + Some(Self::Events) => Some(log_ffi::log_id_t::EVENTS), + Some(Self::System) => Some(log_ffi::log_id_t::SYSTEM), + Some(Self::Crash) => Some(log_ffi::log_id_t::CRASH), + Some(Self::Kernel) => Some(log_ffi::log_id_t::KERNEL), + Some(Self::Security) => Some(log_ffi::log_id_t::SECURITY), + Some(Self::Stats) => Some(log_ffi::log_id_t::STATS), + None => None, + } + } +} diff --git a/src/lib.rs b/src/lib.rs index 4f89913..a05d23d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -69,23 +69,23 @@ extern crate android_log_sys as log_ffi; use log::{Log, Metadata, Record}; use std::ffi::{CStr, CString}; use std::fmt; -use std::mem::{MaybeUninit}; +use std::mem::MaybeUninit; use std::sync::OnceLock; -pub use env_filter::{Builder as FilterBuilder, Filter}; -pub use config::{Config}; -pub use id::{LogId}; use crate::arrays::{fill_tag_bytes, uninit_array}; use crate::platform_log_writer::PlatformLogWriter; +pub use config::Config; +pub use env_filter::{Builder as FilterBuilder, Filter}; +pub use id::LogId; pub(crate) type FormatFn = Box fmt::Result + Sync + Send>; +mod arrays; mod config; mod id; +mod platform_log_writer; #[cfg(test)] mod tests; -mod platform_log_writer; -mod arrays; /// Outputs log to Android system. #[cfg(target_os = "android")] @@ -238,4 +238,4 @@ pub fn init_once(config: Config) { } else if let Some(level) = log_level { log::set_max_level(level); } -} \ No newline at end of file +} diff --git a/src/platform_log_writer.rs b/src/platform_log_writer.rs index 9842b7c..85826ca 100644 --- a/src/platform_log_writer.rs +++ b/src/platform_log_writer.rs @@ -1,311 +1,307 @@ -use std::ffi::CStr; -use std::{fmt, mem, ptr}; -use std::mem::MaybeUninit; -use log::Level; -#[cfg(target_os = "android")] -use log_ffi::LogPriority; -use crate::{android_log, uninit_array, LogId, LOGGING_MSG_MAX_LEN}; -use crate::arrays::slice_assume_init_ref; - -/// The purpose of this "writer" is to split logged messages on whitespace when the log message -/// length exceeds the maximum. Without allocations. -pub struct PlatformLogWriter<'a> { - #[cfg(target_os = "android")] - priority: LogPriority, - #[cfg(not(target_os = "android"))] - priority: Level, - #[cfg(target_os = "android")] - buf_id: Option, - #[cfg(not(target_os = "android"))] - buf_id: Option, - len: usize, - last_newline_index: usize, - tag: &'a CStr, - buffer: [MaybeUninit; LOGGING_MSG_MAX_LEN + 1], -} - -impl PlatformLogWriter<'_> { - #[cfg(target_os = "android")] - pub fn new_with_priority( - buf_id: Option, - priority: log_ffi::LogPriority, - tag: &CStr, - ) -> PlatformLogWriter<'_> { - #[allow(deprecated)] // created an issue #35 for this - PlatformLogWriter { - priority, - buf_id: LogId::to_native(buf_id), - len: 0, - last_newline_index: 0, - tag, - buffer: uninit_array(), - } - } - - #[cfg(target_os = "android")] - pub fn new(buf_id: Option, level: Level, tag: &CStr) -> PlatformLogWriter<'_> { - PlatformLogWriter::new_with_priority( - buf_id, - match level { - Level::Warn => LogPriority::WARN, - Level::Info => LogPriority::INFO, - Level::Debug => LogPriority::DEBUG, - Level::Error => LogPriority::ERROR, - Level::Trace => LogPriority::VERBOSE, - }, - tag, - ) - } - - #[cfg(not(target_os = "android"))] - pub fn new(buf_id: Option, level: Level, tag: &CStr) -> PlatformLogWriter<'_> { - #[allow(deprecated)] // created an issue #35 for this - PlatformLogWriter { - priority: level, - buf_id, - len: 0, - last_newline_index: 0, - tag, - buffer: uninit_array(), - } - } - - /// Flush some bytes to android logger. - /// - /// If there is a newline, flush up to it. - /// If there was no newline, flush all. - /// - /// Not guaranteed to flush everything. - fn temporal_flush(&mut self) { - let total_len = self.len; - - if total_len == 0 { - return; - } - - if self.last_newline_index > 0 { - let copy_from_index = self.last_newline_index; - let remaining_chunk_len = total_len - copy_from_index; - - unsafe { self.output_specified_len(copy_from_index) }; - self.copy_bytes_to_start(copy_from_index, remaining_chunk_len); - self.len = remaining_chunk_len; - } else { - unsafe { self.output_specified_len(total_len) }; - self.len = 0; - } - self.last_newline_index = 0; - } - - /// Flush everything remaining to android logger. - pub fn flush(&mut self) { - let total_len = self.len; - - if total_len == 0 { - return; - } - - unsafe { self.output_specified_len(total_len) }; - self.len = 0; - self.last_newline_index = 0; - } - - /// Output buffer up until the \0 which will be placed at `len` position. - /// - /// # Safety - /// The first `len` bytes of `self.buffer` must be initialized. - unsafe fn output_specified_len(&mut self, len: usize) { - let mut last_byte = MaybeUninit::new(b'\0'); - - mem::swap( - &mut last_byte, - self.buffer.get_mut(len).expect("`len` is out of bounds"), - ); - - let initialized = unsafe { slice_assume_init_ref(&self.buffer[..len + 1]) }; - let msg = CStr::from_bytes_with_nul(initialized) - .expect("Unreachable: nul terminator was placed at `len`"); - android_log(self.buf_id, self.priority, self.tag, msg); - - unsafe { *self.buffer.get_unchecked_mut(len) = last_byte }; - } - - /// Copy `len` bytes from `index` position to starting position. - fn copy_bytes_to_start(&mut self, index: usize, len: usize) { - let dst = self.buffer.as_mut_ptr(); - let src = unsafe { self.buffer.as_ptr().add(index) }; - unsafe { ptr::copy(src, dst, len) }; - } -} - -impl fmt::Write for PlatformLogWriter<'_> { - fn write_str(&mut self, s: &str) -> fmt::Result { - let mut incoming_bytes = s.as_bytes(); - - while !incoming_bytes.is_empty() { - let len = self.len; - - // write everything possible to buffer and mark last \n - let new_len = len + incoming_bytes.len(); - let last_newline = self.buffer[len..LOGGING_MSG_MAX_LEN] - .iter_mut() - .zip(incoming_bytes) - .enumerate() - .fold(None, |acc, (i, (output, input))| { - output.write(*input); - if *input == b'\n' { - Some(i) - } else { - acc - } - }); - - // update last \n index - if let Some(newline) = last_newline { - self.last_newline_index = len + newline; - } - - // calculate how many bytes were written - let written_len = if new_len <= LOGGING_MSG_MAX_LEN { - // if the len was not exceeded - self.len = new_len; - new_len - len // written len - } else { - // if new length was exceeded - self.len = LOGGING_MSG_MAX_LEN; - self.temporal_flush(); - - LOGGING_MSG_MAX_LEN - len // written len - }; - - incoming_bytes = &incoming_bytes[written_len..]; - } - - Ok(()) - } -} - -#[cfg(test)] -pub mod tests { - use std::ffi::CStr; - use std::fmt::Write; - use log::Level; - use crate::arrays::slice_assume_init_ref; - use crate::platform_log_writer::PlatformLogWriter; - - #[test] - fn platform_log_writer_init_values() { - let tag = CStr::from_bytes_with_nul(b"tag\0").unwrap(); - - let writer = PlatformLogWriter::new(None, Level::Warn, tag); - - assert_eq!(writer.tag, tag); - // Android uses LogPriority instead, which doesn't implement equality checks - #[cfg(not(target_os = "android"))] - assert_eq!(writer.priority, Level::Warn); - } - - #[test] - fn temporal_flush() { - let mut writer = get_tag_writer(); - - writer - .write_str("12\n\n567\n90") - .expect("Unable to write to PlatformLogWriter"); - - assert_eq!(writer.len, 10); - writer.temporal_flush(); - // Should have flushed up until the last newline. - assert_eq!(writer.len, 3); - assert_eq!(writer.last_newline_index, 0); - assert_eq!( - unsafe { slice_assume_init_ref(&writer.buffer[..writer.len]) }, - "\n90".as_bytes() - ); - - writer.temporal_flush(); - // Should have flushed all remaining bytes. - assert_eq!(writer.len, 0); - assert_eq!(writer.last_newline_index, 0); - } - - #[test] - fn flush() { - let mut writer = get_tag_writer(); - writer - .write_str("abcdefghij\n\nklm\nnopqr\nstuvwxyz") - .expect("Unable to write to PlatformLogWriter"); - - writer.flush(); - - assert_eq!(writer.last_newline_index, 0); - assert_eq!(writer.len, 0); - } - - #[test] - fn last_newline_index() { - let mut writer = get_tag_writer(); - - writer - .write_str("12\n\n567\n90") - .expect("Unable to write to PlatformLogWriter"); - - assert_eq!(writer.last_newline_index, 7); - } - - #[test] - fn output_specified_len_leaves_buffer_unchanged() { - let mut writer = get_tag_writer(); - let log_string = "abcdefghij\n\nklm\nnopqr\nstuvwxyz"; - writer - .write_str(log_string) - .expect("Unable to write to PlatformLogWriter"); - - unsafe { writer.output_specified_len(5) }; - - assert_eq!( - unsafe { slice_assume_init_ref(&writer.buffer[..log_string.len()]) }, - log_string.as_bytes() - ); - } - - #[test] - fn copy_bytes_to_start() { - let mut writer = get_tag_writer(); - writer - .write_str("0123456789") - .expect("Unable to write to PlatformLogWriter"); - - writer.copy_bytes_to_start(3, 2); - - assert_eq!( - unsafe { slice_assume_init_ref(&writer.buffer[..10]) }, - "3423456789".as_bytes() - ); - } - - #[test] - fn copy_bytes_to_start_nop() { - let test_string = "Test_string_with\n\n\n\nnewlines\n"; - let mut writer = get_tag_writer(); - writer - .write_str(test_string) - .expect("Unable to write to PlatformLogWriter"); - - writer.copy_bytes_to_start(0, 20); - writer.copy_bytes_to_start(10, 0); - - assert_eq!( - unsafe { slice_assume_init_ref(&writer.buffer[..test_string.len()]) }, - test_string.as_bytes() - ); - } - - fn get_tag_writer() -> PlatformLogWriter<'static> { - PlatformLogWriter::new( - None, - Level::Warn, - CStr::from_bytes_with_nul(b"tag\0").unwrap(), - ) - } -} \ No newline at end of file +use crate::arrays::slice_assume_init_ref; +use crate::{LOGGING_MSG_MAX_LEN, LogId, android_log, uninit_array}; +use log::Level; +#[cfg(target_os = "android")] +use log_ffi::LogPriority; +use std::ffi::CStr; +use std::mem::MaybeUninit; +use std::{fmt, mem, ptr}; + +/// The purpose of this "writer" is to split logged messages on whitespace when the log message +/// length exceeds the maximum. Without allocations. +pub struct PlatformLogWriter<'a> { + #[cfg(target_os = "android")] + priority: LogPriority, + #[cfg(not(target_os = "android"))] + priority: Level, + #[cfg(target_os = "android")] + buf_id: Option, + #[cfg(not(target_os = "android"))] + buf_id: Option, + len: usize, + last_newline_index: usize, + tag: &'a CStr, + buffer: [MaybeUninit; LOGGING_MSG_MAX_LEN + 1], +} + +impl PlatformLogWriter<'_> { + #[cfg(target_os = "android")] + pub fn new_with_priority( + buf_id: Option, + priority: log_ffi::LogPriority, + tag: &CStr, + ) -> PlatformLogWriter<'_> { + #[allow(deprecated)] // created an issue #35 for this + PlatformLogWriter { + priority, + buf_id: LogId::to_native(buf_id), + len: 0, + last_newline_index: 0, + tag, + buffer: uninit_array(), + } + } + + #[cfg(target_os = "android")] + pub fn new(buf_id: Option, level: Level, tag: &CStr) -> PlatformLogWriter<'_> { + PlatformLogWriter::new_with_priority( + buf_id, + match level { + Level::Warn => LogPriority::WARN, + Level::Info => LogPriority::INFO, + Level::Debug => LogPriority::DEBUG, + Level::Error => LogPriority::ERROR, + Level::Trace => LogPriority::VERBOSE, + }, + tag, + ) + } + + #[cfg(not(target_os = "android"))] + pub fn new(buf_id: Option, level: Level, tag: &CStr) -> PlatformLogWriter<'_> { + #[allow(deprecated)] // created an issue #35 for this + PlatformLogWriter { + priority: level, + buf_id, + len: 0, + last_newline_index: 0, + tag, + buffer: uninit_array(), + } + } + + /// Flush some bytes to android logger. + /// + /// If there is a newline, flush up to it. + /// If there was no newline, flush all. + /// + /// Not guaranteed to flush everything. + fn temporal_flush(&mut self) { + let total_len = self.len; + + if total_len == 0 { + return; + } + + if self.last_newline_index > 0 { + let copy_from_index = self.last_newline_index; + let remaining_chunk_len = total_len - copy_from_index; + + unsafe { self.output_specified_len(copy_from_index) }; + self.copy_bytes_to_start(copy_from_index, remaining_chunk_len); + self.len = remaining_chunk_len; + } else { + unsafe { self.output_specified_len(total_len) }; + self.len = 0; + } + self.last_newline_index = 0; + } + + /// Flush everything remaining to android logger. + pub fn flush(&mut self) { + let total_len = self.len; + + if total_len == 0 { + return; + } + + unsafe { self.output_specified_len(total_len) }; + self.len = 0; + self.last_newline_index = 0; + } + + /// Output buffer up until the \0 which will be placed at `len` position. + /// + /// # Safety + /// The first `len` bytes of `self.buffer` must be initialized. + unsafe fn output_specified_len(&mut self, len: usize) { + let mut last_byte = MaybeUninit::new(b'\0'); + + mem::swap( + &mut last_byte, + self.buffer.get_mut(len).expect("`len` is out of bounds"), + ); + + let initialized = unsafe { slice_assume_init_ref(&self.buffer[..len + 1]) }; + let msg = CStr::from_bytes_with_nul(initialized) + .expect("Unreachable: nul terminator was placed at `len`"); + android_log(self.buf_id, self.priority, self.tag, msg); + + unsafe { *self.buffer.get_unchecked_mut(len) = last_byte }; + } + + /// Copy `len` bytes from `index` position to starting position. + fn copy_bytes_to_start(&mut self, index: usize, len: usize) { + let dst = self.buffer.as_mut_ptr(); + let src = unsafe { self.buffer.as_ptr().add(index) }; + unsafe { ptr::copy(src, dst, len) }; + } +} + +impl fmt::Write for PlatformLogWriter<'_> { + fn write_str(&mut self, s: &str) -> fmt::Result { + let mut incoming_bytes = s.as_bytes(); + + while !incoming_bytes.is_empty() { + let len = self.len; + + // write everything possible to buffer and mark last \n + let new_len = len + incoming_bytes.len(); + let last_newline = self.buffer[len..LOGGING_MSG_MAX_LEN] + .iter_mut() + .zip(incoming_bytes) + .enumerate() + .fold(None, |acc, (i, (output, input))| { + output.write(*input); + if *input == b'\n' { Some(i) } else { acc } + }); + + // update last \n index + if let Some(newline) = last_newline { + self.last_newline_index = len + newline; + } + + // calculate how many bytes were written + let written_len = if new_len <= LOGGING_MSG_MAX_LEN { + // if the len was not exceeded + self.len = new_len; + new_len - len // written len + } else { + // if new length was exceeded + self.len = LOGGING_MSG_MAX_LEN; + self.temporal_flush(); + + LOGGING_MSG_MAX_LEN - len // written len + }; + + incoming_bytes = &incoming_bytes[written_len..]; + } + + Ok(()) + } +} + +#[cfg(test)] +pub mod tests { + use crate::arrays::slice_assume_init_ref; + use crate::platform_log_writer::PlatformLogWriter; + use log::Level; + use std::ffi::CStr; + use std::fmt::Write; + + #[test] + fn platform_log_writer_init_values() { + let tag = CStr::from_bytes_with_nul(b"tag\0").unwrap(); + + let writer = PlatformLogWriter::new(None, Level::Warn, tag); + + assert_eq!(writer.tag, tag); + // Android uses LogPriority instead, which doesn't implement equality checks + #[cfg(not(target_os = "android"))] + assert_eq!(writer.priority, Level::Warn); + } + + #[test] + fn temporal_flush() { + let mut writer = get_tag_writer(); + + writer + .write_str("12\n\n567\n90") + .expect("Unable to write to PlatformLogWriter"); + + assert_eq!(writer.len, 10); + writer.temporal_flush(); + // Should have flushed up until the last newline. + assert_eq!(writer.len, 3); + assert_eq!(writer.last_newline_index, 0); + assert_eq!( + unsafe { slice_assume_init_ref(&writer.buffer[..writer.len]) }, + "\n90".as_bytes() + ); + + writer.temporal_flush(); + // Should have flushed all remaining bytes. + assert_eq!(writer.len, 0); + assert_eq!(writer.last_newline_index, 0); + } + + #[test] + fn flush() { + let mut writer = get_tag_writer(); + writer + .write_str("abcdefghij\n\nklm\nnopqr\nstuvwxyz") + .expect("Unable to write to PlatformLogWriter"); + + writer.flush(); + + assert_eq!(writer.last_newline_index, 0); + assert_eq!(writer.len, 0); + } + + #[test] + fn last_newline_index() { + let mut writer = get_tag_writer(); + + writer + .write_str("12\n\n567\n90") + .expect("Unable to write to PlatformLogWriter"); + + assert_eq!(writer.last_newline_index, 7); + } + + #[test] + fn output_specified_len_leaves_buffer_unchanged() { + let mut writer = get_tag_writer(); + let log_string = "abcdefghij\n\nklm\nnopqr\nstuvwxyz"; + writer + .write_str(log_string) + .expect("Unable to write to PlatformLogWriter"); + + unsafe { writer.output_specified_len(5) }; + + assert_eq!( + unsafe { slice_assume_init_ref(&writer.buffer[..log_string.len()]) }, + log_string.as_bytes() + ); + } + + #[test] + fn copy_bytes_to_start() { + let mut writer = get_tag_writer(); + writer + .write_str("0123456789") + .expect("Unable to write to PlatformLogWriter"); + + writer.copy_bytes_to_start(3, 2); + + assert_eq!( + unsafe { slice_assume_init_ref(&writer.buffer[..10]) }, + "3423456789".as_bytes() + ); + } + + #[test] + fn copy_bytes_to_start_nop() { + let test_string = "Test_string_with\n\n\n\nnewlines\n"; + let mut writer = get_tag_writer(); + writer + .write_str(test_string) + .expect("Unable to write to PlatformLogWriter"); + + writer.copy_bytes_to_start(0, 20); + writer.copy_bytes_to_start(10, 0); + + assert_eq!( + unsafe { slice_assume_init_ref(&writer.buffer[..test_string.len()]) }, + test_string.as_bytes() + ); + } + + fn get_tag_writer() -> PlatformLogWriter<'static> { + PlatformLogWriter::new( + None, + Level::Warn, + CStr::from_bytes_with_nul(b"tag\0").unwrap(), + ) + } +} diff --git a/src/tests.rs b/src/tests.rs index c28e8fb..562a12f 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -1,79 +1,79 @@ -use super::*; -use std::sync::atomic::{AtomicBool, Ordering}; -use log::LevelFilter; - -#[test] -fn check_config_values() { - // Filter is checked in config_filter_match below. - let config = Config::default() - .with_max_level(LevelFilter::Trace) - .with_log_buffer(LogId::System) - .with_tag("my_app"); - - assert_eq!(config.log_level, Some(LevelFilter::Trace)); - assert_eq!(config.buf_id, Some(LogId::System)); - assert_eq!(config.tag, Some(CString::new("my_app").unwrap())); -} - -#[test] -fn log_calls_formatter() { - static FORMAT_FN_WAS_CALLED: AtomicBool = AtomicBool::new(false); - let config = Config::default() - .with_max_level(LevelFilter::Info) - .format(|_, _| { - FORMAT_FN_WAS_CALLED.store(true, Ordering::SeqCst); - Ok(()) - }); - let logger = AndroidLogger::new(config); - - logger.log(&Record::builder().level(log::Level::Info).build()); - - assert!(FORMAT_FN_WAS_CALLED.load(Ordering::SeqCst)); -} - -#[test] -fn logger_enabled_threshold() { - let logger = AndroidLogger::new(Config::default().with_max_level(LevelFilter::Info)); - - assert!(logger.enabled(&log::MetadataBuilder::new().level(log::Level::Warn).build())); - assert!(logger.enabled(&log::MetadataBuilder::new().level(log::Level::Info).build())); - assert!(!logger.enabled(&log::MetadataBuilder::new().level(log::Level::Debug).build())); -} - -// Test whether the filter gets called correctly. Not meant to be exhaustive for all filter -// options, as these are handled directly by the filter itself. -#[test] -fn config_filter_match() { - let info_record = Record::builder().level(log::Level::Info).build(); - let debug_record = Record::builder().level(log::Level::Debug).build(); - - let info_all_filter = env_filter::Builder::new().parse("info").build(); - let info_all_config = Config::default().with_filter(info_all_filter); - - assert!(info_all_config.filter_matches(&info_record)); - assert!(!info_all_config.filter_matches(&debug_record)); -} - -#[test] -fn fill_tag_bytes_truncates_long_tag() { - let too_long_tag = [b'a'; LOGGING_TAG_MAX_LEN + 20]; - - let mut result = uninit_array(); - let tag = fill_tag_bytes(&mut result, &too_long_tag); - - let mut expected_result = vec![b'a'; LOGGING_TAG_MAX_LEN - 2]; - expected_result.extend("..\0".as_bytes()); - assert_eq!(tag.to_bytes_with_nul(), expected_result); -} - -#[test] -fn fill_tag_bytes_keeps_short_tag() { - let short_tag = [b'a'; 3]; - - let mut result = uninit_array(); - let tag = fill_tag_bytes(&mut result, &short_tag); - - let mut expected_result = short_tag.to_vec(); - expected_result.push(0); - assert_eq!(tag.to_bytes_with_nul(), expected_result); -} \ No newline at end of file +use super::*; +use log::LevelFilter; +use std::sync::atomic::{AtomicBool, Ordering}; + +#[test] +fn check_config_values() { + // Filter is checked in config_filter_match below. + let config = Config::default() + .with_max_level(LevelFilter::Trace) + .with_log_buffer(LogId::System) + .with_tag("my_app"); + + assert_eq!(config.log_level, Some(LevelFilter::Trace)); + assert_eq!(config.buf_id, Some(LogId::System)); + assert_eq!(config.tag, Some(CString::new("my_app").unwrap())); +} + +#[test] +fn log_calls_formatter() { + static FORMAT_FN_WAS_CALLED: AtomicBool = AtomicBool::new(false); + let config = Config::default() + .with_max_level(LevelFilter::Info) + .format(|_, _| { + FORMAT_FN_WAS_CALLED.store(true, Ordering::SeqCst); + Ok(()) + }); + let logger = AndroidLogger::new(config); + + logger.log(&Record::builder().level(log::Level::Info).build()); + + assert!(FORMAT_FN_WAS_CALLED.load(Ordering::SeqCst)); +} + +#[test] +fn logger_enabled_threshold() { + let logger = AndroidLogger::new(Config::default().with_max_level(LevelFilter::Info)); + + assert!(logger.enabled(&log::MetadataBuilder::new().level(log::Level::Warn).build())); + assert!(logger.enabled(&log::MetadataBuilder::new().level(log::Level::Info).build())); + assert!(!logger.enabled(&log::MetadataBuilder::new().level(log::Level::Debug).build())); +} + +// Test whether the filter gets called correctly. Not meant to be exhaustive for all filter +// options, as these are handled directly by the filter itself. +#[test] +fn config_filter_match() { + let info_record = Record::builder().level(log::Level::Info).build(); + let debug_record = Record::builder().level(log::Level::Debug).build(); + + let info_all_filter = env_filter::Builder::new().parse("info").build(); + let info_all_config = Config::default().with_filter(info_all_filter); + + assert!(info_all_config.filter_matches(&info_record)); + assert!(!info_all_config.filter_matches(&debug_record)); +} + +#[test] +fn fill_tag_bytes_truncates_long_tag() { + let too_long_tag = [b'a'; LOGGING_TAG_MAX_LEN + 20]; + + let mut result = uninit_array(); + let tag = fill_tag_bytes(&mut result, &too_long_tag); + + let mut expected_result = vec![b'a'; LOGGING_TAG_MAX_LEN - 2]; + expected_result.extend("..\0".as_bytes()); + assert_eq!(tag.to_bytes_with_nul(), expected_result); +} + +#[test] +fn fill_tag_bytes_keeps_short_tag() { + let short_tag = [b'a'; 3]; + + let mut result = uninit_array(); + let tag = fill_tag_bytes(&mut result, &short_tag); + + let mut expected_result = short_tag.to_vec(); + expected_result.push(0); + assert_eq!(tag.to_bytes_with_nul(), expected_result); +}