foundry_config/
fs_permissions.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
//! Support for controlling fs access

use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::{
    fmt,
    path::{Path, PathBuf},
    str::FromStr,
};

/// Configures file system access
///
/// E.g. for cheat codes (`vm.writeFile`)
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(transparent)]
pub struct FsPermissions {
    /// what kind of access is allowed
    pub permissions: Vec<PathPermission>,
}

impl FsPermissions {
    /// Creates anew instance with the given `permissions`
    pub fn new(permissions: impl IntoIterator<Item = PathPermission>) -> Self {
        Self { permissions: permissions.into_iter().collect() }
    }

    /// Adds a new permission
    pub fn add(&mut self, permission: PathPermission) {
        self.permissions.push(permission)
    }

    /// Returns true if access to the specified path is allowed with the specified.
    ///
    /// This first checks permission, and only if it is granted, whether the path is allowed.
    ///
    /// We only allow paths that are inside  allowed paths.
    ///
    /// Caution: This should be called with normalized paths if the `allowed_paths` are also
    /// normalized.
    pub fn is_path_allowed(&self, path: &Path, kind: FsAccessKind) -> bool {
        self.find_permission(path).map(|perm| perm.is_granted(kind)).unwrap_or_default()
    }

    /// Returns the permission for the matching path.
    ///
    /// This finds the longest matching path with resolved sym links, e.g. if we have the following
    /// permissions:
    ///
    /// `./out` = `read`
    /// `./out/contracts` = `read-write`
    ///
    /// And we check for `./out/contracts/MyContract.sol` we will get `read-write` as permission.
    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() {
                    // the longest path takes precedence
                    if perm.path < active_perm.path {
                        continue;
                    }
                }
                permission = Some(perm);
            }
        }
        permission.map(|perm| perm.access)
    }

    /// Updates all `allowed_paths` and joins ([`Path::join`]) the `root` with all entries
    pub fn join_all(&mut self, root: &Path) {
        self.permissions.iter_mut().for_each(|perm| {
            perm.path = root.join(&perm.path);
        })
    }

    /// Same as [`Self::join_all`] but consumes the type
    pub fn joined(mut self, root: &Path) -> Self {
        self.join_all(root);
        self
    }

    /// Removes all existing permissions for the given path
    pub fn remove(&mut self, path: &Path) {
        self.permissions.retain(|permission| permission.path != path)
    }

    /// Returns true if no permissions are configured
    pub fn is_empty(&self) -> bool {
        self.permissions.is_empty()
    }

    /// Returns the number of configured permissions
    pub fn len(&self) -> usize {
        self.permissions.len()
    }
}

/// Represents an access permission to a single path
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct PathPermission {
    /// Permission level to access the `path`
    pub access: FsAccessPermission,
    /// The targeted path guarded by the permission
    pub path: PathBuf,
}

impl PathPermission {
    /// Returns a new permission for the path and the given access
    pub fn new(path: impl Into<PathBuf>, access: FsAccessPermission) -> Self {
        Self { path: path.into(), access }
    }

    /// Returns a new read-only permission for the path
    pub fn read(path: impl Into<PathBuf>) -> Self {
        Self::new(path, FsAccessPermission::Read)
    }

    /// Returns a new read-write permission for the path
    pub fn read_write(path: impl Into<PathBuf>) -> Self {
        Self::new(path, FsAccessPermission::ReadWrite)
    }

    /// Returns a new write-only permission for the path
    pub fn write(path: impl Into<PathBuf>) -> Self {
        Self::new(path, FsAccessPermission::Write)
    }

    /// Returns a non permission for the path
    pub fn none(path: impl Into<PathBuf>) -> Self {
        Self::new(path, FsAccessPermission::None)
    }

    /// Returns true if the access is allowed
    pub fn is_granted(&self, kind: FsAccessKind) -> bool {
        self.access.is_granted(kind)
    }
}

/// Represents the operation on the fs
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum FsAccessKind {
    /// read from fs (`vm.readFile`)
    Read,
    /// write to fs (`vm.writeFile`)
    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"),
        }
    }
}

/// Determines the status of file system access
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum FsAccessPermission {
    /// FS access is _not_ allowed
    #[default]
    None,
    /// FS access is allowed, this includes `read` + `write`
    ReadWrite,
    /// Only reading is allowed
    Read,
    /// Only writing is allowed
    Write,
}

impl FsAccessPermission {
    /// Returns true if the access is allowed
    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);
    }
}