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 package com.android.tradefed.command;
17 
18 import com.android.tradefed.config.ConfigurationException;
19 import com.android.tradefed.log.LogUtil.CLog;
20 import com.android.tradefed.util.QuotationAwareTokenizer;
21 
22 import java.io.BufferedReader;
23 import java.io.File;
24 import java.io.FileReader;
25 import java.io.IOException;
26 import java.util.Arrays;
27 import java.util.Collection;
28 import java.util.HashMap;
29 import java.util.HashSet;
30 import java.util.LinkedList;
31 import java.util.List;
32 import java.util.Map;
33 import java.util.Objects;
34 import java.util.regex.Matcher;
35 import java.util.regex.Pattern;
36 
37 /**
38  * Parser for file that contains set of command lines.
39  * <p/>
40  * The syntax of the given file should be series of lines. Each line is a command; that is, a
41  * configuration plus its options:
42  * <pre>
43  *   [options] config-name
44  *   [options] config-name2
45  *   ...
46  * </pre>
47  */
48 public class CommandFileParser {
49 
50     /**
51      * A pattern that matches valid macro usages and captures the name of the macro.
52      * Macro names must start with an alpha character, and may contain alphanumerics, underscores,
53      * or hyphens.
54      */
55     private static final Pattern MACRO_PATTERN = Pattern.compile("([a-z][a-z0-9_-]*)\\(\\)",
56             Pattern.CASE_INSENSITIVE);
57 
58     private Map<String, CommandLine> mMacros = new HashMap<String, CommandLine>();
59     private Map<String, List<CommandLine>> mLongMacros = new HashMap<String, List<CommandLine>>();
60     private List<CommandLine> mLines = new LinkedList<CommandLine>();
61 
62     private Collection<String> mIncludedFiles = new HashSet<String>();
63 
64     @SuppressWarnings("serial")
65     public static class CommandLine extends LinkedList<String> {
66         private final File mFile;
67         private final int mLineNumber;
68 
CommandLine(File file, int lineNumber)69         CommandLine(File file, int lineNumber) {
70             super();
71             mFile = file;
72             mLineNumber = lineNumber;
73         }
74 
CommandLine(Collection<? extends String> c, File file, int lineNumber)75         CommandLine(Collection<? extends String> c, File file, int lineNumber) {
76             super(c);
77             mFile = file;
78             mLineNumber = lineNumber;
79         }
80 
asArray()81         public String[] asArray() {
82             String[] arrayContents = new String[size()];
83             int i = 0;
84             for (String a : this) {
85                 arrayContents[i] = a;
86                 i++;
87             }
88             return arrayContents;
89         }
90 
getFile()91         public File getFile() {
92             return mFile;
93         }
94 
getLineNumber()95         public int getLineNumber() {
96             return mLineNumber;
97         }
98 
99         @Override
equals(Object o)100         public boolean equals(Object o) {
101             if(o instanceof CommandLine) {
102                 CommandLine otherLine = (CommandLine) o;
103                 return super.equals(o) &&
104                         Objects.equals(otherLine.getFile(), mFile) &&
105                         otherLine.getLineNumber() == mLineNumber;
106             }
107             return false;
108         }
109 
110         @Override
hashCode()111         public int hashCode() {
112             int listHash = super.hashCode();
113             return Objects.hash(listHash, mFile, mLineNumber);
114         }
115     }
116 
117     /**
118      * Represents a bitmask.  Useful because it caches the number of bits which are set.
119      */
120     static class Bitmask {
121         private List<Boolean> mBitmask = new LinkedList<Boolean>();
122         private int mNumBitsSet = 0;
123 
Bitmask(int nBits)124         public Bitmask(int nBits) {
125             this(nBits, false);
126         }
127 
Bitmask(int nBits, boolean initialValue)128         public Bitmask(int nBits, boolean initialValue) {
129             for (int i = 0; i < nBits; ++i) {
130                 mBitmask.add(initialValue);
131             }
132             if (initialValue) {
133                 mNumBitsSet = nBits;
134             }
135         }
136 
137         /**
138          * Return the number of bits which are set (rather than unset)
139          */
getSetCount()140         public int getSetCount() {
141             return mNumBitsSet;
142         }
143 
get(int idx)144         public boolean get(int idx) {
145             return mBitmask.get(idx);
146         }
147 
set(int idx)148         public boolean set(int idx) {
149             boolean retVal = mBitmask.set(idx, true);
150             if (!retVal) {
151                 mNumBitsSet++;
152             }
153             return retVal;
154         }
155 
unset(int idx)156         public boolean unset(int idx) {
157             boolean retVal = mBitmask.set(idx, false);
158             if (retVal) {
159                 mNumBitsSet--;
160             }
161             return retVal;
162         }
163 
remove(int idx)164         public boolean remove(int idx) {
165             boolean retVal = mBitmask.remove(idx);
166             if (retVal) {
167                 mNumBitsSet--;
168             }
169             return retVal;
170         }
171 
add(int idx, boolean val)172         public void add(int idx, boolean val) {
173             mBitmask.add(idx, val);
174             if (val) {
175                 mNumBitsSet++;
176             }
177         }
178 
179         /**
180          * Insert a bunch of identical values in the specified spot in the mask
181          *
182          * @param idx the index where the first new value should be set.
183          * @param count the number of new values to insert
184          * @param val the parity of the new values
185          */
addN(int idx, int count, boolean val)186         public void addN(int idx, int count, boolean val) {
187             for (int i = 0; i < count; ++i) {
188                 add(idx, val);
189             }
190         }
191     }
192 
193     /**
194      * Checks if a line matches the expected format for a (short) macro:
195      * MACRO (name) = (token) [(token)...]
196      * This method verifies that:
197      * <ol>
198      *   <li>Line is at least four tokens long</li>
199      *   <li>The first token is "MACRO" (case-sensitive)</li>
200      *   <li>The third token is an equal-sign</li>
201      * </ol>
202      *
203      * @return {@code true} if the line matches the macro format, {@false} otherwise
204      */
isLineMacro(CommandLine line)205     private static boolean isLineMacro(CommandLine line) {
206         return line.size() >= 4 && "MACRO".equals(line.get(0)) && "=".equals(line.get(2));
207     }
208 
209     /**
210      * Checks if a line matches the expected format for the opening line of a long macro:
211      * LONG MACRO (name)
212      *
213      * @return {@code true} if the line matches the long macro format, {@code false} otherwise
214      */
isLineLongMacro(CommandLine line)215     private static boolean isLineLongMacro(CommandLine line) {
216         return line.size() == 3 && "LONG".equals(line.get(0)) && "MACRO".equals(line.get(1));
217     }
218 
219     /**
220      * Checks if a line matches the expected format for an INCLUDE directive
221      *
222      * @return {@code true} if the line is an INCLUDE directive, {@code false} otherwise
223      */
isLineIncludeDirective(CommandLine line)224     private static boolean isLineIncludeDirective(CommandLine line) {
225         return line.size() == 2 && "INCLUDE".equals(line.get(0));
226     }
227 
228     /**
229      * Checks if a line should be parsed or ignored.  Basically, ignore if the line is commented
230      * or is empty.
231      *
232      * @param line A {@link String} containing the line of input to check
233      * @return {@code true} if we should parse the line, {@code false} if we should ignore it.
234      */
shouldParseLine(String line)235     private static boolean shouldParseLine(String line) {
236         line = line.trim();
237         return !(line.isEmpty() || line.startsWith("#"));
238     }
239 
240     /**
241      * Return the command files included by the last parsed command file.
242      */
getIncludedFiles()243     public Collection<String> getIncludedFiles() {
244         return mIncludedFiles;
245     }
246 
247     /**
248      * Does a single pass of the input CommandFile, storing input lines as macros, long macros, or
249      * commands.
250      *
251      * Note that this method may call itself recursively to handle the INCLUDE directive.
252      */
scanFile(File file)253     private void scanFile(File file) throws IOException, ConfigurationException {
254         if (mIncludedFiles.contains(file.getAbsolutePath())) {
255             // Repeated include; ignore
256             CLog.v("Skipping repeated include of file %s.", file.toString());
257             return;
258         } else {
259             mIncludedFiles.add(file.getAbsolutePath());
260         }
261 
262         BufferedReader fileReader = createCommandFileReader(file);
263         String inputLine = null;
264         int lineNumber = 0;
265         try {
266             while ((inputLine = fileReader.readLine()) != null) {
267                 lineNumber++;
268                 inputLine = inputLine.trim();
269                 if (shouldParseLine(inputLine)) {
270                     CommandLine lArgs = null;
271                     try {
272                         String[] args = QuotationAwareTokenizer.tokenizeLine(inputLine);
273                         lArgs = new CommandLine(Arrays.asList(args), file,
274                                 lineNumber);
275                     } catch (IllegalArgumentException e) {
276                         throw new ConfigurationException(e.getMessage());
277                     }
278 
279                     if (isLineMacro(lArgs)) {
280                         // Expected format: MACRO <name> = <token> [<token>...]
281                         String name = lArgs.get(1);
282                         CommandLine expansion = new CommandLine(lArgs.subList(3, lArgs.size()),
283                                 file, lineNumber);
284                         CommandLine prev = mMacros.put(name, expansion);
285                         if (prev != null) {
286                             CLog.w("Overwrote short macro '%s' while parsing file %s", name, file);
287                             CLog.w("value '%s' replaced previous value '%s'", expansion, prev);
288                         }
289                     } else if (isLineLongMacro(lArgs)) {
290                         // Expected format: LONG MACRO <name>\n(multiline expansion)\nEND MACRO
291                         String name = lArgs.get(2);
292                         List<CommandLine> expansion = new LinkedList<CommandLine>();
293 
294                         inputLine = fileReader.readLine();
295                         lineNumber++;
296                         while (!"END MACRO".equals(inputLine)) {
297                             if (inputLine == null) {
298                                 // Syntax error
299                                 throw new ConfigurationException(String.format(
300                                         "Syntax error: Unexpected EOF while reading definition " +
301                                         "for LONG MACRO %s.", name));
302                             }
303                             if (shouldParseLine(inputLine)) {
304                                 // Store the tokenized line
305                                 CommandLine line = new CommandLine(Arrays.asList(
306                                         QuotationAwareTokenizer.tokenizeLine(inputLine)),
307                                         file, lineNumber);
308                                 expansion.add(line);
309                             }
310 
311                             // Advance
312                             inputLine = fileReader.readLine();
313                             lineNumber++;
314                         }
315                         CLog.d("Parsed %d-line definition for long macro %s", expansion.size(),
316                                 name);
317 
318                         List<CommandLine> prev = mLongMacros.put(name, expansion);
319                         if (prev != null) {
320                             CLog.w("Overwrote long macro %s while parsing file %s", name, file);
321                             CLog.w("%d-line definition replaced previous %d-line definition",
322                                     expansion.size(), prev.size());
323                         }
324                     } else if (isLineIncludeDirective(lArgs)) {
325                         File toScan = new File(lArgs.get(1));
326                         if (toScan.isAbsolute()) {
327                             CLog.d("Got an include directive for absolute path %s.", lArgs.get(1));
328                         } else {
329                             File parent = file.getParentFile();
330                             toScan = new File(parent, lArgs.get(1));
331                             CLog.d("Got an include directive for relative path %s, using '%s' " +
332                                     "for parent dir", lArgs.get(1), parent);
333                         }
334                         scanFile(toScan);
335                     } else {
336                         mLines.add(lArgs);
337                     }
338                 }
339             }
340         } finally {
341             fileReader.close();
342         }
343     }
344 
345     /**
346      * Parses the commands contained in {@code file}, doing macro expansions as necessary
347      *
348      * @param file the {@link File} to parse
349      * @return the list of parsed commands
350      * @throws IOException if failed to read file
351      * @throws ConfigurationException if content of file could not be parsed
352      */
parseFile(File file)353     public List<CommandLine> parseFile(File file) throws IOException,
354             ConfigurationException {
355         // clear state from last call
356         mIncludedFiles.clear();
357         mMacros.clear();
358         mLongMacros.clear();
359         mLines.clear();
360 
361         // Parse this cmdfile and all of its dependencies.
362         scanFile(file);
363 
364         // remove original file from list of includes, as call above has side effect of adding it to
365         // mIncludedFiles
366         mIncludedFiles.remove(file.getAbsolutePath());
367 
368         // Now perform macro expansion
369         /**
370          * inputBitmask is used to stop iterating when we're sure there are no more macros to
371          * expand.  It is a bitmask where the (k)th bit represents the (k)th element in
372          * {@code mLines.}
373          * <p>
374          * Each bit starts as {@code true}, meaning that each line in mLines may have macro calls to
375          * be expanded.  We set bits of {@code inputBitmask} to {@code false} once we've determined
376          * that the corresponding lines of {@code mLines} have been fully expanded, which allows us
377          * to skip those lines on subsequent scans.
378          * <p>
379          * {@code inputBitmaskCount} stores the quantity of {@code true} bits in
380          * {@code inputBitmask}.  Once {@code inputBitmaskCount == 0}, we are done expanding macros.
381          */
382         Bitmask inputBitmask = new Bitmask(mLines.size(), true);
383 
384         // Do a maximum of 20 iterations of expansion
385         // FIXME: make this configurable
386         for (int iCount = 0; iCount < 20 && inputBitmask.getSetCount() > 0; ++iCount) {
387             CLog.d("### Expansion iteration %d", iCount);
388 
389             int inputIdx = 0;
390             while (inputIdx < mLines.size()) {
391                 if (!inputBitmask.get(inputIdx)) {
392                     // Skip this line; we've already determined that it doesn't contain any macro
393                     // calls to be expanded.
394                     CLog.d("skipping input line %s", mLines.get(inputIdx));
395                     ++inputIdx;
396                     continue;
397                 }
398 
399                 CommandLine line = mLines.get(inputIdx);
400                 boolean sawMacro = expandMacro(line);
401                 List<CommandLine> longMacroExpansion = expandLongMacro(line, !sawMacro);
402 
403                 if (longMacroExpansion == null) {
404                     if (sawMacro) {
405                         // We saw and expanded a short macro.  This may have pulled in another macro
406                         // to expand, so leave inputBitmask alone.
407                     } else {
408                         // We did not find any macros (long or short) to expand, thus all expansions
409                         // are done for this CommandLine.  Update inputBitmask appropriately.
410                         inputBitmask.unset(inputIdx);
411                     }
412 
413                     // Finally, advance.
414                     ++inputIdx;
415                 } else {
416                     // We expanded a long macro.  First, actually insert the expansion in place of
417                     // the macro call
418                     mLines.remove(inputIdx);
419                     inputBitmask.remove(inputIdx);
420                     mLines.addAll(inputIdx, longMacroExpansion);
421                     inputBitmask.addN(inputIdx, longMacroExpansion.size(), true);
422 
423                     // And advance past the end of the expanded macro
424                     inputIdx += longMacroExpansion.size();
425                 }
426             }
427         }
428         return mLines;
429     }
430 
431     /**
432      * Performs one level of macro expansion for the first macro used in the line
433      */
expandLongMacro(CommandLine line, boolean checkMissingMacro)434     private List<CommandLine> expandLongMacro(CommandLine line, boolean checkMissingMacro)
435             throws ConfigurationException {
436         for (int idx = 0; idx < line.size(); ++idx) {
437             String token = line.get(idx);
438             Matcher matchMacro = MACRO_PATTERN.matcher(token);
439             if (matchMacro.matches()) {
440                 // we hit a macro; expand it
441                 List<CommandLine> expansion = new LinkedList<CommandLine>();
442                 String name = matchMacro.group(1);
443                 List<CommandLine> longMacro = mLongMacros.get(name);
444                 if (longMacro == null) {
445                     if (checkMissingMacro) {
446                         // If the expandMacro method hits an unrecognized macro, it will leave it in
447                         // the stream for this method.  If it's not recognized here, throw an
448                         // exception
449                         throw new ConfigurationException(String.format(
450                                 "Macro call '%s' does not match any macro definitions.", name));
451                     } else {
452                         // At this point, it may just be a short macro
453                         CLog.d("Macro call '%s' doesn't match any long macro definitions.", name);
454                         return null;
455                     }
456                 }
457 
458                 LinkedList<String> prefix = new LinkedList<>(line.subList(0, idx));
459                 LinkedList<String> suffix = new LinkedList<>(line.subList(idx, line.size()));
460                 suffix.remove(0);
461                 for (CommandLine macroLine : longMacro) {
462                     CommandLine expanded = new CommandLine(line.getFile(),
463                             line.getLineNumber());
464                     expanded.addAll(prefix);
465                     expanded.addAll(macroLine);
466                     expanded.addAll(suffix);
467                     expansion.add(expanded);
468                 }
469 
470                 // Only expand a single macro usage at a time
471                 return expansion;
472             }
473         }
474         return null;
475     }
476 
477     /**
478      * Performs one level of macro expansion for every macro used in the line
479      *
480      * @return {@code true} if a macro was found and expanded, {@code false} if no macro was found
481      */
expandMacro(CommandLine line)482     private boolean expandMacro(CommandLine line) {
483         boolean sawMacro = false;
484 
485         int idx = 0;
486         while (idx < line.size()) {
487             String token = line.get(idx);
488             Matcher matchMacro = MACRO_PATTERN.matcher(token);
489             if (matchMacro.matches() && mMacros.containsKey(matchMacro.group(1))) {
490                 // we hit a macro; expand it
491                 String name = matchMacro.group(1);
492                 CommandLine macro = mMacros.get(name);
493                 CLog.d("Gotcha!  Expanding macro '%s' to '%s'", name, macro);
494                 line.remove(idx);
495                 line.addAll(idx, macro);
496                 idx += macro.size();
497                 sawMacro = true;
498             } else {
499                 ++idx;
500             }
501         }
502         return sawMacro;
503     }
504 
505     /**
506      * Create a reader for the command file data.
507      * <p/>
508      * Exposed for unit testing.
509      *
510      * @param file the command {@link File}
511      * @return the {@link BufferedReader}
512      * @throws IOException if failed to read data
513      */
createCommandFileReader(File file)514     BufferedReader createCommandFileReader(File file) throws IOException {
515         return new BufferedReader(new FileReader(file));
516     }
517 }
518