diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/CMakeLists.txt | 17 | ||||
-rw-r--r-- | src/archive.c | 76 | ||||
-rw-r--r-- | src/compat.c | 16 | ||||
-rw-r--r-- | src/config.c | 163 | ||||
-rw-r--r-- | src/config_global.c | 186 | ||||
-rw-r--r-- | src/deps.c | 188 | ||||
-rw-r--r-- | src/find.c | 154 | ||||
-rw-r--r-- | src/fs.c | 264 | ||||
-rw-r--r-- | src/install.c | 63 | ||||
-rw-r--r-- | src/relocation.c | 237 | ||||
-rw-r--r-- | src/rpath.c | 233 | ||||
-rw-r--r-- | src/shell.c | 112 | ||||
-rw-r--r-- | src/spm.c | 60 | ||||
-rw-r--r-- | src/strings.c | 431 |
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; +} |