HexKit.java

package space.sunqian.common.codec;

import space.sunqian.annotations.Nonnull;
import space.sunqian.annotations.Nullable;
import space.sunqian.common.base.exception.FsRuntimeException;
import space.sunqian.common.io.BufferKit;
import space.sunqian.common.io.ByteArrayOperator;
import space.sunqian.common.io.IORuntimeException;

import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;

/**
 * Utilities for Hex.
 *
 * @author sunqian
 */
public class HexKit {

    /**
     * Returns a {@code hex} encoder encoding in upper case. This method is equivalent to {@code encoder(true)}.
     *
     * @return a {@code hex} encoder encoding in upper case
     */
    public static Encoder encoder() {
        return encoder(true);
    }

    /**
     * Returns a {@code hex} encoder. The {@code upper} parameter specifies whether the encoder uses upper case for
     * encoding ({@code true} for {@code A-F} and {@code false} for {@code a-f}).
     *
     * @param upper {@code true} for upper case, otherwise {@code false}
     * @return a {@code hex} encoder
     */
    public static Encoder encoder(boolean upper) {
        return upper ? EncoderImpl.UPPER : EncoderImpl.LOWER;
    }

    /**
     * Returns a {@code hex} decoder with the strict mode. This method is equivalent to {@code decoder(true)}.
     *
     * @return a {@code hex} decoder with the strict mod
     */
    public static Decoder decoder() {
        return decoder(true);
    }

    /**
     * Returns a {@code hex} decoder. The {@code strict} parameter specifies whether the decoder is strict or not, a
     * strict decoder will throw an exception if it encounters an invalid hex character, and an un-strict decoder will
     * ignore the invalid hex characters.
     *
     * @param strict {@code true} for strict, otherwise {@code false}
     * @return a {@code hex} decoder with the specified mod
     */
    public static Decoder decoder(boolean strict) {
        return strict ? DecoderImpl.STRICT : DecoderImpl.LOOSE;
    }

    private static final class EncoderImpl implements Encoder, ByteArrayOperator {

        private static final @Nonnull EncoderImpl UPPER = new EncoderImpl(true);
        private static final @Nonnull EncoderImpl LOWER = new EncoderImpl(false);

        private static final char @Nonnull [] UPPER_DICT = {
            '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'
        };

        private static final char @Nonnull [] LOWER_DICT = {
            '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'
        };

        private final boolean upper;

        private EncoderImpl(boolean upper) {
            this.upper = upper;
        }

        @Override
        public byte @Nonnull [] encode(byte @Nonnull [] bytes) throws HexException {
            byte[] dst = new byte[bytes.length * 2];
            process(bytes, 0, dst, 0, bytes.length);
            return dst;
        }

        @Override
        public byte @Nonnull [] encode(@Nonnull ByteBuffer buffer) throws HexException {
            byte[] dst = new byte[buffer.remaining() * 2];
            ByteBuffer dstBuf = ByteBuffer.wrap(dst);
            BufferKit.process(buffer, dstBuf, this);
            return dst;
        }

        @Override
        public int process(byte @Nonnull [] src, int srcOff, byte @Nonnull [] dst, int dstOff, int len) {
            char[] dict = upper ? UPPER_DICT : LOWER_DICT;
            for (int i = srcOff, j = dstOff; i < srcOff + len; ) {
                int bits = src[i++];
                dst[j++] = (byte) dict[((bits >> 4) & 0x0f)];
                dst[j++] = (byte) dict[(bits & 0x0f)];
            }
            return len * 2;
        }
    }

    private static final class DecoderImpl implements Decoder, ByteArrayOperator {

        private static final @Nonnull DecoderImpl STRICT = new DecoderImpl(true);
        private static final @Nonnull DecoderImpl LOOSE = new DecoderImpl(false);

        private final boolean strict;

        private DecoderImpl(boolean strict) {
            this.strict = strict;
        }

        @Override
        public byte @Nonnull [] decode(byte @Nonnull [] bytes) throws HexException {
            checkLen(bytes.length);
            byte[] dst = new byte[bytes.length / 2];
            int actualLen = process(bytes, 0, dst, 0, bytes.length);
            return actualLen == dst.length ? dst : Arrays.copyOfRange(dst, 0, actualLen);
        }

        @Override
        public byte @Nonnull [] decode(@Nonnull ByteBuffer buffer) throws HexException {
            checkLen(buffer.remaining());
            byte[] dst = new byte[buffer.remaining() / 2];
            ByteBuffer dstBuf = ByteBuffer.wrap(dst);
            try {
                int actualLen = BufferKit.process(buffer, dstBuf, this);
                return actualLen == dst.length ? dst : Arrays.copyOfRange(dst, 0, actualLen);
            } catch (IORuntimeException e) {
                throw (HexException) e.getCause();
            }
        }

        @Override
        public int process(
            byte @Nonnull [] src, int srcOff, byte @Nonnull [] dst, int dstOff, int len
        ) throws HexException {
            if (strict) {
                return processStrict(src, srcOff, dst, dstOff, len);
            } else {
                return processLoose(src, srcOff, dst, dstOff, len);
            }
        }

        private int processStrict(
            byte @Nonnull [] src, int srcOff, byte @Nonnull [] dst, int dstOff, int len
        ) throws HexException {
            for (int i = 0, j = dstOff; i < len; ) {
                int bits1 = toDigit((char) src[i + srcOff]);
                if (bits1 < 0) {
                    throw new HexException(i, "The hex string contains invalid character at position: " + i + ".");
                }
                i++;
                int bits2 = toDigit((char) src[i + srcOff]);
                if (bits2 < 0) {
                    throw new HexException(i, "The hex string contains invalid character at position: " + i + ".");
                }
                i++;
                int bits = ((bits1 << 4) | bits2);
                dst[j++] = (byte) bits;
            }
            return len / 2;
        }

        public int processLoose(
            byte @Nonnull [] src, int srcOff, byte @Nonnull [] dst, int dstOff, int len
        ) throws HexException {
            int i = 0, j = dstOff, count = 0;
            int bits1 = -1;
            while (i < len) {
                int bits = toDigit((char) src[i + srcOff]);
                if (bits < 0) {
                    i++;
                    continue;
                }
                if (bits1 < 0) {
                    bits1 = bits;
                } else {
                    int c = ((bits1 << 4) | bits);
                    dst[j++] = (byte) c;
                    count++;
                    bits1 = -1;
                }
                i++;
            }
            if (bits1 >= 0) {
                throw new HexException("The valid hex string is not a multiple of 2.");
            }
            return count;
        }

        private int toDigit(char c) {
            if (c >= '0' && c <= '9') {
                return c - '0';
            }
            if (c >= 'a' && c <= 'f') {
                return c - 'a' + 10;
            }
            if (c >= 'A' && c <= 'F') {
                return c - 'A' + 10;
            }
            return -1;
        }

        private void checkLen(int len) throws HexException {
            if (strict) {
                if (len % 2 != 0) {
                    throw new HexException("The length of hex string is not a multiple of 2.");
                }
            }
        }
    }

    /**
     * Encoder for Hex. The implementation should be immutable and thread-safe.
     */
    public interface Encoder {

        /**
         * Encodes the given bytes to hex string as byte array.
         *
         * @param bytes the given bytes to encode
         * @return the hex string as byte array
         * @throws HexException if any error occurs
         */
        byte @Nonnull [] encode(byte @Nonnull [] bytes) throws HexException;

        /**
         * Encodes the given buffer to hex string as byte array. The position of the given buffer will increment to its
         * limit.
         *
         * @param buffer the given buffer to encode
         * @return the hex string as byte array
         * @throws HexException if any error occurs
         */
        byte @Nonnull [] encode(@Nonnull ByteBuffer buffer) throws HexException;

        /**
         * Encodes the given bytes to hex string.
         *
         * @param bytes the given bytes to encode
         * @return the hex string
         * @throws HexException if any error occurs
         */
        default @Nonnull String encodeToString(byte @Nonnull [] bytes) throws HexException {
            byte[] dst = encode(bytes);
            return new String(dst, StandardCharsets.ISO_8859_1);
        }

        /**
         * Encodes the given buffer to hex string. The position of the given buffer will increment to its limit.
         *
         * @param buffer the given buffer to encode
         * @return the hex string
         * @throws HexException if any error occurs
         */
        default @Nonnull String encodeToString(@Nonnull ByteBuffer buffer) throws HexException {
            byte[] dst = encode(buffer);
            return new String(dst, StandardCharsets.ISO_8859_1);
        }
    }

    /**
     * Decoder for Hex. The implementation should be immutable and thread-safe.
     */
    public interface Decoder {

        /**
         * Decodes the given bytes of hex string to the original bytes.
         *
         * @param bytes the given bytes of hex string
         * @return the original bytes
         * @throws HexException if any error occurs
         */
        byte @Nonnull [] decode(byte @Nonnull [] bytes) throws HexException;

        /**
         * Decodes the given buffer of hex string to the original bytes. The position of the given buffer will increment
         * to its limit.
         *
         * @param buffer the given buffer of hex string
         * @return the original bytes
         * @throws HexException if any error occurs
         */
        byte @Nonnull [] decode(@Nonnull ByteBuffer buffer) throws HexException;

        /**
         * Decodes the given hex string to the original bytes.
         *
         * @param hex the given hex string
         * @return the original bytes
         * @throws HexException if any error occurs
         */
        default byte @Nonnull [] decode(@Nonnull String hex) throws HexException {
            byte[] src = hex.getBytes(StandardCharsets.ISO_8859_1);
            return decode(src);
        }
    }

    /**
     * Exception for hex encoding/decoding. The {@link #position()} returns the position where this exception occurs.
     *
     * @author sunqian
     */
    public static class HexException extends FsRuntimeException {

        private final long position;

        /**
         * Empty constructor.
         */
        public HexException() {
            super();
            this.position = -1;
        }

        /**
         * Constructs with the message.
         *
         * @param message the message
         */
        public HexException(@Nullable String message) {
            super(message);
            this.position = -1;
        }

        /**
         * Constructs with the message and cause.
         *
         * @param message the message
         * @param cause   the cause
         */
        public HexException(@Nullable String message, @Nullable Throwable cause) {
            super(message, cause);
            this.position = -1;
        }

        /**
         * Constructs with the cause.
         *
         * @param cause the cause
         */
        public HexException(@Nullable Throwable cause) {
            super(cause);
            this.position = -1;
        }

        /**
         * Empty with the position.
         *
         * @param position the position where this exception occurs
         */
        public HexException(long position) {
            super();
            this.position = position;
        }

        /**
         * Constructs with the position and message.
         *
         * @param position the position where this exception occurs
         * @param message  the message
         */
        public HexException(long position, @Nullable String message) {
            super(message);
            this.position = position;
        }

        /**
         * Constructs with the position, message and cause.
         *
         * @param position the position where this exception occurs
         * @param message  the message
         * @param cause    the cause
         */
        public HexException(long position, @Nullable String message, @Nullable Throwable cause) {
            super(message, cause);
            this.position = position;
        }

        /**
         * Constructs with the position and cause.
         *
         * @param position the position where this exception occurs
         * @param cause    the cause
         */
        public HexException(long position, @Nullable Throwable cause) {
            super(cause);
            this.position = position;
        }

        /**
         * Returns the position where this exception occurs, may be {@code -1} if the position is unknown.
         *
         * @return the position where this exception occurs, may be {@code -1} if the position is unknown
         */
        public long position() {
            return position;
        }
    }

    private HexKit() {
    }
}