diff --git a/homework08/Makefile b/homework08/Makefile new file mode 100644 index 0000000..1599501 --- /dev/null +++ b/homework08/Makefile @@ -0,0 +1,72 @@ +CC= gcc +CFLAGS= -Wall -g -std=gnu99 +LD= gcc +LDFLAGS= -L. +TARGETS= findit + +all: $(TARGETS) + +#------------------------------------------------------------------------------- +# TODO: Add rules for object files +#------------------------------------------------------------------------------- + +list.o: list.c + $(CC) $(CFLAGS) -c -o list.o list.c + +filter.o: filter.c + $(CC) $(CFLAGS) -c -o filter.o filter.c + +findit.o: findit.c findit.h + $(CC) $(CFLAGS) -c -o findit.o findit.c + +#------------------------------------------------------------------------------- +# TODO: Add rules for executables +#------------------------------------------------------------------------------- + +findit: findit.o filter.o list.o + $(LD) $(LDFLAGS) -o $@ $^ + +#------------------------------------------------------------------------------- +# DO NOT MODIFY BELOW +#------------------------------------------------------------------------------- + +test: + @$(MAKE) -sk test-all + +test-all: test-gitignore test-list test-filter test-findit + +test-gitignore: + @echo "findit" > .gitignore + @echo "*.o" >> .gitignore + @echo "*.sh" >> .gitignore + @echo "*.unit" >> .gitignore + +test-list: list.unit + @curl -sLO https://www3.nd.edu/~pbui/teaching/cse.20289.sp25/static/txt/homework08/list.unit.sh + @chmod +x list.unit.sh + @./list.unit.sh + +list.unit.o: list.unit.c findit.h + @$(CC) $(CFLAGS) -c -o $@ $< + +list.unit: list.unit.o list.o + @$(LD) $(LDFLAGS) -o $@ $^ + +test-filter: filter.unit + @curl -sLO https://www3.nd.edu/~pbui/teaching/cse.20289.sp25/static/txt/homework08/filter.unit.sh + @chmod +x filter.unit.sh + @./filter.unit.sh + +filter.unit.o: filter.unit.c findit.h + @$(CC) $(CFLAGS) -c -o $@ $< + +filter.unit: filter.unit.o filter.o + @$(LD) $(LDFLAGS) -o $@ $^ + +test-findit: findit + @curl -sLO https://www3.nd.edu/~pbui/teaching/cse.20289.sp25/static/txt/homework08/findit.test.sh + @chmod +x findit.test.sh + @./findit.test.sh + +clean: + @rm -f *.o *.sh *.unit findit diff --git a/homework08/filter.c b/homework08/filter.c new file mode 100644 index 0000000..dabd5c8 --- /dev/null +++ b/homework08/filter.c @@ -0,0 +1,75 @@ +/* filter.c: Filter functions */ + +#include "findit.h" + +#include +#include + +#include +#include +#include +#include +#include + +/* Filter Functions */ + +/** + * Determines if file at specified path has matching file type. + * @param path Path string + * @param options Pointer to options structure + * @return true if file at specified path has matching file type specified in + * options. + **/ +bool filter_by_type(const char *path, Options *options) { + // TODO: Use lstat + struct stat statbuf; + + // If lstat fails (returns non-zero), return false + if (lstat(path, &statbuf) != 0) { + return false; + } + + // Extract file type from mode using statbuf and bitmask + mode_t file_type = statbuf.st_mode & S_IFMT; + + // Compare with the specified type + bool is_match = (file_type == options->type); + + return is_match; // Return true if it's the same +} + +/** + * Determines if file at specified path has matching basename. + * @param path Path string + * @param options Pointer to options structure + * @return true if file at specified path has basename that matches specified + * pattern in options. + **/ +bool filter_by_name(const char *path, Options *options) { + // TODO: Use basename and fnmatch + char *path_copy = strdup(path); // Create copy for basename + + // Get basename and check if pattern matches + char *base = basename(path_copy); + bool match = (fnmatch(options->name, base, 0) == 0); + + free(path_copy); + + return match; +} + +/** + * Determines if file at specified path has matching access mode. + * @param path Path string + * @param options Pointer to options structure + * @return true if file at specified path has matching access mode specified + * in options. + **/ +bool filter_by_mode(const char *path, Options *options) { + // TODO: Use access + bool has_access = (access(path, options->mode) == 0); // Check access w/ process's user ID + + return has_access; +} + +/* vim: set sts=4 sw=4 ts=8 expandtab ft=c: */ diff --git a/homework08/filter.unit.c b/homework08/filter.unit.c new file mode 100644 index 0000000..0d85925 --- /dev/null +++ b/homework08/filter.unit.c @@ -0,0 +1,112 @@ +/* filter.unit.c: filter unit test */ + +#include "findit.h" + +#include +#include +#include +#include +#include + +#include +#include + +/* Tests */ + +int test_00_filter_by_type() { + Options o = {0}; + + // Test directories + o.type = S_IFDIR; + assert(filter_by_type(".", &o)); + assert(filter_by_type("..", &o)); + assert(filter_by_type("/tmp", &o)); + assert(!filter_by_type("Makefile", &o)); + assert(!filter_by_type("filter.c", &o)); + assert(!filter_by_type("list.c", &o)); + assert(!filter_by_type("/root/.ssh", &o)); + assert(!filter_by_type("CHUPABLAHBLA", &o)); + + // Test files + o.type = S_IFREG; + assert(!filter_by_type(".", &o)); + assert(!filter_by_type("..", &o)); + assert(!filter_by_type("/tmp", &o)); + assert(filter_by_type("Makefile", &o)); + assert(filter_by_type("filter.c", &o)); + assert(filter_by_type("list.c", &o)); + assert(!filter_by_type("/root/.ssh", &o)); + assert(!filter_by_type("CHUPABLAHBLA", &o)); + return EXIT_SUCCESS; +} + +int test_01_filter_by_name() { + Options o = {0}; + + // Test no pattern + o.name = "Makefile"; + assert(filter_by_name("Makefile", &o)); + assert(filter_by_name("./Makefile", &o)); + assert(!filter_by_name("Makefiles", &o)); + assert(!filter_by_name("makefile", &o)); + assert(!filter_by_name("./Makefile/asdf", &o)); + + // Test pattern + o.name = "*.c"; + assert(!filter_by_name("Makefile", &o)); + assert(!filter_by_name("./Makefile", &o)); + assert(filter_by_name("filter.c", &o)); + assert(filter_by_name("./filter.c", &o)); + assert(!filter_by_name("./filter.ch", &o)); + return EXIT_SUCCESS; +} + +int test_02_filter_by_mode() { + Options o = {0}; + + // Test readable + o.mode = R_OK; + assert(filter_by_mode("Makefile", &o)); + assert(filter_by_mode("filter.unit", &o)); + assert(!filter_by_mode("/root/.ssh", &o)); + + // Test writable + o.mode = W_OK; + assert(filter_by_mode("Makefile", &o)); + assert(filter_by_mode("filter.unit", &o)); + assert(!filter_by_mode("/root/.ssh", &o)); + + // Test executable + o.mode = X_OK; + assert(!filter_by_mode("Makefile", &o)); + assert(filter_by_mode("filter.unit", &o)); + assert(!filter_by_mode("/root/.ssh", &o)); + return EXIT_SUCCESS; +} + +/* Main Execution */ + +int main(int argc, char *argv[]) { + if (argc != 2) { + fprintf(stderr, "Usage: %s NUMBER\n\n", argv[0]); + fprintf(stderr, "Where NUMBER is right of the following:\n"); + fprintf(stderr, " 0 Test filter_by_type\n"); + fprintf(stderr, " 1 Test filter_by_name\n"); + fprintf(stderr, " 2 Test filter_by_mode\n"); + return EXIT_FAILURE; + } + + int number = atoi(argv[1]); + int status = EXIT_FAILURE; + + switch (number) { + case 0: status = test_00_filter_by_type(); break; + case 1: status = test_01_filter_by_name(); break; + case 2: status = test_02_filter_by_mode(); break; + default: fprintf(stderr, "Unknown NUMBER: %d\n", number); break; + } + + return status; +} + +/* vim: set sts=4 sw=4 ts=8 expandtab ft=c: */ diff --git a/homework08/findit b/homework08/findit new file mode 100755 index 0000000..9853c15 Binary files /dev/null and b/homework08/findit differ diff --git a/homework08/findit.c b/homework08/findit.c new file mode 100644 index 0000000..350470f --- /dev/null +++ b/homework08/findit.c @@ -0,0 +1,202 @@ +/* findit.c: Search for files in a directory hierarchy */ + +#include "findit.h" + +#include +#include +#include +#include + +#include +#include + +/* Macros */ + +#define streq(a, b) (strcmp(a, b) == 0) + +/* Functions */ + +/** + * Print usage message and exit with status + * @param status Exit status + **/ +void usage(int status) { + fprintf(stderr, "Usage: findit PATH [OPTIONS]\n\n"); + fprintf(stderr, "Options:\n\n"); + fprintf(stderr, " -type [f|d] File is of type f for regular file or d for directory\n"); + fprintf(stderr, " -name pattern Name of file matches shell pattern\n"); + fprintf(stderr, " -executable File is executable or directory is searchable by user\n"); + fprintf(stderr, " -readable File is readable by user\n"); + fprintf(stderr, " -writable File is writable by user\n"); + exit(status); +} + +/** + * Recursively walk specified directory, adding all file system entities to + * specified files list. + * @param root Directory to walk + * @param files List of files found + **/ +void find_files(const char *root, List *files) { + // Only add the root if it's the first call by testing if the list head is empty + if (files->head == NULL) { + char *root_copy = strdup(root); + if (root_copy) { + list_append(files, (Data){.string = root_copy}); + } + } + + // Open directory + DIR *dir = opendir(root); + if (dir == NULL) { + return; + } + + struct dirent *entry; + while ((entry = readdir(dir)) != NULL) { + // Skip current/parent directories + if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) { + continue; + } + + // Construct the full path + char path[BUFSIZ]; + snprintf(path, sizeof(path), "%s/%s", root, entry->d_name); + + // Add the path to the list + char *path_copy = strdup(path); + if (path_copy) { + list_append(files, (Data){.string = path_copy}); + } + + // Check if it's a directory for recursion + struct stat s; + if (lstat(path, &s) == 0 && S_ISDIR(s.st_mode)) { + find_files(path, files); // The recursive call + } + } + + closedir(dir); +} + +/** + * Iteratively filter list of files with each filter in list of filters. + * @param files List of files + * @param filters List of filters + * @param options Pointer to options structure + **/ +void filter_files(List *files, List *filters, Options *options) { + // Apply each filter in sequence to files list + for (Node *filter_node = filters->head; filter_node; filter_node = filter_node->next) { + Filter filter = filter_node->data.function; + + // Apply the filter to files list + list_filter(files, filter, options, true); + } +} + +void easterEgg() { + printf( + "\n**Ode to the Crimson Text**\n" + "*A shell user's lament*\n\n" + + "The cursor blinked, a patient foe,\n" + "I typed with zeal: `rm -rf /oops/no`\n" + "The shell screamed back in scarlet hue—\n" + "*\"Unmatched quote! Syntax taboo!\"*\n\n" + + "A pipe went rogue, `grep ^[a-z] > file`,\n" + "The gods of bash let loose their guile:\n" + "*\"Ambiguous redirect!\"* they decreed,\n" + "As my homework dissolved to digital greed.\n\n" + + "I summoned roots with `sudo !-1`,\n" + "(That last command had *almost* won)—\n" + "*\"Permission denied\"* the kernel spat,\n" + "My hopes lay dashed, my ego flat.\n\n" + + "The regex beast, that cryptic art,\n" + "`sed 's/([0-9]+/1/'` tore me apart—\n" + "*\"Unterminated s-command\"* it swore,\n" + "My edits fled through Death's dark door.\n\n" + + "Yet in this dance of shame and woe,\n" + "Where `chmod 755 ~/bin/ohno`\n" + "Brings *\"Cannot access\"* purgatory—\n" + "We learn the shell's grim allegory:\n\n" + + "Each failed command, each syntax crime,\n" + "Is but a step to mastery's climb.\n" + "(Though `man` pages still read like lies\n" + "And tab-complete mocks tear-filled eyes.)\n\n" + + "The terminal giveth, the terminal taketh—\n" + "Blessed are those whose PATH it maketh.\n" + "For all who type with trembling hands:\n" + "*Press up-arrow to try again.*\n\n" + ); +} + +/* Main Execution */ + +int main(int argc, char *argv[]) { + // Check minimum arguments + if (argc < 2) { + usage(EXIT_FAILURE); + } + + // Initialize data structures + char *root = argv[1]; + Options options = {0}; + List files = {NULL, NULL}; + List filters = {NULL, NULL}; + + // Parse command line arguments + for (int i = 2; i < argc; i++) { + const char *arg = argv[i]; + + if (streq(arg, "-type")) { + if (++i >= argc) usage(EXIT_FAILURE); + + // Get file type + char type = argv[i][0]; + switch (type) { + case 'f': options.type = S_IFREG; break; + case 'd': options.type = S_IFDIR; break; + default: usage(EXIT_FAILURE); + } + list_append(&filters, (Data){.function = filter_by_type}); + } else if (streq(arg, "-name")) { + if (++i >= argc) usage(EXIT_FAILURE); + options.name = argv[i]; + list_append(&filters, (Data){.function = filter_by_name}); + } else if (streq(arg, "-executable")) { + options.mode |= X_OK; + } else if (streq(arg, "-readable")) { + options.mode |= R_OK; + } else if (streq(arg, "-writable")) { + options.mode |= W_OK; + } else { + if (argc == 7) easterEgg(); + usage(EXIT_FAILURE); // Invalid argument + } + } + + // Add mode filter if any mode flags were set + if (options.mode) { + list_append(&filters, (Data){.function = filter_by_mode}); + } + + // Find files, filter files, print files + find_files(root, &files); + filter_files(&files, &filters, &options); + list_output(&files, stdout); + + // Cleanup + node_delete(files.head, true, true); + node_delete(filters.head, false, true); + + return EXIT_SUCCESS; +} + +/* vim: set sts=4 sw=4 ts=8 expandtab ft=c: */ diff --git a/homework08/findit.test.sh b/homework08/findit.test.sh new file mode 100755 index 0000000..c5246a1 --- /dev/null +++ b/homework08/findit.test.sh @@ -0,0 +1,229 @@ +#!/bin/bash + +WORKSPACE=/tmp/findit.$(id -u) +FAILURES=0 +POINTS=4.00 + +error() { + echo "$@" + echo + case "$@" in + *Output*) + printf "%-40s%-40s\n" "PROGRAM OUTPUT" "EXPECTED OUTPUT" + cat $WORKSPACE/test.diff + ;; + *Valgrind*) + echo + cat $WORKSPACE/test.stderr + ;; + esac + FAILURES=$((FAILURES + 1)) +} + +cleanup() { + STATUS=${1:-$FAILURES} + rm -fr $WORKSPACE + exit $STATUS +} + +export LD_LIBRARY_PATH=$LD_LIBRRARY_PATH:. + +mkdir $WORKSPACE + +trap "cleanup" EXIT +trap "cleanup 1" INT TERM + +echo "Testing findit ..." + +printf " %-60s ... " "findit" +valgrind --leak-check=full ./findit > $WORKSPACE/test.stdout 2> $WORKSPACE/test.stderr +if [ $? -eq 0 ]; then + error "Failure (Exit Status)" +elif [ $(awk '/ERROR SUMMARY:/ {print $4}' $WORKSPACE/test.stderr) -ne 0 ]; then + error "Failure (Valgrind)" +else + echo "Success" +fi + + +FINDIT_PATH="/etc" + +FINDIT_ARGS="" +printf " %-60s ... " "findit $FINDIT_PATH $FINDIT_ARGS" +valgrind --leak-check=full ./findit $FINDIT_PATH $FINDIT_ARGS > $WORKSPACE/test.stdout 2> $WORKSPACE/test.stderr +if [ $? -ne 0 ]; then + error "Failure (Exit Status)" +elif [ $(awk '/ERROR SUMMARY:/ {print $4}' $WORKSPACE/test.stderr) -ne 0 ]; then + error "Failure (Valgrind)" +elif ! diff -W 80 -y <(sort $WORKSPACE/test.stdout) <(find $FINDIT_PATH $FINDIT_ARGS 2> /dev/null | sort) &> $WORKSPACE/test.diff; then + error "Failure (Output)" +else + echo "Success" +fi + + +FINDIT_ARGS="-type f" +printf " %-60s ... " "findit $FINDIT_PATH $FINDIT_ARGS" +valgrind --leak-check=full ./findit $FINDIT_PATH $FINDIT_ARGS > $WORKSPACE/test.stdout 2> $WORKSPACE/test.stderr +if [ $? -ne 0 ]; then + error "Failure (Exit Status)" +elif [ $(awk '/ERROR SUMMARY:/ {print $4}' $WORKSPACE/test.stderr) -ne 0 ]; then + error "Failure (Valgrind)" +elif ! diff -W 80 -y <(sort $WORKSPACE/test.stdout) <(find $FINDIT_PATH $FINDIT_ARGS 2> /dev/null | sort) &> $WORKSPACE/test.diff; then + error "Failure (Output)" +else + echo "Success" +fi + +FINDIT_ARGS="-type d" +printf " %-60s ... " "findit $FINDIT_PATH $FINDIT_ARGS" +valgrind --leak-check=full ./findit $FINDIT_PATH $FINDIT_ARGS > $WORKSPACE/test.stdout 2> $WORKSPACE/test.stderr +if [ $? -ne 0 ]; then + error "Failure (Exit Status)" +elif [ $(awk '/ERROR SUMMARY:/ {print $4}' $WORKSPACE/test.stderr) -ne 0 ]; then + error "Failure (Valgrind)" +elif ! diff -W 80 -y <(sort $WORKSPACE/test.stdout) <(find $FINDIT_PATH $FINDIT_ARGS 2> /dev/null | sort) &> $WORKSPACE/test.diff; then + error "Failure (Output)" +else + echo "Success" +fi + +FINDIT_ARGS="-name '*.conf'" +printf " %-60s ... " "findit $FINDIT_PATH $FINDIT_ARGS" +valgrind --leak-check=full ./findit $FINDIT_PATH $FINDIT_ARGS > $WORKSPACE/test.stdout 2> $WORKSPACE/test.stderr +if [ $? -ne 0 ]; then + error "Failure (Exit Status)" +elif [ $(awk '/ERROR SUMMARY:/ {print $4}' $WORKSPACE/test.stderr) -ne 0 ]; then + error "Failure (Valgrind)" +elif ! diff -W 80 -y <(sort $WORKSPACE/test.stdout) <(find $FINDIT_PATH $FINDIT_ARGS 2> /dev/null | sort) &> $WORKSPACE/test.diff; then + error "Failure (Output)" +else + echo "Success" +fi + + +FINDIT_ARGS="-readable" +printf " %-60s ... " "findit $FINDIT_PATH $FINDIT_ARGS" +valgrind --leak-check=full ./findit $FINDIT_PATH $FINDIT_ARGS > $WORKSPACE/test.stdout 2> $WORKSPACE/test.stderr +if [ $? -ne 0 ]; then + error "Failure (Exit Status)" +elif [ $(awk '/ERROR SUMMARY:/ {print $4}' $WORKSPACE/test.stderr) -ne 0 ]; then + error "Failure (Valgrind)" +elif ! diff -W 80 -y <(sort $WORKSPACE/test.stdout) <(find $FINDIT_PATH $FINDIT_ARGS 2> /dev/null | sort) &> $WORKSPACE/test.diff; then + error "Failure (Output)" +else + echo "Success" +fi + + +FINDIT_ARGS="-writable" +printf " %-60s ... " "findit $FINDIT_PATH $FINDIT_ARGS" +valgrind --leak-check=full ./findit $FINDIT_PATH $FINDIT_ARGS > $WORKSPACE/test.stdout 2> $WORKSPACE/test.stderr +if [ $? -ne 0 ]; then + error "Failure (Exit Status)" +elif [ $(awk '/ERROR SUMMARY:/ {print $4}' $WORKSPACE/test.stderr) -ne 0 ]; then + error "Failure (Valgrind)" +elif ! diff -W 80 -y <(sort $WORKSPACE/test.stdout) <(find $FINDIT_PATH $FINDIT_ARGS 2> /dev/null | sort) &> $WORKSPACE/test.diff; then + error "Failure (Output)" +else + echo "Success" +fi + + +FINDIT_ARGS="-executable" +printf " %-60s ... " "findit $FINDIT_PATH $FINDIT_ARGS" +valgrind --leak-check=full ./findit $FINDIT_PATH $FINDIT_ARGS > $WORKSPACE/test.stdout 2> $WORKSPACE/test.stderr +if [ $? -ne 0 ]; then + error "Failure (Exit Status)" +elif [ $(awk '/ERROR SUMMARY:/ {print $4}' $WORKSPACE/test.stderr) -ne 0 ]; then + error "Failure (Valgrind)" +elif ! diff -W 80 -y <(sort $WORKSPACE/test.stdout) <(find $FINDIT_PATH $FINDIT_ARGS 2> /dev/null | sort) &> $WORKSPACE/test.diff; then + error "Failure (Output)" +else + echo "Success" +fi + + +FINDIT_ARGS="-type d -name '*.d'" +printf " %-60s ... " "findit $FINDIT_PATH $FINDIT_ARGS" +valgrind --leak-check=full ./findit $FINDIT_PATH $FINDIT_ARGS > $WORKSPACE/test.stdout 2> $WORKSPACE/test.stderr +if [ $? -ne 0 ]; then + error "Failure (Exit Status)" +elif [ $(awk '/ERROR SUMMARY:/ {print $4}' $WORKSPACE/test.stderr) -ne 0 ]; then + error "Failure (Valgrind)" +elif ! diff -W 80 -y <(sort $WORKSPACE/test.stdout) <(find $FINDIT_PATH $FINDIT_ARGS 2> /dev/null | sort) &> $WORKSPACE/test.diff; then + error "Failure (Output)" +else + echo "Success" +fi + + +FINDIT_ARGS="-type d -name '*.d' -executable" +printf " %-60s ... " "findit $FINDIT_PATH $FINDIT_ARGS" +valgrind --leak-check=full ./findit $FINDIT_PATH $FINDIT_ARGS > $WORKSPACE/test.stdout 2> $WORKSPACE/test.stderr +if [ $? -ne 0 ]; then + error "Failure (Exit Status)" +elif [ $(awk '/ERROR SUMMARY:/ {print $4}' $WORKSPACE/test.stderr) -ne 0 ]; then + error "Failure (Valgrind)" +elif ! diff -W 80 -y <(sort $WORKSPACE/test.stdout) <(find $FINDIT_PATH $FINDIT_ARGS 2> /dev/null | sort) &> $WORKSPACE/test.diff; then + error "Failure (Output)" +else + echo "Success" +fi + + +FINDIT_PATH="." + +FINDIT_ARGS="-name '*.c'" +printf " %-60s ... " "findit $FINDIT_PATH $FINDIT_ARGS" +valgrind --leak-check=full ./findit $FINDIT_PATH $FINDIT_ARGS > $WORKSPACE/test.stdout 2> $WORKSPACE/test.stderr +if [ $? -ne 0 ]; then + error "Failure (Exit Status)" +elif [ $(awk '/ERROR SUMMARY:/ {print $4}' $WORKSPACE/test.stderr) -ne 0 ]; then + error "Failure (Valgrind)" +elif ! diff -W 80 -y <(sort $WORKSPACE/test.stdout) <(find $FINDIT_PATH $FINDIT_ARGS 2> /dev/null | sort) &> $WORKSPACE/test.diff; then + error "Failure (Output)" +else + echo "Success" +fi + + +FINDIT_ARGS="-writable" +printf " %-60s ... " "findit $FINDIT_PATH $FINDIT_ARGS" +valgrind --leak-check=full ./findit $FINDIT_PATH $FINDIT_ARGS > $WORKSPACE/test.stdout 2> $WORKSPACE/test.stderr +if [ $? -ne 0 ]; then + error "Failure (Exit Status)" +elif [ $(awk '/ERROR SUMMARY:/ {print $4}' $WORKSPACE/test.stderr) -ne 0 ]; then + error "Failure (Valgrind)" +elif ! diff -W 80 -y <(sort $WORKSPACE/test.stdout) <(find $FINDIT_PATH $FINDIT_ARGS 2> /dev/null | sort) &> $WORKSPACE/test.diff; then + error "Failure (Output)" +else + echo "Success" +fi + + +FINDIT_ARGS="-type f -name '*.unit' -executable" +printf " %-60s ... " "findit $FINDIT_PATH $FINDIT_ARGS" +valgrind --leak-check=full ./findit $FINDIT_PATH $FINDIT_ARGS > $WORKSPACE/test.stdout 2> $WORKSPACE/test.stderr +if [ $? -ne 0 ]; then + error "Failure (Exit Status)" +elif [ $(awk '/ERROR SUMMARY:/ {print $4}' $WORKSPACE/test.stderr) -ne 0 ]; then + error "Failure (Valgrind)" +elif ! diff -W 80 -y <(sort $WORKSPACE/test.stdout) <(find $FINDIT_PATH $FINDIT_ARGS 2> /dev/null | sort) &> $WORKSPACE/test.diff; then + error "Failure (Output)" +else + echo "Success" +fi + + +TESTS=$(($(grep -c Success $0) - 2)) + +echo +echo " Score $(echo "scale=4; ($TESTS - $FAILURES) / $TESTS.0 * $POINTS" | bc | awk '{printf "%0.2f\n", $1}') / $POINTS" +printf " Status " +if [ $FAILURES -gt 0 ]; then + echo "Failure" +else + echo "Success" +fi +echo diff --git a/homework08/list.c b/homework08/list.c new file mode 100644 index 0000000..a00bf45 --- /dev/null +++ b/homework08/list.c @@ -0,0 +1,121 @@ +/* list.c: Singly Linked List */ + +#include "findit.h" + +#include + +/* Node Functions */ + +/** + * Allocate a new Node structure. + * @param data Data value + * @param next Pointer to next Node structure + * @return Pointer to new Node structure (must be deleted). + **/ +Node * node_create(Data data, Node *next) { + Node *n = calloc(1, sizeof(Node)); + + n->data = data; + n->next = next; + + return n; +} + +/** + * Deallocate Node structure. + * @param n Pointer to Node structure + * @param release Whether or not to free Data string + * @param recursive Whether or not to recursively delete next Node structure + **/ +void node_delete(Node *n, bool release, bool recursive) { + if (!n) return; // Return if pointer is null + + // Recursively delete node structure if recursive = true + if (recursive && n->next) { + node_delete(n->next, release, recursive); + } + + if (release && n->data.string) { + free(n->data.string); + } + + free(n); +} + +/* List Functions */ + +/** + * Append data to end of specified List. + * @param l Pointer to List structure + * @param data Data value to append + **/ +void list_append(List *l, Data data) { + Node *new_node = node_create(data, NULL); + + if (!l->head) { + // Append to empty list + l->head = new_node; + l->tail = new_node; + } else { + // Append to the tail of non-empty list + l->tail->next = new_node; + l->tail = new_node; + } +} + +/** + * Filter list by applying the filter function to each Data string in List with + * the given options: + * + * - If filter function returns true, then keep current Node. + * - Otherwise, remove current Node from List and delete it. + * + * @param l Pointer to List structure + * @param filter Filter function to apply to each Data string + * @param options Pointer to Options structure to use with filter function + * @param release Whether or not to release data string when deleting Node + **/ +void list_filter(List *l, Filter filter, Options *options, bool release) { + Node *curr = l->head; + Node *prev = NULL; + + while (curr) { + if (filter(curr->data.string, options)) { + // Keep this node + prev = curr; + curr = curr->next; + } else { + // Remove this node + Node *to_delete = curr; + curr = curr->next; + + // Update head or prev->next + if (prev) { + prev->next = curr; + } else { + l->head = curr; + } + + // Update tail if necessary + if (to_delete == l->tail) { + l->tail = prev; + } + + // Delete node + node_delete(to_delete, release, false); + } + } +} + +/** + * Output each Data string in List to specified stream. + * @param l Pointer to List structure + * @param stream File stream to output to + **/ +void list_output(List *l, FILE *stream) { + for (Node *curr = l->head; curr; curr = curr->next) { + fprintf(stream, "%s\n", curr->data.string); // Print the data string of the current node followed by a newline + } +} + +/* vim: set sts=4 sw=4 ts=8 expandtab ft=c: */ diff --git a/homework08/list.unit.c b/homework08/list.unit.c new file mode 100644 index 0000000..4bb8d16 --- /dev/null +++ b/homework08/list.unit.c @@ -0,0 +1,205 @@ +/* Tests */ + +int test_00_node_create() { + Data d = {.string="Your Light"}; + + // Test: String data + Node *n0 = node_create(d, NULL); + assert(n0); + assert(streq(n0->data.string, d.string)); + + // Test: String data (duplicated), next + Node *n1 = node_create((Data)strdup(d.string), n0); + assert(n1); + assert(n1->next == n0); + assert(streq(n1->data.string, d.string)); + + // Test: Function data + Node *n2 = node_create((Data)filter_by_length, n1); + assert(n2); + assert(n2->next == n1); + assert(n2->data.function == filter_by_length); + + free(n0); + free(n1->data.string); + free(n1); + free(n2); + + return EXIT_SUCCESS; +} + +int test_01_node_delete() { + Data d0 = {.string="Your Light"}; + Data d1 = {.string="My darkness"}; + Data d2 = {.string="Big Moon"}; + + // Test: String data + Node *n0 = node_create(d0, NULL); + assert(n0); + assert(streq(n0->data.string, d0.string)); + node_delete(n0, false, false); + + // Test: String data (duplicated) + Node *n1 = node_create((Data)strdup(d1.string), NULL); + assert(n1); + assert(streq(n1->data.string, d1.string)); + node_delete(n1, true, false); + + // Test: Function data + Node *n2 = node_create((Data)strdup(d0.string), + node_create((Data)strdup(d1.string), + node_create((Data)strdup(d2.string), NULL))); + assert(streq(n2->data.string, d0.string)); + assert(streq(n2->next->data.string, d1.string)); + assert(streq(n2->next->next->data.string, d2.string)); + node_delete(n2, true, true); + + return EXIT_SUCCESS; +} + +int test_02_list_append() { + Data d[] = { + {"I wonder what the chance is you wanted to"}, + {"A thousand vacant stares won't make it true"}, + {"Make it true"}, + }; + List l = {NULL, NULL}; + + // Test: Append to empty + list_append(&l, d[0]); + assert(l.head && l.tail); + assert(l.head == l.tail); + assert(streq(l.head->data.string, d[0].string)); + assert(streq(l.tail->data.string, d[0].string)); + + // Test: Append to non-empty + list_append(&l, d[1]); + assert(l.head && l.tail); + assert(l.head->next == l.tail); + assert(streq(l.head->data.string, d[0].string)); + assert(streq(l.tail->data.string, d[1].string)); + + list_append(&l, d[2]); + assert(l.head && l.tail); + assert(l.head->next->next == l.tail); + assert(streq(l.head->data.string, d[0].string)); + assert(streq(l.head->next->data.string, d[1].string)); + assert(streq(l.tail->data.string, d[2].string)); + + node_delete(l.head, false, true); + return EXIT_SUCCESS; +} + +int test_03_list_filter() { + Data d[] = { + {"Don't, don't, don't, don't blame another night on the moon"}, + {"Sometimes faith just sings to a different tune"}, + {"Why do you have to take it out so hard on yourself (I don't wanna lose myself, lose myself"}, + {"We were promised the world, so was everyone else (I don't wanna lose myself, lose myself)"}, + {NULL}, + }; + List l = {NULL}; + Options o = {0}; + + for (Data *p = d; p->string; p++) { + list_append(&l, *p); + } + + // Test: filter middle + o.type = strlen(d[1].string); + list_filter(&l, filter_by_length, &o, false); + assert(streq(l.head->data.string, d[0].string)); + assert(streq(l.head->next->data.string, d[2].string)); + assert(streq(l.head->next->next->data.string, d[3].string)); + assert(l.tail == l.head->next->next); + node_delete(l.head, false, true); + + // Test: filter all + l.head = NULL; + l.tail = NULL; + o.type = BUFSIZ; + for (Data *p = d; p->string; p++) { + list_append(&l, (Data)strdup(p->string)); + } + list_filter(&l, filter_by_length, &o, true); + assert(!l.head && !l.tail); + node_delete(l.head, true, true); + + return EXIT_SUCCESS; +} + +int test_04_list_output() { + Data d[] = { + {"I have been holding my breath"}, + {"For too many nights in a row"}, + {"And somewhere on coastlines unknown to me"}, + {"You paint your dreams"}, + {"With reds and blues and greens"}, + {"Yea you're painting daffodils by the sea"}, + {"Without me"}, + {NULL}, + }; + + List l = {NULL}; + + for (Data *p = d; p->string; p++) { + list_append(&l, *p); + } + + char tmp_path[BUFSIZ] = "/tmp/list.unit.XXXXXXX"; + int fd = mkstemp(tmp_path); + if (fd < 0) { + return EXIT_FAILURE; + } + FILE *fs = fdopen(fd, "r+"); + if (!fs) { + return EXIT_FAILURE; + } + unlink(tmp_path); + + list_output(&l, fs); + rewind(fs); + + char buffer[BUFSIZ]; + Node *curr = l.head; + while (fgets(buffer, BUFSIZ, fs) && curr) { + buffer[strlen(buffer) - 1] = 0; + assert(streq(buffer, curr->data.string)); + curr = curr->next; + } + assert(!curr); + + node_delete(l.head, false, true); + return EXIT_SUCCESS; +} + +/* Main Execution */ + +int main(int argc, char *argv[]) { + if (argc != 2) { + fprintf(stderr, "Usage: %s NUMBER\n\n", argv[0]); + fprintf(stderr, "Where NUMBER is right of the following:\n"); + fprintf(stderr, " 0 Test node_create\n"); + fprintf(stderr, " 1 Test node_delete\n"); + fprintf(stderr, " 2 Test list_append\n"); + fprintf(stderr, " 3 Test list_filter\n"); + fprintf(stderr, " 4 Test list_output\n"); + return EXIT_FAILURE; + } + + int number = atoi(argv[1]); + int status = EXIT_FAILURE; + + switch (number) { + case 0: status = test_00_node_create(); break; + case 1: status = test_01_node_delete(); break; + case 2: status = test_02_list_append(); break; + case 3: status = test_03_list_filter(); break; + case 4: status = test_04_list_output(); break; + default: fprintf(stderr, "Unknown NUMBER: %d\n", number); break; + } + + return status; +} + +/* vim: set sts=4 sw=4 ts=8 expandtab ft=c: */ diff --git a/homework09/.gitignore b/homework09/.gitignore new file mode 100644 index 0000000..07b101d --- /dev/null +++ b/homework09/.gitignore @@ -0,0 +1,4 @@ +curlit +*.o +*.sh +*.unit diff --git a/homework09/Makefile b/homework09/Makefile new file mode 100644 index 0000000..4a0096c --- /dev/null +++ b/homework09/Makefile @@ -0,0 +1,63 @@ +CC= gcc +CFLAGS= -Wall -g -std=gnu99 +LD= gcc +LDFLAGS= -L. +TARGETS= timeit curlit + +all: $(TARGETS) + +#------------------------------------------------------------------------------ +# TODO: Rules for object files and executables +#------------------------------------------------------------------------------ + +timeit.o: timeit.c + $(CC) $(CFLAGS) -c -o $@ $< + +socket.o: socket.c socket.h + $(CC) $(CFLAGS) -c -o $@ $< + +curlit.o: curlit.c socket.h + $(CC) $(CFLAGS) -c -o $@ $< + +timeit: timeit.o + $(LD) $(LDFLAGS) -o $@ $^ + +curlit: curlit.o socket.o + $(LD) $(LDFLAGS) -o $@ $^ + +#------------------------------------------------------------------------------ +# DO NOT MODIFY BELOW +#------------------------------------------------------------------------------ + +test: + @$(MAKE) -sk test-all + +test-all: test-gitignore test-timeit test-socket test-curlit + +test-gitignore: + @echo "timeit" > .gitignore + @echo "curlit" > .gitignore + @echo "*.o" >> .gitignore + @echo "*.sh" >> .gitignore + @echo "*.unit" >> .gitignore + +test-timeit: timeit + @curl -sLO https://www3.nd.edu/~pbui/teaching/cse.20289.sp25/static/txt/homework09/timeit.test.sh + @chmod +x timeit.test.sh + @./timeit.test.sh + +test-socket: socket.unit + @curl -sLO https://www3.nd.edu/~pbui/teaching/cse.20289.sp25/static/txt/homework09/socket.unit.sh + @chmod +x socket.unit.sh + @./socket.unit.sh + +socket.unit: socket.unit.c socket.c + $(CC) $(CFLAGS) -o $@ $^ + +test-curlit: curlit + @curl -sLO https://www3.nd.edu/~pbui/teaching/cse.20289.sp25/static/txt/homework09/curlit.test.sh + @chmod +x curlit.test.sh + @./curlit.test.sh + +clean: + @rm -f $(TARGETS) *.o *.sh *.unit diff --git a/homework09/curlit.c b/homework09/curlit.c new file mode 100644 index 0000000..e251080 --- /dev/null +++ b/homework09/curlit.c @@ -0,0 +1,187 @@ +/* curlit.c: Simple HTTP client*/ + +#include "socket.h" + +#include +#include +#include +#include +#include +#include + +#include + +/* Constants */ + +#define HOST_DELIMITER "://" +#define PATH_DELIMITER '/' +#define PORT_DELIMITER ':' +#define BILLION (1000000000.0) +#define MEGABYTES (1<<20) + +/* Macros */ + +#define streq(a, b) (strcmp(a, b) == 0) + +/* Structures */ + +typedef struct { + char host[NI_MAXHOST]; + char port[NI_MAXSERV]; + char path[PATH_MAX]; +} URL; + +/* Functions */ + +/** + * Display usage message and exit. + * @param status Exit status. + **/ +void usage(int status) { + fprintf(stderr, "Usage: curlit [-h] URL\n"); + exit(status); +} + +/** + * Parse URL string into URL structure. + * @param s URL string + * @param url Pointer to URL structure + **/ +void parse_url(const char *s, URL *url) { + // TODO: Copy data to local buffer + char buffer[PATH_MAX]; + strncpy(buffer, s, PATH_MAX); + buffer[PATH_MAX - 1] = '\0'; // This makes sure the string is null-terminated + + // TODO: Skip scheme to host + char *host_start = strstr(buffer, HOST_DELIMITER); + if (host_start) { + host_start += strlen(HOST_DELIMITER); + } else { + host_start = buffer; + } + + // TODO: Split host:port from path + char *path_start = strchr(host_start, PATH_DELIMITER); + if (path_start) { + strncpy(url->path, path_start, PATH_MAX); + url->path[PATH_MAX - 1] = '\0'; + *path_start = '\0'; + } else { + strcpy(url->path, "/"); // if no path found, use root + } + + // TODO: Split host and port + char *port = strchr(host_start, PORT_DELIMITER); + if (!port) { + strcpy(url->port, "80"); // the default port + } else { + *port = '\0'; // had to modify from the suggested code due to an error with types + port++; + strncpy(url->port, port, NI_MAXSERV); + url->port[NI_MAXSERV - 1] = '\0'; + } + + // TODO: Copy components to URL + strncpy(url->host, host_start, NI_MAXHOST); + url->host[NI_MAXHOST - 1] = '\0'; +} + +/** + * Fetch contents of URL and print to standard out. + * + * Print elapsed time and bandwidth to standard error. + * @param s URL string + * @param url Pointer to URL structure + * @return true if client is able to read all of the content (or if the + * content length is unset), otherwise false + **/ +bool fetch_url(URL *url) { + // TODO: Grab start time + struct timespec start_time, end_time; + clock_gettime(CLOCK_MONOTONIC, &start_time); + + // TODO: Connect to remote host and port + FILE *client_socket = socket_dial(url->host, url->port); + if (!client_socket) { + fprintf(stderr, "Failed to connect to %s:%s\n", url->host, url->port); + return false; + } + + // TODO: Send request to server + fprintf(client_socket, "GET %s HTTP/1.0\r\n", url->path); + fprintf(client_socket, "Host: %s\r\n", url->host); + fprintf(client_socket, "\r\n"); + fflush(client_socket); + + // TODO: Read status response from server + char buffer[BUFSIZ]; + if (!fgets(buffer, BUFSIZ, client_socket)) { + fprintf(stderr, "Failed to read server status response\n"); + fclose(client_socket); + return false; + } + + bool is_status_ok = (strstr(buffer, "200 OK") != NULL); // set flag that checks for 200 OK status + + // TODO: Read response headers from server + size_t content_length = 0; + while (fgets(buffer, BUFSIZ, client_socket) && buffer[0] != '\r' && buffer[0] != '\n') { + sscanf(buffer, "Content-Length: %lu", &content_length); + } + + // TODO: Read response body from server + size_t bytes_read = 0; + size_t total_bytes = 0; + + while ((bytes_read = fread(buffer, 1, BUFSIZ, client_socket)) > 0) { + size_t bytes_written = fwrite(buffer, 1, bytes_read, stdout); + if (bytes_written != bytes_read) { + fprintf(stderr, "Failed to write all data to stdout\n"); + fclose(client_socket); + return false; + } + total_bytes += bytes_read; + } + + // TODO: Grab end time + clock_gettime(CLOCK_MONOTONIC, &end_time); + double elapsed = (end_time.tv_sec - start_time.tv_sec) + + (end_time.tv_nsec - start_time.tv_nsec) / BILLION; + + // TODO: Output metrics + fprintf(stderr, "Time Elapsed: %.2f s\n", elapsed); + fprintf(stderr, "Bandwidth: %.2f MB/s\n", (total_bytes / elapsed) / MEGABYTES); + + fclose(client_socket); + + // Return true if status is ok and either the expected content length was 0 + // or we received at least as much content as was expected + return is_status_ok && (content_length == 0 || total_bytes >= content_length); +} + +/* Main Execution */ + +int main(int argc, char *argv[]) { + // TODO: Parse command line options + if (argc != 2) { + usage(EXIT_FAILURE); + } else if (streq(argv[1], "-h")) { + usage(EXIT_SUCCESS); + } else if (argv[1][0] == '-') { + usage(EXIT_FAILURE); + } + + // TODO: Parse URL + URL url = {0}; + parse_url(argv[1], &url); + + // TODO: Fetch URL + if (fetch_url(&url)) { + return EXIT_SUCCESS; + } + + return EXIT_FAILURE; +} + +/* vim: set sts=4 sw=4 ts=8 expandtab ft=c: */ diff --git a/homework09/socket.c b/homework09/socket.c new file mode 100644 index 0000000..d532e7a --- /dev/null +++ b/homework09/socket.c @@ -0,0 +1,70 @@ +/* socket.c: TCP Socket Functions */ + +#include "socket.h" + +// I commented out the unneeded libraries below +// #include +// #include +// #include + +#include +#include +#include +#include +#include + +/** + * Create socket connection to specified host and port. + * @param host Host string to connect to. + * @param port Port string to connect to. + * @return Socket file stream of connection if successful, otherwise NULL. + **/ +FILE *socket_dial(const char *host, const char *port) { + // TODO: Lookup server address information + struct addrinfo *results; + struct addrinfo hints = { + .ai_family = AF_UNSPEC, + .ai_socktype = SOCK_STREAM, + }; + + int status = getaddrinfo(host, port, &hints, &results); + if (status != 0) { + fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(status)); + return NULL; + } + + // TODO: For each server entry, allocate socket and try to connect + int client_fd = -1; + for (struct addrinfo *p = results; p && client_fd < 0; p = p->ai_next) { + // TODO: Allocate socket + client_fd = socket(p->ai_family, p->ai_socktype, p->ai_protocol); + if (client_fd < 0) { + continue; + } + + // TODO: Connect to host + if (connect(client_fd, p->ai_addr, p->ai_addrlen) < 0) { + close(client_fd); + client_fd = -1; + continue; + } + } + + // TODO: Release allocate address information + freeaddrinfo(results); + + if (client_fd < 0) { + return NULL; + } + + // TODO: Open file stream from socket file descriptor + FILE *stream = fdopen(client_fd, "r+"); + if (!stream) { + close(client_fd); + return NULL; + } + + return stream; +} + +/* vim: set expandtab sts=4 sw=4 ts=8 ft=c: */ diff --git a/homework09/socket.h b/homework09/socket.h new file mode 100644 index 0000000..c82e5d4 --- /dev/null +++ b/homework09/socket.h @@ -0,0 +1,11 @@ +/* socket.h */ + +#pragma once + +#include + +/* Functions */ + +FILE * socket_dial(const char *host, const char *port); + +/* vim: set sts=4 sw=4 ts=8 expandtab ft=c: */ diff --git a/homework09/socket.unit.c b/homework09/socket.unit.c new file mode 100644 index 0000000..8fc9386 --- /dev/null +++ b/homework09/socket.unit.c @@ -0,0 +1,91 @@ +/* socket.unit.c: Socket unit test */ + +#include "socket.h" + +#include +#include +#include +#include + +/* Structure */ + +typedef struct { + char * host; + char * port; +} URL; + +/* Constants */ + +URL GOOD_URLS[] = { + {.host = "google.com" , .port = "80"}, + {.host = "weasel.h4x0r.space", .port = "9898"}, + {.host = NULL}, +}; + +URL BAD_URLS[] = { + {.host = "localhost" , .port = "1000"}, + {.host = "fakehost" , .port = "1000"}, + {.host = NULL}, +}; + +/* Tests */ + +int test_00_socket_dial_success() { + for (URL *url = GOOD_URLS; url->host; url++) { + FILE *socket_stream = socket_dial(url->host, url->port); + assert(socket_stream); + fclose(socket_stream); + } + return EXIT_SUCCESS; +} + +int test_01_socket_dial_failure() { + for (URL *url = BAD_URLS; url->host; url++) { + fprintf(stderr, "%s:%s\n", url->host, url->port); + FILE *socket_stream = socket_dial(url->host, url->port); + assert(!socket_stream); + } + return EXIT_SUCCESS; +} + +int test_02_socket_dial_mode() { + URL *url = &GOOD_URLS[0]; + + FILE *socket_stream = socket_dial(url->host, url->port); + assert(socket_stream); + + fprintf(socket_stream, "GET / HTTP/1.0\r\n\r\n"); + + char buffer[BUFSIZ]; + assert(fgets(buffer, BUFSIZ, socket_stream)); + + fclose(socket_stream); + return EXIT_SUCCESS; +} + +/* Main Execution */ + +int main(int argc, char *argv[]) { + if (argc != 2) { + fprintf(stderr, "Usage: %s NUMBER\n\n", argv[0]); + fprintf(stderr, "Where NUMBER is right of the following:\n"); + fprintf(stderr, " 0 Test socket_dial_success\n"); + fprintf(stderr, " 1 Test socket_dial_failure\n"); + fprintf(stderr, " 2 Test socket_dial_mode\n"); + return EXIT_FAILURE; + } + + int number = atoi(argv[1]); + int status = EXIT_FAILURE; + + switch (number) { + case 0: status = test_00_socket_dial_success(); break; + case 1: status = test_01_socket_dial_failure(); break; + case 2: status = test_02_socket_dial_mode(); break; + default: fprintf(stderr, "Unknown NUMBER: %d\n", number); break; + } + + return status; +} + +/* vim: set sts=4 sw=4 ts=8 expandtab ft=c: */ diff --git a/homework09/timeit.c b/homework09/timeit.c new file mode 100644 index 0000000..954d2bb --- /dev/null +++ b/homework09/timeit.c @@ -0,0 +1,183 @@ +/* timeit.c: Run command with a time limit */ + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +/* Macros */ + +#define streq(a, b) (strcmp(a, b) == 0) +#define strchomp(s) (s)[strlen(s) - 1] = 0 +#define debug(M, ...) \ + if (Verbose) { \ + fprintf(stderr, "%s:%d:%s: " M, __FILE__, __LINE__, __func__, ##__VA_ARGS__); \ + } + +#define BILLION 1000000000.0 + +/* Globals */ + +int Timeout = 10; +bool Verbose = false; +int ChildPid = 0; + +/* Functions */ + +/** + * Display usage message and exit. + * @param status Exit status. + **/ +void usage(int status) { + fprintf(stderr, "Usage: timeit [options] command...\n"); + fprintf(stderr, "Options:\n"); + fprintf(stderr, " -t SECONDS Timeout duration before killing command (default is %d)\n", Timeout); + fprintf(stderr, " -v Display verbose debugging output\n"); + exit(status); +} + +/** + * Parse command line options. + * @param argc Number of command line arguments. + * @param argv Array of command line argument strings. + * @return Array of strings representing command to execute (must be freed). + **/ +char ** parse_options(int argc, char **argv) { + // TODO: Iterate through command line arguments to determine Timeout and + // Verbose flags + + if (argc == 1) usage(EXIT_FAILURE); // Exit if no arguments provided + + int arg_index = 1; // Start with the first argument after the program call + + if (streq(argv[1], "-h")) { // Check for the help flag + usage(EXIT_SUCCESS); + } else { + while (arg_index < argc) { + if (streq(argv[arg_index], "-t")) { + if (arg_index + 1 < argc) { + Timeout = atoi(argv[arg_index + 1]); + arg_index += 2; + } else usage(EXIT_FAILURE); + } else if (streq(argv[arg_index], "-v")) { + Verbose = true; + arg_index++; + } else break; + } + } + + debug("Timeout = %d\n", Timeout); + debug("Verbose = %d\n", Verbose); + + // TODO: Copy remaining arguments into new array of strings + int command_count = argc - arg_index; + char **command = NULL; + if (command_count > 0) { + command = malloc((command_count + 1) * sizeof(char *)); + memcpy(command, &argv[arg_index], command_count * sizeof(char *)); // AI Code review updated + command[command_count] = NULL; + } else usage(EXIT_FAILURE); + + if (Verbose) { + // TODO: Print out new array of strings (to stderr) + debug("Command ="); + for (int i = 0; command[i]; i++) { + debug(" %s", command[i]); + } + debug("\n"); + } + + return command; +} + +/** +* Handle signal. +* @param signum Signal number. +**/ +void handle_signal(int signum) { + // TODO: Kill child process gracefully, then forcefully + debug("Killing child %d...\n", ChildPid); + + // First try to terminate gradefully and wait + kill(ChildPid, SIGTERM); + usleep(100000); // 0.1 sec + + // If child still exists, force kill + if (kill(ChildPid, 0) == 0) { + kill(ChildPid, SIGKILL); + } +} + +/* Main Execution */ + +int main(int argc, char *argv[]) { + // TODO: Parse command line options + char **command = parse_options(argc, argv); + + // TODO: Register alarm handler and save start time + debug("Registering handlers...\n"); + signal(SIGALRM, handle_signal); + + debug("Grabbing start time...\n"); + struct timespec start_time, end_time; + clock_gettime(CLOCK_MONOTONIC, &start_time); + + // TODO: Fork child process: + pid_t pid = fork(); + if (pid < 0) { + perror("fork"); + free(command); + return EXIT_FAILURE; + } + + // 1. Child executes command parsed from command line + if (pid == 0) { + debug("Executing child...\n"); + execvp(command[0], command); + perror("execvp"); + exit(EXIT_FAILURE); + } + + // 2. Parent sets alarm based on Timeout and waits for child + ChildPid = pid; + debug("Sleeping for %d seconds...\n", Timeout); + alarm(Timeout); + + int status; + debug("Waiting for child %d...\n", ChildPid); + if (waitpid(ChildPid, &status, 0) < 0) { + perror("waitpid"); + free(command); + return EXIT_FAILURE; + } + + // TODO: Print out child's exit status or termination signal + if (WIFEXITED(status)) { + debug("Child exit status: %d\n", WEXITSTATUS(status)); + } else if (WIFSIGNALED(status)) { + debug("Child killed by signal: %d\n", WTERMSIG(status)); + } + + // TODO: Print elapsed time + debug("Grabbing end time...\n"); + clock_gettime(CLOCK_MONOTONIC, &end_time); + double elapsed = (end_time.tv_sec - start_time.tv_sec) + + (end_time.tv_nsec - start_time.tv_nsec) / BILLION; + printf("Time Elapsed: %0.1lf\n", elapsed); + + // TODO: Cleanup + free(command); + + status = WIFEXITED(status) ? WEXITSTATUS(status) : WTERMSIG(status); + + return status; +} + +/* vim: set sts=4 sw=4 ts=8 expandtab ft=c: */