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').map(|arg| arg.trim()).filter(|arg| !arg.is_empty())
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                "recovery" => {}
63                "--wipe_data" => handler.wipe_data(),
64                "--sideload" => handler.sideload(/*auto_reboot=*/ false),
65                "--sideload_auto_reboot" => handler.sideload(/*auto_reboot=*/ true),
66                "--prompt_and_wipe_data" => handler.prompt_and_wipe_data(reason),
67                _ => handler.other(arg, reason),
68            }
69        }
70    }
71}
72
73impl From<BootloaderMessageRaw> for BootloaderMessage {
74    fn from(raw: BootloaderMessageRaw) -> Self {
75        Self {
76            command: bytes_to_string(&raw.command),
77            status: bytes_to_string(&raw.status),
78            recovery: bytes_to_string(&raw.recovery),
79        }
80    }
81}
82
83impl TryFrom<BootloaderMessage> for BootloaderMessageRaw {
84    type Error = Error;
85
86    fn try_from(message: BootloaderMessage) -> Result<Self, Error> {
87        let mut raw = BootloaderMessageRaw::default();
88
89        let BootloaderMessage { command, status, recovery } = message;
90
91        // Ensure fields will fit before copying them.
92        if command.len() > raw.command.len() {
93            return Err(anyhow!("command field exceeds storage size"));
94        }
95        if status.len() > raw.status.len() {
96            return Err(anyhow!("status field exceeds storage size"));
97        }
98        if recovery.len() > raw.recovery.len() {
99            return Err(anyhow!("recovery arguments exceed storage size"));
100        }
101
102        raw.command[0..command.len()].copy_from_slice(command.as_bytes());
103        raw.status[0..status.len()].copy_from_slice(status.as_bytes());
104        raw.recovery[0..recovery.len()].copy_from_slice(recovery.as_bytes());
105
106        Ok(raw)
107    }
108}
109
110/// Converts a byte buffer to a Rust [`String`], where `buf` is *possibly* a null-terminated UTF-8
111/// string. Invalid UTF-8 characters will be emitted as "�" (U+FFFD). Bytes after the first null
112/// character, if present, will be ignored, otherwise all of `buf` is used.
113fn bytes_to_string(buf: &[u8]) -> String {
114    if let Some((contents, _)) = buf.split_once_str(&[0u8]) {
115        contents.as_bstr().to_string()
116    } else {
117        buf.as_bstr().to_string()
118    }
119}
120
121/// Handler for actions which are specified in the bootloader recovery message.
122pub trait RecoveryActionHandler {
123    /// Invoked when the "wipe_data" recovery action is encountered.
124    fn wipe_data(&mut self);
125
126    /// Invoked when the "sideload" or "sideload_auto_reboot" recovery action is encountered.
127    fn sideload(&mut self, auto_reboot: bool);
128
129    /// Invoked when the "prompt_and_wipe_data" recovery action is encountered.
130    fn prompt_and_wipe_data(&mut self, reason: Option<&str>);
131
132    /// Invoked when an unknown recovery action is encountered.
133    fn other(&mut self, arg: &str, reason: Option<&str>);
134}
135
136#[cfg(test)]
137mod tests {
138    use super::*;
139
140    #[test]
141    fn test_bytes_to_string_with_null() {
142        let buf = b"hello\0world";
143        assert_eq!(bytes_to_string(buf), "hello");
144    }
145
146    #[test]
147    fn test_bytes_to_string_without_null() {
148        let buf = b"hello";
149        assert_eq!(bytes_to_string(buf), "hello");
150    }
151
152    #[test]
153    fn test_bytes_to_string_invalid_utf8() {
154        let buf = b"hello\xffworld";
155        // invalid utf8 is replaced by replacement char
156        assert_eq!(bytes_to_string(buf), "hello\u{FFFD}world");
157    }
158
159    #[test]
160    fn test_with_args() {
161        let msg = BootloaderMessage::with_args("test\nargs");
162        assert_eq!(msg.recovery, "test\nargs");
163        let args: Vec<_> = msg.recovery_args().collect();
164        assert_eq!(args, ["test", "args"]);
165    }
166
167    #[test]
168    fn test_raw_roundtrip() {
169        let original = BootloaderMessage {
170            command: "cmd".to_string(),
171            status: "stat".to_string(),
172            recovery: "rec".to_string(),
173        };
174
175        let raw: BootloaderMessageRaw = original.clone().try_into().expect("convert to raw");
176        let converted: BootloaderMessage = raw.into();
177
178        assert_eq!(converted.command, original.command);
179        assert_eq!(converted.status, original.status);
180        assert_eq!(converted.recovery, original.recovery);
181    }
182
183    #[test]
184    fn test_raw_overflow() {
185        let long_string = "a".repeat(1000);
186        let msg = BootloaderMessage { command: long_string, ..Default::default() };
187        let result: Result<BootloaderMessageRaw, _> = msg.try_into();
188        assert!(result.is_err());
189    }
190
191    #[derive(Default)]
192    struct MockHandler {
193        wipe_data_called: bool,
194        sideload_called: Vec<bool>,
195        prompt_and_wipe_data_reason: Option<String>,
196        other_calls: Vec<(String, Option<String>)>,
197    }
198
199    impl RecoveryActionHandler for MockHandler {
200        fn wipe_data(&mut self) {
201            self.wipe_data_called = true;
202        }
203        fn sideload(&mut self, auto_reboot: bool) {
204            self.sideload_called.push(auto_reboot);
205        }
206        fn prompt_and_wipe_data(&mut self, reason: Option<&str>) {
207            self.prompt_and_wipe_data_reason = reason.map(String::from);
208        }
209        fn other(&mut self, arg: &str, reason: Option<&str>) {
210            self.other_calls.push((arg.to_string(), reason.map(String::from)));
211        }
212    }
213
214    #[test]
215    fn test_handle_recovery_actions() {
216        let msg = BootloaderMessage::with_args(
217            "recovery\n  \n--wipe_data\n--sideload\n--reason=test_reason\nunknown_arg",
218        );
219        let mut handler = MockHandler::default();
220        msg.handle_recovery_actions(&mut handler);
221
222        assert!(handler.wipe_data_called);
223        assert_eq!(handler.sideload_called, [false]);
224        assert_eq!(
225            handler.other_calls,
226            [("unknown_arg".to_string(), Some("test_reason".to_string()))]
227        );
228    }
229
230    #[test]
231    fn test_empty_recovery_string() {
232        let msg = BootloaderMessage::with_args("");
233        assert_eq!(msg.recovery_args().count(), 0);
234
235        let mut handler = MockHandler::default();
236        msg.handle_recovery_actions(&mut handler);
237
238        assert!(!handler.wipe_data_called);
239        assert!(handler.sideload_called.is_empty());
240        assert!(handler.prompt_and_wipe_data_reason.is_none());
241        assert!(handler.other_calls.is_empty());
242    }
243}