flail/ext/
mod.rs

1use bitflags::bitflags;
2use eyre::{eyre, Result};
3use lazy_static::lazy_static;
4use log::*;
5use uuid::Uuid;
6
7use std::ffi::{CStr, CString};
8use std::fs::OpenOptions;
9use std::mem::MaybeUninit;
10use std::os::unix::ffi::OsStrExt;
11use std::path::{Path, PathBuf};
12use std::sync::{Arc, RwLock};
13use std::time::SystemTime;
14
15use self::block::*;
16use self::file::*;
17use self::inode::*;
18use self::io::*;
19use self::messages::*;
20
21pub mod block;
22pub mod facade;
23pub mod file;
24pub mod inode;
25pub mod io;
26pub mod messages;
27
28#[derive(Debug, Clone)]
29pub struct ExtFilesystem(Arc<RwLock<libe2fs_sys::ext2_filsys>>, PathBuf);
30// SAFETY: I promise I'm doing my best here :sob:
31// All accesses to the ext2_filsys pointer are through an RwLock, and then
32// libe2fs does its own locking internally if it's compiled w/ support.
33unsafe impl Send for ExtFilesystem {}
34unsafe impl Sync for ExtFilesystem {}
35
36lazy_static! {
37    static ref DEFAULT_IO_MANAGER: IoManager = {
38        #[cfg(not(target_os = "windows"))]
39        IoManager(Arc::new(RwLock::new(unsafe {
40            *libe2fs_sys::unix_io_manager
41        })))
42    };
43}
44
45impl ExtFilesystem {
46    pub const ROOT_INODE: u32 = libe2fs_sys::EXT2_ROOT_INO;
47    pub const LPF_INODE: u32 = 11;
48
49    pub fn create<P: Into<PathBuf>>(path: P, size_bytes: u64) -> Result<Self> {
50        // create file of size_bytes at path
51        let path = path.into();
52        debug!(
53            "creating ext filesystem at {path:?} of size {size_bytes}",
54            path = path,
55            size_bytes = size_bytes
56        );
57        let file = OpenOptions::new()
58            .write(true)
59            .create(true)
60            .read(true)
61            .create_new(true)
62            .open(&path)?;
63        file.set_len(size_bytes)?;
64
65        // initialise superblock
66        debug!("initialising superblock...");
67        let (err, fs) = unsafe {
68            let mut fs = MaybeUninit::uninit();
69            let block_size = 1_024;
70            let inode_ratio = 8_192;
71            let blocks_count = size_bytes / block_size;
72            let path = CString::new(path.to_string_lossy().as_bytes())?;
73
74            // hardware sector sizes
75            let mut lsector_size = 0;
76            let mut psector_size = 0;
77            let err = libe2fs_sys::ext2fs_get_device_sectsize(path.as_ptr(), &mut lsector_size);
78            if err != 0 {
79                return report(err);
80            }
81            let err =
82                libe2fs_sys::ext2fs_get_device_phys_sectsize(path.as_ptr(), &mut psector_size);
83            if err != 0 {
84                return report(err);
85            }
86
87            let mut superblock = libe2fs_sys::ext2_super_block {
88                s_rev_level: 1,
89                s_log_block_size: 0,
90                // TODO: validate
91                s_blocks_per_group: 0,
92                s_blocks_count: blocks_count as u32,
93                s_blocks_count_hi: (blocks_count >> 32) as u32,
94                s_first_meta_bg: 0,
95                s_log_cluster_size: 0,
96                s_desc_size: libe2fs_sys::EXT2_MIN_DESC_SIZE_64BIT as u16,
97                // we don't use old inode size because it's old
98                s_inode_size: 0,
99                s_inodes_count: (blocks_count * block_size / inode_ratio).try_into()?,
100                s_r_blocks_count: 5,
101
102                s_algorithm_usage_bitmap: 0,
103                s_backup_bgs: [0, 0],
104                s_checksum_seed: 0,
105                s_checksum: 0,
106                s_creator_os: libe2fs_sys::EXT2_OS_LINUX,
107                s_block_group_nr: 0,
108                s_checkinterval: 0,
109                s_checksum_type: 0,
110                s_clusters_per_group: 0,
111                s_def_hash_version: 0,
112                s_def_resgid: 0,
113                s_def_resuid: 0,
114                s_default_mount_opts: 0,
115                s_encoding: 0,
116                s_encoding_flags: 0,
117                s_encrypt_algos: [0, 0, 0, 0],
118                s_encrypt_pw_salt: [0; 16],
119                s_encryption_level: 0,
120                s_error_count: 0,
121                s_errors: 0,
122                s_feature_compat: 0,
123                s_feature_incompat: libe2fs_sys::EXT4_FEATURE_INCOMPAT_64BIT
124                    | libe2fs_sys::EXT3_FEATURE_INCOMPAT_EXTENTS,
125                s_feature_ro_compat: libe2fs_sys::EXT2_FEATURE_RO_COMPAT_LARGE_FILE
126                    | libe2fs_sys::EXT4_FEATURE_RO_COMPAT_HUGE_FILE
127                    | libe2fs_sys::EXT4_FEATURE_RO_COMPAT_DIR_NLINK,
128                s_first_data_block: 0,
129                s_first_error_block: 0,
130                s_first_error_errcode: 0,
131                s_first_error_func: [0; 32],
132                s_first_error_ino: 0,
133                s_first_error_line: 0,
134                s_first_error_time: 0,
135                s_first_error_time_hi: 0,
136                s_first_ino: 0,
137                s_flags: 0,
138                s_free_blocks_count: 0,
139                s_free_blocks_hi: 0,
140                s_free_inodes_count: 0,
141                s_grp_quota_inum: 0,
142                s_hash_seed: [0; 4],
143                s_kbytes_written: 0,
144                s_last_error_block: 0,
145                s_last_error_errcode: 0,
146                s_last_error_func: [0; 32],
147                s_last_error_ino: 0,
148                s_last_error_line: 0,
149                s_last_error_time: 0,
150                s_last_error_time_hi: 0,
151                s_last_mounted: [0; 64],
152                s_last_orphan: 0,
153                s_inodes_per_group: 0,
154                s_jnl_backup_type: 0,
155                s_jnl_blocks: [0; 17],
156                s_journal_dev: 0,
157                s_journal_inum: 0,
158                s_journal_uuid: [0; 16],
159                s_lastcheck: 0,
160                s_lastcheck_hi: 0,
161                s_log_groups_per_flex: 0,
162                s_max_mnt_count: 0,
163                s_mmp_block: 0,
164                s_mmp_update_interval: 0,
165                s_mtime: 0,
166                s_mtime_hi: 0,
167                s_mkfs_time: 0,
168                s_mkfs_time_hi: 0,
169                s_mount_opts: [0; 64],
170                s_prealloc_blocks: 0,
171                s_prealloc_dir_blocks: 0,
172                s_lpf_ino: 0,
173                s_magic: libe2fs_sys::EXT2_SUPER_MAGIC.try_into()?,
174                s_min_extra_isize: 0,
175                s_minor_rev_level: 0,
176                s_mnt_count: 0,
177                s_orphan_file_inum: 0,
178                s_overhead_clusters: 0,
179                s_prj_quota_inum: 0,
180                s_r_blocks_count_hi: 0,
181                s_raid_stride: 0,
182                s_raid_stripe_width: 0,
183                s_reserved: [0; 94],
184                s_reserved_gdt_blocks: 0,
185                s_reserved_pad: 0,
186                s_snapshot_id: 0,
187                s_snapshot_inum: 0,
188                s_snapshot_list: 0,
189                s_snapshot_r_blocks_count: 0,
190                s_state: 0,
191                s_usr_quota_inum: 0,
192                s_uuid: [0; 16],
193                s_volume_name: [0; 16],
194                s_want_extra_isize: 0,
195                s_wtime: 0,
196                s_wtime_hi: 0,
197            };
198            let io_manager = DEFAULT_IO_MANAGER.clone().0;
199            let mut io_manager = io_manager.write().unwrap();
200            let err = libe2fs_sys::ext2fs_initialize(
201                path.as_ptr(),
202                (libe2fs_sys::EXT2_FLAG_EXCLUSIVE
203                    | libe2fs_sys::EXT2_FLAG_64BITS
204                    | libe2fs_sys::EXT2_FLAG_SKIP_MMP
205                    | libe2fs_sys::EXT2_FLAG_RW) as i32,
206                &mut superblock as *mut _,
207                &mut *io_manager,
208                fs.as_mut_ptr(),
209            );
210            (err, fs)
211        };
212
213        if err != 0 {
214            return report(err);
215        }
216
217        let fs: libe2fs_sys::ext2_filsys = unsafe { fs.assume_init() };
218
219        // we skip journals for now
220        // TODO: support journaling
221
222        debug!("updating superblock accounting...");
223        unsafe { *(*fs).super_ }.s_kbytes_written = 1;
224
225        debug!("generating uuid...");
226        let uuid = Uuid::new_v4();
227        let uuid = uuid.as_bytes();
228        unsafe { *(*fs).super_ }.s_uuid = *uuid;
229
230        // TODO: support setting periodic fsck
231
232        debug!("setting creatoros...");
233        let creatoros = libe2fs_sys::EXT2_OS_LINUX;
234        unsafe { *(*fs).super_ }.s_creator_os = creatoros;
235
236        debug!("setting volume label...");
237        unsafe { *(*fs).super_ }.s_volume_name = [
238            'f'.try_into()?,
239            'l'.try_into()?,
240            'a'.try_into()?,
241            'i'.try_into()?,
242            'l'.try_into()?,
243            0,
244            0,
245            0,
246            0,
247            0,
248            0,
249            0,
250            0,
251            0,
252            0,
253            0,
254        ];
255
256        debug!("setting checksum...");
257        unsafe { *(*fs).super_ }.s_checksum_type = libe2fs_sys::EXT2_CRC32C_CHKSUM as u8;
258
259        debug!("allocating group tables...");
260        let err = unsafe { libe2fs_sys::ext2fs_allocate_tables(fs) };
261        if err != 0 {
262            return report(err);
263        }
264
265        // ext2fs_convert_subcluster_bitmap(fs, &fs->block_map);
266        // whatever that is
267        debug!("converting subcluster bitmaps...");
268        let err =
269            unsafe { libe2fs_sys::ext2fs_convert_subcluster_bitmap(fs, &mut (*fs).block_map) };
270        if err != 0 {
271            return report(err);
272        }
273
274        // calculate overhead
275        debug!("calculating overhead...");
276        let mut overhead: u64 = 0;
277        let err = unsafe {
278            libe2fs_sys::ext2fs_count_used_clusters(
279                fs,
280                (*(*fs).super_).s_first_data_block as u64,
281                libe2fs_sys::ext2fs_blocks_count((*fs).super_) - 1,
282                &mut overhead,
283            )
284        };
285        if err != 0 {
286            return report(err);
287        }
288
289        // TODO: support mmp someday
290
291        // set overhead clusters (we read this earlier)
292        unsafe { *(*fs).super_ }.s_overhead_clusters = overhead as u32;
293
294        debug!("updating accounting...");
295        unsafe { *(*fs).super_ }.s_checkinterval = 0;
296        unsafe { *(*fs).super_ }.s_max_mnt_count = 1;
297
298        debug!("flushing!");
299        let err = unsafe { libe2fs_sys::ext2fs_flush(fs) };
300        if err != 0 {
301            return report(err);
302        }
303
304        debug!("creating root dir...");
305        let err = unsafe {
306            libe2fs_sys::ext2fs_mkdir(fs, Self::ROOT_INODE, Self::ROOT_INODE, std::ptr::null_mut())
307        };
308        if err != 0 {
309            return report(err);
310        }
311
312        debug!("creating l+f...");
313        let lpf_name = CString::new("lost+found")?;
314        let err = unsafe { libe2fs_sys::ext2fs_mkdir(fs, Self::ROOT_INODE, 0, lpf_name.as_ptr()) };
315        if err != 0 {
316            return report(err);
317        }
318
319        debug!("reserving inodes...");
320        for i in Self::ROOT_INODE + 1..unsafe { *(*fs).super_ }.s_first_ino {
321            unsafe {
322                libe2fs_sys::ext2fs_inode_alloc_stats2(fs, i, 1, 0);
323            }
324        }
325        unsafe {
326            (*fs).flags |=
327                (libe2fs_sys::EXT2_FLAG_IB_DIRTY | libe2fs_sys::EXT2_FLAG_CHANGED) as i32;
328        }
329
330        debug!("creating bad block inode...");
331        unsafe {
332            libe2fs_sys::ext2fs_mark_generic_bmap(
333                (*fs).inode_map,
334                libe2fs_sys::EXT2_BAD_INO as u64,
335            );
336            libe2fs_sys::ext2fs_inode_alloc_stats2(fs, libe2fs_sys::EXT2_BAD_INO, 1, 0);
337            let err = libe2fs_sys::ext2fs_update_bb_inode(fs, std::ptr::null_mut());
338            if err != 0 {
339                return report(err);
340            }
341        }
342
343        debug!("flushing!");
344        let err = unsafe { libe2fs_sys::ext2fs_flush(fs) };
345        if err != 0 {
346            return report(err);
347        }
348
349        Ok(Self(Arc::new(RwLock::new(fs)), path))
350    }
351
352    pub fn open<P: Into<PathBuf> + std::fmt::Debug>(
353        name: P,
354        block_size: Option<u32>,
355        flags: Option<ExtFilesystemOpenFlags>,
356    ) -> Result<Self> {
357        // assumes flags=0, superblock=0,
358        // from openfs.c:
359        /*
360         *  Note: if superblock is non-zero, block-size must also be non-zero.
361         * 	Superblock and block_size can be zero to use the default size.
362         *
363         * Valid flags for ext2fs_open()
364         *
365         * 	EXT2_FLAG_RW	- Open the filesystem for read/write.
366         * 	EXT2_FLAG_FORCE - Open the filesystem even if some of the
367         *				features aren't supported.
368         *	EXT2_FLAG_JOURNAL_DEV_OK - Open an ext3 journal device
369         *	EXT2_FLAG_SKIP_MMP - Open without multi-mount protection check.
370         *	EXT2_FLAG_64BITS - Allow 64-bit bitfields (needed for large
371         *				filesystems)
372         */
373
374        let mut fs = MaybeUninit::uninit();
375        let name = name.into().canonicalize()?;
376        let (err, fs) = unsafe {
377            debug!("preparing to open ext filesystem...");
378            debug!("input = {name:#?}");
379            debug!("opening ext filesystem at '{name:?}'");
380            let name = CString::new(name.to_string_lossy().as_bytes())?;
381            let io_manager = DEFAULT_IO_MANAGER.clone().0;
382            let mut io_manager = io_manager.write().unwrap();
383            debug!("got io manager");
384            let err = libe2fs_sys::ext2fs_open(
385                name.as_ptr(),
386                flags.unwrap_or(ExtFilesystemOpenFlags::OPEN_64BIT).bits(),
387                0,
388                block_size.unwrap_or(0),
389                &mut *io_manager,
390                fs.as_mut_ptr(),
391            );
392            (err, fs)
393        };
394
395        if err == 0 {
396            let fs = unsafe { fs.assume_init() };
397            let out = Self(Arc::new(RwLock::new(fs)), name);
398            debug!("@ starting setup @");
399            out.read_bitmaps()?;
400
401            let lpf_inode = out.read_inode(Self::LPF_INODE);
402            if lpf_inode.is_err() {
403                debug!("creating missing /lost+found...");
404                out.mkdir("/", "lost+found")?;
405            }
406
407            debug!("@ finished setup @");
408
409            Ok(out)
410        } else {
411            report(err)
412        }
413    }
414
415    pub fn iterate_dir<F, P: Into<PathBuf>>(&self, dir: P, mut f: F) -> Result<()>
416    where
417        F: FnMut(*mut libe2fs_sys::ext2_dir_entry, i32, i32, &str, &[i8]) -> Result<i32>,
418    {
419        let dir = dir.into();
420        debug!("iterate dir at {dir:?}");
421        let inode = self.find_inode(&dir)?;
422        debug!("found inode {}", inode.0);
423        let fs = self.0.read().unwrap();
424
425        debug!("creating trampoline...");
426        let iterator = get_dir_iterator_trampoline(&f);
427        debug!("boing!");
428
429        let err = unsafe {
430            debug!("iterating {dir:?} with user-provided iterator...");
431            libe2fs_sys::ext2fs_dir_iterate(
432                *fs,
433                inode.num(),
434                0,
435                &mut [0u8; 4_096] as *mut _ as *mut i8,
436                Some(iterator),
437                &mut f as *mut _ as *mut ::std::ffi::c_void,
438            )
439        };
440        if err == 0 {
441            Ok(())
442        } else {
443            report(err)
444        }
445    }
446
447    pub fn root_inode(&self) -> Result<ExtInode> {
448        self.read_inode(Self::ROOT_INODE)
449    }
450
451    pub fn read_inode(&self, inode: u32) -> Result<ExtInode> {
452        debug!("reading inode {inode}...");
453        let mut inode_ptr = MaybeUninit::uninit();
454        let err = unsafe {
455            libe2fs_sys::ext2fs_read_inode(
456                self.0.read().unwrap().as_mut().unwrap(),
457                inode,
458                inode_ptr.as_mut_ptr(),
459            )
460        };
461        if err == 0 {
462            Ok(unsafe { ExtInode(inode, *inode_ptr.assume_init_mut()) })
463        } else {
464            report(err)
465        }
466    }
467
468    pub fn find_inode<P: Into<PathBuf>>(&self, path: P) -> Result<ExtInode> {
469        let path = path.into();
470        debug!("finding inode for {path:?}...");
471        let path = CString::new(path.to_str().unwrap())?;
472        let mut inode = MaybeUninit::uninit();
473        let err = unsafe {
474            debug!("naming inode at {path:?}");
475            let fs = self.0.read().unwrap();
476            libe2fs_sys::ext2fs_namei(
477                *fs,
478                libe2fs_sys::EXT2_ROOT_INO,
479                libe2fs_sys::EXT2_ROOT_INO,
480                path.as_ptr(),
481                inode.as_mut_ptr(),
482            )
483        };
484        if err == 0 {
485            debug!("found inode, reading...");
486            self.read_inode(unsafe { *inode.assume_init_mut() })
487        } else {
488            report(err)
489        }
490    }
491
492    pub fn find_inode_follow<P: Into<PathBuf>>(&self, path: P) -> Result<ExtInode> {
493        let path = path.into();
494        debug!("finding inode for {path:?}...");
495        let path = CString::new(path.to_str().unwrap())?;
496        let mut inode = MaybeUninit::uninit();
497        let err = unsafe {
498            debug!("naming inode at {path:?}");
499            let fs = self.0.read().unwrap();
500            libe2fs_sys::ext2fs_namei_follow(
501                *fs,
502                libe2fs_sys::EXT2_ROOT_INO,
503                libe2fs_sys::EXT2_ROOT_INO,
504                path.as_ptr(),
505                inode.as_mut_ptr(),
506            )
507        };
508        if err == 0 {
509            debug!("found inode, reading...");
510            self.read_inode(unsafe { *inode.assume_init_mut() })
511        } else {
512            report(err)
513        }
514    }
515
516    pub fn lookup<P: Into<PathBuf> + Clone>(&self, dir: P, name: &str) -> Result<ExtInode> {
517        {
518            let dir = dir.clone();
519            debug!("looking up {name} in {:?}...", dir.into());
520        }
521        let dir_inode_number = self.find_inode(dir)?.0;
522        debug!("found dir inode: {dir_inode_number}");
523
524        let name = match name.strip_prefix('/') {
525            Some(name) => name,
526            None => name,
527        };
528        let name = CString::new(name)?;
529
530        let mut inode = MaybeUninit::uninit();
531        let err = unsafe {
532            let fs = self.0.read().unwrap();
533            debug!("looking up {name:?} in {dir_inode_number} via ext2fs_lookup...");
534            libe2fs_sys::ext2fs_lookup(
535                *fs,
536                dir_inode_number,
537                name.as_ptr(),
538                name.as_bytes().len().try_into()?,
539                std::ptr::null_mut(),
540                inode.as_mut_ptr(),
541            )
542        };
543        if err == 0 {
544            self.read_inode(unsafe { inode.assume_init() })
545        } else {
546            report(err)
547        }
548    }
549
550    pub fn get_pathname(&self, inode: u32) -> Result<String> {
551        debug!("reading pathname for inode {}", inode);
552        let mut name = MaybeUninit::<&[i8]>::uninit();
553        let err = unsafe {
554            libe2fs_sys::ext2fs_get_pathname(
555                self.0.read().unwrap().as_mut().unwrap(),
556                libe2fs_sys::EXT2_ROOT_INO,
557                inode,
558                name.as_mut_ptr() as *mut *mut ::std::ffi::c_char,
559            )
560        };
561        let name = unsafe { name.assume_init() };
562        debug!("received {} byte(s)", name.len());
563        if err == 0 {
564            Ok(String::from_utf8(name.iter().map(|i| *i as u8).collect())?)
565        } else {
566            report(err)
567        }
568    }
569
570    pub fn open_file(&self, inode: u32, flags: Option<ExtFileOpenFlags>) -> Result<ExtFile> {
571        let mut file = MaybeUninit::uninit();
572        let err = unsafe {
573            libe2fs_sys::ext2fs_file_open2(
574                self.0.read().unwrap().as_mut().unwrap(),
575                inode,
576                std::ptr::null_mut(),
577                flags.unwrap_or(ExtFileOpenFlags::empty()).bits(),
578                file.as_mut_ptr(),
579            )
580        };
581
582        if err == 0 {
583            Ok(ExtFile(unsafe { file.assume_init() }, ExtFileState::Open))
584        } else {
585            report(err)
586        }
587    }
588
589    pub fn close_file(&self, file: &mut ExtFile) -> Result<()> {
590        if file.1 == ExtFileState::Closed {
591            return Err(eyre!("file already closed!"));
592        }
593
594        let err = unsafe { libe2fs_sys::ext2fs_file_close(file.0) };
595        if err == 0 {
596            file.1 = ExtFileState::Closed;
597            Ok(())
598        } else {
599            report(err)
600        }
601    }
602
603    pub fn get_inode(&self, file: &ExtFile) -> Result<ExtInode> {
604        let inode = unsafe { libe2fs_sys::ext2fs_file_get_inode(file.0) };
605        let inode_num = unsafe { libe2fs_sys::ext2fs_file_get_inode_num(file.0) };
606        if inode.is_null() {
607            Err(ExtError::ENOENT.into())
608        } else {
609            Ok(ExtInode(inode_num, unsafe { *inode }))
610        }
611    }
612
613    pub fn get_inode_number(&self, file: &ExtFile) -> Result<u32> {
614        let inode = unsafe { libe2fs_sys::ext2fs_file_get_inode_num(file.0) };
615        if inode == 0 {
616            Err(ExtError::ENOENT.into())
617        } else {
618            Ok(inode)
619        }
620    }
621
622    pub fn read_file(&self, file: &ExtFile, buf: &mut [u8]) -> Result<usize> {
623        debug!("reading up to {} bytes from {file:?}", buf.len());
624        let mut got = MaybeUninit::uninit();
625        let err = unsafe {
626            libe2fs_sys::ext2fs_file_read(
627                file.0,
628                buf.as_mut_ptr() as *mut ::std::ffi::c_void,
629                buf.len() as u32,
630                got.as_mut_ptr(),
631            )
632        };
633        let bytes_read = unsafe { got.assume_init() };
634        debug!("read {bytes_read} bytes");
635        if bytes_read != buf.len() as u32 {
636            warn!("read {} bytes, expected {}", bytes_read, buf.len());
637        }
638        if err == 0 {
639            Ok(bytes_read as usize)
640        } else {
641            report(err)
642        }
643    }
644
645    pub fn write_file(&self, file: &ExtFile, buf: &[u8]) -> Result<usize> {
646        let mut written = MaybeUninit::uninit();
647        debug!("attempting to write {} bytes to {file:?}", buf.len());
648        let ext_file = file.0 as *mut libe2fs_sys::real_ext2_file;
649        let err = unsafe {
650            libe2fs_sys::ext2fs_file_write(
651                ext_file as *mut libe2fs_sys::ext2_file,
652                buf.as_ptr() as *const ::std::ffi::c_void,
653                buf.len() as u32,
654                written.as_mut_ptr(),
655            )
656        };
657
658        if err != 0 {
659            return report(err);
660        }
661
662        let written = unsafe { written.assume_init() };
663
664        // update the true size of the inode
665        unsafe {
666            let mut inode = self.read_inode((*ext_file).ino)?;
667            debug!("final inode size: {}", inode.1.i_size);
668            let err = libe2fs_sys::ext2fs_write_inode(
669                self.0.read().unwrap().as_mut().unwrap(),
670                (*ext_file).ino,
671                &mut inode.1,
672            );
673
674            if err != 0 {
675                return report(err);
676            }
677        }
678
679        let err =
680            unsafe { libe2fs_sys::ext2fs_file_flush(ext_file as *mut libe2fs_sys::ext2_file) };
681        if err == 0 {
682            self.flush()?;
683            // self.seek(file, written as u64, libe2fs_sys::SEEK_CUR as i32)?;
684            debug!("write succeeded");
685            Ok(written as usize)
686        } else {
687            report(err)
688        }
689    }
690
691    pub fn flush_file(&self, file: &ExtFile) -> Result<()> {
692        let err = unsafe { libe2fs_sys::ext2fs_file_flush(file.0) };
693        if err == 0 {
694            Ok(())
695        } else {
696            report(err)
697        }
698    }
699
700    pub fn new_inode(&self, dir: u32, mode: u16) -> Result<ExtInode> {
701        let mut inode = MaybeUninit::uninit();
702        let fs = *self.0.read().unwrap();
703
704        debug!("creating new inode in dir {dir} with mode {mode}");
705        let err = unsafe {
706            libe2fs_sys::ext2fs_new_inode(
707                fs,
708                dir,
709                libe2fs_sys::LINUX_S_IFREG as i32 | 0o0600,
710                (*fs).inode_map,
711                inode.as_mut_ptr(),
712            )
713        };
714
715        if err == 0 {
716            let inum = unsafe { inode.assume_init() };
717            // let mut inode = self.read_inode(inum)?;
718            debug!("created inode: {inum}");
719            // once we have the inode, set its mode to be a file
720            let mut inode = libe2fs_sys::ext2_inode {
721                i_mode: mode | libe2fs_sys::LINUX_S_IFREG as u16,
722                i_uid: 0,
723                i_size: 0,
724                i_atime: 0,
725                i_ctime: 0,
726                i_mtime: 0,
727                i_dtime: 0,
728                i_gid: 0,
729                i_links_count: 0,
730                i_blocks: unsafe { (*fs).blocksize / 512 },
731                // set extents flag, since we like modern ext4 features
732                i_flags: libe2fs_sys::EXT4_EXTENTS_FL,
733                osd1: libe2fs_sys::ext2_inode__bindgen_ty_1 {
734                    linux1: libe2fs_sys::ext2_inode__bindgen_ty_1__bindgen_ty_1 { l_i_version: 0 },
735                },
736                i_block: [0; 15],
737                i_generation: 0,
738                i_file_acl: 0,
739                i_size_high: 0,
740                i_faddr: 0,
741                osd2: libe2fs_sys::ext2_inode__bindgen_ty_2 {
742                    linux2: libe2fs_sys::ext2_inode__bindgen_ty_2__bindgen_ty_1 {
743                        l_i_blocks_hi: 0,
744                        l_i_file_acl_high: 0,
745                        l_i_uid_high: 0,
746                        l_i_gid_high: 0,
747                        l_i_checksum_lo: 0,
748                        l_i_reserved: 0,
749                    },
750                },
751            };
752
753            unsafe {
754                let err =
755                    libe2fs_sys::ext2fs_iblk_set(fs, &mut inode as *mut libe2fs_sys::ext2_inode, 1);
756                if err != 0 {
757                    return report(err);
758                }
759                debug!("iblk_set");
760            }
761
762            debug!("attaching data block...");
763            // find the next free block and set it on the inode. this value
764            // will be written to the blocks bitmap later.
765            let data_block = self.new_block(&mut ExtInode(inum, inode))?;
766            debug!("data block: {data_block}");
767            // TODO: support directories later with ext2fs_new_dir_block!
768
769            // now that we know what our data block is, we need to add it to
770            // the inode's extents tree.
771            debug!("adding data block to extents tree...");
772
773            unsafe {
774                let mut handle = MaybeUninit::uninit();
775                let err =
776                    libe2fs_sys::ext2fs_extent_open2(fs, inum, &mut inode, handle.as_mut_ptr());
777                if err != 0 {
778                    return report(err);
779                }
780                let err =
781                    libe2fs_sys::ext2fs_extent_set_bmap(handle.assume_init(), 0, data_block, 0);
782                if err != 0 {
783                    return report(err);
784                }
785            }
786
787            debug!("uses {} 512b-i_blocks", inode.i_blocks);
788
789            // flush inode to disk!
790            debug!("writing new inode...");
791            // update inode group unused inodes area to remove this inode
792            // fs, inode, inuse, isdir
793            unsafe {
794                libe2fs_sys::ext2fs_inode_alloc_stats2(fs, inum, 1, 0);
795                libe2fs_sys::ext2fs_block_alloc_stats2(fs, data_block, 1);
796            }
797
798            let err = unsafe { libe2fs_sys::ext2fs_write_new_inode(fs, inum, &mut inode) };
799            if err == 0 {
800                self.flush()?;
801                Ok(ExtInode(inum, inode))
802            } else {
803                report(err)
804            }
805        } else {
806            report(err)
807        }
808    }
809
810    pub fn new_block(&self, inode: &mut ExtInode) -> Result<u64> {
811        let mut block = MaybeUninit::uninit();
812        let fs = *self.0.read().unwrap();
813        let err = unsafe {
814            libe2fs_sys::ext2fs_new_block2(
815                fs,
816                self.find_inode_goal(inode)?.0,
817                std::ptr::null_mut(),
818                block.as_mut_ptr(),
819            )
820        };
821        if err == 0 {
822            let block = unsafe { block.assume_init() };
823            debug!("created block {block}");
824            Ok(block)
825        } else {
826            report(err)
827        }
828    }
829
830    pub fn find_inode_goal(&self, inode: &mut ExtInode) -> Result<ExtBlock> {
831        let fs = *self.0.read().unwrap();
832        debug!("finding goal for inode {}", inode.0);
833        let block_number =
834            unsafe { libe2fs_sys::ext2fs_find_inode_goal(fs, inode.0, std::ptr::null_mut(), 0) };
835        Ok(ExtBlock(block_number))
836    }
837
838    pub fn next_free_block(&self) -> Result<u64> {
839        let fs = *self.0.read().unwrap();
840        let mut out = MaybeUninit::<u64>::uninit();
841        let res = unsafe {
842            // FIXME: THIS IS REALLY STUPID.
843            // Just search from the first data block to the end of the fs.
844            let fs = *fs;
845            let superblock = *fs.super_;
846            libe2fs_sys::ext2fs_find_first_zero_generic_bmap(
847                fs.block_map,
848                superblock.s_first_data_block as u64,
849                (superblock.s_blocks_count - superblock.s_first_data_block) as u64,
850                out.as_mut_ptr(),
851            )
852        };
853        if res != 0 {
854            return report(res);
855        }
856        let out = unsafe { out.assume_init() };
857        debug!("found next free block: block #{out}");
858        Ok(out)
859    }
860
861    pub fn inode_bitmap(&self) -> ExtInodeBitmap {
862        let fs = *self.0.read().unwrap();
863        ExtInodeBitmap(unsafe { *fs }.inode_map)
864    }
865
866    pub fn block_bitmap(&self) -> ExtBlockBitmap {
867        let fs = *self.0.read().unwrap();
868        ExtBlockBitmap(unsafe { *fs }.block_map)
869    }
870
871    pub fn mkdir<P: Into<PathBuf>, S: Into<String>>(&self, parent: P, name: S) -> Result<()> {
872        let parent = parent.into();
873        let name = name.into();
874        debug!(
875            "mkdir {}/{name}",
876            parent.display().to_string().trim_end_matches('/')
877        );
878        let name = CString::new(name)?;
879        let parent_inode = self.find_inode(&parent)?;
880        debug!("parent_inode: {}", parent_inode.0);
881        debug!("creating: {:?}", name);
882
883        let err = unsafe {
884            let fs = self.0.write().unwrap();
885            // pass 0 to automatically allocate new inode
886            // http://fs.csl.utoronto.ca/~sunk/libext2fs.html#Creating-and-expanding-directories
887            libe2fs_sys::ext2fs_mkdir(
888                *fs,
889                parent_inode.0,
890                0,
891                name.as_bytes_with_nul().as_ptr() as *mut _,
892            )
893        };
894        if err == 0 {
895            self.flush()?;
896            debug!("mkdir: success");
897            Ok(())
898        } else {
899            report(err)
900        }
901    }
902
903    pub fn read_bitmaps(&self) -> Result<()> {
904        let err =
905            unsafe { libe2fs_sys::ext2fs_read_bitmaps(self.0.read().unwrap().as_mut().unwrap()) };
906        if err == 0 {
907            Ok(())
908        } else {
909            report(err)
910        }
911    }
912
913    pub fn write_bitmaps(&self) -> Result<()> {
914        let err = unsafe {
915            // libe2fs_sys::ext2fs_write_bitmaps(self.0.read().unwrap().as_mut().unwrap())
916            let fs = *self.0.write().unwrap();
917            debug!("writing inode bitmap...");
918            let err = libe2fs_sys::ext2fs_write_inode_bitmap(&mut *fs as libe2fs_sys::ext2_filsys);
919            if err == 0 {
920                debug!("writing block bitmap...");
921                let err =
922                    libe2fs_sys::ext2fs_write_block_bitmap(&mut *fs as libe2fs_sys::ext2_filsys);
923                if err == 0 {
924                    debug!("done writing bitmaps");
925                    0
926                } else {
927                    err
928                }
929            } else {
930                err
931            }
932        };
933        if err == 0 {
934            Ok(())
935        } else {
936            debug!("writing bitmap failed with error {err}");
937            report(err)
938        }
939    }
940
941    pub fn flush(&self) -> Result<()> {
942        let fs = *self.0.write().unwrap();
943        unsafe {
944            (*fs).flags |= (libe2fs_sys::EXT2_FLAG_DIRTY | libe2fs_sys::EXT2_FLAG_CHANGED) as i32;
945        };
946        let err = unsafe { libe2fs_sys::ext2fs_flush(fs) };
947        if err == 0 {
948            Ok(())
949        } else {
950            dbg!(err);
951            report(err)
952        }
953    }
954
955    pub fn touch<P: Into<PathBuf>>(&self, path: P, mode: u16) -> Result<ExtFile> {
956        let fs = *self.0.write().unwrap();
957        let path = path.into();
958
959        let inum = unsafe {
960            let mut inum = MaybeUninit::<u32>::uninit();
961            let err = libe2fs_sys::ext2fs_namei(
962                fs,
963                Self::ROOT_INODE,
964                Self::ROOT_INODE,
965                CString::new(path.to_string_lossy().as_bytes())?.as_ptr(),
966                inum.as_mut_ptr(),
967            );
968            if err != 0 {
969                debug!("touch: could not find inum, allocating new inode");
970                self.new_inode(Self::ROOT_INODE, mode)?.0
971            } else {
972                inum.assume_init()
973            }
974        };
975
976        unsafe {
977            let file = {
978                let mut file = MaybeUninit::<libe2fs_sys::ext2_file_t>::uninit();
979                let err = libe2fs_sys::ext2fs_file_open2(
980                    fs,
981                    inum,
982                    std::ptr::null_mut(),
983                    (ExtFileOpenFlags::CREATE | ExtFileOpenFlags::WRITE).bits(),
984                    file.as_mut_ptr(),
985                );
986                if err != 0 {
987                    return report(err);
988                }
989                file.assume_init()
990            };
991
992            let fs = *self.0.write().unwrap();
993            let mut inode = self.get_inode(&ExtFile(file, ExtFileState::Open))?;
994            debug!("inode size: {}", inode.1.i_size);
995
996            inode.1.i_links_count = 1;
997
998            // write this inode
999            let err = libe2fs_sys::ext2fs_write_inode(fs, inum, &mut inode.1);
1000            if err != 0 {
1001                return report(err);
1002            }
1003            debug!("wrote inode");
1004
1005            // link the inode into the fs hierarchy!
1006            let parent_inum = self.find_inode(path.parent().unwrap())?.0;
1007            let file_name = path.file_name().unwrap();
1008            debug!("linking {file_name:?} @ {inum} to parent inode {parent_inum}");
1009            let file_name = CString::new(file_name.as_bytes())?;
1010            let err = libe2fs_sys::ext2fs_link(
1011                fs,
1012                parent_inum,
1013                file_name.as_ptr(),
1014                inum,
1015                libe2fs_sys::EXT2_FT_REG_FILE.try_into()?,
1016            );
1017            if err != 0 {
1018                return report(err);
1019            }
1020        }
1021
1022        self.flush()?;
1023
1024        let file = self.open_file(self.find_inode(path)?.0, Some(ExtFileOpenFlags::WRITE))?;
1025
1026        Ok(file)
1027    }
1028
1029    pub fn write_to_file<P: Into<PathBuf>>(&self, path: P, buf: &[u8]) -> Result<usize> {
1030        let fs = *self.0.write().unwrap();
1031        let path = path.into();
1032
1033        let inum = unsafe {
1034            let mut inum = MaybeUninit::<u32>::uninit();
1035            let err = libe2fs_sys::ext2fs_namei(
1036                fs,
1037                Self::ROOT_INODE,
1038                Self::ROOT_INODE,
1039                CString::new(path.to_string_lossy().as_bytes())?.as_ptr(),
1040                inum.as_mut_ptr(),
1041            );
1042            if err != 0 {
1043                debug!("write_to_file: could not find inum, allocating new inode");
1044                self.new_inode(Self::ROOT_INODE, 0)?.0
1045            } else {
1046                inum.assume_init()
1047            }
1048        };
1049
1050        let file = unsafe {
1051            let mut file = MaybeUninit::<libe2fs_sys::ext2_file_t>::uninit();
1052            let err = libe2fs_sys::ext2fs_file_open2(
1053                fs,
1054                inum,
1055                std::ptr::null_mut(),
1056                (ExtFileOpenFlags::CREATE | ExtFileOpenFlags::WRITE).bits(),
1057                file.as_mut_ptr(),
1058            );
1059            if err != 0 {
1060                return report(err);
1061            }
1062            file.assume_init()
1063        };
1064
1065        // write buf to file
1066        let mut written = 0;
1067        let err = unsafe {
1068            libe2fs_sys::ext2fs_file_write(
1069                file,
1070                buf.as_ptr() as *const libc::c_void,
1071                buf.len() as u32,
1072                &mut written,
1073            )
1074        };
1075        if err != 0 {
1076            return report(err);
1077        }
1078
1079        unsafe {
1080            let fs = *self.0.write().unwrap();
1081            let mut inode = self.get_inode(&ExtFile(file, ExtFileState::Open))?;
1082            // libe2fs_sys::ext2fs_file_close(file as *mut libe2fs_sys::ext2_file);
1083            // debug!("closed file");
1084            debug!("inode size: {}", inode.1.i_size);
1085
1086            inode.1.i_links_count = 1;
1087
1088            // write this inode
1089            let err = libe2fs_sys::ext2fs_write_inode(fs, inum, &mut inode.1);
1090            if err != 0 {
1091                return report(err);
1092            }
1093            debug!("wrote inode");
1094
1095            // link the inode into the fs hierarchy!
1096            let parent_inum = self.find_inode(path.parent().unwrap())?.0;
1097            let file_name = path.file_name().unwrap();
1098            debug!("linking {file_name:?} @ {inum} to parent inode {parent_inum}");
1099            let file_name = CString::new(file_name.as_bytes())?;
1100            let err = libe2fs_sys::ext2fs_link(
1101                fs,
1102                parent_inum,
1103                file_name.as_ptr(),
1104                inum,
1105                libe2fs_sys::EXT2_FT_REG_FILE.try_into()?,
1106            );
1107            if err != 0 {
1108                return report(err);
1109            }
1110        }
1111
1112        self.flush()?;
1113
1114        Ok(written as usize)
1115    }
1116
1117    pub fn unlink<P: Into<PathBuf>>(&self, path: P) -> Result<()> {
1118        let fs = *self.0.write().unwrap();
1119        let path = path.into();
1120        let file_name = path
1121            .file_name()
1122            .expect("cannot unlink files without a name");
1123
1124        let inode = self.find_inode(&path)?;
1125        let parent_inum = self
1126            .find_inode(path.parent().unwrap_or(PathBuf::from("/").as_path()))?
1127            .0;
1128
1129        debug!("unlinking {file_name:?}...");
1130        let err = unsafe {
1131            libe2fs_sys::ext2fs_unlink(
1132                fs,
1133                parent_inum,
1134                CString::from_vec_unchecked(file_name.as_bytes().to_vec()).as_ptr(),
1135                inode.0,
1136                0,
1137            )
1138        };
1139
1140        if err != 0 {
1141            return report(err);
1142        }
1143
1144        Ok(())
1145    }
1146
1147    pub fn link<P: Into<PathBuf>>(&self, path: P, new_path: P) -> Result<()> {
1148        let fs = *self.0.write().unwrap();
1149        let path = path.into();
1150        let new_path = new_path.into();
1151        let file_name = path
1152            .file_name()
1153            .expect("cannot unlink files without a name");
1154
1155        let mut inode = self.find_inode(&path)?;
1156        let new_parent_inum = self
1157            .find_inode(new_path.parent().unwrap_or(PathBuf::from("/").as_path()))?
1158            .0;
1159
1160        debug!("linking {file_name:?}...");
1161        let err = unsafe {
1162            libe2fs_sys::ext2fs_link(
1163                fs,
1164                new_parent_inum,
1165                CString::from_vec_unchecked(file_name.as_bytes().to_vec()).as_ptr(),
1166                inode.0,
1167                libe2fs_sys::EXT2_FT_REG_FILE.try_into()?,
1168            )
1169        };
1170
1171        if err != 0 {
1172            return report(err);
1173        }
1174
1175        inode.1.i_links_count += 1;
1176        let err = unsafe { libe2fs_sys::ext2fs_write_inode(fs, inode.0, &mut inode.1) };
1177        if err != 0 {
1178            return report(err);
1179        }
1180
1181        Ok(())
1182    }
1183
1184    pub fn delete<P: Into<PathBuf>>(&self, path: P) -> Result<()> {
1185        let fs = *self.0.write().unwrap();
1186        let path = path.into();
1187        let file_name = path
1188            .file_name()
1189            .expect("cannot unlink files without a name");
1190
1191        let mut inode = self.find_inode(&path)?;
1192        // TODO: Is this actually the right behaviour?
1193        let parent_inum = self
1194            .find_inode(path.parent().unwrap_or(PathBuf::from("/").as_path()))?
1195            .0;
1196
1197        debug!("unlinking {file_name:?}...");
1198        let err = unsafe {
1199            libe2fs_sys::ext2fs_unlink(
1200                fs,
1201                parent_inum,
1202                CString::from_vec_unchecked(file_name.as_bytes().to_vec()).as_ptr(),
1203                inode.0,
1204                0,
1205            )
1206        };
1207        if err != 0 {
1208            return report(err);
1209        }
1210
1211        inode.1.i_links_count -= 1;
1212        inode.1.i_dtime = SystemTime::now()
1213            .duration_since(SystemTime::UNIX_EPOCH)
1214            .unwrap()
1215            .as_secs() as u32;
1216
1217        let err = unsafe { libe2fs_sys::ext2fs_write_inode(fs, inode.0, &mut inode.1) };
1218        if err != 0 {
1219            return report(err);
1220        }
1221
1222        // obliterate any remaining blocks
1223        if unsafe { libe2fs_sys::ext2fs_inode_has_valid_blocks2(fs, &mut inode.1 as *mut _) != 0 } {
1224            let err = unsafe {
1225                libe2fs_sys::ext2fs_punch(
1226                    fs,
1227                    inode.0,
1228                    &mut inode.1 as *mut _,
1229                    std::ptr::null_mut(),
1230                    0,
1231                    u64::MAX,
1232                )
1233            };
1234            if err != 0 {
1235                return report(err);
1236            }
1237        }
1238
1239        unsafe {
1240            // fs, ino, in_use, is_dir
1241            libe2fs_sys::ext2fs_inode_alloc_stats2(fs, inode.0, -1, 0);
1242        }
1243
1244        self.flush()?;
1245
1246        Ok(())
1247    }
1248
1249    pub fn write_inode(&self, inode: &mut ExtInode) -> Result<()> {
1250        let err = unsafe {
1251            libe2fs_sys::ext2fs_write_inode(
1252                self.0.read().unwrap().as_mut().unwrap(),
1253                inode.0,
1254                &mut inode.1,
1255            )
1256        };
1257
1258        if err != 0 {
1259            report(err)
1260        } else {
1261            Ok(())
1262        }
1263    }
1264
1265    pub fn symlink<P1: AsRef<Path>, P2: AsRef<Path>>(
1266        &self,
1267        symlink_parent_dir: &ExtInode,
1268        symlink_inode: Option<&ExtInode>,
1269        symlink_name: P1,
1270        symlink_target_path: P2,
1271    ) -> Result<()> {
1272        let symlink_name = symlink_name.as_ref();
1273        let symlink_target_path = symlink_target_path.as_ref();
1274
1275        let symlink_target_path = CString::new(
1276            symlink_target_path
1277                .as_os_str()
1278                .to_string_lossy()
1279                .to_string(),
1280        )
1281        .unwrap();
1282        let symlink_name =
1283            CString::new(symlink_name.as_os_str().to_string_lossy().to_string()).unwrap();
1284
1285        unsafe {
1286            libe2fs_sys::ext2fs_symlink(
1287                self.0.read().unwrap().as_mut().unwrap(),
1288                symlink_parent_dir.0,
1289                symlink_inode.map(|i| i.0).unwrap_or(0),
1290                symlink_name.as_ptr(),
1291                symlink_target_path.as_ptr(),
1292            );
1293        };
1294
1295        Ok(())
1296    }
1297
1298    pub fn seek(&self, file: &ExtFile, offset: u64, direction: i32) -> Result<()> {
1299        debug!("seeking to {offset}");
1300        let err = unsafe {
1301            libe2fs_sys::ext2fs_file_llseek(file.0, offset, direction, std::ptr::null_mut())
1302        };
1303        if err == 0 {
1304            debug!("seek ok");
1305            Ok(())
1306        } else {
1307            report(err)
1308        }
1309    }
1310
1311    // #[cfg(target_os = "windows")]
1312    // pub fn default_io_manager() -> IoManager {
1313    //     unimplemented!("Windows support is not yet implemented")
1314    // }
1315
1316    // #[cfg(not(target_os = "windows"))]
1317    // pub fn default_io_manager() -> IoManager {
1318    //     DEFAULT_IO_MANAGER.clone()
1319    // }
1320}
1321
1322impl Drop for ExtFilesystem {
1323    fn drop(&mut self) {
1324        unsafe {
1325            debug!("drop: writing bitmaps...");
1326            self.write_bitmaps().unwrap();
1327            let fs = self.0.write().unwrap();
1328            debug!("closing fs...");
1329            let err = libe2fs_sys::ext2fs_close(fs.as_mut().unwrap());
1330            if err != 0 {
1331                Err::<(), ExtError>(ExtError::from(err as u32)).unwrap();
1332            }
1333        }
1334    }
1335}
1336
1337pub trait ExtBitmap {
1338    fn is_32bit(&self) -> bool;
1339    fn is_64bit(&self) -> bool;
1340}
1341
1342pub(crate) fn report<T>(error: i64) -> Result<T> {
1343    if error > 100_000 {
1344        let err: ExtEtMessage = error.into();
1345        Err(err.into())
1346    } else {
1347        let err: ExtError = (error as u32).into();
1348        Err(err.into())
1349    }
1350}
1351
1352pub type DirIteratorCallback = unsafe extern "C" fn(
1353    *mut libe2fs_sys::ext2_dir_entry,
1354    i32,
1355    i32,
1356    *mut i8,
1357    *mut ::std::ffi::c_void,
1358) -> i32;
1359
1360unsafe extern "C" fn dir_iterator_trampoline<F>(
1361    dir_entry: *mut libe2fs_sys::ext2_dir_entry,
1362    offset: i32,
1363    block_size: i32,
1364    buf: *mut i8,
1365    user_data: *mut ::std::ffi::c_void,
1366) -> i32
1367where
1368    F: FnMut(*mut libe2fs_sys::ext2_dir_entry, i32, i32, &str, &[i8]) -> Result<i32>,
1369{
1370    let name = CStr::from_ptr(unsafe { *dir_entry }.name.as_ptr())
1371        .to_str()
1372        .unwrap();
1373    debug!("got dir entry: {name}");
1374    let buf = std::slice::from_raw_parts(buf, block_size as usize);
1375    debug!("built buf!");
1376    let user_data = &mut *(user_data as *mut F);
1377    debug!("invoking user fn!");
1378    user_data(dir_entry, offset, block_size, name, buf).unwrap()
1379}
1380
1381fn get_dir_iterator_trampoline<F>(_closure: &F) -> DirIteratorCallback
1382where
1383    F: FnMut(*mut libe2fs_sys::ext2_dir_entry, i32, i32, &str, &[i8]) -> Result<i32>,
1384{
1385    dir_iterator_trampoline::<F>
1386}
1387
1388bitflags! {
1389    #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
1390    pub struct ExtFilesystemOpenFlags: i32 {
1391        const OPEN_RW = libe2fs_sys::EXT2_FLAG_RW as i32;
1392        const FORCE = libe2fs_sys::EXT2_FLAG_FORCE as i32;
1393        const JOURNAL_DEV_OK = libe2fs_sys::EXT2_FLAG_JOURNAL_DEV_OK as i32;
1394        const SKIP_MMP = libe2fs_sys::EXT2_FLAG_SKIP_MMP as i32;
1395        const OPEN_64BIT = libe2fs_sys::EXT2_FLAG_64BITS as i32;
1396    }
1397
1398    #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
1399    pub struct ExtFileOpenFlags: i32 {
1400        const WRITE = libe2fs_sys::EXT2_FILE_WRITE as i32;
1401        const CREATE = libe2fs_sys::EXT2_FILE_CREATE as i32;
1402    }
1403}
1404
1405#[cfg(test)]
1406mod tests {
1407    use std::fs;
1408    use std::path::Path;
1409
1410    use pretty_assertions::{assert_eq, assert_ne};
1411
1412    use super::*;
1413
1414    use eyre::Result;
1415
1416    #[ctor::ctor]
1417    fn initialize() {
1418        pretty_env_logger::init();
1419    }
1420
1421    pub struct TempImage(PathBuf, TempDir);
1422
1423    impl TempImage {
1424        pub fn new<P: Into<PathBuf>>(path: P) -> Result<Self> {
1425            let path = path.into();
1426            let tmp = TempDir::new()?;
1427            let mut tmp_path = tmp.path_view();
1428            tmp_path.push(path.file_name().unwrap());
1429            fs::copy(&path, &tmp_path)?;
1430
1431            Ok(Self(tmp_path, tmp))
1432        }
1433
1434        #[allow(unused)]
1435        pub fn path_view(&self) -> &Path {
1436            &self.0
1437        }
1438    }
1439
1440    impl Drop for TempImage {
1441        fn drop(&mut self) {
1442            std::fs::remove_file(&self.0).unwrap();
1443        }
1444    }
1445
1446    pub struct TempDir {
1447        path: PathBuf,
1448    }
1449
1450    impl TempDir {
1451        pub fn new() -> Result<TempDir> {
1452            let mut path = std::env::temp_dir();
1453            path.push(format!("flail-workdir-{}", rand::random::<u64>()));
1454            fs::create_dir_all(&path)?;
1455
1456            Ok(TempDir { path })
1457        }
1458
1459        pub fn path_view(&self) -> PathBuf {
1460            self.path.clone()
1461        }
1462    }
1463
1464    impl Drop for TempDir {
1465        fn drop(&mut self) {
1466            if self.path.exists() {
1467                std::fs::remove_dir_all(&self.path).unwrap();
1468            }
1469        }
1470    }
1471
1472    #[test]
1473    pub fn test_reading_directories_works() -> Result<()> {
1474        let fs = ExtFilesystem::open(
1475            "./fixtures/hello-world.ext4",
1476            None,
1477            Some(ExtFilesystemOpenFlags::OPEN_64BIT | ExtFilesystemOpenFlags::OPEN_RW),
1478        )?;
1479
1480        fs.iterate_dir(
1481            "/",
1482            |dir_entry: *mut libe2fs_sys::ext2_dir_entry,
1483             _offset,
1484             _block_size,
1485             name: &str,
1486             _priv_data| {
1487                assert_ne!((unsafe { *dir_entry }).inode, 0);
1488                debug!("reading inode {}", unsafe { *dir_entry }.inode);
1489                debug!("got path: {name}!!!");
1490                assert_ne!(name.len(), 0);
1491                Ok(0)
1492            },
1493        )?;
1494
1495        Ok(())
1496    }
1497
1498    #[test]
1499    pub fn test_read_write_works() -> Result<()> {
1500        let img = TempImage::new("./fixtures/empty.ext4")?;
1501
1502        let fs = ExtFilesystem::open(
1503            img.path_view(),
1504            None,
1505            Some(ExtFilesystemOpenFlags::OPEN_64BIT | ExtFilesystemOpenFlags::OPEN_RW),
1506        )?;
1507
1508        let data = "hello flail!";
1509
1510        let inode = fs.new_inode(ExtFilesystem::ROOT_INODE, 0o700)?;
1511        let written = {
1512            let file = fs.open_file(
1513                inode.num(),
1514                Some(ExtFileOpenFlags::CREATE | ExtFileOpenFlags::WRITE),
1515            )?;
1516            debug!("write data: '{data}'");
1517            let written = fs.write_file(&file, data.as_bytes())?;
1518            assert_eq!(data.len(), written);
1519            debug!("wrote {written} bytes");
1520            written
1521        };
1522
1523        {
1524            let file = fs.open_file(inode.num(), None)?;
1525            let mut out_buffer = vec![0u8; data.len()];
1526            let read = fs.read_file(&file, &mut out_buffer)?;
1527            assert_eq!(written, read);
1528            debug!("read {read} bytes");
1529            assert_eq!(data.as_bytes(), out_buffer.as_slice());
1530        }
1531
1532        Ok(())
1533    }
1534
1535    #[test]
1536    pub fn test_mkdir_works() -> Result<()> {
1537        let img = TempImage::new("./fixtures/empty.ext4")?;
1538
1539        let fs = ExtFilesystem::open(
1540            img.path_view(),
1541            None,
1542            Some(ExtFilesystemOpenFlags::OPEN_64BIT | ExtFilesystemOpenFlags::OPEN_RW),
1543        )?;
1544
1545        fs.mkdir("/", "foo")?;
1546
1547        let inode = fs.find_inode("/foo")?;
1548        assert_eq!(true, inode.0 > 0);
1549
1550        Ok(())
1551    }
1552
1553    #[test]
1554    pub fn test_passes_fsck() -> Result<()> {
1555        {
1556            let img = TempImage::new("./fixtures/empty.ext4")?;
1557
1558            {
1559                let fs = ExtFilesystem::open(
1560                    img.path_view(),
1561                    None,
1562                    Some(ExtFilesystemOpenFlags::OPEN_64BIT | ExtFilesystemOpenFlags::OPEN_RW),
1563                )?;
1564
1565                fs.mkdir("/", "foo")?;
1566            }
1567
1568            let fsck = std::process::Command::new("fsck.ext4")
1569                .arg("-f")
1570                .arg("-n")
1571                .arg(img.path_view())
1572                .spawn()?
1573                .wait()?;
1574
1575            assert!(fsck.success());
1576        }
1577
1578        {
1579            let img = TempImage::new("./fixtures/empty.ext4")?;
1580
1581            {
1582                // write /test.txt
1583                let fs = ExtFilesystem::open(
1584                    img.path_view(),
1585                    None,
1586                    Some(ExtFilesystemOpenFlags::OPEN_64BIT | ExtFilesystemOpenFlags::OPEN_RW),
1587                )?;
1588
1589                let data = "hello flail";
1590
1591                debug!("write data: '{data}'");
1592                let written = fs.write_to_file("/test.txt", data.as_bytes())?;
1593
1594                assert_eq!(data.len(), written);
1595                debug!("wrote {written} bytes");
1596            }
1597
1598            let fsck = std::process::Command::new("fsck.ext4")
1599                .arg("-f")
1600                .arg("-n")
1601                .arg(img.path_view())
1602                .spawn()?
1603                .wait()?;
1604
1605            assert!(fsck.success());
1606
1607            {
1608                // read /test.txt
1609                let fs = ExtFilesystem::open(
1610                    img.path_view(),
1611                    None,
1612                    Some(ExtFilesystemOpenFlags::OPEN_64BIT | ExtFilesystemOpenFlags::OPEN_RW),
1613                )?;
1614
1615                let mut out_buffer = vec![0u8; 11];
1616
1617                let inode = fs.lookup("/", "/test.txt")?;
1618                let file = fs.open_file(inode.0, None)?;
1619                let read = fs.read_file(&file, &mut out_buffer)?;
1620
1621                assert_eq!(11, read);
1622                debug!("read {read} bytes");
1623                assert_eq!("hello flail", std::str::from_utf8(&out_buffer)?);
1624            }
1625
1626            let fsck = std::process::Command::new("fsck.ext4")
1627                .arg("-f")
1628                .arg("-n")
1629                .arg(img.path_view())
1630                .spawn()?
1631                .wait()?;
1632
1633            assert!(fsck.success());
1634
1635            {
1636                // unlink /test.txt
1637                let fs = ExtFilesystem::open(
1638                    img.path_view(),
1639                    None,
1640                    Some(ExtFilesystemOpenFlags::OPEN_64BIT | ExtFilesystemOpenFlags::OPEN_RW),
1641                )?;
1642
1643                fs.delete("/test.txt")?;
1644            }
1645
1646            let fsck = std::process::Command::new("fsck.ext4")
1647                .arg("-f")
1648                .arg("-n")
1649                .arg(img.path_view())
1650                .spawn()?
1651                .wait()?;
1652
1653            assert!(fsck.success());
1654        }
1655
1656        Ok(())
1657    }
1658
1659    #[test]
1660    pub fn test_making_new_fs_works() -> Result<()> {
1661        let temp = TempDir::new()?;
1662        let img = temp.path_view().join("test.img");
1663        dbg!(&img);
1664
1665        // create 16M fs image
1666        {
1667            let fs = ExtFilesystem::create(&img, 16 * 1024 * 1024)?;
1668            let data = "hello flail";
1669
1670            debug!("write data: '{data}'");
1671            let written = fs.write_to_file("/test.txt", data.as_bytes())?;
1672
1673            assert_eq!(data.len(), written);
1674            debug!("wrote {written} bytes");
1675        }
1676
1677        let fsck = std::process::Command::new("fsck.ext4")
1678            .arg("-f")
1679            .arg("-n")
1680            .arg(img.clone())
1681            .spawn()?
1682            .wait()?;
1683
1684        assert!(fsck.success());
1685
1686        {
1687            // read /test.txt
1688            let fs = ExtFilesystem::open(
1689                &img,
1690                None,
1691                Some(ExtFilesystemOpenFlags::OPEN_64BIT | ExtFilesystemOpenFlags::OPEN_RW),
1692            )?;
1693
1694            let mut out_buffer = vec![0u8; 11];
1695
1696            let inode = fs.lookup("/", "/test.txt")?;
1697            let file = fs.open_file(inode.0, None)?;
1698            let read = fs.read_file(&file, &mut out_buffer)?;
1699
1700            assert_eq!(11, read);
1701            debug!("read {read} bytes");
1702            assert_eq!("hello flail", std::str::from_utf8(&out_buffer)?);
1703        }
1704
1705        Ok(())
1706    }
1707}