Skip to main content

bootloader_message/
lib.rs

1// Copyright 2026 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 anyhow::{Error, anyhow};
6use bstr::ByteSlice as _;
7use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout};
8
9// See bootable/recovery/bootloader_message for the canonical format.
10#[repr(C)]
11#[derive(Copy, Clone, KnownLayout, FromBytes, IntoBytes, Immutable)]
12pub struct BootloaderMessageRaw {
13    command: [u8; 32],
14    status: [u8; 32],
15    recovery: [u8; 768],
16    _stage: [u8; 32],
17    _reserved: [u8; 1184],
18}
19
20impl Default for BootloaderMessageRaw {
21    fn default() -> Self {
22        Self {
23            command: [0; _],
24            status: [0; _],
25            recovery: [0; _],
26            _stage: [0; _],
27            _reserved: [0; _],
28        }
29    }
30}
31
32/// Processed bootloader message.
33#[derive(Debug, Clone, Default)]
34pub struct BootloaderMessage {
35    command: String,
36    status: String,
37    recovery: String,
38}
39
40impl BootloaderMessage {
41    /// Creates a new bootloader message with the given recovery arguments.
42    pub fn with_args(args: &str) -> Self {
43        Self { recovery: args.into(), ..Default::default() }
44    }
45
46    /// Returns an iterator over all arguments specified in the bootloader message's recovery field.
47    /// Arguments are assumed to use a newline character (`\n`) as a delimiter.
48    pub fn recovery_args(&self) -> impl Iterator<Item = &str> {
49        self.recovery.split('\n')
50    }
51}
52
53impl From<BootloaderMessageRaw> for BootloaderMessage {
54    fn from(raw: BootloaderMessageRaw) -> Self {
55        Self {
56            command: bytes_to_string(&raw.command),
57            status: bytes_to_string(&raw.status),
58            recovery: bytes_to_string(&raw.recovery),
59        }
60    }
61}
62
63impl TryFrom<BootloaderMessage> for BootloaderMessageRaw {
64    type Error = Error;
65
66    fn try_from(message: BootloaderMessage) -> Result<Self, Error> {
67        let mut raw = BootloaderMessageRaw::default();
68
69        let BootloaderMessage { command, status, recovery } = message;
70
71        // Ensure fields will fit before copying them.
72        if command.len() > raw.command.len() {
73            return Err(anyhow!("command field exceeds storage size"));
74        }
75        if status.len() > raw.status.len() {
76            return Err(anyhow!("status field exceeds storage size"));
77        }
78        if recovery.len() > raw.recovery.len() {
79            return Err(anyhow!("recovery arguments exceed storage size"));
80        }
81
82        raw.command[0..command.len()].copy_from_slice(command.as_bytes());
83        raw.status[0..status.len()].copy_from_slice(status.as_bytes());
84        raw.recovery[0..recovery.len()].copy_from_slice(recovery.as_bytes());
85
86        Ok(raw)
87    }
88}
89
90/// Converts a byte buffer to a Rust [`String`], where `buf` is *possibly* a null-terminated UTF-8
91/// string. Invalid UTF-8 characters will be emitted as "�" (U+FFFD). Bytes after the first null
92/// character, if present, will be ignored, otherwise all of `buf` is used.
93fn bytes_to_string(buf: &[u8]) -> String {
94    if let Some((contents, _)) = buf.split_once_str(&[0u8]) {
95        contents.as_bstr().to_string()
96    } else {
97        buf.as_bstr().to_string()
98    }
99}
100
101#[cfg(test)]
102mod tests {
103    use super::*;
104
105    #[test]
106    fn test_bytes_to_string_with_null() {
107        let buf = b"hello\0world";
108        assert_eq!(bytes_to_string(buf), "hello");
109    }
110
111    #[test]
112    fn test_bytes_to_string_without_null() {
113        let buf = b"hello";
114        assert_eq!(bytes_to_string(buf), "hello");
115    }
116
117    #[test]
118    fn test_bytes_to_string_invalid_utf8() {
119        let buf = b"hello\xffworld";
120        // invalid utf8 is replaced by replacement char
121        assert_eq!(bytes_to_string(buf), "hello\u{FFFD}world");
122    }
123
124    #[test]
125    fn test_with_args() {
126        let msg = BootloaderMessage::with_args("test\nargs");
127        assert_eq!(msg.recovery, "test\nargs");
128        let args: Vec<_> = msg.recovery_args().collect();
129        assert_eq!(args, ["test", "args"]);
130    }
131
132    #[test]
133    fn test_raw_roundtrip() {
134        let original = BootloaderMessage {
135            command: "cmd".to_string(),
136            status: "stat".to_string(),
137            recovery: "rec".to_string(),
138        };
139
140        let raw: BootloaderMessageRaw = original.clone().try_into().expect("convert to raw");
141        let converted: BootloaderMessage = raw.into();
142
143        assert_eq!(converted.command, original.command);
144        assert_eq!(converted.status, original.status);
145        assert_eq!(converted.recovery, original.recovery);
146    }
147
148    #[test]
149    fn test_raw_overflow() {
150        let long_string = "a".repeat(1000);
151        let msg = BootloaderMessage { command: long_string, ..Default::default() };
152        let result: Result<BootloaderMessageRaw, _> = msg.try_into();
153        assert!(result.is_err());
154    }
155}