use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::{
fmt,
path::{Path, PathBuf},
str::FromStr,
};
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(transparent)]
pub struct FsPermissions {
pub permissions: Vec<PathPermission>,
}
impl FsPermissions {
pub fn new(permissions: impl IntoIterator<Item = PathPermission>) -> Self {
Self { permissions: permissions.into_iter().collect() }
}
pub fn add(&mut self, permission: PathPermission) {
self.permissions.push(permission)
}
pub fn is_path_allowed(&self, path: &Path, kind: FsAccessKind) -> bool {
self.find_permission(path).map(|perm| perm.is_granted(kind)).unwrap_or_default()
}
pub fn find_permission(&self, path: &Path) -> Option<FsAccessPermission> {
let mut permission: Option<&PathPermission> = None;
for perm in &self.permissions {
let permission_path = dunce::canonicalize(&perm.path).unwrap_or(perm.path.clone());
if path.starts_with(permission_path) {
if let Some(active_perm) = permission.as_ref() {
if perm.path < active_perm.path {
continue;
}
}
permission = Some(perm);
}
}
permission.map(|perm| perm.access)
}
pub fn join_all(&mut self, root: &Path) {
self.permissions.iter_mut().for_each(|perm| {
perm.path = root.join(&perm.path);
})
}
pub fn joined(mut self, root: &Path) -> Self {
self.join_all(root);
self
}
pub fn remove(&mut self, path: &Path) {
self.permissions.retain(|permission| permission.path != path)
}
pub fn is_empty(&self) -> bool {
self.permissions.is_empty()
}
pub fn len(&self) -> usize {
self.permissions.len()
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct PathPermission {
pub access: FsAccessPermission,
pub path: PathBuf,
}
impl PathPermission {
pub fn new(path: impl Into<PathBuf>, access: FsAccessPermission) -> Self {
Self { path: path.into(), access }
}
pub fn read(path: impl Into<PathBuf>) -> Self {
Self::new(path, FsAccessPermission::Read)
}
pub fn read_write(path: impl Into<PathBuf>) -> Self {
Self::new(path, FsAccessPermission::ReadWrite)
}
pub fn write(path: impl Into<PathBuf>) -> Self {
Self::new(path, FsAccessPermission::Write)
}
pub fn none(path: impl Into<PathBuf>) -> Self {
Self::new(path, FsAccessPermission::None)
}
pub fn is_granted(&self, kind: FsAccessKind) -> bool {
self.access.is_granted(kind)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum FsAccessKind {
Read,
Write,
}
impl fmt::Display for FsAccessKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Read => f.write_str("read"),
Self::Write => f.write_str("write"),
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum FsAccessPermission {
#[default]
None,
ReadWrite,
Read,
Write,
}
impl FsAccessPermission {
pub fn is_granted(&self, kind: FsAccessKind) -> bool {
match (self, kind) {
(Self::ReadWrite, _) => true,
(Self::None, _) => false,
(Self::Read, FsAccessKind::Read) => true,
(Self::Write, FsAccessKind::Write) => true,
_ => false,
}
}
}
impl FromStr for FsAccessPermission {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"true" | "read-write" | "readwrite" => Ok(Self::ReadWrite),
"false" | "none" => Ok(Self::None),
"read" => Ok(Self::Read),
"write" => Ok(Self::Write),
_ => Err(format!("Unknown variant {s}")),
}
}
}
impl fmt::Display for FsAccessPermission {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::ReadWrite => f.write_str("read-write"),
Self::None => f.write_str("none"),
Self::Read => f.write_str("read"),
Self::Write => f.write_str("write"),
}
}
}
impl Serialize for FsAccessPermission {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
match self {
Self::ReadWrite => serializer.serialize_bool(true),
Self::None => serializer.serialize_bool(false),
Self::Read => serializer.serialize_str("read"),
Self::Write => serializer.serialize_str("write"),
}
}
}
impl<'de> Deserialize<'de> for FsAccessPermission {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde(untagged)]
enum Status {
Bool(bool),
String(String),
}
match Status::deserialize(deserializer)? {
Status::Bool(enabled) => {
let status = if enabled { Self::ReadWrite } else { Self::None };
Ok(status)
}
Status::String(val) => val.parse().map_err(serde::de::Error::custom),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn can_parse_permission() {
assert_eq!(FsAccessPermission::ReadWrite, "true".parse().unwrap());
assert_eq!(FsAccessPermission::ReadWrite, "readwrite".parse().unwrap());
assert_eq!(FsAccessPermission::ReadWrite, "read-write".parse().unwrap());
assert_eq!(FsAccessPermission::None, "false".parse().unwrap());
assert_eq!(FsAccessPermission::None, "none".parse().unwrap());
assert_eq!(FsAccessPermission::Read, "read".parse().unwrap());
assert_eq!(FsAccessPermission::Write, "write".parse().unwrap());
}
#[test]
fn nested_permissions() {
let permissions = FsPermissions::new(vec![
PathPermission::read("./"),
PathPermission::write("./out"),
PathPermission::read_write("./out/contracts"),
]);
let permission =
permissions.find_permission(Path::new("./out/contracts/MyContract.sol")).unwrap();
assert_eq!(FsAccessPermission::ReadWrite, permission);
let permission = permissions.find_permission(Path::new("./out/MyContract.sol")).unwrap();
assert_eq!(FsAccessPermission::Write, permission);
}
}