/* This file is part of VoltDB.
 * Copyright (C) 2008-2020 VoltDB Inc.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with VoltDB.  If not, see <http://www.gnu.org/licenses/>.
 */

package org.voltdb.utils;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.util.zip.DeflaterOutputStream;
import java.util.zip.InflaterInputStream;

import org.voltdb.common.Constants;

/**
 * Encode and decode strings and byte arrays to/from hexidecimal
 * string values. This was originally added so binary values could
 * be added to the VoltDB catalogs.
 *
 */
public class Encoder {
    private static final int caseDiff = ('a' - 'A');
    /**
     *
     * @param data A binary array of bytes.
     * @return A hex-encoded string with double length.
     */
    public static String hexEncode(byte[] data) {
        return hexEncode(data, false);
    }

    public static String hexEncode(byte[] data, boolean quote) {
        if (data == null)
            return null;

        StringBuilder sb = new StringBuilder(quote ? "x'" : "");
        for (byte b : data) {
            // hex encoding same way as java.net.URLEncoder.
            char ch = Character.forDigit((b >> 4) & 0xF, 16);
            // to uppercase
            if (Character.isLetter(ch)) {
                ch -= caseDiff;
            }
            sb.append(ch);
            ch = Character.forDigit(b & 0xF, 16);
            if (Character.isLetter(ch)) {
                ch -= caseDiff;
            }
            sb.append(ch);
        }
        if (quote)
            sb.append('\'');
        return sb.toString();
    }

    /**
     *
     * @param string A string to be hex encoded.
     * @return The double-length hex encoded string.
     */
    public static String hexEncode(String string) {
        // this will need to be less "western" in the future
        return hexEncode(string.getBytes(Constants.UTF8ENCODING));
    }

    /**
     *
     * @param hexString An (even-length) hexadecimal string to be decoded.
     * @return The binary byte array value for the string (half length).
     */
    public static byte[] hexDecode(String hexString) {
        byte[] retval = null;
        final String errorText = "String is not properly hex-encoded.";

        if ((hexString.length() % 2) != 0)
            throw new RuntimeException(errorText);

        try {
            retval = new byte[hexString.length() / 2];
            for (int i = 0; i < retval.length; i++) {
                int value = Integer.parseInt(hexString.substring(2 * i, 2 * i + 2), 16);
                retval[i] = (byte) value;
            }
        }
        catch (IllegalArgumentException exc) {
            // parseInt can throw a NumberFormatException, which is a subclass of
            // IllegalArgumentException, so both kinds of failure come here.
            throw new RuntimeException(errorText);
        }
        return retval;
    }

    /**
     *
     * @param hexString A string of hexidecimal chars of even length.
     * @return A string value de-hexed from the input.
     */
    public static String hexDecodeToString(String hexString) {
        byte[] decodedValue = hexDecode(hexString);
        return new String(decodedValue, Constants.UTF8ENCODING);
    }

    public static boolean isHexEncodedString(String hexString) {
        if ((hexString.length() % 2) != 0)
            return false;
        try {
            for (int i = 0; i < hexString.length(); i++) {
                int value = Integer.parseInt(hexString.substring(i, i + 1), 16);
                if ((value < 0) || (value > 15)) return false;
            }
        } catch (NumberFormatException e) {
            return false;
        }
        return true;
    }

    public static String compressAndBase64Encode(String string) {
        try {
            byte[] inBytes = string.getBytes(Constants.UTF8ENCODING);
            ByteArrayOutputStream baos = new ByteArrayOutputStream((int)(string.length() * 0.7));
            DeflaterOutputStream dos = new DeflaterOutputStream(baos);
            dos.write(inBytes);
            dos.close();
            byte[] outBytes = baos.toByteArray();
            return Base64.encodeToString(outBytes, false);
        }
        catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    public static String compressAndBase64Encode(byte[] bytes) {
        try {
            ByteArrayOutputStream baos = new ByteArrayOutputStream(bytes.length);
            DeflaterOutputStream dos = new DeflaterOutputStream(baos);
            dos.write(bytes);
            dos.close();
            byte[] outBytes = baos.toByteArray();
            return Base64.encodeToString(outBytes, false);
        }
        catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    public static String decodeBase64AndDecompress(String string) {
        if (string.length() == 0) {
            return "";
        }
        byte bytes[] = decodeBase64AndDecompressToBytes(string);
        return new String(bytes, Constants.UTF8ENCODING);
    }

    public static byte[] decodeBase64AndDecompressToBytes(String string) {
        byte bytes[] = Base64.decodeFast(string);
        if (string.length() == 0) {
            return new byte[0];
        }
        ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
        InflaterInputStream dis = new InflaterInputStream(bais);

        byte buffer[] = new byte[1024 * 8];
        int length = 0;
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        try {
            while ( (length = dis.read( buffer )) >= 0) {
                baos.write(buffer, 0, length);
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        return baos.toByteArray();
    }

    public static String base64Encode(String string) {
        try {
            final byte[] inBytes = string.getBytes(Constants.UTF8ENCODING);
            return Base64.encodeToString(inBytes, false);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    public static String base64Encode(byte[] bytes) {
        return Base64.encodeToString(bytes, false);
    }

    public static byte[] base64Decode(String string) {
        return Base64.decodeFast(string);
    }

    public static String base64DecodeToString(String string) {
        byte[] decodedValue = base64Decode(string);
        return new String(decodedValue, Constants.UTF8ENCODING);
    }
}

@@ -1390,119 +1390,8 @@
 ) %7B%0A
-        return hexEncode(data, false);%0A    %7D%0A%0A    public static String hexEncode(byte%5B%5D data, boolean quote) %7B%0A
     
@@ -1487,25 +1487,8 @@
 der(
-quote ? %22x'%22 : %22%22
 );%0A 
@@ -1961,56 +1961,8 @@
   %7D%0A
-        if (quote)%0A            sb.append('%5C'');%0A
     

Revert "Encoder: Update encode so it can quote the hex string"