gt:             if(outputStreamOpen) throw new IOException("Must close first");
context: package freenet.support.io;
  
  import freenet.client.async.ClientContext;
  import freenet.crypt.MasterSecret;
  import freenet.support.api.LockableRandomAccessBuffer;
  import freenet.support.api.RandomAccessBucket;
  
  import java.io.*;
  
  /** Pads a bucket to the next power of 2 file size. 
   * Note that self-terminating formats do not work with AEADCryptBucket; it needs to know the real 
   * length. This pads with FileUtil.fill(), which is reasonably random but is faster than using
   * SecureRandom, and vastly more secure than using a non-secure Random.
   */
  public class PaddedRandomAccessBucket implements RandomAccessBucket, Serializable {
      
      private static final long serialVersionUID = 1L;
      private final RandomAccessBucket underlying;
      private long size;
      private transient boolean outputStreamOpen;
      private boolean readOnly;
  
      /** Create a PaddedBucket, assumed to be empty */
      public PaddedRandomAccessBucket(RandomAccessBucket underlying) {
          this(underlying, 0);
      }
      
      /** Create a PaddedBucket, specifying the actual size of the existing bucket, which we
       * do not store on disk.
       * @param underlying The underlying bucket.
       * @param size The actual size of the data.
       */
      public PaddedRandomAccessBucket(RandomAccessBucket underlying, long size) {
          this.underlying = underlying;
          this.size = size;
      }
      
      protected PaddedRandomAccessBucket() {
          // For serialization.
          underlying = null;
          size = 0;
      }
  
      @Override
      public OutputStream getOutputStream() throws IOException {
          OutputStream os; 
          synchronized(this) {
              if(outputStreamOpen) throw new IOException("Already have an OutputStream for "+this);
              os = underlying.getOutputStream();
              outputStreamOpen = true;
              size = 0;
          }
          return new MyOutputStream(os);
      }
      
      @Override
      public OutputStream getOutputStreamUnbuffered() throws IOException {
          OutputStream os; 
          synchronized(this) {
              if(outputStreamOpen) throw new IOException("Already have an OutputStream for "+this);
              os = underlying.getOutputStreamUnbuffered();
              outputStreamOpen = true;
              size = 0;
          }
          return new MyOutputStream(os);
      }
      
      private class MyOutputStream extends FilterOutputStream {
          
          private boolean closed;
          
          MyOutputStream(OutputStream os) {
              super(os);
          }
          
          @Override
          public void write(int b) throws IOException {
              out.write(b);
              synchronized(PaddedRandomAccessBucket.this) {
                  size++;
              }
          }
          
          @Override
          public void write(byte[] buf) throws IOException {
              out.write(buf);
              synchronized(PaddedRandomAccessBucket.this) {
                  if(closed) throw new IOException("Already closed");
                  size += buf.length;
              }
          }
          
          @Override
          public void write(byte[] buf, int offset, int length) throws IOException {
              out.write(buf, offset, length);
              synchronized(PaddedRandomAccessBucket.this) {
                  if(closed) throw new IOException("Already closed");
                  size += length;
              }
          }
          
          @Override
          public void close() throws IOException {
              try {
                  long padding;
                  synchronized(PaddedRandomAccessBucket.this) {
                      if(closed) return;
                      closed = true;
                      long paddedLength = paddedLength(size);
                      padding = paddedLength - size;
                  }
                  FileUtil.fill(out, padding);
                  out.close();
              } finally {
                  synchronized(PaddedRandomAccessBucket.this) {
                      outputStreamOpen = false;
                  }
              }
          }
          
          public String toString() {
              return "TrivialPaddedBucketOutputStream:"+out+"("+PaddedRandomAccessBucket.this+")";
          }
  
      }
      
      private static final long MIN_PADDED_SIZE = 1024;
      
      private long paddedLength(long size) {
          if(size < MIN_PADDED_SIZE) size = MIN_PADDED_SIZE;
          if(size == MIN_PADDED_SIZE) return size;
          long min = MIN_PADDED_SIZE;
          long max = MIN_PADDED_SIZE << 1;
          while(true) {
              if(max < 0)
                  throw new Error("Impossible size: "+size+" - min="+min+", max="+max);
              if(size < min)
                  throw new IllegalStateException("???");
              if((size >= min) && (size <= max)) {
                  return max;
              }
              min = max;
              max = max << 1;
          }
      }
  
      @Override
      public InputStream getInputStream() throws IOException {
          return new MyInputStream(underlying.getInputStream());
      }
      
      @Override
      public InputStream getInputStreamUnbuffered() throws IOException {
          return new MyInputStream(underlying.getInputStreamUnbuffered());
      }
      
      private class MyInputStream extends FilterInputStream {
  
          private long counter;
          
          public MyInputStream(InputStream is) {
              super(is);
          }
          
          @Override
          public int read() throws IOException {
              synchronized(PaddedRandomAccessBucket.this) {
                  if(counter >= size) return -1;
              }
              int ret = in.read();
              synchronized(PaddedRandomAccessBucket.this) {
                  counter++;
              }
              return ret;
          }
          
          @Override
          public int read(byte[] buf) throws IOException {
              return read(buf, 0, buf.length);
          }
          
          @Override
          public int read(byte[] buf, int offset, int length) throws IOException {
              synchronized(PaddedRandomAccessBucket.this) {
                  if(length < 0) return -1;
                  if(length == 0) return 0;
                  if(counter >= size) return -1;
                  if(counter + length >= size) {
                      length = (int)Math.min(length, size - counter);
                  }
              }
              int ret = in.read(buf, offset, length);
              synchronized(PaddedRandomAccessBucket.this) {
                  if(ret > 0)
                  counter += ret;
              }
              return ret;
          }
          
          public long skip(long length) throws IOException {
              synchronized(PaddedRandomAccessBucket.this) {
                  if(counter >= size) return -1;
                  if(counter + length >= size) {
                      length = (int)Math.min(length, counter + length - size);
                  }
              }
              long ret = in.skip(length);
              synchronized(PaddedRandomAccessBucket.this) {
                  if(ret > 0) counter += ret;
              }
              return ret;
          }
          
          @Override
          public synchronized int available() throws IOException {
              long max = size - counter;
              int ret = in.available();
              if(max < ret) ret = (int)max;
              if(ret < 0) return 0;
              return ret;
          }
          
      }
  
      @Override
      public String getName() {
          return "Padded:"+underlying.getName();
      }
  
      @Override
      /** Get the size of the data written to the bucket (not the padded size). */
      public synchronized long size() {
          return size;
      }
  
      @Override
      public synchronized boolean isReadOnly() {
          return readOnly;
      }
  
      @Override
      public synchronized void setReadOnly() {
          readOnly = true;
      }
  
      @Override
      public void free() {
          underlying.free();
      }
  
      @Override
      public RandomAccessBucket createShadow() {
          RandomAccessBucket shadow = underlying.createShadow();
          PaddedRandomAccessBucket ret = new PaddedRandomAccessBucket(shadow, size);
          ret.setReadOnly();
          return ret;
      }
  
      @Override
      public void onResume(ClientContext context) throws ResumeFailedException {
          underlying.onResume(context);
      }
      
      static final int MAGIC = 0x95c42e34;
      static final int VERSION = 1;
  
      @Override
      public void storeTo(DataOutputStream dos) throws IOException {
          dos.writeInt(MAGIC);
          dos.writeInt(VERSION);
          dos.writeLong(size);
          dos.writeBoolean(readOnly);
          underlying.storeTo(dos);
      }
      
      protected PaddedRandomAccessBucket(DataInputStream dis, FilenameGenerator fg, 
              PersistentFileTracker persistentFileTracker, MasterSecret masterKey) 
      throws IOException, StorageFormatException, ResumeFailedException {
          int version = dis.readInt();
          if(version != VERSION) throw new StorageFormatException("Bad version");
          size = dis.readLong();
          readOnly = dis.readBoolean();
          underlying = (RandomAccessBucket) BucketTools.restoreFrom(dis, fg, persistentFileTracker, masterKey);
      }
  
      @Override
      public LockableRandomAccessBuffer toRandomAccessBuffer() throws IOException {
          synchronized(this) {
  