#ifdef _WIN32
  #include <direct.h>
  #include <winsock2.h>
  #include <windows.h>
  #include <fileapi.h>
#else
  #include <netinet/in.h>
  #include <netdb.h>
  #include <sys/socket.h>
  #include <arpa/inet.h>
  #define MAX_PATH PATH_MAX
#endif

#include <git2.h>
#include <string.h>
#include <stdio.h>
#include <errno.h>
#include <lua.h>
#include <lualib.h>
#include <lauxlib.h>
#include <ctype.h>
#include <dirent.h>
#include <unistd.h>
#include <fcntl.h>

#include <sys/stat.h>
#include <git2.h>
#include <mbedtls/sha256.h>
#include <mbedtls/x509.h>
#include <mbedtls/entropy.h>
#include <mbedtls/ctr_drbg.h>
#include <mbedtls/ssl.h>
#include <mbedtls/error.h>
#include <mbedtls/net.h>
#ifdef MBEDTLS_DEBUG_C
  #include <mbedtls/debug.h>
#endif

#include <zlib.h>
#include <microtar.h>
#include <zip.h>

#ifdef __APPLE__
  #include <Security/Security.h>
#endif

static char hex_digits[] = "0123456789abcdef";
static int lpm_hash(lua_State* L) {
  size_t len;
  const char* data = luaL_checklstring(L, 1, &len);
  const char* type = luaL_optstring(L, 2, "string");
  static const int digest_length = 32;
  unsigned char buffer[digest_length];
  mbedtls_sha256_context hash_ctx;
  mbedtls_sha256_init(&hash_ctx);
  mbedtls_sha256_starts_ret(&hash_ctx, 0);
  if (strcmp(type, "file") == 0) {
    FILE* file = fopen(data, "rb");
    if (!file) {
      mbedtls_sha256_free(&hash_ctx);
      return luaL_error(L, "can't open %s", data);
    }
    while (1) {
      unsigned char chunk[4096];
      size_t bytes = fread(chunk, 1, sizeof(chunk), file);
      mbedtls_sha256_update_ret(&hash_ctx, chunk, bytes);
      if (bytes < 4096)
        break;
    }
    fclose(file);
  } else {
    mbedtls_sha256_update_ret(&hash_ctx, data, len);
  }
  mbedtls_sha256_finish_ret(&hash_ctx, buffer);
  mbedtls_sha256_free(&hash_ctx);
  char hex_buffer[digest_length * 2 + 1];
  for (size_t i = 0; i < digest_length; ++i) {
    hex_buffer[i*2+0] = hex_digits[buffer[i] >> 4];
    hex_buffer[i*2+1] = hex_digits[buffer[i] & 0xF];
  }
  lua_pushlstring(L, hex_buffer, digest_length * 2);
  return 1;
}

int lpm_symlink(lua_State* L) {
  #ifndef _WIN32
    if (symlink(luaL_checkstring(L, 1), luaL_checkstring(L, 2)))
      return luaL_error(L, "can't create symlink %s: %s", luaL_checkstring(L, 2), strerror(errno));
    return 0;
  #else
    return luaL_error(L, "can't create symbolic link %s: your operating system sucks", luaL_checkstring(L, 2));
  #endif
}

int lpm_chmod(lua_State* L) {
  if (chmod(luaL_checkstring(L, 1), luaL_checkinteger(L, 2)))
    return luaL_error(L, "can't chmod %s: %s", luaL_checkstring(L, 1), strerror(errno));
  return 0;
}

/** BEGIN STOLEN LITE CODE **/
#if _WIN32
static LPWSTR utfconv_utf8towc(const char *str) {
  LPWSTR output;
  int len = MultiByteToWideChar(CP_UTF8, 0, str, -1, NULL, 0);
  if (len == 0)
    return NULL;
  output = (LPWSTR) malloc(sizeof(WCHAR) * len);
  if (output == NULL)
    return NULL;
  len = MultiByteToWideChar(CP_UTF8, 0, str, -1, output, len);
  if (len == 0) {
    free(output);
    return NULL;
  }
  return output;
}

static char *utfconv_wctoutf8(LPCWSTR str) {
  char *output;
  int len = WideCharToMultiByte(CP_UTF8, 0, str, -1, NULL, 0, NULL, NULL);
  if (len == 0)
    return NULL;
  output = (char *) malloc(sizeof(char) * len);
  if (output == NULL)
    return NULL;
  len = WideCharToMultiByte(CP_UTF8, 0, str, -1, output, len, NULL, NULL);
  if (len == 0) {
    free(output);
    return NULL;
  }
  return output;
}
#endif

static int lpm_ls(lua_State *L) {
  const char *path = luaL_checkstring(L, 1);

#ifdef _WIN32
  lua_settop(L, 1);
  lua_pushstring(L, path[0] == 0 || strchr("\\/", path[strlen(path) - 1]) != NULL ? "*" : "\\*");
  lua_concat(L, 2);
  path = lua_tostring(L, -1);

  LPWSTR wpath = utfconv_utf8towc(path);
  if (wpath == NULL)
    return luaL_error(L, "can't ls %s: invalid utf8 character conversion", path);
    
  WIN32_FIND_DATAW fd;
  HANDLE find_handle = FindFirstFileExW(wpath, FindExInfoBasic, &fd, FindExSearchNameMatch, NULL, 0);
  free(wpath);
  if (find_handle == INVALID_HANDLE_VALUE)
    return luaL_error(L, "can't ls %s: %d", path, GetLastError());
  char mbpath[MAX_PATH * 4]; // utf-8 spans 4 bytes at most
  int len, i = 1;
  lua_newtable(L);

  do
  {
    if (wcscmp(fd.cFileName, L".") == 0) { continue; }
    if (wcscmp(fd.cFileName, L"..") == 0) { continue; }

    len = WideCharToMultiByte(CP_UTF8, 0, fd.cFileName, -1, mbpath, MAX_PATH * 4, NULL, NULL);
    if (len == 0) { break; }
    lua_pushlstring(L, mbpath, len - 1); // len includes \0
    lua_rawseti(L, -2, i++);
  } while (FindNextFileW(find_handle, &fd));

  int err = GetLastError();
  FindClose(find_handle);
  if (err != ERROR_NO_MORE_FILES)
    return luaL_error(L, "can't ls %s: %d", path, GetLastError());
  return 1;
#else
  DIR *dir = opendir(path);
  if (!dir)
    return luaL_error(L, "can't ls %s: %d", path, strerror(errno));
  lua_newtable(L);
  int i = 1;
  struct dirent *entry;
  while ( (entry = readdir(dir)) ) {
    if (strcmp(entry->d_name, "." ) == 0) { continue; }
    if (strcmp(entry->d_name, "..") == 0) { continue; }
    lua_pushstring(L, entry->d_name);
    lua_rawseti(L, -2, i);
    i++;
  }
  closedir(dir);
  return 1;
#endif
}

static int lpm_rmdir(lua_State *L) {
  const char *path = luaL_checkstring(L, 1);
#ifdef _WIN32
  LPWSTR wpath = utfconv_utf8towc(path);
  int deleted = RemoveDirectoryW(wpath);
  free(wpath);
  if (!deleted)
    return luaL_error(L, "can't rmdir %s: %d", path, GetLastError());
#else
  if (remove(path))
    return luaL_error(L, "can't rmdir %s: %s", path, strerror(errno));
#endif
  return 0;
}

static int lpm_mkdir(lua_State *L) {
  const char *path = luaL_checkstring(L, 1);
#ifdef _WIN32
  LPWSTR wpath = utfconv_utf8towc(path);
  if (wpath == NULL)
    return luaL_error(L, "can't mkdir %s: invalid utf8 character conversion", path);
  int err = _wmkdir(wpath);
  free(wpath);
#else
  int err = mkdir(path, S_IRUSR|S_IWUSR|S_IXUSR|S_IRGRP|S_IXGRP|S_IROTH|S_IXOTH);
#endif
  if (err < 0) 
    return luaL_error(L, "can't mkdir %s: %s", path, strerror(errno));
  return 0;
}

static int lpm_stat(lua_State *L) {
  const char *path = luaL_checkstring(L, 1);
  lua_newtable(L);
#ifdef _WIN32
  #define realpath(x, y) _wfullpath(y, x, MAX_PATH)
  struct _stat s;
  LPWSTR wpath = utfconv_utf8towc(path);
  if (wpath == NULL)
    return luaL_error(L, "can't stat %s: invalid utf8 character conversion", path);
  int err = _wstat(wpath, &s);
  LPWSTR wfullpath = realpath(wpath, NULL);
  free(wpath);
  if (!wfullpath) return 0;
  char *abs_path = utfconv_wctoutf8(wfullpath);
  free(wfullpath);
#else
  struct stat s;
  int err = lstat(path, &s);
  char *abs_path = realpath(path, NULL);
#endif
  if (err || !abs_path) {
    lua_pushnil(L);
    lua_pushstring(L, strerror(errno));
    return 2;
  }
  lua_pushstring(L, abs_path); lua_setfield(L, -2, "abs_path");
  lua_pushvalue(L, 1); lua_setfield(L, -2, "path");

#if __linux__
  if (S_ISLNK(s.st_mode)) {
    char buffer[PATH_MAX];
    ssize_t len = readlink(path, buffer, sizeof(buffer));
    if (len < 0)
      return 0;
    lua_pushlstring(L, buffer, len);
  } else
    lua_pushnil(L);
  lua_setfield(L, -2, "symlink");
  if (S_ISLNK(s.st_mode))
    err = stat(path, &s);
  if (err)
    return 1;
#endif
  lua_pushinteger(L, s.st_mtime); lua_setfield(L, -2, "modified");
  lua_pushinteger(L, s.st_size); lua_setfield(L, -2, "size");
  lua_pushinteger(L, s.st_mode); lua_setfield(L, -2, "mode");
  if (S_ISREG(s.st_mode)) {
    lua_pushstring(L, "file");
  } else if (S_ISDIR(s.st_mode)) {
    lua_pushstring(L, "dir");
  } else {
    lua_pushnil(L);
  }
  lua_setfield(L, -2, "type");
  return 1;
}
/** END STOLEN LITE CODE **/

static const char* git_error_last_string() {
  const git_error* last_error = git_error_last();
  return last_error->message;
}

static int git_get_id(git_oid* commit_id, git_repository* repository, const char* name) {
  int length = strlen(name);
  int is_hex = length == 40;
  for (int i = 0; is_hex && i < length; ++i)
    is_hex = isxdigit(name[i]);
  if (!is_hex)
    return git_reference_name_to_id(commit_id, repository, name);
  return git_oid_fromstr(commit_id, name);
}

static git_repository* luaL_checkgitrepo(lua_State* L, int index) {
  const char* path = luaL_checkstring(L, index);
  git_repository* repository;
  if (git_repository_open(&repository, path))
    return (void*)(long long)luaL_error(L, "git open error: %s", git_error_last_string());
  return repository;
}


static git_commit* git_retrieve_commit(git_repository* repository, const char* commit_name) {
  git_oid commit_id;
  git_commit* commit;
  if (git_get_id(&commit_id, repository, commit_name) || git_commit_lookup(&commit, repository, &commit_id))
    return NULL;
  return commit;
}


static int lpm_reset(lua_State* L) {
  git_repository* repository = luaL_checkgitrepo(L, 1);
  const char* commit_name = luaL_checkstring(L, 2);
  const char* type = luaL_checkstring(L, 3);
  git_commit* commit = git_retrieve_commit(repository, commit_name);
  if (!commit) {
    git_repository_free(repository);
    return luaL_error(L, "git retrieve commit error: %s", git_error_last_string());
  }
  git_reset_t reset_type = GIT_RESET_SOFT;
  if (strcmp(type, "mixed") == 0)
    reset_type = GIT_RESET_MIXED;
  else if (strcmp(type, "hard") == 0)
    reset_type = GIT_RESET_HARD;
  int result = git_reset(repository, (git_object*)commit, reset_type, NULL);
  git_commit_free(commit);
  git_repository_free(repository);
  if (result)
    return luaL_error(L, "git reset error: %s", git_error_last_string());
  return 0;
}

static int lpm_revparse(lua_State* L) {
  git_repository* repository = luaL_checkgitrepo(L, 1);
  git_oid commit_id;
  int got_commit = git_get_id(&commit_id, repository, "HEAD");
  git_repository_free(repository);
  if (got_commit)
    return luaL_error(L, "git retrieve commit error: %s", git_error_last_string());
  int digest_length = sizeof(commit_id.id);
  char hex_buffer[digest_length * 2];
  for (size_t i = 0; i < digest_length; ++i) {
    hex_buffer[i*2+0] = hex_digits[commit_id.id[i] >> 4];
    hex_buffer[i*2+1] = hex_digits[commit_id.id[i] & 0xF];
  }
  lua_pushlstring(L, hex_buffer, digest_length * 2);
  return 1;
}

static int lpm_init(lua_State* L) {
  const char* path = luaL_checkstring(L, 1);
  const char* url = luaL_checkstring(L, 2);
  git_repository* repository;
  if (git_repository_init(&repository, path, 0))
    return luaL_error(L, "git init error: %s", git_error_last_string());
  git_remote* remote;
  if (git_remote_create(&remote, repository, "origin", url)) {
    git_repository_free(repository);
    return luaL_error(L, "git remote add error: %s", git_error_last_string());
  }
  git_remote_free(remote);
  git_repository_free(repository);
  return 0;
}

static int no_verify_ssl, has_setup_ssl, print_trace;
static mbedtls_x509_crt x509_certificate;
static mbedtls_entropy_context entropy_context;
static mbedtls_ctr_drbg_context drbg_context;
static mbedtls_ssl_config ssl_config;
static mbedtls_ssl_context ssl_context;

static int lpm_git_transport_certificate_check_cb(struct git_cert *cert, int valid, const char *host, void *payload) {
  return 0; // If no_verify_ssl is enabled, basically always return 0 when this is set as callback.
}

static int lpm_git_transfer_progress_cb(const git_transfer_progress *stats, void *payload) {
  lua_State* L = payload;
  lua_pushvalue(L, 2);
  lua_pushinteger(L, stats->received_bytes);
  lua_pushinteger(L, stats->total_objects);
  lua_pushinteger(L, stats->indexed_objects);
  lua_pushinteger(L, stats->received_objects);
  lua_pushinteger(L, stats->local_objects);
  lua_pushinteger(L, stats->total_deltas);
  lua_pushinteger(L, stats->indexed_deltas);
  lua_call(L, 7, 1);
  int value = lua_tointeger(L, -1);
  lua_pop(L, 1);
  return value;
}

static int lpm_fetch(lua_State* L) {
  git_repository* repository = luaL_checkgitrepo(L, 1);
  git_remote* remote;
  if (git_remote_lookup(&remote, repository, "origin")) {
    git_repository_free(repository);
    return luaL_error(L, "git remote fetch error: %s", git_error_last_string());
  }
  git_fetch_options fetch_opts = GIT_FETCH_OPTIONS_INIT;
  fetch_opts.download_tags = GIT_REMOTE_DOWNLOAD_TAGS_ALL;
  fetch_opts.callbacks.payload = L;
  if (no_verify_ssl)
    fetch_opts.callbacks.certificate_check = lpm_git_transport_certificate_check_cb;
  if (lua_type(L, 2) == LUA_TFUNCTION)
    fetch_opts.callbacks.transfer_progress = lpm_git_transfer_progress_cb;
  if (git_remote_fetch(remote, NULL, &fetch_opts, NULL)) {
    git_remote_free(remote);
    git_repository_free(repository);
    return luaL_error(L, "git remote fetch error: %s", git_error_last_string());
  }
  git_remote_free(remote);
  git_repository_free(repository);
  if (lua_type(L, 2) == LUA_TFUNCTION) {
    lua_pushvalue(L, 2);
    lua_pushboolean(L, 1);
    lua_call(L, 1, 0);
  }
  return 0;
}

static int mbedtls_snprintf(char* buffer, int len, int status, const char* str, ...) {
  char mbed_buffer[128];
  mbedtls_strerror(status, mbed_buffer, sizeof(mbed_buffer));
  int error_len = strlen(mbed_buffer);
  va_list va;
  int offset = 0;
  va_start(va, str);
    offset = vsnprintf(buffer, len, str, va);
  va_end(va);
  if (offset < len - 2) {
    strcat(buffer, ": ");
    if (offset < len - error_len - 2)
      strcat(buffer, mbed_buffer);
  }
  return strlen(buffer);
}

static int luaL_mbedtls_error(lua_State* L, int code, const char* str, ...) {
  char vsnbuffer[1024];
  char mbed_buffer[128];
  mbedtls_strerror(code, mbed_buffer, sizeof(mbed_buffer));
  va_list va;
  va_start(va, str);
      vsnprintf(vsnbuffer, sizeof(vsnbuffer), str, va);
  va_end(va);
  return luaL_error(L, "%s: %s", vsnbuffer, mbed_buffer);
}

static void lpm_tls_debug(void *ctx, int level, const char *file, int line, const char *str) {
  fprintf(stderr, "%s:%04d: |%d| %s", file, line, level, str);
  fflush(stderr);
}

static void lpm_libgit2_debug(git_trace_level_t level, const char *msg) {
  fprintf(stderr, "[libgit2]: %s\n", msg);
  fflush(stderr);
}

static int lpm_trace(lua_State* L) {
  print_trace = lua_toboolean(L, 1) ? 1 : 0;
  return 0;
}

static int lpm_certs(lua_State* L) {
  const char* type = luaL_checkstring(L, 1);
  int status;
  if (has_setup_ssl) {
    mbedtls_ssl_config_free(&ssl_config);
    mbedtls_ctr_drbg_free(&drbg_context);
    mbedtls_entropy_free(&entropy_context);
    mbedtls_x509_crt_free(&x509_certificate);
  }
  mbedtls_x509_crt_init(&x509_certificate);
  mbedtls_entropy_init(&entropy_context);
  mbedtls_ctr_drbg_init(&drbg_context);
  if ((status = mbedtls_ctr_drbg_seed(&drbg_context, mbedtls_entropy_func, &entropy_context, NULL, 0)) != 0)
    return luaL_mbedtls_error(L, status, "failed to setup mbedtls_x509");
  mbedtls_ssl_config_init(&ssl_config);
  status = mbedtls_ssl_config_defaults(&ssl_config, MBEDTLS_SSL_IS_CLIENT, MBEDTLS_SSL_TRANSPORT_STREAM, MBEDTLS_SSL_PRESET_DEFAULT);
  if (status)
    return luaL_mbedtls_error(L, status, "can't set ssl_config defaults");
  mbedtls_ssl_conf_max_version(&ssl_config, MBEDTLS_SSL_MAJOR_VERSION_3, MBEDTLS_SSL_MINOR_VERSION_3);
  mbedtls_ssl_conf_min_version(&ssl_config, MBEDTLS_SSL_MAJOR_VERSION_3, MBEDTLS_SSL_MINOR_VERSION_3);
  mbedtls_ssl_conf_authmode(&ssl_config, MBEDTLS_SSL_VERIFY_REQUIRED);
  mbedtls_ssl_conf_rng(&ssl_config, mbedtls_ctr_drbg_random, &drbg_context);
  mbedtls_ssl_conf_read_timeout(&ssl_config, 5000);
  #if defined(MBEDTLS_DEBUG_C)
  if (print_trace) {
    mbedtls_debug_set_threshold(5);
    mbedtls_ssl_conf_dbg(&ssl_config, lpm_tls_debug, NULL);
    git_trace_set(GIT_TRACE_TRACE, lpm_libgit2_debug);
  }
  #endif
  has_setup_ssl = 1;
  if (strcmp(type, "noverify") == 0) {
    no_verify_ssl = 1;
    mbedtls_ssl_conf_authmode(&ssl_config, MBEDTLS_SSL_VERIFY_OPTIONAL);
  } else {
    const char* path = luaL_checkstring(L, 2);
    if (strcmp(type, "dir") == 0) {
      git_libgit2_opts(GIT_OPT_SET_SSL_CERT_LOCATIONS, NULL, path);
    } else {
      if (strcmp(type, "system") == 0) {
        #if _WIN32
          FILE* file = fopen(path, "wb");
          if (!file)
            return luaL_error(L, "can't open cert store %s for writing: %s", path, strerror(errno));
          HCERTSTORE hSystemStore = CertOpenSystemStore(0, TEXT("ROOT"));
          if (!hSystemStore) {
            fclose(file);
            return luaL_error(L, "error getting system certificate store");
          }
          PCCERT_CONTEXT pCertContext = NULL;
          while (1) {
            pCertContext = CertEnumCertificatesInStore(hSystemStore, pCertContext);
            if (!pCertContext)
              break;
            BYTE keyUsage[2];
            if (pCertContext->dwCertEncodingType & X509_ASN_ENCODING && (CertGetIntendedKeyUsage(pCertContext->dwCertEncodingType, pCertContext->pCertInfo, keyUsage, sizeof(keyUsage)) && (keyUsage[0] & CERT_KEY_CERT_SIGN_KEY_USAGE))) {
              DWORD size = 0;
              CryptBinaryToString(pCertContext->pbCertEncoded, pCertContext->cbCertEncoded, CRYPT_STRING_BASE64HEADER, NULL, &size);
              char* buffer = malloc(size);
              CryptBinaryToString(pCertContext->pbCertEncoded, pCertContext->cbCertEncoded, CRYPT_STRING_BASE64HEADER, buffer, &size);
              fwrite(buffer, sizeof(char), size, file);
              free(buffer);
            }
          }
          fclose(file);
          CertCloseStore(hSystemStore, 0);
        #elif __APPLE__ // https://developer.apple.com/forums/thread/691009; see also curl's mac version
          return luaL_error(L, "can't use system on mac yet");
        #else
          return luaL_error(L, "can't use system certificates except on windows or mac");
        #endif
      }
      git_libgit2_opts(GIT_OPT_SET_SSL_CERT_LOCATIONS, path, NULL);
      if ((status = mbedtls_x509_crt_parse_file(&x509_certificate, path)) != 0)
        return luaL_mbedtls_error(L, status, "mbedtls_x509_crt_parse_file failed to parse CA certificate %s", path);
      mbedtls_ssl_conf_ca_chain(&ssl_config, &x509_certificate, NULL);
    }
  }
  return 0;
}


static int mkdirp(char* path, int len) {
  for (int i = 0; i < len; ++i) {
    if (path[i] == '/' && i > 0) {
      path[i] = 0;
      #ifndef _WIN32
        if (mkdir(path, S_IRUSR|S_IWUSR|S_IXUSR|S_IRGRP|S_IXGRP|S_IROTH|S_IXOTH) && errno != EEXIST)
      #else
        if (mkdir(path) && errno != EEXIST)
      #endif
        return -1;
      path[i] = '/';
    }
  }
  return 0;
}

static int gzip_read(mtar_t* tar, void* data, unsigned int size) { return gzread(tar->stream, data, size) >= 0 ? MTAR_ESUCCESS : -1; }
static int gzip_seek(mtar_t* tar, unsigned int pos) { return gzseek(tar->stream, pos, SEEK_SET) >= 0 ? MTAR_ESUCCESS : -1; }
static int gzip_close(mtar_t* tar) { return gzclose(tar->stream) == Z_OK ? MTAR_ESUCCESS : -1; }

#define FA_RDONLY       0x01            // FILE_ATTRIBUTE_READONLY
#define FA_DIREC        0x10            // FILE_ATTRIBUTE_DIRECTORY

static int lpm_extract(lua_State* L) {
  const char* src = luaL_checkstring(L, 1);
  const char* dst = luaL_checkstring(L, 2);

  if (strstr(src, ".zip")) {
    int zip_error_code;
    zip_t* archive = zip_open(src, ZIP_RDONLY, &zip_error_code);
    if (!archive) {
      zip_error_t zip_error;
      zip_error_init_with_code(&zip_error, zip_error_code);
      lua_pushfstring(L, "can't open zip archive %s: %s", src, zip_error_strerror(&zip_error));
      zip_error_fini(&zip_error);
      return lua_error(L);
    }
    zip_int64_t entries = zip_get_num_entries(archive, 0);
    for (zip_int64_t i = 0; i < entries; ++i) {    
      zip_file_t* zip_file = zip_fopen_index(archive, i, 0);
      const char* zip_name = zip_get_name(archive, i, ZIP_FL_ENC_GUESS);
      if (!zip_file) {
        lua_pushfstring(L, "can't read zip archive file %s: %s", zip_name, zip_strerror(archive));
        zip_close(archive);
        return lua_error(L);
      }
      char target[MAX_PATH];
      int target_length = snprintf(target, sizeof(target), "%s/%s", dst, zip_name);
      if (mkdirp(target, target_length)) {
        zip_fclose(zip_file);
        zip_close(archive);
        return luaL_error(L, "can't extract zip archive file %s, can't create directory %s: %s", src, target, strerror(errno));
      }
      if (target[target_length-1] != '/') {
        FILE* file = fopen(target, "wb");
        if (!file) {
          zip_fclose(zip_file);
          zip_close(archive);
          return luaL_error(L, "can't write file %s: %s", target, strerror(errno));
        }
        
        mode_t m = S_IRUSR | S_IRGRP | S_IROTH;
        zip_uint8_t os;
        zip_uint32_t attr;
        zip_file_get_external_attributes(archive, i, 0, &os, &attr);
        if (os == ZIP_OPSYS_DOS) {
          if (0 == (attr & FA_RDONLY))
              m |= S_IWUSR | S_IWGRP | S_IWOTH;
          if (attr & FA_DIREC)
              m = (S_IFDIR | (m & ~S_IFMT)) | S_IXUSR | S_IXGRP | S_IXOTH;
        } else {
          m = (attr >> 16) & ~0222; 
        }
        if (chmod(target, m)) {
          zip_fclose(zip_file);
          zip_close(archive);
          return luaL_error(L, "can't chmod file %s: %s", target, strerror(errno));
        }
        while (1) {
          char buffer[8192];
          zip_int64_t length = zip_fread(zip_file, buffer, sizeof(buffer));
          if (length == -1) {
            lua_pushfstring(L, "can't read zip archive file  %s: %s", zip_name, zip_file_strerror(zip_file));
            zip_fclose(zip_file);
            zip_close(archive);
            return lua_error(L);
          }
          if (length == 0) break;
          fwrite(buffer, sizeof(char), length, file);
        }  
        fclose(file);
      }
      zip_fclose(zip_file);
    }
    zip_close(archive);
  } else if (strstr(src, ".tar")) {
    mtar_t tar = {0};
    int err;
    char actual_src[PATH_MAX];
    if (strstr(src, ".gz")) {
      gzFile gzfile = gzopen(src, "rb");
      if (!gzfile)
        return luaL_error(L, "can't open tar.gz archive %s: %s", src, strerror(errno));
      /* It's increidbly slow to do it this way, probably because of all the seeking.
      For now, just gunzip the whole file at once, and then untar it.
      tar.read = gzip_read;
      tar.seek = gzip_seek;
      tar.close = gzip_close;*/
      char buffer[8192];
      int len = strlen(src);
      strncpy(actual_src, src, len - 3);
      actual_src[len-3] = 0;
      FILE* file = fopen(actual_src, "wb");
      while (1) {
        int length = gzread(gzfile, buffer, sizeof(buffer));
        if (length == 0)
          break;
        fwrite(buffer, sizeof(char), length, file);
      }
      char error[128];
      error[0] = 0;
      if (!gzeof(gzfile)) {
        int error_number;
        strncpy(error, gzerror(gzfile, &error_number), sizeof(error));
        error[sizeof(error)-1] = 0;
      }
      fclose(file);
      gzclose(gzfile);
      if (error[0])
        return luaL_error(L, "can't unzip tar archive %s: %s", src, error);
    } else {
      strcpy(actual_src, src);
    }
    if ((err = mtar_open(&tar, actual_src, "r")))
      return luaL_error(L, "can't open tar archive %s: %s", src, mtar_strerror(err));
    mtar_header_t h;
    while ((mtar_read_header(&tar, &h)) != MTAR_ENULLRECORD ) {
      if (h.type == MTAR_TREG) {
        char target[MAX_PATH];
        int target_length = snprintf(target, sizeof(target), "%s/%s", dst, h.name);
        if (mkdirp(target, target_length)) {
          mtar_close(&tar);
          return luaL_error(L, "can't extract tar archive file %s, can't create directory %s: %s", src, target, strerror(errno));
        }
        char buffer[8192];
        FILE* file = fopen(target, "wb");
        if (!file) {
          mtar_close(&tar);
          return luaL_error(L, "can't extract tar archive file %s, can't create file %s: %s", src, target, strerror(errno));
        }
        if (chmod(target, h.mode))
           return luaL_error(L, "can't extract tar archive file %s, can't chmod file %s: %s", src, target, strerror(errno));
        int remaining = h.size;
        while (remaining > 0) {
          int read_size = remaining < sizeof(buffer) ? remaining : sizeof(buffer);
          if (mtar_read_data(&tar, buffer, read_size) != MTAR_ESUCCESS) {
            fclose(file);
            mtar_close(&tar);
            return luaL_error(L, "can't write file %s: %s", target, strerror(errno));
          }
          fwrite(buffer, sizeof(char), read_size, file);
          remaining -= read_size;
        }
        fclose(file);
      }
      mtar_next(&tar);
    }
    mtar_close(&tar);
    if (strstr(src, ".gz"))
      unlink(actual_src);
  } else
    return luaL_error(L, "unrecognized archive format %s", src);
  return 0;
}


static int lpm_socket_write(int fd, const char* buf, int len, mbedtls_ssl_context* ctx) {
  if (ctx)
    return mbedtls_ssl_write(ctx, buf, len);
  return write(fd, buf, len);
}

static int lpm_socket_read(int fd, char* buf, int len, mbedtls_ssl_context* ctx) {
  if (ctx)
    return mbedtls_ssl_read(ctx, buf, len);
  return read(fd, buf, len);
}

static int strncicmp(const char* a, const char* b, int n) {
  for (int i = 0; i < n; ++i) {
    if (a[i] == 0 && b[i] != 0) return -1;
    if (a[i] != 0 && b[i] == 0) return 1;
    int lowera = tolower(a[i]), lowerb = tolower(b[i]);
    if (lowera == lowerb) continue;
    if (lowera < lowerb) return -1;
    return 1;
  }
  return 0;
}

static const char* get_header(const char* buffer, const char* header, int* len) {
  const char* line_end = strstr(buffer, "\r\n");
  const char* header_end = strstr(buffer, "\r\n\r\n");
  int header_len = strlen(header);
  while (line_end && line_end < header_end) {
    if (strncicmp(line_end + 2, header, header_len) == 0) {
      const char* offset = line_end + header_len + 3;
      while (*offset == ' ') { ++offset; }
      const char* end = strstr(offset, "\r\n");
      if (len)
        *len = end - offset;
      return offset;
    }
    line_end = strstr(line_end + 2, "\r\n");
  }
  return NULL;
}

static int lpm_get(lua_State* L) {
  long response_code;
  char err[1024] = {0};
  const char* protocol = luaL_checkstring(L, 1);
  const char* hostname = luaL_checkstring(L, 2);

  int s = -2;
  mbedtls_net_context net_context;
  mbedtls_ssl_context ssl_context;
  mbedtls_ssl_context* ssl_ctx = NULL;
  mbedtls_net_context* net_ctx = NULL;
  if (strcmp(protocol, "https") == 0) {
    int status;
    const char* port = lua_tostring(L, 3);
    // https://gist.github.com/Barakat/675c041fd94435b270a25b5881987a30
    ssl_ctx = &ssl_context;
    mbedtls_ssl_init(&ssl_context);
    if ((status = mbedtls_ssl_setup(&ssl_context, &ssl_config)) != 0) {
      mbedtls_snprintf(err, sizeof(err), status, "can't set up ssl for %s: %d", hostname, status); goto cleanup;
    }
    net_ctx = &net_context;
    mbedtls_net_init(&net_context);
    mbedtls_net_set_block(&net_context);
    mbedtls_ssl_set_bio(&ssl_context, &net_context, mbedtls_net_send, NULL, mbedtls_net_recv_timeout);
    if ((status = mbedtls_net_connect(&net_context, hostname, port, MBEDTLS_NET_PROTO_TCP)) != 0) {
      mbedtls_snprintf(err, sizeof(err), status, "can't connect to hostname %s", hostname); goto cleanup;
    } else if ((status = mbedtls_ssl_set_hostname(&ssl_context, hostname)) != 0) {
      mbedtls_snprintf(err, sizeof(err), status, "can't set hostname %s", hostname); goto cleanup;
    } else if ((status = mbedtls_ssl_handshake(&ssl_context)) != 0) {
      mbedtls_snprintf(err, sizeof(err), status, "can't handshake with %s", hostname); goto cleanup;
    } else if (((status = mbedtls_ssl_get_verify_result(&ssl_context)) != 0) && !no_verify_ssl) {
      mbedtls_snprintf(err, sizeof(err), status, "can't verify result for %s", hostname); goto cleanup;
    }
  } else {
    int port = luaL_checkinteger(L, 3);
    struct hostent *host = gethostbyname(hostname);
    struct sockaddr_in dest_addr = {0};
    if (!host)
      return luaL_error(L, "can't resolve hostname %s", hostname);
    s = socket(AF_INET, SOCK_STREAM, 0);
    #ifdef _WIN32
      DWORD timeout = 5 * 1000;
      setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, (const char*)&timeout, sizeof timeout);
    #else
      struct timeval tv;
      tv.tv_sec = 5;
      tv.tv_usec = 0;
      setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, (const char*)&tv, sizeof tv);
    #endif
    dest_addr.sin_family = AF_INET;
    dest_addr.sin_port = htons(port);
    dest_addr.sin_addr.s_addr = *(long*)(host->h_addr);
    const char* ip = inet_ntoa(dest_addr.sin_addr);
    if (connect(s, (struct sockaddr *) &dest_addr, sizeof(struct sockaddr)) == -1 ) {
      close(s);
      return luaL_error(L, "can't connect to host %s [%s] on port %d", hostname, ip, port);
    }
  }
  
  const char* rest = luaL_checkstring(L, 4);
  char buffer[4096]; 
  int buffer_length = snprintf(buffer, sizeof(buffer), "GET %s HTTP/1.1\r\nHost: %s\r\nConnection: close\r\n\r\n", rest, hostname);
  buffer_length = lpm_socket_write(s, buffer, buffer_length, ssl_ctx);
  if (buffer_length < 0) {
    snprintf(err, sizeof(err), "can't write to socket %s: %s", hostname, strerror(errno)); goto cleanup;
  }
  int bytes_read = 0;
  const char* header_end = NULL;
  while (!header_end && bytes_read < sizeof(buffer)) {
    buffer_length = lpm_socket_read(s, &buffer[bytes_read], sizeof(buffer) - bytes_read - 1, ssl_ctx);
    if (buffer_length < 0) {
      snprintf(err, sizeof(err), "can't read from socket %s: %s", hostname,strerror(errno)); goto cleanup;
    }
    bytes_read += buffer_length;
    buffer[bytes_read] = 0;
    header_end = strstr(buffer, "\r\n\r\n");
  }
  if (!header_end) {
    snprintf(err, sizeof(err), "can't parse response headers for %s%s", hostname, rest); goto cleanup;
  }
  header_end += 4;
  const char* protocol_end = strstr(buffer, " ");
  int code = atoi(protocol_end + 1);
  if (code != 200) {
    if (code >= 301 && code <= 303) {
      int len;
      const char* location = get_header(buffer, "location", &len);
      if (location) {
        lua_pushnil(L);
        lua_newtable(L);
        lua_pushlstring(L, location, len);
        lua_setfield(L, -2, "location");
      } else
        snprintf(err, sizeof(err), "received invalid %d-response from %s%s: %d", code, hostname, rest, code); 
      goto cleanup;
    } else {
      snprintf(err, sizeof(err), "received non 200-response from %s%s: %d", hostname, rest, code); goto cleanup;
    }
  }
  const char* content_length_value = get_header(buffer, "content-length", NULL);
  int content_length = -1;
  if (content_length_value)
    content_length = atoi(content_length_value);
  const char* path = luaL_optstring(L, 5, NULL);
  int callback_function = lua_type(L, 6) == LUA_TFUNCTION ? 6 : 0;
  
  int body_length = buffer_length - (header_end - buffer);
  int total_downloaded = body_length;
  int remaining = content_length - body_length;
  if (path) {
    FILE* file = fopen(path, "wb");
    fwrite(header_end, sizeof(char), body_length, file);
    while (content_length == -1 || remaining > 0) {
      int length = lpm_socket_read(s, buffer, sizeof(buffer), ssl_ctx);
      if (length == 0) break;
      if (length < 0) {
        snprintf(err, sizeof(err), "error retrieving full response for %s%s: %s (%d)", hostname, rest, strerror(errno), length); goto cleanup;
      }
      if (callback_function) {
        lua_pushvalue(L, callback_function);
        lua_pushinteger(L, total_downloaded);
        lua_call(L, 1, 0);
      }
      fwrite(buffer, sizeof(char), length, file);
      remaining -= length;
      total_downloaded += length;
    }
    fclose(file);
    lua_pushnil(L);
  } else {
    luaL_Buffer B;
    luaL_buffinit(L, &B);
    luaL_addlstring(&B, header_end, body_length);
    while (content_length == -1 || remaining > 0) {
      int length = lpm_socket_read(s, buffer, sizeof(buffer), ssl_ctx);
      if (length == 0) break;
      if (length < 0) {
        snprintf(err, sizeof(err), "error retrieving full response for %s%s: %s (%d)", hostname, rest, strerror(errno), length); goto cleanup;
      }
      if (callback_function) {
        lua_pushvalue(L, callback_function);
        lua_pushinteger(L, total_downloaded);
        lua_call(L, 1, 0);
      }
      luaL_addlstring(&B, buffer, length);
      remaining -= length;
      total_downloaded += length;
    }
    luaL_pushresult(&B);
  }
  if (content_length != -1 && remaining != 0) {
    snprintf(err, sizeof(err), "error retrieving full response for %s%s", hostname, rest); goto cleanup;
  }
  if (callback_function) {
    lua_pushvalue(L, callback_function);
    lua_pushboolean(L, 1);
    lua_call(L, 1, 0);
  }
  lua_newtable(L);
  cleanup:
    if (ssl_ctx)
      mbedtls_ssl_free(ssl_ctx);
    if (net_ctx)
      mbedtls_net_free(net_ctx);
    if (s != -2) 
      close(s);
    if (err[0])
      return luaL_error(L, "%s", err);
  return 2;
}

static int lpm_chdir(lua_State* L) {
  if (chdir(luaL_checkstring(L, 1)))
    return luaL_error(L, "error chdiring: %s", strerror(errno));
  return 0;
}

static int lpm_pwd(lua_State* L) {
  char buffer[MAX_PATH];
  if (!getcwd(buffer, sizeof(buffer)))
    return luaL_error(L, "error getcwd: %s", strerror(errno));
  lua_pushstring(L, buffer);
  return 1;
}

static const luaL_Reg system_lib[] = {
  { "ls",        lpm_ls    },    // Returns an array of files.
  { "stat",      lpm_stat  },    // Returns info about a single file.
  { "mkdir",     lpm_mkdir },    // Makes a directory.
  { "rmdir",     lpm_rmdir },    // Removes a directory.
  { "hash",      lpm_hash  },    // Returns a hex sha256 hash.
  { "symlink",   lpm_symlink },  // Creates a symlink.
  { "chmod",     lpm_chmod },    // Chmod's a file.
  { "init",      lpm_init },     // Initializes a git repository with the specified remote.
  { "fetch",     lpm_fetch },    // Updates a git repository with the specified remote.
  { "reset",     lpm_reset },    // Updates a git repository to the specified commit/hash/branch.
  { "revparse",  lpm_revparse }, // Gets a commit id.
  { "get",       lpm_get },      // HTTP(s) GET request.
  { "extract",   lpm_extract },  // Extracts .tar.gz, and .zip files.
  { "trace",     lpm_trace },    // Sets trace bit.
  { "certs",     lpm_certs },    // Sets the SSL certificate chain folder/file.
  { "chdir",     lpm_chdir },    // Changes directory. Only use for --post actions.
  { "pwd",       lpm_pwd },      // Gets existing directory. Only use for --post actions.
  { NULL,        NULL }
};


#ifndef LPM_VERSION
  #define LPM_VERSION "unknown"
#endif


#ifndef ARCH_PROCESSOR
  #if __x86_64__ || _WIN64 || __MINGW64__
    #define ARCH_PROCESSOR "x86_64"
  #elif __i386__
    #define ARCH_PROCESSOR "x86"
  #else
    #error "Please define -DARCH_PROCESSOR."
  #endif
#endif
#ifndef ARCH_PLATFORM
  #if _WIN32
    #define ARCH_PLATFORM "windows"
  #elif __linux__
    #define ARCH_PLATFORM "linux"
  #elif __APPLE__
    #define ARCH_PLATFORM "darwin"
  #else
    #error "Please define -DARCH_PLATFORM."
  #endif
#endif
#ifndef LITE_ARCH_TUPLE
  #define LITE_ARCH_TUPLE ARCH_PROCESSOR "-" ARCH_PLATFORM
#endif


#ifdef LPM_STATIC
  extern const char src_lpm_luac[];
  extern unsigned int src_lpm_luac_len;
#endif

int main(int argc, char* argv[]) {
  git_libgit2_init();
  lua_State* L = luaL_newstate();
  luaL_openlibs(L);
  luaL_newlib(L, system_lib); 
  lua_setglobal(L, "system");
  lua_newtable(L);
  for (int i = 0; i < argc; ++i) {
    lua_pushstring(L, argv[i]);
    lua_rawseti(L, -2, i+1);
  }
  lua_setglobal(L, "ARGV");
  lua_pushliteral(L, LPM_VERSION);
  lua_setglobal(L, "VERSION");
  lua_pushliteral(L, ARCH_PLATFORM);
  lua_setglobal(L, "PLATFORM");
  #if _WIN32 
    lua_pushliteral(L, "\\");
  #else
    lua_pushliteral(L, "/");
  #endif
  lua_setglobal(L, "PATHSEP");
  lua_pushliteral(L, LITE_ARCH_TUPLE);
  lua_setglobal(L, "ARCH");
  #ifndef LPM_STATIC
  if (luaL_loadfile(L, "src/lpm.lua") || lua_pcall(L, 0, 1, 0)) {
  #else
  if (luaL_loadbuffer(L, src_lpm_luac, src_lpm_luac_len, "lpm.lua") || lua_pcall(L, 0, 1, 0)) {
  #endif
    fprintf(stderr, "internal error when starting the application: %s\n", lua_tostring(L, -1));
    return -1;
  }
  int status = lua_tointeger(L, -1);
  lua_close(L);
  git_libgit2_shutdown();
  return status;
}