Skip to content

Commit 092f57d

Browse files
committed
⚡ Add EntryName::sanitize and EntryName::from_{utf8,path,path_lossy}_preserve_root
1 parent a748eb9 commit 092f57d

2 files changed

Lines changed: 126 additions & 21 deletions

File tree

lib/src/entry/name.rs

Lines changed: 119 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
1-
use crate::util::{path::normalize_path, str::join_with_capacity, utf8path::normalize_utf8path};
2-
use camino::{Utf8Component, Utf8Path, Utf8PathBuf};
1+
use crate::util::{
2+
path::normalize_path,
3+
str::join_with_capacity,
4+
utf8path::{normalize_utf8path, sanitize_utf8path},
5+
};
6+
use camino::{Utf8Path, Utf8PathBuf};
37
use std::borrow::Cow;
48
use std::error::Error;
59
use std::ffi::{OsStr, OsString};
610
use std::fmt::{self, Display, Formatter};
7-
use std::path::{Component, Path, PathBuf};
11+
use std::path::{Path, PathBuf};
812
use std::str::{self, Utf8Error};
913

1014
/// A UTF-8 encoded entry name.
@@ -25,15 +29,7 @@ pub struct EntryName(String);
2529

2630
impl EntryName {
2731
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()))
32+
Self::new_from_utf8path_preserve_root(path).sanitize()
3733
}
3834

3935
#[inline]
@@ -69,16 +65,118 @@ impl EntryName {
6965
Self::from_path_lossy(&p.into())
7066
}
7167

68+
#[inline]
7269
fn from_path_lossy(p: &Path) -> Self {
73-
let p = normalize_path(p);
74-
let iter = p.components().filter_map(|c| match c {
75-
Component::Prefix(_)
76-
| Component::RootDir
77-
| Component::CurDir
78-
| Component::ParentDir => None,
79-
Component::Normal(p) => Some(p.to_string_lossy()),
80-
});
81-
Self(join_with_capacity(iter, "/", p.as_os_str().len()))
70+
Self::from_path_lossy_preserve_root(p).sanitize()
71+
}
72+
73+
/// Creates an [EntryName] from a path, preserving absolute path components.
74+
///
75+
/// This method is similar to the `From` implementations for path-like types, but preserves absolute path components.
76+
///
77+
/// # Examples
78+
///
79+
/// ```rust
80+
/// use libpna::EntryName;
81+
///
82+
/// assert_eq!("foo.txt", EntryName::from_utf8_preserve_root("foo.txt"));
83+
/// #[cfg(windows)]
84+
/// assert_eq!("\\foo.txt", EntryName::from_utf8_preserve_root("/foo.txt"));
85+
/// #[cfg(unix)]
86+
/// assert_eq!("/foo.txt", EntryName::from_utf8_preserve_root("/foo.txt"));
87+
/// assert_eq!("foo.txt", EntryName::from_utf8_preserve_root("./foo.txt"));
88+
/// #[cfg(windows)]
89+
/// assert_eq!("..\\foo.txt", EntryName::from_utf8_preserve_root("../foo.txt"));
90+
/// #[cfg(unix)]
91+
/// assert_eq!("../foo.txt", EntryName::from_utf8_preserve_root("../foo.txt"));
92+
/// assert_eq!("foo.txt", EntryName::from_utf8_preserve_root("bar/../foo.txt"));
93+
/// ```
94+
#[inline]
95+
pub fn from_utf8_preserve_root(path: &str) -> Self {
96+
Self::new_from_utf8path_preserve_root(Utf8Path::new(path))
97+
}
98+
99+
#[inline]
100+
fn new_from_utf8path_preserve_root(path: &Utf8Path) -> Self {
101+
let path = normalize_utf8path(path);
102+
Self(path.into_string())
103+
}
104+
105+
/// Creates an [EntryName] from a path, preserving absolute path components.
106+
///
107+
/// This method is similar to the `From` implementations for path-like types, but preserves absolute path components.
108+
///
109+
/// # Examples
110+
///
111+
/// ```rust
112+
/// use libpna::EntryName;
113+
///
114+
/// assert_eq!("foo.txt", EntryName::from_path_preserve_root("foo.txt".as_ref()).unwrap());
115+
/// #[cfg(windows)]
116+
/// assert_eq!("\\foo.txt", EntryName::from_path_preserve_root("/foo.txt".as_ref()).unwrap());
117+
/// #[cfg(unix)]
118+
/// assert_eq!("/foo.txt", EntryName::from_path_preserve_root("/foo.txt".as_ref()).unwrap());
119+
/// assert_eq!("foo.txt", EntryName::from_path_preserve_root("./foo.txt".as_ref()).unwrap());
120+
/// #[cfg(windows)]
121+
/// assert_eq!("..\\foo.txt", EntryName::from_path_preserve_root("../foo.txt".as_ref()).unwrap());
122+
/// #[cfg(unix)]
123+
/// assert_eq!("../foo.txt", EntryName::from_path_preserve_root("../foo.txt".as_ref()).unwrap());
124+
/// assert_eq!("foo.txt", EntryName::from_path_preserve_root("bar/../foo.txt".as_ref()).unwrap());
125+
/// ```
126+
#[inline]
127+
pub fn from_path_preserve_root(name: &Path) -> Result<Self, EntryNameError> {
128+
let path = str::from_utf8(name.as_os_str().as_encoded_bytes())?;
129+
Ok(Self::new_from_utf8path_preserve_root(Utf8Path::new(path)))
130+
}
131+
132+
/// Creates an [EntryName] from a path, preserving absolute path components.
133+
///
134+
/// This method is similar to the `From` implementations for path-like types, but preserves absolute path components.
135+
///
136+
/// # Examples
137+
///
138+
/// ```rust
139+
/// use libpna::EntryName;
140+
///
141+
/// assert_eq!("foo.txt", EntryName::from_path_lossy_preserve_root("foo.txt".as_ref()));
142+
/// #[cfg(windows)]
143+
/// assert_eq!("\\foo.txt", EntryName::from_path_lossy_preserve_root("/foo.txt".as_ref()));
144+
/// #[cfg(unix)]
145+
/// assert_eq!("/foo.txt", EntryName::from_path_lossy_preserve_root("/foo.txt".as_ref()));
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("bar/../foo.txt".as_ref()));
152+
/// ```
153+
#[inline]
154+
pub fn from_path_lossy_preserve_root(name: &Path) -> Self {
155+
let p = normalize_path(name);
156+
Self::new_from_utf8path_preserve_root(Utf8Path::new(&p.to_string_lossy()))
157+
}
158+
159+
/// Returns a sanitized copy of this entry name that contains only normal components.
160+
///
161+
/// Sanitization discards prefixes, root separators, `.` and `..` segments so the
162+
/// resulting entry name is always relative and safe to embed in an archive.
163+
///
164+
/// # Examples
165+
///
166+
/// ```rust
167+
/// use libpna::EntryName;
168+
///
169+
/// let name = EntryName::from_utf8_preserve_root("/var/../tmp/./log");
170+
/// assert_eq!("tmp/log", name.sanitize());
171+
/// ```
172+
#[inline]
173+
pub fn sanitize(&self) -> Self {
174+
let path = sanitize_utf8path(&self.0);
175+
Self(join_with_capacity(
176+
path.components(),
177+
"/",
178+
path.as_str().len(),
179+
))
82180
}
83181

84182
#[inline]

lib/src/util/utf8path.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,13 @@ pub(crate) fn normalize_utf8path(path: impl AsRef<Utf8Path>) -> Utf8PathBuf {
3333
buf
3434
}
3535

36+
pub(crate) fn sanitize_utf8path(path: impl AsRef<Utf8Path>) -> Utf8PathBuf {
37+
let path = path.as_ref();
38+
path.components()
39+
.filter(|c| matches!(c, Utf8Component::Normal(_)))
40+
.collect()
41+
}
42+
3643
#[cfg(test)]
3744
mod tests {
3845
use super::*;

0 commit comments

Comments
 (0)