001: /**
002: * JavaGuard -- an obfuscation package for Java classfiles.
003: *
004: * Copyright (c) 2002 Thorsten Heit (theit@gmx.de)
005: *
006: * This library is free software; you can redistribute it and/or
007: * modify it under the terms of the GNU Lesser General Public
008: * License as published by the Free Software Foundation; either
009: * version 2 of the License, or (at your option) any later version.
010: *
011: * This library is distributed in the hope that it will be useful,
012: * but WITHOUT ANY WARRANTY; without even the implied warranty of
013: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
014: * Lesser General Public License for more details.
015: *
016: * You should have received a copy of the GNU Lesser General Public
017: * License along with this library; if not, write to the Free Software
018: * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
019: *
020: * The author may be contacted at theit@gmx.de.
021: *
022: *
023: * $Id: ScriptFile.java,v 1.5 2002/05/11 18:55:54 glurk Exp $
024: */package net.sf.javaguard;
025:
026: import java.io.*;
027: import java.util.*;
028:
029: /** Parser for a JavaGuard script file.
030: *
031: * @author <a href="mailto:theit@gmx.de">Thorsten Heit</a>
032: */
033: public class ScriptFile implements ScriptConstants {
034: /** Holds all parsed entries from the script file. */
035: private Vector entries;
036:
037: /** Holds the script file name. */
038: private String name;
039:
040: /** Constructor that reads and parses the given script file.
041: * @param aScript file object for a valid JavaGuard script
042: * @throws IOException if an error occurs during reading
043: */
044: public ScriptFile(File aScript) throws IOException {
045: entries = new Vector();
046: setName(aScript.getName());
047:
048: InputStream is = null;
049: try {
050: is = new FileInputStream(aScript);
051: StreamTokenizer tk = new StreamTokenizer(
052: new BufferedReader(new InputStreamReader(is)));
053: // use our own syntax schema
054: tk.resetSyntax();
055: tk.eolIsSignificant(true);
056: // everything below ' ' should be treated as a whitespace character...
057: tk.whitespaceChars(0x00, 0x20);
058: // ...and everything else as a word character
059: tk.wordChars('!', '~');
060: // enable C/C++-style and shell script-like comments:
061: tk.slashSlashComments(true);
062: tk.slashStarComments(true);
063: tk.commentChar('#');
064: readEntries(tk);
065: } finally {
066: if (null != is) {
067: is.close();
068: }
069: }
070: }
071:
072: /** Reads all script file entries from the given input stream.
073: * @param tk the input stream for the script file
074: * @throws IOException if an error occurs during reading
075: */
076: private void readEntries(StreamTokenizer tk) throws IOException {
077: ScriptEntry scriptEntry;
078: try {
079: while (null != (scriptEntry = readNextEntry(tk))) {
080: entries.add(scriptEntry);
081: }
082: } catch (IOException ex) {
083: IOException ioex = new IOException("Parse error at line "
084: + tk.lineno() + ": " + ex.getMessage() + ".");
085: ioex.fillInStackTrace();
086: throw ioex;
087: } catch (IllegalArgumentException iaex) {
088: IOException ioex = new IOException("Parse error at line "
089: + tk.lineno() + ": " + iaex.getMessage() + ".");
090: ioex.fillInStackTrace();
091: throw ioex;
092: }
093: }
094:
095: /** Read the next entry in the script file.
096: * @param tk the valid input stream for the script file
097: * @return next entry in the script file; null if nothing more is available
098: * @throws IOException if an error occurs during reading
099: * @throws IllegalArgumentException if an error occurs during parsing
100: */
101: private ScriptEntry readNextEntry(StreamTokenizer tk)
102: throws IOException, IllegalArgumentException {
103: // Reset the 'next error' state
104: ScriptEntry entry = null;
105:
106: // read the next token in the stream
107: readNextToken(tk);
108: if (tk.ttype == StreamTokenizer.TT_EOF)
109: return null;
110:
111: // parse and validate the actual token (which must be a script file directive)
112: int type = parseDirective(tk);
113: if (type < 0) {
114: throw new IllegalArgumentException(
115: "Unknown script file directive");
116: }
117:
118: // each script directive requires at least one additional word following it
119: readNextTokenInLine(tk);
120: entry = new ScriptEntry(type, tk.sval);
121:
122: // depending on the script directive read additional parameters
123: switch (type) {
124: case TYPE_ATTRIBUTE:
125: if (!Tools.isInArrayIgnoreCase(entry.getName(), validAttrs)) {
126: throw new IllegalArgumentException(
127: "Unknown attribute option specified");
128: }
129: // optionally read an additional info string for the attribute
130: if (conditionalReadNextTokenInLine(tk)) {
131: entry.setInfo(tk.sval);
132: }
133: break;
134:
135: case TYPE_RENAME:
136: if (!Tools
137: .isInArrayIgnoreCase(entry.getName(), renameAttrs)) {
138: throw new IllegalArgumentException(
139: "Unknown rename option specified");
140: }
141: // optionally read an additional info string for the attribute
142: if (conditionalReadNextTokenInLine(tk)) {
143: entry.setInfo(tk.sval);
144: }
145: break;
146:
147: case TYPE_PRESERVE:
148: if (!Tools.isInArrayIgnoreCase(entry.getName(),
149: preserveAttrs)) {
150: throw new IllegalArgumentException(
151: "Unknown preserve option specified");
152: }
153: if (conditionalReadNextTokenInLine(tk)) {
154: entry.setInfo(tk.sval);
155: }
156: break;
157:
158: case TYPE_PACKAGE:
159: // read additional options, the same as for a ".class" directive
160: readClassOptions(tk, entry);
161: break;
162:
163: case TYPE_PACKAGE_MAP:
164: // read the mapping name for a ".package_map" directive
165: readNextTokenInLine(tk);
166: checkJavaIdentifier(tk.sval);
167: entry.setMappedName(tk.sval);
168: // read additional options, the same as for a ".class" directive
169: readClassOptions(tk, entry);
170: break;
171:
172: case TYPE_CLASS:
173: // read additional options for a ".class" directive
174: readClassOptions(tk, entry);
175: break;
176:
177: case TYPE_CLASS_MAP:
178: // read the mapping name and additional options a ".class_map" directive
179: readNextTokenInLine(tk);
180: checkJavaInnerIdentifier(tk.sval);
181: entry.setMappedName(tk.sval);
182: readClassOptions(tk, entry);
183: break;
184:
185: case TYPE_METHOD:
186: // optionally read the descriptor for the ".method" directive
187: if (conditionalReadNextTokenInLine(tk)) {
188: checkMethodDescriptor(tk.sval);
189: entry.setDescriptor(tk.sval);
190: }
191: break;
192:
193: case TYPE_METHOD_MAP:
194: // read the descriptor for the ".method_map" directive...
195: readNextTokenInLine(tk);
196: checkMethodDescriptor(tk.sval);
197: entry.setDescriptor(tk.sval);
198: // ...and read the mapping name
199: readNextTokenInLine(tk);
200: checkJavaIdentifier(tk.sval);
201: entry.setMappedName(tk.sval);
202: break;
203:
204: case TYPE_FIELD:
205: // optionally read the descriptor for the ".field" directive
206: if (conditionalReadNextTokenInLine(tk)) {
207: checkJavaType(tk.sval);
208: entry.setDescriptor(tk.sval);
209: }
210: break;
211:
212: case TYPE_FIELD_MAP: {
213: // If there are two more tokens in the current line available the first
214: // one specifies the descriptor and the second the mapped name;
215: // otherwise the token specifies the mapped name
216: readNextTokenInLine(tk);
217: String tmp = tk.sval;
218: if (conditionalReadNextTokenInLine(tk)) {
219: // two tokens available -> first = descriptor, second = mapped name
220: checkJavaType(tmp);
221: entry.setDescriptor(tmp);
222: entry.setMappedName(tk.sval);
223: } else {
224: entry.setMappedName(tmp);
225: }
226: checkJavaIdentifier(entry.getMappedName());
227: break;
228: }
229:
230: case TYPE_IGNORE_METHOD:
231: // try to read an optional method descriptor
232: if (conditionalReadNextTokenInLine(tk)) {
233: checkMethodDescriptor(tk.sval);
234: entry.setDescriptor(tk.sval);
235: }
236: break;
237:
238: case TYPE_IGNORE_FIELD:
239: // try to read an optional field descriptor
240: if (conditionalReadNextTokenInLine(tk)) {
241: checkJavaType(tk.sval);
242: entry.setDescriptor(tk.sval);
243: }
244: break;
245:
246: case TYPE_OBFUSCATE_METHOD:
247: // try to read an optional method descriptor
248: if (conditionalReadNextTokenInLine(tk)) {
249: checkMethodDescriptor(tk.sval);
250: entry.setDescriptor(tk.sval);
251: }
252: break;
253:
254: case TYPE_OBFUSCATE_FIELD:
255: // try to read an optional field descriptor
256: if (conditionalReadNextTokenInLine(tk)) {
257: checkJavaType(tk.sval);
258: entry.setDescriptor(tk.sval);
259: }
260: break;
261: }
262:
263: return entry;
264: }
265:
266: /** Reads the next word token from the given input stream.
267: * @param tk the valid input stream
268: * @throws IOException if an error occurs during reading
269: */
270: private void readNextToken(StreamTokenizer tk) throws IOException {
271: int token = StreamTokenizer.TT_EOL;
272:
273: // read the next word token
274: while (token == StreamTokenizer.TT_EOL) {
275: token = tk.nextToken();
276: }
277: }
278:
279: /** Reads the next word token in the current line in the given input stream.
280: * @param tk the valid input stream
281: * @throws IOException if an error occurs during reading
282: */
283: private void readNextTokenInLine(StreamTokenizer tk)
284: throws IOException {
285: int token = StreamTokenizer.TT_NUMBER;
286:
287: // read the next token
288: while (token == StreamTokenizer.TT_NUMBER) {
289: token = tk.nextToken();
290: }
291: if (token != StreamTokenizer.TT_WORD) {
292: throw new IOException("Unexpected end of file at line "
293: + tk.lineno() + " of script file.");
294: }
295: }
296:
297: /** Tries to read the next available word in the current line in the input
298: * stream.
299: * @return true if the current line contains one more word; false else
300: * @param tk the input stream
301: * @throws IOException if an error occurs during reading
302: */
303: private boolean conditionalReadNextTokenInLine(StreamTokenizer tk)
304: throws IOException {
305: int token = StreamTokenizer.TT_NUMBER;
306:
307: // read the next token
308: while (token == StreamTokenizer.TT_NUMBER) {
309: token = tk.nextToken();
310: }
311: // if the next token is not a word push it back to the input stream
312: if (token != StreamTokenizer.TT_WORD) {
313: tk.pushBack();
314: return false;
315: }
316: return true;
317: }
318:
319: /** Read and parse optional parameters that can be specified in <code>.class</code>
320: * or <code>.class_map</code> directives.
321: * @param tk the valid input stream
322: * @param entry the current script file entry
323: * @throws IOException if an error occurs during reading
324: * @throws IllegalArgumentException if an illegal or an invalid class option
325: * is found
326: */
327: private void readClassOptions(StreamTokenizer tk, ScriptEntry entry)
328: throws IOException, IllegalArgumentException {
329: while (conditionalReadNextTokenInLine(tk)) {
330: int subtype;
331: switch (subtype = parseToken(tk.sval, options, optionTypes)) {
332: case OPTION_TYPE_PUBLIC:
333: entry.setRetainPublic(true);
334: break;
335:
336: case OPTION_TYPE_PROTECTED:
337: entry.setRetainProtected(true);
338: break;
339:
340: case OPTION_TYPE_METHOD:
341: entry.setRetainMethods(true);
342: break;
343:
344: case OPTION_TYPE_FIELD:
345: entry.setRetainFields(true);
346: break;
347:
348: default:
349: throw new IllegalArgumentException(
350: "Unknown class option");
351: }
352: }
353: // check whether the "field" or "method" option are specified together
354: // with the "public" or "protected" option
355: if ((entry.canRetainFields() || entry.canRetainMethods())
356: && !entry.canRetainPublic()
357: && !entry.canRetainProtected()) {
358: throw new IllegalArgumentException(
359: "Missing option \"public\" or \"protected\"");
360: }
361: // check whether the "public" or "protected" keywords were given
362: // without any "field" or "method" modifier
363: if ((entry.canRetainPublic() || entry.canRetainProtected())
364: && !entry.canRetainFields()
365: && !entry.canRetainMethods()) {
366: // no "method" and "field" modifier are specified
367: // -> all public/protected elements should be retained
368: entry.setRetainFields(true);
369: entry.setRetainMethods(true);
370: }
371: }
372:
373: /** Checks whether the current token is a valid script file directive.
374: * @return the code of the directive if the token is a valid script file
375: * directive; -1 else
376: * @param tk the input stream
377: */
378: private int parseDirective(StreamTokenizer tk) {
379: if (tk.ttype == StreamTokenizer.TT_WORD) {
380: return parseToken(tk.sval, directives, directiveTypes);
381: }
382: return -1;
383: }
384:
385: /** Checks whether a string exists in an array of strings. If yes return an
386: * <code>int</code> value representing the string code.
387: * @param str The string to check
388: * @param list An array of strings; may not be null
389: * @param codes An array of <code>int</code> values representing the string
390: * codes. Must have the same size as the <code>list</code> parameter.
391: * @return An <code>int</code> value from the <code>codes</code> array if
392: * the string exists in the list; -1 else
393: */
394: private int parseToken(String str, String[] list, int[] codes) {
395: for (int i = 0; i < list.length; i++) {
396: if (str.equalsIgnoreCase(list[i])) {
397: return codes[i];
398: }
399: }
400: return -1;
401: }
402:
403: /** Checks whether a given method descriptor is valid.
404: * @param s the string to check
405: * @throws IllegalArgumentException if the descriptor is invalid
406: */
407: private void checkMethodDescriptor(String s)
408: throws IllegalArgumentException {
409: if (s.length() == 0 || s.charAt(0) != '(') {
410: throw new IllegalArgumentException(
411: "Empty or invalid method descriptor.");
412: }
413: s = s.substring(1);
414:
415: // Check each type
416: while (s.length() > 0 && s.charAt(0) != ')') {
417: s = checkFirstJavaType(s);
418: }
419: checkJavaType(s.substring(1));
420: }
421:
422: /** Checks whether the first Java type is valid.
423: * @param s the string to check
424: * @throws IllegalArgumentException if the first type is invalid.
425: * @return all but first type in the string
426: */
427: private String checkFirstJavaType(String s)
428: throws IllegalArgumentException {
429: // Pull off the array specifiers
430: while (s.charAt(0) == '[') {
431: s = s.substring(1);
432: if (s.length() == 0) {
433: throw new IllegalArgumentException("Invalid Java type");
434: }
435: }
436:
437: // Check a type
438: int pos = 0;
439: switch (s.charAt(0)) {
440: case 'B':
441: case 'C':
442: case 'D':
443: case 'F':
444: case 'I':
445: case 'J':
446: case 'S':
447: case 'V':
448: case 'Z':
449: break;
450:
451: case 'L':
452: pos = s.indexOf(';');
453: if (pos == -1) {
454: throw new IllegalArgumentException(
455: "Invalid class or interface type specification");
456: }
457: // Check the class type
458: checkClassSpec(s.substring(0, pos));
459: break;
460:
461: default:
462: throw new IllegalArgumentException("Unknown Java type");
463: }
464: return s.substring(pos + 1);
465: }
466:
467: /** Checks whether the Java type is valid.
468: * @param s the string to check
469: * @throws IllegalArgumentException if the string is invalid
470: */
471: private void checkJavaType(String s)
472: throws IllegalArgumentException {
473: if (!checkFirstJavaType(s).equals("")) {
474: throw new IllegalArgumentException("Invalid Java type");
475: }
476: }
477:
478: /** Checks whether the given string specifies a correct class specification.
479: * @param s the string to check
480: * @throws IllegalArgumentException if the string is invalid
481: */
482: private void checkClassSpec(String s)
483: throws IllegalArgumentException {
484: if (s.length() == 0) {
485: throw new IllegalArgumentException(
486: "Class specification may not be empty");
487: }
488:
489: int pos = -1;
490: // check all possible inner classes first
491: while ((pos = s.lastIndexOf('$')) != -1) {
492: checkJavaInnerIdentifier(s.substring(pos + 1));
493: s = s.substring(0, pos);
494: }
495: // now check all class and package names
496: while ((pos = s.lastIndexOf('/')) != -1) {
497: checkJavaIdentifier(s.substring(pos + 1));
498: s = s.substring(0, pos);
499: }
500: checkJavaIdentifier(s);
501: }
502:
503: /** Checks whether the given string is a valid Java identifier.
504: * @param s the string to check
505: * @throws IllegalArgumentException if the string is invalid
506: */
507: private void checkJavaIdentifier(String s)
508: throws IllegalArgumentException {
509: if (s.length() == 0
510: || !Character.isJavaIdentifierStart(s.charAt(0))) {
511: throw new IllegalArgumentException(
512: "Identifier empty or invalid start character");
513: }
514: for (int i = 1; i < s.length(); i++) {
515: if (!Character.isJavaIdentifierPart(s.charAt(i))) {
516: throw new IllegalArgumentException(
517: "Invalid character inside the identifier");
518: }
519: }
520: }
521:
522: /** Checks whether the given string is a valid Java identifier. Allows
523: * anonymous inner class names like '4'.
524: * @param s the string to check
525: * @throws IllegalArgumentException if the string is invalid
526: */
527: private void checkJavaInnerIdentifier(String s)
528: throws IllegalArgumentException {
529: if (s.length() == 0) {
530: throw new IllegalArgumentException(
531: "Identifier may not be empty");
532: }
533: for (int i = 0; i < s.length(); i++) {
534: if (!Character.isJavaIdentifierPart(s.charAt(i))) {
535: throw new IllegalArgumentException(
536: "Invalid character inside the identifier");
537: }
538: }
539: }
540:
541: /** Returns an iterator over the elements in the script file in proper
542: * sequence.
543: * @return an iterator over the elements in this list in proper sequence.
544: */
545: public Iterator iterator() {
546: return entries.iterator();
547: }
548:
549: /** Stores the script file name.
550: * @param aName the script file name
551: * @see #getName
552: */
553: public void setName(String aName) {
554: this .name = aName;
555: }
556:
557: /** Returns the current script file name.
558: * @return script file name
559: * @see #setName
560: */
561: public String getName() {
562: return name;
563: }
564: }
|