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