#![doc(html_root_url = "https://docs.rs/rust_release_artefact/0.1.3")]
#![warn(missing_docs)]
extern crate libflate;
#[macro_use]
extern crate log;
#[cfg(feature = "serde")]
#[macro_use]
extern crate serde;
extern crate tar;
extern crate walkdir;
extern crate xz2;
use std::collections;
use std::error;
use std::fmt;
use std::fs;
use std::io;
use std::path;
fn read_manifest<A: AsRef<path::Path>>(
root: A,
) -> Result<collections::BTreeSet<path::PathBuf>, Error> {
let root = root.as_ref();
let manifest_path = root.join("manifest.in");
debug!("Reading component manifest from {:?}", manifest_path);
let manifest = fs::File::open(manifest_path)?;
let manifest = io::BufReader::new(manifest);
let mut res = collections::BTreeSet::new();
use std::io::BufRead;
for each in manifest.lines() {
let line = each?;
debug!("Read line: {:?}", line);
if line.starts_with("file:") {
let path = path::PathBuf::new().join(&line[5..]);
debug!("Adding path {:?}", path);
res.insert(path);
} else if line.starts_with("dir:") {
let rel_path = path::Path::new(&line[4..])
.iter()
.collect::<path::PathBuf>();
let walk_root = root.join(rel_path);
for each in walkdir::WalkDir::new(&walk_root) {
let each = each?;
let path = each.path();
if !each.file_type().is_file() {
debug!("Item at {:?} is not a file, skipping", path);
continue;
}
let relpath = path.strip_prefix(&root).map_err(|_| {
Error::WildPath(walk_root.clone(), path.into())
})?;
debug!("Adding path {:?}", relpath);
res.insert(relpath.into());
}
} else {
return Err(Error::UnrecognisedManifestRule(line.into()));
}
}
Ok(res)
}
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
pub struct ExtractedArtefact {
pub version: String,
pub git_commit_hash: Option<String>,
pub components: collections::BTreeMap<String, Component>,
}
impl ExtractedArtefact {
pub fn new<P: AsRef<path::Path>>(
stage: P,
) -> Result<ExtractedArtefact, Error> {
use io::Read;
let stage = stage.as_ref();
debug!("Reading artefact from staging area {:?}", stage);
let artefact_path = {
let mut artefact_paths = stage
.read_dir()?
.collect::<Result<Vec<_>, io::Error>>()?
.into_iter()
.map(|entry| entry.path())
.filter(|path| path.join("rust-installer-version").is_file())
.collect::<Vec<_>>();
debug!("Found potential artefacts: {:?}", artefact_paths);
match artefact_paths.len() {
0 => return Err(Error::NoArtefacts(stage.to_owned())),
1 => artefact_paths.remove(0),
_ => return Err(Error::MultipleArtefacts(stage.to_owned())),
}
};
let artefact_format_path = artefact_path.join("rust-installer-version");
debug!("Reading artefact format from {:?}", artefact_format_path);
let mut artefact_format = String::new();
fs::File::open(artefact_format_path)?
.read_to_string(&mut artefact_format)?;
if artefact_format.trim() != "3" {
return Err(Error::UnrecognisedFormat(artefact_format));
}
let version_path = artefact_path.join("version");
debug!("Reading artefact version from {:?}", version_path);
let mut version = String::new();
fs::File::open(version_path)?.read_to_string(&mut version)?;
let hash_path = artefact_path.join("git-commit-hash");
debug!("Reading git commit hash from {:?}", hash_path);
let mut buf = String::new();
let git_commit_hash = fs::File::open(hash_path)
.and_then(|mut handle| handle.read_to_string(&mut buf))
.map(|_| buf)
.ok();
let components_path = artefact_path.join("components");
debug!("Reaading artefact components from {:?}", components_path);
let mut component_names = String::new();
fs::File::open(components_path)?.read_to_string(&mut component_names)?;
let mut components = collections::BTreeMap::new();
for name in component_names.lines() {
let root = artefact_path.join(name);
let files = read_manifest(&root)?;
if files.len() == 0 {
return Err(Error::EmptyComponent(name.into()));
}
components.insert(name.to_owned(), Component { root, files });
}
if components.len() == 0 {
return Err(Error::NoComponents);
}
Ok(ExtractedArtefact {
version,
git_commit_hash,
components,
})
}
pub fn from_tar_gz<R: io::BufRead, P: AsRef<path::Path>>(
source: R,
stage: P,
) -> Result<ExtractedArtefact, Error> {
debug!("Decompressing source with gzip");
let source = libflate::gzip::Decoder::new(source)?;
debug!("Unpacking source as tar file");
tar::Archive::new(source).unpack(stage.as_ref())?;
ExtractedArtefact::new(stage)
}
pub fn from_tar_xz<R: io::BufRead, P: AsRef<path::Path>>(
source: R,
stage: P,
) -> Result<ExtractedArtefact, Error> {
debug!("Decompressing source with xz");
let source = xz2::bufread::XzDecoder::new(source);
debug!("Unpacking source as tar file");
tar::Archive::new(source).unpack(stage.as_ref())?;
ExtractedArtefact::new(stage)
}
}
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
pub struct Component {
pub root: path::PathBuf,
pub files: collections::BTreeSet<path::PathBuf>,
}
impl Component {
pub fn install_to<P: AsRef<path::Path>>(
&self,
dest_root: P,
) -> io::Result<()> {
for relative_path in &self.files {
let relative_path = relative_path.iter().collect::<path::PathBuf>();
let source_path = self.root.join(&relative_path);
let dest_path = dest_root.as_ref().join(&relative_path);
debug!("Installing from {:?} to {:?}", source_path, dest_path);
fs::create_dir_all(
dest_path.parent().unwrap_or(dest_root.as_ref()),
)?;
fs::remove_file(&dest_path).or_else(|err| {
if err.kind() == io::ErrorKind::NotFound {
Ok(())
} else {
Err(err)
}
})?;
fs::hard_link(&source_path, &dest_path).or_else(|_| {
fs::copy(&source_path, &dest_path).map(|_| ())
})?;
}
Ok(())
}
}
#[derive(Debug)]
pub enum Error {
IoError(io::Error),
WalkDirError(walkdir::Error),
NoArtefacts(path::PathBuf),
MultipleArtefacts(path::PathBuf),
UnrecognisedFormat(String),
NoComponents,
EmptyComponent(String),
UnrecognisedManifestRule(String),
WildPath(path::PathBuf, path::PathBuf),
}
impl std::convert::From<io::Error> for Error {
fn from(err: io::Error) -> Error {
Error::IoError(err)
}
}
impl std::convert::From<walkdir::Error> for Error {
fn from(err: walkdir::Error) -> Error {
Error::WalkDirError(err)
}
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
&Error::IoError(ref inner) => write!(f, "{}", inner),
&Error::WalkDirError(ref inner) => write!(f, "{}", inner),
&Error::NoArtefacts(ref path) => {
write!(f, "Staging area {:?} contains no artefacts", path)
}
&Error::MultipleArtefacts(ref path) => {
write!(f, "Staging area {:?} contains multiple artefacts", path)
}
&Error::UnrecognisedFormat(ref value) => {
write!(f, "Artefact in unrecognised format {:?}", value)
}
&Error::NoComponents => write!(f, "Artefact has no components"),
&Error::EmptyComponent(ref name) => {
write!(f, "Artefact component {:?} contains no files", name)
}
&Error::UnrecognisedManifestRule(ref rule) => {
write!(f, "Component manifest has unrecognised rule {:?}", rule,)
}
&Error::WildPath(ref root, ref path) => write!(
f,
"While exploring {:?}, found outside path {:?}",
root, path,
),
}
}
}
impl error::Error for Error {
fn description(&self) -> &str {
match self {
&Error::IoError(ref inner) => {
<_ as error::Error>::description(inner)
}
&Error::WalkDirError(ref inner) => {
<_ as error::Error>::description(inner)
}
&Error::NoArtefacts(_) => "Staging area contains no artefacts",
&Error::MultipleArtefacts(_) => {
"Staging area contains multiple artefacts"
}
&Error::UnrecognisedFormat(_) => "Artefact is in an unknown format",
&Error::NoComponents => "Artefact contains no components",
&Error::EmptyComponent(_) => "Artefact component contains no files",
&Error::UnrecognisedManifestRule(_) => {
"Component manifest has unrecognised rule"
}
&Error::WildPath(_, _) => {
"While exploring within a directory, found a path outside it."
}
}
}
}
#[cfg(test)]
mod tests {
extern crate env_logger;
extern crate tempfile;
extern crate walkdir;
use std::collections;
use std::fs;
use std::io;
use std::path;
use std::io::Read;
use std::io::Write;
fn make_extracted_artefact() -> io::Result<tempfile::TempDir> {
let stage = tempfile::tempdir()?;
let artefact_path = stage.path().join("some-artefact");
fs::create_dir_all(&artefact_path)?;
fs::File::create(artefact_path.join("rust-installer-version"))?
.write(b"3\n")?;
fs::File::create(artefact_path.join("version"))?
.write(b"1.2.3 (4d90ac38c 2018-04-03)")?;
fs::File::create(artefact_path.join("git-commit-hash"))?
.write(b"4d90ac38c0b61bb69470b61ea2cccea0df48d9e5")?;
fs::File::create(artefact_path.join("components"))?
.write(b"component-a\ncomponent-b\n")?;
let component_a_path = artefact_path.join("component-a");
fs::create_dir_all(&component_a_path)?;
fs::File::create(component_a_path.join("a-file-1"))?
.write(b"a-data-1")?;
fs::File::create(component_a_path.join("a-file-2"))?
.write(b"a-data-2")?;
fs::File::create(component_a_path.join("manifest.in"))?
.write(b"file:a-file-1\nfile:a-file-2\n")?;
let component_b_path = artefact_path.join("component-b");
fs::create_dir_all(&component_b_path)?;
fs::File::create(component_b_path.join("b-file-1"))?
.write(b"b-data-1")?;
let component_b_subdir_path = component_b_path.join("subdir");
fs::create_dir_all(&component_b_subdir_path)?;
fs::File::create(component_b_subdir_path.join("sub-file-1"))?
.write(b"sub-data-1")?;
fs::File::create(component_b_subdir_path.join("sub-file-2"))?
.write(b"sub-data-2")?;
fs::File::create(component_b_subdir_path.join("sub-file-3"))?
.write(b"sub-data-3")?;
fs::File::create(component_b_path.join("manifest.in"))?
.write(b"file:b-file-1\ndir:subdir\n")?;
Ok(stage)
}
#[test]
fn open_extracted_artefact() {
let _ = env_logger::try_init();
let stage = make_extracted_artefact().unwrap();
let artefact = super::ExtractedArtefact::new(stage.path()).unwrap();
assert_eq!(artefact.version, "1.2.3 (4d90ac38c 2018-04-03)",);
assert_eq!(
artefact.git_commit_hash,
Some("4d90ac38c0b61bb69470b61ea2cccea0df48d9e5".into()),
);
assert_eq!(
artefact.components.keys().collect::<Vec<_>>(),
vec!["component-a", "component-b"],
);
assert_eq!(
artefact.components.get("component-a").unwrap(),
&super::Component {
root: stage.path().join("some-artefact/component-a"),
files: {
let mut res = collections::BTreeSet::new();
res.insert(path::PathBuf::new().join("a-file-1"));
res.insert(path::PathBuf::new().join("a-file-2"));
res
},
},
);
assert_eq!(
artefact.components.get("component-b").unwrap(),
&super::Component {
root: stage.path().join("some-artefact/component-b"),
files: {
let mut res = collections::BTreeSet::new();
res.insert(path::PathBuf::new().join("b-file-1"));
res.insert(path::PathBuf::new().join("subdir/sub-file-1"));
res.insert(path::PathBuf::new().join("subdir/sub-file-2"));
res.insert(path::PathBuf::new().join("subdir/sub-file-3"));
res
},
},
);
assert_eq!(artefact.components.get("component-c"), None);
}
#[test]
fn test_no_artefacts_found() {
let _ = env_logger::try_init();
let stage = make_extracted_artefact().unwrap();
fs::remove_file(
stage.path().join("some-artefact/rust-installer-version"),
).unwrap();
let err = super::ExtractedArtefact::new(stage.path()).expect_err(
"Artefact did not detect missing artefact format file?",
);
assert_eq!(
format!("{}", err),
format!("Staging area {:?} contains no artefacts", stage.path()),
);
}
#[test]
fn test_multiple_artefacts_found() {
let _ = env_logger::try_init();
let stage = make_extracted_artefact().unwrap();
let other_artefact_path = stage.path().join("other-artefact");
fs::create_dir_all(&other_artefact_path).unwrap();
fs::File::create(other_artefact_path.join("rust-installer-version"))
.unwrap()
.write(b"3\n")
.unwrap();
let err = super::ExtractedArtefact::new(stage.path()).expect_err(
"Artefact did not detect multiple artefact format files?",
);
assert_eq!(
format!("{}", err),
format!(
"Staging area {:?} contains multiple artefacts",
stage.path()
),
);
}
#[test]
fn test_wrong_artefact_format() {
let _ = env_logger::try_init();
let stage = make_extracted_artefact().unwrap();
fs::OpenOptions::new()
.write(true)
.truncate(true)
.open(stage.path().join("some-artefact/rust-installer-version"))
.unwrap()
.write(b"37")
.unwrap();
let err = super::ExtractedArtefact::new(stage.path())
.expect_err("Artefact did not detect bogus version?");
assert_eq!(
format!("{}", err),
"Artefact in unrecognised format \"37\"",
);
}
#[test]
fn test_artefact_with_no_version() {
let _ = env_logger::try_init();
let stage = make_extracted_artefact().unwrap();
fs::remove_file(stage.path().join("some-artefact/version")).unwrap();
let err = super::ExtractedArtefact::new(stage.path())
.expect_err("Artefact did not detect missing version?");
assert_eq!(
format!("{}", err),
"No such file or directory (os error 2)",
);
}
#[test]
fn test_artefact_with_no_git_commit_hash() {
let _ = env_logger::try_init();
let stage = make_extracted_artefact().unwrap();
fs::remove_file(stage.path().join("some-artefact/git-commit-hash"))
.unwrap();
let artefact = super::ExtractedArtefact::new(stage.path())
.expect("Artefact requires git commit hash?");
assert_eq!(artefact.git_commit_hash, None);
}
#[test]
fn test_artefact_with_no_component_list() {
let _ = env_logger::try_init();
let stage = make_extracted_artefact().unwrap();
fs::remove_file(stage.path().join("some-artefact/components")).unwrap();
let err = super::ExtractedArtefact::new(stage.path())
.expect_err("Artefact did not detect missing component list?");
assert_eq!(
format!("{}", err),
"No such file or directory (os error 2)",
);
}
#[test]
fn test_artefact_with_empty_component_list() {
let _ = env_logger::try_init();
let stage = make_extracted_artefact().unwrap();
fs::OpenOptions::new()
.write(true)
.truncate(true)
.open(stage.path().join("some-artefact/components"))
.unwrap();
let err = super::ExtractedArtefact::new(stage.path())
.expect_err("Artefact did not detect empty component list?");
assert_eq!(format!("{}", err), "Artefact has no components");
}
#[test]
fn test_artefact_with_invalid_utf8_component_name() {
let _ = env_logger::try_init();
let stage = make_extracted_artefact().unwrap();
fs::OpenOptions::new()
.write(true)
.append(true)
.open(stage.path().join("some-artefact/components"))
.unwrap()
.write(b"\x88\x88\n")
.unwrap();
let err = super::ExtractedArtefact::new(stage.path())
.expect_err("Artefact did not detect invalid component name?");
assert_eq!(format!("{}", err), "stream did not contain valid UTF-8",);
}
#[test]
fn test_artefact_with_nul_in_component_name() {
let _ = env_logger::try_init();
let stage = make_extracted_artefact().unwrap();
fs::OpenOptions::new()
.write(true)
.append(true)
.open(stage.path().join("some-artefact/components"))
.unwrap()
.write(b"invalid\0component\n")
.unwrap();
let err = super::ExtractedArtefact::new(stage.path())
.expect_err("Artefact did not detect invalid component name?");
assert_eq!(format!("{}", err), "data provided contains a nul byte",);
}
#[test]
fn test_artefact_with_missing_components() {
let _ = env_logger::try_init();
let stage = make_extracted_artefact().unwrap();
fs::OpenOptions::new()
.write(true)
.append(true)
.open(stage.path().join("some-artefact/components"))
.unwrap()
.write(b"missing-component\n")
.unwrap();
let err = super::ExtractedArtefact::new(stage.path())
.expect_err("Artefact did not detect bogus component?");
assert_eq!(
format!("{}", err),
"No such file or directory (os error 2)",
);
}
#[test]
fn test_component_with_invalid_manifest_line() {
let _ = env_logger::try_init();
let stage = make_extracted_artefact().unwrap();
fs::OpenOptions::new()
.write(true)
.append(true)
.open(stage.path().join("some-artefact/component-a/manifest.in"))
.unwrap()
.write(b"bogus\n")
.unwrap();
let err = super::ExtractedArtefact::new(stage.path())
.expect_err("Artefact did not detect bogus manifest?");
assert_eq!(
format!("{}", err),
"Component manifest has unrecognised rule \"bogus\"",
);
}
#[test]
fn test_component_install() {
let _ = env_logger::try_init();
let stage = make_extracted_artefact().unwrap();
let artefact = super::ExtractedArtefact::new(stage.path()).unwrap();
fn files_in_path(path: &path::Path) -> Vec<path::PathBuf> {
let mut res = walkdir::WalkDir::new(path)
.into_iter()
.filter(|r| r.is_ok())
.map(|r| r.unwrap())
.filter(|dentry| dentry.file_type().is_file())
.map(|dentry| dentry.path().to_path_buf())
.collect::<Vec<_>>();
res.sort();
res
}
let dest_a = tempfile::tempdir().unwrap();
artefact
.components
.get("component-a")
.unwrap()
.install_to(dest_a.path())
.expect("Could not install component-a");
assert_eq!(
files_in_path(dest_a.path()),
vec![
dest_a.path().join("a-file-1"),
dest_a.path().join("a-file-2"),
],
);
let dest_b = tempfile::tempdir().unwrap();
artefact
.components
.get("component-b")
.unwrap()
.install_to(dest_b.path())
.expect("Could not install component-b");
assert_eq!(
files_in_path(dest_b.path()),
vec![
dest_b.path().join("b-file-1"),
dest_b.path().join("subdir/sub-file-1"),
dest_b.path().join("subdir/sub-file-2"),
dest_b.path().join("subdir/sub-file-3"),
],
);
}
#[test]
fn double_installation_does_not_corrupt_source() {
let stage = make_extracted_artefact().unwrap();
let artefact = super::ExtractedArtefact::new(stage.path()).unwrap();
let dest_b = tempfile::tempdir().unwrap();
let component_b = artefact.components.get("component-b").unwrap();
component_b
.install_to(dest_b.path())
.expect("Could not install component-b");
let mut b_file =
fs::File::open(component_b.root.join("b-file-1")).unwrap();
let mut buf = String::new();
b_file
.read_to_string(&mut buf)
.expect("Could not read b-file-1");
assert_eq!(buf, "b-data-1");
}
#[test]
fn errors_are_send_and_sync() {
fn assert_send<T: Send>() {}
fn assert_sync<T: Sync>() {}
assert_send::<super::Error>();
assert_sync::<super::Error>();
}
}