diff --git a/src/main/java/org/apache/commons/imaging/bytesource/InputStreamByteSource.java b/src/main/java/org/apache/commons/imaging/bytesource/InputStreamByteSource.java index 1a8778681..1273ee595 100644 --- a/src/main/java/org/apache/commons/imaging/bytesource/InputStreamByteSource.java +++ b/src/main/java/org/apache/commons/imaging/bytesource/InputStreamByteSource.java @@ -23,8 +23,8 @@ import java.util.Objects; import org.apache.commons.imaging.ImagingException; -import org.apache.commons.imaging.common.Allocator; import org.apache.commons.imaging.common.BinaryFunctions; +import org.apache.commons.imaging.common.KnownSizeByteArrayBuilder; import org.apache.commons.io.IOUtils; import org.apache.commons.io.build.AbstractOrigin.InputStreamOrigin; @@ -191,18 +191,9 @@ public byte[] getByteArray(final long position, final int length) throws IOExcep final InputStream cis = getInputStream(); BinaryFunctions.skipBytes(cis, position); - final byte[] bytes = Allocator.byteArray(length); - int total = 0; - while (true) { - final int read = cis.read(bytes, total, bytes.length - total); - if (read < 1) { - throw new ImagingException("Could not read block."); - } - total += read; - if (total >= length) { - return bytes; - } - } + KnownSizeByteArrayBuilder byteArrayBuilder = new KnownSizeByteArrayBuilder(length); + byteArrayBuilder.addAllBytesFrom(cis); + return byteArrayBuilder.createByteArray(); } private Block getFirstBlock() throws IOException { diff --git a/src/main/java/org/apache/commons/imaging/common/Allocator.java b/src/main/java/org/apache/commons/imaging/common/Allocator.java index e75640f4b..c74eeb4be 100644 --- a/src/main/java/org/apache/commons/imaging/common/Allocator.java +++ b/src/main/java/org/apache/commons/imaging/common/Allocator.java @@ -19,6 +19,8 @@ import java.math.BigInteger; import java.util.ArrayList; +import java.util.Collection; +import java.util.List; import java.util.function.IntFunction; /** @@ -36,23 +38,12 @@ public class Allocator { LIMIT = Integer.getInteger(CANONICAL_NAME, DEFAULT); } - /** - * Allocates an Object of type T of the requested size. - * - * @param The return array type - * @param request The requested size. - * @param factory The array factory. - * @return a new byte array. - * @throws AllocationRequestException Thrown when the request exceeds the limit. - * @see #check(int) - */ - public static T apply(final int request, final IntFunction factory) { - return factory.apply(check(request)); - } - /** * Allocates an array of type T of the requested size. * + *

Important: If possible avoid this method and avoid Object arrays, and instead + * use {@link List}s created with {@link #arrayList(int)}. + * * @param The return array type * @param request The requested size. * @param factory The array factory. @@ -62,22 +53,55 @@ public static T apply(final int request, final IntFunction factory) { * @see #check(int) */ public static T[] array(final int request, final IntFunction factory, final int eltShallowByteSize) { - check(request * eltShallowByteSize); + check(Math.multiplyExact(request, eltShallowByteSize)); return factory.apply(request); } /** - * Allocates an Object array of type T of the requested size. + * Similar to {@link #array(int, IntFunction, int)}, except that the caller is reasonable sure that + * the requested size is 'trusted', that is: + *

    + *
  • It is a constant value which cannot be influenced by the user in any way + *
  • Or, it relates to the size of memory which has already been successfully allocated, + * for example an array of a different type or a {@link Collection} + *
+ */ + public static T[] arrayTrusted(final int request, final IntFunction factory, final int eltShallowByteSize) { + // For now simply delegate to `array`; this 'trusted' method here is at the moment + // mainly intended to detect untrusted calls + return array(request, factory, eltShallowByteSize); + } + + /** + * Creates an array list of the requested initial capacity. The capacity can be + * untrusted; that is, it may come from metadata which can directly be + * controlled by the (potentially malicious) user. + */ + public static ArrayList arrayList(int initialCapacity) { + // Limit the capacity to a reasonable maximum + return new ArrayList<>(Math.min(initialCapacity, 1024)); + } + + /** + * Creates an array list of the requested initial capacity. The capacity should be + * trusted; that is, it should either be a constant or there should already + * be an existing collection of the same size. * - * @param The return array type - * @param request The requested size. - * @return a new byte array. - * @throws AllocationRequestException Thrown when the request exceeds the limit. - * @see #check(int) + *

This method must not be called with with an untrusted capacity, for example + * one which has only been read from user-controlled metadata; use {@link #arrayList(int)} + * for that. */ - public static ArrayList arrayList(final int request) { - check(24 + request * 4); // 4 bytes per element - return apply(request, ArrayList::new); + public static ArrayList arrayListTrusted(int initialCapacity) { + // For sanity still check requested capacity + check(Math.addExact(24, Math.multiplyExact(initialCapacity, 4))); // 4 bytes per element + return new ArrayList<>(initialCapacity); + } + + /** + * Creates an array list with the size of the given collection as initial capacity. + */ + public static ArrayList arrayListWithCapacityFor(Collection collection) { + return arrayListTrusted(collection.size()); } /** @@ -101,7 +125,22 @@ public static byte[] byteArray(final int request) { * @see #check(int, int) */ public static byte[] byteArray(final long request) { - return new byte[check(request, Byte.BYTES)]; + return byteArray(Math.toIntExact(request)); + } + + /** + * Similar to {@link #byteArray(int)}, except that the caller is reasonable sure that + * the requested size is 'trusted', that is: + *

    + *
  • It is a constant value which cannot be influenced by the user in any way + *
  • Or, it relates to the size of memory which has already been successfully allocated, + * for example an array of a different type or a {@link Collection} + *
+ */ + public static byte[] byteArrayTrusted(final int request) { + // For now simply delegate to `byteArray`; this 'trusted' method here is at the moment + // mainly intended to detect untrusted calls + return byteArray(request); } /** @@ -116,11 +155,26 @@ public static char[] charArray(final int request) { return new char[check(request, Character.BYTES)]; } + /** + * Similar to {@link #charArray(int)}, except that the caller is reasonable sure that + * the requested size is 'trusted', that is: + *
    + *
  • It is a constant value which cannot be influenced by the user in any way + *
  • Or, it relates to the size of memory which has already been successfully allocated, + * for example an array of a different type or a {@link Collection} + *
+ */ + public static char[] charArrayTrusted(final int request) { + // For now simply delegate to `charArray`; this 'trusted' method here is at the moment + // mainly intended to detect untrusted calls + return charArray(request); + } + /** * Checks a request for meeting allocation limits. *

* The default limit is {@value #DEFAULT}, override with the system property - * "org.apache.commons.imaging.common.mylzw.AllocationChecker". + * "org.apache.commons.imaging.common.Allocator". *

* * @param request an allocation request. @@ -128,6 +182,10 @@ public static char[] charArray(final int request) { * @throws AllocationRequestException Thrown when the request exceeds the limit. */ public static int check(final int request) { + // Check for numeric overflow from caller + if (request < 0) { + throw new IllegalArgumentException("Invalid request value: " + request); + } if (request > LIMIT) { throw new AllocationRequestException(LIMIT, request); } @@ -138,7 +196,7 @@ public static int check(final int request) { * Checks a request for meeting allocation limits. *

* The default limit is {@value #DEFAULT}, override with the system property - * "org.apache.commons.imaging.common.mylzw.AllocationChecker". + * "org.apache.commons.imaging.common.Allocator". *

* * @param request an allocation request count. @@ -147,6 +205,11 @@ public static int check(final int request) { * @throws AllocationRequestException Thrown when the request exceeds the limit. */ public static int check(final int request, final int elementSize) { + // Check for numeric overflow from caller + if (request < 0) { + throw new IllegalArgumentException("Invalid request value: " + request); + } + int multiplyExact; try { multiplyExact = Math.multiplyExact(request, elementSize); @@ -159,26 +222,6 @@ public static int check(final int request, final int elementSize) { return request; } - /** - * Checks a request for meeting allocation limits. - *

- * The default limit is {@value #DEFAULT}, override with the system property - * "org.apache.commons.imaging.common.mylzw.AllocationChecker". - *

- * - * @param request an allocation request count is cast down to an int. - * @param elementSize The element size. - * @return the request. - * @throws AllocationRequestException Thrown when the request exceeds the limit. - */ - public static int check(final long request, final int elementSize) { - try { - return check(Math.toIntExact(request), elementSize); - } catch (ArithmeticException e) { - throw new AllocationRequestException(LIMIT, request, e); - } - } - /** * Checks that allocating a byte array of the requested size is within the limit. * @@ -189,6 +232,19 @@ public static int checkByteArray(final int request) { return check(request, Byte.BYTES); } + /** + * Similar to {@link #checkByteArray(int)}, except that the caller is reasonable sure that + * the requested size is 'trusted', that is: + *
    + *
  • It is a constant value which cannot be influenced by the user in any way + *
  • Or, it relates to the size of memory which has already been successfully allocated, + * for example an array of a different type or a {@link Collection} + *
+ */ + public static int checkByteArrayTrusted(final int request) { + return checkByteArray(request); + } + /** * Allocates a double array of the requested size. * @@ -201,6 +257,21 @@ public static double[] doubleArray(final int request) { return new double[check(request, Double.BYTES)]; } + /** + * Similar to {@link #doubleArray(int)}, except that the caller is reasonable sure that + * the requested size is 'trusted', that is: + *
    + *
  • It is a constant value which cannot be influenced by the user in any way + *
  • Or, it relates to the size of memory which has already been successfully allocated, + * for example an array of a different type or a {@link Collection} + *
+ */ + public static double[] doubleArrayTrusted(final int request) { + // For now simply delegate to `doubleArray`; this 'trusted' method here is at the moment + // mainly intended to detect untrusted calls + return doubleArray(request); + } + /** * Allocates a float array of the requested size. * @@ -213,6 +284,21 @@ public static float[] floatArray(final int request) { return new float[check(request, Float.BYTES)]; } + /** + * Similar to {@link #floatArray(int)}, except that the caller is reasonable sure that + * the requested size is 'trusted', that is: + *
    + *
  • It is a constant value which cannot be influenced by the user in any way + *
  • Or, it relates to the size of memory which has already been successfully allocated, + * for example an array of a different type or a {@link Collection} + *
+ */ + public static float[] floatArrayTrusted(final int request) { + // For now simply delegate to `floatArray`; this 'trusted' method here is at the moment + // mainly intended to detect untrusted calls + return floatArray(request); + } + /** * Allocates a int array of the requested size. * @@ -226,15 +312,18 @@ public static int[] intArray(final int request) { } /** - * Allocates a long array of the requested size. - * - * @param request The requested size. - * @return a new long array. - * @throws AllocationRequestException Thrown when the request exceeds the limit. - * @see #check(int, int) + * Similar to {@link #intArray(int)}, except that the caller is reasonable sure that + * the requested size is 'trusted', that is: + *
    + *
  • It is a constant value which cannot be influenced by the user in any way + *
  • Or, it relates to the size of memory which has already been successfully allocated, + * for example an array of a different type or a {@link Collection} + *
*/ - public static long[] longArray(final int request) { - return new long[check(request, Long.BYTES)]; + public static int[] intArrayTrusted(final int request) { + // For now simply delegate to `intArray`; this 'trusted' method here is at the moment + // mainly intended to detect untrusted calls + return intArray(request); } /** @@ -249,4 +338,18 @@ public static short[] shortArray(final int request) { return new short[check(request, Short.BYTES)]; } + /** + * Similar to {@link #shortArray(int)}, except that the caller is reasonable sure that + * the requested size is 'trusted', that is: + *
    + *
  • It is a constant value which cannot be influenced by the user in any way + *
  • Or, it relates to the size of memory which has already been successfully allocated, + * for example an array of a different type or a {@link Collection} + *
+ */ + public static short[] shortArrayTrusted(final int request) { + // For now simply delegate to `shortArray`; this 'trusted' method here is at the moment + // mainly intended to detect untrusted calls + return shortArray(request); + } } diff --git a/src/main/java/org/apache/commons/imaging/common/BasicCParser.java b/src/main/java/org/apache/commons/imaging/common/BasicCParser.java index cef20f828..e7d4d2927 100644 --- a/src/main/java/org/apache/commons/imaging/common/BasicCParser.java +++ b/src/main/java/org/apache/commons/imaging/common/BasicCParser.java @@ -353,7 +353,8 @@ public static String[] tokenizeRow(final String row) { ++numLiveTokens; } } - final String[] liveTokens = Allocator.array(numLiveTokens, String[]::new, 24); + // Trusted because length is based on length of existing array + final String[] liveTokens = Allocator.arrayTrusted(numLiveTokens, String[]::new, 24); int next = 0; for (final String token : tokens) { if (token != null && !token.isEmpty()) { diff --git a/src/main/java/org/apache/commons/imaging/common/BinaryFunctions.java b/src/main/java/org/apache/commons/imaging/common/BinaryFunctions.java index 689a4a914..6ac7f9a8b 100644 --- a/src/main/java/org/apache/commons/imaging/common/BinaryFunctions.java +++ b/src/main/java/org/apache/commons/imaging/common/BinaryFunctions.java @@ -21,6 +21,7 @@ import java.io.PrintWriter; import java.io.RandomAccessFile; import java.nio.ByteOrder; +import java.util.Arrays; import java.util.logging.Logger; import org.apache.commons.imaging.ImagingException; @@ -71,7 +72,7 @@ public static int findNull(final byte[] src, final String message) throws Imagin public static byte[] getBytes(final RandomAccessFile raf, final long pos, final int length, final String exception) throws IOException { - if (length < 0) { + if (length < 0 || length > raf.length()) { throw new IOException(String.format("%s, invalid length: %d", exception, length)); } Allocator.checkByteArray(length); @@ -230,11 +231,13 @@ public static byte[] readBytes(final String name, final InputStream is, final in public static byte[] readBytes(final String name, final InputStream is, final int length, final String exception) throws IOException { + KnownSizeByteArrayBuilder byteArrayBuilder = new KnownSizeByteArrayBuilder(length); try { - return IOUtils.toByteArray(is, Allocator.check(length)); + byteArrayBuilder.addAllBytesFrom(is); } catch (IOException e) { - throw new IOException(exception + ", name: " + name + ", length: " + length); + throw new IOException(exception + ", name: " + name + ", length: " + length, e); } + return byteArrayBuilder.createByteArray(); } public static byte[] remainingBytes(final String name, final byte[] bytes, final int count) { @@ -282,9 +285,11 @@ public static long skipBytes(final InputStream is, final long length, final Stri } public static byte[] slice(final byte[] bytes, final int start, final int count) { - final byte[] result = Allocator.byteArray(count); - System.arraycopy(bytes, start, result, 0, count); - return result; + // Validate here; otherwise copyOfRange might create an array larger than the original one + if (start < 0 || count < 0 || bytes.length - count < start) { + throw new IllegalArgumentException("Invalid start or count"); + } + return Arrays.copyOfRange(bytes, start, start + count); } public static boolean startsWith(final byte[] buffer, final BinaryConstant search) { diff --git a/src/main/java/org/apache/commons/imaging/common/ByteConversions.java b/src/main/java/org/apache/commons/imaging/common/ByteConversions.java index 2cc020585..92bd7b153 100644 --- a/src/main/java/org/apache/commons/imaging/common/ByteConversions.java +++ b/src/main/java/org/apache/commons/imaging/common/ByteConversions.java @@ -57,11 +57,21 @@ public static byte[] toBytes(final double[] values, final ByteOrder byteOrder) { return toBytes(values, 0, values.length, byteOrder); } + // TODO Replace this with java.util.Objects.checkFromIndexSize(int, int, int) once targeting Java 9+ + private static void checkOffsetAndLength(int arrayLength, int offset, int length) { + if (offset < 0 || length < 0 || arrayLength - length < offset) { + throw new IllegalArgumentException("Invalid offset or length"); + } + } + private static byte[] toBytes(final double[] values, final int offset, final int length, final ByteOrder byteOrder) { - final byte[] result = Allocator.byteArray(length * 8); + checkOffsetAndLength(values.length, offset, length); + + // Trusted because length is based on length of existing array + final byte[] result = Allocator.byteArrayTrusted(length * Double.BYTES); for (int i = 0; i < length; i++) { - toBytes(values[offset + i], byteOrder, result, i * 8); + toBytes(values[offset + i], byteOrder, result, i * Double.BYTES); } return result; } @@ -92,9 +102,12 @@ public static byte[] toBytes(final float[] values, final ByteOrder byteOrder) { } private static byte[] toBytes(final float[] values, final int offset, final int length, final ByteOrder byteOrder) { - final byte[] result = Allocator.byteArray(length * 4); + checkOffsetAndLength(values.length, offset, length); + + // Trusted because length is based on length of existing array + final byte[] result = Allocator.byteArrayTrusted(length * Float.BYTES); for (int i = 0; i < length; i++) { - toBytes(values[offset + i], byteOrder, result, i * 4); + toBytes(values[offset + i], byteOrder, result, i * Float.BYTES); } return result; } @@ -124,9 +137,12 @@ public static byte[] toBytes(final int[] values, final ByteOrder byteOrder) { } private static byte[] toBytes(final int[] values, final int offset, final int length, final ByteOrder byteOrder) { - final byte[] result = Allocator.byteArray(length * 4); + checkOffsetAndLength(values.length, offset, length); + + // Trusted because length is based on length of existing array + final byte[] result = Allocator.byteArrayTrusted(length * Integer.BYTES); for (int i = 0; i < length; i++) { - toBytes(values[offset + i], byteOrder, result, i * 4); + toBytes(values[offset + i], byteOrder, result, i * Integer.BYTES); } return result; } @@ -166,7 +182,10 @@ public static byte[] toBytes(final RationalNumber[] values, final ByteOrder byte private static byte[] toBytes(final RationalNumber[] values, final int offset, final int length, final ByteOrder byteOrder) { - final byte[] result = Allocator.byteArray(length * 8); + checkOffsetAndLength(values.length, offset, length); + + // Trusted because length is based on length of existing array + final byte[] result = Allocator.byteArrayTrusted(length * 8); for (int i = 0; i < length; i++) { toBytes(values[offset + i], byteOrder, result, i * 8); } @@ -194,9 +213,12 @@ public static byte[] toBytes(final short[] values, final ByteOrder byteOrder) { } private static byte[] toBytes(final short[] values, final int offset, final int length, final ByteOrder byteOrder) { - final byte[] result = Allocator.byteArray(length * 2); + checkOffsetAndLength(values.length, offset, length); + + // Trusted because length is based on length of existing array + final byte[] result = Allocator.byteArrayTrusted(length * Short.BYTES); for (int i = 0; i < length; i++) { - toBytes(values[offset + i], byteOrder, result, i * 2); + toBytes(values[offset + i], byteOrder, result, i * Short.BYTES); } return result; } @@ -233,8 +255,11 @@ public static double[] toDoubles(final byte[] bytes, final ByteOrder byteOrder) private static double[] toDoubles(final byte[] bytes, final int offset, final int length, final ByteOrder byteOrder) { - final double[] result = Allocator.doubleArray(length / 8); - Arrays.setAll(result, i -> toDouble(bytes, offset + 8 * i, byteOrder)); + checkOffsetAndLength(bytes.length, offset, length); + + // Trusted because length is based on length of existing array + final double[] result = Allocator.doubleArrayTrusted(length / Double.BYTES); + Arrays.setAll(result, i -> toDouble(bytes, offset + Double.BYTES * i, byteOrder)); return result; } @@ -262,9 +287,12 @@ public static float[] toFloats(final byte[] bytes, final ByteOrder byteOrder) { private static float[] toFloats(final byte[] bytes, final int offset, final int length, final ByteOrder byteOrder) { - final float[] result = Allocator.floatArray(length / 4); + checkOffsetAndLength(bytes.length, offset, length); + + // Trusted because length is based on length of existing array + final float[] result = Allocator.floatArrayTrusted(length / Float.BYTES); for (int i = 0; i < result.length; i++) { - result[i] = toFloat(bytes, offset + 4 * i, byteOrder); + result[i] = toFloat(bytes, offset + Float.BYTES * i, byteOrder); } return result; } @@ -290,8 +318,11 @@ public static int[] toInts(final byte[] bytes, final ByteOrder byteOrder) { private static int[] toInts(final byte[] bytes, final int offset, final int length, final ByteOrder byteOrder) { - final int[] result = Allocator.intArray(length / 4); - Arrays.setAll(result, i -> toInt(bytes, offset + 4 * i, byteOrder)); + checkOffsetAndLength(bytes.length, offset, length); + + // Trusted because length is based on length of existing array + final int[] result = Allocator.intArrayTrusted(length / Integer.BYTES); + Arrays.setAll(result, i -> toInt(bytes, offset + Integer.BYTES * i, byteOrder)); return result; } @@ -349,7 +380,10 @@ private static RationalNumber[] toRationals( final int length, final ByteOrder byteOrder, final boolean unsignedType) { - final RationalNumber[] result = new RationalNumber[length / 8]; + checkOffsetAndLength(bytes.length, offset, length); + + // Trusted because length is based on length of existing array + final RationalNumber[] result = Allocator.arrayTrusted(length / 8, RationalNumber[]::new, 1); Arrays.setAll(result, i -> toRational(bytes, offset + 8 * i, byteOrder, unsignedType)); return result; } @@ -368,9 +402,12 @@ public static short[] toShorts(final byte[] bytes, final ByteOrder byteOrder) { private static short[] toShorts(final byte[] bytes, final int offset, final int length, final ByteOrder byteOrder) { - final short[] result = Allocator.shortArray(length / 2); + checkOffsetAndLength(bytes.length, offset, length); + + // Trusted because length is based on length of existing array + final short[] result = Allocator.shortArrayTrusted(length / Short.BYTES); for (int i = 0; i < result.length; i++) { - result[i] = toShort(bytes, offset + 2 * i, byteOrder); + result[i] = toShort(bytes, offset + Short.BYTES * i, byteOrder); } return result; } @@ -394,7 +431,10 @@ public static int[] toUInt16s(final byte[] bytes, final ByteOrder byteOrder) { private static int[] toUInt16s(final byte[] bytes, final int offset, final int length, final ByteOrder byteOrder) { - final int[] result = Allocator.intArray(length / 2); + checkOffsetAndLength(bytes.length, offset, length); + + // Trusted because length is based on length of existing array + final int[] result = Allocator.intArrayTrusted(length / 2); Arrays.setAll(result, i -> toUInt16(bytes, offset + 2 * i, byteOrder)); return result; } diff --git a/src/main/java/org/apache/commons/imaging/common/ImageBuilder.java b/src/main/java/org/apache/commons/imaging/common/ImageBuilder.java index 121c3b731..d0a5d42c5 100644 --- a/src/main/java/org/apache/commons/imaging/common/ImageBuilder.java +++ b/src/main/java/org/apache/commons/imaging/common/ImageBuilder.java @@ -127,14 +127,14 @@ private void checkBounds(final int x, final int y, final int w, final int h) { if (x < 0 || x >= width) { throw new RasterFormatException("subimage x is outside raster"); } - if (x + w > width) { + if (width - w < x) { throw new RasterFormatException( "subimage (x+width) is outside raster"); } if (y < 0 || y >= height) { throw new RasterFormatException("subimage y is outside raster"); } - if (y + h > height) { + if (height - h < y) { throw new RasterFormatException( "subimage (y+height) is outside raster"); } @@ -145,7 +145,7 @@ private void checkBounds(final int x, final int y, final int w, final int h) { * * @param width image width (must be greater than zero) * @param height image height (must be greater than zero) - * @throws RasterFormatException if {@code width} or {@code height} are equal or less than zero + * @throws RasterFormatException if {@code width} or {@code height} are equal or less than zero, or if they are too large */ private void checkDimensions(final int width, final int height) { if (width <= 0) { @@ -154,6 +154,10 @@ private void checkDimensions(final int width, final int height) { if (height <= 0) { throw new RasterFormatException("zero or negative height value"); } + // Check for overflow + if (width * height < 0) { + throw new RasterFormatException("too large width or height value"); + } } /** diff --git a/src/main/java/org/apache/commons/imaging/common/KnownSizeByteArrayBuilder.java b/src/main/java/org/apache/commons/imaging/common/KnownSizeByteArrayBuilder.java new file mode 100644 index 000000000..2b2cd3438 --- /dev/null +++ b/src/main/java/org/apache/commons/imaging/common/KnownSizeByteArrayBuilder.java @@ -0,0 +1,264 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.commons.imaging.common; + +import java.io.ByteArrayOutputStream; +import java.io.EOFException; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.apache.commons.io.output.UnsynchronizedByteArrayOutputStream; + +/** + * Internal class + * + *

Builder for a byte array of known size. The main purpose of this class is to + * provide a reasonably efficient way to create a byte array of a certain known size + * without eagerly allocating an array of that size, since that could be abused to + * cause a denial of service attack if the desired array lenght can be controlled + * by a malicious user. + * + *

This class is similar to {@link ByteArrayOutputStream} respectively {@link UnsynchronizedByteArrayOutputStream} + * with the main difference that it is most likely more efficient because + *

    + *
  • it knows the expected byte array length in advance (which is not the same + * as an 'initial capacity') and can therefore allocate temporary byte arrays more + * efficiently + *
  • it does not use any (redundant) synchronization + *
  • it is only single-use and can return the internal temporary byte array + * without having to create any copies + *
+ */ +public class KnownSizeByteArrayBuilder { + // See also https://stackoverflow.com/q/3038392 + private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 10; + static final int INITIAL_SECTION_SIZE = 4096; + + private final int expectedSize; + + /** Total number of bytes added so far */ + private int addedBytesCount = 0; + private List sections = new ArrayList<>(); + /** Current section where bytes are added to; {@code null} if final byte array has already been created */ + private byte[] currentSection; + /** End index (exclusive) of {@link #currentSection}, respectively start index of next write */ + private int currentSectionEnd = 0; + + /** + * @param expectedSize The expected final array size; the number of bytes must + * not be greater than this, but it may be lower, see {@link #createByteArray(boolean)} + */ + public KnownSizeByteArrayBuilder(int expectedSize) { + if (expectedSize < 0 || expectedSize > MAX_ARRAY_SIZE) { + throw new IllegalArgumentException("Invalid size: " + expectedSize); + } + Allocator.checkByteArray(expectedSize); + + this.expectedSize = expectedSize; + + currentSection = new byte[Math.min(expectedSize, INITIAL_SECTION_SIZE)]; + sections.add(currentSection); + } + + private void checkByteArrayCreated() { + if (currentSection == null) { + throw new IllegalStateException("Byte array has already been created"); + } + } + + private void ensureSpaceInCurrentSection() { + if (currentSectionEnd >= currentSection.length) { + int nextSectionSize = currentSection.length * 2; + // In case of overflow or if next section would be equal or larger than remaining expected, + // directly create final byte array + if (nextSectionSize < 0 || nextSectionSize >= (expectedSize - addedBytesCount)) { + currentSection = new byte[expectedSize]; + + int copyStartPos = 0; + for (byte[] section : sections) { + System.arraycopy(section, 0, currentSection, copyStartPos, section.length); + copyStartPos += section.length; + } + // Won't be used anymore; also let garbage collection collect the sections + sections = null; + currentSectionEnd = copyStartPos; + } else { + currentSection = new byte[nextSectionSize]; + sections.add(currentSection); + currentSectionEnd = 0; + } + } + } + + /** + * Returns the number of so far added bytes. + */ + public int getAddedBytesCount() { + return addedBytesCount; + } + + /** + * Add the number of expected bytes from the input stream. + * + * @throws EOFException If the input stream provides less than the expected number of bytes + */ + public void addAllBytesFrom(InputStream in) throws IOException { + checkByteArrayCreated(); + + while (true) { + int missingBytesCount = expectedSize - addedBytesCount; + if (missingBytesCount <= 0) { + return; + } + + ensureSpaceInCurrentSection(); + + int maxRead = Math.min(missingBytesCount, currentSection.length - currentSectionEnd); + int readCount = in.read(currentSection, currentSectionEnd, maxRead); + if (readCount < 0) { + throw new EOFException("Unexpected EOF; was expecting more bytes"); + } + if (readCount > maxRead) { + throw new RuntimeException("Bad InputStream implementation read more bytes than requested: " + in.toString()); + } + addedBytesCount += readCount; + currentSectionEnd += readCount; + } + } + + /** + * Adds a single byte. + * + * @throws IllegalStateException If all expected bytes have already been added + */ + public void addByte(byte b) { + checkByteArrayCreated(); + + if (addedBytesCount >= expectedSize) { + throw new IllegalStateException("Already added all expected bytes"); + } + + ensureSpaceInCurrentSection(); + currentSection[currentSectionEnd] = b; + currentSectionEnd++; + addedBytesCount++; + } + + /** + * Adds bytes from an array. + * + * @throws IllegalStateException If {@code length} is greater than the remaining expected bytes + */ + public void addBytes(byte[] bytes, int start, int length) { + if (start < 0 || length < 0 || bytes.length - length < start) { + throw new IllegalArgumentException("Bad start or length value"); + } + + if (length == 0) { + return; + } + if (length > expectedSize - addedBytesCount) { + throw new IllegalStateException("Trying to add more bytes than expected"); + } + + do { + ensureSpaceInCurrentSection(); + int copyCount = Math.min(length, currentSection.length - currentSectionEnd); + System.arraycopy(bytes, start, currentSection, currentSectionEnd, copyCount); + currentSectionEnd += copyCount; + addedBytesCount += copyCount; + + start += copyCount; + length -= copyCount; + } while (length > 0); + } + + /** + * Creates the final byte array. This method can only be called at most once; + * afterwards none of the other methods of this class can be called anymore. + * + * @param allowSmallerThanExpected Whether to allow creating the array even + * if not all expected bytes (as specified originally for the constructor) + * have been added yet + */ + public byte[] createByteArray(boolean allowSmallerThanExpected) { + checkByteArrayCreated(); + + byte[] resultArray; + + if (addedBytesCount >= expectedSize) { + // Current section contains complete content + resultArray = currentSection; + } else if (allowSmallerThanExpected) { + // Check if sections contains only currentSection + if (sections == null || sections.size() == 1) { + resultArray = Arrays.copyOf(currentSection, currentSectionEnd); + } else { + resultArray = new byte[addedBytesCount]; + + int copyStartPos = 0; + // Copy the complete content of all sections except the last (= currentSection) + for (int sectionIndex = 0; sectionIndex < sections.size() - 1; sectionIndex++) { + byte[] section = sections.get(sectionIndex); + System.arraycopy(section, 0, resultArray, copyStartPos, section.length); + copyStartPos += section.length; + } + + System.arraycopy(currentSection, 0, resultArray, copyStartPos, currentSectionEnd); + } + } else { + throw new IllegalStateException("Not all bytes have been added yet"); + } + + currentSection = null; + sections = null; + return resultArray; + } + + /** + * Creates the final byte array; must only be called once. + * + *

This is a convenience methods which calls {@link #createByteArray(boolean) createByteArray(false)}. + */ + public byte[] createByteArray() { + return createByteArray(false); + } + + /** + * Creates an {@link OutputStream} which can be used to add bytes to this builder. + * This is mainly intended for cases when working with an API which expects an {@code OutputStream}; + * in all other cases prefer one of the other methods of this class. + */ + public OutputStream asOutputStream() { + return new OutputStream() { + @Override + public void write(int b) throws IOException { + addByte((byte) b); + } + + @Override + public void write(byte[] b, int off, int len) throws IOException { + addBytes(b, off, len); + } + }; + } +} diff --git a/src/main/java/org/apache/commons/imaging/common/PackBits.java b/src/main/java/org/apache/commons/imaging/common/PackBits.java index 20aaa956d..c03431096 100644 --- a/src/main/java/org/apache/commons/imaging/common/PackBits.java +++ b/src/main/java/org/apache/commons/imaging/common/PackBits.java @@ -27,7 +27,7 @@ public class PackBits { public byte[] compress(final byte[] bytes) throws IOException { // max length 1 extra byte for every 128 try (UnsynchronizedByteArrayOutputStream baos = UnsynchronizedByteArrayOutputStream.builder() - .setBufferSize(Allocator.checkByteArray(bytes.length * 2)) + .setBufferSize(Allocator.checkByteArrayTrusted(bytes.length * 2)) .get()) { int ptr = 0; while (ptr < bytes.length) { diff --git a/src/main/java/org/apache/commons/imaging/common/ZlibDeflate.java b/src/main/java/org/apache/commons/imaging/common/ZlibDeflate.java index cef6ace4c..98115a0b2 100644 --- a/src/main/java/org/apache/commons/imaging/common/ZlibDeflate.java +++ b/src/main/java/org/apache/commons/imaging/common/ZlibDeflate.java @@ -47,7 +47,7 @@ public class ZlibDeflate { * @see DeflaterOutputStream */ public static byte[] compress(final byte[] bytes) throws ImagingException { - final ByteArrayOutputStream out = new ByteArrayOutputStream(Allocator.checkByteArray(bytes.length / 2)); + final ByteArrayOutputStream out = new ByteArrayOutputStream(Allocator.checkByteArrayTrusted(bytes.length / 2)); try (DeflaterOutputStream compressOut = new DeflaterOutputStream(out)) { compressOut.write(bytes); } catch (final IOException e) { diff --git a/src/main/java/org/apache/commons/imaging/formats/dcx/DcxImageParser.java b/src/main/java/org/apache/commons/imaging/formats/dcx/DcxImageParser.java index c07961ddc..9e96f8d3c 100644 --- a/src/main/java/org/apache/commons/imaging/formats/dcx/DcxImageParser.java +++ b/src/main/java/org/apache/commons/imaging/formats/dcx/DcxImageParser.java @@ -158,7 +158,7 @@ private DcxHeader readDcxHeader(final ByteSource byteSource) try (InputStream is = byteSource.getInputStream()) { final int id = read4Bytes("Id", is, "Not a Valid DCX File", getByteOrder()); final int size = 1024; - final List pageTable = Allocator.arrayList(size); + final List pageTable = Allocator.arrayListTrusted(size); for (int i = 0; i < size; i++) { final long pageOffset = 0xFFFFffffL & read4Bytes("PageTable", is, "Not a Valid DCX File", getByteOrder()); if (pageOffset == 0) { diff --git a/src/main/java/org/apache/commons/imaging/formats/gif/GifImageParser.java b/src/main/java/org/apache/commons/imaging/formats/gif/GifImageParser.java index 7e48e656b..c855a41fb 100644 --- a/src/main/java/org/apache/commons/imaging/formats/gif/GifImageParser.java +++ b/src/main/java/org/apache/commons/imaging/formats/gif/GifImageParser.java @@ -175,7 +175,7 @@ private List findAllImageData(final GifImageContents imageContents throw new ImagingException("GIF: Invalid amount of Graphic Control Extensions"); } - final List imageData = Allocator.arrayList(descriptors.size()); + final List imageData = Allocator.arrayListWithCapacityFor(descriptors); for(int i = 0; i < descriptors.size(); i++) { final ImageDescriptor descriptor = descriptors.get(i); if (descriptor == null) { @@ -235,7 +235,7 @@ public List getAllBufferedImages(final ByteSource byteSource) } final List imageData = findAllImageData(imageContents); - final List result = Allocator.arrayList(imageData.size()); + final List result = Allocator.arrayListWithCapacityFor(imageData); for(final GifImageData id : imageData) { result.add(getBufferedImage(ghi, id, imageContents.globalColorTable)); } @@ -347,7 +347,8 @@ private int[] getColorTable(final byte[] bytes) throws ImagingException { } final int length = bytes.length / 3; - final int[] result = Allocator.intArray(length); + // Trusted because length is based on length of existing array + final int[] result = Allocator.intArrayTrusted(length); for (int i = 0; i < length; i++) { final int red = 0xff & bytes[(i * 3) + 0]; @@ -490,7 +491,7 @@ public ImageMetadata getMetadata(final ByteSource byteSource, final GifImagingPa } final List imageData = findAllImageData(imageContents); - final List metadataItems = Allocator.arrayList(imageData.size()); + final List metadataItems = Allocator.arrayListWithCapacityFor(imageData); for(final GifImageData id : imageData) { final DisposalMethod disposalMethod = createDisposalMethodFromIntValue(id.gce.dispose); metadataItems.add(new GifImageMetadataItem(id.gce.delay, id.descriptor.imageLeftPosition, id.descriptor.imageTopPosition, disposalMethod)); diff --git a/src/main/java/org/apache/commons/imaging/formats/ico/IcoImageParser.java b/src/main/java/org/apache/commons/imaging/formats/ico/IcoImageParser.java index f3bbc1681..e2a610087 100644 --- a/src/main/java/org/apache/commons/imaging/formats/ico/IcoImageParser.java +++ b/src/main/java/org/apache/commons/imaging/formats/ico/IcoImageParser.java @@ -438,7 +438,7 @@ private IconData readBitmapIconData(final byte[] iconData, final IconInfo fIconI : colorsUsed); final int bitmapSize = 14 + 56 + restOfFile.length; - final ByteArrayOutputStream baos = new ByteArrayOutputStream(Allocator.checkByteArray(bitmapSize)); + final ByteArrayOutputStream baos = new ByteArrayOutputStream(Allocator.checkByteArrayTrusted(bitmapSize)); try (BinaryOutputStream bos = BinaryOutputStream.littleEndian(baos)) { bos.write('B'); bos.write('M'); diff --git a/src/main/java/org/apache/commons/imaging/formats/jpeg/JpegImageParser.java b/src/main/java/org/apache/commons/imaging/formats/jpeg/JpegImageParser.java index 49733d914..4e1fab8f7 100644 --- a/src/main/java/org/apache/commons/imaging/formats/jpeg/JpegImageParser.java +++ b/src/main/java/org/apache/commons/imaging/formats/jpeg/JpegImageParser.java @@ -123,7 +123,8 @@ private byte[] assembleSegments(final List segments, final boolean } } - final byte[] result = Allocator.byteArray(total); + // Trusted because length is based on length of existing arrays + final byte[] result = Allocator.byteArrayTrusted(total); int progress = 0; for (final App2Segment segment : segments) { @@ -459,7 +460,7 @@ public ImageInfo getImageInfo(final ByteSource byteSource, final JpegImagingPara final List commentSegments = readSegments(byteSource, new int[] { JpegConstants.COM_MARKER}, false); - final List comments = Allocator.arrayList(commentSegments.size()); + final List comments = Allocator.arrayListWithCapacityFor(commentSegments); for (final Segment commentSegment : commentSegments) { final ComSegment comSegment = (ComSegment) commentSegment; comments.add(new String(comSegment.getComment(), StandardCharsets.UTF_8)); diff --git a/src/main/java/org/apache/commons/imaging/formats/jpeg/decoder/JpegDecoder.java b/src/main/java/org/apache/commons/imaging/formats/jpeg/decoder/JpegDecoder.java index 30e33a414..189774307 100644 --- a/src/main/java/org/apache/commons/imaging/formats/jpeg/decoder/JpegDecoder.java +++ b/src/main/java/org/apache/commons/imaging/formats/jpeg/decoder/JpegDecoder.java @@ -109,7 +109,8 @@ static JpegInputStream[] splitByRstMarkers(final int[] scanPayload) { final List intervalStarts = getIntervalStartPositions(scanPayload); // get number of intervals in payload to init an array of appropriate length final int intervalCount = intervalStarts.size(); - final JpegInputStream[] streams = Allocator.array(intervalCount, JpegInputStream[]::new, JpegInputStream.SHALLOW_SIZE); + // Trusted because length is based on size of existing List + final JpegInputStream[] streams = Allocator.arrayTrusted(intervalCount, JpegInputStream[]::new, JpegInputStream.SHALLOW_SIZE); for (int i = 0; i < intervalCount; i++) { final int from = intervalStarts.get(i); int to; @@ -144,7 +145,7 @@ static JpegInputStream[] splitByRstMarkers(final int[] scanPayload) { private Block[] allocateMcuMemory() throws ImagingException { final Block[] mcu = Allocator.array(sosSegment.numberOfComponents, Block[]::new, Block.SHALLOW_SIZE); for (int i = 0; i < sosSegment.numberOfComponents; i++) { - final SosSegment.Component scanComponent = sosSegment.getComponents(i); + final SosSegment.Component scanComponent = sosSegment.getComponent(i); SofnSegment.Component frameComponent = null; for (int j = 0; j < sofnSegment.numberOfComponents; j++) { if (sofnSegment.getComponents(j).componentIdentifier == scanComponent.scanComponentSelector) { @@ -208,7 +209,7 @@ private int extend(int v, final int t) { private void readMcu(final JpegInputStream is, final int[] preds, final Block[] mcu) throws ImagingException { for (int i = 0; i < sosSegment.numberOfComponents; i++) { - final SosSegment.Component scanComponent = sosSegment.getComponents(i); + final SosSegment.Component scanComponent = sosSegment.getComponent(i); SofnSegment.Component frameComponent = null; for (int j = 0; j < sofnSegment.numberOfComponents; j++) { if (sofnSegment.getComponents(j).componentIdentifier == scanComponent.scanComponentSelector) { @@ -379,9 +380,9 @@ public boolean visitSegment(final int marker, final byte[] markerBytes, } quantizationTables[table.destinationIdentifier] = table; final int mSize = 64; - final int[] quantizationMatrixInt = Allocator.intArray(mSize); + final int[] quantizationMatrixInt = Allocator.intArrayTrusted(mSize); ZigZag.zigZagToBlock(table.getElements(), quantizationMatrixInt); - final float[] quantizationMatrixFloat = Allocator.floatArray(mSize); + final float[] quantizationMatrixFloat = Allocator.floatArrayTrusted(mSize); for (int j = 0; j < mSize; j++) { quantizationMatrixFloat[j] = quantizationMatrixInt[j]; } @@ -423,7 +424,8 @@ public void visitSos(final int marker, final byte[] markerBytes, final byte[] im // the payload contains the entropy-encoded segments (or ECS) divided by RST markers // or only one ECS if the entropy-encoded data is not divided by RST markers // length of payload = length of image data - length of data already read - final int[] scanPayload = Allocator.intArray(imageData.length - segmentLength); + // Trusted because length is based on length of existing array + final int[] scanPayload = Allocator.intArrayTrusted(imageData.length - segmentLength); int payloadReadCount = 0; while (payloadReadCount < scanPayload.length) { scanPayload[payloadReadCount] = is.read(); @@ -444,7 +446,8 @@ public void visitSos(final int marker, final byte[] markerBytes, final byte[] im final int xMCUs = (sofnSegment.width + hSize - 1) / hSize; final int yMCUs = (sofnSegment.height + vSize - 1) / vSize; final Block[] mcu = allocateMcuMemory(); - final Block[] scaledMCU = Allocator.array(mcu.length, Block[]::new, Block.SHALLOW_SIZE); + // Trusted because length is based on length of existing array + final Block[] scaledMCU = Allocator.arrayTrusted(mcu.length, Block[]::new, Block.SHALLOW_SIZE); Arrays.setAll(scaledMCU, i -> new Block(hSize, vSize)); final int[] preds = Allocator.intArray(sofnSegment.numberOfComponents); ColorModel colorModel; diff --git a/src/main/java/org/apache/commons/imaging/formats/jpeg/iptc/IptcParser.java b/src/main/java/org/apache/commons/imaging/formats/jpeg/iptc/IptcParser.java index a1a572f32..991ab9dd1 100644 --- a/src/main/java/org/apache/commons/imaging/formats/jpeg/iptc/IptcParser.java +++ b/src/main/java/org/apache/commons/imaging/formats/jpeg/iptc/IptcParser.java @@ -35,7 +35,6 @@ import java.util.Arrays; import java.util.Comparator; import java.util.List; -import java.util.Objects; import java.util.logging.Level; import java.util.logging.Logger; @@ -86,7 +85,8 @@ private Charset findCharset(final byte[] codedCharset) { } // check if encoding is a escape sequence // normalize encoding byte sequence - final byte[] codedCharsetNormalized = Allocator.byteArray(codedCharset.length); + // Trusted because length is based on length of existing array + final byte[] codedCharsetNormalized = Allocator.byteArrayTrusted(codedCharset.length); int j = 0; for (final byte element : codedCharset) { if (element != ' ') { @@ -94,7 +94,7 @@ private Charset findCharset(final byte[] codedCharset) { } } - if (Objects.deepEquals(codedCharsetNormalized, CHARACTER_ESCAPE_SEQUENCE)) { + if (Arrays.equals(codedCharsetNormalized, CHARACTER_ESCAPE_SEQUENCE)) { return StandardCharsets.UTF_8; } return DEFAULT_CHARSET; diff --git a/src/main/java/org/apache/commons/imaging/formats/jpeg/segments/SosSegment.java b/src/main/java/org/apache/commons/imaging/formats/jpeg/segments/SosSegment.java index 43c9382df..2be501006 100644 --- a/src/main/java/org/apache/commons/imaging/formats/jpeg/segments/SosSegment.java +++ b/src/main/java/org/apache/commons/imaging/formats/jpeg/segments/SosSegment.java @@ -21,6 +21,8 @@ import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; +import java.util.Collections; +import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; @@ -44,7 +46,7 @@ public Component(final int scanComponentSelector, final int dcCodingTableSelecto private static final Logger LOGGER = Logger.getLogger(SosSegment.class.getName()); public final int numberOfComponents; - private final Component[] components; + private final List components; public final int startOfSpectralSelection; public final int endOfSpectralSelection; public final int successiveApproximationBitHigh; @@ -69,7 +71,7 @@ public SosSegment(final int marker, final int markerLength, final InputStream is // Debug.debug("number_of_components_in_scan", // numberOfComponents); - components = Allocator.array(numberOfComponents, Component[]::new, Component.SHALLOW_SIZE); + List components = Allocator.arrayList(numberOfComponents); for (int i = 0; i < numberOfComponents; i++) { final int scanComponentSelector = readByte("scanComponentSelector", is, "Not a Valid JPEG File"); // Debug.debug("scanComponentSelector", scanComponentSelector); @@ -82,9 +84,10 @@ public SosSegment(final int marker, final int markerLength, final InputStream is final int dcCodingTableSelector = (acDcEntropyCodingTableSelector >> 4) & 0xf; final int acCodingTableSelector = acDcEntropyCodingTableSelector & 0xf; - components[i] = new Component(scanComponentSelector, - dcCodingTableSelector, acCodingTableSelector); + components.add(new Component(scanComponentSelector, + dcCodingTableSelector, acCodingTableSelector)); } + this.components = Collections.unmodifiableList(components); startOfSpectralSelection = readByte("startOfSpectralSelection", is, "Not a Valid JPEG File"); @@ -102,11 +105,11 @@ public SosSegment(final int marker, final int markerLength, final InputStream is } /** - * Returns a copy of all the components. + * Returns an unmodifiable list of all components. * @return all the components */ - public Component[] getComponents() { - return components.clone(); + public List getComponents() { + return Collections.unmodifiableList(components); } /** @@ -114,8 +117,8 @@ public Component[] getComponents() { * @param index the component index * @return the component */ - public Component getComponents(final int index) { - return components[index]; + public Component getComponent(final int index) { + return components.get(index); } @Override diff --git a/src/main/java/org/apache/commons/imaging/formats/pcx/PcxWriter.java b/src/main/java/org/apache/commons/imaging/formats/pcx/PcxWriter.java index 40c7550a6..9c1a9046f 100644 --- a/src/main/java/org/apache/commons/imaging/formats/pcx/PcxWriter.java +++ b/src/main/java/org/apache/commons/imaging/formats/pcx/PcxWriter.java @@ -266,7 +266,8 @@ private void writePixels(final BufferedImage src, final int bitDepth, final int private void writePixels32(final BufferedImage src, final int bytesPerLine, final BinaryOutputStream bos) throws IOException { - final int[] rgbs = Allocator.intArray(src.getWidth()); + // Trusted because length is based on size of existing image + final int[] rgbs = Allocator.intArrayTrusted(src.getWidth()); final byte[] plane = Allocator.byteArray(4 * bytesPerLine); for (int y = 0; y < src.getHeight(); y++) { src.getRGB(0, y, src.getWidth(), 1, rgbs, 0, src.getWidth()); diff --git a/src/main/java/org/apache/commons/imaging/formats/png/PngImageParser.java b/src/main/java/org/apache/commons/imaging/formats/png/PngImageParser.java index 96147ce76..6b5704667 100644 --- a/src/main/java/org/apache/commons/imaging/formats/png/PngImageParser.java +++ b/src/main/java/org/apache/commons/imaging/formats/png/PngImageParser.java @@ -330,7 +330,7 @@ public BufferedImage getBufferedImage(final ByteSource byteSource, final PngImag public List getChunkTypes(final InputStream is) throws ImagingException, IOException { final List chunks = readChunks(is, null, false); - final List chunkTypes = Allocator.arrayList(chunks.size()); + final List chunkTypes = Allocator.arrayListWithCapacityFor(chunks); for (final PngChunk chunk : chunks) { chunkTypes.add(getChunkTypeName(chunk.getChunkType())); } @@ -437,8 +437,9 @@ public ImageInfo getImageInfo(final ByteSource byteSource, final PngImagingParam final List iTXts = filterChunks(chunks, ChunkType.iTXt); final int chunkCount = tEXts.size() + zTXts.size() + iTXts.size(); - final List comments = Allocator.arrayList(chunkCount); - final List textChunks = Allocator.arrayList(chunkCount); + // Trusted because size is based on sizes of existing lists + final List comments = Allocator.arrayListTrusted(chunkCount); + final List textChunks = Allocator.arrayListTrusted(chunkCount); for (final PngChunk tEXt : tEXts) { final PngChunkText pngChunktEXt = (PngChunkText) tEXt; diff --git a/src/main/java/org/apache/commons/imaging/formats/png/ScanExpediter.java b/src/main/java/org/apache/commons/imaging/formats/png/ScanExpediter.java index 812509c18..53b4f6431 100644 --- a/src/main/java/org/apache/commons/imaging/formats/png/ScanExpediter.java +++ b/src/main/java/org/apache/commons/imaging/formats/png/ScanExpediter.java @@ -210,7 +210,8 @@ byte[] unfilterScanline(final FilterType filterType, final byte[] src, final byt final int bytesPerPixel) throws ImagingException, IOException { final ScanlineFilter filter = getScanlineFilter(filterType, bytesPerPixel); - final byte[] dst = Allocator.byteArray(src.length); + // Trusted because length is based on length of existing array + final byte[] dst = Allocator.byteArrayTrusted(src.length); filter.unfilter(src, dst, prev); return dst; } diff --git a/src/main/java/org/apache/commons/imaging/formats/png/chunks/PngChunkIccp.java b/src/main/java/org/apache/commons/imaging/formats/png/chunks/PngChunkIccp.java index 9800a26de..22b19ef0a 100644 --- a/src/main/java/org/apache/commons/imaging/formats/png/chunks/PngChunkIccp.java +++ b/src/main/java/org/apache/commons/imaging/formats/png/chunks/PngChunkIccp.java @@ -82,7 +82,8 @@ public PngChunkIccp(final int length, final int chunkType, final int crc, final compressionMethod = bytes[index + 1]; final int compressedProfileLength = bytes.length - (index + 1 + 1); - compressedProfile = Allocator.byteArray(compressedProfileLength); + // Trusted because length is based on length of existing array + compressedProfile = Allocator.byteArrayTrusted(compressedProfileLength); System.arraycopy(bytes, index + 1 + 1, compressedProfile, 0, compressedProfileLength); if (LOGGER.isLoggable(Level.FINEST)) { diff --git a/src/main/java/org/apache/commons/imaging/formats/png/chunks/PngChunkItxt.java b/src/main/java/org/apache/commons/imaging/formats/png/chunks/PngChunkItxt.java index 9bfd9c1c4..ccecd443a 100644 --- a/src/main/java/org/apache/commons/imaging/formats/png/chunks/PngChunkItxt.java +++ b/src/main/java/org/apache/commons/imaging/formats/png/chunks/PngChunkItxt.java @@ -77,7 +77,8 @@ public PngChunkItxt(final int length, final int chunkType, final int crc, final if (compressed) { final int compressedTextLength = bytes.length - index; - final byte[] compressedText = Allocator.byteArray(compressedTextLength); + // Trusted because length is based on length of existing array + final byte[] compressedText = Allocator.byteArrayTrusted(compressedTextLength); System.arraycopy(bytes, index, compressedText, 0, compressedTextLength); text = new String(IOUtils.toByteArray(new InflaterInputStream(new ByteArrayInputStream(compressedText))), StandardCharsets.UTF_8); diff --git a/src/main/java/org/apache/commons/imaging/formats/png/chunks/PngChunkZtxt.java b/src/main/java/org/apache/commons/imaging/formats/png/chunks/PngChunkZtxt.java index 68cdcb0f4..4a0300c13 100644 --- a/src/main/java/org/apache/commons/imaging/formats/png/chunks/PngChunkZtxt.java +++ b/src/main/java/org/apache/commons/imaging/formats/png/chunks/PngChunkZtxt.java @@ -46,7 +46,8 @@ public PngChunkZtxt(final int length, final int chunkType, final int crc, final } final int compressedTextLength = bytes.length - index; - final byte[] compressedText = Allocator.byteArray(compressedTextLength); + // Trusted because length is based on length of existing array + final byte[] compressedText = Allocator.byteArrayTrusted(compressedTextLength); System.arraycopy(bytes, index, compressedText, 0, compressedTextLength); text = new String(IOUtils.toByteArray(new InflaterInputStream(new ByteArrayInputStream(compressedText))), StandardCharsets.ISO_8859_1); diff --git a/src/main/java/org/apache/commons/imaging/formats/psd/datareaders/CompressedDataReader.java b/src/main/java/org/apache/commons/imaging/formats/psd/datareaders/CompressedDataReader.java index 0e2463839..9f2b7a240 100644 --- a/src/main/java/org/apache/commons/imaging/formats/psd/datareaders/CompressedDataReader.java +++ b/src/main/java/org/apache/commons/imaging/formats/psd/datareaders/CompressedDataReader.java @@ -61,7 +61,8 @@ public void readData(final InputStream is, final BufferedImage bi, final PsdImag final int depth = header.depth; final int channelCount = dataParser.getBasicChannelsCount(); - final int[][][] data = new int[Allocator.check(channelCount)][Allocator.check(height)][]; + Allocator.check(Math.multiplyExact(channelCount, height), Integer.BYTES); + final int[][][] data = new int[channelCount][height][]; // channels[0] = for (int channel = 0; channel < channelCount; channel++) { for (int y = 0; y < height; y++) { diff --git a/src/main/java/org/apache/commons/imaging/formats/psd/datareaders/UncompressedDataReader.java b/src/main/java/org/apache/commons/imaging/formats/psd/datareaders/UncompressedDataReader.java index 81044645f..3a93f4e82 100644 --- a/src/main/java/org/apache/commons/imaging/formats/psd/datareaders/UncompressedDataReader.java +++ b/src/main/java/org/apache/commons/imaging/formats/psd/datareaders/UncompressedDataReader.java @@ -51,8 +51,8 @@ public void readData(final InputStream is, final BufferedImage bi, final MyBitInputStream mbis = new MyBitInputStream(is, ByteOrder.BIG_ENDIAN, false); // we want all samples to be bytes try (BitsToByteInputStream bbis = new BitsToByteInputStream(mbis, 8)) { - final int[][][] data = new int[Allocator.check(channelCount)][Allocator - .check(height)][Allocator.check(width)]; + Allocator.check(Math.multiplyExact(channelCount, Math.multiplyExact(height, width)), Integer.BYTES); + final int[][][] data = new int[channelCount][height][width]; for (int channel = 0; channel < channelCount; channel++) { for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { diff --git a/src/main/java/org/apache/commons/imaging/formats/rgbe/RgbeInfo.java b/src/main/java/org/apache/commons/imaging/formats/rgbe/RgbeInfo.java index 118d4c814..89a7e73a9 100644 --- a/src/main/java/org/apache/commons/imaging/formats/rgbe/RgbeInfo.java +++ b/src/main/java/org/apache/commons/imaging/formats/rgbe/RgbeInfo.java @@ -108,7 +108,8 @@ public float[][] getPixelData() throws IOException, ImagingException { final byte[] scanLineBytes = ByteConversions.toBytes((short) wd, ByteOrder.BIG_ENDIAN); final byte[] rgbe = Allocator.byteArray(wd * 4); - final float[][] out = new float[3][Allocator.check(wd * ht)]; + Allocator.check(Math.multiplyExact(3, Math.multiplyExact(wd, ht)), Float.BYTES); + final float[][] out = new float[3][wd * ht]; for (int i = 0; i < ht; i++) { BinaryFunctions.readAndVerifyBytes(in, TWO_TWO, "Scan line " + i + " expected to start with 0x2 0x2"); diff --git a/src/main/java/org/apache/commons/imaging/formats/tiff/TiffDirectory.java b/src/main/java/org/apache/commons/imaging/formats/tiff/TiffDirectory.java index af7a350bb..6ff77f20a 100644 --- a/src/main/java/org/apache/commons/imaging/formats/tiff/TiffDirectory.java +++ b/src/main/java/org/apache/commons/imaging/formats/tiff/TiffDirectory.java @@ -19,7 +19,6 @@ import java.awt.image.BufferedImage; import java.io.IOException; import java.nio.ByteOrder; -import java.util.ArrayList; import java.util.Collections; import java.util.Iterator; import java.util.List; @@ -172,8 +171,11 @@ public ByteOrder getByteOrder() { return headerByteOrder; } + /** + * Returns an unmodifiable list of all directory entries. + */ public List getDirectoryEntries() { - return new ArrayList<>(entries); + return Collections.unmodifiableList(entries); } @Override @@ -733,7 +735,7 @@ private List getRawImageDataElements( + ") != byteCounts.length(" + byteCounts.length + ")"); } - final List result = Allocator.arrayList(offsets.length); + final List result = Allocator.arrayListTrusted(offsets.length); for (int i = 0; i < offsets.length; i++) { result.add(new ImageDataElement(offsets[i], byteCounts[i])); } diff --git a/src/main/java/org/apache/commons/imaging/formats/tiff/TiffField.java b/src/main/java/org/apache/commons/imaging/formats/tiff/TiffField.java index 43183a507..0a37e5cca 100644 --- a/src/main/java/org/apache/commons/imaging/formats/tiff/TiffField.java +++ b/src/main/java/org/apache/commons/imaging/formats/tiff/TiffField.java @@ -156,25 +156,29 @@ public double[] getDoubleArrayValue() throws ImagingException { } if (o instanceof Number[]) { final Number[] numbers = (Number[]) o; - final double[] result = Allocator.doubleArray(numbers.length); + // Trusted because length is based on length of existing array + final double[] result = Allocator.doubleArrayTrusted(numbers.length); Arrays.setAll(result, i -> numbers[i].doubleValue()); return result; } if (o instanceof short[]) { final short[] numbers = (short[]) o; - final double[] result = Allocator.doubleArray(numbers.length); + // Trusted because length is based on length of existing array + final double[] result = Allocator.doubleArrayTrusted(numbers.length); Arrays.setAll(result, i -> numbers[i]); return result; } if (o instanceof int[]) { final int[] numbers = (int[]) o; - final double[] result = Allocator.doubleArray(numbers.length); + // Trusted because length is based on length of existing array + final double[] result = Allocator.doubleArrayTrusted(numbers.length); Arrays.setAll(result, i -> numbers[i]); return result; } if (o instanceof float[]) { final float[] numbers = (float[]) o; - final double[] result = Allocator.doubleArray(numbers.length); + // Trusted because length is based on length of existing array + final double[] result = Allocator.doubleArrayTrusted(numbers.length); Arrays.setAll(result, i -> numbers[i]); return result; } @@ -220,13 +224,15 @@ public int[] getIntArrayValue() throws ImagingException { } if (o instanceof Number[]) { final Number[] numbers = (Number[]) o; - final int[] result = Allocator.intArray(numbers.length); + // Trusted because length is based on length of existing array + final int[] result = Allocator.intArrayTrusted(numbers.length); Arrays.setAll(result, i -> numbers[i].intValue()); return result; } if (o instanceof short[]) { final short[] numbers = (short[]) o; - final int[] result = Allocator.intArray(numbers.length); + // Trusted because length is based on length of existing array + final int[] result = Allocator.intArrayTrusted(numbers.length); Arrays.setAll(result, i -> 0xffff & numbers[i]); return result; } diff --git a/src/main/java/org/apache/commons/imaging/formats/tiff/TiffImageParser.java b/src/main/java/org/apache/commons/imaging/formats/tiff/TiffImageParser.java index 17ed6eca2..d05efc581 100644 --- a/src/main/java/org/apache/commons/imaging/formats/tiff/TiffImageParser.java +++ b/src/main/java/org/apache/commons/imaging/formats/tiff/TiffImageParser.java @@ -508,7 +508,8 @@ public ImageInfo getImageInfo(final ByteSource byteSource, final TiffImagingPara final int bitsPerPixel = bitsPerSample; // assume grayscale; // dunno if this handles colormapped images correctly. - final List comments = Allocator.arrayList(directory.size()); + // Trusted because `TiffDirectory.size()` reports size of existing List + final List comments = Allocator.arrayListTrusted(directory.size()); for (final TiffField field : directory) { final String comment = field.toString(); comments.add(comment); diff --git a/src/main/java/org/apache/commons/imaging/formats/tiff/TiffRasterDataInt.java b/src/main/java/org/apache/commons/imaging/formats/tiff/TiffRasterDataInt.java index 213bb968a..04a31c093 100644 --- a/src/main/java/org/apache/commons/imaging/formats/tiff/TiffRasterDataInt.java +++ b/src/main/java/org/apache/commons/imaging/formats/tiff/TiffRasterDataInt.java @@ -113,7 +113,8 @@ public TiffRasterDataInt(final int width, final int height, final int[] data) { */ @Override public float[] getData() { - final float[] result = Allocator.floatArray(nCells); + // Trusted because length is based on length of existing `data` array + final float[] result = Allocator.floatArrayTrusted(nCells); for (int i = 0; i < nCells; i++) { result[i] = data[i]; } diff --git a/src/main/java/org/apache/commons/imaging/formats/tiff/fieldtypes/FieldTypeAscii.java b/src/main/java/org/apache/commons/imaging/formats/tiff/fieldtypes/FieldTypeAscii.java index 4efdf7260..d3a155ae1 100644 --- a/src/main/java/org/apache/commons/imaging/formats/tiff/fieldtypes/FieldTypeAscii.java +++ b/src/main/java/org/apache/commons/imaging/formats/tiff/fieldtypes/FieldTypeAscii.java @@ -40,7 +40,8 @@ public Object getValue(final TiffField entry) { nullCount++; } } - final String[] strings = Allocator.array(nullCount, String[]::new, 24); + // Trusted because length is based on length of existing array + final String[] strings = Allocator.arrayTrusted(nullCount, String[]::new, 24); int stringsAdded = 0; strings[0] = ""; // if we have a 0 length string int nextStringPos = 0; @@ -85,15 +86,18 @@ public byte[] writeData(final Object o, final ByteOrder byteOrder) throws Imagin throw new ImagingException("Unknown data type: " + o); } final String[] strings = (String[]) o; + final byte[][] stringBytes = new byte[strings.length][]; int totalLength = 0; - for (final String string : strings) { + for (int i = 0; i < strings.length; i++) { + final String string = strings[i]; final byte[] bytes = string.getBytes(StandardCharsets.UTF_8); totalLength += (bytes.length + 1); + stringBytes[i] = bytes; } - final byte[] result = Allocator.byteArray(totalLength); + // Trusted because length is based on length of existing arrays + final byte[] result = Allocator.byteArrayTrusted(totalLength); int position = 0; - for (final String string : strings) { - final byte[] bytes = string.getBytes(StandardCharsets.UTF_8); + for (final byte[] bytes : stringBytes) { System.arraycopy(bytes, 0, result, position, bytes.length); position += (bytes.length + 1); } diff --git a/src/main/java/org/apache/commons/imaging/formats/tiff/fieldtypes/FieldTypeDouble.java b/src/main/java/org/apache/commons/imaging/formats/tiff/fieldtypes/FieldTypeDouble.java index be46f2410..7359aef8b 100644 --- a/src/main/java/org/apache/commons/imaging/formats/tiff/fieldtypes/FieldTypeDouble.java +++ b/src/main/java/org/apache/commons/imaging/formats/tiff/fieldtypes/FieldTypeDouble.java @@ -49,7 +49,8 @@ public byte[] writeData(final Object o, final ByteOrder byteOrder) throws Imagin if (!(o instanceof Double[])) { throw new ImagingException("Invalid data", o); } - final double[] values = Allocator.doubleArray(((Double[]) o).length); + // Trusted because length is based on length of existing array + final double[] values = Allocator.doubleArrayTrusted(((Double[]) o).length); Arrays.setAll(values, i -> ((Double[]) o)[i].doubleValue()); return ByteConversions.toBytes(values, byteOrder); } diff --git a/src/main/java/org/apache/commons/imaging/formats/tiff/fieldtypes/FieldTypeFloat.java b/src/main/java/org/apache/commons/imaging/formats/tiff/fieldtypes/FieldTypeFloat.java index e85556437..e66898339 100644 --- a/src/main/java/org/apache/commons/imaging/formats/tiff/fieldtypes/FieldTypeFloat.java +++ b/src/main/java/org/apache/commons/imaging/formats/tiff/fieldtypes/FieldTypeFloat.java @@ -50,7 +50,8 @@ public byte[] writeData(final Object o, final ByteOrder byteOrder) throws Imagin throw new ImagingException("Invalid data", o); } final Float[] numbers = (Float[]) o; - final float[] values = Allocator.floatArray(numbers.length); + // Trusted because length is based on length of existing array + final float[] values = Allocator.floatArrayTrusted(numbers.length); for (int i = 0; i < values.length; i++) { values[i] = numbers[i].floatValue(); } diff --git a/src/main/java/org/apache/commons/imaging/formats/tiff/fieldtypes/FieldTypeLong.java b/src/main/java/org/apache/commons/imaging/formats/tiff/fieldtypes/FieldTypeLong.java index f9e920c6d..d36889cda 100644 --- a/src/main/java/org/apache/commons/imaging/formats/tiff/fieldtypes/FieldTypeLong.java +++ b/src/main/java/org/apache/commons/imaging/formats/tiff/fieldtypes/FieldTypeLong.java @@ -51,7 +51,8 @@ public byte[] writeData(final Object o, final ByteOrder byteOrder) throws Imagin throw new ImagingException("Invalid data", o); } final Integer[] numbers = (Integer[]) o; - final int[] values = Allocator.intArray(numbers.length); + // Trusted because length is based on length of existing array + final int[] values = Allocator.intArrayTrusted(numbers.length); for (int i = 0; i < values.length; i++) { values[i] = numbers[i]; } diff --git a/src/main/java/org/apache/commons/imaging/formats/tiff/fieldtypes/FieldTypeRational.java b/src/main/java/org/apache/commons/imaging/formats/tiff/fieldtypes/FieldTypeRational.java index bb59ab190..68a2f636f 100644 --- a/src/main/java/org/apache/commons/imaging/formats/tiff/fieldtypes/FieldTypeRational.java +++ b/src/main/java/org/apache/commons/imaging/formats/tiff/fieldtypes/FieldTypeRational.java @@ -57,7 +57,8 @@ public byte[] writeData(final Object o, final ByteOrder byteOrder) throws Imagin } if (o instanceof Number[]) { final Number[] numbers = (Number[]) o; - final RationalNumber[] rationalNumbers = Allocator.array(numbers.length, RationalNumber[]::new, + // Trusted because length is based on length of existing array + final RationalNumber[] rationalNumbers = Allocator.arrayTrusted(numbers.length, RationalNumber[]::new, RationalNumber.SHALLOW_SIZE); Arrays.setAll(rationalNumbers, RationalNumber::valueOf); return ByteConversions.toBytes(rationalNumbers, byteOrder); @@ -66,7 +67,8 @@ public byte[] writeData(final Object o, final ByteOrder byteOrder) throws Imagin throw new ImagingException("Invalid data", o); } final double[] numbers = (double[]) o; - final RationalNumber[] rationalNumbers = Allocator.array(numbers.length, RationalNumber[]::new, + // Trusted because length is based on length of existing array + final RationalNumber[] rationalNumbers = Allocator.arrayTrusted(numbers.length, RationalNumber[]::new, RationalNumber.SHALLOW_SIZE); Arrays.setAll(rationalNumbers, RationalNumber::valueOf); return ByteConversions.toBytes(rationalNumbers, byteOrder); diff --git a/src/main/java/org/apache/commons/imaging/formats/tiff/fieldtypes/FieldTypeShort.java b/src/main/java/org/apache/commons/imaging/formats/tiff/fieldtypes/FieldTypeShort.java index 608663d59..1b0d01a3d 100644 --- a/src/main/java/org/apache/commons/imaging/formats/tiff/fieldtypes/FieldTypeShort.java +++ b/src/main/java/org/apache/commons/imaging/formats/tiff/fieldtypes/FieldTypeShort.java @@ -50,7 +50,8 @@ public byte[] writeData(final Object o, final ByteOrder byteOrder) throws Imagin throw new ImagingException("Invalid data", o); } final Short[] numbers = (Short[]) o; - final short[] values = Allocator.shortArray(numbers.length); + // Trusted because length is based on length of existing array + final short[] values = Allocator.shortArrayTrusted(numbers.length); for (int i = 0; i < values.length; i++) { values[i] = numbers[i].shortValue(); } diff --git a/src/main/java/org/apache/commons/imaging/formats/tiff/taginfos/TagInfoAscii.java b/src/main/java/org/apache/commons/imaging/formats/tiff/taginfos/TagInfoAscii.java index ce7c8945d..84ef0a878 100644 --- a/src/main/java/org/apache/commons/imaging/formats/tiff/taginfos/TagInfoAscii.java +++ b/src/main/java/org/apache/commons/imaging/formats/tiff/taginfos/TagInfoAscii.java @@ -42,7 +42,8 @@ public String[] getValue(final ByteOrder byteOrder, final byte[] bytes) { nullCount++; } } - final String[] strings = Allocator.array(nullCount + 1, String[]::new, 24); + // Trusted because length is based on length of existing array + final String[] strings = Allocator.arrayTrusted(nullCount + 1, String[]::new, 24); int stringsAdded = 0; strings[0] = ""; // if we have a 0 length string int nextStringPos = 0; diff --git a/src/main/java/org/apache/commons/imaging/formats/tiff/taginfos/TagInfoGpsText.java b/src/main/java/org/apache/commons/imaging/formats/tiff/taginfos/TagInfoGpsText.java index e383c9dca..54bc4d973 100644 --- a/src/main/java/org/apache/commons/imaging/formats/tiff/taginfos/TagInfoGpsText.java +++ b/src/main/java/org/apache/commons/imaging/formats/tiff/taginfos/TagInfoGpsText.java @@ -90,7 +90,8 @@ public byte[] encodeValue(final FieldType fieldType, final Object value, final B final String decodedAscii = new String(asciiBytes, TEXT_ENCODING_ASCII.encodingName); if (decodedAscii.equals(s)) { // no unicode/non-ascii values. - final byte[] result = Allocator.byteArray(asciiBytes.length + TEXT_ENCODING_ASCII.prefix.length); + // Trusted because length is based on length of existing arrays + final byte[] result = Allocator.byteArrayTrusted(asciiBytes.length + TEXT_ENCODING_ASCII.prefix.length); System.arraycopy(TEXT_ENCODING_ASCII.prefix, 0, result, 0, TEXT_ENCODING_ASCII.prefix.length); System.arraycopy(asciiBytes, 0, result, TEXT_ENCODING_ASCII.prefix.length, asciiBytes.length); return result; @@ -103,7 +104,8 @@ public byte[] encodeValue(final FieldType fieldType, final Object value, final B encoding = TEXT_ENCODING_UNICODE_LE; } final byte[] unicodeBytes = s.getBytes(encoding.encodingName); - final byte[] result = Allocator.byteArray(unicodeBytes.length + encoding.prefix.length); + // Trusted because length is based on length of existing arrays + final byte[] result = Allocator.byteArrayTrusted(unicodeBytes.length + encoding.prefix.length); System.arraycopy(encoding.prefix, 0, result, 0, encoding.prefix.length); System.arraycopy(unicodeBytes, 0, result, encoding.prefix.length, unicodeBytes.length); return result; diff --git a/src/main/java/org/apache/commons/imaging/formats/tiff/write/ImageDataOffsets.java b/src/main/java/org/apache/commons/imaging/formats/tiff/write/ImageDataOffsets.java index c227bd136..00cfccd28 100644 --- a/src/main/java/org/apache/commons/imaging/formats/tiff/write/ImageDataOffsets.java +++ b/src/main/java/org/apache/commons/imaging/formats/tiff/write/ImageDataOffsets.java @@ -30,7 +30,8 @@ class ImageDataOffsets { this.imageDataOffsets = imageDataOffsets; this.imageDataOffsetsField = imageDataOffsetsField; - outputItems = Allocator.array(imageData.length, TiffOutputItem[]::new, TiffOutputItem.Value.SHALLOW_SIZE); + // Trusted because length is based on length of existing array + outputItems = Allocator.arrayTrusted(imageData.length, TiffOutputItem[]::new, TiffOutputItem.Value.SHALLOW_SIZE); Arrays.setAll(outputItems, i -> new TiffOutputItem.Value("TIFF image data", imageData[i].getData())); } diff --git a/src/main/java/org/apache/commons/imaging/formats/tiff/write/TiffOutputDirectory.java b/src/main/java/org/apache/commons/imaging/formats/tiff/write/TiffOutputDirectory.java index b72f796ab..7dccadac2 100644 --- a/src/main/java/org/apache/commons/imaging/formats/tiff/write/TiffOutputDirectory.java +++ b/src/main/java/org/apache/commons/imaging/formats/tiff/write/TiffOutputDirectory.java @@ -635,8 +635,9 @@ protected List getOutputItems( // TiffOutputField imageDataOffsetsField = null; - final int[] imageDataOffsets = Allocator.intArray(imageData.length); - final int[] imageDataByteCounts = Allocator.intArray(imageData.length); + // Trusted because length is based on length of existing array + final int[] imageDataOffsets = Allocator.intArrayTrusted(imageData.length); + final int[] imageDataByteCounts = Allocator.intArrayTrusted(imageData.length); Arrays.setAll(imageDataByteCounts, i -> imageData[i].length); // Append imageData-related fields to first directory diff --git a/src/main/java/org/apache/commons/imaging/formats/xbm/XbmImageParser.java b/src/main/java/org/apache/commons/imaging/formats/xbm/XbmImageParser.java index 7f50dbdf5..32279caaf 100644 --- a/src/main/java/org/apache/commons/imaging/formats/xbm/XbmImageParser.java +++ b/src/main/java/org/apache/commons/imaging/formats/xbm/XbmImageParser.java @@ -42,9 +42,9 @@ import org.apache.commons.imaging.ImageParser; import org.apache.commons.imaging.ImagingException; import org.apache.commons.imaging.bytesource.ByteSource; -import org.apache.commons.imaging.common.Allocator; import org.apache.commons.imaging.common.BasicCParser; import org.apache.commons.imaging.common.ImageMetadata; +import org.apache.commons.imaging.common.KnownSizeByteArrayBuilder; public class XbmImageParser extends ImageParser { @@ -297,8 +297,8 @@ private BufferedImage readXbmImage(final XbmHeader xbmHeader, final BasicCParser } final int rowLength = (xbmHeader.width + 7) / 8; - final byte[] imageData = Allocator.byteArray(rowLength * xbmHeader.height); - int i = 0; + final int expectedImageDataSize = rowLength * xbmHeader.height; + final KnownSizeByteArrayBuilder imageDataBuilder = new KnownSizeByteArrayBuilder(expectedImageDataSize); for (int y = 0; y < xbmHeader.height; y++) { for (int x = 0; x < xbmHeader.width; x += inputWidth) { token = cParser.nextToken(); @@ -313,12 +313,12 @@ private BufferedImage readXbmImage(final XbmHeader xbmHeader, final BasicCParser final int value = Integer.parseInt(token.substring(2), 16); final int flipped = Integer.reverse(value) >>> (32 - inputWidth); if (inputWidth == 16) { - imageData[i++] = (byte) (flipped >>> 8); + imageDataBuilder.addByte((byte) (flipped >>> 8)); if ((x + 8) < xbmHeader.width) { - imageData[i++] = (byte) flipped; + imageDataBuilder.addByte((byte) flipped); } } else { - imageData[i++] = (byte) flipped; + imageDataBuilder.addByte((byte) flipped); } token = cParser.nextToken(); @@ -327,7 +327,7 @@ private BufferedImage readXbmImage(final XbmHeader xbmHeader, final BasicCParser + "premature end of file"); } if (!",".equals(token) - && ((i < imageData.length) || !"}".equals(token))) { + && ((imageDataBuilder.getAddedBytesCount() < expectedImageDataSize) || !"}".equals(token))) { throw new ImagingException("Parsing XBM file failed, " + "punctuation error"); } @@ -336,6 +336,7 @@ private BufferedImage readXbmImage(final XbmHeader xbmHeader, final BasicCParser final int[] palette = { 0xffffff, 0x000000 }; final ColorModel colorModel = new IndexColorModel(1, 2, palette, 0, false, -1, DataBuffer.TYPE_BYTE); + final byte[] imageData = imageDataBuilder.createByteArray(); final DataBufferByte dataBuffer = new DataBufferByte(imageData, imageData.length); final WritableRaster raster = Raster.createPackedRaster(dataBuffer, xbmHeader.width, xbmHeader.height, 1, null); diff --git a/src/main/java/org/apache/commons/imaging/formats/xpm/XpmImageParser.java b/src/main/java/org/apache/commons/imaging/formats/xpm/XpmImageParser.java index 00ffb197a..472dacfc2 100644 --- a/src/main/java/org/apache/commons/imaging/formats/xpm/XpmImageParser.java +++ b/src/main/java/org/apache/commons/imaging/formats/xpm/XpmImageParser.java @@ -507,7 +507,8 @@ private BufferedImage readXpmImage(final XpmHeader xpmHeader, final BasicCParser WritableRaster raster; int bpp; if (xpmHeader.palette.size() <= (1 << 8)) { - final int[] palette = Allocator.intArray(xpmHeader.palette.size()); + // Trusted because length is based on size of existing Map + final int[] palette = Allocator.intArrayTrusted(xpmHeader.palette.size()); for (final Entry entry : xpmHeader.palette.entrySet()) { final PaletteEntry paletteEntry = entry.getValue(); palette[paletteEntry.index] = paletteEntry.getBestArgb(); @@ -519,11 +520,12 @@ private BufferedImage readXpmImage(final XpmHeader xpmHeader, final BasicCParser int pixelStride = bands; int size = scanlineStride * (xpmHeader.height - 1) + // first (h - 1) scans pixelStride * xpmHeader.width; // last scan - Allocator.check(Byte.SIZE, size); + Allocator.check(size, Byte.SIZE); raster = Raster.createInterleavedRaster(DataBuffer.TYPE_BYTE, xpmHeader.width, xpmHeader.height, bands, null); bpp = 8; } else if (xpmHeader.palette.size() <= (1 << 16)) { - final int[] palette = Allocator.intArray(xpmHeader.palette.size()); + // Trusted because length is based on size of existing Map + final int[] palette = Allocator.intArrayTrusted(xpmHeader.palette.size()); for (final Entry entry : xpmHeader.palette.entrySet()) { final PaletteEntry paletteEntry = entry.getValue(); palette[paletteEntry.index] = paletteEntry.getBestArgb(); @@ -535,12 +537,12 @@ private BufferedImage readXpmImage(final XpmHeader xpmHeader, final BasicCParser int pixelStride = bands; int size = scanlineStride * (xpmHeader.height - 1) + // first (h - 1) scans pixelStride * xpmHeader.width; // last scan - Allocator.check(Short.SIZE, size); + Allocator.check(size, Short.SIZE); raster = Raster.createInterleavedRaster(DataBuffer.TYPE_USHORT, xpmHeader.width, xpmHeader.height, bands, null); bpp = 16; } else { colorModel = new DirectColorModel(32, 0x00ff0000, 0x0000ff00, 0x000000ff, 0xff000000); - Allocator.check(Integer.SIZE, xpmHeader.width * xpmHeader.height); + Allocator.check(Math.multiplyExact(xpmHeader.width, xpmHeader.height), Integer.SIZE); raster = Raster.createPackedRaster(DataBuffer.TYPE_INT, xpmHeader.width, xpmHeader.height, new int[] { 0x00ff0000, 0x0000ff00, 0x000000ff, 0xff000000 }, null); bpp = 32; @@ -587,7 +589,8 @@ private BufferedImage readXpmImage(final XpmHeader xpmHeader, final BasicCParser private String toColor(final int color) { final String hex = Integer.toHexString(color); if (hex.length() < 6) { - final char[] zeroes = Allocator.charArray(6 - hex.length()); + // Trusted because length is based on length of existing String + final char[] zeroes = Allocator.charArrayTrusted(6 - hex.length()); Arrays.fill(zeroes, '0'); return "#" + new String(zeroes) + hex; } diff --git a/src/main/java/org/apache/commons/imaging/icc/IccProfileParser.java b/src/main/java/org/apache/commons/imaging/icc/IccProfileParser.java index 7bdd53aa2..a743d6938 100644 --- a/src/main/java/org/apache/commons/imaging/icc/IccProfileParser.java +++ b/src/main/java/org/apache/commons/imaging/icc/IccProfileParser.java @@ -25,6 +25,7 @@ import java.io.IOException; import java.io.InputStream; import java.nio.ByteOrder; +import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; @@ -241,8 +242,7 @@ private IccProfileInfo readIccProfileInfo(InputStream is) throws IOException { final int tagCount = read4Bytes("TagCount", is, "Not a Valid ICC Profile", getByteOrder()); - // List tags = new ArrayList(); - final IccTag[] tags = Allocator.array(tagCount, IccTag[]::new, IccTag.SHALLOW_SIZE); + final List tags = Allocator.arrayList(tagCount); for (int i = 0; i < tagCount; i++) { final int tagSignature = read4Bytes("TagSignature[" + i + "]", is, "Not a Valid ICC Profile", @@ -270,8 +270,7 @@ private IccProfileInfo readIccProfileInfo(InputStream is) throws IOException { final IccTag tag = new IccTag(tagSignature, offsetToData, elementSize, fIccTagType); // tag.dump("\t" + i + ": "); - tags[i] = tag; - // tags .add(tag); + tags.add(tag); } // read stream to end, filling cache. @@ -286,7 +285,7 @@ private IccProfileInfo readIccProfileInfo(InputStream is) throws IOException { final IccProfileInfo result = new IccProfileInfo(data, profileSize, cmmTypeSignature, profileVersion, profileDeviceClassSignature, colorSpace, profileConnectionSpace, profileFileSignature, primaryPlatformSignature, variousFlags, deviceManufacturer, deviceModel, renderingIntent, - profileCreatorSignature, null, tags); + profileCreatorSignature, null, tags.toArray(new IccTag[0])); if (LOGGER.isLoggable(Level.FINEST)) { LOGGER.finest("issRGB: " + result.isSrgb()); diff --git a/src/main/java/org/apache/commons/imaging/mylzw/MyLzwCompressor.java b/src/main/java/org/apache/commons/imaging/mylzw/MyLzwCompressor.java index 3961804de..5dc01c00d 100644 --- a/src/main/java/org/apache/commons/imaging/mylzw/MyLzwCompressor.java +++ b/src/main/java/org/apache/commons/imaging/mylzw/MyLzwCompressor.java @@ -176,7 +176,7 @@ private int codeFromString(final byte[] bytes, final int start, final int length } public byte[] compress(final byte[] bytes) throws IOException { - try (ByteArrayOutputStream baos = new ByteArrayOutputStream(Allocator.checkByteArray(bytes.length)); + try (ByteArrayOutputStream baos = new ByteArrayOutputStream(Allocator.checkByteArrayTrusted(bytes.length)); MyBitOutputStream bos = new MyBitOutputStream(baos, byteOrder)) { initializeStringTable(); diff --git a/src/main/java/org/apache/commons/imaging/mylzw/MyLzwDecompressor.java b/src/main/java/org/apache/commons/imaging/mylzw/MyLzwDecompressor.java index 64530f612..60670e71b 100644 --- a/src/main/java/org/apache/commons/imaging/mylzw/MyLzwDecompressor.java +++ b/src/main/java/org/apache/commons/imaging/mylzw/MyLzwDecompressor.java @@ -16,7 +16,6 @@ */ package org.apache.commons.imaging.mylzw; -import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -24,7 +23,7 @@ import java.util.Arrays; import org.apache.commons.imaging.ImagingException; -import org.apache.commons.imaging.common.Allocator; +import org.apache.commons.imaging.common.KnownSizeByteArrayBuilder; public final class MyLzwDecompressor { @@ -106,8 +105,11 @@ private void clearTable() { public byte[] decompress(final InputStream is, final int expectedLength) throws IOException { int code; int oldCode = -1; - try (MyBitInputStream mbis = new MyBitInputStream(is, byteOrder, tiffLZWMode); - ByteArrayOutputStream baos = new ByteArrayOutputStream(Allocator.checkByteArray(expectedLength))) { + + KnownSizeByteArrayBuilder byteArrayBuilder = new KnownSizeByteArrayBuilder(expectedLength); + OutputStream out = byteArrayBuilder.asOutputStream(); + + try (MyBitInputStream mbis = new MyBitInputStream(is, byteOrder, tiffLZWMode)) { clearTable(); @@ -123,14 +125,14 @@ public byte[] decompress(final InputStream is, final int expectedLength) throws if (code == eoiCode) { break; } - writeToResult(baos, stringFromCode(code)); + writeToResult(out, stringFromCode(code)); } else if (isInTable(code)) { - writeToResult(baos, stringFromCode(code)); + writeToResult(out, stringFromCode(code)); addStringToTable(appendBytes(stringFromCode(oldCode), firstChar(stringFromCode(code)))); } else { final byte[] outString = appendBytes(stringFromCode(oldCode), firstChar(stringFromCode(oldCode))); - writeToResult(baos, outString); + writeToResult(out, outString); addStringToTable(outString); } oldCode = code; @@ -140,7 +142,8 @@ public byte[] decompress(final InputStream is, final int expectedLength) throws } } - return baos.toByteArray(); + // Byte array may be smaller than originally requested length + return byteArrayBuilder.createByteArray(true); } } diff --git a/src/main/java/org/apache/commons/imaging/palette/MedianCutQuantizer.java b/src/main/java/org/apache/commons/imaging/palette/MedianCutQuantizer.java index 868188ba8..0844f6866 100644 --- a/src/main/java/org/apache/commons/imaging/palette/MedianCutQuantizer.java +++ b/src/main/java/org/apache/commons/imaging/palette/MedianCutQuantizer.java @@ -57,7 +57,8 @@ private Map groupColors1(final BufferedImage image, final i final int width = image.getWidth(); final int height = image.getHeight(); - final int[] row = Allocator.intArray(width); + // Trusted because length is based on width of existing image + final int[] row = Allocator.intArrayTrusted(width); for (int y = 0; y < height; y++) { image.getRGB(0, y, width, 1, row, 0, width); for (int x = 0; x < width; x++) { @@ -92,7 +93,8 @@ public Palette process(final BufferedImage image, final int maxColors, if (discreteColors <= maxColors) { Debug.debug("lossless palette: " + discreteColors); - final int[] palette = Allocator.intArray(discreteColors); + // Trusted because length is based on size of existing Map + final int[] palette = Allocator.intArrayTrusted(discreteColors); final List colorCounts = new ArrayList<>( colorMap.values()); diff --git a/src/main/java/org/apache/commons/imaging/palette/PaletteFactory.java b/src/main/java/org/apache/commons/imaging/palette/PaletteFactory.java index d8b63ea93..69c7ccf7d 100644 --- a/src/main/java/org/apache/commons/imaging/palette/PaletteFactory.java +++ b/src/main/java/org/apache/commons/imaging/palette/PaletteFactory.java @@ -337,7 +337,7 @@ public boolean isGrayscale(final BufferedImage src) { public Palette makeExactRgbPaletteFancy(final BufferedImage src) { // map what rgb values have been used - final byte[] rgbmap = Allocator.byteArray(256 * 256 * 32); + final byte[] rgbmap = Allocator.byteArrayTrusted(256 * 256 * 32); final int width = src.getWidth(); final int height = src.getHeight(); @@ -408,7 +408,8 @@ public SimplePalette makeExactRgbPaletteSimple(final BufferedImage src, final in } } - final int[] result = Allocator.intArray(rgbs.size()); + // Trusted because length is based on size of existing Set + final int[] result = Allocator.intArrayTrusted(rgbs.size()); int next = 0; for (final int rgb : rgbs) { result[next++] = rgb; diff --git a/src/test/java/org/apache/commons/imaging/common/BinaryFunctionsTest.java b/src/test/java/org/apache/commons/imaging/common/BinaryFunctionsTest.java new file mode 100644 index 000000000..f3c0be90f --- /dev/null +++ b/src/test/java/org/apache/commons/imaging/common/BinaryFunctionsTest.java @@ -0,0 +1,36 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.commons.imaging.common; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class BinaryFunctionsTest { + @ParameterizedTest + @ValueSource(ints = {Integer.MIN_VALUE, -1, 6, Integer.MAX_VALUE}) + void testSlice_Invalid(int length) { + IllegalArgumentException e = assertThrows(IllegalArgumentException.class, () -> BinaryFunctions.slice(new byte[5], -4, length)); + assertEquals("Invalid start or count", e.getMessage()); + + e = assertThrows(IllegalArgumentException.class, () -> BinaryFunctions.slice(new byte[5], 0, length)); + assertEquals("Invalid start or count", e.getMessage()); + } +} diff --git a/src/test/java/org/apache/commons/imaging/common/KnownSizeByteArrayBuilderTest.java b/src/test/java/org/apache/commons/imaging/common/KnownSizeByteArrayBuilderTest.java new file mode 100644 index 000000000..06f07c192 --- /dev/null +++ b/src/test/java/org/apache/commons/imaging/common/KnownSizeByteArrayBuilderTest.java @@ -0,0 +1,233 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.commons.imaging.common; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.io.ByteArrayInputStream; +import java.io.EOFException; +import java.io.IOException; +import java.io.OutputStream; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class KnownSizeByteArrayBuilderTest { + private static byte[] getTestBytes(int length) { + byte[] bytes = new byte[length]; + + for (int i = 0; i < bytes.length; i++) { + // Uses a number > 127 and <= 255 to cover positive and negative byte values + // Number is a prime number to avoid matching internal section sizes of builder + bytes[i] = (byte) (i % 251); + } + + return bytes; + } + + @Test + void testEmpty() { + KnownSizeByteArrayBuilder builder = new KnownSizeByteArrayBuilder(0); + assertArrayEquals(new byte[0], builder.createByteArray()); + } + + @Test + void testInvalidExpected() { + IllegalArgumentException e = assertThrows(IllegalArgumentException.class, () -> new KnownSizeByteArrayBuilder(-1)); + assertEquals("Invalid size: -1", e.getMessage()); + + e = assertThrows(IllegalArgumentException.class, () -> new KnownSizeByteArrayBuilder(Integer.MAX_VALUE)); + assertEquals("Invalid size: " + Integer.MAX_VALUE, e.getMessage()); + } + + + @ParameterizedTest + @ValueSource(ints = { + 1, + 10, + KnownSizeByteArrayBuilder.INITIAL_SECTION_SIZE + 100, + KnownSizeByteArrayBuilder.INITIAL_SECTION_SIZE * 2, + KnownSizeByteArrayBuilder.INITIAL_SECTION_SIZE * 2 + 100, + KnownSizeByteArrayBuilder.INITIAL_SECTION_SIZE * 5 + 100, + }) + void testAddByte(int expectedSize) { + byte[] testData = getTestBytes(expectedSize); + KnownSizeByteArrayBuilder builder = new KnownSizeByteArrayBuilder(expectedSize); + assertEquals(0, builder.getAddedBytesCount()); + + for (byte b : testData) { + builder.addByte(b); + } + + assertEquals(expectedSize, builder.getAddedBytesCount()); + byte[] actualArray = builder.createByteArray(); + assertArrayEquals(testData, actualArray); + } + + @Test + void testAddByte_MoreThanExpected() { + KnownSizeByteArrayBuilder builder = new KnownSizeByteArrayBuilder(1); + builder.addByte((byte) 1); + + IllegalStateException e = assertThrows(IllegalStateException.class, () -> builder.addByte((byte) 1)); + assertEquals("Already added all expected bytes", e.getMessage()); + } + + @Test + void testAddBytes() { + byte[] testData = getTestBytes(KnownSizeByteArrayBuilder.INITIAL_SECTION_SIZE + 100); + KnownSizeByteArrayBuilder builder = new KnownSizeByteArrayBuilder(testData.length); + + int index = 0; + int nextAddLength = 0; + + while (index < testData.length) { + byte[] array = testData.clone(); + builder.addBytes(array, index, nextAddLength); + + assertArrayEquals(testData, array, "Builder modified content of given array"); + + index += nextAddLength; + nextAddLength = Math.min((nextAddLength + 1) * 2, testData.length - index); + } + + assertArrayEquals(testData, builder.createByteArray()); + } + + @Test + void testAddBytes_MoreThanExpected() { + KnownSizeByteArrayBuilder builder = new KnownSizeByteArrayBuilder(1); + + IllegalStateException e = assertThrows(IllegalStateException.class, () -> builder.addBytes(new byte[2], 0, 2)); + assertEquals("Trying to add more bytes than expected", e.getMessage()); + } + + @Test + void testAddAllBytesFrom() throws IOException { + byte[] testData = getTestBytes(KnownSizeByteArrayBuilder.INITIAL_SECTION_SIZE + 100); + KnownSizeByteArrayBuilder builder = new KnownSizeByteArrayBuilder(testData.length); + + ByteArrayInputStream inputStream = new ByteArrayInputStream(testData); + builder.addAllBytesFrom(inputStream); + + assertEquals(-1, inputStream.read(), "Should have reached end"); + assertArrayEquals(testData, builder.createByteArray()); + } + + @Test + void testAddAllBytesFrom_LessThanExpected() { + int expected = 10; + KnownSizeByteArrayBuilder builder = new KnownSizeByteArrayBuilder(expected); + ByteArrayInputStream inputStream = new ByteArrayInputStream(new byte[expected - 1]); + + EOFException e = assertThrows(EOFException.class, () -> builder.addAllBytesFrom(inputStream)); + assertEquals("Unexpected EOF; was expecting more bytes", e.getMessage()); + } + + @Test + void testAsOutputStream() throws IOException { + byte[] testData = getTestBytes(KnownSizeByteArrayBuilder.INITIAL_SECTION_SIZE + 100); + KnownSizeByteArrayBuilder builder = new KnownSizeByteArrayBuilder(testData.length); + OutputStream outputStream = builder.asOutputStream(); + + int index = 0; + int nextAddLength = 0; + + // Test OutputStream.write(int) + for (; index < 5; index++) { + outputStream.write(testData[index]); + } + + while (index < testData.length) { + byte[] array = testData.clone(); + outputStream.write(array, index, nextAddLength); + + assertArrayEquals(testData, array, "OutputStream modified content of given array"); + + index += nextAddLength; + nextAddLength = Math.min((nextAddLength + 1) * 2, testData.length - index); + } + + assertArrayEquals(testData, builder.createByteArray()); + } + + @ParameterizedTest + @ValueSource(ints = { + 1, + 10, + KnownSizeByteArrayBuilder.INITIAL_SECTION_SIZE + 100, + KnownSizeByteArrayBuilder.INITIAL_SECTION_SIZE * 2 + 100, + }) + void testCreateByteArray_LessThanExpected(int dataLength) { + byte[] testData = getTestBytes(dataLength); + // Set expected size higher than actual data length + KnownSizeByteArrayBuilder builder = new KnownSizeByteArrayBuilder(testData.length + 1); + + byte[] array = testData.clone(); + builder.addBytes(array, 0, testData.length); + assertArrayEquals(testData, array, "Builder modified content of given array"); + + IllegalStateException e = assertThrows(IllegalStateException.class, () -> builder.createByteArray(false)); + assertEquals("Not all bytes have been added yet", e.getMessage()); + + assertArrayEquals(testData, builder.createByteArray(true)); + } + + /** + * Similar to {@link #testCreateByteArray_LessThanExpected(int)}, except that the expected + * size is set way higher than the actual size. + */ + @ParameterizedTest + @ValueSource(ints = { + 1, + 10, + KnownSizeByteArrayBuilder.INITIAL_SECTION_SIZE + 100, + KnownSizeByteArrayBuilder.INITIAL_SECTION_SIZE * 2 + 100, + }) + void testCreateByteArray_LotLessThanExpected(int dataLength) { + byte[] testData = getTestBytes(dataLength); + // Set expected size higher than actual data length + KnownSizeByteArrayBuilder builder = new KnownSizeByteArrayBuilder(testData.length * 3); + + byte[] array = testData.clone(); + builder.addBytes(array, 0, testData.length); + assertArrayEquals(testData, array, "Builder modified content of given array"); + + IllegalStateException e = assertThrows(IllegalStateException.class, () -> builder.createByteArray(false)); + assertEquals("Not all bytes have been added yet", e.getMessage()); + + assertArrayEquals(testData, builder.createByteArray(true)); + } + + @Test + void testCreateByteArray_MoreThanOnce() { + KnownSizeByteArrayBuilder builder = new KnownSizeByteArrayBuilder(1); + builder.addByte((byte) 1); + + assertArrayEquals(new byte[] {1}, builder.createByteArray()); + + IllegalStateException e = assertThrows(IllegalStateException.class, () -> builder.createByteArray()); + assertEquals("Byte array has already been created", e.getMessage()); + + e = assertThrows(IllegalStateException.class, () -> builder.addByte((byte) 1)); + assertEquals("Byte array has already been created", e.getMessage()); + } +} diff --git a/src/test/java/org/apache/commons/imaging/formats/icns/IcnsImageParserTest.java b/src/test/java/org/apache/commons/imaging/formats/icns/IcnsImageParserTest.java index 3f0d9ee75..b1ceef44f 100644 --- a/src/test/java/org/apache/commons/imaging/formats/icns/IcnsImageParserTest.java +++ b/src/test/java/org/apache/commons/imaging/formats/icns/IcnsImageParserTest.java @@ -17,24 +17,45 @@ package org.apache.commons.imaging.formats.icns; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.assertThrows; -import org.apache.commons.imaging.common.AllocationRequestException; +import java.io.EOFException; +import java.io.IOException; + import org.junit.jupiter.api.Test; public class IcnsImageParserTest { + /** + * Gets the byte at the specified {@code index} from the int value. + */ + private static byte intByte(int value, int index) { + return (byte) ((value >> (index * Byte.SIZE)) & 0xFF); + } + @Test public void testGetImageSize() throws Exception { + // org.apache.commons.imaging.common.Allocator.DEFAULT + final int fileSize = 1_073_741_824; + final int elementSize = fileSize - 8; + final byte[] bytes = { - // Header - 'i', 'c', 'n', 's', - // (Too large) file size - (byte) 0b0111_1111, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF - 10, - // Type (does not matter?) - 0, 0, 0, 0, - // (Too large) element size - (byte) 0b0111_1111, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF - 10 - 8, }; - assertThrows(AllocationRequestException.class, () -> new IcnsImageParser().getImageSize(bytes)); + // Header + 'i', 'c', 'n', 's', + // (Too large) file size + intByte(fileSize, 3), intByte(fileSize, 2), intByte(fileSize, 1), intByte(fileSize, 0), + // Type (does not matter?) + 0, 0, 0, 0, + // (Too large) element size + intByte(elementSize, 3), intByte(elementSize, 2), intByte(elementSize, 1), intByte(elementSize, 0), + }; + IOException e = assertThrows(IOException.class, () -> new IcnsImageParser().getImageSize(bytes)); + assertEquals("Not a valid ICNS file, name: Data, length: 1073741808", e.getMessage()); + + Throwable cause = e.getCause(); + assertInstanceOf(EOFException.class, cause); + assertEquals("Unexpected EOF; was expecting more bytes", cause.getMessage()); } } diff --git a/src/test/java/org/apache/commons/imaging/formats/jpeg/segments/SosSegmentTest.java b/src/test/java/org/apache/commons/imaging/formats/jpeg/segments/SosSegmentTest.java index d01e2ca35..0beb5699d 100644 --- a/src/test/java/org/apache/commons/imaging/formats/jpeg/segments/SosSegmentTest.java +++ b/src/test/java/org/apache/commons/imaging/formats/jpeg/segments/SosSegmentTest.java @@ -39,7 +39,6 @@ public void testCreatesSosSegmentTakingThreeArguments() throws IOException { public void testGetComponentsTakingNoArguments() throws IOException { final byte[] byteArray = new byte[5]; final SosSegment sosSegment = new SosSegment((-1044), byteArray); - sosSegment.getComponents(); assertEquals(0, sosSegment.successiveApproximationBitHigh); assertEquals(0, sosSegment.successiveApproximationBitLow); diff --git a/src/test/java/org/apache/commons/imaging/formats/png/PngImageParserTest.java b/src/test/java/org/apache/commons/imaging/formats/png/PngImageParserTest.java index 018e6babd..10e104387 100644 --- a/src/test/java/org/apache/commons/imaging/formats/png/PngImageParserTest.java +++ b/src/test/java/org/apache/commons/imaging/formats/png/PngImageParserTest.java @@ -17,16 +17,18 @@ package org.apache.commons.imaging.formats.png; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import java.awt.image.BufferedImage; import java.io.ByteArrayOutputStream; +import java.io.EOFException; import java.io.IOException; import org.apache.commons.imaging.ImageInfo; -import org.apache.commons.imaging.common.AllocationRequestException; import org.junit.jupiter.api.Test; public class PngImageParserTest extends PngBaseTest { @@ -38,17 +40,32 @@ private static byte[] getPngImageBytes(final BufferedImage image, final PngImagi } } + /** + * Gets the byte at the specified {@code index} from the int value. + */ + private static byte intByte(int value, int index) { + return (byte) ((value >> (index * Byte.SIZE)) & 0xFF); + } + @Test public void testGetImageSize() { + // org.apache.commons.imaging.common.Allocator.DEFAULT + final int length = 1_073_741_824; + final byte[] bytes = { // Header (byte) 0x89, 'P', 'N', 'G', '\r', '\n', 0x1A, '\n', // (Too large) Length - (byte) 0b0111_1111 , (byte) 0xFF, (byte) 0xFF, (byte) 0xFF - 10, + intByte(length, 3), intByte(length, 2), intByte(length, 1), intByte(length, 0), // Chunk type 'I', 'H', 'D', 'R', }; - assertThrows(AllocationRequestException.class, () -> new PngImageParser().getImageSize(bytes)); + IOException e = assertThrows(IOException.class, () -> new PngImageParser().getImageSize(bytes)); + assertEquals("Not a Valid PNG File: Couldn't read Chunk Data., name: Chunk Data, length: 1073741824", e.getMessage()); + + Throwable cause = e.getCause(); + assertInstanceOf(EOFException.class, cause); + assertEquals("Unexpected EOF; was expecting more bytes", cause.getMessage()); } @Test