Skip to main content

fuchsia_pkg_testing/
package.rs

1// Copyright 2019 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5//! Test tools for building Fuchsia packages.
6
7use anyhow::{Context as _, Error, format_err};
8use blobfs_ramdisk::BlobfsRamdisk;
9use camino::{Utf8Path, Utf8PathBuf};
10use fidl_fuchsia_io as fio;
11use fuchsia_merkle::Hash;
12use fuchsia_pkg::{MetaContents, MetaSubpackages, PackageManifest};
13use fuchsia_url::PackageName;
14use fuchsia_url::fuchsia_pkg::PinnedAbsolutePackageUrl;
15use futures::join;
16use futures::prelude::*;
17use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
18use std::convert::TryInto as _;
19use std::fs::{self, File};
20use std::io::{self, Read};
21use std::path::{Path, PathBuf};
22use tempfile::TempDir;
23use version_history::AbiRevision;
24use walkdir::WalkDir;
25use zx::Status;
26
27/// A package generated by a [`PackageBuilder`], suitable for assembling into a TUF repository.
28#[derive(Debug)]
29pub struct Package {
30    name: PackageName,
31    meta_far_merkle: Hash,
32    _artifacts_tmp: TempDir,
33    artifacts: Utf8PathBuf,
34    // If None then the package has subpackages but this `Package` does not have all the blobs
35    // needed by those subpackages.
36    subpackage_blobs: Option<HashMap<Hash, Vec<u8>>>,
37}
38
39#[derive(Debug, PartialEq)]
40enum PackageEntry {
41    Directory,
42    File(Vec<u8>),
43}
44
45impl PackageEntry {
46    fn is_dir(&self) -> bool {
47        matches!(self, PackageEntry::Directory)
48    }
49}
50pub struct BlobFile {
51    pub merkle: fuchsia_merkle::Hash,
52    pub file: File,
53}
54
55/// Contents of a Blob.
56pub struct BlobContents {
57    /// Merkle hash of the blob.
58    pub merkle: fuchsia_merkle::Hash,
59
60    /// Binary contents of the blob.
61    pub contents: Vec<u8>,
62}
63
64impl Package {
65    /// The merkle root of the package's meta.far.
66    pub fn hash(&self) -> &Hash {
67        &self.meta_far_merkle
68    }
69
70    /// The package's meta.far.
71    pub fn meta_far(&self) -> io::Result<File> {
72        File::open(self.artifacts.join("meta.far"))
73    }
74
75    /// The name of the package.
76    pub fn name(&self) -> &PackageName {
77        &self.name
78    }
79
80    /// The pinned fuchsia-pkg url of the package on fuchsia.com.
81    pub fn pinned_fuchsia_url(&self) -> PinnedAbsolutePackageUrl {
82        let unpinned = format!("fuchsia-pkg://fuchsia.com/{}", self.name).parse().unwrap();
83        PinnedAbsolutePackageUrl::from_unpinned(unpinned, self.meta_far_merkle)
84    }
85
86    /// The directory containing the blobs contained in the package, including the meta.far.
87    pub fn artifacts(&self) -> &Utf8Path {
88        &self.artifacts
89    }
90
91    /// Builds and returns the package located at "/pkg" in the current namespace.
92    pub async fn identity() -> Result<Self, Error> {
93        Self::from_dir("/pkg").await
94    }
95
96    /// Builds and returns the package located at the given path in the current namespace.
97    pub async fn from_dir(root: impl AsRef<Path>) -> Result<Self, Error> {
98        let root = root.as_ref();
99        let package_directory = fuchsia_pkg::PackageDirectory::from_proxy(
100            fuchsia_fs::directory::open_in_namespace(root.to_str().unwrap(), fio::PERM_READABLE)?,
101        );
102
103        let meta_package = package_directory.meta_package().await.context("read meta/package")?;
104        let abi_revision = package_directory.abi_revision().await.context("read abi revision")?;
105
106        let mut pkg =
107            PackageBuilder::new_with_abi_revision(meta_package.name().as_ref(), abi_revision);
108
109        fn is_generated_file(path: &Path) -> bool {
110            matches!(
111                path.to_str(),
112                Some("meta/contents")
113                    | Some("meta/package")
114                    | Some(AbiRevision::PATH)
115                    | Some(MetaSubpackages::PATH)
116            )
117        }
118
119        // Add all non-generated files from this package into `pkg`.
120        for entry in WalkDir::new(root) {
121            let entry = entry?;
122            let path = entry.path();
123            if !entry.file_type().is_file() || is_generated_file(path.strip_prefix(root).unwrap()) {
124                continue;
125            }
126
127            let relative_path = path.strip_prefix(root).unwrap();
128            let f = File::open(path).context("open package blob")?;
129            pkg = pkg.add_resource_at(relative_path.to_str().unwrap(), f);
130        }
131
132        let subpackages = package_directory
133            .meta_subpackages()
134            .await
135            .context("read meta subpackages")?
136            .into_subpackages();
137        if !subpackages.is_empty() {
138            for (name, hash) in subpackages.into_iter() {
139                pkg = pkg.add_subpackage_by_hash(name, hash);
140            }
141        }
142
143        pkg.build().await
144    }
145
146    /// Returns the parsed contents of the meta/contents file.
147    pub fn meta_contents(&self) -> Result<MetaContents, Error> {
148        let mut raw_meta_far = self.meta_far()?;
149        let mut meta_far = fuchsia_archive::Utf8Reader::new(&mut raw_meta_far)?;
150        let raw_meta_contents = meta_far.read_file("meta/contents")?;
151
152        Ok(MetaContents::deserialize(raw_meta_contents.as_slice())?)
153    }
154
155    /// Returns the parsed contents of the subpackages manifest.
156    pub fn meta_subpackages(&self) -> Result<MetaSubpackages, Error> {
157        let mut raw_meta_far = self.meta_far()?;
158        let mut meta_far = fuchsia_archive::Utf8Reader::new(&mut raw_meta_far)?;
159        Ok(match meta_far.read_file(MetaSubpackages::PATH) {
160            Ok(bytes) => MetaSubpackages::deserialize(std::io::BufReader::new(bytes.as_slice()))?,
161            Err(fuchsia_archive::Error::PathNotPresent(_)) => MetaSubpackages::default(),
162            Err(e) => Err(e)?,
163        })
164    }
165
166    /// Returns a set of all unique blobs contained in this package, including meta.far and
167    /// subpackage blobs.
168    ///
169    /// # Panics
170    /// If either there are unknown subpackage blobs or there is an error reading meta/contents.
171    pub fn list_blobs(&self) -> BTreeSet<Hash> {
172        self.meta_contents()
173            .expect("loading meta/contents")
174            .into_hashes_undeduplicated()
175            .chain([self.meta_far_merkle])
176            .chain(
177                self.subpackage_blobs
178                    .as_ref()
179                    .unwrap_or_else(|| {
180                        panic!(
181                            "cannot list blobs for package {} with unknown subpackage blobs",
182                            self.name()
183                        )
184                    })
185                    .keys()
186                    .copied(),
187            )
188            .collect()
189    }
190
191    /// Returns an iterator of merkle/File pairs for each content blob in the package.
192    ///
193    /// Does not include the meta.far, see `meta_far()` and `meta_far_merkle_root()`, instead.
194    pub fn content_blob_files(&self) -> impl Iterator<Item = BlobFile> {
195        let manifest =
196            fuchsia_pkg::PackageManifest::try_load_from(self.artifacts().join("manifest.json"))
197                .unwrap();
198        struct Blob {
199            merkle: fuchsia_merkle::Hash,
200            path: Utf8PathBuf,
201        }
202        #[allow(clippy::needless_collect)]
203        let blobs = manifest
204            .into_blobs()
205            .into_iter()
206            .filter(|blob| blob.path != PackageManifest::META_FAR_BLOB_PATH)
207            .map(|blob| Blob {
208                merkle: blob.merkle,
209                path: self.artifacts().join(&blob.source_path),
210            })
211            .collect::<Vec<_>>();
212
213        blobs
214            .into_iter()
215            .map(|blob| BlobFile { merkle: blob.merkle, file: File::open(blob.path).unwrap() })
216    }
217
218    /// Returns a tuple of the contents of the meta far and the contents of all content blobs in the package.
219    pub fn contents(&self) -> (BlobContents, HashMap<Hash, Vec<u8>>) {
220        (
221            BlobContents {
222                merkle: self.meta_far_merkle,
223                contents: io::BufReader::new(self.meta_far().unwrap())
224                    .bytes()
225                    .collect::<Result<Vec<u8>, _>>()
226                    .unwrap(),
227            },
228            self.content_blob_files()
229                .map(|blob_file| {
230                    (
231                        blob_file.merkle,
232                        io::BufReader::new(blob_file.file)
233                            .bytes()
234                            .collect::<Result<Vec<u8>, _>>()
235                            .unwrap(),
236                    )
237                })
238                .collect(),
239        )
240    }
241
242    /// Returns None if this `Package` has subpackages but doesn't have the blobs.
243    pub fn content_and_subpackage_blobs(&self) -> Option<HashMap<Hash, Vec<u8>>> {
244        if let Some(subpackage_blobs) = &self.subpackage_blobs {
245            let mut subpackage_blobs = subpackage_blobs.clone();
246            subpackage_blobs.extend(self.content_blob_files().map(|blob_file| {
247                (
248                    blob_file.merkle,
249                    io::BufReader::new(blob_file.file)
250                        .bytes()
251                        .collect::<Result<Vec<u8>, _>>()
252                        .unwrap(),
253                )
254            }));
255            Some(subpackage_blobs)
256        } else {
257            None
258        }
259    }
260
261    /// Writes the meta.far and all content blobs to blobfs.
262    /// Does not write the subpackage blobs, if any.
263    pub async fn write_to_blobfs_ignore_subpackages(&self, blobfs_ramdisk: &BlobfsRamdisk) {
264        fn read_file(file: &std::fs::File) -> Vec<u8> {
265            let mut ret = vec![];
266            std::io::BufReader::new(file).read_to_end(&mut ret).unwrap();
267            ret
268        }
269
270        blobfs_ramdisk
271            .write_blob(*self.hash(), &read_file(&self.meta_far().unwrap()))
272            .await
273            .expect("write_blob failed");
274        for blob in self.content_blob_files() {
275            blobfs_ramdisk
276                .write_blob(blob.merkle, &read_file(&blob.file))
277                .await
278                .expect("write_blob failed");
279        }
280    }
281
282    /// Writes the meta.far and all content and subpackage blobs to blobfs.
283    pub async fn write_to_blobfs(&self, blobfs_ramdisk: &BlobfsRamdisk) {
284        let subpackage_blobs = self
285            .subpackage_blobs
286            .as_ref()
287            .expect("package must know the subpackage blobs to write them");
288        let () = self.write_to_blobfs_ignore_subpackages(blobfs_ramdisk).await;
289        for (hash, content) in subpackage_blobs {
290            blobfs_ramdisk.write_blob(*hash, content).await.expect("write_blob failed");
291        }
292    }
293
294    /// Verifies that the given directory serves the contents of this package.
295    pub async fn verify_contents(
296        &self,
297        dir: &fio::DirectoryProxy,
298    ) -> Result<(), VerificationError> {
299        let mut raw_meta_far = self.meta_far()?;
300        let mut meta_far = fuchsia_archive::Utf8Reader::new(&mut raw_meta_far)?;
301        let mut expected_paths = HashSet::new();
302
303        // Verify all entries referenced by meta/contents exist and have the correct merkle root.
304        let raw_meta_contents = meta_far.read_file("meta/contents")?;
305        let meta_contents = MetaContents::deserialize(raw_meta_contents.as_slice())?;
306        for (path, merkle) in meta_contents.contents() {
307            let actual_merkle = fuchsia_merkle::root_from_slice(read_file(dir, path).await?);
308            if merkle != &actual_merkle {
309                return Err(VerificationError::DifferentFileData { path: path.to_owned() });
310            }
311            expected_paths.insert(path.to_owned());
312        }
313
314        // Verify all entries in the meta FAR exist and have the correct contents.
315        for path in meta_far.list().map(|e| e.path().to_string()).collect::<Vec<_>>() {
316            if read_file(dir, path.as_str()).await? != meta_far.read_file(path.as_str())? {
317                return Err(VerificationError::DifferentFileData { path });
318            }
319            expected_paths.insert(path);
320        }
321
322        // Verify no other entries exist in the served directory.
323        let mut stream = fuchsia_fs::directory::readdir_recursive(dir, /*timeout=*/ None);
324        while let Some(entry) = stream.try_next().await? {
325            let path = entry.name;
326            if !expected_paths.contains(path.as_str()) {
327                return Err(VerificationError::ExtraFile { path });
328            }
329        }
330
331        Ok(())
332    }
333
334    /// The blobs used by all of the subpackages of this package (recursively).
335    /// If None, this `Package` has subpackages but does not have the blobs needed by those
336    /// subpackages.
337    pub fn subpackage_blobs(&self) -> Option<&HashMap<Hash, Vec<u8>>> {
338        self.subpackage_blobs.as_ref()
339    }
340}
341
342async fn read_file(dir: &fio::DirectoryProxy, path: &str) -> Result<Vec<u8>, VerificationError> {
343    let (file, server_end) = fidl::endpoints::create_proxy::<fio::FileMarker>();
344
345    let flags = fio::Flags::FLAG_SEND_REPRESENTATION | fio::PERM_READABLE;
346    dir.open(path, flags, &fio::Options::default(), server_end.into_channel())
347        .expect("open3 request failed to send");
348
349    let mut events = file.take_event_stream();
350    let open = async move {
351        let event = match events.next().await.expect("Some(event)") {
352            Ok(representation) => match representation {
353                fio::FileEvent::OnOpen_ { s, info } => {
354                    match Status::ok(s) {
355                        Err(Status::NOT_FOUND) => {
356                            Err(VerificationError::MissingFile { path: path.to_owned() })
357                        }
358                        Err(status) => {
359                            Err(format_err!("unable to open {:?}: {:?}", path, status).into())
360                        }
361                        Ok(()) => Ok(()),
362                    }?;
363
364                    match *info.expect("fio::FileEvent to have fio::NodeInfoDeprecated") {
365                        fio::NodeInfoDeprecated::File(fio::FileObject { event, .. }) => event,
366                        other => {
367                            panic!(
368                                "fio::NodeInfoDeprecated from fio::FileEventStream to be File variant with event: {other:?}"
369                            )
370                        }
371                    }
372                }
373                fio::FileEvent::OnRepresentation { payload } => match payload {
374                    fio::Representation::File(fio::FileInfo { observer, .. }) => observer,
375                    other => {
376                        panic!(
377                            "ConnectionInfo from fio::FileEventStream to be File variant with event: {other:?}"
378                        )
379                    }
380                },
381                fio::FileEvent::_UnknownEvent { ordinal, .. } => {
382                    panic!("unknown file event {ordinal}")
383                }
384            },
385            // If not found, Open3 will send an epitaph when closing the channel.
386            Err(fidl::Error::ClientChannelClosed { epitaph, .. })
387                if epitaph == zx::Status::NOT_FOUND =>
388            {
389                return Err(VerificationError::MissingFile { path: path.to_owned() });
390            }
391            Err(other_e) => {
392                return Err(
393                    format_err!("open fidl request at {:?} failed: {:?}", path, other_e).into()
394                );
395            }
396        };
397
398        // Files served by the package will either provide an event in its describe info (if that
399        // file is actually a blob from blobfs) or not provide an event (if that file is, for
400        // example, a file contained within the meta far being served in the meta/ directory).
401        //
402        // If the file is a blobfs blob, we want to make sure it is readable. We can just try to
403        // read from it, but the preferred method to wait for a blobfs blob to become readable is
404        // to wait on the USER_0 signal to become asserted on the file's event.
405        //
406        // As all blobs served by a package should already be readable, we assert that USER_0 is
407        // already asserted on the event.
408        if let Some(event) = event {
409            match event
410                .wait_one(
411                    zx::Signals::USER_0,
412                    zx::MonotonicInstant::after(zx::MonotonicDuration::from_seconds(0)),
413                )
414                .to_result()
415            {
416                Err(Status::TIMED_OUT) => Err(VerificationError::from(format_err!(
417                    "file served by blobfs is not complete/readable as USER_0 signal was not set on the File's event: {}",
418                    path
419                ))),
420                Err(other_status) => Err(VerificationError::from(format_err!(
421                    "wait_handle failed with status: {:?} {:?}",
422                    other_status,
423                    path
424                ))),
425                Ok(_) => Ok(()),
426            }
427        } else {
428            Ok(())
429        }
430    };
431
432    let read = async {
433        let result =
434            file.get_backing_memory(fio::VmoFlags::READ).await?.map_err(Status::err_from_raw);
435
436        let mut expect_empty_blob = false;
437
438        // Attempt to get the backing VMO, which is faster. Fall back to reading over FIDL
439        match result {
440            Ok(vmo) => {
441                let size = vmo.get_content_size().context("unable to get vmo size")?;
442                let mut buf = vec![0u8; size as usize];
443                let () = vmo.read(&mut buf[..], 0).context("unable to read from vmo")?;
444                return Ok(buf);
445            }
446            Err(status) => match status {
447                Status::NOT_SUPPORTED => {}
448                Status::BAD_STATE => {
449                    // may or may not be intended behavior, but the empty blob will not provide a vmo,
450                    // failing with BAD_STATE. Verify in the read path below that the blob is indeed
451                    // zero length if this happens.
452                    expect_empty_blob = true;
453                }
454                status => {
455                    return Err(VerificationError::from(format_err!(
456                        "unexpected error opening file buffer: {:?}",
457                        status
458                    )));
459                }
460            },
461        }
462
463        let mut buf = vec![];
464        loop {
465            let chunk = file
466                .read(fio::MAX_BUF)
467                .await
468                .context("file read to respond")?
469                .map_err(Status::err_from_raw)
470                .map_err(|status| VerificationError::FileReadError { path: path.into(), status })?;
471
472            if chunk.is_empty() {
473                if expect_empty_blob {
474                    assert_eq!(buf, Vec::<u8>::new());
475                }
476                return Ok(buf);
477            }
478
479            buf.extend(chunk);
480        }
481    };
482
483    let (open, read) = join!(open, read);
484    let close_result = file.close().await;
485    let result = open.and(read)?;
486    // Only check close_result if everything that came before it looks good.
487    let close_result = close_result.context("file close to respond")?;
488    close_result.map_err(|status| {
489        format_err!("unable to close {:?}: {:?}", path, zx::Status::err_from_raw(status))
490    })?;
491    Ok(result)
492}
493
494/// An error that can occur while verifying the contents of a directory.
495#[derive(Debug)]
496pub enum VerificationError {
497    /// The directory is serving a file that isn't in the package.
498    ExtraFile {
499        /// Path to the extra file.
500        path: String,
501    },
502    /// The directory is not serving a particular file that it should be serving.
503    MissingFile {
504        /// Path to the missing file.
505        path: String,
506    },
507    /// The actual merkle of the file does not match the merkle listed in the meta FAR.
508    DifferentFileData {
509        /// Path to the file.
510        path: String,
511    },
512    /// Read method on file failed.
513    FileReadError {
514        /// Path to the file
515        path: String,
516        /// Read result
517        status: Status,
518    },
519    /// Anything else.
520    Other(Error),
521}
522
523impl<T: Into<Error>> From<T> for VerificationError {
524    fn from(x: T) -> Self {
525        VerificationError::Other(x.into())
526    }
527}
528
529/// A builder to simplify construction of Fuchsia packages.
530pub struct PackageBuilder {
531    name: PackageName,
532    contents: BTreeMap<PathBuf, PackageEntry>,
533
534    has_subpackages: bool,
535    // If None the package has subpackages but this `PackageBuilder` does not have the blobs
536    // needed by those subpackages.
537    subpackage_blobs: Option<HashMap<Hash, Vec<u8>>>,
538
539    builder: fuchsia_pkg::PackageBuilder,
540    _artifacts_tmp: TempDir,
541    artifacts: Utf8PathBuf,
542}
543
544impl PackageBuilder {
545    /// Creates a new `PackageBuilder`.
546    ///
547    /// # Panics
548    ///
549    /// Panics if either:
550    /// * `name` is an invalid package name.
551    /// * Creating a tempdir fails.
552    pub fn new(name: impl Into<String>) -> Self {
553        Self::new_with_abi_revision(
554            name,
555            // Default to ABI revision for API level 7.
556            0xECCEA2F70ACD6FC0.into(),
557        )
558    }
559
560    /// Creates a new `PackageBuilder`, just like `PackageBuilder::new()`, but
561    /// allowing the caller to specify the ABI revision with which to stamp the
562    /// test package.
563    pub fn new_with_abi_revision(name: impl Into<String>, abi_revision: AbiRevision) -> Self {
564        let name = name.into();
565
566        let artifacts_tmp = tempfile::tempdir().expect("create tempdir for package");
567        let artifacts = Utf8Path::from_path(artifacts_tmp.path())
568            .expect("checking packagedir is UTF-8")
569            .to_path_buf();
570
571        fs::create_dir(artifacts.join("contents")).expect("create /packages/contents");
572
573        let mut builder = fuchsia_pkg::PackageBuilder::new(&name, abi_revision);
574        builder.manifest_path(artifacts.join("manifest.json"));
575        builder.repository("fuchsia.com");
576        builder.manifest_blobs_relative_to(fuchsia_pkg::RelativeTo::File);
577
578        Self {
579            builder,
580            name: name.try_into().unwrap(),
581            contents: BTreeMap::new(),
582            has_subpackages: false,
583            subpackage_blobs: Some(HashMap::new()),
584            _artifacts_tmp: artifacts_tmp,
585            artifacts,
586        }
587    }
588
589    /// Create a subdirectory within the package.
590    ///
591    /// # Panics
592    ///
593    /// Panics if the package contains a file entry at `path` or any of its ancestors.
594    pub fn dir(mut self, path: impl Into<PathBuf>) -> PackageDir {
595        let path = path.into();
596        self.make_dirs(&path);
597        PackageDir::new(self, path)
598    }
599
600    /// Adds the provided `contents` to the package at the given `path`.
601    ///
602    /// # Panics
603    ///
604    /// Panics if either:
605    /// * The package already contains a file or directory at `path`.
606    /// * The package contains a file at any of `path`'s ancestors.
607    pub fn add_resource_at(
608        mut self,
609        path: impl Into<PathBuf>,
610        mut contents: impl io::Read,
611    ) -> Self {
612        let path = path.into();
613        let path_str = path.to_str().unwrap();
614        let () = fuchsia_url::Resource::validate_str(
615            path.to_str().unwrap_or_else(|| panic!("path must be utf8: {path:?}")),
616        )
617        .unwrap_or_else(|_| panic!("path must be an object relative path expression: {path:?}"));
618
619        let mut data = vec![];
620        contents.read_to_end(&mut data).unwrap();
621
622        if path.starts_with("meta/") {
623            self.builder
624                .add_contents_to_far(path_str, &data, self.artifacts.join("contents"))
625                .expect("adding meta blob to succeed");
626        } else {
627            self.builder
628                .add_contents_as_blob(path_str, &data, self.artifacts.join("contents"))
629                .expect("adding blob to succeed");
630        }
631
632        let replaced = self.contents.insert(path.clone(), PackageEntry::File(data));
633        assert_eq!(None, replaced, "already contains an entry at {path:?}");
634        self
635    }
636
637    fn make_dirs(&mut self, path: &Path) {
638        for ancestor in path.ancestors() {
639            if ancestor == Path::new("") {
640                continue;
641            }
642            assert!(
643                self.contents
644                    .entry(ancestor.to_owned())
645                    .or_insert(PackageEntry::Directory)
646                    .is_dir(),
647                "{ancestor:?} is not a directory"
648            );
649        }
650    }
651
652    /// Adds the provided `subpackage` to the package with name `name`.
653    ///
654    /// # Panics
655    ///
656    /// Panics if either:
657    /// * `name` is not a valid RelativePackageUrl
658    /// * There is already a subpackage called `name`
659    pub fn add_subpackage(
660        mut self,
661        name: impl TryInto<fuchsia_url::RelativePackageUrl>,
662        subpackage: &Package,
663    ) -> Self {
664        let name = name.try_into().map_err(|_| ()).expect("valid RelativePackageUrl");
665        let manifest_path = subpackage.artifacts().join("manifest.json").into();
666
667        self.builder.add_subpackage(&name, *subpackage.hash(), manifest_path).unwrap();
668        self.has_subpackages = true;
669
670        match (&mut self.subpackage_blobs, &subpackage.subpackage_blobs) {
671            (Some(current_blobs), Some(new_blobs)) => {
672                let (meta_far, content_blobs) = subpackage.contents();
673                current_blobs.insert(meta_far.merkle, meta_far.contents);
674                current_blobs.extend(content_blobs);
675                current_blobs.extend(new_blobs.iter().map(|(k, v)| (*k, v.clone())));
676            }
677            (Some(_), None) => self.subpackage_blobs = None,
678            (None, Some(_)) | (None, None) => {}
679        }
680
681        self
682    }
683
684    /// Adds a subpackage with name `name` and hash `hash` to the package.
685    /// Because the blobs of the subpackage are not provided, the `Package` built from this
686    /// `PackageBuilder` will not have the subpackage blobs.
687    ///
688    /// # Panics
689    ///
690    /// Panics if either:
691    /// * `name` is not a valid RelativePackageUrl
692    /// * There is already a subpackage called `name`
693    pub fn add_subpackage_by_hash(
694        mut self,
695        name: impl TryInto<fuchsia_url::RelativePackageUrl>,
696        hash: Hash,
697    ) -> Self {
698        let name = name.try_into().map_err(|_| ()).expect("valid RelativePackageUrl");
699
700        self.builder.add_subpackage(&name, hash, "".into()).unwrap();
701        self.has_subpackages = true;
702        self.subpackage_blobs = None;
703        self
704    }
705
706    /// Builds the package.
707    pub async fn build(self) -> Result<Package, Error> {
708        // self.artifacts contains outputs from package creation (manifest.json/meta.far) as well
709        // as all blobs contained in the package.
710        //
711        // Layout of self.artifacts:
712        // - manifest.json
713        // - meta.far
714        // - contents/
715        // -   meta/
716        // -     non-generated meta.far files
717        // -   file/dir{N}
718
719        let manifest = self.builder.build(&self.artifacts, self.artifacts.join("meta.far"))?;
720        let meta_far_merkle =
721            manifest.blobs().iter().find(|b| b.path == "meta/").context("finding meta/")?.merkle;
722
723        // clean up after ourselves
724        fs::remove_file(self.artifacts.join("meta/fuchsia.abi/abi-revision"))?;
725        fs::remove_dir(self.artifacts.join("meta/fuchsia.abi"))?;
726        if self.has_subpackages {
727            fs::remove_file(self.artifacts.join("meta/fuchsia.pkg/subpackages"))?;
728            fs::remove_dir(self.artifacts.join("meta/fuchsia.pkg"))?;
729        }
730        fs::remove_file(self.artifacts.join("meta/package"))?;
731        fs::remove_dir(self.artifacts.join("meta"))?;
732
733        Ok(Package {
734            name: self.name,
735            meta_far_merkle,
736            _artifacts_tmp: self._artifacts_tmp,
737            artifacts: self.artifacts,
738            subpackage_blobs: self.subpackage_blobs,
739        })
740    }
741}
742
743/// A subdirectory of a package being built.
744pub struct PackageDir {
745    pkg: PackageBuilder,
746    path: PathBuf,
747}
748
749impl PackageDir {
750    fn new(pkg: PackageBuilder, path: impl Into<PathBuf>) -> Self {
751        Self { pkg, path: path.into() }
752    }
753
754    /// Adds the provided `contents` to the package at the given `path`, relative to this
755    /// `PackageDir`.
756    ///
757    /// # Panics
758    /// If the package already contains a resource at `path`, relative to this `PackageDir`.
759    pub fn add_resource_at(mut self, path: impl AsRef<Path>, contents: impl io::Read) -> Self {
760        self.pkg = self.pkg.add_resource_at(self.path.join(path.as_ref()), contents);
761        self
762    }
763
764    /// Finish adding resources to this directory, returning the modified [`PackageBuilder`].
765    pub fn finish(self) -> PackageBuilder {
766        self.pkg
767    }
768}
769
770#[cfg(test)]
771mod tests {
772    use super::*;
773    use assert_matches::assert_matches;
774    use fuchsia_pkg::MetaPackage;
775
776    #[test]
777    #[should_panic(expected = "adding blob to succeed")]
778    fn test_panics_file_with_existing_parent_as_file() {
779        let _: Result<(), Error> = {
780            PackageBuilder::new("test")
781                .add_resource_at("data", "data contents".as_bytes())
782                .add_resource_at("data/foo", "data/foo contents".as_bytes());
783            Ok(())
784        };
785    }
786
787    #[test]
788    #[should_panic(expected = r#""data" is not a directory"#)]
789    fn test_panics_dir_with_existing_file() {
790        let _: Result<(), Error> = {
791            PackageBuilder::new("test")
792                .add_resource_at("data", "data contents".as_bytes())
793                .dir("data");
794            Ok(())
795        };
796    }
797
798    #[test]
799    #[should_panic(expected = r#""data" is not a directory"#)]
800    fn test_panics_nested_dir_with_existing_file() {
801        let _: Result<(), Error> = {
802            PackageBuilder::new("test")
803                .add_resource_at("data", "data contents".as_bytes())
804                .dir("data/foo");
805            Ok(())
806        };
807    }
808
809    #[test]
810    #[should_panic(expected = "adding blob to succeed")]
811    fn test_panics_file_with_existing_dir() {
812        let _: Result<(), Error> = {
813            PackageBuilder::new("test")
814                .dir("data")
815                .add_resource_at("foo", "data/foo contents".as_bytes())
816                .finish()
817                .add_resource_at("data", "data contents".as_bytes());
818            Ok(())
819        };
820    }
821
822    #[fuchsia::test]
823    async fn test_basic() -> Result<(), Error> {
824        let pkg = PackageBuilder::new("rolldice")
825            .dir("bin")
826            .add_resource_at("rolldice", "asldkfjaslkdfjalskdjfalskdf".as_bytes())
827            .finish()
828            .build()
829            .await?;
830
831        assert_eq!(
832            pkg.meta_far_merkle,
833            "de210ba39b8f597cc1986c37b369c990707649f63bb8fa23b244a38274018b78".parse()?
834        );
835        assert_eq!(pkg.meta_far_merkle, fuchsia_merkle::root_from_reader(pkg.meta_far()?)?);
836        assert_eq!(
837            pkg.list_blobs(),
838            BTreeSet::from([
839                "de210ba39b8f597cc1986c37b369c990707649f63bb8fa23b244a38274018b78".parse()?,
840                "b5b34f6234631edc7ccaa25533e2050e5d597a7331c8974306b617a3682a3197".parse()?
841            ])
842        );
843
844        Ok(())
845    }
846
847    #[fuchsia::test]
848    async fn test_content_blob_files() -> Result<(), Error> {
849        let pkg = PackageBuilder::new("rolldice")
850            .dir("bin")
851            .add_resource_at("rolldice", "asldkfjaslkdfjalskdjfalskdf".as_bytes())
852            .add_resource_at("rolldice2", "asldkfjaslkdfjalskdjfalskdf".as_bytes())
853            .finish()
854            .build()
855            .await?;
856
857        let mut iter = pkg.content_blob_files();
858        // 2 identical entries
859        for _ in 0..2 {
860            let BlobFile { merkle, mut file } = iter.next().unwrap();
861            assert_eq!(
862                merkle,
863                "b5b34f6234631edc7ccaa25533e2050e5d597a7331c8974306b617a3682a3197".parse().unwrap()
864            );
865            let mut contents = vec![];
866            file.read_to_end(&mut contents).unwrap();
867            assert_eq!(contents, b"asldkfjaslkdfjalskdjfalskdf")
868        }
869        assert_eq!(iter.next().map(|b| b.merkle), None);
870
871        Ok(())
872    }
873
874    #[fuchsia::test]
875    async fn test_dir_semantics() -> Result<(), Error> {
876        let with_dir = PackageBuilder::new("data-file")
877            .dir("data")
878            .add_resource_at("file", "contents".as_bytes())
879            .finish()
880            .build()
881            .await?;
882
883        let with_direct = PackageBuilder::new("data-file")
884            .add_resource_at("data/file", "contents".as_bytes())
885            .build()
886            .await?;
887
888        assert_eq!(with_dir.hash(), with_direct.hash());
889
890        Ok(())
891    }
892
893    /// Creates a clone of the contents of /pkg in a tempdir so that tests can manipulate its
894    /// contents.
895    fn make_this_package_dir() -> Result<tempfile::TempDir, Error> {
896        let dir = tempfile::tempdir()?;
897
898        let this_package_root = Path::new("/pkg");
899
900        for entry in WalkDir::new(this_package_root) {
901            let entry = entry?;
902            let path = entry.path();
903
904            let relative_path = path.strip_prefix(this_package_root).unwrap();
905            let rebased_path = dir.path().join(relative_path);
906
907            if entry.file_type().is_dir() {
908                fs::create_dir_all(rebased_path)?;
909            } else if entry.file_type().is_file() {
910                fs::copy(path, rebased_path)?;
911            }
912        }
913
914        Ok(dir)
915    }
916
917    #[fuchsia::test]
918    async fn test_from_dir() {
919        let abi_revision = AbiRevision::from_u64(0x5836508c2defac54); // Random value.
920
921        let root = {
922            let dir = tempfile::tempdir().unwrap();
923
924            fs::create_dir(dir.path().join("meta")).unwrap();
925            fs::create_dir(dir.path().join("data")).unwrap();
926
927            MetaPackage::from_name_and_variant_zero("asdf".parse().unwrap())
928                .serialize(File::create(dir.path().join("meta/package")).unwrap())
929                .unwrap();
930
931            fs::create_dir(dir.path().join("meta/fuchsia.abi")).unwrap();
932            fs::write(dir.path().join("meta/fuchsia.abi/abi-revision"), abi_revision.as_bytes())
933                .unwrap();
934
935            fs::write(dir.path().join("data/hello"), "world").unwrap();
936
937            dir
938        };
939
940        let from_dir = Package::from_dir(root.path()).await.unwrap();
941
942        let pkg = PackageBuilder::new_with_abi_revision("asdf", abi_revision)
943            .add_resource_at("data/hello", "world".as_bytes())
944            .build()
945            .await
946            .unwrap();
947
948        assert_eq!(from_dir.meta_far_merkle, pkg.meta_far_merkle);
949    }
950
951    #[fuchsia::test]
952    async fn test_identity() -> Result<(), Error> {
953        let pkg = Package::identity().await.unwrap();
954
955        assert_eq!(pkg.meta_far_merkle, fuchsia_merkle::root_from_reader(pkg.meta_far()?)?);
956
957        // Verify the generated package's merkle root is the same as this test package's merkle root.
958        assert_eq!(pkg.meta_far_merkle, fs::read_to_string("/pkg/meta")?.parse()?);
959
960        let this_pkg_dir = fuchsia_fs::directory::open_in_namespace("/pkg", fio::PERM_READABLE)?;
961        pkg.verify_contents(&this_pkg_dir).await.expect("contents to be equivalent");
962
963        let pkg_dir = make_this_package_dir()?;
964
965        let this_pkg_dir = fuchsia_fs::directory::open_in_namespace(
966            pkg_dir.path().to_str().unwrap(),
967            fio::PERM_READABLE,
968        )?;
969
970        assert_matches!(pkg.verify_contents(&this_pkg_dir).await, Ok(()));
971
972        Ok(())
973    }
974
975    #[fuchsia::test]
976    async fn test_verify_contents_rejects_extra_blob() -> Result<(), Error> {
977        let pkg = Package::identity().await?;
978        let pkg_dir = make_this_package_dir()?;
979
980        fs::write(pkg_dir.path().join("unexpected"), "unexpected file".as_bytes())?;
981
982        let pkg_dir_proxy = fuchsia_fs::directory::open_in_namespace(
983            pkg_dir.path().to_str().unwrap(),
984            fio::PERM_READABLE,
985        )?;
986
987        assert_matches!(
988            pkg.verify_contents(&pkg_dir_proxy).await,
989            Err(VerificationError::ExtraFile{ref path}) if path == "unexpected");
990
991        Ok(())
992    }
993
994    #[fuchsia::test]
995    async fn test_verify_contents_rejects_extra_meta_file() -> Result<(), Error> {
996        let pkg = Package::identity().await?;
997        let pkg_dir = make_this_package_dir()?;
998
999        fs::write(pkg_dir.path().join("meta/unexpected"), "unexpected file".as_bytes())?;
1000
1001        let pkg_dir_proxy = fuchsia_fs::directory::open_in_namespace(
1002            pkg_dir.path().to_str().unwrap(),
1003            fio::PERM_READABLE,
1004        )?;
1005
1006        assert_matches!(
1007            pkg.verify_contents(&pkg_dir_proxy).await,
1008            Err(VerificationError::ExtraFile{ref path}) if path == "meta/unexpected");
1009
1010        Ok(())
1011    }
1012
1013    #[fuchsia::test]
1014    async fn test_verify_contents_rejects_missing_blob() -> Result<(), Error> {
1015        let pkg = Package::identity().await?;
1016        let pkg_dir = make_this_package_dir()?;
1017
1018        fs::remove_file(pkg_dir.path().join("bin/fuchsia_pkg_testing_lib_test"))?;
1019
1020        let pkg_dir_proxy = fuchsia_fs::directory::open_in_namespace(
1021            pkg_dir.path().to_str().unwrap(),
1022            fio::PERM_READABLE,
1023        )?;
1024
1025        assert_matches!(
1026            pkg.verify_contents(&pkg_dir_proxy).await,
1027            Err(VerificationError::MissingFile{ref path}) if path == "bin/fuchsia_pkg_testing_lib_test");
1028
1029        Ok(())
1030    }
1031
1032    #[fuchsia::test]
1033    async fn test_verify_contents_rejects_different_contents() -> Result<(), Error> {
1034        let pkg = Package::identity().await?;
1035        let pkg_dir = make_this_package_dir()?;
1036
1037        fs::write(pkg_dir.path().join("bin/fuchsia_pkg_testing_lib_test"), "broken".as_bytes())?;
1038
1039        let pkg_dir_proxy = fuchsia_fs::directory::open_in_namespace(
1040            pkg_dir.path().to_str().unwrap(),
1041            fio::PERM_READABLE,
1042        )?;
1043
1044        assert_matches!(
1045            pkg.verify_contents(&pkg_dir_proxy).await,
1046            Err(VerificationError::DifferentFileData{ref path}) if path == "bin/fuchsia_pkg_testing_lib_test");
1047
1048        Ok(())
1049    }
1050
1051    #[fuchsia::test]
1052    async fn test_meta_subpackages_with_no_subpackages() {
1053        let pkg = PackageBuilder::new("pkg").build().await.unwrap();
1054
1055        assert!(pkg.meta_subpackages().unwrap().subpackages().is_empty());
1056    }
1057
1058    #[fuchsia::test]
1059    async fn test_add_subpackage() {
1060        // Package with subpackage.
1061        let sub_sub_pkg = PackageBuilder::new("sub-sub-pkg")
1062            .add_resource_at("c-blob", "c-blob-contents".as_bytes())
1063            .build()
1064            .await
1065            .unwrap();
1066
1067        let sub_pkg = PackageBuilder::new("sub-pkg")
1068            .add_resource_at("b-blob", "b-blob-contents".as_bytes())
1069            .add_subpackage("subpackage-1", &sub_sub_pkg)
1070            .build()
1071            .await
1072            .unwrap();
1073
1074        let mut expected_subpackage_blobs = HashMap::new();
1075        let (sub_sub_pkg_meta_far, content_blobs) = sub_sub_pkg.contents();
1076        expected_subpackage_blobs
1077            .insert(sub_sub_pkg_meta_far.merkle, sub_sub_pkg_meta_far.contents);
1078        expected_subpackage_blobs
1079            .insert(content_blobs.into_keys().next().unwrap(), b"c-blob-contents".to_vec());
1080
1081        assert_eq!(*sub_pkg.subpackage_blobs().unwrap(), expected_subpackage_blobs);
1082        assert_eq!(
1083            sub_pkg.meta_subpackages().unwrap(),
1084            MetaSubpackages::from_iter([(
1085                fuchsia_url::RelativePackageUrl::parse("subpackage-1").unwrap(),
1086                sub_sub_pkg_meta_far.merkle
1087            )])
1088        );
1089        let (sub_pkg_meta_far, content_blobs) = sub_pkg.contents();
1090        let mut expected_all_blobs = content_blobs
1091            .keys()
1092            .copied()
1093            .chain([sub_pkg_meta_far.merkle])
1094            .chain(expected_subpackage_blobs.keys().copied())
1095            .collect();
1096        assert_eq!(sub_pkg.list_blobs(), expected_all_blobs);
1097
1098        // Package with subpackage that is a superpackage.
1099        let pkg = PackageBuilder::new("pkg")
1100            .add_subpackage("subpackage-0", &sub_pkg)
1101            .build()
1102            .await
1103            .unwrap();
1104
1105        expected_subpackage_blobs.insert(sub_pkg_meta_far.merkle, sub_pkg_meta_far.contents);
1106        expected_subpackage_blobs
1107            .insert(content_blobs.into_keys().next().unwrap(), b"b-blob-contents".to_vec());
1108
1109        assert_eq!(*pkg.subpackage_blobs().unwrap(), expected_subpackage_blobs);
1110        assert_eq!(
1111            pkg.meta_subpackages().unwrap(),
1112            MetaSubpackages::from_iter([(
1113                fuchsia_url::RelativePackageUrl::parse("subpackage-0").unwrap(),
1114                sub_pkg_meta_far.merkle
1115            )])
1116        );
1117        expected_all_blobs.insert(*pkg.hash());
1118        assert_eq!(pkg.list_blobs(), expected_all_blobs);
1119    }
1120
1121    #[fuchsia::test]
1122    async fn test_add_subpackage_by_hash() {
1123        let pkg = PackageBuilder::new("pkg")
1124            .add_subpackage_by_hash("subpackage-name", Hash::from([0; 32]))
1125            .build()
1126            .await
1127            .unwrap();
1128
1129        assert_eq!(pkg.subpackage_blobs(), None);
1130        assert_eq!(
1131            pkg.meta_subpackages().unwrap(),
1132            MetaSubpackages::from_iter([(
1133                fuchsia_url::RelativePackageUrl::parse("subpackage-name").unwrap(),
1134                Hash::from([0; 32])
1135            )])
1136        );
1137    }
1138}