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