1pub mod error;
2mod validation;
3
4use crate::error::InvalidS3PathComponent;
5use std::borrow::Cow;
6use std::fmt::Formatter;
7use std::ops::Deref;
8use std::path::PathBuf;
9
10#[macro_export]
12macro_rules! s3_path {
13 ($($component:expr),*) => {{
14 let components = &[$(::std::borrow::Cow::Borrowed($component)),*];
15 S3Path::new(components)
16 }}
17}
18
19#[repr(transparent)]
23#[derive(PartialEq, Eq)]
24pub struct S3Path<'i>([Cow<'i, str>]);
25
26#[derive(Clone, PartialEq, Eq, Default)]
28pub struct S3PathBuf {
29 components: Vec<Cow<'static, str>>,
30}
31
32impl<'i> PartialEq<&S3Path<'i>> for S3PathBuf {
34 fn eq(&self, other: &&S3Path<'i>) -> bool {
35 self.as_path() == *other
36 }
37}
38
39impl<'i> PartialEq<S3PathBuf> for &S3Path<'i> {
41 fn eq(&self, other: &S3PathBuf) -> bool {
42 *self == other.as_path()
43 }
44}
45
46impl<'i> AsRef<S3Path<'i>> for S3Path<'i> {
47 fn as_ref(&self) -> &S3Path<'i> {
48 self
49 }
50}
51
52impl<'i> AsRef<S3Path<'i>> for S3PathBuf {
53 fn as_ref(&self) -> &S3Path<'i> {
54 self.deref()
55 }
56}
57
58impl AsRef<S3PathBuf> for S3PathBuf {
59 fn as_ref(&self) -> &S3PathBuf {
60 self
61 }
62}
63
64fn write_components<C: AsRef<str>>(
65 components: impl Iterator<Item = C>,
66 f: &mut Formatter,
67) -> Result<(), std::fmt::Error> {
68 for (i, c) in components.enumerate() {
69 if i > 0 {
70 f.write_str("/")?;
71 }
72 f.write_str(c.as_ref())?;
73 }
74 Ok(())
75}
76
77impl<'i> std::fmt::Display for S3Path<'i> {
78 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
79 write_components(self.0.iter(), f)?;
80 Ok(())
81 }
82}
83
84impl<'i> std::fmt::Debug for S3Path<'i> {
85 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
86 write_components(self.0.iter(), f)?;
87 Ok(())
88 }
89}
90
91impl std::fmt::Display for S3PathBuf {
92 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
93 write_components(self.components.iter(), f)?;
94 Ok(())
95 }
96}
97
98impl std::fmt::Debug for S3PathBuf {
99 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
100 write_components(self.components.iter(), f)?;
101 Ok(())
102 }
103}
104
105impl<'i> S3Path<'i> {
106 pub fn new(components: &'i [Cow<'i, str>]) -> Result<&'i S3Path<'i>, InvalidS3PathComponent> {
108 for component in components {
109 validation::validate_component(component)?;
110 }
111 Ok(unsafe { &*(components as *const [Cow<'i, str>] as *const S3Path<'i>) })
113 }
114
115 pub fn to_owned(&'i self) -> S3PathBuf {
117 S3PathBuf {
118 components: self.0.iter().map(|it| Cow::Owned(it.to_string())).collect(),
119 }
120 }
121
122 pub fn join<C: Into<Cow<'static, str>>>(
124 &self,
125 component: C,
126 ) -> Result<S3PathBuf, InvalidS3PathComponent> {
127 let mut path = self.to_owned();
128 path.join(component)?;
129 Ok(path)
130 }
131
132 pub fn is_empty(&'i self) -> bool {
134 self.0.is_empty()
135 }
136
137 pub fn len(&'i self) -> usize {
139 self.0.len()
140 }
141
142 pub fn components(&'i self) -> impl Iterator<Item = &'i str> {
144 self.0.iter().map(move |it| it.as_ref())
145 }
146
147 pub fn get(&'i self, index: usize) -> Option<&'i str> {
149 self.0.get(index).map(|it| it.as_ref())
150 }
151
152 pub fn last(&'i self) -> Option<&'i str> {
154 self.0.last().map(|it| it.as_ref())
155 }
156
157 pub fn parent(&'i self) -> Option<&'i S3Path<'i>> {
159 if self.0.is_empty() {
160 None
161 } else {
162 let parent_slice = &self.0[..self.0.len() - 1];
163 Some(
164 unsafe { &*(parent_slice as *const [Cow<'i, str>] as *const S3Path<'i>) },
166 )
167 }
168 }
169
170 pub fn to_std_path_buf(&self) -> PathBuf {
177 let mut path = PathBuf::new();
178 for c in &self.0 {
179 path = path.join(c.as_ref());
180 }
181 path
182 }
183}
184
185impl Deref for S3PathBuf {
187 type Target = S3Path<'static>;
188
189 fn deref(&self) -> &Self::Target {
190 unsafe {
193 &*(self.components.as_slice() as *const [Cow<'static, str>] as *const S3Path<'static>)
194 }
195 }
196}
197
198impl S3PathBuf {
199 pub fn new() -> Self {
200 Self::default()
201 }
202
203 pub fn try_from<C: Into<Cow<'static, str>>, I: IntoIterator<Item = C>>(
204 components: I,
205 ) -> Result<Self, InvalidS3PathComponent> {
206 let mut path = S3PathBuf::new();
207 for component in components {
208 path.join(component)?;
209 }
210 Ok(path)
211 }
212
213 pub fn try_from_str(string: impl AsRef<str>) -> Result<Self, InvalidS3PathComponent> {
214 let mut path = S3PathBuf::new();
215 for c in string.as_ref().split('/') {
216 if !c.is_empty() {
218 path.join(Cow::Owned(c.to_string()))?;
219 }
220 }
221 Ok(path)
222 }
223
224 pub fn join(
225 &mut self,
226 component: impl Into<Cow<'static, str>>,
227 ) -> Result<&mut Self, InvalidS3PathComponent> {
228 let comp = component.into();
229 validation::validate_component(&comp)?;
230 self.components.push(comp);
231 Ok(self)
232 }
233
234 #[must_use]
235 #[inline]
236 pub fn as_path(&self) -> &S3Path<'_> {
237 self.deref()
238 }
239
240 pub fn pop(&mut self) -> Option<Cow<'static, str>> {
242 self.components.pop()
243 }
244}
245
246impl TryFrom<&str> for S3PathBuf {
247 type Error = InvalidS3PathComponent;
248
249 fn try_from(value: &str) -> Result<Self, Self::Error> {
250 let mut path = S3PathBuf::new();
251 for c in value.split('/') {
252 if !c.is_empty() {
254 path.join(Cow::Owned(c.to_string()))?;
255 }
256 }
257 Ok(path)
258 }
259}
260
261impl TryFrom<String> for S3PathBuf {
262 type Error = InvalidS3PathComponent;
263
264 fn try_from(value: String) -> Result<Self, Self::Error> {
265 TryFrom::try_from(value.as_str())
266 }
267}
268
269#[cfg(test)]
270impl assertr::assertions::HasLength for S3PathBuf {
271 fn length(&self) -> usize {
272 self.len()
273 }
274}
275
276#[cfg(test)]
277mod test {
278 use crate::S3PathBuf;
279 use assertr::prelude::*;
280
281 mod s3_path_buf {
282 use crate::S3PathBuf;
283 use assertr::prelude::*;
284
285 #[test]
286 fn new_is_initially_empty() {
287 let path = S3PathBuf::new();
288 assert_that(path).has_display_value("");
289 }
290
291 #[test]
292 fn construct_using_new_and_join_components() {
293 let mut path = S3PathBuf::new();
294 path.join("foo").unwrap();
295 path.join("bar").unwrap();
296 assert_that(path).has_display_value("foo/bar");
297 }
298
299 #[test]
300 fn try_from_str_parses_empty_string_as_empty_path() {
301 let path = S3PathBuf::try_from_str("").unwrap();
302 assert_that(path).has_display_value("");
303 }
304
305 #[test]
306 fn try_from_str_parses_single_slash_as_empty_path() {
307 let path = S3PathBuf::try_from_str("/").unwrap();
308 assert_that(path).has_display_value("");
309 }
310
311 #[test]
312 fn try_from_str_removes_leading_and_trailing_slashes() {
313 let path = S3PathBuf::try_from_str("/foo/bar/").unwrap();
314 assert_that(path).has_display_value("foo/bar");
315 }
316
317 #[test]
318 fn try_from_str_ignores_repeated_slashes() {
319 let path = S3PathBuf::try_from_str("foo/////bar").unwrap();
320 assert_that(path).has_display_value("foo/bar");
321 }
322
323 #[test]
324 fn construct_using_try_from_given_str() {
325 let path = S3PathBuf::try_from_str("foo/bar").unwrap();
326 assert_that(path).has_display_value("foo/bar");
327 }
328
329 #[test]
330 fn construct_using_try_from_given_string() {
331 let path = S3PathBuf::try_from_str("foo/bar".to_string()).unwrap();
332 assert_that(path).has_display_value("foo/bar");
333 }
334
335 #[test]
336 fn try_from_static_str_array() {
337 let path = S3PathBuf::try_from(["foo", "bar"]).unwrap();
338 assert_that(path).has_display_value("foo/bar");
339 }
340
341 #[test]
342 fn try_from_string_array() {
343 let path = S3PathBuf::try_from(["foo".to_string(), "bar".to_string()]).unwrap();
344 assert_that(path).has_display_value("foo/bar");
345 }
346
347 #[test]
348 fn reject_invalid_characters() {
349 let mut path = S3PathBuf::new();
350 let result = path.join("invalid/path");
351 assert_that(result.is_err()).is_true();
352
353 let result = S3PathBuf::try_from_str("foo/bar$baz");
354 assert_that(result.is_err()).is_true();
355 }
356
357 #[test]
358 fn as_path() {
359 let path_buf = S3PathBuf::try_from(["foo", "bar"]).unwrap();
360 let path = path_buf.as_path();
361 assert_that(path).has_display_value("foo/bar");
362 let cloned = path.to_owned();
363 drop(path_buf);
364 assert_that(cloned).has_display_value("foo/bar");
365 }
366
367 #[test] fn is_empty_returns_true_when_path_has_no_components() {
369 let path_buf = S3PathBuf::new();
370 assert_that(path_buf.is_empty()).is_true();
371 }
372
373 #[test] fn is_empty_returns_false_when_path_has_components() {
375 let path_buf = S3PathBuf::try_from(["foo", "bar"]).unwrap();
376 assert_that(path_buf.is_empty()).is_false();
377 }
378
379 #[test] fn len_returns_number_of_components() {
381 let path_buf = S3PathBuf::try_from(["foo", "bar"]).unwrap();
382 assert_that(path_buf.len()).is_equal_to(2);
383 }
384
385 #[test] fn components_iterates_over_all_components() {
387 let path_buf = S3PathBuf::try_from(["foo", "bar"]).unwrap();
388 assert_that(path_buf.components()).contains_exactly(&["foo", "bar"]);
389 }
390
391 #[test] fn get_returns_component_at_index() {
393 let path_buf = S3PathBuf::try_from(["foo", "bar"]).unwrap();
394 assert_that(path_buf.get(1)).is_some().is_equal_to("bar");
395 }
396
397 #[test] fn last_returns_last_component() {
399 let path_buf = S3PathBuf::try_from(["foo", "bar"]).unwrap();
400 assert_that(path_buf.last()).is_some().is_equal_to("bar");
401 }
402
403 #[test] fn parent_returns_none_when_path_has_no_components() {
405 let path_buf = S3PathBuf::new();
406 assert_that(path_buf.parent()).is_none();
407 }
408
409 #[test] fn parent_returns_empty_component_when_path_has_only_one_component() {
411 let path_buf = S3PathBuf::try_from(["foo"]).unwrap();
412 assert_that(path_buf.parent())
413 .is_some()
414 .has_display_value("");
415 }
416
417 #[test] fn parent_returns_view_of_parent_path_when_path_has_multiple_components() {
419 let path_buf = S3PathBuf::try_from(["foo", "bar", "baz"]).unwrap();
420 assert_that(path_buf.parent())
421 .is_some()
422 .has_display_value("foo/bar");
423 }
424
425 #[test] fn to_std_path_buf_returns_empty_path_buf_when_s3_path_has_zero_components() {
427 let path_buf = S3PathBuf::new();
428 assert_that(path_buf.to_std_path_buf().display()).has_display_value("");
429 }
430
431 #[test] fn to_std_path_buf_joins_components_with_path_separator_does_not_add_slashes() {
433 let path_buf = S3PathBuf::try_from(["foo", "bar"]).unwrap();
434 assert_that(path_buf.to_std_path_buf().display()).has_display_value("foo/bar");
435 }
436 }
437
438 mod s3_path {
439 use crate::S3Path;
440 use assertr::prelude::*;
441 use std::borrow::Cow;
442
443 #[test]
444 fn macro_handles_zero_components() {
445 let path = s3_path!().unwrap();
446 assert_that(path).has_display_value("");
447 }
448
449 #[test]
450 fn macro_handles_one_component() {
451 let path = s3_path!("foo").unwrap();
452 assert_that(path).has_display_value("foo");
453 }
454
455 #[test]
456 fn macro_handles_multiple_components() {
457 let path = s3_path!("foo", "bar").unwrap();
458 assert_that(path).has_display_value("foo/bar");
459 }
460
461 #[test]
462 fn new_is_initially_empty() {
463 let path = S3Path::new(&[]).unwrap();
464 assert_that(path).has_display_value("");
465 }
466
467 #[test]
468 fn construct_using_new_and_join_components() {
469 let path = S3Path::new(&[Cow::Borrowed("foo"), Cow::Borrowed("bar")]).unwrap();
470 assert_that(path).has_display_value("foo/bar");
471 }
472
473 #[test]
474 fn to_owned_converts_to_owned_path() {
475 let path = s3_path!("foo", "bar").unwrap();
476 let path_owned = path.to_owned();
477 assert_that(path_owned).has_display_value("foo/bar");
478 }
479
480 #[test]
481 fn join_converts_to_owned_path_and_appends_component() {
482 let path = s3_path!("foo", "bar").unwrap();
483 let path_owned = path.join("baz").unwrap();
484 assert_that(path_owned).has_display_value("foo/bar/baz");
485 }
486 }
487
488 mod validation {
489 use crate::S3PathBuf;
490 use assertr::prelude::*;
491
492 #[test]
493 fn reject_invalid_characters() {
494 assert_that(S3PathBuf::try_from(["foo-bar"]))
495 .is_ok()
496 .has_display_value("foo-bar");
497 assert_that(S3PathBuf::try_from(["foo_bar"]))
498 .is_ok()
499 .has_display_value("foo_bar");
500 assert_that(S3PathBuf::try_from(["foo.bar"]))
501 .is_ok()
502 .has_display_value("foo.bar");
503 assert_that(S3PathBuf::try_from([".test"]))
504 .is_ok()
505 .has_display_value(".test");
506 assert_that(S3PathBuf::try_from(["..test"]))
507 .is_ok()
508 .has_display_value("..test");
509
510 assert_that(S3PathBuf::try_from(["foo:bar"])).is_err();
511 assert_that(S3PathBuf::try_from(["foo;bar"])).is_err();
512 assert_that(S3PathBuf::try_from(["foo$bar"])).is_err();
513 assert_that(S3PathBuf::try_from(["foo&bar"])).is_err();
514 assert_that(S3PathBuf::try_from(["foo#bar"])).is_err();
515 assert_that(S3PathBuf::try_from(["foo/bar"])).is_err();
516 assert_that(S3PathBuf::try_from(["foo|bar"])).is_err();
517 assert_that(S3PathBuf::try_from(["foo\\bar"])).is_err();
518 assert_that(S3PathBuf::try_from(["."])).is_err();
519 assert_that(S3PathBuf::try_from([".."])).is_err();
520 }
521 }
522
523 mod take_any_path {
524 use crate::{S3Path, S3PathBuf};
525
526 fn take_any_path<'p>(path: impl AsRef<S3Path<'p>>) {
527 let _path: &S3Path<'p> = path.as_ref();
528 }
529
530 #[test]
531 fn takes_owned_s3_path_buf() {
532 take_any_path(S3PathBuf::new());
533 }
534
535 #[test]
536 fn takes_borrowed_s3_path_buf() {
537 take_any_path(&S3PathBuf::new());
538 }
539
540 #[test]
541 fn takes_s3_path() {
542 take_any_path(S3Path::new(&[]).unwrap());
543 }
544 }
545
546 #[test]
547 fn comparison_between_types() {
548 let path_buf = S3PathBuf::try_from(["foo", "bar"]).unwrap();
549 let path = path_buf.as_path();
550
551 assert_that(path == path_buf).is_true();
552 assert_that(path_buf == path).is_true();
553 }
554}