#![cfg_attr(not(any(feature = "std", test)), no_std)]
#![cfg_attr(docsrs, feature(doc_cfg))]
#![forbid(unsafe_code)]
#![warn(
clippy::arithmetic_side_effects,
clippy::as_conversions,
clippy::must_use_candidate,
clippy::use_self
)]
#![warn(missing_docs)]
#![warn(unreachable_pub)]
extern crate alloc;
mod block_cache;
mod block_group;
mod block_index;
mod block_size;
mod checksum;
mod dir;
mod dir_block;
mod dir_entry;
mod dir_entry_hash;
mod dir_htree;
mod error;
mod extent;
mod features;
mod file;
mod file_type;
mod format;
mod inode;
mod iters;
mod journal;
mod label;
mod metadata;
mod path;
mod reader;
mod resolve;
mod superblock;
mod util;
mod uuid;
#[cfg(all(test, feature = "std"))]
mod test_util;
use alloc::boxed::Box;
use alloc::rc::Rc;
use alloc::string::String;
use alloc::vec;
use alloc::vec::Vec;
use block_cache::BlockCache;
use block_group::BlockGroupDescriptor;
use block_index::FsBlockIndex;
use core::cell::RefCell;
use core::fmt::{self, Debug, Formatter};
use error::CorruptKind;
use features::ReadOnlyCompatibleFeatures;
use inode::{Inode, InodeIndex};
use journal::Journal;
use resolve::FollowSymlinks;
use superblock::Superblock;
use util::usize_from_u32;
pub use dir_entry::{DirEntry, DirEntryName, DirEntryNameError};
pub use error::{Corrupt, Ext4Error, Incompatible};
pub use features::IncompatibleFeatures;
pub use file::File;
pub use file_type::FileType;
pub use format::BytesDisplay;
pub use iters::read_dir::ReadDir;
pub use label::Label;
pub use metadata::Metadata;
pub use path::{Component, Components, Path, PathBuf, PathError};
pub use reader::{Ext4Read, MemIoError};
pub use uuid::Uuid;
struct Ext4Inner {
superblock: Superblock,
block_group_descriptors: Vec<BlockGroupDescriptor>,
journal: Journal,
block_cache: RefCell<BlockCache>,
reader: RefCell<Box<dyn Ext4Read>>,
}
#[derive(Clone)]
pub struct Ext4(Rc<Ext4Inner>);
impl Ext4 {
pub fn load(mut reader: Box<dyn Ext4Read>) -> Result<Self, Ext4Error> {
let superblock_start = 1024;
let mut data = vec![0; Superblock::SIZE_IN_BYTES_ON_DISK];
reader
.read(superblock_start, &mut data)
.map_err(Ext4Error::Io)?;
let superblock = Superblock::from_bytes(&data)?;
let block_cache =
BlockCache::new(superblock.block_size, superblock.blocks_count)?;
let mut fs = Self(Rc::new(Ext4Inner {
block_group_descriptors: BlockGroupDescriptor::read_all(
&superblock,
&mut *reader,
)?,
reader: RefCell::new(reader),
superblock,
journal: Journal::empty(),
block_cache: RefCell::new(block_cache),
}));
let journal = Journal::load(&fs)?;
Rc::get_mut(&mut fs.0).unwrap().journal = journal;
Ok(fs)
}
#[cfg(feature = "std")]
pub fn load_from_path<P: AsRef<std::path::Path>>(
path: P,
) -> Result<Self, Ext4Error> {
fn inner(path: &std::path::Path) -> Result<Ext4, Ext4Error> {
let file = std::fs::File::open(path)
.map_err(|e| Ext4Error::Io(Box::new(e)))?;
Ext4::load(Box::new(file))
}
inner(path.as_ref())
}
#[must_use]
pub fn label(&self) -> &Label {
&self.0.superblock.label
}
#[must_use]
pub fn uuid(&self) -> Uuid {
self.0.superblock.uuid
}
fn has_metadata_checksums(&self) -> bool {
self.0
.superblock
.read_only_compatible_features
.contains(ReadOnlyCompatibleFeatures::METADATA_CHECKSUMS)
}
fn read_root_inode(&self) -> Result<Inode, Ext4Error> {
let root_inode_index = InodeIndex::new(2).unwrap();
Inode::read(self, root_inode_index)
}
fn read_from_block(
&self,
original_block_index: FsBlockIndex,
offset_within_block: u32,
dst: &mut [u8],
) -> Result<(), Ext4Error> {
let block_index = self.0.journal.map_block_index(original_block_index);
let err = || {
Ext4Error::from(CorruptKind::BlockRead {
block_index,
original_block_index,
offset_within_block,
read_len: dst.len(),
})
};
if block_index == 0 && offset_within_block < 1024 {
return Err(err());
}
if block_index >= self.0.superblock.blocks_count {
return Err(err());
}
let block_size = self.0.superblock.block_size;
if offset_within_block >= block_size {
return Err(err());
}
let read_end = usize_from_u32(offset_within_block)
.checked_add(dst.len())
.ok_or_else(err)?;
if read_end > block_size {
return Err(err());
}
let mut block_cache = self.0.block_cache.borrow_mut();
let cached_block = block_cache.get_or_insert_blocks(
block_index,
|buf: &mut [u8]| {
let start_byte = block_index
.checked_mul(block_size.to_u64())
.ok_or_else(err)?;
self.0
.reader
.borrow_mut()
.read(start_byte, buf)
.map_err(Ext4Error::Io)
},
)?;
dst.copy_from_slice(
&cached_block[usize_from_u32(offset_within_block)..read_end],
);
Ok(())
}
fn read_inode_file(&self, inode: &Inode) -> Result<Vec<u8>, Ext4Error> {
let file_size_in_bytes = usize::try_from(inode.metadata.size_in_bytes)
.map_err(|_| Ext4Error::FileTooLarge)?;
let mut dst = vec![0; file_size_in_bytes];
let mut file = File::open_inode(self, inode.clone())?;
let mut remaining = dst.as_mut();
loop {
let bytes_read = file.read_bytes(remaining)?;
if bytes_read == 0 {
break;
}
remaining = &mut remaining[bytes_read..];
}
Ok(dst)
}
fn path_to_inode(
&self,
path: Path<'_>,
follow: FollowSymlinks,
) -> Result<Inode, Ext4Error> {
resolve::resolve_path(self, path, follow).map(|v| v.0)
}
}
impl Ext4 {
pub fn canonicalize<'p, P>(&self, path: P) -> Result<PathBuf, Ext4Error>
where
P: TryInto<Path<'p>>,
{
let path = path.try_into().map_err(|_| Ext4Error::MalformedPath)?;
resolve::resolve_path(self, path, FollowSymlinks::All).map(|v| v.1)
}
pub fn open<'p, P>(&self, path: P) -> Result<File, Ext4Error>
where
P: TryInto<Path<'p>>,
{
File::open(self, path.try_into().map_err(|_| Ext4Error::MalformedPath)?)
}
pub fn read<'p, P>(&self, path: P) -> Result<Vec<u8>, Ext4Error>
where
P: TryInto<Path<'p>>,
{
fn inner(fs: &Ext4, path: Path<'_>) -> Result<Vec<u8>, Ext4Error> {
let inode = fs.path_to_inode(path, FollowSymlinks::All)?;
if inode.metadata.is_dir() {
return Err(Ext4Error::IsADirectory);
}
if !inode.metadata.file_type.is_regular_file() {
return Err(Ext4Error::IsASpecialFile);
}
fs.read_inode_file(&inode)
}
inner(self, path.try_into().map_err(|_| Ext4Error::MalformedPath)?)
}
pub fn read_to_string<'p, P>(&self, path: P) -> Result<String, Ext4Error>
where
P: TryInto<Path<'p>>,
{
fn inner(fs: &Ext4, path: Path<'_>) -> Result<String, Ext4Error> {
let content = fs.read(path)?;
String::from_utf8(content).map_err(|_| Ext4Error::NotUtf8)
}
inner(self, path.try_into().map_err(|_| Ext4Error::MalformedPath)?)
}
pub fn read_link<'p, P>(&self, path: P) -> Result<PathBuf, Ext4Error>
where
P: TryInto<Path<'p>>,
{
fn inner(fs: &Ext4, path: Path<'_>) -> Result<PathBuf, Ext4Error> {
let inode =
fs.path_to_inode(path, FollowSymlinks::ExcludeFinalComponent)?;
inode.symlink_target(fs)
}
inner(self, path.try_into().map_err(|_| Ext4Error::MalformedPath)?)
}
pub fn read_dir<'p, P>(&self, path: P) -> Result<ReadDir, Ext4Error>
where
P: TryInto<Path<'p>>,
{
fn inner(fs: &Ext4, path: Path<'_>) -> Result<ReadDir, Ext4Error> {
let inode = fs.path_to_inode(path, FollowSymlinks::All)?;
if !inode.metadata.is_dir() {
return Err(Ext4Error::NotADirectory);
}
ReadDir::new(fs.clone(), &inode, path.into())
}
inner(self, path.try_into().map_err(|_| Ext4Error::MalformedPath)?)
}
pub fn exists<'p, P>(&self, path: P) -> Result<bool, Ext4Error>
where
P: TryInto<Path<'p>>,
{
fn inner(fs: &Ext4, path: Path<'_>) -> Result<bool, Ext4Error> {
match fs.path_to_inode(path, FollowSymlinks::All) {
Ok(_) => Ok(true),
Err(Ext4Error::NotFound) => Ok(false),
Err(err) => Err(err),
}
}
inner(self, path.try_into().map_err(|_| Ext4Error::MalformedPath)?)
}
pub fn metadata<'p, P>(&self, path: P) -> Result<Metadata, Ext4Error>
where
P: TryInto<Path<'p>>,
{
fn inner(fs: &Ext4, path: Path<'_>) -> Result<Metadata, Ext4Error> {
let inode = fs.path_to_inode(path, FollowSymlinks::All)?;
Ok(inode.metadata)
}
inner(self, path.try_into().map_err(|_| Ext4Error::MalformedPath)?)
}
pub fn symlink_metadata<'p, P>(
&self,
path: P,
) -> Result<Metadata, Ext4Error>
where
P: TryInto<Path<'p>>,
{
fn inner(fs: &Ext4, path: Path<'_>) -> Result<Metadata, Ext4Error> {
let inode =
fs.path_to_inode(path, FollowSymlinks::ExcludeFinalComponent)?;
Ok(inode.metadata)
}
inner(self, path.try_into().map_err(|_| Ext4Error::MalformedPath)?)
}
}
impl Debug for Ext4 {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.debug_struct("Ext4")
.field("superblock", &self.0.superblock)
.field("block_group_descriptors", &self.0.block_group_descriptors)
.finish_non_exhaustive()
}
}
#[cfg(feature = "std")]
#[cfg(test)]
mod tests {
use super::*;
use test_util::load_test_disk1;
#[test]
fn test_load_errors() {
assert!(matches!(
Ext4::load(Box::new(vec![])).unwrap_err(),
Ext4Error::Io(_)
));
assert_eq!(
Ext4::load(Box::new(vec![0; 2048])).unwrap_err(),
CorruptKind::SuperblockMagic
);
let mut fs_data = vec![0; 2048];
fs_data[1024..2048]
.copy_from_slice(include_bytes!("../test_data/raw_superblock.bin"));
assert!(matches!(
Ext4::load(Box::new(fs_data.clone())).unwrap_err(),
Ext4Error::Io(_)
));
fs_data.resize(3048usize, 0u8);
assert_eq!(
Ext4::load(Box::new(fs_data.clone())).unwrap_err(),
CorruptKind::BlockGroupDescriptorChecksum(0)
);
}
#[test]
fn test_invalid_ext4_data() {
let mut data = vec![0; 1024];
data.extend(include_bytes!("../test_data/not_ext4.bin"));
assert_eq!(
Ext4::load(Box::new(data)).unwrap_err(),
CorruptKind::InvalidBlockSize
);
}
fn block_read_error(
block_index: FsBlockIndex,
offset_within_block: u32,
read_len: usize,
) -> CorruptKind {
CorruptKind::BlockRead {
block_index,
original_block_index: block_index,
offset_within_block,
read_len,
}
}
#[test]
fn test_read_from_block_first_1024() {
let fs = load_test_disk1();
let mut dst = vec![0; 1];
assert_eq!(
fs.read_from_block(0, 1023, &mut dst).unwrap_err(),
block_read_error(0, 1023, 1),
);
}
#[test]
fn test_read_from_block_past_file_end() {
let fs = load_test_disk1();
let mut dst = vec![0; 1024];
assert_eq!(
fs.read_from_block(999_999_999, 0, &mut dst).unwrap_err(),
block_read_error(999_999_999, 0, 1024),
);
}
#[test]
fn test_read_from_block_invalid_offset() {
let fs = load_test_disk1();
let mut dst = vec![0; 1024];
assert_eq!(
fs.read_from_block(1, 1024, &mut dst).unwrap_err(),
block_read_error(1, 1024, 1024),
);
}
#[test]
fn test_read_from_block_past_block_end() {
let fs = load_test_disk1();
let mut dst = vec![0; 25];
assert_eq!(
fs.read_from_block(1, 1000, &mut dst).unwrap_err(),
block_read_error(1, 1000, 25),
);
}
#[test]
fn test_path_to_inode() {
let fs = load_test_disk1();
let follow = FollowSymlinks::All;
let inode = fs
.path_to_inode(Path::try_from("/").unwrap(), follow)
.unwrap();
assert_eq!(inode.index.get(), 2);
assert!(
fs.path_to_inode(Path::try_from("/empty_file").unwrap(), follow)
.is_ok()
);
assert!(
fs.path_to_inode(Path::try_from("/./empty_file").unwrap(), follow)
.is_ok()
);
let inode = fs
.path_to_inode(Path::try_from("/empty_dir/..").unwrap(), follow)
.unwrap();
assert_eq!(inode.index.get(), 2);
assert!(
fs.path_to_inode(Path::try_from("/sym_simple").unwrap(), follow)
.is_ok()
);
assert!(
fs.path_to_inode(Path::try_from("empty_file").unwrap(), follow)
.is_err()
);
assert!(
fs.path_to_inode(
Path::try_from("/empty_dir/does_not_exist").unwrap(),
follow
)
.is_err()
);
assert!(
fs.path_to_inode(
Path::try_from("/empty_file/does_not_exist").unwrap(),
follow
)
.is_err()
);
}
}