| // 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 { |
| crate::{ |
| path_to_string::PathToStringExt, MetaContents, MetaPackage, MetaSubpackages, |
| PackageBuildManifest, PackageManifest, RelativeTo, SubpackageEntry, |
| }, |
| anyhow::{anyhow, bail, ensure, Context, Result}, |
| camino::Utf8PathBuf, |
| fuchsia_merkle::Hash, |
| fuchsia_url::RelativePackageUrl, |
| std::{ |
| collections::BTreeMap, |
| fs::File, |
| io::{BufReader, BufWriter, Cursor}, |
| path::{Path, PathBuf}, |
| }, |
| tempfile::NamedTempFile, |
| tempfile_ext::NamedTempFileExt as _, |
| version_history::AbiRevision, |
| }; |
| |
| /// Paths which will be generated by `PackageBuilder` itself and which should not be manually added. |
| const RESERVED_PATHS: &[&str] = &[MetaContents::PATH, MetaPackage::PATH, ABI_REVISION_FILE_PATH]; |
| pub const ABI_REVISION_FILE_PATH: &str = "meta/fuchsia.abi/abi-revision"; |
| |
| /// A builder for Fuchsia Packages |
| pub struct PackageBuilder { |
| /// The name of the package being created. |
| name: String, |
| |
| /// The abi_revision to embed in the package. |
| abi_revision: AbiRevision, |
| |
| /// The contents that are to be placed inside the FAR itself, not as |
| /// separate blobs. |
| far_contents: BTreeMap<String, String>, |
| |
| /// The contents that are to be attached to the package as external blobs. |
| blobs: BTreeMap<String, String>, |
| |
| /// Optional path to serialize the PackageManifest to |
| manifest_path: Option<Utf8PathBuf>, |
| |
| /// Optionally make the blob 'source_path's relative to the path the |
| /// PackageManifest is serialized to. |
| blob_sources_relative: RelativeTo, |
| |
| /// Optional (possibly different) name to publish the package under. |
| /// This changes the name that's placed in the output package manifest. |
| published_name: Option<String>, |
| |
| /// Optional package repository. |
| repository: Option<String>, |
| |
| /// Metafile of subpackages. |
| subpackages: BTreeMap<RelativePackageUrl, (Hash, PathBuf)>, |
| |
| /// Whether new files with the same path should overwrwite previous files at that path. |
| overwrite_files: bool, |
| } |
| |
| impl PackageBuilder { |
| /// Create a new PackageBuilder. |
| /// |
| /// The ABI revision is included in the package exactly as given - we don't |
| /// validate it here. |
| pub fn new(name: impl AsRef<str>, abi_revision: AbiRevision) -> Self { |
| PackageBuilder { |
| name: name.as_ref().to_string(), |
| abi_revision, |
| far_contents: BTreeMap::default(), |
| blobs: BTreeMap::default(), |
| manifest_path: None, |
| blob_sources_relative: RelativeTo::default(), |
| published_name: None, |
| repository: None, |
| subpackages: BTreeMap::default(), |
| overwrite_files: false, |
| } |
| } |
| |
| /// Create a new PackageBuilder with the platform-internal ABI revision. |
| /// |
| /// The platform-internal ABI revision is only appropriate for packages that |
| /// are only ever read by binaries from the exact same release. For packages |
| /// that can be built by tools from one release and then run on an OS from a |
| /// different release, a different ABI revision that defines the |
| /// compatibility guarantees must be selected. |
| pub fn new_platform_internal_package(name: impl AsRef<str>) -> Self { |
| PackageBuilder::new( |
| name, |
| version_history::HISTORY.get_abi_revision_for_platform_components(), |
| ) |
| } |
| |
| /// Create a PackageBuilder from a PackageBuildManifest. |
| /// |
| /// The ABI revision is included in the package exactly as given - we don't |
| /// validate it here. Returns an error if the given manifest already |
| /// specified an ABI revision. |
| pub fn from_package_build_manifest( |
| manifest: &PackageBuildManifest, |
| abi_revision: AbiRevision, |
| ) -> Result<Self> { |
| // Read the package name from `meta/package`, or error out if it's missing. |
| let meta_package = if let Some(path) = manifest.far_contents().get("meta/package") { |
| let f = File::open(path).with_context(|| format!("opening {path}"))?; |
| |
| MetaPackage::deserialize(BufReader::new(f))? |
| } else { |
| return Err(anyhow!("package missing meta/package entry")); |
| }; |
| |
| ensure!(meta_package.variant().is_zero(), "package variant must be zero"); |
| |
| // Ensure the manifest doesn't include `meta/fuchsia.abi/abi-revision` - |
| // if it did, it could conflict with the value of --api-level from the |
| // command line. |
| if manifest.far_contents().get("meta/fuchsia.abi/abi-revision").is_some() { |
| bail!( |
| "Manifest must not include entry for 'meta/fuchsia.abi/abi-revision'. \ |
| Pass --api-level to package-tool instead." |
| ) |
| }; |
| |
| let mut builder = PackageBuilder::new(meta_package.name(), abi_revision); |
| |
| for (at_path, file) in manifest.external_contents() { |
| builder |
| .add_file_as_blob(at_path, file) |
| .with_context(|| format!("adding file {at_path} as blob {file}"))?; |
| } |
| |
| for (at_path, file) in manifest.far_contents() { |
| // Ignore files that the package builder will automatically create. |
| if at_path == "meta/package" { |
| continue; |
| } |
| |
| builder |
| .add_file_to_far(at_path, file) |
| .with_context(|| format!("adding file {at_path} to far {file}"))?; |
| } |
| |
| Ok(builder) |
| } |
| |
| /// Create a PackageBuilder from an existing manifest. Requires an out directory for temporarily |
| /// unpacking `meta.far` contents. |
| pub fn from_manifest( |
| original_manifest: PackageManifest, |
| outdir: impl AsRef<Path>, |
| ) -> Result<Self> { |
| // parse the existing manifest, copying everything over |
| let mut abi_rev = None; |
| let mut inner_name = None; |
| let mut meta_blobs = BTreeMap::new(); |
| let mut blob_paths = BTreeMap::new(); |
| let mut subpackage_names = BTreeMap::new(); |
| for blob in original_manifest.blobs() { |
| if blob.path == PackageManifest::META_FAR_BLOB_PATH { |
| let meta_far_contents = std::fs::read(&blob.source_path) |
| .with_context(|| format!("reading {}", blob.source_path))?; |
| let PackagedMetaFar { abi_revision, name, meta_contents, .. } = |
| PackagedMetaFar::parse(&meta_far_contents).context("parsing meta/")?; |
| abi_rev = Some(abi_revision); |
| inner_name = Some(name); |
| meta_blobs = meta_contents; |
| } else { |
| blob_paths.insert(blob.path.clone(), blob.source_path.clone()); |
| } |
| } |
| for subpackage in original_manifest.subpackages() { |
| subpackage_names.insert( |
| subpackage.name.clone(), |
| (subpackage.merkle, subpackage.manifest_path.clone()), |
| ); |
| } |
| let abi_rev = abi_rev.ok_or_else(|| anyhow!("did not find {}", ABI_REVISION_FILE_PATH))?; |
| let inner_name = inner_name.ok_or_else(|| anyhow!("did not find {}", MetaPackage::PATH))?; |
| |
| let mut builder = PackageBuilder::new(inner_name, abi_rev); |
| builder.published_name(original_manifest.name()); |
| if let Some(repository) = original_manifest.repository() { |
| builder.repository(repository); |
| } |
| |
| for (path, contents) in meta_blobs { |
| builder |
| .add_contents_to_far(&path, contents, &outdir) |
| .with_context(|| format!("adding {path} to far"))?; |
| } |
| |
| for (path, source_path) in blob_paths { |
| builder |
| .add_file_as_blob(&path, &source_path) |
| .with_context(|| format!("adding {path}"))?; |
| } |
| |
| for (name, (merkle, manifest_path)) in subpackage_names { |
| builder |
| .add_subpackage( |
| &name.parse().context("parsing subpackage name")?, |
| merkle, |
| manifest_path.into(), |
| ) |
| .with_context(|| format!("adding {name}"))?; |
| } |
| |
| Ok(builder) |
| } |
| |
| /// Specify a path to write out the json package manifest to. |
| pub fn manifest_path(&mut self, manifest_path: impl Into<Utf8PathBuf>) { |
| self.manifest_path = Some(manifest_path.into()) |
| } |
| |
| pub fn manifest_blobs_relative_to(&mut self, relative_to: RelativeTo) { |
| self.blob_sources_relative = relative_to |
| } |
| |
| /// Specify whether new additions of a file that have already been added should overwrite, |
| /// or fail |
| pub fn overwrite_files(&mut self, overwrite_files: bool) { |
| self.overwrite_files = overwrite_files |
| } |
| |
| fn validate_ok_to_modify(&self, at_path: &str) -> Result<()> { |
| let at_path = at_path.as_ref(); |
| if RESERVED_PATHS.contains(&at_path) { |
| bail!("Cannot add '{}', it will be created by the PackageBuilder", at_path); |
| } |
| |
| Ok(()) |
| } |
| |
| fn validate_ok_to_add_in_far(&self, at_path: impl AsRef<str>) -> Result<()> { |
| let at_path = at_path.as_ref(); |
| self.validate_ok_to_modify(at_path)?; |
| |
| // Never allow overwriting a blob path if we think we're writing into a far |
| if self.blobs.contains_key(at_path) { |
| return Err(anyhow!( |
| "Package '{}' already contains a file (as a blob) at: '{}'", |
| self.name, |
| at_path |
| )); |
| } |
| |
| if self.far_contents.contains_key(at_path) && !self.overwrite_files { |
| return Err(anyhow!( |
| "Package '{}' already contains a file (in the far) at: '{}'", |
| self.name, |
| at_path |
| )); |
| } |
| |
| Ok(()) |
| } |
| |
| fn validate_ok_to_add_as_blob(&self, at_path: impl AsRef<str>) -> Result<()> { |
| let at_path = at_path.as_ref(); |
| self.validate_ok_to_modify(at_path)?; |
| |
| // Never allow overwriting a path in the far if we think we're writing a blob |
| if self.far_contents.contains_key(at_path) { |
| return Err(anyhow!( |
| "Package '{}' already contains a file (in the far) at: '{}'", |
| self.name, |
| at_path |
| )); |
| } |
| if self.blobs.contains_key(at_path) && !self.overwrite_files { |
| return Err(anyhow!( |
| "Package '{}' already contains a file (as a blob) at: '{}'", |
| self.name, |
| at_path |
| )); |
| } |
| |
| Ok(()) |
| } |
| |
| /// Add a file to the package's far. |
| /// |
| /// Errors |
| /// |
| /// Will return an error if the path for the file is already being used. |
| /// Will return an error if any special package metadata paths are used. |
| pub fn add_file_to_far( |
| &mut self, |
| at_path: impl AsRef<str>, |
| file: impl AsRef<str>, |
| ) -> Result<()> { |
| let at_path = at_path.as_ref(); |
| let file = file.as_ref(); |
| self.validate_ok_to_add_in_far(at_path)?; |
| |
| self.far_contents.insert(at_path.to_string(), file.to_string()); |
| |
| Ok(()) |
| } |
| |
| /// Remove a file from the package's meta.far. |
| /// |
| /// Errors |
| /// |
| /// Will return an error if the file is not already in the meta.far |
| pub fn remove_file_from_far(&mut self, at_path: impl AsRef<str>) -> Result<()> { |
| self.validate_ok_to_modify(at_path.as_ref())?; |
| match self.far_contents.remove(at_path.as_ref()) { |
| Some(_key) => Ok(()), |
| None => Err(anyhow!("file not in meta.far")), |
| } |
| } |
| |
| /// Add a file to the package as a blob itself. |
| /// |
| /// Errors |
| /// |
| /// Will return an error if the path for the file is already being used. |
| /// Will return an error if any special package metadata paths are used. |
| pub fn add_file_as_blob( |
| &mut self, |
| at_path: impl AsRef<str>, |
| file: impl AsRef<str>, |
| ) -> Result<()> { |
| let at_path = at_path.as_ref(); |
| let file = file.as_ref(); |
| self.validate_ok_to_add_as_blob(at_path)?; |
| |
| self.blobs.insert(at_path.to_string(), file.to_string()); |
| |
| Ok(()) |
| } |
| |
| /// Remove a file currently in the package as a blob |
| /// |
| /// Errors |
| /// |
| /// Will return an error if the file is not already in the package contents |
| pub fn remove_blob_file(&mut self, at_path: impl AsRef<str>) -> Result<()> { |
| self.validate_ok_to_modify(at_path.as_ref())?; |
| match self.blobs.remove(at_path.as_ref()) { |
| Some(_key) => Ok(()), |
| None => Err(anyhow!("file not in package contents")), |
| } |
| } |
| |
| /// Write the contents to a file, and add that file as a blob at the given |
| /// path within the package. |
| pub fn add_contents_as_blob<C: AsRef<[u8]>>( |
| &mut self, |
| at_path: impl AsRef<str>, |
| contents: C, |
| gendir: impl AsRef<Path>, |
| ) -> Result<()> { |
| // Preflight that the file paths are valid before attempting to write. |
| self.validate_ok_to_add_as_blob(&at_path)?; |
| let source_path = Self::write_contents_to_file(gendir, at_path.as_ref(), contents)?; |
| self.add_file_as_blob(at_path, source_path.path_to_string()?) |
| } |
| |
| /// Write the contents to a file, and add that file to the metafar at the |
| /// given path within the package. |
| pub fn add_contents_to_far<C: AsRef<[u8]>>( |
| &mut self, |
| at_path: impl AsRef<str>, |
| contents: C, |
| gendir: impl AsRef<Path>, |
| ) -> Result<()> { |
| // Preflight that the file paths are valid before attempting to write. |
| self.validate_ok_to_add_in_far(&at_path)?; |
| let source_path = Self::write_contents_to_file(gendir, at_path.as_ref(), contents)?; |
| self.add_file_to_far(at_path, source_path.path_to_string()?) |
| } |
| |
| /// Helper fn to write the contents to a file, creating the parent dirs as needed when doing so. |
| fn write_contents_to_file<C: AsRef<[u8]>>( |
| gendir: impl AsRef<Path>, |
| file_path: impl AsRef<Path>, |
| contents: C, |
| ) -> Result<PathBuf> { |
| let file_path = gendir.as_ref().join(file_path); |
| if let Some(parent_dir) = file_path.parent() { |
| std::fs::create_dir_all(parent_dir) |
| .context(format!("creating parent directories for {}", file_path.display()))?; |
| } |
| std::fs::write(&file_path, contents) |
| .context(format!("writing contents to file: {}", file_path.display()))?; |
| Ok(file_path) |
| } |
| |
| /// Helper fn to include a subpackage into this package. |
| pub fn add_subpackage( |
| &mut self, |
| url: &RelativePackageUrl, |
| package_hash: Hash, |
| package_manifest_path: PathBuf, |
| ) -> Result<()> { |
| if self.subpackages.contains_key(url) { |
| return Err(anyhow!("duplicate entry for {:?}", url)); |
| } |
| self.subpackages.insert(url.clone(), (package_hash, package_manifest_path)); |
| Ok(()) |
| } |
| |
| /// Set the name of the package. |
| pub fn name(&mut self, name: impl AsRef<str>) { |
| self.name = name.as_ref().to_string(); |
| } |
| |
| /// Set a different name for the package to be published by (and to be |
| /// included in the generated PackageManifest), than the one embedded in the |
| /// package itself. |
| pub fn published_name(&mut self, published_name: impl AsRef<str>) { |
| self.published_name = Some(published_name.as_ref().into()); |
| } |
| |
| /// Set a repository for the package to be included in the generated PackageManifest. |
| pub fn repository(&mut self, repository: impl AsRef<str>) { |
| self.repository = Some(repository.as_ref().into()); |
| } |
| |
| /// Read the contents of a file already added to the builder's meta.far. |
| pub fn read_contents_from_far(&self, file_path: &str) -> Result<Vec<u8>> { |
| if let Some(p) = self.far_contents.get(file_path) { |
| std::fs::read(p).with_context(|| format!("reading {p}")) |
| } else { |
| bail!("couldn't find `{}` in package", file_path); |
| } |
| } |
| |
| /// Build the package, using the specified dir, returning the |
| /// PackageManifest. |
| /// |
| /// If a path for the manifest was specified, the PackageManifest will also |
| /// be written to there. |
| /// |
| /// The `gendir` param is assumed to be a path to folder which is only used |
| /// by this package's creation, so this fn does not try to create paths |
| /// within it that are unique across different packages. |
| pub fn build( |
| self, |
| gendir: impl AsRef<Path>, |
| metafar_path: impl AsRef<Path>, |
| ) -> Result<PackageManifest> { |
| let gendir = gendir.as_ref(); |
| let metafar_path = metafar_path.as_ref(); |
| |
| let PackageBuilder { |
| name, |
| abi_revision, |
| mut far_contents, |
| blobs, |
| manifest_path, |
| blob_sources_relative, |
| published_name, |
| repository, |
| subpackages, |
| overwrite_files: _, |
| } = self; |
| |
| far_contents.insert( |
| MetaPackage::PATH.to_string(), |
| create_meta_package_file(gendir, &name) |
| .with_context(|| format!("Writing the {} file", MetaPackage::PATH))?, |
| ); |
| |
| let abi_revision_file = |
| Self::write_contents_to_file(gendir, ABI_REVISION_FILE_PATH, abi_revision.as_bytes()) |
| .with_context(|| format!("Writing the {ABI_REVISION_FILE_PATH} file"))?; |
| |
| far_contents.insert( |
| ABI_REVISION_FILE_PATH.to_string(), |
| abi_revision_file.path_to_string().with_context(|| { |
| format!("Adding the {ABI_REVISION_FILE_PATH} file to the package") |
| })?, |
| ); |
| |
| // Only add the subpackages file if we were configured with any subpackages. |
| if !subpackages.is_empty() { |
| far_contents.insert( |
| MetaSubpackages::PATH.to_string(), |
| create_meta_subpackages_file(gendir, subpackages.clone()).with_context(|| { |
| format!("Adding the {} file to the package", MetaSubpackages::PATH) |
| })?, |
| ); |
| } |
| |
| let package_build_manifest = |
| PackageBuildManifest::from_external_and_far_contents(blobs, far_contents) |
| .with_context(|| "creating creation manifest".to_string())?; |
| |
| let package_manifest = crate::build::build( |
| &package_build_manifest, |
| metafar_path, |
| published_name.unwrap_or(name), |
| subpackages |
| .into_iter() |
| .map(|(name, (merkle, package_manifest_path))| SubpackageEntry { |
| name, |
| merkle, |
| package_manifest_path, |
| }) |
| .collect(), |
| repository, |
| ) |
| .with_context(|| format!("building package manifest {}", metafar_path.display()))?; |
| |
| Ok(if let Some(manifest_path) = manifest_path { |
| if let RelativeTo::File = blob_sources_relative { |
| let copy = package_manifest.clone(); |
| copy.write_with_relative_paths(&manifest_path).with_context(|| { |
| format!( |
| "Failed to create package manifest with relative paths at: {manifest_path}" |
| ) |
| })?; |
| |
| package_manifest |
| } else { |
| // Write the package manifest to a file. |
| let mut tmp = if let Some(parent) = manifest_path.parent() { |
| NamedTempFile::new_in(parent)? |
| } else { |
| NamedTempFile::new()? |
| }; |
| |
| serde_json::ser::to_writer(BufWriter::new(&mut tmp), &package_manifest) |
| .with_context(|| { |
| format!("writing package manifest to {}", tmp.path().display()) |
| })?; |
| |
| tmp.persist_if_changed(&manifest_path).with_context(|| { |
| format!("Failed to persist package manifest: {manifest_path}") |
| })?; |
| |
| package_manifest |
| } |
| } else { |
| package_manifest |
| }) |
| } |
| } |
| |
| /// Construct a meta/package file in `gendir`. |
| /// |
| /// Returns the path that the file was created at. |
| fn create_meta_package_file(gendir: &Path, name: impl Into<String>) -> Result<String> { |
| let package_name = name.into(); |
| let meta_package_path = gendir.join(MetaPackage::PATH); |
| if let Some(parent_dir) = meta_package_path.parent() { |
| std::fs::create_dir_all(parent_dir)?; |
| } |
| |
| let file = std::fs::File::create(&meta_package_path)?; |
| let meta_package = MetaPackage::from_name_and_variant_zero(package_name.try_into()?); |
| meta_package.serialize(file)?; |
| meta_package_path.path_to_string() |
| } |
| |
| /// Results of parsing an existing meta.far for repackaging or testing purposes. |
| struct PackagedMetaFar { |
| /// Package's name. |
| name: String, |
| |
| /// Package's ABI revision. |
| abi_revision: AbiRevision, |
| |
| /// Map of package paths to blob contents. |
| meta_contents: BTreeMap<String, Vec<u8>>, |
| } |
| |
| impl PackagedMetaFar { |
| fn parse(bytes: &[u8]) -> Result<Self> { |
| let mut meta_far = |
| fuchsia_archive::Utf8Reader::new(Cursor::new(bytes)).context("reading FAR")?; |
| |
| let mut abi_revision = None; |
| let mut name = None; |
| let mut meta_contents = BTreeMap::new(); |
| |
| // collect paths separately, we need mutable access to reader for the bytes of each |
| let meta_paths = meta_far.list().map(|e| e.path().to_owned()).collect::<Vec<_>>(); |
| |
| // copy the contents of the meta.far, skipping files that PackageBuilder will write |
| for path in meta_paths { |
| let contents = meta_far.read_file(&path).with_context(|| format!("reading {path}"))?; |
| |
| if path == MetaContents::PATH { |
| continue; |
| } else if path == MetaPackage::PATH { |
| ensure!(name.is_none(), "only one name per package"); |
| let mp = MetaPackage::deserialize(Cursor::new(&contents)) |
| .context("deserializing meta/package")?; |
| name = Some(mp.name().to_string()); |
| } else if path == ABI_REVISION_FILE_PATH { |
| ensure!(abi_revision.is_none(), "only one abi revision per package"); |
| ensure!(contents.len() == 8, "ABI revision must be encoded as 8 bytes"); |
| abi_revision = Some(AbiRevision::try_from(contents.as_slice()).unwrap()); |
| } else { |
| meta_contents.insert(path, contents); |
| } |
| } |
| let abi_revision = |
| abi_revision.ok_or_else(|| anyhow!("did not find {}", ABI_REVISION_FILE_PATH))?; |
| let name = name.ok_or_else(|| anyhow!("did not find {}", MetaPackage::PATH))?; |
| |
| Ok(Self { name, abi_revision, meta_contents }) |
| } |
| } |
| |
| /// Construct a meta/fuchsia.pkg/subpackages file in `gendir`. |
| /// |
| /// Returns the path that the file was created at. |
| fn create_meta_subpackages_file( |
| gendir: &Path, |
| subpackages: BTreeMap<RelativePackageUrl, (Hash, PathBuf)>, |
| ) -> Result<String> { |
| let meta_subpackages_path = gendir.join(MetaSubpackages::PATH); |
| if let Some(parent_dir) = meta_subpackages_path.parent() { |
| std::fs::create_dir_all(parent_dir)?; |
| } |
| |
| let meta_subpackages = MetaSubpackages::from_iter( |
| subpackages.into_iter().map(|(name, (merkle, _))| (name, merkle)), |
| ); |
| let file = std::fs::File::create(&meta_subpackages_path)?; |
| meta_subpackages.serialize(file)?; |
| meta_subpackages_path.path_to_string() |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use {super::*, camino::Utf8Path, fuchsia_merkle::MerkleTreeBuilder, tempfile::TempDir}; |
| |
| const FAKE_ABI_REVISION: AbiRevision = AbiRevision::from_u64(0x5836508c2defac54); |
| |
| #[test] |
| fn test_create_meta_package_file() { |
| let gen_dir = TempDir::new().unwrap(); |
| let name = "some_test_package"; |
| let meta_package_path = gen_dir.as_ref().join("meta/package"); |
| let created_path = create_meta_package_file(gen_dir.path(), name).unwrap(); |
| assert_eq!(created_path, meta_package_path.path_to_string().unwrap()); |
| |
| let raw_contents = std::fs::read(meta_package_path).unwrap(); |
| let meta_package = MetaPackage::deserialize(std::io::Cursor::new(raw_contents)).unwrap(); |
| assert_eq!(meta_package.name().as_ref(), "some_test_package"); |
| assert!(meta_package.variant().is_zero()); |
| } |
| |
| #[test] |
| fn test_builder() { |
| let outdir = TempDir::new().unwrap(); |
| let metafar_path = outdir.path().join("meta.far"); |
| |
| // Create a file to write to the package metafar |
| let far_source_file_path = NamedTempFile::new_in(&outdir).unwrap(); |
| std::fs::write(&far_source_file_path, "some data for far").unwrap(); |
| |
| // Create a file to include as a blob |
| let blob_source_file_path = NamedTempFile::new_in(&outdir).unwrap(); |
| let blob_contents = "some data for blob"; |
| std::fs::write(&blob_source_file_path, blob_contents).unwrap(); |
| |
| // Pre-calculate the blob's hash |
| let mut merkle_builder = MerkleTreeBuilder::new(); |
| merkle_builder.write(blob_contents.as_bytes()); |
| let blob_hash = merkle_builder.finish().root(); |
| |
| let subpackage_url = "subpackage0".parse::<RelativePackageUrl>().unwrap(); |
| let subpackage_hash = Hash::from([0; fuchsia_hash::HASH_SIZE]); |
| let subpackage_package_manifest_path = "subpackages/package_manifest.json"; |
| |
| // Create the builder |
| let mut builder = PackageBuilder::new("some_pkg_name", FAKE_ABI_REVISION); |
| builder |
| .add_file_as_blob("some/blob", blob_source_file_path.path().path_to_string().unwrap()) |
| .unwrap(); |
| builder |
| .add_file_to_far( |
| "meta/some/file", |
| far_source_file_path.path().path_to_string().unwrap(), |
| ) |
| .unwrap(); |
| builder |
| .add_subpackage( |
| &subpackage_url, |
| subpackage_hash, |
| subpackage_package_manifest_path.into(), |
| ) |
| .unwrap(); |
| |
| // Build the package |
| let manifest = builder.build(&outdir, &metafar_path).unwrap(); |
| |
| // Validate the returned manifest |
| assert_eq!(manifest.name().as_ref(), "some_pkg_name"); |
| |
| let (blobs, subpackages) = manifest.into_blobs_and_subpackages(); |
| |
| // Validate that the blob has the correct hash and contents |
| let blob_info = blobs.iter().find(|info| info.path == "some/blob").unwrap().clone(); |
| assert_eq!(blob_hash, blob_info.merkle); |
| assert_eq!(blob_contents, std::fs::read_to_string(blob_info.source_path).unwrap()); |
| |
| // Validate that the subpackage has the correct hash and manifest path |
| let subpackage_info = |
| subpackages.iter().find(|info| info.name == "subpackage0").unwrap().clone(); |
| assert_eq!(subpackage_hash, subpackage_info.merkle); |
| assert_eq!(subpackage_package_manifest_path, subpackage_info.manifest_path); |
| |
| // Validate that the metafar contains the additional file in meta |
| let mut metafar = std::fs::File::open(metafar_path).unwrap(); |
| let mut far_reader = fuchsia_archive::Utf8Reader::new(&mut metafar).unwrap(); |
| let far_file_data = far_reader.read_file("meta/some/file").unwrap(); |
| let far_file_data = std::str::from_utf8(far_file_data.as_slice()).unwrap(); |
| assert_eq!(far_file_data, "some data for far"); |
| |
| // Validate that the abi_revision was written correctly |
| let abi_revision_data = far_reader.read_file("meta/fuchsia.abi/abi-revision").unwrap(); |
| let abi_revision_data: [u8; 8] = abi_revision_data.try_into().unwrap(); |
| let abi_revision = AbiRevision::from_bytes(abi_revision_data); |
| assert_eq!(abi_revision, FAKE_ABI_REVISION); |
| } |
| |
| #[test] |
| fn test_from_manifest() { |
| let first_outdir = TempDir::new().unwrap(); |
| |
| // Create an initial package with non-default outputs for generated files |
| let inner_name = "some_pkg_name"; |
| let mut first_builder = PackageBuilder::new(inner_name, FAKE_ABI_REVISION); |
| // Set a different published name |
| let published_name = "some_other_pkg_name"; |
| first_builder.published_name(published_name); |
| |
| // Create a file to write to the package metafar |
| let first_far_source_file_path = NamedTempFile::new_in(&first_outdir).unwrap(); |
| let first_far_contents = "some data for far"; |
| std::fs::write(&first_far_source_file_path, first_far_contents).unwrap(); |
| first_builder |
| .add_file_to_far("meta/some/file", first_far_source_file_path.path().to_string_lossy()) |
| .unwrap(); |
| |
| // Create a file to include as a blob |
| let first_blob_source_file_path = NamedTempFile::new_in(&first_outdir).unwrap(); |
| let first_blob_contents = "some data for blob"; |
| std::fs::write(&first_blob_source_file_path, first_blob_contents).unwrap(); |
| first_builder |
| .add_file_as_blob("some/blob", first_blob_source_file_path.path().to_string_lossy()) |
| .unwrap(); |
| |
| let first_subpackage_url = "subpackage0".parse::<RelativePackageUrl>().unwrap(); |
| let first_subpackage_hash = Hash::from([0; fuchsia_hash::HASH_SIZE]); |
| let first_subpackage_package_manifest_path = "subpackages/package_manifest.json"; |
| |
| first_builder |
| .add_subpackage( |
| &first_subpackage_url, |
| first_subpackage_hash, |
| first_subpackage_package_manifest_path.into(), |
| ) |
| .unwrap(); |
| |
| // Build the package |
| let first_manifest = |
| first_builder.build(&first_outdir, first_outdir.path().join("meta.far")).unwrap(); |
| assert_eq!( |
| first_manifest.blobs().len(), |
| 2, |
| "package should have a meta.far and a single blob" |
| ); |
| assert_eq!( |
| first_manifest.subpackages().len(), |
| 1, |
| "package should have a single subpackage" |
| ); |
| let blob_info = first_manifest |
| .blobs() |
| .iter() |
| .find(|blob_info| blob_info.path == PackageManifest::META_FAR_BLOB_PATH) |
| .unwrap(); |
| let mut metafar = std::fs::File::open(&blob_info.source_path).unwrap(); |
| let far_reader = fuchsia_archive::Utf8Reader::new(&mut metafar).unwrap(); |
| let first_paths_in_far = |
| far_reader.list().map(|e| e.path().to_string()).collect::<Vec<_>>(); |
| |
| // Re-parse the package into a builder for further modification |
| let second_outdir = TempDir::new().unwrap(); |
| let mut second_builder = |
| PackageBuilder::from_manifest(first_manifest.clone(), second_outdir.path()).unwrap(); |
| |
| // Create another file to write to the package metafar |
| let second_far_source_file_path = NamedTempFile::new_in(&second_outdir).unwrap(); |
| let second_far_contents = "some more data for far"; |
| std::fs::write(&second_far_source_file_path, second_far_contents).unwrap(); |
| second_builder |
| .add_file_to_far( |
| "meta/some/other/file", |
| second_far_source_file_path.path().to_string_lossy(), |
| ) |
| .unwrap(); |
| |
| // Create a file to include as a blob |
| let second_blob_source_file_path = NamedTempFile::new_in(&second_outdir).unwrap(); |
| let second_blob_contents = "some more data for blobs"; |
| std::fs::write(&second_blob_source_file_path, second_blob_contents).unwrap(); |
| second_builder |
| .add_file_as_blob( |
| "some/other/blob", |
| second_blob_source_file_path.path().to_string_lossy(), |
| ) |
| .unwrap(); |
| |
| // Write the package again after we've modified its contents |
| let second_metafar_path = second_outdir.path().join("meta.far"); |
| let second_manifest = second_builder.build(&second_outdir, second_metafar_path).unwrap(); |
| assert_eq!(first_manifest.name(), second_manifest.name(), "package names must match"); |
| assert_eq!( |
| second_manifest.blobs().len(), |
| 3, |
| "package should have a meta.far and two blobs" |
| ); |
| assert_eq!( |
| second_manifest.subpackages().len(), |
| 1, |
| "package should STILL have a single subpackage" |
| ); |
| |
| // Validate the contents of the package after re-writing |
| for blob_info in second_manifest.blobs() { |
| match &*blob_info.path { |
| PackageManifest::META_FAR_BLOB_PATH => { |
| // Validate that the metafar contains the additional file in meta |
| let mut metafar = std::fs::File::open(&blob_info.source_path).unwrap(); |
| let mut far_reader = fuchsia_archive::Utf8Reader::new(&mut metafar).unwrap(); |
| let paths_in_far = |
| far_reader.list().map(|e| e.path().to_string()).collect::<Vec<_>>(); |
| assert_eq!( |
| paths_in_far.len(), |
| first_paths_in_far.len() + 1, |
| "must have the original files and one added one" |
| ); |
| |
| for far_path in paths_in_far { |
| let far_bytes = far_reader.read_file(&far_path).unwrap(); |
| match &*far_path { |
| MetaContents::PATH => (), // separate tests check this matches blobs |
| MetaPackage::PATH => { |
| let mp = MetaPackage::deserialize(Cursor::new(&far_bytes)).unwrap(); |
| assert_eq!(mp.name().as_ref(), inner_name); |
| } |
| MetaSubpackages::PATH => { |
| let ms = |
| MetaSubpackages::deserialize(Cursor::new(&far_bytes)).unwrap(); |
| assert_eq!(ms.subpackages().len(), 1); |
| let (url, hash) = ms.subpackages().iter().next().unwrap(); |
| assert_eq!(url, &first_subpackage_url); |
| assert_eq!(hash, &first_subpackage_hash); |
| } |
| ABI_REVISION_FILE_PATH => { |
| assert_eq!(far_bytes, FAKE_ABI_REVISION.as_bytes()); |
| } |
| "meta/some/file" => { |
| assert_eq!(far_bytes, first_far_contents.as_bytes()); |
| } |
| "meta/some/other/file" => { |
| assert_eq!(far_bytes, second_far_contents.as_bytes()); |
| } |
| other => panic!("unrecognized file in meta.far: {other}"), |
| } |
| } |
| } |
| "some/blob" => { |
| assert_eq!( |
| std::fs::read_to_string(&blob_info.source_path).unwrap(), |
| first_blob_contents, |
| ); |
| } |
| "some/other/blob" => { |
| assert_eq!( |
| std::fs::read_to_string(&blob_info.source_path).unwrap(), |
| second_blob_contents, |
| ) |
| } |
| other => panic!("unrecognized path in blobs `{other}`"), |
| } |
| } |
| } |
| |
| #[test] |
| fn test_removes() { |
| let gendir = TempDir::new().unwrap(); |
| let mut builder = PackageBuilder::new("some_pkg_name", FAKE_ABI_REVISION); |
| assert!(builder.add_contents_to_far("meta/foo", "foo", gendir.path()).is_ok()); |
| assert!(builder.add_contents_to_far("meta/bar", "bar", gendir.path()).is_ok()); |
| |
| assert!(builder.add_contents_as_blob("baz", "baz", gendir.path()).is_ok()); |
| assert!(builder.add_contents_as_blob("boom", "boom", gendir.path()).is_ok()); |
| |
| assert!(builder.remove_file_from_far("meta/foo").is_ok()); |
| assert!(builder.remove_file_from_far("meta/does_not_exist").is_err()); |
| |
| assert!(builder.remove_blob_file("baz").is_ok()); |
| assert!(builder.remove_blob_file("does_not_exist").is_err()); |
| |
| let outdir = TempDir::new().unwrap(); |
| let metafar_path = outdir.path().join("meta.far"); |
| |
| let pkg_manifest = builder.build(&outdir, &metafar_path).unwrap(); |
| |
| // We should be able to build the package, and it should not have our |
| // removed files in either the meta.far or the contents. |
| for blob_info in pkg_manifest.blobs() { |
| match &*blob_info.path { |
| PackageManifest::META_FAR_BLOB_PATH => { |
| let mut metafar = std::fs::File::open(&blob_info.source_path).unwrap(); |
| let mut far_reader = fuchsia_archive::Utf8Reader::new(&mut metafar).unwrap(); |
| let paths_in_far = |
| far_reader.list().map(|e| e.path().to_string()).collect::<Vec<_>>(); |
| |
| for far_path in paths_in_far { |
| let far_bytes = far_reader.read_file(&far_path).unwrap(); |
| match &*far_path { |
| MetaContents::PATH => (), // we have separate tests for the meta.far metadata |
| MetaPackage::PATH => (), |
| MetaSubpackages::PATH => (), |
| ABI_REVISION_FILE_PATH => (), |
| "meta/bar" => { |
| assert_eq!(far_bytes, "bar".as_bytes()); |
| } |
| other => panic!("unrecognized file in meta.far: {other}"), |
| } |
| } |
| } |
| "boom" => { |
| assert_eq!(std::fs::read_to_string(&blob_info.source_path).unwrap(), "boom",); |
| } |
| other => panic!("unrecognized path in blobs `{other}`"), |
| } |
| } |
| } |
| |
| #[test] |
| fn test_build_rejects_meta_contents() { |
| let mut builder = PackageBuilder::new("some_pkg_name", FAKE_ABI_REVISION); |
| assert!(builder.add_file_to_far("meta/contents", "some/src/file").is_err()); |
| assert!(builder.add_file_as_blob("meta/contents", "some/src/file").is_err()); |
| } |
| |
| #[test] |
| fn test_build_rejects_meta_package() { |
| let mut builder = PackageBuilder::new("some_pkg_name", FAKE_ABI_REVISION); |
| assert!(builder.add_file_to_far("meta/package", "some/src/file").is_err()); |
| assert!(builder.add_file_as_blob("meta/package", "some/src/file").is_err()); |
| } |
| |
| #[test] |
| fn test_build_rejects_abi_revision() { |
| let mut builder = PackageBuilder::new("some_pkg_name", FAKE_ABI_REVISION); |
| assert!(builder.add_file_to_far("meta/fuchsia.abi/abi-revision", "some/src/file").is_err()); |
| assert!(builder |
| .add_file_as_blob("meta/fuchsia.abi/abi-revision", "some/src/file") |
| .is_err()); |
| } |
| |
| #[test] |
| fn test_builder_rejects_path_in_far_when_existing_path_in_far() { |
| let mut builder = PackageBuilder::new("some_pkg_name", FAKE_ABI_REVISION); |
| builder.add_file_to_far("some/far/file", "some/src/file").unwrap(); |
| assert!(builder.add_file_to_far("some/far/file", "some/src/file").is_err()); |
| } |
| |
| #[test] |
| fn test_builder_allows_overwrite_path_in_far_when_flag_set() { |
| let mut builder = PackageBuilder::new("some_pkg_name", FAKE_ABI_REVISION); |
| builder.overwrite_files(true); |
| builder.add_file_to_far("some/far/file", "some/src/file").unwrap(); |
| assert!(builder.add_file_to_far("some/far/file", "some/src/file").is_ok()); |
| } |
| |
| #[test] |
| fn test_builder_rejects_path_as_blob_when_existing_path_in_far() { |
| let mut builder = PackageBuilder::new("some_pkg_name", FAKE_ABI_REVISION); |
| builder.add_file_to_far("some/far/file", "some/src/file").unwrap(); |
| assert!(builder.add_file_as_blob("some/far/file", "some/src/file").is_err()); |
| } |
| |
| #[test] |
| fn test_builder_rejects_path_as_blob_when_existing_path_in_far_and_overwrite_set() { |
| // even if we set the overwrite flag, we shouldn't allow a blob to overwrite a file in the far |
| let mut builder = PackageBuilder::new("some_pkg_name", FAKE_ABI_REVISION); |
| builder.overwrite_files(true); |
| builder.add_file_to_far("some/far/file", "some/src/file").unwrap(); |
| assert!(builder.add_file_as_blob("some/far/file", "some/src/file").is_err()); |
| } |
| |
| #[test] |
| fn test_builder_rejects_path_in_far_when_existing_path_as_blob() { |
| let mut builder = PackageBuilder::new("some_pkg_name", FAKE_ABI_REVISION); |
| builder.add_file_as_blob("some/far/file", "some/src/file").unwrap(); |
| assert!(builder.add_file_to_far("some/far/file", "some/src/file").is_err()); |
| } |
| |
| #[test] |
| fn test_builder_rejects_path_in_far_when_existing_path_as_blob_and_overwrite_set() { |
| // even if we set the overwrite flag, we shouldn't allow a far file to overwrite a blob |
| let mut builder = PackageBuilder::new("some_pkg_name", FAKE_ABI_REVISION); |
| builder.overwrite_files(true); |
| builder.add_file_as_blob("some/far/file", "some/src/file").unwrap(); |
| assert!(builder.add_file_to_far("some/far/file", "some/src/file").is_err()); |
| } |
| |
| #[test] |
| fn test_builder_rejects_path_in_blob_when_existing_path_as_blob() { |
| let mut builder = PackageBuilder::new("some_pkg_name", FAKE_ABI_REVISION); |
| builder.add_file_as_blob("some/far/file", "some/src/file").unwrap(); |
| assert!(builder.add_file_as_blob("some/far/file", "some/src/file").is_err()); |
| } |
| |
| #[test] |
| fn test_builder_allows_overwrite_path_as_blob_when_flag_set() { |
| let mut builder = PackageBuilder::new("some_pkg_name", FAKE_ABI_REVISION); |
| builder.overwrite_files(true); |
| builder.add_file_as_blob("some/far/file", "some/src/file").unwrap(); |
| assert!(builder.add_file_as_blob("some/far/file", "some/src/file").is_ok()); |
| } |
| |
| #[test] |
| fn test_builder_makes_file_relative_manifests_when_asked() { |
| let tmp = TempDir::new().unwrap(); |
| let outdir = Utf8Path::from_path(tmp.path()).unwrap(); |
| |
| let metafar_path = outdir.join("meta.far"); |
| let manifest_path = outdir.join("package_manifest.json"); |
| |
| // Create a file to write to the package metafar |
| let far_source_file_path = NamedTempFile::new_in(outdir).unwrap(); |
| std::fs::write(&far_source_file_path, "some data for far").unwrap(); |
| |
| // Create a file to include as a blob |
| let blob_source_file_path = outdir.join("contents/data_file"); |
| std::fs::create_dir_all(blob_source_file_path.parent().unwrap()).unwrap(); |
| let blob_contents = "some data for blob"; |
| std::fs::write(&blob_source_file_path, blob_contents).unwrap(); |
| |
| // Create the builder |
| let mut builder = PackageBuilder::new("some_pkg_name", FAKE_ABI_REVISION); |
| builder.add_file_as_blob("some/blob", &blob_source_file_path).unwrap(); |
| builder |
| .add_file_to_far( |
| "meta/some/file", |
| far_source_file_path.path().path_to_string().unwrap(), |
| ) |
| .unwrap(); |
| |
| // set it to write a manifest, with file-relative paths. |
| builder.manifest_path(manifest_path); |
| builder.manifest_blobs_relative_to(RelativeTo::File); |
| |
| // Build the package |
| let manifest = builder.build(outdir, metafar_path).unwrap(); |
| |
| // Ensure that the loaded manifest has paths still relative to the working directory, even |
| // though serialized paths should be relative to the manifest itself. |
| manifest |
| .blobs() |
| .iter() |
| .find(|b| b.source_path == blob_source_file_path) |
| .expect("The manifest should have paths relative to the working directory"); |
| |
| // The written manifest is tested in [crate::package_manifest::host_tests] |
| } |
| |
| #[test] |
| fn test_builder_add_subpackages() { |
| let outdir = TempDir::new().unwrap(); |
| let metafar_path = outdir.path().join("meta.far"); |
| |
| let mut builder = PackageBuilder::new("some_pkg_name", FAKE_ABI_REVISION); |
| |
| let pkg1_url = "pkg1".parse::<RelativePackageUrl>().unwrap(); |
| let pkg1_hash = Hash::from([0; fuchsia_hash::HASH_SIZE]); |
| let pkg1_package_manifest_path = outdir.path().join("path1/package_manifest.json"); |
| |
| let pkg2_url = "pkg2".parse::<RelativePackageUrl>().unwrap(); |
| let pkg2_hash = Hash::from([1; fuchsia_hash::HASH_SIZE]); |
| let pkg2_package_manifest_path = outdir.path().join("path2/package_manifest.json"); |
| |
| builder.add_subpackage(&pkg1_url, pkg1_hash, pkg1_package_manifest_path).unwrap(); |
| builder.add_subpackage(&pkg2_url, pkg2_hash, pkg2_package_manifest_path).unwrap(); |
| |
| // Build the package. |
| builder.build(&outdir, &metafar_path).unwrap(); |
| |
| // Validate that the metafar contains the subpackages. |
| let mut metafar = std::fs::File::open(metafar_path).unwrap(); |
| let mut far_reader = fuchsia_archive::Utf8Reader::new(&mut metafar).unwrap(); |
| let far_file_data = far_reader.read_file(MetaSubpackages::PATH).unwrap(); |
| |
| assert_eq!( |
| MetaSubpackages::deserialize(Cursor::new(&far_file_data)).unwrap(), |
| MetaSubpackages::from_iter([(pkg1_url, pkg1_hash), (pkg2_url, pkg2_hash)]) |
| ); |
| } |
| |
| #[test] |
| fn test_builder_rejects_subpackages_collisions() { |
| let url = "pkg".parse::<RelativePackageUrl>().unwrap(); |
| let hash1 = Hash::from([0; fuchsia_hash::HASH_SIZE]); |
| let package_manifest_path1 = PathBuf::from("path1/package_manifest.json"); |
| let hash2 = Hash::from([0; fuchsia_hash::HASH_SIZE]); |
| let package_manifest_path2 = PathBuf::from("path2/package_manifest.json"); |
| |
| let mut builder = PackageBuilder::new("some_pkg_name", FAKE_ABI_REVISION); |
| builder.add_subpackage(&url, hash1, package_manifest_path1).unwrap(); |
| assert!(builder.add_subpackage(&url, hash2, package_manifest_path2).is_err()); |
| } |
| } |