use path_clean::clean;
use std::collections::{BTreeMap, HashSet};
use std::fs::{canonicalize as canonicalize_path, symlink_metadata, File};
use std::io::{self, BufReader, Write};
use std::process::Command;
use walkdir::WalkDir;
use crate::crypto::HashAlgorithm;
use crate::interchange::Json;
use crate::models::{LinkMetadata, Metablock, TargetDescription};
use crate::{
crypto,
crypto::PrivateKey,
models::{LinkMetadataBuilder, VirtualTargetPath},
};
use crate::{Error, Result};
pub fn record_artifact(
path: &str,
hash_algorithms: &[HashAlgorithm],
lstrip_paths: Option<&[&str]>,
) -> Result<(VirtualTargetPath, TargetDescription)> {
let file = File::open(path)?;
let mut reader = BufReader::new(file);
let (_length, hashes) = crypto::calculate_hashes(&mut reader, hash_algorithms)?;
let lstripped_path = apply_left_strip(path, lstrip_paths)?;
Ok((VirtualTargetPath::new(lstripped_path)?, hashes))
}
fn apply_left_strip(path: &str, lstrip_paths: Option<&[&str]>) -> Result<String> {
if lstrip_paths.is_none() {
return Ok(String::from(path));
}
let l_paths = lstrip_paths.unwrap();
let mut stripped_path = path;
let mut find_prefix = "";
for l_path in l_paths.iter() {
if !path.starts_with(l_path) {
continue;
}
if !find_prefix.is_empty() && find_prefix.len() >= l_path.len() {
continue;
}
stripped_path = path.strip_prefix(l_path).ok_or_else(|| {
Error::from(io::Error::new(
std::io::ErrorKind::Other,
format!(
"Lstrip Error: error stripping {} from path {}",
l_path, path
),
))
})?;
find_prefix = l_path;
}
Ok(String::from(stripped_path))
}
pub fn record_artifacts(
paths: &[&str],
hash_algorithms: Option<&[&str]>,
lstrip_paths: Option<&[&str]>,
) -> Result<BTreeMap<VirtualTargetPath, TargetDescription>> {
let available_algorithms = HashAlgorithm::return_all();
let hash_algorithms = match hash_algorithms {
Some(hashes) => {
let mut map = vec![];
for hash in hashes {
if !available_algorithms.contains_key(*hash) {
return Err(Error::UnknownHashAlgorithm((*hash).to_string()));
}
let value = available_algorithms.get(*hash).unwrap();
map.push(value.clone());
}
map
}
None => vec![HashAlgorithm::Sha256],
};
let hash_algorithms = &hash_algorithms[..];
let mut artifacts: BTreeMap<VirtualTargetPath, TargetDescription> = BTreeMap::new();
for path in paths {
let path = clean(path);
let mut walker = WalkDir::new(path).follow_links(true).into_iter();
let mut visited_sym_links = HashSet::new();
while let Some(entry) = walker.next() {
let path = dir_entry_to_path(entry)?;
let file_type = std::fs::symlink_metadata(&path)?.file_type();
if file_type.is_symlink() {
if visited_sym_links.contains(&path) {
walker.skip_current_dir();
} else {
visited_sym_links.insert(String::from(&path));
let s_path = match std::fs::read_link(&path)?.as_path().to_str() {
Some(str) => String::from(str),
None => break,
};
if symlink_metadata(&s_path)?.file_type().is_file() {
let (virtual_target_path, hashes) =
record_artifact(&path, hash_algorithms, lstrip_paths)?;
if artifacts.contains_key(&virtual_target_path) {
return Err(Error::LinkGatheringError(format!(
"non unique stripped path {}",
virtual_target_path.to_string()
)));
}
artifacts.insert(virtual_target_path, hashes);
}
}
}
if file_type.is_file() {
let (virtual_target_path, hashes) =
record_artifact(&path, hash_algorithms, lstrip_paths)?;
if artifacts.contains_key(&virtual_target_path) {
return Err(Error::LinkGatheringError(format!(
"non unique stripped path {}",
virtual_target_path.to_string()
)));
}
artifacts.insert(virtual_target_path, hashes);
}
}
}
Ok(artifacts)
}
pub fn run_command(cmd_args: &[&str], run_dir: Option<&str>) -> Result<BTreeMap<String, String>> {
let mut byproducts: BTreeMap<String, String> = BTreeMap::new();
if cmd_args.is_empty() {
return Ok(byproducts);
}
let executable = cmd_args[0];
let args = (&cmd_args[1..])
.iter()
.map(|arg| {
if VirtualTargetPath::new((*arg).into()).is_ok() {
let absolute_path = canonicalize_path(*arg);
match absolute_path {
Ok(path_buf) => match path_buf.to_str() {
Some(p) => p,
None => *arg,
},
Err(_) => *arg,
};
}
*arg
})
.collect::<Vec<&str>>();
let mut cmd = Command::new(executable);
let mut cmd = cmd.args(args);
if let Some(dir) = run_dir {
cmd = cmd.current_dir(dir)
}
let output = match cmd.output() {
Ok(out) => out,
Err(err) => {
return Err(Error::IllegalArgument(format!(
"Something went wrong with run_command inside in_toto_run. Error: {:?}",
err
)))
}
};
io::stdout().write_all(&output.stdout)?;
io::stderr().write_all(&output.stderr)?;
let stdout = match String::from_utf8(output.stdout) {
Ok(output) => output,
Err(error) => {
return Err(Error::from(io::Error::new(
std::io::ErrorKind::Other,
format!("Utf8Error: {}", error),
)))
}
};
let stderr = match String::from_utf8(output.stderr) {
Ok(output) => output,
Err(error) => {
return Err(Error::from(io::Error::new(
std::io::ErrorKind::Other,
format!("Utf8Error: {}", error),
)))
}
};
let status = match output.status.code() {
Some(code) => code.to_string(),
None => "Process terminated by signal".to_string(),
};
byproducts.insert("stdout".to_string(), stdout);
byproducts.insert("stderr".to_string(), stderr);
byproducts.insert("return-value".to_string(), status);
Ok(byproducts)
}
pub fn in_toto_run(
name: &str,
run_dir: Option<&str>,
material_paths: &[&str],
product_paths: &[&str],
cmd_args: &[&str],
key: Option<&PrivateKey>,
hash_algorithms: Option<&[&str]>,
lstrip_paths: Option<&[&str]>,
) -> Result<Metablock<Json, LinkMetadata>> {
let materials = record_artifacts(material_paths, hash_algorithms, lstrip_paths)?;
let byproducts = run_command(cmd_args, run_dir)?;
let products = record_artifacts(product_paths, hash_algorithms, lstrip_paths)?;
let link_metadata_builder = LinkMetadataBuilder::new()
.name(name.to_string())
.materials(materials)
.byproducts(byproducts)
.products(products);
match key {
Some(k) => link_metadata_builder.signed::<Json>(k),
None => link_metadata_builder.unsigned::<Json>(),
}
}
fn dir_entry_to_path(
entry: std::result::Result<walkdir::DirEntry, walkdir::Error>,
) -> Result<String> {
let path = match entry {
Ok(dir_entry) => match dir_entry.path().to_str() {
Some(str) => String::from(str),
None => {
return Err(Error::IllegalArgument(format!(
"Invalid Path {}; non-UTF-8 string",
dir_entry.path().display()
)))
}
},
Err(error) => {
if error.loop_ancestor().is_some() {
match error.path() {
None => {
return Err(Error::from(io::Error::new(
std::io::ErrorKind::Other,
format!("Walkdir Error: {}", error),
)))
}
Some(error_path) => {
let sym_path = match error_path.to_str() {
Some(str) => String::from(str),
None => {
return Err(Error::IllegalArgument(format!(
"Invalid Path {}; non-UTF-8 string",
error_path.display()
)))
}
};
sym_path
}
}
} else {
return Err(Error::from(io::Error::new(
std::io::ErrorKind::Other,
format!("Walkdir Error: {}", error),
)));
}
}
};
Ok(clean(&path))
}
#[cfg(test)]
mod test {
use data_encoding::HEXLOWER;
use std::collections::HashMap;
use super::*;
fn create_target_description(
hash_algorithm: crypto::HashAlgorithm,
hash_value: &[u8],
) -> TargetDescription {
let mut hash = HashMap::new();
hash.insert(
hash_algorithm,
crypto::HashValue::new(HEXLOWER.decode(hash_value).unwrap()),
);
hash
}
#[test]
fn test_record_artifacts() {
let mut expected: BTreeMap<VirtualTargetPath, TargetDescription> = BTreeMap::new();
expected.insert(
VirtualTargetPath::new("tests/test_runlib/.hidden/foo".to_string()).unwrap(),
create_target_description(
crypto::HashAlgorithm::Sha256,
b"7d865e959b2466918c9863afca942d0fb89d7c9ac0c99bafc3749504ded97730",
),
);
expected.insert(
VirtualTargetPath::new("tests/test_runlib/.hidden/.bar".to_string()).unwrap(),
create_target_description(
crypto::HashAlgorithm::Sha256,
b"b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c",
),
);
expected.insert(
VirtualTargetPath::new("tests/test_runlib/hello./world".to_string()).unwrap(),
create_target_description(
crypto::HashAlgorithm::Sha256,
b"25623b53e0984428da972f4c635706d32d01ec92dcd2ab39066082e0b9488c9d",
),
);
assert_eq!(
record_artifacts(&["tests/test_runlib"], None, None).unwrap(),
expected
);
assert_eq!(record_artifacts(&["tests"], None, None).is_ok(), true);
assert_eq!(
record_artifacts(&["file-does-not-exist"], None, None).is_err(),
true
);
}
#[test]
fn test_prefix_record_artifacts() {
let mut expected: BTreeMap<VirtualTargetPath, TargetDescription> = BTreeMap::new();
expected.insert(
VirtualTargetPath::new("world".to_string()).unwrap(),
create_target_description(
crypto::HashAlgorithm::Sha256,
b"25623b53e0984428da972f4c635706d32d01ec92dcd2ab39066082e0b9488c9d",
),
);
assert_eq!(
record_artifacts(
&["tests/test_prefix/left"],
None,
Some(&["tests/test_prefix/left/"])
)
.unwrap(),
expected
);
assert_eq!(
record_artifacts(
&["tests/test_prefix"],
None,
Some(&["tests/test_prefix/left/", "tests/test_prefix/right/"])
)
.is_err(),
true
);
}
#[test]
fn test_left_strip() {
let mut stripped_path: String;
stripped_path = apply_left_strip(
"tests/test_runlib/.hidden/foo",
Some(&["tests/test_runlib"]),
)
.unwrap();
assert_eq!(stripped_path, "/.hidden/foo");
stripped_path = apply_left_strip(
"tests/test_runlib/.hidden/foo",
Some(&["tests/test_runlib/"]),
)
.unwrap();
assert_eq!(stripped_path, ".hidden/foo");
stripped_path = apply_left_strip(
"tests/test_runlib/.hidden/foo",
Some(&["tests/test_runlib/.hidden/"]),
)
.unwrap();
assert_eq!(stripped_path, "foo");
stripped_path = apply_left_strip(
"tests/test_runlib/.hidden/foo",
Some(&["path-does-not-exist"]),
)
.unwrap();
assert_eq!(stripped_path, "tests/test_runlib/.hidden/foo");
stripped_path = apply_left_strip(
"tests/test_runlib/.hidden/foo",
Some(&["path-does-not-exist", "tests/"]),
)
.unwrap();
assert_eq!(stripped_path, "test_runlib/.hidden/foo");
stripped_path = apply_left_strip(
"tests/test_runlib/.hidden/foo",
Some(&["tests/", "tests/test_runlib/.hidden/"]),
)
.unwrap();
assert_eq!(stripped_path, "foo");
}
#[test]
fn test_run_command() {
let byproducts = run_command(&["sh", "-c", "printf hello"], Some("tests")).unwrap();
let mut expected = BTreeMap::new();
expected.insert("stdout".to_string(), "hello".to_string());
expected.insert("stderr".to_string(), "".to_string());
expected.insert("return-value".to_string(), "0".to_string());
assert_eq!(byproducts, expected);
assert_eq!(
run_command(&["command-does-not-exist", "true"], None).is_err(),
true
);
}
}