1 /*
2  * Copyright (C) 2022, The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *     http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 //! Reports redundant AIDL libraries included in a partition.
18 
19 use anyhow::{Context, Result};
20 use clap::Parser;
21 use std::collections::BTreeMap;
22 use std::fs::File;
23 use std::io::BufReader;
24 use std::path::{Path, PathBuf};
25 
26 #[derive(Parser, Debug)]
27 #[structopt()]
28 struct Opt {
29     /// JSON file with list of files installed in a partition, e.g. "$OUT/installed-files.json".
30     #[clap(long)]
31     installed_files_json: PathBuf,
32 
33     /// JSON file with metadata for AIDL interfaces. Optional, but fewer checks are performed when
34     /// unset.
35     #[clap(long)]
36     aidl_metadata_json: Option<PathBuf>,
37 }
38 
39 /// "aidl_metadata.json" entry.
40 #[derive(Debug, serde::Deserialize)]
41 struct AidlInterfaceMetadata {
42     /// Name of module defining package.
43     name: String,
44 }
45 
46 /// "installed-files.json" entry.
47 #[derive(Debug, serde::Deserialize)]
48 struct InstalledFile {
49     /// Full file path.
50     #[serde(rename = "Name")]
51     name: String,
52     /// File size.
53     #[serde(rename = "Size")]
54     size: u64,
55 }
56 
57 #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
58 enum LibDir {
59     Lib,
60     Lib64,
61 }
62 
63 /// An instance of an AIDL interface lib.
64 #[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
65 struct AidlInstance {
66     installed_path: String,
67     size: u64,
68     name: String,
69     variant: String, // e.g. "ndk" or "cpp"
70     version: usize,
71     lib_dir: LibDir,
72 }
73 
74 /// Deserializes a JSON file at `path` into an object of type `T`.
read_json_file<T: serde::de::DeserializeOwned>(path: &Path) -> Result<T>75 fn read_json_file<T: serde::de::DeserializeOwned>(path: &Path) -> Result<T> {
76     let file = File::open(path).with_context(|| format!("failed to open: {}", path.display()))?;
77     serde_json::from_reader(BufReader::new(file))
78         .with_context(|| format!("failed to read: {}", path.display()))
79 }
80 
81 /// Extracts AIDL lib info an `InstalledFile`, mainly by parsing the file path. Returns `None` if
82 /// it doesn't look like an AIDL lib.
extract_aidl_instance(installed_file: &InstalledFile) -> Option<AidlInstance>83 fn extract_aidl_instance(installed_file: &InstalledFile) -> Option<AidlInstance> {
84     // example: android.hardware.security.keymint-V2-ndk.so
85     let lib_regex = regex::Regex::new(r".*/(lib|lib64)/([^-]*)-V(\d+)-([^.]+)\.")
86         .expect("failed to parse regex");
87     let captures = lib_regex.captures(&installed_file.name)?;
88     let (dir, name, version, variant) = (&captures[1], &captures[2], &captures[3], &captures[4]);
89     Some(AidlInstance {
90         installed_path: installed_file.name.clone(),
91         size: installed_file.size,
92         name: name.to_string(),
93         variant: variant.to_string(),
94         version: version.parse().unwrap(),
95         lib_dir: if dir == "lib64" { LibDir::Lib64 } else { LibDir::Lib },
96     })
97 }
98 
main() -> Result<()>99 fn main() -> Result<()> {
100     let args = Opt::parse();
101 
102     // Read the metadata file if available.
103     let metadata_list: Option<Vec<AidlInterfaceMetadata>> = match &args.aidl_metadata_json {
104         Some(aidl_metadata_json) => read_json_file(aidl_metadata_json)?,
105         None => None,
106     };
107     let is_valid_aidl_lib = |name: &str| match &metadata_list {
108         Some(x) => x.iter().any(|metadata| metadata.name == name),
109         None => true,
110     };
111 
112     // Read the "installed-files.json" and create a list of AidlInstance.
113     let installed_files: Vec<InstalledFile> = read_json_file(&args.installed_files_json)?;
114     let instances: Vec<AidlInstance> = installed_files
115         .iter()
116         .filter_map(extract_aidl_instance)
117         .filter(|instance| {
118             if !is_valid_aidl_lib(&instance.name) {
119                 eprintln!(
120                     "WARNING: {} looks like an AIDL lib, but has no metadata",
121                     &instance.installed_path
122                 );
123                 return false;
124             }
125             true
126         })
127         .collect();
128 
129     // Group redundant AIDL lib instances together.
130     let groups: BTreeMap<(String, LibDir), Vec<&AidlInstance>> =
131         instances.iter().fold(BTreeMap::new(), |mut acc, x| {
132             let key = (x.name.clone(), x.lib_dir);
133             acc.entry(key).or_default().push(x);
134             acc
135         });
136     let mut total_wasted_bytes = 0;
137     for (group_key, mut instances) in groups {
138         if instances.len() > 1 {
139             instances.sort();
140             // Prefer the highest version, break ties favoring ndk.
141             let preferred_instance = instances
142                 .iter()
143                 .max_by_key(|x| (x.version, i32::from(x.variant == "ndk")))
144                 .unwrap();
145             let wasted_bytes: u64 =
146                 instances.iter().filter(|x| *x != preferred_instance).map(|x| x.size).sum();
147             println!("Found redundant AIDL instances for {:?}", group_key);
148             for instance in instances.iter() {
149                 println!(
150                     "\t{}\t({:.2} KiB){}",
151                     instance.installed_path,
152                     instance.size as f64 / 1024.0,
153                     if instance == preferred_instance { " <- preferred" } else { "" }
154                 );
155             }
156             total_wasted_bytes += wasted_bytes;
157             println!("\t(potential savings: {:.2} KiB)", wasted_bytes as f64 / 1024.0);
158             println!();
159         }
160     }
161     println!("total potential savings: {:.2} KiB", total_wasted_bytes as f64 / 1024.0);
162 
163     Ok(())
164 }
165 
166 #[cfg(test)]
167 mod tests {
168     use super::*;
169     use clap::CommandFactory;
170 
171     #[test]
verify_opt()172     fn verify_opt() {
173         Opt::command().debug_assert();
174     }
175 }
176