fdf/
filetype.rs

1use crate::BytePath as _;
2
3use libc::{
4    DT_BLK, DT_CHR, DT_DIR, DT_FIFO, DT_LNK, DT_REG, DT_SOCK, S_IFBLK, S_IFCHR, S_IFDIR, S_IFIFO,
5    S_IFLNK, S_IFMT, S_IFREG, S_IFSOCK, mode_t,
6};
7
8use std::{os::unix::fs::FileTypeExt as _, path::Path};
9#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
10#[allow(
11    clippy::exhaustive_enums,
12    reason = "This is exhaustive (there aren't anymore filetypes than this)"
13)]
14/// Represents the type of a file in the filesystem
15///
16/// This enum provides a cross-platform abstraction for file types with
17/// specialised support for Unix filesystem semantics. It can be constructed
18/// from various sources including dirent `d_type` values, stat mode bits,
19/// and path-based lookups.
20///
21/// # Examples
22/// ```
23/// use fdf::FileType;
24/// use libc::DT_DIR;
25///
26/// // Create from dirent d_type
27/// let dir_type = FileType::from_dtype(DT_DIR);
28/// assert!(dir_type.is_dir());
29///
30/// // Check if a file type is traversible
31/// assert!(dir_type.is_traversible());
32/// ```
33///
34pub enum FileType {
35    /// Block special device file (e.g., /dev/sda)
36    BlockDevice,
37    /// Character special device file (e.g., /dev/tty)
38    CharDevice,
39    /// Directory
40    Directory,
41    /// FIFO (named pipe)
42    Pipe,
43    /// Symbolic link
44    Symlink,
45    /// Regular file
46    RegularFile,
47    /// Socket file
48    Socket,
49    /// Unknown file type (should be rare on supported filesystems)
50    Unknown,
51}
52
53impl FileType {
54    #[must_use]
55    #[inline]
56    /// Converts a libc `dirent` `d_type` value to a `FileType`
57    ///
58    /// This is the preferred method when `d_type` is available, as it avoids
59    /// expensive filesystem lookups. However, note that some filesystems
60    /// may not support `d_type` or may set it to `DT_UNKNOWN`.
61    ///
62    /// # Parameters
63    /// - `d_type`: The file type from a `dirent` structure
64    ///
65    /// # Examples
66    /// ```
67    /// use fdf::FileType;
68    /// use libc::{DT_DIR, DT_REG};
69    ///
70    /// assert!(FileType::from_dtype(DT_DIR).is_dir());
71    /// assert!(FileType::from_dtype(DT_REG).is_regular_file());
72    /// ```
73    pub const fn from_dtype(d_type: u8) -> Self {
74        match d_type {
75            DT_REG => Self::RegularFile,
76            DT_DIR => Self::Directory,
77            DT_BLK => Self::BlockDevice,
78            DT_CHR => Self::CharDevice,
79            DT_FIFO => Self::Pipe,
80            DT_LNK => Self::Symlink,
81            DT_SOCK => Self::Socket,
82            _ => Self::Unknown, /*DT_UNKNOWN */
83        }
84    }
85    /// Returns true if this represents a directory  (cost free check)
86    #[inline]
87    #[must_use]
88    pub const fn is_dir(&self) -> bool {
89        matches!(*self, Self::Directory)
90    }
91
92    /// Returns true if this represents a regular file  (cost free check)
93    #[inline]
94    #[must_use]
95    pub const fn is_regular_file(&self) -> bool {
96        matches!(*self, Self::RegularFile)
97    }
98
99    /// Returns true if this represents a symbolic link  (cost free check)
100    #[inline]
101    #[must_use]
102    pub const fn is_symlink(&self) -> bool {
103        matches!(*self, Self::Symlink)
104    }
105
106    /// Returns true if this represents a block device  (cost free check)
107    #[inline]
108    #[must_use]
109    pub const fn is_block_device(&self) -> bool {
110        matches!(*self, Self::BlockDevice)
111    }
112
113    /// Returns true if this represents a character device  (cost free check)
114    #[inline]
115    #[must_use]
116    pub const fn is_char_device(&self) -> bool {
117        matches!(*self, Self::CharDevice)
118    }
119
120    /// Returns true if this represents a FIFO (named pipe)  (cost free check)
121    #[inline]
122    #[must_use]
123    pub const fn is_pipe(&self) -> bool {
124        matches!(*self, Self::Pipe)
125    }
126
127    /// Returns true if this represents a socket (cost free check)
128    #[inline]
129    #[must_use]
130    pub const fn is_socket(&self) -> bool {
131        matches!(*self, Self::Socket)
132    }
133
134    /// Returns true if this represents an unknown file type  (cost free check)
135    #[inline]
136    #[must_use]
137    pub const fn is_unknown(&self) -> bool {
138        matches!(*self, Self::Unknown)
139    }
140
141    /// Returns true if the file type is traversible (directory or symlink)
142    ///
143    /// This is useful for determining whether a directory entry can be
144    /// explored further during filesystem traversal.
145    #[inline]
146    #[must_use]
147    pub const fn is_traversible(&self) -> bool {
148        matches!(*self, Self::Directory | Self::Symlink)
149    }
150
151    #[must_use]
152    #[inline]
153    /// Fallback method to determine file type when `d_type` is unavailable or `DT_UNKNOWN`
154    ///
155    /// This method first checks the `d_type` value, and if it's `DT_UNKNOWN`,
156    /// falls back to a more expensive lstat-based lookup using the file path.
157    ///
158    /// # Parameters
159    /// - `d_type`: The file type from a dirent structure
160    /// - `file_path`: The path to the file for fallback lookup
161    ///
162    /// # Notes
163    /// While ext4 and BTRFS (and others, not entirely tested!) typically provide reliable `d_type` values,
164    /// other filesystems like NTFS, XFS, or FUSE-based filesystems
165    /// may require the fallback path.
166    pub fn from_dtype_fallback(d_type: u8, file_path: &[u8]) -> Self {
167        //i wouldve just chained the function calls but it's clearer this way
168        match d_type {
169            DT_REG => Self::RegularFile,
170            DT_DIR => Self::Directory,
171            DT_BLK => Self::BlockDevice,
172            DT_CHR => Self::CharDevice,
173            DT_FIFO => Self::Pipe,
174            DT_LNK => Self::Symlink,
175            DT_SOCK => Self::Socket,
176            _ => Self::from_bytes(file_path),
177        }
178    }
179
180    #[must_use]
181    #[inline]
182    /// Determines file type using an lstat call on the provided path
183    ///
184    /// This is more expensive but more reliable than relying on `d_type`,
185    /// especially on filesystems that don't fully support dirent `d_type`.
186    ///
187    /// # Parameters
188    /// - `file_path`: The path to the file to stat (must be a valid filepath)
189    pub fn from_bytes(file_path: &[u8]) -> Self {
190        file_path
191            .get_lstat()
192            .map_or(Self::Unknown, |stat| Self::from_mode(stat.st_mode))
193    }
194
195    #[must_use]
196    #[inline]
197    /// Converts Unix mode bits to a `FileType`
198    ///
199    /// This extracts the file type from the `st_mode` field of a stat structure.
200    ///
201    /// # Parameters
202    /// - `mode`: The mode bits from a stat structure
203    pub const fn from_mode(mode: mode_t) -> Self {
204        match mode & S_IFMT {
205            S_IFREG => Self::RegularFile,
206            S_IFDIR => Self::Directory,
207            S_IFBLK => Self::BlockDevice,
208            S_IFCHR => Self::CharDevice,
209            S_IFIFO => Self::Pipe,
210            S_IFLNK => Self::Symlink,
211            S_IFSOCK => Self::Socket,
212            _ => Self::Unknown,
213        }
214    }
215    /// Determines file type using the standard library's metadata lookup
216    ///
217    /// This method is primarily useful for verification and testing purposes,
218    /// not for use within performance-critical iteration code paths.
219    ///
220    /// # Parameters
221    /// - `path_start`: The path to examine
222    #[must_use]
223    #[inline]
224    pub fn from_path<P: AsRef<Path>>(path_start: P) -> Self {
225        Path::new(path_start.as_ref())
226            .symlink_metadata()
227            .map_or(Self::Unknown, |metadata| match metadata.file_type() {
228                ft if ft.is_dir() => Self::Directory,
229                ft if ft.is_file() => Self::RegularFile,
230                ft if ft.is_symlink() => Self::Symlink,
231                ft if ft.is_block_device() => Self::BlockDevice,
232                ft if ft.is_char_device() => Self::CharDevice,
233                ft if ft.is_fifo() => Self::Pipe,
234                ft if ft.is_socket() => Self::Socket,
235                _ => Self::Unknown,
236            })
237    }
238    #[must_use]
239    #[inline]
240    /// Converts a `libc::stat` structure to a `FileType`
241    ///
242    /// Useful when you already have a stat structure and want to avoid
243    /// additional filesystem lookups.
244    ///
245    /// # Parameters
246    /// - `stat`: The stat structure to extract the file type from
247    pub const fn from_stat(stat: &libc::stat) -> Self {
248        Self::from_mode(stat.st_mode)
249    }
250}
251
252impl core::fmt::Display for FileType {
253    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
254        match *self {
255            Self::BlockDevice => write!(f, "Block device"),
256            Self::CharDevice => write!(f, "Character device"),
257            Self::Directory => write!(f, "Directory"),
258            Self::Pipe => write!(f, "FIFO"),
259            Self::Symlink => write!(f, "Symlink"),
260            Self::RegularFile => write!(f, "Regular file"),
261            Self::Socket => write!(f, "Socket"),
262            Self::Unknown => write!(f, "Unknown"),
263        }
264    }
265}
266
267impl From<libc::stat> for FileType {
268    /// Converts a `libc::stat` directly to a `FileType`
269    fn from(stat: libc::stat) -> Self {
270        Self::from_stat(&stat)
271    }
272}