• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 android_system_virtualizationservice::aidl::android::system::virtualizationservice::{
18     DiskImage::DiskImage, Partition::Partition, VirtualMachineAppConfig::DebugLevel::DebugLevel,
19     VirtualMachineAppConfig::VirtualMachineAppConfig,
20     VirtualMachineRawConfig::VirtualMachineRawConfig,
21 };
22 use android_system_virtualizationservice::binder::ParcelFileDescriptor;
23 use anyhow::{anyhow, bail, Context, Result};
24 use binder::wait_for_interface;
25 use log::{info, warn};
26 use microdroid_metadata::{ApexPayload, ApkPayload, Metadata};
27 use microdroid_payload_config::{ApexConfig, VmPayloadConfig};
28 use once_cell::sync::OnceCell;
29 use packagemanager_aidl::aidl::android::content::pm::{
30     IPackageManagerNative::IPackageManagerNative, StagedApexInfo::StagedApexInfo,
31 };
32 use regex::Regex;
33 use serde::Deserialize;
34 use serde_xml_rs::from_reader;
35 use std::collections::HashSet;
36 use std::fs::{metadata, File, OpenOptions};
37 use std::path::{Path, PathBuf};
38 use std::process::Command;
39 use std::time::SystemTime;
40 use vmconfig::open_parcel_file;
41 
42 /// The list of APEXes which microdroid requires.
43 // TODO(b/192200378) move this to microdroid.json?
44 const MICRODROID_REQUIRED_APEXES: [&str; 1] = ["com.android.os.statsd"];
45 const MICRODROID_REQUIRED_APEXES_DEBUG: [&str; 1] = ["com.android.adbd"];
46 
47 const APEX_INFO_LIST_PATH: &str = "/apex/apex-info-list.xml";
48 
49 const PACKAGE_MANAGER_NATIVE_SERVICE: &str = "package_native";
50 
51 /// Represents the list of APEXes
52 #[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
53 struct ApexInfoList {
54     #[serde(rename = "apex-info")]
55     list: Vec<ApexInfo>,
56 }
57 
58 #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq)]
59 struct ApexInfo {
60     #[serde(rename = "moduleName")]
61     name: String,
62     #[serde(rename = "versionCode")]
63     version: u64,
64     #[serde(rename = "modulePath")]
65     path: PathBuf,
66 
67     #[serde(default)]
68     has_classpath_jar: bool,
69 
70     // The field claims to be milliseconds but is actually seconds.
71     #[serde(rename = "lastUpdateMillis")]
72     last_update_seconds: u64,
73 
74     #[serde(rename = "isFactory")]
75     is_factory: bool,
76 
77     #[serde(rename = "isActive")]
78     is_active: bool,
79 
80     #[serde(rename = "provideSharedApexLibs")]
81     provide_shared_apex_libs: bool,
82 }
83 
84 impl ApexInfoList {
85     /// Loads ApexInfoList
load() -> Result<&'static ApexInfoList>86     fn load() -> Result<&'static ApexInfoList> {
87         static INSTANCE: OnceCell<ApexInfoList> = OnceCell::new();
88         INSTANCE.get_or_try_init(|| {
89             let apex_info_list = File::open(APEX_INFO_LIST_PATH)
90                 .context(format!("Failed to open {}", APEX_INFO_LIST_PATH))?;
91             let mut apex_info_list: ApexInfoList = from_reader(apex_info_list)
92                 .context(format!("Failed to parse {}", APEX_INFO_LIST_PATH))?;
93 
94             // For active APEXes, we run derive_classpath and parse its output to see if it
95             // contributes to the classpath(s). (This allows us to handle any new classpath env
96             // vars seamlessly.)
97             let classpath_vars = run_derive_classpath()?;
98             let classpath_apexes = find_apex_names_in_classpath(&classpath_vars)?;
99 
100             for apex_info in apex_info_list.list.iter_mut() {
101                 apex_info.has_classpath_jar = classpath_apexes.contains(&apex_info.name);
102             }
103 
104             Ok(apex_info_list)
105         })
106     }
107 
108     // Override apex info with the staged one
override_staged_apex(&mut self, staged_apex_info: &StagedApexInfo) -> Result<()>109     fn override_staged_apex(&mut self, staged_apex_info: &StagedApexInfo) -> Result<()> {
110         let mut need_to_add: Option<ApexInfo> = None;
111         for apex_info in self.list.iter_mut() {
112             if staged_apex_info.moduleName == apex_info.name {
113                 if apex_info.is_active && apex_info.is_factory {
114                     // Copy the entry to the end as factory/non-active after the loop
115                     // to keep the factory version. Typically this step is unncessary,
116                     // but some apexes (like sharedlibs) need to be kept even if it's inactive.
117                     need_to_add.replace(ApexInfo { is_active: false, ..apex_info.clone() });
118                     // And make this one as non-factory. Note that this one is still active
119                     // and overridden right below.
120                     apex_info.is_factory = false;
121                 }
122                 // Active one is overridden with the staged one.
123                 if apex_info.is_active {
124                     apex_info.version = staged_apex_info.versionCode as u64;
125                     apex_info.path = PathBuf::from(&staged_apex_info.diskImagePath);
126                     apex_info.has_classpath_jar = staged_apex_info.hasClassPathJars;
127                     apex_info.last_update_seconds = last_updated(&apex_info.path)?;
128                 }
129             }
130         }
131         if let Some(info) = need_to_add {
132             self.list.push(info);
133         }
134         Ok(())
135     }
136 }
137 
last_updated<P: AsRef<Path>>(path: P) -> Result<u64>138 fn last_updated<P: AsRef<Path>>(path: P) -> Result<u64> {
139     let metadata = metadata(path)?;
140     Ok(metadata.modified()?.duration_since(SystemTime::UNIX_EPOCH)?.as_secs())
141 }
142 
143 impl ApexInfo {
matches(&self, apex_config: &ApexConfig) -> bool144     fn matches(&self, apex_config: &ApexConfig) -> bool {
145         // Match with pseudo name "{CLASSPATH}" which represents APEXes contributing
146         // to any derive_classpath environment variable
147         if apex_config.name == "{CLASSPATH}" && self.has_classpath_jar {
148             return true;
149         }
150         if apex_config.name == self.name {
151             return true;
152         }
153         false
154     }
155 }
156 
157 struct PackageManager {
158     apex_info_list: &'static ApexInfoList,
159 }
160 
161 impl PackageManager {
new() -> Result<Self>162     fn new() -> Result<Self> {
163         let apex_info_list = ApexInfoList::load()?;
164         Ok(Self { apex_info_list })
165     }
166 
get_apex_list(&self, prefer_staged: bool) -> Result<ApexInfoList>167     fn get_apex_list(&self, prefer_staged: bool) -> Result<ApexInfoList> {
168         // get the list of active apexes
169         let mut list = self.apex_info_list.clone();
170         // When prefer_staged, we override ApexInfo by consulting "package_native"
171         if prefer_staged {
172             let pm =
173                 wait_for_interface::<dyn IPackageManagerNative>(PACKAGE_MANAGER_NATIVE_SERVICE)
174                     .context("Failed to get service when prefer_staged is set.")?;
175             let staged =
176                 pm.getStagedApexModuleNames().context("getStagedApexModuleNames failed")?;
177             for name in staged {
178                 if let Some(staged_apex_info) =
179                     pm.getStagedApexInfo(&name).context("getStagedApexInfo failed")?
180                 {
181                     list.override_staged_apex(&staged_apex_info)?;
182                 }
183             }
184         }
185         Ok(list)
186     }
187 }
188 
make_metadata_file( config_path: &str, apex_infos: &[&ApexInfo], temporary_directory: &Path, ) -> Result<ParcelFileDescriptor>189 fn make_metadata_file(
190     config_path: &str,
191     apex_infos: &[&ApexInfo],
192     temporary_directory: &Path,
193 ) -> Result<ParcelFileDescriptor> {
194     let metadata_path = temporary_directory.join("metadata");
195     let metadata = Metadata {
196         version: 1,
197         apexes: apex_infos
198             .iter()
199             .enumerate()
200             .map(|(i, apex_info)| {
201                 Ok(ApexPayload {
202                     name: apex_info.name.clone(),
203                     partition_name: format!("microdroid-apex-{}", i),
204                     last_update_seconds: apex_info.last_update_seconds,
205                     is_factory: apex_info.is_factory,
206                     ..Default::default()
207                 })
208             })
209             .collect::<Result<_>>()?,
210         apk: Some(ApkPayload {
211             name: "apk".to_owned(),
212             payload_partition_name: "microdroid-apk".to_owned(),
213             idsig_partition_name: "microdroid-apk-idsig".to_owned(),
214             ..Default::default()
215         })
216         .into(),
217         payload_config_path: format!("/mnt/apk/{}", config_path),
218         ..Default::default()
219     };
220 
221     // Write metadata to file.
222     let mut metadata_file = OpenOptions::new()
223         .create_new(true)
224         .read(true)
225         .write(true)
226         .open(&metadata_path)
227         .with_context(|| format!("Failed to open metadata file {:?}", metadata_path))?;
228     microdroid_metadata::write_metadata(&metadata, &mut metadata_file)?;
229 
230     // Re-open the metadata file as read-only.
231     open_parcel_file(&metadata_path, false)
232 }
233 
234 /// Creates a DiskImage with partitions:
235 ///   metadata: metadata
236 ///   microdroid-apex-0: apex 0
237 ///   microdroid-apex-1: apex 1
238 ///   ..
239 ///   microdroid-apk: apk
240 ///   microdroid-apk-idsig: idsig
241 ///   extra-apk-0:   additional apk 0
242 ///   extra-idsig-0: additional idsig 0
243 ///   extra-apk-1:   additional apk 1
244 ///   extra-idsig-1: additional idsig 1
245 ///   ..
make_payload_disk( app_config: &VirtualMachineAppConfig, apk_file: File, idsig_file: File, vm_payload_config: &VmPayloadConfig, temporary_directory: &Path, ) -> Result<DiskImage>246 fn make_payload_disk(
247     app_config: &VirtualMachineAppConfig,
248     apk_file: File,
249     idsig_file: File,
250     vm_payload_config: &VmPayloadConfig,
251     temporary_directory: &Path,
252 ) -> Result<DiskImage> {
253     if vm_payload_config.extra_apks.len() != app_config.extraIdsigs.len() {
254         bail!(
255             "payload config has {} apks, but app config has {} idsigs",
256             vm_payload_config.extra_apks.len(),
257             app_config.extraIdsigs.len()
258         );
259     }
260 
261     let pm = PackageManager::new()?;
262     let apex_list = pm.get_apex_list(vm_payload_config.prefer_staged)?;
263 
264     // collect APEXes from config
265     let mut apex_infos =
266         collect_apex_infos(&apex_list, &vm_payload_config.apexes, app_config.debugLevel);
267 
268     // Pass sorted list of apexes. Sorting key shouldn't use `path` because it will change after
269     // reboot with prefer_staged. `last_update_seconds` is added to distinguish "samegrade"
270     // update.
271     apex_infos.sort_by_key(|info| (&info.name, &info.version, &info.last_update_seconds));
272     info!("Microdroid payload APEXes: {:?}", apex_infos.iter().map(|ai| &ai.name));
273 
274     let metadata_file =
275         make_metadata_file(&app_config.configPath, &apex_infos, temporary_directory)?;
276     // put metadata at the first partition
277     let mut partitions = vec![Partition {
278         label: "payload-metadata".to_owned(),
279         image: Some(metadata_file),
280         writable: false,
281     }];
282 
283     for (i, apex_info) in apex_infos.iter().enumerate() {
284         let apex_file = open_parcel_file(&apex_info.path, false)?;
285         partitions.push(Partition {
286             label: format!("microdroid-apex-{}", i),
287             image: Some(apex_file),
288             writable: false,
289         });
290     }
291     partitions.push(Partition {
292         label: "microdroid-apk".to_owned(),
293         image: Some(ParcelFileDescriptor::new(apk_file)),
294         writable: false,
295     });
296     partitions.push(Partition {
297         label: "microdroid-apk-idsig".to_owned(),
298         image: Some(ParcelFileDescriptor::new(idsig_file)),
299         writable: false,
300     });
301 
302     // we've already checked that extra_apks and extraIdsigs are in the same size.
303     let extra_apks = &vm_payload_config.extra_apks;
304     let extra_idsigs = &app_config.extraIdsigs;
305     for (i, (extra_apk, extra_idsig)) in extra_apks.iter().zip(extra_idsigs.iter()).enumerate() {
306         partitions.push(Partition {
307             label: format!("extra-apk-{}", i),
308             image: Some(ParcelFileDescriptor::new(File::open(PathBuf::from(&extra_apk.path))?)),
309             writable: false,
310         });
311 
312         partitions.push(Partition {
313             label: format!("extra-idsig-{}", i),
314             image: Some(ParcelFileDescriptor::new(extra_idsig.as_ref().try_clone()?)),
315             writable: false,
316         });
317     }
318 
319     Ok(DiskImage { image: None, partitions, writable: false })
320 }
321 
run_derive_classpath() -> Result<String>322 fn run_derive_classpath() -> Result<String> {
323     let result = Command::new("/apex/com.android.sdkext/bin/derive_classpath")
324         .arg("/proc/self/fd/1")
325         .output()
326         .context("Failed to run derive_classpath")?;
327 
328     if !result.status.success() {
329         bail!("derive_classpath returned {}", result.status);
330     }
331 
332     String::from_utf8(result.stdout).context("Converting derive_classpath output")
333 }
334 
find_apex_names_in_classpath(classpath_vars: &str) -> Result<HashSet<String>>335 fn find_apex_names_in_classpath(classpath_vars: &str) -> Result<HashSet<String>> {
336     // Each line should be in the format "export <var name> <paths>", where <paths> is a
337     // colon-separated list of paths to JARs. We don't care about the var names, and we're only
338     // interested in paths that look like "/apex/<apex name>/<anything>" so we know which APEXes
339     // contribute to at least one var.
340     let mut apexes = HashSet::new();
341 
342     let pattern = Regex::new(r"^export [^ ]+ ([^ ]+)$").context("Failed to construct Regex")?;
343     for line in classpath_vars.lines() {
344         if let Some(captures) = pattern.captures(line) {
345             if let Some(paths) = captures.get(1) {
346                 apexes.extend(paths.as_str().split(':').filter_map(|path| {
347                     let path = path.strip_prefix("/apex/")?;
348                     Some(path[..path.find('/')?].to_owned())
349                 }));
350                 continue;
351             }
352         }
353         warn!("Malformed line from derive_classpath: {}", line);
354     }
355 
356     Ok(apexes)
357 }
358 
359 // Collect ApexInfos from VM config
collect_apex_infos<'a>( apex_list: &'a ApexInfoList, apex_configs: &[ApexConfig], debug_level: DebugLevel, ) -> Vec<&'a ApexInfo>360 fn collect_apex_infos<'a>(
361     apex_list: &'a ApexInfoList,
362     apex_configs: &[ApexConfig],
363     debug_level: DebugLevel,
364 ) -> Vec<&'a ApexInfo> {
365     let mut additional_apexes: Vec<&str> = MICRODROID_REQUIRED_APEXES.to_vec();
366     if debug_level != DebugLevel::NONE {
367         additional_apexes.extend(MICRODROID_REQUIRED_APEXES_DEBUG.to_vec());
368     }
369 
370     apex_list
371         .list
372         .iter()
373         .filter(|ai| {
374             apex_configs.iter().any(|cfg| ai.matches(cfg) && ai.is_active)
375                 || additional_apexes.iter().any(|name| name == &ai.name && ai.is_active)
376                 || ai.provide_shared_apex_libs
377         })
378         .collect()
379 }
380 
add_microdroid_images( config: &VirtualMachineAppConfig, temporary_directory: &Path, apk_file: File, idsig_file: File, instance_file: File, vm_payload_config: &VmPayloadConfig, vm_config: &mut VirtualMachineRawConfig, ) -> Result<()>381 pub fn add_microdroid_images(
382     config: &VirtualMachineAppConfig,
383     temporary_directory: &Path,
384     apk_file: File,
385     idsig_file: File,
386     instance_file: File,
387     vm_payload_config: &VmPayloadConfig,
388     vm_config: &mut VirtualMachineRawConfig,
389 ) -> Result<()> {
390     vm_config.disks.push(make_payload_disk(
391         config,
392         apk_file,
393         idsig_file,
394         vm_payload_config,
395         temporary_directory,
396     )?);
397 
398     vm_config.disks[1].partitions.push(Partition {
399         label: "vbmeta".to_owned(),
400         image: Some(open_parcel_file(
401             Path::new("/apex/com.android.virt/etc/fs/microdroid_vbmeta_bootconfig.img"),
402             false,
403         )?),
404         writable: false,
405     });
406     let bootconfig_image = "/apex/com.android.virt/etc/microdroid_bootconfig.".to_owned()
407         + match config.debugLevel {
408             DebugLevel::NONE => "normal",
409             DebugLevel::APP_ONLY => "app_debuggable",
410             DebugLevel::FULL => "full_debuggable",
411             _ => return Err(anyhow!("unsupported debug level: {:?}", config.debugLevel)),
412         };
413     vm_config.disks[1].partitions.push(Partition {
414         label: "bootconfig".to_owned(),
415         image: Some(open_parcel_file(Path::new(&bootconfig_image), false)?),
416         writable: false,
417     });
418 
419     // instance image is at the second partition in the second disk.
420     vm_config.disks[1].partitions.push(Partition {
421         label: "vm-instance".to_owned(),
422         image: Some(ParcelFileDescriptor::new(instance_file)),
423         writable: true,
424     });
425 
426     Ok(())
427 }
428 
429 #[cfg(test)]
430 mod tests {
431     use super::*;
432     use tempfile::NamedTempFile;
433 
434     #[test]
test_find_apex_names_in_classpath()435     fn test_find_apex_names_in_classpath() {
436         let vars = r#"
437 export FOO /apex/unterminated
438 export BAR /apex/valid.apex/something
439 wrong
440 export EMPTY
441 export OTHER /foo/bar:/baz:/apex/second.valid.apex/:gibberish:"#;
442         let expected = vec!["valid.apex", "second.valid.apex"];
443         let expected: HashSet<_> = expected.into_iter().map(ToString::to_string).collect();
444 
445         assert_eq!(find_apex_names_in_classpath(vars).unwrap(), expected);
446     }
447 
448     #[test]
test_collect_apexes()449     fn test_collect_apexes() {
450         let apex_info_list = ApexInfoList {
451             list: vec![
452                 ApexInfo {
453                     // 0
454                     name: "com.android.adbd".to_string(),
455                     path: PathBuf::from("adbd"),
456                     has_classpath_jar: false,
457                     last_update_seconds: 12345678,
458                     is_factory: true,
459                     is_active: true,
460                     ..Default::default()
461                 },
462                 ApexInfo {
463                     // 1
464                     name: "com.android.os.statsd".to_string(),
465                     path: PathBuf::from("statsd"),
466                     has_classpath_jar: false,
467                     last_update_seconds: 12345678,
468                     is_factory: true,
469                     is_active: false,
470                     ..Default::default()
471                 },
472                 ApexInfo {
473                     // 2
474                     name: "com.android.os.statsd".to_string(),
475                     path: PathBuf::from("statsd/updated"),
476                     has_classpath_jar: false,
477                     last_update_seconds: 12345678 + 1,
478                     is_factory: false,
479                     is_active: true,
480                     ..Default::default()
481                 },
482                 ApexInfo {
483                     // 3
484                     name: "no_classpath".to_string(),
485                     path: PathBuf::from("no_classpath"),
486                     has_classpath_jar: false,
487                     last_update_seconds: 12345678,
488                     is_factory: true,
489                     is_active: true,
490                     ..Default::default()
491                 },
492                 ApexInfo {
493                     // 4
494                     name: "has_classpath".to_string(),
495                     path: PathBuf::from("has_classpath"),
496                     has_classpath_jar: true,
497                     last_update_seconds: 87654321,
498                     is_factory: true,
499                     is_active: false,
500                     ..Default::default()
501                 },
502                 ApexInfo {
503                     // 5
504                     name: "has_classpath".to_string(),
505                     path: PathBuf::from("has_classpath/updated"),
506                     has_classpath_jar: true,
507                     last_update_seconds: 87654321 + 1,
508                     is_factory: false,
509                     is_active: true,
510                     ..Default::default()
511                 },
512                 ApexInfo {
513                     // 6
514                     name: "apex-foo".to_string(),
515                     path: PathBuf::from("apex-foo"),
516                     has_classpath_jar: false,
517                     last_update_seconds: 87654321,
518                     is_factory: true,
519                     is_active: false,
520                     ..Default::default()
521                 },
522                 ApexInfo {
523                     // 7
524                     name: "apex-foo".to_string(),
525                     path: PathBuf::from("apex-foo/updated"),
526                     has_classpath_jar: false,
527                     last_update_seconds: 87654321 + 1,
528                     is_factory: false,
529                     is_active: true,
530                     ..Default::default()
531                 },
532                 ApexInfo {
533                     // 8
534                     name: "sharedlibs".to_string(),
535                     path: PathBuf::from("apex-foo"),
536                     last_update_seconds: 87654321,
537                     is_factory: true,
538                     provide_shared_apex_libs: true,
539                     ..Default::default()
540                 },
541                 ApexInfo {
542                     // 9
543                     name: "sharedlibs".to_string(),
544                     path: PathBuf::from("apex-foo/updated"),
545                     last_update_seconds: 87654321 + 1,
546                     is_active: true,
547                     provide_shared_apex_libs: true,
548                     ..Default::default()
549                 },
550             ],
551         };
552         let apex_configs = vec![
553             ApexConfig { name: "apex-foo".to_string() },
554             ApexConfig { name: "{CLASSPATH}".to_string() },
555         ];
556         assert_eq!(
557             collect_apex_infos(&apex_info_list, &apex_configs, DebugLevel::FULL),
558             vec![
559                 // Pass active/required APEXes
560                 &apex_info_list.list[0],
561                 &apex_info_list.list[2],
562                 // Pass active APEXes specified in the config
563                 &apex_info_list.list[5],
564                 &apex_info_list.list[7],
565                 // Pass both preinstalled(inactive) and updated(active) for "sharedlibs" APEXes
566                 &apex_info_list.list[8],
567                 &apex_info_list.list[9],
568             ]
569         );
570     }
571 
572     #[test]
test_prefer_staged_apex_with_factory_active_apex()573     fn test_prefer_staged_apex_with_factory_active_apex() {
574         let single_apex = ApexInfo {
575             name: "foo".to_string(),
576             version: 1,
577             path: PathBuf::from("foo.apex"),
578             is_factory: true,
579             is_active: true,
580             ..Default::default()
581         };
582         let mut apex_info_list = ApexInfoList { list: vec![single_apex.clone()] };
583 
584         let staged = NamedTempFile::new().unwrap();
585         apex_info_list
586             .override_staged_apex(&StagedApexInfo {
587                 moduleName: "foo".to_string(),
588                 versionCode: 2,
589                 diskImagePath: staged.path().to_string_lossy().to_string(),
590                 ..Default::default()
591             })
592             .expect("should be ok");
593 
594         assert_eq!(
595             apex_info_list,
596             ApexInfoList {
597                 list: vec![
598                     ApexInfo {
599                         version: 2,
600                         is_factory: false,
601                         path: staged.path().to_owned(),
602                         last_update_seconds: last_updated(staged.path()).unwrap(),
603                         ..single_apex.clone()
604                     },
605                     ApexInfo { is_active: false, ..single_apex },
606                 ],
607             }
608         );
609     }
610 
611     #[test]
test_prefer_staged_apex_with_factory_and_inactive_apex()612     fn test_prefer_staged_apex_with_factory_and_inactive_apex() {
613         let factory_apex = ApexInfo {
614             name: "foo".to_string(),
615             version: 1,
616             path: PathBuf::from("foo.apex"),
617             is_factory: true,
618             ..Default::default()
619         };
620         let active_apex = ApexInfo {
621             name: "foo".to_string(),
622             version: 2,
623             path: PathBuf::from("foo.downloaded.apex"),
624             is_active: true,
625             ..Default::default()
626         };
627         let mut apex_info_list =
628             ApexInfoList { list: vec![factory_apex.clone(), active_apex.clone()] };
629 
630         let staged = NamedTempFile::new().unwrap();
631         apex_info_list
632             .override_staged_apex(&StagedApexInfo {
633                 moduleName: "foo".to_string(),
634                 versionCode: 3,
635                 diskImagePath: staged.path().to_string_lossy().to_string(),
636                 ..Default::default()
637             })
638             .expect("should be ok");
639 
640         assert_eq!(
641             apex_info_list,
642             ApexInfoList {
643                 list: vec![
644                     // factory apex isn't touched
645                     factory_apex,
646                     // update active one
647                     ApexInfo {
648                         version: 3,
649                         path: staged.path().to_owned(),
650                         last_update_seconds: last_updated(staged.path()).unwrap(),
651                         ..active_apex
652                     },
653                 ],
654             }
655         );
656     }
657 }
658