/* GIMP - The GNU Image Manipulation Program
 * Copyright (C) 1995 Spencer Kimball and Peter Mattis
 *
 * gimpinterpreterdb.c
 * (C) 2005 Manish Singh <yosh@gimp.org>
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */

/*
 * The binfmt_misc bits are derived from linux/fs/binfmt_misc.c
 * Copyright (C) 1997 Richard Günther
 */

/*
 * The sh-bang code is derived from linux/fs/binfmt_script.c
 * Copyright (C) 1996  Martin von Löwis
 * original #!-checking implemented by tytso.
 */

#include "config.h"

#include <stdlib.h>
#include <string.h>

#include <gio/gio.h>

#include "libgimpbase/gimpbase.h"

#include "plug-in-types.h"

#include "gimpinterpreterdb.h"

#include "gimp-intl.h"


#define BUFSIZE 4096


typedef struct _GimpInterpreterMagic GimpInterpreterMagic;

struct _GimpInterpreterMagic
{
  gulong    offset;
  gchar    *magic;
  gchar    *mask;
  guint     size;
  gchar    *program;
};


static void     gimp_interpreter_db_finalize         (GObject            *object);

static void     gimp_interpreter_db_load_interp_file (GimpInterpreterDB  *db,
                                                      GFile              *file);

static void     gimp_interpreter_db_add_program      (GimpInterpreterDB  *db,
                                                      GFile              *file,
                                                      gchar              *buffer);
static void     gimp_interpreter_db_add_binfmt_misc  (GimpInterpreterDB  *db,
                                                      GFile              *file,
                                                      gchar              *buffer);

static gboolean gimp_interpreter_db_add_extension    (GFile              *file,
                                                      GimpInterpreterDB  *db,
                                                      gchar             **tokens);
static gboolean gimp_interpreter_db_add_magic        (GimpInterpreterDB  *db,
                                                      gchar             **tokens);

static void     gimp_interpreter_db_clear_magics     (GimpInterpreterDB  *db);

static void     gimp_interpreter_db_resolve_programs (GimpInterpreterDB  *db);


G_DEFINE_TYPE (GimpInterpreterDB, gimp_interpreter_db, G_TYPE_OBJECT)

#define parent_class gimp_interpreter_db_parent_class


static void
gimp_interpreter_db_class_init (GimpInterpreterDBClass *class)
{
  GObjectClass *object_class = G_OBJECT_CLASS (class);

  object_class->finalize = gimp_interpreter_db_finalize;
}

static void
gimp_interpreter_db_init (GimpInterpreterDB *db)
{
}

static void
gimp_interpreter_db_finalize (GObject *object)
{
  GimpInterpreterDB *db = GIMP_INTERPRETER_DB (object);

  gimp_interpreter_db_clear (db);

  G_OBJECT_CLASS (parent_class)->finalize (object);
}

GimpInterpreterDB *
gimp_interpreter_db_new (gboolean verbose)
{
  GimpInterpreterDB *db = g_object_new (GIMP_TYPE_INTERPRETER_DB, NULL);

  db->verbose = verbose;

  return db;
}

void
gimp_interpreter_db_load (GimpInterpreterDB *db,
                          GList             *path)
{
  GList *list;

  g_return_if_fail (GIMP_IS_INTERPRETER_DB (db));

  gimp_interpreter_db_clear (db);

  db->programs = g_hash_table_new_full (g_str_hash, g_str_equal,
                                        g_free, g_free);

  db->extensions = g_hash_table_new_full (g_str_hash, g_str_equal,
                                          g_free, g_free);

  db->magic_names = g_hash_table_new_full (g_str_hash, g_str_equal,
                                           g_free, NULL);

  db->extension_names = g_hash_table_new_full (g_str_hash, g_str_equal,
                                               g_free, NULL);

  for (list = path; list; list = g_list_next (list))
    {
      GFile           *dir = list->data;
      GFileEnumerator *enumerator;

      enumerator =
        g_file_enumerate_children (dir,
                                   G_FILE_ATTRIBUTE_STANDARD_NAME ","
                                   G_FILE_ATTRIBUTE_STANDARD_IS_HIDDEN ","
                                   G_FILE_ATTRIBUTE_STANDARD_TYPE,
                                   G_FILE_QUERY_INFO_NONE,
                                   NULL, NULL);

      if (enumerator)
        {
          GFileInfo *info;

          while ((info = g_file_enumerator_next_file (enumerator,
                                                      NULL, NULL)))
            {
              if (! g_file_info_get_attribute_boolean (info, G_FILE_ATTRIBUTE_STANDARD_IS_HIDDEN) &&
                  g_file_info_get_attribute_uint32 (info, G_FILE_ATTRIBUTE_STANDARD_TYPE) == G_FILE_TYPE_REGULAR)
                {
                  GFile *file = g_file_enumerator_get_child (enumerator, info);

                  gimp_interpreter_db_load_interp_file (db, file);

                  g_object_unref (file);
                }

              g_object_unref (info);
            }

          g_object_unref (enumerator);
        }
    }

  gimp_interpreter_db_resolve_programs (db);
}

void
gimp_interpreter_db_clear (GimpInterpreterDB *db)
{
  g_return_if_fail (GIMP_IS_INTERPRETER_DB (db));

  if (db->magic_names)
    {
      g_hash_table_destroy (db->magic_names);
      db->magic_names = NULL;
    }

  if (db->extension_names)
    {
      g_hash_table_destroy (db->extension_names);
      db->extension_names = NULL;
    }

  if (db->programs)
    {
      g_hash_table_destroy (db->programs);
      db->programs = NULL;
    }

  if (db->extensions)
    {
      g_hash_table_destroy (db->extensions);
      db->extensions = NULL;
    }

  gimp_interpreter_db_clear_magics (db);
}

static void
gimp_interpreter_db_load_interp_file (GimpInterpreterDB *db,
                                      GFile             *file)
{
  GInputStream     *input;
  GDataInputStream *data_input;
  gchar            *buffer;
  gsize             buffer_len;
  GError           *error = NULL;

  if (db->verbose)
    g_print ("Parsing '%s'\n", gimp_file_get_utf8_name (file));

  input = G_INPUT_STREAM (g_file_read (file, NULL, &error));
  if (! input)
    {
      g_message (_("Could not open '%s' for reading: %s"),
                 gimp_file_get_utf8_name (file),
                 error->message);
      g_clear_error (&error);
      return;
    }

  data_input = g_data_input_stream_new (input);
  g_object_unref (input);

  while ((buffer = g_data_input_stream_read_line (data_input, &buffer_len,
                                                  NULL, &error)))
    {
      /* Skip comments */
      if (buffer[0] == '#')
        {
          g_free (buffer);
          continue;
        }

      if (g_ascii_isalnum (buffer[0]) || (buffer[0] == '/'))
        {
          gimp_interpreter_db_add_program (db, file, buffer);
        }
      else if (! g_ascii_isspace (buffer[0]) && (buffer[0] != '\0'))
        {
          gimp_interpreter_db_add_binfmt_misc (db, file, buffer);
        }

      g_free (buffer);
    }

  if (error)
    {
      g_message (_("Error reading '%s': %s"),
                 gimp_file_get_utf8_name (file),
                 error->message);
      g_clear_error (&error);
    }

  g_object_unref (data_input);
}

static void
gimp_interpreter_db_add_program (GimpInterpreterDB  *db,
                                 GFile              *file,
                                 gchar              *buffer)
{
  gchar *name;
  gchar *program;
  gchar *p;

  p = strchr (buffer, '=');
  if (! p)
    return;

  *p = '\0';

  name = buffer;
  program = p + 1;
  g_strchomp (program);

  if (! g_file_test (program, G_FILE_TEST_IS_EXECUTABLE))
    {
      gchar *prog;

      prog = g_find_program_in_path (program);
      if (! prog || ! g_file_test (prog, G_FILE_TEST_IS_EXECUTABLE))
        {
          g_message (_("Bad interpreter referenced in interpreter file %s: %s"),
                     gimp_file_get_utf8_name (file),
                     gimp_filename_to_utf8 (program));
          if (prog)
            g_free (prog);
          return;
        }
      program = prog;
    }
  else
    program = g_strdup (program);

  if (! g_hash_table_lookup (db->programs, name))
    g_hash_table_insert (db->programs, g_strdup (name), program);
}

static void
gimp_interpreter_db_add_binfmt_misc (GimpInterpreterDB *db,
                                     GFile             *file,
                                     gchar             *buffer)
{
  gchar **tokens = NULL;
  gchar  *name, *type, *program;
  gsize   count;
  gchar   del[2];

  count = strlen (buffer);

  if ((count < 10) || (count > 255))
    goto bail;

  buffer = g_strndup (buffer, count + 9);

  del[0] = *buffer;
  del[1] = '\0';

  memset (buffer + count, del[0], 8);

  tokens = g_strsplit (buffer + 1, del, -1);

  g_free (buffer);

  name    = tokens[0];
  type    = tokens[1];
  program = tokens[5];

  if ((name[0] == '\0') || (program[0] == '\0') ||
      (type[0] == '\0') || (type[1] != '\0'))
    goto bail;

  switch (type[0])
    {
    case 'E':
      if (! gimp_interpreter_db_add_extension (file, db, tokens))
        goto bail;
      break;

    case 'M':
      if (! gimp_interpreter_db_add_magic (db, tokens))
        goto bail;
      break;

    default:
      goto bail;
    }

  goto out;

bail:
  g_message (_("Bad binary format string in interpreter file %s"),
             gimp_file_get_utf8_name (file));

out:
  g_strfreev (tokens);
}

static gboolean
gimp_interpreter_db_add_extension (GFile              *file,
                                   GimpInterpreterDB  *db,
                                   gchar             **tokens)
{
  const gchar *name      = tokens[0];
  const gchar *extension = tokens[3];
  const gchar *program   = tokens[5];

  if (! g_hash_table_lookup (db->extension_names, name))
    {
      gchar *prog;

      if (extension[0] == '\0' || extension[0] == '/')
        return FALSE;

      if (! g_file_test (program, G_FILE_TEST_IS_EXECUTABLE))
        {
          prog = g_find_program_in_path (program);
          if (! prog || ! g_file_test (prog, G_FILE_TEST_IS_EXECUTABLE))
            {
              g_message (_("Bad interpreter referenced in interpreter file %s: %s"),
                         gimp_file_get_utf8_name (file),
                         gimp_filename_to_utf8 (program));
              if (prog)
                g_free (prog);
              return FALSE;
            }
        }
      else
        prog = g_strdup (program);

      g_hash_table_insert (db->extensions, g_strdup (extension), prog);
      g_hash_table_insert (db->extension_names, g_strdup (name), prog);
    }

  return TRUE;
}

static gboolean
scanarg (const gchar *s)
{
  gchar c;

  while ((c = *s++) != '\0')
    {
      if (c == '\\' && *s == 'x')
        {
          s++;

          if (! g_ascii_isxdigit (*s++))
            return FALSE;

          if (! g_ascii_isxdigit (*s++))
            return FALSE;
        }
   }

  return TRUE;
}

static guint
unquote (gchar *from)
{
  gchar *s = from;
  gchar *p = from;
  gchar  c;

  while ((c = *s++) != '\0')
    {
      if (c == '\\' && *s == 'x')
        {
          s++;
          *p = g_ascii_xdigit_value (*s++) << 4;
          *p++ |= g_ascii_xdigit_value (*s++);
          continue;
        }

      *p++ = c;
    }

  return p - from;
}

static gboolean
gimp_interpreter_db_add_magic (GimpInterpreterDB  *db,
                               gchar             **tokens)
{
  GimpInterpreterMagic *interp_magic;
  gchar                *name, *num, *magic, *mask, *program;
  gulong                offset;
  guint                 size;

  name    = tokens[0];
  num     = tokens[2];
  magic   = tokens[3];
  mask    = tokens[4];
  program = tokens[5];

  if (! g_hash_table_lookup (db->magic_names, name))
    {
      if (num[0] != '\0')
        {
          offset = strtoul (num, &num, 10);

          if (num[0] != '\0')
            return FALSE;

          if (offset > (BUFSIZE / 4))
            return FALSE;
        }
      else
        {
          offset = 0;
        }

      if (! scanarg (magic))
        return FALSE;

      if (! scanarg (mask))
        return FALSE;

      size = unquote (magic);

      if ((size + offset) > (BUFSIZE / 2))
        return FALSE;

      if (mask[0] == '\0')
        mask = NULL;
      else if (unquote (mask) != size)
        return FALSE;

      interp_magic = g_slice_new (GimpInterpreterMagic);

      interp_magic->offset  = offset;
      interp_magic->magic   = g_memdup2 (magic, size);
      interp_magic->mask    = g_memdup2 (mask, size);
      interp_magic->size    = size;
      interp_magic->program = g_strdup (program);

      db->magics = g_slist_append (db->magics, interp_magic);

      g_hash_table_insert (db->magic_names, g_strdup (name), interp_magic);
    }

  return TRUE;
}

static void
gimp_interpreter_db_clear_magics (GimpInterpreterDB *db)
{
  GimpInterpreterMagic *magic;
  GSList               *list, *last;

  list = db->magics;
  db->magics = NULL;

  while (list)
    {
      magic = list->data;

      g_free (magic->magic);
      g_free (magic->mask);
      g_free (magic->program);

      g_slice_free (GimpInterpreterMagic, magic);

      last = list;
      list = list->next;

      g_slist_free_1 (last);
    }
}

#ifdef INTERP_DEBUG
static void
print_kv (gpointer key,
          gpointer value,
          gpointer user_data)
{
  g_print ("%s: %s\n", (gchar *) key, (gchar *) value);
}

static gchar *
quote (gchar *s,
       guint  size)
{
  GString *d;
  guint    i;

  if (s == NULL)
    return "(null)";

  d = g_string_sized_new (size * 4);

  for (i = 0; i < size; i++)
    g_string_append_printf (d, "\\x%02x", ((guint) s[i]) & 0xff);

  return g_string_free (d, FALSE);
}
#endif

static gboolean
resolve_program (gpointer key,
                 gpointer value,
                 gpointer user_data)
{
  GimpInterpreterDB *db = user_data;
  gchar             *program;

  program = g_hash_table_lookup (db->programs, value);

  if (program != NULL)
    {
      g_free (value);
      value = g_strdup (program);
    }

  g_hash_table_insert (db->extensions, key, value);

  return TRUE;
}

static void
gimp_interpreter_db_resolve_programs (GimpInterpreterDB *db)
{
  GSList     *list;
  GHashTable *extensions;

  for (list = db->magics; list; list = list->next)
    {
      GimpInterpreterMagic *magic = list->data;
      const gchar          *program;

      program = g_hash_table_lookup (db->programs, magic->program);

      if (program != NULL)
        {
          g_free (magic->program);
          magic->program = g_strdup (program);
        }
    }

  extensions = db->extensions;
  db->extensions = g_hash_table_new_full (g_str_hash, g_str_equal,
                                          g_free, g_free);

  g_hash_table_foreach_steal (extensions, resolve_program, db);

  g_hash_table_destroy (extensions);

#ifdef INTERP_DEBUG
  g_print ("Programs:\n");
  g_hash_table_foreach (db->programs, print_kv, NULL);

  g_print ("\nExtensions:\n");
  g_hash_table_foreach (db->extensions, print_kv, NULL);

  g_print ("\nMagics:\n");

  list = db->magics;

  while (list)
    {
      GimpInterpreterMagic *magic;

      magic = list->data;
      g_print ("program: %s, offset: %lu, magic: %s, mask: %s\n",
               magic->program, magic->offset,
               quote (magic->magic, magic->size),
               quote (magic->mask, magic->size));

      list = list->next;
    }

  g_print ("\n");
#endif
}

static gchar *
resolve_extension (GimpInterpreterDB *db,
                   const gchar       *program_path)
{
  gchar       *filename;
  gchar       *p;
  const gchar *program;

  filename = g_path_get_basename (program_path);

  p = strrchr (filename, '.');
  if (! p)
    {
      g_free (filename);
      return NULL;
    }

  program = g_hash_table_lookup (db->extensions, p + 1);

  g_free (filename);

  return g_strdup (program);
}

static gchar *
resolve_sh_bang (GimpInterpreterDB  *db,
                 const gchar        *program_path,
                 gchar              *buffer,
                 gssize              len,
                 gchar             **interp_arg)
{
  gchar *cp;
  gchar *name;
  gchar *program;

  cp = strchr (buffer, '\n');
  if (! cp)
    cp = buffer + len - 1;

  *cp = '\0';

  while (cp > buffer)
    {
      cp--;
      if ((*cp == ' ') || (*cp == '\t') || (*cp == '\r'))
        *cp = '\0';
      else
        break;
    }

  for (cp = buffer + 2; (*cp == ' ') || (*cp == '\t'); cp++);

  if (*cp == '\0')
    return NULL;

  name = cp;

  for ( ; *cp && (*cp != ' ') && (*cp != '\t'); cp++)
    /* nothing */ ;

  while ((*cp == ' ') || (*cp == '\t'))
    *cp++ = '\0';

  if (*cp)
    {
      if (strcmp ("/usr/bin/env", name) == 0)
        {
          program = g_hash_table_lookup (db->programs, cp);

          if (program)
            {
              /* Shift program name and arguments to the right, if and
               * only if we recorded a specific interpreter for such
               * script. Otherwise let `env` tool do its job.
               */
              name = cp;

              for ( ; *cp && (*cp != ' ') && (*cp != '\t'); cp++)
                ;

              while ((*cp == ' ') || (*cp == '\t'))
                *cp++ = '\0';
            }
        }

      if (*cp)
        *interp_arg = g_strdup (cp);
    }

  program = g_hash_table_lookup (db->programs, name);
  if (! program)
    program = name;

  return g_strdup (program);
}

static gchar *
resolve_magic (GimpInterpreterDB *db,
               const gchar       *program_path,
               gchar             *buffer)
{
  GSList                *list;
  GimpInterpreterMagic  *magic;
  gchar                 *s;
  guint                  i;

  list = db->magics;

  while (list)
    {
      magic = list->data;

      s = buffer + magic->offset;

      if (magic->mask)
        {
          for (i = 0; i < magic->size; i++)
            if ((*s++ ^ magic->magic[i]) & magic->mask[i])
              break;
        }
      else
        {
          for (i = 0; i < magic->size; i++)
            if ((*s++ ^ magic->magic[i]))
              break;
        }

      if (i == magic->size)
        return g_strdup (magic->program);

      list = list->next;
    }

  return NULL;
}

gchar *
gimp_interpreter_db_resolve (GimpInterpreterDB  *db,
                             const gchar        *program_path,
                             gchar             **interp_arg)
{
  GFile        *file;
  GInputStream *input;
  gchar        *program = NULL;

  g_return_val_if_fail (GIMP_IS_INTERPRETER_DB (db), NULL);
  g_return_val_if_fail (program_path != NULL, NULL);
  g_return_val_if_fail (interp_arg != NULL, NULL);

  *interp_arg = NULL;

  file = g_file_new_for_path (program_path);
  input = G_INPUT_STREAM (g_file_read (file, NULL, NULL));
  g_object_unref (file);

  if (input)
    {
      gsize bytes_read;
      gchar buffer[BUFSIZE];

      memset (buffer, 0, sizeof (buffer));
      g_input_stream_read_all (input, buffer,
                               sizeof (buffer) - 1, /* leave one nul at the end */
                               &bytes_read, NULL, NULL);
      g_object_unref (input);

      if (bytes_read)
        {
          if (bytes_read > 3 && buffer[0] == '#' && buffer[1] == '!')
            program = resolve_sh_bang (db, program_path, buffer, bytes_read, interp_arg);

          if (! program)
            program = resolve_magic (db, program_path, buffer);
        }
    }

  if (! program)
    program = resolve_extension (db, program_path);

  return program;
}

static void
collect_extensions (const gchar *ext,
                    const gchar *program G_GNUC_UNUSED,
                    GString     *str)
{
  if (str->len)
    g_string_append_c (str, G_SEARCHPATH_SEPARATOR);

  g_string_append_c (str, '.');
  g_string_append (str, ext);
}

/**
 * gimp_interpreter_db_get_extensions:
 * @db:
 *
 * Returns: a newly allocated string with all registered file
 *               extensions separated by %G_SEARCHPATH_SEPARATOR;
 *               or %NULL if no extensions are registered
 **/
gchar *
gimp_interpreter_db_get_extensions (GimpInterpreterDB *db)
{
  GString *str;

  g_return_val_if_fail (GIMP_IS_INTERPRETER_DB (db), NULL);

  if (g_hash_table_size (db->extensions) == 0)
    return NULL;

  str = g_string_new (NULL);

  g_hash_table_foreach (db->extensions, (GHFunc) collect_extensions, str);

  return g_string_free (str, FALSE);
}
