use crate::files::{Refused, components, platform};
use std::{
fmt,
io::Read as _,
path::{Path, PathBuf},
};
#[derive(Debug)]
pub enum AccessError {
Path(String),
Link(String),
Unexpected(String),
TooLarge(String),
Locked,
Io(String, std::io::Error),
}
impl fmt::Display for AccessError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Path(path) => write!(formatter, "{path}: not a relative path beneath the root"),
Self::Link(path) => write!(formatter, "{path}: symbolic links are not followed"),
Self::Unexpected(path) => {
write!(formatter, "{path}: not a regular file or directory")
}
Self::TooLarge(path) => write!(formatter, "{path}: file is too large"),
Self::Locked => write!(formatter, "another edit holds the source tree's lock"),
Self::Io(path, error) => write!(formatter, "{path}: {error}"),
}
}
}
impl std::error::Error for AccessError {}
impl AccessError {
#[doc(hidden)]
#[must_use]
pub fn pending_removal(path: &str) -> Self {
Self::Io(
path.to_owned(),
std::io::Error::new(std::io::ErrorKind::ResourceBusy, RemovalPending),
)
}
#[must_use]
pub fn removal_pending(&self) -> bool {
type Inner = dyn std::error::Error + Send + Sync;
matches!(self, Self::Io(_, error) if error.get_ref().is_some_and(Inner::is::<RemovalPending>))
}
}
#[derive(Debug)]
pub(crate) struct RemovalPending;
impl fmt::Display for RemovalPending {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("it is removed, but stays until another program closes it")
}
}
impl std::error::Error for RemovalPending {}
fn refused(path: &str, refused: Refused) -> AccessError {
match refused {
Refused::Link(prefix) => AccessError::Link(prefix),
Refused::Missing | Refused::Unexpected => AccessError::Unexpected(path.to_owned()),
Refused::Io(error) => AccessError::Io(path.to_owned(), error),
}
}
pub const LOCK_NAME: &str = ".tilewright.lock";
pub(crate) fn checked(path: &str) -> Result<Vec<&str>, AccessError> {
if path.is_empty() {
return Ok(Vec::new());
}
if path.len() > 4096 || path.split('/').any(|name| checked_name(name).is_err()) {
return Err(AccessError::Path(path.to_owned()));
}
Ok(components(path))
}
fn checked_name(name: &str) -> Result<&str, AccessError> {
if name.is_empty()
|| name == "."
|| name == ".."
|| name.len() > 255
|| name.contains(['/', '\\', ':', '\0'])
{
Err(AccessError::Path(name.to_owned()))
} else {
Ok(name)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Access {
pub mode: u32,
pub user: u32,
pub group: u32,
pub control: [u8; 32],
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Audience {
reach: u32,
user: Option<u32>,
group: Option<u32>,
control: [u8; 32],
}
impl Access {
#[must_use]
pub const fn group_reads_alone(&self) -> bool {
self.mode & 0o040 != 0 && self.mode & 0o004 == 0
}
#[must_use]
pub const fn nested(&self, directory: bool) -> bool {
let bits = if directory {
self.mode & 0o111
} else {
(self.mode & 0o444) >> 2
};
let (owner, group, other) = (bits & 0o100 != 0, bits & 0o010 != 0, bits & 0o001 != 0);
(owner || !group) && (group || !other)
}
#[must_use]
pub fn audience(&self, directory: bool) -> Audience {
let reach = if directory {
(self.mode & 0o111) << 2
} else {
self.mode & 0o444
};
Audience {
reach,
user: (reach != 0o444).then_some(self.user),
group: (reach & 0o040 != 0 && reach & 0o004 == 0).then_some(self.group),
control: self.control,
}
}
#[must_use]
pub fn new_file(&self) -> Self {
#[cfg(unix)]
let user = rustix::process::geteuid().as_raw();
#[cfg(not(unix))]
let user = 0;
Self {
mode: self.mode & 0o666,
user,
..*self
}
}
}
pub struct SourceRoot {
path: PathBuf,
root: platform::Root,
}
impl SourceRoot {
#[must_use]
pub fn new(root: &Path) -> Self {
Self {
path: root.to_path_buf(),
root: platform::Root::new(root),
}
}
#[must_use]
pub fn try_clone(&self) -> Self {
Self {
path: self.path.clone(),
root: self.root.try_clone(),
}
}
#[must_use]
pub fn path(&self) -> &Path {
&self.path
}
pub(crate) fn platform(&self) -> platform::Root {
self.root.try_clone()
}
pub fn exists(&self, path: &str) -> Result<bool, AccessError> {
if path.is_empty() {
return self.directory(path, false).map(|root| root.is_some());
}
match self.root.kind(&checked(path)?) {
Ok(crate::files::Found::Missing) | Err(Refused::Missing | Refused::Unexpected) => {
Ok(false)
}
Ok(_) => Ok(true),
Err(Refused::Link(prefix)) if prefix == path => Ok(true),
Err(other) => Err(refused(path, other)),
}
}
pub fn read(&self, path: &str, limit: u64) -> Result<Option<Vec<u8>>, AccessError> {
let parts = checked(path)?;
if parts.is_empty() {
return Err(AccessError::Unexpected(path.to_owned()));
}
let (file, length) = match self.root.file(&parts) {
Ok(opened) => opened,
Err(Refused::Missing) => return Ok(None),
Err(other) => return Err(refused(path, other)),
};
if length > limit {
return Err(AccessError::TooLarge(path.to_owned()));
}
let mut bytes = Vec::new();
file.take(limit.saturating_add(1))
.read_to_end(&mut bytes)
.map_err(|error| AccessError::Io(path.to_owned(), error))?;
if u64::try_from(bytes.len()).unwrap_or(u64::MAX) > limit {
return Err(AccessError::TooLarge(path.to_owned()));
}
Ok(Some(bytes))
}
pub fn directory(
&self,
path: &str,
create: bool,
) -> Result<Option<SourceDirectory>, AccessError> {
match self.root.directory(&checked(path)?, create) {
Ok(directory) => Ok(Some(SourceDirectory {
directory,
path: path.to_owned(),
})),
Err(Refused::Missing) => Ok(None),
Err(other) => Err(refused(path, other)),
}
}
pub fn make_directory(&self, path: &str) -> Result<bool, AccessError> {
self.root
.make_directory(&checked(path)?)
.map_err(|error| refused(path, error))
}
pub fn names(
&self,
path: &str,
limit: usize,
) -> Result<Option<(std::collections::BTreeSet<String>, usize)>, AccessError> {
let listing = match self.root.list(&checked(path)?) {
Ok(listing) => listing,
Err(Refused::Missing) => return Ok(None),
Err(other) => return Err(refused(path, other)),
};
let mut names = std::collections::BTreeSet::new();
let mut count = 0;
for entry in listing {
if count >= limit {
return Err(AccessError::TooLarge(path.to_owned()));
}
count += 1;
let entry = entry.map_err(|error| AccessError::Io(path.to_owned(), error))?;
names.extend(entry.name);
}
Ok(Some((names, count)))
}
pub fn lock(&self) -> Result<SourceLock, AccessError> {
match self.root.lock() {
Ok(Some(lock)) => Ok(SourceLock(lock)),
Ok(None) => Err(AccessError::Locked),
Err(other) => Err(refused(".", other)),
}
}
}
pub struct Mounts(platform::Mounts);
impl Mounts {
#[must_use]
pub fn read() -> Self {
Self(platform::Mounts::read())
}
}
pub struct SourceLock(#[allow(dead_code)] platform::Lock);
pub struct SourceDirectory {
directory: platform::Directory,
path: String,
}
impl SourceDirectory {
fn entry(&self, name: &str) -> String {
crate::files::join(&self.path, name)
}
pub fn write(
&self,
name: &str,
access: Option<&Access>,
bytes: &[u8],
) -> Result<(), AccessError> {
self.directory
.write(checked_name(name)?, access, bytes)
.map_err(|error| refused(&self.entry(name), error))
}
pub fn access(&self, name: &str) -> Result<Option<Access>, AccessError> {
self.directory
.access(checked_name(name)?)
.map_err(|error| refused(&self.entry(name), error))
}
pub fn own_access(&self) -> Result<Access, AccessError> {
self.directory
.own_access()
.map_err(|error| refused(&self.path, error))
}
pub fn set_own_access(&self, access: &Access) -> Result<(), AccessError> {
self.directory
.set_own_access(access)
.map_err(|error| refused(&self.path, error))
}
pub fn check_final_name(&self, staged: &str, name: &str) -> Result<(), AccessError> {
self.directory
.check_final_name(checked_name(staged)?, checked_name(name)?)
.map_err(|error| refused(&self.entry(name), error))
}
pub fn rename(&self, from: &str, to: &str) -> Result<(), AccessError> {
self.directory
.rename(checked_name(from)?, checked_name(to)?)
.map_err(|error| refused(&self.entry(to), error))
}
pub fn remove_file(&self, name: &str) -> Result<bool, AccessError> {
self.directory
.remove_file(checked_name(name)?)
.map_err(|error| refused(&self.entry(name), error))
}
pub fn remove_directory(&self, name: &str) -> Result<bool, AccessError> {
self.directory
.remove_directory(checked_name(name)?)
.map_err(|error| refused(&self.entry(name), error))
}
pub fn unchecked_file_system(&self, mounts: &Mounts) -> Result<Option<String>, AccessError> {
self.directory
.unchecked_file_system(&mounts.0)
.map_err(|error| refused(&self.path, error))
}
pub fn sync(&self) -> Result<bool, AccessError> {
self.directory
.sync()
.map_err(|error| refused(&self.path, error))
}
}
impl fmt::Debug for SourceRoot {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("SourceRoot")
}
}