use std::borrow::Cow;
use std::env::current_dir;
use std::ffi::OsStr;
use std::ffi::OsString;
#[cfg(any(test, feature = "test"))]
use std::fmt::Display;
use std::fs::canonicalize;
use std::io;
use std::io::ErrorKind;
use std::os::unix::ffi::OsStrExt as _;
use std::path::Component;
use std::path::Path;
use std::path::PathBuf;
use std::process::Command;
use std::process::Output;
use std::process::Stdio;
use anyhow::bail;
use anyhow::Context as _;
use anyhow::Result;
#[derive(Clone, Debug)]
pub enum Either<L, R> {
Left(L),
Right(R),
}
impl<L, R, T> Iterator for Either<L, R>
where
L: Iterator<Item = T>,
R: Iterator<Item = T>,
{
type Item = T;
fn next(&mut self) -> Option<T> {
match self {
Self::Left(left) => left.next(),
Self::Right(right) => right.next(),
}
}
}
impl<L, R, T> AsRef<T> for Either<L, R>
where
L: AsRef<T>,
R: AsRef<T>,
T: ?Sized,
{
fn as_ref(&self) -> &T {
match self {
Self::Left(left) => left.as_ref(),
Self::Right(right) => right.as_ref(),
}
}
}
#[cfg(any(test, feature = "test"))]
pub fn join<I, T>(joiner: char, iter: I) -> Option<String>
where
I: IntoIterator<Item = T>,
T: Display,
{
let mut iter = iter.into_iter();
iter.next().map(|first| {
iter.fold(first.to_string(), |list, item| {
format!("{list}{joiner}{item}")
})
})
}
pub fn concat_command<C, A, S>(command: C, args: A) -> OsString
where
C: AsRef<OsStr>,
A: IntoIterator<Item = S>,
S: AsRef<OsStr>,
{
args
.into_iter()
.fold(command.as_ref().to_os_string(), |mut cmd, arg| {
cmd.push(OsStr::new(" "));
cmd.push(arg.as_ref());
cmd
})
}
pub fn format_command<C, A, S>(command: C, args: A) -> String
where
C: AsRef<OsStr>,
A: IntoIterator<Item = S>,
S: AsRef<OsStr>,
{
concat_command(command, args).to_string_lossy().to_string()
}
pub fn bytes_to_path(bytes: &[u8]) -> Cow<'_, Path> {
AsRef::<Path>::as_ref(OsStr::from_bytes(bytes)).into()
}
#[cfg(any(test, feature = "test"))]
pub fn vec_to_path_buf(vec: Vec<u8>) -> Result<PathBuf> {
use std::os::unix::ffi::OsStringExt as _;
Ok(PathBuf::from(OsString::from_vec(vec)))
}
pub fn normalize(path: &Path) -> PathBuf {
let components = path.components();
let path = PathBuf::with_capacity(path.as_os_str().len());
let mut path = components.fold(path, |mut path, component| {
match component {
Component::Prefix(..) | Component::RootDir => (),
Component::CurDir => return path,
Component::ParentDir => {
if let Some(prev) = path.components().next_back() {
match prev {
Component::CurDir => {
unreachable!()
},
Component::Prefix(..) | Component::RootDir | Component::ParentDir => (),
Component::Normal(..) => {
path.pop();
return path
},
}
}
},
Component::Normal(c) => {
path.push(c);
return path
},
}
path.push(component.as_os_str());
path
});
let () = path.shrink_to_fit();
path
}
pub fn canonicalize_non_strict(path: &Path) -> io::Result<PathBuf> {
let mut path = path;
let input = path;
let resolved = loop {
match canonicalize(path) {
Ok(resolved) => break Cow::Owned(resolved),
Err(err) if err.kind() == ErrorKind::NotFound => (),
e => return e,
}
match path.parent() {
None => {
path = Path::new("");
break Cow::Borrowed(path)
},
Some(parent) if parent == Path::new("") => {
path = parent;
break Cow::Owned(current_dir()?)
},
Some(parent) => {
let parent_len = parent.as_os_str().as_bytes().len();
let path_bytes = path.as_os_str().as_bytes();
path = Path::new(OsStr::from_bytes(
path_bytes
.get(parent_len + 1..)
.expect("constructed path has no trailing separator"),
));
},
}
};
let input_bytes = input.as_os_str().as_bytes();
let path_len = path.as_os_str().as_bytes().len();
let unresolved = input_bytes
.get(path_len..)
.expect("failed to access input path sub-string");
let complete = resolved.join(OsStr::from_bytes(unresolved));
let normalized = normalize(&complete);
Ok(normalized)
}
pub fn check<C, A, S>(command: C, args: A) -> Result<bool>
where
C: AsRef<OsStr>,
A: IntoIterator<Item = S> + Clone,
S: AsRef<OsStr>,
{
let status = Command::new(command.as_ref())
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.args(args.clone())
.status()
.with_context(|| format!("failed to run `{}`", format_command(command.as_ref(), args)))?;
Ok(status.success())
}
fn evaluate<C, A, S>(output: &Output, command: C, args: A) -> Result<()>
where
C: AsRef<OsStr>,
A: IntoIterator<Item = S>,
S: AsRef<OsStr>,
{
if !output.status.success() {
let code = if let Some(code) = output.status.code() {
format!(" ({code})")
} else {
" (terminated by signal)".to_string()
};
let stderr = String::from_utf8_lossy(&output.stderr);
let stderr = stderr.trim_end();
let stderr = if !stderr.is_empty() {
format!(": {stderr}")
} else {
String::new()
};
bail!(
"`{}` reported non-zero exit-status{code}{stderr}",
format_command(command, args),
);
}
Ok(())
}
fn run_impl<C, A, S>(command: C, args: A, stdout: Stdio) -> Result<Output>
where
C: AsRef<OsStr>,
A: IntoIterator<Item = S> + Clone,
S: AsRef<OsStr>,
{
let output = Command::new(command.as_ref())
.stdin(Stdio::null())
.stdout(stdout)
.args(args.clone())
.output()
.with_context(|| {
format!(
"failed to run `{}`",
format_command(command.as_ref(), args.clone())
)
})?;
let () = evaluate(&output, command, args)?;
Ok(output)
}
pub fn run<C, A, S>(command: C, args: A) -> Result<()>
where
C: AsRef<OsStr>,
A: IntoIterator<Item = S> + Clone,
S: AsRef<OsStr>,
{
let _output = run_impl(command, args, Stdio::null())?;
Ok(())
}
pub fn output<C, A, S>(command: C, args: A) -> Result<Vec<u8>>
where
C: AsRef<OsStr>,
A: IntoIterator<Item = S> + Clone,
S: AsRef<OsStr>,
{
let output = run_impl(command, args, Stdio::piped())?;
Ok(output.stdout)
}
pub fn pipeline<C1, A1, S1, C2, A2, S2>(
command1: C1,
args1: A1,
command2: C2,
args2: A2,
) -> Result<()>
where
C1: AsRef<OsStr>,
A1: IntoIterator<Item = S1> + Clone,
S1: AsRef<OsStr>,
C2: AsRef<OsStr>,
A2: IntoIterator<Item = S2> + Clone,
S2: AsRef<OsStr>,
{
let mut child1 = Command::new(command1.as_ref())
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.args(args1.clone())
.spawn()
.with_context(|| {
format!(
"failed to run `{}`",
format_command(command1.as_ref(), args1.clone())
)
})?;
let stdout = child1
.stdout
.take()
.expect("created process does not have stdout set");
let child2 = Command::new(command2.as_ref())
.stdin(stdout)
.stdout(Stdio::null())
.stderr(Stdio::piped())
.args(args2.clone())
.spawn()
.with_context(|| {
format!(
"failed to run `{}`",
format_command(command2.as_ref(), args2.clone())
)
})?;
let output1 = child1.wait_with_output().with_context(|| {
format!(
"failed to run `{}`",
format_command(command1.as_ref(), args1.clone())
)
})?;
let output2 = child2.wait_with_output().with_context(|| {
format!(
"failed to run `{}`",
format_command(command2.as_ref(), args2.clone())
)
})?;
let () = evaluate(&output1, command1, args1)?;
let () = evaluate(&output2, command2, args2)?;
Ok(())
}
pub fn escape(character: &str, string: &str) -> String {
debug_assert_eq!(
character.len(),
1,
"string to escape (`{character}`) is not a single ASCII character"
);
string.replace(character, &(character.to_owned() + character))
}
pub fn unescape(character: &str, string: &str) -> String {
debug_assert_eq!(
character.len(),
1,
"string to escape (`{character}`) is not a single ASCII character"
);
string.replace(&(character.to_owned() + character), character)
}
pub fn split_once_escaped<'str>(
string: &'str str,
character: &str,
) -> Option<(&'str str, &'str str)> {
debug_assert_eq!(
character.len(),
1,
"string to escape (`{character}`) is not a single ASCII character"
);
debug_assert!(string.is_ascii(), "{string}");
let mut substr = string;
let mut subidx = 0usize;
while let Some(idx) = substr.find(character) {
let mut next_idx = idx + 1;
let next_str = substr.get(next_idx..)?;
if !next_str.starts_with(character) {
let first = string
.get(..subidx + idx)
.expect("calculated escape string sub-string out-of-bounds");
return Some((first, next_str))
} else {
next_idx += 1;
}
substr = substr.get(next_idx..)?;
subidx += next_idx;
}
None
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[test]
fn escape_unescape() {
fn test(string: &str, expected: &str) {
let escaped = escape("_", string);
assert_eq!(escaped, expected);
let unescaped = unescape("_", &escaped);
assert_eq!(unescaped, string);
}
test("", "");
test("_", "__");
test("a_", "a__");
test("_a_", "__a__");
test("a_b", "a__b");
test("a__b", "a____b");
test("a_b_c_d", "a__b__c__d");
test("a_b_c_d__", "a__b__c__d____");
}
#[test]
fn escaped_splitting() {
assert_eq!(split_once_escaped("foo", "_"), None);
assert_eq!(split_once_escaped("foo_", "_"), Some(("foo", "")));
assert_eq!(split_once_escaped("foo_bar", "_"), Some(("foo", "bar")));
assert_eq!(
split_once_escaped("foo_bar_baz", "_"),
Some(("foo", "bar_baz"))
);
assert_eq!(
split_once_escaped("foo__bar_baz", "_"),
Some(("foo__bar", "baz"))
);
assert_eq!(
split_once_escaped("foo_bar__baz", "_"),
Some(("foo", "bar__baz"))
);
assert_eq!(split_once_escaped("foo__bar", "_"), None);
assert_eq!(split_once_escaped("foo__", "_"), None);
assert_eq!(split_once_escaped("foo__bar__baz", "_"), None);
assert_eq!(split_once_escaped("_bar_baz", "_"), Some(("", "bar_baz")));
assert_eq!(split_once_escaped("__bar_baz", "_"), Some(("__bar", "baz")));
assert_eq!(split_once_escaped("_", "_"), Some(("", "")));
assert_eq!(split_once_escaped("__", "_"), None);
}
#[test]
fn path_normalization() {
assert_eq!(normalize(Path::new("tmp/foobar/..")), Path::new("tmp"));
assert_eq!(normalize(Path::new("/tmp/foobar/..")), Path::new("/tmp"));
assert_eq!(normalize(Path::new("/tmp/.")), Path::new("/tmp"));
assert_eq!(normalize(Path::new("/tmp/./blah")), Path::new("/tmp/blah"));
assert_eq!(normalize(Path::new("/tmp/../blah")), Path::new("/blah"));
assert_eq!(normalize(Path::new("./foo")), Path::new("foo"));
assert_eq!(
normalize(Path::new("./foo/")).as_os_str(),
Path::new("foo").as_os_str()
);
assert_eq!(normalize(Path::new("foo")), Path::new("foo"));
assert_eq!(
normalize(Path::new("foo/")).as_os_str(),
Path::new("foo").as_os_str()
);
assert_eq!(normalize(Path::new("../foo")), Path::new("../foo"));
assert_eq!(normalize(Path::new("../foo/")), Path::new("../foo"));
assert_eq!(
normalize(Path::new("./././relative-dir-that-does-not-exist/../file")),
Path::new("file")
);
}
#[test]
fn non_strict_canonicalization() {
let dir = current_dir().unwrap();
let path = Path::new("relative-path-that-does-not-exist");
let real = canonicalize_non_strict(path).unwrap();
assert_eq!(real, dir.join(path));
let dir = current_dir().unwrap();
let path = Path::new("relative-path-that-does-not-exist/");
let real = canonicalize_non_strict(path).unwrap();
assert_eq!(
real.as_os_str(),
dir
.join(Path::new("relative-path-that-does-not-exist"))
.as_os_str()
);
let path = Path::new("relative-dir-that-does-not-exist/file");
let real = canonicalize_non_strict(path).unwrap();
assert_eq!(real, dir.join(path));
let path = Path::new("./relative-dir-that-does-not-exist/file");
let real = canonicalize_non_strict(path).unwrap();
assert_eq!(real, dir.join(normalize(path)));
let path = Path::new("./././relative-dir-that-does-not-exist/../file");
let real = canonicalize_non_strict(path).unwrap();
assert_eq!(real, dir.join("file"));
let path = Path::new("../relative-path-that-does-not-exist");
let real = canonicalize_non_strict(path).unwrap();
assert_eq!(
real,
dir
.parent()
.unwrap()
.join("relative-path-that-does-not-exist")
);
let path = Path::new("../relative-dir-that-does-not-exist/file");
let real = canonicalize_non_strict(path).unwrap();
assert_eq!(
real,
dir
.parent()
.unwrap()
.join("relative-dir-that-does-not-exist/file")
);
let path = Path::new("/absolute-path-that-does-not-exist");
let real = canonicalize_non_strict(path).unwrap();
assert_eq!(real, path);
let path = Path::new("/absolute-dir-that-does-not-exist/file");
let real = canonicalize_non_strict(path).unwrap();
assert_eq!(real, path);
let dir = TempDir::new().unwrap();
let dir = dir.path();
let path = dir;
let real = canonicalize_non_strict(path).unwrap();
assert_eq!(real, path);
let path = dir.join("foobar");
let real = canonicalize_non_strict(&path).unwrap();
assert_eq!(real, path);
}
}