/* * This file is part of jHDF. A pure Java library for accessing HDF5 files. * * http://jhdf.io * * Copyright (c) 2020 James Mudd * * MIT License see 'LICENSE' file */ package io.jhdf; import org.apache.commons.lang3.ArrayUtils; import java.math.BigInteger; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.nio.charset.CharsetEncoder; import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.BitSet; import static java.nio.ByteOrder.LITTLE_ENDIAN; public final class Utils { private static final CharsetEncoder ASCII = StandardCharsets.US_ASCII.newEncoder(); private Utils() { // No instances } /** * Converts an address to a hex string for display * * @param address to convert to Hex * @return the address as a hex string */ public static String toHex(long address) { if (address == Constants.UNDEFINED_ADDRESS) { return "UNDEFINED"; } return "0x" + Long.toHexString(address); } /** * Reads ASCII string from the buffer until a null character is reached. This * will read from the buffers current position. After the method the buffer * position will be after the null character. * * @param buffer to read from * @return the string read from the buffer * @throws IllegalArgumentException if the end of the buffer if reached before * and null terminator */ public static String readUntilNull(ByteBuffer buffer) { StringBuilder sb = new StringBuilder(buffer.remaining()); while (buffer.hasRemaining()) { byte b = buffer.get(); if (b == Constants.NULL) { return sb.toString(); } sb.append((char) b); } throw new IllegalArgumentException("End of buffer reached before NULL"); } /** * Check the provided name to see if it is valid for a HDF5 identifier. Checks * name only contains ASCII characters and does not contain '/' or '.' which are * reserved characters. * * @param name To check if valid * @return <code>true</code> if this is a valid HDF5 name, <code>false</code> * otherwise */ public static boolean validateName(String name) { return ASCII.canEncode(name) && !name.contains("/") && !name.contains("."); } /** * Moves the position of the {@link ByteBuffer} to the next position aligned on * 8 bytes. If the buffer position is already a multiple of 8 the position will * not be changed. * * @param bb the buffer to be aligned */ public static void seekBufferToNextMultipleOfEight(ByteBuffer bb) { int pos = bb.position(); if (pos % 8 == 0) { return; // Already on a 8 byte multiple } bb.position(pos + (8 - (pos % 8))); } /** * This reads the requested number of bytes from the buffer and returns the data * as an unsigned <code>int</code>. After this call the buffer position will be * advanced by the specified length. * <p> * This is used in HDF5 to read "size of lengths" and "size of offsets" * * @param buffer to read from * @param length the number of bytes to read * @return the <code>int</code> value read from the buffer * @throws ArithmeticException if the data cannot be safely converted to an * unsigned <code>int</code> * @throws IllegalArgumentException if the length requested is not supported i.e * > 8 */ public static int readBytesAsUnsignedInt(ByteBuffer buffer, int length) { switch (length) { case 1: return Byte.toUnsignedInt(buffer.get()); case 2: return Short.toUnsignedInt(buffer.getShort()); case 3: return readArbitraryLengthBytesAsUnsignedInt(buffer, length); case 4: int value = buffer.getInt(); if (value < 0) { throw new ArithmeticException("Could not convert to unsigned"); } return value; case 5: case 6: case 7: return readArbitraryLengthBytesAsUnsignedInt(buffer, length); case 8: // Throws if the long can't be converted safely return Math.toIntExact(buffer.getLong()); default: throw new IllegalArgumentException("Couldn't read " + length + " bytes as int"); } } /** * This method is used when the length required is awkward i.e. no support * directly from {@link ByteBuffer} * * @param buffer to read from * @param length the number of bytes to read * @return the long value read from the buffer * @throws ArithmeticException if the data cannot be safely converted to an * unsigned long */ private static int readArbitraryLengthBytesAsUnsignedInt(ByteBuffer buffer, int length) { // Here we will use BigInteger to convert a byte array byte[] bytes = new byte[length]; buffer.get(bytes); // BigInteger needs big endian so flip the order if needed if (buffer.order() == LITTLE_ENDIAN) { ArrayUtils.reverse(bytes); } // Convert to a unsigned long throws if it overflows return new BigInteger(1, bytes).intValueExact(); } /** * This reads the requested number of bytes from the buffer and returns the data * as an unsigned long. After this call the buffer position will be advanced by * the specified length. * <p> * This is used in HDF5 to read "size of lengths" and "size of offsets" * * @param buffer to read from * @param length the number of bytes to read * @return the long value read from the buffer * @throws ArithmeticException if the data cannot be safely converted to an * unsigned long * @throws IllegalArgumentException if the length requested is not supported; */ public static long readBytesAsUnsignedLong(ByteBuffer buffer, int length) { switch (length) { case 1: return Byte.toUnsignedLong(buffer.get()); case 2: return Short.toUnsignedLong(buffer.getShort()); case 3: return readArbitraryLengthBytesAsUnsignedLong(buffer, length); case 4: return Integer.toUnsignedLong(buffer.getInt()); case 5: case 6: case 7: return readArbitraryLengthBytesAsUnsignedLong(buffer, length); case 8: long value = buffer.getLong(); if (value < 0 && value != Constants.UNDEFINED_ADDRESS) { throw new ArithmeticException("Could not convert to unsigned"); } return value; default: throw new IllegalArgumentException("Couldn't read " + length + " bytes as int"); } } /** * This method is used when the length required is awkward i.e. no support * directly from {@link ByteBuffer} * * @param buffer to read from * @param length the number of bytes to read * @return the long value read from the buffer * @throws ArithmeticException if the data cannot be safely converted to an * unsigned long */ private static long readArbitraryLengthBytesAsUnsignedLong(ByteBuffer buffer, int length) { // Here we will use BigInteger to convert a byte array byte[] bytes = new byte[length]; buffer.get(bytes); // BigInteger needs big endian so flip the order if needed if (buffer.order() == LITTLE_ENDIAN) { ArrayUtils.reverse(bytes); } // Convert to a unsigned long throws if it overflows return new BigInteger(1, bytes).longValueExact(); } /** * Creates a new {@link ByteBuffer} of the specified length. The new buffer will * start at the current position of the source buffer and will be of the * specified length. The {@link ByteOrder} of the new buffer will be the same as * the source buffer. After the call the source buffer position will be * incremented by the length of the sub-buffer. The new buffer will share the * backing data with the source buffer. * * @param source the buffer to take the sub buffer from * @param length the size of the new sub-buffer * @return the new sub buffer */ public static ByteBuffer createSubBuffer(ByteBuffer source, int length) { ByteBuffer headerData = source.slice(); headerData.limit(length); headerData.order(source.order()); // Move the buffer past this header source.position(source.position() + length); return headerData; } private static final BigInteger TWO = BigInteger.valueOf(2); /** * Takes a {@link BitSet} and a range of bits to inspect and converts the bits * to a integer. * * @param bits to inspect * @param start the first bit * @param length the number of bits to inspect * @return the integer represented by the provided bits */ public static int bitsToInt(BitSet bits, int start, int length) { if (length <= 0) { throw new IllegalArgumentException("length must be >0"); } BigInteger result = BigInteger.ZERO; for (int i = 0; i < length; i++) { if (bits.get(start + i)) { result = result.add(TWO.pow(i)); } } return result.intValue(); } /** * Calculates how many bytes are needed to store the given unsigned number. * * @param number to store * @return the number of bytes needed to hold this number * @throws IllegalArgumentException if a negative number is given */ public static int bytesNeededToHoldNumber(long number) { if (number < 0) { throw new IllegalArgumentException("Only for unsigned numbers"); } if (number == 0) { return 1; } return (int) Math.ceil(BigInteger.valueOf(number).bitLength() / 8.0); } public static int[] linearIndexToDimensionIndex(int index, int[] dimensions) { int[] dimIndex = new int[dimensions.length]; for (int i = dimIndex.length - 1; i >= 0; i--) { dimIndex[i] = index % dimensions[i]; index = index / dimensions[i]; } return dimIndex; } public static int dimensionIndexToLinearIndex(int[] index, int[] dimensions) { int linear = 0; for (int i = 0; i < dimensions.length; i++) { int temp = index[i]; for (int j = i + 1; j < dimensions.length; j++) { temp *= dimensions[j]; } linear += temp; } return linear; } /** * Calculates the chunk offset from a given chunk index * * @param chunkIndex The index to calculate for * @param chunkDimensions The chunk dimensions * @param datasetDimensions The dataset dimensions * @return The chunk offset for the chunk of this index */ public static int[] chunkIndexToChunkOffset(int chunkIndex, int[] chunkDimensions, int[] datasetDimensions) { final int[] chunkOffset = new int[chunkDimensions.length]; // Start from the slowest dim for (int i = 0; i < chunkOffset.length; i++) { // Find out how many chunks make one chunk in this dim int chunksBelowThisDim = 1; // Start one dim faster for (int j = i+ 1; j < chunkOffset.length; j++) { chunksBelowThisDim *= (int) Math.ceil((double) datasetDimensions[j] / chunkDimensions[j]); } chunkOffset[i] = (chunkIndex / chunksBelowThisDim ) * chunkDimensions[i]; chunkIndex -= chunkOffset[i]/chunkDimensions[i] * chunksBelowThisDim; } return chunkOffset; } /** * Removes the zeroth (leading) index. e.g [1,2,3] → [2,3] * * @param dims the array to strip * @return dims with the zeroth element removed */ public static int[] stripLeadingIndex(int[] dims) { return Arrays.copyOfRange(dims, 1, dims.length); } }