bootloader_message/
lib.rs1use anyhow::{Error, anyhow};
6use bstr::ByteSlice as _;
7use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout};
8
9#[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#[derive(Debug, Clone, Default)]
36pub struct BootloaderMessage {
37 command: String,
38 status: String,
39 recovery: String,
40}
41
42impl BootloaderMessage {
43 pub fn with_args(args: &str) -> Self {
45 Self { recovery: args.into(), ..Default::default() }
46 }
47
48 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(false),
65 "--sideload_auto_reboot" => handler.sideload(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 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
110fn 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
121pub trait RecoveryActionHandler {
123 fn wipe_data(&mut self);
125
126 fn sideload(&mut self, auto_reboot: bool);
128
129 fn prompt_and_wipe_data(&mut self, reason: Option<&str>);
131
132 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 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}