foundry_config/
fs_permissions.rs

1//! Support for controlling fs access
2
3use serde::{Deserialize, Deserializer, Serialize, Serializer};
4use std::{
5    fmt,
6    path::{Path, PathBuf},
7    str::FromStr,
8};
9
10/// Configures file system access
11///
12/// E.g. for cheat codes (`vm.writeFile`)
13#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
14#[serde(transparent)]
15pub struct FsPermissions {
16    /// what kind of access is allowed
17    pub permissions: Vec<PathPermission>,
18}
19
20impl FsPermissions {
21    /// Creates anew instance with the given `permissions`
22    pub fn new(permissions: impl IntoIterator<Item = PathPermission>) -> Self {
23        Self { permissions: permissions.into_iter().collect() }
24    }
25
26    /// Adds a new permission
27    pub fn add(&mut self, permission: PathPermission) {
28        self.permissions.push(permission)
29    }
30
31    /// Returns true if access to the specified path is allowed with the specified.
32    ///
33    /// This first checks permission, and only if it is granted, whether the path is allowed.
34    ///
35    /// We only allow paths that are inside  allowed paths.
36    ///
37    /// Caution: This should be called with normalized paths if the `allowed_paths` are also
38    /// normalized.
39    pub fn is_path_allowed(&self, path: &Path, kind: FsAccessKind) -> bool {
40        self.find_permission(path).map(|perm| perm.is_granted(kind)).unwrap_or_default()
41    }
42
43    /// Returns the permission for the matching path.
44    ///
45    /// This finds the longest matching path with resolved sym links, e.g. if we have the following
46    /// permissions:
47    ///
48    /// `./out` = `read`
49    /// `./out/contracts` = `read-write`
50    ///
51    /// And we check for `./out/contracts/MyContract.sol` we will get `read-write` as permission.
52    pub fn find_permission(&self, path: &Path) -> Option<FsAccessPermission> {
53        let mut permission: Option<&PathPermission> = None;
54        for perm in &self.permissions {
55            let permission_path = dunce::canonicalize(&perm.path).unwrap_or(perm.path.clone());
56            if path.starts_with(permission_path) {
57                if let Some(active_perm) = permission.as_ref() {
58                    // the longest path takes precedence
59                    if perm.path < active_perm.path {
60                        continue;
61                    }
62                }
63                permission = Some(perm);
64            }
65        }
66        permission.map(|perm| perm.access)
67    }
68
69    /// Updates all `allowed_paths` and joins ([`Path::join`]) the `root` with all entries
70    pub fn join_all(&mut self, root: &Path) {
71        self.permissions.iter_mut().for_each(|perm| {
72            perm.path = root.join(&perm.path);
73        })
74    }
75
76    /// Same as [`Self::join_all`] but consumes the type
77    pub fn joined(mut self, root: &Path) -> Self {
78        self.join_all(root);
79        self
80    }
81
82    /// Removes all existing permissions for the given path
83    pub fn remove(&mut self, path: &Path) {
84        self.permissions.retain(|permission| permission.path != path)
85    }
86
87    /// Returns true if no permissions are configured
88    pub fn is_empty(&self) -> bool {
89        self.permissions.is_empty()
90    }
91
92    /// Returns the number of configured permissions
93    pub fn len(&self) -> usize {
94        self.permissions.len()
95    }
96}
97
98/// Represents an access permission to a single path
99#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
100pub struct PathPermission {
101    /// Permission level to access the `path`
102    pub access: FsAccessPermission,
103    /// The targeted path guarded by the permission
104    pub path: PathBuf,
105}
106
107impl PathPermission {
108    /// Returns a new permission for the path and the given access
109    pub fn new(path: impl Into<PathBuf>, access: FsAccessPermission) -> Self {
110        Self { path: path.into(), access }
111    }
112
113    /// Returns a new read-only permission for the path
114    pub fn read(path: impl Into<PathBuf>) -> Self {
115        Self::new(path, FsAccessPermission::Read)
116    }
117
118    /// Returns a new read-write permission for the path
119    pub fn read_write(path: impl Into<PathBuf>) -> Self {
120        Self::new(path, FsAccessPermission::ReadWrite)
121    }
122
123    /// Returns a new write-only permission for the path
124    pub fn write(path: impl Into<PathBuf>) -> Self {
125        Self::new(path, FsAccessPermission::Write)
126    }
127
128    /// Returns a non permission for the path
129    pub fn none(path: impl Into<PathBuf>) -> Self {
130        Self::new(path, FsAccessPermission::None)
131    }
132
133    /// Returns true if the access is allowed
134    pub fn is_granted(&self, kind: FsAccessKind) -> bool {
135        self.access.is_granted(kind)
136    }
137}
138
139/// Represents the operation on the fs
140#[derive(Clone, Copy, Debug, PartialEq, Eq)]
141pub enum FsAccessKind {
142    /// read from fs (`vm.readFile`)
143    Read,
144    /// write to fs (`vm.writeFile`)
145    Write,
146}
147
148impl fmt::Display for FsAccessKind {
149    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
150        match self {
151            Self::Read => f.write_str("read"),
152            Self::Write => f.write_str("write"),
153        }
154    }
155}
156
157/// Determines the status of file system access
158#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
159pub enum FsAccessPermission {
160    /// FS access is _not_ allowed
161    #[default]
162    None,
163    /// FS access is allowed, this includes `read` + `write`
164    ReadWrite,
165    /// Only reading is allowed
166    Read,
167    /// Only writing is allowed
168    Write,
169}
170
171impl FsAccessPermission {
172    /// Returns true if the access is allowed
173    pub fn is_granted(&self, kind: FsAccessKind) -> bool {
174        match (self, kind) {
175            (Self::ReadWrite, _) => true,
176            (Self::None, _) => false,
177            (Self::Read, FsAccessKind::Read) => true,
178            (Self::Write, FsAccessKind::Write) => true,
179            _ => false,
180        }
181    }
182}
183
184impl FromStr for FsAccessPermission {
185    type Err = String;
186
187    fn from_str(s: &str) -> Result<Self, Self::Err> {
188        match s {
189            "true" | "read-write" | "readwrite" => Ok(Self::ReadWrite),
190            "false" | "none" => Ok(Self::None),
191            "read" => Ok(Self::Read),
192            "write" => Ok(Self::Write),
193            _ => Err(format!("Unknown variant {s}")),
194        }
195    }
196}
197
198impl fmt::Display for FsAccessPermission {
199    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
200        match self {
201            Self::ReadWrite => f.write_str("read-write"),
202            Self::None => f.write_str("none"),
203            Self::Read => f.write_str("read"),
204            Self::Write => f.write_str("write"),
205        }
206    }
207}
208
209impl Serialize for FsAccessPermission {
210    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
211    where
212        S: Serializer,
213    {
214        match self {
215            Self::ReadWrite => serializer.serialize_bool(true),
216            Self::None => serializer.serialize_bool(false),
217            Self::Read => serializer.serialize_str("read"),
218            Self::Write => serializer.serialize_str("write"),
219        }
220    }
221}
222
223impl<'de> Deserialize<'de> for FsAccessPermission {
224    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
225    where
226        D: Deserializer<'de>,
227    {
228        #[derive(Deserialize)]
229        #[serde(untagged)]
230        enum Status {
231            Bool(bool),
232            String(String),
233        }
234        match Status::deserialize(deserializer)? {
235            Status::Bool(enabled) => {
236                let status = if enabled { Self::ReadWrite } else { Self::None };
237                Ok(status)
238            }
239            Status::String(val) => val.parse().map_err(serde::de::Error::custom),
240        }
241    }
242}
243
244#[cfg(test)]
245mod tests {
246    use super::*;
247
248    #[test]
249    fn can_parse_permission() {
250        assert_eq!(FsAccessPermission::ReadWrite, "true".parse().unwrap());
251        assert_eq!(FsAccessPermission::ReadWrite, "readwrite".parse().unwrap());
252        assert_eq!(FsAccessPermission::ReadWrite, "read-write".parse().unwrap());
253        assert_eq!(FsAccessPermission::None, "false".parse().unwrap());
254        assert_eq!(FsAccessPermission::None, "none".parse().unwrap());
255        assert_eq!(FsAccessPermission::Read, "read".parse().unwrap());
256        assert_eq!(FsAccessPermission::Write, "write".parse().unwrap());
257    }
258
259    #[test]
260    fn nested_permissions() {
261        let permissions = FsPermissions::new(vec![
262            PathPermission::read("./"),
263            PathPermission::write("./out"),
264            PathPermission::read_write("./out/contracts"),
265        ]);
266
267        let permission =
268            permissions.find_permission(Path::new("./out/contracts/MyContract.sol")).unwrap();
269        assert_eq!(FsAccessPermission::ReadWrite, permission);
270        let permission = permissions.find_permission(Path::new("./out/MyContract.sol")).unwrap();
271        assert_eq!(FsAccessPermission::Write, permission);
272    }
273}