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.join($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<'i> PartialEq<S3PathBuf> for S3Path<'i> {
99 fn eq(&self, other: &S3PathBuf) -> bool {
100 self == other.as_path()
101 }
102}
103
104impl<'i> PartialEq<S3PathBuf> for &S3Path<'i> {
106 fn eq(&self, other: &S3PathBuf) -> bool {
107 *self == other.as_path()
108 }
109}
110
111impl<'i> PartialEq<S3PathBuf> for &&S3Path<'i> {
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.deref()
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<'i> std::fmt::Display for S3Path<'i> {
150 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
151 write_components(self.0.iter(), f)?;
152 Ok(())
153 }
154}
155
156impl<'i> std::fmt::Debug for S3Path<'i> {
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> {
180 for component in components {
181 validation::validate_component(component)?;
182 }
183 Ok(unsafe { &*(components as *const [Cow<'i, str>] as *const S3Path<'i>) })
185 }
186
187 pub fn to_owned(&'i self) -> S3PathBuf {
189 S3PathBuf {
190 components: self.0.iter().map(|it| Cow::Owned(it.to_string())).collect(),
191 }
192 }
193
194 pub fn join<C: Into<Cow<'static, str>>>(
196 &self,
197 component: C,
198 ) -> Result<S3PathBuf, InvalidS3PathComponent> {
199 let mut path = self.to_owned();
200 path.join(component)?;
201 Ok(path)
202 }
203
204 pub fn is_empty(&'i self) -> bool {
206 self.0.is_empty()
207 }
208
209 pub fn len(&'i self) -> usize {
211 self.0.len()
212 }
213
214 pub fn components(&'i self) -> impl Iterator<Item = &'i str> {
216 self.0.iter().map(move |it| it.as_ref())
217 }
218
219 pub fn get(&'i self, index: usize) -> Option<&'i str> {
221 self.0.get(index).map(|it| it.as_ref())
222 }
223
224 pub fn last(&'i self) -> Option<&'i str> {
226 self.0.last().map(|it| it.as_ref())
227 }
228
229 pub fn parent(&'i self) -> Option<&'i S3Path<'i>> {
231 if self.0.is_empty() {
232 None
233 } else {
234 let parent_slice = &self.0[..self.0.len() - 1];
235 Some(
236 unsafe { &*(parent_slice as *const [Cow<'i, str>] as *const S3Path<'i>) },
238 )
239 }
240 }
241
242 pub fn to_std_path_buf(&self) -> PathBuf {
249 let mut path = PathBuf::new();
250 for c in &self.0 {
251 path = path.join(c.as_ref());
252 }
253 path
254 }
255}
256
257impl Deref for S3PathBuf {
259 type Target = S3Path<'static>;
260
261 fn deref(&self) -> &Self::Target {
262 unsafe {
265 &*(self.components.as_slice() as *const [Cow<'static, str>] as *const S3Path<'static>)
266 }
267 }
268}
269
270impl S3PathBuf {
271 pub fn new() -> Self {
272 Self::default()
273 }
274
275 pub fn try_from<C: Into<Cow<'static, str>>, I: IntoIterator<Item = C>>(
276 components: I,
277 ) -> Result<Self, InvalidS3PathComponent> {
278 let mut path = S3PathBuf::new();
279 for component in components {
280 path.join(component)?;
281 }
282 Ok(path)
283 }
284
285 pub fn try_from_str(string: impl AsRef<str>) -> Result<Self, InvalidS3PathComponent> {
286 let mut path = S3PathBuf::new();
287 for c in string.as_ref().split('/') {
288 if !c.is_empty() {
290 path.join(Cow::Owned(c.to_string()))?;
291 }
292 }
293 Ok(path)
294 }
295
296 pub fn join(
297 &mut self,
298 component: impl Into<Cow<'static, str>>,
299 ) -> Result<&mut Self, InvalidS3PathComponent> {
300 let comp = component.into();
301 validation::validate_component(&comp)?;
302 self.components.push(comp);
303 Ok(self)
304 }
305
306 #[must_use]
307 #[inline]
308 pub fn as_path(&self) -> &S3Path<'_> {
309 self.deref()
310 }
311
312 pub fn pop(&mut self) -> Option<Cow<'static, str>> {
314 self.components.pop()
315 }
316}
317
318impl TryFrom<&str> for S3PathBuf {
319 type Error = InvalidS3PathComponent;
320
321 fn try_from(value: &str) -> Result<Self, Self::Error> {
322 let mut path = S3PathBuf::new();
323 for c in value.split('/') {
324 if !c.is_empty() {
326 path.join(Cow::Owned(c.to_string()))?;
327 }
328 }
329 Ok(path)
330 }
331}
332
333impl TryFrom<String> for S3PathBuf {
334 type Error = InvalidS3PathComponent;
335
336 fn try_from(value: String) -> Result<Self, Self::Error> {
337 TryFrom::try_from(value.as_str())
338 }
339}
340
341#[cfg(test)]
342impl assertr::assertions::HasLength for S3PathBuf {
343 fn length(&self) -> usize {
344 self.len()
345 }
346}
347
348#[cfg(test)]
349mod test {
350 use crate::S3PathBuf;
351 use assertr::prelude::*;
352
353 mod s3_path_buf {
354 use crate::S3PathBuf;
355 use assertr::prelude::*;
356
357 #[test]
358 fn new_is_initially_empty() {
359 let path = S3PathBuf::new();
360 assert_that(path).has_display_value("");
361 }
362
363 #[test]
364 fn construct_using_new_and_join_components() {
365 let mut path = S3PathBuf::new();
366 path.join("foo").unwrap();
367 path.join("bar").unwrap();
368 assert_that(path).has_display_value("foo/bar");
369 }
370
371 #[test]
372 fn try_from_str_parses_empty_string_as_empty_path() {
373 let path = S3PathBuf::try_from_str("").unwrap();
374 assert_that(path).has_display_value("");
375 }
376
377 #[test]
378 fn try_from_str_parses_single_slash_as_empty_path() {
379 let path = S3PathBuf::try_from_str("/").unwrap();
380 assert_that(path).has_display_value("");
381 }
382
383 #[test]
384 fn try_from_str_removes_leading_and_trailing_slashes() {
385 let path = S3PathBuf::try_from_str("/foo/bar/").unwrap();
386 assert_that(path).has_display_value("foo/bar");
387 }
388
389 #[test]
390 fn try_from_str_ignores_repeated_slashes() {
391 let path = S3PathBuf::try_from_str("foo/////bar").unwrap();
392 assert_that(path).has_display_value("foo/bar");
393 }
394
395 #[test]
396 fn construct_using_try_from_given_str() {
397 let path = S3PathBuf::try_from_str("foo/bar").unwrap();
398 assert_that(path).has_display_value("foo/bar");
399 }
400
401 #[test]
402 fn construct_using_try_from_given_string() {
403 let path = S3PathBuf::try_from_str("foo/bar".to_string()).unwrap();
404 assert_that(path).has_display_value("foo/bar");
405 }
406
407 #[test]
408 fn try_from_static_str_array() {
409 let path = S3PathBuf::try_from(["foo", "bar"]).unwrap();
410 assert_that(path).has_display_value("foo/bar");
411 }
412
413 #[test]
414 fn try_from_string_array() {
415 let path = S3PathBuf::try_from(["foo".to_string(), "bar".to_string()]).unwrap();
416 assert_that(path).has_display_value("foo/bar");
417 }
418
419 #[test]
420 fn reject_invalid_characters() {
421 let mut path = S3PathBuf::new();
422 let result = path.join("invalid/path");
423 assert_that(result.is_err()).is_true();
424
425 let result = S3PathBuf::try_from_str("foo/bar$baz");
426 assert_that(result.is_err()).is_true();
427 }
428
429 #[test]
430 fn as_path() {
431 let path_buf = S3PathBuf::try_from(["foo", "bar"]).unwrap();
432 let path = path_buf.as_path();
433 assert_that(path).has_display_value("foo/bar");
434 let cloned = path.to_owned();
435 drop(path_buf);
436 assert_that(cloned).has_display_value("foo/bar");
437 }
438
439 #[test] fn is_empty_returns_true_when_path_has_no_components() {
441 let path_buf = S3PathBuf::new();
442 assert_that(path_buf.is_empty()).is_true();
443 }
444
445 #[test] fn is_empty_returns_false_when_path_has_components() {
447 let path_buf = S3PathBuf::try_from(["foo", "bar"]).unwrap();
448 assert_that(path_buf.is_empty()).is_false();
449 }
450
451 #[test] fn len_returns_number_of_components() {
453 let path_buf = S3PathBuf::try_from(["foo", "bar"]).unwrap();
454 assert_that(path_buf.len()).is_equal_to(2);
455 }
456
457 #[test] fn components_iterates_over_all_components() {
459 let path_buf = S3PathBuf::try_from(["foo", "bar"]).unwrap();
460 assert_that(path_buf.components()).contains_exactly(&["foo", "bar"]);
461 }
462
463 #[test] fn get_returns_component_at_index() {
465 let path_buf = S3PathBuf::try_from(["foo", "bar"]).unwrap();
466 assert_that(path_buf.get(1)).is_some().is_equal_to("bar");
467 }
468
469 #[test] fn last_returns_last_component() {
471 let path_buf = S3PathBuf::try_from(["foo", "bar"]).unwrap();
472 assert_that(path_buf.last()).is_some().is_equal_to("bar");
473 }
474
475 #[test] fn parent_returns_none_when_path_has_no_components() {
477 let path_buf = S3PathBuf::new();
478 assert_that(path_buf.parent()).is_none();
479 }
480
481 #[test] fn parent_returns_empty_component_when_path_has_only_one_component() {
483 let path_buf = S3PathBuf::try_from(["foo"]).unwrap();
484 assert_that(path_buf.parent())
485 .is_some()
486 .has_display_value("");
487 }
488
489 #[test] fn parent_returns_view_of_parent_path_when_path_has_multiple_components() {
491 let path_buf = S3PathBuf::try_from(["foo", "bar", "baz"]).unwrap();
492 assert_that(path_buf.parent())
493 .is_some()
494 .has_display_value("foo/bar");
495 }
496
497 #[test] fn to_std_path_buf_returns_empty_path_buf_when_s3_path_has_zero_components() {
499 let path_buf = S3PathBuf::new();
500 assert_that(path_buf.to_std_path_buf().display()).has_display_value("");
501 }
502
503 #[test] fn to_std_path_buf_joins_components_with_path_separator_does_not_add_slashes() {
505 let path_buf = S3PathBuf::try_from(["foo", "bar"]).unwrap();
506 assert_that(path_buf.to_std_path_buf().display()).has_display_value("foo/bar");
507 }
508
509 mod s3_path_buf_macro {
510 use assertr::prelude::*;
511 use std::borrow::Cow;
512
513 #[test]
514 fn taking_nothing() {
515 let path = s3_path_buf!().unwrap();
516 assert_that(path).has_display_value("");
517 }
518
519 #[test]
520 fn taking_single_static_str() {
521 let path = s3_path_buf!("foo").unwrap();
522 assert_that(path).has_display_value("foo");
523 }
524
525 #[test]
526 fn taking_owned_string_and_static_str() {
527 let foo = String::from("foo");
528 let path = s3_path_buf!(foo, "bar").unwrap();
529 assert_that(path).has_display_value("foo/bar");
530 }
531
532 #[test]
533 fn taking_owned_string_and_static_str_and_cow() {
534 let foo = String::from("foo");
535 let path = s3_path_buf!(foo, "bar", Cow::Borrowed("baz")).unwrap();
536 assert_that(path).has_display_value("foo/bar/baz");
537 }
538
539 #[test]
540 fn allows_a_trailing_comma() {
541 let path = s3_path_buf!("foo",).unwrap();
542 assert_that(path).has_display_value("foo");
543 }
544 }
545 }
546
547 mod s3_path {
548 use crate::S3Path;
549 use assertr::prelude::*;
550 use std::borrow::Cow;
551
552 #[test]
553 fn new_is_initially_empty() {
554 let path = S3Path::new(&[]).unwrap();
555 assert_that(path).has_display_value("");
556 }
557
558 #[test]
559 fn construct_using_new_and_join_components() {
560 let path = S3Path::new(&[Cow::Borrowed("foo"), Cow::Borrowed("bar")]).unwrap();
561 assert_that(path).has_display_value("foo/bar");
562 }
563
564 #[test]
565 fn to_owned_converts_to_owned_path() {
566 let path = s3_path!("foo", "bar").unwrap();
567 let path_owned = path.to_owned();
568 assert_that(path_owned).has_display_value("foo/bar");
569 }
570
571 #[test]
572 fn join_converts_to_owned_path_and_appends_component() {
573 let path = s3_path!("foo", "bar").unwrap();
574 let path_owned = path.join("baz").unwrap();
575 assert_that(path_owned).has_display_value("foo/bar/baz");
576 }
577
578 mod s3_path_macro {
579 use assertr::prelude::*;
580
581 #[test]
582 fn handles_zero_components() {
583 let path = s3_path!().unwrap();
584 assert_that(path).has_display_value("");
585 }
586
587 #[test]
588 fn handles_one_component() {
589 let path = s3_path!("foo").unwrap();
590 assert_that(path).has_display_value("foo");
591 }
592
593 #[test]
594 fn handles_multiple_components() {
595 let path = s3_path!("foo", "bar").unwrap();
596 assert_that(path).has_display_value("foo/bar");
597 }
598
599 #[test]
600 fn allows_a_trailing_comma() {
601 let path = s3_path!("foo",).unwrap();
602 assert_that(path).has_display_value("foo");
603 }
604 }
605 }
606
607 mod validation {
608 use crate::S3PathBuf;
609 use assertr::prelude::*;
610
611 #[test]
612 fn reject_invalid_characters() {
613 assert_that(S3PathBuf::try_from(["foo-bar"]))
614 .is_ok()
615 .has_display_value("foo-bar");
616 assert_that(S3PathBuf::try_from(["foo_bar"]))
617 .is_ok()
618 .has_display_value("foo_bar");
619 assert_that(S3PathBuf::try_from(["foo.bar"]))
620 .is_ok()
621 .has_display_value("foo.bar");
622 assert_that(S3PathBuf::try_from([".test"]))
623 .is_ok()
624 .has_display_value(".test");
625 assert_that(S3PathBuf::try_from(["..test"]))
626 .is_ok()
627 .has_display_value("..test");
628
629 assert_that(S3PathBuf::try_from(["foo:bar"])).is_err();
630 assert_that(S3PathBuf::try_from(["foo;bar"])).is_err();
631 assert_that(S3PathBuf::try_from(["foo$bar"])).is_err();
632 assert_that(S3PathBuf::try_from(["foo&bar"])).is_err();
633 assert_that(S3PathBuf::try_from(["foo#bar"])).is_err();
634 assert_that(S3PathBuf::try_from(["foo/bar"])).is_err();
635 assert_that(S3PathBuf::try_from(["foo|bar"])).is_err();
636 assert_that(S3PathBuf::try_from(["foo\\bar"])).is_err();
637 assert_that(S3PathBuf::try_from(["."])).is_err();
638 assert_that(S3PathBuf::try_from([".."])).is_err();
639 }
640 }
641
642 mod take_any_path {
643 use crate::{S3Path, S3PathBuf};
644
645 fn take_any_path<'p>(path: impl AsRef<S3Path<'p>>) {
646 let _path: &S3Path<'p> = path.as_ref();
647 }
648
649 #[test]
650 fn takes_owned_s3_path_buf() {
651 take_any_path(S3PathBuf::new());
652 }
653
654 #[test]
655 fn takes_borrowed_s3_path_buf() {
656 take_any_path(&S3PathBuf::new());
657 }
658
659 #[test]
660 fn takes_s3_path() {
661 take_any_path(S3Path::new(&[]).unwrap());
662 }
663 }
664
665 #[test]
666 fn comparison_between_types() {
667 let path_buf = S3PathBuf::try_from(["foo", "bar"]).unwrap();
668 let path = path_buf.as_path();
669
670 assert_that(path == path).is_true();
671 assert_that(path_buf == path_buf).is_true();
672
673 assert_that(path == path_buf).is_true();
674 assert_that(path == &path_buf).is_true();
675 assert_that(&path == path_buf).is_true();
676 assert_that(&path == &path_buf).is_true();
677
678 assert_that(path_buf == path).is_true();
679 assert_that(path_buf == &path).is_true();
680 assert_that(&path_buf == path).is_true();
681 assert_that(&path_buf == &path).is_true();
682
683 assert_that(&path_buf).is_equal_to(path);
685 assert_that(s3_path_buf!("foo", "bar"))
686 .is_ok()
687 .is_equal_to(path);
688 }
689}