Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 70 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
name: CI

on:
push:
branches: [main]
pull_request:
workflow_dispatch:

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

env:
CARGO_TERM_COLOR: always
RUSTFLAGS: -D warnings

jobs:
lint:
name: fmt and clippy
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt, clippy
- uses: Swatinem/rust-cache@v2
- run: cargo fmt --check
- run: cargo clippy --all-targets

test:
name: test (${{ matrix.os }})
strategy:
fail-fast: false
matrix:
# Windows is the primary target; Linux also runs every image through
# e2fsck, which the Windows runners lack.
os: [ubuntu-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v5
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- name: Install e2fsprogs
if: runner.os == 'Linux'
run: |
sudo apt-get update
sudo apt-get install -y e2fsprogs
echo "EXT4KIT_REQUIRE_E2FSPROGS=1" >> "$GITHUB_ENV"
- name: Unit tests
run: cargo test --lib --bins
- name: Integration tests
run: cargo test --test cli

oci:
name: OCI integration tests
runs-on: ubuntu-latest
env:
EXT4KIT_REQUIRE_E2FSPROGS: "1"
steps:
- uses: actions/checkout@v5
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- name: Install e2fsprogs
run: |
sudo apt-get update
sudo apt-get install -y e2fsprogs
- name: Pull the base image into the Docker engine
run: docker pull docker.io/library/alpine:3.20
- name: OCI tests (registry and docker-daemon)
run: cargo test --test oci -- --ignored
27 changes: 21 additions & 6 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@ on-disk change has to be verified with e2fsprogs (see [Verifying images](#verify

```sh
cargo build
cargo test # ~70 unit tests, run offline in well under a second
cargo test --lib --bins # unit tests, offline, well under a second
cargo test --test cli # end-to-end: builds images and runs e2fsck (~15 s)
cargo test # both of the above
cargo test --test oci -- --ignored # --from tests; need network and Docker
cargo test <name_substring> # a single test
cargo fmt # CI-style check: cargo fmt --check
cargo clippy --all-targets # must report zero warnings
Expand Down Expand Up @@ -87,17 +90,26 @@ streaming import design.

## Tests

- Unit tests live in `#[cfg(test)] mod tests` beside the code. There is no
`tests/` directory.
- Unit tests live in `#[cfg(test)] mod tests` beside the code.
- End-to-end tests live in `tests/`. `cli.rs` runs the real binary on
generated host trees and checks every image with `e2fsck -fn` and
`debugfs`, using the helpers in `tests/common/mod.rs` (`create`, then
`Image::check` and `cat`/`stat`/`owner`/`list`). e2fsprogs is found
natively or through WSL, like the verify script. When it is missing the
checks are skipped with a note; CI sets `EXT4KIT_REQUIRE_E2FSPROGS=1` so
they fail instead. `oci.rs` holds `#[ignore]`d `--from` tests that need
Docker Hub or a Docker engine.
- Build small images in memory with `Ext4ImageBuilder::new(16)`, then call
`create_root_dir()`, `create_lost_and_found()` and `reserve_inodes()`
(see `fresh_builder()` in `core/directory.rs`). Assert by reading back
through the builder (`lookup_dir_entry`, `inode`, `directory_entries`,
`get_dir_blocks`) and by comparing free counts before and after.
- OCI tests build synthetic tar/gzip layers in memory (`layer()` helper in
`oci/mod.rs`). Tests must not need the network, Docker or Podman.
- Unit tests can't catch a structurally wrong image. For on-disk changes you
also need an end-to-end check with `e2fsck`, described next.
`oci/mod.rs`). Tests that are not `#[ignore]`d must not need the network,
Docker or Podman.
- Unit tests can't catch a structurally wrong image. For a new on-disk feature,
add a case to `tests/cli.rs` whose fixture exercises it, and use the
verify script described next for ad-hoc checks.

## Verifying images

Expand Down Expand Up @@ -129,6 +141,9 @@ trees. The `/verify-image` skill in `.claude/skills/` walks through this.
Windows path list, and backslashes passed to `wsl` vanish. Give `--add` a
`C:/...` path from `cygpath -m` (as in the snippet above), use
`MSYS_NO_PATHCONV=1` for `wsl`, or just use the verify script.
- `debugfs -R 'htree DIR'` prints "Directory block does not have space for
checksum" on every indexed directory. Images built by `mke2fs` without
`metadata_csum` get the same message, so it is not an ext4kit bug.
- The ext4 hash seed is random per image, so dx-directory layouts differ from
run to run. Do not write tests that compare whole-image bytes.
- `--from` without `docker://` queries the local engine first. Behavior can
Expand Down
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,6 @@ tokio = { version = "1", features = ["rt-multi-thread", "io-util"] }
tokio-util = { version = "0.7", features = ["io-util"] }
zerocopy = { version = "0.8.59", features = ["derive"] }
zstd = "0.13"

[dev-dependencies]
tempfile = "3"
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,19 @@ files, run `debugfs -R 'stat /path/to/file' output.img` to inspect the extent
tree. Run `debugfs -R 'stat /path/to/directory' output.img` to confirm a grown
directory's size, block count, and directory extents.

## Testing

```sh
cargo test # unit tests plus end-to-end image tests
cargo test --test oci -- --ignored # --from tests (Docker Hub and a Docker engine)
```

The end-to-end tests in `tests/` run the CLI and check every image with
`e2fsck` and `debugfs`. They find e2fsprogs on Linux, or through WSL on
Windows, and skip those checks with a note when it is missing. CI
(`.github/workflows/ci.yml`) runs the tests on Linux and Windows, and runs the
`--from` tests on Linux.

## Documentation

- [Architecture](docs/architecture.md)
Expand Down
9 changes: 5 additions & 4 deletions src/host/tree.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ use std::{
collections::HashMap,
fs,
path::{Path, PathBuf},
time::{SystemTime, UNIX_EPOCH},
};

#[derive(Clone, Copy, Default)]
Expand Down Expand Up @@ -56,10 +55,10 @@ pub(super) struct HostMetadata {

impl HostMetadata {
pub(super) fn from_std(metadata: &fs::Metadata, is_dir: bool) -> Self {
let default_mode = if is_dir { 0o755 } else { 0o644 };
#[cfg(unix)]
{
use std::os::unix::fs::MetadataExt;
let _ = is_dir; // Unix hosts supply real permission bits.
Self {
size: metadata.len(),
uid: metadata.uid(),
Expand All @@ -72,6 +71,7 @@ impl HostMetadata {
}
#[cfg(not(unix))]
{
let default_mode = if is_dir { 0o755 } else { 0o644 };
let mode = if metadata.permissions().readonly() {
default_mode & !0o222
} else {
Expand Down Expand Up @@ -106,9 +106,10 @@ fn clamp_unix_timestamp(seconds: i64) -> u32 {
seconds.clamp(0, i64::from(u32::MAX)) as u32
}

fn system_time_seconds(time: std::io::Result<SystemTime>) -> u32 {
#[cfg(not(unix))]
fn system_time_seconds(time: std::io::Result<std::time::SystemTime>) -> u32 {
time.ok()
.and_then(|value| value.duration_since(UNIX_EPOCH).ok())
.and_then(|value| value.duration_since(std::time::UNIX_EPOCH).ok())
.map_or(0, |value| value.as_secs().min(u64::from(u32::MAX)) as u32)
}

Expand Down
2 changes: 1 addition & 1 deletion src/xattr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ pub(crate) fn posix_acl_to_ext4_disk(value: &[u8]) -> Result<Vec<u8>> {
}
let mut disk = Vec::with_capacity(value.len());
disk.extend_from_slice(&1u32.to_le_bytes());
for entry in value[4..].chunks_exact(8) {
for entry in value[4..].as_chunks::<8>().0 {
let tag = u16::from_le_bytes(entry[..2].try_into().unwrap());
let perm = u16::from_le_bytes(entry[2..4].try_into().unwrap());
if perm > 0o7 {
Expand Down
Loading
Loading