/* * Copyright (C) 2010 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.tradefed.targetprep; import com.android.tradefed.util.MultiMap; import java.io.BufferedReader; import java.io.File; import java.io.IOException; import java.io.InputStreamReader; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.zip.ZipEntry; import java.util.zip.ZipException; import java.util.zip.ZipFile; /** * A class that parses out required versions of auxiliary image files needed to flash a device. * (e.g. bootloader, baseband, etc) */ public class FlashingResourcesParser implements IFlashingResourcesParser { /** * A filtering interface, intended to allow {@link FlashingResourcesParser} to ignore some * resources that it otherwise might use */ public static interface Constraint { /** * Check if the provided {@code item} passes the constraint. * @return {@code true} for accept, {@code false} for reject */ public boolean shouldAccept(String item); } private static final String ANDROID_INFO_FILE_NAME = "android-info.txt"; /** * Some resource files use "require-foo=bar", others use "foo=bar". This expression handles * both. */ private static final Pattern REQUIRE_PATTERN = Pattern.compile("(?:require\\s)?(.*?)=(.*)"); /** * Some resource files have special product-specific requirements, for instance: * {@code require-for-product:product1 version-bootloader=xyz} would only require bootloader * {@code xyz} for device {@code product1}. This pattern matches the require-for-product line */ private static final Pattern PRODUCT_REQUIRE_PATTERN = Pattern.compile("require-for-product:(\\S+) +(.*?)=(.*)"); // expected keys public static final String PRODUCT_KEY = "product"; public static final String BOARD_KEY = "board"; public static final String BOOTLOADER_VERSION_KEY = "version-bootloader"; public static final String BASEBAND_VERSION_KEY = "version-baseband"; // key-value pairs of build requirements private AndroidInfo mReqs; /** * A typedef for {@code Map<String, MultiMap<String, String>>}. Useful parsed * format for storing the data encoded in ANDROID_INFO_FILE_NAME */ @SuppressWarnings("serial") public static class AndroidInfo extends HashMap<String, MultiMap<String, String>> {} /** * Create a {@link FlashingResourcesParser} and have it parse the specified device image for * flashing requirements. Flashing requirements must pass the appropriate constraint (if one * exists) before being added. Rejected requirements will be dropped silently. * * @param deviceImgZipFile The {@code updater.zip} file to be flashed * @param c A map from key name to {@link Constraint}. Image names will be checked against * the appropriate constraint (if any) as a prereq for being added. May be null to * disable filtering. */ public FlashingResourcesParser(File deviceImgZipFile, Map<String, Constraint> c) throws TargetSetupError { mReqs = getBuildRequirements(deviceImgZipFile, c); } /** * Create a {@link FlashingResourcesParser} and have it parse the specified device image for * flashing requirements. * * @param deviceImgZipFile The {@code updater.zip} file to be flashed */ public FlashingResourcesParser(File deviceImgZipFile) throws TargetSetupError { this(deviceImgZipFile, null); } /** * Constructs a FlashingResourcesParser with the supplied AndroidInfo Reader * <p/> * Exposed for unit testing * * @param infoReader a {@link BufferedReader} containing the equivalent of android-info.txt to * parse * @param c A map from key name to {@link Constraint}. Image names will be checked against * the appropriate constraint (if any) as a prereq for being added. May be null to * disable filtering. */ public FlashingResourcesParser(BufferedReader infoReader, Map<String, Constraint> c) throws TargetSetupError, IOException { mReqs = parseAndroidInfo(infoReader, c); } /** * Constructs a FlashingResourcesParser with the supplied AndroidInfo Reader * <p/> * Exposed for unit testing * * @param infoReader a {@link BufferedReader} containing the equivalent of android-info.txt to * parse */ public FlashingResourcesParser(BufferedReader infoReader) throws TargetSetupError, IOException { this(infoReader, null); } /** * {@inheritDoc} * <p/> * If multiple versions are listed, get the latest with the assumption that versions sort from * oldest to newest alphabetically. */ @Override public String getRequiredBootloaderVersion() { return getRequiredImageVersion(BOOTLOADER_VERSION_KEY); } /** * {@inheritDoc} * <p/> * If multiple versions are listed, get the latest with the assumption that versions sort from * oldest to newest alphabetically. */ @Override public String getRequiredBasebandVersion() { return getRequiredImageVersion(BASEBAND_VERSION_KEY); } /** * {@inheritDoc} * <p/> * If multiple versions are listed, get the latest with the assumption that versions sort from * oldest to newest alphabetically. */ @Override public String getRequiredImageVersion(String imageVersionKey) { // Use null to designate the global product requirements return getRequiredImageVersion(imageVersionKey, null); } /** * {@inheritDoc} * <p/> * If multiple versions are listed, get the latest with the assumption that versions sort from * oldest to newest alphabetically. */ @Override public String getRequiredImageVersion(String imageVersionKey, String productName) { MultiMap<String, String> productReqs = mReqs.get(productName); if (productReqs == null && productName != null) { // There aren't any product-specific requirements for productName. Fall back to global // requirements. return getRequiredImageVersion(imageVersionKey, null); } // Get the latest version assuming versions are sorted alphabetically. String result = getNewest(productReqs.get(imageVersionKey)); if (result != null) { // If there's a result, return it return result; } if (result == null && productName != null) { // There aren't any product-specific requirements for this particular imageVersionKey // for productName. Fall back to global requirements. return getRequiredImageVersion(imageVersionKey, null); } // Neither a specific nor a global result exists; return null return null; } /** * {@inheritDoc} */ @Override public Collection<String> getRequiredBoards() { Collection<String> all = new ArrayList<String>(); MultiMap<String, String> boardReqs = mReqs.get(null); if (boardReqs == null) { return null; } Collection<String> board = boardReqs.get(BOARD_KEY); Collection<String> product = boardReqs.get(PRODUCT_KEY); // board overrides product here if (board != null) { all.addAll(board); } else if (product != null) { all.addAll(product); } else { return null; } return all; } /** * Gets the newest element in the given {@link Collection} or <code>null</code> with the * assumption that newer elements follow older elements when sorted alphabetically. */ private static String getNewest(Collection<String> values) { if (values == null || values.isEmpty()) { return null; } String newest = null; for (String element : values) { if (newest == null || element.compareTo(newest) > 0) { newest = element; } } return newest; } /** * This parses android-info.txt from system image zip and returns key value pairs of required * image files. * <p/> * Expects the following syntax: * <p/> * <i>[require] key=value1[|value2]</i> * * @returns a {@link Map} of parsed key value pairs, or <code>null</code> if data could not be * parsed */ static AndroidInfo getBuildRequirements(File deviceImgZipFile, Map<String, Constraint> constraints) throws TargetSetupError { ZipFile deviceZip = null; BufferedReader infoReader = null; try { deviceZip = new ZipFile(deviceImgZipFile); ZipEntry androidInfoEntry = deviceZip.getEntry(ANDROID_INFO_FILE_NAME); if (androidInfoEntry == null) { throw new TargetSetupError(String.format("Could not find %s in device image zip %s", ANDROID_INFO_FILE_NAME, deviceImgZipFile.getName())); } infoReader = new BufferedReader(new InputStreamReader( deviceZip.getInputStream(androidInfoEntry))); return parseAndroidInfo(infoReader, constraints); } catch (ZipException e) { throw new TargetSetupError(String.format("Could not read device image zip %s", deviceImgZipFile.getName()), e); } catch (IOException e) { throw new TargetSetupError(String.format("Could not read device image zip %s", deviceImgZipFile.getName()), e); } finally { if (deviceZip != null) { try { deviceZip.close(); } catch (IOException e) { // ignore } } if (infoReader != null) { try { infoReader.close(); } catch (IOException e) { // ignore } } } } /** * Returns the current value for the provided key if one exists, or creates and returns a new * value if one does not exist. */ private static MultiMap<String, String> getOrCreateEntry(AndroidInfo map, String key) { if (map.containsKey(key)) { return map.get(key); } else { MultiMap<String, String> value = new MultiMap<String, String>(); map.put(key, value); return value; } } /** * Parses the required build attributes from an android-info data source. * <p/> * Exposed as package-private for unit testing. * * @param infoReader the {@link BufferedReader} to read android-info text data from * @return a Map of parsed attribute name-value pairs * @throws IOException */ static AndroidInfo parseAndroidInfo(BufferedReader infoReader, Map<String, Constraint> constraints) throws IOException { AndroidInfo requiredImageMap = new AndroidInfo(); boolean eof = false; while (!eof) { String line = infoReader.readLine(); if (line != null) { Matcher matcher = PRODUCT_REQUIRE_PATTERN.matcher(line); if (matcher.matches()) { String product = matcher.group(1); String key = matcher.group(2); String values = matcher.group(3); // Requirements specific to product {@code product} MultiMap<String, String> reqs = getOrCreateEntry(requiredImageMap, product); for (String value : values.split("\\|")) { reqs.put(key, value); } } else { matcher = REQUIRE_PATTERN.matcher(line); if (matcher.matches()) { String key = matcher.group(1); String values = matcher.group(2); Constraint c = null; if (constraints != null) { c = constraints.get(key); } // Use a null product identifier to designate requirements for all products MultiMap<String, String> reqs = getOrCreateEntry(requiredImageMap, null); for (String value : values.split("\\|")) { if ((c == null) || c.shouldAccept(value)) { reqs.put(key, value); } } } } } else { eof = true; } } return requiredImageMap; } }