aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorJoseph Hunkeler <jhunkeler@gmail.com>2019-12-18 21:57:51 -0500
committerJoseph Hunkeler <jhunkeler@gmail.com>2019-12-18 21:57:51 -0500
commit534657dd6fc2ee98159e41d2700554fed0da2c4e (patch)
treed9316757b060d68e5640ea9ecb56b29c62adedfc /src
parent3166627f1485e2f05421fe874b2236852fe5d017 (diff)
downloadspmc-534657dd6fc2ee98159e41d2700554fed0da2c4e.tar.gz
Refactor project structure
Diffstat (limited to 'src')
-rw-r--r--src/CMakeLists.txt17
-rw-r--r--src/archive.c76
-rw-r--r--src/compat.c16
-rw-r--r--src/config.c163
-rw-r--r--src/config_global.c186
-rw-r--r--src/deps.c188
-rw-r--r--src/find.c154
-rw-r--r--src/fs.c264
-rw-r--r--src/install.c63
-rw-r--r--src/relocation.c237
-rw-r--r--src/rpath.c233
-rw-r--r--src/shell.c112
-rw-r--r--src/spm.c60
-rw-r--r--src/strings.c431
14 files changed, 2200 insertions, 0 deletions
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
new file mode 100644
index 0000000..009eb92
--- /dev/null
+++ b/src/CMakeLists.txt
@@ -0,0 +1,17 @@
+include_directories(
+ ${CMAKE_SOURCE_DIR}/include
+ ${CMAKE_BINARY_DIR}/include
+)
+
+add_executable(spm spm.c config.c compat.c deps.c fs.c rpath.c find.c shell.c archive.c strings.c relocation.c install.c config_global.c)
+target_link_libraries(spm rt)
+install(
+ TARGETS spm
+ DESTINATION ${CMAKE_INSTALL_PREFIX}/bin
+)
+install(
+ FILES
+ ${CMAKE_BINARY_DIR}/include/config.h
+ ${CMAKE_SOURCE_DIR}/include/spm.h
+ DESTINATION "${CMAKE_INSTALL_PREFIX}/include/${PROJECT_NAME}"
+)
diff --git a/src/archive.c b/src/archive.c
new file mode 100644
index 0000000..7ec9d04
--- /dev/null
+++ b/src/archive.c
@@ -0,0 +1,76 @@
+#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];
+
+ sprintf(cmd, "tar xf %s -C %s %s 2>&1", archive, destination, filename);
+ if (exists(archive) != 0) {
+ fprintf(stderr, "%s :: ", archive);
+ fprintf(SYSERROR);
+ return -1;
+ }
+
+ shell(&proc, SHELL_OUTPUT, cmd);
+ if (!proc) {
+ fprintf(SYSERROR);
+ return -1;
+ }
+
+ status = proc->returncode;
+ shell_free(proc);
+
+ return status;
+}
+
+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, "&;|");
+ // sanitize destination
+ strchrdel(destination, "&;|");
+
+ 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/src/compat.c b/src/compat.c
new file mode 100644
index 0000000..082a602
--- /dev/null
+++ b/src/compat.c
@@ -0,0 +1,16 @@
+#include <string.h>
+#include "config.h"
+
+#ifndef HAVE_STRSEP
+// 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 \ No newline at end of file
diff --git a/src/config.c b/src/config.c
new file mode 100644
index 0000000..93ff673
--- /dev/null
+++ b/src/config.c
@@ -0,0 +1,163 @@
+/**
+ * @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.
+ *
+ * @param filename
+ * @return success=`ConfigItem` array, failure=NULL
+ */
+ConfigItem **config_read(const char *filename) {
+ const char sep = '=';
+ char *line = (char *)calloc(CONFIG_BUFFER_SIZE, sizeof(char));
+ FILE *fp = fopen(filename, "r");
+ if (!fp) {
+ // errno will be set, so die, and let the caller handle it
+ return NULL;
+ }
+ int record_initial = 2;
+ ConfigItem **config = (ConfigItem **) calloc(record_initial, sizeof(ConfigItem *));
+ int record = 0;
+
+ while (fgets(line, CONFIG_BUFFER_SIZE, 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 %d: 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));
+
+ // 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);
+
+ // 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);
+
+ // increment record count
+ record++;
+ // Expand config by another record
+ config = (ConfigItem **)reallocarray(config, record + record_initial + 1, sizeof(ConfigItem *));
+ }
+ free(line);
+ return config;
+}
+
+/**
+ * Free memory allocated by `config_read`
+ * @param item `ConfigItem` array
+ */
+void config_free(ConfigItem **item) {
+ for (int i = 0; item[i] != NULL; i++) {
+ free(item[i]);
+ }
+ free(item);
+}
+
+/**
+ * If the configuration contains `key` return a pointer to that record
+ * @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 (int 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/src/config_global.c b/src/config_global.c
new file mode 100644
index 0000000..b1c6dc2
--- /dev/null
+++ b/src/config_global.c
@@ -0,0 +1,186 @@
+#include "spm.h"
+
+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;
+}
+
+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);
+}
+
+char *get_user_tmp_dir(void) {
+ char template[PATH_MAX];
+ char *ucd = get_user_conf_dir();
+ sprintf(template, "%s%ctmp", ucd, DIRSEP);
+
+ if (access(template, F_OK) != 0) {
+ if (mkdirs(template, 0755) != 0) {
+ return NULL;
+ }
+ }
+
+ free(ucd);
+ return strdup(template);
+}
+
+char *get_user_package_dir(void) {
+ char template[PATH_MAX];
+ char *ucd = get_user_conf_dir();
+ sprintf(template, "%s%cpkgs", ucd, DIRSEP);
+
+ if (access(template, F_OK) != 0) {
+ if (mkdirs(template, 0755) != 0) {
+ return NULL;
+ }
+ }
+
+ free(ucd);
+ return strdup(template);
+}
+
+/**
+ * Check whether SPM has access to external programs it needs
+ */
+void check_runtime_environment(void) {
+ int bad_rt = 0;
+ char *required[] = {
+ "patchelf",
+ "rsync",
+ "tar",
+ "bash",
+ "reloc",
+ NULL,
+ };
+ 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);
+ }
+}
+
+
+
+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.config = NULL;
+
+ if (uname(&SPM_GLOBAL.sysinfo) != 0) {
+ fprintf(SYSERROR);
+ exit(1);
+ }
+
+ 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 temp directory
+ item = config_get(SPM_GLOBAL.config, "tmp_dir");
+ if (item) {
+ SPM_GLOBAL.tmp_dir = 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 = item->value;
+ 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();
+ }
+}
+
+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.user_config_basedir) {
+ free(SPM_GLOBAL.user_config_basedir);
+ }
+ if (SPM_GLOBAL.user_config_file) {
+ free(SPM_GLOBAL.user_config_file);
+ }
+ if (SPM_GLOBAL.config) {
+ config_free(SPM_GLOBAL.config);
+ }
+}
+
+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("\n");
+}
diff --git a/src/deps.c b/src/deps.c
new file mode 100644
index 0000000..99f6148
--- /dev/null
+++ b/src/deps.c
@@ -0,0 +1,188 @@
+//
+// Created by jhunk on 12/16/19.
+//
+#include "spm.h"
+
+int exists(const char *filename) {
+ return access(filename, F_OK);
+}
+
+int dep_seen(Dependencies **deps, const char *name) {
+ if (!deps) {
+ return -1;
+ }
+ for (int i = 0; i != (*deps)->records; i++) {
+ if (strstr((*deps)->list[i], name) != NULL) {
+ return 1;
+ }
+ }
+ return 0;
+}
+
+int dep_init(Dependencies **deps) {
+ (*deps) = (Dependencies *)calloc(1, sizeof(Dependencies));
+ (*deps)->__size = 2;
+ (*deps)->records = 0;
+ (*deps)->list = (char **)calloc((*deps)->__size, sizeof(char *));
+ if (!(*deps)->list) {
+ return -1;
+ }
+ return 0;
+}
+
+void dep_free(Dependencies **deps) {
+ if ((*deps) != NULL) {
+ return;
+ }
+ for (int i = 0; i < (*deps)->__size; i++) {
+ if ((*deps)->list[i] != NULL) {
+ free((*deps)->list[i]);
+ }
+ }
+ free((*deps));
+}
+
+int dep_append(Dependencies **deps, char *_name) {
+ char *name = NULL;
+ char *bname = NULL;
+
+ if (!(*deps)) {
+ return -1;
+ }
+
+ name = find_package(_name);
+ if (!name) {
+ perror(_name);
+ fprintf(SYSERROR);
+ return -1;
+ }
+
+ bname = basename(name);
+ if (!bname) {
+ perror(name);
+ fprintf(SYSERROR);
+ return -1;
+ }
+
+ (*deps)->__size++;
+ (*deps)->list = (char **)realloc((*deps)->list, sizeof(char *) * (*deps)->__size);
+ if (!(*deps)->list) {
+ free(name);
+ return -1;
+ }
+
+ (*deps)->list[(*deps)->records] = (char *)calloc(strlen(bname) + 1, sizeof(char));
+ if (!(*deps)->list[(*deps)->records]) {
+ free(name);
+ return -1;
+ }
+
+ strcpy((*deps)->list[(*deps)->records], bname);
+ (*deps)->records++;
+
+ free(name);
+ return 0;
+}
+
+int dep_solve(Dependencies **deps, const char *filename) {
+ if (!(*deps)) {
+ return -1;
+ }
+ if (exists(filename) != 0) {
+ return -1;
+ }
+ FILE *fp = fopen(filename, "r");
+ if (!fp) {
+ perror(filename);
+ return -1;
+ }
+
+ char data[BUFSIZ];
+ memset(data, '\0', sizeof(data));
+
+ char *line = data;
+ int line_count = 0;
+ while (fgets(line, BUFSIZ, fp) != NULL) {
+ size_t line_length = strlen(line);
+ if (line_length > 1 && strstr(line, "\r\n")) {
+ line[line_length - 2] = '\0';
+ }
+ if (line_length > 1 && line[line_length - 1] == '\n') {
+ line[line_length - 1] = '\0';
+ }
+ if (strcmp(line, "") == 0) {
+ continue;
+ }
+ line_count++;
+ if (dep_seen(deps, line) > 0) {
+ // Already seen this dependency. Skip it.
+ continue;
+ }
+ else {
+ // Have not seen this dependency before
+ if (dep_append(deps, line) == 0) {
+ dep_solve(deps, line);
+ }
+ }
+ }
+ fclose(fp);
+ return line_count;
+}
+
+int dep_all(Dependencies **deps, const char *_package) {
+ static int next = 0;
+ char *package = NULL;
+ char depfile[PATH_MAX];
+ char template[PATH_MAX];
+ char suffix[PATH_MAX] = "spm_depends_all_XXXXXX";
+
+ // Verify the requested package pattern exists
+ package = find_package(_package);
+ if (!package) {
+ perror(_package);
+ fprintf(SYSERROR);
+ return -1;
+ }
+
+ // Create a new temporary directory and extract the requested package into it
+ sprintf(template, "%s%c%s", TMP_DIR, DIRSEP, suffix);
+ char *tmpdir = mkdtemp(template);
+ if (!tmpdir) {
+ perror(template);
+ fprintf(SYSERROR);
+ return -1;
+ }
+ if (tar_extract_file(package, ".SPM_DEPENDS", tmpdir) < 0) {
+ perror(package);
+ fprintf(SYSERROR);
+ return -1;
+ }
+
+ // Scan depencency tree
+ sprintf(depfile, "%s%c%s", tmpdir, DIRSEP, ".SPM_DEPENDS");
+ int resolved = dep_solve(deps, depfile);
+
+ // NOTE:
+ // 1. `resolved` is the number of dependencies for the package we're scanning
+ // 2. `next` permits us to converge on `resolved`, otherwise `i` would reset to `0` each time `dep_all` is called
+ for (int i = next; i < resolved; i++) {
+ next++;
+ if (dep_seen(deps, (*deps)->list[i])) {
+ dep_all(deps, (*deps)->list[i]);
+ }
+ }
+
+ // Remove temporary data
+ unlink(depfile);
+ unlink(tmpdir);
+ return 0;
+}
+
+void dep_show(Dependencies **deps) {
+ if ((*deps) == NULL) {
+ return;
+ }
+ for (int i = 0; i < (*deps)->records; i++) {
+ printf("%d: %s\n", i, (*deps)->list[i]);
+ }
+}
diff --git a/src/find.c b/src/find.c
new file mode 100644
index 0000000..84ded16
--- /dev/null
+++ b/src/find.c
@@ -0,0 +1,154 @@
+#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 *rootpath = NULL;
+ char *path = NULL;
+
+ // GUARD
+ if (!root || !filename || strstr(filename, "..") || strstr(filename, "./")) {
+ return NULL;
+ }
+
+ if (!(path = (char *)calloc(PATH_MAX + 1, sizeof(char)))) {
+ fprintf(SYSERROR);
+ exit(errno);
+ }
+
+ if (!(rootpath = realpath(root, NULL))) {
+ return NULL;
+ }
+
+ strcat(path, rootpath);
+ strcat(path, "/");
+ strcat(path, filename);
+
+ // Save a little time if the file exists
+ if (access(path, F_OK) != -1) {
+ return 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);
+ }
+ return NULL;
+ }
+
+ // Resize path to the length of the first match
+ char *want = results.gl_pathv[0];
+ if (!(path = (char *)realloc(path, sizeof(char) * (strlen(want) + 1)))) {
+ fprintf(SYSERROR);
+ exit(errno);
+ }
+
+ // Replace path string with wanted path string
+ strncpy(path, want, strlen(want));
+
+ free(rootpath);
+ globfree(&results);
+ return 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) {
+ return find_file(PKG_DIR, filename);
+}
+
+/**
+ * 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 < 0) {
+ return -1;
+ }
+ char *buffer = (char *)calloc((size_t) file_len, sizeof(char));
+ if (!buffer) {
+ 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 < file_len; i++) {
+ if (!memcmp(&buffer[i], pattern, pattern_len)) {
+ 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/src/fs.c b/src/fs.c
new file mode 100644
index 0000000..27bdf2f
--- /dev/null
+++ b/src/fs.c
@@ -0,0 +1,264 @@
+#include "spm.h"
+
+FSTree *fstree(const char *_path) {
+ FTS *parent = NULL;
+ FTSENT *node = NULL;
+ FSTree *fsdata = NULL;
+ char *path = realpath(_path, NULL);
+ char *root[2] = { path, NULL };
+
+ 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(strlen(path) + 1, sizeof(char));
+ fsdata->dirs = (char **)calloc(dirs_size, sizeof(char *));
+ fsdata->files= (char **)calloc(files_size, sizeof(char *));
+
+ strncpy(fsdata->root, path, strlen(path));
+ parent = fts_open(root, FTS_PHYSICAL | FTS_NOCHDIR, &_fstree_compare);
+
+ if (parent != NULL) {
+ while ((node = fts_read(parent)) != NULL) {
+ 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);
+ return fsdata;
+}
+
+int _fstree_compare(const FTSENT **one, const FTSENT **two) {
+ return (strcmp((*one)->fts_name, (*two)->fts_name));
+}
+
+int rmdirs(const char *_path) {
+ if (access(_path, F_OK) != 0) {
+ return -1;
+ }
+
+ FSTree *data = fstree(_path);
+ if (data->files) {
+ for (int i = 0; data->files[i] != NULL; i++) {
+ remove(data->files[i]);
+ }
+ }
+ if (data->dirs) {
+ for (int i = data->dirs_length - 1; i != 0; i--) {
+ remove(data->dirs[i]);
+ }
+ }
+ remove(data->root);
+
+ fstree_free(data);
+ return 0;
+}
+
+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]);
+ }
+ }
+ if (fsdata->dirs != NULL) {
+ for (int i = 0; fsdata->dirs[i] != NULL; i++) {
+ free(fsdata->dirs[i]);
+ }
+ }
+ free(fsdata);
+ }
+}
+
+/**
+ * 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.
+ *
+ * @param path a system path
+ * @return string (caller is responsible for `free`ing memory)
+ */
+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) {
+ 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 = strrchr(path, DIRSEP);
+ if (!last) {
+ return NULL;
+ }
+
+ // Perform a lookahead ensuring the string is valid beyond the last separator
+ if ((last + 1) != NULL) {
+ result = last + 1;
+ }
+
+ 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[PATH_MAX];
+
+ memset(cmd, '\0', sizeof(cmd));
+ memset(args_combined, '\0', sizeof(args_combined));
+ strcpy(args_combined, "--archive --hard-links ");
+ if (args) {
+ strcat(args_combined, _args);
+ }
+
+ sprintf(cmd, "rsync %s \"%s\" \"%s\"", args_combined, source, destination);
+ // sanitize command
+ strchrdel(cmd, "&;|");
+ 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, proc->output);
+ }
+ shell_free(proc);
+
+ if (args) {
+ free(args);
+ }
+ free(source);
+ free(destination);
+ return returncode;
+}
+
+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;
+}
diff --git a/src/install.c b/src/install.c
new file mode 100644
index 0000000..e647d31
--- /dev/null
+++ b/src/install.c
@@ -0,0 +1,63 @@
+#include "spm.h"
+
+int install(const char *destroot, const char *_package) {
+ char *package = find_package(_package);
+ if (!package) {
+ fprintf(SYSERROR);
+ return -1;
+ }
+
+ if (exists(destroot) != 0) {
+ if (mkdirs(destroot, 0755) != 0) {
+ fprintf(SYSERROR);
+ return -2;
+ }
+ }
+
+ char cwd[PATH_MAX];
+ char source[PATH_MAX];
+ char template[PATH_MAX];
+ char suffix[PATH_MAX] = "spm_destroot_XXXXXX";
+ sprintf(template, "%s%c%s", TMP_DIR, DIRSEP, suffix);
+
+ // Create a new temporary directory and extract the requested package into it
+ char *tmpdir = mkdtemp(template);
+ tar_extract_archive(package, tmpdir);
+
+ getcwd(cwd, sizeof(cwd));
+
+ RelocationEntry **b_record = NULL;
+ RelocationEntry **t_record = NULL;
+ chdir(tmpdir);
+ {
+ // Rewrite binary prefixes
+ RelocationEntry **b_record = prefixes_read(".SPM_PREFIX_BIN");
+ if (b_record) {
+ for (int i = 0; b_record[i] != NULL; i++) {
+ relocate(b_record[i]->path, b_record[i]->prefix, destroot);
+ }
+ }
+
+ // Rewrite text prefixes
+ RelocationEntry **t_record = prefixes_read(".SPM_PREFIX_TEXT");
+ if (t_record) {
+ for (int i = 0; t_record[i] != NULL; i++) {
+ file_replace_text(t_record[i]->path, t_record[i]->prefix, destroot);
+ }
+ }
+
+ prefixes_free(b_record);
+ prefixes_free(t_record);
+ }
+ chdir(cwd);
+
+
+ // Append a trailing slash to tmpdir to direct rsync to copy files, not the directory, into destroot
+ sprintf(source, "%s%c", tmpdir, DIRSEP);
+ if (rsync(NULL, source, destroot) != 0) {
+ exit(1);
+ }
+ rmdirs(tmpdir);
+
+ free(package);
+}
diff --git a/src/relocation.c b/src/relocation.c
new file mode 100644
index 0000000..13ae799
--- /dev/null
+++ b/src/relocation.c
@@ -0,0 +1,237 @@
+#include "spm.h"
+
+int replace_text(char *data, const char *spattern, const char *sreplacement) {
+ 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\n");
+ return -1;
+ }
+
+ while (*tmp != '\0') {
+ if (strncmp(tmp, spattern, spattern_len) == 0) {
+ memmove(tmp, sreplacement, sreplacement_len);
+ memmove(tmp + sreplacement_len, tmp + spattern_len, data_len - spattern_len);
+ }
+ tmp++;
+ }
+ return 0;
+}
+
+/**
+ * Replace all occurrences of `oldstr` in file `path` with `newstr`
+ * @param filename
+ * @param oldstr
+ * @param newstr
+ * @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) {
+ 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
+ * @return success=array of RelocationEntry, failure=NULL
+ */
+RelocationEntry **prefixes_read(const char *filename) {
+ size_t i = 0;
+ int record_count = 0;
+ int parity = 0;
+ FILE *fp = fopen(filename, "r");
+ if (!fp) {
+ fprintf(SYSERROR);
+ return NULL;
+ }
+ RelocationEntry **entry = NULL;
+ char line[BUFSIZ];
+ memset(line, '\0', BUFSIZ);
+
+ while (fgets(line, BUFSIZ, fp) != NULL) {
+ if (isempty(line)) {
+ 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: %d %% 2 = %d)\n", filename, record_count, parity);
+ return NULL;
+ }
+ record_count /= 2;
+
+ entry = (RelocationEntry **)calloc(record_count + 1, sizeof(RelocationEntry *));
+ if (!entry) {
+ return NULL;
+ }
+ for (int 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;
+ while (fgets(line, BUFSIZ, fp) != NULL) {
+ char *wtf = line;
+ if (isempty(line)) {
+ continue;
+ }
+ if (startswith(line, "#") == 0) {
+ 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(line) + 1, sizeof(char));
+ if (!entry[i]->prefix) {
+ fclose(fp);
+ return NULL;
+ }
+ strncpy(entry[i]->prefix, line, strlen(line));
+ // 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(line) + 1, sizeof(char));
+ if (!entry[i]->path) {
+ fclose(fp);
+ return NULL;
+ }
+ strncpy(entry[i]->path, line, strlen(line));
+ entry[i]->path = strip(entry[i]->path);
+ do_path = 0;
+ }
+ i++;
+ }
+ fclose(fp);
+ return entry;
+}
+
+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];
+
+ memset(cmd, '\0', sizeof(cmd));
+ sprintf(cmd, "reloc \"%s\" \"%s\" \"%s\" \"%s\"", oldstr, newstr, filename, filename);
+
+ // sanitize command
+ strchrdel(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, proc->output);
+ }
+
+ shell_free(proc);
+ free(oldstr);
+ free(newstr);
+ free(filename);
+ return returncode;
+}
+
diff --git a/src/rpath.c b/src/rpath.c
new file mode 100644
index 0000000..f499e98
--- /dev/null
+++ b/src/rpath.c
@@ -0,0 +1,233 @@
+#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, "&;|");
+ strchrdel(filename, "&;|");
+ sprintf(sh_cmd, "patchelf %s %s", args, filename);
+
+ 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;
+ }
+
+ Process *proc_info = NULL;
+ char *rpath = NULL;
+
+ // sanitize input path
+ strchrdel(filename, "&;|");
+
+ 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 NULL;
+ }
+ char *filename = strdup(_filename);
+ if (!filename) {
+ return NULL;
+ }
+ char *path = strdup(filename);
+ if (!path) {
+ free(filename);
+ return NULL;
+ }
+
+ Process *proc_info = NULL;
+ char *rpath = NULL;
+
+ // sanitize input path
+ strchrdel(path, "&;|");
+
+ Process *pe = patchelf(filename, "--print-rpath");
+ 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) {
+ const char *origin = "$ORIGIN/";
+ char *filename = realpath(_filename, NULL);
+ if (!filename) {
+ return NULL;
+ }
+ char *nearest_lib = rpath_autodetect(filename);
+ if (!nearest_lib) {
+ return NULL;
+ }
+ char *result = (char *)calloc(strlen(origin) + strlen(nearest_lib) + 1, sizeof(char));
+ if (!result) {
+ return NULL;
+ }
+ sprintf(result, "%s%s", origin, nearest_lib);
+ free(filename);
+ free(nearest_lib);
+ return result;
+}
+
+int rpath_set(const char *filename, char *_rpath) {
+ int returncode;
+
+ char *rpath_new = rpath_generate(filename);
+ if (!rpath_new) {
+ return -1;
+ }
+
+ char *rpath_orig = rpath_get(filename);
+ if (!rpath_orig) {
+ return -1;
+ }
+
+ // Are the original and new RPATH identical?
+ if (strcmp(rpath_orig, rpath_new) == 0) {
+ free(rpath_new);
+ free(rpath_orig);
+ return 0;
+ }
+
+ Process *pe = patchelf(filename, "--set-rpath");
+ if (pe) {
+ returncode = pe->returncode;
+ }
+ shell_free(pe);
+ free(rpath_new);
+ free(rpath_orig);
+ return pe->returncode;
+}
+
+/**
+ * Using `filename` as a starting point, step backward through the filesystem looking for a lib directory
+ * @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) {
+ int has_real_libdir = 0;
+ char *rootdir = dirname(filename);
+ char *start = realpath(rootdir, NULL);
+ char *cwd = realpath(".", NULL);
+ char *result = NULL;
+
+ // Change directory to the requested root
+ chdir(start);
+
+ char visit[PATH_MAX]; // Current directory
+ char tmp[PATH_MAX]; // Current directory with lib directory appended
+ char relative[PATH_MAX]; // Generated relative path to lib directory
+ char sep[2]; // Holds the platform's directory separator
+
+ // Initialize character arrays;
+ visit[0] = '\0';
+ tmp[0] = '\0';
+ relative[0] = '\0';
+ sprintf(sep, "%c", DIRSEP);
+
+ while(1) {
+ // Where are we in the file system?
+ getcwd(visit, sizeof(visit));
+ // Using the current visit path, check if it contains a lib directory
+ sprintf(tmp, "%s%clib", visit, DIRSEP);
+ if (access(tmp, F_OK) == 0) {
+ strcat(relative, "lib");
+ has_real_libdir = 1; // gate for memory allocation below
+ break;
+ }
+ // Reaching the top of the file system indicates our search for a lib directory failed
+ else if (strcmp(visit, "/") == 0) {
+ break;
+ }
+
+ // Assemble relative path step for this location
+ strcat(relative, "..");
+ strcat(relative, sep);
+
+ // Step one directory level back
+ chdir("..");
+ }
+
+ // If we found a viable lib directory, allocate memory for it
+ if (has_real_libdir) {
+ result = (char *)calloc(strlen(relative) + 1, sizeof(char));
+ if (!result) {
+ chdir(cwd); // return to calling directory
+ return NULL;
+ }
+ // Copy character array data to the result
+ strncpy(result, relative, strlen(relative));
+ }
+
+ chdir(cwd); // return to calling directory
+ free(rootdir);
+ free(cwd);
+ free(start);
+ return result;
+}
diff --git a/src/shell.c b/src/shell.c
new file mode 100644
index 0000000..8905c60
--- /dev/null
+++ b/src/shell.c
@@ -0,0 +1,112 @@
+#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);
+
+ size_t bytes_read = 0;
+ size_t i = 0;
+ size_t new_buf_size = 0;
+ 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) {
+ 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) {
+ (*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/src/spm.c b/src/spm.c
new file mode 100644
index 0000000..3607117
--- /dev/null
+++ b/src/spm.c
@@ -0,0 +1,60 @@
+/**
+ * SPM - Simple Package Manager
+ * @file spm.c
+ */
+#include "spm.h"
+
+int main(int argc, char *argv[]) {
+ // not much to see here yet
+ // at the moment this will all be random tests, for better or worse
+ // everything here is subject to change without notice
+
+ // Initialize configuration data
+ init_config_global();
+ show_global_config();
+
+ // Ensure external programs are available for use.
+ check_runtime_environment();
+
+ // Install a package to test things out
+ char *match;
+ char *package;
+ const char *root = "/tmp/root";
+ if ((match = find_package("python")) == NULL) {
+ fprintf(SYSERROR);
+ exit(1);
+ }
+ if ((package = basename(match)) == NULL) {
+ fprintf(stderr, "Unable to derive package name from package path:\n\t-> %s\n", match);
+ exit(1);
+ }
+
+ Dependencies *deps = NULL;
+ dep_init(&deps);
+
+ if (dep_all(&deps, package) < 0) {
+ dep_free(&deps);
+ free_global_config();
+ exit(1);
+ }
+
+ printf("%s requires:\n", package);
+ dep_show(&deps);
+
+ // Install dependencies first
+ for (int i = 0; i < deps->records; i++) {
+ if (install(root, deps->list[i]) < 0) {
+ fprintf(SYSERROR);
+ exit(errno);
+ }
+ }
+ // Install package
+ if (install(root, package) < 0) {
+ fprintf(SYSERROR);
+ exit(errno);
+ }
+
+ dep_free(&deps);
+ free_global_config();
+ return 0;
+}
diff --git a/src/strings.c b/src/strings.c
new file mode 100644
index 0000000..ad784d1
--- /dev/null
+++ b/src/strings.c
@@ -0,0 +1,431 @@
+#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 0 = success, -1 = failure
+ */
+int startswith(const char *sptr, const char *pattern) {
+ for (size_t i = 0; i < strlen(pattern); i++) {
+ if (sptr[i] != pattern[i]) {
+ return -1;
+ }
+ }
+ return 0;
+}
+
+/**
+ * Scan for `pattern` string at the end of `sptr`
+ *
+ * @param sptr string to scan
+ * @param pattern string to search for
+ * @return 0 = success, -1 = failure
+ */
+int endswith(const char *sptr, const char *pattern) {
+ size_t sptr_size = strlen(sptr);
+ size_t pattern_size = strlen(pattern);
+ for (size_t s = sptr_size - pattern_size, p = 0 ; s < sptr_size; s++, p++) {
+ if (sptr[s] != pattern[p]) {
+ return -1;
+ }
+ }
+ return 0;
+}
+
+/**
+ * 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)
+{
+ 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(1, sizeof(char) * strlen(token) + 1);
+ if (!result[i]) {
+ free(sptr);
+ return NULL;
+ }
+ strncpy(result[i], token, strlen(token)); // 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);
+}
+
+/**
+ * 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=0, no=1, failure=-1
+ */
+int strstr_array(char **arr, const char *str) {
+ if (!arr) {
+ return -1;
+ }
+
+ for (int i = 0; arr[i] != NULL; i++) {
+ if (strstr(arr[i], str) != NULL) {
+ return 0;
+ }
+ }
+ return 1;
+}
+
+/**
+ * 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;
+ }
+
+ int 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;
+ int 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;
+ }
+ strncpy(result[rec], arr[i], strlen(arr[i]));
+ 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)) {
+ 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) {
+ if (!strlen(sptr)) {
+ return sptr;
+ }
+ strchrdel(sptr, " \r\n");
+ 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)) {
+ 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;
+}