use crate::level::Level;
use core::mem::size_of;
pub(crate) const VERSION: u8 = 0x01;
pub(crate) const LOG_RECORD: u8 = 1;
pub(crate) const END_OF_BUFFER: u8 = 2;
#[allow(unused)]
pub(crate) const HEADER_SIZE: usize = size_of::<u8>() + size_of::<u8>() + size_of::<u16>() + size_of::<u8>() + size_of::<u16>() + size_of::<u8>() + size_of::<u64>(); #[allow(unused)]
pub(crate) const FORMAT_SECTION_SIZE: usize = size_of::<u64>() + size_of::<u16>(); #[allow(unused)]
pub(crate) const SOURCE_SECTION_SIZE: usize = size_of::<u64>() + size_of::<u16>() + size_of::<u32>(); #[allow(unused)]
pub(crate) const COUNT_SIZE: usize = size_of::<u8>();
pub(crate) const BASE_RECORD_SIZE: usize =
HEADER_SIZE + FORMAT_SECTION_SIZE + SOURCE_SECTION_SIZE + COUNT_SIZE;
pub(crate) const FLAG_FORMAT: u16 = 0x01;
pub(crate) const FLAG_SOURCE: u16 = 0x02;
pub(crate) const FLAG_THREAD: u16 = 0x04;
pub(crate) const FLAG_PROCESS: u16 = 0x08;
pub(crate) const FLAG_COMPLEX: u16 = 0x10;
pub(crate) const MAX_RECORD_SIZE: usize = u16::MAX as usize;
#[allow(clippy::too_many_arguments)]
#[inline]
pub(crate) fn assemble(
scratch: &mut Vec<u8>,
level: Level,
timestamp: u64,
fmt: &'static str,
file: &'static str,
line: u32,
n_args: u8,
total_size: usize,
write_args: impl FnOnce(&mut [u8]),
) {
let flags = FLAG_FORMAT | FLAG_SOURCE;
let total = total_size as u16;
let level_u8 = level.to_u8();
debug_assert!(total_size <= MAX_RECORD_SIZE);
debug_assert!(fmt.len() <= u16::MAX as usize);
debug_assert!(file.len() <= u16::MAX as usize);
scratch.clear();
scratch.reserve(total_size);
unsafe {
let base = scratch.as_mut_ptr().add(scratch.len());
let buf = std::slice::from_raw_parts_mut(base, total_size);
let mut pos = 0usize;
macro_rules! put {
($bytes:expr) => {{
let b = $bytes;
buf[pos..pos + b.len()].copy_from_slice(&b);
pos += b.len();
}};
}
put!([VERSION]);
put!([LOG_RECORD]);
put!(total.to_le_bytes());
put!([level_u8]);
put!(flags.to_le_bytes());
put!([0u8]); put!(timestamp.to_le_bytes());
put!((fmt.as_ptr() as u64).to_le_bytes());
put!((fmt.len() as u16).to_le_bytes());
put!((file.as_ptr() as u64).to_le_bytes());
put!((file.len() as u16).to_le_bytes());
put!(line.to_le_bytes());
put!([n_args]);
write_args(&mut buf[pos..]);
scratch.set_len(total_size);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::encode::Loggable;
fn check_assemble(
scratch: &mut Vec<u8>,
level: Level,
timestamp: u64,
fmt: &'static str,
file: &'static str,
line: u32,
args: &[&dyn Loggable],
) -> bool {
if args.len() > u8::MAX as usize {
return false;
}
if fmt.len() > u16::MAX as usize || file.len() > u16::MAX as usize {
return false;
}
let mut args_bytes = 0usize;
for arg in args {
args_bytes += arg.encoded_size();
}
let total_size =
HEADER_SIZE + FORMAT_SECTION_SIZE + SOURCE_SECTION_SIZE + 1 + args.len() + args_bytes;
if total_size > MAX_RECORD_SIZE {
return false;
}
let n_args = args.len() as u8;
assemble(
scratch,
level,
timestamp,
fmt,
file,
line,
n_args,
total_size,
|buf| {
let mut pos = 0usize;
for arg in args {
buf[pos] = arg.type_tag();
pos += 1;
}
for arg in args {
let s = arg.encoded_size();
arg.encode(&mut buf[pos..pos + s]);
pos += s;
}
},
);
true
}
fn read_u16(bytes: &[u8], offset: usize) -> u16 {
u16::from_le_bytes(bytes[offset..offset + 2].try_into().unwrap())
}
fn read_u32(bytes: &[u8], offset: usize) -> u32 {
u32::from_le_bytes(bytes[offset..offset + 4].try_into().unwrap())
}
fn read_u64(bytes: &[u8], offset: usize) -> u64 {
u64::from_le_bytes(bytes[offset..offset + 8].try_into().unwrap())
}
#[test]
fn header_fields_are_written() {
let mut buf = Vec::new();
let ok = check_assemble(&mut buf, Level::Warn, 0xABCD, "hi", "f.rs", 7, &[]);
assert!(ok);
assert_eq!(buf[0], VERSION);
assert_eq!(buf[1], LOG_RECORD);
assert_eq!(read_u16(&buf, 2) as usize, buf.len());
assert_eq!(buf[4], Level::Warn.to_u8());
assert_eq!(read_u16(&buf, 5), FLAG_FORMAT | FLAG_SOURCE);
assert_eq!(buf[7], 0);
assert_eq!(read_u64(&buf, 8), 0xABCD);
}
#[test]
fn format_and_source_sections_reference_the_static_strs() {
let fmt = "value {}";
let file = "src/x.rs";
let mut buf = Vec::new();
check_assemble(&mut buf, Level::Info, 0, fmt, file, 42, &[&1u64]);
let fmt_ptr = read_u64(&buf, HEADER_SIZE);
let fmt_len = read_u16(&buf, HEADER_SIZE + 8);
assert_eq!(fmt_ptr, fmt.as_ptr() as u64);
assert_eq!(fmt_len as usize, fmt.len());
let src = HEADER_SIZE + FORMAT_SECTION_SIZE;
assert_eq!(read_u64(&buf, src), file.as_ptr() as u64);
assert_eq!(read_u16(&buf, src + 8) as usize, file.len());
assert_eq!(read_u32(&buf, src + 10), 42);
}
#[test]
fn arguments_are_tag_grouped_then_payload_grouped() {
let mut buf = Vec::new();
check_assemble(
&mut buf,
Level::Info,
0,
"{} {}",
"f",
1,
&[&0x1234u16, &true],
);
let args_at = HEADER_SIZE + FORMAT_SECTION_SIZE + SOURCE_SECTION_SIZE;
assert_eq!(buf[args_at], 2); assert_eq!(buf[args_at + 1], 0x06); assert_eq!(buf[args_at + 2], 0x0A); assert_eq!(read_u16(&buf, args_at + 3), 0x1234);
assert_eq!(buf[args_at + 5], 1); }
#[test]
fn total_size_matches_buffer_length() {
let mut buf = Vec::new();
check_assemble(&mut buf, Level::Error, 0, "{}", "f", 1, &[&"hello"]);
assert_eq!(read_u16(&buf, 2) as usize, buf.len());
}
#[test]
fn rejects_more_than_255_arguments() {
let args: Vec<&dyn Loggable> = (0..256).map(|_| &1u8 as &dyn Loggable).collect();
let mut buf = Vec::new();
assert!(!check_assemble(
&mut buf,
Level::Info,
0,
"x",
"f",
1,
&args
));
}
#[test]
fn rejects_record_larger_than_u16_total_size() {
let big = "x".repeat(u16::MAX as usize);
let mut buf = Vec::new();
assert!(!check_assemble(
&mut buf,
Level::Info,
0,
"{}",
"f",
1,
&[&big.as_str()]
));
}
#[test]
fn zero_args_writes_count_zero() {
let mut buf = Vec::new();
check_assemble(&mut buf, Level::Info, 0, "static", "f", 1, &[]);
let args_at = HEADER_SIZE + FORMAT_SECTION_SIZE + SOURCE_SECTION_SIZE;
assert_eq!(buf[args_at], 0);
assert_eq!(buf.len(), args_at + 1);
}
}