Skip to main content

diagnostics_message/
ffi.rs

1// Copyright 2025 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
5use crate::error::MessageError;
6use crate::{ExtendedMetadata, MessageFormatter};
7use bumpalo::Bump;
8use bumpalo::collections::{String as BumpaloString, Vec as BumpaloVec};
9use diagnostics_data::{ExtendedMoniker, Severity};
10use diagnostics_log_encoding::{Argument, Record, Value};
11use flyweights::FlyStr;
12use static_assertions::const_assert;
13use std::fmt::Write;
14use std::marker::PhantomData;
15use std::ops::Deref;
16use std::str;
17use zx::BootInstant;
18
19pub use crate::constants::*;
20
21/// Array for FFI purposes between C++ and Rust.
22/// If len is 0, ptr is allowed to be nullptr,
23/// otherwise, ptr must be valid.
24#[repr(C)]
25pub struct CppArray<'a, T> {
26    /// Number of elements in the array
27    pub len: usize,
28    /// Pointer to the first element in the array,
29    /// may be null in the case of a 0 length array,
30    /// but is not guaranteed to always be null of
31    /// len is 0.
32    pub ptr: *const T,
33
34    phantom: PhantomData<&'a T>,
35}
36
37impl<T> Deref for CppArray<'_, T> {
38    type Target = [T];
39
40    fn deref(&self) -> &Self::Target {
41        if self.len == 0 || self.ptr.is_null() {
42            &[]
43        } else {
44            // SAFETY: The `CPPArray` is constructed from valid slices or arrays,
45            // ensuring `self.ptr` points to `self.len` elements of type `T`.
46            unsafe { std::slice::from_raw_parts(self.ptr, self.len) }
47        }
48    }
49}
50
51impl<T> Default for CppArray<'_, T> {
52    fn default() -> Self {
53        CppArray { len: 0, ptr: std::ptr::null(), phantom: PhantomData }
54    }
55}
56
57impl<'a> From<&'a str> for CppString<'a> {
58    fn from(value: &'a str) -> Self {
59        Self { inner: CppArray { len: value.len(), ptr: value.as_ptr(), phantom: PhantomData } }
60    }
61}
62
63/// Represents a UTF-8 encoded string for FFI purposes.
64/// This is equivalent to a CppArray<u8> as it is
65/// #[repr(transparent)], but with the additional
66/// constraint that the contents of the array
67/// is a valid UTF-8 string.
68#[derive(Default)]
69#[repr(transparent)]
70pub struct CppString<'a> {
71    inner: CppArray<'a, u8>,
72}
73
74impl Deref for CppString<'_> {
75    type Target = str;
76
77    fn deref(&self) -> &Self::Target {
78        if self.inner.len == 0 || self.inner.ptr.is_null() {
79            ""
80        } else {
81            // SAFETY: CppString is always constructed from valid UTF-8.
82            unsafe {
83                std::str::from_utf8_unchecked(std::slice::from_raw_parts(
84                    self.inner.ptr,
85                    self.inner.len,
86                ))
87            }
88        }
89    }
90}
91
92impl<'a> From<Option<&'a str>> for CppString<'a> {
93    fn from(value: Option<&'a str>) -> Self {
94        value.map(|v| v.into()).unwrap_or_default()
95    }
96}
97
98impl<'a, T> From<&'a [T]> for CppArray<'a, T> {
99    fn from(value: &'a [T]) -> Self {
100        CppArray { len: value.len(), ptr: value.as_ptr(), phantom: PhantomData }
101    }
102}
103
104impl<'a> From<BumpaloString<'a>> for CppString<'a> {
105    fn from(value: BumpaloString<'a>) -> Self {
106        value.into_bump_str().into()
107    }
108}
109
110/// Log message representation for FFI with C++
111#[repr(C)]
112pub struct LogMessage<'a> {
113    /// Severity of a log message.
114    pub severity: u8,
115    /// Tags in a log message, guaranteed to be non-null.
116    pub tags: CppArray<'a, CppString<'a>>,
117    /// Process ID from a LogMessage, or 0 if unknown
118    pub pid: u64,
119    /// Thread ID from a LogMessage, or 0 if unknown
120    pub tid: u64,
121    /// Number of dropped log messages.
122    pub dropped: u64,
123    /// The UTF-encoded log message, guaranteed to be valid UTF-8.
124    pub message: CppString<'a>,
125    /// Timestamp on the boot timeline of the log message,
126    /// in nanoseconds.
127    pub timestamp: i64,
128}
129
130// These are allocated using the Bumpalo allocator.
131const_assert!(!std::mem::needs_drop::<LogMessage<'_>>());
132
133pub struct CPPLogMessageBuilder<'a> {
134    severity: u8,
135    tags: BumpaloVec<'a, BumpaloString<'a>>,
136    pid: Option<u64>,
137    tid: Option<u64>,
138    dropped: u64,
139    file: Option<String>,
140    line: Option<u64>,
141    moniker: Option<BumpaloString<'a>>,
142    message: Option<String>,
143    timestamp: i64,
144    kvps: String,
145    allocator: &'a Bump,
146}
147
148// Escape quotes in a string per the Feedback format
149fn escape_quotes(input: &str, output: &mut String) {
150    for ch in input.chars() {
151        if ch == '"' || ch == '\\' {
152            output.push('\\');
153        }
154        output.push(ch);
155    }
156}
157
158impl<'a> CPPLogMessageBuilder<'a> {
159    fn set_raw_severity(mut self, raw_severity: u8) -> Self {
160        self.severity = raw_severity;
161        self
162    }
163
164    fn add_tag(mut self, tag: impl Into<String>) -> Self {
165        self.tags.push(BumpaloString::from_str_in(&tag.into(), self.allocator));
166        self
167    }
168
169    fn set_pid(mut self, pid: u64) -> Self {
170        self.pid = Some(pid);
171        self
172    }
173
174    fn set_tid(mut self, tid: u64) -> Self {
175        self.tid = Some(tid);
176        self
177    }
178
179    fn set_dropped(mut self, dropped: u64) -> Self {
180        self.dropped = dropped;
181        self
182    }
183
184    fn set_file(mut self, file: impl Into<String>) -> Self {
185        self.file = Some(file.into());
186        self
187    }
188
189    fn set_line(mut self, line: u64) -> Self {
190        self.line = Some(line);
191        self
192    }
193
194    fn set_message(mut self, msg: impl Into<String>) -> Self {
195        self.message = Some(msg.into());
196        self
197    }
198
199    fn add_kvp(mut self, kvp: &Argument<'_>) -> Self {
200        if !self.kvps.is_empty() {
201            self.kvps.push(' ');
202        }
203
204        self.kvps.push_str(kvp.name());
205        self.kvps.push('=');
206        match kvp.value() {
207            Value::Text(value) => {
208                self.kvps.push('"');
209                escape_quotes(&value, &mut self.kvps);
210                self.kvps.push('"');
211            }
212            Value::SignedInt(value) => {
213                write!(self.kvps, "{value}").unwrap();
214            }
215            Value::UnsignedInt(value) => {
216                write!(self.kvps, "{value}").unwrap();
217            }
218            Value::Floating(value) => {
219                write!(self.kvps, "{value}").unwrap();
220            }
221            Value::Boolean(value) => {
222                if value {
223                    write!(self.kvps, "true").unwrap();
224                } else {
225                    write!(self.kvps, "false").unwrap();
226                }
227            }
228        }
229        self
230    }
231
232    fn set_moniker(mut self, value: &str) -> Self {
233        self.moniker = Some(BumpaloString::from_str_in(value, self.allocator));
234        self
235    }
236
237    pub fn build(mut self) -> &'a mut LogMessage<'a> {
238        let allocator = self.allocator;
239
240        // Format the message in accordance with the Feedback format
241        let msg_str = self
242            .message
243            .as_ref()
244            .map(|value| bumpalo::format!(in &allocator,"{value}",))
245            .unwrap_or_else(|| BumpaloString::new_in(allocator));
246
247        let mut output = match (&self.file, &self.line) {
248            (Some(file), Some(line)) => {
249                let mut value = bumpalo::format!(in &allocator, "[{file}({line})]",);
250                if !msg_str.is_empty() {
251                    value.push(' ');
252                }
253                value
254            }
255            _ => BumpaloString::new_in(allocator),
256        };
257
258        output.push_str(&msg_str);
259        if !msg_str.is_empty() && !self.kvps.is_empty() {
260            output.push(' ');
261        }
262        output.push_str(&self.kvps);
263
264        if let Some(moniker) = &self.moniker {
265            let component_name = moniker.split("/").last();
266            if let Some(component_name) = component_name
267                && !self.tags.iter().any(|value| value.as_str() == component_name)
268            {
269                self.tags.insert(0, bumpalo::format!(in &allocator, "{}", component_name));
270            }
271        }
272
273        let tags: &[_] = self
274            .allocator
275            .alloc_slice_fill_iter(self.tags.drain(..).map(|s| s.into_bump_str().into()));
276
277        allocator.alloc(LogMessage {
278            severity: self.severity,
279            dropped: self.dropped,
280            tags: tags.into(),
281            pid: self.pid.unwrap_or(0),
282            tid: self.tid.unwrap_or(0),
283            message: output.into_bump_str().into(),
284            timestamp: self.timestamp,
285        })
286    }
287}
288
289struct CPPLogMessageBuilderBuilder<'a>(&'a Bump);
290
291impl<'a> CPPLogMessageBuilderBuilder<'a> {
292    fn configure(
293        self,
294        _component_url: Option<FlyStr>,
295        moniker: Option<ExtendedMoniker>,
296        severity: Severity,
297        timestamp: BootInstant,
298    ) -> Result<CPPLogMessageBuilder<'a>, MessageError> {
299        Ok(CPPLogMessageBuilder {
300            severity: severity as u8,
301            tags: BumpaloVec::new_in(self.0),
302            pid: None,
303            tid: None,
304            dropped: 0,
305            file: None,
306            timestamp: timestamp.into_nanos(),
307            line: None,
308            allocator: self.0,
309            kvps: String::new(),
310            moniker: moniker.map(|value| bumpalo::format!(in self.0,"{}", value)),
311            message: None,
312        })
313    }
314}
315
316pub fn build_logs_data<'a>(
317    input: &Record<'_>,
318    source: Option<ExtendedMetadata>,
319    allocator: &'a Bump,
320) -> Result<&'a mut LogMessage<'a>, MessageError> {
321    let builder = CPPLogMessageBuilderBuilder(allocator);
322    let (raw_severity, severity) = Severity::parse_exact(input.severity);
323    let (maybe_moniker, maybe_url, _) = source
324        .map(|value| (Some(value.moniker), Some(value.url), Some(value.rolled_out_logs)))
325        .unwrap_or((None, None, None));
326    let mut builder =
327        builder.configure(maybe_url.map(FlyStr::new), None, severity, input.timestamp)?;
328    if let Some(moniker) = maybe_moniker {
329        builder = builder.set_moniker(moniker.as_ref());
330    }
331    if let Some(raw_severity) = raw_severity {
332        builder = builder.set_raw_severity(raw_severity);
333    }
334
335    for argument in input.arguments.iter() {
336        match argument {
337            Argument::Tag(tag) => {
338                builder = builder.add_tag(tag.as_ref());
339            }
340            Argument::Pid(pid) => {
341                builder = builder.set_pid(pid.raw_koid());
342            }
343            Argument::Tid(tid) => {
344                builder = builder.set_tid(tid.raw_koid());
345            }
346            Argument::Dropped(dropped) => {
347                builder = builder.set_dropped(*dropped);
348            }
349            Argument::File(file) => {
350                builder = builder.set_file(file.as_ref());
351            }
352            Argument::Line(line) => {
353                builder = builder.set_line(*line);
354            }
355            Argument::Message(msg) => {
356                builder = builder.set_message(msg.as_ref());
357            }
358            Argument::Other { value: _, name: _ } => builder = builder.add_kvp(argument),
359        }
360    }
361
362    Ok(builder.build())
363}
364
365/// Constructs a `CPPLogsMessage` from the provided bytes, assuming the bytes
366/// are in the format specified as in the [log encoding], and come from
367///
368/// an Archivist LogStream with moniker, URL, and dropped logs output enabled.
369/// [log encoding] https://fuchsia.dev/fuchsia-src/development/logs/encodings
370pub fn ffi_from_extended_record<'a, 'b>(
371    bytes: &'a [u8],
372    allocator: &'b Bump,
373) -> Result<(&'b mut LogMessage<'b>, &'a [u8]), MessageError> {
374    let (input, remaining) = diagnostics_log_encoding::parse::parse_record(bytes)?;
375    let (source, new_remaining) = if remaining.len() >= 16 {
376        let moniker_len = u32::from_le_bytes(remaining[0..4].try_into().unwrap()) as usize;
377        let component_url_len = u32::from_le_bytes(remaining[4..8].try_into().unwrap()) as usize;
378        let rolled_out_logs = u64::from_le_bytes(remaining[8..16].try_into().unwrap());
379        let mut offset: usize = 16;
380
381        // NOTE: This addition is safe as all platforms Fuchsia supports are 64-bit,
382        // so usize will never overflow.
383        let moniker_padded_len = (moniker_len + 7) & !7;
384        let component_url_padded_len = (component_url_len + 7) & !7;
385        let moniker_padded_end = offset + moniker_padded_len;
386        let url_padded_end = moniker_padded_end + component_url_padded_len;
387        if url_padded_end > remaining.len() {
388            return Err(MessageError::OutOfBounds);
389        }
390
391        let moniker = str::from_utf8(&remaining[offset..offset + moniker_len])?;
392        offset += moniker_padded_len;
393        let url = str::from_utf8(&remaining[offset..offset + component_url_len])?;
394        offset += component_url_padded_len;
395        (
396            Some(ExtendedMetadata {
397                moniker: ExtendedMoniker::parse_str(moniker)?,
398                url: url.into(),
399                rolled_out_logs,
400            }),
401            &remaining[offset..],
402        )
403    } else {
404        (None, remaining)
405    };
406    let record = build_logs_data(&input, source, allocator)?;
407    Ok((record, new_remaining))
408}
409
410pub struct CPPMessageFormatter<'a>(pub &'a Bump);
411impl<'a> MessageFormatter for &CPPMessageFormatter<'a> {
412    type Result = &'a mut LogMessage<'a>;
413
414    fn format(
415        &mut self,
416        record: &Record<'_>,
417        metadata: Option<ExtendedMetadata>,
418    ) -> Result<Self::Result, MessageError> {
419        build_logs_data(record, metadata, self.0)
420    }
421}
422
423#[cfg(test)]
424mod test {
425    use super::*;
426    use crate::MessageParser;
427    use bumpalo::Bump;
428    use diagnostics_log_encoding::encode::{Encoder, EncoderOpts};
429    use diagnostics_log_encoding::{Argument, Header, LOG_CONTROL_BIT, Record};
430    use std::io::Cursor;
431    use zx::BootInstant;
432
433    fn overwrite_header_tag(bytes: &mut [u8], tag: u32) {
434        if bytes.len() >= 8 {
435            let mut header = Header(u64::from_le_bytes(bytes[0..8].try_into().unwrap()));
436            header.set_tag(tag);
437            bytes[0..8].copy_from_slice(&header.0.to_le_bytes());
438        }
439    }
440
441    #[fuchsia::test]
442    fn test_short_read() {
443        let mut parser = MessageParser::default();
444        let allocator = Bump::new();
445        let formatter = CPPMessageFormatter(&allocator);
446        let bytes = vec![0u8; 7];
447        let res = parser.parse_next(&bytes, &formatter);
448        assert!(matches!(res, Err(MessageError::ShortRead { len: 7 })));
449    }
450
451    #[fuchsia::test]
452    fn test_normal_parsing() {
453        let mut parser = MessageParser::default();
454        let allocator = Bump::new();
455        let formatter = CPPMessageFormatter(&allocator);
456
457        let record = Record {
458            timestamp: BootInstant::from_nanos(72),
459            severity: 0x30,
460            arguments: vec![Argument::message("hello world")],
461        };
462        let mut buffer = Cursor::new(vec![0u8; 1024]);
463        let mut encoder = Encoder::new(&mut buffer, EncoderOpts::default());
464        encoder.write_record(record).unwrap();
465
466        let len = buffer.position() as usize;
467        let mut bytes = buffer.into_inner();
468        bytes.truncate(len);
469
470        let res = parser.parse_next(&bytes, &formatter).unwrap();
471        assert!(res.0.is_some());
472        let log_message = &res.0.unwrap();
473        assert_eq!(&*log_message.message, "hello world");
474        assert_eq!(log_message.timestamp, 72);
475        assert_eq!(log_message.severity, 0x30);
476    }
477
478    #[fuchsia::test]
479    fn test_escaping_in_kvp() {
480        let mut parser = MessageParser::default();
481        let allocator = Bump::new();
482        let formatter = CPPMessageFormatter(&allocator);
483
484        let record = Record {
485            timestamp: BootInstant::from_nanos(72),
486            severity: 0x30,
487            arguments: vec![
488                Argument::message("hello world"),
489                Argument::new("key", r#"val"with\escapes"#),
490            ],
491        };
492        let mut buffer = Cursor::new(vec![0u8; 1024]);
493        let mut encoder = Encoder::new(&mut buffer, EncoderOpts::default());
494        encoder.write_record(record).unwrap();
495
496        let len = buffer.position() as usize;
497        let mut bytes = buffer.into_inner();
498        bytes.truncate(len);
499
500        let res = parser.parse_next(&bytes, &formatter).unwrap();
501        assert!(res.0.is_some());
502        let log_message = &res.0.unwrap();
503        assert_eq!(&*log_message.message, r#"hello world key="val\"with\\escapes""#);
504    }
505
506    #[fuchsia::test]
507    fn test_out_of_bounds_extended_record() {
508        let allocator = Bump::new();
509
510        let record = Record {
511            timestamp: BootInstant::from_nanos(72),
512            severity: 0x30,
513            arguments: vec![Argument::message("hello world")],
514        };
515        let mut buffer = Cursor::new(vec![0u8; 1024]);
516        let mut encoder = Encoder::new(&mut buffer, EncoderOpts::default());
517        encoder.write_record(record).unwrap();
518        let len = buffer.position() as usize;
519        let mut bytes = buffer.into_inner();
520        bytes.truncate(len);
521
522        // Append a corrupt moniker_len or component_url_len in the remaining slice.
523        let extended_metadata_suffix = [
524            0xE8, 0x03, 0x00, 0x00, // moniker_len = 1000
525            0xE8, 0x03, 0x00, 0x00, // component_url_len = 1000
526            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // rolled_out_logs = 0
527        ];
528        bytes.extend_from_slice(&extended_metadata_suffix);
529
530        let res = ffi_from_extended_record(&bytes, &allocator);
531        assert!(matches!(res, Err(MessageError::OutOfBounds)));
532    }
533
534    #[fuchsia::test]
535    fn test_control_message_tags() {
536        let allocator = Bump::new();
537        let formatter = CPPMessageFormatter(&allocator);
538        let mut parser = MessageParser::default();
539
540        let tag_id = 0;
541
542        let control_record = Record {
543            timestamp: BootInstant::from_nanos(72),
544            severity: 0x30,
545            arguments: vec![
546                Argument::new("moniker", "test/moniker"),
547                Argument::new("url", "fuchsia-pkg://test"),
548            ],
549        };
550
551        let mut buffer = Cursor::new(vec![0u8; 1024]);
552        let mut encoder = Encoder::new(&mut buffer, EncoderOpts::default());
553        encoder.write_record(control_record).unwrap();
554
555        let len = buffer.position() as usize;
556        let mut bytes = buffer.into_inner();
557        bytes.truncate(len);
558
559        overwrite_header_tag(&mut bytes, LOG_CONTROL_BIT);
560
561        let (log, _) = parser.parse_next(&bytes, &formatter).unwrap();
562        assert!(log.is_none());
563
564        let tag_data = parser.tag_map.get(&tag_id).unwrap();
565        assert_eq!(tag_data.moniker, ExtendedMoniker::parse_str("test/moniker").unwrap());
566        assert_eq!(tag_data.url, "fuchsia-pkg://test");
567
568        let rolled_out_record = Record {
569            timestamp: BootInstant::from_nanos(73),
570            severity: 0x30,
571            arguments: vec![Argument::new("rolled_out", 5u64)],
572        };
573
574        let mut buffer2 = Cursor::new(vec![0u8; 1024]);
575        let mut encoder2 = Encoder::new(&mut buffer2, EncoderOpts::default());
576        encoder2.write_record(rolled_out_record).unwrap();
577
578        let len2 = buffer2.position() as usize;
579        let mut bytes2 = buffer2.into_inner();
580        bytes2.truncate(len2);
581
582        overwrite_header_tag(&mut bytes2, LOG_CONTROL_BIT);
583
584        let (log2, _) = parser.parse_next(&bytes2, &formatter).unwrap();
585        assert!(log2.is_some());
586
587        let tag_data2 = parser.tag_map.get(&tag_id).unwrap();
588        assert_eq!(&*log2.unwrap().message, "rolled_out=5");
589        assert_eq!(tag_data2.moniker, ExtendedMoniker::parse_str("test/moniker").unwrap());
590
591        let normal_record = Record {
592            timestamp: BootInstant::from_nanos(74),
593            severity: 0x30,
594            arguments: vec![Argument::message("some log with tag")],
595        };
596
597        let mut buffer3 = Cursor::new(vec![0u8; 1024]);
598        let mut encoder3 = Encoder::new(&mut buffer3, EncoderOpts::default());
599        encoder3.write_record(normal_record).unwrap();
600
601        let len3 = buffer3.position() as usize;
602        let mut bytes3 = buffer3.into_inner();
603        bytes3.truncate(len3);
604
605        overwrite_header_tag(&mut bytes3, tag_id);
606
607        let (log3, _) = parser.parse_next(&bytes3, &formatter).unwrap();
608
609        let log_msg3 = &log3.unwrap();
610        assert_eq!(&*log_msg3.message, "some log with tag");
611        let tags = &*log_msg3.tags;
612        assert_eq!(tags.len(), 1);
613        assert_eq!(&*tags[0], "moniker");
614    }
615
616    #[fuchsia::test]
617    fn test_message_with_kvps() {
618        let mut parser = MessageParser::default();
619        let allocator = Bump::new();
620        let formatter = CPPMessageFormatter(&allocator);
621
622        let record = Record {
623            timestamp: BootInstant::from_nanos(100),
624            severity: 0x10,
625            arguments: vec![
626                Argument::message("A message"),
627                Argument::new("key1", "value1"),
628                Argument::new("key2", 123u64),
629            ],
630        };
631        let mut buffer = Cursor::new(vec![0u8; 1024]);
632        let mut encoder = Encoder::new(&mut buffer, EncoderOpts::default());
633        encoder.write_record(record).unwrap();
634
635        let len = buffer.position() as usize;
636        let mut bytes = buffer.into_inner();
637        bytes.truncate(len);
638
639        let res = parser.parse_next(&bytes, &formatter).unwrap();
640        assert!(res.0.is_some());
641        let log_message = res.0.unwrap();
642        assert_eq!(&*log_message.message, "A message key1=\"value1\" key2=123");
643    }
644
645    #[fuchsia::test]
646    fn test_file_line_message_with_kvps() {
647        let mut parser = MessageParser::default();
648        let allocator = Bump::new();
649        let formatter = CPPMessageFormatter(&allocator);
650
651        let record = Record {
652            timestamp: BootInstant::from_nanos(100),
653            severity: 0x10,
654            arguments: vec![
655                Argument::file("src/file.rs"),
656                Argument::line(42),
657                Argument::message("Another message"),
658                Argument::new("temp", 30.5),
659                Argument::new("valid", true),
660            ],
661        };
662        let mut buffer = Cursor::new(vec![0u8; 1024]);
663        let mut encoder = Encoder::new(&mut buffer, EncoderOpts::default());
664        encoder.write_record(record).unwrap();
665
666        let len = buffer.position() as usize;
667        let mut bytes = buffer.into_inner();
668        bytes.truncate(len);
669
670        let res = parser.parse_next(&bytes, &formatter).unwrap();
671        assert!(res.0.is_some());
672        let log_message = res.0.unwrap();
673        assert_eq!(&*log_message.message, "[src/file.rs(42)] Another message temp=30.5 valid=true");
674    }
675
676    #[fuchsia::test]
677    fn test_only_kvps() {
678        let mut parser = MessageParser::default();
679        let allocator = Bump::new();
680        let formatter = CPPMessageFormatter(&allocator);
681
682        let record = Record {
683            timestamp: BootInstant::from_nanos(100),
684            severity: 0x10,
685            arguments: vec![Argument::new("status", "ok"), Argument::new("code", 200i64)],
686        };
687        let mut buffer = Cursor::new(vec![0u8; 1024]);
688        let mut encoder = Encoder::new(&mut buffer, EncoderOpts::default());
689        encoder.write_record(record).unwrap();
690
691        let len = buffer.position() as usize;
692        let mut bytes = buffer.into_inner();
693        bytes.truncate(len);
694
695        let res = parser.parse_next(&bytes, &formatter).unwrap();
696        assert!(res.0.is_some());
697        let log_message = res.0.unwrap();
698        assert_eq!(&*log_message.message, "status=\"ok\" code=200");
699    }
700
701    #[fuchsia::test]
702    fn test_cpp_array_and_string_deref_handles_null_and_zero_len() {
703        use std::ops::Deref;
704
705        // Test CppArray deref handles null pointer or zero length
706        let empty_array: CppArray<'_, u32> =
707            CppArray { len: 0, ptr: std::ptr::null(), phantom: PhantomData };
708        assert_eq!(empty_array.deref(), &[] as &[u32]);
709
710        let zero_len_array: CppArray<'_, u32> =
711            CppArray { len: 0, ptr: 0x1234 as *const u32, phantom: PhantomData };
712        assert_eq!(zero_len_array.deref(), &[] as &[u32]);
713
714        let null_ptr_array: CppArray<'_, u32> =
715            CppArray { len: 5, ptr: std::ptr::null(), phantom: PhantomData };
716        assert_eq!(null_ptr_array.deref(), &[] as &[u32]);
717
718        // Test CppString deref handles null pointer or zero length
719        let empty_string = CppString::default();
720        assert_eq!(empty_string.deref(), "");
721
722        let zero_len_string = CppString {
723            inner: CppArray { len: 0, ptr: 0x1234 as *const u8, phantom: PhantomData },
724        };
725        assert_eq!(zero_len_string.deref(), "");
726
727        let null_ptr_string =
728            CppString { inner: CppArray { len: 5, ptr: std::ptr::null(), phantom: PhantomData } };
729        assert_eq!(null_ptr_string.deref(), "");
730    }
731}