diff options
Diffstat (limited to 'src/lpm.c')
-rw-r--r-- | src/lpm.c | 575 |
1 files changed, 575 insertions, 0 deletions
diff --git a/src/lpm.c b/src/lpm.c new file mode 100644 index 0000000..760c372 --- /dev/null +++ b/src/lpm.c @@ -0,0 +1,575 @@ +#include <git2.h> +#include <string.h> +#include <stdio.h> +#include <errno.h> +#include <lua.h> +#include <lauxlib.h> +#include <ctype.h> +#include <lualib.h> +#include <dirent.h> +#include <unistd.h> +#include <archive.h> +#include <archive_entry.h> + +#include <sys/stat.h> +#include <git2.h> +#include <openssl/evp.h> +#include <curl/curl.h> + +#ifdef _WIN32 + #include <direct.h> + #include <winsock2.h> + #include <windows.h> + #include <fileapi.h> +#else + #define MAX_PATH PATH_MAX +#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"); + unsigned char buffer[EVP_MAX_MD_SIZE]; + EVP_MD_CTX* c = EVP_MD_CTX_new(); + EVP_MD_CTX_init(c); + EVP_DigestInit_ex(c, EVP_sha256(), NULL); + if (strcmp(type, "file") == 0) { + FILE* file = fopen(data, "rb"); + if (!file) { + EVP_DigestFinal(c, buffer, NULL); + return luaL_error(L, "can't open %s", data); + } + while (1) { + unsigned char chunk[4096]; + size_t bytes = fread(chunk, 1, sizeof(chunk), file); + EVP_DigestUpdate(c, chunk, bytes); + if (bytes < 4096) + break; + } + fclose(file); + } else { + EVP_DigestUpdate(c, data, len); + } + int digest_length; + EVP_DigestFinal(c, buffer, &digest_length); + EVP_MD_CTX_free(c); + char hex_buffer[EVP_MAX_MD_SIZE * 2]; + 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); + hex_buffer[digest_length*2]=0; + 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"); + 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_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 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; + 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); + return 0; +} + + +static CURL *curl; +static int lpm_certs(lua_State* L) { + const char* type = luaL_checkstring(L, 1); + const char* path = luaL_checkstring(L, 2); + if (strcmp(type, "dir") == 0) { + git_libgit2_opts(GIT_OPT_SET_SSL_CERT_LOCATIONS, NULL, path); + curl_easy_setopt(curl, CURLOPT_CAINFO, path); + } else { + git_libgit2_opts(GIT_OPT_SET_SSL_CERT_LOCATIONS, path, NULL); + curl_easy_setopt(curl, CURLOPT_CAPATH, path); + } + return 0; +} + +static int lpm_extract(lua_State* L) { + const char* src = luaL_checkstring(L, 1); + const char* dst = luaL_optstring(L, 2, "."); + + char error_buffer[1024] = {0}; + struct archive_entry *entry; + const void *buff; + int flags = 0; + int r; + size_t size; +#if ARCHIVE_VERSION_NUMBER >= 3000000 + int64_t offset; +#else + off_t offset; +#endif + struct archive *ar = archive_read_new(); + struct archive *aw = archive_write_disk_new(); + archive_write_disk_set_options(aw, flags); + archive_read_support_format_tar(ar); + archive_read_support_format_zip(ar); + archive_read_support_filter_gzip(ar); + if ((r = archive_read_open_filename(ar, src, 10240))) { + snprintf(error_buffer, sizeof(error_buffer), "error extracting archive %s: %s", src, archive_error_string(ar)); + goto cleanup; + } + for (;;) { + int r = archive_read_next_header(ar, &entry); + if (r == ARCHIVE_EOF) + break; + if (r != ARCHIVE_OK) { + snprintf(error_buffer, sizeof(error_buffer), "error extracting archive %s: %s", src, archive_error_string(ar)); + goto cleanup; + } + char path[MAX_PATH]; + strcpy(path, dst); strcat(path, "/"); + strncat(path, archive_entry_pathname(entry), sizeof(path) - 3); path[MAX_PATH-1] = 0; + archive_entry_set_pathname(entry, path); + if (archive_write_header(aw, entry) != ARCHIVE_OK) { + snprintf(error_buffer, sizeof(error_buffer), "error extracting archive %s: %s", src, archive_error_string(aw)); + goto cleanup; + } + for (;;) { + int r = archive_read_data_block(ar, &buff, &size, &offset); + if (r == ARCHIVE_EOF) + break; + if (r != ARCHIVE_OK) { + snprintf(error_buffer, sizeof(error_buffer), "error extracting archive %s: %s", src, archive_error_string(ar)); + goto cleanup; + } + if (archive_write_data_block(aw, buff, size, offset) != ARCHIVE_OK) { + snprintf(error_buffer, sizeof(error_buffer), "error extracting archive %s: %s", src, archive_error_string(aw)); + goto cleanup; + } + } + if (archive_write_finish_entry(aw) != ARCHIVE_OK) { + snprintf(error_buffer, sizeof(error_buffer), "error extracting archive %s: %s", src, archive_error_string(aw)); + goto cleanup; + } + } + cleanup: + archive_read_close(ar); + archive_read_free(ar); + archive_write_close(aw); + archive_write_free(aw); + if (error_buffer[0]) + return luaL_error(L, "error extracting archive %s: %s", src, archive_error_string(ar)); + return 0; +} + +static size_t lpm_curl_write_callback(char *ptr, size_t size, size_t nmemb, void *BL) { + luaL_Buffer* B = BL; + luaL_addlstring(B, ptr, size*nmemb); + return size*nmemb; +} + +static int lpm_get(lua_State* L) { + long response_code; + const char* url = luaL_checkstring(L, 1); + const char* path = luaL_optstring(L, 2, NULL); + // curl_easy_reset(curl); + curl_easy_setopt(curl, CURLOPT_URL, url); + curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1); + #ifdef _WIN32 + curl_easy_setopt(curl, CURLOPT_SSL_OPTIONS, CURLSSLOPT_NATIVE_CA); + #endif + if (path) { + FILE* file = fopen(path, "wb"); + if (!file) + return luaL_error(L, "error opening file %s: %s", path, strerror(errno)); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, fwrite); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, file); + CURLcode res = curl_easy_perform(curl); + if (res != CURLE_OK) { + fclose(file); + return luaL_error(L, "curl error accessing %s: %s", url, curl_easy_strerror(res)); + } + fclose(file); + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &response_code); + if (response_code != 200) + return luaL_error(L, "curl error accessing %s, non-200 response code: %d", url, response_code); + lua_pushnil(L); + lua_newtable(L); + return 2; + } else { + luaL_Buffer B; + luaL_buffinit(L, &B); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, lpm_curl_write_callback); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &B); + CURLcode res = curl_easy_perform(curl); + if (res != CURLE_OK) + return luaL_error(L, "curl error accessing %s: %s", url, curl_easy_strerror(res)); + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &response_code); + if (response_code != 200) + return luaL_error(L, "curl error accessing %s, non-200 response code: %d", url, response_code); + luaL_pushresult(&B); + lua_newtable(L); + } + return 2; +} + + +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. + { "get", lpm_get }, // HTTP(s) GET request. + { "extract", lpm_extract }, // Extracts .tar.gz, and .zip files. + { "certs", lpm_certs }, // Sets the SSL certificate chain folder/file. + { NULL, NULL } +}; + + +#ifndef LPM_VERSION + #define LPM_VERSION "unknown" +#endif + + +#ifndef LITE_ARCH_TUPLE + #if __x86_64__ || _WIN64 || __MINGW64__ + #define ARCH_PROCESSOR "x86_64" + #else + #define ARCH_PROCESSOR "x86" + #endif + #if _WIN32 + #define ARCH_PLATFORM "windows" + #elif __linux__ + #define ARCH_PLATFORM "linux" + #elif __APPLE__ + #define ARCH_PLATFORM "darwin" + #else + #error "Please define -DLITE_ARCH_TUPLE." + #endif + #define LITE_ARCH_TUPLE ARCH_PROCESSOR "-" ARCH_PLATFORM +#endif + + +extern const char lpm_lua[]; +extern unsigned int lpm_lua_len; +int main(int argc, char* argv[]) { + curl = curl_easy_init(); + if (!curl) + return -1; + 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"); + #if _WIN32 + lua_pushliteral(L, "windows"); + lua_pushliteral(L, "\\"); + #else + lua_pushliteral(L, "posix"); + lua_pushliteral(L, "/"); + #endif + lua_setglobal(L, "PATHSEP"); + lua_setglobal(L, "PLATFORM"); + lua_pushliteral(L, LITE_ARCH_TUPLE); + lua_setglobal(L, "ARCH"); + #if LPM_LIVE + if (luaL_loadfile(L, "lpm.lua") || lua_pcall(L, 0, 1, 0)) { + #else + if (luaL_loadbuffer(L, lpm_lua, lpm_lua_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(); + curl_easy_cleanup(curl); + return status; +} |