am-fs-ext4 0.5.1

Pure-Rust ext4 filesystem driver. Exposes a C ABI (fs_ext4_*) suitable for FFI from C/C++/Go/etc.
Documentation
name: CI

on:
  push:
    branches: [main]
    tags: ['v*']
  pull_request:
    branches: [main]

jobs:
  test:
    name: test-${{ matrix.os }}
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        os: [ubuntu-latest]
    steps:
      - uses: actions/checkout@v4
      # The crate has a path dep on am-fs-core (../rust-fs-core).
      # Clone the sibling repo alongside so cargo can resolve it.
      # Anonymous HTTPS clone — rust-fs-core is public.
      - name: Clone sibling rust-fs-core
        run: git clone --depth 1 --branch v0.2.5 https://github.com/antimatter-studios/rust-fs-core.git ../rust-fs-core
      - name: Install Rust
        uses: dtolnay/rust-toolchain@stable
        with:
          components: rustfmt, clippy
      - name: Install qemu (for test-disk generator)
        run: |
          sudo apt-get update -qq
          sudo apt-get install -y -qq qemu-system-x86 libarchive-tools
      - name: Cache cargo registry
        uses: actions/cache@v4
        with:
          path: |
            ~/.cargo/registry
            ~/.cargo/git
            target
          key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
      - name: Cache alpine VM assets
        uses: actions/cache@v4
        with:
          path: test-disks/.vm-cache
          key: vm-cache-alpine-3.21.4
      - name: Generate test disks
        run: bash test-disks/build-ext4-feature-images.sh
      - run: cargo fmt --check
      - run: cargo clippy --locked --all-targets -- -D warnings
      - run: cargo test --locked --release

      # `c_char` is signed on x86_64 and on Apple Silicon, and UNSIGNED on
      # aarch64-linux — so a `[0i8; N]` that builds everywhere we test can
      # still fail to compile for a real target. That is exactly what got
      # through: the runner above is the only thing gating this crate, and it
      # is x86_64, so nothing here could see it.
      #
      # Compile-only, and no aarch64 runner needed: the type mismatch is a
      # build error, so `cargo check` against the target is the whole test.
      - name: Check aarch64-linux (c_char is unsigned there)
        run: |
          rustup target add aarch64-unknown-linux-gnu
          cargo check --locked --all-targets --target aarch64-unknown-linux-gnu

  # Validate the mkfs_ext4 CLI binary's output by running the kernel's own
  # fsck.ext4 (from e2fsprogs) against an image we generated. This is the
  # canonical "does Linux think this is a valid ext4 volume?" check — much
  # stricter than our own crate's read-path round-trip because it exercises
  # every assertion in e2fsck's superblock + group-descriptor + inode-table
  # validation, including the metadata-csum verification.
  #
  # Runs on every PR + push so a regression in the mkfs path is caught
  # before the binary ships to a tag. There's no Windows equivalent for
  # ext4 because Windows chkdsk doesn't speak ext4.
  validate-mkfs-bin:
    name: validate mkfs_ext4 (fsck.ext4 strict)
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      # The crate has a path dep on am-fs-core (../rust-fs-core).
      # Clone the sibling repo alongside so cargo can resolve it.
      - name: Clone sibling rust-fs-core
        run: git clone --depth 1 --branch v0.2.5 https://github.com/antimatter-studios/rust-fs-core.git ../rust-fs-core
      - name: Install Rust
        uses: dtolnay/rust-toolchain@stable
      - name: Install e2fsprogs
        run: |
          sudo apt-get update -qq
          sudo apt-get install -y -qq e2fsprogs
      - name: Cache cargo registry
        uses: actions/cache@v4
        with:
          path: |
            ~/.cargo/registry
            ~/.cargo/git
            target
          key: ${{ runner.os }}-cargo-mkfs-${{ hashFiles('**/Cargo.lock') }}
      - name: Build mkfs_ext4
        run: cargo build --locked --release --bin mkfs_ext4
      # 32 MiB at the default 4 KiB block size is one block group.
      - name: Format 32 MiB image and validate with fsck.ext4
        run: |
          truncate -s 32M /tmp/out.img
          ./target/release/mkfs_ext4 -L CITEST -U deadbeef-cafe-1234-5678-0123456789ab /tmp/out.img
          # -f forces a check even if FS thinks it's clean; -n declines
          # any repair offer (so this is read-only and exits non-zero
          # if anything is wrong rather than silently auto-fixing).
          fsck.ext4 -fnv /tmp/out.img
      # Every real-world volume this formatter writes is multi-group, and for
      # a long time none of that code had faced fsck: the backup superblock +
      # GDT loop, the s_block_group_nr patch and its checksum recompute, the
      # short-final-group bitmap padding and the cross-group free-block
      # accumulation were checked only by this crate's own reader, which
      # trusts the counters it is shown and cannot see a wrong checksum.
      #
      # 320 MiB / 4 KiB = three groups with a half-length tail; 640 MiB = five
      # whole groups, the smallest count that reaches past "groups 0 and 1"
      # into the sparse-super powers-of-3/5/7 rule (backups in 0, 1 and 3).
      # Both images are sparse, so the runner pays for the ~2 MiB of metadata
      # per group and nothing else.
      - name: Format multi-group images and validate with fsck.ext4
        run: |
          truncate -s 320M /tmp/mg3.img
          truncate -s 640M /tmp/mg5.img
          ./target/release/mkfs_ext4 -L CIMULTI3 /tmp/mg3.img
          ./target/release/mkfs_ext4 -L CIMULTI5 /tmp/mg5.img
          fsck.ext4 -fnv /tmp/mg3.img
          fsck.ext4 -fnv /tmp/mg5.img
          # Open each one through a BACKUP superblock too. The primary is
          # what the checks above read; -b points fsck at a backup instead,
          # so its s_block_group_nr and its recomputed checksum have to be
          # right or the open fails outright. Block 32768 is group 1 and
          # block 98304 is group 3, at 4 KiB blocks.
          fsck.ext4 -fn -b 32768 -B 4096 /tmp/mg3.img
          fsck.ext4 -fn -b 32768 -B 4096 /tmp/mg5.img
          fsck.ext4 -fn -b 98304 -B 4096 /tmp/mg5.img
          # The group table is where a short final group shows up: group 2 of
          # mg3.img must end at the last block of the device (81919), not at
          # a full group boundary.
          dumpe2fs /tmp/mg3.img | grep -E '^Group [0-9]+:'
          dumpe2fs /tmp/mg3.img | grep -q 'Group 2: (Blocks 65536-81919)'
      - name: Verify volume label and UUID round-tripped
        run: |
          # tune2fs prints the on-disk superblock metadata; grep confirms
          # the CLI args propagated to the bytes fsck just validated.
          tune2fs -l /tmp/out.img > /tmp/sb.txt
          cat /tmp/sb.txt
          grep -q "Filesystem volume name:.*CITEST" /tmp/sb.txt
          grep -qi "Filesystem UUID:.*deadbeef-cafe-1234-5678-0123456789ab" /tmp/sb.txt