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