blob: ed20aa054ce4c9444eeada7b46c0ea48727bc3d6 [file] [log] [blame]
// Copyright 2022 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},
fuchsia_hash::Hash,
fuchsia_pkg::{PackageManifest, PackagePath},
fuchsia_url::{PinnedAbsolutePackageUrl, RepositoryUrl},
serde_json::json,
std::{
collections::{BTreeMap, BTreeSet},
fs::File,
io::Write,
path::Path,
str::FromStr,
},
};
/// `WritablePackageList` represents a collection of packages that can be populated and
/// written into a file. This allows for gradual migration for packages index config
/// files (boot, base, cache) to JSON incrementally.
/// TODO(https://fxbug.dev/42176515): refactor out once base_packages are migrated to JSON format.
pub trait WritablePackageList {
/// Add a new package with `name` and `merkle`.
fn insert(
&mut self,
repository: Option<impl AsRef<str>>,
name: impl AsRef<str>,
merkle: Hash,
) -> Result<()>;
/// Generate the file to be used as the package index.
fn write(&self, out: &mut impl Write) -> Result<()>;
/// Returns whether the list has contents to write.
fn is_empty(&self) -> bool;
/// Pulls out the path and merkle from `package` and adds it to `packages` with a path to
/// merkle mapping.
fn add_package(&mut self, package: PackageManifest) -> Result<()> {
let package_name = package.name().as_ref();
if package_name == "system_image" || package_name == "update" {
return Err(anyhow!("system_image and update packages are not allowed"));
}
let package_repository = package.repository();
let path = package.package_path().to_string();
package
.blobs()
.into_iter()
.find(|blob| blob.path == "meta/")
.ok_or(anyhow!("Failed to add package {} to the list, unable to find meta blob", path))
.and_then(|meta_blob| self.insert(package_repository, path, meta_blob.merkle))
}
/// Helper fn to handle the (repeated) process of writing a list of packages
/// out to the expected file, and returning a (destination, source) tuple
/// for inclusion in the package's contents.
fn write_index_file(
&self,
gendir: impl AsRef<Path>,
name: &str,
destination: impl AsRef<str>,
) -> Result<(String, String)> {
// TODO(https://fxbug.dev/42156218) Decide on a consistent pattern for using gendir and
// how intermediate files should be named and where in gendir they should
// be placed.
//
// For a file of destination "data/foo.txt", and a gendir of "assembly/gendir",
// this creates "assembly/gendir/data/foo.txt".
let path = gendir.as_ref().join(destination.as_ref());
let path_str = path
.to_str()
.ok_or(anyhow!(format!("package index path is not valid UTF-8: {}", path.display())))?;
// Create any parent dirs necessary.
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent).context(format!(
"Failed to create parent dir {} for {} in gendir",
parent.display(),
destination.as_ref()
))?;
}
let mut index_file = File::create(&path)
.context(format!("Failed to create the {} packages index file: {}", name, path_str))?;
self.write(&mut index_file).context(format!(
"Failed to write the {} package index file: {}",
name,
path.display()
))?;
Ok((destination.as_ref().to_string(), path_str.to_string()))
}
}
/// A list of mappings between package name and merkle, which can be written to
/// a file to be used as a package index.
#[derive(Default, Debug)]
pub struct PackageList {
// Map between package name and merkle.
packages: BTreeMap<String, Hash>,
}
impl WritablePackageList for PackageList {
/// Add a new package with `name` and `merkle`.
fn insert(
&mut self,
_repository: Option<impl AsRef<str>>,
name: impl AsRef<str>,
merkle: Hash,
) -> Result<()> {
self.packages.insert(name.as_ref().to_string(), merkle);
Ok(())
}
/// Generate the file to be used as a package index.
fn write(&self, out: &mut impl Write) -> Result<()> {
for (name, merkle) in self.packages.iter() {
writeln!(out, "{}={}", name, merkle)?;
}
Ok(())
}
fn is_empty(&self) -> bool {
self.packages.is_empty()
}
}
/// A list of package URLs pinned to a hash, which can be written to a file.
#[derive(Default, Debug)]
pub struct PackageUrlList {
/// Using a BTreeSet to ensure output consistency, i.e. order of output package list is not
/// subject to insertion order.
packages: BTreeSet<PinnedAbsolutePackageUrl>,
}
impl PackageUrlList {
/// Returns a reference to the list absolute package urls
/// that this instance contains.
pub fn get_packages(&self) -> Vec<&PinnedAbsolutePackageUrl> {
return self.packages.iter().collect();
}
}
impl WritablePackageList for PackageUrlList {
/// Insert a new pinned to hash URL into the list.
fn insert(
&mut self,
repository: Option<impl AsRef<str>>,
name: impl AsRef<str>,
merkle: Hash,
) -> Result<()> {
let repository =
repository.ok_or(anyhow!("Unable to create package url: empty repository field."))?;
let path = PackagePath::from_str(name.as_ref())
.map_err(|e| anyhow!("Failed to parse package path: {}", e))?;
let url = PinnedAbsolutePackageUrl::new_with_path(
RepositoryUrl::parse_host(repository.as_ref().to_string())
.context("Failed to create repository url")?,
&format!("/{}", path),
merkle,
)
.map_err(|e| anyhow!("Failed to create package url: {}", e))?;
self.packages.insert(url);
Ok(())
}
/// Generate the file to be placed in the Base Package.
fn write(&self, writer: &mut impl Write) -> Result<()> {
// If there are no packages, we should generate an empty file.
if self.packages.is_empty() {
return Ok(());
}
let contents = json!({
"version": "1",
"content": &self.packages,
});
serde_json::to_writer(writer, &contents).map_err(|e| anyhow!("Error writing JSON: {}", e))
}
fn is_empty(&self) -> bool {
self.packages.is_empty()
}
}
impl PartialEq<PackageList> for Vec<(String, Hash)> {
fn eq(&self, other: &PackageList) -> bool {
if self.len() == other.packages.len() {
for item in self {
match other.packages.get(&item.0) {
Some(hash) => {
if hash != &item.1 {
return false;
}
}
None => {
return false;
}
}
}
return true;
}
return false;
}
}
#[cfg(test)]
mod tests {
use super::*;
use fuchsia_pkg::{BlobInfo, MetaPackage, PackageManifestBuilder};
#[test]
fn package_list() {
let mut out: Vec<u8> = Vec::new();
let mut packages = PackageList::default();
packages.insert(Some("testrepository.com"), "package2", Hash::from([34u8; 32])).unwrap();
packages.insert(Some("testrepository.com"), "package0", Hash::from([0u8; 32])).unwrap();
packages.insert(Some("testrepository.com"), "package1", Hash::from([17u8; 32])).unwrap();
packages.write(&mut out).unwrap();
assert_eq!(
b"package0=0000000000000000000000000000000000000000000000000000000000000000\n\
package1=1111111111111111111111111111111111111111111111111111111111111111\n\
package2=2222222222222222222222222222222222222222222222222222222222222222\n",
&*out
);
}
#[test]
fn package_url_list() {
let mut out: Vec<u8> = Vec::new();
let mut packages = PackageUrlList::default();
packages.insert(Some("testrepository.com"), "package2/0", Hash::from([34u8; 32])).unwrap();
packages.insert(Some("testrepository.com"), "package0/0", Hash::from([0u8; 32])).unwrap();
packages.insert(Some("testrepository.com"), "package1/0", Hash::from([17u8; 32])).unwrap();
packages.write(&mut out).unwrap();
assert_eq!(
br#"{"content":["fuchsia-pkg://testrepository.com/package0/0?hash=0000000000000000000000000000000000000000000000000000000000000000","fuchsia-pkg://testrepository.com/package1/0?hash=1111111111111111111111111111111111111111111111111111111111111111","fuchsia-pkg://testrepository.com/package2/0?hash=2222222222222222222222222222222222222222222222222222222222222222"],"version":"1"}"#,
&*out
);
}
#[test]
fn empty_package_url_list() {
let mut out: Vec<u8> = Vec::new();
let packages = PackageUrlList::default();
packages.write(&mut out).unwrap();
assert_eq!(b"", &*out);
}
#[test]
fn test_add_package_to() {
let system_image = generate_test_manifest("system_image", None);
let update = generate_test_manifest("update", None);
let valid = generate_test_manifest("valid", None);
let mut packages = PackageList::default();
assert!(WritablePackageList::add_package(&mut packages, system_image).is_err());
assert!(WritablePackageList::add_package(&mut packages, update).is_err());
assert!(WritablePackageList::add_package(&mut packages, valid).is_ok());
}
#[test]
fn test_add_package_to_url_list() {
let system_image = generate_test_manifest("system_image", None);
let update = generate_test_manifest("update", None);
let valid = generate_test_manifest("valid", None);
let mut packages = PackageUrlList::default();
assert!(WritablePackageList::add_package(&mut packages, system_image).is_err());
assert!(WritablePackageList::add_package(&mut packages, update).is_err());
assert!(WritablePackageList::add_package(&mut packages, valid).is_ok());
}
// 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.
fn generate_test_manifest(name: &str, file_path: Option<&Path>) -> PackageManifest {
let meta_source = format!("path/to/{}/meta.far", name);
let file_source = match file_path {
Some(path) => path.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.repository("testrepository.com");
let builder = builder.add_blob(BlobInfo {
source_path: meta_source,
path: "meta/".into(),
merkle: "0000000000000000000000000000000000000000000000000000000000000000"
.parse()
.unwrap(),
size: 1,
});
let builder = builder.add_blob(BlobInfo {
source_path: file_source,
path: "data/file.txt".into(),
merkle: "1111111111111111111111111111111111111111111111111111111111111111"
.parse()
.unwrap(),
size: 1,
});
builder.build()
}
}