/*
 * Copyright (c) 2013 The Chromium OS Authors. All rights reserved.
 * Use of this source code is governed by a BSD-style license that can be
 * found in the LICENSE file.
 */

#define _GNU_SOURCE /* for RTLD_NEXT in dlfcn.h */

#include <glib.h>

#include <dlfcn.h>
#include <errno.h>
#include <fcntl.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>

/* The purpose of this library is to override the open/creat syscalls to
 * redirect these calls for selected devices. Adding the library file to
 * LD_PRELOAD is the general way to accomplish this. The arbitrary file mapping
 * is specified in the environment variable FILE_REDIRECTION_PRELOADS as
 * follows:
 *
 * FILE_REDIRECTIONS_PRELOAD=<path1>=<target1>:<path2>=<target2>
 *
 * Here, <path1> etc are the absolute paths to files for which open/close should
 * be intercepted. <target1> etc are the alternative files to which these calls
 * should be redirected.
 *
 *  - ':' is used to separate file mappings
 *  - The special character ':' in the paths should be escaped with '\'
 *
 *  Example:
 *    export FILE_REDIRECTIONS_PRELOAD=/tmp/file1=/tmp/file2
 *    LD_PRELOAD=./libfakesyscalls.so ./write_to_tmp_file1
 *
 *  where write_to_tmp_file1 is some executable that opens and writes to
 *  /tmp/file1. When the program exits, /tmp/file2 would have been created and
 *  written to, not /tmp/file1.
 *
 *  cf: fakesyscalls-exercise.c
 *
 *  Thread safety: This library is not thread-safe. If two threads
 *  simultaneously call open/creat for the first time, internal data-structures
 *  in the library can be corrupted.
 *  It is safe to have subsequent calls to open/creat be concurrent.
 */

#ifdef FAKE_SYSCALLS_DEBUG
static const char *k_tmp_logging_file_full_path = "/tmp/fake_syscalls.dbg";
static FILE *debug_file;

#define fake_syscalls_debug_init() \
  debug_file = fopen (k_tmp_logging_file_full_path, "w")

#define fake_syscalls_debug(...) \
  do { \
    if (debug_file) { \
      fprintf (debug_file, __VA_ARGS__); \
      fprintf (debug_file, "\n"); \
    } \
  } while (0)

#define fake_syscalls_debug_finish() \
  do { \
    if (debug_file) { \
      fclose (debug_file); \
      debug_file = NULL; \
    } \
  } while (0)

#else /* FAKE_SYSCALLS_DEBUG */
#define fake_syscalls_debug_init()
#define fake_syscalls_debug(...)
#define fake_syscalls_debug_finish()
#endif  /* FAKE_SYSCALLS_DEBUG */

static GHashTable *file_redirection_map;

static const char *k_env_file_redirections = "FILE_REDIRECTIONS_PRELOAD";
static const char *k_func_open = "open";
static const char *k_func_creat = "creat";

void __attribute__ ((constructor))
fake_syscalls_init (void)
{
  fake_syscalls_debug_init ();
  fake_syscalls_debug ("Initialized fakesyscalls library.");
}

void __attribute__ ((destructor))
fake_syscalls_finish (void)
{
  if (file_redirection_map)
    g_hash_table_unref (file_redirection_map);
  fake_syscalls_debug ("Quit fakesyscalls library.");
  fake_syscalls_debug_finish ();
}

static void
abort_on_error (GError *error) {
  if (!error)
    return;

  fake_syscalls_debug ("Aborting on error: |%s|", error->message);
  g_error_free (error);
  fake_syscalls_debug_finish ();
  g_assert (0);
}

static void
setup_redirection_map (void)
{
  const char *orig_env;
  GRegex *entry_delimiter, *key_value_delimiter, *escaped_colon;
  gchar *buf;
  gchar **redirections;
  gchar **redirections_iter;
  GError *error = NULL;

  file_redirection_map = g_hash_table_new_full (g_str_hash, g_str_equal, g_free,
                                                g_free);

  orig_env = getenv (k_env_file_redirections);
  if (orig_env == NULL)
    orig_env = "";
  fake_syscalls_debug ("FILE_REDIRECTIONS_PRELOAD=|%s|", orig_env);

  entry_delimiter = g_regex_new ("(?:([^\\\\]):)|(?:^:)", 0, 0, &error);
  abort_on_error (error);
  key_value_delimiter = g_regex_new ("=", 0, 0, &error);
  abort_on_error (error);
  escaped_colon = g_regex_new ("(?:[^\\\\]\\\\:)|(?:^\\\\:)", 0, 0, &error);
  abort_on_error (error);

  buf = g_regex_replace (entry_delimiter, orig_env, -1, 0, "\\1;", 0, &error);
  abort_on_error (error);
  redirections = g_strsplit (buf, ";", 0);
  g_free (buf);

  for (redirections_iter = redirections;
       *redirections_iter;
       ++redirections_iter) {
    gchar **parts;

    if (g_strcmp0 ("", *redirections_iter) == 0)
      continue;

    /* Any ':' in the map has to be escaped with a '\' to allow for ':' to act
     * as delimiter. Clean away the '\'.
     */
    buf = g_regex_replace_literal (escaped_colon, *redirections_iter, -1, 0,
                                   ":", 0, &error);
    abort_on_error (error);
    parts = g_regex_split (key_value_delimiter, buf, 0);
    g_free (buf);

    if (g_strv_length (parts) != 2) {
      fake_syscalls_debug ("Error parsing redirection: |%s|. Malformed map?",
                           *redirections_iter);
      g_strfreev (parts);
      continue;
    }
    if (strlen (parts[0]) == 0 || parts[0][0] != '/' ||
        strlen (parts[1]) == 0 || parts[1][0] != '/') {
      fake_syscalls_debug ("Error parsing redirection: |%s|."
                           "Invalid absolute paths.",
                           *redirections_iter);
      g_strfreev (parts);
      continue;
    }

    fake_syscalls_debug ("Inserted redirection: |%s|->|%s|",
                         parts[0], parts[1]);
    g_hash_table_insert (file_redirection_map,
                         g_strdup (parts[0]), g_strdup (parts[1]));
    g_strfreev (parts);
  }

  g_regex_unref (entry_delimiter);
  g_regex_unref (key_value_delimiter);
  g_regex_unref (escaped_colon);
  g_strfreev (redirections);
}

int
open (const char *pathname, int flags, ...)
{
  static int(*realfunc)(const char *, int, ...);
  const char *redirection;
  va_list ap;
  gboolean is_creat = FALSE;
  mode_t mode = S_IRUSR;  /* Make compiler happy. Remain restrictive. */

  if (file_redirection_map == NULL)
    setup_redirection_map ();

  redirection = (char *) g_hash_table_lookup (file_redirection_map, pathname);
  if (redirection == NULL)
    redirection = pathname;

  if (realfunc == NULL)
    realfunc = (int(*)(const char *, int, ...))dlsym (RTLD_NEXT, k_func_open);

  is_creat = flags & O_CREAT;

  if (is_creat) {
    va_start (ap, flags);
    mode = va_arg (ap, mode_t);
    va_end (ap);
    fake_syscalls_debug (
        "Redirect: open (%s, %d, %d) --> open (%s, %d, %d)",
        pathname, flags, mode, redirection, flags, mode);
    return realfunc (redirection, flags, mode);
  } else {
    fake_syscalls_debug (
        "Redirect: open (%s, %d) --> open (%s, %d)",
        pathname, flags, redirection, flags);
    return realfunc (redirection, flags);
  }
}

int
creat (const char *pathname, mode_t mode)
{
  static int(*realfunc)(const char *, mode_t);
  const char *redirection;

  if (file_redirection_map == NULL)
    setup_redirection_map ();

  redirection = (char *) g_hash_table_lookup (file_redirection_map, pathname);
  if (redirection == NULL)
    redirection = pathname;
  fake_syscalls_debug (
      "Redirect: creat (%s, %d) --> creat (%s, %d)",
      pathname, mode, redirection, mode);

  if (realfunc == NULL)
    realfunc = (int(*)(const char *, mode_t))dlsym (RTLD_NEXT, k_func_creat);

  return realfunc (redirection, mode);
}