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_dirent_t, zbi_bootfs_header_t, ZBI_BOOTFS_MAGIC, ZBI_BOOTFS_MAX_NAME_LEN,
9    ZBI_BOOTFS_PAGE_SIZE,
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 lazy_static::lazy_static;
265    use std::collections::HashMap;
266    use std::fs::File;
267    use std::io::prelude::*;
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    lazy_static! {
285        static ref GOLDEN_FILES: HashMap<String, Vec<u8>> = {
286            let mut m = HashMap::new();
287            read_file_into_hashmap(GOLDEN_DIR, "dir/empty", &mut m);
288            read_file_into_hashmap(GOLDEN_DIR, "dir/lorem.txt", &mut m);
289            read_file_into_hashmap(GOLDEN_DIR, "dir/simple-copy.txt", &mut m);
290            read_file_into_hashmap(GOLDEN_DIR, "empty", &mut m);
291            read_file_into_hashmap(GOLDEN_DIR, "random.dat", &mut m);
292            read_file_into_hashmap(GOLDEN_DIR, "simple.txt", &mut m);
293            m
294        };
295    }
296
297    fn read_file_to_vmo(path: &str) -> Result<zx::Vmo, Error> {
298        let mut file_buffer = Vec::new();
299        File::open(path)?.read_to_end(&mut file_buffer)?;
300
301        let vmo = zx::Vmo::create(file_buffer.len() as u64)?;
302        vmo.write(&file_buffer, 0)?;
303        Ok(vmo)
304    }
305
306    #[test]
307    fn dirent_from_raw_fails_on_bad_cstring() {
308        const NAME_LEN: u8 = 3;
309        let mut dirent_buf = [0; ZBI_BOOTFS_DIRENT_SIZE + NAME_LEN as usize];
310
311        dirent_buf[0] = NAME_LEN;
312        dirent_buf[ZBI_BOOTFS_DIRENT_SIZE] = 'o' as u8;
313        dirent_buf[ZBI_BOOTFS_DIRENT_SIZE + 1] = 'k' as u8;
314        // This should be NUL...but it's not for this test.
315        dirent_buf[ZBI_BOOTFS_DIRENT_SIZE + 2] = 'a' as u8;
316
317        let dirent = ZbiBootfsDirent::parse(&dirent_buf[..])
318            .expect("Failed to create ZbiBootfsDirent from raw buffer");
319        match dirent.name().unwrap_err() {
320            BootfsParserError::InvalidNameString { cause: _cause } => (),
321            _ => panic!("ZbiBootfsDirent.name did not fail with correct error"),
322        }
323    }
324
325    #[test]
326    fn dirent_from_raw_fails_on_non_utf8_string() {
327        const NAME_LEN: u8 = 3;
328        let mut dirent_buf = [0; ZBI_BOOTFS_DIRENT_SIZE + NAME_LEN as usize];
329
330        // This is an invalid UTF-8 sequence.
331        dirent_buf[0] = NAME_LEN;
332        dirent_buf[ZBI_BOOTFS_DIRENT_SIZE] = 0xC3;
333        dirent_buf[ZBI_BOOTFS_DIRENT_SIZE + 1] = 0x28;
334        dirent_buf[ZBI_BOOTFS_DIRENT_SIZE + 2] = '\0' as u8;
335
336        // Assert that it actually IS an invalid UTF-8 string.
337        let char_sequence = &dirent_buf[ZBI_BOOTFS_DIRENT_SIZE..dirent_buf.len()];
338        assert_eq!(true, String::from_utf8(char_sequence.to_vec()).is_err());
339
340        let dirent = ZbiBootfsDirent::parse(&dirent_buf[..])
341            .expect("Failed to create ZbiBootfsDirent from raw buffer");
342        match dirent.name().unwrap_err() {
343            BootfsParserError::InvalidNameFormat { cause: _cause } => (),
344            _ => panic!("ZbiBootfsDirent.name did not fail with correct error"),
345        }
346    }
347
348    #[test]
349    fn create_bootfs_parser() {
350        let vmo = read_file_to_vmo(BASIC_BOOTFS_UNCOMPRESSED_FILE).unwrap();
351        assert_eq!(true, BootfsParser::create_from_vmo(vmo).is_ok());
352    }
353
354    #[test]
355    fn process_basic_bootfs() {
356        let vmo = read_file_to_vmo(BASIC_BOOTFS_UNCOMPRESSED_FILE).unwrap();
357        let parser = BootfsParser::create_from_vmo(vmo).expect("Failed to read bootfs file");
358
359        let mut files = Box::new(HashMap::new());
360
361        parser.iter().for_each(|result| {
362            let result = result.expect("Failed to process bootfs payload");
363            let BootfsEntry { name, payload, .. } = result;
364            files.insert(name, payload.unwrap());
365        });
366
367        assert_eq!(*GOLDEN_FILES, *files);
368    }
369
370    #[test]
371    fn process_bootfs_zero_copy() {
372        let vmo = read_file_to_vmo(BASIC_BOOTFS_UNCOMPRESSED_FILE).unwrap();
373        let vmo_dup =
374            vmo.duplicate_handle(zx::Rights::SAME_RIGHTS).expect("Failed to duplicate vmo");
375
376        let mut files = Box::new(HashMap::new());
377
378        let parser = BootfsParser::create_from_vmo(vmo_dup).expect("Failed to read bootfs file");
379        parser.zero_copy_iter().for_each(|result| {
380            let result = result.expect("Failed to process bootfs payload");
381            let BootfsEntry { name, offset, size, payload } = result;
382
383            // The zero_copy iterator doesn't make a copy of the files, but using the offset and
384            // size we can read it from the vmo.
385            assert!(payload.is_none());
386
387            let buffer_size = usize::try_from(size).unwrap();
388            let mut bytes = vec![0; buffer_size];
389            vmo.read(&mut bytes, offset).expect("Failed to read data from the vmo");
390
391            files.insert(name, bytes);
392        });
393
394        assert_eq!(*GOLDEN_FILES, *files);
395    }
396
397    #[test]
398    fn process_bootfs_with_invalid_header() {
399        let vmo = read_file_to_vmo(BASIC_BOOTFS_UNCOMPRESSED_FILE).unwrap();
400        let new_header = [0; ZBI_BOOTFS_HEADER_SIZE];
401
402        // Wipe the header of a known good bootfs payload.
403        vmo.write(&new_header, 0).expect("Failed to wipe bootfs header");
404
405        match BootfsParser::create_from_vmo(vmo).unwrap_err() {
406            BootfsParserError::BadMagic => (),
407            _ => panic!("BootfsParser::create_from_vmo did not fail with correct error"),
408        }
409    }
410
411    #[test]
412    fn process_bootfs_with_invalid_direntry() {
413        let vmo = read_file_to_vmo(BASIC_BOOTFS_UNCOMPRESSED_FILE).unwrap();
414        let new_header = [0; ZBI_BOOTFS_DIRENT_SIZE];
415
416        // Wipe the first direntry of a known good bootfs payload.
417        // The first direntry starts immediately after the header.
418        vmo.write(&new_header, ZBI_BOOTFS_HEADER_SIZE as u64).expect("Failed to wipe direntry");
419
420        let parser = BootfsParser::create_from_vmo(vmo).expect("Failed to create BootfsParser");
421        parser.iter().for_each(|result| match result.unwrap_err() {
422            BootfsParserError::InvalidNameLength { entry_index, max_name_len, name_len } => {
423                assert_eq!(0, entry_index);
424                assert_eq!(ZBI_BOOTFS_MAX_NAME_LEN, max_name_len);
425                assert_eq!(0, name_len);
426            }
427            _ => panic!("parser did not fail with correct error"),
428        });
429    }
430
431    #[test]
432    fn process_bootfs_undersized_dirsize() {
433        let vmo = read_file_to_vmo(BASIC_BOOTFS_UNCOMPRESSED_FILE).unwrap();
434        let new_header = [(ZBI_BOOTFS_DIRENT_SIZE + 1) as u8, 0, 0, 0];
435
436        // Change dirsize to ZBI_BOOTFS_DIRENT_SIZE+1.
437        // It is the second u32 value in the zbi_bootfs_header_t struct.
438        vmo.write(&new_header, size_of::<u32>() as u64).expect("Failed to change dirsize");
439
440        let parser = BootfsParser::create_from_vmo(vmo).expect("Failed to create BootfsParser");
441        parser.iter().for_each(|result| match result.unwrap_err() {
442            BootfsParserError::DirEntryTooBig { entry_index, dirsize } => {
443                assert_eq!(0, entry_index);
444                assert_eq!(ZBI_BOOTFS_DIRENT_SIZE + 1, dirsize as usize);
445            }
446            _ => panic!("parser did not fail with correct error"),
447        });
448    }
449}