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 std::ffi::CStr;
12use std::mem::size_of;
13use std::str::Utf8Error;
14use thiserror::Error;
15use zerocopy::Ref;
16
17const ZBI_BOOTFS_HEADER_SIZE: usize = size_of::<zbi_bootfs_header_t>();
18
19pub fn zbi_bootfs_page_align(size: u32) -> u32 {
20 size.wrapping_add(ZBI_BOOTFS_PAGE_SIZE - 1) & !(ZBI_BOOTFS_PAGE_SIZE - 1)
21}
22
23pub fn zbi_bootfs_is_aligned(size: u32) -> bool {
24 size % ZBI_BOOTFS_PAGE_SIZE == 0
25}
26
27#[derive(Debug, Error, Eq, PartialEq)]
28pub enum BootfsParserError {
29 #[error("Invalid magic for bootfs payload")]
30 BadMagic,
31
32 #[error("Directory entry {} exceeds available dirsize of {}", entry_index, dirsize)]
33 DirEntryTooBig { entry_index: u32, dirsize: u32 },
34
35 #[error("Failed to read payload: {}", status)]
36 FailedToReadPayload { status: zx::Status },
37
38 #[error("Failed to parse bootfs header")]
39 FailedToParseHeader,
40
41 #[error("Failed to parse directory entry")]
42 FailedToParseDirEntry,
43
44 #[error("Failed to read name as UTF-8: {}", cause)]
45 InvalidNameFormat {
46 #[source]
47 cause: Utf8Error,
48 },
49
50 #[error("Failed to find null terminated string for name: {}", cause)]
51 InvalidNameString {
52 #[source]
53 cause: std::ffi::FromBytesWithNulError,
54 },
55
56 #[error(
57 "name_len must be between 1 and {}, found {} for directory entry {}",
58 max_name_len,
59 name_len,
60 entry_index
61 )]
62 InvalidNameLength { name_len: u32, max_name_len: u32, entry_index: u32 },
63
64 #[error("Data offset {} for directory entry {} is not page-aligned", offset, entry_index)]
65 UnalignedDataOffset { offset: u32, entry_index: u32 },
66}
67
68fn parse_dirent_name(name_bytes: &[u8]) -> Result<&str, BootfsParserError> {
69 match CStr::from_bytes_with_nul(name_bytes) {
74 Ok(bytes) => bytes.to_str().map_err(|cause| BootfsParserError::InvalidNameFormat { cause }),
75 Err(cause) => Err(BootfsParserError::InvalidNameString { cause }),
76 }
77}
78
79#[derive(Debug)]
81pub struct BootfsParser {
82 dir_bytes: Vec<u8>,
83}
84
85impl BootfsParser {
86 pub fn create_from_vmo(vmo: &zx::Vmo) -> Result<BootfsParser, BootfsParserError> {
90 let header: zbi_bootfs_header_t = vmo
91 .read_to_object(0)
92 .map_err(|status| BootfsParserError::FailedToReadPayload { status })?;
93 if header.magic.get() == ZBI_BOOTFS_MAGIC {
94 let dirsize = header.dirsize.get() as u64;
95 let vmo_size = vmo
96 .get_size()
97 .map_err(|status| BootfsParserError::FailedToReadPayload { status })?;
98 if dirsize > vmo_size.saturating_sub(ZBI_BOOTFS_HEADER_SIZE as u64) {
99 return Err(BootfsParserError::FailedToParseHeader);
100 }
101
102 let dir_bytes = vmo
105 .read_to_vec::<u8>(ZBI_BOOTFS_HEADER_SIZE as u64, dirsize)
106 .map_err(|status| BootfsParserError::FailedToReadPayload { status })?;
107 Ok(Self { dir_bytes })
108 } else {
109 Err(BootfsParserError::BadMagic)
110 }
111 }
112
113 pub fn iter<'a>(
114 &'a self,
115 vmo: &'a zx::Vmo,
116 ) -> impl Iterator<Item = Result<BootfsEntry, BootfsParserError>> + 'a {
117 BootfsParserIterator::new(self, vmo)
118 }
119
120 pub fn zero_copy_iter(
121 &self,
122 ) -> impl Iterator<Item = Result<ZeroCopyBootfsEntry<'_>, BootfsParserError>> + '_ {
123 ZeroCopyBootfsParserIterator::new(self)
124 }
125}
126
127#[derive(Debug)]
128pub struct ZeroCopyBootfsEntry<'a> {
129 pub name: &'a str,
130 pub offset: u64,
131 pub size: u64,
132}
133
134#[derive(Debug)]
135struct ZeroCopyBootfsParserIterator<'parser> {
136 dir_bytes: &'parser [u8],
137 entry_index: u32,
138}
139
140impl<'parser> ZeroCopyBootfsParserIterator<'parser> {
141 pub fn new(parser: &'parser BootfsParser) -> Self {
142 Self { dir_bytes: &parser.dir_bytes, entry_index: 0 }
143 }
144
145 fn next_entry(&mut self) -> Result<ZeroCopyBootfsEntry<'parser>, BootfsParserError> {
146 let dirsize = self.dir_bytes.len() as u32;
147 let (dirent, rest) = Ref::<_, zbi_bootfs_dirent_t>::from_prefix(self.dir_bytes)
148 .map_err(|_| BootfsParserError::FailedToParseDirEntry)?;
149
150 let name_len = dirent.name_len.get();
151 if name_len < 1 || name_len > ZBI_BOOTFS_MAX_NAME_LEN {
152 return Err(BootfsParserError::InvalidNameLength {
153 entry_index: self.entry_index,
154 max_name_len: ZBI_BOOTFS_MAX_NAME_LEN,
155 name_len,
156 });
157 }
158
159 static_assertions::const_assert_eq!(size_of::<zbi_bootfs_dirent_t>(), 12);
163 let padded_name_len = name_len.next_multiple_of(4) as usize;
164 let Some((name_and_padding, rest)) = rest.split_at_checked(padded_name_len) else {
165 return Err(BootfsParserError::DirEntryTooBig {
166 dirsize,
167 entry_index: self.entry_index,
168 });
169 };
170
171 let data_off = dirent.data_off.get();
172 if !zbi_bootfs_is_aligned(data_off) {
173 return Err(BootfsParserError::UnalignedDataOffset {
174 offset: data_off,
175 entry_index: self.entry_index,
176 });
177 }
178
179 let name = parse_dirent_name(&name_and_padding[..(name_len as usize)])?;
180 let offset: u64 = data_off.into();
181 let size: u64 = dirent.data_len.get().into();
182
183 self.dir_bytes = rest;
184 self.entry_index += 1;
185
186 Ok(ZeroCopyBootfsEntry { name, offset, size })
187 }
188}
189
190impl<'parser> Iterator for ZeroCopyBootfsParserIterator<'parser> {
191 type Item = Result<ZeroCopyBootfsEntry<'parser>, BootfsParserError>;
192
193 fn next(&mut self) -> Option<Self::Item> {
194 if self.dir_bytes.is_empty() {
195 return None;
196 }
197
198 let result = self.next_entry();
199 if result.is_err() {
200 self.dir_bytes = &[];
201 }
202 Some(result)
203 }
204}
205
206#[derive(Debug)]
207pub struct BootfsEntry {
208 pub name: String,
209 pub offset: u64,
210 pub size: u64,
211 pub payload: Vec<u8>,
212}
213
214#[derive(Debug)]
215struct BootfsParserIterator<'a> {
216 vmo: &'a zx::Vmo,
217 iter: ZeroCopyBootfsParserIterator<'a>,
218}
219
220impl<'a> BootfsParserIterator<'a> {
221 pub fn new(parser: &'a BootfsParser, vmo: &'a zx::Vmo) -> Self {
222 Self { vmo, iter: ZeroCopyBootfsParserIterator::new(parser) }
223 }
224}
225
226impl Iterator for BootfsParserIterator<'_> {
227 type Item = Result<BootfsEntry, BootfsParserError>;
228
229 fn next(&mut self) -> Option<Self::Item> {
230 let entry = self.iter.next()?;
231 let result = entry.and_then(|zero_copy_entry| {
232 let payload = self
233 .vmo
234 .read_to_vec::<u8>(zero_copy_entry.offset, zero_copy_entry.size)
235 .map_err(|status| BootfsParserError::FailedToReadPayload { status })?;
236 Ok(BootfsEntry {
237 name: zero_copy_entry.name.to_string(),
238 offset: zero_copy_entry.offset,
239 size: zero_copy_entry.size,
240 payload,
241 })
242 });
243 if result.is_err() {
244 self.iter.dir_bytes = &[];
245 }
246 Some(result)
247 }
248}
249
250#[cfg(test)]
251mod tests {
252 use super::*;
253 use anyhow::Error;
254 use std::collections::HashMap;
255 use std::fs::File;
256 use std::io::prelude::*;
257 use std::sync::LazyLock;
258
259 const ZBI_BOOTFS_DIRENT_SIZE: usize = size_of::<zbi_bootfs_dirent_t>();
260 static GOLDEN_DIR: &str = "/pkg/data/golden/";
261 static BASIC_BOOTFS_UNCOMPRESSED_FILE: &str = "/pkg/data/basic.bootfs.uncompressed";
262
263 fn read_file_into_hashmap(dir: &str, filename: &str, map: &mut HashMap<String, Vec<u8>>) {
264 let mut file_buffer = Vec::new();
265 let path = format!("{}{}", dir, filename);
266
267 File::open(&path)
268 .unwrap_or_else(|e| panic!("Failed to open file {}: {:?}", &path, e))
269 .read_to_end(&mut file_buffer)
270 .unwrap_or_else(|e| panic!("Failed to read file {}: {:?}", &path, e));
271 map.insert(filename.to_string(), file_buffer);
272 }
273
274 static GOLDEN_FILES: LazyLock<HashMap<String, Vec<u8>>> = LazyLock::new(|| {
275 let mut m = HashMap::new();
276 read_file_into_hashmap(GOLDEN_DIR, "dir/empty", &mut m);
277 read_file_into_hashmap(GOLDEN_DIR, "dir/lorem.txt", &mut m);
278 read_file_into_hashmap(GOLDEN_DIR, "dir/simple-copy.txt", &mut m);
279 read_file_into_hashmap(GOLDEN_DIR, "empty", &mut m);
280 read_file_into_hashmap(GOLDEN_DIR, "random.dat", &mut m);
281 read_file_into_hashmap(GOLDEN_DIR, "simple.txt", &mut m);
282 m
283 });
284
285 fn read_file_to_vmo(path: &str) -> Result<zx::Vmo, Error> {
286 let mut file_buffer = Vec::new();
287 File::open(path)?.read_to_end(&mut file_buffer)?;
288
289 let vmo = zx::Vmo::create(file_buffer.len() as u64)?;
290 vmo.write(&file_buffer, 0)?;
291 Ok(vmo)
292 }
293
294 #[test]
295 fn dirent_from_raw_fails_on_bad_cstring() {
296 let name_bytes = b"oka";
298 match parse_dirent_name(name_bytes).unwrap_err() {
299 BootfsParserError::InvalidNameString { cause: _cause } => (),
300 _ => panic!("parse_dirent_name did not fail with correct error"),
301 }
302 }
303
304 #[test]
305 fn dirent_from_raw_fails_on_non_utf8_string() {
306 let name_bytes = [0xC3, 0x28, b'\0'];
308
309 assert_eq!(true, String::from_utf8(name_bytes.to_vec()).is_err());
311
312 match parse_dirent_name(&name_bytes).unwrap_err() {
313 BootfsParserError::InvalidNameFormat { cause: _cause } => (),
314 _ => panic!("parse_dirent_name did not fail with correct error"),
315 }
316 }
317
318 #[test]
319 fn create_bootfs_parser() {
320 let vmo = read_file_to_vmo(BASIC_BOOTFS_UNCOMPRESSED_FILE).unwrap();
321 assert_eq!(true, BootfsParser::create_from_vmo(&vmo).is_ok());
322 }
323
324 #[test]
325 fn process_basic_bootfs() {
326 let vmo = read_file_to_vmo(BASIC_BOOTFS_UNCOMPRESSED_FILE).unwrap();
327 let parser = BootfsParser::create_from_vmo(&vmo).expect("Failed to read bootfs file");
328
329 let mut files = Box::new(HashMap::new());
330
331 parser.iter(&vmo).for_each(|result| {
332 let result = result.expect("Failed to process bootfs payload");
333 let BootfsEntry { name, payload, .. } = result;
334 files.insert(name, payload);
335 });
336
337 assert_eq!(*GOLDEN_FILES, *files);
338 }
339
340 #[test]
341 fn process_bootfs_zero_copy() {
342 let vmo = read_file_to_vmo(BASIC_BOOTFS_UNCOMPRESSED_FILE).unwrap();
343
344 let mut files = Box::new(HashMap::new());
345
346 let parser = BootfsParser::create_from_vmo(&vmo).expect("Failed to read bootfs file");
347 parser.zero_copy_iter().for_each(|result| {
348 let result = result.expect("Failed to process bootfs payload");
349 let ZeroCopyBootfsEntry { name, offset, size } = result;
350
351 let buffer_size = usize::try_from(size).unwrap();
352 let mut bytes = vec![0; buffer_size];
353 vmo.read(&mut bytes, offset).expect("Failed to read data from the vmo");
354
355 files.insert(name.to_string(), bytes);
356 });
357
358 assert_eq!(*GOLDEN_FILES, *files);
359 }
360
361 #[test]
362 fn process_bootfs_with_invalid_header() {
363 let vmo = read_file_to_vmo(BASIC_BOOTFS_UNCOMPRESSED_FILE).unwrap();
364 let new_header = [0; ZBI_BOOTFS_HEADER_SIZE];
365
366 vmo.write(&new_header, 0).expect("Failed to wipe bootfs header");
368
369 match BootfsParser::create_from_vmo(&vmo).unwrap_err() {
370 BootfsParserError::BadMagic => (),
371 _ => panic!("BootfsParser::create_from_vmo did not fail with correct error"),
372 }
373 }
374
375 #[test]
376 fn process_bootfs_with_invalid_direntry() {
377 let vmo = read_file_to_vmo(BASIC_BOOTFS_UNCOMPRESSED_FILE).unwrap();
378 let new_header = [0; ZBI_BOOTFS_DIRENT_SIZE];
379
380 vmo.write(&new_header, ZBI_BOOTFS_HEADER_SIZE as u64).expect("Failed to wipe direntry");
383
384 let parser = BootfsParser::create_from_vmo(&vmo).expect("Failed to create BootfsParser");
385 parser.iter(&vmo).for_each(|result| match result.unwrap_err() {
386 BootfsParserError::InvalidNameLength { entry_index, max_name_len, name_len } => {
387 assert_eq!(0, entry_index);
388 assert_eq!(ZBI_BOOTFS_MAX_NAME_LEN, max_name_len);
389 assert_eq!(0, name_len);
390 }
391 _ => panic!("parser did not fail with correct error"),
392 });
393 }
394
395 #[test]
396 fn process_bootfs_undersized_dirsize() {
397 let vmo = read_file_to_vmo(BASIC_BOOTFS_UNCOMPRESSED_FILE).unwrap();
398 let new_header = [(ZBI_BOOTFS_DIRENT_SIZE + 1) as u8, 0, 0, 0];
399
400 vmo.write(&new_header, size_of::<u32>() as u64).expect("Failed to change dirsize");
403
404 let parser = BootfsParser::create_from_vmo(&vmo).expect("Failed to create BootfsParser");
405 parser.iter(&vmo).for_each(|result| match result.unwrap_err() {
406 BootfsParserError::DirEntryTooBig { entry_index, dirsize } => {
407 assert_eq!(0, entry_index);
408 assert_eq!(ZBI_BOOTFS_DIRENT_SIZE + 1, dirsize as usize);
409 }
410 _ => panic!("parser did not fail with correct error"),
411 });
412 }
413
414 #[test]
415 fn process_bootfs_oversized_dirsize_fails() {
416 let vmo = read_file_to_vmo(BASIC_BOOTFS_UNCOMPRESSED_FILE).unwrap();
417 let vmo_size = vmo.get_size().unwrap();
418
419 let oversized_dirsize = (vmo_size - ZBI_BOOTFS_HEADER_SIZE as u64 + 1) as u32;
422 vmo.write(&oversized_dirsize.to_le_bytes(), size_of::<u32>() as u64)
423 .expect("Failed to change dirsize");
424
425 match BootfsParser::create_from_vmo(&vmo).unwrap_err() {
426 BootfsParserError::FailedToParseHeader => (),
427 err => panic!("Expected FailedToParseHeader, got: {:?}", err),
428 }
429 }
430
431 #[test]
432 fn process_bootfs_truncated_direntry_fails() {
433 let vmo = read_file_to_vmo(BASIC_BOOTFS_UNCOMPRESSED_FILE).unwrap();
434
435 let truncated_dirsize = (ZBI_BOOTFS_DIRENT_SIZE - 1) as u32;
437 vmo.write(&truncated_dirsize.to_le_bytes(), size_of::<u32>() as u64)
438 .expect("Failed to change dirsize");
439
440 let parser = BootfsParser::create_from_vmo(&vmo).expect("Failed to create BootfsParser");
441 let mut iter = parser.iter(&vmo);
442 match iter.next() {
443 Some(Err(BootfsParserError::FailedToParseDirEntry)) => (),
444 err => panic!("Expected FailedToParseDirEntry, got: {:?}", err),
445 }
446 assert!(iter.next().is_none());
447
448 let mut zero_copy_iter = parser.zero_copy_iter();
449 match zero_copy_iter.next() {
450 Some(Err(BootfsParserError::FailedToParseDirEntry)) => (),
451 err => panic!("Expected FailedToParseDirEntry, got: {:?}", err),
452 }
453 assert!(zero_copy_iter.next().is_none());
454 }
455
456 #[test]
457 fn process_bootfs_truncated_entry_with_unaligned_offset_fails_with_dir_entry_too_big() {
458 let vmo = read_file_to_vmo(BASIC_BOOTFS_UNCOMPRESSED_FILE).unwrap();
459
460 let undersized_dirsize = (ZBI_BOOTFS_DIRENT_SIZE + 1) as u32;
462 vmo.write(&undersized_dirsize.to_le_bytes(), size_of::<u32>() as u64)
463 .expect("Failed to change dirsize");
464
465 let unaligned_offset = ZBI_BOOTFS_PAGE_SIZE + 1;
467 let data_off_pos = (ZBI_BOOTFS_HEADER_SIZE + 2 * size_of::<u32>()) as u64;
468 vmo.write(&unaligned_offset.to_le_bytes(), data_off_pos)
469 .expect("Failed to change data_off");
470
471 let parser = BootfsParser::create_from_vmo(&vmo).expect("Failed to create BootfsParser");
472 let mut iter = parser.iter(&vmo);
473 match iter.next() {
474 Some(Err(BootfsParserError::DirEntryTooBig { entry_index, dirsize })) => {
475 assert_eq!(0, entry_index);
476 assert_eq!(undersized_dirsize as usize, dirsize as usize);
477 }
478 err => panic!("Expected DirEntryTooBig, got: {:?}", err),
479 }
480 assert!(iter.next().is_none());
481
482 let mut zero_copy_iter = parser.zero_copy_iter();
483 match zero_copy_iter.next() {
484 Some(Err(BootfsParserError::DirEntryTooBig { entry_index, dirsize })) => {
485 assert_eq!(0, entry_index);
486 assert_eq!(undersized_dirsize as usize, dirsize as usize);
487 }
488 err => panic!("Expected DirEntryTooBig, got: {:?}", err),
489 }
490 assert!(zero_copy_iter.next().is_none());
491 }
492
493 #[test]
494 fn process_bootfs_unaligned_data_off_fails() {
495 let vmo = read_file_to_vmo(BASIC_BOOTFS_UNCOMPRESSED_FILE).unwrap();
496
497 let unaligned_offset = ZBI_BOOTFS_PAGE_SIZE + 1;
500 let data_off_pos = (ZBI_BOOTFS_HEADER_SIZE + 2 * size_of::<u32>()) as u64;
501 vmo.write(&unaligned_offset.to_le_bytes(), data_off_pos)
502 .expect("Failed to change data_off");
503
504 let parser = BootfsParser::create_from_vmo(&vmo).expect("Failed to create BootfsParser");
505 parser.iter(&vmo).for_each(|result| match result.unwrap_err() {
506 BootfsParserError::UnalignedDataOffset { offset, entry_index } => {
507 assert_eq!(unaligned_offset, offset);
508 assert_eq!(0, entry_index);
509 }
510 err => panic!("Expected UnalignedDataOffset, got: {:?}", err),
511 });
512 }
513}