@@ -25,15 +25,8 @@ pub struct EntryName(String);
2525
2626impl EntryName {
2727 fn new_from_utf8path ( path : & Utf8Path ) -> Self {
28- let path = normalize_utf8path ( path) ;
29- let iter = path. components ( ) . filter_map ( |c| match c {
30- Utf8Component :: Prefix ( _)
31- | Utf8Component :: RootDir
32- | Utf8Component :: CurDir
33- | Utf8Component :: ParentDir => None ,
34- Utf8Component :: Normal ( p) => Some ( p) ,
35- } ) ;
36- Self ( join_with_capacity ( iter, "/" , path. as_str ( ) . len ( ) ) )
28+ let normalized = normalize_utf8path ( path) ;
29+ Self ( normalized. into_string ( ) ) . sanitize ( )
3730 }
3831
3932 #[ inline]
@@ -71,7 +64,124 @@ impl EntryName {
7164
7265 #[ inline]
7366 fn from_path_lossy ( p : & Path ) -> Self {
74- Self :: new_from_utf8 ( & p. to_string_lossy ( ) )
67+ Self :: from_path_lossy_preserve_root ( p) . sanitize ( )
68+ }
69+
70+ /// Creates an [EntryName] from a path, preserving absolute path components.
71+ ///
72+ /// This method is similar to the `From` implementations for path-like types, but preserves absolute path components.
73+ ///
74+ /// # Examples
75+ ///
76+ /// ```rust
77+ /// use libpna::EntryName;
78+ ///
79+ /// assert_eq!("foo.txt", EntryName::from_utf8_preserve_root("foo.txt"));
80+ /// #[cfg(windows)]
81+ /// assert_eq!("\\foo.txt", EntryName::from_utf8_preserve_root("/foo.txt"));
82+ /// #[cfg(unix)]
83+ /// assert_eq!("/foo.txt", EntryName::from_utf8_preserve_root("/foo.txt"));
84+ /// assert_eq!("./foo.txt", EntryName::from_utf8_preserve_root("./foo.txt"));
85+ /// #[cfg(windows)]
86+ /// assert_eq!("..\\foo.txt", EntryName::from_utf8_preserve_root("../foo.txt"));
87+ /// #[cfg(unix)]
88+ /// assert_eq!("../foo.txt", EntryName::from_utf8_preserve_root("../foo.txt"));
89+ /// assert_eq!("bar/../foo.txt", EntryName::from_utf8_preserve_root("bar/../foo.txt"));
90+ /// ```
91+ #[ inline]
92+ pub fn from_utf8_preserve_root ( path : & str ) -> Self {
93+ Self :: new_from_utf8path_preserve_root ( Utf8Path :: new ( path) )
94+ }
95+
96+ #[ inline]
97+ fn new_from_utf8path_preserve_root ( path : & Utf8Path ) -> Self {
98+ // Preserve root, prefix, and parent components as-is to mirror bsdtar `-P`.
99+ Self ( path. as_str ( ) . to_owned ( ) )
100+ }
101+
102+ /// Creates an [EntryName] from a path, preserving absolute path components.
103+ ///
104+ /// This method is similar to the `From` implementations for path-like types, but preserves absolute path components.
105+ ///
106+ /// # Examples
107+ ///
108+ /// ```rust
109+ /// use libpna::EntryName;
110+ ///
111+ /// assert_eq!("foo.txt", EntryName::from_path_preserve_root("foo.txt".as_ref()).unwrap());
112+ /// #[cfg(windows)]
113+ /// assert_eq!("\\foo.txt", EntryName::from_path_preserve_root("/foo.txt".as_ref()).unwrap());
114+ /// #[cfg(unix)]
115+ /// assert_eq!("/foo.txt", EntryName::from_path_preserve_root("/foo.txt".as_ref()).unwrap());
116+ /// assert_eq!("./foo.txt", EntryName::from_path_preserve_root("./foo.txt".as_ref()).unwrap());
117+ /// #[cfg(windows)]
118+ /// assert_eq!("..\\foo.txt", EntryName::from_path_preserve_root("../foo.txt".as_ref()).unwrap());
119+ /// #[cfg(unix)]
120+ /// assert_eq!("../foo.txt", EntryName::from_path_preserve_root("../foo.txt".as_ref()).unwrap());
121+ /// assert_eq!("bar/../foo.txt", EntryName::from_path_preserve_root("bar/../foo.txt".as_ref()).unwrap());
122+ /// ```
123+ ///
124+ /// # Errors
125+ ///
126+ /// Returns [`EntryNameError`] if the path cannot be represented as valid UTF-8.
127+ #[ inline]
128+ pub fn from_path_preserve_root ( name : & Path ) -> Result < Self , EntryNameError > {
129+ let path = str:: from_utf8 ( name. as_os_str ( ) . as_encoded_bytes ( ) ) ?;
130+ Ok ( Self :: new_from_utf8path_preserve_root ( Utf8Path :: new ( path) ) )
131+ }
132+
133+ /// Creates an [EntryName] from a path, preserving absolute path components.
134+ ///
135+ /// This method is similar to the `From` implementations for path-like types, but preserves absolute path components.
136+ ///
137+ /// # Errors
138+ ///
139+ /// Returns an [`EntryNameError`] if it cannot be represented as valid UTF-8.
140+ ///
141+ /// # Examples
142+ ///
143+ /// ```rust
144+ /// use libpna::EntryName;
145+ ///
146+ /// assert_eq!("foo.txt", EntryName::from_path_lossy_preserve_root("foo.txt".as_ref()));
147+ /// #[cfg(windows)]
148+ /// assert_eq!("\\foo.txt", EntryName::from_path_lossy_preserve_root("/foo.txt".as_ref()));
149+ /// #[cfg(unix)]
150+ /// assert_eq!("/foo.txt", EntryName::from_path_lossy_preserve_root("/foo.txt".as_ref()));
151+ /// assert_eq!("./foo.txt", EntryName::from_path_lossy_preserve_root("./foo.txt".as_ref()));
152+ /// #[cfg(windows)]
153+ /// assert_eq!("..\\foo.txt", EntryName::from_path_lossy_preserve_root("../foo.txt".as_ref()));
154+ /// #[cfg(unix)]
155+ /// assert_eq!("../foo.txt", EntryName::from_path_lossy_preserve_root("../foo.txt".as_ref()));
156+ /// assert_eq!("bar/../foo.txt", EntryName::from_path_lossy_preserve_root("bar/../foo.txt".as_ref()));
157+ /// ```
158+ #[ inline]
159+ pub fn from_path_lossy_preserve_root ( name : & Path ) -> Self {
160+ Self :: new_from_utf8path_preserve_root ( Utf8Path :: new ( & name. to_string_lossy ( ) ) )
161+ }
162+
163+ /// Returns a sanitized copy of this entry name that contains only normal components.
164+ ///
165+ /// Sanitization discards prefixes, root separators, `.` and `..` segments so the
166+ /// resulting entry name is always relative and safe to embed in an archive.
167+ ///
168+ /// # Examples
169+ ///
170+ /// ```rust
171+ /// use libpna::EntryName;
172+ ///
173+ /// let name = EntryName::from_utf8_preserve_root("/var/../tmp/./log");
174+ /// assert_eq!("tmp/log", name.sanitize());
175+ /// ```
176+ #[ inline]
177+ pub fn sanitize ( & self ) -> Self {
178+ let path = normalize_utf8path ( Utf8Path :: new ( & self . 0 ) ) ;
179+ Self ( join_with_capacity (
180+ path. components ( )
181+ . filter ( |c| matches ! ( c, Utf8Component :: Normal ( _) ) ) ,
182+ "/" ,
183+ path. as_str ( ) . len ( ) ,
184+ ) )
75185 }
76186
77187 #[ inline]
@@ -478,6 +588,22 @@ mod tests {
478588 assert ! ( EntryName :: try_from( invalid_os_str) . is_err( ) ) ;
479589 }
480590
591+ #[ test]
592+ fn preserve_root_keeps_unsafe_components ( ) {
593+ assert_eq ! (
594+ "/../foo" ,
595+ EntryName :: from_utf8_preserve_root( "/../foo" ) . as_str( )
596+ ) ;
597+ assert_eq ! (
598+ "bar/../foo" ,
599+ EntryName :: from_utf8_preserve_root( "bar/../foo" ) . as_str( )
600+ ) ;
601+ assert_eq ! (
602+ "../foo" ,
603+ EntryName :: from_utf8_preserve_root( "../foo" ) . as_str( )
604+ ) ;
605+ }
606+
481607 #[ test]
482608 fn type_conversions ( ) {
483609 // Path conversions
0 commit comments