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')
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(false),
64 "--sideload_auto_reboot" => handler.sideload(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 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
109fn 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
120pub trait RecoveryActionHandler {
122 fn wipe_data(&mut self);
124
125 fn sideload(&mut self, auto_reboot: bool);
127
128 fn prompt_and_wipe_data(&mut self, reason: Option<&str>);
130
131 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 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}