#![allow(dead_code)]
use core::fmt;
use core::mem;
use core::ptr;
use super::encode::{Decoder, TypeTag};
use super::metadata::LogMetadata;
use super::record::RecordHeader;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DecodeError {
BufferTooShort,
UnknownTag(u8),
InvalidUtf8,
DecoderFailed,
}
#[derive(Debug, Clone, PartialEq)]
pub enum DecodedArg {
I8(i8),
I16(i16),
I32(i32),
I64(i64),
I128(i128),
U8(u8),
U16(u16),
U32(u32),
U64(u64),
U128(u128),
F32(f32),
F64(f64),
Bool(bool),
Str(String),
Usize(u64),
Isize(i64),
Custom(String),
}
impl fmt::Display for DecodedArg {
#[allow(clippy::match_same_arms)]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::I8(v) => write!(f, "{v}"),
Self::I16(v) => write!(f, "{v}"),
Self::I32(v) => write!(f, "{v}"),
Self::I64(v) => write!(f, "{v}"),
Self::I128(v) => write!(f, "{v}"),
Self::U8(v) => write!(f, "{v}"),
Self::U16(v) => write!(f, "{v}"),
Self::U32(v) => write!(f, "{v}"),
Self::U64(v) => write!(f, "{v}"),
Self::U128(v) => write!(f, "{v}"),
Self::F32(v) => write!(f, "{v}"),
Self::F64(v) => write!(f, "{v}"),
Self::Bool(v) => write!(f, "{v}"),
Self::Str(v) => write!(f, "{v}"),
Self::Usize(v) => write!(f, "{v}"),
Self::Isize(v) => write!(f, "{v}"),
Self::Custom(v) => write!(f, "{v}"),
}
}
}
fn decode_one(data: &[u8]) -> Result<(DecodedArg, usize), DecodeError> {
let (&tag_byte, rest) = data.split_first().ok_or(DecodeError::BufferTooShort)?;
match TypeTag::try_from(tag_byte).map_err(|()| DecodeError::UnknownTag(tag_byte))? {
TypeTag::I8 => decode_fixed::<1>(rest, |b| DecodedArg::I8(i8::from_ne_bytes(b))),
TypeTag::I16 => decode_fixed::<2>(rest, |b| DecodedArg::I16(i16::from_ne_bytes(b))),
TypeTag::I32 => decode_fixed::<4>(rest, |b| DecodedArg::I32(i32::from_ne_bytes(b))),
TypeTag::I64 => decode_fixed::<8>(rest, |b| DecodedArg::I64(i64::from_ne_bytes(b))),
TypeTag::I128 => decode_fixed::<16>(rest, |b| DecodedArg::I128(i128::from_ne_bytes(b))),
TypeTag::U8 => decode_fixed::<1>(rest, |b| DecodedArg::U8(u8::from_ne_bytes(b))),
TypeTag::U16 => decode_fixed::<2>(rest, |b| DecodedArg::U16(u16::from_ne_bytes(b))),
TypeTag::U32 => decode_fixed::<4>(rest, |b| DecodedArg::U32(u32::from_ne_bytes(b))),
TypeTag::U64 => decode_fixed::<8>(rest, |b| DecodedArg::U64(u64::from_ne_bytes(b))),
TypeTag::U128 => decode_fixed::<16>(rest, |b| DecodedArg::U128(u128::from_ne_bytes(b))),
TypeTag::F32 => decode_fixed::<4>(rest, |b| DecodedArg::F32(f32::from_ne_bytes(b))),
TypeTag::F64 => decode_fixed::<8>(rest, |b| DecodedArg::F64(f64::from_ne_bytes(b))),
TypeTag::Bool => decode_fixed::<1>(rest, |b| DecodedArg::Bool(b[0] != 0)),
TypeTag::Str => decode_str(rest),
TypeTag::Usize => decode_fixed::<8>(rest, |b| DecodedArg::Usize(u64::from_ne_bytes(b))),
TypeTag::Isize => decode_fixed::<8>(rest, |b| DecodedArg::Isize(i64::from_ne_bytes(b))),
TypeTag::Custom => decode_custom(rest),
}
}
fn decode_fixed<const N: usize>(
data: &[u8],
make: impl FnOnce([u8; N]) -> DecodedArg,
) -> Result<(DecodedArg, usize), DecodeError> {
let buf = data
.get(..N)
.ok_or(DecodeError::BufferTooShort)?
.try_into()
.map_err(|_| DecodeError::BufferTooShort)?;
Ok((make(buf), 1 + N))
}
fn decode_str(data: &[u8]) -> Result<(DecodedArg, usize), DecodeError> {
const LEN_SIZE: usize = size_of::<u32>();
if data.len() < LEN_SIZE {
return Err(DecodeError::BufferTooShort);
}
let len = u32::from_ne_bytes(data[..LEN_SIZE].try_into().unwrap()) as usize;
if data.len() < LEN_SIZE + len {
return Err(DecodeError::BufferTooShort);
}
let s = core::str::from_utf8(&data[LEN_SIZE..LEN_SIZE + len])
.map_err(|_| DecodeError::InvalidUtf8)?;
Ok((DecodedArg::Str(s.to_owned()), 1 + LEN_SIZE + len))
}
fn decode_custom(data: &[u8]) -> Result<(DecodedArg, usize), DecodeError> {
let ptr_size = size_of::<Decoder>();
if data.len() < ptr_size + 4 {
return Err(DecodeError::BufferTooShort);
}
let mut len_buf = [0u8; 4];
len_buf.copy_from_slice(&data[ptr_size..ptr_size + 4]);
let payload_len = u32::from_ne_bytes(len_buf) as usize;
let payload_start = ptr_size + 4;
if data.len() < payload_start + payload_len {
return Err(DecodeError::BufferTooShort);
}
let decoder: Decoder = unsafe {
let mut slot = mem::MaybeUninit::<Decoder>::uninit();
ptr::copy_nonoverlapping(data.as_ptr(), slot.as_mut_ptr().cast::<u8>(), ptr_size);
slot.assume_init()
};
let payload = &data[payload_start..payload_start + payload_len];
let mut out = String::new();
decoder(payload, &mut out).map_err(|_| DecodeError::DecoderFailed)?;
Ok((DecodedArg::Custom(out), 1 + ptr_size + 4 + payload_len))
}
pub struct RawDecodedRecord {
pub timestamp_ns: u64,
pub logger_ptr: usize,
pub metadata: &'static LogMetadata,
pub args: Vec<DecodedArg>,
}
pub struct LogRecord {
pub timestamp_ns: u64,
pub logger_name: String,
pub metadata: &'static LogMetadata,
pub args: Vec<DecodedArg>,
}
#[cfg_attr(feature = "rtsan", rtsan_standalone::blocking)]
pub unsafe fn decode_record(data: &[u8]) -> Result<RawDecodedRecord, DecodeError> {
if data.len() < RecordHeader::SIZE {
return Err(DecodeError::BufferTooShort);
}
let header = unsafe { ptr::read_unaligned(data.as_ptr().cast::<RecordHeader>()) };
let args_data = &data[RecordHeader::SIZE..];
if args_data.len() < header.encoded_args_size as usize {
return Err(DecodeError::BufferTooShort);
}
let metadata: &'static LogMetadata = unsafe { &*(header.metadata_ptr as *const LogMetadata) };
let mut args = Vec::with_capacity(metadata.arg_count as usize);
let mut offset = 0;
let args_end = header.encoded_args_size as usize;
while offset < args_end {
let (arg, consumed) = decode_one(&args_data[offset..args_end])?;
args.push(arg);
offset += consumed;
}
Ok(RawDecodedRecord {
timestamp_ns: header.timestamp_ns,
logger_ptr: header.logger_ptr,
metadata,
args,
})
}
#[cfg(test)]
mod tests {
use core::ptr;
use super::super::encode::{CustomEncode, Decoder, Encode, TypeTag};
use super::*;
macro_rules! test_decode_roundtrip {
($name:ident, bool, $tag:ident, Bool, $val:expr) => {
#[test]
fn $name() {
let val: bool = $val;
let mut buf = vec![0u8; val.encoded_size()];
unsafe { val.encode_to(buf.as_mut_ptr()) };
let (arg, consumed) = decode_one(&buf).unwrap();
assert_eq!(consumed, 1 + size_of::<bool>());
assert_eq!(arg, DecodedArg::Bool(val));
}
};
($name:ident, $ty:ty, $tag:ident, $variant:ident, $val:expr, cast $wire:ty) => {
test_decode_roundtrip!($name, $ty, $wire, $tag, $variant, $val);
};
($name:ident, $ty:ty, $tag:ident, $variant:ident, $val:expr) => {
test_decode_roundtrip!($name, $ty, $ty, $tag, $variant, $val);
};
($name:ident, $ty:ty, $wire:ty, $tag:ident, $variant:ident, $val:expr) => {
#[test]
fn $name() {
let val: $ty = $val;
let mut buf = vec![0u8; val.encoded_size()];
unsafe { val.encode_to(buf.as_mut_ptr()) };
let (arg, consumed) = decode_one(&buf).unwrap();
assert_eq!(consumed, 1 + size_of::<$wire>());
let DecodedArg::$variant(got) = arg else {
panic!("expected {}", stringify!($variant))
};
assert_eq!(got.to_ne_bytes(), (val as $wire).to_ne_bytes());
}
};
}
test_decode_roundtrip!(decode_i8_roundtrip, i8, I8, I8, i8::MIN);
test_decode_roundtrip!(decode_i16_roundtrip, i16, I16, I16, -1_000_i16);
test_decode_roundtrip!(decode_i32_roundtrip, i32, I32, I32, -999_i32);
test_decode_roundtrip!(decode_i64_roundtrip, i64, I64, I64, i64::MIN);
test_decode_roundtrip!(decode_i128_roundtrip, i128, I128, I128, i128::MIN);
test_decode_roundtrip!(decode_u8_roundtrip, u8, U8, U8, u8::MAX);
test_decode_roundtrip!(decode_u16_roundtrip, u16, U16, U16, u16::MAX);
test_decode_roundtrip!(decode_u32_roundtrip, u32, U32, U32, u32::MAX);
test_decode_roundtrip!(decode_u64_roundtrip, u64, U64, U64, u64::MAX);
test_decode_roundtrip!(decode_u128_roundtrip, u128, U128, U128, u128::MAX);
test_decode_roundtrip!(decode_bool_true, bool, Bool, Bool, true);
test_decode_roundtrip!(decode_bool_false, bool, Bool, Bool, false);
test_decode_roundtrip!(decode_usize_roundtrip, usize, Usize, Usize, usize::MAX, cast u64);
test_decode_roundtrip!(decode_isize_roundtrip, isize, Isize, Isize, isize::MIN, cast i64);
test_decode_roundtrip!(decode_f32_roundtrip, f32, F32, F32, core::f32::consts::PI);
test_decode_roundtrip!(decode_f64_roundtrip, f64, F64, F64, core::f64::consts::E);
#[test]
fn decode_str_roundtrip() {
let val: &str = "world";
let mut buf = vec![0u8; val.encoded_size()];
unsafe { val.encode_to(buf.as_mut_ptr()) };
let (arg, consumed) = decode_one(&buf).unwrap();
assert_eq!(consumed, val.encoded_size());
assert_eq!(arg, DecodedArg::Str("world".to_owned()));
}
#[test]
fn decode_one_empty_slice_returns_err() {
assert!(decode_one(&[]).is_err());
}
#[test]
fn decode_one_unknown_tag_returns_err() {
assert!(decode_one(&[17]).is_err());
assert!(decode_one(&[u8::MAX]).is_err());
}
#[test]
fn decode_one_truncated_fixed_returns_err() {
assert!(decode_one(&[TypeTag::I32 as u8]).is_err());
assert!(decode_one(&[TypeTag::I32 as u8, 0, 0, 0]).is_err());
}
#[test]
fn decode_one_truncated_str_returns_err() {
let mut buf = [0u8; 5];
buf[0] = TypeTag::Str as u8;
let len: u32 = 10;
buf[1..5].copy_from_slice(&len.to_ne_bytes());
assert!(decode_one(&buf).is_err());
}
#[test]
fn decode_one_truncated_custom_returns_err() {
assert!(decode_one(&[TypeTag::Custom as u8]).is_err());
let partial = vec![TypeTag::Custom as u8; 1 + size_of::<usize>()];
assert!(decode_one(&partial).is_err());
let mut buf = vec![0u8; 1 + size_of::<usize>() + 4];
buf[0] = TypeTag::Custom as u8;
let len: u32 = 4;
buf[1 + size_of::<usize>()..].copy_from_slice(&len.to_ne_bytes());
assert!(decode_one(&buf).is_err());
}
use crate::level::LogLevel;
use crate::metadata::LogMetadata;
use crate::record::RecordHeader;
use crate::testutil::{Color, Marker, Point2D};
static TEST_META: LogMetadata = LogMetadata {
level: LogLevel::Info,
fmt_str: "",
file: "f.rs",
line: 1,
module_path: "m",
arg_count: 0,
};
fn make_header_buf(logger_ptr: usize) -> Vec<u8> {
let header = RecordHeader::new(
42_000_000_000,
(&raw const TEST_META) as usize,
logger_ptr,
0,
);
let mut buf = vec![0u8; RecordHeader::SIZE];
unsafe { header.write_to(buf.as_mut_ptr()) };
buf
}
#[test]
fn decode_record_preserves_logger_ptr() {
let buf = make_header_buf(0xDEAD_BEEF);
let raw = unsafe { decode_record(&buf) }.unwrap();
assert_eq!(raw.logger_ptr, 0xDEAD_BEEF);
}
#[test]
fn decode_record_preserves_timestamp_ns() {
let buf = make_header_buf(0);
let raw = unsafe { decode_record(&buf) }.unwrap();
assert_eq!(raw.timestamp_ns, 42_000_000_000);
}
#[test]
fn decode_record_empty_args() {
let buf = make_header_buf(0);
let raw = unsafe { decode_record(&buf) }.unwrap();
assert!(raw.args.is_empty());
}
#[test]
fn decode_record_too_short_returns_err() {
assert!(unsafe { decode_record(&[]) }.is_err());
assert!(unsafe { decode_record(&[0u8; RecordHeader::SIZE - 1]) }.is_err());
}
#[test]
fn log_record_logger_name_field() {
let rec = LogRecord {
timestamp_ns: 0,
logger_name: "payments".to_owned(),
metadata: &TEST_META,
args: vec![],
};
assert_eq!(rec.logger_name, "payments");
}
#[test]
fn log_record_fields_match_construction() {
let rec = LogRecord {
timestamp_ns: 99,
logger_name: "app".to_owned(),
metadata: &TEST_META,
args: vec![DecodedArg::U32(7)],
};
assert_eq!(rec.timestamp_ns, 99);
assert_eq!(rec.metadata.file, "f.rs");
assert_eq!(rec.args.len(), 1);
}
fn pack_custom(decoder: Decoder, payload: &[u8]) -> Vec<u8> {
let ptr_size = size_of::<Decoder>();
let mut buf = vec![0u8; 1 + ptr_size + 4 + payload.len()];
buf[0] = TypeTag::Custom as u8;
unsafe {
ptr::copy_nonoverlapping(
ptr::addr_of!(decoder).cast::<u8>(),
buf[1..].as_mut_ptr(),
ptr_size,
);
}
let len_bytes = u32::try_from(payload.len()).unwrap().to_ne_bytes();
buf[1 + ptr_size..1 + ptr_size + 4].copy_from_slice(&len_bytes);
buf[1 + ptr_size + 4..].copy_from_slice(payload);
buf
}
fn encode_tagged<T: Encode>(val: &T) -> Vec<u8> {
let mut buf = vec![0u8; val.encoded_size()];
unsafe { val.encode_to(buf.as_mut_ptr()) };
buf
}
#[test]
fn decode_custom_manual_color() {
let decoder: Decoder =
|bytes, out| write!(out, "rgb({}, {}, {})", bytes[0], bytes[1], bytes[2]);
let buf = pack_custom(decoder, &[255, 128, 0]);
let (arg, consumed) = decode_one(&buf).unwrap();
assert_eq!(consumed, buf.len());
assert_eq!(arg, DecodedArg::Custom("rgb(255, 128, 0)".to_owned()));
}
#[test]
fn decode_custom_manual_zero_payload() {
let decoder: Decoder = |_bytes, out| write!(out, "empty");
let buf = pack_custom(decoder, &[]);
let (arg, consumed) = decode_one(&buf).unwrap();
assert_eq!(consumed, buf.len());
assert_eq!(arg, DecodedArg::Custom("empty".to_owned()));
}
#[test]
fn decode_custom_color_roundtrip() {
let val = Color {
r: 255,
g: 128,
b: 0,
};
let buf = encode_tagged(&val);
let (arg, consumed) = decode_one(&buf).unwrap();
assert_eq!(consumed, buf.len());
assert_eq!(arg, DecodedArg::Custom("rgb(255, 128, 0)".to_owned()));
}
#[test]
fn decode_custom_point2d_roundtrip() {
let val = Point2D { x: 1.5, y: -3.25 };
let buf = encode_tagged(&val);
let (arg, consumed) = decode_one(&buf).unwrap();
assert_eq!(consumed, buf.len());
let DecodedArg::Custom(ref s) = arg else {
panic!("expected Custom")
};
assert!(s.contains("1.5"), "expected x=1.5 in output, got: {s:?}");
assert!(
s.contains("-3.25"),
"expected y=-3.25 in output, got: {s:?}"
);
}
#[test]
fn decode_custom_marker_roundtrip() {
let val = Marker;
let buf = encode_tagged(&val);
let (arg, consumed) = decode_one(&buf).unwrap();
assert_eq!(consumed, buf.len());
assert_eq!(arg, DecodedArg::Custom("marker".to_owned()));
}
#[test]
fn decode_custom_consumed_bytes() {
let val = Color { r: 0, g: 0, b: 0 };
let buf = encode_tagged(&val);
let (_, consumed) = decode_one(&buf).unwrap();
assert_eq!(consumed, 1 + size_of::<usize>() + 4 + val.payload_size());
}
#[test]
fn decoded_arg_custom_display() {
let arg = DecodedArg::Custom("rgb(1, 2, 3)".to_owned());
assert_eq!(format!("{arg}"), "rgb(1, 2, 3)");
}
}