/*
 * Copyright © 2019 Collabora Ltd.
 *
 * SPDX-License-Identifier: MIT
 *
 * Permission is hereby granted, free of charge, to any person obtaining
 * a copy of this software and associated documentation files (the
 * "Software"), to deal in the Software without restriction, including
 * without limitation the rights to use, copy, modify, merge, publish,
 * distribute, sublicense, and/or sell copies of the Software, and to
 * permit persons to whom the Software is furnished to do so, subject to
 * the following conditions:
 *
 * The above copyright notice and this permission notice shall be included
 * in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
 * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
 * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
 * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
 * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

/*
 * This helper takes a SONAME as an argument, and optionally a filename for
 * symbols, and outputs a parsable JSON with the path, the dependencies and
 * the possible missing symbols of the requested library.
 *
 * For a usage example see `srt_check_library_presence` in
 * `steam-runtime-tools/library.c`.
 */

#include <argz.h>
#include <dlfcn.h>
#include <errno.h>
#include <getopt.h>
#include <link.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sysexits.h>

#define BASE "Base"

static void print_json_string_content (const char *s);
static void clean_exit (void *handle);
static bool has_symbol (void *handle, const char *symbol);
static bool has_versioned_symbol (void *handle,
                                  const char *symbol,
                                  const char * version);

static void oom (void) __attribute__((__noreturn__));
static void
oom (void)
{
  fprintf (stderr, "Out of memory");
  exit (EX_OSERR);
}

#define asprintf_or_die(...) \
do { \
    if (asprintf (__VA_ARGS__) < 0) \
      oom (); \
} while (0)

#define argz_add_or_die(...) \
do { \
    if (argz_add (__VA_ARGS__) != 0) \
      oom (); \
} while (0)

enum
{
  OPTION_HELP = 1,
  OPTION_DEB_SYMBOLS,
  OPTION_HIDDEN_DEPENDENCY,
  OPTION_VERSION,
};

struct option long_options[] =
{
    { "hidden-dependency", required_argument, NULL, OPTION_HIDDEN_DEPENDENCY },
    { "deb-symbols", no_argument, NULL, OPTION_DEB_SYMBOLS },
    { "help", no_argument, NULL, OPTION_HELP },
    { "version", no_argument, NULL, OPTION_VERSION },
    { NULL, 0, NULL, 0 }
};

static void usage (int code) __attribute__((__noreturn__));

/*
 * Print usage information and exit with status @code.
 */
static void
usage (int code)
{
  FILE *fp;

  if (code == 0)
    fp = stdout;
  else
    fp = stderr;

  fprintf (fp, "Usage: %s [OPTIONS] SONAME [SYMBOLS_FILENAME]\n",
           program_invocation_short_name);
  exit (code);
}

int
main (int argc,
      char **argv)
{
  const char *entry;
  const char *soname;
  const char *version;
  const char *symbol;
  void *handle = NULL;
  struct link_map *dep_map = NULL;
  struct link_map *the_library = NULL;
  FILE *fp;
  char *missing_symbols = NULL;
  char *misversioned_symbols = NULL;
  size_t missing_n = 0;
  size_t misversioned_n = 0;
  char *line = NULL;
  size_t len = 0;
  ssize_t chars;
  bool first;
  bool deb_symbols = false;
  size_t dependency_counter = 0;
  int opt;

  while ((opt = getopt_long (argc, argv, "", long_options, NULL)) != -1)
    {
      switch (opt)
        {
          case OPTION_HIDDEN_DEPENDENCY:
            dependency_counter++;
            break;

          case OPTION_DEB_SYMBOLS:
            deb_symbols = true;
            break;

          case OPTION_HELP:
            usage (0);
            break;

          case OPTION_VERSION:
            /* Output version number as YAML for machine-readability,
             * inspired by `ostree --version` and `docker version` */
            printf (
                "%s:\n"
                " Package: steam-runtime-tools\n"
                " Version: %s\n",
                argv[0], VERSION);
            return 0;

          case '?':
          default:
            usage (1);
            break;  /* not reached */
        }
    }

  if (argc < optind + 1 || argc > optind + 2)
    {
      usage (1);
    }

  const char *hidden_deps[dependency_counter + 1];

  if (dependency_counter > 0)
    {
      /* Reset getopt */
      optind = 1;

      size_t i = 0;
      while ((opt = getopt_long (argc, argv, "", long_options, NULL)) != -1)
        {
          switch (opt)
            {
              case OPTION_HIDDEN_DEPENDENCY:
                hidden_deps[i] = optarg;
                i++;
                break;

              default:
                break;
            }
        }
    }

  soname = argv[optind];
  printf ("{\n");
  printf ("  \"");
  print_json_string_content (soname);
  printf ("\": {");

  for (size_t i = 0; i < dependency_counter; i++)
    {
      /* We don't call "dlclose" on global hidden dependencies, otherwise ubsan
       * will report an indirect memory leak */
      if (dlopen (hidden_deps[i], RTLD_NOW|RTLD_GLOBAL) == NULL)
        {
          fprintf (stderr, "Unable to find the dependency library: %s\n", dlerror ());
          clean_exit (handle);
          return 1;
        }
    }

  handle = dlopen (soname, RTLD_NOW);
  if (handle == NULL)
    {
      fprintf (stderr, "Unable to find the library: %s\n", dlerror ());
      clean_exit (handle);
      return 1;
    }
  /* Using RTLD_DI_LINKMAP insted of RTLD_DI_ORIGIN we don't need to worry
   * about allocating a big enough array for the path. */
  if (dlinfo (handle, RTLD_DI_LINKMAP, &the_library) != 0 || the_library == NULL)
    {
      fprintf (stderr, "Unable to obtain the path: %s\n", dlerror ());
      clean_exit (handle);
      return 1;
    }
  printf ("\n    \"path\": \"");
  print_json_string_content (the_library->l_name);
  printf ("\"");

  if (argc >= optind + 2)
    {
      size_t soname_len = strlen (soname);
      bool found_our_soname = false;
      bool in_our_soname = false;

      if (strcmp(argv[optind + 1], "-") == 0)
        fp = stdin;
      else
        fp = fopen(argv[optind + 1], "r");

      if (fp == NULL)
        {
          int saved_errno = errno;

          fprintf (stderr, "Error reading \"%s\": %s\n",
                   argv[optind + 1], strerror (saved_errno));
          clean_exit (handle);
          return 1;
        }

      while ((chars = getline(&line, &len, fp)) != -1)
        {
          if (line[chars - 1] == '\n')
            line[chars - 1] = '\0';

          /* Skip any empty line */
          if (chars > 1)
            {
              char *pointer_into_line;

              if (deb_symbols)
                {
                  if (line[0] == '#' || line[0] == '*' || line[0] == '|')
                    {
                      /* comment or metadata lines, ignore:
                       * "# comment"
                       * "* Field: Value"
                       * "| alternative-dependency" */
                      continue;
                    }
                  else if (line[0] == ' ')
                    {
                      /* this line represents a symbol:
                       * " symbol@Base 1.2-3~" */
                      if (!in_our_soname)
                        {
                          /* this is a symbol from a different library,
                           * ignore */
                          continue;
                        }
                    }
                  else
                    {
                      /* This line introduces a new SONAME, which might
                       * be the one we are interested in:
                       * "libz.so.1 zlib1g #MINVER#" */
                      if (strncmp (soname, line, soname_len) == 0
                          && (line[soname_len] == ' ' || line[soname_len] == '\t'))
                        {
                          found_our_soname = true;
                          in_our_soname = true;
                        }
                      else
                        {
                          in_our_soname = false;
                        }

                      /* This is not a symbol */
                      continue;
                    }

                  pointer_into_line = &line[1];
                }
              else
                {
                  pointer_into_line = line;
                }

              symbol = strsep (&pointer_into_line, "@");
              version = strsep (&pointer_into_line, deb_symbols ? "@ \t" : "@");
              if (symbol == NULL)
                {
                  fprintf (stderr, "Probably the symbol@version pair is mispelled.");
                  free (line);
                  free (missing_symbols);
                  free (misversioned_symbols);
                  clean_exit (handle);
                  return 1;
                }

              if (version == NULL || strcmp (version, BASE) == 0)
                {
                  if (!has_symbol (handle, symbol))
                    argz_add_or_die (&missing_symbols, &missing_n, symbol);
                }
              else
                {
                  if (strcmp (symbol, version) == 0)
                    {
                      /* Ignore: dlsym() and dlvsym() don't find the
                       * special symbol representing the version itself,
                       * because it is neither data nor code. */
                    }
                  else if (!has_versioned_symbol (handle, symbol, version))
                    {
                      char * merged_string;
                      asprintf_or_die (&merged_string, "%s@%s", symbol, version);
                      if (has_symbol (handle, symbol))
                          argz_add_or_die (&misversioned_symbols, &misversioned_n, merged_string);
                      else
                          argz_add_or_die (&missing_symbols, &missing_n, merged_string);

                      free (merged_string);
                    }
                }
            }
        }
      free (line);
      fclose (fp);

      if (deb_symbols && !found_our_soname)
        {
          fprintf (stderr, "Warning: \"%s\" does not describe ABI of \"%s\"\n",
                   argv[optind + 1], soname);
        }

      first = true;
      entry = 0;
      printf (",\n    \"missing_symbols\": [");
      while ((entry = argz_next (missing_symbols, missing_n, entry)))
        {
          if (first)
            first = false;
          else
            printf (",");

          printf ("\n      \"");
          print_json_string_content (entry);
          printf ("\"");
        }
      printf ("\n    ]");

      first = true;
      entry = 0;
      printf (",\n    \"misversioned_symbols\": [");
      while ((entry = argz_next (misversioned_symbols, misversioned_n, entry)))
        {
          if (first)
            first = false;
          else
            printf (",");

          printf ("\n      \"");
          print_json_string_content (entry);
          printf ("\"");
        }
      printf ("\n    ]");

      free (missing_symbols);
      free (misversioned_symbols);
    }
  dep_map = the_library;

  /* Some loaded libraries may be before our handle.
   * To list them all we move the pointer at the beginning. */
  while (dep_map != NULL && dep_map->l_prev != NULL)
    dep_map = dep_map->l_prev;

  printf (",\n    \"dependencies\": [");
  first = true;
  for (; dep_map != NULL; dep_map = dep_map->l_next)
    {
      if (dep_map == the_library || strcmp (dep_map->l_name, "") == 0)
        continue;

      if (first)
        {
          printf ("\n      \"");
          first = false;
        }
      else
        printf (",\n      \"");

      print_json_string_content (dep_map->l_name);
      printf ("\"");
    }

  printf ("\n    ]\n  }");
  clean_exit (handle);

  return 0;
}

static void
print_json_string_content (const char *s)
{
  const char *p;

  for (p = s; *p != '\0'; p++)
    {
      if (*p == '"' || *p == '\\' || *p <= 0x1F)
        printf ("\\u%04x", *p);
      else
        printf ("%c", *p);
    }
}

static void
clean_exit (void *handle)
{
    if (handle != NULL)
      dlclose (handle);

    printf ("\n}\n");
}

static bool
has_symbol (void *handle,
            const char *symbol)
{
  (void) dlerror ();  /* clear old error indicator */
  (void) dlsym (handle, symbol);
  return (dlerror () == NULL);
}

static bool
has_versioned_symbol (void *handle,
                      const char *symbol,
                      const char *version)
{
  (void) dlerror ();  /* clear old error indicator */
  (void) dlvsym (handle, symbol, version);
  return (dlerror () == NULL);
}