summaryrefslogtreecommitdiff
path: root/src/com/android/bitmap/util/InputStreamBuffer.java
blob: c1c15472d630ff9f97d2aa77dff9f170dd74c02e (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
/*
 * Copyright (C) 2013 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.bitmap.util;

import android.util.Log;

import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;

/**
 * Wrapper for {@link InputStream} that allows you to read bytes from it like a byte[]. An
 * internal buffer is kept as small as possible to avoid large unnecessary allocations.
 *
 * <p/>
 * Care must be taken so that the internal buffer is kept small. The best practice is to
 * precalculate the maximum buffer size that you will need. For example,
 * say you have a loop that reads bytes from index <code>0</code> to <code>10</code>,
 * skips to index <code>N</code>, reads from index <code>N</code> to <code>N+10</code>, etc. Then
 * you know that the internal buffer can have a maximum size of <code>10</code>,
 * and you should set the <code>bufferSize</code> parameter to <code>10</code> in the constructor.
 *
 * <p/>
 * Use {@link #advanceTo(int)} to declare that you will not need to access lesser indexes. This
 * helps to keep the internal buffer small. In the above example, after reading bytes from index
 * <code>0</code> to <code>10</code>, you should call <code>advanceTo(N)</code> so that internal
 * buffer becomes filled with bytes from index <code>N</code> to <code>N+10</code>.
 *
 * <p/>
 * If you know that you are reading bytes from a <strong>strictly</strong> increasing or equal
 * index, then you should set the <code>autoAdvance</code> parameter to <code>true</code> in the
 * constructor. For complicated access patterns, or when you prefer to control the internal
 * buffer yourself, set <code>autoAdvance</code> to <code>false</code>. When
 * <code>autoAdvance</code> is enabled, every time an index is beyond the buffer length,
 * the buffer will be shifted forward such that the index requested becomes the first element in
 * the buffer.
 *
 * <p/>
 * All public methods with parameter <code>index</code> are absolute indexed. The index is from
 * the beginning of the wrapped input stream.
 */
public class InputStreamBuffer {

    private static final boolean DEBUG = false;
    private static final int DEBUG_MAX_BUFFER_SIZE = 80;
    private static final String TAG = InputStreamBuffer.class.getSimpleName();

    private InputStream mInputStream;
    private byte[] mBuffer;
    private boolean mAutoAdvance;
    /** Byte count the buffer is offset by. */
    private int mOffset = 0;
    /** Number of bytes filled in the buffer. */
    private int mFilled = 0;

    /**
     * Construct a new wrapper for an InputStream.
     *
     * <p/>
     * If <code>autoAdvance</code> is true, behavior is undefined if you call {@link #get(int)}
     * or {@link #has(int)} with an index N, then some arbitrary time later call {@link #get(int)}
     * or {@link #has(int)} with an index M < N. The wrapper may return the right value,
     * if the buffer happens to still contain index M, but more likely it will throw an
     * {@link IllegalStateException}.
     *
     * <p/>
     * If <code>autoAdvance</code> is false, you must be diligent and call {@link #advanceTo(int)}
     * at the appropriate times to ensure that the internal buffer is not unnecessarily resized
     * and reallocated.
     *
     * @param inputStream The input stream to wrap. The input stream will not be closed by the
     *                    wrapper.
     * @param bufferSize  The initial size for the internal buffer. The buffer size should be
     *                    carefully chosen to avoid resizing and reallocating the internal buffer.
     *                    The internal buffer size used will be the least power of two greater
     *                    than this parameter.
     * @param autoAdvance Determines the behavior when you need to read an index that is beyond
     *                    the internal buffer size. If true, the internal buffer will shift so
     *                    that the requested index becomes the first element. If false,
     *                    the internal buffer size will grow to the smallest power of 2 which is
     *                    greater than the requested index.
     */
    public InputStreamBuffer(final InputStream inputStream, int bufferSize,
            final boolean autoAdvance) {
        mInputStream = inputStream;
        if (bufferSize <= 0) {
            throw new IllegalArgumentException(
                    String.format("Buffer size %d must be positive.", bufferSize));
        }
        bufferSize = leastPowerOf2(bufferSize);
        mBuffer = new byte[bufferSize];
        mAutoAdvance = autoAdvance;
    }

    /**
     * Attempt to get byte at the requested index from the wrapped input stream. If the internal
     * buffer contains the requested index, return immediately. If the index is less than the
     * head of the buffer, or the index is greater or equal to the size of the wrapped input stream,
     * a runtime exception is thrown.
     *
     * <p/>
     * If the index is not in the internal buffer, but it can be requested from the input stream,
     * {@link #fill(int)} will be called first, and the byte at the index returned.
     *
     * <p/>
     * You should always call {@link #has(int)} with the same index, unless you are sure that no
     * exceptions will be thrown as described above.
     *
     * <p/>
     * Consider calling {@link #advanceTo(int)} if you know that you will never request a lesser
     * index in the future.
     * @param index The requested index.
     * @return The byte at that index.
     */
    public byte get(final int index) throws IllegalStateException, IndexOutOfBoundsException {
        Trace.beginSection("get");
        if (has(index)) {
            final int i = index - mOffset;
            Trace.endSection();
            return mBuffer[i];
        } else {
            Trace.endSection();
            throw new IndexOutOfBoundsException(
                    String.format("Index %d beyond length.", index));
        }
    }

    /**
     * Attempt to return whether the requested index is within the size of the wrapped input
     * stream. One side effect is {@link #fill(int)} will be called.
     *
     * <p/>
     * If this method returns true, it is guaranteed that {@link #get(int)} with the same index
     * will not fail. That means that if the requested index is within the size of the wrapped
     * input stream, but the index is less than the head of the internal buffer,
     * a runtime exception is thrown.
     *
     * <p/>
     * See {@link #get(int)} for caveats. A lot of the same warnings about exceptions and
     * <code>advanceTo()</code> apply.
     * @param index The requested index.
     * @return True if requested index is within the size of the wrapped input stream. False if
     * the index is beyond the size.
     */
    public boolean has(final int index) throws IllegalStateException, IndexOutOfBoundsException {
        Trace.beginSection("has");
        if (index < mOffset) {
            Trace.endSection();
            throw new IllegalStateException(
                    String.format("Index %d is before buffer %d", index, mOffset));
        }

        final int i = index - mOffset;

        // Requested index not in internal buffer.
        if (i >= mFilled || i >= mBuffer.length) {
            Trace.endSection();
            return fill(index);
        }

        Trace.endSection();
        return true;
    }

    /**
     * Attempts to advance the head of the buffer to the requested index. If the index is less
     * than the head of the buffer, the internal state will not be changed.
     *
     * <p/>
     * Advancing does not fill the internal buffer. The next {@link #get(int)} or
     * {@link #has(int)} call will fill the buffer.
     */
    public void advanceTo(final int index) throws IllegalStateException, IndexOutOfBoundsException {
        Trace.beginSection("advance to");
        final int i = index - mOffset;
        if (i <= 0) {
            // noop
            Trace.endSection();
            return;
        } else if (i < mFilled) {
            // Shift elements starting at i to position 0.
            shiftToBeginning(i);
            mOffset = index;
            mFilled = mFilled - i;
        } else if (mInputStream != null) {
            // Burn some bytes from the input stream to match the new index.
            int burn = i - mFilled;
            boolean empty = false;
            int fails = 0;
            try {
                while (burn > 0) {
                    final long burned = mInputStream.skip(burn);
                    if (burned <= 0) {
                        fails++;
                    } else {
                        burn -= burned;
                    }

                    if (fails >= 5) {
                        empty = true;
                        break;
                    }
                }
            } catch (IOException ignored) {
                empty = true;
            }

            if (empty) {
                //Mark input stream as consumed.
                mInputStream = null;
            }

            mOffset = index - burn;
            mFilled = 0;
        } else {
            // Advancing beyond the input stream.
            mOffset = index;
            mFilled = 0;
        }

        if (Log.isLoggable(TAG, Log.DEBUG)) {
            Log.d(TAG, String.format("advanceTo %d buffer: %s", i, this));
        }
        Trace.endSection();
    }

    /**
     * Attempt to fill the internal buffer fully. The buffer will be modified such that the
     * requested index will always be in the buffer. If the index is less
     * than the head of the buffer, a runtime exception is thrown.
     *
     * <p/>
     * If the requested index is already in bounds of the buffer, then the buffer will just be
     * filled.
     *
     * <p/>
     * Otherwise, if <code>autoAdvance</code> was set to true in the constructor,
     * {@link #advanceTo(int)} will be called with the requested index,
     * and then the buffer filled. If <code>autoAdvance</code> was set to false,
     * we allocate a single larger buffer of a least multiple-of-two size that can contain the
     * requested index. The elements in the old buffer are copied over to the head of the new
     * buffer. Then the entire buffer is filled.
     * @param index The requested index.
     * @return True if the byte at the requested index has been filled. False if the wrapped
     * input stream ends before we reach the index.
     */
    private boolean fill(final int index) {
        Trace.beginSection("fill");
        if (index < mOffset) {
            Trace.endSection();
            throw new IllegalStateException(
                    String.format("Index %d is before buffer %d", index, mOffset));
        }

        int i = index - mOffset;
        // Can't fill buffer anymore if input stream is consumed.
        if (mInputStream == null) {
            Trace.endSection();
            return false;
        }

        // Increase buffer size if necessary.
        int length = i + 1;
        if (length > mBuffer.length) {
            if (mAutoAdvance) {
                advanceTo(index);
                i = index - mOffset;
            } else {
                length = leastPowerOf2(length);
                Log.w(TAG, String.format(
                        "Increasing buffer length from %d to %d. Bad buffer size chosen, "
                                + "or advanceTo() not called.",
                        mBuffer.length, length));
                mBuffer = Arrays.copyOf(mBuffer, length);
            }
        }

        // Read from input stream to fill buffer.
        int read = -1;
        try {
            read = mInputStream.read(mBuffer, mFilled, mBuffer.length - mFilled);
        } catch (IOException ignored) {
        }

        if (read != -1) {
            mFilled = mFilled + read;
        } else {
            // Mark input stream as consumed.
            mInputStream = null;
        }

        if (Log.isLoggable(TAG, Log.DEBUG)) {
            Log.d(TAG, String.format("fill %d      buffer: %s", i, this));
        }

        Trace.endSection();
        return i < mFilled;
    }

    /**
     * Modify the internal buffer so that all the bytes are shifted towards the head by
     * <code>i</code>. In other words, the byte at index <code>i</code> will now be at index
     * <code>0</code>. Bytes from a lesser index are tossed.
     * @param i How much to shift left.
     */
    private void shiftToBeginning(final int i) {
        if (i >= mBuffer.length) {
            throw new IndexOutOfBoundsException(
                    String.format("Index %d out of bounds. Length %d", i, mBuffer.length));
        }
        for (int j = 0; j + i < mFilled; j++) {
            mBuffer[j] = mBuffer[j + i];
        }
    }

    @Override
    public String toString() {
        if (DEBUG) {
            return toDebugString();
        }
        return String.format("+%d+%d [%d]", mOffset, mBuffer.length, mFilled);
    }

    public String toDebugString() {
        Trace.beginSection("to debug string");
        final StringBuilder sb = new StringBuilder();
        sb.append("+").append(mOffset);
        sb.append("+").append(mBuffer.length);
        sb.append(" [");
        for (int i = 0; i < mBuffer.length && i < DEBUG_MAX_BUFFER_SIZE; i++) {
            if (i > 0) {
                sb.append(",");
            }
            if (i < mFilled) {
                sb.append(String.format("%02X", mBuffer[i]));
            } else {
                sb.append("__");
            }
        }
        if (mInputStream != null) {
            sb.append("...");
        }
        sb.append("]");

        Trace.endSection();
        return sb.toString();
    }

    /**
     * Calculate the least power of two greater than or equal to the input.
     */
    private static int leastPowerOf2(int n) {
        n--;
        n |= n >> 1;
        n |= n >> 2;
        n |= n >> 4;
        n |= n >> 8;
        n |= n >> 16;
        n++;
        return n;
    }
}