├── .cirrus.yml ├── .github ├── codecov.yml ├── dependabot.yml └── workflows │ ├── ci.yml │ ├── dependency-review.yml │ ├── docs.yml │ ├── publish.yml │ └── scorecards.yml ├── .gitignore ├── CHANGELOG.md ├── Cargo.toml ├── LICENSE.txt ├── README.md ├── SECURITY.md ├── bindgen ├── bindings_freebsd.rs ├── bindings_freebsd14.diff ├── bindings_linux.rs ├── bindings_macos.rs └── wrapper.h ├── build.rs ├── ci ├── bindgen.sh ├── coverage.sh ├── docs.sh ├── format.sh └── lint.sh ├── examples └── exacl.rs ├── src ├── acl.rs ├── aclentry.rs ├── bindings.rs ├── bititer.rs ├── failx.rs ├── flag.rs ├── format │ ├── format_no_serde.rs │ ├── format_serde.rs │ └── mod.rs ├── lib.rs ├── perm.rs ├── qualifier.rs ├── sys.rs ├── unix.rs └── util │ ├── mod.rs │ ├── util_common.rs │ ├── util_freebsd.rs │ ├── util_linux.rs │ └── util_macos.rs └── tests ├── run_tests.sh ├── test_api.rs ├── test_examples.rs ├── testsuite_darwin.sh ├── testsuite_freebsd_acls.sh ├── testsuite_freebsd_nfsv4acls.sh ├── testsuite_linux.sh ├── testsuite_malformed_all.sh └── valgrind.supp /.cirrus.yml: -------------------------------------------------------------------------------- 1 | # Config file for cirrus-ci.org 2 | # Adapted from https://github.com/Stebalien/xattr 3 | 4 | task: 5 | name: CI / build (freebsd) 6 | 7 | matrix: 8 | - name: CI / build (freebsd-13.3) 9 | freebsd_instance: 10 | image_family: freebsd-13-3 11 | - name: CI / build (freebsd-14.0) 12 | freebsd_instance: 13 | image_family: freebsd-14-0 14 | 15 | sysinfo_script: 16 | # Record info about the test environment. 17 | - mount 18 | - df -h 19 | - sysctl hw.model hw.ncpu hw.physmem 20 | - freebsd-version 21 | # Create a 5 MB memory based FS with acls enabled and 22 | # mount it to a sub-directory of /tmp (where it won't 23 | # interfere with other uses of /tmp.) 24 | - mkdir /tmp/exacl_acls /tmp/exacl_nfsv4acls 25 | - mdmfs -o acls -s 5m md /tmp/exacl_acls 26 | - mdmfs -o nfsv4acls -s 5m md /tmp/exacl_nfsv4acls 27 | - mount 28 | - env 29 | - pkg info 30 | 31 | setup_script: 32 | # Install Rust. 33 | - pkg install -y bash llvm11 34 | - curl https://sh.rustup.rs -sSf --output rustup.sh 35 | - sh rustup.sh -y 36 | # Install shunit2. 37 | - mkdir -p /tmp/bin 38 | - curl https://raw.githubusercontent.com/kward/shunit2/master/shunit2 -sSf --output /tmp/bin/shunit2 39 | - chmod ugo+x /tmp/bin/shunit2 40 | 41 | test_script: 42 | - . $HOME/.cargo/env 43 | # Set up path for shunit2. 44 | - export PATH="$PATH:/tmp/bin" 45 | - cargo fetch 46 | - cargo build # Build no-serde 47 | - cargo build --features serde # Build with serde 48 | - cargo test --no-run --features serde # Compile only 49 | # Run tests on our mem-based FS with acls. 50 | - export TMPDIR=/tmp/exacl_acls 51 | - cargo test --features serde 52 | - export TMPDIR=/tmp/exacl_nfsv4acls 53 | - cargo test --features serde 54 | - ./tests/run_tests.sh 55 | - export TMPDIR=/tmp 56 | - ./ci/bindgen.sh 57 | 58 | lint_script: 59 | - . $HOME/.cargo/env 60 | - ./ci/lint.sh 61 | -------------------------------------------------------------------------------- /.github/codecov.yml: -------------------------------------------------------------------------------- 1 | # Reference: 2 | # https://docs.codecov.io/docs/codecovyml-reference 3 | 4 | comment: false 5 | 6 | coverage: 7 | status: 8 | project: 9 | default: 10 | informational: true 11 | 12 | patch: off 13 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # Reference: 2 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 3 | 4 | version: 2 5 | 6 | updates: 7 | - package-ecosystem: "cargo" 8 | directory: "/" 9 | schedule: 10 | interval: "weekly" 11 | 12 | - package-ecosystem: "github-actions" 13 | directory: "/" 14 | schedule: 15 | interval: "weekly" 16 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ "*" ] 6 | schedule: 7 | # Every Saturday at 4:30 AM UTC. 8 | - cron: '30 4 * * 6' 9 | 10 | env: 11 | CARGO_TERM_COLOR: always 12 | 13 | permissions: 14 | contents: read 15 | 16 | jobs: 17 | build: 18 | 19 | runs-on: ${{ matrix.os }} 20 | strategy: 21 | fail-fast: false 22 | matrix: 23 | os: [ubuntu-24.04, ubuntu-22.04, ubuntu-20.04, macos-14, macos-13, macos-12] 24 | 25 | steps: 26 | - name: Harden Runner 27 | uses: step-security/harden-runner@63c24ba6bd7ba022e95695ff85de572c04a18142 # v2.7.0 28 | with: 29 | egress-policy: audit 30 | 31 | - name: Checkout 32 | uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 33 | - name: Update Rust Toolchain 34 | run: rustup update 35 | - name: Install dependencies (macOS) 36 | run: brew install shunit2 shellcheck shfmt 37 | if: runner.os == 'macOS' 38 | - name: Install dependencies (Linux) 39 | run: | 40 | sudo apt-get update 41 | sudo apt-get -y install libacl1-dev acl shunit2 shellcheck 42 | if: runner.os == 'Linux' 43 | - name: Fetch 44 | run: cargo fetch 45 | - name: Build (no-serde) 46 | run: cargo build 47 | - name: Build (serde) 48 | run: cargo build --features serde 49 | - name: Unit Test (no-serde) 50 | run: cargo test 51 | - name: Unit Test (serde) 52 | run: RUST_LOG=debug cargo test --features serde 53 | - name: Run integration tests 54 | run: ./tests/run_tests.sh 55 | - name: Run memory tests (Linux) 56 | run: | 57 | sudo NEEDRESTART_MODE=l apt-get install -y valgrind 58 | ./tests/run_tests.sh memcheck 59 | if: runner.os == 'Linux' 60 | - name: Run TMPFS tests (Linux) 61 | run: | 62 | mkdir /run/user/$UID/exacl 63 | export TMPDIR=/run/user/$UID/exacl 64 | RUST_LOG=debug cargo test --features serde 65 | ./tests/run_tests.sh 66 | if: runner.os == 'Linux' 67 | - name: Code coverage 68 | env: 69 | CODECOV_TOKEN: ${{ secrets.EXACL_CODECOV_TOKEN }} 70 | run: ./ci/coverage.sh codecov 71 | - name: Lint Check 72 | run: ./ci/lint.sh 73 | - name: Format Check 74 | run: ./ci/format.sh 75 | - name: Docs Check 76 | run: ./ci/docs.sh 77 | - name: Bindgen Check 78 | run: ./ci/bindgen.sh 79 | -------------------------------------------------------------------------------- /.github/workflows/dependency-review.yml: -------------------------------------------------------------------------------- 1 | # Dependency Review Action 2 | # 3 | # This Action will scan dependency manifest files that change as part of a Pull Request, 4 | # surfacing known-vulnerable versions of the packages declared or updated in the PR. 5 | # Once installed, if the workflow run is marked as required, 6 | # PRs introducing known-vulnerable packages will be blocked from merging. 7 | # 8 | # Source repository: https://github.com/actions/dependency-review-action 9 | name: 'Dependency Review' 10 | on: [pull_request] 11 | 12 | permissions: 13 | contents: read 14 | 15 | jobs: 16 | dependency-review: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - name: Harden Runner 20 | uses: step-security/harden-runner@63c24ba6bd7ba022e95695ff85de572c04a18142 # v2.7.0 21 | with: 22 | egress-policy: audit 23 | 24 | - name: 'Checkout Repository' 25 | uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 26 | - name: 'Dependency Review' 27 | uses: actions/dependency-review-action@0c155c5e8556a497adf53f2c18edabf945ed8e70 # v4.3.2 28 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: Docs 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | 7 | env: 8 | CARGO_TERM_COLOR: always 9 | 10 | permissions: 11 | contents: read 12 | 13 | jobs: 14 | publish-docs: 15 | 16 | permissions: 17 | contents: write # for peaceiris/actions-gh-pages to push pages branch 18 | runs-on: ubuntu-20.04 19 | 20 | steps: 21 | - name: Harden Runner 22 | uses: step-security/harden-runner@63c24ba6bd7ba022e95695ff85de572c04a18142 # v2.7.0 23 | with: 24 | egress-policy: audit 25 | 26 | - name: Checkout 27 | uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 28 | - name: Install dependencies (Linux) 29 | run: sudo apt-get -y install libacl1-dev 30 | - name: Build Docs 31 | run: ./ci/docs.sh 32 | - name: Publish Docs to Github Pages 33 | uses: peaceiris/actions-gh-pages@4f9cc6602d3f66b9c108549d475ec49e8ef4d45e # v4.0.0 34 | with: 35 | github_token: ${{ secrets.GITHUB_TOKEN }} 36 | publish_dir: ./target/doc 37 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | env: 8 | CARGO_TERM_COLOR: always 9 | 10 | permissions: 11 | contents: read 12 | 13 | jobs: 14 | 15 | publish-crate: 16 | runs-on: ubuntu-20.04 17 | 18 | steps: 19 | - name: Harden Runner 20 | uses: step-security/harden-runner@63c24ba6bd7ba022e95695ff85de572c04a18142 # v2.7.0 21 | with: 22 | egress-policy: audit 23 | 24 | - name: Checkout 25 | uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 26 | - name: Install dependencies (Linux) 27 | run: sudo apt-get -y install libacl1-dev 28 | - name: Publish Crate 29 | env: 30 | CARGO_REGISTRY_TOKEN: ${{ secrets.CRATES_TOKEN }} 31 | run: cargo publish 32 | -------------------------------------------------------------------------------- /.github/workflows/scorecards.yml: -------------------------------------------------------------------------------- 1 | # This workflow uses actions that are not certified by GitHub. They are provided 2 | # by a third-party and are governed by separate terms of service, privacy 3 | # policy, and support documentation. 4 | 5 | name: Scorecard supply-chain security 6 | on: 7 | # For Branch-Protection check. Only the default branch is supported. See 8 | # https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection 9 | branch_protection_rule: 10 | # To guarantee Maintained check is occasionally updated. See 11 | # https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained 12 | schedule: 13 | - cron: '20 7 * * 2' 14 | push: 15 | branches: ["main"] 16 | 17 | # Declare default permissions as read only. 18 | permissions: read-all 19 | 20 | jobs: 21 | analysis: 22 | name: Scorecard analysis 23 | runs-on: ubuntu-latest 24 | permissions: 25 | # Needed to upload the results to code-scanning dashboard. 26 | security-events: write 27 | # Needed to publish results and get a badge (see publish_results below). 28 | id-token: write 29 | contents: read 30 | actions: read 31 | 32 | steps: 33 | - name: Harden Runner 34 | uses: step-security/harden-runner@63c24ba6bd7ba022e95695ff85de572c04a18142 # v2.7.0 35 | with: 36 | egress-policy: audit 37 | 38 | - name: "Checkout code" 39 | uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 40 | with: 41 | persist-credentials: false 42 | 43 | - name: "Run analysis" 44 | uses: ossf/scorecard-action@0864cf19026789058feabb7e87baa5f140aac736 # v2.3.1 45 | with: 46 | results_file: results.sarif 47 | results_format: sarif 48 | # (Optional) "write" PAT token. Uncomment the `repo_token` line below if: 49 | # - you want to enable the Branch-Protection check on a *public* repository, or 50 | # - you are installing Scorecards on a *private* repository 51 | # To create the PAT, follow the steps in https://github.com/ossf/scorecard-action#authentication-with-pat. 52 | # repo_token: ${{ secrets.SCORECARD_TOKEN }} 53 | 54 | # Public repositories: 55 | # - Publish results to OpenSSF REST API for easy access by consumers 56 | # - Allows the repository to include the Scorecard badge. 57 | # - See https://github.com/ossf/scorecard-action#publishing-results. 58 | # For private repositories: 59 | # - `publish_results` will always be set to `false`, regardless 60 | # of the value entered here. 61 | publish_results: true 62 | 63 | # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF 64 | # format to the repository Actions tab. 65 | - name: "Upload artifact" 66 | uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3 67 | with: 68 | name: SARIF file 69 | path: results.sarif 70 | retention-days: 5 71 | 72 | # Upload the results to GitHub's code scanning dashboard. 73 | - name: "Upload to code-scanning" 74 | uses: github/codeql-action/upload-sarif@f079b8493333aace61c81488f8bd40919487bd9f # v3.25.7 75 | with: 76 | sarif_file: results.sarif 77 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | .DS_Store 4 | /.vscode -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [HEAD] - tbd 4 | 5 | - Update CI builds to use FreeBSD 13.3, ubuntu-24.04, and macos-14. 6 | - Update versions of Github Actions used in CI. 7 | - Update valgrind suppressions for newer versions of Rust. 8 | - Fix clippy warnings. 9 | 10 | ## [0.12.0] - 2024-02-02 11 | 12 | - Fix typo in rustdoc comments for getfacl/setfacl. (Thanks @sylvestre) 13 | - Support FreeBSD 14 in CI builds. (Thanks @asomers) 14 | - Run CI tests on macos-13 and macos-12. 15 | - Update `bitflags` and `uuid` dependency versions. 16 | - Update build version dependency for `bindgen`. 17 | - Fix clippy warnings. 18 | 19 | ## [0.11.0] - 2023-09-25 20 | 21 | - Upgrade `bitflags` to 2.4.0 from 1.x. `bitflags` is used to implement the Perm, Flag and AclOption API's. 22 | - Tests should support systems where daemon uid/gid is other than 1/1. 23 | - Tests should accommodate varying limits on ext, tmpfs, xfs file systems. 24 | - Update version dependencies for `bindgen`, `ctor`. 25 | - Fix clippy warnings. 26 | 27 | ## [0.10.0] - 2023-01-02 28 | 29 | - Update version dependencies for `bindgen`, `clap`, and `env_logger`. 30 | - Include ubuntu-22.04, macos-12, and freebsd-13.1 in CI build. 31 | - Fix code coverage CI script to address GHA build issue. 32 | - Fix clippy warnings. 33 | 34 | ## [0.9.0] - 2022-06-08 35 | 36 | - Fix compilation on various Linux architectures where `c_char` is signed (Issue #107). 37 | - Disable `layout_tests` option in `bindgen`. 38 | - Update version dependencies for `bindgen` and `uuid`. 39 | - Improve code coverage CI script. 40 | - Fix clippy warnings. 41 | 42 | ## [0.8.0] - 2022-02-03 43 | 44 | - `serde` is now an optional dependency. Use `features = ["serde"]` to enable (Issue #95). 45 | - Remove the `num_enum` dependency (PR #94, contributed by bjorn3). 46 | - Update example code to use clap 3. 47 | 48 | ## [0.7.0] - 2021-12-25 49 | 50 | - Add the `from_mode` top level function. 51 | - Remove `Acl` (low level interface) from the public exported API. 52 | - Remove dependency on the `nix` crate. 53 | - Update version dependencies for bindgen and env_logger. 54 | - Update Rust edition from 2018 to 2021. 55 | 56 | ## [0.6.0] - 2021-06-20 57 | 58 | - Fix new rust clippy warnings. 59 | - Update version dependencies for bindgen and nix. 60 | - Update valgrind suppressions used in testing. 61 | 62 | ## [0.5.0] - 2021-02-22 63 | 64 | - Add support for NFSv4 ACL's on `FreeBSD`. 65 | - Remove support for platform-specific text formats. 66 | 67 | ## [0.4.0] - 2021-01-13 68 | 69 | - Add support for symbolic links on `FreeBSD`. 70 | - Add support for `ACCESS_ACL` option to `getfacl` and `setfacl`. 71 | - Allow for `-` in permission abbreviation, e.g. `r-x`. 72 | - Update rust toolchain to latest stable version and fix clippy/lint issues. 73 | - Fix package metadata for docs.rs; improve platform-specific documentation. 74 | 75 | ## [0.3.0] - 2021-01-02 76 | 77 | - Add support for Posix.1e ACLs on `FreeBSD`. 78 | - Add `from_str` and `to_string` top-level functions. 79 | - Remove the `Acl::check` function from public API. 80 | 81 | ## [0.2.0] - 2020-12-22 82 | 83 | - Implement buildtime_bindgen feature; use prebuilt bindings by default. 84 | - Implement `FromStr` and `Display` for `AclEntry`. 85 | - Add `to_writer` and `from_reader` top-level functions for parsing text. 86 | 87 | ## [0.1.1] - 2020-12-08 88 | 89 | - Fix docs build on docs.rs by including platform bindings for macos and linux. 90 | 91 | ## [0.1.0] - 2020-12-06 92 | 93 | Initial release. 94 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "exacl" 3 | version = "0.12.0" 4 | authors = ["Bill Fisher "] 5 | description = "Manipulate file system access control lists (ACL) on macOS, Linux, and FreeBSD" 6 | repository = "https://github.com/byllyfish/exacl" 7 | documentation = "https://byllyfish.github.io/exacl" 8 | license = "MIT" 9 | edition = "2021" 10 | keywords = ["acl", "access", "control"] 11 | categories = ["filesystem"] 12 | 13 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 14 | 15 | [features] 16 | # There are two optional features that you can enable: 17 | # - serde 18 | # - buildtime_bindgen 19 | 20 | default = [] 21 | 22 | # Use bindgen to build OS-specific bindings. 23 | # 24 | # On Linux, the bindings depend on the system header. This header 25 | # is only present on systems that have the `libacl1-dev` package installed. 26 | 27 | buildtime_bindgen = ["bindgen"] 28 | 29 | [dependencies] 30 | bitflags = "2.4.2" 31 | log = "0.4.20" 32 | uuid = "1.7.0" 33 | scopeguard = "1.2.0" 34 | serde = { version = "1.0", optional = true, features = ["derive"] } 35 | 36 | [build-dependencies] 37 | bindgen = { version = "0.69.2", optional = true } 38 | 39 | [dev-dependencies] 40 | tempfile = "3.9.0" 41 | ctor = "0.2.6" 42 | 43 | # Used by exacl.rs example. 44 | clap = { version = "4.4.18", features = ["derive"] } 45 | env_logger = "0.11.0" 46 | serde_json = "1.0.111" 47 | 48 | [package.metadata.docs.rs] 49 | rustc-args = ["--cfg", "docsrs"] 50 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020-2023 William W. Fisher 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Exacl 2 | 3 | [![CRATE]][crates] [![API]][docs] [![CI]][actions] [![BUILD]][cirrus] [![COV]][codecov] 4 | 5 | [CRATE]: https://img.shields.io/crates/v/exacl 6 | [crates]: https://crates.io/crates/exacl 7 | [CI]: https://github.com/byllyfish/exacl/workflows/CI/badge.svg 8 | [actions]: https://github.com/byllyfish/exacl/actions?query=branch%3Amain 9 | [API]: https://docs.rs/exacl/badge.svg 10 | [docs]: https://byllyfish.github.io/exacl 11 | [BUILD]: https://api.cirrus-ci.com/github/byllyfish/exacl.svg 12 | [cirrus]: https://cirrus-ci.com/github/byllyfish/exacl 13 | [COV]: https://codecov.io/gh/byllyfish/exacl/branch/main/graph/badge.svg?token=SWkSyVc1w6 14 | [codecov]: https://codecov.io/gh/byllyfish/exacl 15 | 16 | Rust library to manipulate file system access control lists (ACL) on `macOS`, `Linux`, and `FreeBSD`. 17 | 18 | ## Example 19 | 20 | ```rust 21 | use exacl::{getfacl, setfacl, AclEntry, Perm}; 22 | 23 | // Get the ACL from "./tmp/foo". 24 | let mut acl = getfacl("./tmp/foo", None)?; 25 | 26 | // Print the contents of the ACL. 27 | for entry in &acl { 28 | println!("{entry}"); 29 | } 30 | 31 | // Add an ACL entry to the end. 32 | acl.push(AclEntry::allow_user("some_user", Perm::READ, None)); 33 | 34 | // Set the ACL for "./tmp/foo". 35 | setfacl(&["./tmp/foo"], &acl, None)?; 36 | ``` 37 | 38 | ## Benefits 39 | 40 | - Supports the Posix ACL's used by Linux and FreeBSD. 41 | - Supports the extended ACL's used by macOS and FreeBSD/NFSv4. 42 | - Supports reading/writing of ACL's as delimited text. 43 | - Supports serde (optional) for easy reading/writing of ACL's to JSON, YAML and other common formats. 44 | 45 | ## API 46 | 47 | This module provides two high level functions, `getfacl` and `setfacl`. 48 | 49 | - `getfacl` retrieves the ACL for a file or directory. 50 | - `setfacl` sets the ACL for files or directories. 51 | 52 | On Linux and FreeBSD, the ACL contains entries for the default ACL, if 53 | present. 54 | 55 | Both `getfacl` and `setfacl` work with a `Vec`. The 56 | `AclEntry` structure contains five fields: 57 | 58 | - kind : `AclEntryKind` - the kind of entry (User, Group, Other, Mask, 59 | or Unknown). 60 | - name : `String` - name of the principal being given access. You can 61 | use a user/group name, decimal uid/gid, or UUID (on macOS). 62 | - perms : `Perm` - permission bits for the entry. 63 | - flags : `Flag` - flags indicating whether an entry is inherited, etc. 64 | - allow : `bool` - true if entry is allowed; false means deny. Linux only 65 | supports allow=true. 66 | 67 | 68 | ## More Examples 69 | 70 | Here are some more examples showing how to use the library. 71 | 72 | Get an ACL in common delimited string format: 73 | 74 | ```rust 75 | let acl = exacl::getfacl("/tmp/file", None)?; 76 | let result = exacl::to_string(&acl)?; 77 | ``` 78 | 79 | Get an ACL in JSON format: 80 | 81 | ```rust 82 | let acl = exacl::getfacl("/tmp/file", None)?; 83 | let result = serde_json::to_string(&acl)?; 84 | ``` 85 | 86 | Create a linux ACL for permissions that allow the owning user and group to read/write a file 87 | but no one else except for "fred". 88 | 89 | ```rust 90 | let mut acl = exacl::from_mode(0o660); 91 | acl.push(AclEntry::allow_user("fred", Perm::READ | Perm::WRITE, None)); 92 | exacl::setfacl(&["/tmp/file"], &acl, None)?; 93 | ``` 94 | 95 | Create a linux ACL for directory permissions that gives full access to the owning user and group 96 | and read-only access to members of the accounting group. Any sub-directories created should 97 | automatically have the same ACL (via the default ACL). 98 | 99 | ```rust 100 | let mut acl = exacl::from_mode(0o770); 101 | acl.push(AclEntry::allow_group( 102 | "accounting", 103 | Perm::READ | Perm::EXECUTE, 104 | None, 105 | )); 106 | 107 | // Make default_acl a copy of access_acl with the DEFAULT flag set. 108 | let mut default_acl: Vec = acl.clone(); 109 | for entry in &mut default_acl { 110 | entry.flags |= Flag::DEFAULT; 111 | } 112 | acl.append(&mut default_acl); 113 | 114 | exacl::setfacl(&["./tmp/dir"], &acl, None)?; 115 | ``` 116 | 117 | ## Build and Test 118 | 119 | On Linux, you must install the `libacl1-dev` package to build exacl. The integration tests 120 | require `shunit2` which can be installed via apt or homebrew. 121 | 122 | ``` 123 | sudo apt install libacl1-dev shunit2 124 | ``` 125 | 126 | To run the unit tests with debug logging, type: `RUST_LOG=debug cargo test` 127 | 128 | To run the integration tests, type: 129 | 130 | ``` 131 | cargo test --features serde; ./tests/run_tests.sh 132 | ``` 133 | 134 | ### Bindgen Feature 135 | 136 | If there is a problem building exacl on your system, try enabling the bindgen feature. 137 | 138 | ``` 139 | cargo test --features bindgen 140 | ``` 141 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | Security updates are applied only to the latest release. 6 | 7 | ## Reporting a Vulnerability 8 | 9 | If you have discovered a security vulnerability in this project, please report it privately. **Do not disclose it as a public issue.** This gives us time to work with you to fix the issue before public exposure, reducing the chance that the exploit will be used before a patch is released. 10 | 11 | Please disclose it at [security advisory](https://github.com/byllyfish/exacl/security/advisories/new). 12 | 13 | This project is maintained by a team of volunteers on a reasonable-effort basis. As such, please give us at least 60 days to work on a fix before public exposure. 14 | -------------------------------------------------------------------------------- /bindgen/bindings_freebsd.rs: -------------------------------------------------------------------------------- 1 | pub const ENOENT: u32 = 2; 2 | pub const ENOMEM: u32 = 12; 3 | pub const EINVAL: u32 = 22; 4 | pub const ERANGE: u32 = 34; 5 | pub const ENOTSUP: u32 = 45; 6 | pub const ACL_MAX_ENTRIES: u32 = 254; 7 | pub const ACL_BRAND_UNKNOWN: u32 = 0; 8 | pub const ACL_BRAND_POSIX: u32 = 1; 9 | pub const ACL_BRAND_NFS4: u32 = 2; 10 | pub const ACL_UNDEFINED_TAG: u32 = 0; 11 | pub const ACL_USER_OBJ: u32 = 1; 12 | pub const ACL_USER: u32 = 2; 13 | pub const ACL_GROUP_OBJ: u32 = 4; 14 | pub const ACL_GROUP: u32 = 8; 15 | pub const ACL_MASK: u32 = 16; 16 | pub const ACL_OTHER: u32 = 32; 17 | pub const ACL_OTHER_OBJ: u32 = 32; 18 | pub const ACL_EVERYONE: u32 = 64; 19 | pub const ACL_ENTRY_TYPE_ALLOW: u32 = 256; 20 | pub const ACL_ENTRY_TYPE_DENY: u32 = 512; 21 | pub const ACL_ENTRY_TYPE_AUDIT: u32 = 1024; 22 | pub const ACL_ENTRY_TYPE_ALARM: u32 = 2048; 23 | pub const ACL_TYPE_ACCESS_OLD: u32 = 0; 24 | pub const ACL_TYPE_DEFAULT_OLD: u32 = 1; 25 | pub const ACL_TYPE_ACCESS: u32 = 2; 26 | pub const ACL_TYPE_DEFAULT: u32 = 3; 27 | pub const ACL_TYPE_NFS4: u32 = 4; 28 | pub const ACL_EXECUTE: u32 = 1; 29 | pub const ACL_WRITE: u32 = 2; 30 | pub const ACL_READ: u32 = 4; 31 | pub const ACL_PERM_NONE: u32 = 0; 32 | pub const ACL_PERM_BITS: u32 = 7; 33 | pub const ACL_POSIX1E_BITS: u32 = 7; 34 | pub const ACL_READ_DATA: u32 = 8; 35 | pub const ACL_LIST_DIRECTORY: u32 = 8; 36 | pub const ACL_WRITE_DATA: u32 = 16; 37 | pub const ACL_ADD_FILE: u32 = 16; 38 | pub const ACL_APPEND_DATA: u32 = 32; 39 | pub const ACL_ADD_SUBDIRECTORY: u32 = 32; 40 | pub const ACL_READ_NAMED_ATTRS: u32 = 64; 41 | pub const ACL_WRITE_NAMED_ATTRS: u32 = 128; 42 | pub const ACL_DELETE_CHILD: u32 = 256; 43 | pub const ACL_READ_ATTRIBUTES: u32 = 512; 44 | pub const ACL_WRITE_ATTRIBUTES: u32 = 1024; 45 | pub const ACL_DELETE: u32 = 2048; 46 | pub const ACL_READ_ACL: u32 = 4096; 47 | pub const ACL_WRITE_ACL: u32 = 8192; 48 | pub const ACL_WRITE_OWNER: u32 = 16384; 49 | pub const ACL_SYNCHRONIZE: u32 = 32768; 50 | pub const ACL_FULL_SET: u32 = 65529; 51 | pub const ACL_MODIFY_SET: u32 = 40953; 52 | pub const ACL_READ_SET: u32 = 4680; 53 | pub const ACL_WRITE_SET: u32 = 1200; 54 | pub const ACL_NFS4_PERM_BITS: u32 = 65529; 55 | pub const ACL_FIRST_ENTRY: u32 = 0; 56 | pub const ACL_NEXT_ENTRY: u32 = 1; 57 | pub const ACL_ENTRY_FILE_INHERIT: u32 = 1; 58 | pub const ACL_ENTRY_DIRECTORY_INHERIT: u32 = 2; 59 | pub const ACL_ENTRY_NO_PROPAGATE_INHERIT: u32 = 4; 60 | pub const ACL_ENTRY_INHERIT_ONLY: u32 = 8; 61 | pub const ACL_ENTRY_SUCCESSFUL_ACCESS: u32 = 16; 62 | pub const ACL_ENTRY_FAILED_ACCESS: u32 = 32; 63 | pub const ACL_ENTRY_INHERITED: u32 = 128; 64 | pub const ACL_FLAGS_BITS: u32 = 191; 65 | pub const ACL_TEXT_VERBOSE: u32 = 1; 66 | pub const ACL_TEXT_NUMERIC_IDS: u32 = 2; 67 | pub const ACL_TEXT_APPEND_ID: u32 = 4; 68 | pub const _PC_ACL_NFS4: u32 = 64; 69 | pub type __uint32_t = ::std::os::raw::c_uint; 70 | pub type __int64_t = ::std::os::raw::c_long; 71 | pub type __time_t = __int64_t; 72 | pub type __gid_t = __uint32_t; 73 | pub type __uid_t = __uint32_t; 74 | pub type gid_t = __gid_t; 75 | pub type time_t = __time_t; 76 | pub type uid_t = __uid_t; 77 | pub type acl_tag_t = u32; 78 | pub type acl_perm_t = u32; 79 | pub type acl_entry_type_t = u16; 80 | pub type acl_flag_t = u16; 81 | pub type acl_type_t = ::std::os::raw::c_int; 82 | pub type acl_permset_t = *mut ::std::os::raw::c_int; 83 | pub type acl_flagset_t = *mut u16; 84 | pub type acl_entry_t = *mut ::std::os::raw::c_void; 85 | pub type acl_t = *mut ::std::os::raw::c_void; 86 | extern "C" { 87 | pub fn acl_add_flag_np(_flagset_d: acl_flagset_t, _flag: acl_flag_t) -> ::std::os::raw::c_int; 88 | } 89 | extern "C" { 90 | pub fn acl_add_perm(_permset_d: acl_permset_t, _perm: acl_perm_t) -> ::std::os::raw::c_int; 91 | } 92 | extern "C" { 93 | pub fn acl_calc_mask(_acl_p: *mut acl_t) -> ::std::os::raw::c_int; 94 | } 95 | extern "C" { 96 | pub fn acl_clear_flags_np(_flagset_d: acl_flagset_t) -> ::std::os::raw::c_int; 97 | } 98 | extern "C" { 99 | pub fn acl_clear_perms(_permset_d: acl_permset_t) -> ::std::os::raw::c_int; 100 | } 101 | extern "C" { 102 | pub fn acl_copy_entry(_dest_d: acl_entry_t, _src_d: acl_entry_t) -> ::std::os::raw::c_int; 103 | } 104 | extern "C" { 105 | pub fn acl_copy_ext(_buf_p: *mut ::std::os::raw::c_void, _acl: acl_t, _size: isize) -> isize; 106 | } 107 | extern "C" { 108 | pub fn acl_copy_int(_buf_p: *const ::std::os::raw::c_void) -> acl_t; 109 | } 110 | extern "C" { 111 | pub fn acl_create_entry( 112 | _acl_p: *mut acl_t, 113 | _entry_p: *mut acl_entry_t, 114 | ) -> ::std::os::raw::c_int; 115 | } 116 | extern "C" { 117 | pub fn acl_create_entry_np( 118 | _acl_p: *mut acl_t, 119 | _entry_p: *mut acl_entry_t, 120 | _index: ::std::os::raw::c_int, 121 | ) -> ::std::os::raw::c_int; 122 | } 123 | extern "C" { 124 | pub fn acl_delete_entry(_acl: acl_t, _entry_d: acl_entry_t) -> ::std::os::raw::c_int; 125 | } 126 | extern "C" { 127 | pub fn acl_delete_entry_np(_acl: acl_t, _index: ::std::os::raw::c_int) 128 | -> ::std::os::raw::c_int; 129 | } 130 | extern "C" { 131 | pub fn acl_delete_fd_np( 132 | _filedes: ::std::os::raw::c_int, 133 | _type: acl_type_t, 134 | ) -> ::std::os::raw::c_int; 135 | } 136 | extern "C" { 137 | pub fn acl_delete_file_np( 138 | _path_p: *const ::std::os::raw::c_char, 139 | _type: acl_type_t, 140 | ) -> ::std::os::raw::c_int; 141 | } 142 | extern "C" { 143 | pub fn acl_delete_link_np( 144 | _path_p: *const ::std::os::raw::c_char, 145 | _type: acl_type_t, 146 | ) -> ::std::os::raw::c_int; 147 | } 148 | extern "C" { 149 | pub fn acl_delete_def_file(_path_p: *const ::std::os::raw::c_char) -> ::std::os::raw::c_int; 150 | } 151 | extern "C" { 152 | pub fn acl_delete_def_link_np(_path_p: *const ::std::os::raw::c_char) -> ::std::os::raw::c_int; 153 | } 154 | extern "C" { 155 | pub fn acl_delete_flag_np( 156 | _flagset_d: acl_flagset_t, 157 | _flag: acl_flag_t, 158 | ) -> ::std::os::raw::c_int; 159 | } 160 | extern "C" { 161 | pub fn acl_delete_perm(_permset_d: acl_permset_t, _perm: acl_perm_t) -> ::std::os::raw::c_int; 162 | } 163 | extern "C" { 164 | pub fn acl_dup(_acl: acl_t) -> acl_t; 165 | } 166 | extern "C" { 167 | pub fn acl_free(_obj_p: *mut ::std::os::raw::c_void) -> ::std::os::raw::c_int; 168 | } 169 | extern "C" { 170 | pub fn acl_from_text(_buf_p: *const ::std::os::raw::c_char) -> acl_t; 171 | } 172 | extern "C" { 173 | pub fn acl_get_brand_np( 174 | _acl: acl_t, 175 | _brand_p: *mut ::std::os::raw::c_int, 176 | ) -> ::std::os::raw::c_int; 177 | } 178 | extern "C" { 179 | pub fn acl_get_entry( 180 | _acl: acl_t, 181 | _entry_id: ::std::os::raw::c_int, 182 | _entry_p: *mut acl_entry_t, 183 | ) -> ::std::os::raw::c_int; 184 | } 185 | extern "C" { 186 | pub fn acl_get_fd(_fd: ::std::os::raw::c_int) -> acl_t; 187 | } 188 | extern "C" { 189 | pub fn acl_get_fd_np(fd: ::std::os::raw::c_int, _type: acl_type_t) -> acl_t; 190 | } 191 | extern "C" { 192 | pub fn acl_get_file(_path_p: *const ::std::os::raw::c_char, _type: acl_type_t) -> acl_t; 193 | } 194 | extern "C" { 195 | pub fn acl_get_entry_type_np( 196 | _entry_d: acl_entry_t, 197 | _entry_type_p: *mut acl_entry_type_t, 198 | ) -> ::std::os::raw::c_int; 199 | } 200 | extern "C" { 201 | pub fn acl_get_link_np(_path_p: *const ::std::os::raw::c_char, _type: acl_type_t) -> acl_t; 202 | } 203 | extern "C" { 204 | pub fn acl_get_qualifier(_entry_d: acl_entry_t) -> *mut ::std::os::raw::c_void; 205 | } 206 | extern "C" { 207 | pub fn acl_get_flag_np(_flagset_d: acl_flagset_t, _flag: acl_flag_t) -> ::std::os::raw::c_int; 208 | } 209 | extern "C" { 210 | pub fn acl_get_perm_np(_permset_d: acl_permset_t, _perm: acl_perm_t) -> ::std::os::raw::c_int; 211 | } 212 | extern "C" { 213 | pub fn acl_get_flagset_np( 214 | _entry_d: acl_entry_t, 215 | _flagset_p: *mut acl_flagset_t, 216 | ) -> ::std::os::raw::c_int; 217 | } 218 | extern "C" { 219 | pub fn acl_get_permset( 220 | _entry_d: acl_entry_t, 221 | _permset_p: *mut acl_permset_t, 222 | ) -> ::std::os::raw::c_int; 223 | } 224 | extern "C" { 225 | pub fn acl_get_tag_type( 226 | _entry_d: acl_entry_t, 227 | _tag_type_p: *mut acl_tag_t, 228 | ) -> ::std::os::raw::c_int; 229 | } 230 | extern "C" { 231 | pub fn acl_init(_count: ::std::os::raw::c_int) -> acl_t; 232 | } 233 | extern "C" { 234 | pub fn acl_set_fd(_fd: ::std::os::raw::c_int, _acl: acl_t) -> ::std::os::raw::c_int; 235 | } 236 | extern "C" { 237 | pub fn acl_set_fd_np( 238 | _fd: ::std::os::raw::c_int, 239 | _acl: acl_t, 240 | _type: acl_type_t, 241 | ) -> ::std::os::raw::c_int; 242 | } 243 | extern "C" { 244 | pub fn acl_set_file( 245 | _path_p: *const ::std::os::raw::c_char, 246 | _type: acl_type_t, 247 | _acl: acl_t, 248 | ) -> ::std::os::raw::c_int; 249 | } 250 | extern "C" { 251 | pub fn acl_set_entry_type_np( 252 | _entry_d: acl_entry_t, 253 | _entry_type: acl_entry_type_t, 254 | ) -> ::std::os::raw::c_int; 255 | } 256 | extern "C" { 257 | pub fn acl_set_link_np( 258 | _path_p: *const ::std::os::raw::c_char, 259 | _type: acl_type_t, 260 | _acl: acl_t, 261 | ) -> ::std::os::raw::c_int; 262 | } 263 | extern "C" { 264 | pub fn acl_set_flagset_np( 265 | _entry_d: acl_entry_t, 266 | _flagset_d: acl_flagset_t, 267 | ) -> ::std::os::raw::c_int; 268 | } 269 | extern "C" { 270 | pub fn acl_set_permset( 271 | _entry_d: acl_entry_t, 272 | _permset_d: acl_permset_t, 273 | ) -> ::std::os::raw::c_int; 274 | } 275 | extern "C" { 276 | pub fn acl_set_qualifier( 277 | _entry_d: acl_entry_t, 278 | _tag_qualifier_p: *const ::std::os::raw::c_void, 279 | ) -> ::std::os::raw::c_int; 280 | } 281 | extern "C" { 282 | pub fn acl_set_tag_type(_entry_d: acl_entry_t, _tag_type: acl_tag_t) -> ::std::os::raw::c_int; 283 | } 284 | extern "C" { 285 | pub fn acl_size(_acl: acl_t) -> isize; 286 | } 287 | extern "C" { 288 | pub fn acl_to_text(_acl: acl_t, _len_p: *mut isize) -> *mut ::std::os::raw::c_char; 289 | } 290 | extern "C" { 291 | pub fn acl_to_text_np( 292 | _acl: acl_t, 293 | _len_p: *mut isize, 294 | _flags: ::std::os::raw::c_int, 295 | ) -> *mut ::std::os::raw::c_char; 296 | } 297 | extern "C" { 298 | pub fn acl_valid(_acl: acl_t) -> ::std::os::raw::c_int; 299 | } 300 | extern "C" { 301 | pub fn acl_valid_fd_np( 302 | _fd: ::std::os::raw::c_int, 303 | _type: acl_type_t, 304 | _acl: acl_t, 305 | ) -> ::std::os::raw::c_int; 306 | } 307 | extern "C" { 308 | pub fn acl_valid_file_np( 309 | _path_p: *const ::std::os::raw::c_char, 310 | _type: acl_type_t, 311 | _acl: acl_t, 312 | ) -> ::std::os::raw::c_int; 313 | } 314 | extern "C" { 315 | pub fn acl_valid_link_np( 316 | _path_p: *const ::std::os::raw::c_char, 317 | _type: acl_type_t, 318 | _acl: acl_t, 319 | ) -> ::std::os::raw::c_int; 320 | } 321 | extern "C" { 322 | pub fn acl_is_trivial_np( 323 | _acl: acl_t, 324 | _trivialp: *mut ::std::os::raw::c_int, 325 | ) -> ::std::os::raw::c_int; 326 | } 327 | extern "C" { 328 | pub fn acl_strip_np(_acl: acl_t, recalculate_mask: ::std::os::raw::c_int) -> acl_t; 329 | } 330 | #[repr(C)] 331 | #[derive(Debug, Copy, Clone)] 332 | pub struct group { 333 | pub gr_name: *mut ::std::os::raw::c_char, 334 | pub gr_passwd: *mut ::std::os::raw::c_char, 335 | pub gr_gid: gid_t, 336 | pub gr_mem: *mut *mut ::std::os::raw::c_char, 337 | } 338 | extern "C" { 339 | pub fn getgrgid_r( 340 | arg1: gid_t, 341 | arg2: *mut group, 342 | arg3: *mut ::std::os::raw::c_char, 343 | arg4: usize, 344 | arg5: *mut *mut group, 345 | ) -> ::std::os::raw::c_int; 346 | } 347 | extern "C" { 348 | pub fn getgrnam_r( 349 | arg1: *const ::std::os::raw::c_char, 350 | arg2: *mut group, 351 | arg3: *mut ::std::os::raw::c_char, 352 | arg4: usize, 353 | arg5: *mut *mut group, 354 | ) -> ::std::os::raw::c_int; 355 | } 356 | #[repr(C)] 357 | #[derive(Debug, Copy, Clone)] 358 | pub struct passwd { 359 | pub pw_name: *mut ::std::os::raw::c_char, 360 | pub pw_passwd: *mut ::std::os::raw::c_char, 361 | pub pw_uid: uid_t, 362 | pub pw_gid: gid_t, 363 | pub pw_change: time_t, 364 | pub pw_class: *mut ::std::os::raw::c_char, 365 | pub pw_gecos: *mut ::std::os::raw::c_char, 366 | pub pw_dir: *mut ::std::os::raw::c_char, 367 | pub pw_shell: *mut ::std::os::raw::c_char, 368 | pub pw_expire: time_t, 369 | pub pw_fields: ::std::os::raw::c_int, 370 | } 371 | extern "C" { 372 | pub fn getpwnam_r( 373 | arg1: *const ::std::os::raw::c_char, 374 | arg2: *mut passwd, 375 | arg3: *mut ::std::os::raw::c_char, 376 | arg4: usize, 377 | arg5: *mut *mut passwd, 378 | ) -> ::std::os::raw::c_int; 379 | } 380 | extern "C" { 381 | pub fn getpwuid_r( 382 | arg1: uid_t, 383 | arg2: *mut passwd, 384 | arg3: *mut ::std::os::raw::c_char, 385 | arg4: usize, 386 | arg5: *mut *mut passwd, 387 | ) -> ::std::os::raw::c_int; 388 | } 389 | extern "C" { 390 | pub fn pathconf( 391 | arg1: *const ::std::os::raw::c_char, 392 | arg2: ::std::os::raw::c_int, 393 | ) -> ::std::os::raw::c_long; 394 | } 395 | extern "C" { 396 | pub fn lpathconf( 397 | arg1: *const ::std::os::raw::c_char, 398 | arg2: ::std::os::raw::c_int, 399 | ) -> ::std::os::raw::c_long; 400 | } 401 | -------------------------------------------------------------------------------- /bindgen/bindings_freebsd14.diff: -------------------------------------------------------------------------------- 1 | 69d68 2 | < pub type __uint16_t = ::std::os::raw::c_ushort; 3 | 74d72 4 | < pub type __mode_t = __uint16_t; 5 | 77d74 6 | < pub type mode_t = __mode_t; 7 | 105,107d101 8 | < pub fn acl_cmp_np(_acl1: acl_t, _acl2: acl_t) -> ::std::os::raw::c_int; 9 | < } 10 | < extern "C" { 11 | 173,186d166 12 | < pub fn acl_equiv_mode_np(_acl: acl_t, _mode_p: *mut mode_t) -> ::std::os::raw::c_int; 13 | < } 14 | < extern "C" { 15 | < pub fn acl_extended_file_np(_path_p: *const ::std::os::raw::c_char) -> ::std::os::raw::c_int; 16 | < } 17 | < extern "C" { 18 | < pub fn acl_extended_file_nofollow_np( 19 | < _path_p: *const ::std::os::raw::c_char, 20 | < ) -> ::std::os::raw::c_int; 21 | < } 22 | < extern "C" { 23 | < pub fn acl_extended_link_np(_path_p: *const ::std::os::raw::c_char) -> ::std::os::raw::c_int; 24 | < } 25 | < extern "C" { 26 | 188,190d167 27 | < } 28 | < extern "C" { 29 | < pub fn acl_from_mode_np(_mode: mode_t) -> acl_t; 30 | -------------------------------------------------------------------------------- /bindgen/bindings_linux.rs: -------------------------------------------------------------------------------- 1 | pub const ENOENT: u32 = 2; 2 | pub const ENOMEM: u32 = 12; 3 | pub const EINVAL: u32 = 22; 4 | pub const ERANGE: u32 = 34; 5 | pub const ENOTSUP: u32 = 95; 6 | pub const ACL_READ: u32 = 4; 7 | pub const ACL_WRITE: u32 = 2; 8 | pub const ACL_EXECUTE: u32 = 1; 9 | pub const ACL_UNDEFINED_TAG: u32 = 0; 10 | pub const ACL_USER_OBJ: u32 = 1; 11 | pub const ACL_USER: u32 = 2; 12 | pub const ACL_GROUP_OBJ: u32 = 4; 13 | pub const ACL_GROUP: u32 = 8; 14 | pub const ACL_MASK: u32 = 16; 15 | pub const ACL_OTHER: u32 = 32; 16 | pub const ACL_TYPE_ACCESS: u32 = 32768; 17 | pub const ACL_TYPE_DEFAULT: u32 = 16384; 18 | pub const ACL_FIRST_ENTRY: u32 = 0; 19 | pub const ACL_NEXT_ENTRY: u32 = 1; 20 | pub const ACL_MULTI_ERROR: u32 = 4096; 21 | pub const ACL_DUPLICATE_ERROR: u32 = 8192; 22 | pub const ACL_MISS_ERROR: u32 = 12288; 23 | pub const ACL_ENTRY_ERROR: u32 = 16384; 24 | pub type __uid_t = ::std::os::raw::c_uint; 25 | pub type __gid_t = ::std::os::raw::c_uint; 26 | pub type __mode_t = ::std::os::raw::c_uint; 27 | pub type gid_t = __gid_t; 28 | pub type mode_t = __mode_t; 29 | pub type uid_t = __uid_t; 30 | #[repr(C)] 31 | #[derive(Debug, Copy, Clone)] 32 | pub struct __acl_ext { 33 | _unused: [u8; 0], 34 | } 35 | #[repr(C)] 36 | #[derive(Debug, Copy, Clone)] 37 | pub struct __acl_entry_ext { 38 | _unused: [u8; 0], 39 | } 40 | #[repr(C)] 41 | #[derive(Debug, Copy, Clone)] 42 | pub struct __acl_permset_ext { 43 | _unused: [u8; 0], 44 | } 45 | pub type acl_type_t = ::std::os::raw::c_uint; 46 | pub type acl_tag_t = ::std::os::raw::c_int; 47 | pub type acl_perm_t = ::std::os::raw::c_uint; 48 | pub type acl_t = *mut __acl_ext; 49 | pub type acl_entry_t = *mut __acl_entry_ext; 50 | pub type acl_permset_t = *mut __acl_permset_ext; 51 | extern "C" { 52 | pub fn acl_init(count: ::std::os::raw::c_int) -> acl_t; 53 | } 54 | extern "C" { 55 | pub fn acl_dup(acl: acl_t) -> acl_t; 56 | } 57 | extern "C" { 58 | pub fn acl_free(obj_p: *mut ::std::os::raw::c_void) -> ::std::os::raw::c_int; 59 | } 60 | extern "C" { 61 | pub fn acl_valid(acl: acl_t) -> ::std::os::raw::c_int; 62 | } 63 | extern "C" { 64 | pub fn acl_copy_entry(dest_d: acl_entry_t, src_d: acl_entry_t) -> ::std::os::raw::c_int; 65 | } 66 | extern "C" { 67 | pub fn acl_create_entry(acl_p: *mut acl_t, entry_p: *mut acl_entry_t) -> ::std::os::raw::c_int; 68 | } 69 | extern "C" { 70 | pub fn acl_delete_entry(acl: acl_t, entry_d: acl_entry_t) -> ::std::os::raw::c_int; 71 | } 72 | extern "C" { 73 | pub fn acl_get_entry( 74 | acl: acl_t, 75 | entry_id: ::std::os::raw::c_int, 76 | entry_p: *mut acl_entry_t, 77 | ) -> ::std::os::raw::c_int; 78 | } 79 | extern "C" { 80 | pub fn acl_add_perm(permset_d: acl_permset_t, perm: acl_perm_t) -> ::std::os::raw::c_int; 81 | } 82 | extern "C" { 83 | pub fn acl_calc_mask(acl_p: *mut acl_t) -> ::std::os::raw::c_int; 84 | } 85 | extern "C" { 86 | pub fn acl_clear_perms(permset_d: acl_permset_t) -> ::std::os::raw::c_int; 87 | } 88 | extern "C" { 89 | pub fn acl_delete_perm(permset_d: acl_permset_t, perm: acl_perm_t) -> ::std::os::raw::c_int; 90 | } 91 | extern "C" { 92 | pub fn acl_get_permset( 93 | entry_d: acl_entry_t, 94 | permset_p: *mut acl_permset_t, 95 | ) -> ::std::os::raw::c_int; 96 | } 97 | extern "C" { 98 | pub fn acl_set_permset(entry_d: acl_entry_t, permset_d: acl_permset_t) 99 | -> ::std::os::raw::c_int; 100 | } 101 | extern "C" { 102 | pub fn acl_get_qualifier(entry_d: acl_entry_t) -> *mut ::std::os::raw::c_void; 103 | } 104 | extern "C" { 105 | pub fn acl_get_tag_type( 106 | entry_d: acl_entry_t, 107 | tag_type_p: *mut acl_tag_t, 108 | ) -> ::std::os::raw::c_int; 109 | } 110 | extern "C" { 111 | pub fn acl_set_qualifier( 112 | entry_d: acl_entry_t, 113 | tag_qualifier_p: *const ::std::os::raw::c_void, 114 | ) -> ::std::os::raw::c_int; 115 | } 116 | extern "C" { 117 | pub fn acl_set_tag_type(entry_d: acl_entry_t, tag_type: acl_tag_t) -> ::std::os::raw::c_int; 118 | } 119 | extern "C" { 120 | pub fn acl_copy_ext(buf_p: *mut ::std::os::raw::c_void, acl: acl_t, size: isize) -> isize; 121 | } 122 | extern "C" { 123 | pub fn acl_copy_int(buf_p: *const ::std::os::raw::c_void) -> acl_t; 124 | } 125 | extern "C" { 126 | pub fn acl_from_text(buf_p: *const ::std::os::raw::c_char) -> acl_t; 127 | } 128 | extern "C" { 129 | pub fn acl_size(acl: acl_t) -> isize; 130 | } 131 | extern "C" { 132 | pub fn acl_to_text(acl: acl_t, len_p: *mut isize) -> *mut ::std::os::raw::c_char; 133 | } 134 | extern "C" { 135 | pub fn acl_delete_def_file(path_p: *const ::std::os::raw::c_char) -> ::std::os::raw::c_int; 136 | } 137 | extern "C" { 138 | pub fn acl_get_fd(fd: ::std::os::raw::c_int) -> acl_t; 139 | } 140 | extern "C" { 141 | pub fn acl_get_file(path_p: *const ::std::os::raw::c_char, type_: acl_type_t) -> acl_t; 142 | } 143 | extern "C" { 144 | pub fn acl_set_fd(fd: ::std::os::raw::c_int, acl: acl_t) -> ::std::os::raw::c_int; 145 | } 146 | extern "C" { 147 | pub fn acl_set_file( 148 | path_p: *const ::std::os::raw::c_char, 149 | type_: acl_type_t, 150 | acl: acl_t, 151 | ) -> ::std::os::raw::c_int; 152 | } 153 | extern "C" { 154 | pub fn acl_to_any_text( 155 | acl: acl_t, 156 | prefix: *const ::std::os::raw::c_char, 157 | separator: ::std::os::raw::c_char, 158 | options: ::std::os::raw::c_int, 159 | ) -> *mut ::std::os::raw::c_char; 160 | } 161 | extern "C" { 162 | pub fn acl_cmp(acl1: acl_t, acl2: acl_t) -> ::std::os::raw::c_int; 163 | } 164 | extern "C" { 165 | pub fn acl_check(acl: acl_t, last: *mut ::std::os::raw::c_int) -> ::std::os::raw::c_int; 166 | } 167 | extern "C" { 168 | pub fn acl_from_mode(mode: mode_t) -> acl_t; 169 | } 170 | extern "C" { 171 | pub fn acl_equiv_mode(acl: acl_t, mode_p: *mut mode_t) -> ::std::os::raw::c_int; 172 | } 173 | extern "C" { 174 | pub fn acl_extended_file(path_p: *const ::std::os::raw::c_char) -> ::std::os::raw::c_int; 175 | } 176 | extern "C" { 177 | pub fn acl_extended_file_nofollow( 178 | path_p: *const ::std::os::raw::c_char, 179 | ) -> ::std::os::raw::c_int; 180 | } 181 | extern "C" { 182 | pub fn acl_extended_fd(fd: ::std::os::raw::c_int) -> ::std::os::raw::c_int; 183 | } 184 | extern "C" { 185 | pub fn acl_entries(acl: acl_t) -> ::std::os::raw::c_int; 186 | } 187 | extern "C" { 188 | pub fn acl_error(code: ::std::os::raw::c_int) -> *const ::std::os::raw::c_char; 189 | } 190 | extern "C" { 191 | pub fn acl_get_perm(permset_d: acl_permset_t, perm: acl_perm_t) -> ::std::os::raw::c_int; 192 | } 193 | #[repr(C)] 194 | #[derive(Debug, Copy, Clone)] 195 | pub struct group { 196 | pub gr_name: *mut ::std::os::raw::c_char, 197 | pub gr_passwd: *mut ::std::os::raw::c_char, 198 | pub gr_gid: __gid_t, 199 | pub gr_mem: *mut *mut ::std::os::raw::c_char, 200 | } 201 | extern "C" { 202 | pub fn getgrgid_r( 203 | __gid: __gid_t, 204 | __resultbuf: *mut group, 205 | __buffer: *mut ::std::os::raw::c_char, 206 | __buflen: usize, 207 | __result: *mut *mut group, 208 | ) -> ::std::os::raw::c_int; 209 | } 210 | extern "C" { 211 | pub fn getgrnam_r( 212 | __name: *const ::std::os::raw::c_char, 213 | __resultbuf: *mut group, 214 | __buffer: *mut ::std::os::raw::c_char, 215 | __buflen: usize, 216 | __result: *mut *mut group, 217 | ) -> ::std::os::raw::c_int; 218 | } 219 | #[repr(C)] 220 | #[derive(Debug, Copy, Clone)] 221 | pub struct passwd { 222 | pub pw_name: *mut ::std::os::raw::c_char, 223 | pub pw_passwd: *mut ::std::os::raw::c_char, 224 | pub pw_uid: __uid_t, 225 | pub pw_gid: __gid_t, 226 | pub pw_gecos: *mut ::std::os::raw::c_char, 227 | pub pw_dir: *mut ::std::os::raw::c_char, 228 | pub pw_shell: *mut ::std::os::raw::c_char, 229 | } 230 | extern "C" { 231 | pub fn getpwuid_r( 232 | __uid: __uid_t, 233 | __resultbuf: *mut passwd, 234 | __buffer: *mut ::std::os::raw::c_char, 235 | __buflen: usize, 236 | __result: *mut *mut passwd, 237 | ) -> ::std::os::raw::c_int; 238 | } 239 | extern "C" { 240 | pub fn getpwnam_r( 241 | __name: *const ::std::os::raw::c_char, 242 | __resultbuf: *mut passwd, 243 | __buffer: *mut ::std::os::raw::c_char, 244 | __buflen: usize, 245 | __result: *mut *mut passwd, 246 | ) -> ::std::os::raw::c_int; 247 | } 248 | -------------------------------------------------------------------------------- /bindgen/bindings_macos.rs: -------------------------------------------------------------------------------- 1 | pub const ENOENT: u32 = 2; 2 | pub const ENOMEM: u32 = 12; 3 | pub const EINVAL: u32 = 22; 4 | pub const ERANGE: u32 = 34; 5 | pub const ENOTSUP: u32 = 45; 6 | pub const ACL_MAX_ENTRIES: u32 = 128; 7 | pub const O_SYMLINK: u32 = 2097152; 8 | pub const ID_TYPE_UID: u32 = 0; 9 | pub const ID_TYPE_GID: u32 = 1; 10 | pub type __uint32_t = ::std::os::raw::c_uint; 11 | pub type __darwin_time_t = ::std::os::raw::c_long; 12 | pub type u_int64_t = ::std::os::raw::c_ulonglong; 13 | pub type __darwin_gid_t = __uint32_t; 14 | pub type __darwin_id_t = __uint32_t; 15 | pub type __darwin_uid_t = __uint32_t; 16 | pub type __darwin_uuid_t = [::std::os::raw::c_uchar; 16usize]; 17 | pub type gid_t = __darwin_gid_t; 18 | pub type id_t = __darwin_id_t; 19 | pub type uid_t = __darwin_uid_t; 20 | pub const acl_perm_t_ACL_READ_DATA: acl_perm_t = 2; 21 | pub const acl_perm_t_ACL_LIST_DIRECTORY: acl_perm_t = 2; 22 | pub const acl_perm_t_ACL_WRITE_DATA: acl_perm_t = 4; 23 | pub const acl_perm_t_ACL_ADD_FILE: acl_perm_t = 4; 24 | pub const acl_perm_t_ACL_EXECUTE: acl_perm_t = 8; 25 | pub const acl_perm_t_ACL_SEARCH: acl_perm_t = 8; 26 | pub const acl_perm_t_ACL_DELETE: acl_perm_t = 16; 27 | pub const acl_perm_t_ACL_APPEND_DATA: acl_perm_t = 32; 28 | pub const acl_perm_t_ACL_ADD_SUBDIRECTORY: acl_perm_t = 32; 29 | pub const acl_perm_t_ACL_DELETE_CHILD: acl_perm_t = 64; 30 | pub const acl_perm_t_ACL_READ_ATTRIBUTES: acl_perm_t = 128; 31 | pub const acl_perm_t_ACL_WRITE_ATTRIBUTES: acl_perm_t = 256; 32 | pub const acl_perm_t_ACL_READ_EXTATTRIBUTES: acl_perm_t = 512; 33 | pub const acl_perm_t_ACL_WRITE_EXTATTRIBUTES: acl_perm_t = 1024; 34 | pub const acl_perm_t_ACL_READ_SECURITY: acl_perm_t = 2048; 35 | pub const acl_perm_t_ACL_WRITE_SECURITY: acl_perm_t = 4096; 36 | pub const acl_perm_t_ACL_CHANGE_OWNER: acl_perm_t = 8192; 37 | pub const acl_perm_t_ACL_SYNCHRONIZE: acl_perm_t = 1048576; 38 | pub type acl_perm_t = ::std::os::raw::c_uint; 39 | pub const acl_tag_t_ACL_UNDEFINED_TAG: acl_tag_t = 0; 40 | pub const acl_tag_t_ACL_EXTENDED_ALLOW: acl_tag_t = 1; 41 | pub const acl_tag_t_ACL_EXTENDED_DENY: acl_tag_t = 2; 42 | pub type acl_tag_t = ::std::os::raw::c_uint; 43 | pub const acl_type_t_ACL_TYPE_EXTENDED: acl_type_t = 256; 44 | pub const acl_type_t_ACL_TYPE_ACCESS: acl_type_t = 0; 45 | pub const acl_type_t_ACL_TYPE_DEFAULT: acl_type_t = 1; 46 | pub const acl_type_t_ACL_TYPE_AFS: acl_type_t = 2; 47 | pub const acl_type_t_ACL_TYPE_CODA: acl_type_t = 3; 48 | pub const acl_type_t_ACL_TYPE_NTFS: acl_type_t = 4; 49 | pub const acl_type_t_ACL_TYPE_NWFS: acl_type_t = 5; 50 | pub type acl_type_t = ::std::os::raw::c_uint; 51 | pub const acl_entry_id_t_ACL_FIRST_ENTRY: acl_entry_id_t = 0; 52 | pub const acl_entry_id_t_ACL_NEXT_ENTRY: acl_entry_id_t = -1; 53 | pub const acl_entry_id_t_ACL_LAST_ENTRY: acl_entry_id_t = -2; 54 | pub type acl_entry_id_t = ::std::os::raw::c_int; 55 | pub const acl_flag_t_ACL_FLAG_DEFER_INHERIT: acl_flag_t = 1; 56 | pub const acl_flag_t_ACL_FLAG_NO_INHERIT: acl_flag_t = 131072; 57 | pub const acl_flag_t_ACL_ENTRY_INHERITED: acl_flag_t = 16; 58 | pub const acl_flag_t_ACL_ENTRY_FILE_INHERIT: acl_flag_t = 32; 59 | pub const acl_flag_t_ACL_ENTRY_DIRECTORY_INHERIT: acl_flag_t = 64; 60 | pub const acl_flag_t_ACL_ENTRY_LIMIT_INHERIT: acl_flag_t = 128; 61 | pub const acl_flag_t_ACL_ENTRY_ONLY_INHERIT: acl_flag_t = 256; 62 | pub type acl_flag_t = ::std::os::raw::c_uint; 63 | #[repr(C)] 64 | #[derive(Debug, Copy, Clone)] 65 | pub struct _acl { 66 | _unused: [u8; 0], 67 | } 68 | #[repr(C)] 69 | #[derive(Debug, Copy, Clone)] 70 | pub struct _acl_entry { 71 | _unused: [u8; 0], 72 | } 73 | #[repr(C)] 74 | #[derive(Debug, Copy, Clone)] 75 | pub struct _acl_permset { 76 | _unused: [u8; 0], 77 | } 78 | #[repr(C)] 79 | #[derive(Debug, Copy, Clone)] 80 | pub struct _acl_flagset { 81 | _unused: [u8; 0], 82 | } 83 | pub type acl_t = *mut _acl; 84 | pub type acl_entry_t = *mut _acl_entry; 85 | pub type acl_permset_t = *mut _acl_permset; 86 | pub type acl_flagset_t = *mut _acl_flagset; 87 | pub type acl_permset_mask_t = u_int64_t; 88 | extern "C" { 89 | pub fn acl_dup(acl: acl_t) -> acl_t; 90 | } 91 | extern "C" { 92 | pub fn acl_free(obj_p: *mut ::std::os::raw::c_void) -> ::std::os::raw::c_int; 93 | } 94 | extern "C" { 95 | pub fn acl_init(count: ::std::os::raw::c_int) -> acl_t; 96 | } 97 | extern "C" { 98 | pub fn acl_copy_entry(dest_d: acl_entry_t, src_d: acl_entry_t) -> ::std::os::raw::c_int; 99 | } 100 | extern "C" { 101 | pub fn acl_create_entry(acl_p: *mut acl_t, entry_p: *mut acl_entry_t) -> ::std::os::raw::c_int; 102 | } 103 | extern "C" { 104 | pub fn acl_create_entry_np( 105 | acl_p: *mut acl_t, 106 | entry_p: *mut acl_entry_t, 107 | entry_index: ::std::os::raw::c_int, 108 | ) -> ::std::os::raw::c_int; 109 | } 110 | extern "C" { 111 | pub fn acl_delete_entry(acl: acl_t, entry_d: acl_entry_t) -> ::std::os::raw::c_int; 112 | } 113 | extern "C" { 114 | pub fn acl_get_entry( 115 | acl: acl_t, 116 | entry_id: ::std::os::raw::c_int, 117 | entry_p: *mut acl_entry_t, 118 | ) -> ::std::os::raw::c_int; 119 | } 120 | extern "C" { 121 | pub fn acl_valid(acl: acl_t) -> ::std::os::raw::c_int; 122 | } 123 | extern "C" { 124 | pub fn acl_valid_fd_np( 125 | fd: ::std::os::raw::c_int, 126 | type_: acl_type_t, 127 | acl: acl_t, 128 | ) -> ::std::os::raw::c_int; 129 | } 130 | extern "C" { 131 | pub fn acl_valid_file_np( 132 | path: *const ::std::os::raw::c_char, 133 | type_: acl_type_t, 134 | acl: acl_t, 135 | ) -> ::std::os::raw::c_int; 136 | } 137 | extern "C" { 138 | pub fn acl_valid_link_np( 139 | path: *const ::std::os::raw::c_char, 140 | type_: acl_type_t, 141 | acl: acl_t, 142 | ) -> ::std::os::raw::c_int; 143 | } 144 | extern "C" { 145 | pub fn acl_add_perm(permset_d: acl_permset_t, perm: acl_perm_t) -> ::std::os::raw::c_int; 146 | } 147 | extern "C" { 148 | pub fn acl_calc_mask(acl_p: *mut acl_t) -> ::std::os::raw::c_int; 149 | } 150 | extern "C" { 151 | pub fn acl_clear_perms(permset_d: acl_permset_t) -> ::std::os::raw::c_int; 152 | } 153 | extern "C" { 154 | pub fn acl_delete_perm(permset_d: acl_permset_t, perm: acl_perm_t) -> ::std::os::raw::c_int; 155 | } 156 | extern "C" { 157 | pub fn acl_get_perm_np(permset_d: acl_permset_t, perm: acl_perm_t) -> ::std::os::raw::c_int; 158 | } 159 | extern "C" { 160 | pub fn acl_get_permset( 161 | entry_d: acl_entry_t, 162 | permset_p: *mut acl_permset_t, 163 | ) -> ::std::os::raw::c_int; 164 | } 165 | extern "C" { 166 | pub fn acl_set_permset(entry_d: acl_entry_t, permset_d: acl_permset_t) 167 | -> ::std::os::raw::c_int; 168 | } 169 | extern "C" { 170 | pub fn acl_maximal_permset_mask_np(mask_p: *mut acl_permset_mask_t) -> ::std::os::raw::c_int; 171 | } 172 | extern "C" { 173 | pub fn acl_get_permset_mask_np( 174 | entry_d: acl_entry_t, 175 | mask_p: *mut acl_permset_mask_t, 176 | ) -> ::std::os::raw::c_int; 177 | } 178 | extern "C" { 179 | pub fn acl_set_permset_mask_np( 180 | entry_d: acl_entry_t, 181 | mask: acl_permset_mask_t, 182 | ) -> ::std::os::raw::c_int; 183 | } 184 | extern "C" { 185 | pub fn acl_add_flag_np(flagset_d: acl_flagset_t, flag: acl_flag_t) -> ::std::os::raw::c_int; 186 | } 187 | extern "C" { 188 | pub fn acl_clear_flags_np(flagset_d: acl_flagset_t) -> ::std::os::raw::c_int; 189 | } 190 | extern "C" { 191 | pub fn acl_delete_flag_np(flagset_d: acl_flagset_t, flag: acl_flag_t) -> ::std::os::raw::c_int; 192 | } 193 | extern "C" { 194 | pub fn acl_get_flag_np(flagset_d: acl_flagset_t, flag: acl_flag_t) -> ::std::os::raw::c_int; 195 | } 196 | extern "C" { 197 | pub fn acl_get_flagset_np( 198 | obj_p: *mut ::std::os::raw::c_void, 199 | flagset_p: *mut acl_flagset_t, 200 | ) -> ::std::os::raw::c_int; 201 | } 202 | extern "C" { 203 | pub fn acl_set_flagset_np( 204 | obj_p: *mut ::std::os::raw::c_void, 205 | flagset_d: acl_flagset_t, 206 | ) -> ::std::os::raw::c_int; 207 | } 208 | extern "C" { 209 | pub fn acl_get_qualifier(entry_d: acl_entry_t) -> *mut ::std::os::raw::c_void; 210 | } 211 | extern "C" { 212 | pub fn acl_get_tag_type( 213 | entry_d: acl_entry_t, 214 | tag_type_p: *mut acl_tag_t, 215 | ) -> ::std::os::raw::c_int; 216 | } 217 | extern "C" { 218 | pub fn acl_set_qualifier( 219 | entry_d: acl_entry_t, 220 | tag_qualifier_p: *const ::std::os::raw::c_void, 221 | ) -> ::std::os::raw::c_int; 222 | } 223 | extern "C" { 224 | pub fn acl_set_tag_type(entry_d: acl_entry_t, tag_type: acl_tag_t) -> ::std::os::raw::c_int; 225 | } 226 | extern "C" { 227 | pub fn acl_delete_def_file(path_p: *const ::std::os::raw::c_char) -> ::std::os::raw::c_int; 228 | } 229 | extern "C" { 230 | pub fn acl_get_fd(fd: ::std::os::raw::c_int) -> acl_t; 231 | } 232 | extern "C" { 233 | pub fn acl_get_fd_np(fd: ::std::os::raw::c_int, type_: acl_type_t) -> acl_t; 234 | } 235 | extern "C" { 236 | pub fn acl_get_file(path_p: *const ::std::os::raw::c_char, type_: acl_type_t) -> acl_t; 237 | } 238 | extern "C" { 239 | pub fn acl_get_link_np(path_p: *const ::std::os::raw::c_char, type_: acl_type_t) -> acl_t; 240 | } 241 | extern "C" { 242 | pub fn acl_set_fd(fd: ::std::os::raw::c_int, acl: acl_t) -> ::std::os::raw::c_int; 243 | } 244 | extern "C" { 245 | pub fn acl_set_fd_np( 246 | fd: ::std::os::raw::c_int, 247 | acl: acl_t, 248 | acl_type: acl_type_t, 249 | ) -> ::std::os::raw::c_int; 250 | } 251 | extern "C" { 252 | pub fn acl_set_file( 253 | path_p: *const ::std::os::raw::c_char, 254 | type_: acl_type_t, 255 | acl: acl_t, 256 | ) -> ::std::os::raw::c_int; 257 | } 258 | extern "C" { 259 | pub fn acl_set_link_np( 260 | path_p: *const ::std::os::raw::c_char, 261 | type_: acl_type_t, 262 | acl: acl_t, 263 | ) -> ::std::os::raw::c_int; 264 | } 265 | extern "C" { 266 | pub fn acl_copy_ext(buf_p: *mut ::std::os::raw::c_void, acl: acl_t, size: isize) -> isize; 267 | } 268 | extern "C" { 269 | pub fn acl_copy_ext_native( 270 | buf_p: *mut ::std::os::raw::c_void, 271 | acl: acl_t, 272 | size: isize, 273 | ) -> isize; 274 | } 275 | extern "C" { 276 | pub fn acl_copy_int(buf_p: *const ::std::os::raw::c_void) -> acl_t; 277 | } 278 | extern "C" { 279 | pub fn acl_copy_int_native(buf_p: *const ::std::os::raw::c_void) -> acl_t; 280 | } 281 | extern "C" { 282 | pub fn acl_from_text(buf_p: *const ::std::os::raw::c_char) -> acl_t; 283 | } 284 | extern "C" { 285 | pub fn acl_size(acl: acl_t) -> isize; 286 | } 287 | extern "C" { 288 | pub fn acl_to_text(acl: acl_t, len_p: *mut isize) -> *mut ::std::os::raw::c_char; 289 | } 290 | extern "C" { 291 | pub fn open( 292 | arg1: *const ::std::os::raw::c_char, 293 | arg2: ::std::os::raw::c_int, 294 | ... 295 | ) -> ::std::os::raw::c_int; 296 | } 297 | pub type uuid_t = __darwin_uuid_t; 298 | extern "C" { 299 | pub fn mbr_uid_to_uuid(uid: uid_t, uu: *mut ::std::os::raw::c_uchar) -> ::std::os::raw::c_int; 300 | } 301 | extern "C" { 302 | pub fn mbr_gid_to_uuid(gid: gid_t, uu: *mut ::std::os::raw::c_uchar) -> ::std::os::raw::c_int; 303 | } 304 | extern "C" { 305 | pub fn mbr_uuid_to_id( 306 | uu: *mut ::std::os::raw::c_uchar, 307 | uid_or_gid: *mut id_t, 308 | id_type: *mut ::std::os::raw::c_int, 309 | ) -> ::std::os::raw::c_int; 310 | } 311 | #[repr(C)] 312 | #[derive(Debug, Copy, Clone)] 313 | pub struct group { 314 | pub gr_name: *mut ::std::os::raw::c_char, 315 | pub gr_passwd: *mut ::std::os::raw::c_char, 316 | pub gr_gid: gid_t, 317 | pub gr_mem: *mut *mut ::std::os::raw::c_char, 318 | } 319 | extern "C" { 320 | pub fn getgrgid_r( 321 | arg1: gid_t, 322 | arg2: *mut group, 323 | arg3: *mut ::std::os::raw::c_char, 324 | arg4: usize, 325 | arg5: *mut *mut group, 326 | ) -> ::std::os::raw::c_int; 327 | } 328 | extern "C" { 329 | pub fn getgrnam_r( 330 | arg1: *const ::std::os::raw::c_char, 331 | arg2: *mut group, 332 | arg3: *mut ::std::os::raw::c_char, 333 | arg4: usize, 334 | arg5: *mut *mut group, 335 | ) -> ::std::os::raw::c_int; 336 | } 337 | #[repr(C)] 338 | #[derive(Debug, Copy, Clone)] 339 | pub struct passwd { 340 | pub pw_name: *mut ::std::os::raw::c_char, 341 | pub pw_passwd: *mut ::std::os::raw::c_char, 342 | pub pw_uid: uid_t, 343 | pub pw_gid: gid_t, 344 | pub pw_change: __darwin_time_t, 345 | pub pw_class: *mut ::std::os::raw::c_char, 346 | pub pw_gecos: *mut ::std::os::raw::c_char, 347 | pub pw_dir: *mut ::std::os::raw::c_char, 348 | pub pw_shell: *mut ::std::os::raw::c_char, 349 | pub pw_expire: __darwin_time_t, 350 | } 351 | extern "C" { 352 | pub fn getpwuid_r( 353 | arg1: uid_t, 354 | arg2: *mut passwd, 355 | arg3: *mut ::std::os::raw::c_char, 356 | arg4: usize, 357 | arg5: *mut *mut passwd, 358 | ) -> ::std::os::raw::c_int; 359 | } 360 | extern "C" { 361 | pub fn getpwnam_r( 362 | arg1: *const ::std::os::raw::c_char, 363 | arg2: *mut passwd, 364 | arg3: *mut ::std::os::raw::c_char, 365 | arg4: usize, 366 | arg5: *mut *mut passwd, 367 | ) -> ::std::os::raw::c_int; 368 | } 369 | extern "C" { 370 | pub fn close(arg1: ::std::os::raw::c_int) -> ::std::os::raw::c_int; 371 | } 372 | -------------------------------------------------------------------------------- /bindgen/wrapper.h: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #if __APPLE__ 6 | // MacOS makes us translate between GUID and UID/GID. 7 | # include 8 | #elif __linux__ 9 | // Linux supplies non-standard ACL extensions in a different header. 10 | # include 11 | #endif 12 | #include 13 | #include 14 | #include 15 | -------------------------------------------------------------------------------- /build.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | use std::path::Path; 3 | 4 | #[cfg(feature = "buildtime_bindgen")] 5 | const BINDGEN_FAILURE_MSG: &str = r#"Could not generate bindings. 6 | 7 | On Linux, the 'sys/acl.h' file is installed by the `libacl1-dev` package. To 8 | install this package, please use `apt-get install libacl1-dev`. 9 | 10 | If you still have problems, please create a GitHub issue at: 11 | https://github.com/byllyfish/exacl/issues 12 | 13 | "#; 14 | 15 | fn main() { 16 | let out_dir = env::var("OUT_DIR").unwrap(); 17 | let out_path = Path::new(&out_dir).join("bindings.rs"); 18 | let wrapper = "bindgen/wrapper.h"; 19 | 20 | // Tell cargo to tell rustc to link libacl.so, only on Linux. 21 | #[cfg(target_os = "linux")] 22 | println!("cargo:rustc-link-lib=acl"); 23 | 24 | // Tell cargo to invalidate the built crate whenever the wrapper changes 25 | println!("cargo:rerun-if-changed={wrapper}"); 26 | 27 | #[cfg(feature = "buildtime_bindgen")] 28 | bindgen_bindings(wrapper, &out_path); 29 | 30 | #[cfg(not(feature = "buildtime_bindgen"))] 31 | prebuilt_bindings(&out_path); 32 | } 33 | 34 | #[cfg(feature = "buildtime_bindgen")] 35 | fn bindgen_bindings(wrapper: &str, out_path: &Path) { 36 | // Build bindings for "wrapper.h". Tell cargo to invalidate the built 37 | // crate when any included header file changes. 38 | let mut builder = bindgen::Builder::default() 39 | .header(wrapper) 40 | .parse_callbacks(Box::new(bindgen::CargoCallbacks::new())) 41 | .disable_header_comment() 42 | .layout_tests(false); // no layout tests for passwd/group structs. 43 | 44 | if cfg!(target_os = "macos") { 45 | // Pass output of `xcrun --sdk macosx --show-sdk-path`. 46 | builder = builder.clang_arg("-isysroot/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk"); 47 | } 48 | 49 | // Specify the types, functions, and constants we want to include. 50 | let types = ["acl_.*", "uid_t", "gid_t"]; 51 | let funcs = [ 52 | "acl_.*", 53 | "getpw(nam|uid)_r", 54 | "getgr(nam|gid)_r", 55 | "mbr_uid_to_uuid", 56 | "mbr_gid_to_uuid", 57 | "mbr_uuid_to_id", 58 | #[cfg(target_os = "macos")] 59 | "open", 60 | #[cfg(target_os = "macos")] 61 | "close", 62 | #[cfg(target_os = "freebsd")] 63 | "pathconf", 64 | #[cfg(target_os = "freebsd")] 65 | "lpathconf", 66 | ]; 67 | let vars = [ 68 | "ACL_.*", 69 | ".*_ACL_NFS4", 70 | "ENOENT", 71 | "ENOTSUP", 72 | "EINVAL", 73 | "ENOMEM", 74 | "ERANGE", 75 | #[cfg(target_os = "macos")] 76 | "O_SYMLINK", 77 | "ID_TYPE_UID", 78 | "ID_TYPE_GID", 79 | ]; 80 | 81 | for type_ in &types { 82 | builder = builder.allowlist_type(type_); 83 | } 84 | 85 | for func_ in &funcs { 86 | builder = builder.allowlist_function(func_); 87 | } 88 | 89 | for var_ in &vars { 90 | builder = builder.allowlist_var(var_); 91 | } 92 | 93 | // Generate the bindings. 94 | let bindings = builder.generate().expect(BINDGEN_FAILURE_MSG); 95 | 96 | // Write the bindings. 97 | bindings 98 | .write_to_file(out_path) 99 | .expect("Couldn't write bindings!"); 100 | } 101 | 102 | #[cfg(not(feature = "buildtime_bindgen"))] 103 | fn prebuilt_bindings(out_path: &Path) { 104 | let target = env::var("CARGO_CFG_TARGET_OS").unwrap(); 105 | 106 | // Untrusted input check. 107 | match target.as_str() { 108 | "macos" | "linux" | "freebsd" => (), 109 | s => panic!("Unsupported target OS: {}", s), 110 | }; 111 | 112 | let bindings_path = format!("bindgen/bindings_{target}.rs"); 113 | if let Err(err) = std::fs::copy(&bindings_path, out_path) { 114 | panic!("Can't copy {:?} to {:?}: {}", bindings_path, out_path, err); 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /ci/bindgen.sh: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | 3 | # Test the `buildtime_bindgen` feature. 4 | 5 | set -eu 6 | 7 | # Build using bindgen. 8 | cargo clean 9 | cargo build --features buildtime_bindgen 10 | 11 | # Set $target to current OS name. 12 | os="$(uname -s)" 13 | case "$os" in 14 | "Darwin") 15 | target="macos" 16 | ;; 17 | "Linux") 18 | target="linux" 19 | ;; 20 | "FreeBSD") 21 | target="freebsd" 22 | ;; 23 | *) 24 | echo "Unknown OS: $os" 25 | exit 1 26 | ;; 27 | esac 28 | 29 | # Test that generated bindings match the prebuilt version. 30 | prebuilt_bindings="./bindgen/bindings_$target.rs" 31 | bindings=$(find ./target/debug/build -name "bindings.rs") 32 | 33 | echo "Comparing $bindings and $prebuilt_bindings" 34 | 35 | diff_out="$(mktemp)" 36 | trap '{ rm -f -- "$diff_out"; }' EXIT 37 | 38 | if diff "$bindings" "$prebuilt_bindings" >"$diff_out"; then 39 | echo "Success." 40 | rm "$diff_out" 41 | exit 0 42 | fi 43 | 44 | echo "Differences exist." 45 | 46 | # FreeBSD 14 includes several additional ACL API's that are not used. 47 | # Check the diff output against the approved diff output. 48 | freebsd_diff="./bindgen/bindings_freebsd14.diff" 49 | 50 | if [ "$target" = "freebsd" ]; then 51 | echo "Comparing diff output ($diff_out) and $freebsd_diff" 52 | diff "$diff_out" "$freebsd_diff" 53 | echo "Success." 54 | exit 0 55 | else 56 | cat "$diff_out" 57 | fi 58 | 59 | exit 1 60 | -------------------------------------------------------------------------------- /ci/coverage.sh: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | 3 | # Script to analyze Rust code using grcov. 4 | # 5 | # Usage: ./code_coverage.sh [open|codecov] 6 | 7 | set -e 8 | 9 | arg1="$1" 10 | os=$(uname -s | tr '[:upper:]' '[:lower:]') 11 | 12 | # To use nightly rust: NIGHTLY="+nightly" 13 | NIGHTLY="" 14 | 15 | if [ -n "$NIGHTLY" ]; then 16 | rustup install nightly 17 | fi 18 | 19 | # Set up grcov. 20 | rustup component add llvm-tools-preview 21 | export RUSTFLAGS="-Cinstrument-coverage" 22 | export LLVM_PROFILE_FILE="exacl-%p-%m.profraw" 23 | cargo $NIGHTLY install grcov 24 | 25 | # Build & Test 26 | cargo $NIGHTLY clean 27 | cargo $NIGHTLY test --features serde 28 | cargo $NIGHTLY build --features serde 29 | ./tests/run_tests.sh 30 | 31 | if [ "$arg1" = "open" ]; then 32 | echo "Producing HTML Report locally" 33 | grcov . --binary-path ./target/debug/ -s . -t html --branch --ignore-not-existing --ignore "/*" -o ./target/debug/coverage/ 34 | open target/debug/coverage/src/index.html 35 | elif [ "$arg1" = "codecov" ]; then 36 | echo "Producing lcov report and uploading it to codecov.io" 37 | grcov . --binary-path ./target/debug/ -s . -t lcov --branch --ignore-not-existing --ignore "/*" -o lcov.info 38 | bash <(curl -s https://codecov.io/bash) -f lcov.info -n "$os" 39 | fi 40 | 41 | exit 0 42 | -------------------------------------------------------------------------------- /ci/docs.sh: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | 3 | # Script to build rust docs. 4 | # 5 | # Usage: ./ci/docs.sh [open] 6 | 7 | set -e 8 | 9 | arg1="$1" 10 | 11 | # Install Rust nightly. 12 | rustup install nightly 13 | 14 | export RUSTDOCFLAGS='--cfg docsrs' 15 | export DOCS_RS=1 16 | 17 | if [ "$arg1" = "open" ]; then 18 | cargo +nightly doc --no-deps --open 19 | else 20 | cargo +nightly doc --no-deps 21 | 22 | # Add an index.html file that redirects to our main page. 23 | if [ ! -f "target/doc/index.html" ]; then 24 | echo '' >"target/doc/index.html" 25 | fi 26 | fi 27 | 28 | exit 0 29 | -------------------------------------------------------------------------------- /ci/format.sh: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | 3 | # Script to check code formatting. 4 | 5 | set -eu 6 | 7 | # Check rust formatting. 8 | cargo fmt -- --check 9 | 10 | # Check shell script formatting. 11 | if command -v shfmt &>/dev/null; then 12 | shfmt -i 4 -d ci/*.sh tests/*.sh 13 | else 14 | echo "shfmt is not installed." 15 | fi 16 | 17 | exit 0 18 | -------------------------------------------------------------------------------- /ci/lint.sh: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | 3 | # Script to run lint checks. 4 | 5 | set -eu 6 | 7 | # Space-separated list of ignored clippy lints. 8 | IGNORE="similar-names wildcard_imports use_self module_name_repetitions needless_raw_string_hashes" 9 | 10 | allow="" 11 | for name in $IGNORE; do 12 | allow="$allow -A clippy::$name" 13 | done 14 | 15 | # Check rust code with clippy. 16 | rustup component add clippy 17 | cargo clippy --version 18 | cargo clean 19 | cargo clippy --all-targets --all-features -- -D clippy::all -W clippy::pedantic -W clippy::cargo -W clippy::nursery $allow 20 | 21 | # Check bash scripts with shellcheck. 22 | shellcheck --version || exit 0 23 | SHELLCHECK_OPTS="-e SC2086" shellcheck ci/*.sh tests/*.sh 24 | 25 | exit 0 26 | -------------------------------------------------------------------------------- /examples/exacl.rs: -------------------------------------------------------------------------------- 1 | //! Program to get/set extended ACL's. 2 | //! 3 | //! To read an ACL from myfile and write it to stdout as JSON: 4 | //! exacl myfile 5 | //! 6 | //! To set the ACL for myfile from JSON passed via stdin (complete replacement): 7 | //! exacl --set myfile 8 | //! 9 | //! To get/set the ACL of a symlink itself, instead of the file it points to, 10 | //! use the -s option. 11 | //! 12 | //! To get/set the default ACL (on Linux), use the -d option. 13 | 14 | use exacl::{getfacl, setfacl, AclEntry, AclOption}; 15 | use std::io; 16 | use std::path::{Path, PathBuf}; 17 | use std::process; 18 | 19 | use clap::Parser; 20 | 21 | #[derive(clap::Parser)] 22 | #[command(name = "exacl", about = "Read or write a file's ACL.")] 23 | #[allow(clippy::struct_excessive_bools)] 24 | struct Opt { 25 | /// Set file's ACL. 26 | #[arg(long)] 27 | set: bool, 28 | 29 | /// Get or set the access ACL. 30 | #[arg(short = 'a', long)] 31 | access: bool, 32 | 33 | /// Get or set the default ACL. 34 | #[arg(short = 'd', long)] 35 | default: bool, 36 | 37 | /// Get or set the ACL of a symlink itself. 38 | #[arg(short = 's', long)] 39 | symlink: bool, 40 | 41 | /// Format of input or output. 42 | #[arg(value_enum, short = 'f', long, default_value = "json")] 43 | format: Format, 44 | 45 | /// Input files 46 | #[arg()] 47 | files: Vec, 48 | } 49 | 50 | #[derive(Copy, Clone, Debug, clap::ValueEnum)] 51 | #[value(rename_all = "lower")] 52 | enum Format { 53 | Json, 54 | Std, 55 | } 56 | 57 | const EXIT_SUCCESS: i32 = 0; 58 | const EXIT_FAILURE: i32 = 1; 59 | 60 | fn main() { 61 | env_logger::init(); 62 | 63 | let opt = Opt::parse(); 64 | 65 | let mut options = AclOption::empty(); 66 | if opt.access { 67 | options |= AclOption::ACCESS_ACL; 68 | } 69 | if opt.default { 70 | options |= AclOption::DEFAULT_ACL; 71 | } 72 | if opt.symlink { 73 | options |= AclOption::SYMLINK_ACL; 74 | } 75 | 76 | let exit_code = if opt.set { 77 | set_acl(&opt.files, options, opt.format) 78 | } else { 79 | get_acl(&opt.files, options, opt.format) 80 | }; 81 | 82 | process::exit(exit_code); 83 | } 84 | 85 | fn get_acl(paths: &[PathBuf], options: AclOption, format: Format) -> i32 { 86 | for path in paths { 87 | if let Err(err) = dump_acl(path, options, format) { 88 | eprintln!("{err}"); 89 | return EXIT_FAILURE; 90 | } 91 | } 92 | 93 | EXIT_SUCCESS 94 | } 95 | 96 | fn set_acl(paths: &[PathBuf], options: AclOption, format: Format) -> i32 { 97 | let Some(entries) = read_input(format) else { 98 | return EXIT_FAILURE; 99 | }; 100 | 101 | if let Err(err) = setfacl(paths, &entries, options) { 102 | eprintln!("{err}"); 103 | return EXIT_FAILURE; 104 | } 105 | 106 | EXIT_SUCCESS 107 | } 108 | 109 | fn dump_acl(path: &Path, options: AclOption, format: Format) -> io::Result<()> { 110 | let entries = getfacl(path, options)?; 111 | 112 | match format { 113 | #[cfg(feature = "serde")] 114 | Format::Json => { 115 | serde_json::to_writer(io::stdout(), &entries)?; 116 | println!(); // add newline 117 | } 118 | #[cfg(not(feature = "serde"))] 119 | Format::Json => { 120 | panic!("serde not supported"); 121 | } 122 | Format::Std => exacl::to_writer(io::stdout(), &entries)?, 123 | }; 124 | 125 | Ok(()) 126 | } 127 | 128 | fn read_input(format: Format) -> Option> { 129 | let reader = io::BufReader::new(io::stdin()); 130 | 131 | let entries: Vec = match format { 132 | // Read JSON format. 133 | #[cfg(feature = "serde")] 134 | Format::Json => match serde_json::from_reader(reader) { 135 | Ok(entries) => entries, 136 | Err(err) => { 137 | eprintln!("JSON parser error: {err}"); 138 | return None; 139 | } 140 | }, 141 | #[cfg(not(feature = "serde"))] 142 | Format::Json => { 143 | panic!("serde not supported"); 144 | } 145 | // Read Std format. 146 | Format::Std => match exacl::from_reader(reader) { 147 | Ok(entries) => entries, 148 | Err(err) => { 149 | eprintln!("Std parser error: {err}"); 150 | return None; 151 | } 152 | }, 153 | }; 154 | 155 | Some(entries) 156 | } 157 | -------------------------------------------------------------------------------- /src/bindings.rs: -------------------------------------------------------------------------------- 1 | //! Rust bindings to system C API; exported via `sys`. 2 | 3 | #![allow( 4 | dead_code, 5 | non_camel_case_types, 6 | non_upper_case_globals, 7 | clippy::unseparated_literal_suffix, 8 | clippy::unreadable_literal, 9 | deref_nullptr, // https://github.com/rust-lang/rust-bindgen/issues/1651 10 | clippy::too_many_lines, 11 | clippy::borrow_as_ptr, 12 | clippy::struct_field_names 13 | )] 14 | 15 | include!(concat!(env!("OUT_DIR"), "/bindings.rs")); 16 | -------------------------------------------------------------------------------- /src/bititer.rs: -------------------------------------------------------------------------------- 1 | //! Implements a generic bit iterator. 2 | //! 3 | //! Works with built-in integer types or bitflags. You just have to implement 4 | //! the `BitIterable` trait. 5 | 6 | use std::ops::BitXorAssign; 7 | 8 | pub trait BitIterable: Copy + BitXorAssign { 9 | fn lsb(self) -> Option; 10 | fn msb(self) -> Option; 11 | } 12 | 13 | pub struct BitIter(pub T); 14 | 15 | impl Iterator for BitIter { 16 | type Item = T; 17 | 18 | fn next(&mut self) -> Option { 19 | self.0.lsb().map(|bit| { 20 | self.0 ^= bit; 21 | bit 22 | }) 23 | } 24 | } 25 | 26 | impl DoubleEndedIterator for BitIter { 27 | fn next_back(&mut self) -> Option { 28 | self.0.msb().map(|bit| { 29 | self.0 ^= bit; 30 | bit 31 | }) 32 | } 33 | } 34 | 35 | //////////////////////////////////////////////////////////////////////////////// 36 | 37 | #[cfg(test)] 38 | mod bititer_tests { 39 | #![allow(clippy::unreadable_literal)] 40 | 41 | use super::*; 42 | use bitflags::bitflags; 43 | 44 | impl BitIterable for u32 { 45 | fn lsb(self) -> Option { 46 | if self == 0 { 47 | return None; 48 | } 49 | Some(1 << self.trailing_zeros()) 50 | } 51 | 52 | fn msb(self) -> Option { 53 | if self == 0 { 54 | return None; 55 | } 56 | Some(1 << (31 - self.leading_zeros())) 57 | } 58 | } 59 | 60 | #[test] 61 | fn test_bititer_u32() { 62 | assert!(BitIter(0).next().is_none()); 63 | 64 | let v = BitIter(1).collect::>(); 65 | assert_eq!(v, vec![1]); 66 | 67 | let v = BitIter(1 << 31).collect::>(); 68 | assert_eq!(v, vec![1 << 31]); 69 | 70 | let v = BitIter(2 + 4 + 16 + 64).collect::>(); 71 | assert_eq!(v, vec![2, 4, 16, 64]); 72 | 73 | let v = BitIter(u32::MAX).collect::>(); 74 | assert_eq!(v.len(), 32); 75 | assert_eq!( 76 | v, 77 | vec![ 78 | 1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096, 8192, 16384, 32768, 65536, 79 | 131072, 262144, 524288, 1048576, 2097152, 4194304, 8388608, 16777216, 33554432, 80 | 67108864, 134217728, 268435456, 536870912, 1073741824, 2147483648 81 | ] 82 | ); 83 | } 84 | 85 | #[test] 86 | fn test_bititer_u32_rev() { 87 | assert!(BitIter(0).next_back().is_none()); 88 | 89 | let v = BitIter(1).rev().collect::>(); 90 | assert_eq!(v, vec![1]); 91 | 92 | let v = BitIter(1 << 31).rev().collect::>(); 93 | assert_eq!(v, vec![1 << 31]); 94 | 95 | let v = BitIter(2 + 4 + 16 + 64).rev().collect::>(); 96 | assert_eq!(v, vec![64, 16, 4, 2]); 97 | 98 | let v = BitIter(u32::MAX).rev().collect::>(); 99 | assert_eq!(v.len(), 32); 100 | assert_eq!( 101 | v, 102 | vec![ 103 | 2147483648, 1073741824, 536870912, 268435456, 134217728, 67108864, 33554432, 104 | 16777216, 8388608, 4194304, 2097152, 1048576, 524288, 262144, 131072, 65536, 32768, 105 | 16384, 8192, 4096, 2048, 1024, 512, 256, 128, 64, 32, 16, 8, 4, 2, 1 106 | ] 107 | ); 108 | } 109 | 110 | bitflags! { 111 | #[derive(Copy, Clone, Debug, Default, PartialEq)] 112 | struct TestBit: u32 { 113 | const BIT1 = 1 << 0; 114 | const BIT2 = 1 << 1; 115 | const BIT3 = 1 << 5; 116 | } 117 | } 118 | 119 | impl BitIterable for TestBit { 120 | fn lsb(self) -> Option { 121 | if self.is_empty() { 122 | return None; 123 | } 124 | let low_bit = 1 << self.bits().trailing_zeros(); 125 | Some(TestBit::from_bits_retain(low_bit)) 126 | } 127 | 128 | fn msb(self) -> Option { 129 | #[allow(clippy::cast_possible_truncation)] 130 | const MAX_BITS: u32 = 8 * std::mem::size_of::() as u32 - 1; 131 | if self.is_empty() { 132 | return None; 133 | } 134 | let high_bit = 1 << (MAX_BITS - self.bits().leading_zeros()); 135 | Some(TestBit::from_bits_retain(high_bit)) 136 | } 137 | } 138 | 139 | #[test] 140 | fn test_bititer_bitflags() { 141 | let bits = TestBit::BIT1 | TestBit::BIT2 | TestBit::BIT3; 142 | 143 | let v = BitIter(TestBit::empty()).collect::>(); 144 | assert_eq!(v, vec![]); 145 | 146 | let v = BitIter(bits.bits()).collect::>(); 147 | assert_eq!(v, vec![1, 2, 32]); 148 | 149 | let v = BitIter(bits).collect::>(); 150 | assert_eq!( 151 | v, 152 | vec![ 153 | TestBit::from_bits_retain(1), 154 | TestBit::from_bits_retain(2), 155 | TestBit::from_bits_retain(32) 156 | ] 157 | ); 158 | } 159 | 160 | #[test] 161 | fn test_bititer_bitflags_rev() { 162 | let bits = TestBit::BIT1 | TestBit::BIT2 | TestBit::BIT3; 163 | 164 | let v = BitIter(TestBit::empty()).rev().collect::>(); 165 | assert_eq!(v, vec![]); 166 | 167 | let v = BitIter(bits.bits()).rev().collect::>(); 168 | assert_eq!(v, vec![32, 2, 1]); 169 | 170 | let v = BitIter(bits).rev().collect::>(); 171 | assert_eq!( 172 | v, 173 | vec![ 174 | TestBit::from_bits_retain(32), 175 | TestBit::from_bits_retain(2), 176 | TestBit::from_bits_retain(1) 177 | ] 178 | ); 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /src/failx.rs: -------------------------------------------------------------------------------- 1 | //! Error handling convenience functions. 2 | 3 | #![allow(dead_code)] 4 | 5 | use log::debug; 6 | use std::fmt; 7 | use std::io; 8 | use std::path::Path; 9 | 10 | /// Log a message and return an [`io::Error`] with the value of errno. 11 | pub fn log_err(ret: R, func: &str, arg: T) -> io::Error 12 | where 13 | R: fmt::Display, 14 | T: fmt::Debug, 15 | { 16 | let err = io::Error::last_os_error(); 17 | debug!("{}({:?}) returned {}, err={}", func, arg, ret, err); 18 | err 19 | } 20 | 21 | /// Log a message and return an [`io::Error`] for a given error code. 22 | pub fn log_from_err(ret: i32, func: &str, arg: T) -> io::Error 23 | where 24 | T: fmt::Debug, 25 | { 26 | assert!(ret > 0); 27 | let err = io::Error::from_raw_os_error(ret); 28 | debug!("{}({:?}) returned {}, err={}", func, arg, ret, err); 29 | err 30 | } 31 | 32 | /// Log a message and return an [`io::Result`] with the value of errno. 33 | pub fn fail_err(ret: R, func: &str, arg: T) -> io::Result 34 | where 35 | R: fmt::Display, 36 | T: fmt::Debug, 37 | { 38 | Err(log_err(ret, func, arg)) 39 | } 40 | 41 | /// Log a message and return an [`io::Result`] for a given error code. 42 | pub fn fail_from_err(ret: i32, func: &str, arg: T) -> io::Result 43 | where 44 | T: fmt::Debug, 45 | { 46 | Err(log_from_err(ret, func, arg)) 47 | } 48 | 49 | /// Return a custom [`io::Result`] with the given message. 50 | pub fn fail_custom(msg: &str) -> io::Result { 51 | Err(io::Error::new(io::ErrorKind::Other, msg)) 52 | } 53 | 54 | /// Return a custom [`io::Error`] that prefixes the given error. 55 | pub fn custom_err(msg: &str, err: &io::Error) -> io::Error { 56 | io::Error::new(err.kind(), format!("{msg}: {err}")) 57 | } 58 | 59 | /// Return a custom [`io::Error`] that prefixes the given error with filename. 60 | pub fn path_err(path: &Path, err: &io::Error) -> io::Error { 61 | io::Error::new(err.kind(), format!("File {path:?}: {err}")) 62 | } 63 | -------------------------------------------------------------------------------- /src/flag.rs: -------------------------------------------------------------------------------- 1 | //! Implements the inheritance flags. 2 | 3 | use crate::bititer::{BitIter, BitIterable}; 4 | use crate::format; 5 | use crate::sys::*; 6 | 7 | use bitflags::bitflags; 8 | #[cfg(feature = "serde")] 9 | use serde::{de, ser, Deserialize, Serialize}; 10 | use std::fmt; 11 | 12 | bitflags! { 13 | /// Represents ACL entry inheritance flags. 14 | #[derive(PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Clone, Copy, Default)] 15 | pub struct Flag : acl_flag_t { 16 | /// ACL entry was inherited. 17 | #[cfg(any(docsrs, target_os = "macos", target_os = "freebsd"))] 18 | #[cfg_attr(docsrs, doc(cfg(any(target_os = "macos", target_os = "freebsd"))))] 19 | const INHERITED = np::ACL_ENTRY_INHERITED; 20 | 21 | /// Inherit to files. 22 | #[cfg(any(docsrs, target_os = "macos", target_os = "freebsd"))] 23 | #[cfg_attr(docsrs, doc(cfg(any(target_os = "macos", target_os = "freebsd"))))] 24 | const FILE_INHERIT = np::ACL_ENTRY_FILE_INHERIT; 25 | 26 | /// Inherit to directories. 27 | #[cfg(any(docsrs, target_os = "macos", target_os = "freebsd"))] 28 | #[cfg_attr(docsrs, doc(cfg(any(target_os = "macos", target_os = "freebsd"))))] 29 | const DIRECTORY_INHERIT = np::ACL_ENTRY_DIRECTORY_INHERIT; 30 | 31 | /// Clear the DIRECTORY_INHERIT flag in the ACL entry that is inherited. 32 | #[cfg(any(docsrs, target_os = "macos", target_os = "freebsd"))] 33 | #[cfg_attr(docsrs, doc(cfg(any(target_os = "macos", target_os = "freebsd"))))] 34 | const LIMIT_INHERIT = np::ACL_ENTRY_LIMIT_INHERIT; 35 | 36 | /// Don't consider this entry when processing the ACL. Just inherit it. 37 | #[cfg(any(docsrs, target_os = "macos", target_os = "freebsd"))] 38 | #[cfg_attr(docsrs, doc(cfg(any(target_os = "macos", target_os = "freebsd"))))] 39 | const ONLY_INHERIT = np::ACL_ENTRY_ONLY_INHERIT; 40 | 41 | /// Specifies a default ACL entry on Linux. 42 | #[cfg(any(docsrs, target_os = "linux", target_os = "freebsd"))] 43 | #[cfg_attr(docsrs, doc(cfg(any(target_os = "linux", target_os = "freebsd"))))] 44 | const DEFAULT = 1 << 13; 45 | 46 | #[cfg(any(docsrs, target_os = "freebsd"))] 47 | #[cfg_attr(docsrs, doc(cfg(target_os = "freebsd")))] 48 | /// NFSv4 Specific Flags on FreeBSD. 49 | const NFS4_SPECIFIC = Self::INHERITED.bits() | Self::FILE_INHERIT.bits() | Self::DIRECTORY_INHERIT.bits() | Self::LIMIT_INHERIT.bits() | Self::ONLY_INHERIT.bits(); 50 | } 51 | } 52 | 53 | impl Flag { 54 | // On FreeBSD, acl_flag_t is a u16. On Linux and macOS, acl_flag_t is a u32. 55 | // To appease the linter, provide a helper function to cast Flag to u32. 56 | const fn as_u32(self) -> u32 { 57 | #[allow(clippy::unnecessary_cast)] 58 | return self.bits() as u32; 59 | } 60 | } 61 | 62 | impl BitIterable for Flag { 63 | fn lsb(self) -> Option { 64 | if self.is_empty() { 65 | return None; 66 | } 67 | let low_bit = 1u32 << self.bits().trailing_zeros(); 68 | Some(Flag::from_bits_retain(low_bit as acl_flag_t)) 69 | } 70 | 71 | fn msb(self) -> Option { 72 | // FIXME: Replace computation with `BITS` once it lands in stable. 73 | #[allow(clippy::cast_possible_truncation)] 74 | const MAX_BITS: u32 = 8 * std::mem::size_of::() as u32 - 1; 75 | 76 | if self.is_empty() { 77 | return None; 78 | } 79 | let high_bit = 1u32 << (MAX_BITS - self.bits().leading_zeros()); 80 | Some(Flag::from_bits_retain(high_bit as acl_flag_t)) 81 | } 82 | } 83 | 84 | #[derive(Copy, Clone, Debug, PartialEq, Eq)] 85 | #[cfg_attr(feature = "serde", derive(Deserialize, Serialize))] 86 | #[repr(u32)] 87 | #[allow(non_camel_case_types)] 88 | pub enum FlagName { 89 | // *N.B.* Update the corresponding table in format/format_no_serde.rs 90 | // if any of these entries change. 91 | #[cfg(any(target_os = "macos", target_os = "freebsd"))] 92 | inherited = Flag::INHERITED.as_u32(), 93 | 94 | #[cfg(any(target_os = "macos", target_os = "freebsd"))] 95 | file_inherit = Flag::FILE_INHERIT.as_u32(), 96 | 97 | #[cfg(any(target_os = "macos", target_os = "freebsd"))] 98 | directory_inherit = Flag::DIRECTORY_INHERIT.as_u32(), 99 | 100 | #[cfg(any(target_os = "macos", target_os = "freebsd"))] 101 | limit_inherit = Flag::LIMIT_INHERIT.as_u32(), 102 | 103 | #[cfg(any(target_os = "macos", target_os = "freebsd"))] 104 | only_inherit = Flag::ONLY_INHERIT.as_u32(), 105 | 106 | #[cfg(any(target_os = "linux", target_os = "freebsd"))] 107 | default = Flag::DEFAULT.as_u32(), 108 | } 109 | 110 | impl FlagName { 111 | const fn from_flag(flag: Flag) -> Option { 112 | match flag { 113 | #[cfg(any(target_os = "macos", target_os = "freebsd"))] 114 | Flag::INHERITED => Some(FlagName::inherited), 115 | 116 | #[cfg(any(target_os = "macos", target_os = "freebsd"))] 117 | Flag::FILE_INHERIT => Some(FlagName::file_inherit), 118 | 119 | #[cfg(any(target_os = "macos", target_os = "freebsd"))] 120 | Flag::DIRECTORY_INHERIT => Some(FlagName::directory_inherit), 121 | 122 | #[cfg(any(target_os = "macos", target_os = "freebsd"))] 123 | Flag::LIMIT_INHERIT => Some(FlagName::limit_inherit), 124 | 125 | #[cfg(any(target_os = "macos", target_os = "freebsd"))] 126 | Flag::ONLY_INHERIT => Some(FlagName::only_inherit), 127 | 128 | #[cfg(any(target_os = "linux", target_os = "freebsd"))] 129 | Flag::DEFAULT => Some(FlagName::default), 130 | 131 | _ => None, 132 | } 133 | } 134 | 135 | const fn to_flag(self) -> Flag { 136 | Flag::from_bits_retain(self as acl_flag_t) 137 | } 138 | } 139 | 140 | impl fmt::Display for FlagName { 141 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 142 | format::write_flagname(f, *self) 143 | } 144 | } 145 | 146 | impl fmt::Display for Flag { 147 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 148 | let mut iter = BitIter(*self & Flag::all()); 149 | 150 | if let Some(flag) = iter.next() { 151 | write!(f, "{}", FlagName::from_flag(flag).unwrap())?; 152 | 153 | for flag in iter { 154 | write!(f, ",{}", FlagName::from_flag(flag).unwrap())?; 155 | } 156 | } 157 | 158 | Ok(()) 159 | } 160 | } 161 | 162 | /// Parse an abbreviated flag ("d"). 163 | #[cfg(any(target_os = "linux", target_os = "freebsd"))] 164 | fn parse_flag_abbreviation(s: &str) -> Option { 165 | match s { 166 | "d" => Some(Flag::DEFAULT), 167 | _ => None, 168 | } 169 | } 170 | 171 | #[cfg(target_os = "macos")] 172 | const fn parse_flag_abbreviation(_s: &str) -> Option { 173 | None 174 | } 175 | 176 | impl std::str::FromStr for FlagName { 177 | type Err = format::Error; 178 | 179 | fn from_str(s: &str) -> Result { 180 | format::read_flagname(s) 181 | } 182 | } 183 | 184 | impl std::str::FromStr for Flag { 185 | type Err = format::Error; 186 | 187 | fn from_str(s: &str) -> Result { 188 | let mut result = Flag::empty(); 189 | 190 | for item in s.split(',') { 191 | let word = item.trim(); 192 | if !word.is_empty() { 193 | if let Some(flag) = parse_flag_abbreviation(word) { 194 | result |= flag; 195 | } else { 196 | result |= word.parse::()?.to_flag(); 197 | } 198 | } 199 | } 200 | 201 | Ok(result) 202 | } 203 | } 204 | 205 | #[cfg(feature = "serde")] 206 | impl ser::Serialize for Flag { 207 | fn serialize(&self, serializer: S) -> Result 208 | where 209 | S: ser::Serializer, 210 | { 211 | use ser::SerializeSeq; 212 | let mut seq = serializer.serialize_seq(None)?; 213 | 214 | for flag in BitIter(*self) { 215 | seq.serialize_element(&FlagName::from_flag(flag))?; 216 | } 217 | 218 | seq.end() 219 | } 220 | } 221 | 222 | #[cfg(feature = "serde")] 223 | impl<'de> de::Deserialize<'de> for Flag { 224 | fn deserialize(deserializer: D) -> Result 225 | where 226 | D: de::Deserializer<'de>, 227 | { 228 | struct FlagVisitor; 229 | 230 | impl<'de> de::Visitor<'de> for FlagVisitor { 231 | type Value = Flag; 232 | 233 | fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { 234 | formatter.write_str("list of flags") 235 | } 236 | 237 | fn visit_seq(self, mut seq: A) -> Result 238 | where 239 | A: de::SeqAccess<'de>, 240 | { 241 | let mut flags: Flag = Flag::empty(); 242 | 243 | while let Some(value) = seq.next_element()? { 244 | let name: FlagName = value; 245 | flags |= name.to_flag(); 246 | } 247 | 248 | Ok(flags) 249 | } 250 | } 251 | 252 | deserializer.deserialize_seq(FlagVisitor) 253 | } 254 | } 255 | 256 | //////////////////////////////////////////////////////////////////////////////// 257 | 258 | #[cfg(test)] 259 | mod flag_tests { 260 | use super::*; 261 | 262 | #[test] 263 | fn test_flag_display() { 264 | assert_eq!(Flag::empty().to_string(), ""); 265 | 266 | #[cfg(target_os = "macos")] 267 | { 268 | let flags = Flag::INHERITED | Flag::FILE_INHERIT; 269 | assert_eq!(flags.to_string(), "inherited,file_inherit"); 270 | 271 | let bad_flag = Flag::from_bits_retain(0x0080_0000) | Flag::INHERITED; 272 | assert_eq!(bad_flag.to_string(), "inherited"); 273 | 274 | assert_eq!( 275 | Flag::all().to_string(), 276 | "inherited,file_inherit,directory_inherit,limit_inherit,only_inherit" 277 | ); 278 | } 279 | 280 | #[cfg(target_os = "linux")] 281 | { 282 | let flags = Flag::DEFAULT; 283 | assert_eq!(flags.to_string(), "default"); 284 | 285 | let bad_flag = Flag::from_bits_retain(0x8000) | Flag::DEFAULT; 286 | assert_eq!(bad_flag.to_string(), "default"); 287 | 288 | assert_eq!(Flag::all().to_string(), "default"); 289 | } 290 | 291 | #[cfg(target_os = "freebsd")] 292 | { 293 | let flags = Flag::DEFAULT; 294 | assert_eq!(flags.to_string(), "default"); 295 | 296 | let bad_flag = Flag::from_bits_retain(0x8000) | Flag::DEFAULT; 297 | assert_eq!(bad_flag.to_string(), "default"); 298 | 299 | assert_eq!( 300 | Flag::all().to_string(), 301 | "file_inherit,directory_inherit,limit_inherit,only_inherit,inherited,default" 302 | ); 303 | } 304 | } 305 | 306 | #[test] 307 | fn test_flag_fromstr() { 308 | #[cfg(target_os = "macos")] 309 | { 310 | assert_eq!(Flag::empty(), "".parse::().unwrap()); 311 | 312 | let flags = Flag::INHERITED | Flag::FILE_INHERIT; 313 | assert_eq!(flags, "inherited,file_inherit".parse().unwrap()); 314 | 315 | assert_eq!( 316 | Flag::all(), 317 | "inherited,file_inherit,directory_inherit,limit_inherit,only_inherit" 318 | .parse() 319 | .unwrap() 320 | ); 321 | 322 | assert_eq!("unknown variant `bad_flag`, expected one of `inherited`, `file_inherit`, `directory_inherit`, `limit_inherit`, `only_inherit`", "bad_flag".parse::().unwrap_err().to_string()); 323 | } 324 | 325 | #[cfg(target_os = "linux")] 326 | { 327 | assert_eq!(Flag::empty(), "".parse::().unwrap()); 328 | 329 | assert_eq!(Flag::DEFAULT, "d".parse().unwrap()); 330 | assert_eq!(Flag::all(), "default".parse().unwrap()); 331 | 332 | assert_eq!( 333 | "unknown variant `bad_flag`, expected `default`", 334 | "bad_flag".parse::().unwrap_err().to_string() 335 | ); 336 | } 337 | 338 | #[cfg(target_os = "freebsd")] 339 | { 340 | assert_eq!(Flag::empty(), "".parse::().unwrap()); 341 | 342 | assert_eq!(Flag::DEFAULT, "d".parse().unwrap()); 343 | assert_eq!( 344 | Flag::all(), 345 | "default,inherited,file_inherit,directory_inherit,limit_inherit,only_inherit" 346 | .parse() 347 | .unwrap() 348 | ); 349 | 350 | assert_eq!( 351 | "unknown variant `bad_flag`, expected one of `inherited`, `file_inherit`, `directory_inherit`, `limit_inherit`, `only_inherit`, `default`", 352 | "bad_flag".parse::().unwrap_err().to_string() 353 | ); 354 | } 355 | } 356 | } 357 | -------------------------------------------------------------------------------- /src/format/format_no_serde.rs: -------------------------------------------------------------------------------- 1 | //! Implements helper functions for the built-in `AclEntry` format. 2 | //! These are used when `serde` is not available 3 | 4 | use std::fmt; 5 | use std::io; 6 | 7 | use crate::aclentry::AclEntryKind; 8 | use crate::flag::FlagName; 9 | use crate::perm::PermName; 10 | 11 | const ACLENTRYKINDS: &'static [(AclEntryKind, &'static str)] = &[ 12 | (AclEntryKind::User, "user"), 13 | (AclEntryKind::Group, "group"), 14 | #[cfg(any(target_os = "linux", target_os = "freebsd"))] 15 | (AclEntryKind::Mask, "mask"), 16 | #[cfg(any(target_os = "linux", target_os = "freebsd"))] 17 | (AclEntryKind::Other, "other"), 18 | #[cfg(target_os = "freebsd")] 19 | (AclEntryKind::Everyone, "everyone"), 20 | (AclEntryKind::Unknown, "unknown"), 21 | ]; 22 | 23 | const FLAGS: &'static [(FlagName, &'static str)] = &[ 24 | #[cfg(any(target_os = "macos", target_os = "freebsd"))] 25 | (FlagName::inherited, "inherited"), 26 | #[cfg(any(target_os = "macos", target_os = "freebsd"))] 27 | (FlagName::file_inherit, "file_inherit"), 28 | #[cfg(any(target_os = "macos", target_os = "freebsd"))] 29 | (FlagName::directory_inherit, "directory_inherit"), 30 | #[cfg(any(target_os = "macos", target_os = "freebsd"))] 31 | (FlagName::limit_inherit, "limit_inherit"), 32 | #[cfg(any(target_os = "macos", target_os = "freebsd"))] 33 | (FlagName::only_inherit, "only_inherit"), 34 | #[cfg(any(target_os = "linux", target_os = "freebsd"))] 35 | (FlagName::default, "default"), 36 | ]; 37 | 38 | const PERMS: &'static [(PermName, &'static str)] = &[ 39 | (PermName::read, "read"), 40 | (PermName::write, "write"), 41 | (PermName::execute, "execute"), 42 | #[cfg(target_os = "freebsd")] 43 | (PermName::read_data, "read_data"), 44 | #[cfg(target_os = "freebsd")] 45 | (PermName::write_data, "write_data"), 46 | #[cfg(any(target_os = "macos", target_os = "freebsd"))] 47 | (PermName::delete, "delete"), 48 | #[cfg(any(target_os = "macos", target_os = "freebsd"))] 49 | (PermName::append, "append"), 50 | #[cfg(any(target_os = "macos", target_os = "freebsd"))] 51 | (PermName::delete_child, "delete_child"), 52 | #[cfg(any(target_os = "macos", target_os = "freebsd"))] 53 | (PermName::readattr, "readattr"), 54 | #[cfg(any(target_os = "macos", target_os = "freebsd"))] 55 | (PermName::writeattr, "writeattr"), 56 | #[cfg(any(target_os = "macos", target_os = "freebsd"))] 57 | (PermName::readextattr, "readextattr"), 58 | #[cfg(any(target_os = "macos", target_os = "freebsd"))] 59 | (PermName::writeextattr, "writeextattr"), 60 | #[cfg(any(target_os = "macos", target_os = "freebsd"))] 61 | (PermName::readsecurity, "readsecurity"), 62 | #[cfg(any(target_os = "macos", target_os = "freebsd"))] 63 | (PermName::writesecurity, "writesecurity"), 64 | #[cfg(any(target_os = "macos", target_os = "freebsd"))] 65 | (PermName::chown, "chown"), 66 | #[cfg(any(target_os = "macos", target_os = "freebsd"))] 67 | (PermName::sync, "sync"), 68 | ]; 69 | 70 | /// Write value of an enum as a string using the given (enum, str) table. 71 | fn write_enum( 72 | f: &mut fmt::Formatter, 73 | value: T, 74 | table: &'static [(T, &'static str)], 75 | ) -> fmt::Result { 76 | match table.iter().find(|item| item.0 == value) { 77 | Some((_, name)) => write!(f, "{name}"), 78 | None => write!(f, "!!"), 79 | } 80 | } 81 | 82 | /// Read value of an enum from a string using the given (enum, str) table. 83 | fn read_enum(s: &str, table: &'static [(T, &'static str)]) -> Result { 84 | match table.iter().find(|item| item.1 == s) { 85 | Some((value, _)) => Ok(*value), 86 | None => Err(err_enum(s, table)), 87 | } 88 | } 89 | 90 | /// Produce the error message when the variant can't be found. 91 | fn err_enum(s: &str, table: &'static [(T, &'static str)]) -> Error { 92 | let variants = table 93 | .iter() 94 | .map(|item| format!("`{}`", item.1)) 95 | .collect::>() 96 | .join(", "); 97 | 98 | let msg = if table.len() == 1 { 99 | format!("unknown variant `{s}`, expected {variants}") 100 | } else { 101 | format!("unknown variant `{s}`, expected one of {variants}") 102 | }; 103 | 104 | Error::Message(msg) 105 | } 106 | 107 | /// Write value of an AclEntryKind. 108 | pub fn write_aclentrykind(f: &mut fmt::Formatter, value: AclEntryKind) -> fmt::Result { 109 | write_enum(f, value, ACLENTRYKINDS) 110 | } 111 | 112 | // Read value of an AclEntryKind. 113 | pub fn read_aclentrykind(s: &str) -> Result { 114 | read_enum(s, ACLENTRYKINDS) 115 | } 116 | 117 | /// Write value of a FlagName. 118 | pub fn write_flagname(f: &mut fmt::Formatter, value: FlagName) -> fmt::Result { 119 | write_enum(f, value, FLAGS) 120 | } 121 | 122 | // Read value of a FlagName. 123 | pub fn read_flagname(s: &str) -> Result { 124 | read_enum(s, FLAGS) 125 | } 126 | 127 | /// Write value of a PermName. 128 | pub fn write_permname(f: &mut fmt::Formatter, value: PermName) -> fmt::Result { 129 | write_enum(f, value, PERMS) 130 | } 131 | 132 | // Read value of a PermName. 133 | pub fn read_permname(s: &str) -> Result { 134 | read_enum(s, PERMS) 135 | } 136 | 137 | //////////////////////////////////////////////////////////////////////////////// 138 | 139 | #[derive(Clone, Debug, PartialEq)] 140 | pub enum Error { 141 | Message(String), 142 | NotImplemented, 143 | } 144 | 145 | impl From for io::Error { 146 | fn from(err: Error) -> Self { 147 | io::Error::new(io::ErrorKind::InvalidInput, err) 148 | } 149 | } 150 | 151 | impl fmt::Display for Error { 152 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 153 | match self { 154 | Error::Message(msg) => write!(f, "{msg}"), 155 | Error::NotImplemented => write!(f, "Not implemented"), 156 | } 157 | } 158 | } 159 | 160 | impl std::error::Error for Error {} 161 | 162 | type Result = std::result::Result; 163 | -------------------------------------------------------------------------------- /src/format/mod.rs: -------------------------------------------------------------------------------- 1 | //! Implements helper functions for the built-in `AclEntry` format. 2 | 3 | #[cfg(feature = "serde")] 4 | mod format_serde; 5 | 6 | #[cfg(not(feature = "serde"))] 7 | mod format_no_serde; 8 | 9 | #[cfg(feature = "serde")] 10 | pub use format_serde::{ 11 | read_aclentrykind, read_flagname, read_permname, write_aclentrykind, write_flagname, 12 | write_permname, Error, 13 | }; 14 | 15 | #[cfg(not(feature = "serde"))] 16 | pub use format_no_serde::{ 17 | read_aclentrykind, read_flagname, read_permname, write_aclentrykind, write_flagname, 18 | write_permname, Error, 19 | }; 20 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! # exacl 2 | //! 3 | //! Manipulate file system access control lists (ACL) on `macOS`, `Linux`, and 4 | //! `FreeBSD`. 5 | //! 6 | //! ## Example 7 | //! 8 | //! ```no_run 9 | //! # fn main() -> Result<(), Box> { 10 | //! use exacl::{getfacl, setfacl, AclEntry, Perm}; 11 | //! 12 | //! // Get the ACL from "./tmp/foo". 13 | //! let mut acl = getfacl("./tmp/foo", None)?; 14 | //! 15 | //! // Print the contents of the ACL. 16 | //! for entry in &acl { 17 | //! println!("{entry}"); 18 | //! } 19 | //! 20 | //! // Add an ACL entry to the end. 21 | //! acl.push(AclEntry::allow_user("some_user", Perm::READ, None)); 22 | //! 23 | //! // Set the ACL for "./tmp/foo". 24 | //! setfacl(&["./tmp/foo"], &acl, None)?; 25 | //! 26 | //! # Ok(()) } 27 | //! ``` 28 | //! 29 | //! ## API 30 | //! 31 | //! This module provides two high level functions, [`getfacl`] and [`setfacl`]. 32 | //! 33 | //! - [`getfacl`] retrieves the ACL for a file or directory. 34 | //! - [`setfacl`] sets the ACL for files or directories. 35 | //! 36 | //! On Linux and `FreeBSD`, the ACL contains entries for the default ACL, if 37 | //! present. 38 | //! 39 | //! Both [`getfacl`] and [`setfacl`] work with a `Vec`. The 40 | //! [`AclEntry`] structure contains five fields: 41 | //! 42 | //! - kind : [`AclEntryKind`] - the kind of entry (User, Group, Other, Mask, 43 | //! or Unknown). 44 | //! - name : [`String`] - name of the principal being given access. You can 45 | //! use a user/group name, decimal uid/gid, or UUID (on macOS). 46 | //! - perms : [`Perm`] - permission bits for the entry. 47 | //! - flags : [`Flag`] - flags indicating whether an entry is inherited, etc. 48 | //! - allow : [`bool`] - true if entry is allowed; false means deny. Linux only 49 | //! supports allow=true. 50 | 51 | #![warn(missing_docs)] 52 | #![cfg_attr(docsrs, feature(doc_cfg))] 53 | 54 | mod acl; 55 | mod aclentry; 56 | mod bindings; 57 | mod bititer; 58 | mod failx; 59 | mod flag; 60 | mod format; 61 | mod perm; 62 | mod qualifier; 63 | mod sys; 64 | mod unix; 65 | mod util; 66 | 67 | // Export AclOption, AclEntry, AclEntryKind, Flag and Perm. 68 | pub use acl::AclOption; 69 | pub use aclentry::{AclEntry, AclEntryKind}; 70 | pub use flag::Flag; 71 | pub use perm::Perm; 72 | 73 | use acl::Acl; 74 | use failx::custom_err; 75 | use std::io::{self, BufRead}; 76 | use std::path::Path; 77 | 78 | #[cfg(not(target_os = "macos"))] 79 | use failx::fail_custom; 80 | 81 | /// Get access control list (ACL) for a file or directory. 82 | /// 83 | /// On success, returns a vector of [`AclEntry`] with all access control entries 84 | /// for the specified path. The semantics and permissions of the access control 85 | /// list depend on the underlying platform. 86 | /// 87 | /// # macOS 88 | /// 89 | /// The ACL only includes the extended entries beyond the normal permission mode 90 | /// of the file. macOS provides several ACL entry flags to specify how entries 91 | /// may be inherited by directory sub-items. If there's no extended ACL for a 92 | /// file, this function may return zero entries. 93 | /// 94 | /// If `path` points to a symlink, `getfacl` returns the ACL of the file pointed 95 | /// to by the symlink. Use [`AclOption::SYMLINK_ACL`] to obtain the ACL of a symlink 96 | /// itself. 97 | /// 98 | /// [`AclOption::DEFAULT_ACL`] option is not supported on macOS. 99 | /// 100 | /// # Linux 101 | /// 102 | /// The ACL includes entries related to the permission mode of the file. These 103 | /// are marked with empty names (""). 104 | /// 105 | /// Both the access ACL and the default ACL are returned in one list, with 106 | /// the default ACL entries indicated by a [`Flag::DEFAULT`] flag. 107 | /// 108 | /// If `path` points to a symlink, `getfacl` returns the ACL of the file pointed 109 | /// to by the symlink. [`AclOption::SYMLINK_ACL`] is not supported on Linux. 110 | /// 111 | /// [`AclOption::DEFAULT_ACL`] causes `getfacl` to only include entries for the 112 | /// default ACL, if present for a directory path. When called with 113 | /// [`AclOption::DEFAULT_ACL`], `getfacl` may return zero entries. 114 | /// 115 | /// # Example 116 | /// 117 | /// ```no_run 118 | /// # fn main() -> Result<(), Box> { 119 | /// use exacl::getfacl; 120 | /// 121 | /// let entries = getfacl("./tmp/foo", None)?; 122 | /// # Ok(()) } 123 | /// ``` 124 | /// 125 | /// # Errors 126 | /// 127 | /// Returns an [`io::Error`] on failure. 128 | /// 129 | pub fn getfacl(path: P, options: O) -> io::Result> 130 | where 131 | P: AsRef, 132 | O: Into>, 133 | { 134 | _getfacl(path.as_ref(), options.into().unwrap_or_default()) 135 | } 136 | 137 | #[cfg(target_os = "macos")] 138 | fn _getfacl(path: &Path, options: AclOption) -> io::Result> { 139 | Acl::read(path, options)?.entries() 140 | } 141 | 142 | #[cfg(not(target_os = "macos"))] 143 | fn _getfacl(path: &Path, options: AclOption) -> io::Result> { 144 | if options.contains(AclOption::ACCESS_ACL | AclOption::DEFAULT_ACL) { 145 | fail_custom("ACCESS_ACL and DEFAULT_ACL are mutually exclusive options") 146 | } else if options.intersects(AclOption::ACCESS_ACL | AclOption::DEFAULT_ACL) { 147 | Acl::read(path, options)?.entries() 148 | } else { 149 | let acl = Acl::read(path, options)?; 150 | let mut entries = acl.entries()?; 151 | 152 | if acl.is_posix() { 153 | let mut default = Acl::read( 154 | path, 155 | options | AclOption::DEFAULT_ACL | AclOption::IGNORE_EXPECTED_FILE_ERR, 156 | )? 157 | .entries()?; 158 | 159 | entries.append(&mut default); 160 | } 161 | Ok(entries) 162 | } 163 | } 164 | 165 | /// Set access control list (ACL) for specified files and directories. 166 | /// 167 | /// Sets the ACL for the specified paths using the given access control entries. 168 | /// The semantics and permissions of the access control list depend on the 169 | /// underlying platform. 170 | /// 171 | /// # macOS 172 | /// 173 | /// The ACL contains extended entries beyond the usual mode permission bits. 174 | /// An entry may allow or deny access to a specific user or group. 175 | /// To specify inherited entries, use the provided [Flag] values. 176 | /// 177 | /// ### macOS Example 178 | /// 179 | /// ```ignore 180 | /// # fn main() -> Result<(), Box> { 181 | /// use exacl::{setfacl, AclEntry, Flag, Perm}; 182 | /// 183 | /// let entries = vec![ 184 | /// AclEntry::allow_user("some_user", Perm::READ | Perm::WRITE, None), 185 | /// AclEntry::deny_group("some_group", Perm::WRITE, None) 186 | /// ]; 187 | /// 188 | /// setfacl(&["./tmp/foo"], &entries, None)?; 189 | /// # Ok(()) } 190 | /// ``` 191 | /// 192 | /// # Linux 193 | /// 194 | /// Each entry can only allow access; denying access using allow=false is not 195 | /// supported on Linux. 196 | /// 197 | /// The ACL *must* contain entries for the permission modes of the file. Use 198 | /// the [`AclEntry::allow_other`] and [`AclEntry::allow_mask`] functions to 199 | /// specify the mode's other and mask permissions. Use "" as the name for the 200 | /// file owner and group owner. 201 | /// 202 | /// If an ACL contains a named user or group, there should be a 203 | /// [`AclEntryKind::Mask`] entry included. If a one entry is not provided, one 204 | /// will be computed. 205 | /// 206 | /// The access control entries may include entries for the default ACL, if one 207 | /// is desired. When `setfacl` is called with no [`Flag::DEFAULT`] entries, it 208 | /// deletes the default ACL. 209 | /// 210 | /// ### Linux Example 211 | /// 212 | /// ```ignore 213 | /// # fn main() -> Result<(), Box> { 214 | /// use exacl::{setfacl, AclEntry, Flag, Perm}; 215 | /// 216 | /// let entries = vec![ 217 | /// AclEntry::allow_user("", Perm::READ | Perm::WRITE, None), 218 | /// AclEntry::allow_group("", Perm::READ, None), 219 | /// AclEntry::allow_other(Perm::empty(), None), 220 | /// AclEntry::allow_user("some_user", Perm::READ | Perm::WRITE, None), 221 | /// ]; 222 | /// 223 | /// setfacl(&["./tmp/foo"], &entries, None)?; 224 | /// # Ok(()) } 225 | /// ``` 226 | /// 227 | /// # Errors 228 | /// 229 | /// Returns an [`io::Error`] on failure. 230 | /// 231 | pub fn setfacl(paths: &[P], entries: &[AclEntry], options: O) -> io::Result<()> 232 | where 233 | P: AsRef, 234 | O: Into>, 235 | { 236 | _setfacl(paths, entries, options.into().unwrap_or_default()) 237 | } 238 | 239 | #[cfg(target_os = "macos")] 240 | fn _setfacl

(paths: &[P], entries: &[AclEntry], options: AclOption) -> io::Result<()> 241 | where 242 | P: AsRef, 243 | { 244 | let acl = Acl::from_entries(entries).map_err(|err| custom_err("Invalid ACL", &err))?; 245 | for path in paths { 246 | acl.write(path.as_ref(), options)?; 247 | } 248 | 249 | Ok(()) 250 | } 251 | 252 | #[cfg(not(target_os = "macos"))] 253 | fn _setfacl

(paths: &[P], entries: &[AclEntry], options: AclOption) -> io::Result<()> 254 | where 255 | P: AsRef, 256 | { 257 | if options.contains(AclOption::ACCESS_ACL | AclOption::DEFAULT_ACL) { 258 | fail_custom("ACCESS_ACL and DEFAULT_ACL are mutually exclusive options")?; 259 | } else if options.intersects(AclOption::ACCESS_ACL | AclOption::DEFAULT_ACL) { 260 | let acl = Acl::from_entries(entries).map_err(|err| custom_err("Invalid ACL", &err))?; 261 | 262 | for path in paths { 263 | acl.write(path.as_ref(), options)?; 264 | } 265 | } else { 266 | let (access_acl, default_acl) = 267 | Acl::from_unified_entries(entries).map_err(|err| custom_err("Invalid ACL", &err))?; 268 | 269 | if access_acl.is_empty() { 270 | fail_custom("Invalid ACL: missing required entries")?; 271 | } 272 | 273 | for path in paths { 274 | let path = path.as_ref(); 275 | if access_acl.is_posix() { 276 | // Try to set default acl first. This will fail if path is not 277 | // a directory and default_acl is non-empty. This ordering 278 | // avoids leaving the file's ACL in a partially changed state 279 | // after an error (simply because it was a non-directory). 280 | default_acl.write( 281 | path, 282 | options | AclOption::DEFAULT_ACL | AclOption::IGNORE_EXPECTED_FILE_ERR, 283 | )?; 284 | } 285 | access_acl.write(path, options)?; 286 | } 287 | } 288 | 289 | Ok(()) 290 | } 291 | 292 | /// Write ACL entries to text. 293 | /// 294 | /// Each ACL entry is printed on a separate line. The five fields are separated 295 | /// by colons: 296 | /// 297 | /// ```text 298 | /// :::: 299 | /// 300 | /// - one of "allow" or "deny" 301 | /// - comma-separated list of flags 302 | /// - one of "user", "group", "other", "mask", "unknown" 303 | /// - user/group name (or decimal id if not known) 304 | /// - comma-separated list of permissions 305 | /// ``` 306 | /// 307 | /// Each record, including the last, is terminated by a final newline. 308 | /// 309 | /// # Sample Output 310 | /// 311 | /// ```text 312 | /// allow::group:admin:read,write 313 | /// ``` 314 | /// 315 | /// # Errors 316 | /// 317 | /// Returns an [`io::Error`] on failure. 318 | pub fn to_writer(mut writer: W, entries: &[AclEntry]) -> io::Result<()> { 319 | for entry in entries { 320 | writeln!(writer, "{entry}")?; 321 | } 322 | 323 | Ok(()) 324 | } 325 | 326 | /// Read ACL entries from text. 327 | /// 328 | /// Each ACL entry is presented on a separate line. A comment begins with `#` 329 | /// and proceeds to the end of the line. Within a field, leading or trailing 330 | /// white space are ignored. 331 | /// 332 | /// ```text 333 | /// Three allowed forms: 334 | /// 335 | /// :::: 336 | /// ::: 337 | /// :: 338 | /// 339 | /// - one of "allow" or "deny" 340 | /// - comma-separated list of flags 341 | /// - one of "user", "group", "other", "mask", "unknown" 342 | /// - user/group name (decimal id accepted) 343 | /// - comma-separated list of permissions 344 | /// ``` 345 | /// 346 | /// Supported flags and permissions vary by platform. 347 | /// 348 | /// Supported abbreviations: d = default, r = read, w = write, x = execute, 349 | /// u = user, g = group, o = other, m = mask 350 | /// 351 | /// # Sample Input 352 | /// 353 | /// ```text 354 | /// allow::group:admin:read,write 355 | /// g:admin:rw # ignored 356 | /// d:u:chip:rw 357 | /// deny:file_inherit:user:chet:rwx 358 | /// ``` 359 | /// 360 | /// # Errors 361 | /// 362 | /// Returns an [`io::Error`] on failure. 363 | pub fn from_reader(reader: R) -> io::Result> { 364 | let mut result = Vec::::new(); 365 | let buf = io::BufReader::new(reader); 366 | 367 | for line_result in buf.lines() { 368 | let line = line_result?; 369 | 370 | let src_line = trim_comment(&line).trim(); 371 | if !src_line.is_empty() { 372 | result.push(src_line.parse::()?); 373 | } 374 | } 375 | 376 | Ok(result) 377 | } 378 | 379 | /// Return line with end of line comment removed. 380 | fn trim_comment(line: &str) -> &str { 381 | line.find('#').map_or(line, |n| &line[0..n]) 382 | } 383 | 384 | /// Write ACL entries to text. 385 | /// 386 | /// See `to_writer` for the format. 387 | /// 388 | /// # Errors 389 | /// 390 | /// Returns an [`io::Error`] on failure. 391 | pub fn to_string(entries: &[AclEntry]) -> io::Result { 392 | let mut buf = Vec::::with_capacity(128); 393 | to_writer(&mut buf, entries)?; 394 | String::from_utf8(buf).map_err(|err| io::Error::new(io::ErrorKind::Other, err)) 395 | } 396 | 397 | /// Read ACL entries from text. 398 | /// 399 | /// See `from_reader` for the format. 400 | /// 401 | /// # Errors 402 | /// 403 | /// Returns an [`io::Error`] on failure. 404 | pub fn from_str(s: &str) -> io::Result> { 405 | from_reader(s.as_bytes()) 406 | } 407 | 408 | /// Construct a minimal ACL from the traditional `mode` permission bits. 409 | /// 410 | /// Returns a `Vec` for a minimal ACL with three entries corresponding 411 | /// to the owner/group/other permission bits given in `mode`. 412 | /// 413 | /// Extra bits outside the mask 0o777 are ignored. 414 | #[cfg(any(docsrs, target_os = "linux", target_os = "freebsd"))] 415 | #[cfg_attr(docsrs, doc(cfg(any(target_os = "linux", target_os = "freebsd"))))] 416 | #[must_use] 417 | pub fn from_mode(mode: u32) -> Vec { 418 | vec![ 419 | AclEntry::allow_user("", Perm::from_bits_truncate((mode >> 6) & 7), None), 420 | AclEntry::allow_group("", Perm::from_bits_truncate((mode >> 3) & 7), None), 421 | AclEntry::allow_other(Perm::from_bits_truncate(mode & 7), None), 422 | ] 423 | } 424 | -------------------------------------------------------------------------------- /src/qualifier.rs: -------------------------------------------------------------------------------- 1 | //! Implements the `Qualifier` type for internal use 2 | 3 | use crate::failx::*; 4 | use crate::unix; 5 | use std::fmt; 6 | use std::io; 7 | #[cfg(target_os = "macos")] 8 | use uuid::Uuid; 9 | 10 | #[cfg(any(target_os = "linux", target_os = "freebsd"))] 11 | const OWNER_NAME: &str = ""; 12 | #[cfg(any(target_os = "linux", target_os = "freebsd"))] 13 | const OTHER_NAME: &str = ""; 14 | #[cfg(any(target_os = "linux", target_os = "freebsd"))] 15 | const MASK_NAME: &str = ""; 16 | #[cfg(target_os = "freebsd")] 17 | const EVERYONE_NAME: &str = ""; 18 | 19 | /// A Qualifier specifies the principal that is allowed/denied access to a 20 | /// resource. 21 | #[derive(Debug, PartialEq, Eq)] 22 | pub enum Qualifier { 23 | User(unix::uid_t), 24 | Group(unix::gid_t), 25 | 26 | #[cfg(target_os = "macos")] 27 | Guid(Uuid), 28 | 29 | #[cfg(any(target_os = "linux", target_os = "freebsd"))] 30 | UserObj, 31 | #[cfg(any(target_os = "linux", target_os = "freebsd"))] 32 | GroupObj, 33 | #[cfg(any(target_os = "linux", target_os = "freebsd"))] 34 | Other, 35 | #[cfg(any(target_os = "linux", target_os = "freebsd"))] 36 | Mask, 37 | #[cfg(target_os = "freebsd")] 38 | Everyone, 39 | 40 | Unknown(String), 41 | } 42 | 43 | impl Qualifier { 44 | /// Create qualifier object from a GUID. 45 | #[cfg(target_os = "macos")] 46 | pub fn from_guid(guid: Uuid) -> io::Result { 47 | let qualifier = match unix::guid_to_id(guid)? { 48 | (Some(uid), None) => Qualifier::User(uid), 49 | (None, Some(gid)) => Qualifier::Group(gid), 50 | (None, None) => Qualifier::Guid(guid), 51 | _ => unreachable!("guid_to_id bug"), 52 | }; 53 | 54 | Ok(qualifier) 55 | } 56 | 57 | /// Create qualifier object from a user name. 58 | #[cfg(target_os = "macos")] 59 | pub fn user_named(name: &str) -> io::Result { 60 | match unix::name_to_uid(name) { 61 | Ok(uid) => Ok(Qualifier::User(uid)), 62 | Err(err) => { 63 | // Try to parse name as a GUID. 64 | Uuid::parse_str(name).map_or(Err(err), Qualifier::from_guid) 65 | } 66 | } 67 | } 68 | 69 | /// Create qualifier object from a user name. 70 | #[cfg(any(target_os = "linux", target_os = "freebsd"))] 71 | pub fn user_named(name: &str) -> io::Result { 72 | match name { 73 | OWNER_NAME => Ok(Qualifier::UserObj), 74 | s => match unix::name_to_uid(s) { 75 | Ok(uid) => Ok(Qualifier::User(uid)), 76 | Err(err) => Err(err), 77 | }, 78 | } 79 | } 80 | 81 | /// Create qualifier object from a group name. 82 | #[cfg(target_os = "macos")] 83 | pub fn group_named(name: &str) -> io::Result { 84 | match unix::name_to_gid(name) { 85 | Ok(gid) => Ok(Qualifier::Group(gid)), 86 | Err(err) => Uuid::parse_str(name).map_or(Err(err), Qualifier::from_guid), 87 | } 88 | } 89 | 90 | /// Create qualifier object from a group name. 91 | #[cfg(any(target_os = "linux", target_os = "freebsd"))] 92 | pub fn group_named(name: &str) -> io::Result { 93 | match name { 94 | OWNER_NAME => Ok(Qualifier::GroupObj), 95 | s => match unix::name_to_gid(s) { 96 | Ok(gid) => Ok(Qualifier::Group(gid)), 97 | Err(err) => Err(err), 98 | }, 99 | } 100 | } 101 | 102 | /// Create qualifier from mask. 103 | #[cfg(any(target_os = "linux", target_os = "freebsd"))] 104 | pub fn mask_named(name: &str) -> io::Result { 105 | match name { 106 | MASK_NAME => Ok(Qualifier::Mask), 107 | s => fail_custom(&format!("unknown mask name: {s:?}")), 108 | } 109 | } 110 | 111 | /// Create qualifier from other. 112 | #[cfg(any(target_os = "linux", target_os = "freebsd"))] 113 | pub fn other_named(name: &str) -> io::Result { 114 | match name { 115 | OTHER_NAME => Ok(Qualifier::Other), 116 | s => fail_custom(&format!("unknown other name: {s:?}")), 117 | } 118 | } 119 | 120 | /// Create qualifier from everyone. 121 | #[cfg(target_os = "freebsd")] 122 | pub fn everyone_named(name: &str) -> io::Result { 123 | match name { 124 | EVERYONE_NAME => Ok(Qualifier::Everyone), 125 | s => fail_custom(&format!("unknown everyone name: {s:?}")), 126 | } 127 | } 128 | 129 | /// Return the GUID for the user/group. 130 | #[cfg(target_os = "macos")] 131 | pub fn guid(&self) -> io::Result { 132 | match self { 133 | Qualifier::User(uid) => unix::uid_to_guid(*uid), 134 | Qualifier::Group(gid) => unix::gid_to_guid(*gid), 135 | Qualifier::Guid(guid) => Ok(*guid), 136 | Qualifier::Unknown(tag) => fail_custom(&format!("unknown tag: {tag:?}")), 137 | } 138 | } 139 | 140 | /// Return the name of the user/group. 141 | pub fn name(&self) -> io::Result { 142 | let result = match self { 143 | Qualifier::User(uid) => unix::uid_to_name(*uid)?, 144 | Qualifier::Group(gid) => unix::gid_to_name(*gid)?, 145 | #[cfg(target_os = "macos")] 146 | Qualifier::Guid(guid) => guid.to_string(), 147 | #[cfg(any(target_os = "linux", target_os = "freebsd"))] 148 | Qualifier::UserObj | Qualifier::GroupObj => OWNER_NAME.to_string(), 149 | #[cfg(any(target_os = "linux", target_os = "freebsd"))] 150 | Qualifier::Other => OTHER_NAME.to_string(), 151 | #[cfg(any(target_os = "linux", target_os = "freebsd"))] 152 | Qualifier::Mask => MASK_NAME.to_string(), 153 | #[cfg(target_os = "freebsd")] 154 | Qualifier::Everyone => EVERYONE_NAME.to_string(), 155 | 156 | Qualifier::Unknown(s) => s.clone(), 157 | }; 158 | 159 | Ok(result) 160 | } 161 | } 162 | 163 | impl fmt::Display for Qualifier { 164 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 165 | match self { 166 | Qualifier::User(uid) => write!(f, "user:{uid}"), 167 | Qualifier::Group(gid) => write!(f, "group:{gid}"), 168 | #[cfg(target_os = "macos")] 169 | Qualifier::Guid(guid) => write!(f, "guid:{guid}"), 170 | #[cfg(any(target_os = "linux", target_os = "freebsd"))] 171 | Qualifier::UserObj => write!(f, "user"), 172 | #[cfg(any(target_os = "linux", target_os = "freebsd"))] 173 | Qualifier::GroupObj => write!(f, "group"), 174 | #[cfg(any(target_os = "linux", target_os = "freebsd"))] 175 | Qualifier::Other => write!(f, "other"), 176 | #[cfg(any(target_os = "linux", target_os = "freebsd"))] 177 | Qualifier::Mask => write!(f, "mask"), 178 | #[cfg(target_os = "freebsd")] 179 | Qualifier::Everyone => write!(f, "everyone"), 180 | Qualifier::Unknown(s) => write!(f, "unknown:{s}"), 181 | } 182 | } 183 | } 184 | 185 | //////////////////////////////////////////////////////////////////////////////// 186 | 187 | #[cfg(test)] 188 | mod qualifier_tests { 189 | use super::*; 190 | 191 | /// Retrieve `user_id` and `group_id` of unix entity with specified name. 192 | #[cfg(any(target_os = "linux", target_os = "freebsd"))] 193 | fn getent(name: &str) -> (u32, u32) { 194 | use std::str::FromStr; 195 | 196 | let cmd = std::process::Command::new("getent") 197 | .arg("passwd") 198 | .arg(name) 199 | .output() 200 | .expect("Valid command"); 201 | let out = String::from_utf8(cmd.stdout).expect("Valid utf8"); 202 | let tokens = out.split(':').collect::>(); 203 | assert_eq!(tokens[0], name); 204 | 205 | let user_id = u32::from_str(tokens[2]).expect("Valid uid"); 206 | let group_id = u32::from_str(tokens[3]).expect("Valid gid"); 207 | 208 | (user_id, group_id) 209 | } 210 | 211 | #[test] 212 | #[cfg(target_os = "macos")] 213 | fn test_from_guid() { 214 | let user = 215 | Qualifier::from_guid(Uuid::parse_str("ffffeeee-dddd-cccc-bbbb-aaaa00000059").unwrap()) 216 | .ok(); 217 | assert_eq!(user, Some(Qualifier::User(89))); 218 | 219 | let group = 220 | Qualifier::from_guid(Uuid::parse_str("abcdefab-cdef-abcd-efab-cdef00000059").unwrap()) 221 | .ok(); 222 | assert_eq!(group, Some(Qualifier::Group(89))); 223 | 224 | let user = Qualifier::from_guid(Uuid::nil()).ok(); 225 | assert_eq!(user, Some(Qualifier::Guid(Uuid::nil()))); 226 | } 227 | 228 | #[test] 229 | fn test_user_named() { 230 | let user = Qualifier::user_named("89").ok(); 231 | assert_eq!(user, Some(Qualifier::User(89))); 232 | 233 | #[cfg(target_os = "macos")] 234 | { 235 | let user = Qualifier::user_named("_spotlight").ok(); 236 | assert_eq!(user, Some(Qualifier::User(89))); 237 | 238 | let user = Qualifier::user_named("ffffeeee-dddd-cccc-bbbb-aaaa00000059").ok(); 239 | assert_eq!(user, Some(Qualifier::User(89))); 240 | } 241 | 242 | #[cfg(any(target_os = "linux", target_os = "freebsd"))] 243 | { 244 | let (user_id, _) = getent("daemon"); 245 | let user = Qualifier::user_named("daemon").ok(); 246 | assert_eq!(user, Some(Qualifier::User(user_id))); 247 | } 248 | } 249 | 250 | #[test] 251 | fn test_group_named() { 252 | let group = Qualifier::group_named("89").ok(); 253 | assert_eq!(group, Some(Qualifier::Group(89))); 254 | 255 | #[cfg(target_os = "macos")] 256 | { 257 | let group = Qualifier::group_named("_spotlight").ok(); 258 | assert_eq!(group, Some(Qualifier::Group(89))); 259 | 260 | let group = Qualifier::group_named("abcdefab-cdef-abcd-efab-cdef00000059").ok(); 261 | assert_eq!(group, Some(Qualifier::Group(89))); 262 | } 263 | 264 | #[cfg(any(target_os = "linux", target_os = "freebsd"))] 265 | { 266 | let (_, group_id) = getent("daemon"); 267 | let group = Qualifier::group_named("daemon").ok(); 268 | assert_eq!(group, Some(Qualifier::Group(group_id))); 269 | } 270 | } 271 | } 272 | -------------------------------------------------------------------------------- /src/sys.rs: -------------------------------------------------------------------------------- 1 | //! Rust bindings to system C API. 2 | 3 | #![allow(dead_code, non_camel_case_types)] // constant is never used 4 | 5 | pub use crate::bindings::*; 6 | 7 | // Demangle some MacOS constants. Linux provides these as-is. 8 | 9 | #[cfg(target_os = "macos")] 10 | pub const ACL_READ: acl_perm_t = acl_perm_t_ACL_READ_DATA; 11 | 12 | #[cfg(target_os = "macos")] 13 | pub const ACL_WRITE: acl_perm_t = acl_perm_t_ACL_WRITE_DATA; 14 | 15 | #[cfg(target_os = "macos")] 16 | pub const ACL_EXECUTE: acl_perm_t = acl_perm_t_ACL_EXECUTE; 17 | 18 | // Linux doesn't have ACL flags; adding acl_flag_t makes the code more orthogonal. 19 | // On FreeBSD, acl_flag_t is a u16. 20 | #[cfg(target_os = "linux")] 21 | pub type acl_flag_t = u32; 22 | 23 | // Linux doesn't have ACL_MAX_ENTRIES, so define it as 2 billion. 24 | #[cfg(target_os = "linux")] 25 | pub const ACL_MAX_ENTRIES: u32 = 2_000_000_000; 26 | 27 | // MacOS and FreeBSD use acl_get_perm_np(). 28 | #[cfg(any(target_os = "macos", target_os = "freebsd"))] 29 | pub unsafe fn acl_get_perm(permset_d: acl_permset_t, perm: acl_perm_t) -> ::std::os::raw::c_int { 30 | acl_get_perm_np(permset_d, perm) 31 | } 32 | 33 | /// Non-portable ACL Permissions & Flags (`macOS` only) 34 | #[cfg(all(target_os = "macos", not(docsrs)))] 35 | pub mod np { 36 | use super::*; 37 | 38 | pub const ACL_DELETE: acl_perm_t = acl_perm_t_ACL_DELETE; 39 | pub const ACL_APPEND_DATA: acl_perm_t = acl_perm_t_ACL_APPEND_DATA; 40 | pub const ACL_DELETE_CHILD: acl_perm_t = acl_perm_t_ACL_DELETE_CHILD; 41 | pub const ACL_READ_ATTRIBUTES: acl_perm_t = acl_perm_t_ACL_READ_ATTRIBUTES; 42 | pub const ACL_WRITE_ATTRIBUTES: acl_perm_t = acl_perm_t_ACL_WRITE_ATTRIBUTES; 43 | pub const ACL_READ_EXTATTRIBUTES: acl_perm_t = acl_perm_t_ACL_READ_EXTATTRIBUTES; 44 | pub const ACL_WRITE_EXTATTRIBUTES: acl_perm_t = acl_perm_t_ACL_WRITE_EXTATTRIBUTES; 45 | pub const ACL_READ_SECURITY: acl_perm_t = acl_perm_t_ACL_READ_SECURITY; 46 | pub const ACL_WRITE_SECURITY: acl_perm_t = acl_perm_t_ACL_WRITE_SECURITY; 47 | pub const ACL_CHANGE_OWNER: acl_perm_t = acl_perm_t_ACL_CHANGE_OWNER; 48 | pub const ACL_SYNCHRONIZE: acl_perm_t = acl_perm_t_ACL_SYNCHRONIZE; 49 | 50 | pub const ACL_FLAG_DEFER_INHERIT: acl_flag_t = acl_flag_t_ACL_FLAG_DEFER_INHERIT; 51 | pub const ACL_FLAG_NO_INHERIT: acl_flag_t = acl_flag_t_ACL_FLAG_NO_INHERIT; 52 | pub const ACL_ENTRY_INHERITED: acl_flag_t = acl_flag_t_ACL_ENTRY_INHERITED; 53 | pub const ACL_ENTRY_FILE_INHERIT: acl_flag_t = acl_flag_t_ACL_ENTRY_FILE_INHERIT; 54 | pub const ACL_ENTRY_DIRECTORY_INHERIT: acl_flag_t = acl_flag_t_ACL_ENTRY_DIRECTORY_INHERIT; 55 | pub const ACL_ENTRY_LIMIT_INHERIT: acl_flag_t = acl_flag_t_ACL_ENTRY_LIMIT_INHERIT; 56 | pub const ACL_ENTRY_ONLY_INHERIT: acl_flag_t = acl_flag_t_ACL_ENTRY_ONLY_INHERIT; 57 | } 58 | 59 | /// Non-portable ACL Permissions & Flags (`FreeBSD` only) 60 | #[cfg(all(target_os = "freebsd", not(docsrs)))] 61 | pub mod np { 62 | use super::{acl_flag_t, acl_perm_t}; 63 | 64 | pub const ACL_READ_DATA: acl_perm_t = super::ACL_READ_DATA; 65 | pub const ACL_WRITE_DATA: acl_perm_t = super::ACL_WRITE_DATA; 66 | // `ACL_EXECUTE` is portable. 67 | pub const ACL_DELETE: acl_perm_t = super::ACL_DELETE; 68 | pub const ACL_APPEND_DATA: acl_perm_t = super::ACL_APPEND_DATA; 69 | pub const ACL_DELETE_CHILD: acl_perm_t = super::ACL_DELETE_CHILD; 70 | pub const ACL_READ_ATTRIBUTES: acl_perm_t = super::ACL_READ_ATTRIBUTES; 71 | pub const ACL_WRITE_ATTRIBUTES: acl_perm_t = super::ACL_WRITE_ATTRIBUTES; 72 | pub const ACL_READ_EXTATTRIBUTES: acl_perm_t = super::ACL_READ_NAMED_ATTRS; 73 | pub const ACL_WRITE_EXTATTRIBUTES: acl_perm_t = super::ACL_WRITE_NAMED_ATTRS; 74 | pub const ACL_READ_SECURITY: acl_perm_t = super::ACL_READ_ACL; 75 | pub const ACL_WRITE_SECURITY: acl_perm_t = super::ACL_WRITE_ACL; 76 | pub const ACL_CHANGE_OWNER: acl_perm_t = super::ACL_WRITE_OWNER; 77 | pub const ACL_SYNCHRONIZE: acl_perm_t = super::ACL_SYNCHRONIZE; 78 | 79 | pub const ACL_ENTRY_INHERITED: acl_flag_t = super::ACL_ENTRY_INHERITED as acl_flag_t; 80 | pub const ACL_ENTRY_FILE_INHERIT: acl_flag_t = super::ACL_ENTRY_FILE_INHERIT as acl_flag_t; 81 | pub const ACL_ENTRY_DIRECTORY_INHERIT: acl_flag_t = 82 | super::ACL_ENTRY_DIRECTORY_INHERIT as acl_flag_t; 83 | pub const ACL_ENTRY_LIMIT_INHERIT: acl_flag_t = 84 | super::ACL_ENTRY_NO_PROPAGATE_INHERIT as acl_flag_t; 85 | pub const ACL_ENTRY_ONLY_INHERIT: acl_flag_t = super::ACL_ENTRY_INHERIT_ONLY as acl_flag_t; 86 | pub const ACL_ENTRY_SUCCESSFUL_ACCESS: acl_flag_t = 87 | super::ACL_ENTRY_SUCCESSFUL_ACCESS as acl_flag_t; 88 | pub const ACL_ENTRY_FAILED_ACCESS: acl_flag_t = super::ACL_ENTRY_FAILED_ACCESS as acl_flag_t; 89 | } 90 | 91 | /// Non-portable ACL Permissions (Docs only). These are fabricated constants to 92 | /// make it possible for docs to be built on macOS and Linux. 93 | #[cfg(docsrs)] 94 | pub mod np { 95 | use super::*; 96 | 97 | pub const ACL_READ_DATA: acl_perm_t = 1 << 8; 98 | pub const ACL_WRITE_DATA: acl_perm_t = 1 << 9; 99 | pub const ACL_DELETE: acl_perm_t = 1 << 10; 100 | pub const ACL_APPEND_DATA: acl_perm_t = 1 << 11; 101 | pub const ACL_DELETE_CHILD: acl_perm_t = 1 << 12; 102 | pub const ACL_READ_ATTRIBUTES: acl_perm_t = 1 << 13; 103 | pub const ACL_WRITE_ATTRIBUTES: acl_perm_t = 1 << 14; 104 | pub const ACL_READ_EXTATTRIBUTES: acl_perm_t = 1 << 15; 105 | pub const ACL_WRITE_EXTATTRIBUTES: acl_perm_t = 1 << 16; 106 | pub const ACL_READ_SECURITY: acl_perm_t = 1 << 17; 107 | pub const ACL_WRITE_SECURITY: acl_perm_t = 1 << 18; 108 | pub const ACL_CHANGE_OWNER: acl_perm_t = 1 << 19; 109 | pub const ACL_SYNCHRONIZE: acl_perm_t = 1 << 20; 110 | 111 | pub const ACL_FLAG_DEFER_INHERIT: acl_flag_t = 1 << 21; 112 | pub const ACL_FLAG_NO_INHERIT: acl_flag_t = 1 << 22; 113 | pub const ACL_ENTRY_INHERITED: acl_flag_t = 1 << 23; 114 | pub const ACL_ENTRY_FILE_INHERIT: acl_flag_t = 1 << 24; 115 | pub const ACL_ENTRY_DIRECTORY_INHERIT: acl_flag_t = 1 << 25; 116 | pub const ACL_ENTRY_LIMIT_INHERIT: acl_flag_t = 1 << 26; 117 | pub const ACL_ENTRY_ONLY_INHERIT: acl_flag_t = 1 << 27; 118 | } 119 | 120 | // Convenience constants where the API expects a signed i32 type, but bindgen 121 | // provides u32. (FIXME: Replace with bindgen ParseCallbacks::int_macro?) 122 | 123 | pub mod sg { 124 | #![allow(clippy::cast_possible_wrap)] 125 | 126 | use super::*; 127 | 128 | pub const ENOENT: i32 = super::ENOENT as i32; 129 | pub const ENOTSUP: i32 = super::ENOTSUP as i32; 130 | pub const EINVAL: i32 = super::EINVAL as i32; 131 | pub const ENOMEM: i32 = super::ENOMEM as i32; 132 | pub const ERANGE: i32 = super::ERANGE as i32; 133 | pub const ACL_MAX_ENTRIES: i32 = super::ACL_MAX_ENTRIES as i32; 134 | 135 | #[cfg(target_os = "macos")] 136 | pub const ACL_TYPE_EXTENDED: acl_type_t = super::acl_type_t_ACL_TYPE_EXTENDED; 137 | #[cfg(any(target_os = "linux", target_os = "freebsd"))] 138 | pub const ACL_TYPE_ACCESS: acl_type_t = super::ACL_TYPE_ACCESS as acl_type_t; 139 | #[cfg(any(target_os = "linux", target_os = "freebsd"))] 140 | pub const ACL_TYPE_DEFAULT: acl_type_t = super::ACL_TYPE_DEFAULT as acl_type_t; 141 | #[cfg(target_os = "freebsd")] 142 | pub const ACL_TYPE_NFS4: acl_type_t = super::ACL_TYPE_NFS4 as acl_type_t; 143 | #[cfg(target_os = "freebsd")] 144 | pub const ACL_BRAND_UNKNOWN: i32 = super::ACL_BRAND_UNKNOWN as i32; 145 | #[cfg(target_os = "freebsd")] 146 | pub const ACL_BRAND_POSIX: i32 = super::ACL_BRAND_POSIX as i32; 147 | #[cfg(target_os = "freebsd")] 148 | pub const ACL_BRAND_NFS4: i32 = super::ACL_BRAND_NFS4 as i32; 149 | #[cfg(target_os = "freebsd")] 150 | pub const ACL_ENTRY_TYPE_ALLOW: acl_entry_type_t = 151 | super::ACL_ENTRY_TYPE_ALLOW as acl_entry_type_t; 152 | #[cfg(target_os = "freebsd")] 153 | pub const ACL_ENTRY_TYPE_DENY: acl_entry_type_t = 154 | super::ACL_ENTRY_TYPE_DENY as acl_entry_type_t; 155 | 156 | #[cfg(target_os = "macos")] 157 | pub const ACL_FIRST_ENTRY: i32 = super::acl_entry_id_t_ACL_FIRST_ENTRY; 158 | #[cfg(any(target_os = "linux", target_os = "freebsd"))] 159 | pub const ACL_FIRST_ENTRY: i32 = super::ACL_FIRST_ENTRY as i32; 160 | 161 | #[cfg(target_os = "macos")] 162 | pub const ACL_NEXT_ENTRY: i32 = super::acl_entry_id_t_ACL_NEXT_ENTRY; 163 | #[cfg(any(target_os = "linux", target_os = "freebsd"))] 164 | pub const ACL_NEXT_ENTRY: i32 = super::ACL_NEXT_ENTRY as i32; 165 | 166 | #[cfg(target_os = "macos")] 167 | pub const O_SYMLINK: i32 = super::O_SYMLINK as i32; 168 | 169 | #[cfg(target_os = "macos")] 170 | pub const ACL_EXTENDED_ALLOW: acl_tag_t = super::acl_tag_t_ACL_EXTENDED_ALLOW; 171 | #[cfg(target_os = "macos")] 172 | pub const ACL_EXTENDED_DENY: acl_tag_t = super::acl_tag_t_ACL_EXTENDED_DENY; 173 | #[cfg(any(target_os = "linux", target_os = "freebsd"))] 174 | pub const ACL_USER_OBJ: acl_tag_t = super::ACL_USER_OBJ as acl_tag_t; 175 | #[cfg(any(target_os = "linux", target_os = "freebsd"))] 176 | pub const ACL_USER: acl_tag_t = super::ACL_USER as acl_tag_t; 177 | #[cfg(any(target_os = "linux", target_os = "freebsd"))] 178 | pub const ACL_GROUP_OBJ: acl_tag_t = super::ACL_GROUP_OBJ as acl_tag_t; 179 | #[cfg(any(target_os = "linux", target_os = "freebsd"))] 180 | pub const ACL_GROUP: acl_tag_t = super::ACL_GROUP as acl_tag_t; 181 | #[cfg(any(target_os = "linux", target_os = "freebsd"))] 182 | pub const ACL_MASK: acl_tag_t = super::ACL_MASK as acl_tag_t; 183 | #[cfg(any(target_os = "linux", target_os = "freebsd"))] 184 | pub const ACL_OTHER: acl_tag_t = super::ACL_OTHER as acl_tag_t; 185 | #[cfg(target_os = "freebsd")] 186 | pub const ACL_EVERYONE: acl_tag_t = super::ACL_EVERYONE as acl_tag_t; 187 | 188 | #[cfg(target_os = "macos")] 189 | pub const ID_TYPE_UID: i32 = super::ID_TYPE_UID as i32; 190 | #[cfg(target_os = "macos")] 191 | pub const ID_TYPE_GID: i32 = super::ID_TYPE_GID as i32; 192 | 193 | #[cfg(target_os = "freebsd")] 194 | pub const PC_ACL_NFS4: i32 = super::_PC_ACL_NFS4 as i32; 195 | 196 | #[test] 197 | fn test_signed() { 198 | assert!(super::ENOENT as i32 >= 0); 199 | assert!(super::ENOTSUP as i32 >= 0); 200 | assert!(super::EINVAL as i32 >= 0); 201 | assert!(super::ENOMEM as i32 >= 0); 202 | assert!(super::ACL_MAX_ENTRIES as i32 >= 0); 203 | 204 | #[cfg(target_os = "linux")] 205 | assert!(super::ACL_FIRST_ENTRY as i32 >= 0); 206 | 207 | #[cfg(target_os = "linux")] 208 | assert!(super::ACL_NEXT_ENTRY as i32 >= 0); 209 | 210 | #[cfg(target_os = "macos")] 211 | assert!(super::O_SYMLINK as i32 >= 0); 212 | 213 | #[cfg(target_os = "macos")] 214 | assert!(super::ID_TYPE_UID as i32 >= 0); 215 | #[cfg(target_os = "macos")] 216 | assert!(super::ID_TYPE_GID as i32 >= 0); 217 | } 218 | } 219 | -------------------------------------------------------------------------------- /src/unix.rs: -------------------------------------------------------------------------------- 1 | //! Implements utilities for converting user/group names to uid/gid. 2 | 3 | use crate::failx::*; 4 | use crate::sys::{getgrgid_r, getgrnam_r, getpwnam_r, getpwuid_r, group, passwd, sg}; 5 | #[cfg(target_os = "macos")] 6 | use crate::sys::{id_t, mbr_gid_to_uuid, mbr_uid_to_uuid, mbr_uuid_to_id}; 7 | 8 | use std::ffi::{CStr, CString}; 9 | use std::io; 10 | use std::mem; 11 | use std::os::raw::c_char; 12 | use std::ptr; 13 | #[cfg(target_os = "macos")] 14 | use uuid::Uuid; 15 | 16 | // Export uid_t and gid_t. 17 | pub use crate::sys::{gid_t, uid_t}; 18 | 19 | // Max buffer sizes for getpwnam_r, getgrnam_r, et al. are usually determined 20 | // by calling sysconf with SC_GETPW_R_SIZE_MAX or SC_GETGR_R_SIZE_MAX. Rather 21 | // than calling sysconf, this code hard-wires the default value and quadruples 22 | // the buffer size as needed, up to a maximum of 1MB. 23 | 24 | // SC_GETPW_R_SIZE_MAX/SC_GETGR_R_SIZE_MAX default to 1024 on vanilla Ubuntu 25 | // and 4096 on macOS/FreeBSD. We start the initial buffer size at 4096 bytes. 26 | 27 | const INITIAL_BUFSIZE: usize = 4096; // 4KB 28 | const MAX_BUFSIZE: usize = 1_048_576; // 1MB 29 | 30 | /// Convert user name to uid. 31 | pub fn name_to_uid(name: &str) -> io::Result { 32 | let mut pwd = mem::MaybeUninit::::uninit(); 33 | let mut buf = Vec::::with_capacity(INITIAL_BUFSIZE); 34 | let mut result = ptr::null_mut(); 35 | let cstr = CString::new(name)?; 36 | 37 | let mut ret; 38 | loop { 39 | ret = unsafe { 40 | getpwnam_r( 41 | cstr.as_ptr(), 42 | pwd.as_mut_ptr(), 43 | buf.as_mut_ptr(), 44 | buf.capacity(), 45 | &mut result, 46 | ) 47 | }; 48 | 49 | if ret == 0 || ret != sg::ERANGE || buf.capacity() >= MAX_BUFSIZE { 50 | break; 51 | } 52 | 53 | // Quadruple buffer size and try again. 54 | buf.reserve(4 * buf.capacity()); 55 | } 56 | 57 | if ret != 0 { 58 | return fail_err(ret, "getpwnam_r", name); 59 | } 60 | 61 | if !result.is_null() { 62 | let uid = unsafe { pwd.assume_init().pw_uid }; 63 | return Ok(uid); 64 | } 65 | 66 | // Try to parse name as a decimal user ID. 67 | if let Ok(num) = name.parse::() { 68 | return Ok(num); 69 | } 70 | 71 | fail_custom(&format!("unknown user name: {name:?}")) 72 | } 73 | 74 | /// Convert group name to gid. 75 | pub fn name_to_gid(name: &str) -> io::Result { 76 | let mut grp = mem::MaybeUninit::::uninit(); 77 | let mut buf = Vec::::with_capacity(INITIAL_BUFSIZE); 78 | let mut result = ptr::null_mut(); 79 | let cstr = CString::new(name)?; 80 | 81 | let mut ret; 82 | loop { 83 | ret = unsafe { 84 | getgrnam_r( 85 | cstr.as_ptr(), 86 | grp.as_mut_ptr(), 87 | buf.as_mut_ptr(), 88 | buf.capacity(), 89 | &mut result, 90 | ) 91 | }; 92 | 93 | if ret == 0 || ret != sg::ERANGE || buf.capacity() >= MAX_BUFSIZE { 94 | break; 95 | } 96 | 97 | // Quadruple buffer size and try again. 98 | buf.reserve(4 * buf.capacity()); 99 | } 100 | 101 | if ret != 0 { 102 | return fail_err(ret, "getgrnam_r", name); 103 | } 104 | 105 | if !result.is_null() { 106 | let gid = unsafe { grp.assume_init().gr_gid }; 107 | return Ok(gid); 108 | } 109 | 110 | // Try to parse name as a decimal group ID. 111 | if let Ok(num) = name.parse::() { 112 | return Ok(num); 113 | } 114 | 115 | fail_custom(&format!("unknown group name: {name:?}")) 116 | } 117 | 118 | /// Convert uid to user name. 119 | pub fn uid_to_name(uid: uid_t) -> io::Result { 120 | let mut pwd = mem::MaybeUninit::::uninit(); 121 | let mut buf = Vec::::with_capacity(INITIAL_BUFSIZE); 122 | let mut result = ptr::null_mut(); 123 | 124 | let mut ret; 125 | loop { 126 | ret = unsafe { 127 | getpwuid_r( 128 | uid, 129 | pwd.as_mut_ptr(), 130 | buf.as_mut_ptr(), 131 | buf.capacity(), 132 | &mut result, 133 | ) 134 | }; 135 | 136 | if ret == 0 || ret != sg::ERANGE || buf.capacity() >= MAX_BUFSIZE { 137 | break; 138 | } 139 | 140 | // Quadruple buffer size and try again. 141 | buf.reserve(4 * buf.capacity()); 142 | } 143 | 144 | if ret != 0 { 145 | return fail_err(ret, "getpwuid_r", uid); 146 | } 147 | 148 | if !result.is_null() { 149 | let cstr = unsafe { CStr::from_ptr(pwd.assume_init().pw_name) }; 150 | return Ok(cstr.to_string_lossy().into_owned()); 151 | } 152 | 153 | Ok(uid.to_string()) 154 | } 155 | 156 | /// Convert gid to group name. 157 | pub fn gid_to_name(gid: gid_t) -> io::Result { 158 | let mut grp = mem::MaybeUninit::::uninit(); 159 | let mut buf = Vec::::with_capacity(INITIAL_BUFSIZE); 160 | let mut result = ptr::null_mut(); 161 | 162 | let mut ret; 163 | loop { 164 | ret = unsafe { 165 | getgrgid_r( 166 | gid, 167 | grp.as_mut_ptr(), 168 | buf.as_mut_ptr(), 169 | buf.capacity(), 170 | &mut result, 171 | ) 172 | }; 173 | 174 | if ret == 0 || ret != sg::ERANGE || buf.capacity() >= MAX_BUFSIZE { 175 | break; 176 | } 177 | 178 | // Quadruple buffer size and try again. 179 | buf.reserve(4 * buf.capacity()); 180 | } 181 | 182 | if ret != 0 { 183 | return fail_err(ret, "getgrgid_r", gid); 184 | } 185 | 186 | if !result.is_null() { 187 | let cstr = unsafe { CStr::from_ptr(grp.assume_init().gr_name) }; 188 | return Ok(cstr.to_string_lossy().into_owned()); 189 | } 190 | 191 | Ok(gid.to_string()) 192 | } 193 | 194 | /// Convert uid to GUID. 195 | #[cfg(target_os = "macos")] 196 | pub fn uid_to_guid(uid: uid_t) -> io::Result { 197 | let mut bytes = [0u8; 16]; 198 | 199 | // On error, returns one of {EIO, ENOENT, EAUTH, EINVAL, ENOMEM}. 200 | let ret = unsafe { mbr_uid_to_uuid(uid, bytes.as_mut_ptr()) }; 201 | if ret != 0 { 202 | return fail_from_err(ret, "mbr_uid_to_uuid", uid); 203 | } 204 | 205 | Ok(Uuid::from_bytes(bytes)) 206 | } 207 | 208 | /// Convert gid to GUID. 209 | #[cfg(target_os = "macos")] 210 | pub fn gid_to_guid(gid: gid_t) -> io::Result { 211 | let mut bytes = [0u8; 16]; 212 | 213 | // On error, returns one of {EIO, ENOENT, EAUTH, EINVAL, ENOMEM}. 214 | let ret = unsafe { mbr_gid_to_uuid(gid, bytes.as_mut_ptr()) }; 215 | if ret != 0 { 216 | return fail_from_err(ret, "mbr_gid_to_uuid", gid); 217 | } 218 | 219 | Ok(Uuid::from_bytes(bytes)) 220 | } 221 | 222 | /// Convert GUID to uid/gid. 223 | /// 224 | /// Returns a pair of options (Option[uid], Option[gid]). Either one option must 225 | /// be set or neither is set. If neither is set, the GUID was not found. 226 | #[cfg(target_os = "macos")] 227 | pub fn guid_to_id(guid: Uuid) -> io::Result<(Option, Option)> { 228 | let mut id_c: id_t = 0; 229 | let mut idtype: i32 = 0; 230 | let mut bytes = guid.into_bytes(); 231 | 232 | // On error, returns one of {EIO, ENOENT, EAUTH, EINVAL, ENOMEM}. 233 | let ret = unsafe { mbr_uuid_to_id(bytes.as_mut_ptr(), &mut id_c, &mut idtype) }; 234 | if ret == sg::ENOENT { 235 | // GUID was not found. 236 | return Ok((None, None)); 237 | } 238 | 239 | if ret != 0 { 240 | return fail_from_err(ret, "mbr_uuid_to_id", guid); 241 | } 242 | 243 | let result = match idtype { 244 | sg::ID_TYPE_UID => (Some(id_c), None), 245 | sg::ID_TYPE_GID => (None, Some(id_c)), 246 | _ => { 247 | return fail_custom(&format!( 248 | "mbr_uuid_to_id: Unknown idtype {idtype:?} for guid {guid:?}" 249 | )) 250 | } 251 | }; 252 | 253 | Ok(result) 254 | } 255 | 256 | //////////////////////////////////////////////////////////////////////////////// 257 | 258 | #[cfg(test)] 259 | mod unix_tests { 260 | use super::*; 261 | 262 | /// Retrieve `user_id` and `group_id` of unix entity with specified name. 263 | #[cfg(any(target_os = "linux", target_os = "freebsd"))] 264 | fn getent(name: &str) -> (u32, u32) { 265 | use std::str::FromStr; 266 | 267 | let cmd = std::process::Command::new("getent") 268 | .arg("passwd") 269 | .arg(name) 270 | .output() 271 | .expect("Valid command"); 272 | let out = String::from_utf8(cmd.stdout).expect("Valid utf8"); 273 | let tokens = out.split(':').collect::>(); 274 | assert_eq!(tokens[0], name); 275 | 276 | let user_id = u32::from_str(tokens[2]).expect("Valid uid"); 277 | let group_id = u32::from_str(tokens[3]).expect("Valid gid"); 278 | 279 | (user_id, group_id) 280 | } 281 | 282 | #[test] 283 | fn test_name_to_uid() { 284 | let msg = name_to_uid("").unwrap_err().to_string(); 285 | assert_eq!(msg, "unknown user name: \"\""); 286 | 287 | let msg = name_to_uid("non_existant").unwrap_err().to_string(); 288 | assert_eq!(msg, "unknown user name: \"non_existant\""); 289 | 290 | assert_eq!(name_to_uid("500").ok(), Some(500)); 291 | 292 | #[cfg(target_os = "macos")] 293 | assert_eq!(name_to_uid("_spotlight").ok(), Some(89)); 294 | 295 | #[cfg(any(target_os = "linux", target_os = "freebsd"))] 296 | { 297 | let (user_id, _) = getent("daemon"); 298 | assert_eq!(name_to_uid("daemon").ok(), Some(user_id)); 299 | } 300 | } 301 | 302 | #[test] 303 | fn test_name_to_gid() { 304 | let msg = name_to_gid("").unwrap_err().to_string(); 305 | assert_eq!(msg, "unknown group name: \"\""); 306 | 307 | let msg = name_to_gid("non_existant").unwrap_err().to_string(); 308 | assert_eq!(msg, "unknown group name: \"non_existant\""); 309 | 310 | assert_eq!(name_to_gid("500").ok(), Some(500)); 311 | 312 | #[cfg(target_os = "macos")] 313 | assert_eq!(name_to_gid("_spotlight").ok(), Some(89)); 314 | 315 | #[cfg(any(target_os = "linux", target_os = "freebsd"))] 316 | { 317 | let (_, group_id) = getent("daemon"); 318 | assert_eq!(name_to_gid("daemon").ok(), Some(group_id)); 319 | } 320 | } 321 | 322 | #[test] 323 | fn test_uid_to_name() { 324 | assert_eq!(uid_to_name(1500).unwrap(), "1500"); 325 | 326 | #[cfg(target_os = "macos")] 327 | assert_eq!(uid_to_name(89).unwrap(), "_spotlight"); 328 | 329 | #[cfg(any(target_os = "linux", target_os = "freebsd"))] 330 | { 331 | let (user_id, _) = getent("daemon"); 332 | assert_eq!(uid_to_name(user_id).unwrap(), "daemon"); 333 | } 334 | } 335 | 336 | #[test] 337 | fn test_gid_to_name() { 338 | assert_eq!(gid_to_name(1500).unwrap(), "1500"); 339 | 340 | #[cfg(target_os = "macos")] 341 | assert_eq!(gid_to_name(89).unwrap(), "_spotlight"); 342 | 343 | #[cfg(any(target_os = "linux", target_os = "freebsd"))] 344 | { 345 | let (_, group_id) = getent("daemon"); 346 | assert_eq!(gid_to_name(group_id).unwrap(), "daemon"); 347 | } 348 | } 349 | 350 | #[test] 351 | #[cfg(target_os = "macos")] 352 | fn test_uid_to_guid() { 353 | assert_eq!( 354 | uid_to_guid(89).ok(), 355 | Some(Uuid::parse_str("ffffeeee-dddd-cccc-bbbb-aaaa00000059").unwrap()) 356 | ); 357 | 358 | assert_eq!( 359 | uid_to_guid(1500).ok(), 360 | Some(Uuid::parse_str("ffffeeee-dddd-cccc-bbbb-aaaa000005dc").unwrap()) 361 | ); 362 | } 363 | 364 | #[test] 365 | #[cfg(target_os = "macos")] 366 | fn test_gid_to_guid() { 367 | assert_eq!( 368 | gid_to_guid(89).ok(), 369 | Some(Uuid::parse_str("abcdefab-cdef-abcd-efab-cdef00000059").unwrap()) 370 | ); 371 | 372 | assert_eq!( 373 | gid_to_guid(1500).ok(), 374 | Some(Uuid::parse_str("aaaabbbb-cccc-dddd-eeee-ffff000005dc").unwrap()) 375 | ); 376 | 377 | assert_eq!( 378 | gid_to_guid(20).ok(), 379 | Some(Uuid::parse_str("abcdefab-cdef-abcd-efab-cdef00000014").unwrap()) 380 | ); 381 | } 382 | 383 | #[test] 384 | #[cfg(target_os = "macos")] 385 | fn test_guid_to_id() { 386 | assert_eq!( 387 | guid_to_id(Uuid::parse_str("ffffeeee-dddd-cccc-bbbb-aaaa00000059").unwrap()).unwrap(), 388 | (Some(89), None) 389 | ); 390 | 391 | assert_eq!( 392 | guid_to_id(Uuid::parse_str("ffffeeee-dddd-cccc-bbbb-aaaa000005dc").unwrap()).unwrap(), 393 | (Some(1500), None) 394 | ); 395 | 396 | assert_eq!( 397 | guid_to_id(Uuid::parse_str("abcdefab-cdef-abcd-efab-cdef00000059").unwrap()).unwrap(), 398 | (None, Some(89)) 399 | ); 400 | 401 | assert_eq!( 402 | guid_to_id(Uuid::parse_str("aaaabbbb-cccc-dddd-eeee-ffff000005dc").unwrap()).unwrap(), 403 | (None, Some(1500)) 404 | ); 405 | 406 | assert_eq!( 407 | guid_to_id(Uuid::parse_str("abcdefab-cdef-abcd-efab-cdef00000014").unwrap()).unwrap(), 408 | (None, Some(20)) 409 | ); 410 | 411 | assert_eq!(guid_to_id(Uuid::nil()).unwrap(), (None, None)); 412 | } 413 | } 414 | -------------------------------------------------------------------------------- /src/util/mod.rs: -------------------------------------------------------------------------------- 1 | //! Provides a cross-platform, minimal ACL API. 2 | //! 3 | //! Types: 4 | //! `acl_t` 5 | //! `acl_entry_t` 6 | //! 7 | //! Functions: 8 | //! `xacl_init` - create a new empty ACL 9 | //! `xacl_free` - destroy ACL 10 | //! `xacl_foreach` - apply a function to each entry in an ACL 11 | //! `xacl_is_empty` - return true if an ACL is empty 12 | //! `xacl_is_posix` - return true if ACL has Posix.1e semantics. 13 | //! `xacl_add_entry` - append new entry to an ACL 14 | //! `xacl_get_entry` - retrieve contents from an ACL entry 15 | //! `xacl_get_file` - get ACL from file path 16 | //! `xacl_set_file` - set ACL for file path 17 | //! `xacl_is_nfs4` - return true if file path uses `NFSv4` ACL on `FreeBSD` 18 | 19 | mod util_common; 20 | 21 | #[cfg(target_os = "freebsd")] 22 | mod util_freebsd; 23 | 24 | #[cfg(target_os = "linux")] 25 | mod util_linux; 26 | 27 | #[cfg(target_os = "macos")] 28 | mod util_macos; 29 | 30 | // Re-export acl_entry_t and acl_t from crate::sys. 31 | pub use crate::sys::{acl_entry_t, acl_t}; 32 | 33 | #[cfg(target_os = "freebsd")] 34 | pub use util_freebsd::{ 35 | xacl_add_entry, xacl_foreach, xacl_free, xacl_get_entry, xacl_get_file, xacl_init, 36 | xacl_is_empty, xacl_is_nfs4, xacl_is_posix, xacl_set_file, 37 | }; 38 | 39 | #[cfg(target_os = "linux")] 40 | pub use util_linux::{ 41 | xacl_add_entry, xacl_foreach, xacl_free, xacl_get_entry, xacl_get_file, xacl_init, 42 | xacl_is_empty, xacl_is_posix, xacl_set_file, 43 | }; 44 | 45 | #[cfg(target_os = "macos")] 46 | pub use util_macos::{ 47 | xacl_add_entry, xacl_foreach, xacl_free, xacl_get_entry, xacl_get_file, xacl_init, 48 | xacl_is_empty, xacl_is_posix, xacl_set_file, 49 | }; 50 | -------------------------------------------------------------------------------- /src/util/util_common.rs: -------------------------------------------------------------------------------- 1 | use crate::bititer::BitIter; 2 | use crate::failx::*; 3 | use crate::perm::Perm; 4 | use crate::sys::*; 5 | 6 | use std::ffi::c_void; 7 | use std::io; 8 | use std::ptr; 9 | 10 | /// Free memory allocated by native acl_* routines. 11 | pub fn xacl_free(ptr: *mut T) { 12 | assert!(!ptr.is_null()); 13 | let ret = unsafe { acl_free(ptr.cast::()) }; 14 | assert_eq!(ret, 0); 15 | } 16 | 17 | /// Return true if acl is empty. 18 | pub fn xacl_is_empty(acl: acl_t) -> bool { 19 | let mut entry: acl_entry_t = ptr::null_mut(); 20 | 21 | !xacl_get_entry(acl, sg::ACL_FIRST_ENTRY, &mut entry) 22 | } 23 | 24 | /// Return next entry in ACL. 25 | fn xacl_get_entry(acl: acl_t, entry_id: i32, entry_p: *mut acl_entry_t) -> bool { 26 | let ret = unsafe { acl_get_entry(acl, entry_id, entry_p) }; 27 | 28 | // MacOS: Zero indicates success. 29 | #[cfg(target_os = "macos")] 30 | return ret == 0; 31 | 32 | // Linux, FreeBSD: One indicates success. 33 | #[cfg(any(target_os = "linux", target_os = "freebsd"))] 34 | return ret == 1; 35 | } 36 | 37 | /// Iterate over entries in a native ACL. 38 | pub fn xacl_foreach io::Result<()>>( 39 | acl: acl_t, 40 | mut func: F, 41 | ) -> io::Result<()> { 42 | let mut entry: acl_entry_t = ptr::null_mut(); 43 | let mut entry_id = sg::ACL_FIRST_ENTRY; 44 | 45 | assert!(!acl.is_null()); 46 | loop { 47 | if !xacl_get_entry(acl, entry_id, &mut entry) { 48 | break; 49 | } 50 | assert!(!entry.is_null()); 51 | func(entry)?; 52 | entry_id = sg::ACL_NEXT_ENTRY; 53 | } 54 | 55 | Ok(()) 56 | } 57 | 58 | /// Create a new empty ACL with the given capacity. 59 | /// 60 | /// Client must call `xacl_free` when done with result. 61 | pub fn xacl_init(capacity: usize) -> io::Result { 62 | let size = match i32::try_from(capacity) { 63 | Ok(size) if size <= sg::ACL_MAX_ENTRIES => size, 64 | _ => return fail_custom("Too many ACL entries"), 65 | }; 66 | 67 | let acl = unsafe { acl_init(size) }; 68 | if acl.is_null() { 69 | return fail_err("null", "acl_init", capacity); 70 | } 71 | 72 | Ok(acl) 73 | } 74 | 75 | /// Create a new entry in the specified ACL. 76 | /// 77 | /// N.B. Memory reallocation may cause `acl` ptr to change. 78 | pub fn xacl_create_entry(acl: &mut acl_t) -> io::Result { 79 | let mut entry: acl_entry_t = ptr::null_mut(); 80 | 81 | let ret = unsafe { acl_create_entry(&mut *acl, &mut entry) }; 82 | if ret != 0 { 83 | return fail_err(ret, "acl_create_entry", ()); 84 | } 85 | 86 | Ok(entry) 87 | } 88 | 89 | /// Get tag type from entry. 90 | pub fn xacl_get_tag_type(entry: acl_entry_t) -> io::Result { 91 | let mut tag: acl_tag_t = 0; 92 | 93 | let ret = unsafe { acl_get_tag_type(entry, &mut tag) }; 94 | if ret != 0 { 95 | return fail_err(ret, "acl_get_tag_type", ()); 96 | } 97 | 98 | Ok(tag) 99 | } 100 | 101 | /// Get permissions from the entry. 102 | pub fn xacl_get_perm(entry: acl_entry_t) -> io::Result { 103 | let mut permset: acl_permset_t = std::ptr::null_mut(); 104 | 105 | let ret = unsafe { acl_get_permset(entry, &mut permset) }; 106 | if ret != 0 { 107 | return fail_err(ret, "acl_get_permset", ()); 108 | } 109 | 110 | assert!(!permset.is_null()); 111 | 112 | let mut perms = Perm::empty(); 113 | for perm in BitIter(Perm::all()) { 114 | let res = unsafe { acl_get_perm(permset, perm.bits()) }; 115 | debug_assert!((0..=1).contains(&res)); 116 | if res == 1 { 117 | perms |= perm; 118 | } 119 | } 120 | 121 | Ok(perms) 122 | } 123 | 124 | /// Set tag type for ACL entry. 125 | pub fn xacl_set_tag_type(entry: acl_entry_t, tag: acl_tag_t) -> io::Result<()> { 126 | let ret = unsafe { acl_set_tag_type(entry, tag) }; 127 | if ret != 0 { 128 | return fail_err(ret, "acl_set_tag_type", ()); 129 | } 130 | 131 | Ok(()) 132 | } 133 | 134 | /// Set permissions for the entry. 135 | pub fn xacl_set_perm(entry: acl_entry_t, perms: Perm) -> io::Result<()> { 136 | let mut permset: acl_permset_t = std::ptr::null_mut(); 137 | 138 | let ret_get = unsafe { acl_get_permset(entry, &mut permset) }; 139 | if ret_get != 0 { 140 | return fail_err(ret_get, "acl_get_permset", ()); 141 | } 142 | 143 | assert!(!permset.is_null()); 144 | 145 | let ret_clear = unsafe { acl_clear_perms(permset) }; 146 | if ret_clear != 0 { 147 | return fail_err(ret_clear, "acl_clear_perms", ()); 148 | } 149 | 150 | for perm in BitIter(perms) { 151 | let ret = unsafe { acl_add_perm(permset, perm.bits()) }; 152 | debug_assert!(ret == 0); 153 | } 154 | 155 | Ok(()) 156 | } 157 | -------------------------------------------------------------------------------- /src/util/util_linux.rs: -------------------------------------------------------------------------------- 1 | use crate::failx::*; 2 | use crate::flag::Flag; 3 | use crate::perm::Perm; 4 | use crate::qualifier::Qualifier; 5 | use crate::sys::*; 6 | use crate::util::util_common; 7 | 8 | use scopeguard::defer; 9 | use std::ffi::{c_void, CString}; 10 | use std::io; 11 | use std::os::unix::ffi::OsStrExt; 12 | use std::path::Path; 13 | 14 | pub use util_common::{xacl_create_entry, xacl_foreach, xacl_free, xacl_init, xacl_is_empty}; 15 | 16 | use util_common::*; 17 | 18 | const fn get_acl_type(default_acl: bool) -> acl_type_t { 19 | if default_acl { 20 | sg::ACL_TYPE_DEFAULT 21 | } else { 22 | sg::ACL_TYPE_ACCESS 23 | } 24 | } 25 | 26 | pub fn xacl_get_file(path: &Path, symlink_acl: bool, default_acl: bool) -> io::Result { 27 | if symlink_acl { 28 | return fail_custom("Linux does not support symlinks with ACL's."); 29 | } 30 | 31 | let acl_type = get_acl_type(default_acl); 32 | let c_path = CString::new(path.as_os_str().as_bytes())?; 33 | let acl = unsafe { acl_get_file(c_path.as_ptr(), acl_type) }; 34 | 35 | if acl.is_null() { 36 | let func = if default_acl { 37 | "acl_get_file/default" 38 | } else { 39 | "acl_get_file/access" 40 | }; 41 | return fail_err("null", func, &c_path); 42 | } 43 | 44 | Ok(acl) 45 | } 46 | 47 | pub fn xacl_set_file( 48 | path: &Path, 49 | acl: acl_t, 50 | symlink_acl: bool, 51 | default_acl: bool, 52 | ) -> io::Result<()> { 53 | if symlink_acl { 54 | return fail_custom("Linux does not support symlinks with ACL's"); 55 | } 56 | 57 | let c_path = CString::new(path.as_os_str().as_bytes())?; 58 | let acl_type = get_acl_type(default_acl); 59 | let ret = unsafe { acl_set_file(c_path.as_ptr(), acl_type, acl) }; 60 | if ret != 0 { 61 | let func = if default_acl { 62 | "acl_set_file/default" 63 | } else { 64 | "acl_set_file/access" 65 | }; 66 | return fail_err(ret, func, &c_path); 67 | } 68 | 69 | Ok(()) 70 | } 71 | 72 | fn xacl_get_qualifier(entry: acl_entry_t) -> io::Result { 73 | let tag = xacl_get_tag_type(entry)?; 74 | 75 | let id = if tag == sg::ACL_USER || tag == sg::ACL_GROUP { 76 | let id_ptr = unsafe { acl_get_qualifier(entry).cast::() }; 77 | if id_ptr.is_null() { 78 | return fail_err("null", "acl_get_qualifier", ()); 79 | } 80 | defer! { xacl_free(id_ptr) }; 81 | Some(unsafe { *id_ptr }) 82 | } else { 83 | None 84 | }; 85 | 86 | let result = match tag { 87 | sg::ACL_USER => Qualifier::User(id.unwrap()), 88 | sg::ACL_GROUP => Qualifier::Group(id.unwrap()), 89 | sg::ACL_USER_OBJ => Qualifier::UserObj, 90 | sg::ACL_GROUP_OBJ => Qualifier::GroupObj, 91 | sg::ACL_OTHER => Qualifier::Other, 92 | sg::ACL_MASK => Qualifier::Mask, 93 | tag => Qualifier::Unknown(format!("@tag {tag}")), 94 | }; 95 | 96 | Ok(result) 97 | } 98 | 99 | fn xacl_get_tag_qualifier(_acl: acl_t, entry: acl_entry_t) -> io::Result<(bool, Qualifier)> { 100 | let qualifier = xacl_get_qualifier(entry)?; 101 | Ok((true, qualifier)) 102 | } 103 | 104 | #[allow(clippy::unnecessary_wraps)] 105 | const fn xacl_get_flags(_acl: acl_t, _entry: acl_entry_t) -> io::Result { 106 | Ok(Flag::empty()) // noop 107 | } 108 | 109 | pub fn xacl_get_entry(acl: acl_t, entry: acl_entry_t) -> io::Result<(bool, Qualifier, Perm, Flag)> { 110 | let (allow, qualifier) = xacl_get_tag_qualifier(acl, entry)?; 111 | let perms = xacl_get_perm(entry)?; 112 | let flags = xacl_get_flags(acl, entry)?; 113 | 114 | Ok((allow, qualifier, perms, flags)) 115 | } 116 | 117 | pub fn xacl_set_qualifier(entry: acl_entry_t, mut id: uid_t) -> io::Result<()> { 118 | let id_ptr = std::ptr::addr_of_mut!(id); 119 | 120 | let ret = unsafe { acl_set_qualifier(entry, id_ptr.cast::()) }; 121 | if ret != 0 { 122 | return fail_err(ret, "acl_set_qualifier", ()); 123 | } 124 | 125 | Ok(()) 126 | } 127 | 128 | pub fn xacl_set_tag_qualifier( 129 | entry: acl_entry_t, 130 | allow: bool, 131 | qualifier: &Qualifier, 132 | ) -> io::Result<()> { 133 | if !allow { 134 | return fail_custom("allow=false is not supported on Linux"); 135 | } 136 | 137 | match qualifier { 138 | Qualifier::User(uid) => { 139 | xacl_set_tag_type(entry, sg::ACL_USER)?; 140 | xacl_set_qualifier(entry, *uid)?; 141 | } 142 | Qualifier::Group(gid) => { 143 | xacl_set_tag_type(entry, sg::ACL_GROUP)?; 144 | xacl_set_qualifier(entry, *gid)?; 145 | } 146 | Qualifier::UserObj => { 147 | xacl_set_tag_type(entry, sg::ACL_USER_OBJ)?; 148 | } 149 | Qualifier::GroupObj => { 150 | xacl_set_tag_type(entry, sg::ACL_GROUP_OBJ)?; 151 | } 152 | Qualifier::Other => { 153 | xacl_set_tag_type(entry, sg::ACL_OTHER)?; 154 | } 155 | Qualifier::Mask => { 156 | xacl_set_tag_type(entry, sg::ACL_MASK)?; 157 | } 158 | Qualifier::Unknown(tag) => { 159 | return fail_custom(&format!("unknown tag: {tag}")); 160 | } 161 | } 162 | 163 | Ok(()) 164 | } 165 | 166 | #[allow(clippy::unnecessary_wraps)] 167 | const fn xacl_set_flags(_entry: acl_entry_t, _flags: Flag) -> io::Result<()> { 168 | Ok(()) // noop 169 | } 170 | 171 | pub fn xacl_add_entry( 172 | acl: &mut acl_t, 173 | allow: bool, 174 | qualifier: &Qualifier, 175 | perms: Perm, 176 | flags: Flag, 177 | ) -> io::Result { 178 | // Check for duplicates already in the list. 179 | xacl_foreach(*acl, |entry| { 180 | let (_, prev) = xacl_get_tag_qualifier(*acl, entry)?; 181 | if prev == *qualifier { 182 | let default = if flags.contains(Flag::DEFAULT) { 183 | "default " 184 | } else { 185 | "" 186 | }; 187 | fail_custom(&format!("duplicate {default}entry for \"{prev}\""))?; 188 | } 189 | Ok(()) 190 | })?; 191 | 192 | let entry = xacl_create_entry(acl)?; 193 | xacl_set_tag_qualifier(entry, allow, qualifier)?; 194 | xacl_set_perm(entry, perms)?; 195 | xacl_set_flags(entry, flags)?; 196 | 197 | Ok(entry) 198 | } 199 | 200 | pub const fn xacl_is_posix(_acl: acl_t) -> bool { 201 | true 202 | } 203 | 204 | #[cfg(test)] 205 | mod util_linux_test { 206 | use super::*; 207 | 208 | #[test] 209 | fn test_acl_api_misuse() { 210 | // Create empty list and add an entry. 211 | let mut acl = xacl_init(1).unwrap(); 212 | let entry = xacl_create_entry(&mut acl).unwrap(); 213 | 214 | // Setting tag other than 1 or 2 results in EINVAL error. 215 | let err = xacl_set_tag_type(entry, 0).unwrap_err(); 216 | assert_eq!(err.raw_os_error(), Some(sg::EINVAL)); 217 | 218 | // Setting qualifier without first setting tag to a valid value results in EINVAL. 219 | let err = xacl_set_qualifier(entry, 500).unwrap_err(); 220 | assert_eq!(err.raw_os_error(), Some(sg::EINVAL)); 221 | 222 | // Try to set entry using unknown qualifier -- this should fail. 223 | let err = 224 | xacl_set_tag_qualifier(entry, true, &Qualifier::Unknown("x".to_string())).unwrap_err(); 225 | assert!(err.to_string().contains("unknown tag: x")); 226 | 227 | // Add another entry and set it to a valid value. 228 | let entry2 = xacl_create_entry(&mut acl).unwrap(); 229 | xacl_set_tag_type(entry2, sg::ACL_USER_OBJ).unwrap(); 230 | 231 | xacl_free(acl); 232 | } 233 | 234 | #[test] 235 | fn test_empty_acl() { 236 | let file = tempfile::NamedTempFile::new().unwrap(); 237 | let dir = tempfile::TempDir::new().unwrap(); 238 | 239 | let acl = xacl_init(1).unwrap(); 240 | assert!(xacl_is_empty(acl)); 241 | 242 | // Empty acl is not "valid". 243 | let ret = unsafe { acl_valid(acl) }; 244 | assert_eq!(ret, -1); 245 | 246 | // Write an empty access ACL to a file. Still works? 247 | xacl_set_file(file.as_ref(), acl, false, false) 248 | .ok() 249 | .unwrap(); 250 | 251 | // Write an empty default ACL to a file. Still works? 252 | // FIXME: Fails on ubuntu-18.04. 253 | //xacl_set_file(file.as_ref(), acl, false, true).ok().unwrap(); 254 | 255 | // Write an empty access ACL to a directory. Still works? 256 | xacl_set_file(dir.as_ref(), acl, false, false).ok().unwrap(); 257 | 258 | // Write an empty default ACL to a directory. Okay on Linux, FreeBSD. 259 | xacl_set_file(dir.as_ref(), acl, false, true).ok().unwrap(); 260 | 261 | xacl_free(acl); 262 | } 263 | 264 | #[test] 265 | fn test_uninitialized_entry() { 266 | let mut acl = xacl_init(1).unwrap(); 267 | let entry_p = xacl_create_entry(&mut acl).unwrap(); 268 | 269 | let (allow, qualifier) = xacl_get_tag_qualifier(acl, entry_p).unwrap(); 270 | assert_eq!(qualifier.name().unwrap(), "@tag 0"); 271 | assert!(allow); 272 | 273 | xacl_free(acl); 274 | } 275 | } 276 | -------------------------------------------------------------------------------- /src/util/util_macos.rs: -------------------------------------------------------------------------------- 1 | use crate::bititer::BitIter; 2 | use crate::failx::*; 3 | use crate::flag::Flag; 4 | use crate::perm::Perm; 5 | use crate::qualifier::Qualifier; 6 | use crate::sys::*; 7 | use crate::util::util_common; 8 | 9 | use scopeguard::defer; 10 | use std::ffi::{c_void, CString}; 11 | use std::io; 12 | use std::os::unix::ffi::OsStrExt; 13 | use std::path::Path; 14 | use uuid::Uuid; 15 | 16 | pub use util_common::{xacl_create_entry, xacl_foreach, xacl_free, xacl_init, xacl_is_empty}; 17 | 18 | use util_common::*; 19 | 20 | /// Return true if path exists, even if it's a symlink to nowhere. 21 | fn path_exists(path: &Path, symlink_only: bool) -> bool { 22 | if symlink_only { 23 | path.symlink_metadata().is_ok() 24 | } else { 25 | path.exists() 26 | } 27 | } 28 | 29 | /// Get the native ACL for a specific file or directory. 30 | /// 31 | /// If the file is a symlink, the `symlink_acl` argument determines whether to 32 | /// get the ACL from the symlink itself (true) or the file it points to (false). 33 | pub fn xacl_get_file(path: &Path, symlink_acl: bool, default_acl: bool) -> io::Result { 34 | if default_acl { 35 | return fail_custom("macOS does not support default ACL"); 36 | } 37 | 38 | let c_path = CString::new(path.as_os_str().as_bytes())?; 39 | let acl = if symlink_acl { 40 | unsafe { acl_get_link_np(c_path.as_ptr(), acl_type_t_ACL_TYPE_EXTENDED) } 41 | } else { 42 | unsafe { acl_get_file(c_path.as_ptr(), acl_type_t_ACL_TYPE_EXTENDED) } 43 | }; 44 | 45 | if acl.is_null() { 46 | let func = if symlink_acl { 47 | "acl_get_link_np" 48 | } else { 49 | "acl_get_file" 50 | }; 51 | let err = log_err("null", func, &c_path); 52 | 53 | // acl_get_file et al. can return NULL (ENOENT) if the file exists, but 54 | // there is no ACL. If the path exists, return an *empty* ACL. 55 | if err.raw_os_error() == Some(sg::ENOENT) && path_exists(path, symlink_acl) { 56 | return xacl_init(1); 57 | } 58 | 59 | return Err(err); 60 | } 61 | 62 | Ok(acl) 63 | } 64 | 65 | /// Set the acl for a symlink using `acl_set_fd`. 66 | fn xacl_set_file_symlink_alt(c_path: &CString, acl: acl_t) -> io::Result<()> { 67 | let fd = unsafe { open(c_path.as_ptr(), sg::O_SYMLINK) }; 68 | if fd < 0 { 69 | return fail_err(fd, "open", c_path); 70 | } 71 | defer! { unsafe{ close(fd) }; } 72 | 73 | let ret = unsafe { acl_set_fd(fd, acl) }; 74 | if ret != 0 { 75 | return fail_err(ret, "acl_set_fd", fd); 76 | } 77 | 78 | Ok(()) 79 | } 80 | 81 | pub fn xacl_set_file( 82 | path: &Path, 83 | acl: acl_t, 84 | symlink_acl: bool, 85 | default_acl: bool, 86 | ) -> io::Result<()> { 87 | if default_acl { 88 | return fail_custom("macOS does not support default ACL"); 89 | } 90 | 91 | let c_path = CString::new(path.as_os_str().as_bytes())?; 92 | let ret = if symlink_acl { 93 | unsafe { acl_set_link_np(c_path.as_ptr(), acl_type_t_ACL_TYPE_EXTENDED, acl) } 94 | } else { 95 | unsafe { acl_set_file(c_path.as_ptr(), acl_type_t_ACL_TYPE_EXTENDED, acl) } 96 | }; 97 | 98 | if ret != 0 { 99 | let err = log_err(ret, "acl_set_link_np", &c_path); 100 | 101 | // acl_set_link_np() returns ENOTSUP for symlinks. Work-around this 102 | // by using acl_set_fd(). 103 | if err.raw_os_error() == Some(sg::ENOTSUP) && symlink_acl { 104 | return xacl_set_file_symlink_alt(&c_path, acl); 105 | } 106 | 107 | return Err(err); 108 | } 109 | 110 | Ok(()) 111 | } 112 | 113 | /// Get the GUID qualifier and resolve it to a User/Group if possible. 114 | /// 115 | /// Only call this function for `ACL_EXTENDED_ALLOW` or `ACL_EXTENDED_DENY`. 116 | fn xacl_get_qualifier(entry: acl_entry_t) -> io::Result { 117 | let uuid_ptr = unsafe { acl_get_qualifier(entry).cast::() }; 118 | if uuid_ptr.is_null() { 119 | return fail_err("null", "acl_get_qualifier", ()); 120 | } 121 | defer! { xacl_free(uuid_ptr) } 122 | 123 | let guid = unsafe { *uuid_ptr }; 124 | Qualifier::from_guid(guid) 125 | } 126 | 127 | /// Get tag and qualifier from the entry. 128 | fn xacl_get_tag_qualifier(_acl: acl_t, entry: acl_entry_t) -> io::Result<(bool, Qualifier)> { 129 | let tag = xacl_get_tag_type(entry)?; 130 | 131 | let result = match tag { 132 | sg::ACL_EXTENDED_ALLOW => (true, xacl_get_qualifier(entry)?), 133 | sg::ACL_EXTENDED_DENY => (false, xacl_get_qualifier(entry)?), 134 | _ => (false, Qualifier::Unknown(format!("@tag {tag}"))), 135 | }; 136 | 137 | Ok(result) 138 | } 139 | 140 | /// Get flags from the entry. 141 | fn xacl_get_flags_np(obj: *mut c_void) -> io::Result { 142 | assert!(!obj.is_null()); 143 | 144 | let mut flagset: acl_flagset_t = std::ptr::null_mut(); 145 | let ret = unsafe { acl_get_flagset_np(obj, &mut flagset) }; 146 | if ret != 0 { 147 | return fail_err(ret, "acl_get_flagset_np", ()); 148 | } 149 | 150 | assert!(!flagset.is_null()); 151 | 152 | let mut flags = Flag::empty(); 153 | for flag in BitIter(Flag::all()) { 154 | let res = unsafe { acl_get_flag_np(flagset, flag.bits()) }; 155 | debug_assert!((0..=1).contains(&res)); 156 | if res == 1 { 157 | flags |= flag; 158 | } 159 | } 160 | 161 | Ok(flags) 162 | } 163 | 164 | fn xacl_get_flags(_acl: acl_t, entry: acl_entry_t) -> io::Result { 165 | xacl_get_flags_np(entry.cast::()) 166 | } 167 | 168 | pub fn xacl_get_entry(acl: acl_t, entry: acl_entry_t) -> io::Result<(bool, Qualifier, Perm, Flag)> { 169 | let (allow, qualifier) = xacl_get_tag_qualifier(acl, entry)?; 170 | let perms = xacl_get_perm(entry)?; 171 | let flags = xacl_get_flags(acl, entry)?; 172 | 173 | Ok((allow, qualifier, perms, flags)) 174 | } 175 | 176 | /// Set qualifier for entry. 177 | /// 178 | /// Used in test. 179 | pub fn xacl_set_qualifier(entry: acl_entry_t, qualifier: &Qualifier) -> io::Result<()> { 180 | // Translate qualifier User/Group to guid. 181 | let mut bytes = qualifier.guid()?.into_bytes(); 182 | 183 | let ret = unsafe { acl_set_qualifier(entry, bytes.as_mut_ptr().cast::()) }; 184 | if ret != 0 { 185 | return fail_err(ret, "acl_set_qualifier", ()); 186 | } 187 | 188 | Ok(()) 189 | } 190 | 191 | /// Set tag and qualifier for ACL entry. 192 | fn xacl_set_tag_qualifier( 193 | entry: acl_entry_t, 194 | allow: bool, 195 | qualifier: &Qualifier, 196 | ) -> io::Result<()> { 197 | let tag = if let Qualifier::Unknown(_) = qualifier { 198 | debug_assert!(!allow); 199 | sg::ACL_EXTENDED_DENY 200 | } else if allow { 201 | sg::ACL_EXTENDED_ALLOW 202 | } else { 203 | sg::ACL_EXTENDED_DENY 204 | }; 205 | 206 | xacl_set_tag_type(entry, tag)?; 207 | xacl_set_qualifier(entry, qualifier)?; 208 | 209 | Ok(()) 210 | } 211 | 212 | fn xacl_set_flags_np(obj: *mut c_void, flags: Flag) -> io::Result<()> { 213 | assert!(!obj.is_null()); 214 | 215 | let mut flagset: acl_flagset_t = std::ptr::null_mut(); 216 | let ret_get = unsafe { acl_get_flagset_np(obj, &mut flagset) }; 217 | if ret_get != 0 { 218 | return fail_err(ret_get, "acl_get_flagset_np", ()); 219 | } 220 | 221 | assert!(!flagset.is_null()); 222 | 223 | let ret_clear = unsafe { acl_clear_flags_np(flagset) }; 224 | if ret_clear != 0 { 225 | return fail_err(ret_clear, "acl_clear_flags_np", ()); 226 | } 227 | 228 | for flag in BitIter(flags) { 229 | let ret = unsafe { acl_add_flag_np(flagset, flag.bits()) }; 230 | debug_assert!(ret == 0); 231 | } 232 | 233 | Ok(()) 234 | } 235 | 236 | fn xacl_set_flags(entry: acl_entry_t, flags: Flag) -> io::Result<()> { 237 | xacl_set_flags_np(entry.cast::(), flags) 238 | } 239 | 240 | pub fn xacl_add_entry( 241 | acl: &mut acl_t, 242 | allow: bool, 243 | qualifier: &Qualifier, 244 | perms: Perm, 245 | flags: Flag, 246 | ) -> io::Result { 247 | let entry = xacl_create_entry(acl)?; 248 | xacl_set_tag_qualifier(entry, allow, qualifier)?; 249 | xacl_set_perm(entry, perms)?; 250 | xacl_set_flags(entry, flags)?; 251 | 252 | Ok(entry) 253 | } 254 | 255 | pub const fn xacl_is_posix(_acl: acl_t) -> bool { 256 | false 257 | } 258 | 259 | //////////////////////////////////////////////////////////////////////////////// 260 | 261 | #[cfg(test)] 262 | mod util_macos_test { 263 | use super::*; 264 | 265 | #[test] 266 | fn test_acl_init() { 267 | use std::convert::TryInto; 268 | let max_entries: usize = ACL_MAX_ENTRIES.try_into().unwrap(); 269 | 270 | let acl = xacl_init(max_entries).ok().unwrap(); 271 | assert!(!acl.is_null()); 272 | xacl_free(acl); 273 | 274 | // Custom error if we try to allocate MAX_ENTRIES + 1. 275 | let err = xacl_init(max_entries + 1).unwrap_err(); 276 | assert_eq!(err.to_string(), "Too many ACL entries"); 277 | } 278 | 279 | #[test] 280 | fn test_acl_too_big() { 281 | let mut acl = xacl_init(3).ok().unwrap(); 282 | assert!(!acl.is_null()); 283 | 284 | for _ in 0..ACL_MAX_ENTRIES { 285 | xacl_create_entry(&mut acl).unwrap(); 286 | } 287 | 288 | // Memory error if we try to allocate MAX_ENTRIES + 1. 289 | let err = xacl_create_entry(&mut acl).unwrap_err(); 290 | assert_eq!(err.raw_os_error(), Some(sg::ENOMEM)); 291 | 292 | xacl_free(acl); 293 | } 294 | 295 | #[test] 296 | fn test_acl_api_misuse() { 297 | let mut acl = xacl_init(1).unwrap(); 298 | let entry = xacl_create_entry(&mut acl).unwrap(); 299 | 300 | // Setting tag other than 1 or 2 results in EINVAL error. 301 | let err = xacl_set_tag_type(entry, 0).unwrap_err(); 302 | assert_eq!(err.raw_os_error(), Some(sg::EINVAL)); 303 | 304 | // Setting qualifier without first setting tag to a valid value results in EINVAL. 305 | let err = xacl_set_qualifier(entry, &Qualifier::Guid(Uuid::nil())).unwrap_err(); 306 | assert_eq!(err.raw_os_error(), Some(sg::EINVAL)); 307 | 308 | let entry2 = xacl_create_entry(&mut acl).unwrap(); 309 | xacl_set_tag_type(entry2, 1).unwrap(); 310 | 311 | xacl_free(acl); 312 | } 313 | 314 | #[test] 315 | fn test_uninitialized_entry() { 316 | let mut acl = xacl_init(1).unwrap(); 317 | let entry_p = xacl_create_entry(&mut acl).unwrap(); 318 | 319 | let (allow, qualifier) = xacl_get_tag_qualifier(acl, entry_p).unwrap(); 320 | assert_eq!(qualifier.name().unwrap(), "@tag 0"); 321 | assert!(!allow); 322 | 323 | xacl_free(acl); 324 | } 325 | } 326 | -------------------------------------------------------------------------------- /tests/run_tests.sh: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | 3 | # Run all test suites. 4 | # 5 | # If run with `memcheck` argument, run all tests under valgrind. 6 | 7 | OS=$(uname -s | tr '[:upper:]' '[:lower:]') 8 | 9 | arg1="$1" 10 | script_dir=$(dirname "$0") 11 | cd "$script_dir" || exit 1 12 | 13 | if [ ! -f ../target/debug/examples/exacl ]; then 14 | echo "exacl executable not found!" 15 | exit 1 16 | fi 17 | 18 | unit_tests() { 19 | # Find executable files without file extensions. 20 | find ../target/debug/deps -type f -executable -print | grep -vE '\w+\.\w+$' 21 | } 22 | 23 | print_header() { 24 | # shellcheck disable=SC2046 25 | printf "\n%s\n%s\n" "$1" $(printf '=%.0s' $(seq 1 ${#1})) 26 | } 27 | 28 | exit_status=0 29 | 30 | if [ "$arg1" = "memcheck" ]; then 31 | # Enable memory check command and re-run unit tests under memcheck. 32 | export MEMCHECK="valgrind -q --error-exitcode=9 --leak-check=full --errors-for-leak-kinds=definite --suppressions=valgrind.supp --gen-suppressions=all" 33 | 34 | vers=$(valgrind --version) 35 | echo "Running tests with memcheck ($vers)" 36 | echo 37 | 38 | for test in $(unit_tests); do 39 | $MEMCHECK "$test" 40 | status=$? 41 | 42 | # Track if any memcheck returns a non-zero exit status. 43 | if [ $status -ne 0 ]; then 44 | exit_status=$status 45 | fi 46 | done 47 | fi 48 | 49 | for test in testsuite*_all.sh testsuite*_"$OS".sh; do 50 | if [ ! -f "$test" ]; then 51 | continue 52 | fi 53 | 54 | print_header "$test" 55 | ./"$test" 56 | status=$? 57 | 58 | # Track if any test returns a non-zero exit status. 59 | if [ $status -ne 0 ]; then 60 | exit_status=$status 61 | fi 62 | done 63 | 64 | # Run FreeBSD-specific tests. 65 | saved_tmp="$TMPDIR" 66 | for option in acls nfsv4acls; do 67 | if [ -d "/tmp/exacl_$option" ]; then 68 | export TMPDIR="/tmp/exacl_$option" 69 | for test in testsuite*_"$OS"_"$option".sh; do 70 | print_header "$test" 71 | ./"$test" 72 | status=$? 73 | 74 | # Track if any test returns a non-zero exit status. 75 | if [ $status -ne 0 ]; then 76 | exit_status=$status 77 | fi 78 | done 79 | fi 80 | done 81 | export TMPDIR="$saved_tmp" 82 | 83 | # Log non-zero exit status. 84 | if [ $exit_status -ne 0 ]; then 85 | echo "Exit Status: $exit_status" 86 | fi 87 | 88 | exit $exit_status 89 | -------------------------------------------------------------------------------- /tests/test_api.rs: -------------------------------------------------------------------------------- 1 | //! API Tests for exacl module. 2 | 3 | use ctor::ctor; 4 | use exacl::{getfacl, setfacl, AclEntry, AclOption, Perm}; 5 | use log::debug; 6 | use std::io; 7 | 8 | #[ctor] 9 | fn init() { 10 | env_logger::init(); 11 | } 12 | 13 | #[test] 14 | fn test_getfacl_file() -> io::Result<()> { 15 | let file = tempfile::NamedTempFile::new()?; 16 | let entries = getfacl(&file, None)?; 17 | 18 | #[cfg(target_os = "macos")] 19 | assert_eq!(entries.len(), 0); 20 | 21 | #[cfg(any(target_os = "linux", target_os = "freebsd"))] 22 | assert_eq!(entries.len(), 3); 23 | 24 | debug!("test_getfacl_file: {}", exacl::to_string(&entries)?); 25 | 26 | // Test default ACL on macOS (should fail). 27 | #[cfg(target_os = "macos")] 28 | { 29 | let result = getfacl(&file, AclOption::DEFAULT_ACL); 30 | assert!(result 31 | .unwrap_err() 32 | .to_string() 33 | .contains("macOS does not support default ACL")); 34 | } 35 | 36 | // Test default ACL (should be error; files don't have default ACL). 37 | #[cfg(target_os = "linux")] 38 | { 39 | let result = getfacl(&file, AclOption::DEFAULT_ACL); 40 | assert!(result 41 | .unwrap_err() 42 | .to_string() 43 | .contains("Permission denied")); 44 | } 45 | 46 | // Test default ACL (should be error; files don't have default ACL). 47 | #[cfg(target_os = "freebsd")] 48 | { 49 | let result = getfacl(&file, AclOption::DEFAULT_ACL); 50 | // If file is using NFSv4 ACL, the error message will be 51 | // "Default ACL not supported", otherwise the error message will be 52 | // "Invalid argument". 53 | let errmsg = result.unwrap_err().to_string(); 54 | assert!( 55 | errmsg.contains("Default ACL not supported") || errmsg.contains("Invalid argument") 56 | ); 57 | } 58 | 59 | Ok(()) 60 | } 61 | 62 | #[test] 63 | fn test_setfacl_file() -> io::Result<()> { 64 | let file = tempfile::NamedTempFile::new()?; 65 | let mut entries = getfacl(&file, None)?; 66 | 67 | entries.push(AclEntry::allow_user("500", Perm::READ, None)); 68 | setfacl(&[file], &entries, None)?; 69 | 70 | Ok(()) 71 | } 72 | 73 | /// Get the type of filesystem from `df -Th` command output. 74 | #[cfg(target_os = "linux")] 75 | fn get_filesystem(path: &std::path::PathBuf) -> String { 76 | let df = std::process::Command::new("df") 77 | .arg("-Th") 78 | .arg(path) 79 | .stdout(std::process::Stdio::piped()) 80 | .spawn() 81 | .expect("df is a valid unix command"); 82 | let sed = std::process::Command::new("sed") 83 | .arg("1d") 84 | .stdin(df.stdout.unwrap()) 85 | .stdout(std::process::Stdio::piped()) 86 | .spawn() 87 | .expect("sed is a valid unix command"); 88 | let tr = std::process::Command::new("tr") 89 | .arg("-s") 90 | .arg(" ") 91 | .stdin(sed.stdout.unwrap()) 92 | .stdout(std::process::Stdio::piped()) 93 | .spawn() 94 | .expect("tr is a valid unix command"); 95 | let cut = std::process::Command::new("cut") 96 | .arg("-d") 97 | .arg(" ") 98 | .arg("-f2") 99 | .stdin(tr.stdout.unwrap()) 100 | .output() 101 | .expect("cut is a valid unix command"); 102 | String::from_utf8(cut.stdout) 103 | .expect("FS should be valid utf8") 104 | .trim_end() 105 | .to_string() 106 | } 107 | 108 | #[test] 109 | #[cfg(target_os = "linux")] 110 | fn test_too_many_entries() -> io::Result<()> { 111 | use std::collections::HashMap; 112 | const UNTESTED: u32 = 65535; 113 | 114 | let path = std::env::temp_dir(); 115 | let fs = get_filesystem(&path); 116 | debug!("Running on filesystem: {{{}}} TMPDIR={:?}", fs, path); 117 | 118 | let supported_fs = HashMap::from([ 119 | ("brtfs", UNTESTED), 120 | // FIXME: xfs is not tested. -wwf 121 | // https://elixir.bootlin.com/linux/latest/source/fs/xfs/libxfs/xfs_format.h#L1809 122 | ("xfs", 5461), // max ext attr size = 64KB 123 | ("tmpfs", 8191), 124 | ("ext2", 507), 125 | ("ext3", 507), 126 | ("ext4", 507), 127 | ("gpfs", UNTESTED), 128 | ("nss", UNTESTED), 129 | ]); 130 | assert!( 131 | supported_fs.contains_key(fs.as_str()), 132 | "Not a supported filesystem: {fs}" 133 | ); 134 | let max_entries = supported_fs[fs.as_str()]; 135 | if max_entries == UNTESTED { 136 | debug!("Filesystem {} is not tested!", fs); 137 | } 138 | 139 | let mut entries = vec![ 140 | AclEntry::allow_user("", Perm::READ, None), 141 | AclEntry::allow_group("", Perm::READ, None), 142 | AclEntry::allow_other(Perm::empty(), None), 143 | AclEntry::allow_mask(Perm::READ, None), 144 | ]; 145 | let max_entries = max_entries.saturating_sub(u32::try_from(entries.len()).unwrap()); 146 | 147 | let offset = 500; 148 | for i in 0..max_entries { 149 | entries.push(AclEntry::allow_user( 150 | &(offset + i as usize).to_string(), 151 | Perm::READ, 152 | None, 153 | )); 154 | } 155 | 156 | let files = [tempfile::NamedTempFile::new_in(path)?]; 157 | debug!("Call setfacl with {} entries...", entries.len()); 158 | setfacl(&files, &entries, None)?; 159 | debug!("{} entries were added and it is okay", entries.len()); 160 | 161 | // Add last entry. 162 | entries.push(AclEntry::allow_user( 163 | (u32::MAX - 1).to_string().as_str(), 164 | Perm::READ, 165 | None, 166 | )); 167 | 168 | // Last entry is one too many. 169 | let err = setfacl(&files, &entries, None).unwrap_err(); 170 | debug!("Got error as expected: {}", err); 171 | assert!( 172 | err.to_string().contains("No space left on device") 173 | || err.to_string().contains("Argument list too long") 174 | ); 175 | 176 | Ok(()) 177 | } 178 | 179 | #[test] 180 | fn test_reader_writer() -> io::Result<()> { 181 | let input = r#" 182 | u:aaa:rwx#comment 183 | g:bbb:rwx 184 | u:ccc:rx 185 | "#; 186 | 187 | let entries = exacl::from_str(input)?; 188 | let actual = exacl::to_string(&entries)?; 189 | 190 | let expected = r#"allow::user:aaa:read,write,execute 191 | allow::group:bbb:read,write,execute 192 | allow::user:ccc:read,execute 193 | "#; 194 | assert_eq!(expected, actual); 195 | 196 | Ok(()) 197 | } 198 | 199 | #[test] 200 | #[cfg(any(target_os = "linux", target_os = "freebsd"))] 201 | fn test_exclusive_acloptions() { 202 | let path = "/tmp"; 203 | 204 | let err1 = getfacl(path, AclOption::ACCESS_ACL | AclOption::DEFAULT_ACL).unwrap_err(); 205 | assert_eq!( 206 | err1.to_string(), 207 | "ACCESS_ACL and DEFAULT_ACL are mutually exclusive options" 208 | ); 209 | 210 | let err2 = setfacl(&[path], &[], AclOption::ACCESS_ACL | AclOption::DEFAULT_ACL).unwrap_err(); 211 | assert_eq!( 212 | err2.to_string(), 213 | "ACCESS_ACL and DEFAULT_ACL are mutually exclusive options" 214 | ); 215 | } 216 | 217 | #[test] 218 | #[cfg(target_os = "macos")] 219 | fn test_exclusive_acloptions() { 220 | let path = "/tmp"; 221 | 222 | let err1 = getfacl(path, AclOption::ACCESS_ACL | AclOption::DEFAULT_ACL).unwrap_err(); 223 | assert_eq!( 224 | err1.to_string(), 225 | "File \"/tmp\": macOS does not support default ACL" 226 | ); 227 | 228 | let err2 = setfacl(&[path], &[], AclOption::ACCESS_ACL | AclOption::DEFAULT_ACL).unwrap_err(); 229 | assert_eq!( 230 | err2.to_string(), 231 | "File \"/tmp\": macOS does not support default ACL" 232 | ); 233 | } 234 | 235 | #[test] 236 | #[cfg(any(target_os = "linux", target_os = "freebsd"))] 237 | fn test_from_mode() { 238 | let acl_7777 = exacl::to_string(&exacl::from_mode(0o7777)).unwrap(); 239 | assert_eq!(acl_7777, "allow::user::read,write,execute\nallow::group::read,write,execute\nallow::other::read,write,execute\n"); 240 | 241 | let acl_000 = exacl::to_string(&exacl::from_mode(0o000)).unwrap(); 242 | assert_eq!(acl_000, "allow::user::\nallow::group::\nallow::other::\n"); 243 | 244 | let acl_123 = exacl::to_string(&exacl::from_mode(0o123)).unwrap(); 245 | assert_eq!( 246 | acl_123, 247 | "allow::user::execute\nallow::group::write\nallow::other::write,execute\n" 248 | ); 249 | 250 | let acl_12345 = exacl::to_string(&exacl::from_mode(0o12345)).unwrap(); 251 | assert_eq!( 252 | acl_12345, 253 | "allow::user::write,execute\nallow::group::read\nallow::other::read,execute\n" 254 | ); 255 | } 256 | -------------------------------------------------------------------------------- /tests/test_examples.rs: -------------------------------------------------------------------------------- 1 | //! Test example code used in documentation. 2 | 3 | use std::io; 4 | 5 | #[test] 6 | fn test_string_format() -> io::Result<()> { 7 | let file = tempfile::NamedTempFile::new()?; 8 | 9 | let acl = exacl::getfacl(&file, None)?; 10 | let result = exacl::to_string(&acl)?; 11 | println!("test_string_format: {result:?}"); 12 | 13 | Ok(()) 14 | } 15 | 16 | #[test] 17 | #[cfg(feature = "serde")] 18 | fn test_json_format() -> io::Result<()> { 19 | let file = tempfile::NamedTempFile::new()?; 20 | 21 | let acl = exacl::getfacl(&file, None)?; 22 | let result = serde_json::to_string(&acl)?; 23 | println!("test_json_format: {result:?}"); 24 | 25 | Ok(()) 26 | } 27 | 28 | #[test] 29 | #[cfg(any(target_os = "linux", target_os = "freebsd"))] 30 | fn test_linux_acl() -> io::Result<()> { 31 | use exacl::{AclEntry, Perm}; 32 | 33 | let mut acl = exacl::from_mode(0o660); 34 | acl.push(AclEntry::allow_user("fred", Perm::READ | Perm::WRITE, None)); 35 | 36 | assert_eq!( 37 | exacl::to_string(&acl)?, 38 | "allow::user::read,write\nallow::group::read,write\nallow::other::\nallow::user:fred:read,write\n" 39 | ); 40 | //exacl::setfacl(&["/tmp/file"], &acl, None)?; 41 | 42 | Ok(()) 43 | } 44 | 45 | #[test] 46 | #[cfg(any(target_os = "linux", target_os = "freebsd"))] 47 | fn test_linux_acl_default() -> io::Result<()> { 48 | use exacl::{AclEntry, Flag, Perm}; 49 | 50 | let mut acl = exacl::from_mode(0o770); 51 | acl.push(AclEntry::allow_group( 52 | "accounting", 53 | Perm::READ | Perm::WRITE | Perm::EXECUTE, 54 | None, 55 | )); 56 | 57 | // Make default_acl a copy of access_acl. 58 | let mut default_acl: Vec = acl.clone(); 59 | for entry in &mut default_acl { 60 | entry.flags |= Flag::DEFAULT; 61 | } 62 | acl.append(&mut default_acl); 63 | 64 | assert_eq!(exacl::to_string(&acl)?, "allow::user::read,write,execute\nallow::group::read,write,execute\nallow::other::\nallow::group:accounting:read,write,execute\nallow:default:user::read,write,execute\nallow:default:group::read,write,execute\nallow:default:other::\nallow:default:group:accounting:read,write,execute\n"); 65 | //exacl::setfacl(&["./tmp/dir"], &acl, None)?; 66 | 67 | Ok(()) 68 | } 69 | -------------------------------------------------------------------------------- /tests/testsuite_darwin.sh: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | 3 | # Basic test suite for exacl tool (Darwin/macOS). 4 | 5 | set -u -o pipefail 6 | 7 | EXACL='../target/debug/examples/exacl' 8 | 9 | ME=$(id -un) 10 | ME_NUM=$(id -u) 11 | MY_GROUP=$(id -gn) 12 | MY_GROUP_NUM=$(id -g) 13 | 14 | # Return true if file is readable. 15 | isReadable() { 16 | cat "$1" >/dev/null 2>&1 17 | return $? 18 | } 19 | 20 | # Return true if file is writable (tries to overwrite file). 21 | isWritable() { 22 | echo "x" 2>/dev/null >"$1" 23 | # shellcheck disable=SC2320 24 | return $? 25 | } 26 | 27 | # Return true if directory is readable. 28 | isReadableDir() { 29 | ls "$1" >/dev/null 2>&1 30 | return $? 31 | } 32 | 33 | # Return true if link is readable. 34 | isReadableLink() { 35 | readlink "$1" >/dev/null 2>&1 36 | return $? 37 | } 38 | 39 | # Put quotes back on JSON text. 40 | quotifyJson() { 41 | echo "$1" | sed -E -e 's/([A-Za-z0-9_-]+)/"\1"/g' -e 's/:"false"/:false/g' -e 's/:"true"/:true/g' 42 | } 43 | 44 | getAcl() { 45 | # shellcheck disable=SC2010 46 | ls -le "$1" 2>/dev/null | grep -E '^ \d+: ' 47 | } 48 | 49 | # Called by shunit2 before all tests run. 50 | oneTimeSetUp() { 51 | # Use temp directory managed by shunit2. 52 | DIR="$SHUNIT_TMPDIR" 53 | FILE1="$DIR/file1" 54 | DIR1="$DIR/dir1" 55 | LINK1="$DIR/link1" 56 | LINK2="$DIR/link2" 57 | 58 | # Create empty file, dir, and links. 59 | umask 077 60 | touch "$FILE1" 61 | mkdir "$DIR1" 62 | ln -s link1_to_nowhere "$LINK1" 63 | ln -s file1 "$LINK2" 64 | } 65 | 66 | testReadAclFromMissingFile() { 67 | msg=$($EXACL $DIR/non_existant 2>&1) 68 | assertEquals 1 $? 69 | assertEquals \ 70 | "File \"$DIR/non_existant\": No such file or directory (os error 2)" \ 71 | "$msg" 72 | } 73 | 74 | testReadAclForFile1() { 75 | msg=$($EXACL $FILE1) 76 | assertEquals 0 $? 77 | assertEquals "[]" "$msg" 78 | 79 | isReadable "$FILE1" && isWritable "$FILE1" 80 | assertEquals 0 $? 81 | 82 | # Add ACL entry for current user to "deny read". 83 | chmod +a "$ME deny read" "$FILE1" 84 | 85 | msg=$($EXACL $FILE1) 86 | assertEquals 0 $? 87 | assertEquals \ 88 | "[{kind:user,name:$ME,perms:[read],flags:[],allow:false}]" \ 89 | "${msg//\"/}" 90 | 91 | ! isReadable "$FILE1" && isWritable "$FILE1" 92 | assertEquals 0 $? 93 | 94 | # Remove user write perm. 95 | chmod u-w "$FILE1" 96 | ! isReadable "$FILE1" && ! isWritable "$FILE1" 97 | assertEquals 0 $? 98 | 99 | # Add ACL entry for current group to "allow write". 100 | chmod +a "$MY_GROUP allow write" "$FILE1" 101 | 102 | msg=$($EXACL $FILE1) 103 | assertEquals 0 $? 104 | assertEquals \ 105 | "[{kind:user,name:$ME,perms:[read],flags:[],allow:false},{kind:group,name:$MY_GROUP,perms:[write],flags:[],allow:true}]" \ 106 | "${msg//\"/}" 107 | 108 | ! isReadable "$FILE1" && isWritable "$FILE1" 109 | assertEquals 0 $? 110 | 111 | # Re-add user write perm that we removed above. Clear the ACL. 112 | chmod u+w "$FILE1" 113 | chmod -N "$FILE1" 114 | } 115 | 116 | testReadAclForDir1() { 117 | msg=$($EXACL $DIR1) 118 | assertEquals 0 $? 119 | assertEquals "[]" "$msg" 120 | 121 | # Add ACL entry for current user to "deny read" with inheritance flags. 122 | chmod +a "$ME deny read,file_inherit,directory_inherit,only_inherit" "$DIR1" 123 | 124 | msg=$($EXACL $DIR1) 125 | assertEquals 0 $? 126 | assertEquals \ 127 | "[{kind:user,name:$ME,perms:[read],flags:[file_inherit,directory_inherit,only_inherit],allow:false}]" \ 128 | "${msg//\"/}" 129 | 130 | isReadableDir "$DIR1" 131 | assertEquals 0 $? 132 | 133 | # Create subfile in DIR1. 134 | subfile="$DIR1/subfile" 135 | touch "$subfile" 136 | 137 | ! isReadable "$subfile" && isWritable "$subfile" 138 | assertEquals 0 $? 139 | 140 | msg=$($EXACL $subfile) 141 | assertEquals 0 $? 142 | assertEquals \ 143 | "[{kind:user,name:$ME,perms:[read],flags:[inherited],allow:false}]" \ 144 | "${msg//\"/}" 145 | 146 | # Create subdirectory in DIR1. 147 | subdir="$DIR1/subdir" 148 | mkdir "$subdir" 149 | 150 | msg=$($EXACL $subdir) 151 | assertEquals 0 $? 152 | assertEquals \ 153 | "[{kind:user,name:$ME,perms:[read],flags:[inherited,file_inherit,directory_inherit],allow:false}]" \ 154 | "${msg//\"/}" 155 | 156 | # Clear directory ACL's so we can delete them. 157 | chmod -a# 0 "$subdir" 158 | chmod -a# 0 "$DIR1" 159 | 160 | rmdir "$subdir" 161 | rm "$subfile" 162 | } 163 | 164 | testReadAclForLink1() { 165 | # Test symlink that goes nowhere. 166 | msg=$($EXACL $LINK1 2>&1) 167 | assertEquals 1 $? 168 | assertEquals "File \"$LINK1\": No such file or directory (os error 2)" "$msg" 169 | 170 | # Test symlink with no ACL. 171 | msg=$($EXACL --symlink $LINK1) 172 | assertEquals 0 $? 173 | assertEquals "[]" "$msg" 174 | 175 | # Add ACL entry for current user to "deny read". 176 | chmod -h +a "$ME deny read" "$LINK1" 177 | assertEquals 0 $? 178 | 179 | ! isReadableLink "$LINK1" 180 | assertEquals 0 $? 181 | 182 | msg=$($EXACL --symlink $LINK1) 183 | assertEquals 0 $? 184 | assertEquals \ 185 | "[{kind:user,name:$ME,perms:[read],flags:[],allow:false}]" \ 186 | "${msg//\"/}" 187 | 188 | # It appears that you can't further modify the ACL of a symbolic link if 189 | # you don't have 'read' access to the link anymore. 190 | msg=$(chmod -h -a# 0 "$LINK1" 2>&1) 191 | assertEquals 1 $? 192 | assertEquals \ 193 | "chmod: No ACL present '$LINK1' 194 | chmod: Failed to set ACL on file '$LINK1': Permission denied" \ 195 | "$msg" 196 | 197 | # Recreate the symlink here. 198 | ln -fs link1_to_nowhere "$LINK1" 199 | } 200 | 201 | testReadAclForLink2() { 202 | # Test symlink to file1. 203 | msg=$($EXACL $LINK2) 204 | assertEquals 0 $? 205 | assertEquals "[]" "$msg" 206 | 207 | # Add ACL entry for current user to "deny read". 208 | chmod +a "$ME deny read" "$LINK2" 209 | 210 | msg=$($EXACL $LINK2) 211 | assertEquals 0 $? 212 | assertEquals \ 213 | "[{kind:user,name:$ME,perms:[read],flags:[],allow:false}]" \ 214 | "${msg//\"/}" 215 | } 216 | 217 | testWriteAclToMissingFile() { 218 | input="[]" 219 | msg=$(echo "$input" | $EXACL --set $DIR/non_existant 2>&1) 220 | assertEquals 1 $? 221 | assertEquals \ 222 | "File \"$DIR/non_existant\": No such file or directory (os error 2)" \ 223 | "$msg" 224 | } 225 | 226 | testWriteAclToFile1() { 227 | # Set ACL to empty. 228 | input="[]" 229 | msg=$(echo "$input" | $EXACL --set $FILE1 2>&1) 230 | assertEquals 0 $? 231 | assertEquals "" "$msg" 232 | 233 | # Verify it's empty. 234 | msg=$($EXACL $FILE1) 235 | assertEquals 0 $? 236 | assertEquals "[]" "$msg" 237 | 238 | isReadable "$FILE1" && isWritable "$FILE1" 239 | assertEquals 0 $? 240 | 241 | # Set ACL for current user to "deny read". 242 | input=$(quotifyJson "[{kind:user,name:$ME,perms:[read],flags:[],allow:false}]") 243 | msg=$(echo "$input" | $EXACL --set $FILE1 2>&1) 244 | assertEquals 0 $? 245 | assertEquals "" "$msg" 246 | 247 | ! isReadable "$FILE1" && isWritable "$FILE1" 248 | assertEquals 0 $? 249 | 250 | # Check ACL using ls. 251 | msg=$(getAcl $FILE1) 252 | assertEquals \ 253 | " 0: user:$ME deny read" \ 254 | "$msg" 255 | 256 | # Check ACL again. 257 | msg=$($EXACL $FILE1) 258 | assertEquals 0 $? 259 | assertEquals \ 260 | "[{kind:user,name:$ME,perms:[read],flags:[],allow:false}]" \ 261 | "${msg//\"/}" 262 | } 263 | 264 | testWriteAclToDir1() { 265 | # Set ACL to empty. 266 | input="[]" 267 | msg=$(echo "$input" | $EXACL --set $DIR1 2>&1) 268 | assertEquals 0 $? 269 | assertEquals "" "$msg" 270 | 271 | # Verify it's empty. 272 | msg=$($EXACL $DIR1) 273 | assertEquals 0 $? 274 | assertEquals "[]" "$msg" 275 | 276 | isReadableDir "$DIR1" 277 | assertEquals 0 $? 278 | 279 | # Set ACL for current user to "deny read". 280 | input=$(quotifyJson "[{kind:user,name:$ME,perms:[read],flags:[],allow:false}]") 281 | msg=$(echo "$input" | $EXACL --set $DIR1 2>&1) 282 | assertEquals 0 $? 283 | assertEquals "" "$msg" 284 | 285 | ! isReadable "$DIR1" 286 | assertEquals 0 $? 287 | 288 | # Read ACL back. 289 | msg=$($EXACL $DIR1) 290 | assertEquals 0 $? 291 | assertEquals "$input" "$msg" 292 | } 293 | 294 | testWriteAclToLink1() { 295 | # Set ACL to empty. 296 | input="[]" 297 | msg=$(echo "$input" | $EXACL --symlink --set $LINK1 2>&1) 298 | assertEquals 0 $? 299 | assertEquals \ 300 | "" \ 301 | "$msg" 302 | 303 | isReadableLink "$LINK1" 304 | assertEquals 0 $? 305 | 306 | # Set ACL for current user to "deny read". 307 | input=$(quotifyJson "[{kind:user,name:$ME,perms:[read],flags:[],allow:false}]") 308 | msg=$(echo "$input" | $EXACL --symlink --set $LINK1 2>&1) 309 | assertEquals 0 $? 310 | assertEquals \ 311 | "" \ 312 | "$msg" 313 | 314 | ! isReadableLink "$LINK1" 315 | assertEquals 0 $? 316 | 317 | # Check ACL using ls. 318 | msg=$(getAcl $LINK1) 319 | assertEquals \ 320 | " 0: user:$ME deny read" \ 321 | "$msg" 322 | 323 | # Check ACL again. 324 | msg=$($EXACL --symlink $LINK1) 325 | assertEquals 0 $? 326 | assertEquals \ 327 | "[{kind:user,name:$ME,perms:[read],flags:[],allow:false}]" \ 328 | "${msg//\"/}" 329 | 330 | # Set ACL back to empty. We've removed READ permission for the link, so 331 | # this will fail. 332 | input="[]" 333 | msg=$(echo "$input" | $EXACL --symlink --set $LINK1 2>&1) 334 | assertEquals 1 $? 335 | assertEquals \ 336 | "File \"$LINK1\": Permission denied (os error 13)" \ 337 | "$msg" 338 | } 339 | 340 | testWriteAllFilePerms() { 341 | all="read,write,execute,delete,append,delete_child,readattr,writeattr,readextattr,writeextattr,readsecurity,writesecurity,chown,sync" 342 | input=$(quotifyJson "[{kind:user,name:$ME,perms:[$all],flags:[],allow:true}]") 343 | msg=$(echo "$input" | $EXACL --set $FILE1) 344 | assertEquals 0 $? 345 | assertEquals "" "$msg" 346 | 347 | msg=$($EXACL $FILE1) 348 | assertEquals 0 $? 349 | assertEquals \ 350 | "[{kind:user,name:$ME,perms:[$all],flags:[],allow:true}]" \ 351 | "${msg//\"/}" 352 | 353 | # ls output omits delete_child and sync. 354 | ls_perms="read,write,execute,delete,append,readattr,writeattr,readextattr,writeextattr,readsecurity,writesecurity,chown" 355 | msg=$(getAcl $FILE1) 356 | assertEquals \ 357 | " 0: user:$ME allow $ls_perms" \ 358 | "$msg" 359 | } 360 | 361 | testWriteAllFileFlags() { 362 | entry_flags="inherited,file_inherit,directory_inherit,limit_inherit,only_inherit" 363 | all="$entry_flags" 364 | input=$(quotifyJson "[{kind:user,name:$ME,perms:[read],flags:[$all],allow:true}]") 365 | msg=$(echo "$input" | $EXACL --set $FILE1) 366 | assertEquals 0 $? 367 | assertEquals "" "$msg" 368 | 369 | # N.B. "defer_inherit" flag is not returned. 370 | msg=$($EXACL $FILE1) 371 | assertEquals 0 $? 372 | assertEquals \ 373 | "[{kind:user,name:$ME,perms:[read],flags:[$entry_flags],allow:true}]" \ 374 | "${msg//\"/}" 375 | 376 | # ls output only shows inherited and limit_inherit. 377 | ls_perms="read,limit_inherit" 378 | msg=$(getAcl $FILE1) 379 | assertEquals \ 380 | " 0: user:$ME inherited allow $ls_perms" \ 381 | "$msg" 382 | } 383 | 384 | testWriteAllDirPerms() { 385 | all="read,write,execute,delete,append,delete_child,readattr,writeattr,readextattr,writeextattr,readsecurity,writesecurity,chown,sync" 386 | input=$(quotifyJson "[{kind:user,name:$ME,perms:[$all],flags:[],allow:true}]") 387 | msg=$(echo "$input" | $EXACL --set $DIR1) 388 | assertEquals 0 $? 389 | assertEquals "" "$msg" 390 | 391 | msg=$($EXACL $DIR1) 392 | assertEquals 0 $? 393 | assertEquals \ 394 | "[{kind:user,name:$ME,perms:[$all],flags:[],allow:true}]" \ 395 | "${msg//\"/}" 396 | } 397 | 398 | testWriteAllDirFlags() { 399 | entry_flags="inherited,file_inherit,directory_inherit,limit_inherit,only_inherit" 400 | all="$entry_flags" 401 | input=$(quotifyJson "[{kind:user,name:$ME,perms:[read],flags:[$all],allow:true}]") 402 | msg=$(echo "$input" | $EXACL --set $DIR1) 403 | assertEquals 0 $? 404 | assertEquals "" "$msg" 405 | 406 | # N.B. "defer_inherit" flag is not returned. 407 | msg=$($EXACL $DIR1) 408 | assertEquals 0 $? 409 | assertEquals \ 410 | "[{kind:user,name:$ME,perms:[read],flags:[$entry_flags],allow:true}]" \ 411 | "${msg//\"/}" 412 | } 413 | 414 | testWriteAclNumericUID() { 415 | # Set ACL for current user to "deny read". 416 | input=$(quotifyJson "[{kind:user,name:$ME_NUM,perms:[read],flags:[],allow:false}]") 417 | msg=$(echo "$input" | $EXACL --set $FILE1 2>&1) 418 | assertEquals 0 $? 419 | assertEquals "" "$msg" 420 | 421 | ! isReadable "$FILE1" && isWritable "$FILE1" 422 | assertEquals 0 $? 423 | 424 | # Check ACL again. 425 | msg=$($EXACL $FILE1) 426 | assertEquals 0 $? 427 | assertEquals \ 428 | "[{kind:user,name:$ME,perms:[read],flags:[],allow:false}]" \ 429 | "${msg//\"/}" 430 | } 431 | 432 | testWriteAclNumericGID() { 433 | # Set ACL for current group to "deny read". 434 | input=$(quotifyJson "[{kind:group,name:$MY_GROUP_NUM,perms:[read],flags:[],allow:false}]") 435 | msg=$(echo "$input" | $EXACL --set $FILE1 2>&1) 436 | assertEquals 0 $? 437 | assertEquals "" "$msg" 438 | 439 | ! isReadable "$FILE1" && isWritable "$FILE1" 440 | assertEquals 0 $? 441 | 442 | # Check ACL again. 443 | msg=$($EXACL $FILE1) 444 | assertEquals 0 $? 445 | assertEquals \ 446 | "[{kind:group,name:$MY_GROUP,perms:[read],flags:[],allow:false}]" \ 447 | "${msg//\"/}" 448 | } 449 | 450 | testWriteAclGUID() { 451 | # Set ACL for _spotlight group to "deny read" using GUID. 452 | spotlight_group="ABCDEFAB-CDEF-ABCD-EFAB-CDEF00000059" 453 | input=$(quotifyJson "[{kind:group,name:$spotlight_group,perms:[read],flags:[],allow:false}]") 454 | msg=$(echo "$input" | $EXACL --set $FILE1 2>&1) 455 | assertEquals 0 $? 456 | assertEquals "" "$msg" 457 | 458 | # Check ACL again. 459 | msg=$($EXACL $FILE1) 460 | assertEquals 0 $? 461 | assertEquals \ 462 | "[{kind:group,name:_spotlight,perms:[read],flags:[],allow:false}]" \ 463 | "${msg//\"/}" 464 | } 465 | 466 | testWriteAclGUID_nil() { 467 | # Set ACL for _spotlight group to "deny read" using GUID. 468 | nil_uuid="00000000-0000-0000-0000-000000000000" 469 | input=$(quotifyJson "[{kind:group,name:$nil_uuid,perms:[read],flags:[],allow:false}]") 470 | msg=$(echo "$input" | $EXACL --set $FILE1 2>&1) 471 | assertEquals 0 $? 472 | assertEquals "" "$msg" 473 | 474 | # Check ACL again. Note: change in kind. 475 | msg=$($EXACL $FILE1) 476 | assertEquals 0 $? 477 | assertEquals \ 478 | "[{kind:user,name:$nil_uuid,perms:[read],flags:[],allow:false}]" \ 479 | "${msg//\"/}" 480 | } 481 | 482 | testDefaultAclFails() { 483 | # Test that exacl returns an error; default acl not supported on macOS. 484 | msg=$($EXACL --default $DIR1 2>&1) 485 | assertEquals 1 $? 486 | assertEquals \ 487 | "File \"$DIR1\": macOS does not support default ACL" \ 488 | "$msg" 489 | 490 | msg=$(echo "[]" | $EXACL --set --default $DIR1 2>&1) 491 | assertEquals 1 $? 492 | assertEquals \ 493 | "File \"$DIR1\": macOS does not support default ACL" \ 494 | "$msg" 495 | } 496 | 497 | testMissingFlags() { 498 | input=$(quotifyJson "[{kind:user,name:501,perms:[execute],allow:true}]") 499 | msg=$(echo "$input" | $EXACL --set $DIR/non_existant 2>&1) 500 | assertEquals 1 $? 501 | assertEquals \ 502 | "File \"$DIR/non_existant\": No such file or directory (os error 2)" \ 503 | "${msg//\`/}" 504 | } 505 | 506 | testMissingAllow() { 507 | input=$(quotifyJson "[{kind:user,name:501,perms:[execute],flags:[]}]") 508 | msg=$(echo "$input" | $EXACL --set $DIR/non_existant 2>&1) 509 | assertEquals 1 $? 510 | assertEquals \ 511 | "File \"$DIR/non_existant\": No such file or directory (os error 2)" \ 512 | "${msg//\`/}" 513 | } 514 | 515 | # Duplicate entry is not an error on macOS. 516 | testDuplicateEntry() { 517 | input=$(quotifyJson "[{kind:user,name:501,perms:[execute],flags:[]},{kind:user,name:501,perms:[execute],flags:[]}]") 518 | msg=$(echo "$input" | $EXACL --set $DIR/non_existant 2>&1) 519 | assertEquals 1 $? 520 | assertEquals \ 521 | "File \"$DIR/non_existant\": No such file or directory (os error 2)" \ 522 | "${msg//\`/}" 523 | } 524 | 525 | # shellcheck disable=SC1091 526 | . shunit2 527 | -------------------------------------------------------------------------------- /tests/testsuite_malformed_all.sh: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | 3 | # Test suite that tests exacl tool with malformed input. 4 | 5 | set -u -o pipefail 6 | 7 | EXACL='../target/debug/examples/exacl' 8 | 9 | # Add memcheck command if defined. 10 | if [ -n "${MEMCHECK+x}" ]; then 11 | echo "# MEMCHECK=$MEMCHECK" 12 | EXACL="$MEMCHECK $EXACL" 13 | fi 14 | 15 | # Retrieve name of OS: "Darwin", "Linux", or "FreeBSD" 16 | CURRENT_OS=$(uname -s) 17 | 18 | # Put quotes back on JSON text. 19 | quotifyJson() { 20 | echo "$1" | sed -E -e 's/([@A-Za-z0-9_-]+)/"\1"/g' -e 's/:"false"/:false/g' -e 's/:"true"/:true/g' -e 's/:,/:"",/g' 21 | } 22 | 23 | testInvalidType() { 24 | input="{}" 25 | msg=$(echo "$input" | $EXACL --set non_existant 2>&1) 26 | assertEquals 1 $? 27 | assertEquals \ 28 | "JSON parser error: invalid type: map, expected a sequence at line 1 column 1" \ 29 | "$msg" 30 | 31 | input="[" 32 | msg=$(echo "$input" | $EXACL --set non_existant 2>&1) 33 | assertEquals 1 $? 34 | assertEquals \ 35 | "JSON parser error: EOF while parsing a list at line 2 column 0" \ 36 | "$msg" 37 | } 38 | 39 | testInvalidKind() { 40 | input=$(quotifyJson "[{kind:invalid,name:,perms:[execute],flags:[],allow:true}]") 41 | msg=$(echo "$input" | $EXACL --set non_existant 2>&1) 42 | assertEquals 1 $? 43 | 44 | if [ "$CURRENT_OS" = "Darwin" ]; then 45 | expected='user, group, unknown' 46 | elif [ "$CURRENT_OS" = "FreeBSD" ]; then 47 | expected='user, group, mask, other, everyone, unknown' 48 | else 49 | expected='user, group, mask, other, unknown' 50 | fi 51 | 52 | assertEquals \ 53 | "JSON parser error: unknown variant invalid, expected one of $expected at line 1 column 18" \ 54 | "${msg//\`/}" 55 | } 56 | 57 | testInvalidUser() { 58 | input=$(quotifyJson "[{kind:user,name:non_existant_user,perms:[execute],flags:[],allow:true}]") 59 | msg=$(echo "$input" | $EXACL --set non_existant 2>&1) 60 | assertEquals 1 $? 61 | assertEquals \ 62 | "Invalid ACL: entry 0: unknown user name: \"non_existant_user\"" \ 63 | "$msg" 64 | 65 | input=$(quotifyJson "[{kind:user,name:4294967296,perms:[execute],flags:[],allow:true}]") 66 | msg=$(echo "$input" | $EXACL --set non_existant 2>&1) 67 | assertEquals 1 $? 68 | assertEquals \ 69 | "Invalid ACL: entry 0: unknown user name: \"4294967296\"" \ 70 | "$msg" 71 | } 72 | 73 | testInvalidGroup() { 74 | input=$(quotifyJson "[{kind:group,name:non_existant_group,perms:[execute],flags:[],allow:true}]") 75 | msg=$(echo "$input" | $EXACL --set non_existant 2>&1) 76 | assertEquals 1 $? 77 | assertEquals \ 78 | "Invalid ACL: entry 0: unknown group name: \"non_existant_group\"" \ 79 | "$msg" 80 | 81 | input=$(quotifyJson "[{kind:group,name:4294967296,perms:[execute],flags:[],allow:true}]") 82 | msg=$(echo "$input" | $EXACL --set non_existant 2>&1) 83 | assertEquals 1 $? 84 | assertEquals \ 85 | "Invalid ACL: entry 0: unknown group name: \"4294967296\"" \ 86 | "$msg" 87 | } 88 | 89 | testInvalidGUID() { 90 | input=$(quotifyJson "[{kind:group,name:00000000-0000-0000-000-000000000000,perms:[execute],flags:[],allow:true}]") 91 | msg=$(echo "$input" | $EXACL --set non_existant 2>&1) 92 | assertEquals 1 $? 93 | assertEquals \ 94 | "Invalid ACL: entry 0: unknown group name: \"00000000-0000-0000-000-000000000000\"" \ 95 | "$msg" 96 | } 97 | 98 | testUnknownKind() { 99 | input=$(quotifyJson "[{kind:unknown,name:501,perms:[execute],flags:[],allow:true}]") 100 | msg=$(echo "$input" | $EXACL --set non_existant 2>&1) 101 | assertEquals 1 $? 102 | assertEquals \ 103 | 'Invalid ACL: entry 0: unsupported kind: "unknown"' \ 104 | "$msg" 105 | } 106 | 107 | testInvalidPerm() { 108 | input=$(quotifyJson "[{kind:user,name:501,perms:[whatever],flags:[],allow:true}]") 109 | msg=$(echo "$input" | $EXACL --set non_existant 2>&1 | sed -e 's/`//g') 110 | assertEquals 1 $? 111 | 112 | if [ "$CURRENT_OS" = "Darwin" ]; then 113 | expected='read, write, execute, delete, append, delete_child, readattr, writeattr, readextattr, writeextattr, readsecurity, writesecurity, chown, sync' 114 | elif [ "$CURRENT_OS" = "FreeBSD" ]; then 115 | expected='read, write, execute, read_data, write_data, delete, append, delete_child, readattr, writeattr, readextattr, writeextattr, readsecurity, writesecurity, chown, sync' 116 | else 117 | expected='read, write, execute' 118 | fi 119 | 120 | assertEquals \ 121 | "JSON parser error: unknown variant whatever, expected one of $expected at line 1 column 48" \ 122 | "${msg//\`/}" 123 | } 124 | 125 | testInvalidFlag() { 126 | input=$(quotifyJson "[{kind:user,name:501,perms:[execute],flags:[whatever],allow:true}]") 127 | msg=$(echo "$input" | $EXACL --set non_existant 2>&1 | sed -e 's/`//g') 128 | assertEquals 1 $? 129 | 130 | if [ "$CURRENT_OS" = "Darwin" ]; then 131 | expected='expected one of inherited, file_inherit, directory_inherit, limit_inherit, only_inherit' 132 | elif [ "$CURRENT_OS" = "FreeBSD" ]; then 133 | expected='expected one of inherited, file_inherit, directory_inherit, limit_inherit, only_inherit, default' 134 | else 135 | expected='expected default' 136 | fi 137 | 138 | assertEquals \ 139 | "JSON parser error: unknown variant whatever, $expected at line 1 column 68" \ 140 | "${msg//\`/}" 141 | } 142 | 143 | testExtraAttribute() { 144 | input=$(quotifyJson "[{kind:user,name:501,perms:[execute],flags:[],allow:true,ignore:0}]") 145 | msg=$(echo "$input" | $EXACL --set non_existant 2>&1) 146 | assertEquals 1 $? 147 | assertEquals \ 148 | 'JSON parser error: unknown field ignore, expected one of kind, name, perms, flags, allow at line 1 column 82' \ 149 | "${msg//\`/}" 150 | } 151 | 152 | testDuplicateAttribute() { 153 | input=$(quotifyJson "[{kind:user,name:501,perms:[execute],flags:[],allow:true,allow:false}]") 154 | msg=$(echo "$input" | $EXACL --set non_existant 2>&1) 155 | assertEquals 1 $? 156 | assertEquals \ 157 | 'JSON parser error: duplicate field allow at line 1 column 81' \ 158 | "${msg//\`/}" 159 | } 160 | 161 | testMisspelledAttribute() { 162 | input=$(quotifyJson "[{kin:user,name:501,perms:[execute],flags:[],allow:true}]") 163 | msg=$(echo "$input" | $EXACL --set non_existant 2>&1) 164 | assertEquals 1 $? 165 | assertEquals \ 166 | 'JSON parser error: unknown field kin, expected one of kind, name, perms, flags, allow at line 1 column 8' \ 167 | "${msg//\`/}" 168 | } 169 | 170 | testPermsInvalidType() { 171 | input=$(quotifyJson "[{kind:user,name:501,perms:0,flags:[],allow:true}]") 172 | msg=$(echo "$input" | $EXACL --set non_existant 2>&1) 173 | assertEquals 1 $? 174 | assertEquals \ 175 | 'JSON parser error: invalid type: string "0", expected list of permissions at line 1 column 40' \ 176 | "$msg" 177 | } 178 | 179 | testFlagsInvalidType() { 180 | input=$(quotifyJson "[{kind:user,name:501,perms:[read],flags:0,allow:true}]") 181 | msg=$(echo "$input" | $EXACL --set non_existant 2>&1) 182 | assertEquals 1 $? 183 | assertEquals \ 184 | 'JSON parser error: invalid type: string "0", expected list of flags at line 1 column 57' \ 185 | "$msg" 186 | } 187 | 188 | testInterleavedEntryIndex() { 189 | # Test that interleaved access/default entries produce the correct ACL index 190 | # when there's an unknown user name. Skip this test on MacOS. 191 | if [ "$CURRENT_OS" = "Darwin" ]; then 192 | return 0 193 | fi 194 | 195 | input=$(quotifyJson "[{kind:user,name:,perms:[write,read],flags:[],allow:true},{kind:user,name:,perms:[write,read],flags:[default],allow:true},{kind:group,name:,perms:[],flags:[],allow:true},{kind:group,name:,perms:[],flags:[default],allow:true},{kind:user,name:non_existant,perms:[],flags:[],allow:true},{kind:other,name:,perms:[],flags:[default],allow:true}]") 196 | msg=$(echo "$input" | $EXACL --set non_existant 2>&1) 197 | assertEquals 1 $? 198 | assertEquals \ 199 | 'Invalid ACL: entry 4: unknown user name: "non_existant"' \ 200 | "$msg" 201 | } 202 | 203 | testInvalidMask() { 204 | # Ignore test on macOS. 205 | if [ "$CURRENT_OS" = "Darwin" ]; then 206 | return 0 207 | fi 208 | 209 | input=$(quotifyJson "[{kind:mask,name:invalid,perms:[],flags:[],allow:true}]") 210 | msg=$(echo "$input" | $EXACL --set non_existant 2>&1) 211 | assertEquals 1 $? 212 | assertEquals \ 213 | "Invalid ACL: entry 0: unknown mask name: \"invalid\"" \ 214 | "$msg" 215 | } 216 | 217 | testInvalidOther() { 218 | # Ignore test on macOS. 219 | if [ "$CURRENT_OS" = "Darwin" ]; then 220 | return 0 221 | fi 222 | 223 | input=$(quotifyJson "[{kind:other,name:invalid,perms:[],flags:[],allow:true}]") 224 | msg=$(echo "$input" | $EXACL --set non_existant 2>&1) 225 | assertEquals 1 $? 226 | assertEquals \ 227 | "Invalid ACL: entry 0: unknown other name: \"invalid\"" \ 228 | "$msg" 229 | } 230 | 231 | testInvalidStdFormat() { 232 | input=$'group:a:read\nuser:x' 233 | msg=$(echo "$input" | $EXACL -f std --set non_existant 2>&1) 234 | assertEquals 1 $? 235 | assertEquals \ 236 | "Std parser error: Unknown ACL format: user:x" \ 237 | "${msg//\`/}" 238 | } 239 | 240 | # shellcheck disable=SC1091 241 | . shunit2 242 | -------------------------------------------------------------------------------- /tests/valgrind.supp: -------------------------------------------------------------------------------- 1 | # Valgrind suppression file. 2 | # 3 | # N.B. We need separate entries for statx(buf) and statx(file_name) because 4 | # glob "*" doesn't work in syscall name. 5 | 6 | # syscall, statx 7 | 8 | { 9 | statx(buf) points to unaddressable byte(s) 10 | Memcheck:Param 11 | statx(buf) 12 | fun:syscall 13 | fun:statx 14 | fun:_ZN3std3sys4unix2fs9try_statx* 15 | } 16 | 17 | { 18 | statx(file_name) points to unaddressable byte(s) 19 | Memcheck:Param 20 | statx(file_name) 21 | fun:syscall 22 | fun:statx 23 | fun:_ZN3std3sys4unix2fs9try_statx* 24 | } 25 | 26 | # statx, statx 27 | 28 | { 29 | statx(file_name) points to unaddressable byte(s) 30 | Memcheck:Param 31 | statx(file_name) 32 | fun:statx 33 | fun:statx 34 | fun:_ZN3std3sys4unix2fs9try_statx* 35 | } 36 | 37 | { 38 | statx(buf) points to unaddressable byte(s) 39 | Memcheck:Param 40 | statx(buf) 41 | fun:statx 42 | fun:statx 43 | fun:_ZN3std3sys4unix2fs9try_statx* 44 | } 45 | 46 | # Suppressions updated for rust 1.77.1. 47 | 48 | { 49 | Syscall param statx(file_name) points to unaddressable byte(s) 50 | Memcheck:Param 51 | statx(file_name) 52 | fun:statx 53 | fun:statx 54 | fun:_ZN3std3sys3pal4unix2fs9try_statx* 55 | } 56 | 57 | { 58 | Syscall param statx(buf) points to unaddressable byte(s) 59 | Memcheck:Param 60 | statx(buf) 61 | fun:statx 62 | fun:statx 63 | fun:_ZN3std3sys3pal4unix2fs9try_statx* 64 | } 65 | --------------------------------------------------------------------------------