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