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