rust-fs-core 0.3.4

Pure-Rust block-device framework — BlockRead/BlockDevice traits + FileDevice + CallbackDevice + LRU cache. Foundation crate for the rust-fs-* drivers and rust-img-* containers.
Documentation
name: CI
on:
  push:
    branches: [main]
  pull_request:
env:
  CARGO_TERM_COLOR: always
# Read-only unless a job asks for more. This repository's default token
# is read-write, and a workflow that declares nothing inherits it, so every
# job here would otherwise hold a write token beside actions it does not
# pin.
permissions:
  contents: read

jobs:
  test:
    name: test / ${{ matrix.os }}
    strategy:
      fail-fast: false
      matrix:
        os: [ubuntu-latest, macos-latest, windows-latest]
    runs-on: ${{ matrix.os }}
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
        with:
          toolchain: 1.95.0
          components: clippy, rustfmt
      - uses: Swatinem/rust-cache@v2
      - name: verify the packaged family scripts
        # A consumer with no checkout of this repository gets these from the
        # published crate, through cargo, so the package must carry them.
        shell: bash
        run: |
          files="$(cargo package --locked --list)"
          for script in core.sh output-budget.sh test-floor.sh semver-check.sh family-check.sh guest-rust-toolchain.sh stage-siblings.sh guest-rust-run.sh ci-gate.sh package-cli.sh; do
            grep -Fqx "scripts/$script" <<<"$files" || {
              echo "package omits scripts/$script" >&2
              exit 1
            }
          done
      - run: cargo build --locked --all-targets
      # AM_FS_CORE_ALLOW_UNPRIVILEGED_SKIP ACKNOWLEDGES A REAL GAP.
      # tests/device_node_size.rs covers the Linux device-size probe's
      # success path with a loop device, and losetup needs root, which
      # this runner is not. Without this the test FAILS rather than
      # skipping quietly -- libtest discards a passing test's output, so
      # a printed "skipped" line reaches nobody.
      #
      # What is still covered on this runner: the probe's FAILURE path,
      # via /dev/zero, which needs no privileges. What is not: the
      # success path, i.e. BLKGETSIZE64 returning a real size. macOS
      # covers its own equivalent in full, unprivileged, via hdiutil.
      #
      # To close this rather than acknowledge it, run the suite as root.
      # EXPECT_OVERFLOW_CHECKS ARMS THE ONE CHECK A TEXT SCAN CANNOT DO.
      # This run has no `--release`, so overflow-checks are on, and it is
      # what makes every arithmetic test in this crate mean anything.
      # Measured: a temporary `black_box(250u8) + black_box(10u8)`
      # panicked under `cargo test --locked` and passed under `cargo test
      # --locked --release`.
      #
      # The variable tells `overflow_checks::the_build_the_gate_asked_to
      # _check_does_check` in src/lib.rs that THIS is the run that must
      # trap an overflow -- asking the build directly rather than
      # inferring it from which spellings of "disabled" happen to be
      # absent from Cargo.toml. It is declared here rather than inline as
      # `VAR=1 cargo test` because this job also runs on windows-latest,
      # where that prefix is a PowerShell syntax error.
      #
      # Do not add `--release` to this run, and do not drop the variable:
      # tests/ci_profile.rs reads this file and fails if no `cargo test`
      # in it runs without `--release` while setting this. See #90.
      - name: test (quiet, budgeted, with an executed-test floor)
        shell: bash
        run: |
          bash scripts/tier.sh "test (debug)" debug 750 50000 -- cargo test --locked --all-targets --features cli
          bash scripts/core.sh test-floor debug 330
        env:
          AM_FS_CORE_ALLOW_UNPRIVILEGED_SKIP: "1"
          EXPECT_OVERFLOW_CHECKS: "1"
      - name: keep the full test transcript
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: tier-logs-test-${{ matrix.os }}
          path: tmp/logs/
          if-no-files-found: warn
          retention-days: 7
      # Twice: once as a consumer's static library builds this crate (no
      # features), and once with the `cli` feature the tools turn on, so a
      # lint in either shape is caught.
      - run: cargo clippy --locked --all-targets -- -D warnings
      - run: cargo clippy --locked --all-targets --features cli -- -D warnings
      # `cargo doc` EXITS 0 ON WARNINGS, so an unresolved intra-doc link
      # reaches main unseen unless warnings are denied here. Two did (#88).
      # Declared through `env:` rather than an inline `VAR=... cargo doc`
      # because this job also runs on windows-latest.
      - run: cargo doc --no-deps --locked --features cli
        env:
          RUSTDOCFLAGS: "-D warnings"
  fmt:
    name: fmt
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
        with:
          toolchain: 1.95.0
          components: rustfmt
      - run: cargo fmt --check
      - name: the one required check stands for every job
        run: bash scripts/core.sh ci-gate
      - name: the agent guide carries the shared block
        # AGENTS.md embeds agent-core, byte-identical with every sibling. This
        # runs the script directly rather than through `chore lint`, because
        # CI here does not install chore -- a guard wired only into a task
        # nothing runs is a guard that reports protection it is not providing.
        run: scripts/agents-core-check.sh
      - name: the shell tests
        # Every tests/scripts/*.sh, so a new one is gated by being added. Plain
        # scripts rather than cargo tests, so they cannot count towards the
        # executed-test floor.
        shell: bash
        run: for t in tests/scripts/*.sh; do bash "$t" || exit 1; done
  coverage:
    name: coverage
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
        with:
          toolchain: 1.95.0
          components: llvm-tools-preview
      - uses: Swatinem/rust-cache@v2
      - uses: taiki-e/install-action@cargo-llvm-cov
      # THE SAME ACKNOWLEDGEMENT AS THE `test` JOB, and it has to be
      # here rather than inherited: this job runs the suite too, under
      # llvm-cov, and it is not root either. Without it
      # tests/device_node_size.rs fails here while `test` passes, which
      # is what happened. `tests/ci_acknowledges_the_privilege_gap.rs`
      # now fails if a job runs the suite without saying this.
      - name: coverage (quiet, budgeted, fail under 90%)
        shell: bash
        run: |
          bash scripts/tier.sh coverage coverage 800 60000 -- cargo llvm-cov --features cli --html --output-dir target/llvm-cov --fail-under-lines 90
          bash scripts/core.sh test-floor coverage 330
        env:
          AM_FS_CORE_ALLOW_UNPRIVILEGED_SKIP: "1"
      - name: keep the coverage transcript and report
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: coverage-evidence
          path: |
            tmp/logs/
            target/llvm-cov/html/
          if-no-files-found: warn
          retention-days: 14

  semver:
    # `chore check:semver`, spelled out because CI here does not install
    # chore: this tree's public API against the newest version on crates.io,
    # refusing a break the version in Cargo.toml does not declare. Why, and
    # what it cannot see, is at the top of scripts/semver-check.sh.
    name: semver (public API against crates.io)
    runs-on: ubuntu-latest
    timeout-minutes: 30
    env:
      # The version scripts/semver-check.sh expects. A prebuilt binary,
      # checksum-verified by the action, rather than a ten-minute build.
      CARGO_SEMVER_CHECKS_VERSION: "0.50.0"
      # A failing tier prints only its verdict by default. Here the tail IS
      # the answer -- which items broke -- so it is printed in full.
      OUTPUT_BUDGET_FAIL_TAIL: "200"
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
        with:
          toolchain: 1.95.0
      - uses: Swatinem/rust-cache@v2
      - uses: taiki-e/install-action@v2
        with:
          tool: cargo-semver-checks@${{ env.CARGO_SEMVER_CHECKS_VERSION }}
      - name: semver (quiet, budgeted)
        shell: bash
        run: bash scripts/tier.sh semver semver 40 4000 -- bash scripts/core.sh semver-check


  ci-ok:
    name: ci-ok
    # THE ONE REQUIRED CHECK (#154). Branch protection used to name five
    # contexts by hand -- `fmt`, `coverage` and the three `test / <os>`
    # matrix legs. Requiring this gate instead means a runner can be added
    # to the matrix, or `coverage` renamed, or a job split in two, without
    # leaving a required check that never reports -- which GitHub reads as
    # permanently pending, with enforce_admins on and nothing to point at.
    # It is also the only shape in which a job-level `if:` is safe.
    #
    # It runs no tests itself, deliberately: it is a claim about the other
    # jobs, so it must not be able to pass work of its own off as theirs.
    # That is also why tests/ci_acknowledges_the_privilege_gap.rs does not
    # ask it for AM_FS_CORE_ALLOW_UNPRIVILEGED_SKIP -- it never runs the
    # suite, so it has no privilege gap to acknowledge.
    if: always()
    needs: [test, fmt, coverage, semver]
    runs-on: ubuntu-latest
    steps:
      - name: Every job ran and succeeded
        env:
          NEEDS: ${{ toJSON(needs) }}
        run: |
          set -euo pipefail
          echo "$NEEDS" | jq -r 'to_entries[] | "\(.key): \(.value.result)"'
          bad="$(echo "$NEEDS" | jq -r '[to_entries[] | select(.value.result != "success") | .key] | join(", ")')"
          if [ -n "$bad" ]; then
            echo "::error::not green: $bad (failed, cancelled or skipped)"
            exit 1
          fi
          echo "all required jobs succeeded"