    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     * Create an 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
    074        return context.eof ? 0 : 1;
    075    }
    076
    077    /**
    078     * Returns true if decoding behavior is strict. Decoding will raise an
    079     * {@link IllegalArgumentException} if trailing bits are not part of a valid encoding.
    080     *
    081     * <p>
    082     * The default is false for lenient encoding. Decoding will compose trailing bits
    083     * into 8-bit bytes and discard the remainder.
    084     * </p>
    085     *
    086     * @return true if using strict decoding
    087     * @since 1.15
    088     */
    089    public boolean isStrictDecoding() {
    090        return baseNCodec.isStrictDecoding();
    091    }
    092
    093    /**
    094     * Marks the current position in this input stream.
    095     * <p>
    096     * The {@link #mark} method of {@link BaseNCodecInputStream} does nothing.
    097     * </p>
    098     *
    099     * @param readLimit the maximum limit of bytes that can be read before the mark position becomes invalid.
    100     * @see #markSupported()
    101     * @since 1.7
    102     */
    103    @Override
    104    public synchronized void mark(final int readLimit) {
    105        // noop
    106    }
    107
    108    /**
    109     * {@inheritDoc}
    110     *
    111     * @return Always returns {@code false}
    112     */
    113    @Override
    114    public boolean markSupported() {
    115        return false; // not an easy job to support marks
    116    }
    117
    118    /**
    119     * Reads one {@code byte} from this input stream.
    120     *
    121     * @return the byte as an integer in the range 0 to 255. Returns -1 if EOF has been reached.
    122     * @throws IOException
    123     *             if an I/O error occurs.
    124     */
    125    @Override
    126    public int read() throws IOException {
    127        int r = read(singleByte, 0, 1);
    128        while (r == 0) {
    129            r = read(singleByte, 0, 1);
    130        }
    131        if (r > 0) {
    132            final byte b = singleByte[0];
    133            return b < 0 ? 256 + b : b;
    134        }
    135        return EOF;
    136    }
    137
    138    /**
    139     * Attempts to read {@code len} bytes into the specified {@code b} array starting at {@code offset}
    140     * from this InputStream.
    141     *
    142     * @param array
    143     *            destination byte array
    144     * @param offset
    145     *            where to start writing the bytes
    146     * @param len
    147     *            maximum number of bytes to read
    148     *
    149     * @return number of bytes read
    150     * @throws IOException
    151     *             if an I/O error occurs.
    152     * @throws NullPointerException
    153     *             if the byte array parameter is null
    154     * @throws IndexOutOfBoundsException
    155     *             if offset, len or buffer size are invalid
    156     */
    157    @Override
    158    public int read(final byte[] array, final int offset, final int len) throws IOException {
    159        Objects.requireNonNull(array, "array");
    160        if (offset < 0 || len < 0) {
    161            throw new IndexOutOfBoundsException();
    162        }
    163        if (offset > array.length || offset + len > array.length) {
    164            throw new IndexOutOfBoundsException();
    165        }
    166        if (len == 0) {
    167            return 0;
    168        }
    169        int readLen = 0;
    170        /*
    171         Rationale for while-loop on (readLen == 0):
    172         -----
    173         Base32.readResults() usually returns > 0 or EOF (-1).  In the
    174         rare case where it returns 0, we just keep trying.
    175
    176         This is essentially an undocumented contract for InputStream
    177         implementors that want their code to work properly with
    178         java.io.InputStreamReader, since the latter hates it when
    179         InputStream.read(byte[]) returns a zero.  Unfortunately our
    180         readResults() call must return 0 if a large amount of the data
    181         being decoded was non-base32, so this while-loop enables proper
    182         interop with InputStreamReader for that scenario.
    183         -----
    184         This is a fix for CODEC-101
    185        */
    186        // Attempt to read the request length
    187        while (readLen < len) {
    188            if (!baseNCodec.hasData(context)) {
    189                // Obtain more data.
    190                // buf is reused across calls to read to avoid repeated allocations
    191                final int c = in.read(buf);
    192                if (doEncode) {
    193                    baseNCodec.encode(buf, 0, c, context);
    194                } else {
    195                    baseNCodec.decode(buf, 0, c, context);
    196                }
    197            }
    198            final int read = baseNCodec.readResults(array, offset + readLen, len - readLen, context);
    199            if (read < 0) {
    200                // Return the amount read or EOF
    201                return readLen != 0 ? readLen : -1;
    202            }
    203            readLen += read;
    204        }
    205        return readLen;
    206    }
    207
    208    /**
    209     * Repositions this stream to the position at the time the mark method was last called on this input stream.
    210     * <p>
    211     * The {@link #reset} method of {@link BaseNCodecInputStream} does nothing except throw an {@link IOException}.
    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
    233        // skip in chunks of 512 bytes
    234        final byte[] b = new byte[512];
    235        long todo = n;
    236
    237        while (todo > 0) {
    238            int len = (int) Math.min(b.length, todo);
    239            len = this.read(b, 0, len);
    240            if (len == EOF) {
    241                break;
    242            }
    243            todo -= len;
    244        }
    245
    246        return n - todo;
    247    }
    248}