fuchsia_pkg/
build.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
5use crate::errors::BuildError;
6use crate::{MetaContents, MetaPackage, MetaPackageError, PackageBuildManifest, PackageManifest};
7use anyhow::Result;
8use fuchsia_merkle::Hash;
9use fuchsia_url::RelativePackageUrl;
10use std::collections::{BTreeMap, btree_map};
11use std::io::{Seek, SeekFrom};
12use std::path::PathBuf;
13use std::{fs, io};
14use tempfile::NamedTempFile;
15use version_history::AbiRevision;
16
17#[derive(Clone, Debug, PartialEq, Eq)]
18pub(crate) struct BlobEntry {
19    pub(crate) source_path: PathBuf,
20    pub(crate) hash: Hash,
21    pub(crate) size: u64,
22}
23
24#[derive(Clone, Debug, PartialEq, Eq)]
25pub(crate) struct SubpackageEntry {
26    pub name: RelativePackageUrl,
27    pub merkle: Hash,
28    pub package_manifest_path: PathBuf,
29}
30
31pub(crate) fn build(
32    creation_manifest: &PackageBuildManifest,
33    meta_far_path: impl Into<PathBuf>,
34    published_name: impl AsRef<str>,
35    subpackages: Vec<SubpackageEntry>,
36    repository: Option<String>,
37    abi_revision: AbiRevision,
38) -> Result<PackageManifest, BuildError> {
39    build_with_file_system(
40        creation_manifest,
41        meta_far_path,
42        published_name,
43        subpackages,
44        repository,
45        abi_revision,
46        &ActualFileSystem {},
47    )
48}
49
50// Used to mock out native filesystem for testing
51pub(crate) trait FileSystem<'a> {
52    type File: io::Read;
53    fn open(&'a self, path: &str) -> Result<Self::File, io::Error>;
54    fn len(&self, path: &str) -> Result<u64, io::Error>;
55    fn read(&self, path: &str) -> Result<Vec<u8>, io::Error>;
56}
57
58struct ActualFileSystem;
59
60impl FileSystem<'_> for ActualFileSystem {
61    type File = std::fs::File;
62    fn open(&self, path: &str) -> Result<Self::File, io::Error> {
63        fs::File::open(path)
64    }
65    fn len(&self, path: &str) -> Result<u64, io::Error> {
66        Ok(fs::metadata(path)?.len())
67    }
68    fn read(&self, path: &str) -> Result<Vec<u8>, io::Error> {
69        fs::read(path)
70    }
71}
72
73pub(crate) fn build_with_file_system<'a>(
74    creation_manifest: &PackageBuildManifest,
75    meta_far_path: impl Into<PathBuf>,
76    published_name: impl AsRef<str>,
77    subpackages: Vec<SubpackageEntry>,
78    repository: Option<String>,
79    abi_revision: AbiRevision,
80    file_system: &'a impl FileSystem<'a>,
81) -> Result<PackageManifest, BuildError> {
82    let meta_far_path = meta_far_path.into();
83    let published_name = published_name.as_ref();
84
85    if creation_manifest.far_contents().get("meta/package").is_none() {
86        return Err(BuildError::MetaPackage(MetaPackageError::MetaPackageMissing));
87    };
88
89    let meta_package = MetaPackage::from_name_and_variant_zero(
90        published_name.parse().map_err(BuildError::PackageName)?,
91    );
92    let mut blobs: BTreeMap<String, BlobEntry> = BTreeMap::new();
93
94    let external_content_infos =
95        get_external_content_infos(creation_manifest.external_contents(), file_system)?;
96
97    for (path, info) in external_content_infos.iter() {
98        blobs.insert(
99            path.to_string(),
100            BlobEntry {
101                source_path: PathBuf::from(info.source_path),
102                size: info.size,
103                hash: info.hash,
104            },
105        );
106    }
107
108    let meta_contents = MetaContents::from_map(
109        external_content_infos.iter().map(|(path, info)| (path.clone(), info.hash)).collect(),
110    )?;
111
112    let mut meta_contents_bytes = Vec::new();
113    meta_contents.serialize(&mut meta_contents_bytes)?;
114
115    let mut far_contents: BTreeMap<&str, Vec<u8>> = BTreeMap::new();
116    for (resource_path, source_path) in creation_manifest.far_contents() {
117        far_contents.insert(
118            resource_path,
119            file_system.read(source_path).map_err(|e| (e, source_path.into()))?,
120        );
121    }
122
123    let insert_generated_file =
124        |resource_path: &'static str, content, far_contents: &mut BTreeMap<_, _>| match far_contents
125            .entry(resource_path)
126        {
127            btree_map::Entry::Vacant(entry) => {
128                entry.insert(content);
129                Ok(())
130            }
131            btree_map::Entry::Occupied(_) => Err(BuildError::ConflictingResource {
132                conflicting_resource_path: resource_path.to_string(),
133            }),
134        };
135    insert_generated_file("meta/contents", meta_contents_bytes, &mut far_contents)?;
136    let mut meta_entries: BTreeMap<&str, (u64, Box<dyn io::Read>)> = BTreeMap::new();
137    for (resource_path, content) in &far_contents {
138        meta_entries.insert(resource_path, (content.len() as u64, Box::new(content.as_slice())));
139    }
140
141    // Write the meta-far to a temporary file.
142    let mut meta_far_file = if let Some(parent) = meta_far_path.parent() {
143        NamedTempFile::new_in(parent)?
144    } else {
145        NamedTempFile::new()?
146    };
147    fuchsia_archive::write(&meta_far_file, meta_entries)?;
148
149    // Calculate the merkle of the meta-far.
150    meta_far_file.seek(SeekFrom::Start(0))?;
151    let meta_far_merkle = fuchsia_merkle::root_from_reader(&meta_far_file)?;
152
153    // Calculate the size of the meta-far.
154    let meta_far_size = meta_far_file.as_file().metadata()?.len();
155
156    // Replace the existing meta-far with the new file.
157    if let Err(err) = meta_far_file.persist(&meta_far_path) {
158        return Err(BuildError::IoErrorWithPath { cause: err.error, path: meta_far_path });
159    }
160
161    // Add the meta-far as an entry to the package.
162    blobs.insert(
163        "meta/".to_string(),
164        BlobEntry { source_path: meta_far_path, size: meta_far_size, hash: meta_far_merkle },
165    );
166
167    let package_manifest =
168        PackageManifest::from_parts(meta_package, repository, blobs, subpackages, abi_revision)?;
169    Ok(package_manifest)
170}
171
172struct ExternalContentInfo<'a> {
173    source_path: &'a str,
174    size: u64,
175    hash: Hash,
176}
177
178fn get_external_content_infos<'a, 'b>(
179    external_contents: &'a BTreeMap<String, String>,
180    file_system: &'b impl FileSystem<'b>,
181) -> Result<BTreeMap<String, ExternalContentInfo<'a>>, BuildError> {
182    external_contents
183        .iter()
184        .map(|(resource_path, source_path)| -> Result<(String, ExternalContentInfo<'_>), BuildError> {
185            let file = file_system.open(source_path)
186                .map_err(|e| (e, source_path.into()))?;
187            Ok((
188                resource_path.clone(),
189                ExternalContentInfo {
190                    source_path,
191                    size: file_system.len(source_path)?,
192                    hash: fuchsia_merkle::root_from_reader(file)?,
193                },
194            ))
195        })
196        .collect()
197}
198
199#[cfg(test)]
200mod test_build_with_file_system {
201    use super::*;
202    use crate::MetaPackage;
203    use crate::test::*;
204    use assert_matches::assert_matches;
205    use maplit::{btreemap, hashmap};
206    use proptest::prelude::*;
207    use rand::SeedableRng as _;
208    use std::collections::{HashMap, HashSet};
209    use std::fs::File;
210    use tempfile::TempDir;
211    use version_history::AbiRevision;
212
213    const GENERATED_FAR_CONTENTS: [&str; 2] = ["meta/contents", "meta/package"];
214
215    const FAKE_ABI_REVISION: AbiRevision = AbiRevision::from_u64(0xabcdef);
216
217    struct FakeFileSystem {
218        content_map: HashMap<String, Vec<u8>>,
219    }
220
221    impl FakeFileSystem {
222        fn from_creation_manifest_with_random_contents(
223            creation_manifest: &PackageBuildManifest,
224            rng: &mut impl rand::Rng,
225        ) -> FakeFileSystem {
226            let mut content_map = HashMap::new();
227            for (resource_path, host_path) in
228                creation_manifest.far_contents().iter().chain(creation_manifest.external_contents())
229            {
230                if *resource_path == *"meta/package" {
231                    let mut v = vec![];
232                    let meta_package =
233                        MetaPackage::from_name_and_variant_zero("my-package-name".parse().unwrap());
234                    meta_package.serialize(&mut v).unwrap();
235                    content_map.insert(host_path.to_string(), v);
236                } else {
237                    let file_size = rng.random_range(0..6000);
238                    content_map.insert(
239                        host_path.to_string(),
240                        rng.sample_iter(&rand::distr::StandardUniform).take(file_size).collect(),
241                    );
242                }
243            }
244            Self { content_map }
245        }
246    }
247
248    impl<'a> FileSystem<'a> for FakeFileSystem {
249        type File = &'a [u8];
250        fn open(&'a self, path: &str) -> Result<Self::File, io::Error> {
251            Ok(self.content_map.get(path).unwrap().as_slice())
252        }
253        fn len(&self, path: &str) -> Result<u64, io::Error> {
254            Ok(self.content_map.get(path).unwrap().len() as u64)
255        }
256        fn read(&self, path: &str) -> Result<Vec<u8>, io::Error> {
257            Ok(self.content_map.get(path).unwrap().clone())
258        }
259    }
260
261    #[test]
262    fn test_verify_far_contents_with_fixed_inputs() {
263        let outdir = TempDir::new().unwrap();
264        let meta_far_path = outdir.path().join("meta.far");
265
266        let creation_manifest = PackageBuildManifest::from_external_and_far_contents(
267            btreemap! {
268                "lib/mylib.so".to_string() => "host/mylib.so".to_string()
269            },
270            btreemap! {
271                "meta/my_component.cml".to_string() => "host/my_component.cml".to_string(),
272                "meta/package".to_string() => "host/meta/package".to_string()
273            },
274        )
275        .unwrap();
276        let component_manifest_contents = "my_component.cml contents";
277        let mut v = vec![];
278        let meta_package =
279            MetaPackage::from_name_and_variant_zero("my-package-name".parse().unwrap());
280        meta_package.serialize(&mut v).unwrap();
281        let file_system = FakeFileSystem {
282            content_map: hashmap! {
283                "host/mylib.so".to_string() => "mylib.so contents".as_bytes().to_vec(),
284                "host/my_component.cml".to_string() => component_manifest_contents.as_bytes().to_vec(),
285                "host/meta/package".to_string() => v.clone()
286            },
287        };
288        build_with_file_system(
289            &creation_manifest,
290            &meta_far_path,
291            "published-name",
292            vec![],
293            None,
294            FAKE_ABI_REVISION,
295            &file_system,
296        )
297        .unwrap();
298        let mut reader =
299            fuchsia_archive::Utf8Reader::new(File::open(&meta_far_path).unwrap()).unwrap();
300        let actual_meta_package_bytes = reader.read_file("meta/package").unwrap();
301        let expected_meta_package_bytes = v.as_slice();
302        assert_eq!(actual_meta_package_bytes.as_slice(), expected_meta_package_bytes);
303        let actual_meta_contents_bytes = reader.read_file("meta/contents").unwrap();
304        let expected_meta_contents_bytes =
305            b"lib/mylib.so=4a886105646222c10428e5793868b13f536752d4b87e6497cdf9caed37e67410\n";
306        assert_eq!(actual_meta_contents_bytes.as_slice(), &expected_meta_contents_bytes[..]);
307        let actual_meta_component_bytes = reader.read_file("meta/my_component.cml").unwrap();
308        assert_eq!(actual_meta_component_bytes.as_slice(), component_manifest_contents.as_bytes());
309    }
310
311    #[test]
312    fn test_reject_conflict_with_generated_file() {
313        let outdir = TempDir::new().unwrap();
314        let meta_far_path = outdir.path().join("meta.far");
315
316        let creation_manifest = PackageBuildManifest::from_external_and_far_contents(
317            BTreeMap::new(),
318            btreemap! {
319                "meta/contents".to_string() => "some-host-path".to_string(),
320                "meta/package".to_string() => "host/meta/package".to_string()
321            },
322        )
323        .unwrap();
324        let mut v = vec![];
325        let meta_package =
326            MetaPackage::from_name_and_variant_zero("my-package-name".parse().unwrap());
327        meta_package.serialize(&mut v).unwrap();
328        let file_system = FakeFileSystem {
329            content_map: hashmap! {
330                "some-host-path".to_string() => Vec::new(),
331                "host/meta/package".to_string() => v
332            },
333        };
334        let result = build_with_file_system(
335            &creation_manifest,
336            meta_far_path,
337            "published-name",
338            vec![],
339            None,
340            FAKE_ABI_REVISION,
341            &file_system,
342        );
343        assert_matches!(
344            result,
345            Err(BuildError::ConflictingResource {
346                conflicting_resource_path: path
347            }) if path == *"meta/contents"
348        );
349    }
350    proptest! {
351        #![proptest_config(ProptestConfig{
352            failure_persistence: None,
353            ..Default::default()
354        })]
355
356        #[test]
357        fn test_meta_far_directory_names_are_exactly_generated_files_and_creation_manifest_far_contents(
358            creation_manifest in random_creation_manifest(),
359            seed: u64)
360        {
361            let outdir = TempDir::new().unwrap();
362            let meta_far_path = outdir.path().join("meta.far");
363
364            let mut private_key_bytes = [0u8; 32];
365            let mut prng = rand::rngs::StdRng::seed_from_u64(seed);
366            prng.fill(&mut private_key_bytes);
367            let file_system = FakeFileSystem::from_creation_manifest_with_random_contents(
368                &creation_manifest, &mut prng);
369            build_with_file_system(
370                &creation_manifest,
371                &meta_far_path,
372                "published-name",
373                vec![],
374                None,
375                FAKE_ABI_REVISION,
376                &file_system,
377            )
378                .unwrap();
379            let reader =
380                fuchsia_archive::Utf8Reader::new(File::open(&meta_far_path).unwrap()).unwrap();
381            let expected_far_directory_names = {
382                let mut map: HashSet<&str> = HashSet::new();
383                for path in GENERATED_FAR_CONTENTS.iter() {
384                    map.insert(*path);
385                }
386                for (path, _) in creation_manifest.far_contents().iter() {
387                    map.insert(path);
388                }
389                map
390            };
391            let actual_far_directory_names = reader.list().map(|e| e.path()).collect();
392            prop_assert_eq!(expected_far_directory_names, actual_far_directory_names);
393        }
394
395        #[test]
396        fn test_meta_far_contains_creation_manifest_far_contents(
397            creation_manifest in random_creation_manifest(),
398            seed: u64)
399        {
400            let outdir = TempDir::new().unwrap();
401            let meta_far_path = outdir.path().join("meta.far");
402
403            let mut private_key_bytes = [0u8; 32];
404            let mut prng = rand::rngs::StdRng::seed_from_u64(seed);
405            prng.fill(&mut private_key_bytes);
406            let file_system = FakeFileSystem::from_creation_manifest_with_random_contents(
407                &creation_manifest, &mut prng);
408            build_with_file_system(
409                &creation_manifest,
410                &meta_far_path,
411                "published-name",
412                vec![],
413                None,
414                FAKE_ABI_REVISION,
415                &file_system,
416            )
417                .unwrap();
418            let mut reader =
419                fuchsia_archive::Utf8Reader::new(File::open(&meta_far_path).unwrap()).unwrap();
420            for (resource_path, host_path) in creation_manifest.far_contents().iter() {
421                let expected_contents = file_system.content_map.get(host_path).unwrap();
422                let actual_contents = reader.read_file(resource_path).unwrap();
423                prop_assert_eq!(expected_contents, &actual_contents);
424            }
425        }
426
427        #[test]
428        fn test_meta_far_meta_contents_lists_creation_manifest_external_contents(
429            creation_manifest in random_creation_manifest(),
430            seed: u64)
431        {
432            let outdir = TempDir::new().unwrap();
433            let meta_far_path = outdir.path().join("meta.far");
434
435            let mut private_key_bytes = [0u8; 32];
436            let mut prng = rand::rngs::StdRng::seed_from_u64(seed);
437            prng.fill(&mut private_key_bytes);
438            let file_system = FakeFileSystem::from_creation_manifest_with_random_contents(
439                &creation_manifest, &mut prng);
440            build_with_file_system(
441                &creation_manifest,
442                &meta_far_path,
443                "published-name",
444                vec![],
445                None,
446                FAKE_ABI_REVISION,
447                &file_system,
448            )
449                .unwrap();
450            let mut reader =
451                fuchsia_archive::Utf8Reader::new(File::open(&meta_far_path).unwrap()).unwrap();
452            let meta_contents =
453                MetaContents::deserialize(
454                    reader.read_file("meta/contents").unwrap().as_slice())
455                .unwrap();
456            let actual_external_contents: HashSet<&str> = meta_contents
457                .contents()
458                .keys()
459                .map(|s| s.as_str())
460                .collect();
461            let expected_external_contents: HashSet<&str> =
462                HashSet::from_iter(
463                    creation_manifest
464                        .external_contents()
465                        .keys()
466                        .map(|s| s.as_str()));
467            prop_assert_eq!(expected_external_contents, actual_external_contents);
468        }
469    }
470}
471
472#[cfg(test)]
473mod test_build {
474    use super::*;
475    use crate::MetaPackage;
476    use crate::test::*;
477    use proptest::prelude::*;
478    use rand::SeedableRng as _;
479    use std::io::Write;
480    use tempfile::TempDir;
481
482    const FAKE_ABI_REVISION: AbiRevision = AbiRevision::from_u64(0xabcdef);
483
484    // Creates a temporary directory, then for each host path in the `PackageBuildManifest`'s
485    // external contents and far contents maps creates a file in the temporary directory with path
486    // "${TEMP_DIR}/${HOST_PATH}" and random size and contents. Returns a new `PackageBuildManifest`
487    // with updated host paths and the `TempDir`.
488    fn populate_filesystem_from_creation_manifest(
489        creation_manifest: PackageBuildManifest,
490        rng: &mut impl rand::Rng,
491    ) -> (PackageBuildManifest, TempDir) {
492        let temp_dir = TempDir::new().unwrap();
493        let temp_dir_path = temp_dir.path();
494        fn populate_filesystem_and_make_new_map(
495            path_prefix: &std::path::Path,
496            resource_to_host_path: &BTreeMap<String, String>,
497            rng: &mut impl rand::Rng,
498        ) -> BTreeMap<String, String> {
499            let mut new_map = BTreeMap::new();
500            for (resource_path, host_path) in resource_to_host_path {
501                let new_host_path = PathBuf::from(path_prefix.join(host_path).to_str().unwrap());
502                fs::create_dir_all(new_host_path.parent().unwrap()).unwrap();
503                let mut f = fs::File::create(&new_host_path).unwrap();
504                if *resource_path == *"meta/package" {
505                    let meta_package =
506                        MetaPackage::from_name_and_variant_zero("my-package-name".parse().unwrap());
507                    meta_package.serialize(f).unwrap();
508                } else {
509                    let file_size = rng.random_range(0..6000);
510                    f.write_all(
511                        rng.sample_iter(&rand::distr::StandardUniform)
512                            .take(file_size)
513                            .collect::<Vec<u8>>()
514                            .as_slice(),
515                    )
516                    .unwrap();
517                }
518                new_map.insert(
519                    resource_path.to_string(),
520                    new_host_path.into_os_string().into_string().unwrap(),
521                );
522            }
523            new_map
524        }
525        let new_far_contents = populate_filesystem_and_make_new_map(
526            temp_dir_path,
527            creation_manifest.far_contents(),
528            rng,
529        );
530        let new_external_contents = populate_filesystem_and_make_new_map(
531            temp_dir_path,
532            creation_manifest.external_contents(),
533            rng,
534        );
535        let new_creation_manifest = PackageBuildManifest::from_external_and_far_contents(
536            new_external_contents,
537            new_far_contents,
538        )
539        .unwrap();
540        (new_creation_manifest, temp_dir)
541    }
542
543    proptest! {
544        #![proptest_config(ProptestConfig{
545            failure_persistence: None,
546            ..Default::default()
547        })]
548
549        #[test]
550        fn test_meta_far_contains_creation_manifest_far_contents(
551            creation_manifest in random_creation_manifest(),
552            seed: u64)
553        {
554            let outdir = TempDir::new().unwrap();
555            let meta_far_path = outdir.path().join("meta.far");
556
557            let mut prng = rand::rngs::StdRng::seed_from_u64(seed);
558            let (creation_manifest, _temp_dir) = populate_filesystem_from_creation_manifest(creation_manifest, &mut prng);
559            let mut private_key_bytes = [0u8; 32];
560            prng.fill(&mut private_key_bytes);
561            build(
562                &creation_manifest,
563                &meta_far_path,
564                "published-name",
565                vec![],
566                None,
567                FAKE_ABI_REVISION,
568            )
569                .unwrap();
570            let mut reader =
571                fuchsia_archive::Utf8Reader::new(fs::File::open(&meta_far_path).unwrap()).unwrap();
572            for (resource_path, host_path) in creation_manifest.far_contents().iter() {
573                let expected_contents = std::fs::read(host_path).unwrap();
574                let actual_contents = reader.read_file(resource_path).unwrap();
575                prop_assert_eq!(expected_contents, actual_contents);
576            }
577        }
578    }
579}