From b5a43ed60a8bc3afeed7fa2dc656236f771aa918 Sat Dec 12 15:00:00 2015
From: iwakeh <iwakeh@users.ourproject.org>
Date: Sat, 12 Dec 2015 15:00:00 +0000
Subject: [PATCH] Patch for #17821

---
 src/org/torproject/descriptor/ExitList.java        |  26 ++++++
 src/org/torproject/descriptor/ExitListEntry.java   |   8 +-
 .../descriptor/impl/ExitListEntryImpl.java         |  88 +++++++++++-------
 .../torproject/descriptor/impl/ExitListImpl.java   |  77 +++++++++------
 .../descriptor/impl/ExitListImplTest.java          | 103 +++++++++++++++++++++
 5 files changed, 239 insertions(+), 63 deletions(-)
 create mode 100644 test/org/torproject/descriptor/impl/ExitListImplTest.java

diff --git a/src/org/torproject/descriptor/ExitList.java b/src/org/torproject/descriptor/ExitList.java
index 09d7c25..3807a13 100644
--- a/src/org/torproject/descriptor/ExitList.java
+++ b/src/org/torproject/descriptor/ExitList.java
@@ -2,15 +2,41 @@
  * See LICENSE for licensing information */
 package org.torproject.descriptor;
 
+import java.util.Map;
 import java.util.Set;
 
 /* Exit list containing all known exit scan results at a given time. */
 public interface ExitList extends Descriptor {
 
+  public final static String EOL = "\n";
+
+  /* Exit list entry containing results from a single exit scan. */
+  public interface Entry {
+
+    /* Return the scanned relay's fingerprint. */
+    public String getFingerprint();
+
+    /* Return the publication time of the scanned relay's last known
+     * descriptor. */
+    public long getPublishedMillis();
+
+    /* Return the publication time of the network status that this scan was
+     * based on. */
+    public long getLastStatusMillis();
+
+    /* Return the IP addresses that were determined in the scan. */
+    public Map<String, Long> getExitAddresses();
+  }
+
   /* Return the download time of the exit list. */
   public long getDownloadedMillis();
 
   /* Return the unordered set of exit scan results. */
+  /* Use getEntries instead. */
+  @Deprecated
   public Set<ExitListEntry> getExitListEntries();
+
+  /* Return the unordered set of exit scan results. */
+  public Set<ExitList.Entry> getEntries();
 }
 
diff --git a/src/org/torproject/descriptor/ExitListEntry.java b/src/org/torproject/descriptor/ExitListEntry.java
index 201a172..4c01513 100644
--- a/src/org/torproject/descriptor/ExitListEntry.java
+++ b/src/org/torproject/descriptor/ExitListEntry.java
@@ -3,7 +3,9 @@
 package org.torproject.descriptor;
 
 /* Exit list entry containing results from a single exit scan. */
-public interface ExitListEntry {
+/* Use org.torproject.descriptor.ExitList.Entry instead. */
+@Deprecated
+public interface ExitListEntry extends ExitList.Entry {
 
   /* Return the scanned relay's fingerprint. */
   public String getFingerprint();
@@ -16,10 +18,10 @@ public interface ExitListEntry {
    * based on. */
   public long getLastStatusMillis();
 
-  /* Return the IP address that was determined in the scan. */
+  /* Return one IP address that was determined in the scan. */
   public String getExitAddress();
 
-  /* Return the scan time. */
+  /* Return the scan time of the ExitAddress returned by getExitAddress. */
   public long getScanMillis();
 }
 
diff --git a/src/org/torproject/descriptor/impl/ExitListEntryImpl.java b/src/org/torproject/descriptor/impl/ExitListEntryImpl.java
index a03e373..a0065ff 100644
--- a/src/org/torproject/descriptor/impl/ExitListEntryImpl.java
+++ b/src/org/torproject/descriptor/impl/ExitListEntryImpl.java
@@ -3,15 +3,19 @@
 package org.torproject.descriptor.impl;
 
 import org.torproject.descriptor.DescriptorParseException;
+import org.torproject.descriptor.ExitList;
+
 import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 import java.util.Scanner;
 import java.util.SortedSet;
 import java.util.TreeSet;
 
 import org.torproject.descriptor.ExitListEntry;
 
-public class ExitListEntryImpl implements ExitListEntry {
+public class ExitListEntryImpl implements ExitListEntry, ExitList.Entry {
 
   private byte[] exitListEntryBytes;
   public byte[] getExitListEntryBytes() {
@@ -37,56 +41,63 @@ public class ExitListEntryImpl implements ExitListEntry {
     this.checkAndClearKeywords();
   }
 
-  private SortedSet<String> exactlyOnceKeywords;
+  private SortedSet<String> keywordCountingSet;
   private void initializeKeywords() {
-    this.exactlyOnceKeywords = new TreeSet<String>();
-    this.exactlyOnceKeywords.add("ExitNode");
-    this.exactlyOnceKeywords.add("Published");
-    this.exactlyOnceKeywords.add("LastStatus");
-    this.exactlyOnceKeywords.add("ExitAddress");
+    this.keywordCountingSet = new TreeSet<String>();
+    this.keywordCountingSet.add("ExitNode");
+    this.keywordCountingSet.add("Published");
+    this.keywordCountingSet.add("LastStatus");
+    this.keywordCountingSet.add("ExitAddress");
   }
 
   private void parsedExactlyOnceKeyword(String keyword)
       throws DescriptorParseException {
-    if (!this.exactlyOnceKeywords.contains(keyword)) {
+    if (!this.keywordCountingSet.contains(keyword)) {
       throw new DescriptorParseException("Duplicate '" + keyword
           + "' line in exit list entry.");
     }
-    this.exactlyOnceKeywords.remove(keyword);
+    this.keywordCountingSet.remove(keyword);
   }
 
   private void checkAndClearKeywords() throws DescriptorParseException {
-    for (String missingKeyword : this.exactlyOnceKeywords) {
+    for (String missingKeyword : this.keywordCountingSet) {
       throw new DescriptorParseException("Missing '" + missingKeyword
           + "' line in exit list entry.");
     }
-    this.exactlyOnceKeywords = null;
+    this.keywordCountingSet = null;
   }
 
   private void parseExitListEntryBytes()
       throws DescriptorParseException {
     Scanner s = new Scanner(new String(this.exitListEntryBytes)).
-        useDelimiter("\n");
+        useDelimiter(ExitList.EOL);
     while (s.hasNext()) {
       String line = s.next();
       String[] parts = line.split(" ");
       String keyword = parts[0];
-      if (keyword.equals("ExitNode")) {
-        this.parseExitNodeLine(line, parts);
-      } else if (keyword.equals("Published")) {
-        this.parsePublishedLine(line, parts);
-      } else if (keyword.equals("LastStatus")) {
-        this.parseLastStatusLine(line, parts);
-      } else if (keyword.equals("ExitAddress")) {
-        this.parseExitAddressLine(line, parts);
-      } else if (this.failUnrecognizedDescriptorLines) {
-        throw new DescriptorParseException("Unrecognized line '" + line
-            + "' in exit list entry.");
-      } else {
-        if (this.unrecognizedLines == null) {
-          this.unrecognizedLines = new ArrayList<String>();
-        }
-        this.unrecognizedLines.add(line);
+      switch(keyword){
+        case "ExitNode":
+          this.parseExitNodeLine(line, parts);
+          break;
+        case "Published":
+          this.parsePublishedLine(line, parts);
+          break;
+        case "LastStatus":
+          this.parseLastStatusLine(line, parts);
+          break;
+        case "ExitAddress":
+          this.parseExitAddressLine(line, parts);
+          break;
+        default:
+          if (this.failUnrecognizedDescriptorLines) {
+            throw new DescriptorParseException("Unrecognized line '" + line
+                                               + "' in exit list entry.");
+          } else {
+            if (this.unrecognizedLines == null) {
+              this.unrecognizedLines = new ArrayList<String>();
+            }
+            this.unrecognizedLines.add(line);
+          }
       }
     }
   }
@@ -130,10 +141,9 @@ public class ExitListEntryImpl implements ExitListEntry {
       throw new DescriptorParseException("Invalid line '" + line + "' in "
           + "exit list entry.");
     }
-    this.parsedExactlyOnceKeyword(parts[0]);
-    this.exitAddress = ParseHelper.parseIpv4Address(line, parts[1]);
-    this.scanMillis = ParseHelper.parseTimestampAtIndex(line, parts,
-        2, 3);
+    this.keywordCountingSet.remove(parts[0]);
+    exitAddresses.put(ParseHelper.parseIpv4Address(line, parts[1]),
+        ParseHelper.parseTimestampAtIndex(line, parts, 2, 3));
   }
 
   private String fingerprint;
@@ -153,12 +163,24 @@ public class ExitListEntryImpl implements ExitListEntry {
 
   private String exitAddress;
   public String getExitAddress() {
+    if(null == exitAddress){
+      this.exitAddress = exitAddresses.entrySet().iterator().next().getKey();
+      this.scanMillis = exitAddresses.entrySet().iterator().next().getValue();
+    }
     return this.exitAddress;
   }
 
+  private Map<String, Long> exitAddresses = new HashMap<>();
+  public Map<String, Long> getExitAddresses(){
+    return new HashMap<>(this.exitAddresses);
+  }
+
   private long scanMillis;
   public long getScanMillis() {
-    return this.scanMillis;
+    if(null == exitAddress){
+      getExitAddress();
+    }
+    return scanMillis;
   }
 }
 
diff --git a/src/org/torproject/descriptor/impl/ExitListImpl.java b/src/org/torproject/descriptor/impl/ExitListImpl.java
index 53dc112..8e12882 100644
--- a/src/org/torproject/descriptor/impl/ExitListImpl.java
+++ b/src/org/torproject/descriptor/impl/ExitListImpl.java
@@ -15,7 +15,6 @@ import java.util.TimeZone;
 import org.torproject.descriptor.ExitList;
 import org.torproject.descriptor.ExitListEntry;
 
-/* TODO Add test class. */
 public class ExitListImpl extends DescriptorImpl implements ExitList {
 
   protected ExitListImpl(byte[] rawDescriptorBytes, String fileName,
@@ -52,36 +51,57 @@ public class ExitListImpl extends DescriptorImpl implements ExitList {
       throw new DescriptorParseException("Descriptor is empty.");
     }
     String descriptorString = new String(rawDescriptorBytes);
-    Scanner s = new Scanner(descriptorString).useDelimiter("\n");
+    Scanner s = new Scanner(descriptorString).useDelimiter(EOL);
     StringBuilder sb = new StringBuilder();
+    boolean firstEntry = true;
     while (s.hasNext()) {
       String line = s.next();
+      if (line.startsWith("@")) { // skip annotation
+        if (!s.hasNext()) {
+          throw new DescriptorParseException("Descriptor is empty.");
+        } else {
+          line = s.next();
+        }
+      }
       String[] parts = line.split(" ");
       String keyword = parts[0];
-      if (keyword.equals("Downloaded")) {
-        this.downloadedMillis = ParseHelper.parseTimestampAtIndex(line,
-            parts, 1, 2);
-      } else if (keyword.equals("ExitNode")) {
-        sb = new StringBuilder();
-        sb.append(line + "\n");
-      } else if (keyword.equals("Published")) {
-        sb.append(line + "\n");
-      } else if (keyword.equals("LastStatus")) {
-        sb.append(line + "\n");
-      } else if (keyword.equals("ExitAddress")) {
-        String exitListEntryString = sb.toString() + line + "\n";
-        byte[] exitListEntryBytes = exitListEntryString.getBytes();
-        this.parseExitListEntry(exitListEntryBytes);
-      } else if (this.failUnrecognizedDescriptorLines) {
-        throw new DescriptorParseException("Unrecognized line '" + line
-            + "' in exit list.");
-      } else {
-        if (this.unrecognizedLines == null) {
-          this.unrecognizedLines = new ArrayList<String>();
-        }
-        this.unrecognizedLines.add(line);
+      switch (keyword){
+        case "Downloaded":
+          this.downloadedMillis = ParseHelper.parseTimestampAtIndex(line,
+              parts, 1, 2);
+          break;
+        case "ExitNode":
+          if(!firstEntry){
+            this.parseExitListEntry(sb.toString().getBytes());
+          }else{
+            firstEntry = false;
+          }
+          sb = new StringBuilder();
+          sb.append(line).append(ExitList.EOL);
+          break;
+        case "Published":
+          sb.append(line).append(ExitList.EOL);
+          break;
+        case "LastStatus":
+          sb.append(line).append(ExitList.EOL);
+          break;
+        case "ExitAddress":
+          sb.append(line).append(ExitList.EOL);
+          break;
+        default:
+          if (this.failUnrecognizedDescriptorLines) {
+              throw new DescriptorParseException("Unrecognized line '" + line
+                                                 + "' in exit list.");
+          } else {
+              if (this.unrecognizedLines == null) {
+                  this.unrecognizedLines = new ArrayList<String>();
+              }
+              this.unrecognizedLines.add(line);
+          }
       }
     }
+    // parse the last descriptor
+    this.parseExitListEntry(sb.toString().getBytes());
   }
 
   protected void parseExitListEntry(byte[] exitListEntryBytes)
@@ -104,10 +124,13 @@ public class ExitListImpl extends DescriptorImpl implements ExitList {
     return this.downloadedMillis;
   }
 
-  private Set<ExitListEntry> exitListEntries =
-      new HashSet<ExitListEntry>();
+  private Set<ExitListEntry> exitListEntries = new HashSet<>();
   public Set<ExitListEntry> getExitListEntries() {
-    return new HashSet<ExitListEntry>(this.exitListEntries);
+    return new HashSet<>(this.exitListEntries);
+  }
+
+  public Set<ExitList.Entry> getEntries() {
+    return new HashSet<ExitList.Entry>(this.exitListEntries);
   }
 }
 
diff --git a/test/org/torproject/descriptor/impl/ExitListImplTest.java b/test/org/torproject/descriptor/impl/ExitListImplTest.java
new file mode 100644
index 0000000..7e4c4d6
--- /dev/null
+++ b/test/org/torproject/descriptor/impl/ExitListImplTest.java
@@ -0,0 +1,103 @@
+/* Copyright 2015 The Tor Project
+ * See LICENSE for licensing information */
+package org.torproject.descriptor.impl;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.util.List;
+import java.util.Map;
+
+import org.junit.Test;
+
+import org.torproject.descriptor.Descriptor;
+
+public class ExitListImplTest {
+
+  @Test()
+  public void testAnnotatedInput() throws Exception{
+    ExitListImpl result = new ExitListImpl((tordnselAnnotation + input)
+        .getBytes("US-ASCII"), fileName, false);
+    assertEquals("Expected one annotation.", 1,
+        result.getAnnotations().size());
+    assertEquals(tordnselAnnotation.substring(0, 18),
+        result.getAnnotations().get(0));
+    assertEquals(1441065722000L, result.getDownloadedMillis());
+    assertTrue("Unrecognized lines: " + result.getUnrecognizedLines(),
+        result.getUnrecognizedLines().isEmpty());
+    assertEquals("Found: " + result.getExitListEntries(),
+        5, result.getExitListEntries().size());
+    assertEquals("Found: " + result.getExitListEntries(),
+        5, result.getEntries().size());
+  }
+
+  @Test()
+  public void testMultipleExitAddresses() throws Exception{
+    ExitListImpl result = new ExitListImpl(
+        (tordnselAnnotation + multiExitAddressInput)
+            .getBytes("US-ASCII"), fileName, false);
+    assertTrue("Unrecognized lines: " + result.getUnrecognizedLines(),
+        result.getUnrecognizedLines().isEmpty());
+    Map<String, Long> map = result.getExitListEntries()
+        .iterator().next().getExitAddresses();
+    assertEquals("Found: " + map, 3, map.size());
+    assertTrue("Map: " + map, map.containsKey("81.7.17.171"));
+    assertTrue("Map: " + map, map.containsKey("81.7.17.172"));
+    assertTrue("Map: " + map, map.containsKey("81.7.17.173"));
+  }
+
+  @Test()
+  public void testInsufficientInput() throws Exception{
+    try{
+      ExitListImpl result =
+          new ExitListImpl((tordnselAnnotation + insufficientInput)
+              .getBytes("US-ASCII"), fileName, false);
+    }catch(DescriptorParseException dpe){
+      return;
+    }
+    fail("expected DescriptorParseException");
+  }
+
+  private static final String tordnselAnnotation = "@type tordnsel 1.0\n";
+  private static final String fileName = "2015-09-01-00-02-02";
+  private static final String insufficientInput = "Downloaded 2015-09-01 00:02:02\n"
+      + "ExitNode 0011BD2485AD45D984EC4159C88FC066E5E3300E\n"
+      + "Published 2015-08-31 16:17:30\n"
+      + "LastStatus 2015-08-31 17:03:18\n";
+  private static final String multiExitAddressInput =
+      "Downloaded 2015-09-01 00:02:02\n"
+      + "ExitNode 0011BD2485AD45D984EC4159C88FC066E5E3300E\n"
+      + "Published 2015-08-31 16:17:30\n"
+      + "LastStatus 2015-08-31 17:03:18\n"
+      + "ExitAddress 81.7.17.171 2015-08-31 18:09:52\n"
+      + "ExitAddress 81.7.17.172 2015-08-31 18:10:52\n"
+      + "ExitAddress 81.7.17.173 2015-08-31 18:11:52\n";
+  private static final String input = "Downloaded 2015-09-01 00:02:02\n"
+      + "ExitNode 0011BD2485AD45D984EC4159C88FC066E5E3300E\n"
+      + "Published 2015-08-31 16:17:30\n"
+      + "LastStatus 2015-08-31 17:03:18\n"
+      + "ExitAddress 162.247.72.201 2015-08-31 17:09:23\n"
+      + "ExitNode 0098C475875ABC4AA864738B1D1079F711C38287\n"
+      + "Published 2015-08-31 13:59:24\n"
+      + "LastStatus 2015-08-31 15:03:20\n"
+      + "ExitAddress 162.248.160.151 2015-08-31 15:07:27\n"
+      + "ExitNode 00C4B4731658D3B4987132A3F77100CFCB190D97\n"
+      + "Published 2015-08-31 17:47:52\n"
+      + "LastStatus 2015-08-31 18:03:17\n"
+      + "ExitAddress 81.7.17.171 2015-08-31 18:09:52\n"
+      + "ExitAddress 81.7.17.172 2015-08-31 18:10:52\n"
+      + "ExitAddress 81.7.17.173 2015-08-31 18:11:52\n"
+      + "ExitNode 00F2D93EBAF2F51D6EE4DCB0F37D91D72F824B16\n"
+      + "Published 2015-08-31 14:39:05\n"
+      + "LastStatus 2015-08-31 16:02:18\n"
+      + "ExitAddress 23.239.18.57 2015-08-31 16:06:07\n"
+      + "ExitNode 011B1D1E876B2C835D01FB9D407F2E00B28077F6\n"
+      + "Published 2015-08-31 05:14:35\n"
+      + "LastStatus 2015-08-31 06:03:29\n"
+      + "ExitAddress 104.131.51.150 2015-08-31 06:04:07\n";
+}
+
-- 
2.0.0.head

