| // Copyright (c) 2003-2004 Brian Wellington (bwelling@xbill.org) |
| // |
| // Copyright (C) 2003-2004 Nominum, Inc. |
| // |
| // Permission to use, copy, modify, and distribute this software for any |
| // purpose with or without fee is hereby granted, provided that the above |
| // copyright notice and this permission notice appear in all copies. |
| // |
| // THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES |
| // WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF |
| // MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR ANY |
| // SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES |
| // WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN |
| // ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT |
| // OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. |
| // |
| |
| package org.xbill.DNS; |
| |
| import java.io.*; |
| import java.net.*; |
| |
| import org.xbill.DNS.utils.*; |
| |
| /** |
| * Tokenizer is used to parse DNS records and zones from text format, |
| * |
| * @author Brian Wellington |
| * @author Bob Halley |
| */ |
| |
| public class Tokenizer { |
| |
| private static String delim = " \t\n;()\""; |
| private static String quotes = "\""; |
| |
| /** End of file */ |
| public static final int EOF = 0; |
| |
| /** End of line */ |
| public static final int EOL = 1; |
| |
| /** Whitespace; only returned when wantWhitespace is set */ |
| public static final int WHITESPACE = 2; |
| |
| /** An identifier (unquoted string) */ |
| public static final int IDENTIFIER = 3; |
| |
| /** A quoted string */ |
| public static final int QUOTED_STRING = 4; |
| |
| /** A comment; only returned when wantComment is set */ |
| public static final int COMMENT = 5; |
| |
| private PushbackInputStream is; |
| private boolean ungottenToken; |
| private int multiline; |
| private boolean quoting; |
| private String delimiters; |
| private Token current; |
| private StringBuffer sb; |
| private boolean wantClose; |
| |
| private String filename; |
| private int line; |
| |
| public static class Token { |
| /** The type of token. */ |
| public int type; |
| |
| /** The value of the token, or null for tokens without values. */ |
| public String value; |
| |
| private |
| Token() { |
| type = -1; |
| value = null; |
| } |
| |
| private Token |
| set(int type, StringBuffer value) { |
| if (type < 0) |
| throw new IllegalArgumentException(); |
| this.type = type; |
| this.value = value == null ? null : value.toString(); |
| return this; |
| } |
| |
| /** |
| * Converts the token to a string containing a representation useful |
| * for debugging. |
| */ |
| public String |
| toString() { |
| switch (type) { |
| case EOF: |
| return "<eof>"; |
| case EOL: |
| return "<eol>"; |
| case WHITESPACE: |
| return "<whitespace>"; |
| case IDENTIFIER: |
| return "<identifier: " + value + ">"; |
| case QUOTED_STRING: |
| return "<quoted_string: " + value + ">"; |
| case COMMENT: |
| return "<comment: " + value + ">"; |
| default: |
| return "<unknown>"; |
| } |
| } |
| |
| /** Indicates whether this token contains a string. */ |
| public boolean |
| isString() { |
| return (type == IDENTIFIER || type == QUOTED_STRING); |
| } |
| |
| /** Indicates whether this token contains an EOL or EOF. */ |
| public boolean |
| isEOL() { |
| return (type == EOL || type == EOF); |
| } |
| } |
| |
| static class TokenizerException extends TextParseException { |
| String message; |
| |
| public |
| TokenizerException(String filename, int line, String message) { |
| super(filename + ":" + line + ": " + message); |
| this.message = message; |
| } |
| |
| public String |
| getBaseMessage() { |
| return message; |
| } |
| } |
| |
| /** |
| * Creates a Tokenizer from an arbitrary input stream. |
| * @param is The InputStream to tokenize. |
| */ |
| public |
| Tokenizer(InputStream is) { |
| if (!(is instanceof BufferedInputStream)) |
| is = new BufferedInputStream(is); |
| this.is = new PushbackInputStream(is, 2); |
| ungottenToken = false; |
| multiline = 0; |
| quoting = false; |
| delimiters = delim; |
| current = new Token(); |
| sb = new StringBuffer(); |
| filename = "<none>"; |
| line = 1; |
| } |
| |
| /** |
| * Creates a Tokenizer from a string. |
| * @param s The String to tokenize. |
| */ |
| public |
| Tokenizer(String s) { |
| this(new ByteArrayInputStream(s.getBytes())); |
| } |
| |
| /** |
| * Creates a Tokenizer from a file. |
| * @param f The File to tokenize. |
| */ |
| public |
| Tokenizer(File f) throws FileNotFoundException { |
| this(new FileInputStream(f)); |
| wantClose = true; |
| filename = f.getName(); |
| } |
| |
| private int |
| getChar() throws IOException { |
| int c = is.read(); |
| if (c == '\r') { |
| int next = is.read(); |
| if (next != '\n') |
| is.unread(next); |
| c = '\n'; |
| } |
| if (c == '\n') |
| line++; |
| return c; |
| } |
| |
| private void |
| ungetChar(int c) throws IOException { |
| if (c == -1) |
| return; |
| is.unread(c); |
| if (c == '\n') |
| line--; |
| } |
| |
| private int |
| skipWhitespace() throws IOException { |
| int skipped = 0; |
| while (true) { |
| int c = getChar(); |
| if (c != ' ' && c != '\t') { |
| if (!(c == '\n' && multiline > 0)) { |
| ungetChar(c); |
| return skipped; |
| } |
| } |
| skipped++; |
| } |
| } |
| |
| private void |
| checkUnbalancedParens() throws TextParseException { |
| if (multiline > 0) |
| throw exception("unbalanced parentheses"); |
| } |
| |
| /** |
| * Gets the next token from a tokenizer. |
| * @param wantWhitespace If true, leading whitespace will be returned as a |
| * token. |
| * @param wantComment If true, comments are returned as tokens. |
| * @return The next token in the stream. |
| * @throws TextParseException The input was invalid. |
| * @throws IOException An I/O error occurred. |
| */ |
| public Token |
| get(boolean wantWhitespace, boolean wantComment) throws IOException { |
| int type; |
| int c; |
| |
| if (ungottenToken) { |
| ungottenToken = false; |
| if (current.type == WHITESPACE) { |
| if (wantWhitespace) |
| return current; |
| } else if (current.type == COMMENT) { |
| if (wantComment) |
| return current; |
| } else { |
| if (current.type == EOL) |
| line++; |
| return current; |
| } |
| } |
| int skipped = skipWhitespace(); |
| if (skipped > 0 && wantWhitespace) |
| return current.set(WHITESPACE, null); |
| type = IDENTIFIER; |
| sb.setLength(0); |
| while (true) { |
| c = getChar(); |
| if (c == -1 || delimiters.indexOf(c) != -1) { |
| if (c == -1) { |
| if (quoting) |
| throw exception("EOF in " + |
| "quoted string"); |
| else if (sb.length() == 0) |
| return current.set(EOF, null); |
| else |
| return current.set(type, sb); |
| } |
| if (sb.length() == 0 && type != QUOTED_STRING) { |
| if (c == '(') { |
| multiline++; |
| skipWhitespace(); |
| continue; |
| } else if (c == ')') { |
| if (multiline <= 0) |
| throw exception("invalid " + |
| "close " + |
| "parenthesis"); |
| multiline--; |
| skipWhitespace(); |
| continue; |
| } else if (c == '"') { |
| if (!quoting) { |
| quoting = true; |
| delimiters = quotes; |
| type = QUOTED_STRING; |
| } else { |
| quoting = false; |
| delimiters = delim; |
| skipWhitespace(); |
| } |
| continue; |
| } else if (c == '\n') { |
| return current.set(EOL, null); |
| } else if (c == ';') { |
| while (true) { |
| c = getChar(); |
| if (c == '\n' || c == -1) |
| break; |
| sb.append((char)c); |
| } |
| if (wantComment) { |
| ungetChar(c); |
| return current.set(COMMENT, sb); |
| } else if (c == -1 && |
| type != QUOTED_STRING) |
| { |
| checkUnbalancedParens(); |
| return current.set(EOF, null); |
| } else if (multiline > 0) { |
| skipWhitespace(); |
| sb.setLength(0); |
| continue; |
| } else |
| return current.set(EOL, null); |
| } else |
| throw new IllegalStateException(); |
| } else |
| ungetChar(c); |
| break; |
| } else if (c == '\\') { |
| c = getChar(); |
| if (c == -1) |
| throw exception("unterminated escape sequence"); |
| sb.append('\\'); |
| } else if (quoting && c == '\n') { |
| throw exception("newline in quoted string"); |
| } |
| sb.append((char)c); |
| } |
| if (sb.length() == 0 && type != QUOTED_STRING) { |
| checkUnbalancedParens(); |
| return current.set(EOF, null); |
| } |
| return current.set(type, sb); |
| } |
| |
| /** |
| * Gets the next token from a tokenizer, ignoring whitespace and comments. |
| * @return The next token in the stream. |
| * @throws TextParseException The input was invalid. |
| * @throws IOException An I/O error occurred. |
| */ |
| public Token |
| get() throws IOException { |
| return get(false, false); |
| } |
| |
| /** |
| * Returns a token to the stream, so that it will be returned by the next call |
| * to get(). |
| * @throws IllegalStateException There are already ungotten tokens. |
| */ |
| public void |
| unget() { |
| if (ungottenToken) |
| throw new IllegalStateException |
| ("Cannot unget multiple tokens"); |
| if (current.type == EOL) |
| line--; |
| ungottenToken = true; |
| } |
| |
| /** |
| * Gets the next token from a tokenizer and converts it to a string. |
| * @return The next token in the stream, as a string. |
| * @throws TextParseException The input was invalid or not a string. |
| * @throws IOException An I/O error occurred. |
| */ |
| public String |
| getString() throws IOException { |
| Token next = get(); |
| if (!next.isString()) { |
| throw exception("expected a string"); |
| } |
| return next.value; |
| } |
| |
| private String |
| _getIdentifier(String expected) throws IOException { |
| Token next = get(); |
| if (next.type != IDENTIFIER) |
| throw exception("expected " + expected); |
| return next.value; |
| } |
| |
| /** |
| * Gets the next token from a tokenizer, ensures it is an unquoted string, |
| * and converts it to a string. |
| * @return The next token in the stream, as a string. |
| * @throws TextParseException The input was invalid or not an unquoted string. |
| * @throws IOException An I/O error occurred. |
| */ |
| public String |
| getIdentifier() throws IOException { |
| return _getIdentifier("an identifier"); |
| } |
| |
| /** |
| * Gets the next token from a tokenizer and converts it to a long. |
| * @return The next token in the stream, as a long. |
| * @throws TextParseException The input was invalid or not a long. |
| * @throws IOException An I/O error occurred. |
| */ |
| public long |
| getLong() throws IOException { |
| String next = _getIdentifier("an integer"); |
| if (!Character.isDigit(next.charAt(0))) |
| throw exception("expected an integer"); |
| try { |
| return Long.parseLong(next); |
| } catch (NumberFormatException e) { |
| throw exception("expected an integer"); |
| } |
| } |
| |
| /** |
| * Gets the next token from a tokenizer and converts it to an unsigned 32 bit |
| * integer. |
| * @return The next token in the stream, as an unsigned 32 bit integer. |
| * @throws TextParseException The input was invalid or not an unsigned 32 |
| * bit integer. |
| * @throws IOException An I/O error occurred. |
| */ |
| public long |
| getUInt32() throws IOException { |
| long l = getLong(); |
| if (l < 0 || l > 0xFFFFFFFFL) |
| throw exception("expected an 32 bit unsigned integer"); |
| return l; |
| } |
| |
| /** |
| * Gets the next token from a tokenizer and converts it to an unsigned 16 bit |
| * integer. |
| * @return The next token in the stream, as an unsigned 16 bit integer. |
| * @throws TextParseException The input was invalid or not an unsigned 16 |
| * bit integer. |
| * @throws IOException An I/O error occurred. |
| */ |
| public int |
| getUInt16() throws IOException { |
| long l = getLong(); |
| if (l < 0 || l > 0xFFFFL) |
| throw exception("expected an 16 bit unsigned integer"); |
| return (int) l; |
| } |
| |
| /** |
| * Gets the next token from a tokenizer and converts it to an unsigned 8 bit |
| * integer. |
| * @return The next token in the stream, as an unsigned 8 bit integer. |
| * @throws TextParseException The input was invalid or not an unsigned 8 |
| * bit integer. |
| * @throws IOException An I/O error occurred. |
| */ |
| public int |
| getUInt8() throws IOException { |
| long l = getLong(); |
| if (l < 0 || l > 0xFFL) |
| throw exception("expected an 8 bit unsigned integer"); |
| return (int) l; |
| } |
| |
| /** |
| * Gets the next token from a tokenizer and parses it as a TTL. |
| * @return The next token in the stream, as an unsigned 32 bit integer. |
| * @throws TextParseException The input was not valid. |
| * @throws IOException An I/O error occurred. |
| * @see TTL |
| */ |
| public long |
| getTTL() throws IOException { |
| String next = _getIdentifier("a TTL value"); |
| try { |
| return TTL.parseTTL(next); |
| } |
| catch (NumberFormatException e) { |
| throw exception("expected a TTL value"); |
| } |
| } |
| |
| /** |
| * Gets the next token from a tokenizer and parses it as if it were a TTL. |
| * @return The next token in the stream, as an unsigned 32 bit integer. |
| * @throws TextParseException The input was not valid. |
| * @throws IOException An I/O error occurred. |
| * @see TTL |
| */ |
| public long |
| getTTLLike() throws IOException { |
| String next = _getIdentifier("a TTL-like value"); |
| try { |
| return TTL.parse(next, false); |
| } |
| catch (NumberFormatException e) { |
| throw exception("expected a TTL-like value"); |
| } |
| } |
| |
| /** |
| * Gets the next token from a tokenizer and converts it to a name. |
| * @param origin The origin to append to relative names. |
| * @return The next token in the stream, as a name. |
| * @throws TextParseException The input was invalid or not a valid name. |
| * @throws IOException An I/O error occurred. |
| * @throws RelativeNameException The parsed name was relative, even with the |
| * origin. |
| * @see Name |
| */ |
| public Name |
| getName(Name origin) throws IOException { |
| String next = _getIdentifier("a name"); |
| try { |
| Name name = Name.fromString(next, origin); |
| if (!name.isAbsolute()) |
| throw new RelativeNameException(name); |
| return name; |
| } |
| catch (TextParseException e) { |
| throw exception(e.getMessage()); |
| } |
| } |
| |
| /** |
| * Gets the next token from a tokenizer and converts it to an IP Address. |
| * @param family The address family. |
| * @return The next token in the stream, as an InetAddress |
| * @throws TextParseException The input was invalid or not a valid address. |
| * @throws IOException An I/O error occurred. |
| * @see Address |
| */ |
| public InetAddress |
| getAddress(int family) throws IOException { |
| String next = _getIdentifier("an address"); |
| try { |
| return Address.getByAddress(next, family); |
| } |
| catch (UnknownHostException e) { |
| throw exception(e.getMessage()); |
| } |
| } |
| |
| /** |
| * Gets the next token from a tokenizer, which must be an EOL or EOF. |
| * @throws TextParseException The input was invalid or not an EOL or EOF token. |
| * @throws IOException An I/O error occurred. |
| */ |
| public void |
| getEOL() throws IOException { |
| Token next = get(); |
| if (next.type != EOL && next.type != EOF) { |
| throw exception("expected EOL or EOF"); |
| } |
| } |
| |
| /** |
| * Returns a concatenation of the remaining strings from a Tokenizer. |
| */ |
| private String |
| remainingStrings() throws IOException { |
| StringBuffer buffer = null; |
| while (true) { |
| Tokenizer.Token t = get(); |
| if (!t.isString()) |
| break; |
| if (buffer == null) |
| buffer = new StringBuffer(); |
| buffer.append(t.value); |
| } |
| unget(); |
| if (buffer == null) |
| return null; |
| return buffer.toString(); |
| } |
| |
| /** |
| * Gets the remaining string tokens until an EOL/EOF is seen, concatenates |
| * them together, and converts the base64 encoded data to a byte array. |
| * @param required If true, an exception will be thrown if no strings remain; |
| * otherwise null be be returned. |
| * @return The byte array containing the decoded strings, or null if there |
| * were no strings to decode. |
| * @throws TextParseException The input was invalid. |
| * @throws IOException An I/O error occurred. |
| */ |
| public byte [] |
| getBase64(boolean required) throws IOException { |
| String s = remainingStrings(); |
| if (s == null) { |
| if (required) |
| throw exception("expected base64 encoded string"); |
| else |
| return null; |
| } |
| byte [] array = base64.fromString(s); |
| if (array == null) |
| throw exception("invalid base64 encoding"); |
| return array; |
| } |
| |
| /** |
| * Gets the remaining string tokens until an EOL/EOF is seen, concatenates |
| * them together, and converts the base64 encoded data to a byte array. |
| * @return The byte array containing the decoded strings, or null if there |
| * were no strings to decode. |
| * @throws TextParseException The input was invalid. |
| * @throws IOException An I/O error occurred. |
| */ |
| public byte [] |
| getBase64() throws IOException { |
| return getBase64(false); |
| } |
| |
| /** |
| * Gets the remaining string tokens until an EOL/EOF is seen, concatenates |
| * them together, and converts the hex encoded data to a byte array. |
| * @param required If true, an exception will be thrown if no strings remain; |
| * otherwise null be be returned. |
| * @return The byte array containing the decoded strings, or null if there |
| * were no strings to decode. |
| * @throws TextParseException The input was invalid. |
| * @throws IOException An I/O error occurred. |
| */ |
| public byte [] |
| getHex(boolean required) throws IOException { |
| String s = remainingStrings(); |
| if (s == null) { |
| if (required) |
| throw exception("expected hex encoded string"); |
| else |
| return null; |
| } |
| byte [] array = base16.fromString(s); |
| if (array == null) |
| throw exception("invalid hex encoding"); |
| return array; |
| } |
| |
| /** |
| * Gets the remaining string tokens until an EOL/EOF is seen, concatenates |
| * them together, and converts the hex encoded data to a byte array. |
| * @return The byte array containing the decoded strings, or null if there |
| * were no strings to decode. |
| * @throws TextParseException The input was invalid. |
| * @throws IOException An I/O error occurred. |
| */ |
| public byte [] |
| getHex() throws IOException { |
| return getHex(false); |
| } |
| |
| /** |
| * Gets the next token from a tokenizer and decodes it as hex. |
| * @return The byte array containing the decoded string. |
| * @throws TextParseException The input was invalid. |
| * @throws IOException An I/O error occurred. |
| */ |
| public byte [] |
| getHexString() throws IOException { |
| String next = _getIdentifier("a hex string"); |
| byte [] array = base16.fromString(next); |
| if (array == null) |
| throw exception("invalid hex encoding"); |
| return array; |
| } |
| |
| /** |
| * Gets the next token from a tokenizer and decodes it as base32. |
| * @param b32 The base32 context to decode with. |
| * @return The byte array containing the decoded string. |
| * @throws TextParseException The input was invalid. |
| * @throws IOException An I/O error occurred. |
| */ |
| public byte [] |
| getBase32String(base32 b32) throws IOException { |
| String next = _getIdentifier("a base32 string"); |
| byte [] array = b32.fromString(next); |
| if (array == null) |
| throw exception("invalid base32 encoding"); |
| return array; |
| } |
| |
| /** |
| * Creates an exception which includes the current state in the error message |
| * @param s The error message to include. |
| * @return The exception to be thrown |
| */ |
| public TextParseException |
| exception(String s) { |
| return new TokenizerException(filename, line, s); |
| } |
| |
| /** |
| * Closes any files opened by this tokenizer. |
| */ |
| public void |
| close() { |
| if (wantClose) { |
| try { |
| is.close(); |
| } |
| catch (IOException e) { |
| } |
| } |
| } |
| |
| protected void |
| finalize() { |
| close(); |
| } |
| |
| } |