org.conqat.lib.commons.io.ByteArrayUtils Maven / Gradle / Ivy
Show all versions of teamscale-lib-commons Show documentation
/*
* Copyright (c) CQSE GmbH
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.conqat.lib.commons.io;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.OptionalLong;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;
import org.conqat.lib.commons.assertion.CCSMAssert;
import org.conqat.lib.commons.collections.CollectionUtils;
import org.conqat.lib.commons.filesystem.FileSystemUtils;
import org.conqat.lib.commons.string.StringUtils;
/**
* Utility methods for dealing with raw byte arrays. This is located in the I/O package, as the
* typical application for these methods is binary I/O on byte array level.
*/
public class ByteArrayUtils {
/**
* An empty byte array. Analog to {@link StringUtils#EMPTY_STRING}.
*/
public static final byte[] EMPTY_ARRAY = {};
/**
* Converts an integer value to a byte array. The returned array has a length of
* {@link Integer#BYTES}.
*/
public static byte[] intToByteArray(int value) {
byte[] bytes = new byte[Integer.BYTES];
storeIntInStartOfArray(value, bytes);
return bytes;
}
/**
* Converts an array of integer values to an array of bytes by converting each int to its 4 byte
* representation and concatenating the results.
*/
public static byte[] intsToByteArray(int... values) {
byte[] bytes = new byte[Integer.BYTES * values.length];
for (int i = 0; i < values.length; ++i) {
storeIntInArray(values[i], bytes, i * Integer.BYTES);
}
return bytes;
}
/** Stores the given int at the first 4 bytes of the array. */
public static void storeIntInStartOfArray(int value, byte[] bytes) {
storeIntInArray(value, bytes, 0);
}
/** Stores the given int at the first 4 bytes of the array. */
public static void storeIntInArray(int value, byte[] bytes, int startOffset) {
bytes[startOffset] = (byte) (value >> 24);
bytes[startOffset + 1] = (byte) (value >> 16);
bytes[startOffset + 2] = (byte) (value >> 8);
bytes[startOffset + 3] = (byte) (value);
}
/**
* Converts a double value to a byte array. The returned array has a length of {@link Double#BYTES}
*/
public static byte[] doubleToByteArray(double value) {
long longBits = Double.doubleToRawLongBits(value);
return ByteArrayUtils.longToByteArray(longBits);
}
/**
* Converts a long value to a byte array. The returned array has a length of {@link Long#BYTES}
*/
public static byte[] longToByteArray(long value) {
byte[] bytes = new byte[Long.BYTES];
putLongIntoByteArray(bytes, 0, value);
return bytes;
}
/**
* Converts a byte array to an integer value.
*
* Overall, this method is only guaranteed to work if the input array was created by
* {@link #intToByteArray(int)}.
*/
public static int byteArrayToInt(byte[] bytes) {
CCSMAssert.isTrue(bytes.length == Integer.BYTES, "bytes.length must be 4");
return readIntFromStartOfArray(bytes);
}
/**
* Reads an int stored with {@link #storeIntInStartOfArray(int, byte[])} from the first 4 bytes of
* the array.
*/
public static int readIntFromStartOfArray(byte[] bytes) {
return getIntFromByteArray(bytes, 0);
}
/**
* Converts a byte array to a long value.
*
* Overall, this method is only guaranteed to work if the input array was created by
* {@link #longToByteArray(long)}.
*/
public static long byteArrayToLong(byte[] bytes) {
CCSMAssert.isTrue(bytes.length == Long.BYTES, "bytes.length must be 8");
return getLongFromByteArray(bytes, 0);
}
/**
* Converts a byte array to a double value.
*
* Overall, this method is only guaranteed to work if the input array was created by
* {@link #doubleToByteArray(double)}.
*/
public static double byteArrayToDouble(byte[] value) {
long longBits = ByteArrayUtils.byteArrayToLong(value);
return Double.longBitsToDouble(longBits);
}
/**
* Converts a byte array to an optional long value, by mapping a null input array to empty.
*/
public static OptionalLong byteArrayToOptionalLong(byte[] bytes) {
if (bytes == null) {
return OptionalLong.empty();
}
return OptionalLong.of(byteArrayToLong(bytes));
}
/**
* Decompresses a single byte[] using GZIP. A null input array will cause this method to return
* null.
*
* @throws IOException
* if the input array is not valid GZIP compressed data (as created by
* {@link #compress(byte[])}).
*/
public static byte[] decompress(byte[] value) throws IOException {
if (value == null) {
return null;
}
ByteArrayOutputStream bos = new ByteArrayOutputStream(value.length);
ByteArrayInputStream bis = new ByteArrayInputStream(value);
GZIPInputStream gzis = new GZIPInputStream(bis);
FileSystemUtils.copy(gzis, bos);
// it does not matter if we close in case of exceptions, as these are
// in-memory resources
gzis.close();
bos.close();
return bos.toByteArray();
}
/**
* Compresses a single byte[] using GZIP. A null input array will cause this method to return null.
*/
public static byte[] compress(byte[] value) {
if (value == null) {
return null;
}
ByteArrayOutputStream bos = new ByteArrayOutputStream(value.length);
try {
GZIPOutputStream gzos = new GZIPOutputStream(bos);
gzos.write(value);
// it does not matter if we close in case of exceptions, as this is
// an in-memory resource
gzos.close();
} catch (IOException e) {
throw new AssertionError("Can not happen as we work in memory: " + e.getMessage());
}
return bos.toByteArray();
}
/** Returns whether the prefix is a prefix of the given key. */
public static boolean isPrefix(byte[] prefix, byte[] key) {
return isPrefix(prefix, key, 0);
}
/**
* Returns whether the prefix
is a prefix of the given key
when only
* looking at the part of key
starting at startIndex
.
*/
public static boolean isPrefix(byte[] prefix, byte[] key, int startIndex) {
if (key.length - startIndex < prefix.length) {
return false;
}
for (int i = 0; i < prefix.length; ++i) {
if (prefix[i] != key[i + startIndex]) {
return false;
}
}
return true;
}
/** Returns true if a1 is (lexicographically) less than a2. */
public static boolean isLess(byte[] a1, byte[] a2, boolean resultIfEqual) {
int limit = Math.min(a1.length, a2.length);
for (int i = 0; i < limit; ++i) {
if (unsignedByte(a1[i]) < unsignedByte(a2[i])) {
return true;
}
if (unsignedByte(a1[i]) > unsignedByte(a2[i])) {
return false;
}
}
if (a1.length < a2.length) {
return true;
}
if (a1.length > a2.length) {
return false;
}
return resultIfEqual;
}
/** Returns the unsigned byte interpretation of the parameter. */
public static int unsignedByte(byte b) {
return b & 0xff;
}
/** Returns the unsigned byte interpretation of the parameter as long. */
public static long unsignedByteAsLong(byte b) {
return b & 0xffL;
}
/** Returns the concatenation of the given arrays. */
public static byte[] concat(byte[]... arrays) {
int length = 0;
for (byte[] array : arrays) {
length += array.length;
}
byte[] result = new byte[length];
int start = 0;
for (byte[] array : arrays) {
System.arraycopy(array, 0, result, start, array.length);
start += array.length;
}
return result;
}
/** Returns the concatenation of the given arrays. */
public static byte[] concat(Collection arrays) {
return concat(CollectionUtils.toArray(arrays, byte[].class));
}
/** Truncates the given array to the given length. */
public static byte[] truncate(byte[] array, int newLength) {
if (array.length < newLength) {
return array;
} else {
byte[] truncated = new byte[newLength];
System.arraycopy(array, 0, truncated, 0, newLength);
return truncated;
}
}
/**
* Creates a hex dump of the provided bytes. This is similar to output from hexdump tools and
* primarily used for debugging. The output string will contain in each line 16 bytes of data first
* printed as hex numbers and then as a string interpretation. Each line is also prefixed with an
* offset.
*/
public static String hexDump(byte[] data) {
return hexDump(data, 16);
}
/**
* Creates a hex dump of the provided bytes. This is similar to output from hexdump tools and
* primarily used for debugging. The output string will contain in each line width
* bytes of data first printed as hex numbers and then as a string interpretation. Each line is also
* prefixed with an offset.
*/
public static String hexDump(byte[] data, int width) {
CCSMAssert.isTrue(width >= 1, "Width must be positive!");
StringBuilder builder = new StringBuilder();
for (int i = 0; i < data.length; i += width) {
hexDumpAppendLine(data, i, Math.min(data.length, i + width), width, builder);
}
return builder.toString();
}
/**
* Reads an integer from the given byte array at the given offset.
*/
public static int getIntFromByteArray(byte[] data, int offset) {
return (data[offset] & 255) << 24 | (data[offset + 1] & 255) << 16 | (data[offset + 2] & 255) << 8
| (data[offset + 3] & 255);
}
/**
* Converts a list of integers to a space optimized byte array. (i.e. by just concatenating the 4
* byte integers next to each other).
*
* Deserialize using {@link #deserializeIntegerList(byte[])}.
*/
public static byte[] serializeIntegerList(List integers) {
byte[] data = new byte[integers.size() * Integer.BYTES];
for (int i = 0; i < integers.size(); i++) {
putIntIntoByteArray(data, i * Integer.BYTES, integers.get(i));
}
return data;
}
/**
* Deserializes a byte array of integers, that was created using
* {@link #serializeIntegerList(List)}.
*/
public static List deserializeIntegerList(byte[] data) {
int listSize = data.length / Integer.BYTES;
List result = new ArrayList<>(listSize);
for (int i = 0; i < listSize; i++) {
Integer value = getIntFromByteArray(data, Integer.BYTES * i);
result.add(value);
}
return result;
}
/**
* Read a 4-byte integer from a given input stream.
*/
private static int readIntFromStream(ByteArrayInputStream inputStream) throws IOException {
byte[] intBytes = new byte[Integer.BYTES];
int elementSize = inputStream.read(intBytes);
if (elementSize == 0) {
return 0;
} else if (elementSize != Integer.BYTES) {
throw new IOException("Did expect an integer with 4 bytes");
}
return byteArrayToInt(intBytes);
}
/**
* Reads a long from the given byte array at the given offset.
*/
public static long getLongFromByteArray(byte[] data, int offset) {
return (long) (data[offset] & 255) << 56 | (long) (data[offset + 1] & 255) << 48
| (long) (data[offset + 2] & 255) << 40 | (long) (data[offset + 3] & 255) << 32
| (long) (data[offset + 4] & 255) << 24 | (data[offset + 5] & 255) << 16 | (data[offset + 6] & 255) << 8
| (data[offset + 7] & 255);
}
/**
* Reads a double from the given byte array at the given offset.
*/
public static double getDoubleFromByteArray(byte[] data, int offset) {
return Double.longBitsToDouble(getLongFromByteArray(data, offset));
}
/**
* Puts the given integer into the byte array at position of the offset.
*/
public static void putIntIntoByteArray(byte[] data, int offset, int value) {
data[offset] = (byte) ((value & 0xff000000) >> 24);
data[offset + 1] = (byte) ((value & 0x00ff0000) >> 16);
data[offset + 2] = (byte) ((value & 0x0000ff00) >> 8);
data[offset + 3] = (byte) (value & 0x000000ff);
}
/**
* Puts the given long into the byte array at position of the offset.
*/
public static void putLongIntoByteArray(byte[] data, int offset, long value) {
data[offset] = (byte) (value >>> 56);
data[offset + 1] = (byte) (value >>> 48);
data[offset + 2] = (byte) (value >>> 40);
data[offset + 3] = (byte) (value >>> 32);
data[offset + 4] = (byte) (value >>> 24);
data[offset + 5] = (byte) (value >>> 16);
data[offset + 6] = (byte) (value >>> 8);
data[offset + 7] = (byte) value;
}
/**
* Puts the given double into the byte array at position of the offset.
*/
public static void putDoubleIntoByteArray(byte[] data, int offset, double value) {
long longBits = Double.doubleToRawLongBits(value);
putLongIntoByteArray(data, offset, longBits);
}
/**
* Appends a single line to the hex dump for {@link #hexDump(byte[], int)}. The start is inclusive,
* the end is exclusive.
*/
private static void hexDumpAppendLine(byte[] data, int startOffset, int endOffset, int width,
StringBuilder builder) {
builder.append(String.format("%06d: ", startOffset));
for (int i = startOffset; i < endOffset; ++i) {
builder.append(String.format("%02x ", data[i]));
}
if (endOffset - startOffset < width) {
builder.append(StringUtils.fillString((width - (endOffset - startOffset)) * 3, StringUtils.SPACE_CHAR));
}
builder.append(StringUtils.SPACE_CHAR);
for (int i = startOffset; i < endOffset; ++i) {
boolean isInPrintableAsciiRange = (33 <= data[i] && data[i] <= 126);
if (isInPrintableAsciiRange) {
builder.append((char) data[i]);
} else {
builder.append('.');
}
}
builder.append(StringUtils.LINE_SEPARATOR);
}
/**
* Returns whether the given bytes start with the
* magic bytes that
* mark a ZIP file.
*/
public static boolean startsWithZipMagicBytes(byte[] data) {
return isPrefix(new byte[] { 0x50, 0x4b, 0x03, 0x04 }, data);
}
/**
* Returns the first index in searchIn
at or after the start index containing
* searchFor
(or -1 if not found).
*/
public static int indexOf(byte[] searchFor, byte[] searchIn, int startIndex) {
return indexOf(searchFor, searchIn, startIndex, searchIn.length);
}
/**
* Returns the first index in searchIn
at or after the start index containing
* searchFor
(or -1 if not found). endIndex is the index of the first byte that is not
* considered in the match (exclusive).
*/
public static int indexOf(byte[] searchFor, byte[] searchIn, int startIndex, int endIndex) {
if (startIndex >= endIndex || startIndex + searchFor.length > endIndex) {
return -1;
}
for (int i = startIndex; i <= endIndex - searchFor.length; ++i) {
if (isPrefix(searchFor, searchIn, i)) {
return i;
}
}
return -1;
}
/**
* Perform a split with no limit according to {@link #split(byte[], byte[], int)}.
*/
public static List split(byte[] bytes, byte[] separatorBytes) {
return split(bytes, separatorBytes, Integer.MAX_VALUE);
}
/**
* Splits the byte array at the separator bytes given maximum split amount of times. The result
* {@link ArrayList} is never longer than the maximum split.
*
* @param bytes
* The bytes to split. An empty list is returned if this is null.
* @param separatorBytes
* Non-null array of bytes to split at. Must have positive length.
* @param maxSplits
* the maximum number of splits to perform starting at the beginning of the bytes array.
* An empty list is returned if this is 0 or negative.
*/
public static List split(byte[] bytes, byte[] separatorBytes, int maxSplits) {
CCSMAssert.isNotNull(separatorBytes, "Separator bytes for byte array split can't be null.");
CCSMAssert.isTrue(separatorBytes.length > 0, "Separator bytes array must have positive length.");
List result = new ArrayList<>();
if (maxSplits <= 0 || bytes == null) {
return result;
}
int start = 0;
for (int i = 0; i < bytes.length; i++) {
if (result.size() == maxSplits - 1) {
break;
}
if (ByteArrayUtils.isPrefix(separatorBytes, bytes, i)) {
result.add(Arrays.copyOfRange(bytes, start, i));
i += separatorBytes.length;
start = i;
}
}
result.add(Arrays.copyOfRange(bytes, start, bytes.length));
return result;
}
/**
* If the data bytes start with the given prefix, a copy of {@code data} without the prefix is
* returned. Otherwise, the original data array is returned unaltered.
*/
public static byte[] removePrefix(byte[] prefix, byte[] data) {
if (isPrefix(prefix, data)) {
return Arrays.copyOfRange(data, prefix.length, data.length);
}
return data;
}
}