1pub mod bootfs;
6
7use bootfs::{
8 ZBI_BOOTFS_MAGIC, ZBI_BOOTFS_MAX_NAME_LEN, ZBI_BOOTFS_PAGE_SIZE, zbi_bootfs_dirent_t,
9 zbi_bootfs_header_t,
10};
11use byteorder::{ByteOrder, LittleEndian};
12
13use std::ffi::CStr;
14use std::mem::size_of;
15use std::str::Utf8Error;
16use thiserror::Error;
17use zerocopy::{Ref, SplitByteSlice};
18
19const ZBI_BOOTFS_DIRENT_SIZE: usize = size_of::<zbi_bootfs_dirent_t>();
20const ZBI_BOOTFS_HEADER_SIZE: usize = size_of::<zbi_bootfs_header_t>();
21
22fn zbi_bootfs_dirent_size(name_len: u32) -> u32 {
25 (ZBI_BOOTFS_DIRENT_SIZE as u32 + name_len + 3) & !3u32
26}
27
28pub fn zbi_bootfs_page_align(size: u32) -> u32 {
29 size.wrapping_add(ZBI_BOOTFS_PAGE_SIZE - 1) & !(ZBI_BOOTFS_PAGE_SIZE - 1)
30}
31
32pub fn zbi_bootfs_is_aligned(size: u32) -> bool {
33 size % ZBI_BOOTFS_PAGE_SIZE == 0
34}
35
36#[derive(Debug, Error, Eq, PartialEq)]
37pub enum BootfsParserError {
38 #[error("Invalid magic for bootfs payload")]
39 BadMagic,
40
41 #[error("Directory entry {} exceeds available dirsize of {}", entry_index, dirsize)]
42 DirEntryTooBig { entry_index: u32, dirsize: u32 },
43
44 #[error("Failed to read payload: {}", status)]
45 FailedToReadPayload { status: zx::Status },
46
47 #[error("Failed to parse bootfs header")]
48 FailedToParseHeader,
49
50 #[error("Failed to parse directory entry")]
51 FailedToParseDirEntry,
52
53 #[error("Failed to read name as UTF-8: {}", cause)]
54 InvalidNameFormat {
55 #[source]
56 cause: Utf8Error,
57 },
58
59 #[error("Failed to find null terminated string for name: {}", cause)]
60 InvalidNameString {
61 #[source]
62 cause: std::ffi::FromBytesWithNulError,
63 },
64
65 #[error(
66 "name_len must be between 1 and {}, found {} for directory entry {}",
67 max_name_len,
68 name_len,
69 entry_index
70 )]
71 InvalidNameLength { name_len: u32, max_name_len: u32, entry_index: u32 },
72}
73
74#[derive(Debug)]
75struct ZbiBootfsDirent<B: SplitByteSlice> {
76 header: Ref<B, zbi_bootfs_dirent_t>,
77 name_bytes: B,
78}
79impl<B: SplitByteSlice> ZbiBootfsDirent<B> {
80 pub fn parse(bytes: B) -> Result<ZbiBootfsDirent<B>, BootfsParserError> {
81 let (header, name_bytes) = Ref::<B, zbi_bootfs_dirent_t>::from_prefix(bytes)
82 .map_err(Into::into)
83 .map_err(|_: zerocopy::SizeError<_, _>| BootfsParserError::FailedToParseDirEntry)?;
84
85 Ok(ZbiBootfsDirent { header, name_bytes })
86 }
87
88 pub fn data_len(&self) -> u32 {
89 return self.header.data_len.get();
90 }
91
92 pub fn data_off(&self) -> u32 {
93 return self.header.data_off.get();
94 }
95
96 pub fn name(&self) -> Result<&str, BootfsParserError> {
97 match CStr::from_bytes_with_nul(&self.name_bytes[..self.header.name_len.get() as usize]) {
102 Ok(bytes) => {
103 bytes.to_str().map_err(|cause| BootfsParserError::InvalidNameFormat { cause })
104 }
105 Err(cause) => Err(BootfsParserError::InvalidNameString { cause }),
106 }
107 }
108}
109
110#[derive(Debug)]
112pub struct BootfsParser {
113 pub(self) dirsize: u32,
115 pub(self) vmo: zx::Vmo,
116}
117impl BootfsParser {
118 pub fn create_from_vmo(vmo: zx::Vmo) -> Result<BootfsParser, BootfsParserError> {
122 let mut header_bytes = [0; ZBI_BOOTFS_HEADER_SIZE];
123 vmo.read(&mut header_bytes, 0)
124 .map_err(|status| BootfsParserError::FailedToReadPayload { status })?;
125
126 let header = Ref::<_, zbi_bootfs_header_t>::from_bytes(&header_bytes[..])
127 .map_err(Into::into)
128 .map_err(|_: zerocopy::SizeError<_, _>| BootfsParserError::FailedToParseHeader)?;
129 if header.magic.get() == ZBI_BOOTFS_MAGIC {
130 Ok(Self { vmo, dirsize: header.dirsize.get() })
131 } else {
132 Err(BootfsParserError::BadMagic)
133 }
134 }
135
136 pub fn iter(&self) -> impl Iterator<Item = Result<BootfsEntry, BootfsParserError>> + '_ {
137 BootfsParserIterator::new(&self, false)
138 }
139
140 pub fn zero_copy_iter(
141 &self,
142 ) -> impl Iterator<Item = Result<BootfsEntry, BootfsParserError>> + '_ {
143 BootfsParserIterator::new(&self, true)
144 }
145}
146
147#[derive(Debug)]
148pub struct BootfsEntry {
149 pub name: String,
150 pub offset: u64,
151 pub size: u64,
152
153 pub payload: Option<Vec<u8>>,
155}
156
157#[derive(Debug)]
158struct BootfsParserIterator<'parser> {
159 available_dirsize: u32,
160 dir_offset: u32,
161 entry_index: u32,
162 errored: bool,
163 zero_copy: bool,
164 parser: &'parser BootfsParser,
165}
166impl<'parser> BootfsParserIterator<'parser> {
167 pub fn new(parser: &'parser BootfsParser, zero_copy: bool) -> Self {
168 Self {
169 available_dirsize: parser.dirsize,
170 dir_offset: ZBI_BOOTFS_HEADER_SIZE as u32,
171 entry_index: 0,
172 errored: false,
173 zero_copy,
174 parser,
175 }
176 }
177}
178
179impl<'parser> Iterator for BootfsParserIterator<'parser> {
180 type Item = Result<BootfsEntry, BootfsParserError>;
181
182 fn next(&mut self) -> Option<Self::Item> {
183 if self.available_dirsize <= ZBI_BOOTFS_DIRENT_SIZE as u32 || self.errored {
184 return None;
185 }
186
187 let mut name_len_buf = [0; size_of::<u32>()];
189 if let Err(status) = self.parser.vmo.read(&mut name_len_buf, self.dir_offset.into()) {
190 self.errored = true;
191 return Some(Err(BootfsParserError::FailedToReadPayload { status }));
192 }
193
194 let name_len = LittleEndian::read_u32(&name_len_buf);
195 if name_len < 1 || name_len > ZBI_BOOTFS_MAX_NAME_LEN {
196 self.errored = true;
197 return Some(Err(BootfsParserError::InvalidNameLength {
198 entry_index: self.entry_index,
199 max_name_len: ZBI_BOOTFS_MAX_NAME_LEN,
200 name_len,
201 }));
202 }
203
204 let dirent_size = zbi_bootfs_dirent_size(name_len);
205 if dirent_size > self.available_dirsize {
206 self.errored = true;
207 return Some(Err(BootfsParserError::DirEntryTooBig {
208 dirsize: self.available_dirsize,
209 entry_index: self.entry_index,
210 }));
211 }
212
213 let mut dirent_buffer = vec![0; dirent_size as usize];
215 if let Err(status) = self.parser.vmo.read(&mut dirent_buffer, self.dir_offset.into()) {
216 self.errored = true;
217 return Some(Err(BootfsParserError::FailedToReadPayload { status }));
218 }
219
220 match ZbiBootfsDirent::parse(&dirent_buffer[..]) {
221 Ok(dirent) => {
222 let mut payload = None;
224 let offset: u64 = zbi_bootfs_page_align(dirent.data_off()).into();
225 let size: u64 = dirent.data_len().into();
226
227 if !self.zero_copy {
228 let buffer_size = usize::try_from(size).unwrap_or_else(|_| {
229 self.errored = true;
230 return 0;
231 });
232 let mut buffer = vec![0; buffer_size];
233 if let Err(status) = self.parser.vmo.read(&mut buffer, offset) {
234 self.errored = true;
235 return Some(Err(BootfsParserError::FailedToReadPayload { status }));
236 }
237
238 payload = Some(buffer);
239 }
240
241 self.dir_offset += dirent_buffer.len() as u32;
242 self.available_dirsize -= dirent_size;
243 self.entry_index += 1;
244
245 Some(dirent.name().map(|name| BootfsEntry {
246 name: name.to_owned(),
247 offset,
248 size,
249 payload,
250 }))
251 }
252 Err(err) => {
253 self.errored = true;
254 Some(Err(err))
255 }
256 }
257 }
258}
259
260#[cfg(test)]
261mod tests {
262 use super::*;
263 use anyhow::Error;
264 use std::collections::HashMap;
265 use std::fs::File;
266 use std::io::prelude::*;
267 use std::sync::LazyLock;
268
269 static GOLDEN_DIR: &str = "/pkg/data/golden/";
270 static BASIC_BOOTFS_UNCOMPRESSED_FILE: &str = "/pkg/data/basic.bootfs.uncompressed";
271
272 fn read_file_into_hashmap(dir: &str, filename: &str, map: &mut HashMap<String, Vec<u8>>) {
273 let mut file_buffer = Vec::new();
274 let path = format!("{}{}", dir, filename);
275
276 File::open(&path)
277 .unwrap_or_else(|e| panic!("Failed to open file {}: {:?}", &path, e))
278 .read_to_end(&mut file_buffer)
279 .unwrap_or_else(|e| panic!("Failed to read file {}: {:?}", &path, e));
280 map.insert(filename.to_string(), file_buffer);
281 }
282
283 static GOLDEN_FILES: LazyLock<HashMap<String, Vec<u8>>> = LazyLock::new(|| {
284 let mut m = HashMap::new();
285 read_file_into_hashmap(GOLDEN_DIR, "dir/empty", &mut m);
286 read_file_into_hashmap(GOLDEN_DIR, "dir/lorem.txt", &mut m);
287 read_file_into_hashmap(GOLDEN_DIR, "dir/simple-copy.txt", &mut m);
288 read_file_into_hashmap(GOLDEN_DIR, "empty", &mut m);
289 read_file_into_hashmap(GOLDEN_DIR, "random.dat", &mut m);
290 read_file_into_hashmap(GOLDEN_DIR, "simple.txt", &mut m);
291 m
292 });
293
294 fn read_file_to_vmo(path: &str) -> Result<zx::Vmo, Error> {
295 let mut file_buffer = Vec::new();
296 File::open(path)?.read_to_end(&mut file_buffer)?;
297
298 let vmo = zx::Vmo::create(file_buffer.len() as u64)?;
299 vmo.write(&file_buffer, 0)?;
300 Ok(vmo)
301 }
302
303 #[test]
304 fn dirent_from_raw_fails_on_bad_cstring() {
305 const NAME_LEN: u8 = 3;
306 let mut dirent_buf = [0; ZBI_BOOTFS_DIRENT_SIZE + NAME_LEN as usize];
307
308 dirent_buf[0] = NAME_LEN;
309 dirent_buf[ZBI_BOOTFS_DIRENT_SIZE] = 'o' as u8;
310 dirent_buf[ZBI_BOOTFS_DIRENT_SIZE + 1] = 'k' as u8;
311 dirent_buf[ZBI_BOOTFS_DIRENT_SIZE + 2] = 'a' as u8;
313
314 let dirent = ZbiBootfsDirent::parse(&dirent_buf[..])
315 .expect("Failed to create ZbiBootfsDirent from raw buffer");
316 match dirent.name().unwrap_err() {
317 BootfsParserError::InvalidNameString { cause: _cause } => (),
318 _ => panic!("ZbiBootfsDirent.name did not fail with correct error"),
319 }
320 }
321
322 #[test]
323 fn dirent_from_raw_fails_on_non_utf8_string() {
324 const NAME_LEN: u8 = 3;
325 let mut dirent_buf = [0; ZBI_BOOTFS_DIRENT_SIZE + NAME_LEN as usize];
326
327 dirent_buf[0] = NAME_LEN;
329 dirent_buf[ZBI_BOOTFS_DIRENT_SIZE] = 0xC3;
330 dirent_buf[ZBI_BOOTFS_DIRENT_SIZE + 1] = 0x28;
331 dirent_buf[ZBI_BOOTFS_DIRENT_SIZE + 2] = '\0' as u8;
332
333 let char_sequence = &dirent_buf[ZBI_BOOTFS_DIRENT_SIZE..dirent_buf.len()];
335 assert_eq!(true, String::from_utf8(char_sequence.to_vec()).is_err());
336
337 let dirent = ZbiBootfsDirent::parse(&dirent_buf[..])
338 .expect("Failed to create ZbiBootfsDirent from raw buffer");
339 match dirent.name().unwrap_err() {
340 BootfsParserError::InvalidNameFormat { cause: _cause } => (),
341 _ => panic!("ZbiBootfsDirent.name did not fail with correct error"),
342 }
343 }
344
345 #[test]
346 fn create_bootfs_parser() {
347 let vmo = read_file_to_vmo(BASIC_BOOTFS_UNCOMPRESSED_FILE).unwrap();
348 assert_eq!(true, BootfsParser::create_from_vmo(vmo).is_ok());
349 }
350
351 #[test]
352 fn process_basic_bootfs() {
353 let vmo = read_file_to_vmo(BASIC_BOOTFS_UNCOMPRESSED_FILE).unwrap();
354 let parser = BootfsParser::create_from_vmo(vmo).expect("Failed to read bootfs file");
355
356 let mut files = Box::new(HashMap::new());
357
358 parser.iter().for_each(|result| {
359 let result = result.expect("Failed to process bootfs payload");
360 let BootfsEntry { name, payload, .. } = result;
361 files.insert(name, payload.unwrap());
362 });
363
364 assert_eq!(*GOLDEN_FILES, *files);
365 }
366
367 #[test]
368 fn process_bootfs_zero_copy() {
369 let vmo = read_file_to_vmo(BASIC_BOOTFS_UNCOMPRESSED_FILE).unwrap();
370 let vmo_dup =
371 vmo.duplicate_handle(zx::Rights::SAME_RIGHTS).expect("Failed to duplicate vmo");
372
373 let mut files = Box::new(HashMap::new());
374
375 let parser = BootfsParser::create_from_vmo(vmo_dup).expect("Failed to read bootfs file");
376 parser.zero_copy_iter().for_each(|result| {
377 let result = result.expect("Failed to process bootfs payload");
378 let BootfsEntry { name, offset, size, payload } = result;
379
380 assert!(payload.is_none());
383
384 let buffer_size = usize::try_from(size).unwrap();
385 let mut bytes = vec![0; buffer_size];
386 vmo.read(&mut bytes, offset).expect("Failed to read data from the vmo");
387
388 files.insert(name, bytes);
389 });
390
391 assert_eq!(*GOLDEN_FILES, *files);
392 }
393
394 #[test]
395 fn process_bootfs_with_invalid_header() {
396 let vmo = read_file_to_vmo(BASIC_BOOTFS_UNCOMPRESSED_FILE).unwrap();
397 let new_header = [0; ZBI_BOOTFS_HEADER_SIZE];
398
399 vmo.write(&new_header, 0).expect("Failed to wipe bootfs header");
401
402 match BootfsParser::create_from_vmo(vmo).unwrap_err() {
403 BootfsParserError::BadMagic => (),
404 _ => panic!("BootfsParser::create_from_vmo did not fail with correct error"),
405 }
406 }
407
408 #[test]
409 fn process_bootfs_with_invalid_direntry() {
410 let vmo = read_file_to_vmo(BASIC_BOOTFS_UNCOMPRESSED_FILE).unwrap();
411 let new_header = [0; ZBI_BOOTFS_DIRENT_SIZE];
412
413 vmo.write(&new_header, ZBI_BOOTFS_HEADER_SIZE as u64).expect("Failed to wipe direntry");
416
417 let parser = BootfsParser::create_from_vmo(vmo).expect("Failed to create BootfsParser");
418 parser.iter().for_each(|result| match result.unwrap_err() {
419 BootfsParserError::InvalidNameLength { entry_index, max_name_len, name_len } => {
420 assert_eq!(0, entry_index);
421 assert_eq!(ZBI_BOOTFS_MAX_NAME_LEN, max_name_len);
422 assert_eq!(0, name_len);
423 }
424 _ => panic!("parser did not fail with correct error"),
425 });
426 }
427
428 #[test]
429 fn process_bootfs_undersized_dirsize() {
430 let vmo = read_file_to_vmo(BASIC_BOOTFS_UNCOMPRESSED_FILE).unwrap();
431 let new_header = [(ZBI_BOOTFS_DIRENT_SIZE + 1) as u8, 0, 0, 0];
432
433 vmo.write(&new_header, size_of::<u32>() as u64).expect("Failed to change dirsize");
436
437 let parser = BootfsParser::create_from_vmo(vmo).expect("Failed to create BootfsParser");
438 parser.iter().for_each(|result| match result.unwrap_err() {
439 BootfsParserError::DirEntryTooBig { entry_index, dirsize } => {
440 assert_eq!(0, entry_index);
441 assert_eq!(ZBI_BOOTFS_DIRENT_SIZE + 1, dirsize as usize);
442 }
443 _ => panic!("parser did not fail with correct error"),
444 });
445 }
446}