/* See LICENSE for licensing and NOTICE for copyright. */
package org.cryptacular.util;

import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.util.regex.Pattern;
import org.cryptacular.CryptUtil;
import org.cryptacular.codec.Base64Decoder;
import org.cryptacular.pem.Constants;

/**
 * Utility class with helper methods for common PEM encoding operations.
 *
 * @author  Middleware Services
 */
public final class PemUtil
{

  /** Pattern used to split multiple PEM-encoded objects in a single file. */
  private static final Pattern PEM_SPLITTER = Pattern.compile("-----(?:BEGIN|END) [A-Z ]+-----");

  /** Pattern used to a file by line terminator. */
  private static final Pattern LINE_SPLITTER = Pattern.compile("[\r\n]+");


  /** Private constructor of utility class. */
  private PemUtil() {}


  /**
   * Determines whether the data in the given byte array is base64-encoded data of PEM encoding. The determination is
   * made using as little data from the given array as necessary to make a reasonable determination about encoding.
   *
   * @param  data  Data to test for PEM encoding
   *
   * @return  True if data appears to be PEM encoded, false otherwise.
   */
  public static boolean isPem(final byte[] data)
  {
    CryptUtil.assertNotNullArgOr(
      data,
      v -> v.length < Constants.RFC1421_MAX_LINE_LENGTH,
      "Data must be at least " + Constants.RFC1421_MAX_LINE_LENGTH + " bytes long");
    final String start = new String(data, 0, 10, ByteUtil.ASCII_CHARSET).trim();
    if (!start.startsWith(Constants.RFC1421_ENCAPSULATION_BEGIN_MARKER) &&
        !start.startsWith(Constants.RFC4716_ENCAPSULATION_BEGIN_MARKER) &&
        !start.startsWith(Constants.RFC1421_HEADER_FIELD_PROC_TYPE))
    {
      // Check all bytes in first line to make sure they are in the range
      // of base64 character set encoding
      for (int i = 0; i < Constants.RFC7468_MAX_LINE_LENGTH; i++) {
        if (!isBase64Char(data[i])) {
          // Last two bytes may be padding character '=' (61)
          if (i > Constants.RFC7468_MAX_LINE_LENGTH - 3) {
            if (data[i] != 61) {
              return false;
            }
          } else {
            return false;
          }
        }
      }
    }
    return true;
  }


  /**
   * Determines whether the given byte represents an ASCII character in the character set for base64 encoding.
   *
   * @param  b  Byte to test.
   *
   * @return  True if the byte represents an ASCII character in the set of valid characters for base64 encoding, false
   *          otherwise. The padding character '=' is not considered valid since it may only appear at the end of a
   *          base64 encoded value.
   */
  public static boolean isBase64Char(final byte b)
  {
    return !(b < 47 || b > 122 || b > 57 && b < 65 || b > 90 && b < 97) || b == 43;
  }


  /**
   * Decodes a PEM-encoded cryptographic object into the raw bytes of its ASN.1 encoding. Header/footer data and
   * metadata info, e.g. Proc-Type, are ignored.
   *
   * @param  pem  Bytes of PEM-encoded data to decode.
   *
   * @return  ASN.1 encoded bytes.
   */
  public static byte[] decode(final byte[] pem)
  {
    return decode(new String(CryptUtil.assertNotNullArg(pem, "PEM cannot be null"), ByteUtil.ASCII_CHARSET));
  }


  /**
   * Decodes one or more PEM-encoded cryptographic objects into the raw bytes of their ASN.1 encoding. All header and
   * metadata, e.g. Proc-Type, are ignored. If multiple cryptographic objects are represented, the decoded bytes of
   * each object are concatenated together and returned.
   *
   * @param  pem  PEM-encoded data to decode.
   *
   * @return  ASN.1 encoded bytes.
   */
  public static byte[] decode(final String pem)
  {
    CryptUtil.assertNotNullArg(pem, "PEM cannot be null");
    final Base64Decoder decoder = new Base64Decoder();
    final CharBuffer buffer = CharBuffer.allocate(pem.length());
    final ByteBuffer output = ByteBuffer.allocate(pem.length() * 3 / 4);
    // There may be multiple PEM-encoded objects in the input
    for (String object : PEM_SPLITTER.split(pem)) {
      buffer.clear();
      for (String line : LINE_SPLITTER.split(object)) {
        if (line.startsWith(Constants.RFC1421_HEADER_FIELD_DEK_INFO) ||
            line.startsWith(Constants.RFC1421_HEADER_FIELD_PROC_TYPE))
        {
          continue;
        }
        buffer.append(line);
      }
      buffer.flip();
      decoder.decode(buffer, output);
      decoder.finalize(output);
    }
    output.flip();
    return ByteUtil.toArray(output);
  }
}
