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; +}  | 
