Skip to content

Commit 0956493

Browse files
committed
[CODEC-265] BaseNCodec to expand buffer using overflow conscious code.
Add a test to encode a 1 Gigabyte byte[]. Added overflow conscious code for buffer expansion. This is based on java.util.ArrayList but modified to use a local copy of JDK 1.8 Integer.compareUnsigned(int, int). Unlike the java.util.ArrayList if the minCapcity exceeds the head room for the maximum array size, the capacity returned is the required minCapcity and not Integer.MAX_VALUE.
1 parent 9bd2cda commit 0956493

3 files changed

Lines changed: 290 additions & 13 deletions

File tree

src/main/java/org/apache/commons/codec/binary/BaseNCodec.java

Lines changed: 82 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,18 @@ public String toString() {
144144
*/
145145
private static final int DEFAULT_BUFFER_SIZE = 8192;
146146

147+
/**
148+
* The maximum size buffer to allocate.
149+
*
150+
* <p>This is set to the same size used in the JDK {@code java.util.ArrayList}:</p>
151+
* <blockquote>
152+
* Some VMs reserve some header words in an array.
153+
* Attempts to allocate larger arrays may result in
154+
* OutOfMemoryError: Requested array size exceeds VM limit.
155+
* </blockquote>
156+
*/
157+
private static final int MAX_BUFFER_SIZE = Integer.MAX_VALUE - 8;
158+
147159
/** Mask used to extract 8 bits, used in decoding bytes */
148160
protected static final int MASK_8BITS = 0xff;
149161

@@ -243,18 +255,69 @@ protected int getDefaultBufferSize() {
243255
/**
244256
* Increases our buffer by the {@link #DEFAULT_BUFFER_RESIZE_FACTOR}.
245257
* @param context the context to be used
258+
* @param minCapacity the minimum required capacity
259+
* @return the resized byte[] buffer
260+
* @throws OutOfMemoryError if the {@code minCapacity} is negative
246261
*/
247-
private byte[] resizeBuffer(final Context context) {
248-
if (context.buffer == null) {
249-
context.buffer = new byte[getDefaultBufferSize()];
250-
context.pos = 0;
251-
context.readPos = 0;
252-
} else {
253-
final byte[] b = new byte[context.buffer.length * DEFAULT_BUFFER_RESIZE_FACTOR];
254-
System.arraycopy(context.buffer, 0, b, 0, context.buffer.length);
255-
context.buffer = b;
262+
private static byte[] resizeBuffer(final Context context, final int minCapacity) {
263+
// Overflow-conscious code treats the min and new capacity as unsigned.
264+
final int oldCapacity = context.buffer.length;
265+
int newCapacity = oldCapacity * DEFAULT_BUFFER_RESIZE_FACTOR;
266+
if (compareUnsigned(newCapacity, minCapacity) < 0) {
267+
newCapacity = minCapacity;
256268
}
257-
return context.buffer;
269+
if (compareUnsigned(newCapacity, MAX_BUFFER_SIZE) > 0) {
270+
newCapacity = createPositiveCapacity(minCapacity);
271+
}
272+
273+
final byte[] b = new byte[newCapacity];
274+
System.arraycopy(context.buffer, 0, b, 0, context.buffer.length);
275+
context.buffer = b;
276+
return b;
277+
}
278+
279+
/**
280+
* Compares two {@code int} values numerically treating the values
281+
* as unsigned. Taken from JDK 1.8.
282+
*
283+
* <p>TODO: Replace with JDK 1.8 Integer::compareUnsigned(int, int).</p>
284+
*
285+
* @param x the first {@code int} to compare
286+
* @param y the second {@code int} to compare
287+
* @return the value {@code 0} if {@code x == y}; a value less
288+
* than {@code 0} if {@code x < y} as unsigned values; and
289+
* a value greater than {@code 0} if {@code x > y} as
290+
* unsigned values
291+
*/
292+
private static int compareUnsigned(int x, int y) {
293+
return Integer.compare(x + Integer.MIN_VALUE, y + Integer.MIN_VALUE);
294+
}
295+
296+
/**
297+
* Create a positive capacity at least as large the minimum required capacity.
298+
* If the minimum capacity is negative then this throws an OutOfMemoryError as no array
299+
* can be allocated.
300+
*
301+
* @param minCapacity the minimum capacity
302+
* @return the capacity
303+
* @throws OutOfMemoryError if the {@code minCapacity} is negative
304+
*/
305+
private static int createPositiveCapacity(int minCapacity) {
306+
if (minCapacity < 0) {
307+
// overflow
308+
throw new OutOfMemoryError("Unable to allocate array size: " + (minCapacity & 0xffffffffL));
309+
}
310+
// This is called when we require buffer expansion to a very big array.
311+
// Use the conservative maximum buffer size if possible, otherwise the biggest required.
312+
//
313+
// Note: In this situation JDK 1.8 java.util.ArrayList returns Integer.MAX_VALUE.
314+
// This excludes some VMs that can exceed MAX_BUFFER_SIZE but not allocate a full
315+
// Integer.MAX_VALUE length array.
316+
// The result is that we may have to allocate an array of this size more than once if
317+
// the capacity must be expanded again.
318+
return (minCapacity > MAX_BUFFER_SIZE) ?
319+
minCapacity :
320+
MAX_BUFFER_SIZE;
258321
}
259322

260323
/**
@@ -265,8 +328,15 @@ private byte[] resizeBuffer(final Context context) {
265328
* @return the buffer
266329
*/
267330
protected byte[] ensureBufferSize(final int size, final Context context){
268-
if ((context.buffer == null) || (context.buffer.length < context.pos + size)){
269-
return resizeBuffer(context);
331+
if (context.buffer == null) {
332+
context.buffer = new byte[getDefaultBufferSize()];
333+
context.pos = 0;
334+
context.readPos = 0;
335+
336+
// Overflow-conscious:
337+
// x + y > z == x + y - z > 0
338+
} else if (context.pos + size - context.buffer.length > 0) {
339+
return resizeBuffer(context, context.pos + size);
270340
}
271341
return context.buffer;
272342
}

src/test/java/org/apache/commons/codec/binary/Base64Test.java

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
import org.apache.commons.codec.DecoderException;
3333
import org.apache.commons.codec.EncoderException;
3434
import org.apache.commons.lang3.ArrayUtils;
35-
import org.junit.Ignore;
35+
import org.junit.Assume;
3636
import org.junit.Test;
3737

3838
/**
@@ -1378,4 +1378,38 @@ private static void assertBase64DecodingOfTrailingBits(int nbits) {
13781378
}
13791379
}
13801380
}
1381+
1382+
/**
1383+
* Test for CODEC-265: Encode a 1GiB file.
1384+
*
1385+
* @see <a href="https://issues.apache.org/jira/projects/CODEC/issues/CODEC-265">CODEC-265</a>
1386+
*/
1387+
@Test
1388+
public void testCodec265() {
1389+
// 1GiB file to encode: 2^30 bytes
1390+
final int size1GiB = 1 << 30;
1391+
1392+
// Expecting a size of 4 output bytes per 3 input bytes plus the trailing bytes
1393+
// padded to a block size of 4.
1394+
int blocks = (int) Math.ceil(size1GiB / 3.0);
1395+
final int expectedLength = 4 * blocks;
1396+
1397+
// This test is memory hungry. Check we can run it.
1398+
final long presumableFreeMemory = BaseNCodecTest.getPresumableFreeMemory();
1399+
1400+
// Estimate the maximum memory required:
1401+
// 1GiB + 1GiB + ~2GiB + ~1.33GiB + 32 KiB = ~5.33GiB
1402+
//
1403+
// 1GiB: Input buffer to encode
1404+
// 1GiB: Existing working buffer (due to doubling of default buffer size of 8192)
1405+
// ~2GiB: New working buffer to allocate (due to doubling)
1406+
// ~1.33GiB: Expected output size (since the working buffer is copied at the end)
1407+
// 32KiB: Some head room
1408+
final long estimatedMemory = (long) size1GiB * 4 + expectedLength + 32 * 1024;
1409+
Assume.assumeTrue("Not enough free memory for the test", presumableFreeMemory > estimatedMemory);
1410+
1411+
final byte[] bytes = new byte[size1GiB];
1412+
final byte[] encoded = Base64.encodeBase64(bytes);
1413+
assertEquals(expectedLength, encoded.length);
1414+
}
13811415
}

src/test/java/org/apache/commons/codec/binary/BaseNCodecTest.java

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@
2323
import static org.junit.Assert.assertNotNull;
2424
import static org.junit.Assert.assertTrue;
2525

26+
import org.apache.commons.codec.binary.BaseNCodec.Context;
27+
import org.junit.Assert;
28+
import org.junit.Assume;
2629
import org.junit.Before;
2730
import org.junit.Test;
2831

@@ -191,4 +194,174 @@ void decode(final byte[] pArray, final int i, final int length, final Context co
191194
// Then
192195
assertEquals(0x25, actualPaddingByte);
193196
}
197+
198+
@Test
199+
public void testEnsureBufferSize() {
200+
final BaseNCodec ncodec = new NoOpBaseNCodec();
201+
final Context context = new Context();
202+
Assert.assertNull("Initial buffer should be null", context.buffer);
203+
204+
// Test initialisation
205+
context.pos = 76979;
206+
context.readPos = 273;
207+
ncodec.ensureBufferSize(0, context);
208+
Assert.assertNotNull("buffer should be initialised", context.buffer);
209+
Assert.assertEquals("buffer should be initialised to default size", ncodec.getDefaultBufferSize(), context.buffer.length);
210+
Assert.assertEquals("context position", 0, context.pos);
211+
Assert.assertEquals("context read position", 0, context.readPos);
212+
213+
// Test when no expansion is required
214+
ncodec.ensureBufferSize(1, context);
215+
Assert.assertEquals("buffer should not expand unless required", ncodec.getDefaultBufferSize(), context.buffer.length);
216+
217+
// Test expansion
218+
int length = context.buffer.length;
219+
context.pos = length;
220+
int extra = 1;
221+
ncodec.ensureBufferSize(extra, context);
222+
Assert.assertTrue("buffer should expand", context.buffer.length >= length + extra);
223+
224+
// Test expansion beyond double the buffer size.
225+
// Hits the edge case where the required capacity is more than the default expansion.
226+
length = context.buffer.length;
227+
context.pos = length;
228+
extra = length * 10;
229+
ncodec.ensureBufferSize(extra, context);
230+
Assert.assertTrue("buffer should expand beyond double capacity", context.buffer.length >= length + extra);
231+
}
232+
233+
/**
234+
* Test to expand to the max buffer size.
235+
*/
236+
@Test
237+
public void testEnsureBufferSizeExpandsToMaxBufferSize() {
238+
assertEnsureBufferSizeExpandsToMaxBufferSize(false);
239+
}
240+
241+
/**
242+
* Test to expand to beyond the max buffer size.
243+
*
244+
* <p>Note: If the buffer is required to expand to above the max buffer size it may not work
245+
* on all VMs and may have to be annotated with @Ignore.</p>
246+
*/
247+
@Test
248+
public void testEnsureBufferSizeExpandsToBeyondMaxBufferSize() {
249+
assertEnsureBufferSizeExpandsToMaxBufferSize(true);
250+
}
251+
252+
private static void assertEnsureBufferSizeExpandsToMaxBufferSize(boolean exceedMaxBufferSize) {
253+
// This test is memory hungry.
254+
// By default expansion will double the buffer size.
255+
// Using a buffer that must be doubled to get close to 2GiB requires at least 3GiB
256+
// of memory for the test (1GiB existing + 2GiB new).
257+
// As a compromise we use an empty buffer and rely on the expansion switching
258+
// to the minimum required capacity if doubling is not enough.
259+
260+
// To effectively use a full buffer of ~1GiB change the following for: 1 << 30.
261+
// Setting to zero has the lowest memory footprint for this test.
262+
final int length = 0;
263+
264+
final long presumableFreeMemory = getPresumableFreeMemory();
265+
// 2GiB + 32 KiB + length
266+
// 2GiB: Buffer to allocate
267+
// 32KiB: Some head room
268+
// length: Existing buffer
269+
final long estimatedMemory = (1L << 31) + 32 * 1024 + length;
270+
Assume.assumeTrue("Not enough free memory for the test", presumableFreeMemory > estimatedMemory);
271+
272+
final int max = Integer.MAX_VALUE - 8;
273+
274+
// Check the conservative maximum buffer size can actually be exceeded by the VM
275+
// otherwise the test is not valid.
276+
if (exceedMaxBufferSize) {
277+
assumeCanAllocateBufferSize(max + 1);
278+
// Free-memory.
279+
// This may not be necessary as the byte[] is now out of scope
280+
System.gc();
281+
}
282+
283+
final BaseNCodec ncodec = new NoOpBaseNCodec();
284+
final Context context = new Context();
285+
286+
// Allocate the initial buffer
287+
context.buffer = new byte[length];
288+
context.pos = length;
289+
// Compute the extra to reach or exceed the max buffer size
290+
int extra = max - length;
291+
if (exceedMaxBufferSize) {
292+
extra++;
293+
}
294+
ncodec.ensureBufferSize(extra, context);
295+
Assert.assertTrue(context.buffer.length >= length + extra);
296+
}
297+
298+
/**
299+
* Verify this VM can allocate the given size byte array. Otherwise skip the test.
300+
*/
301+
private static void assumeCanAllocateBufferSize(int size) {
302+
byte[] bytes = null;
303+
try {
304+
bytes = new byte[size];
305+
} catch (OutOfMemoryError ignore) {
306+
// ignore
307+
}
308+
Assume.assumeTrue("Cannot allocate array of size: " + size, bytes != null);
309+
}
310+
311+
/**
312+
* Gets the presumable free memory; an estimate of the amount of memory that could be allocated.
313+
*
314+
* <p>This performs a garbage clean-up and the obtains the presumed amount of free memory
315+
* that can be allocated in this VM. This is computed as:<p>
316+
*
317+
* <pre>
318+
* System.gc();
319+
* long allocatedMemory = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory();
320+
* long presumableFreeMemory = Runtime.getRuntime().maxMemory() - allocatedMemory;
321+
* </pre>
322+
*
323+
* @return the presumable free memory
324+
* @see <a href="https://stackoverflow.com/a/18366283">
325+
* Christian Fries StackOverflow answer on Java available memory</a>
326+
*/
327+
static long getPresumableFreeMemory() {
328+
System.gc();
329+
final long allocatedMemory = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory();
330+
return Runtime.getRuntime().maxMemory() - allocatedMemory;
331+
}
332+
333+
@Test(expected = OutOfMemoryError.class)
334+
public void testEnsureBufferSizeThrowsOnOverflow() {
335+
final BaseNCodec ncodec = new NoOpBaseNCodec();
336+
final Context context = new Context();
337+
338+
final int length = 10;
339+
context.buffer = new byte[length];
340+
context.pos = length;
341+
final int extra = Integer.MAX_VALUE;
342+
ncodec.ensureBufferSize(extra, context);
343+
}
344+
345+
/**
346+
* Extend BaseNCodec without implementation (no operations = NoOp).
347+
* Used for testing the memory allocation in {@link BaseNCodec#ensureBufferSize(int, Context)}.
348+
*/
349+
private static class NoOpBaseNCodec extends BaseNCodec {
350+
NoOpBaseNCodec() {
351+
super(0, 0, 0, 0);
352+
}
353+
354+
@Override
355+
void encode(byte[] pArray, int i, int length, Context context) {
356+
}
357+
358+
@Override
359+
void decode(byte[] pArray, int i, int length, Context context) {
360+
}
361+
362+
@Override
363+
protected boolean isInAlphabet(byte value) {
364+
return false;
365+
}
366+
}
194367
}

0 commit comments

Comments
 (0)