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, PinnedAbsolutePackageUrl};
14use futures::join;
15use futures::prelude::*;
16use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
17use std::convert::TryInto as _;
18use std::fs::{self, File};
19use std::io::{self, Read};
20use std::path::{Path, PathBuf};
21use tempfile::TempDir;
22use version_history::AbiRevision;
23use walkdir::WalkDir;
24use zx::Status;
25use zx::prelude::*;
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 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.clone());
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 { status, .. })
387                if status == 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_handle(
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 = file.get_backing_memory(fio::VmoFlags::READ).await?.map_err(Status::from_raw);
434
435        let mut expect_empty_blob = false;
436
437        // Attempt to get the backing VMO, which is faster. Fall back to reading over FIDL
438        match result {
439            Ok(vmo) => {
440                let size = vmo.get_content_size().context("unable to get vmo size")?;
441                let mut buf = vec![0u8; size as usize];
442                let () = vmo.read(&mut buf[..], 0).context("unable to read from vmo")?;
443                return Ok(buf);
444            }
445            Err(status) => match status {
446                Status::NOT_SUPPORTED => {}
447                Status::BAD_STATE => {
448                    // may or may not be intended behavior, but the empty blob will not provide a vmo,
449                    // failing with BAD_STATE. Verify in the read path below that the blob is indeed
450                    // zero length if this happens.
451                    expect_empty_blob = true;
452                }
453                status => {
454                    return Err(VerificationError::from(format_err!(
455                        "unexpected error opening file buffer: {:?}",
456                        status
457                    )));
458                }
459            },
460        }
461
462        let mut buf = vec![];
463        loop {
464            let chunk = file
465                .read(fio::MAX_BUF)
466                .await
467                .context("file read to respond")?
468                .map_err(Status::from_raw)
469                .map_err(|status| VerificationError::FileReadError { path: path.into(), status })?;
470
471            if chunk.is_empty() {
472                if expect_empty_blob {
473                    assert_eq!(buf, Vec::<u8>::new());
474                }
475                return Ok(buf);
476            }
477
478            buf.extend(chunk);
479        }
480    };
481
482    let (open, read) = join!(open, read);
483    let close_result = file.close().await;
484    let result = open.and(read)?;
485    // Only check close_result if everything that came before it looks good.
486    let close_result = close_result.context("file close to respond")?;
487    close_result.map_err(|status| {
488        format_err!("unable to close {:?}: {:?}", path, zx::Status::from_raw(status))
489    })?;
490    Ok(result)
491}
492
493/// An error that can occur while verifying the contents of a directory.
494#[derive(Debug)]
495pub enum VerificationError {
496    /// The directory is serving a file that isn't in the package.
497    ExtraFile {
498        /// Path to the extra file.
499        path: String,
500    },
501    /// The directory is not serving a particular file that it should be serving.
502    MissingFile {
503        /// Path to the missing file.
504        path: String,
505    },
506    /// The actual merkle of the file does not match the merkle listed in the meta FAR.
507    DifferentFileData {
508        /// Path to the file.
509        path: String,
510    },
511    /// Read method on file failed.
512    FileReadError {
513        /// Path to the file
514        path: String,
515        /// Read result
516        status: Status,
517    },
518    /// Anything else.
519    Other(Error),
520}
521
522impl<T: Into<Error>> From<T> for VerificationError {
523    fn from(x: T) -> Self {
524        VerificationError::Other(x.into())
525    }
526}
527
528/// A builder to simplify construction of Fuchsia packages.
529pub struct PackageBuilder {
530    name: PackageName,
531    contents: BTreeMap<PathBuf, PackageEntry>,
532
533    has_subpackages: bool,
534    // If None the package has subpackages but this `PackageBuilder` does not have the blobs
535    // needed by those subpackages.
536    subpackage_blobs: Option<HashMap<Hash, Vec<u8>>>,
537
538    builder: fuchsia_pkg::PackageBuilder,
539    _artifacts_tmp: TempDir,
540    artifacts: Utf8PathBuf,
541}
542
543impl PackageBuilder {
544    /// Creates a new `PackageBuilder`.
545    ///
546    /// # Panics
547    ///
548    /// Panics if either:
549    /// * `name` is an invalid package name.
550    /// * Creating a tempdir fails.
551    pub fn new(name: impl Into<String>) -> Self {
552        Self::new_with_abi_revision(
553            name,
554            // Default to ABI revision for API level 7.
555            0xECCEA2F70ACD6FC0.into(),
556        )
557    }
558
559    /// Creates a new `PackageBuilder`, just like `PackageBuilder::new()`, but
560    /// allowing the caller to specify the ABI revision with which to stamp the
561    /// test package.
562    pub fn new_with_abi_revision(name: impl Into<String>, abi_revision: AbiRevision) -> Self {
563        let name = name.into();
564
565        let artifacts_tmp = tempfile::tempdir().expect("create tempdir for package");
566        let artifacts = Utf8Path::from_path(artifacts_tmp.path())
567            .expect("checking packagedir is UTF-8")
568            .to_path_buf();
569
570        fs::create_dir(artifacts.join("contents")).expect("create /packages/contents");
571
572        let mut builder = fuchsia_pkg::PackageBuilder::new(&name, abi_revision);
573        builder.manifest_path(artifacts.join("manifest.json"));
574        builder.repository("fuchsia.com");
575        builder.manifest_blobs_relative_to(fuchsia_pkg::RelativeTo::File);
576
577        Self {
578            builder,
579            name: name.try_into().unwrap(),
580            contents: BTreeMap::new(),
581            has_subpackages: false,
582            subpackage_blobs: Some(HashMap::new()),
583            _artifacts_tmp: artifacts_tmp,
584            artifacts,
585        }
586    }
587
588    /// Create a subdirectory within the package.
589    ///
590    /// # Panics
591    ///
592    /// Panics if the package contains a file entry at `path` or any of its ancestors.
593    pub fn dir(mut self, path: impl Into<PathBuf>) -> PackageDir {
594        let path = path.into();
595        self.make_dirs(&path);
596        PackageDir::new(self, path)
597    }
598
599    /// Adds the provided `contents` to the package at the given `path`.
600    ///
601    /// # Panics
602    ///
603    /// Panics if either:
604    /// * The package already contains a file or directory at `path`.
605    /// * The package contains a file at any of `path`'s ancestors.
606    pub fn add_resource_at(
607        mut self,
608        path: impl Into<PathBuf>,
609        mut contents: impl io::Read,
610    ) -> Self {
611        let path = path.into();
612        let path_str = path.to_str().unwrap();
613        let () = fuchsia_url::validate_resource_path(
614            path.to_str().unwrap_or_else(|| panic!("path must be utf8: {path:?}")),
615        )
616        .unwrap_or_else(|_| panic!("path must be an object relative path expression: {path:?}"));
617
618        let mut data = vec![];
619        contents.read_to_end(&mut data).unwrap();
620
621        if path.starts_with("meta/") {
622            self.builder
623                .add_contents_to_far(path_str, &data, self.artifacts.join("contents"))
624                .expect("adding meta blob to succeed");
625        } else {
626            self.builder
627                .add_contents_as_blob(path_str, &data, self.artifacts.join("contents"))
628                .expect("adding blob to succeed");
629        }
630
631        let replaced = self.contents.insert(path.clone(), PackageEntry::File(data));
632        assert_eq!(None, replaced, "already contains an entry at {path:?}");
633        self
634    }
635
636    fn make_dirs(&mut self, path: &Path) {
637        for ancestor in path.ancestors() {
638            if ancestor == Path::new("") {
639                continue;
640            }
641            assert!(
642                self.contents
643                    .entry(ancestor.to_owned())
644                    .or_insert(PackageEntry::Directory)
645                    .is_dir(),
646                "{ancestor:?} is not a directory"
647            );
648        }
649    }
650
651    /// Adds the provided `subpackage` to the package with name `name`.
652    ///
653    /// # Panics
654    ///
655    /// Panics if either:
656    /// * `name` is not a valid RelativePackageUrl
657    /// * There is already a subpackage called `name`
658    pub fn add_subpackage(
659        mut self,
660        name: impl TryInto<fuchsia_url::RelativePackageUrl>,
661        subpackage: &Package,
662    ) -> Self {
663        let name = name.try_into().map_err(|_| ()).expect("valid RelativePackageUrl");
664        let manifest_path = subpackage.artifacts().join("manifest.json").into();
665
666        self.builder.add_subpackage(&name, *subpackage.hash(), manifest_path).unwrap();
667        self.has_subpackages = true;
668
669        match (&mut self.subpackage_blobs, &subpackage.subpackage_blobs) {
670            (Some(current_blobs), Some(new_blobs)) => {
671                let (meta_far, content_blobs) = subpackage.contents();
672                current_blobs.insert(meta_far.merkle, meta_far.contents);
673                current_blobs.extend(content_blobs);
674                current_blobs.extend(new_blobs.iter().map(|(k, v)| (*k, v.clone())));
675            }
676            (Some(_), None) => self.subpackage_blobs = None,
677            (None, Some(_)) | (None, None) => {}
678        }
679
680        self
681    }
682
683    /// Adds a subpackage with name `name` and hash `hash` to the package.
684    /// Because the blobs of the subpackage are not provided, the `Package` built from this
685    /// `PackageBuilder` will not have the subpackage blobs.
686    ///
687    /// # Panics
688    ///
689    /// Panics if either:
690    /// * `name` is not a valid RelativePackageUrl
691    /// * There is already a subpackage called `name`
692    pub fn add_subpackage_by_hash(
693        mut self,
694        name: impl TryInto<fuchsia_url::RelativePackageUrl>,
695        hash: Hash,
696    ) -> Self {
697        let name = name.try_into().map_err(|_| ()).expect("valid RelativePackageUrl");
698
699        self.builder.add_subpackage(&name, hash, "".into()).unwrap();
700        self.has_subpackages = true;
701        self.subpackage_blobs = None;
702        self
703    }
704
705    /// Builds the package.
706    pub async fn build(self) -> Result<Package, Error> {
707        // self.artifacts contains outputs from package creation (manifest.json/meta.far) as well
708        // as all blobs contained in the package.
709        //
710        // Layout of self.artifacts:
711        // - manifest.json
712        // - meta.far
713        // - contents/
714        // -   meta/
715        // -     non-generated meta.far files
716        // -   file/dir{N}
717
718        let manifest = self.builder.build(&self.artifacts, self.artifacts.join("meta.far"))?;
719        let meta_far_merkle =
720            manifest.blobs().iter().find(|b| b.path == "meta/").context("finding meta/")?.merkle;
721
722        // clean up after ourselves
723        fs::remove_file(self.artifacts.join("meta/fuchsia.abi/abi-revision"))?;
724        fs::remove_dir(self.artifacts.join("meta/fuchsia.abi"))?;
725        if self.has_subpackages {
726            fs::remove_file(self.artifacts.join("meta/fuchsia.pkg/subpackages"))?;
727            fs::remove_dir(self.artifacts.join("meta/fuchsia.pkg"))?;
728        }
729        fs::remove_file(self.artifacts.join("meta/package"))?;
730        fs::remove_dir(self.artifacts.join("meta"))?;
731
732        Ok(Package {
733            name: self.name,
734            meta_far_merkle,
735            _artifacts_tmp: self._artifacts_tmp,
736            artifacts: self.artifacts,
737            subpackage_blobs: self.subpackage_blobs,
738        })
739    }
740}
741
742/// A subdirectory of a package being built.
743pub struct PackageDir {
744    pkg: PackageBuilder,
745    path: PathBuf,
746}
747
748impl PackageDir {
749    fn new(pkg: PackageBuilder, path: impl Into<PathBuf>) -> Self {
750        Self { pkg, path: path.into() }
751    }
752
753    /// Adds the provided `contents` to the package at the given `path`, relative to this
754    /// `PackageDir`.
755    ///
756    /// # Panics
757    /// If the package already contains a resource at `path`, relative to this `PackageDir`.
758    pub fn add_resource_at(mut self, path: impl AsRef<Path>, contents: impl io::Read) -> Self {
759        self.pkg = self.pkg.add_resource_at(self.path.join(path.as_ref()), contents);
760        self
761    }
762
763    /// Finish adding resources to this directory, returning the modified [`PackageBuilder`].
764    pub fn finish(self) -> PackageBuilder {
765        self.pkg
766    }
767}
768
769#[cfg(test)]
770mod tests {
771    use super::*;
772    use assert_matches::assert_matches;
773    use fuchsia_pkg::MetaPackage;
774
775    #[test]
776    #[should_panic(expected = "adding blob to succeed")]
777    fn test_panics_file_with_existing_parent_as_file() {
778        let _: Result<(), Error> = {
779            PackageBuilder::new("test")
780                .add_resource_at("data", "data contents".as_bytes())
781                .add_resource_at("data/foo", "data/foo contents".as_bytes());
782            Ok(())
783        };
784    }
785
786    #[test]
787    #[should_panic(expected = r#""data" is not a directory"#)]
788    fn test_panics_dir_with_existing_file() {
789        let _: Result<(), Error> = {
790            PackageBuilder::new("test")
791                .add_resource_at("data", "data contents".as_bytes())
792                .dir("data");
793            Ok(())
794        };
795    }
796
797    #[test]
798    #[should_panic(expected = r#""data" is not a directory"#)]
799    fn test_panics_nested_dir_with_existing_file() {
800        let _: Result<(), Error> = {
801            PackageBuilder::new("test")
802                .add_resource_at("data", "data contents".as_bytes())
803                .dir("data/foo");
804            Ok(())
805        };
806    }
807
808    #[test]
809    #[should_panic(expected = "adding blob to succeed")]
810    fn test_panics_file_with_existing_dir() {
811        let _: Result<(), Error> = {
812            PackageBuilder::new("test")
813                .dir("data")
814                .add_resource_at("foo", "data/foo contents".as_bytes())
815                .finish()
816                .add_resource_at("data", "data contents".as_bytes());
817            Ok(())
818        };
819    }
820
821    #[fuchsia_async::run_singlethreaded(test)]
822    async fn test_basic() -> Result<(), Error> {
823        let pkg = PackageBuilder::new("rolldice")
824            .dir("bin")
825            .add_resource_at("rolldice", "asldkfjaslkdfjalskdjfalskdf".as_bytes())
826            .finish()
827            .build()
828            .await?;
829
830        assert_eq!(
831            pkg.meta_far_merkle,
832            "de210ba39b8f597cc1986c37b369c990707649f63bb8fa23b244a38274018b78".parse()?
833        );
834        assert_eq!(pkg.meta_far_merkle, fuchsia_merkle::root_from_reader(pkg.meta_far()?)?);
835        assert_eq!(
836            pkg.list_blobs(),
837            BTreeSet::from([
838                "de210ba39b8f597cc1986c37b369c990707649f63bb8fa23b244a38274018b78".parse()?,
839                "b5b34f6234631edc7ccaa25533e2050e5d597a7331c8974306b617a3682a3197".parse()?
840            ])
841        );
842
843        Ok(())
844    }
845
846    #[fuchsia_async::run_singlethreaded(test)]
847    async fn test_content_blob_files() -> Result<(), Error> {
848        let pkg = PackageBuilder::new("rolldice")
849            .dir("bin")
850            .add_resource_at("rolldice", "asldkfjaslkdfjalskdjfalskdf".as_bytes())
851            .add_resource_at("rolldice2", "asldkfjaslkdfjalskdjfalskdf".as_bytes())
852            .finish()
853            .build()
854            .await?;
855
856        let mut iter = pkg.content_blob_files();
857        // 2 identical entries
858        for _ in 0..2 {
859            let BlobFile { merkle, mut file } = iter.next().unwrap();
860            assert_eq!(
861                merkle,
862                "b5b34f6234631edc7ccaa25533e2050e5d597a7331c8974306b617a3682a3197".parse().unwrap()
863            );
864            let mut contents = vec![];
865            file.read_to_end(&mut contents).unwrap();
866            assert_eq!(contents, b"asldkfjaslkdfjalskdjfalskdf")
867        }
868        assert_eq!(iter.next().map(|b| b.merkle), None);
869
870        Ok(())
871    }
872
873    #[fuchsia_async::run_singlethreaded(test)]
874    async fn test_dir_semantics() -> Result<(), Error> {
875        let with_dir = PackageBuilder::new("data-file")
876            .dir("data")
877            .add_resource_at("file", "contents".as_bytes())
878            .finish()
879            .build()
880            .await?;
881
882        let with_direct = PackageBuilder::new("data-file")
883            .add_resource_at("data/file", "contents".as_bytes())
884            .build()
885            .await?;
886
887        assert_eq!(with_dir.hash(), with_direct.hash());
888
889        Ok(())
890    }
891
892    /// Creates a clone of the contents of /pkg in a tempdir so that tests can manipulate its
893    /// contents.
894    fn make_this_package_dir() -> Result<tempfile::TempDir, Error> {
895        let dir = tempfile::tempdir()?;
896
897        let this_package_root = Path::new("/pkg");
898
899        for entry in WalkDir::new(this_package_root) {
900            let entry = entry?;
901            let path = entry.path();
902
903            let relative_path = path.strip_prefix(this_package_root).unwrap();
904            let rebased_path = dir.path().join(relative_path);
905
906            if entry.file_type().is_dir() {
907                fs::create_dir_all(rebased_path)?;
908            } else if entry.file_type().is_file() {
909                fs::copy(path, rebased_path)?;
910            }
911        }
912
913        Ok(dir)
914    }
915
916    #[fuchsia_async::run_singlethreaded(test)]
917    async fn test_from_dir() {
918        let abi_revision = AbiRevision::from_u64(0x5836508c2defac54); // Random value.
919
920        let root = {
921            let dir = tempfile::tempdir().unwrap();
922
923            fs::create_dir(dir.path().join("meta")).unwrap();
924            fs::create_dir(dir.path().join("data")).unwrap();
925
926            MetaPackage::from_name_and_variant_zero("asdf".parse().unwrap())
927                .serialize(File::create(dir.path().join("meta/package")).unwrap())
928                .unwrap();
929
930            fs::create_dir(dir.path().join("meta/fuchsia.abi")).unwrap();
931            fs::write(dir.path().join("meta/fuchsia.abi/abi-revision"), abi_revision.as_bytes())
932                .unwrap();
933
934            fs::write(dir.path().join("data/hello"), "world").unwrap();
935
936            dir
937        };
938
939        let from_dir = Package::from_dir(root.path()).await.unwrap();
940
941        let pkg = PackageBuilder::new_with_abi_revision("asdf", abi_revision)
942            .add_resource_at("data/hello", "world".as_bytes())
943            .build()
944            .await
945            .unwrap();
946
947        assert_eq!(from_dir.meta_far_merkle, pkg.meta_far_merkle);
948    }
949
950    #[fuchsia_async::run_singlethreaded(test)]
951    async fn test_identity() -> Result<(), Error> {
952        let pkg = Package::identity().await.unwrap();
953
954        assert_eq!(pkg.meta_far_merkle, fuchsia_merkle::root_from_reader(pkg.meta_far()?)?);
955
956        // Verify the generated package's merkle root is the same as this test package's merkle root.
957        assert_eq!(pkg.meta_far_merkle, fs::read_to_string("/pkg/meta")?.parse()?);
958
959        let this_pkg_dir = fuchsia_fs::directory::open_in_namespace("/pkg", fio::PERM_READABLE)?;
960        pkg.verify_contents(&this_pkg_dir).await.expect("contents to be equivalent");
961
962        let pkg_dir = make_this_package_dir()?;
963
964        let this_pkg_dir = fuchsia_fs::directory::open_in_namespace(
965            pkg_dir.path().to_str().unwrap(),
966            fio::PERM_READABLE,
967        )?;
968
969        assert_matches!(pkg.verify_contents(&this_pkg_dir).await, Ok(()));
970
971        Ok(())
972    }
973
974    #[fuchsia_async::run_singlethreaded(test)]
975    async fn test_verify_contents_rejects_extra_blob() -> Result<(), Error> {
976        let pkg = Package::identity().await?;
977        let pkg_dir = make_this_package_dir()?;
978
979        fs::write(pkg_dir.path().join("unexpected"), "unexpected file".as_bytes())?;
980
981        let pkg_dir_proxy = fuchsia_fs::directory::open_in_namespace(
982            pkg_dir.path().to_str().unwrap(),
983            fio::PERM_READABLE,
984        )?;
985
986        assert_matches!(
987            pkg.verify_contents(&pkg_dir_proxy).await,
988            Err(VerificationError::ExtraFile{ref path}) if path == "unexpected");
989
990        Ok(())
991    }
992
993    #[fuchsia_async::run_singlethreaded(test)]
994    async fn test_verify_contents_rejects_extra_meta_file() -> Result<(), Error> {
995        let pkg = Package::identity().await?;
996        let pkg_dir = make_this_package_dir()?;
997
998        fs::write(pkg_dir.path().join("meta/unexpected"), "unexpected file".as_bytes())?;
999
1000        let pkg_dir_proxy = fuchsia_fs::directory::open_in_namespace(
1001            pkg_dir.path().to_str().unwrap(),
1002            fio::PERM_READABLE,
1003        )?;
1004
1005        assert_matches!(
1006            pkg.verify_contents(&pkg_dir_proxy).await,
1007            Err(VerificationError::ExtraFile{ref path}) if path == "meta/unexpected");
1008
1009        Ok(())
1010    }
1011
1012    #[fuchsia_async::run_singlethreaded(test)]
1013    async fn test_verify_contents_rejects_missing_blob() -> Result<(), Error> {
1014        let pkg = Package::identity().await?;
1015        let pkg_dir = make_this_package_dir()?;
1016
1017        fs::remove_file(pkg_dir.path().join("bin/fuchsia_pkg_testing_lib_test"))?;
1018
1019        let pkg_dir_proxy = fuchsia_fs::directory::open_in_namespace(
1020            pkg_dir.path().to_str().unwrap(),
1021            fio::PERM_READABLE,
1022        )?;
1023
1024        assert_matches!(
1025            pkg.verify_contents(&pkg_dir_proxy).await,
1026            Err(VerificationError::MissingFile{ref path}) if path == "bin/fuchsia_pkg_testing_lib_test");
1027
1028        Ok(())
1029    }
1030
1031    #[fuchsia_async::run_singlethreaded(test)]
1032    async fn test_verify_contents_rejects_different_contents() -> Result<(), Error> {
1033        let pkg = Package::identity().await?;
1034        let pkg_dir = make_this_package_dir()?;
1035
1036        fs::write(pkg_dir.path().join("bin/fuchsia_pkg_testing_lib_test"), "broken".as_bytes())?;
1037
1038        let pkg_dir_proxy = fuchsia_fs::directory::open_in_namespace(
1039            pkg_dir.path().to_str().unwrap(),
1040            fio::PERM_READABLE,
1041        )?;
1042
1043        assert_matches!(
1044            pkg.verify_contents(&pkg_dir_proxy).await,
1045            Err(VerificationError::DifferentFileData{ref path}) if path == "bin/fuchsia_pkg_testing_lib_test");
1046
1047        Ok(())
1048    }
1049
1050    #[fuchsia_async::run_singlethreaded(test)]
1051    async fn test_meta_subpackages_with_no_subpackages() {
1052        let pkg = PackageBuilder::new("pkg").build().await.unwrap();
1053
1054        assert!(pkg.meta_subpackages().unwrap().subpackages().is_empty());
1055    }
1056
1057    #[fuchsia_async::run_singlethreaded(test)]
1058    async fn test_add_subpackage() {
1059        // Package with subpackage.
1060        let sub_sub_pkg = PackageBuilder::new("sub-sub-pkg")
1061            .add_resource_at("c-blob", "c-blob-contents".as_bytes())
1062            .build()
1063            .await
1064            .unwrap();
1065
1066        let sub_pkg = PackageBuilder::new("sub-pkg")
1067            .add_resource_at("b-blob", "b-blob-contents".as_bytes())
1068            .add_subpackage("subpackage-1", &sub_sub_pkg)
1069            .build()
1070            .await
1071            .unwrap();
1072
1073        let mut expected_subpackage_blobs = HashMap::new();
1074        let (sub_sub_pkg_meta_far, content_blobs) = sub_sub_pkg.contents();
1075        expected_subpackage_blobs
1076            .insert(sub_sub_pkg_meta_far.merkle, sub_sub_pkg_meta_far.contents);
1077        expected_subpackage_blobs
1078            .insert(content_blobs.into_keys().next().unwrap(), b"c-blob-contents".to_vec());
1079
1080        assert_eq!(*sub_pkg.subpackage_blobs().unwrap(), expected_subpackage_blobs);
1081        assert_eq!(
1082            sub_pkg.meta_subpackages().unwrap(),
1083            MetaSubpackages::from_iter([(
1084                fuchsia_url::RelativePackageUrl::parse("subpackage-1").unwrap(),
1085                sub_sub_pkg_meta_far.merkle
1086            )])
1087        );
1088        let (sub_pkg_meta_far, content_blobs) = sub_pkg.contents();
1089        let mut expected_all_blobs = content_blobs
1090            .keys()
1091            .copied()
1092            .chain([sub_pkg_meta_far.merkle])
1093            .chain(expected_subpackage_blobs.keys().copied())
1094            .collect();
1095        assert_eq!(sub_pkg.list_blobs(), expected_all_blobs);
1096
1097        // Package with subpackage that is a superpackage.
1098        let pkg = PackageBuilder::new("pkg")
1099            .add_subpackage("subpackage-0", &sub_pkg)
1100            .build()
1101            .await
1102            .unwrap();
1103
1104        expected_subpackage_blobs.insert(sub_pkg_meta_far.merkle, sub_pkg_meta_far.contents);
1105        expected_subpackage_blobs
1106            .insert(content_blobs.into_keys().next().unwrap(), b"b-blob-contents".to_vec());
1107
1108        assert_eq!(*pkg.subpackage_blobs().unwrap(), expected_subpackage_blobs);
1109        assert_eq!(
1110            pkg.meta_subpackages().unwrap(),
1111            MetaSubpackages::from_iter([(
1112                fuchsia_url::RelativePackageUrl::parse("subpackage-0").unwrap(),
1113                sub_pkg_meta_far.merkle
1114            )])
1115        );
1116        expected_all_blobs.insert(*pkg.hash());
1117        assert_eq!(pkg.list_blobs(), expected_all_blobs);
1118    }
1119
1120    #[fuchsia_async::run_singlethreaded(test)]
1121    async fn test_add_subpackage_by_hash() {
1122        let pkg = PackageBuilder::new("pkg")
1123            .add_subpackage_by_hash("subpackage-name", Hash::from([0; 32]))
1124            .build()
1125            .await
1126            .unwrap();
1127
1128        assert_eq!(pkg.subpackage_blobs(), None);
1129        assert_eq!(
1130            pkg.meta_subpackages().unwrap(),
1131            MetaSubpackages::from_iter([(
1132                fuchsia_url::RelativePackageUrl::parse("subpackage-name").unwrap(),
1133                Hash::from([0; 32])
1134            )])
1135        );
1136    }
1137}