/*
 * Copyright (c) 2016 GitHub, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include "bcc_proc.h"

#include <ctype.h>
#include <dirent.h>
#include <fcntl.h>
#include <inttypes.h>
#include <limits.h>
#include <math.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>

#include "bcc_elf.h"
#include "bcc_perf_map.h"
#include "bcc_zip.h"

#ifdef __x86_64__
// https://www.kernel.org/doc/Documentation/x86/x86_64/mm.txt
const unsigned long long kernelAddrSpace = 0x00ffffffffffffff;
#else
const unsigned long long kernelAddrSpace = 0x0;
#endif

char *bcc_procutils_which(const char *binpath) {
  char buffer[PATH_MAX];
  const char *PATH;

  if (strchr(binpath, '/'))
    return bcc_elf_is_exe(binpath) ? strdup(binpath) : 0;

  if (!(PATH = getenv("PATH")))
    return 0;

  while (PATH) {
    const char *next = strchr(PATH, ':') ?: strchr(PATH, '\0');
    const size_t path_len = next - PATH;

    if (path_len) {
      int ret = snprintf(buffer, sizeof(buffer), "%.*s/%s",
	                  (int)path_len, PATH, binpath);
      if (ret < 0 || ret >= sizeof(buffer))
        return 0;

      if (bcc_elf_is_exe(buffer))
        return strdup(buffer);
    }

    PATH = *next ? (next + 1) : 0;
  }

  return 0;
}

#define STARTS_WITH(mapname, prefix) (!strncmp(mapname, prefix, sizeof(prefix)-1))

int bcc_mapping_is_file_backed(const char *mapname) {
  return mapname[0] && !(
    STARTS_WITH(mapname, "//anon") ||
    STARTS_WITH(mapname, "/dev/zero") ||
    STARTS_WITH(mapname, "/anon_hugepage") ||
    STARTS_WITH(mapname, "[stack") ||
    STARTS_WITH(mapname, "/SYSV") ||
    STARTS_WITH(mapname, "[heap]") ||
    STARTS_WITH(mapname, "[vsyscall]"));
}

/*
Finds a file descriptor for a given inode if it's a memory-backed fd.
*/
static char *_procutils_memfd_path(const int pid, const uint64_t inum) {
  char path_buffer[PATH_MAX + 1];
  char *path = NULL;
  char *dirstr;
  DIR *dirstream;
  struct stat sb;
  struct dirent *dent;

  snprintf(path_buffer, (PATH_MAX + 1), "/proc/%d/fd", pid);
  dirstr = malloc(strlen(path_buffer) + 1);
  strcpy(dirstr, path_buffer);
  dirstream = opendir(dirstr);

  if (dirstream == NULL) {
    free(dirstr);
    return NULL;
  }

  while (path == NULL && (dent = readdir(dirstream)) != NULL) {
    snprintf(path_buffer, (PATH_MAX + 1), "%s/%s", dirstr, dent->d_name);
    if (stat(path_buffer, &sb) == -1)
      continue;

    if (sb.st_ino == inum) {
      char *pid_fd_path = malloc(strlen(path_buffer) + 1);
      strcpy(pid_fd_path, path_buffer);
      path = pid_fd_path;
    }
  }
  closedir(dirstream);
  free(dirstr);

  return path;
}

static int _procfs_might_be_zip_path(const char *path) {
  return strstr(path, ".zip") || strstr(path, ".apk");
}

static char *_procfs_find_zip_entry(const char *path, int pid,
                                    uint32_t *offset) {
  char ns_relative_path[PATH_MAX];
  int rc = snprintf(ns_relative_path, sizeof(ns_relative_path),
                    "/proc/%d/root%s", pid, path);
  if (rc < 0 || rc >= sizeof(ns_relative_path)) {
    return NULL;
  }

  struct bcc_zip_archive *archive = bcc_zip_archive_open(ns_relative_path);
  if (archive == NULL) {
    return NULL;
  }

  struct bcc_zip_entry entry;
  if (bcc_zip_archive_find_entry_at_offset(archive, *offset, &entry) ||
      entry.compression) {
    bcc_zip_archive_close(archive);
    return NULL;
  }

  char *result = malloc(strlen(path) + entry.name_length + 3);
  if (result == NULL) {
    bcc_zip_archive_close(archive);
    return NULL;
  }

  sprintf(result, "%s!/%.*s", path, entry.name_length, entry.name);
  *offset -= entry.data_offset;
  bcc_zip_archive_close(archive);
  return result;
}

// return: 0 -> callback returned < 0, stopped iterating
//        -1 -> callback never indicated to stop
int _procfs_maps_each_module(FILE *procmap, int pid,
                             bcc_procutils_modulecb callback, void *payload) {
  char buf[PATH_MAX + 1], perm[5];
  char *name, *resolved_name;
  mod_info mod;
  uint8_t enter_ns;
  while (true) {
    enter_ns = 1;
    buf[0] = '\0';
    // From fs/proc/task_mmu.c:show_map_vma
    if (fscanf(procmap,
               "%" PRIx64 "-%" PRIx64 " %4s %llx %" PRIx64 ":%" PRIx64
               " %" PRIu64 "%[^\n]",
               &mod.start_addr, &mod.end_addr, perm, &mod.file_offset,
               &mod.dev_major, &mod.dev_minor, &mod.inode, buf) != 8)
      break;

    if (perm[2] != 'x')
      continue;

    name = buf;
    while (isspace(*name))
      name++;
    mod.name = name;
    if (!bcc_mapping_is_file_backed(name))
      continue;

    resolved_name = NULL;
    if (strstr(name, "/memfd:")) {
      resolved_name = _procutils_memfd_path(pid, mod.inode);
      if (resolved_name != NULL) {
        enter_ns = 0;
      }
    } else if (_procfs_might_be_zip_path(mod.name)) {
      uint32_t zip_entry_offset = mod.file_offset;
      resolved_name = _procfs_find_zip_entry(mod.name, pid, &zip_entry_offset);
      if (resolved_name != NULL) {
        mod.file_offset = zip_entry_offset;
      }
    }

    if (resolved_name != NULL) {
      strncpy(buf, resolved_name, PATH_MAX);
      buf[PATH_MAX] = 0;
      free(resolved_name);
      mod.name = buf;
    }

    if (callback(&mod, enter_ns, payload) < 0)
      return 0;
  }

  return -1;
}

int bcc_procutils_each_module(int pid, bcc_procutils_modulecb callback,
                              void *payload) {
  char procmap_filename[128];
  FILE *procmap;
  snprintf(procmap_filename, sizeof(procmap_filename), "/proc/%ld/maps",
           (long)pid);
  procmap = fopen(procmap_filename, "r");
  if (!procmap)
    return -1;

  _procfs_maps_each_module(procmap, pid, callback, payload);

  // Address mapping for the entire address space maybe in /tmp/perf-<PID>.map
  // This will be used if symbols aren't resolved in an earlier mapping.
  char map_path[4096];
  // Try perf-<PID>.map path with process's mount namespace, chroot and NSPID,
  // in case it is generated by the process itself.
  mod_info mod;
  memset(&mod, 0, sizeof(mod_info));
  if (bcc_perf_map_path(map_path, sizeof(map_path), pid)) {
    mod.name = map_path;
    mod.end_addr = -1;
    if (callback(&mod, 1, payload) < 0)
      goto done;
  }
  // Try perf-<PID>.map path with global root and PID, in case it is generated
  // by other Process. Avoid checking mount namespace for this.
  memset(&mod, 0, sizeof(mod_info));
  int res = snprintf(map_path, 4096, "/tmp/perf-%d.map", pid);
  if (res > 0 && res < 4096) {
    mod.name = map_path;
    mod.end_addr = -1;
    if (callback(&mod, 0, payload) < 0)
      goto done;
  }

done:
  fclose(procmap);
  return 0;
}

int bcc_procutils_each_ksym(bcc_procutils_ksymcb callback, void *payload) {
  char line[2048];
  char *symname, *endsym, *modname, *endmod = NULL;
  FILE *kallsyms;
  unsigned long long addr;

  kallsyms = fopen("/proc/kallsyms", "r");
  if (!kallsyms)
    return -1;

  while (fgets(line, sizeof(line), kallsyms)) {
    addr = strtoull(line, &symname, 16);
    if (addr == 0 || addr == ULLONG_MAX)
      continue;
    if (addr < kernelAddrSpace)
      continue;

    symname++;
    // Ignore data symbols
    if (*symname == 'b' || *symname == 'B' || *symname == 'd' ||
        *symname == 'D' || *symname == 'r' || *symname =='R')
      continue;

    endsym = (symname = symname + 2);
    while (*endsym && !isspace(*endsym)) endsym++;
    *endsym = '\0';

    // Parse module name if it's available
    modname = endsym + 1;
    while (*modname && isspace(*endsym)) modname++;

    if (*modname && *modname == '[') {
      endmod = ++modname;
      while (*endmod && *endmod != ']') endmod++;
      if (*endmod)
        *(endmod) = '\0';
      else
        endmod = NULL;
    }

    if (!endmod)
      modname = "kernel";

    callback(symname, modname, addr, payload);
  }

  fclose(kallsyms);
  return 0;
}

#define CACHE1_HEADER "ld.so-1.7.0"
#define CACHE1_HEADER_LEN (sizeof(CACHE1_HEADER) - 1)

#define CACHE2_HEADER "glibc-ld.so.cache"
#define CACHE2_HEADER_LEN (sizeof(CACHE2_HEADER) - 1)
#define CACHE2_VERSION "1.1"

struct ld_cache1_entry {
  int32_t flags;
  uint32_t key;
  uint32_t value;
};

struct ld_cache1 {
  char header[CACHE1_HEADER_LEN];
  uint32_t entry_count;
  struct ld_cache1_entry entries[0];
};

struct ld_cache2_entry {
  int32_t flags;
  uint32_t key;
  uint32_t value;
  uint32_t pad1_;
  uint64_t pad2_;
};

struct ld_cache2 {
  char header[CACHE2_HEADER_LEN];
  char version[3];
  uint32_t entry_count;
  uint32_t string_table_len;
  uint32_t pad_[5];
  struct ld_cache2_entry entries[0];
};

static int lib_cache_count;
static struct ld_lib {
  char *libname;
  char *path;
  int flags;
} * lib_cache;

static int read_cache1(const char *ld_map) {
  struct ld_cache1 *ldcache = (struct ld_cache1 *)ld_map;
  const char *ldstrings =
      (const char *)(ldcache->entries + ldcache->entry_count);
  uint32_t i;

  lib_cache =
      (struct ld_lib *)malloc(ldcache->entry_count * sizeof(struct ld_lib));
  lib_cache_count = (int)ldcache->entry_count;

  for (i = 0; i < ldcache->entry_count; ++i) {
    const char *key = ldstrings + ldcache->entries[i].key;
    const char *val = ldstrings + ldcache->entries[i].value;
    const int flags = ldcache->entries[i].flags;

    lib_cache[i].libname = strdup(key);
    lib_cache[i].path = strdup(val);
    lib_cache[i].flags = flags;
  }
  return 0;
}

static int read_cache2(const char *ld_map) {
  struct ld_cache2 *ldcache = (struct ld_cache2 *)ld_map;
  uint32_t i;

  if (memcmp(ld_map, CACHE2_HEADER, CACHE2_HEADER_LEN))
    return -1;

  lib_cache =
      (struct ld_lib *)malloc(ldcache->entry_count * sizeof(struct ld_lib));
  lib_cache_count = (int)ldcache->entry_count;

  for (i = 0; i < ldcache->entry_count; ++i) {
    const char *key = ld_map + ldcache->entries[i].key;
    const char *val = ld_map + ldcache->entries[i].value;
    const int flags = ldcache->entries[i].flags;

    lib_cache[i].libname = strdup(key);
    lib_cache[i].path = strdup(val);
    lib_cache[i].flags = flags;
  }
  return 0;
}

static int load_ld_cache(const char *cache_path) {
  struct stat st;
  size_t ld_size;
  const char *ld_map;
  int ret, fd = open(cache_path, O_RDONLY);

  if (fd < 0)
    return -1;

  if (fstat(fd, &st) < 0 || st.st_size < sizeof(struct ld_cache1)) {
    close(fd);
    return -1;
  }

  ld_size = st.st_size;
  ld_map = (const char *)mmap(NULL, ld_size, PROT_READ, MAP_PRIVATE, fd, 0);
  if (ld_map == MAP_FAILED) {
    close(fd);
    return -1;
  }

  if (memcmp(ld_map, CACHE1_HEADER, CACHE1_HEADER_LEN) == 0) {
    const struct ld_cache1 *cache1 = (struct ld_cache1 *)ld_map;
    size_t cache1_len = sizeof(struct ld_cache1) +
                        (cache1->entry_count * sizeof(struct ld_cache1_entry));
    cache1_len = (cache1_len + 0x7) & ~0x7ULL;

    if (ld_size > (cache1_len + sizeof(struct ld_cache2)))
      ret = read_cache2(ld_map + cache1_len);
    else
      ret = read_cache1(ld_map);
  } else {
    ret = read_cache2(ld_map);
  }

  munmap((void *)ld_map, ld_size);
  close(fd);
  return ret;
}

#define LD_SO_CACHE "/etc/ld.so.cache"
#define FLAG_TYPE_MASK 0x00ff
#define TYPE_ELF_LIBC6 0x0003
#define FLAG_ABI_MASK 0xff00
#define ABI_SPARC_LIB64 0x0100
#define ABI_IA64_LIB64 0x0200
#define ABI_X8664_LIB64 0x0300
#define ABI_S390_LIB64 0x0400
#define ABI_POWERPC_LIB64 0x0500
#define ABI_AARCH64_LIB64 0x0a00

static bool match_so_flags(int flags) {
  if ((flags & FLAG_TYPE_MASK) != TYPE_ELF_LIBC6)
    return false;

  switch (flags & FLAG_ABI_MASK) {
  case ABI_SPARC_LIB64:
  case ABI_IA64_LIB64:
  case ABI_X8664_LIB64:
  case ABI_S390_LIB64:
  case ABI_POWERPC_LIB64:
  case ABI_AARCH64_LIB64:
    return (sizeof(void *) == 8);
  }

  return sizeof(void *) == 4;
}

static bool which_so_in_process(const char* libname, int pid, char* libpath) {
  int ret, found = false;
  char endline[4096], *mapname = NULL, *newline;
  char mappings_file[128];
  const size_t search_len = strlen(libname) + strlen("/lib.");
  char search1[search_len + 1];
  char search2[search_len + 1];

  snprintf(mappings_file, sizeof(mappings_file), "/proc/%ld/maps", (long)pid);
  FILE *fp = fopen(mappings_file, "r");
  if (!fp)
    return NULL;

  snprintf(search1, search_len + 1, "/lib%s.", libname);
  snprintf(search2, search_len + 1, "/lib%s-", libname);

  do {
    ret = fscanf(fp, "%*x-%*x %*s %*x %*s %*d");
    if (!fgets(endline, sizeof(endline), fp))
      break;

    mapname = endline;
    newline = strchr(endline, '\n');
    if (newline)
      newline[0] = '\0';

    while (isspace(mapname[0])) mapname++;

    if (strstr(mapname, ".so") && (strstr(mapname, search1) ||
                                   strstr(mapname, search2))) {
      const size_t mapnamelen = strlen(mapname);
      if (mapnamelen >= PATH_MAX) {
        fprintf(stderr, "Found mapped library path is too long\n");
        break;
      }
      found = true;
      memcpy(libpath, mapname, mapnamelen + 1);
      break;
    }
  } while (ret != EOF);

  fclose(fp);
  return found;
}

static bool which_so_in_ldconfig_cache(const char* libname, char* libpath) {
  const size_t soname_len = strlen(libname) + strlen("lib.so");
  char soname[soname_len + 1];
  int i;

  if (lib_cache_count < 0)
    return false;

  if (!lib_cache_count && load_ld_cache(LD_SO_CACHE) < 0) {
    lib_cache_count = -1;
    return false;
  }

  snprintf(soname, soname_len + 1, "lib%s.so", libname);

  for (i = 0; i < lib_cache_count; ++i) {
    if (!strncmp(lib_cache[i].libname, soname, soname_len) &&
        match_so_flags(lib_cache[i].flags)) {
      
      const char* path = lib_cache[i].path;
      const size_t pathlen = strlen(path);
      if (pathlen >= PATH_MAX) {
        fprintf(stderr, "Found library path is too long\n");
        return false;
      }
      memcpy(libpath, path, pathlen + 1);
      return true;
    }
  }

  return false;
}

char *bcc_procutils_which_so(const char *libname, int pid) {
  char libpath[PATH_MAX];

  if (strchr(libname, '/'))
    return strdup(libname);

  if (pid && which_so_in_process(libname, pid, libpath))
    return strdup(libpath);

  if (which_so_in_ldconfig_cache(libname, libpath))
    return strdup(libpath);

  return NULL;
}

char *bcc_procutils_which_so_in_process(const char *libname, int pid) {
  char libpath[PATH_MAX];
  if (pid && which_so_in_process(libname, pid, libpath))
    return strdup(libpath);
  return NULL;
}

void bcc_procutils_free(const char *ptr) {
  free((void *)ptr);
}

/* Detects the following languages + C. */
const char *languages[] = {"java", "node", "perl", "php", "python", "ruby"};
const char *language_c = "c";
const int nb_languages = 6;

const char *bcc_procutils_language(int pid) {
  char procfilename[24], line[4096], pathname[32], *str;
  FILE *procfile;
  int i, ret;

  /* Look for clues in the absolute path to the executable. */
  snprintf(procfilename, sizeof(procfilename), "/proc/%ld/exe", (long)pid);
  if (realpath(procfilename, line)) {
    for (i = 0; i < nb_languages; i++)
      if (strstr(line, languages[i]))
        return languages[i];
  }

  snprintf(procfilename, sizeof(procfilename), "/proc/%ld/maps", (long)pid);
  procfile = fopen(procfilename, "r");
  if (!procfile)
    return NULL;

  /* Look for clues in memory mappings. */
  bool libc = false;
  do {
    char perm[8], dev[8];
    long long begin, end, size, inode;
    ret = fscanf(procfile, "%llx-%llx %s %llx %s %lld", &begin, &end, perm,
                 &size, dev, &inode);
    if (!fgets(line, sizeof(line), procfile))
      break;
    if (ret == 6) {
      char *mapname = line;
      char *newline = strchr(line, '\n');
      if (newline)
        newline[0] = '\0';
      while (isspace(mapname[0])) mapname++;
      for (i = 0; i < nb_languages; i++) {
        snprintf(pathname, sizeof(pathname), "/lib%s", languages[i]);
        if (strstr(mapname, pathname)) {
          fclose(procfile);
          return languages[i];
	}
        if ((str = strstr(mapname, "libc")) &&
            (str[4] == '-' || str[4] == '.'))
          libc = true;
      }
    }
  } while (ret && ret != EOF);

  fclose(procfile);

  /* Return C as the language if libc was found and nothing else. */
  return libc ? language_c : NULL;
}
