gt: 				if (plainRoutingKey == null)
context: /* This code is part of Freenet. It is distributed under the GNU General
   * Public License, version 2 (or at your option any later version). See
   * http://www.gnu.org/ for further details of the GPL. */
  package freenet.store.saltedhash;
  
  import java.io.EOFException;
  import java.io.File;
  import java.io.IOException;
  import java.io.RandomAccessFile;
  import java.nio.ByteBuffer;
  import java.nio.channels.FileChannel;
  import java.util.Arrays;
  import java.util.LinkedList;
  import java.util.List;
  import java.util.ListIterator;
  import java.util.Map;
  import java.util.Random;
  import java.util.SortedSet;
  import java.util.TreeMap;
  import java.util.TreeSet;
  import java.util.concurrent.TimeUnit;
  import java.util.concurrent.atomic.AtomicLong;
  import java.util.concurrent.locks.Condition;
  import java.util.concurrent.locks.Lock;
  import java.util.concurrent.locks.ReadWriteLock;
  import java.util.concurrent.locks.ReentrantLock;
  import java.util.concurrent.locks.ReentrantReadWriteLock;
  
  import org.tanukisoftware.wrapper.WrapperManager;
  
  import freenet.crypt.BlockCipher;
  import freenet.crypt.DSAPublicKey;
  import freenet.crypt.UnsupportedCipherException;
  import freenet.crypt.ciphers.Rijndael;
  import freenet.keys.KeyVerifyException;
  import freenet.keys.SSKBlock;
  import freenet.l10n.NodeL10n;
  import freenet.node.FastRunnable;
  import freenet.node.SemiOrderedShutdownHook;
  import freenet.node.stats.StoreAccessStats;
  import freenet.node.useralerts.AbstractUserAlert;
  import freenet.node.useralerts.UserAlert;
  import freenet.node.useralerts.UserAlertManager;
  import freenet.store.BlockMetadata;
  import freenet.store.FreenetStore;
  import freenet.store.KeyCollisionException;
  import freenet.store.StorableBlock;
  import freenet.store.StoreCallback;
  import freenet.support.BloomFilter;
  import freenet.support.Fields;
  import freenet.support.HTMLNode;
  import freenet.support.HexUtil;
  import freenet.support.Logger;
  import freenet.support.Logger.LogLevel;
  import freenet.support.Ticker;
  import freenet.support.io.Closer;
  import freenet.support.io.FileUtil;
  import freenet.support.io.NativeThread;
  import freenet.support.math.MersenneTwister;
  
  /**
   * Index-less data store based on salted hash.
   *
   * Provide a pseudo-random replacement based on a salt value generated on create. Keys are check
   * against a bloom filter before probing. Data are encrypted using the route key and the salt, so
   * there is no way to recover the data without holding the route key. (For debugging, you can set
   * OPTION_SAVE_PLAINKEY=true in source code)
   *
   * @author sdiz
   */
  public class SaltedHashFreenetStore<T extends StorableBlock> implements FreenetStore<T> {
  	/** Option for saving plainkey. 
  	 * SECURITY: This should NEVER be enabled for a client-cache! */
  	private static final boolean OPTION_SAVE_PLAINKEY = false;
  	private static final int OPTION_MAX_PROBE = 5;
  
  	private static final byte FLAG_DIRTY = 0x1;
  	private static final byte FLAG_REBUILD_BLOOM = 0x2;
  
  	/** Alternative to a Bloom filter which allows us to know exactly which slots to check,
  	 * so radically reduces disk I/O even when there is a hit.
  	 * 
  	 * Each slot in a 4 byte integer.
  	 * bit 31 - Must be 1. 0 indicates we have not checked this slot so must read the entry.
  	 * bit 30 - ENTRY_FLAG_OCCUPIED: 0 = Slot is free, 1 = slot is occupied.
  	 * bit 29 - ENTRY_NEW_BLOCK: 0 = Old (pre-1224) or should not be in store, 1 = New and should be in store.
  	 * bit 28 - ENTRY_WRONG_STORE: 0 = Stored in correct store, 1 = stored in wrong store.
  	 * bit 0...23 - The first 3 bytes of the salted key.
  	 */
  	private final ResizablePersistentIntBuffer slotFilter;
  	private final File slotFilterFile;
  	/** If true, don't create a slot filter, don't keep it up to date, don't
  	 * do anything with it. */
  	private boolean slotFilterDisabled;
  	/** If true, then treat the slot filter as authoritative. If the slot filter
  	 * gives a certain content for a particular slot, assume it is right. This
  	 * saves a lot of seeks, both when reading and when writing. Note that the 
  	 * slot filter will indicate when it doesn't have any information about a 
  	 * slot, which is the default, which is why it has to be rebuilt on 
  	 * conversion from an old store. We normally also check slotFilterDisabled 
  	 * to see whether there *is* a slot filter. */
  	private static final boolean USE_SLOT_FILTER = true;
  	
  	private static final int SLOT_CHECKED = 1 << 31;
  	private static final int SLOT_OCCUPIED = 1 << 30;
  	private static final int SLOT_NEW_BLOCK = 1 << 29;
  	private static final int SLOT_WRONG_STORE = 1 << 28;
  
  	private static boolean logMINOR;
  	private static boolean logDEBUG;
  
  	private final File baseDir;
  	private final String name;
  	private final StoreCallback<T> callback;
  	private final boolean collisionPossible;
  	private final int headerBlockLength;
  	private final int fullKeyLength;
  	private final int dataBlockLength;
  	private final Random random;
  	private final File bloomFile;
  
  	private long storeSize;
  	private int generation;
  	private int flags;
  
  	private boolean preallocate = true;
  	public static boolean NO_CLEANER_SLEEP = false;
  	static final int SLOT_FILTER_INTERVAL = -1; // Write immediately.
  
  	/** If we have no space in this store, try writing it to the alternate store,
  	 * with the wrong store flag set. Note that we do not *read from* it, the caller
  	 * must do that. IMPORTANT LOCKING NOTE: This must only happen in one direction!
  	 * If two stores have altStore set to each other, deadlock is likely! (Infinite
  	 * recursion is also possible). However, fortunately we don't need to do it
  	 * bidirectionally - the cache needs more space from the store, but the store
  	 * grows so slowly it will hardly ever need more space from the cache. */
  	private SaltedHashFreenetStore<T> altStore;
  
  	public void setAltStore(SaltedHashFreenetStore<T> store) {
  		if(store.altStore != null) throw new IllegalStateException("Target must not have an altStore - deadlock can result");
  		altStore = store;
  	}
  
  	public static <T extends StorableBlock> SaltedHashFreenetStore<T> construct(File baseDir, String name, StoreCallback<T> callback, Random random,
  	        long maxKeys, boolean useSlotFilter, SemiOrderedShutdownHook shutdownHook, boolean preallocate, boolean resizeOnStart, Ticker exec, byte[] masterKey)
  	        throws IOException {
  		return new SaltedHashFreenetStore<T>(baseDir, name, callback, random, maxKeys, useSlotFilter,
  		        shutdownHook, preallocate, resizeOnStart, masterKey);
  	}
  
  	private SaltedHashFreenetStore(File baseDir, String name, StoreCallback<T> callback, Random random, long maxKeys,
  	        boolean enableSlotFilters, SemiOrderedShutdownHook shutdownHook, boolean preallocate, boolean resizeOnStart, byte[] masterKey) throws IOException {
  		logMINOR = Logger.shouldLog(LogLevel.MINOR, this);
  		logDEBUG = Logger.shouldLog(LogLevel.DEBUG, this);
  
  		this.baseDir = baseDir;
  		this.name = name;
  
  		this.callback = callback;
  		collisionPossible = callback.collisionPossible();
  		headerBlockLength = callback.headerLength();
  		fullKeyLength = callback.fullKeyLength();
  		dataBlockLength = callback.dataLength();
  
  		hdPadding = ((headerBlockLength + dataBlockLength) % 512 == 0 ? 0
  		        : 512 - (headerBlockLength + dataBlockLength) % 512);
  
  		this.random = random;
  		storeSize = maxKeys;
  		this.preallocate = preallocate;
  
  		lockManager = new LockManager();
  
  		// Create a directory it not exist
  		this.baseDir.mkdirs();
  		
  		if(storeSize > Integer.MAX_VALUE) // FIXME 64-bit.
  			throw new IllegalArgumentException("Store size over MAXINT not supported due to ResizablePersistentIntBuffer limitations.");
  
  		configFile = new File(this.baseDir, name + ".config");
  		boolean newStore = loadConfigFile(masterKey);
  		if(storeSize != 0 && storeSize != maxKeys && prevStoreSize == 0) {
  			// If not already resizing, start resizing to the new store size.
  			prevStoreSize = storeSize;
  			storeSize = maxKeys;
  			writeConfigFile();
  		}
  
  		newStore |= openStoreFiles(baseDir, name);
  
  		bloomFile = new File(this.baseDir, name + ".bloom");
  		if(bloomFile.exists()) {
  			bloomFile.delete();
  			System.err.println("Deleted old bloom filter for "+name+" - obsoleted by slot filter");
  			System.err.println("We will need to rebuild the slot filters, it will take a while and there will be a lot of disk access, but once it's done there should be a lot less disk access.");
  		}
  		
  		slotFilterFile = new File(this.baseDir, name + ".slotfilter");
  		int size = (int)Math.max(storeSize, prevStoreSize);
  		slotFilterDisabled = !enableSlotFilters;
  		if(!slotFilterDisabled) {
  			slotFilter = new ResizablePersistentIntBuffer(slotFilterFile, size, SLOT_FILTER_INTERVAL);
  			System.err.println("Slot filter (" + slotFilterFile + ") for " + name + " is loaded (new="+slotFilter.isNew()+").");
  		} else
  			slotFilter = null;
  
  		if ((flags & FLAG_DIRTY) != 0)
  			System.err.println("Datastore(" + name + ") is dirty.");
  
  		flags |= FLAG_DIRTY; // datastore is now dirty until flushAndClose()
  		writeConfigFile();
  
  		callback.setStore(this);
  		shutdownHook.addEarlyJob(new NativeThread(new ShutdownDB(), "Shutdown salted hash store", NativeThread.HIGH_PRIORITY, true));
  
  		cleanerThread = new Cleaner();
  		cleanerStatusUserAlert = new CleanerStatusUserAlert(cleanerThread);
  
  		// finish all resizing before continue
  		if (resizeOnStart && prevStoreSize != 0 && cleanerGlobalLock.tryLock()) {
  			System.out.println("Resizing datastore (" + name + ")");
  			try {
  				cleanerThread.resizeStore(prevStoreSize, false);
  			} finally {
  				cleanerGlobalLock.unlock();
  			}
  			writeConfigFile();
  		}
  		
  		if(((!slotFilterDisabled) && slotFilter.isNew()) && !newStore) {
  			flags |= FLAG_REBUILD_BLOOM;
  			System.out.println("Rebuilding slot filter because new");
  		} else if((flags & FLAG_REBUILD_BLOOM) != 0)
  			System.out.println("Slot filter still needs rebuilding");
  	}
  
  	private boolean started = false;
  
  	/** If start can be completed quickly, or longStart is true, then do it.
  	 * If longStart is false and start cannot be completed quickly, return
  	 * true. Don't start twice.
  	 * @throws IOException */
  	public boolean start(Ticker ticker, boolean longStart) throws IOException {
  
  		if(started) return true;
  		
  		if(!slotFilterDisabled)
  			slotFilter.start(ticker);
  
  		long curStoreFileSize = hdRAF.length();
  
  		long curMetaFileSize = metaRAF.length();
  
  		// If prevStoreSize is nonzero, that means that we are either shrinking or
  		// growing. Either way, the file size should be between the old size and the
  		// new size. If it is not, we should pad it until it is.
  
  		long smallerSize = storeSize;
  		if(prevStoreSize < storeSize && prevStoreSize > 0)
  			smallerSize = prevStoreSize;
  
  		if((smallerSize * (headerBlockLength + dataBlockLength + hdPadding) > curStoreFileSize) ||
  				(smallerSize * Entry.METADATA_LENGTH > curMetaFileSize)) {
  			// Pad it up to the minimum size before proceeding.
  			if(longStart) {
  				setStoreFileSize(storeSize, true);
  				curStoreFileSize = hdRAF.length();
  				curMetaFileSize = metaRAF.length();
  			} else
  				return true;
  		}
  
  		// Otherwise the resize will be completed by the Cleaner thread.
  		// However, we do still need to set storeFileOffsetReady
  
  		storeFileOffsetReady = Math.min(curStoreFileSize / (headerBlockLength + dataBlockLength + hdPadding), curMetaFileSize / Entry.METADATA_LENGTH);
  
  		if(ticker == null) {
  			cleanerThread.start();
  		} else
  			ticker.queueTimedJob(new FastRunnable() {
  
  				@Override
  				public void run() {
  					cleanerThread.start();
  				}
  
  			}, "Start cleaner thread", 0, true, false);
  
  		started = true;
  
  		return false;
  	}
  
  	@Override
  	public T fetch(byte[] routingKey, byte[] fullKey, boolean dontPromote, boolean canReadClientCache, boolean canReadSlashdotCache, boolean ignoreOldBlocks, BlockMetadata meta) throws IOException {
  		if (logMINOR)
  			Logger.minor(this, "Fetch " + HexUtil.bytesToHex(routingKey) + " for " + callback);
  
  		try {
  			int retry = 0;
  			while (!configLock.readLock().tryLock(2, TimeUnit.SECONDS)) {
  				if (shutdown)
  					return null;
  				if (retry++ > 10)
  					throw new IOException("lock timeout (20s)");
  			}
  		} catch(InterruptedException e) {
  			throw new IOException("interrupted: " +e);
  		}
  		byte[] digestedKey = cipherManager.getDigestedKey(routingKey);
  		try {
  			Map<Long, Condition> lockMap = lockDigestedKey(digestedKey, true);
  			if (lockMap == null) {
  				if (logDEBUG)
  					Logger.debug(this, "cannot lock key: " + HexUtil.bytesToHex(routingKey) + ", shutting down?");
  				return null;
  			}
  			try {
  				Entry entry = probeEntry(digestedKey, routingKey, true);
  				if (entry == null) {
  					misses.incrementAndGet();
  					return null;
  				}
  
  				if((entry.flag & Entry.ENTRY_NEW_BLOCK) == 0) {
  					if(ignoreOldBlocks) {
  						Logger.normal(this, "Ignoring old block");
  						return null;
  					}
  					if(meta != null)
  						meta.setOldBlock();
  				}
  
  				try {
  					T block = entry.getStorableBlock(routingKey, fullKey, canReadClientCache, canReadSlashdotCache, meta, null);
  					if (block == null) {
  						misses.incrementAndGet();
  						return null;
  					}
  					hits.incrementAndGet();
  					return block;
  				} catch (KeyVerifyException e) {
  					Logger.minor(this, "key verification exception", e);
  					misses.incrementAndGet();
  					return null;
  				}
  			} finally {
  				unlockDigestedKey(digestedKey, true, lockMap);
  			}
  		} finally {
  			configLock.readLock().unlock();
  		}
  	}
  
  	/**
  	 * Find and lock an entry with a specific routing key. This function would <strong>not</strong>
  	 * lock the entries.
  	 *
  	 * @param routingKey
  	 * @param withData
  	 * @return <code>Entry</code> object
  	 * @throws IOException
  	 */
  	private Entry probeEntry(byte[] digestedKey, byte[] routingKey, boolean withData) throws IOException {
  		
  		Entry entry = probeEntry0(digestedKey, routingKey, storeSize, withData);
  
  		if (entry == null && prevStoreSize != 0)
  			entry = probeEntry0(digestedKey, routingKey, prevStoreSize, withData);
  
  		return entry;
  	}
  
  	private Entry probeEntry0(byte[] digestedKey, byte[] routingKey, long probeStoreSize, boolean withData) throws IOException {
  		Entry entry = null;
  		long[] offset = getOffsetFromDigestedKey(digestedKey, probeStoreSize);
  
  		for (int i = 0; i < offset.length; i++) {
  			if (logDEBUG)
  				Logger.debug(this, "probing for i=" + i + ", offset=" + offset[i]);
  
  			try {
  				if(storeFileOffsetReady == -1 || offset[i] < this.storeFileOffsetReady) {
  					entry = readEntry(offset[i], digestedKey, routingKey, withData);
  					if (entry != null)
  						return entry;
  				}
  			} catch (EOFException e) {
  				if (prevStoreSize == 0) // may occur on store shrinking
  					Logger.error(this, "EOFException on probeEntry", e);
  				continue;
  			}
  		}
  		return null;
  	}
  
  	@Override
  	public void put(T block, byte[] data, byte[] header, boolean overwrite, boolean isOldBlock) throws IOException, KeyCollisionException {
  		put(block, data, header, overwrite, isOldBlock, false);
  	}
  
  	public boolean put(T block, byte[] data, byte[] header, boolean overwrite, boolean isOldBlock, boolean wrongStore) throws IOException, KeyCollisionException {
  		byte[] routingKey = block.getRoutingKey();
  		byte[] fullKey = block.getFullKey();
  
  		if (logMINOR)
  			Logger.minor(this, "Putting " + HexUtil.bytesToHex(routingKey) + " (" + name + ")");
  
  		try {
  			int retry = 0;
  			while (!configLock.readLock().tryLock(2, TimeUnit.SECONDS)) {
  				if (shutdown)
  					return true;
  				if (retry++ > 10)
  					throw new IOException("lock timeout (20s)");
  			}
  		} catch(InterruptedException e) {
  			throw new IOException("interrupted: " +e);
  		}
  		byte[] digestedKey = cipherManager.getDigestedKey(routingKey);
  		try {
  			Map<Long, Condition> lockMap = lockDigestedKey(digestedKey, false);
  			if (lockMap == null) {
  				if (logDEBUG)
  					Logger.debug(this, "cannot lock key: " + HexUtil.bytesToHex(routingKey) + ", shutting down?");
  				return false;
  			}
  			try {
  				/*
  				 * Use lazy loading here. This may lost data if digestedRoutingKey collide but
  				 * collisionPossible is false. Should be very rare as digestedRoutingKey is a
  				 * SHA-256 hash.
  				 */
  				Entry oldEntry = probeEntry(digestedKey, routingKey, false);
  				if (oldEntry != null && !oldEntry.isFree()) {
  					long oldOffset = oldEntry.curOffset;
  					try {
  						if (!collisionPossible) {
  							if((oldEntry.flag & Entry.ENTRY_NEW_BLOCK) == 0 && !isOldBlock) {
  								oldEntry = readEntry(oldEntry.curOffset, digestedKey, routingKey, true);
  								// Currently flagged as an old block
  								oldEntry.flag |= Entry.ENTRY_NEW_BLOCK;
  								if(logMINOR) Logger.minor(this, "Setting old block to new block");
  								oldEntry.storeSize = storeSize;
  								writeEntry(oldEntry, digestedKey, oldOffset);
  							}
  							return true;
  						}
  						oldEntry.setHD(readHD(oldOffset)); // read from disk
  						T oldBlock = oldEntry.getStorableBlock(routingKey, fullKey, false, false, null, (block instanceof SSKBlock) ? ((SSKBlock)block).getPubKey() : null);
  						if (block.equals(oldBlock)) {
  							if(logDEBUG) Logger.debug(this, "Block already stored");
  							if((oldEntry.flag & Entry.ENTRY_NEW_BLOCK) == 0 && !isOldBlock) {
  								// Currently flagged as an old block
  								oldEntry.flag |= Entry.ENTRY_NEW_BLOCK;
  								if(logMINOR) Logger.minor(this, "Setting old block to new block");
  								oldEntry.storeSize = storeSize;
  								writeEntry(oldEntry, digestedKey, oldOffset);
  							}
  							return false; // already in store
  						} else if (!overwrite) {
  							throw new KeyCollisionException();
  						}
  					} catch (KeyVerifyException e) {
  						// ignore
  					}
  
  					// Overwrite old offset with same key
  					Entry entry = new Entry(routingKey, header, data, !isOldBlock, wrongStore);
  					writeEntry(entry, digestedKey, oldOffset);
  					if (oldEntry.generation != generation)
  						keyCount.incrementAndGet();
  					return true;
  				}
  
  				Entry entry = new Entry(routingKey, header, data, !isOldBlock, wrongStore);
  				long[] offset = entry.getOffset();
  
  				int firstWrongStoreIndex = -1;
  				int wrongStoreCount = 0;
  
  				for (int i = 0; i < offset.length; i++) {
  					if(offset[i] < storeFileOffsetReady) {
  						long flag = getFlag(offset[i], false);
  						if((flag & Entry.ENTRY_FLAG_OCCUPIED) == 0) {
  							// write to free block
  							if (logDEBUG)
  								Logger.debug(this, "probing, write to i=" + i + ", offset=" + offset[i]);
  							writeEntry(entry, digestedKey, offset[i]);
  							keyCount.incrementAndGet();
  							onWrite();
  							return true;
  						} else if(((flag & Entry.ENTRY_WRONG_STORE) == Entry.ENTRY_WRONG_STORE)) {
  							if (wrongStoreCount == 0)
  								firstWrongStoreIndex = i;
  							wrongStoreCount++;
  						}
  					}
  				}
  
  				if((!wrongStore) && altStore != null) {
  					if(altStore.put(block, data, header, overwrite, isOldBlock, true)) {
  						if(logMINOR) Logger.minor(this, "Successfully wrote block to wrong store "+altStore+" on "+this);
  						return true;
  					} else {
  						if(logMINOR) Logger.minor(this, "Writing to wrong store "+altStore+" on "+this+" failed");
  					}
  				}
  
  				// There are no free slots for this Entry, so some slot will have to get overwritten.
  				int indexToOverwrite = -1;
  
  				if(wrongStore) {
  					// Distribute overwrites evenly between the right store and the wrong store.
  					int a = OPTION_MAX_PROBE;
  					int b = wrongStoreCount;
  					if(random.nextInt(a+b) < b)
  						// Allow the overwrite to happen in the wrong store.
  						indexToOverwrite = firstWrongStoreIndex;
  					else
  						// Force the overwrite to happen in the right store.
  						return false;
  				}
  				else {
  					// By default, overwrite offset[0] when not writing to wrong store.
  					indexToOverwrite = 0;
  				}
  
  				// Do the overwriting.
  				if (logDEBUG)
  					Logger.debug(this, "collision, write to i=" + indexToOverwrite + ", offset=" + offset[indexToOverwrite]);
  				oldEntry = readEntry(offset[indexToOverwrite], null, null, false);
  				writeEntry(entry, digestedKey, offset[indexToOverwrite]);
  				if (oldEntry.generation != generation)
  					keyCount.incrementAndGet();
  				onWrite();
  				return true;
  			} finally {
  				unlockDigestedKey(digestedKey, false, lockMap);
  			}
  		} finally {
  			configLock.readLock().unlock();
  		}
  	}
  
  	private boolean onWrite() {
  		return (writes.incrementAndGet() % (storeSize*2) == 0);
  	}
  	
  	// ------------- Entry I/O
  	// meta-data file
  	private File metaFile;
  	private RandomAccessFile metaRAF;
  	private FileChannel metaFC;
  	// header+data file
  	private File hdFile;
  	private RandomAccessFile hdRAF;
  	private FileChannel hdFC;
  	private final int hdPadding;
  
  	/**
  	 * Data entry
  	 *
  	 * <pre>
  	 *  META-DATA BLOCK
  	 *       +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
  	 *       |0|1|2|3|4|5|6|7|8|9|A|B|C|D|E|F|
  	 *  +----+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
  	 *  |0000|                               |
  	 *  +----+     Digested Routing Key      |
  	 *  |0010|                               |
  	 *  +----+-------------------------------+
  	 *  |0020|       Data Encrypt IV         |
  	 *  +----+---------------+---------------+
  	 *  |0030|     Flag      |  Store Size   |
  	 *  +----+---------------+---------------+
  	 *  |0040|       Plain Routing Key       |
  	 *  |0050| (Only if ENTRY_FLAG_PLAINKEY) |
  	 *  +----+-------+-----------------------+
  	 *  |0060|  Gen  |    Reserved           |
  	 *  +----+-------+-----------------------+
  	 *  |0070|            Reserved           |
  	 *  +----+-------------------------------+
  	 *
  	 *  Gen = Generation
  	 * </pre>
  	 */
  	class Entry {
  		/** Flag for occupied space */
  		private final static long ENTRY_FLAG_OCCUPIED = 0x00000001L;
  		/** Flag for plain key available */
  		private final static long ENTRY_FLAG_PLAINKEY = 0x00000002L;
  		/** Flag for block added after we stopped caching local (and high htl) requests */
  		private final static long ENTRY_NEW_BLOCK = 0x00000004L;
  		/** Flag set if the block was stored in the wrong datastore i.e. store instead of cache */
  		private final static long ENTRY_WRONG_STORE = 0x00000008L;
  
  		/** Control block length */
  		private static final int METADATA_LENGTH = 0x80;
  
  		byte[] plainRoutingKey;
  		byte[] digestedRoutingKey;
  		byte[] dataEncryptIV;
  		private long flag;
  		private long storeSize;
  		private int generation;
  		byte[] header;
  		byte[] data;
  
  		boolean isEncrypted;
  		private long curOffset = -1;
  
  		private Entry() {
  		}
  
  		private Entry(ByteBuffer metaDataBuf, ByteBuffer hdBuf) {
  			assert metaDataBuf.remaining() == METADATA_LENGTH;
  
  			digestedRoutingKey = new byte[0x20];
  			metaDataBuf.get(digestedRoutingKey);
  
  			dataEncryptIV = new byte[0x10];
  			metaDataBuf.get(dataEncryptIV);
  
  			flag = metaDataBuf.getLong();
  			storeSize = metaDataBuf.getLong();
  
  			if ((flag & ENTRY_FLAG_PLAINKEY) != 0) {
  				plainRoutingKey = new byte[0x20];
  				metaDataBuf.get(plainRoutingKey);
  			}
  
  			metaDataBuf.position(0x60);
  			generation = metaDataBuf.getInt();
  
  			isEncrypted = true;
  
  			if (hdBuf != null)
  				setHD(hdBuf);
  		}
  
  		/**
  		 * Set header/data after construction.
  		 *
  		 * @param storeBuf
  		 * @param store
  		 */
  		private void setHD(ByteBuffer hdBuf) {
  			assert hdBuf.remaining() == headerBlockLength + dataBlockLength + hdPadding;
  			assert isEncrypted;
  
  			header = new byte[headerBlockLength];
  			hdBuf.get(header);
  
  			data = new byte[dataBlockLength];
  			hdBuf.get(data);
  		}
  
  		/**
  		 * Create a new entry
  		 *
  		 * @param plainRoutingKey
  		 * @param header
  		 * @param data
  		 */
  		private Entry(byte[] plainRoutingKey, byte[] header, byte[] data, boolean newBlock, boolean wrongStore) {
  			this.plainRoutingKey = plainRoutingKey;
  
  			flag = ENTRY_FLAG_OCCUPIED;
  			if(newBlock)
  				flag |= ENTRY_NEW_BLOCK;
  			if(wrongStore)
  				flag |= ENTRY_WRONG_STORE;
  			this.storeSize = SaltedHashFreenetStore.this.storeSize;
  			this.generation = SaltedHashFreenetStore.this.generation;
  
  			// header/data will be overwritten in encrypt()/decrypt(),
  			// let's make a copy here
  			this.header = new byte[headerBlockLength];
  			System.arraycopy(header, 0, this.header, 0, headerBlockLength);
  			this.data = new byte[dataBlockLength];
  			System.arraycopy(data, 0, this.data, 0, dataBlockLength);
  
  			if (OPTION_SAVE_PLAINKEY) {
  				flag |= ENTRY_FLAG_PLAINKEY;
  			}
  
  			isEncrypted = false;
  		}
  
  		private ByteBuffer toMetaDataBuffer() {
  			ByteBuffer out = ByteBuffer.allocate(METADATA_LENGTH);
  			cipherManager.encrypt(this, random);
  
  			out.put(getDigestedRoutingKey());
  			out.put(dataEncryptIV);
  			out.putLong(flag);
  			out.putLong(storeSize);
  
  			if ((flag & ENTRY_FLAG_PLAINKEY) != 0 && plainRoutingKey != null) {
  				assert plainRoutingKey.length == 0x20;
  				out.put(plainRoutingKey);
  			}
  
  			out.position(0x60);
  			out.putInt(generation);
  
  			out.position(0);
  			return out;
  		}
  
  		private ByteBuffer toHDBuffer() {
  			assert isEncrypted; // should have encrypted to get dataEncryptIV in control buffer
  			assert header.length == headerBlockLength;
  			assert data.length == dataBlockLength;
  
  			if (header == null || data == null)
  				return null;
  
  			ByteBuffer out = ByteBuffer.allocate(headerBlockLength + dataBlockLength + hdPadding);
  			out.put(header);
  			out.put(data);
  
  			out.position(0);
  			return out;
  		}
  
  		private T getStorableBlock(byte[] routingKey, byte[] fullKey, boolean canReadClientCache, boolean canReadSlashdotCache, BlockMetadata meta, DSAPublicKey knownKey) throws KeyVerifyException {
  			if (isFree() || header == null || data == null)
  				return null; // this is a free block
  			if (!cipherManager.decrypt(this, routingKey))
  				return null;
  
  			T block = callback.construct(data, header, routingKey, fullKey, canReadClientCache, canReadSlashdotCache, meta, knownKey);
  			byte[] blockRoutingKey = block.getRoutingKey();
  
  			if (!Arrays.equals(blockRoutingKey, routingKey)) {
  				// can't recover, as decrypt() depends on a correct route key
  				return null;
  			}
  
  			return block;
  		}
  
  		private long[] getOffset() {
  			if (digestedRoutingKey != null)
  				return getOffsetFromDigestedKey(digestedRoutingKey, storeSize);
  			else
  				return getOffsetFromPlainKey(plainRoutingKey, storeSize);
  		}
  
  		private boolean isFree() {
  			return (flag & ENTRY_FLAG_OCCUPIED) == 0;
  		}
  
  		byte[] getDigestedRoutingKey() {
  			if (digestedRoutingKey == null)
  