package net.i2p.data;
/*
* free (adj.): unencumbered; not under the control of others
* Written by jrandom in 2003 and released into the public domain
* with no warranty of any kind, either expressed or implied.
* It probably won't make your computer catch on fire, or eat
* your children, but it might. Use at your own risk.
*
*/
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.EOFException;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.UnsupportedEncodingException;
import java.math.BigInteger;
import java.security.MessageDigest;
import java.text.DecimalFormat;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Pattern;
import java.util.zip.Deflater;
import net.i2p.I2PAppContext;
import net.i2p.util.ByteCache;
import net.i2p.util.FileUtil;
import net.i2p.util.OrderedProperties;
import net.i2p.util.ReusableGZIPInputStream;
import net.i2p.util.ReusableGZIPOutputStream;
import net.i2p.util.SecureFileOutputStream;
import net.i2p.util.SystemVersion;
import net.i2p.util.Translate;
/**
* Defines some simple IO routines for dealing with marshalling data structures
*
* @author jrandom
*/
public class DataHelper {
/** See storeProps(). 600-750 ms on RPi. */
private static final boolean SHOULD_SYNC = !(SystemVersion.isAndroid() || SystemVersion.isARM());
/**
* Map of String to itself to cache common
* keys in RouterInfo, RouterAddress, and BlockfileNamingService properties.
* Reduces Object proliferation caused by frequent deserialization.
* @since 0.8.12
*/
private static final Map<String, String> _propertiesKeyCache;
static {
String keys[] = {
// NTCP/SSU RouterAddress options
"cost", "host", "port",
// SSU RouterAddress options
"key", "mtu",
"ihost0", "iport0", "ikey0", "itag0", "iexp0",
"ihost1", "iport1", "ikey1", "itag1", "iexp1",
"ihost2", "iport2", "ikey2", "itag2", "iexp2",
// RouterInfo options
"caps", "coreVersion", "netId", "router.version",
"netdb.knownLeaseSets", "netdb.knownRouters",
"stat_bandwidthReceiveBps.60m",
"stat_bandwidthSendBps.60m",
"stat_tunnel.buildClientExpire.60m",
"stat_tunnel.buildClientReject.60m",
"stat_tunnel.buildClientSuccess.60m",
"stat_tunnel.buildExploratoryExpire.60m",
"stat_tunnel.buildExploratoryReject.60m",
"stat_tunnel.buildExploratorySuccess.60m",
"stat_tunnel.participatingTunnels.60m",
"stat_uptime",
"family", "family.key", "family.sig",
// BlockfileNamingService
"version", "created", "upgraded", "lists",
"a", "m", "s", "v"
};
_propertiesKeyCache = new HashMap<String, String>(keys.length);
for (int i = 0; i < keys.length; i++) {
_propertiesKeyCache.put(keys[i], keys[i]);
}
}
private static final Pattern ILLEGAL_KEY = Pattern.compile("[#=\r\n;]");
private static final Pattern ILLEGAL_VALUE = Pattern.compile("[#\r\n]");
/** Read a mapping from the stream, as defined by the I2P data structure spec,
* and store it into a Properties object.
*
* A mapping is a set of key / value pairs. It starts with a 2 byte Integer (ala readLong(rawStream, 2))
* defining how many bytes make up the mapping. After that comes that many bytes making
* up a set of UTF-8 encoded characters. The characters are organized as key=value;.
* The key is a String (ala readString(rawStream)) unique as a key within the current
* mapping that does not include the UTF-8 characters '=' or ';'. After the key
* comes the literal UTF-8 character '='. After that comes a String (ala readString(rawStream))
* for the value. Finally after that comes the literal UTF-8 character ';'. This key=value;
* is repeated until there are no more bytes (not characters!) left as defined by the
* first two byte integer.
*
* As of 0.9.18, 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 an OrderedProperties
*/
public static Properties readProperties(InputStream rawStream)
throws DataFormatException, IOException {
Properties props = new OrderedProperties();
readProperties(rawStream, props);
return props;
}
/**
* Ditto, load into an existing properties
*
* As of 0.9.18, throws DataFormatException on duplicate key
*
* @param props the Properties to load into
* @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 the parameter props
* @since 0.8.13
*/
public static Properties readProperties(InputStream rawStream, Properties props)
throws DataFormatException, IOException {
long size = readLong(rawStream, 2);
byte data[] = new byte[(int) size];
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);
while (in.available() > 0) {
String key = readString(in);
String cached = _propertiesKeyCache.get(key);
if (cached != null)
key = cached;
int b = in.read();
if (b != '=')
throw new DataFormatException("Bad key");
String val = readString(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;
}
/**
* Write a mapping to the stream, as defined by the I2P data structure spec,
* and store it into a Properties object. See readProperties for the format.
* Output is sorted by property name.
* Property keys and values must not contain '=' or ';', this is not checked and they are not escaped
* Keys and values must be 255 bytes or less,
* Formatted length must not exceed 65535 bytes
*
* 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, 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
*/
public static void writeProperties(OutputStream rawStream, Properties props)
throws DataFormatException, IOException {
writeProperties(rawStream, props, false);
}
/**
* Writes the props to the stream, sorted by property name.
* See readProperties() for the format.
* Property keys and values must not contain '=' or ';', this is not checked and they are not escaped
* Keys and values must be 255 bytes or less,
* Formatted length must not exceed 65535 bytes
*
* Properties from the defaults table of props (if any) are not written out by this method.
*
* jrandom disabled UTF-8 in mid-2004, for performance reasons,
* i.e. slow foo.getBytes("UTF-8")
* Re-enable it so we can pass UTF-8 tunnel names through the I2CP SessionConfig.
*
* Use utf8 = false for RouterAddress (fast, non UTF-8)
* Use utf8 = true for SessionConfig (slow, UTF-8)
* @param props source may be null
* @throws DataFormatException if a length limit is exceeded
*/
public static void writeProperties(OutputStream rawStream, Properties props, boolean utf8)
throws DataFormatException, IOException {
writeProperties(rawStream, props, utf8, props != null && !(props instanceof OrderedProperties));
}
/**
* Writes the props to the stream, sorted by property name if sort == true or
* if props is an OrderedProperties.
* See readProperties() for the format.
* Property keys and values must not contain '=' or ';', this is not checked and they are not escaped
* Keys and values must be 255 bytes or less,
* Formatted length must not exceed 65535 bytes
*
* Properties from the defaults table of props (if any) are not written out by this method.
*
* jrandom disabled UTF-8 in mid-2004, for performance reasons,
* i.e. slow foo.getBytes("UTF-8")
* Re-enable it so we can pass UTF-8 tunnel names through the I2CP SessionConfig.
*
* Use utf8 = false for RouterAddress (fast, non UTF-8)
* Use utf8 = true for SessionConfig (slow, UTF-8)
* @param props source may be null
* @param sort should we sort the properties? (set to false if already sorted, e.g. OrderedProperties)
* @throws DataFormatException if any string is over 255 bytes long, or if the total length
* (not including the two length bytes) is greater than 65535 bytes.
* @since 0.8.7
*/
public static void writeProperties(OutputStream rawStream, Properties props, boolean utf8, boolean sort)
throws DataFormatException, IOException {
if (props != null && !props.isEmpty()) {
Properties p;
if (sort) {
p = new OrderedProperties();
p.putAll(props);
} else {
p = props;
}
ByteArrayOutputStream baos = new ByteArrayOutputStream(p.size() * 64);
for (Map.Entry<Object, Object> entry : p.entrySet()) {
String key = (String) entry.getKey();
String val = (String) entry.getValue();
if (utf8)
writeStringUTF8(baos, key);
else
writeString(baos, key);
baos.write('=');
if (utf8)
writeStringUTF8(baos, val);
else
writeString(baos, val);
baos.write(';');
}
if (baos.size() > 65535)
throw new DataFormatException("Properties too big (65535 max): " + baos.size());
byte propBytes[] = baos.toByteArray();
writeLong(rawStream, 2, propBytes.length);
rawStream.write(propBytes);
} else {
writeLong(rawStream, 2, 0);
}
}
/*
* Writes the props to the byte array, sorted
* See readProperties() for the format.
* Property keys and values must not contain '=' or ';', this is not checked and they are not escaped
* Keys and values must be 255 bytes or less,
* Formatted length must not exceed 65535 bytes
* Strings will be UTF-8 encoded in the byte array.
* Warning - confusing method name, Properties is the source.
*
* Properties from the defaults table of props (if any) are not written out by this method.
*
* @deprecated unused
*
* @param target returned array as specified in data structure spec
* @param props source may be null
* @return new offset
* @throws DataFormatException if any string is over 255 bytes long, or if the total length
* (not including the two length bytes) is greater than 65535 bytes.
*/
@Deprecated
public static int toProperties(byte target[], int offset, Properties props) throws DataFormatException, IOException {
if (props != null) {
OrderedProperties p = new OrderedProperties();
p.putAll(props);
ByteArrayOutputStream baos = new ByteArrayOutputStream(p.size() * 64);
for (Map.Entry<Object, Object> entry : p.entrySet()) {
String key = (String) entry.getKey();
String val = (String) entry.getValue();
writeStringUTF8(baos, key);
baos.write('=');
writeStringUTF8(baos, val);
baos.write(';');
}
if (baos.size() > 65535)
throw new DataFormatException("Properties too big (65535 max): " + baos.size());
byte propBytes[] = baos.toByteArray();
toLong(target, offset, 2, propBytes.length);
offset += 2;
System.arraycopy(propBytes, 0, target, offset, propBytes.length);
offset += propBytes.length;
return offset;
} else {
toLong(target, offset, 2, 0);
return offset + 2;
}
}
/**
* Reads the props from the byte array and puts them in the Properties target
* See readProperties() for the format.
* Warning - confusing method name, Properties is the target.
* Strings must be UTF-8 encoded in the byte array.
*
* As of 0.9.18, throws DataFormatException on duplicate key
*
* @param source source
* @param target returned Properties
* @return new offset
*/
public static int fromProperties(byte source[], int offset, Properties target) throws DataFormatException {
int size = (int)fromLong(source, offset, 2);
offset += 2;
ByteArrayInputStream in = new ByteArrayInputStream(source, offset, size);
while (in.available() > 0) {
String key;
try {
key = readString(in);
String cached = _propertiesKeyCache.get(key);
if (cached != null)
key = cached;
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 b = in.read();
if (b != ';')
throw new DataFormatException("Bad value");
} catch (IOException ioe) {
throw new DataFormatException("Bad value", ioe);
}
Object old= target.put(key, val);
if (old != null)
throw new DataFormatException("Duplicate key " + key);
}
return offset + size;
}
/**
* Writes the props to returned byte array, not sorted
* (unless the opts param is an OrderedProperties)
* Strings will be UTF-8 encoded in the byte array.
* See readProperties() for the format.
* Property keys and values must not contain '=' or ';', this is not checked and they are not escaped
* Keys and values must be 255 bytes or less,
* Formatted length must not exceed 65535 bytes
* Warning - confusing method name, Properties is the source.
*
* Properties from the defaults table of props (if any) are not written out by this method.
*
* @throws DataFormatException if key, value, or total is too long
*/
public static byte[] toProperties(Properties opts) throws DataFormatException {
try {
ByteArrayOutputStream baos = new ByteArrayOutputStream(2 + (32 * opts.size()));
writeProperties(baos, opts, true, false);
return baos.toByteArray();
} catch (IOException ioe) {
throw new RuntimeException("IO error writing to memory?! " + ioe.getMessage());
}
}
/**
* Pretty print the mapping, unsorted
* (unless the options param is an OrderedProperties)
*/
public static String toString(Properties options) {
return toString((Map<?, ?>) options);
}
/**
* Pretty print the mapping, unsorted
* (unless the options param is an OrderedProperties)
* @since 0.9.4
*/
public static String toString(Map<?, ?> options) {
StringBuilder buf = new StringBuilder();
if (options != null) {
for (Map.Entry<?, ?> entry : options.entrySet()) {
String key = (String) entry.getKey();
String val = (String) entry.getValue();
buf.append("[").append(key).append("] = [").append(val).append("]");
}
} else {
buf.append("(null properties map)");
}
return buf.toString();
}
/**
* A more efficient Properties.load
*
* Some of the other differences:
* - UTF-8 encoding, not ISO-8859-1
* - No escaping! This does not process or drop backslashes
* - '#' or ';' starts a comment line, but '!' does not
* - Leading whitespace is not trimmed
* - '=' is the only key-termination character (not ':' or whitespace)
*
* As of 0.9.10, an empty value is allowed.
*
* As in Java Properties, duplicate keys are allowed, last one wins.
*
*/
public static void loadProps(Properties props, File file) throws IOException {
loadProps(props, file, false);
}
/**
* @param forceLowerCase if true forces the keys to lower case (not the values)
*/
public static void loadProps(Properties props, File file, boolean forceLowerCase) throws IOException {
loadProps(props, new FileInputStream(file), forceLowerCase);
}
public static void loadProps(Properties props, InputStream inStr) throws IOException {
loadProps(props, inStr, false);
}
/**
* @param forceLowerCase if true forces the keys to lower case (not the values)
*/
public static void loadProps(Properties props, InputStream inStr, boolean forceLowerCase) throws IOException {
BufferedReader in = null;
try {
in = new BufferedReader(new InputStreamReader(inStr, "UTF-8"), 16*1024);
String line = null;
while ( (line = in.readLine()) != null) {
if (line.trim().length() <= 0) continue;
if (line.charAt(0) == '#') continue;
if (line.charAt(0) == ';') continue;
if (line.indexOf('#') > 0) // trim off any end of line comment
line = line.substring(0, line.indexOf('#')).trim();
int split = line.indexOf('=');
if (split <= 0) continue;
String key = line.substring(0, split);
String val = line.substring(split+1).trim();
// Unescape line breaks after loading.
// Remember: "\" needs escaping both for regex and string.
// For some reason this was turning \r (one backslash) into CR,
// I think it needed one more \\ in the pattern?,
// which sucks if your username is randy on DOS,
// it was a horrible idea anyway
//val = val.replaceAll("\\\\r","\r");
//val = val.replaceAll("\\\\n","\n");
// as of 0.9.10, an empty value is allowed
if (forceLowerCase)
props.setProperty(key.toLowerCase(Locale.US), val);
else
props.setProperty(key, val);
}
} finally {
if (in != null) try { in.close(); } catch (IOException ioe) {}
}
}
/**
* Writes the props to the file, unsorted (unless props is an OrderedProperties)
* Note that this does not escape the \r or \n that are unescaped in loadProps() above.
* As of 0.8.1, file will be mode 600.
*
* Properties from the defaults table of props (if any) are not written out by this method.
*
* Leading or trailing whitespace in values is not checked but
* will be trimmed by loadProps()
*
* @throws IllegalArgumentException if a key contains any of "#=\n" or starts with ';',
* or a value contains '#' or '\n'
*/
public static void storeProps(Properties props, File file) throws IOException {
PrintWriter out = null;
IllegalArgumentException iae = null;
File tmpFile = new File(file.getPath() + ".tmp");
try {
FileOutputStream fos = new SecureFileOutputStream(tmpFile);
out = new PrintWriter(new BufferedWriter(new OutputStreamWriter(fos, "UTF-8")));
out.println("# NOTE: This I2P config file must use UTF-8 encoding");
for (Map.Entry<Object, Object> entry : props.entrySet()) {
String name = (String) entry.getKey();
String val = (String) entry.getValue();
if (ILLEGAL_KEY.matcher(name).matches()) {
if (iae == null)
iae = new IllegalArgumentException("Invalid character (one of \"#;=\\r\\n\") in key: \"" +
name + "\" = \"" + val + '\"');
continue;
}
if (ILLEGAL_VALUE.matcher(val).matches()) {
if (iae == null)
iae = new IllegalArgumentException("Invalid character (one of \"#\\r\\n\") in value: \"" +
name + "\" = \"" + val + '\"');
continue;
}
out.println(name + "=" + val);
}
if (SHOULD_SYNC) {
out.flush();
fos.getFD().sync();
}
out.close();
if (out.checkError()) {
out = null;
tmpFile.delete();
throw new IOException("Failed to write properties to " + tmpFile);
}
out = null;
if (!FileUtil.rename(tmpFile, file))
throw new IOException("Failed rename from " + tmpFile + " to " + file);
} finally {
if (out != null) out.close();
}
if (iae != null)
throw iae;
}
/**
* Pretty print the collection
*
*/
public static String toString(Collection<?> col) {
StringBuilder buf = new StringBuilder();
if (col != null) {
for (Iterator<?> iter = col.iterator(); iter.hasNext();) {
Object o = iter.next();
buf.append("[").append(o).append("]");
if (iter.hasNext()) buf.append(", ");
}
} else {
buf.append("null");
}
return buf.toString();
}
/**
* Lower-case hex with leading zeros.
* Use toHexString(byte[]) to not get leading zeros
* @param buf may be null (returns "")
* @return String of length 2*buf.length
*/
public static String toString(byte buf[]) {
if (buf == null) return "";
return toString(buf, buf.length);
}
private static final byte[] EMPTY_BUFFER = new byte[0];
/**
* Lower-case hex with leading zeros.
* Use toHexString(byte[]) to not get leading zeros
* @param buf may be null
* @param len number of bytes. If greater than buf.length, additional zeros will be prepended
* @return String of length 2*len
*/
public static String toString(byte buf[], int len) {
if (buf == null) buf = EMPTY_BUFFER;
StringBuilder out = new StringBuilder();
if (len > buf.length) {
for (int i = 0; i < len - buf.length; i++)
out.append("00");
}
int min = Math.min(buf.length, len);
for (int i = 0; i < min; i++) {
int bi = buf[i] & 0xff;
if (bi < 16)
out.append('0');
out.append(Integer.toHexString(bi));
}
return out.toString();
}
/**
* Positive decimal without leading zeros.
* @param buf may be null (returns "0")
* @param len unused
* @return (new BigInteger(1, buf)).toString()
* @deprecated unused
*/
public static String toDecimalString(byte buf[], int len) {
if (buf == null)
return "0";
BigInteger val = new BigInteger(1, buf);
return val.toString();
}
/**
* Lower-case hex without leading zeros.
* Use toString(byte[] to get leading zeros
* @param data may be null (returns "00")
*/
public final static String toHexString(byte data[]) {
if ((data == null) || (data.length <= 0)) return "00";
BigInteger bi = new BigInteger(1, data);
return bi.toString(16);
}
/**
* @param val non-null, may have leading minus sign
* @return minimum-length representation (with possible leading 0 byte)
* @deprecated unused
*/
public final static byte[] fromHexString(String val) {
BigInteger bv = new BigInteger(val, 16);
return bv.toByteArray();
}
/** Read the stream for an integer as defined by the I2P data structure specification.
* Integers are a fixed number of bytes (numBytes), stored as unsigned integers in network byte order.
* @param rawStream stream to read from
* @param numBytes number of bytes to read and format into a number, 1 to 8
* @throws DataFormatException if negative (only possible if numBytes = 8) (since 0.8.12)
* @throws EOFException since 0.8.2, if there aren't enough bytes to read the number
* @throws IOException if there is an IO error reading the number
* @return number
*/
public static long readLong(InputStream rawStream, int numBytes)
throws DataFormatException, IOException {
if (numBytes > 8)
throw new DataFormatException("readLong doesn't currently support reading numbers > 8 bytes [as thats bigger than java's long]");
long rv = 0;
for (int i = 0; i < numBytes; i++) {
int cur = rawStream.read();
// was DataFormatException
if (cur == -1) throw new EOFException("EOF reading " + numBytes + " byte value");
// we loop until we find a nonzero byte (or we reach the end)
if (cur != 0) {
// ok, data found, now iterate through it to fill the rv
rv = cur & 0xff;
for (int j = i + 1; j < numBytes; j++) {
rv <<= 8;
cur = rawStream.read();
// was DataFormatException
if (cur == -1)
throw new EOFException("EOF reading " + numBytes + " byte value");
rv |= cur & 0xff;
}
break;
}
}
if (rv < 0)
throw new DataFormatException("fromLong got a negative? " + rv + " numBytes=" + numBytes);
return rv;
}
/** Write an integer as defined by the I2P data structure specification to the stream.
* Integers are a fixed number of bytes (numBytes), stored as unsigned integers in network byte order.
* @param value value to write out, non-negative
* @param rawStream stream to write to
* @param numBytes number of bytes to write the number into, 1-8 (padding as necessary)
* @throws DataFormatException if value is negative or if numBytes not 1-8
* @throws IOException if there is an IO error writing to the stream
*/
public static void writeLong(OutputStream rawStream, int numBytes, long value)
throws DataFormatException, IOException {
if (numBytes <= 0 || numBytes > 8)
// probably got the args backwards
throw new DataFormatException("Bad byte count " + numBytes);
if (value < 0)
throw new DataFormatException("Value is negative (" + value + ")");
for (int i = (numBytes - 1) * 8; i >= 0; i -= 8) {
byte cur = (byte) (value >> i);
rawStream.write(cur);
}
}
/**
* Big endian.
*
* @param numBytes 1-8
* @param value non-negative
* @return an array of length numBytes
*/
public static byte[] toLong(int numBytes, long value) throws IllegalArgumentException {
byte val[] = new byte[numBytes];
toLong(val, 0, numBytes, value);
return val;
}
/**
* Big endian.
*
* @param numBytes 1-8
* @param value non-negative
*/
public static void toLong(byte target[], int offset, int numBytes, long value) throws IllegalArgumentException {
if (numBytes <= 0 || numBytes > 8) throw new IllegalArgumentException("Invalid number of bytes");
if (value < 0) throw new IllegalArgumentException("Negative value not allowed");
for (int i = offset + numBytes - 1; i >= offset; i--) {
target[i] = (byte) value;
value >>= 8;
}
}
/**
* Little endian, i.e. backwards. Not for use in I2P protocols.
*
* @param numBytes 1-8
* @param value non-negative
* @since 0.8.12
*/
public static void toLongLE(byte target[], int offset, int numBytes, long value) {
if (numBytes <= 0 || numBytes > 8) throw new IllegalArgumentException("Invalid number of bytes");
if (value < 0) throw new IllegalArgumentException("Negative value not allowed");
int limit = offset + numBytes;
for (int i = offset; i < limit; i++) {
target[i] = (byte) value;
value >>= 8;
}
}
/**
* Big endian.
*
* @param src if null returns 0
* @param numBytes 1-8
* @return non-negative
* @throws ArrayIndexOutOfBoundsException
* @throws IllegalArgumentException if negative (only possible if numBytes = 8)
*/
public static long fromLong(byte src[], int offset, int numBytes) {
if (numBytes <= 0 || numBytes > 8) throw new IllegalArgumentException("Invalid number of bytes");
if ( (src == null) || (src.length == 0) )
return 0;
long rv = 0;
int limit = offset + numBytes;
for (int i = offset; i < limit; i++) {
rv <<= 8;
rv |= src[i] & 0xFF;
}
if (rv < 0)
throw new IllegalArgumentException("fromLong got a negative? " + rv + ": offset="+ offset +" numBytes="+numBytes);
return rv;
}
/**
* Little endian, i.e. backwards. Not for use in I2P protocols.
*
* @param numBytes 1-8
* @return non-negative
* @throws ArrayIndexOutOfBoundsException
* @throws IllegalArgumentException if negative (only possible if numBytes = 8)
* @since 0.8.12
*/
public static long fromLongLE(byte src[], int offset, int numBytes) {
if (numBytes <= 0 || numBytes > 8) throw new IllegalArgumentException("Invalid number of bytes");
long rv = 0;
for (int i = offset + numBytes - 1; i >= offset; i--) {
rv <<= 8;
rv |= src[i] & 0xFF;
}
if (rv < 0)
throw new IllegalArgumentException("fromLong got a negative? " + rv + ": offset="+ offset +" numBytes="+numBytes);
return rv;
}
/** Read in a date from the stream as specified by the I2P data structure spec.
* A date is an 8 byte unsigned integer in network byte order specifying the number of
* milliseconds since midnight on January 1, 1970 in the GMT timezone. If the number is
* 0, the date is undefined or null. (yes, this means you can't represent midnight on 1/1/1970)
* @param in stream to read from
* @throws DataFormatException if the stream doesn't contain a validly formatted date
* @throws IOException if there is an IO error reading the date
* @return date read, or null
*/
public static Date readDate(InputStream in) throws DataFormatException, IOException {
long date = readLong(in, DATE_LENGTH);
if (date == 0L) return null;
return new Date(date);
}
/** Write out a date to the stream as specified by the I2P data structure spec.
* @param out stream to write to
* @param date date to write (can be null)
* @throws DataFormatException if the date is not valid
* @throws IOException if there is an IO error writing the date
*/
public static void writeDate(OutputStream out, Date date)
throws DataFormatException, IOException {
if (date == null)
writeLong(out, DATE_LENGTH, 0L);
else
writeLong(out, DATE_LENGTH, date.getTime());
}
/** @deprecated unused */
@Deprecated
public static byte[] toDate(Date date) throws IllegalArgumentException {
if (date == null)
return toLong(DATE_LENGTH, 0L);
else
return toLong(DATE_LENGTH, date.getTime());
}
public static void toDate(byte target[], int offset, long when) throws IllegalArgumentException {
toLong(target, offset, DATE_LENGTH, when);
}
public static Date fromDate(byte src[], int offset) throws DataFormatException {
if ( (src == null) || (offset + DATE_LENGTH > src.length) )
throw new DataFormatException("Not enough data to read a date");
try {
long when = fromLong(src, offset, DATE_LENGTH);
if (when <= 0)
return null;
else
return new Date(when);
} catch (IllegalArgumentException iae) {
throw new DataFormatException(iae.getMessage());
}
}
public static final int DATE_LENGTH = 8;
/** Read in a string from the stream as specified by the I2P data structure spec.
* A string is 1 or more bytes where the first byte is the number of bytes (not characters!)
* in the string and the remaining 0-255 bytes are the non-null terminated UTF-8 encoded character array.
*
* @param in stream to read from
* @throws DataFormatException if the stream doesn't contain a validly formatted string
* @throws EOFException since 0.8.2, 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
*/
public static String readString(InputStream in) throws DataFormatException, IOException {
int size = in.read();
if (size == -1)
throw new EOFException("EOF reading string");
if (size == 0)
return ""; // reduce object proliferation
size &= 0xff;
byte raw[] = new byte[size];
int read = read(in, raw);
// was DataFormatException
if (read != size) throw new EOFException("EOF reading string");
// the following constructor throws an UnsupportedEncodingException which is an IOException,
// but that's only if UTF-8 is not supported. Other encoding errors are not thrown.
return new String(raw, "UTF-8");
}
/** Write out a string to the stream as specified by the I2P data structure spec. Note that the max
* size for a string allowed by the spec is 255 bytes.
*
* WARNING - this method destroys the encoding, and therefore violates
* the data structure spec.
*
* @param out stream to write string
* @param string string to write out: null strings are perfectly 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
*/
public static void writeString(OutputStream out, String string)
throws DataFormatException, IOException {
if (string == null) {
out.write((byte) 0);
} else {
int len = string.length();
if (len > 255)
throw new DataFormatException("The I2P data spec limits strings to 255 bytes or less, but this is "
+ len + " [" + string + "]");
out.write((byte) len);
for (int i = 0; i < len; i++)
out.write((byte)(string.charAt(i) & 0xFF));
}
}
/** Write out a string to the stream as specified by the I2P data structure spec. Note that the max
* size for a string allowed by the spec is 255 bytes.
*
* This method correctly uses UTF-8
*
* @param out stream to write string
* @param string UTF-8 string to write out: null strings are perfectly 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
* @since public since 0.9.26
*/
public static void writeStringUTF8(OutputStream out, String string)
throws DataFormatException, IOException {
if (string == null) {
out.write((byte) 0);
} else {
// the following method throws an UnsupportedEncodingException which is an IOException,
// but that's only if UTF-8 is not supported. Other encoding errors are not thrown.
byte[] raw = string.getBytes("UTF-8");
int len = raw.length;
if (len > 255)
throw new DataFormatException("The I2P data spec limits strings to 255 bytes or less, but this is "
+ len + " [" + string + "]");
out.write((byte) len);
out.write(raw);
}
}
/** Read in a boolean as specified by the I2P data structure spec.
* A boolean is 1 byte that is either 0 (false), 1 (true), or 2 (null)
* @param in stream to read from
* @throws DataFormatException if the boolean is not valid
* @throws IOException if there is an IO error reading the boolean
* @return boolean value, or null
* @deprecated unused
*/
@Deprecated
public static Boolean readBoolean(InputStream in) throws DataFormatException, IOException {
int val = in.read();
switch (val) {
case -1:
throw new EOFException("EOF reading boolean");
case 0:
return Boolean.FALSE;
case 1:
return Boolean.TRUE;
case 2:
return null;
default:
throw new DataFormatException("Uhhh.. readBoolean read a value that isn't a known ternary val (0,1,2): "
+ val);
}
}
/** Write out a boolean as specified by the I2P data structure spec.
* A boolean is 1 byte that is either 0 (false), 1 (true), or 2 (null)
* @param out stream to write to
* @param bool boolean value, or null
* @throws DataFormatException if the boolean is not valid
* @throws IOException if there is an IO error writing the boolean
* @deprecated unused
*/
@Deprecated
public static void writeBoolean(OutputStream out, Boolean bool)
throws DataFormatException, IOException {
if (bool == null)
writeLong(out, 1, BOOLEAN_UNKNOWN);
else if (Boolean.TRUE.equals(bool))
writeLong(out, 1, BOOLEAN_TRUE);
else
writeLong(out, 1, BOOLEAN_FALSE);
}
/** @deprecated unused */
@Deprecated
public static Boolean fromBoolean(byte data[], int offset) {
if (data[offset] == BOOLEAN_TRUE)
return Boolean.TRUE;
else if (data[offset] == BOOLEAN_FALSE)
return Boolean.FALSE;
else
return null;
}
/** @deprecated unused */
@Deprecated
public static void toBoolean(byte data[], int offset, boolean value) {
data[offset] = (value ? BOOLEAN_TRUE : BOOLEAN_FALSE);
}
/** @deprecated unused */
@Deprecated
public static void toBoolean(byte data[], int offset, Boolean value) {
if (value == null)
data[offset] = BOOLEAN_UNKNOWN;
else
data[offset] = (value.booleanValue() ? BOOLEAN_TRUE : BOOLEAN_FALSE);
}
/** deprecated - used only in DatabaseLookupMessage */
public static final byte BOOLEAN_TRUE = 0x1;
/** deprecated - used only in DatabaseLookupMessage */
public static final byte BOOLEAN_FALSE = 0x0;
/** @deprecated unused */
@Deprecated
public static final byte BOOLEAN_UNKNOWN = 0x2;
/** @deprecated unused */
@Deprecated
public static final int BOOLEAN_LENGTH = 1;
//
// The following comparator helpers make it simpler to write consistently comparing
// functions for objects based on their value, not JVM memory address
//
/**
* Helper util to compare two objects, including null handling.
* <p>
*
* This treats (null == null) as true, and (null == (!null)) as false.
*/
public final static boolean eq(Object lhs, Object rhs) {
try {
boolean eq = (((lhs == null) && (rhs == null)) || ((lhs != null) && (lhs.equals(rhs))));
return eq;
} catch (ClassCastException cce) {
return false;
}
}
/**
* Run a deep comparison across the two collections.
* <p>
*
* This treats (null == null) as true, (null == (!null)) as false, and then
* comparing each element via eq(object, object). <p>
*
* If the size of the collections are not equal, the comparison returns false.
* The collection order should be consistent, as this simply iterates across both and compares
* based on the value of each at each step along the way.
*
*/
public final static boolean eq(Collection<?> lhs, Collection<?> rhs) {
if ((lhs == null) && (rhs == null)) return true;
if ((lhs == null) || (rhs == null)) return false;
if (lhs.size() != rhs.size()) return false;
Iterator<?> liter = lhs.iterator();
Iterator<?> riter = rhs.iterator();
while ((liter.hasNext()) && (riter.hasNext()))
if (!(eq(liter.next(), riter.next()))) return false;
return true;
}
/**
* Run a comparison on the byte arrays, byte by byte. <p>
*
* This treats (null == null) as true, (null == (!null)) as false,
* and unequal length arrays as false.
*
* Variable time.
*
* @return Arrays.equals(lhs, rhs)
*/
public final static boolean eq(byte lhs[], byte rhs[]) {
return Arrays.equals(lhs, rhs);
}
/**
* Compare two integers, really just for consistency.
* @deprecated inefficient
*/
@Deprecated
public final static boolean eq(int lhs, int rhs) {
return lhs == rhs;
}
/**
* Compare two longs, really just for consistency.
* @deprecated inefficient
*/
@Deprecated
public final static boolean eq(long lhs, long rhs) {
return lhs == rhs;
}
/**
* Compare two bytes, really just for consistency.
* @deprecated inefficient
*/
@Deprecated
public final static boolean eq(byte lhs, byte rhs) {
return lhs == rhs;
}
/**
* Unlike eq(byte[], byte[]), this returns false if either lhs or rhs is null.
* Variable time.
*
* @throws ArrayIndexOutOfBoundsException if either array isn't long enough
*/
public final static boolean eq(byte lhs[], int offsetLeft, byte rhs[], int offsetRight, int length) {
if ( (lhs == null) || (rhs == null) ) return false;
for (int i = 0; i < length; i++) {
if (lhs[offsetLeft + i] != rhs[offsetRight + i])
return false;
}
return true;
}
/**
* Unlike eq(), this throws NPE if either lhs or rhs is null.
* Constant time.
*
* @throws NullPointerException if lhs or rhs is null
* @throws ArrayIndexOutOfBoundsException if either array isn't long enough
* @since 0.9.13
*/
public final static boolean eqCT(byte lhs[], int offsetLeft, byte rhs[], int offsetRight, int length) {
int r = 0;
for (int i = 0; i < length; i++) {
r |= lhs[offsetLeft + i] ^ rhs[offsetRight + i];
}
return r == 0;
}
/**
* Big endian compare, treats bytes as unsigned.
* Shorter arg is lesser.
* Args may be null, null is less than non-null.
* Variable time.
*/
public final static int compareTo(byte lhs[], byte rhs[]) {
if ((rhs == null) && (lhs == null)) return 0;
if (lhs == null) return -1;
if (rhs == null) return 1;
if (rhs.length < lhs.length) return 1;
if (rhs.length > lhs.length) return -1;
for (int i = 0; i < rhs.length; i++) {
if ((rhs[i] & 0xff) > (lhs[i] & 0xff))
return -1;
else if ((rhs[i] & 0xff) < (lhs[i] & 0xff)) return 1;
}
return 0;
}
/**
* @return null if either arg is null or the args are not equal length
*/
public final static byte[] xor(byte lhs[], byte rhs[]) {
if ((lhs == null) || (rhs == null) || (lhs.length != rhs.length)) return null;
byte diff[] = new byte[lhs.length];
xor(lhs, 0, rhs, 0, diff, 0, lhs.length);
return diff;
}
/**
* xor the lhs with the rhs, storing the result in out.
*
* @param lhs one of the source arrays
* @param startLeft starting index in the lhs array to begin the xor
* @param rhs the other source array
* @param startRight starting index in the rhs array to begin the xor
* @param out output array
* @param startOut starting index in the out array to store the result
* @param len how many bytes into the various arrays to xor
*/
public final static void xor(byte lhs[], int startLeft, byte rhs[], int startRight, byte out[], int startOut, int len) {
if ( (lhs == null) || (rhs == null) || (out == null) )
throw new NullPointerException("Null params to xor");
if (lhs.length < startLeft + len)
throw new IllegalArgumentException("Left hand side is too short");
if (rhs.length < startRight + len)
throw new IllegalArgumentException("Right hand side is too short");
if (out.length < startOut + len)
throw new IllegalArgumentException("Result is too short");
for (int i = 0; i < len; i++)
out[startOut + i] = (byte) (lhs[startLeft + i] ^ rhs[startRight + i]);
}
//
// The following hashcode helpers make it simpler to write consistently hashing
// functions for objects based on their value, not JVM memory address
//
/**
* Calculate the hashcode of the object, using 0 for null
*
*/
public static int hashCode(Object obj) {
if (obj == null) return 0;
return obj.hashCode();
}
/**
* Calculate the hashcode of the date, using 0 for null
*
*/
public static int hashCode(Date obj) {
if (obj == null) return 0;
return (int) obj.getTime();
}
/**
* Calculate the hashcode of the byte array, using 0 for null
*
*/
public static int hashCode(byte b[]) {
// Java 5 now has its own method, and the old way
// was horrible for arrays much smaller than 32.
// otoh, for sizes >> 32, java's method may be too slow
int rv = 0;
if (b != null) {
if (b.length <= 32) {
rv = Arrays.hashCode(b);
} else {
for (int i = 0; i < 32; i++)
rv ^= (b[i] << i); // xor better than + in tests
}
}
return rv;
}
/**
* Calculate the hashcode of the collection, using 0 for null
*
*/
public static int hashCode(Collection<?> col) {
if (col == null) return 0;
int c = 0;
for (Iterator<?> iter = col.iterator(); iter.hasNext();)
c = 7 * c + hashCode(iter.next());
return c;
}
/**
* This is different than InputStream.skip(), in that it
* does repeated reads until the full amount is skipped.
* To fix findbugs issues with skip().
*
* Guaranteed to skip exactly n bytes or throw an IOE.
*
* http://stackoverflow.com/questions/14057720/robust-skipping-of-data-in-a-java-io-inputstream-and-its-subtypes
* http://stackoverflow.com/questions/11511093/java-inputstream-skip-return-value-near-end-of-file
*
* @since 0.9.9
*/
public static void skip(InputStream in, long n) throws IOException {
if (n < 0)
throw new IllegalArgumentException();
if (n == 0)
return;
long read = 0;
long nm1 = n - 1;
if (nm1 > 0) {
// skip all but the last byte
do {
long c = in.skip(nm1 - read);
if (c < 0)
throw new EOFException("EOF while skipping " + n + ", read only " + read);
if (c == 0) {
// see second SO link above
if (in.read() == -1)
throw new EOFException("EOF while skipping " + n + ", read only " + read);
read++;
} else {
read += c;
}
} while (read < nm1);
}
// read the last byte to check for EOF
if (in.read() == -1)
throw new EOFException("EOF while skipping " + n + ", read only " + read);
}
/**
* This is different than InputStream.read(target), in that it
* does repeated reads until the full data is received.
*
* As of 0.9.27, throws EOFException if the full length is not read.
*
* @return target.length
* @throws EOFException if the full length is not read (since 0.9.27)
*/
public static int read(InputStream in, byte target[]) throws IOException {
return read(in, target, 0, target.length);
}
/**
* WARNING - This is different than InputStream.read(target, offset, length)
* for a nonzero offset, in that it
* returns the new offset (== old offset + length).
* It also does repeated reads until the full data is received.
*
* WARNING - Broken for nonzero offset before 0.9.27.
* As of 0.9.27, throws EOFException if the full length is not read.
*
* @return the new offset (== old offset + length)
* @throws EOFException if the full length is not read (since 0.9.27)
*/
public static int read(InputStream in, byte target[], int offset, int length) throws IOException {
int cur = 0;
while (cur < length) {
int numRead = in.read(target, offset + cur, length - cur);
if (numRead == -1) {
throw new EOFException("EOF after reading " + cur + " bytes of " + length + " byte value");
}
cur += numRead;
}
return offset + cur;
}
/**
* Read a newline delimited line from the stream, returning the line (without
* the newline), or null if EOF reached on an empty line
* Warning - strips \n but not \r
* Warning - 8KB line length limit as of 0.7.13, @throws IOException if exceeded
* Warning - not UTF-8
*
* @return null on EOF
*/
public static String readLine(InputStream in) throws IOException { return readLine(in, (MessageDigest) null); }
/**
* update the hash along the way
* Warning - strips \n but not \r
* Warning - 8KB line length limit as of 0.7.13, @throws IOException if exceeded
* Warning - not UTF-8
*
* @param hash null OK
* @return null on EOF
* @since 0.8.8
*/
public static String readLine(InputStream in, MessageDigest hash) throws IOException {
StringBuilder buf = new StringBuilder(128);
boolean ok = readLine(in, buf, hash);
if (ok)
return buf.toString();
else
return null;
}
/** ridiculously long, just to prevent OOM DOS @since 0.7.13 */
private static final int MAX_LINE_LENGTH = 8*1024;
/**
* Read in a line, placing it into the buffer (excluding the newline).
* Warning - strips \n but not \r
* Warning - 8KB line length limit as of 0.7.13, @throws IOException if exceeded
* Warning - not UTF-8
*
* @return true if the line was read, false if eof was reached on an empty line
* (returns true for non-empty last line without a newline)
*/
public static boolean readLine(InputStream in, StringBuilder buf) throws IOException {
return readLine(in, buf, (MessageDigest) null);
}
/**
* update the hash along the way
* Warning - strips \n but not \r
* Warning - 8KB line length limit as of 0.7.13, @throws IOException if exceeded
* Warning - not UTF-8
*
* @param hash null OK
* @return true if the line was read, false if eof was reached on an empty line
* (returns true for non-empty last line without a newline)
* @since 0.8.8
*/
public static boolean readLine(InputStream in, StringBuilder buf, MessageDigest hash) throws IOException {
int c = -1;
int i = 0;
while ( (c = in.read()) != -1) {
if (++i > MAX_LINE_LENGTH)
throw new IOException("Line too long - max " + MAX_LINE_LENGTH);
if (hash != null) hash.update((byte)c);
if (c == '\n')
break;
buf.append((char)c);
}
return c != -1 || i > 0;
}
/**
* update the hash along the way
* @since 0.8.8
*/
public static void write(OutputStream out, byte data[], MessageDigest hash) throws IOException {
hash.update(data);
out.write(data);
}
/**
* NOTE: formatDuration2() recommended in most cases for readability
*/
public static String formatDuration(long ms) {
if (ms < 5 * 1000) {
return ms + "ms";
} else if (ms < 3 * 60 * 1000) {
return (ms / 1000) + "s";
} else if (ms < 120 * 60 * 1000) {
return (ms / (60 * 1000)) + "m";
} else if (ms < 3 * 24 * 60 * 60 * 1000) {
return (ms / (60 * 60 * 1000)) + "h";
} else if (ms < 3L * 365 * 24 * 60 * 60 * 1000) {
return (ms / (24 * 60 * 60 * 1000)) + "d";
} else if (ms < 1000L * 365 * 24 * 60 * 60 * 1000) {
return (ms / (365L * 24 * 60 * 60 * 1000)) + "y";
} else {
return "n/a";
}
}
/**
* Like formatDuration but with a non-breaking space after the number,
* 0 is unitless, and the unit is translated.
* This seems consistent with most style guides out there.
* Use only in HTML.
* Thresholds are a little lower than in formatDuration() also,
* as precision is less important in the GUI than in logging.
*
* Negative numbers handled correctly.
*
* @since 0.8.2
*/
public static String formatDuration2(long ms) {
if (ms == 0)
return "0";
String t;
long ams = ms >= 0 ? ms : 0 - ms;
if (ams < 3 * 1000) {
// NOTE TO TRANSLATORS: Feel free to translate all these as you see fit, there are several options...
// spaces or not, '.' or not, plural or not. Try not to make it too long, it is used in
// a lot of tables.
// milliseconds
// Note to translators, may be negative or zero, 2999 maximum.
// {0,number,####} prevents 1234 from being output as 1,234 in the English locale.
// If you want the digit separator in your locale, translate as {0}.
// alternates: msec, msecs
t = ngettext("1 ms", "{0,number,####} ms", (int) ms);
} else if (ams < 2 * 60 * 1000) {
// seconds
// alternates: secs, sec. 'seconds' is probably too long.
t = ngettext("1 sec", "{0} sec", (int) (ms / 1000));
} else if (ams < 120 * 60 * 1000) {
// minutes
// alternates: mins, min. 'minutes' is probably too long.
t = ngettext("1 min", "{0} min", (int) (ms / (60 * 1000)));
} else if (ams < 2 * 24 * 60 * 60 * 1000) {
// hours
// alternates: hrs, hr., hrs.
t = ngettext("1 hour", "{0} hours", (int) (ms / (60 * 60 * 1000)));
} else if (ams < 3L * 365 * 24 * 60 * 60 * 1000) {
// days
t = ngettext("1 day", "{0} days", (int) (ms / (24 * 60 * 60 * 1000)));
} else if (ams < 1000L * 365 * 24 * 60 * 60 * 1000) {
// years
t = ngettext("1 year", "{0} years", (int) (ms / (365L * 24 * 60 * 60 * 1000)));
} else {
return _t("n/a");
}
// Replace minus sign to work around
// bug in Chrome (and IE?), line breaks at the minus sign
// http://code.google.com/p/chromium/issues/detail?id=46683
// − seems to work on text browsers OK
// Although it's longer than a standard '-' on graphical browsers
// http://www.cs.tut.fi/~jkorpela/dashes.html
if (ms < 0)
t = t.replace("-", "−");
// do it here to keep out of the tags for translator sanity
return t.replace(" ", " ");
}
/**
* Like formatDuration2(long) but with microsec and nanosec also.
*
* @since 0.9.19
*/
public static String formatDuration2(double ms) {
if (ms == 0d)
return "0";
String t;
double adms = ms >= 0 ? ms : 0 - ms;
long lms = (long) ms;
long ams = lms >= 0 ? lms : 0 - lms;
if (adms < 0.000000001d) {
return "0";
} else if (adms < 0.001d) {
t = ngettext("1 ns", "{0,number,###} ns", (int) Math.round(ms * 1000000d));
} else if (adms < 1.0d) {
t = ngettext("1 μs", "{0,number,###} μs", (int) Math.round(ms * 1000d));
} else if (ams < 3 * 1000) {
t = ngettext("1 ms", "{0,number,####} ms", (int) Math.round(ms));
} else if (ams < 2 * 60 * 1000) {
t = ngettext("1 sec", "{0} sec", (int) (ms / 1000));
} else if (ams < 120 * 60 * 1000) {
t = ngettext("1 min", "{0} min", (int) (ms / (60 * 1000)));
} else if (ams < 2 * 24 * 60 * 60 * 1000) {
t = ngettext("1 hour", "{0} hours", (int) (ms / (60 * 60 * 1000)));
} else if (ams < 3L * 365 * 24 * 60 * 60 * 1000) {
// days
t = ngettext("1 day", "{0} days", (int) (ms / (24 * 60 * 60 * 1000)));
} else if (ams < 1000L * 365 * 24 * 60 * 60 * 1000) {
// years
t = ngettext("1 year", "{0} years", (int) (ms / (365L * 24 * 60 * 60 * 1000)));
} else {
return _t("n/a");
}
if (ms < 0)
t = t.replace("-", "−");
return t.replace(" ", " ");
}
private static final String BUNDLE_NAME = "net.i2p.router.web.messages";
private static String _t(String key) {
return Translate.getString(key, I2PAppContext.getGlobalContext(), BUNDLE_NAME);
}
private static String ngettext(String s, String p, int n) {
return Translate.getString(n, s, p, I2PAppContext.getGlobalContext(), BUNDLE_NAME);
}
/**
* Caller should append 'B' or 'b' as appropriate
* NOTE: formatDuration2() recommended in most cases for readability
*/
public static String formatSize(long bytes) {
float val = bytes;
int scale = 0;
while (val >= 1024.0f) {
scale++;
val /= 1024.0f;
}
DecimalFormat fmt = new DecimalFormat("##0.00");
String str = fmt.format(val);
switch (scale) {
case 1: return str + "K";
case 2: return str + "M";
case 3: return str + "G";
case 4: return str + "T";
case 5: return str + "P";
case 6: return str + "E";
case 7: return str + "Z";
case 8: return str + "Y";
default: return bytes + "";
}
}
/**
* Like formatSize but with a non-breaking space after the number
* This seems consistent with most style guides out there.
* Use only in HTML
* @since 0.7.14
*/
public static String formatSize2(long bytes) {
double val = bytes;
int scale = 0;
while (val >= 1024) {
scale++;
val /= 1024;
}
DecimalFormat fmt = new DecimalFormat("##0.00");
String str = fmt.format(val);
switch (scale) {
case 1: return str + " K";
case 2: return str + " M";
case 3: return str + " G";
case 4: return str + " T";
case 5: return str + " P";
case 6: return str + " E";
case 7: return str + " Z";
case 8: return str + " Y";
default: return bytes + " ";
}
}
/**
* Strip out any HTML (simply removing any less than / greater than symbols)
* @param orig may be null, returns empty string if null
*/
public static String stripHTML(String orig) {
if (orig == null) return "";
String t1 = orig.replace('<', ' ');
String rv = t1.replace('>', ' ');
rv = rv.replace('\"', ' ');
rv = rv.replace('\'', ' ');
return rv;
}
private static final String escapeChars[] = {"&", "\"", "<", ">", "'"};
private static final String escapeCodes[] = {"&", """, "<", ">", "'"};
/**
* Escape a string for inclusion in HTML
* @param unescaped the unescaped string, may be null
* @return the escaped string, or null if null is passed in
*/
public static String escapeHTML(String unescaped) {
if (unescaped == null) return null;
String escaped = unescaped;
for (int i = 0; i < escapeChars.length; i++) {
escaped = escaped.replace(escapeChars[i], escapeCodes[i]);
}
return escaped;
}
/**
* Unescape a string taken from HTML
* @param escaped the escaped string, may be null
* @return the unescaped string, or null if null is passed in
*/
/**** unused, uncomment if you need it
public static String unescapeHTML(String escaped) {
if (escaped == null) return null;
String unescaped = escaped;
for (int i = 0; i < escapeChars.length; i++) {
unescaped = unescaped.replace(escapeCodes[i], escapeChars[i]);
}
return unescaped;
}
****/
/** */
public static final int MAX_UNCOMPRESSED = 40*1024;
public static final int MAX_COMPRESSION = Deflater.BEST_COMPRESSION;
public static final int NO_COMPRESSION = Deflater.NO_COMPRESSION;
/**
* Compress the data and return a new GZIP compressed byte array.
* The compressed data conforms to RFC 1952,
* with a 10-byte gzip header and a 8-byte gzip checksum footer.
*
* Prior to 0.9.29, this would return a zero-length output
* for a zero-length input. As of 0.9.29, output is valid for
* a zero-length input also.
*
* @throws IllegalArgumentException if input size is over 40KB
* @throws IllegalStateException on compression failure, as of 0.9.29
* @return null if orig is null
*/
public static byte[] compress(byte orig[]) {
return compress(orig, 0, orig.length);
}
/**
* Compress the data and return a new GZIP compressed byte array.
* The compressed data conforms to RFC 1952,
* with a 10-byte gzip header and a 8-byte gzip checksum footer.
*
* Prior to 0.9.29, this would return a zero-length output
* for a zero-length input. As of 0.9.29, output is valid for
* a zero-length input also.
*
* @throws IllegalArgumentException if size is over 40KB
* @throws IllegalStateException on compression failure, as of 0.9.29
* @return null if orig is null
*/
public static byte[] compress(byte orig[], int offset, int size) {
return compress(orig, offset, size, MAX_COMPRESSION);
}
/**
* Compress the data and return a new GZIP compressed byte array.
* The compressed data conforms to RFC 1952,
* with a 10-byte gzip header and a 8-byte gzip checksum footer.
*
* Prior to 0.9.29, this would return a zero-length output
* for a zero-length input. As of 0.9.29, output is valid for
* a zero-length input also.
*
* @throws IllegalArgumentException if size is over 40KB
* @throws IllegalStateException on compression failure, as of 0.9.29
* @param level the compression level, 0 to 9
* @return null if orig is null
*/
public static byte[] compress(byte orig[], int offset, int size, int level) {
if (orig == null) return orig;
if (size > MAX_UNCOMPRESSED)
throw new IllegalArgumentException("tell jrandom size=" + size);
ReusableGZIPOutputStream out = ReusableGZIPOutputStream.acquire();
out.setLevel(level);
try {
out.write(orig, offset, size);
out.finish();
out.flush();
byte rv[] = out.getData();
//if (_log.shouldLog(Log.DEBUG))
// _log.debug("Compression of " + orig.length + " into " + rv.length + " (or " + 100.0d
// * (((double) orig.length) / ((double) rv.length)) + "% savings)");
// ticket 1915
// If we have a bug where the deflator didn't flush, this will catch it.
// gzip header is 10 bytes and footer is 8 bytes.
// size for zero-length input is 20.
if (rv.length <= 18)
throw new IllegalStateException("Compression failed, input size: " + size + " output size: " + rv.length);
return rv;
} catch (IOException ioe) {
// Apache Harmony 5.0M13
//java.io.IOException: attempt to write after finish
//at java.util.zip.DeflaterOutputStream.write(DeflaterOutputStream.java:181)
//at net.i2p.util.ResettableGZIPOutputStream.write(ResettableGZIPOutputStream.java:122)
//at net.i2p.data.DataHelper.compress(DataHelper.java:1048)
// ...
ioe.printStackTrace();
throw new IllegalStateException("Compression failed, input size: " + size, ioe);
} finally {
ReusableGZIPOutputStream.release(out);
}
}
/**
* Decompress the GZIP compressed data (returning null on error).
* @throws IOException if uncompressed is over 40 KB,
* or on a decompression error
* @return null if orig is null
*/
public static byte[] decompress(byte orig[]) throws IOException {
return (orig != null ? decompress(orig, 0, orig.length) : null);
}
/**
* Decompress the GZIP compressed data (returning null on error).
* @throws IOException if uncompressed is over 40 KB,
* or on a decompression error
* @return null if orig is null
*/
public static byte[] decompress(byte orig[], int offset, int length) throws IOException {
if (orig == null) return orig;
if (offset + length > orig.length)
throw new IOException("Bad params arrlen " + orig.length + " off " + offset + " len " + length);
ReusableGZIPInputStream in = ReusableGZIPInputStream.acquire();
in.initialize(new ByteArrayInputStream(orig, offset, length));
// don't make this a static field, or else I2PAppContext gets initialized too early
ByteCache cache = ByteCache.getInstance(8, MAX_UNCOMPRESSED);
ByteArray outBuf = cache.acquire();
try {
int written = 0;
while (true) {
int read = in.read(outBuf.getData(), written, MAX_UNCOMPRESSED-written);
if (read == -1)
break;
written += read;
if (written >= MAX_UNCOMPRESSED) {
if (in.available() > 0)
throw new IOException("Uncompressed data larger than " + MAX_UNCOMPRESSED);
break;
}
}
byte rv[] = new byte[written];
System.arraycopy(outBuf.getData(), 0, rv, 0, written);
return rv;
} finally {
cache.release(outBuf);
ReusableGZIPInputStream.release(in);
}
}
/**
* Same as orig.getBytes("UTF-8") but throws an unchecked RuntimeException
* instead of an UnsupportedEncodingException if no UTF-8, for ease of use.
*
* @return null if orig is null
* @throws RuntimeException
*/
public static byte[] getUTF8(String orig) {
if (orig == null) return null;
try {
return orig.getBytes("UTF-8");
} catch (UnsupportedEncodingException uee) {
throw new RuntimeException("no utf8!?");
}
}
/**
* Same as orig.getBytes("UTF-8") but throws an unchecked RuntimeException
* instead of an UnsupportedEncodingException if no UTF-8, for ease of use.
*
* @return null if orig is null
* @throws RuntimeException
* @deprecated unused
*/
public static byte[] getUTF8(StringBuffer orig) {
if (orig == null) return null;
return getUTF8(orig.toString());
}
/**
* Same as new String(orig, "UTF-8") but throws an unchecked RuntimeException
* instead of an UnsupportedEncodingException if no UTF-8, for ease of use.
* Used by Syndie.
*
* @return null if orig is null
* @throws RuntimeException
*/
public static String getUTF8(byte orig[]) {
if (orig == null) return null;
try {
return new String(orig, "UTF-8");
} catch (UnsupportedEncodingException uee) {
throw new RuntimeException("no utf8!?");
}
}
/**
* Same as new String(orig, "UTF-8") but throws an unchecked RuntimeException
* instead of an UnsupportedEncodingException if no UTF-8, for ease of use.
*
* @return null if orig is null
* @throws RuntimeException
*/
public static String getUTF8(byte orig[], int offset, int len) {
if (orig == null) return null;
try {
return new String(orig, offset, len, "UTF-8");
} catch (UnsupportedEncodingException uee) {
throw new RuntimeException("no utf8!?");
}
}
/**
* Roughly the same as orig.getBytes("ISO-8859-1") but much faster and
* will not throw an exception.
*
* Warning - misnamed, converts to ISO-8859-1.
*
* @param orig non-null, truncates to 8-bit chars
* @since 0.9.5
*/
public static byte[] getASCII(String orig) {
byte[] rv = new byte[orig.length()];
for (int i = 0; i < rv.length; i++) {
rv[i] = (byte)orig.charAt(i);
}
return rv;
}
/**
* Same as s.split(regex) but caches the compiled pattern for speed.
* This saves about 10 microseconds (Bulldozer) on subsequent invocations.
*
* @param s non-null
* @param regex non-null
* @throws java.util.regex.PatternSyntaxException unchecked
* @since 0.9.24
*/
public static String[] split(String s, String regex) {
return split(s, regex, 0);
}
private static final ConcurrentHashMap<String, Pattern> patterns = new ConcurrentHashMap<String, Pattern>();
/**
* Same as s.split(regex, limit) but caches the compiled pattern for speed.
* This saves about 10 microseconds (Bulldozer) on subsequent invocations.
*
* @param s non-null
* @param regex non-null
* @param limit result threshold
* @throws java.util.regex.PatternSyntaxException unchecked
* @since 0.9.24
*/
public static String[] split(String s, String regex, int limit) {
Pattern p = patterns.get(regex);
if (p == null) {
p = Pattern.compile(regex);
patterns.putIfAbsent(regex, p);
}
return p.split(s, limit);
}
/**
* Copy in to out. Caller MUST close the streams.
*
* @param in non-null
* @param out non-null
* @since 0.9.29
*/
public static void copy(InputStream in, OutputStream out) throws IOException {
final ByteCache cache = ByteCache.getInstance(8, 8*1024);
final ByteArray ba = cache.acquire();
try {
final byte buf[] = ba.getData();
int read;
while ((read = in.read(buf)) != -1) {
out.write(buf, 0, read);
}
} finally {
cache.release(ba);
}
}
}