1use 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
50pub(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 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 meta_far_file.seek(SeekFrom::Start(0))?;
151 let meta_far_merkle = fuchsia_merkle::root_from_reader(&meta_far_file)?;
152
153 let meta_far_size = meta_far_file.as_file().metadata()?.len();
155
156 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 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 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}