1 // Copyright 2021, The Android Open Source Project
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
6 //
7 //     http://www.apache.org/licenses/LICENSE-2.0
8 //
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS,
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 // See the License for the specific language governing permissions and
13 // limitations under the License.
14 
15 //! Payload disk image
16 
17 use crate::debug_config::DebugConfig;
18 use android_system_virtualizationservice::aidl::android::system::virtualizationservice::{
19     DiskImage::DiskImage,
20     Partition::Partition,
21     VirtualMachineAppConfig::DebugLevel::DebugLevel,
22     VirtualMachineAppConfig::{Payload::Payload, VirtualMachineAppConfig},
23     VirtualMachineRawConfig::VirtualMachineRawConfig,
24 };
25 use anyhow::{anyhow, bail, Context, Result};
26 use binder::{wait_for_interface, ParcelFileDescriptor};
27 use log::{info, warn};
28 use microdroid_metadata::{ApexPayload, ApkPayload, Metadata, PayloadConfig, PayloadMetadata};
29 use microdroid_payload_config::{ApexConfig, VmPayloadConfig};
30 use once_cell::sync::OnceCell;
31 use packagemanager_aidl::aidl::android::content::pm::{
32     IPackageManagerNative::IPackageManagerNative, StagedApexInfo::StagedApexInfo,
33 };
34 use regex::Regex;
35 use serde::Deserialize;
36 use serde_xml_rs::from_reader;
37 use std::collections::HashSet;
38 use std::ffi::OsStr;
39 use std::fs::{metadata, File, OpenOptions};
40 use std::path::{Path, PathBuf};
41 use std::process::Command;
42 use std::time::SystemTime;
43 use vmconfig::open_parcel_file;
44 
45 const APEX_INFO_LIST_PATH: &str = "/apex/apex-info-list.xml";
46 
47 const PACKAGE_MANAGER_NATIVE_SERVICE: &str = "package_native";
48 
49 /// Represents the list of APEXes
50 #[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
51 struct ApexInfoList {
52     #[serde(rename = "apex-info")]
53     list: Vec<ApexInfo>,
54 }
55 
56 #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq)]
57 struct ApexInfo {
58     #[serde(rename = "moduleName")]
59     name: String,
60     #[serde(rename = "versionCode")]
61     version: u64,
62     #[serde(rename = "modulePath")]
63     path: PathBuf,
64 
65     #[serde(default)]
66     has_classpath_jar: bool,
67 
68     // The field claims to be milliseconds but is actually seconds.
69     #[serde(rename = "lastUpdateMillis")]
70     last_update_seconds: u64,
71 
72     #[serde(rename = "isFactory")]
73     is_factory: bool,
74 
75     #[serde(rename = "isActive")]
76     is_active: bool,
77 
78     #[serde(rename = "provideSharedApexLibs")]
79     provide_shared_apex_libs: bool,
80 
81     #[serde(rename = "preinstalledModulePath")]
82     preinstalled_path: PathBuf,
83 }
84 
85 impl ApexInfoList {
86     /// Loads ApexInfoList
load() -> Result<&'static ApexInfoList>87     fn load() -> Result<&'static ApexInfoList> {
88         static INSTANCE: OnceCell<ApexInfoList> = OnceCell::new();
89         INSTANCE.get_or_try_init(|| {
90             let apex_info_list = File::open(APEX_INFO_LIST_PATH)
91                 .context(format!("Failed to open {}", APEX_INFO_LIST_PATH))?;
92             let mut apex_info_list: ApexInfoList = from_reader(apex_info_list)
93                 .context(format!("Failed to parse {}", APEX_INFO_LIST_PATH))?;
94 
95             // For active APEXes, we run derive_classpath and parse its output to see if it
96             // contributes to the classpath(s). (This allows us to handle any new classpath env
97             // vars seamlessly.)
98             if !cfg!(early) {
99                 let classpath_vars = run_derive_classpath()?;
100                 let classpath_apexes = find_apex_names_in_classpath(&classpath_vars)?;
101 
102                 for apex_info in apex_info_list.list.iter_mut() {
103                     apex_info.has_classpath_jar = classpath_apexes.contains(&apex_info.name);
104                 }
105             }
106 
107             Ok(apex_info_list)
108         })
109     }
110 
111     // Override apex info with the staged one
override_staged_apex(&mut self, staged_apex_info: &StagedApexInfo) -> Result<()>112     fn override_staged_apex(&mut self, staged_apex_info: &StagedApexInfo) -> Result<()> {
113         let mut need_to_add: Option<ApexInfo> = None;
114         for apex_info in self.list.iter_mut() {
115             if staged_apex_info.moduleName == apex_info.name {
116                 if apex_info.is_active && apex_info.is_factory {
117                     // Copy the entry to the end as factory/non-active after the loop
118                     // to keep the factory version. Typically this step is unncessary,
119                     // but some apexes (like sharedlibs) need to be kept even if it's inactive.
120                     need_to_add.replace(ApexInfo { is_active: false, ..apex_info.clone() });
121                     // And make this one as non-factory. Note that this one is still active
122                     // and overridden right below.
123                     apex_info.is_factory = false;
124                 }
125                 // Active one is overridden with the staged one.
126                 if apex_info.is_active {
127                     apex_info.version = staged_apex_info.versionCode as u64;
128                     apex_info.path = PathBuf::from(&staged_apex_info.diskImagePath);
129                     apex_info.has_classpath_jar = staged_apex_info.hasClassPathJars;
130                     apex_info.last_update_seconds = last_updated(&apex_info.path)?;
131                 }
132             }
133         }
134         if let Some(info) = need_to_add {
135             self.list.push(info);
136         }
137         Ok(())
138     }
139 }
140 
last_updated<P: AsRef<Path>>(path: P) -> Result<u64>141 fn last_updated<P: AsRef<Path>>(path: P) -> Result<u64> {
142     let metadata = metadata(path)?;
143     Ok(metadata.modified()?.duration_since(SystemTime::UNIX_EPOCH)?.as_secs())
144 }
145 
146 impl ApexInfo {
matches(&self, apex_config: &ApexConfig) -> bool147     fn matches(&self, apex_config: &ApexConfig) -> bool {
148         // Match with pseudo name "{CLASSPATH}" which represents APEXes contributing
149         // to any derive_classpath environment variable
150         if apex_config.name == "{CLASSPATH}" && self.has_classpath_jar {
151             return true;
152         }
153         if apex_config.name == self.name {
154             return true;
155         }
156         false
157     }
158 }
159 
160 struct PackageManager {
161     apex_info_list: &'static ApexInfoList,
162 }
163 
164 impl PackageManager {
new() -> Result<Self>165     fn new() -> Result<Self> {
166         let apex_info_list = ApexInfoList::load()?;
167         Ok(Self { apex_info_list })
168     }
169 
get_apex_list(&self, prefer_staged: bool) -> Result<ApexInfoList>170     fn get_apex_list(&self, prefer_staged: bool) -> Result<ApexInfoList> {
171         // get the list of active apexes
172         let mut list = self.apex_info_list.clone();
173         // When prefer_staged, we override ApexInfo by consulting "package_native"
174         if prefer_staged {
175             if cfg!(early) {
176                 return Err(anyhow!("Can't turn on prefer_staged on early boot VMs"));
177             }
178             let pm =
179                 wait_for_interface::<dyn IPackageManagerNative>(PACKAGE_MANAGER_NATIVE_SERVICE)
180                     .context("Failed to get service when prefer_staged is set.")?;
181             let staged = pm.getStagedApexInfos().context("getStagedApexInfos failed")?;
182             for apex in staged {
183                 list.override_staged_apex(&apex)?;
184             }
185         }
186         Ok(list)
187     }
188 }
189 
make_metadata_file( app_config: &VirtualMachineAppConfig, apex_infos: &[&ApexInfo], temporary_directory: &Path, ) -> Result<ParcelFileDescriptor>190 fn make_metadata_file(
191     app_config: &VirtualMachineAppConfig,
192     apex_infos: &[&ApexInfo],
193     temporary_directory: &Path,
194 ) -> Result<ParcelFileDescriptor> {
195     let payload_metadata = match &app_config.payload {
196         Payload::PayloadConfig(payload_config) => PayloadMetadata::Config(PayloadConfig {
197             payload_binary_name: payload_config.payloadBinaryName.clone(),
198             extra_apk_count: payload_config.extraApks.len().try_into()?,
199             special_fields: Default::default(),
200         }),
201         Payload::ConfigPath(config_path) => {
202             PayloadMetadata::ConfigPath(format!("/mnt/apk/{}", config_path))
203         }
204     };
205 
206     let metadata = Metadata {
207         version: 1,
208         apexes: apex_infos
209             .iter()
210             .enumerate()
211             .map(|(i, apex_info)| {
212                 Ok(ApexPayload {
213                     name: apex_info.name.clone(),
214                     partition_name: format!("microdroid-apex-{}", i),
215                     last_update_seconds: apex_info.last_update_seconds,
216                     is_factory: apex_info.is_factory,
217                     ..Default::default()
218                 })
219             })
220             .collect::<Result<_>>()?,
221         apk: Some(ApkPayload {
222             name: "apk".to_owned(),
223             payload_partition_name: "microdroid-apk".to_owned(),
224             idsig_partition_name: "microdroid-apk-idsig".to_owned(),
225             ..Default::default()
226         })
227         .into(),
228         payload: Some(payload_metadata),
229         ..Default::default()
230     };
231 
232     // Write metadata to file.
233     let metadata_path = temporary_directory.join("metadata");
234     let mut metadata_file = OpenOptions::new()
235         .create_new(true)
236         .read(true)
237         .write(true)
238         .open(&metadata_path)
239         .with_context(|| format!("Failed to open metadata file {:?}", metadata_path))?;
240     microdroid_metadata::write_metadata(&metadata, &mut metadata_file)?;
241 
242     // Re-open the metadata file as read-only.
243     open_parcel_file(&metadata_path, false)
244 }
245 
246 /// Creates a DiskImage with partitions:
247 ///   payload-metadata: metadata
248 ///   microdroid-apex-0: apex 0
249 ///   microdroid-apex-1: apex 1
250 ///   ..
251 ///   microdroid-apk: apk
252 ///   microdroid-apk-idsig: idsig
253 ///   extra-apk-0:   additional apk 0
254 ///   extra-idsig-0: additional idsig 0
255 ///   extra-apk-1:   additional apk 1
256 ///   extra-idsig-1: additional idsig 1
257 ///   ..
make_payload_disk( app_config: &VirtualMachineAppConfig, debug_config: &DebugConfig, apk_file: File, idsig_file: File, extra_apk_files: Vec<File>, vm_payload_config: &VmPayloadConfig, temporary_directory: &Path, ) -> Result<DiskImage>258 fn make_payload_disk(
259     app_config: &VirtualMachineAppConfig,
260     debug_config: &DebugConfig,
261     apk_file: File,
262     idsig_file: File,
263     extra_apk_files: Vec<File>,
264     vm_payload_config: &VmPayloadConfig,
265     temporary_directory: &Path,
266 ) -> Result<DiskImage> {
267     if extra_apk_files.len() != app_config.extraIdsigs.len() {
268         bail!(
269             "payload config has {} apks, but app config has {} idsigs",
270             vm_payload_config.extra_apks.len(),
271             app_config.extraIdsigs.len()
272         );
273     }
274 
275     let pm = PackageManager::new()?;
276     let apex_list = pm.get_apex_list(vm_payload_config.prefer_staged)?;
277 
278     // collect APEXes from config
279     let mut apex_infos = collect_apex_infos(&apex_list, &vm_payload_config.apexes, debug_config)?;
280 
281     // Pass sorted list of apexes. Sorting key shouldn't use `path` because it will change after
282     // reboot with prefer_staged. `last_update_seconds` is added to distinguish "samegrade"
283     // update.
284     apex_infos.sort_by_key(|info| (&info.name, &info.version, &info.last_update_seconds));
285     info!("Microdroid payload APEXes: {:?}", apex_infos.iter().map(|ai| &ai.name));
286 
287     let metadata_file = make_metadata_file(app_config, &apex_infos, temporary_directory)?;
288     // put metadata at the first partition
289     let mut partitions = vec![Partition {
290         label: "payload-metadata".to_owned(),
291         image: Some(metadata_file),
292         writable: false,
293         guid: None,
294     }];
295 
296     for (i, apex_info) in apex_infos.iter().enumerate() {
297         let path = if cfg!(early) {
298             let path = &apex_info.preinstalled_path;
299             if path.extension().and_then(OsStr::to_str).unwrap_or("") != "apex" {
300                 bail!("compressed APEX {} not supported", path.display());
301             }
302             path
303         } else {
304             &apex_info.path
305         };
306         let apex_file = open_parcel_file(path, false)?;
307         partitions.push(Partition {
308             label: format!("microdroid-apex-{}", i),
309             image: Some(apex_file),
310             writable: false,
311             guid: None,
312         });
313     }
314     partitions.push(Partition {
315         label: "microdroid-apk".to_owned(),
316         image: Some(ParcelFileDescriptor::new(apk_file)),
317         writable: false,
318         guid: None,
319     });
320     partitions.push(Partition {
321         label: "microdroid-apk-idsig".to_owned(),
322         image: Some(ParcelFileDescriptor::new(idsig_file)),
323         writable: false,
324         guid: None,
325     });
326 
327     // we've already checked that extra_apks and extraIdsigs are in the same size.
328     let extra_idsigs = &app_config.extraIdsigs;
329     for (i, (extra_apk_file, extra_idsig)) in
330         extra_apk_files.into_iter().zip(extra_idsigs.iter()).enumerate()
331     {
332         partitions.push(Partition {
333             label: format!("extra-apk-{i}"),
334             image: Some(ParcelFileDescriptor::new(extra_apk_file)),
335             writable: false,
336             guid: None,
337         });
338 
339         partitions.push(Partition {
340             label: format!("extra-idsig-{i}"),
341             image: Some(ParcelFileDescriptor::new(
342                 extra_idsig
343                     .as_ref()
344                     .try_clone()
345                     .with_context(|| format!("Failed to clone the extra idsig #{i}"))?,
346             )),
347             writable: false,
348             guid: None,
349         });
350     }
351 
352     Ok(DiskImage { image: None, partitions, writable: false })
353 }
354 
run_derive_classpath() -> Result<String>355 fn run_derive_classpath() -> Result<String> {
356     let result = Command::new("/apex/com.android.sdkext/bin/derive_classpath")
357         .arg("/proc/self/fd/1")
358         .output()
359         .context("Failed to run derive_classpath")?;
360 
361     if !result.status.success() {
362         bail!("derive_classpath returned {}", result.status);
363     }
364 
365     String::from_utf8(result.stdout).context("Converting derive_classpath output")
366 }
367 
find_apex_names_in_classpath(classpath_vars: &str) -> Result<HashSet<String>>368 fn find_apex_names_in_classpath(classpath_vars: &str) -> Result<HashSet<String>> {
369     // Each line should be in the format "export <var name> <paths>", where <paths> is a
370     // colon-separated list of paths to JARs. We don't care about the var names, and we're only
371     // interested in paths that look like "/apex/<apex name>/<anything>" so we know which APEXes
372     // contribute to at least one var.
373     let mut apexes = HashSet::new();
374 
375     let pattern = Regex::new(r"^export [^ ]+ ([^ ]+)$").context("Failed to construct Regex")?;
376     for line in classpath_vars.lines() {
377         if let Some(captures) = pattern.captures(line) {
378             if let Some(paths) = captures.get(1) {
379                 apexes.extend(paths.as_str().split(':').filter_map(|path| {
380                     let path = path.strip_prefix("/apex/")?;
381                     Some(path[..path.find('/')?].to_owned())
382                 }));
383                 continue;
384             }
385         }
386         warn!("Malformed line from derive_classpath: {}", line);
387     }
388 
389     Ok(apexes)
390 }
391 
check_apexes_are_from_allowed_partitions(requested_apexes: &Vec<&ApexInfo>) -> Result<()>392 fn check_apexes_are_from_allowed_partitions(requested_apexes: &Vec<&ApexInfo>) -> Result<()> {
393     const ALLOWED_PARTITIONS: [&str; 2] = ["/system", "/system_ext"];
394     for apex in requested_apexes {
395         if !ALLOWED_PARTITIONS.iter().any(|p| apex.preinstalled_path.starts_with(p)) {
396             bail!("Non-system APEX {} is not supported in Microdroid", apex.name);
397         }
398     }
399     Ok(())
400 }
401 
402 // Collect ApexInfos from VM config
collect_apex_infos<'a>( apex_list: &'a ApexInfoList, apex_configs: &[ApexConfig], debug_config: &DebugConfig, ) -> Result<Vec<&'a ApexInfo>>403 fn collect_apex_infos<'a>(
404     apex_list: &'a ApexInfoList,
405     apex_configs: &[ApexConfig],
406     debug_config: &DebugConfig,
407 ) -> Result<Vec<&'a ApexInfo>> {
408     // APEXes which any Microdroid VM needs.
409     // TODO(b/192200378) move this to microdroid.json?
410     let required_apexes: &[_] =
411         if debug_config.should_include_debug_apexes() { &["com.android.adbd"] } else { &[] };
412 
413     let apex_infos = apex_list
414         .list
415         .iter()
416         .filter(|ai| {
417             apex_configs.iter().any(|cfg| ai.matches(cfg) && ai.is_active)
418                 || required_apexes.iter().any(|name| name == &ai.name && ai.is_active)
419                 || ai.provide_shared_apex_libs
420         })
421         .collect();
422 
423     check_apexes_are_from_allowed_partitions(&apex_infos)?;
424     Ok(apex_infos)
425 }
426 
add_microdroid_vendor_image(vendor_image: File, vm_config: &mut VirtualMachineRawConfig)427 pub fn add_microdroid_vendor_image(vendor_image: File, vm_config: &mut VirtualMachineRawConfig) {
428     vm_config.disks.push(DiskImage {
429         image: None,
430         writable: false,
431         partitions: vec![Partition {
432             label: "microdroid-vendor".to_owned(),
433             image: Some(ParcelFileDescriptor::new(vendor_image)),
434             writable: false,
435             guid: None,
436         }],
437     })
438 }
439 
add_microdroid_system_images( config: &VirtualMachineAppConfig, instance_file: File, storage_image: Option<File>, os_name: &str, vm_config: &mut VirtualMachineRawConfig, ) -> Result<()>440 pub fn add_microdroid_system_images(
441     config: &VirtualMachineAppConfig,
442     instance_file: File,
443     storage_image: Option<File>,
444     os_name: &str,
445     vm_config: &mut VirtualMachineRawConfig,
446 ) -> Result<()> {
447     let debug_suffix = match config.debugLevel {
448         DebugLevel::NONE => "normal",
449         DebugLevel::FULL => "debuggable",
450         _ => return Err(anyhow!("unsupported debug level: {:?}", config.debugLevel)),
451     };
452     let initrd = format!("/apex/com.android.virt/etc/{os_name}_initrd_{debug_suffix}.img");
453     vm_config.initrd = Some(open_parcel_file(Path::new(&initrd), false)?);
454 
455     let mut writable_partitions = vec![Partition {
456         label: "vm-instance".to_owned(),
457         image: Some(ParcelFileDescriptor::new(instance_file)),
458         writable: true,
459         guid: None,
460     }];
461 
462     if let Some(file) = storage_image {
463         writable_partitions.push(Partition {
464             label: "encryptedstore".to_owned(),
465             image: Some(ParcelFileDescriptor::new(file)),
466             writable: true,
467             guid: None,
468         });
469     }
470 
471     vm_config.disks.push(DiskImage {
472         image: None,
473         partitions: writable_partitions,
474         writable: true,
475     });
476 
477     Ok(())
478 }
479 
480 #[allow(clippy::too_many_arguments)] // TODO: Fewer arguments
add_microdroid_payload_images( config: &VirtualMachineAppConfig, debug_config: &DebugConfig, temporary_directory: &Path, apk_file: File, idsig_file: File, extra_apk_files: Vec<File>, vm_payload_config: &VmPayloadConfig, vm_config: &mut VirtualMachineRawConfig, ) -> Result<()>481 pub fn add_microdroid_payload_images(
482     config: &VirtualMachineAppConfig,
483     debug_config: &DebugConfig,
484     temporary_directory: &Path,
485     apk_file: File,
486     idsig_file: File,
487     extra_apk_files: Vec<File>,
488     vm_payload_config: &VmPayloadConfig,
489     vm_config: &mut VirtualMachineRawConfig,
490 ) -> Result<()> {
491     vm_config.disks.push(make_payload_disk(
492         config,
493         debug_config,
494         apk_file,
495         idsig_file,
496         extra_apk_files,
497         vm_payload_config,
498         temporary_directory,
499     )?);
500 
501     Ok(())
502 }
503 
504 #[cfg(test)]
505 mod tests {
506     use super::*;
507     use std::collections::HashMap;
508     use tempfile::NamedTempFile;
509 
510     #[test]
test_find_apex_names_in_classpath()511     fn test_find_apex_names_in_classpath() {
512         let vars = r#"
513 export FOO /apex/unterminated
514 export BAR /apex/valid.apex/something
515 wrong
516 export EMPTY
517 export OTHER /foo/bar:/baz:/apex/second.valid.apex/:gibberish:"#;
518         let expected = vec!["valid.apex", "second.valid.apex"];
519         let expected: HashSet<_> = expected.into_iter().map(ToString::to_string).collect();
520 
521         assert_eq!(find_apex_names_in_classpath(vars).unwrap(), expected);
522     }
523 
524     #[test]
test_collect_apexes() -> Result<()>525     fn test_collect_apexes() -> Result<()> {
526         let apex_infos_for_test = [
527             (
528                 "adbd",
529                 ApexInfo {
530                     name: "com.android.adbd".to_string(),
531                     path: PathBuf::from("adbd"),
532                     preinstalled_path: PathBuf::from("/system/adbd"),
533                     has_classpath_jar: false,
534                     last_update_seconds: 12345678,
535                     is_factory: true,
536                     is_active: false,
537                     ..Default::default()
538                 },
539             ),
540             (
541                 "adbd_updated",
542                 ApexInfo {
543                     name: "com.android.adbd".to_string(),
544                     path: PathBuf::from("adbd"),
545                     preinstalled_path: PathBuf::from("/system/adbd"),
546                     has_classpath_jar: false,
547                     last_update_seconds: 12345678 + 1,
548                     is_factory: false,
549                     is_active: true,
550                     ..Default::default()
551                 },
552             ),
553             (
554                 "no_classpath",
555                 ApexInfo {
556                     name: "no_classpath".to_string(),
557                     path: PathBuf::from("no_classpath"),
558                     has_classpath_jar: false,
559                     last_update_seconds: 12345678,
560                     is_factory: true,
561                     is_active: true,
562                     ..Default::default()
563                 },
564             ),
565             (
566                 "has_classpath",
567                 ApexInfo {
568                     name: "has_classpath".to_string(),
569                     path: PathBuf::from("has_classpath"),
570                     has_classpath_jar: true,
571                     last_update_seconds: 87654321,
572                     is_factory: true,
573                     is_active: false,
574                     ..Default::default()
575                 },
576             ),
577             (
578                 "has_classpath_updated",
579                 ApexInfo {
580                     name: "has_classpath".to_string(),
581                     path: PathBuf::from("has_classpath/updated"),
582                     preinstalled_path: PathBuf::from("/system/has_classpath"),
583                     has_classpath_jar: true,
584                     last_update_seconds: 87654321 + 1,
585                     is_factory: false,
586                     is_active: true,
587                     ..Default::default()
588                 },
589             ),
590             (
591                 "apex-foo",
592                 ApexInfo {
593                     name: "apex-foo".to_string(),
594                     path: PathBuf::from("apex-foo"),
595                     preinstalled_path: PathBuf::from("/system/apex-foo"),
596                     has_classpath_jar: false,
597                     last_update_seconds: 87654321,
598                     is_factory: true,
599                     is_active: false,
600                     ..Default::default()
601                 },
602             ),
603             (
604                 "apex-foo-updated",
605                 ApexInfo {
606                     name: "apex-foo".to_string(),
607                     path: PathBuf::from("apex-foo/updated"),
608                     preinstalled_path: PathBuf::from("/system/apex-foo"),
609                     has_classpath_jar: false,
610                     last_update_seconds: 87654321 + 1,
611                     is_factory: false,
612                     is_active: true,
613                     ..Default::default()
614                 },
615             ),
616             (
617                 "sharedlibs",
618                 ApexInfo {
619                     name: "sharedlibs".to_string(),
620                     path: PathBuf::from("apex-foo"),
621                     preinstalled_path: PathBuf::from("/system/apex-foo"),
622                     last_update_seconds: 87654321,
623                     is_factory: true,
624                     provide_shared_apex_libs: true,
625                     ..Default::default()
626                 },
627             ),
628             (
629                 "sharedlibs-updated",
630                 ApexInfo {
631                     name: "sharedlibs".to_string(),
632                     path: PathBuf::from("apex-foo/updated"),
633                     preinstalled_path: PathBuf::from("/system/apex-foo"),
634                     last_update_seconds: 87654321 + 1,
635                     is_active: true,
636                     provide_shared_apex_libs: true,
637                     ..Default::default()
638                 },
639             ),
640         ];
641         let apex_info_list = ApexInfoList {
642             list: apex_infos_for_test.iter().map(|(_, info)| info).cloned().collect(),
643         };
644         let apex_info_map = HashMap::from(apex_infos_for_test);
645         let apex_configs = vec![
646             ApexConfig { name: "apex-foo".to_string() },
647             ApexConfig { name: "{CLASSPATH}".to_string() },
648         ];
649         assert_eq!(
650             collect_apex_infos(
651                 &apex_info_list,
652                 &apex_configs,
653                 &DebugConfig::new_with_debug_level(DebugLevel::FULL)
654             )?,
655             vec![
656                 // Pass active/required APEXes
657                 &apex_info_map["adbd_updated"],
658                 // Pass active APEXes specified in the config
659                 &apex_info_map["has_classpath_updated"],
660                 &apex_info_map["apex-foo-updated"],
661                 // Pass both preinstalled(inactive) and updated(active) for "sharedlibs" APEXes
662                 &apex_info_map["sharedlibs"],
663                 &apex_info_map["sharedlibs-updated"],
664             ]
665         );
666         Ok(())
667     }
668 
669     #[test]
test_check_allowed_partitions_vendor_not_allowed() -> Result<()>670     fn test_check_allowed_partitions_vendor_not_allowed() -> Result<()> {
671         let apex_info_list = ApexInfoList {
672             list: vec![ApexInfo {
673                 name: "apex-vendor".to_string(),
674                 path: PathBuf::from("apex-vendor"),
675                 preinstalled_path: PathBuf::from("/vendor/apex-vendor"),
676                 is_active: true,
677                 ..Default::default()
678             }],
679         };
680         let apex_configs = vec![ApexConfig { name: "apex-vendor".to_string() }];
681 
682         let ret = collect_apex_infos(
683             &apex_info_list,
684             &apex_configs,
685             &DebugConfig::new_with_debug_level(DebugLevel::NONE),
686         );
687         assert!(ret
688             .is_err_and(|ret| ret.to_string()
689                 == "Non-system APEX apex-vendor is not supported in Microdroid"));
690 
691         Ok(())
692     }
693 
694     #[test]
test_check_allowed_partitions_system_ext_allowed() -> Result<()>695     fn test_check_allowed_partitions_system_ext_allowed() -> Result<()> {
696         let apex_info_list = ApexInfoList {
697             list: vec![ApexInfo {
698                 name: "apex-system_ext".to_string(),
699                 path: PathBuf::from("apex-system_ext"),
700                 preinstalled_path: PathBuf::from("/system_ext/apex-system_ext"),
701                 is_active: true,
702                 ..Default::default()
703             }],
704         };
705 
706         let apex_configs = vec![ApexConfig { name: "apex-system_ext".to_string() }];
707 
708         assert_eq!(
709             collect_apex_infos(
710                 &apex_info_list,
711                 &apex_configs,
712                 &DebugConfig::new_with_debug_level(DebugLevel::NONE)
713             )?,
714             vec![&apex_info_list.list[0]]
715         );
716 
717         Ok(())
718     }
719 
720     #[test]
test_prefer_staged_apex_with_factory_active_apex()721     fn test_prefer_staged_apex_with_factory_active_apex() {
722         let single_apex = ApexInfo {
723             name: "foo".to_string(),
724             version: 1,
725             path: PathBuf::from("foo.apex"),
726             is_factory: true,
727             is_active: true,
728             ..Default::default()
729         };
730         let mut apex_info_list = ApexInfoList { list: vec![single_apex.clone()] };
731 
732         let staged = NamedTempFile::new().unwrap();
733         apex_info_list
734             .override_staged_apex(&StagedApexInfo {
735                 moduleName: "foo".to_string(),
736                 versionCode: 2,
737                 diskImagePath: staged.path().to_string_lossy().to_string(),
738                 ..Default::default()
739             })
740             .expect("should be ok");
741 
742         assert_eq!(
743             apex_info_list,
744             ApexInfoList {
745                 list: vec![
746                     ApexInfo {
747                         version: 2,
748                         is_factory: false,
749                         path: staged.path().to_owned(),
750                         last_update_seconds: last_updated(staged.path()).unwrap(),
751                         ..single_apex.clone()
752                     },
753                     ApexInfo { is_active: false, ..single_apex },
754                 ],
755             }
756         );
757     }
758 
759     #[test]
test_prefer_staged_apex_with_factory_and_inactive_apex()760     fn test_prefer_staged_apex_with_factory_and_inactive_apex() {
761         let factory_apex = ApexInfo {
762             name: "foo".to_string(),
763             version: 1,
764             path: PathBuf::from("foo.apex"),
765             is_factory: true,
766             ..Default::default()
767         };
768         let active_apex = ApexInfo {
769             name: "foo".to_string(),
770             version: 2,
771             path: PathBuf::from("foo.downloaded.apex"),
772             is_active: true,
773             ..Default::default()
774         };
775         let mut apex_info_list =
776             ApexInfoList { list: vec![factory_apex.clone(), active_apex.clone()] };
777 
778         let staged = NamedTempFile::new().unwrap();
779         apex_info_list
780             .override_staged_apex(&StagedApexInfo {
781                 moduleName: "foo".to_string(),
782                 versionCode: 3,
783                 diskImagePath: staged.path().to_string_lossy().to_string(),
784                 ..Default::default()
785             })
786             .expect("should be ok");
787 
788         assert_eq!(
789             apex_info_list,
790             ApexInfoList {
791                 list: vec![
792                     // factory apex isn't touched
793                     factory_apex,
794                     // update active one
795                     ApexInfo {
796                         version: 3,
797                         path: staged.path().to_owned(),
798                         last_update_seconds: last_updated(staged.path()).unwrap(),
799                         ..active_apex
800                     },
801                 ],
802             }
803         );
804     }
805 }
806