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