├── .gitignore ├── .github └── workflows │ ├── run-on-host.sh │ ├── macos.yml │ ├── linux.yml │ ├── unsupported.yml │ ├── freebsd.yml │ ├── netbsd.yml │ └── android.yml ├── README.md ├── src ├── error.rs ├── sys │ ├── mod.rs │ ├── unsupported.rs │ ├── linux_macos.rs │ └── bsd.rs ├── util.rs └── lib.rs ├── LICENSE-MIT ├── Cargo.toml ├── tests └── main.rs └── LICENSE-APACHE /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /.github/workflows/run-on-host.sh: -------------------------------------------------------------------------------- 1 | #!/system/bin/sh 2 | 3 | CMD="$1" 4 | shift 5 | 6 | cd /data/local/tmp || exit 1 7 | exec "/data/host$CMD" "$@" 8 | -------------------------------------------------------------------------------- /.github/workflows/macos.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - master 5 | pull_request: 6 | branches: 7 | - master 8 | 9 | name: macos 10 | 11 | jobs: 12 | build: 13 | runs-on: macos-latest 14 | timeout-minutes: 30 15 | steps: 16 | - uses: actions/checkout@v4 17 | - uses: dtolnay/rust-toolchain@stable 18 | - run: cargo build --verbose 19 | - run: cargo test --verbose 20 | -------------------------------------------------------------------------------- /.github/workflows/linux.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - master 5 | pull_request: 6 | branches: 7 | - master 8 | 9 | name: linux 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | timeout-minutes: 30 15 | steps: 16 | - uses: actions/checkout@v4 17 | - uses: dtolnay/rust-toolchain@stable 18 | - run: cargo build --verbose 19 | - run: cargo test --verbose 20 | -------------------------------------------------------------------------------- /.github/workflows/unsupported.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - master 5 | pull_request: 6 | branches: 7 | - master 8 | 9 | name: unsupported 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | timeout-minutes: 30 15 | steps: 16 | - uses: actions/checkout@v4 17 | - uses: dtolnay/rust-toolchain@stable 18 | with: 19 | targets: x86_64-unknown-illumos 20 | - run: cargo build --verbose --target x86_64-unknown-illumos 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | xattr 2 | ===== 3 | 4 | A small library for setting, getting, and listing extended attributes. 5 | 6 | Supported Platforms: Android, Linux, MacOS, FreeBSD, and NetBSD. 7 | 8 | API Documentation: https://docs.rs/xattr/latest/xattr/ 9 | 10 | Unsupported Platforms 11 | -------------------------- 12 | 13 | This library includes no-op support for unsupported Unix platforms. That is, it will 14 | build on *all* Unix platforms but always fail on unsupported Unix platforms. 15 | 16 | 1. You can turn this off by disabling the default `unsupported` feature. If you 17 | do so, this library will fail to compile on unsupported platforms. 18 | 2. Alternatively, you can detect unsupported platforms at runtime by checking 19 | the `xattr::SUPPORTED_PLATFORM` boolean. 20 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | use std::error::Error; 2 | use std::fmt; 3 | 4 | /// The error type returned on unsupported platforms. 5 | /// 6 | /// On unsupported platforms, all operations will fail with an `io::Error` with 7 | /// a kind `io::ErrorKind::Unsupported` and an `UnsupportedPlatformError` error as the inner error. 8 | /// While you *could* check the inner error, it's probably simpler just to check 9 | /// `xattr::SUPPORTED_PLATFORM`. 10 | /// 11 | /// This error mostly exists for pretty error messages. 12 | #[derive(Copy, Clone, Debug)] 13 | pub struct UnsupportedPlatformError; 14 | 15 | impl Error for UnsupportedPlatformError { 16 | fn description(&self) -> &str { 17 | "unsupported platform" 18 | } 19 | } 20 | 21 | impl fmt::Display for UnsupportedPlatformError { 22 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 23 | write!( 24 | f, 25 | "unsupported platform, please file a bug at `https://github.com/Stebalien/xattr'" 26 | ) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /.github/workflows/freebsd.yml: -------------------------------------------------------------------------------- 1 | # Run cargo tests in a FreeBSD VM. This needs to run on one of the GitHub macos runners, because 2 | # they are currently the only ones to support virtualization. 3 | # 4 | # See https://github.com/vmactions/freebsd-vm 5 | 6 | on: 7 | push: 8 | branches: 9 | - master 10 | pull_request: 11 | branches: 12 | - master 13 | 14 | name: freebsd 15 | 16 | jobs: 17 | test: 18 | runs-on: ubuntu-22.04 19 | steps: 20 | - uses: actions/checkout@v4 21 | - name: Run tests in FreeBSD VM 22 | uses: vmactions/freebsd-vm@v1 23 | with: 24 | usesh: true 25 | prepare: | 26 | pkg install -y curl 27 | curl https://sh.rustup.rs -sSf --output rustup.sh 28 | sh rustup.sh -y --profile minimal --default-toolchain stable 29 | export PATH="${HOME}/.cargo/bin:$PATH" 30 | echo "~~~~ rustc --version ~~~~" 31 | rustc --version 32 | 33 | run: | 34 | export PATH="${HOME}/.cargo/bin:$PATH" 35 | ls -la 36 | cargo build --verbose 37 | cargo test --verbose 38 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Steven Allen 2 | 3 | Permission is hereby granted, free of charge, to any 4 | person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the 6 | Software without restriction, including without 7 | limitation the rights to use, copy, modify, merge, 8 | publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software 10 | is furnished to do so, subject to the following 11 | conditions: 12 | 13 | The above copyright notice and this permission notice 14 | shall be included in all copies or substantial portions 15 | of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 18 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 19 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 21 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 24 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 25 | DEALINGS IN THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "xattr" 3 | edition = "2021" 4 | version = "1.6.1" 5 | authors = ["Steven Allen "] 6 | description = "unix extended filesystem attributes" 7 | 8 | documentation = "https://docs.rs/xattr" 9 | repository = "https://github.com/Stebalien/xattr" 10 | keywords = ["xattr", "filesystem", "unix"] 11 | license = "MIT OR Apache-2.0" 12 | include = ["README.md", "LICENSE-*", "Cargo.toml", "src/**/*.rs", "tests/**/*.rs"] 13 | 14 | [features] 15 | default = ["unsupported"] 16 | # Adds a dummy implementation for unsupported platforms. This is useful when 17 | # developing platform-independent code that doesn't absolutely need xattr 18 | # support. 19 | # 20 | # You can disable this feature if you want compilation to fail on unsupported 21 | # platforms. This would make sense if you absolutely need xattr support. 22 | unsupported = [] 23 | 24 | [target.'cfg(any(target_os = "android", target_os = "linux", target_os = "macos", target_os = "hurd"))'.dependencies.rustix] 25 | version = "1.0.0" 26 | default-features = false 27 | features = ["fs", "std"] 28 | 29 | [target.'cfg(any(target_os = "freebsd", target_os = "netbsd"))'.dependencies] 30 | libc = "0.2.150" 31 | 32 | [dev-dependencies] 33 | tempfile = "3" 34 | -------------------------------------------------------------------------------- /.github/workflows/netbsd.yml: -------------------------------------------------------------------------------- 1 | # Run cargo tests in a NetBSD VM. This needs to run on one of the GitHub macos runners, because 2 | # they are currently the only ones to support virtualization. 3 | # 4 | # See https://github.com/vmactions/netbsd-vm 5 | # 6 | # The standard filesystem on NetBSD installs, ffs, doesn't support extended attributes. The 7 | # filesystem type needs to be set fo FFSv2ea (on NetBSD 10 or later) for this to be enabled. 8 | # See http://wikimirror.netbsd.de/tutorials/acls_and_extended_attributes_on_ffs/ 9 | 10 | # Disabled until NetBSD 10 is released and available via vmactions 11 | on: workflow_dispatch 12 | 13 | name: netbsd 14 | 15 | jobs: 16 | test: 17 | runs-on: ubuntu-22.04 18 | steps: 19 | - uses: actions/checkout@v4 20 | - name: Run tests in NetBSD VM 21 | uses: vmactions/netbsd-vm@v1 22 | with: 23 | usesh: true 24 | prepare: | 25 | /usr/sbin/pkg_add curl 26 | curl https://sh.rustup.rs -sSf --output rustup.sh 27 | sh rustup.sh -y --profile minimal --default-toolchain stable 28 | export PATH="${HOME}/.cargo/bin:$PATH" 29 | echo "~~~~ rustc --version ~~~~" 30 | rustc --version 31 | 32 | run: | 33 | export PATH="${HOME}/.cargo/bin:$PATH" 34 | ls -la 35 | cargo build --verbose 36 | cargo test --verbose 37 | -------------------------------------------------------------------------------- /src/sys/mod.rs: -------------------------------------------------------------------------------- 1 | macro_rules! platforms { 2 | ($($($platform:expr);* => $module:ident),*) => { 3 | $( 4 | #[cfg(any($(target_os = $platform),*))] 5 | #[cfg_attr(not(any($(target_os = $platform),*)), allow(dead_code))] 6 | mod $module; 7 | 8 | #[cfg(any($(target_os = $platform),*))] 9 | pub use self::$module::*; 10 | )* 11 | 12 | #[cfg(all(feature = "unsupported", not(any($($(target_os = $platform),*),*))))] 13 | #[cfg_attr(any($($(target_os = $platform),*),*), allow(dead_code))] 14 | mod unsupported; 15 | 16 | #[cfg(all(feature = "unsupported", not(any($($(target_os = $platform),*),*))))] 17 | pub use self::unsupported::*; 18 | 19 | /// A constant indicating whether or not the target platform is supported. 20 | /// 21 | /// To make programmer's lives easier, this library builds on all platforms. 22 | /// However, all function calls on unsupported platforms will return 23 | /// `io::Error`s. 24 | /// 25 | /// Note: If you would like compilation to simply fail on unsupported platforms, 26 | /// turn of the `unsupported` feature. 27 | pub const SUPPORTED_PLATFORM: bool = cfg!(any($($(target_os = $platform),*),*)); 28 | } 29 | } 30 | 31 | platforms! { 32 | "android"; "linux"; "macos"; "hurd" => linux_macos, 33 | "freebsd"; "netbsd" => bsd 34 | } 35 | -------------------------------------------------------------------------------- /src/sys/unsupported.rs: -------------------------------------------------------------------------------- 1 | use std::ffi::{OsStr, OsString}; 2 | use std::io; 3 | use std::os::unix::io::BorrowedFd; 4 | use std::path::Path; 5 | 6 | use crate::UnsupportedPlatformError; 7 | 8 | pub const ENOATTR: i32 = 0; 9 | pub const ERANGE: i32 = 0; 10 | 11 | /// An iterator over a set of extended attributes names. 12 | #[derive(Clone, Default)] 13 | pub struct XAttrs; 14 | 15 | impl Iterator for XAttrs { 16 | type Item = OsString; 17 | fn next(&mut self) -> Option { 18 | None 19 | } 20 | 21 | fn size_hint(&self) -> (usize, Option) { 22 | (0, Some(0)) 23 | } 24 | } 25 | 26 | pub fn get_fd(_: BorrowedFd<'_>, _: &OsStr) -> io::Result> { 27 | Err(io::Error::new( 28 | io::ErrorKind::Unsupported, 29 | UnsupportedPlatformError, 30 | )) 31 | } 32 | 33 | pub fn set_fd(_: BorrowedFd<'_>, _: &OsStr, _: &[u8]) -> io::Result<()> { 34 | Err(io::Error::new( 35 | io::ErrorKind::Unsupported, 36 | UnsupportedPlatformError, 37 | )) 38 | } 39 | 40 | pub fn remove_fd(_: BorrowedFd<'_>, _: &OsStr) -> io::Result<()> { 41 | Err(io::Error::new( 42 | io::ErrorKind::Unsupported, 43 | UnsupportedPlatformError, 44 | )) 45 | } 46 | 47 | pub fn list_fd(_: BorrowedFd<'_>) -> io::Result { 48 | Err(io::Error::new( 49 | io::ErrorKind::Unsupported, 50 | UnsupportedPlatformError, 51 | )) 52 | } 53 | 54 | pub fn get_path(_: &Path, _: &OsStr, _: bool) -> io::Result> { 55 | Err(io::Error::new( 56 | io::ErrorKind::Unsupported, 57 | UnsupportedPlatformError, 58 | )) 59 | } 60 | 61 | pub fn set_path(_: &Path, _: &OsStr, _: &[u8], _: bool) -> io::Result<()> { 62 | Err(io::Error::new( 63 | io::ErrorKind::Unsupported, 64 | UnsupportedPlatformError, 65 | )) 66 | } 67 | 68 | pub fn remove_path(_: &Path, _: &OsStr, _: bool) -> io::Result<()> { 69 | Err(io::Error::new( 70 | io::ErrorKind::Unsupported, 71 | UnsupportedPlatformError, 72 | )) 73 | } 74 | 75 | pub fn list_path(_: &Path, _: bool) -> io::Result { 76 | Err(io::Error::new( 77 | io::ErrorKind::Unsupported, 78 | UnsupportedPlatformError, 79 | )) 80 | } 81 | -------------------------------------------------------------------------------- /src/util.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | use std::mem::MaybeUninit; 3 | 4 | pub fn extract_noattr(result: io::Result>) -> io::Result>> { 5 | result.map(Some).or_else(|e| { 6 | if e.raw_os_error() == Some(crate::sys::ENOATTR) { 7 | Ok(None) 8 | } else { 9 | Err(e) 10 | } 11 | }) 12 | } 13 | 14 | /// Calls `get_value` to with a buffer and `get_size` to estimate the size of the buffer if/when 15 | /// `get_value` returns ERANGE. 16 | #[allow(dead_code)] 17 | pub fn allocate_loop(mut get_value: F, mut get_size: S) -> io::Result> 18 | where 19 | F: for<'a> FnMut(&'a mut [MaybeUninit]) -> io::Result<&'a mut [u8]>, 20 | S: FnMut() -> io::Result, 21 | { 22 | // Start by assuming the return value is <= 4KiB. If it is, we can do this in one syscall. 23 | const INITIAL_BUFFER_SIZE: usize = 4096; 24 | match get_value(&mut [MaybeUninit::::uninit(); INITIAL_BUFFER_SIZE]) { 25 | Ok(val) => return Ok(val.to_vec()), 26 | Err(e) if e.raw_os_error() != Some(crate::sys::ERANGE) => return Err(e), 27 | _ => {} 28 | } 29 | 30 | // If that fails, we ask for the size and try again with a buffer of the correct size. 31 | let mut vec: Vec = Vec::new(); 32 | loop { 33 | vec.reserve_exact(get_size()?); 34 | match get_value(vec.spare_capacity_mut()) { 35 | Ok(initialized) => { 36 | unsafe { 37 | let len = initialized.len(); 38 | assert_eq!( 39 | initialized.as_ptr(), 40 | vec.as_ptr(), 41 | "expected the same buffer" 42 | ); 43 | vec.set_len(len); 44 | } 45 | // Only shrink to fit if we've over-allocated by MORE than one byte. Unfortunately, 46 | // on FreeBSD, we have to over-allocate by one byte to determine if we've read all 47 | // the attributes. 48 | if vec.capacity() > vec.len() + 1 { 49 | vec.shrink_to_fit(); 50 | } 51 | return Ok(vec); 52 | } 53 | Err(e) if e.raw_os_error() != Some(crate::sys::ERANGE) => return Err(e), 54 | _ => {} // try again 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /.github/workflows/android.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - master 5 | pull_request: 6 | branches: 7 | - master 8 | 9 | name: android 10 | 11 | env: 12 | NDK_VERSION: "r26d" 13 | ANDROID_VERSION: "12.0.0" 14 | 15 | jobs: 16 | build: 17 | runs-on: ubuntu-latest 18 | timeout-minutes: 30 19 | steps: 20 | - uses: actions/checkout@v4 21 | - uses: dtolnay/rust-toolchain@stable 22 | with: 23 | targets: x86_64-linux-android 24 | - name: Setup Android Environment 25 | run: | 26 | NDK_URL="https://dl.google.com/android/repository/android-ndk-${NDK_VERSION}-linux.zip" 27 | NDK_DIR="${HOME}/android-ndk-${NDK_VERSION}" 28 | 29 | # Download and extract NDK 30 | echo "Downloading NDK from $NDK_URL" 31 | wget -q "$NDK_URL" -O android-ndk.zip 32 | unzip -q android-ndk.zip -d "$HOME" 33 | 34 | # Set ANDROID_NDK_ROOT environment variable 35 | echo "ANDROID_NDK_ROOT=$NDK_DIR" >> $GITHUB_ENV 36 | 37 | # Setup & start redroid container 38 | sudo apt-get install -y linux-modules-extra-$(uname -r) android-tools-adb 39 | sudo modprobe binder_linux devices=binder,hwbinder,vndbinder 40 | 41 | docker run -itd --rm --privileged \ 42 | --mount "type=bind,src=$(pwd),dst=/data/host$(pwd),ro" \ 43 | --mount "type=bind,src=/tmp,dst=/data/host/tmp,ro" \ 44 | --mount "type=bind,src=$(pwd)/.github/workflows/run-on-host.sh,dst=/system/xbin/run-on-host,ro" \ 45 | --name redroid-test \ 46 | -p 5555:5555 \ 47 | redroid/redroid:${ANDROID_VERSION}_64only-latest 48 | 49 | # Start ADB server and connect to our redroid container 50 | adb start-server 51 | timeout 60 bash -c 'until adb connect localhost:5555; do sleep 2; done' 52 | 53 | # Configure Android environment 54 | echo "CARGO_TARGET_X86_64_LINUX_ANDROID_RUNNER=adb -s localhost:5555 shell run-on-host" >> $GITHUB_ENV 55 | echo "CC_X86_64_linux_android=$NDK_DIR/toolchains/llvm/prebuilt/linux-x86_64/bin/x86_64-linux-android34-clang" >> $GITHUB_ENV 56 | echo "AR_x86_64_linux_android=$NDK_DIR/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-ar" >> $GITHUB_ENV 57 | echo "CARGO_TARGET_X86_64_LINUX_ANDROID_LINKER=$NDK_DIR/toolchains/llvm/prebuilt/linux-x86_64/bin/x86_64-linux-android34-clang" >> $GITHUB_ENV 58 | echo "CARGO_TARGET_AARCH64_LINUX_ANDROID_RUSTFLAGS=-C link-arg=-Wl,--as-needed" >> $GITHUB_ENV 59 | - run: cargo build --verbose --target x86_64-linux-android 60 | - run: cargo test --verbose --target x86_64-linux-android -- --nocapture 61 | - name: Stop the redroid container 62 | run: docker stop redroid-test || true 63 | -------------------------------------------------------------------------------- /src/sys/linux_macos.rs: -------------------------------------------------------------------------------- 1 | use std::ffi::{OsStr, OsString}; 2 | use std::io; 3 | use std::os::unix::ffi::OsStrExt; 4 | use std::os::unix::io::BorrowedFd; 5 | use std::path::Path; 6 | 7 | use rustix::fs as rfs; 8 | 9 | #[cfg(not(target_os = "macos"))] 10 | pub const ENOATTR: i32 = rustix::io::Errno::NODATA.raw_os_error(); 11 | 12 | #[cfg(target_os = "macos")] 13 | pub const ENOATTR: i32 = rustix::io::Errno::NOATTR.raw_os_error(); 14 | 15 | pub const ERANGE: i32 = rustix::io::Errno::RANGE.raw_os_error(); 16 | 17 | /// An iterator over a set of extended attributes names. 18 | #[derive(Default, Clone)] 19 | pub struct XAttrs { 20 | data: Box<[u8]>, 21 | offset: usize, 22 | } 23 | 24 | // Yes, I could avoid these allocations on linux/macos. However, if we ever want to be freebsd 25 | // compatible, we need to be able to prepend the namespace to the extended attribute names. 26 | // Furthermore, borrowing makes the API messy. 27 | impl Iterator for XAttrs { 28 | type Item = OsString; 29 | fn next(&mut self) -> Option { 30 | let data = &self.data[self.offset..]; 31 | if data.is_empty() { 32 | None 33 | } else { 34 | // always null terminated (unless empty). 35 | let end = data.iter().position(|&b| b == 0u8).unwrap(); 36 | self.offset += end + 1; 37 | Some(OsStr::from_bytes(&data[..end]).to_owned()) 38 | } 39 | } 40 | 41 | fn size_hint(&self) -> (usize, Option) { 42 | if self.data.len() == self.offset { 43 | (0, Some(0)) 44 | } else { 45 | (1, None) 46 | } 47 | } 48 | } 49 | 50 | /// A macro to abstract away some of the boilerplate when calling `allocate_loop` with rustix 51 | /// functions. Unfortunately, we can't write this as a helper function because I need to call 52 | /// generic rustix function with two different types. 53 | macro_rules! allocate_loop { 54 | (|$buf:ident| $($e:tt)*) => { 55 | crate::util::allocate_loop( 56 | |$buf| Ok($($e)*?.0), 57 | || { 58 | let $buf: &mut [u8] = &mut []; 59 | Ok($($e)*?) 60 | }, 61 | ) 62 | }; 63 | } 64 | 65 | pub fn get_fd(fd: BorrowedFd<'_>, name: &OsStr) -> io::Result> { 66 | allocate_loop!(|buf| rfs::fgetxattr(fd, name, buf)) 67 | } 68 | 69 | pub fn set_fd(fd: BorrowedFd<'_>, name: &OsStr, value: &[u8]) -> io::Result<()> { 70 | rfs::fsetxattr(fd, name, value, rfs::XattrFlags::empty())?; 71 | Ok(()) 72 | } 73 | 74 | pub fn remove_fd(fd: BorrowedFd<'_>, name: &OsStr) -> io::Result<()> { 75 | rfs::fremovexattr(fd, name)?; 76 | Ok(()) 77 | } 78 | 79 | pub fn list_fd(fd: BorrowedFd<'_>) -> io::Result { 80 | let vec = allocate_loop!(|buf| rfs::flistxattr(fd, buf))?; 81 | Ok(XAttrs { 82 | data: vec.into_boxed_slice(), 83 | offset: 0, 84 | }) 85 | } 86 | 87 | pub fn get_path(path: &Path, name: &OsStr, deref: bool) -> io::Result> { 88 | if deref { 89 | allocate_loop!(|buf| rfs::getxattr(path, name, buf)) 90 | } else { 91 | allocate_loop!(|buf| rfs::lgetxattr(path, name, buf)) 92 | } 93 | } 94 | 95 | pub fn set_path(path: &Path, name: &OsStr, value: &[u8], deref: bool) -> io::Result<()> { 96 | let setxattr_func = if deref { rfs::setxattr } else { rfs::lsetxattr }; 97 | setxattr_func(path, name, value, rfs::XattrFlags::empty())?; 98 | Ok(()) 99 | } 100 | 101 | pub fn remove_path(path: &Path, name: &OsStr, deref: bool) -> io::Result<()> { 102 | if deref { 103 | rfs::removexattr(path, name) 104 | } else { 105 | rfs::lremovexattr(path, name) 106 | }?; 107 | Ok(()) 108 | } 109 | 110 | pub fn list_path(path: &Path, deref: bool) -> io::Result { 111 | let vec = if deref { 112 | allocate_loop!(|buf| rfs::listxattr(path, buf)) 113 | } else { 114 | allocate_loop!(|buf| rfs::llistxattr(path, buf)) 115 | }?; 116 | Ok(XAttrs { 117 | data: vec.into_boxed_slice(), 118 | offset: 0, 119 | }) 120 | } 121 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::comparison_chain)] 2 | //! A pure-Rust library to manage extended attributes. 3 | //! 4 | //! It provides support for manipulating extended attributes 5 | //! (`xattrs`) on modern Unix filesystems. See the `attr(5)` 6 | //! manpage for more details. 7 | //! 8 | //! An extension trait [`FileExt`] is provided to directly work with 9 | //! standard `File` objects and file descriptors. 10 | //! 11 | //! If the path argument is a symlink, the get/set/list/remove functions 12 | //! operate on the symlink itself. To operate on the symlink target, use 13 | //! the _deref variant of these functions. 14 | //! 15 | //! ```rust 16 | //! let mut xattrs = xattr::list("/").unwrap().peekable(); 17 | //! 18 | //! if xattrs.peek().is_none() { 19 | //! println!("no xattr set on root"); 20 | //! return; 21 | //! } 22 | //! 23 | //! println!("Extended attributes:"); 24 | //! for attr in xattrs { 25 | //! println!(" - {:?}", attr); 26 | //! } 27 | //! ``` 28 | 29 | mod error; 30 | mod sys; 31 | mod util; 32 | 33 | use std::ffi::OsStr; 34 | use std::fs::File; 35 | use std::os::unix::io::{AsRawFd, BorrowedFd}; 36 | use std::path::Path; 37 | use std::{fmt, io}; 38 | 39 | pub use error::UnsupportedPlatformError; 40 | pub use sys::{XAttrs, SUPPORTED_PLATFORM}; 41 | 42 | /// Get an extended attribute for the specified file. 43 | pub fn get(path: P, name: N) -> io::Result>> 44 | where 45 | P: AsRef, 46 | N: AsRef, 47 | { 48 | util::extract_noattr(sys::get_path(path.as_ref(), name.as_ref(), false)) 49 | } 50 | 51 | /// Get an extended attribute for the specified file (dereference symlinks). 52 | pub fn get_deref(path: P, name: N) -> io::Result>> 53 | where 54 | P: AsRef, 55 | N: AsRef, 56 | { 57 | util::extract_noattr(sys::get_path(path.as_ref(), name.as_ref(), true)) 58 | } 59 | 60 | /// Set an extended attribute on the specified file. 61 | pub fn set(path: P, name: N, value: &[u8]) -> io::Result<()> 62 | where 63 | P: AsRef, 64 | N: AsRef, 65 | { 66 | sys::set_path(path.as_ref(), name.as_ref(), value, false) 67 | } 68 | 69 | /// Set an extended attribute on the specified file (dereference symlinks). 70 | pub fn set_deref(path: P, name: N, value: &[u8]) -> io::Result<()> 71 | where 72 | P: AsRef, 73 | N: AsRef, 74 | { 75 | sys::set_path(path.as_ref(), name.as_ref(), value, true) 76 | } 77 | 78 | /// Remove an extended attribute from the specified file. 79 | pub fn remove(path: P, name: N) -> io::Result<()> 80 | where 81 | P: AsRef, 82 | N: AsRef, 83 | { 84 | sys::remove_path(path.as_ref(), name.as_ref(), false) 85 | } 86 | 87 | /// Remove an extended attribute from the specified file (dereference symlinks). 88 | pub fn remove_deref(path: P, name: N) -> io::Result<()> 89 | where 90 | P: AsRef, 91 | N: AsRef, 92 | { 93 | sys::remove_path(path.as_ref(), name.as_ref(), true) 94 | } 95 | 96 | /// List extended attributes attached to the specified file. 97 | /// 98 | /// Note: this may not list *all* attributes. Speficially, it definitely won't list any trusted 99 | /// attributes unless you are root and it may not list system attributes. 100 | pub fn list

(path: P) -> io::Result 101 | where 102 | P: AsRef, 103 | { 104 | sys::list_path(path.as_ref(), false) 105 | } 106 | 107 | /// List extended attributes attached to the specified file (dereference symlinks). 108 | pub fn list_deref

(path: P) -> io::Result 109 | where 110 | P: AsRef, 111 | { 112 | sys::list_path(path.as_ref(), true) 113 | } 114 | 115 | /// Extension trait to manipulate extended attributes on `File`-like objects. 116 | pub trait FileExt: AsRawFd { 117 | /// Get an extended attribute for the specified file. 118 | fn get_xattr(&self, name: N) -> io::Result>> 119 | where 120 | N: AsRef, 121 | { 122 | // SAFETY: Implement I/O safety later. 123 | let fd = unsafe { BorrowedFd::borrow_raw(self.as_raw_fd()) }; 124 | util::extract_noattr(sys::get_fd(fd, name.as_ref())) 125 | } 126 | 127 | /// Set an extended attribute on the specified file. 128 | fn set_xattr(&self, name: N, value: &[u8]) -> io::Result<()> 129 | where 130 | N: AsRef, 131 | { 132 | let fd = unsafe { BorrowedFd::borrow_raw(self.as_raw_fd()) }; 133 | sys::set_fd(fd, name.as_ref(), value) 134 | } 135 | 136 | /// Remove an extended attribute from the specified file. 137 | fn remove_xattr(&self, name: N) -> io::Result<()> 138 | where 139 | N: AsRef, 140 | { 141 | let fd = unsafe { BorrowedFd::borrow_raw(self.as_raw_fd()) }; 142 | sys::remove_fd(fd, name.as_ref()) 143 | } 144 | 145 | /// List extended attributes attached to the specified file. 146 | /// 147 | /// Note: this may not list *all* attributes. Speficially, it definitely won't list any trusted 148 | /// attributes unless you are root and it may not list system attributes. 149 | fn list_xattr(&self) -> io::Result { 150 | let fd = unsafe { BorrowedFd::borrow_raw(self.as_raw_fd()) }; 151 | sys::list_fd(fd) 152 | } 153 | } 154 | 155 | impl FileExt for File {} 156 | 157 | impl fmt::Debug for XAttrs { 158 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 159 | // Waiting on https://github.com/rust-lang/rust/issues/117729 to stabilize... 160 | struct AsList<'a>(&'a XAttrs); 161 | impl<'a> fmt::Debug for AsList<'a> { 162 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 163 | f.debug_list().entries(self.0.clone()).finish() 164 | } 165 | } 166 | f.debug_tuple("XAttrs").field(&AsList(self)).finish() 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /tests/main.rs: -------------------------------------------------------------------------------- 1 | use std::collections::BTreeSet; 2 | use std::ffi::OsStr; 3 | use xattr::FileExt; 4 | 5 | use tempfile::{tempfile, tempfile_in, NamedTempFile}; 6 | 7 | #[test] 8 | #[cfg(any( 9 | target_os = "linux", 10 | target_os = "freebsd", 11 | target_os = "macos", 12 | target_os = "android" 13 | ))] 14 | fn test_fd() { 15 | use std::os::unix::ffi::OsStrExt; 16 | let tmp = tempfile().unwrap(); 17 | assert!(tmp.get_xattr("user.test").unwrap().is_none()); 18 | assert_eq!( 19 | tmp.list_xattr() 20 | .unwrap() 21 | .filter(|x| x.as_bytes().starts_with(b"user.")) 22 | .count(), 23 | 0 24 | ); 25 | 26 | tmp.set_xattr("user.test", b"my test").unwrap(); 27 | assert_eq!(tmp.get_xattr("user.test").unwrap().unwrap(), b"my test"); 28 | assert_eq!( 29 | tmp.list_xattr() 30 | .unwrap() 31 | .filter(|x| x.as_bytes().starts_with(b"user.")) 32 | .collect::>(), 33 | vec![OsStr::new("user.test")] 34 | ); 35 | 36 | tmp.remove_xattr("user.test").unwrap(); 37 | assert!(tmp.get_xattr("user.test").unwrap().is_none()); 38 | assert_eq!( 39 | tmp.list_xattr() 40 | .unwrap() 41 | .filter(|x| x.as_bytes().starts_with(b"user.")) 42 | .count(), 43 | 0 44 | ); 45 | } 46 | 47 | #[test] 48 | #[cfg(any( 49 | target_os = "linux", 50 | target_os = "freebsd", 51 | target_os = "macos", 52 | target_os = "android" 53 | ))] 54 | fn test_path() { 55 | use std::os::unix::ffi::OsStrExt; 56 | let tmp = NamedTempFile::new().unwrap(); 57 | assert!(xattr::get(tmp.path(), "user.test").unwrap().is_none()); 58 | assert_eq!( 59 | xattr::list(tmp.path()) 60 | .unwrap() 61 | .filter(|x| x.as_bytes().starts_with(b"user.")) 62 | .count(), 63 | 0 64 | ); 65 | 66 | xattr::set(tmp.path(), "user.test", b"my test").unwrap(); 67 | assert_eq!( 68 | xattr::get(tmp.path(), "user.test").unwrap().unwrap(), 69 | b"my test" 70 | ); 71 | assert_eq!( 72 | xattr::list(tmp.path()) 73 | .unwrap() 74 | .filter(|x| x.as_bytes().starts_with(b"user.")) 75 | .collect::>(), 76 | vec![OsStr::new("user.test")] 77 | ); 78 | 79 | xattr::remove(tmp.path(), "user.test").unwrap(); 80 | assert!(xattr::get(tmp.path(), "user.test").unwrap().is_none()); 81 | assert_eq!( 82 | xattr::list(tmp.path()) 83 | .unwrap() 84 | .filter(|x| x.as_bytes().starts_with(b"user.")) 85 | .count(), 86 | 0 87 | ); 88 | } 89 | 90 | #[test] 91 | #[cfg(any( 92 | target_os = "linux", 93 | target_os = "freebsd", 94 | target_os = "macos", 95 | target_os = "android" 96 | ))] 97 | fn test_missing() { 98 | assert!(xattr::get("/var/empty/does-not-exist", "user.test").is_err()); 99 | } 100 | 101 | #[test] 102 | #[cfg(any( 103 | target_os = "linux", 104 | target_os = "freebsd", 105 | target_os = "macos", 106 | target_os = "android" 107 | ))] 108 | fn test_debug() { 109 | use std::os::unix::ffi::OsStrExt; 110 | 111 | // Only works on "real" filesystems. 112 | let tmp = tempfile().unwrap(); 113 | 114 | tmp.set_xattr("user.myattr", b"value").unwrap(); 115 | let mut attrs = tmp.list_xattr().unwrap(); 116 | 117 | let debugstr = format!("{:?}", attrs); 118 | 119 | // Debug is idempotent 120 | assert_eq!(debugstr, format!("{:?}", attrs)); 121 | 122 | // It produces the right value. We can't just assert that it's equal to the expected value as 123 | // the system may have added additional attributes (selinux, etc.). See #68. 124 | assert!(debugstr.starts_with("XAttrs([")); 125 | assert!(debugstr.ends_with("])")); 126 | assert!(debugstr.contains(r#""user.myattr""#)); 127 | 128 | // It doesn't affect the underlying iterator. 129 | assert_eq!( 130 | "user.myattr", 131 | attrs.find(|x| x.as_bytes().starts_with(b"user.")).unwrap() 132 | ); 133 | 134 | // drain it. 135 | let _ = attrs.by_ref().count(); 136 | 137 | // An empty iterator produces the right value. 138 | assert_eq!(r#"XAttrs([])"#, format!("{:?}", attrs)); 139 | } 140 | 141 | #[test] 142 | #[cfg(any( 143 | target_os = "linux", 144 | target_os = "freebsd", 145 | target_os = "macos", 146 | target_os = "android" 147 | ))] 148 | fn test_multi() { 149 | use std::os::unix::ffi::OsStrExt; 150 | // Only works on "real" filesystems. 151 | let tmp = tempfile().unwrap(); 152 | let mut items: BTreeSet<_> = [ 153 | OsStr::new("user.test1"), 154 | OsStr::new("user.test2"), 155 | OsStr::new("user.test3"), 156 | ] 157 | .iter() 158 | .cloned() 159 | .collect(); 160 | 161 | for it in &items { 162 | tmp.set_xattr(it, b"value").unwrap(); 163 | } 164 | for it in tmp 165 | .list_xattr() 166 | .unwrap() 167 | .filter(|x| x.as_bytes().starts_with(b"user.")) 168 | { 169 | assert!(items.remove(&*it)); 170 | } 171 | assert!(items.is_empty()); 172 | } 173 | 174 | // This test is skipped on android because the /data/tmp filesystem doesn't support >4kib of 175 | // extended attributes. 176 | #[test] 177 | #[cfg(any(target_os = "linux", target_os = "freebsd", target_os = "macos"))] 178 | fn test_large() { 179 | use std::ffi::OsString; 180 | use std::os::unix::ffi::OsStrExt; 181 | // On Linux, this only works in tmpfs! 182 | let tmp = if cfg!(target_os = "linux") { 183 | tempfile_in("/dev/shm") 184 | } else { 185 | tempfile() 186 | } 187 | .unwrap(); 188 | let mut items: BTreeSet = (0..100) 189 | .map(|i| format!("user.test{i:0100}").into()) 190 | .collect(); 191 | 192 | for it in &items { 193 | tmp.set_xattr(it, b"value").unwrap(); 194 | } 195 | for it in tmp 196 | .list_xattr() 197 | .unwrap() 198 | .filter(|x| x.as_bytes().starts_with(b"user.")) 199 | { 200 | assert!(items.remove(&*it)); 201 | } 202 | assert!(items.is_empty()); 203 | } 204 | 205 | // Tests the deref API variants - regression test for 206 | // https://github.com/Stebalien/xattr/issues/57 207 | #[test] 208 | #[cfg(any( 209 | target_os = "linux", 210 | target_os = "freebsd", 211 | target_os = "macos", 212 | target_os = "android" 213 | ))] 214 | fn test_path_deref() { 215 | use std::os::unix::ffi::OsStrExt; 216 | // Only works on "real" filesystems. 217 | let tmp = NamedTempFile::new().unwrap(); 218 | assert!(xattr::get_deref(tmp.path(), "user.test").unwrap().is_none()); 219 | assert_eq!( 220 | xattr::list_deref(tmp.path()) 221 | .unwrap() 222 | .filter(|x| x.as_bytes().starts_with(b"user.")) 223 | .count(), 224 | 0 225 | ); 226 | 227 | xattr::set_deref(tmp.path(), "user.test", b"my test").unwrap(); 228 | assert_eq!( 229 | xattr::get_deref(tmp.path(), "user.test").unwrap().unwrap(), 230 | b"my test" 231 | ); 232 | assert_eq!( 233 | xattr::list_deref(tmp.path()) 234 | .unwrap() 235 | .filter(|x| x.as_bytes().starts_with(b"user.")) 236 | .collect::>(), 237 | vec![OsStr::new("user.test")] 238 | ); 239 | 240 | xattr::remove_deref(tmp.path(), "user.test").unwrap(); 241 | assert!(xattr::get_deref(tmp.path(), "user.test").unwrap().is_none()); 242 | assert_eq!( 243 | xattr::list_deref(tmp.path()) 244 | .unwrap() 245 | .filter(|x| x.as_bytes().starts_with(b"user.")) 246 | .count(), 247 | 0 248 | ); 249 | } 250 | -------------------------------------------------------------------------------- /src/sys/bsd.rs: -------------------------------------------------------------------------------- 1 | //! FreeBSD and NetBSD xattr support. 2 | 3 | use libc::{c_int, c_void, size_t, EPERM}; 4 | use std::ffi::{CString, OsStr, OsString}; 5 | use std::mem::MaybeUninit; 6 | use std::os::unix::ffi::{OsStrExt, OsStringExt}; 7 | use std::os::unix::io::{AsRawFd, BorrowedFd}; 8 | use std::path::Path; 9 | use std::ptr; 10 | use std::{io, slice}; 11 | 12 | use libc::{ 13 | extattr_delete_fd, extattr_delete_file, extattr_delete_link, extattr_get_fd, extattr_get_file, 14 | extattr_get_link, extattr_list_fd, extattr_list_file, extattr_list_link, extattr_set_fd, 15 | extattr_set_file, extattr_set_link, EXTATTR_NAMESPACE_SYSTEM, EXTATTR_NAMESPACE_USER, 16 | }; 17 | 18 | pub const ENOATTR: i32 = libc::ENOATTR; 19 | pub const ERANGE: i32 = libc::ERANGE; 20 | 21 | const EXTATTR_NAMESPACE_USER_STRING: &str = "user"; 22 | const EXTATTR_NAMESPACE_SYSTEM_STRING: &str = "system"; 23 | const EXTATTR_NAMESPACE_NAMES: [&str; 3] = [ 24 | "empty", 25 | EXTATTR_NAMESPACE_USER_STRING, 26 | EXTATTR_NAMESPACE_SYSTEM_STRING, 27 | ]; 28 | 29 | fn path_to_c(path: &Path) -> io::Result { 30 | match CString::new(path.as_os_str().as_bytes()) { 31 | Ok(name) => Ok(name), 32 | Err(_) => Err(io::Error::new(io::ErrorKind::NotFound, "file not found")), 33 | } 34 | } 35 | 36 | #[inline] 37 | fn cvt(res: libc::ssize_t) -> io::Result { 38 | if res < 0 { 39 | Err(io::Error::last_os_error()) 40 | } else { 41 | Ok(res as usize) 42 | } 43 | } 44 | 45 | #[inline] 46 | fn slice_parts(buf: &mut [MaybeUninit]) -> (*mut c_void, size_t) { 47 | if buf.is_empty() { 48 | (ptr::null_mut(), 0) 49 | } else { 50 | (buf.as_mut_ptr().cast(), buf.len() as size_t) 51 | } 52 | } 53 | 54 | fn allocate_loop libc::ssize_t>(f: F) -> io::Result> { 55 | crate::util::allocate_loop( 56 | |buf| unsafe { 57 | let (ptr, len) = slice_parts(buf); 58 | let new_len = cvt(f(ptr, len))?; 59 | if new_len < len { 60 | Ok(slice::from_raw_parts_mut(ptr.cast(), new_len)) 61 | } else { 62 | // If the length of the value isn't strictly smaller than the length of the value 63 | // read, there may be more to read. Fake an ERANGE error so we can try again with a 64 | // bigger buffer. 65 | Err(io::Error::from_raw_os_error(crate::sys::ERANGE)) 66 | } 67 | }, 68 | // Estimate size + 1 because, on freebsd, the only way to tell if we've read the entire 69 | // value is read a value smaller than the buffer we passed. 70 | || Ok(cvt(f(ptr::null_mut(), 0))? + 1), 71 | ) 72 | } 73 | 74 | /// An iterator over a set of extended attributes names. 75 | #[derive(Default, Clone)] 76 | pub struct XAttrs { 77 | user_attrs: Box<[u8]>, 78 | system_attrs: Box<[u8]>, 79 | offset: usize, 80 | } 81 | 82 | impl Iterator for XAttrs { 83 | type Item = OsString; 84 | fn next(&mut self) -> Option { 85 | if self.user_attrs.is_empty() && self.system_attrs.is_empty() { 86 | return None; 87 | } 88 | 89 | if self.offset == self.user_attrs.len() + self.system_attrs.len() { 90 | return None; 91 | } 92 | 93 | let data = if self.offset < self.system_attrs.len() { 94 | &self.system_attrs[self.offset..] 95 | } else { 96 | &self.user_attrs[self.offset - self.system_attrs.len()..] 97 | }; 98 | 99 | let siz = data[0] as usize; 100 | 101 | self.offset += siz + 1; 102 | if self.offset < self.system_attrs.len() { 103 | Some(prefix_namespace( 104 | OsStr::from_bytes(&data[1..siz + 1]), 105 | EXTATTR_NAMESPACE_SYSTEM, 106 | )) 107 | } else { 108 | Some(prefix_namespace( 109 | OsStr::from_bytes(&data[1..siz + 1]), 110 | EXTATTR_NAMESPACE_USER, 111 | )) 112 | } 113 | } 114 | 115 | fn size_hint(&self) -> (usize, Option) { 116 | if self.user_attrs.len() + self.system_attrs.len() == self.offset { 117 | (0, Some(0)) 118 | } else { 119 | (1, None) 120 | } 121 | } 122 | } 123 | 124 | // This could use libc::extattr_string_to_namespace, but it's awkward because 125 | // that requires nul-terminated strings, which Rust's std is loathe to provide. 126 | fn name_to_ns(name: &OsStr) -> io::Result<(c_int, CString)> { 127 | let mut groups = name.as_bytes().splitn(2, |&b| b == b'.').take(2); 128 | let nsname = match groups.next() { 129 | Some(s) => s, 130 | None => { 131 | return Err(io::Error::new( 132 | io::ErrorKind::InvalidInput, 133 | "couldn't find namespace", 134 | )) 135 | } 136 | }; 137 | 138 | let propname = match groups.next() { 139 | Some(s) => s, 140 | None => { 141 | return Err(io::Error::new( 142 | io::ErrorKind::InvalidInput, 143 | "couldn't find attribute", 144 | )) 145 | } 146 | }; 147 | 148 | let ns_int = match EXTATTR_NAMESPACE_NAMES 149 | .iter() 150 | .position(|&s| s.as_bytes() == nsname) 151 | { 152 | Some(i) => i, 153 | None => { 154 | return Err(io::Error::new( 155 | io::ErrorKind::InvalidInput, 156 | "no matching namespace", 157 | )) 158 | } 159 | }; 160 | 161 | Ok((ns_int as c_int, CString::new(propname)?)) 162 | } 163 | 164 | fn prefix_namespace(attr: &OsStr, ns: c_int) -> OsString { 165 | let nsname = EXTATTR_NAMESPACE_NAMES[ns as usize].as_bytes(); 166 | let attr = attr.as_bytes(); 167 | let mut v = Vec::with_capacity(nsname.len() + attr.len() + 1); 168 | v.extend_from_slice(nsname); 169 | v.extend_from_slice(b"."); 170 | v.extend_from_slice(attr); 171 | OsString::from_vec(v) 172 | } 173 | 174 | pub fn get_fd(fd: BorrowedFd<'_>, name: &OsStr) -> io::Result> { 175 | let (ns, name) = name_to_ns(name)?; 176 | unsafe { allocate_loop(|ptr, len| extattr_get_fd(fd.as_raw_fd(), ns, name.as_ptr(), ptr, len)) } 177 | } 178 | 179 | pub fn set_fd(fd: BorrowedFd<'_>, name: &OsStr, value: &[u8]) -> io::Result<()> { 180 | let (ns, name) = name_to_ns(name)?; 181 | let ret = unsafe { 182 | extattr_set_fd( 183 | fd.as_raw_fd(), 184 | ns, 185 | name.as_ptr(), 186 | value.as_ptr() as *const c_void, 187 | value.len() as size_t, 188 | ) 189 | }; 190 | if ret == -1 { 191 | Err(io::Error::last_os_error()) 192 | } else { 193 | Ok(()) 194 | } 195 | } 196 | 197 | pub fn remove_fd(fd: BorrowedFd<'_>, name: &OsStr) -> io::Result<()> { 198 | let (ns, name) = name_to_ns(name)?; 199 | let ret = unsafe { extattr_delete_fd(fd.as_raw_fd(), ns, name.as_ptr()) }; 200 | if ret != 0 { 201 | Err(io::Error::last_os_error()) 202 | } else { 203 | Ok(()) 204 | } 205 | } 206 | 207 | pub fn list_fd(fd: BorrowedFd<'_>) -> io::Result { 208 | let sysvec = unsafe { 209 | let res = allocate_loop(|ptr, len| { 210 | extattr_list_fd(fd.as_raw_fd(), EXTATTR_NAMESPACE_SYSTEM, ptr, len) 211 | }); 212 | // On FreeBSD, system attributes require root privileges to view. However, 213 | // to mimic the behavior of listxattr in linux and osx, we need to query 214 | // them anyway and return empty results if we get EPERM 215 | match res { 216 | Ok(v) => v, 217 | Err(err) => { 218 | if err.raw_os_error() == Some(EPERM) { 219 | Vec::new() 220 | } else { 221 | return Err(err); 222 | } 223 | } 224 | } 225 | }; 226 | 227 | let uservec = unsafe { 228 | allocate_loop(|ptr, len| extattr_list_fd(fd.as_raw_fd(), EXTATTR_NAMESPACE_USER, ptr, len))? 229 | }; 230 | 231 | Ok(XAttrs { 232 | system_attrs: sysvec.into_boxed_slice(), 233 | user_attrs: uservec.into_boxed_slice(), 234 | offset: 0, 235 | }) 236 | } 237 | 238 | pub fn get_path(path: &Path, name: &OsStr, deref: bool) -> io::Result> { 239 | let (ns, name) = name_to_ns(name)?; 240 | let path = path_to_c(path)?; 241 | let extattr_get_func = if deref { 242 | extattr_get_file 243 | } else { 244 | extattr_get_link 245 | }; 246 | unsafe { 247 | allocate_loop(|ptr, len| extattr_get_func(path.as_ptr(), ns, name.as_ptr(), ptr, len)) 248 | } 249 | } 250 | 251 | pub fn set_path(path: &Path, name: &OsStr, value: &[u8], deref: bool) -> io::Result<()> { 252 | let (ns, name) = name_to_ns(name)?; 253 | let path = path_to_c(path)?; 254 | let extattr_set_func = if deref { 255 | extattr_set_file 256 | } else { 257 | extattr_set_link 258 | }; 259 | let ret = unsafe { 260 | extattr_set_func( 261 | path.as_ptr(), 262 | ns, 263 | name.as_ptr(), 264 | value.as_ptr() as *const c_void, 265 | value.len() as size_t, 266 | ) 267 | }; 268 | if ret == -1 { 269 | Err(io::Error::last_os_error()) 270 | } else { 271 | Ok(()) 272 | } 273 | } 274 | 275 | pub fn remove_path(path: &Path, name: &OsStr, deref: bool) -> io::Result<()> { 276 | let (ns, name) = name_to_ns(name)?; 277 | let path = path_to_c(path)?; 278 | let extattr_delete_func = if deref { 279 | extattr_delete_file 280 | } else { 281 | extattr_delete_link 282 | }; 283 | let ret = unsafe { extattr_delete_func(path.as_ptr(), ns, name.as_ptr()) }; 284 | if ret != 0 { 285 | Err(io::Error::last_os_error()) 286 | } else { 287 | Ok(()) 288 | } 289 | } 290 | 291 | pub fn list_path(path: &Path, deref: bool) -> io::Result { 292 | let path = path_to_c(path)?; 293 | let extattr_list_func = if deref { 294 | extattr_list_file 295 | } else { 296 | extattr_list_link 297 | }; 298 | let sysvec = unsafe { 299 | let res = allocate_loop(|ptr, len| { 300 | extattr_list_func(path.as_ptr(), EXTATTR_NAMESPACE_SYSTEM, ptr, len) 301 | }); 302 | // On FreeBSD, system attributes require root privileges to view. However, 303 | // to mimic the behavior of listxattr in linux and osx, we need to query 304 | // them anyway and return empty results if we get EPERM 305 | match res { 306 | Ok(v) => v, 307 | Err(err) => { 308 | if err.raw_os_error() == Some(EPERM) { 309 | Vec::new() 310 | } else { 311 | return Err(err); 312 | } 313 | } 314 | } 315 | }; 316 | 317 | let uservec = unsafe { 318 | allocate_loop(|ptr, len| { 319 | extattr_list_func(path.as_ptr(), EXTATTR_NAMESPACE_USER, ptr, len) 320 | })? 321 | }; 322 | 323 | Ok(XAttrs { 324 | system_attrs: sysvec.into_boxed_slice(), 325 | user_attrs: uservec.into_boxed_slice(), 326 | offset: 0, 327 | }) 328 | } 329 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | --------------------------------------------------------------------------------