[tor-commits] [metrics-lib/master] Use a single relay and bridge descriptor implementation.

karsten at torproject.org karsten at torproject.org
Thu Jan 12 09:23:03 UTC 2012


commit ef6de2f9ad26fb064844d3c6edccf6ce7a829faf
Author: Karsten Loesing <karsten.loesing at gmx.net>
Date:   Thu Jan 12 09:59:32 2012 +0100

    Use a single relay and bridge descriptor implementation.
    
    Relay and bridge server and extra-info descriptors are similar enough to
    implement them in a single class each.  It should be clear from the
    descriptor source (relay or bridge reader) whether the descriptor was
    published by a relay or a bridge.
---
 .../descriptor/BridgeExtraInfoDescriptor.java      |    7 -
 .../descriptor/BridgeServerDescriptor.java         |    7 -
 .../torproject/descriptor/ExtraInfoDescriptor.java |  211 ++++
 .../descriptor/RelayExtraInfoDescriptor.java       |  211 ----
 .../descriptor/RelayServerDescriptor.java          |  110 --
 .../torproject/descriptor/ServerDescriptor.java    |  110 ++
 .../torproject/descriptor/impl/DescriptorImpl.java |    4 +-
 .../descriptor/impl/ExtraInfoDescriptorImpl.java   |  574 +++++++++++
 .../impl/RelayExtraInfoDescriptorImpl.java         |  574 -----------
 .../descriptor/impl/RelayServerDescriptorImpl.java |  532 ----------
 .../descriptor/impl/ServerDescriptorImpl.java      |  532 ++++++++++
 .../impl/RelayServerDescriptorImplTest.java        | 1080 --------------------
 .../descriptor/impl/ServerDescriptorImplTest.java  | 1080 ++++++++++++++++++++
 13 files changed, 2509 insertions(+), 2523 deletions(-)

diff --git a/src/org/torproject/descriptor/BridgeExtraInfoDescriptor.java b/src/org/torproject/descriptor/BridgeExtraInfoDescriptor.java
deleted file mode 100644
index ed0238d..0000000
--- a/src/org/torproject/descriptor/BridgeExtraInfoDescriptor.java
+++ /dev/null
@@ -1,7 +0,0 @@
-/* Copyright 2011, 2012 The Tor Project
- * See LICENSE for licensing information */
-package org.torproject.descriptor;
-
-public interface BridgeExtraInfoDescriptor extends Descriptor {
-}
-
diff --git a/src/org/torproject/descriptor/BridgeServerDescriptor.java b/src/org/torproject/descriptor/BridgeServerDescriptor.java
deleted file mode 100644
index ec759b1..0000000
--- a/src/org/torproject/descriptor/BridgeServerDescriptor.java
+++ /dev/null
@@ -1,7 +0,0 @@
-/* Copyright 2011, 2012 The Tor Project
- * See LICENSE for licensing information */
-package org.torproject.descriptor;
-
-public interface BridgeServerDescriptor extends Descriptor {
-}
-
diff --git a/src/org/torproject/descriptor/ExtraInfoDescriptor.java b/src/org/torproject/descriptor/ExtraInfoDescriptor.java
new file mode 100644
index 0000000..bd4bcd8
--- /dev/null
+++ b/src/org/torproject/descriptor/ExtraInfoDescriptor.java
@@ -0,0 +1,211 @@
+/* Copyright 2012 The Tor Project
+ * See LICENSE for licensing information */
+package org.torproject.descriptor;
+
+import java.util.List;
+import java.util.SortedMap;
+
+public interface ExtraInfoDescriptor extends Descriptor {
+
+  /* Return the relay's nickname. */
+  public String getNickname();
+
+  /* Return the relay's fingerprint. */
+  public String getFingerprint();
+
+  /* Return the publication time of this descriptor. */
+  public long getPublishedMillis();
+
+  /* Return the read history contained in this descriptor, or null if no
+   * read history is contained. */
+  public BandwidthHistory getReadHistory();
+
+  /* Return the write history contained in this descriptor, or null if no
+   * read history is contained. */
+  public BandwidthHistory getWriteHistory();
+
+  /* Return the SHA1 digest of the GeoIP database used by this relay, or
+   * null if no GeoIP database digest is included. */
+  public String getGeoipDbDigest();
+
+  /* Return the end of the included directory request statistics interval,
+   * or -1 if no directory request statistics are included. */
+  public long getDirreqStatsEndMillis();
+
+  /* Return the interval length of the included directory request
+   * statistics, or -1 if no directory request statistics are included. */
+  public long getDirreqStatsIntervalLength();
+
+  /* Return statistics on unique IP addresses requesting v2 network
+   * statuses with map keys being country codes and map values being
+   * numbers of unique IP addresses rounded up to the nearest multiple of
+   * 8, or null if no such statistics are included. */
+  public SortedMap<String, Integer> getDirreqV2Ips();
+
+  /* Return statistics on unique IP addresses requesting v3 network status
+   * consensuses with map keys being country codes and map values being
+   * numbers of unique IP addresses rounded up to the nearest multiple of
+   * 8, or null if no such statistics are included. */
+  public SortedMap<String, Integer> getDirreqV3Ips();
+
+  /* Return statistics on directory requests for v2 network statuses with
+   * map keys being country codes and map values being request numbers
+   * rounded up to the nearest multiple of 8, or null if no such
+   * statistics are included. */
+  public SortedMap<String, Integer> getDirreqV2Reqs();
+
+  /* Return statistics on directory requests for v3 network status
+   * consensuses with map keys being country codes and map values being
+   * request numbers rounded up to the nearest multiple of 8, or null if
+   * no such statistics are included. */
+  public SortedMap<String, Integer> getDirreqV3Reqs();
+
+  /* Return the share of requests for v2 network statuses that the
+   * directory expects to receive from clients, or -1.0 if no such
+   * statistics are included. */
+  public double getDirreqV2Share();
+
+  /* Return the share of requests for v3 network status consensuses that
+   * the directory expects to receive from clients, or -1.0 if no such
+   * statistics are included. */
+  public double getDirreqV3Share();
+
+  /* Return statistics on directory request responses for v2 network
+   * statuses with map keys being response strings and map values being
+   * response numbers rounded up to the nearest multiple of 4, or null if
+   * no such statistics are included. */
+  public SortedMap<String, Integer> getDirreqV2Resp();
+
+  /* Return statistics on directory request responses for v3 network
+   * status consensuses with map keys being response strings and map
+   * values being response numbers rounded up to the nearest multiple of
+   * 4, or null if no such statistics are included. */
+  public SortedMap<String, Integer> getDirreqV3Resp();
+
+  /* Return statistics on direct directory requests asking for v2 network
+   * statuses with map keys being statistic keys and map values being
+   * statistic values, or null if no such statistics are included. */
+  public SortedMap<String, Integer> getDirreqV2DirectDl();
+
+  /* Return statistics on direct directory requests asking for v3 network
+   * status consensuses with map keys being statistic keys and map
+   * values being statistic values, or null if no such statistics are
+   * included. */
+  public SortedMap<String, Integer> getDirreqV3DirectDl();
+
+  /* Return statistics on tunneled directory requests asking for v2
+   * network statuses with map keys being statistic keys and map values
+   * being statistic values, or null if no such statistics are
+   * included. */
+  public SortedMap<String, Integer> getDirreqV2TunneledDl();
+
+  /* Return statistics on tunneled directory requests asking for v3
+   * network status consensuses with map keys being statistic keys and map
+   * values being statistic values, or null if no such statistics are
+   * included. */
+  public SortedMap<String, Integer> getDirreqV3TunneledDl();
+
+  /* Return the directory request read history contained in this
+   * descriptor, or null if no directory request read history is
+   * contained. */
+  public BandwidthHistory getDirreqReadHistory();
+
+  /* Return the directory request write history contained in this
+   * descriptor, or null if no directory request write history is
+   * contained. */
+  public BandwidthHistory getDirreqWriteHistory();
+
+  /* Return the end of the included entry statistics interval, or -1 if no
+   * entry statistics are included. */
+  public long getEntryStatsEndMillis();
+
+  /* Return the interval length of the included entry statistics, or -1 if
+   * no entry statistics are included. */
+  public long getEntryStatsIntervalLength();
+
+  /* Return statistics on client IP addresses with map keys being country
+   * codes and map values being the number of unique IP addresses that
+   * have connected from that country rounded up to the nearest multiple
+   * of 8, or null if no entry statistics are included. */
+  public SortedMap<String, Integer> getEntryIps();
+
+  /* Return the end of the included cell statistics interval, or -1 if no
+   * cell statistics are included. */
+  public long getCellStatsEndMillis();
+
+  /* Return the interval length of the included cell statistics, or -1 if
+   * no cell statistics are included. */
+  public long getCellStatsIntervalLength();
+
+  /* Return the mean number of processed cells per circuit by circuit
+   * deciles. */
+  public List<Integer> getCellProcessedCells();
+
+  /* Return the mean number of cells contained in circuit queues by
+   * circuit deciles. */
+  public List<Integer> getCellQueueCells();
+
+  /* Return the mean times in milliseconds that cells spend in circuit
+   * queues by circuit deciles. */
+  public List<Integer> getCellTimeInQueue();
+
+  /* Return the mean number of circuits included in any of the cell
+   * statistics deciles, or -1 if no cell statistics are included. */
+  public int getCellCircuitsPerDecile();
+
+  /* Return the end of the included statistics interval on bi-directional
+   * connection usage, or -1 if no such statistics are included. */
+  public long getConnBiDirectStatsEndMillis();
+
+  /* Return the interval length of the included statistics on
+   * bi-directional connection usage, or -1 if no such statistics are
+   * included. */
+  public long getConnBiDirectIntervalLength();
+
+  /* Return the number of connections on which this relay read and wrote
+   * less than 2 KiB/s in a 10-second interval, or -1 if no statistics on
+   * bi-directional connection usage are included. */
+  public int getConnBiDirectBelow();
+
+  /* Return the number of connections on which this relay read and wrote
+   * at least 2 KiB/s in a 10-second interval and at least 10 times more
+   * in read direction than in write direction, or -1 if no statistics on
+   * bi-directional connection usage are included. */
+  public int getConnBiDirectRead();
+
+  /* Return the number of connections on which this relay read and wrote
+   * at least 2 KiB/s in a 10-second interval and at least 10 times more
+   * in write direction than in read direction, or -1 if no statistics on
+   * bi-directional connection usage are included. */
+  public int getConnBiDirectWrite();
+
+  /* Return the number of connections on which this relay read and wrote
+   * at least 2 KiB/s in a 10-second interval but not 10 times more in
+   * either direction, or -1 if no statistics on bi-directional connection
+   * usage are included. */
+  public int getConnBiDirectBoth();
+
+  /* Return the end of the included exit statistics interval, or -1 if no
+   * exit statistics are included. */
+  public long getExitStatsEndMillis();
+
+  /* Return the interval length of the included exit statistics, or -1 if
+   * no exit statistics are included. */
+  public long getExitStatsIntervalLength();
+
+  /* Return statistics on KiB written by port with map keys being ports
+   * and map values being KiB rounded up to the next full KiB, or null if
+   * no exit statistics are included. */
+  public SortedMap<Integer, Integer> getExitKibibytesWritten();
+
+  /* Return statistics on KiB read by port with map keys being ports and
+   * map values being KiB rounded up to the next full KiB, or null if no
+   * exit statistics are included. */
+  public SortedMap<Integer, Integer> getExitKibibytesRead();
+
+  /* Return statistics on opened exit streams with map keys being ports
+   * and map values being the number of opened streams, rounded up to the
+   * nearest multiple of 4, or null if no exit statistics are included. */
+  public SortedMap<Integer, Integer> getExitStreamsOpened();
+}
+
diff --git a/src/org/torproject/descriptor/RelayExtraInfoDescriptor.java b/src/org/torproject/descriptor/RelayExtraInfoDescriptor.java
deleted file mode 100644
index 853a60a..0000000
--- a/src/org/torproject/descriptor/RelayExtraInfoDescriptor.java
+++ /dev/null
@@ -1,211 +0,0 @@
-/* Copyright 2012 The Tor Project
- * See LICENSE for licensing information */
-package org.torproject.descriptor;
-
-import java.util.List;
-import java.util.SortedMap;
-
-public interface RelayExtraInfoDescriptor extends Descriptor {
-
-  /* Return the relay's nickname. */
-  public String getNickname();
-
-  /* Return the relay's fingerprint. */
-  public String getFingerprint();
-
-  /* Return the publication time of this descriptor. */
-  public long getPublishedMillis();
-
-  /* Return the read history contained in this descriptor, or null if no
-   * read history is contained. */
-  public BandwidthHistory getReadHistory();
-
-  /* Return the write history contained in this descriptor, or null if no
-   * read history is contained. */
-  public BandwidthHistory getWriteHistory();
-
-  /* Return the SHA1 digest of the GeoIP database used by this relay, or
-   * null if no GeoIP database digest is included. */
-  public String getGeoipDbDigest();
-
-  /* Return the end of the included directory request statistics interval,
-   * or -1 if no directory request statistics are included. */
-  public long getDirreqStatsEndMillis();
-
-  /* Return the interval length of the included directory request
-   * statistics, or -1 if no directory request statistics are included. */
-  public long getDirreqStatsIntervalLength();
-
-  /* Return statistics on unique IP addresses requesting v2 network
-   * statuses with map keys being country codes and map values being
-   * numbers of unique IP addresses rounded up to the nearest multiple of
-   * 8, or null if no such statistics are included. */
-  public SortedMap<String, Integer> getDirreqV2Ips();
-
-  /* Return statistics on unique IP addresses requesting v3 network status
-   * consensuses with map keys being country codes and map values being
-   * numbers of unique IP addresses rounded up to the nearest multiple of
-   * 8, or null if no such statistics are included. */
-  public SortedMap<String, Integer> getDirreqV3Ips();
-
-  /* Return statistics on directory requests for v2 network statuses with
-   * map keys being country codes and map values being request numbers
-   * rounded up to the nearest multiple of 8, or null if no such
-   * statistics are included. */
-  public SortedMap<String, Integer> getDirreqV2Reqs();
-
-  /* Return statistics on directory requests for v3 network status
-   * consensuses with map keys being country codes and map values being
-   * request numbers rounded up to the nearest multiple of 8, or null if
-   * no such statistics are included. */
-  public SortedMap<String, Integer> getDirreqV3Reqs();
-
-  /* Return the share of requests for v2 network statuses that the
-   * directory expects to receive from clients, or -1.0 if no such
-   * statistics are included. */
-  public double getDirreqV2Share();
-
-  /* Return the share of requests for v3 network status consensuses that
-   * the directory expects to receive from clients, or -1.0 if no such
-   * statistics are included. */
-  public double getDirreqV3Share();
-
-  /* Return statistics on directory request responses for v2 network
-   * statuses with map keys being response strings and map values being
-   * response numbers rounded up to the nearest multiple of 4, or null if
-   * no such statistics are included. */
-  public SortedMap<String, Integer> getDirreqV2Resp();
-
-  /* Return statistics on directory request responses for v3 network
-   * status consensuses with map keys being response strings and map
-   * values being response numbers rounded up to the nearest multiple of
-   * 4, or null if no such statistics are included. */
-  public SortedMap<String, Integer> getDirreqV3Resp();
-
-  /* Return statistics on direct directory requests asking for v2 network
-   * statuses with map keys being statistic keys and map values being
-   * statistic values, or null if no such statistics are included. */
-  public SortedMap<String, Integer> getDirreqV2DirectDl();
-
-  /* Return statistics on direct directory requests asking for v3 network
-   * status consensuses with map keys being statistic keys and map
-   * values being statistic values, or null if no such statistics are
-   * included. */
-  public SortedMap<String, Integer> getDirreqV3DirectDl();
-
-  /* Return statistics on tunneled directory requests asking for v2
-   * network statuses with map keys being statistic keys and map values
-   * being statistic values, or null if no such statistics are
-   * included. */
-  public SortedMap<String, Integer> getDirreqV2TunneledDl();
-
-  /* Return statistics on tunneled directory requests asking for v3
-   * network status consensuses with map keys being statistic keys and map
-   * values being statistic values, or null if no such statistics are
-   * included. */
-  public SortedMap<String, Integer> getDirreqV3TunneledDl();
-
-  /* Return the directory request read history contained in this
-   * descriptor, or null if no directory request read history is
-   * contained. */
-  public BandwidthHistory getDirreqReadHistory();
-
-  /* Return the directory request write history contained in this
-   * descriptor, or null if no directory request write history is
-   * contained. */
-  public BandwidthHistory getDirreqWriteHistory();
-
-  /* Return the end of the included entry statistics interval, or -1 if no
-   * entry statistics are included. */
-  public long getEntryStatsEndMillis();
-
-  /* Return the interval length of the included entry statistics, or -1 if
-   * no entry statistics are included. */
-  public long getEntryStatsIntervalLength();
-
-  /* Return statistics on client IP addresses with map keys being country
-   * codes and map values being the number of unique IP addresses that
-   * have connected from that country rounded up to the nearest multiple
-   * of 8, or null if no entry statistics are included. */
-  public SortedMap<String, Integer> getEntryIps();
-
-  /* Return the end of the included cell statistics interval, or -1 if no
-   * cell statistics are included. */
-  public long getCellStatsEndMillis();
-
-  /* Return the interval length of the included cell statistics, or -1 if
-   * no cell statistics are included. */
-  public long getCellStatsIntervalLength();
-
-  /* Return the mean number of processed cells per circuit by circuit
-   * deciles. */
-  public List<Integer> getCellProcessedCells();
-
-  /* Return the mean number of cells contained in circuit queues by
-   * circuit deciles. */
-  public List<Integer> getCellQueueCells();
-
-  /* Return the mean times in milliseconds that cells spend in circuit
-   * queues by circuit deciles. */
-  public List<Integer> getCellTimeInQueue();
-
-  /* Return the mean number of circuits included in any of the cell
-   * statistics deciles, or -1 if no cell statistics are included. */
-  public int getCellCircuitsPerDecile();
-
-  /* Return the end of the included statistics interval on bi-directional
-   * connection usage, or -1 if no such statistics are included. */
-  public long getConnBiDirectStatsEndMillis();
-
-  /* Return the interval length of the included statistics on
-   * bi-directional connection usage, or -1 if no such statistics are
-   * included. */
-  public long getConnBiDirectIntervalLength();
-
-  /* Return the number of connections on which this relay read and wrote
-   * less than 2 KiB/s in a 10-second interval, or -1 if no statistics on
-   * bi-directional connection usage are included. */
-  public int getConnBiDirectBelow();
-
-  /* Return the number of connections on which this relay read and wrote
-   * at least 2 KiB/s in a 10-second interval and at least 10 times more
-   * in read direction than in write direction, or -1 if no statistics on
-   * bi-directional connection usage are included. */
-  public int getConnBiDirectRead();
-
-  /* Return the number of connections on which this relay read and wrote
-   * at least 2 KiB/s in a 10-second interval and at least 10 times more
-   * in write direction than in read direction, or -1 if no statistics on
-   * bi-directional connection usage are included. */
-  public int getConnBiDirectWrite();
-
-  /* Return the number of connections on which this relay read and wrote
-   * at least 2 KiB/s in a 10-second interval but not 10 times more in
-   * either direction, or -1 if no statistics on bi-directional connection
-   * usage are included. */
-  public int getConnBiDirectBoth();
-
-  /* Return the end of the included exit statistics interval, or -1 if no
-   * exit statistics are included. */
-  public long getExitStatsEndMillis();
-
-  /* Return the interval length of the included exit statistics, or -1 if
-   * no exit statistics are included. */
-  public long getExitStatsIntervalLength();
-
-  /* Return statistics on KiB written by port with map keys being ports
-   * and map values being KiB rounded up to the next full KiB, or null if
-   * no exit statistics are included. */
-  public SortedMap<Integer, Integer> getExitKibibytesWritten();
-
-  /* Return statistics on KiB read by port with map keys being ports and
-   * map values being KiB rounded up to the next full KiB, or null if no
-   * exit statistics are included. */
-  public SortedMap<Integer, Integer> getExitKibibytesRead();
-
-  /* Return statistics on opened exit streams with map keys being ports
-   * and map values being the number of opened streams, rounded up to the
-   * nearest multiple of 4, or null if no exit statistics are included. */
-  public SortedMap<Integer, Integer> getExitStreamsOpened();
-}
-
diff --git a/src/org/torproject/descriptor/RelayServerDescriptor.java b/src/org/torproject/descriptor/RelayServerDescriptor.java
deleted file mode 100644
index 2f48def..0000000
--- a/src/org/torproject/descriptor/RelayServerDescriptor.java
+++ /dev/null
@@ -1,110 +0,0 @@
-/* Copyright 2011, 2012 The Tor Project
- * See LICENSE for licensing information */
-package org.torproject.descriptor;
-
-import java.util.List;
-
-/* Contains a relay server descriptor. */
-public interface RelayServerDescriptor extends Descriptor {
-
-  /* Return the relay's nickname. */
-  public String getNickname();
-
-  /* Return the relay's IPv4 address in dotted-quad format. */
-  public String getAddress();
-
-  /* Return the relay's OR port. */
-  public int getOrPort();
-
-  /* Return the relay's SOCKS port which should always be 0. */
-  public int getSocksPort();
-
-  /* Return the relay's directory port. */
-  public int getDirPort();
-
-  /* Return the average bandwidth in bytes per second that the relay is
-   * willing to sustain over long periods. */
-  public int getBandwidthRate();
-
-  /* Return the burst bandwidth in bytes per second that the relay is
-   * willing to sustain in very short intervals. */
-  public int getBandwidthBurst();
-
-  /* Return the observed bandwidth in bytes per second as an estimate of
-   * the capacity that the relay can handle. */
-  public int getBandwidthObserved();
-
-  /* Return the platform string containing the Tor software version and
-   * the operating system. */
-  public String getPlatform();
-
-  /* Return the time when this descriptor and the corresponding extra-info
-   * document was generated. */
-  public long getPublishedMillis();
-
-  /* Return the relay fingerprint, or null if this descriptor does not
-   * contain a fingerprint line. */
-  public String getFingerprint();
-
-  /* Return whether the relay was hibernating when this descriptor was
-   * published. */
-  public boolean isHibernating();
-
-  /* Return the number of seconds that this relay has been running, or -1
-   * if the descriptor does not contain an uptime line. */
-  public int getUptime();
-
-  /* Return the relay's exit policy consisting of one or more accept or
-   * reject lines. */
-  public List<String> getExitPolicyLines();
-
-  /* Return the contact information for this relay, or null if no contact
-   * information is included in the descriptor. */
-  public String getContact();
-
-  /* Return the nicknames or ($-prefixed) fingerprints contained in the
-   * family line of this relay, or null if the descriptor does not contain
-   * a family line. */
-  public List<String> getFamilyEntries();
-
-  /* Return the relay's read history.  (Current Tor versions include their
-   * bandwidth histories in their extra-info descriptors, not in their
-   * server descriptors.) */
-  public BandwidthHistory getReadHistory();
-
-  /* Return the relay's write history.  (Current Tor versions include
-   * their bandwidth histories in their extra-info descriptors, not in
-   * their server descriptors.) */
-  public BandwidthHistory getWriteHistory();
-
-  /* Return true if the relay uses the enhanced DNS logic, or false if
-   * doesn't use it or doesn't include an eventdns line in its
-   * descriptor. */
-  public boolean getUsesEnhancedDnsLogic();
-
-  /* Return whether this relay is a directory cache that provides
-   * extra-info descriptors. */
-  public boolean getCachesExtraInfo();
-
-  /* Return the digest of the relay's extra-info descriptor, or null if
-   * the relay did not upload a corresponding extra-info descriptor. */
-  public String getExtraInfoDigest();
-
-  /* Return the hidden service descriptor version(s) that this relay
-   * stores and serves, or null if it doesn't store and serve any hidden
-   * service descriptors. */
-  public List<Integer> getHiddenServiceDirVersions();
-
-  /* Return the list of link protocol versions that this relay
-   * supports. */
-  public List<Integer> getLinkProtocolVersions();
-
-  /* Return the list of circuit protocol versions that this relay
-   * supports. */
-  public List<Integer> getCircuitProtocolVersions();
-
-  /* Return whether this relay allows single-hop circuits to make exit
-   * connections. */
-  public boolean getAllowSingleHopExits();
-}
-
diff --git a/src/org/torproject/descriptor/ServerDescriptor.java b/src/org/torproject/descriptor/ServerDescriptor.java
new file mode 100644
index 0000000..98a0f02
--- /dev/null
+++ b/src/org/torproject/descriptor/ServerDescriptor.java
@@ -0,0 +1,110 @@
+/* Copyright 2011, 2012 The Tor Project
+ * See LICENSE for licensing information */
+package org.torproject.descriptor;
+
+import java.util.List;
+
+/* Contains a relay server descriptor. */
+public interface ServerDescriptor extends Descriptor {
+
+  /* Return the relay's nickname. */
+  public String getNickname();
+
+  /* Return the relay's IPv4 address in dotted-quad format. */
+  public String getAddress();
+
+  /* Return the relay's OR port. */
+  public int getOrPort();
+
+  /* Return the relay's SOCKS port which should always be 0. */
+  public int getSocksPort();
+
+  /* Return the relay's directory port. */
+  public int getDirPort();
+
+  /* Return the average bandwidth in bytes per second that the relay is
+   * willing to sustain over long periods. */
+  public int getBandwidthRate();
+
+  /* Return the burst bandwidth in bytes per second that the relay is
+   * willing to sustain in very short intervals. */
+  public int getBandwidthBurst();
+
+  /* Return the observed bandwidth in bytes per second as an estimate of
+   * the capacity that the relay can handle. */
+  public int getBandwidthObserved();
+
+  /* Return the platform string containing the Tor software version and
+   * the operating system. */
+  public String getPlatform();
+
+  /* Return the time when this descriptor and the corresponding extra-info
+   * document was generated. */
+  public long getPublishedMillis();
+
+  /* Return the relay fingerprint, or null if this descriptor does not
+   * contain a fingerprint line. */
+  public String getFingerprint();
+
+  /* Return whether the relay was hibernating when this descriptor was
+   * published. */
+  public boolean isHibernating();
+
+  /* Return the number of seconds that this relay has been running, or -1
+   * if the descriptor does not contain an uptime line. */
+  public int getUptime();
+
+  /* Return the relay's exit policy consisting of one or more accept or
+   * reject lines. */
+  public List<String> getExitPolicyLines();
+
+  /* Return the contact information for this relay, or null if no contact
+   * information is included in the descriptor. */
+  public String getContact();
+
+  /* Return the nicknames or ($-prefixed) fingerprints contained in the
+   * family line of this relay, or null if the descriptor does not contain
+   * a family line. */
+  public List<String> getFamilyEntries();
+
+  /* Return the relay's read history.  (Current Tor versions include their
+   * bandwidth histories in their extra-info descriptors, not in their
+   * server descriptors.) */
+  public BandwidthHistory getReadHistory();
+
+  /* Return the relay's write history.  (Current Tor versions include
+   * their bandwidth histories in their extra-info descriptors, not in
+   * their server descriptors.) */
+  public BandwidthHistory getWriteHistory();
+
+  /* Return true if the relay uses the enhanced DNS logic, or false if
+   * doesn't use it or doesn't include an eventdns line in its
+   * descriptor. */
+  public boolean getUsesEnhancedDnsLogic();
+
+  /* Return whether this relay is a directory cache that provides
+   * extra-info descriptors. */
+  public boolean getCachesExtraInfo();
+
+  /* Return the digest of the relay's extra-info descriptor, or null if
+   * the relay did not upload a corresponding extra-info descriptor. */
+  public String getExtraInfoDigest();
+
+  /* Return the hidden service descriptor version(s) that this relay
+   * stores and serves, or null if it doesn't store and serve any hidden
+   * service descriptors. */
+  public List<Integer> getHiddenServiceDirVersions();
+
+  /* Return the list of link protocol versions that this relay
+   * supports. */
+  public List<Integer> getLinkProtocolVersions();
+
+  /* Return the list of circuit protocol versions that this relay
+   * supports. */
+  public List<Integer> getCircuitProtocolVersions();
+
+  /* Return whether this relay allows single-hop circuits to make exit
+   * connections. */
+  public boolean getAllowSingleHopExits();
+}
+
diff --git a/src/org/torproject/descriptor/impl/DescriptorImpl.java b/src/org/torproject/descriptor/impl/DescriptorImpl.java
index 24c1fe9..259a1de 100644
--- a/src/org/torproject/descriptor/impl/DescriptorImpl.java
+++ b/src/org/torproject/descriptor/impl/DescriptorImpl.java
@@ -37,11 +37,11 @@ public abstract class DescriptorImpl implements Descriptor {
       }
     } else if (firstLines.startsWith("router ") ||
         firstLines.contains("\nrouter ")) {
-      parsedDescriptors.addAll(RelayServerDescriptorImpl.
+      parsedDescriptors.addAll(ServerDescriptorImpl.
           parseDescriptors(rawDescriptorBytes));
     } else if (firstLines.startsWith("extra-info ") ||
         firstLines.contains("\nextra-info ")) {
-      parsedDescriptors.addAll(RelayExtraInfoDescriptorImpl.
+      parsedDescriptors.addAll(ExtraInfoDescriptorImpl.
           parseDescriptors(rawDescriptorBytes));
     } else {
       throw new DescriptorParseException("Could not detect relay "
diff --git a/src/org/torproject/descriptor/impl/ExtraInfoDescriptorImpl.java b/src/org/torproject/descriptor/impl/ExtraInfoDescriptorImpl.java
new file mode 100644
index 0000000..6dae9eb
--- /dev/null
+++ b/src/org/torproject/descriptor/impl/ExtraInfoDescriptorImpl.java
@@ -0,0 +1,574 @@
+/* Copyright 2012 The Tor Project
+ * See LICENSE for licensing information */
+package org.torproject.descriptor.impl;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.StringReader;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.SortedMap;
+import org.torproject.descriptor.ExtraInfoDescriptor;
+import org.torproject.descriptor.BandwidthHistory;
+
+/* TODO Implement methods to parse the various statistics (other than
+ * bandwidth histories. */
+/* TODO Write a test class. */
+public class ExtraInfoDescriptorImpl extends DescriptorImpl
+    implements ExtraInfoDescriptor {
+
+  protected static List<ExtraInfoDescriptor> parseDescriptors(
+      byte[] descriptorsBytes) {
+    List<ExtraInfoDescriptor> parsedDescriptors =
+        new ArrayList<ExtraInfoDescriptor>();
+    List<byte[]> splitDescriptorsBytes =
+        DescriptorImpl.splitRawDescriptorBytes(descriptorsBytes,
+        "extra-info ");
+    try {
+      for (byte[] descriptorBytes : splitDescriptorsBytes) {
+        ExtraInfoDescriptor parsedDescriptor =
+            new ExtraInfoDescriptorImpl(descriptorBytes);
+        parsedDescriptors.add(parsedDescriptor);
+      }
+    } catch (DescriptorParseException e) {
+      /* TODO Handle this error somehow. */
+      System.err.println("Failed to parse descriptor.  Skipping.");
+      e.printStackTrace();
+    }
+    return parsedDescriptors;
+  }
+
+  protected ExtraInfoDescriptorImpl(byte[] descriptorBytes)
+      throws DescriptorParseException {
+    super(descriptorBytes);
+    this.parseDescriptorBytes();
+    Set<String> exactlyOnceKeywords = new HashSet<String>(Arrays.asList((
+        "extra-info,published,router-signature").split(",")));
+    this.checkExactlyOnceKeywords(exactlyOnceKeywords);
+    Set<String> atMostOnceKeywords = new HashSet<String>(Arrays.asList((
+        "read-history,write-history,geoip-db-digest,dirreq-stats-end,"
+        + "dirreq-v2-ips,dirreq-v3-ips,dirreq-v2-reqs,dirreq-v3-reqs,"
+        + "dirreq-v2-share,dirreq-v3-share,dirreq-v2-resp,dirreq-v3-resp,"
+        + "dirreq-v2-direct-dl,dirreq-v3-direct-dl,dirreq-v2-tunneled-dl,"
+        + "dirreq-v3-tunneled-dl,dirreq-read-history,"
+        + "dirreq-write-history,entry-stats-end,entry-ips,cell-stats-end,"
+        + "cell-processed-cells,cell-queued-cells,cell-time-in-queue,"
+        + "cell-circuits-per-decile,conn-bi-direct,exit-stats-end,"
+        + "exit-kibibytes-written,exit-kibibytes-read,"
+        + "exit-streams-opened").split(",")));
+    this.checkAtMostOnceKeywords(atMostOnceKeywords);
+    /* TODO Add more checks to see that only statistics details lines are
+     * included with corresponding statistics interval lines. */
+    this.checkFirstKeyword("extra-info");
+    this.checkLastKeyword("router-signature");
+    return;
+  }
+
+  private void parseDescriptorBytes() throws DescriptorParseException {
+    try {
+      BufferedReader br = new BufferedReader(new StringReader(
+          new String(this.rawDescriptorBytes)));
+      String line;
+      boolean skipCrypto = false;
+      while ((line = br.readLine()) != null) {
+        String lineNoOpt = line.startsWith("opt ") ?
+            line.substring("opt ".length()) : line;
+        String[] partsNoOpt = lineNoOpt.split(" ");
+        String keyword = partsNoOpt[0];
+        if (keyword.equals("extra-info")) {
+          this.parseExtraInfoLine(line, lineNoOpt, partsNoOpt);
+        } else if (keyword.equals("published")) {
+          this.parsePublishedLine(line, lineNoOpt, partsNoOpt);
+        } else if (keyword.equals("read-history")) {
+          this.parseReadHistoryLine(line, lineNoOpt, partsNoOpt);
+        } else if (keyword.equals("write-history")) {
+          this.parseWriteHistoryLine(line, lineNoOpt, partsNoOpt);
+        } else if (keyword.equals("geoip-db-digest")) {
+          this.parseGeoipDbDigestLine(line, lineNoOpt, partsNoOpt);
+        } else if (keyword.equals("geoip-start-time")) {
+          this.parseGeoipStartTimeLine(line, lineNoOpt, partsNoOpt);
+        } else if (keyword.equals("geoip-client-origins")) {
+          this.parseGeoipClientOriginsLine(line, lineNoOpt, partsNoOpt);
+        } else if (keyword.equals("dirreq-stats-end")) {
+          this.parseDirreqStatsEndLine(line, lineNoOpt, partsNoOpt);
+        } else if (keyword.equals("dirreq-v2-ips")) {
+          this.parseDirreqV2IpsLine(line, lineNoOpt, partsNoOpt);
+        } else if (keyword.equals("dirreq-v3-ips")) {
+          this.parseDirreqV3IpsLine(line, lineNoOpt, partsNoOpt);
+        } else if (keyword.equals("dirreq-v2-reqs")) {
+          this.parseDirreqV2ReqsLine(line, lineNoOpt, partsNoOpt);
+        } else if (keyword.equals("dirreq-v3-reqs")) {
+          this.parseDirreqV3ReqsLine(line, lineNoOpt, partsNoOpt);
+        } else if (keyword.equals("dirreq-v2-share")) {
+          this.parseDirreqV2ShareLine(line, lineNoOpt, partsNoOpt);
+        } else if (keyword.equals("dirreq-v3-share")) {
+          this.parseDirreqV3ShareLine(line, lineNoOpt, partsNoOpt);
+        } else if (keyword.equals("dirreq-v2-resp")) {
+          this.parseDirreqV2RespLine(line, lineNoOpt, partsNoOpt);
+        } else if (keyword.equals("dirreq-v3-resp")) {
+          this.parseDirreqV3RespLine(line, lineNoOpt, partsNoOpt);
+        } else if (keyword.equals("dirreq-v2-direct-dl")) {
+          this.parseDirreqV2DirectDlLine(line, lineNoOpt, partsNoOpt);
+        } else if (keyword.equals("dirreq-v3-direct-dl")) {
+          this.parseDirreqV3DirectDlLine(line, lineNoOpt, partsNoOpt);
+        } else if (keyword.equals("dirreq-v2-tunneled-dl")) {
+          this.parseDirreqV2TunneledDlLine(line, lineNoOpt, partsNoOpt);
+        } else if (keyword.equals("dirreq-v3-tunneled-dl")) {
+          this.parseDirreqV3TunneledDlLine(line, lineNoOpt, partsNoOpt);
+        } else if (keyword.equals("dirreq-read-history")) {
+          this.parseDirreqReadHistoryLine(line, lineNoOpt, partsNoOpt);
+        } else if (keyword.equals("dirreq-write-history")) {
+          this.parseDirreqWriteHistoryLine(line, lineNoOpt, partsNoOpt);
+        } else if (keyword.equals("entry-stats-end")) {
+          this.parseEntryStatsEndLine(line, lineNoOpt, partsNoOpt);
+        } else if (keyword.equals("entry-ips")) {
+          this.parseEntryIpsLine(line, lineNoOpt, partsNoOpt);
+        } else if (keyword.equals("cell-stats-end")) {
+          this.parseCellStatsEndLine(line, lineNoOpt, partsNoOpt);
+        } else if (keyword.equals("cell-processed-cells")) {
+          this.parseCellProcessedCellsLine(line, lineNoOpt, partsNoOpt);
+        } else if (keyword.equals("cell-queued-cells")) {
+          this.parseCellQueuedCellsLine(line, lineNoOpt, partsNoOpt);
+        } else if (keyword.equals("cell-time-in-queue")) {
+          this.parseCellTimeInQueueLine(line, lineNoOpt, partsNoOpt);
+        } else if (keyword.equals("cell-circuits-per-decile")) {
+          this.parseCellCircuitsPerDecileLine(line, lineNoOpt,
+              partsNoOpt);
+        } else if (keyword.equals("conn-bi-direct")) {
+          this.parseConnBiDirectLine(line, lineNoOpt, partsNoOpt);
+        } else if (keyword.equals("exit-stats-end")) {
+          this.parseExitStatsEndLine(line, lineNoOpt, partsNoOpt);
+        } else if (keyword.equals("exit-kibibytes-written")) {
+          this.parseExitKibibytesWrittenLine(line, lineNoOpt, partsNoOpt);
+        } else if (keyword.equals("exit-kibibytes-read")) {
+          this.parseExitKibibytesReadLine(line, lineNoOpt, partsNoOpt);
+        } else if (keyword.equals("exit-streams-opened")) {
+          this.parseExitStreamsOpenedLine(line, lineNoOpt, partsNoOpt);
+        } else if (keyword.equals("router-signature")) {
+          this.parseRouterSignatureLine(line, lineNoOpt, partsNoOpt);
+        } else if (line.startsWith("-----BEGIN")) {
+          skipCrypto = true;
+        } else if (line.startsWith("-----END")) {
+          skipCrypto = false;
+        } else if (!skipCrypto) {
+          /* 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.  In theory,
+           * dir-spec.txt says that unknown lines should be ignored.  This
+           * also applies to the other descriptors. */
+          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);
+    }
+  }
+
+  private void parseExtraInfoLine(String line, String lineNoOpt,
+      String[] partsNoOpt) throws DescriptorParseException {
+    if (partsNoOpt.length != 3) {
+      throw new DescriptorParseException("Illegal line '" + line
+          + "' in extra-info descriptor.");
+    }
+    this.nickname = ParseHelper.parseNickname(line, partsNoOpt[1]);
+    this.fingerprint = ParseHelper.parseTwentyByteHexString(line,
+        partsNoOpt[2]);
+  }
+
+  private void parsePublishedLine(String line, String lineNoOpt,
+      String[] partsNoOpt) throws DescriptorParseException {
+    this.publishedMillis = ParseHelper.parseTimestampAtIndex(line,
+        partsNoOpt, 1, 2);
+  }
+
+  private void parseReadHistoryLine(String line, String lineNoOpt,
+      String[] partsNoOpt) throws DescriptorParseException {
+    this.readHistory = new BandwidthHistoryImpl(line, lineNoOpt,
+        partsNoOpt);
+  }
+
+  private void parseWriteHistoryLine(String line, String lineNoOpt,
+      String[] partsNoOpt) throws DescriptorParseException {
+    this.writeHistory = new BandwidthHistoryImpl(line, lineNoOpt,
+        partsNoOpt);
+  }
+
+  private void parseGeoipDbDigestLine(String line, String lineNoOpt,
+      String[] partsNoOpt) throws DescriptorParseException {
+    /* TODO Implement me. */
+  }
+
+  private void parseGeoipStartTimeLine(String line, String lineNoOpt,
+      String[] partsNoOpt) throws DescriptorParseException {
+    /* TODO Implement me. */
+  }
+
+  private void parseGeoipClientOriginsLine(String line, String lineNoOpt,
+      String[] partsNoOpt) throws DescriptorParseException {
+    /* TODO Implement me. */
+  }
+
+  private void parseDirreqStatsEndLine(String line, String lineNoOpt,
+      String[] partsNoOpt) throws DescriptorParseException {
+    /* TODO Implement me. */
+  }
+
+  private void parseDirreqV2IpsLine(String line, String lineNoOpt,
+      String[] partsNoOpt) throws DescriptorParseException {
+    /* TODO Implement me. */
+  }
+
+  private void parseDirreqV3IpsLine(String line, String lineNoOpt,
+      String[] partsNoOpt) throws DescriptorParseException {
+    /* TODO Implement me. */
+  }
+
+  private void parseDirreqV2ReqsLine(String line, String lineNoOpt,
+      String[] partsNoOpt) throws DescriptorParseException {
+    /* TODO Implement me. */
+  }
+
+  private void parseDirreqV3ReqsLine(String line, String lineNoOpt,
+      String[] partsNoOpt) throws DescriptorParseException {
+    /* TODO Implement me. */
+  }
+
+  private void parseDirreqV2ShareLine(String line, String lineNoOpt,
+      String[] partsNoOpt) throws DescriptorParseException {
+    /* TODO Implement me. */
+  }
+
+  private void parseDirreqV3ShareLine(String line, String lineNoOpt,
+      String[] partsNoOpt) throws DescriptorParseException {
+    /* TODO Implement me. */
+  }
+
+  private void parseDirreqV2RespLine(String line, String lineNoOpt,
+      String[] partsNoOpt) throws DescriptorParseException {
+    /* TODO Implement me. */
+  }
+
+  private void parseDirreqV3RespLine(String line, String lineNoOpt,
+      String[] partsNoOpt) throws DescriptorParseException {
+    /* TODO Implement me. */
+  }
+
+  private void parseDirreqV2DirectDlLine(String line, String lineNoOpt,
+      String[] partsNoOpt) throws DescriptorParseException {
+    /* TODO Implement me. */
+  }
+
+  private void parseDirreqV3DirectDlLine(String line, String lineNoOpt,
+      String[] partsNoOpt) throws DescriptorParseException {
+    /* TODO Implement me. */
+  }
+
+  private void parseDirreqV2TunneledDlLine(String line, String lineNoOpt,
+      String[] partsNoOpt) throws DescriptorParseException {
+    /* TODO Implement me. */
+  }
+
+  private void parseDirreqV3TunneledDlLine(String line, String lineNoOpt,
+      String[] partsNoOpt) throws DescriptorParseException {
+    /* TODO Implement me. */
+  }
+
+  private void parseDirreqReadHistoryLine(String line, String lineNoOpt,
+      String[] partsNoOpt) throws DescriptorParseException {
+    this.dirreqReadHistory = new BandwidthHistoryImpl(line, lineNoOpt,
+        partsNoOpt);
+  }
+
+  private void parseDirreqWriteHistoryLine(String line, String lineNoOpt,
+      String[] partsNoOpt) throws DescriptorParseException {
+    this.dirreqWriteHistory = new BandwidthHistoryImpl(line, lineNoOpt,
+        partsNoOpt);
+  }
+
+  private void parseEntryStatsEndLine(String line, String lineNoOpt,
+      String[] partsNoOpt) throws DescriptorParseException {
+    /* TODO Implement me. */
+  }
+
+  private void parseEntryIpsLine(String line, String lineNoOpt,
+      String[] partsNoOpt) throws DescriptorParseException {
+    /* TODO Implement me. */
+  }
+
+  private void parseCellStatsEndLine(String line, String lineNoOpt,
+      String[] partsNoOpt) throws DescriptorParseException {
+    /* TODO Implement me. */
+  }
+
+  private void parseCellProcessedCellsLine(String line, String lineNoOpt,
+      String[] partsNoOpt) throws DescriptorParseException {
+    /* TODO Implement me. */
+  }
+
+  private void parseCellQueuedCellsLine(String line, String lineNoOpt,
+      String[] partsNoOpt) throws DescriptorParseException {
+    /* TODO Implement me. */
+  }
+
+  private void parseCellTimeInQueueLine(String line, String lineNoOpt,
+      String[] partsNoOpt) throws DescriptorParseException {
+    /* TODO Implement me. */
+  }
+
+  private void parseCellCircuitsPerDecileLine(String line,
+      String lineNoOpt, String[] partsNoOpt)
+      throws DescriptorParseException {
+    /* TODO Implement me. */
+  }
+
+  private void parseConnBiDirectLine(String line, String lineNoOpt,
+      String[] partsNoOpt) throws DescriptorParseException {
+    /* TODO Implement me. */
+  }
+
+  private void parseExitStatsEndLine(String line, String lineNoOpt,
+      String[] partsNoOpt) throws DescriptorParseException {
+    /* TODO Implement me. */
+  }
+
+  private void parseExitKibibytesWrittenLine(String line,
+      String lineNoOpt, String[] partsNoOpt)
+      throws DescriptorParseException {
+    /* TODO Implement me. */
+  }
+
+  private void parseExitKibibytesReadLine(String line, String lineNoOpt,
+      String[] partsNoOpt) throws DescriptorParseException {
+    /* TODO Implement me. */
+  }
+
+  private void parseExitStreamsOpenedLine(String line, String lineNoOpt,
+      String[] partsNoOpt) throws DescriptorParseException {
+    /* TODO Implement me. */
+  }
+
+  private void parseRouterSignatureLine(String line, String lineNoOpt,
+      String[] partsNoOpt) throws DescriptorParseException {
+    if (!lineNoOpt.equals("router-signature")) {
+      throw new DescriptorParseException("Illegal line '" + line + "'.");
+    }
+    /* Not parsing crypto parts (yet). */
+  }
+
+  private String nickname;
+  public String getNickname() {
+    return this.nickname;
+  }
+
+  private String fingerprint;
+  public String getFingerprint() {
+    return this.fingerprint;
+  }
+
+  private long publishedMillis;
+  public long getPublishedMillis() {
+    return this.publishedMillis;
+  }
+
+  private BandwidthHistory readHistory;
+  public BandwidthHistory getReadHistory() {
+    return this.readHistory;
+  }
+
+  private BandwidthHistory writeHistory;
+  public BandwidthHistory getWriteHistory() {
+    return this.writeHistory;
+  }
+
+  public String getGeoipDbDigest() {
+    /* TODO Implement me. */
+    throw new UnsupportedOperationException();
+  }
+
+  public long getDirreqStatsEndMillis() {
+    /* TODO Implement me. */
+    throw new UnsupportedOperationException();
+  }
+
+  public long getDirreqStatsIntervalLength() {
+    /* TODO Implement me. */
+    throw new UnsupportedOperationException();
+  }
+
+  public SortedMap<String, Integer> getDirreqV2Ips() {
+    /* TODO Implement me. */
+    throw new UnsupportedOperationException();
+  }
+
+  public SortedMap<String, Integer> getDirreqV3Ips() {
+    /* TODO Implement me. */
+    throw new UnsupportedOperationException();
+  }
+
+  public SortedMap<String, Integer> getDirreqV2Reqs() {
+    /* TODO Implement me. */
+    throw new UnsupportedOperationException();
+  }
+
+  public SortedMap<String, Integer> getDirreqV3Reqs() {
+    /* TODO Implement me. */
+    throw new UnsupportedOperationException();
+  }
+
+  public double getDirreqV2Share() {
+    /* TODO Implement me. */
+    throw new UnsupportedOperationException();
+  }
+
+  public double getDirreqV3Share() {
+    /* TODO Implement me. */
+    throw new UnsupportedOperationException();
+  }
+
+  public SortedMap<String, Integer> getDirreqV2Resp() {
+    /* TODO Implement me. */
+    throw new UnsupportedOperationException();
+  }
+
+  public SortedMap<String, Integer> getDirreqV3Resp() {
+    /* TODO Implement me. */
+    throw new UnsupportedOperationException();
+  }
+
+  public SortedMap<String, Integer> getDirreqV2DirectDl() {
+    /* TODO Implement me. */
+    throw new UnsupportedOperationException();
+  }
+
+  public SortedMap<String, Integer> getDirreqV3DirectDl() {
+    /* TODO Implement me. */
+    throw new UnsupportedOperationException();
+  }
+
+  public SortedMap<String, Integer> getDirreqV2TunneledDl() {
+    /* TODO Implement me. */
+    throw new UnsupportedOperationException();
+  }
+
+  public SortedMap<String, Integer> getDirreqV3TunneledDl() {
+    /* TODO Implement me. */
+    throw new UnsupportedOperationException();
+  }
+
+  private BandwidthHistory dirreqReadHistory;
+  public BandwidthHistory getDirreqReadHistory() {
+    return this.dirreqReadHistory;
+  }
+
+  private BandwidthHistory dirreqWriteHistory;
+  public BandwidthHistory getDirreqWriteHistory() {
+    return this.dirreqWriteHistory;
+  }
+
+  public long getEntryStatsEndMillis() {
+    /* TODO Implement me. */
+    throw new UnsupportedOperationException();
+  }
+
+  public long getEntryStatsIntervalLength() {
+    /* TODO Implement me. */
+    throw new UnsupportedOperationException();
+  }
+
+  public SortedMap<String, Integer> getEntryIps() {
+    /* TODO Implement me. */
+    throw new UnsupportedOperationException();
+  }
+
+  public long getCellStatsEndMillis() {
+    /* TODO Implement me. */
+    throw new UnsupportedOperationException();
+  }
+
+  public long getCellStatsIntervalLength() {
+    /* TODO Implement me. */
+    throw new UnsupportedOperationException();
+  }
+
+  public List<Integer> getCellProcessedCells() {
+    /* TODO Implement me. */
+    throw new UnsupportedOperationException();
+  }
+
+  public List<Integer> getCellQueueCells() {
+    /* TODO Implement me. */
+    throw new UnsupportedOperationException();
+  }
+
+  public List<Integer> getCellTimeInQueue() {
+    /* TODO Implement me. */
+    throw new UnsupportedOperationException();
+  }
+
+  public int getCellCircuitsPerDecile() {
+    /* TODO Implement me. */
+    throw new UnsupportedOperationException();
+  }
+
+  public long getConnBiDirectStatsEndMillis() {
+    /* TODO Implement me. */
+    throw new UnsupportedOperationException();
+  }
+
+  public long getConnBiDirectIntervalLength() {
+    /* TODO Implement me. */
+    throw new UnsupportedOperationException();
+  }
+
+  public int getConnBiDirectBelow() {
+    /* TODO Implement me. */
+    throw new UnsupportedOperationException();
+  }
+
+  public int getConnBiDirectRead() {
+    /* TODO Implement me. */
+    throw new UnsupportedOperationException();
+  }
+
+  public int getConnBiDirectWrite() {
+    /* TODO Implement me. */
+    throw new UnsupportedOperationException();
+  }
+
+  public int getConnBiDirectBoth() {
+    /* TODO Implement me. */
+    throw new UnsupportedOperationException();
+  }
+
+  public long getExitStatsEndMillis() {
+    /* TODO Implement me. */
+    throw new UnsupportedOperationException();
+  }
+
+  public long getExitStatsIntervalLength() {
+    /* TODO Implement me. */
+    throw new UnsupportedOperationException();
+  }
+
+  public SortedMap<Integer, Integer> getExitKibibytesWritten() {
+    /* TODO Implement me. */
+    throw new UnsupportedOperationException();
+  }
+
+  public SortedMap<Integer, Integer> getExitKibibytesRead() {
+    /* TODO Implement me. */
+    throw new UnsupportedOperationException();
+  }
+
+  public SortedMap<Integer, Integer> getExitStreamsOpened() {
+    /* TODO Implement me. */
+    throw new UnsupportedOperationException();
+  }
+}
+
diff --git a/src/org/torproject/descriptor/impl/RelayExtraInfoDescriptorImpl.java b/src/org/torproject/descriptor/impl/RelayExtraInfoDescriptorImpl.java
deleted file mode 100644
index a07caec..0000000
--- a/src/org/torproject/descriptor/impl/RelayExtraInfoDescriptorImpl.java
+++ /dev/null
@@ -1,574 +0,0 @@
-/* Copyright 2012 The Tor Project
- * See LICENSE for licensing information */
-package org.torproject.descriptor.impl;
-
-import java.io.BufferedReader;
-import java.io.IOException;
-import java.io.StringReader;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Set;
-import java.util.SortedMap;
-import org.torproject.descriptor.RelayExtraInfoDescriptor;
-import org.torproject.descriptor.BandwidthHistory;
-
-/* TODO Implement methods to parse the various statistics (other than
- * bandwidth histories. */
-/* TODO Write a test class. */
-public class RelayExtraInfoDescriptorImpl extends DescriptorImpl
-    implements RelayExtraInfoDescriptor {
-
-  protected static List<RelayExtraInfoDescriptor> parseDescriptors(
-      byte[] descriptorsBytes) {
-    List<RelayExtraInfoDescriptor> parsedDescriptors =
-        new ArrayList<RelayExtraInfoDescriptor>();
-    List<byte[]> splitDescriptorsBytes =
-        DescriptorImpl.splitRawDescriptorBytes(descriptorsBytes,
-        "extra-info ");
-    try {
-      for (byte[] descriptorBytes : splitDescriptorsBytes) {
-        RelayExtraInfoDescriptor parsedDescriptor =
-            new RelayExtraInfoDescriptorImpl(descriptorBytes);
-        parsedDescriptors.add(parsedDescriptor);
-      }
-    } catch (DescriptorParseException e) {
-      /* TODO Handle this error somehow. */
-      System.err.println("Failed to parse descriptor.  Skipping.");
-      e.printStackTrace();
-    }
-    return parsedDescriptors;
-  }
-
-  protected RelayExtraInfoDescriptorImpl(byte[] descriptorBytes)
-      throws DescriptorParseException {
-    super(descriptorBytes);
-    this.parseDescriptorBytes();
-    Set<String> exactlyOnceKeywords = new HashSet<String>(Arrays.asList((
-        "extra-info,published,router-signature").split(",")));
-    this.checkExactlyOnceKeywords(exactlyOnceKeywords);
-    Set<String> atMostOnceKeywords = new HashSet<String>(Arrays.asList((
-        "read-history,write-history,geoip-db-digest,dirreq-stats-end,"
-        + "dirreq-v2-ips,dirreq-v3-ips,dirreq-v2-reqs,dirreq-v3-reqs,"
-        + "dirreq-v2-share,dirreq-v3-share,dirreq-v2-resp,dirreq-v3-resp,"
-        + "dirreq-v2-direct-dl,dirreq-v3-direct-dl,dirreq-v2-tunneled-dl,"
-        + "dirreq-v3-tunneled-dl,dirreq-read-history,"
-        + "dirreq-write-history,entry-stats-end,entry-ips,cell-stats-end,"
-        + "cell-processed-cells,cell-queued-cells,cell-time-in-queue,"
-        + "cell-circuits-per-decile,conn-bi-direct,exit-stats-end,"
-        + "exit-kibibytes-written,exit-kibibytes-read,"
-        + "exit-streams-opened").split(",")));
-    this.checkAtMostOnceKeywords(atMostOnceKeywords);
-    /* TODO Add more checks to see that only statistics details lines are
-     * included with corresponding statistics interval lines. */
-    this.checkFirstKeyword("extra-info");
-    this.checkLastKeyword("router-signature");
-    return;
-  }
-
-  private void parseDescriptorBytes() throws DescriptorParseException {
-    try {
-      BufferedReader br = new BufferedReader(new StringReader(
-          new String(this.rawDescriptorBytes)));
-      String line;
-      boolean skipCrypto = false;
-      while ((line = br.readLine()) != null) {
-        String lineNoOpt = line.startsWith("opt ") ?
-            line.substring("opt ".length()) : line;
-        String[] partsNoOpt = lineNoOpt.split(" ");
-        String keyword = partsNoOpt[0];
-        if (keyword.equals("extra-info")) {
-          this.parseExtraInfoLine(line, lineNoOpt, partsNoOpt);
-        } else if (keyword.equals("published")) {
-          this.parsePublishedLine(line, lineNoOpt, partsNoOpt);
-        } else if (keyword.equals("read-history")) {
-          this.parseReadHistoryLine(line, lineNoOpt, partsNoOpt);
-        } else if (keyword.equals("write-history")) {
-          this.parseWriteHistoryLine(line, lineNoOpt, partsNoOpt);
-        } else if (keyword.equals("geoip-db-digest")) {
-          this.parseGeoipDbDigestLine(line, lineNoOpt, partsNoOpt);
-        } else if (keyword.equals("geoip-start-time")) {
-          this.parseGeoipStartTimeLine(line, lineNoOpt, partsNoOpt);
-        } else if (keyword.equals("geoip-client-origins")) {
-          this.parseGeoipClientOriginsLine(line, lineNoOpt, partsNoOpt);
-        } else if (keyword.equals("dirreq-stats-end")) {
-          this.parseDirreqStatsEndLine(line, lineNoOpt, partsNoOpt);
-        } else if (keyword.equals("dirreq-v2-ips")) {
-          this.parseDirreqV2IpsLine(line, lineNoOpt, partsNoOpt);
-        } else if (keyword.equals("dirreq-v3-ips")) {
-          this.parseDirreqV3IpsLine(line, lineNoOpt, partsNoOpt);
-        } else if (keyword.equals("dirreq-v2-reqs")) {
-          this.parseDirreqV2ReqsLine(line, lineNoOpt, partsNoOpt);
-        } else if (keyword.equals("dirreq-v3-reqs")) {
-          this.parseDirreqV3ReqsLine(line, lineNoOpt, partsNoOpt);
-        } else if (keyword.equals("dirreq-v2-share")) {
-          this.parseDirreqV2ShareLine(line, lineNoOpt, partsNoOpt);
-        } else if (keyword.equals("dirreq-v3-share")) {
-          this.parseDirreqV3ShareLine(line, lineNoOpt, partsNoOpt);
-        } else if (keyword.equals("dirreq-v2-resp")) {
-          this.parseDirreqV2RespLine(line, lineNoOpt, partsNoOpt);
-        } else if (keyword.equals("dirreq-v3-resp")) {
-          this.parseDirreqV3RespLine(line, lineNoOpt, partsNoOpt);
-        } else if (keyword.equals("dirreq-v2-direct-dl")) {
-          this.parseDirreqV2DirectDlLine(line, lineNoOpt, partsNoOpt);
-        } else if (keyword.equals("dirreq-v3-direct-dl")) {
-          this.parseDirreqV3DirectDlLine(line, lineNoOpt, partsNoOpt);
-        } else if (keyword.equals("dirreq-v2-tunneled-dl")) {
-          this.parseDirreqV2TunneledDlLine(line, lineNoOpt, partsNoOpt);
-        } else if (keyword.equals("dirreq-v3-tunneled-dl")) {
-          this.parseDirreqV3TunneledDlLine(line, lineNoOpt, partsNoOpt);
-        } else if (keyword.equals("dirreq-read-history")) {
-          this.parseDirreqReadHistoryLine(line, lineNoOpt, partsNoOpt);
-        } else if (keyword.equals("dirreq-write-history")) {
-          this.parseDirreqWriteHistoryLine(line, lineNoOpt, partsNoOpt);
-        } else if (keyword.equals("entry-stats-end")) {
-          this.parseEntryStatsEndLine(line, lineNoOpt, partsNoOpt);
-        } else if (keyword.equals("entry-ips")) {
-          this.parseEntryIpsLine(line, lineNoOpt, partsNoOpt);
-        } else if (keyword.equals("cell-stats-end")) {
-          this.parseCellStatsEndLine(line, lineNoOpt, partsNoOpt);
-        } else if (keyword.equals("cell-processed-cells")) {
-          this.parseCellProcessedCellsLine(line, lineNoOpt, partsNoOpt);
-        } else if (keyword.equals("cell-queued-cells")) {
-          this.parseCellQueuedCellsLine(line, lineNoOpt, partsNoOpt);
-        } else if (keyword.equals("cell-time-in-queue")) {
-          this.parseCellTimeInQueueLine(line, lineNoOpt, partsNoOpt);
-        } else if (keyword.equals("cell-circuits-per-decile")) {
-          this.parseCellCircuitsPerDecileLine(line, lineNoOpt,
-              partsNoOpt);
-        } else if (keyword.equals("conn-bi-direct")) {
-          this.parseConnBiDirectLine(line, lineNoOpt, partsNoOpt);
-        } else if (keyword.equals("exit-stats-end")) {
-          this.parseExitStatsEndLine(line, lineNoOpt, partsNoOpt);
-        } else if (keyword.equals("exit-kibibytes-written")) {
-          this.parseExitKibibytesWrittenLine(line, lineNoOpt, partsNoOpt);
-        } else if (keyword.equals("exit-kibibytes-read")) {
-          this.parseExitKibibytesReadLine(line, lineNoOpt, partsNoOpt);
-        } else if (keyword.equals("exit-streams-opened")) {
-          this.parseExitStreamsOpenedLine(line, lineNoOpt, partsNoOpt);
-        } else if (keyword.equals("router-signature")) {
-          this.parseRouterSignatureLine(line, lineNoOpt, partsNoOpt);
-        } else if (line.startsWith("-----BEGIN")) {
-          skipCrypto = true;
-        } else if (line.startsWith("-----END")) {
-          skipCrypto = false;
-        } else if (!skipCrypto) {
-          /* 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.  In theory,
-           * dir-spec.txt says that unknown lines should be ignored.  This
-           * also applies to the other descriptors. */
-          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);
-    }
-  }
-
-  private void parseExtraInfoLine(String line, String lineNoOpt,
-      String[] partsNoOpt) throws DescriptorParseException {
-    if (partsNoOpt.length != 3) {
-      throw new DescriptorParseException("Illegal line '" + line
-          + "' in extra-info descriptor.");
-    }
-    this.nickname = ParseHelper.parseNickname(line, partsNoOpt[1]);
-    this.fingerprint = ParseHelper.parseTwentyByteHexString(line,
-        partsNoOpt[2]);
-  }
-
-  private void parsePublishedLine(String line, String lineNoOpt,
-      String[] partsNoOpt) throws DescriptorParseException {
-    this.publishedMillis = ParseHelper.parseTimestampAtIndex(line,
-        partsNoOpt, 1, 2);
-  }
-
-  private void parseReadHistoryLine(String line, String lineNoOpt,
-      String[] partsNoOpt) throws DescriptorParseException {
-    this.readHistory = new BandwidthHistoryImpl(line, lineNoOpt,
-        partsNoOpt);
-  }
-
-  private void parseWriteHistoryLine(String line, String lineNoOpt,
-      String[] partsNoOpt) throws DescriptorParseException {
-    this.writeHistory = new BandwidthHistoryImpl(line, lineNoOpt,
-        partsNoOpt);
-  }
-
-  private void parseGeoipDbDigestLine(String line, String lineNoOpt,
-      String[] partsNoOpt) throws DescriptorParseException {
-    /* TODO Implement me. */
-  }
-
-  private void parseGeoipStartTimeLine(String line, String lineNoOpt,
-      String[] partsNoOpt) throws DescriptorParseException {
-    /* TODO Implement me. */
-  }
-
-  private void parseGeoipClientOriginsLine(String line, String lineNoOpt,
-      String[] partsNoOpt) throws DescriptorParseException {
-    /* TODO Implement me. */
-  }
-
-  private void parseDirreqStatsEndLine(String line, String lineNoOpt,
-      String[] partsNoOpt) throws DescriptorParseException {
-    /* TODO Implement me. */
-  }
-
-  private void parseDirreqV2IpsLine(String line, String lineNoOpt,
-      String[] partsNoOpt) throws DescriptorParseException {
-    /* TODO Implement me. */
-  }
-
-  private void parseDirreqV3IpsLine(String line, String lineNoOpt,
-      String[] partsNoOpt) throws DescriptorParseException {
-    /* TODO Implement me. */
-  }
-
-  private void parseDirreqV2ReqsLine(String line, String lineNoOpt,
-      String[] partsNoOpt) throws DescriptorParseException {
-    /* TODO Implement me. */
-  }
-
-  private void parseDirreqV3ReqsLine(String line, String lineNoOpt,
-      String[] partsNoOpt) throws DescriptorParseException {
-    /* TODO Implement me. */
-  }
-
-  private void parseDirreqV2ShareLine(String line, String lineNoOpt,
-      String[] partsNoOpt) throws DescriptorParseException {
-    /* TODO Implement me. */
-  }
-
-  private void parseDirreqV3ShareLine(String line, String lineNoOpt,
-      String[] partsNoOpt) throws DescriptorParseException {
-    /* TODO Implement me. */
-  }
-
-  private void parseDirreqV2RespLine(String line, String lineNoOpt,
-      String[] partsNoOpt) throws DescriptorParseException {
-    /* TODO Implement me. */
-  }
-
-  private void parseDirreqV3RespLine(String line, String lineNoOpt,
-      String[] partsNoOpt) throws DescriptorParseException {
-    /* TODO Implement me. */
-  }
-
-  private void parseDirreqV2DirectDlLine(String line, String lineNoOpt,
-      String[] partsNoOpt) throws DescriptorParseException {
-    /* TODO Implement me. */
-  }
-
-  private void parseDirreqV3DirectDlLine(String line, String lineNoOpt,
-      String[] partsNoOpt) throws DescriptorParseException {
-    /* TODO Implement me. */
-  }
-
-  private void parseDirreqV2TunneledDlLine(String line, String lineNoOpt,
-      String[] partsNoOpt) throws DescriptorParseException {
-    /* TODO Implement me. */
-  }
-
-  private void parseDirreqV3TunneledDlLine(String line, String lineNoOpt,
-      String[] partsNoOpt) throws DescriptorParseException {
-    /* TODO Implement me. */
-  }
-
-  private void parseDirreqReadHistoryLine(String line, String lineNoOpt,
-      String[] partsNoOpt) throws DescriptorParseException {
-    this.dirreqReadHistory = new BandwidthHistoryImpl(line, lineNoOpt,
-        partsNoOpt);
-  }
-
-  private void parseDirreqWriteHistoryLine(String line, String lineNoOpt,
-      String[] partsNoOpt) throws DescriptorParseException {
-    this.dirreqWriteHistory = new BandwidthHistoryImpl(line, lineNoOpt,
-        partsNoOpt);
-  }
-
-  private void parseEntryStatsEndLine(String line, String lineNoOpt,
-      String[] partsNoOpt) throws DescriptorParseException {
-    /* TODO Implement me. */
-  }
-
-  private void parseEntryIpsLine(String line, String lineNoOpt,
-      String[] partsNoOpt) throws DescriptorParseException {
-    /* TODO Implement me. */
-  }
-
-  private void parseCellStatsEndLine(String line, String lineNoOpt,
-      String[] partsNoOpt) throws DescriptorParseException {
-    /* TODO Implement me. */
-  }
-
-  private void parseCellProcessedCellsLine(String line, String lineNoOpt,
-      String[] partsNoOpt) throws DescriptorParseException {
-    /* TODO Implement me. */
-  }
-
-  private void parseCellQueuedCellsLine(String line, String lineNoOpt,
-      String[] partsNoOpt) throws DescriptorParseException {
-    /* TODO Implement me. */
-  }
-
-  private void parseCellTimeInQueueLine(String line, String lineNoOpt,
-      String[] partsNoOpt) throws DescriptorParseException {
-    /* TODO Implement me. */
-  }
-
-  private void parseCellCircuitsPerDecileLine(String line,
-      String lineNoOpt, String[] partsNoOpt)
-      throws DescriptorParseException {
-    /* TODO Implement me. */
-  }
-
-  private void parseConnBiDirectLine(String line, String lineNoOpt,
-      String[] partsNoOpt) throws DescriptorParseException {
-    /* TODO Implement me. */
-  }
-
-  private void parseExitStatsEndLine(String line, String lineNoOpt,
-      String[] partsNoOpt) throws DescriptorParseException {
-    /* TODO Implement me. */
-  }
-
-  private void parseExitKibibytesWrittenLine(String line,
-      String lineNoOpt, String[] partsNoOpt)
-      throws DescriptorParseException {
-    /* TODO Implement me. */
-  }
-
-  private void parseExitKibibytesReadLine(String line, String lineNoOpt,
-      String[] partsNoOpt) throws DescriptorParseException {
-    /* TODO Implement me. */
-  }
-
-  private void parseExitStreamsOpenedLine(String line, String lineNoOpt,
-      String[] partsNoOpt) throws DescriptorParseException {
-    /* TODO Implement me. */
-  }
-
-  private void parseRouterSignatureLine(String line, String lineNoOpt,
-      String[] partsNoOpt) throws DescriptorParseException {
-    if (!lineNoOpt.equals("router-signature")) {
-      throw new DescriptorParseException("Illegal line '" + line + "'.");
-    }
-    /* Not parsing crypto parts (yet). */
-  }
-
-  private String nickname;
-  public String getNickname() {
-    return this.nickname;
-  }
-
-  private String fingerprint;
-  public String getFingerprint() {
-    return this.fingerprint;
-  }
-
-  private long publishedMillis;
-  public long getPublishedMillis() {
-    return this.publishedMillis;
-  }
-
-  private BandwidthHistory readHistory;
-  public BandwidthHistory getReadHistory() {
-    return this.readHistory;
-  }
-
-  private BandwidthHistory writeHistory;
-  public BandwidthHistory getWriteHistory() {
-    return this.writeHistory;
-  }
-
-  public String getGeoipDbDigest() {
-    /* TODO Implement me. */
-    throw new UnsupportedOperationException();
-  }
-
-  public long getDirreqStatsEndMillis() {
-    /* TODO Implement me. */
-    throw new UnsupportedOperationException();
-  }
-
-  public long getDirreqStatsIntervalLength() {
-    /* TODO Implement me. */
-    throw new UnsupportedOperationException();
-  }
-
-  public SortedMap<String, Integer> getDirreqV2Ips() {
-    /* TODO Implement me. */
-    throw new UnsupportedOperationException();
-  }
-
-  public SortedMap<String, Integer> getDirreqV3Ips() {
-    /* TODO Implement me. */
-    throw new UnsupportedOperationException();
-  }
-
-  public SortedMap<String, Integer> getDirreqV2Reqs() {
-    /* TODO Implement me. */
-    throw new UnsupportedOperationException();
-  }
-
-  public SortedMap<String, Integer> getDirreqV3Reqs() {
-    /* TODO Implement me. */
-    throw new UnsupportedOperationException();
-  }
-
-  public double getDirreqV2Share() {
-    /* TODO Implement me. */
-    throw new UnsupportedOperationException();
-  }
-
-  public double getDirreqV3Share() {
-    /* TODO Implement me. */
-    throw new UnsupportedOperationException();
-  }
-
-  public SortedMap<String, Integer> getDirreqV2Resp() {
-    /* TODO Implement me. */
-    throw new UnsupportedOperationException();
-  }
-
-  public SortedMap<String, Integer> getDirreqV3Resp() {
-    /* TODO Implement me. */
-    throw new UnsupportedOperationException();
-  }
-
-  public SortedMap<String, Integer> getDirreqV2DirectDl() {
-    /* TODO Implement me. */
-    throw new UnsupportedOperationException();
-  }
-
-  public SortedMap<String, Integer> getDirreqV3DirectDl() {
-    /* TODO Implement me. */
-    throw new UnsupportedOperationException();
-  }
-
-  public SortedMap<String, Integer> getDirreqV2TunneledDl() {
-    /* TODO Implement me. */
-    throw new UnsupportedOperationException();
-  }
-
-  public SortedMap<String, Integer> getDirreqV3TunneledDl() {
-    /* TODO Implement me. */
-    throw new UnsupportedOperationException();
-  }
-
-  private BandwidthHistory dirreqReadHistory;
-  public BandwidthHistory getDirreqReadHistory() {
-    return this.dirreqReadHistory;
-  }
-
-  private BandwidthHistory dirreqWriteHistory;
-  public BandwidthHistory getDirreqWriteHistory() {
-    return this.dirreqWriteHistory;
-  }
-
-  public long getEntryStatsEndMillis() {
-    /* TODO Implement me. */
-    throw new UnsupportedOperationException();
-  }
-
-  public long getEntryStatsIntervalLength() {
-    /* TODO Implement me. */
-    throw new UnsupportedOperationException();
-  }
-
-  public SortedMap<String, Integer> getEntryIps() {
-    /* TODO Implement me. */
-    throw new UnsupportedOperationException();
-  }
-
-  public long getCellStatsEndMillis() {
-    /* TODO Implement me. */
-    throw new UnsupportedOperationException();
-  }
-
-  public long getCellStatsIntervalLength() {
-    /* TODO Implement me. */
-    throw new UnsupportedOperationException();
-  }
-
-  public List<Integer> getCellProcessedCells() {
-    /* TODO Implement me. */
-    throw new UnsupportedOperationException();
-  }
-
-  public List<Integer> getCellQueueCells() {
-    /* TODO Implement me. */
-    throw new UnsupportedOperationException();
-  }
-
-  public List<Integer> getCellTimeInQueue() {
-    /* TODO Implement me. */
-    throw new UnsupportedOperationException();
-  }
-
-  public int getCellCircuitsPerDecile() {
-    /* TODO Implement me. */
-    throw new UnsupportedOperationException();
-  }
-
-  public long getConnBiDirectStatsEndMillis() {
-    /* TODO Implement me. */
-    throw new UnsupportedOperationException();
-  }
-
-  public long getConnBiDirectIntervalLength() {
-    /* TODO Implement me. */
-    throw new UnsupportedOperationException();
-  }
-
-  public int getConnBiDirectBelow() {
-    /* TODO Implement me. */
-    throw new UnsupportedOperationException();
-  }
-
-  public int getConnBiDirectRead() {
-    /* TODO Implement me. */
-    throw new UnsupportedOperationException();
-  }
-
-  public int getConnBiDirectWrite() {
-    /* TODO Implement me. */
-    throw new UnsupportedOperationException();
-  }
-
-  public int getConnBiDirectBoth() {
-    /* TODO Implement me. */
-    throw new UnsupportedOperationException();
-  }
-
-  public long getExitStatsEndMillis() {
-    /* TODO Implement me. */
-    throw new UnsupportedOperationException();
-  }
-
-  public long getExitStatsIntervalLength() {
-    /* TODO Implement me. */
-    throw new UnsupportedOperationException();
-  }
-
-  public SortedMap<Integer, Integer> getExitKibibytesWritten() {
-    /* TODO Implement me. */
-    throw new UnsupportedOperationException();
-  }
-
-  public SortedMap<Integer, Integer> getExitKibibytesRead() {
-    /* TODO Implement me. */
-    throw new UnsupportedOperationException();
-  }
-
-  public SortedMap<Integer, Integer> getExitStreamsOpened() {
-    /* TODO Implement me. */
-    throw new UnsupportedOperationException();
-  }
-}
-
diff --git a/src/org/torproject/descriptor/impl/RelayServerDescriptorImpl.java b/src/org/torproject/descriptor/impl/RelayServerDescriptorImpl.java
deleted file mode 100644
index 472a2bc..0000000
--- a/src/org/torproject/descriptor/impl/RelayServerDescriptorImpl.java
+++ /dev/null
@@ -1,532 +0,0 @@
-/* Copyright 2012 The Tor Project
- * See LICENSE for licensing information */
-package org.torproject.descriptor.impl;
-
-import java.io.BufferedReader;
-import java.io.IOException;
-import java.io.StringReader;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Set;
-import org.torproject.descriptor.RelayServerDescriptor;
-import org.torproject.descriptor.BandwidthHistory;
-
-/* Contains a relay server descriptor. */
-public class RelayServerDescriptorImpl extends DescriptorImpl
-    implements RelayServerDescriptor {
-
-  protected static List<RelayServerDescriptor> parseDescriptors(
-      byte[] descriptorsBytes) {
-    List<RelayServerDescriptor> parsedDescriptors =
-        new ArrayList<RelayServerDescriptor>();
-    List<byte[]> splitDescriptorsBytes =
-        DescriptorImpl.splitRawDescriptorBytes(descriptorsBytes,
-        "router ");
-    try {
-      for (byte[] descriptorBytes : splitDescriptorsBytes) {
-        RelayServerDescriptor parsedDescriptor =
-            new RelayServerDescriptorImpl(descriptorBytes);
-        parsedDescriptors.add(parsedDescriptor);
-      }
-    } catch (DescriptorParseException e) {
-      /* TODO Handle this error somehow. */
-      System.err.println("Failed to parse descriptor.  Skipping.");
-      e.printStackTrace();
-    }
-    return parsedDescriptors;
-  }
-
-  protected RelayServerDescriptorImpl(byte[] descriptorBytes)
-      throws DescriptorParseException {
-    super(descriptorBytes);
-    this.parseDescriptorBytes();
-    Set<String> exactlyOnceKeywords = new HashSet<String>(Arrays.asList((
-        "router,bandwidth,published,onion-key,signing-key,"
-        + "router-signature").split(",")));
-    this.checkExactlyOnceKeywords(exactlyOnceKeywords);
-    Set<String> atMostOnceKeywords = new HashSet<String>(Arrays.asList((
-        "platform,fingerprint,hibernating,uptime,contact,family,"
-        + "read-history,write-history,eventdns,caches-extra-info,"
-        + "extra-info-digest,hidden-service-dir,protocols,"
-        + "allow-single-hop-exits").split(",")));
-    this.checkAtMostOnceKeywords(atMostOnceKeywords);
-    this.checkFirstKeyword("router");
-    this.checkLastKeyword("router-signature");
-    if (this.getKeywordCount("accept") == 0 &&
-        this.getKeywordCount("reject") == 0) {
-      throw new DescriptorParseException("Either keyword 'accept' or "
-          + "'reject' must be contained at least once.");
-    }
-    return;
-  }
-
-  private void parseDescriptorBytes() throws DescriptorParseException {
-    try {
-      BufferedReader br = new BufferedReader(new StringReader(
-          new String(this.rawDescriptorBytes)));
-      String line;
-      boolean skipCrypto = false;
-      while ((line = br.readLine()) != null) {
-        if (line.startsWith("@")) {
-          continue;
-        }
-        String lineNoOpt = line.startsWith("opt ") ?
-            line.substring("opt ".length()) : line;
-        String[] partsNoOpt = lineNoOpt.split(" ");
-        String keyword = partsNoOpt[0];
-        if (keyword.equals("router")) {
-          this.parseRouterLine(line, lineNoOpt, partsNoOpt);
-        } else if (keyword.equals("bandwidth")) {
-          this.parseBandwidthLine(line, lineNoOpt, partsNoOpt);
-        } else if (keyword.equals("platform")) {
-          this.parsePlatformLine(line, lineNoOpt, partsNoOpt);
-        } else if (keyword.equals("published")) {
-          this.parsePublishedLine(line, lineNoOpt, partsNoOpt);
-        } else if (keyword.equals("fingerprint")) {
-          this.parseFingerprintLine(line, lineNoOpt, partsNoOpt);
-        } else if (keyword.equals("hibernating")) {
-          this.parseHibernatingLine(line, lineNoOpt, partsNoOpt);
-        } else if (keyword.equals("uptime")) {
-          this.parseUptimeLine(line, lineNoOpt, partsNoOpt);
-        } else if (keyword.equals("onion-key")) {
-          this.parseOnionKeyLine(line, lineNoOpt, partsNoOpt);
-        } else if (keyword.equals("signing-key")) {
-          this.parseSigningKeyLine(line, lineNoOpt, partsNoOpt);
-        } else if (keyword.equals("accept")) {
-          this.parseAcceptLine(line, lineNoOpt, partsNoOpt);
-        } else if (keyword.equals("reject")) {
-          this.parseRejectLine(line, lineNoOpt, partsNoOpt);
-        } else if (keyword.equals("router-signature")) {
-          this.parseRouterSignatureLine(line, lineNoOpt, partsNoOpt);
-        } else if (keyword.equals("contact")) {
-          this.parseContactLine(line, lineNoOpt, partsNoOpt);
-        } else if (keyword.equals("family")) {
-          this.parseFamilyLine(line, lineNoOpt, partsNoOpt);
-        } else if (keyword.equals("read-history")) {
-          this.parseReadHistoryLine(line, lineNoOpt, partsNoOpt);
-        } else if (keyword.equals("write-history")) {
-          this.parseWriteHistoryLine(line, lineNoOpt, partsNoOpt);
-        } else if (keyword.equals("eventdns")) {
-          this.parseEventdnsLine(line, lineNoOpt, partsNoOpt);
-        } else if (keyword.equals("caches-extra-info")) {
-          this.parseCachesExtraInfoLine(line, lineNoOpt, partsNoOpt);
-        } else if (keyword.equals("extra-info-digest")) {
-          this.parseExtraInfoDigestLine(line, lineNoOpt, partsNoOpt);
-        } else if (keyword.equals("hidden-service-dir")) {
-          this.parseHiddenServiceDirLine(line, lineNoOpt, partsNoOpt);
-        } else if (keyword.equals("protocols")) {
-          this.parseProtocolsLine(line, lineNoOpt, partsNoOpt);
-        } else if (keyword.equals("allow-single-hop-exits")) {
-          this.parseAllowSingleHopExitsLine(line, lineNoOpt, partsNoOpt);
-        } else if (line.startsWith("-----BEGIN")) {
-          skipCrypto = true;
-        } else if (line.startsWith("-----END")) {
-          skipCrypto = false;
-        } else if (!skipCrypto) {
-          /* 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.  In theory,
-           * dir-spec.txt says that unknown lines should be ignored.  This
-           * also applies to the other descriptors. */
-          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);
-    }
-  }
-
-  private void parseRouterLine(String line, String lineNoOpt,
-      String[] partsNoOpt) throws DescriptorParseException {
-    if (partsNoOpt.length != 6) {
-      throw new DescriptorParseException("Illegal line '" + line
-          + "' in server descriptor.");
-    }
-    this.nickname = ParseHelper.parseNickname(line, partsNoOpt[1]);
-    this.address = ParseHelper.parseIpv4Address(line, partsNoOpt[2]);
-    this.orPort = ParseHelper.parsePort(line, partsNoOpt[3]);
-    this.socksPort = ParseHelper.parsePort(line, partsNoOpt[4]);
-    this.dirPort = ParseHelper.parsePort(line, partsNoOpt[5]);
-  }
-
-  private void parseBandwidthLine(String line, String lineNoOpt,
-      String[] partsNoOpt) throws DescriptorParseException {
-    if (partsNoOpt.length != 4) {
-      throw new DescriptorParseException("Wrong number of values in line "
-          + "'" + line + "'.");
-    }
-    boolean isValid = false;
-    try {
-      this.bandwidthRate = Integer.parseInt(partsNoOpt[1]);
-      this.bandwidthBurst = Integer.parseInt(partsNoOpt[2]);
-      this.bandwidthObserved = Integer.parseInt(partsNoOpt[3]);
-      if (this.bandwidthRate >= 0 && this.bandwidthBurst >= 0 &&
-          this.bandwidthObserved >= 0) {
-        isValid = true;
-      }
-    } catch (NumberFormatException e) {
-      /* Handle below. */
-    }
-    if (!isValid) {
-      throw new DescriptorParseException("Illegal values in line '" + line
-          + "'.");
-    }
-  }
-
-  private void parsePlatformLine(String line, String lineNoOpt,
-      String[] partsNoOpt) throws DescriptorParseException {
-    if (lineNoOpt.length() > "platform ".length()) {
-      this.platform = lineNoOpt.substring("platform ".length());
-    } else {
-      this.platform = "";
-    }
-  }
-
-  private void parsePublishedLine(String line, String lineNoOpt,
-      String[] partsNoOpt) throws DescriptorParseException {
-    this.publishedMillis = ParseHelper.parseTimestampAtIndex(line,
-        partsNoOpt, 1, 2);
-  }
-
-  private void parseFingerprintLine(String line, String lineNoOpt,
-      String[] partsNoOpt) throws DescriptorParseException {
-    if (lineNoOpt.length() != "fingerprint".length() + 5 * 10) {
-      throw new DescriptorParseException("Illegal line '" + line + "'.");
-    }
-    this.fingerprint = ParseHelper.parseTwentyByteHexString(line,
-        lineNoOpt.substring("fingerprint ".length()).replaceAll(" ", ""));
-  }
-
-  private void parseHibernatingLine(String line, String lineNoOpt,
-      String[] partsNoOpt) throws DescriptorParseException {
-    if (partsNoOpt.length != 2) {
-      throw new DescriptorParseException("Illegal line '" + line + "'.");
-    }
-    if (partsNoOpt[1].equals("1")) {
-      this.hibernating = true;
-    } else if (partsNoOpt[1].equals("0")) {
-      this.hibernating = false;
-    } else {
-      throw new DescriptorParseException("Illegal line '" + line + "'.");
-    }
-  }
-
-  private void parseUptimeLine(String line, String lineNoOpt,
-      String[] partsNoOpt) throws DescriptorParseException {
-    if (partsNoOpt.length != 2) {
-      throw new DescriptorParseException("Wrong number of values in line "
-          + "'" + line + "'.");
-    }
-    boolean isValid = false;
-    try {
-      this.uptime = Integer.parseInt(partsNoOpt[1]);
-      if (this.uptime >= 0) {
-        isValid = true;
-      }
-    } catch (NumberFormatException e) {
-      /* Handle below. */
-    }
-    if (!isValid) {
-      throw new DescriptorParseException("Illegal value in line '" + line
-          + "'.");
-    }
-  }
-
-  private void parseOnionKeyLine(String line, String lineNoOpt,
-      String[] partsNoOpt) throws DescriptorParseException {
-    /* Not parsing crypto parts (yet). */
-  }
-
-  private void parseSigningKeyLine(String line, String lineNoOpt,
-      String[] partsNoOpt) throws DescriptorParseException {
-    /* Not parsing crypto parts (yet). */
-  }
-
-  private void parseAcceptLine(String line, String lineNoOpt,
-      String[] partsNoOpt) throws DescriptorParseException {
-    this.parseExitPolicyLine(line, lineNoOpt, partsNoOpt);
-  }
-
-  private void parseRejectLine(String line, String lineNoOpt,
-      String[] partsNoOpt) throws DescriptorParseException {
-    this.parseExitPolicyLine(line, lineNoOpt, partsNoOpt);
-  }
-
-  private void parseExitPolicyLine(String line, String lineNoOpt,
-      String[] partsNoOpt) throws DescriptorParseException {
-    if (partsNoOpt.length != 2) {
-      throw new DescriptorParseException("Illegal line '" + line + "'.");
-    }
-    ParseHelper.parseExitPattern(line, partsNoOpt[1]);
-    this.exitPolicyLines.add(lineNoOpt);
-  }
-
-  private void parseRouterSignatureLine(String line, String lineNoOpt,
-      String[] partsNoOpt) throws DescriptorParseException {
-    if (!lineNoOpt.equals("router-signature")) {
-      throw new DescriptorParseException("Illegal line '" + line + "'.");
-    }
-    /* Not parsing crypto parts (yet). */
-  }
-
-  private void parseContactLine(String line, String lineNoOpt,
-      String[] partsNoOpt) throws DescriptorParseException {
-    if (lineNoOpt.length() > "contact ".length()) {
-      this.contact = lineNoOpt.substring("contact ".length());
-    } else {
-      this.contact = "";
-    }
-  }
-
-  private void parseFamilyLine(String line, String lineNoOpt,
-      String[] partsNoOpt) throws DescriptorParseException {
-    this.familyEntries = new ArrayList<String>();
-    for (int i = 1; i < partsNoOpt.length; i++) {
-      if (partsNoOpt[i].startsWith("$")) {
-        this.familyEntries.add("$"
-            + ParseHelper.parseTwentyByteHexString(line,
-            partsNoOpt[i].substring(1)));
-      } else {
-        this.familyEntries.add(ParseHelper.parseNickname(line,
-            partsNoOpt[i]));
-      }
-    }
-  }
-
-  private void parseReadHistoryLine(String line, String lineNoOpt,
-      String[] partsNoOpt) throws DescriptorParseException {
-    this.readHistory = new BandwidthHistoryImpl(line, lineNoOpt,
-        partsNoOpt);
-  }
-
-  private void parseWriteHistoryLine(String line, String lineNoOpt,
-      String[] partsNoOpt) throws DescriptorParseException {
-    this.writeHistory = new BandwidthHistoryImpl(line, lineNoOpt,
-        partsNoOpt);
-  }
-
-  private void parseEventdnsLine(String line, String lineNoOpt,
-      String[] partsNoOpt) throws DescriptorParseException {
-    if (partsNoOpt.length != 2) {
-      throw new DescriptorParseException("Illegal line '" + line + "'.");
-    }
-    if (partsNoOpt[1].equals("true")) {
-      this.usesEnhancedDnsLogic = true;
-    } else if (partsNoOpt[1].equals("false")) {
-      this.usesEnhancedDnsLogic = false;
-    } else {
-      throw new DescriptorParseException("Illegal line '" + line + "'.");
-    }
-  }
-
-  private void parseCachesExtraInfoLine(String line, String lineNoOpt,
-      String[] partsNoOpt) throws DescriptorParseException {
-    if (!lineNoOpt.equals("caches-extra-info")) {
-      throw new DescriptorParseException("Illegal line '" + line + "'.");
-    }
-    this.cachesExtraInfo = true;
-  }
-
-  private void parseExtraInfoDigestLine(String line, String lineNoOpt,
-      String[] partsNoOpt) throws DescriptorParseException {
-    if (partsNoOpt.length != 2) {
-      throw new DescriptorParseException("Illegal line '" + line + "'.");
-    }
-    this.extraInfoDigest = ParseHelper.parseTwentyByteHexString(line,
-        partsNoOpt[1]);
-  }
-
-  private void parseHiddenServiceDirLine(String line, String lineNoOpt,
-      String[] partsNoOpt) throws DescriptorParseException {
-    this.hiddenServiceDirVersions = new ArrayList<Integer>();
-    if (partsNoOpt.length == 1) {
-      this.hiddenServiceDirVersions.add(2);
-    } else {
-      try {
-        for (int i = 1; i < partsNoOpt.length; i++) {
-          this.hiddenServiceDirVersions.add(Integer.parseInt(
-              partsNoOpt[i]));
-        }
-      } catch (NumberFormatException e) {
-        throw new DescriptorParseException("Illegal value in line '"
-            + line + "'.");
-      }
-    }
-  }
-
-  private void parseProtocolsLine(String line, String lineNoOpt,
-      String[] partsNoOpt) throws DescriptorParseException {
-    List<String> partsList = Arrays.asList(partsNoOpt);
-    boolean isValid = true;
-    this.linkProtocolVersions = new ArrayList<Integer>();
-    this.circuitProtocolVersions = new ArrayList<Integer>();
-    List<Integer> protocolVersions = null;
-    for (int i = 1; i < partsNoOpt.length; i++) {
-      String part = partsNoOpt[i];
-      if (part.equals("Link")) {
-        protocolVersions = this.linkProtocolVersions;
-      } else if (part.equals("Circuit")) {
-        protocolVersions = this.circuitProtocolVersions;
-      } else if (protocolVersions == null) {
-        isValid = false;
-        break;
-      } else {
-        try {
-          protocolVersions.add(Integer.parseInt(part));
-        } catch (NumberFormatException e) {
-          isValid = false;
-          break;
-        }
-      }
-    }
-    if (protocolVersions != this.circuitProtocolVersions) {
-      isValid = false;
-    }
-    if (!isValid) {
-      throw new DescriptorParseException("Illegal line '" + line + "'.");
-    }
-  }
-
-  private void parseAllowSingleHopExitsLine(String line, String lineNoOpt,
-      String[] partsNoOpt) throws DescriptorParseException {
-    if (!lineNoOpt.equals("allow-single-hop-exits")) {
-      throw new DescriptorParseException("Illegal line '" + line + "'.");
-    }
-    this.allowSingleHopExits = true;
-  }
-
-  private String nickname;
-  public String getNickname() {
-    return this.nickname;
-  }
-
-  private String address;
-  public String getAddress() {
-    return this.address;
-  }
-
-  private int orPort;
-  public int getOrPort() {
-    return this.orPort;
-  }
-
-  private int socksPort;
-  public int getSocksPort() {
-    return this.socksPort;
-  }
-
-  private int dirPort;
-  public int getDirPort() {
-    return this.dirPort;
-  }
-
-  private int bandwidthRate;
-  public int getBandwidthRate() {
-    return this.bandwidthRate;
-  }
-
-  private int bandwidthBurst;
-  public int getBandwidthBurst() {
-    return this.bandwidthBurst;
-  }
-
-  private int bandwidthObserved;
-  public int getBandwidthObserved() {
-    return this.bandwidthObserved;
-  }
-
-  private String platform;
-  public String getPlatform() {
-    return this.platform;
-  }
-
-  private long publishedMillis;
-  public long getPublishedMillis() {
-    return this.publishedMillis;
-  }
-
-  private String fingerprint;
-  public String getFingerprint() {
-    return this.fingerprint;
-  }
-
-  private boolean hibernating;
-  public boolean isHibernating() {
-    return this.hibernating;
-  }
-
-  private int uptime = -1;
-  public int getUptime() {
-    return this.uptime;
-  }
-
-  private List<String> exitPolicyLines = new ArrayList<String>();
-  public List<String> getExitPolicyLines() {
-    return new ArrayList<String>(this.exitPolicyLines);
-  }
-
-  private String contact;
-  public String getContact() {
-    return this.contact;
-  }
-
-  private List<String> familyEntries;
-  public List<String> getFamilyEntries() {
-    return this.familyEntries == null ? null :
-        new ArrayList<String>(this.familyEntries);
-  }
-
-  private BandwidthHistory readHistory;
-  public BandwidthHistory getReadHistory() {
-    return this.readHistory;
-  }
-
-  private BandwidthHistory writeHistory;
-  public BandwidthHistory getWriteHistory() {
-    return this.writeHistory;
-  }
-
-  private boolean usesEnhancedDnsLogic;
-  public boolean getUsesEnhancedDnsLogic() {
-    return this.usesEnhancedDnsLogic;
-  }
-
-  private boolean cachesExtraInfo;
-  public boolean getCachesExtraInfo() {
-    return this.cachesExtraInfo;
-  }
-
-  private String extraInfoDigest;
-  public String getExtraInfoDigest() {
-    return this.extraInfoDigest;
-  }
-
-  private List<Integer> hiddenServiceDirVersions;
-  public List<Integer> getHiddenServiceDirVersions() {
-    return this.hiddenServiceDirVersions == null ? null :
-        new ArrayList<Integer>(this.hiddenServiceDirVersions);
-  }
-
-  private List<Integer> linkProtocolVersions;
-  public List<Integer> getLinkProtocolVersions() {
-    return this.linkProtocolVersions == null ? null :
-        new ArrayList<Integer>(this.linkProtocolVersions);
-  }
-
-  private List<Integer> circuitProtocolVersions;
-  public List<Integer> getCircuitProtocolVersions() {
-    return this.circuitProtocolVersions == null ? null :
-        new ArrayList<Integer>(this.circuitProtocolVersions);
-  }
-
-  private boolean allowSingleHopExits;
-  public boolean getAllowSingleHopExits() {
-    return this.allowSingleHopExits;
-  }
-}
-
diff --git a/src/org/torproject/descriptor/impl/ServerDescriptorImpl.java b/src/org/torproject/descriptor/impl/ServerDescriptorImpl.java
new file mode 100644
index 0000000..878955e
--- /dev/null
+++ b/src/org/torproject/descriptor/impl/ServerDescriptorImpl.java
@@ -0,0 +1,532 @@
+/* Copyright 2012 The Tor Project
+ * See LICENSE for licensing information */
+package org.torproject.descriptor.impl;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.StringReader;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import org.torproject.descriptor.ServerDescriptor;
+import org.torproject.descriptor.BandwidthHistory;
+
+/* Contains a relay server descriptor. */
+public class ServerDescriptorImpl extends DescriptorImpl
+    implements ServerDescriptor {
+
+  protected static List<ServerDescriptor> parseDescriptors(
+      byte[] descriptorsBytes) {
+    List<ServerDescriptor> parsedDescriptors =
+        new ArrayList<ServerDescriptor>();
+    List<byte[]> splitDescriptorsBytes =
+        DescriptorImpl.splitRawDescriptorBytes(descriptorsBytes,
+        "router ");
+    try {
+      for (byte[] descriptorBytes : splitDescriptorsBytes) {
+        ServerDescriptor parsedDescriptor =
+            new ServerDescriptorImpl(descriptorBytes);
+        parsedDescriptors.add(parsedDescriptor);
+      }
+    } catch (DescriptorParseException e) {
+      /* TODO Handle this error somehow. */
+      System.err.println("Failed to parse descriptor.  Skipping.");
+      e.printStackTrace();
+    }
+    return parsedDescriptors;
+  }
+
+  protected ServerDescriptorImpl(byte[] descriptorBytes)
+      throws DescriptorParseException {
+    super(descriptorBytes);
+    this.parseDescriptorBytes();
+    Set<String> exactlyOnceKeywords = new HashSet<String>(Arrays.asList((
+        "router,bandwidth,published,onion-key,signing-key,"
+        + "router-signature").split(",")));
+    this.checkExactlyOnceKeywords(exactlyOnceKeywords);
+    Set<String> atMostOnceKeywords = new HashSet<String>(Arrays.asList((
+        "platform,fingerprint,hibernating,uptime,contact,family,"
+        + "read-history,write-history,eventdns,caches-extra-info,"
+        + "extra-info-digest,hidden-service-dir,protocols,"
+        + "allow-single-hop-exits").split(",")));
+    this.checkAtMostOnceKeywords(atMostOnceKeywords);
+    this.checkFirstKeyword("router");
+    this.checkLastKeyword("router-signature");
+    if (this.getKeywordCount("accept") == 0 &&
+        this.getKeywordCount("reject") == 0) {
+      throw new DescriptorParseException("Either keyword 'accept' or "
+          + "'reject' must be contained at least once.");
+    }
+    return;
+  }
+
+  private void parseDescriptorBytes() throws DescriptorParseException {
+    try {
+      BufferedReader br = new BufferedReader(new StringReader(
+          new String(this.rawDescriptorBytes)));
+      String line;
+      boolean skipCrypto = false;
+      while ((line = br.readLine()) != null) {
+        if (line.startsWith("@")) {
+          continue;
+        }
+        String lineNoOpt = line.startsWith("opt ") ?
+            line.substring("opt ".length()) : line;
+        String[] partsNoOpt = lineNoOpt.split(" ");
+        String keyword = partsNoOpt[0];
+        if (keyword.equals("router")) {
+          this.parseRouterLine(line, lineNoOpt, partsNoOpt);
+        } else if (keyword.equals("bandwidth")) {
+          this.parseBandwidthLine(line, lineNoOpt, partsNoOpt);
+        } else if (keyword.equals("platform")) {
+          this.parsePlatformLine(line, lineNoOpt, partsNoOpt);
+        } else if (keyword.equals("published")) {
+          this.parsePublishedLine(line, lineNoOpt, partsNoOpt);
+        } else if (keyword.equals("fingerprint")) {
+          this.parseFingerprintLine(line, lineNoOpt, partsNoOpt);
+        } else if (keyword.equals("hibernating")) {
+          this.parseHibernatingLine(line, lineNoOpt, partsNoOpt);
+        } else if (keyword.equals("uptime")) {
+          this.parseUptimeLine(line, lineNoOpt, partsNoOpt);
+        } else if (keyword.equals("onion-key")) {
+          this.parseOnionKeyLine(line, lineNoOpt, partsNoOpt);
+        } else if (keyword.equals("signing-key")) {
+          this.parseSigningKeyLine(line, lineNoOpt, partsNoOpt);
+        } else if (keyword.equals("accept")) {
+          this.parseAcceptLine(line, lineNoOpt, partsNoOpt);
+        } else if (keyword.equals("reject")) {
+          this.parseRejectLine(line, lineNoOpt, partsNoOpt);
+        } else if (keyword.equals("router-signature")) {
+          this.parseRouterSignatureLine(line, lineNoOpt, partsNoOpt);
+        } else if (keyword.equals("contact")) {
+          this.parseContactLine(line, lineNoOpt, partsNoOpt);
+        } else if (keyword.equals("family")) {
+          this.parseFamilyLine(line, lineNoOpt, partsNoOpt);
+        } else if (keyword.equals("read-history")) {
+          this.parseReadHistoryLine(line, lineNoOpt, partsNoOpt);
+        } else if (keyword.equals("write-history")) {
+          this.parseWriteHistoryLine(line, lineNoOpt, partsNoOpt);
+        } else if (keyword.equals("eventdns")) {
+          this.parseEventdnsLine(line, lineNoOpt, partsNoOpt);
+        } else if (keyword.equals("caches-extra-info")) {
+          this.parseCachesExtraInfoLine(line, lineNoOpt, partsNoOpt);
+        } else if (keyword.equals("extra-info-digest")) {
+          this.parseExtraInfoDigestLine(line, lineNoOpt, partsNoOpt);
+        } else if (keyword.equals("hidden-service-dir")) {
+          this.parseHiddenServiceDirLine(line, lineNoOpt, partsNoOpt);
+        } else if (keyword.equals("protocols")) {
+          this.parseProtocolsLine(line, lineNoOpt, partsNoOpt);
+        } else if (keyword.equals("allow-single-hop-exits")) {
+          this.parseAllowSingleHopExitsLine(line, lineNoOpt, partsNoOpt);
+        } else if (line.startsWith("-----BEGIN")) {
+          skipCrypto = true;
+        } else if (line.startsWith("-----END")) {
+          skipCrypto = false;
+        } else if (!skipCrypto) {
+          /* 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.  In theory,
+           * dir-spec.txt says that unknown lines should be ignored.  This
+           * also applies to the other descriptors. */
+          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);
+    }
+  }
+
+  private void parseRouterLine(String line, String lineNoOpt,
+      String[] partsNoOpt) throws DescriptorParseException {
+    if (partsNoOpt.length != 6) {
+      throw new DescriptorParseException("Illegal line '" + line
+          + "' in server descriptor.");
+    }
+    this.nickname = ParseHelper.parseNickname(line, partsNoOpt[1]);
+    this.address = ParseHelper.parseIpv4Address(line, partsNoOpt[2]);
+    this.orPort = ParseHelper.parsePort(line, partsNoOpt[3]);
+    this.socksPort = ParseHelper.parsePort(line, partsNoOpt[4]);
+    this.dirPort = ParseHelper.parsePort(line, partsNoOpt[5]);
+  }
+
+  private void parseBandwidthLine(String line, String lineNoOpt,
+      String[] partsNoOpt) throws DescriptorParseException {
+    if (partsNoOpt.length != 4) {
+      throw new DescriptorParseException("Wrong number of values in line "
+          + "'" + line + "'.");
+    }
+    boolean isValid = false;
+    try {
+      this.bandwidthRate = Integer.parseInt(partsNoOpt[1]);
+      this.bandwidthBurst = Integer.parseInt(partsNoOpt[2]);
+      this.bandwidthObserved = Integer.parseInt(partsNoOpt[3]);
+      if (this.bandwidthRate >= 0 && this.bandwidthBurst >= 0 &&
+          this.bandwidthObserved >= 0) {
+        isValid = true;
+      }
+    } catch (NumberFormatException e) {
+      /* Handle below. */
+    }
+    if (!isValid) {
+      throw new DescriptorParseException("Illegal values in line '" + line
+          + "'.");
+    }
+  }
+
+  private void parsePlatformLine(String line, String lineNoOpt,
+      String[] partsNoOpt) throws DescriptorParseException {
+    if (lineNoOpt.length() > "platform ".length()) {
+      this.platform = lineNoOpt.substring("platform ".length());
+    } else {
+      this.platform = "";
+    }
+  }
+
+  private void parsePublishedLine(String line, String lineNoOpt,
+      String[] partsNoOpt) throws DescriptorParseException {
+    this.publishedMillis = ParseHelper.parseTimestampAtIndex(line,
+        partsNoOpt, 1, 2);
+  }
+
+  private void parseFingerprintLine(String line, String lineNoOpt,
+      String[] partsNoOpt) throws DescriptorParseException {
+    if (lineNoOpt.length() != "fingerprint".length() + 5 * 10) {
+      throw new DescriptorParseException("Illegal line '" + line + "'.");
+    }
+    this.fingerprint = ParseHelper.parseTwentyByteHexString(line,
+        lineNoOpt.substring("fingerprint ".length()).replaceAll(" ", ""));
+  }
+
+  private void parseHibernatingLine(String line, String lineNoOpt,
+      String[] partsNoOpt) throws DescriptorParseException {
+    if (partsNoOpt.length != 2) {
+      throw new DescriptorParseException("Illegal line '" + line + "'.");
+    }
+    if (partsNoOpt[1].equals("1")) {
+      this.hibernating = true;
+    } else if (partsNoOpt[1].equals("0")) {
+      this.hibernating = false;
+    } else {
+      throw new DescriptorParseException("Illegal line '" + line + "'.");
+    }
+  }
+
+  private void parseUptimeLine(String line, String lineNoOpt,
+      String[] partsNoOpt) throws DescriptorParseException {
+    if (partsNoOpt.length != 2) {
+      throw new DescriptorParseException("Wrong number of values in line "
+          + "'" + line + "'.");
+    }
+    boolean isValid = false;
+    try {
+      this.uptime = Integer.parseInt(partsNoOpt[1]);
+      if (this.uptime >= 0) {
+        isValid = true;
+      }
+    } catch (NumberFormatException e) {
+      /* Handle below. */
+    }
+    if (!isValid) {
+      throw new DescriptorParseException("Illegal value in line '" + line
+          + "'.");
+    }
+  }
+
+  private void parseOnionKeyLine(String line, String lineNoOpt,
+      String[] partsNoOpt) throws DescriptorParseException {
+    /* Not parsing crypto parts (yet). */
+  }
+
+  private void parseSigningKeyLine(String line, String lineNoOpt,
+      String[] partsNoOpt) throws DescriptorParseException {
+    /* Not parsing crypto parts (yet). */
+  }
+
+  private void parseAcceptLine(String line, String lineNoOpt,
+      String[] partsNoOpt) throws DescriptorParseException {
+    this.parseExitPolicyLine(line, lineNoOpt, partsNoOpt);
+  }
+
+  private void parseRejectLine(String line, String lineNoOpt,
+      String[] partsNoOpt) throws DescriptorParseException {
+    this.parseExitPolicyLine(line, lineNoOpt, partsNoOpt);
+  }
+
+  private void parseExitPolicyLine(String line, String lineNoOpt,
+      String[] partsNoOpt) throws DescriptorParseException {
+    if (partsNoOpt.length != 2) {
+      throw new DescriptorParseException("Illegal line '" + line + "'.");
+    }
+    ParseHelper.parseExitPattern(line, partsNoOpt[1]);
+    this.exitPolicyLines.add(lineNoOpt);
+  }
+
+  private void parseRouterSignatureLine(String line, String lineNoOpt,
+      String[] partsNoOpt) throws DescriptorParseException {
+    if (!lineNoOpt.equals("router-signature")) {
+      throw new DescriptorParseException("Illegal line '" + line + "'.");
+    }
+    /* Not parsing crypto parts (yet). */
+  }
+
+  private void parseContactLine(String line, String lineNoOpt,
+      String[] partsNoOpt) throws DescriptorParseException {
+    if (lineNoOpt.length() > "contact ".length()) {
+      this.contact = lineNoOpt.substring("contact ".length());
+    } else {
+      this.contact = "";
+    }
+  }
+
+  private void parseFamilyLine(String line, String lineNoOpt,
+      String[] partsNoOpt) throws DescriptorParseException {
+    this.familyEntries = new ArrayList<String>();
+    for (int i = 1; i < partsNoOpt.length; i++) {
+      if (partsNoOpt[i].startsWith("$")) {
+        this.familyEntries.add("$"
+            + ParseHelper.parseTwentyByteHexString(line,
+            partsNoOpt[i].substring(1)));
+      } else {
+        this.familyEntries.add(ParseHelper.parseNickname(line,
+            partsNoOpt[i]));
+      }
+    }
+  }
+
+  private void parseReadHistoryLine(String line, String lineNoOpt,
+      String[] partsNoOpt) throws DescriptorParseException {
+    this.readHistory = new BandwidthHistoryImpl(line, lineNoOpt,
+        partsNoOpt);
+  }
+
+  private void parseWriteHistoryLine(String line, String lineNoOpt,
+      String[] partsNoOpt) throws DescriptorParseException {
+    this.writeHistory = new BandwidthHistoryImpl(line, lineNoOpt,
+        partsNoOpt);
+  }
+
+  private void parseEventdnsLine(String line, String lineNoOpt,
+      String[] partsNoOpt) throws DescriptorParseException {
+    if (partsNoOpt.length != 2) {
+      throw new DescriptorParseException("Illegal line '" + line + "'.");
+    }
+    if (partsNoOpt[1].equals("true")) {
+      this.usesEnhancedDnsLogic = true;
+    } else if (partsNoOpt[1].equals("false")) {
+      this.usesEnhancedDnsLogic = false;
+    } else {
+      throw new DescriptorParseException("Illegal line '" + line + "'.");
+    }
+  }
+
+  private void parseCachesExtraInfoLine(String line, String lineNoOpt,
+      String[] partsNoOpt) throws DescriptorParseException {
+    if (!lineNoOpt.equals("caches-extra-info")) {
+      throw new DescriptorParseException("Illegal line '" + line + "'.");
+    }
+    this.cachesExtraInfo = true;
+  }
+
+  private void parseExtraInfoDigestLine(String line, String lineNoOpt,
+      String[] partsNoOpt) throws DescriptorParseException {
+    if (partsNoOpt.length != 2) {
+      throw new DescriptorParseException("Illegal line '" + line + "'.");
+    }
+    this.extraInfoDigest = ParseHelper.parseTwentyByteHexString(line,
+        partsNoOpt[1]);
+  }
+
+  private void parseHiddenServiceDirLine(String line, String lineNoOpt,
+      String[] partsNoOpt) throws DescriptorParseException {
+    this.hiddenServiceDirVersions = new ArrayList<Integer>();
+    if (partsNoOpt.length == 1) {
+      this.hiddenServiceDirVersions.add(2);
+    } else {
+      try {
+        for (int i = 1; i < partsNoOpt.length; i++) {
+          this.hiddenServiceDirVersions.add(Integer.parseInt(
+              partsNoOpt[i]));
+        }
+      } catch (NumberFormatException e) {
+        throw new DescriptorParseException("Illegal value in line '"
+            + line + "'.");
+      }
+    }
+  }
+
+  private void parseProtocolsLine(String line, String lineNoOpt,
+      String[] partsNoOpt) throws DescriptorParseException {
+    List<String> partsList = Arrays.asList(partsNoOpt);
+    boolean isValid = true;
+    this.linkProtocolVersions = new ArrayList<Integer>();
+    this.circuitProtocolVersions = new ArrayList<Integer>();
+    List<Integer> protocolVersions = null;
+    for (int i = 1; i < partsNoOpt.length; i++) {
+      String part = partsNoOpt[i];
+      if (part.equals("Link")) {
+        protocolVersions = this.linkProtocolVersions;
+      } else if (part.equals("Circuit")) {
+        protocolVersions = this.circuitProtocolVersions;
+      } else if (protocolVersions == null) {
+        isValid = false;
+        break;
+      } else {
+        try {
+          protocolVersions.add(Integer.parseInt(part));
+        } catch (NumberFormatException e) {
+          isValid = false;
+          break;
+        }
+      }
+    }
+    if (protocolVersions != this.circuitProtocolVersions) {
+      isValid = false;
+    }
+    if (!isValid) {
+      throw new DescriptorParseException("Illegal line '" + line + "'.");
+    }
+  }
+
+  private void parseAllowSingleHopExitsLine(String line, String lineNoOpt,
+      String[] partsNoOpt) throws DescriptorParseException {
+    if (!lineNoOpt.equals("allow-single-hop-exits")) {
+      throw new DescriptorParseException("Illegal line '" + line + "'.");
+    }
+    this.allowSingleHopExits = true;
+  }
+
+  private String nickname;
+  public String getNickname() {
+    return this.nickname;
+  }
+
+  private String address;
+  public String getAddress() {
+    return this.address;
+  }
+
+  private int orPort;
+  public int getOrPort() {
+    return this.orPort;
+  }
+
+  private int socksPort;
+  public int getSocksPort() {
+    return this.socksPort;
+  }
+
+  private int dirPort;
+  public int getDirPort() {
+    return this.dirPort;
+  }
+
+  private int bandwidthRate;
+  public int getBandwidthRate() {
+    return this.bandwidthRate;
+  }
+
+  private int bandwidthBurst;
+  public int getBandwidthBurst() {
+    return this.bandwidthBurst;
+  }
+
+  private int bandwidthObserved;
+  public int getBandwidthObserved() {
+    return this.bandwidthObserved;
+  }
+
+  private String platform;
+  public String getPlatform() {
+    return this.platform;
+  }
+
+  private long publishedMillis;
+  public long getPublishedMillis() {
+    return this.publishedMillis;
+  }
+
+  private String fingerprint;
+  public String getFingerprint() {
+    return this.fingerprint;
+  }
+
+  private boolean hibernating;
+  public boolean isHibernating() {
+    return this.hibernating;
+  }
+
+  private int uptime = -1;
+  public int getUptime() {
+    return this.uptime;
+  }
+
+  private List<String> exitPolicyLines = new ArrayList<String>();
+  public List<String> getExitPolicyLines() {
+    return new ArrayList<String>(this.exitPolicyLines);
+  }
+
+  private String contact;
+  public String getContact() {
+    return this.contact;
+  }
+
+  private List<String> familyEntries;
+  public List<String> getFamilyEntries() {
+    return this.familyEntries == null ? null :
+        new ArrayList<String>(this.familyEntries);
+  }
+
+  private BandwidthHistory readHistory;
+  public BandwidthHistory getReadHistory() {
+    return this.readHistory;
+  }
+
+  private BandwidthHistory writeHistory;
+  public BandwidthHistory getWriteHistory() {
+    return this.writeHistory;
+  }
+
+  private boolean usesEnhancedDnsLogic;
+  public boolean getUsesEnhancedDnsLogic() {
+    return this.usesEnhancedDnsLogic;
+  }
+
+  private boolean cachesExtraInfo;
+  public boolean getCachesExtraInfo() {
+    return this.cachesExtraInfo;
+  }
+
+  private String extraInfoDigest;
+  public String getExtraInfoDigest() {
+    return this.extraInfoDigest;
+  }
+
+  private List<Integer> hiddenServiceDirVersions;
+  public List<Integer> getHiddenServiceDirVersions() {
+    return this.hiddenServiceDirVersions == null ? null :
+        new ArrayList<Integer>(this.hiddenServiceDirVersions);
+  }
+
+  private List<Integer> linkProtocolVersions;
+  public List<Integer> getLinkProtocolVersions() {
+    return this.linkProtocolVersions == null ? null :
+        new ArrayList<Integer>(this.linkProtocolVersions);
+  }
+
+  private List<Integer> circuitProtocolVersions;
+  public List<Integer> getCircuitProtocolVersions() {
+    return this.circuitProtocolVersions == null ? null :
+        new ArrayList<Integer>(this.circuitProtocolVersions);
+  }
+
+  private boolean allowSingleHopExits;
+  public boolean getAllowSingleHopExits() {
+    return this.allowSingleHopExits;
+  }
+}
+
diff --git a/test/org/torproject/descriptor/impl/RelayServerDescriptorImplTest.java b/test/org/torproject/descriptor/impl/RelayServerDescriptorImplTest.java
deleted file mode 100644
index f806502..0000000
--- a/test/org/torproject/descriptor/impl/RelayServerDescriptorImplTest.java
+++ /dev/null
@@ -1,1080 +0,0 @@
-/* Copyright 2012 The Tor Project
- * See LICENSE for licensing information */
-package org.torproject.descriptor.impl;
-
-import org.torproject.descriptor.BandwidthHistory;
-import org.torproject.descriptor.RelayServerDescriptor;
-
-import java.util.*;
-
-import org.junit.*;
-import org.junit.rules.*;
-import static org.junit.Assert.*;
-
-/* Test parsing of relay server descriptors. */
-public class RelayServerDescriptorImplTest {
-
-  /* Helper class to build a descriptor based on default data and
-   * modifications requested by test methods. */
-  private static class DescriptorBuilder {
-    private String routerLine = "router saberrider2008 94.134.192.243 "
-        + "9001 0 0";
-    private static RelayServerDescriptor createWithRouterLine(
-        String line) throws DescriptorParseException {
-      DescriptorBuilder db = new DescriptorBuilder();
-      db.routerLine = line;
-      return new RelayServerDescriptorImpl(db.buildDescriptor());
-    }
-    private String bandwidthLine = "bandwidth 51200 51200 53470";
-    private static RelayServerDescriptor createWithBandwidthLine(
-        String line) throws DescriptorParseException {
-      DescriptorBuilder db = new DescriptorBuilder();
-      db.bandwidthLine = line;
-      return new RelayServerDescriptorImpl(db.buildDescriptor());
-    }
-    private String platformLine = "platform Tor 0.2.2.35 "
-        + "(git-b04388f9e7546a9f) on Linux i686";
-    private static RelayServerDescriptor createWithPlatformLine(
-        String line) throws DescriptorParseException {
-      DescriptorBuilder db = new DescriptorBuilder();
-      db.platformLine = line;
-      return new RelayServerDescriptorImpl(db.buildDescriptor());
-    }
-    private String publishedLine = "published 2012-01-01 04:03:19";
-    private static RelayServerDescriptor createWithPublishedLine(
-        String line) throws DescriptorParseException {
-      DescriptorBuilder db = new DescriptorBuilder();
-      db.publishedLine = line;
-      return new RelayServerDescriptorImpl(db.buildDescriptor());
-    }
-    private String fingerprintLine = "opt fingerprint D873 3048 FC8E "
-        + "C910 2466 AD8F 3098 622B F1BF 71FD";
-    private static RelayServerDescriptor createWithFingerprintLine(
-        String line) throws DescriptorParseException {
-      DescriptorBuilder db = new DescriptorBuilder();
-      db.fingerprintLine = line;
-      return new RelayServerDescriptorImpl(db.buildDescriptor());
-    }
-    private String hibernatingLine = null;
-    private static RelayServerDescriptor createWithHibernatingLine(
-        String line) throws DescriptorParseException {
-      DescriptorBuilder db = new DescriptorBuilder();
-      db.hibernatingLine = line;
-      return new RelayServerDescriptorImpl(db.buildDescriptor());
-    }
-    private String uptimeLine = "uptime 48";
-    private static RelayServerDescriptor createWithUptimeLine(
-        String line) throws DescriptorParseException {
-      DescriptorBuilder db = new DescriptorBuilder();
-      db.uptimeLine = line;
-      return new RelayServerDescriptorImpl(db.buildDescriptor());
-    }
-    private String onionKeyLines = "onion-key\n"
-        + "-----BEGIN RSA PUBLIC KEY-----\n"
-        + "MIGJAoGBAKM+iiHhO6eHsvd6Xjws9z9EQB1V/Bpuy5ciGJ1U4V9SeiKooSo5Bp"
-        + "PL\no3XT+6PIgzl3R6uycjS3Ejk47vLEJdcVTm/VG6E0ppu3olIynCI4QryfCE"
-        + "uC3cTF\n9wE4WXY4nX7w0RTN18UVLxrt1A9PP0cobFNiPs9rzJCbKFfacOkpAg"
-        + "MBAAE=\n"
-        + "-----END RSA PUBLIC KEY-----";
-    private static RelayServerDescriptor createWithOnionKeyLines(
-        String lines) throws DescriptorParseException {
-      DescriptorBuilder db = new DescriptorBuilder();
-      db.onionKeyLines = lines;
-      return new RelayServerDescriptorImpl(db.buildDescriptor());
-    }
-    private String signingKeyLines = "signing-key\n"
-        + "-----BEGIN RSA PUBLIC KEY-----\n"
-        + "MIGJAoGBALMm3r3QDh482Ewe6Ub9wvRIfmEkoNX6q5cEAtQRNHSDcNx41gjELb"
-        + "cl\nEniVMParBYACKfOxkS+mTTnIRDKVNEJTsDOwryNrc4X9JnPc/nn6ymYPiN"
-        + "DhUROG\n8URDIhQoixcUeyyrVB8sxliSstKimulGnB7xpjYOlO8JKaHLNL4TAg"
-        + "MBAAE=\n"
-        + "-----END RSA PUBLIC KEY-----";
-    private static RelayServerDescriptor createWithSigningKeyLines(
-        String lines) throws DescriptorParseException {
-      DescriptorBuilder db = new DescriptorBuilder();
-      db.signingKeyLines = lines;
-      return new RelayServerDescriptorImpl(db.buildDescriptor());
-    }
-    private String exitPolicyLines = "reject *:*";
-    private static RelayServerDescriptor createWithExitPolicyLines(
-        String lines) throws DescriptorParseException {
-      DescriptorBuilder db = new DescriptorBuilder();
-      db.exitPolicyLines = lines;
-      return new RelayServerDescriptorImpl(db.buildDescriptor());
-    }
-    private String contactLine = "contact Random Person <nobody AT "
-        + "example dot com>";
-    private static RelayServerDescriptor createWithContactLine(
-        String line) throws DescriptorParseException {
-      DescriptorBuilder db = new DescriptorBuilder();
-      db.contactLine = line;
-      return new RelayServerDescriptorImpl(db.buildDescriptor());
-    }
-    private String familyLine = null;
-    private static RelayServerDescriptor createWithFamilyLine(
-        String line) throws DescriptorParseException {
-      DescriptorBuilder db = new DescriptorBuilder();
-      db.familyLine = line;
-      return new RelayServerDescriptorImpl(db.buildDescriptor());
-    }
-    private String readHistoryLine = null;
-    private static RelayServerDescriptor createWithReadHistoryLine(
-        String line) throws DescriptorParseException {
-      DescriptorBuilder db = new DescriptorBuilder();
-      db.readHistoryLine = line;
-      return new RelayServerDescriptorImpl(db.buildDescriptor());
-    }
-    private String writeHistoryLine = null;
-    private static RelayServerDescriptor createWithWriteHistoryLine(
-        String line) throws DescriptorParseException {
-      DescriptorBuilder db = new DescriptorBuilder();
-      db.writeHistoryLine = line;
-      return new RelayServerDescriptorImpl(db.buildDescriptor());
-    }
-    private String eventdnsLine = null;
-    private static RelayServerDescriptor createWithEventdnsLine(
-        String line) throws DescriptorParseException {
-      DescriptorBuilder db = new DescriptorBuilder();
-      db.eventdnsLine = line;
-      return new RelayServerDescriptorImpl(db.buildDescriptor());
-    }
-    private String cachesExtraInfoLine = null;
-    private static RelayServerDescriptor createWithCachesExtraInfoLine(
-        String line) throws DescriptorParseException {
-      DescriptorBuilder db = new DescriptorBuilder();
-      db.cachesExtraInfoLine = line;
-      return new RelayServerDescriptorImpl(db.buildDescriptor());
-    }
-    private String extraInfoDigestLine = "opt extra-info-digest "
-        + "1469D1550738A25B1E7B47CDDBCD7B2899F51B74";
-    private static RelayServerDescriptor createWithExtraInfoDigestLine(
-        String line) throws DescriptorParseException {
-      DescriptorBuilder db = new DescriptorBuilder();
-      db.extraInfoDigestLine = line;
-      return new RelayServerDescriptorImpl(db.buildDescriptor());
-    }
-    private String hiddenServiceDirLine = "opt hidden-service-dir";
-    private static RelayServerDescriptor createWithHiddenServiceDirLine(
-        String line) throws DescriptorParseException {
-      DescriptorBuilder db = new DescriptorBuilder();
-      db.hiddenServiceDirLine = line;
-      return new RelayServerDescriptorImpl(db.buildDescriptor());
-    }
-    private String protocolsLine = "opt protocols Link 1 2 Circuit 1";
-    private static RelayServerDescriptor createWithProtocolsLine(
-        String line) throws DescriptorParseException {
-      DescriptorBuilder db = new DescriptorBuilder();
-      db.protocolsLine = line;
-      return new RelayServerDescriptorImpl(db.buildDescriptor());
-    }
-    private String allowSingleHopExitsLine = null;
-    private static RelayServerDescriptor
-        createWithAllowSingleHopExitsLine(String line)
-        throws DescriptorParseException {
-      DescriptorBuilder db = new DescriptorBuilder();
-      db.allowSingleHopExitsLine = line;
-      return new RelayServerDescriptorImpl(db.buildDescriptor());
-    }
-    private String routerSignatureLines = "router-signature\n"
-        + "-----BEGIN SIGNATURE-----\n"
-        + "o4j+kH8UQfjBwepUnr99v0ebN8RpzHJ/lqYsTojXHy9kMr1RNI9IDeSzA7PSqT"
-        + "uV\n4PL8QsGtlfwthtIoZpB2srZeyN/mcpA9fa1JXUrt/UN9K/+32Cyaad7h0n"
-        + "HE6Xfb\njqpXDpnBpvk4zjmzjjKYnIsUWTnADmu0fo3xTRqXi7g=\n"
-        + "-----END SIGNATURE-----";
-    private static RelayServerDescriptor createWithRouterSignatureLines(
-        String line) throws DescriptorParseException {
-      DescriptorBuilder db = new DescriptorBuilder();
-      db.routerSignatureLines = line;
-      return new RelayServerDescriptorImpl(db.buildDescriptor());
-    }
-    private byte[] buildDescriptor() {
-      StringBuilder sb = new StringBuilder();
-      if (this.routerLine != null) {
-        sb.append(this.routerLine + "\n");
-      }
-      if (this.bandwidthLine != null) {
-        sb.append(this.bandwidthLine + "\n");
-      }
-      if (this.platformLine != null) {
-        sb.append(this.platformLine + "\n");
-      }
-      if (this.publishedLine != null) {
-        sb.append(this.publishedLine + "\n");
-      }
-      if (this.fingerprintLine != null) {
-        sb.append(this.fingerprintLine + "\n");
-      }
-      if (this.hibernatingLine != null) {
-        sb.append(this.hibernatingLine + "\n");
-      }
-      if (this.uptimeLine != null) {
-        sb.append(this.uptimeLine + "\n");
-      }
-      if (this.onionKeyLines != null) {
-        sb.append(this.onionKeyLines + "\n");
-      }
-      if (this.signingKeyLines != null) {
-        sb.append(this.signingKeyLines + "\n");
-      }
-      if (this.exitPolicyLines != null) {
-        sb.append(this.exitPolicyLines + "\n");
-      }
-      if (this.contactLine != null) {
-        sb.append(this.contactLine + "\n");
-      }
-      if (this.familyLine != null) {
-        sb.append(this.familyLine + "\n");
-      }
-      if (this.readHistoryLine != null) {
-        sb.append(this.readHistoryLine + "\n");
-      }
-      if (this.writeHistoryLine != null) {
-        sb.append(this.writeHistoryLine + "\n");
-      }
-      if (this.eventdnsLine != null) {
-        sb.append(this.eventdnsLine + "\n");
-      }
-      if (this.cachesExtraInfoLine != null) {
-        sb.append(this.cachesExtraInfoLine + "\n");
-      }
-      if (this.extraInfoDigestLine != null) {
-        sb.append(this.extraInfoDigestLine + "\n");
-      }
-      if (this.hiddenServiceDirLine != null) {
-        sb.append(this.hiddenServiceDirLine + "\n");
-      }
-      if (this.protocolsLine != null) {
-        sb.append(this.protocolsLine + "\n");
-      }
-      if (this.allowSingleHopExitsLine != null) {
-        sb.append(this.allowSingleHopExitsLine + "\n");
-      }
-      if (this.routerSignatureLines != null) {
-        sb.append(this.routerSignatureLines + "\n");
-      }
-      return sb.toString().getBytes();
-    }
-  }
-
-  @Test()
-  public void testSampleDescriptor() throws DescriptorParseException {
-    DescriptorBuilder db = new DescriptorBuilder();
-    RelayServerDescriptor descriptor =
-        new RelayServerDescriptorImpl(db.buildDescriptor());
-    assertEquals("saberrider2008", descriptor.getNickname());
-    assertEquals("94.134.192.243", descriptor.getAddress());
-    assertEquals(9001, (int) descriptor.getOrPort());
-    assertEquals(0, (int) descriptor.getSocksPort());
-    assertEquals(0, (int) descriptor.getDirPort());
-    assertEquals("Tor 0.2.2.35 (git-b04388f9e7546a9f) on Linux i686",
-        descriptor.getPlatform());
-    assertEquals(Arrays.asList(new Integer[] {1, 2}),
-        descriptor.getLinkProtocolVersions());
-    assertEquals(Arrays.asList(new Integer[] {1}),
-        descriptor.getCircuitProtocolVersions());
-    assertEquals(1325390599000L, descriptor.getPublishedMillis());
-    assertEquals("D8733048FC8EC9102466AD8F3098622BF1BF71FD",
-        descriptor.getFingerprint());
-    assertEquals(48, (int) descriptor.getUptime());
-    assertEquals(51200, (int) descriptor.getBandwidthRate());
-    assertEquals(51200, (int) descriptor.getBandwidthBurst());
-    assertEquals(53470, (int) descriptor.getBandwidthObserved());
-    assertEquals("1469D1550738A25B1E7B47CDDBCD7B2899F51B74",
-        descriptor.getExtraInfoDigest());
-    assertEquals(Arrays.asList(new Integer[] {2}),
-        descriptor.getHiddenServiceDirVersions());
-    assertEquals("Random Person <nobody AT example dot com>",
-        descriptor.getContact());
-    assertEquals(Arrays.asList(new String[] {"reject *:*"}),
-        descriptor.getExitPolicyLines());
-    assertFalse(descriptor.isHibernating());
-    assertNull(descriptor.getFamilyEntries());
-    assertNull(descriptor.getReadHistory());
-    assertNull(descriptor.getWriteHistory());
-    assertFalse(descriptor.getUsesEnhancedDnsLogic());
-    assertFalse(descriptor.getCachesExtraInfo());
-    assertFalse(descriptor.getAllowSingleHopExits());
-  }
-
-  @Test(expected = DescriptorParseException.class)
-  public void testRouterLineMissing() throws DescriptorParseException {
-    DescriptorBuilder.createWithRouterLine(null);
-  }
-
-  @Test()
-  public void testRouterOpt() throws DescriptorParseException {
-    RelayServerDescriptor descriptor = DescriptorBuilder.
-        createWithRouterLine("opt router saberrider2008 "
-        + "94.134.192.243 9001 0 0");
-    assertEquals("saberrider2008", descriptor.getNickname());
-    assertEquals("94.134.192.243", descriptor.getAddress());
-    assertEquals(9001, (int) descriptor.getOrPort());
-    assertEquals(0, (int) descriptor.getSocksPort());
-    assertEquals(0, (int) descriptor.getDirPort());
-  }
-
-  @Test(expected = DescriptorParseException.class)
-  public void testRouterLinePrecedingHibernatingLine()
-      throws DescriptorParseException {
-    DescriptorBuilder.createWithRouterLine("hibernating 1\nrouter "
-        + "saberrider2008 94.134.192.243 9001 0 0");
-  }
-
-  @Test(expected = DescriptorParseException.class)
-  public void testNicknameMissing() throws DescriptorParseException {
-    DescriptorBuilder.createWithRouterLine("router  94.134.192.243 9001 "
-        + "0 0");
-  }
-
-  @Test(expected = DescriptorParseException.class)
-  public void testNicknameInvalidChar() throws DescriptorParseException {
-    DescriptorBuilder.createWithRouterLine("router $aberrider2008 "
-        + "94.134.192.243 9001 0 0");
-  }
-
-  @Test(expected = DescriptorParseException.class)
-  public void testNicknameTooLong() throws DescriptorParseException {
-    DescriptorBuilder.createWithRouterLine("router "
-        + "saberrider2008ReallyLongNickname 94.134.192.243 9001 0 0");
-  }
-
-  @Test(expected = DescriptorParseException.class)
-  public void testAddress24() throws DescriptorParseException {
-    DescriptorBuilder.createWithRouterLine("router saberrider2008 "
-        + "94.134.192/24 9001 0 0");
-  }
-
-  @Test(expected = DescriptorParseException.class)
-  public void testAddress294() throws DescriptorParseException {
-    DescriptorBuilder.createWithRouterLine("router saberrider2008 "
-        + "294.134.192.243 9001 0 0");
-  }
-
-  @Test(expected = DescriptorParseException.class)
-  public void testAddressMissing() throws DescriptorParseException {
-    DescriptorBuilder.createWithRouterLine("router saberrider2008  9001 "
-        + "0 0");
-  }
-
-  @Test(expected = DescriptorParseException.class)
-  public void testOrPort99001() throws DescriptorParseException {
-    DescriptorBuilder.createWithRouterLine("router saberrider2008 "
-        + "94.134.192.243 99001 0 0");
-  }
-
-  @Test(expected = DescriptorParseException.class)
-  public void testOrPortMissing() throws DescriptorParseException {
-    DescriptorBuilder.createWithRouterLine("router saberrider2008 "
-        + "94.134.192.243  0 0");
-  }
-
-  @Test(expected = DescriptorParseException.class)
-  public void testOrPortOne() throws DescriptorParseException {
-    DescriptorBuilder.createWithRouterLine("router saberrider2008 "
-        + "94.134.192.243 one 0 0");
-  }
-
-  @Test(expected = DescriptorParseException.class)
-  public void testOrPortNewline() throws DescriptorParseException {
-    DescriptorBuilder.createWithRouterLine("router saberrider2008 "
-        + "94.134.192.243 0\n 0 0");
-  }
-
-  @Test(expected = DescriptorParseException.class)
-  public void testDirPortMissing() throws DescriptorParseException {
-    DescriptorBuilder.createWithRouterLine("router saberrider2008 "
-        + "94.134.192.243 9001 0 ");
-  }
-
-  @Test()
-  public void testPlatformMissing() throws DescriptorParseException {
-    RelayServerDescriptor descriptor = DescriptorBuilder.
-        createWithPlatformLine(null);
-    assertNull(descriptor.getPlatform());
-  }
-
-  @Test()
-  public void testPlatformOpt() throws DescriptorParseException {
-    RelayServerDescriptor descriptor = DescriptorBuilder.
-        createWithPlatformLine("opt platform Tor 0.2.2.35 "
-        + "(git-b04388f9e7546a9f) on Linux i686");
-    assertEquals("Tor 0.2.2.35 (git-b04388f9e7546a9f) on Linux i686",
-        descriptor.getPlatform());
-  }
-
-  @Test()
-  public void testPlatformNoSpace() throws DescriptorParseException {
-    RelayServerDescriptor descriptor = DescriptorBuilder.
-        createWithPlatformLine("platform");
-    assertEquals("", descriptor.getPlatform());
-  }
-
-  @Test()
-  public void testPlatformSpace() throws DescriptorParseException {
-    RelayServerDescriptor descriptor = DescriptorBuilder.
-        createWithPlatformLine("platform ");
-    assertEquals("", descriptor.getPlatform());
-  }
-
-  @Test()
-  public void testProtocolsNoOpt() throws DescriptorParseException {
-    RelayServerDescriptor descriptor = DescriptorBuilder.
-        createWithProtocolsLine("protocols Link 1 2 Circuit 1");
-    assertEquals(Arrays.asList(new Integer[] {1, 2}),
-        descriptor.getLinkProtocolVersions());
-    assertEquals(Arrays.asList(new Integer[] {1}),
-        descriptor.getCircuitProtocolVersions());
-  }
-
-  @Test(expected = DescriptorParseException.class)
-  public void testProtocolsAB() throws DescriptorParseException {
-    DescriptorBuilder.createWithProtocolsLine("opt protocols Link A B "
-        + "Circuit 1");
-  }
-
-  @Test(expected = DescriptorParseException.class)
-  public void testProtocolsNoCircuitVersions()
-      throws DescriptorParseException {
-    DescriptorBuilder.createWithProtocolsLine("opt protocols Link 1 2");
-  }
-
-  @Test(expected = DescriptorParseException.class)
-  public void testPublishedMissing() throws DescriptorParseException {
-    DescriptorBuilder.createWithPublishedLine(null);
-  }
-
-  @Test()
-  public void testPublishedOpt() throws DescriptorParseException {
-    RelayServerDescriptor descriptor = DescriptorBuilder.
-        createWithPublishedLine("opt published 2012-01-01 04:03:19");
-    assertEquals(1325390599000L, descriptor.getPublishedMillis());
-  }
-
-  @Test(expected = DescriptorParseException.class)
-  public void testPublished3012() throws DescriptorParseException {
-    DescriptorBuilder.createWithPublishedLine("published 3012-01-01 "
-        + "04:03:19");
-  }
-
-  @Test(expected = DescriptorParseException.class)
-  public void testPublished1912() throws DescriptorParseException {
-    DescriptorBuilder.createWithPublishedLine("published 1912-01-01 "
-        + "04:03:19");
-  }
-
-  @Test(expected = DescriptorParseException.class)
-  public void testPublishedFeb31() throws DescriptorParseException {
-    DescriptorBuilder.createWithPublishedLine("published 2012-02-31 "
-        + "04:03:19");
-  }
-
-  @Test()
-  public void testFingerprintNoOpt() throws DescriptorParseException {
-    RelayServerDescriptor descriptor = DescriptorBuilder.
-        createWithFingerprintLine("fingerprint D873 3048 FC8E C910 2466 "
-            + "AD8F 3098 622B F1BF 71FD");
-    assertEquals("D8733048FC8EC9102466AD8F3098622BF1BF71FD",
-        descriptor.getFingerprint());
-  }
-
-  @Test(expected = DescriptorParseException.class)
-  public void testFingerprintG() throws DescriptorParseException {
-    DescriptorBuilder.createWithFingerprintLine("opt fingerprint G873 "
-        + "3048 FC8E C910 2466 AD8F 3098 622B F1BF 71FD");
-  }
-
-  @Test(expected = DescriptorParseException.class)
-  public void testFingerprintTooShort() throws DescriptorParseException {
-    DescriptorBuilder.createWithFingerprintLine("opt fingerprint D873 "
-        + "3048 FC8E C910 2466 AD8F 3098 622B F1BF");
-  }
-
-  @Test(expected = DescriptorParseException.class)
-  public void testFingerprintTooLong() throws DescriptorParseException {
-    DescriptorBuilder.createWithFingerprintLine("opt fingerprint D873 "
-        + "3048 FC8E C910 2466 AD8F 3098 622B F1BF 71FD D873");
-  }
-
-  @Test(expected = DescriptorParseException.class)
-  public void testFingerprintNoSpaces() throws DescriptorParseException {
-    DescriptorBuilder.createWithFingerprintLine("opt fingerprint "
-        + "D8733048FC8EC9102466AD8F3098622BF1BF71FD");
-  }
-
-  @Test()
-  public void testUptimeMissing() throws DescriptorParseException {
-    RelayServerDescriptor descriptor = DescriptorBuilder.
-        createWithUptimeLine(null);
-    assertEquals(-1, (int) descriptor.getUptime());
-  }
-
-  @Test()
-  public void testUptimeOpt() throws DescriptorParseException {
-    RelayServerDescriptor descriptor = DescriptorBuilder.
-        createWithUptimeLine("opt uptime 48");
-  }
-
-  @Test(expected = DescriptorParseException.class)
-  public void testUptimeFourtyEight() throws DescriptorParseException {
-    DescriptorBuilder.createWithUptimeLine("uptime fourty-eight");
-  }
-
-  @Test(expected = DescriptorParseException.class)
-  public void testUptimeMinusOne() throws DescriptorParseException {
-    DescriptorBuilder.createWithUptimeLine("uptime -1");
-  }
-
-  @Test(expected = DescriptorParseException.class)
-  public void testUptimeSpace() throws DescriptorParseException {
-    DescriptorBuilder.createWithUptimeLine("uptime ");
-  }
-
-  @Test(expected = DescriptorParseException.class)
-  public void testUptimeNoSpace() throws DescriptorParseException {
-    DescriptorBuilder.createWithUptimeLine("uptime");
-  }
-
-  @Test(expected = DescriptorParseException.class)
-  public void testUptimeFourEight() throws DescriptorParseException {
-    DescriptorBuilder.createWithUptimeLine("uptime 4 8");
-  }
-
-  @Test()
-  public void testBandwidthOpt() throws DescriptorParseException {
-    RelayServerDescriptor descriptor = DescriptorBuilder.
-        createWithBandwidthLine("opt bandwidth 51200 51200 53470");
-    assertEquals(51200, (int) descriptor.getBandwidthRate());
-    assertEquals(51200, (int) descriptor.getBandwidthBurst());
-    assertEquals(53470, (int) descriptor.getBandwidthObserved());
-  }
-
-  @Test(expected = DescriptorParseException.class)
-  public void testBandwidthMissing() throws DescriptorParseException {
-    DescriptorBuilder.createWithBandwidthLine(null);
-  }
-
-  @Test(expected = DescriptorParseException.class)
-  public void testBandwidthTwoValues() throws DescriptorParseException {
-    DescriptorBuilder.createWithBandwidthLine("bandwidth 51200 51200");
-  }
-
-  @Test(expected = DescriptorParseException.class)
-  public void testBandwidthFourValues() throws DescriptorParseException {
-    DescriptorBuilder.createWithBandwidthLine("bandwidth 51200 51200 "
-        + "53470 53470");
-  }
-
-  @Test(expected = DescriptorParseException.class)
-  public void testBandwidthMinusOneTwoThree()
-      throws DescriptorParseException {
-    DescriptorBuilder.createWithBandwidthLine("bandwidth -1 -2 -3");
-  }
-
-  @Test()
-  public void testExtraInfoDigestNoOpt() throws DescriptorParseException {
-    RelayServerDescriptor descriptor = DescriptorBuilder.
-        createWithExtraInfoDigestLine("extra-info-digest "
-        + "1469D1550738A25B1E7B47CDDBCD7B2899F51B74");
-    assertEquals("1469D1550738A25B1E7B47CDDBCD7B2899F51B74",
-        descriptor.getExtraInfoDigest());
-  }
-
-  @Test(expected = DescriptorParseException.class)
-  public void testExtraInfoDigestNoSpace()
-      throws DescriptorParseException {
-    DescriptorBuilder.createWithExtraInfoDigestLine("opt "
-        + "extra-info-digest");
-  }
-
-  @Test(expected = DescriptorParseException.class)
-  public void testExtraInfoDigestTooShort()
-      throws DescriptorParseException {
-    DescriptorBuilder.createWithExtraInfoDigestLine("opt "
-        + "extra-info-digest 1469D1550738A25B1E7B47CDDBCD7B2899F5");
-  }
-
-  @Test(expected = DescriptorParseException.class)
-  public void testExtraInfoDigestTooLong()
-      throws DescriptorParseException {
-    DescriptorBuilder.createWithExtraInfoDigestLine("opt "
-        + "extra-info-digest "
-        + "1469D1550738A25B1E7B47CDDBCD7B2899F51B741469");
-  }
-
-  @Test()
-  public void testExtraInfoDigestMissing()
-      throws DescriptorParseException {
-    RelayServerDescriptor descriptor = DescriptorBuilder.
-        createWithExtraInfoDigestLine(null);
-    assertNull(descriptor.getExtraInfoDigest());
-  }
-
-  @Test(expected = DescriptorParseException.class)
-  public void testOnionKeyMissing() throws DescriptorParseException {
-    DescriptorBuilder.createWithOnionKeyLines(null);
-  }
-
-  @Test()
-  public void testOnionKeyOpt() throws DescriptorParseException {
-    RelayServerDescriptor descriptor = DescriptorBuilder.
-        createWithOnionKeyLines("opt onion-key\n"
-        + "-----BEGIN RSA PUBLIC KEY-----\n"
-        + "MIGJAoGBAKM+iiHhO6eHsvd6Xjws9z9EQB1V/Bpuy5ciGJ1U4V9SeiKooSo5Bp"
-        + "PL\no3XT+6PIgzl3R6uycjS3Ejk47vLEJdcVTm/VG6E0ppu3olIynCI4QryfCE"
-        + "uC3cTF\n9wE4WXY4nX7w0RTN18UVLxrt1A9PP0cobFNiPs9rzJCbKFfacOkpAg"
-        + "MBAAE=\n"
-        + "-----END RSA PUBLIC KEY-----");
-  }
-
-  @Test(expected = DescriptorParseException.class)
-  public void testSigningKeyMissing() throws DescriptorParseException {
-    DescriptorBuilder.createWithSigningKeyLines(null);
-  }
-
-  @Test()
-  public void testSigningKeyOpt() throws DescriptorParseException {
-    RelayServerDescriptor descriptor = DescriptorBuilder.
-        createWithSigningKeyLines("opt signing-key\n"
-        + "-----BEGIN RSA PUBLIC KEY-----\n"
-        + "MIGJAoGBALMm3r3QDh482Ewe6Ub9wvRIfmEkoNX6q5cEAtQRNHSDcNx41gjELb"
-        + "cl\nEniVMParBYACKfOxkS+mTTnIRDKVNEJTsDOwryNrc4X9JnPc/nn6ymYPiN"
-        + "DhUROG\n8URDIhQoixcUeyyrVB8sxliSstKimulGnB7xpjYOlO8JKaHLNL4TAg"
-        + "MBAAE=\n"
-        + "-----END RSA PUBLIC KEY-----");
-  }
-
-  @Test()
-  public void testHiddenServiceDirMissing()
-      throws DescriptorParseException {
-    RelayServerDescriptor descriptor = DescriptorBuilder.
-        createWithHiddenServiceDirLine(null);
-    assertNull(descriptor.getHiddenServiceDirVersions());
-  }
-
-  @Test()
-  public void testHiddenServiceDirNoOpt()
-      throws DescriptorParseException {
-    RelayServerDescriptor descriptor = DescriptorBuilder.
-        createWithHiddenServiceDirLine("hidden-service-dir");
-    assertEquals(Arrays.asList(new Integer[] {2}),
-        descriptor.getHiddenServiceDirVersions());
-  }
-
-  @Test()
-  public void testHiddenServiceDirVersions2And3()
-      throws DescriptorParseException {
-    RelayServerDescriptor descriptor = DescriptorBuilder.
-        createWithHiddenServiceDirLine("hidden-service-dir 2 3");
-    assertEquals(Arrays.asList(new Integer[] {2, 3}),
-        descriptor.getHiddenServiceDirVersions());
-  }
-
-  @Test()
-  public void testContactMissing() throws DescriptorParseException {
-    RelayServerDescriptor descriptor = DescriptorBuilder.
-        createWithContactLine(null);
-    assertNull(descriptor.getContact());
-  }
-
-  @Test()
-  public void testContactOpt() throws DescriptorParseException {
-    RelayServerDescriptor descriptor = DescriptorBuilder.
-        createWithContactLine("opt contact Random Person");
-    assertEquals("Random Person", descriptor.getContact());
-  }
-
-  @Test(expected = DescriptorParseException.class)
-  public void testContactDuplicate() throws DescriptorParseException {
-    DescriptorBuilder.createWithContactLine("contact Random "
-        + "Person\ncontact Random Person");
-  }
-
-  @Test()
-  public void testContactNoSpace() throws DescriptorParseException {
-    RelayServerDescriptor descriptor = DescriptorBuilder.
-        createWithContactLine("contact");
-    assertEquals("", descriptor.getContact());
-  }
-
-  @Test()
-  public void testExitPolicyRejectAllAcceptAll()
-      throws DescriptorParseException {
-    RelayServerDescriptor descriptor = DescriptorBuilder.
-        createWithExitPolicyLines("reject *:*\naccept *:*");
-    assertEquals(Arrays.asList(new String[] {"reject *:*", "accept *:*"}),
-        descriptor.getExitPolicyLines());
-  }
-
-  @Test()
-  public void testExitPolicyOpt() throws DescriptorParseException {
-    RelayServerDescriptor descriptor = DescriptorBuilder.
-        createWithExitPolicyLines("opt reject *:*");
-    assertEquals(Arrays.asList(new String[] {"reject *:*"}),
-        descriptor.getExitPolicyLines());
-  }
-
-  @Test(expected = DescriptorParseException.class)
-  public void testExitPolicyNoPort() throws DescriptorParseException {
-    DescriptorBuilder.createWithExitPolicyLines("reject *");
-  }
-
-  @Test()
-  public void testExitPolicyAccept80RejectAll()
-      throws DescriptorParseException {
-    RelayServerDescriptor descriptor = DescriptorBuilder.
-        createWithExitPolicyLines("accept *:80\nreject *:*");
-    assertEquals(Arrays.asList(new String[] {"accept *:80",
-        "reject *:*"}), descriptor.getExitPolicyLines());
-  }
-
-  @Test(expected = DescriptorParseException.class)
-  public void testExitPolicyReject321() throws DescriptorParseException {
-    DescriptorBuilder.createWithExitPolicyLines("reject "
-        + "123.123.123.321:80");
-  }
-
-  @Test(expected = DescriptorParseException.class)
-  public void testExitPolicyRejectPort66666()
-      throws DescriptorParseException {
-    DescriptorBuilder.createWithExitPolicyLines("reject *:66666");
-  }
-
-  @Test(expected = DescriptorParseException.class)
-  public void testExitPolicyProjectAll() throws DescriptorParseException {
-    DescriptorBuilder.createWithExitPolicyLines("project *:*");
-  }
-
-  @Test(expected = DescriptorParseException.class)
-  public void testExitPolicyMissing() throws DescriptorParseException {
-    DescriptorBuilder.createWithExitPolicyLines(null);
-  }
-
-  @Test(expected = DescriptorParseException.class)
-  public void testRouterSignatureMissing()
-      throws DescriptorParseException {
-    DescriptorBuilder.createWithRouterSignatureLines(null);
-  }
-
-  @Test()
-  public void testRouterSignatureOpt()
-      throws DescriptorParseException {
-    DescriptorBuilder.createWithRouterSignatureLines("opt "
-        + "router-signature\n"
-        + "-----BEGIN SIGNATURE-----\n"
-        + "crypto lines are ignored anyway\n"
-        + "-----END SIGNATURE-----");
-  }
-
-  @Test(expected = DescriptorParseException.class)
-  public void testRouterSignatureNotLastLine()
-      throws DescriptorParseException {
-    DescriptorBuilder.createWithRouterSignatureLines("router-signature\n"
-        + "-----BEGIN SIGNATURE-----\n"
-        + "o4j+kH8UQfjBwepUnr99v0ebN8RpzHJ/lqYsTojXHy9kMr1RNI9IDeSzA7PSqT"
-        + "uV\n4PL8QsGtlfwthtIoZpB2srZeyN/mcpA9fa1JXUrt/UN9K/+32Cyaad7h0n"
-        + "HE6Xfb\njqpXDpnBpvk4zjmzjjKYnIsUWTnADmu0fo3xTRqXi7g=\n"
-        + "-----END SIGNATURE-----\ncontact me");
-  }
-
-  @Test()
-  public void testHibernatingOpt() throws DescriptorParseException {
-    RelayServerDescriptor descriptor = DescriptorBuilder.
-        createWithHibernatingLine("opt hibernating 1");
-    assertTrue(descriptor.isHibernating());
-  }
-
-  @Test()
-  public void testHibernatingFalse() throws DescriptorParseException {
-    RelayServerDescriptor descriptor = DescriptorBuilder.
-        createWithHibernatingLine("hibernating 0");
-    assertFalse(descriptor.isHibernating());
-  }
-
-  @Test()
-  public void testHibernatingTrue() throws DescriptorParseException {
-    RelayServerDescriptor descriptor = DescriptorBuilder.
-        createWithHibernatingLine("hibernating 1");
-    assertTrue(descriptor.isHibernating());
-  }
-
-  @Test(expected = DescriptorParseException.class)
-  public void testHibernatingYep() throws DescriptorParseException {
-    DescriptorBuilder.createWithHibernatingLine("hibernating yep");
-  }
-
-  @Test(expected = DescriptorParseException.class)
-  public void testHibernatingNoSpace() throws DescriptorParseException {
-    DescriptorBuilder.createWithHibernatingLine("hibernating");
-  }
-
-  @Test()
-  public void testFamilyOpt() throws DescriptorParseException {
-    RelayServerDescriptor descriptor = DescriptorBuilder.
-        createWithFamilyLine("opt family saberrider2008");
-    assertEquals(Arrays.asList(new String[] {"saberrider2008"}),
-        descriptor.getFamilyEntries());
-  }
-
-  @Test()
-  public void testFamilyFingerprint() throws DescriptorParseException {
-    RelayServerDescriptor descriptor = DescriptorBuilder.
-        createWithFamilyLine("family "
-        + "$D8733048FC8EC9102466AD8F3098622BF1BF71FD");
-    assertEquals(Arrays.asList(new String[] {
-        "$D8733048FC8EC9102466AD8F3098622BF1BF71FD"}),
-        descriptor.getFamilyEntries());
-  }
-
-  @Test()
-  public void testFamilyNickname() throws DescriptorParseException {
-    RelayServerDescriptor descriptor = DescriptorBuilder.
-        createWithFamilyLine("family saberrider2008");
-    assertEquals(Arrays.asList(new String[] {"saberrider2008"}),
-        descriptor.getFamilyEntries());
-  }
-
-  @Test(expected = DescriptorParseException.class)
-  public void testFamilyDuplicate() throws DescriptorParseException {
-    DescriptorBuilder.createWithFamilyLine("family "
-        + "saberrider2008\nfamily saberrider2008");
-  }
-
-  @Test(expected = DescriptorParseException.class)
-  public void testFamilyNicknamePrefix() throws DescriptorParseException {
-    DescriptorBuilder.createWithFamilyLine("family $saberrider2008");
-  }
-
-  @Test(expected = DescriptorParseException.class)
-  public void testFamilyFingerprintNoPrefix()
-      throws DescriptorParseException {
-    DescriptorBuilder.createWithFamilyLine("family "
-        + "D8733048FC8EC9102466AD8F3098622BF1BF71FD");
-  }
-
-  @Test()
-  public void testWriteHistory() throws DescriptorParseException {
-    String writeHistoryLine = "write-history 2012-01-01 03:51:44 (900 s) "
-        + "4345856,261120,7591936,1748992";
-    RelayServerDescriptor descriptor = DescriptorBuilder.
-        createWithWriteHistoryLine(writeHistoryLine);
-    assertNotNull(descriptor.getWriteHistory());
-    BandwidthHistory parsedWriteHistory = descriptor.getWriteHistory();
-    assertEquals(writeHistoryLine, parsedWriteHistory.getLine());
-    assertEquals(1325389904000L, (long) parsedWriteHistory.
-        getHistoryEndMillis());
-    assertEquals(900L, (long) parsedWriteHistory.getIntervalLength());
-    SortedMap<Long, Long> bandwidthValues = parsedWriteHistory.
-        getBandwidthValues();
-    assertEquals(4345856L, (long) bandwidthValues.remove(1325387204000L));
-    assertEquals(261120L, (long) bandwidthValues.remove(1325388104000L));
-    assertEquals(7591936L, (long) bandwidthValues.remove(1325389004000L));
-    assertEquals(1748992L, (long) bandwidthValues.remove(1325389904000L));
-    assertTrue(bandwidthValues.isEmpty());
-  }
-
-  @Test()
-  public void testWriteHistoryOpt() throws DescriptorParseException {
-    RelayServerDescriptor descriptor = DescriptorBuilder.
-        createWithWriteHistoryLine("opt write-history 2012-01-01 "
-        + "03:51:44 (900 s) 4345856,261120,7591936,1748992");
-    assertNotNull(descriptor.getWriteHistory());
-  }
-
-  @Test(expected = DescriptorParseException.class)
-  public void testWriteHistory3012() throws DescriptorParseException {
-    DescriptorBuilder.createWithWriteHistoryLine("write-history "
-        + "3012-01-01 03:51:44 (900 s) 4345856,261120,7591936,1748992");
-  }
-
-  @Test(expected = DescriptorParseException.class)
-  public void testWriteHistoryNoSeconds()
-      throws DescriptorParseException {
-    DescriptorBuilder.createWithWriteHistoryLine("write-history "
-        + "2012-01-01 03:51 (900 s) 4345856,261120,7591936,1748992");
-  }
-
-  @Test(expected = DescriptorParseException.class)
-  public void testWriteHistoryNoParathenses()
-      throws DescriptorParseException {
-    DescriptorBuilder.createWithWriteHistoryLine("write-history "
-        + "2012-01-01 03:51:44 900 s 4345856,261120,7591936,1748992");
-  }
-
-  @Test(expected = DescriptorParseException.class)
-  public void testWriteHistoryNoSpaceSeconds()
-      throws DescriptorParseException {
-    DescriptorBuilder.createWithWriteHistoryLine("write-history "
-        + "2012-01-01 03:51:44 (900s) 4345856,261120,7591936,1748992");
-  }
-
-  @Test(expected = DescriptorParseException.class)
-  public void testWriteHistoryTrailingComma()
-      throws DescriptorParseException {
-    DescriptorBuilder.createWithWriteHistoryLine("write-history "
-        + "2012-01-01 03:51:44 (900 s) 4345856,261120,7591936,");
-  }
-
-  @Test(expected = DescriptorParseException.class)
-  public void testWriteHistoryOneTwoThree()
-      throws DescriptorParseException {
-    DescriptorBuilder.createWithWriteHistoryLine("write-history "
-        + "2012-01-01 03:51:44 (900 s) one,two,three");
-  }
-
-  @Test()
-  public void testWriteHistoryNoValuesSpace()
-      throws DescriptorParseException {
-    RelayServerDescriptor descriptor = DescriptorBuilder.
-        createWithWriteHistoryLine("write-history 2012-01-01 03:51:44 "
-        + "(900 s) ");
-    assertEquals(900, (long) descriptor.getWriteHistory().
-        getIntervalLength());
-    assertTrue(descriptor.getWriteHistory().getBandwidthValues().
-        isEmpty());
-  }
-
-  @Test()
-  public void testWriteHistoryNoValuesNoSpace()
-      throws DescriptorParseException {
-    RelayServerDescriptor descriptor = DescriptorBuilder.
-        createWithWriteHistoryLine("write-history 2012-01-01 03:51:44 "
-        + "(900 s)");
-    assertEquals(900, (long) descriptor.getWriteHistory().
-        getIntervalLength());
-    assertTrue(descriptor.getWriteHistory().getBandwidthValues().
-        isEmpty());
-  }
-
-  @Test(expected = DescriptorParseException.class)
-  public void testWriteHistoryNoS() throws DescriptorParseException {
-    RelayServerDescriptor descriptor = DescriptorBuilder.
-        createWithWriteHistoryLine("write-history 2012-01-01 03:51:44 "
-        + "(900 ");
-  }
-
-  @Test(expected = DescriptorParseException.class)
-  public void testWriteHistoryTrailingNumber()
-      throws DescriptorParseException {
-    DescriptorBuilder.createWithWriteHistoryLine("write-history "
-        + "2012-01-01 03:51:44 (900 s) 4345856 1");
-  }
-
-  @Test()
-  public void testWriteHistory1800Seconds()
-      throws DescriptorParseException {
-    RelayServerDescriptor descriptor = DescriptorBuilder.
-        createWithWriteHistoryLine("write-history 2012-01-01 03:51:44 "
-        + "(1800 s) 4345856");
-    assertEquals(1800L, (long) descriptor.getWriteHistory().
-        getIntervalLength());
-  }
-
-  @Test()
-  public void testReadHistory() throws DescriptorParseException {
-    String readHistoryLine = "read-history 2012-01-01 03:51:44 (900 s) "
-        + "4268032,139264,7797760,1415168";
-    RelayServerDescriptor descriptor = DescriptorBuilder.
-        createWithReadHistoryLine(readHistoryLine);
-    assertNotNull(descriptor.getReadHistory());
-    BandwidthHistory parsedReadHistory = descriptor.getReadHistory();
-    assertEquals(readHistoryLine, parsedReadHistory.getLine());
-    assertEquals(1325389904000L, (long) parsedReadHistory.
-        getHistoryEndMillis());
-    assertEquals(900L, (long) parsedReadHistory.getIntervalLength());
-    SortedMap<Long, Long> bandwidthValues = parsedReadHistory.
-        getBandwidthValues();
-    assertEquals(4268032L, (long) bandwidthValues.remove(1325387204000L));
-    assertEquals(139264L, (long) bandwidthValues.remove(1325388104000L));
-    assertEquals(7797760L, (long) bandwidthValues.remove(1325389004000L));
-    assertEquals(1415168L, (long) bandwidthValues.remove(1325389904000L));
-    assertTrue(bandwidthValues.isEmpty());
-  }
-
-  /* TODO There are some old server descriptors with " read-history"
-   * lines.  Find out if these were spec-compliant and if other lines may
-   * start with leading spaces, too. */
-  @Test(expected = DescriptorParseException.class)
-  public void testReadHistoryLeadingSpace()
-      throws DescriptorParseException {
-    String readHistoryLine = " read-history 2012-01-01 03:51:44 (900 s) "
-        + "4268032,139264,7797760,1415168";
-    RelayServerDescriptor descriptor = DescriptorBuilder.
-        createWithReadHistoryLine(readHistoryLine);
-  }
-
-  @Test()
-  public void testEventdnsOpt() throws DescriptorParseException {
-    RelayServerDescriptor descriptor = DescriptorBuilder.
-        createWithEventdnsLine("opt eventdns true");
-    assertTrue(descriptor.getUsesEnhancedDnsLogic());
-  }
-
-  @Test()
-  public void testEventdnsTrue() throws DescriptorParseException {
-    RelayServerDescriptor descriptor = DescriptorBuilder.
-        createWithEventdnsLine("eventdns true");
-    assertTrue(descriptor.getUsesEnhancedDnsLogic());
-  }
-
-  @Test()
-  public void testEventdnsFalse() throws DescriptorParseException {
-    RelayServerDescriptor descriptor = DescriptorBuilder.
-        createWithEventdnsLine("eventdns false");
-    assertFalse(descriptor.getUsesEnhancedDnsLogic());
-  }
-
-  @Test(expected = DescriptorParseException.class)
-  public void testEventdns1() throws DescriptorParseException {
-    DescriptorBuilder.createWithEventdnsLine("eventdns 1");
-  }
-
-  @Test(expected = DescriptorParseException.class)
-  public void testEventdnsNo() throws DescriptorParseException {
-    DescriptorBuilder.createWithEventdnsLine("eventdns no");
-  }
-
-  @Test()
-  public void testCachesExtraInfoOpt() throws DescriptorParseException {
-    RelayServerDescriptor descriptor = DescriptorBuilder.
-        createWithCachesExtraInfoLine("opt caches-extra-info");
-    assertTrue(descriptor.getCachesExtraInfo());
-  }
-
-  @Test()
-  public void testCachesExtraInfoNoSpace()
-      throws DescriptorParseException {
-    RelayServerDescriptor descriptor = DescriptorBuilder.
-        createWithCachesExtraInfoLine("caches-extra-info");
-    assertTrue(descriptor.getCachesExtraInfo());
-  }
-
-  @Test(expected = DescriptorParseException.class)
-  public void testCachesExtraInfoTrue() throws DescriptorParseException {
-    DescriptorBuilder.createWithCachesExtraInfoLine("caches-extra-info "
-        + "true");
-  }
-
-  @Test()
-  public void testAllowSingleHopExitsOpt()
-      throws DescriptorParseException {
-    RelayServerDescriptor descriptor = DescriptorBuilder.
-        createWithCachesExtraInfoLine("opt allow-single-hop-exits");
-    assertTrue(descriptor.getAllowSingleHopExits());
-  }
-
-  @Test()
-  public void testAllowSingleHopExitsNoSpace()
-      throws DescriptorParseException {
-    RelayServerDescriptor descriptor = DescriptorBuilder.
-        createWithCachesExtraInfoLine("allow-single-hop-exits");
-    assertTrue(descriptor.getAllowSingleHopExits());
-  }
-
-  @Test(expected = DescriptorParseException.class)
-  public void testAllowSingleHopExitsTrue()
-      throws DescriptorParseException {
-    DescriptorBuilder.createWithCachesExtraInfoLine(
-        "allow-single-hop-exits true");
-  }
-}
-
diff --git a/test/org/torproject/descriptor/impl/ServerDescriptorImplTest.java b/test/org/torproject/descriptor/impl/ServerDescriptorImplTest.java
new file mode 100644
index 0000000..c0721f5
--- /dev/null
+++ b/test/org/torproject/descriptor/impl/ServerDescriptorImplTest.java
@@ -0,0 +1,1080 @@
+/* Copyright 2012 The Tor Project
+ * See LICENSE for licensing information */
+package org.torproject.descriptor.impl;
+
+import org.torproject.descriptor.BandwidthHistory;
+import org.torproject.descriptor.ServerDescriptor;
+
+import java.util.*;
+
+import org.junit.*;
+import org.junit.rules.*;
+import static org.junit.Assert.*;
+
+/* Test parsing of relay server descriptors. */
+public class ServerDescriptorImplTest {
+
+  /* Helper class to build a descriptor based on default data and
+   * modifications requested by test methods. */
+  private static class DescriptorBuilder {
+    private String routerLine = "router saberrider2008 94.134.192.243 "
+        + "9001 0 0";
+    private static ServerDescriptor createWithRouterLine(
+        String line) throws DescriptorParseException {
+      DescriptorBuilder db = new DescriptorBuilder();
+      db.routerLine = line;
+      return new ServerDescriptorImpl(db.buildDescriptor());
+    }
+    private String bandwidthLine = "bandwidth 51200 51200 53470";
+    private static ServerDescriptor createWithBandwidthLine(
+        String line) throws DescriptorParseException {
+      DescriptorBuilder db = new DescriptorBuilder();
+      db.bandwidthLine = line;
+      return new ServerDescriptorImpl(db.buildDescriptor());
+    }
+    private String platformLine = "platform Tor 0.2.2.35 "
+        + "(git-b04388f9e7546a9f) on Linux i686";
+    private static ServerDescriptor createWithPlatformLine(
+        String line) throws DescriptorParseException {
+      DescriptorBuilder db = new DescriptorBuilder();
+      db.platformLine = line;
+      return new ServerDescriptorImpl(db.buildDescriptor());
+    }
+    private String publishedLine = "published 2012-01-01 04:03:19";
+    private static ServerDescriptor createWithPublishedLine(
+        String line) throws DescriptorParseException {
+      DescriptorBuilder db = new DescriptorBuilder();
+      db.publishedLine = line;
+      return new ServerDescriptorImpl(db.buildDescriptor());
+    }
+    private String fingerprintLine = "opt fingerprint D873 3048 FC8E "
+        + "C910 2466 AD8F 3098 622B F1BF 71FD";
+    private static ServerDescriptor createWithFingerprintLine(
+        String line) throws DescriptorParseException {
+      DescriptorBuilder db = new DescriptorBuilder();
+      db.fingerprintLine = line;
+      return new ServerDescriptorImpl(db.buildDescriptor());
+    }
+    private String hibernatingLine = null;
+    private static ServerDescriptor createWithHibernatingLine(
+        String line) throws DescriptorParseException {
+      DescriptorBuilder db = new DescriptorBuilder();
+      db.hibernatingLine = line;
+      return new ServerDescriptorImpl(db.buildDescriptor());
+    }
+    private String uptimeLine = "uptime 48";
+    private static ServerDescriptor createWithUptimeLine(
+        String line) throws DescriptorParseException {
+      DescriptorBuilder db = new DescriptorBuilder();
+      db.uptimeLine = line;
+      return new ServerDescriptorImpl(db.buildDescriptor());
+    }
+    private String onionKeyLines = "onion-key\n"
+        + "-----BEGIN RSA PUBLIC KEY-----\n"
+        + "MIGJAoGBAKM+iiHhO6eHsvd6Xjws9z9EQB1V/Bpuy5ciGJ1U4V9SeiKooSo5Bp"
+        + "PL\no3XT+6PIgzl3R6uycjS3Ejk47vLEJdcVTm/VG6E0ppu3olIynCI4QryfCE"
+        + "uC3cTF\n9wE4WXY4nX7w0RTN18UVLxrt1A9PP0cobFNiPs9rzJCbKFfacOkpAg"
+        + "MBAAE=\n"
+        + "-----END RSA PUBLIC KEY-----";
+    private static ServerDescriptor createWithOnionKeyLines(
+        String lines) throws DescriptorParseException {
+      DescriptorBuilder db = new DescriptorBuilder();
+      db.onionKeyLines = lines;
+      return new ServerDescriptorImpl(db.buildDescriptor());
+    }
+    private String signingKeyLines = "signing-key\n"
+        + "-----BEGIN RSA PUBLIC KEY-----\n"
+        + "MIGJAoGBALMm3r3QDh482Ewe6Ub9wvRIfmEkoNX6q5cEAtQRNHSDcNx41gjELb"
+        + "cl\nEniVMParBYACKfOxkS+mTTnIRDKVNEJTsDOwryNrc4X9JnPc/nn6ymYPiN"
+        + "DhUROG\n8URDIhQoixcUeyyrVB8sxliSstKimulGnB7xpjYOlO8JKaHLNL4TAg"
+        + "MBAAE=\n"
+        + "-----END RSA PUBLIC KEY-----";
+    private static ServerDescriptor createWithSigningKeyLines(
+        String lines) throws DescriptorParseException {
+      DescriptorBuilder db = new DescriptorBuilder();
+      db.signingKeyLines = lines;
+      return new ServerDescriptorImpl(db.buildDescriptor());
+    }
+    private String exitPolicyLines = "reject *:*";
+    private static ServerDescriptor createWithExitPolicyLines(
+        String lines) throws DescriptorParseException {
+      DescriptorBuilder db = new DescriptorBuilder();
+      db.exitPolicyLines = lines;
+      return new ServerDescriptorImpl(db.buildDescriptor());
+    }
+    private String contactLine = "contact Random Person <nobody AT "
+        + "example dot com>";
+    private static ServerDescriptor createWithContactLine(
+        String line) throws DescriptorParseException {
+      DescriptorBuilder db = new DescriptorBuilder();
+      db.contactLine = line;
+      return new ServerDescriptorImpl(db.buildDescriptor());
+    }
+    private String familyLine = null;
+    private static ServerDescriptor createWithFamilyLine(
+        String line) throws DescriptorParseException {
+      DescriptorBuilder db = new DescriptorBuilder();
+      db.familyLine = line;
+      return new ServerDescriptorImpl(db.buildDescriptor());
+    }
+    private String readHistoryLine = null;
+    private static ServerDescriptor createWithReadHistoryLine(
+        String line) throws DescriptorParseException {
+      DescriptorBuilder db = new DescriptorBuilder();
+      db.readHistoryLine = line;
+      return new ServerDescriptorImpl(db.buildDescriptor());
+    }
+    private String writeHistoryLine = null;
+    private static ServerDescriptor createWithWriteHistoryLine(
+        String line) throws DescriptorParseException {
+      DescriptorBuilder db = new DescriptorBuilder();
+      db.writeHistoryLine = line;
+      return new ServerDescriptorImpl(db.buildDescriptor());
+    }
+    private String eventdnsLine = null;
+    private static ServerDescriptor createWithEventdnsLine(
+        String line) throws DescriptorParseException {
+      DescriptorBuilder db = new DescriptorBuilder();
+      db.eventdnsLine = line;
+      return new ServerDescriptorImpl(db.buildDescriptor());
+    }
+    private String cachesExtraInfoLine = null;
+    private static ServerDescriptor createWithCachesExtraInfoLine(
+        String line) throws DescriptorParseException {
+      DescriptorBuilder db = new DescriptorBuilder();
+      db.cachesExtraInfoLine = line;
+      return new ServerDescriptorImpl(db.buildDescriptor());
+    }
+    private String extraInfoDigestLine = "opt extra-info-digest "
+        + "1469D1550738A25B1E7B47CDDBCD7B2899F51B74";
+    private static ServerDescriptor createWithExtraInfoDigestLine(
+        String line) throws DescriptorParseException {
+      DescriptorBuilder db = new DescriptorBuilder();
+      db.extraInfoDigestLine = line;
+      return new ServerDescriptorImpl(db.buildDescriptor());
+    }
+    private String hiddenServiceDirLine = "opt hidden-service-dir";
+    private static ServerDescriptor createWithHiddenServiceDirLine(
+        String line) throws DescriptorParseException {
+      DescriptorBuilder db = new DescriptorBuilder();
+      db.hiddenServiceDirLine = line;
+      return new ServerDescriptorImpl(db.buildDescriptor());
+    }
+    private String protocolsLine = "opt protocols Link 1 2 Circuit 1";
+    private static ServerDescriptor createWithProtocolsLine(
+        String line) throws DescriptorParseException {
+      DescriptorBuilder db = new DescriptorBuilder();
+      db.protocolsLine = line;
+      return new ServerDescriptorImpl(db.buildDescriptor());
+    }
+    private String allowSingleHopExitsLine = null;
+    private static ServerDescriptor
+        createWithAllowSingleHopExitsLine(String line)
+        throws DescriptorParseException {
+      DescriptorBuilder db = new DescriptorBuilder();
+      db.allowSingleHopExitsLine = line;
+      return new ServerDescriptorImpl(db.buildDescriptor());
+    }
+    private String routerSignatureLines = "router-signature\n"
+        + "-----BEGIN SIGNATURE-----\n"
+        + "o4j+kH8UQfjBwepUnr99v0ebN8RpzHJ/lqYsTojXHy9kMr1RNI9IDeSzA7PSqT"
+        + "uV\n4PL8QsGtlfwthtIoZpB2srZeyN/mcpA9fa1JXUrt/UN9K/+32Cyaad7h0n"
+        + "HE6Xfb\njqpXDpnBpvk4zjmzjjKYnIsUWTnADmu0fo3xTRqXi7g=\n"
+        + "-----END SIGNATURE-----";
+    private static ServerDescriptor createWithRouterSignatureLines(
+        String line) throws DescriptorParseException {
+      DescriptorBuilder db = new DescriptorBuilder();
+      db.routerSignatureLines = line;
+      return new ServerDescriptorImpl(db.buildDescriptor());
+    }
+    private byte[] buildDescriptor() {
+      StringBuilder sb = new StringBuilder();
+      if (this.routerLine != null) {
+        sb.append(this.routerLine + "\n");
+      }
+      if (this.bandwidthLine != null) {
+        sb.append(this.bandwidthLine + "\n");
+      }
+      if (this.platformLine != null) {
+        sb.append(this.platformLine + "\n");
+      }
+      if (this.publishedLine != null) {
+        sb.append(this.publishedLine + "\n");
+      }
+      if (this.fingerprintLine != null) {
+        sb.append(this.fingerprintLine + "\n");
+      }
+      if (this.hibernatingLine != null) {
+        sb.append(this.hibernatingLine + "\n");
+      }
+      if (this.uptimeLine != null) {
+        sb.append(this.uptimeLine + "\n");
+      }
+      if (this.onionKeyLines != null) {
+        sb.append(this.onionKeyLines + "\n");
+      }
+      if (this.signingKeyLines != null) {
+        sb.append(this.signingKeyLines + "\n");
+      }
+      if (this.exitPolicyLines != null) {
+        sb.append(this.exitPolicyLines + "\n");
+      }
+      if (this.contactLine != null) {
+        sb.append(this.contactLine + "\n");
+      }
+      if (this.familyLine != null) {
+        sb.append(this.familyLine + "\n");
+      }
+      if (this.readHistoryLine != null) {
+        sb.append(this.readHistoryLine + "\n");
+      }
+      if (this.writeHistoryLine != null) {
+        sb.append(this.writeHistoryLine + "\n");
+      }
+      if (this.eventdnsLine != null) {
+        sb.append(this.eventdnsLine + "\n");
+      }
+      if (this.cachesExtraInfoLine != null) {
+        sb.append(this.cachesExtraInfoLine + "\n");
+      }
+      if (this.extraInfoDigestLine != null) {
+        sb.append(this.extraInfoDigestLine + "\n");
+      }
+      if (this.hiddenServiceDirLine != null) {
+        sb.append(this.hiddenServiceDirLine + "\n");
+      }
+      if (this.protocolsLine != null) {
+        sb.append(this.protocolsLine + "\n");
+      }
+      if (this.allowSingleHopExitsLine != null) {
+        sb.append(this.allowSingleHopExitsLine + "\n");
+      }
+      if (this.routerSignatureLines != null) {
+        sb.append(this.routerSignatureLines + "\n");
+      }
+      return sb.toString().getBytes();
+    }
+  }
+
+  @Test()
+  public void testSampleDescriptor() throws DescriptorParseException {
+    DescriptorBuilder db = new DescriptorBuilder();
+    ServerDescriptor descriptor =
+        new ServerDescriptorImpl(db.buildDescriptor());
+    assertEquals("saberrider2008", descriptor.getNickname());
+    assertEquals("94.134.192.243", descriptor.getAddress());
+    assertEquals(9001, (int) descriptor.getOrPort());
+    assertEquals(0, (int) descriptor.getSocksPort());
+    assertEquals(0, (int) descriptor.getDirPort());
+    assertEquals("Tor 0.2.2.35 (git-b04388f9e7546a9f) on Linux i686",
+        descriptor.getPlatform());
+    assertEquals(Arrays.asList(new Integer[] {1, 2}),
+        descriptor.getLinkProtocolVersions());
+    assertEquals(Arrays.asList(new Integer[] {1}),
+        descriptor.getCircuitProtocolVersions());
+    assertEquals(1325390599000L, descriptor.getPublishedMillis());
+    assertEquals("D8733048FC8EC9102466AD8F3098622BF1BF71FD",
+        descriptor.getFingerprint());
+    assertEquals(48, (int) descriptor.getUptime());
+    assertEquals(51200, (int) descriptor.getBandwidthRate());
+    assertEquals(51200, (int) descriptor.getBandwidthBurst());
+    assertEquals(53470, (int) descriptor.getBandwidthObserved());
+    assertEquals("1469D1550738A25B1E7B47CDDBCD7B2899F51B74",
+        descriptor.getExtraInfoDigest());
+    assertEquals(Arrays.asList(new Integer[] {2}),
+        descriptor.getHiddenServiceDirVersions());
+    assertEquals("Random Person <nobody AT example dot com>",
+        descriptor.getContact());
+    assertEquals(Arrays.asList(new String[] {"reject *:*"}),
+        descriptor.getExitPolicyLines());
+    assertFalse(descriptor.isHibernating());
+    assertNull(descriptor.getFamilyEntries());
+    assertNull(descriptor.getReadHistory());
+    assertNull(descriptor.getWriteHistory());
+    assertFalse(descriptor.getUsesEnhancedDnsLogic());
+    assertFalse(descriptor.getCachesExtraInfo());
+    assertFalse(descriptor.getAllowSingleHopExits());
+  }
+
+  @Test(expected = DescriptorParseException.class)
+  public void testRouterLineMissing() throws DescriptorParseException {
+    DescriptorBuilder.createWithRouterLine(null);
+  }
+
+  @Test()
+  public void testRouterOpt() throws DescriptorParseException {
+    ServerDescriptor descriptor = DescriptorBuilder.
+        createWithRouterLine("opt router saberrider2008 "
+        + "94.134.192.243 9001 0 0");
+    assertEquals("saberrider2008", descriptor.getNickname());
+    assertEquals("94.134.192.243", descriptor.getAddress());
+    assertEquals(9001, (int) descriptor.getOrPort());
+    assertEquals(0, (int) descriptor.getSocksPort());
+    assertEquals(0, (int) descriptor.getDirPort());
+  }
+
+  @Test(expected = DescriptorParseException.class)
+  public void testRouterLinePrecedingHibernatingLine()
+      throws DescriptorParseException {
+    DescriptorBuilder.createWithRouterLine("hibernating 1\nrouter "
+        + "saberrider2008 94.134.192.243 9001 0 0");
+  }
+
+  @Test(expected = DescriptorParseException.class)
+  public void testNicknameMissing() throws DescriptorParseException {
+    DescriptorBuilder.createWithRouterLine("router  94.134.192.243 9001 "
+        + "0 0");
+  }
+
+  @Test(expected = DescriptorParseException.class)
+  public void testNicknameInvalidChar() throws DescriptorParseException {
+    DescriptorBuilder.createWithRouterLine("router $aberrider2008 "
+        + "94.134.192.243 9001 0 0");
+  }
+
+  @Test(expected = DescriptorParseException.class)
+  public void testNicknameTooLong() throws DescriptorParseException {
+    DescriptorBuilder.createWithRouterLine("router "
+        + "saberrider2008ReallyLongNickname 94.134.192.243 9001 0 0");
+  }
+
+  @Test(expected = DescriptorParseException.class)
+  public void testAddress24() throws DescriptorParseException {
+    DescriptorBuilder.createWithRouterLine("router saberrider2008 "
+        + "94.134.192/24 9001 0 0");
+  }
+
+  @Test(expected = DescriptorParseException.class)
+  public void testAddress294() throws DescriptorParseException {
+    DescriptorBuilder.createWithRouterLine("router saberrider2008 "
+        + "294.134.192.243 9001 0 0");
+  }
+
+  @Test(expected = DescriptorParseException.class)
+  public void testAddressMissing() throws DescriptorParseException {
+    DescriptorBuilder.createWithRouterLine("router saberrider2008  9001 "
+        + "0 0");
+  }
+
+  @Test(expected = DescriptorParseException.class)
+  public void testOrPort99001() throws DescriptorParseException {
+    DescriptorBuilder.createWithRouterLine("router saberrider2008 "
+        + "94.134.192.243 99001 0 0");
+  }
+
+  @Test(expected = DescriptorParseException.class)
+  public void testOrPortMissing() throws DescriptorParseException {
+    DescriptorBuilder.createWithRouterLine("router saberrider2008 "
+        + "94.134.192.243  0 0");
+  }
+
+  @Test(expected = DescriptorParseException.class)
+  public void testOrPortOne() throws DescriptorParseException {
+    DescriptorBuilder.createWithRouterLine("router saberrider2008 "
+        + "94.134.192.243 one 0 0");
+  }
+
+  @Test(expected = DescriptorParseException.class)
+  public void testOrPortNewline() throws DescriptorParseException {
+    DescriptorBuilder.createWithRouterLine("router saberrider2008 "
+        + "94.134.192.243 0\n 0 0");
+  }
+
+  @Test(expected = DescriptorParseException.class)
+  public void testDirPortMissing() throws DescriptorParseException {
+    DescriptorBuilder.createWithRouterLine("router saberrider2008 "
+        + "94.134.192.243 9001 0 ");
+  }
+
+  @Test()
+  public void testPlatformMissing() throws DescriptorParseException {
+    ServerDescriptor descriptor = DescriptorBuilder.
+        createWithPlatformLine(null);
+    assertNull(descriptor.getPlatform());
+  }
+
+  @Test()
+  public void testPlatformOpt() throws DescriptorParseException {
+    ServerDescriptor descriptor = DescriptorBuilder.
+        createWithPlatformLine("opt platform Tor 0.2.2.35 "
+        + "(git-b04388f9e7546a9f) on Linux i686");
+    assertEquals("Tor 0.2.2.35 (git-b04388f9e7546a9f) on Linux i686",
+        descriptor.getPlatform());
+  }
+
+  @Test()
+  public void testPlatformNoSpace() throws DescriptorParseException {
+    ServerDescriptor descriptor = DescriptorBuilder.
+        createWithPlatformLine("platform");
+    assertEquals("", descriptor.getPlatform());
+  }
+
+  @Test()
+  public void testPlatformSpace() throws DescriptorParseException {
+    ServerDescriptor descriptor = DescriptorBuilder.
+        createWithPlatformLine("platform ");
+    assertEquals("", descriptor.getPlatform());
+  }
+
+  @Test()
+  public void testProtocolsNoOpt() throws DescriptorParseException {
+    ServerDescriptor descriptor = DescriptorBuilder.
+        createWithProtocolsLine("protocols Link 1 2 Circuit 1");
+    assertEquals(Arrays.asList(new Integer[] {1, 2}),
+        descriptor.getLinkProtocolVersions());
+    assertEquals(Arrays.asList(new Integer[] {1}),
+        descriptor.getCircuitProtocolVersions());
+  }
+
+  @Test(expected = DescriptorParseException.class)
+  public void testProtocolsAB() throws DescriptorParseException {
+    DescriptorBuilder.createWithProtocolsLine("opt protocols Link A B "
+        + "Circuit 1");
+  }
+
+  @Test(expected = DescriptorParseException.class)
+  public void testProtocolsNoCircuitVersions()
+      throws DescriptorParseException {
+    DescriptorBuilder.createWithProtocolsLine("opt protocols Link 1 2");
+  }
+
+  @Test(expected = DescriptorParseException.class)
+  public void testPublishedMissing() throws DescriptorParseException {
+    DescriptorBuilder.createWithPublishedLine(null);
+  }
+
+  @Test()
+  public void testPublishedOpt() throws DescriptorParseException {
+    ServerDescriptor descriptor = DescriptorBuilder.
+        createWithPublishedLine("opt published 2012-01-01 04:03:19");
+    assertEquals(1325390599000L, descriptor.getPublishedMillis());
+  }
+
+  @Test(expected = DescriptorParseException.class)
+  public void testPublished3012() throws DescriptorParseException {
+    DescriptorBuilder.createWithPublishedLine("published 3012-01-01 "
+        + "04:03:19");
+  }
+
+  @Test(expected = DescriptorParseException.class)
+  public void testPublished1912() throws DescriptorParseException {
+    DescriptorBuilder.createWithPublishedLine("published 1912-01-01 "
+        + "04:03:19");
+  }
+
+  @Test(expected = DescriptorParseException.class)
+  public void testPublishedFeb31() throws DescriptorParseException {
+    DescriptorBuilder.createWithPublishedLine("published 2012-02-31 "
+        + "04:03:19");
+  }
+
+  @Test()
+  public void testFingerprintNoOpt() throws DescriptorParseException {
+    ServerDescriptor descriptor = DescriptorBuilder.
+        createWithFingerprintLine("fingerprint D873 3048 FC8E C910 2466 "
+            + "AD8F 3098 622B F1BF 71FD");
+    assertEquals("D8733048FC8EC9102466AD8F3098622BF1BF71FD",
+        descriptor.getFingerprint());
+  }
+
+  @Test(expected = DescriptorParseException.class)
+  public void testFingerprintG() throws DescriptorParseException {
+    DescriptorBuilder.createWithFingerprintLine("opt fingerprint G873 "
+        + "3048 FC8E C910 2466 AD8F 3098 622B F1BF 71FD");
+  }
+
+  @Test(expected = DescriptorParseException.class)
+  public void testFingerprintTooShort() throws DescriptorParseException {
+    DescriptorBuilder.createWithFingerprintLine("opt fingerprint D873 "
+        + "3048 FC8E C910 2466 AD8F 3098 622B F1BF");
+  }
+
+  @Test(expected = DescriptorParseException.class)
+  public void testFingerprintTooLong() throws DescriptorParseException {
+    DescriptorBuilder.createWithFingerprintLine("opt fingerprint D873 "
+        + "3048 FC8E C910 2466 AD8F 3098 622B F1BF 71FD D873");
+  }
+
+  @Test(expected = DescriptorParseException.class)
+  public void testFingerprintNoSpaces() throws DescriptorParseException {
+    DescriptorBuilder.createWithFingerprintLine("opt fingerprint "
+        + "D8733048FC8EC9102466AD8F3098622BF1BF71FD");
+  }
+
+  @Test()
+  public void testUptimeMissing() throws DescriptorParseException {
+    ServerDescriptor descriptor = DescriptorBuilder.
+        createWithUptimeLine(null);
+    assertEquals(-1, (int) descriptor.getUptime());
+  }
+
+  @Test()
+  public void testUptimeOpt() throws DescriptorParseException {
+    ServerDescriptor descriptor = DescriptorBuilder.
+        createWithUptimeLine("opt uptime 48");
+  }
+
+  @Test(expected = DescriptorParseException.class)
+  public void testUptimeFourtyEight() throws DescriptorParseException {
+    DescriptorBuilder.createWithUptimeLine("uptime fourty-eight");
+  }
+
+  @Test(expected = DescriptorParseException.class)
+  public void testUptimeMinusOne() throws DescriptorParseException {
+    DescriptorBuilder.createWithUptimeLine("uptime -1");
+  }
+
+  @Test(expected = DescriptorParseException.class)
+  public void testUptimeSpace() throws DescriptorParseException {
+    DescriptorBuilder.createWithUptimeLine("uptime ");
+  }
+
+  @Test(expected = DescriptorParseException.class)
+  public void testUptimeNoSpace() throws DescriptorParseException {
+    DescriptorBuilder.createWithUptimeLine("uptime");
+  }
+
+  @Test(expected = DescriptorParseException.class)
+  public void testUptimeFourEight() throws DescriptorParseException {
+    DescriptorBuilder.createWithUptimeLine("uptime 4 8");
+  }
+
+  @Test()
+  public void testBandwidthOpt() throws DescriptorParseException {
+    ServerDescriptor descriptor = DescriptorBuilder.
+        createWithBandwidthLine("opt bandwidth 51200 51200 53470");
+    assertEquals(51200, (int) descriptor.getBandwidthRate());
+    assertEquals(51200, (int) descriptor.getBandwidthBurst());
+    assertEquals(53470, (int) descriptor.getBandwidthObserved());
+  }
+
+  @Test(expected = DescriptorParseException.class)
+  public void testBandwidthMissing() throws DescriptorParseException {
+    DescriptorBuilder.createWithBandwidthLine(null);
+  }
+
+  @Test(expected = DescriptorParseException.class)
+  public void testBandwidthTwoValues() throws DescriptorParseException {
+    DescriptorBuilder.createWithBandwidthLine("bandwidth 51200 51200");
+  }
+
+  @Test(expected = DescriptorParseException.class)
+  public void testBandwidthFourValues() throws DescriptorParseException {
+    DescriptorBuilder.createWithBandwidthLine("bandwidth 51200 51200 "
+        + "53470 53470");
+  }
+
+  @Test(expected = DescriptorParseException.class)
+  public void testBandwidthMinusOneTwoThree()
+      throws DescriptorParseException {
+    DescriptorBuilder.createWithBandwidthLine("bandwidth -1 -2 -3");
+  }
+
+  @Test()
+  public void testExtraInfoDigestNoOpt() throws DescriptorParseException {
+    ServerDescriptor descriptor = DescriptorBuilder.
+        createWithExtraInfoDigestLine("extra-info-digest "
+        + "1469D1550738A25B1E7B47CDDBCD7B2899F51B74");
+    assertEquals("1469D1550738A25B1E7B47CDDBCD7B2899F51B74",
+        descriptor.getExtraInfoDigest());
+  }
+
+  @Test(expected = DescriptorParseException.class)
+  public void testExtraInfoDigestNoSpace()
+      throws DescriptorParseException {
+    DescriptorBuilder.createWithExtraInfoDigestLine("opt "
+        + "extra-info-digest");
+  }
+
+  @Test(expected = DescriptorParseException.class)
+  public void testExtraInfoDigestTooShort()
+      throws DescriptorParseException {
+    DescriptorBuilder.createWithExtraInfoDigestLine("opt "
+        + "extra-info-digest 1469D1550738A25B1E7B47CDDBCD7B2899F5");
+  }
+
+  @Test(expected = DescriptorParseException.class)
+  public void testExtraInfoDigestTooLong()
+      throws DescriptorParseException {
+    DescriptorBuilder.createWithExtraInfoDigestLine("opt "
+        + "extra-info-digest "
+        + "1469D1550738A25B1E7B47CDDBCD7B2899F51B741469");
+  }
+
+  @Test()
+  public void testExtraInfoDigestMissing()
+      throws DescriptorParseException {
+    ServerDescriptor descriptor = DescriptorBuilder.
+        createWithExtraInfoDigestLine(null);
+    assertNull(descriptor.getExtraInfoDigest());
+  }
+
+  @Test(expected = DescriptorParseException.class)
+  public void testOnionKeyMissing() throws DescriptorParseException {
+    DescriptorBuilder.createWithOnionKeyLines(null);
+  }
+
+  @Test()
+  public void testOnionKeyOpt() throws DescriptorParseException {
+    ServerDescriptor descriptor = DescriptorBuilder.
+        createWithOnionKeyLines("opt onion-key\n"
+        + "-----BEGIN RSA PUBLIC KEY-----\n"
+        + "MIGJAoGBAKM+iiHhO6eHsvd6Xjws9z9EQB1V/Bpuy5ciGJ1U4V9SeiKooSo5Bp"
+        + "PL\no3XT+6PIgzl3R6uycjS3Ejk47vLEJdcVTm/VG6E0ppu3olIynCI4QryfCE"
+        + "uC3cTF\n9wE4WXY4nX7w0RTN18UVLxrt1A9PP0cobFNiPs9rzJCbKFfacOkpAg"
+        + "MBAAE=\n"
+        + "-----END RSA PUBLIC KEY-----");
+  }
+
+  @Test(expected = DescriptorParseException.class)
+  public void testSigningKeyMissing() throws DescriptorParseException {
+    DescriptorBuilder.createWithSigningKeyLines(null);
+  }
+
+  @Test()
+  public void testSigningKeyOpt() throws DescriptorParseException {
+    ServerDescriptor descriptor = DescriptorBuilder.
+        createWithSigningKeyLines("opt signing-key\n"
+        + "-----BEGIN RSA PUBLIC KEY-----\n"
+        + "MIGJAoGBALMm3r3QDh482Ewe6Ub9wvRIfmEkoNX6q5cEAtQRNHSDcNx41gjELb"
+        + "cl\nEniVMParBYACKfOxkS+mTTnIRDKVNEJTsDOwryNrc4X9JnPc/nn6ymYPiN"
+        + "DhUROG\n8URDIhQoixcUeyyrVB8sxliSstKimulGnB7xpjYOlO8JKaHLNL4TAg"
+        + "MBAAE=\n"
+        + "-----END RSA PUBLIC KEY-----");
+  }
+
+  @Test()
+  public void testHiddenServiceDirMissing()
+      throws DescriptorParseException {
+    ServerDescriptor descriptor = DescriptorBuilder.
+        createWithHiddenServiceDirLine(null);
+    assertNull(descriptor.getHiddenServiceDirVersions());
+  }
+
+  @Test()
+  public void testHiddenServiceDirNoOpt()
+      throws DescriptorParseException {
+    ServerDescriptor descriptor = DescriptorBuilder.
+        createWithHiddenServiceDirLine("hidden-service-dir");
+    assertEquals(Arrays.asList(new Integer[] {2}),
+        descriptor.getHiddenServiceDirVersions());
+  }
+
+  @Test()
+  public void testHiddenServiceDirVersions2And3()
+      throws DescriptorParseException {
+    ServerDescriptor descriptor = DescriptorBuilder.
+        createWithHiddenServiceDirLine("hidden-service-dir 2 3");
+    assertEquals(Arrays.asList(new Integer[] {2, 3}),
+        descriptor.getHiddenServiceDirVersions());
+  }
+
+  @Test()
+  public void testContactMissing() throws DescriptorParseException {
+    ServerDescriptor descriptor = DescriptorBuilder.
+        createWithContactLine(null);
+    assertNull(descriptor.getContact());
+  }
+
+  @Test()
+  public void testContactOpt() throws DescriptorParseException {
+    ServerDescriptor descriptor = DescriptorBuilder.
+        createWithContactLine("opt contact Random Person");
+    assertEquals("Random Person", descriptor.getContact());
+  }
+
+  @Test(expected = DescriptorParseException.class)
+  public void testContactDuplicate() throws DescriptorParseException {
+    DescriptorBuilder.createWithContactLine("contact Random "
+        + "Person\ncontact Random Person");
+  }
+
+  @Test()
+  public void testContactNoSpace() throws DescriptorParseException {
+    ServerDescriptor descriptor = DescriptorBuilder.
+        createWithContactLine("contact");
+    assertEquals("", descriptor.getContact());
+  }
+
+  @Test()
+  public void testExitPolicyRejectAllAcceptAll()
+      throws DescriptorParseException {
+    ServerDescriptor descriptor = DescriptorBuilder.
+        createWithExitPolicyLines("reject *:*\naccept *:*");
+    assertEquals(Arrays.asList(new String[] {"reject *:*", "accept *:*"}),
+        descriptor.getExitPolicyLines());
+  }
+
+  @Test()
+  public void testExitPolicyOpt() throws DescriptorParseException {
+    ServerDescriptor descriptor = DescriptorBuilder.
+        createWithExitPolicyLines("opt reject *:*");
+    assertEquals(Arrays.asList(new String[] {"reject *:*"}),
+        descriptor.getExitPolicyLines());
+  }
+
+  @Test(expected = DescriptorParseException.class)
+  public void testExitPolicyNoPort() throws DescriptorParseException {
+    DescriptorBuilder.createWithExitPolicyLines("reject *");
+  }
+
+  @Test()
+  public void testExitPolicyAccept80RejectAll()
+      throws DescriptorParseException {
+    ServerDescriptor descriptor = DescriptorBuilder.
+        createWithExitPolicyLines("accept *:80\nreject *:*");
+    assertEquals(Arrays.asList(new String[] {"accept *:80",
+        "reject *:*"}), descriptor.getExitPolicyLines());
+  }
+
+  @Test(expected = DescriptorParseException.class)
+  public void testExitPolicyReject321() throws DescriptorParseException {
+    DescriptorBuilder.createWithExitPolicyLines("reject "
+        + "123.123.123.321:80");
+  }
+
+  @Test(expected = DescriptorParseException.class)
+  public void testExitPolicyRejectPort66666()
+      throws DescriptorParseException {
+    DescriptorBuilder.createWithExitPolicyLines("reject *:66666");
+  }
+
+  @Test(expected = DescriptorParseException.class)
+  public void testExitPolicyProjectAll() throws DescriptorParseException {
+    DescriptorBuilder.createWithExitPolicyLines("project *:*");
+  }
+
+  @Test(expected = DescriptorParseException.class)
+  public void testExitPolicyMissing() throws DescriptorParseException {
+    DescriptorBuilder.createWithExitPolicyLines(null);
+  }
+
+  @Test(expected = DescriptorParseException.class)
+  public void testRouterSignatureMissing()
+      throws DescriptorParseException {
+    DescriptorBuilder.createWithRouterSignatureLines(null);
+  }
+
+  @Test()
+  public void testRouterSignatureOpt()
+      throws DescriptorParseException {
+    DescriptorBuilder.createWithRouterSignatureLines("opt "
+        + "router-signature\n"
+        + "-----BEGIN SIGNATURE-----\n"
+        + "crypto lines are ignored anyway\n"
+        + "-----END SIGNATURE-----");
+  }
+
+  @Test(expected = DescriptorParseException.class)
+  public void testRouterSignatureNotLastLine()
+      throws DescriptorParseException {
+    DescriptorBuilder.createWithRouterSignatureLines("router-signature\n"
+        + "-----BEGIN SIGNATURE-----\n"
+        + "o4j+kH8UQfjBwepUnr99v0ebN8RpzHJ/lqYsTojXHy9kMr1RNI9IDeSzA7PSqT"
+        + "uV\n4PL8QsGtlfwthtIoZpB2srZeyN/mcpA9fa1JXUrt/UN9K/+32Cyaad7h0n"
+        + "HE6Xfb\njqpXDpnBpvk4zjmzjjKYnIsUWTnADmu0fo3xTRqXi7g=\n"
+        + "-----END SIGNATURE-----\ncontact me");
+  }
+
+  @Test()
+  public void testHibernatingOpt() throws DescriptorParseException {
+    ServerDescriptor descriptor = DescriptorBuilder.
+        createWithHibernatingLine("opt hibernating 1");
+    assertTrue(descriptor.isHibernating());
+  }
+
+  @Test()
+  public void testHibernatingFalse() throws DescriptorParseException {
+    ServerDescriptor descriptor = DescriptorBuilder.
+        createWithHibernatingLine("hibernating 0");
+    assertFalse(descriptor.isHibernating());
+  }
+
+  @Test()
+  public void testHibernatingTrue() throws DescriptorParseException {
+    ServerDescriptor descriptor = DescriptorBuilder.
+        createWithHibernatingLine("hibernating 1");
+    assertTrue(descriptor.isHibernating());
+  }
+
+  @Test(expected = DescriptorParseException.class)
+  public void testHibernatingYep() throws DescriptorParseException {
+    DescriptorBuilder.createWithHibernatingLine("hibernating yep");
+  }
+
+  @Test(expected = DescriptorParseException.class)
+  public void testHibernatingNoSpace() throws DescriptorParseException {
+    DescriptorBuilder.createWithHibernatingLine("hibernating");
+  }
+
+  @Test()
+  public void testFamilyOpt() throws DescriptorParseException {
+    ServerDescriptor descriptor = DescriptorBuilder.
+        createWithFamilyLine("opt family saberrider2008");
+    assertEquals(Arrays.asList(new String[] {"saberrider2008"}),
+        descriptor.getFamilyEntries());
+  }
+
+  @Test()
+  public void testFamilyFingerprint() throws DescriptorParseException {
+    ServerDescriptor descriptor = DescriptorBuilder.
+        createWithFamilyLine("family "
+        + "$D8733048FC8EC9102466AD8F3098622BF1BF71FD");
+    assertEquals(Arrays.asList(new String[] {
+        "$D8733048FC8EC9102466AD8F3098622BF1BF71FD"}),
+        descriptor.getFamilyEntries());
+  }
+
+  @Test()
+  public void testFamilyNickname() throws DescriptorParseException {
+    ServerDescriptor descriptor = DescriptorBuilder.
+        createWithFamilyLine("family saberrider2008");
+    assertEquals(Arrays.asList(new String[] {"saberrider2008"}),
+        descriptor.getFamilyEntries());
+  }
+
+  @Test(expected = DescriptorParseException.class)
+  public void testFamilyDuplicate() throws DescriptorParseException {
+    DescriptorBuilder.createWithFamilyLine("family "
+        + "saberrider2008\nfamily saberrider2008");
+  }
+
+  @Test(expected = DescriptorParseException.class)
+  public void testFamilyNicknamePrefix() throws DescriptorParseException {
+    DescriptorBuilder.createWithFamilyLine("family $saberrider2008");
+  }
+
+  @Test(expected = DescriptorParseException.class)
+  public void testFamilyFingerprintNoPrefix()
+      throws DescriptorParseException {
+    DescriptorBuilder.createWithFamilyLine("family "
+        + "D8733048FC8EC9102466AD8F3098622BF1BF71FD");
+  }
+
+  @Test()
+  public void testWriteHistory() throws DescriptorParseException {
+    String writeHistoryLine = "write-history 2012-01-01 03:51:44 (900 s) "
+        + "4345856,261120,7591936,1748992";
+    ServerDescriptor descriptor = DescriptorBuilder.
+        createWithWriteHistoryLine(writeHistoryLine);
+    assertNotNull(descriptor.getWriteHistory());
+    BandwidthHistory parsedWriteHistory = descriptor.getWriteHistory();
+    assertEquals(writeHistoryLine, parsedWriteHistory.getLine());
+    assertEquals(1325389904000L, (long) parsedWriteHistory.
+        getHistoryEndMillis());
+    assertEquals(900L, (long) parsedWriteHistory.getIntervalLength());
+    SortedMap<Long, Long> bandwidthValues = parsedWriteHistory.
+        getBandwidthValues();
+    assertEquals(4345856L, (long) bandwidthValues.remove(1325387204000L));
+    assertEquals(261120L, (long) bandwidthValues.remove(1325388104000L));
+    assertEquals(7591936L, (long) bandwidthValues.remove(1325389004000L));
+    assertEquals(1748992L, (long) bandwidthValues.remove(1325389904000L));
+    assertTrue(bandwidthValues.isEmpty());
+  }
+
+  @Test()
+  public void testWriteHistoryOpt() throws DescriptorParseException {
+    ServerDescriptor descriptor = DescriptorBuilder.
+        createWithWriteHistoryLine("opt write-history 2012-01-01 "
+        + "03:51:44 (900 s) 4345856,261120,7591936,1748992");
+    assertNotNull(descriptor.getWriteHistory());
+  }
+
+  @Test(expected = DescriptorParseException.class)
+  public void testWriteHistory3012() throws DescriptorParseException {
+    DescriptorBuilder.createWithWriteHistoryLine("write-history "
+        + "3012-01-01 03:51:44 (900 s) 4345856,261120,7591936,1748992");
+  }
+
+  @Test(expected = DescriptorParseException.class)
+  public void testWriteHistoryNoSeconds()
+      throws DescriptorParseException {
+    DescriptorBuilder.createWithWriteHistoryLine("write-history "
+        + "2012-01-01 03:51 (900 s) 4345856,261120,7591936,1748992");
+  }
+
+  @Test(expected = DescriptorParseException.class)
+  public void testWriteHistoryNoParathenses()
+      throws DescriptorParseException {
+    DescriptorBuilder.createWithWriteHistoryLine("write-history "
+        + "2012-01-01 03:51:44 900 s 4345856,261120,7591936,1748992");
+  }
+
+  @Test(expected = DescriptorParseException.class)
+  public void testWriteHistoryNoSpaceSeconds()
+      throws DescriptorParseException {
+    DescriptorBuilder.createWithWriteHistoryLine("write-history "
+        + "2012-01-01 03:51:44 (900s) 4345856,261120,7591936,1748992");
+  }
+
+  @Test(expected = DescriptorParseException.class)
+  public void testWriteHistoryTrailingComma()
+      throws DescriptorParseException {
+    DescriptorBuilder.createWithWriteHistoryLine("write-history "
+        + "2012-01-01 03:51:44 (900 s) 4345856,261120,7591936,");
+  }
+
+  @Test(expected = DescriptorParseException.class)
+  public void testWriteHistoryOneTwoThree()
+      throws DescriptorParseException {
+    DescriptorBuilder.createWithWriteHistoryLine("write-history "
+        + "2012-01-01 03:51:44 (900 s) one,two,three");
+  }
+
+  @Test()
+  public void testWriteHistoryNoValuesSpace()
+      throws DescriptorParseException {
+    ServerDescriptor descriptor = DescriptorBuilder.
+        createWithWriteHistoryLine("write-history 2012-01-01 03:51:44 "
+        + "(900 s) ");
+    assertEquals(900, (long) descriptor.getWriteHistory().
+        getIntervalLength());
+    assertTrue(descriptor.getWriteHistory().getBandwidthValues().
+        isEmpty());
+  }
+
+  @Test()
+  public void testWriteHistoryNoValuesNoSpace()
+      throws DescriptorParseException {
+    ServerDescriptor descriptor = DescriptorBuilder.
+        createWithWriteHistoryLine("write-history 2012-01-01 03:51:44 "
+        + "(900 s)");
+    assertEquals(900, (long) descriptor.getWriteHistory().
+        getIntervalLength());
+    assertTrue(descriptor.getWriteHistory().getBandwidthValues().
+        isEmpty());
+  }
+
+  @Test(expected = DescriptorParseException.class)
+  public void testWriteHistoryNoS() throws DescriptorParseException {
+    ServerDescriptor descriptor = DescriptorBuilder.
+        createWithWriteHistoryLine("write-history 2012-01-01 03:51:44 "
+        + "(900 ");
+  }
+
+  @Test(expected = DescriptorParseException.class)
+  public void testWriteHistoryTrailingNumber()
+      throws DescriptorParseException {
+    DescriptorBuilder.createWithWriteHistoryLine("write-history "
+        + "2012-01-01 03:51:44 (900 s) 4345856 1");
+  }
+
+  @Test()
+  public void testWriteHistory1800Seconds()
+      throws DescriptorParseException {
+    ServerDescriptor descriptor = DescriptorBuilder.
+        createWithWriteHistoryLine("write-history 2012-01-01 03:51:44 "
+        + "(1800 s) 4345856");
+    assertEquals(1800L, (long) descriptor.getWriteHistory().
+        getIntervalLength());
+  }
+
+  @Test()
+  public void testReadHistory() throws DescriptorParseException {
+    String readHistoryLine = "read-history 2012-01-01 03:51:44 (900 s) "
+        + "4268032,139264,7797760,1415168";
+    ServerDescriptor descriptor = DescriptorBuilder.
+        createWithReadHistoryLine(readHistoryLine);
+    assertNotNull(descriptor.getReadHistory());
+    BandwidthHistory parsedReadHistory = descriptor.getReadHistory();
+    assertEquals(readHistoryLine, parsedReadHistory.getLine());
+    assertEquals(1325389904000L, (long) parsedReadHistory.
+        getHistoryEndMillis());
+    assertEquals(900L, (long) parsedReadHistory.getIntervalLength());
+    SortedMap<Long, Long> bandwidthValues = parsedReadHistory.
+        getBandwidthValues();
+    assertEquals(4268032L, (long) bandwidthValues.remove(1325387204000L));
+    assertEquals(139264L, (long) bandwidthValues.remove(1325388104000L));
+    assertEquals(7797760L, (long) bandwidthValues.remove(1325389004000L));
+    assertEquals(1415168L, (long) bandwidthValues.remove(1325389904000L));
+    assertTrue(bandwidthValues.isEmpty());
+  }
+
+  /* TODO There are some old server descriptors with " read-history"
+   * lines.  Find out if these were spec-compliant and if other lines may
+   * start with leading spaces, too. */
+  @Test(expected = DescriptorParseException.class)
+  public void testReadHistoryLeadingSpace()
+      throws DescriptorParseException {
+    String readHistoryLine = " read-history 2012-01-01 03:51:44 (900 s) "
+        + "4268032,139264,7797760,1415168";
+    ServerDescriptor descriptor = DescriptorBuilder.
+        createWithReadHistoryLine(readHistoryLine);
+  }
+
+  @Test()
+  public void testEventdnsOpt() throws DescriptorParseException {
+    ServerDescriptor descriptor = DescriptorBuilder.
+        createWithEventdnsLine("opt eventdns true");
+    assertTrue(descriptor.getUsesEnhancedDnsLogic());
+  }
+
+  @Test()
+  public void testEventdnsTrue() throws DescriptorParseException {
+    ServerDescriptor descriptor = DescriptorBuilder.
+        createWithEventdnsLine("eventdns true");
+    assertTrue(descriptor.getUsesEnhancedDnsLogic());
+  }
+
+  @Test()
+  public void testEventdnsFalse() throws DescriptorParseException {
+    ServerDescriptor descriptor = DescriptorBuilder.
+        createWithEventdnsLine("eventdns false");
+    assertFalse(descriptor.getUsesEnhancedDnsLogic());
+  }
+
+  @Test(expected = DescriptorParseException.class)
+  public void testEventdns1() throws DescriptorParseException {
+    DescriptorBuilder.createWithEventdnsLine("eventdns 1");
+  }
+
+  @Test(expected = DescriptorParseException.class)
+  public void testEventdnsNo() throws DescriptorParseException {
+    DescriptorBuilder.createWithEventdnsLine("eventdns no");
+  }
+
+  @Test()
+  public void testCachesExtraInfoOpt() throws DescriptorParseException {
+    ServerDescriptor descriptor = DescriptorBuilder.
+        createWithCachesExtraInfoLine("opt caches-extra-info");
+    assertTrue(descriptor.getCachesExtraInfo());
+  }
+
+  @Test()
+  public void testCachesExtraInfoNoSpace()
+      throws DescriptorParseException {
+    ServerDescriptor descriptor = DescriptorBuilder.
+        createWithCachesExtraInfoLine("caches-extra-info");
+    assertTrue(descriptor.getCachesExtraInfo());
+  }
+
+  @Test(expected = DescriptorParseException.class)
+  public void testCachesExtraInfoTrue() throws DescriptorParseException {
+    DescriptorBuilder.createWithCachesExtraInfoLine("caches-extra-info "
+        + "true");
+  }
+
+  @Test()
+  public void testAllowSingleHopExitsOpt()
+      throws DescriptorParseException {
+    ServerDescriptor descriptor = DescriptorBuilder.
+        createWithCachesExtraInfoLine("opt allow-single-hop-exits");
+    assertTrue(descriptor.getAllowSingleHopExits());
+  }
+
+  @Test()
+  public void testAllowSingleHopExitsNoSpace()
+      throws DescriptorParseException {
+    ServerDescriptor descriptor = DescriptorBuilder.
+        createWithCachesExtraInfoLine("allow-single-hop-exits");
+    assertTrue(descriptor.getAllowSingleHopExits());
+  }
+
+  @Test(expected = DescriptorParseException.class)
+  public void testAllowSingleHopExitsTrue()
+      throws DescriptorParseException {
+    DescriptorBuilder.createWithCachesExtraInfoLine(
+        "allow-single-hop-exits true");
+  }
+}
+





More information about the tor-commits mailing list