Skip to content

Commit

Permalink
Add a RootDirectory API
Browse files Browse the repository at this point in the history
Right now cap-std uses `RESOLVE_BENEATH`, not `RESOLVE_IN_ROOT`
which means absolute symlinks can't work.

It's really handy to use cap-std where we can; I particularly
like doing so in unit tests for example.

However...we really do need to support absolute
symlinks for many cases. Add a handy `RootDirectory` which
exposes just a read-only subset of APIs - in particular
we basically just want `open()` (and convenience wrappers
like `read_to_string()` on this).

For other writing cases right now, one needs to construct
a regular `Dir`.

Note that unlike the rest of cap-std, absolutely¹ no attempt
is made to handle cases where `openat2` isn't accessible
(non-Linux, cases where the kernel is too old, etc.)

¹ Pun not intended

Signed-off-by: Colin Walters <walters@verbum.org>
  • Loading branch information
cgwalters committed Jul 13, 2024
1 parent 4c49d64 commit 61162a1
Show file tree
Hide file tree
Showing 4 changed files with 178 additions and 2 deletions.
11 changes: 10 additions & 1 deletion src/dirext.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ pub trait CapStdExtDirExt {
/// Open a directory, but return `Ok(None)` if it does not exist.
fn open_dir_optional(&self, path: impl AsRef<Path>) -> Result<Option<Dir>>;

/// Create a special variant of [`cap_std::fs::Dir`] which uses
#[cfg(any(target_os = "android", target_os = "linux"))]
fn open_dir_rooted_ext(&self, path: impl AsRef<Path>) -> Result<crate::RootDir>;

/// Create the target directory, but do nothing if a directory already exists at that path.
/// The return value will be `true` if the directory was created. An error will be
/// returned if the path is a non-directory. Symbolic links will be followed.
Expand Down Expand Up @@ -244,7 +248,7 @@ pub trait CapStdExtDirExtUtf8 {
C: FnMut(&str, &str) -> std::cmp::Ordering;
}

fn map_optional<R>(r: Result<R>) -> Result<Option<R>> {
pub(crate) fn map_optional<R>(r: Result<R>) -> Result<Option<R>> {
match r {
Ok(v) => Ok(Some(v)),
Err(e) => {
Expand Down Expand Up @@ -304,6 +308,11 @@ impl CapStdExtDirExt for Dir {
map_optional(self.open_dir(path.as_ref()))
}

#[cfg(any(target_os = "android", target_os = "linux"))]
fn open_dir_rooted_ext(&self, path: impl AsRef<Path>) -> Result<crate::RootDir> {
crate::RootDir::new(self, path)
}

fn ensure_dir_with(
&self,
p: impl AsRef<Path>,
Expand Down
4 changes: 4 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ pub use cap_tempfile::cap_std;
pub mod cmdext;
pub mod dirext;

#[cfg(any(target_os = "android", target_os = "linux"))]
mod rootdir;
pub use rootdir::*;

/// Prelude, intended for glob import.
pub mod prelude {
#[cfg(not(windows))]
Expand Down
119 changes: 119 additions & 0 deletions src/rootdir.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
use std::fs;
use std::io;
use std::io::Read;
use std::path::Path;

use cap_std::fs::Dir;
use cap_tempfile::cap_std;
use rustix::fd::AsFd;
use rustix::fd::BorrowedFd;
use rustix::fs::OFlags;
use rustix::fs::ResolveFlags;
use rustix::path::Arg;

pub(crate) fn open_beneath_rdonly(start: &BorrowedFd, path: &Path) -> io::Result<fs::File> {
// We loop forever on EAGAIN right now. The cap-std version loops just 4 times,
// which seems really arbitrary.
let r = path.into_with_c_str(|path_c_str| 'start: loop {
match rustix::fs::openat2(
start,
path_c_str,
OFlags::CLOEXEC | OFlags::RDONLY,
rustix::fs::Mode::empty(),
ResolveFlags::IN_ROOT | ResolveFlags::NO_MAGICLINKS,
) {
Ok(file) => {
return Ok(file);
}
Err(rustix::io::Errno::AGAIN | rustix::io::Errno::INTR) => {
continue 'start;
}
Err(e) => {
return Err(e);
}
}
})?;
Ok(r.into())
}

/// Wrapper for a [`cap_std::fs::Dir`] that is defined to use `RESOLVE_IN_ROOT``
/// semantics when opening files and subdirectories. This currently only
/// offers a subset of the methods, primarily reading.
///
/// # When and how to use this
///
/// In general, if your use case possibly involves reading files that may be
/// absolute symlinks, or relative symlinks that may go outside the provided
/// directory, you will need to use this API instead of [`cap_std::fs::Dir`].
///
/// # Performing writes
///
/// If you want to simultaneously perform other operations (such as writing), at the moment
/// it requires explicitly maintaining a duplicate copy of a [`cap_std::fs::Dir`]
/// instance, or using direct [`rustix::fs`] APIs.
#[derive(Debug)]
pub struct RootDir(Dir);

impl RootDir {
/// Create a new instance from an existing [`cap_std::fs::Dir`] instance.
pub fn new(src: &Dir, path: impl AsRef<Path>) -> io::Result<Self> {
src.open_dir(path).map(Self)
}

/// Create a new instance from an ambient path.
pub fn open_ambient_root(
path: impl AsRef<Path>,
authority: cap_std::AmbientAuthority,
) -> io::Result<Self> {
Dir::open_ambient_dir(path, authority).map(Self)
}

/// Open a file in this root, read-only.
pub fn open(&self, path: impl AsRef<Path>) -> io::Result<fs::File> {
let path = path.as_ref();
open_beneath_rdonly(&self.0.as_fd(), path)
}

/// Open a file read-only, but return `Ok(None)` if it does not exist.
pub fn open_optional(&self, path: impl AsRef<Path>) -> io::Result<Option<fs::File>> {
crate::dirext::map_optional(self.open(path))
}

/// Read the contents of a file into a vector.
pub fn read(&self, path: impl AsRef<Path>) -> io::Result<Vec<u8>> {
let mut f = self.open(path.as_ref())?;
let mut r = Vec::new();
f.read_to_end(&mut r)?;
Ok(r)
}

/// Read the contents of a file as a string.
pub fn read_to_string(&self, path: impl AsRef<Path>) -> io::Result<String> {
let mut f = self.open(path.as_ref())?;
let mut s = String::new();
f.read_to_string(&mut s)?;
Ok(s)
}

/// Return the directory entries.
pub fn entries(&self) -> io::Result<cap_std::fs::ReadDir> {
self.0.entries()
}

/// Return the directory entries of the target subdirectory.
pub fn read_dir(&self, path: impl AsRef<Path>) -> io::Result<cap_std::fs::ReadDir> {
self.0.read_dir(path.as_ref())
}

/// Create a [`cap_std::fs::Dir`] pointing to the same directory as `self`.
/// This view will *not* use `RESOLVE_IN_ROOT`.
pub fn reopen_cap_std(&self) -> io::Result<Dir> {
Dir::reopen_dir(&self.0.as_fd())
}
}

impl From<Dir> for RootDir {
fn from(dir: Dir) -> Self {
Self(dir)
}
}
46 changes: 45 additions & 1 deletion tests/it/main.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
use anyhow::Result;

use cap_std::fs::{Dir, File, Permissions, PermissionsExt};
use cap_std_ext::cap_std;
use cap_std_ext::cmdext::CapStdExtCommandExt;
use cap_std_ext::dirext::CapStdExtDirExt;
use cap_std_ext::{cap_std, RootDir};
use std::io::Write;
use std::path::Path;
use std::{process::Command, sync::Arc};
Expand Down Expand Up @@ -339,3 +339,47 @@ fn filenames_utf8() -> Result<()> {
}
Ok(())
}

#[test]
fn test_open() -> Result<()> {
let td = &cap_tempfile::TempDir::new(cap_std::ambient_authority())?;
let root = RootDir::new(td, ".").unwrap();

assert!(root.open_optional("foo").unwrap().is_none());

td.create_dir("etc")?;
td.create_dir_all("usr/lib")?;

let authjson = "usr/lib/auth.json";
assert!(root.open(authjson).is_err());
assert!(root.open_optional(authjson).unwrap().is_none());
td.write(authjson, "auth contents")?;
assert!(root.open_optional(authjson).unwrap().is_some());
let contents = root.read_to_string(authjson).unwrap();
assert_eq!(&contents, "auth contents");

td.symlink_contents("/usr/lib/auth.json", "etc/auth.json")?;

let contents = root.read_to_string("/etc/auth.json").unwrap();
assert_eq!(&contents, "auth contents");

// But this should fail due to an escape
assert!(td.read_to_string("etc/auth.json").is_err());
Ok(())
}

#[test]
fn test_entries() -> Result<()> {
let td = &cap_tempfile::TempDir::new(cap_std::ambient_authority())?;
let root = RootDir::new(td, ".").unwrap();

td.create_dir("etc")?;
td.create_dir_all("usr/lib")?;

let ents = root
.entries()
.unwrap()
.collect::<std::io::Result<Vec<_>>>()?;
assert_eq!(ents.len(), 2);
Ok(())
}

0 comments on commit 61162a1

Please sign in to comment.