mountpoint_s3_fs/metablock/
path.rs1use 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#[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 pub fn root() -> Self {
31 Self {
32 key: Default::default(),
33 name_offset: 0,
34 }
35 }
36
37 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 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 debug_assert_eq!(key.len(), key.capacity());
55 let key = key.into_boxed_str();
56 Ok(Self { name_offset, key })
57 }
58
59 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 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 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 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 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 fn try_from(full_key: String) -> Result<Self, Self::Error> {
146 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 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#[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 pub fn bucket_name(&self) -> &str {
181 &self.path.bucket
182 }
183
184 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#[derive(Debug, Clone, Copy)]
206pub struct ValidName<'a>(&'a str);
207
208impl<'a> ValidName<'a> {
209 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 pub fn parse_str(name: &'a str) -> Result<Self, InodeError> {
217 if !name.is_empty() &&
219 name != "." &&
221 name != ".." &&
222 !name.as_bytes().contains(&b'/') &&
224 !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 !name.is_empty() &&
325 name != "." &&
327 name != ".." &&
328 !name.as_bytes().contains(&b'/') &&
330 !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}