1use crate::util::{str::join_with_capacity, utf8path::normalize_utf8path};
2use camino::{Utf8Component, Utf8Path};
3use std::borrow::Cow;
4use std::error::Error;
5use std::ffi::{OsStr, OsString};
6use std::fmt::{self, Display, Formatter};
7use std::path::{Path, PathBuf};
8use std::str::{self, Utf8Error};
9
10#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
24pub struct EntryName(String);
25
26impl EntryName {
27 #[inline]
28 fn new_from_utf8(name: &str) -> Self {
29 Self::from_utf8_preserve_root(name).sanitize()
30 }
31
32 #[inline]
33 fn new_from_path(name: &Path) -> Result<Self, EntryNameError> {
34 let name = str::from_utf8(name.as_os_str().as_encoded_bytes())?;
35 Ok(Self::new_from_utf8(name))
36 }
37
38 #[inline]
56 pub fn from_lossy<T: Into<PathBuf>>(p: T) -> Self {
57 Self::from_path_lossy(&p.into())
58 }
59
60 #[inline]
61 fn from_path_lossy(p: &Path) -> Self {
62 Self::from_path_lossy_preserve_root(p).sanitize()
63 }
64
65 #[inline]
81 pub fn from_utf8_preserve_root(path: &str) -> Self {
82 Self::new_preserve_root(path.into())
83 }
84
85 #[inline]
86 fn new_preserve_root(path: String) -> Self {
87 Self(path)
88 }
89
90 #[inline]
108 pub fn from_path_preserve_root(name: &Path) -> Result<Self, EntryNameError> {
109 let path = str::from_utf8(name.as_os_str().as_encoded_bytes())?;
110 Ok(Self::from_utf8_preserve_root(path))
111 }
112
113 #[inline]
131 pub fn from_path_lossy_preserve_root(name: &Path) -> Self {
132 Self::new_preserve_root(name.to_string_lossy().into())
133 }
134
135 #[inline]
149 pub fn sanitize(&self) -> Self {
150 let path = normalize_utf8path(Utf8Path::new(&self.0));
151 Self(join_with_capacity(
152 path.components()
153 .filter(|c| matches!(c, Utf8Component::Normal(_))),
154 "/",
155 path.as_str().len(),
156 ))
157 }
158
159 #[inline]
160 pub(crate) fn as_bytes(&self) -> &[u8] {
161 self.0.as_bytes()
162 }
163
164 #[inline]
175 pub fn as_str(&self) -> &str {
176 self.0.as_str()
177 }
178
179 #[inline]
192 pub fn as_os_str(&self) -> &OsStr {
193 self.0.as_ref()
194 }
195
196 #[inline]
208 pub fn as_path(&self) -> &Path {
209 self.0.as_ref()
210 }
211}
212
213impl From<String> for EntryName {
214 #[inline]
215 fn from(value: String) -> Self {
216 Self::new_from_utf8(&value)
217 }
218}
219
220impl From<&String> for EntryName {
221 #[inline]
222 fn from(value: &String) -> Self {
223 Self::new_from_utf8(value)
224 }
225}
226
227impl From<&str> for EntryName {
228 #[inline]
239 fn from(value: &str) -> Self {
240 Self::new_from_utf8(value)
241 }
242}
243
244impl From<Cow<'_, str>> for EntryName {
245 #[inline]
254 fn from(value: Cow<'_, str>) -> Self {
255 Self::new_from_utf8(&value)
256 }
257}
258
259impl From<&Cow<'_, str>> for EntryName {
260 #[inline]
261 fn from(value: &Cow<'_, str>) -> Self {
262 Self::new_from_utf8(value)
263 }
264}
265
266impl TryFrom<&OsStr> for EntryName {
267 type Error = EntryNameError;
268
269 #[inline]
270 fn try_from(value: &OsStr) -> Result<Self, Self::Error> {
271 Self::new_from_path(Path::new(value))
272 }
273}
274
275impl TryFrom<OsString> for EntryName {
276 type Error = EntryNameError;
277
278 #[inline]
279 fn try_from(value: OsString) -> Result<Self, Self::Error> {
280 Self::new_from_path(Path::new(&value))
281 }
282}
283
284impl TryFrom<&OsString> for EntryName {
285 type Error = EntryNameError;
286
287 #[inline]
288 fn try_from(value: &OsString) -> Result<Self, Self::Error> {
289 Self::new_from_path(Path::new(value))
290 }
291}
292
293impl TryFrom<Cow<'_, OsStr>> for EntryName {
294 type Error = EntryNameError;
295
296 #[inline]
297 fn try_from(value: Cow<'_, OsStr>) -> Result<Self, Self::Error> {
298 Self::new_from_path(Path::new(&value))
299 }
300}
301
302impl TryFrom<&Path> for EntryName {
303 type Error = EntryNameError;
304
305 #[inline]
315 fn try_from(value: &Path) -> Result<Self, Self::Error> {
316 Self::new_from_path(value)
317 }
318}
319
320impl TryFrom<PathBuf> for EntryName {
321 type Error = EntryNameError;
322
323 #[inline]
333 fn try_from(value: PathBuf) -> Result<Self, Self::Error> {
334 Self::new_from_path(&value)
335 }
336}
337
338impl TryFrom<&PathBuf> for EntryName {
339 type Error = EntryNameError;
340
341 #[inline]
351 fn try_from(value: &PathBuf) -> Result<Self, Self::Error> {
352 Self::new_from_path(value)
353 }
354}
355
356impl TryFrom<Cow<'_, Path>> for EntryName {
357 type Error = EntryNameError;
358
359 #[inline]
370 fn try_from(value: Cow<'_, Path>) -> Result<Self, Self::Error> {
371 Self::new_from_path(&value)
372 }
373}
374
375impl TryFrom<&[u8]> for EntryName {
376 type Error = EntryNameError;
377
378 #[inline]
379 fn try_from(value: &[u8]) -> Result<Self, Self::Error> {
380 Ok(Self::from(str::from_utf8(value)?))
381 }
382}
383
384impl Display for EntryName {
385 #[inline]
386 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
387 Display::fmt(&self.0, f)
388 }
389}
390
391impl AsRef<str> for EntryName {
392 #[inline]
393 fn as_ref(&self) -> &str {
394 self.as_str()
395 }
396}
397
398impl AsRef<OsStr> for EntryName {
399 #[inline]
400 fn as_ref(&self) -> &OsStr {
401 self.as_os_str()
402 }
403}
404
405impl AsRef<Path> for EntryName {
406 #[inline]
407 fn as_ref(&self) -> &Path {
408 self.as_path()
409 }
410}
411
412impl PartialEq<str> for EntryName {
413 #[inline]
414 fn eq(&self, other: &str) -> bool {
415 PartialEq::eq(self.as_str(), other)
416 }
417}
418
419impl PartialEq<&str> for EntryName {
420 #[inline]
428 fn eq(&self, other: &&str) -> bool {
429 PartialEq::eq(self.as_str(), *other)
430 }
431}
432
433impl PartialEq<EntryName> for str {
434 #[inline]
435 fn eq(&self, other: &EntryName) -> bool {
436 PartialEq::eq(self, other.as_str())
437 }
438}
439
440impl PartialEq<EntryName> for &str {
441 #[inline]
449 fn eq(&self, other: &EntryName) -> bool {
450 PartialEq::eq(self, &other.as_str())
451 }
452}
453
454#[derive(Clone, Eq, PartialEq, Debug)]
456pub struct EntryNameError(Utf8Error);
457
458impl Error for EntryNameError {}
459
460impl Display for EntryNameError {
461 #[inline]
462 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
463 Display::fmt(&self.0, f)
464 }
465}
466
467impl From<Utf8Error> for EntryNameError {
468 #[inline]
469 fn from(value: Utf8Error) -> Self {
470 Self(value)
471 }
472}
473
474#[cfg(test)]
475mod tests {
476 use super::*;
477 #[cfg(unix)]
478 use std::os::unix::ffi::OsStrExt;
479 #[cfg(all(target_family = "wasm", target_os = "unknown"))]
480 use wasm_bindgen_test::wasm_bindgen_test as test;
481
482 #[test]
483 fn remove_root() {
484 assert_eq!("test.txt", EntryName::from("/test.txt"));
485 assert_eq!("test/test.txt", EntryName::from("/test/test.txt"));
486 }
487
488 #[test]
489 fn remove_last() {
490 assert_eq!("test", EntryName::from("test/"));
491 assert_eq!("test/test", EntryName::from("test/test/"));
492 }
493
494 #[cfg(target_os = "windows")]
495 #[test]
496 fn remove_prefix() {
497 assert_eq!("test.txt", EntryName::from("C:\\test.txt"));
498 assert_eq!("test/test.txt", EntryName::from("C:\\test\\test.txt"));
499 }
500
501 #[test]
502 fn basic_string_conversion() {
503 assert_eq!("test.txt", EntryName::from(String::from("test.txt")));
505 assert_eq!("test.txt", EntryName::from(&String::from("test.txt")));
506
507 assert_eq!("test.txt", EntryName::from("test.txt"));
509
510 assert_eq!("test.txt", EntryName::from(Cow::from("test.txt")));
512 assert_eq!("test.txt", EntryName::from(&Cow::from("test.txt")));
513 }
514
515 #[test]
516 fn special_characters() {
517 assert_eq!("日本語.txt", EntryName::from("日本語.txt"));
519 assert_eq!("test/日本語.txt", EntryName::from("test/日本語.txt"));
520 assert_eq!("日本語/テスト.txt", EntryName::from("日本語/テスト.txt"));
521
522 assert_eq!("test@example.com", EntryName::from("test@example.com"));
524 assert_eq!("test#123", EntryName::from("test#123"));
525 assert_eq!("test$123", EntryName::from("test$123"));
526 assert_eq!("test+123", EntryName::from("test+123"));
527 assert_eq!("test-123", EntryName::from("test-123"));
528 assert_eq!("test_123", EntryName::from("test_123"));
529 }
530
531 #[test]
532 fn path_normalization() {
533 assert_eq!("test.txt", EntryName::from("./test.txt"));
535 assert_eq!("test/test.txt", EntryName::from("./test/test.txt"));
536
537 assert_eq!("test.txt", EntryName::from("../test.txt"));
539 assert_eq!("test/test.txt", EntryName::from("../test/test.txt"));
540 assert_eq!("test.txt", EntryName::from("test/../test.txt"));
541
542 assert_eq!("test/test.txt", EntryName::from("test//test.txt"));
544 assert_eq!("test/test.txt", EntryName::from("test///test.txt"));
545 assert_eq!("test/test.txt", EntryName::from("///test///test.txt"));
546 }
547
548 #[test]
549 fn error_cases() {
550 let invalid_utf8: &[u8] = &[0x74, 0x65, 0x73, 0x74, 0xFF, 0x2E, 0x74, 0x78, 0x74];
552 assert!(EntryName::try_from(invalid_utf8).is_err());
553 }
554
555 #[cfg(unix)]
556 #[test]
557 fn unix_error_cases() {
558 let invalid_bytes = [0x74, 0x65, 0x73, 0x74, 0xFF, 0x2E, 0x74, 0x78, 0x74];
559 let invalid_os_str = OsStr::from_bytes(&invalid_bytes);
560 assert!(EntryName::try_from(invalid_os_str).is_err());
561 }
562
563 #[test]
564 fn preserve_root_keeps_unsafe_components() {
565 assert_eq!(
566 "/../foo",
567 EntryName::from_utf8_preserve_root("/../foo").as_str()
568 );
569 assert_eq!(
570 "bar/../foo",
571 EntryName::from_utf8_preserve_root("bar/../foo").as_str()
572 );
573 assert_eq!(
574 "../foo",
575 EntryName::from_utf8_preserve_root("../foo").as_str()
576 );
577 }
578
579 #[test]
580 fn preserve_root_edge_cases() {
581 assert_eq!("", EntryName::from_utf8_preserve_root(""));
583 assert_eq!("..", EntryName::from_utf8_preserve_root(".."));
585 assert_eq!(".", EntryName::from_utf8_preserve_root("."));
587 assert_eq!("/", EntryName::from_utf8_preserve_root("/"));
589 assert_eq!("../../..", EntryName::from_utf8_preserve_root("../../.."));
591 }
592
593 #[test]
594 fn sanitize_edge_cases() {
595 assert_eq!("", EntryName::from_utf8_preserve_root("").sanitize());
597 assert_eq!("", EntryName::from_utf8_preserve_root("..").sanitize());
599 assert_eq!("", EntryName::from_utf8_preserve_root(".").sanitize());
601 assert_eq!("", EntryName::from_utf8_preserve_root("/").sanitize());
603 assert_eq!(
605 "",
606 EntryName::from_utf8_preserve_root("../../..").sanitize()
607 );
608 assert_eq!(
610 "foo",
611 EntryName::from_utf8_preserve_root("/../foo").sanitize()
612 );
613 assert_eq!(
614 "foo",
615 EntryName::from_utf8_preserve_root("./foo").sanitize()
616 );
617 }
618
619 #[test]
620 fn type_conversions() {
621 let path = Path::new("test.txt");
623 assert_eq!("test.txt", EntryName::try_from(path).unwrap());
624
625 let path_buf = PathBuf::from("test.txt");
626 assert_eq!("test.txt", EntryName::try_from(&path_buf).unwrap());
627
628 let os_str = OsStr::new("test.txt");
630 assert_eq!("test.txt", EntryName::try_from(os_str).unwrap());
631
632 let os_string = OsString::from("test.txt");
633 assert_eq!("test.txt", EntryName::try_from(&os_string).unwrap());
634 }
635
636 #[test]
637 fn comparisons() {
638 let name1 = EntryName::from("test.txt");
639 let name2 = EntryName::from("test.txt");
640 let name3 = EntryName::from("other.txt");
641
642 assert_eq!(name1, name2);
644 assert_eq!(name1, "test.txt");
645 assert_eq!("test.txt", name1);
646
647 assert_ne!(name1, name3);
649 assert_ne!(name1, "other.txt");
650 assert_ne!("other.txt", name1);
651 }
652
653 #[cfg(unix)]
654 #[test]
655 fn unix_lossy_conversion() {
656 let invalid_bytes = [0x74, 0x65, 0x73, 0x74, 0xFF, 0x2E, 0x74, 0x78, 0x74];
658 let invalid_path = PathBuf::from(OsStr::from_bytes(&invalid_bytes));
659 let name = EntryName::from_lossy(invalid_path);
660 assert_eq!("test\u{FFFD}.txt", name.as_str());
661
662 let invalid_bytes = [0x74, 0x65, 0x73, 0x74, 0xFF, 0xFF, 0x2E, 0x74, 0x78, 0x74];
664 let invalid_path = PathBuf::from(OsStr::from_bytes(&invalid_bytes));
665 let name = EntryName::from_lossy(invalid_path);
666 assert_eq!("test\u{FFFD}\u{FFFD}.txt", name.as_str());
667
668 let invalid_bytes = [0xFF, 0x74, 0x65, 0x73, 0x74, 0x2E, 0x74, 0x78, 0x74];
670 let invalid_path = PathBuf::from(OsStr::from_bytes(&invalid_bytes));
671 let name = EntryName::from_lossy(invalid_path);
672 assert_eq!("\u{FFFD}test.txt", name.as_str());
673
674 let invalid_bytes = [0x74, 0x65, 0x73, 0x74, 0x2E, 0x74, 0x78, 0x74, 0xFF];
676 let invalid_path = PathBuf::from(OsStr::from_bytes(&invalid_bytes));
677 let name = EntryName::from_lossy(invalid_path);
678 assert_eq!("test.txt\u{FFFD}", name.as_str());
679 }
680
681 #[test]
682 fn as_ref_implementations() {
683 let name = EntryName::from("test.txt");
684
685 let str_ref: &str = name.as_ref();
687 assert_eq!("test.txt", str_ref);
688
689 let os_str_ref: &OsStr = name.as_ref();
691 assert_eq!(OsStr::new("test.txt"), os_str_ref);
692
693 let path_ref: &Path = name.as_ref();
695 assert_eq!(Path::new("test.txt"), path_ref);
696 }
697}