Skip to content

Commit 6edf195

Browse files
committed
⚡ Add EntryName::sanitize and EntryName::from_{utf8,path,path_lossy}_preserve_root
1 parent 63433b6 commit 6edf195

1 file changed

Lines changed: 136 additions & 10 deletions

File tree

lib/src/entry/name.rs

Lines changed: 136 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -25,15 +25,8 @@ pub struct EntryName(String);
2525

2626
impl 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

Comments
 (0)