    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 Context context = new Context();
    043
    044    protected BaseNCodecInputStream(final InputStream input, final BaseNCodec baseNCodec, final boolean doEncode) {
    045        super(input);
    046        this.doEncode = doEncode;
    047        this.baseNCodec = baseNCodec;
    048    }
    049
    050    /**
    051     * {@inheritDoc}
    052     *
    053     * @return {@code 0} if the {@link InputStream} has reached {@code EOF},
    054     * {@code 1} otherwise
    055     * @since 1.7
    056     */
    057    @Override
    058    public int available() throws IOException {
    059        // Note: the logic is similar to the InflaterInputStream:
    060        //       as long as we have not reached EOF, indicate that there is more
    061        //       data available. As we do not know for sure how much data is left,
    062        //       just return 1 as a safe guess.
    063
    064        return context.eof ? 0 : 1;
    065    }
    066
    067    /**
    068     * Returns true if decoding behavior is strict. Decoding will raise an
    069     * {@link IllegalArgumentException} if trailing bits are not part of a valid encoding.
    070     *
    071     * <p>The default is false for lenient encoding. Decoding will compose trailing bits
    072     * into 8-bit bytes and discard the remainder.
    073     *
    074     * @return true if using strict decoding
    075     * @since 1.15
    076     */
    077    public boolean isStrictDecoding() {
    078        return baseNCodec.isStrictDecoding();
    079    }
    080
    081    /**
    082     * Marks the current position in this input stream.
    083     * <p>The {@link #mark} method of {@link BaseNCodecInputStream} does nothing.</p>
    084     *
    085     * @param readLimit the maximum limit of bytes that can be read before the mark position becomes invalid.
    086     * @see #markSupported()
    087     * @since 1.7
    088     */
    089    @Override
    090    public synchronized void mark(final int readLimit) {
    091        // noop
    092    }
    093
    094    /**
    095     * {@inheritDoc}
    096     *
    097     * @return Always returns {@code false}
    098     */
    099    @Override
    100    public boolean markSupported() {
    101        return false; // not an easy job to support marks
    102    }
    103
    104    /**
    105     * Reads one {@code byte} from this input stream.
    106     *
    107     * @return the byte as an integer in the range 0 to 255. Returns -1 if EOF has been reached.
    108     * @throws IOException
    109     *             if an I/O error occurs.
    110     */
    111    @Override
    112    public int read() throws IOException {
    113        int r = read(singleByte, 0, 1);
    114        while (r == 0) {
    115            r = read(singleByte, 0, 1);
    116        }
    117        if (r > 0) {
    118            final byte b = singleByte[0];
    119            return b < 0 ? 256 + b : b;
    120        }
    121        return EOF;
    122    }
    123
    124    /**
    125     * Attempts to read {@code len} bytes into the specified {@code b} array starting at {@code offset}
    126     * from this InputStream.
    127     *
    128     * @param array
    129     *            destination byte array
    130     * @param offset
    131     *            where to start writing the bytes
    132     * @param len
    133     *            maximum number of bytes to read
    134     *
    135     * @return number of bytes read
    136     * @throws IOException
    137     *             if an I/O error occurs.
    138     * @throws NullPointerException
    139     *             if the byte array parameter is null
    140     * @throws IndexOutOfBoundsException
    141     *             if offset, len or buffer size are invalid
    142     */
    143    @Override
    144    public int read(final byte array[], final int offset, final int len) throws IOException {
    145        Objects.requireNonNull(array, "array");
    146        if (offset < 0 || len < 0) {
    147            throw new IndexOutOfBoundsException();
    148        } else if (offset > array.length || offset + len > array.length) {
    149            throw new IndexOutOfBoundsException();
    150        } else if (len == 0) {
    151            return 0;
    152        } else {
    153            int readLen = 0;
    154            /*
    155             Rationale for while-loop on (readLen == 0):
    156             -----
    157             Base32.readResults() usually returns > 0 or EOF (-1).  In the
    158             rare case where it returns 0, we just keep trying.
    159
    160             This is essentially an undocumented contract for InputStream
    161             implementors that want their code to work properly with
    162             java.io.InputStreamReader, since the latter hates it when
    163             InputStream.read(byte[]) returns a zero.  Unfortunately our
    164             readResults() call must return 0 if a large amount of the data
    165             being decoded was non-base32, so this while-loop enables proper
    166             interop with InputStreamReader for that scenario.
    167             -----
    168             This is a fix for CODEC-101
    169            */
    170            while (readLen == 0) {
    171                if (!baseNCodec.hasData(context)) {
    172                    final byte[] buf = new byte[doEncode ? 4096 : 8192];
    173                    final int c = in.read(buf);
    174                    if (doEncode) {
    175                        baseNCodec.encode(buf, 0, c, context);
    176                    } else {
    177                        baseNCodec.decode(buf, 0, c, context);
    178                    }
    179                }
    180                readLen = baseNCodec.readResults(array, offset, len, context);
    181            }
    182            return readLen;
    183        }
    184    }
    185
    186    /**
    187     * Repositions this stream to the position at the time the mark method was last called on this input stream.
    188     * <p>
    189     * The {@link #reset} method of {@link BaseNCodecInputStream} does nothing except throw an {@link IOException}.
    190     *
    191     * @throws IOException if this method is invoked
    192     * @since 1.7
    193     */
    194    @Override
    195    public synchronized void reset() throws IOException {
    196        throw new IOException("mark/reset not supported");
    197    }
    198
    199    /**
    200     * {@inheritDoc}
    201     *
    202     * @throws IllegalArgumentException if the provided skip length is negative
    203     * @since 1.7
    204     */
    205    @Override
    206    public long skip(final long n) throws IOException {
    207        if (n < 0) {
    208            throw new IllegalArgumentException("Negative skip length: " + n);
    209        }
    210
    211        // skip in chunks of 512 bytes
    212        final byte[] b = new byte[512];
    213        long todo = n;
    214
    215        while (todo > 0) {
    216            int len = (int) Math.min(b.length, todo);
    217            len = this.read(b, 0, len);
    218            if (len == EOF) {
    219                break;
    220            }
    221            todo -= len;
    222        }
    223
    224        return n - todo;
    225    }
    226}