package net.i2p.data; /* * Public domain */ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.Arrays; import net.i2p.crypto.SHA256Generator; /** * A SimpleDataStructure contains only a single fixed-length byte array. * The main reason to do this is to override * toByteArray() and fromByteArray(), which are used by toBase64(), fromBase64(), * and calculateHash() in DataStructureImpl - otherwise these would go through * a wasteful array-to-stream-to-array pass. * It also centralizes a lot of common code. * * Implemented in 0.8.2 and retrofitted over several of the classes in this package. * * As of 0.8.3, SDS objects may be cached. An SDS may be instantiated with null data, * and setData(null) is also OK. However, * once non-null data is set, the data reference is immutable; * subsequent attempts to set the data via setData(), readBytes(), * fromByteArray(), or fromBase64() will throw a RuntimeException. * * @since 0.8.2 * @author zzz */ public abstract class SimpleDataStructure extends DataStructureImpl { protected byte[] _data; /** A new instance with the data set to null. Call readBytes(), setData(), or fromByteArray() after this to set the data */ public SimpleDataStructure() { } /** @throws IllegalArgumentException if data is not the legal number of bytes (but null is ok) */ public SimpleDataStructure(byte data[]) { setData(data); } /** * The legal length of the byte array in this data structure * @since 0.8.2 */ abstract public int length(); /** * Get the data reference (not a copy) * @return the byte array, or null if unset */ public byte[] getData() { return _data; } /** * Sets the data. * @param data of correct length, or null * @throws IllegalArgumentException if data is not the legal number of bytes (but null is ok) * @throws RuntimeException if data already set. */ public void setData(byte[] data) { if (_data != null) throw new RuntimeException("Data already set"); if (data != null && data.length != length()) throw new IllegalArgumentException("Bad data length: " + data.length + "; required: " + length()); _data = data; } /** * Sets the data. * @param in the stream to read * @throws RuntimeException if data already set. */ public void readBytes(InputStream in) throws DataFormatException, IOException { if (_data != null) throw new RuntimeException("Data already set"); int length = length(); _data = new byte[length]; int read = read(in, _data); if (read != length) throw new DataFormatException("EOF reading " + getClass().getSimpleName() + ", read: " + read + ", required: " + length); } public void writeBytes(OutputStream out) throws DataFormatException, IOException { if (_data == null) throw new DataFormatException("No data to write out"); out.write(_data); } @Override public String toBase64() { if (_data == null) return null; return Base64.encode(_data); } /** * Sets the data. * @throws DataFormatException if decoded data is not the legal number of bytes or on decoding error * @throws RuntimeException if data already set. */ @Override public void fromBase64(String data) throws DataFormatException { if (data == null) throw new DataFormatException("Null data passed in"); byte[] d = Base64.decode(data); if (d == null) throw new DataFormatException("Bad Base64 encoded data"); if (d.length != length()) throw new DataFormatException("Bad decoded data length, expected " + length() + " got " + d.length); // call setData() instead of _data = data in case overridden setData(d); } /** @return the SHA256 hash of the byte array, or null if the data is null */ @Override public Hash calculateHash() { if (_data != null) return SHA256Generator.getInstance().calculateHash(_data); return null; } /** * Overridden for efficiency. * @return same thing as getData() */ @Override public byte[] toByteArray() { return _data; } /** * Overridden for efficiency. * Does the same thing as setData() but null not allowed. * @param data non-null * @throws DataFormatException if null or wrong length * @throws RuntimeException if data already set. */ @Override public void fromByteArray(byte data[]) throws DataFormatException { if (data == null) throw new DataFormatException("Null data passed in"); if (data.length != length()) throw new DataFormatException("Bad data length: " + data.length + "; required: " + length()); // call setData() instead of _data = data in case overridden setData(data); } @Override public String toString() { StringBuilder buf = new StringBuilder(64); buf.append('[').append(getClass().getSimpleName()).append(": "); int length = length(); if (_data == null) { buf.append("null"); } else if (length <= 32) { buf.append(toBase64()); } else { buf.append("size: ").append(Integer.toString(length)); } buf.append(']'); return buf.toString(); } /** * We assume the data has enough randomness in it, so use the first 4 bytes for speed. * If this is not the case, override in the extending class. */ @Override public int hashCode() { if (_data == null) return 0; int rv = _data[0]; for (int i = 1; i < 4; i++) rv ^= (_data[i] << (i*8)); return rv; } /** * Warning - this returns true for two different classes with the same size * and same data, e.g. SessionKey and SessionTag, but you wouldn't * put them in the same Set, would you? */ @Override public boolean equals(Object obj) { if (obj == this) return true; if ((obj == null) || !(obj instanceof SimpleDataStructure)) return false; return Arrays.equals(_data, ((SimpleDataStructure) obj)._data); } }