// 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();
}
}