use crate::error::{Corrupt, Ext4Error};
use crate::file_type::FileType;
use crate::format::{format_bytes_debug, BytesDisplay};
use crate::inode::{Inode, InodeIndex};
use crate::metadata::Metadata;
use crate::path::{Path, PathBuf};
use crate::util::{read_u16le, read_u32le};
use crate::Ext4;
use alloc::rc::Rc;
use core::error::Error;
use core::fmt::{self, Debug, Display, Formatter};
use core::hash::{Hash, Hasher};
use core::str::Utf8Error;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum DirEntryNameError {
Empty,
TooLong,
ContainsNull,
ContainsSeparator,
}
impl Display for DirEntryNameError {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
Self::Empty => write!(f, "direntry name is empty"),
Self::TooLong => {
write!(f, "directory entry name is longer than 255 bytes")
}
Self::ContainsNull => {
write!(f, "directory entry name contains a null byte")
}
Self::ContainsSeparator => {
write!(f, "directory entry name contains a path separator")
}
}
}
}
impl Error for DirEntryNameError {}
#[derive(Clone, Copy, Eq, Ord, PartialOrd, Hash)]
pub struct DirEntryName<'a>(pub(crate) &'a [u8]);
impl<'a> DirEntryName<'a> {
pub const MAX_LEN: usize = 255;
#[inline]
pub fn as_str(&self) -> Result<&'a str, Utf8Error> {
core::str::from_utf8(self.0)
}
pub fn display(&self) -> BytesDisplay {
BytesDisplay(self.0)
}
}
impl<'a> AsRef<[u8]> for DirEntryName<'a> {
fn as_ref(&self) -> &'a [u8] {
self.0
}
}
impl<'a> Debug for DirEntryName<'a> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
format_bytes_debug(self.0, f)
}
}
impl<'a, T> PartialEq<T> for DirEntryName<'a>
where
T: AsRef<[u8]>,
{
fn eq(&self, other: &T) -> bool {
self.0 == other.as_ref()
}
}
impl<'a> TryFrom<&'a [u8]> for DirEntryName<'a> {
type Error = DirEntryNameError;
fn try_from(bytes: &'a [u8]) -> Result<Self, DirEntryNameError> {
if bytes.is_empty() {
Err(DirEntryNameError::Empty)
} else if bytes.len() > Self::MAX_LEN {
Err(DirEntryNameError::TooLong)
} else if bytes.contains(&0) {
Err(DirEntryNameError::ContainsNull)
} else if bytes.contains(&Path::SEPARATOR) {
Err(DirEntryNameError::ContainsSeparator)
} else {
Ok(Self(bytes))
}
}
}
impl<'a, const N: usize> TryFrom<&'a [u8; N]> for DirEntryName<'a> {
type Error = DirEntryNameError;
fn try_from(bytes: &'a [u8; N]) -> Result<Self, DirEntryNameError> {
Self::try_from(bytes.as_slice())
}
}
impl<'a> TryFrom<&'a str> for DirEntryName<'a> {
type Error = DirEntryNameError;
fn try_from(s: &'a str) -> Result<Self, DirEntryNameError> {
Self::try_from(s.as_bytes())
}
}
#[derive(Clone, Eq, Ord, PartialOrd)]
struct DirEntryNameBuf {
data: [u8; DirEntryName::MAX_LEN],
len: u8,
}
impl DirEntryNameBuf {
#[inline]
#[must_use]
fn as_bytes(&self) -> &[u8] {
&self.data[..usize::from(self.len)]
}
#[inline]
#[must_use]
fn as_dir_entry_name(&self) -> DirEntryName<'_> {
DirEntryName(self.as_bytes())
}
}
impl Debug for DirEntryNameBuf {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
format_bytes_debug(self.as_bytes(), f)
}
}
impl PartialEq<Self> for DirEntryNameBuf {
fn eq(&self, other: &Self) -> bool {
self.as_bytes() == other.as_bytes()
}
}
impl Hash for DirEntryNameBuf {
fn hash<H>(&self, hasher: &mut H)
where
H: Hasher,
{
self.as_bytes().hash(hasher);
}
}
impl TryFrom<&[u8]> for DirEntryNameBuf {
type Error = DirEntryNameError;
fn try_from(bytes: &[u8]) -> Result<Self, DirEntryNameError> {
DirEntryName::try_from(bytes)?;
let mut name = Self {
data: [0; DirEntryName::MAX_LEN],
len: u8::try_from(bytes.len()).unwrap(),
};
name.data[..bytes.len()].copy_from_slice(bytes);
Ok(name)
}
}
#[derive(Clone, Debug)]
pub struct DirEntry {
fs: Ext4,
pub(crate) inode: InodeIndex,
name: DirEntryNameBuf,
path: Rc<PathBuf>,
file_type: FileType,
}
impl DirEntry {
pub(crate) fn from_bytes(
fs: Ext4,
bytes: &[u8],
inode: InodeIndex,
path: Rc<PathBuf>,
) -> Result<(Option<Self>, usize), Ext4Error> {
const NAME_OFFSET: usize = 8;
let err = || Ext4Error::Corrupt(Corrupt::DirEntry(inode.get()));
if bytes.len() < NAME_OFFSET {
return Err(err());
}
let points_to_inode = read_u32le(bytes, 0);
let rec_len = read_u16le(bytes, 4);
let rec_len = usize::from(rec_len);
if rec_len < NAME_OFFSET {
return Err(err());
}
let Some(points_to_inode) = InodeIndex::new(points_to_inode) else {
return Ok((None, rec_len));
};
let name_len = *bytes.get(6).unwrap();
let name_len_usize = usize::from(name_len);
let name_slice = bytes
.get(NAME_OFFSET..NAME_OFFSET + name_len_usize)
.ok_or(err())?;
let file_type = bytes[7];
let file_type = FileType::from_dir_entry(file_type)
.map_err(|_| Ext4Error::Corrupt(Corrupt::DirEntry(inode.get())))?;
let name = DirEntryNameBuf::try_from(name_slice).map_err(|_| err())?;
let entry = Self {
fs,
inode: points_to_inode,
name,
path,
file_type,
};
Ok((Some(entry), rec_len))
}
#[must_use]
#[inline]
pub fn file_name(&self) -> DirEntryName<'_> {
self.name.as_dir_entry_name()
}
#[must_use]
pub fn path(&self) -> PathBuf {
self.path.join(self.name.as_bytes())
}
pub fn file_type(&self) -> Result<FileType, Ext4Error> {
Ok(self.file_type)
}
pub fn metadata(&self) -> Result<Metadata, Ext4Error> {
let inode = Inode::read(&self.fs, self.inode)?;
Ok(inode.metadata)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::hash::DefaultHasher;
#[test]
fn test_dir_entry_debug() {
let src = "abc😁\n".as_bytes();
let expected = "abc😁\\n"; assert_eq!(format!("{:?}", DirEntryName(src)), expected);
let mut src_vec = src.to_vec();
src_vec.resize(255, 0);
assert_eq!(
format!(
"{:?}",
DirEntryNameBuf {
data: src_vec.try_into().unwrap(),
len: src.len().try_into().unwrap(),
}
),
expected
);
}
#[test]
fn test_dir_entry_display() {
let name = DirEntryName([0xc3, 0x28].as_slice());
assert_eq!(format!("{}", name.display()), "�(");
}
#[test]
fn test_dir_entry_construction() {
let expected_name = DirEntryName(b"abc");
let mut v = b"abc".to_vec();
v.resize(255, 0);
let expected_name_buf = DirEntryNameBuf {
data: v.try_into().unwrap(),
len: 3,
};
let src: &[u8] = b"abc";
assert_eq!(DirEntryName::try_from(src).unwrap(), expected_name);
assert_eq!(DirEntryNameBuf::try_from(src).unwrap(), expected_name_buf);
let src: &str = "abc";
assert_eq!(DirEntryName::try_from(src).unwrap(), expected_name);
let src: &[u8; 3] = b"abc";
assert_eq!(DirEntryName::try_from(src).unwrap(), expected_name);
let src: &[u8] = b"";
assert_eq!(DirEntryName::try_from(src), Err(DirEntryNameError::Empty));
assert_eq!(
DirEntryNameBuf::try_from(src),
Err(DirEntryNameError::Empty)
);
let src: &[u8] = [1; 256].as_slice();
assert_eq!(
DirEntryName::try_from(src),
Err(DirEntryNameError::TooLong)
);
assert_eq!(
DirEntryNameBuf::try_from(src),
Err(DirEntryNameError::TooLong)
);
let src: &[u8] = b"\0".as_slice();
assert_eq!(
DirEntryName::try_from(src),
Err(DirEntryNameError::ContainsNull)
);
assert_eq!(
DirEntryNameBuf::try_from(src),
Err(DirEntryNameError::ContainsNull)
);
let src: &[u8] = b"/".as_slice();
assert_eq!(
DirEntryName::try_from(src),
Err(DirEntryNameError::ContainsSeparator)
);
assert_eq!(
DirEntryNameBuf::try_from(src),
Err(DirEntryNameError::ContainsSeparator)
);
}
#[test]
fn test_dir_entry_name_buf_hash() {
fn get_hash<T: Hash>(v: T) -> u64 {
let mut s = DefaultHasher::new();
v.hash(&mut s);
s.finish()
}
let name = DirEntryNameBuf::try_from(b"abc".as_slice()).unwrap();
assert_eq!(get_hash(name), get_hash(b"abc"));
}
#[cfg(feature = "std")]
#[test]
fn test_dir_entry_from_bytes() {
let fs = crate::load_test_disk1();
let inode1 = InodeIndex::new(1).unwrap();
let inode2 = InodeIndex::new(2).unwrap();
let path = Rc::new(PathBuf::new("path"));
let mut bytes = Vec::new();
bytes.extend(2u32.to_le_bytes()); bytes.extend(72u16.to_le_bytes()); bytes.push(3u8); bytes.push(1u8); bytes.extend("abc".bytes()); bytes.resize(72, 0u8);
let (entry, len) =
DirEntry::from_bytes(fs.clone(), &bytes, inode1, path.clone())
.unwrap();
let entry = entry.unwrap();
assert_eq!(len, 72);
assert_eq!(entry.inode, inode2);
assert_eq!(
entry.name,
DirEntryNameBuf::try_from("abc".as_bytes()).unwrap()
);
assert_eq!(entry.path, path);
assert_eq!(entry.file_type, FileType::Regular);
assert_eq!(entry.file_name(), "abc");
assert_eq!(entry.path(), "path/abc");
let mut bytes = Vec::new();
bytes.extend(0u32.to_le_bytes()); bytes.extend(72u16.to_le_bytes()); bytes.resize(72, 0u8);
let (entry, len) =
DirEntry::from_bytes(fs.clone(), &bytes, inode1, path.clone())
.unwrap();
assert!(entry.is_none());
assert_eq!(len, 72);
let err = DirEntry::from_bytes(fs.clone(), &[], inode1, path.clone())
.unwrap_err();
assert_eq!(*err.as_corrupt().unwrap(), Corrupt::DirEntry(1));
let mut bytes = Vec::new();
bytes.extend(2u32.to_le_bytes()); bytes.extend(72u16.to_le_bytes()); bytes.push(3u8); bytes.push(8u8); bytes.extend("a".bytes()); assert!(
DirEntry::from_bytes(fs.clone(), &bytes, inode1, path.clone())
.is_err()
);
let mut bytes = Vec::new();
bytes.extend(2u32.to_le_bytes()); bytes.extend(72u16.to_le_bytes()); bytes.push(3u8); bytes.push(8u8); bytes.extend("ab/".bytes()); bytes.resize(72, 0u8);
assert!(DirEntry::from_bytes(fs.clone(), &bytes, inode1, path).is_err());
}
#[test]
fn test_dir_entry_name_as_ref() {
let name = DirEntryName::try_from(b"abc".as_slice()).unwrap();
let bytes: &[u8] = name.as_ref();
assert_eq!(bytes, b"abc");
}
#[test]
fn test_dir_entry_name_partial_eq() {
let name = DirEntryName::try_from(b"abc".as_slice()).unwrap();
assert_eq!(name, name);
let v: &str = "abc";
assert_eq!(name, v);
let v: &[u8] = b"abc";
assert_eq!(name, v);
let v: &[u8; 3] = b"abc";
assert_eq!(name, v);
}
#[test]
fn test_dir_entry_name_buf_as_dir_entry_name() {
let name = DirEntryNameBuf::try_from(b"abc".as_slice()).unwrap();
let r: DirEntryName<'_> = name.as_dir_entry_name();
assert_eq!(r, "abc");
}
#[test]
fn test_dir_entry_name_as_str() {
let name = DirEntryName::try_from(b"abc".as_slice()).unwrap();
assert_eq!(name.as_str().unwrap(), "abc");
let name = DirEntryName([0xc3, 0x28].as_slice());
assert!(name.as_str().is_err());
}
}