From 75dd22510b98abb0f1a4b836e4ec6886363c0a5c Mon Sep 17 00:00:00 2001
From: zzz <zzz@mail.i2p>
Date: Fri, 22 Apr 2016 23:37:55 +0000
Subject: [PATCH] Addressbook: Fix changedest action - Implement adddest action
 - Logging improvements BFNS: Fix lookupAll() NPE - Fix addDestination() UOE -
 Support long property values DataHelper: Properties methods cleanup
 HostTxtEntry: Test improvements

---
 .../java/src/net/i2p/addressbook/Daemon.java  | 166 +++++++++-------
 .../src/net/i2p/addressbook/HostTxtEntry.java |  71 ++++---
 .../client/naming/BlockfileNamingService.java | 186 ++++++++++++++++--
 core/java/src/net/i2p/data/DataHelper.java    |  42 ++--
 4 files changed, 319 insertions(+), 146 deletions(-)

diff --git a/apps/addressbook/java/src/net/i2p/addressbook/Daemon.java b/apps/addressbook/java/src/net/i2p/addressbook/Daemon.java
index 243a61f2e8..d7df8fed33 100644
--- a/apps/addressbook/java/src/net/i2p/addressbook/Daemon.java
+++ b/apps/addressbook/java/src/net/i2p/addressbook/Daemon.java
@@ -202,40 +202,50 @@ public class Daemon {
                                     String polddest = hprops.getProperty(HostTxtEntry.PROP_OLDDEST);
                                     if (polddest != null) {
                                         Destination pod = new Destination(polddest);
-                                        // fill in oldDest for .txt naming service
-                                        if (isKnown && isTextFile)
-                                            oldDest = router.lookup(key);
-                                        if (pod.equals(dest)) {
-                                            // invalid
-                                            if (log != null)
-                                                log.append("Action: " + action + " failed because" +
-                                                           " identical old and new destinations for " + key +
-                                                           " from " + addressbook.getLocation());
-                                            invalid++;
-                                            continue;
-                                        } else if (!isKnown) {
+                                        List<Destination> pod2 = router.lookupAll(key);
+                                        if (pod2 == null) {
                                             // we didn't know it before, so we'll add it
-                                        } else if (dest.equals(oldDest)) {
+                                            // TODO check inner sig anyway?
+                                        } else if (pod2.contains(dest)) {
                                             // we knew it before, with the same dest
                                             old++;
                                             continue;
-                                        } else if (pod.equals(oldDest)) {
+                                        } else if (pod2.contains(pod)) {
                                             // checks out, so verify the inner sig
                                             if (!he.hasValidInnerSig()) {
                                                 if (log != null)
                                                     log.append("Action: " + action + " failed because" +
                                                                " inner signature for key " + key +
                                                                " failed" +
-                                                               " from " + addressbook.getLocation());
+                                                               ". From: " + addressbook.getLocation());
                                                 invalid++;
                                                 continue;
                                             }
                                             // TODO Requires NamingService support
                                             // if (isTextFile), do we replace or not? check sigType.isAvailable()
-                                            // router.addAltDest(dest)
-                                            if (log != null)
-                                                log.append("Action: " + action + " unimplemented" +
-                                                           " from " + addressbook.getLocation());
+                                            boolean success = router.addDestination(key, dest, props);
+                                            if (log != null) {
+                                                if (success)
+                                                    log.append("Additional address for " + key +
+                                                               " added to address book. From: " + addressbook.getLocation());
+                                                else
+                                                    log.append("Failed to add additional address for " + key +
+                                                               " From: " + addressbook.getLocation());
+                                            }
+                                            // now update the published addressbook
+                                            // ditto
+                                            if (published != null) {
+                                                if (publishedNS == null)
+                                                    publishedNS = new SingleFileNamingService(I2PAppContext.getGlobalContext(), published.getAbsolutePath());
+                                                success = publishedNS.addDestination(key, dest, props);
+                                                if (log != null && !success)
+                                                    log.append("Add to published address book " + published.getAbsolutePath() + " failed for " + key);
+                                            }
+                                            nnew++;
+                                            continue;
+                                        } else {
+                                            // mismatch, disallow
+                                            logMismatch(log, action, key, pod2, he.getDest(), addressbook);
                                             invalid++;
                                             continue;
                                         }
@@ -261,18 +271,14 @@ public class Daemon {
                                             // checks out, so we'll add the new one
                                         } else {
                                             // mismatch, disallow
-                                            if (log != null)
-                                                log.append("Action: " + action + " failed because" +
-                                                           " destination for old name " + poldname +
-                                                           " does not match" +
-                                                           " from " + addressbook.getLocation());
+                                            logMismatch(log, action, key, pod, he.getDest(), addressbook);
                                             invalid++;
                                             continue;
                                         }
                                     } else {
                                         if (log != null)
                                             log.append("Action: " + action + " failed, missing required parameters" +
-                                                       " from " + addressbook.getLocation());
+                                                       ". From: " + addressbook.getLocation());
                                         invalid++;
                                         continue;
                                     }
@@ -292,7 +298,7 @@ public class Daemon {
                                                 log.append("Action: " + action + " failed because" +
                                                            " old name " + poldname +
                                                            " is invalid" +
-                                                           " from " + addressbook.getLocation());
+                                                           ". From: " + addressbook.getLocation());
                                             invalid++;
                                             continue;
                                         }
@@ -300,6 +306,7 @@ public class Daemon {
                                         List<Destination> pod2 = router.lookupAll(poldname);
                                         if (pod2 == null) {
                                             // we didn't have the old name
+                                            // TODO check inner sig anyway?
                                         } else if (pod2.contains(pod)) {
                                             // checks out, so verify the inner sig
                                             if (!he.hasValidInnerSig()) {
@@ -307,24 +314,20 @@ public class Daemon {
                                                     log.append("Action: " + action + " failed because" +
                                                                " inner signature for old name " + poldname +
                                                                " failed" +
-                                                               " from " + addressbook.getLocation());
+                                                               ". From: " + addressbook.getLocation());
                                                 invalid++;
                                                 continue;
                                             }
                                         } else {
                                             // mismatch, disallow
-                                            if (log != null)
-                                                log.append("Action: " + action + " failed because" +
-                                                           " destination for old name " + poldname +
-                                                           " does not match provided" +
-                                                           " from " + addressbook.getLocation());
+                                            logMismatch(log, action, key, pod2, polddest, addressbook);
                                             invalid++;
                                             continue;
                                         }
                                     } else {
                                         if (log != null)
                                             log.append("Action: " + action + " failed, missing required parameters" +
-                                                       " from " + addressbook.getLocation());
+                                                       ". From: " + addressbook.getLocation());
                                         invalid++;
                                         continue;
                                     }
@@ -335,37 +338,44 @@ public class Daemon {
                                     String polddest = hprops.getProperty(HostTxtEntry.PROP_OLDDEST);
                                     if (polddest != null) {
                                         Destination pod = new Destination(polddest);
-                                        // fill in oldDest for .txt naming service
-                                        if (isKnown && isTextFile)
-                                            oldDest = router.lookup(key);
-                                        if (!isKnown) {
+                                        List<Destination> pod2 = router.lookupAll(key);
+                                        if (pod2 == null) {
                                             // we didn't have the old name
-                                        } else if (pod.equals(oldDest)) {
+                                            // TODO check inner sig anyway?
+                                        } else if (pod2.contains(dest)) {
+                                            // we already have the new dest
+                                            old++;
+                                            continue;
+                                        } else if (pod2.contains(pod)) {
                                             // checks out, so verify the inner sig
                                             if (!he.hasValidInnerSig()) {
                                                 if (log != null)
                                                     log.append("Action: " + action + " failed because" +
                                                                " inner signature for key " + key +
                                                                " failed" +
-                                                               " from " + addressbook.getLocation());
+                                                               ". From: " + addressbook.getLocation());
                                                 invalid++;
                                                 continue;
                                             }
+                                            if (log != null) {
+                                                if (pod2.size() == 1)
+                                                    log.append("Changing destination for " + key +
+                                                               ". From: " + addressbook.getLocation());
+                                                else
+                                                    log.append("Replacing " + pod2.size() + " destinations for " + key +
+                                                               ". From: " + addressbook.getLocation());
+                                            }
                                             // TODO set flag to do non-putifabsent for published below
                                         } else {
                                             // mismatch, disallow
-                                            if (log != null)
-                                                log.append("Action: " + action + " failed because" +
-                                                           " destination for key " + key +
-                                                           " does not match provided" +
-                                                           " from " + addressbook.getLocation());
+                                            logMismatch(log, action, key, pod2, polddest, addressbook);
                                             invalid++;
                                             continue;
                                         }
                                     } else {
                                         if (log != null)
                                             log.append("Action: " + action + " failed, missing required parameters" +
-                                                       " from " + addressbook.getLocation());
+                                                       ". From: " + addressbook.getLocation());
                                         invalid++;
                                         continue;
                                     }
@@ -393,11 +403,11 @@ public class Daemon {
                                                 if (success)
                                                     log.append("Removed: " + poldname +
                                                                " to be replaced with " + key +
-                                                               " from " + addressbook.getLocation());
+                                                               ". From: " + addressbook.getLocation());
                                                 else
                                                     log.append("Remove failed for: " + poldname +
                                                                " to be replaced with " + key +
-                                                               " from " + addressbook.getLocation());
+                                                               ". From: " + addressbook.getLocation());
                                             }
                                             // now update the published addressbook
                                             if (published != null) {
@@ -409,18 +419,13 @@ public class Daemon {
                                             }
                                         } else {
                                             // mismatch, disallow
-                                            if (log != null)
-                                                log.append("Action: " + action + " failed because" +
-                                                           " destination for old name " + poldname +
-                                                           " does not match new name " + key +
-                                                           " from " + addressbook.getLocation());
-                                            invalid++;
+                                            logMismatch(log, action, key, pod, he.getDest(), addressbook);
                                             continue;
                                         }
                                     } else {
                                         if (log != null)
                                             log.append("Action: " + action + " failed, missing required parameters" +
-                                                       " from " + addressbook.getLocation());
+                                                       ". From: " + addressbook.getLocation());
                                         invalid++;
                                         continue;
                                     }
@@ -446,11 +451,11 @@ public class Daemon {
                                                 if (success)
                                                     log.append("Removed: " + poldname +
                                                                " as requested" +
-                                                               " from " + addressbook.getLocation());
+                                                               ". From: " + addressbook.getLocation());
                                                 else
                                                     log.append("Remove failed for: " + poldname +
                                                                " as requested" +
-                                                               " from " + addressbook.getLocation());
+                                                               ". From: " + addressbook.getLocation());
                                             }
                                             // now update the published addressbook
                                             if (published != null) {
@@ -462,17 +467,13 @@ public class Daemon {
                                             }
                                         } else if (pod2 != null) {
                                             // mismatch, disallow
-                                            if (log != null)
-                                                log.append("Action: " + action + " failed because" +
-                                                           " destination for " + poldname +
-                                                           " does not match" +
-                                                           " from " + addressbook.getLocation());
+                                            logMismatch(log, action, key, pod2, polddest, addressbook);
                                             invalid++;
                                         }
                                     } else {
                                         if (log != null)
                                             log.append("Action: " + action + " failed, missing required parameters" +
-                                                       " from " + addressbook.getLocation());
+                                                       ". From: " + addressbook.getLocation());
                                         invalid++;
                                     }
                                     continue;
@@ -500,11 +501,11 @@ public class Daemon {
                                                     if (success)
                                                         log.append("Removed: " + poldname +
                                                                    " as requested" +
-                                                                   " from " + addressbook.getLocation());
+                                                                   ". From: " + addressbook.getLocation());
                                                     else
                                                         log.append("Remove failed for: " + poldname +
                                                                    " as requested" +
-                                                                   " from " + addressbook.getLocation());
+                                                                   ". From: " + addressbook.getLocation());
                                                 }
                                                 // now update the published addressbook
                                                 if (published != null) {
@@ -516,11 +517,7 @@ public class Daemon {
                                                 }
                                             } else if (pod2 != null) {
                                                 // mismatch, disallow
-                                                if (log != null)
-                                                    log.append("Action: " + action + " failed because" +
-                                                               " destination for " + poldname +
-                                                               " does not match" +
-                                                               " from " + addressbook.getLocation());
+                                                logMismatch(log, action, key, pod2, polddest, addressbook);
                                                 invalid++;
                                             }
                                         }
@@ -546,11 +543,11 @@ public class Daemon {
                                                 if (success)
                                                     log.append("Removed: " + rev +
                                                                " as requested" +
-                                                               " from " + addressbook.getLocation());
+                                                               ". From: " + addressbook.getLocation());
                                                 else
                                                     log.append("Remove failed for: " + rev +
                                                                " as requested" +
-                                                               " from " + addressbook.getLocation());
+                                                               ". From: " + addressbook.getLocation());
                                             }
                                             // now update the published addressbook
                                             if (published != null) {
@@ -564,7 +561,7 @@ public class Daemon {
                                     } else {
                                         if (log != null)
                                             log.append("Action: " + action + " failed, missing required parameters" +
-                                                       " from " + addressbook.getLocation());
+                                                       ". From: " + addressbook.getLocation());
                                         invalid++;
                                     }
                                     continue;
@@ -575,7 +572,7 @@ public class Daemon {
                                 } else {
                                     if (log != null)
                                         log.append("Action: " + action + " unrecognized" +
-                                                   " from " + addressbook.getLocation());
+                                                   ". From: " + addressbook.getLocation());
                                     invalid++;
                                     continue;
                                 }
@@ -602,7 +599,7 @@ public class Daemon {
                                 knownNames.add(key);
                             nnew++;
                         } else if (log != null) {
-                            log.append("Bad hostname " + key + " from "
+                            log.append("Bad hostname " + key + ". From: "
                                    + addressbook.getLocation());
                             invalid++;
                         }        
@@ -612,7 +609,7 @@ public class Daemon {
                         if (isTextFile)
                             oldDest = router.lookup(key);
                         if (oldDest != null && !oldDest.toBase64().equals(entry.getValue())) {
-                            log.append("Conflict for " + key + " from "
+                            log.append("Conflict for " + key + ". From: "
                                        + addressbook.getLocation()
                                        + ". Destination in remote address book is "
                                        + entry.getValue());
@@ -645,6 +642,25 @@ public class Daemon {
         subscriptions.write();
     }
 
+    private static void logMismatch(Log log, String action, String name, List<Destination> dests,
+                                    String olddest, AddressBook addressbook) {
+        if (log != null) {
+            StringBuilder buf = new StringBuilder(16);
+            final int sz = dests.size();
+            for (int i = 0; i < sz; i++) {
+                buf.append(dests.get(i).toBase64().substring(0, 6));
+                if (i != sz - 1)
+                    buf.append(", ");
+            }
+            log.append("Action: " + action + " failed because" +
+                       " destinations for " + name +
+                       " (" + buf + ')' +
+                       " do not include" +
+                       " (" + olddest.substring(0, 6) + ')' +
+                       ". From: " + addressbook.getLocation());
+        }
+    }
+
     /**
      * Run an update, using the Map settings to provide the parameters.
      * 
diff --git a/apps/addressbook/java/src/net/i2p/addressbook/HostTxtEntry.java b/apps/addressbook/java/src/net/i2p/addressbook/HostTxtEntry.java
index 0852c52884..d3706b6622 100644
--- a/apps/addressbook/java/src/net/i2p/addressbook/HostTxtEntry.java
+++ b/apps/addressbook/java/src/net/i2p/addressbook/HostTxtEntry.java
@@ -19,6 +19,7 @@ import net.i2p.util.OrderedProperties;
 import java.io.File;
 import java.io.OutputStreamWriter;
 import java.io.StringWriter;
+import java.util.Arrays;
 import net.i2p.data.Base32;
 import net.i2p.data.PrivateKeyFile;
 import net.i2p.data.SigningPrivateKey;
@@ -398,12 +399,31 @@ class HostTxtEntry {
         props.setProperty(sigprop, s.toBase64());
     }
 
+    /**
+     *  Usage: HostTxtEntry [-i] [-x] [hostname.i2p] [key=val]...
+     */
     public static void main(String[] args) throws Exception {
-        int astart = 0;
-        if (args.length > 0 && args[0].equals("-i"))
-            astart++;
+        boolean inner = false;
+        boolean remove = false;
+        if (args.length > 0 && args[0].equals("-i")) {
+            inner = true;
+            args = Arrays.copyOfRange(args, 1, args.length);
+        }
+        if (args.length > 0 && args[0].equals("-x")) {
+            remove = true;
+            args = Arrays.copyOfRange(args, 1, args.length);
+        }
+        String host;
+        if (args.length > 0 && args[0].endsWith(".i2p")) {
+            host = args[0];
+            args = Arrays.copyOfRange(args, 1, args.length);
+        } else {
+            byte[] rand = new byte[5];
+            RandomSource.getInstance().nextBytes(rand);
+            host = Base32.encode(rand) + ".i2p";
+        }
         OrderedProperties props = new OrderedProperties();
-        for (int i = astart; i < args.length; i++) {
+        for (int i = 0; i < args.length; i++) {
             int eq = args[i].indexOf("=");
             props.setProperty(args[i].substring(0, eq), args[i].substring(eq + 1));
         }
@@ -412,28 +432,25 @@ class HostTxtEntry {
         File f = new File("tmp-eepPriv.dat");
         PrivateKeyFile pkf = new PrivateKeyFile(f);
         pkf.createIfAbsent(SigType.EdDSA_SHA512_Ed25519);
-        f.delete();
+        //f.delete();
         PrivateKeyFile pkf2;
-        if (astart != 0) {
+        if (inner) {
             // inner
             File f2 = new File("tmp-eepPriv2.dat");
             pkf2 = new PrivateKeyFile(f2);
             pkf2.createIfAbsent(SigType.DSA_SHA1);
-            f2.delete();
+            //f2.delete();
             props.setProperty(PROP_OLDDEST, pkf2.getDestination().toBase64());
         } else {
             pkf2 = null;
         }
-        byte[] rand = new byte[5];
-        RandomSource.getInstance().nextBytes(rand);
-        String host = Base32.encode(rand) + ".i2p";
         HostTxtEntry he = new HostTxtEntry(host, pkf.getDestination().toBase64(), props);
         BufferedWriter out = new BufferedWriter(new OutputStreamWriter(System.out));
         //out.write("Before signing:\n");
         //he.write(out);
         //out.flush();
         SigningPrivateKey priv = pkf.getSigningPrivKey();
-        if (astart != 0) {
+        if (inner) {
             SigningPrivateKey priv2 = pkf2.getSigningPrivKey();
             he.signInner(priv2);
             //out.write("After signing inner:\n");
@@ -443,7 +460,7 @@ class HostTxtEntry {
         //out.write("After signing:\n");
         he.write(out);
         out.flush();
-        if (astart > 0 && !he.hasValidInnerSig())
+        if (inner && !he.hasValidInnerSig())
             throw new IllegalStateException("Inner fail 1");
         if (!he.hasValidSig())
             throw new IllegalStateException("Outer fail 1");
@@ -455,25 +472,27 @@ class HostTxtEntry {
         String line = sw.toString();
         line = line.substring(line.indexOf(PROPS_SEPARATOR) + 2);
         HostTxtEntry he2 = new HostTxtEntry(host, pkf.getDestination().toBase64(), line);
-        if (astart > 0 && !he2.hasValidInnerSig())
+        if (inner && !he2.hasValidInnerSig())
             throw new IllegalStateException("Inner fail 2");
         if (!he2.hasValidSig())
             throw new IllegalStateException("Outer fail 2");
 
         // 'remove' tests (corrupts earlier sigs)
-        he.getProps().remove(PROP_SIG);
-        he.signRemove(priv);
-        //out.write("Remove entry:\n");
-        sw = new StringWriter(1024);
-        buf = new BufferedWriter(sw);
-        he.writeRemove(buf);
-        buf.flush();
-        out.write(sw.toString());
-        out.flush();
-        line = sw.toString().substring(2).trim();
-        HostTxtEntry he3 = new HostTxtEntry(line);
-        if (!he3.hasValidRemoveSig())
-            throw new IllegalStateException("Remove verify fail");
+        if (remove) {
+            he.getProps().remove(PROP_SIG);
+            he.signRemove(priv);
+            //out.write("Remove entry:\n");
+            sw = new StringWriter(1024);
+            buf = new BufferedWriter(sw);
+            he.writeRemove(buf);
+            buf.flush();
+            out.write(sw.toString());
+            out.flush();
+            line = sw.toString().substring(2).trim();
+            HostTxtEntry he3 = new HostTxtEntry(line);
+            if (!he3.hasValidRemoveSig())
+                throw new IllegalStateException("Remove verify fail");
+        }
 
         //out.write("Test passed\n");
         //out.flush();
diff --git a/core/java/src/net/i2p/client/naming/BlockfileNamingService.java b/core/java/src/net/i2p/client/naming/BlockfileNamingService.java
index cd1460d389..2ba0c7b233 100644
--- a/core/java/src/net/i2p/client/naming/BlockfileNamingService.java
+++ b/core/java/src/net/i2p/client/naming/BlockfileNamingService.java
@@ -10,6 +10,7 @@ package net.i2p.client.naming;
 import java.io.BufferedReader;
 import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
+import java.io.EOFException;
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.InputStreamReader;
@@ -135,6 +136,7 @@ public class BlockfileNamingService extends DummyNamingService {
     
     private static final String DUMMY = "";
     private static final int NEGATIVE_CACHE_SIZE = 32;
+    private static final int MAX_VALUE_LENGTH = 4096;
 
     /**
      *  Opens the database at hostsdb.blockfile or creates a new
@@ -852,18 +854,17 @@ public class BlockfileNamingService extends DummyNamingService {
                 try {
                     DestEntry de = getEntry(list, key);
                     if (de != null) {
-                        int sz = de.destList.size();
-                        // if any are invalid, assume they all are
-                        boolean invalid = false;
-                        for (int i = 0; i < sz; i++) {
-                            if (!validate(key, de, listname))
-                                invalid = true;
-                        }
-                        if (invalid)
+                        if (!validate(key, de, listname))
                             continue;
-                        rv = de.destList;
-                        if (storedOptions != null)
-                            storedOptions.addAll(de.propsList);
+                        if (de.destList != null) {
+                            rv = de.destList;
+                            if (storedOptions != null)
+                                storedOptions.addAll(de.propsList);
+                        } else {
+                            rv = Collections.singletonList(de.dest);
+                            if (storedOptions != null)
+                                storedOptions.add(de.props);
+                        }
                         break;
                     }
                 } catch (IOException ioe) {
@@ -1491,10 +1492,10 @@ public class BlockfileNamingService extends DummyNamingService {
             // For now, non-DSA at the front, DSA at the end
             SigType type = d.getSigningPublicKey().getType();
             if (type != SigType.DSA_SHA1 && type.isAvailable()) {
-                dests.add(0, d);
+                newDests.add(0, d);
                 storedOptions.add(0, options);
             } else {
-                dests.add(d);
+                newDests.add(d);
                 storedOptions.add(options);
             }
             return put(hostname, newDests, storedOptions, false);
@@ -1562,6 +1563,12 @@ public class BlockfileNamingService extends DummyNamingService {
                      de != null &&
                      de.dest != null &&
                      de.dest.getPublicKey() != null;
+        if (_isVersion4 && rv && de.destList != null) {
+            // additional checks for multi-dest
+            rv = de.propsList != null &&
+                 de.destList.size() == de.propsList.size() &&
+                 !de.destList.contains(null);
+        }
         if ((!rv) && (!_readOnly))
             _invalid.add(new InvalidEntry(key, listname));
         return rv;
@@ -1706,13 +1713,26 @@ public class BlockfileNamingService extends DummyNamingService {
      *  and is serialized in that order.
      */
     private static class DestEntry {
-        /** may be null */
+        /** May be null.
+         *  If more than one dest, contains the first props.
+         */
         public Properties props;
-        /** may not be null */
+
+        /** May not be null.
+         *  If more than one dest, contains the first dest.
+         */
         public Destination dest;
-        /** may be null - v4 only - same size as destList - may contain null entries */
+
+        /** May be null - v4 only - same size as destList - may contain null entries
+         *  Only non-null if more than one dest.
+         *  First entry always equal to props.
+         */
         public List<Properties> propsList;
-        /** may be null - v4 only - same size as propsList */
+
+        /** May be null - v4 only - same size as propsList
+         *  Only non-null if more than one dest.
+         *  First entry always equal to dest.
+         */
         public List<Destination> destList;
 
         @Override
@@ -1796,7 +1816,7 @@ public class BlockfileNamingService extends DummyNamingService {
                         d = de.destList.get(i);
                     }
                     try {
-                        DataHelper.writeProperties(baos, p, true, false);
+                        writeProperties(baos, p);
                     } catch (DataFormatException dfe) {
                         logError("DB Write Fail - properties too big?", dfe);
                         baos.write(new byte[2]);
@@ -1819,7 +1839,7 @@ public class BlockfileNamingService extends DummyNamingService {
                 int sz = bais.read() & 0xff;
                 if (sz <= 0)
                     throw new DataFormatException("bad dest count " + sz);
-                rv.props = DataHelper.readProperties(bais);
+                rv.props = readProperties(bais);
                 rv.dest = Destination.create(bais);
                 if (sz > 1) {
                     rv.propsList = new ArrayList<Properties>(sz);
@@ -1827,7 +1847,7 @@ public class BlockfileNamingService extends DummyNamingService {
                     rv.propsList.add(rv.props);
                     rv.destList.add(rv.dest);
                     for (int i = 1; i < sz; i++) {
-                        rv.propsList.add(DataHelper.readProperties(bais));
+                        rv.propsList.add(readProperties(bais));
                         rv.destList.add(Destination.create(bais));
                     }
                 }
@@ -1842,6 +1862,132 @@ public class BlockfileNamingService extends DummyNamingService {
         }
     }
 
+    /**
+     * Same as DataHelper.writeProperties, UTF-8, unsorted,
+     * except that values may up to 4K bytes.
+     *
+     * @param props source may be null
+     * @throws DataFormatException if any key string is over 255 bytes long,
+     *                             if any value string is over 4096 bytes long, or if the total length
+     *                             (not including the two length bytes) is greater than 65535 bytes.
+     * @since 0.9.26
+     */
+    private static void writeProperties(ByteArrayOutputStream rawStream, Properties p) 
+            throws DataFormatException, IOException {
+        if (p != null && !p.isEmpty()) {
+            ByteArrayOutputStream baos = new ByteArrayOutputStream(p.size() * 32);
+            for (Map.Entry<Object, Object> entry : p.entrySet()) {
+                String key = (String) entry.getKey();
+                String val = (String) entry.getValue();
+                DataHelper.writeStringUTF8(baos, key);
+                baos.write('=');
+                writeLongStringUTF8(baos, val);
+                baos.write(';');
+            }
+            if (baos.size() > 65535)
+                throw new DataFormatException("Properties too big (65535 max): " + baos.size());
+            byte propBytes[] = baos.toByteArray();
+            DataHelper.writeLong(rawStream, 2, propBytes.length);
+            rawStream.write(propBytes);
+        } else {
+            DataHelper.writeLong(rawStream, 2, 0);
+        }
+    }
+
+    /**
+     * Same as DataHelper.readProperties, UTF-8, unsorted,
+     * except that values may up to 4K bytes.
+     *
+     * Throws DataFormatException on duplicate key
+     *
+     * @param rawStream stream to read the mapping from
+     * @throws DataFormatException if the format is invalid
+     * @throws IOException if there is a problem reading the data
+     * @return a Properties
+     * @since 0.9.26
+     */
+    public static Properties readProperties(ByteArrayInputStream in) 
+        throws DataFormatException, IOException {
+        Properties props = new Properties();
+        int size = (int) DataHelper.readLong(in, 2);
+        // this doesn't prevent reading past the end on corruption
+        int ignore = in.available() - size;
+        while (in.available() > ignore) {
+            String key = DataHelper.readString(in);
+            int b = in.read();
+            if (b != '=')
+                throw new DataFormatException("Bad key " + b);
+            String val = readLongString(in);
+            b = in.read();
+            if (b != ';')
+                throw new DataFormatException("Bad value");
+            Object old = props.put(key, val);
+            if (old != null)
+                throw new DataFormatException("Duplicate key " + key);
+        }
+        return props;
+    }
+
+    /**
+     * Same as DataHelper.writeStringUTF8, except that
+     * strings up to 4K bytes are allowed.
+     * Format is: one-byte length + data, or 0xff + two-byte length + data
+     *
+     * @param out stream to write string
+     * @param string to write out: null strings are valid, but strings of excess length will
+     *               cause a DataFormatException to be thrown
+     * @throws DataFormatException if the string is not valid
+     * @throws IOException if there is an IO error writing the string
+     */
+    private static void writeLongStringUTF8(ByteArrayOutputStream out, String string) 
+        throws DataFormatException, IOException {
+        if (string == null) {
+            out.write(0);
+        } else {
+            byte[] raw = string.getBytes("UTF-8");
+            int len = raw.length;
+            if (len >= 255) {
+                if (len > MAX_VALUE_LENGTH)
+                    throw new DataFormatException(MAX_VALUE_LENGTH + " max, but this is "
+                                              + len + " [" + string + "]");
+                out.write(0xff);
+                DataHelper.writeLong(out, 2, len);
+            } else {
+                out.write(len);
+            }
+            out.write(raw);
+        }
+    }
+
+    /**
+     * Same as DataHelper.readString, except that
+     * strings up to 4K bytes are allowed.
+     * Format is: one-byte length + data, or 0xff + two-byte length + data
+     *
+     * @param in stream to read from
+     * @throws DataFormatException if the stream doesn't contain a validly formatted string
+     * @throws EOFException if there aren't enough bytes to read the string
+     * @throws IOException if there is an IO error reading the string
+     * @return UTF-8 string
+     */
+    private static String readLongString(ByteArrayInputStream in) throws DataFormatException, IOException {
+        int size = in.read();
+        if (size < 0)
+            throw new EOFException("EOF reading string");
+        if (size == 0xff) {
+            size = (int) DataHelper.readLong(in, 2);
+            if (size > MAX_VALUE_LENGTH)
+                throw new DataFormatException(MAX_VALUE_LENGTH + " max, but this is " + size);
+        }
+        if (size == 0)
+            return "";
+        byte raw[] = new byte[size];
+        int read = DataHelper.read(in, raw);
+        if (read != size)
+            throw new EOFException("EOF reading string");
+        return new String(raw, "UTF-8");
+    }
+
     /**
      *  Used to store entries that need deleting
      */
diff --git a/core/java/src/net/i2p/data/DataHelper.java b/core/java/src/net/i2p/data/DataHelper.java
index dcd1ce71cf..240ff42fbb 100644
--- a/core/java/src/net/i2p/data/DataHelper.java
+++ b/core/java/src/net/i2p/data/DataHelper.java
@@ -56,8 +56,6 @@ import net.i2p.util.Translate;
  * @author jrandom
  */
 public class DataHelper {
-    private static final byte[] EQUAL_BYTES = getUTF8("=");
-    private static final byte[] SEMICOLON_BYTES = getUTF8(";");
 
     /**
      *  Map of String to itself to cache common
@@ -148,22 +146,18 @@ public class DataHelper {
         int read = read(rawStream, data);
         if (read != size) throw new DataFormatException("Not enough data to read the properties, expected " + size + " but got " + read);
         ByteArrayInputStream in = new ByteArrayInputStream(data);
-        byte eqBuf[] = new byte[EQUAL_BYTES.length];
-        byte semiBuf[] = new byte[SEMICOLON_BYTES.length];
         while (in.available() > 0) {
             String key = readString(in);
             String cached = _propertiesKeyCache.get(key);
             if (cached != null)
                 key = cached;
-            read = read(in, eqBuf);
-            if ((read != eqBuf.length) || (!eq(eqBuf, EQUAL_BYTES))) {
+            int b = in.read();
+            if (b != '=')
                 throw new DataFormatException("Bad key");
-            }
             String val = readString(in);
-            read = read(in, semiBuf);
-            if ((read != semiBuf.length) || (!eq(semiBuf, SEMICOLON_BYTES))) {
+            b = in.read();
+            if (b != ';')
                 throw new DataFormatException("Bad value");
-            }
             Object old = props.put(key, val);
             if (old != null)
                 throw new DataFormatException("Duplicate key " + key);
@@ -182,7 +176,7 @@ public class DataHelper {
      * Properties from the defaults table of props (if any) are not written out by this method.
      *
      * @param rawStream stream to write to
-     * @param props properties to write out
+     * @param props properties to write out, may be null
      * @throws DataFormatException if there is not enough valid data to write out,
      *                             or a length limit is exceeded
      * @throws IOException if there is an IO error writing out the data
@@ -239,7 +233,7 @@ public class DataHelper {
      */
     public static void writeProperties(OutputStream rawStream, Properties props, boolean utf8, boolean sort) 
             throws DataFormatException, IOException {
-        if (props != null) {
+        if (props != null && !props.isEmpty()) {
             Properties p;
             if (sort) {
                 p = new OrderedProperties();
@@ -255,12 +249,12 @@ public class DataHelper {
                     writeStringUTF8(baos, key);
                 else
                     writeString(baos, key);
-                baos.write(EQUAL_BYTES);
+                baos.write('=');
                 if (utf8)
                     writeStringUTF8(baos, val);
                 else
                     writeString(baos, val);
-                baos.write(SEMICOLON_BYTES);
+                baos.write(';');
             }
             if (baos.size() > 65535)
                 throw new DataFormatException("Properties too big (65535 max): " + baos.size());
@@ -301,9 +295,9 @@ public class DataHelper {
                 String key = (String) entry.getKey();
                 String val = (String) entry.getValue();
                 writeStringUTF8(baos, key);
-                baos.write(EQUAL_BYTES);
+                baos.write('=');
                 writeStringUTF8(baos, val);
-                baos.write(SEMICOLON_BYTES);
+                baos.write(';');
             }
             if (baos.size() > 65535)
                 throw new DataFormatException("Properties too big (65535 max): " + baos.size());
@@ -335,8 +329,6 @@ public class DataHelper {
         int size = (int)fromLong(source, offset, 2);
         offset += 2;
         ByteArrayInputStream in = new ByteArrayInputStream(source, offset, size);
-        byte eqBuf[] = new byte[EQUAL_BYTES.length];
-        byte semiBuf[] = new byte[SEMICOLON_BYTES.length];
         while (in.available() > 0) {
             String key;
             try {
@@ -344,20 +336,18 @@ public class DataHelper {
                 String cached = _propertiesKeyCache.get(key);
                 if (cached != null)
                     key = cached;
-                int read = read(in, eqBuf);
-                if ((read != eqBuf.length) || (!eq(eqBuf, EQUAL_BYTES))) {
+                int b = in.read();
+                if (b != '=')
                     throw new DataFormatException("Bad key");
-                }
             } catch (IOException ioe) {
                 throw new DataFormatException("Bad key", ioe);
             }
             String val;
             try {
                 val = readString(in);
-                int read = read(in, semiBuf);
-                if ((read != semiBuf.length) || (!eq(semiBuf, SEMICOLON_BYTES))) {
+                int b = in.read();
+                if (b != ';')
                     throw new DataFormatException("Bad value");
-                }
             } catch (IOException ioe) {
                 throw new DataFormatException("Bad value", ioe);
             }
@@ -910,8 +900,9 @@ public class DataHelper {
      *               cause a DataFormatException to be thrown
      * @throws DataFormatException if the string is not valid
      * @throws IOException if there is an IO error writing the string
+     * @since public since 0.9.26
      */
-    private static void writeStringUTF8(OutputStream out, String string) 
+    public static void writeStringUTF8(OutputStream out, String string) 
         throws DataFormatException, IOException {
         if (string == null) {
             out.write((byte) 0);
@@ -936,6 +927,7 @@ public class DataHelper {
      * @return boolean value, or null
      * @deprecated unused
      */
+    @Deprecated
     public static Boolean readBoolean(InputStream in) throws DataFormatException, IOException {
         int val = in.read();
         switch (val) {
-- 
GitLab