[tor-commits] [metrics-lib/master] Make consensus parsing a lot more robust.

karsten at torproject.org karsten at torproject.org
Wed Dec 14 18:18:00 UTC 2011


commit 228d6564f8e7b6bec635216d64282423f407025a
Author: Karsten Loesing <karsten.loesing at gmx.net>
Date:   Wed Dec 14 19:17:26 2011 +0100

    Make consensus parsing a lot more robust.
---
 src/org/torproject/descriptor/DirSourceEntry.java  |    3 +
 .../torproject/descriptor/NetworkStatusEntry.java  |   25 +-
 .../descriptor/RelayNetworkStatusConsensus.java    |   17 +-
 .../descriptor/impl/DescriptorParseException.java  |   10 +
 .../descriptor/impl/DirSourceEntryImpl.java        |  115 +++-
 .../descriptor/impl/NetworkStatusEntryImpl.java    |  217 ++++--
 .../torproject/descriptor/impl/ParseHelper.java    |  147 +++
 .../impl/RelayNetworkStatusConsensusImpl.java      |  430 ++++++---
 .../impl/RelayNetworkStatusVoteImpl.java           |   15 +-
 .../impl/RelayNetworkStatusConsensusImplTest.java  |  932 ++++++++++++--------
 10 files changed, 1307 insertions(+), 604 deletions(-)

diff --git a/src/org/torproject/descriptor/DirSourceEntry.java b/src/org/torproject/descriptor/DirSourceEntry.java
index 57ce684..190b1df 100644
--- a/src/org/torproject/descriptor/DirSourceEntry.java
+++ b/src/org/torproject/descriptor/DirSourceEntry.java
@@ -22,6 +22,9 @@ public interface DirSourceEntry {
   /* Return the ORPort. */
   public int getOrPort();
 
+  /* Return whether the dir-source was created using a legacy key. */
+  public boolean isLegacy();
+
   /* Return the contact line. */
   public String getContactLine();
 
diff --git a/src/org/torproject/descriptor/NetworkStatusEntry.java b/src/org/torproject/descriptor/NetworkStatusEntry.java
index 156d07b..5d48818 100644
--- a/src/org/torproject/descriptor/NetworkStatusEntry.java
+++ b/src/org/torproject/descriptor/NetworkStatusEntry.java
@@ -30,16 +30,29 @@ public interface NetworkStatusEntry {
   /* Return the DirPort. */
   public int getDirPort();
 
-  /* Return the relay flags. */
+  /* Return the relay flags or null if the status entry didn't contain any
+   * relay flags. */
   public SortedSet<String> getFlags();
 
-  /* Return the Tor software version. */
+  /* Return the Tor software version or null if the status entry didn't
+   * contain a version line. */
   public String getVersion();
 
-  /* Return the bandwidth weight line. */
-  public String getBandwidth();
+  /* Return the bandwidth weight or -1L if the status entry didn't
+   * contain a bandwidth line. */
+  public long getBandwidth();
 
-  /* Return the port summary line. */
-  public String getPorts();
+  /* Return the measured bandwidth or -1L if the status entry didn't
+   * contain a bandwidth line or didn't contain a Measured= keyword in its
+   * bandwidth line. */
+  public long getMeasured();
+
+  /* Return the default policy of the port summary or null if the status
+   * entry didn't contain a port summary line. */
+  public String getDefaultPolicy();
+
+  /* Return the port list of the port summary or null if the status entry
+   * didn't contain a port summary line. */
+  public String getPortList();
 }
 
diff --git a/src/org/torproject/descriptor/RelayNetworkStatusConsensus.java b/src/org/torproject/descriptor/RelayNetworkStatusConsensus.java
index 17beeb5..42757ce 100644
--- a/src/org/torproject/descriptor/RelayNetworkStatusConsensus.java
+++ b/src/org/torproject/descriptor/RelayNetworkStatusConsensus.java
@@ -24,8 +24,11 @@ public interface RelayNetworkStatusConsensus extends Descriptor {
   /* Return the valid-until time in milliseconds. */
   public long getValidUntilMillis();
 
-  /* Return a list of the voting-delay times in seconds. */
-  public List<Long> getVotingDelay();
+  /* Return the VoteSeconds time in seconds. */
+  public long getVoteSeconds();
+
+  /* Return the DistSeconds time in seconds. */
+  public long getDistSeconds();
 
   /* Return recommended server versions or null if the consensus doesn't
    * contain recommended server versions. */
@@ -38,8 +41,9 @@ public interface RelayNetworkStatusConsensus extends Descriptor {
   /* Return known relay flags. */
   public SortedSet<String> getKnownFlags();
 
-  /* Return consensus parameters. */
-  public SortedMap<String, String> getConsensusParams();
+  /* Return consensus parameters or null if the consensus doesn't contain
+   * consensus parameters. */
+  public SortedMap<String, Integer> getConsensusParams();
 
   /* Return dir-source entries representing the directories of which
    * votes are contained in this consensus. */
@@ -58,7 +62,8 @@ public interface RelayNetworkStatusConsensus extends Descriptor {
   /* Return directory signatures. */
   public SortedMap<String, String> getDirectorySignatures();
 
-  /* Return bandwidth weights. */
-  public SortedMap<String, String> getBandwidthWeights();
+  /* Return bandwidth weights or null if the consensus doesn't contain
+   * bandwidth weights. */
+  public SortedMap<String, Integer> getBandwidthWeights();
 }
 
diff --git a/src/org/torproject/descriptor/impl/DescriptorParseException.java b/src/org/torproject/descriptor/impl/DescriptorParseException.java
new file mode 100644
index 0000000..8670a3b
--- /dev/null
+++ b/src/org/torproject/descriptor/impl/DescriptorParseException.java
@@ -0,0 +1,10 @@
+/* Copyright 2011 The Tor Project
+ * See LICENSE for licensing information */
+package org.torproject.descriptor.impl;
+
+public class DescriptorParseException extends Exception {
+  protected DescriptorParseException(String message) {
+    super(message);
+  }
+}
+
diff --git a/src/org/torproject/descriptor/impl/DirSourceEntryImpl.java b/src/org/torproject/descriptor/impl/DirSourceEntryImpl.java
index a2d0b34..a38da87 100644
--- a/src/org/torproject/descriptor/impl/DirSourceEntryImpl.java
+++ b/src/org/torproject/descriptor/impl/DirSourceEntryImpl.java
@@ -6,6 +6,8 @@ import java.io.BufferedReader;
 import java.io.IOException;
 import java.io.StringReader;
 import java.text.ParseException;
+import java.util.SortedSet;
+import java.util.TreeSet;
 import org.torproject.descriptor.DirSourceEntry;
 
 public class DirSourceEntryImpl implements DirSourceEntry {
@@ -16,30 +18,110 @@ public class DirSourceEntryImpl implements DirSourceEntry {
   }
 
   protected DirSourceEntryImpl(byte[] dirSourceEntryBytes)
-      throws ParseException {
+      throws DescriptorParseException {
     this.dirSourceEntryBytes = dirSourceEntryBytes;
+    this.initializeKeywords();
+    this.parseDirSourceEntryBytes();
+    this.checkKeywords();
+  }
+
+  private SortedSet<String> exactlyOnceKeywords, atMostOnceKeywords;
+  private void initializeKeywords() {
+    this.exactlyOnceKeywords = new TreeSet<String>();
+    this.exactlyOnceKeywords.add("dir-source");
+    this.exactlyOnceKeywords.add("vote-digest");
+    this.atMostOnceKeywords = new TreeSet<String>();
+    this.atMostOnceKeywords.add("contact");
+  }
+
+  private void parsedExactlyOnceKeyword(String keyword)
+      throws DescriptorParseException {
+    if (!this.exactlyOnceKeywords.contains(keyword)) {
+      throw new DescriptorParseException("Duplicate '" + keyword
+          + "' line in dir-source.");
+    }
+    this.exactlyOnceKeywords.remove(keyword);
+  }
+
+  private void parsedAtMostOnceKeyword(String keyword)
+      throws DescriptorParseException {
+    if (!this.atMostOnceKeywords.contains(keyword)) {
+      throw new DescriptorParseException("Duplicate " + keyword + "line "
+          + "in dir-source.");
+    }
+    this.atMostOnceKeywords.remove(keyword);
+  }
+
+  private void checkKeywords() throws DescriptorParseException {
+    if (!this.exactlyOnceKeywords.isEmpty()) {
+      throw new DescriptorParseException("dir-source does not contain a '"
+          + this.exactlyOnceKeywords.first() + "' line.");
+    }
+  }
+
+  private void parseDirSourceEntryBytes()
+      throws DescriptorParseException {
     try {
       BufferedReader br = new BufferedReader(new StringReader(
           new String(this.dirSourceEntryBytes)));
       String line;
       while ((line = br.readLine()) != null) {
-        if (line.startsWith("dir-source ")) {
-          String[] parts = line.split(" ");
-          this.nickname = parts[1];
-          this.identity = parts[2];
-          this.ip = parts[4];
-          this.dirPort = Integer.parseInt(parts[5]);
-          this.orPort = Integer.parseInt(parts[6]);
-        } else if (line.startsWith("contact ")) {
-          this.contactLine = line.substring("contact ".length());
-        } else if (line.startsWith("vote-digest ")) {
-          this.voteDigest = line.split(" ")[1];
+        if (line.startsWith("dir-source")) {
+          this.parseDirSourceLine(line);
+        } else if (line.startsWith("contact")) {
+          this.parseContactLine(line);
+        } else if (line.startsWith("vote-digest")) {
+          this.parseVoteDigestLine(line);
+        } else {
+          /* TODO Should we really throw an exception here? */
+          throw new DescriptorParseException("Unknown line '" + line
+              + "' in dir-source entry.");
         }
       }
     } catch (IOException e) {
-      /* TODO This cannot happen, right? */
+      throw new RuntimeException("Internal error: Ran into an "
+          + "IOException while parsing a String in memory.  Something's "
+          + "really wrong.", e);
+    }
+  }
+
+  private void parseDirSourceLine(String line)
+      throws DescriptorParseException {
+    this.parsedExactlyOnceKeyword("dir-source");
+    String[] parts = line.split(" ");
+    String nickname = parts[1];
+    if (nickname.endsWith("-legacy")) {
+      nickname = nickname.substring(0, nickname.length()
+          - "-legacy".length());
+      this.isLegacy = true;
+      this.parsedExactlyOnceKeyword("vote-digest");
+    }
+    this.nickname = ParseHelper.parseNickname(line, nickname);
+    this.identity = ParseHelper.parseTwentyByteHexString(line, parts[2]);
+    this.ip = ParseHelper.parseIpv4Address(line, parts[4]);
+    this.dirPort = ParseHelper.parsePort(line, parts[5]);
+    this.orPort = ParseHelper.parsePort(line, parts[6]);
+  }
+
+  private void parseContactLine(String line)
+      throws DescriptorParseException {
+    this.parsedAtMostOnceKeyword("contact");
+    if (line.length() > "contact ".length()) {
+      this.contactLine = line.substring("contact ".length());
+    } else {
+      this.contactLine = "";
+    }
+  }
+
+  private void parseVoteDigestLine(String line)
+      throws DescriptorParseException {
+    this.parsedExactlyOnceKeyword("vote-digest");
+    String[] parts = line.split(" ");
+    if (parts.length != 2) {
+      throw new DescriptorParseException("Invalid line '" + line + "'.");
     }
-    /* TODO Implement some plausibility tests. */
+    this.voteDigest = ParseHelper.parseTwentyByteHexString(line,
+        parts[1]);
   }
 
   private String nickname;
@@ -52,6 +134,11 @@ public class DirSourceEntryImpl implements DirSourceEntry {
     return this.identity;
   }
 
+  private boolean isLegacy;
+  public boolean isLegacy() {
+    return this.isLegacy;
+  }
+
   private String ip;
   public String getIp() {
     return this.ip;
diff --git a/src/org/torproject/descriptor/impl/NetworkStatusEntryImpl.java b/src/org/torproject/descriptor/impl/NetworkStatusEntryImpl.java
index 610fb1a..6612272 100644
--- a/src/org/torproject/descriptor/impl/NetworkStatusEntryImpl.java
+++ b/src/org/torproject/descriptor/impl/NetworkStatusEntryImpl.java
@@ -5,14 +5,10 @@ package org.torproject.descriptor.impl;
 import java.io.BufferedReader;
 import java.io.IOException;
 import java.io.StringReader;
-import java.text.ParseException;
-import java.text.SimpleDateFormat;
 import java.util.Arrays;
+import java.util.SortedMap;
 import java.util.SortedSet;
-import java.util.TimeZone;
 import java.util.TreeSet;
-import org.apache.commons.codec.binary.Base64;
-import org.apache.commons.codec.binary.Hex;
 import org.torproject.descriptor.NetworkStatusEntry;
 
 public class NetworkStatusEntryImpl implements NetworkStatusEntry {
@@ -22,60 +18,163 @@ public class NetworkStatusEntryImpl implements NetworkStatusEntry {
     return this.statusEntryBytes;
   }
 
-  private static SimpleDateFormat dateTimeFormat;
-  static {
-    dateTimeFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
-    dateTimeFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
-  }
-
   protected NetworkStatusEntryImpl(byte[] statusEntryBytes)
-      throws ParseException {
+      throws DescriptorParseException {
     this.statusEntryBytes = statusEntryBytes;
+    this.initializeKeywords();
+    this.parseStatusEntryBytes();
+  }
+
+  private SortedSet<String> atMostOnceKeywords;
+  private void initializeKeywords() {
+    this.atMostOnceKeywords = new TreeSet<String>();
+    this.atMostOnceKeywords.add("s");
+    this.atMostOnceKeywords.add("v");
+    this.atMostOnceKeywords.add("w");
+    this.atMostOnceKeywords.add("p");
+    this.atMostOnceKeywords.add("m");
+  }
+
+  private void parsedAtMostOnceKeyword(String keyword)
+      throws DescriptorParseException {
+    if (!this.atMostOnceKeywords.contains(keyword)) {
+      throw new DescriptorParseException("Duplicate '" + keyword
+          + "' line in status entry.");
+    }
+    this.atMostOnceKeywords.remove(keyword);
+  }
+
+  private void parseStatusEntryBytes() throws DescriptorParseException {
     try {
       BufferedReader br = new BufferedReader(new StringReader(
           new String(this.statusEntryBytes)));
-      String line;
+      String line = br.readLine();
+      if (line == null || !line.startsWith("r ")) {
+        throw new DescriptorParseException("Status entry must start with "
+            + "an r line.");
+      }
+      String[] rLineParts = line.split(" ");
+      this.parseRLine(line, rLineParts);
       while ((line = br.readLine()) != null) {
-        if (line.startsWith("r ")) {
-          String[] parts = line.split(" ");
-          if (parts.length < 9) {
-            throw new RuntimeException("r line '" + line + "' has fewer "
-                + "space-separated elements than expected.");
-          }
-          this.nickname = parts[1];
-          this.fingerprint = Hex.encodeHexString(Base64.decodeBase64(
-              parts[2] + "=")).toLowerCase();
-          this.descriptor = Hex.encodeHexString(Base64.decodeBase64(
-              parts[3] + "=")).toLowerCase();
-          this.publishedMillis = dateTimeFormat.parse(parts[4] + " "
-              + parts[5]).getTime();
-          this.address = parts[6];
-          this.orPort = Integer.parseInt(parts[7]);
-          this.dirPort = Integer.parseInt(parts[8]);
-        } else if (line.equals("s")) {
-          /* No flags to add. */
-        } else if (line.startsWith("s ")) {
-          this.flags.addAll(Arrays.asList(line.substring("s ".length()).
-              split(" ")));
-        } else if (line.startsWith("v ") || line.startsWith("opt v")) {
-          this.version = line.substring(
-              line.startsWith("v ") ? "v ".length() : "opt v".length());
-        } else if (line.startsWith("w ")) {
-          this.bandwidth = line.substring("w ".length());
-        } else if (line.startsWith("p ")) {
-          this.ports = line.substring(2);
-        } else if (line.startsWith("m ")) {
-          /* TODO Parse m lines in votes. */
+        String[] parts = !line.startsWith("opt ") ? line.split(" ") :
+            line.substring("opt ".length()).split(" ");
+        String keyword = parts[0];
+        if (keyword.equals("s")) {
+          this.parseSLine(line, parts);
+        } else if (keyword.equals("v")) {
+          this.parseVLine(line, parts);
+        } else if (keyword.equals("w")) {
+          this.parseWLine(line, parts);
+        } else if (keyword.equals("p")) {
+          this.parsePLine(line, parts);
+        } else if (keyword.equals("m")) {
+          this.parseMLine(line, parts);
         } else {
-          throw new RuntimeException("Unknown line '" + line + "' in "
-              + "status entry.");
+          /* TODO Is throwing an exception the right thing to do here?
+           * This is probably fine for development, but once the library
+           * is in production use, this seems annoying. */
+          throw new DescriptorParseException("Unknown line '" + line
+              + "' in status entry.");
         }
       }
     } catch (IOException e) {
-      /* TODO Do something. */
+      throw new RuntimeException("Internal error: Ran into an "
+          + "IOException while parsing a String in memory.  Something's "
+          + "really wrong.", e);
+    }
+  }
+
+  private void parseRLine(String line, String[] parts)
+      throws DescriptorParseException {
+    if (parts.length < 9) {
+      throw new RuntimeException("r line '" + line + "' has fewer "
+          + "space-separated elements than expected.");
+    }
+    this.nickname = ParseHelper.parseNickname(line, parts[1]);
+    this.fingerprint = ParseHelper.parseTwentyByteBase64String(line,
+        parts[2]);
+    this.descriptor = ParseHelper.parseTwentyByteBase64String(line,
+        parts[3]);
+    this.publishedMillis = ParseHelper.parseTimestampAtIndex(line, parts,
+        4, 5);
+    this.address = ParseHelper.parseIpv4Address(line, parts[6]);
+    this.orPort = ParseHelper.parsePort(line, parts[7]);
+    this.dirPort = ParseHelper.parsePort(line, parts[8]);
+  }
+
+  private void parseSLine(String line, String[] parts)
+      throws DescriptorParseException {
+    this.parsedAtMostOnceKeyword("s");
+    this.flags = new TreeSet<String>();
+    for (int i = 1; i < parts.length; i++) {
+      this.flags.add(parts[i]);
+    }
+  }
+
+  private void parseVLine(String line, String[] parts)
+      throws DescriptorParseException {
+    this.parsedAtMostOnceKeyword("v");
+    String noOptLine = line;
+    if (noOptLine.startsWith("opt ")) {
+      noOptLine = noOptLine.substring(4);
+    }
+    if (noOptLine.length() < 3) {
+      throw new DescriptorParseException("Invalid line '" + line + "' in "
+          + "status entry.");
+    } else {
+      this.version = noOptLine.substring(2);
+    }
+  }
+
+  private void parseWLine(String line, String[] parts)
+      throws DescriptorParseException {
+    this.parsedAtMostOnceKeyword("w");
+    SortedMap<String, Integer> pairs = ParseHelper.parseKeyValuePairs(
+        line, parts, 1);
+    if (pairs.isEmpty()) {
+      throw new DescriptorParseException("Illegal line '" + line + "'.");
+    }
+    if (pairs.containsKey("Bandwidth")) {
+      this.bandwidth = pairs.remove("Bandwidth");
+    }
+    if (pairs.containsKey("Measured")) {
+      this.measured = pairs.remove("Measured");
+    }
+    if (!pairs.isEmpty()) {
+      throw new DescriptorParseException("Unknown key-value pair in "
+          + "line '" + line + "'.");
     }
-    /* TODO Add some plausibility checks, like if we have a nickname
-     * etc. */
+  }
+
+  private void parsePLine(String line, String[] parts)
+      throws DescriptorParseException {
+    this.parsedAtMostOnceKeyword("p");
+    boolean isValid = true;
+    if (parts.length != 3) {
+      isValid = false;
+    } else if (!parts[1].equals("accept") && !parts[1].equals("reject")) {
+      isValid = false;
+    } else {
+      this.defaultPolicy = parts[1];
+      this.portList = parts[2];
+      String[] ports = parts[2].split(",", -1);
+      for (int i = 0; i < ports.length; i++) {
+        if (ports[i].length() < 1) {
+          isValid = false;
+          break;
+        }
+      }
+    }
+    if (!isValid) {
+      throw new DescriptorParseException("Illegal line '" + line + "'.");
+    }
+  }
+
+  private void parseMLine(String line, String[] parts)
+      throws DescriptorParseException {
+    this.parsedAtMostOnceKeyword("m");
+    /* TODO Implement parsing of m lines in votes.  Try to find where m
+     * lines are specified first. */
   }
 
   private String nickname;
@@ -113,7 +212,7 @@ public class NetworkStatusEntryImpl implements NetworkStatusEntry {
     return this.dirPort;
   }
 
-  private SortedSet<String> flags = new TreeSet<String>();
+  private SortedSet<String> flags;
   public SortedSet<String> getFlags() {
     return new TreeSet<String>(this.flags);
   }
@@ -123,14 +222,24 @@ public class NetworkStatusEntryImpl implements NetworkStatusEntry {
     return this.version;
   }
 
-  private String bandwidth;
-  public String getBandwidth() {
+  private long bandwidth = -1L;
+  public long getBandwidth() {
     return this.bandwidth;
   }
 
-  private String ports;
-  public String getPorts() {
-    return this.ports;
+  private long measured = -1L;
+  public long getMeasured() {
+    return this.measured;
+  }
+
+  private String defaultPolicy;
+  public String getDefaultPolicy() {
+    return this.defaultPolicy;
+  }
+
+  private String portList;
+  public String getPortList() {
+    return this.portList;
   }
 }
 
diff --git a/src/org/torproject/descriptor/impl/ParseHelper.java b/src/org/torproject/descriptor/impl/ParseHelper.java
new file mode 100644
index 0000000..5f4a1bd
--- /dev/null
+++ b/src/org/torproject/descriptor/impl/ParseHelper.java
@@ -0,0 +1,147 @@
+/* Copyright 2011 The Tor Project
+ * See LICENSE for licensing information */
+package org.torproject.descriptor.impl;
+
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.SortedMap;
+import java.util.TimeZone;
+import java.util.TreeMap;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import org.apache.commons.codec.binary.Base64;
+import org.apache.commons.codec.binary.Hex;
+
+public class ParseHelper {
+
+  private static Pattern ipv4Pattern =
+     Pattern.compile("^[0-9\\.]{7,15}$");
+  public static String parseIpv4Address(String line, String address)
+      throws DescriptorParseException {
+    boolean isValid = true;
+    if (!ipv4Pattern.matcher(address).matches()) {
+      isValid = false;
+    } else {
+      String[] parts = address.split("\\.", -1);
+      if (parts.length != 4) {
+        isValid = false;
+      } else {
+        for (int i = 0; i < 4; i++) {
+          try {
+            int octetValue = Integer.parseInt(parts[i]);
+            if (octetValue < 0 || octetValue > 255) {
+              isValid = false;
+            }
+          } catch (NumberFormatException e) {
+            isValid = false;
+          }
+        }
+      }
+    }
+    if (!isValid) {
+      throw new DescriptorParseException("'" + address + "' in line '"
+          + line + "' is not a valid IPv4 address.");
+    }
+    return address;
+  }
+
+  public static int parsePort(String line, String portString)
+      throws DescriptorParseException {
+    int port = -1;
+    try {
+      port = Integer.parseInt(portString);
+    } catch (NumberFormatException e) {
+      throw new DescriptorParseException("'" + portString + "' in line '"
+          + line + "' is not a valid port number.");
+    }
+    if (port < 0 || port > 65535) {
+      throw new DescriptorParseException("'" + portString + "' in line '"
+          + line + "' is not a valid port number.");
+    }
+    return port;
+  }
+
+  private static SimpleDateFormat dateTimeFormat = new SimpleDateFormat(
+      "yyyy-MM-dd HH:mm:ss");
+  static {
+    dateTimeFormat.setLenient(false);
+    dateTimeFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
+  }
+  public static long parseTimestampAtIndex(String line, String[] parts,
+      int dateIndex, int timeIndex) throws DescriptorParseException {
+    if (dateIndex >= parts.length || timeIndex >= parts.length) {
+      throw new DescriptorParseException("Line '" + line + "' does not "
+          + "contain a timestamp at the expected position.");
+    }
+    long result = -1L;
+    try {
+      result = dateTimeFormat.parse(
+          parts[dateIndex] + " " + parts[timeIndex]).getTime();
+    } catch (ParseException e) {
+      /* Leave result at -1L. */
+    }
+    if (result < 0L || result > 2000000000000L) {
+      throw new DescriptorParseException("Illegal timestamp format in "
+          + "line '" + line + "'.");
+    }
+    return result;
+  }
+
+  private static Pattern twentyByteHexPattern =
+      Pattern.compile("^[0-9A-F]{40}$");
+  public static String parseTwentyByteHexString(String line,
+      String hexString) throws DescriptorParseException {
+    if (!twentyByteHexPattern.matcher(hexString).matches()) {
+      throw new DescriptorParseException("Illegal hex string in line '"
+          + line + "'.");
+    }
+    return hexString;
+  }
+
+  public static SortedMap<String, Integer> parseKeyValuePairs(String line,
+      String[] parts, int startIndex) throws DescriptorParseException {
+    SortedMap<String, Integer> result = new TreeMap<String, Integer>();
+    for (int i = startIndex; i < parts.length; i++) {
+      String pair = parts[i];
+      String[] pairParts = pair.split("=");
+      if (pairParts.length != 2) {
+        throw new DescriptorParseException("Illegal key-value pair in "
+            + "line '" + line + "'.");
+      }
+      String pairName = pairParts[0];
+      try {
+        int pairValue = Integer.parseInt(pairParts[1]);
+        result.put(pairName, pairValue);
+      } catch (NumberFormatException e) {
+        throw new DescriptorParseException("Illegal value in line '"
+            + line + "'.");
+      }
+    }
+    return result;
+  }
+
+  private static Pattern nicknamePattern =
+      Pattern.compile("^[0-9a-zA-Z]{1,19}$");
+  public static String parseNickname(String line, String nickname)
+      throws DescriptorParseException {
+    if (!nicknamePattern.matcher(nickname).matches()) {
+      throw new DescriptorParseException("Illegal nickname in line '"
+          + line + "'.");
+    }
+    return nickname;
+  }
+
+  private static Pattern base64Pattern =
+      Pattern.compile("^[0-9a-zA-Z+/]{27}$");
+  public static String parseTwentyByteBase64String(String line,
+      String base64String) throws DescriptorParseException {
+    if (!base64Pattern.matcher(base64String).matches()) {
+      throw new DescriptorParseException("'" + base64String
+          + "' in line '" + line + "' is not a valid base64-encoded "
+          + "20-byte value.");
+    }
+    return Hex.encodeHexString(Base64.decodeBase64(base64String + "=")).
+        toUpperCase();
+  }
+}
+
diff --git a/src/org/torproject/descriptor/impl/RelayNetworkStatusConsensusImpl.java b/src/org/torproject/descriptor/impl/RelayNetworkStatusConsensusImpl.java
index 7cbe94c..0771382 100644
--- a/src/org/torproject/descriptor/impl/RelayNetworkStatusConsensusImpl.java
+++ b/src/org/torproject/descriptor/impl/RelayNetworkStatusConsensusImpl.java
@@ -13,8 +13,6 @@ import java.util.SortedSet;
 import java.util.TimeZone;
 import java.util.TreeMap;
 import java.util.TreeSet;
-import java.text.ParseException;
-import java.text.SimpleDateFormat;
 import org.torproject.descriptor.Descriptor;
 import org.torproject.descriptor.DirSourceEntry;
 import org.torproject.descriptor.NetworkStatusEntry;
@@ -41,187 +39,324 @@ public class RelayNetworkStatusConsensusImpl
       }
       byte[] descBytes = new byte[end - start];
       System.arraycopy(consensusBytes, start, descBytes, 0, end - start);
-      RelayNetworkStatusConsensus parsedConsensus =
-          new RelayNetworkStatusConsensusImpl(descBytes);
-      parsedConsensuses.add(parsedConsensus);
       start = end;
+      try {
+        RelayNetworkStatusConsensus parsedConsensus =
+            new RelayNetworkStatusConsensusImpl(descBytes);
+        parsedConsensuses.add(parsedConsensus);
+      } catch (DescriptorParseException e) {
+        /* TODO Handle this error somehow. */
+        System.err.println("Failed to parse consensus.  Skipping.");
+        e.printStackTrace();
+      }
     }
     return parsedConsensuses;
   }
 
-  protected RelayNetworkStatusConsensusImpl(byte[] consensusBytes) {
+  protected RelayNetworkStatusConsensusImpl(byte[] consensusBytes)
+      throws DescriptorParseException {
     this.consensusBytes = consensusBytes;
+    this.initializeKeywords();
     this.parseConsensusBytes();
-    this.checkConsistency();
-    /* TODO Find a way to handle parse and consistency-check problems. */
+    this.checkKeywords();
+  }
+
+  private SortedSet<String> exactlyOnceKeywords, atMostOnceKeywords;
+  private void initializeKeywords() {
+    this.exactlyOnceKeywords = new TreeSet<String>();
+    this.exactlyOnceKeywords.add("vote-status");
+    this.exactlyOnceKeywords.add("consensus-method");
+    this.exactlyOnceKeywords.add("valid-after");
+    this.exactlyOnceKeywords.add("fresh-until");
+    this.exactlyOnceKeywords.add("valid-until");
+    this.exactlyOnceKeywords.add("voting-delay");
+    this.exactlyOnceKeywords.add("known-flags");
+    this.exactlyOnceKeywords.add("directory-footer");
+    this.atMostOnceKeywords = new TreeSet<String>();
+    this.atMostOnceKeywords.add("client-versions");
+    this.atMostOnceKeywords.add("server-versions");
+    this.atMostOnceKeywords.add("params");
+    this.atMostOnceKeywords.add("bandwidth-weights");
+  }
+
+  private void parsedExactlyOnceKeyword(String keyword)
+      throws DescriptorParseException {
+    if (!this.exactlyOnceKeywords.contains(keyword)) {
+      throw new DescriptorParseException("Duplicate '" + keyword
+          + "' line in consensus.");
+    }
+    this.exactlyOnceKeywords.remove(keyword);
+  }
+
+  private void parsedAtMostOnceKeyword(String keyword)
+      throws DescriptorParseException {
+    if (!this.atMostOnceKeywords.contains(keyword)) {
+      throw new DescriptorParseException("Duplicate " + keyword + "line "
+          + "in consensus.");
+    }
+    this.atMostOnceKeywords.remove(keyword);
+  }
+
+  private void checkKeywords() throws DescriptorParseException {
+    if (!this.exactlyOnceKeywords.isEmpty()) {
+      throw new DescriptorParseException("Consensus does not contain a '"
+          + this.exactlyOnceKeywords.first() + "' line.");
+    }
   }
 
-  private void parseConsensusBytes() {
+  private void parseConsensusBytes() throws DescriptorParseException {
     try {
       BufferedReader br = new BufferedReader(new StringReader(
           new String(this.consensusBytes)));
-      String line;
-      SimpleDateFormat dateTimeFormat = new SimpleDateFormat(
-          "yyyy-MM-dd HH:mm:ss");
-      dateTimeFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
+      String line = br.readLine();
+      if (line == null || !line.equals("network-status-version 3")) {
+        throw new DescriptorParseException("Consensus must start with "
+            + "line 'network-status-version 3'.");
+      }
+      this.networkStatusVersion = 3;
       StringBuilder dirSourceEntryLines = null, statusEntryLines = null;
       boolean skipSignature = false;
       while ((line = br.readLine()) != null) {
-        if (line.startsWith("network-status-version ")) {
-          this.networkStatusVersion = Integer.parseInt(line.substring(
-              "network-status-version ".length()));
-        } else if (line.startsWith("vote-status ")) {
-          if (!line.equals("vote-status consensus")) {
-            throw new RuntimeException("Line '" + line + "' indicates "
-                + "that this string is not a consensus.  Aborting "
-                + "parsing.");
-          }
-        } else if (line.startsWith("consensus-method ")) {
-          this.consensusMethod = Integer.parseInt(line.substring(
-              "consensus-method ".length()));
-        } else if (line.startsWith("valid-after ")) {
-          this.validAfterMillis = dateTimeFormat.parse(
-              line.substring("valid-after ".length())).getTime();
-        } else if (line.startsWith("fresh-until ")) {
-          this.freshUntilMillis = dateTimeFormat.parse(
-              line.substring("fresh-until ".length())).getTime();
-        } else if (line.startsWith("valid-until ")) {
-          this.validUntilMillis = dateTimeFormat.parse(
-              line.substring("valid-until ".length())).getTime();
-        } else if (line.startsWith("voting-delay ")) {
-          for (String votingDelayString : line.substring(
-              "voting-delay ".length()).split(" ")) {
-            this.votingDelay.add(Long.parseLong(votingDelayString));
-          }
-        } else if (line.startsWith("client-versions ")) {
-          this.recommendedClientVersions = new TreeSet<String>(
-              Arrays.asList(line.split(" ")[1].split(",")));
-        } else if (line.startsWith("server-versions ")) {
-          this.recommendedServerVersions = new TreeSet<String>(
-              Arrays.asList(line.split(" ")[1].split(",")));
-        } else if (line.startsWith("known-flags ")) {
-          for (String flag : line.substring("known-flags ".length()).
-              split(" ")) {
-            this.knownFlags.add(flag);
-          }
-        } else if (line.startsWith("params ")) {
-          if (line.length() > "params ".length()) {
-            for (String param :
-                line.substring("params ".length()).split(" ")) {
-              String paramName = param.split("=")[0];
-              String paramValue = param.split("=")[1];
-              this.consensusParams.put(paramName, paramValue);
-            }
-          }
-        } else if (line.startsWith("dir-source ") ||
-            line.startsWith("r ") || line.equals("directory-footer")) {
-          /* TODO Add code for parsing legacy dir sources. */
+        if (line.length() < 1) {
+          throw new DescriptorParseException("Empty lines are not "
+              + "allowed in a consensus.");
+        }
+        String[] parts = line.split(" ");
+        if (parts.length < 1) {
+          throw new DescriptorParseException("No keyword found in line '"
+              + line + "'.");
+        }
+        String keyword = parts[0];
+        if (keyword.length() < 1) {
+          throw new DescriptorParseException("Empty keyword in line '"
+              + line + "'.");
+        }
+        if (keyword.equals("vote-status")) {
+          this.parseVoteStatusLine(line, parts);
+        } else if (keyword.equals("consensus-method")) {
+          this.parseConsensusMethodLine(line, parts);
+        } else if (keyword.equals("valid-after")) {
+          this.parseValidAfterLine(line, parts);
+        } else if (keyword.equals("fresh-until")) {
+          this.parseFreshUntilLine(line, parts);
+        } else if (keyword.equals("valid-until")) {
+          this.parseValidUntilLine(line, parts);
+        } else if (keyword.equals("voting-delay")) {
+          this.parseVotingDelayLine(line, parts);
+        } else if (keyword.equals("client-versions")) {
+          this.parseClientVersionsLine(line, parts);
+        } else if (keyword.equals("server-versions")) {
+          this.parseServerVersionsLine(line, parts);
+        } else if (keyword.equals("known-flags")) {
+          this.parseKnownFlagsLine(line, parts);
+        } else if (keyword.equals("params")) {
+          this.parseParamsLine(line, parts);
+        } else if (keyword.equals("dir-source") || keyword.equals("r") ||
+            keyword.equals("directory-footer")) {
           if (dirSourceEntryLines != null) {
-            DirSourceEntry dirSourceEntry = new DirSourceEntryImpl(
-                dirSourceEntryLines.toString().getBytes());
-            this.dirSourceEntries.put(dirSourceEntry.getIdentity(),
-                dirSourceEntry);
+            this.parseDirSourceEntryLines(dirSourceEntryLines.toString());
             dirSourceEntryLines = null;
           }
           if (statusEntryLines != null) {
-            NetworkStatusEntryImpl statusEntry =
-                new NetworkStatusEntryImpl(
-                statusEntryLines.toString().getBytes());
-            this.statusEntries.put(statusEntry.getFingerprint(),
-                statusEntry);
+            this.parseStatusEntryLines(statusEntryLines.toString());
             statusEntryLines = null;
           }
-          if (line.startsWith("dir-source ")) {
-            dirSourceEntryLines = new StringBuilder();
-            dirSourceEntryLines.append(line + "\n");
-          } else if (line.startsWith("r ")) {
-            statusEntryLines = new StringBuilder();
-            statusEntryLines.append(line + "\n");
+          if (keyword.equals("dir-source")) {
+            dirSourceEntryLines = new StringBuilder(line + "\n");
+          } else if (keyword.equals("r")) {
+            statusEntryLines = new StringBuilder(line + "\n");
+          } else if (keyword.equals("directory-footer")) {
+            this.parsedExactlyOnceKeyword("directory-footer");
+          }
+        } else if (keyword.equals("contact") ||
+            keyword.equals("vote-digest")) {
+          if (dirSourceEntryLines == null) {
+            throw new DescriptorParseException(keyword + " line with no "
+                + "preceding dir-source line.");
           }
-        } else if (line.startsWith("contact ") ||
-            line.startsWith("vote-digest ")) {
           dirSourceEntryLines.append(line + "\n");
-        } else if (line.startsWith("s ") || line.equals("s") ||
-            line.startsWith("v ") || line.startsWith("w ") ||
-            line.startsWith("p ")) {
-          statusEntryLines.append(line + "\n");
-        } else if (line.startsWith("bandwidth-weights ")) {
-          if (line.length() > "bandwidth-weights ".length()) {
-            for (String weight : line.substring("bandwidth-weights ".
-                length()).split(" ")) {
-              String weightName = weight.split("=")[0];
-              String weightValue = weight.split("=")[1];
-              this.bandwidthWeights.put(weightName, weightValue);
-            }
+        } else if (keyword.equals("s") || keyword.equals("v") ||
+            keyword.equals("w") || keyword.equals("p")) {
+          if (statusEntryLines == null) {
+            throw new DescriptorParseException(keyword + " line with no "
+                + "preceding r line.");
           }
-          
-        } else if (line.startsWith("directory-signature ")) {
-          String[] parts = line.split(" ");
-          String identity = parts[1];
-          String signingKeyDigest = parts[2];
-          this.directorySignatures.put(identity, signingKeyDigest);
+          statusEntryLines.append(line + "\n");
+        } else if (keyword.equals("bandwidth-weights")) {
+          this.parseBandwidthWeightsLine(line, parts);
+        } else if (keyword.equals("directory-signature")) {
+          this.parseDirectorySignatureLine(line, parts);
         } else if (line.equals("-----BEGIN SIGNATURE-----")) {
           skipSignature = true;
         } else if (line.equals("-----END SIGNATURE-----")) {
           skipSignature = false;
         } else if (!skipSignature) {
-          throw new RuntimeException("Unrecognized line '" + line + "'.");
+          /* TODO Is throwing an exception the right thing to do here?
+           * This is probably fine for development, but once the library
+           * is in production use, this seems annoying. */
+          throw new DescriptorParseException("Unrecognized line '" + line
+              + "'.");
         }
       }
     } catch (IOException e) {
       throw new RuntimeException("Internal error: Ran into an "
           + "IOException while parsing a String in memory.  Something's "
           + "really wrong.", e);
-    } catch (ParseException e) {
-      /* TODO Handle me. */
-    } catch (NumberFormatException e) {
-      /* TODO Handle me.  In theory, we shouldn't catch runtime
-       * exceptions, but in this case it keeps the parsing code small. */
-    } catch (ArrayIndexOutOfBoundsException e) {
-      /* TODO Handle me.  In theory, we shouldn't catch runtime
-       * exceptions, but in this case it keeps the parsing code small. */
     }
   }
 
-  private void checkConsistency() {
-    if (this.networkStatusVersion == 0) {
-      throw new RuntimeException("Consensus doesn't contain a "
-          + "'network-status-version' line.");
+  private void parseVoteStatusLine(String line, String[] parts)
+      throws DescriptorParseException {
+    this.parsedExactlyOnceKeyword("vote-status");
+    if (parts.length != 2 || !parts[1].equals("consensus")) {
+      throw new DescriptorParseException("Line '" + line + "' indicates "
+          + "that this is not a consensus.");
     }
-    if (this.consensusMethod == 0) {
-      throw new RuntimeException("Consensus doesn't contain a "
-          + "'consensus-method' line.");
+  }
+
+  private void parseConsensusMethodLine(String line, String[] parts)
+      throws DescriptorParseException {
+    this.parsedExactlyOnceKeyword("consensus-method");
+    if (parts.length != 2) {
+      throw new DescriptorParseException("Illegal line '" + line
+          + "' in consensus.");
     }
-    if (this.validAfterMillis == 0L) {
-      throw new RuntimeException("Consensus doesn't contain a "
-          + "'valid-after' line.");
+    try {
+      this.consensusMethod = Integer.parseInt(parts[1]);
+    } catch (NumberFormatException e) {
+      throw new DescriptorParseException("Illegal consensus method "
+          + "number in line '" + line + "'.");
     }
-    if (this.freshUntilMillis == 0L) {
-      throw new RuntimeException("Consensus doesn't contain a "
-          + "'fresh-until' line.");
+    if (this.consensusMethod < 1) {
+      throw new DescriptorParseException("Illegal consensus method "
+          + "number in line '" + line + "'.");
     }
-    if (this.validUntilMillis == 0L) {
-      throw new RuntimeException("Consensus doesn't contain a "
-          + "'valid-until' line.");
+  }
+
+  private void parseValidAfterLine(String line, String[] parts)
+      throws DescriptorParseException {
+    this.parsedExactlyOnceKeyword("valid-after");
+    this.validAfterMillis = ParseHelper.parseTimestampAtIndex(line, parts,
+        1, 2);
+  }
+
+  private void parseFreshUntilLine(String line, String[] parts)
+      throws DescriptorParseException {
+    this.parsedExactlyOnceKeyword("fresh-until");
+    this.freshUntilMillis = ParseHelper.parseTimestampAtIndex(line, parts,
+        1, 2);
+  }
+
+  private void parseValidUntilLine(String line, String[] parts)
+      throws DescriptorParseException {
+    this.parsedExactlyOnceKeyword("valid-until");
+    this.validUntilMillis = ParseHelper.parseTimestampAtIndex(line, parts,
+        1, 2);
+  }
+
+  private void parseVotingDelayLine(String line, String[] parts)
+      throws DescriptorParseException {
+    this.parsedExactlyOnceKeyword("voting-delay");
+    if (parts.length != 3) {
+      throw new DescriptorParseException("Wrong number of values in line "
+          + "'" + line + "'.");
     }
-    if (this.votingDelay.isEmpty()) {
-      throw new RuntimeException("Consensus doesn't contain a "
-          + "'voting-delay' line.");
+    try {
+      this.voteSeconds = Long.parseLong(parts[1]);
+      this.distSeconds = Long.parseLong(parts[2]);
+    } catch (NumberFormatException e) {
+      throw new DescriptorParseException("Illegal values in line '" + line
+          + "'.");
+    }
+  }
+
+  private void parseClientVersionsLine(String line, String[] parts)
+      throws DescriptorParseException {
+    this.parsedAtMostOnceKeyword("client-versions");
+    this.recommendedClientVersions = this.parseClientOrServerVersions(
+        line, parts);
+  }
+
+  private void parseServerVersionsLine(String line, String[] parts)
+      throws DescriptorParseException {
+    this.parsedAtMostOnceKeyword("server-versions");
+    this.recommendedServerVersions = this.parseClientOrServerVersions(
+        line, parts);
+  }
+
+  private SortedSet<String> parseClientOrServerVersions(String line,
+      String[] parts) throws DescriptorParseException {
+    SortedSet<String> result = new TreeSet<String>();
+    if (parts.length == 1) {
+      return result;
+    } else if (parts.length > 2) {
+      throw new DescriptorParseException("Illegal versions line '" + line
+          + "'.");
     }
-    if (this.knownFlags.isEmpty()) {
-      throw new RuntimeException("Consensus doesn't contain a "
-          + "'known-flags' line.");
+    String[] versions = parts[1].split(",", -1);
+    for (int i = 0; i < versions.length; i++) {
+      String version = versions[i];
+      if (version.length() < 1) {
+        throw new DescriptorParseException("Illegal versions line '"
+            + line + "'.");
+      }
+      result.add(version);
     }
-    if (this.dirSourceEntries.isEmpty()) {
-      throw new RuntimeException("Consensus doesn't contain any "
-          + "'dir-source' lines.");
+    return result;
+  }
+
+  private void parseKnownFlagsLine(String line, String[] parts)
+      throws DescriptorParseException {
+    this.parsedExactlyOnceKeyword("known-flags");
+    if (parts.length < 2) {
+      throw new DescriptorParseException("No known flags in line '" + line
+          + "'.");
     }
-    if (this.statusEntries.isEmpty()) {
-      throw new RuntimeException("Consensus doesn't contain any 'r' "
-          + "lines.");
+    for (int i = 1; i < parts.length; i++) {
+      this.knownFlags.add(parts[i]);
     }
   }
 
+  private void parseParamsLine(String line, String[] parts)
+      throws DescriptorParseException {
+    this.parsedAtMostOnceKeyword("params");
+    this.consensusParams = ParseHelper.parseKeyValuePairs(line, parts, 1);
+  }
+
+  private void parseDirSourceEntryLines(String string)
+      throws DescriptorParseException {
+    DirSourceEntry dirSourceEntry = new DirSourceEntryImpl(
+        string.getBytes());
+    this.dirSourceEntries.put(dirSourceEntry.getIdentity(),
+        dirSourceEntry);
+  }
+
+  private void parseStatusEntryLines(String string)
+      throws DescriptorParseException {
+    NetworkStatusEntryImpl statusEntry = new NetworkStatusEntryImpl(
+        string.getBytes());
+    this.statusEntries.put(statusEntry.getFingerprint(), statusEntry);
+  }
+
+  private void parseBandwidthWeightsLine(String line, String[] parts)
+      throws DescriptorParseException {
+    this.parsedAtMostOnceKeyword("bandwidth-weights");
+    this.bandwidthWeights = ParseHelper.parseKeyValuePairs(line, parts,
+        1);
+  }
+
+  private void parseDirectorySignatureLine(String line, String[] parts)
+      throws DescriptorParseException {
+    if (parts.length != 3) {
+      throw new DescriptorParseException("Illegal line '" + line + "'.");
+    }
+    String identity = ParseHelper.parseTwentyByteHexString(line,
+        parts[1]);
+    String signingKeyDigest = ParseHelper.parseTwentyByteHexString(line,
+        parts[2]);
+    this.directorySignatures.put(identity, signingKeyDigest);
+  }
+
   private byte[] consensusBytes;
   public byte[] getRawDescriptorBytes() {
     return this.consensusBytes;
@@ -252,9 +387,14 @@ public class RelayNetworkStatusConsensusImpl
     return this.validUntilMillis;
   }
 
-  private List<Long> votingDelay = new ArrayList<Long>();
-  public List<Long> getVotingDelay() {
-    return new ArrayList<Long>(this.votingDelay);
+  private long voteSeconds;
+  public long getVoteSeconds() {
+    return this.voteSeconds;
+  }
+
+  private long distSeconds;
+  public long getDistSeconds() {
+    return this.distSeconds;
   }
 
   private SortedSet<String> recommendedClientVersions;
@@ -274,10 +414,10 @@ public class RelayNetworkStatusConsensusImpl
     return new TreeSet<String>(this.knownFlags);
   }
 
-  private SortedMap<String, String> consensusParams =
-      new TreeMap<String, String>();
-  public SortedMap<String, String> getConsensusParams() {
-    return new TreeMap<String, String>(this.consensusParams);
+  private SortedMap<String, Integer> consensusParams;
+  public SortedMap<String, Integer> getConsensusParams() {
+    return this.consensusParams == null ? null:
+        new TreeMap<String, Integer>(this.consensusParams);
   }
 
   private SortedMap<String, DirSourceEntry> dirSourceEntries =
@@ -304,10 +444,10 @@ public class RelayNetworkStatusConsensusImpl
     return new TreeMap<String, String>(this.directorySignatures);
   }
 
-  private SortedMap<String, String> bandwidthWeights =
-      new TreeMap<String, String>();
-  public SortedMap<String, String> getBandwidthWeights() {
-    return new TreeMap<String, String>(this.bandwidthWeights);
+  private SortedMap<String, Integer> bandwidthWeights;
+  public SortedMap<String, Integer> getBandwidthWeights() {
+    return this.bandwidthWeights == null ? null :
+        new TreeMap<String, Integer>(this.bandwidthWeights);
   }
 }
 
diff --git a/src/org/torproject/descriptor/impl/RelayNetworkStatusVoteImpl.java b/src/org/torproject/descriptor/impl/RelayNetworkStatusVoteImpl.java
index bf2668b..238a512 100644
--- a/src/org/torproject/descriptor/impl/RelayNetworkStatusVoteImpl.java
+++ b/src/org/torproject/descriptor/impl/RelayNetworkStatusVoteImpl.java
@@ -149,11 +149,16 @@ public class RelayNetworkStatusVoteImpl
         } else if (line.startsWith("r ") ||
             line.equals("directory-footer")) {
           if (statusEntryLines != null) {
-            NetworkStatusEntryImpl statusEntry =
-                new NetworkStatusEntryImpl(
-                statusEntryLines.toString().getBytes());
-            this.statusEntries.put(statusEntry.getFingerprint(),
-                statusEntry);
+            try {
+              NetworkStatusEntryImpl statusEntry =
+                  new NetworkStatusEntryImpl(
+                  statusEntryLines.toString().getBytes());
+              this.statusEntries.put(statusEntry.getFingerprint(),
+                  statusEntry);
+            } catch (DescriptorParseException e) {
+              System.err.println("Could not parse status entry in vote.  "
+                  + "Skipping.");
+            }
             statusEntryLines = null;
           }
           if (line.startsWith("r ")) {
diff --git a/test/org/torproject/descriptor/impl/RelayNetworkStatusConsensusImplTest.java b/test/org/torproject/descriptor/impl/RelayNetworkStatusConsensusImplTest.java
index 4a7a7be..520473d 100644
--- a/test/org/torproject/descriptor/impl/RelayNetworkStatusConsensusImplTest.java
+++ b/test/org/torproject/descriptor/impl/RelayNetworkStatusConsensusImplTest.java
@@ -21,94 +21,120 @@ public class RelayNetworkStatusConsensusImplTest {
    * modifications requested by test methods. */
   private static class ConsensusBuilder {
     private String networkStatusVersionLine = "network-status-version 3";
-    private static void createWithNetworkStatusVersionLine(String line) {
+    private static RelayNetworkStatusConsensus
+        createWithNetworkStatusVersionLine(String line)
+        throws DescriptorParseException {
       ConsensusBuilder cb = new ConsensusBuilder();
       cb.networkStatusVersionLine = line;
-      new RelayNetworkStatusConsensusImpl(cb.buildConsensus());
+      return new RelayNetworkStatusConsensusImpl(cb.buildConsensus());
     }
     private String voteStatusLine = "vote-status consensus";
-    private static void createWithVoteStatusLine(String line) {
+    private static RelayNetworkStatusConsensus
+        createWithVoteStatusLine(String line)
+        throws DescriptorParseException {
       ConsensusBuilder cb = new ConsensusBuilder();
       cb.voteStatusLine = line;
-      new RelayNetworkStatusConsensusImpl(cb.buildConsensus());
+      return new RelayNetworkStatusConsensusImpl(cb.buildConsensus());
     }
     private String consensusMethodLine = "consensus-method 11";
-    private static void createWithConsensusMethodLine(String line) {
+    private static RelayNetworkStatusConsensus
+        createWithConsensusMethodLine(String line)
+        throws DescriptorParseException {
       ConsensusBuilder cb = new ConsensusBuilder();
       cb.consensusMethodLine = line;
-      new RelayNetworkStatusConsensusImpl(cb.buildConsensus());
+      return new RelayNetworkStatusConsensusImpl(cb.buildConsensus());
     }
     private String validAfterLine = "valid-after 2011-11-30 09:00:00";
-    private static void createWithValidAfterLine(String line) {
+    private static RelayNetworkStatusConsensus
+        createWithValidAfterLine(String line)
+        throws DescriptorParseException {
       ConsensusBuilder cb = new ConsensusBuilder();
       cb.validAfterLine = line;
-      new RelayNetworkStatusConsensusImpl(cb.buildConsensus());
+      return new RelayNetworkStatusConsensusImpl(cb.buildConsensus());
     }
     private String freshUntilLine = "fresh-until 2011-11-30 10:00:00";
-    private static void createWithFreshUntilLine(String line) {
+    private static RelayNetworkStatusConsensus
+        createWithFreshUntilLine(String line)
+        throws DescriptorParseException {
       ConsensusBuilder cb = new ConsensusBuilder();
       cb.freshUntilLine = line;
-      new RelayNetworkStatusConsensusImpl(cb.buildConsensus());
+      return new RelayNetworkStatusConsensusImpl(cb.buildConsensus());
     }
     private String validUntilLine = "valid-until 2011-11-30 12:00:00";
-    private static void createWithValidUntilLine(String line) {
+    private static RelayNetworkStatusConsensus
+        createWithValidUntilLine(String line)
+        throws DescriptorParseException {
       ConsensusBuilder cb = new ConsensusBuilder();
       cb.validUntilLine = line;
-      new RelayNetworkStatusConsensusImpl(cb.buildConsensus());
+      return new RelayNetworkStatusConsensusImpl(cb.buildConsensus());
     }
     private String votingDelayLine = "voting-delay 300 300";
-    private static void createWithVotingDelayLine(String line) {
+    private static RelayNetworkStatusConsensus
+        createWithVotingDelayLine(String line)
+        throws DescriptorParseException {
       ConsensusBuilder cb = new ConsensusBuilder();
       cb.votingDelayLine = line;
-      new RelayNetworkStatusConsensusImpl(cb.buildConsensus());
+      return new RelayNetworkStatusConsensusImpl(cb.buildConsensus());
     }
     private String clientVersionsLine = "client-versions 0.2.1.31,"
         + "0.2.2.34,0.2.3.6-alpha,0.2.3.7-alpha,0.2.3.8-alpha";
-    private static void createWithClientVersionsLine(String line) {
+    private static RelayNetworkStatusConsensus
+        createWithClientVersionsLine(String line)
+        throws DescriptorParseException {
       ConsensusBuilder cb = new ConsensusBuilder();
       cb.clientVersionsLine = line;
-      new RelayNetworkStatusConsensusImpl(cb.buildConsensus());
+      return new RelayNetworkStatusConsensusImpl(cb.buildConsensus());
     }
     private String serverVersionsLine = "server-versions 0.2.1.31,"
         + "0.2.2.34,0.2.3.6-alpha,0.2.3.7-alpha,0.2.3.8-alpha";
-    private static void createWithServerVersionsLine(String line) {
+    private static RelayNetworkStatusConsensus
+        createWithServerVersionsLine(String line)
+        throws DescriptorParseException {
       ConsensusBuilder cb = new ConsensusBuilder();
       cb.serverVersionsLine = line;
-      new RelayNetworkStatusConsensusImpl(cb.buildConsensus());
+      return new RelayNetworkStatusConsensusImpl(cb.buildConsensus());
     }
     private String knownFlagsLine = "known-flags Authority BadExit Exit "
         + "Fast Guard HSDir Named Running Stable Unnamed V2Dir Valid";
-    private static void createWithKnownFlagsLine(String line) {
+    private static RelayNetworkStatusConsensus
+        createWithKnownFlagsLine(String line)
+        throws DescriptorParseException {
       ConsensusBuilder cb = new ConsensusBuilder();
       cb.knownFlagsLine = line;
-      new RelayNetworkStatusConsensusImpl(cb.buildConsensus());
+      return new RelayNetworkStatusConsensusImpl(cb.buildConsensus());
     }
     private String paramsLine = "params "
         + "CircuitPriorityHalflifeMsec=30000 bwauthbestratio=1 "
         + "bwauthcircs=1 bwauthdescbw=0 bwauthkp=10000 bwauthpid=1 "
         + "bwauthtd=5000 bwauthti=50000 bwauthtidecay=5000 cbtnummodes=3 "
         + "cbtquantile=80 circwindow=1000 refuseunknownexits=1";
-    private static void createWithParamsLine(String line) {
+    private static RelayNetworkStatusConsensus
+        createWithParamsLine(String line)
+        throws DescriptorParseException {
       ConsensusBuilder cb = new ConsensusBuilder();
       cb.paramsLine = line;
-      new RelayNetworkStatusConsensusImpl(cb.buildConsensus());
+      return new RelayNetworkStatusConsensusImpl(cb.buildConsensus());
     }
     private List<String> dirSources = new ArrayList<String>();
     private List<String> statusEntries = new ArrayList<String>();
     private String directoryFooterLine = "directory-footer";
-    private static void createWithDirectoryFooterLine(String line) {
+    private static RelayNetworkStatusConsensus
+        createWithDirectoryFooterLine(String line)
+        throws DescriptorParseException {
       ConsensusBuilder cb = new ConsensusBuilder();
       cb.directoryFooterLine = line;
-      new RelayNetworkStatusConsensusImpl(cb.buildConsensus());
+      return new RelayNetworkStatusConsensusImpl(cb.buildConsensus());
     }
     private String bandwidthWeightsLine = "bandwidth-weights Wbd=285 "
         + "Wbe=0 Wbg=0 Wbm=10000 Wdb=10000 Web=10000 Wed=1021 Wee=10000 "
         + "Weg=1021 Wem=10000 Wgb=10000 Wgd=8694 Wgg=10000 Wgm=10000 "
         + "Wmb=10000 Wmd=285 Wme=0 Wmg=0 Wmm=10000";
-    private static void createWithBandwidthWeightsLine(String line) {
+    private static RelayNetworkStatusConsensus
+        createWithBandwidthWeightsLine(String line)
+        throws DescriptorParseException {
       ConsensusBuilder cb = new ConsensusBuilder();
       cb.bandwidthWeightsLine = line;
-      new RelayNetworkStatusConsensusImpl(cb.buildConsensus());
+      return new RelayNetworkStatusConsensusImpl(cb.buildConsensus());
     }
     private List<String> directorySignatures = new ArrayList<String>();
     private ConsensusBuilder() {
@@ -217,60 +243,78 @@ public class RelayNetworkStatusConsensusImplTest {
   /* Helper class to build a directory source based on default data and
    * modifications requested by test methods. */
   private static class DirSourceBuilder {
-    private static void createWithDirSource(String dirSourceString) {
+    private static RelayNetworkStatusConsensus
+        createWithDirSource(String dirSourceString)
+        throws DescriptorParseException {
       ConsensusBuilder cb = new ConsensusBuilder();
       cb.dirSources.add(dirSourceString);
-      new RelayNetworkStatusConsensusImpl(cb.buildConsensus());
+      return new RelayNetworkStatusConsensusImpl(cb.buildConsensus());
     }
     private String nickname = "gabelmoo";
-    private static void createWithNickname(String string) {
+    private static RelayNetworkStatusConsensus
+        createWithNickname(String string)
+        throws DescriptorParseException {
       DirSourceBuilder dsb = new DirSourceBuilder();
       dsb.nickname = string;
-      createWithDirSource(dsb.buildDirSource());
+      return createWithDirSource(dsb.buildDirSource());
     }
     private String identity = "ED03BB616EB2F60BEC80151114BB25CEF515B226";
-    private static void createWithIdentity(String string) {
+    private static RelayNetworkStatusConsensus
+        createWithIdentity(String string)
+        throws DescriptorParseException {
       DirSourceBuilder dsb = new DirSourceBuilder();
       dsb.identity = string;
-      createWithDirSource(dsb.buildDirSource());
+      return createWithDirSource(dsb.buildDirSource());
     }
     private String hostName = "212.112.245.170";
-    private static void createWithHostName(String string) {
+    private static RelayNetworkStatusConsensus
+        createWithHostName(String string)
+        throws DescriptorParseException {
       DirSourceBuilder dsb = new DirSourceBuilder();
       dsb.hostName = string;
-      createWithDirSource(dsb.buildDirSource());
+      return createWithDirSource(dsb.buildDirSource());
     }
     private String address = "212.112.245.170";
-    private static void createWithAddress(String string) {
+    private static RelayNetworkStatusConsensus
+        createWithAddress(String string)
+        throws DescriptorParseException {
       DirSourceBuilder dsb = new DirSourceBuilder();
       dsb.address = string;
-      createWithDirSource(dsb.buildDirSource());
+      return createWithDirSource(dsb.buildDirSource());
     }
     private String dirPort = "80";
-    private static void createWithDirPort(String string) {
+    private static RelayNetworkStatusConsensus
+        createWithDirPort(String string)
+        throws DescriptorParseException {
       DirSourceBuilder dsb = new DirSourceBuilder();
       dsb.dirPort = string;
-      createWithDirSource(dsb.buildDirSource());
+      return createWithDirSource(dsb.buildDirSource());
     }
     private String orPort = "443";
-    private static void createWithOrPort(String string) {
+    private static RelayNetworkStatusConsensus
+        createWithOrPort(String string)
+        throws DescriptorParseException {
       DirSourceBuilder dsb = new DirSourceBuilder();
       dsb.orPort = string;
-      createWithDirSource(dsb.buildDirSource());
+      return createWithDirSource(dsb.buildDirSource());
     }
     private String contactLine = "contact 4096R/C5AA446D Sebastian Hahn "
         + "<tor at sebastianhahn.net>";
-    private static void createWithContactLine(String line) {
+    private static RelayNetworkStatusConsensus
+        createWithContactLine(String line)
+        throws DescriptorParseException {
       DirSourceBuilder dsb = new DirSourceBuilder();
       dsb.contactLine = line;
-      createWithDirSource(dsb.buildDirSource());
+      return createWithDirSource(dsb.buildDirSource());
     }
     private String voteDigestLine =
         "vote-digest 0F398A5834D2C139E1D92310B09F814F243354D1";
-    private static void createWithVoteDigestLine(String line) {
+    private static RelayNetworkStatusConsensus
+        createWithVoteDigestLine(String line)
+        throws DescriptorParseException {
       DirSourceBuilder dsb = new DirSourceBuilder();
       dsb.voteDigestLine = line;
-      createWithDirSource(dsb.buildDirSource());
+      return createWithDirSource(dsb.buildDirSource());
     }
     private String buildDirSource() {
       StringBuilder sb = new StringBuilder();
@@ -294,76 +338,93 @@ public class RelayNetworkStatusConsensusImplTest {
   /* Helper class to build a status entry based on default data and
    * modifications requested by test methods. */
   private static class StatusEntryBuilder {
-    private static void createWithStatusEntry(String statusEntryString) {
+    private static RelayNetworkStatusConsensus
+        createWithStatusEntry(String statusEntryString)
+        throws DescriptorParseException {
       ConsensusBuilder cb = new ConsensusBuilder();
       cb.statusEntries.add(statusEntryString);
-      new RelayNetworkStatusConsensusImpl(cb.buildConsensus());
+      return new RelayNetworkStatusConsensusImpl(cb.buildConsensus());
     }
     private String nickname = "right2privassy3";
-    private static void createWithNickname(String string) {
+    private static RelayNetworkStatusConsensus
+        createWithNickname(String string)
+        throws DescriptorParseException {
       StatusEntryBuilder seb = new StatusEntryBuilder();
       seb.nickname = string;
-      createWithStatusEntry(seb.buildStatusEntry());
+      return createWithStatusEntry(seb.buildStatusEntry());
     }
     private String fingerprintBase64 = "ADQ6gCT3DiFHKPDFr3rODBUI8HM";
-    private static void createWithFingerprintBase64(String string) {
+    private static RelayNetworkStatusConsensus
+        createWithFingerprintBase64(String string)
+        throws DescriptorParseException {
       StatusEntryBuilder seb = new StatusEntryBuilder();
       seb.fingerprintBase64 = string;
-      createWithStatusEntry(seb.buildStatusEntry());
+      return createWithStatusEntry(seb.buildStatusEntry());
     }
     private String descriptorBase64 = "Yiti+nayuT2Efe2X1+M4nslwVuU";
-    private static void createWithDescriptorBase64(String string) {
+    private static RelayNetworkStatusConsensus
+        createWithDescriptorBase64(String string)
+        throws DescriptorParseException {
       StatusEntryBuilder seb = new StatusEntryBuilder();
       seb.descriptorBase64 = string;
-      createWithStatusEntry(seb.buildStatusEntry());
+      return createWithStatusEntry(seb.buildStatusEntry());
     }
     private String publishedString = "2011-11-29 21:34:27";
-    private static void createWithPublishedString(String string) {
+    private static RelayNetworkStatusConsensus
+        createWithPublishedString(String string)
+        throws DescriptorParseException {
       StatusEntryBuilder seb = new StatusEntryBuilder();
       seb.publishedString = string;
-      createWithStatusEntry(seb.buildStatusEntry());
+      return createWithStatusEntry(seb.buildStatusEntry());
     }
     private String address = "50.63.8.215";
-    private static void createWithAddress(String string) {
+    private static RelayNetworkStatusConsensus
+        createWithAddress(String string) throws DescriptorParseException {
       StatusEntryBuilder seb = new StatusEntryBuilder();
       seb.address = string;
-      createWithStatusEntry(seb.buildStatusEntry());
+      return createWithStatusEntry(seb.buildStatusEntry());
     }
     private String orPort = "9023";
-    private static void createWithOrPort(String string) {
+    private static RelayNetworkStatusConsensus
+        createWithOrPort(String string) throws DescriptorParseException {
       StatusEntryBuilder seb = new StatusEntryBuilder();
       seb.orPort = string;
-      createWithStatusEntry(seb.buildStatusEntry());
+      return createWithStatusEntry(seb.buildStatusEntry());
     }
     private String dirPort = "0";
-    private static void createWithDirPort(String string) {
+    private static RelayNetworkStatusConsensus
+        createWithDirPort(String string) throws DescriptorParseException {
       StatusEntryBuilder seb = new StatusEntryBuilder();
       seb.dirPort = string;
-      createWithStatusEntry(seb.buildStatusEntry());
+      return createWithStatusEntry(seb.buildStatusEntry());
     }
     private String sLine = "s Exit Fast Named Running Stable Valid";
-    private static void createWithSLine(String line) {
+    private static RelayNetworkStatusConsensus
+        createWithSLine(String line) throws DescriptorParseException {
       StatusEntryBuilder seb = new StatusEntryBuilder();
       seb.sLine = line;
-      createWithStatusEntry(seb.buildStatusEntry());
+      return createWithStatusEntry(seb.buildStatusEntry());
     }
     private String vLine = "v Tor 0.2.1.29 (r8e9b25e6c7a2e70c)";
-    private static void createWithVLine(String line) {
+    private static RelayNetworkStatusConsensus
+        createWithVLine(String line) throws DescriptorParseException {
       StatusEntryBuilder seb = new StatusEntryBuilder();
       seb.vLine = line;
-      createWithStatusEntry(seb.buildStatusEntry());
+      return createWithStatusEntry(seb.buildStatusEntry());
     }
     private String wLine = "w Bandwidth=1";
-    private static void createWithWLine(String line) {
+    private static RelayNetworkStatusConsensus
+        createWithWLine(String line) throws DescriptorParseException {
       StatusEntryBuilder seb = new StatusEntryBuilder();
       seb.wLine = line;
-      createWithStatusEntry(seb.buildStatusEntry());
+      return createWithStatusEntry(seb.buildStatusEntry());
     }
     private String pLine = "p accept 80,1194,1220,1293";
-    private static void createWithPLine(String line) {
+    private static RelayNetworkStatusConsensus
+        createWithPLine(String line) throws DescriptorParseException {
       StatusEntryBuilder seb = new StatusEntryBuilder();
       seb.pLine = line;
-      createWithStatusEntry(seb.buildStatusEntry());
+      return createWithStatusEntry(seb.buildStatusEntry());
     }
     private String buildStatusEntry() {
       StringBuilder sb = new StringBuilder();
@@ -393,24 +454,29 @@ public class RelayNetworkStatusConsensusImplTest {
   /* Helper class to build a directory signature based on default data and
    * modifications requested by test methods. */
   private static class DirectorySignatureBuilder {
-    private static void createWithDirectorySignature(
-        String directorySignatureString) {
+    private static RelayNetworkStatusConsensus
+        createWithDirectorySignature(String directorySignatureString)
+        throws DescriptorParseException {
       ConsensusBuilder cb = new ConsensusBuilder();
       cb.directorySignatures.add(directorySignatureString);
-      new RelayNetworkStatusConsensusImpl(cb.buildConsensus());
+      return new RelayNetworkStatusConsensusImpl(cb.buildConsensus());
     }
     private String identity = "ED03BB616EB2F60BEC80151114BB25CEF515B226";
-    private static void createWithIdentity(String string) {
+    private static RelayNetworkStatusConsensus
+        createWithIdentity(String string)
+        throws DescriptorParseException {
       DirectorySignatureBuilder dsb = new DirectorySignatureBuilder();
       dsb.identity = string;
-      createWithDirectorySignature(dsb.buildDirectorySignature());
+      return createWithDirectorySignature(dsb.buildDirectorySignature());
     }
     private String signingKey =
         "845CF1D0B370CA443A8579D18E7987E7E532F639";
-    private static void createWithSigningKey(String string) {
+    private static RelayNetworkStatusConsensus
+        createWithSigningKey(String string)
+        throws DescriptorParseException {
       DirectorySignatureBuilder dsb = new DirectorySignatureBuilder();
       dsb.signingKey = string;
-      createWithDirectorySignature(dsb.buildDirectorySignature());
+      return createWithDirectorySignature(dsb.buildDirectorySignature());
     }
     private String buildDirectorySignature() {
       String directorySignature = "directory-signature " + identity + " "
@@ -426,505 +492,622 @@ public class RelayNetworkStatusConsensusImplTest {
     }
   }
 
-  /* TODO We should check this. */
   @Test()
-  public void testSampleConsensus() {
+  public void testSampleConsensus() throws DescriptorParseException {
     ConsensusBuilder cb = new ConsensusBuilder();
-    new RelayNetworkStatusConsensusImpl(cb.buildConsensus());
+    RelayNetworkStatusConsensus consensus =
+        new RelayNetworkStatusConsensusImpl(cb.buildConsensus());
+    assertEquals(3, consensus.getNetworkStatusVersion());
+    assertEquals(11, consensus.getConsensusMethod());
+    assertEquals(1322643600000L, consensus.getValidAfterMillis());
+    assertEquals(1322647200000L, consensus.getFreshUntilMillis());
+    assertEquals(1322654400000L, consensus.getValidUntilMillis());
+    assertEquals(300L, consensus.getVoteSeconds());
+    assertEquals(300L, consensus.getDistSeconds());
+    assertTrue(consensus.getRecommendedClientVersions().contains(
+        "0.2.3.8-alpha"));
+    assertTrue(consensus.getRecommendedServerVersions().contains(
+        "0.2.3.8-alpha"));
+    assertTrue(consensus.getKnownFlags().contains("Running"));
+    assertEquals(30000, (int) consensus.getConsensusParams().get(
+        "CircuitPriorityHalflifeMsec"));
+    assertEquals("86.59.21.38", consensus.getDirSourceEntries().get(
+        "14C131DFC5C6F93646BE72FA1401C02A8DF2E8B4").getIp());
+    assertTrue(consensus.containsStatusEntry(
+        "00795A6E8D91C270FC23B30F388A495553E01894"));
+    assertEquals("188.177.149.216", consensus.getStatusEntry(
+        "00795A6E8D91C270FC23B30F388A495553E01894").getAddress());
+    assertEquals("3509BA5A624403A905C74DA5C8A0CEC9E0D3AF86",
+        consensus.getDirectorySignatures().get(
+        "14C131DFC5C6F93646BE72FA1401C02A8DF2E8B4"));
+    assertEquals(285, (int) consensus.getBandwidthWeights().get("Wbd"));
+  }
+
+  @Test(expected = DescriptorParseException.class)
+  public void testNetworkStatusVersionNoLine()
+      throws DescriptorParseException {
+    ConsensusBuilder.createWithNetworkStatusVersionLine(null);
   }
 
-  /* TODO Throwing a RuntimeException here (and in most places below) is
-   * bad.  Maybe we should define a DescriptorParseException. */
-  @Test(expected = RuntimeException.class)
-  public void testNetworkStatusVersionNoLine() {
-    ConsensusBuilder.createWithNetworkStatusVersionLine(null);
+  @Test(expected = DescriptorParseException.class)
+  public void testNetworkStatusVersionNewLine()
+      throws DescriptorParseException {
+    ConsensusBuilder.createWithNetworkStatusVersionLine(
+        "network-status-version 3\n");
   }
 
-  @Test(expected = RuntimeException.class)
-  public void testNetworkStatusVersionPrefixLineAtChar() {
+  @Test(expected = DescriptorParseException.class)
+  public void testNetworkStatusVersionPrefixLineAtChar()
+      throws DescriptorParseException {
     ConsensusBuilder.createWithNetworkStatusVersionLine(
         "@consensus\nnetwork-status-version 3");
   }
 
-  /* TODO This doesn't break.  Should it? */
-  @Test()
-  public void testNetworkStatusVersionPrefixDirectoryFooter() {
+  @Test(expected = DescriptorParseException.class)
+  public void testNetworkStatusVersionPrefixLine()
+      throws DescriptorParseException {
     ConsensusBuilder.createWithNetworkStatusVersionLine(
         "directory-footer\nnetwork-status-version 3");
   }
-  
-  @Test(expected = RuntimeException.class)
-  public void testNetworkStatusVersionPrefixLinePoundChar() {
+
+  @Test(expected = DescriptorParseException.class)
+  public void testNetworkStatusVersionPrefixLinePoundChar()
+      throws DescriptorParseException {
     ConsensusBuilder.createWithNetworkStatusVersionLine(
         "#consensus\nnetwork-status-version 3");
   }
-  
-  @Test(expected = RuntimeException.class)
-  public void testNetworkStatusVersionNoSpace() {
+
+  @Test(expected = DescriptorParseException.class)
+  public void testNetworkStatusVersionNoSpace()
+      throws DescriptorParseException {
     ConsensusBuilder.createWithNetworkStatusVersionLine(
         "network-status-version");
   }
 
-  @Test(expected = RuntimeException.class)
-  public void testNetworkStatusVersionOneSpace() {
+  @Test(expected = DescriptorParseException.class)
+  public void testNetworkStatusVersionOneSpace()
+      throws DescriptorParseException {
     ConsensusBuilder.createWithNetworkStatusVersionLine(
         "network-status-version ");
   }
 
-  /* TODO The parser should only accept version 3 and throw an Exception
-   * here. */
-  @Test()
-  public void testNetworkStatusVersion42() {
+  @Test(expected = DescriptorParseException.class)
+  public void testNetworkStatusVersion42()
+      throws DescriptorParseException {
     ConsensusBuilder.createWithNetworkStatusVersionLine(
         "network-status-version 42");
   }
 
-  @Test(expected = RuntimeException.class)
-  public void testNetworkStatusVersionFourtyTwo() {
+  @Test(expected = DescriptorParseException.class)
+  public void testNetworkStatusVersionFourtyTwo()
+      throws DescriptorParseException {
     ConsensusBuilder.createWithNetworkStatusVersionLine(
         "network-status-version FourtyTwo");
   }
 
-  /* TODO Shouldn't this throw an exception? */
-  @Test()
-  public void testVoteStatusNoLine() {
+  @Test(expected = DescriptorParseException.class)
+  public void testVoteStatusNoLine() throws DescriptorParseException {
     ConsensusBuilder.createWithVoteStatusLine(null);
   }
 
-  @Test(expected = RuntimeException.class)
-  public void testVoteStatusNoSpace() {
+  @Test(expected = DescriptorParseException.class)
+  public void testNetworkStatusVersionSpaceBefore()
+      throws DescriptorParseException {
+    ConsensusBuilder.createWithNetworkStatusVersionLine(
+        " network-status-version 3");
+  }
+
+  @Test(expected = DescriptorParseException.class)
+  public void testVoteStatusSpaceBefore() throws DescriptorParseException {
+    ConsensusBuilder.createWithVoteStatusLine(" vote-status consensus");
+  }
+
+  @Test(expected = DescriptorParseException.class)
+  public void testVoteStatusNoSpace() throws DescriptorParseException {
     ConsensusBuilder.createWithVoteStatusLine("vote-status");
   }
 
-  @Test(expected = RuntimeException.class)
-  public void testVoteStatusOneSpace() {
+  @Test(expected = DescriptorParseException.class)
+  public void testVoteStatusOneSpace() throws DescriptorParseException {
     ConsensusBuilder.createWithVoteStatusLine("vote-status ");
   }
 
-  /* TODO Should this be accepted or not? */
-  @Test(expected = RuntimeException.class)
-  public void testVoteStatusConsensusOneSpace() {
+  @Test()
+  public void testVoteStatusConsensusOneSpace()
+      throws DescriptorParseException {
     ConsensusBuilder.createWithVoteStatusLine("vote-status consensus ");
   }
 
-  @Test(expected = RuntimeException.class)
-  public void testVoteStatusVote() {
+  @Test(expected = DescriptorParseException.class)
+  public void testVoteStatusVote() throws DescriptorParseException {
     ConsensusBuilder.createWithVoteStatusLine("vote-status vote");
   }
 
-  @Test(expected = RuntimeException.class)
-  public void testVoteStatusTheMagicVoteStatus() {
+  @Test(expected = DescriptorParseException.class)
+  public void testVoteStatusTheMagicVoteStatus()
+      throws DescriptorParseException {
     ConsensusBuilder.createWithVoteStatusLine(
         "vote-status TheMagicVoteStatus");
   }
 
-  @Test(expected = RuntimeException.class)
-  public void testConsensusMethodNoLine() {
+  @Test(expected = DescriptorParseException.class)
+  public void testConsensusMethodNoLine()
+      throws DescriptorParseException {
     ConsensusBuilder.createWithConsensusMethodLine(null);
   }
 
-  @Test(expected = RuntimeException.class)
-  public void testConsensusMethodNoSpace() {
+  @Test(expected = DescriptorParseException.class)
+  public void testConsensusMethodNoSpace()
+      throws DescriptorParseException {
     ConsensusBuilder.createWithConsensusMethodLine("consensus-method");
   }
 
-  @Test(expected = RuntimeException.class)
-  public void testConsensusMethodOneSpace() {
+  @Test(expected = DescriptorParseException.class)
+  public void testConsensusMethodOneSpace()
+      throws DescriptorParseException {
     ConsensusBuilder.createWithConsensusMethodLine("consensus-method ");
   }
 
-  @Test(expected = RuntimeException.class)
-  public void testConsensusMethodEleven() {
+  @Test(expected = DescriptorParseException.class)
+  public void testConsensusMethodEleven()
+      throws DescriptorParseException {
     ConsensusBuilder.createWithConsensusMethodLine(
         "consensus-method eleven");
   }
 
-  /* TODO We shouldn't allow negative values here. */
-  @Test()
-  public void testConsensusMethodMinusOne() {
+  @Test(expected = DescriptorParseException.class)
+  public void testConsensusMethodMinusOne()
+      throws DescriptorParseException {
     ConsensusBuilder.createWithConsensusMethodLine("consensus-method -1");
   }
 
-  @Test(expected = RuntimeException.class)
-  public void testConsensusMethodNinePeriod() {
+  @Test(expected = DescriptorParseException.class)
+  public void testConsensusMethodNinePeriod()
+      throws DescriptorParseException {
     ConsensusBuilder.createWithConsensusMethodLine("consensus-method "
         + "999999999999999999999999999999999999999999999999999999999999");
   }
 
-  @Test(expected = RuntimeException.class)
-  public void testValidAfterNoLine() {
+  @Test(expected = DescriptorParseException.class)
+  public void testConsensusMethodTwoLines()
+      throws DescriptorParseException {
+    ConsensusBuilder.createWithConsensusMethodLine(
+        "consensus-method 1\nconsensus-method 1");
+  }
+
+  @Test(expected = DescriptorParseException.class)
+  public void testValidAfterNoLine() throws DescriptorParseException {
     ConsensusBuilder.createWithValidAfterLine(null);
   }
 
-  @Test(expected = RuntimeException.class)
-  public void testValidAfterNoSpace() {
+  @Test(expected = DescriptorParseException.class)
+  public void testValidAfterNoSpace() throws DescriptorParseException {
     ConsensusBuilder.createWithValidAfterLine("valid-after");
   }
 
-  @Test(expected = RuntimeException.class)
-  public void testValidAfterOneSpace() {
+  @Test(expected = DescriptorParseException.class)
+  public void testValidAfterOneSpace() throws DescriptorParseException {
     ConsensusBuilder.createWithValidAfterLine("valid-after ");
   }
 
-  @Test(expected = RuntimeException.class)
-  public void testValidAfterLongAgo() {
+  @Test(expected = DescriptorParseException.class)
+  public void testValidAfterLongAgo() throws DescriptorParseException {
     ConsensusBuilder.createWithValidAfterLine("valid-after long ago");
   }
 
-  /* TODO Wow, this should really throw an exception! */
-  @Test()
-  public void testValidAfterFeb30() {
+  @Test(expected = DescriptorParseException.class)
+  public void testValidAfterFeb30() throws DescriptorParseException {
     ConsensusBuilder.createWithValidAfterLine(
         "valid-after 2011-02-30 09:00:00");
   }
 
-  @Test(expected = RuntimeException.class)
-  public void testFreshUntilNoLine() {
+  @Test(expected = DescriptorParseException.class)
+  public void testFreshUntilNoLine() throws DescriptorParseException {
     ConsensusBuilder.createWithFreshUntilLine(null);
   }
 
-  @Test(expected = RuntimeException.class)
-  public void testFreshUntilAroundTen() {
+  @Test(expected = DescriptorParseException.class)
+  public void testFreshUntilAroundTen() throws DescriptorParseException {
     ConsensusBuilder.createWithFreshUntilLine(
         "fresh-until 2011-11-30 around ten");
   }
 
-  @Test(expected = RuntimeException.class)
-  public void testValidUntilTomorrowMorning() {
+  @Test(expected = DescriptorParseException.class)
+  public void testValidUntilTomorrowMorning()
+      throws DescriptorParseException {
     ConsensusBuilder.createWithValidUntilLine(
         "valid-until tomorrow morning");
   }
 
-  @Test(expected = RuntimeException.class)
-  public void testVotingDelayNoLine() {
+  @Test(expected = DescriptorParseException.class)
+  public void testVotingDelayNoLine() throws DescriptorParseException {
     ConsensusBuilder.createWithVotingDelayLine(null);
   }
 
-  @Test(expected = RuntimeException.class)
-  public void testVotingDelayNoSpace() {
+  @Test(expected = DescriptorParseException.class)
+  public void testVotingDelayNoSpace() throws DescriptorParseException {
     ConsensusBuilder.createWithVotingDelayLine("voting-delay");
   }
 
-  @Test(expected = RuntimeException.class)
-  public void testVotingDelayOneSpace() {
+  @Test(expected = DescriptorParseException.class)
+  public void testVotingDelayOneSpace() throws DescriptorParseException {
     ConsensusBuilder.createWithVotingDelayLine("voting-delay ");
   }
 
-  /* TODO This should throw an exception. */
-  @Test()
-  public void testVotingDelayTriple() {
+  @Test(expected = DescriptorParseException.class)
+  public void testVotingDelayTriple() throws DescriptorParseException {
     ConsensusBuilder.createWithVotingDelayLine(
         "voting-delay 300 300 300");
   }
 
-  /* TODO This should throw an exception. */
-  @Test()
-  public void testVotingDelaySingle() {
+  @Test(expected = DescriptorParseException.class)
+  public void testVotingDelaySingle() throws DescriptorParseException {
     ConsensusBuilder.createWithVotingDelayLine("voting-delay 300");
   }
 
-  @Test(expected = RuntimeException.class)
-  public void testVotingDelayOneTwo() {
+  @Test(expected = DescriptorParseException.class)
+  public void testVotingDelayOneTwo() throws DescriptorParseException {
     ConsensusBuilder.createWithVotingDelayLine("voting-delay one two");
   }
 
-  /* TODO Should this be forbidden? */
   @Test()
-  public void testClientVersionsNoLineServerVersionsNoLine() {
+  public void testClientServerVersionsNoLine()
+      throws DescriptorParseException {
     ConsensusBuilder cb = new ConsensusBuilder();
     cb.clientVersionsLine = null;
     cb.serverVersionsLine = null;
-    new RelayNetworkStatusConsensusImpl(cb.buildConsensus());
+    RelayNetworkStatusConsensus consensus =
+        new RelayNetworkStatusConsensusImpl(cb.buildConsensus());
+    assertNull(consensus.getRecommendedClientVersions());
+    assertNull(consensus.getRecommendedServerVersions());
   }
 
-  /* TODO Should this be forbidden? */
   @Test()
-  public void testServerVersionsNoLine() {
-    ConsensusBuilder.createWithServerVersionsLine(null);
+  public void testServerVersionsNoLine() throws DescriptorParseException {
+    RelayNetworkStatusConsensus consensus =
+        ConsensusBuilder.createWithServerVersionsLine(null);
+    assertNotNull(consensus.getRecommendedClientVersions());
+    assertNull(consensus.getRecommendedServerVersions());
   }
 
-  /* TODO Should this be forbidden? */
   @Test()
-  public void testClientVersionsNoLine() {
-    ConsensusBuilder.createWithClientVersionsLine(null);
+  public void testClientVersionsNoLine() throws DescriptorParseException {
+    RelayNetworkStatusConsensus consensus =
+        ConsensusBuilder.createWithClientVersionsLine(null);
+    assertNull(consensus.getRecommendedClientVersions());
+    assertNotNull(consensus.getRecommendedServerVersions());
   }
 
-  @Test(expected = RuntimeException.class)
-  public void testClientVersionsNoSpace() {
-    ConsensusBuilder.createWithClientVersionsLine("client-versions");
+  @Test()
+  public void testClientVersionsNoSpace()
+      throws DescriptorParseException {
+    RelayNetworkStatusConsensus consensus =
+        ConsensusBuilder.createWithClientVersionsLine("client-versions");
+    assertNotNull(consensus.getRecommendedClientVersions());
+    assertTrue(consensus.getRecommendedClientVersions().isEmpty());
   }
 
-  @Test(expected = RuntimeException.class)
-  public void testClientVersionsOneSpace() {
-    ConsensusBuilder.createWithClientVersionsLine("client-versions ");
+  @Test()
+  public void testClientVersionsOneSpace()
+      throws DescriptorParseException {
+    RelayNetworkStatusConsensus consensus =
+        ConsensusBuilder.createWithClientVersionsLine("client-versions ");
+    assertNotNull(consensus.getRecommendedClientVersions());
+    assertTrue(consensus.getRecommendedClientVersions().isEmpty());
   }
 
-  /* TODO This should be caught. */
-  @Test()
-  public void testClientVersionsComma() {
+  @Test(expected = DescriptorParseException.class)
+  public void testClientVersionsComma() throws DescriptorParseException {
     ConsensusBuilder.createWithClientVersionsLine("client-versions ,");
   }
 
-  /* TODO This should be caught. */
-  @Test()
-  public void testClientVersionsCommaVersion() {
+  @Test(expected = DescriptorParseException.class)
+  public void testClientVersionsCommaVersion()
+      throws DescriptorParseException {
     ConsensusBuilder.createWithClientVersionsLine(
         "client-versions ,0.2.2.34");
   }
 
-  @Test(expected = RuntimeException.class)
-  public void testKnownFlagsNoLine() {
+  @Test(expected = DescriptorParseException.class)
+  public void testKnownFlagsNoLine() throws DescriptorParseException {
     ConsensusBuilder.createWithKnownFlagsLine(null);
   }
 
-  @Test(expected = RuntimeException.class)
-  public void testKnownFlagsNoSpace() {
+  @Test(expected = DescriptorParseException.class)
+  public void testKnownFlagsNoSpace() throws DescriptorParseException {
     ConsensusBuilder.createWithKnownFlagsLine("known-flags");
   }
 
-  /* TODO Looks like this okay, right? */
-  @Test()
-  public void testKnownFlagsOneSpace() {
+  @Test(expected = DescriptorParseException.class)
+  public void testKnownFlagsOneSpace() throws DescriptorParseException {
     ConsensusBuilder.createWithKnownFlagsLine("known-flags ");
   }
 
-  /* TODO Make sure that the params line is optional. */
   @Test()
-  public void testParamsNoLine() {
-    ConsensusBuilder.createWithParamsLine(null);
+  public void testParamsNoLine() throws DescriptorParseException {
+    RelayNetworkStatusConsensus consensus =
+        ConsensusBuilder.createWithParamsLine(null);
+    assertNull(consensus.getConsensusParams());
   }
 
-  /* TODO If it's okay to provide an empty params line, this one should be
-   * accepted, too. */
-  @Test(expected = RuntimeException.class)
-  public void testParamsNoSpace() {
-    ConsensusBuilder.createWithParamsLine("params");
+  @Test()
+  public void testParamsNoSpace() throws DescriptorParseException {
+    RelayNetworkStatusConsensus consensus =
+        ConsensusBuilder.createWithParamsLine("params");
+    assertNotNull(consensus.getConsensusParams());
+    assertTrue(consensus.getConsensusParams().isEmpty());
   }
 
-  /* TODO Is this okay? */
   @Test()
-  public void testParamsOneSpace() {
-    ConsensusBuilder.createWithParamsLine("params ");
+  public void testParamsOneSpace() throws DescriptorParseException {
+    RelayNetworkStatusConsensus consensus =
+        ConsensusBuilder.createWithParamsLine("params ");
+    assertNotNull(consensus.getConsensusParams());
+    assertTrue(consensus.getConsensusParams().isEmpty());
   }
 
-  /* TODO Hmm, and this is okay? */
   @Test()
-  public void testParamsThreeSpaces() {
-    ConsensusBuilder.createWithParamsLine("params   ");
+  public void testParamsThreeSpaces() throws DescriptorParseException {
+    RelayNetworkStatusConsensus consensus =
+        ConsensusBuilder.createWithParamsLine("params   ");
+    assertNotNull(consensus.getConsensusParams());
+    assertTrue(consensus.getConsensusParams().isEmpty());
   }
 
-  /* TODO The error message here looked strange.  Investigate. */
-  @Test(expected = RuntimeException.class)
-  public void testParamsNoEqualSign() {
+  @Test(expected = DescriptorParseException.class)
+  public void testParamsNoEqualSign() throws DescriptorParseException {
     ConsensusBuilder.createWithParamsLine("params key-value");
   }
 
-  /* TODO We should check this. */
+  @Test(expected = DescriptorParseException.class)
+  public void testParamsOneTooLargeNegative()
+      throws DescriptorParseException {
+    ConsensusBuilder.createWithParamsLine("params min=-2147483649");
+  }
+
   @Test()
-  public void testDirSourceIdentityTooShort() {
-    DirSourceBuilder.createWithIdentity("ED03BB616EB2F60BEC8015111");
+  public void testParamsLargestNegative()
+      throws DescriptorParseException {
+    RelayNetworkStatusConsensus consensus =
+        ConsensusBuilder.createWithParamsLine("params min=-2147483648");
+    assertEquals(1, consensus.getConsensusParams().size());
+    assertEquals(-2147483648,
+        (int) consensus.getConsensusParams().get("min"));
+  }
+
+  @Test()
+  public void testParamsLargestPositive()
+      throws DescriptorParseException {
+    RelayNetworkStatusConsensus consensus =
+        ConsensusBuilder.createWithParamsLine("params max=2147483647");
+    assertEquals(1, consensus.getConsensusParams().size());
+    assertEquals(2147483647,
+        (int) consensus.getConsensusParams().get("max"));
+  }
+
+  @Test(expected = DescriptorParseException.class)
+  public void testParamsOneTooLargePositive()
+      throws DescriptorParseException {
+    ConsensusBuilder.createWithParamsLine("params max=2147483648");
   }
 
-  /* TODO We should check this. */
   @Test()
-  public void testDirSourceIdentityTooLong() {
+  public void testDirSourceLegacyNickname()
+      throws DescriptorParseException {
+    DirSourceBuilder dsb = new DirSourceBuilder();
+    dsb.nickname = "gabelmoo-legacy";
+    dsb.identity = "81349FC1F2DBA2C2C11B45CB9706637D480AB913";
+    dsb.contactLine = null;
+    dsb.voteDigestLine = null;
+    RelayNetworkStatusConsensus consensus =
+        DirSourceBuilder.createWithDirSource(dsb.buildDirSource());
+    assertEquals(3, consensus.getDirSourceEntries().size());
+    assertTrue(consensus.getDirSourceEntries().get(
+        "81349FC1F2DBA2C2C11B45CB9706637D480AB913").isLegacy());
+  }
+
+  @Test(expected = DescriptorParseException.class)
+  public void testDirSourceIdentityTooShort()
+      throws DescriptorParseException {
+    DirSourceBuilder.createWithIdentity("ED03BB616EB2F60BEC8015111");
+  }
+
+  @Test(expected = DescriptorParseException.class)
+  public void testDirSourceIdentityTooLong()
+      throws DescriptorParseException {
     DirSourceBuilder.createWithIdentity("ED03BB616EB2F60BEC8015111"
         + "4BB25CEF515B226ED03BB616EB2F60BEC8");
   }
 
-  /* TODO We should check this. */
-  @Test()
-  public void testDirSourceAddress24() {
+  @Test(expected = DescriptorParseException.class)
+  public void testDirSourceAddress24() throws DescriptorParseException {
     DirSourceBuilder.createWithAddress("212.112.245");
   }
 
-  /* TODO We should check this. */
-  @Test()
-  public void testDirSourceAddress40() {
+  @Test(expected = DescriptorParseException.class)
+  public void testDirSourceAddress40() throws DescriptorParseException {
     DirSourceBuilder.createWithAddress("212.112.245.170.123");
   }
 
-  /* TODO We should check this. */
-  @Test()
-  public void testDirSourceDirPortMinusOne() {
+  @Test(expected = DescriptorParseException.class)
+  public void testDirSourceDirPortMinusOne()
+      throws DescriptorParseException {
     DirSourceBuilder.createWithDirPort("-1");
   }
 
-  /* TODO We should check this. */
-  @Test()
-  public void testDirSourceDirPort66666() {
+  @Test(expected = DescriptorParseException.class)
+  public void testDirSourceDirPort66666()
+      throws DescriptorParseException {
     DirSourceBuilder.createWithDirPort("66666");
   }
 
-  /* TODO We should check this. */
-  @Test(expected = RuntimeException.class)
-  public void testDirSourceDirPortOnions() {
+  @Test(expected = DescriptorParseException.class)
+  public void testDirSourceDirPortOnions()
+      throws DescriptorParseException {
     DirSourceBuilder.createWithDirPort("onions");
   }
 
-  /* TODO We should check this. */
-  @Test(expected = RuntimeException.class)
-  public void testDirSourceOrPortOnions() {
+  @Test(expected = DescriptorParseException.class)
+  public void testDirSourceOrPortOnions()
+      throws DescriptorParseException {
     DirSourceBuilder.createWithOrPort("onions");
   }
 
-  /* TODO We should check this. */
   @Test()
-  public void testDirSourceContactNoLine() {
-    DirSourceBuilder.createWithContactLine(null);
+  public void testDirSourceContactNoLine()
+      throws DescriptorParseException {
+    RelayNetworkStatusConsensus consensus =
+        DirSourceBuilder.createWithContactLine(null);
+    assertNull(consensus.getDirSourceEntries().get(
+        "ED03BB616EB2F60BEC80151114BB25CEF515B226").getContactLine());
   }
 
-  /* TODO We should check this. */
-  @Test(expected = RuntimeException.class)
-  public void testDirSourceContactLineNoSpace() {
-    DirSourceBuilder.createWithContactLine("contact");
+  @Test()
+  public void testDirSourceContactLineNoSpace()
+      throws DescriptorParseException {
+    RelayNetworkStatusConsensus consensus =
+        DirSourceBuilder.createWithContactLine("contact");
+    assertNotNull(consensus.getDirSourceEntries().get(
+        "ED03BB616EB2F60BEC80151114BB25CEF515B226").getContactLine());
   }
 
-  /* TODO We should check this. */
   @Test()
-  public void testDirSourceContactLineOneSpace() {
-    DirSourceBuilder.createWithContactLine("contact ");
+  public void testDirSourceContactLineOneSpace()
+      throws DescriptorParseException {
+    RelayNetworkStatusConsensus consensus =
+        DirSourceBuilder.createWithContactLine("contact ");
+    assertNotNull(consensus.getDirSourceEntries().get(
+        "ED03BB616EB2F60BEC80151114BB25CEF515B226").getContactLine());
   }
 
-  /* TODO We should check this. */
-  @Test()
-  public void testDirSourceVoteDigestNoLine() {
+  @Test(expected = DescriptorParseException.class)
+  public void testDirSourceVoteDigestNoLine()
+      throws DescriptorParseException {
     DirSourceBuilder.createWithVoteDigestLine(null);
   }
 
-  /* TODO We should check this. */
-  @Test(expected = RuntimeException.class)
-  public void testDirSourceVoteDigestLineNoSpace() {
+  @Test(expected = DescriptorParseException.class)
+  public void testDirSourceVoteDigestLineNoSpace()
+      throws DescriptorParseException {
     DirSourceBuilder.createWithVoteDigestLine("vote-digest");
   }
 
-  /* TODO We should check this. */
-  @Test(expected = RuntimeException.class)
-  public void testDirSourceVoteDigestLineOneSpace() {
+  @Test(expected = DescriptorParseException.class)
+  public void testDirSourceVoteDigestLineOneSpace()
+      throws DescriptorParseException {
     DirSourceBuilder.createWithVoteDigestLine("vote-digest ");
   }
 
-  /* TODO We should check this. */
-  @Test()
-  public void testNicknameNotAllowedChars() {
+  @Test(expected = DescriptorParseException.class)
+  public void testNicknameNotAllowedChars()
+      throws DescriptorParseException {
     StatusEntryBuilder.createWithNickname("notAll()wed");
   }
 
-  /* TODO We should check this. */
-  @Test()
-  public void testNicknameTooLong() {
+  @Test(expected = DescriptorParseException.class)
+  public void testNicknameTooLong() throws DescriptorParseException {
     StatusEntryBuilder.createWithNickname("1234567890123456789tooLong");
   }
 
-  /* TODO We should check this. */
-  @Test()
-  public void testFingerprintTooShort() {
+  @Test(expected = DescriptorParseException.class)
+  public void testFingerprintTooShort() throws DescriptorParseException {
     StatusEntryBuilder.createWithFingerprintBase64("TooShort");
   }
 
-  /* TODO We should check this. */
-  @Test()
-  public void testFingerprintEndsWithEqualSign() {
+  @Test(expected = DescriptorParseException.class)
+  public void testFingerprintEndsWithEqualSign()
+      throws DescriptorParseException {
     StatusEntryBuilder.createWithFingerprintBase64(
         "ADQ6gCT3DiFHKPDFr3rODBUI8H=");
   }
 
-  /* TODO We should check this. */
-  @Test()
-  public void testFingerprintTooLong() {
+  @Test(expected = DescriptorParseException.class)
+  public void testFingerprintTooLong() throws DescriptorParseException {
     StatusEntryBuilder.createWithFingerprintBase64(
         "ADQ6gCT3DiFHKPDFr3rODBUI8HMAAAA");
   }
 
-  /* TODO We should check this. */
-  @Test()
-  public void testDescriptorTooShort() {
+  @Test(expected = DescriptorParseException.class)
+  public void testDescriptorTooShort() throws DescriptorParseException {
     StatusEntryBuilder.createWithDescriptorBase64("TooShort");
   }
 
-  /* TODO We should check this. */
-  @Test()
-  public void testDescriptorEndsWithEqualSign() {
+  @Test(expected = DescriptorParseException.class)
+  public void testDescriptorEndsWithEqualSign()
+      throws DescriptorParseException {
     StatusEntryBuilder.createWithDescriptorBase64(
         "ADQ6gCT3DiFHKPDFr3rODBUI8H=");
   }
 
-  /* TODO We should check this. */
-  @Test()
-  public void testDescriptorTooLong() {
+  @Test(expected = DescriptorParseException.class)
+  public void testDescriptorTooLong() throws DescriptorParseException {
     StatusEntryBuilder.createWithDescriptorBase64(
         "Yiti+nayuT2Efe2X1+M4nslwVuUAAAA");
   }
 
-  /* TODO We should check this. */
-  @Test()
-  public void testPublished1960() {
+  @Test(expected = DescriptorParseException.class)
+  public void testPublished1960() throws DescriptorParseException {
     StatusEntryBuilder.createWithPublishedString("1960-11-29 21:34:27");
   }
 
-  /* TODO We should check this. */
-  @Test()
-  public void testPublished9999() {
+  @Test(expected = DescriptorParseException.class)
+  public void testPublished9999() throws DescriptorParseException {
     StatusEntryBuilder.createWithPublishedString("9999-11-29 21:34:27");
   }
 
-  /* TODO We should check this. */
-  @Test()
-  public void testAddress256() {
+  @Test(expected = DescriptorParseException.class)
+  public void testAddress256() throws DescriptorParseException {
     StatusEntryBuilder.createWithAddress("256.63.8.215");
   }
 
-  /* TODO We should check this. */
-  @Test()
-  public void testAddress24() {
+  @Test(expected = DescriptorParseException.class)
+  public void testAddress24() throws DescriptorParseException {
     StatusEntryBuilder.createWithAddress("50.63.8/24");
   }
 
-  /* TODO We should check this. */
-  @Test()
-  public void testAddressV6() {
+  @Test(expected = DescriptorParseException.class)
+  public void testAddressV6() throws DescriptorParseException {
     StatusEntryBuilder.createWithAddress("::1");
   }
 
-  /* TODO We should check this. */
-  @Test()
-  public void testOrPort66666() {
+  @Test(expected = DescriptorParseException.class)
+  public void testOrPort66666() throws DescriptorParseException {
     StatusEntryBuilder.createWithOrPort("66666");
   }
 
-  /* TODO We should check this. */
-  @Test()
-  public void testOrPortEighty() {
+  @Test(expected = DescriptorParseException.class)
+  public void testOrPortEighty() throws DescriptorParseException {
     StatusEntryBuilder.createWithOrPort("eighty");
   }
 
-  /* TODO We should check this. */
-  @Test()
-  public void testDirPortMinusOne() {
+  @Test(expected = DescriptorParseException.class)
+  public void testDirPortMinusOne() throws DescriptorParseException {
     StatusEntryBuilder.createWithDirPort("-1");
   }
 
-  /* TODO We should check this. */
-  @Test()
-  public void testDirPortZero() {
+  @Test(expected = DescriptorParseException.class)
+  public void testDirPortZero() throws DescriptorParseException {
     StatusEntryBuilder.createWithDirPort("zero");
   }
 
-  /* TODO We should check this. */
   @Test()
-  public void testSLineNoSpace() {
-    StatusEntryBuilder.createWithSLine("s");
+  public void testSLineNoSpace() throws DescriptorParseException {
+    RelayNetworkStatusConsensus consensus =
+        StatusEntryBuilder.createWithSLine("s");
+    assertTrue(consensus.getStatusEntry(
+        "00343A8024F70E214728F0C5AF7ACE0C1508F073").getFlags().isEmpty());
   }
 
-  /* TODO We should check this. */
   @Test()
-  public void testSLineOneSpace() {
-    StatusEntryBuilder.createWithSLine("s ");
+  public void testSLineOneSpace() throws DescriptorParseException {
+    RelayNetworkStatusConsensus consensus =
+        StatusEntryBuilder.createWithSLine("s ");
+    assertTrue(consensus.getStatusEntry(
+        "00343A8024F70E214728F0C5AF7ACE0C1508F073").getFlags().isEmpty());
   }
 
-  /* TODO We should detect this. */
-  @Test()
-  public void testTwoSLines() {
+  @Test(expected = DescriptorParseException.class)
+  public void testTwoSLines() throws DescriptorParseException {
     StatusEntryBuilder sb = new StatusEntryBuilder();
     sb.sLine = sb.sLine + "\n" + sb.sLine;
     ConsensusBuilder cb = new ConsensusBuilder();
@@ -932,27 +1115,23 @@ public class RelayNetworkStatusConsensusImplTest {
     new RelayNetworkStatusConsensusImpl(cb.buildConsensus());
   }
 
-  /* TODO This is not allowed, right? */
-  @Test(expected = RuntimeException.class)
-  public void testWLineNoSpace() {
-    StatusEntryBuilder.createWithSLine("w");
+  @Test(expected = DescriptorParseException.class)
+  public void testWLineNoSpace() throws DescriptorParseException {
+    StatusEntryBuilder.createWithWLine("w");
   }
 
-  /* TODO We should check this. */
-  @Test()
-  public void testWLineOneSpace() {
-    StatusEntryBuilder.createWithSLine("w ");
+  @Test(expected = DescriptorParseException.class)
+  public void testWLineOneSpace() throws DescriptorParseException {
+    StatusEntryBuilder.createWithWLine("w ");
   }
 
-  /* TODO We should check this. */
-  @Test()
-  public void testWLineWarpSeven() {
+  @Test(expected = DescriptorParseException.class)
+  public void testWLineWarpSeven() throws DescriptorParseException {
     StatusEntryBuilder.createWithWLine("w Warp=7");
   }
 
-  /* TODO We should detect this. */
-  @Test()
-  public void testTwoWLines() {
+  @Test(expected = DescriptorParseException.class)
+  public void testTwoWLines() throws DescriptorParseException {
     StatusEntryBuilder sb = new StatusEntryBuilder();
     sb.wLine = sb.wLine + "\n" + sb.wLine;
     ConsensusBuilder cb = new ConsensusBuilder();
@@ -960,33 +1139,28 @@ public class RelayNetworkStatusConsensusImplTest {
     new RelayNetworkStatusConsensusImpl(cb.buildConsensus());
   }
 
-  /* TODO We should check this. */
-  @Test()
-  public void testPLineNoPolicy() {
+  @Test(expected = DescriptorParseException.class)
+  public void testPLineNoPolicy() throws DescriptorParseException {
     StatusEntryBuilder.createWithPLine("p 80");
   }
 
-  /* TODO We should check this. */
-  @Test()
-  public void testPLineNoPorts() {
+  @Test(expected = DescriptorParseException.class)
+  public void testPLineNoPorts() throws DescriptorParseException {
     StatusEntryBuilder.createWithPLine("p accept");
   }
 
-  /* TODO We should check this. */
-  @Test()
-  public void testPLineNoPolicyNoPorts() {
+  @Test(expected = DescriptorParseException.class)
+  public void testPLineNoPolicyNoPorts() throws DescriptorParseException {
     StatusEntryBuilder.createWithPLine("p ");
   }
 
-  /* TODO We should check this. */
-  @Test()
-  public void testPLineProject() {
+  @Test(expected = DescriptorParseException.class)
+  public void testPLineProject() throws DescriptorParseException {
     StatusEntryBuilder.createWithPLine("p project 80");
   }
 
-  /* TODO We should detect this. */
-  @Test()
-  public void testTwoPLines() {
+  @Test(expected = DescriptorParseException.class)
+  public void testTwoPLines() throws DescriptorParseException {
     StatusEntryBuilder sb = new StatusEntryBuilder();
     sb.pLine = sb.pLine + "\n" + sb.pLine;
     ConsensusBuilder cb = new ConsensusBuilder();
@@ -994,86 +1168,96 @@ public class RelayNetworkStatusConsensusImplTest {
     new RelayNetworkStatusConsensusImpl(cb.buildConsensus());
   }
 
-  /* TODO Should we allow this? */
-  @Test(expected = RuntimeException.class)
-  public void testNoStatusEntries() {
+  @Test()
+  public void testNoStatusEntries() throws DescriptorParseException {
     ConsensusBuilder cb = new ConsensusBuilder();
     cb.statusEntries.clear();
-    new RelayNetworkStatusConsensusImpl(cb.buildConsensus());
+    RelayNetworkStatusConsensus consensus =
+        new RelayNetworkStatusConsensusImpl(cb.buildConsensus());
+    assertFalse(consensus.containsStatusEntry(
+        "00795A6E8D91C270FC23B30F388A495553E01894"));
   }
 
-  /* TODO Why does this not break?  Ah, maybe it just leaves out one
-   * status entry.  Ugh.  It should break! */
-  @Test()
-  public void testDirectoryFooterNoLine() {
+  @Test(expected = DescriptorParseException.class)
+  public void testDirectoryFooterNoLine()
+      throws DescriptorParseException {
     ConsensusBuilder.createWithDirectoryFooterLine(null);
   }
 
-  @Test(expected = RuntimeException.class)
-  public void testDirectoryFooterLineSpace() {
+  @Test()
+  public void testDirectoryFooterLineSpace()
+      throws DescriptorParseException {
     ConsensusBuilder.createWithDirectoryFooterLine("directory-footer ");
   }
 
- /* TODO Make sure that this is really okay in the code. */
   @Test()
-  public void testBandwidthWeightsNoLine() {
-    ConsensusBuilder.createWithBandwidthWeightsLine(null);
+  public void testBandwidthWeightsNoLine()
+      throws DescriptorParseException {
+    RelayNetworkStatusConsensus consensus =
+        ConsensusBuilder.createWithBandwidthWeightsLine(null);
+    assertNull(consensus.getBandwidthWeights());
   }
 
-  @Test(expected = RuntimeException.class)
-  public void testBandwidthWeightsLineNoSpace() {
-    ConsensusBuilder.createWithBandwidthWeightsLine("bandwidth-weights");
+  @Test()
+  public void testBandwidthWeightsLineNoSpace()
+      throws DescriptorParseException {
+    RelayNetworkStatusConsensus consensus = ConsensusBuilder.
+        createWithBandwidthWeightsLine("bandwidth-weights");
+    assertNotNull(consensus.getBandwidthWeights());
   }
 
-  /* TODO We should check this. */
   @Test()
-  public void testBandwidthWeightsLineOneSpace() {
-    ConsensusBuilder.createWithBandwidthWeightsLine("bandwidth-weights ");
+  public void testBandwidthWeightsLineOneSpace()
+      throws DescriptorParseException {
+    RelayNetworkStatusConsensus consensus = ConsensusBuilder.
+        createWithBandwidthWeightsLine("bandwidth-weights ");
+    assertNotNull(consensus.getBandwidthWeights());
   }
 
-  /* TODO We should check this. */
-  @Test()
-  public void testBandwidthWeightsLineNoEqualSign() {
+  @Test(expected = DescriptorParseException.class)
+  public void testBandwidthWeightsLineNoEqualSign()
+      throws DescriptorParseException {
     ConsensusBuilder.createWithBandwidthWeightsLine(
         "bandwidth-weights Wbd-285");
   }
 
-  /* TODO We should check this. */
-  @Test()
-  public void testDirectorySignatureIdentityTooShort() {
+  @Test(expected = DescriptorParseException.class)
+  public void testDirectorySignatureIdentityTooShort()
+      throws DescriptorParseException {
     DirectorySignatureBuilder.createWithIdentity("ED03BB616EB2F60");
   }
 
-  /* TODO We should check this. */
-  @Test()
-  public void testDirectorySignatureIdentityTooLong() {
+  @Test(expected = DescriptorParseException.class)
+  public void testDirectorySignatureIdentityTooLong()
+      throws DescriptorParseException {
     DirectorySignatureBuilder.createWithIdentity(
         "ED03BB616EB2F60BEC80151114BB25CEF515B226ED03BB616EB2F60BEC");
   }
 
-  /* TODO We should check this. */
-  @Test()
-  public void testDirectorySignatureSigningKeyTooShort() {
+  @Test(expected = DescriptorParseException.class)
+  public void testDirectorySignatureSigningKeyTooShort()
+      throws DescriptorParseException {
     DirectorySignatureBuilder.createWithSigningKey("845CF1D0B370CA4");
   }
 
-  /* TODO We should check this. */
-  @Test()
-  public void testDirectorySignatureSigningKeyTooLong() {
+  @Test(expected = DescriptorParseException.class)
+  public void testDirectorySignatureSigningKeyTooLong()
+      throws DescriptorParseException {
     DirectorySignatureBuilder.createWithSigningKey(
         "845CF1D0B370CA443A8579D18E7987E7E532F639845CF1D0B370CA443A");
   }
 
-  @Test(expected = RuntimeException.class)
-  public void testNonAsciiByte20() {
+  @Test(expected = DescriptorParseException.class)
+  public void testNonAsciiByte20() throws DescriptorParseException {
     ConsensusBuilder cb = new ConsensusBuilder();
     byte[] consensusBytes = cb.buildConsensus();
     consensusBytes[20] = (byte) 200;
     new RelayNetworkStatusConsensusImpl(consensusBytes);
   }
 
-  @Test(expected = RuntimeException.class)
-  public void testNonAsciiByteMinusOne() {
+  @Test(expected = DescriptorParseException.class)
+  public void testNonAsciiByteMinusOne()
+      throws DescriptorParseException {
     ConsensusBuilder cb = new ConsensusBuilder();
     cb.networkStatusVersionLine = "Xnetwork-status-version 3";
     byte[] consensusBytes = cb.buildConsensus();



More information about the tor-commits mailing list