rust_release_artefact 0.1.3

Safely extract installable files from Rust release artefacts.
Documentation
#![doc(html_root_url = "https://docs.rs/rust_release_artefact/0.1.3")]
#![warn(missing_docs)]
//! Safely extract installable files from Rust release artefacts.
//!
//! Introduction
//! ============
//!
//! Each new release of the Rust toolchain
//! includes a number of components—some required, some optional—that
//! can be combined together.
//! These components are made available as artefacts
//! in a standard format that includes the files to be installed,
//! as well as metadata describing them.
//! Installing a component
//! is therefore more complex than
//! just extracting an archive,
//! since not all files should be placed in the destination.
//!
//! This library interprets a Rust artefact's metadata,
//! and provides a list of the components it contains,
//! as well as the specific list of installable files in each component.
//!
//! Once you've downloaded an artefact,
//! use [`ExtractedArtefact::from_tar_gz()`]
//! or [`ExtractedArtefact::from_tar_xz()`]
//! to extract it and retrieve the metadata
//! (the place you got the artefact from
//! should tell you which format it's in).
//! If you have previously extracted an artefact,
//! you can re-read the metadata directly
//! using [`ExtractedArtefact::new()`].
//!
//! [`ExtractedArtefact::new()`]: struct.ExtractedArtefact.html#method.new
//! [`ExtractedArtefact::from_tar_gz()`]: struct.ExtractedArtefact.html#method.from_tar_gz
//! [`ExtractedArtefact::from_tar_xz()`]: struct.ExtractedArtefact.html#method.from_tar_xz
//!
//! First Example
//! =============
//!
//!     extern crate rust_release_artefact as rra;
//!
//!     use std::error;
//!     use std::fs;
//!     use std::io;
//!     use std::path;
//!
//!     fn install_from_tar_gz(
//!         artefact_path: &path::Path,
//!         stage: &path::Path,
//!         component_name: &str,
//!         dest_root: &path::Path,
//!     ) -> Result<(), Box<error::Error>> {
//!         // Open the file containing the artefact.
//!         let handle = fs::File::open(artefact_path)?;
//!
//!         // Extract it to the given staging path and read the metadata.
//!         // We're assuming the staging path is already canonicalized, and
//!         // the artefact is in .tar.gz format.
//!         let extracted_artefact = rra::ExtractedArtefact::from_tar_gz(
//!             io::BufReader::new(handle),
//!             stage,
//!         )?;
//!
//!         // Get the requested component from the artefact.
//!         let component = extracted_artefact.components
//!             .get(component_name)
//!             .ok_or("Could not find component")?;
//!
//!         println!(
//!             "Installing component {} version {} to {:?}",
//!             component_name,
//!             extracted_artefact.version,
//!             dest_root,
//!         );
//!
//!         // Install the component into the destination.
//!         // We're also assuming dest_root is already canonicalized.
//!         component.install_to(dest_root)?;
//!
//!         // All done!
//!         Ok(())
//!     }
//!
//! Capabilities
//! ============
//!
//! Extract downloaded artefacts
//! ----------------------------
//!
//! Once you have downloaded a release artefact,
//! you can extract it into a staging area with
//! the [`ExtractedArtefact::from_tar_gz()`]
//! or [`ExtractedArtefact::from_tar_xz()`] functions
//! (depending on the format).
//!
//!     extern crate rust_release_artefact as rra;
//!
//!     use std::fs;
//!     use std::io;
//!     use std::path;
//!
//!     # fn example() -> Result<(), Box<std::error::Error>> {
//!     let handle = fs::File::open("path/to/artefact.tar.gz")?;
//!
//!     // Make sure the staging area exists.
//!     let staging_area = path::Path::new("path/to/staging/area");
//!     fs::create_dir_all(&staging_area)?;
//!
//!     // Canonicalize the staging area path, so Windows can handle long path
//!     // names.
//!     let staging_area = staging_area.canonicalize()?;
//!
//!     let extracted_artefact = rra::ExtractedArtefact::from_tar_gz(
//!         io::BufReader::new(handle),
//!         staging_area,
//!     )?;
//!     # Ok(())
//!     # }
//!
//! Read artefact metadata
//! ----------------------
//!
//! An [`ExtractedArtefact`] struct
//! represents the artefact's metadata,
//! including the components in this artefact
//! and the complete list of installable files in each component.
//!
//!     # extern crate rust_release_artefact as rra;
//!     # fn example() -> Result<(), Box<std::error::Error>> {
//!     # let extracted_artefact = rra::ExtractedArtefact::new("src")?;
//!
//!     println!("Version: {:?}", extracted_artefact.version);
//!     println!("Git commit hash: {:?}", extracted_artefact.git_commit_hash);
//!
//!     for (name, component) in &extracted_artefact.components {
//!         println!("Component: {:?} in {:?}", name, component.root);
//!         for path in &component.files {
//!             println!("  - {:?}", path);
//!         }
//!     }
//!     # Ok(())
//!     # }
//!
//! [`ExtractedArtefact`]: struct.ExtractedArtefact.html
//!
//! Install components to a given destination
//! -----------------------------------------
//!
//! The [`Component`] struct represents
//! an installable component of the artefact,
//! whose files are in the artefact's staging area,
//! ready to be installed to a target location.
//! The handy [`Component::install_to()`] method
//! does exactly that.
//!
//!     # extern crate rust_release_artefact as rra;
//!     # fn example() -> Result<(), Box<std::error::Error>> {
//!     # let extracted_artefact = rra::ExtractedArtefact::new("src")?;
//!     let component = extracted_artefact.components.get("my-component")
//!         .ok_or("no such component?")?;
//!
//!     // Make sure the destination exists.
//!     let destination = std::path::Path::new("path/to/install/destination");
//!     std::fs::create_dir_all(&destination)?;
//!
//!     // Canonicalize the staging area path, so Windows can handle long path
//!     // names.
//!     let destination = destination.canonicalize()?;
//!
//!     component.install_to(destination)?;
//!     # Ok(())
//!     # }
//!
//! [`Component`]: struct.Component.html
//! [`Component::install_to()`]: struct.Component.html#method.install_to
//!
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;

/// Returns the files described by the manifest in the given directory.
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:") {
            // Take the given path, split it on path-delimiters, then re-join
            // it back together. This does nothing on POSIX platforms, but on
            // Windows it should convert any slashes to backslashes. Windows
            // *generally* accepts either one in file-system paths, except in
            // the context of "NT-style" absolute paths when only backslashes
            // are accepted. So just to be safe, we'll convert them.
            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();

                // We only care about installing files.
                if !each.file_type().is_file() {
                    debug!("Item at {:?} is not a file, skipping", path);
                    continue;
                }

                let relpath = path.strip_prefix(&root).map_err(|_| {
                    // Somehow, even though we're not following
                    // symlinks, we've wandered into a part of the
                    // filesystem that's not below `root`. Not quite
                    // sure how this happened, but not much we can do about it
                    // now.
                    Error::WildPath(walk_root.clone(), path.into())
                })?;

                debug!("Adding path {:?}", relpath);
                res.insert(relpath.into());
            }
        } else {
            return Err(Error::UnrecognisedManifestRule(line.into()));
        }
    }

    Ok(res)
}

/// The metadata and content of a previously-extracted artefact.
///
/// Once you've downloaded a Rust release artefact,
/// you must extract it to a staging area
/// to examine it or install it.
/// Use the [`from_tar_gz()`] or [`from_tar_xz()`] methods
/// (whichever is appropriate)
/// to extract your artefact.
/// You may use a temporary directory as the staging area,
/// or (if you might want to work with the same artefact again later)
/// use a more permanent directory
/// and call the [`new()`] method in future.
///
/// When an `ExtractedArtefact` struct is created,
/// all of the artefact metadata is read into memory,
/// so it's still usable if the staging area is removed.
/// However, this does not include the artefact *data*,
/// so if you want to call [`Component::install_to()`]
/// you'll need to keep the staging area around at least that long.
///
/// Example
/// -------
///
/// See [Read artefact metadata](index.html#read-artefact-metadata)
/// in the crate-wide documentation.
///
/// [`Component::install_to()`]: struct.Component.html#method.install_to
/// [`new()`]: struct.ExtractedArtefact.html#method.new
/// [`from_tar_gz()`]: struct.ExtractedArtefact.html#method.from_tar_gz
/// [`from_tar_xz()`]: struct.ExtractedArtefact.html#method.from_tar_xz
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
pub struct ExtractedArtefact {
    /// The version of the package that was built to produce this artefact.
    ///
    /// This should be a [SemVer] version number of the form `X.Y.Z`,
    /// followed by a space,
    /// an open-parenthesis,
    /// a Git commit ID truncated to 9 digits,
    /// a space,
    /// the date of that commit in `YYYY-MM-DD` format,
    /// and a close-parenthesis: `1.2.3 (a1b2c3d4e 2018-04-17)`
    ///
    /// [SemVer]: https://semver.org/
    pub version: String,
    /// The complete Git commit ID of this version of this package.
    ///
    /// Not every artefact includes a commit ID.
    pub git_commit_hash: Option<String>,
    /// A mapping from component names to the files each component contains.
    ///
    /// An artefact must contain at least one component, and may contain more.
    pub components: collections::BTreeMap<String, Component>,
}

impl ExtractedArtefact {
    /// Read previously-extracted artefact metadata.
    ///
    /// `stage` must be the path to
    /// a directory containing an extracted artefact,
    /// such as one populated by
    /// the [`from_tar_gz()`] or [`from_tar_xz()`] methods.
    ///
    /// Errors
    /// ------
    ///
    /// This method may return any variant of [`Error`].
    ///
    /// The staging area will not be modfied,
    /// even if an error is returned.
    ///
    /// Portability
    /// -----------
    ///
    /// Rust release artefacts may include a deeply-nested directory structure,
    /// which can exceed Windows' traditional [260 character limit][winpath].
    /// It is recommended that you canonicalize `stage`
    /// before passing it to this method,
    /// since the Windows canonical form
    /// lifts the path length limit to around 32,000 characters.
    ///
    /// [winpath]: https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx#maxpath
    ///
    /// Example
    /// -------
    ///
    ///     extern crate rust_release_artefact as rra;
    ///
    ///     use std::error;
    ///     use std::fs;
    ///     use std::io;
    ///     use std::path;
    ///
    ///     fn extract_tar_gz_if_needed(
    ///         source: &path::Path,
    ///         stage: &path::Path,
    ///     ) -> Result<rra::ExtractedArtefact, rra::Error> {
    ///
    ///         // Before we go to all the effort of extracting this artefact,
    ///         // perhaps it's already been extracted?
    ///         match rra::ExtractedArtefact::new(stage) {
    ///
    ///             // Yep, looks good, let's use it.
    ///             Ok(extracted_artefact) => return Ok(extracted_artefact),
    ///
    ///             // The stage is empty, let's continue extracting.
    ///             Err(rra::Error::NoArtefacts(_)) => {}
    ///
    ///             // The stage already contains a broken artefact,
    ///             // and extracting another one won't help matters.
    ///             Err(e) => return Err(e)
    ///         }
    ///
    ///         // Extract the given artefact.
    ///         let handle = fs::File::open(source)?;
    ///         let extracted_artefact = rra::ExtractedArtefact::from_tar_gz(
    ///             io::BufReader::new(handle),
    ///             stage,
    ///         )?;
    ///
    ///         // Everything's fine!
    ///         Ok(extracted_artefact)
    ///     }
    ///
    /// [`Error`]: enum.Error.html
    /// [`from_tar_gz()`]: struct.ExtractedArtefact.html#method.from_tar_gz
    /// [`from_tar_xz()`]: struct.ExtractedArtefact.html#method.from_tar_xz
    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);

        // Figure out which subdirectory has the artefact in it.
        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())),
            }
        };

        // Check the artefact is in the correct format.
        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));
        }

        // Read the artefact version.
        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)?;

        // Read the artefact's commit hash.
        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();

        // Read the list of artefact components.
        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,
        })
    }

    /// Extract an artefact in `.tar.gz` format and read its metadata.
    ///
    /// The artefact `source` is extracted to the directory `stage`,
    /// and its content is read to produce an `ExtractedArtefact`.
    /// Nothing will be written outside `stage`,
    /// even if a malformed artefact attempts to do so.
    ///
    /// `stage` must already exist and be writable;
    /// if it is not empty then the extracted artefact
    /// may be corrupted by the existing contents.
    /// You may want to create a fresh, temporary `stage` directory
    /// every time you want to examine an artefact,
    /// but since extraction is expensive
    /// you could also create a permanent directory with a predictable name.
    /// If the directory exists,
    /// pass it to the [`new()`] method,
    /// otherwise create it and pass it to this function.
    ///
    /// If your artefact is in `.tar.xz` format,
    /// see [`from_tar_xz()`].
    ///
    /// Errors
    /// ------
    ///
    /// This function may return any of the variants of the [`Error`] enum.
    ///
    /// If an error is returned,
    /// the staging area may be left in an invalid state.
    /// You may re-attempt extracting the same artefact to the staging area,
    /// but don't extract another artefact to it,
    /// or try to use it with the [`new()`] method.
    ///
    /// Portability
    /// -----------
    ///
    /// Rust release artefacts may include a deeply-nested directory structure,
    /// which can exceed Windows' traditional [260 character limit][winpath].
    /// It is recommended that you canonicalize `stage`
    /// before passing it to this method,
    /// since the Windows canonical form
    /// lifts the path length limit to around 32,000 characters.
    ///
    /// [winpath]: https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx#maxpath
    ///
    /// Example
    /// -------
    ///
    ///     extern crate rust_release_artefact as rra;
    ///
    ///     use std::fs;
    ///     use std::io;
    ///     use std::path;
    ///
    ///     # fn example() -> Result<(), Box<std::error::Error>> {
    ///     let handle = fs::File::open("path/to/artefact.tar.gz")?;
    ///
    ///     // Make sure the staging area exists.
    ///     let staging_area = path::Path::new("path/to/staging/area");
    ///     fs::create_dir_all(&staging_area)?;
    ///
    ///     // Canonicalize the staging area path, so Windows can handle long path
    ///     // names.
    ///     let staging_area = staging_area.canonicalize()?;
    ///
    ///     let extracted_artefact = rra::ExtractedArtefact::from_tar_gz(
    ///         io::BufReader::new(handle),
    ///         staging_area,
    ///     )?;
    ///     # Ok(())
    ///     # }
    ///
    /// [`Error`]: enum.Error.html
    /// [`new()`]: struct.ExtractedArtefact.html#method.new
    /// [`from_tar_xz()`]: struct.ExtractedArtefact.html#method.from_tar_xz
    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)
    }

    /// Extract an artefact in `.tar.xz` format and read its metadata.
    ///
    /// The artefact `source` is extracted to the directory `stage`,
    /// and its content is read to produce an `ExtractedArtefact`.
    /// Nothing will be written outside `stage`,
    /// even if a malformed artefact attempts to do so.
    ///
    /// `stage` must already exist and be writable;
    /// if it is not empty then the extracted artefact
    /// may be corrupted by the existing contents.
    /// You may want to create a fresh, temporary `stage` directory
    /// every time you want to examine an artefact,
    /// but since extraction is expensive
    /// you could also create a permanent directory with a predictable name.
    /// If the directory exists,
    /// pass it to the [`new()`] method,
    /// otherwise create it and pass it to this function.
    ///
    /// If your artefact is in `.tar.gz` format,
    /// see [`from_tar_gz()`].
    ///
    /// Errors
    /// ------
    ///
    /// This function may return any of the variants of the [`Error`] enum.
    ///
    /// If an error is returned,
    /// the staging area may be left in an invalid state.
    /// You may re-attempt extracting the same artefact to the staging area,
    /// but don't extract another artefact to it,
    /// or try to use it with the [`new()`] method.
    ///
    /// Portability
    /// -----------
    ///
    /// Rust release artefacts may include a deeply-nested directory structure,
    /// which can exceed Windows' traditional [260 character limit][winpath].
    /// It is recommended that you canonicalize `stage`
    /// before passing it to this method,
    /// since the Windows canonical form
    /// lifts the path length limit to around 32,000 characters.
    ///
    /// [winpath]: https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx#maxpath
    ///
    /// Example
    /// -------
    ///
    ///     extern crate rust_release_artefact as rra;
    ///
    ///     use std::fs;
    ///     use std::io;
    ///     use std::path;
    ///
    ///     # fn example() -> Result<(), Box<std::error::Error>> {
    ///     let handle = fs::File::open("path/to/artefact.tar.xz")?;
    ///
    ///     // Make sure the staging area exists.
    ///     let staging_area = path::Path::new("path/to/staging/area");
    ///     fs::create_dir_all(&staging_area)?;
    ///
    ///     // Canonicalize the staging area path, so Windows can handle long path
    ///     // names.
    ///     let staging_area = staging_area.canonicalize()?;
    ///
    ///     let extracted_artefact = rra::ExtractedArtefact::from_tar_xz(
    ///         io::BufReader::new(handle),
    ///         staging_area,
    ///     )?;
    ///     # Ok(())
    ///     # }
    ///
    /// [`Error`]: enum.Error.html
    /// [`new()`]: struct.ExtractedArtefact.html#method.new
    /// [`from_tar_gz()`]: struct.ExtractedArtefact.html#method.from_tar_gz
    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)
    }
}

/// An installable component of an artefact.
///
/// A `Component` describes a subset of
/// the files in an [`ExtractedArtefact`],
/// which you may want to install in a target location.
/// For example,
/// in order to compile Rust programs,
/// you will need to install all the files of both
/// the `rustc` (compiler) and `rust-std` (standard library) components
/// into the same location.
///
/// A `Component` does not store all the relevant files itself,
/// but just their paths inside
/// the `ExtractedArtefact`'s staging area.
/// Therefore, if you want to use the files described by a `Component`,
/// you must ensure the staging area is not cleaned up
/// before you use them.
///
/// Example
/// -------
///
///     extern crate rust_release_artefact as rra;
///
///     use std::fs;
///     use std::io;
///     use std::path;
///     use std::process;
///
///     # fn example() -> Result<(), Box<std::error::Error>> {
///     # let handle = fs::File::open("path/to/artefact.tar.xz")?;
///     # let extracted_artefact = rra::ExtractedArtefact::from_tar_xz(
///     #     io::BufReader::new(handle),
///     #     "path/to/staging/area/",
///     # )?;
///     // Assume we have an artefact containing a Cargo component.
///     let cargo = extracted_artefact.components
///         .get("cargo")
///         .ok_or("Artefact does not contain cargo component")?;
///
///     // Make sure our destination exists.
///     let dest_root = path::Path::new("path/to/my/rust/toolchain");
///     fs::create_dir_all(&dest_root)?;
///
///     // Canonicalize our destination path, so Windows can handle long path
///     // names.
///     let dest_root = dest_root.canonicalize()?;
///
///     // Install it to our target location.
///     cargo.install_to(&dest_root)?;
///
///     // Now we should be able to run Cargo.
///     process::Command::new(dest_root.join("bin/cargo"))
///         .arg("build")
///         .arg("--release")
///         .spawn()?;
///
///     # Ok(())
///     # }
///
/// [`ExtractedArtefact`]: struct.ExtractedArtefact.html
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
pub struct Component {
    /// The absolute filesystem path to
    /// the directory containing all the files in the component.
    pub root: path::PathBuf,
    /// Relative paths from `root` to each file in the component.
    ///
    /// When installing this component,
    /// place each file at the same relative path
    /// inside the destination directory.
    pub files: collections::BTreeSet<path::PathBuf>,
}

impl Component {
    /// Install this component's files into the destination directory.
    ///
    /// `dest_root` should be a writable directory,
    /// or a path at which a writable directory may be created.
    ///
    /// For each relative path in `self.files`,
    /// this method constructs a source path by joining it to `self.root`,
    /// a destination path by joining it to `dest_root`,
    /// and then installs the file at the source path to the destination.
    ///
    /// To increase performance and reduce disk-space usage,
    /// this method will attempt to hard-link files rather than copying them.
    /// If hard-linking a file is not possible, it will be copied instead.
    ///
    /// Errors
    /// ------
    ///
    /// This method will return an error if `dest_root`
    /// or a directory within it could not be created,
    /// if a destination file already exists
    /// and could not be removed,
    /// or if a file could not be copied.
    ///
    /// Portability
    /// -----------
    ///
    /// Components may include a deeply-nested directory structure,
    /// which can exceed Windows' traditional [260 character limit][winpath].
    /// It is recommended that you use canonicalize `dest_root`
    /// before passing it to this method,
    /// since the Windows canonical form
    /// lifts the path length limit to around 32,000 characters.
    ///
    /// [winpath]: https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx#maxpath
    ///
    /// Example
    /// -------
    ///
    /// See the [`Component`] documentation.
    ///
    /// [`Component`]: struct.Component.html#example
    pub fn install_to<P: AsRef<path::Path>>(
        &self,
        dest_root: P,
    ) -> io::Result<()> {
        for relative_path in &self.files {
            // Take the given path, split it on path-delimiters, then re-join
            // it back together. This does nothing on POSIX platforms, but on
            // Windows it should convert any slashes to backslashes. Windows
            // *generally* accepts either one in file-system paths, except in
            // the context of "NT-style" absolute paths when only backslashes
            // are accepted. So just to be safe, we'll convert them.
            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);

            // Make sure the destination exists.
            fs::create_dir_all(
                dest_path.parent().unwrap_or(dest_root.as_ref()),
            )?;

            // Hard-linking will fail if the destination exists, and copying
            // might break things if the destination exists and is a hard-link
            // to the source. Since we want to blow away the target anyway,
            // let's just remove it immediately.
            fs::remove_file(&dest_path).or_else(|err| {
                if err.kind() == io::ErrorKind::NotFound {
                    // Already gone! No worries here.
                    Ok(())
                } else {
                    // Something else went wrong, report it.
                    Err(err)
                }
            })?;

            fs::hard_link(&source_path, &dest_path).or_else(|_| {
                // Can't hard-link it, let's try copying instead.
                fs::copy(&source_path, &dest_path).map(|_| ())
            })?;
        }

        Ok(())
    }
}

/// All the possible errors this crate can produce.
#[derive(Debug)]
pub enum Error {
    /// A filesystem access or permission error.
    ///
    /// Straight from `std::io` in the Rust standard library,
    /// this could be anything related to reading or writing the filesystem,
    /// including permission errors,
    /// missing files,
    /// or metadata files containing invalid UTF-8 data.
    IoError(io::Error),

    /// A filesystem navigation error.
    ///
    /// Straight from the `walkdir` crate,
    /// this error arises while interpreting artefact metadata
    /// and looking for all the files that belong to a component.
    WalkDirError(walkdir::Error),

    /// The given source or stage contains no valid release artefacts.
    ///
    /// Returned when an [`ExtractedArtefact`] is created
    /// from an archive or stage directory
    /// that does not contain release artefact metadata.
    ///
    /// The included `PathBuf` is the absolute path
    /// to the directory that does not contain any artefacts.
    ///
    /// [`ExtractedArtefact`]: struct.ExtractedArtefact.html
    NoArtefacts(path::PathBuf),

    /// The given source or stage contains multiple release artefacts.
    ///
    /// Returned when an [`ExtractedArtefact`] is created
    /// from an archive or stage directory
    /// that contains multiple sets of release artefact metadata.
    /// This is a problem because [`ExtractedArtefact`]
    /// can only represent a single artefact's metadata.
    ///
    /// The included `PathBuf` is the absolute path
    /// to the directory that contains multiple sets of artefact metadata.
    ///
    /// [`ExtractedArtefact`]: struct.ExtractedArtefact.html
    MultipleArtefacts(path::PathBuf),

    /// The given source or stage contains artefact metadata in an unrecognised
    /// format.
    ///
    /// Rust's release artefacts include a marker file
    /// that describes which version of the metadata format they use.
    /// This error is returned when an [`ExtractedArtefact`] is created
    /// from an archive or stage directory
    /// that contains a marker file
    /// that mentions an unrecognised metadata format.
    ///
    /// The included `String` is the content of the marker file.
    ///
    /// [`ExtractedArtefact`]: struct.ExtractedArtefact.html
    UnrecognisedFormat(String),

    /// The artefact metadata lists no components.
    ///
    /// Returned when an [`ExtractedArtefact`] is created
    /// from an archive or stage directory
    /// whose metadata includes an empty list of components.
    ///
    /// [`ExtractedArtefact`]: struct.ExtractedArtefact.html
    NoComponents,

    /// A component contains no files.
    ///
    /// Returned when an [`ExtractedArtefact`] is created
    /// from an archive or stage directory
    /// where a component's manifest lists no files.
    ///
    /// The included `String` is the name of the problem component.
    ///
    /// [`ExtractedArtefact`]: struct.ExtractedArtefact.html
    EmptyComponent(String),

    /// A component's manifest contains an unrecognised rule.
    ///
    /// Returned when an [`ExtractedArtefact`] is created
    /// from an archive or stage directory
    /// where a component's manifest
    /// (which is a list of rules describing
    /// which files this component installs)
    /// includes an unrecognised rule.
    ///
    /// The included `String` is the text of the unrecognised rule.
    ///
    /// [`ExtractedArtefact`]: struct.ExtractedArtefact.html
    UnrecognisedManifestRule(String),

    /// While scanning for files inside a component,
    /// a file was found outside that component's directory.
    ///
    /// This should not be possible,
    /// so if it happens something has gone horribly wrong.
    ///
    /// The included paths are the component directory being scanned,
    /// and the file that was found outside it, respectively.
    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();

        // Remove the format version marker
        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();

        // Create another format version marker in a different directory.
        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();

        // Change the format marker to a different value.
        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();

        // Remove the version file
        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();

        // Remove the version file
        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();

        // Remove the component list
        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();

        // Truncate the component list
        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();

        // Write invalid UTF-8 to the component list
        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();

        // Write invalid UTF-8 to the component list
        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();

        // Add an extra component to the list
        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();

        // Add a bogus entry to a component manifest.
        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
        }

        // Extract component-a to a temporary directory.
        let dest_a = tempfile::tempdir().unwrap();
        artefact
            .components
            .get("component-a")
            .unwrap()
            .install_to(dest_a.path())
            .expect("Could not install component-a");

        // Make sure we created all the files we expected.
        assert_eq!(
            files_in_path(dest_a.path()),
            vec![
                dest_a.path().join("a-file-1"),
                dest_a.path().join("a-file-2"),
            ],
        );

        // Extract component-b to a temporary directory.
        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();

        // Install component-b
        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's check the source is still OK.
        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>();
    }
}