Skip to main content

s3_path/
lib.rs

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/// Var-arg macro to create an `S3Path`, borrowing from the given string literals.
11///
12/// ```
13/// use s3_path::s3_path;
14///
15/// let path = s3_path!("foo", "bar").unwrap();
16/// ```
17#[macro_export]
18macro_rules! s3_path {
19    ($($component:expr),* $(,)?) => {{
20        let components = &[$(::std::borrow::Cow::Borrowed($component)),*];
21        $crate::S3Path::new(components)
22    }}
23}
24
25/// Var-arg macro to create an `S3Path` from individual components.
26///
27/// ```
28/// use std::borrow::Cow;
29/// use s3_path::s3_path_buf;
30///
31/// let path = s3_path_buf!("foo", String::from("bar"), Cow::Borrowed("baz")).unwrap();
32/// ```
33///
34/// Every component passed into this macro must either
35/// - be a `&'static str`
36/// - an owned `String`
37/// - or a `Cow<'static, str>`
38///
39/// str-slices of arbitrary lifetime cannot be used, as an `S3PathBuf` requires ownership,
40/// and this API enforces that any allocation, if needed, is performed at call-site.
41#[macro_export]
42macro_rules! s3_path_buf {
43    ($($component:expr),* $(,)?) => {{
44        #[allow(unused_mut)] // In case zero components are passed in.
45        let mut path = $crate::S3PathBuf::new();
46        #[allow(unused_mut)] // In case zero components are passed in.
47        let mut error = None;
48        $(
49            if error.is_none() {
50                match path.push($component) {
51                    Ok(_) => {},
52                    Err(e) => error = Some(e),
53                }
54            }
55        )*
56        match error {
57            Some(err) => Result::<$crate::S3PathBuf, $crate::error::InvalidS3PathComponent>::Err(err),
58            None => Result::<$crate::S3PathBuf, $crate::error::InvalidS3PathComponent>::Ok(path),
59        }
60    }}
61}
62
63/// A borrowed, unsized S3 storage path.
64///
65// Must be repr(transparent) to safely convert from the slice.
66#[repr(transparent)]
67#[derive(PartialEq, Eq)]
68pub struct S3Path<'i>([Cow<'i, str>]);
69
70/// An owned S3 storage path.
71#[derive(Clone, PartialEq, Eq, Default)]
72pub struct S3PathBuf {
73    components: Vec<Cow<'static, str>>,
74}
75
76/// Allow comparisons between `S3Path` and `S3PathBuf`.
77impl PartialEq<S3Path<'_>> for S3PathBuf {
78    fn eq(&self, other: &S3Path<'_>) -> bool {
79        self.as_path() == other
80    }
81}
82
83/// Allow comparisons between `S3Path` and `S3PathBuf`.
84impl<'i> PartialEq<&S3Path<'i>> for S3PathBuf {
85    fn eq(&self, other: &&S3Path<'i>) -> bool {
86        self.as_path() == *other
87    }
88}
89
90/// Allow comparisons between `S3Path` and `S3PathBuf`.
91impl<'i> PartialEq<&&S3Path<'i>> for S3PathBuf {
92    fn eq(&self, other: &&&S3Path<'i>) -> bool {
93        self.as_path() == **other
94    }
95}
96
97/// Allow comparisons between `S3Path` and `S3PathBuf`.
98impl PartialEq<S3PathBuf> for S3Path<'_> {
99    fn eq(&self, other: &S3PathBuf) -> bool {
100        self == other.as_path()
101    }
102}
103
104/// Allow comparisons between `S3Path` and `S3PathBuf`.
105impl PartialEq<S3PathBuf> for &S3Path<'_> {
106    fn eq(&self, other: &S3PathBuf) -> bool {
107        *self == other.as_path()
108    }
109}
110
111/// Allow comparisons between `S3Path` and `S3PathBuf`.
112impl PartialEq<S3PathBuf> for &&S3Path<'_> {
113    fn eq(&self, other: &S3PathBuf) -> bool {
114        **self == other.as_path()
115    }
116}
117
118impl<'i> AsRef<S3Path<'i>> for S3Path<'i> {
119    fn as_ref(&self) -> &S3Path<'i> {
120        self
121    }
122}
123
124impl<'i> AsRef<S3Path<'i>> for S3PathBuf {
125    fn as_ref(&self) -> &S3Path<'i> {
126        self
127    }
128}
129
130impl AsRef<S3PathBuf> for S3PathBuf {
131    fn as_ref(&self) -> &S3PathBuf {
132        self
133    }
134}
135
136fn write_components<C: AsRef<str>>(
137    components: impl Iterator<Item = C>,
138    f: &mut Formatter,
139) -> Result<(), std::fmt::Error> {
140    for (i, c) in components.enumerate() {
141        if i > 0 {
142            f.write_str("/")?;
143        }
144        f.write_str(c.as_ref())?;
145    }
146    Ok(())
147}
148
149impl std::fmt::Display for S3Path<'_> {
150    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
151        write_components(self.0.iter(), f)?;
152        Ok(())
153    }
154}
155
156impl std::fmt::Debug for S3Path<'_> {
157    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
158        write_components(self.0.iter(), f)?;
159        Ok(())
160    }
161}
162
163impl std::fmt::Display for S3PathBuf {
164    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
165        write_components(self.components.iter(), f)?;
166        Ok(())
167    }
168}
169
170impl std::fmt::Debug for S3PathBuf {
171    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
172        write_components(self.components.iter(), f)?;
173        Ok(())
174    }
175}
176
177impl<'i> S3Path<'i> {
178    /// Create a new `S3Path` from a slice of static `components`.
179    ///
180    /// # Errors
181    ///
182    /// Returns `Err` when any given `component`
183    /// - is empty
184    /// - contains characters other than: ascii alphanumeric characters, '-', '_' and '.'
185    /// - is equal to `.` or `..`
186    pub fn new(components: &'i [Cow<'i, str>]) -> Result<&'i S3Path<'i>, InvalidS3PathComponent> {
187        for component in components {
188            validation::validate_component(component)?;
189        }
190        // Safety: S3Path is repr(transparent) over [Cow<'i, str>].
191        Ok(unsafe { &*(std::ptr::from_ref::<[Cow<'i, str>]>(components) as *const S3Path<'i>) })
192    }
193
194    /// Converts to an owned `S3PathBuf`.
195    #[must_use]
196    pub fn to_owned(&'i self) -> S3PathBuf {
197        S3PathBuf {
198            components: self.0.iter().map(|it| Cow::Owned(it.to_string())).collect(),
199        }
200    }
201
202    /// Converts to an owned `S3PathBuf` and appends `component` to it after validating it.
203    ///
204    /// # Errors
205    ///
206    /// Returns `Err` when the given `component`
207    /// - is empty
208    /// - contains characters other than: ascii alphanumeric characters, '-', '_' and '.'
209    /// - is equal to `.` or `..`
210    pub fn join<C: Into<Cow<'static, str>>>(
211        &self,
212        component: C,
213    ) -> Result<S3PathBuf, InvalidS3PathComponent> {
214        let mut path = self.to_owned();
215        path.push(component)?;
216        Ok(path)
217    }
218
219    /// Returns true if this path has no components.
220    #[must_use]
221    pub fn is_empty(&'i self) -> bool {
222        self.0.is_empty()
223    }
224
225    /// Returns the number of components in this path.
226    #[must_use]
227    pub fn len(&'i self) -> usize {
228        self.0.len()
229    }
230
231    /// Returns an iterator over the components of this path.
232    pub fn components(&'i self) -> impl Iterator<Item = &'i str> {
233        self.0.iter().map(std::convert::AsRef::as_ref)
234    }
235
236    /// Returns the component at the given index, or None if the index is out of bounds.
237    pub fn get(&'i self, index: usize) -> Option<&'i str> {
238        self.0.get(index).map(std::convert::AsRef::as_ref)
239    }
240
241    /// Returns the last component of this path, or None if the path is empty.
242    pub fn last(&'i self) -> Option<&'i str> {
243        self.0.last().map(std::convert::AsRef::as_ref)
244    }
245
246    /// Returns all but the last component of this path, or None if the path is empty.
247    #[must_use]
248    pub fn parent(&'i self) -> Option<&'i S3Path<'i>> {
249        if self.0.is_empty() {
250            None
251        } else {
252            let parent_slice = &self.0[..self.0.len() - 1];
253            Some(
254                // Safety: S3Path is repr(transparent) over [Cow<'i, str>]
255                unsafe {
256                    &*(std::ptr::from_ref::<[Cow<'i, str>]>(parent_slice) as *const S3Path<'i>)
257                },
258            )
259        }
260    }
261
262    /// Convert this S3 path to a `std::path::PathBuf`, allowing you to use this S3 path as a
263    /// system file path.
264    ///
265    /// Our strong guarantee that path components only consist of ascii alphanumeric characters,
266    /// '-', '_' and '.' and that no path traversal components ('.' and '..') are allowed, makes
267    /// this a safe operation.
268    #[must_use]
269    pub fn to_std_path_buf(&self) -> PathBuf {
270        let mut path = PathBuf::new();
271        for c in &self.0 {
272            path.push(c.as_ref());
273        }
274        path
275    }
276}
277
278// Deref - NameBuf can be automatically converted to &Name<'static>
279impl Deref for S3PathBuf {
280    type Target = S3Path<'static>;
281
282    fn deref(&self) -> &Self::Target {
283        // Safety: This is safe because Name is repr(transparent) over [Cow<'i, str>] and we
284        // store [Cow<'static>, str>], with 'static outliving any lifetime 'i.
285        unsafe {
286            &*(std::ptr::from_ref::<[Cow<'static, str>]>(self.components.as_slice())
287                as *const S3Path<'static>)
288        }
289    }
290}
291
292impl S3PathBuf {
293    /// Creates an empty S3 path. Call `join` or `push` to extend it with additional path segments.
294    #[must_use]
295    pub fn new() -> Self {
296        Self::default()
297    }
298
299    /// Validates and adds all `components` to the returned `S3PathBuf`.
300    ///
301    /// NO component is parsed for slashes ('/') to be split up further!
302    ///
303    /// # Errors
304    ///
305    /// Returns `Err` when any given component
306    /// - is empty
307    /// - contains characters other than: ascii alphanumeric characters, '-', '_' and '.'
308    /// - is equal to `.` or `..`
309    pub fn try_from<C: Into<Cow<'static, str>>, I: IntoIterator<Item = C>>(
310        components: I,
311    ) -> Result<Self, InvalidS3PathComponent> {
312        let mut path = S3PathBuf::new();
313        for component in components {
314            path.push(component)?;
315        }
316        Ok(path)
317    }
318
319    /// Splits `string` at each occurrence of a `/`, then validates and add all components to the
320    /// returned `S3PathBuf`.
321    ///
322    /// Multiple consecutive slashes, as in "foo//bar", are treated as one.
323    ///
324    /// # Errors
325    ///
326    /// Returns `Err` when any component read
327    /// - contains characters other than: ascii alphanumeric characters, '-', '_' and '.'
328    /// - is equal to `.` or `..`
329    pub fn try_from_str(string: impl AsRef<str>) -> Result<Self, InvalidS3PathComponent> {
330        let mut path = S3PathBuf::new();
331        for c in string.as_ref().split('/') {
332            // Skip empty components from consecutive slashes
333            if !c.is_empty() {
334                path.push(Cow::Owned(c.to_string()))?;
335            }
336        }
337        Ok(path)
338    }
339
340    /// Extend the last component of the path, if there is one, with [addition].
341    ///
342    /// If this path is empty, pushes [addition] the initial path component instead.
343    ///
344    /// ```
345    /// use s3_path::S3PathBuf;
346    /// let mut file = S3PathBuf::try_from_str("my/file").unwrap();
347    /// file.extend(".txt").unwrap();
348    /// ```
349    ///
350    /// # Errors
351    ///
352    /// Returns `Err` when the given component
353    /// - is empty
354    /// - contains characters other than: ascii alphanumeric characters, '-', '_' and '.'
355    /// - is equal to `.` or `..`
356    pub fn extend(
357        &mut self,
358        addition: impl Into<Cow<'static, str>>,
359    ) -> Result<&mut Self, InvalidS3PathComponent> {
360        let addition = addition.into();
361
362        match self.components.pop() {
363            Some(last) => {
364                let combined = format!("{}{}", last, addition);
365                validation::validate_component(&combined)?;
366                self.components.push(Cow::Owned(combined))
367            }
368            None => {
369                validation::validate_component(&addition)?;
370                self.components.push(addition)
371            }
372        }
373
374        Ok(self)
375    }
376
377    /// Adds `component` to the path after validating it.
378    ///
379    /// # Errors
380    ///
381    /// Returns `Err` when the given component
382    /// - is empty
383    /// - contains characters other than: ascii alphanumeric characters, '-', '_' and '.'
384    /// - is equal to `.` or `..`
385    pub fn push(
386        &mut self,
387        component: impl Into<Cow<'static, str>>,
388    ) -> Result<&mut Self, InvalidS3PathComponent> {
389        let comp = component.into();
390        validation::validate_component(&comp)?;
391        self.components.push(comp);
392        Ok(self)
393    }
394
395    /// Clones this path and pushes [component] onto it.
396    ///
397    /// Leaves this path untouched. Great for quickly creating multiple paths having the same root.
398    pub fn join(
399        &self,
400        component: impl Into<Cow<'static, str>>,
401    ) -> Result<Self, InvalidS3PathComponent> {
402        let mut clone = self.clone();
403        clone.push(component)?;
404        Ok(clone)
405    }
406
407    #[must_use]
408    #[inline]
409    pub fn as_path(&self) -> &S3Path<'_> {
410        self
411    }
412
413    /// Pop the last component from this path, returning true if a component was removed
414    pub fn pop(&mut self) -> Option<Cow<'static, str>> {
415        self.components.pop()
416    }
417}
418
419#[cfg(test)]
420impl assertr::assertions::HasLength for S3PathBuf {
421    fn length(&self) -> usize {
422        self.len()
423    }
424}
425
426#[cfg(test)]
427mod test {
428    use crate::S3PathBuf;
429    use assertr::prelude::*;
430
431    mod s3_path_buf {
432        use crate::S3PathBuf;
433        use assertr::prelude::*;
434
435        #[test]
436        fn new_is_initially_empty() {
437            let path = S3PathBuf::new();
438            assert_that(path).has_display_value("");
439        }
440
441        #[test]
442        fn construct_using_new_and_push_components() {
443            let mut path = S3PathBuf::new();
444            path.push("foo").unwrap();
445            path.push("bar").unwrap();
446            assert_that(path).has_display_value("foo/bar");
447        }
448
449        #[test]
450        fn try_from_str_parses_empty_string_as_empty_path() {
451            let path = S3PathBuf::try_from_str("").unwrap();
452            assert_that(path).has_display_value("");
453        }
454
455        #[test]
456        fn try_from_str_parses_single_slash_as_empty_path() {
457            let path = S3PathBuf::try_from_str("/").unwrap();
458            assert_that(path).has_display_value("");
459        }
460
461        #[test]
462        fn try_from_str_removes_leading_and_trailing_slashes() {
463            let path = S3PathBuf::try_from_str("/foo/bar/").unwrap();
464            assert_that(path).has_display_value("foo/bar");
465        }
466
467        #[test]
468        fn try_from_str_ignores_repeated_slashes() {
469            let path = S3PathBuf::try_from_str("foo/////bar").unwrap();
470            assert_that(path).has_display_value("foo/bar");
471        }
472
473        #[test]
474        fn construct_using_try_from_given_str() {
475            let path = S3PathBuf::try_from_str("foo/bar").unwrap();
476            assert_that(path).has_display_value("foo/bar");
477        }
478
479        #[test]
480        fn construct_using_try_from_given_string() {
481            let path = S3PathBuf::try_from_str(String::from("foo/bar")).unwrap();
482            assert_that(path).has_display_value("foo/bar");
483        }
484
485        #[test]
486        fn try_from_static_str_array() {
487            let path = S3PathBuf::try_from(["foo", "bar"]).unwrap();
488            assert_that(path).has_display_value("foo/bar");
489        }
490
491        #[test]
492        fn try_from_string_array() {
493            let path = S3PathBuf::try_from(["foo".to_string(), "bar".to_string()]).unwrap();
494            assert_that(path).has_display_value("foo/bar");
495        }
496
497        #[test]
498        fn reject_invalid_characters() {
499            let mut path = S3PathBuf::new();
500            let result = path.push("invalid/path");
501            assert_that(result).is_err();
502
503            let result = S3PathBuf::try_from_str("foo/bar$baz");
504            assert_that(result).is_err();
505        }
506
507        #[test]
508        fn extend_mutates_original() {
509            let mut foo = S3PathBuf::try_from_str("foo").unwrap();
510            foo.extend(".txt").unwrap();
511
512            assert_that(foo).has_display_value("foo.txt");
513        }
514
515        #[test]
516        fn push_mutates_original() {
517            let mut foo = S3PathBuf::try_from_str("foo").unwrap();
518            let foo_bar = foo.push("bar").unwrap();
519
520            assert_that(foo_bar).has_display_value("foo/bar");
521            assert_that(foo).has_display_value("foo/bar");
522        }
523
524        #[test]
525        fn join_creates_clone() {
526            let foo = S3PathBuf::try_from_str("foo").unwrap();
527            let foo_bar = foo.join("bar").unwrap();
528
529            assert_that(foo).has_display_value("foo");
530            assert_that(foo_bar).has_display_value("foo/bar");
531        }
532
533        #[test]
534        fn as_path() {
535            let path_buf = S3PathBuf::try_from(["foo", "bar"]).unwrap();
536            let path = path_buf.as_path();
537            assert_that(path).has_display_value("foo/bar");
538            let cloned = path.to_owned();
539            drop(path_buf);
540            assert_that(cloned).has_display_value("foo/bar");
541        }
542
543        #[test] // Function `is_empty` inherited through deref to S3Path!
544        fn is_empty_returns_true_when_path_has_no_components() {
545            let path_buf = S3PathBuf::new();
546            assert_that(path_buf.is_empty()).is_true();
547        }
548
549        #[test] // Function `is_empty` inherited through deref to S3Path!
550        fn is_empty_returns_false_when_path_has_components() {
551            let path_buf = S3PathBuf::try_from(["foo", "bar"]).unwrap();
552            assert_that(path_buf.is_empty()).is_false();
553        }
554
555        #[test] // Function `len` inherited through deref to S3Path!
556        fn len_returns_number_of_components() {
557            let path_buf = S3PathBuf::try_from(["foo", "bar"]).unwrap();
558            assert_that(path_buf.len()).is_equal_to(2);
559        }
560
561        #[test] // Function `components` inherited through deref to S3Path!
562        fn components_iterates_over_all_components() {
563            let path_buf = S3PathBuf::try_from(["foo", "bar"]).unwrap();
564            assert_that(path_buf.components()).contains_exactly(["foo", "bar"]);
565        }
566
567        #[test] // Function `get` inherited through deref to S3Path!
568        fn get_returns_component_at_index() {
569            let path_buf = S3PathBuf::try_from(["foo", "bar"]).unwrap();
570            assert_that(path_buf.get(1)).is_some().is_equal_to("bar");
571        }
572
573        #[test] // Function `last` inherited through deref to S3Path!
574        fn last_returns_last_component() {
575            let path_buf = S3PathBuf::try_from(["foo", "bar"]).unwrap();
576            assert_that(path_buf.last()).is_some().is_equal_to("bar");
577        }
578
579        #[test] // Function `parent` inherited through deref to S3Path!
580        fn parent_returns_none_when_path_has_no_components() {
581            let path_buf = S3PathBuf::new();
582            assert_that(path_buf.parent()).is_none();
583        }
584
585        #[test] // Function `parent` inherited through deref to S3Path!
586        fn parent_returns_empty_component_when_path_has_only_one_component() {
587            let path_buf = S3PathBuf::try_from(["foo"]).unwrap();
588            assert_that(path_buf.parent())
589                .is_some()
590                .has_display_value("");
591        }
592
593        #[test] // Function `parent` inherited through deref to S3Path!
594        fn parent_returns_view_of_parent_path_when_path_has_multiple_components() {
595            let path_buf = S3PathBuf::try_from(["foo", "bar", "baz"]).unwrap();
596            assert_that(path_buf.parent())
597                .is_some()
598                .has_display_value("foo/bar");
599        }
600
601        #[test] // Function `to_std_path_buf` inherited through deref to S3Path!
602        fn to_std_path_buf_returns_empty_path_buf_when_s3_path_has_zero_components() {
603            let path_buf = S3PathBuf::new();
604            assert_that(path_buf.to_std_path_buf().display()).has_display_value("");
605        }
606
607        #[test] // Function `to_std_path_buf` inherited through deref to S3Path!
608        fn to_std_path_buf_joins_components_with_path_separator_does_not_add_slashes() {
609            let path_buf = S3PathBuf::try_from(["foo", "bar"]).unwrap();
610            assert_that(path_buf.to_std_path_buf().display()).has_display_value("foo/bar");
611        }
612
613        mod s3_path_buf_macro {
614            use assertr::prelude::*;
615            use std::borrow::Cow;
616
617            #[test]
618            fn taking_nothing() {
619                let path = s3_path_buf!().unwrap();
620                assert_that(path).has_display_value("");
621            }
622
623            #[test]
624            fn taking_single_static_str() {
625                let path = s3_path_buf!("foo").unwrap();
626                assert_that(path).has_display_value("foo");
627            }
628
629            #[test]
630            fn taking_owned_string_and_static_str() {
631                let foo = String::from("foo");
632                let path = s3_path_buf!(foo, "bar").unwrap();
633                assert_that(path).has_display_value("foo/bar");
634            }
635
636            #[test]
637            fn taking_owned_string_and_static_str_and_cow() {
638                let foo = String::from("foo");
639                let path = s3_path_buf!(foo, "bar", Cow::Borrowed("baz")).unwrap();
640                assert_that(path).has_display_value("foo/bar/baz");
641            }
642
643            #[test]
644            fn allows_a_trailing_comma() {
645                let path = s3_path_buf!("foo",).unwrap();
646                assert_that(path).has_display_value("foo");
647            }
648        }
649    }
650
651    mod s3_path {
652        use crate::S3Path;
653        use assertr::prelude::*;
654        use std::borrow::Cow;
655
656        #[test]
657        fn new_is_initially_empty() {
658            let path = S3Path::new(&[]).unwrap();
659            assert_that(path).has_display_value("");
660        }
661
662        #[test]
663        fn construct_using_new() {
664            let path = S3Path::new(&[Cow::Borrowed("foo"), Cow::Borrowed("bar")]).unwrap();
665            assert_that(path).has_display_value("foo/bar");
666        }
667
668        #[test]
669        fn to_owned_converts_to_owned_path() {
670            let path = s3_path!("foo", "bar").unwrap();
671            let path_owned = path.to_owned();
672            assert_that(path_owned).has_display_value("foo/bar");
673        }
674
675        #[test]
676        fn join_converts_to_owned_path_and_appends_component() {
677            let path = s3_path!("foo", "bar").unwrap();
678            let path_owned = path.join("baz").unwrap();
679            assert_that(path_owned).has_display_value("foo/bar/baz");
680        }
681
682        mod s3_path_macro {
683            use assertr::prelude::*;
684
685            #[test]
686            fn handles_zero_components() {
687                let path = s3_path!().unwrap();
688                assert_that(path).has_display_value("");
689            }
690
691            #[test]
692            fn handles_one_component() {
693                let path = s3_path!("foo").unwrap();
694                assert_that(path).has_display_value("foo");
695            }
696
697            #[test]
698            fn handles_multiple_components() {
699                let path = s3_path!("foo", "bar").unwrap();
700                assert_that(path).has_display_value("foo/bar");
701            }
702
703            #[test]
704            fn allows_a_trailing_comma() {
705                let path = s3_path!("foo",).unwrap();
706                assert_that(path).has_display_value("foo");
707            }
708        }
709    }
710
711    mod validation {
712        use crate::S3PathBuf;
713        use assertr::prelude::*;
714
715        #[test]
716        fn reject_invalid_characters() {
717            assert_that(S3PathBuf::try_from(["foo-bar"]))
718                .is_ok()
719                .has_display_value("foo-bar");
720            assert_that(S3PathBuf::try_from(["foo_bar"]))
721                .is_ok()
722                .has_display_value("foo_bar");
723            assert_that(S3PathBuf::try_from(["foo.bar"]))
724                .is_ok()
725                .has_display_value("foo.bar");
726            assert_that(S3PathBuf::try_from([".test"]))
727                .is_ok()
728                .has_display_value(".test");
729            assert_that(S3PathBuf::try_from(["..test"]))
730                .is_ok()
731                .has_display_value("..test");
732
733            assert_that(S3PathBuf::try_from(["foo:bar"])).is_err();
734            assert_that(S3PathBuf::try_from(["foo;bar"])).is_err();
735            assert_that(S3PathBuf::try_from(["foo$bar"])).is_err();
736            assert_that(S3PathBuf::try_from(["foo&bar"])).is_err();
737            assert_that(S3PathBuf::try_from(["foo#bar"])).is_err();
738            assert_that(S3PathBuf::try_from(["foo/bar"])).is_err();
739            assert_that(S3PathBuf::try_from(["foo|bar"])).is_err();
740            assert_that(S3PathBuf::try_from(["foo\\bar"])).is_err();
741            assert_that(S3PathBuf::try_from(["."])).is_err();
742            assert_that(S3PathBuf::try_from([".."])).is_err();
743        }
744    }
745
746    mod take_any_path {
747        use crate::{S3Path, S3PathBuf};
748
749        fn take_any_path<'p>(path: impl AsRef<S3Path<'p>>) {
750            let _path: &S3Path<'p> = path.as_ref();
751        }
752
753        #[test]
754        fn takes_owned_s3_path_buf() {
755            take_any_path(S3PathBuf::new());
756        }
757
758        #[test]
759        fn takes_borrowed_s3_path_buf() {
760            take_any_path(&S3PathBuf::new());
761        }
762
763        #[test]
764        fn takes_s3_path() {
765            take_any_path(S3Path::new(&[]).unwrap());
766        }
767    }
768
769    #[test]
770    #[allow(clippy::op_ref)]
771    fn comparison_between_types() {
772        let path_buf = S3PathBuf::try_from(["foo", "bar"]).unwrap();
773        let path = path_buf.as_path();
774
775        assert_that(path == path).is_true();
776        assert_that(path_buf == path_buf).is_true();
777
778        assert_that(path == path_buf).is_true();
779        assert_that(path == &path_buf).is_true();
780        assert_that(&path == path_buf).is_true();
781        assert_that(&path == &path_buf).is_true();
782
783        assert_that(path_buf == path).is_true();
784        assert_that(path_buf == &path).is_true();
785        assert_that(&path_buf == path).is_true();
786        assert_that(&path_buf == &path).is_true();
787
788        // Also works with assertr.
789        assert_that(&path_buf).is_equal_to(path);
790        assert_that(s3_path_buf!("foo", "bar"))
791            .is_ok()
792            .is_equal_to(path);
793    }
794}