Skip to main content

ext4_lwext4/
fs.rs

1//! Ext4 filesystem operations.
2
3use crate::blockdev::{BlockDevice, BlockDeviceWrapper};
4use crate::dir::Dir;
5use crate::error::{check_errno, check_errno_with_path, Error, Result};
6use crate::file::File;
7use crate::types::{FileType, FsStats, Metadata, OpenFlags};
8use ext4_lwext4_sys::{
9    ext4_atime_get, ext4_cache_flush, ext4_ctime_get, ext4_device_register, ext4_device_unregister,
10    ext4_dir_mk, ext4_dir_rm, ext4_flink, ext4_fremove, ext4_frename, ext4_fsymlink,
11    ext4_inode_exist, ext4_journal_start, ext4_journal_stop, ext4_mode_get, ext4_mode_set,
12    ext4_mount, ext4_mount_point_stats, ext4_mount_stats, ext4_mtime_get, ext4_owner_get,
13    ext4_owner_set, ext4_readlink, ext4_recover, ext4_umount,
14};
15use std::ffi::{c_char, CStr, CString};
16use std::pin::Pin;
17use std::sync::atomic::{AtomicU64, Ordering};
18
19// Counter for generating unique device names
20static DEVICE_COUNTER: AtomicU64 = AtomicU64::new(0);
21
22/// An ext4 filesystem instance.
23///
24/// This is the main entry point for filesystem operations. Create an instance
25/// by mounting a block device, then use the various methods to manipulate files
26/// and directories.
27///
28/// # Example
29/// ```no_run
30/// use ext4_lwext4::{Ext4Fs, FileBlockDevice, OpenFlags};
31///
32/// // Open a disk image and mount
33/// let device = FileBlockDevice::open("disk.img").unwrap();
34/// let fs = Ext4Fs::mount(device, false).unwrap();
35///
36/// // Create a directory
37/// fs.mkdir("/data", 0o755).unwrap();
38///
39/// // Write a file (use a block to ensure file is dropped before umount)
40/// {
41///     let mut file = fs.open("/data/hello.txt", OpenFlags::CREATE | OpenFlags::WRITE).unwrap();
42///     // ... write operations ...
43/// }
44///
45/// // Unmount when done
46/// fs.umount().unwrap();
47/// ```
48pub struct Ext4Fs {
49    /// Wrapper holding the block device and C structures (kept alive for the C library)
50    #[allow(dead_code)]
51    wrapper: Pin<Box<BlockDeviceWrapper>>,
52    /// Device name for lwext4
53    device_name: CString,
54    /// Mount point path
55    mount_point: CString,
56    /// Whether mounted read-only
57    read_only: bool,
58    /// Whether journal is active
59    journal_active: bool,
60}
61
62impl Ext4Fs {
63    /// Mount an ext4 filesystem from a block device.
64    ///
65    /// # Arguments
66    /// * `device` - The block device containing the filesystem
67    /// * `read_only` - Whether to mount read-only
68    ///
69    /// # Returns
70    /// A mounted `Ext4Fs` instance
71    pub fn mount<B: BlockDevice + 'static>(device: B, read_only: bool) -> Result<Self> {
72        // Generate unique device and mount point names
73        let id = DEVICE_COUNTER.fetch_add(1, Ordering::SeqCst);
74        let device_name = CString::new(format!("ext4dev{}", id)).unwrap();
75        let mount_point = CString::new(format!("/mp{}/", id)).unwrap();
76
77        // Create the wrapper
78        let wrapper = BlockDeviceWrapper::new(device);
79
80        // Register the device with lwext4
81        let ret = unsafe { ext4_device_register(wrapper.as_bdev_ptr(), device_name.as_ptr()) };
82        check_errno(ret)?;
83
84        // Mount the filesystem
85        let ret = unsafe { ext4_mount(device_name.as_ptr(), mount_point.as_ptr(), read_only) };
86        if ret != 0 {
87            // Unregister device on mount failure
88            unsafe { ext4_device_unregister(device_name.as_ptr()) };
89            return Err(Error::from(ret));
90        }
91
92        // Recover journal if needed
93        let ret = unsafe { ext4_recover(mount_point.as_ptr()) };
94        if ret != 0 {
95            // Continue even if recovery fails - might not have journal
96        }
97
98        // Start journaling if not read-only
99        let journal_active = if !read_only {
100            let ret = unsafe { ext4_journal_start(mount_point.as_ptr()) };
101            ret == 0
102        } else {
103            false
104        };
105
106        Ok(Self {
107            wrapper,
108            device_name,
109            mount_point,
110            read_only,
111            journal_active,
112        })
113    }
114
115    /// Unmount the filesystem.
116    ///
117    /// This flushes all pending writes and releases the block device.
118    pub fn umount(self) -> Result<()> {
119        // Stop journaling if active
120        if self.journal_active {
121            unsafe { ext4_journal_stop(self.mount_point.as_ptr()) };
122        }
123
124        // Flush cache
125        unsafe { ext4_cache_flush(self.mount_point.as_ptr()) };
126
127        // Unmount
128        let ret = unsafe { ext4_umount(self.mount_point.as_ptr()) };
129        check_errno(ret)?;
130
131        // Unregister device
132        let ret = unsafe { ext4_device_unregister(self.device_name.as_ptr()) };
133        check_errno(ret)?;
134
135        Ok(())
136    }
137
138    /// Get the mount point path used internally.
139    #[allow(dead_code)]
140    pub(crate) fn mount_point(&self) -> &CStr {
141        &self.mount_point
142    }
143
144    /// Create a full path by prepending the mount point.
145    pub(crate) fn make_path(&self, path: &str) -> Result<CString> {
146        // Remove leading slash from path if present
147        let path = path.strip_prefix('/').unwrap_or(path);
148        let mount_point = self.mount_point.to_str().map_err(|_| {
149            Error::InvalidArgument("invalid mount point".to_string())
150        })?;
151        let full_path = format!("{}{}", mount_point, path);
152        CString::new(full_path).map_err(Error::from)
153    }
154
155    /// Check if filesystem is mounted read-only.
156    pub fn is_read_only(&self) -> bool {
157        self.read_only
158    }
159
160    /// Get filesystem statistics.
161    pub fn stat(&self) -> Result<FsStats> {
162        let mut stats = ext4_mount_stats::default();
163        let ret = unsafe { ext4_mount_point_stats(self.mount_point.as_ptr(), &mut stats) };
164        check_errno(ret)?;
165
166        // Extract volume name, handling null termination
167        let volume_name = unsafe {
168            let name_bytes = &stats.volume_name;
169            let len = name_bytes.iter().position(|&c| c == 0).unwrap_or(16);
170            let slice = std::slice::from_raw_parts(name_bytes.as_ptr() as *const u8, len);
171            String::from_utf8_lossy(slice).into_owned()
172        };
173
174        Ok(FsStats {
175            block_size: stats.block_size,
176            total_blocks: stats.blocks_count,
177            free_blocks: stats.free_blocks_count,
178            total_inodes: stats.inodes_count as u64,
179            free_inodes: stats.free_inodes_count as u64,
180            block_group_count: stats.block_group_count,
181            blocks_per_group: stats.blocks_per_group,
182            inodes_per_group: stats.inodes_per_group,
183            volume_name,
184        })
185    }
186
187    /// Open a file.
188    ///
189    /// # Arguments
190    /// * `path` - Path to the file
191    /// * `flags` - Open flags (READ, WRITE, CREATE, etc.)
192    pub fn open(&self, path: &str, flags: OpenFlags) -> Result<File<'_>> {
193        File::open(self, path, flags)
194    }
195
196    /// Open a directory for iteration.
197    pub fn open_dir(&self, path: &str) -> Result<Dir<'_>> {
198        Dir::open(self, path)
199    }
200
201    /// Create a directory.
202    ///
203    /// # Arguments
204    /// * `path` - Path for the new directory
205    /// * `mode` - Permissions (e.g., 0o755)
206    pub fn mkdir(&self, path: &str, mode: u32) -> Result<()> {
207        if self.read_only {
208            return Err(Error::ReadOnly);
209        }
210
211        let full_path = self.make_path(path)?;
212        let ret = unsafe { ext4_dir_mk(full_path.as_ptr()) };
213        check_errno_with_path(ret, path)?;
214
215        // Set permissions
216        if mode != 0 {
217            self.set_permissions(path, mode)?;
218        }
219
220        Ok(())
221    }
222
223    /// Remove a file.
224    pub fn remove(&self, path: &str) -> Result<()> {
225        if self.read_only {
226            return Err(Error::ReadOnly);
227        }
228
229        let full_path = self.make_path(path)?;
230        let ret = unsafe { ext4_fremove(full_path.as_ptr()) };
231        check_errno_with_path(ret, path)
232    }
233
234    /// Remove a directory (recursively).
235    pub fn rmdir(&self, path: &str) -> Result<()> {
236        if self.read_only {
237            return Err(Error::ReadOnly);
238        }
239
240        let full_path = self.make_path(path)?;
241        let ret = unsafe { ext4_dir_rm(full_path.as_ptr()) };
242        check_errno_with_path(ret, path)
243    }
244
245    /// Rename a file or directory.
246    pub fn rename(&self, from: &str, to: &str) -> Result<()> {
247        if self.read_only {
248            return Err(Error::ReadOnly);
249        }
250
251        let from_path = self.make_path(from)?;
252        let to_path = self.make_path(to)?;
253        let ret = unsafe { ext4_frename(from_path.as_ptr(), to_path.as_ptr()) };
254        check_errno_with_path(ret, from)
255    }
256
257    /// Create a hard link.
258    pub fn link(&self, src: &str, dst: &str) -> Result<()> {
259        if self.read_only {
260            return Err(Error::ReadOnly);
261        }
262
263        let src_path = self.make_path(src)?;
264        let dst_path = self.make_path(dst)?;
265        let ret = unsafe { ext4_flink(src_path.as_ptr(), dst_path.as_ptr()) };
266        check_errno_with_path(ret, src)
267    }
268
269    /// Create a symbolic link.
270    ///
271    /// # Arguments
272    /// * `target` - The path the symlink points to
273    /// * `path` - Path for the new symlink
274    pub fn symlink(&self, target: &str, path: &str) -> Result<()> {
275        if self.read_only {
276            return Err(Error::ReadOnly);
277        }
278
279        let target_cstr = CString::new(target)?;
280        let path_full = self.make_path(path)?;
281        let ret = unsafe { ext4_fsymlink(target_cstr.as_ptr(), path_full.as_ptr()) };
282        check_errno_with_path(ret, path)
283    }
284
285    /// Read the target of a symbolic link.
286    pub fn readlink(&self, path: &str) -> Result<String> {
287        let full_path = self.make_path(path)?;
288        let mut buf = vec![0u8; 4096];
289        let mut rcnt: usize = 0;
290
291        let ret = unsafe {
292            ext4_readlink(
293                full_path.as_ptr(),
294                buf.as_mut_ptr() as *mut c_char,
295                buf.len(),
296                &mut rcnt,
297            )
298        };
299        check_errno_with_path(ret, path)?;
300
301        buf.truncate(rcnt);
302        String::from_utf8(buf).map_err(|_| Error::InvalidArgument("invalid UTF-8 in symlink".to_string()))
303    }
304
305    /// Check if a path exists.
306    pub fn exists(&self, path: &str) -> bool {
307        self.metadata(path).is_ok()
308    }
309
310    /// Check if a path exists and is a file.
311    pub fn is_file(&self, path: &str) -> bool {
312        let full_path = match self.make_path(path) {
313            Ok(p) => p,
314            Err(_) => return false,
315        };
316        unsafe { ext4_inode_exist(full_path.as_ptr(), FileType::RegularFile.to_raw() as i32) == 0 }
317    }
318
319    /// Check if a path exists and is a directory.
320    pub fn is_dir(&self, path: &str) -> bool {
321        let full_path = match self.make_path(path) {
322            Ok(p) => p,
323            Err(_) => return false,
324        };
325        unsafe { ext4_inode_exist(full_path.as_ptr(), FileType::Directory.to_raw() as i32) == 0 }
326    }
327
328    /// Get file metadata.
329    pub fn metadata(&self, path: &str) -> Result<Metadata> {
330        let full_path = self.make_path(path)?;
331
332        // Get mode to check existence
333        let mut mode: u32 = 0;
334        let ret = unsafe { ext4_mode_get(full_path.as_ptr(), &mut mode) };
335        check_errno_with_path(ret, path)?;
336
337        // Determine file type from mode
338        let file_type = match mode & 0o170000 {
339            0o100000 => FileType::RegularFile,
340            0o040000 => FileType::Directory,
341            0o120000 => FileType::Symlink,
342            0o060000 => FileType::BlockDevice,
343            0o020000 => FileType::CharDevice,
344            0o010000 => FileType::Fifo,
345            0o140000 => FileType::Socket,
346            _ => FileType::Unknown,
347        };
348
349        // Get owner
350        let mut uid: u32 = 0;
351        let mut gid: u32 = 0;
352        unsafe { ext4_owner_get(full_path.as_ptr(), &mut uid, &mut gid) };
353
354        // Get timestamps
355        let mut atime: u32 = 0;
356        let mut mtime: u32 = 0;
357        let mut ctime: u32 = 0;
358        unsafe {
359            ext4_atime_get(full_path.as_ptr(), &mut atime);
360            ext4_mtime_get(full_path.as_ptr(), &mut mtime);
361            ext4_ctime_get(full_path.as_ptr(), &mut ctime);
362        }
363
364        // For file size, we need to open the file temporarily
365        let size = if file_type == FileType::RegularFile {
366            if let Ok(file) = File::open(self, path, OpenFlags::READ) {
367                file.size()
368            } else {
369                0
370            }
371        } else {
372            0
373        };
374
375        Ok(Metadata {
376            file_type,
377            size,
378            blocks: 0, // Not easily available without reading inode directly
379            mode: mode & 0o7777, // Mask out file type bits
380            uid,
381            gid,
382            atime: atime as u64,
383            mtime: mtime as u64,
384            ctime: ctime as u64,
385            nlink: 1, // Not easily available
386        })
387    }
388
389    /// Set file permissions.
390    pub fn set_permissions(&self, path: &str, mode: u32) -> Result<()> {
391        if self.read_only {
392            return Err(Error::ReadOnly);
393        }
394
395        let full_path = self.make_path(path)?;
396        let ret = unsafe { ext4_mode_set(full_path.as_ptr(), mode) };
397        check_errno_with_path(ret, path)
398    }
399
400    /// Set file owner.
401    pub fn set_owner(&self, path: &str, uid: u32, gid: u32) -> Result<()> {
402        if self.read_only {
403            return Err(Error::ReadOnly);
404        }
405
406        let full_path = self.make_path(path)?;
407        let ret = unsafe { ext4_owner_set(full_path.as_ptr(), uid, gid) };
408        check_errno_with_path(ret, path)
409    }
410
411    /// Flush all pending writes to disk.
412    pub fn sync(&self) -> Result<()> {
413        let ret = unsafe { ext4_cache_flush(self.mount_point.as_ptr()) };
414        check_errno(ret)
415    }
416}
417
418impl Drop for Ext4Fs {
419    fn drop(&mut self) {
420        // Note: We can't return errors from drop, so we just try our best
421        if self.journal_active {
422            unsafe { ext4_journal_stop(self.mount_point.as_ptr()) };
423        }
424        unsafe {
425            ext4_cache_flush(self.mount_point.as_ptr());
426            ext4_umount(self.mount_point.as_ptr());
427            ext4_device_unregister(self.device_name.as_ptr());
428        }
429    }
430}