blob: fbd2d13ce10bbba1e4783ea0ba89054a24a898f9 [file] [log] [blame]
// Copyright 2020 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be found in the LICENSE file.
use super::buffer::Accounted;
use super::error::StreamError;
use byteorder::{ByteOrder, LittleEndian};
use fidl_fuchsia_logger::{LogLevelFilter, LogMessage};
use fuchsia_zircon as zx;
use libc::{c_char, c_int};
use std::{convert::TryFrom, mem, str};
pub const METADATA_SIZE: usize = mem::size_of::<fx_log_metadata_t>();
pub const MIN_PACKET_SIZE: usize = METADATA_SIZE + 1;
pub const MAX_DATAGRAM_LEN: usize = 2032;
pub const MAX_TAGS: usize = 5;
pub const MAX_TAG_LEN: usize = 64;
/// A type-safe(r) [`LogMessage`].
///
/// [`LogMessage`]: https://fuchsia.dev/reference/fidl/fuchsia.logger#LogMessage
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Message {
/// Size this message took up on the wire.
pub size: usize,
/// Message severity reported by the writer.
pub severity: Severity,
/// Timestamp reported by the writer.
pub time: zx::Time,
/// Process koid as reported by the writer.
pub pid: zx::sys::zx_koid_t,
/// Thread koid as reported by the writer.
pub tid: zx::sys::zx_koid_t,
/// Number of logs the writer had to drop before writing this message.
pub dropped_logs: usize,
/// Tags annotating the context or semantics of this message.
pub tags: Vec<String>,
/// The message's string contents.
pub contents: String,
}
impl Accounted for Message {
fn bytes_used(&self) -> usize {
self.size
}
}
impl Message {
/// Parse the provided buffer as if it implements the [logger/syslog wire format].
///
/// Note that this is distinct from the parsing we perform for the debuglog log, which aslo
/// takes a `&[u8]` and is why we don't implement this as `TryFrom`.
///
/// [logger/syslog wire format]: https://fuchsia.googlesource.com/fuchsia/+/master/zircon/system/ulib/syslog/include/lib/syslog/wire_format.h
pub(super) fn from_logger(bytes: &[u8]) -> Result<Self, StreamError> {
if bytes.len() < MIN_PACKET_SIZE {
return Err(StreamError::ShortRead { len: bytes.len() });
}
let terminator = bytes[bytes.len() - 1];
if terminator != 0 {
return Err(StreamError::NotNullTerminated { terminator });
}
let pid = LittleEndian::read_u64(&bytes[..8]);
let tid = LittleEndian::read_u64(&bytes[8..16]);
let time = zx::Time::from_nanos(LittleEndian::read_i64(&bytes[16..24]));
let raw_severity = LittleEndian::read_i32(&bytes[24..28]);
let severity = Severity::try_from(raw_severity)?;
let dropped_logs = LittleEndian::read_u32(&bytes[28..METADATA_SIZE]) as usize;
// start reading tags after the header
let mut cursor = METADATA_SIZE;
let mut tag_len = bytes[cursor] as usize;
let mut tags = Vec::new();
while tag_len != 0 {
if tags.len() == MAX_TAGS {
return Err(StreamError::TooManyTags);
}
if tag_len > MAX_TAG_LEN - 1 {
return Err(StreamError::TagTooLong { index: tags.len(), len: tag_len });
}
if (cursor + tag_len + 1) > bytes.len() {
return Err(StreamError::OutOfBounds);
}
let tag_start = cursor + 1;
let tag_end = tag_start + tag_len;
let tag = str::from_utf8(&bytes[tag_start..tag_end])?;
tags.push(tag.to_owned());
cursor = tag_end;
tag_len = bytes[cursor] as usize;
}
let msg_start = cursor + 1;
let mut msg_end = cursor + 1;
while msg_end < bytes.len() {
if bytes[msg_end] == 0 {
let contents = str::from_utf8(&bytes[msg_start..msg_end])?.to_owned();
return Ok(Self {
size: cursor + contents.len() + 1,
tags,
contents,
pid,
tid,
time,
severity,
dropped_logs,
});
}
msg_end += 1;
}
Err(StreamError::OutOfBounds)
}
/// Convert this `Message` to a FIDL representation suitable for sending to `LogListenerSafe`.
pub fn for_listener(&self) -> LogMessage {
LogMessage {
pid: self.pid,
tid: self.tid,
time: self.time.into_nanos(),
severity: self.severity.for_listener(),
dropped_logs: self.dropped_logs as _,
tags: self.tags.clone(),
msg: self.contents.clone(),
}
}
}
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
#[repr(u8)]
pub enum Severity {
// giving these variants concrete discriminants creates a straightforward derived Ord impl
// TODO(fxrev.dev/375515) use the new constants defined in FIDL
Trace = 0x10,
Debug = 0x20,
Info = 0x30,
Warn = 0x40,
Error = 0x50,
Fatal = 0x60,
}
impl Severity {
fn for_listener(self) -> fx_log_severity_t {
match self {
// TODO(fxrev.dev/375515) update these to match new constants
Severity::Trace => -2,
Severity::Debug => -1,
Severity::Info => LogLevelFilter::Info as _,
Severity::Warn => LogLevelFilter::Warn as _,
Severity::Error => LogLevelFilter::Error as _,
Severity::Fatal => LogLevelFilter::Fatal as _,
}
}
}
impl TryFrom<fx_log_severity_t> for Severity {
type Error = StreamError;
fn try_from(raw: fx_log_severity_t) -> Result<Self, StreamError> {
if let Some(filter) = LogLevelFilter::from_primitive(raw as i8) {
Ok(Severity::from(filter))
} else if -10 <= raw && raw <= -2 {
// legacy values supported for high "verbosity" in our c++ frontends
// see //src/diagnostics/archivist/tests/logs/cpp/logs_tests.cc for examples
Ok(Severity::Trace)
} else {
Err(StreamError::InvalidSeverity { provided: raw })
}
}
}
impl From<LogLevelFilter> for Severity {
fn from(filter: LogLevelFilter) -> Self {
match filter {
// None here == -1, which is one level below Info==0
LogLevelFilter::None => Severity::Debug,
LogLevelFilter::Info => Severity::Info,
LogLevelFilter::Warn => Severity::Warn,
LogLevelFilter::Error => Severity::Error,
LogLevelFilter::Fatal => Severity::Fatal,
}
}
}
#[allow(non_camel_case_types)]
pub(super) type fx_log_severity_t = c_int;
#[repr(C)]
#[derive(Debug, Copy, Clone, Default, Eq, PartialEq)]
pub struct fx_log_metadata_t {
pub pid: zx::sys::zx_koid_t,
pub tid: zx::sys::zx_koid_t,
pub time: zx::sys::zx_time_t,
pub severity: fx_log_severity_t,
pub dropped_logs: u32,
}
#[repr(C)]
#[derive(Clone)]
pub struct fx_log_packet_t {
pub metadata: fx_log_metadata_t,
// Contains concatenated tags and message and a null terminating character at
// the end.
// char(tag_len) + "tag1" + char(tag_len) + "tag2\0msg\0"
pub data: [c_char; MAX_DATAGRAM_LEN - METADATA_SIZE],
}
impl Default for fx_log_packet_t {
fn default() -> fx_log_packet_t {
fx_log_packet_t {
data: [0; MAX_DATAGRAM_LEN - METADATA_SIZE],
metadata: Default::default(),
}
}
}
#[cfg(test)]
impl fx_log_packet_t {
/// This struct has no padding bytes, but we can't use zerocopy because it needs const
/// generics to support arrays this large.
pub fn as_bytes(&self) -> &[u8] {
unsafe {
std::slice::from_raw_parts(
(self as *const Self) as *const u8,
mem::size_of::<fx_log_packet_t>(),
)
}
}
/// Fills data with a single value for defined region.
pub fn fill_data(&mut self, region: std::ops::Range<usize>, with: c_char) {
self.data[region].iter_mut().for_each(|c| *c = with);
}
/// Copies bytes to data at specifies offset.
pub fn add_data<T: std::convert::TryInto<c_char> + Copy>(&mut self, offset: usize, bytes: &[T])
where
<T as std::convert::TryInto<c_char>>::Error: std::fmt::Debug,
{
self.data[offset..(offset + bytes.len())]
.iter_mut()
.enumerate()
.for_each(|(i, x)| *x = bytes[i].try_into().unwrap());
}
}
#[cfg(test)]
mod tests {
use super::*;
#[repr(C, packed)]
pub struct fx_log_metadata_t_packed {
pub pid: zx::sys::zx_koid_t,
pub tid: zx::sys::zx_koid_t,
pub time: zx::sys::zx_time_t,
pub severity: fx_log_severity_t,
pub dropped_logs: u32,
}
#[repr(C, packed)]
pub struct fx_log_packet_t_packed {
pub metadata: fx_log_metadata_t_packed,
/// Contains concatenated tags and message and a null terminating character at the end.
/// `char(tag_len) + "tag1" + char(tag_len) + "tag2\0msg\0"`
pub data: [c_char; MAX_DATAGRAM_LEN - METADATA_SIZE],
}
#[test]
fn abi_test() {
assert_eq!(METADATA_SIZE, 32);
assert_eq!(MAX_TAGS, 5);
assert_eq!(MAX_TAG_LEN, 64);
assert_eq!(mem::size_of::<fx_log_packet_t>(), MAX_DATAGRAM_LEN);
// Test that there is no padding
assert_eq!(mem::size_of::<fx_log_packet_t>(), mem::size_of::<fx_log_packet_t_packed>());
assert_eq!(mem::size_of::<fx_log_metadata_t>(), mem::size_of::<fx_log_metadata_t_packed>());
}
fn test_packet() -> fx_log_packet_t {
let mut packet: fx_log_packet_t = Default::default();
packet.metadata.pid = 1;
packet.metadata.tid = 2;
packet.metadata.time = 3;
packet.metadata.severity = -1;
packet.metadata.dropped_logs = 10;
packet
}
#[test]
fn short_reads() {
let packet = test_packet();
let one_short = &packet.as_bytes()[..METADATA_SIZE];
let two_short = &packet.as_bytes()[..METADATA_SIZE - 1];
assert_eq!(Message::from_logger(one_short), Err(StreamError::ShortRead { len: 32 }));
assert_eq!(Message::from_logger(two_short), Err(StreamError::ShortRead { len: 31 }));
}
#[test]
fn unterminated() {
let mut packet = test_packet();
let end = 9;
packet.data[end] = 1;
let buffer = &packet.as_bytes()[..MIN_PACKET_SIZE + end];
let parsed = Message::from_logger(buffer);
assert_eq!(parsed, Err(StreamError::NotNullTerminated { terminator: 1 }));
}
#[test]
fn tags_no_message() {
let mut packet = test_packet();
let end = 12;
packet.data[0] = end as c_char - 1;
packet.fill_data(1..end, 'A' as _);
packet.data[end] = 0;
let buffer = &packet.as_bytes()[..MIN_PACKET_SIZE + end]; // omit null-terminated
let parsed = Message::from_logger(buffer);
assert_eq!(parsed, Err(StreamError::OutOfBounds));
}
#[test]
fn tags_with_message() {
let mut packet = test_packet();
let a_start = 1;
let a_count = 11;
let a_end = a_start + a_count;
packet.data[0] = a_count as c_char;
packet.fill_data(a_start..a_end, 'A' as _);
packet.data[a_end] = 0; // terminate tags
let b_start = a_start + a_count + 1;
let b_count = 5;
let b_end = b_start + b_count;
packet.fill_data(b_start..b_end, 'B' as _);
let data_size = b_start + b_count;
let buffer = &packet.as_bytes()[..METADATA_SIZE + data_size + 1]; // null-terminate message
let parsed = Message::from_logger(buffer).unwrap();
assert_eq!(
parsed,
Message {
size: METADATA_SIZE + data_size,
pid: packet.metadata.pid,
tid: packet.metadata.tid,
time: zx::Time::from_nanos(packet.metadata.time),
severity: Severity::Debug,
dropped_logs: packet.metadata.dropped_logs as usize,
tags: vec![String::from("AAAAAAAAAAA")],
contents: String::from("BBBBB"),
}
);
}
#[test]
fn two_tags_no_message() {
let mut packet = test_packet();
let a_start = 1;
let a_count = 11;
let a_end = a_start + a_count;
packet.data[0] = a_count as c_char;
packet.fill_data(a_start..a_end, 'A' as _);
let b_start = a_end + 1;
let b_count = 5;
let b_end = b_start + b_count;
packet.data[a_end] = b_count as c_char;
packet.fill_data(b_start..b_end, 'B' as _);
let buffer = &packet.as_bytes()[..MIN_PACKET_SIZE + b_end];
let parsed = Message::from_logger(buffer);
assert_eq!(parsed, Err(StreamError::OutOfBounds));
}
#[test]
fn two_tags_with_message() {
let mut packet = test_packet();
let a_start = 1;
let a_count = 11;
let a_end = a_start + a_count;
packet.data[0] = a_count as c_char;
packet.fill_data(a_start..a_end, 'A' as _);
let b_start = a_end + 1;
let b_count = 5;
let b_end = b_start + b_count;
packet.data[a_end] = b_count as c_char;
packet.fill_data(b_start..b_end, 'B' as _);
let c_start = b_end + 1;
let c_count = 5;
let c_end = c_start + c_count;
packet.fill_data(c_start..c_end, 'C' as _);
let data_size = c_start + c_count;
let buffer = &packet.as_bytes()[..METADATA_SIZE + data_size + 1]; // null-terminated
let parsed = Message::from_logger(buffer).unwrap();
assert_eq!(
parsed,
Message {
size: METADATA_SIZE + data_size,
pid: packet.metadata.pid,
tid: packet.metadata.tid,
time: zx::Time::from_nanos(packet.metadata.time),
severity: Severity::Debug,
dropped_logs: packet.metadata.dropped_logs as usize,
tags: vec![String::from("AAAAAAAAAAA"), String::from("BBBBB")],
contents: String::from("CCCCC"),
}
);
}
#[test]
fn max_tags_with_message() {
let mut packet = test_packet();
let tags_start = 1;
let tag_len = 2;
let tag_size = tag_len + 1; // the length-prefix byte
for tag_num in 0..MAX_TAGS {
let start = tags_start + (tag_size * tag_num);
let end = start + tag_len;
packet.data[start - 1] = tag_len as c_char;
let ascii = 'A' as c_char + tag_num as c_char;
packet.fill_data(start..end, ascii);
}
let msg_start = tags_start + (tag_size * MAX_TAGS);
let msg_len = 5;
let msg_end = msg_start + msg_len;
let msg_ascii = 'A' as c_char + MAX_TAGS as c_char;
packet.fill_data(msg_start..msg_end, msg_ascii);
let min_buffer = &packet.as_bytes()[..METADATA_SIZE + msg_end + 1]; // null-terminated
let full_buffer = &packet.as_bytes()[..];
let min_parsed = Message::from_logger(min_buffer).unwrap();
let full_parsed = Message::from_logger(full_buffer).unwrap();
let expected_message = Message {
size: METADATA_SIZE + msg_end,
pid: packet.metadata.pid,
tid: packet.metadata.tid,
time: zx::Time::from_nanos(packet.metadata.time),
severity: Severity::Debug,
dropped_logs: packet.metadata.dropped_logs as usize,
contents: String::from_utf8(vec![msg_ascii as u8; msg_len]).unwrap(),
tags: (0..MAX_TAGS as _)
.map(|tag_num| {
String::from_utf8(vec![('A' as c_char + tag_num) as u8; tag_len]).unwrap()
})
.collect(),
};
assert_eq!(min_parsed, expected_message);
assert_eq!(full_parsed, expected_message);
}
#[test]
fn max_tags() {
let mut packet = test_packet();
let tags_start = 1;
let tag_len = 2;
let tag_size = tag_len + 1; // the length-prefix byte
for tag_num in 0..MAX_TAGS {
let start = tags_start + (tag_size * tag_num);
let end = start + tag_len;
packet.data[start - 1] = tag_len as c_char;
let ascii = 'A' as c_char + tag_num as c_char;
packet.fill_data(start..end, ascii);
}
let msg_start = tags_start + (tag_size * MAX_TAGS);
let buffer_missing_terminator = &packet.as_bytes()[..METADATA_SIZE + msg_start];
assert_eq!(
Message::from_logger(buffer_missing_terminator),
Err(StreamError::OutOfBounds),
"can't parse an empty message without a nul terminator"
);
let buffer = &packet.as_bytes()[..METADATA_SIZE + msg_start + 1]; // null-terminated
let parsed = Message::from_logger(buffer).unwrap();
assert_eq!(
parsed,
Message {
size: METADATA_SIZE + msg_start,
pid: packet.metadata.pid,
tid: packet.metadata.tid,
time: zx::Time::from_nanos(packet.metadata.time),
severity: Severity::Debug,
dropped_logs: packet.metadata.dropped_logs as usize,
tags: (0..MAX_TAGS as _)
.map(|tag_num| String::from_utf8(vec![('A' as c_char + tag_num) as u8; 2])
.unwrap())
.collect(),
contents: String::new(),
}
);
}
#[test]
fn no_tags_with_message() {
let mut packet = test_packet();
packet.data[0] = 0;
packet.data[1] = 'A' as _;
packet.data[2] = 'A' as _; // measured size ends here
packet.data[3] = 0;
let buffer = &packet.as_bytes()[..METADATA_SIZE + 4]; // 0 tag size + 2 byte message + null
let parsed = Message::from_logger(buffer).unwrap();
assert_eq!(
parsed,
Message {
size: METADATA_SIZE + 3,
pid: packet.metadata.pid,
tid: packet.metadata.tid,
time: zx::Time::from_nanos(packet.metadata.time),
severity: Severity::Debug,
dropped_logs: packet.metadata.dropped_logs as usize,
tags: vec![],
contents: String::from("AA"),
}
);
}
}