From 4ad9067c0b6242cef27180d5ac4f431bd9cfa690 Mon Sep 17 00:00:00 2001 From: Aleksa Sarai Date: Tue, 10 Sep 2024 18:41:25 +1000 Subject: [PATCH] tests: add integration tests for capi from Rust This lets us finally get test coverage of our C API directly, as well as coverage information.The only thing to note is that we can't test resolve_partial because that is an internal resolver thing that isn't exposed to the C API. One thing to note is that the pathrs_reopen() tests are testing the ~O_CLOEXEC path right now even though that doesn't match the Rust API. We probably want to add fd flag tests to pathrs_reopen(), which will require correcting this issue. Signed-off-by: Aleksa Sarai --- Makefile | 6 +- src/capi/procfs.rs | 9 + src/tests/capi/handle.rs | 101 ++++++++++ src/tests/capi/mod.rs | 31 +++ src/tests/capi/procfs.rs | 104 ++++++++++ src/tests/capi/root.rs | 382 +++++++++++++++++++++++++++++++++++++ src/tests/capi/utils.rs | 164 ++++++++++++++++ src/tests/mod.rs | 2 + src/tests/test_procfs.rs | 41 +++- src/tests/test_resolve.rs | 57 +++++- src/tests/test_root_ops.rs | 12 ++ 11 files changed, 894 insertions(+), 15 deletions(-) create mode 100644 src/tests/capi/handle.rs create mode 100644 src/tests/capi/mod.rs create mode 100644 src/tests/capi/procfs.rs create mode 100644 src/tests/capi/root.rs create mode 100644 src/tests/capi/utils.rs diff --git a/Makefile b/Makefile index 7add5b32..e0a35c96 100644 --- a/Makefile +++ b/Makefile @@ -59,11 +59,11 @@ lint-rust: .PHONY: test-rust-doctest test-rust-doctest: - $(CARGO_NIGHTLY) llvm-cov --no-report --branch --doc + $(CARGO_NIGHTLY) llvm-cov --no-report --branch --all-features --doc .PHONY: test-rust-unpriv test-rust-unpriv: - $(CARGO_NIGHTLY) llvm-cov --no-report --branch nextest --no-fail-fast + $(CARGO_NIGHTLY) llvm-cov --no-report --branch --features capi nextest --no-fail-fast .PHONY: test-rust-root test-rust-root: @@ -73,7 +73,7 @@ test-rust-root: # support cfg(feature=...) for target runner configs. # See . CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_RUNNER='sudo -E' \ - $(CARGO_NIGHTLY) llvm-cov --no-report --branch --features _test_as_root nextest --no-fail-fast + $(CARGO_NIGHTLY) llvm-cov --no-report --branch --features capi,_test_as_root nextest --no-fail-fast .PHONY: test-rust test-rust: diff --git a/src/capi/procfs.rs b/src/capi/procfs.rs index 6de80c03..c581a8dc 100644 --- a/src/capi/procfs.rs +++ b/src/capi/procfs.rs @@ -61,6 +61,15 @@ impl From for ProcfsBase { } } +impl From for CProcfsBase { + fn from(base: ProcfsBase) -> Self { + match base { + ProcfsBase::ProcSelf => CProcfsBase::PATHRS_PROC_SELF, + ProcfsBase::ProcThreadSelf => CProcfsBase::PATHRS_PROC_THREAD_SELF, + } + } +} + /// Safely open a path inside a `/proc` handle. /// /// Any bind-mounts or other over-mounts will (depending on what kernel features diff --git a/src/tests/capi/handle.rs b/src/tests/capi/handle.rs new file mode 100644 index 00000000..b5c5220c --- /dev/null +++ b/src/tests/capi/handle.rs @@ -0,0 +1,101 @@ +/* + * libpathrs: safe path resolution on Linux + * Copyright (C) 2019-2024 Aleksa Sarai + * Copyright (C) 2019-2024 SUSE LLC + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +use crate::{ + capi, + flags::OpenFlags, + tests::{ + capi::utils::{self as capi_utils, CapiError}, + traits::HandleImpl, + }, +}; + +use std::{ + fs::File, + os::unix::io::{AsFd, BorrowedFd, OwnedFd}, +}; + +#[derive(Debug)] +pub struct CapiHandle { + inner: OwnedFd, +} + +impl CapiHandle { + fn from_fd_unchecked>(fd: Fd) -> Self { + Self { inner: fd.into() } + } + + fn try_clone(&self) -> Result { + Ok(Self::from_fd_unchecked(self.inner.try_clone()?)) + } + + fn reopen>(&self, flags: F) -> Result { + let fd = self.inner.as_fd(); + let flags = flags.into(); + + capi_utils::call_capi_fd(|| capi::core::pathrs_reopen(fd.into(), flags.bits())) + .map(File::from) + } +} + +impl AsFd for CapiHandle { + fn as_fd(&self) -> BorrowedFd<'_> { + self.inner.as_fd() + } +} + +impl From for OwnedFd { + fn from(handle: CapiHandle) -> Self { + handle.inner + } +} + +impl HandleImpl for CapiHandle { + type Cloned = CapiHandle; + type Error = CapiError; + + fn from_fd_unchecked>(fd: Fd) -> Self::Cloned { + Self::Cloned::from_fd_unchecked(fd) + } + + fn try_clone(&self) -> Result { + self.try_clone().map_err(From::from) + } + + fn reopen>(&self, flags: F) -> Result { + self.reopen(flags) + } +} + +impl HandleImpl for &CapiHandle { + type Cloned = CapiHandle; + type Error = CapiError; + + fn from_fd_unchecked>(fd: Fd) -> Self::Cloned { + Self::Cloned::from_fd_unchecked(fd) + } + + fn try_clone(&self) -> Result { + CapiHandle::try_clone(self).map_err(From::from) + } + + fn reopen>(&self, flags: F) -> Result { + CapiHandle::reopen(self, flags) + } +} diff --git a/src/tests/capi/mod.rs b/src/tests/capi/mod.rs new file mode 100644 index 00000000..0f8968d9 --- /dev/null +++ b/src/tests/capi/mod.rs @@ -0,0 +1,31 @@ +/* + * libpathrs: safe path resolution on Linux + * Copyright (C) 2019-2024 Aleksa Sarai + * Copyright (C) 2019-2024 SUSE LLC + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +#![allow(unsafe_code)] + +mod utils; + +mod root; +pub(in crate::tests) use root::*; + +mod handle; +pub(in crate::tests) use handle::*; + +mod procfs; +pub(in crate::tests) use procfs::*; diff --git a/src/tests/capi/procfs.rs b/src/tests/capi/procfs.rs new file mode 100644 index 00000000..f23ece45 --- /dev/null +++ b/src/tests/capi/procfs.rs @@ -0,0 +1,104 @@ +/* + * libpathrs: safe path resolution on Linux + * Copyright (C) 2019-2024 Aleksa Sarai + * Copyright (C) 2019-2024 SUSE LLC + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +use crate::{ + capi::{self, procfs::CProcfsBase}, + flags::OpenFlags, + procfs::ProcfsBase, + tests::{ + capi::utils::{self as capi_utils, CapiError}, + traits::ProcfsHandleImpl, + }, +}; + +use std::{ + fs::File, + path::{Path, PathBuf}, +}; + +// NOTE: The C API only lets us access the global PROCFS_HANDLE reference. +#[derive(Debug)] +pub struct CapiProcfsHandle; + +impl CapiProcfsHandle { + fn open_follow, F: Into>( + &self, + base: ProcfsBase, + subpath: P, + oflags: F, + ) -> Result { + let base: CProcfsBase = base.into(); + let subpath = capi_utils::path_to_cstring(subpath); + let oflags = oflags.into(); + + capi_utils::call_capi_fd(|| unsafe { + capi::procfs::pathrs_proc_open(base, subpath.as_ptr(), oflags.bits()) + }) + .map(File::from) + } + + fn open, F: Into>( + &self, + base: ProcfsBase, + subpath: P, + oflags: F, + ) -> Result { + // The C API exposes ProcfsHandle::open using O_NOFOLLOW. + self.open_follow(base, subpath, oflags.into() | OpenFlags::O_NOFOLLOW) + } + + fn readlink>(&self, base: ProcfsBase, subpath: P) -> Result { + let base: CProcfsBase = base.into(); + let subpath = capi_utils::path_to_cstring(subpath); + + capi_utils::call_capi_readlink(|linkbuf, linkbuf_size| unsafe { + capi::procfs::pathrs_proc_readlink(base, subpath.as_ptr(), linkbuf, linkbuf_size) + }) + } +} + +impl ProcfsHandleImpl for CapiProcfsHandle { + type Error = CapiError; + + fn open_follow, F: Into>( + &self, + base: ProcfsBase, + subpath: P, + flags: F, + ) -> Result { + self.open_follow(base, subpath, flags) + } + + fn open, F: Into>( + &self, + base: ProcfsBase, + subpath: P, + flags: F, + ) -> Result { + self.open(base, subpath, flags) + } + + fn readlink>( + &self, + base: ProcfsBase, + subpath: P, + ) -> Result { + self.readlink(base, subpath) + } +} diff --git a/src/tests/capi/root.rs b/src/tests/capi/root.rs new file mode 100644 index 00000000..a9fa02ec --- /dev/null +++ b/src/tests/capi/root.rs @@ -0,0 +1,382 @@ +/* + * libpathrs: safe path resolution on Linux + * Copyright (C) 2019-2024 Aleksa Sarai + * Copyright (C) 2019-2024 SUSE LLC + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +use crate::{ + capi, + flags::{OpenFlags, RenameFlags}, + tests::{ + capi::{ + utils::{self as capi_utils, CapiError}, + CapiHandle, + }, + traits::{HandleImpl, RootImpl}, + }, + InodeType, Resolver, +}; + +use std::{ + fs::Permissions, + os::unix::{ + fs::PermissionsExt, + io::{AsFd, BorrowedFd, OwnedFd}, + }, + path::{Path, PathBuf}, +}; + +#[derive(Debug)] +pub(in crate::tests) struct CapiRoot { + inner: OwnedFd, +} + +impl CapiRoot { + pub(in crate::tests) fn open>(path: P) -> Result { + let path = capi_utils::path_to_cstring(path); + + capi_utils::call_capi_fd(|| unsafe { capi::core::pathrs_root_open(path.as_ptr()) }) + .map(Self::from_fd_unchecked) + } + + pub(in crate::tests) fn from_fd_unchecked>(fd: Fd) -> Self { + Self { inner: fd.into() } + } + + fn try_clone(&self) -> Result { + Ok(Self::from_fd_unchecked(self.inner.try_clone()?)) + } + + fn resolve>(&self, path: P) -> Result { + let root_fd = self.inner.as_fd(); + let path = capi_utils::path_to_cstring(path); + + capi_utils::call_capi_fd(|| unsafe { + capi::core::pathrs_resolve(root_fd.into(), path.as_ptr()) + }) + .map(CapiHandle::from_fd_unchecked) + } + + fn resolve_nofollow>(&self, path: P) -> Result { + let root_fd = self.inner.as_fd(); + let path = capi_utils::path_to_cstring(path); + + capi_utils::call_capi_fd(|| unsafe { + capi::core::pathrs_resolve_nofollow(root_fd.into(), path.as_ptr()) + }) + .map(CapiHandle::from_fd_unchecked) + } + + fn readlink>(&self, path: P) -> Result { + let root_fd = self.inner.as_fd(); + let path = capi_utils::path_to_cstring(path); + + capi_utils::call_capi_readlink(|linkbuf, linkbuf_size| unsafe { + capi::core::pathrs_readlink(root_fd.into(), path.as_ptr(), linkbuf, linkbuf_size) + }) + } + + fn create>(&self, path: P, inode_type: &InodeType) -> Result<(), CapiError> { + let root_fd = self.inner.as_fd(); + let path = capi_utils::path_to_cstring(path); + + capi_utils::call_capi_zst(|| unsafe { + match inode_type { + InodeType::File(perm) => capi::core::pathrs_mknod( + root_fd.into(), + path.as_ptr(), + libc::S_IFREG | perm.mode(), + 0, + ), + InodeType::Directory(perm) => { + capi::core::pathrs_mkdir(root_fd.into(), path.as_ptr(), perm.mode()) + } + InodeType::Symlink(target) => { + let target = capi_utils::path_to_cstring(target); + capi::core::pathrs_symlink(root_fd.into(), path.as_ptr(), target.as_ptr()) + } + InodeType::Hardlink(target) => { + let target = capi_utils::path_to_cstring(target); + capi::core::pathrs_hardlink(root_fd.into(), path.as_ptr(), target.as_ptr()) + } + InodeType::Fifo(perm) => capi::core::pathrs_mknod( + root_fd.into(), + path.as_ptr(), + libc::S_IFIFO | perm.mode(), + 0, + ), + InodeType::CharacterDevice(perm, dev) => capi::core::pathrs_mknod( + root_fd.into(), + path.as_ptr(), + libc::S_IFCHR | perm.mode(), + *dev, + ), + InodeType::BlockDevice(perm, dev) => capi::core::pathrs_mknod( + root_fd.into(), + path.as_ptr(), + libc::S_IFBLK | perm.mode(), + *dev, + ), + } + }) + } + + fn create_file>( + &self, + path: P, + flags: OpenFlags, + perm: &Permissions, + ) -> Result { + let root_fd = self.inner.as_fd(); + let path = capi_utils::path_to_cstring(path); + + capi_utils::call_capi_fd(|| unsafe { + capi::core::pathrs_creat(root_fd.into(), path.as_ptr(), flags.bits(), perm.mode()) + }) + .map(CapiHandle::from_fd_unchecked) + } + + fn mkdir_all>( + &self, + path: P, + perm: &Permissions, + ) -> Result { + let root_fd = self.inner.as_fd(); + let path = capi_utils::path_to_cstring(path); + + capi_utils::call_capi_fd(|| unsafe { + capi::core::pathrs_mkdir_all(root_fd.into(), path.as_ptr(), perm.mode()) + }) + .map(CapiHandle::from_fd_unchecked) + } + + fn remove_dir>(&self, path: P) -> Result<(), CapiError> { + let root_fd = self.inner.as_fd(); + let path = capi_utils::path_to_cstring(path); + + capi_utils::call_capi_zst(|| unsafe { + capi::core::pathrs_rmdir(root_fd.into(), path.as_ptr()) + }) + } + + fn remove_file>(&self, path: P) -> Result<(), CapiError> { + let root_fd = self.inner.as_fd(); + let path = capi_utils::path_to_cstring(path); + + capi_utils::call_capi_zst(|| unsafe { + capi::core::pathrs_unlink(root_fd.into(), path.as_ptr()) + }) + } + + fn remove_all>(&self, path: P) -> Result<(), CapiError> { + let root_fd = self.inner.as_fd(); + let path = capi_utils::path_to_cstring(path); + + capi_utils::call_capi_zst(|| unsafe { + capi::core::pathrs_remove_all(root_fd.into(), path.as_ptr()) + }) + } + + fn rename>( + &self, + source: P, + destination: P, + rflags: RenameFlags, + ) -> Result<(), CapiError> { + let root_fd = self.inner.as_fd(); + let source = capi_utils::path_to_cstring(source); + let destination = capi_utils::path_to_cstring(destination); + + capi_utils::call_capi_zst(|| unsafe { + capi::core::pathrs_rename( + root_fd.into(), + source.as_ptr(), + destination.as_ptr(), + rflags.bits(), + ) + }) + } +} + +impl AsFd for CapiRoot { + fn as_fd(&self) -> BorrowedFd<'_> { + self.inner.as_fd() + } +} + +impl From for OwnedFd { + fn from(root: CapiRoot) -> Self { + root.inner + } +} + +impl RootImpl for CapiRoot { + type Cloned = CapiRoot; + type Handle = CapiHandle; + // NOTE: We can't use anyhow::Error here. + // + type Error = CapiError; + + fn from_fd_unchecked>(fd: Fd, resolver: Resolver) -> Self::Cloned { + assert_eq!( + resolver, + Resolver::default(), + "cannot use non-default Resolver with capi" + ); + Self::Cloned::from_fd_unchecked(fd) + } + + fn resolver(&self) -> Resolver { + Resolver::default() + } + + fn try_clone(&self) -> Result { + self.try_clone() + } + + fn resolve>(&self, path: P) -> Result { + self.resolve(path) + } + + fn resolve_nofollow>(&self, path: P) -> Result { + self.resolve_nofollow(path) + } + + fn readlink>(&self, path: P) -> Result { + self.readlink(path) + } + + fn create>(&self, path: P, inode_type: &InodeType) -> Result<(), Self::Error> { + self.create(path, inode_type) + } + + fn create_file>( + &self, + path: P, + flags: OpenFlags, + perm: &Permissions, + ) -> Result { + self.create_file(path, flags, perm) + } + + fn mkdir_all>( + &self, + path: P, + perm: &Permissions, + ) -> Result { + self.mkdir_all(path, perm) + } + + fn remove_dir>(&self, path: P) -> Result<(), Self::Error> { + self.remove_dir(path) + } + + fn remove_file>(&self, path: P) -> Result<(), Self::Error> { + self.remove_file(path) + } + + fn remove_all>(&self, path: P) -> Result<(), Self::Error> { + self.remove_all(path) + } + + fn rename>( + &self, + source: P, + destination: P, + rflags: RenameFlags, + ) -> Result<(), Self::Error> { + self.rename(source, destination, rflags) + } +} + +impl RootImpl for &CapiRoot { + type Cloned = CapiRoot; + type Handle = CapiHandle; + // NOTE: We can't use anyhow::Error here. + // + type Error = CapiError; + + fn from_fd_unchecked>(fd: Fd, resolver: Resolver) -> Self::Cloned { + assert_eq!( + resolver, + Resolver::default(), + "cannot use non-default Resolver with capi" + ); + Self::Cloned::from_fd_unchecked(fd) + } + + fn resolver(&self) -> Resolver { + Resolver::default() + } + + fn try_clone(&self) -> Result { + CapiRoot::try_clone(self) + } + + fn resolve>(&self, path: P) -> Result { + CapiRoot::resolve(self, path) + } + + fn resolve_nofollow>(&self, path: P) -> Result { + CapiRoot::resolve_nofollow(self, path) + } + + fn readlink>(&self, path: P) -> Result { + CapiRoot::readlink(self, path) + } + + fn create>(&self, path: P, inode_type: &InodeType) -> Result<(), Self::Error> { + CapiRoot::create(self, path, inode_type) + } + + fn create_file>( + &self, + path: P, + flags: OpenFlags, + perm: &Permissions, + ) -> Result { + CapiRoot::create_file(self, path, flags, perm) + } + + fn mkdir_all>( + &self, + path: P, + perm: &Permissions, + ) -> Result { + CapiRoot::mkdir_all(self, path, perm) + } + + fn remove_dir>(&self, path: P) -> Result<(), Self::Error> { + CapiRoot::remove_dir(self, path) + } + + fn remove_file>(&self, path: P) -> Result<(), Self::Error> { + CapiRoot::remove_file(self, path) + } + + fn remove_all>(&self, path: P) -> Result<(), Self::Error> { + CapiRoot::remove_all(self, path) + } + + fn rename>( + &self, + source: P, + destination: P, + rflags: RenameFlags, + ) -> Result<(), Self::Error> { + CapiRoot::rename(self, source, destination, rflags) + } +} diff --git a/src/tests/capi/utils.rs b/src/tests/capi/utils.rs new file mode 100644 index 00000000..c131f242 --- /dev/null +++ b/src/tests/capi/utils.rs @@ -0,0 +1,164 @@ +/* + * libpathrs: safe path resolution on Linux + * Copyright (C) 2019-2024 Aleksa Sarai + * Copyright (C) 2019-2024 SUSE LLC + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +use crate::{ + capi::error as capi_error, + error::{ErrorExt, ErrorKind}, + tests::traits::ErrorImpl, +}; + +use std::{ + ffi::{CStr, CString, OsStr}, + fmt, + os::unix::{ + ffi::OsStrExt, + io::{FromRawFd, OwnedFd}, + }, + path::{Path, PathBuf}, +}; + +use errno::Errno; + +#[derive(Debug, Clone, thiserror::Error)] +pub struct CapiError { + errno: Option, + description: String, +} + +impl fmt::Display for CapiError { + fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(fmt, "{}", self.description)?; + if let Some(errno) = self.errno { + write!(fmt, " ({errno})")?; + } + Ok(()) + } +} + +impl ErrorExt for CapiError { + fn with_wrap(self, context_fn: F) -> Self + where + F: FnOnce() -> String, + { + Self { + errno: self.errno, + description: context_fn() + ": " + &self.description, + } + } +} + +impl ErrorImpl for CapiError { + fn kind(&self) -> ErrorKind { + if let Some(errno) = self.errno { + ErrorKind::OsError(Some(errno.0)) + } else { + // TODO TODO: We should probably expose the libpathrs internal + // ErrorKind types in the C API somehow? + ErrorKind::InvalidArgument + } + } +} + +fn fetch_error(res: libc::c_int) -> Result { + if res >= 0 { + Ok(res) + } else { + // SAFETY: pathrs_errorinfo is safe to call in general. + match unsafe { capi_error::pathrs_errorinfo(res) } { + Some(err) => { + let errno = match err.saved_errno as i32 { + 0 => None, + errno => Some(Errno(errno)), + }; + // SAFETY: pathrs_errorinfo returns a valid string pointer. We + // can't take ownership because pathrs_errorinfo_free() will do + // the freeing for us. + let description = unsafe { CStr::from_ptr(err.description) } + .to_string_lossy() + .to_string(); + + // Free the error from the error map, now that we copied the + // contents. + // SAFETY: We are the only ones holding a reference to the err + // pointer and don't touch it later, so we can free it freely. + unsafe { capi_error::pathrs_errorinfo_free(err as *mut _) } + + Err(CapiError { errno, description }) + } + None => panic!("unknown error id {res}"), + } + } +} + +pub(in crate::tests) fn path_to_cstring>(path: P) -> CString { + CString::new(path.as_ref().as_os_str().as_bytes()) + .expect("normal path conversion shouldn't result in spurious nul bytes") +} + +pub(in crate::tests) fn call_capi(func: Func) -> Result +where + Func: Fn() -> libc::c_int, +{ + fetch_error(func()) +} + +pub(in crate::tests) fn call_capi_zst(func: Func) -> Result<(), CapiError> +where + Func: Fn() -> libc::c_int, +{ + call_capi(func).map(|val| { + assert_eq!( + val, 0, + "call_capi_zst must only be called on methods that return <= 0: got {val}" + ); + }) +} + +pub(in crate::tests) fn call_capi_fd(func: Func) -> Result +where + Func: Fn() -> libc::c_int, +{ + // SAFETY: The caller has guaranteed us that the closure will return an fd. + call_capi(func).map(|fd| unsafe { OwnedFd::from_raw_fd(fd) }) +} + +pub(in crate::tests) fn call_capi_readlink(func: Func) -> Result +where + Func: Fn(*mut libc::c_char, libc::size_t) -> libc::c_int, +{ + // Start with a small buffer so we can exercise the retry logic. + let mut linkbuf: Vec = Vec::with_capacity(0); + let mut realsize = 8; + while realsize > linkbuf.capacity() { + linkbuf.reserve(realsize); + realsize = fetch_error(func( + linkbuf.as_mut_ptr() as *mut libc::c_char, + linkbuf.capacity(), + ))? as usize; + } + // SAFETY: The C code guarantees that realsize is how many bytes are filled. + unsafe { linkbuf.set_len(realsize) }; + + Ok(PathBuf::from(OsStr::from_bytes( + // readlink does *not* append a null terminator! + CString::new(linkbuf) + .expect("constructing a CString from the C API's copied CString should work") + .to_bytes(), + ))) +} diff --git a/src/tests/mod.rs b/src/tests/mod.rs index b236f43c..b19a7ec1 100644 --- a/src/tests/mod.rs +++ b/src/tests/mod.rs @@ -19,6 +19,8 @@ pub(crate) mod common; +#[cfg(feature = "capi")] +pub(in crate::tests) mod capi; pub(in crate::tests) mod traits; mod test_procfs; diff --git a/src/tests/test_procfs.rs b/src/tests/test_procfs.rs index 04452454..885f75f3 100644 --- a/src/tests/test_procfs.rs +++ b/src/tests/test_procfs.rs @@ -17,6 +17,8 @@ * along with this program. If not, see . */ +#[cfg(feature = "capi")] +use crate::tests::capi::CapiProcfsHandle; use crate::{ error::ErrorKind, flags::OpenFlags, @@ -30,7 +32,7 @@ use anyhow::Error; macro_rules! procfs_tests { // Create the actual test functions. - (@fn [<$func_prefix:ident $test_name:ident>] $procfs_inst:block . $procfs_op:ident ($($args:expr),*) => (over_mounts: $over_mounts:expr, error: $expect_error:expr) ;) => { + (@rust-fn [<$func_prefix:ident $test_name:ident>] $procfs_inst:block . $procfs_op:ident ($($args:expr),*) => (over_mounts: $over_mounts:expr, error: $expect_error:expr) ;) => { paste::paste! { #[test] #[cfg_attr(not(feature = "_test_as_root"), ignore)] @@ -81,36 +83,63 @@ macro_rules! procfs_tests { } }; + // Create the actual test function for the C API. + (@capi-fn [<$func_prefix:ident $test_name:ident>] $procfs_inst:block . $procfs_op:ident ($($args:expr),*) => (over_mounts: $over_mounts:expr, error: $expect_error:expr) ;) => { + paste::paste! { + #[test] + #[cfg(feature = "capi")] + #[cfg_attr(not(feature = "_test_as_root"), ignore)] + fn []() -> Result<(), Error> { + utils::[]( + || $procfs_inst, + $($args,)* + $over_mounts, + ExpectedResult::$expect_error, + ) + } + } + }; + // Create a test for each ProcfsHandle::new_* method. (@impl $test_name:ident $procfs_var:ident . $procfs_op:ident ($($args:tt)*) => ($($tt:tt)*) ;) => { procfs_tests! { - @fn [] + @rust-fn [] { ProcfsHandle::new() }.$procfs_op($($args)*) => (over_mounts: false, $($tt)*); } procfs_tests! { - @fn [] + @rust-fn [] { ProcfsHandle::new_fsopen() }.$procfs_op($($args)*) => (over_mounts: false, $($tt)*); } procfs_tests! { - @fn [] + @rust-fn [] { ProcfsHandle::new_open_tree(OpenTreeFlags::OPEN_TREE_CLONE) }.$procfs_op($($args)*) => (over_mounts: false, $($tt)*); } procfs_tests! { - @fn [] + @rust-fn [] { ProcfsHandle::new_open_tree(OpenTreeFlags::OPEN_TREE_CLONE | OpenTreeFlags::AT_RECURSIVE) }.$procfs_op($($args)*) => (over_mounts: true, $($tt)*); } procfs_tests! { - @fn [] + @rust-fn [] { ProcfsHandle::new_unsafe_open() }.$procfs_op($($args)*) => (over_mounts: true, $($tt)*); } + + // Assume that PROCFS_HANDLE is fsopen(2)-based. + // + // TODO: Figure out the fd type of PROCFS_HANDLE. In principle we would + // expect to be able to do fsopen(2) (otherwise the fsopen(2) tests will + // fail) but it would be nice to avoid possible spurrious errors. + procfs_tests! { + @capi-fn [] + { Ok(CapiProcfsHandle) }.$procfs_op($($args)*) => (over_mounts: false, $($tt)*); + } }; // procfs_tests! { abc: readlink("foo") => (error: ExpectedResult::Some(ErrorKind::OsError(Some(libc::ENOENT)))) } diff --git a/src/tests/test_resolve.rs b/src/tests/test_resolve.rs index fa3ed454..729c2c84 100644 --- a/src/tests/test_resolve.rs +++ b/src/tests/test_resolve.rs @@ -17,6 +17,8 @@ * along with this program. If not, see . */ +#[cfg(feature = "capi")] +use crate::tests::capi::CapiRoot; use crate::{ error::ErrorKind, flags::ResolverFlags, tests::common as tests_common, ResolverBackend, Root, }; @@ -32,7 +34,7 @@ macro_rules! resolve_tests { // test_err: resolve(...) => Err(ErrorKind::...) // } // } - ([$root_dir:expr] fn $test_name:ident (mut $root_var:ident : Root) $body:block => $expected:expr) => { + ([$root_dir:expr] rust-fn $test_name:ident (mut $root_var:ident : Root) $body:block => $expected:expr) => { paste::paste! { #[test] fn []() -> Result<(), Error> { @@ -133,11 +135,28 @@ macro_rules! resolve_tests { } }; - ([$root_dir:expr] @impl $test_name:ident $op_name:ident ($path:expr, $rflags:expr, $no_follow_trailing:expr) => $expected:expr) => { + ([$root_dir:expr] capi-fn $test_name:ident ($root_var:ident : CapiRoot) $body:block => $expected:expr) => { + paste::paste! { + #[cfg(feature = "capi")] + #[test] + fn []() -> Result<(), Error> { + let root_dir = $root_dir; + let $root_var = CapiRoot::open(&root_dir)?; + + { $body } + + // Make sure root_dir is not dropped earlier. + let _root_dir = root_dir; + Ok(()) + } + } + }; + + ([$root_dir:expr] @rust-impl $test_name:ident $op_name:ident ($path:expr, $rflags:expr, $no_follow_trailing:expr) => $expected:expr) => { paste::paste! { resolve_tests! { [$root_dir] - fn [<$op_name _ $test_name>](mut root: Root) { + rust-fn [<$op_name _ $test_name>](mut root: Root) { root.resolver.flags = $rflags; let expected = $expected; @@ -152,24 +171,50 @@ macro_rules! resolve_tests { } }; + ([$root_dir:expr] @capi-impl $test_name:ident $op_name:ident ($path:expr, $no_follow_trailing:expr) => $expected:expr) => { + paste::paste! { + resolve_tests! { + [$root_dir] + capi-fn [<$op_name _ $test_name>](root: CapiRoot) { + let expected = $expected; + utils::[]( + &root, + $path, + $no_follow_trailing, + expected, + )?; + } => $expected + } + } + }; + ([$root_dir:expr] @impl $test_name:ident $op_name:ident ($path:expr, rflags = $($rflag:ident)|+) => $expected:expr ) => { resolve_tests! { [$root_dir] - @impl $test_name $op_name($path, $(ResolverFlags::$rflag)|*, false) => $expected + @rust-impl $test_name $op_name($path, $(ResolverFlags::$rflag)|*, false) => $expected } + // The C API doesn't support custom ResolverFlags. }; ([$root_dir:expr] @impl $test_name:ident $op_name:ident ($path:expr, no_follow_trailing = $no_follow_trailing:expr) => $expected:expr ) => { resolve_tests! { [$root_dir] - @impl $test_name $op_name($path, ResolverFlags::empty(), $no_follow_trailing) => $expected + @rust-impl $test_name $op_name($path, ResolverFlags::empty(), $no_follow_trailing) => $expected + } + resolve_tests! { + [$root_dir] + @capi-impl $test_name $op_name($path, $no_follow_trailing) => $expected } }; ([$root_dir:expr] @impl $test_name:ident $op_name:ident ($path:expr) => $expected:expr ) => { resolve_tests! { [$root_dir] - @impl $test_name $op_name($path, ResolverFlags::empty(), false) => $expected + @rust-impl $test_name $op_name($path, ResolverFlags::empty(), false) => $expected + } + resolve_tests! { + [$root_dir] + @capi-impl $test_name $op_name($path, false) => $expected } }; diff --git a/src/tests/test_root_ops.rs b/src/tests/test_root_ops.rs index 7c2af152..ca70aa0c 100644 --- a/src/tests/test_root_ops.rs +++ b/src/tests/test_root_ops.rs @@ -17,6 +17,8 @@ * along with this program. If not, see . */ +#[cfg(feature = "capi")] +use crate::tests::capi; use crate::{ error::ErrorKind, flags::{OpenFlags, RenameFlags}, @@ -109,6 +111,16 @@ macro_rules! root_op_tests { $body } + + $(#[$meta])* + #[cfg(feature = "capi")] + #[test] + fn []() -> Result<(), Error> { + let root_dir = tests_common::create_basic_tree()?; + let $root_var = capi::CapiRoot::open(&root_dir)?; + + $body + } } };