001/****************************************************************
002 * Licensed to the Apache Software Foundation (ASF) under one   *
003 * or more contributor license agreements.  See the NOTICE file *
004 * distributed with this work for additional information        *
005 * regarding copyright ownership.  The ASF licenses this file   *
006 * to you under the Apache License, Version 2.0 (the            *
007 * "License"); you may not use this file except in compliance   *
008 * with the License.  You may obtain a copy of the License at   *
009 *                                                              *
010 *   http://www.apache.org/licenses/LICENSE-2.0                 *
011 *                                                              *
012 * Unless required by applicable law or agreed to in writing,   *
013 * software distributed under the License is distributed on an  *
014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
015 * KIND, either express or implied.  See the License for the    *
016 * specific language governing permissions and limitations      *
017 * under the License.                                           *
018 ****************************************************************/
019
020package org.apache.james.mime4j.codec;
021
022import java.io.IOException;
023import java.io.InputStream;
024
025import org.apache.james.mime4j.util.ByteArrayBuffer;
026
027/**
028 * Performs Base-64 decoding on an underlying stream.
029 */
030public class Base64InputStream extends InputStream {
031    private static final int ENCODED_BUFFER_SIZE = 1536;
032
033    private static final int[] BASE64_DECODE = new int[256];
034
035    static {
036        for (int i = 0; i < 256; i++)
037            BASE64_DECODE[i] = -1;
038        for (int i = 0; i < Base64OutputStream.BASE64_TABLE.length; i++)
039            BASE64_DECODE[Base64OutputStream.BASE64_TABLE[i] & 0xff] = i;
040    }
041
042    private static final byte BASE64_PAD = '=';
043
044    private static final int EOF = -1;
045
046    private final byte[] singleByte = new byte[1];
047
048    private final InputStream in;
049    private final byte[] encoded;
050    private final ByteArrayBuffer decodedBuf;
051
052    private int position = 0; // current index into encoded buffer
053    private int size = 0; // current size of encoded buffer
054
055    private boolean closed = false;
056    private boolean eof; // end of file or pad character reached
057
058    private final DecodeMonitor monitor;
059
060    public Base64InputStream(InputStream in, DecodeMonitor monitor) {
061        this(ENCODED_BUFFER_SIZE, in, monitor);
062    }
063
064    protected Base64InputStream(int bufsize, InputStream in, DecodeMonitor monitor) {
065        if (in == null)
066            throw new IllegalArgumentException();
067        this.encoded = new byte[bufsize];
068        this.decodedBuf = new ByteArrayBuffer(512);
069        this.in = in;
070        this.monitor = monitor;
071    }
072
073    public Base64InputStream(InputStream in) {
074        this(in, false);
075    }
076
077    public Base64InputStream(InputStream in, boolean strict) {
078        this(ENCODED_BUFFER_SIZE, in, strict ? DecodeMonitor.STRICT : DecodeMonitor.SILENT);
079    }
080
081    @Override
082    public int read() throws IOException {
083        if (closed)
084            throw new IOException("Stream has been closed");
085
086        while (true) {
087            int bytes = read0(singleByte, 0, 1);
088            if (bytes == EOF)
089                return EOF;
090
091            if (bytes == 1)
092                return singleByte[0] & 0xff;
093        }
094    }
095
096    @Override
097    public int read(byte[] buffer) throws IOException {
098        if (closed)
099            throw new IOException("Stream has been closed");
100
101        if (buffer == null)
102            throw new NullPointerException();
103
104        if (buffer.length == 0)
105            return 0;
106
107        return read0(buffer, 0, buffer.length);
108    }
109
110    @Override
111    public int read(byte[] buffer, int offset, int length) throws IOException {
112        if (closed)
113            throw new IOException("Stream has been closed");
114
115        if (buffer == null)
116            throw new NullPointerException();
117
118        if (offset < 0 || length < 0 || offset + length > buffer.length)
119            throw new IndexOutOfBoundsException();
120
121        if (length == 0)
122            return 0;
123
124        return read0(buffer, offset, length);
125    }
126
127    @Override
128    public void close() throws IOException {
129        if (closed)
130            return;
131
132        closed = true;
133    }
134
135    private int read0(final byte[] buffer, final int off, final int len) throws IOException {
136        int from = off;
137        int to = off + len;
138        int index = off;
139
140        // check if a previous invocation left decoded content
141        if (decodedBuf.length() > 0) {
142            int chunk = Math.min(decodedBuf.length(), len);
143            System.arraycopy(decodedBuf.buffer(), 0, buffer, index, chunk);
144            decodedBuf.remove(0, chunk);
145            index += chunk;
146        }
147
148        // eof or pad reached?
149
150        if (eof)
151            return index == from ? EOF : index - from;
152
153        // decode into given buffer
154
155        int data = 0; // holds decoded data; up to four sextets
156        int sextets = 0; // number of sextets
157
158        while (index < to) {
159            // make sure buffer not empty
160
161            while (position == size) {
162                int n = in.read(encoded, 0, encoded.length);
163                if (n == EOF) {
164                    eof = true;
165
166                    if (sextets != 0) {
167                        // error in encoded data
168                        handleUnexpectedEof(sextets);
169                    }
170
171                    return index == from ? EOF : index - from;
172                } else if (n > 0) {
173                    position = 0;
174                    size = n;
175                } else {
176                    assert n == 0;
177                }
178            }
179
180            // decode buffer
181
182            while (position < size && index < to) {
183                int value = encoded[position++] & 0xff;
184
185                if (value == BASE64_PAD) {
186                    index = decodePad(data, sextets, buffer, index, to);
187                    return index - from;
188                }
189
190                int decoded = BASE64_DECODE[value];
191                if (decoded < 0) { // -1: not a base64 char
192                    if (value != 0x0D && value != 0x0A && value != 0x20) {
193                        if (monitor.warn("Unexpected base64 byte: "+(byte) value, "ignoring."))
194                            throw new IOException("Unexpected base64 byte");
195                    }
196                    continue;
197                }
198
199                data = (data << 6) | decoded;
200                sextets++;
201
202                if (sextets == 4) {
203                    sextets = 0;
204
205                    byte b1 = (byte) (data >>> 16);
206                    byte b2 = (byte) (data >>> 8);
207                    byte b3 = (byte) data;
208
209                    if (index < to - 2) {
210                        buffer[index++] = b1;
211                        buffer[index++] = b2;
212                        buffer[index++] = b3;
213                    } else {
214                        if (index < to - 1) {
215                            buffer[index++] = b1;
216                            buffer[index++] = b2;
217                            decodedBuf.append(b3);
218                        } else if (index < to) {
219                            buffer[index++] = b1;
220                            decodedBuf.append(b2);
221                            decodedBuf.append(b3);
222                        } else {
223                            decodedBuf.append(b1);
224                            decodedBuf.append(b2);
225                            decodedBuf.append(b3);
226                        }
227
228                        assert index == to;
229                        return to - from;
230                    }
231                }
232            }
233        }
234
235        assert sextets == 0;
236        assert index == to;
237        return to - from;
238    }
239
240    private int decodePad(int data, int sextets, final byte[] buffer,
241            int index, final int end) throws IOException {
242        eof = true;
243
244        if (sextets == 2) {
245            // one byte encoded as "XY=="
246
247            byte b = (byte) (data >>> 4);
248            if (index < end) {
249                buffer[index++] = b;
250            } else {
251                decodedBuf.append(b);
252            }
253        } else if (sextets == 3) {
254            // two bytes encoded as "XYZ="
255
256            byte b1 = (byte) (data >>> 10);
257            byte b2 = (byte) ((data >>> 2) & 0xFF);
258
259            if (index < end - 1) {
260                buffer[index++] = b1;
261                buffer[index++] = b2;
262            } else if (index < end) {
263                buffer[index++] = b1;
264                decodedBuf.append(b2);
265            } else {
266                decodedBuf.append(b1);
267                decodedBuf.append(b2);
268            }
269        } else {
270            // error in encoded data
271            handleUnexpecedPad(sextets);
272        }
273
274        return index;
275    }
276
277    private void handleUnexpectedEof(int sextets) throws IOException {
278        if (monitor.warn("Unexpected end of BASE64 stream", "dropping " + sextets + " sextet(s)"))
279            throw new IOException("Unexpected end of BASE64 stream");
280    }
281
282    private void handleUnexpecedPad(int sextets) throws IOException {
283        if (monitor.warn("Unexpected padding character", "dropping " + sextets + " sextet(s)"))
284            throw new IOException("Unexpected padding character");
285    }
286}