001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017
018package org.apache.commons.codec.binary;
019
020import static org.apache.commons.codec.binary.BaseNCodec.EOF;
021
022import java.io.FilterInputStream;
023import java.io.IOException;
024import java.io.InputStream;
025import java.util.Objects;
026
027import org.apache.commons.codec.binary.BaseNCodec.Context;
028
029/**
030 * Abstract superclass for Base-N input streams.
031 *
032 * @since 1.5
033 */
034public class BaseNCodecInputStream extends FilterInputStream {
035
036    private final BaseNCodec baseNCodec;
037
038    private final boolean doEncode;
039
040    private final byte[] singleByte = new byte[1];
041
042    private final byte[] buf;
043
044    private final Context context = new Context();
045
046    /**
047     * Constructs a new instance.
048     *
049     * @param inputStream the input stream
050     * @param baseNCodec the codec
051     * @param doEncode set to true to perform encoding, else decoding
052     */
053    protected BaseNCodecInputStream(final InputStream inputStream, final BaseNCodec baseNCodec, final boolean doEncode) {
054        super(inputStream);
055        this.doEncode = doEncode;
056        this.baseNCodec = baseNCodec;
057        this.buf = new byte[doEncode ? 4096 : 8192];
058    }
059
060    /**
061     * {@inheritDoc}
062     *
063     * @return {@code 0} if the {@link InputStream} has reached {@code EOF},
064     * {@code 1} otherwise
065     * @since 1.7
066     */
067    @Override
068    public int available() throws IOException {
069        // Note: the logic is similar to the InflaterInputStream:
070        //       as long as we have not reached EOF, indicate that there is more
071        //       data available. As we do not know for sure how much data is left,
072        //       just return 1 as a safe guess.
073        return context.eof ? 0 : 1;
074    }
075
076    /**
077     * Returns true if decoding behavior is strict. Decoding will raise an
078     * {@link IllegalArgumentException} if trailing bits are not part of a valid encoding.
079     *
080     * <p>
081     * The default is false for lenient encoding. Decoding will compose trailing bits
082     * into 8-bit bytes and discard the remainder.
083     * </p>
084     *
085     * @return true if using strict decoding
086     * @since 1.15
087     */
088    public boolean isStrictDecoding() {
089        return baseNCodec.isStrictDecoding();
090    }
091
092    /**
093     * Marks the current position in this input stream.
094     * <p>
095     * The {@link #mark} method of {@link BaseNCodecInputStream} does nothing.
096     * </p>
097     *
098     * @param readLimit the maximum limit of bytes that can be read before the mark position becomes invalid.
099     * @see #markSupported()
100     * @since 1.7
101     */
102    @Override
103    public synchronized void mark(final int readLimit) {
104        // noop
105    }
106
107    /**
108     * {@inheritDoc}
109     *
110     * @return Always returns {@code false}
111     */
112    @Override
113    public boolean markSupported() {
114        return false; // not an easy job to support marks
115    }
116
117    /**
118     * Reads one {@code byte} from this input stream.
119     *
120     * @return the byte as an integer in the range 0 to 255. Returns -1 if EOF has been reached.
121     * @throws IOException
122     *             if an I/O error occurs.
123     */
124    @Override
125    public int read() throws IOException {
126        int r = read(singleByte, 0, 1);
127        while (r == 0) {
128            r = read(singleByte, 0, 1);
129        }
130        if (r > 0) {
131            final byte b = singleByte[0];
132            return b < 0 ? 256 + b : b;
133        }
134        return EOF;
135    }
136
137    /**
138     * Attempts to read {@code len} bytes into the specified {@code b} array starting at {@code offset}
139     * from this InputStream.
140     *
141     * @param array
142     *            destination byte array
143     * @param offset
144     *            where to start writing the bytes
145     * @param len
146     *            maximum number of bytes to read
147     *
148     * @return number of bytes read
149     * @throws IOException
150     *             if an I/O error occurs.
151     * @throws NullPointerException
152     *             if the byte array parameter is null
153     * @throws IndexOutOfBoundsException
154     *             if offset, len or buffer size are invalid
155     */
156    @Override
157    public int read(final byte[] array, final int offset, final int len) throws IOException {
158        Objects.requireNonNull(array, "array");
159        if (offset < 0 || len < 0) {
160            throw new IndexOutOfBoundsException();
161        }
162        if (offset > array.length || offset + len > array.length) {
163            throw new IndexOutOfBoundsException();
164        }
165        if (len == 0) {
166            return 0;
167        }
168        int readLen = 0;
169        /*
170         Rationale for while-loop on (readLen == 0):
171         -----
172         Base32.readResults() usually returns > 0 or EOF (-1).  In the
173         rare case where it returns 0, we just keep trying.
174
175         This is essentially an undocumented contract for InputStream
176         implementors that want their code to work properly with
177         java.io.InputStreamReader, since the latter hates it when
178         InputStream.read(byte[]) returns a zero.  Unfortunately our
179         readResults() call must return 0 if a large amount of the data
180         being decoded was non-base32, so this while-loop enables proper
181         interop with InputStreamReader for that scenario.
182         -----
183         This is a fix for CODEC-101
184        */
185        // Attempt to read the request length
186        while (readLen < len) {
187            if (!baseNCodec.hasData(context)) {
188                // Obtain more data.
189                // buf is reused across calls to read to avoid repeated allocations
190                final int c = in.read(buf);
191                if (doEncode) {
192                    baseNCodec.encode(buf, 0, c, context);
193                } else {
194                    baseNCodec.decode(buf, 0, c, context);
195                }
196            }
197            final int read = baseNCodec.readResults(array, offset + readLen, len - readLen, context);
198            if (read < 0) {
199                // Return the amount read or EOF
200                return readLen != 0 ? readLen : -1;
201            }
202            readLen += read;
203        }
204        return readLen;
205    }
206
207    /**
208     * Repositions this stream to the position at the time the mark method was last called on this input stream.
209     * <p>
210     * The {@link #reset} method of {@link BaseNCodecInputStream} does nothing except throw an {@link IOException}.
211     * </p>
212     *
213     * @throws IOException if this method is invoked
214     * @since 1.7
215     */
216    @Override
217    public synchronized void reset() throws IOException {
218        throw new IOException("mark/reset not supported");
219    }
220
221    /**
222     * {@inheritDoc}
223     *
224     * @throws IllegalArgumentException if the provided skip length is negative
225     * @since 1.7
226     */
227    @Override
228    public long skip(final long n) throws IOException {
229        if (n < 0) {
230            throw new IllegalArgumentException("Negative skip length: " + n);
231        }
232        // skip in chunks of 512 bytes
233        final byte[] b = new byte[512];
234        long todo = n;
235        while (todo > 0) {
236            int len = (int) Math.min(b.length, todo);
237            len = this.read(b, 0, len);
238            if (len == EOF) {
239                break;
240            }
241            todo -= len;
242        }
243        return n - todo;
244    }
245}