1use 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
33pub(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 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 meta_far_file.seek(SeekFrom::Start(0))?;
133 let meta_far_merkle = MerkleTree::from_reader(&meta_far_file)?.root();
134
135 let meta_far_size = meta_far_file.as_file().metadata()?.len();
137
138 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 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 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}