mod error;
use std::env::{split_paths, var_os};
use std::fs::File;
use std::io::Read;
use std::path::{Path, PathBuf};
use bzip2::read::BzDecoder;
use glob::glob;
use semver::{Version, VersionReq};
use tar::Archive;
pub use error::Error;
const DEFAULT_SCAR_PATH: &str = "/usr/share/scar";
const SCAR_PATH_ENV_VAR: &str = "SCAR_PATH";
pub fn default_scar_path() -> Vec<PathBuf> {
match var_os(SCAR_PATH_ENV_VAR) {
Some(paths) => split_paths(&paths).collect(),
None => vec![PathBuf::from(DEFAULT_SCAR_PATH)],
}
}
pub struct SmartContractArchive {
pub contract: Vec<u8>,
pub metadata: SmartContractMetadata,
}
impl SmartContractArchive {
pub fn from_scar_file<P: AsRef<Path>>(
name: &str,
version: &str,
paths: &[P],
) -> Result<SmartContractArchive, Error> {
let file_path = find_scar(name, version, paths)?;
let scar_file = File::open(&file_path).map_err(|err| {
Error::new_with_source(
&format!("failed to open file {}", file_path.display()),
err.into(),
)
})?;
let mut archive = Archive::new(BzDecoder::new(scar_file));
let archive_entries = archive.entries().map_err(|err| {
Error::new_with_source(
&format!("failed to read scar file {}", file_path.display()),
err.into(),
)
})?;
let mut metadata = None;
let mut contract = None;
for entry in archive_entries {
let mut entry = entry.map_err(|err| {
Error::new_with_source(
&format!(
"invalid scar file: failed to read archive entry from {}",
file_path.display()
),
err.into(),
)
})?;
let path = entry
.path()
.map_err(|err| {
Error::new_with_source(
&format!(
"invalid scar file: failed to get path of archive entry from {}",
file_path.display()
),
err.into(),
)
})?
.into_owned();
if path_is_manifest(&path) {
metadata = Some(serde_yaml::from_reader(entry).map_err(|err| {
Error::new_with_source(
&format!(
"invalid scar file: manifest.yaml invalid in {}",
file_path.display()
),
err.into(),
)
})?);
} else if path_is_wasm(&path) {
let mut contract_bytes = vec![];
entry.read_to_end(&mut contract_bytes).map_err(|err| {
Error::new_with_source(
&format!(
"invalid scar file: failed to read smart contract in {}",
file_path.display()
),
err.into(),
)
})?;
contract = Some(contract_bytes);
}
}
let contract = contract.ok_or_else(|| {
Error::new(&format!(
"invalid scar file: smart contract not found in {}",
file_path.display()
))
})?;
let metadata: SmartContractMetadata = metadata.ok_or_else(|| {
Error::new(&format!(
"invalid scar file: manifest.yaml not found in {}",
file_path.display()
))
})?;
validate_metadata(name, &metadata.name)?;
Ok(SmartContractArchive { contract, metadata })
}
}
#[derive(Debug, Deserialize, Serialize)]
pub struct SmartContractMetadata {
pub name: String,
pub version: String,
pub inputs: Vec<String>,
pub outputs: Vec<String>,
}
fn find_scar<P: AsRef<Path>>(name: &str, version: &str, paths: &[P]) -> Result<PathBuf, Error> {
let file_name_pattern = format!("{}_*.scar", name);
validate_scar_file_name(name)?;
let version_req = VersionReq::parse(version)?;
paths
.iter()
.map(|path| {
let file_path_pattern = path.as_ref().join(&file_name_pattern);
let pattern_string = file_path_pattern
.to_str()
.ok_or_else(|| Error::new("name is not valid UTF-8"))?;
Ok(glob(pattern_string)?)
})
.collect::<Result<Vec<_>, Error>>()?
.into_iter()
.flatten()
.filter_map(|path_res| path_res.ok())
.filter_map(|path| {
let file_stem = path.file_stem()?.to_str()?;
let version_str = (*file_stem.splitn(2, '_').collect::<Vec<_>>().get(1)?).to_string();
let version = Version::parse(&version_str).ok()?;
if version_req.matches(&version) {
Some((path, version))
} else {
None
}
})
.max_by(|(_, x_version), (_, y_version)| x_version.cmp(y_version))
.map(|(path, _)| path)
.ok_or_else(|| {
let paths = paths
.iter()
.map(|path| path.as_ref().display())
.collect::<Vec<_>>();
Error::new(&format!(
"could not find contract '{}' that meets version requirement '{}' in paths '{:?}'",
name, version_req, paths,
))
})
}
fn validate_scar_file_name(name: &str) -> Result<(), Error> {
if name.contains('_') {
return Err(Error::new(&format!(
"invalid scar file name, must not include '_': {}",
name
)));
}
Ok(())
}
fn validate_metadata(file_name: &str, contract_name: &str) -> Result<(), Error> {
if file_name != contract_name.replace('_', "-") {
return Err(Error::new(&format!(
"scar file name `{}` does not match contract name in manifest `{}`",
file_name, contract_name,
)));
}
Ok(())
}
fn path_is_manifest(path: &std::path::Path) -> bool {
path.file_name()
.map(|file_name| file_name == "manifest.yaml")
.unwrap_or(false)
}
fn path_is_wasm(path: &std::path::Path) -> bool {
match path.extension() {
Some(extension) => extension == "wasm",
None => false,
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
use std::path::Path;
use bzip2::write::BzEncoder;
use bzip2::Compression;
use serde::Serialize;
use serial_test::serial;
use tar::Builder;
use tempdir::TempDir;
const MOCK_CONTRACT_BYTES: &[u8] = &[0x00, 0x01, 0x02, 0x03];
#[test]
#[serial(scar_path)]
fn default_scar_path_env_unset() {
std::env::remove_var(SCAR_PATH_ENV_VAR);
assert_eq!(default_scar_path(), vec![PathBuf::from(DEFAULT_SCAR_PATH)]);
}
#[test]
#[serial(scar_path)]
fn default_scar_path_env_set() {
let paths = vec!["/test/dir", "/other/dir", ".", "~/"];
let joined_paths = std::env::join_paths(&paths).expect("failed to join paths");
std::env::set_var(SCAR_PATH_ENV_VAR, joined_paths);
assert_eq!(
default_scar_path(),
paths
.iter()
.map(|path| PathBuf::from(path))
.collect::<Vec<_>>()
);
}
#[test]
fn find_scar_invalid_name() {
let dir = new_temp_dir();
write_mock_scar(&dir, "non_existent", "0.1.0");
assert!(find_scar("non_existent", "0.1.0", &[&dir]).is_err());
}
#[test]
fn find_scar_invalid_version() {
let dir = new_temp_dir();
write_mock_scar(&dir, "nonexistent", "0.1..0");
assert!(find_scar("nonexistent", "0.1..0", &[&dir]).is_err());
}
#[test]
fn find_scar_no_matching_name() {
let dir = new_temp_dir();
write_mock_scar(&dir, "mock", "0.1.0");
assert!(find_scar("nonexistent", "0.1.0", &[&dir]).is_err());
}
#[test]
fn find_scar_not_in_path() {
let dir = new_temp_dir();
write_mock_scar(&dir, "mock", "0.1.0");
assert!(find_scar("mock", "0.1.0", &["/some/other/dir"]).is_err());
}
#[test]
fn find_scar_insufficient_release() {
let dir = new_temp_dir();
write_mock_scar(&dir, "mock", "0.1.0-dev");
assert!(find_scar("mock", "0.1.0", &[&dir]).is_err());
}
#[test]
fn find_scar_insufficient_patch() {
let dir = new_temp_dir();
write_mock_scar(&dir, "mock", "0.1.0");
assert!(find_scar("mock", "0.1.1", &[&dir]).is_err());
}
#[test]
fn find_scar_insufficient_minor() {
let dir = new_temp_dir();
write_mock_scar(&dir, "mock", "0.1.0");
assert!(find_scar("mock", "0.2.0", &[&dir]).is_err());
}
#[test]
fn find_scar_insufficient_major() {
let dir = new_temp_dir();
write_mock_scar(&dir, "mock", "0.1.0");
assert!(find_scar("mock", "1.0.0", &[&dir]).is_err());
}
#[test]
fn find_scar_any_version() {
let dir = new_temp_dir();
let scar_path = write_mock_scar(&dir, "mock", "0.1.0");
assert_eq!(
find_scar("mock", "*", &[&dir]).expect("failed to find scar"),
scar_path
);
}
#[test]
fn find_scar_exact_version() {
let dir = new_temp_dir();
let scar_path = write_mock_scar(&dir, "mock", "0.1.2-dev");
assert_eq!(
find_scar("mock", "0.1.2-dev", &[&dir]).expect("failed to find scar"),
scar_path
);
}
#[test]
fn find_scar_minimum_minor() {
let dir = new_temp_dir();
let scar_path = write_mock_scar(&dir, "mock", "0.1.2");
assert_eq!(
find_scar("mock", "0.1", &[&dir]).expect("failed to find scar"),
scar_path
);
}
#[test]
fn find_scar_minimum_major() {
let dir = new_temp_dir();
let scar_path = write_mock_scar(&dir, "mock", "1.2.3");
assert_eq!(
find_scar("mock", "1", &[&dir]).expect("failed to find scar"),
scar_path
);
}
#[test]
fn find_scar_highest_matching_patch() {
let dir = new_temp_dir();
write_mock_scar(&dir, "mock", "0.1.2-dev");
let scar_path = write_mock_scar(&dir, "mock", "0.1.2");
assert_eq!(
find_scar("mock", "0.1.2", &[&dir]).expect("failed to find scar"),
scar_path
);
}
#[test]
fn find_scar_highest_matching_minor() {
let dir = new_temp_dir();
write_mock_scar(&dir, "mock", "0.1.1");
let scar_path = write_mock_scar(&dir, "mock", "0.1.2");
assert_eq!(
find_scar("mock", "0.1", &[&dir]).expect("failed to find scar"),
scar_path
);
}
#[test]
fn find_scar_highest_matching_major() {
let dir = new_temp_dir();
write_mock_scar(&dir, "mock", "1.1.0");
let scar_path = write_mock_scar(&dir, "mock", "1.2.0");
assert_eq!(
find_scar("mock", "1", &[&dir]).expect("failed to find scar"),
scar_path
);
}
#[test]
fn find_scar_highest_matching_across_paths() {
let thread_id = format!("{:?}", std::thread::current().id());
let dir1 = TempDir::new(&format!("{}1", thread_id)).expect("failed to create temp dir1");
let dir2 = TempDir::new(&format!("{}2", thread_id)).expect("failed to create temp dir2");
write_mock_scar(&dir1, "mock", "1.2.3");
let scar_path = write_mock_scar(&dir2, "mock", "1.2.4");
assert_eq!(
find_scar("mock", "1", &[&dir1, &dir2]).expect("failed to find scar"),
scar_path
);
}
#[test]
fn load_scar_file_successful() {
let dir = new_temp_dir();
write_mock_scar(&dir, "mock-scar", "1.0.0");
let scar = SmartContractArchive::from_scar_file("mock-scar", "1.0.0", &[&dir])
.expect("failed to load scar");
assert_eq!(scar.contract, MOCK_CONTRACT_BYTES);
assert_eq!(scar.metadata.name, mock_smart_contract_metadata().name);
assert_eq!(
scar.metadata.version,
mock_smart_contract_metadata().version
);
assert_eq!(scar.metadata.inputs, mock_smart_contract_metadata().inputs);
assert_eq!(
scar.metadata.outputs,
mock_smart_contract_metadata().outputs
);
}
#[test]
fn load_scar_manifest_not_found() {
let dir = new_temp_dir();
let contract_file_path = write_contract(&dir);
write_scar_file::<&Path>(
dir.as_ref(),
"mock-scar",
"1.0.0",
None,
Some(contract_file_path.as_path()),
);
assert!(SmartContractArchive::from_scar_file("mock-scar", "1.0.0", &[&dir]).is_err());
}
#[test]
fn load_scar_manifest_invalid() {
let dir = new_temp_dir();
let manifest_file_path = write_manifest(&dir, &[0x00]);
let contract_file_path = write_contract(&dir);
write_scar_file::<&Path>(
dir.as_ref(),
"mock-scar",
"1.0.0",
Some(manifest_file_path.as_path()),
Some(contract_file_path.as_path()),
);
assert!(SmartContractArchive::from_scar_file("mock-scar", "1.0.0", &[&dir]).is_err());
}
#[test]
fn load_scar_contract_not_found() {
let dir = new_temp_dir();
let manifest_file_path = write_manifest(&dir, &mock_smart_contract_metadata());
write_scar_file::<&Path>(
dir.as_ref(),
"mock-scar",
"1.0.0",
Some(manifest_file_path.as_path()),
None,
);
assert!(SmartContractArchive::from_scar_file("mock-scar", "1.0.0", &[&dir]).is_err());
}
#[test]
fn load_scar_manifest_not_matching() {
let dir = new_temp_dir();
let manifest_file_path = write_manifest(&dir, &mock_invalid_smart_contract_metadata());
write_scar_file::<&Path>(
dir.as_ref(),
"mock-scar",
"1.0.0",
Some(&manifest_file_path),
None,
);
assert!(SmartContractArchive::from_scar_file("mock-scar", "1.0.0", &[&dir]).is_err());
}
fn new_temp_dir() -> TempDir {
let thread_id = format!("{:?}", std::thread::current().id());
TempDir::new(&thread_id).expect("failed to create temp dir")
}
fn write_mock_scar<P: AsRef<Path>>(dir: P, name: &str, version: &str) -> PathBuf {
let manifest_file_path = write_manifest(&dir, &mock_smart_contract_metadata());
let contract_file_path = write_contract(&dir);
write_scar_file::<&Path>(
dir.as_ref(),
name,
version,
Some(manifest_file_path.as_path()),
Some(contract_file_path.as_path()),
)
}
fn write_manifest<P: AsRef<Path>, T: Serialize>(dir: P, manifest: &T) -> PathBuf {
let manifest_file_path = dir.as_ref().join("manifest.yaml");
let manifest_file =
File::create(manifest_file_path.as_path()).expect("failed to create manifest file");
serde_yaml::to_writer(manifest_file, manifest).expect("failed to write manifest file");
manifest_file_path
}
fn write_contract<P: AsRef<Path>>(dir: P) -> PathBuf {
let contract_file_path = dir.as_ref().join("mock.wasm");
let mut contract_file =
File::create(contract_file_path.as_path()).expect("failed to create contract file");
contract_file
.write_all(MOCK_CONTRACT_BYTES)
.expect("failed to write contract file");
contract_file_path
}
fn write_scar_file<P: AsRef<Path>>(
dir: P,
name: &str,
version: &str,
manifest_file_path: Option<P>,
contract_file_path: Option<P>,
) -> PathBuf {
let scar_file_path = dir.as_ref().join(format!("{}_{}.scar", name, version));
let scar_file = File::create(&scar_file_path).expect("failed to create scar file");
let mut scar_file_builder = Builder::new(BzEncoder::new(scar_file, Compression::fast()));
if let Some(manifest_file_path) = manifest_file_path {
scar_file_builder
.append_path_with_name(manifest_file_path, "manifest.yaml")
.expect("failed to add manifest to scar file");
}
if let Some(contract_file_path) = contract_file_path {
scar_file_builder
.append_path_with_name(contract_file_path, "mock.wasm")
.expect("failed to add contract to scar file");
}
scar_file_builder
.finish()
.expect("failed to write scar file");
scar_file_path
}
fn mock_smart_contract_metadata() -> SmartContractMetadata {
SmartContractMetadata {
name: "mock_scar".into(),
version: "1.0".into(),
inputs: vec!["abcdef".into()],
outputs: vec!["012345".into()],
}
}
fn mock_invalid_smart_contract_metadata() -> SmartContractMetadata {
SmartContractMetadata {
name: "invalid_scar".into(),
version: "1.0".into(),
inputs: vec!["abcdef".into()],
outputs: vec!["012345".into()],
}
}
}