Skip to content

Commit

Permalink
Merge pull request #54 from cgwalters/open-rooted
Browse files Browse the repository at this point in the history
Add a `RootDirectory` API
  • Loading branch information
cgwalters authored Jul 16, 2024
2 parents 4c49d64 + e5c8579 commit d6b9abc
Show file tree
Hide file tree
Showing 4 changed files with 179 additions and 2 deletions.
12 changes: 11 additions & 1 deletion src/dirext.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@ 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 `RESOLVE_IN_ROOT`
/// to support absolute symlinks.
#[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 +249,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 +309,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_rootdir_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_rootdir_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 d6b9abc

Please sign in to comment.