Skip to main content

diagnostics_log_encoding/
parse.rs

1// Copyright 2020 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5//! Parse diagnostic records from streams, returning FIDL-generated structs that match expected
6//! diagnostic service APIs.
7
8use crate::{ArgType, Argument, Header, RawSeverity, Record, Value, constants, zx};
9use std::borrow::Cow;
10use thiserror::Error;
11use zerocopy::FromBytes;
12
13/// Extracts the basic information of a log message: timestamp and severity.
14pub fn basic_info(buf: &[u8]) -> Result<(zx::BootInstant, RawSeverity), ParseError> {
15    let (header, after_header) =
16        Header::read_from_prefix(buf).map_err(|_| ParseError::InvalidHeader)?;
17    if header.raw_type() != crate::TRACING_FORMAT_LOG_RECORD_TYPE {
18        return Err(ParseError::InvalidRecordType);
19    }
20    let (timestamp, _) =
21        i64::read_from_prefix(after_header).map_err(|_| ParseError::InvalidTimestamp)?;
22    Ok((zx::BootInstant::from_nanos(timestamp), header.severity()))
23}
24
25/// Attempt to parse a diagnostic record from the head of this buffer, returning the record and any
26/// unused portion of the buffer if successful.
27pub fn parse_record(buf: &[u8]) -> Result<(Record<'_>, &[u8]), ParseError> {
28    let (header, after_header) =
29        Header::read_from_prefix(buf).map_err(|_| ParseError::InvalidHeader)?;
30
31    if header.raw_type() != crate::TRACING_FORMAT_LOG_RECORD_TYPE {
32        return Err(ParseError::ValueOutOfValidRange);
33    }
34
35    let (timestamp, remaining_buffer) =
36        i64::read_from_prefix(after_header).map_err(|_| ParseError::InvalidTimestamp)?;
37
38    let arguments_length = if header.size_words() >= 2 {
39        // Remove two word lengths for header and timestamp.
40        (header.size_words() - 2) as usize * 8
41    } else {
42        return Err(ParseError::ValueOutOfValidRange);
43    };
44
45    let Some((mut arguments_buffer, remaining)) =
46        remaining_buffer.split_at_checked(arguments_length)
47    else {
48        return Err(ParseError::ValueOutOfValidRange);
49    };
50
51    let mut arguments = vec![];
52    let mut state = ParseState::Initial;
53    while !arguments_buffer.is_empty() {
54        let (argument, rem) = parse_argument_internal(arguments_buffer, &mut state)?;
55        arguments_buffer = rem;
56        arguments.push(argument);
57    }
58
59    Ok((
60        Record {
61            timestamp: zx::BootInstant::from_nanos(timestamp),
62            severity: header.severity(),
63            arguments,
64        },
65        remaining,
66    ))
67}
68
69/// Internal parser state.
70/// Used to support handling of invalid utf-8 in msg fields.
71enum ParseState {
72    /// Initial parsing state
73    Initial,
74    /// We're in a message
75    InMessage,
76    /// We're in arguments (no special Unicode treatment)
77    InArguments,
78}
79
80/// Parses an argument
81pub fn parse_argument(buf: &[u8]) -> Result<(Argument<'_>, &[u8]), ParseError> {
82    parse_argument_internal(buf, &mut ParseState::Initial)
83}
84
85fn parse_argument_internal<'a>(
86    buf: &'a [u8],
87    state: &mut ParseState,
88) -> Result<(Argument<'a>, &'a [u8]), ParseError> {
89    let (header, after_header) =
90        Header::read_from_prefix(buf).map_err(|_| ParseError::InvalidArgumentHeader)?;
91    let arg_ty = ArgType::try_from(header.raw_type())?;
92
93    let (name, after_name) = string_ref(header.name_ref(), after_header, false)?;
94    if matches!(state, ParseState::Initial) && name == Cow::Borrowed(constants::MESSAGE) {
95        *state = ParseState::InMessage;
96    }
97    let (value, after_value) = match arg_ty {
98        ArgType::Null => (Value::UnsignedInt(1), after_name),
99        ArgType::I64 => {
100            let (n, rem) =
101                i64::read_from_prefix(after_name).map_err(|_| ParseError::InvalidArgument)?;
102            (Value::SignedInt(n), rem)
103        }
104        ArgType::U64 => {
105            let (n, rem) =
106                u64::read_from_prefix(after_name).map_err(|_| ParseError::InvalidArgument)?;
107            (Value::UnsignedInt(n), rem)
108        }
109        ArgType::F64 => {
110            let (n, rem) =
111                f64::read_from_prefix(after_name).map_err(|_| ParseError::InvalidArgument)?;
112            (Value::Floating(n), rem)
113        }
114        ArgType::String => {
115            let (s, rem) =
116                string_ref(header.value_ref(), after_name, matches!(state, ParseState::InMessage))?;
117            (Value::Text(s), rem)
118        }
119        ArgType::Bool => (Value::Boolean(header.bool_val()), after_name),
120        ArgType::Pointer | ArgType::Koid | ArgType::I32 | ArgType::U32 => {
121            return Err(ParseError::Unsupported);
122        }
123    };
124    if matches!(state, ParseState::InMessage) {
125        *state = ParseState::InArguments;
126    }
127
128    Ok((Argument::new(name, value), after_value))
129}
130
131fn string_ref(
132    ref_mask: u16,
133    buf: &[u8],
134    support_invalid_utf8: bool,
135) -> Result<(Cow<'_, str>, &[u8]), ParseError> {
136    if ref_mask == 0 {
137        return Ok((Cow::Borrowed(""), buf));
138    }
139    if (ref_mask & (1 << 15)) == 0 {
140        return Err(ParseError::Unsupported);
141    }
142    // zero out the top bit
143    let name_len = (ref_mask & !(1 << 15)) as usize;
144    let Some((name, after_name)) = buf.split_at_checked(name_len) else {
145        return Err(ParseError::ValueOutOfValidRange);
146    };
147    let parsed = if support_invalid_utf8 {
148        String::from_utf8_lossy(name)
149    } else {
150        let name = std::str::from_utf8(name)?;
151        Cow::Borrowed(name)
152    };
153    // In Fuchsia Trace Format (FXT), inline string contents are padded with zeroes up to the next
154    // 8-byte word boundary:
155    // 1. `name_len % 8` calculates how many bytes exist in the final partial 8-byte word (0..=7).
156    // 2. `8 - (name_len % 8)` determines the distance to the next 8-byte boundary (1..=8).
157    // 3. The outer `% 8` converts 8 back to 0 when `name_len` is already a multiple of 8.
158    let padding_len = (8 - (name_len % 8)) % 8;
159    let Some((_padding, after_padding)) = after_name.split_at_checked(padding_len) else {
160        return Err(ParseError::ValueOutOfValidRange);
161    };
162    Ok((parsed, after_padding))
163}
164
165/// Errors which occur when interacting with streams of diagnostic records.
166#[derive(Debug, Clone, Error)]
167pub enum ParseError {
168    /// We attempted to parse bytes as a type for which the bytes are not a valid pattern.
169    #[error("value out of range")]
170    ValueOutOfValidRange,
171
172    /// We attempted to parse or encode values which are not yet supported by this implementation of
173    /// the Fuchsia Tracing format.
174    #[error("unsupported value type")]
175    Unsupported,
176
177    /// We failed to parse a record header.
178    #[error("found invalid header")]
179    InvalidHeader,
180
181    /// We failed to parse a record header.
182    #[error("found invalid header in an argument")]
183    InvalidArgumentHeader,
184
185    /// We failed to parse a record timestamp.
186    #[error("found invalid timestamp after header")]
187    InvalidTimestamp,
188
189    /// We failed to parse a record argument.
190    #[error("found invalid argument")]
191    InvalidArgument,
192
193    /// The record type wasn't the one we expected: LOGS
194    #[error("found invalid record type")]
195    InvalidRecordType,
196
197    /// We failed to parse a complete item.
198    #[error("parsing terminated early, remaining bytes: {0:?}")]
199    Incomplete(usize),
200}
201
202impl From<std::str::Utf8Error> for ParseError {
203    fn from(_: std::str::Utf8Error) -> Self {
204        ParseError::ValueOutOfValidRange
205    }
206}
207
208#[cfg(test)]
209mod tests {
210    use super::*;
211    use crate::encode::{Encoder, EncoderOpts};
212    use fidl_fuchsia_diagnostics_types::Severity;
213    use std::io::Cursor;
214
215    #[fuchsia::test]
216    fn basic_structured_info() {
217        let expected_timestamp = zx::BootInstant::from_nanos(72);
218        let record = Record {
219            timestamp: expected_timestamp,
220            severity: Severity::Error.into_primitive(),
221            arguments: vec![],
222        };
223        let mut buffer = Cursor::new(vec![0u8; 1000]);
224        let mut encoder = Encoder::new(&mut buffer, EncoderOpts::default());
225        encoder.write_record(record).unwrap();
226        let encoded = &buffer.get_ref().as_slice()[..buffer.position() as usize];
227
228        let (timestamp, severity) = basic_info(encoded).unwrap();
229        assert_eq!(timestamp, expected_timestamp);
230        assert_eq!(severity, Severity::Error.into_primitive());
231    }
232
233    #[fuchsia::test]
234    fn parse_record_with_zeros() {
235        let expected_timestamp = zx::BootInstant::from_nanos(72);
236        let record = Record {
237            timestamp: expected_timestamp,
238            severity: Severity::Error.into_primitive(),
239            arguments: vec![],
240        };
241        let mut buffer = Cursor::new(vec![0u8; 1000]);
242        let mut encoder = Encoder::new(&mut buffer, EncoderOpts::default());
243        encoder.write_record(record.clone()).unwrap();
244
245        // Ensure that some additional padding is ok to parse the record.
246        let encoded = &buffer.get_ref().as_slice()[..buffer.position() as usize + 3];
247
248        let (result_record, rem) = parse_record(encoded).unwrap();
249        assert_eq!(rem.len(), 3);
250        assert_eq!(record, result_record);
251    }
252
253    #[fuchsia::test]
254    fn test_string_ref_padding() {
255        let mut buf = vec![b'f', b'o', b'o', 0, 0, 0, 0, 0];
256        buf.extend_from_slice(b"extra");
257        let ref_mask = (1 << 15) | 3;
258        let (parsed, remaining) = string_ref(ref_mask, &buf, false).unwrap();
259        assert_eq!(parsed, "foo");
260        assert_eq!(remaining, b"extra");
261    }
262}