aboutsummaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorJoseph Hunkeler <jhunkeler@gmail.com>2020-03-18 22:25:27 -0400
committerJoseph Hunkeler <jhunkeler@gmail.com>2020-03-18 22:25:27 -0400
commitccaeb7092b5ad40b1b3833c987ba3ec4d47f0bb8 (patch)
treeae167772a9a2343aa77bf8944b56abe853f6a2ec /lib
parent3731bb4679ee8716d4f579d5744c36a2d1b4a257 (diff)
downloadspmc-ccaeb7092b5ad40b1b3833c987ba3ec4d47f0bb8.tar.gz
Refactor project: build/install libspm[_static.a].so to make unit testing possible
Diffstat (limited to 'lib')
-rw-r--r--lib/CMakeLists.txt57
-rw-r--r--lib/archive.c110
-rw-r--r--lib/checksum.c42
-rw-r--r--lib/compat.c23
-rw-r--r--lib/config.c227
-rw-r--r--lib/config_global.c375
-rw-r--r--lib/environment.c406
-rw-r--r--lib/extern/url.c655
-rw-r--r--lib/find.c151
-rw-r--r--lib/fs.c504
-rw-r--r--lib/install.c328
-rw-r--r--lib/internal_cmd.c401
-rw-r--r--lib/manifest.c668
-rw-r--r--lib/metadata.c166
-rw-r--r--lib/mime.c156
-rw-r--r--lib/mirrors.c180
-rw-r--r--lib/purge.c93
-rw-r--r--lib/relocation.c440
-rw-r--r--lib/resolve.c65
-rw-r--r--lib/rpath.c303
-rw-r--r--lib/shell.c116
-rw-r--r--lib/shlib.c72
-rw-r--r--lib/spm_build.c23
-rw-r--r--lib/str.c699
-rw-r--r--lib/strlist.c339
-rw-r--r--lib/user_input.c75
-rw-r--r--lib/version_spec.c445
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;
+}