use std::ops::Deref;
use std::path::{Path, PathBuf};
use serde::Deserialize;
use tempfile::TempDir;
use crate::{Error, Result};
mod copy;
mod document;
mod import;
mod local;
mod output;
mod template;
#[derive(Debug, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Action {
Copy(copy::Action),
Import(import::Action),
Local(local::Action),
Output(output::Action),
Template(template::Action),
Document(document::Action),
}
pub(crate) fn validate_contained_path(path: &Path, field: &str) -> Result<()> {
if path.is_absolute()
|| path
.components()
.any(|component| matches!(component, std::path::Component::ParentDir))
{
return Err(Error::io(
std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!(
"{field} must be relative and cannot contain '..': {}",
path.display()
),
),
"Invalid path in workflow action",
));
}
Ok(())
}
impl Action {
pub async fn execute(&self, pipe: &mut Pipe) -> Result<()> {
match self {
Action::Copy(action) => action.execute(pipe).await,
Action::Import(action) => action.execute(pipe).await,
Action::Local(action) => action.execute(pipe).await,
Action::Output(action) => action.execute(pipe).await,
Action::Template(action) => action.execute(pipe).await,
Action::Document(action) => action.execute(pipe).await,
}
}
}
#[derive(Debug)]
pub struct Pipe(Option<PipePath>, Option<PipePath>);
impl Pipe {
#[inline]
pub const fn new() -> Pipe {
Pipe(None, None)
}
#[inline]
pub fn pipe(self) -> Pipe {
Pipe(self.1, None)
}
#[inline]
pub fn input(&self) -> Option<PathBuf> {
self.0.as_deref().map(std::path::Path::to_path_buf)
}
#[inline]
pub fn output(&mut self, path: impl Into<PipePath>) -> PathBuf {
self.1.insert(path.into()).to_path_buf()
}
#[inline]
pub fn output_path(&mut self, path: &Path) -> PathBuf {
self.output(path)
}
#[inline]
pub fn output_temp(&mut self) -> Result<PathBuf> {
Ok(self.output(PipePath::temp()?))
}
#[allow(dead_code)]
#[inline]
pub fn output_temp_with_prefix(&mut self, prefix: &str) -> Result<PathBuf> {
Ok(self.output(PipePath::temp_with_prefix(prefix)?))
}
}
#[derive(Debug)]
pub enum PipePath {
Path(PathBuf),
Temp(TempDir),
}
impl PipePath {
fn path(path: &Path) -> PipePath {
PipePath::Path(path.into())
}
fn temp() -> Result<PipePath> {
Self::temp_with_prefix("adnv")
}
fn temp_with_prefix(prefix: &str) -> Result<PipePath> {
Ok(PipePath::Temp(
TempDir::with_prefix(prefix).map_err(Error::TempDirectory)?,
))
}
pub fn as_path(&self) -> &Path {
match self {
PipePath::Path(path) => path,
PipePath::Temp(dir) => dir.path(),
}
}
}
impl<'a> From<&'a Path> for PipePath {
fn from(path: &'a Path) -> Self {
Self::path(path)
}
}
impl From<PathBuf> for PipePath {
fn from(path: PathBuf) -> Self {
Self::Path(path)
}
}
impl From<TempDir> for PipePath {
fn from(tempdir: TempDir) -> Self {
Self::Temp(tempdir)
}
}
impl Deref for PipePath {
type Target = Path;
fn deref(&self) -> &Self::Target {
self.as_path()
}
}
#[cfg(test)]
mod tests {
use std::path::Path;
use super::validate_contained_path;
#[test]
fn contained_paths_are_accepted() {
for path in ["out", "docs/reference", "./docs", "a/b/c.md"] {
assert!(
validate_contained_path(Path::new(path), "test").is_ok(),
"rejected a legitimate path: {path}"
);
}
}
#[test]
fn pipe_carries_the_previous_output_once() {
let mut pipe = super::Pipe::new();
assert_eq!(pipe.input(), None, "a fresh pipe has no input");
pipe.output_path(Path::new("/x"));
assert_eq!(pipe.input(), None, "an output is not readable as input yet");
let pipe = pipe.pipe();
assert_eq!(
pipe.input(),
Some(Path::new("/x").to_path_buf()),
"the output of the previous step became the input"
);
let pipe = pipe.pipe();
assert_eq!(
pipe.input(),
None,
"a step that produced nothing must leave no input behind"
);
}
#[test]
fn escaping_paths_are_rejected() {
for path in [
"/etc",
"/home/user/.ssh/authorized_keys",
"../..",
"a/../../b",
] {
assert!(
validate_contained_path(Path::new(path), "test").is_err(),
"accepted an escaping path: {path}"
);
}
}
}