Skip to main content

ext4_view/
dir_entry.rs

1// Copyright 2024 Google LLC
2//
3// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
4// https://www.apache.org/licenses/LICENSE-2.0> or the MIT license
5// <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your
6// option. This file may not be copied, modified, or distributed
7// except according to those terms.
8
9use crate::Ext4;
10use crate::error::{CorruptKind, Ext4Error};
11use crate::file_type::FileType;
12use crate::format::{BytesDisplay, format_bytes_debug};
13use crate::inode::{Inode, InodeIndex};
14use crate::metadata::Metadata;
15use crate::path::{Path, PathBuf};
16use crate::util::{read_u16le, read_u32le};
17use alloc::rc::Rc;
18use core::error::Error;
19use core::fmt::{self, Debug, Display, Formatter};
20use core::hash::{Hash, Hasher};
21use core::num::NonZero;
22use core::str::Utf8Error;
23
24/// Error returned when [`DirEntryName`] construction fails.
25#[derive(Clone, Copy, Debug, Eq, PartialEq)]
26#[non_exhaustive]
27pub enum DirEntryNameError {
28    /// Name is empty.
29    Empty,
30
31    /// Name is longer than [`DirEntryName::MAX_LEN`].
32    TooLong,
33
34    /// Name contains a null byte.
35    ContainsNull,
36
37    /// Name contains a path separator.
38    ContainsSeparator,
39}
40
41impl Display for DirEntryNameError {
42    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
43        match self {
44            Self::Empty => write!(f, "direntry name is empty"),
45            Self::TooLong => {
46                write!(f, "directory entry name is longer than 255 bytes")
47            }
48            Self::ContainsNull => {
49                write!(f, "directory entry name contains a null byte")
50            }
51            Self::ContainsSeparator => {
52                write!(f, "directory entry name contains a path separator")
53            }
54        }
55    }
56}
57
58impl Error for DirEntryNameError {}
59
60/// Name of a [`DirEntry`], stored as a reference.
61///
62/// This is guaranteed at construction to be a valid directory entry
63/// name.
64#[derive(Clone, Copy, Eq, Ord, PartialOrd, Hash)]
65pub struct DirEntryName<'a>(pub(crate) &'a [u8]);
66
67impl<'a> DirEntryName<'a> {
68    /// Maximum length of a `DirEntryName`.
69    pub const MAX_LEN: usize = 255;
70
71    /// Convert to a `&str` if the name is valid UTF-8.
72    #[inline]
73    pub fn as_str(&self) -> Result<&'a str, Utf8Error> {
74        core::str::from_utf8(self.0)
75    }
76
77    /// Get an object that implements [`Display`] to allow conveniently
78    /// printing names that may or may not be valid UTF-8. Non-UTF-8
79    /// characters will be replaced with '�'.
80    ///
81    /// [`Display`]: core::fmt::Display
82    pub fn display(&self) -> BytesDisplay<'_> {
83        BytesDisplay(self.0)
84    }
85}
86
87impl<'a> AsRef<[u8]> for DirEntryName<'a> {
88    fn as_ref(&self) -> &'a [u8] {
89        self.0
90    }
91}
92
93impl Debug for DirEntryName<'_> {
94    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
95        format_bytes_debug(self.0, f)
96    }
97}
98
99impl<T> PartialEq<T> for DirEntryName<'_>
100where
101    T: AsRef<[u8]>,
102{
103    fn eq(&self, other: &T) -> bool {
104        self.0 == other.as_ref()
105    }
106}
107
108impl<'a> TryFrom<&'a [u8]> for DirEntryName<'a> {
109    type Error = DirEntryNameError;
110
111    fn try_from(bytes: &'a [u8]) -> Result<Self, DirEntryNameError> {
112        if bytes.is_empty() {
113            Err(DirEntryNameError::Empty)
114        } else if bytes.len() > Self::MAX_LEN {
115            Err(DirEntryNameError::TooLong)
116        } else if bytes.contains(&0) {
117            Err(DirEntryNameError::ContainsNull)
118        } else if bytes.contains(&Path::SEPARATOR) {
119            Err(DirEntryNameError::ContainsSeparator)
120        } else {
121            Ok(Self(bytes))
122        }
123    }
124}
125
126impl<'a, const N: usize> TryFrom<&'a [u8; N]> for DirEntryName<'a> {
127    type Error = DirEntryNameError;
128
129    fn try_from(bytes: &'a [u8; N]) -> Result<Self, DirEntryNameError> {
130        Self::try_from(bytes.as_slice())
131    }
132}
133
134impl<'a> TryFrom<&'a str> for DirEntryName<'a> {
135    type Error = DirEntryNameError;
136
137    fn try_from(s: &'a str) -> Result<Self, DirEntryNameError> {
138        Self::try_from(s.as_bytes())
139    }
140}
141
142#[derive(Clone, Eq, Ord, PartialOrd)]
143struct DirEntryNameBuf {
144    data: [u8; DirEntryName::MAX_LEN],
145    len: u8,
146}
147
148impl DirEntryNameBuf {
149    #[inline]
150    #[must_use]
151    fn as_bytes(&self) -> &[u8] {
152        &self.data[..usize::from(self.len)]
153    }
154
155    #[inline]
156    #[must_use]
157    fn as_dir_entry_name(&self) -> DirEntryName<'_> {
158        DirEntryName(self.as_bytes())
159    }
160}
161
162impl Debug for DirEntryNameBuf {
163    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
164        format_bytes_debug(self.as_bytes(), f)
165    }
166}
167
168// Manual implementation of `PartialEq` because we don't want to compare
169// the entire `data` array, only up to `len`.
170impl PartialEq<Self> for DirEntryNameBuf {
171    fn eq(&self, other: &Self) -> bool {
172        self.as_bytes() == other.as_bytes()
173    }
174}
175
176// Manual implementation of `Hash` because we don't want to include the
177// entire `data` array, only up to `len` (see also `PartialEq` impl).
178impl Hash for DirEntryNameBuf {
179    fn hash<H>(&self, hasher: &mut H)
180    where
181        H: Hasher,
182    {
183        self.as_bytes().hash(hasher);
184    }
185}
186
187impl TryFrom<&[u8]> for DirEntryNameBuf {
188    type Error = DirEntryNameError;
189
190    fn try_from(bytes: &[u8]) -> Result<Self, DirEntryNameError> {
191        // This performs all the necessary validation of the input.
192        DirEntryName::try_from(bytes)?;
193
194        let mut name = Self {
195            data: [0; DirEntryName::MAX_LEN],
196            // OK to unwrap: already checked against `MAX_LEN`.
197            len: u8::try_from(bytes.len()).unwrap(),
198        };
199        name.data[..bytes.len()].copy_from_slice(bytes);
200        Ok(name)
201    }
202}
203
204/// Directory entry.
205#[derive(Clone)]
206pub struct DirEntry {
207    fs: Ext4,
208
209    /// Number of the inode that this entry points to.
210    pub(crate) inode: InodeIndex,
211
212    /// Raw name of the entry.
213    name: DirEntryNameBuf,
214
215    /// Path that `read_dir` was called with. This is shared via `Rc` so
216    /// that only one allocation is required.
217    path: Rc<PathBuf>,
218
219    /// Entry file type.
220    file_type: FileType,
221}
222
223impl DirEntry {
224    /// Read a `DirEntry` from a byte slice.
225    ///
226    /// If no error occurs, this returns `(Option<DirEntry>, usize)`.
227    /// * The first value in this tuple is an `Option` because some
228    ///   special data is stored in directory blocks that aren't
229    ///   actually directory entries. If the inode pointed to by the
230    ///   entry is zero, this value is set to None.
231    /// * The `NonZero<usize>` in this tuple is the overall length of
232    ///   the entry's data. This is used when iterating over raw dir
233    ///   entry data. A `NonZero` is used because it's important for
234    ///   iterators that call `DirEntry::from_bytes` to make forward
235    ///   progress. A length of zero would cause them to enter an
236    ///   infinite loop.
237    pub(crate) fn from_bytes(
238        fs: Ext4,
239        bytes: &[u8],
240        inode: InodeIndex,
241        path: Rc<PathBuf>,
242    ) -> Result<(Option<Self>, NonZero<usize>), Ext4Error> {
243        const NAME_OFFSET: usize = 8;
244
245        // Check size (the full entry will usually be larger than this),
246        // but these header fields must be present.
247        if bytes.len() < NAME_OFFSET {
248            return Err(
249                CorruptKind::DirEntryMissingHeader(inode, bytes.len()).into()
250            );
251        }
252
253        // Get the inode that this entry points to. If zero, this is a
254        // special type of entry (such as a checksum entry or hash tree
255        // node entry).
256        let points_to_inode = read_u32le(bytes, 0);
257
258        // Get the full size of the entry.
259        let rec_len = read_u16le(bytes, 4);
260        let rec_len = usize::from(rec_len);
261
262        // Check that the rec_len is somewhat reasonable. Too small a
263        // value could indicate the wrong data is being read. And
264        // notably, a value of zero would cause an infinite loop when
265        // iterating over entries.
266        if rec_len < NAME_OFFSET {
267            return Err(
268                CorruptKind::DirEntryRecordTooSmall(inode, rec_len).into()
269            );
270        }
271        // OK to unwrap: above check ensures that `rec_len >= NAME_OFFSET`.
272        let rec_len = NonZero::new(rec_len).unwrap();
273
274        // As described above, an inode of zero is used for special
275        // entries. Return early since the rest of the fields won't be
276        // valid.
277        let Some(points_to_inode) = InodeIndex::new(points_to_inode) else {
278            return Ok((None, rec_len));
279        };
280
281        // Get the size of the entry's name field.
282        // OK to unwrap: already checked length.
283        let name_len = *bytes.get(6).unwrap();
284        let name_len_usize = usize::from(name_len);
285
286        // OK to unwrap: `NAME_OFFSET` is 8 and `name_len_usize` is
287        // at most 255, so the result fits in a `u16`, which is the
288        // minimum size of `usize`.
289        let name_end: usize = NAME_OFFSET.checked_add(name_len_usize).unwrap();
290
291        // Get the entry's name.
292        let name_slice = bytes
293            .get(NAME_OFFSET..name_end)
294            .ok_or(CorruptKind::DirEntryNameTooLarge(inode, name_len))?;
295
296        // Note: this value is only valid if `FILE_TYPE_IN_DIR_ENTRY` is
297        // in the incompatible features set. That requirement is checked
298        // when reading the superblock.
299        //
300        // This requirement could be relaxed in the future by passing in
301        // a filesystem reference and reading the pointed-to inode.
302        let file_type = bytes[7];
303        let file_type = FileType::from_dir_entry(file_type).map_err(|_| {
304            CorruptKind::DirEntryInvalidFileType(inode, file_type)
305        })?;
306
307        let name = DirEntryNameBuf::try_from(name_slice)
308            .map_err(|e| CorruptKind::DirEntryInvalidName(inode, e))?;
309        let entry = Self {
310            fs,
311            inode: points_to_inode,
312            name,
313            path,
314            file_type,
315        };
316        Ok((Some(entry), rec_len))
317    }
318
319    /// Get the directory entry's name.
320    #[must_use]
321    #[inline]
322    pub fn file_name(&self) -> DirEntryName<'_> {
323        self.name.as_dir_entry_name()
324    }
325
326    /// Get the entry's path.
327    ///
328    /// This appends the entry's name to the path that `Ext4::read_dir`
329    /// was called with.
330    #[must_use]
331    pub fn path(&self) -> PathBuf {
332        self.path.join(self.name.as_bytes())
333    }
334
335    /// Get the entry's file type.
336    pub fn file_type(&self) -> Result<FileType, Ext4Error> {
337        // Currently this function cannot fail, but return a `Result` to
338        // preserve that option for the future (may be needed for
339        // filesystems without `FILE_TYPE_IN_DIR_ENTRY`). This also
340        // matches the `std::fs::DirEntry` API.
341        Ok(self.file_type)
342    }
343
344    /// Get [`Metadata`] for the entry.
345    ///
346    /// If the entry is a symlink, metadata for the symlink itself will
347    /// be returned, not the symlink target.
348    pub fn metadata(&self) -> Result<Metadata, Ext4Error> {
349        let inode = Inode::read(&self.fs, self.inode)?;
350        Ok(inode.metadata)
351    }
352}
353
354impl Debug for DirEntry {
355    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
356        f.debug_tuple("DirEntry").field(&self.path()).finish()
357    }
358}
359
360#[cfg(test)]
361mod tests {
362    use super::*;
363    use std::hash::DefaultHasher;
364
365    #[test]
366    fn test_dir_entry_debug() {
367        let src = "abc😁\n".as_bytes();
368        let expected = r#""abc😁\n""#; // Note the escaped slash.
369        assert_eq!(format!("{:?}", DirEntryName(src)), expected);
370
371        let mut src_vec = src.to_vec();
372        src_vec.resize(255, 0);
373        assert_eq!(
374            format!(
375                "{:?}",
376                DirEntryNameBuf {
377                    data: src_vec.try_into().unwrap(),
378                    len: src.len().try_into().unwrap(),
379                }
380            ),
381            expected
382        );
383    }
384
385    #[test]
386    fn test_dir_entry_display() {
387        let name = DirEntryName([0xc3, 0x28].as_slice());
388        assert_eq!(format!("{}", name.display()), "�(");
389    }
390
391    #[test]
392    fn test_dir_entry_construction() {
393        let expected_name = DirEntryName(b"abc");
394        let mut v = b"abc".to_vec();
395        v.resize(255, 0);
396        let expected_name_buf = DirEntryNameBuf {
397            data: v.try_into().unwrap(),
398            len: 3,
399        };
400
401        // Successful construction from a byte slice.
402        let src: &[u8] = b"abc";
403        assert_eq!(DirEntryName::try_from(src).unwrap(), expected_name);
404        assert_eq!(DirEntryNameBuf::try_from(src).unwrap(), expected_name_buf);
405
406        // Successful construction from a string.
407        let src: &str = "abc";
408        assert_eq!(DirEntryName::try_from(src).unwrap(), expected_name);
409
410        // Successful construction from a byte array.
411        let src: &[u8; 3] = b"abc";
412        assert_eq!(DirEntryName::try_from(src).unwrap(), expected_name);
413
414        // Error: empty.
415        let src: &[u8] = b"";
416        assert_eq!(DirEntryName::try_from(src), Err(DirEntryNameError::Empty));
417        assert_eq!(
418            DirEntryNameBuf::try_from(src),
419            Err(DirEntryNameError::Empty)
420        );
421
422        // Error: too long.
423        let src: &[u8] = [1; 256].as_slice();
424        assert_eq!(
425            DirEntryName::try_from(src),
426            Err(DirEntryNameError::TooLong)
427        );
428        assert_eq!(
429            DirEntryNameBuf::try_from(src),
430            Err(DirEntryNameError::TooLong)
431        );
432
433        // Error:: contains null.
434        let src: &[u8] = b"\0".as_slice();
435        assert_eq!(
436            DirEntryName::try_from(src),
437            Err(DirEntryNameError::ContainsNull)
438        );
439        assert_eq!(
440            DirEntryNameBuf::try_from(src),
441            Err(DirEntryNameError::ContainsNull)
442        );
443
444        // Error: contains separator.
445        let src: &[u8] = b"/".as_slice();
446        assert_eq!(
447            DirEntryName::try_from(src),
448            Err(DirEntryNameError::ContainsSeparator)
449        );
450        assert_eq!(
451            DirEntryNameBuf::try_from(src),
452            Err(DirEntryNameError::ContainsSeparator)
453        );
454    }
455
456    #[test]
457    fn test_dir_entry_name_buf_hash() {
458        fn get_hash<T: Hash>(v: T) -> u64 {
459            let mut s = DefaultHasher::new();
460            v.hash(&mut s);
461            s.finish()
462        }
463
464        let name = DirEntryNameBuf::try_from(b"abc".as_slice()).unwrap();
465        assert_eq!(get_hash(name), get_hash(b"abc"));
466    }
467
468    #[cfg(feature = "std")]
469    #[test]
470    fn test_dir_entry_from_bytes() {
471        let fs = crate::test_util::load_test_disk1();
472
473        let inode1 = InodeIndex::new(1).unwrap();
474        let inode2 = InodeIndex::new(2).unwrap();
475        let path = Rc::new(PathBuf::new("path"));
476
477        // Read a normal entry.
478        let mut bytes = Vec::new();
479        bytes.extend(2u32.to_le_bytes()); // inode
480        bytes.extend(72u16.to_le_bytes()); // record length
481        bytes.push(3u8); // name length
482        bytes.push(1u8); // file type
483        bytes.extend("abc".bytes()); // name
484        bytes.resize(72, 0u8);
485        let (entry, len) =
486            DirEntry::from_bytes(fs.clone(), &bytes, inode1, path.clone())
487                .unwrap();
488        let entry = entry.unwrap();
489        assert_eq!(len.get(), 72);
490        assert_eq!(entry.inode, inode2);
491        assert_eq!(
492            entry.name,
493            DirEntryNameBuf::try_from("abc".as_bytes()).unwrap()
494        );
495        assert_eq!(entry.path, path);
496        assert_eq!(entry.file_type, FileType::Regular);
497        assert_eq!(entry.file_name(), "abc");
498        assert_eq!(entry.path(), "path/abc");
499
500        // Special entry: inode is zero.
501        let mut bytes = Vec::new();
502        bytes.extend(0u32.to_le_bytes()); // inode
503        bytes.extend(72u16.to_le_bytes()); // record length
504        bytes.resize(72, 0u8);
505        let (entry, len) =
506            DirEntry::from_bytes(fs.clone(), &bytes, inode1, path.clone())
507                .unwrap();
508        assert!(entry.is_none());
509        assert_eq!(len.get(), 72);
510
511        // Error: not enough data for the header.
512        assert_eq!(
513            DirEntry::from_bytes(fs.clone(), &[], inode1, path.clone())
514                .unwrap_err(),
515            CorruptKind::DirEntryMissingHeader(inode1, 0)
516        );
517
518        // Error: not enough data for a full record.
519        let mut bytes = Vec::new();
520        bytes.extend(2u32.to_le_bytes()); // inode
521        bytes.extend(7u16.to_le_bytes()); // record length
522        bytes.resize(72, 0u8);
523        assert_eq!(
524            DirEntry::from_bytes(fs.clone(), &bytes, inode1, path.clone())
525                .unwrap_err(),
526            CorruptKind::DirEntryRecordTooSmall(inode1, 7)
527        );
528
529        // Error: invalid file type.
530        let mut bytes = Vec::new();
531        bytes.extend(2u32.to_le_bytes()); // inode
532        bytes.extend(72u16.to_le_bytes()); // record length
533        bytes.push(3u8); // name length
534        bytes.push(123u8); // file type
535        bytes.extend("abc".bytes()); // name
536        bytes.resize(72, 0u8);
537        assert_eq!(
538            DirEntry::from_bytes(fs.clone(), &bytes, inode1, path.clone())
539                .unwrap_err(),
540            CorruptKind::DirEntryInvalidFileType(inode1, 123),
541        );
542
543        // Error: not enough data for the name.
544        let mut bytes = Vec::new();
545        bytes.extend(2u32.to_le_bytes()); // inode
546        bytes.extend(72u16.to_le_bytes()); // record length
547        bytes.push(3u8); // name length
548        bytes.push(1u8); // file type
549        bytes.extend("a".bytes()); // name
550        assert_eq!(
551            DirEntry::from_bytes(fs.clone(), &bytes, inode1, path.clone())
552                .unwrap_err(),
553            CorruptKind::DirEntryNameTooLarge(inode1, 3),
554        );
555
556        // Error: name contains invalid characters.
557        let mut bytes = Vec::new();
558        bytes.extend(2u32.to_le_bytes()); // inode
559        bytes.extend(72u16.to_le_bytes()); // record length
560        bytes.push(3u8); // name length
561        bytes.push(1u8); // file type
562        bytes.extend("ab/".bytes()); // name
563        bytes.resize(72, 0u8);
564        assert_eq!(
565            DirEntry::from_bytes(fs.clone(), &bytes, inode1, path).unwrap_err(),
566            CorruptKind::DirEntryInvalidName(
567                inode1,
568                DirEntryNameError::ContainsSeparator
569            ),
570        );
571    }
572
573    #[test]
574    fn test_dir_entry_name_as_ref() {
575        let name = DirEntryName::try_from(b"abc".as_slice()).unwrap();
576        let bytes: &[u8] = name.as_ref();
577        assert_eq!(bytes, b"abc");
578    }
579
580    #[test]
581    fn test_dir_entry_name_partial_eq() {
582        let name = DirEntryName::try_from(b"abc".as_slice()).unwrap();
583        assert_eq!(name, name);
584
585        let v: &str = "abc";
586        assert_eq!(name, v);
587
588        let v: &[u8] = b"abc";
589        assert_eq!(name, v);
590
591        let v: &[u8; 3] = b"abc";
592        assert_eq!(name, v);
593    }
594
595    #[test]
596    fn test_dir_entry_name_buf_as_dir_entry_name() {
597        let name = DirEntryNameBuf::try_from(b"abc".as_slice()).unwrap();
598        let r: DirEntryName<'_> = name.as_dir_entry_name();
599        assert_eq!(r, "abc");
600    }
601
602    #[test]
603    fn test_dir_entry_name_as_str() {
604        let name = DirEntryName::try_from(b"abc".as_slice()).unwrap();
605        assert_eq!(name.as_str().unwrap(), "abc");
606
607        let name = DirEntryName([0xc3, 0x28].as_slice());
608        assert!(name.as_str().is_err());
609    }
610}