Mega Code Archive

 
Categories / Java / File Input Output
 

Performs Base-64 decoding on an underlying stream

/****************************************************************  * Licensed to the Apache Software Foundation (ASF) under one   *  * or more contributor license agreements.  See the NOTICE file *  * distributed with this work for additional information        *  * regarding copyright ownership.  The ASF licenses this file   *  * to you under the Apache License, Version 2.0 (the            *  * "License"); you may not use this file except in compliance   *  * with the License.  You may obtain a copy of the License at   *  *                                                              *  *   http://www.apache.org/licenses/LICENSE-2.0                 *  *                                                              *  * Unless required by applicable law or agreed to in writing,   *  * software distributed under the License is distributed on an  *  * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *  * KIND, either express or implied.  See the License for the    *  * specific language governing permissions and limitations      *  * under the License.                                           *  ****************************************************************/ import java.io.FilterOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.nio.BufferUnderflowException; import java.util.HashSet; import java.util.Iterator; import java.util.NoSuchElementException; import java.util.Set; /**  * Performs Base-64 decoding on an underlying stream.  */ public class Base64InputStream extends InputStream {     private static final int ENCODED_BUFFER_SIZE = 1536;     private static final int[] BASE64_DECODE = new int[256];     static {         for (int i = 0; i < 256; i++)             BASE64_DECODE[i] = -1;         for (int i = 0; i < Base64OutputStream.BASE64_TABLE.length; i++)             BASE64_DECODE[Base64OutputStream.BASE64_TABLE[i] & 0xff] = i;     }     private static final byte BASE64_PAD = '=';     private static final int EOF = -1;     private final byte[] singleByte = new byte[1];     private boolean strict;     private final InputStream in;     private boolean closed = false;     private final byte[] encoded = new byte[ENCODED_BUFFER_SIZE];     private int position = 0; // current index into encoded buffer     private int size = 0; // current size of encoded buffer     private final ByteQueue q = new ByteQueue();     private boolean eof; // end of file or pad character reached     public Base64InputStream(InputStream in) {         this(in, false);     }     public Base64InputStream(InputStream in, boolean strict) {         if (in == null)             throw new IllegalArgumentException();         this.in = in;         this.strict = strict;     }     @Override     public int read() throws IOException {         if (closed)             throw new IOException("Base64InputStream has been closed");         while (true) {             int bytes = read0(singleByte, 0, 1);             if (bytes == EOF)                 return EOF;             if (bytes == 1)                 return singleByte[0] & 0xff;         }     }     @Override     public int read(byte[] buffer) throws IOException {         if (closed)             throw new IOException("Base64InputStream has been closed");         if (buffer == null)             throw new NullPointerException();         if (buffer.length == 0)             return 0;         return read0(buffer, 0, buffer.length);     }     @Override     public int read(byte[] buffer, int offset, int length) throws IOException {         if (closed)             throw new IOException("Base64InputStream has been closed");         if (buffer == null)             throw new NullPointerException();         if (offset < 0 || length < 0 || offset + length > buffer.length)             throw new IndexOutOfBoundsException();         if (length == 0)             return 0;         return read0(buffer, offset, offset + length);     }     @Override     public void close() throws IOException {         if (closed)             return;         closed = true;     }     private int read0(final byte[] buffer, final int from, final int to)             throws IOException {         int index = from; // index into given buffer         // check if a previous invocation left decoded bytes in the queue         int qCount = q.count();         while (qCount-- > 0 && index < to) {             buffer[index++] = q.dequeue();         }         // eof or pad reached?         if (eof)             return index == from ? EOF : index - from;         // decode into given buffer         int data = 0; // holds decoded data; up to four sextets         int sextets = 0; // number of sextets         while (index < to) {             // make sure buffer not empty             while (position == size) {                 int n = in.read(encoded, 0, encoded.length);                 if (n == EOF) {                     eof = true;                     if (sextets != 0) {                         // error in encoded data                         handleUnexpectedEof(sextets);                     }                     return index == from ? EOF : index - from;                 } else if (n > 0) {                     position = 0;                     size = n;                 } else {                     assert n == 0;                 }             }             // decode buffer             while (position < size && index < to) {                 int value = encoded[position++] & 0xff;                 if (value == BASE64_PAD) {                     index = decodePad(data, sextets, buffer, index, to);                     return index - from;                 }                 int decoded = BASE64_DECODE[value];                 if (decoded < 0) // -1: not a base64 char                     continue;                 data = (data << 6) | decoded;                 sextets++;                 if (sextets == 4) {                     sextets = 0;                     byte b1 = (byte) (data >>> 16);                     byte b2 = (byte) (data >>> 8);                     byte b3 = (byte) data;                     if (index < to - 2) {                         buffer[index++] = b1;                         buffer[index++] = b2;                         buffer[index++] = b3;                     } else {                         if (index < to - 1) {                             buffer[index++] = b1;                             buffer[index++] = b2;                             q.enqueue(b3);                         } else if (index < to) {                             buffer[index++] = b1;                             q.enqueue(b2);                             q.enqueue(b3);                         } else {                             q.enqueue(b1);                             q.enqueue(b2);                             q.enqueue(b3);                         }                         assert index == to;                         return to - from;                     }                 }             }         }         assert sextets == 0;         assert index == to;         return to - from;     }     private int decodePad(int data, int sextets, final byte[] buffer,             int index, final int end) throws IOException {         eof = true;         if (sextets == 2) {             // one byte encoded as "XY=="             byte b = (byte) (data >>> 4);             if (index < end) {                 buffer[index++] = b;             } else {                 q.enqueue(b);             }         } else if (sextets == 3) {             // two bytes encoded as "XYZ="             byte b1 = (byte) (data >>> 10);             byte b2 = (byte) ((data >>> 2) & 0xFF);             if (index < end - 1) {                 buffer[index++] = b1;                 buffer[index++] = b2;             } else if (index < end) {                 buffer[index++] = b1;                 q.enqueue(b2);             } else {                 q.enqueue(b1);                 q.enqueue(b2);             }         } else {             // error in encoded data             handleUnexpecedPad(sextets);         }         return index;     }     private void handleUnexpectedEof(int sextets) throws IOException {         if (strict)             throw new IOException("unexpected end of file");     }     private void handleUnexpecedPad(int sextets) throws IOException {         if (strict)             throw new IOException("unexpected padding character");     } } class ByteQueue implements Iterable<Byte> {   private UnboundedFifoByteBuffer buf;   private int initialCapacity = -1;   public ByteQueue() {       buf = new UnboundedFifoByteBuffer();   }   public ByteQueue(int initialCapacity) {       buf = new UnboundedFifoByteBuffer(initialCapacity);       this.initialCapacity = initialCapacity;   }   public void enqueue(byte b) {       buf.add(b);   }   public byte dequeue() {       return buf.remove();   }   public int count() {       return buf.size();   }   public void clear() {       if (initialCapacity != -1)           buf = new UnboundedFifoByteBuffer(initialCapacity);       else           buf = new UnboundedFifoByteBuffer();   }   public Iterator<Byte> iterator() {       return buf.iterator();   } } /**  * UnboundedFifoByteBuffer is a very efficient buffer implementation.  * According to performance testing, it exhibits a constant access time, but it  * also outperforms ArrayList when used for the same purpose.  * <p>  * The removal order of an <code>UnboundedFifoByteBuffer</code> is based on the insertion  * order; elements are removed in the same order in which they were added.  * The iteration order is the same as the removal order.  * <p>  * The {@link #remove()} and {@link #get()} operations perform in constant time.  * The {@link #add(Object)} operation performs in amortized constant time.  All  * other operations perform in linear time or worse.  * <p>  * Note that this implementation is not synchronized.  The following can be  * used to provide synchronized access to your <code>UnboundedFifoByteBuffer</code>:  * <pre>  *   Buffer fifo = BufferUtils.synchronizedBuffer(new UnboundedFifoByteBuffer());  * </pre>  * <p>  * This buffer prevents null objects from being added.  *  * @since Commons Collections 3.0 (previously in main package v2.1)  */ class UnboundedFifoByteBuffer {     protected byte[] buffer;     protected int head;     protected int tail;     /**      * Constructs an UnboundedFifoByteBuffer with the default number of elements.      * It is exactly the same as performing the following:      *      * <pre>      *   new UnboundedFifoByteBuffer(32);      * </pre>      */     public UnboundedFifoByteBuffer() {         this(32);     }     /**      * Constructs an UnboundedFifoByteBuffer with the specified number of elements.      * The integer must be a positive integer.      *      * @param initialSize  the initial size of the buffer      * @throws IllegalArgumentException  if the size is less than 1      */     public UnboundedFifoByteBuffer(int initialSize) {         if (initialSize <= 0) {             throw new IllegalArgumentException("The size must be greater than 0");         }         buffer = new byte[initialSize + 1];         head = 0;         tail = 0;     }     /**      * Returns the number of elements stored in the buffer.      *      * @return this buffer's size      */     public int size() {         int size = 0;         if (tail < head) {             size = buffer.length - head + tail;         } else {             size = tail - head;         }         return size;     }     /**      * Returns true if this buffer is empty; false otherwise.      *      * @return true if this buffer is empty      */     public boolean isEmpty() {         return (size() == 0);     }     /**      * Adds the given element to this buffer.      *      * @param b  the byte to add      * @return true, always      */     public boolean add(final byte b) {         if (size() + 1 >= buffer.length) {             byte[] tmp = new byte[((buffer.length - 1) * 2) + 1];             int j = 0;             for (int i = head; i != tail;) {                 tmp[j] = buffer[i];                 buffer[i] = 0;                 j++;                 i++;                 if (i == buffer.length) {                     i = 0;                 }             }             buffer = tmp;             head = 0;             tail = j;         }         buffer[tail] = b;         tail++;         if (tail >= buffer.length) {             tail = 0;         }         return true;     }     /**      * Returns the next object in the buffer.      *      * @return the next object in the buffer      * @throws BufferUnderflowException  if this buffer is empty      */     public byte get() {         if (isEmpty()) {             throw new IllegalStateException("The buffer is already empty");         }         return buffer[head];     }     /**      * Removes the next object from the buffer      *      * @return the removed object      * @throws BufferUnderflowException  if this buffer is empty      */     public byte remove() {         if (isEmpty()) {             throw new IllegalStateException("The buffer is already empty");         }         byte element = buffer[head];         head++;         if (head >= buffer.length) {             head = 0;         }         return element;     }     /**      * Increments the internal index.      *      * @param index  the index to increment      * @return the updated index      */     private int increment(int index) {         index++;         if (index >= buffer.length) {             index = 0;         }         return index;     }     /**      * Decrements the internal index.      *      * @param index  the index to decrement      * @return the updated index      */     private int decrement(int index) {         index--;         if (index < 0) {             index = buffer.length - 1;         }         return index;     }     /**      * Returns an iterator over this buffer's elements.      *      * @return an iterator over this buffer's elements      */     public Iterator<Byte> iterator() {         return new Iterator<Byte>() {             private int index = head;             private int lastReturnedIndex = -1;             public boolean hasNext() {                 return index != tail;             }             public Byte next() {                 if (!hasNext()) {                     throw new NoSuchElementException();                 }                 lastReturnedIndex = index;                 index = increment(index);                 return new Byte(buffer[lastReturnedIndex]);             }             public void remove() {                 if (lastReturnedIndex == -1) {                     throw new IllegalStateException();                 }                 // First element can be removed quickly                 if (lastReturnedIndex == head) {                     UnboundedFifoByteBuffer.this.remove();                     lastReturnedIndex = -1;                     return;                 }                 // Other elements require us to shift the subsequent elements                 int i = lastReturnedIndex + 1;                 while (i != tail) {                     if (i >= buffer.length) {                         buffer[i - 1] = buffer[0];                         i = 0;                     } else {                         buffer[i - 1] = buffer[i];                         i++;                     }                 }                 lastReturnedIndex = -1;                 tail = decrement(tail);                 buffer[tail] = 0;                 index = decrement(index);             }         };     } } class Base64OutputStream extends FilterOutputStream {   // Default line length per RFC 2045 section 6.8.   private static final int DEFAULT_LINE_LENGTH = 76;   // CRLF line separator per RFC 2045 section 2.1.   private static final byte[] CRLF_SEPARATOR = { '\r', '\n' };   // This array is a lookup table that translates 6-bit positive integer index   // values into their "Base64 Alphabet" equivalents as specified in Table 1   // of RFC 2045.   static final byte[] BASE64_TABLE = { 'A', 'B', 'C', 'D', 'E', 'F',           'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S',           'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f',           'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's',           't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5',           '6', '7', '8', '9', '+', '/' };   // Byte used to pad output.   private static final byte BASE64_PAD = '=';   // This set contains all base64 characters including the pad character. Used   // solely to check if a line separator contains any of these characters.   private static final Set<Byte> BASE64_CHARS = new HashSet<Byte>();   static {       for (byte b : BASE64_TABLE) {           BASE64_CHARS.add(b);       }       BASE64_CHARS.add(BASE64_PAD);   }   // Mask used to extract 6 bits   private static final int MASK_6BITS = 0x3f;   private static final int ENCODED_BUFFER_SIZE = 2048;   private final byte[] singleByte = new byte[1];   private final int lineLength;   private final byte[] lineSeparator;   private boolean closed = false;   private final byte[] encoded;   private int position = 0;   private int data = 0;   private int modulus = 0;   private int linePosition = 0;   /**    * Creates a <code>Base64OutputStream</code> that writes the encoded data    * to the given output stream using the default line length (76) and line    * separator (CRLF).    *     * @param out    *            underlying output stream.    */   public Base64OutputStream(OutputStream out) {       this(out, DEFAULT_LINE_LENGTH, CRLF_SEPARATOR);   }   /**    * Creates a <code>Base64OutputStream</code> that writes the encoded data    * to the given output stream using the given line length and the default    * line separator (CRLF).    * <p>    * The given line length will be rounded up to the nearest multiple of 4. If    * the line length is zero then the output will not be split into lines.    *     * @param out    *            underlying output stream.    * @param lineLength    *            desired line length.    */   public Base64OutputStream(OutputStream out, int lineLength) {       this(out, lineLength, CRLF_SEPARATOR);   }   /**    * Creates a <code>Base64OutputStream</code> that writes the encoded data    * to the given output stream using the given line length and line    * separator.    * <p>    * The given line length will be rounded up to the nearest multiple of 4. If    * the line length is zero then the output will not be split into lines and    * the line separator is ignored.    * <p>    * The line separator must not include characters from the BASE64 alphabet    * (including the padding character <code>=</code>).    *     * @param out    *            underlying output stream.    * @param lineLength    *            desired line length.    * @param lineSeparator    *            line separator to use.    */   public Base64OutputStream(OutputStream out, int lineLength,           byte[] lineSeparator) {       super(out);       if (out == null)           throw new IllegalArgumentException();       if (lineLength < 0)           throw new IllegalArgumentException();       checkLineSeparator(lineSeparator);       this.lineLength = lineLength;       this.lineSeparator = new byte[lineSeparator.length];       System.arraycopy(lineSeparator, 0, this.lineSeparator, 0,               lineSeparator.length);       this.encoded = new byte[ENCODED_BUFFER_SIZE];   }   @Override   public final void write(final int b) throws IOException {       if (closed)           throw new IOException("Base64OutputStream has been closed");       singleByte[0] = (byte) b;       write0(singleByte, 0, 1);   }   @Override   public final void write(final byte[] buffer) throws IOException {       if (closed)           throw new IOException("Base64OutputStream has been closed");       if (buffer == null)           throw new NullPointerException();       if (buffer.length == 0)           return;       write0(buffer, 0, buffer.length);   }   @Override   public final void write(final byte[] buffer, final int offset,           final int length) throws IOException {       if (closed)           throw new IOException("Base64OutputStream has been closed");       if (buffer == null)           throw new NullPointerException();       if (offset < 0 || length < 0 || offset + length > buffer.length)           throw new IndexOutOfBoundsException();       if (length == 0)           return;       write0(buffer, offset, offset + length);   }   @Override   public void flush() throws IOException {       if (closed)           throw new IOException("Base64OutputStream has been closed");       flush0();   }   @Override   public void close() throws IOException {       if (closed)           return;       closed = true;       close0();   }   private void write0(final byte[] buffer, final int from, final int to)           throws IOException {       for (int i = from; i < to; i++) {           data = (data << 8) | (buffer[i] & 0xff);           if (++modulus == 3) {               modulus = 0;               // write line separator if necessary               if (lineLength > 0 && linePosition >= lineLength) {                   // writeLineSeparator() inlined for performance reasons                   linePosition = 0;                   if (encoded.length - position < lineSeparator.length)                       flush0();                   for (byte ls : lineSeparator)                       encoded[position++] = ls;               }               // encode data into 4 bytes               if (encoded.length - position < 4)                   flush0();               encoded[position++] = BASE64_TABLE[(data >> 18) & MASK_6BITS];               encoded[position++] = BASE64_TABLE[(data >> 12) & MASK_6BITS];               encoded[position++] = BASE64_TABLE[(data >> 6) & MASK_6BITS];               encoded[position++] = BASE64_TABLE[data & MASK_6BITS];               linePosition += 4;           }       }   }   private void flush0() throws IOException {       if (position > 0) {           out.write(encoded, 0, position);           position = 0;       }   }   private void close0() throws IOException {       if (modulus != 0)           writePad();       // write line separator at the end of the encoded data       if (lineLength > 0 && linePosition > 0) {           writeLineSeparator();       }       flush0();   }   private void writePad() throws IOException {       // write line separator if necessary       if (lineLength > 0 && linePosition >= lineLength) {           writeLineSeparator();       }       // encode data into 4 bytes       if (encoded.length - position < 4)           flush0();       if (modulus == 1) {           encoded[position++] = BASE64_TABLE[(data >> 2) & MASK_6BITS];           encoded[position++] = BASE64_TABLE[(data << 4) & MASK_6BITS];           encoded[position++] = BASE64_PAD;           encoded[position++] = BASE64_PAD;       } else {           assert modulus == 2;           encoded[position++] = BASE64_TABLE[(data >> 10) & MASK_6BITS];           encoded[position++] = BASE64_TABLE[(data >> 4) & MASK_6BITS];           encoded[position++] = BASE64_TABLE[(data << 2) & MASK_6BITS];           encoded[position++] = BASE64_PAD;       }       linePosition += 4;   }   private void writeLineSeparator() throws IOException {       linePosition = 0;       if (encoded.length - position < lineSeparator.length)           flush0();       for (byte ls : lineSeparator)           encoded[position++] = ls;   }   private void checkLineSeparator(byte[] lineSeparator) {       if (lineSeparator.length > ENCODED_BUFFER_SIZE)           throw new IllegalArgumentException("line separator length exceeds "                   + ENCODED_BUFFER_SIZE);       for (byte b : lineSeparator) {           if (BASE64_CHARS.contains(b)) {               throw new IllegalArgumentException(                       "line separator must not contain base64 character '"                               + (char) (b & 0xff) + "'");           }       }   } }