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