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::MonikerWithUrl;
7use bumpalo::collections::{String as BumpaloString, Vec as BumpaloVec};
8use bumpalo::Bump;
9use diagnostics_data::{ExtendedMoniker, Severity};
10use diagnostics_log_encoding::{Argument, Record, Value};
11use flyweights::FlyStr;
12use std::str;
13use zx::BootInstant;
14
15pub use crate::constants::*;
16
17struct ArchivistArguments<'a> {
18    builder: CPPLogMessageBuilder<'a>,
19    archivist_argument_count: usize,
20    record: Record<'a>,
21}
22
23#[cfg(fuchsia_api_level_less_than = "HEAD")]
24fn parse_archivist_args<'a>(
25    builder: CPPLogMessageBuilder<'a>,
26    input: Record<'a>,
27) -> Result<ArchivistArguments<'a>, MessageError> {
28    Ok(ArchivistArguments { builder, archivist_argument_count: 0, record: input })
29}
30
31#[cfg(fuchsia_api_level_at_least = "HEAD")]
32fn parse_archivist_args<'a>(
33    mut builder: CPPLogMessageBuilder<'a>,
34    input: Record<'a>,
35) -> Result<ArchivistArguments<'a>, MessageError> {
36    let mut has_moniker = false;
37    let mut archivist_argument_count = 0;
38    for argument in input.arguments.iter().rev() {
39        // If Archivist records are expected, they should always be at the end.
40        // If no Archivist records are expected, treat them as regular key-value-pairs.
41        match argument {
42            Argument::Other { value, name } => {
43                if name == fidl_fuchsia_diagnostics::MONIKER_ARG_NAME {
44                    if let Value::Text(moniker) = value {
45                        builder = builder.set_moniker(ExtendedMoniker::parse_str(moniker)?);
46                        archivist_argument_count += 1;
47                        has_moniker = true;
48                        continue;
49                    }
50                }
51                if name == fidl_fuchsia_diagnostics::COMPONENT_URL_ARG_NAME {
52                    if let Value::Text(_) = value {
53                        archivist_argument_count += 1;
54                        continue;
55                    }
56                }
57                if name == fidl_fuchsia_diagnostics::ROLLED_OUT_ARG_NAME {
58                    if let Value::UnsignedInt(_) = value {
59                        archivist_argument_count += 1;
60                        continue;
61                    }
62                }
63                break;
64            }
65            _ => break,
66        }
67    }
68    if !has_moniker {
69        return Err(MessageError::MissingMoniker);
70    }
71    Ok(ArchivistArguments { builder, archivist_argument_count, record: input })
72}
73
74/// Array for FFI purposes between C++ and Rust.
75/// If len is 0, ptr is allowed to be nullptr,
76/// otherwise, ptr must be valid.
77#[repr(C)]
78pub struct CPPArray<T> {
79    /// Number of elements in the array
80    pub len: usize,
81    /// Pointer to the first element in the array,
82    /// may be null in the case of a 0 length array,
83    /// but is not guaranteed to always be null of
84    /// len is 0.
85    pub ptr: *const T,
86}
87
88impl CPPArray<u8> {
89    /// # Safety
90    ///
91    /// input must refer to a valid string, sized according to len.
92    /// A valid string consists of UTF-8 characters. The caller
93    /// is responsible for ensuring the byte sequence consists of valid UTF-8
94    /// characters.
95    ///
96    pub unsafe fn as_utf8_str(&self) -> &str {
97        std::str::from_utf8_unchecked(std::slice::from_raw_parts(self.ptr, self.len))
98    }
99}
100
101impl From<&str> for CPPArray<u8> {
102    fn from(value: &str) -> Self {
103        CPPArray { len: value.len(), ptr: value.as_ptr() }
104    }
105}
106
107impl From<Option<&str>> for CPPArray<u8> {
108    fn from(value: Option<&str>) -> Self {
109        if let Some(value) = value {
110            CPPArray { len: value.len(), ptr: value.as_ptr() }
111        } else {
112            CPPArray { len: 0, ptr: std::ptr::null() }
113        }
114    }
115}
116
117impl<T> From<&Vec<T>> for CPPArray<T> {
118    fn from(value: &Vec<T>) -> Self {
119        CPPArray { len: value.len(), ptr: value.as_ptr() }
120    }
121}
122
123/// Log message representation for FFI with C++
124#[repr(C)]
125pub struct LogMessage<'a> {
126    /// Severity of a log message.
127    severity: u8,
128    /// Tags in a log message, guaranteed to be non-null.
129    tags: CPPArray<CPPArray<u8>>,
130    /// Process ID from a LogMessage, or 0 if unknown
131    pid: u64,
132    /// Thread ID from a LogMessage, or 0 if unknown
133    tid: u64,
134    /// Number of dropped log messages.
135    dropped: u64,
136    /// The UTF-encoded log message, guaranteed to be valid UTF-8.
137    message: CPPArray<u8>,
138    /// Timestamp on the boot timeline of the log message,
139    /// in nanoseconds.
140    timestamp: i64,
141    /// Pointer to the builder is owned by this CPPLogMessage.
142    /// Dropping this CPPLogMessage will free the builder.
143    builder: *mut CPPLogMessageBuilder<'a>,
144}
145
146impl Drop for LogMessage<'_> {
147    fn drop(&mut self) {
148        unsafe {
149            // SAFETY: Rust guarantees destructors only run once in sound code.
150            // Initializing the CPPLogMessage requires the builder to be set
151            // to a valid pointer, so it is safe to drop the CPPLogMessageBuilder
152            // in the destructor to free resources on the Rust side of the FFI boundary.
153            std::ptr::drop_in_place(self.builder);
154        }
155    }
156}
157
158pub struct CPPLogMessageBuilder<'a> {
159    severity: u8,
160    tags: BumpaloVec<'a, BumpaloString<'a>>,
161    pid: Option<u64>,
162    tid: Option<u64>,
163    dropped: u64,
164    file: Option<String>,
165    line: Option<u64>,
166    moniker: Option<BumpaloString<'a>>,
167    message: Option<String>,
168    timestamp: i64,
169    kvps: BumpaloVec<'a, Argument<'a>>,
170    allocator: &'a Bump,
171}
172
173// Escape quotes in a string per the Feedback format
174fn escape_quotes(input: &str, output: &mut BumpaloString<'_>) {
175    for ch in input.chars() {
176        if ch == '\"' {
177            output.push('\\');
178        }
179        output.push(ch);
180    }
181}
182
183impl<'a> CPPLogMessageBuilder<'a> {
184    fn convert_string_vec(&self, strings: &[BumpaloString<'_>]) -> CPPArray<CPPArray<u8>> {
185        CPPArray {
186            len: strings.len(),
187            ptr: self
188                .allocator
189                .alloc_slice_fill_iter(strings.iter().map(|value| value.as_str().into()))
190                .as_ptr(),
191        }
192    }
193
194    fn set_raw_severity(mut self, raw_severity: u8) -> Self {
195        self.severity = raw_severity;
196        self
197    }
198
199    fn add_tag(mut self, tag: impl Into<String>) -> Self {
200        self.tags.push(BumpaloString::from_str_in(&tag.into(), self.allocator));
201        self
202    }
203
204    fn set_pid(mut self, pid: u64) -> Self {
205        self.pid = Some(pid);
206        self
207    }
208
209    fn set_tid(mut self, tid: u64) -> Self {
210        self.tid = Some(tid);
211        self
212    }
213
214    fn set_dropped(mut self, dropped: u64) -> Self {
215        self.dropped = dropped;
216        self
217    }
218
219    fn set_file(mut self, file: impl Into<String>) -> Self {
220        self.file = Some(file.into());
221        self
222    }
223
224    fn set_line(mut self, line: u64) -> Self {
225        self.line = Some(line);
226        self
227    }
228
229    fn set_message(mut self, msg: impl Into<String>) -> Self {
230        self.message = Some(msg.into());
231        self
232    }
233
234    fn add_kvp(mut self, kvp: Argument<'a>) -> Self {
235        self.kvps.push(kvp);
236        self
237    }
238    #[cfg(fuchsia_api_level_at_least = "HEAD")]
239    fn set_moniker(mut self, value: ExtendedMoniker) -> Self {
240        self.moniker = Some(bumpalo::format!(in self.allocator,"{}", value));
241        self
242    }
243    fn build(self) -> &'a mut LogMessage<'a> {
244        let allocator = self.allocator;
245        let builder = allocator.alloc(self);
246
247        // Format the message in accordance with the Feedback format
248        let msg_str = builder
249            .message
250            .as_ref()
251            .map(|value| bumpalo::format!(in &allocator,"{value}",))
252            .unwrap_or_else(|| BumpaloString::new_in(allocator));
253
254        let mut kvp_str = BumpaloString::new_in(allocator);
255        for kvp in &builder.kvps {
256            kvp_str = bumpalo::format!(in &allocator, "{kvp_str} {}=", kvp.name());
257            match kvp.value() {
258                Value::Text(value) => {
259                    kvp_str.push('"');
260                    escape_quotes(&value, &mut kvp_str);
261                    kvp_str.push('"');
262                }
263                Value::SignedInt(value) => {
264                    kvp_str.push_str(&bumpalo::format!(in &allocator, "{}",value))
265                }
266                Value::UnsignedInt(value) => {
267                    kvp_str.push_str(&bumpalo::format!(in &allocator, "{}",value))
268                }
269                Value::Floating(value) => {
270                    kvp_str.push_str(&bumpalo::format!(in &allocator, "{}",value))
271                }
272                Value::Boolean(value) => {
273                    if value {
274                        kvp_str.push_str("true");
275                    } else {
276                        kvp_str.push_str("false");
277                    }
278                }
279            }
280        }
281
282        let mut output = match (&builder.file, &builder.line) {
283            (Some(file), Some(line)) => {
284                let mut value = bumpalo::format!(in &allocator, "[{file}({line})]",);
285                if !msg_str.is_empty() {
286                    value.push(' ');
287                }
288                value
289            }
290            _ => BumpaloString::new_in(allocator),
291        };
292
293        output.push_str(&msg_str);
294        output.push_str(&kvp_str);
295
296        if let Some(moniker) = &builder.moniker {
297            let component_name = moniker.split("/").last();
298            if let Some(component_name) = component_name {
299                if !builder.tags.iter().any(|value| value.as_str() == component_name) {
300                    builder.tags.insert(0, bumpalo::format!(in &allocator, "{}", component_name));
301                }
302            }
303        }
304
305        let log_message = LogMessage {
306            builder,
307            severity: builder.severity,
308            dropped: builder.dropped,
309            tags: builder.convert_string_vec(&builder.tags),
310            pid: builder.pid.unwrap_or(0),
311            tid: builder.tid.unwrap_or(0),
312            message: output.as_str().into(),
313            timestamp: builder.timestamp,
314        };
315
316        allocator.alloc(log_message)
317    }
318}
319
320struct CPPLogMessageBuilderBuilder<'a>(&'a Bump);
321
322impl<'a> CPPLogMessageBuilderBuilder<'a> {
323    fn configure(
324        self,
325        _component_url: Option<FlyStr>,
326        moniker: Option<ExtendedMoniker>,
327        severity: Severity,
328        timestamp: BootInstant,
329    ) -> Result<CPPLogMessageBuilder<'a>, MessageError> {
330        Ok(CPPLogMessageBuilder {
331            severity: severity as u8,
332            tags: BumpaloVec::new_in(self.0),
333            pid: None,
334            tid: None,
335            dropped: 0,
336            file: None,
337            timestamp: timestamp.into_nanos(),
338            line: None,
339            allocator: self.0,
340            kvps: BumpaloVec::new_in(self.0),
341            moniker: moniker.map(|value| bumpalo::format!(in self.0,"{}", value)),
342            message: None,
343        })
344    }
345}
346
347fn build_logs_data<'a>(
348    input: Record<'a>,
349    source: Option<MonikerWithUrl>,
350    allocator: &'a Bump,
351    expect_extended_attribution: bool,
352) -> Result<CPPLogMessageBuilder<'a>, MessageError> {
353    let builder = CPPLogMessageBuilderBuilder(allocator);
354    let (raw_severity, severity) = Severity::parse_exact(input.severity);
355    let (maybe_moniker, maybe_url) =
356        source.map(|value| (Some(value.moniker), Some(value.url))).unwrap_or((None, None));
357    let mut builder = builder.configure(maybe_url, maybe_moniker, severity, input.timestamp)?;
358    if let Some(raw_severity) = raw_severity {
359        builder = builder.set_raw_severity(raw_severity);
360    }
361    let (archivist_argument_count, input) = if !expect_extended_attribution {
362        (0, input)
363    } else {
364        let arguments = parse_archivist_args(builder, input)?;
365        builder = arguments.builder;
366        (arguments.archivist_argument_count, arguments.record)
367    };
368    let input_argument_len = input.arguments.len();
369    for argument in input.arguments.into_iter().take(input_argument_len - archivist_argument_count)
370    {
371        match argument {
372            Argument::Tag(tag) => {
373                builder = builder.add_tag(tag.as_ref());
374            }
375            Argument::Pid(pid) => {
376                builder = builder.set_pid(pid.raw_koid());
377            }
378            Argument::Tid(tid) => {
379                builder = builder.set_tid(tid.raw_koid());
380            }
381            Argument::Dropped(dropped) => {
382                builder = builder.set_dropped(dropped);
383            }
384            Argument::File(file) => {
385                builder = builder.set_file(file.as_ref());
386            }
387            Argument::Line(line) => {
388                builder = builder.set_line(line);
389            }
390            Argument::Message(msg) => {
391                builder = builder.set_message(msg.as_ref());
392            }
393            Argument::Other { value: _, name: _ } => builder = builder.add_kvp(argument),
394        }
395    }
396
397    Ok(builder)
398}
399
400/// Constructs a `CPPLogsMessage` from the provided bytes, assuming the bytes
401/// are in the format specified as in the [log encoding], and come from
402///
403/// an Archivist LogStream with moniker, URL, and dropped logs output enabled.
404/// [log encoding] https://fuchsia.dev/fuchsia-src/development/logs/encodings
405pub fn ffi_from_extended_record<'a>(
406    bytes: &'a [u8],
407    allocator: &'a Bump,
408    source: Option<MonikerWithUrl>,
409    expect_extended_attribution: bool,
410) -> Result<(&'a mut LogMessage<'a>, &'a [u8]), MessageError> {
411    let (input, remaining) = diagnostics_log_encoding::parse::parse_record(bytes)?;
412    let record = build_logs_data(input, source, allocator, expect_extended_attribution)?.build();
413    Ok((record, remaining))
414}