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::zx::BootInstant;
11use diagnostics_log_encoding::{Argument, Record, Value};
12use flyweights::FlyStr;
13use static_assertions::const_assert;
14use std::marker::PhantomData;
15use std::ops::Deref;
16use std::str;
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) if name != "." => {
278                    BumpaloString::from_str_in(name, allocator).into_bump_str().into()
279                }
280                _ => CppString::default(),
281            },
282            None => CppString::default(),
283        };
284
285        let tags: &[_] = allocator.alloc_slice_fill_iter(self.tags.drain(..));
286        let kvps: &[_] = allocator.alloc_slice_fill_iter(self.kvps.drain(..));
287
288        allocator.alloc(LogMessage {
289            severity: self.severity,
290            dropped: self.dropped,
291            tags: tags.into(),
292            pid: self.pid.unwrap_or(0),
293            tid: self.tid.unwrap_or(0),
294            message,
295            file,
296            line,
297            moniker_tag,
298            kvps: kvps.into(),
299            timestamp: self.timestamp,
300        })
301    }
302}
303
304struct CPPLogMessageBuilderBuilder<'a>(&'a Bump);
305
306impl<'a> CPPLogMessageBuilderBuilder<'a> {
307    fn configure(
308        self,
309        _component_url: Option<FlyStr>,
310        moniker: Option<ExtendedMoniker>,
311        severity: Severity,
312        timestamp: BootInstant,
313    ) -> Result<CPPLogMessageBuilder<'a>, MessageError> {
314        Ok(CPPLogMessageBuilder {
315            severity: severity as u8,
316            tags: BumpaloVec::new_in(self.0),
317            pid: None,
318            tid: None,
319            dropped: 0,
320            file: None,
321            timestamp: timestamp.into_nanos(),
322            line: None,
323            allocator: self.0,
324            kvps: BumpaloVec::new_in(self.0),
325            moniker: moniker.map(|value| bumpalo::format!(in self.0,"{}", value)),
326            message: None,
327        })
328    }
329}
330
331pub fn build_logs_data<'a>(
332    input: &Record<'a>,
333    source: Option<ExtendedMetadata>,
334    allocator: &'a Bump,
335) -> Result<&'a mut LogMessage<'a>, MessageError> {
336    let builder = CPPLogMessageBuilderBuilder(allocator);
337    let (raw_severity, severity) = Severity::parse_exact(input.severity);
338    let (maybe_moniker, maybe_url, _) = source
339        .map(|value| (Some(value.moniker), Some(value.url), Some(value.rolled_out_logs)))
340        .unwrap_or((None, None, None));
341    let mut builder =
342        builder.configure(maybe_url.map(FlyStr::new), None, severity, input.timestamp)?;
343    if let Some(moniker) = maybe_moniker {
344        builder = builder.set_moniker(moniker.as_ref());
345    }
346    if let Some(raw_severity) = raw_severity {
347        builder = builder.set_raw_severity(raw_severity);
348    }
349
350    for argument in input.arguments.iter() {
351        match argument {
352            Argument::Tag(tag) => {
353                builder = builder.add_tag(tag);
354            }
355            Argument::Pid(pid) => {
356                builder = builder.set_pid(pid.raw_koid());
357            }
358            Argument::Tid(tid) => {
359                builder = builder.set_tid(tid.raw_koid());
360            }
361            Argument::Dropped(dropped) => {
362                builder = builder.set_dropped(*dropped);
363            }
364            Argument::File(file) => {
365                builder = builder.set_file(file);
366            }
367            Argument::Line(line) => {
368                builder = builder.set_line(*line);
369            }
370            Argument::Message(msg) => {
371                builder = builder.set_message(msg);
372            }
373            Argument::Other { .. } => builder = builder.add_kvp(argument),
374        }
375    }
376
377    Ok(builder.build())
378}
379
380/// Constructs a `CPPLogsMessage` from the provided bytes, assuming the bytes
381/// are in the format specified as in the [log encoding], and come from
382/// an Archivist LogStream with moniker, URL, and dropped logs output enabled.
383/// [log encoding] https://fuchsia.dev/fuchsia-src/development/logs/encodings
384///
385/// # Lifetime and Borrowing
386/// Strings within the returned `LogMessage` borrow directly from `bytes`.
387/// Therefore, `bytes` must remain valid and unmodified for the lifetime `'a`
388/// of the returned `LogMessage`.
389pub fn ffi_from_extended_record<'a>(
390    bytes: &'a [u8],
391    allocator: &'a Bump,
392) -> Result<(&'a mut LogMessage<'a>, &'a [u8]), MessageError> {
393    let (input, remaining) = diagnostics_log_encoding::parse::parse_record(bytes)?;
394    let (source, new_remaining) = if remaining.len() >= 16 {
395        let moniker_len = u32::from_le_bytes(remaining[0..4].try_into().unwrap()) as usize;
396        let component_url_len = u32::from_le_bytes(remaining[4..8].try_into().unwrap()) as usize;
397        let rolled_out_logs = u64::from_le_bytes(remaining[8..16].try_into().unwrap());
398        let mut offset: usize = 16;
399
400        // NOTE: This addition is safe as all platforms Fuchsia supports are 64-bit,
401        // so usize will never overflow.
402        let moniker_padded_len = (moniker_len + 7) & !7;
403        let component_url_padded_len = (component_url_len + 7) & !7;
404        let moniker_padded_end = offset + moniker_padded_len;
405        let url_padded_end = moniker_padded_end + component_url_padded_len;
406        if url_padded_end > remaining.len() {
407            return Err(MessageError::OutOfBounds);
408        }
409
410        let moniker = str::from_utf8(&remaining[offset..offset + moniker_len])?;
411        offset += moniker_padded_len;
412        let url = str::from_utf8(&remaining[offset..offset + component_url_len])?;
413        offset += component_url_padded_len;
414        (
415            Some(ExtendedMetadata {
416                moniker: ExtendedMoniker::parse_str(moniker)?,
417                url: url.into(),
418                rolled_out_logs,
419            }),
420            &remaining[offset..],
421        )
422    } else {
423        (None, remaining)
424    };
425    let record = build_logs_data(&input, source, allocator)?;
426    Ok((record, new_remaining))
427}
428
429pub struct CPPMessageFormatter<'a>(pub &'a Bump);
430impl<'a> MessageFormatter<'a> for &CPPMessageFormatter<'a> {
431    type Result = &'a mut LogMessage<'a>;
432
433    fn format(
434        &mut self,
435        record: &Record<'a>,
436        metadata: Option<ExtendedMetadata>,
437    ) -> Result<Self::Result, MessageError> {
438        build_logs_data(record, metadata, self.0)
439    }
440}
441
442#[cfg(test)]
443mod test {
444    use super::*;
445    use crate::MessageParser;
446    use bumpalo::Bump;
447    use diagnostics_log_encoding::encode::{Encoder, EncoderOpts};
448    use diagnostics_log_encoding::zx::BootInstant;
449    use diagnostics_log_encoding::{Argument, Header, LOG_CONTROL_BIT, Record};
450    use std::io::Cursor;
451
452    fn overwrite_header_tag(bytes: &mut [u8], tag: u32) {
453        if bytes.len() >= 8 {
454            let mut header = Header(u64::from_le_bytes(bytes[0..8].try_into().unwrap()));
455            header.set_tag(tag);
456            bytes[0..8].copy_from_slice(&header.0.to_le_bytes());
457        }
458    }
459
460    #[fuchsia::test]
461    fn test_short_read() {
462        let mut parser = MessageParser::default();
463        let allocator = Bump::new();
464        let formatter = CPPMessageFormatter(&allocator);
465        let bytes = vec![0u8; 7];
466        let res = parser.parse_next(&bytes, &formatter);
467        assert!(matches!(res, Err(MessageError::ShortRead { len: 7 })));
468    }
469
470    #[fuchsia::test]
471    fn test_normal_parsing() {
472        let mut parser = MessageParser::default();
473        let allocator = Bump::new();
474        let formatter = CPPMessageFormatter(&allocator);
475
476        let record = Record {
477            timestamp: BootInstant::from_nanos(72),
478            severity: 0x30,
479            arguments: vec![Argument::message("hello world")],
480        };
481        let mut buffer = Cursor::new(vec![0u8; 1024]);
482        let mut encoder = Encoder::new(&mut buffer, EncoderOpts::default());
483        encoder.write_record(record).unwrap();
484
485        let len = buffer.position() as usize;
486        let mut bytes = buffer.into_inner();
487        bytes.truncate(len);
488
489        let res = parser.parse_next(&bytes, &formatter).unwrap();
490        assert!(res.0.is_some());
491        let log_message = &res.0.unwrap();
492        assert_eq!(&*log_message.message, "hello world");
493        assert_eq!(log_message.timestamp, 72);
494        assert_eq!(log_message.severity, 0x30);
495    }
496
497    #[fuchsia::test]
498    fn test_escaping_in_kvp() {
499        let mut parser = MessageParser::default();
500        let allocator = Bump::new();
501        let formatter = CPPMessageFormatter(&allocator);
502
503        let record = Record {
504            timestamp: BootInstant::from_nanos(72),
505            severity: 0x30,
506            arguments: vec![
507                Argument::message("hello world"),
508                Argument::new("key", r#"val"with\escapes"#),
509            ],
510        };
511        let mut buffer = Cursor::new(vec![0u8; 1024]);
512        let mut encoder = Encoder::new(&mut buffer, EncoderOpts::default());
513        encoder.write_record(record).unwrap();
514
515        let len = buffer.position() as usize;
516        let mut bytes = buffer.into_inner();
517        bytes.truncate(len);
518
519        let res = parser.parse_next(&bytes, &formatter).unwrap();
520        assert!(res.0.is_some());
521        let log_message = &res.0.unwrap();
522        assert_eq!(&*log_message.message, "hello world");
523        assert_eq!(log_message.kvps.len, 1);
524        assert_eq!(&*log_message.kvps[0].key, "key");
525        assert!(matches!(
526            &log_message.kvps[0].value,
527            CppValue::Text(val) if &**val == r#"val"with\escapes"#
528        ));
529    }
530
531    #[fuchsia::test]
532    fn test_out_of_bounds_extended_record() {
533        let allocator = Bump::new();
534
535        let record = Record {
536            timestamp: BootInstant::from_nanos(72),
537            severity: 0x30,
538            arguments: vec![Argument::message("hello world")],
539        };
540        let mut buffer = Cursor::new(vec![0u8; 1024]);
541        let mut encoder = Encoder::new(&mut buffer, EncoderOpts::default());
542        encoder.write_record(record).unwrap();
543        let len = buffer.position() as usize;
544        let mut bytes = buffer.into_inner();
545        bytes.truncate(len);
546
547        // Append a corrupt moniker_len or component_url_len in the remaining slice.
548        let extended_metadata_suffix = [
549            0xE8, 0x03, 0x00, 0x00, // moniker_len = 1000
550            0xE8, 0x03, 0x00, 0x00, // component_url_len = 1000
551            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // rolled_out_logs = 0
552        ];
553        bytes.extend_from_slice(&extended_metadata_suffix);
554
555        let res = ffi_from_extended_record(&bytes, &allocator);
556        assert!(matches!(res, Err(MessageError::OutOfBounds)));
557    }
558
559    #[fuchsia::test]
560    fn test_control_message_tags() {
561        let allocator = Bump::new();
562        let formatter = CPPMessageFormatter(&allocator);
563        let mut parser = MessageParser::default();
564
565        let tag_id = 0;
566
567        let control_record = Record {
568            timestamp: BootInstant::from_nanos(72),
569            severity: 0x30,
570            arguments: vec![
571                Argument::new("moniker", "test/moniker"),
572                Argument::new("url", "fuchsia-pkg://test"),
573            ],
574        };
575
576        let mut buffer = Cursor::new(vec![0u8; 1024]);
577        let mut encoder = Encoder::new(&mut buffer, EncoderOpts::default());
578        encoder.write_record(control_record).unwrap();
579
580        let len = buffer.position() as usize;
581        let mut bytes = buffer.into_inner();
582        bytes.truncate(len);
583
584        overwrite_header_tag(&mut bytes, LOG_CONTROL_BIT);
585
586        let (log, _) = parser.parse_next(&bytes, &formatter).unwrap();
587        assert!(log.is_none());
588
589        let tag_data = parser.tag_map.get(&tag_id).unwrap();
590        assert_eq!(tag_data.moniker, ExtendedMoniker::parse_str("test/moniker").unwrap());
591        assert_eq!(tag_data.url, "fuchsia-pkg://test");
592
593        let rolled_out_record = Record {
594            timestamp: BootInstant::from_nanos(73),
595            severity: 0x30,
596            arguments: vec![Argument::new("rolled_out", 5u64)],
597        };
598
599        let mut buffer2 = Cursor::new(vec![0u8; 1024]);
600        let mut encoder2 = Encoder::new(&mut buffer2, EncoderOpts::default());
601        encoder2.write_record(rolled_out_record).unwrap();
602
603        let len2 = buffer2.position() as usize;
604        let mut bytes2 = buffer2.into_inner();
605        bytes2.truncate(len2);
606
607        overwrite_header_tag(&mut bytes2, LOG_CONTROL_BIT);
608
609        let (log2, _) = parser.parse_next(&bytes2, &formatter).unwrap();
610        assert!(log2.is_some());
611
612        let log2_msg = log2.unwrap();
613        assert_eq!(&*log2_msg.message, "");
614        assert_eq!(log2_msg.kvps.len, 1);
615        assert_eq!(&*log2_msg.kvps[0].key, "rolled_out");
616        assert!(matches!(log2_msg.kvps[0].value, CppValue::UnsignedInt(5)));
617        let tag_data2 = parser.tag_map.get(&tag_id).unwrap();
618        assert_eq!(tag_data2.moniker, ExtendedMoniker::parse_str("test/moniker").unwrap());
619
620        let normal_record = Record {
621            timestamp: BootInstant::from_nanos(74),
622            severity: 0x30,
623            arguments: vec![Argument::message("some log with tag")],
624        };
625
626        let mut buffer3 = Cursor::new(vec![0u8; 1024]);
627        let mut encoder3 = Encoder::new(&mut buffer3, EncoderOpts::default());
628        encoder3.write_record(normal_record).unwrap();
629
630        let len3 = buffer3.position() as usize;
631        let mut bytes3 = buffer3.into_inner();
632        bytes3.truncate(len3);
633
634        overwrite_header_tag(&mut bytes3, tag_id);
635
636        let (log3, _) = parser.parse_next(&bytes3, &formatter).unwrap();
637
638        let log_msg3 = &log3.unwrap();
639        assert_eq!(&*log_msg3.message, "some log with tag");
640        assert_eq!(log_msg3.tags.len(), 0);
641        assert_eq!(&*log_msg3.moniker_tag, "moniker");
642    }
643
644    #[fuchsia::test]
645    fn test_message_with_kvps() {
646        let mut parser = MessageParser::default();
647        let allocator = Bump::new();
648        let formatter = CPPMessageFormatter(&allocator);
649
650        let record = Record {
651            timestamp: BootInstant::from_nanos(100),
652            severity: 0x10,
653            arguments: vec![
654                Argument::message("A message"),
655                Argument::new("key1", "value1"),
656                Argument::new("key2", 123u64),
657            ],
658        };
659        let mut buffer = Cursor::new(vec![0u8; 1024]);
660        let mut encoder = Encoder::new(&mut buffer, EncoderOpts::default());
661        encoder.write_record(record).unwrap();
662
663        let len = buffer.position() as usize;
664        let mut bytes = buffer.into_inner();
665        bytes.truncate(len);
666
667        let res = parser.parse_next(&bytes, &formatter).unwrap();
668        assert!(res.0.is_some());
669        let log_message = res.0.unwrap();
670        assert_eq!(&*log_message.message, "A message");
671        assert_eq!(log_message.kvps.len, 2);
672        assert_eq!(&*log_message.kvps[0].key, "key1");
673        assert!(matches!(
674            &log_message.kvps[0].value,
675            CppValue::Text(val) if &**val == "value1"
676        ));
677        assert_eq!(&*log_message.kvps[1].key, "key2");
678        assert!(matches!(&log_message.kvps[1].value, CppValue::UnsignedInt(123)));
679    }
680
681    #[fuchsia::test]
682    fn test_file_line_message_with_kvps() {
683        let mut parser = MessageParser::default();
684        let allocator = Bump::new();
685        let formatter = CPPMessageFormatter(&allocator);
686
687        let record = Record {
688            timestamp: BootInstant::from_nanos(100),
689            severity: 0x10,
690            arguments: vec![
691                Argument::file("src/file.rs"),
692                Argument::line(42),
693                Argument::message("Another message"),
694                Argument::new("temp", 30.5),
695                Argument::new("valid", true),
696            ],
697        };
698        let mut buffer = Cursor::new(vec![0u8; 1024]);
699        let mut encoder = Encoder::new(&mut buffer, EncoderOpts::default());
700        encoder.write_record(record).unwrap();
701
702        let len = buffer.position() as usize;
703        let mut bytes = buffer.into_inner();
704        bytes.truncate(len);
705
706        let res = parser.parse_next(&bytes, &formatter).unwrap();
707        assert!(res.0.is_some());
708        let log_message = res.0.unwrap();
709        assert_eq!(&*log_message.file, "src/file.rs");
710        assert_eq!(log_message.line, 42);
711        assert_eq!(&*log_message.message, "Another message");
712        assert_eq!(log_message.kvps.len, 2);
713        assert_eq!(&*log_message.kvps[0].key, "temp");
714        assert!(matches!(
715            &log_message.kvps[0].value,
716            CppValue::Floating(val) if *val == 30.5
717        ));
718        assert_eq!(&*log_message.kvps[1].key, "valid");
719        assert!(matches!(&log_message.kvps[1].value, CppValue::Boolean(true)));
720    }
721
722    #[fuchsia::test]
723    fn test_only_kvps() {
724        let mut parser = MessageParser::default();
725        let allocator = Bump::new();
726        let formatter = CPPMessageFormatter(&allocator);
727
728        let record = Record {
729            timestamp: BootInstant::from_nanos(100),
730            severity: 0x10,
731            arguments: vec![Argument::new("status", "ok"), Argument::new("code", 200i64)],
732        };
733        let mut buffer = Cursor::new(vec![0u8; 1024]);
734        let mut encoder = Encoder::new(&mut buffer, EncoderOpts::default());
735        encoder.write_record(record).unwrap();
736
737        let len = buffer.position() as usize;
738        let mut bytes = buffer.into_inner();
739        bytes.truncate(len);
740
741        let res = parser.parse_next(&bytes, &formatter).unwrap();
742        assert!(res.0.is_some());
743        let log_message = res.0.unwrap();
744        assert_eq!(&*log_message.message, "");
745        assert_eq!(log_message.kvps.len, 2);
746        assert_eq!(&*log_message.kvps[0].key, "status");
747        assert!(matches!(
748            &log_message.kvps[0].value,
749            CppValue::Text(val) if &**val == "ok"
750        ));
751        assert_eq!(&*log_message.kvps[1].key, "code");
752        assert!(matches!(&log_message.kvps[1].value, CppValue::SignedInt(200)));
753    }
754
755    #[fuchsia::test]
756    fn test_zero_copy_string_borrowing() {
757        let mut parser = MessageParser::default();
758        let allocator = Bump::new();
759        let formatter = CPPMessageFormatter(&allocator);
760
761        let record = Record {
762            timestamp: BootInstant::from_nanos(100),
763            severity: 0x30,
764            arguments: vec![
765                Argument::File("src/lib.rs".into()),
766                Argument::Line(10),
767                Argument::Message("hello zero-copy".into()),
768            ],
769        };
770        let mut buffer = Cursor::new(vec![0u8; 1024]);
771        let mut encoder = Encoder::new(&mut buffer, EncoderOpts::default());
772        encoder.write_record(record).unwrap();
773
774        let len = buffer.position() as usize;
775        let mut bytes = buffer.into_inner();
776        bytes.truncate(len);
777
778        let (log_message, _) = parser.parse_next(&bytes, &formatter).unwrap();
779        let log_message = log_message.unwrap();
780
781        let bytes_start = bytes.as_ptr() as usize;
782        let bytes_end = bytes_start + bytes.len();
783
784        let msg_ptr = log_message.message.inner.ptr as usize;
785        assert!(
786            bytes_start <= msg_ptr && msg_ptr < bytes_end,
787            "message pointer must borrow directly from incoming record buffer"
788        );
789
790        let file_ptr = log_message.file.inner.ptr as usize;
791        assert!(
792            bytes_start <= file_ptr && file_ptr < bytes_end,
793            "file pointer must borrow directly from incoming record buffer"
794        );
795    }
796
797    #[fuchsia::test]
798    fn test_moniker_tag() {
799        let allocator = Bump::new();
800        let builder = CPPLogMessageBuilder {
801            severity: 0x30,
802            tags: BumpaloVec::new_in(&allocator),
803            pid: None,
804            tid: None,
805            dropped: 0,
806            file: None,
807            line: None,
808            moniker: Some(BumpaloString::from_str_in("core/foo", &allocator)),
809            message: None,
810            timestamp: 0,
811            kvps: BumpaloVec::new_in(&allocator),
812            allocator: &allocator,
813        };
814        let msg = builder.build();
815        assert_eq!(&*msg.moniker_tag, "foo");
816
817        let root_builder = CPPLogMessageBuilder {
818            severity: 0x30,
819            tags: BumpaloVec::new_in(&allocator),
820            pid: None,
821            tid: None,
822            dropped: 0,
823            file: None,
824            line: None,
825            moniker: Some(BumpaloString::from_str_in(".", &allocator)),
826            message: None,
827            timestamp: 0,
828            kvps: BumpaloVec::new_in(&allocator),
829            allocator: &allocator,
830        };
831        let root_msg = root_builder.build();
832        assert_eq!(&*root_msg.moniker_tag, "");
833    }
834
835    #[fuchsia::test]
836    fn test_cpp_array_and_string_deref_handles_null_and_zero_len() {
837        use std::ops::Deref;
838
839        // Test CppArray deref handles null pointer or zero length
840        let empty_array: CppArray<'_, u32> =
841            CppArray { len: 0, ptr: std::ptr::null(), phantom: PhantomData };
842        assert_eq!(empty_array.deref(), &[] as &[u32]);
843
844        let zero_len_array: CppArray<'_, u32> =
845            CppArray { len: 0, ptr: 0x1234 as *const u32, phantom: PhantomData };
846        assert_eq!(zero_len_array.deref(), &[] as &[u32]);
847
848        let null_ptr_array: CppArray<'_, u32> =
849            CppArray { len: 5, ptr: std::ptr::null(), phantom: PhantomData };
850        assert_eq!(null_ptr_array.deref(), &[] as &[u32]);
851
852        // Test CppString deref handles null pointer or zero length
853        let empty_string = CppString::default();
854        assert_eq!(empty_string.deref(), "");
855
856        let zero_len_string = CppString {
857            inner: CppArray { len: 0, ptr: 0x1234 as *const u8, phantom: PhantomData },
858        };
859        assert_eq!(zero_len_string.deref(), "");
860
861        let null_ptr_string =
862            CppString { inner: CppArray { len: 5, ptr: std::ptr::null(), phantom: PhantomData } };
863        assert_eq!(null_ptr_string.deref(), "");
864    }
865}