1/*
2 * ProGuard -- shrinking, optimization, obfuscation, and preverification
3 *             of Java bytecode.
4 *
5 * Copyright (c) 2002-2014 Eric Lafortune (eric@graphics.cornell.edu)
6 *
7 * This program is free software; you can redistribute it and/or modify it
8 * under the terms of the GNU General Public License as published by the Free
9 * Software Foundation; either version 2 of the License, or (at your option)
10 * any later version.
11 *
12 * This program is distributed in the hope that it will be useful, but WITHOUT
13 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
14 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
15 * more details.
16 *
17 * You should have received a copy of the GNU General Public License along
18 * with this program; if not, write to the Free Software Foundation, Inc.,
19 * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
20 */
21package proguard;
22
23import java.io.*;
24
25
26/**
27 * An abstract reader of words, with the possibility to include other readers.
28 * Words are separated by spaces or broken off at delimiters. Words containing
29 * spaces or delimiters can be quoted with single or double quotes.
30 * Comments (everything starting with '#' on a single line) are ignored.
31 *
32 * @author Eric Lafortune
33 * @noinspection TailRecursion
34 */
35public abstract class WordReader
36{
37    private static final char COMMENT_CHARACTER = '#';
38
39
40    private File       baseDir;
41    private WordReader includeWordReader;
42    private String     currentLine;
43    private int        currentLineLength;
44    private int        currentIndex;
45    private String     currentWord;
46    private String     currentComments;
47
48
49    /**
50     * Creates a new WordReader with the given base directory.
51     */
52    protected WordReader(File baseDir)
53    {
54        this.baseDir = baseDir;
55    }
56
57
58    /**
59     * Sets the base directory of this reader.
60     */
61    public void setBaseDir(File baseDir)
62    {
63        if (includeWordReader != null)
64        {
65            includeWordReader.setBaseDir(baseDir);
66        }
67        else
68        {
69            this.baseDir = baseDir;
70        }
71    }
72
73
74    /**
75     * Returns the base directory of this reader, if any.
76     */
77    public File getBaseDir()
78    {
79        return includeWordReader != null ?
80            includeWordReader.getBaseDir() :
81            baseDir;
82    }
83
84
85    /**
86     * Specifies to start reading words from the given WordReader. When it is
87     * exhausted, this WordReader will continue to provide its own words.
88     *
89     * @param newIncludeWordReader the WordReader that will start reading words.
90     */
91    public void includeWordReader(WordReader newIncludeWordReader)
92    {
93        if (includeWordReader == null)
94        {
95            includeWordReader = newIncludeWordReader;
96        }
97        else
98        {
99            includeWordReader.includeWordReader(newIncludeWordReader);
100        }
101    }
102
103
104    /**
105     * Reads a word from this WordReader, or from one of its active included
106     * WordReader objects.
107     *
108     * @param isFileName return a complete line (or argument), if the word
109     *                      isn't an option (it doesn't start with '-').
110     * @return the read word.
111     */
112    public String nextWord(boolean isFileName) throws IOException
113    {
114        currentWord = null;
115
116        // See if we have an included reader to produce a word.
117        if (includeWordReader != null)
118        {
119            // Does the included word reader still produce a word?
120            currentWord = includeWordReader.nextWord(isFileName);
121            if (currentWord != null)
122            {
123                // Return it if so.
124                return currentWord;
125            }
126
127            // Otherwise close and ditch the word reader.
128            includeWordReader.close();
129            includeWordReader = null;
130        }
131
132        // Get a word from this reader.
133
134        // Skip any whitespace and comments left on the current line.
135        if (currentLine != null)
136        {
137            // Skip any leading whitespace.
138            while (currentIndex < currentLineLength &&
139                   Character.isWhitespace(currentLine.charAt(currentIndex)))
140            {
141                currentIndex++;
142            }
143
144            // Skip any comments.
145            if (currentIndex < currentLineLength &&
146                isComment(currentLine.charAt(currentIndex)))
147            {
148                currentIndex = currentLineLength;
149            }
150        }
151
152        // Make sure we have a non-blank line.
153        while (currentLine == null || currentIndex == currentLineLength)
154        {
155            currentLine = nextLine();
156            if (currentLine == null)
157            {
158                return null;
159            }
160
161            currentLineLength = currentLine.length();
162
163            // Skip any leading whitespace.
164            currentIndex = 0;
165            while (currentIndex < currentLineLength &&
166                   Character.isWhitespace(currentLine.charAt(currentIndex)))
167            {
168                currentIndex++;
169            }
170
171            // Remember any leading comments.
172            if (currentIndex < currentLineLength &&
173                isComment(currentLine.charAt(currentIndex)))
174            {
175                // Remember the comments.
176                String comment = currentLine.substring(currentIndex + 1);
177                currentComments = currentComments == null ?
178                    comment :
179                    currentComments + '\n' + comment;
180
181                // Skip the comments.
182                currentIndex = currentLineLength;
183            }
184        }
185
186        // Find the word starting at the current index.
187        int startIndex = currentIndex;
188        int endIndex;
189
190        char startChar = currentLine.charAt(startIndex);
191
192        if (isQuote(startChar))
193        {
194            // The next word is starting with a quote character.
195            // Skip the opening quote.
196            startIndex++;
197
198            // The next word is a quoted character string.
199            // Find the closing quote.
200            do
201            {
202                currentIndex++;
203
204                if (currentIndex == currentLineLength)
205                {
206                    currentWord = currentLine.substring(startIndex-1, currentIndex);
207                    throw new IOException("Missing closing quote for "+locationDescription());
208                }
209            }
210            while (currentLine.charAt(currentIndex) != startChar);
211
212            endIndex = currentIndex++;
213        }
214        else if (isFileName &&
215                 !isOption(startChar))
216        {
217            // The next word is a (possibly optional) file name.
218            // Find the end of the line, the first path separator, the first
219            // option, or the first comment.
220            while (currentIndex < currentLineLength)
221            {
222                char currentCharacter = currentLine.charAt(currentIndex);
223                if (isFileDelimiter(currentCharacter) ||
224                    ((isOption(currentCharacter) ||
225                      isComment(currentCharacter)) &&
226                     Character.isWhitespace(currentLine.charAt(currentIndex-1)))) {
227                    break;
228                }
229
230                currentIndex++;
231            }
232
233            endIndex = currentIndex;
234
235            // Trim any trailing whitespace.
236            while (endIndex > startIndex &&
237                   Character.isWhitespace(currentLine.charAt(endIndex-1)))
238            {
239                endIndex--;
240            }
241        }
242        else if (isDelimiter(startChar))
243        {
244            // The next word is a single delimiting character.
245            endIndex = ++currentIndex;
246        }
247        else
248        {
249            // The next word is a simple character string.
250            // Find the end of the line, the first delimiter, or the first
251            // white space.
252            while (currentIndex < currentLineLength)
253            {
254                char currentCharacter = currentLine.charAt(currentIndex);
255                if (isDelimiter(currentCharacter)            ||
256                    Character.isWhitespace(currentCharacter) ||
257                    isComment(currentCharacter)) {
258                    break;
259                }
260
261                currentIndex++;
262            }
263
264            endIndex = currentIndex;
265        }
266
267        // Remember and return the parsed word.
268        currentWord = currentLine.substring(startIndex, endIndex);
269
270        return currentWord;
271    }
272
273
274    /**
275     * Returns the comments collected before returning the last word.
276     * Starts collecting new comments.
277     *
278     * @return the collected comments, or <code>null</code> if there weren't any.
279     */
280    public String lastComments() throws IOException
281    {
282        if (includeWordReader == null)
283        {
284            String comments = currentComments;
285            currentComments = null;
286            return comments;
287        }
288        else
289        {
290            return includeWordReader.lastComments();
291        }
292    }
293
294
295    /**
296     * Constructs a readable description of the current position in this
297     * WordReader and its included WordReader objects.
298     *
299     * @return the description.
300     */
301    public String locationDescription()
302    {
303        return
304            (includeWordReader == null ?
305                (currentWord == null ?
306                    "end of " :
307                    "'" + currentWord + "' in " ) :
308                (includeWordReader.locationDescription() + ",\n" +
309                 "  included from ")) +
310            lineLocationDescription();
311    }
312
313
314    /**
315     * Reads a line from this WordReader, or from one of its active included
316     * WordReader objects.
317     *
318     * @return the read line.
319     */
320    protected abstract String nextLine() throws IOException;
321
322
323    /**
324     * Returns a readable description of the current WordReader position.
325     *
326     * @return the description.
327     */
328    protected abstract String lineLocationDescription();
329
330
331    /**
332     * Closes the FileWordReader.
333     */
334    public void close() throws IOException
335    {
336        // Close and ditch the included word reader, if any.
337        if (includeWordReader != null)
338        {
339            includeWordReader.close();
340            includeWordReader = null;
341        }
342    }
343
344
345    // Small utility methods.
346
347    private boolean isOption(char character)
348    {
349        return character == '-';
350    }
351
352
353    private boolean isComment(char character)
354    {
355        return character == COMMENT_CHARACTER;
356    }
357
358
359    private boolean isDelimiter(char character)
360    {
361        return character == '@' ||
362               character == '{' ||
363               character == '}' ||
364               character == '(' ||
365               character == ')' ||
366               character == ',' ||
367               character == ';' ||
368               character == File.pathSeparatorChar;
369    }
370
371
372    private boolean isFileDelimiter(char character)
373    {
374        return character == '(' ||
375               character == ')' ||
376               character == ',' ||
377               character == ';' ||
378               character == File.pathSeparatorChar;
379    }
380
381
382    private boolean isQuote(char character)
383    {
384        return character == '\'' ||
385               character == '"';
386    }
387}
388