/*
 * Copyright (C) 2011 Colin Walters <walters@verbum.org>
 *
 * SPDX-License-Identifier: LGPL-2.0+
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2 of the License, or (at your option) any later version.
 *
 * This library 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library. If not, see <https://www.gnu.org/licenses/>.
 *
 * Author: Colin Walters <walters@verbum.org>
 */

#include "config.h"

#include "otutil.h"
#if defined(HAVE_OPENSSL)
#include <openssl/evp.h>
#elif defined(HAVE_GNUTLS)
#include <gnutls/gnutls.h>
#include <gnutls/crypto.h>
#endif

#include <string.h>

void
ot_bin2hex (char *out_buf, const guint8 *inbuf, gsize len)
{
  static const gchar hexchars[] = "0123456789abcdef";
  guint i, j;

  for (i = 0, j = 0; i < len; i++, j += 2)
    {
      guchar byte = inbuf[i];
      out_buf[j] = hexchars[byte >> 4];
      out_buf[j+1] = hexchars[byte & 0xF];
    }
  out_buf[j] = '\0';
}

/* I like to think of this as AbstractChecksumProxyFactoryBean. In homage to
 * https://news.ycombinator.com/item?id=4549544
 * aka http://static.springsource.org/spring/docs/2.5.x/api/org/springframework/aop/framework/AbstractSingletonProxyFactoryBean.html
 */
typedef struct {
  gboolean initialized;
  gboolean closed;
#if defined(HAVE_OPENSSL)
  EVP_MD_CTX *checksum;
#elif defined(HAVE_GNUTLS)
  gnutls_hash_hd_t checksum;
#else
  GChecksum *checksum;
#endif
  guint digest_len;
} OtRealChecksum;

G_STATIC_ASSERT (sizeof (OtChecksum) >= sizeof (OtRealChecksum));

void
ot_checksum_init (OtChecksum *checksum)
{
  OtRealChecksum *real = (OtRealChecksum*)checksum;
  g_return_if_fail (!real->initialized);
#if defined(HAVE_OPENSSL)
  real->checksum = EVP_MD_CTX_create ();
  g_assert (real->checksum);
  g_assert (EVP_DigestInit_ex (real->checksum, EVP_sha256 (), NULL));
  real->digest_len = EVP_MD_CTX_size (real->checksum);
#elif defined(HAVE_GNUTLS)
  g_assert (!gnutls_hash_init (&real->checksum, GNUTLS_DIG_SHA256));
  real->digest_len = gnutls_hash_get_len (GNUTLS_DIG_SHA256);
#else
  real->checksum = g_checksum_new (G_CHECKSUM_SHA256);
  real->digest_len = g_checksum_type_get_length (G_CHECKSUM_SHA256);
#endif
  g_assert_cmpint (real->digest_len, ==, _OSTREE_SHA256_DIGEST_LEN);
  real->closed = FALSE;
  real->initialized = TRUE;
}

void
ot_checksum_update (OtChecksum *checksum,
                    const guint8   *buf,
                    size_t          len)
{
  OtRealChecksum *real = (OtRealChecksum*)checksum;
  g_return_if_fail (real->initialized);
  g_return_if_fail (!real->closed);
#if defined(HAVE_OPENSSL)
  g_assert (EVP_DigestUpdate (real->checksum, buf, len));
#elif defined(HAVE_GNUTLS)
  g_assert (!gnutls_hash (real->checksum, buf, len));
#else
  g_checksum_update (real->checksum, buf, len);
#endif
}

static void
ot_checksum_get_digest_internal (OtRealChecksum *real,
                                 guint8      *buf,
                                 size_t       buflen)
{
  g_return_if_fail (real->initialized);
  g_assert_cmpint (buflen, ==, _OSTREE_SHA256_DIGEST_LEN);
#if defined(HAVE_OPENSSL)
  guint digest_len = buflen;
  g_assert (EVP_DigestFinal_ex (real->checksum, buf, &digest_len));
  g_assert_cmpint (digest_len, ==, buflen);
#elif defined(HAVE_GNUTLS)
  gnutls_hash_output (real->checksum, buf);
#else
  gsize digest_len = buflen;
  g_checksum_get_digest (real->checksum, buf, &digest_len);
  g_assert_cmpint (digest_len, ==, buflen);
#endif
}

void
ot_checksum_get_digest (OtChecksum *checksum,
                        guint8      *buf,
                        size_t       buflen)
{
  OtRealChecksum *real = (OtRealChecksum*)checksum;
  ot_checksum_get_digest_internal (real, buf, buflen);
  real->closed = TRUE;
}

void
ot_checksum_get_hexdigest (OtChecksum *checksum,
                           char           *buf,
                           size_t          buflen)
{
  OtRealChecksum *real = (OtRealChecksum*)checksum;
  const guint digest_len = real->digest_len;
  guint8 digest_buf[digest_len];
  ot_checksum_get_digest (checksum, digest_buf, digest_len);
  ot_bin2hex (buf, (guint8*)digest_buf, digest_len);
}

void
ot_checksum_clear (OtChecksum *checksum)
{
  OtRealChecksum *real = (OtRealChecksum*)checksum;
  if (!real->initialized)
    return;
#if defined(HAVE_OPENSSL)
  EVP_MD_CTX_destroy (real->checksum);
#elif defined(HAVE_GNUTLS)
  gnutls_hash_deinit (real->checksum, NULL);
#else
  g_checksum_free (real->checksum);
#endif
  real->initialized = FALSE;
}

guchar *
ot_csum_from_gchecksum (GChecksum  *checksum)
{
  guchar *ret = g_malloc (32);
  gsize len = 32;

  g_checksum_get_digest (checksum, ret, &len);
  g_assert (len == 32);
  return ret;
}

gboolean
ot_gio_write_update_checksum (GOutputStream  *out,
                              gconstpointer   data,
                              gsize           len,
                              gsize          *out_bytes_written,
                              OtChecksum     *checksum,
                              GCancellable   *cancellable,
                              GError        **error)
{
  if (out)
    {
      if (!g_output_stream_write_all (out, data, len, out_bytes_written,
                                      cancellable, error))
        return FALSE;
    }
  else if (out_bytes_written)
    {
      *out_bytes_written = len;
    }

  if (checksum)
    ot_checksum_update (checksum, data, len);
  return TRUE;
}

gboolean
ot_gio_splice_update_checksum (GOutputStream  *out,
                               GInputStream   *in,
                               OtChecksum     *checksum,
                               GCancellable   *cancellable,
                               GError        **error)
{
  g_return_val_if_fail (out != NULL || checksum != NULL, FALSE);

  if (checksum != NULL)
    {
      gsize bytes_read, bytes_written;
      char buf[4096];
      do
        {
          if (!g_input_stream_read_all (in, buf, sizeof (buf), &bytes_read, cancellable, error))
            return FALSE;
          if (!ot_gio_write_update_checksum (out, buf, bytes_read, &bytes_written, checksum,
                                             cancellable, error))
            return FALSE;
        }
      while (bytes_read > 0);
    }
  else if (out != NULL)
    {
      if (g_output_stream_splice (out, in, 0, cancellable, error) < 0)
        return FALSE;
    }

  return TRUE;
}

/* Copy @in to @out, return in @out_csum the binary checksum for
 * all data read.
 */
gboolean
ot_gio_splice_get_checksum (GOutputStream  *out,
                            GInputStream   *in,
                            guchar        **out_csum,
                            GCancellable   *cancellable,
                            GError        **error)
{
  g_auto(OtChecksum) checksum = { 0, };
  ot_checksum_init (&checksum);

  if (!ot_gio_splice_update_checksum (out, in, &checksum, cancellable, error))
    return FALSE;

  guint8 digest[_OSTREE_SHA256_DIGEST_LEN];
  ot_checksum_get_digest (&checksum, digest, sizeof (digest));
  if (out_csum)
    *out_csum = g_memdup (digest, sizeof (digest));
  return TRUE;
}

char *
ot_checksum_file_at (int             dfd,
                     const char     *path,
                     GChecksumType   checksum_type,
                     GCancellable   *cancellable,
                     GError        **error)
{
  g_autoptr(GInputStream) in = NULL;
  if (!ot_openat_read_stream (dfd, path, TRUE, &in, cancellable, error))
    return FALSE;

  g_auto(OtChecksum) checksum = { 0, };
  ot_checksum_init (&checksum);
  if (!ot_gio_splice_update_checksum (NULL, in, &checksum, cancellable, error))
    return FALSE;

  char hexdigest[_OSTREE_SHA256_STRING_LEN+1];
  ot_checksum_get_hexdigest (&checksum, hexdigest, sizeof (hexdigest));
  return g_strdup (hexdigest);
}

void
ot_checksum_bytes (GBytes *data,
                   guint8 out_digest[_OSTREE_SHA256_DIGEST_LEN])
{
  g_auto(OtChecksum) hasher = { 0, };
  ot_checksum_init (&hasher);
  ot_checksum_update_bytes (&hasher, data);
  ot_checksum_get_digest (&hasher, out_digest, _OSTREE_SHA256_DIGEST_LEN);
}
