blob: d294a72244ec344fd68b58e45673aadd1d067519 [file] [log] [blame]
// Copyright 2021 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
use anyhow::{anyhow, Context, Result};
use fuchsia_hash::Hash;
use fuchsia_merkle::MerkleTree;
use fuchsia_pkg::PackageManifest;
use pathdiff::diff_paths;
use std::collections::BTreeMap;
use std::fs::File;
use std::io::Write;
use std::path::{Path, PathBuf};
/// A list of mappings between blob merkle and path on the host.
#[derive(Default, Debug)]
pub struct BlobManifest {
/// Map between file merkle and path on the host.
/// The path is relative to the working directory.
packages: BTreeMap<Hash, PathBuf>,
}
impl BlobManifest {
/// Add all the files from the `package` on the host.
pub fn add_package(&mut self, package: PackageManifest) -> Result<()> {
let (blobs, subpackages) = package.into_blobs_and_subpackages();
for blob in blobs {
self.add_file_with_merkle(blob.source_path, blob.merkle);
}
for subpackage in subpackages {
let subpackage_manifest = PackageManifest::try_load_from(&subpackage.manifest_path)?;
if !self.packages.contains_key(&subpackage_manifest.hash()) {
self.add_package(subpackage_manifest).with_context(|| {
format!(
"adding subpackage '{}' from {}",
subpackage.name, subpackage.manifest_path
)
})?;
}
}
Ok(())
}
/// Add a file from the host at `path`.
pub fn add_file(&mut self, path: impl AsRef<Path>) -> Result<()> {
let file =
File::open(&path).context(format!("Adding file: {}", path.as_ref().display()))?;
let merkle = MerkleTree::from_reader(&file)
.context(format!(
"Failed to calculate the merkle for file: {}",
&path.as_ref().display()
))?
.root();
self.add_file_with_merkle(path, merkle);
Ok(())
}
/// Add a file from the host at `path` with `merkle`.
fn add_file_with_merkle(&mut self, path: impl AsRef<Path>, merkle: Hash) {
self.packages.insert(merkle, path.as_ref().to_path_buf());
}
/// Generate the manifest of blobs to insert into BlobFS and write it to
/// `path`. The blob paths inside the manifest are relative to the manifest
/// itself, therefore we must pass the path to this function. The format of
/// the output is:
///
/// e9d5e=path/on/host/to/meta.far
/// 38203=path/on/host/to/file.json
///
pub fn write(&self, path: impl AsRef<Path>) -> Result<()> {
let manifest_parent = path
.as_ref()
.parent()
.ok_or(anyhow!("Failed to get parent path of the output blob manifest"))?;
let mut out = File::create(&path)
.context(format!("Failed to create file: {}", path.as_ref().display(),))?;
for (merkle, blob_path) in self.packages.iter() {
let blob_path = path_relative_to_dir(&blob_path, &manifest_parent).context(format!(
"Failed to get relative path for blob: {}",
blob_path.display()
))?;
let blob_path = blob_path
.to_str()
.context(format!("File path is not valid UTF-8: {}", blob_path.display()))?;
writeln!(out, "{}={}", merkle, blob_path)
.context(format!("Failed to write blob: {}\nmerkle: {}", blob_path, merkle,))?;
}
Ok(())
}
/// Returns a vector containing the hash and source path for blobs in the manifest.
pub fn to_vec(&self) -> Vec<(Hash, PathBuf)> {
self.packages.iter().map(|(h, p)| (h.clone(), p.clone())).collect::<Vec<_>>()
}
}
/// Rebase |path| onto |dir| even if |dir| is not in the current working directory.
fn path_relative_to_dir(path: impl AsRef<Path>, dir: impl AsRef<Path>) -> Result<PathBuf> {
// Get the canonical paths for the inputs, so that they can be rebased even if they are in
// different directories.
let path = path
.as_ref()
.canonicalize()
.context(format!("Failed to get canonical path for {}", path.as_ref().display()))?;
let dir = dir
.as_ref()
.canonicalize()
.context(format!("Failed to get canonical path for {}", dir.as_ref().display()))?;
// Rebase the paths.
diff_paths(&path, &dir)
.ok_or(anyhow!("Failed to get relative path for file: {}", path.display()))
}
#[cfg(test)]
mod tests {
use super::*;
use fuchsia_pkg::{BlobInfo, MetaPackage, PackageManifestBuilder};
use tempfile::{NamedTempFile, TempDir};
#[test]
fn blob_manifest_with_file() {
// Create a test file.
let mut file = NamedTempFile::new().unwrap();
write!(file, "Council of Ricks").unwrap();
// Put the file in the manifest.
let mut manifest = BlobManifest::default();
manifest.add_file(file.path()).unwrap();
// Write the manifest.
let output = NamedTempFile::new().unwrap();
manifest.write(output.path()).unwrap();
// Ensure the contents are correct.
let filename = file.path().file_name().unwrap().to_string_lossy();
let expected_str = format!(
"11062f138c675302fb551276f6a2afda16a43025549ac0c63164f6f1d9253da4={}\n",
filename
);
let output_str = std::fs::read_to_string(output).unwrap();
assert_eq!(output_str, expected_str);
}
#[test]
fn blob_manifest_with_package() {
// Create a test file.
let mut file = NamedTempFile::new().unwrap();
write!(file, "Council of Ricks").unwrap();
// Create a test package manifest.
let package = generate_test_manifest("package", Some(file.path()));
// Put the package in the manifest.
let mut manifest = BlobManifest::default();
manifest.add_package(package).unwrap();
// Write the manifest.
let output = NamedTempFile::new().unwrap();
manifest.write(output.path()).unwrap();
// Ensure the contents are correct.
let filename = file.path().file_name().unwrap().to_string_lossy();
let expected_str = format!(
"0000000000000000000000000000000000000000000000000000000000000000={}\n",
filename
);
let output_str = std::fs::read_to_string(output).unwrap();
assert_eq!(output_str, expected_str);
}
#[test]
fn blob_manifest_with_file_and_package() {
// Create a test file.
let mut file = NamedTempFile::new().unwrap();
write!(file, "Council of Ricks").unwrap();
// Create a test package manifest.
let package = generate_test_manifest("package", Some(file.path()));
// Add them both to the manifest.
let mut manifest = BlobManifest::default();
manifest.add_file(file.path()).unwrap();
manifest.add_package(package).unwrap();
// Write the manifest.
let output = NamedTempFile::new().unwrap();
manifest.write(output.path()).unwrap();
// Ensure the contents are correct.
let filename = file.path().file_name().unwrap().to_string_lossy();
let expected_str = format!(
"0000000000000000000000000000000000000000000000000000000000000000={}\n\
11062f138c675302fb551276f6a2afda16a43025549ac0c63164f6f1d9253da4={}\n",
filename, filename
);
let output_str = std::fs::read_to_string(output).unwrap();
assert_eq!(output_str, expected_str);
}
#[test]
fn test_relative_blob_path() {
// Save the current working directory so that we can reset it later.
let cwd = std::env::current_dir().unwrap();
// Set a test working directory.
let temp_dir = std::env::temp_dir();
let test_cwd = TempDir::new().unwrap();
let test_cwd_relative = diff_paths(&test_cwd, &temp_dir).unwrap();
std::env::set_current_dir(&test_cwd).unwrap();
// Create a test blob in the test working directory.
let blob_path = test_cwd.path().join("blob");
let mut blob = File::create(&blob_path).unwrap();
write!(blob, "Council of Ricks").unwrap();
// Create a second test directory to rebase the blob path onto, and calculate its path
// relative to the test working directory.
let test_out = TempDir::new().unwrap();
let test_out_relative = diff_paths(&test_out, &test_cwd_relative).unwrap();
// Calculate the expected path to the blob relative to the test output directory.
let blob_path_relative = test_cwd_relative.join("blob");
let expected = PathBuf::from("..").join(&blob_path_relative);
// Ensure the output is correct.
assert_eq!(expected, path_relative_to_dir("blob", test_out_relative).unwrap());
// Reset the working directory.
std::env::set_current_dir(cwd).unwrap();
}
// Generates a package manifest to be used for testing. The `name` is used in the blob file
// names to make each manifest somewhat unique. If supplied, `file_path` will be used as the
// non-meta-far blob source path, which allows the tests to use a real file.
// TODO(https://fxbug.dev/42156958): See if we can share this with BasePackage.
fn generate_test_manifest(name: &str, file_path: Option<impl AsRef<Path>>) -> PackageManifest {
let file_source = match file_path {
Some(path) => path.as_ref().to_string_lossy().into_owned(),
_ => format!("path/to/{}/file.txt", name),
};
let builder = PackageManifestBuilder::new(MetaPackage::from_name_and_variant_zero(
name.parse().unwrap(),
));
let builder = builder.add_blob(BlobInfo {
source_path: file_source,
path: "data/file.txt".into(),
merkle: "0000000000000000000000000000000000000000000000000000000000000000"
.parse()
.unwrap(),
size: 1,
});
builder.build()
}
}