use pna::{EntryName, EntryReference};
use std::{
borrow::Cow,
path::{Component, Path, PathBuf},
};
use super::PathTransformers;
#[derive(Clone, Debug)]
pub(crate) struct PathnameEditor {
strip_components: Option<usize>,
transformers: Option<PathTransformers>,
absolute_paths: bool,
}
impl PathnameEditor {
#[inline]
pub(crate) const fn new(
strip_components: Option<usize>,
transformers: Option<PathTransformers>,
absolute_paths: bool,
) -> Self {
Self {
strip_components,
transformers,
absolute_paths,
}
}
pub(crate) fn edit_entry_name(&self, path: &Path) -> Option<EntryName> {
let transformed: Cow<'_, Path> = if let Some(t) = &self.transformers {
Cow::Owned(PathBuf::from(t.apply(path.to_string_lossy(), false, false)))
} else {
Cow::Borrowed(path)
};
if is_effectively_empty_path(&transformed) {
return None;
}
let stripped = strip_components(&transformed, self.strip_components)?;
if is_effectively_empty_path(&stripped) {
return None;
}
let entry_name = EntryName::from_path_lossy_preserve_root(&stripped);
if self.absolute_paths {
Some(entry_name)
} else {
Some(entry_name.sanitize())
}
}
pub(crate) fn edit_hardlink(&self, target: &Path) -> Option<EntryReference> {
let transformed: Cow<'_, Path> = if let Some(t) = &self.transformers {
Cow::Owned(PathBuf::from(t.apply(
target.to_string_lossy(),
false,
true,
)))
} else {
Cow::Borrowed(target)
};
if is_effectively_empty_path(&transformed) {
return None;
}
let stripped = strip_components(&transformed, self.strip_components)?;
if is_effectively_empty_path(&stripped) {
return None;
}
let entry_reference = EntryReference::from_path_lossy_preserve_root(&stripped);
if self.absolute_paths {
Some(entry_reference)
} else {
Some(entry_reference.sanitize())
}
}
pub(crate) fn edit_symlink(&self, target: &Path) -> EntryReference {
let transformed: Cow<'_, Path> = if let Some(t) = &self.transformers {
Cow::Owned(PathBuf::from(t.apply(
target.to_string_lossy(),
true,
false,
)))
} else {
Cow::Borrowed(target)
};
let entry_reference = EntryReference::from_path_lossy_preserve_root(&transformed);
if self.absolute_paths {
entry_reference
} else {
entry_reference.sanitize()
}
}
}
fn strip_components(path: &Path, count: Option<usize>) -> Option<Cow<'_, Path>> {
let Some(count) = count else {
return Some(Cow::Borrowed(path));
};
if count == 0 {
return Some(Cow::Borrowed(path));
}
let components = path.components();
if components.clone().count() <= count {
return None;
}
Some(Cow::from(PathBuf::from_iter(components.skip(count))))
}
#[inline]
fn is_effectively_empty_path(path: &Path) -> bool {
path.components().all(|c| matches!(c, Component::CurDir))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn strip_path_none_or_zero() {
let original = Path::new("a/b");
match strip_components(original, None).unwrap() {
Cow::Borrowed(p) => assert_eq!(p, original),
Cow::Owned(_) => panic!("expected borrowed path when count is None"),
}
assert_eq!(
strip_components(original, Some(0)).unwrap(),
PathBuf::from("a/b")
);
}
#[test]
fn strip_path_relative() {
assert_eq!(
strip_components(Path::new("a/b/c"), Some(1)).unwrap(),
PathBuf::from("b/c")
);
assert_eq!(
strip_components(Path::new("a/b/c"), Some(2)).unwrap(),
PathBuf::from("c")
);
assert!(strip_components(Path::new("a/b"), Some(3)).is_none());
}
#[test]
fn strip_path_parent_dir_components() {
assert_eq!(
strip_components(Path::new("../a/b"), Some(1)).unwrap(),
PathBuf::from("a/b")
);
assert_eq!(
strip_components(Path::new("../a/b"), Some(2)).unwrap(),
PathBuf::from("b")
);
assert!(strip_components(Path::new("../a/b"), Some(3)).is_none());
}
#[test]
fn editor_no_transforms() {
let editor = PathnameEditor::new(None, None, false);
let name = editor.edit_entry_name(Path::new("a/b/c")).unwrap();
assert_eq!(name.as_str(), "a/b/c");
}
#[test]
fn editor_strip_only() {
let editor = PathnameEditor::new(Some(1), None, false);
let name = editor.edit_entry_name(Path::new("a/b/c")).unwrap();
assert_eq!(name.as_str(), "b/c");
}
#[test]
fn editor_strip_insufficient_components() {
let editor = PathnameEditor::new(Some(5), None, false);
assert!(editor.edit_entry_name(Path::new("a/b")).is_none());
assert!(editor.edit_hardlink(Path::new("a/b")).is_none());
}
#[test]
fn editor_bsdtar_order_transform_then_strip() {
use super::super::{PathTransformers, re::bsd::SubstitutionRules};
let rules = SubstitutionRules::new(vec!["/old/new/".parse().unwrap()]);
let transformers = Some(PathTransformers::BsdSubstitutions(rules));
let editor = PathnameEditor::new(Some(1), transformers, false);
let result = editor.edit_entry_name(Path::new("old/a/b")).unwrap();
assert_eq!(result.as_str(), "a/b");
}
#[test]
fn editor_symlink_no_strip() {
let editor = PathnameEditor::new(Some(2), None, false);
let result = editor.edit_symlink(Path::new("a/b/c"));
assert_eq!(result.as_str(), "a/b/c"); }
#[test]
fn editor_skips_empty_or_curdir_paths() {
let editor = PathnameEditor::new(None, None, false);
assert!(editor.edit_entry_name(Path::new("")).is_none());
assert!(editor.edit_entry_name(Path::new(".")).is_none());
assert!(editor.edit_entry_name(Path::new("./.")).is_none());
assert!(editor.edit_hardlink(Path::new("")).is_none());
assert!(editor.edit_hardlink(Path::new(".")).is_none());
assert!(editor.edit_hardlink(Path::new("./.")).is_none());
}
}