fuchsia_bootfs/
lib.rs

1// Copyright 2019 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
5pub 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
22// Each directory entry has a variable size of [16,268] bytes that
23// must be a multiple of 4 bytes.
24fn 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        // Name is stored as a array reference to a block of characters.
98        // Characters must be UTF-8 encoded.
99        // Valid names are terminated with NUL.
100        // We should fail if either of the above conditions are not met.
101        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/// Parser for bootfs-formatted structures.
111#[derive(Debug)]
112pub struct BootfsParser {
113    // Expose fields for BootfsParserIterator access.
114    pub(self) dirsize: u32,
115    pub(self) vmo: zx::Vmo,
116}
117impl BootfsParser {
118    /// Creates a BootfsParser from an existing VMO.
119    ///
120    /// If `vmo` contains invalid header data, BootfsParserError is returned.
121    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    // Not filled when doing a zero copy parse of bootfs.
154    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        // Read the name_len field only.
188        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        // Now that we know how long the name is, read the whole entry.
214        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                // We have a directory entry now, so retrieve the payload.
223                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    use zx::HandleBased;
269
270    static GOLDEN_DIR: &str = "/pkg/data/golden/";
271    static BASIC_BOOTFS_UNCOMPRESSED_FILE: &str = "/pkg/data/basic.bootfs.uncompressed";
272
273    fn read_file_into_hashmap(dir: &str, filename: &str, map: &mut HashMap<String, Vec<u8>>) {
274        let mut file_buffer = Vec::new();
275        let path = format!("{}{}", dir, filename);
276
277        File::open(&path)
278            .unwrap_or_else(|e| panic!("Failed to open file {}: {:?}", &path, e))
279            .read_to_end(&mut file_buffer)
280            .unwrap_or_else(|e| panic!("Failed to read file {}: {:?}", &path, e));
281        map.insert(filename.to_string(), file_buffer);
282    }
283
284    static GOLDEN_FILES: LazyLock<HashMap<String, Vec<u8>>> = LazyLock::new(|| {
285        let mut m = HashMap::new();
286        read_file_into_hashmap(GOLDEN_DIR, "dir/empty", &mut m);
287        read_file_into_hashmap(GOLDEN_DIR, "dir/lorem.txt", &mut m);
288        read_file_into_hashmap(GOLDEN_DIR, "dir/simple-copy.txt", &mut m);
289        read_file_into_hashmap(GOLDEN_DIR, "empty", &mut m);
290        read_file_into_hashmap(GOLDEN_DIR, "random.dat", &mut m);
291        read_file_into_hashmap(GOLDEN_DIR, "simple.txt", &mut m);
292        m
293    });
294
295    fn read_file_to_vmo(path: &str) -> Result<zx::Vmo, Error> {
296        let mut file_buffer = Vec::new();
297        File::open(path)?.read_to_end(&mut file_buffer)?;
298
299        let vmo = zx::Vmo::create(file_buffer.len() as u64)?;
300        vmo.write(&file_buffer, 0)?;
301        Ok(vmo)
302    }
303
304    #[test]
305    fn dirent_from_raw_fails_on_bad_cstring() {
306        const NAME_LEN: u8 = 3;
307        let mut dirent_buf = [0; ZBI_BOOTFS_DIRENT_SIZE + NAME_LEN as usize];
308
309        dirent_buf[0] = NAME_LEN;
310        dirent_buf[ZBI_BOOTFS_DIRENT_SIZE] = 'o' as u8;
311        dirent_buf[ZBI_BOOTFS_DIRENT_SIZE + 1] = 'k' as u8;
312        // This should be NUL...but it's not for this test.
313        dirent_buf[ZBI_BOOTFS_DIRENT_SIZE + 2] = 'a' as u8;
314
315        let dirent = ZbiBootfsDirent::parse(&dirent_buf[..])
316            .expect("Failed to create ZbiBootfsDirent from raw buffer");
317        match dirent.name().unwrap_err() {
318            BootfsParserError::InvalidNameString { cause: _cause } => (),
319            _ => panic!("ZbiBootfsDirent.name did not fail with correct error"),
320        }
321    }
322
323    #[test]
324    fn dirent_from_raw_fails_on_non_utf8_string() {
325        const NAME_LEN: u8 = 3;
326        let mut dirent_buf = [0; ZBI_BOOTFS_DIRENT_SIZE + NAME_LEN as usize];
327
328        // This is an invalid UTF-8 sequence.
329        dirent_buf[0] = NAME_LEN;
330        dirent_buf[ZBI_BOOTFS_DIRENT_SIZE] = 0xC3;
331        dirent_buf[ZBI_BOOTFS_DIRENT_SIZE + 1] = 0x28;
332        dirent_buf[ZBI_BOOTFS_DIRENT_SIZE + 2] = '\0' as u8;
333
334        // Assert that it actually IS an invalid UTF-8 string.
335        let char_sequence = &dirent_buf[ZBI_BOOTFS_DIRENT_SIZE..dirent_buf.len()];
336        assert_eq!(true, String::from_utf8(char_sequence.to_vec()).is_err());
337
338        let dirent = ZbiBootfsDirent::parse(&dirent_buf[..])
339            .expect("Failed to create ZbiBootfsDirent from raw buffer");
340        match dirent.name().unwrap_err() {
341            BootfsParserError::InvalidNameFormat { cause: _cause } => (),
342            _ => panic!("ZbiBootfsDirent.name did not fail with correct error"),
343        }
344    }
345
346    #[test]
347    fn create_bootfs_parser() {
348        let vmo = read_file_to_vmo(BASIC_BOOTFS_UNCOMPRESSED_FILE).unwrap();
349        assert_eq!(true, BootfsParser::create_from_vmo(vmo).is_ok());
350    }
351
352    #[test]
353    fn process_basic_bootfs() {
354        let vmo = read_file_to_vmo(BASIC_BOOTFS_UNCOMPRESSED_FILE).unwrap();
355        let parser = BootfsParser::create_from_vmo(vmo).expect("Failed to read bootfs file");
356
357        let mut files = Box::new(HashMap::new());
358
359        parser.iter().for_each(|result| {
360            let result = result.expect("Failed to process bootfs payload");
361            let BootfsEntry { name, payload, .. } = result;
362            files.insert(name, payload.unwrap());
363        });
364
365        assert_eq!(*GOLDEN_FILES, *files);
366    }
367
368    #[test]
369    fn process_bootfs_zero_copy() {
370        let vmo = read_file_to_vmo(BASIC_BOOTFS_UNCOMPRESSED_FILE).unwrap();
371        let vmo_dup =
372            vmo.duplicate_handle(zx::Rights::SAME_RIGHTS).expect("Failed to duplicate vmo");
373
374        let mut files = Box::new(HashMap::new());
375
376        let parser = BootfsParser::create_from_vmo(vmo_dup).expect("Failed to read bootfs file");
377        parser.zero_copy_iter().for_each(|result| {
378            let result = result.expect("Failed to process bootfs payload");
379            let BootfsEntry { name, offset, size, payload } = result;
380
381            // The zero_copy iterator doesn't make a copy of the files, but using the offset and
382            // size we can read it from the vmo.
383            assert!(payload.is_none());
384
385            let buffer_size = usize::try_from(size).unwrap();
386            let mut bytes = vec![0; buffer_size];
387            vmo.read(&mut bytes, offset).expect("Failed to read data from the vmo");
388
389            files.insert(name, bytes);
390        });
391
392        assert_eq!(*GOLDEN_FILES, *files);
393    }
394
395    #[test]
396    fn process_bootfs_with_invalid_header() {
397        let vmo = read_file_to_vmo(BASIC_BOOTFS_UNCOMPRESSED_FILE).unwrap();
398        let new_header = [0; ZBI_BOOTFS_HEADER_SIZE];
399
400        // Wipe the header of a known good bootfs payload.
401        vmo.write(&new_header, 0).expect("Failed to wipe bootfs header");
402
403        match BootfsParser::create_from_vmo(vmo).unwrap_err() {
404            BootfsParserError::BadMagic => (),
405            _ => panic!("BootfsParser::create_from_vmo did not fail with correct error"),
406        }
407    }
408
409    #[test]
410    fn process_bootfs_with_invalid_direntry() {
411        let vmo = read_file_to_vmo(BASIC_BOOTFS_UNCOMPRESSED_FILE).unwrap();
412        let new_header = [0; ZBI_BOOTFS_DIRENT_SIZE];
413
414        // Wipe the first direntry of a known good bootfs payload.
415        // The first direntry starts immediately after the header.
416        vmo.write(&new_header, ZBI_BOOTFS_HEADER_SIZE as u64).expect("Failed to wipe direntry");
417
418        let parser = BootfsParser::create_from_vmo(vmo).expect("Failed to create BootfsParser");
419        parser.iter().for_each(|result| match result.unwrap_err() {
420            BootfsParserError::InvalidNameLength { entry_index, max_name_len, name_len } => {
421                assert_eq!(0, entry_index);
422                assert_eq!(ZBI_BOOTFS_MAX_NAME_LEN, max_name_len);
423                assert_eq!(0, name_len);
424            }
425            _ => panic!("parser did not fail with correct error"),
426        });
427    }
428
429    #[test]
430    fn process_bootfs_undersized_dirsize() {
431        let vmo = read_file_to_vmo(BASIC_BOOTFS_UNCOMPRESSED_FILE).unwrap();
432        let new_header = [(ZBI_BOOTFS_DIRENT_SIZE + 1) as u8, 0, 0, 0];
433
434        // Change dirsize to ZBI_BOOTFS_DIRENT_SIZE+1.
435        // It is the second u32 value in the zbi_bootfs_header_t struct.
436        vmo.write(&new_header, size_of::<u32>() as u64).expect("Failed to change dirsize");
437
438        let parser = BootfsParser::create_from_vmo(vmo).expect("Failed to create BootfsParser");
439        parser.iter().for_each(|result| match result.unwrap_err() {
440            BootfsParserError::DirEntryTooBig { entry_index, dirsize } => {
441                assert_eq!(0, entry_index);
442                assert_eq!(ZBI_BOOTFS_DIRENT_SIZE + 1, dirsize as usize);
443            }
444            _ => panic!("parser did not fail with correct error"),
445        });
446    }
447}