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