diff options
Diffstat (limited to 'src/remembering-c.c')
-rw-r--r-- | src/remembering-c.c | 943 |
1 files changed, 943 insertions, 0 deletions
diff --git a/src/remembering-c.c b/src/remembering-c.c new file mode 100644 index 0000000..2dda122 --- /dev/null +++ b/src/remembering-c.c @@ -0,0 +1,943 @@ +#ifndef _POSIX_C_SOURCE +#define _POSIX_C_SOURCE 200809L +#endif + +/* +#include <errno.h> +#include <libgen.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/stat.h> +#include <unistd.h> + +#ifdef FALLIBLE +#include <fallible.h> +#include <fallible/alloc.h> +#include <fallible/string.h> +#endif + +#ifdef TEST +#include <assert.h> + +static void testing(const char *message) { + fprintf(stderr, "testing: %s...", message); +} + +static void test_ok() { + fprintf(stderr, " OK.\n"); +} + +static FILE *testfile() { + char filename[] = "remembering-test.XXXXXX"; + + errno = 0; + int fd = mkstemp(filename); + if (fd == -1) { + perror("tmpfile mkstemp"); + return NULL; + } + + errno = 0; + FILE *f = fdopen(fd, "w"); + if (!f) { + perror("tmpfile fdopen"); + close(fd); + return NULL; + } + + return f; +} + +static char *testdir() { + char dirname[] = "remembering-test.XXXXXX"; + + errno = 0; + char *dircpy = malloc(strlen(dirname) + 1); + if (!dircpy) { + perror("malloc"); + return NULL; + } + + errno = 0; + char *dir = mkdtemp(dirname); + if (!dir) { + perror("mkdtemp"); + free(dircpy); + return NULL; + } + + strcpy(dircpy, dir); + + return dircpy; +} + +static void assert_file_contents(const char *const filename, const char *const contents) { + +} +#endif + +static int print_usage(FILE *stream, char *name) { + errno = 0; + fprintf(stream, "Usage: %s -p PROFILE -c 'COMMAND'\n", + name ? name : "remembering"); + if (errno) { + perror("fprintf"); + return -1; + } + return 0; +} + +#ifdef TEST +static void print_usage_test() { + { + testing("print_usage a-name"); + FILE *f = testfile(); + if (!f) { + exit(EXIT_FAILURE); + } + + int ret = print_usage(f, "a-name"); + fclose(f); + if (ret) { + exit(EXIT_FAILURE); + } + + assert_file_contents(f, "Usage: a-name -p PROFILE -c 'COMMAND'\n"); + + test_ok(); + } + { + testing("print_usage NULL"); + FILE *f = testfile(); + if (!f) { + exit(EXIT_FAILURE); + } + + int ret = print_usage(f, NULL); + fclose(f); + if (ret) { + exit(EXIT_FAILURE); + } + + assert_file_contents(f, "Usage: remembering -p PROFILE -c 'COMMAND'\n"); + + test_ok(); + } +} +#endif + +static void print_help(FILE *stream) { + const char *help_text = + "\n" + "Options:\n" + " -p PROFILE profile to be used for gathering and storing data\n" + " -c 'COMMAND' command to be run, reading from STDIN, writing to " + "STDOUT\n" + " -h show this help\n" + " -V print program version\n" + "See remembering(1) manpages for more information.\n"; + fprintf(stream, "%s", help_text); +} + +static void print_missing(FILE *stream, char *text) { + fprintf(stream, "Missing option: %s\n", text); +} + +static void print_version(FILE *stream) { + fprintf(stream, "remembering-%s %s\n", VERSION, DATE); +} + +static int get_options(FILE *out, FILE *err, int argc, char *argv[], + char **command, char **profile) { + for (int i = 0; i < argc; i++) { + if (strcmp(argv[i], "--help") == 0) { + print_usage(out, argv[0]); + print_help(out); + return 1; + } else if (strcmp(argv[i], "--version")) { + print_version(out); + return 1; + } + } + + char *profilearg = NULL; + char *commandarg = NULL; + int option; + while ((option = getopt(argc, argv, "c:p:hV")) != -1) { + switch (option) { + case 'c': + commandarg = optarg; + break; + case 'p': + profilearg = optarg; + break; + case 'h': + print_usage(out, argv[0]); + print_help(out); + return 1; + case 'V': + print_version(out); + return 1; + } + } + + if (!commandarg) { + print_missing(err, "-c 'COMMAND'"); + print_usage(err, argv[0]); + return -1; + } + + if (!profilearg) { + print_missing(err, "-p 'PROFILE'"); + print_usage(err, argv[0]); + return 1; + } + + *command = strdup(commandarg); + if (!*command) { + perror("strdup"); + return -1; + } + *profile = strdup(profilearg); + if (!*profile) { + perror("strdup"); + free(*command); + return -1; + } + + return 0; +} + +#ifdef FALLIBLE +static int fallible_get_options(FILE *out, FILE *err, int argc, char *argv[], + char **command, char **profile, + const char *const filename, int lineno) { + if (fallible_should_fail(filename, lineno)) { + return -1; + } + return get_options(out, err, argc, argv, command, profile); +} + +#define get_options(out, err, argc, argv, command, profile) \ + fallible_get_options(out, err, argc, argv, command, profile, __FILE__, \ + __LINE__) +#endif + +#ifdef TEST +int arrlen(char **argv) { + int count = 0; + while (argv[count] != NULL) { + count++; + } + return count; +} + +void get_options_test() { + { + testing("get_options when given -h, -V and --help"); + FILE *f = tmpfile(); + if (!f) { + exit(EXIT_FAILURE); + } + + char *argvs[13][6] = { + {"--help", NULL}, + {"other", "arguments before", "--help", NULL}, + {"some", "--help", "arguments after", NULL}, + {"--help", "all", "arguments after", NULL}, + {"-h", NULL}, + {"-V", NULL}, + {"-hc", "1", NULL}, + {"-hp", "2", NULL}, + {"-hV", NULL}, + {"-Vh", NULL}, + {"-p", "p", "-h", NULL}, + {"-p", "p", "-c", "c", "-h", NULL}, + {NULL}, + }; + int rows = 0; + while (argvs[rows][0] != NULL) { + rows++; + } + +#ifdef DISABLE + int ret; + char *command = NULL; + char *profile = NULL; + for (int i = 0; i < rows; i++) { + char **argv = argvs[i]; + ret = get_options(f, f, arrlen(argvs[i]), argv, &command, &profile); + if (ret == -1) { + fclose(f); + exit(EXIT_FAILURE); + } + + assert(command == NULL); + assert(profile == NULL); + assert(ret == 1); + } +#endif + + fclose(f); + test_ok(); + } + // { "" }, +} +#endif + +static int mkdir_p(char *path, mode_t mode) { + struct stat s; + if (stat(path, &s) == 0 && S_ISDIR(s.st_mode)) { + return 0; + } + + char *parent = dirname(path); + int ret = mkdir_p(parent, mode); + if (ret) { + return ret; + } + return mkdir(path, mode); +} + +#ifdef FALLIBLE +static int fallible_mkdir_p(char *path, mode_t mode, const char *const filename, + int lineno) { + if (fallible_should_fail(filename, lineno)) { + return -1; + } + return mkdir_p(path, mode); +} + +#define mkdir_p(path, mode) fallible_mkdir_p(path, mode, __FILE__, __LINE__) +#endif + +#ifdef TEST +static void mkdir_p_test() { +{ +testing("mkdir_p a single directory"); + +char *dir = tmpdir(); +if (!dir) { +exit(EXIT_FAILURE); +} + +int ret = mkdir_p(dir, 0755); +if (ret) { +free(dir); +exit(EXIT_FAILURE); +} + +free(dir); +test_ok(); +} +{ +testing("mkdir_p nested directories"); + +const char *const dir_suffix = "/a/b/c/d/e"; +char *dir_prefix = tmpdir(); +if (!dir_prefix) { +exit(EXIT_FAILURE); +} + + char *dir = malloc(strlen(dir_prefix) + strlen(dir_suffix) + 1); + if (!dir) { + free(dir_prefix); + exit(EXIT_FAILURE); + } + + strcpy(dir, dir_prefix); + strcat(dir, dir_suffix); + +int ret = mkdir_p(dir, 0755); +if (ret) { + free(dir); + free(dir_prefix); +exit(EXIT_FAILURE); +} + + free(dir); + free(dir_prefix); +test_ok(); +} +} +#endif + +static char *expand_profile_name(const char *const profile_name) { + char *prefix = NULL; + char *env_prefix = getenv("XDG_DATA_HOME"); + if (env_prefix) { + prefix = strdup(env_prefix); + if (!prefix) { + return NULL; + } + } else { + char *home = getenv("HOME"); + char *path = "/.local/share/remembering"; + prefix = malloc(strlen(home) + strlen(path) + 1); + if (!prefix) { + return NULL; + } + strcpy(prefix, home); + strcat(prefix, path); + } + char *separator = "/"; + char *expanded_profile = + malloc(strlen(prefix) + strlen(separator) + strlen(profile_name) + 1); + if (!expanded_profile) { + free(prefix); + return NULL; + } + strcpy(expanded_profile, prefix); + strcat(expanded_profile, separator); + strcat(expanded_profile, profile_name); + + free(prefix); + + errno = 0; + int ret = mkdir_p(dirname(expanded_profile), 0755); + if (ret) { + perror("mkdir_p"); + return NULL; + } + return expanded_profile; +} + +#ifdef FALLIBLE +static char *fallible_expand_profile_name(const char *const profile_name, + const char *const filename, + int lineno) { + if (fallible_should_fail(filename, lineno)) { + return NULL; + } + return expand_profile_name(profile_name); +} + +#define expand_profile_name(profile_name) \ + fallible_expand_profile_name(profile_name, __FILE__, __LINE__) +#endif + +#ifdef TEST +static void expand_profile_name_test() { + { + testing("expand_profile_name"); + if (0) { + expand_profile_name("oij"); + } + test_ok(); + } +} +#endif + +static const size_t RANKINGS_INITIAL_SIZE = 100; +static const int RANKINGS_GROWTH_MULTIPLIER = 2; + +struct Rankings { + size_t count; + size_t size; + char **values; + int *ranks; +}; + +static struct Rankings *rankings_new() { + struct Rankings *r = malloc(sizeof(struct Rankings)); + if (!r) { + return NULL; + } + r->count = 0; + r->size = RANKINGS_INITIAL_SIZE; + r->values = malloc(r->size * sizeof(char *)); + if (!r->values) { + free(r); + return NULL; + } + r->ranks = malloc(r->size * sizeof(int)); + if (!r->ranks) { + free(r->values); + free(r); + return NULL; + } + return r; +} + +#ifdef FALLIBLE +static struct Rankings *fallible_rankings_new(const char *const filename, + int lineno) { + if (fallible_should_fail(filename, lineno)) { + return NULL; + } + return rankings_new(); +} + +#define rankings_new() fallible_rankings_new(__FILE__, __LINE__) +#endif + +static void rankings_free(struct Rankings *r) { + if (!r) { + return; + } + for (size_t i = 0; i < r->count; i++) { + free(r->values[i]); + } + free(r->values); + free(r->ranks); + free(r); +} + +static int rankings_insert(struct Rankings *r, char *value, int rank) { + if (r->count == r->size) { + r->size *= RANKINGS_GROWTH_MULTIPLIER; + char **new_values = realloc(r->values, r->size * sizeof(char *)); + if (!new_values) { + return -1; + } + r->values = new_values; + int *new_ranks = realloc(r->ranks, r->size * sizeof(int)); + if (!new_ranks) { + return -1; + } + r->ranks = new_ranks; + } + r->values[r->count] = value; + r->ranks[r->count] = rank; + r->count++; + return 0; +} + +#ifdef FALLIBLE +static int fallible_rankings_insert(struct Rankings *r, char *value, int rank, + const char *const filename, int lineno) { + if (fallible_should_fail(filename, lineno)) { + return -1; + } + return rankings_insert(r, value, rank); +} + +#define rankings_insert(r, value, rank) \ + fallible_rankings_insert(r, value, rank, __FILE__, __LINE__) +#endif + +#ifdef TEST +static void rankings_test() { + { + testing("struct Rankings expands its size when needed"); + struct Rankings *r = rankings_new(); + if (!r) { + exit(EXIT_FAILURE); + } + size_t some_limit = RANKINGS_INITIAL_SIZE + 1; + int ret; + for (size_t i = 0; i < some_limit; i++) { + char *s = strdup("some string"); + ret = rankings_insert(r, s, i); + if (ret) { + free(s); + rankings_free(r); + exit(EXIT_FAILURE); + } + } + + assert(r->size == RANKINGS_INITIAL_SIZE * RANKINGS_GROWTH_MULTIPLIER); + assert(r->count == some_limit); + + rankings_free(r); + test_ok(); + } + + { + testing("an empty Rankings doesn't leak"); + struct Rankings *r = rankings_new(); + if (!r) { + exit(EXIT_FAILURE); + } + + assert(r->size == RANKINGS_INITIAL_SIZE); + assert(r->count == 0); + + rankings_free(r); + test_ok(); + } +} +#endif + +static const char RANKING_DELIMITER = ':'; + +int parse_ranked_line(FILE *stream, const char *entry, char **value, + int *rank) { + char *value_substr = strchr(entry, RANKING_DELIMITER); + if (value_substr == NULL) { + fprintf(stream, "WARN: Missing delimiter ('%c') in line: %s\n", + RANKING_DELIMITER, entry); + return -1; + } + + int rank_strlen = value_substr - entry; + char *rank_str = strndup(entry, rank_strlen); + if (!rank_str) { + return -1; + } + *rank = atoi(rank_str); + free(rank_str); + + *value = malloc(strlen(value_substr) + 1); + if (!*value) { + return -1; + } + strcpy(*value, entry + (rank_strlen + 1 / * RANKING_DELIMITER * /)); + + return 0; +} + +#ifdef FALLIBLE +int fallible_parse_ranked_line(FILE *stream, const char *entry, char **value, + int *rank, const char *const filename, + int lineno) { + if (fallible_should_fail(filename, lineno)) { + return -1; + } + return parse_ranked_line(stream, entry, value, rank); +} + +#define parse_ranked_line(stream, entry, value, rank) \ + fallible_parse_ranked_line(stream, entry, value, rank, __FILE__, __LINE__) +#endif + +#ifdef TEST +static void parse_ranked_line_test() { + { + testing("parse_ranked_line with an empty string"); + FILE *f = tmpfile(); + if (!f) { + exit(EXIT_FAILURE); + } + + char *value; + int rank; + int ret = parse_ranked_line(f, "", &value, &rank); + assert(ret == -1); + fclose(f); + test_ok(); + } + { + testing("parse_ranked_line when RANKING DELIMITER is missing"); + FILE *f = tmpfile(); + if (!f) { + exit(EXIT_FAILURE); + } + + char *value; + int rank, ret; + + ret = parse_ranked_line(f, "0 command", &value, &rank); + assert(ret == -1); + + ret = parse_ranked_line(f, "1", &value, &rank); + assert(ret == -1); + + fclose(f); + test_ok(); + } + { + testing("parse_ranked_line with a happy path examples"); + char *value; + int rank; + int ret; + + ret = parse_ranked_line(stderr, "0:command", &value, &rank); + if (ret) { + exit(EXIT_FAILURE); + } + assert(ret == 0); + assert(strcmp(value, "command") == 0); + assert(rank == 0); + free(value); + + ret = parse_ranked_line(stderr, "10:another command", &value, &rank); + if (ret) { + exit(EXIT_FAILURE); + } + assert(ret == 0); + assert(strcmp(value, "another command") == 0); + assert(rank == 10); + free(value); + + ret = parse_ranked_line(stderr, "123:123", &value, &rank); + if (ret) { + exit(EXIT_FAILURE); + } + assert(ret == 0); + assert(strcmp(value, "123") == 0); + assert(rank == 123); + free(value); + + ret = parse_ranked_line(stderr, "-123:command", &value, &rank); + if (ret) { + exit(EXIT_FAILURE); + } + assert(ret == 0); + assert(strcmp(value, "command") == 0); + assert(rank == -123); + free(value); + + ret = parse_ranked_line(stderr, "0:0", &value, &rank); + if (ret) { + exit(EXIT_FAILURE); + } + assert(ret == 0); + assert(strcmp(value, "0") == 0); + assert(rank == 0); + free(value); + + ret = parse_ranked_line(stderr, "0:command with : in the middle", &value, + &rank); + if (ret) { + exit(EXIT_FAILURE); + } + assert(ret == 0); + assert(strcmp(value, "command with : in the middle") == 0); + assert(rank == 0); + free(value); + + ret = parse_ranked_line( + stderr, "0:::command:with:multiple:::in:the:middle:", &value, &rank); + if (ret) { + exit(EXIT_FAILURE); + } + assert(ret == 0); + assert(strcmp(value, "::command:with:multiple:::in:the:middle:") == 0); + assert(rank == 0); + free(value); + + test_ok(); + } + { + testing("parse_ranked_line with an empty command"); + char *value; + int rank; + + int ret = parse_ranked_line(stderr, "0:", &value, &rank); + if (ret) { + exit(EXIT_FAILURE); + } + assert(ret == 0); + assert(strcmp(value, "") == 0); + assert(rank == 0); + free(value); + + test_ok(); + } + { + testing("parse_ranked_line with a bad rank numbers"); + char *value; + int rank; + int ret; + + ret = parse_ranked_line(stderr, ":command", &value, &rank); + if (ret) { + exit(EXIT_FAILURE); + } + assert(ret == 0); + assert(strcmp(value, "command") == 0); + assert(rank == 0); + free(value); + + ret = parse_ranked_line(stderr, "1 2 3:command", &value, &rank); + if (ret) { + exit(EXIT_FAILURE); + } + assert(ret == 0); + assert(strcmp(value, "command") == 0); + assert(rank == 1); + free(value); + + ret = parse_ranked_line(stderr, ".1:command", &value, &rank); + if (ret) { + exit(EXIT_FAILURE); + } + assert(ret == 0); + assert(strcmp(value, "command") == 0); + assert(rank == 0); + free(value); + + ret = parse_ranked_line(stderr, "3.14:command", &value, &rank); + if (ret) { + exit(EXIT_FAILURE); + } + assert(ret == 0); + assert(strcmp(value, "command") == 0); + assert(rank == 3); + free(value); + + ret = parse_ranked_line(stderr, ":5:command", &value, &rank); + if (ret) { + exit(EXIT_FAILURE); + } + assert(ret == 0); + assert(strcmp(value, "5:command") == 0); + assert(rank == 0); + free(value); + + ret = parse_ranked_line(stderr, "command:command", &value, &rank); + if (ret) { + exit(EXIT_FAILURE); + } + assert(ret == 0); + assert(strcmp(value, "command") == 0); + assert(rank == 0); + free(value); + + test_ok(); + } +} +#endif + +struct GetlineParams { + char *line; + size_t len; + ssize_t read; + FILE *stream; +}; + +int get_stdin(struct GetlineParams *s_params, struct GetlineParams *p_params, + struct Rankings *p_rankings) { + s_params->read = getline(&s_params->line, &s_params->len, s_params->stream); + if (s_params->read == -1) { + while ((p_params->read = getline(&p_params->line, &p_params->len, + p_params->stream)) != -1) { + char *value; + int rank; + int ret; + if ((ret = parse_ranked_line(stderr, p_params->line, &value, &rank)) == + -1) { + return ret; + } + if ((ret = rankings_insert(p_rankings, value, rank)) == -1) { + return ret; + } + } + return 1; + } + return 0; +} + +int get_profile(struct GetlineParams *s_params, struct GetlineParams *p_params, + struct Rankings *s_rankings, struct Rankings *p_rankings) { + p_params->read = getline(&p_params->line, &p_params->len, p_params->stream); + if (p_params->read == -1) { + while ((s_params->read = getline(&s_params->line, &s_params->len, + s_params->stream)) != -1) { + char *s_value_copy = strdup(s_params->line); + if (!s_value_copy) { + return -1; + } + char *p_value_copy = strdup(s_params->line); + if (!p_value_copy) { + free(s_value_copy); + return -1; + } + + int ret; + if ((ret = rankings_insert(s_rankings, s_value_copy, 0)) == -1) { + free(s_value_copy); + free(p_value_copy); + return -1; + } + + // profile grows with stdin entries + if ((ret = rankings_insert(p_rankings, p_value_copy, 0)) == -1) { + free(s_value_copy); + free(p_value_copy); + return -1; + } + } + return 1; + } + return 0; +} + +int merge_stdin_with_profile(char *profile_name, struct Rankings *s_rankings, + struct Rankings *p_rankings) { + FILE *profile = fopen(profile_name, "r"); + struct GetlineParams s_params = { + .line = NULL, .len = 0, .read = 0, .stream = stdin}; + struct GetlineParams p_params = { + .line = NULL, .len = 0, .read = 0, .stream = profile}; + + int stop = get_stdin(&s_params, &p_params, p_rankings) || + get_profile(&s_params, &p_params, s_rankings, p_rankings); + while (!stop) { + char *value; + int rank; + parse_ranked_line(stderr, p_params.line, &value, &rank); + int cmp = strcmp(s_params.line, value); + + if (cmp == 0) { + rankings_insert(s_rankings, value, rank); + rankings_insert(p_rankings, value, rank); // double free + stop = get_stdin(&s_params, &p_params, p_rankings) || + get_profile(&s_params, &p_params, s_rankings, p_rankings); + continue; + } + + if (cmp < 0) { + char *value_copy = malloc(strlen(s_params.line) + 1); + strcpy(value_copy, s_params.line); + int *rank = malloc(sizeof(int)); + *rank = 0; + rankings_insert(s_rankings, value_copy, rank); + rankings_insert(p_rankings, value_copy, rank); + stop = get_stdin(&s_params, &p_params, p_rankings); + tuple_free(&t); + continue; + } + + if (cmp > 0) { + rankings_insert(p_rankings, t.first, t.second); + stop = get_profile(&s_params, &p_params, s_rankings, p_rankings); + tuple_free(&t); + continue; + } + } + + free(s_params.line); + free(p_params.line); + fclose(profile); + + if (stop == -1) { + return stop; + } + return 0; +} + +#ifdef TEST +static void unit_tests() { + print_usage_test(); + get_options_test(); + mkdir_p_test(); + expand_profile_name_test(); + rankings_test(); + parse_ranked_line_test(); +} +#endif + +int main(int argc, char *argv[]) { +#ifdef TEST + unit_tests(); + return EXIT_SUCCESS; +#endif + + int ret; + + char *command, *profile; + ret = get_options(stdout, stderr, argc, argv, &command, &profile); + if (ret == -1) { + return EXIT_FAILURE; + } else if (ret) { + return EXIT_SUCCESS; + } +} +*/ + +int main() { + return 0; +} |