ext4-lwext4 0.1.2

A safe Rust wrapper for ext2/3/4 filesystem operations based on lwext4
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
//! Ext4 filesystem operations.

use crate::blockdev::{BlockDevice, BlockDeviceWrapper};
use crate::dir::Dir;
use crate::error::{check_errno, check_errno_with_path, Error, Result};
use crate::file::File;
use crate::types::{FileType, FsStats, Metadata, OpenFlags};
use ext4_lwext4_sys::{
    ext4_atime_get, ext4_cache_flush, ext4_ctime_get, ext4_device_register, ext4_device_unregister,
    ext4_dir_mk, ext4_dir_rm, ext4_flink, ext4_fremove, ext4_frename, ext4_fsymlink,
    ext4_inode_exist, ext4_journal_start, ext4_journal_stop, ext4_mode_get, ext4_mode_set,
    ext4_mount, ext4_mount_point_stats, ext4_mount_stats, ext4_mtime_get, ext4_owner_get,
    ext4_owner_set, ext4_readlink, ext4_recover, ext4_umount,
};
use std::ffi::{c_char, CStr, CString};
use std::pin::Pin;
use std::sync::atomic::{AtomicU64, Ordering};

// Counter for generating unique device names
static DEVICE_COUNTER: AtomicU64 = AtomicU64::new(0);

/// An ext4 filesystem instance.
///
/// This is the main entry point for filesystem operations. Create an instance
/// by mounting a block device, then use the various methods to manipulate files
/// and directories.
///
/// # Example
/// ```no_run
/// use ext4_lwext4::{Ext4Fs, FileBlockDevice, OpenFlags};
///
/// // Open a disk image and mount
/// let device = FileBlockDevice::open("disk.img").unwrap();
/// let fs = Ext4Fs::mount(device, false).unwrap();
///
/// // Create a directory
/// fs.mkdir("/data", 0o755).unwrap();
///
/// // Write a file (use a block to ensure file is dropped before umount)
/// {
///     let mut file = fs.open("/data/hello.txt", OpenFlags::CREATE | OpenFlags::WRITE).unwrap();
///     // ... write operations ...
/// }
///
/// // Unmount when done
/// fs.umount().unwrap();
/// ```
pub struct Ext4Fs {
    /// Wrapper holding the block device and C structures (kept alive for the C library)
    #[allow(dead_code)]
    wrapper: Pin<Box<BlockDeviceWrapper>>,
    /// Device name for lwext4
    device_name: CString,
    /// Mount point path
    mount_point: CString,
    /// Whether mounted read-only
    read_only: bool,
    /// Whether journal is active
    journal_active: bool,
}

impl Ext4Fs {
    /// Mount an ext4 filesystem from a block device.
    ///
    /// # Arguments
    /// * `device` - The block device containing the filesystem
    /// * `read_only` - Whether to mount read-only
    ///
    /// # Returns
    /// A mounted `Ext4Fs` instance
    pub fn mount<B: BlockDevice + 'static>(device: B, read_only: bool) -> Result<Self> {
        // Generate unique device and mount point names
        let id = DEVICE_COUNTER.fetch_add(1, Ordering::SeqCst);
        let device_name = CString::new(format!("ext4dev{}", id)).unwrap();
        let mount_point = CString::new(format!("/mp{}/", id)).unwrap();

        // Create the wrapper
        let wrapper = BlockDeviceWrapper::new(device);

        // Register the device with lwext4
        let ret = unsafe { ext4_device_register(wrapper.as_bdev_ptr(), device_name.as_ptr()) };
        check_errno(ret)?;

        // Mount the filesystem
        let ret = unsafe { ext4_mount(device_name.as_ptr(), mount_point.as_ptr(), read_only) };
        if ret != 0 {
            // Unregister device on mount failure
            unsafe { ext4_device_unregister(device_name.as_ptr()) };
            return Err(Error::from(ret));
        }

        // Recover journal if needed
        let ret = unsafe { ext4_recover(mount_point.as_ptr()) };
        if ret != 0 {
            // Continue even if recovery fails - might not have journal
        }

        // Start journaling if not read-only
        let journal_active = if !read_only {
            let ret = unsafe { ext4_journal_start(mount_point.as_ptr()) };
            ret == 0
        } else {
            false
        };

        Ok(Self {
            wrapper,
            device_name,
            mount_point,
            read_only,
            journal_active,
        })
    }

    /// Unmount the filesystem.
    ///
    /// This flushes all pending writes and releases the block device.
    pub fn umount(self) -> Result<()> {
        // Stop journaling if active
        if self.journal_active {
            unsafe { ext4_journal_stop(self.mount_point.as_ptr()) };
        }

        // Flush cache
        unsafe { ext4_cache_flush(self.mount_point.as_ptr()) };

        // Unmount
        let ret = unsafe { ext4_umount(self.mount_point.as_ptr()) };
        check_errno(ret)?;

        // Unregister device
        let ret = unsafe { ext4_device_unregister(self.device_name.as_ptr()) };
        check_errno(ret)?;

        Ok(())
    }

    /// Get the mount point path used internally.
    #[allow(dead_code)]
    pub(crate) fn mount_point(&self) -> &CStr {
        &self.mount_point
    }

    /// Create a full path by prepending the mount point.
    pub(crate) fn make_path(&self, path: &str) -> Result<CString> {
        // Remove leading slash from path if present
        let path = path.strip_prefix('/').unwrap_or(path);
        let mount_point = self.mount_point.to_str().map_err(|_| {
            Error::InvalidArgument("invalid mount point".to_string())
        })?;
        let full_path = format!("{}{}", mount_point, path);
        CString::new(full_path).map_err(Error::from)
    }

    /// Check if filesystem is mounted read-only.
    pub fn is_read_only(&self) -> bool {
        self.read_only
    }

    /// Get filesystem statistics.
    pub fn stat(&self) -> Result<FsStats> {
        let mut stats = ext4_mount_stats::default();
        let ret = unsafe { ext4_mount_point_stats(self.mount_point.as_ptr(), &mut stats) };
        check_errno(ret)?;

        // Extract volume name, handling null termination
        let volume_name = unsafe {
            let name_bytes = &stats.volume_name;
            let len = name_bytes.iter().position(|&c| c == 0).unwrap_or(16);
            let slice = std::slice::from_raw_parts(name_bytes.as_ptr() as *const u8, len);
            String::from_utf8_lossy(slice).into_owned()
        };

        Ok(FsStats {
            block_size: stats.block_size,
            total_blocks: stats.blocks_count,
            free_blocks: stats.free_blocks_count,
            total_inodes: stats.inodes_count as u64,
            free_inodes: stats.free_inodes_count as u64,
            block_group_count: stats.block_group_count,
            blocks_per_group: stats.blocks_per_group,
            inodes_per_group: stats.inodes_per_group,
            volume_name,
        })
    }

    /// Open a file.
    ///
    /// # Arguments
    /// * `path` - Path to the file
    /// * `flags` - Open flags (READ, WRITE, CREATE, etc.)
    pub fn open(&self, path: &str, flags: OpenFlags) -> Result<File<'_>> {
        File::open(self, path, flags)
    }

    /// Open a directory for iteration.
    pub fn open_dir(&self, path: &str) -> Result<Dir<'_>> {
        Dir::open(self, path)
    }

    /// Create a directory.
    ///
    /// # Arguments
    /// * `path` - Path for the new directory
    /// * `mode` - Permissions (e.g., 0o755)
    pub fn mkdir(&self, path: &str, mode: u32) -> Result<()> {
        if self.read_only {
            return Err(Error::ReadOnly);
        }

        let full_path = self.make_path(path)?;
        let ret = unsafe { ext4_dir_mk(full_path.as_ptr()) };
        check_errno_with_path(ret, path)?;

        // Set permissions
        if mode != 0 {
            self.set_permissions(path, mode)?;
        }

        Ok(())
    }

    /// Remove a file.
    pub fn remove(&self, path: &str) -> Result<()> {
        if self.read_only {
            return Err(Error::ReadOnly);
        }

        let full_path = self.make_path(path)?;
        let ret = unsafe { ext4_fremove(full_path.as_ptr()) };
        check_errno_with_path(ret, path)
    }

    /// Remove a directory (recursively).
    pub fn rmdir(&self, path: &str) -> Result<()> {
        if self.read_only {
            return Err(Error::ReadOnly);
        }

        let full_path = self.make_path(path)?;
        let ret = unsafe { ext4_dir_rm(full_path.as_ptr()) };
        check_errno_with_path(ret, path)
    }

    /// Rename a file or directory.
    pub fn rename(&self, from: &str, to: &str) -> Result<()> {
        if self.read_only {
            return Err(Error::ReadOnly);
        }

        let from_path = self.make_path(from)?;
        let to_path = self.make_path(to)?;
        let ret = unsafe { ext4_frename(from_path.as_ptr(), to_path.as_ptr()) };
        check_errno_with_path(ret, from)
    }

    /// Create a hard link.
    pub fn link(&self, src: &str, dst: &str) -> Result<()> {
        if self.read_only {
            return Err(Error::ReadOnly);
        }

        let src_path = self.make_path(src)?;
        let dst_path = self.make_path(dst)?;
        let ret = unsafe { ext4_flink(src_path.as_ptr(), dst_path.as_ptr()) };
        check_errno_with_path(ret, src)
    }

    /// Create a symbolic link.
    ///
    /// # Arguments
    /// * `target` - The path the symlink points to
    /// * `path` - Path for the new symlink
    pub fn symlink(&self, target: &str, path: &str) -> Result<()> {
        if self.read_only {
            return Err(Error::ReadOnly);
        }

        let target_cstr = CString::new(target)?;
        let path_full = self.make_path(path)?;
        let ret = unsafe { ext4_fsymlink(target_cstr.as_ptr(), path_full.as_ptr()) };
        check_errno_with_path(ret, path)
    }

    /// Read the target of a symbolic link.
    pub fn readlink(&self, path: &str) -> Result<String> {
        let full_path = self.make_path(path)?;
        let mut buf = vec![0u8; 4096];
        let mut rcnt: usize = 0;

        let ret = unsafe {
            ext4_readlink(
                full_path.as_ptr(),
                buf.as_mut_ptr() as *mut c_char,
                buf.len(),
                &mut rcnt,
            )
        };
        check_errno_with_path(ret, path)?;

        buf.truncate(rcnt);
        String::from_utf8(buf).map_err(|_| Error::InvalidArgument("invalid UTF-8 in symlink".to_string()))
    }

    /// Check if a path exists.
    pub fn exists(&self, path: &str) -> bool {
        self.metadata(path).is_ok()
    }

    /// Check if a path exists and is a file.
    pub fn is_file(&self, path: &str) -> bool {
        let full_path = match self.make_path(path) {
            Ok(p) => p,
            Err(_) => return false,
        };
        unsafe { ext4_inode_exist(full_path.as_ptr(), FileType::RegularFile.to_raw() as i32) == 0 }
    }

    /// Check if a path exists and is a directory.
    pub fn is_dir(&self, path: &str) -> bool {
        let full_path = match self.make_path(path) {
            Ok(p) => p,
            Err(_) => return false,
        };
        unsafe { ext4_inode_exist(full_path.as_ptr(), FileType::Directory.to_raw() as i32) == 0 }
    }

    /// Get file metadata.
    pub fn metadata(&self, path: &str) -> Result<Metadata> {
        let full_path = self.make_path(path)?;

        // Get mode to check existence
        let mut mode: u32 = 0;
        let ret = unsafe { ext4_mode_get(full_path.as_ptr(), &mut mode) };
        check_errno_with_path(ret, path)?;

        // Determine file type from mode
        let file_type = match mode & 0o170000 {
            0o100000 => FileType::RegularFile,
            0o040000 => FileType::Directory,
            0o120000 => FileType::Symlink,
            0o060000 => FileType::BlockDevice,
            0o020000 => FileType::CharDevice,
            0o010000 => FileType::Fifo,
            0o140000 => FileType::Socket,
            _ => FileType::Unknown,
        };

        // Get owner
        let mut uid: u32 = 0;
        let mut gid: u32 = 0;
        unsafe { ext4_owner_get(full_path.as_ptr(), &mut uid, &mut gid) };

        // Get timestamps
        let mut atime: u32 = 0;
        let mut mtime: u32 = 0;
        let mut ctime: u32 = 0;
        unsafe {
            ext4_atime_get(full_path.as_ptr(), &mut atime);
            ext4_mtime_get(full_path.as_ptr(), &mut mtime);
            ext4_ctime_get(full_path.as_ptr(), &mut ctime);
        }

        // For file size, we need to open the file temporarily
        let size = if file_type == FileType::RegularFile {
            if let Ok(file) = File::open(self, path, OpenFlags::READ) {
                file.size()
            } else {
                0
            }
        } else {
            0
        };

        Ok(Metadata {
            file_type,
            size,
            blocks: 0, // Not easily available without reading inode directly
            mode: mode & 0o7777, // Mask out file type bits
            uid,
            gid,
            atime: atime as u64,
            mtime: mtime as u64,
            ctime: ctime as u64,
            nlink: 1, // Not easily available
        })
    }

    /// Set file permissions.
    pub fn set_permissions(&self, path: &str, mode: u32) -> Result<()> {
        if self.read_only {
            return Err(Error::ReadOnly);
        }

        let full_path = self.make_path(path)?;
        let ret = unsafe { ext4_mode_set(full_path.as_ptr(), mode) };
        check_errno_with_path(ret, path)
    }

    /// Set file owner.
    pub fn set_owner(&self, path: &str, uid: u32, gid: u32) -> Result<()> {
        if self.read_only {
            return Err(Error::ReadOnly);
        }

        let full_path = self.make_path(path)?;
        let ret = unsafe { ext4_owner_set(full_path.as_ptr(), uid, gid) };
        check_errno_with_path(ret, path)
    }

    /// Flush all pending writes to disk.
    pub fn sync(&self) -> Result<()> {
        let ret = unsafe { ext4_cache_flush(self.mount_point.as_ptr()) };
        check_errno(ret)
    }
}

impl Drop for Ext4Fs {
    fn drop(&mut self) {
        // Note: We can't return errors from drop, so we just try our best
        if self.journal_active {
            unsafe { ext4_journal_stop(self.mount_point.as_ptr()) };
        }
        unsafe {
            ext4_cache_flush(self.mount_point.as_ptr());
            ext4_umount(self.mount_point.as_ptr());
            ext4_device_unregister(self.device_name.as_ptr());
        }
    }
}