blob: 100df4e25dea7b635cae7b94c54cb0cada1fb507 [file] [log] [blame]
/*-
* SPDX-License-Identifier: BSD-2-Clause-FreeBSD
*
* Copyright (c) 2012, 2010 Zheng Liu <lz@freebsd.org>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
* OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
* OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
* SUCH DAMAGE.
*
* $FreeBSD$
*/
// Copyright 2019 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
use {
crate::{
readers::Reader,
structs::{
BlockGroupDesc32, DirEntry2, DirEntryHeader, EntryType, Extent, ExtentHeader,
ExtentIndex, ExtentTreeNode, INode, InvalidAddressErrorType, ParseToStruct,
ParsingError, SuperBlock, FIRST_BG_PADDING, MIN_EXT4_SIZE, ROOT_INODE_NUM,
},
},
once_cell::sync::OnceCell,
std::{
convert::TryInto,
mem::size_of,
path::{Component, Path},
str,
sync::Arc,
},
vfs::{
directory::immutable, file::vmo::asynchronous::read_only_const, tree_builder::TreeBuilder,
},
zerocopy::ByteSlice,
};
// Assuming/ensuring that we are on a 64bit system where u64 == usize.
assert_eq_size!(u64, usize);
pub struct Parser<T: Reader> {
reader: Arc<T>,
super_block: OnceCell<Arc<SuperBlock>>,
}
/// EXT4 Parser
///
/// Takes in a `Reader` that is able to read arbitrary chunks of data from the filesystem image.
///
/// Basic use:
/// let mut parser = Parser::new(VecReader::new(vec_of_u8));
/// let tree = parser.build_fuchsia_tree()
impl<T: 'static + Reader> Parser<T> {
pub fn new(reader: T) -> Self {
Parser { reader: Arc::new(reader), super_block: OnceCell::new() }
}
/// Returns the Super Block.
///
/// If the super block has been parsed and saved before, return that.
/// Else, parse the super block and save it and return it.
///
/// We never need to re-parse the super block in this read-only
/// implementation.
fn super_block(&self) -> Result<Arc<SuperBlock>, ParsingError> {
Ok(self.super_block.get_or_try_init(|| SuperBlock::parse(self.reader.clone()))?.clone())
}
/// Reads block size from the Super Block.
fn block_size(&self) -> Result<u64, ParsingError> {
self.super_block()?.block_size()
}
/// Reads full raw data from a given block number.
fn block(&self, block_number: u64) -> Result<Box<[u8]>, ParsingError> {
if block_number == 0 {
return Err(ParsingError::InvalidAddress(
InvalidAddressErrorType::Lower,
0,
FIRST_BG_PADDING,
));
}
let block_size = self.block_size()?;
let address = block_number
.checked_mul(block_size)
.ok_or(ParsingError::BlockNumberOutOfBounds(block_number))?;
let mut data = vec![0u8; block_size.try_into().unwrap()];
self.reader.read(address, data.as_mut_slice()).map_err(Into::<ParsingError>::into)?;
Ok(data.into_boxed_slice())
}
/// Reads the INode at the given inode number.
pub fn inode(&self, inode_number: u32) -> Result<Arc<INode>, ParsingError> {
if inode_number < 1 {
// INode number 0 is not allowed per ext4 spec.
return Err(ParsingError::InvalidInode(inode_number));
}
let sb = self.super_block()?;
let block_size = self.block_size()?;
// The first Block Group starts with:
// - 1024 byte padding
// - 1024 byte Super Block
// Then in the next block, there are many blocks worth of Block Group Descriptors.
// If the block size is 2048 bytes or larger, then the 1024 byte padding, and the
// Super Block both fit in the first block (0), and the Block Group Descriptors start
// at block 1.
//
// A 1024 byte block size means the padding takes block 0 and the Super Block takes
// block 1. This means the Block Group Descriptors start in block 2.
let bgd_table_offset = if block_size >= MIN_EXT4_SIZE {
// Padding and Super Block both fit in the first block, so offset to the next
// block.
block_size
} else {
// Block size is less than 2048. The only valid block size smaller than 2048 is 1024.
// Padding and Super Block take one block each, so offset to the third block.
block_size * 2
};
let bgd_offset = (inode_number - 1) as u64 / sb.e2fs_ipg.get() as u64
* size_of::<BlockGroupDesc32>() as u64;
let bgd = BlockGroupDesc32::from_reader_with_offset(
self.reader.clone(),
bgd_table_offset + bgd_offset,
ParsingError::InvalidBlockGroupDesc(block_size),
)?;
// Offset could really be anywhere, and the Reader will enforce reading within the
// filesystem size. Not much can be checked here.
let inode_table_offset =
(inode_number - 1) as u64 % sb.e2fs_ipg.get() as u64 * sb.e2fs_inode_size.get() as u64;
let inode_addr = (bgd.ext2bgd_i_tables.get() as u64 * block_size) + inode_table_offset;
if inode_addr < MIN_EXT4_SIZE {
return Err(ParsingError::InvalidAddress(
InvalidAddressErrorType::Lower,
inode_addr,
MIN_EXT4_SIZE,
));
}
INode::from_reader_with_offset(
self.reader.clone(),
inode_addr,
ParsingError::InvalidInode(inode_number),
)
}
/// Helper function to get the root directory INode.
pub fn root_inode(&self) -> Result<Arc<INode>, ParsingError> {
self.inode(ROOT_INODE_NUM)
}
/// Reads all raw data from a given extent leaf node.
fn extent_data(&self, extent: &Extent, mut allowance: u64) -> Result<Vec<u8>, ParsingError> {
let block_number = extent.target_block_num();
let block_count = extent.e_len.get() as u64;
let block_size = self.block_size()?;
let mut read_len;
let mut data = Vec::with_capacity((block_size * block_count).try_into().unwrap());
for i in 0..block_count {
let block_data = self.block(block_number + i as u64)?;
if allowance >= block_size {
read_len = block_size;
} else {
read_len = allowance;
}
let block_data = &block_data[0..read_len.try_into().unwrap()];
data.append(&mut block_data.to_vec());
allowance -= read_len;
}
Ok(data)
}
/// Reads extent data from a leaf node.
///
/// # Arguments
/// * `extent`: Extent from which to read data from.
/// * `data`: Vec where data that is read is added.
/// * `allowance`: The maximum number of bytes to read from the extent. The
/// given file allowance is updated on each call to track sizing for an
/// entire extent tree.
fn read_extent_data(
&self,
extent: &Extent,
data: &mut Vec<u8>,
allowance: &mut u64,
) -> Result<(), ParsingError> {
let mut extent_data = self.extent_data(&extent, *allowance)?;
let extent_len = extent_data.len() as u64;
if extent_len > *allowance {
return Err(ParsingError::ExtentUnexpectedLength(extent_len, *allowance));
}
*allowance -= extent_len;
data.append(&mut extent_data);
Ok(())
}
/// Reads directory entries from an extent leaf node.
fn read_dir_entries(
&self,
extent: &Extent,
entries: &mut Vec<Arc<DirEntry2>>,
) -> Result<(), ParsingError> {
let block_size = self.block_size()?;
let target_block_offset = extent.target_block_num() * block_size;
// The `e2d_reclen` of the last entry will be large enough fill the
// remaining space of the block.
for block_index in 0..extent.e_len.get() {
let mut dir_entry_offset = 0u64;
while (dir_entry_offset + size_of::<DirEntryHeader>() as u64) < block_size {
let offset =
dir_entry_offset + target_block_offset + (block_index as u64 * block_size);
let de_header = DirEntryHeader::from_reader_with_offset(
self.reader.clone(),
offset,
ParsingError::InvalidDirEntry2(offset),
)?;
let mut de = DirEntry2 {
e2d_ino: de_header.e2d_ino,
e2d_reclen: de_header.e2d_reclen,
e2d_namlen: de_header.e2d_namlen,
e2d_type: de_header.e2d_type,
e2d_name: [0u8; 255],
};
self.reader.read(
offset + size_of::<DirEntryHeader>() as u64,
&mut de.e2d_name[..de.e2d_namlen as usize],
)?;
dir_entry_offset += de.e2d_reclen.get() as u64;
if de.e2d_ino.get() != 0 {
entries.push(Arc::new(de));
}
}
}
Ok(())
}
/// Handles an extent tree leaf node by invoking `extent_handler` for each contained extent.
fn iterate_extents_in_leaf<B: ByteSlice, F: FnMut(&Extent) -> Result<(), ParsingError>>(
&self,
extent_tree_node: &ExtentTreeNode<B>,
extent_handler: &mut F,
) -> Result<(), ParsingError> {
for e_index in 0..extent_tree_node.header.eh_ecount.get() {
let start = size_of::<Extent>() * e_index as usize;
let end = start + size_of::<Extent>() as usize;
let e = Extent::to_struct_ref(
&(extent_tree_node.entries)[start..end],
ParsingError::InvalidExtent(start as u64),
)?;
extent_handler(e)?;
}
Ok(())
}
/// Handles traversal down an extent tree.
fn iterate_extents_in_tree<B: ByteSlice, F: FnMut(&Extent) -> Result<(), ParsingError>>(
&self,
extent_tree_node: &ExtentTreeNode<B>,
extent_handler: &mut F,
) -> Result<(), ParsingError> {
let block_size = self.block_size()?;
match extent_tree_node.header.eh_depth.get() {
0 => {
self.iterate_extents_in_leaf(extent_tree_node, extent_handler)?;
}
1..=4 => {
for e_index in 0..extent_tree_node.header.eh_ecount.get() {
let start: usize = size_of::<Extent>() * e_index as usize;
let end = start + size_of::<Extent>();
let e = ExtentIndex::to_struct_ref(
&(extent_tree_node.entries)[start..end],
ParsingError::InvalidExtent(start as u64),
)?;
let next_level_offset = e.target_block_num() as u64 * block_size;
let next_extent_header = ExtentHeader::from_reader_with_offset(
self.reader.clone(),
next_level_offset,
ParsingError::InvalidExtent(next_level_offset),
)?;
let entry_count = next_extent_header.eh_ecount.get() as usize;
let entry_size = match next_extent_header.eh_depth.get() {
0 => size_of::<Extent>(),
_ => size_of::<ExtentIndex>(),
};
let node_size = size_of::<ExtentHeader>() + (entry_count * entry_size);
let mut data = vec![0u8; node_size];
self.reader.read(next_level_offset, data.as_mut_slice())?;
let next_level_node = ExtentTreeNode::parse(data.as_slice())
.ok_or_else(|| ParsingError::InvalidExtent(next_level_offset))?;
self.iterate_extents_in_tree(&next_level_node, extent_handler)?;
}
}
_ => return Err(ParsingError::InvalidExtentHeader),
};
Ok(())
}
/// Lists directory entries from the directory that is the given Inode.
///
/// Errors if the Inode does not map to a Directory.
pub fn entries_from_inode(
&self,
inode: &Arc<INode>,
) -> Result<Vec<Arc<DirEntry2>>, ParsingError> {
let root_extent_tree_node = inode.extent_tree_node()?;
let mut dir_entries = Vec::new();
self.iterate_extents_in_tree(&root_extent_tree_node, &mut |extent| {
self.read_dir_entries(extent, &mut dir_entries)
})?;
Ok(dir_entries)
}
/// Gets any DirEntry2 that isn't root.
///
/// Root doesn't have a DirEntry2.
///
/// When dynamic loading of files is supported, this is the required mechanism.
pub fn entry_at_path(&self, path: &Path) -> Result<Arc<DirEntry2>, ParsingError> {
let root_inode = self.root_inode()?;
let root_entries = self.entries_from_inode(&root_inode)?;
let mut entry_map = DirEntry2::as_hash_map(root_entries)?;
let mut components = path.components().peekable();
let mut component = components.next();
while component != None {
match component {
Some(Component::RootDir) => {
// Skip
}
Some(Component::Normal(name)) => {
let name = name.to_str().ok_or(ParsingError::InvalidInputPath)?;
if let Some(entry) = entry_map.remove(name) {
if components.peek() == None {
return Ok(entry);
}
match EntryType::from_u8(entry.e2d_type)? {
EntryType::Directory => {
let inode = self.inode(entry.e2d_ino.get())?;
entry_map =
DirEntry2::as_hash_map(self.entries_from_inode(&inode)?)?;
}
_ => {
break;
}
}
}
}
_ => {
break;
}
}
component = components.next();
}
match path.to_str() {
Some(s) => Err(ParsingError::PathNotFound(s.to_string())),
None => Err(ParsingError::PathNotFound(
"Bad path - was not able to convert into string".to_string(),
)),
}
}
/// Reads all raw data for a given inode.
///
/// For a file, this will be the file data. For a symlink,
/// this will be the symlink target.
pub fn read_data(&self, inode_num: u32) -> Result<Vec<u8>, ParsingError> {
let inode = self.inode(inode_num)?;
let mut size_remaining = inode.size();
let mut data = Vec::with_capacity(size_remaining.try_into().unwrap());
// Check for symlink with inline data.
if u16::from(inode.e2di_mode) & 0xa000 != 0 && u32::from(inode.e2di_nblock) == 0 {
data.extend_from_slice(&inode.e2di_blocks[..inode.size().try_into().unwrap()]);
return Ok(data);
}
let root_extent_tree_node = inode.extent_tree_node()?;
let mut extents = Vec::new();
self.iterate_extents_in_tree(&root_extent_tree_node, &mut |extent| {
extents.push(extent.clone());
Ok(())
})?;
let block_size = self.block_size()?;
// Summarized from https://www.kernel.org/doc/ols/2007/ols2007v2-pages-21-34.pdf,
// Section 2.2: Extent and ExtentHeader entries must be sorted by logical block number. This
// enforces that when the extent tree is traversed depth first that a list of extents sorted
// by logical block number is produced. This is a requirement to produce the proper ordering
// of bytes within `data` here.
for extent in extents {
let buffer_offset = extent.e_blk.get() as u64 * block_size;
// File may be sparse. Sparse files will have gaps
// between logical blocks. Fill in any gaps with zeros.
if buffer_offset > data.len() as u64 {
size_remaining -= buffer_offset - data.len() as u64;
data.resize(buffer_offset.try_into().unwrap(), 0);
}
self.read_extent_data(&extent, &mut data, &mut size_remaining)?;
}
Ok(data)
}
/// Progress through the entire directory tree starting from the given INode.
///
/// If given the root directory INode, this will process through every directory entry in the
/// filesystem in a DFS manner.
///
/// Takes in a closure that will be called for each entry found.
/// Closure should return `Ok(true)` in order to continue the process, otherwise the process
/// will stop.
///
/// Returns Ok(true) if it has indexed its subtree successfully. Otherwise, if the receiver
/// chooses to cancel indexing early, an Ok(false) is returned and propagated up.
pub fn index<R>(
&self,
inode: Arc<INode>,
prefix: Vec<&str>,
receiver: &mut R,
) -> Result<bool, ParsingError>
where
R: FnMut(&Parser<T>, Vec<&str>, Arc<DirEntry2>) -> Result<bool, ParsingError>,
{
let entries = self.entries_from_inode(&inode)?;
for entry in entries {
let entry_name = entry.name()?;
if entry_name == "." || entry_name == ".." {
continue;
}
let mut name = Vec::new();
name.append(&mut prefix.clone());
name.push(entry_name);
if !receiver(self, name.clone(), entry.clone())? {
return Ok(false);
}
if EntryType::from_u8(entry.e2d_type)? == EntryType::Directory {
let inode = self.inode(entry.e2d_ino.get())?;
if !self.index(inode, name, receiver)? {
return Ok(false);
}
}
}
Ok(true)
}
/// Returns a `Simple` filesystem as built by `TreeBuilder.build()`.
pub fn build_fuchsia_tree(&self) -> Result<Arc<immutable::Simple>, ParsingError> {
let root_inode = self.root_inode()?;
let mut tree = TreeBuilder::empty_dir();
self.index(root_inode, Vec::new(), &mut |my_self, path, entry| {
let entry_type = EntryType::from_u8(entry.e2d_type)?;
match entry_type {
EntryType::RegularFile => {
let data = my_self.read_data(entry.e2d_ino.into())?;
let bytes = data.as_slice();
tree.add_entry(path.clone(), read_only_const(bytes))
.map_err(|_| ParsingError::BadFile(path.join("/")))?;
}
EntryType::Directory => {
tree.add_empty_dir(path.clone())
.map_err(|_| ParsingError::BadDirectory(path.join("/")))?;
}
_ => {
// TODO(vfcc): Handle other types.
}
}
Ok(true)
})?;
Ok(tree.build())
}
}
#[cfg(test)]
mod tests {
use {
crate::{parser::Parser, readers::VecReader, structs::EntryType},
maplit::hashmap,
sha2::{Digest, Sha256},
std::{
collections::{HashMap, HashSet},
fs,
path::Path,
str,
},
test_case::test_case,
};
#[test]
fn list_root_1_file() {
let data = fs::read("/pkg/data/1file.img").expect("Unable to read file");
let parser = Parser::new(VecReader::new(data));
assert!(parser.super_block().expect("Super Block").check_magic().is_ok());
let root_inode = parser.root_inode().expect("Parse INode");
let entries = parser.entries_from_inode(&root_inode).expect("List entries");
let mut expected_entries = vec!["file1", "lost+found", "..", "."];
for de in &entries {
assert_eq!(expected_entries.pop().unwrap(), de.name().unwrap());
}
assert_eq!(expected_entries.len(), 0);
}
#[test_case(
"/pkg/data/nest.img",
vec!["inner", "file1", "lost+found", "..", "."];
"fs with a single directory")]
#[test_case(
"/pkg/data/extents.img",
vec!["a", "smallfile", "largefile", "sparsefile", "lost+found", "..", "."];
"fs with multiple files with multiple extents")]
fn list_root(ext4_path: &str, mut expected_entries: Vec<&str>) {
let data = fs::read(ext4_path).expect("Unable to read file");
let parser = Parser::new(VecReader::new(data));
assert!(parser.super_block().expect("Super Block").check_magic().is_ok());
let root_inode = parser.root_inode().expect("Parse INode");
let entries = parser.entries_from_inode(&root_inode).expect("List entries");
for de in &entries {
assert_eq!(expected_entries.pop().unwrap(), de.name().unwrap());
}
assert_eq!(expected_entries.len(), 0);
}
#[test]
fn get_from_path() {
let data = fs::read("/pkg/data/nest.img").expect("Unable to read file");
let parser = Parser::new(VecReader::new(data));
assert!(parser.super_block().expect("Super Block").check_magic().is_ok());
let entry = parser.entry_at_path(Path::new("/inner")).expect("Entry at path");
assert_eq!(entry.e2d_ino.get(), 12);
assert_eq!(entry.name().unwrap(), "inner");
let entry = parser.entry_at_path(Path::new("/inner/file2")).expect("Entry at path");
assert_eq!(entry.e2d_ino.get(), 17);
assert_eq!(entry.name().unwrap(), "file2");
}
#[test]
fn read_data() {
let data = fs::read("/pkg/data/1file.img").expect("Unable to read file");
let parser = Parser::new(VecReader::new(data));
assert!(parser.super_block().expect("Super Block").check_magic().is_ok());
let entry = parser.entry_at_path(Path::new("file1")).expect("Entry at path");
assert_eq!(entry.e2d_ino.get(), 15);
assert_eq!(entry.name().unwrap(), "file1");
let data = parser.read_data(entry.e2d_ino.into()).expect("File data");
let compare = "file1 contents.\n";
assert_eq!(data.len(), compare.len());
assert_eq!(str::from_utf8(data.as_slice()).expect("File data"), compare);
}
#[test]
fn fail_inode_zero() {
let data = fs::read("/pkg/data/1file.img").expect("Unable to read file");
let parser = Parser::new(VecReader::new(data));
assert!(parser.inode(0).is_err());
}
#[test]
fn index() {
let data = fs::read("/pkg/data/nest.img").expect("Unable to read file");
let parser = Parser::new(VecReader::new(data));
assert!(parser.super_block().expect("Super Block").check_magic().is_ok());
let mut count = 0;
let mut entries: HashSet<u32> = HashSet::new();
let root_inode = parser.root_inode().expect("Root inode");
parser
.index(root_inode, Vec::new(), &mut |_, _, entry| {
count += 1;
// Make sure each inode only appears once.
assert_ne!(entries.contains(&entry.e2d_ino.get()), true);
entries.insert(entry.e2d_ino.get());
Ok(true)
})
.expect("Index");
assert_eq!(count, 4);
}
#[test_case(
"/pkg/data/extents.img",
hashmap!{
"largefile".to_string() => "de2cf635ae4e0e727f1e412f978001d6a70d2386dc798d4327ec8c77a8e4895d".to_string(),
"smallfile".to_string() => "5891b5b522d5df086d0ff0b110fbd9d21bb4fc7163af34d08286a2e846f6be03".to_string(),
"sparsefile".to_string() => "3f411e42c1417cd8845d7144679812be3e120318d843c8c6e66d8b2c47a700e9".to_string(),
"a/multi/dir/path/within/this/crowded/extents/test/img/empty".to_string() => "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855".to_string(),
},
vec!["a/multi/dir/path/within/this/crowded/extents/test/img", "lost+found"];
"fs with multiple files with multiple extents")]
#[test_case(
"/pkg/data/1file.img",
hashmap!{
"file1".to_string() => "6bc35bfb2ca96c75a1fecde205693c19a827d4b04e90ace330048f3e031487dd".to_string(),
},
vec!["lost+found"];
"fs with one small file")]
#[test_case(
"/pkg/data/nest.img",
hashmap!{
"file1".to_string() => "6bc35bfb2ca96c75a1fecde205693c19a827d4b04e90ace330048f3e031487dd".to_string(),
"inner/file2".to_string() => "215ca145cbac95c9e2a6f5ff91ca1887c837b18e5f58fd2a7a16e2e5a3901e10".to_string(),
},
vec!["inner", "lost+found"];
"fs with a single directory")]
#[test_case(
"/pkg/data/longdir.img",
{
let mut hash = HashMap::new();
for i in 1...1000 {
hash.insert(i.to_string(), "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855".to_string());
}
hash
},
vec!["lost+found"];
"fs with many entries in a directory")]
fn check_data(
ext4_path: &str,
mut file_hashes: HashMap<String, String>,
expected_dirs: Vec<&str>,
) {
let data = fs::read(ext4_path).expect("Unable to read file");
let parser = Parser::new(VecReader::new(data));
assert!(parser.super_block().expect("Super Block").check_magic().is_ok());
let root_inode = parser.root_inode().expect("Root inode");
parser
.index(root_inode, Vec::new(), &mut |my_self, path, entry| {
let entry_type = EntryType::from_u8(entry.e2d_type).expect("Entry Type");
let file_path = path.join("/");
match entry_type {
EntryType::RegularFile => {
let data = my_self.read_data(entry.e2d_ino.into()).expect("File data");
let mut hasher = Sha256::new();
hasher.update(&data);
assert_eq!(
file_hashes.remove(&file_path).unwrap(),
hex::encode(hasher.finalize())
);
}
EntryType::Directory => {
let mut found = false;
// These should be the only possible directories.
for expected_dir in expected_dirs.iter() {
if expected_dir.starts_with(&file_path) {
found = true;
break;
}
}
assert!(found, "Unexpected path {}", file_path);
}
_ => {
assert!(false, "No other types should exist in this image.");
}
}
Ok(true)
})
.expect("Index");
assert!(file_hashes.is_empty(), "Expected files were not found {:?}", file_hashes);
}
}