Skip to main content

mountpoint_s3_fs/metablock/
path.rs

1use std::ops::Deref;
2use std::{ffi::OsStr, fmt::Display};
3
4use thiserror::Error;
5
6use crate::s3::{Prefix, S3Path};
7use crate::sync::Arc;
8
9use super::{InodeError, InodeKind};
10
11/// Key associated with an Inode that can be lookedup.
12///
13/// May not include the [Prefix](super::Prefix). Guaranteed to end in '/' for directories.
14#[derive(Debug, Clone, Eq, PartialEq)]
15pub struct ValidKey {
16    key: Box<str>,
17    name_offset: usize,
18}
19
20#[derive(Debug, Error, Eq, PartialEq)]
21pub enum ValidKeyError {
22    #[error("not a directory key")]
23    NotADirectory,
24    #[error("invalid key {0:?}")]
25    InvalidKey(String),
26}
27
28impl ValidKey {
29    /// Create the root key.
30    pub fn root() -> Self {
31        Self {
32            key: Default::default(),
33            name_offset: 0,
34        }
35    }
36
37    /// Create a new child key.
38    pub fn new_child(&self, name: ValidName, kind: InodeKind) -> Result<Self, ValidKeyError> {
39        let InodeKind::Directory = self.kind() else {
40            return Err(ValidKeyError::NotADirectory);
41        };
42
43        let name_offset = self.key.len();
44        // Allocate the new string with the correct capacity.
45        let mut key =
46            String::with_capacity(name_offset + name.len() + if kind == InodeKind::Directory { 1 } else { 0 });
47        key.push_str(&self.key);
48        key.push_str(&name);
49        if kind == InodeKind::Directory {
50            key.push('/');
51        }
52
53        // No re-allocation required.
54        debug_assert_eq!(key.len(), key.capacity());
55        let key = key.into_boxed_str();
56        Ok(Self { name_offset, key })
57    }
58
59    // Create a new key including a [Prefix].
60    pub fn full_key(&self, prefix: &Prefix) -> Self {
61        let prefix = prefix.as_str();
62        let name_offset = self.name_offset + prefix.len();
63        let mut full_key = String::with_capacity(prefix.len() + self.key.len());
64        full_key.push_str(prefix);
65        full_key.push_str(&self.key);
66        Self {
67            key: full_key.into_boxed_str(),
68            name_offset,
69        }
70    }
71
72    /// The name for this key, i.e. the last path component.
73    ///
74    /// For directories, the name does not include the terminal '/'.
75    pub fn name(&self) -> &str {
76        let len = self.key.len();
77        if len == 0 {
78            return "";
79        }
80        if self.key.as_bytes()[len - 1] == b'/' {
81            &self.key[self.name_offset..(len - 1)]
82        } else {
83            &self.key[self.name_offset..]
84        }
85    }
86
87    /// The name for this key, i.e. the last path component.
88    ///
89    /// This returns a [ValidName] or [None] if name is empty.
90    pub fn valid_name(&self) -> Option<ValidName<'_>> {
91        let name = self.name();
92        if name.is_empty() { None } else { Some(ValidName(name)) }
93    }
94
95    /// The kind of [Inode](super::Inode) associated with this key.
96    pub fn kind(&self) -> InodeKind {
97        match self.key.as_bytes().last() {
98            None | Some(b'/') => InodeKind::Directory,
99            _ => InodeKind::File,
100        }
101    }
102
103    /// Path components for this key.
104    ///
105    /// For directories, this does not include empty component after the terminal '/'.
106    pub fn components(&self) -> Vec<ValidName<'_>> {
107        if self.key.is_empty() {
108            Default::default()
109        } else {
110            self.key.split_terminator('/').map(ValidName).collect()
111        }
112    }
113}
114
115impl Deref for ValidKey {
116    type Target = str;
117
118    fn deref(&self) -> &Self::Target {
119        &self.key
120    }
121}
122
123impl AsRef<str> for ValidKey {
124    fn as_ref(&self) -> &str {
125        &self.key
126    }
127}
128
129impl Display for ValidKey {
130    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
131        f.write_str(&self.key)
132    }
133}
134
135impl From<ValidKey> for String {
136    fn from(value: ValidKey) -> Self {
137        value.key.into_string()
138    }
139}
140
141impl TryFrom<String> for ValidKey {
142    type Error = ValidKeyError;
143
144    /// Constructs a valid key performing checks.
145    fn try_from(full_key: String) -> Result<Self, Self::Error> {
146        // validate
147        let mut last_component = None;
148        for component in full_key.split_terminator('/') {
149            if ValidName::parse_str(component).is_err() {
150                return Err(ValidKeyError::InvalidKey(full_key.to_string()));
151            }
152            last_component = Some(component);
153        }
154
155        // extract name
156        let is_dir = full_key.ends_with('/');
157        let name_len = last_component.map_or(0, |name| if is_dir { name.len() + 1 } else { name.len() });
158        let name_offset = full_key.len() - name_len;
159
160        Ok(Self {
161            key: full_key.into(),
162            name_offset,
163        })
164    }
165}
166
167/// Describes a location of a file or directory in S3
168#[derive(Debug, Clone)]
169pub struct S3Location {
170    pub path: Arc<S3Path>,
171    pub partial_key: ValidKey,
172}
173
174impl S3Location {
175    pub fn new(path: Arc<S3Path>, partial_key: ValidKey) -> Self {
176        Self { path, partial_key }
177    }
178
179    /// Get the bucket name
180    pub fn bucket_name(&self) -> &str {
181        &self.path.bucket
182    }
183
184    /// Get the full key
185    pub fn full_key(&self) -> ValidKey {
186        self.partial_key.full_key(&self.path.prefix)
187    }
188
189    pub fn name(&self) -> &str {
190        self.partial_key.name()
191    }
192}
193
194impl Display for S3Location {
195    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
196        write!(
197            f,
198            "{}{} (bucket: {})",
199            self.path.prefix, self.partial_key, self.path.bucket
200        )
201    }
202}
203
204/// A valid name for an [Inode](super::Inode).
205#[derive(Debug, Clone, Copy)]
206pub struct ValidName<'a>(&'a str);
207
208impl<'a> ValidName<'a> {
209    /// Parse a string into a [ValidName].
210    pub fn parse_os_str(name: &'a OsStr) -> Result<Self, InodeError> {
211        let name_str = name.to_str().ok_or_else(|| InodeError::InvalidFileName(name.into()))?;
212        Self::parse_str(name_str)
213    }
214
215    /// Parse a string into a [ValidName].
216    pub fn parse_str(name: &'a str) -> Result<Self, InodeError> {
217        // Names cannot be empty
218        if !name.is_empty() &&
219            // "." and ".." are reserved names (presented by the filesystem layer)
220            name != "." &&
221            name != ".." &&
222            // The delimiter / can never appear in a name
223            !name.as_bytes().contains(&b'/') &&
224            // NUL is invalid in POSIX names
225            !name.as_bytes().contains(&b'\0')
226        {
227            Ok(Self(name))
228        } else {
229            Err(InodeError::InvalidFileName(name.into()))
230        }
231    }
232}
233
234impl<'a> TryFrom<&'a OsStr> for ValidName<'a> {
235    type Error = InodeError;
236
237    fn try_from(value: &'a OsStr) -> Result<Self, Self::Error> {
238        Self::parse_os_str(value)
239    }
240}
241
242impl<'a> TryFrom<&'a str> for ValidName<'a> {
243    type Error = InodeError;
244
245    fn try_from(value: &'a str) -> Result<Self, Self::Error> {
246        Self::parse_str(value)
247    }
248}
249
250impl Deref for ValidName<'_> {
251    type Target = str;
252
253    fn deref(&self) -> &Self::Target {
254        self.0
255    }
256}
257
258impl AsRef<str> for ValidName<'_> {
259    fn as_ref(&self) -> &str {
260        self.0
261    }
262}
263
264#[cfg(test)]
265mod tests {
266    use std::{ffi::OsString, os::unix::ffi::OsStrExt as _};
267
268    use super::*;
269
270    use proptest::prelude::*;
271    use proptest_derive::Arbitrary;
272    use test_case::test_case;
273
274    fn test_key(components: Vec<Components>) {
275        let mut key_str = OsString::new();
276        let mut key = ValidKey::root();
277
278        for component in components {
279            if key.kind() == InodeKind::File {
280                _ = key
281                    .new_child(ValidName("test"), InodeKind::File)
282                    .expect_err("appending to a file should fail");
283                return;
284            }
285
286            assert!(valid_directory_key(key.as_ref()));
287
288            let kind = if component.is_directory {
289                InodeKind::Directory
290            } else {
291                InodeKind::File
292            };
293
294            let name = &component.name;
295            if !valid_inode_name(name) {
296                _ = ValidName::parse_os_str(name).expect_err("parsing an invalid name should fail");
297                return;
298            }
299
300            let valid_name = ValidName::parse_os_str(name).expect("name should be valid");
301            key = key
302                .new_child(valid_name, kind)
303                .expect("appending to a directory should succeed");
304
305            assert_eq!(key.kind(), kind);
306            assert_eq!(key.name(), name);
307
308            key_str.push(name);
309            if kind == InodeKind::Directory {
310                key_str.push("/");
311            }
312        }
313
314        assert_eq!(key_str, key.as_ref());
315    }
316
317    fn valid_directory_key(key: &str) -> bool {
318        key.is_empty() || key.ends_with('/')
319    }
320
321    fn valid_inode_name<T: AsRef<OsStr>>(name: T) -> bool {
322        let name = name.as_ref();
323        // Names cannot be empty
324        !name.is_empty() &&
325        // "." and ".." are reserved names (presented by the filesystem layer)
326        name != "." &&
327        name != ".." &&
328        // The delimiter / can never appear in a name
329        !name.as_bytes().contains(&b'/') &&
330        // NUL is invalid in POSIX names
331        !name.as_bytes().contains(&b'\0')
332    }
333
334    #[derive(Debug, Arbitrary)]
335    struct Components {
336        name: OsString,
337        is_directory: bool,
338    }
339
340    proptest! {
341        #[test]
342        fn proptest_valid_key(components: Vec<Components>) {
343            test_key(components);
344        }
345    }
346
347    #[test_case("dir1/a.txt", Ok(ValidKey{key: "dir1/a.txt".to_string().into(), name_offset: 5}); "file")]
348    #[test_case("dir1/dir2/", Ok(ValidKey{key: "dir1/dir2/".to_string().into(), name_offset: 5}); "dir")]
349    #[test_case("dir1/dir2", Ok(ValidKey{key: "dir1/dir2".to_string().into(), name_offset: 5}); "another file")]
350    #[test_case("", Ok(ValidKey{key: "".to_string().into(), name_offset: 0}); "empty")]
351    #[test_case("a", Ok(ValidKey{key: "a".to_string().into(), name_offset: 0}); "one char")]
352    #[test_case("dir1/dir2/dir3/a.txt", Ok(ValidKey{key: "dir1/dir2/dir3/a.txt".to_string().into(), name_offset: 15}); "many components")]
353    #[test_case("/", Err(ValidKeyError::InvalidKey("/".to_string())); "just /")]
354    #[test_case("dir1//a.txt", Err(ValidKeyError::InvalidKey("dir1//a.txt".to_string())); "empty component")]
355    #[test_case("dir1/../a.txt", Err(ValidKeyError::InvalidKey("dir1/../a.txt".to_string())); "invalid component")]
356    fn test_valid_key_try_from(source: &str, result: Result<ValidKey, ValidKeyError>) {
357        assert_eq!(ValidKey::try_from(source.to_string()), result);
358    }
359
360    #[test_case("", &[]; "empty key")]
361    #[test_case("file.txt", &["file.txt"]; "file key with single component")]
362    #[test_case("dir/", &["dir"]; "directory key with single component")]
363    #[test_case("dir1/dir2/file.txt", &["dir1", "dir2", "file.txt"]; "file key with multiple components")]
364    #[test_case("dir1/dir2/dir3/", &["dir1", "dir2", "dir3"]; "directory key with multiple components")]
365    fn test_valid_key_components(source: &str, expected_components: &[&str]) {
366        let key = ValidKey::try_from(source.to_string()).unwrap();
367        let components = key.components();
368
369        assert_eq!(components.len(), expected_components.len());
370        for (i, expected) in expected_components.iter().enumerate() {
371            assert_eq!(components[i].as_ref(), *expected);
372        }
373    }
374}