001/*
002 * Copyright 2023 the original author or authors.
003 * <p>
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 * <p>
008 * https://www.apache.org/licenses/LICENSE-2.0
009 * <p>
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016package de.cuioss.tools.codec;
017
018import java.nio.ByteBuffer;
019import java.nio.charset.Charset;
020import java.nio.charset.StandardCharsets;
021
022import lombok.Getter;
023import lombok.ToString;
024
025/**
026 * <h2>Overview</h2> Converts hexadecimal Strings. The Charset used for certain
027 * operation can be set, the default is set in {@link StandardCharsets#UTF_8}
028 *
029 * This class is thread-safe.
030 * <h3>Usage</h3>
031 *
032 * <pre>
033 * String roundtrip = "roundtrip";
034 * assertEquals(roundtrip, new String(Hex.decodeHex(Hex.encodeHex(roundtrip.getBytes()))));
035 * </pre>
036 *
037 *
038 * @author https://github.com/apache/commons-codec/blob/master/src/main/java/org/apache/commons/codec/binary/Hex.java
039 */
040@ToString
041public class Hex {
042
043    /**
044     * Default charset is {@link StandardCharsets#UTF_8}
045     */
046    public static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;
047
048    /**
049     * Used to build output as Hex
050     */
051    private static final char[] DIGITS_LOWER = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd',
052            'e', 'f' };
053
054    /**
055     * Used to build output as Hex
056     */
057    private static final char[] DIGITS_UPPER = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D',
058            'E', 'F' };
059
060    /**
061     * Converts a String representing hexadecimal values into an array of bytes of
062     * those same values. The returned array will be half the length of the passed
063     * String, as it takes two characters to represent any given byte. An exception
064     * is thrown if the passed String has an odd number of elements.
065     *
066     * @param data A String containing hexadecimal digits
067     * @return A byte array containing binary data decoded from the supplied char
068     *         array.
069     * @throws DecoderException Thrown if an odd number or illegal of characters is
070     *                          supplied
071     */
072    public static byte[] decodeHex(final String data) throws DecoderException {
073        return decodeHex(data.toCharArray());
074    }
075
076    /**
077     * Converts an array of characters representing hexadecimal values into an array
078     * of bytes of those same values. The returned array will be half the length of
079     * the passed array, as it takes two characters to represent any given byte. An
080     * exception is thrown if the passed char array has an odd number of elements.
081     *
082     * @param data An array of characters containing hexadecimal digits
083     * @return A byte array containing binary data decoded from the supplied char
084     *         array.
085     * @throws DecoderException Thrown if an odd number or illegal of characters is
086     *                          supplied
087     */
088    @SuppressWarnings("squid:ForLoopCounterChangedCheck") // owolff: original code
089    public static byte[] decodeHex(final char[] data) throws DecoderException {
090
091        final var len = data.length;
092
093        if ((len & 0x01) != 0) {
094            throw new DecoderException("Odd number of characters.");
095        }
096
097        final var out = new byte[len >> 1];
098
099        // two characters form the hex value.
100        for (int i = 0, j = 0; j < len; i++) {
101            var f = toDigit(data[j], j) << 4;
102            j++;
103            f = f | toDigit(data[j], j);
104            j++;
105            out[i] = (byte) (f & 0xFF);
106        }
107
108        return out;
109    }
110
111    /**
112     * Converts an array of bytes into an array of characters representing the
113     * hexadecimal values of each byte in order. The returned array will be double
114     * the length of the passed array, as it takes two characters to represent any
115     * given byte.
116     *
117     * @param data a byte[] to convert to Hex characters
118     * @return A char[] containing lower-case hexadecimal characters
119     */
120    public static char[] encodeHex(final byte[] data) {
121        return encodeHex(data, true);
122    }
123
124    /**
125     * Converts a byte buffer into an array of characters representing the
126     * hexadecimal values of each byte in order. The returned array will be double
127     * the length of the passed array, as it takes two characters to represent any
128     * given byte.
129     *
130     * <p>
131     * All bytes identified by {@link ByteBuffer#remaining()} will be used; after
132     * this method the value {@link ByteBuffer#remaining() remaining()} will be
133     * zero.
134     * </p>
135     *
136     * @param data a byte buffer to convert to Hex characters
137     * @return A char[] containing lower-case hexadecimal characters
138     */
139    public static char[] encodeHex(final ByteBuffer data) {
140        return encodeHex(data, true);
141    }
142
143    /**
144     * Converts an array of bytes into an array of characters representing the
145     * hexadecimal values of each byte in order. The returned array will be double
146     * the length of the passed array, as it takes two characters to represent any
147     * given byte.
148     *
149     * @param data        a byte[] to convert to Hex characters
150     * @param toLowerCase {@code true} converts to lowercase, {@code false} to
151     *                    uppercase
152     * @return A char[] containing hexadecimal characters in the selected case
153     *
154     */
155    public static char[] encodeHex(final byte[] data, final boolean toLowerCase) {
156        return encodeHex(data, toLowerCase ? DIGITS_LOWER : DIGITS_UPPER);
157    }
158
159    /**
160     * Converts a byte buffer into an array of characters representing the
161     * hexadecimal values of each byte in order. The returned array will be double
162     * the length of the passed array, as it takes two characters to represent any
163     * given byte.
164     *
165     * <p>
166     * All bytes identified by {@link ByteBuffer#remaining()} will be used; after
167     * this method the value {@link ByteBuffer#remaining() remaining()} will be
168     * zero.
169     * </p>
170     *
171     * @param data        a byte buffer to convert to Hex characters
172     * @param toLowerCase {@code true} converts to lowercase, {@code false} to
173     *                    uppercase
174     * @return A char[] containing hexadecimal characters in the selected case
175     */
176    public static char[] encodeHex(final ByteBuffer data, final boolean toLowerCase) {
177        return encodeHex(data, toLowerCase ? DIGITS_LOWER : DIGITS_UPPER);
178    }
179
180    /**
181     * Converts an array of bytes into an array of characters representing the
182     * hexadecimal values of each byte in order. The returned array will be double
183     * the length of the passed array, as it takes two characters to represent any
184     * given byte.
185     *
186     * @param data     a byte[] to convert to Hex characters
187     * @param toDigits the output alphabet (must contain at least 16 chars)
188     * @return A char[] containing the appropriate characters from the alphabet For
189     *         best results, this should be either upper- or lower-case hex.
190     *
191     */
192    @SuppressWarnings("squid:ForLoopCounterChangedCheck") // owolff: original code
193    protected static char[] encodeHex(final byte[] data, final char[] toDigits) {
194        final var l = data.length;
195        final var out = new char[l << 1];
196        // two characters form the hex value.
197        for (int i = 0, j = 0; i < l; i++) {
198            out[j] = toDigits[(0xF0 & data[i]) >>> 4];
199            j++;
200            out[j] = toDigits[0x0F & data[i]];
201            j++;
202        }
203        return out;
204    }
205
206    /**
207     * Converts a byte buffer into an array of characters representing the
208     * hexadecimal values of each byte in order. The returned array will be double
209     * the length of the passed array, as it takes two characters to represent any
210     * given byte.
211     *
212     * <p>
213     * All bytes identified by {@link ByteBuffer#remaining()} will be used; after
214     * this method the value {@link ByteBuffer#remaining() remaining()} will be
215     * zero.
216     * </p>
217     *
218     * @param byteBuffer a byte buffer to convert to Hex characters
219     * @param toDigits   the output alphabet (must be at least 16 characters)
220     * @return A char[] containing the appropriate characters from the alphabet For
221     *         best results, this should be either upper- or lower-case hex.
222     *
223     */
224    protected static char[] encodeHex(final ByteBuffer byteBuffer, final char[] toDigits) {
225        return encodeHex(toByteArray(byteBuffer), toDigits);
226    }
227
228    /**
229     * Converts an array of bytes into a String representing the hexadecimal values
230     * of each byte in order. The returned String will be double the length of the
231     * passed array, as it takes two characters to represent any given byte.
232     *
233     * @param data a byte[] to convert to Hex characters
234     * @return A String containing lower-case hexadecimal characters
235     *
236     */
237    public static String encodeHexString(final byte[] data) {
238        return new String(encodeHex(data));
239    }
240
241    /**
242     * Converts an array of bytes into a String representing the hexadecimal values
243     * of each byte in order. The returned String will be double the length of the
244     * passed array, as it takes two characters to represent any given byte.
245     *
246     * @param data        a byte[] to convert to Hex characters
247     * @param toLowerCase {@code true} converts to lowercase, {@code false} to
248     *                    uppercase
249     * @return A String containing lower-case hexadecimal characters
250     *
251     */
252    public static String encodeHexString(final byte[] data, final boolean toLowerCase) {
253        return new String(encodeHex(data, toLowerCase));
254    }
255
256    /**
257     * Converts a byte buffer into a String representing the hexadecimal values of
258     * each byte in order. The returned String will be double the length of the
259     * passed array, as it takes two characters to represent any given byte.
260     *
261     * <p>
262     * All bytes identified by {@link ByteBuffer#remaining()} will be used; after
263     * this method the value {@link ByteBuffer#remaining() remaining()} will be
264     * zero.
265     * </p>
266     *
267     * @param data a byte buffer to convert to Hex characters
268     * @return A String containing lower-case hexadecimal characters
269     *
270     */
271    public static String encodeHexString(final ByteBuffer data) {
272        return new String(encodeHex(data));
273    }
274
275    /**
276     * Converts a byte buffer into a String representing the hexadecimal values of
277     * each byte in order. The returned String will be double the length of the
278     * passed array, as it takes two characters to represent any given byte.
279     *
280     * <p>
281     * All bytes identified by {@link ByteBuffer#remaining()} will be used; after
282     * this method the value {@link ByteBuffer#remaining() remaining()} will be
283     * zero.
284     * </p>
285     *
286     * @param data        a byte buffer to convert to Hex characters
287     * @param toLowerCase {@code true} converts to lowercase, {@code false} to
288     *                    uppercase
289     * @return A String containing lower-case hexadecimal characters
290     *
291     */
292    public static String encodeHexString(final ByteBuffer data, final boolean toLowerCase) {
293        return new String(encodeHex(data, toLowerCase));
294    }
295
296    /**
297     * Convert the byte buffer to a byte array. All bytes identified by
298     * {@link ByteBuffer#remaining()} will be used.
299     *
300     * @param byteBuffer the byte buffer
301     * @return the byte[]
302     */
303    private static byte[] toByteArray(final ByteBuffer byteBuffer) {
304        final var remaining = byteBuffer.remaining();
305        // Use the underlying buffer if possible
306        if (byteBuffer.hasArray()) {
307            final var byteArray = byteBuffer.array();
308            if (remaining == byteArray.length) {
309                byteBuffer.position(remaining);
310                return byteArray;
311            }
312        }
313        // Copy the bytes
314        final var byteArray = new byte[remaining];
315        byteBuffer.get(byteArray);
316        return byteArray;
317    }
318
319    /**
320     * Converts a hexadecimal character to an integer.
321     *
322     * @param ch    A character to convert to an integer digit
323     * @param index The index of the character in the source
324     * @return An integer
325     * @throws DecoderException Thrown if ch is an illegal hex character
326     */
327    protected static int toDigit(final char ch, final int index) throws DecoderException {
328        final var digit = Character.digit(ch, 16);
329        if (digit == -1) {
330            throw new DecoderException("Illegal hexadecimal character " + ch + " at index " + index);
331        }
332        return digit;
333    }
334
335    @Getter
336    private final Charset charset;
337
338    /**
339     * Creates a new codec with the default charset name {@link #DEFAULT_CHARSET}
340     */
341    public Hex() {
342        // use default encoding
343        charset = DEFAULT_CHARSET;
344    }
345
346    /**
347     * Creates a new codec with the given Charset.
348     *
349     * @param charset the charset.
350     *
351     */
352    public Hex(final Charset charset) {
353        this.charset = charset;
354    }
355
356    /**
357     * Creates a new codec with the given charset name.
358     *
359     * @param charsetName the charset name.
360     * @throws java.nio.charset.UnsupportedCharsetException If the named charset is
361     *                                                      unavailable
362     *
363     *                                                      throws
364     *                                                      UnsupportedCharsetException
365     *                                                      if the named charset is
366     *                                                      unavailable
367     */
368    public Hex(final String charsetName) {
369        this(Charset.forName(charsetName));
370    }
371
372    /**
373     * Converts an array of character bytes representing hexadecimal values into an
374     * array of bytes of those same values. The returned array will be half the
375     * length of the passed array, as it takes two characters to represent any given
376     * byte. An exception is thrown if the passed char array has an odd number of
377     * elements.
378     *
379     * @param array An array of character bytes containing hexadecimal digits
380     * @return A byte array containing binary data decoded from the supplied byte
381     *         array (representing characters).
382     * @throws DecoderException Thrown if an odd number of characters is supplied to
383     *                          this function
384     * @see #decodeHex(char[])
385     */
386    public byte[] decode(final byte[] array) throws DecoderException {
387        return decodeHex(new String(array, getCharset()).toCharArray());
388    }
389
390    /**
391     * Converts a buffer of character bytes representing hexadecimal values into an
392     * array of bytes of those same values. The returned array will be half the
393     * length of the passed array, as it takes two characters to represent any given
394     * byte. An exception is thrown if the passed char array has an odd number of
395     * elements.
396     *
397     * <p>
398     * All bytes identified by {@link ByteBuffer#remaining()} will be used; after
399     * this method the value {@link ByteBuffer#remaining() remaining()} will be
400     * zero.
401     * </p>
402     *
403     * @param buffer An array of character bytes containing hexadecimal digits
404     * @return A byte array containing binary data decoded from the supplied byte
405     *         array (representing characters).
406     * @throws DecoderException Thrown if an odd number of characters is supplied to
407     *                          this function
408     * @see #decodeHex(char[])
409     *
410     */
411    public byte[] decode(final ByteBuffer buffer) throws DecoderException {
412        return decodeHex(new String(toByteArray(buffer), getCharset()).toCharArray());
413    }
414
415    /**
416     * Converts a String or an array of character bytes representing hexadecimal
417     * values into an array of bytes of those same values. The returned array will
418     * be half the length of the passed String or array, as it takes two characters
419     * to represent any given byte. An exception is thrown if the passed char array
420     * has an odd number of elements.
421     *
422     * @param object A String, ByteBuffer, byte[], or an array of character bytes
423     *               containing hexadecimal digits
424     * @return A byte array containing binary data decoded from the supplied byte
425     *         array (representing characters).
426     * @throws DecoderException Thrown if an odd number of characters is supplied to
427     *                          this function or the object is not a String or
428     *                          char[]
429     * @see #decodeHex(char[])
430     */
431    public Object decode(final Object object) throws DecoderException {
432        if (object instanceof String string) {
433            return decode(string.toCharArray());
434        }
435        if (object instanceof byte[] bytes) {
436            return decode(bytes);
437        }
438        if (object instanceof ByteBuffer buffer) {
439            return decode(buffer);
440        }
441        try {
442            return decodeHex((char[]) object);
443        } catch (final ClassCastException e) {
444            throw new DecoderException(e.getMessage(), e);
445        }
446    }
447
448    /**
449     * Converts an array of bytes into an array of bytes for the characters
450     * representing the hexadecimal values of each byte in order. The returned array
451     * will be double the length of the passed array, as it takes two characters to
452     * represent any given byte.
453     * <p>
454     * The conversion from hexadecimal characters to the returned bytes is performed
455     * with the charset named by {@link #getCharset()}.
456     * </p>
457     *
458     * @param array a byte[] to convert to Hex characters
459     * @return A byte[] containing the bytes of the lower-case hexadecimal
460     *         characters No longer throws IllegalStateException if the charsetName
461     *         is invalid.
462     * @see #encodeHex(byte[])
463     */
464    public byte[] encode(final byte[] array) {
465        return encodeHexString(array).getBytes(getCharset());
466    }
467
468    /**
469     * Converts byte buffer into an array of bytes for the characters representing
470     * the hexadecimal values of each byte in order. The returned array will be
471     * double the length of the passed array, as it takes two characters to
472     * represent any given byte.
473     *
474     * <p>
475     * The conversion from hexadecimal characters to the returned bytes is performed
476     * with the charset named by {@link #getCharset()}.
477     * </p>
478     *
479     * <p>
480     * All bytes identified by {@link ByteBuffer#remaining()} will be used; after
481     * this method the value {@link ByteBuffer#remaining() remaining()} will be
482     * zero.
483     * </p>
484     *
485     * @param array a byte buffer to convert to Hex characters
486     * @return A byte[] containing the bytes of the lower-case hexadecimal
487     *         characters
488     * @see #encodeHex(byte[])
489     */
490    public byte[] encode(final ByteBuffer array) {
491        return encodeHexString(array).getBytes(getCharset());
492    }
493
494    /**
495     * Converts a String or an array of bytes into an array of characters
496     * representing the hexadecimal values of each byte in order. The returned array
497     * will be double the length of the passed String or array, as it takes two
498     * characters to represent any given byte.
499     * <p>
500     * The conversion from hexadecimal characters to bytes to be encoded to
501     * performed with the charset named by {@link #getCharset()}.
502     * </p>
503     *
504     * @param object a String, ByteBuffer, or byte[] to convert to Hex characters
505     * @return A char[] containing lower-case hexadecimal characters
506     * @throws EncoderException Thrown if the given object is not a String or byte[]
507     * @see #encodeHex(byte[])
508     */
509    public Object encode(final Object object) throws EncoderException {
510        byte[] byteArray;
511        if (object instanceof String string) {
512            byteArray = string.getBytes(getCharset());
513        } else if (object instanceof ByteBuffer buffer) {
514            byteArray = toByteArray(buffer);
515        } else {
516            try {
517                byteArray = (byte[]) object;
518            } catch (final ClassCastException e) {
519                throw new EncoderException(e.getMessage(), e);
520            }
521        }
522        return encodeHex(byteArray);
523    }
524
525}