/*
     This file is part of libextractor.
     Copyright (C) 2002, 2003, 2004, 2005, 2006, 2009, 2012 Vidyut Samanta and Christian Grothoff

     libextractor 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, or (at your
     option) any later version.

     libextractor 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 libextractor; see the file COPYING.  If not, write to the
     Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
     Boston, MA 02110-1301, USA.
 */
/**
 * @file main/extractor_plugpath.c
 * @brief determine path where plugins are installed
 * @author Christian Grothoff
 */

#include "platform.h"
#include "extractor.h"
#include <dirent.h>
#include <sys/types.h>
#include <signal.h>
#include <ltdl.h>

#include "extractor_plugpath.h"
#include "extractor_logging.h"

/**
 * Function to call on paths.
 *
 * @param cls closure
 * @param path a directory path
 */
typedef void (*EXTRACTOR_PathProcessor) (void *cls,
                                         const char *path);


/**
 * Remove a trailing '/bin/' from 'in' (if present).
 *
 * @param in input string, modified
 * @return NULL if 'in' is NULL, otherwise 'in' with '/bin/' removed
 */
static char *
cut_bin (char *in)
{
  size_t p;

  if (NULL == in)
    return NULL;
  p = strlen (in);
  if (p < 4)
    return in;
  if ( ('/' == in[p - 1]) ||
       ('\\' == in[p - 1]) )
    in[--p] = '\0';
  if (0 == strcmp (&in[p - 4],
                   "/bin"))
  {
    in[p - 4] = '\0';
    p -= 4;
  }
  else if (0 == strcmp (&in[p - 4],
                        "\\bin"))
  {
    in[p - 4] = '\0';
    p -= 4;
  }
  return in;
}


#if GNU_LINUX
/**
 * Try to determine path by reading /proc/PID/exe or
 * /proc/PID/maps.
 *
 * Note that this may fail if LE is installed in one directory
 * and the binary linking against it sits elsewhere.
 */
static char *
get_path_from_proc_exe ()
{
  char fn[64];
  char line[1024];
  char dir[1024];
  char *lnk;
  char *ret;
  char *lestr;
  ssize_t size;
  FILE *f;

  snprintf (fn,
            sizeof (fn),
            "/proc/%u/maps",
            getpid ());
  if (NULL != (f = fopen (fn, "r")))
  {
    while (NULL != fgets (line, 1024, f))
    {
      if ( (1 == sscanf (line,
                         "%*x-%*x %*c%*c%*c%*c %*x %*2x:%*2x %*u%*[ ]%s",
                         dir)) &&
           (NULL != (lestr = strstr (dir,
                                     "libextractor")) ) )
      {
        lestr[0] = '\0';
        fclose (f);
        return strdup (dir);
      }
    }
    fclose (f);
  }
  snprintf (fn,
            sizeof (fn),
            "/proc/%u/exe",
            getpid ());
  if (NULL == (lnk = malloc (1029))) /* 1024 + 6 for "/lib/" catenation */
    return NULL;
  size = readlink (fn, lnk, 1023);
  if ( (size <= 0) || (size >= 1024) )
  {
    free (lnk);
    return NULL;
  }
  lnk[size] = '\0';
  while ( ('/' != lnk[size]) &&
          (size > 0) )
    size--;
  if ( (size < 4) ||
       ('/' != lnk[size - 4]) )
  {
    /* not installed in "/bin/" -- binary path probably useless */
    free (lnk);
    return NULL;
  }
  lnk[size] = '\0';
  lnk = cut_bin (lnk);
  if (NULL == (ret = realloc (lnk, strlen (lnk) + 6)))
  {
    LOG_STRERROR ("realloc");
    free (lnk);
    return NULL;
  }
  strcat (ret, "/lib/"); /* guess "lib/" as the library dir */
  return ret;
}


#endif


#if WINDOWS
static HMODULE le_dll = NULL;

BOOL WINAPI
DllMain (HINSTANCE hinstDLL,
         DWORD fdwReason,
         LPVOID lpvReserved)
{
  switch (fdwReason)
  {
  case DLL_PROCESS_ATTACH:
    le_dll = (HMODULE) hinstDLL;
    break;
  }

  return TRUE;
}


/**
 * Try to determine path with win32-specific function
 */
static char *
get_path_from_module_filename ()
{
  char *path;
  char *ret;
  char *idx;

  if (NULL == (path = malloc (4103))) /* 4096+nil+6 for "/lib/" catenation */
    return NULL;
  GetModuleFileName (le_dll, path, 4096);
  idx = path + strlen (path);
  while ( (idx > path) &&
          ('\\' != *idx) &&
          ('/' != *idx) )
    idx--;
  *idx = '\0';
  path = cut_bin (path);
  if (NULL == (ret = realloc (path, strlen (path) + 6)))
  {
    LOG_STRERROR ("realloc");
    free (path);
    return NULL;
  }
  strcat (ret, "/lib/"); /* guess "lib/" as the library dir */
  return ret;
}


#endif


#if DARWIN
#include <dlfcn.h>
#include <mach-o/dyld.h>

/**
 * Signature of the '_NSGetExecutablePath" function.
 *
 * @param buf where to write the path
 * @param number of bytes available in 'buf'
 * @return 0 on success, otherwise desired number of bytes is stored in 'bufsize'
 */
typedef int
(*MyNSGetExecutablePathProto) (char *buf,
                               size_t *bufsize);


/**
 * Try to obtain the path of our executable using '_NSGetExecutablePath'.
 *
 * @return NULL on error
 */
static char *
get_path_from_NSGetExecutablePath ()
{
  static char zero;
  char *path;
  char *ret;
  size_t len;
  MyNSGetExecutablePathProto func;

  path = NULL;
  if (NULL == (func =
                 (MyNSGetExecutablePathProto) dlsym (RTLD_DEFAULT,
                                                     "_NSGetExecutablePath")))
    return NULL;
  path = &zero;
  len = 0;
  /* get the path len, including the trailing \0 */
  (void) func (path, &len);
  if (0 == len)
    return NULL;
  if (NULL == (path = malloc (len)))
  {
    LOG_STRERROR ("malloc");
    return NULL;
  }
  if (0 != func (path, &len))
  {
    free (path);
    return NULL;
  }
  len = strlen (path);
  while ((path[len] != '/') && (len > 0))
    len--;
  path[len] = '\0';
  if (NULL != strstr (path, "/lib"))
    return path;
  path = cut_bin (path);
  if (NULL == (ret = realloc (path, strlen (path) + 5)))
  {
    LOG_STRERROR ("realloc");
    free (path);
    return NULL;
  }
  strcat (ret, "/lib/");
  return ret;
}


/**
 * Try to obtain the path of our executable using '_dyld_image' API.
 *
 * @return NULL on error
 */
static char *
get_path_from_dyld_image ()
{
  const char *path;
  char *s;
  char *p;
  unsigned int i;
  int c;

  c = _dyld_image_count ();
  for (i = 0; i < c; i++)
  {
    if (((void *) _dyld_get_image_header (i)) != (void *) &_mh_dylib_header)
      continue;
    path = _dyld_get_image_name (i);
    if ( (NULL == path) || (0 == strlen (path)) )
      continue;
    if (NULL == (p = strdup (path)))
    {
      LOG_STRERROR ("strdup");
      return NULL;
    }
    s = p + strlen (p);
    while ( (s > p) && ('/' != *s) )
      s--;
    s++;
    *s = '\0';
    return p;
  }
  return NULL;
}


#endif


/**
 * Return the actual path to a file found in the current
 * PATH environment variable.
 *
 * @return path to binary, NULL if not found
 */
static char *
get_path_from_PATH ()
{
  struct stat sbuf;
  char *path;
  char *pos;
  char *end;
  char *buf;
  char *ret;
  const char *p;

  if (NULL == (p = getenv ("PATH")))
    return NULL;
  if (NULL == (path = strdup (p))) /* because we write on it */
  {
    LOG_STRERROR ("strdup");
    return NULL;
  }
  if (NULL == (buf = malloc (strlen (path) + 20)))
  {
    LOG_STRERROR ("malloc");
    free (path);
    return NULL;
  }
  pos = path;
  while (NULL != (end = strchr (pos, ':')))
  {
    *end = '\0';
    sprintf (buf, "%s/%s", pos, "extract");
    if (0 == stat (buf, &sbuf))
    {
      free (buf);
      if (NULL == (pos = strdup (pos)))
      {
        LOG_STRERROR ("strdup");
        free (path);
        return NULL;
      }
      free (path);
      pos = cut_bin (pos);
      if (NULL == (ret = realloc (pos, strlen (pos) + 6)))
      {
        LOG_STRERROR ("realloc");
        free (pos);
        return NULL;
      }
      strcat (ret, "/lib/");
      return ret;
    }
    pos = end + 1;
  }
  sprintf (buf, "%s/%s", pos, "extract");
  if (0 == stat (buf, &sbuf))
  {
    pos = strdup (pos);
    free (buf);
    free (path);
    if (NULL == pos)
      return NULL;
    pos = cut_bin (pos);
    if (NULL == (ret = realloc (pos, strlen (pos) + 6)))
    {
      LOG_STRERROR ("realloc");
      free (pos);
      return NULL;
    }
    strcat (ret, "/lib/");
    return ret;
  }
  free (buf);
  free (path);
  return NULL;
}


/**
 * Create a filename by appending 'fname' to 'path'.
 *
 * @param path the base path
 * @param fname the filename to append
 * @return '$path/$fname', NULL on error
 */
static char *
append_to_dir (const char *path,
               const char *fname)
{
  char *ret;
  size_t slen;

  if (0 == (slen = strlen (path)))
    return NULL;
  if ('/' == fname[0])
    fname++;
  ret = malloc (slen + strlen (fname) + 2);
  if (NULL == ret)
    return NULL;
#ifdef MINGW
  if ('\\' == path[slen - 1])
    sprintf (ret,
             "%s%s",
             path,
             fname);
  else
    sprintf (ret,
             "%s\\%s",
             path,
             fname);
#else
  if ('/' == path[slen - 1])
    sprintf (ret,
             "%s%s",
             path,
             fname);
  else
    sprintf (ret,
             "%s/%s",
             path,
             fname);
#endif
  return ret;
}


/**
 * Iterate over all paths where we expect to find GNU libextractor
 * plugins.
 *
 * @param pp function to call for each path
 * @param pp_cls cls argument for pp.
 */
static void
get_installation_paths (EXTRACTOR_PathProcessor pp,
                        void *pp_cls)
{
  const char *p;
  char *path;
  char *prefix;
  char *d;
  char *saveptr;

  prefix = NULL;
  if (NULL != (p = getenv ("LIBEXTRACTOR_PREFIX")))
  {
    if (NULL == (d = strdup (p)))
    {
      LOG_STRERROR ("strdup");
      return;
    }
    for (prefix = strtok_r (d, ":", &saveptr);
         NULL != prefix;
         prefix = strtok_r (NULL, ":", &saveptr))
      pp (pp_cls, prefix);
    free (d);
    return;
  }
#if GNU_LINUX
  if (NULL == prefix)
    prefix = get_path_from_proc_exe ();
#endif
#if WINDOWS
  if (NULL == prefix)
    prefix = get_path_from_module_filename ();
#endif
#if DARWIN
  if (NULL == prefix)
    prefix = get_path_from_NSGetExecutablePath ();
  if (NULL == prefix)
    prefix = get_path_from_dyld_image ();
#endif
  if (NULL == prefix)
    prefix = get_path_from_PATH ();
  pp (pp_cls, PLUGININSTDIR);
  if (NULL == prefix)
    return;
  path = append_to_dir (prefix, PLUGINDIR);
  if (NULL != path)
  {
    if (0 != strcmp (path,
                     PLUGININSTDIR))
      pp (pp_cls, path);
    free (path);
  }
  free (prefix);
}


/**
 * Closure for #find_plugin_in_path().
 */
struct SearchContext
{
  /**
   * Name of the plugin we are looking for.
   */
  const char *short_name;

  /**
   * Location for storing the path to the plugin upon success.
   */
  char *path;
};


/**
 * Load all plugins from the given directory.
 *
 * @param cls pointer to the "struct EXTRACTOR_PluginList*" to extend
 * @param path path to a directory with plugins
 */
static void
find_plugin_in_path (void *cls,
                     const char *path)
{
  struct SearchContext *sc = cls;
  DIR *dir;
  struct dirent *ent;
  const char *sym_name;
  char *sym;
  char *dot;
  size_t dlen;

  if (NULL != sc->path)
    return;
  if (NULL == (dir = opendir (path)))
    return;
  while (NULL != (ent = readdir (dir)))
  {
    if ('.' == ent->d_name[0])
      continue;
    dlen = strlen (ent->d_name);
    if ( (dlen < 4) ||
         ( (0 != strcmp (&ent->d_name[dlen - 3], ".so")) &&
           (0 != strcasecmp (&ent->d_name[dlen - 4], ".dll")) ) )
      continue; /* only load '.so' and '.dll' */
    if (NULL == (sym_name = strrchr (ent->d_name, '_')))
      continue;
    sym_name++;
    if (NULL == (sym = strdup (sym_name)))
    {
      LOG_STRERROR ("strdup");
      closedir (dir);
      return;
    }
    dot = strchr (sym, '.');
    if (NULL != dot)
      *dot = '\0';
    if (0 == strcmp (sym, sc->short_name))
    {
      sc->path = append_to_dir (path, ent->d_name);
      free (sym);
      break;
    }
    free (sym);
  }
  closedir (dir);
}


/**
 * Given a short name of a library (i.e. "mime"), find
 * the full path of the respective plugin.
 */
char *
EXTRACTOR_find_plugin_ (const char *short_name)
{
  struct SearchContext sc;

  sc.path = NULL;
  sc.short_name = short_name;
  get_installation_paths (&find_plugin_in_path,
                          &sc);
  return sc.path;
}


/**
 * Closure for #load_plugins_from_dir().
 */
struct DefaultLoaderContext
{
  /**
   * Accumulated result list.
   */
  struct EXTRACTOR_PluginList *res;

  /**
   * Flags to use for all plugins.
   */
  enum EXTRACTOR_Options flags;
};


/**
 * Load all plugins from the given directory.
 *
 * @param cls pointer to the "struct EXTRACTOR_PluginList*" to extend
 * @param path path to a directory with plugins
 */
static void
load_plugins_from_dir (void *cls,
                       const char *path)
{
  struct DefaultLoaderContext *dlc = cls;
  DIR *dir;
  struct dirent *ent;
  const char *sym_name;
  char *sym;
  char *dot;
  size_t dlen;

  if (NULL == (dir = opendir (path)))
    return;
  while (NULL != (ent = readdir (dir)))
  {
    if (ent->d_name[0] == '.')
      continue;
    dlen = strlen (ent->d_name);
    if ( (dlen < 4) ||
         ( (0 != strcmp (&ent->d_name[dlen - 3], ".so")) &&
           (0 != strcasecmp (&ent->d_name[dlen - 4], ".dll")) ) )
      continue; /* only load '.so' and '.dll' */
    if (NULL == (sym_name = strrchr (ent->d_name, '_')))
      continue;
    sym_name++;
    if (NULL == (sym = strdup (sym_name)))
    {
      LOG_STRERROR ("strdup");
      closedir (dir);
      return;
    }
    if (NULL != (dot = strchr (sym, '.')))
      *dot = '\0';
    dlc->res = EXTRACTOR_plugin_add (dlc->res,
                                     sym,
                                     NULL,
                                     dlc->flags);
    free (sym);
  }
  closedir (dir);
}


/**
 * Load the default set of plugins. The default can be changed
 * by setting the LIBEXTRACTOR_LIBRARIES environment variable.
 * If it is set to "env", then this function will return
 * #EXTRACTOR_plugin_add_config(NULL, env, flags).  Otherwise,
 * it will load all of the installed plugins and return them.
 *
 * @param flags options for all of the plugins loaded
 * @return the default set of plugins, NULL if no plugins were found
 */
struct EXTRACTOR_PluginList *
EXTRACTOR_plugin_add_defaults (enum EXTRACTOR_Options flags)
{
  struct DefaultLoaderContext dlc;
  char *env;

  env = getenv ("LIBEXTRACTOR_LIBRARIES");
  if (NULL != env)
    return EXTRACTOR_plugin_add_config (NULL, env, flags);
  dlc.res = NULL;
  dlc.flags = flags;
  get_installation_paths (&load_plugins_from_dir,
                          &dlc);
  return dlc.res;
}


/* end of extractor_plugpath.c */
