/*
 *
 *  Multimedia Messaging Service Daemon - The Next Generation
 *
 *  Copyright (C) 2010-2011, Intel Corporation
 *                2021, Chris Talbot <chris@talbothome.com>
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License version 2 as
 *  published by the Free Software Foundation.
 *
 *  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 General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program; if not, write to the Free Software
 *  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 *
 */

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

#include <stdio.h>
#include <sys/stat.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>

#include <glib.h>
#include <glib/gstdio.h>

#include "mms.h"

#ifdef TEMP_FAILURE_RETRY
#define TFR TEMP_FAILURE_RETRY
#else
#define TFR
#endif

static int
create_dirs (const char  *filename,
             const mode_t mode)
{
  struct stat st;
  char *dir;
  const char *prev, *next;
  int err;

  if (filename[0] != '/')
    return -1;

  err = stat (filename, &st);
  if (!err && S_ISREG (st.st_mode))
    return 0;

  dir = g_try_malloc (strlen (filename) + 1);
  if (dir == NULL)
    return -1;

  strcpy (dir, "/");

  for (prev = filename; (next = strchr (prev + 1, '/')); prev = next)
    {
      /* Skip consecutive '/' characters */
      if (next - prev == 1)
        continue;

      strncat (dir, prev + 1, next - prev);

      if (mkdir (dir, mode) == -1 && errno != EEXIST)
        {
          g_free (dir);
          return -1;
        }
    }

  g_free (dir);
  return 0;
}


static char *
generate_uuid_objpath (void)
{
  char *uuid, *reformatted;

  uuid = g_uuid_string_random ();

  reformatted = g_strdelimit (g_strdup (uuid), "-", '1');
  g_free (uuid);

  return reformatted;
}

char *
mms_store_get_path (const char *service_id,
                    const char *uuid)
{
  const char *homedir;

  homedir = g_get_home_dir ();
  if (homedir == NULL)
    return NULL;

  return g_strdup_printf ("%s/.mms/%s/%s", homedir, service_id, uuid);
}

static char *
generate_pdu_pathname (const char *service_id,
                       const char *uuid)
{
  char *pathname;

  pathname = mms_store_get_path (service_id, uuid);
  if (pathname == NULL)
    return NULL;

  if (create_dirs (pathname, S_IRUSR | S_IWUSR | S_IXUSR) != 0)
    {
      g_critical ("Failed to create path %s", pathname);

      g_free (pathname);
      return NULL;
    }

  return pathname;
}

/*
 * Write a buffer to a file in a transactionally safe form
 *
 * Given a buffer, write it to a file named after
 * @filename. However, to make sure the file contents are
 * consistent (ie: a crash right after opening or during write()
 * doesn't leave a file half baked), the contents are written to a
 * file with a temporary name and when closed, it is renamed to the
 * specified name (@filename).
 */
static ssize_t
write_file (const unsigned char *buffer,
            size_t               len,
            const char          *filename)
{
  char *tmp_file;
  ssize_t written;
  int fd;

  tmp_file = g_strdup_printf ("%s.XXXXXX.tmp", filename);

  written = -1;

  fd = TFR (g_mkstemp_full (tmp_file, O_WRONLY | O_CREAT | O_TRUNC,
                            S_IWUSR | S_IRUSR));
  if (fd < 0)
    goto error_mkstemp_full;

  written = TFR (write (fd, buffer, len));

  TFR (fdatasync (fd));

  TFR (close (fd));

  if (written != (ssize_t) len)
    {
      written = -1;
      goto error_write;
    }

  /*
   * Now that the file contents are written, rename to the real
   * file name; this way we are uniquely sure that the whole
   * thing is there.
   */
  unlink (filename);

  /* conserve @written's value from 'write' */
  if (link (tmp_file, filename) == -1)
    written = -1;

 error_write:
  unlink (tmp_file);

 error_mkstemp_full:
  g_free (tmp_file);

  return written;
}

char *
mms_store (const char          *service_id,
           const unsigned char *pdu,
           unsigned int         len)
{
  char *pathname;
  char *uuid;

  uuid = generate_uuid_objpath ();
  if (uuid == NULL)
    return NULL;

  pathname = generate_pdu_pathname (service_id, uuid);
  if (pathname == NULL)
    return NULL;

  if (write_file (pdu, len, pathname) < 0)
    {
      g_critical ("Failed to write to %s", pathname);

      uuid = NULL;
    }

  DBG ("pathname %s", pathname);

  g_free (pathname);

  return uuid;
}

char *
mms_store_file (const char *service_id,
                const char *path)
{
  char *pathname;
  char *uuid;
  int fd;
  struct stat st;
  unsigned char *pdu;

  fd = open (path, O_RDONLY);
  if (fd < 0)
    {
      g_critical ("Failed to open %s\n", path);
      return NULL;
    }

  if (fstat (fd, &st) < 0)
    {
      g_critical ("Failed to fstat %s\n", path);
      close (fd);
      return NULL;
    }

  pdu = mmap (NULL, st.st_size, PROT_READ, MAP_SHARED, fd, 0);
  if (pdu == NULL || pdu == MAP_FAILED)
    {
      g_critical ("Failed to mmap %s\n", path);
      close (fd);
      return NULL;
    }

  uuid = generate_uuid_objpath ();

  munmap (pdu, st.st_size);

  close (fd);

  if (uuid == NULL)
    return NULL;

  pathname = generate_pdu_pathname (service_id, uuid);
  if (pathname == NULL)
    return NULL;

  if (g_rename (path, pathname) < 0)
    {
      g_critical ("Failed to rename %s to %s\n", path, pathname);

      uuid = NULL;
    }

  DBG ("pathname %s", pathname);

  g_free (pathname);

  return uuid;
}

void
mms_store_remove (const char *service_id,
                  const char *uuid)
{
  char *pdu_path;
  char *meta_path;

  pdu_path = mms_store_get_path (service_id, uuid);
  if (pdu_path == NULL)
    return;

  unlink (pdu_path);

  meta_path = g_strdup_printf ("%s%s", pdu_path, MMS_META_UUID_SUFFIX);

  g_free (pdu_path);

  unlink (meta_path);

  g_free (meta_path);
}

GKeyFile *
mms_store_meta_open (const char *service_id,
                     const char *uuid)
{
  GKeyFile *keyfile;
  g_autofree char *pdu_path = NULL;
  g_autofree char *meta_path = NULL;

  pdu_path = generate_pdu_pathname (service_id, uuid);
  if (pdu_path == NULL)
    return NULL;

  meta_path = g_strdup_printf ("%s%s", pdu_path, MMS_META_UUID_SUFFIX);

  keyfile = g_key_file_new ();

  g_key_file_load_from_file (keyfile, meta_path, 0, NULL);

  return keyfile;
}

static void
meta_store_sync (const char *service_id,
                 const char *uuid,
                 GKeyFile   *keyfile)
{
  char *data;
  gsize length = 0;
  char *pdu_path;
  char *meta_path;

  pdu_path = mms_store_get_path (service_id, uuid);
  if (pdu_path == NULL)
    return;

  meta_path = g_strdup_printf ("%s%s", pdu_path, MMS_META_UUID_SUFFIX);

  g_free (pdu_path);

  data = g_key_file_to_data (keyfile, &length, NULL);

  g_file_set_contents (meta_path, data, length, NULL);

  g_free (data);

  g_free (meta_path);
}

void
mms_store_meta_close (const char *service_id,
                      const char *uuid,
                      GKeyFile   *keyfile,
                      gboolean    save)
{
  if (save == TRUE)
    meta_store_sync (service_id, uuid, keyfile);

  g_key_file_free (keyfile);
}

GKeyFile *
mms_settings_open (const char *service_id,
                   const char *store)
{
  GKeyFile *keyfile;
  char *path;

  if (store == NULL)
    return NULL;

  path = mms_store_get_path (service_id, store);
  if (path == NULL)
    return NULL;

  if (create_dirs (path, S_IRUSR | S_IWUSR | S_IXUSR) != 0)
    {
      g_critical ("Failed to create path %s", path);

      g_free (path);
      return NULL;
    }

  keyfile = g_key_file_new ();

  g_key_file_load_from_file (keyfile, path, 0, NULL);

  g_free (path);

  return keyfile;
}

void
mms_settings_sync (const char *service_id,
                   const char *store,
                   GKeyFile   *keyfile)
{
  char *path;
  char *data;
  gsize length = 0;

  path = mms_store_get_path (service_id, store);
  if (path == NULL)
    return;

  data = g_key_file_to_data (keyfile, &length, NULL);

  g_file_set_contents (path, data, length, NULL);

  g_free (data);
  g_free (path);
}

void
mms_settings_close (const char *service_id,
                    const char *store,
                    GKeyFile   *keyfile,
                    gboolean    save)
{
  if (save == TRUE)
    mms_settings_sync (service_id, store, keyfile);

  g_key_file_free (keyfile);
}
