dar-forensic 0.5.0

Forensic-grade reader for Denis Corbin DAR (Disk ARchiver) archives, including the Passware Kit Mobile variant; hardened and fuzz-tested against malicious input.
Documentation
name: CI

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

env:
  CARGO_TERM_COLOR: always
  CARGO_INCREMENTAL: "0"
  RUSTFLAGS: -Dwarnings

jobs:
  fmt:
    name: Format
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
      - uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable
        with:
          components: rustfmt
      - run: cargo fmt --check

  clippy:
    name: Clippy
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
      - uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable
        with:
          components: clippy
      - uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8
      - run: cargo clippy --all-targets -- -D warnings
      # Also lint with the optional `serde` feature so its derives can't rot.
      - run: cargo clippy --all-targets --all-features -- -D warnings
      # And the lean reader (no codec features) — the gating must stay warning-clean.
      - run: cargo clippy --all-targets --no-default-features -- -D warnings

  test:
    name: Test
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
      - uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable
      - uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8
      - run: cargo test
      # Exercise the optional `serde` feature (audit() JSON export).
      - run: cargo test --all-features
      # The lean reader: lists/extracts stored archives with zero codec deps and
      # refuses (not mis-decodes) compressed entries.
      - run: cargo test --no-default-features

  deny:
    name: Cargo Deny
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
      - uses: EmbarkStudios/cargo-deny-action@3fd3802e88374d3fe9159b834c7714ec57d6c979 # v2.0.15

  msrv:
    name: MSRV (1.85)
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
      - uses: dtolnay/rust-toolchain@c56a35af9328d0bc581dc86c05e58f97f7c38a0e # 1.85
      - uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8
      - run: cargo test

  fuzz-check:
    name: Fuzz (build check)
    runs-on: ubuntu-latest
    # The workflow-wide RUSTFLAGS=-Dwarnings must NOT leak into external tool
    # builds (cargo-fuzz) or the nightly fuzz build — nightly warns more freely.
    # Lint enforcement is the clippy/test jobs' responsibility, not this one.
    env:
      RUSTFLAGS: ""
    steps:
      - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
      # cargo-fuzz needs nightly (-Z sanitizer=address); override the channel
      # on the pinned toolchain action instead of using a separate @nightly ref.
      - uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable
        with:
          toolchain: nightly
          components: rust-src
      # Install without --locked: cargo-fuzz's pinned Cargo.lock holds an old
      # rustix (0.36.x) whose `rustc_*` attributes current nightly rejects,
      # breaking the build. Unlocking lets cargo resolve a rustix that compiles.
      - run: cargo install cargo-fuzz
      - run: cargo fuzz build

  secrets:
    name: Secret Scan (gitleaks)
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
        with:
          fetch-depth: 0
      - name: Install gitleaks
        # Pinned version, no `latest` API call: the unauthenticated GitHub API is
        # rate-limited on shared runners, which yields an empty VERSION and a 404.
        # renovate: datasource=github-releases depName=gitleaks/gitleaks
        run: |
          VERSION=8.30.1
          curl -sSfL "https://github.com/gitleaks/gitleaks/releases/download/v${VERSION}/gitleaks_${VERSION}_linux_x64.tar.gz" \
            | tar xz -C /tmp gitleaks
      - name: Run gitleaks
        run: /tmp/gitleaks detect --source .

  coverage:
    name: Coverage (100% lines)
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
      - uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable
        with:
          components: llvm-tools-preview
      - uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8
      - run: cargo install cargo-llvm-cov --locked
      # Union coverage across the supported feature configurations. Some lines
      # are reachable only in a specific build — e.g. the "unsupported codec"
      # finding is produced only by a lean build (a full build decodes every
      # codec, so nothing is unsupported), and the serde paths only with that
      # feature on. A line left uncovered in EVERY configuration is the real
      # failure; lcov merges the runs.
      - run: cargo llvm-cov clean --workspace
      - run: cargo llvm-cov --no-report
      - run: cargo llvm-cov --no-report --all-features
      - run: cargo llvm-cov --no-report --no-default-features
      - run: cargo llvm-cov report --lcov --output-path lcov.info
      # Enforce 100% line coverage of the library. lcov DA:<line>,0 records mark
      # uncovered lines; any such record fails the gate. (lcov merges across
      # binaries correctly, unlike the --summary-only line metric.)
      - name: Enforce 100% line coverage
        run: |
          if grep -qE '^DA:[0-9]+,0$' lcov.info; then
            echo "::error::uncovered lines:"; grep -nE '^(SF:|DA:[0-9]+,0$)' lcov.info
            exit 1
          fi
          echo "100% line coverage ✓"
      # The e2e suite (tests/, exercising only the public API) must independently
      # cover every library line reachable through open()/extract()/audit(). The
      # public-API tests are unioned across the default and lean builds — some
      # paths are reachable only in one (e.g. the "unsupported codec" finding is
      # produced only when a codec is disabled). A small, documented set of guards
      # is unreachable through the public API in any build and is covered by the
      # unit suite instead; those are allowlisted below. File-aware (the report
      # spans src/lib.rs, src/findings.rs, src/bodyfile.rs).
      - name: Enforce e2e (public-API) coverage
        run: |
          cargo llvm-cov clean --workspace
          cargo llvm-cov --no-report --test synthetic --test real_images --test canonical_finding_tests
          cargo llvm-cov --no-report --no-default-features --test synthetic --test real_images --test canonical_finding_tests
          cargo llvm-cov report --lcov --output-path e2e.info
          awk -F: '/^SF:/{f=$2} /^DA:[0-9]+,0$/{split($0,a,","); split(a[1],b,":"); print f" "b[2]}' e2e.info > /tmp/e2e_uncovered.txt
          fail=0
          while read -r file ln; do
            txt=$(sed -n "${ln}p" "$file" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
            case "$txt" in
              "}") ;;                                                            # bare brace — never real missed logic
              "r.seek(SeekFrom::Start(archive_origin))?;") ;;                    # >256 MiB tail-scan fallback
              "if let Some(result) = scan_window(r, label, use_label)? {") ;;    # >256 MiB tail-scan fallback
              "return Ok(result);") ;;                                           # >256 MiB tail-scan fallback
              "fn flush(&mut self) -> std::io::Result<()> {") ;;                 # CapWriter::flush — never called on the streaming path
              "self.inner.flush()") ;;                                           # CapWriter::flush body
              'return Err(DarError::Corrupt("terminator underflows archive".into()));') ;; # all-0xFF file forbidden by magic
              'return Err(DarError::Corrupt("header flag field too large".into()));') ;; # read_header_flags guard — unit-tested
              "return Ok(0); // the flag field was introduced at edition 2") ;;   # read_compr_bs edition-1 path — unit-tested
              "read_infinint(r)?; // skip the initial offset") ;;                 # read_compr_bs initial-offset skip — unit-tested
              'other => Err(DarError::Corrupt(format!("compression '"'"'{}'"'"' not supported in this build", other as char))),') ;; # decode_stream unsupported-codec arm — only reachable in a lean build; unit-tested
              *) echo "::error::e2e leaves a public-API-reachable line uncovered: ${file}:${ln}: ${txt}"; fail=1 ;;
            esac
          done < /tmp/e2e_uncovered.txt
          [ "$fail" = 0 ] && echo "e2e suite covers all public-API-reachable lines ✓ (only documented unit-only guards remain)"
          exit $fail

  geiger:
    name: Unsafe Audit (cargo-geiger)
    runs-on: ubuntu-latest
    continue-on-error: true
    steps:
      - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
      - uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable
      - uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8
      - run: cargo install cargo-geiger --locked
      - run: cargo geiger 2>&1 || true