diff options
Diffstat (limited to 'lib')
-rw-r--r-- | lib/CMakeLists.txt | 57 | ||||
-rw-r--r-- | lib/archive.c | 110 | ||||
-rw-r--r-- | lib/checksum.c | 42 | ||||
-rw-r--r-- | lib/compat.c | 23 | ||||
-rw-r--r-- | lib/config.c | 227 | ||||
-rw-r--r-- | lib/config_global.c | 375 | ||||
-rw-r--r-- | lib/environment.c | 406 | ||||
-rw-r--r-- | lib/extern/url.c | 655 | ||||
-rw-r--r-- | lib/find.c | 151 | ||||
-rw-r--r-- | lib/fs.c | 504 | ||||
-rw-r--r-- | lib/install.c | 328 | ||||
-rw-r--r-- | lib/internal_cmd.c | 401 | ||||
-rw-r--r-- | lib/manifest.c | 668 | ||||
-rw-r--r-- | lib/metadata.c | 166 | ||||
-rw-r--r-- | lib/mime.c | 156 | ||||
-rw-r--r-- | lib/mirrors.c | 180 | ||||
-rw-r--r-- | lib/purge.c | 93 | ||||
-rw-r--r-- | lib/relocation.c | 440 | ||||
-rw-r--r-- | lib/resolve.c | 65 | ||||
-rw-r--r-- | lib/rpath.c | 303 | ||||
-rw-r--r-- | lib/shell.c | 116 | ||||
-rw-r--r-- | lib/shlib.c | 72 | ||||
-rw-r--r-- | lib/spm_build.c | 23 | ||||
-rw-r--r-- | lib/str.c | 699 | ||||
-rw-r--r-- | lib/strlist.c | 339 | ||||
-rw-r--r-- | lib/user_input.c | 75 | ||||
-rw-r--r-- | lib/version_spec.c | 445 |
27 files changed, 7119 insertions, 0 deletions
diff --git a/lib/CMakeLists.txt b/lib/CMakeLists.txt new file mode 100644 index 0000000..3ec0f68 --- /dev/null +++ b/lib/CMakeLists.txt @@ -0,0 +1,57 @@ +include_directories( + ${CMAKE_SOURCE_DIR}/include + ${CMAKE_BINARY_DIR}/include +) + +set(libspm_src + config.c + compat.c + resolve.c + fs.c + rpath.c + find.c + shell.c + archive.c + str.c + relocation.c + install.c + config_global.c + manifest.c + checksum.c + extern/url.c + version_spec.c + spm_build.c + mime.c + internal_cmd.c + environment.c + mirrors.c + strlist.c + shlib.c + user_input.c + metadata.c + purge.c +) + +add_library(libspm_obj OBJECT ${libspm_src}) +set_property(TARGET libspm_obj PROPERTY POSITION_INDEPENDENT_CODE 1) +add_library(libspm SHARED $<TARGET_OBJECTS:libspm_obj>) +add_library(libspm_static STATIC $<TARGET_OBJECTS:libspm_obj>) + + +target_link_libraries(libspm crypto ssl curl) +if (LINUX) + target_link_libraries(libspm rt) +endif() + +if(MSVC) + target_compile_options(libspm PRIVATE /W4 /WX) +else() + target_compile_options(libspm PRIVATE -Wall -Wextra -fstack-protector) +endif() + +set_target_properties(libspm PROPERTIES OUTPUT_NAME "spm") +install( + TARGETS libspm + LIBRARY DESTINATION ${CMAKE_INSTALL_PREFIX}/lib + ARCHIVE DESTINATION ${CMAKE_INSTALL_PREFIX}/lib +) diff --git a/lib/archive.c b/lib/archive.c new file mode 100644 index 0000000..d964469 --- /dev/null +++ b/lib/archive.c @@ -0,0 +1,110 @@ +/** + * @file archive.c + */ +#include "spm.h" + +/** + * Extract a single file from a tar archive into a directory + * + * @param archive path to tar archive + * @param filename known path inside the archive to extract + * @param destination where to extract file to (must exist) + * @return + */ +int tar_extract_file(const char *_archive, const char* _filename, const char *_destination) { + Process *proc = NULL; + int status; + char cmd[PATH_MAX]; + char *archive = strdup(_archive); + if (!archive) { + fprintf(SYSERROR); + return -1; + } + char *filename = strdup(_filename); + if (!filename) { + fprintf(SYSERROR); + return -1; + } + char *destination = strdup(_destination); + if (!destination) { + fprintf(SYSERROR); + return -1; + } + + strchrdel(archive, SHELL_INVALID); + strchrdel(destination, SHELL_INVALID); + strchrdel(filename, SHELL_INVALID); + + sprintf(cmd, "tar xf \"%s\" -C \"%s\" \"%s\" 2>&1", archive, destination, filename); + if (exists(archive) != 0) { + fprintf(stderr, "unable to find archive: %s\n", archive); + fprintf(SYSERROR); + return -1; + } + + shell(&proc, SHELL_OUTPUT, cmd); + if (!proc) { + fprintf(SYSERROR); + return -1; + } + + status = proc->returncode; + if (status != 0) { + fprintf(stderr, "%s\n", proc->output); + } + + shell_free(proc); + free(archive); + free(filename); + free(destination); + return status; +} + +/** + * + * @param _archive + * @param _destination + * @return + */ +int tar_extract_archive(const char *_archive, const char *_destination) { + Process *proc = NULL; + int status; + char cmd[PATH_MAX]; + + if (exists(_archive) != 0) { + //fprintf(SYSERROR); + return -1; + } + + char *archive = strdup(_archive); + if (!archive) { + fprintf(SYSERROR); + return -1; + } + char *destination = strdup(_destination); + if (!destination) { + fprintf(SYSERROR); + return -1; + } + + // sanitize archive + strchrdel(archive, SHELL_INVALID); + // sanitize destination + strchrdel(destination, SHELL_INVALID); + + sprintf(cmd, "tar xf %s -C %s 2>&1", archive, destination); + shell(&proc, SHELL_OUTPUT, cmd); + if (!proc) { + fprintf(SYSERROR); + free(archive); + free(destination); + return -1; + } + + status = proc->returncode; + shell_free(proc); + free(archive); + free(destination); + return status; +} + diff --git a/lib/checksum.c b/lib/checksum.c new file mode 100644 index 0000000..249d6cb --- /dev/null +++ b/lib/checksum.c @@ -0,0 +1,42 @@ +/** + * @file checksum.c + */ +#include "spm.h" +#include <openssl/sha.h> + +/** + * + * @param filename + * @return + */ +char *sha256sum(const char *filename) { + size_t bytes = 0; + unsigned char digest[SHA256_DIGEST_LENGTH]; + char buf[BUFSIZ]; + SHA256_CTX context; + SHA256_Init(&context); + FILE *fp = fopen(filename, "r"); + if (!fp) { + perror(filename); + return NULL; + } + char *result = calloc(SHA256_DIGEST_STRING_LENGTH, sizeof(char)); + if (!result) { + fclose(fp); + perror("SHA256 result"); + return NULL; + } + + while ((bytes = fread(buf, sizeof(char), BUFSIZ, fp)) != 0) { + SHA256_Update(&context, buf, bytes); + } + fclose(fp); + + SHA256_Final(digest, &context); + char *rtmp = result; + for (int i = 0; i < SHA256_DIGEST_LENGTH; i++) { + snprintf(&rtmp[i * 2], 3, "%02x", digest[i]); + } + + return result; +} diff --git a/lib/compat.c b/lib/compat.c new file mode 100644 index 0000000..5fd2cc4 --- /dev/null +++ b/lib/compat.c @@ -0,0 +1,23 @@ +#include "config.h" + +#ifndef HAVE_STRSEP +#include <string.h> +// credit: Dan Cross via https://unixpapa.com/incnote/string.html +char *strsep(char **sp, char *sep) +{ + char *p, *s; + if (sp == NULL || *sp == NULL || **sp == '\0') return(NULL); + s = *sp; + p = s + strcspn(s, sep); + if (*p != '\0') *p++ = '\0'; + *sp = p; + return(s); +} +#endif + +#ifndef HAVE_REALLOCARRAY +#include <stdlib.h> +void *reallocarray (void *__ptr, size_t __nmemb, size_t __size) { + return realloc(__ptr, __nmemb * __size); +} +#endif diff --git a/lib/config.c b/lib/config.c new file mode 100644 index 0000000..88c96fd --- /dev/null +++ b/lib/config.c @@ -0,0 +1,227 @@ +/** + * @file config.c + */ +#include "spm.h" + +/** + * Parse a basic configuration file + * + * NOTE: All values are stored as strings. You need to convert non-string values yourself. + * + * Example: + * ~~~{.c} + * ConfigItem **items = config_read("example.cfg"); + * if (!items) { + * // handle error + * } + * config_free(items); + * ~~~ + * + * + * @param filename + * @return success=`ConfigItem` array, failure=NULL + */ +ConfigItem **config_read(const char *filename) { + const char sep = '='; + size_t record = 0; + char *line = NULL; + FILE *fp = NULL; + + if (SPM_GLOBAL.verbose) { + printf("Reading configuration file: %s\n", filename); + } + + fp = fopen(filename, "r"); + if (!fp) { + // errno will be set, so die, and let the caller handle it + return NULL; + } + + ConfigItem **config = (ConfigItem **) calloc(record + 1, sizeof(ConfigItem *)); + if (!config) { + perror("ConfigItem"); + fprintf(SYSERROR); + fclose(fp); + return NULL; + } + + line = (char *)calloc(CONFIG_BUFFER_SIZE, sizeof(char)); + if (!line) { + perror("config line buffer"); + fprintf(SYSERROR); + return NULL; + } + + while (fgets(line, CONFIG_BUFFER_SIZE - 1, fp) != NULL) { + char *lptr = line; + // Remove trailing space and newlines + lptr = strip(lptr); + + // Remove leading space and newlines + lptr = lstrip(lptr); + + // Skip empty lines + if (isempty(lptr)) { + continue; + } + + // Skip comment-only lines + if (*lptr == '#' || *lptr == ';') { + continue; + } + + // Get a pointer to the key pair separator + char *sep_pos = strchr(lptr, sep); + if (!sep_pos) { + printf("invalid entry on line %zu: missing '%c': '%s'\n", record, sep, lptr); + continue; + } + + // These values are approximations. The real length(s) are recorded using strlen below. + // At most we'll lose a few heap bytes to whitespace, but it's better than allocating PATH_MAX or BUFSIZ + // for a measly ten byte string. + size_t key_length = strcspn(lptr, &sep); + size_t value_length = strlen(sep_pos); + + // Allocate a ConfigItem record + config[record] = (ConfigItem *)calloc(1, sizeof(ConfigItem)); + config[record]->key = (char *)calloc(key_length + 1, sizeof(char)); + config[record]->value = (char *)calloc(value_length + 1, sizeof(char)); + + if (!config[record] || !config[record]->key || !config[record]->value) { + perror("ConfigItem record"); + fprintf(SYSERROR); + return NULL; + } + + // Shortcut our array at this point. Things get pretty ugly otherwise. + char *key = config[record]->key; + char *value = config[record]->value; + + // Copy the array pointers (used to populate config->key/value_length + char *key_orig = key; + char *value_orig = value; + + // Populate the key and remove any trailing space + while (lptr != sep_pos) { + *key++ = *lptr++; + } + key = strip(key_orig); + + // We're at the separator now, so skip over it + lptr++; + // and remove any leading space + lptr = lstrip(lptr); + + // Determine whether the string is surrounded by quotes, if so, get rid of them + if (isquoted(lptr)) { + // Move pointer beyond quote + lptr = strpbrk(lptr, "'\"") + 1; + // Terminate on closing quote + char *tmp = strpbrk(lptr, "'\""); + *tmp = '\0'; + } + + // Populate the value, and ignore any inline comments + while (*lptr) { + if (*lptr == '#' || *lptr == ';') { + // strip trailing whitespace where the comment is and stop processing + value = strip(value); + break; + } + *value++ = *lptr++; + } + + // Populate length data + config[record]->key_length = strlen(key_orig); + config[record]->value_length = strlen(value_orig); + + // Destroy contents of line buffer + memset(line, '\0', CONFIG_BUFFER_SIZE); + + if (SPM_GLOBAL.verbose) { + printf("CONFIG RECORD=%zu, PTR='%p', KEY='%s', VALUE='%s'\n", + record, config[record], config[record]->key, config[record]->value); + } + + // increment record count + record++; + + // Expand config by another record + config = (ConfigItem **)reallocarray(config, record + 1, sizeof(ConfigItem *)); + if (!config) { + perror("ConfigItem array"); + fprintf(SYSERROR); + free(line); + return NULL; + } + + config[record] = NULL; + } + free(line); + return config; +} + +/** + * Free memory allocated by `config_read` + * @param item `ConfigItem` array + */ +void config_free(ConfigItem **item) { + for (size_t i = 0; item[i] != NULL; i++) { + free(item[i]->key); + free(item[i]->value); + free(item[i]); + } + free(item); +} + +/** + * If the `ConfigItem` array contains `key`, return a pointer to that record + * + * Example: + * ~~~{.c} + * char *nptr = NULL; + * ConfigItem *item = config_get(items, "a_number"); + * if (!item) { + * // handle error + * } + * int the_number = strtol(item->value, &nptr, 10); + * printf("%s = %d\n", item->key, the_number); + * ~~~ + * + * @param item pointer to array of config records + * @param key search for key in config records + * @return success=pointer to record, failure=NULL + */ +ConfigItem *config_get(ConfigItem **item, const char *key) { + if (!item) { + return NULL; + } + for (size_t i = 0; item[i] != NULL; i++) { + if (!strcmp(item[i]->key, key)) { + return item[i]; + } + } + return NULL; +} + +void config_test(void) { + ConfigItem **config = config_read("program.conf"); + printf("Data Parsed:\n"); + for (int i = 0; config[i] != NULL; i++) { + printf("key: '%s', value: '%s'\n", config[i]->key, config[i]->value); + } + + printf("Testing config_get():\n"); + ConfigItem *cptr = NULL; + if ((cptr = config_get(config, "integer_value"))) { + printf("%s = %d\n", cptr->key, atoi(cptr->value)); + } + if ((cptr = config_get(config, "float_value"))) { + printf("%s = %.3f\n", cptr->key, atof(cptr->value)); + } + if ((cptr = config_get(config, "string_value"))) { + printf("%s = %s\n", cptr->key, cptr->value); + } + config_free(config); +} diff --git a/lib/config_global.c b/lib/config_global.c new file mode 100644 index 0000000..16d97cf --- /dev/null +++ b/lib/config_global.c @@ -0,0 +1,375 @@ +/** + * @file config_global.c + */ +#include "spm.h" + +/** + * Get path to user's local configuration directory + * (The path will be created if it doesn't exist) + * @return + */ +char *get_user_conf_dir(void) { + char *result = NULL; + wordexp_t wexp; + wordexp("~/.spm", &wexp, 0); + if (wexp.we_wordc != 0) { + result = (char *)calloc(strlen(wexp.we_wordv[0]) + 1, sizeof(char)); + if (!result) { + wordfree(&wexp); + return NULL; + } + strncpy(result, wexp.we_wordv[0], strlen(wexp.we_wordv[0])); + if (access(result, F_OK) != 0) { + mkdirs(result, 0755); + } + } + wordfree(&wexp); + return result; +} + +/** + * Get path to user's local configuration file + * @return + */ +char *get_user_config_file(void) { + const char *filename = "spm.conf"; + char template[PATH_MAX]; + char *ucd = get_user_conf_dir(); + if (!ucd) { + return NULL; + } + // Initialize temporary path + template[0] = '\0'; + + sprintf(template, "%s%c%s", ucd, DIRSEP, filename); + if (access(template, F_OK) != 0) { + // No configuration exists, so fail + return NULL; + } + free(ucd); + // Allocate and return path to configuration file + return strdup(template); +} + +/** + * Determine location of temporary storage location + * @return + */ +char *get_user_tmp_dir(void) { + char *template = NULL; + char *ucd = get_user_conf_dir(); + template = join_ex(DIRSEPS, ucd, "tmp", NULL); + + if (access(template, F_OK) != 0) { + if (mkdirs(template, 0755) != 0) { + return NULL; + } + } + + free(ucd); + return template; +} + +/** + * Determine location of package directory + * @return + */ +char *get_user_package_dir(void) { + char *template = NULL; + char *ucd = get_user_conf_dir(); + + template = join_ex(DIRSEPS, ucd, "pkgs", SPM_GLOBAL.repo_target, NULL); + + if (access(template, F_OK) != 0) { + if (mkdirs(template, 0755) != 0) { + return NULL; + } + } + + free(ucd); + return template; +} + +/** + * Determine location of the package manifest + * @return + */ +char *get_package_manifest(void) { + Manifest *manifest = NULL; + char *template = NULL; + char *ucd = get_user_conf_dir(); + + //free(ucd); + //return strdup(template); + + template = join_ex(DIRSEPS, SPM_GLOBAL.package_dir, SPM_MANIFEST_FILENAME, NULL); + if (access(template, F_OK) != 0) { + fprintf(stderr, "Package manifest not found: %s\n", template); + manifest = manifest_from(PKG_DIR); + if (manifest == NULL) { + perror("manifest generator"); + fprintf(SYSERROR); + return NULL; + } + manifest_write(manifest, PKG_DIR); + manifest_free(manifest); + } + + free(ucd); + return template; +} + + +/** + * Check whether SPM has access to external programs it needs + */ +void check_runtime_environment(void) { + int bad_rt = 0; + char *required[] = { + "file", + "patchelf", + "objdump", + "rsync", + "tar", + "bash", + "reloc", + NULL, + }; + + if (getenv("SHELL") == NULL) { + fprintf(stderr, "Required environment variable 'SHELL' is not defined\n"); + bad_rt = 1; + } + + for (int i = 0; required[i] != NULL; i++) { + char *result = find_executable(required[i]); + if (!result) { + fprintf(stderr, "Required program '%s' is not installed\n", required[i]); + bad_rt = 1; + } + free(result); + } + if (bad_rt) { + exit(1); + } +} + +/** + * + * @param basepath + * @return + */ +SPM_Hierarchy *spm_hierarchy_init(char *basepath) { + SPM_Hierarchy *fs = calloc(1, sizeof(SPM_Hierarchy)); + fs->rootdir = strdup(basepath); + fs->bindir = join((char *[]) {fs->rootdir, "bin", NULL}, DIRSEPS); + fs->includedir = join((char *[]) {fs->rootdir, "include", NULL}, DIRSEPS); + fs->libdir = join((char *[]) {fs->rootdir, "lib", NULL}, DIRSEPS); + fs->datadir = join((char *[]) {fs->rootdir, "share", NULL}, DIRSEPS); + fs->localstatedir = join((char *[]) {fs->rootdir, "var", NULL}, DIRSEPS); + fs->sysconfdir = join((char *[]) {fs->rootdir, "etc", NULL}, DIRSEPS); + fs->mandir = join((char *[]) {fs->datadir, "man", NULL}, DIRSEPS); + fs->tmpdir = join((char *[]) {fs->rootdir, "tmp", NULL}, DIRSEPS); + fs->dbdir = join((char *[]) {fs->localstatedir, "db", NULL}, DIRSEPS); + fs->dbrecdir = join((char *[]) {fs->dbdir, "records", NULL}, DIRSEPS); + + return fs; +} + +/** + * + * @param fs + */ +void spm_hierarchy_free(SPM_Hierarchy *fs) { + free(fs->rootdir); + free(fs->bindir); + free(fs->includedir); + free(fs->libdir); + free(fs->datadir); + free(fs->localstatedir); + free(fs->sysconfdir); + free(fs->mandir); + free(fs->tmpdir); + free(fs->dbdir); + free(fs->dbrecdir); + free(fs); +} + +/** + * Populate global configuration structure + */ +void init_config_global(void) { + SPM_GLOBAL.user_config_basedir = NULL; + SPM_GLOBAL.user_config_file = NULL; + SPM_GLOBAL.package_dir = NULL; + SPM_GLOBAL.tmp_dir = NULL; + SPM_GLOBAL.package_manifest = NULL; + SPM_GLOBAL.config = NULL; + SPM_GLOBAL.verbose = 0; + SPM_GLOBAL.repo_target = NULL; + SPM_GLOBAL.mirror_list = NULL; + SPM_GLOBAL.prompt_user = 1; + + if (uname(&SPM_GLOBAL.sysinfo) != 0) { + fprintf(SYSERROR); + exit(errno); + } + + // Initialize filesystem paths structure + SPM_GLOBAL.fs.bindir = calloc(strlen(SPM_PROGRAM_BIN) + 1, sizeof(char)); + SPM_GLOBAL.fs.includedir = calloc(strlen(SPM_PROGRAM_INCLUDE) + 1, sizeof(char)); + SPM_GLOBAL.fs.libdir = calloc(strlen(SPM_PROGRAM_LIB) + 1, sizeof(char)); + SPM_GLOBAL.fs.datadir = calloc(strlen(SPM_PROGRAM_DATA) + 1, sizeof(char)); + + if (!SPM_GLOBAL.fs.bindir || !SPM_GLOBAL.fs.includedir + || !SPM_GLOBAL.fs.libdir) { + perror("Unable to allocate memory for global filesystem paths"); + fprintf(SYSERROR); + exit(errno); + } + + strcpy(SPM_GLOBAL.fs.bindir, SPM_PROGRAM_BIN); + strcpy(SPM_GLOBAL.fs.includedir, SPM_PROGRAM_INCLUDE); + strcpy(SPM_GLOBAL.fs.libdir, SPM_PROGRAM_LIB); + strcpy(SPM_GLOBAL.fs.datadir, SPM_PROGRAM_DATA); + SPM_GLOBAL.fs.mandir = join((char *[]) {SPM_PROGRAM_DATA, "man", NULL}, DIRSEPS); + + SPM_GLOBAL.user_config_basedir = get_user_conf_dir(); + SPM_GLOBAL.user_config_file = get_user_config_file(); + if (SPM_GLOBAL.user_config_file) { + SPM_GLOBAL.config = config_read(SPM_GLOBAL.user_config_file); + } + + ConfigItem *item = NULL; + + // Initialize repository target (i.e. repository path suffix) + SPM_GLOBAL.repo_target = join((char *[]) {SPM_GLOBAL.sysinfo.sysname, SPM_GLOBAL.sysinfo.machine, NULL}, DIRSEPS); + item = config_get(SPM_GLOBAL.config, "repo_target"); + if (item) { + free(SPM_GLOBAL.repo_target); + SPM_GLOBAL.repo_target = normpath(item->value); + } + + // Initialize mirror list filename + SPM_GLOBAL.mirror_config = join((char *[]) {SPM_GLOBAL.user_config_basedir, SPM_MIRROR_FILENAME, NULL}, DIRSEPS); + item = config_get(SPM_GLOBAL.config, "mirror_config"); + if (item) { + free(SPM_GLOBAL.mirror_config); + SPM_GLOBAL.mirror_config = normpath(item->value); + } + + if (SPM_GLOBAL.mirror_config != NULL) { + SPM_GLOBAL.mirror_list = mirror_list(SPM_GLOBAL.mirror_config); + } + + // Initialize temp directory + item = config_get(SPM_GLOBAL.config, "tmp_dir"); + if (item) { + SPM_GLOBAL.tmp_dir = strdup(item->value); + if (access(SPM_GLOBAL.tmp_dir, F_OK) != 0) { + if (mkdirs(SPM_GLOBAL.tmp_dir, 0755) != 0) { + fprintf(stderr, "Unable to create global temporary directory: %s\n", SPM_GLOBAL.tmp_dir); + fprintf(SYSERROR); + exit(1); + } + } + } + else { + SPM_GLOBAL.tmp_dir = get_user_tmp_dir(); + } + + // Initialize package directory + item = config_get(SPM_GLOBAL.config, "package_dir"); + if (item) { + SPM_GLOBAL.package_dir = calloc(PATH_MAX, sizeof(char)); //strdup(item->value); + strncpy(SPM_GLOBAL.package_dir, item->value, PATH_MAX - 1); + strcat(SPM_GLOBAL.package_dir, DIRSEPS); + strcat(SPM_GLOBAL.package_dir, SPM_GLOBAL.repo_target); + + if (access(SPM_GLOBAL.package_dir, F_OK) != 0) { + if (mkdirs(SPM_GLOBAL.package_dir, 0755) != 0) { + fprintf(stderr, "Unable to create global package directory: %s\n", SPM_GLOBAL.package_dir); + fprintf(SYSERROR); + exit(1); + } + } + } + else { + SPM_GLOBAL.package_dir = get_user_package_dir(); + } + + // Initialize package manifest + item = config_get(SPM_GLOBAL.config, "package_manifest"); + if (item) { + SPM_GLOBAL.package_manifest = strdup(item->value); + if (access(SPM_GLOBAL.package_manifest, F_OK) != 0) { + fprintf(stderr, "Package manifest not found: %s\n", SPM_GLOBAL.package_manifest); + Manifest *manifest = manifest_from(PKG_DIR); + manifest_write(manifest, SPM_GLOBAL.package_manifest); + manifest_free(manifest); + } + } + else { + SPM_GLOBAL.package_manifest = get_package_manifest(); + } +} + +/** + * Free memory allocated for global configuration + */ +void free_global_config(void) { + if (SPM_GLOBAL.package_dir) { + free(SPM_GLOBAL.package_dir); + } + if (SPM_GLOBAL.tmp_dir) { + free(SPM_GLOBAL.tmp_dir); + } + if (SPM_GLOBAL.package_manifest) { + free(SPM_GLOBAL.package_manifest); + } + if (SPM_GLOBAL.user_config_basedir) { + free(SPM_GLOBAL.user_config_basedir); + } + if (SPM_GLOBAL.user_config_file) { + free(SPM_GLOBAL.user_config_file); + } + if (SPM_GLOBAL.repo_target) { + free(SPM_GLOBAL.repo_target); + } + if (SPM_GLOBAL.mirror_config) { + free(SPM_GLOBAL.mirror_config); + } + if (SPM_GLOBAL.mirror_list) { + mirror_list_free(SPM_GLOBAL.mirror_list); + } + + free(SPM_GLOBAL.fs.bindir); + free(SPM_GLOBAL.fs.includedir); + free(SPM_GLOBAL.fs.libdir); + free(SPM_GLOBAL.fs.datadir); + free(SPM_GLOBAL.fs.mandir); + if (SPM_GLOBAL.config) { + config_free(SPM_GLOBAL.config); + } +} + +/** + * Display global configuration data + */ +void show_global_config(void) { + printf("#---------------------------\n"); + printf("#---- SPM CONFIGURATION ----\n"); + printf("#---------------------------\n"); + printf("# base dir: %s\n", SPM_GLOBAL.user_config_basedir ? SPM_GLOBAL.user_config_basedir : "none (check write permission on home directory)"); + printf("# config file: %s\n", SPM_GLOBAL.user_config_file ? SPM_GLOBAL.user_config_file : "none"); + if (SPM_GLOBAL.user_config_file) { + printf("# config file contents:\n"); + for (int i = 0; SPM_GLOBAL.config[i] != NULL; i++) { + printf("# -> %s: %s\n", SPM_GLOBAL.config[i]->key, SPM_GLOBAL.config[i]->value); + } + } + printf("# package storage: %s\n", SPM_GLOBAL.package_dir); + printf("# temp storage: %s\n", SPM_GLOBAL.tmp_dir); + printf("# package manifest: %s\n", SPM_GLOBAL.package_manifest); + printf("\n"); +} diff --git a/lib/environment.c b/lib/environment.c new file mode 100644 index 0000000..d9bfe51 --- /dev/null +++ b/lib/environment.c @@ -0,0 +1,406 @@ +/** + * @file environment.c + */ +#include "spm.h" + +/** + * Print a shell-specific listing of environment variables to `stdout` + * + * Example: + * ~~~{.c} + * int main(int argc, char *argv[], char *arge[]) { + * RuntimeEnv *rt = runtime_copy(arge); + * runtime_export(rt, NULL); + * runtime_free(rt); + * return 0; + * } + * ~~~ + * + * Usage: + * ~~~{.sh} + * $ gcc program.c + * $ ./a.out + * PATH="/thing/stuff/bin:/example/please/bin" + * SHELL="/your/shell" + * CC="/your/compiler" + * ...=... + * + * # You can also use this to modify the shell environment + * # (use `runtime_set` to manipulate the output) + * $ source $(./a.out) + * ~~~ + * + * Example of exporting specific keys from the environment: + * + * ~~~{.c} + * int main(int argc, char *argv[], char *arge[]) { + * RuntimeEnv *rt = runtime_copy(arge); + * + * // inline declaration + * runtime_export(rt, (char *[]) {"PATH", "LS_COLORS", NULL}); + * + * // standard declaration + * char *keys_to_export[] = { + * "PATH", "LS_COLORS", NULL + * } + * runtime_export(rt, keys_to_export); + * + * runtime_free(rt); + * return 0; + * } + * ~~~ + * + * @param env `RuntimeEnv` structure + * @param keys Array of keys to export. A value of `NULL` exports all environment keys + */ +void runtime_export(RuntimeEnv *env, char **keys) { + char *borne[] = { + "bash", + "dash", + "zsh", + NULL, + }; + char *unborne[] = { + "csh" + "tcsh", + NULL, + }; + + char output[BUFSIZ]; + char export_command[7]; // export=6 and setenv=6... convenient + char *_sh = getenv("SHELL"); + char *sh = basename(_sh); + if (sh == NULL) { + fprintf(stderr, "echo SHELL environment variable is not defined"); + exit(1); + } + + for (size_t i = 0; borne[i] != NULL; i++) { + if (strcmp(sh, borne[i]) == 0) { + strcpy(export_command, "export"); + break; + } + } + for (size_t i = 0; unborne[i] != NULL; i++) { + if (strcmp(sh, unborne[i]) == 0) { + strcpy(export_command, "setenv"); + break; + } + } + + for (size_t i = 0; i < strlist_count(env); i++) { + char **pair = split(strlist_item(env, i), "="); + char *key = pair[0]; + char *value = NULL; + + // We split a potentially large string by "=" so: + // Recombine elements pair[1..N] into a single string by "=" + if (pair[1] != NULL) { + value = join(&pair[1], "="); + } + + if (keys != NULL) { + for (size_t j = 0; keys[j] != NULL; j++) { + if (strcmp(keys[j], key) == 0) { + sprintf(output, "%s %s=\"%s\"", export_command, key, value ? value : ""); + puts(output); + } + } + } + else { + sprintf(output, "%s %s=\"%s\"", export_command, key, value ? value : ""); + puts(output); + } + free(value); + split_free(pair); + } +} + +/** + * Populate a `RuntimeEnv` structure + * + * Example: + * + * ~~~{.c} + * int main(int argc, char *argv[], char *arge[]) { + * RuntimeEnv *rt = NULL; + * // Example 1: Copy the shell environment + * rt = runtime_copy(arge); + * // Example 2: Create your own environment + * rt = runtime_copy((char *[]) {"SHELL=/bin/bash", "PATH=/opt/secure:/bin:/usr/bin"}) + * + * runtime_free(rt); + * return 0; + * } + * ~~~ + * + * @param env Array of strings in `var=value` format + * @return `RuntimeEnv` structure + */ +RuntimeEnv *runtime_copy(char **env) { + RuntimeEnv *rt = NULL; + size_t env_count; + for (env_count = 0; env[env_count] != NULL; env_count++); + + rt = strlist_init(); + for (size_t i = 0; i < env_count; i++) { + strlist_append(rt, env[i]); + } + return rt; +} + +/** + * Determine whether or not a key exists in the runtime environment + * + * Example: + * + * ~~~{.c} + * int main(int argc, char *argv[], char *arge[]) { + * RuntimeEnv *rt = runtime_copy(arge); + * if (runtime_contains(rt, "PATH") { + * // $PATH is present + * } + * else { + * // $PATH is NOT present + * } + * + * runtime_free(rt); + * return 0; + * } + * ~~~ + * + * @param env `RuntimeEnv` structure + * @param key Environment variable string + * @return -1=no, positive_value=yes + */ +ssize_t runtime_contains(RuntimeEnv *env, const char *key) { + ssize_t result = -1; + for (size_t i = 0; i < strlist_count(env); i++) { + char **pair = split(strlist_item(env, i), "="); + if (pair == NULL) { + break; + } + if (strcmp(pair[0], key) == 0) { + result = i; + split_free(pair); + break; + } + split_free(pair); + } + return result; +} + +/** + * Retrieve the value of a runtime environment variable + * + * Example: + * + * ~~~{.c} + * int main(int argc, char *argv[], char *arge[]) { + * RuntimeEnv *rt = runtime_copy(arge); + * char *path = runtime_get("PATH"); + * if (path == NULL) { + * // handle error + * } + * + * runtime_free(rt); + * return 0; + * } + * ~~~ + * + * @param env `RuntimeEnv` structure + * @param key Environment variable string + * @return success=string, failure=`NULL` + */ +char *runtime_get(RuntimeEnv *env, const char *key) { + char *result = NULL; + ssize_t key_offset = runtime_contains(env, key); + if (key_offset != -1) { + char **pair = split(strlist_item(env, key_offset), "="); + result = join(&pair[1], "="); + split_free(pair); + } + return result; +} + +/** + * Parse an input string and expand any environment variable(s) found + * + * Example: + * + * ~~~{.c} + * int main(int argc, char *argv[], char *arge[]) { + * RuntimeEnv *rt = runtime_copy(arge); + * char *secure_path = runtime_expand_var(rt, "/opt/secure:$PATH:/aux/bin"); + * if (secure_path == NULL) { + * // handle error + * } + * // secure_path = "/opt/secure:/your/original/path/here:/aux/bin"); + * + * runtime_free(rt); + * return 0; + * } + * ~~~ + * + * @param env `RuntimeEnv` structure + * @param input String to parse + * @return success=expanded string, failure=`NULL` + */ +char *runtime_expand_var(RuntimeEnv *env, const char *input) { + const char delim = '$'; + const char *delim_literal = "$$"; + const char *escape = "\\"; + char *expanded = calloc(BUFSIZ, sizeof(char)); + if (expanded == NULL) { + perror("could not allocate runtime_expand_var buffer"); + fprintf(SYSERROR); + return NULL; + } + + // If there's no environment variables to process return a copy of the input string + if (strchr(input, delim) == NULL) { + return strdup(input); + } + + // Parse the input string + size_t i; + for (i = 0; i < strlen(input); i++) { + char var[MAXNAMLEN]; // environment variable name + memset(var, '\0', MAXNAMLEN); // zero out name + + // Handle literal statement "$$var" + // Value becomes "\$var" + if (strncmp(&input[i], delim_literal, strlen(delim_literal)) == 0) { + strncat(expanded, escape, strlen(escape)); + strncat(expanded, &delim, 1); + i += strlen(delim_literal); + // Ignore opening brace + if (input[i] == '{') { + i++; + } + } + + // Handle variable when encountering a single $ + // Value expands from "$var" to "environment value of var" + if (input[i] == delim) { + // Ignore opening brace + if (input[i+1] == '{') { + i++; + } + char *tmp = NULL; + i++; + + // Construct environment variable name from input + // "$ var" == no + // "$-*)!@ == no + // "$var" == yes + for (size_t c = 0; isalnum(input[i]) || input[i] == '_'; c++, i++) { + // Ignore closing brace + if (input[i] == '}') { + i++; + } + var[c] = input[i]; + } + + tmp = runtime_get(env, var); + if (tmp == NULL) { + // This mimics shell behavior in general. + // Prevent appending whitespace when an environment variable does not exist + if (i > 0) { + i--; + } + continue; + } + // Append expanded environment variable to output + strncat(expanded, tmp, strlen(tmp)); + free(tmp); + } + + // Nothing to do so append input to output + if (input[i] == '}') { + // Unless we ended on a closing brace + continue; + } + strncat(expanded, &input[i], 1); + } + + return expanded; +} + +/** + * Set a runtime environment variable. + * + * + * Note: `_value` is passed through `runtime_expand_var` to provide shell expansion + * + * + * Example: + * + * ~~~{.c} + * int main(int argc, char *argv[], char *arge[]) { + * RuntimeEnv *rt = runtime_copy(arge); + * + * runtime_set(rt, "new_var", "1"); + * char *new_var = runtime_get("new_var"); + * // new_var = 1; + * + * char *path = runtime_get("PATH"); + * // path = /your/path:/here + * + * runtime_set(rt, "PATH", "/opt/secure:$PATH"); + * char *secure_path = runtime_get("PATH"); + * // secure_path = /opt/secure:/your/path:/here + * // NOTE: path and secure_path are COPIES, unlike `getenv()` and `setenv()` that reuse their pointers in `environ` + * + * runtime_free(rt); + * return 0; + * } + * ~~~ + * + * + * @param env `RuntimeEnv` structure + * @param _key Environment variable to set + * @param _value New environment variable value + */ +void runtime_set(RuntimeEnv *env, const char *_key, const char *_value) { + if (_key == NULL) { + return; + } + char *key = strdup(_key); + ssize_t key_offset = runtime_contains(env, key); + char *value = runtime_expand_var(env, _value); + char *now = join((char *[]) {key, value, NULL}, "="); + + if (key_offset < 0) { + strlist_append(env, now); + } + else { + strlist_set(env, key_offset, now); + } + free(now); + free(key); + free(value); +} + +/** + * Update the global `environ` array with data from `RuntimeEnv` + * @param env `RuntimeEnv` structure + */ +void runtime_apply(RuntimeEnv *env) { + for (size_t i = 0; i < strlist_count(env); i++) { + char **pair = split(strlist_item(env, i), "="); + setenv(pair[0], pair[1], 1); + split_free(pair); + } +} + +/** + * Free `RuntimeEnv` allocated by `runtime_copy` + * @param env `RuntimeEnv` structure + */ +void runtime_free(RuntimeEnv *env) { + if (env == NULL) { + return; + } + strlist_free(env); +} diff --git a/lib/extern/url.c b/lib/extern/url.c new file mode 100644 index 0000000..fe54db2 --- /dev/null +++ b/lib/extern/url.c @@ -0,0 +1,655 @@ +/***************************************************************************** + * + * This example source code introduces a c library buffered I/O interface to + * URL reads it supports fopen(), fread(), fgets(), feof(), fclose(), + * rewind(). Supported functions have identical prototypes to their normal c + * lib namesakes and are preceaded by url_ . + * + * Using this code you can replace your program's fopen() with url_fopen() + * and fread() with url_fread() and it become possible to read remote streams + * instead of (only) local files. Local files (ie those that can be directly + * fopened) will drop back to using the underlying clib implementations + * + * See the main() function at the bottom that shows an app that retrieves from + * a specified url using fgets() and fread() and saves as two output files. + * + * Copyright (c) 2003 - 2019 Simtec Electronics + * + * Re-implemented by Vincent Sanders <vince@kyllikki.org> with extensive + * reference to original curl example code + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. The name of the author may not be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * This example requires libcurl 7.9.7 or later. + */ +/* <DESC> + * implements an fopen() abstraction allowing reading from URLs + * </DESC> + */ + +#include <url.h> +#include "url.h" + +/* we use a global one for convenience */ +static CURLM *multi_handle; + +/* curl calls this routine to get more data */ +static size_t write_callback(char *buffer, + size_t size, + size_t nitems, + void *userp) { + char *newbuff; + size_t rembuff; + + URL_FILE *url = (URL_FILE *) userp; + size *= nitems; + + rembuff = url->buffer_len - url->buffer_pos; /* remaining space in buffer */ + + if (size > rembuff) { + /* not enough space in buffer */ + newbuff = realloc(url->buffer, url->buffer_len + (size - rembuff)); + if (newbuff == NULL) { + fprintf(stderr, "callback buffer grow failed\n"); + size = rembuff; + } else { + /* realloc succeeded increase buffer size*/ + url->buffer_len += size - rembuff; + url->buffer = newbuff; + } + } + + memcpy(&url->buffer[url->buffer_pos], buffer, size); + url->buffer_pos += size; + + return size; +} + +/* use to attempt to fill the read buffer up to requested number of bytes */ +static int fill_buffer(URL_FILE *file, size_t want) { + fd_set fdread; + fd_set fdwrite; + fd_set fdexcep; + struct timeval timeout; + int rc; + CURLMcode mc; /* curl_multi_fdset() return code */ + + /* only attempt to fill buffer if transactions still running and buffer + * doesn't exceed required size already + */ + if ((!file->still_running) || (file->buffer_pos > want)) + return 0; + + /* attempt to fill buffer */ + do { + int maxfd = -1; + long curl_timeo = -1; + + FD_ZERO(&fdread); + FD_ZERO(&fdwrite); + FD_ZERO(&fdexcep); + + /* set a suitable timeout to fail on */ + timeout.tv_sec = 60; /* 1 minute */ + timeout.tv_usec = 0; + + curl_multi_timeout(multi_handle, &curl_timeo); + if (curl_timeo >= 0) { + timeout.tv_sec = curl_timeo / 1000; + if (timeout.tv_sec > 1) + timeout.tv_sec = 1; + else + timeout.tv_usec = (curl_timeo % 1000) * 1000; + } + + /* get file descriptors from the transfers */ + mc = curl_multi_fdset(multi_handle, &fdread, &fdwrite, &fdexcep, &maxfd); + + if (mc != CURLM_OK) { + fprintf(stderr, "curl_multi_fdset() failed, code %d.\n", mc); + break; + } + + /* On success the value of maxfd is guaranteed to be >= -1. We call + select(maxfd + 1, ...); specially in case of (maxfd == -1) there are + no fds ready yet so we call select(0, ...) --or Sleep() on Windows-- + to sleep 100ms, which is the minimum suggested value in the + curl_multi_fdset() doc. */ + + if (maxfd == -1) { +#ifdef _WIN32 + Sleep(100); + rc = 0; +#else + /* Portable sleep for platforms other than Windows. */ + struct timeval wait = {0, 100 * 1000}; /* 100ms */ + rc = select(0, NULL, NULL, NULL, &wait); +#endif + } else { + /* Note that on some platforms 'timeout' may be modified by select(). + If you need access to the original value save a copy beforehand. */ + rc = select(maxfd + 1, &fdread, &fdwrite, &fdexcep, &timeout); + } + + switch (rc) { + case -1: + /* select error */ + break; + + case 0: + default: + /* timeout or readable/writable sockets */ + curl_multi_perform(multi_handle, &file->still_running); + file->http_status = get_http_response(multi_handle); + break; + } + } while (file->still_running && (file->buffer_pos < want)); + return 1; +} + +/* use to remove want bytes from the front of a files buffer */ +static int use_buffer(URL_FILE *file, size_t want) { + /* sort out buffer */ + if (file->buffer_pos <= want) { + /* ditch buffer - write will recreate */ + free(file->buffer); + file->buffer = NULL; + file->buffer_pos = 0; + file->buffer_len = 0; + } else { + /* move rest down make it available for later */ + memmove(file->buffer, + &file->buffer[want], + (file->buffer_pos - want)); + + file->buffer_pos -= want; + } + return 0; +} + +/** + * + * @param handle + * @return + */ +long get_http_response(CURLM *handle) { + long http_status = 0; + CURLMsg *m = NULL; + + do { + int msg_queue = 0; + m = curl_multi_info_read(handle, &msg_queue); + if (m != NULL) { + curl_easy_getinfo(m->easy_handle, CURLINFO_RESPONSE_CODE, &http_status); + } + } while (m); + + return http_status; +} + +URL_FILE *url_fopen(const char *url, const char *operation) { + /* this code could check for URLs or types in the 'url' and + basically use the real fopen() for standard files */ + + URL_FILE *file; + (void) operation; + + file = calloc(1, sizeof(URL_FILE)); + if (!file) + return NULL; + + file->http_status = 0; + file->handle.file = fopen(url, operation); + if (file->handle.file) + file->type = CFTYPE_FILE; /* marked as URL */ + + else { + file->type = CFTYPE_CURL; /* marked as URL */ + file->handle.curl = curl_easy_init(); + + curl_easy_setopt(file->handle.curl, CURLOPT_URL, url); + curl_easy_setopt(file->handle.curl, CURLOPT_WRITEDATA, file); + curl_easy_setopt(file->handle.curl, CURLOPT_WRITEFUNCTION, write_callback); + curl_easy_setopt(file->handle.curl, CURLOPT_VERBOSE, 0L); + curl_easy_setopt(file->handle.curl, CURLOPT_FOLLOWLOCATION, 1L); + curl_easy_setopt(file->handle.curl, CURLOPT_FAILONERROR, 1L); + + if (!multi_handle) + multi_handle = curl_multi_init(); + + curl_multi_add_handle(multi_handle, file->handle.curl); + + /* lets start the fetch */ + curl_multi_perform(multi_handle, &file->still_running); + + if ((file->buffer_pos == 0) && (!file->still_running)) { + /* if still_running is 0 now, we should return NULL */ + + /* make sure the easy handle is not in the multi handle anymore */ + curl_multi_remove_handle(multi_handle, file->handle.curl); + + /* cleanup */ + curl_easy_cleanup(file->handle.curl); + + free(file); + + file = NULL; + } + } + return file; +} + +int url_fclose(URL_FILE *file) { + int ret = 0;/* default is good return */ + + switch (file->type) { + case CFTYPE_FILE: + ret = fclose(file->handle.file); /* passthrough */ + break; + + case CFTYPE_CURL: + /* make sure the easy handle is not in the multi handle anymore */ + curl_multi_remove_handle(multi_handle, file->handle.curl); + + /* cleanup */ + curl_easy_cleanup(file->handle.curl); + break; + + default: /* unknown or supported type - oh dear */ + ret = EOF; + errno = EBADF; + break; + } + + free(file->buffer);/* free any allocated buffer space */ + free(file); + + return ret; +} + +int url_feof(URL_FILE *file) { + int ret = 0; + + switch (file->type) { + case CFTYPE_FILE: + ret = feof(file->handle.file); + break; + + case CFTYPE_CURL: + if ((file->buffer_pos == 0) && (!file->still_running)) + ret = 1; + break; + + default: /* unknown or supported type - oh dear */ + ret = -1; + errno = EBADF; + break; + } + return ret; +} + +size_t url_fread(void *ptr, size_t size, size_t nmemb, URL_FILE *file) { + size_t want; + + switch (file->type) { + case CFTYPE_FILE: + want = fread(ptr, size, nmemb, file->handle.file); + break; + + case CFTYPE_CURL: + want = nmemb * size; + + fill_buffer(file, want); + + /* check if there's data in the buffer - if not fill_buffer() + * either errored or EOF */ + if (!file->buffer_pos) + return 0; + + /* ensure only available data is considered */ + if (file->buffer_pos < want) + want = file->buffer_pos; + + /* xfer data to caller */ + memcpy(ptr, file->buffer, want); + + use_buffer(file, want); + + want = want / size; /* number of items */ + break; + + default: /* unknown or supported type - oh dear */ + want = 0; + errno = EBADF; + break; + + } + return want; +} + +char *url_fgets(char *ptr, size_t size, URL_FILE *file) { + size_t want = size - 1;/* always need to leave room for zero termination */ + size_t loop; + + switch (file->type) { + case CFTYPE_FILE: + ptr = fgets(ptr, (int) size, file->handle.file); + break; + + case CFTYPE_CURL: + fill_buffer(file, want); + + /* check if there's data in the buffer - if not fill either errored or + * EOF */ + if (!file->buffer_pos) + return NULL; + + /* ensure only available data is considered */ + if (file->buffer_pos < want) + want = file->buffer_pos; + + /*buffer contains data */ + /* look for newline or eof */ + for (loop = 0; loop < want; loop++) { + if (file->buffer[loop] == '\n') { + want = loop + 1;/* include newline */ + break; + } + } + + /* xfer data to caller */ + memcpy(ptr, file->buffer, want); + ptr[want] = 0;/* always null terminate */ + + use_buffer(file, want); + + break; + + default: /* unknown or supported type - oh dear */ + ptr = NULL; + errno = EBADF; + break; + } + + return ptr;/*success */ +} + +void url_rewind(URL_FILE *file) { + switch (file->type) { + case CFTYPE_FILE: + rewind(file->handle.file); /* passthrough */ + break; + + case CFTYPE_CURL: + /* halt transaction */ + curl_multi_remove_handle(multi_handle, file->handle.curl); + + /* restart */ + curl_multi_add_handle(multi_handle, file->handle.curl); + + /* ditch buffer - write will recreate - resets stream pos*/ + free(file->buffer); + file->buffer = NULL; + file->buffer_pos = 0; + file->buffer_len = 0; + + break; + + default: /* unknown or supported type - oh dear */ + break; + } +} + +#define FGETSFILE "fgets.test" +#define FREADFILE "fread.test" +#define REWINDFILE "rewind.test" + +/* Small main program to retrieve from a url using fgets and fread saving the + * output to two test files (note the fgets method will corrupt binary files if + * they contain 0 chars */ +int _test_url_fopen(int argc, char *argv[]) { + URL_FILE *handle; + FILE *outf; + + size_t nread; + char buffer[256]; + const char *url; + + if (argc < 2) + url = "http://192.168.7.3/testfile";/* default to testurl */ + else + url = argv[1];/* use passed url */ + + /* copy from url line by line with fgets */ + outf = fopen(FGETSFILE, "wb+"); + if (!outf) { + perror("couldn't open fgets output file\n"); + return 1; + } + + handle = url_fopen(url, "r"); + if (!handle) { + printf("couldn't url_fopen() %s\n", url); + fclose(outf); + return 2; + } + + while (!url_feof(handle)) { + url_fgets(buffer, sizeof(buffer), handle); + fwrite(buffer, 1, strlen(buffer), outf); + } + + url_fclose(handle); + + fclose(outf); + + + /* Copy from url with fread */ + outf = fopen(FREADFILE, "wb+"); + if (!outf) { + perror("couldn't open fread output file\n"); + return 1; + } + + handle = url_fopen("testfile", "r"); + if (!handle) { + printf("couldn't url_fopen() testfile\n"); + fclose(outf); + return 2; + } + + do { + nread = url_fread(buffer, 1, sizeof(buffer), handle); + fwrite(buffer, 1, nread, outf); + } while (nread); + + url_fclose(handle); + + fclose(outf); + + + /* Test rewind */ + outf = fopen(REWINDFILE, "wb+"); + if (!outf) { + perror("couldn't open fread output file\n"); + return 1; + } + + handle = url_fopen("testfile", "r"); + if (!handle) { + printf("couldn't url_fopen() testfile\n"); + fclose(outf); + return 2; + } + + nread = url_fread(buffer, 1, sizeof(buffer), handle); + fwrite(buffer, 1, nread, outf); + url_rewind(handle); + + buffer[0] = '\n'; + fwrite(buffer, 1, 1, outf); + + nread = url_fread(buffer, 1, sizeof(buffer), handle); + fwrite(buffer, 1, nread, outf); + + url_fclose(handle); + + fclose(outf); + + return 0;/* all done */ +} + +const char *http_response_str(long code) { + switch (code) { + case 100: + return "Continue"; + case 101: + return "Switching Protocol"; + case 102: + return "Processing (WebDAV)"; + case 103: + return "Early Hints"; + case 200: + return "OK"; + case 201: + return "Created"; + case 202: + return "Accepted"; + case 203: + return "Non-Authoritative Information"; + case 204: + return "No Content"; + case 205: + return "Reset Content"; + case 206: + return "Partial Content"; + case 207: + return "Multi-Status (WebDAV)"; + case 208: + return "Already Reported"; + case 226: + return "IM Used"; + case 300: + return "Multiple Choices"; + case 301: + return "Moved Permanently"; + case 302: + return "Found"; + case 303: + return "See Other"; + case 304: + return "Not Modified"; + case 305: + return "Use Proxy (DEPRECATED)"; + case 306: + return "Unused"; + case 307: + return "Temporary Redirect"; + case 308: + return "Permanent Redirect"; + case 400: + return "Bad Request"; + case 401: + return "Unauthorized"; + case 402: + return "Payment Required"; + case 403: + return "Forbidden"; + case 404: + return "Not Found"; + case 405: + return "Method Not Allowed"; + case 406: + return "Not Acceptable"; + case 407: + return "Proxy Authentication Required"; + case 408: + return "Request Timeout"; + case 409: + return "Conflict"; + case 410: + return "Gone"; + case 411: + return "Length Required"; + case 412: + return "Precondition Failed"; + case 413: + return "Payload Too Large"; + case 414: + return "URI Too Long"; + case 415: + return "Unsupported Media Type"; + case 416: + return "Range Not Satisfiable"; + case 417: + return "Exception Failed"; + case 418: + return "I'm a teapot"; + case 419: + return "Expectation Failed"; + case 421: + return "Misdirected Request"; + case 422: + return "Unprocessable Entity (WebDAV)"; + case 423: + return "Locked (WebDAV)"; + case 424: + return "Failed Dependency (WebDAV)"; + case 425: + return "Too Early"; + case 426: + return "Upgrade Required"; + case 428: + return "Precondition Required"; + case 429: + return "Too Many Requests"; + case 431: + return "Request Header Fields Too Large"; + case 451: + return "Unavailable For Legal Reasons"; + case 500: + return "Internal Server Error"; + case 501: + return "Not Implemented"; + case 502: + return "Bad Gateway"; + case 503: + return "Service Unavailable"; + case 504: + return "Gateway Timeout"; + case 505: + return "HTTP Version Not Supported"; + case 506: + return "Variant Also Negotiates"; + case 507: + return "Insufficient Storage (WebDAV)"; + case 508: + return "Loop Detected (WebDAV)"; + case 510: + return "Not Extended"; + case 511: + return "Network Authentication Required"; + default: + return "Unknown"; + } +} diff --git a/lib/find.c b/lib/find.c new file mode 100644 index 0000000..348a8f7 --- /dev/null +++ b/lib/find.c @@ -0,0 +1,151 @@ +/** + * @file find.c + */ +#include "spm.h" + +/** + * glob callback function + * @param epath path to file that generated the error condition + * @param eerrno the error condition + * @return the error condition + */ +int errglob(const char *epath, int eerrno) { + fprintf(stderr, "glob matching error: %s (%d)", epath, eerrno); + return eerrno; +} + +/** + * Scan a directory for a file by name, or by wildcard + * + * @param root directory path to scan + * @param filename file to find (wildcards accepted) + * @return success=path to file, failure=NULL + */ +char *find_file(const char *root, const char *filename) { + glob_t results; + int glob_flags = 0; + int match = 0; + char path[PATH_MAX]; + memset(path, '\0', PATH_MAX); + + // GUARD + if (!root || !filename || strstr(filename, "..") || strstr(filename, "./")) { + return NULL; + } + + if (realpath(root, path) == NULL) { + perror("Cannot determine realpath()"); + fprintf(SYSERROR); + return NULL; + } + + strcat(path, "/"); + strcat(path, filename); + + // Save a little time if the file exists + if (access(path, F_OK) != -1) { + return strdup(path); + } + + // Inject wildcard + strcat(path, "*"); + // Search for the file + match = glob(path, glob_flags, errglob, &results); + + if (match != 0) { + // report critical errors except GLOB_NOMATCH + if (match == GLOB_NOSPACE || match == GLOB_ABORTED) { + fprintf(SYSERROR); + } + globfree(&results); + return NULL; + } + + // Replace path string with wanted path string + strcpy(path, results.gl_pathv[0]); + + globfree(&results); + return strdup(path); +} + +/** + * Scan the package directory for a package by name + * @param filename file to find + * @return success=path to file, failure=NULL + */ +char *find_package(const char *filename) { + char *repo = join((char *[]) {SPM_GLOBAL.package_dir, SPM_GLOBAL.repo_target, NULL}, DIRSEPS); + char *match = find_file(repo, filename); + free(repo); + return match; +} + +/** + * Determine whether `pattern` is present within a file + * @param filename + * @param pattern + * @return 0=found, 1=not found, -1=OS error + */ +int find_in_file(const char *filename, const char *pattern) { + int result = 1; // default "not found" + + FILE *fp = fopen(filename, "rb"); + if (!fp) { + return -1; + } + + long int file_len = get_file_size(filename); + if (file_len == -1) { + fclose(fp); + return -1; + } + char *buffer = (char *)calloc((size_t) file_len, sizeof(char)); + if (!buffer) { + fclose(fp); + return -1; + } + size_t pattern_len = strlen(pattern); + + fread(buffer, (size_t) file_len, sizeof(char), fp); + fclose(fp); + + for (size_t i = 0; i < (size_t) file_len; i++) { + if (memcmp(&buffer[i], pattern, pattern_len) == 0) { + result = 0; // found + break; + } + } + free(buffer); + return result; +} + +/** + * Get the full path of a shell command + * @param program + * @return success=absolute path to program, failure=NULL + */ +char *find_executable(const char *program) { + int found = 0; + char *result = NULL; + char *env_path = NULL; + env_path = getenv("PATH"); + if (!env_path) { + return NULL; + } + char **search_paths = split(env_path, ":"); + + char buf[PATH_MAX]; + for (int i = 0; search_paths[i] != NULL; i++) { + sprintf(buf, "%s%c%s", search_paths[i], DIRSEP, program); + if (access(buf, F_OK | X_OK) == 0) { + found = 1; + break; + } + memset(buf, '\0', sizeof(buf)); + } + if (found) { + result = strdup(buf); + } + split_free(search_paths); + return result; +} diff --git a/lib/fs.c b/lib/fs.c new file mode 100644 index 0000000..d920248 --- /dev/null +++ b/lib/fs.c @@ -0,0 +1,504 @@ +/** + * @file fs.c + */ +#include "spm.h" + +/** + * + * @param _path + * @return + */ +FSTree *fstree(const char *_path, char **filter_by, unsigned int filter_mode) { + FTS *parent = NULL; + FTSENT *node = NULL; + FSTree *fsdata = NULL; + int no_filter = 0; + char *path = NULL; + char *abspath = realpath(_path, NULL); + + if (filter_mode & SPM_FSTREE_FLT_RELATIVE) { + path = strdup(_path); + } else { + path = abspath; + } + + if (path == NULL) { + perror(_path); + fprintf(SYSERROR); + return NULL; + } + char *root[2] = { path, NULL }; + + if (filter_by == NULL) { + // Create an array with an empty string. This signifies we want don't want to filter any paths. + no_filter = 1; + filter_by = calloc(2, sizeof(char *)); + filter_by[0] = calloc(2, sizeof(char)); + strcpy(filter_by[0], ""); + } + + size_t dirs_size = 2; + size_t dirs_records = 0; + size_t files_size = 2; + size_t files_records = 0; + + fsdata = (FSTree *)calloc(1, sizeof(FSTree)); + fsdata->root = (char *)calloc(PATH_MAX, sizeof(char)); + fsdata->dirs = (char **)calloc(dirs_size, sizeof(char *)); + fsdata->files = (char **)calloc(files_size, sizeof(char *)); + + if (filter_mode & SPM_FSTREE_FLT_RELATIVE) { + // Return an absolute path regardless + strncpy(fsdata->root, abspath, PATH_MAX - 1); + } else { + strncpy(fsdata->root, path, PATH_MAX - 1); + } + + parent = fts_open(root, FTS_PHYSICAL | FTS_NOCHDIR, &_fstree_compare); + + if (parent != NULL) { + while ((node = fts_read(parent)) != NULL) { + for (size_t i = 0; filter_by[i] != NULL; i++) { + // Drop paths containing filter string(s) according to the requested mode + if (filter_mode & SPM_FSTREE_FLT_CONTAINS && strstr(node->fts_path, filter_by[i]) == NULL) { + continue; + } + else if (filter_mode & SPM_FSTREE_FLT_ENDSWITH && !endswith(node->fts_path, filter_by[i])) { + continue; + } + else if (filter_mode & SPM_FSTREE_FLT_STARTSWITH && !startswith(node->fts_path, filter_by[i])) { + continue; + } + switch (node->fts_info) { + case FTS_D: + if (strcmp(node->fts_path, "..") == 0 || strcmp(node->fts_path, ".") == 0) { + continue; + } + fsdata->dirs = (char **) realloc(fsdata->dirs, sizeof(char *) * dirs_size); + fsdata->dirs[dirs_size - 1] = NULL; + fsdata->dirs[dirs_records] = (char *) calloc(strlen(node->fts_path) + 1, sizeof(char)); + strncpy(fsdata->dirs[dirs_records], node->fts_path, strlen(node->fts_path)); + dirs_size++; + dirs_records++; + break; + case FTS_F: + case FTS_SL: + fsdata->files = (char **) realloc(fsdata->files, sizeof(char *) * files_size); + fsdata->files[files_size - 1] = NULL; + fsdata->files[files_records] = (char *) calloc(strlen(node->fts_path) + 1, sizeof(char)); + strncpy(fsdata->files[files_records], node->fts_path, strlen(node->fts_path)); + files_size++; + files_records++; + break; + default: + break; + } + } + } + fts_close(parent); + } + fsdata->dirs_length = dirs_records; + fsdata->files_length = files_records; + free(path); + if (no_filter) { + free(filter_by[0]); + free(filter_by); + } + return fsdata; +} + +/** + * + * @param one + * @param two + * @return + */ +int _fstree_compare(const FTSENT **one, const FTSENT **two) { + return (strcmp((*one)->fts_name, (*two)->fts_name)); +} + +/** + * + * @param _path + * @return + */ +int rmdirs(const char *_path) { + if (access(_path, F_OK) != 0) { + return -1; + } + + FSTree *data = fstree(_path, NULL, SPM_FSTREE_FLT_NONE); + if (data->files) { + for (size_t i = 0; data->files[i] != NULL; i++) { + remove(data->files[i]); + } + } + if (data->dirs) { + for (size_t i = data->dirs_length - 1; i != 0; i--) { + remove(data->dirs[i]); + } + } + remove(data->root); + + fstree_free(data); + return 0; +} + +/** + * Free a `FSTree` structure + * @param fsdata + */ +void fstree_free(FSTree *fsdata) { + if (fsdata != NULL) { + if (fsdata->root != NULL) { + free(fsdata->root); + } + if (fsdata->files != NULL) { + for (int i = 0; fsdata->files[i] != NULL; i++) { + free(fsdata->files[i]); + } + free(fsdata->files); + } + if (fsdata->dirs != NULL) { + for (int i = 0; fsdata->dirs[i] != NULL; i++) { + free(fsdata->dirs[i]); + } + free(fsdata->dirs); + } + free(fsdata); + } +} + +/** + * Expand "~" to the user's home directory + * + * Example: + * ~~~{.c} + * char *home = expandpath("~"); // == /home/username + * char *config = expandpath("~/.config"); // == /home/username/.config + * char *nope = expandpath("/tmp/test"); // == /tmp/test + * char *nada = expandpath("/~/broken"); // == /~/broken + * + * free(home); + * free(config); + * free(nope); + * free(nada); + * ~~~ + * + * @param _path (Must start with a `~`) + * @return success=expanded path or original path, failure=NULL + */ +char *expandpath(const char *_path) { + if (_path == NULL) { + return NULL; + } + const char *homes[] = { + "HOME", + "USERPROFILE", + }; + char home[PATH_MAX]; + char tmp[PATH_MAX]; + char *ptmp = tmp; + char result[PATH_MAX]; + char *sep = NULL; + + memset(home, '\0', sizeof(home)); + memset(ptmp, '\0', sizeof(tmp)); + memset(result, '\0', sizeof(result)); + + strncpy(ptmp, _path, PATH_MAX - 1); + + // Check whether there's a reason to continue processing the string + if (*ptmp != '~') { + return strdup(ptmp); + } + + // Remove tilde from the string and shift its contents to the left + strchrdel(ptmp, "~"); + + // Figure out where the user's home directory resides + for (size_t i = 0; i < sizeof(homes); i++) { + char *tmphome; + if ((tmphome = getenv(homes[i])) != NULL) { + strncpy(home, tmphome, PATH_MAX - 1); + break; + } + } + + // A broken runtime environment means we can't do anything else here + if (isempty(home)) { + return NULL; + } + + // Scan the path for a directory separator + if ((sep = strpbrk(ptmp, "/\\")) != NULL) { + // Jump past it + ptmp = sep + 1; + } + + // Construct the new path + strncat(result, home, PATH_MAX - 1); + if (sep) { + strncat(result, DIRSEPS, PATH_MAX - 1); + strncat(result, ptmp, PATH_MAX - 1); + } + + return strdup(result); +} + +/** + * Converts Win32 path to Unix path, and vice versa + * - On UNIX, Win32 paths will be converted UNIX + * - On Win32, UNIX paths will be converted to Win32 + * + * This function is platform dependent. The string is modified in-place. + * + * @param path a system path + * @return string + */ +char *normpath(const char *path) { + char *result = strdup(path); + char *tmp = result; + + while (*tmp) { + if (*tmp == NOT_DIRSEP) { + *tmp = DIRSEP; + tmp++; + continue; + } + tmp++; + } + return result; +} + + +/** + * Strip file name from directory + * Note: Caller is responsible for freeing memory + * + * @param _path + * @return success=path to directory, failure=NULL + */ +char *dirname(const char *_path) { + if (_path == NULL) { + return NULL; + } + char *path = strdup(_path); + char *last = strrchr(path, DIRSEP); + if (!last) { + return NULL; + } + // Step backward, stopping on the first non-separator + // This ensures strings like "/usr//////" are converted to "/usr", but... + // it will do nothing to fix up a path like "/usr//////bin/bash + char *lookback = last; + while (*(lookback - 1) == DIRSEP) { + lookback--; + } + + *lookback = '\0'; + return path; +} + +/** + * Strip directory from file name + * Note: Caller is responsible for freeing memory + * + * @param _path + * @return success=file name, failure=NULL + */ +char *basename(char *path) { + char *result = NULL; + char *last = NULL; + + if ((last = strrchr(path, DIRSEP)) == NULL) { + return result; + } + // Perform a lookahead ensuring the string is valid beyond the last separator + if (last++ != NULL) { + result = last; + } + + return result; +} + +/** + * Basic rsync wrapper for copying files + * @param _args arguments to pass to rsync (set to `NULL` for default options) + * @param _source source file or directory + * @param _destination destination file or directory + * @return success=0, failure=-1 + */ +int rsync(const char *_args, const char *_source, const char *_destination) { + int returncode; + Process *proc = NULL; + char *args = NULL; + if (_args) { + args = strdup(_args); + } + char *source = strdup(_source); + char *destination = strdup(_destination); + char cmd[PATH_MAX]; + char *args_combined = (char *)calloc(PATH_MAX, sizeof(char)); + + memset(cmd, '\0', sizeof(cmd)); + strcpy(args_combined, "--archive --hard-links "); + if (args) { + strcat(args_combined, _args); + } + + strchrdel(args_combined, SHELL_INVALID); + strchrdel(source, SHELL_INVALID); + strchrdel(destination, SHELL_INVALID); + + snprintf(cmd, PATH_MAX, "rsync %s \"%s\" \"%s\" 2>&1", args_combined, source, destination); + shell(&proc, SHELL_OUTPUT, cmd); + if (!proc) { + if (args) { + free(args); + } + free(source); + free(destination); + return -1; + } + + returncode = proc->returncode; + if (returncode != 0 && proc->output) { + fprintf(stderr, "%s\n", proc->output); + } + shell_free(proc); + + if (args) { + free(args); + } + free(args_combined); + free(source); + free(destination); + return returncode; +} + +/** + * Return the size of a file + * @param filename + * @return + */ +long int get_file_size(const char *filename) { + long int result = 0; + FILE *fp = fopen(filename, "rb"); + if (!fp) { + return -1; + } + fseek(fp, 0, SEEK_END); + result = ftell(fp); + fclose(fp); + return result; +} + +/** + * Attempt to create a directory (or directories) + * @param _path A path to create + * @param mode UNIX permissions (octal) + * @return success=0, failure=-1 (+ errno will be set) + */ +int mkdirs(const char *_path, mode_t mode) { + int result = 0; + char *path = normpath(_path); + char tmp[PATH_MAX]; + tmp[0] = '\0'; + + char sep[2]; + sprintf(sep, "%c", DIRSEP); + char **parts = split(path, sep); + for (int i = 0; parts[i] != NULL; i++) { + strcat(tmp, parts[i]); + strcat(tmp, sep); + if (access(tmp, F_OK) != 0) { + result = mkdir(tmp, mode); + } + } + split_free(parts); + return result; +} + +/** + * Short wrapper for `access`. Check if file exists. + * + * Example: + * ~~~{.c} + * if (exists("example.txt") != 0) { + * // handle error + * } + * ~~~ + * @param filename + * @return + */ +int exists(const char *filename) { + return access(filename, F_OK); +} + +/** + * Convert size in bytes to the closest human-readable unit + * + * NOTE: Caller is responsible for freeing memory + * + * Example: + * ~~~{.c} + * char *output; + * output = human_readable_size(1); // "1B" + * free(output); + * output = human_readable_size(1024) // "1.0K" + * free(output); + * output = human_readable_size(1024000) // "1.0M" + * free(output); + * // and so on + * ~~~ + * + * @param n size to convert + * @return string + */ +char *human_readable_size(uint64_t n) { + size_t i; + double result = (double)n; + char *unit[] = {"B", "K", "M", "G", "T", "P", "E"}; + char r[255]; + memset(r, '\0', sizeof(r)); + + for (i = 0; i < sizeof(unit); i++) { + if (fabs(result) < 1024) { + break; + } + result /= 1024.0; + } + + if (unit[i][0] == 'B') { + sprintf(r, "%0.0lf%s", result, unit[i]); + } + else { + sprintf(r, "%0.2lf%s", result, unit[i]); + } + + return strdup(r); +} + +/** + * Create a named temporary directory + * @param name + * @return success=path, failure=NULL + */ +char *spm_mkdtemp(const char *name, const char *extended_path) { + const char *template_unique = "XXXXXX"; + char *tmpdir = NULL; + char *template = calloc(PATH_MAX, sizeof(char)); + + sprintf(template, "%s%s%s_%s", TMP_DIR, DIRSEPS, name, template_unique); + tmpdir = mkdtemp(template); + if (extended_path != NULL) { + char extended[PATH_MAX] = {0,}; + strncpy(extended, tmpdir, PATH_MAX - 1); + strcat(extended, DIRSEPS); + strcat(extended, extended_path); + mkdirs(extended, 0755); + } + return tmpdir; +} + diff --git a/lib/install.c b/lib/install.c new file mode 100644 index 0000000..e0592db --- /dev/null +++ b/lib/install.c @@ -0,0 +1,328 @@ +/** + * @file install.c + */ +#include <url.h> +#include "spm.h" + +void spm_install_show_package(ManifestPackage *package) { + if (package == NULL) { + fprintf(stderr, "ERROR: package was NULL\n"); + return; + } + printf(" -> %-20s %-10s (origin: %s)\n", package->name, package->version, package->origin); +} + +/** + * Install a package and its dependencies into a destination root. + * The destination is created if it does not exist. + * @param _destroot directory to install package + * @param _package name of archive to install (not a path) + * @return success=0, exists=1, error=-1 (general), -2 (unable to create `destroot`) + */ +int spm_install(SPM_Hierarchy *fs, const char *tmpdir, const char *_package) { + char *package = strdup(_package); + + if (!package) { + fprintf(SYSERROR); + return -1; + } + + if (exists(fs->rootdir) != 0) { + if (SPM_GLOBAL.verbose) { + printf("Creating destination root: %s\n", fs->rootdir); + } + if (mkdirs(fs->rootdir, 0755) != 0) { + fprintf(SYSERROR); + free(package); + return -2; + } + } + + if (SPM_GLOBAL.verbose) { + printf("Extracting archive: %s\n", package); + } + + if (tar_extract_archive(package, tmpdir) != 0) { + fprintf(stderr, "%s: %s\n", package, strerror(errno)); + free(package); + return -1; + } + + free(package); + return 0; +} + +int spm_install_package_record(SPM_Hierarchy *fs, char *tmpdir, char *package_name) { + RuntimeEnv *rt = runtime_copy(__environ); + char *records_topdir = strdup(fs->dbrecdir); + char *records_pkgdir = join((char *[]) {records_topdir, package_name, NULL}, DIRSEPS); + char *descriptor = join((char *[]) {tmpdir, SPM_META_DESCRIPTOR, NULL}, DIRSEPS); + char *filelist = join((char *[]) {tmpdir, SPM_META_FILELIST, NULL}, DIRSEPS); + + if (exists(records_pkgdir) != 0) { + if (mkdirs(records_pkgdir, 0755) != 0) { + return -1; + } + } + + if (exists(descriptor) != 0) { + fprintf(stderr, "Missing: %s\n", descriptor); + return 1; + } + + if (exists(filelist) != 0) { + fprintf(stderr, "Missing: %s\n", filelist); + return 2; + } + + if (rsync(NULL, descriptor, records_pkgdir) != 0) { + fprintf(stderr, "Failed to copy '%s' to '%s'\n", descriptor, records_pkgdir); + return 3; + } + + if (rsync(NULL, filelist, records_pkgdir) != 0) { + fprintf(stderr, "Failed to copy '%s' to '%s'\n", filelist, records_pkgdir); + return 4; + } + + free(records_topdir); + free(records_pkgdir); + free(descriptor); + free(filelist); + runtime_free(rt); + return 0; +} + +int spm_check_installed(SPM_Hierarchy *fs, char *package_name) { + char *records_topdir = join((char *[]) {fs->localstatedir, "db", "records", NULL}, DIRSEPS); + char *records_pkgdir = join((char *[]) {records_topdir, package_name, NULL}, DIRSEPS); + + char *descriptor = join((char *[]) {records_pkgdir, SPM_META_DESCRIPTOR, NULL}, DIRSEPS); + char *filelist = join((char *[]) {records_pkgdir, SPM_META_FILELIST, NULL}, DIRSEPS); + char **data = NULL; + + if ((exists(records_pkgdir) || exists(descriptor) || exists(descriptor)) != 0) { + free(records_topdir); + free(records_pkgdir); + free(descriptor); + free(filelist); + return 0; // does not exist + } + + data = spm_metadata_read(filelist, SPM_METADATA_VERIFY); + if (data == NULL) { + free(records_topdir); + free(records_pkgdir); + free(descriptor); + free(filelist); + return -1; + } + + for (size_t i = 0; data[i] != NULL; i++) { + free(data[i]); + } + free(data); + + free(records_topdir); + free(records_pkgdir); + free(descriptor); + free(filelist); + return 1; // exists +} + +/** + * + * @return + */ +char *spm_install_fetch(const char *pkgdir, const char *_package) { + char *package = strdup(_package); + if (package == NULL) { + perror("could not allocate memory for package name"); + fprintf(SYSERROR); + return NULL; + } + + long response = 0; + char *url = strdup(package); + char *payload = join_ex(DIRSEPS, pkgdir, basename(package), NULL); + size_t tmp_package_len = strlen(payload); + + if (tmp_package_len > strlen(package)) { + char *tmp = realloc(package, (tmp_package_len + 1) * sizeof(char)); + if (tmp == NULL) { + perror("cannot realloc package path"); + return NULL; + } + package = tmp; + } + strcpy(package, payload); + + if (exists(payload) != 0) { + if ((response = fetch(url, package)) >= 400) { + fprintf(stderr, "HTTP(%ld): %s\n", response, http_response_str(response)); + return NULL; + } + } + free(url); + free(payload); + + return package; +} + +/** + * Perform a full package installation + * @param mf + * @param rootdir + * @param packages + * @return 0=success, -1=failed to create storage, -2=denied by user + */ +int spm_do_install(SPM_Hierarchy *fs, ManifestList *mf, StrList *packages) { + size_t num_requirements = 0; + ManifestPackage **requirements = NULL; + char source[PATH_MAX]; + char *tmpdir = spm_mkdtemp("spm_destroot", NULL); + + if (tmpdir == NULL) { + perror("Could not create temporary destination root"); + fprintf(SYSERROR); + return -1; + } + + if (SPM_GLOBAL.verbose) { + printf("Installation root: %s\n", fs->rootdir); + } + + // Produce a dependency tree from requested package(s) + for (size_t i = 0; i < strlist_count(packages); i++) { + char *item = strlist_item(packages, i); + requirements = resolve_dependencies(mf, item); + if (requirements != NULL) { + for (size_t c = num_requirements; requirements[c] != NULL; c++) { + num_requirements++; + } + } + } + + // Install packages + printf("Requested package(s):\n"); + for (size_t i = 0; requirements !=NULL && requirements[i] != NULL; i++) { + spm_install_show_package(requirements[i]); + } + + if (SPM_GLOBAL.prompt_user) { + if (spm_prompt_user("Proceed with installation?", 1) == 0) { + exit(-2); + } + } + + int fetched = 0; + char *package_dir = strdup(SPM_GLOBAL.package_dir); + for (size_t i = 0; requirements != NULL && requirements[i] != NULL; i++) { + char *package_origin = calloc(PATH_MAX, sizeof(char)); + strncpy(package_origin, requirements[i]->origin, PATH_MAX); + + if (strstr(package_origin, SPM_GLOBAL.repo_target) == NULL) { + if (!endswith(package_origin, DIRSEPS)) { + strcat(package_origin, DIRSEPS); + } + strcat(package_origin, SPM_GLOBAL.repo_target); + } + + char *package_path = join((char *[]) {package_origin, requirements[i]->archive, NULL}, DIRSEPS); + char *package_localpath = join_ex(DIRSEPS, package_dir, requirements[i]->archive, NULL); + free(package_origin); + + // Download the archive if necessary + if (strstr(package_path, "://") != NULL && exists(package_localpath) != 0) { + printf("Fetching: %s\n", package_path); + package_path = spm_install_fetch(package_dir, package_path); + if (package_path == NULL) { + free(package_path); + free(package_localpath); + exit(1); + } + fetched = 1; + } + // Or copy the archive if necessary + else { + // TODO: Possibly an issue down the road, but not at the moment + // You have another local manifest in use. Copy any used packages from there into the local package directory. + if (exists(package_localpath) != 0 && strncmp(package_dir, package_path, strlen(package_dir)) != 0) { + printf("Copying: %s\n", package_path); + if (rsync(NULL, package_path, package_dir) != 0) { + fprintf(stderr, "Unable to copy: %s to %s\n", package_path, package_dir); + return -1; + } + fetched = 1; + } else if (exists(package_localpath) != 0) { + // All attempts to retrieve the requested package have failed. Die. + fprintf(stderr, "Package manifest in '%s' claims '%s' exists, however it does not.\n", requirements[i]->origin, package_path); + return -1; + } + } + free(package_path); + free(package_localpath); + } + + // Update the package manifest + if (fetched) { + printf("Updating package manifest...\n"); + Manifest *tmp_manifest = manifest_from(SPM_GLOBAL.package_dir); + manifest_write(tmp_manifest, package_dir); + manifest_free(tmp_manifest); + } + + printf("Installing package(s):\n"); + size_t num_installed = 0; + for (size_t i = 0; requirements != NULL && requirements[i] != NULL; i++) { + char *package_path = join((char *[]) {package_dir, requirements[i]->archive, NULL}, DIRSEPS); + + if (spm_check_installed(fs, requirements[i]->name)) { + printf(" -> %s is already installed\n", requirements[i]->name); + free(package_path); + continue; + } + + spm_install_show_package(requirements[i]); + spm_install(fs, tmpdir, package_path); + + // Relocate installation root + relocate_root(fs->rootdir, tmpdir); + + spm_install_package_record(fs, tmpdir, requirements[i]->name); + num_installed++; + free(package_path); + } + + // free requirements array + for (size_t i = 0; requirements != NULL && requirements[i] != NULL; i++) { + manifest_package_free(requirements[i]); + } + free(package_dir); + + if (num_installed != 0) { + // Append a trailing slash to tmpdir to direct rsync to copy files, not the directory, into destroot + sprintf(source, "%s%c", tmpdir, DIRSEP); + + // Remove metadata files before copying + if (SPM_GLOBAL.verbose) { + printf("Removing metadata\n"); + } + spm_metadata_remove(source); + + // Copy temporary directory to destination + if (SPM_GLOBAL.verbose) { + printf("Installing tree: '%s' => '%s'\n", source, fs->rootdir); + } + + if (rsync(NULL, source, fs->rootdir) != 0) { + exit(1); + } + } + + if (SPM_GLOBAL.verbose) { + printf("Removing temporary storage: '%s'\n", tmpdir); + } + rmdirs(tmpdir); + return 0; +} diff --git a/lib/internal_cmd.c b/lib/internal_cmd.c new file mode 100644 index 0000000..a192ccf --- /dev/null +++ b/lib/internal_cmd.c @@ -0,0 +1,401 @@ +/** + * @file internal_cmd.c + */ +#include "spm.h" + +/** + * List of valid internal commands + */ +static char *internal_commands[] = { + "mkprefixbin", "generate prefix manifest (binary)", + "mkprefixtext", "generate prefix manifest (text)", + "mkmanifest", "generate package repository manifest", + "mkruntime", "emit runtime environment (stdout)", + "mirror_clone", "mirror a mirror", + "rpath_set", "modify binary RPATH", + "rpath_autoset", "determine nearest lib directory and set RPATH", + "get_package_ext", "show the default archive extension", + "get_sys_target", "show this system's arch/platform", + "check_rt_env", "check the integrity of the calling runtime environment", + NULL, NULL, +}; + +/** + * + */ +void mkprefix_interface_usage(void) { + printf("usage: mkprefix[bin|text] {output_file} {dir} {prefix ...}\n"); +} + +/** + * Create prefix manifests from the CLI + * @param argc + * @param argv + * @return return value of `prefixes_write` + */ +int mkprefix_interface(int argc, char **argv) { + char *command = argv[0]; + char *outfile = argv[1]; + char *tree = argv[2]; + + size_t prefix_start = 3; + size_t prefixes = 0; + for (size_t i = prefix_start; i < (size_t) argc; i++) { + prefixes = i; + } + + // Check arguments + if (!outfile) { + fprintf(stderr, "error: missing output file name\n"); + mkprefix_interface_usage(); + return -1; + } + if (!tree) { + fprintf(stderr, "error: missing directory path\n"); + mkprefix_interface_usage(); + return -1; + } + if (!prefixes) { + fprintf(stderr, "error: missing prefix string(s)\n"); + mkprefix_interface_usage(); + return -1; + } + + char **prefix = (char **) calloc(prefixes + 1, sizeof(char *)); + if (!prefix) { + perror("prefix array"); + fprintf(SYSERROR); + return -1; + } + + // Populate array of prefixes; reusing pointers from argv + for (size_t i = 0; (i + prefix_start) < (size_t) argc; i++) { + prefix[i] = argv[(i + prefix_start)]; + } + + if (SPM_GLOBAL.verbose) { + printf("Generating prefix manifest: %s\n", outfile); + } + + int result = 0; + if (strcmp(command, "mkprefixbin") == 0) { + result = prefixes_write(outfile, PREFIX_WRITE_BIN, prefix, tree); + } else if (strcmp(command, "mkprefixtext") == 0) { + result = prefixes_write(outfile, PREFIX_WRITE_TEXT, prefix, tree); + } + return result; +} + +/** + * + */ +void mkmanifest_interface_usage(void) { + printf("usage: mkmanifest [package_dir] [output_dir]\n"); +} + +/** + * Generate a named package manifest + * @param argc + * @param argv + * @return value of `manifest_write` + */ +int mkmanifest_interface(int argc, char **argv) { + Manifest *manifest = NULL; + int result = 0; + char *pkgdir = NULL; + + if (argc < 2) { + mkmanifest_interface_usage(); + return -1; + } + + if ((pkgdir = expandpath(argv[1])) == NULL) { + fprintf(stderr, "bad path\n"); + return -2; + } + + if (exists(pkgdir) != 0) { + fprintf(stderr, "'%s': does not exist\n", pkgdir); + return -3; + } + + manifest = manifest_from(pkgdir); + if (manifest == NULL) { + fprintf(stderr, "no packages\n"); + return -4; + } + + result = manifest_write(manifest, pkgdir); + if (result != 0) { + fprintf(stderr, "an error occurred while writing manifest data\n"); + manifest_free(manifest); + return -5; + } + + free(pkgdir); + manifest_free(manifest); + return result; +} + +/** + * + */ +void mkruntime_interface_usage(void) { + printf("usage: mkruntime {root_dir}\n"); +} + +/** + * + * @param argc + * @param argv + * @return + */ +int mkruntime_interface(int argc, char **argv) { + if (argc < 2) { + mkruntime_interface_usage(); + return -1; + } + + RuntimeEnv *rt = runtime_copy(__environ); + if (rt == NULL) { + return -1; + } + + char *root = argv[1]; + SPM_Hierarchy *fs = spm_hierarchy_init(root); + char *spm_pkgconfigdir = join((char *[]) {fs->libdir, "pkgconfig", NULL}, DIRSEPS); + + runtime_set(rt, "SPM_BIN", fs->bindir); + runtime_set(rt, "SPM_INCLUDE", fs->includedir); + runtime_set(rt, "SPM_LIB", fs->libdir); + runtime_set(rt, "SPM_LIB64", "${SPM_LIB}64"); + runtime_set(rt, "SPM_DATA", fs->datadir); + runtime_set(rt, "SPM_MAN", fs->mandir); + runtime_set(rt, "SPM_LOCALSTATE", fs->localstatedir); + runtime_set(rt, "SPM_PKGCONFIG", spm_pkgconfigdir); + runtime_set(rt, "SPM_PKGCONFIG", "${SPM_PKGCONFIG}:${SPM_LIB64}/pkgconfig:${SPM_DATA}/pkgconfig"); + runtime_set(rt, "SPM_META_DEPENDS", SPM_META_DEPENDS); + runtime_set(rt, "SPM_META_PREFIX_BIN", SPM_META_PREFIX_BIN); + runtime_set(rt, "SPM_META_PREFIX_TEXT", SPM_META_PREFIX_TEXT); + runtime_set(rt, "SPM_META_DESCRIPTOR", SPM_META_DESCRIPTOR); + runtime_set(rt, "SPM_META_FILELIST", SPM_META_FILELIST); + runtime_set(rt, "SPM_META_PREFIX_PLACEHOLDER", SPM_META_PREFIX_PLACEHOLDER); + + runtime_set(rt, "PATH", "$SPM_BIN:$PATH"); + runtime_set(rt, "MANPATH", "$SPM_MAN:$MANPATH"); + runtime_set(rt, "PKG_CONFIG_PATH", "$SPM_PKGCONFIG:$PKG_CONFIG_PATH"); + runtime_set(rt, "ACLOCAL_PATH", "${SPM_DATA}/aclocal"); + + char *spm_ccpath = join((char *[]) {fs->bindir, "gcc"}, DIRSEPS); + if (exists(spm_ccpath) == 0) { + runtime_set(rt, "CC", "$SPM_BIN/gcc"); + } + + runtime_set(rt, "CFLAGS", "-I$SPM_INCLUDE $CFLAGS"); + runtime_set(rt, "LDFLAGS", "-Wl,-rpath=$SPM_LIB:$SPM_LIB64 -L$SPM_LIB -L$SPM_LIB64 $LDFLAGS"); + runtime_export(rt, NULL); + runtime_free(rt); + + free(spm_pkgconfigdir); + free(spm_ccpath); + spm_hierarchy_free(fs); + return 0; +} + +/** + * + */ +void mirror_clone_interface_usage(void) { + printf("usage: mirror_clone {url} {output_dir}\n"); +} + +/** + * Mirror packages referenced by a remote manifest + * @param argc + * @param argv + * @return value of `manifest_write` + */ +int mirror_clone_interface(int argc, char **argv) { + if (argc < 3) { + mirror_clone_interface_usage(); + return -1; + } + char *url = argv[1]; + char *path = argv[2]; + + Manifest *manifest = manifest_read(url); + if (manifest == NULL) { + return -2; + } + + mirror_clone(manifest, path); + manifest_free(manifest); + return 0; +} +/** + * + */ +void rpath_set_interface_usage(void) { + printf("usage: rpath_set {file} {rpath}\n"); +} + +/** + * Set a RPATH from the CLI + * @param argc + * @param argv + * @return return value of `rpath_set` + */ +int rpath_set_interface(int argc, char **argv) { + if (argc < 3) { + rpath_set_interface_usage(); + return -1; + } + char *filename = argv[1]; + char *rpath = argv[2]; + int result = rpath_set(filename, rpath); + if (result < 0) { + fprintf(SYSERROR); + } + return result; +} + +/** + * + */ +void rpath_autoset_interface_usage(void) { + printf("usage: rpath_autoset {file} {topdir}\n"); +} + +/** + * Set a RPATH automatically from the CLI + * @param argc + * @param argv + * @return return value of `rpath_autoset` + */ +int rpath_autoset_interface(int argc, char **argv) { + if (argc < 3) { + rpath_autoset_interface_usage(); + return -1; + } + char *filename = argv[1]; + const char *topdir = argv[2]; + + if (exists(filename) != 0) { + perror(filename); + return -1; + } + + if (exists(topdir) != 0) { + perror(topdir); + return -1; + } + + FSTree *libs = rpath_libraries_available(topdir); + int result = rpath_autoset(filename, libs); + + if (result < 0) { + fprintf(SYSERROR); + } + + return result; +} + +/** + * Dump the default package extension for SPM archives to `stdout` + * @return + */ +int get_package_ext_interface(void) { + puts(SPM_PACKAGE_EXTENSION); + return 0; +} + +/** + * Dump the system arch/platform (i.e. Linux/x86_64) + * @return + */ +int get_sys_target_interface(void) { + puts(SPM_GLOBAL.repo_target); + return 0; +} +/** + * Execute builtin runtime check. + * + * On failure this function will EXIT the program with a non-zero value + * + * @return + */ +int check_runtime_environment_interface(void) { + check_runtime_environment(); + return 0; +} + +/** + * Show a listing of valid internal commands + */ +void internal_command_list(void) { + printf("possible commands:\n"); + for (size_t i = 0; internal_commands[i] != NULL; i += 2) { + printf(" %-20s - %-20s\n", internal_commands[i], internal_commands[i + 1]); + } +} + +/** + * Execute an internal command + * @param argc + * @param argv + * @return success=0, failure=1, error=-1 + */ +int internal_cmd(int argc, char **argv) { + int command_valid = 0; + char *command = argv[1]; + if (argc < 2) { + internal_command_list(); + return 1; + } + + for (int i = 0; internal_commands[i] != NULL; i++) { + if (strcmp(internal_commands[i], command) == 0) { + command_valid = 1; + break; + } + } + + if (!command_valid) { + fprintf(stderr, "error: '%s' is not a valid command\n", command); + internal_command_list(); + return 1; + } + + // Strip the first argument (this level) before passing it along to the interface + int arg_count = argc - 1; + char **arg_array = &argv[1]; + + if (strcmp(command, "mkprefixbin") == 0 || strcmp(command, "mkprefixtext") == 0) { + return mkprefix_interface(arg_count, arg_array); + } + else if (strcmp(command, "mkmanifest") == 0) { + return mkmanifest_interface(arg_count, arg_array); + } + else if (strcmp(command, "mkruntime") == 0) { + return mkruntime_interface(arg_count, arg_array); + } + else if (strcmp(command, "mirror_clone") == 0) { + return mirror_clone_interface(arg_count, arg_array); + } + else if (strcmp(command, "rpath_set") == 0) { + return rpath_set_interface(arg_count, arg_array); + } + else if (strcmp(command, "rpath_autoset") == 0) { + return rpath_autoset_interface(arg_count, arg_array); + } + else if (strcmp(command, "get_package_ext") == 0) { + return get_package_ext_interface(); + } + else if (strcmp(command, "get_sys_target") == 0) { + return get_sys_target_interface(); + } + else if (strcmp(command, "check_rt_env") == 0) { + return check_runtime_environment_interface(); + } + return 0; +} diff --git a/lib/manifest.c b/lib/manifest.c new file mode 100644 index 0000000..1b2b600 --- /dev/null +++ b/lib/manifest.c @@ -0,0 +1,668 @@ +/** + * @file manifest.c + */ +#include "spm.h" +#include <fnmatch.h> +#include "url.h" + +/** + * Compare `ManifestPackage` packages (lazily) + * @param a + * @param b + * @return 0 = same, !0 = different + */ +int manifest_package_cmp(ManifestPackage *a, ManifestPackage *b) { + int result = 0; + if (a == NULL || b == NULL) { + return -1; + } + result += strcmp(a->origin, b->origin); + result += strcmp(a->archive, b->archive); + result += strcmp(a->checksum_sha256, b->checksum_sha256); + return result; +} + +void manifest_package_separator_swap(char **name) { + // Replace unwanted separators in the package name with placeholder to prevent splitting on the wrong one + int delim_count = num_chars((*name), SPM_PACKAGE_MEMBER_SEPARATOR) - SPM_PACKAGE_MIN_DELIM; + + if (delim_count < 0) { + return; + } + + for (size_t t = 0; t < strlen((*name)); t++) { + if (delim_count == 0) break; + if ((*name)[t] == SPM_PACKAGE_MEMBER_SEPARATOR) { + (*name)[t] = SPM_PACKAGE_MEMBER_SEPARATOR_PLACEHOLD; + delim_count--; + } + } +} + +void manifest_package_separator_restore(char **name) { + char separator[2]; + char placeholder[2]; + snprintf(separator, sizeof(separator), "%c", SPM_PACKAGE_MEMBER_SEPARATOR); + snprintf(placeholder, sizeof(placeholder), "%c", SPM_PACKAGE_MEMBER_SEPARATOR_PLACEHOLD); + + replace_text((*name), placeholder, separator); +} + +/** + * Generate a `Manifest` of package data + * @param package_dir a directory containing SPM packages + * @return `Manifest` + */ +Manifest *manifest_from(const char *package_dir) { + char *package_filter[] = {SPM_PACKAGE_EXTENSION, NULL}; // We only want packages + FSTree *fsdata = NULL; + fsdata = fstree(package_dir, package_filter, SPM_FSTREE_FLT_ENDSWITH); + + Manifest *info = (Manifest *)calloc(1, sizeof(Manifest)); + info->records = fsdata->files_length; + info->packages = (ManifestPackage **) calloc(info->records + 1, sizeof(ManifestPackage *)); + if (info->packages == NULL) { + perror("Failed to allocate package array"); + fprintf(SYSERROR); + free(info); + fstree_free(fsdata); + return NULL; + } + + if (SPM_GLOBAL.verbose) { + printf("Initializing package manifest:\n"); + } + strncpy(info->origin, package_dir, SPM_PACKAGE_MEMBER_ORIGIN_SIZE); + + + char *tmpdir = spm_mkdtemp("spm_manifest_from", NULL); + if (!tmpdir) { + perror("failed to create temporary directory"); + fprintf(SYSERROR); + free(info); + fstree_free(fsdata); + return NULL; + } + + for (size_t i = 0; i < fsdata->files_length; i++) { + float percent = (((float)i + 1) / fsdata->files_length) * 100; + + if (SPM_GLOBAL.verbose) { + printf("[%3.0f%%] %s\n", percent, basename(fsdata->files[i])); + } + + // Initialize package record + info->packages[i] = (ManifestPackage *) calloc(1, sizeof(ManifestPackage)); + if (info->packages[i] == NULL) { + perror("Failed to allocate package record"); + fprintf(SYSERROR); + fstree_free(fsdata); + free(info); + rmdirs(tmpdir); + return NULL; + } + + // Swap extra package separators with a bogus character + manifest_package_separator_swap(&fsdata->files[i]); + + // Split the package name into parts + char psep[2]; + snprintf(psep, sizeof(psep), "%c", SPM_PACKAGE_MEMBER_SEPARATOR); + char **parts = split(fsdata->files[i], psep); + + // Restore package separator + manifest_package_separator_restore(&parts[0]); + manifest_package_separator_restore(&fsdata->files[i]); + + // Populate `ManifestPackage` record + info->packages[i]->size = (size_t) get_file_size(fsdata->files[i]); + strncpy(info->packages[i]->origin, info->origin, SPM_PACKAGE_MEMBER_ORIGIN_SIZE); + strncpy(info->packages[i]->archive, basename(fsdata->files[i]), SPM_PACKAGE_MEMBER_SIZE); + strncpy(info->packages[i]->name, basename(parts[0]), SPM_PACKAGE_MEMBER_SIZE); + strncpy(info->packages[i]->version, parts[1], SPM_PACKAGE_MEMBER_SIZE); + strncpy(info->packages[i]->revision, parts[2], SPM_PACKAGE_MEMBER_SIZE); + strdelsuffix(info->packages[i]->revision, SPM_PACKAGE_EXTENSION); + + // Read package requirement specs + char *archive = join((char *[]) {info->origin, info->packages[i]->archive, NULL}, DIRSEPS); + if (tar_extract_file(archive, SPM_META_DEPENDS, tmpdir) != 0) { + // TODO: at this point is the package is invalid? .SPM_DEPENDS should be there... + fprintf(stderr, "extraction failure: %s\n", archive); + rmdirs(tmpdir); + exit(1); + } + char *depfile = join((char *[]) {tmpdir, SPM_META_DEPENDS, NULL}, DIRSEPS); + info->packages[i]->requirements = file_readlines(depfile, 0, 0, NULL); + + // Record count of requirement specs + if (info->packages[i]->requirements != NULL) { + for (size_t rec = 0; info->packages[i]->requirements[rec] != NULL; rec++) { + strip(info->packages[i]->requirements[rec]); + info->packages[i]->requirements_records++; + } + } + + unlink(depfile); + free(depfile); + free(archive); + split_free(parts); + } + + fstree_free(fsdata); + rmdirs(tmpdir); + return info; +} + +/** + * Free a `Manifest` structure + * @param info `Manifest` + */ +void manifest_free(Manifest *info) { + for (size_t i = 0; i < info->records; i++) { + manifest_package_free(info->packages[i]); + } + free(info); +} + +/** + * Free a `ManifestPackage` structure + * @param info `ManifestPackage` + */ +void manifest_package_free(ManifestPackage *info) { + for (size_t i = 0; i < info->requirements_records; i++) { + free(info->requirements[i]); + } + free(info->requirements); + free(info); +} + +/** + * Write a `Manifest` to the configuration directory + * @param info + * @param pkgdir + * @return + */ +int manifest_write(Manifest *info, const char *pkgdir) { + char *reqs = NULL; + char path[PATH_MAX]; + char path_manifest[PATH_MAX]; + + memset(path, '\0', sizeof(path)); + memset(path_manifest, '\0', sizeof(path)); + + strcpy(path, pkgdir); + + // Append the repo target if its missing + if (strstr(path, SPM_GLOBAL.repo_target) == NULL) { + strcat(path, DIRSEPS); + strcat(path, SPM_GLOBAL.repo_target); + } + strcpy(path_manifest, path); + + // Append the manifest filename if its missing + if (!endswith(path_manifest, SPM_MANIFEST_FILENAME)) { + strcat(path_manifest, DIRSEPS); + strcat(path_manifest, SPM_MANIFEST_FILENAME); + } + + FILE *fp = fopen(path_manifest, "w+"); + if (fp == NULL) { + perror(path_manifest); + fprintf(SYSERROR); + return -1; + } +#ifdef _DEBUG + if (SPM_GLOBAL.verbose) { + for (size_t i = 0; i < info->records; i++) { + printf("%-20s: %s\n" + "%-20s: %zu\n" + "%-20s: %s\n" + "%-20s: %s\n" + "%-20s: %s\n" + "%-20s: %zu\n", + "archive", info->packages[i]->archive, + "size", info->packages[i]->size, + "name", info->packages[i]->name, + "version", info->packages[i]->version, + "revision", info->packages[i]->revision, + "requirements_records", info->packages[i]->requirements_records + ); + reqs = join(info->packages[i]->requirements, ", "); + printf("%-20s: %s\n", "requirements", reqs ? reqs : "NONE"); + free(reqs); + printf("\n"); + } + } +#endif + + if (SPM_GLOBAL.verbose) { + printf("Generating manifest file: %s\n", path_manifest); + } + fprintf(fp, "%s\n", SPM_MANIFEST_HEADER); + char data[BUFSIZ]; + for (size_t i = 0; i < info->records; i++) { + // write CSV-like manifest + memset(data, '\0', BUFSIZ); + char *dptr = data; + float percent = (((float)i + 1) / info->records) * 100; + if (SPM_GLOBAL.verbose) { + printf("[%3.0f%%] %s\n", percent, info->packages[i]->archive); + } + reqs = join(info->packages[i]->requirements, ","); + char *archive = join((char *[]) {path, info->packages[i]->archive, NULL}, DIRSEPS); + char *checksum_sha256 = sha256sum(archive); + + sprintf(dptr, "%s|" // archive + "%zu|" // size + "%s|" // name + "%s|" // version + "%s|" // revision + "%zu|" // requirements_records + "%s|" // requirements + "%s" // checksum_md5 + , info->packages[i]->archive, + info->packages[i]->size, + info->packages[i]->name, + info->packages[i]->version, + info->packages[i]->revision, + info->packages[i]->requirements_records, + reqs ? reqs : SPM_MANIFEST_NODATA, + checksum_sha256 ? checksum_sha256 : SPM_MANIFEST_NODATA); + fprintf(fp, "%s\n", dptr); + free(reqs); + free(archive); + if (checksum_sha256 != NULL) + free(checksum_sha256); + } + fclose(fp); + return 0; +} + +/** + * + * @param url + * @param dest + * @return + */ +int fetch(const char *url, const char *dest) { + URL_FILE *handle = NULL; + FILE *outf = NULL; + size_t chunk_size = 0xffff; + size_t nread = 0; + char *buffer = calloc(chunk_size + 1, sizeof(char)); + if (!buffer) { + perror("fetch buffer too big"); + return -1; + } + + handle = url_fopen(url, "r"); + if(!handle) { + fprintf(stderr, "couldn't url_fopen() %s\n", url); + return 2; + } + + outf = fopen(dest, "wb+"); + if(!outf) { + perror("couldn't open fread output file\n"); + return 1; + } + + do { + nread = url_fread(buffer, 1, chunk_size, handle); + if (handle->http_status >= 400) { + free(buffer); + fclose(outf); + if (exists(dest) == 0) { + unlink(dest); + } + + long http_status = handle->http_status; + url_fclose(handle); + return http_status; + } + fwrite(buffer, 1, nread, outf); + } while (nread); + + free(buffer); + fclose(outf); + url_fclose(handle); + return 0; +} + +int manifest_validate(void) { + size_t line_count; + int problems; + char data[BUFSIZ]; + FILE *fp; + + if (exists(SPM_GLOBAL.package_manifest) != 0) { + return -1; + } + + if ((fp = fopen(SPM_GLOBAL.package_manifest, "r")) == NULL) { + perror(SPM_GLOBAL.package_manifest); + return -2; + } + + line_count = 0; + problems = 0; + while (fgets(data, BUFSIZ, fp) != NULL) { + int separators; + if (line_count == 0) { + if (strncmp(data, SPM_MANIFEST_HEADER, strlen(SPM_MANIFEST_HEADER)) != 0) { + fprintf(stderr, "Invalid manifest header: %s (expecting '%s')\n", strip(data), SPM_MANIFEST_HEADER); + problems++; + line_count++; + } + } + else if ((separators = num_chars(data, SPM_MANIFEST_SEPARATOR)) != SPM_MANIFEST_SEPARATOR_MAX) { + fprintf(stderr, "Invalid manifest record on line %zu: %s (expecting %d separators, found %d)\n", line_count, strip(data), SPM_MANIFEST_SEPARATOR_MAX, separators); + problems++; + } + line_count++; + } + return problems; +} + +/** + * Read the package manifest stored in the configuration directory + * @return `Manifest` structure + */ +Manifest *manifest_read(char *file_or_url) { + FILE *fp = NULL; + char *filename = SPM_MANIFEST_FILENAME; + char *tmpdir = NULL; + char path[PATH_MAX]; + char *pathptr = path; + memset(path, '\0', PATH_MAX); + + // When file_or_url is NULL we want to use the global manifest + if (file_or_url == NULL) { + // TODO: move this out + strcpy(path, SPM_GLOBAL.package_dir); + } + else { + tmpdir = spm_mkdtemp("spm_manifest_read_XXXXXX", SPM_GLOBAL.repo_target); + if (exists(tmpdir) != 0) { + fprintf(stderr, "Failed to create temporary storage directory\n"); + fprintf(SYSERROR); + return NULL; + } + + snprintf(pathptr, PATH_MAX - 1, "%s%s%s%s%s", tmpdir, DIRSEPS, SPM_GLOBAL.repo_target, DIRSEPS, filename); + } + + const char *target_is; + if (strstr(file_or_url, SPM_GLOBAL.repo_target) != NULL) { + target_is = ""; + } + else { + target_is = SPM_GLOBAL.repo_target; + } + + char *remote_manifest = join_ex(DIRSEPS, file_or_url, target_is, filename, NULL); + + if (exists(pathptr) != 0) { + // TODO: Move this out + int fetch_status = fetch(remote_manifest, pathptr); + if (fetch_status >= 400) { + fprintf(stderr, "HTTP %d: %s: %s\n", fetch_status, http_response_str(fetch_status), remote_manifest); + free(remote_manifest); + return NULL; + } + else if (fetch_status == 1 || fetch_status < 0) { + free(remote_manifest); + return NULL; + } + } + + int valid = 0; + size_t total_records = 0; + char data[BUFSIZ]; + char *dptr = data; + memset(dptr, '\0', BUFSIZ); + + fp = fopen(pathptr, "r+"); + if (!fp) { + perror(filename); + fprintf(SYSERROR); + return NULL; + } + + while (fgets(dptr, BUFSIZ, fp) != NULL) { + total_records++; + } + total_records--; // header does not count + rewind(fp); + + Manifest *info = (Manifest *)calloc(1, sizeof(Manifest)); + info->packages = (ManifestPackage **)calloc(total_records + 1, sizeof(ManifestPackage *)); + + // Record manifest's origin + memset(info->origin, '\0', SPM_PACKAGE_MEMBER_ORIGIN_SIZE); + if (remote_manifest != NULL) { + strcpy(info->origin, remote_manifest); + } + else { + strcpy(info->origin, path); + } + free(remote_manifest); + + // Check validity of the manifest's formatting and field length + if ((valid = manifest_validate()) != 0) { + return NULL; + } + + // Begin parsing the manifest + char separator = SPM_MANIFEST_SEPARATOR; + size_t i = 0; + + // Consume header + if (fgets(dptr, BUFSIZ, fp) == NULL) { + // file is probably empty + return NULL; + } + + info->records = total_records; + while (fgets(dptr, BUFSIZ, fp) != NULL) { + dptr = strip(dptr); + char *garbage; + char **parts = split(dptr, &separator); + char *_origin = NULL; + if (file_or_url != NULL) { + _origin = strdup(file_or_url); + } + else { + _origin = dirname(path); + } + + info->packages[i] = (ManifestPackage *)calloc(1, sizeof(ManifestPackage)); + + strncpy(info->packages[i]->origin, _origin, SPM_PACKAGE_MEMBER_ORIGIN_SIZE); + free(_origin); + + strncpy(info->packages[i]->archive, parts[0], SPM_PACKAGE_MEMBER_SIZE); + info->packages[i]->size = strtoul(parts[1], &garbage, 10); + strncpy(info->packages[i]->name, parts[2], SPM_PACKAGE_MEMBER_SIZE); + strncpy(info->packages[i]->version, parts[3], SPM_PACKAGE_MEMBER_SIZE); + strncpy(info->packages[i]->revision, parts[4], SPM_PACKAGE_MEMBER_SIZE); + info->packages[i]->requirements_records = (size_t) atoi(parts[5]); + + info->packages[i]->requirements = NULL; + if (strncmp(parts[6], SPM_MANIFEST_NODATA, strlen(SPM_MANIFEST_NODATA)) != 0) { + info->packages[i]->requirements = split(parts[6], ","); + + } + if (strncmp(parts[7], SPM_MANIFEST_NODATA, strlen(SPM_MANIFEST_NODATA)) != 0) { + memset(info->packages[i]->checksum_sha256, '\0', SHA256_DIGEST_STRING_LENGTH); + strcpy(info->packages[i]->checksum_sha256, parts[7]); + + } + + split_free(parts); + i++; + } + + if (tmpdir != NULL) { + rmdirs(tmpdir); + free(tmpdir); + } + fclose(fp); + return info; +} + +/** + * Find a package in a `Manifest` + * @param info `Manifest` + * @param _package package name + * @return found=`ManifestPackage`, not found=NULL + */ +ManifestPackage *manifest_search(Manifest *info, const char *_package) { + ManifestPackage *match = NULL; + char package[PATH_MAX]; + + memset(package, '\0', PATH_MAX); + strncpy(package, _package, PATH_MAX); + + if ((match = find_by_strspec(info, package)) != NULL) { + return match; + } + return NULL; +} + +/** + * Duplicate a `ManifestPackage` + * @param manifest + * @return `ManifestPackage` + */ +ManifestPackage *manifest_package_copy(ManifestPackage *manifest) { + if (manifest == NULL) { + return NULL; + } + + ManifestPackage *result = calloc(1, sizeof(ManifestPackage)); + memcpy(result, manifest, sizeof(ManifestPackage)); + + if (manifest->requirements_records > 0) { + result->requirements = (char **)calloc(manifest->requirements_records, sizeof(char *)); + for (size_t i = 0; i < manifest->requirements_records; i++) { + result->requirements[i] = strdup(manifest->requirements[i]); + } + } + + return result; +} + +/** + * + * @param ManifestList `pManifestList` + */ +void manifestlist_free(ManifestList *pManifestList) { + for (size_t i = 0; i < pManifestList->num_inuse; i++) { + manifest_free(pManifestList->data[i]); + } + free(pManifestList->data); + free(pManifestList); +} + +/** + * Append a value to the list + * @param ManifestList `pManifestList` + * @param str + */ +void manifestlist_append(ManifestList *pManifestList, char *path) { + Manifest *manifest = manifest_read(path); + if (manifest == NULL) { + fprintf(stderr, "Failed to create manifest in memory\n"); + fprintf(SYSERROR); + exit(1); + } + + Manifest **tmp = realloc(pManifestList->data, (pManifestList->num_alloc + 1) * sizeof(Manifest *)); + if (tmp == NULL) { + manifestlist_free(pManifestList); + perror("failed to append to array"); + exit(1); + } + pManifestList->data = tmp; + pManifestList->data[pManifestList->num_inuse] = manifest; + pManifestList->num_inuse++; + pManifestList->num_alloc++; +} + +ManifestPackage *manifestlist_search(ManifestList *pManifestList, const char *_package) { + ManifestPackage *found[255] = {0,}; + ManifestPackage *result = NULL; + ssize_t offset = -1; + + for (size_t i = 0; i < manifestlist_count(pManifestList); i++) { + Manifest *item = manifestlist_item(pManifestList, i); + result = manifest_search(item, _package); + if (result != NULL) { + offset++; + found[offset] = result; + } + } + + if (offset < 0) { + return NULL; + } + return found[offset]; +} + +/** + * Get the count of values stored in a `pManifestList` + * @param ManifestList + * @return + */ +size_t manifestlist_count(ManifestList *pManifestList) { + return pManifestList->num_inuse; +} + +/** + * Set value at index + * @param ManifestList + * @param value string + * @return + */ +void manifestlist_set(ManifestList *pManifestList, size_t index, Manifest *value) { + Manifest *item = NULL; + if (index > manifestlist_count(pManifestList)) { + return; + } + if ((item = manifestlist_item(pManifestList, index)) == NULL) { + return; + } + memcpy(pManifestList->data[index], value, sizeof(Manifest)); +} + +/** + * Retrieve data from a `pManifestList` + * @param ManifestList + * @param index + * @return string + */ +Manifest *manifestlist_item(ManifestList *pManifestList, size_t index) { + if (index > manifestlist_count(pManifestList)) { + return NULL; + } + return pManifestList->data[index]; +} + +/** + * Initialize an empty `pManifestList` + * @return `pManifestList` + */ +ManifestList *manifestlist_init() { + ManifestList *pManifestList = calloc(1, sizeof(ManifestList)); + if (pManifestList == NULL) { + perror("failed to allocate array"); + exit(errno); + } + pManifestList->num_inuse = 0; + pManifestList->num_alloc = 1; + pManifestList->data = calloc(pManifestList->num_alloc, sizeof(char *)); + return pManifestList; +} + + diff --git a/lib/metadata.c b/lib/metadata.c new file mode 100644 index 0000000..6861740 --- /dev/null +++ b/lib/metadata.c @@ -0,0 +1,166 @@ +#include "spm.h" + +extern const char *METADATA_FILES[]; + +static int verify_filelist(size_t lineno, char **a) { + if (lineno == 0) { + if (strncmp((*a), SPM_PACKAGE_HEADER_FILELIST, strlen(SPM_PACKAGE_HEADER_FILELIST)) != 0) { + fprintf(stderr, "invalid or missing header: line %zu: %s (expected: '%s')\n", + lineno, (*a), SPM_PACKAGE_HEADER_FILELIST); + return 1; + } + } + return -1; +} + +#pragma GCC diagnostic ignored "-Wunused-parameter" +static int verify_depends(size_t lineno, char **a) { + return -1; +} + +static int verify_descriptor(size_t lineno, char **a) { + if (lineno == 0) { + if (strncmp((*a), SPM_PACKAGE_HEADER_DESCRIPTOR, strlen(SPM_PACKAGE_HEADER_DESCRIPTOR)) != 0) { + fprintf(stderr, "invalid or missing header: line %zu: %s (expected: '%s')\n", + lineno, (*a), SPM_PACKAGE_HEADER_DESCRIPTOR); + return 1; + } + } + return -1; +} + +static int verify_prefix(size_t lineno, char **a) { + size_t parity = lineno % 2; + if (parity == 0 && *(*a) == '#') { + return 0; + } + else { + return 1; + } +} + +#pragma GCC diagnostic ignored "-Wunused-parameter" +static int verify_no_op(size_t lineno, char **a) { + return -1; +} + +#pragma GCC diagnostic ignored "-Wunused-parameter" +static int reader_metadata(size_t lineno, char **line) { + (*line) = strip((*line)); + if (isempty((*line))) { + return 1; // indicate "continue" + } + return 0; // indicate "ok" +} + +/** + * Detect the type of metadata based on file name and execute the appropriate function against each line + * in the file. Verification can be disabled by passing a non-zero value as the second argument. + * @param filename + * @param no_verify SPM_METADATA_VERIFY or SPM_METADATA_NO_VERIFY + * @return array of strings (line endings removed) + */ +char **spm_metadata_read(const char *_filename, int no_verify) { + char *filename = strdup(_filename); + char **data = NULL; + char **result = NULL; + size_t start = 0; + ReaderFn *func_verify; + + // Guard + if (file_is_metadata(filename) == 0) { + free(filename); + return NULL; + } + + // Setup function pointer and data starting offsets (if any) + if (strcmp(basename(filename), SPM_META_FILELIST) == 0) { + func_verify = verify_filelist; + start = 1; + } else if (strcmp(basename(filename), SPM_META_DESCRIPTOR) == 0) { + func_verify = verify_descriptor; + start = 1; + } else if (strcmp(basename(filename), SPM_META_DEPENDS) == 0) { + func_verify = verify_depends; + } else if (strcmp(basename(filename), SPM_META_PREFIX_BIN) == 0) { + func_verify = verify_prefix; + } else if (strcmp(basename(filename), SPM_META_PREFIX_TEXT) == 0) { + func_verify = verify_prefix; + } else { + func_verify = verify_no_op; + } + + // Get file contents + data = file_readlines(filename, 0, 0, reader_metadata); + + // Strip newlines and whitespace + for (size_t i = 0; data[i] != NULL; i++) { + data[i] = strip(data[i]); + } + + // Perform verification + if (no_verify == 0) { + for (size_t i = 0; data[i] != NULL; i++) { + int status = func_verify(i, &data[i]); + if (status > 0) { + fprintf(stderr, "%s: file verification failed\n", filename); + free(filename); + return NULL; + } else if (status < 0) { + // NOT AN ERROR + // a negative value indicates the verification function has processed enough information + // so we can gracefully + break; + } + } + } + + // If there was a header, duplicate the array from the start of the data + // Otherwise return the array + if (start > 0) { + result = strdup_array(&data[start]); + for (size_t i = 0; data[i] != NULL; i++) { + free(data[i]); + } + free(data); + } else { + result = data; + } + + free(filename); + return result; +} + +/** + * SPM packages contain metadata files that are not useful post-install and would amount to a lot of clutter. + * This function removes these data files from a directory tree + * @param _path + * @return success=0, error=-1 + */ +int spm_metadata_remove(const char *_path) { + if (exists(_path) != 0) { + perror(_path); + fprintf(SYSERROR); + return -1; + } + + for (int i = 0; METADATA_FILES[i] != NULL; i++) { + char path[PATH_MAX]; + sprintf(path, "%s%c%s", _path, DIRSEP, METADATA_FILES[i]); + if (exists(path) != 0) { + continue; + } + if (unlink(path) < 0) { + perror(path); + fprintf(SYSERROR); + return -1; + } + } + return 0; +} + +ConfigItem **spm_descriptor_read(const char *filename) { + ConfigItem **result = config_read(filename); + return result; +} + diff --git a/lib/mime.c b/lib/mime.c new file mode 100644 index 0000000..9e4bdce --- /dev/null +++ b/lib/mime.c @@ -0,0 +1,156 @@ +/** + * @file mime.c + */ +#include "spm.h" +#include <fnmatch.h> + +/** + * Execute OS `file` command + * @param _filename path to file + * @return Process structure + */ +Process *file_command(const char *_filename) { + char *filename = strdup(_filename); + Process *proc_info = NULL; + char sh_cmd[PATH_MAX]; + sh_cmd[0] = '\0'; +#ifdef __APPLE__ + const char *fmt_cmd = "file -I \"%s\" 2>&1"; +#else // GNU + const char *fmt_cmd = "file -i \"%s\" 2>&1"; +#endif + const char *fail_pattern = ": cannot open"; + + strchrdel(filename, SHELL_INVALID); + sprintf(sh_cmd, fmt_cmd, filename); + shell(&proc_info, SHELL_OUTPUT, sh_cmd); + + // POSIXly ridiculous. Return non-zero when a file can't be found, or isn't accessible + if (strstr(proc_info->output, fail_pattern) != NULL) { + proc_info->returncode = 1; + } + free(filename); + return proc_info; +} + +/** + * Execute the `file` command, parse its output, and return the data in a `Mime` structure + * @param filename path to file + * @return Mime structure + */ +Mime *file_mimetype(const char *filename) { + char **output = NULL; + char **parts = NULL; + Mime *type = NULL; + Process *proc = file_command(filename); + + if (proc->returncode != 0) { + fprintf(stderr, "%s\n", proc->output); + fprintf(stderr, "file command returned: %d\n", proc->returncode); + fprintf(SYSERROR); + shell_free(proc); + return NULL; + } + output = split(proc->output, ":"); + if (!output || output[1] == NULL) { + shell_free(proc); + return NULL; + } + parts = split(output[1], ";"); + if (!parts || !parts[0] || !parts[1]) { + shell_free(proc); + return NULL; + } + + char *what = strdup(parts[0]); + what = lstrip(what); + + char *charset = strdup(strchr(parts[1], '=') + 1); + charset = lstrip(charset); + charset[strlen(charset) - 1] = '\0'; + + char *origin = realpath(filename, NULL); + + type = (Mime *)calloc(1, sizeof(Mime)); + type->origin = origin; + type->type = what; + type->charset = charset; + + split_free(output); + split_free(parts); + shell_free(proc); + return type; +} + +/** + * Free a `Mime` structure + * @param m + */ +void mime_free(Mime *m) { + if (m != NULL) { + free(m->origin); + free(m->type); + free(m->charset); + free(m); + } +} + +/** + * Determine if a file is a text file + * @param filename + * @return yes=1, no=0 + */ +int file_is_text(const char *filename) { + int result = 0; + char *path = normpath(filename); + Mime *type = file_mimetype(path); + if (type == NULL) { + fprintf(stderr, "type detection failed: %s\n", filename); + return -1; + } + if (startswith(type->type, "text/")) { + result = 1; + } + free(path); + mime_free(type); + return result; +} + +/** + * Determine if a file is a binary data file + * @param filename + * @return yes=1, no=0 + */ +int file_is_binary(const char *filename) { + int result = 0; + char *path = normpath(filename); + Mime *type = file_mimetype(path); + if (type == NULL) { + fprintf(stderr, "type detection failed: %s\n", filename); + return -1; + } + if (startswith(type->type, "application/") && strcmp(type->charset, "binary") == 0) { + result = 1; + } + free(path); + mime_free(type); + return result; +} + +int file_is_binexec(const char *filename) { + int result = 0; + char *path = normpath(filename); + Mime *type = file_mimetype(path); + if (type == NULL) { + fprintf(stderr, "type detection failed: %s\n", filename); + return -1; + } + // file-5.38: changed mime name associated with executables + // TODO: implement compatibility function to return the correct search pattern + if (fnmatch("application/x-[pic|pie|ex|sh]*", type->type, FNM_PATHNAME) != FNM_NOMATCH && strcmp(type->charset, "binary") == 0) { + result = 1; + } + free(path); + mime_free(type); + return result; +} diff --git a/lib/mirrors.c b/lib/mirrors.c new file mode 100644 index 0000000..cad3f6b --- /dev/null +++ b/lib/mirrors.c @@ -0,0 +1,180 @@ +#include "spm.h" +#include "url.h" + +char **file_readlines(const char *filename, size_t start, size_t limit, ReaderFn *readerFn) { + FILE *fp = NULL; + char **result = NULL; + char *buffer = NULL; + size_t lines = 0; + + if ((fp = fopen(filename, "r")) == NULL) { + perror(filename); + fprintf(SYSERROR); + return NULL; + } + + // Allocate buffer + if ((buffer = calloc(BUFSIZ + 1, sizeof(char))) == NULL) { + perror("line buffer"); + fprintf(SYSERROR); + fclose(fp); + return NULL; + } + + // count number the of lines in the file + while ((fgets(buffer, BUFSIZ, fp)) != NULL) { + lines++; + } + + if (!lines) { + free(buffer); + fclose(fp); + return NULL; + } + + rewind(fp); + + // Handle invalid start offset + if (start > lines) { + start = 0; + } + + // Adjust line count when start offset is non-zero + if (start != 0 && start < lines) { + lines -= start; + } + + + // Handle minimum and maximum limits + if (limit == 0 || limit > lines) { + limit = lines; + } + + // Populate results array + result = calloc(lines + 1, sizeof(char *)); + for (size_t i = start; i < limit; i++) { + if (i < start) { + continue; + } + + if (fgets(buffer, BUFSIZ, fp) == NULL) { + break; + } + + if (readerFn != NULL) { + int status = readerFn(i - start, &buffer); + // A status greater than zero indicates we should ignore this line entirely and "continue" + // A status less than zero indicates we should "break" + // A zero status proceeds normally + if (status > 0) { + i--; + continue; + } else if (status < 0) { + break; + } + } + result[i - start] = strdup(buffer); + } + + free(buffer); + fclose(fp); + return result; +} + +/** + * + * @param filename + * @return + */ +char **mirror_list(const char *filename) { + char **mirrors = NULL; + char **result = NULL; + size_t count; + + // The configuration file isn't critical so if it isn't available, no big deal + if (exists(filename) != 0) { + return NULL; + } + + mirrors = file_readlines(filename, 0, 0, NULL); + if (mirrors == NULL) { + return NULL; + } + + for (count = 0; mirrors[count] != NULL; count++); + if (!count) { + return NULL; + } + + result = calloc(count + 1, sizeof(char **)); + for (size_t i = 0; mirrors[i] != NULL; i++) { + if (startswith(mirrors[i], "#") || isempty(mirrors[i])) { + continue; + } + result[i] = join((char *[]) {mirrors[i], SPM_GLOBAL.repo_target, NULL}, DIRSEPS); + free(mirrors[i]); + } + free(mirrors); + return result; +} + +void mirror_list_free(char **m) { + if (m == NULL) { + return; + } + for (size_t i = 0; m[i] != NULL; i++) { + free(m[i]); + } + free(m); +} + +void mirror_clone(Manifest *info, char *_dest) { + char *dest = NULL; + if (endswith(_dest, SPM_GLOBAL.repo_target) != 0) { + dest = strdup(_dest); + } + else { + dest = join((char *[]) {_dest, SPM_GLOBAL.repo_target, NULL}, DIRSEPS); + } + + if (exists(dest) != 0 && mkdirs(dest, 0755) != 0) { + perror("Unable to create mirror directory"); + fprintf(SYSERROR); + exit(1); + } + + printf("Remote: %s\n", info->origin); + printf("Local: %s\n", dest); + + for (size_t i = 0; i < info->records; i++) { + long response = 0; + char *archive = join((char *[]) {info->packages[i]->origin, SPM_GLOBAL.repo_target, info->packages[i]->archive, NULL}, DIRSEPS); + char *path = join((char *[]) {dest, info->packages[i]->archive, NULL}, DIRSEPS); + if (exists(path) == 0) { + char *checksum = sha256sum(path); + if (strcmp(checksum, info->packages[i]->checksum_sha256) == 0) { + printf("Skipped: %s\n", archive); + free(checksum); + free(archive); + free(path); + continue; + } + } + printf("Fetch: %s\n", archive); + if ((response = fetch(archive, path)) >= 400) { + fprintf(stderr, "WARNING: HTTP(%ld, %s): %s\n", response, http_response_str(response), archive); + } + free(archive); + free(path); + } + + // Now fetch a copy of the physical manifest + char *datafile = join((char *[]) {dest, basename(info->origin), NULL}, DIRSEPS); + long response = 0; + if ((response = fetch(info->origin, datafile) >= 400)) { + fprintf(stderr, "WARNING: HTTP(%ld, %s): %s\n", response, http_response_str(response), info->origin); + } + free(dest); + free(datafile); + printf("done!\n"); +}
\ No newline at end of file diff --git a/lib/purge.c b/lib/purge.c new file mode 100644 index 0000000..997df51 --- /dev/null +++ b/lib/purge.c @@ -0,0 +1,93 @@ +#include "spm.h" + +/** + * Remove a package + * @param fs `SPM_Hierarchy` + * @param _package_name + * @return + */ +int spm_purge(SPM_Hierarchy *fs, const char *_package_name) { + size_t files_count = 0; + char **_files_orig = NULL; + char *path = NULL; + char *package_name = strdup(_package_name); + char *package_topdir = join((char *[]) {fs->dbrecdir, package_name, NULL}, DIRSEPS); + char *descriptor = join((char *[]) {package_topdir, SPM_META_DESCRIPTOR, NULL}, DIRSEPS); + char *filelist = join((char *[]) {package_topdir, SPM_META_FILELIST, NULL}, DIRSEPS); + + if (spm_check_installed(fs, package_name) == 0) { + // package is not installed in this root + free(package_name); + free(package_topdir); + free(descriptor); + free(filelist); + return 1; + } + + ConfigItem **desc = spm_descriptor_read(descriptor); + char *name = config_get(desc, "name")->value; + char *version = config_get(desc, "version")->value; + char *revision = config_get(desc, "revision")->value; + + printf("Removing package: %s-%s-%s\n", name, version, revision); + _files_orig = spm_metadata_read(filelist, SPM_METADATA_VERIFY); + for (size_t i = 0; _files_orig[i] != NULL; i++) { + files_count++; + } + + for (size_t i = 0; _files_orig[i] != NULL; i++) { + path = calloc(PATH_MAX, sizeof(char)); + path = join((char *[]) {fs->rootdir, _files_orig[i], NULL}, DIRSEPS); + if (SPM_GLOBAL.verbose) { + printf(" -> %s\n", path); + } + if (exists(path) != 0) { + printf("%s does not exist\n", path); + } else { + remove(path); + } + free(path); + } + rmdirs(package_topdir); + + free(package_name); + free(package_topdir); + free(descriptor); + free(filelist); + config_free(desc); + return 0; +} + +/** + * Remove packages + * @param fs `SPM_Hierarchy` + * @param packages `StrList` + * @return + */ +int spm_do_purge(SPM_Hierarchy *fs, StrList *packages) { + int status_remove = 0; + + printf("Removing package(s):\n"); + for (size_t i = 0; i < strlist_count(packages); i++) { + char *package = strlist_item(packages, i); + if (spm_check_installed(fs, package) == 0) { + printf("%s is not installed\n", package); + return -1; + } + printf("-> %s\n", package); + } + + if (SPM_GLOBAL.prompt_user) { + if (spm_prompt_user("Proceed with removal?", 1) == 0) { + return -2; + } + } + + for (size_t i = 0; i < strlist_count(packages); i++) { + if ((status_remove = spm_purge(fs, strlist_item(packages, i))) != 0) { + printf("Failed"); + break; + } + } + return status_remove; +}
\ No newline at end of file diff --git a/lib/relocation.c b/lib/relocation.c new file mode 100644 index 0000000..f22a25d --- /dev/null +++ b/lib/relocation.c @@ -0,0 +1,440 @@ +/** + * @file relocation.c + */ +#include "spm.h" + +const char *METADATA_FILES[] = { + SPM_META_DEPENDS, + SPM_META_PREFIX_BIN, + SPM_META_PREFIX_TEXT, + SPM_META_DESCRIPTOR, + SPM_META_FILELIST, + NULL, +}; + +/** + * Replace all occurrences of `spattern` with `sreplacement` in `data` + * + * ~~~{.c} + * char *str = (char *)calloc(100, sizeof(char)); + * strcpy(str, "This are a test."); + * replace_text(str, "are", "is"); + * // str is: "This is a test." + * free(str); + * ~~~ + * + * @param data string to modify + * @param spattern string value to replace + * @param sreplacement replacement string value + * @return success=0, error=-1 + */ +int replace_text(char *data, const char *spattern, const char *sreplacement) { + if (data == NULL || spattern == NULL || sreplacement == NULL) { + return -1; + } + + char *tmp = data; + size_t data_len = strlen(data); + size_t spattern_len = strlen(spattern); + size_t sreplacement_len = strlen(sreplacement); + + if (sreplacement_len > spattern_len) { + fprintf(stderr, "replacement string too long: %zu > %zu\n '%s'\n '%s'\n", sreplacement_len, spattern_len, sreplacement, spattern); + return -1; + } + + while (*tmp != '\0') { + if (strncmp(tmp, spattern, spattern_len) == 0) { + if (sreplacement_len == 1) { + *tmp = *sreplacement; + } else { + memmove(tmp, sreplacement, sreplacement_len); + memmove(tmp + sreplacement_len, tmp + spattern_len, data_len - spattern_len); + memset(tmp + sreplacement_len + (data_len - spattern_len), '\0', 1); + } + } + tmp++; + } + return 0; +} + +/** + * Replace all occurrences of `oldstr` in file `path` with `newstr` + * @param filename file to modify + * @param oldstr string to replace + * @param newstr replacement string + * @return success=0, failure=-1, or value of `ferror()` + */ +int file_replace_text(char *filename, const char *spattern, const char *sreplacement) { + char data[BUFSIZ]; + char tempfile[PATH_MAX]; + FILE *fp = NULL; + if ((fp = fopen(filename, "r")) == NULL) { + perror(filename); + return -1; + } + + sprintf(tempfile, "%s.spmfrt", filename); + FILE *tfp = NULL; + if ((tfp = fopen(tempfile, "w+")) == NULL) { + fclose(fp); + perror(tempfile); + return -1; + } + + // Zero the data buffer + memset(data, '\0', BUFSIZ); + while(fgets(data, BUFSIZ, fp) != NULL) { + replace_text(data, spattern, sreplacement); + fprintf(tfp, "%s", data); + } + fclose(fp); + rewind(tfp); + + // Truncate the original file + if ((fp = fopen(filename, "w+")) == NULL) { + perror(filename); + return -1; + } + // Zero the data buffer once more + memset(data, '\0', BUFSIZ); + // Dump the contents of the temporary file into the original file + while(fgets(data, BUFSIZ, tfp) != NULL) { + fprintf(fp, "%s", data); + } + fclose(fp); + fclose(tfp); + + // Remove temporary file + unlink(tempfile); + return 0; +} + +/** + * Free memory allocated by `prefixes_read` function + * @param entry array of RelocationEntry + */ +void prefixes_free(RelocationEntry **entry) { + if (!entry) { + return; + } + for (int i = 0; entry[i] != NULL; i++) { + if (entry[i]->prefix) free(entry[i]->prefix); + if (entry[i]->path) free(entry[i]->path); + if (entry[i]) free(entry[i]); + } + free(entry); +} + +/** + * Parse a prefix file + * + * The file format is as follows: + * + * ~~~ + * #prefix + * path + * #prefix + * path + * #...N + * ...N + * ~~~ + * @param filename path to prefix manifest + * @return success=array of RelocationEntry, failure=NULL + */ +RelocationEntry **prefixes_read(const char *filename) { + size_t record_count = 0; + size_t parity = 0; + FILE *fp = fopen(filename, "r"); + if (!fp) { + fprintf(SYSERROR); + return NULL; + } + RelocationEntry **entry = NULL; + char line[BUFSIZ]; + char *lptr = line; + memset(lptr, '\0', BUFSIZ); + + while (fgets(lptr, BUFSIZ, fp) != NULL) { + if (isempty(lptr)) { + continue; + } + record_count++; + } + rewind(fp); + + // Initialize the relocation entry array + if (record_count == 0) { + return NULL; + } + + parity = record_count % 2; + if (parity != 0) { + fprintf(stderr, "%s: records are not divisible by 2 (got: %zu %% 2 = %zu)\n", filename, record_count, parity); + return NULL; + } + record_count /= 2; + + entry = (RelocationEntry **)calloc(record_count + 1, sizeof(RelocationEntry *)); + if (!entry) { + return NULL; + } + for (size_t i = 0; i < record_count; i++) { + entry[i] = (RelocationEntry *) calloc(1, sizeof(RelocationEntry)); + if (!entry[i]) { + return NULL; + } + } + + int do_prefix = 0; + int do_path = 0; + size_t i = 0; + while (fgets(lptr, BUFSIZ, fp) != NULL) { + if (isempty(lptr)) { + continue; + } + if (startswith(lptr, "#")) { + do_prefix = 1; + } + else { + do_path = 1; + } + + // Allocate a relocation record + if (!entry[i]) { + fclose(fp); + return NULL; + } + + + if (do_prefix) { + // Populate prefix data (a prefix starts with a #) + entry[i]->prefix = (char *) calloc(strlen(lptr) + 1, sizeof(char)); + if (!entry[i]->prefix) { + fclose(fp); + return NULL; + } + strncpy(entry[i]->prefix, lptr, strlen(lptr)); + // Remove prefix delimiter and whitespace + strchrdel(entry[i]->prefix, "#"); + entry[i]->prefix = strip(entry[i]->prefix); + do_prefix = 0; + continue; + } + + else if (do_path) { + // Populate path data + entry[i]->path = (char *) calloc(strlen(lptr) + 1, sizeof(char)); + if (!entry[i]->path) { + fclose(fp); + return NULL; + } + strncpy(entry[i]->path, lptr, strlen(lptr)); + entry[i]->path = strip(entry[i]->path); + do_path = 0; + } + i++; + } + fclose(fp); + return entry; +} + +/** + * Determine if `filename` is a SPM metadata file + * + * Example: + * + * ~~~{.c} + * #include "spm.h" + * + * int main() { + * if (file_is_metadata(".SPM_DEPENDS")) { + * // file is metadata + * } else { + * // file is not metadata + * } + * } + * ~~~ + * + * @param filename + * @return 0=no, 1=yes + */ +int file_is_metadata(const char *filename) { + for (size_t i = 0; METADATA_FILES[i] != NULL; i++) { + if (strstr(filename, METADATA_FILES[i]) != NULL) { + return 1; + } + } + return 0; +} + +/** + * Scan `tree` for files containing `prefix`. Matches are recorded in `output_file` with the following format: + * + * ~~~ + * #prefix + * path + * #prefix + * path + * #...N + * ...N + * ~~~ + * + * Example: + * ~~~{.c} + * char **prefixes = {"/usr", "/var", NULL}; + * prefixes_write("binary.manifest", PREFIX_WRITE_BIN, prefixes, "/usr/bin"); + * prefixes_write("text.manifest", PREFIX_WRITE_TEXT, prefixes, "/etc"); + * ~~~ + * + * @param output_file file path to create + * @param mode `PREFIX_WRITE_BIN`, `PREFIX_WRITE_TEXT` + * @param prefix array of prefix strings + * @param tree directory to scan + * @return success=0, failure=1, error=-1 + */ +int prefixes_write(const char *output_file, int mode, char **prefix, const char *tree) { + FILE *fp = fopen(output_file, "w+"); + if (!fp) { + perror(output_file); + fprintf(SYSERROR); + return -1; + } + + char *cwd = getcwd(NULL, PATH_MAX); + chdir(tree); + { + FSTree *fsdata = fstree(".", NULL, SPM_FSTREE_FLT_RELATIVE); + if (!fsdata) { + fclose(fp); + fprintf(SYSERROR); + return -1; + } + for (size_t i = 0; i < fsdata->files_length; i++) { + if (file_is_metadata(fsdata->files[i])) { + continue; + } + for (int p = 0; prefix[p] != NULL; p++) { + if (find_in_file(fsdata->files[i], prefix[p]) == 0) { + int proceed = 0; + if (mode == PREFIX_WRITE_BIN) { + proceed = file_is_binary(fsdata->files[i]); + } else if (mode == PREFIX_WRITE_TEXT) { + proceed = file_is_text(fsdata->files[i]); + } + + // file_is_* functions return NULL when they encounter anything but a regular file + if (!proceed) { + continue; + } + // Record in file + fprintf(fp, "#%s\n%s\n", prefix[p], fsdata->files[i]); + } + } + } + } chdir(cwd); + free(cwd); + fclose(fp); + return 0; +} + +/** + * Wrapper for `reloc` program. Replace text in binary data. + * @param _filename + * @param _oldstr + * @param _newstr + * @return + */ +int relocate(const char *_filename, const char *_oldstr, const char *_newstr) { + int returncode; + Process *proc = NULL; + char *oldstr = strdup(_oldstr); + char *newstr = strdup(_newstr); + char *filename = strdup(_filename); + char cmd[PATH_MAX]; + + // sanitize command + strchrdel(oldstr, SHELL_INVALID); + strchrdel(newstr, SHELL_INVALID); + strchrdel(filename, SHELL_INVALID); + + memset(cmd, '\0', sizeof(cmd)); + sprintf(cmd, "reloc \"%s\" \"%s\" \"%s\" \"%s\" 2>&1", oldstr, newstr, filename, filename); + + if (SPM_GLOBAL.verbose > 1) { + printf(" EXEC : %s\n", cmd); + } + + shell(&proc, SHELL_OUTPUT, cmd); + if (!proc) { + free(oldstr); + free(newstr); + free(filename); + return -1; + } + + returncode = proc->returncode; + if (returncode != 0 && proc->output) { + fprintf(stderr, "%s\n", proc->output); + } + + shell_free(proc); + free(oldstr); + free(newstr); + free(filename); + return returncode; +} + +/** + * Parse package metadata and set `baseroot` binaries/text to point to `destroot`. + * `baseroot` should be a temporary directory because its contents are modified + * + * @param destroot + * @param baseroot + */ +void relocate_root(const char *destroot, const char *baseroot) { + RelocationEntry **b_record = NULL; + RelocationEntry **t_record = NULL; + char cwd[PATH_MAX]; + + getcwd(cwd, sizeof(cwd)); + chdir(baseroot); + { + FSTree *libs = rpath_libraries_available("."); + // Rewrite binary prefixes + b_record = prefixes_read(SPM_META_PREFIX_BIN); + if (b_record) { + for (int i = 0; b_record[i] != NULL; i++) { + if (file_is_binexec(b_record[i]->path)) { + if (SPM_GLOBAL.verbose) { + printf("Relocate RPATH: %s\n", b_record[i]->path); + } + rpath_autoset(b_record[i]->path, libs); + } + if (SPM_GLOBAL.verbose) { + printf("Relocate DATA : %s\n", b_record[i]->path); + } + relocate(b_record[i]->path, b_record[i]->prefix, destroot); + } + } + + // Rewrite text prefixes + t_record = prefixes_read(SPM_META_PREFIX_TEXT); + if (t_record) { + for (int i = 0; t_record[i] != NULL; i++) { + if (SPM_GLOBAL.verbose) { + printf("Relocate TEXT : %s\n", t_record[i]->path); + } + if (SPM_GLOBAL.verbose > 1) { + printf(" EDIT : '%s' -> '%s'\n", t_record[i]->prefix, destroot); + } + file_replace_text(t_record[i]->path, t_record[i]->prefix, destroot); + } + } + + prefixes_free(b_record); + prefixes_free(t_record); + } + chdir(cwd); +} + diff --git a/lib/resolve.c b/lib/resolve.c new file mode 100644 index 0000000..1a4448f --- /dev/null +++ b/lib/resolve.c @@ -0,0 +1,65 @@ +/** + * Dependency resolution functions + * @file resolve.c + */ +#include "spm.h" + +static ManifestPackage *requirements[SPM_REQUIREMENT_MAX] = {0, }; + +void resolve_free() { + for (size_t i = 0; i < SPM_REQUIREMENT_MAX; i++) { + if (requirements[i] != NULL) + manifest_package_free(requirements[i]); + } +} + +/** + * Scan global `requirements` array for `archive` + * @param archive + * @return 0 = not found, 1 = found + */ +int resolve_has_dependency(const char *archive) { + for (size_t i = 0; requirements[i] != NULL && i < SPM_REQUIREMENT_MAX; i++) { + if (strcmp(requirements[i]->archive, archive) == 0) { + return 1; + } + } + return 0; +} + +/** + * Recursively scan a package for its dependencies + * @param manifests `ManifestList` struct + * @param spec Package name (accepts version specifiers) + * @return success = array of `ManifestPackage`, not found = NULL + */ +ManifestPackage **resolve_dependencies(ManifestList *manifests, const char *spec) { + static size_t req_i = 0; + ManifestPackage *package = manifestlist_search(manifests, spec); + ManifestPackage *requirement = NULL; + + if (package == NULL) { + return requirements; + } + + for (size_t i = 0; i < package->requirements_records && i < SPM_REQUIREMENT_MAX; i++) { + requirement = manifestlist_search(manifests, package->requirements[i]); + if (requirement == NULL) { + fprintf(stderr, "ERROR: unable to resolve package via manifestlist_search(): '%s'\n", package->requirements[i]); + exit(1); + } + if (resolve_has_dependency(requirement->archive)) { + free(requirement); + } else { + resolve_dependencies(manifests, requirement->archive); + requirements[req_i] = requirement; + } + } + + if (!resolve_has_dependency(package->archive)) { + requirements[req_i] = package; + req_i++; + } + + return requirements; +}
\ No newline at end of file diff --git a/lib/rpath.c b/lib/rpath.c new file mode 100644 index 0000000..4d4d801 --- /dev/null +++ b/lib/rpath.c @@ -0,0 +1,303 @@ +/** + * @file rpath.c + */ +#include "spm.h" + +/** + * Wrapper function to execute `patchelf` with arguments + * @param _filename Path of file to modify + * @param _args Arguments to pass to `patchelf` + * @return success=Process struct, failure=NULL + */ +Process *patchelf(const char *_filename, const char *_args) { + char *filename = strdup(_filename); + char *args = strdup(_args); + Process *proc_info = NULL; + char sh_cmd[PATH_MAX]; + sh_cmd[0] = '\0'; + + strchrdel(args, SHELL_INVALID); + strchrdel(filename, SHELL_INVALID); + sprintf(sh_cmd, "patchelf %s %s 2>&1", args, filename); + + if (SPM_GLOBAL.verbose > 1) { + printf(" EXEC : %s\n", sh_cmd); + } + + shell(&proc_info, SHELL_OUTPUT, sh_cmd); + + free(filename); + free(args); + return proc_info; +} + +/** + * Determine whether a RPATH or RUNPATH is present in file + * + * TODO: Replace with OS-native solution(s) + * + * @param _filename path to executable or library + * @return -1=OS error, 0=has rpath, 1=not found + */ +int has_rpath(const char *_filename) { + int result = 1; // default: not found + + char *filename = strdup(_filename); + if (!filename) { + return -1; + } + + // sanitize input path + strchrdel(filename, SHELL_INVALID); + + Process *pe = patchelf(filename, "--print-rpath"); + strip(pe->output); + if (!isempty(pe->output)) { + result = 0; + } + else { + // something went wrong with patchelf other than + // what we're looking for + result = -1; + } + + free(filename); + shell_free(pe); + return result; +} + +/** + * Returns a RPATH or RUNPATH if one is defined in `_filename` + * + * TODO: Replace with OS-native solution(s) + * + * @param _filename path to executable or library + * @return RPATH string, NULL=error (caller is responsible for freeing memory) + */ +char *rpath_get(const char *_filename) { + if ((has_rpath(_filename)) != 0) { + return strdup(""); + } + char *filename = strdup(_filename); + if (!filename) { + return NULL; + } + char *path = strdup(filename); + if (!path) { + free(filename); + return NULL; + } + + char *rpath = NULL; + + // sanitize input path + strchrdel(path, SHELL_INVALID); + + Process *pe = patchelf(filename, "--print-rpath"); + if (pe->returncode != 0) { + fprintf(stderr, "patchelf error: %s %s\n", path, strip(pe->output)); + return NULL; + } + + rpath = (char *)calloc(strlen(pe->output) + 1, sizeof(char)); + if (!rpath) { + free(filename); + free(path); + shell_free(pe); + return NULL; + } + + strncpy(rpath, pe->output, strlen(pe->output)); + strip(rpath); + + free(filename); + free(path); + shell_free(pe); + return rpath; +} + +/** + * Generate a RPATH in the form of: + * + * `$ORIGIN/relative/path/to/lib/from/_filename/path` + * + * @param _filename + * @return + */ +char *rpath_generate(const char *_filename, FSTree *tree) { + char *filename = realpath(_filename, NULL); + if (!filename) { + return NULL; + } + + char *result = rpath_autodetect(filename, tree); + if (!result) { + free(filename); + return NULL; + } + + free(filename); + return result; +} + +/** + * Set the RPATH of an executable + * @param filename + * @param rpath + * @return + */ +int rpath_set(const char *filename, const char *rpath) { + int returncode = 0; + char args[PATH_MAX]; + + memset(args, '\0', PATH_MAX); + sprintf(args, "--set-rpath '%s'", rpath); // note: rpath requires single-quotes + Process *pe = patchelf(filename, args); + if (pe != NULL) { + returncode = pe->returncode; + } + shell_free(pe); + return returncode; +} + +/** + * Automatically detect the nearest lib directory and set the RPATH of an executable + * @param filename + * @param _rpath + * @return + */ +int rpath_autoset(const char *filename, FSTree *tree) { + int returncode = 0; + + char *rpath_new = rpath_generate(filename, tree); + if (!rpath_new) { + return -1; + } + + returncode = rpath_set(filename, rpath_new); + free(rpath_new); + + return returncode; +} + +/** + * Find shared libraries in a directory tree + * + * @param root directory + * @return `FSTree` + */ +FSTree *rpath_libraries_available(const char *root) { + FSTree *tree = fstree(root, (char *[]) {SPM_SHLIB_EXTENSION, NULL}, SPM_FSTREE_FLT_CONTAINS | SPM_FSTREE_FLT_RELATIVE); + if (tree == NULL) { + perror(root); + fprintf(SYSERROR); + return NULL; + } + return tree; +} + +/** + * Compute a RPATH based on the location `filename` relative to the shared libraries it requires + * + * @param filename path to file (or a directory) + * @return success=relative path from `filename` to nearest lib directory, failure=NULL + */ +char *rpath_autodetect(const char *filename, FSTree *tree) { + const char *origin = "$ORIGIN"; + char *rootdir = dirname(filename); + char *start = realpath(rootdir, NULL); + char *cwd = realpath(".", NULL); + char *result = NULL; + + char *visit = NULL; // Current directory + char _relative[PATH_MAX] = {0,}; // Generated relative path to lib directory + char *relative = _relative; // Pointer to relative path + size_t depth_to_root = 0; + + // BUG: Perl dumps its shared library in a strange place. + // This function returns `$ORIGIN/../../../CORE` which is not what we want to see. + // TODO: We WANT to see this: `$ORIGIN/../lib/perl5/xx.xx.xx/<arch>/CORE` not just the basename() + + // Change directory to the requested root + chdir(start); + + // Count the relative path distance between the location of the binary, and the top of the root + visit = strdup(start); + for (depth_to_root = 0; strcmp(tree->root, visit) != 0; depth_to_root++) { + // Copy the current visit pointer + char *prev = visit; + // Walk back another directory level + visit = dirname(visit); + // Free previous visit pointer + if (prev) free(prev); + } + free(visit); + + // return to calling directory + chdir(cwd); + + StrList *libs = strlist_init(); + if (libs == NULL) { + fprintf(stderr, "failed to initialize library StrList\n"); + fprintf(SYSERROR); + return NULL; + } + + StrList *libs_wanted = shlib_deps(filename); + if (libs_wanted == NULL) { + fprintf(stderr, "failed to retrieve list of share libraries from: %s\n", filename); + fprintf(SYSERROR); + return NULL; + } + + for (size_t i = 0; i < strlist_count(libs_wanted); i++) { + // zero out relative path string + memset(_relative, '\0', sizeof(_relative)); + // Get the shared library name we are going to look for in the tree + char *shared_library = strlist_item(libs_wanted, i); + + // Is the the shared library in the tree? + char *match = NULL; + if ((match = dirname(strstr_array(tree->files, shared_library))) != NULL) { + // Begin generating the relative path string + strcat(relative, origin); + strcat(relative, DIRSEPS); + + // Append the number of relative levels to the relative path string + if (depth_to_root) { + for (size_t d = 0; d < depth_to_root; d++) { + strcat(relative, ".."); + strcat(relative, DIRSEPS); + } + } else { + strcat(relative, ".."); + strcat(relative, DIRSEPS); + } + // Append the match to the relative path string + strcat(relative, basename(match)); + + // Append relative path to array of libraries (if it isn't already in there) + if (strstr_array(libs->data, relative) == NULL) { + strlist_append(libs, relative); + } + } + } + + // Some programs do not require local libraries provided by SPM (i.e. libc) + // Inject "likely" defaults here + if (strlist_count(libs) == 0) { + strlist_append(libs, "$ORIGIN/../lib"); + strlist_append(libs, "$ORIGIN/../lib64"); + } + + // Populate result string + result = join(libs->data, ":"); + + // Clean up + strlist_free(libs); + strlist_free(libs_wanted); + free(rootdir); + free(cwd); + free(start); + return result; +} diff --git a/lib/shell.c b/lib/shell.c new file mode 100644 index 0000000..6b28e64 --- /dev/null +++ b/lib/shell.c @@ -0,0 +1,116 @@ +/** + * @file shell.c + */ +#include "spm.h" + +/** + * A wrapper for `popen()` that executes non-interactive programs and reports their exit value. + * To redirect stdout and stderr you must do so from within the `fmt` string + * + * ~~~{.c} + * int fd = 1; // stdout + * const char *log_file = "log.txt"; + * Process *proc_info; + * int status; + * + * // Send stderr to stdout + * shell(&proc_info, SHELL_OUTPUT, "foo 2>&1"); + * // Send stdout and stderr to /dev/null + * shell(&proc_info, SHELL_OUTPUT,"bar &>/dev/null"); + * // Send stdout from baz to log.txt + * shell(&proc_info, SHELL_OUTPUT, "baz %d>%s", fd, log_file); + * // Do not record or redirect output from any streams + * shell(&proc_info, SHELL_DEFAULT, "biff"); + * ~~~ + * + * @param Process uninitialized `Process` struct will be populated with process data + * @param options change behavior of the function + * @param fmt shell command to execute (accepts `printf` style formatters) + * @param ... variadic arguments (used by `fmt`) + */ +void shell(Process **proc_info, u_int64_t option, const char *fmt, ...) { + va_list args; + va_start(args, fmt); + + clockid_t clkid = CLOCK_REALTIME; + FILE *proc = NULL; + + (*proc_info) = (Process *)calloc(1, sizeof(Process)); + if (!(*proc_info)) { + fprintf(SYSERROR); + exit(errno); + } + (*proc_info)->returncode = -1; + + // outbuf needs to be an integer type because fgetc returns EOF (> char) + int *outbuf = (int *)calloc(1, sizeof(int)); + if (!outbuf) { + fprintf(SYSERROR); + exit(errno); + } + char *cmd = (char *)calloc(PATH_MAX, sizeof(char)); + if (!cmd) { + fprintf(SYSERROR); + exit(errno); + } + + vsnprintf(cmd, PATH_MAX, fmt, args); + + if (option & SHELL_BENCHMARK) { + if (clock_gettime(clkid, &(*proc_info)->start_time) == -1) { + perror("clock_gettime"); + exit(errno); + } + } + + proc = popen(cmd, "r"); + if (!proc) { + free(outbuf); + va_end(args); + return; + } + + if (option & SHELL_BENCHMARK) { + if (clock_gettime(clkid, &(*proc_info)->stop_time) == -1) { + perror("clock_gettime"); + exit(errno); + } + (*proc_info)->time_elapsed = ((*proc_info)->stop_time.tv_sec - (*proc_info)->start_time.tv_sec) + + ((*proc_info)->stop_time.tv_nsec - (*proc_info)->start_time.tv_nsec) / 1E9; + } + + if (option & SHELL_OUTPUT) { + size_t bytes_read = 0; + size_t i = 0; + size_t new_buf_size; + (*proc_info)->output = (char *)calloc(BUFSIZ, sizeof(char)); + + while ((*outbuf = fgetc(proc)) != EOF) { + if (i >= BUFSIZ) { + new_buf_size = BUFSIZ + (i + bytes_read); + (*proc_info)->output = (char *)realloc((*proc_info)->output, new_buf_size); + i = 0; + } + if (*outbuf) { + (*proc_info)->output[bytes_read] = (char)*outbuf; + } + bytes_read++; + i++; + } + } + (*proc_info)->returncode = pclose(proc); + va_end(args); + free(outbuf); + free(cmd); +} + +/** + * Free process resources allocated by `shell()` + * @param proc_info `Process` struct + */ +void shell_free(Process *proc_info) { + if (proc_info->output) { + free(proc_info->output); + } + free(proc_info); +} diff --git a/lib/shlib.c b/lib/shlib.c new file mode 100644 index 0000000..a8222af --- /dev/null +++ b/lib/shlib.c @@ -0,0 +1,72 @@ +#include "spm.h" +#include "shlib.h" + +char *shlib_deps_objdump(const char *_filename) { + // do not expose this function + char *filename = NULL; + char *result = NULL; + Process *proc = NULL; + char cmd[PATH_MAX]; + memset(cmd, '\0', sizeof(cmd)); + + if ((filename = strdup(_filename)) == NULL) { + fprintf(SYSERROR); + return NULL; + } + + strchrdel(filename, SHELL_INVALID); + snprintf(cmd, sizeof(cmd), "%s %s '%s'", SPM_SHLIB_EXEC, "-p", filename); + shell(&proc, SHELL_OUTPUT, cmd); + + if (proc->returncode != 0) { + free(filename); + shell_free(proc); + return NULL; + } + result = strdup(proc->output); + + free(filename); + shell_free(proc); + return result; +} + +StrList *shlib_deps(const char *filename) { + char **data = NULL; + char *output = NULL; + StrList *result = NULL; + + // Get output from objdump + // TODO: use preprocessor or another function to select the correct shlib_deps_*() in the future + if ((output = shlib_deps_objdump(filename)) == NULL) { + return NULL; + } + + // Initialize list array + if ((result = strlist_init()) == NULL) { + free(output); + return NULL; + } + + // Split output into individual lines + if ((data = split(output, "\n")) == NULL) { + free(output); + strlist_free(result); + return NULL; + } + + // Parse output: + // Collapse whitespace and extract the NEEDED libraries (second field) + // AFAIK when "NEEDED" is present, a string containing the library name is guaranteed to be there + for (size_t i = 0; data[i] != NULL; i++) { + data[i] = normalize_space(data[i]); + if (startswith(data[i], "NEEDED")) { + char **field = split(data[i], " "); + strlist_append(result, field[1]); + split_free(field); + } + } + + free(output); + split_free(data); + return result; +} diff --git a/lib/spm_build.c b/lib/spm_build.c new file mode 100644 index 0000000..726b540 --- /dev/null +++ b/lib/spm_build.c @@ -0,0 +1,23 @@ +/** + * @file spm_build.c + */ +#include "spm.h" + +/** + * + * @param argc + * @param argv + * @return + */ +int build(int argc, char **argv) { + printf("build:\n"); + printf("argc: %d\n", argc); + printf("argv:\n"); + for (int i = 0; i < argc; i++) { + printf("%d: %s\n", i, argv[i]); + } + + return 0; +} + + diff --git a/lib/str.c b/lib/str.c new file mode 100644 index 0000000..5db3adc --- /dev/null +++ b/lib/str.c @@ -0,0 +1,699 @@ +/** + * @file strings.c + */ +#include "spm.h" + +/** + * Determine how many times the character `ch` appears in `sptr` string + * @param sptr string to scan + * @param ch character to find + * @return count of characters found + */ +int num_chars(const char *sptr, int ch) { + int result = 0; + for (int i = 0; sptr[i] != '\0'; i++) { + if (sptr[i] == ch) { + result++; + } + } + return result; +} + +/** + * Scan for `pattern` string at the beginning of `sptr` + * + * @param sptr string to scan + * @param pattern string to search for + * @return 1 = found, 0 = not found, -1 = error + */ +int startswith(const char *sptr, const char *pattern) { + if (!sptr) { + return -1; + } + for (size_t i = 0; i < strlen(pattern); i++) { + if (sptr[i] != pattern[i]) { + return 0; + } + } + return 1; +} + +/** + * Scan for `pattern` string at the end of `sptr` + * + * @param sptr string to scan + * @param pattern string to search for + * @return 1 = found, 0 = not found, -1 = error + */ +int endswith(const char *sptr, const char *pattern) { + if (!sptr) { + return -1; + } + ssize_t sptr_size = strlen(sptr); + ssize_t pattern_size = strlen(pattern); + + if (sptr_size == pattern_size) { + if (strcmp(sptr, pattern) == 0) { + return 1; // yes + } + return 0; // no + } + + ssize_t s = sptr_size - pattern_size; + if (s < 0) { + return 0; + } + + for (size_t p = 0 ; s < sptr_size; s++, p++) { + if (sptr[s] != pattern[p]) { + // sptr does not end with pattern + return 0; + } + } + // sptr ends with pattern + return 1; +} + +/** + * Deletes any characters matching `chars` from `sptr` string + * + * @param sptr string to be modified in-place + * @param chars a string containing characters (e.g. " \n" would delete whitespace and line feeds) + */ +void strchrdel(char *sptr, const char *chars) { + while (*sptr != '\0') { + for (int i = 0; chars[i] != '\0'; i++) { + if (*sptr == chars[i]) { + memmove(sptr, sptr + 1, strlen(sptr)); + } + } + sptr++; + } +} + +/** + * Find the integer offset of the first occurrence of `ch` in `sptr` + * + * ~~~{.c} + * char buffer[255]; + * char string[] = "abc=123"; + * long int separator_offset = strchroff(string, '='); + * for (long int i = 0; i < separator_offset); i++) { + * buffer[i] = string[i]; + * } + * ~~~ + * + * @param sptr string to scan + * @param ch character to find + * @return offset to character in string, or 0 on failure + */ +long int strchroff(const char *sptr, int ch) { + char *orig = strdup(sptr); + char *tmp = orig; + long int result = 0; + while (*tmp != '\0') { + if (*tmp == ch) { + break; + } + tmp++; + } + result = tmp - orig; + free(orig); + + return result; +} + +/** + * This function scans `sptr` from right to left removing any matches to `suffix` + * from the string. + * + * @param sptr string to be modified + * @param suffix string to be removed from `sptr` + */ +void strdelsuffix(char *sptr, const char *suffix) { + if (!sptr || !suffix) { + return; + } + size_t sptr_len = strlen(sptr); + size_t suffix_len = strlen(suffix); + intptr_t target_offset = sptr_len - suffix_len; + + // Prevent access to memory below input string + if (target_offset < 0) { + return; + } + + // Create a pointer to + char *target = sptr + target_offset; + if (!strcmp(target, suffix)) { + // Purge the suffix + memset(target, '\0', suffix_len); + // Recursive call continues removing suffix until it is gone + strip(sptr); + } +} + +/** + * Split a string by every delimiter in `delim` string. + * + * Callee must free memory using `split_free()` + * + * @param sptr string to split + * @param delim characters to split on + * @return success=parts of string, failure=NULL + */ +char** split(char *_sptr, const char* delim) +{ + if (_sptr == NULL) { + return NULL; + } + size_t split_alloc = 0; + // Duplicate the input string and save a copy of the pointer to be freed later + char *orig = strdup(_sptr); + char *sptr = orig; + if (!sptr) { + return NULL; + } + + // Determine how many delimiters are present + for (size_t i = 0; i < strlen(delim); i++) { + split_alloc += num_chars(sptr, delim[i]); + } + // Preallocate enough records based on the number of delimiters + char **result = (char **)calloc(split_alloc + 2, sizeof(char *)); + if (!result) { + free(sptr); + return NULL; + } + + // Separate the string into individual parts and store them in the result array + int i = 0; + char *token = NULL; + while((token = strsep(&sptr, delim)) != NULL) { + result[i] = (char *)calloc(strlen(token) + 1, sizeof(char)); + if (!result[i]) { + free(sptr); + return NULL; + } + memcpy(result[i], token, strlen(token) + 1); // copy the string contents into the record + i++; // next record + } + free(orig); + return result; +} + +/** + * Frees memory allocated by `split()` + * @param ptr pointer to array + */ +void split_free(char **ptr) { + for (int i = 0; ptr[i] != NULL; i++) { + free(ptr[i]); + } + free(ptr); +} + +/** + * Create new a string from an array of strings + * + * ~~~{.c} + * char *array[] = { + * "this", + * "is", + * "a", + * "test", + * NULL, + * } + * + * char *test = join(array, " "); // "this is a test" + * char *test2 = join(array, "_"); // "this_is_a_test" + * char *test3 = join(array, ", "); // "this, is, a, test" + * + * free(test); + * free(test2); + * free(test3); + * ~~~ + * + * @param arr + * @param separator characters to insert between elements in string + * @return new joined string + */ +char *join(char **arr, const char *separator) { + char *result = NULL; + int records = 0; + size_t total_bytes = 0; + + if (!arr) { + return NULL; + } + + for (int i = 0; arr[i] != NULL; i++) { + total_bytes += strlen(arr[i]); + records++; + } + total_bytes += (records * strlen(separator)) + 1; + + result = (char *)calloc(total_bytes, sizeof(char)); + for (int i = 0; i < records; i++) { + strcat(result, arr[i]); + if (i < (records - 1)) { + strcat(result, separator); + } + } + return result; +} + +/** + * Join two or more strings by a `separator` string + * @param separator + * @param ... + * @return string + */ +char *join_ex(char *separator, ...) { + va_list ap; // Variadic argument list + size_t separator_len = 0; // Length of separator string + size_t size = 0; // Length of output string + size_t argc = 0; // Number of arguments ^ "..." + char **argv = NULL; // Arguments + char *current = NULL; // Current argument + char *result = NULL; // Output string + + // Initialize array + argv = calloc(argc + 1, sizeof(char *)); + if (argv == NULL) { + perror("join_ex calloc failed"); + return NULL; + } + + // Get length of the separator + separator_len = strlen(separator); + + // Process variadic arguments: + // 1. Iterate over argument list `ap` + // 2. Assign `current` with the value of argument in `ap` + // 3. Extend the `argv` array by the latest argument count `argc` + // 4. Sum the length of the argument and the `separator` passed to the function + // 5. Append `current` string to `argv` array + // 6. Update argument counter `argc` + va_start(ap, separator); + for(argc = 0; (current = va_arg(ap, char *)) != NULL; argc++) { + char **tmp = realloc(argv, (argc + 1) * sizeof(char *)); + if (tmp == NULL) { + perror("join_ex realloc failed"); + return NULL; + } + argv = tmp; + size += strlen(current) + separator_len; + argv[argc] = strdup(current); + } + va_end(ap); + + // Generate output string + result = calloc(size + 1, sizeof(char)); + for (size_t i = 0; i < argc; i++) { + // Append argument to string + strcat(result, argv[i]); + + // Do not append a trailing separator when we reach the last argument + if (i < (argc - 1)) { + strcat(result, separator); + } + free(argv[i]); + } + free(argv); + + return result; +} + +/** + * Extract the string encapsulated by characters listed in `delims` + * + * ~~~{.c} + * char *str = "this is [some data] in a string"; + * char *data = substring_between(string, "[]"); + * // data = "some data"; + * ~~~ + * + * @param sptr string to parse + * @param delims two characters surrounding a string + * @return success=text between delimiters, failure=NULL + */ +char *substring_between(char *sptr, const char *delims) { + // Ensure we have enough delimiters to continue + size_t delim_count = strlen(delims); + if (delim_count != 2) { + return NULL; + } + + // Create pointers to the delimiters + char *start = strpbrk(sptr, &delims[0]); + char *end = strpbrk(sptr, &delims[1]); + + // Ensure the string has both delimiters + if (!start || !end) { + return NULL; + } + + start++; // ignore leading delimiter + + // Get length of the substring + size_t length = end - start; + + char *result = (char *)calloc(length + 1, sizeof(char)); + if (!result) { + return NULL; + } + + // Copy the contents of the substring to the result + char *tmp = result; + while (start != end) { + *tmp = *start; + tmp++; + start++; + } + + return result; +} + +/* + * Helper function for `strsort` + */ +static int _strsort_compare(const void *a, const void *b) { + const char *aa = *(const char**)a; + const char *bb = *(const char**)b; + int result = strcmp(aa, bb); + return result; +} + +/** + * Sort an array of strings alphabetically + * @param arr + */ +void strsort(char **arr) { + size_t arr_size = 0; + + // Determine size of array + for (size_t i = 0; arr[i] != NULL; i++) { + arr_size = i; + } + qsort(arr, arr_size, sizeof(char *), _strsort_compare); +} + +/* + * Helper function for `strsortlen` + */ +static int _strsortlen_asc_compare(const void *a, const void *b) { + const char *aa = *(const char**)a; + const char *bb = *(const char**)b; + size_t len_a = strlen(aa); + size_t len_b = strlen(bb); + return len_a > len_b; +} + +/* + * Helper function for `strsortlen` + */ +static int _strsortlen_dsc_compare(const void *a, const void *b) { + const char *aa = *(const char**)a; + const char *bb = *(const char**)b; + size_t len_a = strlen(aa); + size_t len_b = strlen(bb); + return len_a < len_b; +} +/** + * Sort an array of strings by length + * @param arr + */ +void strsortlen(char **arr, unsigned int sort_mode) { + typedef int (*compar)(const void *, const void *); + + compar fn = _strsortlen_asc_compare; + if (sort_mode != 0) { + fn = _strsortlen_dsc_compare; + } + + size_t arr_size = 0; + + // Determine size of array + for (size_t i = 0; arr[i] != NULL; i++) { + arr_size = i; + } + qsort(arr, arr_size, sizeof(char *), fn); +} + +/** + * Search for string in an array of strings + * @param arr array of strings + * @param str string to search for + * @return yes=`pointer to string`, no=`NULL`, failure=`NULL` + */ +char *strstr_array(char **arr, const char *str) { + if (arr == NULL) { + return NULL; + } + + for (int i = 0; arr[i] != NULL; i++) { + if (strstr(arr[i], str) != NULL) { + return arr[i]; + } + } + return NULL; +} + +/** + * Remove duplicate strings from an array of strings + * @param arr + * @return success=array of unique strings, failure=NULL + */ +char **strdeldup(char **arr) { + if (!arr) { + return NULL; + } + + size_t records; + // Determine the length of the array + for (records = 0; arr[records] != NULL; records++); + + // Allocate enough memory to store the original array contents + // (It might not have duplicate values, for example) + char **result = (char **)calloc(records + 1, sizeof(char *)); + if (!result) { + return NULL; + } + + int rec = 0; + size_t i = 0; + while(i < records) { + // Search for value in results + if (strstr_array(result, arr[i]) == 0) { + // value already exists in results so ignore it + i++; + continue; + } + + // Store unique value + result[rec] = (char *)calloc(strlen(arr[i]) + 1, sizeof(char)); + if (!result[rec]) { + free(result); + return NULL; + } + memcpy(result[rec], arr[i], strlen(arr[i]) + 1); + i++; + rec++; + } + return result; +} + +/** Remove leading whitespace from a string + * @param sptr pointer to string + * @return pointer to first non-whitespace character in string + */ +char *lstrip(char *sptr) { + char *tmp = sptr; + size_t bytes = 0; + while (isblank(*tmp) || isspace(*tmp)) { + bytes++; + tmp++; + } + if (tmp != sptr) { + memmove(sptr, sptr + bytes, strlen(sptr) - bytes); + memset((sptr + strlen(sptr)) - bytes, '\0', bytes); + } + return sptr; +} + +/** + * Remove trailing whitespace from a string + * @param sptr string + * @return truncated string + */ +char *strip(char *sptr) { + size_t len = strlen(sptr); + if (len == 0) { + return sptr; + } + else if (len == 1 && (isblank(*sptr) || isspace(*sptr))) { + *sptr = '\0'; + return sptr; + } + for (size_t i = len; i != 0; --i) { + if (sptr[i] == '\0') { + continue; + } + if (isspace(sptr[i]) || isblank(sptr[i])) { + sptr[i] = '\0'; + } + else { + break; + } + } + return sptr; +} + +/** + * Determine if a string is empty + * @param sptr pointer to string + * @return 0=not empty, 1=empty + */ +int isempty(char *sptr) { + char *tmp = sptr; + while (*tmp) { + if (!isblank(*tmp) || !isspace(*tmp)) { + return 0; + } + tmp++; + } + return 1; +} + +/** + * Determine if a string is encapsulated by quotes + * @param sptr pointer to string + * @return 0=not quoted, 1=quoted + */ +int isquoted(char *sptr) { + const char *quotes = "'\""; + char *quote_open = strpbrk(sptr, quotes); + if (!quote_open) { + return 0; + } + char *quote_close = strpbrk(quote_open + 1, quotes); + if (!quote_close) { + return 0; + } + return 1; +} + +/** + * Determine whether the input character is a relational operator + * Note: `~` is non-standard + * @param ch + * @return 0=no, 1=yes + */ +int isrelational(char ch) { + char symbols[] = "~!=<>"; + char *symbol = symbols; + while (*symbol != '\0') { + if (ch == *symbol) { + return 1; + } + symbol++; + } + return 0; +} + +/** + * Print characters in `s`, `len` times + * @param s + * @param len + */ +void print_banner(const char *s, int len) { + size_t s_len = strlen(s); + if (!s_len) { + return; + } + for (size_t i = 0; i < (len / s_len); i++) { + for (size_t c = 0; c < s_len; c++) { + putchar(s[c]); + } + } + putchar('\n'); +} + +/** + * Collapse whitespace in `s`. The string is modified in place. + * @param s + * @return pointer to `s` + */ +char *normalize_space(char *s) { + size_t len; + size_t trim_pos; + int add_whitespace = 0; + char *result = s; + char *tmp; + if ((tmp = calloc(strlen(s) + 1, sizeof(char))) == NULL) { + perror("could not allocate memory for temporary string"); + return NULL; + } + char *tmp_orig = tmp; + + // count whitespace, if any + for (trim_pos = 0; isblank(s[trim_pos]); trim_pos++); + // trim whitespace from the left, if any + memmove(s, &s[trim_pos], strlen(&s[trim_pos])); + // cull bytes not part of the string after moving + len = strlen(s); + s[len - trim_pos] = '\0'; + + // Generate a new string with extra whitespace stripped out + while (*s != '\0') { + // Skip over any whitespace, but record that we encountered it + if (isblank(*s)) { + s++; + add_whitespace = 1; + continue; + } + // This gate avoids filling tmp with whitespace; we want to make our own + if (add_whitespace) { + *tmp = ' '; + tmp++; + add_whitespace = 0; + } + // Write character in s to tmp + *tmp = *s; + // Increment string pointers + s++; + tmp++; + } + + // Rewrite the input string + strcpy(result, tmp_orig); + free(tmp_orig); + return result; +} + +/** + * Duplicate an array of strings + * @param array + * @return + */ +char **strdup_array(char **array) { + char **result = NULL; + size_t elems = 0; + + // Guard + if (array == NULL) { + return NULL; + } + + // Count elements in `array` + for (elems = 0; array[elems] != NULL; elems++); + + // Create new array + result = calloc(elems + 1, sizeof(char *)); + for (size_t i = 0; i < elems; i++) { + result[i] = strdup(array[i]); + } + + return result; +} diff --git a/lib/strlist.c b/lib/strlist.c new file mode 100644 index 0000000..1cab324 --- /dev/null +++ b/lib/strlist.c @@ -0,0 +1,339 @@ +/** + * String array convenience functions + * @file strlist.c + */ +#include "spm.h" +#include "strlist.h" + +/** + * + * @param pStrList `StrList` + */ +void strlist_free(StrList *pStrList) { + for (size_t i = 0; i < pStrList->num_inuse; i++) { + free(pStrList->data[i]); + } + free(pStrList->data); + free(pStrList); +} + +/** + * Append a value to the list + * @param pStrList `StrList` + * @param str + */ +void strlist_append(StrList *pStrList, char *str) { + char **tmp = realloc(pStrList->data, (pStrList->num_alloc + 1) * sizeof(char *)); + if (tmp == NULL) { + strlist_free(pStrList); + perror("failed to append to array"); + exit(1); + } + pStrList->data = tmp; + pStrList->data[pStrList->num_inuse] = strdup(str); + pStrList->data[pStrList->num_alloc] = NULL; + strcpy(pStrList->data[pStrList->num_inuse], str); + pStrList->num_inuse++; + pStrList->num_alloc++; +} + +/** + * Append the contents of a `StrList` to another `StrList` + * @param pStrList1 `StrList` + * @param pStrList2 `StrList` + */ +void strlist_append_strlist(StrList *pStrList1, StrList *pStrList2) { + size_t count = strlist_count(pStrList2); + for (size_t i = 0; i < count; i++) { + char *item = strlist_item(pStrList2, i); + strlist_append(pStrList1, item); + } +} + +/** + * + * @param a + * @param b + * @return + */ +static int _strlist_cmpfn(const void *a, const void *b) { + const char *aa = *(const char**)a; + const char *bb = *(const char**)b; + int result = strcmp(aa, bb); + return result; +} + +/** + * + * @param a + * @param b + * @return + */ +static int _strlist_asc_cmpfn(const void *a, const void *b) { + const char *aa = *(const char**)a; + const char *bb = *(const char**)b; + size_t len_a = strlen(aa); + size_t len_b = strlen(bb); + return len_a > len_b - strcmp(aa, bb); +} + +/** + * + * @param a + * @param b + * @return + */ +static int _strlist_dsc_cmpfn(const void *a, const void *b) { + const char *aa = *(const char**)a; + const char *bb = *(const char**)b; + size_t len_a = strlen(aa); + size_t len_b = strlen(bb); + return len_a < len_b - strcmp(aa, bb); +} + +/** + * Sort a `StrList` by `mode` + * @param pStrList + * @param mode Available modes: `STRLIST_DEFAULT` (alphabetic), `STRLIST_ASC` (ascending), `STRLIST_DSC` (descending) + */ +void strlist_sort(StrList *pStrList, unsigned int mode) { + void *fn = NULL; + switch (mode) { + case STRLIST_ASC: + fn = _strlist_asc_cmpfn; + break; + case STRLIST_DSC: + fn = _strlist_dsc_cmpfn; + break; + case STRLIST_DEFAULT: + default: + fn = _strlist_cmpfn; + break; + } + + qsort(pStrList->data, pStrList->num_inuse, sizeof(char *), fn); +} + +/** + * Reverse the order of a `StrList` + * @param pStrList + */ +void strlist_reverse(StrList *pStrList) { + char *tmp = NULL; + size_t i = 0; + size_t j = pStrList->num_inuse - 1; + + for (i = 0; i < j; i++) { + tmp = pStrList->data[i]; + pStrList->data[i] = pStrList->data[j]; + pStrList->data[j] = tmp; + j--; + } +} + +/** + * Get the count of values stored in a `StrList` + * @param pStrList + * @return + */ +size_t strlist_count(StrList *pStrList) { + return pStrList->num_inuse; +} + +/** + * Set value at index + * @param pStrList + * @param value string + * @return + */ +void strlist_set(StrList *pStrList, size_t index, char *value) { + char *tmp = NULL; + char *item = NULL; + if (index > strlist_count(pStrList)) { + return; + } + if ((item = strlist_item(pStrList, index)) == NULL) { + return; + } + if (value == NULL) { + pStrList->data[index] = NULL; + } else { + if ((tmp = realloc(pStrList->data[index], strlen(value) + 1)) == NULL) { + perror("realloc strlist_set replacement value"); + return; + } + + pStrList->data[index] = tmp; + memset(pStrList->data[index], '\0', strlen(value) + 1); + strncpy(pStrList->data[index], value, strlen(value)); + } +} + +/** + * Retrieve data from a `StrList` + * @param pStrList + * @param index + * @return string + */ +char *strlist_item(StrList *pStrList, size_t index) { + if (index > strlist_count(pStrList)) { + return NULL; + } + return pStrList->data[index]; +} + +/** + * Alias of `strlist_item` + * @param pStrList + * @param index + * @return string + */ +char *strlist_item_as_str(StrList *pStrList, size_t index) { + return strlist_item(pStrList, index); +} + +/** + * Convert value at index to `char` + * @param pStrList + * @param index + * @return `char` + */ +char strlist_item_as_char(StrList *pStrList, size_t index) { + return (char) *(strlist_item(pStrList, index)); +} + +/** + * Convert value at index to `unsigned char` + * @param pStrList + * @param index + * @return `unsigned char` + */ +unsigned char strlist_item_as_uchar(StrList *pStrList, size_t index) { + return (unsigned char) *(strlist_item(pStrList, index)); +} + +/** + * Convert value at index to `short` + * @param pStrList + * @param index + * @return `short` + */ +short strlist_item_as_short(StrList *pStrList, size_t index) { + return (short)atoi(strlist_item(pStrList, index)); +} + +/** + * Convert value at index to `unsigned short` + * @param pStrList + * @param index + * @return `unsigned short` + */ +unsigned short strlist_item_as_ushort(StrList *pStrList, size_t index) { + return (unsigned short)atoi(strlist_item(pStrList, index)); +} + +/** + * Convert value at index to `int` + * @param pStrList + * @param index + * @return `int` + */ +int strlist_item_as_int(StrList *pStrList, size_t index) { + return atoi(strlist_item(pStrList, index)); +} + +/** + * Convert value at index to `unsigned int` + * @param pStrList + * @param index + * @return `unsigned int` + */ +unsigned int strlist_item_as_uint(StrList *pStrList, size_t index) { + return (unsigned int)atoi(strlist_item(pStrList, index)); +} + +/** + * Convert value at index to `long` + * @param pStrList + * @param index + * @return `long` + */ +long strlist_item_as_long(StrList *pStrList, size_t index) { + return atol(strlist_item(pStrList, index)); +} + +/** + * Convert value at index to `unsigned long` + * @param pStrList + * @param index + * @return `unsigned long` + */ +unsigned long strlist_item_as_ulong(StrList *pStrList, size_t index) { + return (unsigned long)atol(strlist_item(pStrList, index)); +} + +/** + * Convert value at index to `long long` + * @param pStrList + * @param index + * @return `long long` + */ +long long strlist_item_as_long_long(StrList *pStrList, size_t index) { + return (long long)atoll(strlist_item(pStrList, index)); +} + +/** + * Convert value at index to `unsigned long long` + * @param pStrList + * @param index + * @return `unsigned long long` + */ +unsigned long long strlist_item_as_ulong_long(StrList *pStrList, size_t index) { + return (unsigned long long)atoll(strlist_item(pStrList, index)); +} + +/** + * Convert value at index to `float` + * @param pStrList + * @param index + * @return `float` + */ +float strlist_item_as_float(StrList *pStrList, size_t index) { + return (float)atof(strlist_item(pStrList, index)); +} + +/** + * Convert value at index to `double` + * @param pStrList + * @param index + * @return `double` + */ +double strlist_item_as_double(StrList *pStrList, size_t index) { + return atof(strlist_item(pStrList, index)); +} + +/** + * Convert value at index to `long double` + * @param pStrList + * @param index + * @return `long double` + */ +long double strlist_item_as_long_double(StrList *pStrList, size_t index) { + return (long double)atof(strlist_item(pStrList, index)); +} + +/** + * Initialize an empty `StrList` + * @return `StrList` + */ +StrList *strlist_init() { + StrList *pStrList = calloc(1, sizeof(StrList)); + if (pStrList == NULL) { + perror("failed to allocate array"); + exit(errno); + } + pStrList->num_inuse = 0; + pStrList->num_alloc = 1; + pStrList->data = calloc(pStrList->num_alloc, sizeof(char *)); + return pStrList; +} diff --git a/lib/user_input.c b/lib/user_input.c new file mode 100644 index 0000000..3f358fa --- /dev/null +++ b/lib/user_input.c @@ -0,0 +1,75 @@ +#include "spm.h" + +/** + * Basic case-insensitive interactive choice function + * @param answer + * @param answer_default + * @return + */ +int spm_user_yesno(int answer, int empty_input_is_yes) { + int result = 0; + answer = tolower(answer); + + if (answer == 'y') { + result = 1; + } else if (answer == 'n') { + result = 0; + } else { + if (empty_input_is_yes) { + result = 1; + } else { + result = -1; + } + } + + return result; +} + +int spm_prompt_user(const char *msg, int empty_input_is_yes) { + int user_choice = 0; + int status_choice = 0; + char ch_yes = 'y'; + char ch_no = 'n'; + + if (empty_input_is_yes) { + ch_yes = 'Y'; + } else { + ch_no = 'N'; + } + + printf("\n%s [%c/%c] ", msg, ch_yes, ch_no); + while ((user_choice = getchar())) { + status_choice = spm_user_yesno(user_choice, 1); + if (status_choice == 0) { // No + break; + } else if (status_choice == 1) { // Yes + break; + } else { // Only triggers when spm_user_yesno's second argument is zero + puts("Please answer 'y' or 'n'..."); + } + } + puts(""); + + return status_choice; +} + + +void spm_user_yesno_test() { + int choice; + int status; + while ((choice = getchar())) { + status = spm_user_yesno(choice, 1); + if (status == -1) { + puts("Please answer Y or N"); + continue; + } + else if (status == 0) { + puts("You answered no"); + break; + } + else if (status == 1) { + puts("You answered yes"); + break; + } + } +} diff --git a/lib/version_spec.c b/lib/version_spec.c new file mode 100644 index 0000000..06fcd1b --- /dev/null +++ b/lib/version_spec.c @@ -0,0 +1,445 @@ +/** + * @file version_spec.c + */ +#include "spm.h" + +/** + * + * @param str + * @return + */ +char *version_suffix_get_alpha(char *str) { + size_t i; + size_t len = strlen(str); + for (i = 0; i < len; i++) { + // return pointer to the first alphabetic character we find + if (isalpha(str[i])) { + return &str[i]; + } + } + return NULL; +} + +/** + * + * @param str + * @return + */ +char *version_suffix_get_modifier(char *str) { + size_t i; + char *modifiers[] = { + "rc", + "pre", + "dev", + "post", + NULL, + }; + for (i = 0; i < strlen(str); i++) { + for (int m = 0; modifiers[m] != NULL; m++) { + if (strncasecmp(&str[i], modifiers[m], strlen(modifiers[m])) == 0) { + return &str[i]; + } + } + } + return NULL; +} + +/** + * + * @param str + * @return + */ +int64_t version_suffix_modifier_calc(char *str) { + int64_t result = 0; + char *tmp_s = str; + + if (strncasecmp(str, "rc", 2) == 0) { + // do rc + tmp_s += strlen("rc"); + if (isdigit(*tmp_s)) { + result -= atoi(tmp_s); + } + else { + result -= 1; + } + } + else if (strncasecmp(str, "pre", 3) == 0) { + // do pre + tmp_s += strlen("pre"); + if (isdigit(*tmp_s)) { + result -= atoi(tmp_s); + } + else { + result -= 1; + } + } + else if (strncasecmp(str, "dev", 3) == 0) { + // do dev + tmp_s += strlen("dev"); + if (isdigit(*tmp_s)) { + result -= atoi(tmp_s); + } + else { + result -= 1; + } + } + else if (strncasecmp(str, "post", 4) == 0) { + // do post + tmp_s += strlen("post"); + if (isdigit(*tmp_s)) { + result += atoi(tmp_s); + } + else { + result += 1; + } + } + + return result; +} + +/** + * + * @param str + * @return + */ +int version_suffix_alpha_calc(char *str) { + int x = 0; + char chs[255]; + char *ch = chs; + memset(chs, '\0', sizeof(chs)); + strncpy(chs, str, strlen(str)); + + // Handle cases where the two suffixes are not delimited by anything + // Start scanning one character ahead of the alphabetic suffix and terminate the string + // when/if we reach another alphabetic character (presumably a version modifer) + for (int i = 1; chs[i] != '\0'; i++) { + if (isalpha(chs[i])) { + chs[i] = '\0'; + } + } + + // Convert character to hex-ish + x = (*ch - 'a') + 0xa; + + // Ensure the string ends with a digit + if (strlen(str) == 1) { + strcat(ch, "0"); + } + + // Convert trailing numerical value to an integer + while (*ch != '\0') { + if (!isdigit(*ch)) { + ch++; + continue; + } + x += atoi(ch); + break; + } + + return x; +} + +/** + * + * @param version_str + * @return + */ +int64_t version_from(const char *version_str) { + const char *delim = "."; + int64_t result = 0; + if (version_str == NULL) { + return 0; + } + + int seen_alpha = 0; // Does the tail contain a single character, but not a modifier? + int seen_modifier = 0; // Does the tail contain "rc", "dev", "pre", and so forth? + char head[255]; // digits of the string + char tail[255]; // alphabetic characters of the string + char *suffix_alpha = NULL; // pointer to location of the first character after the version + char *suffix_modifier = NULL; // pointer to location of the modifier after the version + char *x = NULL; // pointer to each string delimited by "." + char *vstr = strdup(version_str); + if (!vstr) { + perror("Version string copy"); + return -1; + } + + memset(head, '\0', sizeof(head)); + memset(tail, '\0', sizeof(tail)); + + // Split the version into parts + while ((x = strsep(&vstr, delim)) != NULL) { + int64_t tmp = 0; + + // populate the head (numeric characters) + strncpy(head, x, strlen(x)); + for (size_t i = 0; i < strlen(head); i++) { + if (isalpha(head[i])) { + // populate the tail (alphabetic characters) + strncpy(tail, &head[i], strlen(&head[i])); + head[i] = '\0'; + break; + } + } + + // Detect alphabetic suffix + if (!seen_alpha) { + if ((suffix_alpha = version_suffix_get_alpha(x)) != NULL) { + seen_alpha = 1; + } + } + + // Detect modifier suffix + if (!seen_modifier) { + if ((suffix_modifier = version_suffix_get_modifier(x)) != NULL) { + seen_modifier = 1; + } + } + + // Stop processing if the head starts with something other than numbers + if (!isdigit(head[0])) { + break; + } + + // Convert the head to an integer + tmp = atoi(head); + // Update result. Each portion of the numeric version is its own byte + // Version PARTS are limited to 255 + result = result << 8 | tmp; + } + + if (suffix_alpha != NULL) { + // Convert the alphabetic suffix to an integer + int64_t sac = version_suffix_alpha_calc(suffix_alpha); + result += sac; + } + + if (suffix_modifier != NULL) { + // Convert the modifier string to an integer + int64_t smc = version_suffix_modifier_calc(suffix_modifier); + if (smc < 0) { + result -= ~smc + 1; + } + else { + result += smc; + } + } + + free(vstr); + return result; +} + +/** + * + * @param op + * @return + */ +int version_spec_from(const char *op) { + int flags = VERSION_NOOP; + size_t len = strlen(op); + for (size_t i = 0; i < len; i++) { + if (op[i] == '>') { + flags |= VERSION_GT; + } + else if (op[i] == '<') { + flags |= VERSION_LT; + } + else if (op[i] == '=' || (len > 1 && strncmp(&op[i], "==", 2) == 0)) { + flags |= VERSION_EQ; + } + else if (op[i] == '!') { + flags |= VERSION_NE; + } + else if (op[i] == '~') { + flags |= VERSION_COMPAT; + } + } + return flags; +} + +/** + * + * @param a + * @param b + * @return + */ +static int _find_by_spec_compare(const void *a, const void *b) { + const ManifestPackage *aa = *(const ManifestPackage**)a; + const ManifestPackage *bb = *(const ManifestPackage**)b; + int64_t version_a = version_from(aa->version); + int64_t version_b = version_from(bb->version); + return version_a > version_b; +} + +/** + * + * @param manifest + * @param name + * @param op + * @param version_str + * @return + */ +ManifestPackage **find_by_spec(Manifest *manifest, const char *name, const char *op, const char *version_str) { + size_t record = 0; + ManifestPackage **list = (ManifestPackage **) calloc(manifest->records + 1, sizeof(ManifestPackage *)); + if (!list) { + perror("ManifestPackage array"); + fprintf(SYSERROR); + return NULL; + } + + for (size_t i = 0; i < manifest->records; i++) { + if (strcmp(manifest->packages[i]->name, name) == 0) { + int64_t version_a = version_from(manifest->packages[i]->version); + int64_t version_b = version_from(version_str); + int spec = version_spec_from(op); + + int res = 0; + if (spec & VERSION_GT && spec & VERSION_EQ) { + res = version_a >= version_b; + } + else if (spec & VERSION_LT && spec & VERSION_EQ) { + res = version_a <= version_b; + } + else if (spec & VERSION_NE && spec & VERSION_EQ) { + res = version_a != version_b; + } + else if (spec & VERSION_GT) { + res = version_a > version_b; + } + else if (spec & VERSION_LT) { + res = version_a < version_b; + } + else if (spec & VERSION_COMPAT) { + // TODO + } + else if (spec & VERSION_EQ) { + res = version_a == version_b; + } + + if (res != 0) { + list[record] = manifest_package_copy(manifest->packages[i]); + if (!list[record]) { + perror("Unable to allocate memory for manifest record"); + fprintf(SYSERROR); + return NULL; + } + record++; + } + } + } + qsort(list, record, sizeof(ManifestPackage *), _find_by_spec_compare); + + return list; +} + +static void get_name(char **buf, const char *_str) { + char *str = strdup(_str); + int has_relational = 0; + int is_archive = endswith(str, SPM_PACKAGE_EXTENSION); + for (size_t i = 0; str[i] != '\0'; i++) { + if (isrelational(str[i])) + has_relational = 1; + } + + if (is_archive == 0 && !has_relational) { + strcpy((*buf), str); + } + else if (has_relational) { + size_t stop = 0; + for (stop = 0; !isrelational(str[stop]); stop++); + strncpy((*buf), str, stop); + (*buf)[stop] = '\0'; + } else { + StrList *tmp = strlist_init(); + char sep[2]; + sep[0] = SPM_PACKAGE_MEMBER_SEPARATOR; + sep[1] = '\0'; + + char **parts = split(str, sep); + if (parts != NULL) { + for (size_t i = 0; parts[i] != NULL; i++) { + strlist_append(tmp, parts[i]); + } + } + split_free(parts); + + if (strlist_count(tmp) > SPM_PACKAGE_MIN_DELIM) { + strlist_set(tmp, strlist_count(tmp) - SPM_PACKAGE_MIN_DELIM, NULL); + } + char *result = join(tmp->data, sep); + strcpy((*buf), result); + free(result); + strlist_free(tmp); + } + free(str); +} + +static char *get_operators(char **op, const char *_strspec) { + const char *operators = VERSION_OPERATORS; // note: whitespace is synonymous with ">=" if no operators are present + char *pos = NULL; + pos = strpbrk(_strspec, operators); + if (pos != NULL) { + for (size_t i = 0; !isalnum(*pos) || *pos == '.'; i++) { + (*op)[i] = *pos++; + } + } + return pos; +} + +ManifestPackage *find_by_strspec(Manifest *manifest, const char *_strspec) { + char *pos = NULL; + char s_op[NAME_MAX]; + char s_name[NAME_MAX]; + char s_version[NAME_MAX]; + char *op = s_op; + char *name = s_name; + char *version = s_version; + char *strspec = strdup(_strspec); + + memset(op, '\0', NAME_MAX); + memset(name, '\0', NAME_MAX); + memset(version, '\0', NAME_MAX); + + // Parse name + //for (size_t i = 0; isalnum(_strspec[i]) || _strspec[i] == '_' || _strspec[i] == '-'; i++) { + // name[i] = _strspec[i]; + //} + get_name(&name, strspec); + pos = get_operators(&op, strspec); + + + ManifestPackage **m = NULL; + // No operators found + if (pos == NULL) { + m = find_by_spec(manifest, name, ">=", NULL); + } + + // When `m` is still NULL after applying the default operator + if (m == NULL) { + for (size_t i = 0; *(pos + i) != '\0'; i++) { + version[i] = *(pos + i); + } + m = find_by_spec(manifest, name, op, version); + } + + // When `m` has been populated by either test above, return a COPY of the manifest + if (m != NULL) { + ManifestPackage *selected = NULL; + for (size_t i = 0; m[i] != NULL; i++) { + selected = m[i]; + } + + ManifestPackage *result = manifest_package_copy(selected); + for (size_t i = 0; m[i] != NULL; i++) { + manifest_package_free(m[i]); + } + free(m); + free(strspec); + return result; + } + + // Obviously it didn't work out + free(strspec); + return NULL; +} |