1 // Copyright 2022, 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 //! Retrieve and populate information about userspace.
16 
17 use kmr_wire::SetHalInfoRequest;
18 use regex::Regex;
19 
20 // The OS version property is of form "12" or "12.1" or "12.1.3".
21 const OS_VERSION_PROPERTY: &str = "ro.build.version.release";
22 const OS_VERSION_REGEX: &str = r"^(?P<major>\d{1,2})(\.(?P<minor>\d{1,2}))?(\.(?P<sub>\d{1,2}))?$";
23 
24 // The patchlevel properties are of form "YYYY-MM-DD".
25 /// Name of property that holds the OS patchlevel.
26 pub const OS_PATCHLEVEL_PROPERTY: &str = "ro.build.version.security_patch";
27 const VENDOR_PATCHLEVEL_PROPERTY: &str = "ro.vendor.build.security_patch";
28 const PATCHLEVEL_REGEX: &str = r"^(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})$";
29 
30 // Just use [`String`] for errors here.
31 type Error = String;
32 
33 /// Retrieve a numeric value from a possible match.
extract_u32(value: Option<regex::Match>) -> Result<u32, Error>34 fn extract_u32(value: Option<regex::Match>) -> Result<u32, Error> {
35     match value {
36         Some(m) => {
37             let s = m.as_str();
38             match s.parse::<u32>() {
39                 Ok(v) => Ok(v),
40                 Err(e) => Err(format!("failed to parse integer: {:?}", e)),
41             }
42         }
43         None => Err("failed to find match".to_string()),
44     }
45 }
46 
47 /// Retrieve the value of a property identified by `name`.
get_property(name: &str) -> Result<String, Error>48 pub fn get_property(name: &str) -> Result<String, Error> {
49     match rustutils::system_properties::read(name) {
50         Ok(Some(value)) => Ok(value),
51         Ok(None) => Err(format!("no value for property {}", name)),
52         Err(e) => Err(format!("failed to get property {}: {:?}", name, e)),
53     }
54 }
55 
56 /// Extract a patchlevel in form YYYYMM from a "YYYY-MM-DD" property value.
extract_truncated_patchlevel(prop_value: &str) -> Result<u32, Error>57 pub fn extract_truncated_patchlevel(prop_value: &str) -> Result<u32, Error> {
58     let patchlevel_regex = Regex::new(PATCHLEVEL_REGEX)
59         .map_err(|e| format!("failed to compile patchlevel regexp: {:?}", e))?;
60 
61     let captures = patchlevel_regex
62         .captures(prop_value)
63         .ok_or_else(|| "failed to match patchlevel regex".to_string())?;
64     let year = extract_u32(captures.name("year"))?;
65     let month = extract_u32(captures.name("month"))?;
66     if !(1..=12).contains(&month) {
67         return Err(format!("month out of range: {}", month));
68     }
69     // no day
70     Ok(year * 100 + month)
71 }
72 
73 /// Extract a patchlevel in form YYYYMMDD from a "YYYY-MM-DD" property value.
extract_patchlevel(prop_value: &str) -> Result<u32, Error>74 pub fn extract_patchlevel(prop_value: &str) -> Result<u32, Error> {
75     let patchlevel_regex = Regex::new(PATCHLEVEL_REGEX)
76         .map_err(|e| format!("failed to compile patchlevel regexp: {:?}", e))?;
77 
78     let captures = patchlevel_regex
79         .captures(prop_value)
80         .ok_or_else(|| "failed to match patchlevel regex".to_string())?;
81     let year = extract_u32(captures.name("year"))?;
82     let month = extract_u32(captures.name("month"))?;
83     if !(1..=12).contains(&month) {
84         return Err(format!("month out of range: {}", month));
85     }
86     let day = extract_u32(captures.name("day"))?;
87     if !(1..=31).contains(&day) {
88         return Err(format!("day out of range: {}", day));
89     }
90     Ok(year * 10000 + month * 100 + day)
91 }
92 
93 /// Generate HAL information from property values.
populate_hal_info_from( os_version_prop: &str, os_patchlevel_prop: &str, vendor_patchlevel_prop: &str, ) -> Result<SetHalInfoRequest, Error>94 fn populate_hal_info_from(
95     os_version_prop: &str,
96     os_patchlevel_prop: &str,
97     vendor_patchlevel_prop: &str,
98 ) -> Result<SetHalInfoRequest, Error> {
99     let os_version_regex = Regex::new(OS_VERSION_REGEX)
100         .map_err(|e| format!("failed to compile version regexp: {:?}", e))?;
101     let captures = os_version_regex
102         .captures(os_version_prop)
103         .ok_or_else(|| "failed to match OS version regex".to_string())?;
104     let major = extract_u32(captures.name("major"))?;
105     let minor = extract_u32(captures.name("minor")).unwrap_or(0u32);
106     let sub = extract_u32(captures.name("sub")).unwrap_or(0u32);
107     let os_version = (major * 10000) + (minor * 100) + sub;
108 
109     Ok(SetHalInfoRequest {
110         os_version,
111         os_patchlevel: extract_truncated_patchlevel(os_patchlevel_prop)?,
112         vendor_patchlevel: extract_patchlevel(vendor_patchlevel_prop)?,
113     })
114 }
115 
116 /// Populate a [`SetHalInfoRequest`] based on property values read from the environment.
populate_hal_info() -> Result<SetHalInfoRequest, Error>117 pub fn populate_hal_info() -> Result<SetHalInfoRequest, Error> {
118     let os_version_prop = get_property(OS_VERSION_PROPERTY)
119         .map_err(|e| format!("failed to retrieve property: {:?}", e))?;
120     let os_patchlevel_prop = get_property(OS_PATCHLEVEL_PROPERTY)
121         .map_err(|e| format!("failed to retrieve property: {:?}", e))?;
122     let vendor_patchlevel_prop = get_property(VENDOR_PATCHLEVEL_PROPERTY)
123         .map_err(|e| format!("failed to retrieve property: {:?}", e))?;
124 
125     populate_hal_info_from(&os_version_prop, &os_patchlevel_prop, &vendor_patchlevel_prop)
126 }
127 
128 #[cfg(test)]
129 mod tests {
130     use super::*;
131     use kmr_wire::SetHalInfoRequest;
132     #[test]
test_hal_info()133     fn test_hal_info() {
134         let tests = vec![
135             (
136                 "12",
137                 "2021-02-02",
138                 "2022-03-04",
139                 SetHalInfoRequest {
140                     os_version: 120000,
141                     os_patchlevel: 202102,
142                     vendor_patchlevel: 20220304,
143                 },
144             ),
145             (
146                 "12.5",
147                 "2021-02-02",
148                 "2022-03-04",
149                 SetHalInfoRequest {
150                     os_version: 120500,
151                     os_patchlevel: 202102,
152                     vendor_patchlevel: 20220304,
153                 },
154             ),
155             (
156                 "12.5.7",
157                 "2021-02-02",
158                 "2022-03-04",
159                 SetHalInfoRequest {
160                     os_version: 120507,
161                     os_patchlevel: 202102,
162                     vendor_patchlevel: 20220304,
163                 },
164             ),
165         ];
166         for (os_version, os_patch, vendor_patch, want) in tests {
167             let got = populate_hal_info_from(os_version, os_patch, vendor_patch).unwrap();
168             assert_eq!(
169                 got, want,
170                 "Mismatch for input ({}, {}, {})",
171                 os_version, os_patch, vendor_patch
172             );
173         }
174     }
175 
176     #[test]
test_invalid_hal_info()177     fn test_invalid_hal_info() {
178         let tests = vec![
179             ("xx", "2021-02-02", "2022-03-04", "failed to match OS version"),
180             ("12.xx", "2021-02-02", "2022-03-04", "failed to match OS version"),
181             ("12.5.xx", "2021-02-02", "2022-03-04", "failed to match OS version"),
182             ("12", "20212-02-02", "2022-03-04", "failed to match patchlevel regex"),
183             ("12", "2021-xx-02", "2022-03-04", "failed to match patchlevel"),
184             ("12", "2021-13-02", "2022-03-04", "month out of range"),
185             ("12", "2022-03-04", "2021-xx-02", "failed to match patchlevel"),
186             ("12", "2022-03-04", "2021-13-02", "month out of range"),
187             ("12", "2022-03-04", "2021-03-32", "day out of range"),
188         ];
189         for (os_version, os_patch, vendor_patch, want_err) in tests {
190             let result = populate_hal_info_from(os_version, os_patch, vendor_patch);
191             assert!(result.is_err());
192             let err = result.unwrap_err();
193             assert!(
194                 err.contains(want_err),
195                 "Mismatch for input ({}, {}, {}), got error '{}', want '{}'",
196                 os_version,
197                 os_patch,
198                 vendor_patch,
199                 err,
200                 want_err
201             );
202         }
203     }
204 }
205