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 if symbol.sdks.contains(&String::from("AD_SERVICES-ext")) { 143 ensure!( 144 symbol.sdks.len() == 1, 145 "{:?}: AD_SERVICES-ext is mutually exclusive to all other sdks", 146 symbol 147 ); 148 } 149 for id in symbol.sdks.iter() { 150 ensure!( 151 sdk_shortnames.contains(&id), 152 "{:?}: symbol refers to non-existent sdk {}", 153 symbol, 154 id 155 ); 156 } 157 } 158 159 Ok(()) 160 } 161 162 #[test] test_get_attribute()163 fn test_get_attribute() { 164 use xml::EventReader; 165 166 let mut iter = EventReader::from_str(r#"<tag a="A" b="B" c="C"/>"#).into_iter(); 167 let _ = iter.next().unwrap(); // skip start of doc 168 let Ok(XmlEvent::StartElement { attributes, .. }) = iter.next().unwrap() else { 169 panic!(); 170 }; 171 assert_eq!(get_attribute(&attributes, "tag", "a").unwrap(), "A"); 172 assert!(get_attribute(&attributes, "tag", "no-such-attribute").is_err()); 173 } 174 175 #[test] test_verify_xml_correct_input()176 fn test_verify_xml_correct_input() { 177 verify_xml(&include_bytes!("testdata/correct.xml")[..]).unwrap(); 178 } 179 180 #[test] test_verify_xml_incorrect_input()181 fn test_verify_xml_incorrect_input() { 182 macro_rules! assert_err { 183 ($input_path:expr, $expected_error:expr) => { 184 let error = verify_xml(&include_bytes!($input_path)[..]).unwrap_err().to_string(); 185 assert_eq!(error, $expected_error); 186 }; 187 } 188 189 assert_err!( 190 "testdata/corrupt-xml.xml", 191 "25:1 Unexpected end of stream: still inside the root element" 192 ); 193 assert_err!( 194 "testdata/duplicate-sdk-id.xml", 195 r#"[Sdk { id: "1", shortname: "bar", name: "The bar extensions", reference: "android/os/Build$BAR" }]: multiple sdk entries with identical id value"# 196 ); 197 assert_err!( 198 "testdata/duplicate-sdk-shortname.xml", 199 r#"[Sdk { id: "2", shortname: "foo", name: "The bar extensions", reference: "android/os/Build$BAR" }]: multiple sdk entries with identical shortname value"# 200 ); 201 assert_err!( 202 "testdata/duplicate-sdk-name.xml", 203 r#"[Sdk { id: "2", shortname: "bar", name: "The foo extensions", reference: "android/os/Build$BAR" }]: multiple sdk entries with identical name value"# 204 ); 205 assert_err!( 206 "testdata/duplicate-sdk-reference.xml", 207 r#"[Sdk { id: "2", shortname: "bar", name: "The bar extensions", reference: "android/os/Build$FOO" }]: multiple sdk entries with identical reference value"# 208 ); 209 assert_err!( 210 "testdata/incorrect-sdk-id-format.xml", 211 r#"Sdk { id: "1.0", shortname: "foo", name: "The foo extensions", reference: "android/os/Build$FOO" }: id not a positive int"# 212 ); 213 assert_err!( 214 "testdata/duplicate-symbol-sdks.xml", 215 r#"Symbol { jar: "framework-something", pattern: "*", sdks: ["foo", "bar", "bar"] }: symbol contains duplicate references to the same sdk"# 216 ); 217 assert_err!( 218 "testdata/symbol-refers-to-non-existent-sdk.xml", 219 r#"Symbol { jar: "framework-something", pattern: "*", sdks: ["foo", "does-not-exist", "bar"] }: symbol refers to non-existent sdk does-not-exist"# 220 ); 221 assert_err!( 222 "testdata/whitespace-in-jar.xml", 223 r#"Symbol { jar: "framework something", pattern: "*", sdks: ["foo", "bar"] }: jar contains whitespace"# 224 ); 225 assert_err!( 226 "testdata/whitespace-in-pattern.xml", 227 r#"Symbol { jar: "framework-something-else", pattern: "android.app.appsearch.AppSearchSchema.DocumentPropertyConfig.Builder\n .addIndexableNestedProperties ", sdks: ["bar"] }: pattern contains whitespace"# 228 ); 229 assert_err!( 230 "testdata/adservices-sdk-mixed-with-other-sdk.xml", 231 r#"Symbol { jar: "framework-something", pattern: "*", sdks: ["AD_SERVICES-ext", "foo"] }: AD_SERVICES-ext is mutually exclusive to all other sdks"# 232 ); 233 } 234 235 #[test] test_actual_sdk_extensions_info_contents()236 fn test_actual_sdk_extensions_info_contents() { 237 verify_xml(&include_bytes!("../sdk-extensions-info.xml")[..]).unwrap(); 238 } 239 } 240