Skip to main content

Filesystem

Struct Filesystem 

Source
pub struct Filesystem {
    pub dev: Arc<dyn BlockDevice>,
    pub sb: Superblock,
    pub groups: Vec<BlockGroupDescriptor>,
    pub csum: Checksummer,
    pub flavor: FsFlavor,
    pub journal: Option<Mutex<JournalWriter>>,
    /* private fields */
}

Fields§

§dev: Arc<dyn BlockDevice>§sb: Superblock§groups: Vec<BlockGroupDescriptor>§csum: Checksummer§flavor: FsFlavor

Dialect detected at mount time from the superblock’s feature flags. Drives runtime dispatch where ext2 / ext3 / ext4 differ — most notably the inode block-mapping scheme (extent vs indirect) used when allocating new inodes.

§journal: Option<Mutex<JournalWriter>>

Live-write journal writer, present iff the FS has a journal AND the device is writable. None for read-only mounts and for ext2- style images. Locked per-op so mutating capi calls serialize on the JBD2 sequence cursor.

Implementations§

Source§

impl Filesystem

Source

pub fn mount_recovering(dev: Arc<dyn BlockDevice>) -> Result<Self>

Open an exclusively owned, backed-up device with checked journal recovery. Opening this writable handle is already a mutation: retain an external backup before calling. Only plain JBD2 (no transaction checksums, fast commit or async commit) is currently qualified by this lifecycle.

Source

pub fn finish(self) -> Result<()>

Finish a checked mount, propagating flush errors. Dropping a handle does not claim a clean release: the recovery marker remains for the next owner. After any I/O failure, release ownership and reopen rather than reusing it.

Source

pub fn flush(&self) -> Result<()>

Flush a live mount without clearing its recovery marker or releasing it. Checked journal transactions must already be fully checkpointed. After an error the owner must retire the mount, just as for a failed mutation.

Takes &self so a handle shared between threads can flush while other calls run. The journal writer is held for the whole check, so a transaction another thread is committing either finishes first or starts after: the check never sees one half-done.

Source

pub fn cache_pinned_blocks(&self) -> usize

How many blocks the buffer cache holds pinned: bytes not yet at their final location on the device, which no capacity bound can evict. A read-only mount that replayed a dirty journal pins what it replayed; a writable mount checkpoints every commit, so it pins nothing.

Source

pub fn fresh_read(&mut self) -> Result<()>

Flush and discard checkpointed read caches before physical readback. The mount remains owned and usable. This is not concurrent-writer support: callers must serialize all filesystem access and retire on I/O failure.

Source

pub fn mount(dev: Arc<dyn BlockDevice>) -> Result<Self>

Mount the ext4 filesystem on dev. Read-only unless the device reports is_writable(), in which case a dirty journal is replayed before returning so callers see a consistent on-disk state.

When RO_COMPAT_METADATA_CSUM is set, the superblock checksum is verified — failure aborts the mount with Error::BadChecksum.

Source

pub fn mount_with_cache( dev: Arc<dyn BlockDevice>, blocks: usize, ) -> Result<Self>

Filesystem::mount with a buffer cache of blocks clean blocks rather than DEFAULT_CACHE_BLOCKS. Zero keeps none, so every read reaches the device: the baseline tests/read_path_cost.rs measures the cache against (#68). Journaled blocks awaiting checkpoint are held whatever the capacity.

Source

pub fn mount_lazy(dev: Arc<dyn BlockDevice>) -> Result<Self>

Like mount, but skips the mount-time journal replay even when the device is writable. The caller is responsible for invoking Filesystem::replay_journal_if_dirty once the underlying write path is actually ready to service writes (e.g. in the FSKit case the kernel-level write FD on FSBlockDeviceResource only becomes writable AFTER loadResource returns successfully — replaying mid- loadResource produces EIO).

Until replay runs, reads observe the on-disk pre-replay state and any write through this handle fails with Error::JournalNotReplayed while the journal still says dirty (#375): a commit would overwrite the unreplayed log. This is the lazy/deferred-replay sibling of mount; for most callers mount is correct.

Source

pub fn mount_with_runtime( dev: Arc<dyn BlockDevice>, runtime: Arc<dyn Runtime>, ) -> Result<Self>

Mount with caller-provided wall time and inode-generation policy. The provider must remain valid for the entire mount, including recovery.

Source

pub fn set_volume_label(&mut self, label: &[u8]) -> Result<()>

Set the volume label, s_volume_name: the 16 bytes at superblock offset 0x78, NUL-padded (#447).

label is at most VOLUME_LABEL_MAX bytes and holds no NUL, since the field ends at its first NUL; anything else is InvalidArgument, and nothing is written. An empty label clears it. The label is bytes, as the format stores it: UTF-8 is what tools print, not what the format requires.

Written to the primary superblock and to every backup the volume’s own layout places (the groups Superblock::group_has_super names), each with its checksum restamped on metadata_csum volumes, in one transaction. tune2fs -L does the same: after it, the backup in group 1 carries the new label too, and e2fsck -fn passes the volume (tests/volume_label_oracle.rs). A backup slot that does not hold a superblock (no magic) is left as it is.

Source

pub fn replay_journal_if_dirty(&mut self) -> Result<usize>

Takes &mut self because a replay that applied anything leaves the mount’s own view behind (#376): the superblock and descriptors were read before it, and a journal writer opened over the dirty log holds its pre-replay sequence. All three are brought up to date here, as an eager mount does before it opens its writer.

Source

pub fn orphan_list(&self) -> Result<Vec<u32>>

Phase 6.1 — walk the orphan inode chain rooted at s_last_orphan and return its members in chain order.

The chain has TWO kinds of member, and they are not distinguished here — see Filesystem::recover_orphans, which branches on i_links_count to tell them apart. An inode with no links is an unlink-while-open: its blocks and its inode should be reclaimed. An inode that still has links is a truncate() a crash interrupted: it is still named by its directory entries and only the blocks past i_size should go.

The chain is encoded by overloading i_dtime as “next orphan inode number”; the chain terminates when dtime == 0. We cap at inodes_count to avoid runaway loops on cycle-corrupted images.

Read-only (no recovery yet — that’s Phase 6.2). Returns Ok([]) when there are no orphans.

Source

pub fn recover_orphans(&self) -> Result<usize>

Phase 6.2 — orphan replay.

§THE CHAIN HAS TWO KINDS OF MEMBER AND THEY GET OPPOSITE

TREATMENT

The format documentation (“Orphan file”) names both reasons an inode is on the chain; i_links_count tells them apart, and this branches on it:

  • No links — an unlink-while-open. Nothing names it any more, so its data blocks and its inode-bitmap slot are freed and its body is zeroed with i_dtime = now.
  • Links remaining — a truncate() that a crash interrupted. i_size was already lowered before the machine went down; the blocks past it were not yet freed. The file is still named by its directory entries, so recovery FINISHES THE TRUNCATE and leaves the file in place: free what lies past i_size, rewrite the extent root and i_blocks, clear i_dtime, and touch nothing else.

Treating the second kind as the first is what this used to do, and it destroyed data: the inode of a file the user never deleted was freed and its body — including the block pointers that were the only way back to its contents — zeroed, while the directory entries naming it were left pointing at a free inode.

A member’s only link to the next is its own i_dtime, which both treatments overwrite. So each member is its own commit, which also moves s_last_orphan on to the next member, and in that commit the member’s i_dtime still holds the link. Without a journal the superblock is written last, so a cut anywhere in the commit leaves the head on this member with the link intact, and the next mount walks through it to the rest (see Self::orphan_chain_to_recover). The final i_dtime (a deletion time, or zero for a finished truncate) is written in the NEXT member’s commit, once the head is durably past it. The last member has nothing behind it to lose and gets its final i_dtime at once.

All in one commit, a cut after the head’s group and before a later member’s left the head free with a deletion time and the later member allocated, and the next mount cleared the chain at the head and stranded the rest (Greptile on #201). A cut between two commits can still leave one member off the chain with its link for an i_dtime, which e2fsck -p resets without asking.

Returns the number of orphan inodes reclaimed — completed truncates are not counted, because nothing was reclaimed. No-op (returns 0) when the chain is empty or the device is read-only.

Designed to be called from the mount path AFTER journal replay, so the orphans we’re about to reclaim are guaranteed not still in use by an in-flight kernel-level transaction.

Source

pub fn read_block(&self, block_num: u64) -> Result<Vec<u8>>

Read a whole block by its logical block number. Routes through self.dev, which at mount time is wrapped in a CachedDevice — so this single call benefits from the buffer cache that holds post-commit, pre-checkpoint journaled writes.

Source

pub fn read_inode_raw(&self, ino: u32) -> Result<Vec<u8>>

Read raw inode bytes for a given inode number (does not parse).

Source

pub fn read_inode_verified(&self, ino: u32) -> Result<(Inode, Vec<u8>)>

Read + parse + checksum-verify an inode in one shot.

When RO_COMPAT_METADATA_CSUM is enabled the inode CRC32C is checked (salted by inode number + generation per ext4 spec). A mismatch returns Error::BadChecksum { what: "inode" }.

The target of the symlink at ino, exactly as stored and without a terminating NUL.

A target shorter than i_block (60 bytes) is a fast symlink, held inline in i_block; a longer one is a slow symlink, held in data blocks. That is the boundary the format documentation gives (“Symbolic Links”: inline if the target is less than 60 bytes).

Error::InvalidArgument when the inode is not a symlink, Error::Corrupt when it declares a target longer than any path, and Error::Unsupported when the target is encrypted.

Source

pub fn byte_ceiling(&self) -> Result<u64>

The most bytes anything on this filesystem can really occupy: blocks_count * block_size, checked rather than saturated, and never more than the device (#321). Mount already refuses a superblock larger than its device; taking the minimum here as well keeps the bound honest for a caller that reaches these paths with a superblock mount did not vet.

Source

pub fn lookup_path_bytes(&self, path: &[u8]) -> Result<u32>

Resolve path to an inode number, verifying directory blocks.

A path is bytes, compared byte for byte against the entry names, which have no encoding: one that is not UTF-8 names exactly the file whose name it holds, and one naming nothing is Error::NotFound. Every path-addressed apply_* resolves through here; each &str form is its byte form with str::as_bytes (#418).

Source

pub fn stat_ino(&self, r: impl Into<InodeRef>) -> Result<Inode>

The attributes of the inode r names; Error::Stale if it no longer names a file (see InodeRef).

Source

pub fn lookup_at(&self, dir: impl Into<InodeRef>, name: &[u8]) -> Result<u32>

The inode number name has in directory dir — one step of a path walk. name is bytes, compared exactly, so a name that is not UTF-8 is found. . and .. are ordinary entries here.

Source

pub fn read_dir_ino(&self, dir: impl Into<InodeRef>) -> Result<Vec<DirEntry>>

Every entry of directory dir, . and .. included, in on-disk order. Directory listing by path resolves and calls this.

Source

pub fn read_ino( &self, r: impl Into<InodeRef>, offset: u64, out: &mut [u8], ) -> Result<usize>

Read up to out.len() bytes of the regular file r names, from byte offset. Returns the number of bytes read, short at the end of the file.

The target of the symlink r names; see read_link.

Source

pub fn apply_truncate_ino( &self, r: impl Into<InodeRef>, new_size: u64, ) -> Result<()>

Set the size of the regular file r names, freeing what a shrink drops; a grow is sparse. Error::IsADirectory for a directory and Error::InvalidArgument for any other non-regular file.

Source

pub fn map_inode_logical( &self, inode: &Inode, logical_block: u64, ) -> Result<Option<u64>>

Map a logical block within inode to its physical block, choosing between the extent tree and the legacy direct/indirect scheme based on EXT4_EXTENTS_FL. Returns None for sparse holes and (for the extent path) uninitialised extents — callers wanting zeros there must handle the None case explicitly.

This is the per-inode dispatcher every directory traversal / extent-walking call site should use instead of touching extent::map_logical directly — without it, an ext2/3 inode with raw block pointers in i_block gets misparsed as an extent header (yielding CorruptExtentTree("bad extent header magic")).

An inline-data inode has no block map, so it is refused with Error::Unsupported (#382). The directory writers hand an inline directory to the inline-directory editors before they get here (#428); this refusal is what stops any other caller writing through its bytes.

The indirect path internally maintains its own block cache for the duration of the call; sequential lookups via repeated calls don’t share that cache (file_io’s read paths build a longer-lived cache to amortize across blocks).

Source

pub fn write_inode_raw(&self, ino: u32, raw: &[u8]) -> Result<()>

Write the given raw inode bytes back to disk. Refused as every other write is (refuse_write): Error::ReadOnly on a read-only device, and a volume carrying a feature this driver must not write is left untouched. The first write marks the volume not clean (#323).

Not checksum-aware: callers that update fields affecting the inode CRC32C (anything except checksum_lo / checksum_hi) must recompute

  • patch the checksum into raw before calling this. Not wrapped in a journal transaction — see E11 / journal_apply for the journaled version. Use only when the caller has the full write-ordering story under control.
Source

pub fn patch_inode_size_and_blocks( raw: &mut [u8], new_size: u64, new_block_count: u64, ) -> Result<()>

Patch fields in a raw inode image: size, blocks_count. Leaves all other bytes (including the extent tree header + entries in i_block) intact. new_block_count is in 512-byte sectors per spec (same convention as Inode::blocks).

Source

pub fn patch_inode_block_area(raw: &mut [u8], new_root: &[u8]) -> Result<()>

Overwrite the 60-byte i_block area of an inode image with new_root. Used when an extent-tree mutation changes the inline root.

Source

pub fn apply_truncate_shrink(&self, ino: u32, new_size: u64) -> Result<()>

Shrink a file to new_size. Composes file_mut::plan_truncate_shrink (extent-tree updates + freed-block ranges) with actual disk writes — rewrites the inode and zeros the freed bitmap bits.

The inode write, the bitmap writes, the BGD and the superblock accumulate into one BlockBuffer and commit together. On a mount with a journal that commit is one transaction, atomic with respect to a crash. Without one (an ext2 volume, or ext4 formatted without a journal) commit_block_buffer writes the blocks in turn, and a crash part-way leaves some written and some not (#179).

This said “Not journaled … safe only in a test scratch image”, and promised the transaction as future work. The future work landed; the warning outlived it and was steering callers away from an API that is safe.

Source

pub fn apply_truncate_grow(&self, ino: u32, new_size: u64) -> Result<()>

Source

pub fn apply_fallocate_keep_size( &self, ino: u32, offset: u64, len: u64, ) -> Result<()>

Phase 2.2: fallocate(FALLOC_FL_KEEP_SIZE) — preallocate blocks in the byte range [offset, offset+len) as uninitialized extents. The blocks are reserved (count against i_blocks) but reads return zeros until they’re written. i_size is left unchanged per KEEP_SIZE semantics.

v1 limitations:

  • Range must be entirely unmapped — partially-overlapping ranges return Error::InvalidArgument. (Splitting around existing extents is a follow-up.)
  • Single contiguous physical allocation. If the bitmap can’t serve ceil(len / block_size) contiguous blocks, returns Error::Corrupt("no group has a contiguous free run...").
  • The new extent goes into the inline root while it has a free slot; a full root is promoted to an index node and a deeper tree is descended, with the node blocks drawn in the same transaction.
Source

pub fn apply_fallocate_punch_hole( &self, ino: u32, offset: u64, len: u64, ) -> Result<()>

Phase 2.3 — fallocate(FALLOC_FL_PUNCH_HOLE | FALLOC_FL_KEEP_SIZE). Frees the data blocks wholly inside [offset, offset+len), splitting straddling extents as needed, and zeroes the part of a block at either end that the range covers only in part. Reads of the punched range return zeros (sparse hole) thereafter; i_size is unchanged.

Extent trees of any depth. The surviving extents are laid out again by extent_mut::plan_repack_tree over the tree blocks the file already holds (#258): four or fewer go in the inode, more get as many leaf and index levels as they need, and tree blocks left unused are freed with the data. A punch inside a single extent leaves a head and a tail where there was one record, so the layout can be one block short; that block is allocated, and a full volume fails the punch with the allocator’s error rather than for size.

Limits:

  • Indirect-block (ext2/3) inodes return EINVAL — punch is an ext4-specific kernel API.
Source

pub fn apply_fallocate_zero_range( &self, ino: u32, offset: u64, len: u64, ) -> Result<()>

Phase 2.4 — fallocate(FALLOC_FL_ZERO_RANGE). Logically zero the byte range [offset, offset+len). Implemented as punch-hole, which zeroes the partial edge blocks in place, + KEEP_SIZE preallocate of the whole blocks between them, so reads return zeros (uninitialized-extent semantics) and future writes don’t need an allocation.

Two separate transactions today (punch then alloc); a future optimization could fold them into one.

Source

pub fn apply_chmod(&self, path: &str, mode: u16) -> Result<()>

Change the permission bits on path. Only the low 12 bits of mode (S_ISUID|S_ISGID|S_ISVTX plus rwx/rwx/rwx) are applied; the file-type bits (S_IFMT) are preserved from the existing inode.

Updates i_ctime = now and recomputes the inode checksum on csum- enabled mounts. Returns Error::NotFound if the path doesn’t resolve, Error::ReadOnly on a RO mount.

Source

pub fn apply_chmod_ino(&self, r: impl Into<InodeRef>, mode: u16) -> Result<()>

apply_chmod on the inode r names.

Source

pub fn apply_chown(&self, path: &str, uid: u32, gid: u32) -> Result<()>

Change the owner of path to (uid, gid). Both values are full 32-bit — the inode stores them as hi+lo u16 halves at different offsets per the ext4 on-disk format. Passing u32::MAX for either field leaves that value untouched (Linux lchown(2) convention).

Updates i_ctime = now and recomputes the inode checksum on csum-enabled mounts.

Source

pub fn apply_chown_ino( &self, r: impl Into<InodeRef>, uid: u32, gid: u32, ) -> Result<()>

apply_chown on the inode r names.

Source

pub fn apply_set_flags(&self, path: &str, flags: u32) -> Result<()>

Set the i_flags field (FS_IOC_SETFLAGS) for the inode at path.

Bumps ctime. Fails with Error::ReadOnly on read-only mounts, or Error::InvalidArgument if flags changes any bit outside crate::inode::USER_MODIFIABLE_FLAGS — INDEX, EXTENTS, INLINE_DATA, HUGE_FILE, ENCRYPT, VERITY, CASEFOLD and the like describe how the inode’s existing bytes are read, so flipping one without rewriting them would corrupt the file. Bits outside the mask that are already set may be passed back unchanged.

Source

pub fn apply_removexattr(&self, path: &str, name: &str) -> Result<()>

Remove the extended attribute named name from the inode at path. name must carry a known namespace prefix (e.g. "user.color").

v1 scope: in-inode xattrs only. The in-inode region (bytes between 128 + i_extra_isize and the end of the on-disk inode) is decoded, the matching entry is dropped, and the region is re-encoded in place. External xattr blocks (pointed at by Search the in-inode region first, then the external xattr block. If the external block becomes empty after removal, free it and zero i_file_acl (matches kernel behavior — empty xattr blocks are reaped on the spot rather than left dangling).

Returns:

  • Ok(()) on success.
  • Error::NotFound if the entry isn’t present in either region.
  • Error::InvalidArgument on namespace-prefix issues.
Source

pub fn apply_setxattr(&self, path: &str, name: &str, value: &[u8]) -> Result<()>

Set (create or replace) the extended attribute name with value on the inode at path. name must carry a known namespace prefix (e.g. "user.com.apple.FinderInfo").

Try-order, matching the kernel:

  1. In-inode region — between 128 + i_extra_isize and the end of the on-disk inode. Cheapest; no extra block.
  2. External xattr block — when in-inode is full, fall back to a dedicated block referenced by i_file_acl. Allocates a fresh block when none exists, otherwise rewrites the existing one. Returns Error::NoSpaceLeftOnDevice if even a full block can’t hold the new layout.
Source

pub fn apply_utimens( &self, path: &str, atime_sec: i64, atime_nsec: u32, mtime_sec: i64, mtime_nsec: u32, ) -> Result<()>

Set the access + modification times on path. Mirrors POSIX utimensat(2): atime_sec/nsec and mtime_sec/nsec each replace the inode’s atime/mtime. ctime is bumped to now (POSIX requires the change-time stamp on any attribute write). The TIME_OMIT sentinel on either _sec leaves that pair unchanged (lets callers touch just atime or just mtime).

Each _nsec also takes utimensat(2)’s sentinels: UTIME_OMIT leaves that pair unchanged and UTIME_NOW sets it to the mount’s current time, the _sec beside either being ignored. When both pairs are omitted nothing is written, ctime included, as on Linux. Any other _nsec of 1e9 or more is InvalidArgument, refused before anything is written.

Seconds are signed and 64-bit because that is what the format means: the on-disk base is a signed 32-bit count, extended by the low two bits of the matching *_extra field. A u32 here could not express a pre-1970 date at all, and stored every date past 2038 as one in the 1900s — the base was written and the epoch bits left zero, so the value read back 136 years early.

nsec values are the sub-second timestamp in nanoseconds and are only written when the inode’s i_extra_isize region is large enough to hold them (requires ≥ 160-byte inodes — the ext4 tooling default). That same region holds the epoch bits, so on an inode too small to carry it, a time needing them is refused rather than silently stored as the wrong century.

Source

pub fn apply_utimens_ino( &self, r: impl Into<InodeRef>, atime_sec: i64, atime_nsec: u32, mtime_sec: i64, mtime_nsec: u32, ) -> Result<()>

apply_utimens on the inode r names.

Unlink a regular file / symlink / special file at path.

Semantics:

  • Refuses to unlink a directory (use a future apply_rmdir).
  • Decrements the target inode’s i_links_count. When that reaches zero, frees every data block via plan_truncate_shrink(size → 0), clears the inode bitmap bit, zeroes the inode body, and sets i_dtime = now. When links_count > 1 we only drop the dir entry and decrement — matches POSIX unlink semantics for hard-linked files.
  • Mutates: parent-dir block (entry removal), target inode, block + inode bitmaps, BGD counters, SB counters. No journaling yet — safe only on scratch images (same caveat as apply_truncate_shrink).

Returns Error::NotFound if the path doesn’t exist, Error::NotADirectory if the parent isn’t a directory, and Error::IsADirectory (POSIX EISDIR) if the target is a directory.

apply_unlink of entry name in directory dir.

Source

pub fn apply_create(&self, path: &str, mode: u16) -> Result<u32>

Create a new regular file at path with permission bits mode (e.g. 0o644). Returns the allocated inode number on success.

Semantics:

  • Parent must exist and be a directory.
  • Refuses if path already exists.
  • Allocates an inode via plan_inode_allocation (hints to the parent’s group), marks the bitmap, bumps BGD + SB counters.
  • Initialises the inode as a regular file with EXTENTS flag and an empty extent tree (size=0, blocks=0). Timestamps set to now.
  • Adds the directory entry into the first parent block with room (linear; htree-extending dirs are a follow-up).
  • Not journaled — scratch-image safe, same caveat as other Phase-4 applies.
Source

pub fn apply_create_at( &self, dir: impl Into<InodeRef>, name: &[u8], mode: u16, ) -> Result<u32>

apply_create of entry name in directory dir. name is bytes and need not be UTF-8.

Source

pub fn apply_mknod( &self, path: &str, mode: u16, major: u32, minor: u32, ) -> Result<u32>

Create a special file (FIFO, socket, char device, block device). mode must include the type bits (S_IFIFO, S_IFSOCK, S_IFCHR, or S_IFBLK) plus the permission bits. major and minor are the device numbers (both 0 for FIFOs and sockets). Mirrors POSIX mknod.

Source

pub fn apply_mknod_at( &self, dir: impl Into<InodeRef>, name: &[u8], mode: u16, major: u32, minor: u32, ) -> Result<u32>

apply_mknod of entry name in directory dir.

Create a symbolic link at linkpath whose target is target. Mirrors POSIX symlink(target, linkpath): allocates a fresh inode with mode S_IFLNK, installs the target bytes, and adds a dir entry at the link path.

Two storage paths:

  • Fast symlink (target.len() <= 60): target stored inline in the 60-byte i_block area; no data-block allocation.
  • Slow symlink (61..=255 bytes): one filesystem block is allocated and the target is written there, with an EXTENTS i_block pointing at it.

POSIX caps symlink targets at SYMLINK_MAX (255 bytes on Linux + macOS). Longer returns Error::NameTooLong → ENAMETOOLONG.

apply_symlink: entry name in directory dir, pointing at target. Both are bytes and need not be UTF-8.

Source

pub fn apply_replace_file_content(&self, path: &str, data: &[u8]) -> Result<u64>

Replace the content of path with data. The file must already exist. Frees every existing extent, allocates a single contiguous run of blocks large enough for data, writes the bytes (zero-padding the tail of the last block), then inserts one extent into the inode.

This is the “Finder just saved a document” path — complete rewrite of a file. Piecewise writes / appends / sparse writes come later.

For an extent-mapped inode on a mount with a journal, atomic across the whole replace: freeing the old data, allocating the new run, the bitmap, BGD and superblock updates, the new block contents and the inode all commit as one transaction. Two cases are not (#179):

  • without a journal, the same blocks are written in turn, and a crash part-way leaves some written and some not;
  • an inode that is not extent-mapped goes to apply_replace_file_content_indirect, which builds no transaction at all, on ext3 with a journal too.

Returns the new file size on success.

Source

pub fn apply_pwrite(&self, path: &str, offset: u64, data: &[u8]) -> Result<u64>

Positional write: splice data into the file at byte offset, allocating new physical blocks for any logical blocks that aren’t yet mapped (sparse holes, or blocks past EOF). Existing mapped blocks are read-modify-written for partial overlap; full-block writes go in fresh.

This is the primitive needed by streaming write paths (FUSE/WinFsp/FSKit cache-manager dispatches) — apply_replace_file_content is “save-as”, apply_pwrite is pwrite(2).

Returns the new file size on success.

Allocation behaviour:

  • Each unmapped logical run is satisfied by one or more physical runs. If plan_block_allocation can’t find a single contiguous group-local run sized for the whole logical run, the request is halved and retried — each successful sub-run becomes its own extent. True ENOSPC (single-block allocation also fails) surfaces as Error::NoSpaceLeftOnDevice.
  • Extent inserts try the inline-root path first; on LEAF_FULL_NEEDS_PROMOTION they fall back to plan_insert_extent_deep, which promotes the tree to depth ≥ 1 and allocates the additional internal/leaf node blocks via the same buffer-aware allocator. Tail checksums on tree blocks are patched when metadata_csum is on.

v1 limitations:

  • Extent-tree inodes only. Legacy ext2/3 (direct/indirect blocks) returns Error::InvalidArgument. The streaming-copy use case for this path is on freshly-mkfs’d ext4 volumes that always have EXTENTS_FL.
  • Pre-existing uninitialised extents (from fallocate) in the write range: not handled — the unmapped-run walk treats them the same as holes and tries to insert a fresh extent that would overlap, hitting CorruptExtentTree("extent overlaps existing"). Skipping fallocate-then-write, the streaming copy path doesn’t trigger this.
Source

pub fn apply_pwrite_ino( &self, r: impl Into<InodeRef>, offset: u64, data: &[u8], ) -> Result<u64>

apply_pwrite on the inode r names.

Source

pub fn apply_mkdir(&self, path: &str, mode: u16) -> Result<u32>

Create a subdirectory at path with POSIX mode bits (low 12 bits of mode). Returns the new directory’s inode number. Steps: allocate inode (Orlov-hinted) → allocate one data block → seed it with . / .. → build dir inode → write inode + data block → add dir entry in parent → bump parent’s i_links_count → commit BGD/SB counters.

Not journaled — safe only in scratch-image contexts until transaction wrapping lands.

Source

pub fn apply_mkdir_at( &self, dir: impl Into<InodeRef>, name: &[u8], mode: u16, ) -> Result<u32>

apply_mkdir of entry name in directory dir. name is bytes and need not be UTF-8.

Create a hard link at dst pointing to the same inode as src.

Semantics:

  • src must exist and must NOT be a directory (POSIX forbids directory hardlinks to avoid reference cycles).
  • dst’s parent must exist and be a directory.
  • dst must not already exist.
  • On success the shared inode’s i_links_count is incremented by 1.

Not journaled — same caveat as other Phase-4 ops.

apply_link: a new entry dst_name in directory dir for the inode target names.

Source

pub fn apply_rename( &self, src: &str, dst: &str, replace_if_exists: bool, ) -> Result<()>

Rename src → dst within the same filesystem.

Semantics:

  • Both endpoints are within this mount.
  • Works for files and directories.
  • Cross-parent moves update the moved dir’s .. entry + bump / decrement both parents’ i_links_count.
  • Refuses to move a directory into its own subtree (cycle check).
  • Same source and dest: no-op success.
  • When dst already exists:
    • replace_if_exists = false → returns Error::AlreadyExists.
    • replace_if_exists = true → overwrites dst. See “Atomicity” below for exactly how far that holds. Type-compatibility rules (POSIX rename(2)):
      • file→dir → Error::IsADirectory
      • dir→file → Error::NotADirectory
      • non-empty-dir overwrite → Error::DirectoryNotEmpty
      • src and dst resolve to the same inode (hardlink) → no-op success. Otherwise the previous dst inode’s link count is decremented in the same buffer; if that drops it to zero the inode’s extents and slot are freed in the same atomic commit.
§Atomicity

Both paths stage their work into a single [BlockBuffer] and commit it through the journal, so a crash either applies the whole rename or none of it. That includes growing the destination directory, splitting a full leaf of its htree index (#302), and dropping that index when it has no room to route a new leaf (#347).

Source

pub fn apply_rename_at( &self, src_dir: impl Into<InodeRef>, src_name: &[u8], dst_dir: impl Into<InodeRef>, dst_name: &[u8], replace_if_exists: bool, ) -> Result<()>

apply_rename of entry src_name in directory src_dir to entry dst_name in directory dst_dir. Renaming an entry to itself succeeds and writes nothing; . and .. cannot be renamed or replaced (Error::InvalidArgument).

Source

pub fn apply_rmdir(&self, path: &str) -> Result<()>

Remove an empty directory at path. Requires the target to contain only . and ... Frees the data block(s) + inode, removes the entry from the parent, decrements parent’s i_links_count.

Source

pub fn apply_rmdir_at( &self, dir: impl Into<InodeRef>, name: &[u8], ) -> Result<()>

apply_rmdir of entry name in directory dir. . is refused with Error::InvalidArgument and .. with Error::DirectoryNotEmpty, as rmdir(2) does.

Source§

impl Filesystem

Source

pub fn audit( &self, max_dirs_visited: u32, max_entries_per_dir: u32, ) -> Result<AuditReport>

Run an ext4 audit tool-style read-only audit.

Walks from root, counts how many directory entries reference each inode, and compares that against each inode’s i_links_count. Returns an AuditReport — empty anomalies means every invariant we check held.

The pass is bounded: never visits more than max_dirs_visited directories and never scans more than max_entries_per_dir entries within a single directory. Pass u32::MAX for an unbounded pass.

Source

pub fn audit_repair( &self, max_dirs_visited: u32, max_entries_per_dir: u32, repair: bool, ) -> Result<AuditReport>

Audit + repair convenience wrapper. See audit_with_repair for semantics. No-op on read-only mounts when repair == true (returns Error::ReadOnly).

Trait Implementations§

Source§

impl Drop for Filesystem

A mount that cleared EXT4_VALID_FS puts the state it found back when it is dropped, which is this crate’s unmount (fs_ext4_umount drops the handle). A volume that was not clean when mounted stays not clean. Best effort: a drop cannot report an error, and a device that refuses the write leaves the volume marked not clean, which errs the safe way.

Source§

fn drop(&mut self)

Executes the destructor for this type. Read more
Source§

fn pin_drop(self: Pin<&mut Self>)

🔬This is a nightly-only experimental API. (pin_ergonomics)
Execute the destructor for this type, but different to Drop::drop, it requires self to be pinned. Read more

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = !

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, !>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.