/* GSL - Generic Sound Layer
 * Copyright (C) 2000-2002 Tim Janik
 *
 * This library is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Library 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 General Public License for more details.
 *
 * You should have received a copy of the GNU Library General Public
 * License along with this library; if not, write to the
 * Free Software Foundation, Inc., 59 Temple Place, Suite 330,
 * Boston, MA 02111-1307, USA.
 */
#include	"gslmagic.h"

#include	"gslcommon.h"

#include	<string.h>
#include	<unistd.h>
#include	<errno.h>
#include	<sys/types.h>
#include	<stdlib.h>
#include	<sys/stat.h>
#include	<fcntl.h>


/* --- defines --- */
#define	BFILE_BSIZE		(768)	/* amount of buffering */
#define MAX_MAGIC_STRING 	(256)	/* must be < BFILE_BSIZE / 2 */


/* --- typedefs & structures  --- */
typedef struct _Magic Magic;
typedef struct _BFile BFile;
struct _BFile
{
  gint   fd;
  guint  file_size;
  guint8 header[BFILE_BSIZE];
  guint  offset;
  guint8 buffer[BFILE_BSIZE];
};


/* --- prototypes --- */
static Magic*	magic_create		(gchar		*magic_string,
					 const gchar	*original);
static gboolean	magic_match_file	(BFile		*bfile,
					 Magic       	*magics);
static gboolean	bfile_open		(BFile		*bfile,
					 const gchar    *file_name);
static gboolean	bfile_read		(BFile		*bfile,
					 guint		 offset,
					 void		*mem,
					 guint		 n_bytes);
static void	bfile_close		(BFile		*bfile);
static guint	bfile_get_size		(BFile		*bfile);


/* --- functions --- */
GslMagic*
gsl_magic_list_match_file (GslRing     *magic_list,
			   const gchar *file_name)
{
  GslMagic *rmagic = NULL;
  BFile bfile = { -1, };
  
  g_return_val_if_fail (file_name != NULL, NULL);
  
  if (bfile_open (&bfile, file_name))
    {
      gchar *extension = strrchr (file_name, '.');
      gint rpriority = G_MAXINT;
      GslRing *node;
      
      /* we do a quick scan by extension first */
      if (!rmagic && extension)
	for (node = magic_list; node; node = gsl_ring_walk (magic_list, node))
	  {
	    GslMagic *magic = node->data;
	    
	    if (!magic->extension
		|| strcmp (magic->extension, extension) != 0
		|| rpriority < magic->priority
		|| (rmagic && rpriority == magic->priority))
	      continue;
	    if (magic_match_file (&bfile, magic->match_list))
	      {
		rmagic = magic;
		rpriority = rmagic->priority;
	      }
	  }
      /* then we do a normal walk but skip extension matches */
      if (!rmagic && extension)
	for (node = magic_list; node; node = gsl_ring_walk (magic_list, node))
	  {
	    GslMagic *magic = node->data;
	    
	    if ((magic->extension && strcmp (magic->extension, extension) == 0)
		|| rpriority < magic->priority
		|| (rmagic && rpriority == magic->priority))
	      continue;
	    if (magic_match_file (&bfile, magic->match_list))
              {
		rmagic = magic;
		rpriority = rmagic->priority;
	      }
	  }
      /* for no extension, we do a full walk */
      if (!rmagic && !extension)
	for (node = magic_list; node; node = gsl_ring_walk (magic_list, node))
	  {
	    GslMagic *magic = node->data;
	    
	    if (rpriority < magic->priority ||
		(rmagic && rpriority == magic->priority))
	      continue;
	    if (magic_match_file (&bfile, magic->match_list))
	      {
		rmagic = magic;
		rpriority = rmagic->priority;
	      }
	  }
      bfile_close (&bfile);
    }
  
  return rmagic;
}

GslMagic*
gsl_magic_create (gpointer     data,
		  gint	       priority,
		  const gchar *extension,
		  const gchar *magic_spec)
{
  GslMagic *magic;
  Magic *match_list;
  gchar *magic_string;

  g_return_val_if_fail (magic_spec != NULL, NULL);

  magic_string = g_strdup (magic_spec);
  match_list = magic_create (magic_string, magic_spec);
  g_free (magic_string);
  if (!match_list)
    return NULL;

  magic = g_new (GslMagic, 1);
  magic->data = data;
  magic->extension = g_strdup (extension);
  magic->priority = priority;
  magic->match_list = match_list;

  return magic;
}


/* --- Magic creation/checking --- */
typedef enum
{
  MAGIC_CHECK_ANY,
  MAGIC_CHECK_INT_EQUAL,
  MAGIC_CHECK_INT_GREATER,
  MAGIC_CHECK_INT_SMALLER,
  MAGIC_CHECK_UINT_GREATER,
  MAGIC_CHECK_UINT_SMALLER,
  MAGIC_CHECK_UINT_ZEROS,
  MAGIC_CHECK_UINT_ONES,
  MAGIC_CHECK_STRING_EQUAL,
  MAGIC_CHECK_STRING_GREATER,
  MAGIC_CHECK_STRING_SMALLER
} MagicCheckType;
typedef union
{
  gint32  v_int32;
  guint32 v_uint32;
  gchar  *v_string;
} MagicData;
struct _Magic
{
  Magic         *next;
  gulong         offset;
  guint          data_size;
  MagicCheckType magic_check;
  guint32        data_mask;
  MagicData	 value;
  guint          read_string : 1;
  guint          read_size : 1;
  guint		 need_swap : 1;
  guint          cmp_unsigned : 1;
};
static const gchar *magic_field_delims = " \t,";
static const gchar *magic_string_delims = " \t\n\r";

static gboolean
magic_parse_test (Magic       *magic,
		  const gchar *string)
{
  if (!magic->read_string)
    {
      gchar *f = NULL;
      
      if (string[0] == '<' || string[0] == '>')
	{
	  if (magic->cmp_unsigned)
	    magic->magic_check = string[0] == '<' ? MAGIC_CHECK_UINT_SMALLER : MAGIC_CHECK_UINT_GREATER;
	  else
	    magic->magic_check = string[0] == '<' ? MAGIC_CHECK_INT_SMALLER : MAGIC_CHECK_INT_GREATER;
	  string += 1;
	}
      else if (string[0] == '^' || string[0] == '&')
	{
	  magic->magic_check = string[0] == '&' ? MAGIC_CHECK_UINT_ONES : MAGIC_CHECK_UINT_ZEROS;
	  string += 1;
	}
      else if (string[0] == 'x')
	{
	  magic->magic_check = MAGIC_CHECK_ANY;
	  string += 1;
	}
      else
	{
	  string += string[0] == '=';
	  magic->magic_check = MAGIC_CHECK_INT_EQUAL;
	}
      if (string[0] == '0')
	magic->value.v_int32 = strtol (string, &f, string[1] == 'x' ? 16 : 8);
      else
	magic->value.v_int32 = strtol (string, &f, 10);
      
      return *string == 0 || !f || *f == 0;
    }
  else
    {
      gchar tmp_string[MAX_MAGIC_STRING + 1];
      guint n = 0;
      
      if (string[0] == '<' || string[0] == '>')
	{
	  magic->magic_check = string[0] == '<' ? MAGIC_CHECK_STRING_SMALLER : MAGIC_CHECK_STRING_GREATER;
	  string += 1;
	}
      else
	{
	  string += string[0] == '=';
	  magic->magic_check = MAGIC_CHECK_STRING_EQUAL;
	}
      while (n < MAX_MAGIC_STRING && string[n] && !strchr (magic_string_delims, string[n]))
	{
	  if (string[n] != '\\')
	    tmp_string[n] = string[n];
	  else switch ((++string)[n])
	    {
	    case '\\':  tmp_string[n] = '\\';	break;
	    case 't':   tmp_string[n] = '\t';	break;
	    case 'n':   tmp_string[n] = '\n'; 	break;
	    case 'r':   tmp_string[n] = '\r'; 	break;
	    case 'b':   tmp_string[n] = '\b'; 	break;
	    case 'f':   tmp_string[n] = '\f'; 	break;
	    case 's':   tmp_string[n] = ' ';  	break;
	    case 'e':	tmp_string[n] = 27;   	break;
	    default:
	      if (string[n] >= '0' && string[n] <= '7')
		{
		  tmp_string[n] = string[n] - '0';
		  if (string[n + 1] >= '0' && string[n + 1] <= '7')
		    {
		      string += 1;
		      tmp_string[n] *= 8;
		      tmp_string[n] += string[n] - '0';
		      if (string[n + 1] >= '0' && string[n + 1] <= '7')
			{
			  string += 1;
			  tmp_string[n] *= 8;
			  tmp_string[n] += string[n] - '0';
			}
		    }
		}
	      else
		tmp_string[n] = string[n];
	      break;
	    }
	  n++;
	}
      tmp_string[n] = 0;
      magic->data_size = n;
      magic->value.v_string = g_strdup (tmp_string);

      return TRUE;
    }
}

static gboolean
magic_parse_type (Magic       *magic,
		  const gchar *string)
{
  gchar *f = NULL;

  if (string[0] == 'u')
    {
      string += 1;
      magic->cmp_unsigned = TRUE;
    }
  if (strncmp (string, "byte", 4) == 0)
    {
      string += 4;
      magic->data_size = 1;
    }
  else if (strncmp (string, "short", 5) == 0)
    {
      string += 5;
      magic->data_size = 2;
    }
  else if (strncmp (string, "leshort", 7) == 0)
    {
      string += 7;
      magic->data_size = 2;
      magic->need_swap = G_BYTE_ORDER != G_LITTLE_ENDIAN;
    }
  else if (strncmp (string, "beshort", 7) == 0)
    {
      string += 7;
      magic->data_size = 2;
      magic->need_swap = G_BYTE_ORDER != G_BIG_ENDIAN;
    }
  else if (strncmp (string, "long", 4) == 0)
    {
      string += 4;
      magic->data_size = 4;
    }
  else if (strncmp (string, "lelong", 6) == 0)
    {
      string += 6;
      magic->data_size = 4;
      magic->need_swap = G_BYTE_ORDER != G_LITTLE_ENDIAN;
    }
  else if (strncmp (string, "belong", 6) == 0)
    {
      string += 6;
      magic->data_size = 4;
      magic->need_swap = G_BYTE_ORDER != G_BIG_ENDIAN;
    }
#if 0
  else if (strncmp (string, "size", 4) == 0)
    {
      string += 4;
      magic->data_size = 4;
      magic->read_size = TRUE;
      magic->cmp_unsigned = TRUE;
    }
#endif
  else if (strncmp (string, "string", 6) == 0)
    {
      string += 6;
      magic->data_size = 0;
      magic->read_string = TRUE;
    }
  if (string[0] == '&')
    {
      string += 1;
      if (string[0] == '0')
	magic->data_mask = strtol (string, &f, string[1] == 'x' ? 16 : 8);
      else
	magic->data_mask = strtol (string, &f, 10);
      if (f && *f != 0)
	return FALSE;
      while (*string)
	string++;
    }
  else
    magic->data_mask = 0xffffffff;

  return *string == 0;
}

static gboolean
magic_parse_offset (Magic *magic,
		    gchar *string)
{
  gchar *f = NULL;
  
  if (string[0] == '0')
    magic->offset = strtol (string, &f, string[1] == 'x' ? 16 : 8);
  else
    magic->offset = strtol (string, &f, 10);

  return !f || *f == 0;
}

static Magic*
magic_create (gchar       *magic_string,
	      const gchar *original)
#define SKIP_CLEAN(s)	{ while (*s && !strchr(magic_field_delims, *s)) s++; \
                          while (*s && strchr(magic_field_delims, *s)) \
                            *(s++)=0;}
{
  Magic *magics = NULL;
  gchar *p = magic_string;
  
  while (p && *p)
    {
      gchar *next_line;
      
      if (*p == '#' || *p == '\n')
	{
	  next_line = strchr (p, '\n');
	  if (next_line)
	    next_line++;
	}
      else
	{
	  Magic *tmp = magics;
	  
	  magics = g_new0 (Magic, 1);
	  magics->next = tmp;
	  
	  magic_string = p;
	  SKIP_CLEAN (p);
	  if (!magic_parse_offset (magics, magic_string))
	    {
	      g_warning ("unable to parse magic offset \"%s\" from \"%s\"", magic_string, original);
	      return NULL;
	    }
	  magic_string = p;
	  SKIP_CLEAN (p);
	  if (!magic_parse_type (magics, magic_string))
	    {
	      g_warning ("unable to parse magic type \"%s\" from \"%s\"", magic_string, original);
	      return NULL;
	    }
          magic_string = p;
	  next_line = strchr (magic_string, '\n');
	  if (next_line)
	    *(next_line++) = 0;
	  if (!magics->read_string)
	    SKIP_CLEAN (p);
	  if (!magic_parse_test (magics, magic_string))
	    {
	      g_warning ("unable to parse magic test \"%s\" from \"%s\"", magic_string, original);
	      return NULL;
	    }
	}
      p = next_line;
    }
  
  return magics;
}

static gboolean
magic_check_data (Magic     *magic,
		  MagicData *data)
{
  gint cmp = 0;

  switch (magic->magic_check)
    {
      guint l;
    case MAGIC_CHECK_ANY:
      cmp = 1;
      break;
    case MAGIC_CHECK_INT_EQUAL:
      data->v_int32 &= magic->data_mask;
      cmp = data->v_int32 == magic->value.v_int32;
      break;
    case MAGIC_CHECK_INT_GREATER:
      data->v_int32 &= magic->data_mask;
      cmp = data->v_int32 > magic->value.v_int32;
      break;
    case MAGIC_CHECK_INT_SMALLER:
      data->v_int32 &= magic->data_mask;
      cmp = data->v_int32 < magic->value.v_int32;
      break;
    case MAGIC_CHECK_UINT_GREATER:
      data->v_uint32 &= magic->data_mask;
      cmp = data->v_uint32 > magic->value.v_uint32;
      break;
    case MAGIC_CHECK_UINT_SMALLER:
      data->v_uint32 &= magic->data_mask;
      cmp = data->v_uint32 < magic->value.v_uint32;
      break;
    case MAGIC_CHECK_UINT_ZEROS:
      data->v_uint32 &= magic->data_mask;
      cmp = (data->v_int32 & magic->value.v_int32) == 0;
      break;
    case MAGIC_CHECK_UINT_ONES:
      data->v_uint32 &= magic->data_mask;
      cmp = (data->v_int32 & magic->value.v_int32) == magic->value.v_int32;
      break;
    case MAGIC_CHECK_STRING_EQUAL:
      l = magic->data_size < 1 ? strlen (data->v_string) : magic->data_size;
      cmp = strncmp (data->v_string, magic->value.v_string, l) == 0;
      break;
    case MAGIC_CHECK_STRING_GREATER:
      l = magic->data_size < 1 ? strlen (data->v_string) : magic->data_size;
      cmp = strncmp (data->v_string, magic->value.v_string, l) > 0;
      break;
    case MAGIC_CHECK_STRING_SMALLER:
      l = magic->data_size < 1 ? strlen (data->v_string) : magic->data_size;
      cmp = strncmp (data->v_string, magic->value.v_string, l) < 0;
      break;
    }

  return cmp > 0;
}

static inline gboolean
magic_read_data (BFile     *bfile,
		 Magic     *magic,
		 MagicData *data)
{
  guint file_size = bfile_get_size (bfile);

  if (magic->read_size)
    data->v_uint32 = file_size;
  else if (magic->read_string)
    {
      guint l = magic->data_size;
      
      if (l < 1 || l > MAX_MAGIC_STRING)
	l = MIN (MAX_MAGIC_STRING, file_size - magic->offset);
      if (!bfile_read (bfile, magic->offset, data->v_string, l))
	return FALSE;
      data->v_string[MAX (l, 0)] = 0;
    }
  else
    {
      if (magic->data_size == 4)
	{
	  guint32 uint32 = 0;

	  if (!bfile_read (bfile, magic->offset, &uint32, 4))
	    return FALSE;
	  if (magic->need_swap)
	    data->v_uint32 = GUINT32_SWAP_LE_BE (uint32);
	  else
	    data->v_uint32 = uint32;
	}
      else if (magic->data_size == 2)
	{
	  guint16 uint16 = 0;

          if (!bfile_read (bfile, magic->offset, &uint16, 2))
	    return FALSE;
	  if (magic->need_swap)
	    uint16 = GUINT16_SWAP_LE_BE (uint16);
	  if (magic->cmp_unsigned)
	    data->v_uint32 = uint16;
	  else
	    data->v_int32 = (signed) uint16;
	}
      else if (magic->data_size == 1)
	{
	  guint8 uint8;

          if (!bfile_read (bfile, magic->offset, &uint8, 1))
	    return FALSE;
	  if (magic->cmp_unsigned)
	    data->v_uint32 = uint8;
	  else
	    data->v_int32 = (signed) uint8;
	}
      else
	g_assert_not_reached ();
    }

  return TRUE;
}

static gboolean
magic_match_file (BFile *bfile,
		  Magic *magics)
{
  g_return_val_if_fail (bfile != NULL, FALSE);
  g_return_val_if_fail (magics != NULL, FALSE);

  do
    {
      gchar data_string[MAX_MAGIC_STRING + 1];
      MagicData data;
      
      if (magics->read_string)
	data.v_string = data_string;
      else
	data.v_uint32 = 0;
      
      if (!magic_read_data (bfile, magics, &data) ||
	  !magic_check_data (magics, &data))
	return FALSE;
      magics = magics->next;
    }
  while (magics);

  return TRUE;
}


/* --- buffered file, optimized for magic checks --- */
static gboolean
bfile_open (BFile       *bfile,
	    const gchar *file_name)
{
  struct stat buf = { 0, };
  gint ret;

  g_return_val_if_fail (bfile != NULL, FALSE);
  g_return_val_if_fail (bfile->fd < 0, FALSE);
  g_return_val_if_fail (file_name != NULL, FALSE);

  bfile->fd = open (file_name, O_RDONLY);
  if (bfile->fd < 0)
    return FALSE;

  do
    ret = fstat (bfile->fd, &buf);
  while (ret < 0 && errno == EINTR);
  if (ret < 0)
    {
      bfile_close (bfile);
      return FALSE;
    }
  bfile->file_size = buf.st_size;

  do
    ret = read (bfile->fd, bfile->header, BFILE_BSIZE);
  while (ret < 0 && errno == EINTR);
  if (ret < 0)
    {
      bfile_close (bfile);
      return FALSE;
    }

  bfile->offset = 0;
  memcpy (bfile->buffer, bfile->header, BFILE_BSIZE);

  return TRUE;
}

static gboolean
bfile_read (BFile *bfile,
	    guint  offset,
	    void  *mem,
	    guint  n_bytes)
{
  guint end = offset + n_bytes;
  gint ret;

  g_return_val_if_fail (bfile != NULL, FALSE);
  g_return_val_if_fail (n_bytes < BFILE_BSIZE / 2, FALSE);

  if (end > bfile->file_size || bfile->fd < 0)
    return FALSE;

  if (end < BFILE_BSIZE)
    {
      memcpy (mem, bfile->header + offset, n_bytes);
      return TRUE;
    }
  if (offset >= bfile->offset && end < bfile->offset + BFILE_BSIZE)
    {
      memcpy (mem, bfile->buffer + offset - bfile->offset, n_bytes);
      return TRUE;
    }

  bfile->offset = offset - BFILE_BSIZE / 8;
  do
    ret = lseek (bfile->fd, bfile->offset, SEEK_SET);
  while (ret < 0 && errno == EINTR);
  if (ret < 0)
    {
      bfile_close (bfile);
      return FALSE;
    }
  do
    ret = read (bfile->fd, bfile->buffer, BFILE_BSIZE);
  while (ret < 0 && errno == EINTR);
  if (ret < 0)
    {
      bfile_close (bfile);
      return FALSE;
    }
  if (offset >= bfile->offset && end < bfile->offset + BFILE_BSIZE)
    {
      memcpy (mem, bfile->buffer + offset - bfile->offset, n_bytes);
      return TRUE;
    }

  return FALSE;
}

static guint
bfile_get_size (BFile *bfile)
{
  g_return_val_if_fail (bfile != NULL, 0);

  return bfile->fd >= 0 ? bfile->file_size : 0;
}

static void
bfile_close (BFile *bfile)
{
  g_return_if_fail (bfile != NULL);

  if (bfile->fd >= 0)
    close (bfile->fd);
  bfile->fd = -1;
}
