use crate::utils::PathIterExt;
use std::{
collections::VecDeque,
ffi::{OsStr, OsString},
fmt,
os::unix::ffi::OsStrExt,
path::PathBuf,
rc::Rc,
};
#[derive(thiserror::Error, Debug, PartialEq)]
pub(crate) enum SymlinkStackError {
#[error("[internal] empty stack")]
EmptyStack,
#[error("[internal error] broken symlink stack: trying to pop component {part:?} from an empty stack entry")]
BrokenStackEmpty { part: OsString },
#[error("[internal error] broken symlink stack: trying to pop component {part:?} but expected {expected:?}")]
BrokenStackWrongComponent { part: OsString, expected: OsString },
}
#[derive(Debug)]
struct SymlinkStackEntry<F: fmt::Debug> {
state: (Rc<F>, PathBuf),
unwalked_link_parts: VecDeque<OsString>,
}
#[derive(Debug)]
pub(crate) struct SymlinkStack<F: fmt::Debug>(VecDeque<SymlinkStackEntry<F>>);
impl<F: fmt::Debug> SymlinkStack<F> {
fn do_push(&mut self, (dir, remaining): (&Rc<F>, PathBuf), link_target: PathBuf) {
let dir = Rc::clone(dir);
let link_parts = link_target
.raw_components()
.map(OsString::from)
.filter(|part| !part.is_empty() && part.as_bytes() != b".")
.collect::<VecDeque<OsString>>();
self.0.push_back(SymlinkStackEntry {
state: (dir, remaining),
unwalked_link_parts: link_parts,
})
}
fn do_pop(&mut self, part: &OsStr) -> Result<(), SymlinkStackError> {
if part.as_bytes() == b"." {
return Ok(());
}
let tail_entry = match self.0.len() {
0 => return Err(SymlinkStackError::EmptyStack),
n => self
.0
.get_mut(n - 1)
.expect("VecDeque.get(len-1) should work"),
};
match tail_entry.unwalked_link_parts.front() {
None => return Err(SymlinkStackError::BrokenStackEmpty { part: part.into() }),
Some(expected) => {
if expected != part {
return Err(SymlinkStackError::BrokenStackWrongComponent {
part: part.into(),
expected: expected.into(),
});
}
}
};
let _ = tail_entry.unwalked_link_parts.pop_front();
Ok(())
}
pub(crate) fn pop_part(&mut self, part: &OsStr) -> Result<(), SymlinkStackError> {
match self.do_pop(part) {
Err(SymlinkStackError::EmptyStack) => return Ok(()),
Err(err) => return Err(err),
Ok(_) => (),
};
while !self.0.is_empty() {
let entry = self
.0
.back()
.expect("should be able to get last element in non-empty stack");
if entry.unwalked_link_parts.is_empty() {
self.0.pop_back();
} else {
break;
}
}
Ok(())
}
pub(crate) fn swap_link(
&mut self,
link_part: &OsStr,
(dir, remaining): (&Rc<F>, PathBuf),
link_target: PathBuf,
) -> Result<(), SymlinkStackError> {
match self.do_pop(link_part) {
Err(SymlinkStackError::EmptyStack) | Ok(_) => {
self.do_push((dir, remaining), link_target);
Ok(())
}
Err(err) => Err(err),
}
}
pub(crate) fn pop_top_symlink(&mut self) -> Option<(Rc<F>, PathBuf)> {
self.0.pop_front().map(|entry| entry.state)
}
pub(crate) fn new() -> Self {
Self(VecDeque::new())
}
}
#[cfg(test)]
mod tests {
use super::SymlinkStackError;
use std::{
path::{Path, PathBuf},
rc::Rc,
};
use pretty_assertions::assert_eq;
type SymlinkStack = super::SymlinkStack<String>;
fn dump_stack(stack: &SymlinkStack) {
for (idx, entry) in stack.0.iter().enumerate() {
println!(
"ss[{idx}]: <{}>/{:?} [->{:?}]",
entry.state.0, entry.state.1, entry.unwalked_link_parts
);
}
}
macro_rules! stack_ops {
($ss:ident @impl $do:block => $expected_result:expr) => {
println!("> before operation");
dump_stack(&$ss);
let res = $do;
println!("> after operation");
dump_stack(&$ss);
assert_eq!(res, $expected_result, "unexpected result");
};
($ss:ident @fn swap_link($link_part:expr, $dir:expr, $remaining:expr, $link_target:expr) => $expected_result:expr) => {
stack_ops! {
$ss @impl {
let link_part = Path::new($link_part).as_os_str();
let dir = Rc::new($dir.into());
let remaining = PathBuf::from($remaining);
let link_target = PathBuf::from($link_target);
$ss.swap_link(link_part, (&dir, remaining), link_target)
} => $expected_result
}
};
($ss:ident @fn pop_part($part:expr) => $expected_result:expr) => {
stack_ops! {
$ss @impl {
let part = Path::new($part).as_os_str();
$ss.pop_part(part)
} => $expected_result
}
};
($ss:ident @fn pop_top_symlink() => $expected_result:expr) => {
let expected_result: Option<(String, PathBuf)> = $expected_result
.map(|(current, remaining)| (current.into(), remaining.into()));
stack_ops! {
$ss @impl {
$ss.pop_top_symlink()
.map(|(dir, current)| (String::from(&*dir), current))
} => expected_result
}
};
([$ss:ident] { $( $op:ident ( $($args:tt)* ) => $expected_result:expr );* $(;)? }) => {
$(
{
println!("-- operation {}{:?}", stringify!($op), ($($args)*));
stack_ops! {
$ss @fn $op ( $($args)* ) => $expected_result
}
}
)*
}
}
macro_rules! stack_content {
([$ss:ident] == {
$((($current:expr, $remaining:expr), {$($unwalked_parts:expr),* $(,)?})),* $(,)?
}) => {
{
let stack_contents = $ss.
0
.iter()
.map(|entry| {(
(String::from(&*entry.state.0), entry.state.1.clone()),
entry.unwalked_link_parts.iter().cloned().collect::<Vec<_>>(),
)})
.collect::<Vec<_>>();
let expected = vec![
$(
((String::from($current), $remaining.into()), vec![$($unwalked_parts.into()),*])
),*
];
assert_eq!(stack_contents, expected, "stack content mismatch")
}
}
}
#[test]
fn basic() {
let mut stack = SymlinkStack::new();
stack_ops! {
[stack] {
swap_link("foo", "A", "anotherbit", "bar/baz") => Ok(());
swap_link("bar", "B", "baz", "abcd") => Ok(());
pop_part("abcd") => Ok(());
swap_link("baz", "C", "", "taillink") => Ok(());
pop_part("taillink") => Ok(());
}
};
assert!(stack.0.is_empty(), "stack should be empty");
assert_eq!(
stack.pop_top_symlink(),
None,
"pop_top_symlink should give None with empty stack"
);
stack_ops! {
[stack] {
pop_part("anotherbit") => Ok(());
}
};
assert!(stack.0.is_empty(), "stack should be empty");
assert_eq!(
stack.pop_top_symlink(),
None,
"pop_top_symlink should give None with empty stack"
);
}
#[test]
fn basic_pop_top_symlink() {
let mut stack = SymlinkStack::new();
stack_ops! {
[stack] {
swap_link("foo", "A", "anotherbit", "bar/baz") => Ok(());
swap_link("bar", "B", "baz", "abcd") => Ok(());
pop_part("abcd") => Ok(());
swap_link("baz", "C", "", "taillink") => Ok(());
pop_top_symlink() => Some(("A", "anotherbit"));
}
};
}
#[test]
fn bad_pop_part() {
let mut stack = SymlinkStack::new();
stack_ops! {
[stack] {
swap_link("foo", "A", "anotherbit", "bar/baz") => Ok(());
swap_link("bar", "B", "baz", "abcd") => Ok(());
swap_link("bad", "C", "", "taillink") => Err(SymlinkStackError::BrokenStackWrongComponent {
part: "bad".into(),
expected: "abcd".into(),
});
pop_part("abcd") => Ok(());
swap_link("baz", "C", "", "taillink") => Ok(());
pop_part("bad") => Err(SymlinkStackError::BrokenStackWrongComponent {
part: "bad".into(),
expected: "taillink".into(),
});
pop_part("taillink") => Ok(());
}
};
assert!(stack.0.is_empty(), "stack should be empty");
stack_ops! {
[stack] {
pop_part("anotherbit") => Ok(());
}
};
assert!(stack.0.is_empty(), "stack should be empty");
}
#[test]
fn basic_tail_chain() {
let mut stack = SymlinkStack::new();
stack_ops! {
[stack] {
swap_link("foo", "A", "", "tailA") => Ok(());
swap_link("tailA", "B", "", "tailB") => Ok(());
swap_link("tailB", "C", "", "tailC") => Ok(());
swap_link("tailC", "D", "", "tailD") => Ok(());
swap_link("tailD", "E", "", "foo/taillink") => Ok(());
}
};
stack_content! {
[stack] == {
(("A", ""), {}),
(("B", ""), {}),
(("C", ""), {}),
(("D", ""), {}),
(("E", ""), {"foo", "taillink"}),
}
};
stack_ops! {
[stack] {
pop_part("foo") => Ok(());
}
};
stack_content! {
[stack] == {
(("A", ""), {}),
(("B", ""), {}),
(("C", ""), {}),
(("D", ""), {}),
(("E", ""), {"taillink"}),
}
};
stack_ops! {
[stack] {
pop_part("taillink") => Ok(());
}
};
assert!(stack.0.is_empty(), "stack should be empty");
}
#[test]
fn stacked_tail_chain() {
let mut stack = SymlinkStack::new();
stack_ops! {
[stack] {
swap_link("foo", "A", "", "tailA/subdir") => Ok(());
swap_link("tailA", "B", "", "tailB") => Ok(());
swap_link("tailB", "C", "", "tailC") => Ok(());
swap_link("tailC", "D", "", "tailD") => Ok(());
swap_link("tailD", "E", "", "taillink1/subdir") => Ok(());
swap_link("taillink1", "F", "", "tailE") => Ok(());
swap_link("tailE", "G", "", "tailF") => Ok(());
swap_link("tailF", "H", "", "tailG") => Ok(());
swap_link("tailG", "I", "", "tailH") => Ok(());
swap_link("tailH", "J", "", "tailI") => Ok(());
swap_link("tailI", "K", "", "taillink2/..") => Ok(());
}
};
stack_content! {
[stack] == {
(("A", ""), {"subdir"}),
(("B", ""), {}),
(("C", ""), {}),
(("D", ""), {}),
(("E", ""), {"subdir"}),
(("F", ""), {}),
(("G", ""), {}),
(("H", ""), {}),
(("I", ""), {}),
(("J", ""), {}),
(("K", ""), {"taillink2", ".."}),
}
};
stack_ops! {
[stack] {
pop_part(".") => Ok(());
pop_part(".") => Ok(());
pop_part(".") => Ok(());
pop_part(".") => Ok(());
pop_part(".") => Ok(());
pop_part(".") => Ok(());
pop_part(".") => Ok(());
pop_part(".") => Ok(());
pop_part(".") => Ok(());
pop_part(".") => Ok(());
pop_part("subdir") => Err(SymlinkStackError::BrokenStackWrongComponent {
part: "subdir".into(),
expected: "taillink2".into(),
});
pop_part("..") => Err(SymlinkStackError::BrokenStackWrongComponent {
part: "..".into(),
expected: "taillink2".into(),
});
}
};
stack_content! {
[stack] == {
(("A", ""), {"subdir"}),
(("B", ""), {}),
(("C", ""), {}),
(("D", ""), {}),
(("E", ""), {"subdir"}),
(("F", ""), {}),
(("G", ""), {}),
(("H", ""), {}),
(("I", ""), {}),
(("J", ""), {}),
(("K", ""), {"taillink2", ".."}),
}
};
stack_ops! {
[stack] {
pop_part("taillink2") => Ok(());
}
}
stack_content! {
[stack] == {
(("A", ""), {"subdir"}),
(("B", ""), {}),
(("C", ""), {}),
(("D", ""), {}),
(("E", ""), {"subdir"}),
(("F", ""), {}),
(("G", ""), {}),
(("H", ""), {}),
(("I", ""), {}),
(("J", ""), {}),
(("K", ""), {".."}),
}
};
stack_ops! {
[stack] {
pop_part("..") => Ok(());
}
}
stack_content! {
[stack] == {
(("A", ""), {"subdir"}),
(("B", ""), {}),
(("C", ""), {}),
(("D", ""), {}),
(("E", ""), {"subdir"}),
}
};
stack_ops! {
[stack] {
pop_part("subdir") => Ok(());
}
}
stack_content! {
[stack] == {
(("A", ""), {"subdir"}),
}
};
stack_ops! {
[stack] {
pop_part("subdir") => Ok(());
}
};
assert!(stack.0.is_empty(), "stack should be empty");
}
}