use std::{
borrow::Cow,
fs::{File, OpenOptions},
io::{Read as _, Write},
path::{Path, PathBuf},
};
#[cfg(unix)]
use std::os::unix::fs::OpenOptionsExt as _;
use crate::{dir::FullPathCheck, walk::PathType, CheckedDir, Error, Result, Verifier};
pub struct FileAccess<'a> {
pub(crate) inner: Inner<'a>,
#[cfg(unix)]
create_with_mode: Option<u32>,
follow_final_links: bool,
}
pub(crate) enum Inner<'a> {
CheckedDir(&'a CheckedDir),
Verifier(Verifier<'a>),
}
impl<'a> FileAccess<'a> {
pub(crate) fn from_checked_dir(checked_dir: &'a CheckedDir) -> Self {
Self::from_inner(Inner::CheckedDir(checked_dir))
}
pub(crate) fn from_verifier(verifier: Verifier<'a>) -> Self {
Self::from_inner(Inner::Verifier(verifier))
}
fn from_inner(inner: Inner<'a>) -> Self {
Self {
inner,
#[cfg(unix)]
create_with_mode: None,
follow_final_links: false,
}
}
fn verified_full_path(&self, path: &Path, check_type: FullPathCheck) -> Result<PathBuf> {
match &self.inner {
Inner::CheckedDir(cd) => cd.verified_full_path(path, check_type),
Inner::Verifier(v) => {
let to_verify = match check_type {
FullPathCheck::CheckPath => path,
FullPathCheck::CheckParent => path.parent().unwrap_or(path),
};
v.check(to_verify)?;
Ok(path.into())
}
}
}
fn verifier(&self) -> crate::Verifier {
match &self.inner {
Inner::CheckedDir(cd) => cd.verifier(),
Inner::Verifier(v) => v.clone(),
}
}
fn location_unverified<'b>(&self, path: &'b Path) -> Result<Cow<'b, Path>> {
Ok(match self.inner {
Inner::CheckedDir(cd) => cd.join(path)?.into(),
Inner::Verifier(_) => path.into(),
})
}
pub fn create_with_mode(mut self, mode: u32) -> Self {
#[cfg(unix)]
{
self.create_with_mode = Some(mode);
}
self
}
pub fn follow_final_links(mut self, follow: bool) -> Self {
self.follow_final_links = follow;
self
}
pub fn open<P: AsRef<Path>>(self, path: P, options: &OpenOptions) -> Result<File> {
self.open_internal(path.as_ref(), options)
}
fn open_internal(&self, path: &Path, options: &OpenOptions) -> Result<File> {
let follow_links = self.follow_final_links;
let check_type = if follow_links {
FullPathCheck::CheckPath
} else {
FullPathCheck::CheckParent
};
let path = match self.verified_full_path(path.as_ref(), check_type) {
Ok(path) => path.into(),
Err(Error::NotFound(_)) if follow_links => self.location_unverified(path.as_ref())?,
Err(e) => return Err(e),
};
#[allow(unused_mut)]
let mut options = options.clone();
#[cfg(unix)]
{
let create_mode = self.create_with_mode.unwrap_or(0o600);
options.mode(create_mode);
if !follow_links {
options.custom_flags(libc::O_NOFOLLOW);
}
}
let file = options
.open(&path)
.map_err(|e| Error::io(e, path.as_ref(), "open file"))?;
let meta = file
.metadata()
.map_err(|e| Error::inspecting(e, path.as_ref()))?;
if let Some(error) = self
.verifier()
.check_one(path.as_ref(), PathType::Content, &meta)
.into_iter()
.next()
{
Err(error)
} else {
Ok(file)
}
}
pub fn read_to_string<P: AsRef<Path>>(self, path: P) -> Result<String> {
let path = path.as_ref();
let mut file = self.open(path, OpenOptions::new().read(true))?;
let mut result = String::new();
file.read_to_string(&mut result)
.map_err(|e| Error::io(e, path, "read file"))?;
Ok(result)
}
pub fn read<P: AsRef<Path>>(self, path: P) -> Result<Vec<u8>> {
let path = path.as_ref();
let mut file = self.open(path, OpenOptions::new().read(true))?;
let mut result = Vec::new();
file.read_to_end(&mut result)
.map_err(|e| Error::io(e, path, "read file"))?;
Ok(result)
}
pub fn write_and_replace<P: AsRef<Path>, C: AsRef<[u8]>>(
self,
path: P,
contents: C,
) -> Result<()> {
let path = path.as_ref();
let final_path = self.verified_full_path(path, FullPathCheck::CheckParent)?;
let tmp_name = path.with_extension("tmp");
let _ignore = std::fs::remove_file(&tmp_name);
let mut tmp_file = self.open_internal(
&tmp_name,
OpenOptions::new().create(true).truncate(true).write(true),
)?;
tmp_file
.write_all(contents.as_ref())
.map_err(|e| Error::io(e, &tmp_name, "write to file"))?;
drop(tmp_file);
std::fs::rename(
self.location_unverified(tmp_name.as_path())?,
final_path,
)
.map_err(|e| Error::io(e, path, "replace file"))?;
Ok(())
}
}
#[cfg(test)]
mod test {
#![allow(clippy::bool_assert_comparison)]
#![allow(clippy::clone_on_copy)]
#![allow(clippy::dbg_macro)]
#![allow(clippy::mixed_attributes_style)]
#![allow(clippy::print_stderr)]
#![allow(clippy::print_stdout)]
#![allow(clippy::single_char_pattern)]
#![allow(clippy::unwrap_used)]
#![allow(clippy::unchecked_duration_subtraction)]
#![allow(clippy::useless_vec)]
#![allow(clippy::needless_pass_by_value)]
use std::fs;
use super::*;
use crate::{testing::Dir, Mistrust};
#[test]
fn create_public_in_checked_dir() {
let d = Dir::new();
d.dir("a");
d.chmod("a", 0o700);
let m = Mistrust::builder()
.ignore_prefix(d.canonical_root())
.build()
.unwrap();
let checked = m.verifier().secure_dir(d.path("a")).unwrap();
{
let mut f = checked
.file_access()
.open(
"private-1.txt",
OpenOptions::new().write(true).create_new(true),
)
.unwrap();
f.write_all(b"Hello world\n").unwrap();
checked
.file_access()
.write_and_replace("private-2.txt", b"Hello world 2\n")
.unwrap();
}
{
let mut f = checked
.file_access()
.create_with_mode(0o640)
.open(
"public-1.txt",
OpenOptions::new().write(true).create_new(true),
)
.unwrap();
f.write_all(b"Hello wider world\n").unwrap();
checked
.file_access()
.create_with_mode(0o644)
.write_and_replace("public-2.txt", b"Hello wider world 2")
.unwrap();
}
#[cfg(target_family = "unix")]
{
use std::os::unix::fs::MetadataExt;
assert_eq!(
fs::metadata(d.path("a/private-1.txt")).unwrap().mode() & 0o7777,
0o600
);
assert_eq!(
fs::metadata(d.path("a/private-2.txt")).unwrap().mode() & 0o7777,
0o600
);
assert_eq!(
fs::metadata(d.path("a/public-1.txt")).unwrap().mode() & 0o7777,
0o640
);
assert_eq!(
fs::metadata(d.path("a/public-2.txt")).unwrap().mode() & 0o7777,
0o644
);
}
}
#[test]
#[cfg(unix)]
fn open_symlinks() {
use crate::testing::LinkType;
let d = Dir::new();
d.dir("a");
d.dir("a/b");
d.dir("a/c");
d.file("a/c/file1.txt");
d.link_rel(LinkType::File, "../c/file1.txt", "a/b/present");
d.link_rel(LinkType::File, "../c/file2.txt", "a/b/absent");
d.chmod("a", 0o700);
d.chmod("a/b", 0o700);
d.chmod("a/c", 0o700);
d.chmod("a/c/file1.txt", 0o600);
let m = Mistrust::builder()
.ignore_prefix(d.canonical_root())
.build()
.unwrap();
let contents = m
.file_access()
.follow_final_links(true)
.read(d.path("a/b/present"))
.unwrap();
assert_eq!(
&contents[..],
&b"This space is intentionally left blank"[..]
);
let error = m
.file_access()
.follow_final_links(true)
.read(d.path("a/b/absent"))
.unwrap_err();
assert!(matches!(error, Error::NotFound(_)));
{
let mut f = m
.file_access()
.follow_final_links(true)
.open(
d.path("a/b/present"),
OpenOptions::new().write(true).truncate(true),
)
.unwrap();
f.write_all(b"This is extremely serious!").unwrap();
}
let contents = m
.file_access()
.follow_final_links(true)
.read(d.path("a/b/present"))
.unwrap();
assert_eq!(&contents[..], &b"This is extremely serious!"[..]);
let contents = m.file_access().read(d.path("a/c/file1.txt")).unwrap();
assert_eq!(&contents[..], &b"This is extremely serious!"[..]);
{
let mut f = m
.file_access()
.follow_final_links(true)
.open(
d.path("a/b/absent"),
OpenOptions::new().create(true).write(true),
)
.unwrap();
f.write_all(b"This is extremely silly!").unwrap();
}
let contents = m.file_access().read(d.path("a/c/file2.txt")).unwrap();
assert_eq!(&contents[..], &b"This is extremely silly!"[..]);
}
}