exacl-0.10.0/.cargo_vcs_info.json0000644000000001360000000000100122060ustar { "git": { "sha1": "00373a7cd50252a3db9eaa6764634ff77f633adf" }, "path_in_vcs": "" }exacl-0.10.0/.cirrus.yml000064400000000000000000000033351046102023000131120ustar 00000000000000# Config file for cirrus-ci.org # Adapted from https://github.com/Stebalien/xattr task: name: CI / build (freebsd) matrix: - name: CI / build (freebsd-13.1) freebsd_instance: image_family: freebsd-13-1 - name: CI / build (freebsd-12.2) freebsd_instance: image_family: freebsd-12-2 sysinfo_script: # Record info about the test environment. - mount - df -h - sysctl hw.model hw.ncpu hw.physmem - freebsd-version # Create a 5 MB memory based FS with acls enabled and # mount it to a sub-directory of /tmp (where it won't # interfere with other uses of /tmp.) - mkdir /tmp/exacl_acls /tmp/exacl_nfsv4acls - mdmfs -o acls -s 5m md /tmp/exacl_acls - mdmfs -o nfsv4acls -s 5m md /tmp/exacl_nfsv4acls - mount - env - pkg info setup_script: # Install Rust. - pkg install -y bash llvm11 - curl https://sh.rustup.rs -sSf --output rustup.sh - sh rustup.sh -y # Install shunit2. - mkdir -p /tmp/bin - curl https://raw.githubusercontent.com/kward/shunit2/master/shunit2 -sSf --output /tmp/bin/shunit2 - chmod ugo+x /tmp/bin/shunit2 test_script: - . $HOME/.cargo/env # Set up path for shunit2. - export PATH="$PATH:/tmp/bin" - cargo fetch - cargo build # Build no-serde - cargo build --features serde # Build with serde - cargo test --no-run --features serde # Compile only # Run tests on our mem-based FS with acls. - export TMPDIR=/tmp/exacl_acls - cargo test --features serde - export TMPDIR=/tmp/exacl_nfsv4acls - cargo test --features serde - ./tests/run_tests.sh - export TMPDIR=/tmp - ./ci/bindgen.sh lint_script: - . $HOME/.cargo/env - ./ci/lint.sh exacl-0.10.0/.github/codecov.yml000064400000000000000000000002601046102023000145010ustar 00000000000000# Reference: # https://docs.codecov.io/docs/codecovyml-reference comment: false coverage: status: project: default: informational: true patch: off exacl-0.10.0/.github/dependabot.yml000064400000000000000000000003471046102023000151720ustar 00000000000000# Reference: # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates version: 2 updates: - package-ecosystem: "cargo" directory: "/" schedule: interval: "weekly" exacl-0.10.0/.github/workflows/ci.yml000064400000000000000000000030131046102023000155060ustar 00000000000000name: CI on: push: branches: [ "*" ] pull_request: branches: [ main ] schedule: # Every Saturday at 4:30 AM UTC. - cron: '30 4 * * 6' env: CARGO_TERM_COLOR: always jobs: build: runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: os: [ubuntu-22.04, macos-12, ubuntu-20.04, macos-11, ubuntu-18.04] steps: - name: Checkout uses: actions/checkout@v3 - name: Update Rust Toolchain run: rustup update - name: Install dependencies (macOS) run: brew install shunit2 shellcheck shfmt if: runner.os == 'macOS' - name: Install dependencies (Linux) run: | sudo apt-get update sudo apt-get -y install libacl1-dev acl shunit2 valgrind shellcheck if: runner.os == 'Linux' - name: Fetch run: cargo fetch - name: Build (no-serde) run: cargo build - name: Build (serde) run: cargo build --features serde - name: Unit Test (no-serde) run: cargo test - name: Unit Test (serde) run: cargo test --features serde - name: Run integration tests run: ./tests/run_tests.sh - name: Run memory tests (Linux) run: ./tests/run_tests.sh memcheck if: runner.os == 'Linux' && matrix.os != 'ubuntu-18.04' - name: Code coverage run: ./ci/coverage.sh codecov - name: Lint Check run: ./ci/lint.sh - name: Format Check run: ./ci/format.sh - name: Docs Check run: ./ci/docs.sh - name: Bindgen Check run: ./ci/bindgen.sh exacl-0.10.0/.github/workflows/docs.yml000064400000000000000000000007771046102023000160610ustar 00000000000000name: Docs on: push: branches: [ "main" ] env: CARGO_TERM_COLOR: always jobs: publish-docs: runs-on: ubuntu-20.04 steps: - name: Checkout uses: actions/checkout@v3 - name: Install dependencies (Linux) run: sudo apt-get -y install libacl1-dev - name: Build Docs run: ./ci/docs.sh - name: Publish Docs to Github Pages uses: peaceiris/actions-gh-pages@v3 with: github_token: ${{ secrets.GITHUB_TOKEN }} publish_dir: ./target/doc exacl-0.10.0/.github/workflows/publish.yml000064400000000000000000000005711046102023000165670ustar 00000000000000name: Publish on: release: types: [published] env: CARGO_TERM_COLOR: always jobs: publish-crate: runs-on: ubuntu-20.04 steps: - name: Checkout uses: actions/checkout@v3 - name: Install dependencies (Linux) run: sudo apt-get -y install libacl1-dev - name: Publish Crate run: cargo publish --token ${{ secrets.CRATES_TOKEN }} exacl-0.10.0/.gitignore000064400000000000000000000000451046102023000127650ustar 00000000000000/target Cargo.lock .DS_Store /.vscodeexacl-0.10.0/CHANGELOG.md000064400000000000000000000042371046102023000126150ustar 00000000000000# Changelog ## [0.10.0] - 2023-01-02 - Update version dependencies for `bindgen`, `clap`, and `env_logger`. - Include ubuntu-22.04, macos-12, and freebsd-13.1 in CI build. - Fix code coverage CI script to address GHA build issue. - Fix clippy warnings. ## [0.9.0] - 2022-06-08 - Fix compilation on various Linux architectures where `c_char` is signed (Issue #107). - Disable `layout_tests` option in `bindgen`. - Update version dependencies for `bindgen` and `uuid`. - Improve code coverage CI script. - Fix clippy warnings. ## [0.8.0] - 2022-02-03 - `serde` is now an optional dependency. Use `features = ["serde"]` to enable (Issue #95). - Remove the `num_enum` dependency (PR #94, contributed by bjorn3). - Update example code to use clap 3. ## [0.7.0] - 2021-12-25 - Add the `from_mode` top level function. - Remove `Acl` (low level interface) from the public exported API. - Remove dependency on the `nix` crate. - Update version dependencies for bindgen and env_logger. - Update Rust edition from 2018 to 2021. ## [0.6.0] - 2021-06-20 - Fix new rust clippy warnings. - Update version dependencies for bindgen and nix. - Update valgrind suppressions used in testing. ## [0.5.0] - 2021-02-22 - Add support for NFSv4 ACL's on `FreeBSD`. - Remove support for platform-specific text formats. ## [0.4.0] - 2021-01-13 - Add support for symbolic links on `FreeBSD`. - Add support for `ACCESS_ACL` option to `getfacl` and `setfacl`. - Allow for `-` in permission abbreviation, e.g. `r-x`. - Update rust toolchain to latest stable version and fix clippy/lint issues. - Fix package metadata for docs.rs; improve platform-specific documentation. ## [0.3.0] - 2021-01-02 - Add support for Posix.1e ACLs on `FreeBSD`. - Add `from_str` and `to_string` top-level functions. - Remove the `Acl::check` function from public API. ## [0.2.0] - 2020-12-22 - Implement buildtime_bindgen feature; use prebuilt bindings by default. - Implement `FromStr` and `Display` for `AclEntry`. - Add `to_writer` and `from_reader` top-level functions for parsing text. ## [0.1.1] - 2020-12-08 - Fix docs build on docs.rs by including platform bindings for macos and linux. ## [0.1.0] - 2020-12-06 Initial release. exacl-0.10.0/Cargo.lock0000644000000402330000000000100101630ustar # This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 3 [[package]] name = "aho-corasick" version = "0.7.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac" dependencies = [ "memchr", ] [[package]] name = "bindgen" version = "0.63.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "36d860121800b2a9a94f9b5604b332d5cffb234ce17609ea479d723dbc9d3885" dependencies = [ "bitflags", "cexpr", "clang-sys", "lazy_static", "lazycell", "log", "peeking_take_while", "proc-macro2", "quote", "regex", "rustc-hash", "shlex", "syn", "which", ] [[package]] name = "bitflags" version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "cc" version = "1.0.78" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a20104e2335ce8a659d6dd92a51a767a0c062599c73b343fd152cb401e828c3d" [[package]] name = "cexpr" version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" dependencies = [ "nom", ] [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "clang-sys" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa2e27ae6ab525c3d369ded447057bca5438d86dc3a68f6faafb8269ba82ebf3" dependencies = [ "glob", "libc", "libloading", ] [[package]] name = "clap" version = "4.0.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7db700bc935f9e43e88d00b0850dae18a63773cfbec6d8e070fccf7fef89a39" dependencies = [ "bitflags", "clap_derive", "clap_lex", "is-terminal", "once_cell", "strsim", "termcolor", ] [[package]] name = "clap_derive" version = "4.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0177313f9f02afc995627906bbd8967e2be069f5261954222dac78290c2b9014" dependencies = [ "heck", "proc-macro-error", "proc-macro2", "quote", "syn", ] [[package]] name = "clap_lex" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d4198f73e42b4936b35b5bb248d81d2b595ecb170da0bac7655c54eedfa8da8" dependencies = [ "os_str_bytes", ] [[package]] name = "ctor" version = "0.1.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6d2301688392eb071b0bf1a37be05c469d3cc4dbbd95df672fe28ab021e6a096" dependencies = [ "quote", "syn", ] [[package]] name = "either" version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797" [[package]] name = "env_logger" version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85cdab6a89accf66733ad5a1693a4dcced6aeff64602b634530dd73c1f3ee9f0" dependencies = [ "humantime", "is-terminal", "log", "regex", "termcolor", ] [[package]] name = "errno" version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1" dependencies = [ "errno-dragonfly", "libc", "winapi", ] [[package]] name = "errno-dragonfly" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" dependencies = [ "cc", "libc", ] [[package]] name = "exacl" version = "0.10.0" dependencies = [ "bindgen", "bitflags", "clap", "ctor", "env_logger", "log", "scopeguard", "serde", "serde_json", "tempfile", "uuid", ] [[package]] name = "fastrand" version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7a407cfaa3385c4ae6b23e84623d48c2798d06e3e6a1878f7f59f17b3f86499" dependencies = [ "instant", ] [[package]] name = "glob" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" [[package]] name = "heck" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" [[package]] name = "hermit-abi" version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7" dependencies = [ "libc", ] [[package]] name = "humantime" version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "instant" version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" dependencies = [ "cfg-if", ] [[package]] name = "io-lifetimes" version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46112a93252b123d31a119a8d1a1ac19deac4fac6e0e8b0df58f0d4e5870e63c" dependencies = [ "libc", "windows-sys", ] [[package]] name = "is-terminal" version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28dfb6c8100ccc63462345b67d1bbc3679177c75ee4bf59bf29c8b1d110b8189" dependencies = [ "hermit-abi", "io-lifetimes", "rustix", "windows-sys", ] [[package]] name = "itoa" version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fad582f4b9e86b6caa621cabeb0963332d92eea04729ab12892c2533951e6440" [[package]] name = "lazy_static" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "lazycell" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "libc" version = "0.2.139" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79" [[package]] name = "libloading" version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" dependencies = [ "cfg-if", "winapi", ] [[package]] name = "linux-raw-sys" version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f051f77a7c8e6957c0696eac88f26b0117e54f52d3fc682ab19397a8812846a4" [[package]] name = "log" version = "0.4.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" dependencies = [ "cfg-if", ] [[package]] name = "memchr" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" [[package]] name = "minimal-lexical" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "nom" version = "7.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5507769c4919c998e69e49c839d9dc6e693ede4cc4290d6ad8b41d4f09c548c" dependencies = [ "memchr", "minimal-lexical", ] [[package]] name = "once_cell" version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6f61fba1741ea2b3d6a1e3178721804bb716a68a6aeba1149b5d52e3d464ea66" [[package]] name = "os_str_bytes" version = "6.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b7820b9daea5457c9f21c69448905d723fbd21136ccf521748f23fd49e723ee" [[package]] name = "peeking_take_while" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" [[package]] name = "proc-macro-error" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" dependencies = [ "proc-macro-error-attr", "proc-macro2", "quote", "syn", "version_check", ] [[package]] name = "proc-macro-error-attr" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" dependencies = [ "proc-macro2", "quote", "version_check", ] [[package]] name = "proc-macro2" version = "1.0.49" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57a8eca9f9c4ffde41714334dee777596264c7825420f521abc92b5b5deb63a5" dependencies = [ "unicode-ident", ] [[package]] name = "quote" version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b" dependencies = [ "proc-macro2", ] [[package]] name = "redox_syscall" version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" dependencies = [ "bitflags", ] [[package]] name = "regex" version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e076559ef8e241f2ae3479e36f97bd5741c0330689e217ad51ce2c76808b868a" dependencies = [ "aho-corasick", "memchr", "regex-syntax", ] [[package]] name = "regex-syntax" version = "0.6.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848" [[package]] name = "remove_dir_all" version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" dependencies = [ "winapi", ] [[package]] name = "rustc-hash" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" [[package]] name = "rustix" version = "0.36.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4feacf7db682c6c329c4ede12649cd36ecab0f3be5b7d74e6a20304725db4549" dependencies = [ "bitflags", "errno", "io-lifetimes", "libc", "linux-raw-sys", "windows-sys", ] [[package]] name = "ryu" version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b4b9743ed687d4b4bcedf9ff5eaa7398495ae14e61cba0a295704edbc7decde" [[package]] name = "scopeguard" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" [[package]] name = "serde" version = "1.0.152" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bb7d1f0d3021d347a83e556fc4683dea2ea09d87bccdf88ff5c12545d89d5efb" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" version = "1.0.152" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af487d118eecd09402d70a5d72551860e788df87b464af30e5ea6a38c75c541e" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "serde_json" version = "1.0.91" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877c235533714907a8c2464236f5c4b2a17262ef1bd71f38f35ea592c8da6883" dependencies = [ "itoa", "ryu", "serde", ] [[package]] name = "shlex" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43b2853a4d09f215c24cc5489c992ce46052d359b5109343cbafbf26bc62f8a3" [[package]] name = "strsim" version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" [[package]] name = "syn" version = "1.0.107" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f4064b5b16e03ae50984a5a8ed5d4f8803e6bc1fd170a3cda91a1be4b18e3f5" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "tempfile" version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4" dependencies = [ "cfg-if", "fastrand", "libc", "redox_syscall", "remove_dir_all", "winapi", ] [[package]] name = "termcolor" version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" dependencies = [ "winapi-util", ] [[package]] name = "unicode-ident" version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc" [[package]] name = "uuid" version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "422ee0de9031b5b948b97a8fc04e3aa35230001a722ddd27943e0be31564ce4c" [[package]] name = "version_check" version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" [[package]] name = "which" version = "4.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c831fbbee9e129a8cf93e7747a82da9d95ba8e16621cae60ec2cdc849bacb7b" dependencies = [ "either", "libc", "once_cell", ] [[package]] name = "winapi" version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" dependencies = [ "winapi-i686-pc-windows-gnu", "winapi-x86_64-pc-windows-gnu", ] [[package]] name = "winapi-i686-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" dependencies = [ "winapi", ] [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows-sys" version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" dependencies = [ "windows_aarch64_gnullvm", "windows_aarch64_msvc", "windows_i686_gnu", "windows_i686_msvc", "windows_x86_64_gnu", "windows_x86_64_gnullvm", "windows_x86_64_msvc", ] [[package]] name = "windows_aarch64_gnullvm" version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41d2aa71f6f0cbe00ae5167d90ef3cfe66527d6f613ca78ac8024c3ccab9a19e" [[package]] name = "windows_aarch64_msvc" version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd0f252f5a35cac83d6311b2e795981f5ee6e67eb1f9a7f64eb4500fbc4dcdb4" [[package]] name = "windows_i686_gnu" version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fbeae19f6716841636c28d695375df17562ca208b2b7d0dc47635a50ae6c5de7" [[package]] name = "windows_i686_msvc" version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "84c12f65daa39dd2babe6e442988fc329d6243fdce47d7d2d155b8d874862246" [[package]] name = "windows_x86_64_gnu" version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf7b1b21b5362cbc318f686150e5bcea75ecedc74dd157d874d754a2ca44b0ed" [[package]] name = "windows_x86_64_gnullvm" version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09d525d2ba30eeb3297665bd434a54297e4170c7f1a44cad4ef58095b4cd2028" [[package]] name = "windows_x86_64_msvc" version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5" exacl-0.10.0/Cargo.toml0000644000000030710000000000100102050ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2021" name = "exacl" version = "0.10.0" authors = ["Bill Fisher "] description = "Manipulate file system access control lists (ACL) on macOS, Linux, and FreeBSD" documentation = "https://byllyfish.github.io/exacl" readme = "README.md" keywords = [ "acl", "access", "control", ] categories = ["filesystem"] license = "MIT" repository = "https://github.com/byllyfish/exacl" [package.metadata.docs.rs] rustc-args = [ "--cfg", "docsrs", ] [dependencies.bitflags] version = "1.2.1" [dependencies.log] version = "0.4.11" [dependencies.scopeguard] version = "1.1.0" [dependencies.serde] version = "1.0" features = ["derive"] optional = true [dependencies.uuid] version = "1.1.1" [dev-dependencies.clap] version = "4.0.23" features = ["derive"] [dev-dependencies.ctor] version = "0.1.16" [dev-dependencies.env_logger] version = "0.10.0" [dev-dependencies.serde_json] version = "1.0.59" [dev-dependencies.tempfile] version = "3.1.0" [build-dependencies.bindgen] version = "0.63.0" optional = true [features] buildtime_bindgen = ["bindgen"] default = [] exacl-0.10.0/Cargo.toml.orig000064400000000000000000000024411046102023000136660ustar 00000000000000[package] name = "exacl" version = "0.10.0" authors = ["Bill Fisher "] description = "Manipulate file system access control lists (ACL) on macOS, Linux, and FreeBSD" repository = "https://github.com/byllyfish/exacl" documentation = "https://byllyfish.github.io/exacl" license = "MIT" edition = "2021" keywords = ["acl", "access", "control"] categories = ["filesystem"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [features] # There are two optional features that you can enable: # - serde # - buildtime_bindgen default = [] # Use bindgen to build OS-specific bindings. # # On Linux, the bindings depend on the system header. This header # is only present on systems that have the `libacl1-dev` package installed. buildtime_bindgen = ["bindgen"] [dependencies] bitflags = "1.2.1" log = "0.4.11" uuid = "1.1.1" scopeguard = "1.1.0" serde = { version = "1.0", optional = true, features = ["derive"] } [build-dependencies] bindgen = { version = "0.63.0", optional = true } [dev-dependencies] tempfile = "3.1.0" ctor = "0.1.16" # Used by exacl.rs example. clap = { version = "4.0.23", features = ["derive"] } env_logger = "0.10.0" serde_json = "1.0.59" [package.metadata.docs.rs] rustc-args = ["--cfg", "docsrs"] exacl-0.10.0/LICENSE.txt000064400000000000000000000020521046102023000126200ustar 00000000000000Copyright (c) 2020-2023 William W. Fisher Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. exacl-0.10.0/README.md000064400000000000000000000071171046102023000122630ustar 00000000000000# Exacl [![CRATE]][crates] [![API]][docs] [![CI]][actions] [![BUILD]][cirrus] [![COV]][codecov] [CRATE]: https://img.shields.io/crates/v/exacl [crates]: https://crates.io/crates/exacl [CI]: https://github.com/byllyfish/exacl/workflows/CI/badge.svg [actions]: https://github.com/byllyfish/exacl/actions?query=branch%3Amain [API]: https://docs.rs/exacl/badge.svg [docs]: https://byllyfish.github.io/exacl [BUILD]: https://api.cirrus-ci.com/github/byllyfish/exacl.svg [cirrus]: https://cirrus-ci.com/github/byllyfish/exacl [COV]: https://codecov.io/gh/byllyfish/exacl/branch/main/graph/badge.svg?token=SWkSyVc1w6 [codecov]: https://codecov.io/gh/byllyfish/exacl Rust library to manipulate file system access control lists (ACL) on `macOS`, `Linux`, and `FreeBSD`. ## Example ```rust use exacl::{getfacl, setfacl, AclEntry, Perm}; // Get the ACL from "./tmp/foo". let mut acl = getfacl("./tmp/foo", None)?; // Print the contents of the ACL. for entry in &acl { println!("{entry}"); } // Add an ACL entry to the end. acl.push(AclEntry::allow_user("some_user", Perm::READ, None)); // Set the ACL for "./tmp/foo". setfacl(&["./tmp/foo"], &acl, None)?; ``` ## Benefits - Supports the Posix ACL's used by Linux and FreeBSD. - Supports the extended ACL's used by macOS and FreeBSD/NFSv4. - Supports reading/writing of ACL's as delimited text. - Supports serde (optional) for easy reading/writing of ACL's to JSON, YAML and other common formats. ## API This module provides two high level functions, `getfacl` and `setfacl`. - `getfacl` retrieves the ACL for a file or directory. - `setfacl` sets the ACL for files or directories. On Linux and FreeBSD, the ACL contains entries for the default ACL, if present. Both `getfacl` and `setfacl` work with a `Vec`. The `AclEntry` structure contains five fields: - kind : `AclEntryKind` - the kind of entry (User, Group, Other, Mask, or Unknown). - name : `String` - name of the principal being given access. You can use a user/group name, decimal uid/gid, or UUID (on macOS). - perms : `Perm` - permission bits for the entry. - flags : `Flag` - flags indicating whether an entry is inherited, etc. - allow : `bool` - true if entry is allowed; false means deny. Linux only supports allow=true. ## More Examples Here are some more examples showing how to use the library. Get an ACL in common delimited string format: ```rust let acl = exacl::getfacl("/tmp/file", None)?; let result = exacl::to_string(&acl)?; ``` Get an ACL in JSON format: ```rust let acl = exacl::getfacl("/tmp/file", None)?; let result = serde_json::to_string(&acl)?; ``` Create a linux ACL for permissions that allow the owning user and group to read/write a file but no one else except for "fred". ```rust let mut acl = exacl::from_mode(0o660); acl.push(AclEntry::allow_user("fred", Perm::READ | Perm::WRITE, None)); exacl::setfacl(&["/tmp/file"], &acl, None)?; ``` Create a linux ACL for directory permissions that gives full access to the owning user and group and read-only access to members of the accounting group. Any sub-directories created should automatically have the same ACL (via the default ACL). ```rust let mut acl = exacl::from_mode(0o770); acl.push(AclEntry::allow_group( "accounting", Perm::READ | Perm::EXECUTE, None, )); // Make default_acl a copy of access_acl with the DEFAULT flag set. let mut default_acl: Vec = acl.clone(); for entry in &mut default_acl { entry.flags |= Flag::DEFAULT; } acl.append(&mut default_acl); exacl::setfacl(&["./tmp/dir"], &acl, None)?; ``` exacl-0.10.0/bindgen/bindings_freebsd.rs000064400000000000000000000267751046102023000162620ustar 00000000000000pub const ENOENT: u32 = 2; pub const ENOMEM: u32 = 12; pub const EINVAL: u32 = 22; pub const ERANGE: u32 = 34; pub const ENOTSUP: u32 = 45; pub const ACL_MAX_ENTRIES: u32 = 254; pub const ACL_BRAND_UNKNOWN: u32 = 0; pub const ACL_BRAND_POSIX: u32 = 1; pub const ACL_BRAND_NFS4: u32 = 2; pub const ACL_UNDEFINED_TAG: u32 = 0; pub const ACL_USER_OBJ: u32 = 1; pub const ACL_USER: u32 = 2; pub const ACL_GROUP_OBJ: u32 = 4; pub const ACL_GROUP: u32 = 8; pub const ACL_MASK: u32 = 16; pub const ACL_OTHER: u32 = 32; pub const ACL_OTHER_OBJ: u32 = 32; pub const ACL_EVERYONE: u32 = 64; pub const ACL_ENTRY_TYPE_ALLOW: u32 = 256; pub const ACL_ENTRY_TYPE_DENY: u32 = 512; pub const ACL_ENTRY_TYPE_AUDIT: u32 = 1024; pub const ACL_ENTRY_TYPE_ALARM: u32 = 2048; pub const ACL_TYPE_ACCESS_OLD: u32 = 0; pub const ACL_TYPE_DEFAULT_OLD: u32 = 1; pub const ACL_TYPE_ACCESS: u32 = 2; pub const ACL_TYPE_DEFAULT: u32 = 3; pub const ACL_TYPE_NFS4: u32 = 4; pub const ACL_EXECUTE: u32 = 1; pub const ACL_WRITE: u32 = 2; pub const ACL_READ: u32 = 4; pub const ACL_PERM_NONE: u32 = 0; pub const ACL_PERM_BITS: u32 = 7; pub const ACL_POSIX1E_BITS: u32 = 7; pub const ACL_READ_DATA: u32 = 8; pub const ACL_LIST_DIRECTORY: u32 = 8; pub const ACL_WRITE_DATA: u32 = 16; pub const ACL_ADD_FILE: u32 = 16; pub const ACL_APPEND_DATA: u32 = 32; pub const ACL_ADD_SUBDIRECTORY: u32 = 32; pub const ACL_READ_NAMED_ATTRS: u32 = 64; pub const ACL_WRITE_NAMED_ATTRS: u32 = 128; pub const ACL_DELETE_CHILD: u32 = 256; pub const ACL_READ_ATTRIBUTES: u32 = 512; pub const ACL_WRITE_ATTRIBUTES: u32 = 1024; pub const ACL_DELETE: u32 = 2048; pub const ACL_READ_ACL: u32 = 4096; pub const ACL_WRITE_ACL: u32 = 8192; pub const ACL_WRITE_OWNER: u32 = 16384; pub const ACL_SYNCHRONIZE: u32 = 32768; pub const ACL_FULL_SET: u32 = 65529; pub const ACL_MODIFY_SET: u32 = 40953; pub const ACL_READ_SET: u32 = 4680; pub const ACL_WRITE_SET: u32 = 1200; pub const ACL_NFS4_PERM_BITS: u32 = 65529; pub const ACL_FIRST_ENTRY: u32 = 0; pub const ACL_NEXT_ENTRY: u32 = 1; pub const ACL_ENTRY_FILE_INHERIT: u32 = 1; pub const ACL_ENTRY_DIRECTORY_INHERIT: u32 = 2; pub const ACL_ENTRY_NO_PROPAGATE_INHERIT: u32 = 4; pub const ACL_ENTRY_INHERIT_ONLY: u32 = 8; pub const ACL_ENTRY_SUCCESSFUL_ACCESS: u32 = 16; pub const ACL_ENTRY_FAILED_ACCESS: u32 = 32; pub const ACL_ENTRY_INHERITED: u32 = 128; pub const ACL_FLAGS_BITS: u32 = 191; pub const ACL_TEXT_VERBOSE: u32 = 1; pub const ACL_TEXT_NUMERIC_IDS: u32 = 2; pub const ACL_TEXT_APPEND_ID: u32 = 4; pub const _PC_ACL_NFS4: u32 = 64; pub type __uint32_t = ::std::os::raw::c_uint; pub type __int64_t = ::std::os::raw::c_long; pub type __time_t = __int64_t; pub type __gid_t = __uint32_t; pub type __uid_t = __uint32_t; pub type gid_t = __gid_t; pub type time_t = __time_t; pub type uid_t = __uid_t; pub type acl_tag_t = u32; pub type acl_perm_t = u32; pub type acl_entry_type_t = u16; pub type acl_flag_t = u16; pub type acl_type_t = ::std::os::raw::c_int; pub type acl_permset_t = *mut ::std::os::raw::c_int; pub type acl_flagset_t = *mut u16; pub type acl_entry_t = *mut ::std::os::raw::c_void; pub type acl_t = *mut ::std::os::raw::c_void; extern "C" { pub fn acl_add_flag_np(_flagset_d: acl_flagset_t, _flag: acl_flag_t) -> ::std::os::raw::c_int; } extern "C" { pub fn acl_add_perm(_permset_d: acl_permset_t, _perm: acl_perm_t) -> ::std::os::raw::c_int; } extern "C" { pub fn acl_calc_mask(_acl_p: *mut acl_t) -> ::std::os::raw::c_int; } extern "C" { pub fn acl_clear_flags_np(_flagset_d: acl_flagset_t) -> ::std::os::raw::c_int; } extern "C" { pub fn acl_clear_perms(_permset_d: acl_permset_t) -> ::std::os::raw::c_int; } extern "C" { pub fn acl_copy_entry(_dest_d: acl_entry_t, _src_d: acl_entry_t) -> ::std::os::raw::c_int; } extern "C" { pub fn acl_copy_ext(_buf_p: *mut ::std::os::raw::c_void, _acl: acl_t, _size: isize) -> isize; } extern "C" { pub fn acl_copy_int(_buf_p: *const ::std::os::raw::c_void) -> acl_t; } extern "C" { pub fn acl_create_entry( _acl_p: *mut acl_t, _entry_p: *mut acl_entry_t, ) -> ::std::os::raw::c_int; } extern "C" { pub fn acl_create_entry_np( _acl_p: *mut acl_t, _entry_p: *mut acl_entry_t, _index: ::std::os::raw::c_int, ) -> ::std::os::raw::c_int; } extern "C" { pub fn acl_delete_entry(_acl: acl_t, _entry_d: acl_entry_t) -> ::std::os::raw::c_int; } extern "C" { pub fn acl_delete_entry_np(_acl: acl_t, _index: ::std::os::raw::c_int) -> ::std::os::raw::c_int; } extern "C" { pub fn acl_delete_fd_np( _filedes: ::std::os::raw::c_int, _type: acl_type_t, ) -> ::std::os::raw::c_int; } extern "C" { pub fn acl_delete_file_np( _path_p: *const ::std::os::raw::c_char, _type: acl_type_t, ) -> ::std::os::raw::c_int; } extern "C" { pub fn acl_delete_link_np( _path_p: *const ::std::os::raw::c_char, _type: acl_type_t, ) -> ::std::os::raw::c_int; } extern "C" { pub fn acl_delete_def_file(_path_p: *const ::std::os::raw::c_char) -> ::std::os::raw::c_int; } extern "C" { pub fn acl_delete_def_link_np(_path_p: *const ::std::os::raw::c_char) -> ::std::os::raw::c_int; } extern "C" { pub fn acl_delete_flag_np( _flagset_d: acl_flagset_t, _flag: acl_flag_t, ) -> ::std::os::raw::c_int; } extern "C" { pub fn acl_delete_perm(_permset_d: acl_permset_t, _perm: acl_perm_t) -> ::std::os::raw::c_int; } extern "C" { pub fn acl_dup(_acl: acl_t) -> acl_t; } extern "C" { pub fn acl_free(_obj_p: *mut ::std::os::raw::c_void) -> ::std::os::raw::c_int; } extern "C" { pub fn acl_from_text(_buf_p: *const ::std::os::raw::c_char) -> acl_t; } extern "C" { pub fn acl_get_brand_np( _acl: acl_t, _brand_p: *mut ::std::os::raw::c_int, ) -> ::std::os::raw::c_int; } extern "C" { pub fn acl_get_entry( _acl: acl_t, _entry_id: ::std::os::raw::c_int, _entry_p: *mut acl_entry_t, ) -> ::std::os::raw::c_int; } extern "C" { pub fn acl_get_fd(_fd: ::std::os::raw::c_int) -> acl_t; } extern "C" { pub fn acl_get_fd_np(fd: ::std::os::raw::c_int, _type: acl_type_t) -> acl_t; } extern "C" { pub fn acl_get_file(_path_p: *const ::std::os::raw::c_char, _type: acl_type_t) -> acl_t; } extern "C" { pub fn acl_get_entry_type_np( _entry_d: acl_entry_t, _entry_type_p: *mut acl_entry_type_t, ) -> ::std::os::raw::c_int; } extern "C" { pub fn acl_get_link_np(_path_p: *const ::std::os::raw::c_char, _type: acl_type_t) -> acl_t; } extern "C" { pub fn acl_get_qualifier(_entry_d: acl_entry_t) -> *mut ::std::os::raw::c_void; } extern "C" { pub fn acl_get_flag_np(_flagset_d: acl_flagset_t, _flag: acl_flag_t) -> ::std::os::raw::c_int; } extern "C" { pub fn acl_get_perm_np(_permset_d: acl_permset_t, _perm: acl_perm_t) -> ::std::os::raw::c_int; } extern "C" { pub fn acl_get_flagset_np( _entry_d: acl_entry_t, _flagset_p: *mut acl_flagset_t, ) -> ::std::os::raw::c_int; } extern "C" { pub fn acl_get_permset( _entry_d: acl_entry_t, _permset_p: *mut acl_permset_t, ) -> ::std::os::raw::c_int; } extern "C" { pub fn acl_get_tag_type( _entry_d: acl_entry_t, _tag_type_p: *mut acl_tag_t, ) -> ::std::os::raw::c_int; } extern "C" { pub fn acl_init(_count: ::std::os::raw::c_int) -> acl_t; } extern "C" { pub fn acl_set_fd(_fd: ::std::os::raw::c_int, _acl: acl_t) -> ::std::os::raw::c_int; } extern "C" { pub fn acl_set_fd_np( _fd: ::std::os::raw::c_int, _acl: acl_t, _type: acl_type_t, ) -> ::std::os::raw::c_int; } extern "C" { pub fn acl_set_file( _path_p: *const ::std::os::raw::c_char, _type: acl_type_t, _acl: acl_t, ) -> ::std::os::raw::c_int; } extern "C" { pub fn acl_set_entry_type_np( _entry_d: acl_entry_t, _entry_type: acl_entry_type_t, ) -> ::std::os::raw::c_int; } extern "C" { pub fn acl_set_link_np( _path_p: *const ::std::os::raw::c_char, _type: acl_type_t, _acl: acl_t, ) -> ::std::os::raw::c_int; } extern "C" { pub fn acl_set_flagset_np( _entry_d: acl_entry_t, _flagset_d: acl_flagset_t, ) -> ::std::os::raw::c_int; } extern "C" { pub fn acl_set_permset( _entry_d: acl_entry_t, _permset_d: acl_permset_t, ) -> ::std::os::raw::c_int; } extern "C" { pub fn acl_set_qualifier( _entry_d: acl_entry_t, _tag_qualifier_p: *const ::std::os::raw::c_void, ) -> ::std::os::raw::c_int; } extern "C" { pub fn acl_set_tag_type(_entry_d: acl_entry_t, _tag_type: acl_tag_t) -> ::std::os::raw::c_int; } extern "C" { pub fn acl_size(_acl: acl_t) -> isize; } extern "C" { pub fn acl_to_text(_acl: acl_t, _len_p: *mut isize) -> *mut ::std::os::raw::c_char; } extern "C" { pub fn acl_to_text_np( _acl: acl_t, _len_p: *mut isize, _flags: ::std::os::raw::c_int, ) -> *mut ::std::os::raw::c_char; } extern "C" { pub fn acl_valid(_acl: acl_t) -> ::std::os::raw::c_int; } extern "C" { pub fn acl_valid_fd_np( _fd: ::std::os::raw::c_int, _type: acl_type_t, _acl: acl_t, ) -> ::std::os::raw::c_int; } extern "C" { pub fn acl_valid_file_np( _path_p: *const ::std::os::raw::c_char, _type: acl_type_t, _acl: acl_t, ) -> ::std::os::raw::c_int; } extern "C" { pub fn acl_valid_link_np( _path_p: *const ::std::os::raw::c_char, _type: acl_type_t, _acl: acl_t, ) -> ::std::os::raw::c_int; } extern "C" { pub fn acl_is_trivial_np( _acl: acl_t, _trivialp: *mut ::std::os::raw::c_int, ) -> ::std::os::raw::c_int; } extern "C" { pub fn acl_strip_np(_acl: acl_t, recalculate_mask: ::std::os::raw::c_int) -> acl_t; } #[repr(C)] #[derive(Debug, Copy, Clone)] pub struct group { pub gr_name: *mut ::std::os::raw::c_char, pub gr_passwd: *mut ::std::os::raw::c_char, pub gr_gid: gid_t, pub gr_mem: *mut *mut ::std::os::raw::c_char, } extern "C" { pub fn getgrgid_r( arg1: gid_t, arg2: *mut group, arg3: *mut ::std::os::raw::c_char, arg4: usize, arg5: *mut *mut group, ) -> ::std::os::raw::c_int; } extern "C" { pub fn getgrnam_r( arg1: *const ::std::os::raw::c_char, arg2: *mut group, arg3: *mut ::std::os::raw::c_char, arg4: usize, arg5: *mut *mut group, ) -> ::std::os::raw::c_int; } #[repr(C)] #[derive(Debug, Copy, Clone)] pub struct passwd { pub pw_name: *mut ::std::os::raw::c_char, pub pw_passwd: *mut ::std::os::raw::c_char, pub pw_uid: uid_t, pub pw_gid: gid_t, pub pw_change: time_t, pub pw_class: *mut ::std::os::raw::c_char, pub pw_gecos: *mut ::std::os::raw::c_char, pub pw_dir: *mut ::std::os::raw::c_char, pub pw_shell: *mut ::std::os::raw::c_char, pub pw_expire: time_t, pub pw_fields: ::std::os::raw::c_int, } extern "C" { pub fn getpwnam_r( arg1: *const ::std::os::raw::c_char, arg2: *mut passwd, arg3: *mut ::std::os::raw::c_char, arg4: usize, arg5: *mut *mut passwd, ) -> ::std::os::raw::c_int; } extern "C" { pub fn getpwuid_r( arg1: uid_t, arg2: *mut passwd, arg3: *mut ::std::os::raw::c_char, arg4: usize, arg5: *mut *mut passwd, ) -> ::std::os::raw::c_int; } extern "C" { pub fn pathconf( arg1: *const ::std::os::raw::c_char, arg2: ::std::os::raw::c_int, ) -> ::std::os::raw::c_long; } extern "C" { pub fn lpathconf( arg1: *const ::std::os::raw::c_char, arg2: ::std::os::raw::c_int, ) -> ::std::os::raw::c_long; } exacl-0.10.0/bindgen/bindings_linux.rs000064400000000000000000000161541046102023000157750ustar 00000000000000pub const ENOENT: u32 = 2; pub const ENOMEM: u32 = 12; pub const EINVAL: u32 = 22; pub const ERANGE: u32 = 34; pub const ENOTSUP: u32 = 95; pub const ACL_READ: u32 = 4; pub const ACL_WRITE: u32 = 2; pub const ACL_EXECUTE: u32 = 1; pub const ACL_UNDEFINED_TAG: u32 = 0; pub const ACL_USER_OBJ: u32 = 1; pub const ACL_USER: u32 = 2; pub const ACL_GROUP_OBJ: u32 = 4; pub const ACL_GROUP: u32 = 8; pub const ACL_MASK: u32 = 16; pub const ACL_OTHER: u32 = 32; pub const ACL_TYPE_ACCESS: u32 = 32768; pub const ACL_TYPE_DEFAULT: u32 = 16384; pub const ACL_FIRST_ENTRY: u32 = 0; pub const ACL_NEXT_ENTRY: u32 = 1; pub const ACL_MULTI_ERROR: u32 = 4096; pub const ACL_DUPLICATE_ERROR: u32 = 8192; pub const ACL_MISS_ERROR: u32 = 12288; pub const ACL_ENTRY_ERROR: u32 = 16384; pub type __uid_t = ::std::os::raw::c_uint; pub type __gid_t = ::std::os::raw::c_uint; pub type __mode_t = ::std::os::raw::c_uint; pub type gid_t = __gid_t; pub type mode_t = __mode_t; pub type uid_t = __uid_t; #[repr(C)] #[derive(Debug, Copy, Clone)] pub struct __acl_ext { _unused: [u8; 0], } #[repr(C)] #[derive(Debug, Copy, Clone)] pub struct __acl_entry_ext { _unused: [u8; 0], } #[repr(C)] #[derive(Debug, Copy, Clone)] pub struct __acl_permset_ext { _unused: [u8; 0], } pub type acl_type_t = ::std::os::raw::c_uint; pub type acl_tag_t = ::std::os::raw::c_int; pub type acl_perm_t = ::std::os::raw::c_uint; pub type acl_t = *mut __acl_ext; pub type acl_entry_t = *mut __acl_entry_ext; pub type acl_permset_t = *mut __acl_permset_ext; extern "C" { pub fn acl_init(count: ::std::os::raw::c_int) -> acl_t; } extern "C" { pub fn acl_dup(acl: acl_t) -> acl_t; } extern "C" { pub fn acl_free(obj_p: *mut ::std::os::raw::c_void) -> ::std::os::raw::c_int; } extern "C" { pub fn acl_valid(acl: acl_t) -> ::std::os::raw::c_int; } extern "C" { pub fn acl_copy_entry(dest_d: acl_entry_t, src_d: acl_entry_t) -> ::std::os::raw::c_int; } extern "C" { pub fn acl_create_entry(acl_p: *mut acl_t, entry_p: *mut acl_entry_t) -> ::std::os::raw::c_int; } extern "C" { pub fn acl_delete_entry(acl: acl_t, entry_d: acl_entry_t) -> ::std::os::raw::c_int; } extern "C" { pub fn acl_get_entry( acl: acl_t, entry_id: ::std::os::raw::c_int, entry_p: *mut acl_entry_t, ) -> ::std::os::raw::c_int; } extern "C" { pub fn acl_add_perm(permset_d: acl_permset_t, perm: acl_perm_t) -> ::std::os::raw::c_int; } extern "C" { pub fn acl_calc_mask(acl_p: *mut acl_t) -> ::std::os::raw::c_int; } extern "C" { pub fn acl_clear_perms(permset_d: acl_permset_t) -> ::std::os::raw::c_int; } extern "C" { pub fn acl_delete_perm(permset_d: acl_permset_t, perm: acl_perm_t) -> ::std::os::raw::c_int; } extern "C" { pub fn acl_get_permset( entry_d: acl_entry_t, permset_p: *mut acl_permset_t, ) -> ::std::os::raw::c_int; } extern "C" { pub fn acl_set_permset(entry_d: acl_entry_t, permset_d: acl_permset_t) -> ::std::os::raw::c_int; } extern "C" { pub fn acl_get_qualifier(entry_d: acl_entry_t) -> *mut ::std::os::raw::c_void; } extern "C" { pub fn acl_get_tag_type( entry_d: acl_entry_t, tag_type_p: *mut acl_tag_t, ) -> ::std::os::raw::c_int; } extern "C" { pub fn acl_set_qualifier( entry_d: acl_entry_t, tag_qualifier_p: *const ::std::os::raw::c_void, ) -> ::std::os::raw::c_int; } extern "C" { pub fn acl_set_tag_type(entry_d: acl_entry_t, tag_type: acl_tag_t) -> ::std::os::raw::c_int; } extern "C" { pub fn acl_copy_ext(buf_p: *mut ::std::os::raw::c_void, acl: acl_t, size: isize) -> isize; } extern "C" { pub fn acl_copy_int(buf_p: *const ::std::os::raw::c_void) -> acl_t; } extern "C" { pub fn acl_from_text(buf_p: *const ::std::os::raw::c_char) -> acl_t; } extern "C" { pub fn acl_size(acl: acl_t) -> isize; } extern "C" { pub fn acl_to_text(acl: acl_t, len_p: *mut isize) -> *mut ::std::os::raw::c_char; } extern "C" { pub fn acl_delete_def_file(path_p: *const ::std::os::raw::c_char) -> ::std::os::raw::c_int; } extern "C" { pub fn acl_get_fd(fd: ::std::os::raw::c_int) -> acl_t; } extern "C" { pub fn acl_get_file(path_p: *const ::std::os::raw::c_char, type_: acl_type_t) -> acl_t; } extern "C" { pub fn acl_set_fd(fd: ::std::os::raw::c_int, acl: acl_t) -> ::std::os::raw::c_int; } extern "C" { pub fn acl_set_file( path_p: *const ::std::os::raw::c_char, type_: acl_type_t, acl: acl_t, ) -> ::std::os::raw::c_int; } extern "C" { pub fn acl_to_any_text( acl: acl_t, prefix: *const ::std::os::raw::c_char, separator: ::std::os::raw::c_char, options: ::std::os::raw::c_int, ) -> *mut ::std::os::raw::c_char; } extern "C" { pub fn acl_cmp(acl1: acl_t, acl2: acl_t) -> ::std::os::raw::c_int; } extern "C" { pub fn acl_check(acl: acl_t, last: *mut ::std::os::raw::c_int) -> ::std::os::raw::c_int; } extern "C" { pub fn acl_from_mode(mode: mode_t) -> acl_t; } extern "C" { pub fn acl_equiv_mode(acl: acl_t, mode_p: *mut mode_t) -> ::std::os::raw::c_int; } extern "C" { pub fn acl_extended_file(path_p: *const ::std::os::raw::c_char) -> ::std::os::raw::c_int; } extern "C" { pub fn acl_extended_file_nofollow( path_p: *const ::std::os::raw::c_char, ) -> ::std::os::raw::c_int; } extern "C" { pub fn acl_extended_fd(fd: ::std::os::raw::c_int) -> ::std::os::raw::c_int; } extern "C" { pub fn acl_entries(acl: acl_t) -> ::std::os::raw::c_int; } extern "C" { pub fn acl_error(code: ::std::os::raw::c_int) -> *const ::std::os::raw::c_char; } extern "C" { pub fn acl_get_perm(permset_d: acl_permset_t, perm: acl_perm_t) -> ::std::os::raw::c_int; } #[repr(C)] #[derive(Debug, Copy, Clone)] pub struct group { pub gr_name: *mut ::std::os::raw::c_char, pub gr_passwd: *mut ::std::os::raw::c_char, pub gr_gid: __gid_t, pub gr_mem: *mut *mut ::std::os::raw::c_char, } extern "C" { pub fn getgrgid_r( __gid: __gid_t, __resultbuf: *mut group, __buffer: *mut ::std::os::raw::c_char, __buflen: usize, __result: *mut *mut group, ) -> ::std::os::raw::c_int; } extern "C" { pub fn getgrnam_r( __name: *const ::std::os::raw::c_char, __resultbuf: *mut group, __buffer: *mut ::std::os::raw::c_char, __buflen: usize, __result: *mut *mut group, ) -> ::std::os::raw::c_int; } #[repr(C)] #[derive(Debug, Copy, Clone)] pub struct passwd { pub pw_name: *mut ::std::os::raw::c_char, pub pw_passwd: *mut ::std::os::raw::c_char, pub pw_uid: __uid_t, pub pw_gid: __gid_t, pub pw_gecos: *mut ::std::os::raw::c_char, pub pw_dir: *mut ::std::os::raw::c_char, pub pw_shell: *mut ::std::os::raw::c_char, } extern "C" { pub fn getpwuid_r( __uid: __uid_t, __resultbuf: *mut passwd, __buffer: *mut ::std::os::raw::c_char, __buflen: usize, __result: *mut *mut passwd, ) -> ::std::os::raw::c_int; } extern "C" { pub fn getpwnam_r( __name: *const ::std::os::raw::c_char, __resultbuf: *mut passwd, __buffer: *mut ::std::os::raw::c_char, __buflen: usize, __result: *mut *mut passwd, ) -> ::std::os::raw::c_int; } exacl-0.10.0/bindgen/bindings_macos.rs000064400000000000000000000266251046102023000157440ustar 00000000000000pub const ENOENT: u32 = 2; pub const ENOMEM: u32 = 12; pub const EINVAL: u32 = 22; pub const ERANGE: u32 = 34; pub const ENOTSUP: u32 = 45; pub const ACL_MAX_ENTRIES: u32 = 128; pub const O_SYMLINK: u32 = 2097152; pub const ID_TYPE_UID: u32 = 0; pub const ID_TYPE_GID: u32 = 1; pub type __uint32_t = ::std::os::raw::c_uint; pub type __darwin_time_t = ::std::os::raw::c_long; pub type u_int64_t = ::std::os::raw::c_ulonglong; pub type __darwin_gid_t = __uint32_t; pub type __darwin_id_t = __uint32_t; pub type __darwin_uid_t = __uint32_t; pub type __darwin_uuid_t = [::std::os::raw::c_uchar; 16usize]; pub type gid_t = __darwin_gid_t; pub type id_t = __darwin_id_t; pub type uid_t = __darwin_uid_t; pub const acl_perm_t_ACL_READ_DATA: acl_perm_t = 2; pub const acl_perm_t_ACL_LIST_DIRECTORY: acl_perm_t = 2; pub const acl_perm_t_ACL_WRITE_DATA: acl_perm_t = 4; pub const acl_perm_t_ACL_ADD_FILE: acl_perm_t = 4; pub const acl_perm_t_ACL_EXECUTE: acl_perm_t = 8; pub const acl_perm_t_ACL_SEARCH: acl_perm_t = 8; pub const acl_perm_t_ACL_DELETE: acl_perm_t = 16; pub const acl_perm_t_ACL_APPEND_DATA: acl_perm_t = 32; pub const acl_perm_t_ACL_ADD_SUBDIRECTORY: acl_perm_t = 32; pub const acl_perm_t_ACL_DELETE_CHILD: acl_perm_t = 64; pub const acl_perm_t_ACL_READ_ATTRIBUTES: acl_perm_t = 128; pub const acl_perm_t_ACL_WRITE_ATTRIBUTES: acl_perm_t = 256; pub const acl_perm_t_ACL_READ_EXTATTRIBUTES: acl_perm_t = 512; pub const acl_perm_t_ACL_WRITE_EXTATTRIBUTES: acl_perm_t = 1024; pub const acl_perm_t_ACL_READ_SECURITY: acl_perm_t = 2048; pub const acl_perm_t_ACL_WRITE_SECURITY: acl_perm_t = 4096; pub const acl_perm_t_ACL_CHANGE_OWNER: acl_perm_t = 8192; pub const acl_perm_t_ACL_SYNCHRONIZE: acl_perm_t = 1048576; pub type acl_perm_t = ::std::os::raw::c_uint; pub const acl_tag_t_ACL_UNDEFINED_TAG: acl_tag_t = 0; pub const acl_tag_t_ACL_EXTENDED_ALLOW: acl_tag_t = 1; pub const acl_tag_t_ACL_EXTENDED_DENY: acl_tag_t = 2; pub type acl_tag_t = ::std::os::raw::c_uint; pub const acl_type_t_ACL_TYPE_EXTENDED: acl_type_t = 256; pub const acl_type_t_ACL_TYPE_ACCESS: acl_type_t = 0; pub const acl_type_t_ACL_TYPE_DEFAULT: acl_type_t = 1; pub const acl_type_t_ACL_TYPE_AFS: acl_type_t = 2; pub const acl_type_t_ACL_TYPE_CODA: acl_type_t = 3; pub const acl_type_t_ACL_TYPE_NTFS: acl_type_t = 4; pub const acl_type_t_ACL_TYPE_NWFS: acl_type_t = 5; pub type acl_type_t = ::std::os::raw::c_uint; pub const acl_entry_id_t_ACL_FIRST_ENTRY: acl_entry_id_t = 0; pub const acl_entry_id_t_ACL_NEXT_ENTRY: acl_entry_id_t = -1; pub const acl_entry_id_t_ACL_LAST_ENTRY: acl_entry_id_t = -2; pub type acl_entry_id_t = ::std::os::raw::c_int; pub const acl_flag_t_ACL_FLAG_DEFER_INHERIT: acl_flag_t = 1; pub const acl_flag_t_ACL_FLAG_NO_INHERIT: acl_flag_t = 131072; pub const acl_flag_t_ACL_ENTRY_INHERITED: acl_flag_t = 16; pub const acl_flag_t_ACL_ENTRY_FILE_INHERIT: acl_flag_t = 32; pub const acl_flag_t_ACL_ENTRY_DIRECTORY_INHERIT: acl_flag_t = 64; pub const acl_flag_t_ACL_ENTRY_LIMIT_INHERIT: acl_flag_t = 128; pub const acl_flag_t_ACL_ENTRY_ONLY_INHERIT: acl_flag_t = 256; pub type acl_flag_t = ::std::os::raw::c_uint; #[repr(C)] #[derive(Debug, Copy, Clone)] pub struct _acl { _unused: [u8; 0], } #[repr(C)] #[derive(Debug, Copy, Clone)] pub struct _acl_entry { _unused: [u8; 0], } #[repr(C)] #[derive(Debug, Copy, Clone)] pub struct _acl_permset { _unused: [u8; 0], } #[repr(C)] #[derive(Debug, Copy, Clone)] pub struct _acl_flagset { _unused: [u8; 0], } pub type acl_t = *mut _acl; pub type acl_entry_t = *mut _acl_entry; pub type acl_permset_t = *mut _acl_permset; pub type acl_flagset_t = *mut _acl_flagset; pub type acl_permset_mask_t = u_int64_t; extern "C" { pub fn acl_dup(acl: acl_t) -> acl_t; } extern "C" { pub fn acl_free(obj_p: *mut ::std::os::raw::c_void) -> ::std::os::raw::c_int; } extern "C" { pub fn acl_init(count: ::std::os::raw::c_int) -> acl_t; } extern "C" { pub fn acl_copy_entry(dest_d: acl_entry_t, src_d: acl_entry_t) -> ::std::os::raw::c_int; } extern "C" { pub fn acl_create_entry(acl_p: *mut acl_t, entry_p: *mut acl_entry_t) -> ::std::os::raw::c_int; } extern "C" { pub fn acl_create_entry_np( acl_p: *mut acl_t, entry_p: *mut acl_entry_t, entry_index: ::std::os::raw::c_int, ) -> ::std::os::raw::c_int; } extern "C" { pub fn acl_delete_entry(acl: acl_t, entry_d: acl_entry_t) -> ::std::os::raw::c_int; } extern "C" { pub fn acl_get_entry( acl: acl_t, entry_id: ::std::os::raw::c_int, entry_p: *mut acl_entry_t, ) -> ::std::os::raw::c_int; } extern "C" { pub fn acl_valid(acl: acl_t) -> ::std::os::raw::c_int; } extern "C" { pub fn acl_valid_fd_np( fd: ::std::os::raw::c_int, type_: acl_type_t, acl: acl_t, ) -> ::std::os::raw::c_int; } extern "C" { pub fn acl_valid_file_np( path: *const ::std::os::raw::c_char, type_: acl_type_t, acl: acl_t, ) -> ::std::os::raw::c_int; } extern "C" { pub fn acl_valid_link_np( path: *const ::std::os::raw::c_char, type_: acl_type_t, acl: acl_t, ) -> ::std::os::raw::c_int; } extern "C" { pub fn acl_add_perm(permset_d: acl_permset_t, perm: acl_perm_t) -> ::std::os::raw::c_int; } extern "C" { pub fn acl_calc_mask(acl_p: *mut acl_t) -> ::std::os::raw::c_int; } extern "C" { pub fn acl_clear_perms(permset_d: acl_permset_t) -> ::std::os::raw::c_int; } extern "C" { pub fn acl_delete_perm(permset_d: acl_permset_t, perm: acl_perm_t) -> ::std::os::raw::c_int; } extern "C" { pub fn acl_get_perm_np(permset_d: acl_permset_t, perm: acl_perm_t) -> ::std::os::raw::c_int; } extern "C" { pub fn acl_get_permset( entry_d: acl_entry_t, permset_p: *mut acl_permset_t, ) -> ::std::os::raw::c_int; } extern "C" { pub fn acl_set_permset(entry_d: acl_entry_t, permset_d: acl_permset_t) -> ::std::os::raw::c_int; } extern "C" { pub fn acl_maximal_permset_mask_np(mask_p: *mut acl_permset_mask_t) -> ::std::os::raw::c_int; } extern "C" { pub fn acl_get_permset_mask_np( entry_d: acl_entry_t, mask_p: *mut acl_permset_mask_t, ) -> ::std::os::raw::c_int; } extern "C" { pub fn acl_set_permset_mask_np( entry_d: acl_entry_t, mask: acl_permset_mask_t, ) -> ::std::os::raw::c_int; } extern "C" { pub fn acl_add_flag_np(flagset_d: acl_flagset_t, flag: acl_flag_t) -> ::std::os::raw::c_int; } extern "C" { pub fn acl_clear_flags_np(flagset_d: acl_flagset_t) -> ::std::os::raw::c_int; } extern "C" { pub fn acl_delete_flag_np(flagset_d: acl_flagset_t, flag: acl_flag_t) -> ::std::os::raw::c_int; } extern "C" { pub fn acl_get_flag_np(flagset_d: acl_flagset_t, flag: acl_flag_t) -> ::std::os::raw::c_int; } extern "C" { pub fn acl_get_flagset_np( obj_p: *mut ::std::os::raw::c_void, flagset_p: *mut acl_flagset_t, ) -> ::std::os::raw::c_int; } extern "C" { pub fn acl_set_flagset_np( obj_p: *mut ::std::os::raw::c_void, flagset_d: acl_flagset_t, ) -> ::std::os::raw::c_int; } extern "C" { pub fn acl_get_qualifier(entry_d: acl_entry_t) -> *mut ::std::os::raw::c_void; } extern "C" { pub fn acl_get_tag_type( entry_d: acl_entry_t, tag_type_p: *mut acl_tag_t, ) -> ::std::os::raw::c_int; } extern "C" { pub fn acl_set_qualifier( entry_d: acl_entry_t, tag_qualifier_p: *const ::std::os::raw::c_void, ) -> ::std::os::raw::c_int; } extern "C" { pub fn acl_set_tag_type(entry_d: acl_entry_t, tag_type: acl_tag_t) -> ::std::os::raw::c_int; } extern "C" { pub fn acl_delete_def_file(path_p: *const ::std::os::raw::c_char) -> ::std::os::raw::c_int; } extern "C" { pub fn acl_get_fd(fd: ::std::os::raw::c_int) -> acl_t; } extern "C" { pub fn acl_get_fd_np(fd: ::std::os::raw::c_int, type_: acl_type_t) -> acl_t; } extern "C" { pub fn acl_get_file(path_p: *const ::std::os::raw::c_char, type_: acl_type_t) -> acl_t; } extern "C" { pub fn acl_get_link_np(path_p: *const ::std::os::raw::c_char, type_: acl_type_t) -> acl_t; } extern "C" { pub fn acl_set_fd(fd: ::std::os::raw::c_int, acl: acl_t) -> ::std::os::raw::c_int; } extern "C" { pub fn acl_set_fd_np( fd: ::std::os::raw::c_int, acl: acl_t, acl_type: acl_type_t, ) -> ::std::os::raw::c_int; } extern "C" { pub fn acl_set_file( path_p: *const ::std::os::raw::c_char, type_: acl_type_t, acl: acl_t, ) -> ::std::os::raw::c_int; } extern "C" { pub fn acl_set_link_np( path_p: *const ::std::os::raw::c_char, type_: acl_type_t, acl: acl_t, ) -> ::std::os::raw::c_int; } extern "C" { pub fn acl_copy_ext(buf_p: *mut ::std::os::raw::c_void, acl: acl_t, size: isize) -> isize; } extern "C" { pub fn acl_copy_ext_native( buf_p: *mut ::std::os::raw::c_void, acl: acl_t, size: isize, ) -> isize; } extern "C" { pub fn acl_copy_int(buf_p: *const ::std::os::raw::c_void) -> acl_t; } extern "C" { pub fn acl_copy_int_native(buf_p: *const ::std::os::raw::c_void) -> acl_t; } extern "C" { pub fn acl_from_text(buf_p: *const ::std::os::raw::c_char) -> acl_t; } extern "C" { pub fn acl_size(acl: acl_t) -> isize; } extern "C" { pub fn acl_to_text(acl: acl_t, len_p: *mut isize) -> *mut ::std::os::raw::c_char; } extern "C" { pub fn open( arg1: *const ::std::os::raw::c_char, arg2: ::std::os::raw::c_int, ... ) -> ::std::os::raw::c_int; } pub type uuid_t = __darwin_uuid_t; extern "C" { pub fn mbr_uid_to_uuid(uid: uid_t, uu: *mut ::std::os::raw::c_uchar) -> ::std::os::raw::c_int; } extern "C" { pub fn mbr_gid_to_uuid(gid: gid_t, uu: *mut ::std::os::raw::c_uchar) -> ::std::os::raw::c_int; } extern "C" { pub fn mbr_uuid_to_id( uu: *mut ::std::os::raw::c_uchar, uid_or_gid: *mut id_t, id_type: *mut ::std::os::raw::c_int, ) -> ::std::os::raw::c_int; } #[repr(C)] #[derive(Debug, Copy, Clone)] pub struct group { pub gr_name: *mut ::std::os::raw::c_char, pub gr_passwd: *mut ::std::os::raw::c_char, pub gr_gid: gid_t, pub gr_mem: *mut *mut ::std::os::raw::c_char, } extern "C" { pub fn getgrgid_r( arg1: gid_t, arg2: *mut group, arg3: *mut ::std::os::raw::c_char, arg4: usize, arg5: *mut *mut group, ) -> ::std::os::raw::c_int; } extern "C" { pub fn getgrnam_r( arg1: *const ::std::os::raw::c_char, arg2: *mut group, arg3: *mut ::std::os::raw::c_char, arg4: usize, arg5: *mut *mut group, ) -> ::std::os::raw::c_int; } #[repr(C)] #[derive(Debug, Copy, Clone)] pub struct passwd { pub pw_name: *mut ::std::os::raw::c_char, pub pw_passwd: *mut ::std::os::raw::c_char, pub pw_uid: uid_t, pub pw_gid: gid_t, pub pw_change: __darwin_time_t, pub pw_class: *mut ::std::os::raw::c_char, pub pw_gecos: *mut ::std::os::raw::c_char, pub pw_dir: *mut ::std::os::raw::c_char, pub pw_shell: *mut ::std::os::raw::c_char, pub pw_expire: __darwin_time_t, } extern "C" { pub fn getpwuid_r( arg1: uid_t, arg2: *mut passwd, arg3: *mut ::std::os::raw::c_char, arg4: usize, arg5: *mut *mut passwd, ) -> ::std::os::raw::c_int; } extern "C" { pub fn getpwnam_r( arg1: *const ::std::os::raw::c_char, arg2: *mut passwd, arg3: *mut ::std::os::raw::c_char, arg4: usize, arg5: *mut *mut passwd, ) -> ::std::os::raw::c_int; } extern "C" { pub fn close(arg1: ::std::os::raw::c_int) -> ::std::os::raw::c_int; } exacl-0.10.0/bindgen/wrapper.h000064400000000000000000000005361046102023000142410ustar 00000000000000#include #include #include #include #if __APPLE__ // MacOS makes us translate between GUID and UID/GID. # include #elif __linux__ // Linux supplies non-standard ACL extensions in a different header. # include #endif #include #include #include exacl-0.10.0/build.rs000064400000000000000000000065411046102023000124510ustar 00000000000000use std::env; use std::path::Path; #[cfg(feature = "buildtime_bindgen")] const BINDGEN_FAILURE_MSG: &str = r#"Could not generate bindings. On Linux, the 'sys/acl.h' file is installed by the `libacl1-dev` package. To install this package, please use `apt-get install libacl1-dev`. If you still have problems, please create a GitHub issue at: https://github.com/byllyfish/exacl/issues "#; fn main() { let out_dir = env::var("OUT_DIR").unwrap(); let out_path = Path::new(&out_dir).join("bindings.rs"); let wrapper = "bindgen/wrapper.h"; // Tell cargo to tell rustc to link libacl.so, only on Linux. #[cfg(target_os = "linux")] println!("cargo:rustc-link-lib=acl"); // Tell cargo to invalidate the built crate whenever the wrapper changes println!("cargo:rerun-if-changed={wrapper}"); #[cfg(feature = "buildtime_bindgen")] bindgen_bindings(wrapper, &out_path); #[cfg(not(feature = "buildtime_bindgen"))] prebuilt_bindings(&out_path); } #[cfg(feature = "buildtime_bindgen")] fn bindgen_bindings(wrapper: &str, out_path: &Path) { // Build bindings for "wrapper.h". Tell cargo to invalidate the built // crate when any included header file changes. let mut builder = bindgen::Builder::default() .header(wrapper) .parse_callbacks(Box::new(bindgen::CargoCallbacks)) .disable_header_comment() .layout_tests(false); // no layout tests for passwd/group structs. if cfg!(target_os = "macos") { // Pass output of `xcrun --sdk macosx --show-sdk-path`. builder = builder.clang_arg("-isysroot/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk"); } // Specify the types, functions, and constants we want to include. let types = ["acl_.*", "uid_t", "gid_t"]; let funcs = [ "acl_.*", "getpw(nam|uid)_r", "getgr(nam|gid)_r", "mbr_uid_to_uuid", "mbr_gid_to_uuid", "mbr_uuid_to_id", #[cfg(target_os = "macos")] "open", #[cfg(target_os = "macos")] "close", #[cfg(target_os = "freebsd")] "pathconf", #[cfg(target_os = "freebsd")] "lpathconf", ]; let vars = [ "ACL_.*", ".*_ACL_NFS4", "ENOENT", "ENOTSUP", "EINVAL", "ENOMEM", "ERANGE", #[cfg(target_os = "macos")] "O_SYMLINK", "ID_TYPE_UID", "ID_TYPE_GID", ]; for type_ in &types { builder = builder.allowlist_type(type_); } for func_ in &funcs { builder = builder.allowlist_function(func_); } for var_ in &vars { builder = builder.allowlist_var(var_); } // Generate the bindings. let bindings = builder.generate().expect(BINDGEN_FAILURE_MSG); // Write the bindings. bindings .write_to_file(out_path) .expect("Couldn't write bindings!"); } #[cfg(not(feature = "buildtime_bindgen"))] fn prebuilt_bindings(out_path: &Path) { let target = env::var("CARGO_CFG_TARGET_OS").unwrap(); // Untrusted input check. match target.as_str() { "macos" | "linux" | "freebsd" => (), s => panic!("Unsupported target OS: {}", s), }; let bindings_path = format!("bindgen/bindings_{target}.rs"); if let Err(err) = std::fs::copy(&bindings_path, out_path) { panic!("Can't copy {:?} to {:?}: {}", bindings_path, out_path, err); } } exacl-0.10.0/ci/bindgen.sh000075500000000000000000000012021046102023000133310ustar 00000000000000#! /usr/bin/env bash # Test the `buildtime_bindgen` feature. set -eu # Build using bindgen. cargo clean cargo build --features buildtime_bindgen # Set $target to current OS name. os="$(uname -s)" case "$os" in "Darwin") target="macos" ;; "Linux") target="linux" ;; "FreeBSD") target="freebsd" ;; *) echo "Unknown OS: $os" exit 1 ;; esac # Test that generated bindings match the prebuilt version. prebuilt_bindings="./bindgen/bindings_$target.rs" bindings=$(find ./target/debug/build -name "bindings.rs") echo "Comparing $bindings and $prebuilt_bindings" diff "$bindings" "$prebuilt_bindings" exit 0 exacl-0.10.0/ci/coverage.sh000075500000000000000000000021511046102023000135220ustar 00000000000000#! /usr/bin/env bash # Script to analyze Rust code using grcov. # # Usage: ./code_coverage.sh [open|codecov] set -e arg1="$1" os=$(uname -s | tr '[:upper:]' '[:lower:]') # To use nightly rust: NIGHTLY="+nightly" NIGHTLY="" if [ -n "$NIGHTLY" ]; then rustup install nightly fi # Set up grcov. rustup component add llvm-tools-preview export RUSTFLAGS="-Cinstrument-coverage" export LLVM_PROFILE_FILE="exacl-%p-%m.profraw" cargo $NIGHTLY install grcov # Build & Test cargo $NIGHTLY clean cargo $NIGHTLY test --features serde cargo $NIGHTLY build --features serde ./tests/run_tests.sh if [ "$arg1" = "open" ]; then echo "Producing HTML Report locally" grcov . --binary-path ./target/debug/ -s . -t html --branch --ignore-not-existing --ignore "/*" -o ./target/debug/coverage/ open target/debug/coverage/src/index.html elif [ "$arg1" = "codecov" ]; then echo "Producing lcov report and uploading it to codecov.io" grcov . --binary-path ./target/debug/ -s . -t lcov --branch --ignore-not-existing --ignore "/*" -o lcov.info bash <(curl -s https://codecov.io/bash) -f lcov.info -n "$os" fi exit 0 exacl-0.10.0/ci/docs.sh000075500000000000000000000010301046102023000126520ustar 00000000000000#! /usr/bin/env bash # Script to build rust docs. # # Usage: ./ci/docs.sh [open] set -e arg1="$1" # Install Rust nightly. rustup install nightly export RUSTDOCFLAGS='--cfg docsrs' export DOCS_RS=1 if [ "$arg1" = "open" ]; then cargo +nightly doc --no-deps --open else cargo +nightly doc --no-deps # Add an index.html file that redirects to our main page. if [ ! -f "target/doc/index.html" ]; then echo '' >"target/doc/index.html" fi fi exit 0 exacl-0.10.0/ci/format.sh000075500000000000000000000004211046102023000132150ustar 00000000000000#! /usr/bin/env bash # Script to check code formatting. set -eu # Check rust formatting. cargo fmt -- --check # Check shell script formatting. if command -v shfmt &>/dev/null; then shfmt -i 4 -d ci/*.sh tests/*.sh else echo "shfmt is not installed." fi exit 0 exacl-0.10.0/ci/lint.sh000075500000000000000000000011431046102023000126750ustar 00000000000000#! /usr/bin/env bash # Script to run lint checks. set -eu # Space-separated list of ignored clippy lints. IGNORE="similar-names wildcard_imports use_self module_name_repetitions" allow="" for name in $IGNORE; do allow="$allow -A clippy::$name" done # Check rust code with clippy. rustup component add clippy cargo clippy --version cargo clean cargo clippy --all-targets --all-features -- -D clippy::all -W clippy::pedantic -W clippy::cargo -W clippy::nursery $allow # Check bash scripts with shellcheck. shellcheck --version || exit 0 SHELLCHECK_OPTS="-e SC2086" shellcheck ci/*.sh tests/*.sh exit 0 exacl-0.10.0/examples/exacl.rs000064400000000000000000000074121046102023000142620ustar 00000000000000//! Program to get/set extended ACL's. //! //! To read an ACL from myfile and write it to stdout as JSON: //! exacl myfile //! //! To set the ACL for myfile from JSON passed via stdin (complete replacement): //! exacl --set myfile //! //! To get/set the ACL of a symlink itself, instead of the file it points to, //! use the -s option. //! //! To get/set the default ACL (on Linux), use the -d option. use exacl::{getfacl, setfacl, AclEntry, AclOption}; use std::io; use std::path::{Path, PathBuf}; use std::process; use clap::Parser; #[derive(clap::Parser)] #[command(name = "exacl", about = "Read or write a file's ACL.")] #[allow(clippy::struct_excessive_bools)] struct Opt { /// Set file's ACL. #[arg(long)] set: bool, /// Get or set the access ACL. #[arg(short = 'a', long)] access: bool, /// Get or set the default ACL. #[arg(short = 'd', long)] default: bool, /// Get or set the ACL of a symlink itself. #[arg(short = 's', long)] symlink: bool, /// Format of input or output. #[arg(value_enum, short = 'f', long, default_value = "json")] format: Format, /// Input files #[arg()] files: Vec, } #[derive(Copy, Clone, Debug, clap::ValueEnum)] #[value(rename_all = "lower")] enum Format { Json, Std, } const EXIT_SUCCESS: i32 = 0; const EXIT_FAILURE: i32 = 1; fn main() { env_logger::init(); let opt = Opt::parse(); let mut options = AclOption::empty(); if opt.access { options |= AclOption::ACCESS_ACL; } if opt.default { options |= AclOption::DEFAULT_ACL; } if opt.symlink { options |= AclOption::SYMLINK_ACL; } let exit_code = if opt.set { set_acl(&opt.files, options, opt.format) } else { get_acl(&opt.files, options, opt.format) }; process::exit(exit_code); } fn get_acl(paths: &[PathBuf], options: AclOption, format: Format) -> i32 { for path in paths { if let Err(err) = dump_acl(path, options, format) { eprintln!("{err}"); return EXIT_FAILURE; } } EXIT_SUCCESS } fn set_acl(paths: &[PathBuf], options: AclOption, format: Format) -> i32 { let entries = match read_input(format) { Some(entries) => entries, None => return EXIT_FAILURE, }; if let Err(err) = setfacl(paths, &entries, options) { eprintln!("{err}"); return EXIT_FAILURE; } EXIT_SUCCESS } fn dump_acl(path: &Path, options: AclOption, format: Format) -> io::Result<()> { let entries = getfacl(path, options)?; match format { #[cfg(feature = "serde")] Format::Json => { serde_json::to_writer(io::stdout(), &entries)?; println!(); // add newline } #[cfg(not(feature = "serde"))] Format::Json => { panic!("serde not supported"); } Format::Std => exacl::to_writer(io::stdout(), &entries)?, }; Ok(()) } fn read_input(format: Format) -> Option> { let reader = io::BufReader::new(io::stdin()); let entries: Vec = match format { // Read JSON format. #[cfg(feature = "serde")] Format::Json => match serde_json::from_reader(reader) { Ok(entries) => entries, Err(err) => { eprintln!("JSON parser error: {err}"); return None; } }, #[cfg(not(feature = "serde"))] Format::Json => { panic!("serde not supported"); } // Read Std format. Format::Std => match exacl::from_reader(reader) { Ok(entries) => entries, Err(err) => { eprintln!("Std parser error: {err}"); return None; } }, }; Some(entries) } exacl-0.10.0/src/acl.rs000064400000000000000000000601061046102023000126750ustar 00000000000000//! Provides `Acl` and `AclOption` implementation. use crate::aclentry::AclEntry; #[cfg(any(target_os = "linux", target_os = "freebsd"))] use crate::aclentry::AclEntryKind; use crate::failx::{fail_custom, path_err}; #[cfg(any(target_os = "linux", target_os = "freebsd"))] use crate::flag::Flag; #[cfg(any(target_os = "linux", target_os = "freebsd"))] use crate::perm::Perm; use crate::util::*; use bitflags::bitflags; use scopeguard::{self, ScopeGuard}; use std::io; use std::path::Path; bitflags! { /// Controls how ACL's are accessed. #[derive(Default)] pub struct AclOption : u32 { /// Get/set the access ACL only (Linux and FreeBSD only). const ACCESS_ACL = 0b0001; /// Get/set the default ACL only (Linux and FreeBSD only). const DEFAULT_ACL = 0b0010; /// Get/set the ACL of the symlink itself (macOS only). const SYMLINK_ACL = 0b0100; /// Ignore expected error when using DEFAULT_ACL on a file. #[doc(hidden)] const IGNORE_EXPECTED_FILE_ERR = 0b10000; } } /// Access Control List native object wrapper. /// /// Each [`Acl`] is immutable once constructed. To manipulate its contents, you /// can retrieve a mutable vector of [`AclEntry`], modify the vector's contents, /// then create a new [`Acl`]. pub struct Acl { /// Native acl. acl: acl_t, /// Set to true if `acl` was set from the default ACL for a directory /// using DEFAULT_ACL option. Used to return entries with the `DEFAULT` /// flag set. #[cfg(any(target_os = "linux", target_os = "freebsd"))] default_acl: bool, } impl Acl { /// Convenience function to construct an `Acl`. #[allow(unused_variables)] fn new(acl: acl_t, default_acl: bool) -> Acl { assert!(!acl.is_null()); Acl { acl, #[cfg(any(target_os = "linux", target_os = "freebsd"))] default_acl, } } /// Read ACL for the specified file. /// /// # Errors /// /// Returns an [`io::Error`] on failure. pub fn read(path: &Path, options: AclOption) -> io::Result { let symlink_acl = options.contains(AclOption::SYMLINK_ACL); let default_acl = options.contains(AclOption::DEFAULT_ACL); let result = xacl_get_file(path, symlink_acl, default_acl); match result { Ok(acl) => Ok(Acl::new(acl, default_acl)), Err(err) => { // Trying to access the default ACL of a non-directory on Linux // will return an error. We can catch this error and return an // empty ACL instead; only if `IGNORE_EXPECTED_FILE_ERR` is set. // (Linux returns permission denied. FreeBSD returns invalid // argument.) if default_acl && (err.kind() == io::ErrorKind::PermissionDenied || err.kind() == io::ErrorKind::InvalidInput) && options.contains(AclOption::IGNORE_EXPECTED_FILE_ERR) && is_non_directory(path, symlink_acl) { // Return an empty acl. Ok(Acl::new(xacl_init(1)?, default_acl)) } else { Err(path_err(path, &err)) } } } } /// Write ACL for the specified file. /// /// # Errors /// /// Returns an [`io::Error`] on failure. pub fn write(&self, path: &Path, options: AclOption) -> io::Result<()> { let symlink_acl = options.contains(AclOption::SYMLINK_ACL); let default_acl = options.contains(AclOption::DEFAULT_ACL); // If we're writing a default ACL to a non-directory, and we // specify the `IGNORE_EXPECTED_FILE_ERR` option, this function is a // no-op if the ACL is empty. if default_acl && is_non_directory(path, symlink_acl) { if self.is_empty() && options.contains(AclOption::IGNORE_EXPECTED_FILE_ERR) { return Ok(()); } return fail_custom(&format!( "File {:?}: Non-directory does not have default ACL", path )); } if let Err(err) = xacl_set_file(path, self.acl, symlink_acl, default_acl) { return Err(path_err(path, &err)); } Ok(()) } /// Compute mask. #[cfg(any(target_os = "linux", target_os = "freebsd"))] fn compute_mask_perms(entries: &[AclEntry], filter: (Flag, Flag)) -> Option { let mut perms = Perm::empty(); let mut need_mask = false; for entry in entries { // Skip over undesired entries in a unified ACL. if (entry.flags & filter.1) != filter.0 { continue; } match entry.kind { AclEntryKind::Mask => return None, AclEntryKind::User | AclEntryKind::Group if !entry.name.is_empty() => { perms |= entry.perms; need_mask = true; } AclEntryKind::Group => perms |= entry.perms, _ => (), } } if !need_mask { return None; } Some(perms) } /// Check for required entries that are missing. /// /// It is valid for there to be zero entries. #[cfg(any(target_os = "linux", target_os = "freebsd"))] fn find_missing_entries(entries: &[AclEntry], filter: (Flag, Flag)) -> Option { let mut miss_user = true; let mut miss_group = true; let mut miss_other = true; let mut is_empty = true; for entry in entries { // Skip over undesired entries in a unified ACL. if (entry.flags & filter.1) != filter.0 { continue; } is_empty = false; match entry.kind { AclEntryKind::User if entry.name.is_empty() => miss_user = false, AclEntryKind::Group if entry.name.is_empty() => miss_group = false, AclEntryKind::Other => miss_other = false, _ => (), } } if is_empty { None } else if miss_user { Some(AclEntryKind::User) } else if miss_group { Some(AclEntryKind::Group) } else if miss_other { Some(AclEntryKind::Other) } else { None } } /// Return an ACL from a slice of [`AclEntry`]. /// /// On Linux, if there is no mask `AclEntry`, one will be computed and /// added, if needed. /// /// # Errors /// /// Returns an [`io::Error`] on failure. pub fn from_entries(entries: &[AclEntry]) -> io::Result { let new_acl = xacl_init(entries.len())?; // Use the smart pointer form of scopeguard; `acl_p` can change value // when we create entries in it. let mut acl_p = scopeguard::guard(new_acl, |a| { xacl_free(a); }); for (i, entry) in entries.iter().enumerate() { if let Err(err) = entry.add_to_acl(&mut acl_p) { return fail_custom(&format!("entry {i}: {err}")); } } // Check for missing required entries. #[cfg(any(target_os = "linux", target_os = "freebsd"))] if let Some(kind) = Acl::find_missing_entries(entries, (Flag::empty(), Flag::empty())) { return fail_custom(&format!("missing required entry \"{kind}\"")); } // Check if we need to add a mask entry. #[cfg(any(target_os = "linux", target_os = "freebsd"))] if let Some(mask_perms) = Acl::compute_mask_perms(entries, (Flag::empty(), Flag::empty())) { let mask = AclEntry::allow_mask(mask_perms, None); if let Err(err) = mask.add_to_acl(&mut acl_p) { return fail_custom(&format!("entry -1: {err}")); } } Ok(Acl::new(ScopeGuard::into_inner(acl_p), false)) } /// Return pair of ACL's from slice of [`AclEntry`]. This method separates /// regular access entries from default entries and returns two ACL's, an /// access ACL and default ACL. Either may be empty. /// /// If there is no mask `AclEntry` in an ACL, one will be computed and /// added, if needed. /// /// # Errors /// /// Returns an [`io::Error`] on failure. #[cfg(any(docsrs, target_os = "linux", target_os = "freebsd"))] #[cfg_attr(docsrs, doc(cfg(any(target_os = "linux", target_os = "freebsd"))))] pub fn from_unified_entries(entries: &[AclEntry]) -> io::Result<(Acl, Acl)> { let new_access = xacl_init(entries.len())?; let new_default = xacl_init(entries.len())?; // Use the smart pointer form of scopeguard; acls can change value when // we create entries in them. let mut access_p = scopeguard::guard(new_access, |a| { xacl_free(a); }); let mut default_p = scopeguard::guard(new_default, |a| { xacl_free(a); }); for (i, entry) in entries.iter().enumerate() { let result = if entry.flags.contains(Flag::DEFAULT) { entry.add_to_acl(&mut default_p) } else { entry.add_to_acl(&mut access_p) }; if let Err(err) = result { return fail_custom(&format!("entry {i}: {err}")); } } if xacl_is_posix(*access_p) { // Check for missing entries in both access and default entries. if let Some(kind) = Acl::find_missing_entries(entries, (Flag::empty(), Flag::DEFAULT)) { return fail_custom(&format!("missing required entry \"{kind}\"")); } if let Some(kind) = Acl::find_missing_entries(entries, (Flag::DEFAULT, Flag::DEFAULT)) { return fail_custom(&format!("missing required default entry \"{kind}\"")); } // Check if we need to add a mask entry. if let Some(mask_perms) = Acl::compute_mask_perms(entries, (Flag::empty(), Flag::DEFAULT)) { let mask = AclEntry::allow_mask(mask_perms, None); if let Err(err) = mask.add_to_acl(&mut access_p) { return fail_custom(&format!("mask entry: {err}")); } } if let Some(mask_perms) = Acl::compute_mask_perms(entries, (Flag::DEFAULT, Flag::DEFAULT)) { let mask = AclEntry::allow_mask(mask_perms, Flag::DEFAULT); if let Err(err) = mask.add_to_acl(&mut default_p) { return fail_custom(&format!("default mask entry: {err}")); } } } let access_acl = ScopeGuard::into_inner(access_p); let default_acl = ScopeGuard::into_inner(default_p); Ok((Acl::new(access_acl, false), Acl::new(default_acl, true))) } /// Return ACL as a vector of [`AclEntry`]. /// /// # Errors /// /// Returns an [`io::Error`] on failure. pub fn entries(&self) -> io::Result> { let mut entries = Vec::::with_capacity(8); xacl_foreach(self.acl, |entry_p| { let entry = AclEntry::from_raw(entry_p, self.acl)?; entries.push(entry); Ok(()) })?; #[cfg(any(target_os = "linux", target_os = "freebsd"))] if self.default_acl { // Set DEFAULT flag on each entry. for entry in &mut entries { entry.flags |= Flag::DEFAULT; } } Ok(entries) } /// Return ACL as a string. /// /// This method is provided as a tracing/debugging aid. /// /// # Errors /// /// Returns an [`io::Error`] on failure. #[cfg(test)] pub fn to_string(&self) -> io::Result { use std::io::Write; let mut buf = Vec::new(); for entry in self.entries()? { writeln!(buf, "{entry}")?; } String::from_utf8(buf).map_err(|err| io::Error::new(io::ErrorKind::Other, err)) } /// Return true if ACL is empty. #[must_use] pub fn is_empty(&self) -> bool { xacl_is_empty(self.acl) } /// Return true if ACL is a Posix.1e ACL on Linux or `FreeBSD`. #[must_use] #[allow(clippy::missing_const_for_fn, dead_code)] pub fn is_posix(&self) -> bool { xacl_is_posix(self.acl) } /// Return true if file uses an `NFSv4` ACL (`FreeBSD` only). /// /// Only used in testing. /// /// # Errors /// /// Returns an [`io::Error`] on failure. #[cfg(any(docsrs, target_os = "freebsd"))] #[cfg_attr(docsrs, doc(cfg(target_os = "freebsd")))] #[allow(dead_code)] pub fn is_nfs4(path: &Path, options: AclOption) -> io::Result { xacl_is_nfs4(path, options.contains(AclOption::SYMLINK_ACL)) } } impl Drop for Acl { fn drop(&mut self) { xacl_free(self.acl); } } /// Return true if path exists and it's not a directory. fn is_non_directory(path: &Path, symlink: bool) -> bool { let result = if symlink { path.symlink_metadata() } else { path.metadata() }; result.map_or(false, |meta| !meta.is_dir()) } //////////////////////////////////////////////////////////////////////////////// #[cfg(test)] mod acl_tests { use super::*; use crate::flag::Flag; use crate::perm::Perm; use log::debug; #[test] fn test_read_acl() -> io::Result<()> { let file = tempfile::NamedTempFile::new()?; let acl = Acl::read(file.as_ref(), AclOption::empty())?; let entries = acl.entries()?; #[cfg(target_os = "macos")] assert_eq!(entries.len(), 0); #[cfg(any(target_os = "linux", target_os = "freebsd"))] assert_eq!(entries.len(), 3); for entry in &entries { debug!("{}", entry); } Ok(()) } #[test] #[cfg(target_os = "macos")] fn test_write_acl_macos() -> io::Result<()> { let mut entries = Vec::::new(); let rwx = Perm::READ | Perm::WRITE | Perm::EXECUTE; entries.push(AclEntry::allow_group("_spotlight", rwx, None)); entries.push(AclEntry::allow_user("11501", rwx, None)); entries.push(AclEntry::allow_user("11502", rwx, None)); entries.push(AclEntry::allow_user("11503", rwx, None)); entries.push(AclEntry::deny_group( "11504", rwx, Flag::FILE_INHERIT | Flag::DIRECTORY_INHERIT, )); let file = tempfile::NamedTempFile::new()?; let acl = Acl::from_entries(&entries)?; assert!(!acl.is_empty()); acl.write(file.as_ref(), AclOption::empty())?; // Even though the last entry is a group, the `acl_to_text` representation // displays it as `user`. assert_eq!( acl.to_string()?, r#"allow::group:_spotlight:read,write,execute allow::user:11501:read,write,execute allow::user:11502:read,write,execute allow::user:11503:read,write,execute deny:file_inherit,directory_inherit:group:11504:read,write,execute "# ); let acl2 = Acl::read(file.as_ref(), AclOption::empty())?; let entries2 = acl2.entries()?; assert_eq!(entries2, entries); Ok(()) } #[test] #[cfg(any(target_os = "linux", target_os = "freebsd"))] fn test_write_acl_posix() -> io::Result<()> { let file = tempfile::NamedTempFile::new()?; // Skip the rest of the test if file uses NFSv4 ACL (FIXME). #[cfg(target_os = "freebsd")] if Acl::is_nfs4(file.as_ref(), AclOption::empty())? { return Ok(()); } let mut entries = Vec::::new(); let rwx = Perm::READ | Perm::WRITE | Perm::EXECUTE; entries.push(AclEntry::allow_group("bin", rwx, None)); entries.push(AclEntry::allow_user("11501", rwx, None)); entries.push(AclEntry::allow_user("11502", rwx, None)); entries.push(AclEntry::allow_user("11503", rwx, None)); entries.push(AclEntry::allow_user("", rwx, None)); entries.push(AclEntry::allow_group("", rwx, None)); entries.push(AclEntry::allow_other(rwx, None)); // We do not add a mask entry. One will be automatically added. let acl = Acl::from_entries(&entries)?; acl.write(file.as_ref(), AclOption::empty())?; assert_eq!( acl.to_string()?, r#"allow::user::read,write,execute allow::user:11501:read,write,execute allow::user:11502:read,write,execute allow::user:11503:read,write,execute allow::group::read,write,execute allow::group:bin:read,write,execute allow::mask::read,write,execute allow::other::read,write,execute "# ); let acl2 = Acl::read(file.as_ref(), AclOption::empty())?; let mut entries2 = acl2.entries()?; // Before doing the comparison, add the mask entry. entries.push(AclEntry::allow_mask(rwx, None)); entries.sort(); entries2.sort(); assert_eq!(entries2, entries); Ok(()) } #[test] #[cfg(target_os = "macos")] fn test_write_acl_big() -> io::Result<()> { let mut entries = Vec::::new(); let rwx = Perm::READ | Perm::WRITE | Perm::EXECUTE; for _ in 0..128 { entries.push(AclEntry::allow_user("11501", rwx, None)); } let file = tempfile::NamedTempFile::new()?; let acl = Acl::from_entries(&entries)?; acl.write(file.as_ref(), AclOption::empty())?; let acl2 = Acl::read(file.as_ref(), AclOption::empty())?; let entries2 = acl2.entries()?; assert_eq!(entries2, entries); Ok(()) } #[test] #[cfg(target_os = "macos")] fn test_write_acl_too_big() { let mut entries = Vec::::new(); let rwx = Perm::READ | Perm::WRITE | Perm::EXECUTE; for _ in 0..129 { entries.push(AclEntry::allow_user("11501", rwx, None)); } let err = Acl::from_entries(&entries).err().unwrap(); assert_eq!(err.to_string(), "Too many ACL entries"); } #[test] #[cfg(target_os = "linux")] fn test_read_default_acl() -> io::Result<()> { let dir = tempfile::tempdir()?; let default_acl = Acl::read(dir.as_ref(), AclOption::DEFAULT_ACL)?; assert!(default_acl.is_empty()); Ok(()) } #[test] #[cfg(any(target_os = "linux", target_os = "freebsd"))] fn test_write_default_acl() -> io::Result<()> { let dir = tempfile::tempdir()?; // Skip the rest of the test if file uses NFSv4 ACL (FIXME). #[cfg(target_os = "freebsd")] if Acl::is_nfs4(dir.as_ref(), AclOption::empty())? { return Ok(()); } let mut entries = Vec::::new(); let rwx = Perm::READ | Perm::WRITE | Perm::EXECUTE; entries.push(AclEntry::allow_user("", rwx, None)); entries.push(AclEntry::allow_group("", rwx, None)); entries.push(AclEntry::allow_other(rwx, None)); entries.push(AclEntry::allow_group("bin", rwx, None)); entries.push(AclEntry::allow_mask(rwx, None)); let path = dir.as_ref(); let acl = Acl::from_entries(&entries)?; acl.write(path, AclOption::DEFAULT_ACL)?; let acl2 = Acl::read(path, AclOption::empty())?; assert_ne!(acl.to_string()?, acl2.to_string()?); let default_acl = Acl::read(path, AclOption::DEFAULT_ACL)?; let default_entries = default_acl.entries()?; for entry in &default_entries { assert_eq!(entry.flags, Flag::DEFAULT); } // Test deleting a default ACL by passing an empty acl. debug!("Test deleting a default ACL"); let empty_acl = Acl::from_entries(&[])?; empty_acl.write(path, AclOption::DEFAULT_ACL)?; assert!(Acl::read(path, AclOption::DEFAULT_ACL)?.is_empty()); Ok(()) } #[test] fn test_from_entries() { // 0 entries should result in empty acl. let acl = Acl::from_entries(&[]).unwrap(); assert!(acl.is_empty()); // Test named user on MacOS. #[cfg(target_os = "macos")] { let entries = vec![AclEntry::allow_user("500", Perm::EXECUTE, None)]; let acl = Acl::from_entries(&entries).unwrap(); assert_eq!(acl.to_string().unwrap(), "allow::user:500:execute\n"); } // Test named user on Linux. It should add correct mask. #[cfg(any(target_os = "linux", target_os = "freebsd"))] { let mut entries = vec![ AclEntry::allow_group("", Perm::READ, None), AclEntry::allow_other(Perm::READ, None), AclEntry::allow_user("500", Perm::EXECUTE, None), ]; let err = Acl::from_entries(&entries).err().unwrap(); assert_eq!(err.to_string(), "missing required entry \"user\""); entries.push(AclEntry::allow_user("", Perm::READ, None)); let acl = Acl::from_entries(&entries).unwrap(); #[cfg(target_os = "linux")] let expected = "allow::user::read\nallow::user:500:execute\nallow::group::read\nallow::mask::read,execute\nallow::other::read\n"; #[cfg(target_os = "freebsd")] let expected = "allow::group::read\nallow::other::read\nallow::user:500:execute\nallow::user::read\nallow::mask::read,execute\n"; assert_eq!(acl.to_string().unwrap(), expected); entries.push(AclEntry::allow_group("", Perm::WRITE, None)); let err = Acl::from_entries(&entries).err().unwrap(); assert_eq!(err.to_string(), "entry 4: duplicate entry for \"group\""); } } #[test] #[cfg(any(target_os = "linux", target_os = "freebsd"))] fn test_from_unified_entries() { // 0 entries should result in empty acls. let (a, d) = Acl::from_unified_entries(&[]).unwrap(); assert!(a.is_empty()); assert!(d.is_empty()); let mut entries = vec![ AclEntry::allow_user("500", Perm::EXECUTE, None), AclEntry::allow_user("501", Perm::EXECUTE, Flag::DEFAULT), ]; // Missing required entries. let err = Acl::from_unified_entries(&entries).err().unwrap(); assert_eq!(err.to_string(), "missing required entry \"user\""); entries.push(AclEntry::allow_group("", Perm::WRITE, None)); entries.push(AclEntry::allow_user("", Perm::READ, None)); entries.push(AclEntry::allow_other(Perm::empty(), None)); // Missing required default entries. let err = Acl::from_unified_entries(&entries).err().unwrap(); assert_eq!(err.to_string(), "missing required default entry \"user\""); entries.push(AclEntry::allow_group("", Perm::WRITE, Flag::DEFAULT)); entries.push(AclEntry::allow_user("", Perm::READ, Flag::DEFAULT)); entries.push(AclEntry::allow_other(Perm::empty(), Flag::DEFAULT)); let (a, d) = Acl::from_unified_entries(&entries).unwrap(); #[cfg(target_os = "linux")] let expected1 = "allow::user::read\nallow::user:500:execute\nallow::group::write\nallow::mask::write,execute\nallow::other::\n"; #[cfg(target_os = "freebsd")] let expected1 = "allow::user:500:execute\nallow::group::write\nallow::user::read\nallow::other::\nallow::mask::write,execute\n"; assert_eq!(a.to_string().unwrap(), expected1); #[cfg(target_os = "linux")] let expected2 = "allow:default:user::read\nallow:default:user:501:execute\nallow:default:group::write\nallow:default:mask::write,execute\nallow:default:other::\n"; #[cfg(target_os = "freebsd")] let expected2 = "allow:default:user:501:execute\nallow:default:group::write\nallow:default:user::read\nallow:default:other::\nallow:default:mask::write,execute\n"; assert_eq!(d.to_string().unwrap(), expected2); entries.push(AclEntry::allow_group("", Perm::WRITE, Flag::DEFAULT)); let err = Acl::from_unified_entries(&entries).err().unwrap(); assert_eq!( err.to_string(), "entry 8: duplicate default entry for \"group\"" ); } #[test] fn test_empty_acl() -> io::Result<()> { let acl = Acl::from_entries(&[])?; assert!(acl.is_empty()); Ok(()) } } exacl-0.10.0/src/aclentry.rs000064400000000000000000000473451046102023000137710ustar 00000000000000//! Provides `AclEntry` implementation. use crate::failx::fail_custom; use crate::flag::Flag; use crate::format; use crate::perm::Perm; use crate::qualifier::Qualifier; use crate::util::*; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; use std::cmp::Ordering; use std::fmt; use std::io; /// Kind of ACL entry (User, Group, Mask, Other, or Unknown). #[derive(Copy, Clone, Debug, PartialEq, PartialOrd, Eq, Ord)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "serde", serde(rename_all = "lowercase"))] pub enum AclEntryKind { // *N.B.* Update the corresponding table in format/format_no_serde.rs // if any of these entries change. /// Entry represents a user. User, /// Entry represents a group. Group, /// Entry represents a Posix.1e "mask" entry. #[cfg(any(docsrs, target_os = "linux", target_os = "freebsd"))] #[cfg_attr(docsrs, doc(cfg(any(target_os = "linux", target_os = "freebsd"))))] Mask, /// Entry represents a Posix.1e "other" entry. #[cfg(any(docsrs, target_os = "linux", target_os = "freebsd"))] #[cfg_attr(docsrs, doc(cfg(any(target_os = "linux", target_os = "freebsd"))))] Other, /// Entry represents a NFS "everyone" entry. #[cfg(any(docsrs, target_os = "freebsd"))] #[cfg_attr(docsrs, doc(cfg(target_os = "freebsd")))] Everyone, /// Entry represents a possibly corrupt ACL entry, caused by an unknown tag. Unknown, } /// ACL entry with allow/deny semantics. /// /// ACL entries are ordered so sorting will automatically put the ACL in /// canonical order. /// #[derive(Clone, Debug, PartialEq, Eq)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "serde", serde(deny_unknown_fields))] pub struct AclEntry { /// Kind of entry (User, Group, Other, Mask, Everyone, or Unknown). pub kind: AclEntryKind, /// Name of the principal being given access. You can use a user/group name /// or decimal uid/gid. On macOS you can use a UUID. pub name: String, /// Permission bits for the entry. pub perms: Perm, /// Flags indicating whether an entry is inherited, etc. #[cfg_attr(feature = "serde", serde(default))] pub flags: Flag, /// True if entry is allowed; false means deny. Linux only supports /// allow=true. #[cfg_attr(feature = "serde", serde(default = "default_allow"))] pub allow: bool, } // Default value of allow; used for serde. #[cfg(feature = "serde")] const fn default_allow() -> bool { true } impl Ord for AclEntry { fn cmp(&self, other: &Self) -> Ordering { // Entries with flags last. match (self.flags.is_empty(), other.flags.is_empty()) { (true, false) => return Ordering::Less, (false, true) => return Ordering::Greater, _ => (), } // Denied entries first. let ret = self.allow.cmp(&other.allow); if ret != Ordering::Equal { return ret; } // Order by kind. let ret = self.kind.cmp(&other.kind); if ret != Ordering::Equal { return ret; } // Lastly, order by name. self.name.cmp(&other.name) } } impl PartialOrd for AclEntry { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } } impl AclEntry { /// Construct a new access control entry. #[must_use] fn new( kind: AclEntryKind, name: &str, perms: Perm, flags: Option, allow: bool, ) -> AclEntry { AclEntry { kind, name: String::from(name), perms, flags: flags.unwrap_or_default(), allow, } } /// Construct an ALLOW access control entry for a user. #[must_use] pub fn allow_user(name: &str, perms: Perm, flags: F) -> AclEntry where F: Into>, { AclEntry::new(AclEntryKind::User, name, perms, flags.into(), true) } /// Construct an ALLOW access control entry for a group. #[must_use] pub fn allow_group(name: &str, perms: Perm, flags: F) -> AclEntry where F: Into>, { AclEntry::new(AclEntryKind::Group, name, perms, flags.into(), true) } /// Construct an ALLOW access control entry for mask. #[cfg(any(docsrs, target_os = "linux", target_os = "freebsd"))] #[cfg_attr(docsrs, doc(cfg(any(target_os = "linux", target_os = "freebsd"))))] #[must_use] pub fn allow_mask(perms: Perm, flags: F) -> AclEntry where F: Into>, { AclEntry::new(AclEntryKind::Mask, "", perms, flags.into(), true) } /// Construct an ALLOW access control entry for other. #[cfg(any(docsrs, target_os = "linux", target_os = "freebsd"))] #[cfg_attr(docsrs, doc(cfg(any(target_os = "linux", target_os = "freebsd"))))] #[must_use] pub fn allow_other(perms: Perm, flags: F) -> AclEntry where F: Into>, { AclEntry::new(AclEntryKind::Other, "", perms, flags.into(), true) } /// Construct a DENY access control entry for a user. #[cfg(any(docsrs, target_os = "macos", target_os = "freebsd"))] #[cfg_attr(docsrs, doc(cfg(any(target_os = "macos", target_os = "freebsd"))))] #[must_use] pub fn deny_user(name: &str, perms: Perm, flags: F) -> AclEntry where F: Into>, { AclEntry::new(AclEntryKind::User, name, perms, flags.into(), false) } /// Construct a DENY access control entry for a group. #[cfg(any(docsrs, target_os = "macos", target_os = "freebsd"))] #[cfg_attr(docsrs, doc(cfg(any(target_os = "macos", target_os = "freebsd"))))] #[must_use] pub fn deny_group(name: &str, perms: Perm, flags: F) -> AclEntry where F: Into>, { AclEntry::new(AclEntryKind::Group, name, perms, flags.into(), false) } /// Return an `AclEntry` constructed from a native `acl_entry_t`. pub(crate) fn from_raw(entry: acl_entry_t, acl: acl_t) -> io::Result { let (allow, qualifier, perms, flags) = xacl_get_entry(acl, entry)?; let (kind, name) = match qualifier { Qualifier::Unknown(s) => (AclEntryKind::Unknown, s), #[cfg(target_os = "macos")] Qualifier::User(_) | Qualifier::Guid(_) => (AclEntryKind::User, qualifier.name()?), #[cfg(target_os = "macos")] Qualifier::Group(_) => (AclEntryKind::Group, qualifier.name()?), #[cfg(any(target_os = "linux", target_os = "freebsd"))] Qualifier::User(_) | Qualifier::UserObj => (AclEntryKind::User, qualifier.name()?), #[cfg(any(target_os = "linux", target_os = "freebsd"))] Qualifier::Group(_) | Qualifier::GroupObj => (AclEntryKind::Group, qualifier.name()?), #[cfg(any(target_os = "linux", target_os = "freebsd"))] Qualifier::Mask => (AclEntryKind::Mask, qualifier.name()?), #[cfg(any(target_os = "linux", target_os = "freebsd"))] Qualifier::Other => (AclEntryKind::Other, qualifier.name()?), #[cfg(target_os = "freebsd")] Qualifier::Everyone => (AclEntryKind::Everyone, qualifier.name()?), }; Ok(AclEntry { kind, name, perms, flags, allow, }) } pub(crate) fn add_to_acl(&self, acl: &mut acl_t) -> io::Result<()> { let qualifier = self.qualifier()?; xacl_add_entry(acl, self.allow, &qualifier, self.perms, self.flags)?; Ok(()) } fn qualifier(&self) -> io::Result { let qualifier = match self.kind { AclEntryKind::User => Qualifier::user_named(&self.name)?, AclEntryKind::Group => Qualifier::group_named(&self.name)?, #[cfg(any(target_os = "linux", target_os = "freebsd"))] AclEntryKind::Mask => Qualifier::mask_named(&self.name)?, #[cfg(any(target_os = "linux", target_os = "freebsd"))] AclEntryKind::Other => Qualifier::other_named(&self.name)?, #[cfg(target_os = "freebsd")] AclEntryKind::Everyone => Qualifier::everyone_named(&self.name)?, AclEntryKind::Unknown => { return fail_custom("unsupported kind: \"unknown\""); } }; Ok(qualifier) } } impl fmt::Display for AclEntryKind { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { format::write_aclentrykind(f, *self) } } impl std::str::FromStr for AclEntryKind { type Err = format::Error; fn from_str(s: &str) -> Result { match s { "u" => Ok(AclEntryKind::User), "g" => Ok(AclEntryKind::Group), #[cfg(any(target_os = "linux", target_os = "freebsd"))] "o" => Ok(AclEntryKind::Other), #[cfg(any(target_os = "linux", target_os = "freebsd"))] "m" => Ok(AclEntryKind::Mask), _ => format::read_aclentrykind(s), } } } impl fmt::Display for AclEntry { /// Format an `AclEntry` 5-tuple: /// :::: fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let allow = if self.allow { "allow" } else { "deny" }; write!( f, "{}:{}:{}:{}:{}", allow, self.flags, self.kind, self.name, self.perms ) } } fn parse_allow(value: &str) -> Result { let result = match value { "allow" => true, "deny" => false, s => { return Err(format::Error::Message(format!( "Unknown variant `{s}`, expected one of `allow`, `deny`" ))) } }; Ok(result) } impl std::str::FromStr for AclEntry { type Err = format::Error; fn from_str(s: &str) -> Result { let fields = s.splitn(5, ':').map(str::trim).collect::>(); let entry = match fields.len() { 5 => { // :::: let allow = parse_allow(fields[0])?; let flags = fields[1].parse::()?; let kind = fields[2].parse::()?; let name = fields[3]; let perms = fields[4].parse::()?; AclEntry::new(kind, name, perms, Some(flags), allow) } 4 => { // ::: let allow = true; let flags = fields[0].parse::()?; let kind = fields[1].parse::()?; let name = fields[2]; let perms = fields[3].parse::()?; AclEntry::new(kind, name, perms, Some(flags), allow) } 3 => { // :: let allow = true; let flags = Flag::empty(); let kind = fields[0].parse::()?; let name = fields[1]; let perms = fields[2].parse::()?; AclEntry::new(kind, name, perms, Some(flags), allow) } _ => return Err(format::Error::Message(format!("Unknown ACL format: `{s}`"))), }; Ok(entry) } } //////////////////////////////////////////////////////////////////////////////// #[cfg(test)] mod aclentry_tests { use super::*; #[test] fn test_ordering() { let mut acl = vec![ AclEntry::allow_user("f", Perm::WRITE, None), AclEntry::allow_group("3", Perm::EXECUTE, None), AclEntry::allow_group("d", Perm::EXECUTE, None), AclEntry::allow_user("z", Perm::READ, None), AclEntry::allow_group("z", Perm::READ, None), #[cfg(any(target_os = "macos", target_os = "freebsd"))] AclEntry::deny_user("a", Perm::READ, Flag::FILE_INHERIT), #[cfg(any(target_os = "macos", target_os = "freebsd"))] AclEntry::deny_user("c", Perm::READ, None), ]; acl.sort(); let acl_sorted = vec![ #[cfg(any(target_os = "macos", target_os = "freebsd"))] AclEntry::deny_user("c", Perm::READ, None), AclEntry::allow_user("f", Perm::WRITE, None), AclEntry::allow_user("z", Perm::READ, None), AclEntry::allow_group("3", Perm::EXECUTE, None), AclEntry::allow_group("d", Perm::EXECUTE, None), AclEntry::allow_group("z", Perm::READ, None), #[cfg(any(target_os = "macos", target_os = "freebsd"))] AclEntry::deny_user("a", Perm::READ, Flag::FILE_INHERIT), ]; assert_eq!(acl, acl_sorted); } #[test] #[cfg(any(target_os = "linux", target_os = "freebsd"))] fn test_ordering_linux() { let mut acl = vec![ AclEntry::allow_user("f", Perm::WRITE, None), AclEntry::allow_mask(Perm::READ, None), AclEntry::allow_group("b", Perm::EXECUTE, None), AclEntry::allow_group("d", Perm::EXECUTE, None), AclEntry::allow_user("z", Perm::READ, None), AclEntry::allow_user("", Perm::READ, None), AclEntry::allow_other(Perm::EXECUTE, None), AclEntry::allow_group("z", Perm::READ, None), AclEntry::allow_group("", Perm::READ, None), AclEntry::allow_group("a", Perm::READ, Flag::DEFAULT), ]; acl.sort(); let acl_sorted = vec![ AclEntry::allow_user("", Perm::READ, None), AclEntry::allow_user("f", Perm::WRITE, None), AclEntry::allow_user("z", Perm::READ, None), AclEntry::allow_group("", Perm::READ, None), AclEntry::allow_group("b", Perm::EXECUTE, None), AclEntry::allow_group("d", Perm::EXECUTE, None), AclEntry::allow_group("z", Perm::READ, None), AclEntry::allow_mask(Perm::READ, None), AclEntry::allow_other(Perm::EXECUTE, None), AclEntry::allow_group("a", Perm::READ, Flag::DEFAULT), ]; assert_eq!(acl, acl_sorted); } #[test] fn test_display_kind() { assert_eq!(format!("{}", AclEntryKind::User), "user"); assert_eq!(format!("{}", AclEntryKind::Group), "group"); } #[test] #[cfg(target_os = "macos")] fn test_display_entry() { let perms = Perm::READ | Perm::EXECUTE; let flags = Flag::INHERITED | Flag::FILE_INHERIT; let entry = AclEntry::allow_user("x", perms, flags); assert_eq!( format!("{entry}"), "allow:inherited,file_inherit:user:x:read,execute" ); } #[test] #[cfg(any(target_os = "linux", target_os = "freebsd"))] fn test_display_entry() { let perms = Perm::READ | Perm::EXECUTE; let flags = Flag::DEFAULT; let entry = AclEntry::allow_user("x", perms, flags); assert_eq!(format!("{entry}"), "allow:default:user:x:read,execute"); } #[test] fn test_display_entry_name() { let perms = Perm::READ; // FIXME: Need to have colons in user names escaped on output! let entry = AclEntry::allow_user("x:y", perms, None); assert_eq!(format!("{entry}"), "allow::user:x:y:read"); } #[test] #[cfg(target_os = "macos")] fn test_entry_fromstr() { let entry = "allow:inherited:user:x:read".parse::().unwrap(); assert_eq!(entry.to_string(), "allow:inherited:user:x:read"); let entry = "allow::user:x:read".parse::().unwrap(); assert_eq!(entry.to_string(), "allow::user:x:read"); let entry = "user:x:read".parse::().unwrap(); assert_eq!(entry.to_string(), "allow::user:x:read"); let entry = " deny : inherited : user : x : read " .parse::() .unwrap(); assert_eq!(entry.to_string(), "deny:inherited:user:x:read"); let entry = "inherited:user:x:read".parse::().unwrap(); assert_eq!(entry.to_string(), "allow:inherited:user:x:read"); } #[test] #[cfg(any(target_os = "linux", target_os = "freebsd"))] fn test_entry_fromstr() { let entry = "allow:default:user:x:read".parse::().unwrap(); assert_eq!(entry.to_string(), "allow:default:user:x:read"); let entry = "allow::user:x:read".parse::().unwrap(); assert_eq!(entry.to_string(), "allow::user:x:read"); let entry = "user:x:read".parse::().unwrap(); assert_eq!(entry.to_string(), "allow::user:x:read"); let entry = " deny : default : user : x : read " .parse::() .unwrap(); assert_eq!(entry.to_string(), "deny:default:user:x:read"); let entry = "default:user:x:read".parse::().unwrap(); assert_eq!(entry.to_string(), "allow:default:user:x:read"); } #[test] fn test_entry_fromstr_err() { // Mispelled "allow". let err = "all::user:x:read".parse::().unwrap_err(); assert_eq!( err.to_string(), "Unknown variant `all`, expected one of `allow`, `deny`" ); // Invalid format. let err = "allow:foo".parse::().unwrap_err(); assert_eq!(err.to_string(), "Unknown ACL format: `allow:foo`"); } #[test] fn test_entry_fromstr_roundtrip() { let values = [ ("user:a:read", "allow::user:a:read"), ("group:b:write", "allow::group:b:write"), ("unknown:c:execute", "allow::unknown:c:execute"), #[cfg(any(target_os = "linux", target_os = "freebsd"))] ("other:d:execute", "allow::other:d:execute"), #[cfg(any(target_os = "linux", target_os = "freebsd"))] ("mask:e:write,read", "allow::mask:e:read,write"), ]; for (input, expected) in &values { let entry = input.parse::().unwrap(); assert_eq!(*expected, entry.to_string()); } } #[test] fn test_entry_fromstr_examples() { let values = [ ("u:admin:rwx", "allow::user:admin:read,write,execute"), ("g::rw", "allow::group::read,write"), #[cfg(any(target_os = "linux", target_os = "freebsd"))] ("default:user:admin:r", "allow:default:user:admin:read"), #[cfg(any(target_os = "linux", target_os = "freebsd"))] ("d:group:admin:w", "allow:default:group:admin:write"), ("deny::u:self:x", "deny::user:self:execute"), ]; for (input, expected) in &values { let entry = input.parse::().unwrap(); assert_eq!(*expected, entry.to_string()); } } #[test] fn test_kind_fromstr() { assert_eq!(AclEntryKind::User, "user".parse::().unwrap()); assert_eq!( AclEntryKind::Group, "group".parse::().unwrap() ); assert_eq!( AclEntryKind::Unknown, "unknown".parse::().unwrap() ); #[cfg(any(target_os = "linux", target_os = "freebsd"))] assert_eq!(AclEntryKind::Mask, "mask".parse::().unwrap()); #[cfg(target_os = "macos")] assert_eq!( "unknown variant `x`, expected one of `user`, `group`, `unknown`", "x".parse::().unwrap_err().to_string() ); #[cfg(target_os = "linux")] assert_eq!( "unknown variant `x`, expected one of `user`, `group`, `mask`, `other`, `unknown`", "x".parse::().unwrap_err().to_string() ); #[cfg(target_os = "freebsd")] assert_eq!( "unknown variant `x`, expected one of `user`, `group`, `mask`, `other`, `everyone`, `unknown`", "x".parse::().unwrap_err().to_string() ); } } exacl-0.10.0/src/bindings.rs000064400000000000000000000006121046102023000137270ustar 00000000000000//! Rust bindings to system C API; exported via `sys`. #![allow( dead_code, non_camel_case_types, non_upper_case_globals, clippy::unseparated_literal_suffix, clippy::unreadable_literal, deref_nullptr, // https://github.com/rust-lang/rust-bindgen/issues/1651 clippy::too_many_lines, clippy::borrow_as_ptr )] include!(concat!(env!("OUT_DIR"), "/bindings.rs")); exacl-0.10.0/src/bititer.rs000064400000000000000000000115611046102023000136010ustar 00000000000000//! Implements a generic bit iterator. //! //! Works with built-in integer types or bitflags. You just have to implement //! the `BitIterable` trait. use std::ops::BitXorAssign; pub trait BitIterable: Copy + BitXorAssign { fn lsb(self) -> Option; fn msb(self) -> Option; } pub struct BitIter(pub T); impl Iterator for BitIter { type Item = T; fn next(&mut self) -> Option { self.0.lsb().map(|bit| { self.0 ^= bit; bit }) } } impl DoubleEndedIterator for BitIter { fn next_back(&mut self) -> Option { self.0.msb().map(|bit| { self.0 ^= bit; bit }) } } //////////////////////////////////////////////////////////////////////////////// #[cfg(test)] mod bititer_tests { #![allow(clippy::unreadable_literal)] use super::*; use bitflags::bitflags; impl BitIterable for u32 { fn lsb(self) -> Option { if self == 0 { return None; } Some(1 << self.trailing_zeros()) } fn msb(self) -> Option { if self == 0 { return None; } Some(1 << (31 - self.leading_zeros())) } } #[test] fn test_bititer_u32() { assert!(BitIter(0).next().is_none()); let v = BitIter(1).collect::>(); assert_eq!(v, vec![1]); let v = BitIter(1 << 31).collect::>(); assert_eq!(v, vec![1 << 31]); let v = BitIter(2 + 4 + 16 + 64).collect::>(); assert_eq!(v, vec![2, 4, 16, 64]); let v = BitIter(u32::MAX).collect::>(); assert_eq!(v.len(), 32); assert_eq!( v, vec![ 1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096, 8192, 16384, 32768, 65536, 131072, 262144, 524288, 1048576, 2097152, 4194304, 8388608, 16777216, 33554432, 67108864, 134217728, 268435456, 536870912, 1073741824, 2147483648 ] ); } #[test] fn test_bititer_u32_rev() { assert!(BitIter(0).rev().next().is_none()); let v = BitIter(1).rev().collect::>(); assert_eq!(v, vec![1]); let v = BitIter(1 << 31).rev().collect::>(); assert_eq!(v, vec![1 << 31]); let v = BitIter(2 + 4 + 16 + 64).rev().collect::>(); assert_eq!(v, vec![64, 16, 4, 2]); let v = BitIter(u32::MAX).rev().collect::>(); assert_eq!(v.len(), 32); assert_eq!( v, vec![ 2147483648, 1073741824, 536870912, 268435456, 134217728, 67108864, 33554432, 16777216, 8388608, 4194304, 2097152, 1048576, 524288, 262144, 131072, 65536, 32768, 16384, 8192, 4096, 2048, 1024, 512, 256, 128, 64, 32, 16, 8, 4, 2, 1 ] ); } bitflags! { #[derive(Default)] struct TestBit: u32 { const BIT1 = 1 << 0; const BIT2 = 1 << 1; const BIT3 = 1 << 5; } } impl BitIterable for TestBit { fn lsb(self) -> Option { if self.is_empty() { return None; } Some(TestBit { bits: 1 << self.bits.trailing_zeros(), }) } fn msb(self) -> Option { #[allow(clippy::cast_possible_truncation)] const MAX_BITS: u32 = 8 * std::mem::size_of::() as u32 - 1; if self.is_empty() { return None; } Some(TestBit { bits: 1 << (MAX_BITS - self.bits.leading_zeros()), }) } } #[test] fn test_bititer_bitflags() { let bits = TestBit::BIT1 | TestBit::BIT2 | TestBit::BIT3; let v = BitIter(TestBit::empty()).collect::>(); assert_eq!(v, vec![]); let v = BitIter(bits.bits).collect::>(); assert_eq!(v, vec![1, 2, 32]); let v = BitIter(bits).collect::>(); assert_eq!( v, vec![ TestBit { bits: 1 }, TestBit { bits: 2 }, TestBit { bits: 32 } ] ); } #[test] fn test_bititer_bitflags_rev() { let bits = TestBit::BIT1 | TestBit::BIT2 | TestBit::BIT3; let v = BitIter(TestBit::empty()).rev().collect::>(); assert_eq!(v, vec![]); let v = BitIter(bits.bits).rev().collect::>(); assert_eq!(v, vec![32, 2, 1]); let v = BitIter(bits).rev().collect::>(); assert_eq!( v, vec![ TestBit { bits: 32 }, TestBit { bits: 2 }, TestBit { bits: 1 } ] ); } } exacl-0.10.0/src/failx.rs000064400000000000000000000033061046102023000132400ustar 00000000000000//! Error handling convenience functions. #![allow(dead_code)] use log::debug; use std::fmt; use std::io; use std::path::Path; /// Log a message and return an [`io::Error`] with the value of errno. pub fn log_err(ret: R, func: &str, arg: T) -> io::Error where R: fmt::Display, T: fmt::Debug, { let err = io::Error::last_os_error(); debug!("{}({:?}) returned {}, err={}", func, arg, ret, err); err } /// Log a message and return an [`io::Error`] for a given error code. pub fn log_from_err(ret: i32, func: &str, arg: T) -> io::Error where T: fmt::Debug, { assert!(ret > 0); let err = io::Error::from_raw_os_error(ret); debug!("{}({:?}) returned {}, err={}", func, arg, ret, err); err } /// Log a message and return an [`io::Result`] with the value of errno. pub fn fail_err(ret: R, func: &str, arg: T) -> io::Result where R: fmt::Display, T: fmt::Debug, { Err(log_err(ret, func, arg)) } /// Log a message and return an [`io::Result`] for a given error code. pub fn fail_from_err(ret: i32, func: &str, arg: T) -> io::Result where T: fmt::Debug, { Err(log_from_err(ret, func, arg)) } /// Return a custom [`io::Result`] with the given message. pub fn fail_custom(msg: &str) -> io::Result { Err(io::Error::new(io::ErrorKind::Other, msg)) } /// Return a custom [`io::Error`] that prefixes the given error. pub fn custom_err(msg: &str, err: &io::Error) -> io::Error { io::Error::new(err.kind(), format!("{msg}: {err}")) } /// Return a custom [`io::Error`] that prefixes the given error with filename. pub fn path_err(path: &Path, err: &io::Error) -> io::Error { io::Error::new(err.kind(), format!("File {path:?}: {err}")) } exacl-0.10.0/src/flag.rs000064400000000000000000000262421046102023000130520ustar 00000000000000//! Implements the inheritance flags. use crate::bititer::{BitIter, BitIterable}; use crate::format; use crate::sys::*; use bitflags::bitflags; #[cfg(feature = "serde")] use serde::{de, ser, Deserialize, Serialize}; use std::fmt; bitflags! { /// Represents ACL entry inheritance flags. #[derive(Default)] pub struct Flag : acl_flag_t { /// ACL entry was inherited. #[cfg(any(docsrs, target_os = "macos", target_os = "freebsd"))] #[cfg_attr(docsrs, doc(cfg(any(target_os = "macos", target_os = "freebsd"))))] const INHERITED = np::ACL_ENTRY_INHERITED; /// Inherit to files. #[cfg(any(docsrs, target_os = "macos", target_os = "freebsd"))] #[cfg_attr(docsrs, doc(cfg(any(target_os = "macos", target_os = "freebsd"))))] const FILE_INHERIT = np::ACL_ENTRY_FILE_INHERIT; /// Inherit to directories. #[cfg(any(docsrs, target_os = "macos", target_os = "freebsd"))] #[cfg_attr(docsrs, doc(cfg(any(target_os = "macos", target_os = "freebsd"))))] const DIRECTORY_INHERIT = np::ACL_ENTRY_DIRECTORY_INHERIT; /// Clear the DIRECTORY_INHERIT flag in the ACL entry that is inherited. #[cfg(any(docsrs, target_os = "macos", target_os = "freebsd"))] #[cfg_attr(docsrs, doc(cfg(any(target_os = "macos", target_os = "freebsd"))))] const LIMIT_INHERIT = np::ACL_ENTRY_LIMIT_INHERIT; /// Don't consider this entry when processing the ACL. Just inherit it. #[cfg(any(docsrs, target_os = "macos", target_os = "freebsd"))] #[cfg_attr(docsrs, doc(cfg(any(target_os = "macos", target_os = "freebsd"))))] const ONLY_INHERIT = np::ACL_ENTRY_ONLY_INHERIT; /// Specifies a default ACL entry on Linux. #[cfg(any(docsrs, target_os = "linux", target_os = "freebsd"))] #[cfg_attr(docsrs, doc(cfg(any(target_os = "linux", target_os = "freebsd"))))] const DEFAULT = 1 << 13; #[cfg(any(docsrs, target_os = "freebsd"))] #[cfg_attr(docsrs, doc(cfg(target_os = "freebsd")))] /// NFSv4 Specific Flags on FreeBSD. const NFS4_SPECIFIC = Self::INHERITED.bits | Self::FILE_INHERIT.bits | Self::DIRECTORY_INHERIT.bits | Self::LIMIT_INHERIT.bits | Self::ONLY_INHERIT.bits; } } impl Flag { // On FreeBSD, acl_flag_t is a u16. On Linux and macOS, acl_flag_t is a u32. // To appease the linter, provide a helper function to cast Flag to u32. const fn as_u32(self) -> u32 { #[allow(clippy::unnecessary_cast)] return self.bits as u32; } } impl BitIterable for Flag { fn lsb(self) -> Option { if self.is_empty() { return None; } Some(Flag { bits: 1 << self.bits.trailing_zeros(), }) } fn msb(self) -> Option { // FIXME: Replace computation with `BITS` once it lands in stable. #[allow(clippy::cast_possible_truncation)] const MAX_BITS: acl_flag_t = 8 * std::mem::size_of::() as acl_flag_t - 1; if self.is_empty() { return None; } Some(Flag { bits: 1 << (MAX_BITS - self.bits.leading_zeros() as acl_flag_t), }) } } #[derive(Copy, Clone, Debug, PartialEq, Eq)] #[cfg_attr(feature = "serde", derive(Deserialize, Serialize))] #[repr(u32)] #[allow(non_camel_case_types)] pub enum FlagName { // *N.B.* Update the corresponding table in format/format_no_serde.rs // if any of these entries change. #[cfg(any(target_os = "macos", target_os = "freebsd"))] inherited = Flag::INHERITED.as_u32(), #[cfg(any(target_os = "macos", target_os = "freebsd"))] file_inherit = Flag::FILE_INHERIT.as_u32(), #[cfg(any(target_os = "macos", target_os = "freebsd"))] directory_inherit = Flag::DIRECTORY_INHERIT.as_u32(), #[cfg(any(target_os = "macos", target_os = "freebsd"))] limit_inherit = Flag::LIMIT_INHERIT.as_u32(), #[cfg(any(target_os = "macos", target_os = "freebsd"))] only_inherit = Flag::ONLY_INHERIT.as_u32(), #[cfg(any(target_os = "linux", target_os = "freebsd"))] default = Flag::DEFAULT.as_u32(), } impl FlagName { const fn from_flag(flag: Flag) -> Option { match flag { #[cfg(any(target_os = "macos", target_os = "freebsd"))] Flag::INHERITED => Some(FlagName::inherited), #[cfg(any(target_os = "macos", target_os = "freebsd"))] Flag::FILE_INHERIT => Some(FlagName::file_inherit), #[cfg(any(target_os = "macos", target_os = "freebsd"))] Flag::DIRECTORY_INHERIT => Some(FlagName::directory_inherit), #[cfg(any(target_os = "macos", target_os = "freebsd"))] Flag::LIMIT_INHERIT => Some(FlagName::limit_inherit), #[cfg(any(target_os = "macos", target_os = "freebsd"))] Flag::ONLY_INHERIT => Some(FlagName::only_inherit), #[cfg(any(target_os = "linux", target_os = "freebsd"))] Flag::DEFAULT => Some(FlagName::default), _ => None, } } const fn to_flag(self) -> Flag { Flag { bits: self as acl_flag_t, } } } impl fmt::Display for FlagName { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { format::write_flagname(f, *self) } } impl fmt::Display for Flag { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let mut iter = BitIter(*self & Flag::all()); if let Some(flag) = iter.next() { write!(f, "{}", FlagName::from_flag(flag).unwrap())?; for flag in iter { write!(f, ",{}", FlagName::from_flag(flag).unwrap())?; } } Ok(()) } } /// Parse an abbreviated flag ("d"). #[cfg(any(target_os = "linux", target_os = "freebsd"))] fn parse_flag_abbreviation(s: &str) -> Option { match s { "d" => Some(Flag::DEFAULT), _ => None, } } #[cfg(target_os = "macos")] const fn parse_flag_abbreviation(_s: &str) -> Option { None } impl std::str::FromStr for FlagName { type Err = format::Error; fn from_str(s: &str) -> Result { format::read_flagname(s) } } impl std::str::FromStr for Flag { type Err = format::Error; fn from_str(s: &str) -> Result { let mut result = Flag::empty(); for item in s.split(',') { let word = item.trim(); if !word.is_empty() { if let Some(flag) = parse_flag_abbreviation(word) { result |= flag; } else { result |= word.parse::()?.to_flag(); } } } Ok(result) } } #[cfg(feature = "serde")] impl ser::Serialize for Flag { fn serialize(&self, serializer: S) -> Result where S: ser::Serializer, { use ser::SerializeSeq; let mut seq = serializer.serialize_seq(None)?; for flag in BitIter(*self) { seq.serialize_element(&FlagName::from_flag(flag))?; } seq.end() } } #[cfg(feature = "serde")] impl<'de> de::Deserialize<'de> for Flag { fn deserialize(deserializer: D) -> Result where D: de::Deserializer<'de>, { struct FlagVisitor; impl<'de> de::Visitor<'de> for FlagVisitor { type Value = Flag; fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { formatter.write_str("list of flags") } fn visit_seq(self, mut seq: A) -> Result where A: de::SeqAccess<'de>, { let mut flags: Flag = Flag::empty(); while let Some(value) = seq.next_element()? { let name: FlagName = value; flags |= name.to_flag(); } Ok(flags) } } deserializer.deserialize_seq(FlagVisitor) } } //////////////////////////////////////////////////////////////////////////////// #[cfg(test)] mod flag_tests { use super::*; #[test] fn test_flag_display() { assert_eq!(Flag::empty().to_string(), ""); #[cfg(target_os = "macos")] { let flags = Flag::INHERITED | Flag::FILE_INHERIT; assert_eq!(flags.to_string(), "inherited,file_inherit"); let bad_flag = Flag { bits: 0x0080_0000 } | Flag::INHERITED; assert_eq!(bad_flag.to_string(), "inherited"); assert_eq!( Flag::all().to_string(), "inherited,file_inherit,directory_inherit,limit_inherit,only_inherit" ); } #[cfg(target_os = "linux")] { let flags = Flag::DEFAULT; assert_eq!(flags.to_string(), "default"); let bad_flag = Flag { bits: 0x8000 } | Flag::DEFAULT; assert_eq!(bad_flag.to_string(), "default"); assert_eq!(Flag::all().to_string(), "default"); } #[cfg(target_os = "freebsd")] { let flags = Flag::DEFAULT; assert_eq!(flags.to_string(), "default"); let bad_flag = Flag { bits: 0x8000 } | Flag::DEFAULT; assert_eq!(bad_flag.to_string(), "default"); assert_eq!( Flag::all().to_string(), "file_inherit,directory_inherit,limit_inherit,only_inherit,inherited,default" ); } } #[test] fn test_flag_fromstr() { #[cfg(target_os = "macos")] { assert_eq!(Flag::empty(), "".parse::().unwrap()); let flags = Flag::INHERITED | Flag::FILE_INHERIT; assert_eq!(flags, "inherited,file_inherit".parse().unwrap()); assert_eq!( Flag::all(), "inherited,file_inherit,directory_inherit,limit_inherit,only_inherit" .parse() .unwrap() ); 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()); } #[cfg(target_os = "linux")] { assert_eq!(Flag::empty(), "".parse::().unwrap()); assert_eq!(Flag::DEFAULT, "d".parse().unwrap()); assert_eq!(Flag::all(), "default".parse().unwrap()); assert_eq!( "unknown variant `bad_flag`, expected `default`", "bad_flag".parse::().unwrap_err().to_string() ); } #[cfg(target_os = "freebsd")] { assert_eq!(Flag::empty(), "".parse::().unwrap()); assert_eq!(Flag::DEFAULT, "d".parse().unwrap()); assert_eq!( Flag::all(), "default,inherited,file_inherit,directory_inherit,limit_inherit,only_inherit" .parse() .unwrap() ); assert_eq!( "unknown variant `bad_flag`, expected one of `inherited`, `file_inherit`, `directory_inherit`, `limit_inherit`, `only_inherit`, `default`", "bad_flag".parse::().unwrap_err().to_string() ); } } } exacl-0.10.0/src/format/format_no_serde.rs000064400000000000000000000123661046102023000166010ustar 00000000000000//! Implements helper functions for the built-in `AclEntry` format. //! These are used when `serde` is not available use std::fmt; use std::io; use crate::aclentry::AclEntryKind; use crate::flag::FlagName; use crate::perm::PermName; const ACLENTRYKINDS: &'static [(AclEntryKind, &'static str)] = &[ (AclEntryKind::User, "user"), (AclEntryKind::Group, "group"), #[cfg(any(target_os = "linux", target_os = "freebsd"))] (AclEntryKind::Mask, "mask"), #[cfg(any(target_os = "linux", target_os = "freebsd"))] (AclEntryKind::Other, "other"), #[cfg(target_os = "freebsd")] (AclEntryKind::Everyone, "everyone"), (AclEntryKind::Unknown, "unknown"), ]; const FLAGS: &'static [(FlagName, &'static str)] = &[ #[cfg(any(target_os = "macos", target_os = "freebsd"))] (FlagName::inherited, "inherited"), #[cfg(any(target_os = "macos", target_os = "freebsd"))] (FlagName::file_inherit, "file_inherit"), #[cfg(any(target_os = "macos", target_os = "freebsd"))] (FlagName::directory_inherit, "directory_inherit"), #[cfg(any(target_os = "macos", target_os = "freebsd"))] (FlagName::limit_inherit, "limit_inherit"), #[cfg(any(target_os = "macos", target_os = "freebsd"))] (FlagName::only_inherit, "only_inherit"), #[cfg(any(target_os = "linux", target_os = "freebsd"))] (FlagName::default, "default"), ]; const PERMS: &'static [(PermName, &'static str)] = &[ (PermName::read, "read"), (PermName::write, "write"), (PermName::execute, "execute"), #[cfg(target_os = "freebsd")] (PermName::read_data, "read_data"), #[cfg(target_os = "freebsd")] (PermName::write_data, "write_data"), #[cfg(any(target_os = "macos", target_os = "freebsd"))] (PermName::delete, "delete"), #[cfg(any(target_os = "macos", target_os = "freebsd"))] (PermName::append, "append"), #[cfg(any(target_os = "macos", target_os = "freebsd"))] (PermName::delete_child, "delete_child"), #[cfg(any(target_os = "macos", target_os = "freebsd"))] (PermName::readattr, "readattr"), #[cfg(any(target_os = "macos", target_os = "freebsd"))] (PermName::writeattr, "writeattr"), #[cfg(any(target_os = "macos", target_os = "freebsd"))] (PermName::readextattr, "readextattr"), #[cfg(any(target_os = "macos", target_os = "freebsd"))] (PermName::writeextattr, "writeextattr"), #[cfg(any(target_os = "macos", target_os = "freebsd"))] (PermName::readsecurity, "readsecurity"), #[cfg(any(target_os = "macos", target_os = "freebsd"))] (PermName::writesecurity, "writesecurity"), #[cfg(any(target_os = "macos", target_os = "freebsd"))] (PermName::chown, "chown"), #[cfg(any(target_os = "macos", target_os = "freebsd"))] (PermName::sync, "sync"), ]; /// Write value of an enum as a string using the given (enum, str) table. fn write_enum( f: &mut fmt::Formatter, value: T, table: &'static [(T, &'static str)], ) -> fmt::Result { match table.iter().find(|item| item.0 == value) { Some((_, name)) => write!(f, "{name}"), None => write!(f, "!!"), } } /// Read value of an enum from a string using the given (enum, str) table. fn read_enum(s: &str, table: &'static [(T, &'static str)]) -> Result { match table.iter().find(|item| item.1 == s) { Some((value, _)) => Ok(*value), None => Err(err_enum(s, table)), } } /// Produce the error message when the variant can't be found. fn err_enum(s: &str, table: &'static [(T, &'static str)]) -> Error { let variants = table .iter() .map(|item| format!("`{}`", item.1)) .collect::>() .join(", "); let msg = if table.len() == 1 { format!("unknown variant `{s}`, expected {variants}") } else { format!("unknown variant `{s}`, expected one of {variants}") }; Error::Message(msg) } /// Write value of an AclEntryKind. pub fn write_aclentrykind(f: &mut fmt::Formatter, value: AclEntryKind) -> fmt::Result { write_enum(f, value, ACLENTRYKINDS) } // Read value of an AclEntryKind. pub fn read_aclentrykind(s: &str) -> Result { read_enum(s, ACLENTRYKINDS) } /// Write value of a FlagName. pub fn write_flagname(f: &mut fmt::Formatter, value: FlagName) -> fmt::Result { write_enum(f, value, FLAGS) } // Read value of a FlagName. pub fn read_flagname(s: &str) -> Result { read_enum(s, FLAGS) } /// Write value of a PermName. pub fn write_permname(f: &mut fmt::Formatter, value: PermName) -> fmt::Result { write_enum(f, value, PERMS) } // Read value of a PermName. pub fn read_permname(s: &str) -> Result { read_enum(s, PERMS) } //////////////////////////////////////////////////////////////////////////////// #[derive(Clone, Debug, PartialEq)] pub enum Error { Message(String), NotImplemented, } impl From for io::Error { fn from(err: Error) -> Self { io::Error::new(io::ErrorKind::InvalidInput, err) } } impl fmt::Display for Error { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { Error::Message(msg) => write!(f, "{msg}"), Error::NotImplemented => write!(f, "Not implemented"), } } } impl std::error::Error for Error {} type Result = std::result::Result; exacl-0.10.0/src/format/format_serde.rs000064400000000000000000000346421046102023000161060ustar 00000000000000//! Implements helper functions for the built-in `AclEntry` format. //! These are used when `serde` is available use serde::de::{self, IntoDeserializer, Visitor}; use serde::{ser, Deserialize, Serialize}; use std::fmt; use std::io; use crate::aclentry::AclEntryKind; use crate::flag::FlagName; use crate::perm::PermName; /// Write value of a simple enum as a `serde` serialized string. #[allow(clippy::unnecessary_wraps)] fn write_enum(f: &mut fmt::Formatter, value: T) -> fmt::Result { let mut serializer = EnumSerializer(f); value .serialize(&mut serializer) .expect("can't serialize value"); Ok(()) } // Read value of a simple enum using a stub `serde` deserializer. fn read_enum<'a, T>(s: &'a str) -> Result where T: Deserialize<'a>, { let mut deserializer = EnumDeserializer(s); T::deserialize(&mut deserializer) } /// Write value of an `AclEntryKind`. pub fn write_aclentrykind(f: &mut fmt::Formatter, value: AclEntryKind) -> fmt::Result { write_enum(f, value) } // Read value of an `AclEntryKind`. pub fn read_aclentrykind(s: &str) -> Result { read_enum(s) } /// Write value of a `FlagName`. pub fn write_flagname(f: &mut fmt::Formatter, value: FlagName) -> fmt::Result { write_enum(f, value) } // Read value of a `FlagName`. pub fn read_flagname(s: &str) -> Result { read_enum(s) } /// Write value of a `PermName`. pub fn write_permname(f: &mut fmt::Formatter, value: PermName) -> fmt::Result { write_enum(f, value) } // Read value of a `PermName`. pub fn read_permname(s: &str) -> Result { read_enum(s) } //////////////////////////////////////////////////////////////////////////////// // This is a simple serializer class for enums. #[derive(Clone, Debug, PartialEq, Eq)] pub enum Error { Message(String), NotImplemented, } impl ser::Error for Error { fn custom(msg: T) -> Self { Error::Message(msg.to_string()) } } impl de::Error for Error { fn custom(msg: T) -> Self { Error::Message(msg.to_string()) } } impl From for io::Error { fn from(err: Error) -> Self { io::Error::new(io::ErrorKind::InvalidInput, err) } } impl fmt::Display for Error { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { Error::Message(msg) => write!(f, "{msg}"), Error::NotImplemented => write!(f, "Not implemented"), } } } impl std::error::Error for Error {} type Result = std::result::Result; //////////////////////////////////////////////////////////////////////////////// struct EnumSerializer<'a, 'b>(&'a mut fmt::Formatter<'b>); const fn not_implemented() -> Result { Err(Error::NotImplemented) } impl<'a, 'b> ser::Serializer for &mut EnumSerializer<'a, 'b> { type Ok = (); type Error = Error; type SerializeSeq = Self; type SerializeTuple = Self; type SerializeTupleStruct = Self; type SerializeTupleVariant = Self; type SerializeMap = Self; type SerializeStruct = Self; type SerializeStructVariant = Self; // Serialize a simple enum. fn serialize_unit_variant( self, _name: &'static str, _variant_index: u32, variant: &'static str, ) -> Result<()> { self.0.write_str(variant).expect("can't format enum"); Ok(()) } fn serialize_bool(self, _v: bool) -> Result<()> { not_implemented() } fn serialize_i8(self, _v: i8) -> Result<()> { not_implemented() } fn serialize_i16(self, _v: i16) -> Result<()> { not_implemented() } fn serialize_i32(self, _v: i32) -> Result<()> { not_implemented() } fn serialize_i64(self, _v: i64) -> Result<()> { not_implemented() } fn serialize_u8(self, _v: u8) -> Result<()> { not_implemented() } fn serialize_u16(self, _v: u16) -> Result<()> { not_implemented() } fn serialize_u32(self, _v: u32) -> Result<()> { not_implemented() } fn serialize_u64(self, _v: u64) -> Result<()> { not_implemented() } fn serialize_f32(self, _v: f32) -> Result<()> { not_implemented() } fn serialize_f64(self, _v: f64) -> Result<()> { not_implemented() } fn serialize_char(self, _v: char) -> Result<()> { not_implemented() } fn serialize_str(self, _v: &str) -> Result<()> { not_implemented() } fn serialize_bytes(self, _v: &[u8]) -> Result<()> { not_implemented() } fn serialize_none(self) -> Result<()> { not_implemented() } fn serialize_some(self, _v: &T) -> Result<()> where T: ?Sized + Serialize, { not_implemented() } fn serialize_unit(self) -> Result<()> { not_implemented() } fn serialize_unit_struct(self, _name: &'static str) -> Result<()> { not_implemented() } fn serialize_newtype_struct(self, _name: &'static str, _value: &T) -> Result<()> where T: ?Sized + Serialize, { not_implemented() } fn serialize_newtype_variant( self, _name: &'static str, _variant_index: u32, _variant: &'static str, _value: &T, ) -> Result<()> where T: ?Sized + Serialize, { not_implemented() } fn serialize_seq(self, _len: Option) -> Result { not_implemented() } fn serialize_tuple(self, _len: usize) -> Result { not_implemented() } fn serialize_tuple_struct( self, _name: &'static str, _len: usize, ) -> Result { not_implemented() } fn serialize_tuple_variant( self, _name: &'static str, _variant_index: u32, _variant: &'static str, _len: usize, ) -> Result { not_implemented() } fn serialize_map(self, _len: Option) -> Result { not_implemented() } fn serialize_struct(self, _name: &'static str, _len: usize) -> Result { not_implemented() } fn serialize_struct_variant( self, _name: &'static str, _variant_index: u32, _variant: &'static str, _len: usize, ) -> Result { not_implemented() } } impl<'a, 'b> ser::SerializeSeq for &mut EnumSerializer<'a, 'b> { type Ok = (); type Error = Error; fn serialize_element(&mut self, _value: &T) -> Result<()> where T: ?Sized + Serialize, { not_implemented() } fn end(self) -> Result<()> { not_implemented() } } impl<'a, 'b> ser::SerializeTuple for &mut EnumSerializer<'a, 'b> { type Ok = (); type Error = Error; fn serialize_element(&mut self, _value: &T) -> Result<()> where T: ?Sized + Serialize, { not_implemented() } fn end(self) -> Result<()> { not_implemented() } } impl<'a, 'b> ser::SerializeTupleStruct for &mut EnumSerializer<'a, 'b> { type Ok = (); type Error = Error; fn serialize_field(&mut self, _value: &T) -> Result<()> where T: ?Sized + Serialize, { not_implemented() } fn end(self) -> Result<()> { not_implemented() } } impl<'a, 'b> ser::SerializeTupleVariant for &mut EnumSerializer<'a, 'b> { type Ok = (); type Error = Error; fn serialize_field(&mut self, _value: &T) -> Result<()> where T: ?Sized + Serialize, { not_implemented() } fn end(self) -> Result<()> { not_implemented() } } impl<'a, 'b> ser::SerializeMap for &mut EnumSerializer<'a, 'b> { type Ok = (); type Error = Error; fn serialize_key(&mut self, _key: &T) -> Result<()> where T: ?Sized + Serialize, { not_implemented() } fn serialize_value(&mut self, _value: &T) -> Result<()> where T: ?Sized + Serialize, { not_implemented() } fn end(self) -> Result<()> { not_implemented() } } impl<'a, 'b> ser::SerializeStruct for &mut EnumSerializer<'a, 'b> { type Ok = (); type Error = Error; fn serialize_field(&mut self, _key: &'static str, _value: &T) -> Result<()> where T: ?Sized + Serialize, { not_implemented() } fn end(self) -> Result<()> { not_implemented() } } impl<'a, 'b> ser::SerializeStructVariant for &mut EnumSerializer<'a, 'b> { type Ok = (); type Error = Error; fn serialize_field(&mut self, _key: &'static str, _value: &T) -> Result<()> where T: ?Sized + Serialize, { not_implemented() } fn end(self) -> Result<()> { not_implemented() } } //////////////////////////////////////////////////////////////////////////////// #[cfg(test)] mod serialize_tests { use super::*; #[test] fn test_enum() { #[derive(Serialize)] enum E { Unit, } impl fmt::Display for E { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write_enum(f, self) } } let u = E::Unit; let expected = "Unit"; assert_eq!(format!("{u}"), expected); } } //////////////////////////////////////////////////////////////////////////////// struct EnumDeserializer<'de>(&'de str); impl<'de, 'a> de::Deserializer<'de> for &'a mut EnumDeserializer<'de> { type Error = Error; fn deserialize_any(self, _visitor: V) -> Result where V: Visitor<'de>, { not_implemented() } fn deserialize_bool(self, _visitor: V) -> Result where V: Visitor<'de>, { not_implemented() } fn deserialize_i8(self, _visitor: V) -> Result where V: Visitor<'de>, { not_implemented() } fn deserialize_i16(self, _visitor: V) -> Result where V: Visitor<'de>, { not_implemented() } fn deserialize_i32(self, _visitor: V) -> Result where V: Visitor<'de>, { not_implemented() } fn deserialize_i64(self, _visitor: V) -> Result where V: Visitor<'de>, { not_implemented() } fn deserialize_u8(self, _visitor: V) -> Result where V: Visitor<'de>, { not_implemented() } fn deserialize_u16(self, _visitor: V) -> Result where V: Visitor<'de>, { not_implemented() } fn deserialize_u32(self, _visitor: V) -> Result where V: Visitor<'de>, { not_implemented() } fn deserialize_u64(self, _visitor: V) -> Result where V: Visitor<'de>, { not_implemented() } fn deserialize_f32(self, _visitor: V) -> Result where V: Visitor<'de>, { not_implemented() } fn deserialize_f64(self, _visitor: V) -> Result where V: Visitor<'de>, { not_implemented() } fn deserialize_char(self, _visitor: V) -> Result where V: Visitor<'de>, { not_implemented() } fn deserialize_str(self, _visitor: V) -> Result where V: Visitor<'de>, { not_implemented() } fn deserialize_string(self, _visitor: V) -> Result where V: Visitor<'de>, { not_implemented() } fn deserialize_bytes(self, _visitor: V) -> Result where V: Visitor<'de>, { not_implemented() } fn deserialize_byte_buf(self, _visitor: V) -> Result where V: Visitor<'de>, { not_implemented() } fn deserialize_option(self, _visitor: V) -> Result where V: Visitor<'de>, { not_implemented() } fn deserialize_unit(self, _visitor: V) -> Result where V: Visitor<'de>, { not_implemented() } fn deserialize_unit_struct(self, _name: &'static str, _visitor: V) -> Result where V: Visitor<'de>, { not_implemented() } fn deserialize_newtype_struct(self, _name: &'static str, _visitor: V) -> Result where V: Visitor<'de>, { not_implemented() } fn deserialize_seq(self, _visitor: V) -> Result where V: Visitor<'de>, { not_implemented() } fn deserialize_tuple(self, _len: usize, _visitor: V) -> Result where V: Visitor<'de>, { not_implemented() } fn deserialize_tuple_struct( self, _name: &'static str, _len: usize, _visitor: V, ) -> Result where V: Visitor<'de>, { not_implemented() } fn deserialize_map(self, _visitor: V) -> Result where V: Visitor<'de>, { not_implemented() } fn deserialize_struct( self, _name: &'static str, _fields: &'static [&'static str], _visitor: V, ) -> Result where V: Visitor<'de>, { not_implemented() } fn deserialize_enum( self, _name: &'static str, _variants: &'static [&'static str], visitor: V, ) -> Result where V: Visitor<'de>, { let value = self.0; self.0 = ""; visitor.visit_enum(value.into_deserializer()) } fn deserialize_identifier(self, _visitor: V) -> Result where V: Visitor<'de>, { not_implemented() } fn deserialize_ignored_any(self, _visitor: V) -> Result where V: Visitor<'de>, { not_implemented() } } //////////////////////////////////////////////////////////////////////////////// #[cfg(test)] mod deserialize_tests { use super::*; #[test] fn test_enum() { #[derive(Deserialize, PartialEq, Debug)] enum E { Unit, } assert_eq!(E::Unit, read_enum("Unit").unwrap()); let res: Result = read_enum("Unitx"); assert_eq!( "unknown variant `Unitx`, expected `Unit`", res.unwrap_err().to_string() ); } } exacl-0.10.0/src/format/mod.rs000064400000000000000000000010021046102023000141730ustar 00000000000000//! Implements helper functions for the built-in `AclEntry` format. #[cfg(feature = "serde")] mod format_serde; #[cfg(not(feature = "serde"))] mod format_no_serde; #[cfg(feature = "serde")] pub use format_serde::{ read_aclentrykind, read_flagname, read_permname, write_aclentrykind, write_flagname, write_permname, Error, }; #[cfg(not(feature = "serde"))] pub use format_no_serde::{ read_aclentrykind, read_flagname, read_permname, write_aclentrykind, write_flagname, write_permname, Error, }; exacl-0.10.0/src/lib.rs000064400000000000000000000313431046102023000127050ustar 00000000000000//! # exacl //! //! Manipulate file system access control lists (ACL) on `macOS`, `Linux`, and //! `FreeBSD`. //! //! ## Example //! //! ```no_run //! # fn main() -> Result<(), Box> { //! use exacl::{getfacl, setfacl, AclEntry, Perm}; //! //! // Get the ACL from "./tmp/foo". //! let mut acl = getfacl("./tmp/foo", None)?; //! //! // Print the contents of the ACL. //! for entry in &acl { //! println!("{entry}"); //! } //! //! // Add an ACL entry to the end. //! acl.push(AclEntry::allow_user("some_user", Perm::READ, None)); //! //! // Set the ACL for "./tmp/foo". //! setfacl(&["./tmp/foo"], &acl, None)?; //! //! # Ok(()) } //! ``` //! //! ## API //! //! This module provides two high level functions, [`getfacl`] and [`setfacl`]. //! //! - [`getfacl`] retrieves the ACL for a file or directory. //! - [`setfacl`] sets the ACL for files or directories. //! //! On Linux and `FreeBSD`, the ACL contains entries for the default ACL, if //! present. //! //! Both [`getfacl`] and [`setfacl`] work with a `Vec`. The //! [`AclEntry`] structure contains five fields: //! //! - kind : [`AclEntryKind`] - the kind of entry (User, Group, Other, Mask, //! or Unknown). //! - name : [`String`] - name of the principal being given access. You can //! use a user/group name, decimal uid/gid, or UUID (on macOS). //! - perms : [`Perm`] - permission bits for the entry. //! - flags : [`Flag`] - flags indicating whether an entry is inherited, etc. //! - allow : [`bool`] - true if entry is allowed; false means deny. Linux only //! supports allow=true. #![warn(missing_docs)] #![cfg_attr(docsrs, feature(doc_cfg))] mod acl; mod aclentry; mod bindings; mod bititer; mod failx; mod flag; mod format; mod perm; mod qualifier; mod sys; mod unix; mod util; // Export AclOption, AclEntry, AclEntryKind, Flag and Perm. pub use acl::AclOption; pub use aclentry::{AclEntry, AclEntryKind}; pub use flag::Flag; pub use perm::Perm; use acl::Acl; use failx::custom_err; use std::io::{self, BufRead}; use std::path::Path; #[cfg(not(target_os = "macos"))] use failx::fail_custom; /// Get access control list (ACL) for a file or directory. /// /// On success, returns a vector of [`AclEntry`] with all access control entries /// for the specified path. The semantics and permissions of the access control /// list depend on the underlying platform. /// /// # macOS /// /// The ACL only includes the extended entries beyond the normal permssion mode /// of the file. macOS provides several ACL entry flags to specify how entries /// may be inherited by directory sub-items. If there's no extended ACL for a /// file, this function may return zero entries. /// /// If `path` points to a symlink, `getfacl` returns the ACL of the file pointed /// to by the symlink. Use [`AclOption::SYMLINK_ACL`] to obtain the ACL of a symlink /// itself. /// /// [`AclOption::DEFAULT_ACL`] option is not supported on macOS. /// /// # Linux /// /// The ACL includes entries related to the permission mode of the file. These /// are marked with empty names (""). /// /// Both the access ACL and the default ACL are returned in one list, with /// the default ACL entries indicated by a [`Flag::DEFAULT`] flag. /// /// If `path` points to a symlink, `getfacl` returns the ACL of the file pointed /// to by the symlink. [`AclOption::SYMLINK_ACL`] is not supported on Linux. /// /// [`AclOption::DEFAULT_ACL`] causes `getfacl` to only include entries for the /// default ACL, if present for a directory path. When called with /// [`AclOption::DEFAULT_ACL`], `getfacl` may return zero entries. /// /// # Example /// /// ```no_run /// # fn main() -> Result<(), Box> { /// use exacl::getfacl; /// /// let entries = getfacl("./tmp/foo", None)?; /// # Ok(()) } /// ``` /// /// # Errors /// /// Returns an [`io::Error`] on failure. pub fn getfacl(path: P, options: O) -> io::Result> where P: AsRef, O: Into>, { _getfacl(path.as_ref(), options.into().unwrap_or_default()) } #[cfg(target_os = "macos")] fn _getfacl(path: &Path, options: AclOption) -> io::Result> { Acl::read(path, options)?.entries() } #[cfg(not(target_os = "macos"))] fn _getfacl(path: &Path, options: AclOption) -> io::Result> { if options.contains(AclOption::ACCESS_ACL | AclOption::DEFAULT_ACL) { fail_custom("ACCESS_ACL and DEFAULT_ACL are mutually exclusive options") } else if options.intersects(AclOption::ACCESS_ACL | AclOption::DEFAULT_ACL) { Acl::read(path, options)?.entries() } else { let acl = Acl::read(path, options)?; let mut entries = acl.entries()?; if acl.is_posix() { let mut default = Acl::read( path, options | AclOption::DEFAULT_ACL | AclOption::IGNORE_EXPECTED_FILE_ERR, )? .entries()?; entries.append(&mut default); } Ok(entries) } } /// Set access control list (ACL) for specified files and directories. /// /// Sets the ACL for the specified paths using the given access control entries. /// The semantics and permissions of the access control list depend on the /// underlying platform. /// /// # macOS /// /// The ACL contains extended entries beyond the usual mode permission bits. /// An entry may allow or deny access to a specific user or group. /// To specify inherited entries, use the provided [Flag] values. /// /// ### macOS Example /// /// ```ignore /// # fn main() -> Result<(), Box> { /// use exacl::{setfacl, AclEntry, Flag, Perm}; /// /// let entries = vec![ /// AclEntry::allow_user("some_user", Perm::READ | Perm::WRITE, None), /// AclEntry::deny_group("some_group", Perm::WRITE, None) /// ]; /// /// setfacl(&["./tmp/foo"], &entries, None)?; /// # Ok(()) } /// ``` /// /// # Linux /// /// Each entry can only allow access; denying access using allow=false is not /// supported on Linux. /// /// The ACL *must* contain entries for the permssion modes of the file. Use /// the [`AclEntry::allow_other`] and [`AclEntry::allow_mask`] functions to /// specify the mode's other and mask permissions. Use "" as the name for the /// file owner and group owner. /// /// If an ACL contains a named user or group, there should be a /// [`AclEntryKind::Mask`] entry included. If a one entry is not provided, one /// will be computed. /// /// The access control entries may include entries for the default ACL, if one /// is desired. When `setfacl` is called with no [`Flag::DEFAULT`] entries, it /// deletes the default ACL. /// /// ### Linux Example /// /// ```ignore /// # fn main() -> Result<(), Box> { /// use exacl::{setfacl, AclEntry, Flag, Perm}; /// /// let entries = vec![ /// AclEntry::allow_user("", Perm::READ | Perm::WRITE, None), /// AclEntry::allow_group("", Perm::READ, None), /// AclEntry::allow_other(Perm::empty(), None), /// AclEntry::allow_user("some_user", Perm::READ | Perm::WRITE, None), /// ]; /// /// setfacl(&["./tmp/foo"], &entries, None)?; /// # Ok(()) } /// ``` /// /// # Errors /// /// Returns an [`io::Error`] on failure. pub fn setfacl(paths: &[P], entries: &[AclEntry], options: O) -> io::Result<()> where P: AsRef, O: Into>, { _setfacl(paths, entries, options.into().unwrap_or_default()) } #[cfg(target_os = "macos")] fn _setfacl

(paths: &[P], entries: &[AclEntry], options: AclOption) -> io::Result<()> where P: AsRef, { let acl = Acl::from_entries(entries).map_err(|err| custom_err("Invalid ACL", &err))?; for path in paths { acl.write(path.as_ref(), options)?; } Ok(()) } #[cfg(not(target_os = "macos"))] fn _setfacl

(paths: &[P], entries: &[AclEntry], options: AclOption) -> io::Result<()> where P: AsRef, { if options.contains(AclOption::ACCESS_ACL | AclOption::DEFAULT_ACL) { fail_custom("ACCESS_ACL and DEFAULT_ACL are mutually exclusive options")?; } else if options.intersects(AclOption::ACCESS_ACL | AclOption::DEFAULT_ACL) { let acl = Acl::from_entries(entries).map_err(|err| custom_err("Invalid ACL", &err))?; for path in paths { acl.write(path.as_ref(), options)?; } } else { let (access_acl, default_acl) = Acl::from_unified_entries(entries).map_err(|err| custom_err("Invalid ACL", &err))?; if access_acl.is_empty() { fail_custom("Invalid ACL: missing required entries")?; } for path in paths { let path = path.as_ref(); if access_acl.is_posix() { // Try to set default acl first. This will fail if path is not // a directory and default_acl is non-empty. This ordering // avoids leaving the file's ACL in a partially changed state // after an error (simply because it was a non-directory). default_acl.write( path, options | AclOption::DEFAULT_ACL | AclOption::IGNORE_EXPECTED_FILE_ERR, )?; } access_acl.write(path, options)?; } } Ok(()) } /// Write ACL entries to text. /// /// Each ACL entry is printed on a separate line. The five fields are separated /// by colons: /// /// ```text /// :::: /// /// - one of "allow" or "deny" /// - comma-separated list of flags /// - one of "user", "group", "other", "mask", "unknown" /// - user/group name (or decimal id if not known) /// - comma-separated list of permissions /// ``` /// /// Each record, including the last, is terminated by a final newline. /// /// # Sample Output /// /// ```text /// allow::group:admin:read,write /// ``` /// /// # Errors /// /// Returns an [`io::Error`] on failure. pub fn to_writer(mut writer: W, entries: &[AclEntry]) -> io::Result<()> { for entry in entries { writeln!(writer, "{entry}")?; } Ok(()) } /// Read ACL entries from text. /// /// Each ACL entry is presented on a separate line. A comment begins with `#` /// and proceeds to the end of the line. Within a field, leading or trailing /// white space are ignored. /// /// ```text /// Three allowed forms: /// /// :::: /// ::: /// :: /// /// - one of "allow" or "deny" /// - comma-separated list of flags /// - one of "user", "group", "other", "mask", "unknown" /// - user/group name (decimal id accepted) /// - comma-separated list of permissions /// ``` /// /// Supported flags and permissions vary by platform. /// /// Supported abbreviations: d = default, r = read, w = write, x = execute, /// u = user, g = group, o = other, m = mask /// /// # Sample Input /// /// ```text /// allow::group:admin:read,write /// g:admin:rw # ignored /// d:u:chip:rw /// deny:file_inherit:user:chet:rwx /// ``` /// /// # Errors /// /// Returns an [`io::Error`] on failure. pub fn from_reader(reader: R) -> io::Result> { let mut result = Vec::::new(); let buf = io::BufReader::new(reader); for line_result in buf.lines() { let line = line_result?; let src_line = trim_comment(&line).trim(); if !src_line.is_empty() { result.push(src_line.parse::()?); } } Ok(result) } /// Return line with end of line comment removed. fn trim_comment(line: &str) -> &str { line.find('#').map_or(line, |n| &line[0..n]) } /// Write ACL entries to text. /// /// See `to_writer` for the format. /// /// # Errors /// /// Returns an [`io::Error`] on failure. pub fn to_string(entries: &[AclEntry]) -> io::Result { let mut buf = Vec::::with_capacity(128); to_writer(&mut buf, entries)?; String::from_utf8(buf).map_err(|err| io::Error::new(io::ErrorKind::Other, err)) } /// Read ACL entries from text. /// /// See `from_reader` for the format. /// /// # Errors /// /// Returns an [`io::Error`] on failure. pub fn from_str(s: &str) -> io::Result> { from_reader(s.as_bytes()) } /// Construct a minimal ACL from the traditional `mode` permission bits. /// /// Returns a `Vec` for a minimal ACL with three entries corresponding /// to the owner/group/other permission bits given in `mode`. /// /// Extra bits outside the mask 0o777 are ignored. #[cfg(any(docsrs, target_os = "linux", target_os = "freebsd"))] #[cfg_attr(docsrs, doc(cfg(any(target_os = "linux", target_os = "freebsd"))))] #[must_use] pub fn from_mode(mode: u32) -> Vec { vec![ AclEntry::allow_user("", Perm::from_bits_truncate((mode >> 6) & 7), None), AclEntry::allow_group("", Perm::from_bits_truncate((mode >> 3) & 7), None), AclEntry::allow_other(Perm::from_bits_truncate(mode & 7), None), ] } exacl-0.10.0/src/perm.rs000064400000000000000000000407301046102023000131020ustar 00000000000000//! Implements the permissions flags. use crate::bititer::{BitIter, BitIterable}; use crate::format; use crate::sys::*; use bitflags::bitflags; #[cfg(feature = "serde")] use serde::{de, ser, Deserialize, Serialize}; use std::fmt; bitflags! { /// Represents file access permissions. #[derive(Default)] pub struct Perm : acl_perm_t { /// READ_DATA permission for a file. /// Same as LIST_DIRECTORY permission for a directory. const READ = ACL_READ; /// WRITE_DATA permission for a file. /// Same as ADD_FILE permission for a directory. const WRITE = ACL_WRITE; /// EXECUTE permission for a file. /// Same as SEARCH permission for a directory. const EXECUTE = ACL_EXECUTE; /// DELETE permission for a file. #[cfg(any(docsrs, target_os = "macos", target_os = "freebsd"))] #[cfg_attr(docsrs, doc(cfg(any(target_os = "macos", target_os = "freebsd"))))] const DELETE = np::ACL_DELETE; /// APPEND_DATA permission for a file. /// Same as ADD_SUBDIRECTORY permission for a directory. #[cfg(any(docsrs, target_os = "macos", target_os = "freebsd"))] #[cfg_attr(docsrs, doc(cfg(any(target_os = "macos", target_os = "freebsd"))))] const APPEND = np::ACL_APPEND_DATA; /// DELETE_CHILD permission for a directory. #[cfg(any(docsrs, target_os = "macos", target_os = "freebsd"))] #[cfg_attr(docsrs, doc(cfg(any(target_os = "macos", target_os = "freebsd"))))] const DELETE_CHILD = np::ACL_DELETE_CHILD; /// READ_ATTRIBUTES permission for file or directory. #[cfg(any(docsrs, target_os = "macos", target_os = "freebsd"))] #[cfg_attr(docsrs, doc(cfg(any(target_os = "macos", target_os = "freebsd"))))] const READATTR = np::ACL_READ_ATTRIBUTES; /// WRITE_ATTRIBUTES permission for a file or directory. #[cfg(any(docsrs, target_os = "macos", target_os = "freebsd"))] #[cfg_attr(docsrs, doc(cfg(any(target_os = "macos", target_os = "freebsd"))))] const WRITEATTR = np::ACL_WRITE_ATTRIBUTES; /// READ_EXTATTRIBUTES permission for a file or directory. #[cfg(any(docsrs, target_os = "macos", target_os = "freebsd"))] #[cfg_attr(docsrs, doc(cfg(any(target_os = "macos", target_os = "freebsd"))))] const READEXTATTR = np::ACL_READ_EXTATTRIBUTES; /// WRITE_EXTATTRIBUTES permission for a file or directory. #[cfg(any(docsrs, target_os = "macos", target_os = "freebsd"))] #[cfg_attr(docsrs, doc(cfg(any(target_os = "macos", target_os = "freebsd"))))] const WRITEEXTATTR = np::ACL_WRITE_EXTATTRIBUTES; /// READ_SECURITY permission for a file or directory. #[cfg(any(docsrs, target_os = "macos", target_os = "freebsd"))] #[cfg_attr(docsrs, doc(cfg(any(target_os = "macos", target_os = "freebsd"))))] const READSECURITY = np::ACL_READ_SECURITY; /// WRITE_SECURITY permission for a file or directory. #[cfg(any(docsrs, target_os = "macos", target_os = "freebsd"))] #[cfg_attr(docsrs, doc(cfg(any(target_os = "macos", target_os = "freebsd"))))] const WRITESECURITY = np::ACL_WRITE_SECURITY; /// CHANGE_OWNER permission for a file or directory. #[cfg(any(docsrs, target_os = "macos", target_os = "freebsd"))] #[cfg_attr(docsrs, doc(cfg(any(target_os = "macos", target_os = "freebsd"))))] const CHOWN = np::ACL_CHANGE_OWNER; /// SYNCHRONIZE permission (unsupported). #[cfg(any(docsrs, target_os = "macos", target_os = "freebsd"))] #[cfg_attr(docsrs, doc(cfg(any(target_os = "macos", target_os = "freebsd"))))] const SYNC = np::ACL_SYNCHRONIZE; /// NFSv4 READ_DATA permission. #[cfg(any(docsrs, target_os = "freebsd"))] #[cfg_attr(docsrs, doc(cfg(target_os = "freebsd")))] const READ_DATA = np::ACL_READ_DATA; /// NFSv4 WRITE_DATA permission. #[cfg(any(docsrs, target_os = "freebsd"))] #[cfg_attr(docsrs, doc(cfg(target_os = "freebsd")))] const WRITE_DATA = np::ACL_WRITE_DATA; /// Posix specific permissions. #[cfg(any(docsrs, target_os = "freebsd"))] #[cfg_attr(docsrs, doc(cfg(target_os = "freebsd")))] const POSIX_SPECIFIC = Self::READ.bits | Self::WRITE.bits | Self::EXECUTE.bits; /// All NFSv4 specific permissions. #[cfg(any(docsrs, target_os = "freebsd"))] #[cfg_attr(docsrs, doc(cfg(target_os = "freebsd")))] const NFS4_SPECIFIC = Self::READ_DATA.bits | Self::WRITE_DATA.bits | Self::DELETE.bits | Self::APPEND.bits | Self::DELETE_CHILD.bits | Self::READATTR.bits | Self::WRITEATTR.bits | Self::READEXTATTR.bits | Self::WRITEEXTATTR.bits | Self::READSECURITY.bits | Self::WRITESECURITY.bits | Self::CHOWN.bits | Self::SYNC.bits; } } #[cfg(any(target_os = "linux", target_os = "freebsd"))] type RevPermIter = std::iter::Rev>; impl Perm { #[cfg(target_os = "macos")] fn iter(self) -> BitIter { BitIter(self & Perm::all()) } #[cfg(target_os = "linux")] fn iter(self) -> RevPermIter { BitIter(self & Perm::all()).rev() } #[cfg(target_os = "freebsd")] fn iter(self) -> std::iter::Chain> { BitIter(self & Perm::POSIX_SPECIFIC) .rev() .chain(BitIter(self & Perm::NFS4_SPECIFIC)) } } impl BitIterable for Perm { fn lsb(self) -> Option { if self.is_empty() { return None; } Some(Perm { bits: 1 << self.bits.trailing_zeros(), }) } fn msb(self) -> Option { #[allow(clippy::cast_possible_truncation)] const MAX_BITS: acl_perm_t = 8 * std::mem::size_of::() as acl_perm_t - 1; if self.is_empty() { return None; } Some(Perm { bits: 1 << (MAX_BITS - self.bits.leading_zeros()), }) } } #[derive(Copy, Clone, Debug, PartialEq, Eq)] #[cfg_attr(feature = "serde", derive(Deserialize, Serialize))] #[repr(u32)] #[allow(non_camel_case_types)] pub enum PermName { // *N.B.* Update the corresponding table in format/format_no_serde.rs // if any of these entries change. read = Perm::READ.bits, write = Perm::WRITE.bits, execute = Perm::EXECUTE.bits, #[cfg(target_os = "freebsd")] read_data = Perm::READ_DATA.bits, #[cfg(target_os = "freebsd")] write_data = Perm::WRITE_DATA.bits, #[cfg(any(target_os = "macos", target_os = "freebsd"))] delete = Perm::DELETE.bits, #[cfg(any(target_os = "macos", target_os = "freebsd"))] append = Perm::APPEND.bits, #[cfg(any(target_os = "macos", target_os = "freebsd"))] delete_child = Perm::DELETE_CHILD.bits, #[cfg(any(target_os = "macos", target_os = "freebsd"))] readattr = Perm::READATTR.bits, #[cfg(any(target_os = "macos", target_os = "freebsd"))] writeattr = Perm::WRITEATTR.bits, #[cfg(any(target_os = "macos", target_os = "freebsd"))] readextattr = Perm::READEXTATTR.bits, #[cfg(any(target_os = "macos", target_os = "freebsd"))] writeextattr = Perm::WRITEEXTATTR.bits, #[cfg(any(target_os = "macos", target_os = "freebsd"))] readsecurity = Perm::READSECURITY.bits, #[cfg(any(target_os = "macos", target_os = "freebsd"))] writesecurity = Perm::WRITESECURITY.bits, #[cfg(any(target_os = "macos", target_os = "freebsd"))] chown = Perm::CHOWN.bits, #[cfg(any(target_os = "macos", target_os = "freebsd"))] sync = Perm::SYNC.bits, } impl PermName { const fn from_perm(perm: Perm) -> Option { match perm { Perm::READ => Some(PermName::read), Perm::WRITE => Some(PermName::write), Perm::EXECUTE => Some(PermName::execute), #[cfg(target_os = "freebsd")] Perm::READ_DATA => Some(PermName::read_data), #[cfg(target_os = "freebsd")] Perm::WRITE_DATA => Some(PermName::write_data), #[cfg(any(target_os = "macos", target_os = "freebsd"))] Perm::DELETE => Some(PermName::delete), #[cfg(any(target_os = "macos", target_os = "freebsd"))] Perm::APPEND => Some(PermName::append), #[cfg(any(target_os = "macos", target_os = "freebsd"))] Perm::DELETE_CHILD => Some(PermName::delete_child), #[cfg(any(target_os = "macos", target_os = "freebsd"))] Perm::READATTR => Some(PermName::readattr), #[cfg(any(target_os = "macos", target_os = "freebsd"))] Perm::WRITEATTR => Some(PermName::writeattr), #[cfg(any(target_os = "macos", target_os = "freebsd"))] Perm::READEXTATTR => Some(PermName::readextattr), #[cfg(any(target_os = "macos", target_os = "freebsd"))] Perm::WRITEEXTATTR => Some(PermName::writeextattr), #[cfg(any(target_os = "macos", target_os = "freebsd"))] Perm::READSECURITY => Some(PermName::readsecurity), #[cfg(any(target_os = "macos", target_os = "freebsd"))] Perm::WRITESECURITY => Some(PermName::writesecurity), #[cfg(any(target_os = "macos", target_os = "freebsd"))] Perm::CHOWN => Some(PermName::chown), #[cfg(any(target_os = "macos", target_os = "freebsd"))] Perm::SYNC => Some(PermName::sync), _ => None, } } const fn to_perm(self) -> Perm { Perm { bits: self as u32 } } } impl fmt::Display for PermName { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { format::write_permname(f, *self) } } impl fmt::Display for Perm { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let mut iter = self.iter(); if let Some(perm) = iter.next() { write!(f, "{}", PermName::from_perm(perm).unwrap())?; for perm in iter { write!(f, ",{}", PermName::from_perm(perm).unwrap())?; } } Ok(()) } } /// Parse an abbreviated permission, "rwx", "wx", "r-x" etc. /// /// Order doesn't matter. "xwr" is the same as "rwx". Allow for "r-x" by /// ignoring any number of '-'. Don't allow r, w, or x to be repeated. fn parse_perm_abbreviation(s: &str) -> Option { let mut perms = Perm::empty(); for ch in s.chars() { match ch { 'r' if !perms.contains(Perm::READ) => perms |= Perm::READ, 'w' if !perms.contains(Perm::WRITE) => perms |= Perm::WRITE, 'x' if !perms.contains(Perm::EXECUTE) => perms |= Perm::EXECUTE, '-' => (), // Any other character is invalid. _ => return None, } } Some(perms) } impl std::str::FromStr for PermName { type Err = format::Error; fn from_str(s: &str) -> Result { format::read_permname(s) } } impl std::str::FromStr for Perm { type Err = format::Error; fn from_str(s: &str) -> Result { let mut result = Perm::empty(); for item in s.split(',') { let word = item.trim(); if !word.is_empty() { if let Some(perms) = parse_perm_abbreviation(word) { result |= perms; } else { result |= word.parse::()?.to_perm(); } } } Ok(result) } } #[cfg(feature = "serde")] impl ser::Serialize for Perm { fn serialize(&self, serializer: S) -> Result where S: ser::Serializer, { use ser::SerializeSeq; let mut seq = serializer.serialize_seq(None)?; for perm in self.iter() { seq.serialize_element(&PermName::from_perm(perm))?; } seq.end() } } #[cfg(feature = "serde")] impl<'de> de::Deserialize<'de> for Perm { fn deserialize(deserializer: D) -> Result where D: de::Deserializer<'de>, { struct PermVisitor; impl<'de> de::Visitor<'de> for PermVisitor { type Value = Perm; fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { formatter.write_str("list of permissions") } fn visit_seq(self, mut seq: A) -> Result where A: de::SeqAccess<'de>, { let mut perms: Perm = Perm::empty(); while let Some(value) = seq.next_element()? { let name: PermName = value; perms |= name.to_perm(); } Ok(perms) } } deserializer.deserialize_seq(PermVisitor) } } //////////////////////////////////////////////////////////////////////////////// #[cfg(test)] mod perm_tests { use super::*; #[test] #[cfg(target_os = "macos")] fn test_perm_equivalences() { assert_eq!(acl_perm_t_ACL_READ_DATA, acl_perm_t_ACL_LIST_DIRECTORY); assert_eq!(acl_perm_t_ACL_WRITE_DATA, acl_perm_t_ACL_ADD_FILE); assert_eq!(acl_perm_t_ACL_EXECUTE, acl_perm_t_ACL_SEARCH); assert_eq!(acl_perm_t_ACL_APPEND_DATA, acl_perm_t_ACL_ADD_SUBDIRECTORY); } #[test] fn test_perm_display() { assert_eq!(Perm::empty().to_string(), ""); let perms = Perm::READ | Perm::EXECUTE; assert_eq!(perms.to_string(), "read,execute"); let bad_perm = Perm { bits: 0x0080_0000 } | Perm::READ; assert_eq!(bad_perm.to_string(), "read"); #[cfg(target_os = "macos")] assert_eq!(Perm::all().to_string(), "read,write,execute,delete,append,delete_child,readattr,writeattr,readextattr,writeextattr,readsecurity,writesecurity,chown,sync"); #[cfg(target_os = "linux")] assert_eq!(Perm::all().to_string(), "read,write,execute"); #[cfg(target_os = "freebsd")] assert_eq!(Perm::all().to_string(), "read,write,execute,read_data,write_data,append,readextattr,writeextattr,delete_child,readattr,writeattr,delete,readsecurity,writesecurity,chown,sync"); } #[test] fn test_perm_fromstr() { let flags = Perm::READ | Perm::EXECUTE; assert_eq!(flags, "read, execute".parse().unwrap()); assert_eq!(flags, "rx".parse().unwrap()); assert_eq!(flags, "r-x".parse().unwrap()); assert_eq!(flags, "--x--r--".parse().unwrap()); assert_eq!(flags, "xr".parse().unwrap()); assert_eq!(Perm::WRITE, "w".parse().unwrap()); assert_eq!(Perm::empty(), "".parse().unwrap()); // Duplicate abbreviations not supported. assert!("rr".parse::().is_err()); #[cfg(target_os = "macos")] { assert_eq!("unknown variant `q`, expected one of `read`, `write`, `execute`, `delete`, `append`, `delete_child`, `readattr`, `writeattr`, `readextattr`, `writeextattr`, `readsecurity`, `writesecurity`, `chown`, `sync`", " ,q ".parse::().unwrap_err().to_string()); assert_eq!(Perm::all(), "read,write,execute,delete,append,delete_child,readattr,writeattr,readextattr,writeextattr,readsecurity,writesecurity,chown,sync".parse().unwrap()); } #[cfg(target_os = "linux")] { assert_eq!( "unknown variant `qq`, expected one of `read`, `write`, `execute`", " ,qq ".parse::().unwrap_err().to_string() ); assert_eq!(Perm::all(), "read,write,execute".parse().unwrap()); } #[cfg(target_os = "freebsd")] { assert_eq!( "unknown variant `qq`, expected one of `read`, `write`, `execute`, `read_data`, `write_data`, `delete`, `append`, `delete_child`, `readattr`, `writeattr`, `readextattr`, `writeextattr`, `readsecurity`, `writesecurity`, `chown`, `sync`", " ,qq ".parse::().unwrap_err().to_string() ); assert_eq!(Perm::all(), "read,write,execute,delete,append,delete_child,readattr,writeattr,readextattr,writeextattr,readsecurity,writesecurity,chown,sync,read_data,write_data".parse().unwrap()); } } #[test] #[cfg(any(target_os = "linux", target_os = "freebsd"))] fn test_perm_unix_permission() { // Test that READ, WRITE, EXECUTE constant correspond to the same bits // as the permissions in unix mode. assert_eq!(Perm::READ.bits, 0x04); assert_eq!(Perm::WRITE.bits, 0x02); assert_eq!(Perm::EXECUTE.bits, 0x01); assert_eq!( Perm::from_bits(0x07), Some(Perm::READ | Perm::WRITE | Perm::EXECUTE) ); } } exacl-0.10.0/src/qualifier.rs000064400000000000000000000204741046102023000141230ustar 00000000000000//! Implements the `Qualifier` type for internal use use crate::failx::*; use crate::unix; use std::fmt; use std::io; #[cfg(target_os = "macos")] use uuid::Uuid; #[cfg(any(target_os = "linux", target_os = "freebsd"))] const OWNER_NAME: &str = ""; #[cfg(any(target_os = "linux", target_os = "freebsd"))] const OTHER_NAME: &str = ""; #[cfg(any(target_os = "linux", target_os = "freebsd"))] const MASK_NAME: &str = ""; #[cfg(target_os = "freebsd")] const EVERYONE_NAME: &str = ""; /// A Qualifier specifies the principal that is allowed/denied access to a /// resource. #[derive(Debug, PartialEq, Eq)] pub enum Qualifier { User(unix::uid_t), Group(unix::gid_t), #[cfg(target_os = "macos")] Guid(Uuid), #[cfg(any(target_os = "linux", target_os = "freebsd"))] UserObj, #[cfg(any(target_os = "linux", target_os = "freebsd"))] GroupObj, #[cfg(any(target_os = "linux", target_os = "freebsd"))] Other, #[cfg(any(target_os = "linux", target_os = "freebsd"))] Mask, #[cfg(target_os = "freebsd")] Everyone, Unknown(String), } impl Qualifier { /// Create qualifier object from a GUID. #[cfg(target_os = "macos")] pub fn from_guid(guid: Uuid) -> io::Result { let qualifier = match unix::guid_to_id(guid)? { (Some(uid), None) => Qualifier::User(uid), (None, Some(gid)) => Qualifier::Group(gid), (None, None) => Qualifier::Guid(guid), _ => unreachable!("guid_to_id bug"), }; Ok(qualifier) } /// Create qualifier object from a user name. #[cfg(target_os = "macos")] pub fn user_named(name: &str) -> io::Result { match unix::name_to_uid(name) { Ok(uid) => Ok(Qualifier::User(uid)), Err(err) => { // Try to parse name as a GUID. Uuid::parse_str(name).map_or(Err(err), Qualifier::from_guid) } } } /// Create qualifier object from a user name. #[cfg(any(target_os = "linux", target_os = "freebsd"))] pub fn user_named(name: &str) -> io::Result { match name { OWNER_NAME => Ok(Qualifier::UserObj), s => match unix::name_to_uid(s) { Ok(uid) => Ok(Qualifier::User(uid)), Err(err) => Err(err), }, } } /// Create qualifier object from a group name. #[cfg(target_os = "macos")] pub fn group_named(name: &str) -> io::Result { match unix::name_to_gid(name) { Ok(gid) => Ok(Qualifier::Group(gid)), Err(err) => Uuid::parse_str(name).map_or(Err(err), Qualifier::from_guid), } } /// Create qualifier object from a group name. #[cfg(any(target_os = "linux", target_os = "freebsd"))] pub fn group_named(name: &str) -> io::Result { match name { OWNER_NAME => Ok(Qualifier::GroupObj), s => match unix::name_to_gid(s) { Ok(gid) => Ok(Qualifier::Group(gid)), Err(err) => Err(err), }, } } /// Create qualifier from mask. #[cfg(any(target_os = "linux", target_os = "freebsd"))] pub fn mask_named(name: &str) -> io::Result { match name { MASK_NAME => Ok(Qualifier::Mask), s => fail_custom(&format!("unknown mask name: {s:?}")), } } /// Create qualifier from other. #[cfg(any(target_os = "linux", target_os = "freebsd"))] pub fn other_named(name: &str) -> io::Result { match name { OTHER_NAME => Ok(Qualifier::Other), s => fail_custom(&format!("unknown other name: {s:?}")), } } /// Create qualifier from everyone. #[cfg(target_os = "freebsd")] pub fn everyone_named(name: &str) -> io::Result { match name { EVERYONE_NAME => Ok(Qualifier::Everyone), s => fail_custom(&format!("unknown everyone name: {s:?}")), } } /// Return the GUID for the user/group. #[cfg(target_os = "macos")] pub fn guid(&self) -> io::Result { match self { Qualifier::User(uid) => unix::uid_to_guid(*uid), Qualifier::Group(gid) => unix::gid_to_guid(*gid), Qualifier::Guid(guid) => Ok(*guid), Qualifier::Unknown(tag) => fail_custom(&format!("unknown tag: {tag:?}")), } } /// Return the name of the user/group. pub fn name(&self) -> io::Result { let result = match self { Qualifier::User(uid) => unix::uid_to_name(*uid)?, Qualifier::Group(gid) => unix::gid_to_name(*gid)?, #[cfg(target_os = "macos")] Qualifier::Guid(guid) => guid.to_string(), #[cfg(any(target_os = "linux", target_os = "freebsd"))] Qualifier::UserObj | Qualifier::GroupObj => OWNER_NAME.to_string(), #[cfg(any(target_os = "linux", target_os = "freebsd"))] Qualifier::Other => OTHER_NAME.to_string(), #[cfg(any(target_os = "linux", target_os = "freebsd"))] Qualifier::Mask => MASK_NAME.to_string(), #[cfg(target_os = "freebsd")] Qualifier::Everyone => EVERYONE_NAME.to_string(), Qualifier::Unknown(s) => s.clone(), }; Ok(result) } } impl fmt::Display for Qualifier { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { Qualifier::User(uid) => write!(f, "user:{uid}"), Qualifier::Group(gid) => write!(f, "group:{gid}"), #[cfg(target_os = "macos")] Qualifier::Guid(guid) => write!(f, "guid:{guid}"), #[cfg(any(target_os = "linux", target_os = "freebsd"))] Qualifier::UserObj => write!(f, "user"), #[cfg(any(target_os = "linux", target_os = "freebsd"))] Qualifier::GroupObj => write!(f, "group"), #[cfg(any(target_os = "linux", target_os = "freebsd"))] Qualifier::Other => write!(f, "other"), #[cfg(any(target_os = "linux", target_os = "freebsd"))] Qualifier::Mask => write!(f, "mask"), #[cfg(target_os = "freebsd")] Qualifier::Everyone => write!(f, "everyone"), Qualifier::Unknown(s) => write!(f, "unknown:{s}"), } } } //////////////////////////////////////////////////////////////////////////////// #[cfg(test)] mod qualifier_tests { use super::*; #[test] #[cfg(target_os = "macos")] fn test_from_guid() { let user = Qualifier::from_guid(Uuid::parse_str("ffffeeee-dddd-cccc-bbbb-aaaa00000059").unwrap()) .ok(); assert_eq!(user, Some(Qualifier::User(89))); let group = Qualifier::from_guid(Uuid::parse_str("abcdefab-cdef-abcd-efab-cdef00000059").unwrap()) .ok(); assert_eq!(group, Some(Qualifier::Group(89))); let user = Qualifier::from_guid(Uuid::nil()).ok(); assert_eq!(user, Some(Qualifier::Guid(Uuid::nil()))); } #[test] fn test_user_named() { let user = Qualifier::user_named("89").ok(); assert_eq!(user, Some(Qualifier::User(89))); #[cfg(target_os = "macos")] { let user = Qualifier::user_named("_spotlight").ok(); assert_eq!(user, Some(Qualifier::User(89))); let user = Qualifier::user_named("ffffeeee-dddd-cccc-bbbb-aaaa00000059").ok(); assert_eq!(user, Some(Qualifier::User(89))); } #[cfg(any(target_os = "linux", target_os = "freebsd"))] { let user = Qualifier::user_named("daemon").ok(); assert_eq!(user, Some(Qualifier::User(1))); } } #[test] fn test_group_named() { let group = Qualifier::group_named("89").ok(); assert_eq!(group, Some(Qualifier::Group(89))); #[cfg(target_os = "macos")] { let group = Qualifier::group_named("_spotlight").ok(); assert_eq!(group, Some(Qualifier::Group(89))); let group = Qualifier::group_named("abcdefab-cdef-abcd-efab-cdef00000059").ok(); assert_eq!(group, Some(Qualifier::Group(89))); } #[cfg(any(target_os = "linux", target_os = "freebsd"))] { let group = Qualifier::group_named("daemon").ok(); assert_eq!(group, Some(Qualifier::Group(1))); } } } exacl-0.10.0/src/sys.rs000064400000000000000000000235461046102023000127630ustar 00000000000000//! Rust bindings to system C API. #![allow(dead_code, non_camel_case_types)] // constant is never used pub use crate::bindings::*; // Demangle some MacOS constants. Linux provides these as-is. #[cfg(target_os = "macos")] pub const ACL_READ: acl_perm_t = acl_perm_t_ACL_READ_DATA; #[cfg(target_os = "macos")] pub const ACL_WRITE: acl_perm_t = acl_perm_t_ACL_WRITE_DATA; #[cfg(target_os = "macos")] pub const ACL_EXECUTE: acl_perm_t = acl_perm_t_ACL_EXECUTE; // Linux doesn't have ACL flags; adding acl_flag_t makes the code more orthogonal. // On FreeBSD, acl_flag_t is a u16. #[cfg(target_os = "linux")] pub type acl_flag_t = u32; // Linux doesn't have ACL_MAX_ENTRIES, so define it as 2 billion. #[cfg(target_os = "linux")] pub const ACL_MAX_ENTRIES: u32 = 2_000_000_000; // MacOS and FreeBSD use acl_get_perm_np(). #[cfg(any(target_os = "macos", target_os = "freebsd"))] pub unsafe fn acl_get_perm(permset_d: acl_permset_t, perm: acl_perm_t) -> ::std::os::raw::c_int { acl_get_perm_np(permset_d, perm) } /// Non-portable ACL Permissions & Flags (`macOS` only) #[cfg(all(target_os = "macos", not(docsrs)))] pub mod np { use super::*; pub const ACL_DELETE: acl_perm_t = acl_perm_t_ACL_DELETE; pub const ACL_APPEND_DATA: acl_perm_t = acl_perm_t_ACL_APPEND_DATA; pub const ACL_DELETE_CHILD: acl_perm_t = acl_perm_t_ACL_DELETE_CHILD; pub const ACL_READ_ATTRIBUTES: acl_perm_t = acl_perm_t_ACL_READ_ATTRIBUTES; pub const ACL_WRITE_ATTRIBUTES: acl_perm_t = acl_perm_t_ACL_WRITE_ATTRIBUTES; pub const ACL_READ_EXTATTRIBUTES: acl_perm_t = acl_perm_t_ACL_READ_EXTATTRIBUTES; pub const ACL_WRITE_EXTATTRIBUTES: acl_perm_t = acl_perm_t_ACL_WRITE_EXTATTRIBUTES; pub const ACL_READ_SECURITY: acl_perm_t = acl_perm_t_ACL_READ_SECURITY; pub const ACL_WRITE_SECURITY: acl_perm_t = acl_perm_t_ACL_WRITE_SECURITY; pub const ACL_CHANGE_OWNER: acl_perm_t = acl_perm_t_ACL_CHANGE_OWNER; pub const ACL_SYNCHRONIZE: acl_perm_t = acl_perm_t_ACL_SYNCHRONIZE; pub const ACL_FLAG_DEFER_INHERIT: acl_flag_t = acl_flag_t_ACL_FLAG_DEFER_INHERIT; pub const ACL_FLAG_NO_INHERIT: acl_flag_t = acl_flag_t_ACL_FLAG_NO_INHERIT; pub const ACL_ENTRY_INHERITED: acl_flag_t = acl_flag_t_ACL_ENTRY_INHERITED; pub const ACL_ENTRY_FILE_INHERIT: acl_flag_t = acl_flag_t_ACL_ENTRY_FILE_INHERIT; pub const ACL_ENTRY_DIRECTORY_INHERIT: acl_flag_t = acl_flag_t_ACL_ENTRY_DIRECTORY_INHERIT; pub const ACL_ENTRY_LIMIT_INHERIT: acl_flag_t = acl_flag_t_ACL_ENTRY_LIMIT_INHERIT; pub const ACL_ENTRY_ONLY_INHERIT: acl_flag_t = acl_flag_t_ACL_ENTRY_ONLY_INHERIT; } /// Non-portable ACL Permissions & Flags (`FreeBSD` only) #[cfg(all(target_os = "freebsd", not(docsrs)))] pub mod np { use super::{acl_flag_t, acl_perm_t}; pub const ACL_READ_DATA: acl_perm_t = super::ACL_READ_DATA; pub const ACL_WRITE_DATA: acl_perm_t = super::ACL_WRITE_DATA; // `ACL_EXECUTE` is portable. pub const ACL_DELETE: acl_perm_t = super::ACL_DELETE; pub const ACL_APPEND_DATA: acl_perm_t = super::ACL_APPEND_DATA; pub const ACL_DELETE_CHILD: acl_perm_t = super::ACL_DELETE_CHILD; pub const ACL_READ_ATTRIBUTES: acl_perm_t = super::ACL_READ_ATTRIBUTES; pub const ACL_WRITE_ATTRIBUTES: acl_perm_t = super::ACL_WRITE_ATTRIBUTES; pub const ACL_READ_EXTATTRIBUTES: acl_perm_t = super::ACL_READ_NAMED_ATTRS; pub const ACL_WRITE_EXTATTRIBUTES: acl_perm_t = super::ACL_WRITE_NAMED_ATTRS; pub const ACL_READ_SECURITY: acl_perm_t = super::ACL_READ_ACL; pub const ACL_WRITE_SECURITY: acl_perm_t = super::ACL_WRITE_ACL; pub const ACL_CHANGE_OWNER: acl_perm_t = super::ACL_WRITE_OWNER; pub const ACL_SYNCHRONIZE: acl_perm_t = super::ACL_SYNCHRONIZE; pub const ACL_ENTRY_INHERITED: acl_flag_t = super::ACL_ENTRY_INHERITED as acl_flag_t; pub const ACL_ENTRY_FILE_INHERIT: acl_flag_t = super::ACL_ENTRY_FILE_INHERIT as acl_flag_t; pub const ACL_ENTRY_DIRECTORY_INHERIT: acl_flag_t = super::ACL_ENTRY_DIRECTORY_INHERIT as acl_flag_t; pub const ACL_ENTRY_LIMIT_INHERIT: acl_flag_t = super::ACL_ENTRY_NO_PROPAGATE_INHERIT as acl_flag_t; pub const ACL_ENTRY_ONLY_INHERIT: acl_flag_t = super::ACL_ENTRY_INHERIT_ONLY as acl_flag_t; pub const ACL_ENTRY_SUCCESSFUL_ACCESS: acl_flag_t = super::ACL_ENTRY_SUCCESSFUL_ACCESS as acl_flag_t; pub const ACL_ENTRY_FAILED_ACCESS: acl_flag_t = super::ACL_ENTRY_FAILED_ACCESS as acl_flag_t; } /// Non-portable ACL Permissions (Docs only). These are fabricated constants to /// make it possible for docs to be built on macOS and Linux. #[cfg(docsrs)] pub mod np { use super::*; pub const ACL_READ_DATA: acl_perm_t = 1 << 8; pub const ACL_WRITE_DATA: acl_perm_t = 1 << 9; pub const ACL_DELETE: acl_perm_t = 1 << 10; pub const ACL_APPEND_DATA: acl_perm_t = 1 << 11; pub const ACL_DELETE_CHILD: acl_perm_t = 1 << 12; pub const ACL_READ_ATTRIBUTES: acl_perm_t = 1 << 13; pub const ACL_WRITE_ATTRIBUTES: acl_perm_t = 1 << 14; pub const ACL_READ_EXTATTRIBUTES: acl_perm_t = 1 << 15; pub const ACL_WRITE_EXTATTRIBUTES: acl_perm_t = 1 << 16; pub const ACL_READ_SECURITY: acl_perm_t = 1 << 17; pub const ACL_WRITE_SECURITY: acl_perm_t = 1 << 18; pub const ACL_CHANGE_OWNER: acl_perm_t = 1 << 19; pub const ACL_SYNCHRONIZE: acl_perm_t = 1 << 20; pub const ACL_FLAG_DEFER_INHERIT: acl_flag_t = 1 << 21; pub const ACL_FLAG_NO_INHERIT: acl_flag_t = 1 << 22; pub const ACL_ENTRY_INHERITED: acl_flag_t = 1 << 23; pub const ACL_ENTRY_FILE_INHERIT: acl_flag_t = 1 << 24; pub const ACL_ENTRY_DIRECTORY_INHERIT: acl_flag_t = 1 << 25; pub const ACL_ENTRY_LIMIT_INHERIT: acl_flag_t = 1 << 26; pub const ACL_ENTRY_ONLY_INHERIT: acl_flag_t = 1 << 27; } // Convenience constants where the API expects a signed i32 type, but bindgen // provides u32. (FIXME: Replace with bindgen ParseCallbacks::int_macro?) pub mod sg { #![allow(clippy::cast_possible_wrap)] use super::*; pub const ENOENT: i32 = super::ENOENT as i32; pub const ENOTSUP: i32 = super::ENOTSUP as i32; pub const EINVAL: i32 = super::EINVAL as i32; pub const ENOMEM: i32 = super::ENOMEM as i32; pub const ERANGE: i32 = super::ERANGE as i32; pub const ACL_MAX_ENTRIES: i32 = super::ACL_MAX_ENTRIES as i32; #[cfg(target_os = "macos")] pub const ACL_TYPE_EXTENDED: acl_type_t = super::acl_type_t_ACL_TYPE_EXTENDED; #[cfg(any(target_os = "linux", target_os = "freebsd"))] pub const ACL_TYPE_ACCESS: acl_type_t = super::ACL_TYPE_ACCESS as acl_type_t; #[cfg(any(target_os = "linux", target_os = "freebsd"))] pub const ACL_TYPE_DEFAULT: acl_type_t = super::ACL_TYPE_DEFAULT as acl_type_t; #[cfg(target_os = "freebsd")] pub const ACL_TYPE_NFS4: acl_type_t = super::ACL_TYPE_NFS4 as acl_type_t; #[cfg(target_os = "freebsd")] pub const ACL_BRAND_UNKNOWN: i32 = super::ACL_BRAND_UNKNOWN as i32; #[cfg(target_os = "freebsd")] pub const ACL_BRAND_POSIX: i32 = super::ACL_BRAND_POSIX as i32; #[cfg(target_os = "freebsd")] pub const ACL_BRAND_NFS4: i32 = super::ACL_BRAND_NFS4 as i32; #[cfg(target_os = "freebsd")] pub const ACL_ENTRY_TYPE_ALLOW: acl_entry_type_t = super::ACL_ENTRY_TYPE_ALLOW as acl_entry_type_t; #[cfg(target_os = "freebsd")] pub const ACL_ENTRY_TYPE_DENY: acl_entry_type_t = super::ACL_ENTRY_TYPE_DENY as acl_entry_type_t; #[cfg(target_os = "macos")] pub const ACL_FIRST_ENTRY: i32 = super::acl_entry_id_t_ACL_FIRST_ENTRY; #[cfg(any(target_os = "linux", target_os = "freebsd"))] pub const ACL_FIRST_ENTRY: i32 = super::ACL_FIRST_ENTRY as i32; #[cfg(target_os = "macos")] pub const ACL_NEXT_ENTRY: i32 = super::acl_entry_id_t_ACL_NEXT_ENTRY; #[cfg(any(target_os = "linux", target_os = "freebsd"))] pub const ACL_NEXT_ENTRY: i32 = super::ACL_NEXT_ENTRY as i32; #[cfg(target_os = "macos")] pub const O_SYMLINK: i32 = super::O_SYMLINK as i32; #[cfg(target_os = "macos")] pub const ACL_EXTENDED_ALLOW: acl_tag_t = super::acl_tag_t_ACL_EXTENDED_ALLOW; #[cfg(target_os = "macos")] pub const ACL_EXTENDED_DENY: acl_tag_t = super::acl_tag_t_ACL_EXTENDED_DENY; #[cfg(any(target_os = "linux", target_os = "freebsd"))] pub const ACL_USER_OBJ: acl_tag_t = super::ACL_USER_OBJ as acl_tag_t; #[cfg(any(target_os = "linux", target_os = "freebsd"))] pub const ACL_USER: acl_tag_t = super::ACL_USER as acl_tag_t; #[cfg(any(target_os = "linux", target_os = "freebsd"))] pub const ACL_GROUP_OBJ: acl_tag_t = super::ACL_GROUP_OBJ as acl_tag_t; #[cfg(any(target_os = "linux", target_os = "freebsd"))] pub const ACL_GROUP: acl_tag_t = super::ACL_GROUP as acl_tag_t; #[cfg(any(target_os = "linux", target_os = "freebsd"))] pub const ACL_MASK: acl_tag_t = super::ACL_MASK as acl_tag_t; #[cfg(any(target_os = "linux", target_os = "freebsd"))] pub const ACL_OTHER: acl_tag_t = super::ACL_OTHER as acl_tag_t; #[cfg(target_os = "freebsd")] pub const ACL_EVERYONE: acl_tag_t = super::ACL_EVERYONE as acl_tag_t; #[cfg(target_os = "macos")] pub const ID_TYPE_UID: i32 = super::ID_TYPE_UID as i32; #[cfg(target_os = "macos")] pub const ID_TYPE_GID: i32 = super::ID_TYPE_GID as i32; #[cfg(target_os = "freebsd")] pub const PC_ACL_NFS4: i32 = super::_PC_ACL_NFS4 as i32; #[test] fn test_signed() { assert!(super::ENOENT as i32 >= 0); assert!(super::ENOTSUP as i32 >= 0); assert!(super::EINVAL as i32 >= 0); assert!(super::ENOMEM as i32 >= 0); assert!(super::ACL_MAX_ENTRIES as i32 >= 0); #[cfg(target_os = "linux")] assert!(super::ACL_FIRST_ENTRY as i32 >= 0); #[cfg(target_os = "linux")] assert!(super::ACL_NEXT_ENTRY as i32 >= 0); #[cfg(target_os = "macos")] assert!(super::O_SYMLINK as i32 >= 0); #[cfg(target_os = "macos")] assert!(super::ID_TYPE_UID as i32 >= 0); #[cfg(target_os = "macos")] assert!(super::ID_TYPE_GID as i32 >= 0); } } exacl-0.10.0/src/unix.rs000064400000000000000000000250531046102023000131230ustar 00000000000000//! Implements utilities for converting user/group names to uid/gid. use crate::failx::*; use crate::sys::{getgrgid_r, getgrnam_r, getpwnam_r, getpwuid_r, group, passwd, sg}; #[cfg(target_os = "macos")] use crate::sys::{id_t, mbr_gid_to_uuid, mbr_uid_to_uuid, mbr_uuid_to_id}; use std::ffi::{CStr, CString}; use std::io; use std::mem; use std::os::raw::c_char; use std::ptr; #[cfg(target_os = "macos")] use uuid::Uuid; // Export uid_t and gid_t. pub use crate::sys::{gid_t, uid_t}; // Max buffer sizes for getpwnam_r, getgrnam_r, et al. are usually determined // by calling sysconf with SC_GETPW_R_SIZE_MAX or SC_GETGR_R_SIZE_MAX. Rather // than calling sysconf, this code hard-wires the default value and quadruples // the buffer size as needed, up to a maximum of 1MB. // SC_GETPW_R_SIZE_MAX/SC_GETGR_R_SIZE_MAX default to 1024 on vanilla Ubuntu // and 4096 on macOS/FreeBSD. We start the initial buffer size at 4096 bytes. const INITIAL_BUFSIZE: usize = 4096; // 4KB const MAX_BUFSIZE: usize = 1_048_576; // 1MB /// Convert user name to uid. pub fn name_to_uid(name: &str) -> io::Result { let mut pwd = mem::MaybeUninit::::uninit(); let mut buf = Vec::::with_capacity(INITIAL_BUFSIZE); let mut result = ptr::null_mut(); let cstr = CString::new(name)?; let mut ret; loop { ret = unsafe { getpwnam_r( cstr.as_ptr(), pwd.as_mut_ptr(), buf.as_mut_ptr(), buf.capacity(), &mut result, ) }; if ret == 0 || ret != sg::ERANGE || buf.capacity() >= MAX_BUFSIZE { break; } // Quadruple buffer size and try again. buf.reserve(4 * buf.capacity()); } if ret != 0 { return fail_err(ret, "getpwnam_r", name); } if !result.is_null() { let uid = unsafe { pwd.assume_init().pw_uid }; return Ok(uid); } // Try to parse name as a decimal user ID. if let Ok(num) = name.parse::() { return Ok(num); } fail_custom(&format!("unknown user name: {name:?}")) } /// Convert group name to gid. pub fn name_to_gid(name: &str) -> io::Result { let mut grp = mem::MaybeUninit::::uninit(); let mut buf = Vec::::with_capacity(INITIAL_BUFSIZE); let mut result = ptr::null_mut(); let cstr = CString::new(name)?; let mut ret; loop { ret = unsafe { getgrnam_r( cstr.as_ptr(), grp.as_mut_ptr(), buf.as_mut_ptr(), buf.capacity(), &mut result, ) }; if ret == 0 || ret != sg::ERANGE || buf.capacity() >= MAX_BUFSIZE { break; } // Quadruple buffer size and try again. buf.reserve(4 * buf.capacity()); } if ret != 0 { return fail_err(ret, "getgrnam_r", name); } if !result.is_null() { let gid = unsafe { grp.assume_init().gr_gid }; return Ok(gid); } // Try to parse name as a decimal group ID. if let Ok(num) = name.parse::() { return Ok(num); } fail_custom(&format!("unknown group name: {name:?}")) } /// Convert uid to user name. pub fn uid_to_name(uid: uid_t) -> io::Result { let mut pwd = mem::MaybeUninit::::uninit(); let mut buf = Vec::::with_capacity(INITIAL_BUFSIZE); let mut result = ptr::null_mut(); let mut ret; loop { ret = unsafe { getpwuid_r( uid, pwd.as_mut_ptr(), buf.as_mut_ptr(), buf.capacity(), &mut result, ) }; if ret == 0 || ret != sg::ERANGE || buf.capacity() >= MAX_BUFSIZE { break; } // Quadruple buffer size and try again. buf.reserve(4 * buf.capacity()); } if ret != 0 { return fail_err(ret, "getpwuid_r", uid); } if !result.is_null() { let cstr = unsafe { CStr::from_ptr(pwd.assume_init().pw_name) }; return Ok(cstr.to_string_lossy().into_owned()); } Ok(uid.to_string()) } /// Convert gid to group name. pub fn gid_to_name(gid: gid_t) -> io::Result { let mut grp = mem::MaybeUninit::::uninit(); let mut buf = Vec::::with_capacity(INITIAL_BUFSIZE); let mut result = ptr::null_mut(); let mut ret; loop { ret = unsafe { getgrgid_r( gid, grp.as_mut_ptr(), buf.as_mut_ptr(), buf.capacity(), &mut result, ) }; if ret == 0 || ret != sg::ERANGE || buf.capacity() >= MAX_BUFSIZE { break; } // Quadruple buffer size and try again. buf.reserve(4 * buf.capacity()); } if ret != 0 { return fail_err(ret, "getgrgid_r", gid); } if !result.is_null() { let cstr = unsafe { CStr::from_ptr(grp.assume_init().gr_name) }; return Ok(cstr.to_string_lossy().into_owned()); } Ok(gid.to_string()) } /// Convert uid to GUID. #[cfg(target_os = "macos")] pub fn uid_to_guid(uid: uid_t) -> io::Result { let mut bytes = [0u8; 16]; // On error, returns one of {EIO, ENOENT, EAUTH, EINVAL, ENOMEM}. let ret = unsafe { mbr_uid_to_uuid(uid, bytes.as_mut_ptr()) }; if ret != 0 { return fail_from_err(ret, "mbr_uid_to_uuid", uid); } Ok(Uuid::from_bytes(bytes)) } /// Convert gid to GUID. #[cfg(target_os = "macos")] pub fn gid_to_guid(gid: gid_t) -> io::Result { let mut bytes = [0u8; 16]; // On error, returns one of {EIO, ENOENT, EAUTH, EINVAL, ENOMEM}. let ret = unsafe { mbr_gid_to_uuid(gid, bytes.as_mut_ptr()) }; if ret != 0 { return fail_from_err(ret, "mbr_gid_to_uuid", gid); } Ok(Uuid::from_bytes(bytes)) } /// Convert GUID to uid/gid. /// /// Returns a pair of options (Option[uid], Option[gid]). Either one option must /// be set or neither is set. If neither is set, the GUID was not found. #[cfg(target_os = "macos")] pub fn guid_to_id(guid: Uuid) -> io::Result<(Option, Option)> { let mut id_c: id_t = 0; let mut idtype: i32 = 0; let mut bytes = guid.into_bytes(); // On error, returns one of {EIO, ENOENT, EAUTH, EINVAL, ENOMEM}. let ret = unsafe { mbr_uuid_to_id(bytes.as_mut_ptr(), &mut id_c, &mut idtype) }; if ret == sg::ENOENT { // GUID was not found. return Ok((None, None)); } if ret != 0 { return fail_from_err(ret, "mbr_uuid_to_id", guid); } let result = match idtype { sg::ID_TYPE_UID => (Some(id_c), None), sg::ID_TYPE_GID => (None, Some(id_c)), _ => { return fail_custom(&format!( "mbr_uuid_to_id: Unknown idtype {:?} for guid {:?}", idtype, guid )) } }; Ok(result) } //////////////////////////////////////////////////////////////////////////////// #[cfg(test)] mod unix_tests { use super::*; #[test] fn test_name_to_uid() { let msg = name_to_uid("").unwrap_err().to_string(); assert_eq!(msg, "unknown user name: \"\""); let msg = name_to_uid("non_existant").unwrap_err().to_string(); assert_eq!(msg, "unknown user name: \"non_existant\""); assert_eq!(name_to_uid("500").ok(), Some(500)); #[cfg(target_os = "macos")] assert_eq!(name_to_uid("_spotlight").ok(), Some(89)); #[cfg(any(target_os = "linux", target_os = "freebsd"))] assert_eq!(name_to_uid("daemon").ok(), Some(1)); } #[test] fn test_name_to_gid() { let msg = name_to_gid("").unwrap_err().to_string(); assert_eq!(msg, "unknown group name: \"\""); let msg = name_to_gid("non_existant").unwrap_err().to_string(); assert_eq!(msg, "unknown group name: \"non_existant\""); assert_eq!(name_to_gid("500").ok(), Some(500)); #[cfg(target_os = "macos")] assert_eq!(name_to_gid("_spotlight").ok(), Some(89)); #[cfg(any(target_os = "linux", target_os = "freebsd"))] assert_eq!(name_to_gid("daemon").ok(), Some(1)); } #[test] fn test_uid_to_name() { assert_eq!(uid_to_name(1500).unwrap(), "1500"); #[cfg(target_os = "macos")] assert_eq!(uid_to_name(89).unwrap(), "_spotlight"); #[cfg(any(target_os = "linux", target_os = "freebsd"))] assert_eq!(uid_to_name(1).unwrap(), "daemon"); } #[test] fn test_gid_to_name() { assert_eq!(gid_to_name(1500).unwrap(), "1500"); #[cfg(target_os = "macos")] assert_eq!(gid_to_name(89).unwrap(), "_spotlight"); #[cfg(any(target_os = "linux", target_os = "freebsd"))] assert_eq!(gid_to_name(1).unwrap(), "daemon"); } #[test] #[cfg(target_os = "macos")] fn test_uid_to_guid() { assert_eq!( uid_to_guid(89).ok(), Some(Uuid::parse_str("ffffeeee-dddd-cccc-bbbb-aaaa00000059").unwrap()) ); assert_eq!( uid_to_guid(1500).ok(), Some(Uuid::parse_str("ffffeeee-dddd-cccc-bbbb-aaaa000005dc").unwrap()) ); } #[test] #[cfg(target_os = "macos")] fn test_gid_to_guid() { assert_eq!( gid_to_guid(89).ok(), Some(Uuid::parse_str("abcdefab-cdef-abcd-efab-cdef00000059").unwrap()) ); assert_eq!( gid_to_guid(1500).ok(), Some(Uuid::parse_str("aaaabbbb-cccc-dddd-eeee-ffff000005dc").unwrap()) ); assert_eq!( gid_to_guid(20).ok(), Some(Uuid::parse_str("abcdefab-cdef-abcd-efab-cdef00000014").unwrap()) ); } #[test] #[cfg(target_os = "macos")] fn test_guid_to_id() { assert_eq!( guid_to_id(Uuid::parse_str("ffffeeee-dddd-cccc-bbbb-aaaa00000059").unwrap()).unwrap(), (Some(89), None) ); assert_eq!( guid_to_id(Uuid::parse_str("ffffeeee-dddd-cccc-bbbb-aaaa000005dc").unwrap()).unwrap(), (Some(1500), None) ); assert_eq!( guid_to_id(Uuid::parse_str("abcdefab-cdef-abcd-efab-cdef00000059").unwrap()).unwrap(), (None, Some(89)) ); assert_eq!( guid_to_id(Uuid::parse_str("aaaabbbb-cccc-dddd-eeee-ffff000005dc").unwrap()).unwrap(), (None, Some(1500)) ); assert_eq!( guid_to_id(Uuid::parse_str("abcdefab-cdef-abcd-efab-cdef00000014").unwrap()).unwrap(), (None, Some(20)) ); assert_eq!(guid_to_id(Uuid::nil()).unwrap(), (None, None)); } } exacl-0.10.0/src/util/mod.rs000064400000000000000000000027731046102023000137000ustar 00000000000000//! Provides a cross-platform, minimal ACL API. //! //! Types: //! `acl_t` //! `acl_entry_t` //! //! Functions: //! `xacl_init` - create a new empty ACL //! `xacl_free` - destroy ACL //! `xacl_foreach` - apply a function to each entry in an ACL //! `xacl_is_empty` - return true if an ACL is empty //! `xacl_is_posix` - return true if ACL has Posix.1e semantics. //! `xacl_add_entry` - append new entry to an ACL //! `xacl_get_entry` - retrieve contents from an ACL entry //! `xacl_get_file` - get ACL from file path //! `xacl_set_file` - set ACL for file path //! `xacl_is_nfs4` - return true if file path uses `NFSv4` ACL on `FreeBSD` mod util_common; #[cfg(target_os = "freebsd")] mod util_freebsd; #[cfg(target_os = "linux")] mod util_linux; #[cfg(target_os = "macos")] mod util_macos; // Re-export acl_entry_t and acl_t from crate::sys. pub use crate::sys::{acl_entry_t, acl_t}; #[cfg(target_os = "freebsd")] pub use util_freebsd::{ xacl_add_entry, xacl_foreach, xacl_free, xacl_get_entry, xacl_get_file, xacl_init, xacl_is_empty, xacl_is_nfs4, xacl_is_posix, xacl_set_file, }; #[cfg(target_os = "linux")] pub use util_linux::{ xacl_add_entry, xacl_foreach, xacl_free, xacl_get_entry, xacl_get_file, xacl_init, xacl_is_empty, xacl_is_posix, xacl_set_file, }; #[cfg(target_os = "macos")] pub use util_macos::{ xacl_add_entry, xacl_foreach, xacl_free, xacl_get_entry, xacl_get_file, xacl_init, xacl_is_empty, xacl_is_posix, xacl_set_file, }; exacl-0.10.0/src/util/util_common.rs000064400000000000000000000100101046102023000154250ustar 00000000000000use crate::bititer::BitIter; use crate::failx::*; use crate::perm::Perm; use crate::sys::*; use std::ffi::c_void; use std::io; use std::ptr; /// Free memory allocated by native acl_* routines. pub fn xacl_free(ptr: *mut T) { assert!(!ptr.is_null()); let ret = unsafe { acl_free(ptr.cast::()) }; assert_eq!(ret, 0); } /// Return true if acl is empty. pub fn xacl_is_empty(acl: acl_t) -> bool { let mut entry: acl_entry_t = ptr::null_mut(); !xacl_get_entry(acl, sg::ACL_FIRST_ENTRY, &mut entry) } /// Return next entry in ACL. fn xacl_get_entry(acl: acl_t, entry_id: i32, entry_p: *mut acl_entry_t) -> bool { let ret = unsafe { acl_get_entry(acl, entry_id, entry_p) }; // MacOS: Zero indicates success. #[cfg(target_os = "macos")] return ret == 0; // Linux, FreeBSD: One indicates success. #[cfg(any(target_os = "linux", target_os = "freebsd"))] return ret == 1; } /// Iterate over entries in a native ACL. pub fn xacl_foreach io::Result<()>>( acl: acl_t, mut func: F, ) -> io::Result<()> { let mut entry: acl_entry_t = ptr::null_mut(); let mut entry_id = sg::ACL_FIRST_ENTRY; assert!(!acl.is_null()); loop { if !xacl_get_entry(acl, entry_id, &mut entry) { break; } assert!(!entry.is_null()); func(entry)?; entry_id = sg::ACL_NEXT_ENTRY; } Ok(()) } /// Create a new empty ACL with the given capacity. /// /// Client must call `xacl_free` when done with result. pub fn xacl_init(capacity: usize) -> io::Result { let size = match i32::try_from(capacity) { Ok(size) if size <= sg::ACL_MAX_ENTRIES => size, _ => return fail_custom("Too many ACL entries"), }; let acl = unsafe { acl_init(size) }; if acl.is_null() { return fail_err("null", "acl_init", capacity); } Ok(acl) } /// Create a new entry in the specified ACL. /// /// N.B. Memory reallocation may cause `acl` ptr to change. pub fn xacl_create_entry(acl: &mut acl_t) -> io::Result { let mut entry: acl_entry_t = ptr::null_mut(); let ret = unsafe { acl_create_entry(&mut *acl, &mut entry) }; if ret != 0 { return fail_err(ret, "acl_create_entry", ()); } Ok(entry) } /// Get tag type from entry. pub fn xacl_get_tag_type(entry: acl_entry_t) -> io::Result { let mut tag: acl_tag_t = 0; let ret = unsafe { acl_get_tag_type(entry, &mut tag) }; if ret != 0 { return fail_err(ret, "acl_get_tag_type", ()); } Ok(tag) } /// Get permissions from the entry. pub fn xacl_get_perm(entry: acl_entry_t) -> io::Result { let mut permset: acl_permset_t = std::ptr::null_mut(); let ret = unsafe { acl_get_permset(entry, &mut permset) }; if ret != 0 { return fail_err(ret, "acl_get_permset", ()); } assert!(!permset.is_null()); let mut perms = Perm::empty(); for perm in BitIter(Perm::all()) { let res = unsafe { acl_get_perm(permset, perm.bits()) }; debug_assert!((0..=1).contains(&res)); if res == 1 { perms |= perm; } } Ok(perms) } /// Set tag type for ACL entry. pub fn xacl_set_tag_type(entry: acl_entry_t, tag: acl_tag_t) -> io::Result<()> { let ret = unsafe { acl_set_tag_type(entry, tag) }; if ret != 0 { return fail_err(ret, "acl_set_tag_type", ()); } Ok(()) } /// Set permissions for the entry. pub fn xacl_set_perm(entry: acl_entry_t, perms: Perm) -> io::Result<()> { let mut permset: acl_permset_t = std::ptr::null_mut(); let ret_get = unsafe { acl_get_permset(entry, &mut permset) }; if ret_get != 0 { return fail_err(ret_get, "acl_get_permset", ()); } assert!(!permset.is_null()); let ret_clear = unsafe { acl_clear_perms(permset) }; if ret_clear != 0 { return fail_err(ret_clear, "acl_clear_perms", ()); } for perm in BitIter(perms) { let ret = unsafe { acl_add_perm(permset, perm.bits()) }; debug_assert!(ret == 0); } Ok(()) } exacl-0.10.0/src/util/util_freebsd.rs000064400000000000000000000412271046102023000155650ustar 00000000000000use crate::bititer::BitIter; use crate::failx::*; use crate::flag::Flag; use crate::perm::Perm; use crate::qualifier::Qualifier; use crate::sys::*; use crate::util::util_common; use log::debug; use scopeguard::defer; use std::ffi::{c_void, CString}; use std::io; use std::os::unix::ffi::OsStrExt; use std::path::Path; use std::ptr; pub use util_common::{xacl_create_entry, xacl_foreach, xacl_free, xacl_init, xacl_is_empty}; use util_common::*; fn get_acl_type(acl: acl_t, default_acl: bool) -> acl_type_t { if !acl.is_null() && !xacl_is_posix(acl) { sg::ACL_TYPE_NFS4 } else if default_acl { sg::ACL_TYPE_DEFAULT } else { sg::ACL_TYPE_ACCESS } } /// Get ACL from file path, don't follow symbolic links. fn xacl_get_link(path: &Path, default_acl: bool) -> io::Result { let mut acl_type = get_acl_type(ptr::null_mut(), default_acl); let c_path = CString::new(path.as_os_str().as_bytes())?; let acl = unsafe { acl_get_link_np(c_path.as_ptr(), acl_type) }; if !acl.is_null() { return Ok(acl); } // `acl_get_link_np` returns EINVAL when the ACL type is not appropriate for // the file system object. Retry with NFSv4 type. // FIXME: `default_acl` setting is currently ignored! if io::Error::last_os_error().raw_os_error() == Some(sg::EINVAL) { acl_type = sg::ACL_TYPE_NFS4; let nfs_acl = unsafe { acl_get_link_np(c_path.as_ptr(), acl_type) }; if !nfs_acl.is_null() { return Ok(nfs_acl); } } // Report acl_type and path to file that failed. let func = match acl_type { sg::ACL_TYPE_ACCESS => "acl_get_link_np/access", sg::ACL_TYPE_DEFAULT => "acl_get_link_np/default", sg::ACL_TYPE_NFS4 => "acl_get_link_np/nfs4", _ => "acl_get_link_np/?", }; fail_err("null", func, &c_path) } /// Get ACL from file path. /// /// This code first tries to obtain the Posix.1e ACL. If that's not appropriate /// for the file system object, we try to access the NFS4 ACL. pub fn xacl_get_file(path: &Path, symlink_acl: bool, default_acl: bool) -> io::Result { // Symlinks will use `acl_get_link_np` instead of `acl_get_file`. if symlink_acl { return xacl_get_link(path, default_acl); } let mut acl_type = get_acl_type(ptr::null_mut(), default_acl); let c_path = CString::new(path.as_os_str().as_bytes())?; let acl = unsafe { acl_get_file(c_path.as_ptr(), acl_type) }; if !acl.is_null() { return Ok(acl); } // `acl_get_file` returns EINVAL when the ACL type is not appropriate for // the file system object. Retry with NFSv4 type. if io::Error::last_os_error().raw_os_error() == Some(sg::EINVAL) && xacl_is_nfs4(path, symlink_acl)? { // NFSv4 does not support default ACL. if default_acl { return fail_custom("Default ACL not supported"); } acl_type = sg::ACL_TYPE_NFS4; let nfs_acl = unsafe { acl_get_file(c_path.as_ptr(), acl_type) }; if !nfs_acl.is_null() { return Ok(nfs_acl); } } // Report acl_type and path to file that failed. let func = match acl_type { sg::ACL_TYPE_ACCESS => "acl_get_file/access", sg::ACL_TYPE_DEFAULT => "acl_get_file/default", sg::ACL_TYPE_NFS4 => "acl_get_file/nfs4", _ => "acl_get_file/?", }; fail_err("null", func, &c_path) } fn xacl_set_file_symlink(path: &Path, acl: acl_t, default_acl: bool) -> io::Result<()> { let c_path = CString::new(path.as_os_str().as_bytes())?; if default_acl && xacl_is_empty(acl) { // Special case to delete the ACL. The FreeBSD version of // acl_set_link_np does not handle this case. let ret = unsafe { acl_delete_def_link_np(c_path.as_ptr()) }; if ret != 0 { return fail_err(ret, "acl_delete_def_link_np", &c_path); } return Ok(()); } let acl_type = get_acl_type(acl, default_acl); let ret = unsafe { acl_set_link_np(c_path.as_ptr(), acl_type, acl) }; if ret != 0 { let func = if default_acl { "acl_set_link_np/default" } else { "acl_set_link_np/access" }; return fail_err(ret, func, &c_path); } Ok(()) } fn xacl_repair_nfs4(acl: acl_t) -> io::Result<()> { xacl_foreach(acl, |entry| { let entry_type = xacl_get_entry_type(entry)?; if entry_type == 0 { xacl_set_entry_type(entry, sg::ACL_ENTRY_TYPE_ALLOW)?; } // Translate READ -> READ_DATA and WRITE -> WRITE_DATA. let mut perm = xacl_get_perm(entry)?; let orig_perm = perm; if perm.intersects(Perm::READ) { perm.remove(Perm::READ); perm.insert(Perm::READ_DATA); } if perm.intersects(Perm::WRITE) { perm.remove(Perm::WRITE); perm.insert(Perm::WRITE_DATA); } if perm != orig_perm { xacl_set_perm(entry, perm)?; } Ok(()) }) } pub fn xacl_set_file( path: &Path, acl: acl_t, symlink_acl: bool, default_acl: bool, ) -> io::Result<()> { let is_nfs4 = xacl_is_nfs4(path, symlink_acl)?; if default_acl && is_nfs4 { return fail_custom("Default ACL not supported"); } if !xacl_is_posix(acl) || is_nfs4 { // Fix up the ACL to make sure that all entry types are set. // FIXME: This mutates the acl, violating the immutable invariant. xacl_repair_nfs4(acl)?; } log_brand("xacl_set_file", acl)?; if symlink_acl { return xacl_set_file_symlink(path, acl, default_acl); } let c_path = CString::new(path.as_os_str().as_bytes())?; if default_acl && xacl_is_empty(acl) { // Special case to delete the ACL. The FreeBSD version of // acl_set_file does not handle this case. let ret = unsafe { acl_delete_def_file(c_path.as_ptr()) }; if ret != 0 { return fail_err(ret, "acl_delete_def_file", &c_path); } return Ok(()); } let acl_type = get_acl_type(acl, default_acl); let ret = unsafe { acl_set_file(c_path.as_ptr(), acl_type, acl) }; if ret != 0 { let func = match acl_type { sg::ACL_TYPE_ACCESS => "acl_set_file/access", sg::ACL_TYPE_DEFAULT => "acl_set_file/default", sg::ACL_TYPE_NFS4 => "acl_set_file/nfs4", _ => "acl_set_file/?", }; return fail_err(ret, func, &c_path); } Ok(()) } fn xacl_get_qualifier(entry: acl_entry_t) -> io::Result { let tag = xacl_get_tag_type(entry)?; let id = if tag == sg::ACL_USER || tag == sg::ACL_GROUP { let id_ptr = unsafe { acl_get_qualifier(entry).cast::() }; if id_ptr.is_null() { return fail_err("null", "acl_get_qualifier", ()); } defer! { xacl_free(id_ptr) }; Some(unsafe { *id_ptr }) } else { None }; let result = match tag { sg::ACL_USER => Qualifier::User(id.unwrap()), sg::ACL_GROUP => Qualifier::Group(id.unwrap()), sg::ACL_USER_OBJ => Qualifier::UserObj, sg::ACL_GROUP_OBJ => Qualifier::GroupObj, sg::ACL_OTHER => Qualifier::Other, sg::ACL_MASK => Qualifier::Mask, sg::ACL_EVERYONE => Qualifier::Everyone, tag => Qualifier::Unknown(format!("@tag {tag}")), }; Ok(result) } fn xacl_get_entry_type(entry: acl_entry_t) -> io::Result { let mut entry_type: acl_entry_type_t = 0; let ret = unsafe { acl_get_entry_type_np(entry, &mut entry_type) }; if ret != 0 { return fail_err(ret, "acl_get_entry_type_np", ()); } // FIXME: AUDIT, ALARM entry types are not supported. debug_assert!( entry_type == 0 || entry_type == sg::ACL_ENTRY_TYPE_ALLOW || entry_type == sg::ACL_ENTRY_TYPE_DENY ); Ok(entry_type) } fn xacl_get_tag_qualifier(acl: acl_t, entry: acl_entry_t) -> io::Result<(bool, Qualifier)> { let qualifier = xacl_get_qualifier(entry)?; let allow = if xacl_is_posix(acl) { true } else { xacl_get_entry_type(entry)? == sg::ACL_ENTRY_TYPE_ALLOW }; Ok((allow, qualifier)) } fn xacl_get_flags(acl: acl_t, entry: acl_entry_t) -> io::Result { if xacl_is_posix(acl) { return Ok(Flag::empty()); } let mut flagset: acl_flagset_t = std::ptr::null_mut(); let ret = unsafe { acl_get_flagset_np(entry, &mut flagset) }; if ret != 0 { return fail_err(ret, "acl_get_flagset_np", ()); } assert!(!flagset.is_null()); let mut flags = Flag::empty(); for flag in BitIter(Flag::all() - Flag::DEFAULT) { let res = unsafe { acl_get_flag_np(flagset, flag.bits()) }; debug_assert!((0..=1).contains(&res)); if res == 1 { flags |= flag; } } Ok(flags) } pub fn xacl_get_entry(acl: acl_t, entry: acl_entry_t) -> io::Result<(bool, Qualifier, Perm, Flag)> { let (allow, qualifier) = xacl_get_tag_qualifier(acl, entry)?; let perms = xacl_get_perm(entry)?; let flags = xacl_get_flags(acl, entry)?; Ok((allow, qualifier, perms, flags)) } pub fn xacl_set_qualifier(entry: acl_entry_t, mut id: uid_t) -> io::Result<()> { let id_ptr = std::ptr::addr_of_mut!(id); let ret = unsafe { acl_set_qualifier(entry, id_ptr.cast::()) }; if ret != 0 { return fail_err(ret, "acl_set_qualifier", ()); } Ok(()) } fn xacl_set_entry_type(entry: acl_entry_t, entry_type: acl_entry_type_t) -> io::Result<()> { let ret = unsafe { acl_set_entry_type_np(entry, entry_type) }; if ret != 0 { return fail_err(ret, "acl_set_entry_type_np", ()); } Ok(()) } pub fn xacl_set_tag_qualifier( entry: acl_entry_t, allow: bool, qualifier: &Qualifier, ) -> io::Result<()> { if !allow { xacl_set_entry_type(entry, sg::ACL_ENTRY_TYPE_DENY)?; }; match qualifier { Qualifier::User(uid) => { xacl_set_tag_type(entry, sg::ACL_USER)?; xacl_set_qualifier(entry, *uid)?; } Qualifier::Group(gid) => { xacl_set_tag_type(entry, sg::ACL_GROUP)?; xacl_set_qualifier(entry, *gid)?; } Qualifier::UserObj => { xacl_set_tag_type(entry, sg::ACL_USER_OBJ)?; } Qualifier::GroupObj => { xacl_set_tag_type(entry, sg::ACL_GROUP_OBJ)?; } Qualifier::Other => { xacl_set_tag_type(entry, sg::ACL_OTHER)?; } Qualifier::Mask => { xacl_set_tag_type(entry, sg::ACL_MASK)?; } Qualifier::Everyone => { xacl_set_tag_type(entry, sg::ACL_EVERYONE)?; } Qualifier::Unknown(tag) => { return fail_custom(&format!("unknown tag: {tag}")); } } Ok(()) } fn xacl_set_flags(entry: acl_entry_t, flags: Flag) -> io::Result<()> { if flags.is_empty() || flags == Flag::DEFAULT { return Ok(()); } let mut flagset: acl_flagset_t = std::ptr::null_mut(); let ret_get = unsafe { acl_get_flagset_np(entry, &mut flagset) }; if ret_get != 0 { return fail_err(ret_get, "acl_get_flagset_np", ()); } assert!(!flagset.is_null()); let ret_clear = unsafe { acl_clear_flags_np(flagset) }; if ret_clear != 0 { return fail_err(ret_clear, "acl_clear_flags_np", ()); } for flag in BitIter(flags) { let ret = unsafe { acl_add_flag_np(flagset, flag.bits()) }; debug_assert!(ret == 0); } Ok(()) } pub fn xacl_add_entry( acl: &mut acl_t, allow: bool, qualifier: &Qualifier, perms: Perm, flags: Flag, ) -> io::Result { let nfs4_specific = perms.intersects(Perm::NFS4_SPECIFIC) || flags.intersects(Flag::NFS4_SPECIFIC); if allow && xacl_is_posix(*acl) && !nfs4_specific { // Check for duplicates already in the list. xacl_foreach(*acl, |entry| { let (_, prev) = xacl_get_tag_qualifier(*acl, entry)?; if prev == *qualifier { let default = if flags.contains(Flag::DEFAULT) { "default " } else { "" }; fail_custom(&format!("duplicate {default}entry for \"{prev}\""))?; } Ok(()) })?; } let entry = xacl_create_entry(acl)?; xacl_set_tag_qualifier(entry, allow, qualifier)?; xacl_set_perm(entry, perms)?; xacl_set_flags(entry, flags)?; // If permissions allow NFSv4 specific bits, set entry type `allow` to force // the ACL brand to NFS4. if allow && nfs4_specific { xacl_set_entry_type(entry, sg::ACL_ENTRY_TYPE_ALLOW)?; } Ok(entry) } fn xacl_get_brand(acl: acl_t) -> io::Result { let mut brand: i32 = 0; let ret = unsafe { acl_get_brand_np(acl, &mut brand) }; if ret != 0 { return fail_err(ret, "acl_get_brand_np", ()); } Ok(brand) } pub fn xacl_is_posix(acl: acl_t) -> bool { let brand = xacl_get_brand(acl).expect("xacl_get_brand failed"); debug_assert!( brand == sg::ACL_BRAND_UNKNOWN || brand == sg::ACL_BRAND_POSIX || brand == sg::ACL_BRAND_NFS4 ); // Treat an Unknown branded ACL as Posix. brand == sg::ACL_BRAND_POSIX || brand == sg::ACL_BRAND_UNKNOWN } fn log_brand(func: &str, acl: acl_t) -> io::Result<()> { let brand = match xacl_get_brand(acl)? { sg::ACL_BRAND_UNKNOWN => "brand_unknown".to_owned(), sg::ACL_BRAND_POSIX => "brand_posix".to_owned(), sg::ACL_BRAND_NFS4 => "brand_nfs4".to_owned(), value => value.to_string(), }; debug!("{}: acl {}", func, brand); Ok(()) } pub fn xacl_is_nfs4(path: &Path, symlink: bool) -> io::Result { let c_path = CString::new(path.as_os_str().as_bytes())?; let ret = if symlink { unsafe { lpathconf(c_path.as_ptr(), sg::PC_ACL_NFS4) } } else { unsafe { pathconf(c_path.as_ptr(), sg::PC_ACL_NFS4) } }; if ret < 0 { return fail_err(ret, "pathconf", symlink); } assert!(ret == 0 || ret == 1); Ok(ret == 1) } //////////////////////////////////////////////////////////////////////////////// #[cfg(test)] mod util_freebsd_test { use super::*; #[test] fn test_acl_api_misuse() { // Create empty list and add an entry. let mut acl = xacl_init(1).unwrap(); let entry = xacl_create_entry(&mut acl).unwrap(); // Setting tag other than 1 or 2 results in EINVAL error. let err = xacl_set_tag_type(entry, 0).unwrap_err(); assert_eq!(err.raw_os_error(), Some(sg::EINVAL)); // Setting qualifier without first setting tag to a valid value results in EINVAL. let err = xacl_set_qualifier(entry, 500).unwrap_err(); assert_eq!(err.raw_os_error(), Some(sg::EINVAL)); // Try to set entry using unknown qualifier -- this should fail. let err = xacl_set_tag_qualifier(entry, true, &Qualifier::Unknown("x".to_string())).unwrap_err(); assert!(err.to_string().contains("unknown tag: x")); // Add another entry and set it to a valid value. let entry2 = xacl_create_entry(&mut acl).unwrap(); xacl_set_tag_type(entry2, sg::ACL_USER_OBJ).unwrap(); xacl_free(acl); } #[test] fn test_empty_acl() { let file = tempfile::NamedTempFile::new().unwrap(); let dir = tempfile::TempDir::new().unwrap(); let acl = xacl_init(1).unwrap(); assert!(xacl_is_empty(acl)); // Empty acl is not "valid". let ret = unsafe { acl_valid(acl) }; assert_eq!(ret, -1); // Not on FreeBSD. let err = xacl_set_file(file.as_ref(), acl, false, false) .err() .unwrap(); assert_eq!(err.to_string(), "Invalid argument (os error 22)"); // Write an empty default ACL to a directory. Okay with Posix.1e ACL // but fails on NFSv4, because default ACL is not supported. let result = xacl_set_file(dir.as_ref(), acl, false, true); if xacl_is_nfs4(dir.as_ref(), false).unwrap() { assert_eq!( result.err().unwrap().to_string(), "Default ACL not supported" ); } else { result.ok().unwrap(); } xacl_free(acl); } #[test] fn test_uninitialized_entry() { let mut acl = xacl_init(1).unwrap(); let entry_p = xacl_create_entry(&mut acl).unwrap(); let (allow, qualifier) = xacl_get_tag_qualifier(acl, entry_p).unwrap(); assert_eq!(qualifier.name().unwrap(), "@tag 0"); // FreeBSD: Unbranded entry is treated as Posix. assert!(allow); let brand = xacl_get_brand(acl).unwrap(); assert_eq!(brand, sg::ACL_BRAND_UNKNOWN); xacl_free(acl); } } exacl-0.10.0/src/util/util_linux.rs000064400000000000000000000174751046102023000153220ustar 00000000000000use crate::failx::*; use crate::flag::Flag; use crate::perm::Perm; use crate::qualifier::Qualifier; use crate::sys::*; use crate::util::util_common; use scopeguard::defer; use std::ffi::{c_void, CString}; use std::io; use std::os::unix::ffi::OsStrExt; use std::path::Path; pub use util_common::{xacl_create_entry, xacl_foreach, xacl_free, xacl_init, xacl_is_empty}; use util_common::*; const fn get_acl_type(default_acl: bool) -> acl_type_t { if default_acl { sg::ACL_TYPE_DEFAULT } else { sg::ACL_TYPE_ACCESS } } pub fn xacl_get_file(path: &Path, symlink_acl: bool, default_acl: bool) -> io::Result { if symlink_acl { return fail_custom("Linux does not support symlinks with ACL's."); } let acl_type = get_acl_type(default_acl); let c_path = CString::new(path.as_os_str().as_bytes())?; let acl = unsafe { acl_get_file(c_path.as_ptr(), acl_type) }; if acl.is_null() { let func = if default_acl { "acl_get_file/default" } else { "acl_get_file/access" }; return fail_err("null", func, &c_path); } Ok(acl) } pub fn xacl_set_file( path: &Path, acl: acl_t, symlink_acl: bool, default_acl: bool, ) -> io::Result<()> { if symlink_acl { return fail_custom("Linux does not support symlinks with ACL's"); } let c_path = CString::new(path.as_os_str().as_bytes())?; let acl_type = get_acl_type(default_acl); let ret = unsafe { acl_set_file(c_path.as_ptr(), acl_type, acl) }; if ret != 0 { let func = if default_acl { "acl_set_file/default" } else { "acl_set_file/access" }; return fail_err(ret, func, &c_path); } Ok(()) } fn xacl_get_qualifier(entry: acl_entry_t) -> io::Result { let tag = xacl_get_tag_type(entry)?; let id = if tag == sg::ACL_USER || tag == sg::ACL_GROUP { let id_ptr = unsafe { acl_get_qualifier(entry).cast::() }; if id_ptr.is_null() { return fail_err("null", "acl_get_qualifier", ()); } defer! { xacl_free(id_ptr) }; Some(unsafe { *id_ptr }) } else { None }; let result = match tag { sg::ACL_USER => Qualifier::User(id.unwrap()), sg::ACL_GROUP => Qualifier::Group(id.unwrap()), sg::ACL_USER_OBJ => Qualifier::UserObj, sg::ACL_GROUP_OBJ => Qualifier::GroupObj, sg::ACL_OTHER => Qualifier::Other, sg::ACL_MASK => Qualifier::Mask, tag => Qualifier::Unknown(format!("@tag {tag}")), }; Ok(result) } fn xacl_get_tag_qualifier(_acl: acl_t, entry: acl_entry_t) -> io::Result<(bool, Qualifier)> { let qualifier = xacl_get_qualifier(entry)?; Ok((true, qualifier)) } #[allow(clippy::unnecessary_wraps)] const fn xacl_get_flags(_acl: acl_t, _entry: acl_entry_t) -> io::Result { Ok(Flag::empty()) // noop } pub fn xacl_get_entry(acl: acl_t, entry: acl_entry_t) -> io::Result<(bool, Qualifier, Perm, Flag)> { let (allow, qualifier) = xacl_get_tag_qualifier(acl, entry)?; let perms = xacl_get_perm(entry)?; let flags = xacl_get_flags(acl, entry)?; Ok((allow, qualifier, perms, flags)) } pub fn xacl_set_qualifier(entry: acl_entry_t, mut id: uid_t) -> io::Result<()> { let id_ptr = std::ptr::addr_of_mut!(id); let ret = unsafe { acl_set_qualifier(entry, id_ptr.cast::()) }; if ret != 0 { return fail_err(ret, "acl_set_qualifier", ()); } Ok(()) } pub fn xacl_set_tag_qualifier( entry: acl_entry_t, allow: bool, qualifier: &Qualifier, ) -> io::Result<()> { if !allow { return fail_custom("allow=false is not supported on Linux"); } match qualifier { Qualifier::User(uid) => { xacl_set_tag_type(entry, sg::ACL_USER)?; xacl_set_qualifier(entry, *uid)?; } Qualifier::Group(gid) => { xacl_set_tag_type(entry, sg::ACL_GROUP)?; xacl_set_qualifier(entry, *gid)?; } Qualifier::UserObj => { xacl_set_tag_type(entry, sg::ACL_USER_OBJ)?; } Qualifier::GroupObj => { xacl_set_tag_type(entry, sg::ACL_GROUP_OBJ)?; } Qualifier::Other => { xacl_set_tag_type(entry, sg::ACL_OTHER)?; } Qualifier::Mask => { xacl_set_tag_type(entry, sg::ACL_MASK)?; } Qualifier::Unknown(tag) => { return fail_custom(&format!("unknown tag: {tag}")); } } Ok(()) } #[allow(clippy::unnecessary_wraps)] const fn xacl_set_flags(_entry: acl_entry_t, _flags: Flag) -> io::Result<()> { Ok(()) // noop } pub fn xacl_add_entry( acl: &mut acl_t, allow: bool, qualifier: &Qualifier, perms: Perm, flags: Flag, ) -> io::Result { // Check for duplicates already in the list. xacl_foreach(*acl, |entry| { let (_, prev) = xacl_get_tag_qualifier(*acl, entry)?; if prev == *qualifier { let default = if flags.contains(Flag::DEFAULT) { "default " } else { "" }; fail_custom(&format!("duplicate {default}entry for \"{prev}\""))?; } Ok(()) })?; let entry = xacl_create_entry(acl)?; xacl_set_tag_qualifier(entry, allow, qualifier)?; xacl_set_perm(entry, perms)?; xacl_set_flags(entry, flags)?; Ok(entry) } pub const fn xacl_is_posix(_acl: acl_t) -> bool { true } #[cfg(test)] mod util_linux_test { use super::*; #[test] fn test_acl_api_misuse() { // Create empty list and add an entry. let mut acl = xacl_init(1).unwrap(); let entry = xacl_create_entry(&mut acl).unwrap(); // Setting tag other than 1 or 2 results in EINVAL error. let err = xacl_set_tag_type(entry, 0).unwrap_err(); assert_eq!(err.raw_os_error(), Some(sg::EINVAL)); // Setting qualifier without first setting tag to a valid value results in EINVAL. let err = xacl_set_qualifier(entry, 500).unwrap_err(); assert_eq!(err.raw_os_error(), Some(sg::EINVAL)); // Try to set entry using unknown qualifier -- this should fail. let err = xacl_set_tag_qualifier(entry, true, &Qualifier::Unknown("x".to_string())).unwrap_err(); assert!(err.to_string().contains("unknown tag: x")); // Add another entry and set it to a valid value. let entry2 = xacl_create_entry(&mut acl).unwrap(); xacl_set_tag_type(entry2, sg::ACL_USER_OBJ).unwrap(); xacl_free(acl); } #[test] fn test_empty_acl() { let file = tempfile::NamedTempFile::new().unwrap(); let dir = tempfile::TempDir::new().unwrap(); let acl = xacl_init(1).unwrap(); assert!(xacl_is_empty(acl)); // Empty acl is not "valid". let ret = unsafe { acl_valid(acl) }; assert_eq!(ret, -1); // Write an empty access ACL to a file. Still works? xacl_set_file(file.as_ref(), acl, false, false) .ok() .unwrap(); // Write an empty default ACL to a file. Still works? // FIXME: Fails on ubuntu-18.04. //xacl_set_file(file.as_ref(), acl, false, true).ok().unwrap(); // Write an empty access ACL to a directory. Still works? xacl_set_file(dir.as_ref(), acl, false, false).ok().unwrap(); // Write an empty default ACL to a directory. Okay on Linux, FreeBSD. xacl_set_file(dir.as_ref(), acl, false, true).ok().unwrap(); xacl_free(acl); } #[test] fn test_uninitialized_entry() { let mut acl = xacl_init(1).unwrap(); let entry_p = xacl_create_entry(&mut acl).unwrap(); let (allow, qualifier) = xacl_get_tag_qualifier(acl, entry_p).unwrap(); assert_eq!(qualifier.name().unwrap(), "@tag 0"); assert!(allow); xacl_free(acl); } } exacl-0.10.0/src/util/util_macos.rs000064400000000000000000000221401046102023000152460ustar 00000000000000use crate::bititer::BitIter; use crate::failx::*; use crate::flag::Flag; use crate::perm::Perm; use crate::qualifier::Qualifier; use crate::sys::*; use crate::util::util_common; use scopeguard::defer; use std::ffi::{c_void, CString}; use std::io; use std::os::unix::ffi::OsStrExt; use std::path::Path; use uuid::Uuid; pub use util_common::{xacl_create_entry, xacl_foreach, xacl_free, xacl_init, xacl_is_empty}; use util_common::*; /// Return true if path exists, even if it's a symlink to nowhere. fn path_exists(path: &Path, symlink_only: bool) -> bool { if symlink_only { path.symlink_metadata().is_ok() } else { path.exists() } } /// Get the native ACL for a specific file or directory. /// /// If the file is a symlink, the `symlink_acl` argument determines whether to /// get the ACL from the symlink itself (true) or the file it points to (false). pub fn xacl_get_file(path: &Path, symlink_acl: bool, default_acl: bool) -> io::Result { if default_acl { return fail_custom("macOS does not support default ACL"); } let c_path = CString::new(path.as_os_str().as_bytes())?; let acl = if symlink_acl { unsafe { acl_get_link_np(c_path.as_ptr(), acl_type_t_ACL_TYPE_EXTENDED) } } else { unsafe { acl_get_file(c_path.as_ptr(), acl_type_t_ACL_TYPE_EXTENDED) } }; if acl.is_null() { let func = if symlink_acl { "acl_get_link_np" } else { "acl_get_file" }; let err = log_err("null", func, &c_path); // acl_get_file et al. can return NULL (ENOENT) if the file exists, but // there is no ACL. If the path exists, return an *empty* ACL. if err.raw_os_error() == Some(sg::ENOENT) && path_exists(path, symlink_acl) { return xacl_init(1); } return Err(err); } Ok(acl) } /// Set the acl for a symlink using `acl_set_fd`. fn xacl_set_file_symlink_alt(c_path: &CString, acl: acl_t) -> io::Result<()> { let fd = unsafe { open(c_path.as_ptr(), sg::O_SYMLINK) }; if fd < 0 { return fail_err(fd, "open", c_path); } defer! { unsafe{ close(fd) }; } let ret = unsafe { acl_set_fd(fd, acl) }; if ret != 0 { return fail_err(ret, "acl_set_fd", fd); } Ok(()) } pub fn xacl_set_file( path: &Path, acl: acl_t, symlink_acl: bool, default_acl: bool, ) -> io::Result<()> { if default_acl { return fail_custom("macOS does not support default ACL"); } let c_path = CString::new(path.as_os_str().as_bytes())?; let ret = if symlink_acl { unsafe { acl_set_link_np(c_path.as_ptr(), acl_type_t_ACL_TYPE_EXTENDED, acl) } } else { unsafe { acl_set_file(c_path.as_ptr(), acl_type_t_ACL_TYPE_EXTENDED, acl) } }; if ret != 0 { let err = log_err(ret, "acl_set_link_np", &c_path); // acl_set_link_np() returns ENOTSUP for symlinks. Work-around this // by using acl_set_fd(). if err.raw_os_error() == Some(sg::ENOTSUP) && symlink_acl { return xacl_set_file_symlink_alt(&c_path, acl); } return Err(err); } Ok(()) } /// Get the GUID qualifier and resolve it to a User/Group if possible. /// /// Only call this function for `ACL_EXTENDED_ALLOW` or `ACL_EXTENDED_DENY`. fn xacl_get_qualifier(entry: acl_entry_t) -> io::Result { let uuid_ptr = unsafe { acl_get_qualifier(entry).cast::() }; if uuid_ptr.is_null() { return fail_err("null", "acl_get_qualifier", ()); } defer! { xacl_free(uuid_ptr) } let guid = unsafe { *uuid_ptr }; Qualifier::from_guid(guid) } /// Get tag and qualifier from the entry. fn xacl_get_tag_qualifier(_acl: acl_t, entry: acl_entry_t) -> io::Result<(bool, Qualifier)> { let tag = xacl_get_tag_type(entry)?; let result = match tag { sg::ACL_EXTENDED_ALLOW => (true, xacl_get_qualifier(entry)?), sg::ACL_EXTENDED_DENY => (false, xacl_get_qualifier(entry)?), _ => (false, Qualifier::Unknown(format!("@tag {tag}"))), }; Ok(result) } /// Get flags from the entry. fn xacl_get_flags_np(obj: *mut c_void) -> io::Result { assert!(!obj.is_null()); let mut flagset: acl_flagset_t = std::ptr::null_mut(); let ret = unsafe { acl_get_flagset_np(obj, &mut flagset) }; if ret != 0 { return fail_err(ret, "acl_get_flagset_np", ()); } assert!(!flagset.is_null()); let mut flags = Flag::empty(); for flag in BitIter(Flag::all()) { let res = unsafe { acl_get_flag_np(flagset, flag.bits()) }; debug_assert!((0..=1).contains(&res)); if res == 1 { flags |= flag; } } Ok(flags) } fn xacl_get_flags(_acl: acl_t, entry: acl_entry_t) -> io::Result { xacl_get_flags_np(entry.cast::()) } pub fn xacl_get_entry(acl: acl_t, entry: acl_entry_t) -> io::Result<(bool, Qualifier, Perm, Flag)> { let (allow, qualifier) = xacl_get_tag_qualifier(acl, entry)?; let perms = xacl_get_perm(entry)?; let flags = xacl_get_flags(acl, entry)?; Ok((allow, qualifier, perms, flags)) } /// Set qualifier for entry. /// /// Used in test. pub fn xacl_set_qualifier(entry: acl_entry_t, qualifier: &Qualifier) -> io::Result<()> { // Translate qualifier User/Group to guid. let mut bytes = qualifier.guid()?.into_bytes(); let ret = unsafe { acl_set_qualifier(entry, bytes.as_mut_ptr().cast::()) }; if ret != 0 { return fail_err(ret, "acl_set_qualifier", ()); } Ok(()) } /// Set tag and qualifier for ACL entry. fn xacl_set_tag_qualifier( entry: acl_entry_t, allow: bool, qualifier: &Qualifier, ) -> io::Result<()> { let tag = if let Qualifier::Unknown(_) = qualifier { debug_assert!(!allow); sg::ACL_EXTENDED_DENY } else if allow { sg::ACL_EXTENDED_ALLOW } else { sg::ACL_EXTENDED_DENY }; xacl_set_tag_type(entry, tag)?; xacl_set_qualifier(entry, qualifier)?; Ok(()) } fn xacl_set_flags_np(obj: *mut c_void, flags: Flag) -> io::Result<()> { assert!(!obj.is_null()); let mut flagset: acl_flagset_t = std::ptr::null_mut(); let ret_get = unsafe { acl_get_flagset_np(obj, &mut flagset) }; if ret_get != 0 { return fail_err(ret_get, "acl_get_flagset_np", ()); } assert!(!flagset.is_null()); let ret_clear = unsafe { acl_clear_flags_np(flagset) }; if ret_clear != 0 { return fail_err(ret_clear, "acl_clear_flags_np", ()); } for flag in BitIter(flags) { let ret = unsafe { acl_add_flag_np(flagset, flag.bits()) }; debug_assert!(ret == 0); } Ok(()) } fn xacl_set_flags(entry: acl_entry_t, flags: Flag) -> io::Result<()> { xacl_set_flags_np(entry.cast::(), flags) } pub fn xacl_add_entry( acl: &mut acl_t, allow: bool, qualifier: &Qualifier, perms: Perm, flags: Flag, ) -> io::Result { let entry = xacl_create_entry(acl)?; xacl_set_tag_qualifier(entry, allow, qualifier)?; xacl_set_perm(entry, perms)?; xacl_set_flags(entry, flags)?; Ok(entry) } pub const fn xacl_is_posix(_acl: acl_t) -> bool { false } //////////////////////////////////////////////////////////////////////////////// #[cfg(test)] mod util_macos_test { use super::*; #[test] fn test_acl_init() { use std::convert::TryInto; let max_entries: usize = ACL_MAX_ENTRIES.try_into().unwrap(); let acl = xacl_init(max_entries).ok().unwrap(); assert!(!acl.is_null()); xacl_free(acl); // Custom error if we try to allocate MAX_ENTRIES + 1. let err = xacl_init(max_entries + 1).unwrap_err(); assert_eq!(err.to_string(), "Too many ACL entries"); } #[test] fn test_acl_too_big() { let mut acl = xacl_init(3).ok().unwrap(); assert!(!acl.is_null()); for _ in 0..ACL_MAX_ENTRIES { xacl_create_entry(&mut acl).unwrap(); } // Memory error if we try to allocate MAX_ENTRIES + 1. let err = xacl_create_entry(&mut acl).unwrap_err(); assert_eq!(err.raw_os_error(), Some(sg::ENOMEM)); xacl_free(acl); } #[test] fn test_acl_api_misuse() { let mut acl = xacl_init(1).unwrap(); let entry = xacl_create_entry(&mut acl).unwrap(); // Setting tag other than 1 or 2 results in EINVAL error. let err = xacl_set_tag_type(entry, 0).unwrap_err(); assert_eq!(err.raw_os_error(), Some(sg::EINVAL)); // Setting qualifier without first setting tag to a valid value results in EINVAL. let err = xacl_set_qualifier(entry, &Qualifier::Guid(Uuid::nil())).unwrap_err(); assert_eq!(err.raw_os_error(), Some(sg::EINVAL)); let entry2 = xacl_create_entry(&mut acl).unwrap(); xacl_set_tag_type(entry2, 1).unwrap(); xacl_free(acl); } #[test] fn test_uninitialized_entry() { let mut acl = xacl_init(1).unwrap(); let entry_p = xacl_create_entry(&mut acl).unwrap(); let (allow, qualifier) = xacl_get_tag_qualifier(acl, entry_p).unwrap(); assert_eq!(qualifier.name().unwrap(), "@tag 0"); assert!(!allow); xacl_free(acl); } } exacl-0.10.0/tests/run_tests.sh000075500000000000000000000040731046102023000145310ustar 00000000000000#! /usr/bin/env bash # Run all test suites. # # If run with `memcheck` argument, run all tests under valgrind. OS=$(uname -s | tr '[:upper:]' '[:lower:]') arg1="$1" script_dir=$(dirname "$0") cd "$script_dir" || exit 1 if [ ! -f ../target/debug/examples/exacl ]; then echo "exacl executable not found!" exit 1 fi unit_tests() { # Find executable files without file extensions. find ../target/debug/deps -type f -executable -print | grep -vE '\w+\.\w+$' } print_header() { # shellcheck disable=SC2046 printf "\n%s\n%s\n" "$1" $(printf '=%.0s' $(seq 1 ${#1})) } exit_status=0 if [ "$arg1" = "memcheck" ]; then # Enable memory check command and re-run unit tests under memcheck. export MEMCHECK="valgrind -q --error-exitcode=9 --leak-check=full --errors-for-leak-kinds=definite --suppressions=valgrind.supp --gen-suppressions=all" vers=$(valgrind --version) echo "Running tests with memcheck ($vers)" echo for test in $(unit_tests); do $MEMCHECK "$test" status=$? # Track if any memcheck returns a non-zero exit status. if [ $status -ne 0 ]; then exit_status=$status fi done fi for test in testsuite*_all.sh testsuite*_"$OS".sh; do if [ ! -f "$test" ]; then continue fi print_header "$test" ./"$test" status=$? # Track if any test returns a non-zero exit status. if [ $status -ne 0 ]; then exit_status=$status fi done # Run FreeBSD-specific tests. saved_tmp="$TMPDIR" for option in acls nfsv4acls; do if [ -d "/tmp/exacl_$option" ]; then export TMPDIR="/tmp/exacl_$option" for test in testsuite*_"$OS"_"$option".sh; do print_header "$test" ./"$test" status=$? # Track if any test returns a non-zero exit status. if [ $status -ne 0 ]; then exit_status=$status fi done fi done export TMPDIR="$saved_tmp" # Log non-zero exit status. if [ $exit_status -ne 0 ]; then echo "Exit Status: $exit_status" fi exit $exit_status exacl-0.10.0/tests/test_api.rs000064400000000000000000000122111046102023000143130ustar 00000000000000//! API Tests for exacl module. use ctor::ctor; use exacl::{getfacl, setfacl, AclEntry, AclOption, Perm}; use log::debug; use std::io; #[ctor] fn init() { env_logger::init(); } #[test] fn test_getfacl_file() -> io::Result<()> { let file = tempfile::NamedTempFile::new()?; let entries = getfacl(&file, None)?; #[cfg(target_os = "macos")] assert_eq!(entries.len(), 0); #[cfg(any(target_os = "linux", target_os = "freebsd"))] assert_eq!(entries.len(), 3); debug!("test_getfacl_file: {}", exacl::to_string(&entries)?); // Test default ACL on macOS (should fail). #[cfg(target_os = "macos")] { let result = getfacl(&file, AclOption::DEFAULT_ACL); assert!(result .unwrap_err() .to_string() .contains("macOS does not support default ACL")); } // Test default ACL (should be error; files don't have default ACL). #[cfg(target_os = "linux")] { let result = getfacl(&file, AclOption::DEFAULT_ACL); assert!(result .unwrap_err() .to_string() .contains("Permission denied")); } // Test default ACL (should be error; files don't have default ACL). #[cfg(target_os = "freebsd")] { let result = getfacl(&file, AclOption::DEFAULT_ACL); // If file is using NFSv4 ACL, the error message will be // "Default ACL not supported", otherwise the error message will be // "Invalid argument". let errmsg = result.unwrap_err().to_string(); assert!( errmsg.contains("Default ACL not supported") || errmsg.contains("Invalid argument") ); } Ok(()) } #[test] fn test_setfacl_file() -> io::Result<()> { let file = tempfile::NamedTempFile::new()?; let mut entries = getfacl(&file, None)?; entries.push(AclEntry::allow_user("500", Perm::READ, None)); setfacl(&[file], &entries, None)?; Ok(()) } #[test] #[cfg(target_os = "linux")] fn test_too_many_entries() -> io::Result<()> { use exacl::setfacl; // This test depends on the type of file system. With ext* systems, we // expect ACL's with 508 entries to fail. let mut entries = vec![ AclEntry::allow_user("", Perm::READ, None), AclEntry::allow_group("", Perm::READ, None), AclEntry::allow_other(Perm::empty(), None), AclEntry::allow_mask(Perm::READ, None), ]; for i in 500..1003 { entries.push(AclEntry::allow_user(&i.to_string(), Perm::READ, None)); } let files = [tempfile::NamedTempFile::new()?]; // 507 entries are okay. setfacl(&files, &entries, None)?; debug!("{} entries is okay", entries.len()); // Add 508th entry. entries.push(AclEntry::allow_user("1500", Perm::READ, None)); // 508th entry is one too many. let err = setfacl(&files, &entries, None).unwrap_err(); assert!(err.to_string().contains("No space left on device")); Ok(()) } #[test] fn test_reader_writer() -> io::Result<()> { let input = r#" u:aaa:rwx#comment g:bbb:rwx u:ccc:rx "#; let entries = exacl::from_str(input)?; let actual = exacl::to_string(&entries)?; let expected = r#"allow::user:aaa:read,write,execute allow::group:bbb:read,write,execute allow::user:ccc:read,execute "#; assert_eq!(expected, actual); Ok(()) } #[test] #[cfg(any(target_os = "linux", target_os = "freebsd"))] fn test_exclusive_acloptions() { let path = "/tmp"; let err1 = getfacl(path, AclOption::ACCESS_ACL | AclOption::DEFAULT_ACL).unwrap_err(); assert_eq!( err1.to_string(), "ACCESS_ACL and DEFAULT_ACL are mutually exclusive options" ); let err2 = setfacl(&[path], &[], AclOption::ACCESS_ACL | AclOption::DEFAULT_ACL).unwrap_err(); assert_eq!( err2.to_string(), "ACCESS_ACL and DEFAULT_ACL are mutually exclusive options" ); } #[test] #[cfg(target_os = "macos")] fn test_exclusive_acloptions() { let path = "/tmp"; let err1 = getfacl(path, AclOption::ACCESS_ACL | AclOption::DEFAULT_ACL).unwrap_err(); assert_eq!( err1.to_string(), "File \"/tmp\": macOS does not support default ACL" ); let err2 = setfacl(&[path], &[], AclOption::ACCESS_ACL | AclOption::DEFAULT_ACL).unwrap_err(); assert_eq!( err2.to_string(), "File \"/tmp\": macOS does not support default ACL" ); } #[test] #[cfg(any(target_os = "linux", target_os = "freebsd"))] fn test_from_mode() { let acl_7777 = exacl::to_string(&exacl::from_mode(0o7777)).unwrap(); assert_eq!(acl_7777, "allow::user::read,write,execute\nallow::group::read,write,execute\nallow::other::read,write,execute\n"); let acl_000 = exacl::to_string(&exacl::from_mode(0o000)).unwrap(); assert_eq!(acl_000, "allow::user::\nallow::group::\nallow::other::\n"); let acl_123 = exacl::to_string(&exacl::from_mode(0o123)).unwrap(); assert_eq!( acl_123, "allow::user::execute\nallow::group::write\nallow::other::write,execute\n" ); let acl_12345 = exacl::to_string(&exacl::from_mode(0o12345)).unwrap(); assert_eq!( acl_12345, "allow::user::write,execute\nallow::group::read\nallow::other::read,execute\n" ); } exacl-0.10.0/tests/test_examples.rs000064400000000000000000000037271046102023000153740ustar 00000000000000//! Test example code used in documentation. use std::io; #[test] fn test_string_format() -> io::Result<()> { let file = tempfile::NamedTempFile::new()?; let acl = exacl::getfacl(&file, None)?; let result = exacl::to_string(&acl)?; println!("test_string_format: {result:?}"); Ok(()) } #[test] #[cfg(feature = "serde")] fn test_json_format() -> io::Result<()> { let file = tempfile::NamedTempFile::new()?; let acl = exacl::getfacl(&file, None)?; let result = serde_json::to_string(&acl)?; println!("test_json_format: {result:?}"); Ok(()) } #[test] #[cfg(any(target_os = "linux", target_os = "freebsd"))] fn test_linux_acl() -> io::Result<()> { use exacl::{AclEntry, Perm}; let mut acl = exacl::from_mode(0o660); acl.push(AclEntry::allow_user("fred", Perm::READ | Perm::WRITE, None)); assert_eq!( exacl::to_string(&acl)?, "allow::user::read,write\nallow::group::read,write\nallow::other::\nallow::user:fred:read,write\n" ); //exacl::setfacl(&["/tmp/file"], &acl, None)?; Ok(()) } #[test] #[cfg(any(target_os = "linux", target_os = "freebsd"))] fn test_linux_acl_default() -> io::Result<()> { use exacl::{AclEntry, Flag, Perm}; let mut acl = exacl::from_mode(0o770); acl.push(AclEntry::allow_group( "accounting", Perm::READ | Perm::WRITE | Perm::EXECUTE, None, )); // Make default_acl a copy of access_acl. let mut default_acl: Vec = acl.clone(); for entry in &mut default_acl { entry.flags |= Flag::DEFAULT; } acl.append(&mut default_acl); 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"); //exacl::setfacl(&["./tmp/dir"], &acl, None)?; Ok(()) } exacl-0.10.0/tests/testsuite_darwin.sh000075500000000000000000000343171046102023000161040ustar 00000000000000#! /usr/bin/env bash # Basic test suite for exacl tool (Darwin/macOS). set -u -o pipefail EXACL='../target/debug/examples/exacl' ME=$(id -un) ME_NUM=$(id -u) MY_GROUP=$(id -gn) MY_GROUP_NUM=$(id -g) # Return true if file is readable. isReadable() { cat "$1" >/dev/null 2>&1 return $? } # Return true if file is writable (tries to overwrite file). isWritable() { echo "x" 2>/dev/null >"$1" # shellcheck disable=SC2320 return $? } # Return true if directory is readable. isReadableDir() { ls "$1" >/dev/null 2>&1 return $? } # Return true if link is readable. isReadableLink() { readlink "$1" >/dev/null 2>&1 return $? } # Put quotes back on JSON text. quotifyJson() { echo "$1" | sed -E -e 's/([A-Za-z0-9_-]+)/"\1"/g' -e 's/:"false"/:false/g' -e 's/:"true"/:true/g' } getAcl() { # shellcheck disable=SC2010 ls -le "$1" 2>/dev/null | grep -E '^ \d+: ' } # Called by shunit2 before all tests run. oneTimeSetUp() { # Use temp directory managed by shunit2. DIR="$SHUNIT_TMPDIR" FILE1="$DIR/file1" DIR1="$DIR/dir1" LINK1="$DIR/link1" LINK2="$DIR/link2" # Create empty file, dir, and links. umask 077 touch "$FILE1" mkdir "$DIR1" ln -s link1_to_nowhere "$LINK1" ln -s file1 "$LINK2" } testReadAclFromMissingFile() { msg=$($EXACL $DIR/non_existant 2>&1) assertEquals 1 $? assertEquals \ "File \"$DIR/non_existant\": No such file or directory (os error 2)" \ "$msg" } testReadAclForFile1() { msg=$($EXACL $FILE1) assertEquals 0 $? assertEquals "[]" "$msg" isReadable "$FILE1" && isWritable "$FILE1" assertEquals 0 $? # Add ACL entry for current user to "deny read". chmod +a "$ME deny read" "$FILE1" msg=$($EXACL $FILE1) assertEquals 0 $? assertEquals \ "[{kind:user,name:$ME,perms:[read],flags:[],allow:false}]" \ "${msg//\"/}" ! isReadable "$FILE1" && isWritable "$FILE1" assertEquals 0 $? # Remove user write perm. chmod u-w "$FILE1" ! isReadable "$FILE1" && ! isWritable "$FILE1" assertEquals 0 $? # Add ACL entry for current group to "allow write". chmod +a "$MY_GROUP allow write" "$FILE1" msg=$($EXACL $FILE1) assertEquals 0 $? assertEquals \ "[{kind:user,name:$ME,perms:[read],flags:[],allow:false},{kind:group,name:$MY_GROUP,perms:[write],flags:[],allow:true}]" \ "${msg//\"/}" ! isReadable "$FILE1" && isWritable "$FILE1" assertEquals 0 $? # Re-add user write perm that we removed above. Clear the ACL. chmod u+w "$FILE1" chmod -N "$FILE1" } testReadAclForDir1() { msg=$($EXACL $DIR1) assertEquals 0 $? assertEquals "[]" "$msg" # Add ACL entry for current user to "deny read" with inheritance flags. chmod +a "$ME deny read,file_inherit,directory_inherit,only_inherit" "$DIR1" msg=$($EXACL $DIR1) assertEquals 0 $? assertEquals \ "[{kind:user,name:$ME,perms:[read],flags:[file_inherit,directory_inherit,only_inherit],allow:false}]" \ "${msg//\"/}" isReadableDir "$DIR1" assertEquals 0 $? # Create subfile in DIR1. subfile="$DIR1/subfile" touch "$subfile" ! isReadable "$subfile" && isWritable "$subfile" assertEquals 0 $? msg=$($EXACL $subfile) assertEquals 0 $? assertEquals \ "[{kind:user,name:$ME,perms:[read],flags:[inherited],allow:false}]" \ "${msg//\"/}" # Create subdirectory in DIR1. subdir="$DIR1/subdir" mkdir "$subdir" msg=$($EXACL $subdir) assertEquals 0 $? assertEquals \ "[{kind:user,name:$ME,perms:[read],flags:[inherited,file_inherit,directory_inherit],allow:false}]" \ "${msg//\"/}" # Clear directory ACL's so we can delete them. chmod -a# 0 "$subdir" chmod -a# 0 "$DIR1" rmdir "$subdir" rm "$subfile" } testReadAclForLink1() { # Test symlink that goes nowhere. msg=$($EXACL $LINK1 2>&1) assertEquals 1 $? assertEquals "File \"$LINK1\": No such file or directory (os error 2)" "$msg" # Test symlink with no ACL. msg=$($EXACL --symlink $LINK1) assertEquals 0 $? assertEquals "[]" "$msg" # Add ACL entry for current user to "deny read". chmod -h +a "$ME deny read" "$LINK1" assertEquals 0 $? ! isReadableLink "$LINK1" assertEquals 0 $? msg=$($EXACL --symlink $LINK1) assertEquals 0 $? assertEquals \ "[{kind:user,name:$ME,perms:[read],flags:[],allow:false}]" \ "${msg//\"/}" # It appears that you can't further modify the ACL of a symbolic link if # you don't have 'read' access to the link anymore. msg=$(chmod -h -a# 0 "$LINK1" 2>&1) assertEquals 1 $? assertEquals \ "chmod: No ACL present '$LINK1' chmod: Failed to set ACL on file '$LINK1': Permission denied" \ "$msg" # Recreate the symlink here. ln -fs link1_to_nowhere "$LINK1" } testReadAclForLink2() { # Test symlink to file1. msg=$($EXACL $LINK2) assertEquals 0 $? assertEquals "[]" "$msg" # Add ACL entry for current user to "deny read". chmod +a "$ME deny read" "$LINK2" msg=$($EXACL $LINK2) assertEquals 0 $? assertEquals \ "[{kind:user,name:$ME,perms:[read],flags:[],allow:false}]" \ "${msg//\"/}" } testWriteAclToMissingFile() { input="[]" msg=$(echo "$input" | $EXACL --set $DIR/non_existant 2>&1) assertEquals 1 $? assertEquals \ "File \"$DIR/non_existant\": No such file or directory (os error 2)" \ "$msg" } testWriteAclToFile1() { # Set ACL to empty. input="[]" msg=$(echo "$input" | $EXACL --set $FILE1 2>&1) assertEquals 0 $? assertEquals "" "$msg" # Verify it's empty. msg=$($EXACL $FILE1) assertEquals 0 $? assertEquals "[]" "$msg" isReadable "$FILE1" && isWritable "$FILE1" assertEquals 0 $? # Set ACL for current user to "deny read". input=$(quotifyJson "[{kind:user,name:$ME,perms:[read],flags:[],allow:false}]") msg=$(echo "$input" | $EXACL --set $FILE1 2>&1) assertEquals 0 $? assertEquals "" "$msg" ! isReadable "$FILE1" && isWritable "$FILE1" assertEquals 0 $? # Check ACL using ls. msg=$(getAcl $FILE1) assertEquals \ " 0: user:$ME deny read" \ "$msg" # Check ACL again. msg=$($EXACL $FILE1) assertEquals 0 $? assertEquals \ "[{kind:user,name:$ME,perms:[read],flags:[],allow:false}]" \ "${msg//\"/}" } testWriteAclToDir1() { # Set ACL to empty. input="[]" msg=$(echo "$input" | $EXACL --set $DIR1 2>&1) assertEquals 0 $? assertEquals "" "$msg" # Verify it's empty. msg=$($EXACL $DIR1) assertEquals 0 $? assertEquals "[]" "$msg" isReadableDir "$DIR1" assertEquals 0 $? # Set ACL for current user to "deny read". input=$(quotifyJson "[{kind:user,name:$ME,perms:[read],flags:[],allow:false}]") msg=$(echo "$input" | $EXACL --set $DIR1 2>&1) assertEquals 0 $? assertEquals "" "$msg" ! isReadable "$DIR1" assertEquals 0 $? # Read ACL back. msg=$($EXACL $DIR1) assertEquals 0 $? assertEquals "$input" "$msg" } testWriteAclToLink1() { # Set ACL to empty. input="[]" msg=$(echo "$input" | $EXACL --symlink --set $LINK1 2>&1) assertEquals 0 $? assertEquals \ "" \ "$msg" isReadableLink "$LINK1" assertEquals 0 $? # Set ACL for current user to "deny read". input=$(quotifyJson "[{kind:user,name:$ME,perms:[read],flags:[],allow:false}]") msg=$(echo "$input" | $EXACL --symlink --set $LINK1 2>&1) assertEquals 0 $? assertEquals \ "" \ "$msg" ! isReadableLink "$LINK1" assertEquals 0 $? # Check ACL using ls. msg=$(getAcl $LINK1) assertEquals \ " 0: user:$ME deny read" \ "$msg" # Check ACL again. msg=$($EXACL --symlink $LINK1) assertEquals 0 $? assertEquals \ "[{kind:user,name:$ME,perms:[read],flags:[],allow:false}]" \ "${msg//\"/}" # Set ACL back to empty. We've removed READ permission for the link, so # this will fail. input="[]" msg=$(echo "$input" | $EXACL --symlink --set $LINK1 2>&1) assertEquals 1 $? assertEquals \ "File \"$LINK1\": Permission denied (os error 13)" \ "$msg" } testWriteAllFilePerms() { all="read,write,execute,delete,append,delete_child,readattr,writeattr,readextattr,writeextattr,readsecurity,writesecurity,chown,sync" input=$(quotifyJson "[{kind:user,name:$ME,perms:[$all],flags:[],allow:true}]") msg=$(echo "$input" | $EXACL --set $FILE1) assertEquals 0 $? assertEquals "" "$msg" msg=$($EXACL $FILE1) assertEquals 0 $? assertEquals \ "[{kind:user,name:$ME,perms:[$all],flags:[],allow:true}]" \ "${msg//\"/}" # ls output omits delete_child and sync. ls_perms="read,write,execute,delete,append,readattr,writeattr,readextattr,writeextattr,readsecurity,writesecurity,chown" msg=$(getAcl $FILE1) assertEquals \ " 0: user:$ME allow $ls_perms" \ "$msg" } testWriteAllFileFlags() { entry_flags="inherited,file_inherit,directory_inherit,limit_inherit,only_inherit" all="$entry_flags" input=$(quotifyJson "[{kind:user,name:$ME,perms:[read],flags:[$all],allow:true}]") msg=$(echo "$input" | $EXACL --set $FILE1) assertEquals 0 $? assertEquals "" "$msg" # N.B. "defer_inherit" flag is not returned. msg=$($EXACL $FILE1) assertEquals 0 $? assertEquals \ "[{kind:user,name:$ME,perms:[read],flags:[$entry_flags],allow:true}]" \ "${msg//\"/}" # ls output only shows inherited and limit_inherit. ls_perms="read,limit_inherit" msg=$(getAcl $FILE1) assertEquals \ " 0: user:$ME inherited allow $ls_perms" \ "$msg" } testWriteAllDirPerms() { all="read,write,execute,delete,append,delete_child,readattr,writeattr,readextattr,writeextattr,readsecurity,writesecurity,chown,sync" input=$(quotifyJson "[{kind:user,name:$ME,perms:[$all],flags:[],allow:true}]") msg=$(echo "$input" | $EXACL --set $DIR1) assertEquals 0 $? assertEquals "" "$msg" msg=$($EXACL $DIR1) assertEquals 0 $? assertEquals \ "[{kind:user,name:$ME,perms:[$all],flags:[],allow:true}]" \ "${msg//\"/}" } testWriteAllDirFlags() { entry_flags="inherited,file_inherit,directory_inherit,limit_inherit,only_inherit" all="$entry_flags" input=$(quotifyJson "[{kind:user,name:$ME,perms:[read],flags:[$all],allow:true}]") msg=$(echo "$input" | $EXACL --set $DIR1) assertEquals 0 $? assertEquals "" "$msg" # N.B. "defer_inherit" flag is not returned. msg=$($EXACL $DIR1) assertEquals 0 $? assertEquals \ "[{kind:user,name:$ME,perms:[read],flags:[$entry_flags],allow:true}]" \ "${msg//\"/}" } testWriteAclNumericUID() { # Set ACL for current user to "deny read". input=$(quotifyJson "[{kind:user,name:$ME_NUM,perms:[read],flags:[],allow:false}]") msg=$(echo "$input" | $EXACL --set $FILE1 2>&1) assertEquals 0 $? assertEquals "" "$msg" ! isReadable "$FILE1" && isWritable "$FILE1" assertEquals 0 $? # Check ACL again. msg=$($EXACL $FILE1) assertEquals 0 $? assertEquals \ "[{kind:user,name:$ME,perms:[read],flags:[],allow:false}]" \ "${msg//\"/}" } testWriteAclNumericGID() { # Set ACL for current group to "deny read". input=$(quotifyJson "[{kind:group,name:$MY_GROUP_NUM,perms:[read],flags:[],allow:false}]") msg=$(echo "$input" | $EXACL --set $FILE1 2>&1) assertEquals 0 $? assertEquals "" "$msg" ! isReadable "$FILE1" && isWritable "$FILE1" assertEquals 0 $? # Check ACL again. msg=$($EXACL $FILE1) assertEquals 0 $? assertEquals \ "[{kind:group,name:$MY_GROUP,perms:[read],flags:[],allow:false}]" \ "${msg//\"/}" } testWriteAclGUID() { # Set ACL for _spotlight group to "deny read" using GUID. spotlight_group="ABCDEFAB-CDEF-ABCD-EFAB-CDEF00000059" input=$(quotifyJson "[{kind:group,name:$spotlight_group,perms:[read],flags:[],allow:false}]") msg=$(echo "$input" | $EXACL --set $FILE1 2>&1) assertEquals 0 $? assertEquals "" "$msg" # Check ACL again. msg=$($EXACL $FILE1) assertEquals 0 $? assertEquals \ "[{kind:group,name:_spotlight,perms:[read],flags:[],allow:false}]" \ "${msg//\"/}" } testWriteAclGUID_nil() { # Set ACL for _spotlight group to "deny read" using GUID. nil_uuid="00000000-0000-0000-0000-000000000000" input=$(quotifyJson "[{kind:group,name:$nil_uuid,perms:[read],flags:[],allow:false}]") msg=$(echo "$input" | $EXACL --set $FILE1 2>&1) assertEquals 0 $? assertEquals "" "$msg" # Check ACL again. Note: change in kind. msg=$($EXACL $FILE1) assertEquals 0 $? assertEquals \ "[{kind:user,name:$nil_uuid,perms:[read],flags:[],allow:false}]" \ "${msg//\"/}" } testDefaultAclFails() { # Test that exacl returns an error; default acl not supported on macOS. msg=$($EXACL --default $DIR1 2>&1) assertEquals 1 $? assertEquals \ "File \"$DIR1\": macOS does not support default ACL" \ "$msg" msg=$(echo "[]" | $EXACL --set --default $DIR1 2>&1) assertEquals 1 $? assertEquals \ "File \"$DIR1\": macOS does not support default ACL" \ "$msg" } testMissingFlags() { input=$(quotifyJson "[{kind:user,name:501,perms:[execute],allow:true}]") msg=$(echo "$input" | $EXACL --set $DIR/non_existant 2>&1) assertEquals 1 $? assertEquals \ "File \"$DIR/non_existant\": No such file or directory (os error 2)" \ "${msg//\`/}" } testMissingAllow() { input=$(quotifyJson "[{kind:user,name:501,perms:[execute],flags:[]}]") msg=$(echo "$input" | $EXACL --set $DIR/non_existant 2>&1) assertEquals 1 $? assertEquals \ "File \"$DIR/non_existant\": No such file or directory (os error 2)" \ "${msg//\`/}" } # Duplicate entry is not an error on macOS. testDuplicateEntry() { input=$(quotifyJson "[{kind:user,name:501,perms:[execute],flags:[]},{kind:user,name:501,perms:[execute],flags:[]}]") msg=$(echo "$input" | $EXACL --set $DIR/non_existant 2>&1) assertEquals 1 $? assertEquals \ "File \"$DIR/non_existant\": No such file or directory (os error 2)" \ "${msg//\`/}" } # shellcheck disable=SC1091 . shunit2 exacl-0.10.0/tests/testsuite_freebsd_acls.sh000075500000000000000000000554161046102023000172370ustar 00000000000000#! /usr/bin/env bash # Basic test suite for exacl tool (Linux). set -u -o pipefail EXACL='../target/debug/examples/exacl' # Add memcheck command if defined. if [ -n "${MEMCHECK+x}" ]; then echo "# MEMCHECK=$MEMCHECK" EXACL="$MEMCHECK $EXACL" fi ME=$(id -un) ME_NUM=$(id -u) MY_GROUP=$(id -gn) MY_GROUP_NUM=$(id -g) # Return true if file is readable. isReadable() { cat "$1" >/dev/null 2>&1 return $? } # Return true if file is writable (tries to overwrite file). isWritable() { echo "x" 2>/dev/null >"$1" # shellcheck disable=SC2320 return $? } # Return true if directory is readable. isReadableDir() { ls "$1" >/dev/null 2>&1 return $? } # Return true if link is readable. isReadableLink() { readlink "$1" >/dev/null 2>&1 return $? } fileperms() { stat -f "%Sp" "$1" } # Put quotes back on JSON text. quotifyJson() { 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' } # Called by shunit2 before all tests run. oneTimeSetUp() { # Use temp directory managed by shunit2. DIR="$SHUNIT_TMPDIR" FILE1="$DIR/file1" DIR1="$DIR/dir1" LINK1="$DIR/link1" # Create empty file, dir, and link. umask 077 touch "$FILE1" mkdir "$DIR1" ln -s link1_to_nowhere "$LINK1" } REQUIRED_ENTRIES="{kind:user,name:,perms:[read,write],flags:[],allow:true},{kind:group,name:,perms:[],flags:[],allow:true},{kind:other,name:,perms:[],flags:[],allow:true}" testReadAclFromMissingFile() { msg=$($EXACL $DIR/non_existant 2>&1) assertEquals 1 $? assertEquals \ "File \"$DIR/non_existant\": No such file or directory (os error 2)" \ "$msg" } testReadAclForFile1() { msg=$($EXACL $FILE1) assertEquals 0 $? assertEquals \ "[{kind:user,name:,perms:[read,write],flags:[],allow:true},{kind:group,name:,perms:[],flags:[],allow:true},{kind:other,name:,perms:[],flags:[],allow:true}]" \ "${msg//\"/}" assertEquals "-rw-------" "$(fileperms $FILE1)" isReadable "$FILE1" && isWritable "$FILE1" assertEquals 0 $? # Add ACL entry for current user to "write-only". (Note: owner still has read access) setfacl -m "u:$ME:w" "$FILE1" msg=$($EXACL $FILE1) assertEquals 0 $? assertEquals \ "[{kind:user,name:,perms:[read,write],flags:[],allow:true},{kind:user,name:$ME,perms:[write],flags:[],allow:true},{kind:group,name:,perms:[],flags:[],allow:true},{kind:mask,name:,perms:[write],flags:[],allow:true},{kind:other,name:,perms:[],flags:[],allow:true}]" \ "${msg//\"/}" assertEquals "-rw--w----" "$(fileperms $FILE1)" isReadable "$FILE1" && isWritable "$FILE1" assertEquals 0 $? # Remove owner read perm. chmod u-rw "$FILE1" assertEquals "-----w----" "$(fileperms $FILE1)" # Add ACL entry for current group to "allow write". setfacl -m "g:$MY_GROUP:w" "$FILE1" msg=$($EXACL $FILE1) assertEquals 0 $? assertEquals \ "[{kind:user,name:,perms:[],flags:[],allow:true},{kind:user,name:$ME,perms:[write],flags:[],allow:true},{kind:group,name:,perms:[],flags:[],allow:true},{kind:group,name:$MY_GROUP,perms:[write],flags:[],allow:true},{kind:mask,name:,perms:[write],flags:[],allow:true},{kind:other,name:,perms:[],flags:[],allow:true}]" \ "${msg//\"/}" assertEquals "-----w----" "$(fileperms $FILE1)" # Reset permissions. chmod 600 "$FILE1" setfacl -b "$FILE1" } testReadAclForDir1() { msg=$($EXACL $DIR1) assertEquals 0 $? assertEquals \ "[{kind:user,name:,perms:[read,write,execute],flags:[],allow:true},{kind:group,name:,perms:[],flags:[],allow:true},{kind:other,name:,perms:[],flags:[],allow:true}]" \ "${msg//\"/}" # Add ACL entry for current user to "write-only". (Note: owner still has read access) setfacl -m "u:$ME:w" "$DIR1" msg=$($EXACL $DIR1) assertEquals 0 $? assertEquals \ "[{kind:user,name:,perms:[read,write,execute],flags:[],allow:true},{kind:user,name:$ME,perms:[write],flags:[],allow:true},{kind:group,name:,perms:[],flags:[],allow:true},{kind:mask,name:,perms:[write],flags:[],allow:true},{kind:other,name:,perms:[],flags:[],allow:true}]" \ "${msg//\"/}" assertEquals "drwx-w----" "$(fileperms $DIR1)" isReadableDir "$DIR1" assertEquals 0 $? # TODO: test default ACL in a separate test. # Clear directory ACL's so we can delete them. setfacl -b "$DIR1" } testReadAclForLink1() { # Test symlink with no ACL. msg=$($EXACL $LINK1 2>&1) assertEquals 1 $? assertEquals "File \"$LINK1\": No such file or directory (os error 2)" "$msg" # Test symlink with no ACL. msg=$($EXACL --symlink $LINK1 2>&1) assertEquals 0 $? assertEquals \ "[{kind:user,name:,perms:[read,write,execute],flags:[],allow:true},{kind:group,name:,perms:[],flags:[],allow:true},{kind:other,name:,perms:[],flags:[],allow:true}]" \ "${msg//\"/}" } testWriteAclToMissingFile() { input="[]" msg=$(echo "$input" | $EXACL --set $DIR/non_existant 2>&1) assertEquals 1 $? assertEquals \ "Invalid ACL: missing required entries" \ "$msg" input=$(quotifyJson "[{kind:user,name:,perms:[read,write],flags:[],allow:true},{kind:group,name:,perms:[],flags:[],allow:true},{kind:other,name:,perms:[],flags:[],allow:true}]") msg=$(echo "$input" | $EXACL --set $DIR/non_existant 2>&1) assertEquals 1 $? assertEquals \ "File \"$DIR/non_existant\": No such file or directory (os error 2)" \ "$msg" } testWriteAclToFile1() { # Set ACL to empty. input="[]" msg=$(echo "$input" | $EXACL --set $FILE1 2>&1) assertEquals "set acl to empty" 1 $? assertEquals \ "Invalid ACL: missing required entries" \ "$msg" # Verify ACL. msg=$($EXACL $FILE1) assertEquals "verify acl" 0 $? assertEquals \ "[{kind:user,name:,perms:[read,write],flags:[],allow:true},{kind:group,name:,perms:[],flags:[],allow:true},{kind:mask,name:,perms:[],flags:[],allow:true},{kind:other,name:,perms:[],flags:[],allow:true}]" \ "${msg//\"/}" assertEquals "-rw-------" "$(fileperms $FILE1)" isReadable "$FILE1" && isWritable "$FILE1" assertEquals "is readable" 0 $? # Set ACL for current user to "allow:false". This fails because of brand mismatch (FIXME). input=$(quotifyJson "[{kind:user,name:$ME,perms:[read],flags:[],allow:false}]") msg=$(echo "$input" | $EXACL --set $FILE1 2>&1) assertEquals "check failure" 1 $? assertEquals \ "File \"$FILE1\": Invalid argument (os error 22)" \ "$msg" # Set ACL for current user specifically. input=$(quotifyJson "[{kind:user,name:$ME,perms:[read],flags:[],allow:true}]") msg=$(echo "$input" | $EXACL --set $FILE1 2>&1) assertEquals "check required entry" 1 $? assertEquals \ 'Invalid ACL: missing required entry "user"' \ "$msg" # Set ACL for current user specifically, with required entries. input=$(quotifyJson "[{kind:user,name:,perms:[read,write],flags:[],allow:true},{kind:group,name:,perms:[read,write],flags:[], allow:true},{kind:mask,name:,perms:[read],flags:[], allow:true},{kind:other,name:,perms:[],flags:[], allow:true},{kind:user,name:$ME,perms:[read],flags:[],allow:true}]") msg=$(echo "$input" | $EXACL --set $FILE1 2>&1) assertEquals "check set acl" 0 $? assertEquals \ "" \ "${msg//\"/}" # Check ACL again. msg=$($EXACL $FILE1) assertEquals "check acl again" 0 $? assertEquals \ "[{kind:user,name:,perms:[read,write],flags:[],allow:true},{kind:user,name:$ME,perms:[read],flags:[],allow:true},{kind:group,name:,perms:[read,write],flags:[],allow:true},{kind:mask,name:,perms:[read],flags:[],allow:true},{kind:other,name:,perms:[],flags:[],allow:true}]" \ "${msg//\"/}" # Check ACL with getfacl. msg=$(getfacl -q $FILE1 2>/dev/null) assertEquals "check acl getfacl" 0 $? assertEquals \ "user::rw- user:$ME:r-- group::rw- # effective: r-- mask::r-- other::---" \ "${msg}" } testWriteAclToDir1() { # Set ACL to empty. input="[]" msg=$(echo "$input" | $EXACL --set $DIR1 2>&1) assertEquals 1 $? assertEquals \ "Invalid ACL: missing required entries" \ "$msg" # Verify directory ACL. msg=$($EXACL $DIR1) assertEquals 0 $? assertEquals \ "[{kind:user,name:,perms:[read,write,execute],flags:[],allow:true},{kind:group,name:,perms:[],flags:[],allow:true},{kind:mask,name:,perms:[],flags:[],allow:true},{kind:other,name:,perms:[],flags:[],allow:true}]" \ "${msg//\"/}" assertEquals "drwx------" "$(fileperms $DIR1)" isReadableDir "$DIR1" assertEquals 0 $? # Set ACL for current user to "deny read". Fails due to brand mismatch (FIXME). input=$(quotifyJson "[{kind:user,name:$ME,perms:[read],flags:[],allow:false}]") msg=$(echo "$input" | $EXACL --set $DIR1 2>&1) assertEquals 1 $? assertEquals \ "File \"$DIR1\": Invalid argument (os error 22)" \ "$msg" # Set ACL without mask entry. input=$(quotifyJson "[{kind:user,name:$ME,perms:[read],flags:[],allow:true},{kind:user,name:,perms:[read,write,execute],flags:[],allow:true},{kind:group,name:,perms:[],flags:[],allow:true},{kind:other,name:,perms:[],flags:[],allow:true}]") msg=$(echo "$input" | $EXACL --set $DIR1 2>&1) assertEquals 0 $? assertEquals \ "" \ "$msg" # Read ACL back. msg=$($EXACL $DIR1) assertEquals 0 $? assertEquals \ "[{kind:user,name:,perms:[read,write,execute],flags:[],allow:true},{kind:user,name:$ME,perms:[read],flags:[],allow:true},{kind:group,name:,perms:[],flags:[],allow:true},{kind:mask,name:,perms:[read],flags:[],allow:true},{kind:other,name:,perms:[],flags:[],allow:true}]" \ "${msg//\"/}" # Set ACL with mask entry. input=$(quotifyJson "[{kind:mask,name:,perms:[read],flags:[],allow:true},{kind:user,name:$ME,perms:[read],flags:[],allow:true},{kind:user,name:,perms:[read,write,execute],flags:[],allow:true},{kind:group,name:,perms:[],flags:[],allow:true},{kind:other,name:,perms:[],flags:[],allow:true}]") msg=$(echo "$input" | $EXACL --set $DIR1 2>&1) assertEquals 0 $? assertEquals \ "" \ "$msg" # Read ACL back. msg=$($EXACL $DIR1) assertEquals 0 $? assertEquals \ "[{kind:user,name:,perms:[read,write,execute],flags:[],allow:true},{kind:user,name:$ME,perms:[read],flags:[],allow:true},{kind:group,name:,perms:[],flags:[],allow:true},{kind:mask,name:,perms:[read],flags:[],allow:true},{kind:other,name:,perms:[],flags:[],allow:true}]" \ "${msg//\"/}" assertEquals "drwxr-----" "$(fileperms $DIR1)" # Check ACL with getfacl. msg=$(getfacl -q $DIR1 2>/dev/null) assertEquals "check acl getfacl" 0 $? assertEquals \ "user::rwx user:$ME:r-- group::--- mask::r-- other::---" \ "${msg}" # Reset ACL back to the original. input=$(quotifyJson "[{kind:user,name:,perms:[read,write,execute],flags:[],allow:true},{kind:group,name:,perms:[],flags:[],allow:true},{kind:other,name:,perms:[],flags:[],allow:true}]") msg=$(echo "$input" | $EXACL --set $DIR1 2>&1) assertEquals 0 $? assertEquals \ "" \ "$msg" } testWriteAclToLink1() { # Set ACL to empty. input="[]" msg=$(echo "$input" | $EXACL --set $LINK1 2>&1) assertEquals 1 $? assertEquals \ "Invalid ACL: missing required entries" \ "$msg" input=$(quotifyJson "[{kind:mask,name:,perms:[read],flags:[],allow:true},{kind:user,name:$ME,perms:[read],flags:[],allow:true},{kind:user,name:,perms:[read,write,execute],flags:[],allow:true},{kind:group,name:,perms:[],flags:[],allow:true},{kind:other,name:,perms:[],flags:[],allow:true}]") msg=$(echo "$input" | $EXACL --set $LINK1 2>&1) assertEquals 1 $? assertEquals \ "File \"$LINK1\": No such file or directory (os error 2)" \ "$msg" input=$(quotifyJson "[{kind:mask,name:,perms:[read],flags:[],allow:true},{kind:user,name:$ME,perms:[read],flags:[],allow:true},{kind:user,name:,perms:[read,write,execute],flags:[],allow:true},{kind:group,name:,perms:[],flags:[],allow:true},{kind:other,name:,perms:[],flags:[],allow:true}]") msg=$(echo "$input" | $EXACL --set --symlink $LINK1 2>&1) assertEquals 0 $? assertEquals \ "" \ "$msg" msg=$($EXACL --symlink $LINK1) assertEquals 0 $? assertEquals \ "[{kind:user,name:,perms:[read,write,execute],flags:[],allow:true},{kind:user,name:$ME,perms:[read],flags:[],allow:true},{kind:group,name:,perms:[],flags:[],allow:true},{kind:mask,name:,perms:[read],flags:[],allow:true},{kind:other,name:,perms:[],flags:[],allow:true}]" \ "${msg//\"/}" } testWriteAclNumericUID() { # Set ACL for current user to "deny read". input=$(quotifyJson "[{kind:user,name:$ME_NUM,perms:[read],flags:[],allow:true},$REQUIRED_ENTRIES,{kind:mask,name:,perms:[read],flags:[],allow:true}]") msg=$(echo "$input" | $EXACL --set $FILE1 2>&1) assertEquals 0 $? assertEquals "" "$msg" # Check ACL again. msg=$($EXACL $FILE1) assertEquals 0 $? assertEquals \ "[{kind:user,name:,perms:[read,write],flags:[],allow:true},{kind:user,name:$ME,perms:[read],flags:[],allow:true},{kind:group,name:,perms:[],flags:[],allow:true},{kind:mask,name:,perms:[read],flags:[],allow:true},{kind:other,name:,perms:[],flags:[],allow:true}]" \ "${msg//\"/}" # Check ACL with getfacl. msg=$(getfacl -q $FILE1 2>/dev/null) assertEquals "check acl getfacl" 0 $? assertEquals \ "user::rw- user:$ME:r-- group::--- mask::r-- other::---" \ "${msg}" } testWriteAclNumericGID() { # Set ACL for current group to "read". input=$(quotifyJson "[{kind:group,name:$MY_GROUP_NUM,perms:[read],flags:[],allow:true},$REQUIRED_ENTRIES,{kind:mask,name:,perms:[read],flags:[],allow:true}]") msg=$(echo "$input" | $EXACL --set $FILE1 2>&1) assertEquals 0 $? assertEquals "" "$msg" # Check ACL again. msg=$($EXACL $FILE1) assertEquals 0 $? assertEquals \ "[{kind:user,name:,perms:[read,write],flags:[],allow:true},{kind:group,name:,perms:[],flags:[],allow:true},{kind:group,name:$MY_GROUP,perms:[read],flags:[],allow:true},{kind:mask,name:,perms:[read],flags:[],allow:true},{kind:other,name:,perms:[],flags:[],allow:true}]" \ "${msg//\"/}" } testReadDefaultAcl() { # Reading default acl for a file should fail. msg=$($EXACL --default $FILE1 2>&1) assertEquals 1 $? assertEquals \ "File \"$FILE1\": Invalid argument (os error 22)" \ "$msg" # Reading default acl for a directory. msg=$($EXACL --default $DIR1 2>&1) assertEquals 0 $? assertEquals "[]" "$msg" } testWriteDefaultAcl() { input=$(quotifyJson "[{kind:group,name:$MY_GROUP_NUM,perms:[read],flags:[],allow:true},$REQUIRED_ENTRIES,{kind:mask,name:,perms:[read],flags:[],allow:true}]") msg=$(echo "$input" | $EXACL --set --default $DIR1 2>&1) assertEquals 0 $? assertEquals "" "$msg" # Check ACL again. msg=$($EXACL --default $DIR1) assertEquals 0 $? assertEquals \ "[{kind:user,name:,perms:[read,write],flags:[default],allow:true},{kind:group,name:,perms:[],flags:[default],allow:true},{kind:group,name:$MY_GROUP,perms:[read],flags:[default],allow:true},{kind:mask,name:,perms:[read],flags:[default],allow:true},{kind:other,name:,perms:[],flags:[default],allow:true}]" \ "${msg//\"/}" # Check ACL without --default. msg=$($EXACL $DIR1) assertEquals 0 $? assertEquals \ "[{kind:user,name:,perms:[read,write,execute],flags:[],allow:true},{kind:group,name:,perms:[],flags:[],allow:true},{kind:other,name:,perms:[],flags:[],allow:true},{kind:user,name:,perms:[read,write],flags:[default],allow:true},{kind:group,name:,perms:[],flags:[default],allow:true},{kind:group,name:$MY_GROUP,perms:[read],flags:[default],allow:true},{kind:mask,name:,perms:[read],flags:[default],allow:true},{kind:other,name:,perms:[],flags:[default],allow:true}]" \ "${msg//\"/}" # Check ACL with getfacl. msg=$(getfacl -q $DIR1 2>/dev/null) assertEquals "check acl getfacl" 0 $? assertEquals \ "user::rwx group::--- other::---" \ "${msg}" # Check default ACL with getfacl. msg=$(getfacl -dq $DIR1 2>/dev/null) assertEquals "check default acl getfacl" 0 $? assertEquals \ "user::rw- group::--- group:$MY_GROUP:r-- mask::r-- other::---" \ "${msg}" # Create subfile in DIR1. subfile="$DIR1/subfile" touch "$subfile" msg=$($EXACL $subfile 2>&1) assertEquals 0 $? # Bug? mask has empty perms? assertEquals \ "[{kind:user,name:,perms:[read,write],flags:[],allow:true},{kind:group,name:,perms:[],flags:[],allow:true},{kind:group,name:$MY_GROUP,perms:[read],flags:[],allow:true},{kind:mask,name:,perms:[],flags:[],allow:true},{kind:other,name:,perms:[],flags:[],allow:true}]" \ "${msg//\"/}" rm -f "$subfile" # Delete the default ACL. input="[]" msg=$(echo "$input" | $EXACL --set --default $DIR1 2>&1) assertEquals 0 $? assertEquals \ "" \ "$msg" # Default acl should now be empty. msg=$($EXACL --default $DIR1 2>&1) assertEquals 0 $? assertEquals "[]" "$msg" } testWriteUnifiedAclToFile1() { # Set ACL with required entries. input=$(quotifyJson "[{kind:user,name:,perms:[read,write],flags:[],allow:true},{kind:group,name:,perms:[read,write],flags:[],allow:true},{kind:other,name:,perms:[],flags:[],allow:true},{kind:user,name:,perms:[read,write],flags:[default],allow:true},{kind:group,name:,perms:[read,write],flags:[default],allow:true},{kind:other,name:,perms:[],flags:[default],allow:true}]") msg=$(echo "$input" | $EXACL --set $FILE1 2>&1) assertEquals "set unified acl" 1 $? assertEquals \ "File \"$FILE1\": Non-directory does not have default ACL" \ "$msg" # Check ACL is unchanged. msg=$($EXACL $FILE1) assertEquals "check acl again" 0 $? assertEquals \ "[{kind:user,name:,perms:[read,write],flags:[],allow:true},{kind:group,name:,perms:[],flags:[],allow:true},{kind:group,name:$MY_GROUP,perms:[read],flags:[],allow:true},{kind:mask,name:,perms:[read],flags:[],allow:true},{kind:other,name:,perms:[],flags:[],allow:true}]" \ "${msg//\"/}" } testWriteUnifiedAclToMissingFile() { # Set ACL with required entries. input=$(quotifyJson "[{kind:user,name:,perms:[read,write],flags:[],allow:true},{kind:group,name:,perms:[read,write],flags:[],allow:true},{kind:other,name:,perms:[],flags:[],allow:true},{kind:user,name:,perms:[read,write],flags:[default],allow:true},{kind:group,name:,perms:[read,write],flags:[default],allow:true},{kind:other,name:,perms:[],flags:[default],allow:true}]") msg=$(echo "$input" | $EXACL --set $DIR/non_existant 2>&1) assertEquals "set unified acl" 1 $? assertEquals \ "File \"$DIR/non_existant\": No such file or directory (os error 2)" \ "$msg" } testWriteUnifiedAclToDir1() { # Set ACL with required entries. input=$(quotifyJson "[{kind:user,name:,perms:[read,write],flags:[],allow:true},{kind:group,name:,perms:[read,write],flags:[],allow:true},{kind:other,name:,perms:[],flags:[],allow:true},{kind:user,name:,perms:[read,write],flags:[default],allow:true},{kind:group,name:,perms:[read,write],flags:[default],allow:true},{kind:other,name:,perms:[],flags:[default],allow:true}]") msg=$(echo "$input" | $EXACL --set $DIR1 2>&1) assertEquals "set unified acl" 0 $? assertEquals \ "" \ "$msg" # Check ACL is updated. msg=$($EXACL $DIR1) assertEquals "check acl again" 0 $? assertEquals \ "[{kind:user,name:,perms:[read,write],flags:[],allow:true},{kind:group,name:,perms:[read,write],flags:[],allow:true},{kind:other,name:,perms:[],flags:[],allow:true},{kind:user,name:,perms:[read,write],flags:[default],allow:true},{kind:group,name:,perms:[read,write],flags:[default],allow:true},{kind:other,name:,perms:[],flags:[default],allow:true}]" \ "${msg//\"/}" # Check ACL with getfacl. msg=$(getfacl -q $DIR1 2>/dev/null) assertEquals "check acl getfacl" 0 $? assertEquals \ "user::rw- group::rw- other::---" \ "${msg}" # Check default ACL with getfacl. msg=$(getfacl -dq $DIR1 2>/dev/null) assertEquals "check default acl getfacl" 0 $? assertEquals \ "user::rw- group::rw- other::---" \ "${msg}" } testSetDefault() { # Set ACL with both access and default entries. input=$(quotifyJson "[{kind:user,name:,perms:[execute],flags:[],allow:true},{kind:group,name:,perms:[],flags:[],allow:true},{kind:other,name:,perms:[execute],flags:[],allow:true},{kind:user,name:,perms:[read,write],flags:[default],allow:true},{kind:group,name:,perms:[read,write],flags:[default],allow:true},{kind:other,name:,perms:[],flags:[default],allow:true}]") msg=$(echo "$input" | $EXACL --set --default $DIR1 2>&1) assertEquals "set default acl" 1 $? assertEquals \ 'Invalid ACL: entry 3: duplicate default entry for "user"' \ "$msg" # Check ACL is updated. msg=$($EXACL $DIR1) assertEquals "check acl again" 0 $? assertEquals \ "[{kind:user,name:,perms:[read,write],flags:[],allow:true},{kind:group,name:,perms:[read,write],flags:[],allow:true},{kind:other,name:,perms:[],flags:[],allow:true},{kind:user,name:,perms:[read,write],flags:[default],allow:true},{kind:group,name:,perms:[read,write],flags:[default],allow:true},{kind:other,name:,perms:[],flags:[default],allow:true}]" \ "${msg//\"/}" # Remove the default ACL. input="[]" msg=$(echo "$input" | $EXACL --set --default $DIR1 2>&1) assertEquals "remove default acl" 0 $? assertEquals \ "" \ "$msg" # Check ACL is updated. msg=$($EXACL $DIR1) assertEquals "check acl again" 0 $? assertEquals \ "[{kind:user,name:,perms:[read,write],flags:[],allow:true},{kind:group,name:,perms:[read,write],flags:[],allow:true},{kind:other,name:,perms:[],flags:[],allow:true}]" \ "${msg//\"/}" } testMissingFlags() { input=$(quotifyJson "[{kind:user,name:501,perms:[execute],allow:true}]") msg=$(echo "$input" | $EXACL --set non_existant 2>&1) assertEquals 1 $? assertEquals \ 'Invalid ACL: missing required entry "user"' \ "${msg//\`/}" } testMissingAllow() { input=$(quotifyJson "[{kind:user,name:501,perms:[execute],flags:[]}]") msg=$(echo "$input" | $EXACL --set non_existant 2>&1) assertEquals 1 $? assertEquals \ 'Invalid ACL: missing required entry "user"' \ "${msg//\`/}" } # Multiple ACL entries with the same user/group ID. testDuplicateEntry() { input=$(quotifyJson "[{kind:user,name:501,perms:[execute]},$REQUIRED_ENTRIES,{kind:user,name:501,perms:[execute]}]") msg=$(echo "$input" | $EXACL --set non_existant 2>&1) assertEquals 1 $? assertEquals \ 'Invalid ACL: entry 4: duplicate entry for "user:501"' \ "${msg//\`/}" } # shellcheck disable=SC1091 . shunit2 exacl-0.10.0/tests/testsuite_freebsd_nfsv4acls.sh000075500000000000000000000564471046102023000202250ustar 00000000000000#! /usr/bin/env bash # Basic test suite for exacl tool (FreeBSD). set -u -o pipefail EXACL='../target/debug/examples/exacl' ME=$(id -un) ME_NUM=$(id -u) MY_GROUP=$(id -gn) MY_GROUP_NUM=$(id -g) fileperms() { stat -f "%Sp" "$1" } # Put quotes back on JSON text. quotifyJson() { 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' } # Called by shunit2 before all tests run. oneTimeSetUp() { # Use temp directory managed by shunit2. DIR="$SHUNIT_TMPDIR" FILE1="$DIR/file1" DIR1="$DIR/dir1" LINK1="$DIR/link1" # Create empty file, dir, and link. umask 077 touch "$FILE1" mkdir "$DIR1" ln -s link1_to_nowhere "$LINK1" } REQUIRED_ENTRIES="{kind:user,name:,perms:[read,write],flags:[],allow:true},{kind:group,name:,perms:[],flags:[],allow:true},{kind:other,name:,perms:[],flags:[],allow:true}" testReadAclFromMissingFile() { msg=$($EXACL $DIR/non_existant 2>&1) assertEquals 1 $? assertEquals \ "File \"$DIR/non_existant\": No such file or directory (os error 2)" \ "$msg" } testReadAclForFile1() { msg=$($EXACL -f std $FILE1) assertEquals 0 $? assertEquals \ "allow::user::read_data,write_data,append,readextattr,writeextattr,readattr,writeattr,readsecurity,writesecurity,chown,sync allow::group::readextattr,readattr,readsecurity,sync allow::everyone::readextattr,readattr,readsecurity,sync" \ "${msg//\"/}" assertEquals "-rw-------" "$(fileperms $FILE1)" # Add ACL entry for current user to "write-only". setfacl -m "u:$ME:w::allow" "$FILE1" assertEquals 0 $? msg=$($EXACL -f std $FILE1) assertEquals 0 $? assertEquals \ "allow::user:$ME:write_data allow::user::read_data,write_data,append,readextattr,writeextattr,readattr,writeattr,readsecurity,writesecurity,chown,sync allow::group::readextattr,readattr,readsecurity,sync allow::everyone::readextattr,readattr,readsecurity,sync" \ "${msg//\"/}" assertEquals "-rw-------" "$(fileperms $FILE1)" # Deny execute access for user "777" setfacl -m "g:777:execute::deny" "$FILE1" msg=$($EXACL -f std $FILE1) assertEquals 0 $? assertEquals \ "deny::group:777:execute allow::user:$ME:write_data allow::user::read_data,write_data,append,readextattr,writeextattr,readattr,writeattr,readsecurity,writesecurity,chown,sync allow::group::readextattr,readattr,readsecurity,sync allow::everyone::readextattr,readattr,readsecurity,sync" \ "${msg//\"/}" # Remove owner read perm. chmod u-rw "$FILE1" assertEquals "----------" "$(fileperms $FILE1)" msg=$($EXACL -f std $FILE1) assertEquals 0 $? assertEquals \ "allow::user::readextattr,writeextattr,readattr,writeattr,readsecurity,writesecurity,chown,sync allow::group::readextattr,readattr,readsecurity,sync allow::everyone::readextattr,readattr,readsecurity,sync" \ "${msg//\"/}" # Add ACL entry for current group to "allow write". setfacl -m "g:$MY_GROUP:w::allow" "$FILE1" msg=$($EXACL -f std $FILE1) assertEquals 0 $? assertEquals \ "allow::group:$MY_GROUP:write_data allow::user::readextattr,writeextattr,readattr,writeattr,readsecurity,writesecurity,chown,sync allow::group::readextattr,readattr,readsecurity,sync allow::everyone::readextattr,readattr,readsecurity,sync" \ "${msg//\"/}" assertEquals "----------" "$(fileperms $FILE1)" # Reset permissions. chmod 600 "$FILE1" msg=$($EXACL -f std $FILE1) assertEquals 0 $? assertEquals \ "allow::user::read_data,write_data,append,readextattr,writeextattr,readattr,writeattr,readsecurity,writesecurity,chown,sync allow::group::readextattr,readattr,readsecurity,sync allow::everyone::readextattr,readattr,readsecurity,sync" \ "${msg//\"/}" } testReadAclForDir1() { msg=$($EXACL -f std $DIR1) assertEquals 0 $? assertEquals \ "allow::user::execute,read_data,write_data,append,readextattr,writeextattr,readattr,writeattr,readsecurity,writesecurity,chown,sync allow::group::readextattr,readattr,readsecurity,sync allow::everyone::readextattr,readattr,readsecurity,sync" \ "${msg//\"/}" # Add ACL entry for current user to "write-only". (Note: owner still has read access) setfacl -m "u:$ME:w::allow" "$DIR1" msg=$($EXACL -f std $DIR1) assertEquals 0 $? assertEquals \ "allow::user:$ME:write_data allow::user::execute,read_data,write_data,append,readextattr,writeextattr,readattr,writeattr,readsecurity,writesecurity,chown,sync allow::group::readextattr,readattr,readsecurity,sync allow::everyone::readextattr,readattr,readsecurity,sync" \ "${msg//\"/}" assertEquals "drwx------" "$(fileperms $DIR1)" assertEquals 0 $? # Clear extended ACL entries. setfacl -b "$DIR1" msg=$($EXACL -f std $DIR1) assertEquals 0 $? assertEquals \ "allow::user::execute,read_data,write_data,append,readextattr,writeextattr,readattr,writeattr,readsecurity,writesecurity,chown,sync allow::group::readextattr,readattr,readsecurity,sync allow::everyone::readextattr,readattr,readsecurity,sync" \ "${msg//\"/}" } testReadAclForLink1() { # Test symlink with no ACL. msg=$($EXACL -f std $LINK1 2>&1) assertEquals 1 $? assertEquals "File \"$LINK1\": No such file or directory (os error 2)" "$msg" # Test symlink with no ACL. msg=$($EXACL --symlink -f std $LINK1 2>&1) assertEquals 0 $? assertEquals \ "allow::user::execute,read_data,write_data,append,readextattr,writeextattr,readattr,writeattr,readsecurity,writesecurity,chown,sync allow::group::readextattr,readattr,readsecurity,sync allow::everyone::readextattr,readattr,readsecurity,sync" \ "${msg//\"/}" } testWriteAclToMissingFile() { input="[]" msg=$(echo "$input" | $EXACL --set $DIR/non_existant 2>&1) assertEquals 1 $? assertEquals \ "Invalid ACL: missing required entries" \ "$msg" input=$(quotifyJson "[{kind:user,name:,perms:[read,write],flags:[],allow:true},{kind:group,name:,perms:[],flags:[],allow:true},{kind:other,name:,perms:[],flags:[],allow:true}]") msg=$(echo "$input" | $EXACL --set $DIR/non_existant 2>&1) assertEquals 1 $? assertEquals \ "File \"$DIR/non_existant\": No such file or directory (os error 2)" \ "$msg" } testWriteAclToFile1() { # Set ACL to empty. input="[]" msg=$(echo "$input" | $EXACL --set $FILE1 2>&1) assertEquals "set acl to empty" 1 $? assertEquals \ "Invalid ACL: missing required entries" \ "$msg" # Verify ACL. msg=$($EXACL -f std $FILE1) assertEquals "verify acl" 0 $? assertEquals \ "allow::user::read_data,write_data,append,readextattr,writeextattr,readattr,writeattr,readsecurity,writesecurity,chown,sync allow::group::readextattr,readattr,readsecurity,sync allow::everyone::readextattr,readattr,readsecurity,sync" \ "${msg//\"/}" assertEquals "-rw-------" "$(fileperms $FILE1)" # Set ACL for current user to "allow:false". input=$(quotifyJson "[{kind:user,name:$ME,perms:[read_data],flags:[],allow:false}]") msg=$(echo "$input" | $EXACL --set $FILE1 2>&1) assertEquals "check failure" 0 $? assertEquals \ "" \ "$msg" # Set ACL for current user specifically. input=$(quotifyJson "[{kind:user,name:$ME,perms:[read],flags:[],allow:true}]") msg=$(echo "$input" | $EXACL --set $FILE1 2>&1) assertEquals "check required entry" 1 $? assertEquals \ 'Invalid ACL: missing required entry "user"' \ "$msg" # Set ACL for current user specifically, with required entries. input=$(quotifyJson "[{kind:user,name:,perms:[read_data,write_data],flags:[],allow:true},{kind:group,name:,perms:[read_data,write_data],flags:[],allow:true},{kind:everyone,name:,perms:[],flags:[], allow:true},{kind:user,name:$ME,perms:[read_data],flags:[],allow:true}]") msg=$(echo "$input" | $EXACL --set $FILE1 2>&1) assertEquals "check set acl" 0 $? assertEquals \ "" \ "${msg//\"/}" # Check ACL again. msg=$($EXACL $FILE1) assertEquals "check acl again" 0 $? assertEquals \ "[{kind:user,name:,perms:[read_data,write_data],flags:[],allow:true},{kind:group,name:,perms:[read_data,write_data],flags:[],allow:true},{kind:everyone,name:,perms:[],flags:[],allow:true},{kind:user,name:$ME,perms:[read_data],flags:[],allow:true}]" \ "${msg//\"/}" # Check ACL with getfacl. msg=$(getfacl -q $FILE1 2>/dev/null | sed -e 's/ *//') assertEquals "check acl getfacl" 0 $? assertEquals \ "owner@:rw------------:-------:allow group@:rw------------:-------:allow everyone@:--------------:-------:allow user:$ME:r-------------:-------:allow" \ "${msg}" } testWriteAclToDir1() { # Set ACL to empty. input="[]" msg=$(echo "$input" | $EXACL --set $DIR1 2>&1) assertEquals 1 $? assertEquals \ "Invalid ACL: missing required entries" \ "$msg" # Verify directory ACL. msg=$($EXACL $DIR1) assertEquals 0 $? assertEquals \ "[{kind:user,name:,perms:[execute,read_data,write_data,append,readextattr,writeextattr,readattr,writeattr,readsecurity,writesecurity,chown,sync],flags:[],allow:true},{kind:group,name:,perms:[readextattr,readattr,readsecurity,sync],flags:[],allow:true},{kind:everyone,name:,perms:[readextattr,readattr,readsecurity,sync],flags:[],allow:true}]" \ "${msg//\"/}" assertEquals "drwx------" "$(fileperms $DIR1)" # Set ACL for current user to "deny read". input=$(quotifyJson "[{kind:user,name:$ME,perms:[read_data],flags:[],allow:false}]") msg=$(echo "$input" | $EXACL --set $DIR1 2>&1) assertEquals 0 $? assertEquals \ "" \ "$msg" # Set ACL without mask entry. input=$(quotifyJson "[{kind:user,name:$ME,perms:[read_data],flags:[],allow:true},{kind:user,name:,perms:[execute,read_data,write_data],flags:[],allow:true},{kind:group,name:,perms:[],flags:[],allow:true},{kind:everyone,name:,perms:[],flags:[],allow:true}]") msg=$(echo "$input" | $EXACL --set $DIR1 2>&1) assertEquals 0 $? assertEquals \ "" \ "$msg" # Read ACL back. msg=$($EXACL $DIR1) assertEquals 0 $? assertEquals \ "[{kind:user,name:$ME,perms:[read_data],flags:[],allow:true},{kind:user,name:,perms:[execute,read_data,write_data],flags:[],allow:true},{kind:group,name:,perms:[],flags:[],allow:true},{kind:everyone,name:,perms:[],flags:[],allow:true}]" \ "${msg//\"/}" # Set ACL with mask entry (not valid). input=$(quotifyJson "[{kind:mask,name:,perms:[read_data],flags:[],allow:true},{kind:user,name:$ME,perms:[read_data],flags:[],allow:true},{kind:user,name:,perms:[execute,read_data,write_data],flags:[],allow:true},{kind:group,name:,perms:[],flags:[],allow:true},{kind:everyone,name:,perms:[],flags:[],allow:true}]") msg=$(echo "$input" | $EXACL --set $DIR1 2>&1) assertEquals 1 $? assertEquals \ "Invalid ACL: entry 0: Invalid argument (os error 22)" \ "$msg" # Read ACL back. msg=$($EXACL $DIR1) assertEquals 0 $? assertEquals \ "[{kind:user,name:$ME,perms:[read_data],flags:[],allow:true},{kind:user,name:,perms:[execute,read_data,write_data],flags:[],allow:true},{kind:group,name:,perms:[],flags:[],allow:true},{kind:everyone,name:,perms:[],flags:[],allow:true}]" \ "${msg//\"/}" assertEquals "drwx------" "$(fileperms $DIR1)" # Check ACL with getfacl. msg=$(getfacl -q $DIR1 2>/dev/null | sed -e 's/ *//') assertEquals "check acl getfacl" 0 $? assertEquals \ "user:$ME:r-------------:-------:allow owner@:rwx-----------:-------:allow group@:--------------:-------:allow everyone@:--------------:-------:allow" \ "${msg}" # Reset ACL back to the original. chmod 0700 $DIR1 assertEquals 0 $? } testWriteAclToLink1() { # Set ACL to empty. input="[]" msg=$(echo "$input" | $EXACL --set $LINK1 2>&1) assertEquals 1 $? assertEquals \ "Invalid ACL: missing required entries" \ "$msg" input=$(quotifyJson "[{kind:mask,name:,perms:[read],flags:[],allow:true},{kind:user,name:$ME,perms:[read],flags:[],allow:true},{kind:user,name:,perms:[read,write,execute],flags:[],allow:true},{kind:group,name:,perms:[],flags:[],allow:true},{kind:other,name:,perms:[],flags:[],allow:true}]") msg=$(echo "$input" | $EXACL --set $LINK1 2>&1) assertEquals 1 $? assertEquals \ "File \"$LINK1\": No such file or directory (os error 2)" \ "$msg" input=$(quotifyJson "[{kind:user,name:$ME,perms:[read_data],flags:[],allow:true},{kind:user,name:,perms:[read_data,write_data,execute],flags:[],allow:true},{kind:group,name:,perms:[],flags:[],allow:true},{kind:everyone,name:,perms:[],flags:[],allow:true}]") msg=$(echo "$input" | $EXACL --set --symlink $LINK1 2>&1) assertEquals 0 $? assertEquals \ "" \ "$msg" msg=$($EXACL --symlink $LINK1) assertEquals 0 $? assertEquals \ "[{kind:user,name:$ME,perms:[read_data],flags:[],allow:true},{kind:user,name:,perms:[execute,read_data,write_data],flags:[],allow:true},{kind:group,name:,perms:[],flags:[],allow:true},{kind:everyone,name:,perms:[],flags:[],allow:true}]" \ "${msg//\"/}" } testWriteAclNumericUID() { # Set ACL for current user to "deny read". input=$(quotifyJson "[{kind:user,name:$ME_NUM,perms:[read_data],flags:[],allow:true}]") msg=$(echo "$input" | $EXACL --set $FILE1 2>&1) assertEquals 0 $? assertEquals "" "$msg" # Check ACL again. msg=$($EXACL $FILE1) assertEquals 0 $? assertEquals \ "[{kind:user,name:$ME,perms:[read_data],flags:[],allow:true}]" \ "${msg//\"/}" # Check ACL with getfacl. msg=$(getfacl -q $FILE1 2>/dev/null | sed -e 's/ *//') assertEquals "check acl getfacl" 0 $? assertEquals \ "user:$ME:r-------------:-------:allow" \ "${msg}" } testWriteAclNumericGID() { # Set ACL for current group to "read". input=$(quotifyJson "[{kind:group,name:$MY_GROUP_NUM,perms:[read_data],flags:[],allow:true}]") msg=$(echo "$input" | $EXACL --set $FILE1 2>&1) assertEquals 0 $? assertEquals "" "$msg" # Check ACL again. msg=$($EXACL $FILE1) assertEquals 0 $? assertEquals \ "[{kind:group,name:$MY_GROUP,perms:[read_data],flags:[],allow:true}]" \ "${msg//\"/}" } testReadDefaultAcl() { # Reading default acl for a file should fail. msg=$($EXACL --default $FILE1 2>&1) assertEquals 1 $? assertEquals \ "File \"$FILE1\": Default ACL not supported" \ "${msg}" # Reading default acl for a directory. msg=$($EXACL --default $DIR1 2>&1) assertEquals 1 $? assertEquals \ "File \"$DIR1\": Default ACL not supported" \ "${msg}" } testWriteDefaultAcl() { # This is wrong. (FIXME) input=$(quotifyJson "[{kind:group,name:$MY_GROUP_NUM,perms:[read_data],flags:[],allow:true}]") msg=$(echo "$input" | $EXACL --set --default $DIR1 2>&1) assertEquals 1 $? assertEquals \ "Invalid ACL: missing required entry \"user\"" \ "$msg" # Check ACL again. msg=$($EXACL --default $DIR1 2>&1) assertEquals 1 $? assertEquals \ "File \"$DIR1\": Default ACL not supported" \ "${msg}" # Check ACL without --default. msg=$($EXACL $DIR1) assertEquals 0 $? assertEquals \ "[{kind:user,name:,perms:[execute,read_data,write_data,append,readextattr,writeextattr,readattr,writeattr,readsecurity,writesecurity,chown,sync],flags:[],allow:true},{kind:group,name:,perms:[readextattr,readattr,readsecurity,sync],flags:[],allow:true},{kind:everyone,name:,perms:[readextattr,readattr,readsecurity,sync],flags:[],allow:true}]" \ "${msg//\"/}" # Check ACL with getfacl. msg=$(getfacl -q $DIR1 2>/dev/null | sed -e 's/ *//') assertEquals "check acl getfacl" 0 $? assertEquals \ "owner@:rwxp--aARWcCos:-------:allow group@:------a-R-c--s:-------:allow everyone@:------a-R-c--s:-------:allow" \ "${msg}" # Check default ACL with getfacl. msg=$(getfacl -dq $DIR1 2>&1) assertEquals "check default acl getfacl" 1 $? assertEquals \ "getfacl: $DIR1: there are no default entries in NFSv4 ACLs" \ "${msg}" # Create subfile in DIR1. (FIXME) subfile="$DIR1/subfile" touch "$subfile" msg=$($EXACL $subfile 2>&1) assertEquals 0 $? assertEquals \ "[{kind:user,name:,perms:[read_data,write_data,append,readextattr,writeextattr,readattr,writeattr,readsecurity,writesecurity,chown,sync],flags:[],allow:true},{kind:group,name:,perms:[readextattr,readattr,readsecurity,sync],flags:[],allow:true},{kind:everyone,name:,perms:[readextattr,readattr,readsecurity,sync],flags:[],allow:true}]" \ "${msg//\"/}" rm -f "$subfile" # Delete the default ACL. (FIXME) input="[]" msg=$(echo "$input" | $EXACL --set --default $DIR1 2>&1) assertEquals 1 $? assertEquals \ "File \"$DIR1\": Default ACL not supported" \ "$msg" # Default acl should now be empty. msg=$($EXACL --default $DIR1 2>&1) assertEquals 1 $? assertEquals \ "File \"$DIR1\": Default ACL not supported" \ "${msg}" } testWriteUnifiedAclToFile1() { # Set ACL with required entries. input=$(quotifyJson "[{kind:user,name:,perms:[read,write],flags:[],allow:true},{kind:group,name:,perms:[read,write],flags:[],allow:true},{kind:other,name:,perms:[],flags:[],allow:true},{kind:user,name:,perms:[read,write],flags:[default],allow:true},{kind:group,name:,perms:[read,write],flags:[default],allow:true},{kind:other,name:,perms:[],flags:[default],allow:true}]") msg=$(echo "$input" | $EXACL --set $FILE1 2>&1) assertEquals "set unified acl" 1 $? assertEquals \ "File \"$FILE1\": Non-directory does not have default ACL" \ "$msg" # Check ACL is unchanged. (FIXME) msg=$($EXACL $FILE1) assertEquals "check acl again" 0 $? assertEquals \ "[{kind:group,name:$MY_GROUP,perms:[read_data],flags:[],allow:true}]" \ "${msg//\"/}" } testWriteUnifiedAclToMissingFile() { # Set ACL with required entries. input=$(quotifyJson "[{kind:user,name:,perms:[read,write],flags:[],allow:true},{kind:group,name:,perms:[read,write],flags:[],allow:true},{kind:other,name:,perms:[],flags:[],allow:true},{kind:user,name:,perms:[read,write],flags:[default],allow:true},{kind:group,name:,perms:[read,write],flags:[default],allow:true},{kind:other,name:,perms:[],flags:[default],allow:true}]") msg=$(echo "$input" | $EXACL --set $DIR/non_existant 2>&1) assertEquals "set unified acl" 1 $? assertEquals \ "File \"$DIR/non_existant\": No such file or directory (os error 2)" \ "$msg" } testWriteUnifiedAclToDir1() { # Set ACL with required entries. (FIXME) input=$(quotifyJson "[{kind:user,name:,perms:[read_data,write_data],flags:[],allow:true},{kind:group,name:,perms:[read_data,write_data],flags:[],allow:true},{kind:everyone,name:,perms:[],flags:[],allow:true},{kind:user,name:,perms:[read,write],flags:[default],allow:true},{kind:group,name:,perms:[read,write],flags:[default],allow:true},{kind:other,name:,perms:[],flags:[default],allow:true}]") msg=$(echo "$input" | $EXACL --set $DIR1 2>&1) assertEquals "set unified acl" 0 $? assertEquals \ "" \ "$msg" # Check ACL is updated. (FIXME) msg=$($EXACL $DIR1) assertEquals "check acl again" 0 $? assertEquals \ "[{kind:user,name:,perms:[read_data,write_data],flags:[],allow:true},{kind:group,name:,perms:[read_data,write_data],flags:[],allow:true},{kind:everyone,name:,perms:[],flags:[],allow:true}]" \ "${msg//\"/}" # Check ACL with getfacl. msg=$(getfacl -q $DIR1 2>/dev/null | sed -e 's/ *//') assertEquals "check acl getfacl" 0 $? assertEquals \ "owner@:rw------------:-------:allow group@:rw------------:-------:allow everyone@:--------------:-------:allow" \ "${msg}" # Check default ACL with getfacl. msg=$(getfacl -dq $DIR1 2>&1) assertEquals "check default acl getfacl" 1 $? assertEquals \ "getfacl: $DIR1: there are no default entries in NFSv4 ACLs" \ "${msg}" } testSetDefault() { # Set ACL with both access and default entries. input=$(quotifyJson "[{kind:user,name:,perms:[execute],flags:[],allow:true},{kind:group,name:,perms:[],flags:[],allow:true},{kind:other,name:,perms:[execute],flags:[],allow:true},{kind:user,name:,perms:[read,write],flags:[default],allow:true},{kind:group,name:,perms:[read,write],flags:[default],allow:true},{kind:other,name:,perms:[],flags:[default],allow:true}]") msg=$(echo "$input" | $EXACL --set --default $DIR1 2>&1) assertEquals "set default acl" 1 $? assertEquals \ 'Invalid ACL: entry 3: duplicate default entry for "user"' \ "$msg" # Check ACL is updated. (FIXME) msg=$($EXACL $DIR1) assertEquals "check acl again" 0 $? assertEquals \ "[{kind:user,name:,perms:[read_data,write_data],flags:[],allow:true},{kind:group,name:,perms:[read_data,write_data],flags:[],allow:true},{kind:everyone,name:,perms:[],flags:[],allow:true}]" \ "${msg//\"/}" # Remove the default ACL. (FIXME) input="[]" msg=$(echo "$input" | $EXACL --set --default $DIR1 2>&1) assertEquals "remove default acl" 1 $? assertEquals \ "File \"$DIR1\": Default ACL not supported" \ "$msg" # Check ACL is updated. (FIXME) msg=$($EXACL $DIR1) assertEquals "check acl again" 0 $? assertEquals \ "[{kind:user,name:,perms:[read_data,write_data],flags:[],allow:true},{kind:group,name:,perms:[read_data,write_data],flags:[],allow:true},{kind:everyone,name:,perms:[],flags:[],allow:true}]" \ "${msg//\"/}" } testMissingFlags() { input=$(quotifyJson "[{kind:user,name:501,perms:[execute],allow:true}]") msg=$(echo "$input" | $EXACL --set non_existant 2>&1) assertEquals 1 $? assertEquals \ 'Invalid ACL: missing required entry "user"' \ "${msg//\`/}" } testMissingAllow() { input=$(quotifyJson "[{kind:user,name:501,perms:[execute],flags:[]}]") msg=$(echo "$input" | $EXACL --set non_existant 2>&1) assertEquals 1 $? assertEquals \ 'Invalid ACL: missing required entry "user"' \ "${msg//\`/}" } # Multiple ACL entries with the same user/group ID. testDuplicateEntry() { input=$(quotifyJson "[{kind:user,name:501,perms:[execute]},$REQUIRED_ENTRIES,{kind:user,name:501,perms:[execute]}]") msg=$(echo "$input" | $EXACL --set non_existant 2>&1) assertEquals 1 $? assertEquals \ 'Invalid ACL: entry 4: duplicate entry for "user:501"' \ "${msg//\`/}" } testMixedPerms() { input=$(quotifyJson "[{kind:user,name:,perms:[read_data,write],flags:[],allow:true},{kind:group,name:,perms:[],flags:[],allow:false}]") msg=$(echo "$input" | $EXACL --set $FILE1 2>&1) assertEquals "check set acl" 0 $? assertEquals \ "" \ "${msg//\"/}" # Check ACL again. msg=$($EXACL $FILE1) assertEquals "check acl" 0 $? assertEquals \ "[{kind:user,name:,perms:[read_data,write_data],flags:[],allow:true},{kind:group,name:,perms:[],flags:[],allow:false}]" \ "${msg//\"/}" # Check ACL with getfacl. msg=$(getfacl -q $FILE1 2>/dev/null | sed -e 's/ *//') assertEquals "check acl getfacl" 0 $? assertEquals \ "owner@:rw------------:-------:allow group@:--------------:-------:deny" \ "${msg}" } # shellcheck disable=SC1091 . shunit2 exacl-0.10.0/tests/testsuite_linux.sh000075500000000000000000000606161046102023000157600ustar 00000000000000#! /usr/bin/env bash # Basic test suite for exacl tool (Linux). set -u -o pipefail EXACL='../target/debug/examples/exacl' # Add memcheck command if defined. if [ -n "${MEMCHECK+x}" ]; then echo "# MEMCHECK=$MEMCHECK" EXACL="$MEMCHECK $EXACL" fi ME=$(id -un) ME_NUM=$(id -u) MY_GROUP=$(id -gn) MY_GROUP_NUM=$(id -g) # Return true if file is readable. isReadable() { cat "$1" >/dev/null 2>&1 return $? } # Return true if file is writable (tries to overwrite file). isWritable() { echo "x" 2>/dev/null >"$1" # shellcheck disable=SC2320 return $? } # Return true if directory is readable. isReadableDir() { ls "$1" >/dev/null 2>&1 return $? } # Return true if link is readable. isReadableLink() { readlink "$1" >/dev/null 2>&1 return $? } fileperms() { stat -c "%A" "$1" } # Put quotes back on JSON text. quotifyJson() { 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' } # Called by shunit2 before all tests run. oneTimeSetUp() { # Use temp directory managed by shunit2. DIR="$SHUNIT_TMPDIR" FILE1="$DIR/file1" DIR1="$DIR/dir1" LINK1="$DIR/link1" # Create empty file, dir, and link. umask 077 touch "$FILE1" mkdir "$DIR1" ln -s link1_to_nowhere "$LINK1" } REQUIRED_ENTRIES="{kind:user,name:,perms:[read,write],flags:[],allow:true},{kind:group,name:,perms:[],flags:[],allow:true},{kind:other,name:,perms:[],flags:[],allow:true}" testReadAclFromMissingFile() { msg=$($EXACL $DIR/non_existant 2>&1) assertEquals 1 $? assertEquals \ "File \"$DIR/non_existant\": No such file or directory (os error 2)" \ "$msg" } testReadAclForFile1() { msg=$($EXACL $FILE1) assertEquals 0 $? assertEquals \ "[{kind:user,name:,perms:[read,write],flags:[],allow:true},{kind:group,name:,perms:[],flags:[],allow:true},{kind:other,name:,perms:[],flags:[],allow:true}]" \ "${msg//\"/}" assertEquals "-rw-------" "$(fileperms $FILE1)" isReadable "$FILE1" && isWritable "$FILE1" assertEquals 0 $? # Add ACL entry for current user to "write-only". (Note: owner still has read access) setfacl -m "u:$ME:w" "$FILE1" msg=$($EXACL $FILE1) assertEquals 0 $? assertEquals \ "[{kind:user,name:,perms:[read,write],flags:[],allow:true},{kind:user,name:$ME,perms:[write],flags:[],allow:true},{kind:group,name:,perms:[],flags:[],allow:true},{kind:mask,name:,perms:[write],flags:[],allow:true},{kind:other,name:,perms:[],flags:[],allow:true}]" \ "${msg//\"/}" assertEquals "-rw--w----" "$(fileperms $FILE1)" isReadable "$FILE1" && isWritable "$FILE1" assertEquals 0 $? # Remove owner read perm. chmod u-rw "$FILE1" assertEquals "-----w----" "$(fileperms $FILE1)" ! isReadable "$FILE1" && ! isWritable "$FILE1" assertEquals 0 $? # Add ACL entry for current group to "allow write". setfacl -m "g:$MY_GROUP:w" "$FILE1" msg=$($EXACL $FILE1) assertEquals 0 $? assertEquals \ "[{kind:user,name:,perms:[],flags:[],allow:true},{kind:user,name:$ME,perms:[write],flags:[],allow:true},{kind:group,name:,perms:[],flags:[],allow:true},{kind:group,name:$MY_GROUP,perms:[write],flags:[],allow:true},{kind:mask,name:,perms:[write],flags:[],allow:true},{kind:other,name:,perms:[],flags:[],allow:true}]" \ "${msg//\"/}" assertEquals "-----w----" "$(fileperms $FILE1)" ! isReadable "$FILE1" && ! isWritable "$FILE1" assertEquals 0 $? # Reset permissions. chmod 600 "$FILE1" setfacl -b "$FILE1" } testReadAclForDir1() { msg=$($EXACL $DIR1) assertEquals 0 $? assertEquals \ "[{kind:user,name:,perms:[read,write,execute],flags:[],allow:true},{kind:group,name:,perms:[],flags:[],allow:true},{kind:other,name:,perms:[],flags:[],allow:true}]" \ "${msg//\"/}" # Add ACL entry for current user to "write-only". (Note: owner still has read access) setfacl -m "u:$ME:w" "$DIR1" msg=$($EXACL $DIR1) assertEquals 0 $? assertEquals \ "[{kind:user,name:,perms:[read,write,execute],flags:[],allow:true},{kind:user,name:$ME,perms:[write],flags:[],allow:true},{kind:group,name:,perms:[],flags:[],allow:true},{kind:mask,name:,perms:[write],flags:[],allow:true},{kind:other,name:,perms:[],flags:[],allow:true}]" \ "${msg//\"/}" assertEquals "drwx-w----" "$(fileperms $DIR1)" isReadableDir "$DIR1" assertEquals 0 $? # TODO: test default ACL in a separate test. # Clear directory ACL's so we can delete them. setfacl -b "$DIR1" } testReadAclForLink1() { # Test symlink with no ACL. Not supported on Linux. msg=$($EXACL $LINK1 2>&1) assertEquals 1 $? assertEquals "File \"$LINK1\": No such file or directory (os error 2)" "$msg" # Test symlink with no ACL. Not supported on Linux. msg=$($EXACL --symlink $LINK1 2>&1) assertEquals 1 $? assertEquals "File \"$LINK1\": Linux does not support symlinks with ACL's." "$msg" } testWriteAclToMissingFile() { input="[]" msg=$(echo "$input" | $EXACL --set $DIR/non_existant 2>&1) assertEquals 1 $? assertEquals \ "Invalid ACL: missing required entries" \ "$msg" input=$(quotifyJson "[{kind:user,name:,perms:[read,write],flags:[],allow:true},{kind:group,name:,perms:[],flags:[],allow:true},{kind:other,name:,perms:[],flags:[],allow:true}]") msg=$(echo "$input" | $EXACL --set $DIR/non_existant 2>&1) assertEquals 1 $? assertEquals \ "File \"$DIR/non_existant\": No such file or directory (os error 2)" \ "$msg" } testWriteAclToFile1() { # Set ACL to empty. input="[]" msg=$(echo "$input" | $EXACL --set $FILE1 2>&1) assertEquals "set acl to empty" 1 $? assertEquals \ "Invalid ACL: missing required entries" \ "$msg" # Verify ACL. msg=$($EXACL $FILE1) assertEquals "verify acl" 0 $? assertEquals \ "[{kind:user,name:,perms:[read,write],flags:[],allow:true},{kind:group,name:,perms:[],flags:[],allow:true},{kind:other,name:,perms:[],flags:[],allow:true}]" \ "${msg//\"/}" assertEquals "-rw-------" "$(fileperms $FILE1)" isReadable "$FILE1" && isWritable "$FILE1" assertEquals "is readable" 0 $? # Set ACL for current user to "allow:false". This fails on Linux. input=$(quotifyJson "[{kind:user,name:$ME,perms:[read],flags:[],allow:false}]") msg=$(echo "$input" | $EXACL --set $FILE1 2>&1) assertEquals "check failure" 1 $? assertEquals \ "Invalid ACL: entry 0: allow=false is not supported on Linux" \ "$msg" # Set ACL for current user specifically. input=$(quotifyJson "[{kind:user,name:$ME,perms:[read],flags:[],allow:true}]") msg=$(echo "$input" | $EXACL --set $FILE1 2>&1) assertEquals "check required entry" 1 $? assertEquals \ 'Invalid ACL: missing required entry "user"' \ "$msg" # Set ACL for current user specifically, with required entries. input=$(quotifyJson "[{kind:user,name:,perms:[read,write],flags:[],allow:true},{kind:group,name:,perms:[read,write],flags:[], allow:true},{kind:mask,name:,perms:[read],flags:[], allow:true},{kind:other,name:,perms:[],flags:[], allow:true},{kind:user,name:$ME,perms:[read],flags:[],allow:true}]") msg=$(echo "$input" | $EXACL --set $FILE1 2>&1) assertEquals "check set acl" 0 $? assertEquals \ "" \ "${msg//\"/}" # Check ACL again. msg=$($EXACL $FILE1) assertEquals "check acl again" 0 $? assertEquals \ "[{kind:user,name:,perms:[read,write],flags:[],allow:true},{kind:user,name:$ME,perms:[read],flags:[],allow:true},{kind:group,name:,perms:[read,write],flags:[],allow:true},{kind:mask,name:,perms:[read],flags:[],allow:true},{kind:other,name:,perms:[],flags:[],allow:true}]" \ "${msg//\"/}" # Check ACL with getfacl. msg=$(getfacl -cE $FILE1 2>/dev/null) assertEquals "check acl getfacl" 0 $? assertEquals \ "user::rw- user:$ME:r-- group::rw- mask::r-- other::---" \ "${msg}" } testWriteAclToDir1() { # Set ACL to empty. input="[]" msg=$(echo "$input" | $EXACL --set $DIR1 2>&1) assertEquals 1 $? assertEquals \ "Invalid ACL: missing required entries" \ "$msg" # Verify directory ACL. msg=$($EXACL $DIR1) assertEquals 0 $? assertEquals \ "[{kind:user,name:,perms:[read,write,execute],flags:[],allow:true},{kind:group,name:,perms:[],flags:[],allow:true},{kind:other,name:,perms:[],flags:[],allow:true}]" \ "${msg//\"/}" assertEquals "drwx------" "$(fileperms $DIR1)" isReadableDir "$DIR1" assertEquals 0 $? # Set ACL for current user to "deny read". Fails on Linux. input=$(quotifyJson "[{kind:user,name:$ME,perms:[read],flags:[],allow:false}]") msg=$(echo "$input" | $EXACL --set $DIR1 2>&1) assertEquals 1 $? assertEquals \ "Invalid ACL: entry 0: allow=false is not supported on Linux" \ "$msg" # Set ACL without mask entry. input=$(quotifyJson "[{kind:user,name:$ME,perms:[read],flags:[],allow:true},{kind:user,name:,perms:[read,write,execute],flags:[],allow:true},{kind:group,name:,perms:[],flags:[],allow:true},{kind:other,name:,perms:[],flags:[],allow:true}]") msg=$(echo "$input" | $EXACL --set $DIR1 2>&1) assertEquals 0 $? assertEquals \ "" \ "$msg" # Read ACL back. msg=$($EXACL $DIR1) assertEquals 0 $? assertEquals \ "[{kind:user,name:,perms:[read,write,execute],flags:[],allow:true},{kind:user,name:$ME,perms:[read],flags:[],allow:true},{kind:group,name:,perms:[],flags:[],allow:true},{kind:mask,name:,perms:[read],flags:[],allow:true},{kind:other,name:,perms:[],flags:[],allow:true}]" \ "${msg//\"/}" # Set ACL with mask entry. input=$(quotifyJson "[{kind:mask,name:,perms:[read],flags:[],allow:true},{kind:user,name:$ME,perms:[read],flags:[],allow:true},{kind:user,name:,perms:[read,write,execute],flags:[],allow:true},{kind:group,name:,perms:[],flags:[],allow:true},{kind:other,name:,perms:[],flags:[],allow:true}]") msg=$(echo "$input" | $EXACL --set $DIR1 2>&1) assertEquals 0 $? assertEquals \ "" \ "$msg" # Read ACL back. msg=$($EXACL $DIR1) assertEquals 0 $? assertEquals \ "[{kind:user,name:,perms:[read,write,execute],flags:[],allow:true},{kind:user,name:$ME,perms:[read],flags:[],allow:true},{kind:group,name:,perms:[],flags:[],allow:true},{kind:mask,name:,perms:[read],flags:[],allow:true},{kind:other,name:,perms:[],flags:[],allow:true}]" \ "${msg//\"/}" assertEquals "drwxr-----" "$(fileperms $DIR1)" # Check ACL with getfacl. msg=$(getfacl -cE $DIR1 2>/dev/null) assertEquals "check acl getfacl" 0 $? assertEquals \ "user::rwx user:$ME:r-- group::--- mask::r-- other::---" \ "${msg}" # Reset ACL back to the original. input=$(quotifyJson "[{kind:user,name:,perms:[read,write,execute],flags:[],allow:true},{kind:group,name:,perms:[],flags:[],allow:true},{kind:other,name:,perms:[],flags:[],allow:true}]") msg=$(echo "$input" | $EXACL --set $DIR1 2>&1) assertEquals 0 $? assertEquals \ "" \ "$msg" } testWriteAclToLink1() { # Set ACL to empty. input="[]" msg=$(echo "$input" | $EXACL --set $LINK1 2>&1) assertEquals 1 $? assertEquals \ "Invalid ACL: missing required entries" \ "$msg" input=$(quotifyJson "[{kind:mask,name:,perms:[read],flags:[],allow:true},{kind:user,name:$ME,perms:[read],flags:[],allow:true},{kind:user,name:,perms:[read,write,execute],flags:[],allow:true},{kind:group,name:,perms:[],flags:[],allow:true},{kind:other,name:,perms:[],flags:[],allow:true}]") msg=$(echo "$input" | $EXACL --set $LINK1 2>&1) assertEquals 1 $? assertEquals \ "File \"$LINK1\": No such file or directory (os error 2)" \ "$msg" input=$(quotifyJson "[{kind:mask,name:,perms:[read],flags:[],allow:true},{kind:user,name:$ME,perms:[read],flags:[],allow:true},{kind:user,name:,perms:[read,write,execute],flags:[],allow:true},{kind:group,name:,perms:[],flags:[],allow:true},{kind:other,name:,perms:[],flags:[],allow:true}]") msg=$(echo "$input" | $EXACL --set --symlink $LINK1 2>&1) assertEquals 1 $? assertEquals \ "File \"$LINK1\": Linux does not support symlinks with ACL's" \ "$msg" } testWriteAclNumericUID() { # Set ACL for current user to "deny read". input=$(quotifyJson "[{kind:user,name:$ME_NUM,perms:[read],flags:[],allow:true},$REQUIRED_ENTRIES,{kind:mask,name:,perms:[read],flags:[],allow:true}]") msg=$(echo "$input" | $EXACL --set $FILE1 2>&1) assertEquals 0 $? assertEquals "" "$msg" # Check ACL again. msg=$($EXACL $FILE1) assertEquals 0 $? assertEquals \ "[{kind:user,name:,perms:[read,write],flags:[],allow:true},{kind:user,name:$ME,perms:[read],flags:[],allow:true},{kind:group,name:,perms:[],flags:[],allow:true},{kind:mask,name:,perms:[read],flags:[],allow:true},{kind:other,name:,perms:[],flags:[],allow:true}]" \ "${msg//\"/}" # Check ACL with getfacl. msg=$(getfacl -cE $FILE1 2>/dev/null) assertEquals "check acl getfacl" 0 $? assertEquals \ "user::rw- user:$ME:r-- group::--- mask::r-- other::---" \ "${msg}" } testWriteAclNumericGID() { # Set ACL for current group to "read". input=$(quotifyJson "[{kind:group,name:$MY_GROUP_NUM,perms:[read],flags:[],allow:true},$REQUIRED_ENTRIES,{kind:mask,name:,perms:[read],flags:[],allow:true}]") msg=$(echo "$input" | $EXACL --set $FILE1 2>&1) assertEquals 0 $? assertEquals "" "$msg" # Check ACL again. msg=$($EXACL $FILE1) assertEquals 0 $? assertEquals \ "[{kind:user,name:,perms:[read,write],flags:[],allow:true},{kind:group,name:,perms:[],flags:[],allow:true},{kind:group,name:$MY_GROUP,perms:[read],flags:[],allow:true},{kind:mask,name:,perms:[read],flags:[],allow:true},{kind:other,name:,perms:[],flags:[],allow:true}]" \ "${msg//\"/}" } testReadDefaultAcl() { # Reading default acl for a file should fail. msg=$($EXACL --default $FILE1 2>&1) assertEquals 1 $? assertEquals \ "File \"$FILE1\": Permission denied (os error 13)" \ "$msg" # Reading default acl for a directory. msg=$($EXACL --default $DIR1 2>&1) assertEquals 0 $? assertEquals "[]" "$msg" } testWriteDefaultAcl() { input=$(quotifyJson "[{kind:group,name:$MY_GROUP_NUM,perms:[read],flags:[],allow:true},$REQUIRED_ENTRIES,{kind:mask,name:,perms:[read],flags:[],allow:true}]") msg=$(echo "$input" | $EXACL --set --default $DIR1 2>&1) assertEquals 0 $? assertEquals "" "$msg" # Check ACL again. msg=$($EXACL --default $DIR1) assertEquals 0 $? assertEquals \ "[{kind:user,name:,perms:[read,write],flags:[default],allow:true},{kind:group,name:,perms:[],flags:[default],allow:true},{kind:group,name:$MY_GROUP,perms:[read],flags:[default],allow:true},{kind:mask,name:,perms:[read],flags:[default],allow:true},{kind:other,name:,perms:[],flags:[default],allow:true}]" \ "${msg//\"/}" # Check ACL without --default. msg=$($EXACL $DIR1) assertEquals 0 $? assertEquals \ "[{kind:user,name:,perms:[read,write,execute],flags:[],allow:true},{kind:group,name:,perms:[],flags:[],allow:true},{kind:other,name:,perms:[],flags:[],allow:true},{kind:user,name:,perms:[read,write],flags:[default],allow:true},{kind:group,name:,perms:[],flags:[default],allow:true},{kind:group,name:$MY_GROUP,perms:[read],flags:[default],allow:true},{kind:mask,name:,perms:[read],flags:[default],allow:true},{kind:other,name:,perms:[],flags:[default],allow:true}]" \ "${msg//\"/}" # Check ACL with getfacl. msg=$(getfacl -cE $DIR1 2>/dev/null) assertEquals "check acl getfacl" 0 $? assertEquals \ "user::rwx group::--- other::--- default:user::rw- default:group::--- default:group:$MY_GROUP:r-- default:mask::r-- default:other::---" \ "${msg}" # Create subfile in DIR1. subfile="$DIR1/subfile" touch "$subfile" msg=$($EXACL $subfile 2>&1) assertEquals 0 $? assertEquals \ "[{kind:user,name:,perms:[read,write],flags:[],allow:true},{kind:group,name:,perms:[],flags:[],allow:true},{kind:group,name:$MY_GROUP,perms:[read],flags:[],allow:true},{kind:mask,name:,perms:[read],flags:[],allow:true},{kind:other,name:,perms:[],flags:[],allow:true}]" \ "${msg//\"/}" rm -f "$subfile" # Delete the default ACL. input="[]" msg=$(echo "$input" | $EXACL --set --default $DIR1 2>&1) assertEquals 0 $? assertEquals \ "" \ "$msg" # Default acl should now be empty. msg=$($EXACL --default $DIR1 2>&1) assertEquals 0 $? assertEquals "[]" "$msg" } testWriteUnifiedAclToFile1() { # Set ACL with required entries. input=$(quotifyJson "[{kind:user,name:,perms:[read,write],flags:[],allow:true},{kind:group,name:,perms:[read,write],flags:[],allow:true},{kind:other,name:,perms:[],flags:[],allow:true},{kind:user,name:,perms:[read,write],flags:[default],allow:true},{kind:group,name:,perms:[read,write],flags:[default],allow:true},{kind:other,name:,perms:[],flags:[default],allow:true}]") msg=$(echo "$input" | $EXACL --set $FILE1 2>&1) assertEquals "set unified acl" 1 $? assertEquals \ "File \"$FILE1\": Non-directory does not have default ACL" \ "$msg" # Check ACL is unchanged. msg=$($EXACL $FILE1) assertEquals "check acl again" 0 $? assertEquals \ "[{kind:user,name:,perms:[read,write],flags:[],allow:true},{kind:group,name:,perms:[],flags:[],allow:true},{kind:group,name:$MY_GROUP,perms:[read],flags:[],allow:true},{kind:mask,name:,perms:[read],flags:[],allow:true},{kind:other,name:,perms:[],flags:[],allow:true}]" \ "${msg//\"/}" } testWriteUnifiedAclToMissingFile() { # Set ACL with required entries. input=$(quotifyJson "[{kind:user,name:,perms:[read,write],flags:[],allow:true},{kind:group,name:,perms:[read,write],flags:[],allow:true},{kind:other,name:,perms:[],flags:[],allow:true},{kind:user,name:,perms:[read,write],flags:[default],allow:true},{kind:group,name:,perms:[read,write],flags:[default],allow:true},{kind:other,name:,perms:[],flags:[default],allow:true}]") msg=$(echo "$input" | $EXACL --set $DIR/non_existant 2>&1) assertEquals "set unified acl" 1 $? assertEquals \ "File \"$DIR/non_existant\": No such file or directory (os error 2)" \ "$msg" } testWriteUnifiedAclToDir1() { # Set ACL with required entries. input=$(quotifyJson "[{kind:user,name:,perms:[read,write],flags:[],allow:true},{kind:group,name:,perms:[read,write],flags:[],allow:true},{kind:other,name:,perms:[],flags:[],allow:true},{kind:user,name:,perms:[read,write],flags:[default],allow:true},{kind:group,name:,perms:[read,write],flags:[default],allow:true},{kind:other,name:,perms:[],flags:[default],allow:true}]") msg=$(echo "$input" | $EXACL --set $DIR1 2>&1) assertEquals "set unified acl" 0 $? assertEquals \ "" \ "$msg" # Check ACL is updated. msg=$($EXACL $DIR1) assertEquals "check acl again" 0 $? assertEquals \ "[{kind:user,name:,perms:[read,write],flags:[],allow:true},{kind:group,name:,perms:[read,write],flags:[],allow:true},{kind:other,name:,perms:[],flags:[],allow:true},{kind:user,name:,perms:[read,write],flags:[default],allow:true},{kind:group,name:,perms:[read,write],flags:[default],allow:true},{kind:other,name:,perms:[],flags:[default],allow:true}]" \ "${msg//\"/}" # Check ACL with getfacl. msg=$(getfacl -cE $DIR1 2>/dev/null) assertEquals "check acl getfacl" 0 $? assertEquals \ "user::rw- group::rw- other::--- default:user::rw- default:group::rw- default:other::---" \ "${msg}" } testWriteAccessAclToDir1() { # Check access ACL. msg=$($EXACL --access $DIR1) assertEquals "check acl" 0 $? assertEquals \ "[{kind:user,name:,perms:[read,write],flags:[],allow:true},{kind:group,name:,perms:[read,write],flags:[],allow:true},{kind:other,name:,perms:[],flags:[],allow:true}]" \ "${msg//\"/}" # Set access ACL. input=$(quotifyJson "[{kind:user,name:,perms:[execute],flags:[],allow:true},{kind:group,name:,perms:[],flags:[],allow:true},{kind:other,name:,perms:[],flags:[],allow:true}]") msg=$(echo "$input" | $EXACL --set --access $DIR1 2>&1) assertEquals "set access acl" 0 $? assertEquals \ "" \ "$msg" # Check access ACL is updated, and default ACL is unchanged. msg=$($EXACL $DIR1) assertEquals "check acl again" 0 $? assertEquals \ "[{kind:user,name:,perms:[execute],flags:[],allow:true},{kind:group,name:,perms:[],flags:[],allow:true},{kind:other,name:,perms:[],flags:[],allow:true},{kind:user,name:,perms:[read,write],flags:[default],allow:true},{kind:group,name:,perms:[read,write],flags:[default],allow:true},{kind:other,name:,perms:[],flags:[default],allow:true}]" \ "${msg//\"/}" } testSetDefault() { # Set ACL with both access and default entries. input=$(quotifyJson "[{kind:user,name:,perms:[execute],flags:[],allow:true},{kind:group,name:,perms:[],flags:[],allow:true},{kind:other,name:,perms:[execute],flags:[],allow:true},{kind:user,name:,perms:[read,write],flags:[default],allow:true},{kind:group,name:,perms:[read,write],flags:[default],allow:true},{kind:other,name:,perms:[],flags:[default],allow:true}]") msg=$(echo "$input" | $EXACL --set --default $DIR1 2>&1) assertEquals "set default acl" 1 $? assertEquals \ 'Invalid ACL: entry 3: duplicate default entry for "user"' \ "$msg" # Set ACL with default entries. input=$(quotifyJson "[{kind:user,name:,perms:[read,write],flags:[default],allow:true},{kind:group,name:,perms:[read,write],flags:[default],allow:true},{kind:other,name:,perms:[],flags:[default],allow:true}]") msg=$(echo "$input" | $EXACL --set --default $DIR1 2>&1) assertEquals 0 $? assertEquals \ '' \ "$msg" # Check ACL. msg=$($EXACL $DIR1) assertEquals "check acl again" 0 $? assertEquals \ "[{kind:user,name:,perms:[execute],flags:[],allow:true},{kind:group,name:,perms:[],flags:[],allow:true},{kind:other,name:,perms:[],flags:[],allow:true},{kind:user,name:,perms:[read,write],flags:[default],allow:true},{kind:group,name:,perms:[read,write],flags:[default],allow:true},{kind:other,name:,perms:[],flags:[default],allow:true}]" \ "${msg//\"/}" # Remove the default ACL. input="[]" msg=$(echo "$input" | $EXACL --set --default $DIR1 2>&1) assertEquals "remove default acl" 0 $? assertEquals \ "" \ "$msg" # Check ACL is updated. msg=$($EXACL $DIR1) assertEquals "check acl again" 0 $? assertEquals \ "[{kind:user,name:,perms:[execute],flags:[],allow:true},{kind:group,name:,perms:[],flags:[],allow:true},{kind:other,name:,perms:[],flags:[],allow:true}]" \ "${msg//\"/}" } testMissingFlags() { input=$(quotifyJson "[{kind:user,name:501,perms:[execute],allow:true}]") msg=$(echo "$input" | $EXACL --set non_existant 2>&1) assertEquals 1 $? assertEquals \ 'Invalid ACL: missing required entry "user"' \ "${msg//\`/}" } testMissingAllow() { input=$(quotifyJson "[{kind:user,name:501,perms:[execute],flags:[]}]") msg=$(echo "$input" | $EXACL --set non_existant 2>&1) assertEquals 1 $? assertEquals \ 'Invalid ACL: missing required entry "user"' \ "${msg//\`/}" } # Multiple ACL entries with the same user/group ID. testDuplicateEntry() { input=$(quotifyJson "[{kind:user,name:501,perms:[execute]},$REQUIRED_ENTRIES,{kind:user,name:501,perms:[execute]}]") msg=$(echo "$input" | $EXACL --set non_existant 2>&1) assertEquals 1 $? assertEquals \ 'Invalid ACL: entry 4: duplicate entry for "user:501"' \ "${msg//\`/}" # daemon is uid 1. input=$(quotifyJson "[{kind:user,name:1,perms:[execute]},$REQUIRED_ENTRIES,{kind:user,name:daemon,perms:[read]}]") msg=$(echo "$input" | $EXACL --set non_existant 2>&1) assertEquals 1 $? assertEquals \ 'Invalid ACL: entry 4: duplicate entry for "user:1"' \ "${msg//\`/}" # Test duplicate entry in default entries. input=$(quotifyJson "[$REQUIRED_ENTRIES,{kind:user,name:,perms:[execute],flags:[default]},{kind:group,name:,perms:[execute],flags:[default]},{kind:other,name:,perms:[execute],flags:[default]},{kind:other,name:,perms:[execute],flags:[default]}]") msg=$(echo "$input" | $EXACL --set non_existant 2>&1) assertEquals 1 $? assertEquals \ 'Invalid ACL: entry 6: duplicate default entry for "other"' \ "${msg//\`/}" } # shellcheck disable=SC1091 . shunit2 exacl-0.10.0/tests/testsuite_malformed_all.sh000075500000000000000000000204151046102023000174100ustar 00000000000000#! /usr/bin/env bash # Test suite that tests exacl tool with malformed input. set -u -o pipefail EXACL='../target/debug/examples/exacl' # Add memcheck command if defined. if [ -n "${MEMCHECK+x}" ]; then echo "# MEMCHECK=$MEMCHECK" EXACL="$MEMCHECK $EXACL" fi # Retrieve name of OS: "Darwin", "Linux", or "FreeBSD" CURRENT_OS=$(uname -s) # Put quotes back on JSON text. quotifyJson() { 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' } testInvalidType() { input="{}" msg=$(echo "$input" | $EXACL --set non_existant 2>&1) assertEquals 1 $? assertEquals \ "JSON parser error: invalid type: map, expected a sequence at line 1 column 1" \ "$msg" input="[" msg=$(echo "$input" | $EXACL --set non_existant 2>&1) assertEquals 1 $? assertEquals \ "JSON parser error: EOF while parsing a list at line 2 column 0" \ "$msg" } testInvalidKind() { input=$(quotifyJson "[{kind:invalid,name:,perms:[execute],flags:[],allow:true}]") msg=$(echo "$input" | $EXACL --set non_existant 2>&1) assertEquals 1 $? if [ "$CURRENT_OS" = "Darwin" ]; then expected='user, group, unknown' elif [ "$CURRENT_OS" = "FreeBSD" ]; then expected='user, group, mask, other, everyone, unknown' else expected='user, group, mask, other, unknown' fi assertEquals \ "JSON parser error: unknown variant invalid, expected one of $expected at line 1 column 18" \ "${msg//\`/}" } testInvalidUser() { input=$(quotifyJson "[{kind:user,name:non_existant_user,perms:[execute],flags:[],allow:true}]") msg=$(echo "$input" | $EXACL --set non_existant 2>&1) assertEquals 1 $? assertEquals \ "Invalid ACL: entry 0: unknown user name: \"non_existant_user\"" \ "$msg" input=$(quotifyJson "[{kind:user,name:4294967296,perms:[execute],flags:[],allow:true}]") msg=$(echo "$input" | $EXACL --set non_existant 2>&1) assertEquals 1 $? assertEquals \ "Invalid ACL: entry 0: unknown user name: \"4294967296\"" \ "$msg" } testInvalidGroup() { input=$(quotifyJson "[{kind:group,name:non_existant_group,perms:[execute],flags:[],allow:true}]") msg=$(echo "$input" | $EXACL --set non_existant 2>&1) assertEquals 1 $? assertEquals \ "Invalid ACL: entry 0: unknown group name: \"non_existant_group\"" \ "$msg" input=$(quotifyJson "[{kind:group,name:4294967296,perms:[execute],flags:[],allow:true}]") msg=$(echo "$input" | $EXACL --set non_existant 2>&1) assertEquals 1 $? assertEquals \ "Invalid ACL: entry 0: unknown group name: \"4294967296\"" \ "$msg" } testInvalidGUID() { input=$(quotifyJson "[{kind:group,name:00000000-0000-0000-000-000000000000,perms:[execute],flags:[],allow:true}]") msg=$(echo "$input" | $EXACL --set non_existant 2>&1) assertEquals 1 $? assertEquals \ "Invalid ACL: entry 0: unknown group name: \"00000000-0000-0000-000-000000000000\"" \ "$msg" } testUnknownKind() { input=$(quotifyJson "[{kind:unknown,name:501,perms:[execute],flags:[],allow:true}]") msg=$(echo "$input" | $EXACL --set non_existant 2>&1) assertEquals 1 $? assertEquals \ 'Invalid ACL: entry 0: unsupported kind: "unknown"' \ "$msg" } testInvalidPerm() { input=$(quotifyJson "[{kind:user,name:501,perms:[whatever],flags:[],allow:true}]") msg=$(echo "$input" | $EXACL --set non_existant 2>&1 | sed -e 's/`//g') assertEquals 1 $? if [ "$CURRENT_OS" = "Darwin" ]; then expected='read, write, execute, delete, append, delete_child, readattr, writeattr, readextattr, writeextattr, readsecurity, writesecurity, chown, sync' elif [ "$CURRENT_OS" = "FreeBSD" ]; then expected='read, write, execute, read_data, write_data, delete, append, delete_child, readattr, writeattr, readextattr, writeextattr, readsecurity, writesecurity, chown, sync' else expected='read, write, execute' fi assertEquals \ "JSON parser error: unknown variant whatever, expected one of $expected at line 1 column 48" \ "${msg//\`/}" } testInvalidFlag() { input=$(quotifyJson "[{kind:user,name:501,perms:[execute],flags:[whatever],allow:true}]") msg=$(echo "$input" | $EXACL --set non_existant 2>&1 | sed -e 's/`//g') assertEquals 1 $? if [ "$CURRENT_OS" = "Darwin" ]; then expected='expected one of inherited, file_inherit, directory_inherit, limit_inherit, only_inherit' elif [ "$CURRENT_OS" = "FreeBSD" ]; then expected='expected one of inherited, file_inherit, directory_inherit, limit_inherit, only_inherit, default' else expected='expected default' fi assertEquals \ "JSON parser error: unknown variant whatever, $expected at line 1 column 68" \ "${msg//\`/}" } testExtraAttribute() { input=$(quotifyJson "[{kind:user,name:501,perms:[execute],flags:[],allow:true,ignore:0}]") msg=$(echo "$input" | $EXACL --set non_existant 2>&1) assertEquals 1 $? assertEquals \ 'JSON parser error: unknown field ignore, expected one of kind, name, perms, flags, allow at line 1 column 82' \ "${msg//\`/}" } testDuplicateAttribute() { input=$(quotifyJson "[{kind:user,name:501,perms:[execute],flags:[],allow:true,allow:false}]") msg=$(echo "$input" | $EXACL --set non_existant 2>&1) assertEquals 1 $? assertEquals \ 'JSON parser error: duplicate field allow at line 1 column 81' \ "${msg//\`/}" } testMisspelledAttribute() { input=$(quotifyJson "[{kin:user,name:501,perms:[execute],flags:[],allow:true}]") msg=$(echo "$input" | $EXACL --set non_existant 2>&1) assertEquals 1 $? assertEquals \ 'JSON parser error: unknown field kin, expected one of kind, name, perms, flags, allow at line 1 column 8' \ "${msg//\`/}" } testPermsInvalidType() { input=$(quotifyJson "[{kind:user,name:501,perms:0,flags:[],allow:true}]") msg=$(echo "$input" | $EXACL --set non_existant 2>&1) assertEquals 1 $? assertEquals \ 'JSON parser error: invalid type: string "0", expected list of permissions at line 1 column 40' \ "$msg" } testFlagsInvalidType() { input=$(quotifyJson "[{kind:user,name:501,perms:[read],flags:0,allow:true}]") msg=$(echo "$input" | $EXACL --set non_existant 2>&1) assertEquals 1 $? assertEquals \ 'JSON parser error: invalid type: string "0", expected list of flags at line 1 column 57' \ "$msg" } testInterleavedEntryIndex() { # Test that interleaved access/default entries produce the correct ACL index # when there's an unknown user name. Skip this test on MacOS. if [ "$CURRENT_OS" = "Darwin" ]; then return 0 fi 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}]") msg=$(echo "$input" | $EXACL --set non_existant 2>&1) assertEquals 1 $? assertEquals \ 'Invalid ACL: entry 4: unknown user name: "non_existant"' \ "$msg" } testInvalidMask() { # Ignore test on macOS. if [ "$CURRENT_OS" = "Darwin" ]; then return 0 fi input=$(quotifyJson "[{kind:mask,name:invalid,perms:[],flags:[],allow:true}]") msg=$(echo "$input" | $EXACL --set non_existant 2>&1) assertEquals 1 $? assertEquals \ "Invalid ACL: entry 0: unknown mask name: \"invalid\"" \ "$msg" } testInvalidOther() { # Ignore test on macOS. if [ "$CURRENT_OS" = "Darwin" ]; then return 0 fi input=$(quotifyJson "[{kind:other,name:invalid,perms:[],flags:[],allow:true}]") msg=$(echo "$input" | $EXACL --set non_existant 2>&1) assertEquals 1 $? assertEquals \ "Invalid ACL: entry 0: unknown other name: \"invalid\"" \ "$msg" } testInvalidStdFormat() { input=$'group:a:read\nuser:x' msg=$(echo "$input" | $EXACL -f std --set non_existant 2>&1) assertEquals 1 $? assertEquals \ "Std parser error: Unknown ACL format: user:x" \ "${msg//\`/}" } # shellcheck disable=SC1091 . shunit2 exacl-0.10.0/tests/valgrind.supp000064400000000000000000000014361046102023000146630ustar 00000000000000# Valgrind suppression file. # # N.B. We need separate entries for statx(buf) and statx(file_name) because # glob "*" doesn't work in syscall name. # syscall, statx { statx(buf) points to unaddressable byte(s) Memcheck:Param statx(buf) fun:syscall fun:statx fun:_ZN3std3sys4unix2fs9try_statx* } { statx(file_name) points to unaddressable byte(s) Memcheck:Param statx(file_name) fun:syscall fun:statx fun:_ZN3std3sys4unix2fs9try_statx* } # statx, statx { statx(file_name) points to unaddressable byte(s) Memcheck:Param statx(file_name) fun:statx fun:statx fun:_ZN3std3sys4unix2fs9try_statx* } { statx(buf) points to unaddressable byte(s) Memcheck:Param statx(buf) fun:statx fun:statx fun:_ZN3std3sys4unix2fs9try_statx* }