diffutils-0.4.1/.cargo_vcs_info.json0000644000000001360000000000100130270ustar { "git": { "sha1": "d92132e72154a2f59bd053ee523175b7c235049c" }, "path_in_vcs": "" }diffutils-0.4.1/.github/workflows/ci.yml000064400000000000000000000136041046102023000163360ustar 00000000000000on: [push, pull_request] name: Basic CI env: CARGO_TERM_COLOR: always jobs: check: name: cargo check runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: os: [ubuntu-latest, macOS-latest, windows-latest] steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable - run: cargo check test: name: cargo test runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: os: [ubuntu-latest, macOS-latest, windows-latest] steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable - name: set up PATH on Windows # Needed to use GNU's patch.exe instead of Strawberry Perl patch if: runner.os == 'Windows' run: echo "C:\Program Files\Git\usr\bin" >> $env:GITHUB_PATH - run: cargo test fmt: name: cargo fmt --all -- --check runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable - run: rustup component add rustfmt - run: cargo fmt --all -- --check clippy: name: cargo clippy -- -D warnings runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: os: [ubuntu-latest, macOS-latest, windows-latest] steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable - run: rustup component add clippy - run: cargo clippy -- -D warnings gnu-testsuite: name: GNU test suite runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable - run: cargo build --release # do not fail, the report is merely informative (at least until all tests pass reliably) - run: ./tests/run-upstream-testsuite.sh release || true env: TERM: xterm - uses: actions/upload-artifact@v4 with: name: test-results.json path: tests/test-results.json - run: ./tests/print-test-results.sh tests/test-results.json coverage: name: Code Coverage runs-on: ${{ matrix.job.os }} strategy: fail-fast: false matrix: job: - { os: ubuntu-latest , features: unix } - { os: macos-latest , features: macos } - { os: windows-latest , features: windows } steps: - uses: actions/checkout@v4 - name: Initialize workflow variables id: vars shell: bash run: | ## VARs setup outputs() { step_id="vars"; for var in "$@" ; do echo steps.${step_id}.outputs.${var}="${!var}"; echo "${var}=${!var}" >> $GITHUB_OUTPUT; done; } # toolchain TOOLCHAIN="nightly" ## default to "nightly" toolchain (required for certain required unstable compiler flags) ## !maint: refactor when stable channel has needed support # * specify gnu-type TOOLCHAIN for windows; `grcov` requires gnu-style code coverage data files case ${{ matrix.job.os }} in windows-*) TOOLCHAIN="$TOOLCHAIN-x86_64-pc-windows-gnu" ;; esac; # * use requested TOOLCHAIN if specified if [ -n "${{ matrix.job.toolchain }}" ]; then TOOLCHAIN="${{ matrix.job.toolchain }}" ; fi outputs TOOLCHAIN # target-specific options # * CARGO_FEATURES_OPTION CARGO_FEATURES_OPTION='--all -- --check' ; ## default to '--all-features' for code coverage # * CODECOV_FLAGS CODECOV_FLAGS=$( echo "${{ matrix.job.os }}" | sed 's/[^[:alnum:]]/_/g' ) outputs CODECOV_FLAGS - name: rust toolchain ~ install uses: dtolnay/rust-toolchain@nightly - name: set up PATH on Windows # Needed to use GNU's patch.exe instead of Strawberry Perl patch if: runner.os == 'Windows' run: echo "C:\Program Files\Git\usr\bin" >> $env:GITHUB_PATH - name: Test run: cargo test ${{ steps.vars.outputs.CARGO_FEATURES_OPTION }} --no-fail-fast env: CARGO_INCREMENTAL: "0" RUSTC_WRAPPER: "" RUSTFLAGS: "-Zprofile -Ccodegen-units=1 -Copt-level=0 -Clink-dead-code -Coverflow-checks=off -Zpanic_abort_tests -Cpanic=abort" RUSTDOCFLAGS: "-Cpanic=abort" - name: "`grcov` ~ install" id: build_grcov shell: bash run: | git clone https://github.com/mozilla/grcov.git ~/grcov/ cd ~/grcov # Hardcode the version of crossbeam-epoch. See # https://github.com/uutils/coreutils/issues/3680 sed -i -e "s|tempfile =|crossbeam-epoch = \"=0.9.8\"\ntempfile =|" Cargo.toml cargo install --path . cd - # Uncomment when the upstream issue # https://github.com/mozilla/grcov/issues/849 is fixed # uses: actions-rs/install@v0.1 # with: # crate: grcov # version: latest # use-tool-cache: false - name: Generate coverage data (via `grcov`) id: coverage shell: bash run: | ## Generate coverage data COVERAGE_REPORT_DIR="target/debug" COVERAGE_REPORT_FILE="${COVERAGE_REPORT_DIR}/lcov.info" mkdir -p "${COVERAGE_REPORT_DIR}" # display coverage files grcov . --output-type files --ignore build.rs --ignore "vendor/*" --ignore "/*" --ignore "[a-zA-Z]:/*" --excl-br-line "^\s*((debug_)?assert(_eq|_ne)?!|#\[derive\()" | sort --unique # generate coverage report grcov . --output-type lcov --output-path "${COVERAGE_REPORT_FILE}" --branch --ignore build.rs --ignore "vendor/*" --ignore "/*" --ignore "[a-zA-Z]:/*" --excl-br-line "^\s*((debug_)?assert(_eq|_ne)?!|#\[derive\()" echo "report=${COVERAGE_REPORT_FILE}" >> $GITHUB_OUTPUT - name: Upload coverage results (to Codecov.io) uses: codecov/codecov-action@v4 with: token: ${{ secrets.CODECOV_TOKEN }} file: ${{ steps.coverage.outputs.report }} ## flags: IntegrationTests, UnitTests, ${{ steps.vars.outputs.CODECOV_FLAGS }} flags: ${{ steps.vars.outputs.CODECOV_FLAGS }} name: codecov-umbrella fail_ci_if_error: false diffutils-0.4.1/.github/workflows/fuzzing.yml000064400000000000000000000040361046102023000174360ustar 00000000000000name: Fuzzing # spell-checker:ignore fuzzer on: pull_request: push: branches: - main permissions: contents: read concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} jobs: fuzz-build: name: Build the fuzzers runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@nightly - name: Install `cargo-fuzz` run: cargo install cargo-fuzz - uses: Swatinem/rust-cache@v2 with: shared-key: "cargo-fuzz-cache-key" cache-directories: "fuzz/target" - name: Run `cargo-fuzz build` run: cargo +nightly fuzz build fuzz-run: needs: fuzz-build name: Run the fuzzers runs-on: ubuntu-latest timeout-minutes: 5 env: RUN_FOR: 60 strategy: matrix: test-target: - { name: fuzz_ed, should_pass: true } - { name: fuzz_normal, should_pass: true } - { name: fuzz_patch, should_pass: true } steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@nightly - name: Install `cargo-fuzz` run: cargo install cargo-fuzz - uses: Swatinem/rust-cache@v2 with: shared-key: "cargo-fuzz-cache-key" cache-directories: "fuzz/target" - name: Restore Cached Corpus uses: actions/cache/restore@v4 with: key: corpus-cache-${{ matrix.test-target.name }} path: | fuzz/corpus/${{ matrix.test-target.name }} - name: Run ${{ matrix.test-target.name }} for XX seconds shell: bash continue-on-error: ${{ !matrix.test-target.name.should_pass }} run: | cargo +nightly fuzz run ${{ matrix.test-target.name }} -- -max_total_time=${{ env.RUN_FOR }} -detect_leaks=0 - name: Save Corpus Cache uses: actions/cache/save@v4 with: key: corpus-cache-${{ matrix.test-target.name }} path: | fuzz/corpus/${{ matrix.test-target.name }} diffutils-0.4.1/.github/workflows/release.yml000064400000000000000000000261261046102023000173660ustar 00000000000000# Copyright 2022-2023, axodotdev # SPDX-License-Identifier: MIT or Apache-2.0 # # CI that: # # * checks for a Git Tag that looks like a release # * builds artifacts with cargo-dist (archives, installers, hashes) # * uploads those artifacts to temporary workflow zip # * on success, uploads the artifacts to a Github Release # # Note that the Github Release will be created with a generated # title/body based on your changelogs. name: Release permissions: contents: write # This task will run whenever you push a git tag that looks like a version # like "1.0.0", "v0.1.0-prerelease.1", "my-app/0.1.0", "releases/v1.0.0", etc. # Various formats will be parsed into a VERSION and an optional PACKAGE_NAME, where # PACKAGE_NAME must be the name of a Cargo package in your workspace, and VERSION # must be a Cargo-style SemVer Version (must have at least major.minor.patch). # # If PACKAGE_NAME is specified, then the announcement will be for that # package (erroring out if it doesn't have the given version or isn't cargo-dist-able). # # If PACKAGE_NAME isn't specified, then the announcement will be for all # (cargo-dist-able) packages in the workspace with that version (this mode is # intended for workspaces with only one dist-able package, or with all dist-able # packages versioned/released in lockstep). # # If you push multiple tags at once, separate instances of this workflow will # spin up, creating an independent announcement for each one. However Github # will hard limit this to 3 tags per commit, as it will assume more tags is a # mistake. # # If there's a prerelease-style suffix to the version, then the release(s) # will be marked as a prerelease. on: push: tags: - '**[0-9]+.[0-9]+.[0-9]+*' pull_request: jobs: # Run 'cargo dist plan' (or host) to determine what tasks we need to do plan: runs-on: ubuntu-latest outputs: val: ${{ steps.plan.outputs.manifest }} tag: ${{ !github.event.pull_request && github.ref_name || '' }} tag-flag: ${{ !github.event.pull_request && format('--tag={0}', github.ref_name) || '' }} publishing: ${{ !github.event.pull_request }} env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - uses: actions/checkout@v4 with: submodules: recursive - name: Install cargo-dist # we specify bash to get pipefail; it guards against the `curl` command # failing. otherwise `sh` won't catch that `curl` returned non-0 shell: bash run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.12.0/cargo-dist-installer.sh | sh" # sure would be cool if github gave us proper conditionals... # so here's a doubly-nested ternary-via-truthiness to try to provide the best possible # functionality based on whether this is a pull_request, and whether it's from a fork. # (PRs run on the *source* but secrets are usually on the *target* -- that's *good* # but also really annoying to build CI around when it needs secrets to work right.) - id: plan run: | cargo dist ${{ (!github.event.pull_request && format('host --steps=create --tag={0}', github.ref_name)) || 'plan' }} --output-format=json > plan-dist-manifest.json echo "cargo dist ran successfully" cat plan-dist-manifest.json echo "manifest=$(jq -c "." plan-dist-manifest.json)" >> "$GITHUB_OUTPUT" - name: "Upload dist-manifest.json" uses: actions/upload-artifact@v4 with: name: artifacts-plan-dist-manifest path: plan-dist-manifest.json # Build and packages all the platform-specific things build-local-artifacts: name: build-local-artifacts (${{ join(matrix.targets, ', ') }}) # Let the initial task tell us to not run (currently very blunt) needs: - plan if: ${{ fromJson(needs.plan.outputs.val).ci.github.artifacts_matrix.include != null && (needs.plan.outputs.publishing == 'true' || fromJson(needs.plan.outputs.val).ci.github.pr_run_mode == 'upload') }} strategy: fail-fast: false # Target platforms/runners are computed by cargo-dist in create-release. # Each member of the matrix has the following arguments: # # - runner: the github runner # - dist-args: cli flags to pass to cargo dist # - install-dist: expression to run to install cargo-dist on the runner # # Typically there will be: # - 1 "global" task that builds universal installers # - N "local" tasks that build each platform's binaries and platform-specific installers matrix: ${{ fromJson(needs.plan.outputs.val).ci.github.artifacts_matrix }} runs-on: ${{ matrix.runner }} env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} BUILD_MANIFEST_NAME: target/distrib/${{ join(matrix.targets, '-') }}-dist-manifest.json steps: - uses: actions/checkout@v4 with: submodules: recursive - uses: swatinem/rust-cache@v2 - name: Install cargo-dist run: ${{ matrix.install_dist }} # Get the dist-manifest - name: Fetch local artifacts uses: actions/download-artifact@v4 with: pattern: artifacts-* path: target/distrib/ merge-multiple: true - name: Install dependencies run: | ${{ matrix.packages_install }} - name: Build artifacts run: | # Actually do builds and make zips and whatnot cargo dist build ${{ needs.plan.outputs.tag-flag }} --print=linkage --output-format=json ${{ matrix.dist_args }} > dist-manifest.json echo "cargo dist ran successfully" - id: cargo-dist name: Post-build # We force bash here just because github makes it really hard to get values up # to "real" actions without writing to env-vars, and writing to env-vars has # inconsistent syntax between shell and powershell. shell: bash run: | # Parse out what we just built and upload it to scratch storage echo "paths<> "$GITHUB_OUTPUT" jq --raw-output ".artifacts[]?.path | select( . != null )" dist-manifest.json >> "$GITHUB_OUTPUT" echo "EOF" >> "$GITHUB_OUTPUT" cp dist-manifest.json "$BUILD_MANIFEST_NAME" - name: "Upload artifacts" uses: actions/upload-artifact@v4 with: name: artifacts-build-local-${{ join(matrix.targets, '_') }} path: | ${{ steps.cargo-dist.outputs.paths }} ${{ env.BUILD_MANIFEST_NAME }} # Build and package all the platform-agnostic(ish) things build-global-artifacts: needs: - plan - build-local-artifacts runs-on: "ubuntu-20.04" env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} BUILD_MANIFEST_NAME: target/distrib/global-dist-manifest.json steps: - uses: actions/checkout@v4 with: submodules: recursive - name: Install cargo-dist shell: bash run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.12.0/cargo-dist-installer.sh | sh" # Get all the local artifacts for the global tasks to use (for e.g. checksums) - name: Fetch local artifacts uses: actions/download-artifact@v4 with: pattern: artifacts-* path: target/distrib/ merge-multiple: true - id: cargo-dist shell: bash run: | cargo dist build ${{ needs.plan.outputs.tag-flag }} --output-format=json "--artifacts=global" > dist-manifest.json echo "cargo dist ran successfully" # Parse out what we just built and upload it to scratch storage echo "paths<> "$GITHUB_OUTPUT" jq --raw-output ".artifacts[]?.path | select( . != null )" dist-manifest.json >> "$GITHUB_OUTPUT" echo "EOF" >> "$GITHUB_OUTPUT" cp dist-manifest.json "$BUILD_MANIFEST_NAME" - name: "Upload artifacts" uses: actions/upload-artifact@v4 with: name: artifacts-build-global path: | ${{ steps.cargo-dist.outputs.paths }} ${{ env.BUILD_MANIFEST_NAME }} # Determines if we should publish/announce host: needs: - plan - build-local-artifacts - build-global-artifacts # Only run if we're "publishing", and only if local and global didn't fail (skipped is fine) if: ${{ always() && needs.plan.outputs.publishing == 'true' && (needs.build-global-artifacts.result == 'skipped' || needs.build-global-artifacts.result == 'success') && (needs.build-local-artifacts.result == 'skipped' || needs.build-local-artifacts.result == 'success') }} env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} runs-on: "ubuntu-20.04" outputs: val: ${{ steps.host.outputs.manifest }} steps: - uses: actions/checkout@v4 with: submodules: recursive - name: Install cargo-dist run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.12.0/cargo-dist-installer.sh | sh" # Fetch artifacts from scratch-storage - name: Fetch artifacts uses: actions/download-artifact@v4 with: pattern: artifacts-* path: target/distrib/ merge-multiple: true # This is a harmless no-op for Github Releases, hosting for that happens in "announce" - id: host shell: bash run: | cargo dist host ${{ needs.plan.outputs.tag-flag }} --steps=upload --steps=release --output-format=json > dist-manifest.json echo "artifacts uploaded and released successfully" cat dist-manifest.json echo "manifest=$(jq -c "." dist-manifest.json)" >> "$GITHUB_OUTPUT" - name: "Upload dist-manifest.json" uses: actions/upload-artifact@v4 with: # Overwrite the previous copy name: artifacts-dist-manifest path: dist-manifest.json # Create a Github Release while uploading all files to it announce: needs: - plan - host # use "always() && ..." to allow us to wait for all publish jobs while # still allowing individual publish jobs to skip themselves (for prereleases). # "host" however must run to completion, no skipping allowed! if: ${{ always() && needs.host.result == 'success' }} runs-on: "ubuntu-20.04" env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - uses: actions/checkout@v4 with: submodules: recursive - name: "Download Github Artifacts" uses: actions/download-artifact@v4 with: pattern: artifacts-* path: artifacts merge-multiple: true - name: Cleanup run: | # Remove the granular manifests rm -f artifacts/*-dist-manifest.json - name: Create Github Release uses: ncipollo/release-action@v1 with: tag: ${{ needs.plan.outputs.tag }} name: ${{ fromJson(needs.host.outputs.val).announcement_title }} body: ${{ fromJson(needs.host.outputs.val).announcement_github_body }} prerelease: ${{ fromJson(needs.host.outputs.val).announcement_is_prerelease }} artifacts: "artifacts/*" diffutils-0.4.1/.gitignore000064400000000000000000000000161046102023000136040ustar 00000000000000/target *.swp diffutils-0.4.1/Cargo.lock0000644000000367450000000000100110210ustar # This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 3 [[package]] name = "aho-corasick" version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" dependencies = [ "memchr", ] [[package]] name = "android-tzdata" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" [[package]] name = "android_system_properties" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" dependencies = [ "libc", ] [[package]] name = "anstyle" version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc" [[package]] name = "assert_cmd" version = "2.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed72493ac66d5804837f480ab3766c72bdfab91a65e565fc54fa9e42db0073a8" dependencies = [ "anstyle", "bstr", "doc-comment", "predicates", "predicates-core", "predicates-tree", "wait-timeout", ] [[package]] name = "autocfg" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "bitflags" version = "2.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf" [[package]] name = "bstr" version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c48f0051a4b4c5e0b6d365cd04af53aeaa209e3cc15ec2cdb69e73cc87fbd0dc" dependencies = [ "memchr", "regex-automata", "serde", ] [[package]] name = "bumpalo" version = "3.15.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ff69b9dd49fd426c69a0db9fc04dd934cdb6645ff000864d98f7e2af8830eaa" [[package]] name = "cc" version = "1.0.90" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8cd6604a82acf3039f1144f54b8eb34e91ffba622051189e71b781822d5ee1f5" [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" version = "0.4.38" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" dependencies = [ "android-tzdata", "iana-time-zone", "js-sys", "num-traits", "wasm-bindgen", "windows-targets", ] [[package]] name = "core-foundation-sys" version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" [[package]] name = "diff" version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" [[package]] name = "difflib" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" [[package]] name = "diffutils" version = "0.4.1" dependencies = [ "assert_cmd", "chrono", "diff", "predicates", "pretty_assertions", "regex", "same-file", "tempfile", "unicode-width", ] [[package]] name = "doc-comment" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" [[package]] name = "errno" version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" dependencies = [ "libc", "windows-sys", ] [[package]] name = "fastrand" version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" [[package]] name = "float-cmp" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" dependencies = [ "num-traits", ] [[package]] name = "iana-time-zone" version = "0.1.60" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" dependencies = [ "android_system_properties", "core-foundation-sys", "iana-time-zone-haiku", "js-sys", "wasm-bindgen", "windows-core", ] [[package]] name = "iana-time-zone-haiku" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" dependencies = [ "cc", ] [[package]] name = "js-sys" version = "0.3.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" dependencies = [ "wasm-bindgen", ] [[package]] name = "libc" version = "0.2.153" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" [[package]] name = "linux-raw-sys" version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" [[package]] name = "log" version = "0.4.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" [[package]] name = "memchr" version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" [[package]] name = "normalize-line-endings" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" [[package]] name = "num-traits" version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a" dependencies = [ "autocfg", ] [[package]] name = "once_cell" version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "predicates" version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68b87bfd4605926cdfefc1c3b5f8fe560e3feca9d5552cf68c466d3d8236c7e8" dependencies = [ "anstyle", "difflib", "float-cmp", "normalize-line-endings", "predicates-core", "regex", ] [[package]] name = "predicates-core" version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b794032607612e7abeb4db69adb4e33590fa6cf1149e95fd7cb00e634b92f174" [[package]] name = "predicates-tree" version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "368ba315fb8c5052ab692e68a0eefec6ec57b23a36959c14496f0b0df2c0cecf" dependencies = [ "predicates-core", "termtree", ] [[package]] name = "pretty_assertions" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af7cee1a6c8a5b9208b3cb1061f10c0cb689087b3d8ce85fb9d2dd7a29b6ba66" dependencies = [ "diff", "yansi", ] [[package]] name = "proc-macro2" version = "1.0.78" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" dependencies = [ "unicode-ident", ] [[package]] name = "quote" version = "1.0.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" dependencies = [ "proc-macro2", ] [[package]] name = "regex" version = "1.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c" dependencies = [ "aho-corasick", "memchr", "regex-automata", "regex-syntax", ] [[package]] name = "regex-automata" version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5bb987efffd3c6d0d8f5f89510bb458559eab11e4f869acb20bf845e016259cd" dependencies = [ "aho-corasick", "memchr", "regex-syntax", ] [[package]] name = "regex-syntax" version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" [[package]] name = "rustix" version = "0.38.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ea3e1a662af26cd7a3ba09c0297a31af215563ecf42817c98df621387f4e949" dependencies = [ "bitflags", "errno", "libc", "linux-raw-sys", "windows-sys", ] [[package]] name = "same-file" version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" dependencies = [ "winapi-util", ] [[package]] name = "serde" version = "1.0.197" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" version = "1.0.197" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "syn" version = "2.0.50" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74f1bdc9872430ce9b75da68329d1c1746faf50ffac5f19e02b71e37ff881ffb" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "tempfile" version = "3.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" dependencies = [ "cfg-if", "fastrand", "rustix", "windows-sys", ] [[package]] name = "termtree" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76" [[package]] name = "unicode-ident" version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" [[package]] name = "unicode-width" version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" [[package]] name = "wait-timeout" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6" dependencies = [ "libc", ] [[package]] name = "wasm-bindgen" version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" dependencies = [ "cfg-if", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" dependencies = [ "bumpalo", "log", "once_cell", "proc-macro2", "quote", "syn", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-macro" version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" dependencies = [ "quote", "wasm-bindgen-macro-support", ] [[package]] name = "wasm-bindgen-macro-support" version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", "syn", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" [[package]] name = "winapi" version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" dependencies = [ "winapi-i686-pc-windows-gnu", "winapi-x86_64-pc-windows-gnu", ] [[package]] name = "winapi-i686-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" dependencies = [ "winapi", ] [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows-core" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" dependencies = [ "windows-targets", ] [[package]] name = "windows-sys" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ "windows-targets", ] [[package]] name = "windows-targets" version = "0.52.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d98532992affa02e52709d5b4d145a3668ae10d9081eea4a7f26f719a8476f71" dependencies = [ "windows_aarch64_gnullvm", "windows_aarch64_msvc", "windows_i686_gnu", "windows_i686_msvc", "windows_x86_64_gnu", "windows_x86_64_gnullvm", "windows_x86_64_msvc", ] [[package]] name = "windows_aarch64_gnullvm" version = "0.52.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7269c1442e75af9fa59290383f7665b828efc76c429cc0b7f2ecb33cf51ebae" [[package]] name = "windows_aarch64_msvc" version = "0.52.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f70ab2cebf332b7ecbdd98900c2da5298a8c862472fb35c75fc297eabb9d89b8" [[package]] name = "windows_i686_gnu" version = "0.52.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "679f235acf6b1639408c0f6db295697a19d103b0cdc88146aa1b992c580c647d" [[package]] name = "windows_i686_msvc" version = "0.52.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3480ac194b55ae274a7e135c21645656825da4a7f5b6e9286291b2113c94a78b" [[package]] name = "windows_x86_64_gnu" version = "0.52.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42c46bab241c121402d1cb47d028ea3680ee2f359dcc287482dcf7fdddc73363" [[package]] name = "windows_x86_64_gnullvm" version = "0.52.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc885a4332ee1afb9a1bacf11514801011725570d35675abc229ce7e3afe4d20" [[package]] name = "windows_x86_64_msvc" version = "0.52.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e440c60457f84b0bee09208e62acc7ade264b38c4453f6312b8c9ab1613e73c" [[package]] name = "yansi" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec" diffutils-0.4.1/Cargo.toml0000644000000023400000000000100110240ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2021" name = "diffutils" version = "0.4.1" description = "A CLI app for generating diff files" readme = "README.md" license = "MIT OR Apache-2.0" repository = "https://github.com/uutils/diffutils" [profile.dist] lto = "thin" inherits = "release" [lib] name = "diffutilslib" path = "src/lib.rs" [[bin]] name = "diffutils" path = "src/main.rs" [dependencies.chrono] version = "0.4.38" [dependencies.diff] version = "0.1.13" [dependencies.regex] version = "1.10.4" [dependencies.same-file] version = "1.0.6" [dependencies.unicode-width] version = "0.1.11" [dev-dependencies.assert_cmd] version = "2.0.14" [dev-dependencies.predicates] version = "3.1.0" [dev-dependencies.pretty_assertions] version = "1" [dev-dependencies.tempfile] version = "3.10.1" diffutils-0.4.1/Cargo.toml.orig000064400000000000000000000021071046102023000145060ustar 00000000000000[package] name = "diffutils" version = "0.4.1" edition = "2021" description = "A CLI app for generating diff files" license = "MIT OR Apache-2.0" repository = "https://github.com/uutils/diffutils" [lib] name = "diffutilslib" path = "src/lib.rs" [[bin]] name = "diffutils" path = "src/main.rs" [dependencies] chrono = "0.4.38" diff = "0.1.13" regex = "1.10.4" same-file = "1.0.6" unicode-width = "0.1.11" [dev-dependencies] pretty_assertions = "1" assert_cmd = "2.0.14" predicates = "3.1.0" tempfile = "3.10.1" # The profile that 'cargo dist' will build with [profile.dist] inherits = "release" lto = "thin" # Config for 'cargo dist' [workspace.metadata.dist] # The preferred cargo-dist version to use in CI (Cargo.toml SemVer syntax) cargo-dist-version = "0.12.0" # CI backends to support ci = ["github"] # The installers to generate for each app installers = [] # Target platforms to build apps for (Rust target-triple syntax) targets = ["aarch64-apple-darwin", "x86_64-apple-darwin", "x86_64-unknown-linux-gnu", "x86_64-pc-windows-msvc"] # Publish jobs to run in CI pr-run-mode = "plan" diffutils-0.4.1/LICENSE-APACHE000064400000000000000000000230711046102023000135460ustar 00000000000000Copyright (c) Michael Howell Copyright (c) uutils developers Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS diffutils-0.4.1/LICENSE-MIT000064400000000000000000000020751046102023000132570ustar 00000000000000Copyright (c) Michael Howell Copyright (c) uutils developers Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diffutils-0.4.1/README.md000064400000000000000000000032701046102023000131000ustar 00000000000000[![Crates.io](https://img.shields.io/crates/v/diffutils.svg)](https://crates.io/crates/diffutils) [![Discord](https://img.shields.io/badge/discord-join-7289DA.svg?logo=discord&longCache=true&style=flat)](https://discord.gg/wQVJbvJ) [![License](http://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/uutils/diffutils/blob/main/LICENSE) [![dependency status](https://deps.rs/repo/github/uutils/diffutils/status.svg)](https://deps.rs/repo/github/uutils/diffutils) [![CodeCov](https://codecov.io/gh/uutils/diffutils/branch/main/graph/badge.svg)](https://codecov.io/gh/uutils/diffutils) The goal of this package is to be a drop-in replacement for the [diffutils commands](https://www.gnu.org/software/diffutils/) in Rust. Based on the incomplete diff generator in https://github.com/rust-lang/rust/blob/master/src/tools/compiletest/src/runtest.rs, and made to be compatible with GNU's diff and patch tools. ## Installation Ensure you have Rust installed on your system. You can install Rust through [rustup](https://rustup.rs/). Clone the repository and build the project using Cargo: ```bash git clone https://github.com/uutils/diffutils.git cd diffutils cargo build --release ``` ## Example ```bash cat <fruits_old.txt Apple Banana Cherry EOF cat <fruits_new.txt Apple Fig Cherry EOF $ cargo run -- -u fruits_old.txt fruits_new.txt Finished dev [unoptimized + debuginfo] target(s) in 0.00s Running `target/debug/diffutils -u fruits_old.txt fruits_new.txt` --- fruits_old.txt +++ fruits_new.txt @@ -1,3 +1,3 @@ Apple -Banana +Fig Cherry ``` ## License diffutils is licensed under the MIT and Apache Licenses - see the `LICENSE-MIT` or `LICENSE-APACHE` files for details diffutils-0.4.1/renovate.json000064400000000000000000000001621046102023000143340ustar 00000000000000{ "$schema": "https://docs.renovatebot.com/renovate-schema.json", "extends": [ "config:recommended" ] } diffutils-0.4.1/src/context_diff.rs000064400000000000000000001057571046102023000154470ustar 00000000000000// This file is part of the uutils diffutils package. // // For the full copyright and license information, please view the LICENSE-* // files that was distributed with this source code. use std::collections::VecDeque; use std::io::Write; use crate::params::Params; use crate::utils::do_write_line; use crate::utils::get_modification_time; #[derive(Debug, PartialEq)] pub enum DiffLine { Context(Vec), Change(Vec), Add(Vec), } #[derive(Debug, PartialEq)] struct Mismatch { pub line_number_expected: usize, pub line_number_actual: usize, pub expected: Vec, pub actual: Vec, pub expected_missing_nl: bool, pub actual_missing_nl: bool, pub expected_all_context: bool, pub actual_all_context: bool, } impl Mismatch { fn new(line_number_expected: usize, line_number_actual: usize) -> Mismatch { Mismatch { line_number_expected, line_number_actual, expected: Vec::new(), actual: Vec::new(), expected_missing_nl: false, actual_missing_nl: false, expected_all_context: false, actual_all_context: false, } } } // Produces a diff between the expected output and actual output. fn make_diff( expected: &[u8], actual: &[u8], context_size: usize, stop_early: bool, ) -> Vec { let mut line_number_expected = 1; let mut line_number_actual = 1; let mut context_queue: VecDeque<&[u8]> = VecDeque::with_capacity(context_size); let mut lines_since_mismatch = context_size + 1; let mut results = Vec::new(); let mut mismatch = Mismatch::new(0, 0); let mut expected_lines: Vec<&[u8]> = expected.split(|&c| c == b'\n').collect(); let mut actual_lines: Vec<&[u8]> = actual.split(|&c| c == b'\n').collect(); debug_assert_eq!(b"".split(|&c| c == b'\n').count(), 1); // ^ means that underflow here is impossible let expected_lines_count = expected_lines.len() - 1; let actual_lines_count = actual_lines.len() - 1; if expected_lines.last() == Some(&&b""[..]) { expected_lines.pop(); } if actual_lines.last() == Some(&&b""[..]) { actual_lines.pop(); } // Rust only allows allocations to grow to isize::MAX, and this is bigger than that. let mut expected_lines_change_idx: usize = !0; for result in diff::slice(&expected_lines, &actual_lines) { match result { diff::Result::Left(str) => { if lines_since_mismatch > context_size && lines_since_mismatch > 0 { results.push(mismatch); mismatch = Mismatch::new( line_number_expected - context_queue.len(), line_number_actual - context_queue.len(), ); } while let Some(line) = context_queue.pop_front() { mismatch.expected.push(DiffLine::Context(line.to_vec())); mismatch.actual.push(DiffLine::Context(line.to_vec())); } expected_lines_change_idx = mismatch.expected.len(); mismatch.expected.push(DiffLine::Add(str.to_vec())); if line_number_expected > expected_lines_count { mismatch.expected_missing_nl = true; } line_number_expected += 1; lines_since_mismatch = 0; } diff::Result::Right(str) => { if lines_since_mismatch > context_size && lines_since_mismatch > 0 { results.push(mismatch); mismatch = Mismatch::new( line_number_expected - context_queue.len(), line_number_actual - context_queue.len(), ); expected_lines_change_idx = !0; } while let Some(line) = context_queue.pop_front() { mismatch.expected.push(DiffLine::Context(line.to_vec())); mismatch.actual.push(DiffLine::Context(line.to_vec())); } if let Some(DiffLine::Add(content)) = mismatch.expected.get_mut(expected_lines_change_idx) { let content = std::mem::take(content); mismatch.expected[expected_lines_change_idx] = DiffLine::Change(content); expected_lines_change_idx = expected_lines_change_idx.wrapping_sub(1); // if 0, becomes !0 mismatch.actual.push(DiffLine::Change(str.to_vec())); } else { mismatch.actual.push(DiffLine::Add(str.to_vec())); } if line_number_actual > actual_lines_count { mismatch.actual_missing_nl = true; } line_number_actual += 1; lines_since_mismatch = 0; } diff::Result::Both(str, _) => { expected_lines_change_idx = !0; // if one of them is missing a newline and the other isn't, then they don't actually match if (line_number_actual > actual_lines_count) && (line_number_expected > expected_lines_count) { if context_queue.len() < context_size { while let Some(line) = context_queue.pop_front() { mismatch.expected.push(DiffLine::Context(line.to_vec())); mismatch.actual.push(DiffLine::Context(line.to_vec())); } if lines_since_mismatch < context_size { mismatch.expected.push(DiffLine::Context(str.to_vec())); mismatch.actual.push(DiffLine::Context(str.to_vec())); mismatch.expected_missing_nl = true; mismatch.actual_missing_nl = true; } } lines_since_mismatch = 0; } else if line_number_actual > actual_lines_count { if lines_since_mismatch >= context_size && lines_since_mismatch > 0 { results.push(mismatch); mismatch = Mismatch::new( line_number_expected - context_queue.len(), line_number_actual - context_queue.len(), ); } while let Some(line) = context_queue.pop_front() { mismatch.expected.push(DiffLine::Context(line.to_vec())); mismatch.actual.push(DiffLine::Context(line.to_vec())); } mismatch.expected.push(DiffLine::Change(str.to_vec())); mismatch.actual.push(DiffLine::Change(str.to_vec())); mismatch.actual_missing_nl = true; lines_since_mismatch = 0; } else if line_number_expected > expected_lines_count { if lines_since_mismatch >= context_size && lines_since_mismatch > 0 { results.push(mismatch); mismatch = Mismatch::new( line_number_expected - context_queue.len(), line_number_actual - context_queue.len(), ); } while let Some(line) = context_queue.pop_front() { mismatch.expected.push(DiffLine::Context(line.to_vec())); mismatch.actual.push(DiffLine::Context(line.to_vec())); } mismatch.expected.push(DiffLine::Change(str.to_vec())); mismatch.expected_missing_nl = true; mismatch.actual.push(DiffLine::Change(str.to_vec())); lines_since_mismatch = 0; } else { debug_assert!(context_queue.len() <= context_size); if context_queue.len() >= context_size { let _ = context_queue.pop_front(); } if lines_since_mismatch < context_size { mismatch.expected.push(DiffLine::Context(str.to_vec())); mismatch.actual.push(DiffLine::Context(str.to_vec())); } else if context_size > 0 { context_queue.push_back(str); } lines_since_mismatch += 1; } line_number_expected += 1; line_number_actual += 1; } } if stop_early && !results.is_empty() { // Optimization: stop analyzing the files as soon as there are any differences return results; } } results.push(mismatch); results.remove(0); if results.is_empty() && expected_lines_count != actual_lines_count { let mut mismatch = Mismatch::new(expected_lines.len(), actual_lines.len()); // empty diff and only expected lines has a missing line at end if expected_lines_count != expected_lines.len() { mismatch.expected.push(DiffLine::Change( expected_lines .pop() .expect("can't be empty; produced by split()") .to_vec(), )); mismatch.expected_missing_nl = true; mismatch.actual.push(DiffLine::Change( actual_lines .pop() .expect("can't be empty; produced by split()") .to_vec(), )); results.push(mismatch); } else if actual_lines_count != actual_lines.len() { mismatch.expected.push(DiffLine::Change( expected_lines .pop() .expect("can't be empty; produced by split()") .to_vec(), )); mismatch.actual.push(DiffLine::Change( actual_lines .pop() .expect("can't be empty; produced by split()") .to_vec(), )); mismatch.actual_missing_nl = true; results.push(mismatch); } } // hunks with pure context lines get truncated to empty for mismatch in &mut results { if !mismatch .expected .iter() .any(|x| !matches!(&x, DiffLine::Context(_))) { mismatch.expected_all_context = true; } if !mismatch .actual .iter() .any(|x| !matches!(&x, DiffLine::Context(_))) { mismatch.actual_all_context = true; } } results } #[must_use] pub fn diff(expected: &[u8], actual: &[u8], params: &Params) -> Vec { let from_modified_time = get_modification_time(¶ms.from.to_string_lossy()); let to_modified_time = get_modification_time(¶ms.to.to_string_lossy()); let mut output = format!( "*** {0}\t{1}\n--- {2}\t{3}\n", params.from.to_string_lossy(), from_modified_time, params.to.to_string_lossy(), to_modified_time ) .into_bytes(); let diff_results = make_diff(expected, actual, params.context_count, params.brief); if diff_results.is_empty() { return Vec::new(); } if params.brief { return output; } for result in diff_results { let mut line_number_expected = result.line_number_expected; let mut line_number_actual = result.line_number_actual; let mut expected_count = result.expected.len(); let mut actual_count = result.actual.len(); if expected_count == 0 { line_number_expected -= 1; expected_count = 1; } if actual_count == 0 { line_number_actual -= 1; actual_count = 1; } let end_line_number_expected = expected_count + line_number_expected - 1; let end_line_number_actual = actual_count + line_number_actual - 1; let exp_start = if end_line_number_expected == line_number_expected { String::new() } else { format!("{line_number_expected},") }; let act_start = if end_line_number_actual == line_number_actual { String::new() } else { format!("{line_number_actual},") }; writeln!( output, "***************\n*** {exp_start}{end_line_number_expected} ****" ) .expect("write to Vec is infallible"); if !result.expected_all_context { for line in result.expected { match line { DiffLine::Context(e) => { write!(output, " ").expect("write to Vec is infallible"); do_write_line(&mut output, &e, params.expand_tabs, params.tabsize) .expect("write to Vec is infallible"); writeln!(output).unwrap(); } DiffLine::Change(e) => { write!(output, "! ").expect("write to Vec is infallible"); do_write_line(&mut output, &e, params.expand_tabs, params.tabsize) .expect("write to Vec is infallible"); writeln!(output).unwrap(); } DiffLine::Add(e) => { write!(output, "- ").expect("write to Vec is infallible"); do_write_line(&mut output, &e, params.expand_tabs, params.tabsize) .expect("write to Vec is infallible"); writeln!(output).unwrap(); } } } if result.expected_missing_nl { writeln!(output, r"\ No newline at end of file") .expect("write to Vec is infallible"); } } writeln!(output, "--- {act_start}{end_line_number_actual} ----") .expect("write to Vec is infallible"); if !result.actual_all_context { for line in result.actual { match line { DiffLine::Context(e) => { write!(output, " ").expect("write to Vec is infallible"); do_write_line(&mut output, &e, params.expand_tabs, params.tabsize) .expect("write to Vec is infallible"); writeln!(output).unwrap(); } DiffLine::Change(e) => { write!(output, "! ").expect("write to Vec is infallible"); do_write_line(&mut output, &e, params.expand_tabs, params.tabsize) .expect("write to Vec is infallible"); writeln!(output).unwrap(); } DiffLine::Add(e) => { write!(output, "+ ").expect("write to Vec is infallible"); do_write_line(&mut output, &e, params.expand_tabs, params.tabsize) .expect("write to Vec is infallible"); writeln!(output).unwrap(); } } } if result.actual_missing_nl { writeln!(output, r"\ No newline at end of file") .expect("write to Vec is infallible"); } } } output } #[cfg(test)] mod tests { use super::*; use pretty_assertions::assert_eq; #[test] fn test_permutations() { // test all possible six-line files. let target = "target/context-diff/"; let _ = std::fs::create_dir(target); for &a in &[0, 1, 2] { for &b in &[0, 1, 2] { for &c in &[0, 1, 2] { for &d in &[0, 1, 2] { for &e in &[0, 1, 2] { for &f in &[0, 1, 2] { use std::fs::{self, File}; use std::io::Write; use std::process::Command; let mut alef = Vec::new(); let mut bet = Vec::new(); alef.write_all(if a == 0 { b"a\n" } else { b"b\n" }) .unwrap(); if a != 2 { bet.write_all(b"b\n").unwrap(); } alef.write_all(if b == 0 { b"c\n" } else { b"d\n" }) .unwrap(); if b != 2 { bet.write_all(b"d\n").unwrap(); } alef.write_all(if c == 0 { b"e\n" } else { b"f\n" }) .unwrap(); if c != 2 { bet.write_all(b"f\n").unwrap(); } alef.write_all(if d == 0 { b"g\n" } else { b"h\n" }) .unwrap(); if d != 2 { bet.write_all(b"h\n").unwrap(); } alef.write_all(if e == 0 { b"i\n" } else { b"j\n" }) .unwrap(); if e != 2 { bet.write_all(b"j\n").unwrap(); } alef.write_all(if f == 0 { b"k\n" } else { b"l\n" }) .unwrap(); if f != 2 { bet.write_all(b"l\n").unwrap(); } // This test diff is intentionally reversed. // We want it to turn the alef into bet. let diff = diff( &alef, &bet, &Params { from: "a/alef".into(), to: (&format!("{target}/alef")).into(), context_count: 2, ..Default::default() }, ); File::create(&format!("{target}/ab.diff")) .unwrap() .write_all(&diff) .unwrap(); let mut fa = File::create(&format!("{target}/alef")).unwrap(); fa.write_all(&alef[..]).unwrap(); let mut fb = File::create(&format!("{target}/bet")).unwrap(); fb.write_all(&bet[..]).unwrap(); let _ = fa; let _ = fb; let output = Command::new("patch") .arg("-p0") .arg("--context") .stdin(File::open(&format!("{target}/ab.diff")).unwrap()) .output() .unwrap(); assert!(output.status.success(), "{output:?}"); //println!("{}", String::from_utf8_lossy(&output.stdout)); //println!("{}", String::from_utf8_lossy(&output.stderr)); let alef = fs::read(&format!("{target}/alef")).unwrap(); assert_eq!(alef, bet); } } } } } } } #[test] fn test_permutations_empty_lines() { let target = "target/context-diff/"; // test all possible six-line files with missing newlines. let _ = std::fs::create_dir(target); for &a in &[0, 1, 2] { for &b in &[0, 1, 2] { for &c in &[0, 1, 2] { for &d in &[0, 1, 2] { for &e in &[0, 1, 2] { for &f in &[0, 1, 2] { use std::fs::{self, File}; use std::io::Write; use std::process::Command; let mut alef = Vec::new(); let mut bet = Vec::new(); alef.write_all(if a == 0 { b"\n" } else { b"b\n" }).unwrap(); if a != 2 { bet.write_all(b"b\n").unwrap(); } alef.write_all(if b == 0 { b"\n" } else { b"d\n" }).unwrap(); if b != 2 { bet.write_all(b"d\n").unwrap(); } alef.write_all(if c == 0 { b"\n" } else { b"f\n" }).unwrap(); if c != 2 { bet.write_all(b"f\n").unwrap(); } alef.write_all(if d == 0 { b"\n" } else { b"h\n" }).unwrap(); if d != 2 { bet.write_all(b"h\n").unwrap(); } alef.write_all(if e == 0 { b"\n" } else { b"j\n" }).unwrap(); if e != 2 { bet.write_all(b"j\n").unwrap(); } alef.write_all(if f == 0 { b"\n" } else { b"l\n" }).unwrap(); if f != 2 { bet.write_all(b"l\n").unwrap(); } // This test diff is intentionally reversed. // We want it to turn the alef into bet. let diff = diff( &alef, &bet, &Params { from: "a/alef_".into(), to: (&format!("{target}/alef_")).into(), context_count: 2, ..Default::default() }, ); File::create(&format!("{target}/ab_.diff")) .unwrap() .write_all(&diff) .unwrap(); let mut fa = File::create(&format!("{target}/alef_")).unwrap(); fa.write_all(&alef[..]).unwrap(); let mut fb = File::create(&format!("{target}/bet_")).unwrap(); fb.write_all(&bet[..]).unwrap(); let _ = fa; let _ = fb; let output = Command::new("patch") .arg("-p0") .arg("--context") .stdin(File::open(&format!("{target}/ab_.diff")).unwrap()) .output() .unwrap(); assert!(output.status.success(), "{output:?}"); //println!("{}", String::from_utf8_lossy(&output.stdout)); //println!("{}", String::from_utf8_lossy(&output.stderr)); let alef = fs::read(&format!("{target}/alef_")).unwrap(); assert_eq!(alef, bet); } } } } } } } #[test] fn test_permutations_missing_lines() { let target = "target/context-diff/"; // test all possible six-line files. let _ = std::fs::create_dir(target); for &a in &[0, 1, 2] { for &b in &[0, 1, 2] { for &c in &[0, 1, 2] { for &d in &[0, 1, 2] { for &e in &[0, 1, 2] { for &f in &[0, 1, 2] { use std::fs::{self, File}; use std::io::Write; use std::process::Command; let mut alef = Vec::new(); let mut bet = Vec::new(); alef.write_all(if a == 0 { b"a\n" } else { b"" }).unwrap(); if a != 2 { bet.write_all(b"b\n").unwrap(); } alef.write_all(if b == 0 { b"c\n" } else { b"" }).unwrap(); if b != 2 { bet.write_all(b"d\n").unwrap(); } alef.write_all(if c == 0 { b"e\n" } else { b"" }).unwrap(); if c != 2 { bet.write_all(b"f\n").unwrap(); } alef.write_all(if d == 0 { b"g\n" } else { b"" }).unwrap(); if d != 2 { bet.write_all(b"h\n").unwrap(); } alef.write_all(if e == 0 { b"i\n" } else { b"" }).unwrap(); if e != 2 { bet.write_all(b"j\n").unwrap(); } alef.write_all(if f == 0 { b"k\n" } else { b"" }).unwrap(); if f != 2 { bet.write_all(b"l\n").unwrap(); } if alef.is_empty() && bet.is_empty() { continue; }; // This test diff is intentionally reversed. // We want it to turn the alef into bet. let diff = diff( &alef, &bet, &Params { from: "a/alefx".into(), to: (&format!("{target}/alefx")).into(), context_count: 2, ..Default::default() }, ); File::create(&format!("{target}/abx.diff")) .unwrap() .write_all(&diff) .unwrap(); let mut fa = File::create(&format!("{target}/alefx")).unwrap(); fa.write_all(&alef[..]).unwrap(); let mut fb = File::create(&format!("{target}/betx")).unwrap(); fb.write_all(&bet[..]).unwrap(); let _ = fa; let _ = fb; let output = Command::new("patch") .arg("-p0") .arg("--context") .stdin(File::open(&format!("{target}/abx.diff")).unwrap()) .output() .unwrap(); assert!(output.status.success(), "{output:?}"); //println!("{}", String::from_utf8_lossy(&output.stdout)); //println!("{}", String::from_utf8_lossy(&output.stderr)); let alef = fs::read(&format!("{target}/alefx")).unwrap(); assert_eq!(alef, bet); } } } } } } } #[test] fn test_permutations_reverse() { let target = "target/context-diff/"; // test all possible six-line files. let _ = std::fs::create_dir(target); for &a in &[0, 1, 2] { for &b in &[0, 1, 2] { for &c in &[0, 1, 2] { for &d in &[0, 1, 2] { for &e in &[0, 1, 2] { for &f in &[0, 1, 2] { use std::fs::{self, File}; use std::io::Write; use std::process::Command; let mut alef = Vec::new(); let mut bet = Vec::new(); alef.write_all(if a == 0 { b"a\n" } else { b"f\n" }) .unwrap(); if a != 2 { bet.write_all(b"a\n").unwrap(); } alef.write_all(if b == 0 { b"b\n" } else { b"e\n" }) .unwrap(); if b != 2 { bet.write_all(b"b\n").unwrap(); } alef.write_all(if c == 0 { b"c\n" } else { b"d\n" }) .unwrap(); if c != 2 { bet.write_all(b"c\n").unwrap(); } alef.write_all(if d == 0 { b"d\n" } else { b"c\n" }) .unwrap(); if d != 2 { bet.write_all(b"d\n").unwrap(); } alef.write_all(if e == 0 { b"e\n" } else { b"b\n" }) .unwrap(); if e != 2 { bet.write_all(b"e\n").unwrap(); } alef.write_all(if f == 0 { b"f\n" } else { b"a\n" }) .unwrap(); if f != 2 { bet.write_all(b"f\n").unwrap(); } // This test diff is intentionally reversed. // We want it to turn the alef into bet. let diff = diff( &alef, &bet, &Params { from: "a/alefr".into(), to: (&format!("{target}/alefr")).into(), context_count: 2, ..Default::default() }, ); File::create(&format!("{target}/abr.diff")) .unwrap() .write_all(&diff) .unwrap(); let mut fa = File::create(&format!("{target}/alefr")).unwrap(); fa.write_all(&alef[..]).unwrap(); let mut fb = File::create(&format!("{target}/betr")).unwrap(); fb.write_all(&bet[..]).unwrap(); let _ = fa; let _ = fb; let output = Command::new("patch") .arg("-p0") .arg("--context") .stdin(File::open(&format!("{target}/abr.diff")).unwrap()) .output() .unwrap(); assert!(output.status.success(), "{output:?}"); //println!("{}", String::from_utf8_lossy(&output.stdout)); //println!("{}", String::from_utf8_lossy(&output.stderr)); let alef = fs::read(&format!("{target}/alefr")).unwrap(); assert_eq!(alef, bet); } } } } } } } #[test] fn test_stop_early() { use crate::assert_diff_eq; let from_filename = "foo"; let from = ["a", "b", "c", ""].join("\n"); let to_filename = "bar"; let to = ["a", "d", "c", ""].join("\n"); let diff_full = diff( from.as_bytes(), to.as_bytes(), &Params { from: from_filename.into(), to: to_filename.into(), ..Default::default() }, ); let expected_full = [ "*** foo\tTIMESTAMP", "--- bar\tTIMESTAMP", "***************", "*** 1,3 ****", " a", "! b", " c", "--- 1,3 ----", " a", "! d", " c", "", ] .join("\n"); assert_diff_eq!(diff_full, expected_full); let diff_brief = diff( from.as_bytes(), to.as_bytes(), &Params { from: from_filename.into(), to: to_filename.into(), brief: true, ..Default::default() }, ); let expected_brief = ["*** foo\tTIMESTAMP", "--- bar\tTIMESTAMP", ""].join("\n"); assert_diff_eq!(diff_brief, expected_brief); let nodiff_full = diff( from.as_bytes(), from.as_bytes(), &Params { from: from_filename.into(), to: to_filename.into(), ..Default::default() }, ); assert!(nodiff_full.is_empty()); let nodiff_brief = diff( from.as_bytes(), from.as_bytes(), &Params { from: from_filename.into(), to: to_filename.into(), brief: true, ..Default::default() }, ); assert!(nodiff_brief.is_empty()); } } diffutils-0.4.1/src/ed_diff.rs000064400000000000000000000466461046102023000143540ustar 00000000000000// This file is part of the uutils diffutils package. // // For the full copyright and license information, please view the LICENSE-* // files that was distributed with this source code. use std::io::Write; use crate::params::Params; use crate::utils::do_write_line; #[derive(Debug, PartialEq)] struct Mismatch { pub line_number_expected: usize, pub line_number_actual: usize, pub expected: Vec>, pub actual: Vec>, } #[derive(Debug, PartialEq, Eq)] pub enum DiffError { MissingNL, } impl std::fmt::Display for DiffError { fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { std::fmt::Display::fmt("No newline at end of file", f) } } impl From for String { fn from(_: DiffError) -> String { "No newline at end of file".into() } } impl Mismatch { fn new(line_number_expected: usize, line_number_actual: usize) -> Mismatch { Mismatch { line_number_expected, line_number_actual, expected: Vec::new(), actual: Vec::new(), } } } // Produces a diff between the expected output and actual output. fn make_diff(expected: &[u8], actual: &[u8], stop_early: bool) -> Result, DiffError> { let mut line_number_expected = 1; let mut line_number_actual = 1; let mut results = Vec::new(); let mut mismatch = Mismatch::new(line_number_expected, line_number_actual); let mut expected_lines: Vec<&[u8]> = expected.split(|&c| c == b'\n').collect(); let mut actual_lines: Vec<&[u8]> = actual.split(|&c| c == b'\n').collect(); debug_assert_eq!(b"".split(|&c| c == b'\n').count(), 1); // ^ means that underflow here is impossible let _expected_lines_count = expected_lines.len() - 1; let _actual_lines_count = actual_lines.len() - 1; if expected_lines.last() == Some(&&b""[..]) { expected_lines.pop(); } else { return Err(DiffError::MissingNL); } if actual_lines.last() == Some(&&b""[..]) { actual_lines.pop(); } else { return Err(DiffError::MissingNL); } for result in diff::slice(&expected_lines, &actual_lines) { match result { diff::Result::Left(str) => { if !mismatch.actual.is_empty() { results.push(mismatch); mismatch = Mismatch::new(line_number_expected, line_number_actual); } mismatch.expected.push(str.to_vec()); line_number_expected += 1; } diff::Result::Right(str) => { mismatch.actual.push(str.to_vec()); line_number_actual += 1; } diff::Result::Both(_str, _) => { line_number_expected += 1; line_number_actual += 1; if !mismatch.actual.is_empty() || !mismatch.expected.is_empty() { results.push(mismatch); mismatch = Mismatch::new(line_number_expected, line_number_actual); } else { mismatch.line_number_expected = line_number_expected; mismatch.line_number_actual = line_number_actual; } } } if stop_early && !results.is_empty() { // Optimization: stop analyzing the files as soon as there are any differences return Ok(results); } } if !mismatch.actual.is_empty() || !mismatch.expected.is_empty() { results.push(mismatch); } Ok(results) } pub fn diff(expected: &[u8], actual: &[u8], params: &Params) -> Result, DiffError> { let mut output = Vec::new(); let diff_results = make_diff(expected, actual, params.brief)?; if params.brief && !diff_results.is_empty() { write!(&mut output, "\0").unwrap(); return Ok(output); } let mut lines_offset = 0; for result in diff_results { let line_number_expected: isize = result.line_number_expected as isize + lines_offset; let _line_number_actual: isize = result.line_number_actual as isize + lines_offset; let expected_count: isize = result.expected.len() as isize; let actual_count: isize = result.actual.len() as isize; match (expected_count, actual_count) { (0, 0) => unreachable!(), (0, _) => writeln!(&mut output, "{}a", line_number_expected - 1).unwrap(), (_, 0) => writeln!( &mut output, "{},{}d", line_number_expected, expected_count + line_number_expected - 1 ) .unwrap(), (1, _) => writeln!(&mut output, "{line_number_expected}c").unwrap(), _ => writeln!( &mut output, "{},{}c", line_number_expected, expected_count + line_number_expected - 1 ) .unwrap(), } lines_offset += actual_count - expected_count; if actual_count != 0 { for actual in &result.actual { if actual == b"." { writeln!(&mut output, "..\n.\ns/.//\na").unwrap(); } else { do_write_line(&mut output, actual, params.expand_tabs, params.tabsize).unwrap(); writeln!(&mut output).unwrap(); } } writeln!(&mut output, ".").unwrap(); } } Ok(output) } #[cfg(test)] mod tests { use super::*; use pretty_assertions::assert_eq; pub fn diff_w(expected: &[u8], actual: &[u8], filename: &str) -> Result, DiffError> { let mut output = diff(expected, actual, &Params::default())?; writeln!(&mut output, "w {filename}").unwrap(); Ok(output) } #[test] fn test_basic() { let from = b"a\n"; let to = b"b\n"; let diff = diff(from, to, &Params::default()).unwrap(); let expected = ["1c", "b", ".", ""].join("\n"); assert_eq!(diff, expected.as_bytes()); } #[test] fn test_permutations() { let target = "target/ed-diff/"; // test all possible six-line files. let _ = std::fs::create_dir(target); for &a in &[0, 1, 2] { for &b in &[0, 1, 2] { for &c in &[0, 1, 2] { for &d in &[0, 1, 2] { for &e in &[0, 1, 2] { for &f in &[0, 1, 2] { use std::fs::File; use std::io::Write; let mut alef = Vec::new(); let mut bet = Vec::new(); alef.write_all(if a == 0 { b"a\n" } else { b"b\n" }) .unwrap(); if a != 2 { bet.write_all(b"b\n").unwrap(); } alef.write_all(if b == 0 { b"c\n" } else { b"d\n" }) .unwrap(); if b != 2 { bet.write_all(b"d\n").unwrap(); } alef.write_all(if c == 0 { b"e\n" } else { b"f\n" }) .unwrap(); if c != 2 { bet.write_all(b"f\n").unwrap(); } alef.write_all(if d == 0 { b"g\n" } else { b"h\n" }) .unwrap(); if d != 2 { bet.write_all(b"h\n").unwrap(); } alef.write_all(if e == 0 { b"i\n" } else { b"j\n" }) .unwrap(); if e != 2 { bet.write_all(b"j\n").unwrap(); } alef.write_all(if f == 0 { b"k\n" } else { b"l\n" }) .unwrap(); if f != 2 { bet.write_all(b"l\n").unwrap(); } // This test diff is intentionally reversed. // We want it to turn the alef into bet. let diff = diff_w(&alef, &bet, &format!("{target}/alef")).unwrap(); File::create(&format!("{target}/ab.ed")) .unwrap() .write_all(&diff) .unwrap(); let mut fa = File::create(&format!("{target}/alef")).unwrap(); fa.write_all(&alef[..]).unwrap(); let mut fb = File::create(&format!("{target}/bet")).unwrap(); fb.write_all(&bet[..]).unwrap(); let _ = fa; let _ = fb; #[cfg(not(windows))] // there's no ed on windows { use std::process::Command; let output = Command::new("ed") .arg(&format!("{target}/alef")) .stdin(File::open(&format!("{target}/ab.ed")).unwrap()) .output() .unwrap(); assert!(output.status.success(), "{output:?}"); //println!("{}", String::from_utf8_lossy(&output.stdout)); //println!("{}", String::from_utf8_lossy(&output.stderr)); let alef = std::fs::read(&format!("{target}/alef")).unwrap(); assert_eq!(alef, bet); } } } } } } } } #[test] fn test_permutations_empty_lines() { let target = "target/ed-diff/"; // test all possible six-line files with missing newlines. let _ = std::fs::create_dir(target); for &a in &[0, 1, 2] { for &b in &[0, 1, 2] { for &c in &[0, 1, 2] { for &d in &[0, 1, 2] { for &e in &[0, 1, 2] { for &f in &[0, 1, 2] { use std::fs::File; use std::io::Write; let mut alef = Vec::new(); let mut bet = Vec::new(); alef.write_all(if a == 0 { b"\n" } else { b"b\n" }).unwrap(); if a != 2 { bet.write_all(b"b\n").unwrap(); } alef.write_all(if b == 0 { b"\n" } else { b"d\n" }).unwrap(); if b != 2 { bet.write_all(b"d\n").unwrap(); } alef.write_all(if c == 0 { b"\n" } else { b"f\n" }).unwrap(); if c != 2 { bet.write_all(b"f\n").unwrap(); } alef.write_all(if d == 0 { b"\n" } else { b"h\n" }).unwrap(); if d != 2 { bet.write_all(b"h\n").unwrap(); } alef.write_all(if e == 0 { b"\n" } else { b"j\n" }).unwrap(); if e != 2 { bet.write_all(b"j\n").unwrap(); } alef.write_all(if f == 0 { b"\n" } else { b"l\n" }).unwrap(); if f != 2 { bet.write_all(b"l\n").unwrap(); } // This test diff is intentionally reversed. // We want it to turn the alef into bet. let diff = diff_w(&alef, &bet, &format!("{target}/alef_")).unwrap(); File::create(&format!("{target}/ab_.ed")) .unwrap() .write_all(&diff) .unwrap(); let mut fa = File::create(&format!("{target}/alef_")).unwrap(); fa.write_all(&alef[..]).unwrap(); let mut fb = File::create(&format!("{target}/bet_")).unwrap(); fb.write_all(&bet[..]).unwrap(); let _ = fa; let _ = fb; #[cfg(not(windows))] // there's no ed on windows { use std::process::Command; let output = Command::new("ed") .arg(&format!("{target}/alef_")) .stdin(File::open(&format!("{target}/ab_.ed")).unwrap()) .output() .unwrap(); assert!(output.status.success(), "{output:?}"); //println!("{}", String::from_utf8_lossy(&output.stdout)); //println!("{}", String::from_utf8_lossy(&output.stderr)); let alef = std::fs::read(&format!("{target}/alef_")).unwrap(); assert_eq!(alef, bet); } } } } } } } } #[test] fn test_permutations_reverse() { let target = "target/ed-diff/"; // test all possible six-line files. let _ = std::fs::create_dir(target); for &a in &[0, 1, 2] { for &b in &[0, 1, 2] { for &c in &[0, 1, 2] { for &d in &[0, 1, 2] { for &e in &[0, 1, 2] { for &f in &[0, 1, 2] { use std::fs::File; use std::io::Write; let mut alef = Vec::new(); let mut bet = Vec::new(); alef.write_all(if a == 0 { b"a\n" } else { b"f\n" }) .unwrap(); if a != 2 { bet.write_all(b"a\n").unwrap(); } alef.write_all(if b == 0 { b"b\n" } else { b"e\n" }) .unwrap(); if b != 2 { bet.write_all(b"b\n").unwrap(); } alef.write_all(if c == 0 { b"c\n" } else { b"d\n" }) .unwrap(); if c != 2 { bet.write_all(b"c\n").unwrap(); } alef.write_all(if d == 0 { b"d\n" } else { b"c\n" }) .unwrap(); if d != 2 { bet.write_all(b"d\n").unwrap(); } alef.write_all(if e == 0 { b"e\n" } else { b"b\n" }) .unwrap(); if e != 2 { bet.write_all(b"e\n").unwrap(); } alef.write_all(if f == 0 { b"f\n" } else { b"a\n" }) .unwrap(); if f != 2 { bet.write_all(b"f\n").unwrap(); } // This test diff is intentionally reversed. // We want it to turn the alef into bet. let diff = diff_w(&alef, &bet, &format!("{target}/alefr")).unwrap(); File::create(&format!("{target}/abr.ed")) .unwrap() .write_all(&diff) .unwrap(); let mut fa = File::create(&format!("{target}/alefr")).unwrap(); fa.write_all(&alef[..]).unwrap(); let mut fb = File::create(&format!("{target}/betr")).unwrap(); fb.write_all(&bet[..]).unwrap(); let _ = fa; let _ = fb; #[cfg(not(windows))] // there's no ed on windows { use std::process::Command; let output = Command::new("ed") .arg(&format!("{target}/alefr")) .stdin(File::open(&format!("{target}/abr.ed")).unwrap()) .output() .unwrap(); assert!(output.status.success(), "{output:?}"); //println!("{}", String::from_utf8_lossy(&output.stdout)); //println!("{}", String::from_utf8_lossy(&output.stderr)); let alef = std::fs::read(&format!("{target}/alefr")).unwrap(); assert_eq!(alef, bet); } } } } } } } } #[test] fn test_stop_early() { let from = ["a", "b", "c", ""].join("\n"); let to = ["a", "d", "c", ""].join("\n"); let diff_full = diff(from.as_bytes(), to.as_bytes(), &Params::default()).unwrap(); let expected_full = ["2c", "d", ".", ""].join("\n"); assert_eq!(diff_full, expected_full.as_bytes()); let diff_brief = diff( from.as_bytes(), to.as_bytes(), &Params { brief: true, ..Default::default() }, ) .unwrap(); let expected_brief = "\0".as_bytes(); assert_eq!(diff_brief, expected_brief); let nodiff_full = diff(from.as_bytes(), from.as_bytes(), &Params::default()).unwrap(); assert!(nodiff_full.is_empty()); let nodiff_brief = diff( from.as_bytes(), from.as_bytes(), &Params { brief: true, ..Default::default() }, ) .unwrap(); assert!(nodiff_brief.is_empty()); } } diffutils-0.4.1/src/lib.rs000064400000000000000000000005271046102023000135260ustar 00000000000000pub mod context_diff; pub mod ed_diff; pub mod macros; pub mod normal_diff; pub mod params; pub mod unified_diff; pub mod utils; // Re-export the public functions/types you need pub use context_diff::diff as context_diff; pub use ed_diff::diff as ed_diff; pub use normal_diff::diff as normal_diff; pub use unified_diff::diff as unified_diff; diffutils-0.4.1/src/macros.rs000064400000000000000000000015421046102023000142420ustar 00000000000000// asserts equality of the actual diff and expected diff // considering datetime varitations // // It replaces the modification time in the actual diff // with placeholder "TIMESTAMP" and then asserts the equality // // For eg. // let brief = "*** fruits_old.txt\t2024-03-24 23:43:05.189597645 +0530\n // --- fruits_new.txt\t2024-03-24 23:35:08.922581904 +0530\n"; // // replaced = "*** fruits_old.txt\tTIMESTAMP\n // --- fruits_new.txt\tTIMESTAMP\n"; #[macro_export] macro_rules! assert_diff_eq { ($actual:expr, $expected:expr) => {{ use regex::Regex; use std::str; let diff = str::from_utf8(&$actual).unwrap(); let re = Regex::new(r"\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d+ [+-]\d{4}").unwrap(); let actual = re.replacen(diff, 2, "TIMESTAMP"); assert_eq!(actual, $expected); }}; } diffutils-0.4.1/src/main.rs000064400000000000000000000060111046102023000136760ustar 00000000000000// This file is part of the uutils diffutils package. // // For the full copyright and license information, please view the LICENSE-* // files that was distributed with this source code. use crate::params::{parse_params, Format}; use std::env; use std::ffi::OsString; use std::fs; use std::io::{self, Read, Write}; use std::process::{exit, ExitCode}; mod context_diff; mod ed_diff; mod macros; mod normal_diff; mod params; mod unified_diff; mod utils; // Exit codes are documented at // https://www.gnu.org/software/diffutils/manual/html_node/Invoking-diff.html. // An exit status of 0 means no differences were found, // 1 means some differences were found, // and 2 means trouble. fn main() -> ExitCode { let opts = env::args_os(); let params = parse_params(opts).unwrap_or_else(|error| { eprintln!("{error}"); exit(2); }); // if from and to are the same file, no need to perform any comparison let maybe_report_identical_files = || { if params.report_identical_files { println!( "Files {} and {} are identical", params.from.to_string_lossy(), params.to.to_string_lossy(), ); } }; if params.from == "-" && params.to == "-" || same_file::is_same_file(¶ms.from, ¶ms.to).unwrap_or(false) { maybe_report_identical_files(); return ExitCode::SUCCESS; } // read files fn read_file_contents(filepath: &OsString) -> io::Result> { if filepath == "-" { let mut content = Vec::new(); io::stdin().read_to_end(&mut content).and(Ok(content)) } else { fs::read(filepath) } } let from_content = match read_file_contents(¶ms.from) { Ok(from_content) => from_content, Err(e) => { eprintln!("Failed to read from-file: {e}"); return ExitCode::from(2); } }; let to_content = match read_file_contents(¶ms.to) { Ok(to_content) => to_content, Err(e) => { eprintln!("Failed to read to-file: {e}"); return ExitCode::from(2); } }; // run diff let result: Vec = match params.format { Format::Normal => normal_diff::diff(&from_content, &to_content, ¶ms), Format::Unified => unified_diff::diff(&from_content, &to_content, ¶ms), Format::Context => context_diff::diff(&from_content, &to_content, ¶ms), Format::Ed => ed_diff::diff(&from_content, &to_content, ¶ms).unwrap_or_else(|error| { eprintln!("{error}"); exit(2); }), }; if params.brief && !result.is_empty() { println!( "Files {} and {} differ", params.from.to_string_lossy(), params.to.to_string_lossy() ); } else { io::stdout().write_all(&result).unwrap(); } if result.is_empty() { maybe_report_identical_files(); ExitCode::SUCCESS } else { ExitCode::from(1) } } diffutils-0.4.1/src/normal_diff.rs000064400000000000000000000643741046102023000152520ustar 00000000000000// This file is part of the uutils diffutils package. // // For the full copyright and license information, please view the LICENSE-* // files that was distributed with this source code. use std::io::Write; use crate::params::Params; use crate::utils::do_write_line; #[derive(Debug, PartialEq)] struct Mismatch { pub line_number_expected: usize, pub line_number_actual: usize, pub expected: Vec>, pub actual: Vec>, pub expected_missing_nl: bool, pub actual_missing_nl: bool, } impl Mismatch { fn new(line_number_expected: usize, line_number_actual: usize) -> Mismatch { Mismatch { line_number_expected, line_number_actual, expected: Vec::new(), actual: Vec::new(), expected_missing_nl: false, actual_missing_nl: false, } } } // Produces a diff between the expected output and actual output. fn make_diff(expected: &[u8], actual: &[u8], stop_early: bool) -> Vec { let mut line_number_expected = 1; let mut line_number_actual = 1; let mut results = Vec::new(); let mut mismatch = Mismatch::new(line_number_expected, line_number_actual); let mut expected_lines: Vec<&[u8]> = expected.split(|&c| c == b'\n').collect(); let mut actual_lines: Vec<&[u8]> = actual.split(|&c| c == b'\n').collect(); debug_assert_eq!(b"".split(|&c| c == b'\n').count(), 1); // ^ means that underflow here is impossible let expected_lines_count = expected_lines.len() - 1; let actual_lines_count = actual_lines.len() - 1; if expected_lines.last() == Some(&&b""[..]) { expected_lines.pop(); } if actual_lines.last() == Some(&&b""[..]) { actual_lines.pop(); } for result in diff::slice(&expected_lines, &actual_lines) { match result { diff::Result::Left(str) => { if !mismatch.actual.is_empty() && !mismatch.actual_missing_nl { results.push(mismatch); mismatch = Mismatch::new(line_number_expected, line_number_actual); } mismatch.expected.push(str.to_vec()); mismatch.expected_missing_nl = line_number_expected > expected_lines_count; line_number_expected += 1; } diff::Result::Right(str) => { mismatch.actual.push(str.to_vec()); mismatch.actual_missing_nl = line_number_actual > actual_lines_count; line_number_actual += 1; } diff::Result::Both(str, _) => { match ( line_number_expected > expected_lines_count, line_number_actual > actual_lines_count, ) { (true, false) => { line_number_expected += 1; line_number_actual += 1; mismatch.expected.push(str.to_vec()); mismatch.expected_missing_nl = true; mismatch.actual.push(str.to_vec()); } (false, true) => { line_number_expected += 1; line_number_actual += 1; mismatch.actual.push(str.to_vec()); mismatch.actual_missing_nl = true; mismatch.expected.push(str.to_vec()); } (true, true) | (false, false) => { line_number_expected += 1; line_number_actual += 1; if !mismatch.actual.is_empty() || !mismatch.expected.is_empty() { results.push(mismatch); mismatch = Mismatch::new(line_number_expected, line_number_actual); } else { mismatch.line_number_expected = line_number_expected; mismatch.line_number_actual = line_number_actual; } } } } } if stop_early && !results.is_empty() { // Optimization: stop analyzing the files as soon as there are any differences return results; } } if !mismatch.actual.is_empty() || !mismatch.expected.is_empty() { results.push(mismatch); } results } #[must_use] pub fn diff(expected: &[u8], actual: &[u8], params: &Params) -> Vec { // See https://www.gnu.org/software/diffutils/manual/html_node/Detailed-Normal.html // for details on the syntax of the normal format. let mut output = Vec::new(); let diff_results = make_diff(expected, actual, params.brief); if params.brief && !diff_results.is_empty() { write!(&mut output, "\0").unwrap(); return output; } for result in diff_results { let line_number_expected = result.line_number_expected; let line_number_actual = result.line_number_actual; let expected_count = result.expected.len(); let actual_count = result.actual.len(); match (expected_count, actual_count) { (0, 0) => unreachable!(), (0, _) => writeln!( // 'a' stands for "Add lines" &mut output, "{}a{},{}", line_number_expected - 1, line_number_actual, line_number_actual + actual_count - 1 ) .unwrap(), (_, 0) => writeln!( // 'd' stands for "Delete lines" &mut output, "{},{}d{}", line_number_expected, expected_count + line_number_expected - 1, line_number_actual - 1 ) .unwrap(), (1, 1) => writeln!( // 'c' stands for "Change lines" // exactly one line replaced by one line &mut output, "{line_number_expected}c{line_number_actual}" ) .unwrap(), (1, _) => writeln!( // one line replaced by multiple lines &mut output, "{}c{},{}", line_number_expected, line_number_actual, actual_count + line_number_actual - 1 ) .unwrap(), (_, 1) => writeln!( // multiple lines replaced by one line &mut output, "{},{}c{}", line_number_expected, expected_count + line_number_expected - 1, line_number_actual ) .unwrap(), _ => writeln!( // general case: multiple lines replaced by multiple lines &mut output, "{},{}c{},{}", line_number_expected, expected_count + line_number_expected - 1, line_number_actual, actual_count + line_number_actual - 1 ) .unwrap(), } for expected in &result.expected { write!(&mut output, "< ").unwrap(); do_write_line(&mut output, expected, params.expand_tabs, params.tabsize).unwrap(); writeln!(&mut output).unwrap(); } if result.expected_missing_nl { writeln!(&mut output, r"\ No newline at end of file").unwrap(); } if expected_count != 0 && actual_count != 0 { writeln!(&mut output, "---").unwrap(); } for actual in &result.actual { write!(&mut output, "> ").unwrap(); do_write_line(&mut output, actual, params.expand_tabs, params.tabsize).unwrap(); writeln!(&mut output).unwrap(); } if result.actual_missing_nl { writeln!(&mut output, r"\ No newline at end of file").unwrap(); } } output } #[cfg(test)] mod tests { use super::*; use pretty_assertions::assert_eq; #[test] fn test_basic() { let mut a = Vec::new(); a.write_all(b"a\n").unwrap(); let mut b = Vec::new(); b.write_all(b"b\n").unwrap(); let diff = diff(&a, &b, &Params::default()); let expected = b"1c1\n< a\n---\n> b\n".to_vec(); assert_eq!(diff, expected); } #[test] fn test_permutations() { let target = "target/normal-diff/"; // test all possible six-line files. let _ = std::fs::create_dir(target); for &a in &[0, 1, 2] { for &b in &[0, 1, 2] { for &c in &[0, 1, 2] { for &d in &[0, 1, 2] { for &e in &[0, 1, 2] { for &f in &[0, 1, 2] { use std::fs::{self, File}; use std::io::Write; use std::process::Command; let mut alef = Vec::new(); let mut bet = Vec::new(); alef.write_all(if a == 0 { b"a\n" } else { b"b\n" }) .unwrap(); if a != 2 { bet.write_all(b"b\n").unwrap(); } alef.write_all(if b == 0 { b"c\n" } else { b"d\n" }) .unwrap(); if b != 2 { bet.write_all(b"d\n").unwrap(); } alef.write_all(if c == 0 { b"e\n" } else { b"f\n" }) .unwrap(); if c != 2 { bet.write_all(b"f\n").unwrap(); } alef.write_all(if d == 0 { b"g\n" } else { b"h\n" }) .unwrap(); if d != 2 { bet.write_all(b"h\n").unwrap(); } alef.write_all(if e == 0 { b"i\n" } else { b"j\n" }) .unwrap(); if e != 2 { bet.write_all(b"j\n").unwrap(); } alef.write_all(if f == 0 { b"k\n" } else { b"l\n" }) .unwrap(); if f != 2 { bet.write_all(b"l\n").unwrap(); } // This test diff is intentionally reversed. // We want it to turn the alef into bet. let diff = diff(&alef, &bet, &Params::default()); File::create(&format!("{target}/ab.diff")) .unwrap() .write_all(&diff) .unwrap(); let mut fa = File::create(&format!("{target}/alef")).unwrap(); fa.write_all(&alef[..]).unwrap(); let mut fb = File::create(&format!("{target}/bet")).unwrap(); fb.write_all(&bet[..]).unwrap(); let _ = fa; let _ = fb; let output = Command::new("patch") .arg("-p0") .arg(&format!("{target}/alef")) .stdin(File::open(&format!("{target}/ab.diff")).unwrap()) .output() .unwrap(); assert!(output.status.success(), "{output:?}"); //println!("{}", String::from_utf8_lossy(&output.stdout)); //println!("{}", String::from_utf8_lossy(&output.stderr)); let alef = fs::read(&format!("{target}/alef")).unwrap(); assert_eq!(alef, bet); } } } } } } } #[test] fn test_permutations_missing_line_ending() { let target = "target/normal-diff/"; // test all possible six-line files with missing newlines. let _ = std::fs::create_dir(target); for &a in &[0, 1, 2] { for &b in &[0, 1, 2] { for &c in &[0, 1, 2] { for &d in &[0, 1, 2] { for &e in &[0, 1, 2] { for &f in &[0, 1, 2] { for &g in &[0, 1, 2] { use std::fs::{self, File}; use std::io::Write; use std::process::Command; let mut alef = Vec::new(); let mut bet = Vec::new(); alef.write_all(if a == 0 { b"a\n" } else { b"b\n" }) .unwrap(); if a != 2 { bet.write_all(b"b\n").unwrap(); } alef.write_all(if b == 0 { b"c\n" } else { b"d\n" }) .unwrap(); if b != 2 { bet.write_all(b"d\n").unwrap(); } alef.write_all(if c == 0 { b"e\n" } else { b"f\n" }) .unwrap(); if c != 2 { bet.write_all(b"f\n").unwrap(); } alef.write_all(if d == 0 { b"g\n" } else { b"h\n" }) .unwrap(); if d != 2 { bet.write_all(b"h\n").unwrap(); } alef.write_all(if e == 0 { b"i\n" } else { b"j\n" }) .unwrap(); if e != 2 { bet.write_all(b"j\n").unwrap(); } alef.write_all(if f == 0 { b"k\n" } else { b"l\n" }) .unwrap(); if f != 2 { bet.write_all(b"l\n").unwrap(); } match g { 0 => { alef.pop(); } 1 => { bet.pop(); } 2 => { alef.pop(); bet.pop(); } _ => unreachable!(), } // This test diff is intentionally reversed. // We want it to turn the alef into bet. let diff = diff(&alef, &bet, &Params::default()); File::create(&format!("{target}/abn.diff")) .unwrap() .write_all(&diff) .unwrap(); let mut fa = File::create(&format!("{target}/alefn")).unwrap(); fa.write_all(&alef[..]).unwrap(); let mut fb = File::create(&format!("{target}/betn")).unwrap(); fb.write_all(&bet[..]).unwrap(); let _ = fa; let _ = fb; let output = Command::new("patch") .arg("-p0") .arg("--normal") .arg(&format!("{target}/alefn")) .stdin(File::open(&format!("{target}/abn.diff")).unwrap()) .output() .unwrap(); assert!(output.status.success(), "{output:?}"); //println!("{}", String::from_utf8_lossy(&output.stdout)); //println!("{}", String::from_utf8_lossy(&output.stderr)); let alef = fs::read(&format!("{target}/alefn")).unwrap(); assert_eq!(alef, bet); } } } } } } } } #[test] fn test_permutations_empty_lines() { let target = "target/normal-diff/"; // test all possible six-line files with missing newlines. let _ = std::fs::create_dir(target); for &a in &[0, 1, 2] { for &b in &[0, 1, 2] { for &c in &[0, 1, 2] { for &d in &[0, 1, 2] { for &e in &[0, 1, 2] { for &f in &[0, 1, 2] { use std::fs::{self, File}; use std::io::Write; use std::process::Command; let mut alef = Vec::new(); let mut bet = Vec::new(); alef.write_all(if a == 0 { b"\n" } else { b"b\n" }).unwrap(); if a != 2 { bet.write_all(b"b\n").unwrap(); } alef.write_all(if b == 0 { b"\n" } else { b"d\n" }).unwrap(); if b != 2 { bet.write_all(b"d\n").unwrap(); } alef.write_all(if c == 0 { b"\n" } else { b"f\n" }).unwrap(); if c != 2 { bet.write_all(b"f\n").unwrap(); } alef.write_all(if d == 0 { b"\n" } else { b"h\n" }).unwrap(); if d != 2 { bet.write_all(b"h\n").unwrap(); } alef.write_all(if e == 0 { b"\n" } else { b"j\n" }).unwrap(); if e != 2 { bet.write_all(b"j\n").unwrap(); } alef.write_all(if f == 0 { b"\n" } else { b"l\n" }).unwrap(); if f != 2 { bet.write_all(b"l\n").unwrap(); } // This test diff is intentionally reversed. // We want it to turn the alef into bet. let diff = diff(&alef, &bet, &Params::default()); File::create(&format!("{target}/ab_.diff")) .unwrap() .write_all(&diff) .unwrap(); let mut fa = File::create(&format!("{target}/alef_")).unwrap(); fa.write_all(&alef[..]).unwrap(); let mut fb = File::create(&format!("{target}/bet_")).unwrap(); fb.write_all(&bet[..]).unwrap(); let _ = fa; let _ = fb; let output = Command::new("patch") .arg("-p0") .arg(&format!("{target}/alef_")) .stdin(File::open(&format!("{target}/ab_.diff")).unwrap()) .output() .unwrap(); assert!(output.status.success(), "{output:?}"); //println!("{}", String::from_utf8_lossy(&output.stdout)); //println!("{}", String::from_utf8_lossy(&output.stderr)); let alef = fs::read(&format!("{target}/alef_")).unwrap(); assert_eq!(alef, bet); } } } } } } } #[test] fn test_permutations_reverse() { let target = "target/normal-diff/"; // test all possible six-line files. let _ = std::fs::create_dir(target); for &a in &[0, 1, 2] { for &b in &[0, 1, 2] { for &c in &[0, 1, 2] { for &d in &[0, 1, 2] { for &e in &[0, 1, 2] { for &f in &[0, 1, 2] { use std::fs::{self, File}; use std::io::Write; use std::process::Command; let mut alef = Vec::new(); let mut bet = Vec::new(); alef.write_all(if a == 0 { b"a\n" } else { b"f\n" }) .unwrap(); if a != 2 { bet.write_all(b"a\n").unwrap(); } alef.write_all(if b == 0 { b"b\n" } else { b"e\n" }) .unwrap(); if b != 2 { bet.write_all(b"b\n").unwrap(); } alef.write_all(if c == 0 { b"c\n" } else { b"d\n" }) .unwrap(); if c != 2 { bet.write_all(b"c\n").unwrap(); } alef.write_all(if d == 0 { b"d\n" } else { b"c\n" }) .unwrap(); if d != 2 { bet.write_all(b"d\n").unwrap(); } alef.write_all(if e == 0 { b"e\n" } else { b"b\n" }) .unwrap(); if e != 2 { bet.write_all(b"e\n").unwrap(); } alef.write_all(if f == 0 { b"f\n" } else { b"a\n" }) .unwrap(); if f != 2 { bet.write_all(b"f\n").unwrap(); } // This test diff is intentionally reversed. // We want it to turn the alef into bet. let diff = diff(&alef, &bet, &Params::default()); File::create(&format!("{target}/abr.diff")) .unwrap() .write_all(&diff) .unwrap(); let mut fa = File::create(&format!("{target}/alefr")).unwrap(); fa.write_all(&alef[..]).unwrap(); let mut fb = File::create(&format!("{target}/betr")).unwrap(); fb.write_all(&bet[..]).unwrap(); let _ = fa; let _ = fb; let output = Command::new("patch") .arg("-p0") .arg(&format!("{target}/alefr")) .stdin(File::open(&format!("{target}/abr.diff")).unwrap()) .output() .unwrap(); assert!(output.status.success(), "{output:?}"); //println!("{}", String::from_utf8_lossy(&output.stdout)); //println!("{}", String::from_utf8_lossy(&output.stderr)); let alef = fs::read(&format!("{target}/alefr")).unwrap(); assert_eq!(alef, bet); } } } } } } } #[test] fn test_stop_early() { let from = ["a", "b", "c"].join("\n"); let to = ["a", "d", "c"].join("\n"); let diff_full = diff(from.as_bytes(), to.as_bytes(), &Params::default()); let expected_full = ["2c2", "< b", "---", "> d", ""].join("\n"); assert_eq!(diff_full, expected_full.as_bytes()); let diff_brief = diff( from.as_bytes(), to.as_bytes(), &Params { brief: true, ..Default::default() }, ); let expected_brief = "\0".as_bytes(); assert_eq!(diff_brief, expected_brief); let nodiff_full = diff(from.as_bytes(), from.as_bytes(), &Params::default()); assert!(nodiff_full.is_empty()); let nodiff_brief = diff( from.as_bytes(), from.as_bytes(), &Params { brief: true, ..Default::default() }, ); assert!(nodiff_brief.is_empty()); } } diffutils-0.4.1/src/params.rs000064400000000000000000000553021046102023000142440ustar 00000000000000use std::ffi::OsString; use std::path::PathBuf; use regex::Regex; #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] pub enum Format { #[default] Normal, Unified, Context, Ed, } #[derive(Clone, Debug, Eq, PartialEq)] pub struct Params { pub from: OsString, pub to: OsString, pub format: Format, pub context_count: usize, pub report_identical_files: bool, pub brief: bool, pub expand_tabs: bool, pub tabsize: usize, } impl Default for Params { fn default() -> Self { Self { from: OsString::default(), to: OsString::default(), format: Format::default(), context_count: 3, report_identical_files: false, brief: false, expand_tabs: false, tabsize: 8, } } } pub fn parse_params>(opts: I) -> Result { let mut opts = opts.into_iter().peekable(); // parse CLI let Some(exe) = opts.next() else { return Err("Usage: ".to_string()); }; let mut params = Params::default(); let mut from = None; let mut to = None; let mut format = None; let mut context = None; let tabsize_re = Regex::new(r"^--tabsize=(?\d+)$").unwrap(); while let Some(param) = opts.next() { let next_param = opts.peek(); if param == "--" { break; } if param == "-" { if from.is_none() { from = Some(param); } else if to.is_none() { to = Some(param); } else { return Err(format!("Usage: {} ", exe.to_string_lossy())); } continue; } if param == "-s" || param == "--report-identical-files" { params.report_identical_files = true; continue; } if param == "-q" || param == "--brief" { params.brief = true; continue; } if param == "-t" || param == "--expand-tabs" { params.expand_tabs = true; continue; } if param == "--normal" { if format.is_some() && format != Some(Format::Normal) { return Err("Conflicting output style options".to_string()); } format = Some(Format::Normal); continue; } if param == "-e" || param == "--ed" { if format.is_some() && format != Some(Format::Ed) { return Err("Conflicting output style options".to_string()); } format = Some(Format::Ed); continue; } if tabsize_re.is_match(param.to_string_lossy().as_ref()) { // Because param matches the regular expression, // it is safe to assume it is valid UTF-8. let param = param.into_string().unwrap(); let tabsize_str = tabsize_re .captures(param.as_str()) .unwrap() .name("num") .unwrap() .as_str(); params.tabsize = match tabsize_str.parse::() { Ok(num) => num, Err(_) => return Err(format!("invalid tabsize «{tabsize_str}»")), }; continue; } match match_context_diff_params(¶m, next_param, format) { Ok(DiffStyleMatch { is_match, context_count, next_param_consumed, }) => { if is_match { format = Some(Format::Context); if context_count.is_some() { context = context_count; } if next_param_consumed { opts.next(); } continue; } } Err(error) => return Err(error), } match match_unified_diff_params(¶m, next_param, format) { Ok(DiffStyleMatch { is_match, context_count, next_param_consumed, }) => { if is_match { format = Some(Format::Unified); if context_count.is_some() { context = context_count; } if next_param_consumed { opts.next(); } continue; } } Err(error) => return Err(error), } if param.to_string_lossy().starts_with('-') { return Err(format!("Unknown option: {:?}", param)); } if from.is_none() { from = Some(param); } else if to.is_none() { to = Some(param); } else { return Err(format!("Usage: {} ", exe.to_string_lossy())); } } params.from = if let Some(from) = from { from } else if let Some(param) = opts.next() { param } else { return Err(format!("Usage: {} ", exe.to_string_lossy())); }; params.to = if let Some(to) = to { to } else if let Some(param) = opts.next() { param } else { return Err(format!("Usage: {} ", exe.to_string_lossy())); }; // diff DIRECTORY FILE => diff DIRECTORY/FILE FILE // diff FILE DIRECTORY => diff FILE DIRECTORY/FILE let mut from_path: PathBuf = PathBuf::from(¶ms.from); let mut to_path: PathBuf = PathBuf::from(¶ms.to); if from_path.is_dir() && to_path.is_file() { from_path.push(to_path.file_name().unwrap()); params.from = from_path.into_os_string(); } else if from_path.is_file() && to_path.is_dir() { to_path.push(from_path.file_name().unwrap()); params.to = to_path.into_os_string(); } params.format = format.unwrap_or(Format::default()); if let Some(context_count) = context { params.context_count = context_count; } Ok(params) } struct DiffStyleMatch { is_match: bool, context_count: Option, next_param_consumed: bool, } fn match_context_diff_params( param: &OsString, next_param: Option<&OsString>, format: Option, ) -> Result { const CONTEXT_RE: &str = r"^(-[cC](?\d*)|--context(=(?\d*))?|-(?\d+)c)$"; let regex = Regex::new(CONTEXT_RE).unwrap(); let is_match = regex.is_match(param.to_string_lossy().as_ref()); let mut context_count = None; let mut next_param_consumed = false; if is_match { if format.is_some() && format != Some(Format::Context) { return Err("Conflicting output style options".to_string()); } let captures = regex.captures(param.to_str().unwrap()).unwrap(); let num = captures .name("num1") .or(captures.name("num2")) .or(captures.name("num3")); if let Some(numvalue) = num { if !numvalue.as_str().is_empty() { context_count = Some(numvalue.as_str().parse::().unwrap()); } } if param == "-C" && next_param.is_some() { match next_param.unwrap().to_string_lossy().parse::() { Ok(context_size) => { context_count = Some(context_size); next_param_consumed = true; } Err(_) => { return Err(format!( "invalid context length '{}'", next_param.unwrap().to_string_lossy() )) } } } } Ok(DiffStyleMatch { is_match, context_count, next_param_consumed, }) } fn match_unified_diff_params( param: &OsString, next_param: Option<&OsString>, format: Option, ) -> Result { const UNIFIED_RE: &str = r"^(-[uU](?\d*)|--unified(=(?\d*))?|-(?\d+)u)$"; let regex = Regex::new(UNIFIED_RE).unwrap(); let is_match = regex.is_match(param.to_string_lossy().as_ref()); let mut context_count = None; let mut next_param_consumed = false; if is_match { if format.is_some() && format != Some(Format::Unified) { return Err("Conflicting output style options".to_string()); } let captures = regex.captures(param.to_str().unwrap()).unwrap(); let num = captures .name("num1") .or(captures.name("num2")) .or(captures.name("num3")); if let Some(numvalue) = num { if !numvalue.as_str().is_empty() { context_count = Some(numvalue.as_str().parse::().unwrap()); } } if param == "-U" && next_param.is_some() { match next_param.unwrap().to_string_lossy().parse::() { Ok(context_size) => { context_count = Some(context_size); next_param_consumed = true; } Err(_) => { return Err(format!( "invalid context length '{}'", next_param.unwrap().to_string_lossy() )) } } } } Ok(DiffStyleMatch { is_match, context_count, next_param_consumed, }) } #[cfg(test)] mod tests { use super::*; fn os(s: &str) -> OsString { OsString::from(s) } #[test] fn basics() { assert_eq!( Ok(Params { from: os("foo"), to: os("bar"), ..Default::default() }), parse_params([os("diff"), os("foo"), os("bar")].iter().cloned()) ); assert_eq!( Ok(Params { from: os("foo"), to: os("bar"), ..Default::default() }), parse_params( [os("diff"), os("--normal"), os("foo"), os("bar")] .iter() .cloned() ) ); } #[test] fn basics_ed() { for arg in ["-e", "--ed"] { assert_eq!( Ok(Params { from: os("foo"), to: os("bar"), format: Format::Ed, ..Default::default() }), parse_params([os("diff"), os(arg), os("foo"), os("bar")].iter().cloned()) ); } } #[test] fn context_valid() { for args in [vec!["-c"], vec!["--context"], vec!["--context="]] { let mut params = vec!["diff"]; params.extend(args); params.extend(["foo", "bar"]); assert_eq!( Ok(Params { from: os("foo"), to: os("bar"), format: Format::Context, ..Default::default() }), parse_params(params.iter().map(|x| os(x))) ); } for args in [ vec!["-c42"], vec!["-C42"], vec!["-C", "42"], vec!["--context=42"], vec!["-42c"], ] { let mut params = vec!["diff"]; params.extend(args); params.extend(["foo", "bar"]); assert_eq!( Ok(Params { from: os("foo"), to: os("bar"), format: Format::Context, context_count: 42, ..Default::default() }), parse_params(params.iter().map(|x| os(x))) ); } } #[test] fn context_invalid() { for args in [ vec!["-c", "42"], vec!["-c=42"], vec!["-c="], vec!["-C"], vec!["-C=42"], vec!["-C="], vec!["--context42"], vec!["--context", "42"], vec!["-42C"], ] { let mut params = vec!["diff"]; params.extend(args); params.extend(["foo", "bar"]); assert!(parse_params(params.iter().map(|x| os(x))).is_err()); } } #[test] fn unified_valid() { for args in [vec!["-u"], vec!["--unified"], vec!["--unified="]] { let mut params = vec!["diff"]; params.extend(args); params.extend(["foo", "bar"]); assert_eq!( Ok(Params { from: os("foo"), to: os("bar"), format: Format::Unified, ..Default::default() }), parse_params(params.iter().map(|x| os(x))) ); } for args in [ vec!["-u42"], vec!["-U42"], vec!["-U", "42"], vec!["--unified=42"], vec!["-42u"], ] { let mut params = vec!["diff"]; params.extend(args); params.extend(["foo", "bar"]); assert_eq!( Ok(Params { from: os("foo"), to: os("bar"), format: Format::Unified, context_count: 42, ..Default::default() }), parse_params(params.iter().map(|x| os(x))) ); } } #[test] fn unified_invalid() { for args in [ vec!["-u", "42"], vec!["-u=42"], vec!["-u="], vec!["-U"], vec!["-U=42"], vec!["-U="], vec!["--unified42"], vec!["--unified", "42"], vec!["-42U"], ] { let mut params = vec!["diff"]; params.extend(args); params.extend(["foo", "bar"]); assert!(parse_params(params.iter().map(|x| os(x))).is_err()); } } #[test] fn context_count() { assert_eq!( Ok(Params { from: os("foo"), to: os("bar"), format: Format::Unified, context_count: 54, ..Default::default() }), parse_params( [os("diff"), os("-u54"), os("foo"), os("bar")] .iter() .cloned() ) ); assert_eq!( Ok(Params { from: os("foo"), to: os("bar"), format: Format::Unified, context_count: 54, ..Default::default() }), parse_params( [os("diff"), os("-U54"), os("foo"), os("bar")] .iter() .cloned() ) ); assert_eq!( Ok(Params { from: os("foo"), to: os("bar"), format: Format::Unified, context_count: 54, ..Default::default() }), parse_params( [os("diff"), os("-U"), os("54"), os("foo"), os("bar")] .iter() .cloned() ) ); assert_eq!( Ok(Params { from: os("foo"), to: os("bar"), format: Format::Context, context_count: 54, ..Default::default() }), parse_params( [os("diff"), os("-c54"), os("foo"), os("bar")] .iter() .cloned() ) ); } #[test] fn report_identical_files() { assert_eq!( Ok(Params { from: os("foo"), to: os("bar"), ..Default::default() }), parse_params([os("diff"), os("foo"), os("bar")].iter().cloned()) ); assert_eq!( Ok(Params { from: os("foo"), to: os("bar"), report_identical_files: true, ..Default::default() }), parse_params([os("diff"), os("-s"), os("foo"), os("bar")].iter().cloned()) ); assert_eq!( Ok(Params { from: os("foo"), to: os("bar"), report_identical_files: true, ..Default::default() }), parse_params( [ os("diff"), os("--report-identical-files"), os("foo"), os("bar"), ] .iter() .cloned() ) ); } #[test] fn brief() { assert_eq!( Ok(Params { from: os("foo"), to: os("bar"), ..Default::default() }), parse_params([os("diff"), os("foo"), os("bar")].iter().cloned()) ); assert_eq!( Ok(Params { from: os("foo"), to: os("bar"), brief: true, ..Default::default() }), parse_params([os("diff"), os("-q"), os("foo"), os("bar")].iter().cloned()) ); assert_eq!( Ok(Params { from: os("foo"), to: os("bar"), brief: true, ..Default::default() }), parse_params( [os("diff"), os("--brief"), os("foo"), os("bar"),] .iter() .cloned() ) ); } #[test] fn expand_tabs() { assert_eq!( Ok(Params { from: os("foo"), to: os("bar"), ..Default::default() }), parse_params([os("diff"), os("foo"), os("bar")].iter().cloned()) ); for option in ["-t", "--expand-tabs"] { assert_eq!( Ok(Params { from: os("foo"), to: os("bar"), expand_tabs: true, ..Default::default() }), parse_params( [os("diff"), os(option), os("foo"), os("bar")] .iter() .cloned() ) ); } } #[test] fn tabsize() { assert_eq!( Ok(Params { from: os("foo"), to: os("bar"), ..Default::default() }), parse_params([os("diff"), os("foo"), os("bar")].iter().cloned()) ); assert_eq!( Ok(Params { from: os("foo"), to: os("bar"), tabsize: 0, ..Default::default() }), parse_params( [os("diff"), os("--tabsize=0"), os("foo"), os("bar")] .iter() .cloned() ) ); assert_eq!( Ok(Params { from: os("foo"), to: os("bar"), tabsize: 42, ..Default::default() }), parse_params( [os("diff"), os("--tabsize=42"), os("foo"), os("bar")] .iter() .cloned() ) ); assert!(parse_params( [os("diff"), os("--tabsize"), os("foo"), os("bar")] .iter() .cloned() ) .is_err()); assert!(parse_params( [os("diff"), os("--tabsize="), os("foo"), os("bar")] .iter() .cloned() ) .is_err()); assert!(parse_params( [os("diff"), os("--tabsize=r2"), os("foo"), os("bar")] .iter() .cloned() ) .is_err()); assert!(parse_params( [os("diff"), os("--tabsize=-1"), os("foo"), os("bar")] .iter() .cloned() ) .is_err()); assert!(parse_params( [os("diff"), os("--tabsize=r2"), os("foo"), os("bar")] .iter() .cloned() ) .is_err()); assert!(parse_params( [ os("diff"), os("--tabsize=92233720368547758088"), os("foo"), os("bar") ] .iter() .cloned() ) .is_err()); } #[test] fn double_dash() { assert_eq!( Ok(Params { from: os("-g"), to: os("-h"), ..Default::default() }), parse_params([os("diff"), os("--"), os("-g"), os("-h")].iter().cloned()) ); } #[test] fn default_to_stdin() { assert_eq!( Ok(Params { from: os("foo"), to: os("-"), ..Default::default() }), parse_params([os("diff"), os("foo"), os("-")].iter().cloned()) ); assert_eq!( Ok(Params { from: os("-"), to: os("bar"), ..Default::default() }), parse_params([os("diff"), os("-"), os("bar")].iter().cloned()) ); assert_eq!( Ok(Params { from: os("-"), to: os("-"), ..Default::default() }), parse_params([os("diff"), os("-"), os("-")].iter().cloned()) ); assert!(parse_params([os("diff"), os("foo"), os("bar"), os("-")].iter().cloned()).is_err()); assert!(parse_params([os("diff"), os("-"), os("-"), os("-")].iter().cloned()).is_err()); } #[test] fn missing_arguments() { assert!(parse_params([os("diff")].iter().cloned()).is_err()); assert!(parse_params([os("diff"), os("foo")].iter().cloned()).is_err()); } #[test] fn unknown_argument() { assert!( parse_params([os("diff"), os("-g"), os("foo"), os("bar")].iter().cloned()).is_err() ); assert!(parse_params([os("diff"), os("-g"), os("bar")].iter().cloned()).is_err()); assert!(parse_params([os("diff"), os("-g")].iter().cloned()).is_err()); } #[test] fn empty() { assert!(parse_params([].iter().cloned()).is_err()); } #[test] fn conflicting_output_styles() { for (arg1, arg2) in [ ("-u", "-c"), ("-u", "-e"), ("-c", "-u"), ("-c", "-U42"), ("-u", "--normal"), ("--normal", "-e"), ("--context", "--normal"), ] { assert!(parse_params( [os("diff"), os(arg1), os(arg2), os("foo"), os("bar")] .iter() .cloned() ) .is_err()); } } } diffutils-0.4.1/src/unified_diff.rs000064400000000000000000001246541046102023000154030ustar 00000000000000// This file is part of the uutils diffutils package. // // For the full copyright and license information, please view the LICENSE-* // files that was distributed with this source code. use std::collections::VecDeque; use std::io::Write; use crate::params::Params; use crate::utils::do_write_line; use crate::utils::get_modification_time; #[derive(Debug, PartialEq)] pub enum DiffLine { Context(Vec), Expected(Vec), Actual(Vec), MissingNL, } #[derive(Debug, PartialEq)] struct Mismatch { pub line_number_expected: u32, pub line_number_actual: u32, pub lines: Vec, } impl Mismatch { fn new(line_number_expected: u32, line_number_actual: u32) -> Mismatch { Mismatch { line_number_expected, line_number_actual, lines: Vec::new(), } } } // Produces a diff between the expected output and actual output. fn make_diff( expected: &[u8], actual: &[u8], context_size: usize, stop_early: bool, ) -> Vec { let mut line_number_expected = 1; let mut line_number_actual = 1; let mut context_queue: VecDeque<&[u8]> = VecDeque::with_capacity(context_size); let mut lines_since_mismatch = context_size + 1; let mut results = Vec::new(); let mut mismatch = Mismatch::new(0, 0); let mut expected_lines: Vec<&[u8]> = expected.split(|&c| c == b'\n').collect(); let mut actual_lines: Vec<&[u8]> = actual.split(|&c| c == b'\n').collect(); debug_assert_eq!(b"".split(|&c| c == b'\n').count(), 1); // ^ means that underflow here is impossible let expected_lines_count = expected_lines.len() as u32 - 1; let actual_lines_count = actual_lines.len() as u32 - 1; if expected_lines.last() == Some(&&b""[..]) { expected_lines.pop(); } if actual_lines.last() == Some(&&b""[..]) { actual_lines.pop(); } for result in diff::slice(&expected_lines, &actual_lines) { match result { diff::Result::Left(str) => { if lines_since_mismatch >= context_size && lines_since_mismatch > 0 { results.push(mismatch); mismatch = Mismatch::new( line_number_expected - context_queue.len() as u32, line_number_actual - context_queue.len() as u32, ); } while let Some(line) = context_queue.pop_front() { mismatch.lines.push(DiffLine::Context(line.to_vec())); } if mismatch.lines.last() == Some(&DiffLine::MissingNL) { mismatch.lines.pop(); match mismatch.lines.pop() { Some(DiffLine::Actual(res)) => { // We have to make sure that Actual (the + lines) // always come after Expected (the - lines) mismatch.lines.push(DiffLine::Expected(str.to_vec())); if line_number_expected > expected_lines_count { mismatch.lines.push(DiffLine::MissingNL); } mismatch.lines.push(DiffLine::Actual(res)); mismatch.lines.push(DiffLine::MissingNL); } _ => unreachable!("unterminated Left and Common lines shouldn't be followed by more Left lines"), } } else { mismatch.lines.push(DiffLine::Expected(str.to_vec())); if line_number_expected > expected_lines_count { mismatch.lines.push(DiffLine::MissingNL); } } line_number_expected += 1; lines_since_mismatch = 0; } diff::Result::Right(str) => { if lines_since_mismatch >= context_size && lines_since_mismatch > 0 { results.push(mismatch); mismatch = Mismatch::new( line_number_expected - context_queue.len() as u32, line_number_actual - context_queue.len() as u32, ); } while let Some(line) = context_queue.pop_front() { debug_assert!(mismatch.lines.last() != Some(&DiffLine::MissingNL)); mismatch.lines.push(DiffLine::Context(line.to_vec())); } mismatch.lines.push(DiffLine::Actual(str.to_vec())); if line_number_actual > actual_lines_count { mismatch.lines.push(DiffLine::MissingNL); } line_number_actual += 1; lines_since_mismatch = 0; } diff::Result::Both(str, _) => { // if one of them is missing a newline and the other isn't, then they don't actually match if (line_number_actual > actual_lines_count) && (line_number_expected > expected_lines_count) { if context_queue.len() < context_size { while let Some(line) = context_queue.pop_front() { debug_assert!(mismatch.lines.last() != Some(&DiffLine::MissingNL)); mismatch.lines.push(DiffLine::Context(line.to_vec())); } if lines_since_mismatch < context_size { mismatch.lines.push(DiffLine::Context(str.to_vec())); mismatch.lines.push(DiffLine::MissingNL); } } lines_since_mismatch = 0; } else if line_number_actual > actual_lines_count { if lines_since_mismatch >= context_size && lines_since_mismatch > 0 { results.push(mismatch); mismatch = Mismatch::new( line_number_expected - context_queue.len() as u32, line_number_actual - context_queue.len() as u32, ); } while let Some(line) = context_queue.pop_front() { debug_assert!(mismatch.lines.last() != Some(&DiffLine::MissingNL)); mismatch.lines.push(DiffLine::Context(line.to_vec())); } mismatch.lines.push(DiffLine::Expected(str.to_vec())); mismatch.lines.push(DiffLine::Actual(str.to_vec())); mismatch.lines.push(DiffLine::MissingNL); lines_since_mismatch = 0; } else if line_number_expected > expected_lines_count { if lines_since_mismatch >= context_size && lines_since_mismatch > 0 { results.push(mismatch); mismatch = Mismatch::new( line_number_expected - context_queue.len() as u32, line_number_actual - context_queue.len() as u32, ); } while let Some(line) = context_queue.pop_front() { debug_assert!(mismatch.lines.last() != Some(&DiffLine::MissingNL)); mismatch.lines.push(DiffLine::Context(line.to_vec())); } mismatch.lines.push(DiffLine::Expected(str.to_vec())); mismatch.lines.push(DiffLine::MissingNL); mismatch.lines.push(DiffLine::Actual(str.to_vec())); lines_since_mismatch = 0; } else { debug_assert!(context_queue.len() <= context_size); if context_queue.len() >= context_size { let _ = context_queue.pop_front(); } if lines_since_mismatch < context_size { mismatch.lines.push(DiffLine::Context(str.to_vec())); } else if context_size > 0 { context_queue.push_back(str); } lines_since_mismatch += 1; } line_number_expected += 1; line_number_actual += 1; } } if stop_early && !results.is_empty() { // Optimization: stop analyzing the files as soon as there are any differences return results; } } results.push(mismatch); results.remove(0); if results.is_empty() && expected_lines_count != actual_lines_count { let mut mismatch = Mismatch::new(expected_lines.len() as u32, actual_lines.len() as u32); // empty diff and only expected lines has a missing line at end if expected_lines_count != expected_lines.len() as u32 { mismatch.lines.push(DiffLine::Expected( expected_lines .pop() .expect("can't be empty; produced by split()") .to_vec(), )); mismatch.lines.push(DiffLine::MissingNL); mismatch.lines.push(DiffLine::Actual( actual_lines .pop() .expect("can't be empty; produced by split()") .to_vec(), )); results.push(mismatch); } else if actual_lines_count != actual_lines.len() as u32 { mismatch.lines.push(DiffLine::Expected( expected_lines .pop() .expect("can't be empty; produced by split()") .to_vec(), )); mismatch.lines.push(DiffLine::Actual( actual_lines .pop() .expect("can't be empty; produced by split()") .to_vec(), )); mismatch.lines.push(DiffLine::MissingNL); results.push(mismatch); } } results } #[must_use] pub fn diff(expected: &[u8], actual: &[u8], params: &Params) -> Vec { let from_modified_time = get_modification_time(¶ms.from.to_string_lossy()); let to_modified_time = get_modification_time(¶ms.to.to_string_lossy()); let mut output = format!( "--- {0}\t{1}\n+++ {2}\t{3}\n", params.from.to_string_lossy(), from_modified_time, params.to.to_string_lossy(), to_modified_time ) .into_bytes(); let diff_results = make_diff(expected, actual, params.context_count, params.brief); if diff_results.is_empty() { return Vec::new(); } if params.brief { return output; } for result in diff_results { let mut line_number_expected = result.line_number_expected; let mut line_number_actual = result.line_number_actual; let mut expected_count = 0; let mut actual_count = 0; for line in &result.lines { match line { DiffLine::Expected(_) => { expected_count += 1; } DiffLine::Context(_) => { expected_count += 1; actual_count += 1; } DiffLine::Actual(_) => { actual_count += 1; } DiffLine::MissingNL => {} } } // Let's imagine this diff file // // --- a/something // +++ b/something // @@ -2,0 +3,1 @@ // + x // // In the unified diff format as implemented by GNU diff and patch, // this is an instruction to insert the x *after* the preexisting line 2, // not before. You can demonstrate it this way: // // $ echo -ne '--- a/something\t\n+++ b/something\t\n@@ -2,0 +3,1 @@\n+ x\n' > diff // $ echo -ne 'a\nb\nc\nd\n' > something // $ patch -p1 < diff // patching file something // $ cat something // a // b // x // c // d // // Notice how the x winds up at line 3, not line 2. This requires contortions to // work with our diffing algorithm, which keeps track of the "intended destination line", // not a line that things are supposed to be placed after. It's changing the first number, // not the second, that actually affects where the x goes. // // # change the first number from 2 to 3, and now the x is on line 4 (it's placed after line 3) // $ echo -ne '--- a/something\t\n+++ b/something\t\n@@ -3,0 +3,1 @@\n+ x\n' > diff // $ echo -ne 'a\nb\nc\nd\n' > something // $ patch -p1 < diff // patching file something // $ cat something // a // b // c // x // d // # change the third number from 3 to 1000, and it's obvious that it's the first number that's // # actually being read // $ echo -ne '--- a/something\t\n+++ b/something\t\n@@ -2,0 +1000,1 @@\n+ x\n' > diff // $ echo -ne 'a\nb\nc\nd\n' > something // $ patch -p1 < diff // patching file something // $ cat something // a // b // x // c // d // // Now watch what happens if I add a context line: // // $ echo -ne '--- a/something\t\n+++ b/something\t\n@@ -2,1 +3,2 @@\n+ x\n c\n' > diff // $ echo -ne 'a\nb\nc\nd\n' > something // $ patch -p1 < diff // patching file something // Hunk #1 succeeded at 3 (offset 1 line). // // It technically "succeeded", but this is a warning. We want to produce clean diffs. // Now that I have a context line, I'm supposed to say what line it's actually on, which is the // line that the x will wind up on, and not the line immediately before. // // $ echo -ne '--- a/something\t\n+++ b/something\t\n@@ -3,1 +3,2 @@\n+ x\n c\n' > diff // $ echo -ne 'a\nb\nc\nd\n' > something // $ patch -p1 < diff // patching file something // $ cat something // a // b // x // c // d // // I made this comment because this stuff is not obvious from GNU's // documentation on the format at all. if expected_count == 0 { line_number_expected -= 1; } if actual_count == 0 { line_number_actual -= 1; } let exp_ct = if expected_count == 1 { String::new() } else { format!(",{expected_count}") }; let act_ct = if actual_count == 1 { String::new() } else { format!(",{actual_count}") }; writeln!( output, "@@ -{line_number_expected}{exp_ct} +{line_number_actual}{act_ct} @@" ) .expect("write to Vec is infallible"); for line in result.lines { match line { DiffLine::Expected(e) => { write!(output, "-").expect("write to Vec is infallible"); do_write_line(&mut output, &e, params.expand_tabs, params.tabsize) .expect("write to Vec is infallible"); writeln!(output).unwrap(); } DiffLine::Context(c) => { write!(output, " ").expect("write to Vec is infallible"); do_write_line(&mut output, &c, params.expand_tabs, params.tabsize) .expect("write to Vec is infallible"); writeln!(output).unwrap(); } DiffLine::Actual(r) => { write!(output, "+",).expect("write to Vec is infallible"); do_write_line(&mut output, &r, params.expand_tabs, params.tabsize) .expect("write to Vec is infallible"); writeln!(output).unwrap(); } DiffLine::MissingNL => { writeln!(output, r"\ No newline at end of file") .expect("write to Vec is infallible"); } } } } output } #[cfg(test)] mod tests { use super::*; use pretty_assertions::assert_eq; #[test] fn test_permutations() { let target = "target/unified-diff/"; // test all possible six-line files. let _ = std::fs::create_dir(target); for &a in &[0, 1, 2] { for &b in &[0, 1, 2] { for &c in &[0, 1, 2] { for &d in &[0, 1, 2] { for &e in &[0, 1, 2] { for &f in &[0, 1, 2] { use std::fs::{self, File}; use std::io::Write; use std::process::Command; let mut alef = Vec::new(); let mut bet = Vec::new(); alef.write_all(if a == 0 { b"a\n" } else { b"b\n" }) .unwrap(); if a != 2 { bet.write_all(b"b\n").unwrap(); } alef.write_all(if b == 0 { b"c\n" } else { b"d\n" }) .unwrap(); if b != 2 { bet.write_all(b"d\n").unwrap(); } alef.write_all(if c == 0 { b"e\n" } else { b"f\n" }) .unwrap(); if c != 2 { bet.write_all(b"f\n").unwrap(); } alef.write_all(if d == 0 { b"g\n" } else { b"h\n" }) .unwrap(); if d != 2 { bet.write_all(b"h\n").unwrap(); } alef.write_all(if e == 0 { b"i\n" } else { b"j\n" }) .unwrap(); if e != 2 { bet.write_all(b"j\n").unwrap(); } alef.write_all(if f == 0 { b"k\n" } else { b"l\n" }) .unwrap(); if f != 2 { bet.write_all(b"l\n").unwrap(); } // This test diff is intentionally reversed. // We want it to turn the alef into bet. let diff = diff( &alef, &bet, &Params { from: "a/alef".into(), to: (&format!("{target}/alef")).into(), context_count: 2, ..Default::default() }, ); File::create(&format!("{target}/ab.diff")) .unwrap() .write_all(&diff) .unwrap(); let mut fa = File::create(&format!("{target}/alef")).unwrap(); fa.write_all(&alef[..]).unwrap(); let mut fb = File::create(&format!("{target}/bet")).unwrap(); fb.write_all(&bet[..]).unwrap(); let _ = fa; let _ = fb; println!( "diff: {:?}", String::from_utf8(diff.clone()) .unwrap_or_else(|_| String::from("[Invalid UTF-8]")) ); println!( "alef: {:?}", String::from_utf8(alef.clone()) .unwrap_or_else(|_| String::from("[Invalid UTF-8]")) ); println!( "bet: {:?}", String::from_utf8(bet.clone()) .unwrap_or_else(|_| String::from("[Invalid UTF-8]")) ); let output = Command::new("patch") .arg("-p0") .stdin(File::open(&format!("{target}/ab.diff")).unwrap()) .output() .unwrap(); println!("{}", String::from_utf8_lossy(&output.stdout)); println!("{}", String::from_utf8_lossy(&output.stderr)); assert!(output.status.success(), "{output:?}"); let alef = fs::read(&format!("{target}/alef")).unwrap(); assert_eq!(alef, bet); } } } } } } } #[test] fn test_permutations_missing_line_ending() { let target = "target/unified-diff/"; // test all possible six-line files with missing newlines. let _ = std::fs::create_dir(target); for &a in &[0, 1, 2] { for &b in &[0, 1, 2] { for &c in &[0, 1, 2] { for &d in &[0, 1, 2] { for &e in &[0, 1, 2] { for &f in &[0, 1, 2] { for &g in &[0, 1, 2] { use std::fs::{self, File}; use std::io::Write; use std::process::Command; let mut alef = Vec::new(); let mut bet = Vec::new(); alef.write_all(if a == 0 { b"a\n" } else { b"b\n" }) .unwrap(); if a != 2 { bet.write_all(b"b\n").unwrap(); } alef.write_all(if b == 0 { b"c\n" } else { b"d\n" }) .unwrap(); if b != 2 { bet.write_all(b"d\n").unwrap(); } alef.write_all(if c == 0 { b"e\n" } else { b"f\n" }) .unwrap(); if c != 2 { bet.write_all(b"f\n").unwrap(); } alef.write_all(if d == 0 { b"g\n" } else { b"h\n" }) .unwrap(); if d != 2 { bet.write_all(b"h\n").unwrap(); } alef.write_all(if e == 0 { b"i\n" } else { b"j\n" }) .unwrap(); if e != 2 { bet.write_all(b"j\n").unwrap(); } alef.write_all(if f == 0 { b"k\n" } else { b"l\n" }) .unwrap(); if f != 2 { bet.write_all(b"l\n").unwrap(); } match g { 0 => { alef.pop(); } 1 => { bet.pop(); } 2 => { alef.pop(); bet.pop(); } _ => unreachable!(), } // This test diff is intentionally reversed. // We want it to turn the alef into bet. let diff = diff( &alef, &bet, &Params { from: "a/alefn".into(), to: (&format!("{target}/alefn")).into(), context_count: 2, ..Default::default() }, ); File::create(&format!("{target}/abn.diff")) .unwrap() .write_all(&diff) .unwrap(); let mut fa = File::create(&format!("{target}/alefn")).unwrap(); fa.write_all(&alef[..]).unwrap(); let mut fb = File::create(&format!("{target}/betn")).unwrap(); fb.write_all(&bet[..]).unwrap(); let _ = fa; let _ = fb; let output = Command::new("patch") .arg("-p0") .stdin(File::open(&format!("{target}/abn.diff")).unwrap()) .output() .unwrap(); assert!(output.status.success(), "{output:?}"); //println!("{}", String::from_utf8_lossy(&output.stdout)); //println!("{}", String::from_utf8_lossy(&output.stderr)); let alef = fs::read(&format!("{target}/alefn")).unwrap(); assert_eq!(alef, bet); } } } } } } } } #[test] fn test_permutations_empty_lines() { let target = "target/unified-diff/"; // test all possible six-line files with missing newlines. let _ = std::fs::create_dir(target); for &a in &[0, 1, 2] { for &b in &[0, 1, 2] { for &c in &[0, 1, 2] { for &d in &[0, 1, 2] { for &e in &[0, 1, 2] { for &f in &[0, 1, 2] { for &g in &[0, 1, 2, 3] { use std::fs::{self, File}; use std::io::Write; use std::process::Command; let mut alef = Vec::new(); let mut bet = Vec::new(); alef.write_all(if a == 0 { b"\n" } else { b"b\n" }).unwrap(); if a != 2 { bet.write_all(b"b\n").unwrap(); } alef.write_all(if b == 0 { b"\n" } else { b"d\n" }).unwrap(); if b != 2 { bet.write_all(b"d\n").unwrap(); } alef.write_all(if c == 0 { b"\n" } else { b"f\n" }).unwrap(); if c != 2 { bet.write_all(b"f\n").unwrap(); } alef.write_all(if d == 0 { b"\n" } else { b"h\n" }).unwrap(); if d != 2 { bet.write_all(b"h\n").unwrap(); } alef.write_all(if e == 0 { b"\n" } else { b"j\n" }).unwrap(); if e != 2 { bet.write_all(b"j\n").unwrap(); } alef.write_all(if f == 0 { b"\n" } else { b"l\n" }).unwrap(); if f != 2 { bet.write_all(b"l\n").unwrap(); } match g { 0 => { alef.pop(); } 1 => { bet.pop(); } 2 => { alef.pop(); bet.pop(); } 3 => {} _ => unreachable!(), } // This test diff is intentionally reversed. // We want it to turn the alef into bet. let diff = diff( &alef, &bet, &Params { from: "a/alef_".into(), to: (&format!("{target}/alef_")).into(), context_count: 2, ..Default::default() }, ); File::create(&format!("{target}/ab_.diff")) .unwrap() .write_all(&diff) .unwrap(); let mut fa = File::create(&format!("{target}/alef_")).unwrap(); fa.write_all(&alef[..]).unwrap(); let mut fb = File::create(&format!("{target}/bet_")).unwrap(); fb.write_all(&bet[..]).unwrap(); let _ = fa; let _ = fb; let output = Command::new("patch") .arg("-p0") .stdin(File::open(&format!("{target}/ab_.diff")).unwrap()) .output() .unwrap(); assert!(output.status.success(), "{output:?}"); //println!("{}", String::from_utf8_lossy(&output.stdout)); //println!("{}", String::from_utf8_lossy(&output.stderr)); let alef = fs::read(&format!("{target}/alef_")).unwrap(); assert_eq!(alef, bet); } } } } } } } } #[test] fn test_permutations_missing_lines() { let target = "target/unified-diff/"; // test all possible six-line files. let _ = std::fs::create_dir(target); for &a in &[0, 1, 2] { for &b in &[0, 1, 2] { for &c in &[0, 1, 2] { for &d in &[0, 1, 2] { for &e in &[0, 1, 2] { for &f in &[0, 1, 2] { use std::fs::{self, File}; use std::io::Write; use std::process::Command; let mut alef = Vec::new(); let mut bet = Vec::new(); alef.write_all(if a == 0 { b"a\n" } else { b"" }).unwrap(); if a != 2 { bet.write_all(b"b\n").unwrap(); } alef.write_all(if b == 0 { b"c\n" } else { b"" }).unwrap(); if b != 2 { bet.write_all(b"d\n").unwrap(); } alef.write_all(if c == 0 { b"e\n" } else { b"" }).unwrap(); if c != 2 { bet.write_all(b"f\n").unwrap(); } alef.write_all(if d == 0 { b"g\n" } else { b"" }).unwrap(); if d != 2 { bet.write_all(b"h\n").unwrap(); } alef.write_all(if e == 0 { b"i\n" } else { b"" }).unwrap(); if e != 2 { bet.write_all(b"j\n").unwrap(); } alef.write_all(if f == 0 { b"k\n" } else { b"" }).unwrap(); if f != 2 { bet.write_all(b"l\n").unwrap(); } // This test diff is intentionally reversed. // We want it to turn the alef into bet. let diff = diff( &alef, &bet, &Params { from: "a/alefx".into(), to: (&format!("{target}/alefx")).into(), context_count: 2, ..Default::default() }, ); File::create(&format!("{target}/abx.diff")) .unwrap() .write_all(&diff) .unwrap(); let mut fa = File::create(&format!("{target}/alefx")).unwrap(); fa.write_all(&alef[..]).unwrap(); let mut fb = File::create(&format!("{target}/betx")).unwrap(); fb.write_all(&bet[..]).unwrap(); let _ = fa; let _ = fb; let output = Command::new("patch") .arg("-p0") .stdin(File::open(&format!("{target}/abx.diff")).unwrap()) .output() .unwrap(); assert!(output.status.success(), "{output:?}"); //println!("{}", String::from_utf8_lossy(&output.stdout)); //println!("{}", String::from_utf8_lossy(&output.stderr)); let alef = fs::read(&format!("{target}/alefx")).unwrap(); assert_eq!(alef, bet); } } } } } } } #[test] fn test_permutations_reverse() { let target = "target/unified-diff/"; // test all possible six-line files. let _ = std::fs::create_dir(target); for &a in &[0, 1, 2] { for &b in &[0, 1, 2] { for &c in &[0, 1, 2] { for &d in &[0, 1, 2] { for &e in &[0, 1, 2] { for &f in &[0, 1, 2] { use std::fs::{self, File}; use std::io::Write; use std::process::Command; let mut alef = Vec::new(); let mut bet = Vec::new(); alef.write_all(if a == 0 { b"a\n" } else { b"f\n" }) .unwrap(); if a != 2 { bet.write_all(b"a\n").unwrap(); } alef.write_all(if b == 0 { b"b\n" } else { b"e\n" }) .unwrap(); if b != 2 { bet.write_all(b"b\n").unwrap(); } alef.write_all(if c == 0 { b"c\n" } else { b"d\n" }) .unwrap(); if c != 2 { bet.write_all(b"c\n").unwrap(); } alef.write_all(if d == 0 { b"d\n" } else { b"c\n" }) .unwrap(); if d != 2 { bet.write_all(b"d\n").unwrap(); } alef.write_all(if e == 0 { b"e\n" } else { b"b\n" }) .unwrap(); if e != 2 { bet.write_all(b"e\n").unwrap(); } alef.write_all(if f == 0 { b"f\n" } else { b"a\n" }) .unwrap(); if f != 2 { bet.write_all(b"f\n").unwrap(); } // This test diff is intentionally reversed. // We want it to turn the alef into bet. let diff = diff( &alef, &bet, &Params { from: "a/alefr".into(), to: (&format!("{target}/alefr")).into(), context_count: 2, ..Default::default() }, ); File::create(&format!("{target}/abr.diff")) .unwrap() .write_all(&diff) .unwrap(); let mut fa = File::create(&format!("{target}/alefr")).unwrap(); fa.write_all(&alef[..]).unwrap(); let mut fb = File::create(&format!("{target}/betr")).unwrap(); fb.write_all(&bet[..]).unwrap(); let _ = fa; let _ = fb; let output = Command::new("patch") .arg("-p0") .stdin(File::open(&format!("{target}/abr.diff")).unwrap()) .output() .unwrap(); assert!(output.status.success(), "{output:?}"); //println!("{}", String::from_utf8_lossy(&output.stdout)); //println!("{}", String::from_utf8_lossy(&output.stderr)); let alef = fs::read(&format!("{target}/alefr")).unwrap(); assert_eq!(alef, bet); } } } } } } } #[test] fn test_stop_early() { use crate::assert_diff_eq; let from_filename = "foo"; let from = ["a", "b", "c", ""].join("\n"); let to_filename = "bar"; let to = ["a", "d", "c", ""].join("\n"); let diff_full = diff( from.as_bytes(), to.as_bytes(), &Params { from: from_filename.into(), to: to_filename.into(), ..Default::default() }, ); let expected_full = [ "--- foo\tTIMESTAMP", "+++ bar\tTIMESTAMP", "@@ -1,3 +1,3 @@", " a", "-b", "+d", " c", "", ] .join("\n"); assert_diff_eq!(diff_full, expected_full); let diff_brief = diff( from.as_bytes(), to.as_bytes(), &Params { from: from_filename.into(), to: to_filename.into(), brief: true, ..Default::default() }, ); let expected_brief = ["--- foo\tTIMESTAMP", "+++ bar\tTIMESTAMP", ""].join("\n"); assert_diff_eq!(diff_brief, expected_brief); let nodiff_full = diff( from.as_bytes(), from.as_bytes(), &Params { from: from_filename.into(), to: to_filename.into(), ..Default::default() }, ); assert!(nodiff_full.is_empty()); let nodiff_brief = diff( from.as_bytes(), from.as_bytes(), &Params { from: from_filename.into(), to: to_filename.into(), brief: true, ..Default::default() }, ); assert!(nodiff_brief.is_empty()); } } diffutils-0.4.1/src/utils.rs000064400000000000000000000141351046102023000141200ustar 00000000000000// This file is part of the uutils diffutils package. // // For the full copyright and license information, please view the LICENSE-* // files that was distributed with this source code. use std::io::Write; use unicode_width::UnicodeWidthStr; /// Replace tabs by spaces in the input line. /// Correctly handle multi-bytes characters. /// This assumes that line does not contain any line breaks (if it does, the result is undefined). #[must_use] pub fn do_expand_tabs(line: &[u8], tabsize: usize) -> Vec { let tab = b'\t'; let ntabs = line.iter().filter(|c| **c == tab).count(); if ntabs == 0 { return line.to_vec(); } let mut result = Vec::with_capacity(line.len() + ntabs * (tabsize - 1)); let mut offset = 0; let mut iter = line.split(|c| *c == tab).peekable(); while let Some(chunk) = iter.next() { match String::from_utf8(chunk.to_vec()) { Ok(s) => offset += UnicodeWidthStr::width(s.as_str()), Err(_) => offset += chunk.len(), } result.extend_from_slice(chunk); if iter.peek().is_some() { result.resize(result.len() + tabsize - offset % tabsize, b' '); offset = 0; } } result } /// Write a single line to an output stream, expanding tabs to space if necessary. /// This assumes that line does not contain any line breaks /// (if it does and tabs are to be expanded to spaces, the result is undefined). pub fn do_write_line( output: &mut Vec, line: &[u8], expand_tabs: bool, tabsize: usize, ) -> std::io::Result<()> { if expand_tabs { output.write_all(do_expand_tabs(line, tabsize).as_slice()) } else { output.write_all(line) } } /// Retrieves the modification time of the input file specified by file path /// If an error occurs, it returns the current system time pub fn get_modification_time(file_path: &str) -> String { use chrono::{DateTime, Local}; use std::fs; use std::time::SystemTime; let modification_time: SystemTime = fs::metadata(file_path) .and_then(|m| m.modified()) .unwrap_or(SystemTime::now()); let modification_time: DateTime = modification_time.into(); let modification_time: String = modification_time .format("%Y-%m-%d %H:%M:%S%.9f %z") .to_string(); modification_time } #[cfg(test)] mod tests { use super::*; mod expand_tabs { use super::*; use pretty_assertions::assert_eq; fn assert_tab_expansion(line: &str, tabsize: usize, expected: &str) { assert_eq!( do_expand_tabs(line.as_bytes(), tabsize), expected.as_bytes() ); } #[test] fn basics() { assert_tab_expansion("foo barr baz", 8, "foo barr baz"); assert_tab_expansion("foo\tbarr\tbaz", 8, "foo barr baz"); assert_tab_expansion("foo\tbarr\tbaz", 5, "foo barr baz"); assert_tab_expansion("foo\tbarr\tbaz", 2, "foo barr baz"); } #[test] fn multibyte_chars() { assert_tab_expansion("foo\tépée\tbaz", 8, "foo épée baz"); assert_tab_expansion("foo\t😉\tbaz", 5, "foo 😉 baz"); // Note: The Woman Scientist emoji (👩‍🔬) is a ZWJ sequence combining // the Woman emoji (👩) and the Microscope emoji (🔬). On supported platforms // it is displayed as a single emoji and should have a print size of 2 columns, // but terminal emulators tend to not support this, and display the two emojis // side by side, thus accounting for a print size of 4 columns. assert_tab_expansion("foo\t👩‍🔬\tbaz", 6, "foo 👩‍🔬 baz"); } #[test] fn invalid_utf8() { // [240, 240, 152, 137] is an invalid UTF-8 sequence, so it is handled as 4 bytes assert_eq!( do_expand_tabs(&[240, 240, 152, 137, 9, 102, 111, 111], 8), &[240, 240, 152, 137, 32, 32, 32, 32, 102, 111, 111] ); } } mod write_line { use super::*; use pretty_assertions::assert_eq; fn assert_line_written(line: &str, expand_tabs: bool, tabsize: usize, expected: &str) { let mut output: Vec = Vec::new(); assert!(do_write_line(&mut output, line.as_bytes(), expand_tabs, tabsize).is_ok()); assert_eq!(output, expected.as_bytes()); } #[test] fn basics() { assert_line_written("foo bar baz", false, 8, "foo bar baz"); assert_line_written("foo bar\tbaz", false, 8, "foo bar\tbaz"); assert_line_written("foo bar\tbaz", true, 8, "foo bar baz"); } } mod modification_time { use super::*; #[test] fn set_time() { use chrono::{DateTime, Local}; use std::time::SystemTime; use tempfile::NamedTempFile; let temp = NamedTempFile::new().unwrap(); // set file modification time equal to current time let current = SystemTime::now(); let _ = temp.as_file().set_modified(current); // format current time let current: DateTime = current.into(); let current: String = current.format("%Y-%m-%d %H:%M:%S%.9f %z").to_string(); // verify assert_eq!( current, get_modification_time(&temp.path().to_string_lossy()) ); } #[test] fn invalid_file() { use chrono::{DateTime, Local}; use std::time::SystemTime; let invalid_file = "target/utils/invalid-file"; // store current time before calling `get_modification_time` // Because the file is invalid, it will return SystemTime::now() // which will be greater than previously saved time let current_time: DateTime = SystemTime::now().into(); let m_time: DateTime = get_modification_time(invalid_file).parse().unwrap(); assert!(m_time > current_time); } } } diffutils-0.4.1/tests/integration.rs000064400000000000000000000205661046102023000156630ustar 00000000000000// This file is part of the uutils diffutils package. // // For the full copyright and license information, please view the LICENSE-* // files that was distributed with this source code. use assert_cmd::cmd::Command; use diffutilslib::assert_diff_eq; use predicates::prelude::*; use std::fs::File; use std::io::Write; use tempfile::{tempdir, NamedTempFile}; // Integration tests for the diffutils command #[test] fn unknown_param() -> Result<(), Box> { let mut cmd = Command::cargo_bin("diffutils")?; cmd.arg("--foobar"); cmd.assert() .code(predicate::eq(2)) .failure() .stderr(predicate::str::starts_with("Unknown option: \"--foobar\"")); Ok(()) } #[test] fn cannot_read_files() -> Result<(), Box> { let file = NamedTempFile::new()?; let nofile = NamedTempFile::new()?; let nopath = nofile.into_temp_path(); std::fs::remove_file(&nopath)?; let mut cmd = Command::cargo_bin("diffutils")?; cmd.arg(&nopath).arg(file.path()); cmd.assert() .code(predicate::eq(2)) .failure() .stderr(predicate::str::starts_with("Failed to read from-file")); let mut cmd = Command::cargo_bin("diffutils")?; cmd.arg(file.path()).arg(&nopath); cmd.assert() .code(predicate::eq(2)) .failure() .stderr(predicate::str::starts_with("Failed to read to-file")); let mut cmd = Command::cargo_bin("diffutils")?; cmd.arg(&nopath).arg(&nopath); cmd.assert() .code(predicate::eq(2)) .failure() .stderr(predicate::str::starts_with("Failed to read from-file")); Ok(()) } #[test] fn no_differences() -> Result<(), Box> { let file = NamedTempFile::new()?; for option in ["", "-u", "-c", "-e"] { let mut cmd = Command::cargo_bin("diffutils")?; if !option.is_empty() { cmd.arg(option); } cmd.arg(file.path()).arg(file.path()); cmd.assert() .code(predicate::eq(0)) .success() .stdout(predicate::str::is_empty()); } Ok(()) } #[test] fn no_differences_report_identical_files() -> Result<(), Box> { // same file let mut file1 = NamedTempFile::new()?; file1.write_all("foo\n".as_bytes())?; for option in ["", "-u", "-c", "-e"] { let mut cmd = Command::cargo_bin("diffutils")?; if !option.is_empty() { cmd.arg(option); } cmd.arg("-s").arg(file1.path()).arg(file1.path()); cmd.assert() .code(predicate::eq(0)) .success() .stdout(predicate::eq(format!( "Files {} and {} are identical\n", file1.path().to_string_lossy(), file1.path().to_string_lossy(), ))); } // two files with the same content let mut file2 = NamedTempFile::new()?; file2.write_all("foo\n".as_bytes())?; for option in ["", "-u", "-c", "-e"] { let mut cmd = Command::cargo_bin("diffutils")?; if !option.is_empty() { cmd.arg(option); } cmd.arg("-s").arg(file1.path()).arg(file2.path()); cmd.assert() .code(predicate::eq(0)) .success() .stdout(predicate::eq(format!( "Files {} and {} are identical\n", file1.path().to_string_lossy(), file2.path().to_string_lossy(), ))); } Ok(()) } #[test] fn differences() -> Result<(), Box> { let mut file1 = NamedTempFile::new()?; file1.write_all("foo\n".as_bytes())?; let mut file2 = NamedTempFile::new()?; file2.write_all("bar\n".as_bytes())?; for option in ["", "-u", "-c", "-e"] { let mut cmd = Command::cargo_bin("diffutils")?; if !option.is_empty() { cmd.arg(option); } cmd.arg(file1.path()).arg(file2.path()); cmd.assert() .code(predicate::eq(1)) .failure() .stdout(predicate::str::is_empty().not()); } Ok(()) } #[test] fn differences_brief() -> Result<(), Box> { let mut file1 = NamedTempFile::new()?; file1.write_all("foo\n".as_bytes())?; let mut file2 = NamedTempFile::new()?; file2.write_all("bar\n".as_bytes())?; for option in ["", "-u", "-c", "-e"] { let mut cmd = Command::cargo_bin("diffutils")?; if !option.is_empty() { cmd.arg(option); } cmd.arg("-q").arg(file1.path()).arg(file2.path()); cmd.assert() .code(predicate::eq(1)) .failure() .stdout(predicate::eq(format!( "Files {} and {} differ\n", file1.path().to_string_lossy(), file2.path().to_string_lossy() ))); } Ok(()) } #[test] fn missing_newline() -> Result<(), Box> { let mut file1 = NamedTempFile::new()?; file1.write_all("foo".as_bytes())?; let mut file2 = NamedTempFile::new()?; file2.write_all("bar".as_bytes())?; let mut cmd = Command::cargo_bin("diffutils")?; cmd.arg("-e").arg(file1.path()).arg(file2.path()); cmd.assert() .code(predicate::eq(2)) .failure() .stderr(predicate::str::starts_with("No newline at end of file")); Ok(()) } #[test] fn read_from_stdin() -> Result<(), Box> { let mut file1 = NamedTempFile::new()?; file1.write_all("foo\n".as_bytes())?; let mut file2 = NamedTempFile::new()?; file2.write_all("bar\n".as_bytes())?; let mut cmd = Command::cargo_bin("diffutils")?; cmd.arg("-u") .arg(file1.path()) .arg("-") .write_stdin("bar\n"); cmd.assert().code(predicate::eq(1)).failure(); let output = cmd.output().unwrap().stdout; assert_diff_eq!( output, format!( "--- {}\tTIMESTAMP\n+++ -\tTIMESTAMP\n@@ -1 +1 @@\n-foo\n+bar\n", file1.path().to_string_lossy() ) ); let mut cmd = Command::cargo_bin("diffutils")?; cmd.arg("-u") .arg("-") .arg(file2.path()) .write_stdin("foo\n"); cmd.assert().code(predicate::eq(1)).failure(); let output = cmd.output().unwrap().stdout; assert_diff_eq!( output, format!( "--- -\tTIMESTAMP\n+++ {}\tTIMESTAMP\n@@ -1 +1 @@\n-foo\n+bar\n", file2.path().to_string_lossy() ) ); let mut cmd = Command::cargo_bin("diffutils")?; cmd.arg("-u").arg("-").arg("-"); cmd.assert() .code(predicate::eq(0)) .success() .stdout(predicate::str::is_empty()); #[cfg(unix)] { let mut cmd = Command::cargo_bin("diffutils")?; cmd.arg("-u") .arg(file1.path()) .arg("/dev/stdin") .write_stdin("bar\n"); cmd.assert().code(predicate::eq(1)).failure(); let output = cmd.output().unwrap().stdout; assert_diff_eq!( output, format!( "--- {}\tTIMESTAMP\n+++ /dev/stdin\tTIMESTAMP\n@@ -1 +1 @@\n-foo\n+bar\n", file1.path().to_string_lossy() ) ); } Ok(()) } #[test] fn compare_file_to_directory() -> Result<(), Box> { let tmp_dir = tempdir()?; let directory = tmp_dir.path().join("d"); let _ = std::fs::create_dir(&directory); let a_path = tmp_dir.path().join("a"); let mut a = File::create(&a_path).unwrap(); a.write_all(b"a\n").unwrap(); let da_path = directory.join("a"); let mut da = File::create(&da_path).unwrap(); da.write_all(b"da\n").unwrap(); let mut cmd = Command::cargo_bin("diffutils")?; cmd.arg("-u").arg(&directory).arg(&a_path); cmd.assert().code(predicate::eq(1)).failure(); let output = cmd.output().unwrap().stdout; assert_diff_eq!( output, format!( "--- {}\tTIMESTAMP\n+++ {}\tTIMESTAMP\n@@ -1 +1 @@\n-da\n+a\n", da_path.display(), a_path.display() ) ); let mut cmd = Command::cargo_bin("diffutils")?; cmd.arg("-u").arg(&a_path).arg(&directory); cmd.assert().code(predicate::eq(1)).failure(); let output = cmd.output().unwrap().stdout; assert_diff_eq!( output, format!( "--- {}\tTIMESTAMP\n+++ {}\tTIMESTAMP\n@@ -1 +1 @@\n-a\n+da\n", a_path.display(), da_path.display() ) ); Ok(()) } diffutils-0.4.1/tests/print-test-results.sh000075500000000000000000000020111046102023000171220ustar 00000000000000#!/bin/bash # Print the test results written to a JSON file by run-upstream-testsuite.sh # in a markdown format. The printout includes the name of the test, the result, # the URL to the test script and the contents of stdout and stderr. # It can be used verbatim as the description when filing an issue for a test # with an unexpected result. json="test-results.json" [[ -n $1 ]] && json="$1" codeblock () { echo -e "\`\`\`\n$1\n\`\`\`"; } jq -c '.tests[]' "$json" | while read -r test do name=$(echo "$test" | jq -r '.test') echo "# test: $name" result=$(echo "$test" | jq -r '.result') echo "result: $result" url=$(echo "$test" | jq -r '.url') echo "url: $url" if [[ "$result" != "SKIP" ]] then stdout=$(echo "$test" | jq -r '.stdout' | base64 -d) if [[ -n "$stdout" ]] then echo "## stdout" codeblock "$stdout" fi stderr=$(echo "$test" | jq -r '.stderr' | base64 -d) if [[ -n "$stderr" ]] then echo "## stderr" codeblock "$stderr" fi fi echo "" done diffutils-0.4.1/tests/run-upstream-testsuite.sh000075500000000000000000000107241046102023000200150ustar 00000000000000#!/bin/bash # Run the GNU upstream test suite for diffutils against a local build of the # Rust implementation, print out a summary of the test results, and writes a # JSON file ('test-results.json') containing detailed information about the # test run. # The JSON file contains metadata about the test run, and for each test the # result as well as the contents of stdout, stderr, and of all the files # written by the test script, if any (excluding subdirectories). # The script takes a shortcut to fetch only the test suite from the upstream # repository and carefully avoids running the autotools machinery which is # time-consuming and resource-intensive, and doesn't offer the option to not # build the upstream binaries. As a consequence, the environment in which the # tests are run might not match exactly that used when the upstream tests are # run through the autotools. # By default it expects a release build of the diffutils binary, but a # different build profile can be specified as an argument # (e.g. 'dev' or 'test'). # Unless overridden by the $TESTS environment variable, all tests in the test # suite will be run. Tests targeting a command that is not yet implemented # (e.g. cmp, diff3 or sdiff) are skipped. scriptpath=$(dirname "$(readlink -f "$0")") rev=$(git rev-parse HEAD) # Allow passing a specific profile as parameter (default to "release") profile="release" [[ -n $1 ]] && profile="$1" # Verify that the diffutils binary was built for the requested profile binary="$scriptpath/../target/$profile/diffutils" if [[ ! -x "$binary" ]] then echo "Missing build for profile $profile" exit 1 fi # Work in a temporary directory tempdir=$(mktemp -d) cd "$tempdir" # Check out the upstream test suite gitserver="https://git.savannah.gnu.org" testsuite="$gitserver/git/diffutils.git" echo "Fetching upstream test suite from $testsuite" git clone -n --depth=1 --filter=tree:0 "$testsuite" &> /dev/null cd diffutils git sparse-checkout set --no-cone tests &> /dev/null git checkout &> /dev/null upstreamrev=$(git rev-parse HEAD) # Ensure that calling `diff` invokes the built `diffutils` binary instead of # the upstream `diff` binary that is most likely installed on the system mkdir src cd src ln -s "$binary" diff cd ../tests if [[ -n "$TESTS" ]] then tests="$TESTS" else # Get a list of all upstream tests (default if $TESTS isn't set) echo -e '\n\nprinttests:\n\t@echo "${TESTS}"' >> Makefile.am tests=$(make -f Makefile.am printtests) fi total=$(echo "$tests" | wc -w) echo "Running $total tests" export LC_ALL=C export KEEP=yes exitcode=0 timestamp=$(date -Iseconds) urlroot="$gitserver/cgit/diffutils.git/tree/tests/" passed=0 failed=0 skipped=0 normal="$(tput sgr0)" for test in $tests do result="FAIL" url="$urlroot$test?id=$upstreamrev" # Run only the tests that invoke `diff`, # because other binaries aren't implemented yet if ! grep -E -s -q "(cmp|diff3|sdiff)" "$test" then sh "$test" 1> stdout.txt 2> stderr.txt && result="PASS" || exitcode=1 json+="{\"test\":\"$test\",\"result\":\"$result\"," json+="\"url\":\"$url\"," json+="\"stdout\":\"$(base64 -w0 < stdout.txt)\"," json+="\"stderr\":\"$(base64 -w0 < stderr.txt)\"," json+="\"files\":{" cd gt-$test.* # Note: this doesn't include the contents of subdirectories, # but there isn't much value added in doing so for file in * do [[ -f "$file" ]] && json+="\"$file\":\"$(base64 -w0 < "$file")\"," done json="${json%,}}}," cd - > /dev/null [[ "$result" = "PASS" ]] && (( passed++ )) [[ "$result" = "FAIL" ]] && (( failed++ )) else result="SKIP" (( skipped++ )) json+="{\"test\":\"$test\",\"url\":\"$url\",\"result\":\"$result\"}," fi color=2 # green [[ "$result" = "FAIL" ]] && color=1 # red [[ "$result" = "SKIP" ]] && color=3 # yellow printf " %-40s $(tput setaf $color)$result$(tput sgr0)\n" "$test" done echo "" echo -n "Summary: TOTAL: $total / " echo -n "$(tput setaf 2)PASS$normal: $passed / " echo -n "$(tput setaf 1)FAIL$normal: $failed / " echo "$(tput setaf 3)SKIP$normal: $skipped" echo "" json="\"tests\":[${json%,}]" metadata="\"timestamp\":\"$timestamp\"," metadata+="\"revision\":\"$rev\"," metadata+="\"upstream-revision\":\"$upstreamrev\"," if [[ -n "$GITHUB_ACTIONS" ]] then metadata+="\"branch\":\"$GITHUB_REF\"," fi json="{$metadata $json}" # Clean up cd "$scriptpath" rm -rf "$tempdir" resultsfile="test-results.json" echo "$json" | jq > "$resultsfile" echo "Results written to $scriptpath/$resultsfile" exit $exitcode