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