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