/* * 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.sshd.common.util.buffer; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.StreamCorruptedException; import java.util.function.IntUnaryOperator; import java.util.logging.Level; import org.apache.sshd.common.PropertyResolver; import org.apache.sshd.common.util.GenericUtils; import org.apache.sshd.common.util.NumberUtils; import org.apache.sshd.common.util.ValidateUtils; import org.apache.sshd.common.util.io.IoUtils; import org.apache.sshd.common.util.logging.SimplifiedLog; /** * TODO Add javadoc * * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a> */ public final class BufferUtils { public static final char DEFAULT_HEX_SEPARATOR = ' '; public static final char EMPTY_HEX_SEPARATOR = '\0'; public static final String HEX_DIGITS = "0123456789abcdef"; public static final String HEXDUMP_CHUNK_SIZE = "sshd-hexdump-chunk-size"; public static final int DEFAULT_HEXDUMP_CHUNK_SIZE = 64; public static final Level DEFAULT_HEXDUMP_LEVEL = Level.FINEST; public static final IntUnaryOperator DEFAULT_BUFFER_GROWTH_FACTOR = BufferUtils::getNextPowerOf2; /** * Maximum value of a {@code uint32} field */ public static final long MAX_UINT32_VALUE = 0x0FFFFFFFFL; /** * Maximum value of a {@code uint8} field */ public static final int MAX_UINT8_VALUE = 0x0FF; /** * Private Constructor */ private BufferUtils() { throw new UnsupportedOperationException("No instance allowed"); } public static void dumpHex(SimplifiedLog logger, Level level, String prefix, PropertyResolver resolver, char sep, byte... data) { dumpHex(logger, level, prefix, resolver, sep, data, 0, NumberUtils.length(data)); } public static void dumpHex(SimplifiedLog logger, Level level, String prefix, PropertyResolver resolver, char sep, byte[] data, int offset, int len) { dumpHex(logger, level, prefix, sep, resolver.getIntProperty(HEXDUMP_CHUNK_SIZE, DEFAULT_HEXDUMP_CHUNK_SIZE), data, offset, len); } public static void dumpHex(SimplifiedLog logger, Level level, String prefix, char sep, int chunkSize, byte... data) { dumpHex(logger, level, prefix, sep, chunkSize, data, 0, NumberUtils.length(data)); } public static void dumpHex(SimplifiedLog logger, Level level, String prefix, char sep, int chunkSize, byte[] data, int offset, int len) { if ((logger == null) || (level == null) || (!logger.isEnabled(level))) { return; } StringBuilder sb = new StringBuilder(chunkSize * 3 /* HEX */ + prefix.length() + Long.SIZE /* some extra */); sb.append(prefix); for (int remainLen = len, chunkIndex = 1, curOffset = offset, totalLen = 0; remainLen > 0; chunkIndex++) { sb.setLength(prefix.length()); // reset for next chunk sb.append(" [chunk #").append(chunkIndex).append(']'); int dumpSize = Math.min(chunkSize, remainLen); totalLen += dumpSize; sb.append('(').append(totalLen).append('/').append(len).append(')'); try { appendHex(sb.append(' '), data, curOffset, dumpSize, sep); } catch (IOException e) { // unexpected sb.append(e.getClass().getSimpleName()).append(": ").append(e.getMessage()); } // Pad the last (incomplete) line to align its data view for (int index = dumpSize; index < chunkSize; index++) { if (sep != EMPTY_HEX_SEPARATOR) { sb.append(' '); } sb.append(" "); } sb.append(" "); for (int pos = curOffset, l = 0; l < dumpSize; pos++, l++) { int b = data[pos] & 0xFF; if ((b > ' ') && (b < 0x7E)) { sb.append((char) b); } else { sb.append('.'); } } logger.log(level, sb.toString()); remainLen -= dumpSize; curOffset += dumpSize; } } public static String toHex(byte... array) { return toHex(array, 0, NumberUtils.length(array)); } public static String toHex(char sep, byte... array) { return toHex(array, 0, NumberUtils.length(array), sep); } public static String toHex(byte[] array, int offset, int len) { return toHex(array, offset, len, DEFAULT_HEX_SEPARATOR); } public static String toHex(byte[] array, int offset, int len, char sep) { if (len <= 0) { return ""; } try { return appendHex(new StringBuilder(len * 3 /* 2 HEX + sep */), array, offset, len, sep).toString(); } catch (IOException e) { // unexpected return e.getClass().getSimpleName() + ": " + e.getMessage(); } } public static <A extends Appendable> A appendHex(A sb, char sep, byte... array) throws IOException { return appendHex(sb, array, 0, NumberUtils.length(array), sep); } public static <A extends Appendable> A appendHex(A sb, byte[] array, int offset, int len, char sep) throws IOException { if (len <= 0) { return sb; } for (int curOffset = offset, maxOffset = offset + len; curOffset < maxOffset; curOffset++) { byte b = array[curOffset]; if ((curOffset > offset) && (sep != EMPTY_HEX_SEPARATOR)) { sb.append(sep); } sb.append(HEX_DIGITS.charAt((b >> 4) & 0x0F)); sb.append(HEX_DIGITS.charAt(b & 0x0F)); } return sb; } /** * @param separator The separator between the HEX values - may be {@link #EMPTY_HEX_SEPARATOR} * @param csq The {@link CharSequence} containing the HEX encoded bytes * @return The decoded bytes * @throws IllegalArgumentException If invalid HEX sequence length * @throws NumberFormatException If invalid HEX characters found * @see #decodeHex(char, CharSequence, int, int) */ public static byte[] decodeHex(char separator, CharSequence csq) { return decodeHex(separator, csq, 0, GenericUtils.length(csq)); } /** * @param separator The separator between the HEX values - may be {@link #EMPTY_HEX_SEPARATOR} * @param csq The {@link CharSequence} containing the HEX encoded bytes * @param start Start offset of the HEX sequence (inclusive) * @param end End offset of the HEX sequence (exclusive) * @return The decoded bytes * @throws IllegalArgumentException If invalid HEX sequence length * @throws NumberFormatException If invalid HEX characters found */ public static byte[] decodeHex(char separator, CharSequence csq, int start, int end) { int len = end - start; ValidateUtils.checkTrue(len >= 0, "Bad HEX sequence length: %d", len); if (len == 0) { return GenericUtils.EMPTY_BYTE_ARRAY; } int delta = 2; byte[] bytes; if (separator != EMPTY_HEX_SEPARATOR) { // last character cannot be the separator ValidateUtils.checkTrue((len % 3) == 2, "Invalid separated HEX sequence length: %d", len); bytes = new byte[(len + 1) / 3]; delta++; } else { ValidateUtils.checkTrue((len & 0x01) == 0, "Invalid contiguous HEX sequence length: %d", len); bytes = new byte[len >>> 1]; } int writeLen = 0; for (int curPos = start; curPos < end; curPos += delta, writeLen++) { bytes[writeLen] = fromHex(csq.charAt(curPos), csq.charAt(curPos + 1)); } assert writeLen == bytes.length; return bytes; } /** * @param <S> The {@link OutputStream} generic type * @param stream The target {@link OutputStream} * @param separator The separator between the HEX values - may be {@link #EMPTY_HEX_SEPARATOR} * @param csq The {@link CharSequence} containing the HEX encoded bytes * @return The number of bytes written to the stream * @throws IOException If failed to write * @throws IllegalArgumentException If invalid HEX sequence length * @throws NumberFormatException If invalid HEX characters found * @see #decodeHex(OutputStream, char, CharSequence, int, int) */ public static <S extends OutputStream> int decodeHex(S stream, char separator, CharSequence csq) throws IOException { return decodeHex(stream, separator, csq, 0, GenericUtils.length(csq)); } /** * @param <S> The {@link OutputStream} generic type * @param stream The target {@link OutputStream} * @param separator The separator between the HEX values - may be {@link #EMPTY_HEX_SEPARATOR} * @param csq The {@link CharSequence} containing the HEX encoded bytes * @param start Start offset of the HEX sequence (inclusive) * @param end End offset of the HEX sequence (exclusive) * @return The number of bytes written to the stream * @throws IOException If failed to write * @throws IllegalArgumentException If invalid HEX sequence length * @throws NumberFormatException If invalid HEX characters found */ public static <S extends OutputStream> int decodeHex(S stream, char separator, CharSequence csq, int start, int end) throws IOException { int len = end - start; ValidateUtils.checkTrue(len >= 0, "Bad HEX sequence length: %d", len); int delta = 2; if (separator != EMPTY_HEX_SEPARATOR) { // last character cannot be the separator ValidateUtils.checkTrue((len % 3) == 2, "Invalid separated HEX sequence length: %d", len); delta++; } else { ValidateUtils.checkTrue((len & 0x01) == 0, "Invalid contiguous HEX sequence length: %d", len); } int writeLen = 0; for (int curPos = start; curPos < end; curPos += delta, writeLen++) { stream.write(fromHex(csq.charAt(curPos), csq.charAt(curPos + 1)) & 0xFF); } return writeLen; } public static byte fromHex(char hi, char lo) throws NumberFormatException { int hiValue = HEX_DIGITS.indexOf(((hi >= 'A') && (hi <= 'F')) ? ('a' + (hi - 'A')) : hi); int loValue = HEX_DIGITS.indexOf(((lo >= 'A') && (lo <= 'F')) ? ('a' + (lo - 'A')) : lo); if ((hiValue < 0) || (loValue < 0)) { throw new NumberFormatException("fromHex(" + new String(new char[]{hi, lo}) + ") non-HEX characters"); } return (byte) ((hiValue << 4) + loValue); } /** * Read a 32-bit value in network order * * @param input The {@link InputStream} * @param buf Work buffer to use * @return The read 32-bit value * @throws IOException If failed to read 4 bytes or not enough room in * @see #readInt(InputStream, byte[], int, int) */ public static int readInt(InputStream input, byte[] buf) throws IOException { return readInt(input, buf, 0, NumberUtils.length(buf)); } /** * Read a 32-bit value in network order * * @param input The {@link InputStream} * @param buf Work buffer to use * @param offset Offset in buffer to us * @param len Available length - must have at least 4 bytes available * @return The read 32-bit value * @throws IOException If failed to read 4 bytes or not enough room in * work buffer * @see #readUInt(InputStream, byte[], int, int) */ public static int readInt(InputStream input, byte[] buf, int offset, int len) throws IOException { return (int) readUInt(input, buf, offset, len); } /** * Read a 32-bit value in network order * * @param input The {@link InputStream} * @param buf Work buffer to use * @return The read 32-bit value * @throws IOException If failed to read 4 bytes or not enough room in * @see #readUInt(InputStream, byte[], int, int) */ public static long readUInt(InputStream input, byte[] buf) throws IOException { return readUInt(input, buf, 0, NumberUtils.length(buf)); } /** * Read a 32-bit value in network order * * @param input The {@link InputStream} * @param buf Work buffer to use * @param offset Offset in buffer to us * @param len Available length - must have at least 4 bytes available * @return The read 32-bit value * @throws IOException If failed to read 4 bytes or not enough room in * work buffer * @see #getUInt(byte[], int, int) */ public static long readUInt(InputStream input, byte[] buf, int offset, int len) throws IOException { try { if (len < Integer.BYTES) { throw new IllegalArgumentException("Not enough data for a UINT: required=" + Integer.BYTES + ", available=" + len); } IoUtils.readFully(input, buf, offset, Integer.BYTES); return getUInt(buf, offset, len); } catch (RuntimeException | Error e) { throw new StreamCorruptedException("Failed (" + e.getClass().getSimpleName() + ")" + " to read UINT value: " + e.getMessage()); } } /** * @param buf A buffer holding a 32-bit unsigned integer in <B>big endian</B> * format. <B>Note:</B> if more than 4 bytes are available, then only the * <U>first</U> 4 bytes in the buffer will be used * @return The result as a {@code long} whose 32 high-order bits are zero * @see #getUInt(byte[], int, int) */ public static long getUInt(byte... buf) { return getUInt(buf, 0, NumberUtils.length(buf)); } /** * @param buf A buffer holding a 32-bit unsigned integer in <B>big endian</B> * format. * @param off The offset of the data in the buffer * @param len The available data length. <B>Note:</B> if more than 4 bytes * are available, then only the <U>first</U> 4 bytes in the buffer will be * used (starting at the specified <tt>offset</tt>) * @return The result as a {@code long} whose 32 high-order bits are zero */ public static long getUInt(byte[] buf, int off, int len) { if (len < Integer.BYTES) { throw new IllegalArgumentException("Not enough data for a UINT: required=" + Integer.BYTES + ", available=" + len); } long l = (buf[off] << 24) & 0xff000000L; l |= (buf[off + 1] << 16) & 0x00ff0000L; l |= (buf[off + 2] << 8) & 0x0000ff00L; l |= (buf[off + 3]) & 0x000000ffL; return l; } /** * Writes a 32-bit value in network order (i.e., MSB 1st) * * @param output The {@link OutputStream} to write the value * @param value The 32-bit value * @param buf A work buffer to use - must have enough space to contain 4 bytes * @throws IOException If failed to write the value or work buffer to small * @see #writeInt(OutputStream, int, byte[], int, int) */ public static void writeInt(OutputStream output, int value, byte[] buf) throws IOException { writeUInt(output, value, buf, 0, NumberUtils.length(buf)); } /** * Writes a 32-bit value in network order (i.e., MSB 1st) * * @param output The {@link OutputStream} to write the value * @param value The 32-bit value * @param buf A work buffer to use - must have enough space to contain 4 bytes * @param off The offset to write the value * @param len The available space * @throws IOException If failed to write the value or work buffer to small * @see #writeUInt(OutputStream, long, byte[], int, int) */ public static void writeInt(OutputStream output, int value, byte[] buf, int off, int len) throws IOException { writeUInt(output, value & 0xFFFFFFFFL, buf, off, len); } /** * Writes a 32-bit value in network order (i.e., MSB 1st) * * @param output The {@link OutputStream} to write the value * @param value The 32-bit value * @param buf A work buffer to use - must have enough space to contain 4 bytes * @throws IOException If failed to write the value or work buffer to small * @see #writeUInt(OutputStream, long, byte[], int, int) */ public static void writeUInt(OutputStream output, long value, byte[] buf) throws IOException { writeUInt(output, value, buf, 0, NumberUtils.length(buf)); } /** * Writes a 32-bit value in network order (i.e., MSB 1st) * * @param output The {@link OutputStream} to write the value * @param value The 32-bit value * @param buf A work buffer to use - must have enough space to contain 4 bytes * @param off The offset to write the value * @param len The available space * @throws IOException If failed to write the value or work buffer to small * @see #putUInt(long, byte[], int, int) */ public static void writeUInt(OutputStream output, long value, byte[] buf, int off, int len) throws IOException { try { int writeLen = putUInt(value, buf, off, len); output.write(buf, off, writeLen); } catch (RuntimeException | Error e) { throw new StreamCorruptedException("Failed (" + e.getClass().getSimpleName() + ")" + " to write UINT value=" + value + ": " + e.getMessage()); } } /** * Writes a 32-bit value in network order (i.e., MSB 1st) * * @param value The 32-bit value * @param buf The buffer * @return The number of bytes used in the buffer * @throws IllegalArgumentException if not enough space available * @see #putUInt(long, byte[], int, int) */ public static int putUInt(long value, byte[] buf) { return putUInt(value, buf, 0, NumberUtils.length(buf)); } /** * Writes a 32-bit value in network order (i.e., MSB 1st) * * @param value The 32-bit value * @param buf The buffer * @param off The offset to write the value * @param len The available space * @return The number of bytes used in the buffer * @throws IllegalArgumentException if not enough space available */ public static int putUInt(long value, byte[] buf, int off, int len) { if (len < Integer.BYTES) { throw new IllegalArgumentException("Not enough data for a UINT: required=" + Integer.BYTES + ", available=" + len); } buf[off] = (byte) ((value >> 24) & 0xFF); buf[off + 1] = (byte) ((value >> 16) & 0xFF); buf[off + 2] = (byte) ((value >> 8) & 0xFF); buf[off + 3] = (byte) (value & 0xFF); return Integer.BYTES; } public static boolean equals(byte[] a1, byte[] a2) { int len1 = NumberUtils.length(a1); int len2 = NumberUtils.length(a2); if (len1 != len2) { return false; } else { return equals(a1, 0, a2, 0, len1); } } public static boolean equals(byte[] a1, int a1Offset, byte[] a2, int a2Offset, int length) { int len1 = NumberUtils.length(a1); int len2 = NumberUtils.length(a2); if ((len1 < (a1Offset + length)) || (len2 < (a2Offset + length))) { return false; } while (length-- > 0) { if (a1[a1Offset++] != a2[a2Offset++]) { return false; } } return true; } public static int getNextPowerOf2(int value) { // for 0-7 return 8 return (value < Byte.SIZE) ? Byte.SIZE : NumberUtils.getNextPowerOf2(value); } /** * Used for encodings where we don't know the data length before adding it * to the buffer. The idea is to place a 32-bit "placeholder", * encode the data and then return back to the placeholder and update the * length. The method calculates the encoded data length, moves the write * position to the specified placeholder position, updates the length value * and then moves the write position it back to its original value. * * @param buffer The {@link Buffer} * @param lenPos The offset in the buffer where the length placeholder is * to be update - <B>Note:</B> assumption is that the encoded data starts * <U>immediately</U> after the placeholder * @return The amount of data that has been encoded */ public static int updateLengthPlaceholder(Buffer buffer, int lenPos) { int startPos = lenPos + Integer.BYTES; int endPos = buffer.wpos(); int dataLength = endPos - startPos; // NOTE: although data length is defined as UINT32, we do not expected sizes above Integer.MAX_VALUE ValidateUtils.checkTrue(dataLength >= 0, "Illegal data length: %d", dataLength); buffer.wpos(lenPos); buffer.putInt(dataLength); buffer.wpos(endPos); return dataLength; } /** * Updates a 32-bit "placeholder" location for data length - moves * the write position to the specified placeholder position, updates the length * value and then moves the write position it back to its original value. * * @param buffer The {@link Buffer} * @param lenPos The offset in the buffer where the length placeholder is * to be update - <B>Note:</B> assumption is that the encoded data starts * <U>immediately</U> after the placeholder * @param dataLength The length to update */ public static void updateLengthPlaceholder(Buffer buffer, int lenPos, int dataLength) { int curPos = buffer.wpos(); buffer.wpos(lenPos); buffer.putInt(dataLength); buffer.wpos(curPos); } /** * Invokes {@link Buffer#clear()} * * @param <B> The generic buffer type * @param buffer A {@link Buffer} instance - ignored if {@code null} * @return The same as the input instance */ public static <B extends Buffer> B clear(B buffer) { if (buffer != null) { buffer.clear(); } return buffer; } public static long validateInt32Value(long value, String message) { ValidateUtils.checkTrue(isValidInt32Value(value), message, value); return value; } public static long validateInt32Value(long value, String format, Object arg) { ValidateUtils.checkTrue(isValidInt32Value(value), format, arg); return value; } public static long validateInt32Value(long value, String format, Object... args) { ValidateUtils.checkTrue(isValidInt32Value(value), format, args); return value; } public static boolean isValidInt32Value(long value) { return (value >= Integer.MIN_VALUE) && (value <= Integer.MAX_VALUE); } public static long validateUint32Value(long value, String message) { ValidateUtils.checkTrue(isValidUint32Value(value), message, value); return value; } public static long validateUint32Value(long value, String format, Object arg) { ValidateUtils.checkTrue(isValidUint32Value(value), format, arg); return value; } public static long validateUint32Value(long value, String format, Object... args) { ValidateUtils.checkTrue(isValidUint32Value(value), format, args); return value; } public static boolean isValidUint32Value(long value) { return (value >= 0L) && (value <= MAX_UINT32_VALUE); } }