1 /*
2  * Copyright (C) 2024 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 #[cfg(test)]
18 mod tests {
19     use anyhow::{anyhow, bail, ensure, Result};
20     use itertools::Itertools;
21     use std::fmt::Debug;
22     use std::io::Read;
23     use xml::attribute::OwnedAttribute;
24     use xml::reader::{ParserConfig, XmlEvent};
25 
get_attribute(attributes: &[OwnedAttribute], tag: &str, key: &str) -> Result<String>26     fn get_attribute(attributes: &[OwnedAttribute], tag: &str, key: &str) -> Result<String> {
27         attributes
28             .iter()
29             .find_map(
30                 |attr| {
31                     if attr.name.local_name == key {
32                         Some(attr.value.clone())
33                     } else {
34                         None
35                     }
36                 },
37             )
38             .ok_or_else(|| anyhow!("tag {}: missing attribute {}", tag, key))
39     }
40 
verify_xml<R: Read>(mut source: R) -> Result<()>41     fn verify_xml<R: Read>(mut source: R) -> Result<()> {
42         #[derive(Debug)]
43         struct Sdk {
44             id: String,
45             shortname: String,
46             name: String,
47             reference: String,
48         }
49 
50         #[derive(Debug)]
51         struct Symbol {
52             #[allow(dead_code)]
53             jar: String,
54             #[allow(dead_code)]
55             pattern: String,
56             sdks: Vec<String>,
57         }
58 
59         // this will error out on XML syntax errors
60         let reader = ParserConfig::new().create_reader(&mut source);
61         let events: Vec<_> = reader.into_iter().collect::<Result<Vec<_>, _>>()?;
62 
63         // parse XML
64         let mut sdks = vec![];
65         let mut symbols = vec![];
66         for (name, attributes) in events.into_iter().filter_map(|e| match e {
67             XmlEvent::StartElement { name, attributes, namespace: _ } => {
68                 Some((name.local_name, attributes))
69             }
70             _ => None,
71         }) {
72             match name.as_str() {
73                 "sdk-extensions-info" => {}
74                 "sdk" => {
75                     let sdk = Sdk {
76                         id: get_attribute(&attributes, "sdk", "id")?,
77                         shortname: get_attribute(&attributes, "sdk", "shortname")?,
78                         name: get_attribute(&attributes, "sdk", "name")?,
79                         reference: get_attribute(&attributes, "sdk", "reference")?,
80                     };
81                     sdks.push(sdk);
82                 }
83                 "symbol" => {
84                     let symbol = Symbol {
85                         jar: get_attribute(&attributes, "symbol", "jar")?,
86                         pattern: get_attribute(&attributes, "symbol", "pattern")?,
87                         sdks: get_attribute(&attributes, "symbol", "sdks")?
88                             .split(',')
89                             .map(|s| s.to_owned())
90                             .collect(),
91                     };
92                     symbols.push(symbol);
93                 }
94                 _ => bail!("unknown tag '{}'", name),
95             }
96         }
97 
98         // verify all Sdk fields are unique across all Sdk items
99         let dupes = sdks.iter().duplicates_by(|sdk| &sdk.id).collect::<Vec<_>>();
100         ensure!(dupes.is_empty(), "{:?}: multiple sdk entries with identical id value", dupes);
101 
102         let dupes = sdks.iter().duplicates_by(|sdk| &sdk.shortname).collect::<Vec<_>>();
103         ensure!(
104             dupes.is_empty(),
105             "{:?}: multiple sdk entries with identical shortname value",
106             dupes
107         );
108 
109         let dupes = sdks.iter().duplicates_by(|sdk| &sdk.name).collect::<Vec<_>>();
110         ensure!(dupes.is_empty(), "{:?}: multiple sdk entries with identical name value", dupes);
111 
112         let dupes = sdks.iter().duplicates_by(|sdk| &sdk.reference).collect::<Vec<_>>();
113         ensure!(
114             dupes.is_empty(),
115             "{:?}: multiple sdk entries with identical reference value",
116             dupes
117         );
118 
119         // verify Sdk id field has the expected format (positive integer)
120         for sdk in sdks.iter() {
121             ensure!(sdk.id.parse::<usize>().is_ok(), "{:?}: id not a positive int", sdk);
122         }
123 
124         // verify individual Symbol elements
125         let sdk_shortnames: Vec<_> = sdks.iter().map(|sdk| &sdk.shortname).collect();
126         for symbol in symbols.iter() {
127             ensure!(
128                 symbol.sdks.iter().duplicates().collect::<Vec<_>>().is_empty(),
129                 "{:?}: symbol contains duplicate references to the same sdk",
130                 symbol
131             );
132             ensure!(
133                 !symbol.jar.contains(char::is_whitespace),
134                 "{:?}: jar contains whitespace",
135                 symbol
136             );
137             ensure!(
138                 !symbol.pattern.contains(char::is_whitespace),
139                 "{:?}: pattern contains whitespace",
140                 symbol
141             );
142             for id in symbol.sdks.iter() {
143                 ensure!(
144                     sdk_shortnames.contains(&id),
145                     "{:?}: symbol refers to non-existent sdk {}",
146                     symbol,
147                     id
148                 );
149             }
150         }
151 
152         Ok(())
153     }
154 
155     #[test]
test_get_attribute()156     fn test_get_attribute() {
157         use xml::EventReader;
158 
159         let mut iter = EventReader::from_str(r#"<tag a="A" b="B" c="C"/>"#).into_iter();
160         let _ = iter.next().unwrap(); // skip start of doc
161         let Ok(XmlEvent::StartElement { attributes, .. }) = iter.next().unwrap() else {
162             panic!();
163         };
164         assert_eq!(get_attribute(&attributes, "tag", "a").unwrap(), "A");
165         assert!(get_attribute(&attributes, "tag", "no-such-attribute").is_err());
166     }
167 
168     #[test]
test_verify_xml_correct_input()169     fn test_verify_xml_correct_input() {
170         verify_xml(&include_bytes!("testdata/correct.xml")[..]).unwrap();
171     }
172 
173     #[test]
test_verify_xml_incorrect_input()174     fn test_verify_xml_incorrect_input() {
175         macro_rules! assert_err {
176             ($input_path:expr, $expected_error:expr) => {
177                 let error = verify_xml(&include_bytes!($input_path)[..]).unwrap_err().to_string();
178                 assert_eq!(error, $expected_error);
179             };
180         }
181 
182         assert_err!(
183             "testdata/corrupt-xml.xml",
184             "25:1 Unexpected end of stream: still inside the root element"
185         );
186         assert_err!(
187             "testdata/duplicate-sdk-id.xml",
188             r#"[Sdk { id: "1", shortname: "bar", name: "The bar extensions", reference: "android/os/Build$BAR" }]: multiple sdk entries with identical id value"#
189         );
190         assert_err!(
191             "testdata/duplicate-sdk-shortname.xml",
192             r#"[Sdk { id: "2", shortname: "foo", name: "The bar extensions", reference: "android/os/Build$BAR" }]: multiple sdk entries with identical shortname value"#
193         );
194         assert_err!(
195             "testdata/duplicate-sdk-name.xml",
196             r#"[Sdk { id: "2", shortname: "bar", name: "The foo extensions", reference: "android/os/Build$BAR" }]: multiple sdk entries with identical name value"#
197         );
198         assert_err!(
199             "testdata/duplicate-sdk-reference.xml",
200             r#"[Sdk { id: "2", shortname: "bar", name: "The bar extensions", reference: "android/os/Build$FOO" }]: multiple sdk entries with identical reference value"#
201         );
202         assert_err!(
203             "testdata/incorrect-sdk-id-format.xml",
204             r#"Sdk { id: "1.0", shortname: "foo", name: "The foo extensions", reference: "android/os/Build$FOO" }: id not a positive int"#
205         );
206         assert_err!(
207             "testdata/duplicate-symbol-sdks.xml",
208             r#"Symbol { jar: "framework-something", pattern: "*", sdks: ["foo", "bar", "bar"] }: symbol contains duplicate references to the same sdk"#
209         );
210         assert_err!(
211             "testdata/symbol-refers-to-non-existent-sdk.xml",
212             r#"Symbol { jar: "framework-something", pattern: "*", sdks: ["foo", "does-not-exist", "bar"] }: symbol refers to non-existent sdk does-not-exist"#
213         );
214         assert_err!(
215             "testdata/whitespace-in-jar.xml",
216             r#"Symbol { jar: "framework something", pattern: "*", sdks: ["foo", "bar"] }: jar contains whitespace"#
217         );
218         assert_err!(
219             "testdata/whitespace-in-pattern.xml",
220             r#"Symbol { jar: "framework-something-else", pattern: "android.app.appsearch.AppSearchSchema.DocumentPropertyConfig.Builder\n                .addIndexableNestedProperties ", sdks: ["bar"] }: pattern contains whitespace"#
221         );
222     }
223 
224     #[test]
test_actual_sdk_extensions_info_contents()225     fn test_actual_sdk_extensions_info_contents() {
226         verify_xml(&include_bytes!("../sdk-extensions-info.xml")[..]).unwrap();
227     }
228 }
229