[
  {
    "path": ".github/workflows/rust.yml",
    "content": "name: Rust\n\non:\n  push:\n    branches: [ master, dev-v2 ]\n  pull_request:\n    branches: [ master, dev-v2 ]\n\njobs:\n  build:\n    runs-on: ${{ matrix.os }}\n    strategy:\n      matrix:\n        build: [linux, macos, windows, netbsd]\n        include:\n          - build: linux\n            os: ubuntu-latest\n            target: x86_64-unknown-linux-gnu\n          - build: macos\n            os: macos-latest\n            target: x86_64-apple-darwin\n          - build: windows\n            os: windows-latest\n            target: x86_64-pc-windows-msvc\n          - build: netbsd\n            os: ubuntu-22.04\n            target: x86_64-unknown-netbsd\n\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v2\n        with:\n          fetch-depth: 1\n\n      - name: Install Rust\n        uses: actions-rs/toolchain@v1\n        with:\n          toolchain: stable\n          profile: minimal\n          override: true\n          target: ${{ matrix.target }}\n\n      - name: Use Cross\n        shell: bash\n        run: |\n          cargo install cross\n          echo \"CARGO=cross\" >> $GITHUB_ENV\n          echo \"TARGET_FLAGS=--target ${{ matrix.target }}\" >> $GITHUB_ENV\n\n      - name: Show command used for Cargo\n        run: |\n          echo \"cargo command is: ${{ env.CARGO }}\"\n          echo \"target flag is: ${{ env.TARGET_FLAGS }}\"\n          echo \"target dir is: ${{ env.TARGET_DIR }}\"\n\n      - name: cargo test\n        if: ${{ !startsWith(matrix.build, 'netbsd') }}\n        run: ${{ env.CARGO }} test --verbose ${{ env.TARGET_FLAGS }}\n\n      - name: cargo test (container tests)\n        if: ${{ matrix.build == 'linux' }}\n        # Grant the tests (running inside the cross docker container) access to the host's docker socket\n        env:\n          CROSS_CONTAINER_OPTS: -v /var/run/docker.sock:/var/run/docker.sock -e DOCKER_HOST=unix:///var/run/docker.sock\n          CROSS_CONTAINER_UID: \"0\"\n          CROSS_CONTAINER_GID: \"0\"\n          CROSS_CONTAINER_USER_NAMESPACE: none\n        run: ${{ env.CARGO }} test --verbose --test freedesktop_tests ${{ env.TARGET_FLAGS }} -- --ignored\n\n      - name: cargo test (without chrono)\n        if: ${{ !startsWith(matrix.build, 'netbsd') }}\n        run: ${{ env.CARGO }} test --verbose --no-default-features --features coinit_apartmentthreaded ${{ env.TARGET_FLAGS }}\n\n      - name: cargo build\n        if: ${{ startsWith(matrix.build, 'netbsd') }}\n        run: ${{ env.CARGO }} build --verbose ${{ env.TARGET_FLAGS }}\n\n      - name: cargo build (without chrono)\n        if: ${{ startsWith(matrix.build, 'netbsd') }}\n        run: ${{ env.CARGO }} build --verbose --no-default-features --features coinit_apartmentthreaded ${{ env.TARGET_FLAGS }}\n\n      - name: cargo fmt\n        uses: actions-rs/cargo@v1\n        with:\n          command: fmt\n          args: --all -- --check\n\n      - name: cargo clippy\n        uses: actions-rs/cargo@v1\n        with:\n          command: clippy\n          args: -- -D warnings\n"
  },
  {
    "path": ".gitignore",
    "content": "/target\n/.vscode\n**/*.rs.bk\nCargo.lock\n.DS_Store\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "# Changelog\n\nAll notable changes to this project will be documented in this file.\n\nThe format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),\nand this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).\n\n## 5.2.6 (2026-05-03)\n\n### Bug Fixes\n\n - <csr-id-bed74bd0fe35a0593288cbb41cf3f0b571826872/> replace assert! with ensure_virtually_exists to gracefully handle missing trash files\n   When a .trashinfo file references a file in trash/files that no longer\n   exists, the assert! in metadata() and restore_all() would panic instead\n   of returning a proper error. This caused cosmic-files to crash when\n   encountering such orphaned trash entries.\n   \n   Replace assert! with ensure_virtually_exists() which returns a proper\n   Error::FileSystem instead of panicking, allowing callers to handle\n   the missing file gracefully.\n\n### Other\n\n - <csr-id-8997e9aa267eb7fffb76720405719bb41d319944/> it's parents -> its parents\n - <csr-id-ac00b38e54f386f80a661ada4372e4b75a4427d4/> test canonicalize_path_or_parents\n - <csr-id-ec086e19de97acf1bf6c193201318d96dcc12a7d/> Update log messages to refer to home trash topdir\n\n### Commit Statistics\n\n<csr-read-only-do-not-edit/>\n\n - 8 commits contributed to the release.\n - 4 commits were understood as [conventional](https://www.conventionalcommits.org).\n - 1 unique issue was worked on: [#144](https://github.com/Byron/trash-rs/issues/144)\n\n### Commit Details\n\n<csr-read-only-do-not-edit/>\n\n<details><summary>view details</summary>\n\n * **[#144](https://github.com/Byron/trash-rs/issues/144)**\n    - Test freedesktop trash implementation in container ([`c21e99e`](https://github.com/Byron/trash-rs/commit/c21e99e449da077ea927eef27898b492bf66c64e))\n * **Uncategorized**\n    - Merge pull request #145 from ZlordHUN/fix/graceful-missing-trash-file ([`75bb3b7`](https://github.com/Byron/trash-rs/commit/75bb3b70433db74aa023d0e930d9e49d115aad83))\n    - Replace assert! with ensure_virtually_exists to gracefully handle missing trash files ([`bed74bd`](https://github.com/Byron/trash-rs/commit/bed74bd0fe35a0593288cbb41cf3f0b571826872))\n    - Merge pull request #143 from null-dev/nd/canonicalize-trash-path ([`5a7c7f7`](https://github.com/Byron/trash-rs/commit/5a7c7f77f8025d651b6b964725637e60e8cd37cc))\n    - It's parents -> its parents ([`8997e9a`](https://github.com/Byron/trash-rs/commit/8997e9aa267eb7fffb76720405719bb41d319944))\n    - Test canonicalize_path_or_parents ([`ac00b38`](https://github.com/Byron/trash-rs/commit/ac00b38e54f386f80a661ada4372e4b75a4427d4))\n    - Update log messages to refer to home trash topdir ([`ec086e1`](https://github.com/Byron/trash-rs/commit/ec086e19de97acf1bf6c193201318d96dcc12a7d))\n    - Implement canonicalize_path for non-existent paths ([`59add80`](https://github.com/Byron/trash-rs/commit/59add80bea4f14911862092c1edc9027def98602))\n</details>\n\n## 5.2.5 (2025-10-25)\n\n### Bug Fixes\n\n - <csr-id-23ca2a2ea182fe551ac0d4630ebbb98c3db0abad/> set the `objc2-foundation` to the one that's actually required\n   Otherwise, downstream with Cargo.lock may see build failures.\n\n### Commit Statistics\n\n<csr-read-only-do-not-edit/>\n\n - 2 commits contributed to the release.\n - 1 day passed between releases.\n - 1 commit was understood as [conventional](https://www.conventionalcommits.org).\n - 1 unique issue was worked on: [#142](https://github.com/Byron/trash-rs/issues/142)\n\n### Commit Details\n\n<csr-read-only-do-not-edit/>\n\n<details><summary>view details</summary>\n\n * **[#142](https://github.com/Byron/trash-rs/issues/142)**\n    - Set the `objc2-foundation` to the one that's actually required ([`23ca2a2`](https://github.com/Byron/trash-rs/commit/23ca2a2ea182fe551ac0d4630ebbb98c3db0abad))\n * **Uncategorized**\n    - Release trash v5.2.5 ([`3ca5818`](https://github.com/Byron/trash-rs/commit/3ca5818fac0f4eac0b9244f0bacf16c557683c2d))\n</details>\n\n## 5.2.4 (2025-10-24)\n\n### Bug Fixes\n\n - <csr-id-c3d2d1d094ea4bde31398855b78af20b19abc6f8/> handle cross-device link errors by falling back to copy+delete on Linux\n\n### Commit Statistics\n\n<csr-read-only-do-not-edit/>\n\n - 5 commits contributed to the release over the course of 69 calendar days.\n - 69 days passed between releases.\n - 1 commit was understood as [conventional](https://www.conventionalcommits.org).\n - 0 issues like '(#ID)' were seen in commit messages\n\n### Commit Details\n\n<csr-read-only-do-not-edit/>\n\n<details><summary>view details</summary>\n\n * **Uncategorized**\n    - Release trash v5.2.4 ([`e3092ad`](https://github.com/Byron/trash-rs/commit/e3092add67470b904e327d1183c7692c55b65bf1))\n    - Merge pull request #141 from muni-corn/cross-device-trash ([`d0784e4`](https://github.com/Byron/trash-rs/commit/d0784e42f99ef572050ffc8bc9abd4002cc9dd78))\n    - Refactor and fixes ([`1894bfe`](https://github.com/Byron/trash-rs/commit/1894bfe4ab6677ca833cc60bbd6e568480abbf77))\n    - Handle cross-device link errors by falling back to copy+delete on Linux ([`c3d2d1d`](https://github.com/Byron/trash-rs/commit/c3d2d1d094ea4bde31398855b78af20b19abc6f8))\n    - Merge branch 'feature-upgrade-objc2' ([`cd22e14`](https://github.com/Byron/trash-rs/commit/cd22e1456bffc6027622cd55f131d43dc5372015))\n</details>\n\n## 5.2.3 (2025-08-15)\n\nUpdated the `obj2` crate when building or MacOS.\n\n### Commit Statistics\n\n<csr-read-only-do-not-edit/>\n\n - 4 commits contributed to the release over the course of 114 calendar days.\n - 0 commits were understood as [conventional](https://www.conventionalcommits.org).\n - 0 issues like '(#ID)' were seen in commit messages\n\n### Thanks Clippy\n\n<csr-read-only-do-not-edit/>\n\n[Clippy](https://github.com/rust-lang/rust-clippy) helped 1 time to make code idiomatic. \n\n### Commit Details\n\n<csr-read-only-do-not-edit/>\n\n<details><summary>view details</summary>\n\n * **Uncategorized**\n    - Release trash v5.2.3 ([`a628ccc`](https://github.com/Byron/trash-rs/commit/a628cccc688c6172bd793ef5dd5791fc987402d7))\n    - Prepare patch release notes. ([`f9be785`](https://github.com/Byron/trash-rs/commit/f9be7851acc660023ee0f3a6d1e58d04a4731c9f))\n    - Upgrade objc2 ([`134c357`](https://github.com/Byron/trash-rs/commit/134c3577e4892878e65fa7af8d24d1910df0fe6e))\n    - Thanks clippy ([`b80f7ed`](https://github.com/Byron/trash-rs/commit/b80f7edb1e3db64ae029b02a26d77c11986d9f11))\n</details>\n\n## 5.2.2 (2025-02-22)\n\n<csr-id-083743e848ff1b2a61af47bb3afdd8aa04e3eace/>\n\n### Chore\n\n - <csr-id-083743e848ff1b2a61af47bb3afdd8aa04e3eace/> prepare for objc2 frameworks v0.3\n   These will have a bunch of default features enabled, so let's\n   pre-emptively disable them.\n\n### Bug Fixes\n\n - <csr-id-dffb80d0950c2edd52a7883162fa8923393ea5c8/> Use octal for S_ISVTX sticky bit check\n\n### Commit Statistics\n\n<csr-read-only-do-not-edit/>\n\n - 7 commits contributed to the release over the course of 76 calendar days.\n - 77 days passed between releases.\n - 2 commits were understood as [conventional](https://www.conventionalcommits.org).\n - 0 issues like '(#ID)' were seen in commit messages\n\n### Commit Details\n\n<csr-read-only-do-not-edit/>\n\n<details><summary>view details</summary>\n\n * **Uncategorized**\n    - Release trash v5.2.2 ([`2ac29d2`](https://github.com/Byron/trash-rs/commit/2ac29d20a74c88d5933c1545dd4dd285910d2478))\n    - Merge pull request #136 from nebel/master ([`1704f72`](https://github.com/Byron/trash-rs/commit/1704f72605fd1daf1a61d3caaeb6312260df8367))\n    - Use octal for S_ISVTX sticky bit check ([`dffb80d`](https://github.com/Byron/trash-rs/commit/dffb80d0950c2edd52a7883162fa8923393ea5c8))\n    - Merge pull request #135 from madsmtm/objc2-disable-default-features ([`74894a8`](https://github.com/Byron/trash-rs/commit/74894a8a7a32e66d2ef02ae70285437184a07bd3))\n    - Prepare for objc2 frameworks v0.3 ([`083743e`](https://github.com/Byron/trash-rs/commit/083743e848ff1b2a61af47bb3afdd8aa04e3eace))\n    - Merge pull request #132 from eugenesvk/fr-mac-test-out ([`357c3b8`](https://github.com/Byron/trash-rs/commit/357c3b81111c82184ac83b94b74627266314aa82))\n    - Move MacOS specific tests to their own directory ([`ee7f256`](https://github.com/Byron/trash-rs/commit/ee7f2562ee5fbc13702b5bfbd55fc0214a9ea6e8))\n</details>\n\n## 5.2.1 (2024-12-07)\n\n<csr-id-415c87d81ff859ae40ba5d2e31ffcc44a1ebfffa/>\n<csr-id-6fbad98299ffde1acf2a63552d39e4085664d6f1/>\n<csr-id-3978204c7b5d7ca1038717da3238c82f7bb6a6c6/>\n<csr-id-e58e92baee1f3121114befe73e2a7a1d1dba363e/>\n<csr-id-9ed83e724f944f4eacf2e4cafdf8025548f7a17b/>\n<csr-id-175d6f5de323b2fed7c8049eaf6bb91266171b30/>\n<csr-id-bfbc394a1aba8cb3f348c77f3dffc18a59dde28f/>\n<csr-id-dc7dca02ba13b34d57f63244522044a17e88cecc/>\n<csr-id-9c213c91817d718b1785b9cd8a52d6c87beef936/>\n\n### Bug Fixes\n\n - <csr-id-e1bb697a510ec49008d0b4f9a58b38dc061d7901/> Escape quoted paths when deleting with AppleScript\n - <csr-id-6f0b737668c0f9c19e09657e8cbc98caf90e30a9/> Support for non-UTF8 paths on HFS+ on MacOS\n   Now illegal UTF8 is percent-encoded. Previously this code would have panicked.\n\n### Other\n\n - <csr-id-415c87d81ff859ae40ba5d2e31ffcc44a1ebfffa/> add an overview table to DeleteMethod on Mac\n - <csr-id-6fbad98299ffde1acf2a63552d39e4085664d6f1/> move macos deps behind macos cfg target\n - <csr-id-3978204c7b5d7ca1038717da3238c82f7bb6a6c6/> add simdutf8 for fast utf8 validation\n - <csr-id-e58e92baee1f3121114befe73e2a7a1d1dba363e/> add percent encoding support\n - <csr-id-9ed83e724f944f4eacf2e4cafdf8025548f7a17b/> replace create with create_new to avoid potentially nulling existing files\n\n### Test\n\n - <csr-id-175d6f5de323b2fed7c8049eaf6bb91266171b30/> new delete illegal bytes via Finder\n   Disabled since only works on older FS, but tested manually to work on a USB HFS drive\n - <csr-id-bfbc394a1aba8cb3f348c77f3dffc18a59dde28f/> new delete illegal bytes\n   Disabled since only works on older FS, but tested manually to work on a USB HFS drive\n - <csr-id-dc7dca02ba13b34d57f63244522044a17e88cecc/> add for from_utf8_lossy_pc\n - <csr-id-9c213c91817d718b1785b9cd8a52d6c87beef936/> replace create with create_new to avoid potentially nulling existing files\n\n### Commit Statistics\n\n<csr-read-only-do-not-edit/>\n\n - 26 commits contributed to the release.\n - 11 commits were understood as [conventional](https://www.conventionalcommits.org).\n - 0 issues like '(#ID)' were seen in commit messages\n\n### Commit Details\n\n<csr-read-only-do-not-edit/>\n\n<details><summary>view details</summary>\n\n * **Uncategorized**\n    - Release trash v5.2.1 ([`59b0a8a`](https://github.com/Byron/trash-rs/commit/59b0a8a1a1a4625e281b417f7a6e4d4c2a077aea))\n    - Escape quoted paths when deleting with AppleScript ([`e1bb697`](https://github.com/Byron/trash-rs/commit/e1bb697a510ec49008d0b4f9a58b38dc061d7901))\n    - Support for non-UTF8 paths on HFS+ on MacOS ([`6f0b737`](https://github.com/Byron/trash-rs/commit/6f0b737668c0f9c19e09657e8cbc98caf90e30a9))\n    - Various refactors ([`d23a591`](https://github.com/Byron/trash-rs/commit/d23a59166d52c90d7ee02ca2fb356cad0b330eca))\n    - Merge pull request #125 from eugenesvk/fr-doc-deletemethod ([`47ed29d`](https://github.com/Byron/trash-rs/commit/47ed29da5773e3f94285ee6f77fe4b9ba484ea3b))\n    - Add an overview table to DeleteMethod on Mac ([`415c87d`](https://github.com/Byron/trash-rs/commit/415c87d81ff859ae40ba5d2e31ffcc44a1ebfffa))\n    - Clippy ([`b147384`](https://github.com/Byron/trash-rs/commit/b147384fb820a0577d8228b5872d6dac338d4cf5))\n    - Cargo fmt ([`e499a0e`](https://github.com/Byron/trash-rs/commit/e499a0e1a49dbf3f2e672d1d42cc7b54944b6d9d))\n    - New delete illegal bytes via Finder ([`175d6f5`](https://github.com/Byron/trash-rs/commit/175d6f5de323b2fed7c8049eaf6bb91266171b30))\n    - Fix Finder path generation for AS ([`0359a4d`](https://github.com/Byron/trash-rs/commit/0359a4d13975e2fc5717052b2aa23e0394c7b619))\n    - Cargo fmt ([`5d17879`](https://github.com/Byron/trash-rs/commit/5d17879c0c823209f05d4cbc21629079c67e51ba))\n    - Fix finder extra escaping ([`d7295e8`](https://github.com/Byron/trash-rs/commit/d7295e8edd0934d3d1b3e7e0f9cc745b5d103ce2))\n    - Cargo fmt ([`ab5c49b`](https://github.com/Byron/trash-rs/commit/ab5c49ba45d94fdfa1ed6919daf1d61627de8ab3))\n    - New delete illegal bytes ([`bfbc394`](https://github.com/Byron/trash-rs/commit/bfbc394a1aba8cb3f348c77f3dffc18a59dde28f))\n    - Move macos deps behind macos cfg target ([`6fbad98`](https://github.com/Byron/trash-rs/commit/6fbad98299ffde1acf2a63552d39e4085664d6f1))\n    - Convert delete_using_finder to use binary Paths ([`c013b9a`](https://github.com/Byron/trash-rs/commit/c013b9a95bfde455a85dcf5b382ee7a6aaf145a2))\n    - Convert delete_using_file_mgr to use binary Paths ([`1d18e7a`](https://github.com/Byron/trash-rs/commit/1d18e7a9ae7a92a909c4dc34b441d799f0466b0b))\n    - Remove automatic panicky conversion of potentially binary paths into non-binary strings ([`d7d2187`](https://github.com/Byron/trash-rs/commit/d7d218743dc1e4793a49b0aa95e752e740c2a9c3))\n    - Add simdutf8 for fast utf8 validation ([`3978204`](https://github.com/Byron/trash-rs/commit/3978204c7b5d7ca1038717da3238c82f7bb6a6c6))\n    - Add for from_utf8_lossy_pc ([`dc7dca0`](https://github.com/Byron/trash-rs/commit/dc7dca02ba13b34d57f63244522044a17e88cecc))\n    - Add from_utf8_lossy_pc ([`8481d3c`](https://github.com/Byron/trash-rs/commit/8481d3cccb561e67a98f79ae1bfa8f7ade87d6c4))\n    - Add percent encoding support ([`e58e92b`](https://github.com/Byron/trash-rs/commit/e58e92baee1f3121114befe73e2a7a1d1dba363e))\n    - Merge pull request #126 from eugenesvk/fr-test-file-new ([`823f6fb`](https://github.com/Byron/trash-rs/commit/823f6fb3856f67a99e40ec943488615eb12d3edc))\n    - Try de-clippy ([`243b00d`](https://github.com/Byron/trash-rs/commit/243b00d75f165472f55127cd40cbcbf08f715400))\n    - Replace create with create_new to avoid potentially nulling existing files ([`9c213c9`](https://github.com/Byron/trash-rs/commit/9c213c91817d718b1785b9cd8a52d6c87beef936))\n    - Replace create with create_new to avoid potentially nulling existing files ([`9ed83e7`](https://github.com/Byron/trash-rs/commit/9ed83e724f944f4eacf2e4cafdf8025548f7a17b))\n</details>\n\n## 5.2.0 (2024-10-26)\n\n### New Features\n\n - <csr-id-6d59fa939429d2eede8b7cf22b2e084bc3c546f4/> Short circuiting check for empty trash\n   `is_empty()` is a short circuiting function that checks if the trash is\n   empty on Freedesktop compatible systems and Windows.\n   \n   The main purpose of `is_empty()` is to avoid evaluating the entire trash\n   context when the caller is only interested in whether the trash is empty\n   or not. This is especially useful for full trashes with many items.\n\n### Commit Statistics\n\n<csr-read-only-do-not-edit/>\n\n - 3 commits contributed to the release.\n - 56 days passed between releases.\n - 1 commit was understood as [conventional](https://www.conventionalcommits.org).\n - 0 issues like '(#ID)' were seen in commit messages\n\n### Commit Details\n\n<csr-read-only-do-not-edit/>\n\n<details><summary>view details</summary>\n\n * **Uncategorized**\n    - Release trash v5.2.0 ([`1a0fc59`](https://github.com/Byron/trash-rs/commit/1a0fc5908a29c7e648a76ca706e2aa2a40eedda6))\n    - Merge pull request #120 from joshuamegnauth54/feat-short-circuiting-is-empty ([`0120bbe`](https://github.com/Byron/trash-rs/commit/0120bbe66889e3659b9f09598f3567dd6c00d4b6))\n    - Short circuiting check for empty trash ([`6d59fa9`](https://github.com/Byron/trash-rs/commit/6d59fa939429d2eede8b7cf22b2e084bc3c546f4))\n</details>\n\n## 5.1.1 (2024-08-31)\n\nThis release accelerates removing large folders by removing an unnecessary recursive check.\n\n### Commit Statistics\n\n<csr-read-only-do-not-edit/>\n\n - 6 commits contributed to the release.\n - 22 days passed between releases.\n - 0 commits were understood as [conventional](https://www.conventionalcommits.org).\n - 1 unique issue was worked on: [#114](https://github.com/Byron/trash-rs/issues/114)\n\n### Commit Details\n\n<csr-read-only-do-not-edit/>\n\n<details><summary>view details</summary>\n\n * **[#114](https://github.com/Byron/trash-rs/issues/114)**\n    - Merge pull request #114 from sungsphinx/fix-fedora-atomic ([`3d95173`](https://github.com/Byron/trash-rs/commit/3d95173d19bedf18d8b5b687567707bd99871e19))\n * **Uncategorized**\n    - Release trash v5.1.1 ([`a2920fa`](https://github.com/Byron/trash-rs/commit/a2920fa50ad6dec4fc430c48b9837df2f17cd2f4))\n    - Adjust changelog prior to release ([`bc3e9c1`](https://github.com/Byron/trash-rs/commit/bc3e9c11426df512e3b056111863f8b410eaf043))\n    - Merge pull request #115 from NeumoNeumo/NeumoNeumo-patch-1 ([`df6f3b9`](https://github.com/Byron/trash-rs/commit/df6f3b99728a469f06027b2df486adc631ebc4ba))\n    - Accelerate by removing recursive renaming ([`8f8f5c0`](https://github.com/Byron/trash-rs/commit/8f8f5c06b2ce43d30c373311c643f184b7176d9f))\n    - Fix trashing files on Fedora Atomic variants ([`4d22ee4`](https://github.com/Byron/trash-rs/commit/4d22ee4852ba9b300489d332c210b920d01db8d9))\n</details>\n\n## 5.1.0 (2024-08-09)\n\n### New Features\n\n - <csr-id-791917843a988396935ceff1eb5c982da6655d80/> check for operation abort\n\n### Commit Statistics\n\n<csr-read-only-do-not-edit/>\n\n - 3 commits contributed to the release.\n - 52 days passed between releases.\n - 1 commit was understood as [conventional](https://www.conventionalcommits.org).\n - 0 issues like '(#ID)' were seen in commit messages\n\n### Commit Details\n\n<csr-read-only-do-not-edit/>\n\n<details><summary>view details</summary>\n\n * **Uncategorized**\n    - Release trash v5.1.0 ([`26e55ae`](https://github.com/Byron/trash-rs/commit/26e55aebf89ca3211787c787e8e2b22a412c4203))\n    - Merge pull request #113 from anatawa12/master ([`ca6d598`](https://github.com/Byron/trash-rs/commit/ca6d5980216eb9f2e4d709a08e0454502655e454))\n    - Check for operation abort ([`7919178`](https://github.com/Byron/trash-rs/commit/791917843a988396935ceff1eb5c982da6655d80))\n</details>\n\n## 5.0.0 (2024-06-18)\n\n<csr-id-58b99ef34a0dc6cce11fdc46c9fa18ffb013e33e/>\n\nTo support non-UTF8 encoding in paths, the `name` field changed from `String` \nto `OsString` in the `TrashItem` struct. As it's a return value, one won't see\ncode break unless `name` is actually used.\n\n### Bug Fixes\n\n - <csr-id-15a15f8ad10791318c6d9de95d4fbaefa345fb56/> Support non-Unicode paths\n   There are several spots where paths are assumed to be Unicode. However,\n   some (all?) operating systems support non-Unicode paths which causes\n   `trash-rs` to panic if encountered. I switched some of those code to use\n   `OsString`s instead of `String`s. Unfortunately, I had to add a new\n   dependency, `urlencoding`, in order to properly handle decoding non-UTF8\n   byte slices.\n   \n   As of this commit, the test suite passes and code should be ready, but I\n   will try to remove the `url` crate and use `urlencoding` in its place\n   in the next commit.\n\n### Other\n\n - <csr-id-58b99ef34a0dc6cce11fdc46c9fa18ffb013e33e/> Use objc2-foundation\n\n### Bug Fixes (BREAKING)\n\n - <csr-id-0971b8f7f0f1e20ee4356a40ae6b2ba41900c4b3/> Support non-UTF8 paths.\n   Note that this changes the type of returned paths to `OsString` from String,\n   hence the breaking change.\n\n### Commit Statistics\n\n<csr-read-only-do-not-edit/>\n\n - 11 commits contributed to the release over the course of 34 calendar days.\n - 47 days passed between releases.\n - 3 commits were understood as [conventional](https://www.conventionalcommits.org).\n - 0 issues like '(#ID)' were seen in commit messages\n\n### Commit Details\n\n<csr-read-only-do-not-edit/>\n\n<details><summary>view details</summary>\n\n * **Uncategorized**\n    - Release trash v5.0.0 ([`a754f4a`](https://github.com/Byron/trash-rs/commit/a754f4a8c62737085c31982c1025544f3f36f5e8))\n    - Prepare changelog prior to release ([`02d1a8d`](https://github.com/Byron/trash-rs/commit/02d1a8d2494f26b79b907d516c0444df16d3e55a))\n    - Support non-UTF8 paths. ([`0971b8f`](https://github.com/Byron/trash-rs/commit/0971b8f7f0f1e20ee4356a40ae6b2ba41900c4b3))\n    - Update Windows code to account for API change ([`e4b7119`](https://github.com/Byron/trash-rs/commit/e4b7119fcc369c5594e9e2b5dad8f1a6616593f7))\n    - Simplify Linux/BSD only tests for non-UTF8 paths ([`559b57b`](https://github.com/Byron/trash-rs/commit/559b57bc1497d2a49ca4f463cc27f6c94697939c))\n    - Impl test for listing invalid UTF8 trash items ([`209db9d`](https://github.com/Byron/trash-rs/commit/209db9d76de1f233b05b10f5f3f008b5968b0232))\n    - Cleanup non-Unicode support for readability ([`2f31116`](https://github.com/Byron/trash-rs/commit/2f311164ff44077dc5450ebc0f14c29f70fe57d7))\n    - Remove `url` and replace with `urlencoding` ([`67fb256`](https://github.com/Byron/trash-rs/commit/67fb2568384b7ebd96acba54e40236d9f3e9eb07))\n    - Support non-Unicode paths ([`15a15f8`](https://github.com/Byron/trash-rs/commit/15a15f8ad10791318c6d9de95d4fbaefa345fb56))\n    - Merge pull request #107 from madsmtm/objc2 ([`46585ce`](https://github.com/Byron/trash-rs/commit/46585ceacc3799f74ce9793e6d0669eb4e48b3f8))\n    - Use objc2-foundation ([`58b99ef`](https://github.com/Byron/trash-rs/commit/58b99ef34a0dc6cce11fdc46c9fa18ffb013e33e))\n</details>\n\n## 4.1.1 (2024-05-01)\n\nThis release updates the `windows` dependency (on Windows) to v0.56.\n\n### Commit Statistics\n\n<csr-read-only-do-not-edit/>\n\n - 4 commits contributed to the release.\n - 0 commits were understood as [conventional](https://www.conventionalcommits.org).\n - 0 issues like '(#ID)' were seen in commit messages\n\n### Commit Details\n\n<csr-read-only-do-not-edit/>\n\n<details><summary>view details</summary>\n\n * **Uncategorized**\n    - Release trash v4.1.1 ([`c14d904`](https://github.com/Byron/trash-rs/commit/c14d904864b4c030cc8ce9d8d394c719f40c5a5b))\n    - Update changelog prior to release. ([`47baa0e`](https://github.com/Byron/trash-rs/commit/47baa0ed3aaefa4167b1a972db54223ac710cb8d))\n    - Merge pull request #106 from YizhePKU/bump-windows ([`02f1e6c`](https://github.com/Byron/trash-rs/commit/02f1e6c5620f9a0bbeae68246cb2b180f946a1be))\n    - Bump windows crate to 0.56.0 ([`c0e0f7a`](https://github.com/Byron/trash-rs/commit/c0e0f7a6397bdb65de013ca8e2b58c6ea7ab73af))\n</details>\n\n## 4.1.0 (2024-03-19)\n\n### New Features\n\n - <csr-id-eb659cb0ef5401c7ac1fb514c5d639c10464b730/> add `os_limited::trash_folders()` for use on many unixes.\n\n### Commit Statistics\n\n<csr-read-only-do-not-edit/>\n\n - 5 commits contributed to the release.\n - 7 days passed between releases.\n - 1 commit was understood as [conventional](https://www.conventionalcommits.org).\n - 0 issues like '(#ID)' were seen in commit messages\n\n### Commit Details\n\n<csr-read-only-do-not-edit/>\n\n<details><summary>view details</summary>\n\n * **Uncategorized**\n    - Release trash v4.1.0 ([`35b549f`](https://github.com/Byron/trash-rs/commit/35b549f158428f0016a26c23249c76c6795ba9c3))\n    - Add `os_limited::trash_folders()` for use on many unixes. ([`eb659cb`](https://github.com/Byron/trash-rs/commit/eb659cb0ef5401c7ac1fb514c5d639c10464b730))\n    - Fix lint on Windows ([`daad5b7`](https://github.com/Byron/trash-rs/commit/daad5b7ad04192c7fac48b697e90ca40fb0cb94c))\n    - `trash_folders()` is invalid on Windows ([`32719fb`](https://github.com/Byron/trash-rs/commit/32719fbc82b572b77cd80d4f3645bb44ebca4640))\n    - List valid trash bin paths ([`3eba5c3`](https://github.com/Byron/trash-rs/commit/3eba5c36354eb246e74e31cab655d361313ff3e5))\n</details>\n\n## 4.0.0 (2024-03-12)\n\n### Bug Fixes (BREAKING)\n\n - <csr-id-146ea03fe1c1c168b8a6fd135d9dc5c5c93f35d5/> Assure directory deletions on Windows don't put the entire contents into the trash.\n   Instead, like on other platforms, on Windows it will now put the folder into the trash instead.\r\n   \r\n   Please note that this is not a breaking change in terms of API, but a *potentially* breaking change with older Windows versions. It's unknown if there are side-effects, as it's unknown why Windows had special behaviour previously.\n\n### Commit Statistics\n\n<csr-read-only-do-not-edit/>\n\n - 5 commits contributed to the release.\n - 28 days passed between releases.\n - 1 commit was understood as [conventional](https://www.conventionalcommits.org).\n - 0 issues like '(#ID)' were seen in commit messages\n\n### Commit Details\n\n<csr-read-only-do-not-edit/>\n\n<details><summary>view details</summary>\n\n * **Uncategorized**\n    - Release trash v4.0.0 ([`b6acac9`](https://github.com/Byron/trash-rs/commit/b6acac9ef574649e381d10e3930348b4385ff551))\n    - Assure directory deletions on Windows don't put the entire contents into the trash. ([`146ea03`](https://github.com/Byron/trash-rs/commit/146ea03fe1c1c168b8a6fd135d9dc5c5c93f35d5))\n    - Fix lint by removing unused code ([`d03934f`](https://github.com/Byron/trash-rs/commit/d03934f668d1d405c2030685c318ba19b825c74e))\n    - Attempt to fix second argument issue ([`1435c3d`](https://github.com/Byron/trash-rs/commit/1435c3d566970f5366e990cdea512e1d0a6c738d))\n    - Make delete_all not recursive ([`03ae59d`](https://github.com/Byron/trash-rs/commit/03ae59d7473746e2d704aabab0d1065d3b9a6f58))\n</details>\n\n## 3.3.1 (2024-02-12)\n\n### Bug Fixes\n\n - <csr-id-98049f1316e3902f2c9d5cd51f8de14b86ec5828/> Use `AtomicI32` instead of I64 for compatibility with `armel`\n\n### Commit Statistics\n\n<csr-read-only-do-not-edit/>\n\n - 2 commits contributed to the release.\n - 2 days passed between releases.\n - 1 commit was understood as [conventional](https://www.conventionalcommits.org).\n - 1 unique issue was worked on: [#99](https://github.com/Byron/trash-rs/issues/99)\n\n### Commit Details\n\n<csr-read-only-do-not-edit/>\n\n<details><summary>view details</summary>\n\n * **[#99](https://github.com/Byron/trash-rs/issues/99)**\n    - Use `AtomicI32` instead of I64 for compatibility with `armel` ([`98049f1`](https://github.com/Byron/trash-rs/commit/98049f1316e3902f2c9d5cd51f8de14b86ec5828))\n * **Uncategorized**\n    - Release trash v3.3.1 ([`b6e2d6c`](https://github.com/Byron/trash-rs/commit/b6e2d6c57f499a1851e8b2e4a724b1e0ef5ae54d))\n</details>\n\n## 3.3.0 (2024-02-10)\n\n### New Features\n\n - <csr-id-452be8303c797f44409b487c0cf1e6ffb2899110/> improved error granularity\n   Inform about operating-system specific errors more clearly, thus avoid degenerating error information.\n\n### Bug Fixes\n\n - <csr-id-920ff0c69f6d0309e73f86aaa437aec9508cc873/> Use `AtomicI32` in tests for compatibility with `armel` platform\n\n### Commit Statistics\n\n<csr-read-only-do-not-edit/>\n\n - 6 commits contributed to the release over the course of 5 calendar days.\n - 25 days passed between releases.\n - 2 commits were understood as [conventional](https://www.conventionalcommits.org).\n - 1 unique issue was worked on: [#99](https://github.com/Byron/trash-rs/issues/99)\n\n### Commit Details\n\n<csr-read-only-do-not-edit/>\n\n<details><summary>view details</summary>\n\n * **[#99](https://github.com/Byron/trash-rs/issues/99)**\n    - Use `AtomicI32` in tests for compatibility with `armel` platform ([`920ff0c`](https://github.com/Byron/trash-rs/commit/920ff0c69f6d0309e73f86aaa437aec9508cc873))\n * **Uncategorized**\n    - Release trash v3.3.0 ([`d0d8f26`](https://github.com/Byron/trash-rs/commit/d0d8f26030e0936aa57aa1d0d4e1a34f6a91f5b9))\n    - Improved error granularity ([`452be83`](https://github.com/Byron/trash-rs/commit/452be8303c797f44409b487c0cf1e6ffb2899110))\n    - Removed tracing. ([`2b1c9fa`](https://github.com/Byron/trash-rs/commit/2b1c9fa2a9743c1d5477bf5512ba0f260cfdacb5))\n    - Bug fix for macOS. ([`b238938`](https://github.com/Byron/trash-rs/commit/b238938d7d6387d7340f9c6a30025c9255973180))\n    - Enhanced error reporting. ([`671cef9`](https://github.com/Byron/trash-rs/commit/671cef91f4e3c216f84683e07c82c5849d641b3b))\n</details>\n\n## 3.2.1 (2024-01-15)\n\n### Bug Fixes\n\n - <csr-id-bb868d6812988b56082c2faea083617402e1a259/> find best-possible trash dir, e.g. use `/run/foo/.trash` instead of`/run/.trash` when deleting `/run/foo/bar`.\n\n### Commit Statistics\n\n<csr-read-only-do-not-edit/>\n\n - 10 commits contributed to the release.\n - 5 days passed between releases.\n - 1 commit was understood as [conventional](https://www.conventionalcommits.org).\n - 0 issues like '(#ID)' were seen in commit messages\n\n### Commit Details\n\n<csr-read-only-do-not-edit/>\n\n<details><summary>view details</summary>\n\n * **Uncategorized**\n    - Release trash v3.2.1 ([`d7abb5b`](https://github.com/Byron/trash-rs/commit/d7abb5bb735827b88479fc4879dcfcdcae6e08df))\n    - Find best-possible trash dir, e.g. use `/run/foo/.trash` instead of`/run/.trash` when deleting `/run/foo/bar`. ([`bb868d6`](https://github.com/Byron/trash-rs/commit/bb868d6812988b56082c2faea083617402e1a259))\n    - Refactor ([`8cb3f75`](https://github.com/Byron/trash-rs/commit/8cb3f7519b1294fe8b2e03c0f51fd129bb9f4cf4))\n    - Cargo fmt ([`0b42fc0`](https://github.com/Byron/trash-rs/commit/0b42fc06b44e076aa7aebaee6f8730bc762ee5ed))\n    - Use unstable sort ([`18dadef`](https://github.com/Byron/trash-rs/commit/18dadef0dd39bf3e57450fbf4a7098688fb81df0))\n    - Fixing method os ([`8ba855e`](https://github.com/Byron/trash-rs/commit/8ba855e4bf9982e8b4be993d8df59739b88d72c6))\n    - Sort mount points first ([`b2e4cf2`](https://github.com/Byron/trash-rs/commit/b2e4cf202e108bb419d7a7e5959b45408dac836c))\n    - Refactor ([`da8ce63`](https://github.com/Byron/trash-rs/commit/da8ce63afd331b4e41455be0587a2736c42815bd))\n    - Fix clippy error ([`8f74b17`](https://github.com/Byron/trash-rs/commit/8f74b1789a2257ba5a7acda560f1811df8f5f1ea))\n    - Fixing sometimes choosing incorrect mount point if substring of each other ([`1e9df03`](https://github.com/Byron/trash-rs/commit/1e9df0347cd1298844222a43a6424400e7dc787b))\n</details>\n\n## 3.2.0 (2024-01-10)\n\n<csr-id-be43b098c6c4db66f19c90471cd6ff0c066832ef/>\n\n### New Features\n\n - <csr-id-aa8e5043e285d31644e697aa264f8a11e5dfa2e8/> provide `os_limited::metadata()`.\n   Metadata is currently limited to the amount of things, like bytes or entries,\n   in the metadata item, but there is potential for adding more later.\n\n### Other\n\n - <csr-id-be43b098c6c4db66f19c90471cd6ff0c066832ef/> update ci job to use cargo-cross\n\n### Commit Statistics\n\n<csr-read-only-do-not-edit/>\n\n - 14 commits contributed to the release.\n - 2 commits were understood as [conventional](https://www.conventionalcommits.org).\n - 0 issues like '(#ID)' were seen in commit messages\n\n### Commit Details\n\n<csr-read-only-do-not-edit/>\n\n<details><summary>view details</summary>\n\n * **Uncategorized**\n    - Release trash v3.2.0 ([`03aa7ac`](https://github.com/Byron/trash-rs/commit/03aa7ac1fc279d1cb598c451d8ef342d13232489))\n    - Provide `os_limited::metadata()`. ([`aa8e504`](https://github.com/Byron/trash-rs/commit/aa8e5043e285d31644e697aa264f8a11e5dfa2e8))\n    - Refactor ([`8dad3df`](https://github.com/Byron/trash-rs/commit/8dad3dfc45657962a57a932c40bc37ea1ebe0d7f))\n    - Address review comments ([`63639c3`](https://github.com/Byron/trash-rs/commit/63639c3337cc282a1aaa69ef5afd00f8516e3dcd))\n    - Stub for get_mount_points on unsupported targets ([`fd89ea5`](https://github.com/Byron/trash-rs/commit/fd89ea5d780fa111d12fbe6644dc4153a78565c5))\n    - Windows implementation ([`1a1f75e`](https://github.com/Byron/trash-rs/commit/1a1f75e59b4c18abdf6bc8790a4e54b53dff50df))\n    - Add metadata function, implement for freedesktop ([`3bea3e2`](https://github.com/Byron/trash-rs/commit/3bea3e2f11d5def136455e7bc2377cb05b80147e))\n    - Merge pull request #92 from TD-Sky/unknown-to-fs-error ([`916d769`](https://github.com/Byron/trash-rs/commit/916d7698ebceb0529fa3c43f6baddbd4c39d55f2))\n    - Accepting generic type instead of `&Path` ([`17411be`](https://github.com/Byron/trash-rs/commit/17411be41b96f4a81df8a9cc6fa558d0d250c749))\n    - Be consistent with the style of the project ([`7ee2617`](https://github.com/Byron/trash-rs/commit/7ee26179e59c4920b83fffed20a049e9171e4878))\n    - Keep error converter function and rename it `fs_error` ([`a08118c`](https://github.com/Byron/trash-rs/commit/a08118cf2a924a3224b05d76dd5b012036ef5e05))\n    - More precise file system error ([`c51aa78`](https://github.com/Byron/trash-rs/commit/c51aa7820c70e6d5fc4d408f5c01cd4c8701c59d))\n    - Merge pull request #90 from fujiapple852/build-add-cargo-cross-ci ([`695af32`](https://github.com/Byron/trash-rs/commit/695af324e6ddaee00ea0ee5e44c7d815fd1158ec))\n    - Update ci job to use cargo-cross ([`be43b09`](https://github.com/Byron/trash-rs/commit/be43b098c6c4db66f19c90471cd6ff0c066832ef))\n</details>\n\n## 3.1.2 (2023-10-18)\n\nThis release fixes compile errors on DragonFly, a fork of FreeBSD.\n\n### Commit Statistics\n\n<csr-read-only-do-not-edit/>\n\n - 4 commits contributed to the release.\n - 0 commits were understood as [conventional](https://www.conventionalcommits.org).\n - 0 issues like '(#ID)' were seen in commit messages\n\n### Commit Details\n\n<csr-read-only-do-not-edit/>\n\n<details><summary>view details</summary>\n\n * **Uncategorized**\n    - Release trash v3.1.2 ([`609f6b3`](https://github.com/Byron/trash-rs/commit/609f6b39f6a743e1cbd9226c873b0463730e10ed))\n    - Prepare changelog ([`c81d4dd`](https://github.com/Byron/trash-rs/commit/c81d4ddccc9bd30b5baecb9c69f01437b467a703))\n    - Merge pull request #89 from jbeich/dragonfly ([`ad26100`](https://github.com/Byron/trash-rs/commit/ad261004b4fe350bf7963cc4354e4b5808c61156))\n    - Add DragonFly support via FreeBSD codepath ([`ed1984b`](https://github.com/Byron/trash-rs/commit/ed1984b923a7cdd7dbf03484d02b5da07e27779c))\n</details>\n\n## 3.1.1 (2023-10-18)\n\n### Bug Fixes\n\n - <csr-id-1a5bc2de178ca76fe06631a09305e4f014764084/> compilation on FreeBSD should work now. #(86)\n\n### Commit Statistics\n\n<csr-read-only-do-not-edit/>\n\n - 5 commits contributed to the release over the course of 9 calendar days.\n - 9 days passed between releases.\n - 1 commit was understood as [conventional](https://www.conventionalcommits.org).\n - 0 issues like '(#ID)' were seen in commit messages\n\n### Commit Details\n\n<csr-read-only-do-not-edit/>\n\n<details><summary>view details</summary>\n\n * **Uncategorized**\n    - Release trash v3.1.1 ([`aa6fd20`](https://github.com/Byron/trash-rs/commit/aa6fd20ec75585f7cda5e8745a6cafd2c5b26e91))\n    - Compilation on FreeBSD should work now. #(86) ([`1a5bc2d`](https://github.com/Byron/trash-rs/commit/1a5bc2de178ca76fe06631a09305e4f014764084))\n    - Update freedesktop.rs ([`aa7b7fd`](https://github.com/Byron/trash-rs/commit/aa7b7fd66573631cf17b031b90e5e0139f0fdab6))\n    - Restore statfs for FreeBSD & OpenBSD ([`1562113`](https://github.com/Byron/trash-rs/commit/1562113e12f9020a9c3f866e5adf5e913f4040e6))\n    - Update version in README so it matches the latest published one ([`50e8030`](https://github.com/Byron/trash-rs/commit/50e80304845cbae953b4ecf370c715d728ea9958))\n</details>\n\n## 3.1.0 (2023-10-08)\n\n<csr-id-554c2735c8dd924fd7cebe863b529d91bb0cac0d/>\n\n### New Features\n\n - <csr-id-24e0cb6f9fe15a0db1609e04cda6446e3335f89b/> compatibility with OpenBSD and NetBSD\n - <csr-id-0789b23c6c8e21bc1493455beaca75d46e0aa575/> allow passing in items' ownership or reference\n\n### Other\n\n - <csr-id-554c2735c8dd924fd7cebe863b529d91bb0cac0d/> describe how to retry restoring when encountering `RestoreCollision` error\n\n### Commit Statistics\n\n<csr-read-only-do-not-edit/>\n\n - 10 commits contributed to the release.\n - 88 days passed between releases.\n - 3 commits were understood as [conventional](https://www.conventionalcommits.org).\n - 1 unique issue was worked on: [#84](https://github.com/Byron/trash-rs/issues/84)\n\n### Commit Details\n\n<csr-read-only-do-not-edit/>\n\n<details><summary>view details</summary>\n\n * **[#84](https://github.com/Byron/trash-rs/issues/84)**\n    - Compatibility with OpenBSD and NetBSD ([`24e0cb6`](https://github.com/Byron/trash-rs/commit/24e0cb6f9fe15a0db1609e04cda6446e3335f89b))\n * **Uncategorized**\n    - Release trash v3.1.0 ([`be17cd2`](https://github.com/Byron/trash-rs/commit/be17cd20bb32ab00ceb72cd9afc3ddaed01cacdb))\n    - Bump minor version to indicate a feature change ([`ddb9917`](https://github.com/Byron/trash-rs/commit/ddb99171715727a3339d4a9e2f07a517037b01db))\n    - Merge pull request #81 from TD-Sky/re-restore ([`c87a946`](https://github.com/Byron/trash-rs/commit/c87a9467235e6208e2268d392ac61f332b4d1d09))\n    - Test edition bump ([`b77bd6d`](https://github.com/Byron/trash-rs/commit/b77bd6d32f8d44f59b9fe53806248d0b0860aa18))\n    - Bump version ([`75cc270`](https://github.com/Byron/trash-rs/commit/75cc27093d01628fb79acb1432c8ccdd66d86b2f))\n    - Update dependencies ([`7d1e2bb`](https://github.com/Byron/trash-rs/commit/7d1e2bb0a51d88033428aad62bf87e400c2a334d))\n    - One step closer ([`aee3dce`](https://github.com/Byron/trash-rs/commit/aee3dceac5575e4a2a23633ec5f3da5da79d9e89))\n    - Allow passing in items' ownership or reference ([`0789b23`](https://github.com/Byron/trash-rs/commit/0789b23c6c8e21bc1493455beaca75d46e0aa575))\n    - Describe how to retry restoring when encountering `RestoreCollision` error ([`554c273`](https://github.com/Byron/trash-rs/commit/554c2735c8dd924fd7cebe863b529d91bb0cac0d))\n</details>\n\n## 3.0.6 (2023-07-12)\n\n### Bug Fixes\n\n - <csr-id-3f5e8427cbf299322d66b358ec3fa61ca4a5d66c/> don't recurse into symlink when trashing a directory on windows.\n\n### Commit Statistics\n\n<csr-read-only-do-not-edit/>\n\n - 4 commits contributed to the release.\n - 5 days passed between releases.\n - 1 commit was understood as [conventional](https://www.conventionalcommits.org).\n - 0 issues like '(#ID)' were seen in commit messages\n\n### Commit Details\n\n<csr-read-only-do-not-edit/>\n\n<details><summary>view details</summary>\n\n * **Uncategorized**\n    - Release trash v3.0.6 ([`450edc1`](https://github.com/Byron/trash-rs/commit/450edc1a0d372ae450daf3aec33aabedd3efde3d))\n    - Merge branch 'fix-symlink-traversal' ([`43d44cb`](https://github.com/Byron/trash-rs/commit/43d44cbe0979c92cbc117723387d762ecd9d3191))\n    - Don't recurse into symlink when trashing a directory on windows. ([`3f5e842`](https://github.com/Byron/trash-rs/commit/3f5e8427cbf299322d66b358ec3fa61ca4a5d66c))\n    - Inform about reason for yanking v3.0.5 ([`112e99e`](https://github.com/Byron/trash-rs/commit/112e99ecfd485c5115323b185efc5979eae26edc))\n</details>\n\n## 3.0.5 (2023-07-06)\n\nYANKED: It was discovered that symlinks aren't handled correctly, which can lead to removals of unrelated directory trees.\n\n### Bug Fixes\n\n - <csr-id-c1feece952dcd70163ed06ac2af79fdbb3d692bc/> On **windows**, `delete()` will now delete recursively like on the other platforms.\n   Note that the current implementation may consume a lot of memory as it will traverse the\n   entire directory structure once while storing each path for later trashing.\n\n### Commit Statistics\n\n<csr-read-only-do-not-edit/>\n\n - 9 commits contributed to the release over the course of 1 calendar day.\n - 4 days passed between releases.\n - 1 commit was understood as [conventional](https://www.conventionalcommits.org).\n - 0 issues like '(#ID)' were seen in commit messages\n\n### Commit Details\n\n<csr-read-only-do-not-edit/>\n\n<details><summary>view details</summary>\n\n * **Uncategorized**\n    - Release trash v3.0.5 ([`4655a07`](https://github.com/Byron/trash-rs/commit/4655a0723ab4209872d4037be89d2b0876a70731))\n    - Upgrade serial-test crate ([`0354d36`](https://github.com/Byron/trash-rs/commit/0354d36b7f870317cf57711624fd31054ffc946e))\n    - On **windows**, `delete()` will now delete recursively like on the other platforms. ([`c1feece`](https://github.com/Byron/trash-rs/commit/c1feece952dcd70163ed06ac2af79fdbb3d692bc))\n    - Refactor ([`41edcdf`](https://github.com/Byron/trash-rs/commit/41edcdfc8bdeb410b45ae636da25e3c7275a8a8c))\n    - Removed self as parameter only used in recurssion. ([`a7619c1`](https://github.com/Byron/trash-rs/commit/a7619c13215daaf88316f7e1876cf59c96491cf4))\n    - Reorganized code for cross-platform compatibility. ([`1c09e48`](https://github.com/Byron/trash-rs/commit/1c09e48c7977704b1a8d67078c84ed30b17c983a))\n    - Use recursive deletion on Windows by default. ([`46e0697`](https://github.com/Byron/trash-rs/commit/46e0697c649f9e8184654e47f18f6b2930b6bd67))\n    - Removed Windows only restriction for recursive deletion test. ([`d363dd8`](https://github.com/Byron/trash-rs/commit/d363dd840a0d35348b427ff6d1f6def568e008ed))\n    - Merge branch 'Byron:master' into bug/windows_nonempty_folder ([`0f4b2c8`](https://github.com/Byron/trash-rs/commit/0f4b2c81a209f70592b33675144c1d7922433741))\n</details>\n\n## 3.0.4 (2023-07-01)\n\n### Bug Fixes\n\n - <csr-id-55b0d5c86e2608552836ec0bf3e9aa0ce8c303b8/> Don't use 'oldtime' feature of `chrono` by controlling exactly which features are enabled.\n   That particular feature has [a rustsec advisory](https://rustsec.org/advisories/RUSTSEC-2020-0071) up\n   against it.\n\n### Commit Statistics\n\n<csr-read-only-do-not-edit/>\n\n - 3 commits contributed to the release.\n - 19 days passed between releases.\n - 1 commit was understood as [conventional](https://www.conventionalcommits.org).\n - 1 unique issue was worked on: [#75](https://github.com/Byron/trash-rs/issues/75)\n\n### Commit Details\n\n<csr-read-only-do-not-edit/>\n\n<details><summary>view details</summary>\n\n * **[#75](https://github.com/Byron/trash-rs/issues/75)**\n    - Don't use 'oldtime' feature of `chrono` by controlling exactly which features are enabled. ([`55b0d5c`](https://github.com/Byron/trash-rs/commit/55b0d5c86e2608552836ec0bf3e9aa0ce8c303b8))\n * **Uncategorized**\n    - Release trash v3.0.4 ([`a2343c2`](https://github.com/Byron/trash-rs/commit/a2343c2692aa8d6b5fc8684a654349a14094486b))\n    - Don't use `oldtime` feature of chrono ([`fad81a4`](https://github.com/Byron/trash-rs/commit/fad81a4992fe053e30113f9ab0c7001d12b1ec17))\n</details>\n\n## 3.0.3 (2023-06-11)\n\n### Bug Fixes\n\n - <csr-id-aa8cd7b05f8f0641d7fd73328619c2c45c7e050c/> disallow empty paths from being deleted.\n   Previously passing \"\" for deletion wuold delete the current working directory\n   as it would canonicalize any input path without validating the path is non-empty.\n\n### Commit Statistics\n\n<csr-read-only-do-not-edit/>\n\n - 3 commits contributed to the release over the course of 11 calendar days.\n - 25 days passed between releases.\n - 1 commit was understood as [conventional](https://www.conventionalcommits.org).\n - 1 unique issue was worked on: [#73](https://github.com/Byron/trash-rs/issues/73)\n\n### Commit Details\n\n<csr-read-only-do-not-edit/>\n\n<details><summary>view details</summary>\n\n * **[#73](https://github.com/Byron/trash-rs/issues/73)**\n    - Disallow empty paths from being deleted. ([`aa8cd7b`](https://github.com/Byron/trash-rs/commit/aa8cd7b05f8f0641d7fd73328619c2c45c7e050c))\n * **Uncategorized**\n    - Release trash v3.0.3 ([`841bc13`](https://github.com/Byron/trash-rs/commit/841bc1388959ab3be4f05ad1a90b03aa6bcaea67))\n    - Fix issue #70.Added recursive removal on Windows. ([`05e0cf4`](https://github.com/Byron/trash-rs/commit/05e0cf442354b3b2b9ecfb8ed2b165b8547bc794))\n</details>\n\n## 3.0.2 (2023-05-17)\n\n### Bug Fixes\n\n - <csr-id-75daea606cbdbc4d15a514bb674591d986e57490/> broken symlinks won't cause failure anymore on freedesktop platforms.\n\n### Commit Statistics\n\n<csr-read-only-do-not-edit/>\n\n - 4 commits contributed to the release.\n - 1 commit was understood as [conventional](https://www.conventionalcommits.org).\n - 0 issues like '(#ID)' were seen in commit messages\n\n### Commit Details\n\n<csr-read-only-do-not-edit/>\n\n<details><summary>view details</summary>\n\n * **Uncategorized**\n    - Release trash v3.0.2 ([`e20fe6a`](https://github.com/Byron/trash-rs/commit/e20fe6ae94aa73d07ff31d911ad9ecf98b17f3a8))\n    - Broken symlinks won't cause failure anymore on freedesktop platforms. ([`75daea6`](https://github.com/Byron/trash-rs/commit/75daea606cbdbc4d15a514bb674591d986e57490))\n    - Make `virtually_exists` private ([`454a77e`](https://github.com/Byron/trash-rs/commit/454a77e667b00a0aeb492dab9a81e69e77178802))\n    - Operate broken symbolic links is safe now ([`9198013`](https://github.com/Byron/trash-rs/commit/919801376bc44fa3c4948349690c7e912be2dd3a))\n</details>\n\n## 3.0.1 (2023-01-30)\n\n<csr-id-865a7c6d688cc6dd00dc8b16cd0e4a4fd60d953c/>\n\n### Chore\n\n - <csr-id-865a7c6d688cc6dd00dc8b16cd0e4a4fd60d953c/> bump `windows` crate to 0.44\n   Merge branch 'bump-windows-0.44'\n\n### Commit Statistics\n\n<csr-read-only-do-not-edit/>\n\n - 5 commits contributed to the release over the course of 61 calendar days.\n - 64 days passed between releases.\n - 1 commit was understood as [conventional](https://www.conventionalcommits.org).\n - 0 issues like '(#ID)' were seen in commit messages\n\n### Thanks Clippy\n\n<csr-read-only-do-not-edit/>\n\n[Clippy](https://github.com/rust-lang/rust-clippy) helped 1 time to make code idiomatic. \n\n### Commit Details\n\n<csr-read-only-do-not-edit/>\n\n<details><summary>view details</summary>\n\n * **Uncategorized**\n    - Release trash v3.0.1 ([`eef463a`](https://github.com/Byron/trash-rs/commit/eef463aca73d5c623dd7b52bcb8b01b3b3d76b15))\n    - Bump `windows` crate to 0.44 ([`865a7c6`](https://github.com/Byron/trash-rs/commit/865a7c6d688cc6dd00dc8b16cd0e4a4fd60d953c))\n    - Thanks clippy ([`37dedb3`](https://github.com/Byron/trash-rs/commit/37dedb35ed71e4c43af3af7d39ae5d722c8b5a94))\n    - Update `windows` crate to `0.44` ([`1a347fc`](https://github.com/Byron/trash-rs/commit/1a347fcce57627dd71979ca8399dedba149f9569))\n    - Add `Error::FileSystem` ([`575b8ed`](https://github.com/Byron/trash-rs/commit/575b8ed4c78b76e9ecdf4fe877b6e32cd74cf166))\n</details>\n\n## 3.0.0 (2022-11-27)\n\n<csr-id-a024b44b6e1cd4a357ffabda8f31e82dcc7e78cb/>\n\n### Chore (BREAKING)\n\n - <csr-id-a024b44b6e1cd4a357ffabda8f31e82dcc7e78cb/> Upgrade from `windows` v0.37 to v0.43.\n\n### Commit Statistics\n\n<csr-read-only-do-not-edit/>\n\n - 5 commits contributed to the release.\n - 1 commit was understood as [conventional](https://www.conventionalcommits.org).\n - 0 issues like '(#ID)' were seen in commit messages\n\n### Commit Details\n\n<csr-read-only-do-not-edit/>\n\n<details><summary>view details</summary>\n\n * **Uncategorized**\n    - Release trash v3.0.0 ([`1fb5ad6`](https://github.com/Byron/trash-rs/commit/1fb5ad628868f1480510efe10bdc021ce65b4f32))\n    - Upgrade from `windows` v0.37 to v0.43. ([`a024b44`](https://github.com/Byron/trash-rs/commit/a024b44b6e1cd4a357ffabda8f31e82dcc7e78cb))\n    - Fix Clippy failures on Linux ([`538dea0`](https://github.com/Byron/trash-rs/commit/538dea0e77af2ed70c6f8b17c86b956b8caa6459))\n    - Upgrade windows crate from v0.37 to v0.43 ([`48cdc67`](https://github.com/Byron/trash-rs/commit/48cdc67d09e20f8d07438e45d3ceefd23da6af9a))\n    - Derive Clone for TrashItem ([`fcf6bb5`](https://github.com/Byron/trash-rs/commit/fcf6bb5eded49de4fedb40513c949f11c6da0b12))\n</details>\n\n## 2.1.5 (2022-07-05)\n\n### Bug Fixes\n\n - <csr-id-67244ba2e4c71135b0ab36331dc465615e23211a/> Make chrono a default-enabled optional feature.\n   This allows to turn chrono support off without actually affecting the\n   ability to trash and restore items.\n   `chrono` still has issues to dubious local-time support which relies\n   on a c-library function that can cause undefined behaviour as it\n   accesses an environment variable in a non-threadsafe fashion.\n\n### Commit Statistics\n\n<csr-read-only-do-not-edit/>\n\n - 5 commits contributed to the release.\n - 40 days passed between releases.\n - 1 commit was understood as [conventional](https://www.conventionalcommits.org).\n - 1 unique issue was worked on: [#39](https://github.com/Byron/trash-rs/issues/39)\n\n### Commit Details\n\n<csr-read-only-do-not-edit/>\n\n<details><summary>view details</summary>\n\n * **[#39](https://github.com/Byron/trash-rs/issues/39)**\n    - Make chrono a default-enabled optional feature. ([`67244ba`](https://github.com/Byron/trash-rs/commit/67244ba2e4c71135b0ab36331dc465615e23211a))\n * **Uncategorized**\n    - Release trash v2.1.5 ([`266d780`](https://github.com/Byron/trash-rs/commit/266d7808d2309f0911ebc6c8a0189511c4e77835))\n    - Improve CI stage names; fix feature configuration on windows ([`5591fda`](https://github.com/Byron/trash-rs/commit/5591fdab131de1f6fa5a04bef44d7b394d3f7f72))\n    - Silence clippy ([`d13be48`](https://github.com/Byron/trash-rs/commit/d13be48c59a1a0df3e37aa676cda06cc1f48ece9))\n    - Add rust-cache for faster builds ([`676a43f`](https://github.com/Byron/trash-rs/commit/676a43f7ec7c116a7b40dcf4236bf2156a88fd04))\n</details>\n\n## 2.1.4 (2022-05-25)\n\n### Fixes\n\n- upgrade the `windows` crate to v0.37 to resolve [a build issue](https://github.com/Byron/trash-rs/issues/39) and lay the foundation\n  for more regular updates of the windows support.\n\n### Commit Statistics\n\n<csr-read-only-do-not-edit/>\n\n - 3 commits contributed to the release.\n - 8 days passed between releases.\n - 0 commits were understood as [conventional](https://www.conventionalcommits.org).\n - 2 unique issues were worked on: [#39](https://github.com/Byron/trash-rs/issues/39), [#51](https://github.com/Byron/trash-rs/issues/51)\n\n### Commit Details\n\n<csr-read-only-do-not-edit/>\n\n<details><summary>view details</summary>\n\n * **[#39](https://github.com/Byron/trash-rs/issues/39)**\n    - Prepare changelog ([`7816e07`](https://github.com/Byron/trash-rs/commit/7816e07bab38a79aa6f5d705a4fb40f330ac155b))\n * **[#51](https://github.com/Byron/trash-rs/issues/51)**\n    - Upgrade windows crate ([`d18f9d4`](https://github.com/Byron/trash-rs/commit/d18f9d435d2f76fb982f4bfcc98d5ccfe57c092c))\n * **Uncategorized**\n    - Release trash v2.1.4 ([`17d162f`](https://github.com/Byron/trash-rs/commit/17d162fcf7a53d3d82961a448d4b70b4eb596825))\n</details>\n\n## 2.1.3 (2022-05-17)\n\n### Fixes\n\n- include `windows` crate only on windows for reduced CI build times from ~9s to ~4s.\n\n### Commit Statistics\n\n<csr-read-only-do-not-edit/>\n\n - 5 commits contributed to the release.\n - 3 days passed between releases.\n - 0 commits were understood as [conventional](https://www.conventionalcommits.org).\n - 1 unique issue was worked on: [#5050505050](https://github.com/Byron/trash-rs/issues/5050505050)\n\n### Commit Details\n\n<csr-read-only-do-not-edit/>\n\n<details><summary>view details</summary>\n\n * **[#5050505050](https://github.com/Byron/trash-rs/issues/5050505050)**\n    - Update changelog ([`8e64f34`](https://github.com/Byron/trash-rs/commit/8e64f34bd6f1b823353fae61d60f765615be0024))\n * **Uncategorized**\n    - Release trash v2.1.3 ([`f98bc45`](https://github.com/Byron/trash-rs/commit/f98bc45199cbb24525d2b41c748b9547f3c3ac44))\n    - Merge pull request #50 from rgwood/windows-dep ([`883c5a4`](https://github.com/Byron/trash-rs/commit/883c5a48c8ad07bef4f7e1822a31761211cf304d))\n    - Add names to CI steps ([`ef7003a`](https://github.com/Byron/trash-rs/commit/ef7003a4f83910f318b05a3f51960a33fd444915))\n    - Only use `windows` crate on Windows ([`e088525`](https://github.com/Byron/trash-rs/commit/e088525047a14a531d414fe9cd098e08fe2ff79f))\n</details>\n\n## 2.1.2 (2022-05-13)\n\n### Bug Fixes\n\n - <csr-id-367cf5f2616f1f49b115189b3bede3bb99f8324d/> avoid inconsistency when using relative paths in trashed file info.\n   We use absolute paths now without trying to generate a relative path\n   based on some top directory as the latter seems to be causing\n   inconsistencies on some linux distros, as the restore path ends\n   up being incorrect.\n   \n   Rather go with the absolute truth and don't fiddle with path\n   transformations at all to make it work everywhere.\n\n### Commit Statistics\n\n<csr-read-only-do-not-edit/>\n\n - 2 commits contributed to the release.\n - 2 days passed between releases.\n - 1 commit was understood as [conventional](https://www.conventionalcommits.org).\n - 1 unique issue was worked on: [#39](https://github.com/Byron/trash-rs/issues/39)\n\n### Commit Details\n\n<csr-read-only-do-not-edit/>\n\n<details><summary>view details</summary>\n\n * **[#39](https://github.com/Byron/trash-rs/issues/39)**\n    - Avoid inconsistency when using relative paths in trashed file info. ([`367cf5f`](https://github.com/Byron/trash-rs/commit/367cf5f2616f1f49b115189b3bede3bb99f8324d))\n * **Uncategorized**\n    - Release trash v2.1.2 ([`e0746f0`](https://github.com/Byron/trash-rs/commit/e0746f0df91623231d13531ec33632f03f0588ac))\n</details>\n\n## 2.1.1 (2022-05-10)\n\n### Bug Fixes\n\n - <csr-id-dcda6df8cefa06bf08e7eca7db2c34b050c2d913/> Properly reconstruct paths when restoring files on freedesktop if those were relative.\n   \n   Previously it would be unable to reconstruct original paths if the trash\n   directory was on a mount point due to a 'split brain' of sorts.\n   \n   When trashing files it would create original path information based\n   on them being relative to a mount point, but when restoring them\n   it would reconstruct them to be relative to the trash top level\n   directory.\n   \n   Now the reconstruction happens against to mount point itself which makes\n   restoration match.\n\n### Commit Statistics\n\n<csr-read-only-do-not-edit/>\n\n - 7 commits contributed to the release over the course of 2 calendar days.\n - 3 days passed between releases.\n - 1 commit was understood as [conventional](https://www.conventionalcommits.org).\n - 1 unique issue was worked on: [#47](https://github.com/Byron/trash-rs/issues/47)\n\n### Commit Details\n\n<csr-read-only-do-not-edit/>\n\n<details><summary>view details</summary>\n\n * **[#47](https://github.com/Byron/trash-rs/issues/47)**\n    - Properly reconstruct paths when restoring files on freedesktop if those were relative ([`dcda6df`](https://github.com/Byron/trash-rs/commit/dcda6df8cefa06bf08e7eca7db2c34b050c2d913))\n    - Somewhat hard-code special case for fedora ([`90f0f9b`](https://github.com/Byron/trash-rs/commit/90f0f9b035678efe51a20d4a47fd09158b8ef455))\n    - Proper cleanup after potential assertion failure ([`1f3a600`](https://github.com/Byron/trash-rs/commit/1f3a6005eabd4629fe0743030a612a29fcb7d80c))\n    - Remove unused trait ([`ac913d8`](https://github.com/Byron/trash-rs/commit/ac913d83ed9344d8ed8e18957b2e99136e0b29c1))\n * **Uncategorized**\n    - Release trash v2.1.1 ([`50ab31a`](https://github.com/Byron/trash-rs/commit/50ab31afa9f641a16a1ab50bf1ea8f8bacb0330f))\n    - Update changelog ([`98d32c8`](https://github.com/Byron/trash-rs/commit/98d32c88e85b2b40ea17d372c427ef168ad80b30))\n    - More robust removal of test files in failure case on os specific tests ([`3f6502d`](https://github.com/Byron/trash-rs/commit/3f6502db02e09e36c2fbce2fea054a9a2b9229de))\n</details>\n\n## 2.1.0 (2022-05-06)\n\n### Fixes\n\n- Leading directories are now created on linux to avoid errors when trashing nested directories.\n\n### Commit Statistics\n\n<csr-read-only-do-not-edit/>\n\n - 8 commits contributed to the release.\n - 103 days passed between releases.\n - 0 commits were understood as [conventional](https://www.conventionalcommits.org).\n - 2 unique issues were worked on: [#45](https://github.com/Byron/trash-rs/issues/45), [#47](https://github.com/Byron/trash-rs/issues/47)\n\n### Commit Details\n\n<csr-read-only-do-not-edit/>\n\n<details><summary>view details</summary>\n\n * **[#45](https://github.com/Byron/trash-rs/issues/45)**\n    - Reproduce issue with lack of leading directories and fix it ([`d5b6faa`](https://github.com/Byron/trash-rs/commit/d5b6faa81d59ccd6185261399bc7449432b9deb6))\n * **[#47](https://github.com/Byron/trash-rs/issues/47)**\n    - Try to reproduce ([`8eba501`](https://github.com/Byron/trash-rs/commit/8eba50155e006cf923d8bb77fea88cde6395512e))\n * **Uncategorized**\n    - Release trash v2.1.0 ([`b3a4547`](https://github.com/Byron/trash-rs/commit/b3a45471ce5fcd489a096145e06ac663ed854747))\n    - Prepare upcoming release ([`e3bbb6b`](https://github.com/Byron/trash-rs/commit/e3bbb6be1072675c331176e8d0585cc67910d17b))\n    - Merge branch 'refactor-tests' ([`0e90cac`](https://github.com/Byron/trash-rs/commit/0e90cace515344c68eead8e59180487561849289))\n    - Assure tests don't race ([`d9778ba`](https://github.com/Byron/trash-rs/commit/d9778ba1912c5764cbfaa9c46b2bba5c3d1899eb))\n    - Thanks clippy ([`220a216`](https://github.com/Byron/trash-rs/commit/220a2164e86bf7f0e1e636d24595b6ce4182de14))\n    - Move all intergration tests into corresponding location ([`e5dc62e`](https://github.com/Byron/trash-rs/commit/e5dc62ee2b363a11e57e4aad2c1d128d2f8961e2))\n</details>\n\n## 2.0.4 (2022-01-23)\n\nWe detected the possibility of UB in the Linux and FreeBSD versions of `get_mount_points()` and reduced the likelihood\nof it happening in a multi-threaded environment by synchronizing access. You can read more about the state of\na more permanent fix [in the tracking issue](https://github.com/Byron/trash-rs/issues/42).\n\nAll previous 2.0.* releases which contained this function were yanked from crates-io.\n\n### Fixes\n\n* Make internal `get_mount_points()` thread-safe to reduce chance of UB greatly. \n  This may reduce performance of crates that are using trash from multiple threads somewhat, as a part of the operation\n  is now synchronized.\n* Fix build on FreeBSD, handle UB similarly to the above.\n\n### Commit Statistics\n\n<csr-read-only-do-not-edit/>\n\n - 10 commits contributed to the release over the course of 30 calendar days.\n - 30 days passed between releases.\n - 0 commits were understood as [conventional](https://www.conventionalcommits.org).\n - 0 issues like '(#ID)' were seen in commit messages\n\n### Commit Details\n\n<csr-read-only-do-not-edit/>\n\n<details><summary>view details</summary>\n\n * **Uncategorized**\n    - Release trash v2.0.4 ([`c7edcb1`](https://github.com/Byron/trash-rs/commit/c7edcb175dd125bda5b15e726fc7b36eae3c89a4))\n    - Prepare changelog for next release ([`b65f574`](https://github.com/Byron/trash-rs/commit/b65f574d5aeb8ea3a918e8288c8d13dd082b8f0a))\n    - Add Mutex to linux version of get_mount_points(); document UB chance in lib.rs ([`c5c9c5e`](https://github.com/Byron/trash-rs/commit/c5c9c5e40d345736df7d078bf8e6991acc701e83))\n    - Use Mutex to prevent concurrent access to getmntinfo ([`5c8e0ce`](https://github.com/Byron/trash-rs/commit/5c8e0ce1c700c68fc63c612cc0ea5b3191f6b0d1))\n    - Merge pull request #43 from wezm/num-threads-freebsd ([`8f10c85`](https://github.com/Byron/trash-rs/commit/8f10c852bd9ec2e69353a0dd5397fab1c4ba089f))\n    - Fix build on FreeBSD after refactor ([`f3d31e5`](https://github.com/Byron/trash-rs/commit/f3d31e54dd93c22605e8178958a1caa503be19f4))\n    - Use `num_threads()` to avoid UB in FreeBSD version of get_mount_points() ([`3c153ae`](https://github.com/Byron/trash-rs/commit/3c153ae2f1ed92d8a240a742e90fcb0e483284b8))\n    - Refactor ([`92ab7b9`](https://github.com/Byron/trash-rs/commit/92ab7b91adcde3305cc3e319fb0b59feff8f81cc))\n    - Add BSD compatible implementation of get_mount_points ([`82d2132`](https://github.com/Byron/trash-rs/commit/82d2132f8e1323272f5d8e1f54112589f75c3202))\n    - Run `cargo-diet` for a more minimal crates package ([`561f21d`](https://github.com/Byron/trash-rs/commit/561f21d9de2a56cb0f0c87002d2ead3dc8ca6ab2))\n</details>\n\n## 2.0.3 (2021-12-23)\n\n### Bug Fixes\n\n - <csr-id-cb5b6176aa296853f7a6e3cfa177e1235acaa903/> let dependency specification in Cargo.toml match cfg directives in code\n   This fixes [issue 40](https://github.com/Byron/trash-rs/issues/40).\n\n### Commit Statistics\n\n<csr-read-only-do-not-edit/>\n\n - 6 commits contributed to the release.\n - 1 commit was understood as [conventional](https://www.conventionalcommits.org).\n - 2 unique issues were worked on: [#37](https://github.com/Byron/trash-rs/issues/37), [#40](https://github.com/Byron/trash-rs/issues/40)\n\n### Commit Details\n\n<csr-read-only-do-not-edit/>\n\n<details><summary>view details</summary>\n\n * **[#37](https://github.com/Byron/trash-rs/issues/37)**\n    - Fix some clippy warnings ([`3c566ef`](https://github.com/Byron/trash-rs/commit/3c566ef417350b75e02ea80be51165815014ec74))\n * **[#40](https://github.com/Byron/trash-rs/issues/40)**\n    - Let dependency specification in Cargo.toml match cfg directives in code ([`cb5b617`](https://github.com/Byron/trash-rs/commit/cb5b6176aa296853f7a6e3cfa177e1235acaa903))\n * **Uncategorized**\n    - Release trash v2.0.3 ([`6864e34`](https://github.com/Byron/trash-rs/commit/6864e340890f247f675982744396bae8ea856565))\n    - Disable lint for platforms where it matters ([`b4add86`](https://github.com/Byron/trash-rs/commit/b4add8643cc0659b4318f3113a197794cb0032b0))\n    - Update changelog with `cargo changelog` ([`932cea4`](https://github.com/Byron/trash-rs/commit/932cea48c6ceba2adf0b824c3236b330e232de12))\n    - Add Rust CI status badge ([`b94fce2`](https://github.com/Byron/trash-rs/commit/b94fce2bf74dd5c1ee66735eca32d6ace5db83ea))\n</details>\n\n## v2.0.2 (2021-08-18)\n\n### Changed\n\n- Fix failing to delete files on some freedesktop (eg Linux) systems when the home was not mounted at the root.\n- The `list` function now returns an empty list if there is no trash directory (it used to return an error).\n- Fix for test failing on Linux environments that don't have a desktop environment (more specifically don't have a tool like `gio`)\n\n### Commit Statistics\n\n<csr-read-only-do-not-edit/>\n\n - 10 commits contributed to the release over the course of 104 calendar days.\n - 108 days passed between releases.\n - 0 commits were understood as [conventional](https://www.conventionalcommits.org).\n - 3 unique issues were worked on: [#34](https://github.com/Byron/trash-rs/issues/34), [#35](https://github.com/Byron/trash-rs/issues/35), [#36](https://github.com/Byron/trash-rs/issues/36)\n\n### Commit Details\n\n<csr-read-only-do-not-edit/>\n\n<details><summary>view details</summary>\n\n * **[#34](https://github.com/Byron/trash-rs/issues/34)**\n    - Fix for failing to delete files on Freedesktop systems (eg Linux) ([`bd8679c`](https://github.com/Byron/trash-rs/commit/bd8679c39b163e87c33bec7a669cebdc9ff37358))\n * **[#35](https://github.com/Byron/trash-rs/issues/35)**\n    - Fix for test failing on some Linux environments ([`9da7b59`](https://github.com/Byron/trash-rs/commit/9da7b590a23940693ad2809ca28c7ec904a574a6))\n * **[#36](https://github.com/Byron/trash-rs/issues/36)**\n    - Avoid error from the list function ([`cb59c7e`](https://github.com/Byron/trash-rs/commit/cb59c7e09f6409881c24131bf25cb89930203655))\n * **Uncategorized**\n    - Update version ([`600b59c`](https://github.com/Byron/trash-rs/commit/600b59c3422d5f6f51aca27b867a64650f06c865))\n    - Update windows-rs ([`2b64f38`](https://github.com/Byron/trash-rs/commit/2b64f3832781b2715688c236194392ec31b2c5d3))\n    - Some minor improvements ([`0e281bc`](https://github.com/Byron/trash-rs/commit/0e281bcbfe0bb50d8b68782cdd1da7d7e74355f7))\n    - Merge pull request #29 from ArturKovacs/update-win-rs ([`2a1eaf8`](https://github.com/Byron/trash-rs/commit/2a1eaf8630b2c49b06e28d323d85e95dd0dd514a))\n    - Revert the build script ([`1b4a501`](https://github.com/Byron/trash-rs/commit/1b4a501685fa02e80579fa825156ec1077a39519))\n    - Ran cargo fmt ([`42884ae`](https://github.com/Byron/trash-rs/commit/42884aec20b1ad1b59213b465b34b600a8bf4cff))\n    - Update windows-rs and fix for cross compilation ([`681d7b4`](https://github.com/Byron/trash-rs/commit/681d7b49140c0fd1db33628ee66bf432a5818eac))\n</details>\n\n## v2.0.1 (2021-05-02)\n\n### Changed\n\n- Fix not being able to trash any item on some systems.\n\n### Commit Statistics\n\n<csr-read-only-do-not-edit/>\n\n - 4 commits contributed to the release.\n - 11 days passed between releases.\n - 0 commits were understood as [conventional](https://www.conventionalcommits.org).\n - 0 issues like '(#ID)' were seen in commit messages\n\n### Commit Details\n\n<csr-read-only-do-not-edit/>\n\n<details><summary>view details</summary>\n\n * **Uncategorized**\n    - Update version number ([`6f11f8d`](https://github.com/Byron/trash-rs/commit/6f11f8dd58190afd00b15211584c15919477ad07))\n    - Merge pull request #26 from ArturKovacs/fix-25 ([`13a36ce`](https://github.com/Byron/trash-rs/commit/13a36cec736c8127676f90f45f0c3941590aca1d))\n    - Update changelog ([`812b574`](https://github.com/Byron/trash-rs/commit/812b574f08c73b3b26cd3c1b4b761e209f9544df))\n    - Fix for error when trashing an item ([`a876d0f`](https://github.com/Byron/trash-rs/commit/a876d0f92e48cae89ac4815187b0bdff7634148d))\n</details>\n\n## v2.0.0 (2021-04-20)\n\n### Changed\n\n- The \"Linux\" implementation was replaced by a custom Freedesktop implementation.\n\n### Added\n\n- `list`, `purge_all`, and `restore_all` to Windows and Freedesktop\n\n### Commit Statistics\n\n<csr-read-only-do-not-edit/>\n\n - 32 commits contributed to the release over the course of 4 calendar days.\n - 0 commits were understood as [conventional](https://www.conventionalcommits.org).\n - 0 issues like '(#ID)' were seen in commit messages\n\n### Commit Details\n\n<csr-read-only-do-not-edit/>\n\n<details><summary>view details</summary>\n\n * **Uncategorized**\n    - Merge pull request #11 from ArturKovacs/v2-dev ([`3dcac24`](https://github.com/Byron/trash-rs/commit/3dcac248a029a7f78d41fcdf1645f7ad6dc5bc4d))\n    - Merge branch 'v2-dev' of https://github.com/ArturKovacs/trash-rs into v2-dev ([`e9047a3`](https://github.com/Byron/trash-rs/commit/e9047a364ba531c720d356867649d13dcac1f918))\n    - Update the version number ([`28307a6`](https://github.com/Byron/trash-rs/commit/28307a662c99b150268d7b20d946ee9bd51baa75))\n    - Add test for NsFileManager delete method ([`8aea6ef`](https://github.com/Byron/trash-rs/commit/8aea6ef92cde4daa6336424c91192d80ca62bde6))\n    - Run cargo fmt ([`3ce2160`](https://github.com/Byron/trash-rs/commit/3ce2160a87de0b88901048063cc5d7b5aa8455f2))\n    - Fix clippy error ([`2158550`](https://github.com/Byron/trash-rs/commit/21585507786f8ab0426afbe2dab7dd738b6c8c84))\n    - More tweaks ([`ee2527a`](https://github.com/Byron/trash-rs/commit/ee2527a78134e408f73347b4f6bfaf43d2f9fb29))\n    - Minor tweaks ([`1c43fe7`](https://github.com/Byron/trash-rs/commit/1c43fe7e7de12e1ecd551633b62c6105cfa4019d))\n    - Update readme, add changelog ([`4c1ece3`](https://github.com/Byron/trash-rs/commit/4c1ece3db523de11546f487efc8ae39b01b35b5c))\n    - Add more logging to the freedesktop implementation ([`a94b4ce`](https://github.com/Byron/trash-rs/commit/a94b4ce160a4926d3cf777517ee6768a364b8310))\n    - Remove the Filesystem error kind ([`1138d8c`](https://github.com/Byron/trash-rs/commit/1138d8ccbc6e5bfd84daca86aacd9902326ecd3a))\n    - Don't run the CI for the nightly Rust ([`afa33ba`](https://github.com/Byron/trash-rs/commit/afa33badbab2473f649881d78c9acc49de376697))\n    - Fix clippy error ([`a182fbc`](https://github.com/Byron/trash-rs/commit/a182fbc7cd685151ff109c6a76623e27c2f666af))\n    - Fix freedesktop errors ([`afd17c3`](https://github.com/Byron/trash-rs/commit/afd17c3efd939283d20d2e130cfeb4b609adad42))\n    - Update the list example ([`a18d055`](https://github.com/Byron/trash-rs/commit/a18d055e684589eb2f9176ae6536ec433b023dc1))\n    - Fix warnings on macOS ([`f12cea9`](https://github.com/Byron/trash-rs/commit/f12cea96221d52c3809408009894fa28ac3b8a0c))\n    - Tweaked tests and documentation ([`330b1ec`](https://github.com/Byron/trash-rs/commit/330b1ec4f99376666ed48fb125e95e1928b1be0d))\n    - Documentation improvements ([`18337bf`](https://github.com/Byron/trash-rs/commit/18337bf73c3e89a90e793ba9b6e9741d558a019a))\n    - Rename `extra` to `os_limited` and other tweaks ([`29b6b11`](https://github.com/Byron/trash-rs/commit/29b6b113ffa4af0eeab6b147f5e19cee605e3274))\n    - Update the macOS backend ([`eff82e4`](https://github.com/Byron/trash-rs/commit/eff82e4e11195eb6871a08b5f607e9a0ab921a4c))\n    - Update the macos backend ([`e739014`](https://github.com/Byron/trash-rs/commit/e739014e08c4b883ff9350cbc1beef0e2da10797))\n    - Removed the silly PlatformApi error ([`61fa667`](https://github.com/Byron/trash-rs/commit/61fa667246cc83960d68f6ccfe2f29080ddb4186))\n    - Implement restore_all for windows ([`baa5171`](https://github.com/Byron/trash-rs/commit/baa5171c83a9f007e55dc2c2f412b7aad08815cc))\n    - Implement purge_all on Windows ([`9fc224d`](https://github.com/Byron/trash-rs/commit/9fc224db47b3bd4c34d6a34c3251dc495a076fe9))\n    - Remove the WinNull workaround ([`d8ab41f`](https://github.com/Byron/trash-rs/commit/d8ab41f97a25fdf81a20f0641c8a472e948a7f35))\n    - Ran cargo fmt ([`cf13e78`](https://github.com/Byron/trash-rs/commit/cf13e78e303da0a99fb01c801a030a9f1ff9d8af))\n    - Implement the `list` function for windows ([`6e77795`](https://github.com/Byron/trash-rs/commit/6e777954438acf6db2f0089d6a34fa0b77a60ab1))\n    - Implemented the delete function using `windows-rs` ([`218d0d0`](https://github.com/Byron/trash-rs/commit/218d0d00492833fdb301aeaaf1164b837a2a3af4))\n    - Fix example ([`69dbe38`](https://github.com/Byron/trash-rs/commit/69dbe386af48a1fb3d7a85fbb09235f263a9a5a2))\n    - Don't track the lockfile ([`942108d`](https://github.com/Byron/trash-rs/commit/942108d378c92edf387788d572f91808683b0019))\n    - Merge branch 'master' into v2-dev ([`c2d7a35`](https://github.com/Byron/trash-rs/commit/c2d7a35b7584f17e724853e6e3fdff9efeff5835))\n    - Minor adjustments ([`314e808`](https://github.com/Byron/trash-rs/commit/314e80823fccbb2b78558e9bea2f086e77fba26a))\n</details>\n\n## v1.3.0 (2021-01-24)\n\n### Commit Statistics\n\n<csr-read-only-do-not-edit/>\n\n - 18 commits contributed to the release.\n - 0 commits were understood as [conventional](https://www.conventionalcommits.org).\n - 0 issues like '(#ID)' were seen in commit messages\n\n### Commit Details\n\n<csr-read-only-do-not-edit/>\n\n<details><summary>view details</summary>\n\n * **Uncategorized**\n    - Fix for clippy error ([`a728dce`](https://github.com/Byron/trash-rs/commit/a728dce614add4f3aa10c2b2721a4eb2a9e57cca))\n    - Increment version and update fmt ([`6d2270a`](https://github.com/Byron/trash-rs/commit/6d2270a0cbcebd8ebcc67a0278f81271e355bc63))\n    - Ran fmt and fix for warning ([`ff7cf3b`](https://github.com/Byron/trash-rs/commit/ff7cf3b09916c04ff861047db2b5005621d0597a))\n    - Fix for path canonicalization ([`5dfe5dc`](https://github.com/Byron/trash-rs/commit/5dfe5dc0beaa29a537808d017e5852ad976644e4))\n    - Merge pull request #23 from cbr9/optimize--get-desktop-environment ([`c887b6b`](https://github.com/Byron/trash-rs/commit/c887b6bdbe707320aada2478e5033f101e86aba6))\n    - Optimized get_desktop_environment() ([`a0a7fbb`](https://github.com/Byron/trash-rs/commit/a0a7fbbcd3e0e60b4b59066b65f3f4443ab57dbf))\n    - Update readme ([`30427f0`](https://github.com/Byron/trash-rs/commit/30427f04121bfbd8526d06deaf1d04cc7db145b0))\n    - Oops that Path wasn't completely unused after all ([`ba850ee`](https://github.com/Byron/trash-rs/commit/ba850eee27299e2aca0b1fc634f566f91a40e43b))\n    - Fixed compile warning and ran rustfmt ([`c556d28`](https://github.com/Byron/trash-rs/commit/c556d284887ae72bf95b6fafba357a59f982204d))\n    - Removed Cargo.lock from the gitignore. ([`cdde3a7`](https://github.com/Byron/trash-rs/commit/cdde3a7a34671b9ba26231361319f19459b75567))\n    - Implement `delete` and `delete_all` for macOS ([`cb564ef`](https://github.com/Byron/trash-rs/commit/cb564ef6efcd770cf96c527624da38b14db4b6ff))\n    - Updated readme ([`7a298be`](https://github.com/Byron/trash-rs/commit/7a298be45e22943206617eff9fbc2eca1234223c))\n    - Implement `delete` and `delete_all` for windows. ([`d9a25c8`](https://github.com/Byron/trash-rs/commit/d9a25c8f6addf87eb177184f24a444835fad0b4a))\n    - Add `delete` functions for Linux ([`fedeb83`](https://github.com/Byron/trash-rs/commit/fedeb8350625f252510ae5a2c5bb26fb74876b49))\n    - Update to the readme, incorporating some suggestions by Caleb Bassi ([`9bddccc`](https://github.com/Byron/trash-rs/commit/9bddccc2e8e368f7278135e60d41da601fa20aa4))\n    - Merge pull request #18 from cjbassi/rename-files ([`c49b496`](https://github.com/Byron/trash-rs/commit/c49b4961b1e83548777ea0a24cd99c7e6c6660fe))\n    - Rename readme and license files ([`5a9a5a6`](https://github.com/Byron/trash-rs/commit/5a9a5a66b53803b037636febc9265b66bcfc7334))\n    - Adds a deprecated attribute to the `is_implemented` function. ([`386db96`](https://github.com/Byron/trash-rs/commit/386db96e8eebed0b60d79ac055e8e312f01a605c))\n</details>\n\n## v1.1.0 (2020-08-12)\n\n### Commit Statistics\n\n<csr-read-only-do-not-edit/>\n\n - 2 commits contributed to the release.\n - 87 days passed between releases.\n - 0 commits were understood as [conventional](https://www.conventionalcommits.org).\n - 1 unique issue was worked on: [#17](https://github.com/Byron/trash-rs/issues/17)\n\n### Commit Details\n\n<csr-read-only-do-not-edit/>\n\n<details><summary>view details</summary>\n\n * **[#17](https://github.com/Byron/trash-rs/issues/17)**\n    - Implement std::error::Error for trash::Error ([`8765acf`](https://github.com/Byron/trash-rs/commit/8765acf6ef7a93db322baabb40df1edfc405b437))\n * **Uncategorized**\n    - Increment minor version number ([`281bb93`](https://github.com/Byron/trash-rs/commit/281bb931159f22da85f4f23fcee92cc96e8a28e7))\n</details>\n\n## v1.0.1 (2020-05-16)\n\n<csr-id-576fad719cb240203dec030890d54fe416a42edd/>\n\n### Refactor\n\n - <csr-id-576fad719cb240203dec030890d54fe416a42edd/> port mac implementation to work with v2\n   Updates the existing Mac implementation to compile with v2 of the\n   library. Does not add any new functionality other defining required\n   methods.\n   \n   Tests fail for methods relating to `list`, `purge_all`, or\n   `restore_all`, which are unimplemented.\n\n### Commit Statistics\n\n<csr-read-only-do-not-edit/>\n\n - 59 commits contributed to the release.\n - 218 days passed between releases.\n - 1 commit was understood as [conventional](https://www.conventionalcommits.org).\n - 0 issues like '(#ID)' were seen in commit messages\n\n### Commit Details\n\n<csr-read-only-do-not-edit/>\n\n<details><summary>view details</summary>\n\n * **Uncategorized**\n    - Update readme and increment the patch field in the version number. ([`217b473`](https://github.com/Byron/trash-rs/commit/217b4739d84827744a0b23a23b344e2118ac6f5b))\n    - Merge pull request #15 from myfreeweb/bsd ([`5af79ab`](https://github.com/Byron/trash-rs/commit/5af79aba26d767b1be1c816d18ff3ef7a7b3301d))\n    - Build \"linux\" module on *BSD (any non-macOS unix) ([`9e38ff8`](https://github.com/Byron/trash-rs/commit/9e38ff8ea89a70c9c3c87369413b3f798baa727d))\n    - Fix clippy warnings on linux. ([`0731a64`](https://github.com/Byron/trash-rs/commit/0731a6403f0854ee7098d4cd534bb435684ffd50))\n    - Merge branch 'master' of https://github.com/ArturKovacs/trash ([`1e43dc1`](https://github.com/Byron/trash-rs/commit/1e43dc1682e95ff497d6ce5d4b3185336227506f))\n    - Add .vscode to gitignore ([`f63f7ce`](https://github.com/Byron/trash-rs/commit/f63f7ce308dd3dad6c64e39d210891cc237704a8))\n    - Fix for clippy warning. ([`80eba00`](https://github.com/Byron/trash-rs/commit/80eba00727a72e47b016cda028a6ede7d218085f))\n    - Ran `cargo fmt`. ([`b753689`](https://github.com/Byron/trash-rs/commit/b7536891e17e796ab17a4070100024f1754e3ba0))\n    - Add tests for Windows and MacOS as well as the nightly toolchain. ([`29876c7`](https://github.com/Byron/trash-rs/commit/29876c7f96144c82f8bee565c80d433beaba548d))\n    - Default rust workflow (GitHub Actions) ([`cf7d22f`](https://github.com/Byron/trash-rs/commit/cf7d22fd2db44f52bbc48041edee66be391b2911))\n    - Merge branch 'master' into v2-dev ([`32de332`](https://github.com/Byron/trash-rs/commit/32de3324618619ffefe0cc7ed3c16bb8df647caf))\n    - Merge branch 'master' into v2-dev ([`b3ea819`](https://github.com/Byron/trash-rs/commit/b3ea819c48c619ff0b41df8357da0c4cd1da8d67))\n    - Remove Azure test ([`3e7db4f`](https://github.com/Byron/trash-rs/commit/3e7db4f45f81232567b9beb65860dd4a8651a6fa))\n    - Add GitHub Actions test ([`0c8b1fa`](https://github.com/Byron/trash-rs/commit/0c8b1fa561a574678d4db38fc3f0a10b470aca38))\n    - Fix wording in Readme ([`7025e10`](https://github.com/Byron/trash-rs/commit/7025e102110a00a290619a32822a7bbd823e3925))\n    - Update Readme to reflect the state of development. ([`a31a944`](https://github.com/Byron/trash-rs/commit/a31a94487c46e8ac2b886f59b79b5aa0be195fb8))\n    - Merge pull request #9 from NilsIrl/patch-1 ([`02bb739`](https://github.com/Byron/trash-rs/commit/02bb73950db23156680a2273498a1e815ab6fa3d))\n    - Fix typo ([`6c4d650`](https://github.com/Byron/trash-rs/commit/6c4d650fd8dd2346e20d37a17d6db9e4afd2ce77))\n    - Added test cases and extra documentation. The test cases cover empty input for  `purge_all` and `restore_all`. ([`c275449`](https://github.com/Byron/trash-rs/commit/c275449adde03da9d7ba3064f4b72c9e816c2979))\n    - Moved Linux and Windows specific features to a mod ([`0e2fc93`](https://github.com/Byron/trash-rs/commit/0e2fc93f8bb5c17a9c5c89849453b04344995cab))\n    - Refined windows implementation. Added error kind `RestoreTwins`. ([`d105553`](https://github.com/Byron/trash-rs/commit/d1055538c16e318247b2817cce85ec522b2163a2))\n    - Ran `cargo fmt` ([`743b2f3`](https://github.com/Byron/trash-rs/commit/743b2f3f889b0519275b9d52475b0b68c661382d))\n    - Merge branch 'v2-mac' into v2-dev ([`62a7218`](https://github.com/Byron/trash-rs/commit/62a7218644511d75e7b4d162c70aac2f1a4625e9))\n    - Merge branch 'v2-dev' of https://github.com/ArturKovacs/trash into v2-dev ([`90d3fa6`](https://github.com/Byron/trash-rs/commit/90d3fa62eb7ca525e6fc45c48cdeba95322a7416))\n    - Removed the two previously added errors. Replaced `ZeroMountPointsFound` and `CantOpenMountPointsFile` with `panic!` after coming across https://lukaskalbertodt.github.io/2019/11/14/thoughts-on-error-handling-in-rust.html and reading http://joeduffyblog.com/2016/02/07/the-error-model/ ([`fa03282`](https://github.com/Byron/trash-rs/commit/fa0328204662ea871336087738642a6c9dece33e))\n    - No need for those parentheses ([`534677e`](https://github.com/Byron/trash-rs/commit/534677ed4c23316f956dc43e84f16ca68f744331))\n    - Merge branch 'v2-dev' of https://github.com/ArturKovacs/trash into v2-dev ([`00fc235`](https://github.com/Byron/trash-rs/commit/00fc235235d4f00e34ffb8cd04b975157037ab91))\n    - Add missing error kinds. `ZeroMountPointsFound` and `CantOpenMountPointsFile` were added. ([`6c79f7d`](https://github.com/Byron/trash-rs/commit/6c79f7d95604c68363cd710f99150e655152dbc4))\n    - Improve collision handling and add collision test. ([`6db249b`](https://github.com/Byron/trash-rs/commit/6db249bef9529eab4f8325dbc36e84a47000525b))\n    - Merge pull request #7 from ArturKovacs/v2-linux ([`b957f38`](https://github.com/Byron/trash-rs/commit/b957f3894b6d06b42cc48824a125825b954496c4))\n    - Remove debug lines. ([`3ab9217`](https://github.com/Byron/trash-rs/commit/3ab9217280b6656b4a861e61cc590c326c008d67))\n    - Fix creating the home trash folder. ([`10364c6`](https://github.com/Byron/trash-rs/commit/10364c64508acd4fb564cc761bc746eb2d9dd4b1))\n    - Create home trash if doesn't yet exist. Also added debug print line numbers. ([`e1b2aae`](https://github.com/Byron/trash-rs/commit/e1b2aaeb8fd00d07be250208767ba85217d55010))\n    - Attempt to add RUST_BACKTRACE=1 again. ([`7bd6023`](https://github.com/Byron/trash-rs/commit/7bd6023710e60ddbf6f9f15bcf1bd714f6aaedd9))\n    - Merge branch 'v2-dev' into v2-linux ([`303a274`](https://github.com/Byron/trash-rs/commit/303a274989d0a6b6faf7eab3f0ae9149e6e86d71))\n    - Added RUST_BACKTRACE=1 to test. ([`c1cb106`](https://github.com/Byron/trash-rs/commit/c1cb10611be762cb13fce4fd38c39961ad84317a))\n    - Merge branch 'v2-dev' into v2-linux ([`ecf521f`](https://github.com/Byron/trash-rs/commit/ecf521fbccaae27b4eb160598448341d5e8b7700))\n    - Add ability to trash items from an external drive. ([`16f0ee1`](https://github.com/Byron/trash-rs/commit/16f0ee19beaf116b9dfd44de06b272f8b62fb3fd))\n    - Added a partialy implementaiton of `remove_all`. Can't remove from non-root devices or partitions. ([`1e03167`](https://github.com/Byron/trash-rs/commit/1e031679d6a3c0cd8a780014d3331bd4fd8bcd1d))\n    - Steps towards implementing `remove_all`. ([`20ba354`](https://github.com/Byron/trash-rs/commit/20ba354399d2ce96e7ee624bbf2a03407df163db))\n    - Fix for `list` failing on Linux. This happened because `list` on Linux didn't handle paralell threads manipulating the trash correctly. ([`d6cb6ba`](https://github.com/Byron/trash-rs/commit/d6cb6bac6758a0020b88ef5632bf9f06f748f7ca))\n    - Merge pull request #6 from ayazhafiz/refactor/mac2 ([`adf0ea4`](https://github.com/Byron/trash-rs/commit/adf0ea4fab0ace99b443c5498ba5495c89abcd30))\n    - Remove the Cirrus CI config. ([`fd597fc`](https://github.com/Byron/trash-rs/commit/fd597fc852eddb23472276be6c638e6e40281f67))\n    - Port mac implementation to work with v2 ([`576fad7`](https://github.com/Byron/trash-rs/commit/576fad719cb240203dec030890d54fe416a42edd))\n    - Add MacOS and Linux as targets for CI tests. ([`e409a98`](https://github.com/Byron/trash-rs/commit/e409a983b64d36c2b585ecdb5374357a34f5da53))\n    - Fix OS setup in Azure's config. ([`08db817`](https://github.com/Byron/trash-rs/commit/08db8172080832727bd5002e394054c34c5147ea))\n    - Update Azure's target operating systems. ([`cfb25b6`](https://github.com/Byron/trash-rs/commit/cfb25b6e0d9e8fd00379871760430009c52289cd))\n    - Add Azure Pipelines CI. ([`760dfa6`](https://github.com/Byron/trash-rs/commit/760dfa64e53a2e1228230c073bd551acb868b286))\n    - Add Cirrus CI test. ([`e5c22c4`](https://github.com/Byron/trash-rs/commit/e5c22c4567e2f289a918affc46fa923a962799cf))\n    - Added implementation of purge_all for Linux. Also ran rustfmt and created a rustfmt config. ([`a90f9bf`](https://github.com/Byron/trash-rs/commit/a90f9bfa0d19fd6fad88c91bbd0e6a46c4661a0e))\n    - Now using the url crate for parsing the original location on Linux. ([`02ffe0b`](https://github.com/Byron/trash-rs/commit/02ffe0b336136c730213670d4c5b6eb04addaa55))\n    - Add `list` implementation for linux. ([`5c73fea`](https://github.com/Byron/trash-rs/commit/5c73fea22341c520103564953535c84b7271fc4e))\n    - Add note about coming features in version 2 to the Readme. ([`dddbe25`](https://github.com/Byron/trash-rs/commit/dddbe25171e6f93ffd2b80627d25e8313ff21498))\n    - Improve the Error type and add `create_remove_empty_folder` test. ([`a940b66`](https://github.com/Byron/trash-rs/commit/a940b66abea7769aba8e0b1d99995b8174239877))\n    - Changed `std::mem::uninit` and `std::mem::zeroed` to `std::mem::MaybeUninit`. Plus ran Rustfmt. ([`632a6fb`](https://github.com/Byron/trash-rs/commit/632a6fb31fc4c4bee751f0537ca317fe9f933f5c))\n    - Now `purge_all` doesn't show a dialog on windows. ([`07e3bc2`](https://github.com/Byron/trash-rs/commit/07e3bc25832a49af13ee3c2a1fdc1f425fce8805))\n    - Fix `purge_all` and `restore_all` reading invvalid memory and not executing the operation on the requested items. Add test cases for `purge_all` and `restore_all`. Test are now thread safe. ([`22d5181`](https://github.com/Byron/trash-rs/commit/22d51813759c129e87625eef5d068a1481bfbdb8))\n    - Implement `purge_all` and `restore_all` for Windows. ([`e06c825`](https://github.com/Byron/trash-rs/commit/e06c825e93ce9774733cfcf539e3c7e928cdb8cc))\n    - Run rust fmt. Implement `list` for Windows. ([`3f29636`](https://github.com/Byron/trash-rs/commit/3f29636a978fd5a462db1588040d794d81648be7))\n</details>\n\n## v1.0.0 (2019-10-11)\n\n<csr-id-576fad719cb240203dec030890d54fe416a42edd/>\n\n### Refactor\n\n - <csr-id-576fad719cb240203dec030890d54fe416a42edd/> port mac implementation to work with v2\n   Updates the existing Mac implementation to compile with v2 of the\n   library. Does not add any new functionality other defining required\n   methods.\n   \n   Tests fail for methods relating to `list`, `purge_all`, or\n   `restore_all`, which are unimplemented.\n\n### New Features\n\n - <csr-id-d68cc2aedee5e8316117bec257975da30cbd7483/> implementation for macOS\n   Moves files to trash on macOS by executing an AppleScript command to\n   delete all requested paths.\n\n### Commit Statistics\n\n<csr-read-only-do-not-edit/>\n\n - 19 commits contributed to the release over the course of 99 calendar days.\n - 1 commit was understood as [conventional](https://www.conventionalcommits.org).\n - 0 issues like '(#ID)' were seen in commit messages\n\n### Commit Details\n\n<csr-read-only-do-not-edit/>\n\n<details><summary>view details</summary>\n\n * **Uncategorized**\n    - Updated version number and readme ([`79ee69e`](https://github.com/Byron/trash-rs/commit/79ee69e3e12a9a66146897ab432f29eaa8ac2d28))\n    - Merge pull request #1 from ayazhafiz/feat/mac ([`48a6b11`](https://github.com/Byron/trash-rs/commit/48a6b11cae520ca1b60c42270912402c1d51c018))\n    - Implementation for macOS ([`d68cc2a`](https://github.com/Byron/trash-rs/commit/d68cc2aedee5e8316117bec257975da30cbd7483))\n    - Fix wrong code references in the linux implementation. ([`037fed8`](https://github.com/Byron/trash-rs/commit/037fed8ae6b5ed76cec00037cdc8340d7787d7cb))\n    - Add docs badge to readme ([`88261d5`](https://github.com/Byron/trash-rs/commit/88261d5af0b165a06483ea07e5aa378d2223d067))\n    - Increment version number ([`f758543`](https://github.com/Byron/trash-rs/commit/f75854358b7c8dea23aec6f40362fab4039d9659))\n    - Improve readme. Add remove_all function to mac as unimplemented. ([`2850270`](https://github.com/Byron/trash-rs/commit/2850270004cea47718b18aa3d3b290263ba7b8e3))\n    - Merge branch 'master' of https://github.com/ArturKovacs/trash ([`a651d0f`](https://github.com/Byron/trash-rs/commit/a651d0f3b7c261da9bd2fd65f16166b43d63abf3))\n    - Add folder remove test ([`b7bb22f`](https://github.com/Byron/trash-rs/commit/b7bb22f7b21027ad73d53eded480921f3346a14c))\n    - Updated the Cargo.toml ([`1ec1ef9`](https://github.com/Byron/trash-rs/commit/1ec1ef96b941bbab6672a86b24b0e23afdfe2165))\n    - Add license ([`1725e61`](https://github.com/Byron/trash-rs/commit/1725e612e103dda2e574543ea90821934ed46ae6))\n    - Add doc comments ([`b16a1d3`](https://github.com/Byron/trash-rs/commit/b16a1d3dff8c3b4d9b24b6dc87c9e3781b667c58))\n    - Fix Windows compile error. ([`15e801e`](https://github.com/Byron/trash-rs/commit/15e801e1a242e1b0263fe854e6c9d58a68774dd0))\n    - Add the `remove_all` function. ([`f033dc3`](https://github.com/Byron/trash-rs/commit/f033dc308ee061adc579f7426697c5cb3c280956))\n    - Minor refactoring. ([`9c7363d`](https://github.com/Byron/trash-rs/commit/9c7363dbeae19edb8079a57fcc93d012c8064ef0))\n    - Add Linux support. ([`0429d3b`](https://github.com/Byron/trash-rs/commit/0429d3bf1e7f09e97eae4aefde0cb8c8e283b235))\n    - Fixed platform specific compilation ([`84cba5b`](https://github.com/Byron/trash-rs/commit/84cba5b11acf02d5ba99d776529b93bac54f8094))\n    - Changed required winapi version. ([`d49bce8`](https://github.com/Byron/trash-rs/commit/d49bce8d346e10c08910520383ed4054a3948535))\n    - Initial ([`4c23314`](https://github.com/Byron/trash-rs/commit/4c233148288711419a04fdfa96e36dcb77f0469f))\n</details>\n\n"
  },
  {
    "path": "Cargo.toml",
    "content": "[package]\nname = \"trash\"\nversion = \"5.2.6\"\nauthors = [\"Artur Kovacs <kovacs.artur.barnabas@gmail.com>\"]\nlicense = \"MIT\"\nreadme = \"README.md\"\ndescription = \"A library for moving files and folders to the Recycle Bin\"\nkeywords = [\"remove\", \"trash\", \"rubbish\", \"recycle\", \"bin\"]\nrepository = \"https://github.com/ArturKovacs/trash\"\nedition = \"2021\"\nrust-version = \"1.85.0\"\ninclude = [\"src/**/*\", \"LICENSE.txt\", \"README.md\", \"CHANGELOG.md\", \"build.rs\"]\n\n[features]\ndefault = [\"coinit_apartmentthreaded\", \"chrono\"]\ncoinit_apartmentthreaded = []\ncoinit_multithreaded = []\ncoinit_disable_ole1dde = []\ncoinit_speed_over_memory = []\n\n[dependencies]\nlog = \"0.4\"\n\n[dev-dependencies]\nserial_test = { version = \"2.0.0\", default-features = false }\nchrono = { version = \"0.4.31\", default-features = false, features = [\"clock\"] }\nonce_cell = \"1.18.0\"\nenv_logger = \"0.10.0\"\ntempfile = \"3.8.0\"\ndefer = \"0.2.1\"\n\n[target.'cfg(target_os = \"linux\")'.dev-dependencies]\ntestcontainers = \"0.23\"\ntokio = { version = \"1\", features = [\"macros\", \"rt-multi-thread\", \"time\"] }\n\n\n[target.'cfg(target_os = \"macos\")'.dependencies]\nobjc2 = \"0.6.2\"\nobjc2-foundation = { version = \"0.3.2\", default-features = false, features = [\n    \"std\",\n    \"NSError\",\n    \"NSFileManager\",\n    \"NSString\",\n    \"NSURL\",\n] }\npercent-encoding = \"2.3.1\"\n\n[target.'cfg(all(unix, not(target_os = \"macos\"), not(target_os = \"ios\"), not(target_os = \"android\")))'.dependencies]\nchrono = { version = \"0.4.31\", optional = true, default-features = false, features = [\n    \"clock\",\n] }\nlibc = \"0.2.149\"\nscopeguard = \"1.2.0\"\nurlencoding = \"2.1.3\"\nonce_cell = \"1.18.0\"\n\n[target.'cfg(any(target_os = \"dragonfly\", target_os = \"freebsd\", target_os = \"netbsd\", target_os = \"openbsd\"))'.dependencies]\nonce_cell = \"1.7.2\"\n\n[target.'cfg(windows)'.dependencies]\nwindows = { version = \"0.56.0\", features = [\n    \"Win32_Foundation\",\n    \"Win32_Storage_EnhancedStorage\",\n    \"Win32_System_Com_StructuredStorage\",\n    \"Win32_System_SystemServices\",\n    \"Win32_UI_Shell_PropertiesSystem\",\n] }\nscopeguard = \"1.2.0\"\n\n# workaround for https://github.com/cross-rs/cross/issues/1345\n[package.metadata.cross.target.x86_64-unknown-netbsd]\npre-build = [\n    \"mkdir -p /tmp/netbsd\",\n    \"curl https://cdn.netbsd.org/pub/NetBSD/NetBSD-9.2/amd64/binary/sets/base.tar.xz -O\",\n    \"tar -C /tmp/netbsd -xJf base.tar.xz\",\n    \"cp /tmp/netbsd/usr/lib/libexecinfo.so /usr/local/x86_64-unknown-netbsd/lib\",\n    \"rm base.tar.xz\",\n    \"rm -rf /tmp/netbsd\",\n]\n"
  },
  {
    "path": "LICENSE.txt",
    "content": "Copyright 2019 Artúr Barnabás Kovács\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and\nassociated documentation files (the \"Software\"), to deal in the Software without restriction,\nincluding without limitation the rights to use, copy, modify, merge, publish, distribute,\nsublicense, and/or sell copies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or\nsubstantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT\nNOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\nNONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,\nDAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT\nOF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "\n[![Crates.io](https://img.shields.io/crates/v/trash?color=mediumvioletred)](https://crates.io/crates/trash)\n[![Docs.rs](https://docs.rs/trash/badge.svg)](https://docs.rs/trash)\n[![CI](https://github.com/Byron/trash-rs/actions/workflows/rust.yml/badge.svg)](https://github.com/Byron/trash-rs/actions/workflows/rust.yml)\n\n\n## About\n\nThe `trash` is a Rust library for moving files and folders to the operating system's Recycle Bin or Trash or Rubbish Bin or what have you :D\n\nThe library supports Windows, macOS, and all FreeDesktop Trash compliant environments (including GNOME, KDE, XFCE, and more). \nSee more about the FreeDesktop Trash implementation in the `freedesktop.rs` file.\n\n## Usage\n\n```toml\n# In Cargo.toml\n[dependencies]\ntrash = \"3\"\n```\n\n```rust\n// In main.rs\nuse std::fs::File;\nuse trash;\n\nfn main() {\n    // Let's create and remove a single file\n    File::create_new(\"remove-me\").unwrap();\n    trash::delete(\"remove-me\").unwrap();\n    assert!(File::open(\"remove-me\").is_err());\n\n    // Now let's remove multiple files at once\n    let the_others = [\"remove-me-too\", \"dont-forget-about-me-either\"];\n    for name in the_others.iter() {\n        File::create_new(name).unwrap();\n    }\n    trash::delete_all(&the_others).unwrap();\n    for name in the_others.iter() {\n        assert!(File::open(name).is_err());\n    }\n}\n```\n"
  },
  {
    "path": "examples/delete_method.rs",
    "content": "#[cfg(not(target_os = \"macos\"))]\nfn main() {\n    println!(\"This example is only available on macOS\");\n}\n\n#[cfg(target_os = \"macos\")]\nfn main() {\n    use std::fs::File;\n    use trash::{\n        macos::{DeleteMethod, TrashContextExtMacos},\n        TrashContext,\n    };\n\n    env_logger::init();\n\n    let mut trash_ctx = TrashContext::default();\n    trash_ctx.set_delete_method(DeleteMethod::NsFileManager);\n\n    let path = \"this_file_was_deleted_using_the_ns_file_manager\";\n    File::create_new(path).unwrap();\n    trash_ctx.delete(path).unwrap();\n    assert!(File::open(path).is_err());\n}\n"
  },
  {
    "path": "examples/hello.rs",
    "content": "use std::fs::File;\n\nfn main() {\n    // Let's create and remove a single file\n    File::create_new(\"remove-me\").unwrap();\n    trash::delete(\"remove-me\").unwrap();\n    assert!(File::open(\"remove-me\").is_err());\n\n    // Now let's remove multiple files at once\n    let the_others = [\"remove-me-too\", \"dont-forget-about-me-either\"];\n    for name in the_others.iter() {\n        File::create_new(name).unwrap();\n    }\n    trash::delete_all(&the_others).unwrap();\n    for name in the_others.iter() {\n        assert!(File::open(name).is_err());\n    }\n}\n"
  },
  {
    "path": "examples/list.rs",
    "content": "#[cfg(not(any(\n    target_os = \"windows\",\n    all(unix, not(target_os = \"macos\"), not(target_os = \"ios\"), not(target_os = \"android\"))\n)))]\nfn main() {\n    println!(\"This is currently only supported on Windows, Linux, and other Freedesktop.org compliant OSes\");\n}\n\n#[cfg(any(\n    target_os = \"windows\",\n    all(unix, not(target_os = \"macos\"), not(target_os = \"ios\"), not(target_os = \"android\"))\n))]\nfn main() {\n    use chrono::{DateTime, Local, NaiveDateTime, Utc};\n    let trash_items = trash::os_limited::list().unwrap();\n\n    let now = Local::now();\n    let long_time_ago = now - chrono::Duration::days(42);\n    let old_count = trash_items\n        .iter()\n        .filter(|item| {\n            let naive_deletion_utc = NaiveDateTime::from_timestamp(item.time_deleted, 0);\n            let deletion = DateTime::<Utc>::from_utc(naive_deletion_utc, Utc);\n            deletion < long_time_ago\n        })\n        .count();\n\n    println!(\"There are {} old items in your trash.\", old_count);\n}\n"
  },
  {
    "path": "examples/metadata.rs",
    "content": "#[cfg(not(any(\n    target_os = \"windows\",\n    all(unix, not(target_os = \"macos\"), not(target_os = \"ios\"), not(target_os = \"android\"))\n)))]\nfn main() {\n    println!(\"This is currently only supported on Windows, Linux, and other Freedesktop.org compliant OSes\");\n}\n\n#[cfg(any(\n    target_os = \"windows\",\n    all(unix, not(target_os = \"macos\"), not(target_os = \"ios\"), not(target_os = \"android\"))\n))]\nfn main() {\n    let trash_items = trash::os_limited::list().unwrap();\n\n    for item in trash_items {\n        let metadata = trash::os_limited::metadata(&item).unwrap();\n        println!(\"{:?}: {:?}\", item, metadata);\n    }\n}\n"
  },
  {
    "path": "examples/trash.rs",
    "content": "/// Simple CLI that calls `trash::delete`.\n///\n/// Usage: trash delete <path>\n///\n/// Exits 0 on success, 1 on trash error, 2 on bad arguments.\n///\n/// Note: This binary is used by the freedesktop_tests integration tests.\nfn main() {\n    let args: Vec<String> = std::env::args().collect();\n    if args.len() < 3 || args[1] != \"delete\" {\n        eprintln!(\"Usage: trash delete <path>\");\n        std::process::exit(2);\n    }\n    match trash::delete(&args[2]) {\n        Ok(()) => {}\n        Err(e) => {\n            eprintln!(\"Error: {e:?}\");\n            std::process::exit(1);\n        }\n    }\n}\n"
  },
  {
    "path": "rustfmt.toml",
    "content": "edition = \"2018\"\nuse_try_shorthand = true\nuse_field_init_shorthand = true\nuse_small_heuristics = \"Max\"\nmax_width = 120\n"
  },
  {
    "path": "src/freedesktop.rs",
    "content": "//! This implementation will manage the trash according to the Freedesktop Trash specification,\n//! version 1.0 found at <https://specifications.freedesktop.org/trash-spec/trashspec-1.0.html>\n//!\n//! Most -if not all- Linux based desktop operating systems implement the Trash according to this specification.\n//! In other words: I looked, but I could not find any Linux based desktop OS that used anything else than the\n//! Freedesktop Trash specification.\n//!\n\nuse std::{\n    borrow::{Borrow, Cow},\n    collections::HashSet,\n    ffi::{OsStr, OsString},\n    fs::{self, File, OpenOptions},\n    io::{BufRead, BufReader, ErrorKind, Write},\n    os::unix::{\n        ffi::{OsStrExt, OsStringExt},\n        fs::PermissionsExt,\n    },\n    path::{Component, Path, PathBuf},\n};\n\nuse log::{debug, warn};\n\nuse crate::{Error, TrashContext, TrashItem, TrashItemMetadata, TrashItemSize};\n\ntype FsError = (PathBuf, std::io::Error);\n\n#[derive(Clone, Default, Debug)]\npub struct PlatformTrashContext;\nimpl PlatformTrashContext {\n    pub const fn new() -> Self {\n        PlatformTrashContext\n    }\n}\nimpl TrashContext {\n    pub(crate) fn delete_all_canonicalized(&self, full_paths: Vec<PathBuf>) -> Result<(), Error> {\n        let home_trash = canonicalize_path_or_parents(home_trash()?.as_path())?;\n        let sorted_mount_points = get_sorted_mount_points()?;\n        let home_trash_topdir = get_first_topdir_containing_path(&home_trash, &sorted_mount_points);\n        debug!(\"The 'home trash' topdir is {:?}\", home_trash_topdir);\n        let uid = unsafe { libc::getuid() };\n        for path in full_paths {\n            debug!(\"Deleting {:?}\", path);\n            let topdir = get_first_topdir_containing_path(&path, &sorted_mount_points);\n            debug!(\"The topdir of this file is {:?}\", topdir);\n            if topdir == home_trash_topdir {\n                debug!(\"The topdir was identical to the 'home trash' topdir, so moving to the home trash.\");\n                // Note that the following function creates the trash folder\n                // and its required subfolders in case they don't exist.\n                move_to_trash(path, &home_trash, topdir).map_err(|(p, e)| fs_error(p, e))?;\n            } else {\n                execute_on_mounted_trash_folders(uid, topdir, true, true, |trash_path| {\n                    move_to_trash(&path, trash_path, topdir)\n                })\n                .map_err(|(p, e)| fs_error(p, e))?;\n            }\n        }\n        Ok(())\n    }\n}\n\npub fn list() -> Result<Vec<TrashItem>, Error> {\n    let EvaluatedTrashFolders { trash_folders, home_error, sorted_mount_points } = eval_trash_folders()?;\n\n    if trash_folders.is_empty() {\n        warn!(\"No trash folder was found. The error when looking for the 'home trash' was: {:?}\", home_error);\n        return Ok(vec![]);\n    }\n    // List all items from the set of trash folders\n    let mut result = Vec::new();\n    for folder in &trash_folders {\n        // Read the info files for every file\n        let top_dir = get_first_topdir_containing_path(folder, &sorted_mount_points);\n        let info_folder = folder.join(\"info\");\n        if !info_folder.is_dir() {\n            warn!(\"The path {:?} did not point to a directory, skipping this trash folder.\", info_folder);\n            continue;\n        }\n        let read_dir = match std::fs::read_dir(&info_folder) {\n            Ok(d) => d,\n            Err(e) => {\n                // After all the earlier checks, it's still possible that the directory does not exist at this point (or is not readable)\n                // because another process may have deleted it or modified its access rights in the meantime.\n                // So let's just pring a warning and continue to the rest of the folders\n                warn!(\"The trash info folder {:?} could not be read. Error was {:?}\", info_folder, e);\n                continue;\n            }\n        };\n        #[cfg_attr(not(feature = \"chrono\"), allow(unused_labels))]\n        'trash_item: for entry in read_dir {\n            let info_entry = match entry {\n                Ok(entry) => entry,\n                Err(e) => {\n                    // Another thread or process may have removed that entry by now\n                    debug!(\"Tried resolving the trash info `DirEntry` but it failed with: '{}'\", e);\n                    continue;\n                }\n            };\n            // Entrty should really be an info file but better safe than sorry\n            let file_type = match info_entry.file_type() {\n                Ok(f_type) => f_type,\n                Err(e) => {\n                    // Another thread or process may have removed that entry by now\n                    debug!(\"Tried getting the file type of the trash info `DirEntry` but failed with: {}\", e);\n                    continue;\n                }\n            };\n            let info_path = info_entry.path();\n            if !file_type.is_file() {\n                warn!(\"Found an item that's not a file, among the trash info files. This is unexpected. The path to the item is: '{:?}'\", info_path);\n                continue;\n            }\n            let info_file = match File::open(&info_path) {\n                Ok(file) => file,\n                Err(e) => {\n                    // Another thread or process may have removed that entry by now\n                    debug!(\"Tried opening the trash info '{:?}' but failed with: {}\", info_path, e);\n                    continue;\n                }\n            };\n            let id = info_path.clone().into();\n            let mut name = None;\n            let mut original_parent: Option<PathBuf> = None;\n            #[cfg_attr(not(feature = \"chrono\"), allow(unused_mut))]\n            let mut time_deleted = None;\n\n            let info_reader = BufReader::new(info_file);\n            // Skip 1 because the first line must be \"[Trash Info]\"\n            'info_lines: for line_result in info_reader.lines().skip(1) {\n                // Another thread or process may have removed the infofile by now\n                let line = if let Ok(line) = line_result {\n                    line\n                } else {\n                    break 'info_lines;\n                };\n                let mut split = line.split('=');\n\n                // Just unwraping here because the system is assumed to follow the specification.\n                let key = split.next().unwrap().trim();\n                let value = split.next().unwrap().trim();\n\n                if key == \"Path\" {\n                    let value_path = {\n                        let path = Path::new(value);\n                        if path.is_relative() {\n                            decode_uri_path(top_dir.join(path))\n                        } else {\n                            decode_uri_path(path)\n                        }\n                    };\n                    name = value_path.file_name().map(|name| name.to_owned());\n                    let parent = value_path.parent().expect(\"Absolute path to trashed item should have a parent\");\n                    original_parent = Some(parent.into());\n                } else if key == \"DeletionDate\" {\n                    #[cfg(feature = \"chrono\")]\n                    {\n                        use chrono::{NaiveDateTime, TimeZone};\n                        let parsed_time = NaiveDateTime::parse_from_str(value, \"%Y-%m-%dT%H:%M:%S\");\n                        let naive_local = match parsed_time {\n                            Ok(t) => t,\n                            Err(e) => {\n                                log::error!(\"Failed to parse the deletion date of the trash item {:?}. The deletion date was '{}'. Parse error was: {:?}\", name, value, e);\n                                continue 'trash_item;\n                            }\n                        };\n                        let time = chrono::Local.from_local_datetime(&naive_local).earliest();\n                        match time {\n                            Some(time) => time_deleted = Some(time.timestamp()),\n                            None => {\n                                log::error!(\n                                    \"Failed to convert the local time to a UTC time. Local time was {:?}\",\n                                    naive_local\n                                );\n                                continue 'trash_item;\n                            }\n                        }\n                    }\n                }\n            }\n            if let Some(name) = name {\n                if let Some(original_parent) = original_parent {\n                    if time_deleted.is_none() {\n                        warn!(\"Could not determine the deletion time of the trash item. (The `DeletionDate` field is probably missing from the info file.) The info file path is: '{:?}'\", info_path);\n                    }\n                    result.push(TrashItem { id, name, original_parent, time_deleted: time_deleted.unwrap_or(-1) });\n                } else {\n                    warn!(\"Could not determine the original parent folder of the trash item. (The `Path` field is probably missing from the info file.) The info file path is: '{:?}'\", info_path);\n                }\n            } else {\n                warn!(\"Could not determine the name of the trash item. (The `Path` field is probably missing from the info file.) The info file path is: '{:?}'\", info_path);\n            }\n        }\n    }\n    Ok(result)\n}\n\npub fn is_empty() -> Result<bool, Error> {\n    let trash_folders = trash_folders()?;\n\n    if trash_folders.is_empty() {\n        return Ok(true);\n    }\n\n    for folder in trash_folders {\n        // We're only concerned if the trash contains any files\n        // Therefore, we only need to check if the bin itself is empty\n        let bin = folder.join(\"files\");\n        match bin.read_dir() {\n            Ok(mut entries) => {\n                if let Some(Ok(_)) = entries.next() {\n                    return Ok(false);\n                }\n            }\n            Err(e) => {\n                warn!(\"The trash files folder {:?} could not be read. Error was {:?}\", bin, e);\n            }\n        }\n    }\n\n    Ok(true)\n}\n\npub fn trash_folders() -> Result<HashSet<PathBuf>, Error> {\n    let EvaluatedTrashFolders { trash_folders, home_error, .. } = eval_trash_folders()?;\n\n    if trash_folders.is_empty() {\n        return match home_error {\n            Some(e) => Err(e),\n            None => Err(Error::Unknown {\n                description: \"Could not find a valid 'home trash' nor valid trashes on other mount points\".into(),\n            }),\n        };\n    }\n\n    Ok(trash_folders)\n}\n\nstruct EvaluatedTrashFolders {\n    trash_folders: HashSet<PathBuf>,\n    home_error: Option<Error>,\n    sorted_mount_points: Vec<MountPoint>,\n}\n\nfn eval_trash_folders() -> Result<EvaluatedTrashFolders, Error> {\n    let mut trash_folders = HashSet::new();\n    // Get home trash folder and add it to the set of trash folders.\n    // It may not exist and that's completely fine as long as there are other trash folders.\n    let home_error;\n    match home_trash() {\n        Ok(home_trash) => {\n            if !home_trash.is_dir() {\n                home_error = Some(Error::Unknown {\n                    description:\n                        \"The 'home trash' either does not exist or is not a directory (or a link pointing to a dir)\"\n                            .into(),\n                });\n            } else {\n                trash_folders.insert(home_trash);\n                home_error = None;\n            }\n        }\n        Err(e) => {\n            home_error = Some(e);\n        }\n    }\n\n    // Get all mount-points and attempt to find a trash folder in each adding them to the SET of\n    // trash folders when found one.\n    let uid = unsafe { libc::getuid() };\n    let sorted_mount_points = get_sorted_mount_points()?;\n    for mount in &sorted_mount_points {\n        execute_on_mounted_trash_folders(uid, &mount.mnt_dir, false, false, |trash_path| {\n            trash_folders.insert(trash_path);\n            Ok(())\n        })\n        .map_err(|(p, e)| fs_error(p, e))?;\n    }\n\n    Ok(EvaluatedTrashFolders { trash_folders, home_error, sorted_mount_points })\n}\npub fn metadata(item: &TrashItem) -> Result<TrashItemMetadata, Error> {\n    // When purging an item the \"in-trash\" filename must be parsed from the trashinfo filename\n    // which is the filename in the `id` field.\n    let info_file = &item.id;\n\n    let file = restorable_file_in_trash_from_info_file(info_file);\n    ensure_virtually_exists(&file)?;\n    let metadata = fs::symlink_metadata(&file).map_err(|e| fs_error(&file, e))?;\n    let is_dir = metadata.is_dir();\n    let size = if is_dir {\n        TrashItemSize::Entries(fs::read_dir(&file).map_err(|e| fs_error(&file, e))?.count())\n    } else {\n        TrashItemSize::Bytes(metadata.len())\n    };\n    Ok(TrashItemMetadata { size })\n}\n\n/// The path points to:\n/// - existing file | directory | symlink => Ok(true)\n/// - broken symlink => Ok(true)\n/// - nothing => Ok(false)\n/// - I/O Error => Err(Io)\n#[inline]\nfn virtually_exists(path: &Path) -> std::io::Result<bool> {\n    Ok(path.try_exists()? || path.is_symlink())\n}\n\nconst MISSING_TRASH_FILE: &str = \"trash item referenced by .trashinfo is missing from trash/files\";\n\n/// Ensures the path virtually exists, returning an error if it does not.\n/// This is used instead of `assert!` to gracefully handle cases where the trash file\n/// referenced by a `.trashinfo` file is missing from the `files` directory.\n#[inline]\nfn ensure_virtually_exists(path: &Path) -> Result<(), Error> {\n    if virtually_exists(path).map_err(|e| fs_error(path, e))? {\n        Ok(())\n    } else {\n        Err(fs_error(path, std::io::Error::new(std::io::ErrorKind::NotFound, MISSING_TRASH_FILE)))\n    }\n}\n\npub fn purge_all<I>(items: I) -> Result<(), Error>\nwhere\n    I: IntoIterator,\n    <I as IntoIterator>::Item: Borrow<TrashItem>,\n{\n    for item in items.into_iter() {\n        // When purging an item the \"in-trash\" filename must be parsed from the trashinfo filename\n        // which is the filename in the `id` field.\n        let info_file = &item.borrow().id;\n\n        // A bunch of unwraps here. This is fine because if any of these fail that means\n        // that either there's a bug in this code or the target system didn't follow\n        // the specification.\n        let file = restorable_file_in_trash_from_info_file(info_file);\n        if file.is_dir() {\n            std::fs::remove_dir_all(&file).map_err(|e| fs_error(&file, e))?;\n        // TODO Update directory size cache if there's one.\n        } else {\n            std::fs::remove_file(&file).map_err(|e| fs_error(&file, e))?;\n        }\n        std::fs::remove_file(info_file).map_err(|e| fs_error(info_file, e))?;\n    }\n\n    Ok(())\n}\n\nfn restorable_file_in_trash_from_info_file(info_file: impl AsRef<std::ffi::OsStr>) -> PathBuf {\n    let info_file = info_file.as_ref();\n    let trash_folder = Path::new(info_file).parent().unwrap().parent().unwrap();\n    let name_in_trash = Path::new(info_file).file_stem().unwrap();\n    trash_folder.join(\"files\").join(name_in_trash)\n}\n\npub fn restore_all<I>(items: I) -> Result<(), Error>\nwhere\n    I: IntoIterator<Item = TrashItem>,\n{\n    // Simply read the items' original location from the infofile and attemp to move the items there\n    // and delete the infofile if the move operation was sucessful.\n\n    let mut iter = items.into_iter();\n    while let Some(item) = iter.next() {\n        // The \"in-trash\" filename must be parsed from the trashinfo filename\n        // which is the filename in the `id` field.\n        let info_file = &item.id;\n\n        // A bunch of unwraps here. This is fine because if any of these fail that means\n        // that either there's a bug in this code or the target system didn't follow\n        // the specification.\n        let file = restorable_file_in_trash_from_info_file(info_file);\n        ensure_virtually_exists(&file)?;\n        // TODO add option to forcefully replace any target at the restore location\n        // if it already exists.\n        let original_path = item.original_path();\n        // Make sure the parent exists so that `create_dir` doesn't faile due to that.\n        std::fs::create_dir_all(&item.original_parent).map_err(|e| fs_error(&item.original_parent, e))?;\n        let mut collision = false;\n        if file.is_dir() {\n            // NOTE create_dir_all succeeds when the path already exist but create_dir\n            // fails with `std::io::ErrorKind::AlreadyExists`.\n            if let Err(e) = std::fs::create_dir(&original_path) {\n                if e.kind() == std::io::ErrorKind::AlreadyExists {\n                    collision = true;\n                } else {\n                    return Err(fs_error(&original_path, e));\n                }\n            }\n        } else {\n            // File or symlink\n            if let Err(e) = OpenOptions::new().create_new(true).write(true).open(&original_path) {\n                if e.kind() == std::io::ErrorKind::AlreadyExists {\n                    collision = true;\n                } else {\n                    return Err(fs_error(&original_path, e));\n                }\n            }\n        }\n        if collision {\n            let remaining: Vec<_> = std::iter::once(item).chain(iter).collect();\n            return Err(Error::RestoreCollision { path: original_path, remaining_items: remaining });\n        }\n        std::fs::rename(&file, &original_path).map_err(|e| fs_error(&file, e))?;\n        std::fs::remove_file(info_file).map_err(|e| fs_error(info_file, e))?;\n    }\n    Ok(())\n}\n\n/// According to the specification (see at the top of the file) there are two kinds of\n/// trash-folders for a mounted drive or partition.\n/// 1, .Trash/uid\n/// 2, .Trash-uid\n///\n/// This function executes `op` providing it with a\n/// trash-folder path that's associated with the partition mounted at `topdir`.\n///\nfn execute_on_mounted_trash_folders<F: FnMut(PathBuf) -> Result<(), FsError>>(\n    uid: u32,\n    topdir: impl AsRef<Path>,\n    first_only: bool,\n    create_folder: bool,\n    mut op: F,\n) -> Result<(), FsError> {\n    // See if there's a \".Trash\" directory at the mounted location\n    let topdir = topdir.as_ref();\n    let trash_path = topdir.join(\".Trash\");\n    if trash_path.is_dir() {\n        let validity = folder_validity(&trash_path)?;\n        if validity == TrashValidity::Valid {\n            let users_trash_path = trash_path.join(uid.to_string());\n            if users_trash_path.exists() && users_trash_path.is_dir() {\n                op(users_trash_path)?;\n                if first_only {\n                    return Ok(());\n                }\n            }\n        } else {\n            warn!(\"A Trash folder was found at '{:?}', but it's invalid because it's {:?}\", trash_path, validity);\n        }\n    }\n    // See if there's a \".Trash-$UID\" directory at the mounted location\n    let trash_path = topdir.join(format!(\".Trash-{uid}\"));\n    let should_execute;\n    if !trash_path.exists() || !trash_path.is_dir() {\n        if create_folder {\n            std::fs::create_dir(&trash_path).map_err(|e| (trash_path.to_owned(), e))?;\n            should_execute = true;\n        } else {\n            should_execute = false;\n        }\n    } else {\n        should_execute = true;\n    }\n    if should_execute {\n        op(trash_path)?;\n    }\n    Ok(())\n}\n\nfn move_to_trash(\n    src: impl AsRef<Path>,\n    trash_folder: impl AsRef<Path>,\n    _topdir: impl AsRef<Path>,\n) -> Result<(), FsError> {\n    let src = src.as_ref();\n    let trash_folder = trash_folder.as_ref();\n    let files_folder = trash_folder.join(\"files\");\n    let info_folder = trash_folder.join(\"info\");\n\n    // Ensure the `files` and `info` folders exist\n    std::fs::create_dir_all(&files_folder).map_err(|e| (files_folder.to_owned(), e))?;\n    std::fs::create_dir_all(&info_folder).map_err(|e| (info_folder.to_owned(), e))?;\n\n    // This kind of validity must only apply ot administrator style trash folders\n    // See Trash directories, (1) at https://specifications.freedesktop.org/trash-spec/trashspec-1.0.html\n    //assert_eq!(folder_validity(trash_folder)?, TrashValidity::Valid);\n\n    // When trashing a file one must make sure that every trashed item is uniquely named.\n    // However the `rename` function -that is used in *nix systems to move files- by default\n    // overwrites the destination. Therefore when multiple threads are removing items with identical\n    // names, an implementation might accidently overwrite an item that was just put into the trash\n    // if it's not careful enough.\n    //\n    // The strategy here is to use the `create_new` parameter of `OpenOptions` to\n    // try creating a placeholder file in the trash but don't do so if one with an identical name\n    // already exist. This newly created empty file can then be safely overwritten by the src file\n    // using the `rename` function.\n    let filename = src.file_name().unwrap();\n    let mut appendage = 0usize;\n    loop {\n        appendage += 1;\n        let in_trash_name: Cow<'_, OsStr> = if appendage > 1 {\n            let mut trash_name = filename.to_owned();\n            trash_name.push(format!(\".{appendage}\"));\n            trash_name.into()\n        } else {\n            filename.into()\n        };\n        // Length of name + length of '.trashinfo'\n        let mut info_name = OsString::with_capacity(in_trash_name.len() + 10);\n        info_name.push(&in_trash_name);\n        info_name.push(\".trashinfo\");\n        let info_file_path = info_folder.join(&info_name);\n        let info_result = OpenOptions::new().create_new(true).write(true).open(&info_file_path);\n        match info_result {\n            Err(error) => {\n                if error.kind() == std::io::ErrorKind::AlreadyExists {\n                    continue;\n                } else {\n                    debug!(\"Failed to create the new file {:?}\", info_file_path);\n                    return Err((info_file_path, error));\n                }\n            }\n            Ok(mut file) => {\n                debug!(\"Successfully created {:?}\", info_file_path);\n                // Write the info file before actually moving anything\n                writeln!(file, \"[Trash Info]\")\n                    .and_then(|_| {\n                        let absolute_uri = encode_uri_path(src);\n                        writeln!(file, \"Path={absolute_uri}\").and_then(|_| {\n                            #[cfg(feature = \"chrono\")]\n                            {\n                                let now = chrono::Local::now();\n                                writeln!(file, \"DeletionDate={}\", now.format(\"%Y-%m-%dT%H:%M:%S\"))\n                            }\n                            #[cfg(not(feature = \"chrono\"))]\n                            {\n                                Ok(())\n                            }\n                        })\n                    })\n                    .map_err(|e| (info_file_path.to_owned(), e))?;\n            }\n        }\n        let path = files_folder.join(&in_trash_name);\n        match move_items_no_replace(src, &path) {\n            Err((path, error)) => {\n                debug!(\"Failed moving item to the trash (this is usually OK). {:?}\", error);\n                // Try to delete the info file\n                if let Err(info_err) = std::fs::remove_file(info_file_path) {\n                    warn!(\"Created the trash info file, then failed to move the item to the trash. So far it's OK, but then failed remove the initial info file. There's either a bug in this program or another faulty program is manupulating the Trash. The error was: {:?}\", info_err);\n                }\n                if error.kind() == std::io::ErrorKind::AlreadyExists {\n                    continue;\n                } else {\n                    return Err((path, error));\n                }\n            }\n            Ok(_) => {\n                // We did it!\n                break;\n            }\n        }\n    }\n\n    Ok(())\n}\n\n/// An error may mean that a collision was found.\nfn move_items_no_replace(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> Result<(), FsError> {\n    let src = src.as_ref();\n    let dst = dst.as_ref();\n\n    try_creating_placeholders(src, dst)?;\n\n    // Try to rename first (fastest option for same filesystem)\n    let Err(e) = std::fs::rename(src, dst) else { return Ok(()) };\n\n    let needs_cross_device_copy = e.kind() == ErrorKind::CrossesDevices;\n    if !needs_cross_device_copy {\n        return Err((src.to_owned(), e));\n    }\n\n    debug!(\"Cross-device move detected, falling back to copy+delete for {:?}\", src);\n\n    // Copy the file/directory\n    if src.is_dir() {\n        copy_dir_all(src, dst)?;\n    } else {\n        std::fs::copy(src, dst).map_err(|e| (src.to_owned(), e))?;\n    }\n\n    // Remove the source\n    if src.is_dir() {\n        std::fs::remove_dir_all(src).map_err(|e| (src.to_owned(), e))?;\n    } else {\n        std::fs::remove_file(src).map_err(|e| (src.to_owned(), e))?;\n    }\n\n    Ok(())\n}\n\nfn try_creating_placeholders(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> Result<(), FsError> {\n    let src = src.as_ref();\n    let dst = dst.as_ref();\n    let metadata = src.symlink_metadata().map_err(|e| (src.to_owned(), e))?;\n    if metadata.is_dir() {\n        // NOTE create_dir fails if the directory already exists\n        std::fs::create_dir(dst).map_err(|e| (dst.to_owned(), e))?;\n    } else {\n        // Symlink or file\n        OpenOptions::new().create_new(true).write(true).open(dst).map_err(|e| (dst.to_owned(), e))?;\n    }\n    Ok(())\n}\n\n/// Helper function to recursively copy a directory\nfn copy_dir_all(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> Result<(), FsError> {\n    let src = src.as_ref();\n    let dst = dst.as_ref();\n\n    std::fs::create_dir_all(dst).map_err(|e| (dst.to_owned(), e))?;\n\n    for entry in std::fs::read_dir(src).map_err(|e| (src.to_owned(), e))? {\n        let entry = entry.map_err(|e| (src.to_owned(), e))?;\n        let file_type = entry.file_type().map_err(|e| (entry.path(), e))?;\n        let src_path = entry.path();\n        let dst_path = dst.join(entry.file_name());\n\n        if file_type.is_dir() {\n            copy_dir_all(&src_path, &dst_path)?;\n        } else if file_type.is_symlink() {\n            // Handle symlinks by copying the symlink itself, not the target\n            let target = std::fs::read_link(&src_path).map_err(|e| (src_path.clone(), e))?;\n            std::os::unix::fs::symlink(&target, &dst_path).map_err(|e| (dst_path.clone(), e))?;\n        } else {\n            std::fs::copy(&src_path, &dst_path).map_err(|e| (src_path.clone(), e))?;\n        }\n    }\n\n    Ok(())\n}\n\nfn decode_uri_path(path: impl AsRef<Path>) -> PathBuf {\n    // Paths may be invalid Unicode on most Unixes so they should be treated as byte strings\n    // A higher level crate, such as `url`, can't be used directly since its API intakes valid Rust\n    // strings. Thus, the easiest way is to manually decode each segment of the path and recombine.\n    path.as_ref().iter().map(|part| OsString::from_vec(urlencoding::decode_binary(part.as_bytes()).to_vec())).collect()\n}\n\nfn encode_uri_path(path: impl AsRef<Path>) -> String {\n    // `iter()` cannot be used here because it yields '/' in certain situations, such as\n    // for root directories.\n    // Slashes would be encoded and thus mess up the path\n    let path: PathBuf = path\n        .as_ref()\n        .components()\n        .map(|component| {\n            // Only encode names and not '/', 'C:\\', et cetera\n            if let Component::Normal(part) = component {\n                urlencoding::encode_binary(part.as_bytes()).to_string()\n            } else {\n                component.as_os_str().to_str().expect(\"Path components such as '/' are valid Unicode\").to_owned()\n            }\n        })\n        .collect();\n\n    path.to_str().expect(\"URL encoded bytes is valid Unicode\").to_owned()\n}\n\n#[derive(Eq, PartialEq, Debug)]\nenum TrashValidity {\n    Valid,\n    InvalidSymlink,\n    InvalidNotSticky,\n}\n\nfn folder_validity(path: impl AsRef<Path>) -> Result<TrashValidity, FsError> {\n    /// Mask for the sticky bit\n    /// Taken from: http://man7.org/linux/man-pages/man7/inode.7.html\n    const S_ISVTX: u32 = 0o1000;\n\n    let path = path.as_ref();\n    let metadata = path.symlink_metadata().map_err(|e| (path.to_owned(), e))?;\n    if metadata.file_type().is_symlink() {\n        return Ok(TrashValidity::InvalidSymlink);\n    }\n    let mode = metadata.permissions().mode();\n    let no_sticky_bit = (mode & S_ISVTX) == 0;\n    if no_sticky_bit {\n        return Ok(TrashValidity::InvalidNotSticky);\n    }\n    Ok(TrashValidity::Valid)\n}\n\n/// Corresponds to the definition of \"home_trash\" from\n/// https://specifications.freedesktop.org/trash-spec/trashspec-1.0.html\nfn home_trash() -> Result<PathBuf, Error> {\n    if let Some(data_home) = std::env::var_os(\"XDG_DATA_HOME\") {\n        if !data_home.is_empty() {\n            let data_home_path = AsRef::<Path>::as_ref(data_home.as_os_str());\n            return Ok(data_home_path.join(\"Trash\"));\n        }\n    }\n    if let Some(home) = std::env::var_os(\"HOME\") {\n        if !home.is_empty() {\n            let home_path = AsRef::<Path>::as_ref(home.as_os_str());\n            return Ok(home_path.join(\".local/share/Trash\"));\n        }\n    }\n    Err(Error::Unknown { description: \"Neither the XDG_DATA_HOME nor the HOME environment variable was found\".into() })\n}\n\nfn get_first_topdir_containing_path<'a>(path: &Path, mnt_points: &'a [MountPoint]) -> &'a Path {\n    let root: &'static Path = Path::new(\"/\");\n    mnt_points.iter().map(|mp| mp.mnt_dir.as_path()).find(|mount_path| path.starts_with(mount_path)).unwrap_or(root)\n}\n\n/// Canonicalize a path. If the path doesn't exist, the canonical form of the\n/// path is determined by looking at its parent directories.\nfn canonicalize_path_or_parents(mut path: &Path) -> Result<PathBuf, Error> {\n    let mut popped_path_components = vec![];\n\n    loop {\n        match path.canonicalize() {\n            Ok(canonical) => {\n                return Ok(popped_path_components.iter().rev().fold(canonical, |acc, component| acc.join(component)));\n            }\n            Err(e) if e.kind() == std::io::ErrorKind::NotFound => {\n                let file_name = path.file_name().ok_or(Error::CanonicalizePath { original: path.to_owned() })?;\n                popped_path_components.push(file_name.to_owned());\n                path = path.parent().ok_or(Error::CanonicalizePath { original: path.to_owned() })?;\n            }\n            Err(e) => return Err(Error::FileSystem { path: path.to_owned(), source: e }),\n        }\n    }\n}\n\nstruct MountPoint {\n    mnt_dir: PathBuf,\n    _mnt_type: String,\n    _mnt_fsname: String,\n}\n\n/// Sorted by longest path first\nfn get_sorted_mount_points() -> Result<Vec<MountPoint>, Error> {\n    let mut mount_points = get_mount_points()?;\n    mount_points.sort_unstable_by(|a, b| {\n        let a = a.mnt_dir.as_os_str().as_bytes().len();\n        let b = b.mnt_dir.as_os_str().as_bytes().len();\n        a.cmp(&b).reverse()\n    });\n    Ok(mount_points)\n}\n\n#[cfg(target_os = \"linux\")]\nfn get_mount_points() -> Result<Vec<MountPoint>, Error> {\n    use once_cell::sync::Lazy;\n    use scopeguard::defer;\n    use std::ffi::{CStr, CString};\n    use std::sync::Mutex;\n\n    // The getmntinfo() function writes the array of structures to an internal\n    // static object and returns a pointer to that object.  Subsequent calls to\n    // getmntent() will modify the same object. This means that the function is\n    // not threadsafe. To help prevent multiple threads using it concurrently\n    // via get_mount_points a Mutex is used.\n    // We understand that threads can still call `libc::getmntent(…)` directly\n    // to bypass the lock and trigger UB.\n    static LOCK: Lazy<Mutex<()>> = Lazy::new(|| Mutex::new(()));\n    let _lock = LOCK.lock().unwrap();\n\n    //let file;\n    let read_arg = CString::new(\"r\").unwrap();\n    let mounts_path = CString::new(\"/proc/mounts\").unwrap();\n    let mut file = unsafe { libc::fopen(mounts_path.as_c_str().as_ptr(), read_arg.as_c_str().as_ptr()) };\n    if file.is_null() {\n        let mtab_path = CString::new(\"/etc/mtab\").unwrap();\n        file = unsafe { libc::fopen(mtab_path.as_c_str().as_ptr(), read_arg.as_c_str().as_ptr()) };\n    }\n    if file.is_null() {\n        return Err(Error::Unknown { description: \"Neither '/proc/mounts' nor '/etc/mtab' could be opened.\".into() });\n    }\n    defer! { unsafe { libc::fclose(file); } }\n    let mut result = Vec::new();\n    loop {\n        let mntent = unsafe { libc::getmntent(file) };\n        if mntent.is_null() {\n            break;\n        }\n        let dir = unsafe { CStr::from_ptr((*mntent).mnt_dir).to_str().unwrap() };\n        if dir.is_empty() {\n            continue;\n        }\n        let mount_point = unsafe {\n            MountPoint {\n                mnt_dir: dir.into(),\n                _mnt_fsname: CStr::from_ptr((*mntent).mnt_fsname).to_str().unwrap().into(),\n                _mnt_type: CStr::from_ptr((*mntent).mnt_type).to_str().unwrap().into(),\n            }\n        };\n        result.push(mount_point);\n    }\n    if result.is_empty() {\n        return Err(Error::Unknown {\n            description: \"A mount points file could be opened, but the call to `getmntent` returned NULL.\".into(),\n        });\n    }\n    Ok(result)\n}\n\n#[cfg(any(target_os = \"dragonfly\", target_os = \"freebsd\", target_os = \"openbsd\"))]\nfn get_mount_points() -> Result<Vec<MountPoint>, Error> {\n    use once_cell::sync::Lazy;\n    use std::sync::Mutex;\n\n    // The getmntinfo() function writes the array of structures to an internal\n    // static object and returns a pointer to that object.  Subsequent calls to\n    // getmntinfo() will modify the same object. This means that the function is\n    // not threadsafe. To help prevent multiple threads using it concurrently\n    // via get_mount_points a Mutex is used.\n    // We understand that threads can still call `libc::getmntinfo(…)` directly\n    // to bypass the lock and trigger UB.\n    static LOCK: Lazy<Mutex<()>> = Lazy::new(|| Mutex::new(()));\n    let _lock = LOCK.lock().unwrap();\n\n    fn c_buf_to_str(buf: &[libc::c_char]) -> Option<&str> {\n        let buf: &[u8] = unsafe { std::slice::from_raw_parts(buf.as_ptr() as _, buf.len()) };\n        if let Some(pos) = buf.iter().position(|x| *x == 0) {\n            // Shrink buffer to omit the null bytes\n            std::str::from_utf8(&buf[..pos]).ok()\n        } else {\n            std::str::from_utf8(buf).ok()\n        }\n    }\n    let mut fs_infos: *mut libc::statfs = std::ptr::null_mut();\n    let count = unsafe { libc::getmntinfo(&mut fs_infos, libc::MNT_WAIT) };\n    if count < 1 {\n        return Ok(Vec::new());\n    }\n    let fs_infos: &[libc::statfs] = unsafe { std::slice::from_raw_parts(fs_infos as _, count as _) };\n\n    let mut result = Vec::new();\n    for fs_info in fs_infos {\n        if fs_info.f_mntfromname[0] == 0 || fs_info.f_mntonname[0] == 0 {\n            // If we have missing information, no need to look any further...\n            continue;\n        }\n        let fs_type = c_buf_to_str(&fs_info.f_fstypename).unwrap_or_default();\n        let mount_to = match c_buf_to_str(&fs_info.f_mntonname) {\n            Some(m) => m,\n            None => {\n                debug!(\"Cannot get disk mount point, ignoring it.\");\n                continue;\n            }\n        };\n        let mount_from = c_buf_to_str(&fs_info.f_mntfromname).unwrap_or_default();\n\n        let mount_point =\n            MountPoint { mnt_dir: mount_to.into(), _mnt_fsname: mount_from.into(), _mnt_type: fs_type.into() };\n        result.push(mount_point);\n    }\n    Ok(result)\n}\n\n#[cfg(target_os = \"netbsd\")]\nfn get_mount_points() -> Result<Vec<MountPoint>, Error> {\n    use once_cell::sync::Lazy;\n    use std::sync::Mutex;\n\n    // The getmntinfo() function writes the array of structures to an internal\n    // static object and returns a pointer to that object.  Subsequent calls to\n    // getmntinfo() will modify the same object. This means that the function is\n    // not threadsafe. To help prevent multiple threads using it concurrently\n    // via get_mount_points a Mutex is used.\n    // We understand that threads can still call `libc::getmntinfo(…)` directly\n    // to bypass the lock and trigger UB.\n    // NetBSD does not support statfs since 2005, so we need to use statvfs instead.\n    static LOCK: Lazy<Mutex<()>> = Lazy::new(|| Mutex::new(()));\n    let _lock = LOCK.lock().unwrap();\n\n    fn c_buf_to_str(buf: &[libc::c_char]) -> Option<&str> {\n        let buf: &[u8] = unsafe { std::slice::from_raw_parts(buf.as_ptr() as _, buf.len()) };\n        if let Some(pos) = buf.iter().position(|x| *x == 0) {\n            // Shrink buffer to omit the null bytes\n            std::str::from_utf8(&buf[..pos]).ok()\n        } else {\n            std::str::from_utf8(buf).ok()\n        }\n    }\n    let mut fs_infos: *mut libc::statvfs = std::ptr::null_mut();\n    let count = unsafe { libc::getmntinfo(&mut fs_infos, libc::MNT_WAIT) };\n    if count < 1 {\n        return Ok(Vec::new());\n    }\n    let fs_infos: &[libc::statvfs] = unsafe { std::slice::from_raw_parts(fs_infos as _, count as _) };\n\n    let mut result = Vec::new();\n    for fs_info in fs_infos {\n        if fs_info.f_mntfromname[0] == 0 || fs_info.f_mntonname[0] == 0 {\n            // If we have missing information, no need to look any further...\n            continue;\n        }\n        let fs_type = c_buf_to_str(&fs_info.f_fstypename).unwrap_or_default();\n        let mount_to = match c_buf_to_str(&fs_info.f_mntonname) {\n            Some(m) => m,\n            None => {\n                debug!(\"Cannot get disk mount point, ignoring it.\");\n                continue;\n            }\n        };\n        let mount_from = c_buf_to_str(&fs_info.f_mntfromname).unwrap_or_default();\n\n        let mount_point =\n            MountPoint { mnt_dir: mount_to.into(), _mnt_fsname: mount_from.into(), _mnt_type: fs_type.into() };\n        result.push(mount_point);\n    }\n    Ok(result)\n}\n\n#[cfg(not(any(\n    target_os = \"linux\",\n    target_os = \"dragonfly\",\n    target_os = \"freebsd\",\n    target_os = \"openbsd\",\n    target_os = \"netbsd\"\n)))]\nfn get_mount_points() -> Result<Vec<MountPoint>, Error> {\n    // On platforms that don't have support yet, return an error\n    Err(Error::Unknown { description: \"Mount points cannot be determined on this operating system\".into() })\n}\n\n#[cfg(test)]\nmod tests {\n    use serial_test::serial;\n    use std::{\n        collections::{hash_map::Entry, HashMap},\n        env,\n        ffi::{OsStr, OsString},\n        fmt,\n        fs::File,\n        io::ErrorKind,\n        os::unix::{self, ffi::OsStringExt, fs::PermissionsExt},\n        path::{Path, PathBuf},\n        process::Command,\n    };\n\n    use log::warn;\n\n    use crate::{\n        canonicalize_paths, delete, delete_all,\n        os_limited::{list, purge_all, restore_all},\n        platform::encode_uri_path,\n        tests::get_unique_name,\n        Error,\n    };\n\n    use super::{canonicalize_path_or_parents, decode_uri_path};\n\n    #[test]\n    #[serial]\n    fn test_list() {\n        crate::tests::init_logging();\n\n        let file_name_prefix = get_unique_name();\n        let batches: usize = 2;\n        let files_per_batch: usize = 3;\n        let names: Vec<OsString> = (0..files_per_batch).map(|i| format!(\"{}#{}\", file_name_prefix, i).into()).collect();\n        for _ in 0..batches {\n            for path in &names {\n                File::create_new(path).unwrap();\n            }\n            // eprintln!(\"Deleting {:?}\", names);\n            let result = delete_all_using_system_program(&names);\n            if let Err(SystemTrashError::NoTrashProgram) = &result {\n                // For example may be the case on build systems that don't have a destop environment\n                warn!(\"No system default trashing utility was found, using this crate's implementation\");\n                delete_all(&names).unwrap();\n            } else {\n                result.unwrap();\n            }\n        }\n        let items = list().unwrap();\n        let items: HashMap<_, Vec<_>> = items\n            .into_iter()\n            .filter(|x| x.name.as_encoded_bytes().starts_with(file_name_prefix.as_bytes()))\n            .fold(HashMap::new(), |mut map, x| {\n                match map.entry(x.name.clone()) {\n                    Entry::Occupied(mut entry) => {\n                        entry.get_mut().push(x);\n                    }\n                    Entry::Vacant(entry) => {\n                        entry.insert(vec![x]);\n                    }\n                }\n                map\n            });\n        for name in names {\n            match items.get(&name) {\n                Some(items) => assert_eq!(items.len(), batches),\n                None => panic!(\"ERROR Could not find '{:?}' in {:#?}\", name, items),\n            }\n        }\n\n        // Let's try to purge all the items we just created but ignore any errors\n        // as this test should succeed as long as `list` works properly.\n        let _ = purge_all(items.into_values().flatten());\n    }\n\n    #[test]\n    #[serial]\n    fn test_broken_symlinks() {\n        crate::tests::init_logging();\n\n        let file_name_prefix = get_unique_name();\n        let file_count = 2;\n        let names: Vec<OsString> = (0..file_count).map(|i| format!(\"{}#{}\", file_name_prefix, i).into()).collect();\n        let symlink_names: Vec<OsString> = names.iter().map(|name| format!(\"{:?}-symlink\", name).into()).collect();\n\n        // Test file symbolic link and directory symbolic link\n        File::create_new(&names[0]).unwrap();\n        std::fs::create_dir(&names[1]).unwrap();\n\n        for (i, (name, symlink)) in names.iter().zip(&symlink_names).enumerate() {\n            // Create symbolic link\n            unix::fs::symlink(name, symlink).unwrap();\n\n            // Break the symbolic link\n            if i == 0 {\n                std::fs::remove_file(name).unwrap();\n            } else {\n                std::fs::remove_dir(name).unwrap();\n            }\n\n            // Delete and Restore it without errors\n            delete(symlink).unwrap();\n            let items = list().unwrap();\n            let item = items.into_iter().find(|it| it.name == *symlink).unwrap();\n            restore_all([item.clone()]).expect(\"The broken symbolic link should be restored successfully.\");\n\n            // Delete and Purge it without errors\n            delete(symlink).unwrap();\n            purge_all([item]).expect(\"The broken symbolic link should be purged successfully.\");\n        }\n    }\n\n    #[test]\n    fn uri_enc_dec_roundtrip() {\n        let fake = format!(\"/tmp/{}\", get_unique_name());\n        let path = decode_uri_path(&fake);\n\n        assert_eq!(path.to_str().expect(\"Path is valid Unicode\"), fake, \"Decoded path shouldn't be different\");\n\n        let encoded = encode_uri_path(&path);\n        assert_eq!(encoded, fake, \"URL encoded alphanumeric String shouldn't change\");\n    }\n\n    #[test]\n    fn uri_enc_dec_roundtrip_invalid_unicode() {\n        let base = OsStr::new(&format!(\"/tmp/{}/\", get_unique_name())).to_os_string();\n\n        // Add invalid UTF-8 byte\n        let mut bytes = base.into_encoded_bytes();\n        bytes.push(168);\n        let fake = OsString::from_vec(bytes);\n        assert!(fake.to_str().is_none(), \"Invalid Unicode cannot be a Rust String\");\n\n        let path = decode_uri_path(&fake);\n        assert_eq!(path.as_os_str().as_encoded_bytes(), fake.as_encoded_bytes());\n\n        // Shouldn't panic\n        encode_uri_path(&path);\n    }\n\n    #[test]\n    fn test_canonicalize_path_or_parents() {\n        enum CanonicalizeFixture<'a> {\n            Dir(&'a str),\n            File(&'a str),\n            Symlink { path: &'a str, target: &'a str },\n            Chmod { path: &'a str, mode: u32 },\n        }\n\n        enum CanonicalizeExpectation<'a> {\n            Canonical(&'a str),\n            PermissionDenied,\n        }\n\n        struct CanonicalizeCase<'a> {\n            name: &'a str,\n            setup: &'a [CanonicalizeFixture<'a>],\n            input: &'a str,\n            expected: CanonicalizeExpectation<'a>,\n            teardown: &'a [CanonicalizeFixture<'a>],\n        }\n\n        fn resolve(case_root: &Path, relative_path: &str) -> PathBuf {\n            case_root.join(relative_path)\n        }\n\n        fn apply_fixtures(case_root: &Path, fixtures: &[CanonicalizeFixture<'_>]) {\n            for fixture in fixtures {\n                match fixture {\n                    CanonicalizeFixture::Dir(path) => {\n                        std::fs::create_dir_all(resolve(case_root, path)).unwrap();\n                    }\n                    CanonicalizeFixture::File(path) => {\n                        let path = resolve(case_root, path);\n                        if let Some(parent) = path.parent() {\n                            std::fs::create_dir_all(parent).unwrap();\n                        }\n                        File::create_new(path).unwrap();\n                    }\n                    CanonicalizeFixture::Symlink { path, target } => {\n                        let path = resolve(case_root, path);\n                        if let Some(parent) = path.parent() {\n                            std::fs::create_dir_all(parent).unwrap();\n                        }\n                        unix::fs::symlink(resolve(case_root, target), path).unwrap();\n                    }\n                    CanonicalizeFixture::Chmod { path, mode } => {\n                        std::fs::set_permissions(resolve(case_root, path), std::fs::Permissions::from_mode(*mode))\n                            .unwrap();\n                    }\n                }\n            }\n        }\n\n        let tmp = tempfile::tempdir().unwrap();\n        let root = tmp.path();\n        let cases = [\n            CanonicalizeCase {\n                name: \"path exists\",\n                setup: &[\n                    CanonicalizeFixture::Dir(\"existing-parent\"),\n                    CanonicalizeFixture::File(\"existing-parent/existing-file\"),\n                ],\n                input: \"existing-parent/existing-file\",\n                expected: CanonicalizeExpectation::Canonical(\"existing-parent/existing-file\"),\n                teardown: &[],\n            },\n            CanonicalizeCase {\n                name: \"path doesn't exist\",\n                setup: &[CanonicalizeFixture::Dir(\"existing-parent\")],\n                input: \"existing-parent/missing-file\",\n                expected: CanonicalizeExpectation::Canonical(\"existing-parent/missing-file\"),\n                teardown: &[],\n            },\n            CanonicalizeCase {\n                name: \"path is symlink\",\n                setup: &[\n                    CanonicalizeFixture::Dir(\"existing-parent\"),\n                    CanonicalizeFixture::File(\"existing-parent/path-symlink-target\"),\n                    CanonicalizeFixture::Symlink {\n                        path: \"existing-parent/path-symlink\",\n                        target: \"existing-parent/path-symlink-target\",\n                    },\n                ],\n                input: \"existing-parent/path-symlink\",\n                expected: CanonicalizeExpectation::Canonical(\"existing-parent/path-symlink-target\"),\n                teardown: &[],\n            },\n            CanonicalizeCase {\n                name: \"no perms to path\",\n                setup: &[\n                    CanonicalizeFixture::Dir(\"no-perms-path-parent/locked-path\"),\n                    CanonicalizeFixture::Chmod { path: \"no-perms-path-parent/locked-path\", mode: 0o000 },\n                ],\n                input: \"no-perms-path-parent/locked-path/child\",\n                expected: CanonicalizeExpectation::PermissionDenied,\n                teardown: &[CanonicalizeFixture::Chmod { path: \"no-perms-path-parent/locked-path\", mode: 0o700 }],\n            },\n            CanonicalizeCase {\n                name: \"parent doesn't exist\",\n                setup: &[CanonicalizeFixture::Dir(\"parent-missing-grandparent\")],\n                input: \"parent-missing-grandparent/missing-parent/leaf\",\n                expected: CanonicalizeExpectation::Canonical(\"parent-missing-grandparent/missing-parent/leaf\"),\n                teardown: &[],\n            },\n            CanonicalizeCase {\n                name: \"parent is symlink\",\n                setup: &[\n                    CanonicalizeFixture::Dir(\"parent-symlink-target\"),\n                    CanonicalizeFixture::File(\"parent-symlink-target/leaf\"),\n                    CanonicalizeFixture::Dir(\"parent-symlink-grandparent\"),\n                    CanonicalizeFixture::Symlink {\n                        path: \"parent-symlink-grandparent/parent-link\",\n                        target: \"parent-symlink-target\",\n                    },\n                ],\n                input: \"parent-symlink-grandparent/parent-link/leaf\",\n                expected: CanonicalizeExpectation::Canonical(\"parent-symlink-target/leaf\"),\n                teardown: &[],\n            },\n            CanonicalizeCase {\n                name: \"parent is symlink but path doesn't exist\",\n                setup: &[\n                    CanonicalizeFixture::Dir(\"parent-symlink-target\"),\n                    CanonicalizeFixture::Dir(\"parent-symlink-grandparent\"),\n                    CanonicalizeFixture::Symlink {\n                        path: \"parent-symlink-grandparent/parent-link\",\n                        target: \"parent-symlink-target\",\n                    },\n                ],\n                input: \"parent-symlink-grandparent/parent-link/missing-leaf\",\n                expected: CanonicalizeExpectation::Canonical(\"parent-symlink-target/missing-leaf\"),\n                teardown: &[],\n            },\n            CanonicalizeCase {\n                name: \"no perms to parent\",\n                setup: &[\n                    CanonicalizeFixture::Dir(\"no-perms-parent-grandparent/locked-parent\"),\n                    CanonicalizeFixture::Chmod { path: \"no-perms-parent-grandparent/locked-parent\", mode: 0o000 },\n                ],\n                input: \"no-perms-parent-grandparent/locked-parent/leaf/child\",\n                expected: CanonicalizeExpectation::PermissionDenied,\n                teardown: &[CanonicalizeFixture::Chmod {\n                    path: \"no-perms-parent-grandparent/locked-parent\",\n                    mode: 0o700,\n                }],\n            },\n            CanonicalizeCase {\n                name: \"grandparent doesn't exist\",\n                setup: &[],\n                input: \"missing-grandparent/parent/leaf\",\n                expected: CanonicalizeExpectation::Canonical(\"missing-grandparent/parent/leaf\"),\n                teardown: &[],\n            },\n            CanonicalizeCase {\n                name: \"grandparent is symlink\",\n                setup: &[\n                    CanonicalizeFixture::Dir(\"grandparent-symlink-target/parent\"),\n                    CanonicalizeFixture::File(\"grandparent-symlink-target/parent/leaf\"),\n                    CanonicalizeFixture::Symlink { path: \"grandparent-link\", target: \"grandparent-symlink-target\" },\n                ],\n                input: \"grandparent-link/parent/leaf\",\n                expected: CanonicalizeExpectation::Canonical(\"grandparent-symlink-target/parent/leaf\"),\n                teardown: &[],\n            },\n            CanonicalizeCase {\n                name: \"grandparent is symlink but path doesn't exist\",\n                setup: &[\n                    CanonicalizeFixture::Dir(\"grandparent-symlink-target/parent\"),\n                    CanonicalizeFixture::Symlink { path: \"grandparent-link\", target: \"grandparent-symlink-target\" },\n                ],\n                input: \"grandparent-link/parent/missing-leaf\",\n                expected: CanonicalizeExpectation::Canonical(\"grandparent-symlink-target/parent/missing-leaf\"),\n                teardown: &[],\n            },\n            CanonicalizeCase {\n                name: \"grandparent is symlink but parent doesn't exist\",\n                setup: &[\n                    CanonicalizeFixture::Dir(\"grandparent-symlink-target\"),\n                    CanonicalizeFixture::Symlink { path: \"grandparent-link\", target: \"grandparent-symlink-target\" },\n                ],\n                input: \"grandparent-link/missing-parent/leaf\",\n                expected: CanonicalizeExpectation::Canonical(\"grandparent-symlink-target/missing-parent/leaf\"),\n                teardown: &[],\n            },\n            CanonicalizeCase {\n                name: \"grandparent is symlink + parent is symlink\",\n                setup: &[\n                    CanonicalizeFixture::Dir(\"grandparent-symlink-target\"),\n                    CanonicalizeFixture::Dir(\"gp-parent-symlink-target\"),\n                    CanonicalizeFixture::File(\"gp-parent-symlink-target/leaf\"),\n                    CanonicalizeFixture::Symlink {\n                        path: \"grandparent-symlink-target/parent-link\",\n                        target: \"gp-parent-symlink-target\",\n                    },\n                    CanonicalizeFixture::Symlink { path: \"grandparent-link\", target: \"grandparent-symlink-target\" },\n                ],\n                input: \"grandparent-link/parent-link/leaf\",\n                expected: CanonicalizeExpectation::Canonical(\"gp-parent-symlink-target/leaf\"),\n                teardown: &[],\n            },\n            CanonicalizeCase {\n                name: \"grandparent is symlink + path is symlink\",\n                setup: &[\n                    CanonicalizeFixture::Dir(\"grandparent-symlink-target/parent\"),\n                    CanonicalizeFixture::File(\"gp-path-symlink-target\"),\n                    CanonicalizeFixture::Symlink {\n                        path: \"grandparent-symlink-target/parent/path-link\",\n                        target: \"gp-path-symlink-target\",\n                    },\n                    CanonicalizeFixture::Symlink { path: \"grandparent-link\", target: \"grandparent-symlink-target\" },\n                ],\n                input: \"grandparent-link/parent/path-link\",\n                expected: CanonicalizeExpectation::Canonical(\"gp-path-symlink-target\"),\n                teardown: &[],\n            },\n            CanonicalizeCase {\n                name: \"grandparent is symlink + parent is symlink + path is symlink\",\n                setup: &[\n                    CanonicalizeFixture::Dir(\"grandparent-symlink-target\"),\n                    CanonicalizeFixture::Dir(\"gp-parent-path-parent-target\"),\n                    CanonicalizeFixture::Dir(\"gp-parent-path-symlink-target-parent\"),\n                    CanonicalizeFixture::File(\"gp-parent-path-symlink-target-parent/leaf-target\"),\n                    CanonicalizeFixture::Symlink {\n                        path: \"gp-parent-path-parent-target/path-link\",\n                        target: \"gp-parent-path-symlink-target-parent/leaf-target\",\n                    },\n                    CanonicalizeFixture::Symlink {\n                        path: \"grandparent-symlink-target/path-parent-link\",\n                        target: \"gp-parent-path-parent-target\",\n                    },\n                    CanonicalizeFixture::Symlink { path: \"grandparent-link\", target: \"grandparent-symlink-target\" },\n                ],\n                input: \"grandparent-link/path-parent-link/path-link\",\n                expected: CanonicalizeExpectation::Canonical(\"gp-parent-path-symlink-target-parent/leaf-target\"),\n                teardown: &[],\n            },\n            CanonicalizeCase {\n                name: \"no perms to grandparent\",\n                setup: &[\n                    CanonicalizeFixture::Dir(\"locked-grandparent/parent\"),\n                    CanonicalizeFixture::Chmod { path: \"locked-grandparent\", mode: 0o000 },\n                ],\n                input: \"locked-grandparent/parent/leaf/child\",\n                expected: CanonicalizeExpectation::PermissionDenied,\n                teardown: &[CanonicalizeFixture::Chmod { path: \"locked-grandparent\", mode: 0o700 }],\n            },\n        ];\n\n        for (index, case) in cases.iter().enumerate() {\n            let case_root = root.join(format!(\"case-{index}\"));\n            std::fs::create_dir(&case_root).unwrap();\n            apply_fixtures(&case_root, case.setup);\n\n            let input = resolve(&case_root, case.input);\n            match case.expected {\n                CanonicalizeExpectation::Canonical(expected) => {\n                    let expected = resolve(&case_root, expected);\n                    let canonical = canonicalize_path_or_parents(&input).unwrap_or_else(|err| {\n                        panic!(\"case `{}` unexpectedly failed for {:?}: {:?}\", case.name, input, err);\n                    });\n                    assert_eq!(canonical, expected, \"case `{}` produced an unexpected canonical path\", case.name);\n                }\n                CanonicalizeExpectation::PermissionDenied => match canonicalize_path_or_parents(&input).unwrap_err() {\n                    Error::FileSystem { path, source } => {\n                        assert_eq!(path, input, \"case `{}` returned an unexpected failing path\", case.name);\n                        assert_eq!(\n                            source.kind(),\n                            ErrorKind::PermissionDenied,\n                            \"case `{}` returned an unexpected error\",\n                            case.name\n                        );\n                    }\n                    err => panic!(\"case `{}` returned an unexpected error for {:?}: {:?}\", case.name, input, err),\n                },\n            }\n\n            apply_fixtures(&case_root, case.teardown);\n        }\n    }\n\n    //////////////////////////////////////////////////////////////////////////////////////\n    /// System\n    //////////////////////////////////////////////////////////////////////////////////////\n\n    #[derive(Debug)]\n    pub enum SystemTrashError {\n        NoTrashProgram,\n        Other(Error),\n    }\n    impl fmt::Display for SystemTrashError {\n        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {\n            write!(f, \"SystemTrashError during a `trash` operation: {:?}\", self)\n        }\n    }\n    impl std::error::Error for SystemTrashError {}\n\n    fn is_program_in_path(program: &str) -> bool {\n        if let Some(path_vars) = std::env::var_os(\"PATH\") {\n            for path in std::env::split_paths(&path_vars) {\n                let full_path = path.join(program);\n                if full_path.is_file() {\n                    return true;\n                }\n            }\n        }\n        false\n    }\n\n    /// This is based on the electron library's implementation.\n    /// See: https://github.com/electron/electron/blob/34c4c8d5088fa183f56baea28809de6f2a427e02/shell/common/platform_util_linux.cc#L96\n    pub fn delete_all_canonicalized_using_system_program(full_paths: Vec<PathBuf>) -> Result<(), SystemTrashError> {\n        static DEFAULT_TRASH: &str = \"gio\";\n        let trash = {\n            // Determine desktop environment and set accordingly.\n            let desktop_env = get_desktop_environment();\n            if desktop_env == DesktopEnvironment::Kde4 || desktop_env == DesktopEnvironment::Kde5 {\n                \"kioclient5\"\n            } else if desktop_env == DesktopEnvironment::Kde3 {\n                \"kioclient\"\n            } else {\n                DEFAULT_TRASH\n            }\n        };\n\n        let mut argv = Vec::<OsString>::with_capacity(full_paths.len() + 2);\n\n        if trash == \"kioclient5\" || trash == \"kioclient\" {\n            //argv.push(trash.into());\n            argv.push(\"move\".into());\n            for full_path in &full_paths {\n                argv.push(full_path.into());\n            }\n            argv.push(\"trash:/\".into());\n        } else {\n            //argv.push_back(ELECTRON_DEFAULT_TRASH);\n            argv.push(\"trash\".into());\n            for full_path in &full_paths {\n                argv.push(full_path.into());\n            }\n        }\n        if !is_program_in_path(trash) {\n            return Err(SystemTrashError::NoTrashProgram);\n        }\n        // Execute command\n        let mut command = Command::new(trash);\n        command.args(argv);\n        let result = command.output().map_err(|e| {\n            SystemTrashError::Other(Error::Unknown {\n                description: format!(\"Tried executing: {:?} - Error was: {}\", command, e),\n            })\n        })?;\n        if !result.status.success() {\n            let stderr = String::from_utf8_lossy(&result.stderr);\n            return Err(SystemTrashError::Other(Error::Unknown {\n                description: format!(\"Used '{}', stderr: {}\", trash, stderr),\n            }));\n        }\n        Ok(())\n    }\n\n    pub fn delete_all_using_system_program<I, T>(paths: I) -> Result<(), SystemTrashError>\n    where\n        I: IntoIterator<Item = T>,\n        T: AsRef<Path>,\n    {\n        let full_paths = canonicalize_paths(paths).map_err(SystemTrashError::Other)?;\n        delete_all_canonicalized_using_system_program(full_paths)\n    }\n\n    #[derive(PartialEq)]\n    enum DesktopEnvironment {\n        Other,\n        Cinnamon,\n        Gnome,\n        // KDE3, KDE4 and KDE5 are sufficiently different that we count\n        // them as different desktop environments here.\n        Kde3,\n        Kde4,\n        Kde5,\n        Pantheon,\n        Unity,\n        Xfce,\n    }\n\n    fn env_has_var(name: &str) -> bool {\n        env::var_os(name).is_some()\n    }\n\n    /// See: https://chromium.googlesource.com/chromium/src/+/dd407d416fa941c04e33d81f2b1d8cab8196b633/base/nix/xdg_util.cc#57\n    fn get_desktop_environment() -> DesktopEnvironment {\n        static KDE_SESSION_ENV_VAR: &str = \"KDE_SESSION_VERSION\";\n        // XDG_CURRENT_DESKTOP is the newest standard circa 2012.\n        if let Ok(xdg_current_desktop) = env::var(\"XDG_CURRENT_DESKTOP\") {\n            // It could have multiple values separated by colon in priority order.\n            for value in xdg_current_desktop.split(':') {\n                let value = value.trim();\n                if value.is_empty() {\n                    continue;\n                }\n                match value {\n                    \"Unity\" => {\n                        // gnome-fallback sessions set XDG_CURRENT_DESKTOP to Unity\n                        // DESKTOP_SESSION can be gnome-fallback or gnome-fallback-compiz\n                        if let Ok(desktop_session) = env::var(\"DESKTOP_SESSION\") {\n                            if desktop_session.contains(\"gnome-fallback\") {\n                                return DesktopEnvironment::Gnome;\n                            }\n                        }\n                        return DesktopEnvironment::Unity;\n                    }\n                    \"GNOME\" => {\n                        return DesktopEnvironment::Gnome;\n                    }\n                    \"X-Cinnamon\" => {\n                        return DesktopEnvironment::Cinnamon;\n                    }\n                    \"KDE\" => {\n                        if let Ok(kde_session) = env::var(KDE_SESSION_ENV_VAR) {\n                            if kde_session == \"5\" {\n                                return DesktopEnvironment::Kde5;\n                            }\n                        }\n                        return DesktopEnvironment::Kde4;\n                    }\n                    \"Pantheon\" => {\n                        return DesktopEnvironment::Pantheon;\n                    }\n                    \"XFCE\" => {\n                        return DesktopEnvironment::Xfce;\n                    }\n                    _ => {}\n                }\n            }\n        }\n\n        // DESKTOP_SESSION was what everyone  used in 2010.\n        if let Ok(desktop_session) = env::var(\"DESKTOP_SESSION\") {\n            match desktop_session.as_str() {\n                \"gnome\" | \"mate\" => {\n                    return DesktopEnvironment::Gnome;\n                }\n                \"kde4\" | \"kde-plasma\" => {\n                    return DesktopEnvironment::Kde4;\n                }\n                \"kde\" => {\n                    // This may mean KDE4 on newer systems, so we have to check.\n                    if env_has_var(KDE_SESSION_ENV_VAR) {\n                        return DesktopEnvironment::Kde4;\n                    }\n                    return DesktopEnvironment::Kde3;\n                }\n                \"xubuntu\" => {\n                    return DesktopEnvironment::Xfce;\n                }\n                _ => {}\n            }\n            if desktop_session.contains(\"xfce\") {\n                return DesktopEnvironment::Xfce;\n            }\n        }\n\n        // Fall back on some older environment variables.\n        // Useful particularly in the DESKTOP_SESSION=default case.\n        if env_has_var(\"GNOME_DESKTOP_SESSION_ID\") {\n            return DesktopEnvironment::Gnome;\n        } else if env_has_var(\"KDE_FULL_SESSION\") {\n            if env_has_var(KDE_SESSION_ENV_VAR) {\n                return DesktopEnvironment::Kde4;\n            }\n            return DesktopEnvironment::Kde3;\n        }\n\n        DesktopEnvironment::Other\n    }\n}\n\nfn fs_error(path: impl Into<PathBuf>, source: std::io::Error) -> Error {\n    Error::FileSystem { path: path.into(), source }\n}\n"
  },
  {
    "path": "src/lib.rs",
    "content": "//! This crate provides functions that allow moving files to the operating system's Recycle Bin or\n//! Trash, or the equivalent.\n//!\n//! Furthermore on Linux and on Windows additional functions are available from the `os_limited`\n//! module.\n//!\n//! ### Potential UB on Linux and FreeBSD\n//!\n//! When querying information about mount points, non-threadsafe versions of `libc::getmnt(info|ent)` are\n//! used which can cause UB if another thread calls into the same function, _probably_ only if the mountpoints\n//! changed as well.\n//!\n//! To neutralize the issue, the respective function in this crate has been made thread-safe with a Mutex.\n//!\n//! **If your crate calls into the aforementioned methods directly or indirectly from other threads,\n//! rather not use this crate.**\n//!\n//! As the handling of UB is clearly a trade-off and certainly goes against the zero-chance-of-UB goal\n//! of the Rust community, please interact with us [in the tracking issue](https://github.com/Byron/trash-rs/issues/42)\n//! to help find a more permanent solution.\n//!\n//! ### Notes on the Linux implementation\n//!\n//! This library implements version 1.0 of the [Freedesktop.org\n//! Trash](https://specifications.freedesktop.org/trash-spec/trashspec-1.0.html) specification and\n//! aims to match the behaviour of Ubuntu 18.04 GNOME in cases of ambiguity. Most -if not all- Linux\n//! distributions that ship with a desktop environment follow this specification. For example\n//! GNOME, KDE, and XFCE all use this convention. This crate blindly assumes that the Linux\n//! distribution it runs on, follows this specification.\n//!\n\nuse std::ffi::OsString;\nuse std::hash::{Hash, Hasher};\nuse std::path::{Path, PathBuf};\n\nuse std::fmt;\nuse std::{env::current_dir, error};\n\nuse log::trace;\n\n#[cfg(test)]\npub mod tests;\n\n#[cfg(target_os = \"windows\")]\n#[path = \"windows.rs\"]\nmod platform;\n\n#[cfg(all(unix, not(target_os = \"macos\"), not(target_os = \"ios\"), not(target_os = \"android\")))]\n#[path = \"freedesktop.rs\"]\nmod platform;\n\n#[cfg(target_os = \"macos\")]\npub mod macos;\n#[cfg(target_os = \"macos\")]\nuse macos as platform;\n\npub const DEFAULT_TRASH_CTX: TrashContext = TrashContext::new();\n\n/// A collection of preferences for trash operations.\n#[derive(Clone, Default, Debug)]\npub struct TrashContext {\n    #[cfg_attr(not(target_os = \"macos\"), allow(dead_code))]\n    platform_specific: platform::PlatformTrashContext,\n}\nimpl TrashContext {\n    pub const fn new() -> Self {\n        Self { platform_specific: platform::PlatformTrashContext::new() }\n    }\n\n    /// Removes a single file or directory.\n    ///\n    /// When a symbolic link is provided to this function, the symbolic link will be removed and the link\n    /// target will be kept intact.\n    ///\n    /// # Example\n    ///\n    /// ```\n    /// use std::fs::File;\n    /// use trash::delete;\n    /// File::create_new(\"delete_me\").unwrap();\n    /// trash::delete(\"delete_me\").unwrap();\n    /// assert!(File::open(\"delete_me\").is_err());\n    /// ```\n    pub fn delete<T: AsRef<Path>>(&self, path: T) -> Result<(), Error> {\n        self.delete_all(&[path])\n    }\n\n    /// Removes all files/directories specified by the collection of paths provided as an argument.\n    ///\n    /// When a symbolic link is provided to this function, the symbolic link will be removed and the link\n    /// target will be kept intact.\n    ///\n    /// # Example\n    ///\n    /// ```\n    /// use std::fs::File;\n    /// use trash::delete_all;\n    /// File::create_new(\"delete_me_1\").unwrap();\n    /// File::create_new(\"delete_me_2\").unwrap();\n    /// delete_all(&[\"delete_me_1\", \"delete_me_2\"]).unwrap();\n    /// assert!(File::open(\"delete_me_1\").is_err());\n    /// assert!(File::open(\"delete_me_2\").is_err());\n    /// ```\n    pub fn delete_all<I, T>(&self, paths: I) -> Result<(), Error>\n    where\n        I: IntoIterator<Item = T>,\n        T: AsRef<Path>,\n    {\n        trace!(\"Starting canonicalize_paths\");\n        let full_paths = canonicalize_paths(paths)?;\n        trace!(\"Finished canonicalize_paths\");\n        self.delete_all_canonicalized(full_paths)\n    }\n}\n\n/// Convenience method for `DEFAULT_TRASH_CTX.delete()`.\n///\n/// See: [`TrashContext::delete`](TrashContext::delete)\npub fn delete<T: AsRef<Path>>(path: T) -> Result<(), Error> {\n    DEFAULT_TRASH_CTX.delete(path)\n}\n\n/// Convenience method for `DEFAULT_TRASH_CTX.delete_all()`.\n///\n/// See: [`TrashContext::delete_all`](TrashContext::delete_all)\npub fn delete_all<I, T>(paths: I) -> Result<(), Error>\nwhere\n    I: IntoIterator<Item = T>,\n    T: AsRef<Path>,\n{\n    DEFAULT_TRASH_CTX.delete_all(paths)\n}\n\n/// Provides information about an error.\n#[derive(Debug)]\npub enum Error {\n    Unknown {\n        description: String,\n    },\n\n    Os {\n        code: i32,\n        description: String,\n    },\n\n    /// **freedesktop only**\n    ///\n    /// Error coming from file system\n    #[cfg(all(unix, not(target_os = \"macos\"), not(target_os = \"ios\"), not(target_os = \"android\")))]\n    FileSystem {\n        path: PathBuf,\n        source: std::io::Error,\n    },\n\n    /// One of the target items was a root folder.\n    /// If a list of items are requested to be removed by a single function call (e.g. `delete_all`)\n    /// and this error is returned, then it's guaranteed that none of the items is removed.\n    TargetedRoot,\n\n    /// The `target` does not exist or the process has insufficient permissions to access it.\n    CouldNotAccess {\n        target: String,\n    },\n\n    /// Error while canonicalizing path.\n    CanonicalizePath {\n        /// Path that triggered the error.\n        original: PathBuf,\n    },\n\n    /// Error while converting an [`OsString`] to a [`String`].\n    ///\n    /// This may also happen when converting a [`Path`] or [`PathBuf`] to an [`OsString`].\n    ConvertOsString {\n        /// The string that was attempted to be converted.\n        original: OsString,\n    },\n\n    /// This kind of error happens when a trash item's original parent already contains an item with\n    /// the same name and type (file or folder). In this case an error is produced and the\n    /// restoration of the files is halted meaning that there may be files that could be restored\n    /// but were left in the trash due to the error.\n    ///\n    /// One should not assume any relationship between the order that the items were supplied and\n    /// the list of remaining items. That is to say, it may be that the item that collided was in\n    /// the middle of the provided list but the remaining items' list contains all the provided\n    /// items.\n    ///\n    /// `path`: The path of the file that's blocking the trash item from being restored.\n    ///\n    /// `remaining_items`: All items that were not restored in the order they were provided,\n    /// starting with the item that triggered the error.\n    RestoreCollision {\n        path: PathBuf,\n        remaining_items: Vec<TrashItem>,\n    },\n\n    /// This sort of error is returned when multiple items with the same `original_path` were\n    /// requested to be restored. These items are referred to as twins here. If there are twins\n    /// among the items, then none of the items are restored.\n    ///\n    /// `path`: The `original_path` of the twins.\n    ///\n    /// `items`: The complete list of items that were handed over to the `restore_all` function.\n    RestoreTwins {\n        path: PathBuf,\n        items: Vec<TrashItem>,\n    },\n}\nimpl fmt::Display for Error {\n    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {\n        write!(f, \"Error during a `trash` operation: {self:?}\")\n    }\n}\nimpl error::Error for Error {\n    fn source(&self) -> Option<&(dyn error::Error + 'static)> {\n        match self {\n            #[cfg(all(unix, not(target_os = \"macos\"), not(target_os = \"ios\"), not(target_os = \"android\")))]\n            Self::FileSystem { path: _, source: e } => e.source(),\n            _ => None,\n        }\n    }\n}\npub fn into_unknown<E: std::fmt::Display>(err: E) -> Error {\n    Error::Unknown { description: format!(\"{err}\") }\n}\n\npub(crate) fn canonicalize_paths<I, T>(paths: I) -> Result<Vec<PathBuf>, Error>\nwhere\n    I: IntoIterator<Item = T>,\n    T: AsRef<Path>,\n{\n    let paths = paths.into_iter();\n    paths\n        .map(|x| {\n            let target_ref = x.as_ref();\n            if target_ref.as_os_str().is_empty() {\n                return Err(Error::CanonicalizePath { original: target_ref.to_owned() });\n            }\n            let target = if target_ref.is_relative() {\n                let curr_dir = current_dir()\n                    .map_err(|_| Error::CouldNotAccess { target: \"[Current working directory]\".into() })?;\n                curr_dir.join(target_ref)\n            } else {\n                target_ref.to_owned()\n            };\n            let parent = target.parent().ok_or(Error::TargetedRoot)?;\n            let canonical_parent =\n                parent.canonicalize().map_err(|_| Error::CanonicalizePath { original: parent.to_owned() })?;\n            if let Some(file_name) = target.file_name() {\n                Ok(canonical_parent.join(file_name))\n            } else {\n                // `file_name` is none if the path ends with `..`\n                Ok(canonical_parent)\n            }\n        })\n        .collect::<Result<Vec<_>, _>>()\n}\n\n/// This struct holds information about a single item within the trash.\n///\n/// A trash item can be a file or folder or any other object that the target\n/// operating system allows to put into the trash.\n#[derive(Debug, Clone)]\npub struct TrashItem {\n    /// A system specific identifier of the item in the trash.\n    ///\n    /// On Windows it is the string returned by `IShellItem::GetDisplayName`\n    /// with the `SIGDN_DESKTOPABSOLUTEPARSING` flag.\n    ///\n    /// On Linux it is an absolute path to the `.trashinfo` file associated with\n    /// the item.\n    pub id: OsString,\n\n    /// The name of the item. For example if the folder '/home/user/New Folder'\n    /// was deleted, its `name` is 'New Folder'\n    pub name: OsString,\n\n    /// The path to the parent folder of this item before it was put inside the\n    /// trash. For example if the folder '/home/user/New Folder' is in the\n    /// trash, its `original_parent` is '/home/user'.\n    ///\n    /// To get the full path to the file in its original location use the\n    /// `original_path` function.\n    pub original_parent: PathBuf,\n\n    /// The number of non-leap seconds elapsed between the UNIX Epoch and the\n    /// moment the file was deleted.\n    /// Without the \"chrono\" feature, this will be a negative number on linux only.\n    pub time_deleted: i64,\n}\n\nimpl TrashItem {\n    /// Joins the `original_parent` and `name` fields to obtain the full path to\n    /// the original file.\n    pub fn original_path(&self) -> PathBuf {\n        self.original_parent.join(&self.name)\n    }\n}\nimpl PartialEq for TrashItem {\n    fn eq(&self, other: &Self) -> bool {\n        self.id == other.id\n    }\n}\nimpl Eq for TrashItem {}\nimpl Hash for TrashItem {\n    fn hash<H: Hasher>(&self, state: &mut H) {\n        self.id.hash(state);\n    }\n}\n\n/// Size of a [`TrashItem`] in bytes or entries\n#[derive(Debug, Clone, Copy, Ord, PartialOrd, Eq, PartialEq, Hash)]\npub enum TrashItemSize {\n    /// Number of bytes in a file\n    Bytes(u64),\n    /// Number of entries in a directory, non-recursive\n    Entries(usize),\n}\n\nimpl TrashItemSize {\n    /// The size of a file in bytes, if this item is a file.\n    pub fn size(&self) -> Option<u64> {\n        match self {\n            TrashItemSize::Bytes(s) => Some(*s),\n            TrashItemSize::Entries(_) => None,\n        }\n    }\n\n    /// The amount of entries in the directory, if this is a directory.\n    pub fn entries(&self) -> Option<usize> {\n        match self {\n            TrashItemSize::Bytes(_) => None,\n            TrashItemSize::Entries(e) => Some(*e),\n        }\n    }\n}\n\n/// Metadata about a [`TrashItem`]\n#[derive(Debug, Clone, Copy, Ord, PartialOrd, Eq, PartialEq, Hash)]\npub struct TrashItemMetadata {\n    /// The size of the item, depending on whether or not it is a directory.\n    pub size: TrashItemSize,\n}\n\n#[cfg(any(\n    target_os = \"windows\",\n    all(unix, not(target_os = \"macos\"), not(target_os = \"ios\"), not(target_os = \"android\"))\n))]\npub mod os_limited {\n    //! This module provides functionality which is only supported on Windows and\n    //! Linux or other Freedesktop Trash compliant environment.\n\n    use std::{\n        borrow::Borrow,\n        collections::HashSet,\n        hash::{Hash, Hasher},\n    };\n\n    use super::{platform, Error, TrashItem, TrashItemMetadata};\n\n    /// Returns all [`TrashItem`]s that are currently in the trash.\n    ///\n    /// The items are in no particular order and must be sorted when any kind of ordering is required.\n    ///\n    /// # Example\n    ///\n    /// ```\n    /// use trash::os_limited::list;\n    /// let trash_items = list().unwrap();\n    /// println!(\"{:#?}\", trash_items);\n    /// ```\n    pub fn list() -> Result<Vec<TrashItem>, Error> {\n        platform::list()\n    }\n\n    /// Returns whether the trash is empty or has at least one item.\n    ///\n    /// Unlike calling [`list`], this function short circuits without evaluating every item.\n    ///\n    /// # Example\n    ///\n    /// ```\n    /// use trash::os_limited::is_empty;\n    /// if is_empty().unwrap_or(true) {\n    ///     println!(\"Trash is empty\");\n    /// } else {\n    ///     println!(\"Trash contains at least one item\");\n    /// }\n    /// ```\n    pub fn is_empty() -> Result<bool, Error> {\n        platform::is_empty()\n    }\n\n    /// Returns all valid trash bins on supported Unix platforms.\n    ///\n    /// Valid trash folders include the user's personal \"home trash\" as well as designated trash\n    /// bins across mount points. Some, or all of these, may not exist or be invalid in some way.\n    ///\n    /// # Example\n    ///\n    /// ```\n    /// # #[cfg(all(unix, not(target_os = \"macos\"), not(target_os = \"ios\"), not(target_os = \"android\")))] {\n    /// use trash::os_limited::trash_folders;\n    /// let trash_bins = trash_folders()?;\n    /// println!(\"{trash_bins:#?}\");\n    /// # }\n    /// # Ok::<(), trash::Error>(())\n    /// ```\n    #[cfg(all(unix, not(target_os = \"macos\"), not(target_os = \"ios\"), not(target_os = \"android\")))]\n    pub fn trash_folders() -> Result<HashSet<std::path::PathBuf>, Error> {\n        platform::trash_folders()\n    }\n\n    /// Returns the [`TrashItemMetadata`] for a [`TrashItem`]\n    ///\n    /// # Example\n    ///\n    /// ```\n    /// use trash::os_limited::{list, metadata};\n    /// let trash_items = list().unwrap();\n    /// for item in trash_items {\n    ///     println!(\"{:#?}\", metadata(&item).unwrap());\n    /// }\n    /// ```\n    pub fn metadata(item: &TrashItem) -> Result<TrashItemMetadata, Error> {\n        platform::metadata(item)\n    }\n\n    /// Deletes all the provided [`TrashItem`]s permanently.\n    ///\n    /// This function consumes the provided items.\n    ///\n    /// # Example\n    ///\n    /// Taking items' ownership:\n    ///\n    /// ```\n    /// use std::fs::File;\n    /// use trash::{delete, os_limited::{list, purge_all}};\n    ///\n    /// let filename = \"trash-purge_all-example-ownership\";\n    /// File::create_new(filename).unwrap();\n    /// delete(filename).unwrap();\n    /// // Collect the filtered list just so that we can make sure there's exactly one element.\n    /// // There's no need to `collect` it otherwise.\n    /// let selected: Vec<_> = list().unwrap().into_iter().filter(|x| x.name == filename).collect();\n    /// assert_eq!(selected.len(), 1);\n    /// purge_all(selected).unwrap();\n    /// ```\n    ///\n    /// Taking items' reference:\n    ///\n    /// ```\n    /// use std::fs::File;\n    /// use trash::{delete, os_limited::{list, purge_all}};\n    ///\n    /// let filename = \"trash-purge_all-example-reference\";\n    /// File::create_new(filename).unwrap();\n    /// delete(filename).unwrap();\n    /// let mut selected = list().unwrap();\n    /// selected.retain(|x| x.name == filename);\n    /// assert_eq!(selected.len(), 1);\n    /// purge_all(&selected).unwrap();\n    /// ```\n    pub fn purge_all<I>(items: I) -> Result<(), Error>\n    where\n        I: IntoIterator,\n        <I as IntoIterator>::Item: Borrow<TrashItem>,\n    {\n        platform::purge_all(items)\n    }\n\n    /// Restores all the provided [`TrashItem`] to their original location.\n    ///\n    /// This function consumes the provided items.\n    ///\n    /// # Errors\n    ///\n    /// Errors this function may return include but are not limited to the following.\n    ///\n    /// It may be the case that when restoring a file or a folder, the `original_path` already has\n    /// a new item with the same name. When such a collision happens this function returns a\n    /// [`RestoreCollision`] kind of error.\n    ///\n    /// If two or more of the provided items have identical `original_path`s then a\n    /// [`RestoreTwins`] kind of error is returned.\n    ///\n    /// # Example\n    ///\n    /// Basic usage:\n    ///\n    /// ```\n    /// use std::fs::File;\n    /// use trash::os_limited::{list, restore_all};\n    ///\n    /// let filename = \"trash-restore_all-example\";\n    /// File::create_new(filename).unwrap();\n    /// restore_all(list().unwrap().into_iter().filter(|x| x.name == filename)).unwrap();\n    /// std::fs::remove_file(filename).unwrap();\n    /// ```\n    ///\n    /// Retry restoring when encountering [`RestoreCollision`] error:\n    ///\n    /// ```no_run\n    /// use trash::os_limited::{list, restore_all};\n    /// use trash::Error::RestoreCollision;\n    ///\n    /// let items = list().unwrap();\n    /// if let Err(RestoreCollision { path, mut remaining_items }) = restore_all(items) {\n    ///     // keep all except the one(s) that couldn't be restored\n    ///     remaining_items.retain(|e| e.original_path() != path);\n    ///     restore_all(remaining_items).unwrap();\n    /// }\n    /// ```\n    ///\n    /// [`RestoreCollision`]: Error::RestoreCollision\n    /// [`RestoreTwins`]: Error::RestoreTwins\n    pub fn restore_all<I>(items: I) -> Result<(), Error>\n    where\n        I: IntoIterator<Item = TrashItem>,\n    {\n        // Check for twins here cause that's pretty platform independent.\n        struct ItemWrapper<'a>(&'a TrashItem);\n        impl PartialEq for ItemWrapper<'_> {\n            fn eq(&self, other: &Self) -> bool {\n                self.0.original_path() == other.0.original_path()\n            }\n        }\n        impl Eq for ItemWrapper<'_> {}\n        impl Hash for ItemWrapper<'_> {\n            fn hash<H: Hasher>(&self, state: &mut H) {\n                self.0.original_path().hash(state);\n            }\n        }\n        let items = items.into_iter().collect::<Vec<_>>();\n        let mut item_set = HashSet::with_capacity(items.len());\n        for item in items.iter() {\n            if !item_set.insert(ItemWrapper(item)) {\n                return Err(Error::RestoreTwins { path: item.original_path(), items });\n            }\n        }\n        platform::restore_all(items)\n    }\n}\n"
  },
  {
    "path": "src/macos/mod.rs",
    "content": "use std::{\n    ffi::OsString,\n    path::{Path, PathBuf},\n    process::Command,\n};\n\nuse log::trace;\nuse objc2_foundation::{NSFileManager, NSString, NSURL};\n\nuse crate::{into_unknown, Error, TrashContext};\n\n#[derive(Copy, Clone, Debug)]\n/// There are 2 ways to trash files: via the ≝Finder app or via the OS NsFileManager call\n///\n///   | <br>Feature            |≝<br>Finder     |<br>NsFileManager |\n///   |:-----------------------|:--------------:|:----------------:|\n///   |Undo via \"Put back\"     | ✓              | ✗                |\n///   |Speed                   | ✗<br>Slower    | ✓<br>Faster      |\n///   |No sound                | ✗              | ✓                |\n///   |No extra permissions    | ✗              | ✓                |\n///\npub enum DeleteMethod {\n    /// Use an `osascript`, asking the Finder application to delete the files.\n    ///\n    /// - Might ask the user to give additional permissions to the app\n    /// - Produces the sound that Finder usually makes when deleting a file\n    /// - Shows the \"Put Back\" option in the context menu, when using the Finder application\n    ///\n    /// This is the default.\n    Finder,\n\n    /// Use `trashItemAtURL` from the `NSFileManager` object to delete the files.\n    ///\n    /// - Somewhat faster than the `Finder` method\n    /// - Does *not* require additional permissions\n    /// - Does *not* produce the sound that Finder usually makes when deleting a file\n    /// - Does *not* show the \"Put Back\" option on some systems (the file may be restored by for\n    ///   example dragging out from the Trash folder). This is a macOS bug. Read more about it\n    ///   at:\n    ///   - <https://github.com/sindresorhus/macos-trash/issues/4>\n    ///   - <https://github.com/ArturKovacs/trash-rs/issues/14>\n    NsFileManager,\n}\nimpl DeleteMethod {\n    /// Returns `DeleteMethod::Finder`\n    pub const fn new() -> Self {\n        DeleteMethod::Finder\n    }\n}\nimpl Default for DeleteMethod {\n    fn default() -> Self {\n        Self::new()\n    }\n}\n#[derive(Clone, Default, Debug)]\npub struct PlatformTrashContext {\n    delete_method: DeleteMethod,\n}\nimpl PlatformTrashContext {\n    pub const fn new() -> Self {\n        Self { delete_method: DeleteMethod::new() }\n    }\n}\npub trait TrashContextExtMacos {\n    fn set_delete_method(&mut self, method: DeleteMethod);\n    fn delete_method(&self) -> DeleteMethod;\n}\nimpl TrashContextExtMacos for TrashContext {\n    fn set_delete_method(&mut self, method: DeleteMethod) {\n        self.platform_specific.delete_method = method;\n    }\n    fn delete_method(&self) -> DeleteMethod {\n        self.platform_specific.delete_method\n    }\n}\nimpl TrashContext {\n    pub(crate) fn delete_all_canonicalized(&self, full_paths: Vec<PathBuf>) -> Result<(), Error> {\n        match self.platform_specific.delete_method {\n            DeleteMethod::Finder => delete_using_finder(&full_paths),\n            DeleteMethod::NsFileManager => delete_using_file_mgr(&full_paths),\n        }\n    }\n}\n\nfn delete_using_file_mgr<P: AsRef<Path>>(full_paths: &[P]) -> Result<(), Error> {\n    trace!(\"Starting delete_using_file_mgr\");\n    let file_mgr = NSFileManager::defaultManager();\n    for path in full_paths {\n        let path = path.as_ref().as_os_str().as_encoded_bytes();\n        let path = match std::str::from_utf8(path) {\n            Ok(path_utf8) => NSString::from_str(path_utf8), // utf-8 path, use as is\n            Err(_) => NSString::from_str(&percent_encode(path)), // binary path, %-encode it\n        };\n\n        trace!(\"Starting fileURLWithPath\");\n        let url = NSURL::fileURLWithPath(&path);\n        trace!(\"Finished fileURLWithPath\");\n\n        trace!(\"Calling trashItemAtURL\");\n        let res = file_mgr.trashItemAtURL_resultingItemURL_error(&url, None);\n        trace!(\"Finished trashItemAtURL\");\n\n        if let Err(err) = res {\n            return Err(Error::Unknown {\n                description: format!(\"While deleting '{:?}', `trashItemAtURL` failed: {err}\", &path),\n            });\n        }\n    }\n    Ok(())\n}\n\nfn delete_using_finder<P: AsRef<Path>>(full_paths: &[P]) -> Result<(), Error> {\n    // AppleScript command to move files (or directories) to Trash looks like\n    //   osascript -e 'tell application \"Finder\" to delete { POSIX file \"file1\", POSIX \"file2\" }'\n    // The `-e` flag is used to execute only one line of AppleScript.\n    let mut command = Command::new(\"osascript\");\n    let posix_files = full_paths\n        .iter()\n        .map(|p| {\n            let path_b = p.as_ref().as_os_str().as_encoded_bytes();\n            match std::str::from_utf8(path_b) {\n                Ok(path_utf8) => format!(r#\"POSIX file \"{}\"\"#, esc_quote(path_utf8)), // utf-8 path, escape \\\"\n                Err(_) => format!(r#\"POSIX file \"{}\"\"#, esc_quote(&percent_encode(path_b))), // binary path, %-encode it and escape \\\"\n            }\n        })\n        .collect::<Vec<String>>()\n        .join(\", \");\n    let script = format!(\"tell application \\\"Finder\\\" to delete {{ {posix_files} }}\");\n\n    let argv: Vec<OsString> = vec![\"-e\".into(), script.into()];\n    command.args(argv);\n\n    // Execute command\n    let result = command.output().map_err(into_unknown)?;\n    if !result.status.success() {\n        let stderr = String::from_utf8_lossy(&result.stderr);\n        match result.status.code() {\n            None => {\n                return Err(Error::Unknown {\n                    description: format!(\"The AppleScript exited with error. stderr: {}\", stderr),\n                })\n            }\n\n            Some(code) => {\n                return Err(Error::Os {\n                    code,\n                    description: format!(\"The AppleScript exited with error. stderr: {}\", stderr),\n                })\n            }\n        };\n    }\n    Ok(())\n}\n\n/// std's from_utf8_lossy, but non-utf8 byte sequences are %-encoded instead of being replaced by a special symbol.\n/// Valid utf8, including `%`, are not escaped.\nuse std::borrow::Cow;\nfn percent_encode(input: &[u8]) -> Cow<'_, str> {\n    use percent_encoding::percent_encode_byte as b2pc;\n\n    let mut iter = input.utf8_chunks().peekable();\n    if let Some(chunk) = iter.peek() {\n        if chunk.invalid().is_empty() {\n            return Cow::Borrowed(chunk.valid());\n        }\n    } else {\n        return Cow::Borrowed(\"\");\n    };\n\n    let mut res = String::with_capacity(input.len());\n    for chunk in iter {\n        res.push_str(chunk.valid());\n        let invalid = chunk.invalid();\n        if !invalid.is_empty() {\n            for byte in invalid {\n                res.push_str(b2pc(*byte));\n            }\n        }\n    }\n    Cow::Owned(res)\n}\n\n/// Escapes `\"` or `\\` with `\\` for use in AppleScript text\nfn esc_quote(s: &str) -> Cow<'_, str> {\n    if s.contains(['\"', '\\\\']) {\n        let mut r = String::with_capacity(s.len());\n        let chars = s.chars();\n        for c in chars {\n            match c {\n                '\"' | '\\\\' => {\n                    r.push('\\\\');\n                    r.push(c);\n                } // escapes quote/escape char\n                _ => {\n                    r.push(c);\n                } // no escape required\n            }\n        }\n        Cow::Owned(r)\n    } else {\n        Cow::Borrowed(s)\n    }\n}\n\n#[cfg(test)]\nmod tests;\n"
  },
  {
    "path": "src/macos/tests.rs",
    "content": "use crate::{\n    macos::{percent_encode, DeleteMethod, TrashContextExtMacos},\n    tests::{get_unique_name, init_logging},\n    TrashContext,\n};\nuse serial_test::serial;\nuse std::ffi::OsStr;\nuse std::fs::File;\nuse std::os::unix::ffi::OsStrExt;\nuse std::path::PathBuf;\nuse std::process::Command;\n\n#[test]\n#[serial]\nfn test_delete_with_finder_quoted_paths() {\n    init_logging();\n    let mut trash_ctx = TrashContext::default();\n    trash_ctx.set_delete_method(DeleteMethod::Finder);\n\n    let mut path1 = PathBuf::from(get_unique_name());\n    let mut path2 = PathBuf::from(get_unique_name());\n    path1.set_extension(r#\"a\"b,\"#);\n    path2.set_extension(r#\"x80=%80 slash=\\ pc=% quote=\" comma=,\"#);\n    File::create_new(&path1).unwrap();\n    File::create_new(&path2).unwrap();\n    trash_ctx.delete_all(&[&path1, &path2]).unwrap();\n    assert!(!path1.exists());\n    assert!(!path2.exists());\n}\n\n#[test]\n#[serial]\nfn test_delete_with_ns_file_manager() {\n    init_logging();\n    let mut trash_ctx = TrashContext::default();\n    trash_ctx.set_delete_method(DeleteMethod::NsFileManager);\n\n    let path = get_unique_name();\n    File::create_new(&path).unwrap();\n    trash_ctx.delete(&path).unwrap();\n    assert!(File::open(&path).is_err());\n}\n\n#[test]\n#[serial]\nfn test_delete_binary_path_with_ns_file_manager() {\n    let (_cleanup, tmp) = create_hfs_volume().unwrap();\n    let parent_fs_supports_binary = tmp.path();\n\n    init_logging();\n    for method in [DeleteMethod::NsFileManager, DeleteMethod::Finder] {\n        let mut trash_ctx = TrashContext::default();\n        trash_ctx.set_delete_method(method);\n\n        let mut path_invalid = parent_fs_supports_binary.join(get_unique_name());\n        path_invalid.set_extension(OsStr::from_bytes(b\"\\x80\\\"\\\\\")); //...trash-test-111-0.\\x80 (not push to avoid fail unexisting dir)\n\n        File::create_new(&path_invalid).unwrap();\n\n        assert!(path_invalid.exists());\n        trash_ctx.delete(&path_invalid).unwrap();\n        assert!(!path_invalid.exists());\n    }\n}\n\n#[test]\nfn test_path_byte() {\n    let invalid_utf8 = b\"\\x80\"; // lone continuation byte (128) (invalid utf8)\n    let percent_encoded = \"%80\"; // valid macOS path in a %-escaped encoding\n\n    let mut expected_path = PathBuf::from(get_unique_name());\n    let mut path_with_invalid_utf8 = expected_path.clone();\n\n    path_with_invalid_utf8.push(OsStr::from_bytes(invalid_utf8)); //      trash-test-111-0/\\x80\n    expected_path.push(percent_encoded); //                    trash-test-111-0/%80\n\n    let actual = percent_encode(&path_with_invalid_utf8.as_os_str().as_encoded_bytes()); // trash-test-111-0/%80\n    assert_eq!(std::path::Path::new(actual.as_ref()), expected_path);\n}\n\nfn create_hfs_volume() -> std::io::Result<(impl Drop, tempfile::TempDir)> {\n    let tmp = tempfile::tempdir()?;\n    let dmg_file = tmp.path().join(\"fs.dmg\");\n    let cleanup = {\n        // Create dmg file\n        Command::new(\"hdiutil\").args([\"create\", \"-size\", \"1m\", \"-fs\", \"HFS+\"]).arg(&dmg_file).status()?;\n\n        // Mount dmg file into temporary location\n        Command::new(\"hdiutil\").args([\"attach\", \"-nobrowse\", \"-mountpoint\"]).arg(tmp.path()).arg(&dmg_file).status()?;\n\n        // Ensure that the mount point is always cleaned up\n        defer::defer({\n            let mount_point = tmp.path().to_owned();\n            move || {\n                Command::new(\"hdiutil\")\n                    .arg(\"detach\")\n                    .arg(&mount_point)\n                    .status()\n                    .expect(\"detach temporary test dmg filesystem successfully\");\n            }\n        })\n    };\n    Ok((cleanup, tmp))\n}\n"
  },
  {
    "path": "src/tests.rs",
    "content": "mod utils {\n\n    use std::sync::atomic::{AtomicI32, Ordering};\n\n    use once_cell::sync::Lazy;\n\n    // WARNING Expecting that `cargo test` won't be invoked on the same computer more than once within\n    // a single millisecond\n    static INSTANCE_ID: Lazy<i64> = Lazy::new(|| chrono::Local::now().timestamp_millis());\n    static ID_OFFSET: AtomicI32 = AtomicI32::new(0);\n\n    pub fn get_unique_name() -> String {\n        let id = ID_OFFSET.fetch_add(1, Ordering::SeqCst);\n        format!(\"trash-test-{}-{}\", *INSTANCE_ID, id)\n    }\n\n    pub fn init_logging() {\n        let _ = env_logger::builder().is_test(true).try_init();\n    }\n}\n\npub use utils::{get_unique_name, init_logging};\n\n#[cfg(any(\n    target_os = \"windows\",\n    all(unix, not(target_os = \"macos\"), not(target_os = \"ios\"), not(target_os = \"android\"))\n))]\nmod os_limited {\n    use super::{get_unique_name, init_logging};\n    use serial_test::serial;\n    use std::collections::{hash_map::Entry, HashMap};\n    use std::ffi::{OsStr, OsString};\n    use std::fs::File;\n\n    #[cfg(all(unix, not(target_os = \"macos\"), not(target_os = \"ios\"), not(target_os = \"android\")))]\n    use std::os::unix::ffi::OsStringExt;\n\n    use crate as trash;\n\n    #[test]\n    #[serial]\n    fn list() {\n        const MAX_SECONDS_DIFFERENCE: i64 = 10;\n        init_logging();\n\n        let deletion_time = chrono::Utc::now();\n        let actual_unix_deletion_time = deletion_time.naive_utc().timestamp();\n        assert_eq!(actual_unix_deletion_time, deletion_time.naive_local().timestamp());\n        let file_name_prefix = get_unique_name();\n        let batches: usize = 2;\n        let files_per_batch: usize = 3;\n        let names: Vec<OsString> = (0..files_per_batch).map(|i| format!(\"{}#{}\", file_name_prefix, i).into()).collect();\n        for _ in 0..batches {\n            for path in names.iter() {\n                File::create_new(path).unwrap();\n            }\n            trash::delete_all(&names).unwrap();\n        }\n        let items = trash::os_limited::list().unwrap();\n        let items: HashMap<_, Vec<_>> = items\n            .into_iter()\n            .filter(|x| x.name.as_encoded_bytes().starts_with(file_name_prefix.as_bytes()))\n            .fold(HashMap::new(), |mut map, x| {\n                match map.entry(x.name.clone()) {\n                    Entry::Occupied(mut entry) => {\n                        entry.get_mut().push(x);\n                    }\n                    Entry::Vacant(entry) => {\n                        entry.insert(vec![x]);\n                    }\n                }\n                map\n            });\n        for name in names {\n            match items.get(&name) {\n                Some(items) => {\n                    assert_eq!(items.len(), batches);\n                    for item in items {\n                        if cfg!(feature = \"chrono\") {\n                            let diff = (item.time_deleted - actual_unix_deletion_time).abs();\n                            if diff > MAX_SECONDS_DIFFERENCE {\n                                panic!(\n                                    \"The deleted item does not have the timestamp that represents its deletion time. Expected: {}. Got: {}\",\n                                    actual_unix_deletion_time,\n                                    item.time_deleted\n                                );\n                            }\n                        }\n                    }\n                }\n                None => panic!(\"ERROR Could not find '{:?}' in {:#?}\", name, items),\n            }\n        }\n\n        // Let's try to purge all the items we just created but ignore any errors\n        // as this test should succeed as long as `list` works properly.\n        let _ = trash::os_limited::purge_all(items.iter().flat_map(|(_name, item)| item));\n    }\n\n    #[cfg(all(unix, not(target_os = \"macos\"), not(target_os = \"ios\"), not(target_os = \"android\")))]\n    #[test]\n    #[serial]\n    fn list_invalid_utf8() {\n        let mut name = OsStr::new(&get_unique_name()).to_os_string().into_encoded_bytes();\n        name.push(168);\n        let name = OsString::from_vec(name);\n        File::create_new(&name).unwrap();\n\n        // Delete, list, and remove file with an invalid UTF8 name\n        // Listing items is already exhaustively checked above, so this test is mainly concerned\n        // with checking that listing non-Unicode names does not panic\n        trash::delete(&name).unwrap();\n        let item = trash::os_limited::list().unwrap().into_iter().find(|item| item.name == name).unwrap();\n        let _ = trash::os_limited::purge_all([item]);\n    }\n\n    #[test]\n    fn purge_empty() {\n        init_logging();\n        trash::os_limited::purge_all::<Vec<trash::TrashItem>>(vec![]).unwrap();\n    }\n\n    #[test]\n    fn restore_empty() {\n        init_logging();\n        trash::os_limited::restore_all(vec![]).unwrap();\n    }\n\n    #[test]\n    #[serial]\n    fn purge() {\n        init_logging();\n        let file_name_prefix = get_unique_name();\n        let batches: usize = 2;\n        let files_per_batch: usize = 3;\n        let names: Vec<_> = (0..files_per_batch).map(|i| format!(\"{}#{}\", file_name_prefix, i)).collect();\n        for _ in 0..batches {\n            for path in names.iter() {\n                File::create_new(path).unwrap();\n            }\n            trash::delete_all(&names).unwrap();\n        }\n\n        // Collect it because we need the exact number of items gathered.\n        let targets: Vec<_> = trash::os_limited::list()\n            .unwrap()\n            .into_iter()\n            .filter(|x| x.name.as_encoded_bytes().starts_with(file_name_prefix.as_bytes()))\n            .collect();\n        assert_eq!(targets.len(), batches * files_per_batch);\n        trash::os_limited::purge_all(targets).unwrap();\n        let remaining = trash::os_limited::list()\n            .unwrap()\n            .into_iter()\n            .filter(|x| x.name.as_encoded_bytes().starts_with(file_name_prefix.as_bytes()))\n            .count();\n        assert_eq!(remaining, 0);\n    }\n\n    #[test]\n    #[serial]\n    fn restore() {\n        init_logging();\n        let file_name_prefix = get_unique_name();\n        let file_count: usize = 3;\n        let names: Vec<_> = (0..file_count).map(|i| format!(\"{}#{}\", file_name_prefix, i)).collect();\n        for path in names.iter() {\n            File::create_new(path).unwrap();\n        }\n        trash::delete_all(&names).unwrap();\n\n        // Collect it because we need the exact number of items gathered.\n        let targets: Vec<_> = trash::os_limited::list()\n            .unwrap()\n            .into_iter()\n            .filter(|x| x.name.as_encoded_bytes().starts_with(file_name_prefix.as_bytes()))\n            .collect();\n        assert_eq!(targets.len(), file_count);\n        trash::os_limited::restore_all(targets).unwrap();\n        let remaining = trash::os_limited::list()\n            .unwrap()\n            .into_iter()\n            .filter(|x| x.name.as_encoded_bytes().starts_with(file_name_prefix.as_bytes()))\n            .count();\n        assert_eq!(remaining, 0);\n\n        // They are not in the trash anymore but they should be at their original location\n        let mut missing = Vec::new();\n        for path in names.iter() {\n            if !std::path::Path::new(&path).is_file() {\n                missing.push(path);\n            }\n        }\n        for path in names.iter() {\n            std::fs::remove_file(path).ok();\n        }\n\n        assert_eq!(missing, Vec::<&String>::new());\n    }\n\n    #[test]\n    #[serial]\n    fn restore_collision() {\n        init_logging();\n        let file_name_prefix = get_unique_name();\n        let file_count: usize = 3;\n        let collision_remaining = file_count - 1;\n        let names: Vec<_> = (0..file_count).map(|i| format!(\"{}#{}\", file_name_prefix, i)).collect();\n        for path in names.iter() {\n            File::create_new(path).unwrap();\n        }\n        trash::delete_all(&names).unwrap();\n        for path in names.iter().skip(file_count - collision_remaining) {\n            File::create_new(path).unwrap();\n        }\n        let mut targets: Vec<_> = trash::os_limited::list()\n            .unwrap()\n            .into_iter()\n            .filter(|x| x.name.as_encoded_bytes().starts_with(file_name_prefix.as_bytes()))\n            .collect();\n        targets.sort_by(|a, b| a.name.cmp(&b.name));\n        assert_eq!(targets.len(), file_count);\n        let remaining_count = match trash::os_limited::restore_all(targets) {\n            Err(trash::Error::RestoreCollision { remaining_items, .. }) => {\n                let contains = |v: &Vec<trash::TrashItem>, name: &String| {\n                    for curr in v.iter() {\n                        if curr.name.as_encoded_bytes() == name.as_bytes() {\n                            return true;\n                        }\n                    }\n                    false\n                };\n                // Are all items that got restored reside in the folder?\n                for path in names.iter().filter(|filename| !contains(&remaining_items, filename)) {\n                    assert!(File::open(path).is_ok());\n                }\n                remaining_items.len()\n            }\n            _ => {\n                for path in names.iter() {\n                    std::fs::remove_file(path).ok();\n                }\n                panic!(\"restore_all was expected to return `trash::ErrorKind::RestoreCollision` but did not.\");\n            }\n        };\n        let remaining = trash::os_limited::list()\n            .unwrap()\n            .into_iter()\n            .filter(|x| x.name.as_encoded_bytes().starts_with(file_name_prefix.as_bytes()))\n            .collect::<Vec<_>>();\n        assert_eq!(remaining.len(), remaining_count);\n        trash::os_limited::purge_all(remaining).unwrap();\n        for path in names.iter() {\n            // This will obviously fail on the items that both didn't collide and weren't restored.\n            std::fs::remove_file(path).ok();\n        }\n    }\n\n    #[test]\n    #[serial]\n    fn restore_twins() {\n        init_logging();\n        let file_name_prefix = get_unique_name();\n        let file_count: usize = 4;\n        let names: Vec<_> = (0..file_count).map(|i| format!(\"{}#{}\", file_name_prefix, i)).collect();\n        for path in names.iter() {\n            File::create_new(path).unwrap();\n        }\n        trash::delete_all(&names).unwrap();\n\n        let twin_name = &names[1];\n        File::create_new(twin_name).unwrap();\n        trash::delete(twin_name).unwrap();\n\n        let mut targets: Vec<_> = trash::os_limited::list()\n            .unwrap()\n            .into_iter()\n            .filter(|x| x.name.as_encoded_bytes().starts_with(file_name_prefix.as_bytes()))\n            .collect();\n        targets.sort_by(|a, b| a.name.cmp(&b.name));\n        assert_eq!(targets.len(), file_count + 1); // plus one for one of the twins\n        match trash::os_limited::restore_all(targets) {\n            Err(trash::Error::RestoreTwins { path, items }) => {\n                assert_eq!(path.file_name().unwrap().to_str().unwrap(), twin_name);\n                trash::os_limited::purge_all(items).unwrap();\n            }\n            _ => panic!(\"restore_all was expected to return `trash::ErrorKind::RestoreTwins` but did not.\"),\n        }\n    }\n\n    #[test]\n    #[serial]\n    fn is_empty_matches_list() {\n        init_logging();\n\n        let is_empty_list = trash::os_limited::list().unwrap().is_empty();\n        let is_empty = trash::os_limited::is_empty().unwrap();\n        assert_eq!(is_empty, is_empty_list, \"is_empty() should match empty status from list()\");\n    }\n}\n"
  },
  {
    "path": "src/windows.rs",
    "content": "use crate::{Error, TrashContext, TrashItem, TrashItemMetadata, TrashItemSize};\nuse std::{\n    borrow::Borrow,\n    ffi::{c_void, OsStr, OsString},\n    os::windows::{ffi::OsStrExt, prelude::*},\n    path::PathBuf,\n};\nuse windows::Win32::{\n    Foundation::*, Storage::EnhancedStorage::*, System::Com::*, System::SystemServices::*,\n    UI::Shell::PropertiesSystem::*, UI::Shell::*,\n};\nuse windows::{\n    core::{Interface, PCWSTR, PWSTR},\n    Win32::System::Com::StructuredStorage::PropVariantToBSTR,\n};\n\nconst SCID_ORIGINAL_LOCATION: PROPERTYKEY = PROPERTYKEY { fmtid: PSGUID_DISPLACED, pid: PID_DISPLACED_FROM };\nconst SCID_DATE_DELETED: PROPERTYKEY = PROPERTYKEY { fmtid: PSGUID_DISPLACED, pid: PID_DISPLACED_DATE };\n\nimpl From<windows::core::Error> for Error {\n    fn from(err: windows::core::Error) -> Error {\n        Error::Os { code: err.code().0, description: format!(\"windows error: {err}\") }\n    }\n}\n\nfn to_wide_path(path: impl AsRef<OsStr>) -> Vec<u16> {\n    path.as_ref().encode_wide().chain(std::iter::once(0)).collect()\n}\n\n#[derive(Clone, Default, Debug)]\npub struct PlatformTrashContext;\nimpl PlatformTrashContext {\n    pub const fn new() -> Self {\n        PlatformTrashContext\n    }\n}\nimpl TrashContext {\n    /// See https://docs.microsoft.com/en-us/windows/win32/api/shellapi/ns-shellapi-_shfileopstructa\n    pub(crate) fn delete_specified_canonicalized(&self, full_paths: Vec<PathBuf>) -> Result<(), Error> {\n        ensure_com_initialized();\n        unsafe {\n            let pfo: IFileOperation = CoCreateInstance(&FileOperation as *const _, None, CLSCTX_ALL).unwrap();\n\n            pfo.SetOperationFlags(FOF_NO_UI | FOF_ALLOWUNDO | FOF_WANTNUKEWARNING)?;\n\n            for full_path in full_paths.iter() {\n                let path_prefix = ['\\\\' as u16, '\\\\' as u16, '?' as u16, '\\\\' as u16];\n                let wide_path_container = to_wide_path(full_path);\n                let wide_path_slice = if wide_path_container.starts_with(&path_prefix) {\n                    &wide_path_container[path_prefix.len()..]\n                } else {\n                    &wide_path_container[0..]\n                };\n\n                let shi: IShellItem = SHCreateItemFromParsingName(PCWSTR(wide_path_slice.as_ptr()), None)?;\n\n                pfo.DeleteItem(&shi, None)?;\n            }\n            pfo.PerformOperations()?;\n\n            // https://learn.microsoft.com/en-us/windows/win32/api/shobjidl_core/nf-shobjidl_core-ifileoperation-performoperations\n            // this method can still return a success code. Use the GetAnyOperationsAborted method to determine if this was the case.\n            if pfo.GetAnyOperationsAborted()?.as_bool() {\n                // TODO: return the reason why the operation was aborted.\n                // We may retrieve reason from the IFileOperationProgressSink but\n                // the list of HRESULT codes is not documented.\n                return Err(Error::Unknown { description: \"Some operations were aborted\".into() });\n            }\n            Ok(())\n        }\n    }\n\n    /// Removes all files and folder paths recursively.\n    pub(crate) fn delete_all_canonicalized(&self, full_paths: Vec<PathBuf>) -> Result<(), Error> {\n        self.delete_specified_canonicalized(full_paths)?;\n        Ok(())\n    }\n}\n\npub fn list() -> Result<Vec<TrashItem>, Error> {\n    ensure_com_initialized();\n    unsafe {\n        let mut item_vec = Vec::new();\n\n        let recycle_bin: IShellItem =\n            SHGetKnownFolderItem(&FOLDERID_RecycleBinFolder, KF_FLAG_DEFAULT, HANDLE::default())?;\n\n        let pesi: IEnumShellItems = recycle_bin.BindToHandler(None, &BHID_EnumItems)?;\n\n        loop {\n            let mut fetched_count: u32 = 0;\n            let mut arr = [None];\n            pesi.Next(&mut arr, Some(&mut fetched_count as *mut u32))?;\n\n            if fetched_count == 0 {\n                break;\n            }\n\n            match &arr[0] {\n                Some(item) => {\n                    let id = get_display_name(item, SIGDN_DESKTOPABSOLUTEPARSING)?;\n                    let name = get_display_name(item, SIGDN_PARENTRELATIVE)?;\n                    let item2: IShellItem2 = item.cast()?;\n                    let original_location_variant = item2.GetProperty(&SCID_ORIGINAL_LOCATION)?;\n                    let original_location_bstr = PropVariantToBSTR(&original_location_variant)?;\n                    let original_location = OsString::from_wide(original_location_bstr.as_wide());\n                    let date_deleted = get_date_deleted_unix(&item2)?;\n\n                    // NTFS paths are valid Unicode according to this chart:\n                    // https://en.wikipedia.org/wiki/Filename#Comparison_of_filename_limitations\n                    // Converting a String back to OsString doesn't do extra work\n                    item_vec.push(TrashItem {\n                        id,\n                        name: name.into_string().map_err(|original| Error::ConvertOsString { original })?.into(),\n                        original_parent: PathBuf::from(original_location),\n                        time_deleted: date_deleted,\n                    });\n                }\n                None => {\n                    break;\n                }\n            }\n        }\n\n        Ok(item_vec)\n    }\n}\n\npub fn is_empty() -> Result<bool, Error> {\n    ensure_com_initialized();\n    unsafe {\n        let recycle_bin: IShellItem =\n            SHGetKnownFolderItem(&FOLDERID_RecycleBinFolder, KF_FLAG_DEFAULT, HANDLE::default())?;\n        let pesi: IEnumShellItems = recycle_bin.BindToHandler(None, &BHID_EnumItems)?;\n\n        let mut count = 0u32;\n        let mut items = [None];\n        pesi.Next(&mut items, Some(&mut count as *mut u32))?;\n\n        Ok(count == 0)\n    }\n}\n\npub fn metadata(item: &TrashItem) -> Result<TrashItemMetadata, Error> {\n    ensure_com_initialized();\n    let id_as_wide = to_wide_path(&item.id);\n    let parsing_name = PCWSTR(id_as_wide.as_ptr());\n    let item: IShellItem = unsafe { SHCreateItemFromParsingName(parsing_name, None)? };\n    let is_dir = unsafe { item.GetAttributes(SFGAO_FOLDER)? } == SFGAO_FOLDER;\n    let size = if is_dir {\n        let pesi: IEnumShellItems = unsafe { item.BindToHandler(None, &BHID_EnumItems)? };\n        let mut size = 0;\n        loop {\n            let mut fetched_count: u32 = 0;\n            let mut arr = [None];\n            unsafe { pesi.Next(&mut arr, Some(&mut fetched_count as *mut u32))? };\n\n            if fetched_count == 0 {\n                break;\n            }\n\n            match &arr[0] {\n                Some(_item) => {\n                    size += 1;\n                }\n                None => {\n                    break;\n                }\n            }\n        }\n        TrashItemSize::Entries(size)\n    } else {\n        let item2: IShellItem2 = item.cast()?;\n        TrashItemSize::Bytes(unsafe { item2.GetUInt64(&PKEY_Size)? })\n    };\n    Ok(TrashItemMetadata { size })\n}\n\npub fn purge_all<I>(items: I) -> Result<(), Error>\nwhere\n    I: IntoIterator,\n    <I as IntoIterator>::Item: Borrow<TrashItem>,\n{\n    ensure_com_initialized();\n    unsafe {\n        let pfo: IFileOperation = CoCreateInstance(&FileOperation as *const _, None, CLSCTX_ALL)?;\n        pfo.SetOperationFlags(FOF_NO_UI)?;\n        let mut at_least_one = false;\n        for item in items {\n            at_least_one = true;\n            let id_as_wide = to_wide_path(&item.borrow().id);\n            let parsing_name = PCWSTR(id_as_wide.as_ptr());\n            let trash_item: IShellItem = SHCreateItemFromParsingName(parsing_name, None)?;\n            pfo.DeleteItem(&trash_item, None)?;\n        }\n        if at_least_one {\n            pfo.PerformOperations()?;\n        }\n        Ok(())\n    }\n}\n\npub fn restore_all<I>(items: I) -> Result<(), Error>\nwhere\n    I: IntoIterator<Item = TrashItem>,\n{\n    let items: Vec<_> = items.into_iter().collect();\n\n    // Do a quick and dirty check if the target items already exist at the location\n    // and if they do, return all of them, if they don't just go ahead with the processing\n    // without giving a damn.\n    // Note that this is not 'thread safe' meaning that if a paralell thread (or process)\n    // does this operation the exact same time or creates files or folders right after this check,\n    // then the files that would collide will not be detected and returned as part of an error.\n    // Instead Windows will display a prompt to the user whether they want to replace or skip.\n    for item in items.iter() {\n        let path = item.original_path();\n        if path.exists() {\n            return Err(Error::RestoreCollision { path, remaining_items: items });\n        }\n    }\n    ensure_com_initialized();\n    unsafe {\n        let pfo: IFileOperation = CoCreateInstance(&FileOperation as *const _, None, CLSCTX_ALL)?;\n        pfo.SetOperationFlags(FOF_NO_UI | FOFX_EARLYFAILURE)?;\n        for item in items.iter() {\n            let id_as_wide = to_wide_path(&item.id);\n            let parsing_name = PCWSTR(id_as_wide.as_ptr());\n            let trash_item: IShellItem = SHCreateItemFromParsingName(parsing_name, None)?;\n            let parent_path_wide = to_wide_path(&item.original_parent);\n            let orig_folder_shi: IShellItem = SHCreateItemFromParsingName(PCWSTR(parent_path_wide.as_ptr()), None)?;\n            let name_wstr = to_wide_path(&item.name);\n\n            pfo.MoveItem(&trash_item, &orig_folder_shi, PCWSTR(name_wstr.as_ptr()), None)?;\n        }\n        if !items.is_empty() {\n            pfo.PerformOperations()?;\n        }\n        Ok(())\n    }\n}\n\nunsafe fn get_display_name(psi: &IShellItem, sigdnname: SIGDN) -> Result<OsString, Error> {\n    let name = psi.GetDisplayName(sigdnname)?;\n    let result = wstr_to_os_string(name);\n    CoTaskMemFree(Some(name.0 as *const c_void));\n    Ok(result)\n}\n\nunsafe fn wstr_to_os_string(wstr: PWSTR) -> OsString {\n    let mut len = 0;\n    while *(wstr.0.offset(len)) != 0 {\n        len += 1;\n    }\n    let wstr_slice = std::slice::from_raw_parts(wstr.0, len as usize);\n    OsString::from_wide(wstr_slice)\n}\n\nunsafe fn get_date_deleted_unix(item: &IShellItem2) -> Result<i64, Error> {\n    /// January 1, 1970 as Windows file time\n    const EPOCH_AS_FILETIME: u64 = 116444736000000000;\n    const HUNDREDS_OF_NANOSECONDS: u64 = 10000000;\n\n    let time = item.GetFileTime(&SCID_DATE_DELETED)?;\n    let time_u64 = ((time.dwHighDateTime as u64) << 32) | (time.dwLowDateTime as u64);\n    let rel_to_linux_epoch = time_u64 - EPOCH_AS_FILETIME;\n    let seconds_since_unix_epoch = rel_to_linux_epoch / HUNDREDS_OF_NANOSECONDS;\n\n    Ok(seconds_since_unix_epoch as i64)\n}\n\nstruct CoInitializer {}\nimpl CoInitializer {\n    fn new() -> CoInitializer {\n        //let first = INITIALIZER_THREAD_COUNT.fetch_add(1, Ordering::SeqCst) == 0;\n        #[cfg(all(not(feature = \"coinit_multithreaded\"), not(feature = \"coinit_apartmentthreaded\")))]\n        {\n            0 = \"THIS IS AN ERROR ON PURPOSE. Either the `coinit_multithreaded` or the `coinit_apartmentthreaded` feature must be specified\";\n        }\n        let mut init_mode;\n        #[cfg(feature = \"coinit_multithreaded\")]\n        {\n            init_mode = COINIT_MULTITHREADED;\n        }\n        #[cfg(feature = \"coinit_apartmentthreaded\")]\n        {\n            init_mode = COINIT_APARTMENTTHREADED;\n        }\n\n        // These flags can be combined with either of coinit_multithreaded or coinit_apartmentthreaded.\n        if cfg!(feature = \"coinit_disable_ole1dde\") {\n            init_mode |= COINIT_DISABLE_OLE1DDE;\n        }\n        if cfg!(feature = \"coinit_speed_over_memory\") {\n            init_mode |= COINIT_SPEED_OVER_MEMORY;\n        }\n        let hr = unsafe { CoInitializeEx(None, init_mode) };\n        if hr.is_err() {\n            panic!(\"Call to CoInitializeEx failed. HRESULT: {:?}. Consider using `trash` with the feature `coinit_multithreaded`\", hr);\n        }\n        CoInitializer {}\n    }\n}\nimpl Drop for CoInitializer {\n    fn drop(&mut self) {\n        // TODO: This does not get called because it's a global static.\n        // Is there an atexit in Win32?\n        unsafe {\n            CoUninitialize();\n        }\n    }\n}\nthread_local! {\n    static CO_INITIALIZER: CoInitializer = CoInitializer::new();\n}\nfn ensure_com_initialized() {\n    CO_INITIALIZER.with(|_| {});\n}\n"
  },
  {
    "path": "tests/freedesktop_tests.rs",
    "content": "// Freedesktop trash tests that run entirely inside privileged Docker containers.\n//\n// Every test case spins up a fresh Ubuntu 24.04 container with CAP_SYS_ADMIN\n// (needed for `mount`) and copies the `trash` example binary in via the Docker\n// API. All filesystem mutations happen inside the container, so the host is\n// never touched.\n//\n// Prerequisites:\n// - Docker daemon running and accessible to the current user.\n// - Build the `trash` example binary first:\n//   `cargo build --example trash`\n// - Run this test target explicitly, since the tests are ignored by default:\n//   `cargo test --test freedesktop_tests -- --ignored`.\n\n#![cfg(target_os = \"linux\")]\n\nuse serial_test::serial;\nuse std::path::{Path, PathBuf};\nuse testcontainers::{core::ExecCommand, runners::AsyncRunner, ContainerAsync, GenericImage, ImageExt};\n\nconst IMAGE: &str = \"ubuntu\";\nconst TAG: &str = \"24.04\";\nconst HELPER_PATH: &str = \"/usr/local/bin/trash\";\n\n// ── helpers ──────────────────────────────────────────────────────────────────\n\n/// Locate the compiled `trash` example next to the running integration test.\n///\n/// Cargo does not expose example paths through `CARGO_BIN_EXE_*`, so derive\n/// `target/<triple?>/<profile>/examples/trash` from the current test binary.\nfn find_trash_binary() -> PathBuf {\n    let test_exe = std::env::current_exe().expect(\"failed to locate the running test binary\");\n    let profile_dir =\n        test_exe.parent().and_then(Path::parent).unwrap_or_else(|| panic!(\"unexpected test binary path: {test_exe:?}\"));\n    let helper = profile_dir.join(\"examples\").join(\"trash\");\n    assert!(\n        helper.exists(),\n        \"trash example not found at {helper:?} (build it by running 'cargo build --example trash')\"\n    );\n    helper\n}\n\nstruct TestContainer {\n    inner: ContainerAsync<GenericImage>,\n}\n\nimpl TestContainer {\n    /// Start a privileged container with the `trash` example binary copied in.\n    async fn start() -> Self {\n        let helper = find_trash_binary();\n        let inner = GenericImage::new(IMAGE, TAG)\n            // Keep the container alive for the duration of the test.\n            .with_cmd([\"sleep\", \"infinity\"])\n            // CAP_SYS_ADMIN is required for `mount` inside the container.\n            .with_privileged(true)\n            .with_copy_to(HELPER_PATH, helper)\n            .start()\n            .await\n            .expect(\"failed to start container\");\n\n        let container = Self { inner };\n        // Ensure the copied binary is executable inside the container.\n        container.exec_ok(&format!(\"chmod +x {HELPER_PATH}\")).await;\n        container\n    }\n\n    /// Execute a shell command inside the container and return its exit code.\n    ///\n    /// testcontainers 0.23 runs exec in detached mode, so `exit_code()` can\n    /// return `Ok(None)` until the process actually exits. We poll until the\n    /// exit code appears (up to 5 s).\n    async fn exec_cmd(&self, cmd: &str) -> i64 {\n        let result = self\n            .inner\n            .exec(ExecCommand::new([\"sh\", \"-c\", cmd]))\n            .await\n            .unwrap_or_else(|e| panic!(\"exec({cmd:?}) failed to launch: {e}\"));\n\n        for attempt in 0..50 {\n            match result.exit_code().await {\n                Ok(Some(code)) => return code,\n                Ok(None) => {\n                    if attempt == 49 {\n                        panic!(\"exec({cmd:?}) never exited after 5 s\");\n                    }\n                    tokio::time::sleep(std::time::Duration::from_millis(100)).await;\n                }\n                Err(e) => panic!(\"exit_code() for exec({cmd:?}) failed: {e}\"),\n            }\n        }\n        unreachable!()\n    }\n\n    /// Execute a setup command and panic if it fails.\n    async fn exec_ok(&self, cmd: &str) {\n        let code = self.exec_cmd(cmd).await;\n        assert_eq!(code, 0, \"setup command exited {code}: {cmd}\");\n    }\n\n    /// Run `trash::delete` in the container with optional env vars via\n    /// the copied `trash` example binary.\n    async fn delete(&self, env_vars: &[&str], path: &str) -> i64 {\n        let env_prefix = env_vars.join(\" \");\n        let cmd = if env_prefix.is_empty() {\n            format!(\"{HELPER_PATH} delete {path}\")\n        } else {\n            format!(\"{env_prefix} {HELPER_PATH} delete {path}\")\n        };\n        self.exec_cmd(&cmd).await\n    }\n\n    async fn path_is_file(&self, path: &str) -> bool {\n        self.exec_cmd(&format!(\"test -f {path}\")).await == 0\n    }\n\n    async fn path_exists(&self, path: &str) -> bool {\n        self.exec_cmd(&format!(\"test -e {path}\")).await == 0\n    }\n}\n\n// ── test cases ────────────────────────────────────────────────────────────────\n\n/// The home trash directory (`$HOME/.local/share/Trash`) is a regular directory.\n/// Deleting a file should succeed and place it under `Trash/files/`.\n#[tokio::test]\n#[ignore = \"requires a working Docker daemon and privileged containers\"]\n#[serial]\nasync fn trash_is_dir() {\n    let c = TestContainer::start().await;\n\n    c.exec_ok(\"mkdir -p /home/u/.local/share/Trash && touch /target-file\").await;\n\n    let code = c.delete(&[\"HOME=/home/u\"], \"/target-file\").await;\n    assert_eq!(code, 0, \"delete to a directory trash should succeed\");\n\n    let verify = c.path_is_file(\"/home/u/.local/share/Trash/files/target-file\").await;\n    assert!(verify, \"file should appear in Trash/files/\");\n}\n\n/// The home trash path is a regular *file* (not a directory).\n/// The trash operation should fail because it cannot create subdirectories inside it.\n#[tokio::test]\n#[ignore = \"requires a working Docker daemon and privileged containers\"]\n#[serial]\nasync fn trash_is_file() {\n    let c = TestContainer::start().await;\n\n    c.exec_ok(\"mkdir -p /home/u/.local/share && touch /home/u/.local/share/Trash && touch /target-file\").await;\n\n    let code = c.delete(&[\"HOME=/home/u\"], \"/target-file\").await;\n    assert_ne!(code, 0, \"delete when Trash is a file should fail\");\n\n    // The source file must still be present.\n    let still_there = c.path_is_file(\"/target-file\").await;\n    assert!(still_there, \"source file must not have been removed on failure\");\n}\n\n/// The home trash path is a symbolic link that points to a *directory*.\n/// This is valid – the library follows the symlink and uses the target directory.\n#[tokio::test]\n#[ignore = \"requires a working Docker daemon and privileged containers\"]\n#[serial]\nasync fn trash_is_symlink_to_dir() {\n    let c = TestContainer::start().await;\n\n    c.exec_ok(\n        \"mkdir /actual-trash && \\\n         mkdir -p /home/u/.local/share && \\\n         ln -s /actual-trash /home/u/.local/share/Trash && \\\n         touch /target-file\",\n    )\n    .await;\n\n    let code = c.delete(&[\"HOME=/home/u\"], \"/target-file\").await;\n    assert_eq!(code, 0, \"delete via a symlink-to-dir trash should succeed\");\n\n    // The file ends up in the *real* directory the symlink points to.\n    let verify = c.path_is_file(\"/actual-trash/files/target-file\").await;\n    assert!(verify, \"file should appear in the real trash directory\");\n}\n\n/// The home trash path is a symbolic link that points to a *regular file*.\n/// This is invalid; the trash operation should fail.\n#[tokio::test]\n#[ignore = \"requires a working Docker daemon and privileged containers\"]\n#[serial]\nasync fn trash_is_symlink_to_file() {\n    let c = TestContainer::start().await;\n\n    c.exec_ok(\n        \"touch /actual-file && \\\n         mkdir -p /home/u/.local/share && \\\n         ln -s /actual-file /home/u/.local/share/Trash && \\\n         touch /target-file\",\n    )\n    .await;\n\n    let code = c.delete(&[\"HOME=/home/u\"], \"/target-file\").await;\n    assert_ne!(code, 0, \"delete when Trash symlink points to a file should fail\");\n\n    let still_there = c.path_is_file(\"/target-file\").await;\n    assert!(still_there, \"source file must not have been removed on failure\");\n}\n\n/// The home trash path is a *broken* symbolic link (the target does not exist).\n/// The trash operation should fail.\n#[tokio::test]\n#[ignore = \"requires a working Docker daemon and privileged containers\"]\n#[serial]\nasync fn trash_is_symlink_to_nonexistent() {\n    let c = TestContainer::start().await;\n\n    c.exec_ok(\n        \"mkdir -p /home/u/.local/share && \\\n         ln -s /does-not-exist /home/u/.local/share/Trash && \\\n         touch /target-file\",\n    )\n    .await;\n\n    let code = c.delete(&[\"HOME=/home/u\"], \"/target-file\").await;\n    assert_ne!(code, 0, \"delete when Trash is a broken symlink should fail\");\n\n    let still_there = c.path_is_file(\"/target-file\").await;\n    assert!(still_there, \"source file must not have been removed on failure\");\n}\n\n/// The home trash directory is itself a *mount point* (a separate tmpfs).\n///\n/// Because the source file (`/target-file`) lives on the root filesystem and the\n/// home trash lives on its own mount, the library correctly identifies that they\n/// are on different filesystems.  It therefore creates `/.Trash-0/` (the per-UID\n/// trash on the root mount) instead of using the home trash.\n#[tokio::test]\n#[ignore = \"requires a working Docker daemon and privileged containers\"]\n#[serial]\nasync fn trash_is_mount() {\n    let c = TestContainer::start().await;\n\n    c.exec_ok(\n        \"mkdir -p /home/u/.local/share/Trash && \\\n         mount -t tmpfs tmpfs /home/u/.local/share/Trash && \\\n         touch /target-file\",\n    )\n    .await;\n\n    let code = c.delete(&[\"HOME=/home/u\"], \"/target-file\").await;\n    assert_eq!(code, 0, \"delete should succeed even when Trash is on its own mount\");\n\n    // The file is on the root FS; the library places it in the root FS's per-UID trash.\n    let verify = c.path_is_file(\"/.Trash-0/files/target-file\").await;\n    assert!(verify, \"file should be in /.Trash-0/ (the root FS trash)\");\n\n    // The home trash mount must remain empty.\n    let home_trash_empty = c.exec_cmd(\"test -z \\\"$(ls /home/u/.local/share/Trash/files/ 2>/dev/null)\\\"\").await;\n    assert_eq!(home_trash_empty, 0, \"home Trash mount should be empty\");\n}\n\n/// Complex mount/symlink scenario:\n///\n/// ```text\n///   /foo          — tmpfs mount A\n///   /foo/bar      — tmpfs mount B (separate filesystem inside A)\n///   /foo/bar/baz  — symlink → /foo/alice   (on mount B)\n///   /foo/alice/   — directory on mount A\n/// ```\n///\n/// The file to delete is `/foo/bar/baz/john/doe`.\n/// After symlink resolution its canonical path is `/foo/alice/john/doe`,\n/// which lives on mount A (`/foo`), **not** on mount B (`/foo/bar`).\n///\n/// The library must resolve symlinks before looking up the mount point, so the\n/// trash should end up in `/foo/.Trash-0/` and *not* in `/foo/bar/.Trash-0/`.\n#[tokio::test]\n#[ignore = \"requires a working Docker daemon and privileged containers\"]\n#[serial]\nasync fn trash_complex_mounts_with_symlink() {\n    let c = TestContainer::start().await;\n\n    c.exec_ok(\n        // Build the described layout step by step.\n        \"mkdir -p /foo && \\\n         mount -t tmpfs tmpfs /foo && \\\n         mkdir -p /foo/bar && \\\n         mount -t tmpfs tmpfs /foo/bar && \\\n         mkdir -p /foo/alice/john && \\\n         ln -s /foo/alice /foo/bar/baz && \\\n         touch /foo/bar/baz/john/doe\",\n        // ↑ creates /foo/alice/john/doe through the symlink\n    )\n    .await;\n\n    // Put the home directory on the root FS so it belongs to a different mount.\n    c.exec_ok(\"mkdir -p /home/u/.local/share/Trash\").await;\n\n    let code = c.delete(&[\"HOME=/home/u\"], \"/foo/bar/baz/john/doe\").await;\n    assert_eq!(code, 0, \"delete should succeed\");\n\n    // Canonical path is /foo/alice/john/doe → mount point is /foo.\n    // The trash must be /foo/.Trash-0/files/doe.\n    let in_foo_trash = c.path_is_file(\"/foo/.Trash-0/files/doe\").await;\n    assert!(in_foo_trash, \"file must be trashed under /foo/.Trash-0/ (mount A)\");\n\n    // Must NOT appear under mount B's trash.\n    let in_bar_trash = c.path_exists(\"/foo/bar/.Trash-0/files/doe\").await;\n    assert!(!in_bar_trash, \"file must NOT be in /foo/bar/.Trash-0/ (wrong mount)\");\n\n    // Must NOT appear in the home trash.\n    let in_home_trash = c.path_exists(\"/home/u/.local/share/Trash/files/doe\").await;\n    assert!(!in_home_trash, \"file must NOT be in home Trash (different mount)\");\n}\n\n/// Variant of `trash_complex_mounts_with_symlink` where the user's home trash\n/// is itself reachable only through the symlink (`XDG_DATA_HOME=/foo/bar/baz/john`).\n///\n/// Layout (same mounts and symlink as the previous test):\n///\n/// ```text\n///   /foo              — tmpfs mount A\n///   /foo/bar          — tmpfs mount B\n///   /foo/bar/baz      — symlink → /foo/alice\n///   /foo/alice/john/  — directory on mount A\n/// ```\n///\n/// `XDG_DATA_HOME=/foo/bar/baz/john` → home trash = `/foo/bar/baz/john/Trash`\n///\n/// After symlink resolution that is `/foo/alice/john/Trash`, which lives on\n/// mount A — the **same** mount as the deleted file (`/foo/alice/john/doe`).\n///\n/// Therefore the library should use the home trash directly instead of\n/// creating a per-mount `.Trash-0` directory.\n#[tokio::test]\n#[ignore = \"requires a working Docker daemon and privileged containers\"]\n#[serial]\nasync fn trash_complex_mounts_home_trash_via_symlink() {\n    let c = TestContainer::start().await;\n\n    c.exec_ok(\n        \"mkdir -p /foo && \\\n         mount -t tmpfs tmpfs /foo && \\\n         mkdir -p /foo/bar && \\\n         mount -t tmpfs tmpfs /foo/bar && \\\n         mkdir -p /foo/alice/john && \\\n         ln -s /foo/alice /foo/bar/baz && \\\n         touch /foo/bar/baz/john/doe\",\n    )\n    .await;\n\n    let code = c.delete(&[\"XDG_DATA_HOME=/foo/bar/baz/john\"], \"/foo/bar/baz/john/doe\").await;\n    assert_eq!(code, 0, \"delete should succeed\");\n\n    // The home trash canonicalizes to /foo/alice/john/Trash (mount A), which\n    // is the same mount as the file.  The file must land there.\n    let in_home_trash = c.path_is_file(\"/foo/bar/baz/john/Trash/files/doe\").await;\n    assert!(in_home_trash, \"file must be in the home trash (/foo/alice/john/Trash)\");\n\n    // Must NOT fall back to the per-mount trash on /foo.\n    let in_foo_trash = c.path_exists(\"/foo/.Trash-0/files/doe\").await;\n    assert!(!in_foo_trash, \"file must NOT be in /foo/.Trash-0/\");\n\n    // Must NOT land in /foo/bar's trash (wrong mount).\n    let in_bar_trash = c.path_exists(\"/foo/bar/.Trash-0/files/doe\").await;\n    assert!(!in_bar_trash, \"file must NOT be in /foo/bar/.Trash-0/\");\n}\n\n#[derive(Clone, Copy, Debug, Eq, PartialEq)]\nenum ComplexMount {\n    A,\n    B,\n}\n\nimpl ComplexMount {\n    const fn label(self) -> &'static str {\n        match self {\n            Self::A => \"mount_a\",\n            Self::B => \"mount_b\",\n        }\n    }\n\n    const fn direct_home(self) -> &'static str {\n        match self {\n            Self::A => \"/foo/alice/john\",\n            Self::B => \"/foo/bar/beth/john\",\n        }\n    }\n\n    const fn symlink_home(self) -> &'static str {\n        match self {\n            Self::A => \"/foo/bar/baz/john\",\n            Self::B => \"/foo/bridge/john\",\n        }\n    }\n\n    const fn trash_dir(self) -> &'static str {\n        match self {\n            Self::A => \"/foo/.Trash-0\",\n            Self::B => \"/foo/bar/.Trash-0\",\n        }\n    }\n\n    const fn other(self) -> Self {\n        match self {\n            Self::A => Self::B,\n            Self::B => Self::A,\n        }\n    }\n\n    fn home(self, access: AccessPath) -> &'static str {\n        match access {\n            AccessPath::Direct => self.direct_home(),\n            AccessPath::ViaSymlink => self.symlink_home(),\n        }\n    }\n\n    fn file_path(self, access: AccessPath, file_name: &str) -> String {\n        format!(\"{}/{file_name}\", self.home(access))\n    }\n}\n\n#[derive(Clone, Copy, Debug)]\nenum AccessPath {\n    Direct,\n    ViaSymlink,\n}\n\nimpl AccessPath {\n    const fn label(self) -> &'static str {\n        match self {\n            Self::Direct => \"direct\",\n            Self::ViaSymlink => \"via_symlink\",\n        }\n    }\n}\n\nasync fn setup_complex_mount_permutation_layout(container: &TestContainer) {\n    container\n        .exec_ok(\n            \"mkdir -p /foo && \\\n         mount -t tmpfs tmpfs /foo && \\\n         mkdir -p /foo/bar && \\\n         mount -t tmpfs tmpfs /foo/bar && \\\n         mkdir -p /foo/alice/john && \\\n         mkdir -p /foo/bar/beth/john && \\\n         ln -s /foo/alice /foo/bar/baz && \\\n         ln -s /foo/bar/beth /foo/bridge\",\n        )\n        .await;\n}\n\nasync fn assert_complex_mount_permutation(\n    container: &TestContainer,\n    file_mount: ComplexMount,\n    file_access: AccessPath,\n    home_mount: ComplexMount,\n    home_access: AccessPath,\n) {\n    let case_name = format!(\n        \"file_{}_{}_home_{}_{}\",\n        file_mount.label(),\n        file_access.label(),\n        home_mount.label(),\n        home_access.label(),\n    );\n    let file_name = format!(\"doe-{case_name}\");\n    let file_path = file_mount.file_path(file_access, &file_name);\n    let home_data_dir = home_mount.home(home_access);\n\n    container.exec_ok(&format!(\"touch {file_path}\")).await;\n\n    let env = format!(\"XDG_DATA_HOME={home_data_dir}\");\n    let code = container.delete(&[env.as_str()], &file_path).await;\n    assert_eq!(code, 0, \"{case_name}: delete should succeed\");\n\n    let in_home_trash = container.path_is_file(&format!(\"{home_data_dir}/Trash/files/{file_name}\")).await;\n    let in_file_mount_trash = container.path_exists(&format!(\"{}/files/{file_name}\", file_mount.trash_dir())).await;\n    let in_other_mount_trash =\n        container.path_exists(&format!(\"{}/files/{file_name}\", file_mount.other().trash_dir())).await;\n\n    if file_mount == home_mount {\n        assert!(in_home_trash, \"{case_name}: file must be in the home trash\");\n        assert!(!in_file_mount_trash, \"{case_name}: file must not fall back to the file mount trash\");\n    } else {\n        assert!(!in_home_trash, \"{case_name}: file must not land in the home trash\");\n        assert!(in_file_mount_trash, \"{case_name}: file must land in the file mount trash\");\n    }\n\n    assert!(!in_other_mount_trash, \"{case_name}: file must not land in the unrelated mount trash\");\n}\n\n#[tokio::test]\n#[ignore = \"requires a working Docker daemon and privileged containers\"]\n#[serial]\nasync fn trash_complex_mounts_home_trash_permutations() {\n    let c = TestContainer::start().await;\n    setup_complex_mount_permutation_layout(&c).await;\n\n    for mount in [ComplexMount::A, ComplexMount::B] {\n        for file_access in [AccessPath::Direct, AccessPath::ViaSymlink] {\n            for home_access in [AccessPath::Direct, AccessPath::ViaSymlink] {\n                assert_complex_mount_permutation(&c, mount, file_access, mount, home_access).await;\n            }\n        }\n    }\n}\n\n#[tokio::test]\n#[ignore = \"requires a working Docker daemon and privileged containers\"]\n#[serial]\nasync fn trash_complex_mounts_per_mount_trash_permutations() {\n    let c = TestContainer::start().await;\n    setup_complex_mount_permutation_layout(&c).await;\n\n    for file_mount in [ComplexMount::A, ComplexMount::B] {\n        let home_mount = file_mount.other();\n        for file_access in [AccessPath::Direct, AccessPath::ViaSymlink] {\n            for home_access in [AccessPath::Direct, AccessPath::ViaSymlink] {\n                assert_complex_mount_permutation(&c, file_mount, file_access, home_mount, home_access).await;\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "tests/isolated.rs",
    "content": "use trash::delete;\n\n#[test]\nfn delete_with_empty_path() {\n    let tmp = tempfile::TempDir::new().unwrap();\n    std::env::set_current_dir(tmp.path()).unwrap();\n    assert_eq!(\n        delete(\"\").unwrap_err().to_string(),\n        \"Error during a `trash` operation: CanonicalizePath { original: \\\"\\\" }\"\n    );\n}\n"
  },
  {
    "path": "tests/trash.rs",
    "content": "use std::fs::{create_dir, File};\nuse std::path::{Path, PathBuf};\n\nuse log::trace;\n\nuse serial_test::serial;\nuse trash::{delete, delete_all};\n\nmod util {\n    use std::sync::atomic::{AtomicI32, Ordering};\n\n    use once_cell::sync::Lazy;\n\n    // WARNING Expecting that `cargo test` won't be invoked on the same computer more than once within\n    // a single millisecond\n    static INSTANCE_ID: Lazy<i64> = Lazy::new(|| chrono::Local::now().timestamp_millis());\n    static ID_OFFSET: AtomicI32 = AtomicI32::new(0);\n    pub fn get_unique_name() -> String {\n        let id = ID_OFFSET.fetch_add(1, Ordering::SeqCst);\n        format!(\"trash-test-{}-{}\", *INSTANCE_ID, id)\n    }\n\n    pub fn init_logging() {\n        let _ = env_logger::builder().is_test(true).try_init();\n    }\n}\npub use util::{get_unique_name, init_logging};\n\n#[test]\n#[serial]\nfn test_delete_file() {\n    init_logging();\n    trace!(\"Started test_delete_file\");\n\n    let path = get_unique_name();\n    File::create_new(&path).unwrap();\n\n    delete(&path).unwrap();\n    assert!(File::open(&path).is_err());\n    trace!(\"Finished test_delete_file\");\n}\n\n#[test]\n#[serial]\nfn test_delete_folder() {\n    init_logging();\n    trace!(\"Started test_delete_folder\");\n\n    let path = PathBuf::from(get_unique_name());\n    create_dir(&path).unwrap();\n    File::create_new(path.join(\"file_in_folder\")).unwrap();\n\n    assert!(path.exists());\n    delete(&path).unwrap();\n    assert!(!path.exists());\n\n    trace!(\"Finished test_delete_folder\");\n}\n\n#[test]\nfn test_delete_all() {\n    init_logging();\n    trace!(\"Started test_delete_all\");\n    let count: usize = 3;\n\n    let paths: Vec<_> = (0..count).map(|i| format!(\"test_file_to_delete_{i}\")).collect();\n    for path in paths.iter() {\n        File::create_new(path).unwrap();\n    }\n\n    delete_all(&paths).unwrap();\n    for path in paths.iter() {\n        assert!(File::open(path).is_err());\n    }\n    trace!(\"Finished test_delete_all\");\n}\n\n#[cfg(unix)]\nmod unix {\n    use log::trace;\n    use std::{\n        fs::{create_dir, remove_dir_all, remove_file, File},\n        os::unix::fs::symlink,\n        path::Path,\n    };\n\n    use super::{get_unique_name, init_logging};\n    use crate::delete;\n    // use crate::init_logging;\n\n    #[test]\n    #[ignore = \"permission denied in more recent macOS versions\"]\n    fn test_delete_symlink() {\n        init_logging();\n        trace!(\"Started test_delete_symlink\");\n        let target_path = get_unique_name();\n        File::create_new(&target_path).unwrap();\n\n        let link_path = \"test_link_to_delete\";\n        symlink(&target_path, link_path).unwrap();\n\n        delete(link_path).unwrap();\n        assert!(File::open(link_path).is_err());\n        assert!(File::open(&target_path).is_ok());\n        // Cleanup\n        remove_file(&target_path).unwrap();\n        trace!(\"Finished test_delete_symlink\");\n    }\n\n    #[test]\n    #[ignore = \"permission denied in more recent macOS versions\"]\n    fn test_delete_symlink_in_folder() {\n        init_logging();\n        trace!(\"Started test_delete_symlink_in_folder\");\n        let target_path = \"test_link_target_for_delete_from_folder\";\n        File::create_new(target_path).unwrap();\n\n        let folder = Path::new(\"test_parent_folder_for_link_to_delete\");\n        create_dir(folder).unwrap();\n        let link_path = folder.join(\"test_link_to_delete_from_folder\");\n        symlink(target_path, &link_path).unwrap();\n\n        delete(&link_path).unwrap();\n        assert!(File::open(link_path).is_err());\n        assert!(File::open(target_path).is_ok());\n        // Cleanup\n        remove_file(target_path).unwrap();\n        remove_dir_all(folder).unwrap();\n        trace!(\"Finished test_delete_symlink_in_folder\");\n    }\n}\n\n#[test]\n#[serial]\nfn create_remove_single_file() {\n    // Let's create and remove a single file\n    let name = get_unique_name();\n    File::create_new(&name).unwrap();\n    trash::delete(&name).unwrap();\n    assert!(File::open(&name).is_err());\n}\n\n#[cfg(all(unix, not(target_os = \"macos\"), not(target_os = \"ios\"), not(target_os = \"android\")))]\n#[test]\n#[serial]\nfn create_remove_single_file_invalid_utf8() {\n    use std::ffi::OsStr;\n    let name = unsafe { OsStr::from_encoded_bytes_unchecked(&[168]) };\n    File::create_new(name).unwrap();\n    trash::delete(name).unwrap();\n}\n\n#[test]\nfn recursive_file_deletion() {\n    let parent_dir = Path::new(\"remove-me\");\n    let dir1 = parent_dir.join(\"dir1\");\n    let dir2 = parent_dir.join(\"dir2\");\n    std::fs::create_dir_all(&dir1).unwrap();\n    std::fs::create_dir_all(&dir2).unwrap();\n    File::create_new(dir1.join(\"same-name\")).unwrap();\n    File::create_new(dir2.join(\"same-name\")).unwrap();\n\n    trash::delete(parent_dir).unwrap();\n    assert!(!parent_dir.exists());\n}\n\n#[test]\nfn recursive_file_with_content_deletion() {\n    let parent_dir = Path::new(\"remove-me-content\");\n    let dir1 = parent_dir.join(\"dir1\");\n    let dir2 = parent_dir.join(\"dir2\");\n    std::fs::create_dir_all(&dir1).unwrap();\n    std::fs::create_dir_all(&dir2).unwrap();\n    File::create_new(dir1.join(\"same-name\")).unwrap();\n    std::fs::write(dir2.join(\"same-name\"), b\"some content\").unwrap();\n\n    trash::delete(parent_dir).unwrap();\n    assert!(!parent_dir.exists());\n}\n"
  }
]