1 /* 2 * Copyright (C) 2010 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 package com.android.tradefed.targetprep; 18 19 import com.android.tradefed.command.remote.DeviceDescriptor; 20 import com.android.tradefed.log.LogUtil.CLog; 21 import com.android.tradefed.result.error.InfraErrorIdentifier; 22 import com.android.tradefed.util.MultiMap; 23 24 import org.apache.commons.compress.archivers.zip.ZipArchiveEntry; 25 import org.apache.commons.compress.archivers.zip.ZipFile; 26 27 import java.io.BufferedReader; 28 import java.io.File; 29 import java.io.FileInputStream; 30 import java.io.IOException; 31 import java.io.InputStreamReader; 32 import java.util.ArrayList; 33 import java.util.Collection; 34 import java.util.HashMap; 35 import java.util.Map; 36 import java.util.regex.Matcher; 37 import java.util.regex.Pattern; 38 import java.util.zip.ZipException; 39 40 /** 41 * A class that parses out required versions of auxiliary image files needed to flash a device. 42 * (e.g. bootloader, baseband, etc) 43 */ 44 public class FlashingResourcesParser implements IFlashingResourcesParser { 45 /** 46 * A filtering interface, intended to allow {@link FlashingResourcesParser} to ignore some 47 * resources that it otherwise might use 48 */ 49 public static interface Constraint { 50 /** 51 * Check if the provided {@code item} passes the constraint. 52 * @return {@code true} for accept, {@code false} for reject 53 */ shouldAccept(String item)54 public boolean shouldAccept(String item); 55 } 56 57 private static final String ANDROID_INFO_FILE_NAME = "android-info.txt"; 58 /** 59 * Some resource files use "require-foo=bar", others use "foo=bar". This expression handles 60 * both. 61 */ 62 private static final Pattern REQUIRE_PATTERN = Pattern.compile("(?:require\\s)?(.*?)=(.*)"); 63 /** 64 * Some resource files have special product-specific requirements, for instance: 65 * {@code require-for-product:product1 version-bootloader=xyz} would only require bootloader 66 * {@code xyz} for device {@code product1}. This pattern matches the require-for-product line 67 */ 68 private static final Pattern PRODUCT_REQUIRE_PATTERN = 69 Pattern.compile("require-for-product:(\\S+) +(.*?)=(.*)"); 70 71 // expected keys 72 public static final String PRODUCT_KEY = "product"; 73 public static final String BOARD_KEY = "board"; 74 public static final String BOOTLOADER_VERSION_KEY = "version-bootloader"; 75 public static final String BASEBAND_VERSION_KEY = "version-baseband"; 76 77 // key-value pairs of build requirements 78 private AndroidInfo mReqs; 79 80 /** 81 * A typedef for {@code Map<String, MultiMap<String, String>>}. Useful parsed 82 * format for storing the data encoded in ANDROID_INFO_FILE_NAME 83 */ 84 @SuppressWarnings("serial") 85 public static class AndroidInfo extends HashMap<String, MultiMap<String, String>> {} 86 87 /** 88 * Create a {@link FlashingResourcesParser} and have it parse the specified device image for 89 * flashing requirements. Flashing requirements must pass the appropriate constraint (if one 90 * exists) before being added. Rejected requirements will be dropped silently. 91 * 92 * @param deviceImgZipFile The {@code updater.zip} file to be flashed 93 * @param c A map from key name to {@link Constraint}. Image names will be checked against 94 * the appropriate constraint (if any) as a prereq for being added. May be null to 95 * disable filtering. 96 */ FlashingResourcesParser(File deviceImgZipFile, Map<String, Constraint> c)97 public FlashingResourcesParser(File deviceImgZipFile, Map<String, Constraint> c) 98 throws TargetSetupError { 99 mReqs = getBuildRequirements(deviceImgZipFile, c); 100 } 101 102 /** 103 * Create a {@link FlashingResourcesParser} and have it parse the specified device image for 104 * flashing requirements. 105 * 106 * @param deviceImgZipFile The {@code updater.zip} file to be flashed 107 */ FlashingResourcesParser(File deviceImgZipFile)108 public FlashingResourcesParser(File deviceImgZipFile) throws TargetSetupError { 109 this(deviceImgZipFile, null); 110 } 111 112 /** 113 * Constructs a FlashingResourcesParser with the supplied AndroidInfo Reader 114 * <p/> 115 * Exposed for unit testing 116 * 117 * @param infoReader a {@link BufferedReader} containing the equivalent of android-info.txt to 118 * parse 119 * @param c A map from key name to {@link Constraint}. Image names will be checked against 120 * the appropriate constraint (if any) as a prereq for being added. May be null to 121 * disable filtering. 122 */ FlashingResourcesParser(BufferedReader infoReader, Map<String, Constraint> c)123 public FlashingResourcesParser(BufferedReader infoReader, Map<String, Constraint> c) 124 throws IOException { 125 mReqs = parseAndroidInfo(infoReader, c); 126 } 127 128 /** 129 * Constructs a FlashingResourcesParser with the supplied AndroidInfo Reader 130 * <p/> 131 * Exposed for unit testing 132 * 133 * @param infoReader a {@link BufferedReader} containing the equivalent of android-info.txt to 134 * parse 135 */ FlashingResourcesParser(BufferedReader infoReader)136 public FlashingResourcesParser(BufferedReader infoReader) throws IOException { 137 this(infoReader, null); 138 } 139 140 /** 141 * {@inheritDoc} 142 * <p/> 143 * If multiple versions are listed, get the latest with the assumption that versions sort from 144 * oldest to newest alphabetically. 145 */ 146 @Override getRequiredBootloaderVersion()147 public String getRequiredBootloaderVersion() { 148 return getRequiredImageVersion(BOOTLOADER_VERSION_KEY); 149 } 150 151 /** 152 * {@inheritDoc} 153 * <p/> 154 * If multiple versions are listed, get the latest with the assumption that versions sort from 155 * oldest to newest alphabetically. 156 */ 157 @Override getRequiredBasebandVersion()158 public String getRequiredBasebandVersion() { 159 return getRequiredImageVersion(BASEBAND_VERSION_KEY); 160 } 161 162 /** 163 * {@inheritDoc} 164 * <p/> 165 * If multiple versions are listed, get the latest with the assumption that versions sort from 166 * oldest to newest alphabetically. 167 */ 168 @Override getRequiredImageVersion(String imageVersionKey)169 public String getRequiredImageVersion(String imageVersionKey) { 170 // Use null to designate the global product requirements 171 return getRequiredImageVersion(imageVersionKey, null); 172 } 173 174 /** 175 * {@inheritDoc} 176 * <p/> 177 * If multiple versions are listed, get the latest with the assumption that versions sort from 178 * oldest to newest alphabetically. 179 */ 180 @Override getRequiredImageVersion(String imageVersionKey, String productName)181 public String getRequiredImageVersion(String imageVersionKey, String productName) { 182 MultiMap<String, String> productReqs = mReqs.get(productName); 183 184 if (productReqs == null) { 185 if (productName != null) { 186 // There aren't any product-specific requirements for productName. 187 // Fall back to global requirements. 188 return getRequiredImageVersion(imageVersionKey, null); 189 } else { 190 // No global result exists; return null 191 CLog.w("No global requirements found, android-info.txt might be empty?"); 192 return null; 193 } 194 } 195 196 // Get the latest version assuming versions are sorted alphabetically. 197 String result = getNewest(productReqs.get(imageVersionKey)); 198 199 if (result != null) { 200 // If there's a result, return it 201 return result; 202 } 203 if (result == null && productName != null) { 204 // There aren't any product-specific requirements for this particular imageVersionKey 205 // for productName. Fall back to global requirements. 206 return getRequiredImageVersion(imageVersionKey, null); 207 } 208 209 // Neither a specific nor a global result exists; return null 210 return null; 211 } 212 213 /** 214 * {@inheritDoc} 215 */ 216 @Override getRequiredBoards()217 public Collection<String> getRequiredBoards() { 218 Collection<String> all = new ArrayList<String>(); 219 MultiMap<String, String> boardReqs = mReqs.get(null); 220 if (boardReqs == null) { 221 return null; 222 } 223 224 Collection<String> board = boardReqs.get(BOARD_KEY); 225 Collection<String> product = boardReqs.get(PRODUCT_KEY); 226 227 // board overrides product here 228 if (board != null) { 229 all.addAll(board); 230 } else if (product != null) { 231 all.addAll(product); 232 } else { 233 return null; 234 } 235 236 return all; 237 } 238 239 /** 240 * Gets the newest element in the given {@link Collection} or <code>null</code> with the 241 * assumption that newer elements follow older elements when sorted alphabetically. 242 */ getNewest(Collection<String> values)243 private static String getNewest(Collection<String> values) { 244 if (values == null || values.isEmpty()) { 245 return null; 246 } 247 String newest = null; 248 for (String element : values) { 249 if (newest == null || element.compareTo(newest) > 0) { 250 newest = element; 251 } 252 } 253 return newest; 254 } 255 256 /** 257 * This parses android-info.txt from system image zip and returns key value pairs of required 258 * image files. 259 * <p/> 260 * Expects the following syntax: 261 * <p/> 262 * <i>[require] key=value1[|value2]</i> 263 * 264 * @return a {@link Map} of parsed key value pairs, or <code>null</code> if data could not be 265 * parsed 266 */ getBuildRequirements(File deviceImgZipFile, Map<String, Constraint> constraints)267 static AndroidInfo getBuildRequirements(File deviceImgZipFile, 268 Map<String, Constraint> constraints) throws TargetSetupError { 269 if (deviceImgZipFile == null || !deviceImgZipFile.exists()) { 270 throw new TargetSetupError( 271 String.format("Device image zip %s doesn't not exist", deviceImgZipFile), 272 InfraErrorIdentifier.OPTION_CONFIGURATION_ERROR); 273 } 274 275 ZipFile deviceZip = null; 276 BufferedReader infoReader = null; 277 try { 278 if (deviceImgZipFile.isDirectory()) { 279 File androidInfo = new File(deviceImgZipFile, ANDROID_INFO_FILE_NAME); 280 if (!androidInfo.exists()) { 281 DeviceDescriptor nullDescriptor = null; 282 throw new TargetSetupError( 283 String.format( 284 "Could not find %s in device image directory %s", 285 ANDROID_INFO_FILE_NAME, deviceImgZipFile.getName()), 286 nullDescriptor, 287 InfraErrorIdentifier.CONFIGURED_ARTIFACT_NOT_FOUND); 288 } 289 infoReader = 290 new BufferedReader(new InputStreamReader(new FileInputStream(androidInfo))); 291 return parseAndroidInfo(infoReader, constraints); 292 } else { 293 deviceZip = new ZipFile(deviceImgZipFile); 294 ZipArchiveEntry androidInfoEntry = deviceZip.getEntry(ANDROID_INFO_FILE_NAME); 295 if (androidInfoEntry == null) { 296 DeviceDescriptor nullDescriptor = null; 297 throw new TargetSetupError( 298 String.format( 299 "Could not find %s in device image zip %s", 300 ANDROID_INFO_FILE_NAME, deviceImgZipFile.getName()), 301 nullDescriptor, 302 InfraErrorIdentifier.CONFIGURED_ARTIFACT_NOT_FOUND); 303 } 304 infoReader = 305 new BufferedReader( 306 new InputStreamReader(deviceZip.getInputStream(androidInfoEntry))); 307 308 return parseAndroidInfo(infoReader, constraints); 309 } 310 } catch (ZipException e) { 311 throw new TargetSetupError( 312 String.format("Could not read device image zip %s", deviceImgZipFile.getName()), 313 e, 314 InfraErrorIdentifier.UNDETERMINED); 315 } catch (IOException e) { 316 throw new TargetSetupError( 317 String.format("Could not read device image zip %s", deviceImgZipFile.getName()), 318 e, 319 InfraErrorIdentifier.UNDETERMINED); 320 } finally { 321 if (deviceZip != null) { 322 try { 323 deviceZip.close(); 324 } catch (IOException e) { 325 // ignore 326 } 327 } 328 if (infoReader != null) { 329 try { 330 infoReader.close(); 331 } catch (IOException e) { 332 // ignore 333 } 334 } 335 } 336 } 337 338 /** 339 * Returns the current value for the provided key if one exists, or creates and returns a new 340 * value if one does not exist. 341 */ getOrCreateEntry(AndroidInfo map, String key)342 private static MultiMap<String, String> getOrCreateEntry(AndroidInfo map, String key) { 343 if (map.containsKey(key)) { 344 return map.get(key); 345 } else { 346 MultiMap<String, String> value = new MultiMap<String, String>(); 347 map.put(key, value); 348 return value; 349 } 350 } 351 352 /** 353 * Parses the required build attributes from an android-info data source. 354 * <p/> 355 * Exposed as package-private for unit testing. 356 * 357 * @param infoReader the {@link BufferedReader} to read android-info text data from 358 * @return a Map of parsed attribute name-value pairs 359 * @throws IOException 360 */ parseAndroidInfo(BufferedReader infoReader, Map<String, Constraint> constraints)361 static AndroidInfo parseAndroidInfo(BufferedReader infoReader, 362 Map<String, Constraint> constraints) throws IOException { 363 AndroidInfo requiredImageMap = new AndroidInfo(); 364 365 boolean eof = false; 366 while (!eof) { 367 String line = infoReader.readLine(); 368 if (line != null) { 369 Matcher matcher = PRODUCT_REQUIRE_PATTERN.matcher(line); 370 if (matcher.matches()) { 371 String product = matcher.group(1); 372 String key = matcher.group(2); 373 String values = matcher.group(3); 374 // Requirements specific to product {@code product} 375 MultiMap<String, String> reqs = getOrCreateEntry(requiredImageMap, product); 376 for (String value : values.split("\\|")) { 377 reqs.put(key, value); 378 } 379 } else { 380 matcher = REQUIRE_PATTERN.matcher(line); 381 if (matcher.matches()) { 382 String key = matcher.group(1); 383 String values = matcher.group(2); 384 Constraint c = null; 385 if (constraints != null) { 386 c = constraints.get(key); 387 } 388 389 // Use a null product identifier to designate requirements for all products 390 MultiMap<String, String> reqs = getOrCreateEntry(requiredImageMap, null); 391 for (String value : values.split("\\|")) { 392 if ((c == null) || c.shouldAccept(value)) { 393 reqs.put(key, value); 394 } 395 } 396 } 397 } 398 } else { 399 eof = true; 400 } 401 } 402 return requiredImageMap; 403 } 404 } 405