diff --git a/include/option.h b/include/option.h new file mode 100644 index 0000000..48caf95 --- /dev/null +++ b/include/option.h @@ -0,0 +1,19 @@ +#ifndef _OPTIONS_H_ +#define _OPTIONS_H_ + +#include + +/* + * These are the most possible number of disk inputs we can accept + */ +#define OPTION_MAX_DISKS 2 + +extern const char *option_get_error(); +extern FILE *option_get_input(int); +extern int option_parse(int, char **); +extern void option_print_help(); +extern int option_read_file(int, const char *); +extern void option_set_error(const char *); +extern void option_set_input(int, FILE *); + +#endif diff --git a/sources.cmake b/sources.cmake index 1c4719d..7d47732 100644 --- a/sources.cmake +++ b/sources.cmake @@ -9,6 +9,7 @@ set(erc_sources mos6502.exec.c mos6502.loadstor.c mos6502.stat.c + option.c vm_screen.c vm_segment.c ) diff --git a/src/main.c b/src/main.c index c5fb29d..db9aa34 100644 --- a/src/main.c +++ b/src/main.c @@ -1,15 +1,37 @@ #include #include +#include +#include #include "log.h" +#include "option.h" /* * This function will establish the base environment that we want to use * while we execute. */ static void -init() +init(int argc, char **argv) { + int options_ok; + + // If the option_parse() function returns zero, that means that it's + // signaled to us that we should stop now. Whether that means we are + // stopping in _error_ (bad input), or just because you asked for + // --help, is not really specified. We exit with a non-zero error + // code in any case. + options_ok = option_parse(argc, argv); + if (options_ok == 0) { + const char *err = option_get_error(); + + if (strlen(err) > 0) { + fprintf(stderr, "%s\n", err); + option_print_help(); + } + + exit(1); + } + // We're literally using stdout in this heavy phase of development. log_open(stdout); } @@ -30,7 +52,7 @@ finish() int main(int argc, char **argv) { - init(); + init(argc, argv); // When we exit, we want to wrap up a few loose ends. This syscall // will ensure that `finish()` runs whether we return from main diff --git a/src/option.c b/src/option.c new file mode 100644 index 0000000..512b039 --- /dev/null +++ b/src/option.c @@ -0,0 +1,201 @@ +/* + * options.c + */ + +#include +#include +#include +#include + +#include "option.h" + +/* + * These are the file inputs we may have to the system. What their + * contents are will vary based on emulation context, and these values + * may change through the course of the program's execution. + */ +static FILE *input1 = NULL; +static FILE *input2 = NULL; + +/* + * The size of our error buffer for anything we want to record while our + * option parsing goes on. + */ +#define ERRBUF_SIZE 2048 + +/* + * The alluded-to error buffer. + */ +static char error_buffer[ERRBUF_SIZE] = ""; + +/* + * These are all of the options we allow in our long-form options. It's + * a bit faster to identify them by integer symbols than to do string + * comparisons. + */ +enum options { + HELP, + DISK1, + DISK2, +}; + +/* + * Here are the options we support for program execution. + */ +static struct option long_options[] = { + { "disk1", 1, NULL, DISK1 }, + { "disk2", 1, NULL, DISK2 }, + { "help", 0, NULL, HELP }, +}; + +/* + * This simply returns the pointer of our error buffer, with the + * qualification that the caller cannot modify the error buffer once + * they get the pointer back. + */ +const char * +option_get_error() +{ + return error_buffer; +} + +/* + * Directly set the error buffer with something (that has to be less + * than ERRBUF_SIZE). This function is not itself hugely useful, but + * does allow for some better testing on option_error(). + */ +void +option_set_error(const char *str) +{ + // Use `- 1` so that we can ensure error_buffer is NUL-terminated. + // Otherwise, strncpy will copy all of src but leave the dst + // unterminated. Not that we're _likely_ to so self-injure + // ourselves, but, ya know. + strncpy(error_buffer, str, ERRBUF_SIZE - 1); +} + +/* + * Parse our command-line arguments from the given argc and argv. This + * may or may not be what the kernel passes into the main() function! + * We return 1 if we can continue beyond the option parse phase, and 0 + * if something came up to cause us to exit early. But whether you + * actually exit early is left up to the caller. + */ +int +option_parse(int argc, char **argv) +{ + int index; + int opt = -1; + + // To begin with, let's (effectively) NUL-out the error buffer. + error_buffer[0] = '\0'; + + do { + int input_source = 0; + + opt = getopt_long_only(argc, argv, "", long_options, &index); + + switch (opt) { + case DISK1: + input_source = 1; + break; + + case DISK2: + input_source = 2; + break; + + case HELP: + option_print_help(); + + // The help option should terminate normal execution + return 0; + } + + // We seem to have a request to load a file, so let's do so. + if (input_source) { + if (!option_read_file(input_source, optarg)) { + return 0; + } + } + } while (opt != -1); + + return 1; +} + +/* + * Given a file path, we open a FILE stream and set our given input + * source to that stream. If we cannot do so, we will set an error + * string in the buffer. Assuming all goes well, we will return 1, and + * 0 if not. + */ +int +option_read_file(int source, const char *file) +{ + FILE *stream; + + if (!file) { + snprintf(error_buffer, + ERRBUF_SIZE, + "No file given for --disk%d\n", + source); + return 0; + } + + stream = fopen(file, "r+"); + if (stream == NULL) { + snprintf(error_buffer, + ERRBUF_SIZE, + "--disk%d: %s", + source, + strerror(errno)); + return 0; + } + + option_set_input(source, stream); + + return 1; +} + +/* + * Return the FILE stream for a given input, or NULL if none can be + * found. NULL may also be returned if the input has not previously been + * assigned. + */ +FILE * +option_get_input(int source) +{ + switch (source) { + case 1: return input1; + case 2: return input2; + } + + return NULL; +} + +/* + * Set the given input source to a given FILE stream. If the input + * source is invalid, then nothing is done. + */ +void +option_set_input(int source, FILE *stream) +{ + switch (source) { + case 1: input1 = stream; + case 2: input2 = stream; + } +} + +/* + * Print out a help message. You'll note this is not automatically + * generated; it must be manually updated as we add other options. + */ +void +option_print_help() +{ + fprintf(stderr, "Usage: erc [options...]\n"); + fprintf(stderr, "Options:\n"); + fprintf(stderr, "\ + --disk1=FILE Load FILE into disk drive 1\n\ + --disk2=FILE Load FILE into disk drive 2\n\ + --help Print this help message\n"); +} diff --git a/tests/option.c b/tests/option.c new file mode 100644 index 0000000..8e81f7b --- /dev/null +++ b/tests/option.c @@ -0,0 +1,99 @@ +#include +#include + +#include "option.h" + +static void +setup() +{ + option_set_error(""); + option_set_input(1, NULL); + option_set_input(2, NULL); +} + +static void +teardown() +{ + FILE *stream; + + for (int i = 1; i < OPTION_MAX_DISKS; i++) { + stream = option_get_input(i); + + if (stream + && stream != stdout + && stream != stderr + && stream != stdin + ) { + fclose(stream); + } + } +} + +TestSuite(options, .init = setup, .fini = teardown); + +Test(options, error) +{ + char *str = "hahaha FUN"; + + cr_assert_str_empty(option_get_error()); + + option_set_error(str); + cr_assert_str_eq(option_get_error(), str); +} + +Test(options, input) +{ + cr_assert_eq(option_get_input(1), NULL); + cr_assert_eq(option_get_input(2), NULL); + + option_set_input(2, stdout); + cr_assert_eq(option_get_input(2), stdout); + + option_set_input(3, stderr); + cr_assert_eq(option_get_input(3), NULL); +} + +Test(options, read_file) +{ + char *str = "so much FUN"; + char *bad_file = "/tmp/BLEH"; + char *file = "/tmp/erc-test.txt"; + char buf[256]; + + cr_assert_eq(option_get_input(1), NULL); + + // Maybe we should use sterror(ENOENT)? + cr_assert_eq(option_read_file(1, bad_file), 0); + cr_assert_str_eq(option_get_error(), "--disk1: No such file or directory"); + + option_set_error(""); + + FILE *stream; + stream = fopen(file, "w"); + cr_assert_neq(stream, NULL); + fwrite(str, sizeof(char), strlen(str), stream); + fclose(stream); + + option_read_file(1, file); + fread(buf, sizeof(char), 255, option_get_input(1)); + cr_assert_str_eq(buf, str); + + unlink(file); +} + +/* + * This test is really imperfect... that's because option_parse() does + * a ton of stuff, and is quite complex. I'm punting a lot on the + * complexity here, while pushing as much of the logic as I can into + * other functions that are more easily testable. + */ +Test(options, parse) +{ + int argc = 2; + char *argv[] = { + "prog_name", + "--disk1=etc", + }; + + cr_assert_eq(option_parse(argc, argv), 0); +}