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<'i> PartialEq<&S3Path<'i>> for S3PathBuf {
78 fn eq(&self, other: &&S3Path<'i>) -> bool {
79 self.as_path() == *other
80 }
81}
82
83impl<'i> PartialEq<S3PathBuf> for &S3Path<'i> {
85 fn eq(&self, other: &S3PathBuf) -> bool {
86 *self == other.as_path()
87 }
88}
89
90impl<'i> AsRef<S3Path<'i>> for S3Path<'i> {
91 fn as_ref(&self) -> &S3Path<'i> {
92 self
93 }
94}
95
96impl<'i> AsRef<S3Path<'i>> for S3PathBuf {
97 fn as_ref(&self) -> &S3Path<'i> {
98 self.deref()
99 }
100}
101
102impl AsRef<S3PathBuf> for S3PathBuf {
103 fn as_ref(&self) -> &S3PathBuf {
104 self
105 }
106}
107
108fn write_components<C: AsRef<str>>(
109 components: impl Iterator<Item = C>,
110 f: &mut Formatter,
111) -> Result<(), std::fmt::Error> {
112 for (i, c) in components.enumerate() {
113 if i > 0 {
114 f.write_str("/")?;
115 }
116 f.write_str(c.as_ref())?;
117 }
118 Ok(())
119}
120
121impl<'i> std::fmt::Display for S3Path<'i> {
122 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
123 write_components(self.0.iter(), f)?;
124 Ok(())
125 }
126}
127
128impl<'i> std::fmt::Debug for S3Path<'i> {
129 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
130 write_components(self.0.iter(), f)?;
131 Ok(())
132 }
133}
134
135impl std::fmt::Display for S3PathBuf {
136 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
137 write_components(self.components.iter(), f)?;
138 Ok(())
139 }
140}
141
142impl std::fmt::Debug for S3PathBuf {
143 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
144 write_components(self.components.iter(), f)?;
145 Ok(())
146 }
147}
148
149impl<'i> S3Path<'i> {
150 pub fn new(components: &'i [Cow<'i, str>]) -> Result<&'i S3Path<'i>, InvalidS3PathComponent> {
152 for component in components {
153 validation::validate_component(component)?;
154 }
155 Ok(unsafe { &*(components as *const [Cow<'i, str>] as *const S3Path<'i>) })
157 }
158
159 pub fn to_owned(&'i self) -> S3PathBuf {
161 S3PathBuf {
162 components: self.0.iter().map(|it| Cow::Owned(it.to_string())).collect(),
163 }
164 }
165
166 pub fn join<C: Into<Cow<'static, str>>>(
168 &self,
169 component: C,
170 ) -> Result<S3PathBuf, InvalidS3PathComponent> {
171 let mut path = self.to_owned();
172 path.join(component)?;
173 Ok(path)
174 }
175
176 pub fn is_empty(&'i self) -> bool {
178 self.0.is_empty()
179 }
180
181 pub fn len(&'i self) -> usize {
183 self.0.len()
184 }
185
186 pub fn components(&'i self) -> impl Iterator<Item = &'i str> {
188 self.0.iter().map(move |it| it.as_ref())
189 }
190
191 pub fn get(&'i self, index: usize) -> Option<&'i str> {
193 self.0.get(index).map(|it| it.as_ref())
194 }
195
196 pub fn last(&'i self) -> Option<&'i str> {
198 self.0.last().map(|it| it.as_ref())
199 }
200
201 pub fn parent(&'i self) -> Option<&'i S3Path<'i>> {
203 if self.0.is_empty() {
204 None
205 } else {
206 let parent_slice = &self.0[..self.0.len() - 1];
207 Some(
208 unsafe { &*(parent_slice as *const [Cow<'i, str>] as *const S3Path<'i>) },
210 )
211 }
212 }
213
214 pub fn to_std_path_buf(&self) -> PathBuf {
221 let mut path = PathBuf::new();
222 for c in &self.0 {
223 path = path.join(c.as_ref());
224 }
225 path
226 }
227}
228
229impl Deref for S3PathBuf {
231 type Target = S3Path<'static>;
232
233 fn deref(&self) -> &Self::Target {
234 unsafe {
237 &*(self.components.as_slice() as *const [Cow<'static, str>] as *const S3Path<'static>)
238 }
239 }
240}
241
242impl S3PathBuf {
243 pub fn new() -> Self {
244 Self::default()
245 }
246
247 pub fn try_from<C: Into<Cow<'static, str>>, I: IntoIterator<Item = C>>(
248 components: I,
249 ) -> Result<Self, InvalidS3PathComponent> {
250 let mut path = S3PathBuf::new();
251 for component in components {
252 path.join(component)?;
253 }
254 Ok(path)
255 }
256
257 pub fn try_from_str(string: impl AsRef<str>) -> Result<Self, InvalidS3PathComponent> {
258 let mut path = S3PathBuf::new();
259 for c in string.as_ref().split('/') {
260 if !c.is_empty() {
262 path.join(Cow::Owned(c.to_string()))?;
263 }
264 }
265 Ok(path)
266 }
267
268 pub fn join(
269 &mut self,
270 component: impl Into<Cow<'static, str>>,
271 ) -> Result<&mut Self, InvalidS3PathComponent> {
272 let comp = component.into();
273 validation::validate_component(&comp)?;
274 self.components.push(comp);
275 Ok(self)
276 }
277
278 #[must_use]
279 #[inline]
280 pub fn as_path(&self) -> &S3Path<'_> {
281 self.deref()
282 }
283
284 pub fn pop(&mut self) -> Option<Cow<'static, str>> {
286 self.components.pop()
287 }
288}
289
290impl TryFrom<&str> for S3PathBuf {
291 type Error = InvalidS3PathComponent;
292
293 fn try_from(value: &str) -> Result<Self, Self::Error> {
294 let mut path = S3PathBuf::new();
295 for c in value.split('/') {
296 if !c.is_empty() {
298 path.join(Cow::Owned(c.to_string()))?;
299 }
300 }
301 Ok(path)
302 }
303}
304
305impl TryFrom<String> for S3PathBuf {
306 type Error = InvalidS3PathComponent;
307
308 fn try_from(value: String) -> Result<Self, Self::Error> {
309 TryFrom::try_from(value.as_str())
310 }
311}
312
313#[cfg(test)]
314impl assertr::assertions::HasLength for S3PathBuf {
315 fn length(&self) -> usize {
316 self.len()
317 }
318}
319
320#[cfg(test)]
321mod test {
322 use crate::S3PathBuf;
323 use assertr::prelude::*;
324
325 mod s3_path_buf {
326 use crate::S3PathBuf;
327 use assertr::prelude::*;
328
329 #[test]
330 fn new_is_initially_empty() {
331 let path = S3PathBuf::new();
332 assert_that(path).has_display_value("");
333 }
334
335 #[test]
336 fn construct_using_new_and_join_components() {
337 let mut path = S3PathBuf::new();
338 path.join("foo").unwrap();
339 path.join("bar").unwrap();
340 assert_that(path).has_display_value("foo/bar");
341 }
342
343 #[test]
344 fn try_from_str_parses_empty_string_as_empty_path() {
345 let path = S3PathBuf::try_from_str("").unwrap();
346 assert_that(path).has_display_value("");
347 }
348
349 #[test]
350 fn try_from_str_parses_single_slash_as_empty_path() {
351 let path = S3PathBuf::try_from_str("/").unwrap();
352 assert_that(path).has_display_value("");
353 }
354
355 #[test]
356 fn try_from_str_removes_leading_and_trailing_slashes() {
357 let path = S3PathBuf::try_from_str("/foo/bar/").unwrap();
358 assert_that(path).has_display_value("foo/bar");
359 }
360
361 #[test]
362 fn try_from_str_ignores_repeated_slashes() {
363 let path = S3PathBuf::try_from_str("foo/////bar").unwrap();
364 assert_that(path).has_display_value("foo/bar");
365 }
366
367 #[test]
368 fn construct_using_try_from_given_str() {
369 let path = S3PathBuf::try_from_str("foo/bar").unwrap();
370 assert_that(path).has_display_value("foo/bar");
371 }
372
373 #[test]
374 fn construct_using_try_from_given_string() {
375 let path = S3PathBuf::try_from_str("foo/bar".to_string()).unwrap();
376 assert_that(path).has_display_value("foo/bar");
377 }
378
379 #[test]
380 fn try_from_static_str_array() {
381 let path = S3PathBuf::try_from(["foo", "bar"]).unwrap();
382 assert_that(path).has_display_value("foo/bar");
383 }
384
385 #[test]
386 fn try_from_string_array() {
387 let path = S3PathBuf::try_from(["foo".to_string(), "bar".to_string()]).unwrap();
388 assert_that(path).has_display_value("foo/bar");
389 }
390
391 #[test]
392 fn reject_invalid_characters() {
393 let mut path = S3PathBuf::new();
394 let result = path.join("invalid/path");
395 assert_that(result.is_err()).is_true();
396
397 let result = S3PathBuf::try_from_str("foo/bar$baz");
398 assert_that(result.is_err()).is_true();
399 }
400
401 #[test]
402 fn as_path() {
403 let path_buf = S3PathBuf::try_from(["foo", "bar"]).unwrap();
404 let path = path_buf.as_path();
405 assert_that(path).has_display_value("foo/bar");
406 let cloned = path.to_owned();
407 drop(path_buf);
408 assert_that(cloned).has_display_value("foo/bar");
409 }
410
411 #[test] fn is_empty_returns_true_when_path_has_no_components() {
413 let path_buf = S3PathBuf::new();
414 assert_that(path_buf.is_empty()).is_true();
415 }
416
417 #[test] fn is_empty_returns_false_when_path_has_components() {
419 let path_buf = S3PathBuf::try_from(["foo", "bar"]).unwrap();
420 assert_that(path_buf.is_empty()).is_false();
421 }
422
423 #[test] fn len_returns_number_of_components() {
425 let path_buf = S3PathBuf::try_from(["foo", "bar"]).unwrap();
426 assert_that(path_buf.len()).is_equal_to(2);
427 }
428
429 #[test] fn components_iterates_over_all_components() {
431 let path_buf = S3PathBuf::try_from(["foo", "bar"]).unwrap();
432 assert_that(path_buf.components()).contains_exactly(&["foo", "bar"]);
433 }
434
435 #[test] fn get_returns_component_at_index() {
437 let path_buf = S3PathBuf::try_from(["foo", "bar"]).unwrap();
438 assert_that(path_buf.get(1)).is_some().is_equal_to("bar");
439 }
440
441 #[test] fn last_returns_last_component() {
443 let path_buf = S3PathBuf::try_from(["foo", "bar"]).unwrap();
444 assert_that(path_buf.last()).is_some().is_equal_to("bar");
445 }
446
447 #[test] fn parent_returns_none_when_path_has_no_components() {
449 let path_buf = S3PathBuf::new();
450 assert_that(path_buf.parent()).is_none();
451 }
452
453 #[test] fn parent_returns_empty_component_when_path_has_only_one_component() {
455 let path_buf = S3PathBuf::try_from(["foo"]).unwrap();
456 assert_that(path_buf.parent())
457 .is_some()
458 .has_display_value("");
459 }
460
461 #[test] fn parent_returns_view_of_parent_path_when_path_has_multiple_components() {
463 let path_buf = S3PathBuf::try_from(["foo", "bar", "baz"]).unwrap();
464 assert_that(path_buf.parent())
465 .is_some()
466 .has_display_value("foo/bar");
467 }
468
469 #[test] fn to_std_path_buf_returns_empty_path_buf_when_s3_path_has_zero_components() {
471 let path_buf = S3PathBuf::new();
472 assert_that(path_buf.to_std_path_buf().display()).has_display_value("");
473 }
474
475 #[test] fn to_std_path_buf_joins_components_with_path_separator_does_not_add_slashes() {
477 let path_buf = S3PathBuf::try_from(["foo", "bar"]).unwrap();
478 assert_that(path_buf.to_std_path_buf().display()).has_display_value("foo/bar");
479 }
480
481 mod s3_path_buf_macro {
482 use assertr::prelude::*;
483 use std::borrow::Cow;
484
485 #[test]
486 fn taking_nothing() {
487 let path = s3_path_buf!().unwrap();
488 assert_that(path).has_display_value("");
489 }
490
491 #[test]
492 fn taking_single_static_str() {
493 let path = s3_path_buf!("foo").unwrap();
494 assert_that(path).has_display_value("foo");
495 }
496
497 #[test]
498 fn taking_owned_string_and_static_str() {
499 let foo = String::from("foo");
500 let path = s3_path_buf!(foo, "bar").unwrap();
501 assert_that(path).has_display_value("foo/bar");
502 }
503
504 #[test]
505 fn taking_owned_string_and_static_str_and_cow() {
506 let foo = String::from("foo");
507 let path = s3_path_buf!(foo, "bar", Cow::Borrowed("baz")).unwrap();
508 assert_that(path).has_display_value("foo/bar/baz");
509 }
510
511 #[test]
512 fn allows_a_trailing_comma() {
513 let path = s3_path_buf!("foo",).unwrap();
514 assert_that(path).has_display_value("foo");
515 }
516 }
517 }
518
519 mod s3_path {
520 use crate::S3Path;
521 use assertr::prelude::*;
522 use std::borrow::Cow;
523
524 #[test]
525 fn new_is_initially_empty() {
526 let path = S3Path::new(&[]).unwrap();
527 assert_that(path).has_display_value("");
528 }
529
530 #[test]
531 fn construct_using_new_and_join_components() {
532 let path = S3Path::new(&[Cow::Borrowed("foo"), Cow::Borrowed("bar")]).unwrap();
533 assert_that(path).has_display_value("foo/bar");
534 }
535
536 #[test]
537 fn to_owned_converts_to_owned_path() {
538 let path = s3_path!("foo", "bar").unwrap();
539 let path_owned = path.to_owned();
540 assert_that(path_owned).has_display_value("foo/bar");
541 }
542
543 #[test]
544 fn join_converts_to_owned_path_and_appends_component() {
545 let path = s3_path!("foo", "bar").unwrap();
546 let path_owned = path.join("baz").unwrap();
547 assert_that(path_owned).has_display_value("foo/bar/baz");
548 }
549
550 mod s3_path_macro {
551 use assertr::prelude::*;
552
553 #[test]
554 fn handles_zero_components() {
555 let path = s3_path!().unwrap();
556 assert_that(path).has_display_value("");
557 }
558
559 #[test]
560 fn handles_one_component() {
561 let path = s3_path!("foo").unwrap();
562 assert_that(path).has_display_value("foo");
563 }
564
565 #[test]
566 fn handles_multiple_components() {
567 let path = s3_path!("foo", "bar").unwrap();
568 assert_that(path).has_display_value("foo/bar");
569 }
570
571 #[test]
572 fn allows_a_trailing_comma() {
573 let path = s3_path!("foo",).unwrap();
574 assert_that(path).has_display_value("foo");
575 }
576 }
577 }
578
579 mod validation {
580 use crate::S3PathBuf;
581 use assertr::prelude::*;
582
583 #[test]
584 fn reject_invalid_characters() {
585 assert_that(S3PathBuf::try_from(["foo-bar"]))
586 .is_ok()
587 .has_display_value("foo-bar");
588 assert_that(S3PathBuf::try_from(["foo_bar"]))
589 .is_ok()
590 .has_display_value("foo_bar");
591 assert_that(S3PathBuf::try_from(["foo.bar"]))
592 .is_ok()
593 .has_display_value("foo.bar");
594 assert_that(S3PathBuf::try_from([".test"]))
595 .is_ok()
596 .has_display_value(".test");
597 assert_that(S3PathBuf::try_from(["..test"]))
598 .is_ok()
599 .has_display_value("..test");
600
601 assert_that(S3PathBuf::try_from(["foo:bar"])).is_err();
602 assert_that(S3PathBuf::try_from(["foo;bar"])).is_err();
603 assert_that(S3PathBuf::try_from(["foo$bar"])).is_err();
604 assert_that(S3PathBuf::try_from(["foo&bar"])).is_err();
605 assert_that(S3PathBuf::try_from(["foo#bar"])).is_err();
606 assert_that(S3PathBuf::try_from(["foo/bar"])).is_err();
607 assert_that(S3PathBuf::try_from(["foo|bar"])).is_err();
608 assert_that(S3PathBuf::try_from(["foo\\bar"])).is_err();
609 assert_that(S3PathBuf::try_from(["."])).is_err();
610 assert_that(S3PathBuf::try_from([".."])).is_err();
611 }
612 }
613
614 mod take_any_path {
615 use crate::{S3Path, S3PathBuf};
616
617 fn take_any_path<'p>(path: impl AsRef<S3Path<'p>>) {
618 let _path: &S3Path<'p> = path.as_ref();
619 }
620
621 #[test]
622 fn takes_owned_s3_path_buf() {
623 take_any_path(S3PathBuf::new());
624 }
625
626 #[test]
627 fn takes_borrowed_s3_path_buf() {
628 take_any_path(&S3PathBuf::new());
629 }
630
631 #[test]
632 fn takes_s3_path() {
633 take_any_path(S3Path::new(&[]).unwrap());
634 }
635 }
636
637 #[test]
638 fn comparison_between_types() {
639 let path_buf = S3PathBuf::try_from(["foo", "bar"]).unwrap();
640 let path = path_buf.as_path();
641
642 assert_that(path == path_buf).is_true();
643 assert_that(path_buf == path).is_true();
644 }
645}