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]
18macro_rules! s3_path {
19 ($($component:expr),* $(,)?) => {{
20 let components = &[$(::std::borrow::Cow::Borrowed($component)),*];
21 $crate::S3Path::new(components)
22 }}
23}
24
25#[macro_export]
42macro_rules! s3_path_buf {
43 ($($component:expr),* $(,)?) => {{
44 #[allow(unused_mut)] let mut path = $crate::S3PathBuf::new();
46 #[allow(unused_mut)] 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#[repr(transparent)]
67#[derive(PartialEq, Eq)]
68pub struct S3Path<'i>([Cow<'i, str>]);
69
70#[derive(Clone, PartialEq, Eq, Default)]
72pub struct S3PathBuf {
73 components: Vec<Cow<'static, str>>,
74}
75
76impl PartialEq<S3Path<'_>> for S3PathBuf {
78 fn eq(&self, other: &S3Path<'_>) -> bool {
79 self.as_path() == other
80 }
81}
82
83impl<'i> PartialEq<&S3Path<'i>> for S3PathBuf {
85 fn eq(&self, other: &&S3Path<'i>) -> bool {
86 self.as_path() == *other
87 }
88}
89
90impl<'i> PartialEq<&&S3Path<'i>> for S3PathBuf {
92 fn eq(&self, other: &&&S3Path<'i>) -> bool {
93 self.as_path() == **other
94 }
95}
96
97impl PartialEq<S3PathBuf> for S3Path<'_> {
99 fn eq(&self, other: &S3PathBuf) -> bool {
100 self == other.as_path()
101 }
102}
103
104impl PartialEq<S3PathBuf> for &S3Path<'_> {
106 fn eq(&self, other: &S3PathBuf) -> bool {
107 *self == other.as_path()
108 }
109}
110
111impl 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 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 Ok(unsafe { &*(std::ptr::from_ref::<[Cow<'i, str>]>(components) as *const S3Path<'i>) })
192 }
193
194 #[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 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 #[must_use]
221 pub fn is_empty(&'i self) -> bool {
222 self.0.is_empty()
223 }
224
225 #[must_use]
227 pub fn len(&'i self) -> usize {
228 self.0.len()
229 }
230
231 pub fn components(&'i self) -> impl Iterator<Item = &'i str> {
233 self.0.iter().map(std::convert::AsRef::as_ref)
234 }
235
236 pub fn get(&'i self, index: usize) -> Option<&'i str> {
238 self.0.get(index).map(std::convert::AsRef::as_ref)
239 }
240
241 pub fn last(&'i self) -> Option<&'i str> {
243 self.0.last().map(std::convert::AsRef::as_ref)
244 }
245
246 #[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 unsafe {
256 &*(std::ptr::from_ref::<[Cow<'i, str>]>(parent_slice) as *const S3Path<'i>)
257 },
258 )
259 }
260 }
261
262 #[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
278impl Deref for S3PathBuf {
280 type Target = S3Path<'static>;
281
282 fn deref(&self) -> &Self::Target {
283 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 #[must_use]
295 pub fn new() -> Self {
296 Self::default()
297 }
298
299 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 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 if !c.is_empty() {
334 path.push(Cow::Owned(c.to_string()))?;
335 }
336 }
337 Ok(path)
338 }
339
340 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 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 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 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] 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] 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] 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] 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] 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] 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] 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] 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] 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] 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] 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 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}