mrrc 0.9.1

A Rust library for reading, writing, and manipulating MARC bibliographic records in ISO 2709 binary format
Documentation
name: Fuzz (nightly)

# Scheduled coverage-guided fuzzing. Finds panics, infinite loops, and
# memory issues in the parser that property tests and unit tests miss.
#
# Policy:
#   - Runs nightly + on demand (workflow_dispatch); not a PR gate.
#   - A crash FAILS the job so the red mark surfaces on the Actions
#     dashboard. fuzz/artifacts/* uploads so the reproducer survives.
#   - A scheduled failure files (or comments on) a tracking issue so it
#     cannot pass unnoticed. Triage is manual. Reproducers get copied
#     into tests/ as regressions — see docs/contributing/fuzzing.md.
#   - The grown corpus is cached per target across runs so coverage
#     depth compounds instead of regrowing from the committed seeds.

on:
  schedule:
    # Daily at 03:00 UTC. Offset from memory-safety.yml (02:00) so the
    # two nightly jobs do not contend for the same cache slot.
    - cron: '0 3 * * *'
  workflow_dispatch:
    inputs:
      max_total_time:
        description: 'Seconds to fuzz (default 300 = 5 min)'
        required: false
        default: '300'

permissions:
  contents: read

env:
  RUST_BACKTRACE: 1
  CARGO_TERM_COLOR: always

jobs:
  fuzz:
    name: ${{ matrix.target }} (${{ github.event.inputs.max_total_time || '300' }}s)
    runs-on: ubuntu-latest
    # Generous cap: toolchain install + cargo-fuzz install + build + 5-min
    # fuzz + artifact upload. Raise if workflow_dispatch runs use longer
    # budgets.
    timeout-minutes: 20
    strategy:
      # Each target is independent — let one finding surface without
      # cancelling the other.
      fail-fast: false
      matrix:
        target: [parse_record, roundtrip_binary, error_classification, recovery_mode_consistency, parse_json, parse_marcjson, parse_marcxml, parse_mods, decode_marc8, parse_authority, parse_holdings, parse_bibframe, parse_lenient]

    steps:
      - uses: actions/checkout@v6.0.3

      - name: Install nightly Rust toolchain
        uses: dtolnay/rust-toolchain@nightly

      - name: Install cargo-fuzz
        uses: taiki-e/install-action@v2
        with:
          tool: cargo-fuzz

      - name: Cache cargo + fuzz target
        uses: actions/cache@v5
        with:
          path: |
            ~/.cargo/registry
            ~/.cargo/git
            fuzz/target
          key: ${{ runner.os }}-cargo-fuzz-${{ hashFiles('fuzz/Cargo.toml', 'Cargo.lock') }}
          restore-keys: |
            ${{ runner.os }}-cargo-fuzz-

      - name: Cache fuzz corpus
        uses: actions/cache@v5
        with:
          path: fuzz/corpus/${{ matrix.target }}
          # The run-scoped key never matches an existing entry, so every
          # run restores the newest corpus via restore-keys and saves its
          # own grown corpus as a fresh entry. The committed seeds merge
          # in from checkout either way.
          key: fuzz-corpus-${{ matrix.target }}-${{ github.run_id }}
          restore-keys: |
            fuzz-corpus-${{ matrix.target }}-

      - name: Run ${{ matrix.target }}
        env:
          MAX_TOTAL_TIME: ${{ github.event.inputs.max_total_time || '300' }}
        # Run from the repo root — cargo-fuzz resolves `./fuzz/Cargo.toml`
        # from cwd. `+nightly` overrides the root rust-toolchain.toml pin
        # (1.95.0) with the nightly installed above, which libfuzzer-sys
        # requires. `--target x86_64-unknown-linux-gnu` overrides cargo-fuzz's
        # default musl target, which isn't installed on the runner and is
        # incompatible with `-Zsanitizer=address` (static libc).
        run: cargo +nightly fuzz run ${{ matrix.target }} --target x86_64-unknown-linux-gnu -- -max_total_time=$MAX_TOTAL_TIME

      - name: Minimize corpus before caching
        # Sheds redundant inputs while keeping coverage, so the cached
        # corpus stays small enough for fast libfuzzer startup. Skipped
        # when the fuzz run failed (the cache also skips saving then).
        run: cargo +nightly fuzz cmin ${{ matrix.target }} --target x86_64-unknown-linux-gnu

      - name: Upload crash artifacts
        if: failure()
        uses: actions/upload-artifact@v7
        with:
          name: fuzz-artifacts-${{ matrix.target }}
          path: fuzz/artifacts/
          if-no-files-found: warn
          retention-days: 30

  notify-failure:
    name: File failure issue
    needs: fuzz
    if: (failure() || cancelled()) && github.event_name == 'schedule'
    runs-on: ubuntu-latest
    timeout-minutes: 5
    permissions:
      issues: write
    steps:
      - name: File or update the failure-tracking issue
        env:
          GH_TOKEN: ${{ github.token }}
          TITLE: "Nightly fuzz run failed"
          RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
        run: |
          existing=$(gh issue list --repo "$GITHUB_REPOSITORY" --state open \
            --json number,title \
            --jq '[.[] | select(.title == env.TITLE)][0].number // empty')
          if [ -n "$existing" ]; then
            gh issue comment "$existing" --repo "$GITHUB_REPOSITORY" \
              --body "Failed again: $RUN_URL"
          else
            gh issue create --repo "$GITHUB_REPOSITORY" --title "$TITLE" \
              --body "Scheduled run failed: $RUN_URL — crash artifacts (if any) are attached to the run. Later failures will be added here as comments. Triage playbook: docs/contributing/fuzzing.md."
          fi