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
32const REASON_PREFIX: &str = "--reason=";
33
34/// Processed bootloader message.
35#[derive(Debug, Clone, Default)]
36pub struct BootloaderMessage {
37    command: String,
38    status: String,
39    recovery: String,
40}
41
42impl BootloaderMessage {
43    /// Creates a new bootloader message with the given recovery arguments.
44    pub fn with_args(args: &str) -> Self {
45        Self { recovery: args.into(), ..Default::default() }
46    }
47
48    /// Returns an iterator over all arguments specified in the bootloader message's recovery field.
49    /// Arguments are assumed to use a newline character (`\n`) as a delimiter.
50    fn recovery_args(&self) -> impl Iterator<Item = &str> {
51        self.recovery.split('\n')
52    }
53
54    fn reason(&self) -> Option<&str> {
55        self.recovery_args().find_map(|arg| arg.strip_prefix(REASON_PREFIX))
56    }
57
58    pub fn handle_recovery_actions(&self, handler: &mut impl RecoveryActionHandler) {
59        let reason = self.reason();
60        for arg in self.recovery_args().filter(|arg| !arg.starts_with(REASON_PREFIX)) {
61            match arg {
62                "--wipe_data" => handler.wipe_data(),
63                "--sideload" => handler.sideload(/*auto_reboot=*/ false),
64                "--sideload_auto_reboot" => handler.sideload(/*auto_reboot=*/ true),
65                "--prompt_and_wipe_data" => handler.prompt_and_wipe_data(reason),
66                _ => handler.other(arg, reason),
67            }
68        }
69    }
70}
71
72impl From<BootloaderMessageRaw> for BootloaderMessage {
73    fn from(raw: BootloaderMessageRaw) -> Self {
74        Self {
75            command: bytes_to_string(&raw.command),
76            status: bytes_to_string(&raw.status),
77            recovery: bytes_to_string(&raw.recovery),
78        }
79    }
80}
81
82impl TryFrom<BootloaderMessage> for BootloaderMessageRaw {
83    type Error = Error;
84
85    fn try_from(message: BootloaderMessage) -> Result<Self, Error> {
86        let mut raw = BootloaderMessageRaw::default();
87
88        let BootloaderMessage { command, status, recovery } = message;
89
90        // Ensure fields will fit before copying them.
91        if command.len() > raw.command.len() {
92            return Err(anyhow!("command field exceeds storage size"));
93        }
94        if status.len() > raw.status.len() {
95            return Err(anyhow!("status field exceeds storage size"));
96        }
97        if recovery.len() > raw.recovery.len() {
98            return Err(anyhow!("recovery arguments exceed storage size"));
99        }
100
101        raw.command[0..command.len()].copy_from_slice(command.as_bytes());
102        raw.status[0..status.len()].copy_from_slice(status.as_bytes());
103        raw.recovery[0..recovery.len()].copy_from_slice(recovery.as_bytes());
104
105        Ok(raw)
106    }
107}
108
109/// Converts a byte buffer to a Rust [`String`], where `buf` is *possibly* a null-terminated UTF-8
110/// string. Invalid UTF-8 characters will be emitted as "�" (U+FFFD). Bytes after the first null
111/// character, if present, will be ignored, otherwise all of `buf` is used.
112fn bytes_to_string(buf: &[u8]) -> String {
113    if let Some((contents, _)) = buf.split_once_str(&[0u8]) {
114        contents.as_bstr().to_string()
115    } else {
116        buf.as_bstr().to_string()
117    }
118}
119
120/// Handler for actions which are specified in the bootloader recovery message.
121pub trait RecoveryActionHandler {
122    /// Invoked when the "wipe_data" recovery action is encountered.
123    fn wipe_data(&mut self);
124
125    /// Invoked when the "sideload" or "sideload_auto_reboot" recovery action is encountered.
126    fn sideload(&mut self, auto_reboot: bool);
127
128    /// Invoked when the "prompt_and_wipe_data" recovery action is encountered.
129    fn prompt_and_wipe_data(&mut self, reason: Option<&str>);
130
131    /// Invoked when an unknown recovery action is encountered.
132    fn other(&mut self, arg: &str, reason: Option<&str>);
133}
134
135#[cfg(test)]
136mod tests {
137    use super::*;
138
139    #[test]
140    fn test_bytes_to_string_with_null() {
141        let buf = b"hello\0world";
142        assert_eq!(bytes_to_string(buf), "hello");
143    }
144
145    #[test]
146    fn test_bytes_to_string_without_null() {
147        let buf = b"hello";
148        assert_eq!(bytes_to_string(buf), "hello");
149    }
150
151    #[test]
152    fn test_bytes_to_string_invalid_utf8() {
153        let buf = b"hello\xffworld";
154        // invalid utf8 is replaced by replacement char
155        assert_eq!(bytes_to_string(buf), "hello\u{FFFD}world");
156    }
157
158    #[test]
159    fn test_with_args() {
160        let msg = BootloaderMessage::with_args("test\nargs");
161        assert_eq!(msg.recovery, "test\nargs");
162        let args: Vec<_> = msg.recovery_args().collect();
163        assert_eq!(args, ["test", "args"]);
164    }
165
166    #[test]
167    fn test_raw_roundtrip() {
168        let original = BootloaderMessage {
169            command: "cmd".to_string(),
170            status: "stat".to_string(),
171            recovery: "rec".to_string(),
172        };
173
174        let raw: BootloaderMessageRaw = original.clone().try_into().expect("convert to raw");
175        let converted: BootloaderMessage = raw.into();
176
177        assert_eq!(converted.command, original.command);
178        assert_eq!(converted.status, original.status);
179        assert_eq!(converted.recovery, original.recovery);
180    }
181
182    #[test]
183    fn test_raw_overflow() {
184        let long_string = "a".repeat(1000);
185        let msg = BootloaderMessage { command: long_string, ..Default::default() };
186        let result: Result<BootloaderMessageRaw, _> = msg.try_into();
187        assert!(result.is_err());
188    }
189}