| // Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org) |
| |
| package org.xbill.DNS; |
| |
| import java.io.*; |
| import java.util.*; |
| |
| /** |
| * A DNS master file parser. This incrementally parses the file, returning |
| * one record at a time. When directives are seen, they are added to the |
| * state and used when parsing future records. |
| * |
| * @author Brian Wellington |
| */ |
| |
| public class Master { |
| |
| private Name origin; |
| private File file; |
| private Record last = null; |
| private long defaultTTL; |
| private Master included = null; |
| private Tokenizer st; |
| private int currentType; |
| private int currentDClass; |
| private long currentTTL; |
| private boolean needSOATTL; |
| |
| private Generator generator; |
| private List generators; |
| private boolean noExpandGenerate; |
| |
| Master(File file, Name origin, long initialTTL) throws IOException { |
| if (origin != null && !origin.isAbsolute()) { |
| throw new RelativeNameException(origin); |
| } |
| this.file = file; |
| st = new Tokenizer(file); |
| this.origin = origin; |
| defaultTTL = initialTTL; |
| } |
| |
| /** |
| * Initializes the master file reader and opens the specified master file. |
| * @param filename The master file. |
| * @param origin The initial origin to append to relative names. |
| * @param ttl The initial default TTL. |
| * @throws IOException The master file could not be opened. |
| */ |
| public |
| Master(String filename, Name origin, long ttl) throws IOException { |
| this(new File(filename), origin, ttl); |
| } |
| |
| /** |
| * Initializes the master file reader and opens the specified master file. |
| * @param filename The master file. |
| * @param origin The initial origin to append to relative names. |
| * @throws IOException The master file could not be opened. |
| */ |
| public |
| Master(String filename, Name origin) throws IOException { |
| this(new File(filename), origin, -1); |
| } |
| |
| /** |
| * Initializes the master file reader and opens the specified master file. |
| * @param filename The master file. |
| * @throws IOException The master file could not be opened. |
| */ |
| public |
| Master(String filename) throws IOException { |
| this(new File(filename), null, -1); |
| } |
| |
| /** |
| * Initializes the master file reader. |
| * @param in The input stream containing a master file. |
| * @param origin The initial origin to append to relative names. |
| * @param ttl The initial default TTL. |
| */ |
| public |
| Master(InputStream in, Name origin, long ttl) { |
| if (origin != null && !origin.isAbsolute()) { |
| throw new RelativeNameException(origin); |
| } |
| st = new Tokenizer(in); |
| this.origin = origin; |
| defaultTTL = ttl; |
| } |
| |
| /** |
| * Initializes the master file reader. |
| * @param in The input stream containing a master file. |
| * @param origin The initial origin to append to relative names. |
| */ |
| public |
| Master(InputStream in, Name origin) { |
| this(in, origin, -1); |
| } |
| |
| /** |
| * Initializes the master file reader. |
| * @param in The input stream containing a master file. |
| */ |
| public |
| Master(InputStream in) { |
| this(in, null, -1); |
| } |
| |
| private Name |
| parseName(String s, Name origin) throws TextParseException { |
| try { |
| return Name.fromString(s, origin); |
| } |
| catch (TextParseException e) { |
| throw st.exception(e.getMessage()); |
| } |
| } |
| |
| private void |
| parseTTLClassAndType() throws IOException { |
| String s; |
| boolean seen_class = false; |
| |
| |
| // This is a bit messy, since any of the following are legal: |
| // class ttl type |
| // ttl class type |
| // class type |
| // ttl type |
| // type |
| seen_class = false; |
| s = st.getString(); |
| if ((currentDClass = DClass.value(s)) >= 0) { |
| s = st.getString(); |
| seen_class = true; |
| } |
| |
| currentTTL = -1; |
| try { |
| currentTTL = TTL.parseTTL(s); |
| s = st.getString(); |
| } |
| catch (NumberFormatException e) { |
| if (defaultTTL >= 0) |
| currentTTL = defaultTTL; |
| else if (last != null) |
| currentTTL = last.getTTL(); |
| } |
| |
| if (!seen_class) { |
| if ((currentDClass = DClass.value(s)) >= 0) { |
| s = st.getString(); |
| } else { |
| currentDClass = DClass.IN; |
| } |
| } |
| |
| if ((currentType = Type.value(s)) < 0) |
| throw st.exception("Invalid type '" + s + "'"); |
| |
| // BIND allows a missing TTL for the initial SOA record, and uses |
| // the SOA minimum value. If the SOA is not the first record, |
| // this is an error. |
| if (currentTTL < 0) { |
| if (currentType != Type.SOA) |
| throw st.exception("missing TTL"); |
| needSOATTL = true; |
| currentTTL = 0; |
| } |
| } |
| |
| private long |
| parseUInt32(String s) { |
| if (!Character.isDigit(s.charAt(0))) |
| return -1; |
| try { |
| long l = Long.parseLong(s); |
| if (l < 0 || l > 0xFFFFFFFFL) |
| return -1; |
| return l; |
| } |
| catch (NumberFormatException e) { |
| return -1; |
| } |
| } |
| |
| private void |
| startGenerate() throws IOException { |
| String s; |
| int n; |
| |
| // The first field is of the form start-end[/step] |
| // Regexes would be useful here. |
| s = st.getIdentifier(); |
| n = s.indexOf("-"); |
| if (n < 0) |
| throw st.exception("Invalid $GENERATE range specifier: " + s); |
| String startstr = s.substring(0, n); |
| String endstr = s.substring(n + 1); |
| String stepstr = null; |
| n = endstr.indexOf("/"); |
| if (n >= 0) { |
| stepstr = endstr.substring(n + 1); |
| endstr = endstr.substring(0, n); |
| } |
| long start = parseUInt32(startstr); |
| long end = parseUInt32(endstr); |
| long step; |
| if (stepstr != null) |
| step = parseUInt32(stepstr); |
| else |
| step = 1; |
| if (start < 0 || end < 0 || start > end || step <= 0) |
| throw st.exception("Invalid $GENERATE range specifier: " + s); |
| |
| // The next field is the name specification. |
| String nameSpec = st.getIdentifier(); |
| |
| // Then the ttl/class/type, in the same form as a normal record. |
| // Only some types are supported. |
| parseTTLClassAndType(); |
| if (!Generator.supportedType(currentType)) |
| throw st.exception("$GENERATE does not support " + |
| Type.string(currentType) + " records"); |
| |
| // Next comes the rdata specification. |
| String rdataSpec = st.getIdentifier(); |
| |
| // That should be the end. However, we don't want to move past the |
| // line yet, so put back the EOL after reading it. |
| st.getEOL(); |
| st.unget(); |
| |
| generator = new Generator(start, end, step, nameSpec, |
| currentType, currentDClass, currentTTL, |
| rdataSpec, origin); |
| if (generators == null) |
| generators = new ArrayList(1); |
| generators.add(generator); |
| } |
| |
| private void |
| endGenerate() throws IOException { |
| // Read the EOL that we put back before. |
| st.getEOL(); |
| |
| generator = null; |
| } |
| |
| private Record |
| nextGenerated() throws IOException { |
| try { |
| return generator.nextRecord(); |
| } |
| catch (Tokenizer.TokenizerException e) { |
| throw st.exception("Parsing $GENERATE: " + e.getBaseMessage()); |
| } |
| catch (TextParseException e) { |
| throw st.exception("Parsing $GENERATE: " + e.getMessage()); |
| } |
| } |
| |
| /** |
| * Returns the next record in the master file. This will process any |
| * directives before the next record. |
| * @return The next record. |
| * @throws IOException The master file could not be read, or was syntactically |
| * invalid. |
| */ |
| public Record |
| _nextRecord() throws IOException { |
| Tokenizer.Token token; |
| String s; |
| |
| if (included != null) { |
| Record rec = included.nextRecord(); |
| if (rec != null) |
| return rec; |
| included = null; |
| } |
| if (generator != null) { |
| Record rec = nextGenerated(); |
| if (rec != null) |
| return rec; |
| endGenerate(); |
| } |
| while (true) { |
| Name name; |
| |
| token = st.get(true, false); |
| if (token.type == Tokenizer.WHITESPACE) { |
| Tokenizer.Token next = st.get(); |
| if (next.type == Tokenizer.EOL) |
| continue; |
| else if (next.type == Tokenizer.EOF) |
| return null; |
| else |
| st.unget(); |
| if (last == null) |
| throw st.exception("no owner"); |
| name = last.getName(); |
| } |
| else if (token.type == Tokenizer.EOL) |
| continue; |
| else if (token.type == Tokenizer.EOF) |
| return null; |
| else if (((String) token.value).charAt(0) == '$') { |
| s = token.value; |
| |
| if (s.equalsIgnoreCase("$ORIGIN")) { |
| origin = st.getName(Name.root); |
| st.getEOL(); |
| continue; |
| } else if (s.equalsIgnoreCase("$TTL")) { |
| defaultTTL = st.getTTL(); |
| st.getEOL(); |
| continue; |
| } else if (s.equalsIgnoreCase("$INCLUDE")) { |
| String filename = st.getString(); |
| File newfile; |
| if (file != null) { |
| String parent = file.getParent(); |
| newfile = new File(parent, filename); |
| } else { |
| newfile = new File(filename); |
| } |
| Name incorigin = origin; |
| token = st.get(); |
| if (token.isString()) { |
| incorigin = parseName(token.value, |
| Name.root); |
| st.getEOL(); |
| } |
| included = new Master(newfile, incorigin, |
| defaultTTL); |
| /* |
| * If we continued, we wouldn't be looking in |
| * the new file. Recursing works better. |
| */ |
| return nextRecord(); |
| } else if (s.equalsIgnoreCase("$GENERATE")) { |
| if (generator != null) |
| throw new IllegalStateException |
| ("cannot nest $GENERATE"); |
| startGenerate(); |
| if (noExpandGenerate) { |
| endGenerate(); |
| continue; |
| } |
| return nextGenerated(); |
| } else { |
| throw st.exception("Invalid directive: " + s); |
| } |
| } else { |
| s = token.value; |
| name = parseName(s, origin); |
| if (last != null && name.equals(last.getName())) { |
| name = last.getName(); |
| } |
| } |
| |
| parseTTLClassAndType(); |
| last = Record.fromString(name, currentType, currentDClass, |
| currentTTL, st, origin); |
| if (needSOATTL) { |
| long ttl = ((SOARecord)last).getMinimum(); |
| last.setTTL(ttl); |
| defaultTTL = ttl; |
| needSOATTL = false; |
| } |
| return last; |
| } |
| } |
| |
| /** |
| * Returns the next record in the master file. This will process any |
| * directives before the next record. |
| * @return The next record. |
| * @throws IOException The master file could not be read, or was syntactically |
| * invalid. |
| */ |
| public Record |
| nextRecord() throws IOException { |
| Record rec = null; |
| try { |
| rec = _nextRecord(); |
| } |
| finally { |
| if (rec == null) { |
| st.close(); |
| } |
| } |
| return rec; |
| } |
| |
| /** |
| * Specifies whether $GENERATE statements should be expanded. Whether |
| * expanded or not, the specifications for generated records are available |
| * by calling {@link #generators}. This must be called before a $GENERATE |
| * statement is seen during iteration to have an effect. |
| */ |
| public void |
| expandGenerate(boolean wantExpand) { |
| noExpandGenerate = !wantExpand; |
| } |
| |
| /** |
| * Returns an iterator over the generators specified in the master file; that |
| * is, the parsed contents of $GENERATE statements. |
| * @see Generator |
| */ |
| public Iterator |
| generators() { |
| if (generators != null) |
| return Collections.unmodifiableList(generators).iterator(); |
| else |
| return Collections.EMPTY_LIST.iterator(); |
| } |
| |
| protected void |
| finalize() { |
| st.close(); |
| } |
| |
| } |