View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  
18  package org.apache.commons.codec.binary;
19  
20  import static org.apache.commons.codec.binary.BaseNCodec.EOF;
21  
22  import java.io.FilterInputStream;
23  import java.io.IOException;
24  import java.io.InputStream;
25  import java.util.Objects;
26  
27  import org.apache.commons.codec.binary.BaseNCodec.Context;
28  
29  /**
30   * Abstract superclass for Base-N input streams.
31   *
32   * @since 1.5
33   */
34  public class BaseNCodecInputStream extends FilterInputStream {
35  
36      private final BaseNCodec baseNCodec;
37  
38      private final boolean doEncode;
39  
40      private final byte[] singleByte = new byte[1];
41  
42      private final byte[] buf;
43  
44      private final Context context = new Context();
45  
46      /**
47       * Constructs a new instance.
48       *
49       * @param inputStream the input stream
50       * @param baseNCodec the codec
51       * @param doEncode set to true to perform encoding, else decoding
52       */
53      protected BaseNCodecInputStream(final InputStream inputStream, final BaseNCodec baseNCodec, final boolean doEncode) {
54          super(inputStream);
55          this.doEncode = doEncode;
56          this.baseNCodec = baseNCodec;
57          this.buf = new byte[doEncode ? 4096 : 8192];
58      }
59  
60      /**
61       * {@inheritDoc}
62       *
63       * @return {@code 0} if the {@link InputStream} has reached {@code EOF},
64       * {@code 1} otherwise
65       * @since 1.7
66       */
67      @Override
68      public int available() throws IOException {
69          // Note: the logic is similar to the InflaterInputStream:
70          //       as long as we have not reached EOF, indicate that there is more
71          //       data available. As we do not know for sure how much data is left,
72          //       just return 1 as a safe guess.
73          return context.eof ? 0 : 1;
74      }
75  
76      /**
77       * Returns true if decoding behavior is strict. Decoding will raise an
78       * {@link IllegalArgumentException} if trailing bits are not part of a valid encoding.
79       *
80       * <p>
81       * The default is false for lenient encoding. Decoding will compose trailing bits
82       * into 8-bit bytes and discard the remainder.
83       * </p>
84       *
85       * @return true if using strict decoding
86       * @since 1.15
87       */
88      public boolean isStrictDecoding() {
89          return baseNCodec.isStrictDecoding();
90      }
91  
92      /**
93       * Marks the current position in this input stream.
94       * <p>
95       * The {@link #mark} method of {@link BaseNCodecInputStream} does nothing.
96       * </p>
97       *
98       * @param readLimit the maximum limit of bytes that can be read before the mark position becomes invalid.
99       * @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 }