From 6082e7a34a6f488256cf6c460c5040bc938df940 Mon Sep 17 00:00:00 2001 From: mivirl <> Date: Tue, 21 May 2024 19:20:30 -0500 Subject: [PATCH 1/1] Initial commit --- README.md | 15 ++++ makefile | 69 +++++++++++++++ src/log.h | 61 ++++++++++++++ src/test/ex1.txt | 12 +++ src/test/ex2.txt | 6 ++ src/test/ex3.txt | 9 ++ src/test/test.sh | 6 ++ src/typetest.c | 213 +++++++++++++++++++++++++++++++++++++++++++++++ 8 files changed, 391 insertions(+) create mode 100644 README.md create mode 100644 makefile create mode 100644 src/log.h create mode 100644 src/test/ex1.txt create mode 100644 src/test/ex2.txt create mode 100644 src/test/ex3.txt create mode 100755 src/test/test.sh create mode 100644 src/typetest.c diff --git a/README.md b/README.md new file mode 100644 index 0000000..3128b32 --- /dev/null +++ b/README.md @@ -0,0 +1,15 @@ +# Typing test + +A simple typing test program that reads input from either a file or stdin. +Prints the input file one line at a time without showing feedback until a +line has been submitted by the user. Indicates mistakes after the whole line +has been entered. + +Ignores leading whitespace, either from the file or entered by the user. + +## Build & run + +```sh +make +./build/typetest /path/to/file +``` diff --git a/makefile b/makefile new file mode 100644 index 0000000..ddb07dc --- /dev/null +++ b/makefile @@ -0,0 +1,69 @@ +# Makefile based on https://spin.atomicobject.com/makefile-c-projects/ +TARGET_EXEC ?= typetest + +BUILD_DIR ?= ./build +SRC_DIRS ?= ./src + +SRCS := $(shell find $(SRC_DIRS) -name *.cpp -or -name *.c -or -name *.s) +OBJS := $(SRCS:%=$(BUILD_DIR)/%.o) +DEPS := $(OBJS:.o=.d) + +INC_DIRS := $(shell find $(SRC_DIRS) -type d) +INC_FLAGS := $(addprefix -I,$(INC_DIRS)) + +# Flags from https://best.openssf.org/Compiler-Hardening-Guides/Compiler-Options-Hardening-Guide-for-C-and-C++.html +CPPFLAGS ?= $(INC_FLAGS) -MMD -MP \ + -O2 -Wall -Wformat -Wformat=2 -Wconversion -Wimplicit-fallthrough \ + -Werror=format-security \ + -U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=3 \ + -D_GLIBCXX_ASSERTIONS \ + -fstack-clash-protection -fstack-protector-strong \ + -Wl,-z,nodlopen -Wl,-z,noexecstack \ + -Wl,-z,relro -Wl,-z,now \ + -fPIE -pie \ + -fno-delete-null-pointer-checks -fno-strict-overflow -fno-strict-aliasing \ + -Werror=implicit -Werror=incompatible-pointer-types -Werror=int-conversion \ + -fexceptions + +$(BUILD_DIR)/$(TARGET_EXEC): $(OBJS) + $(CC) $(CPPFLAGS) $(OBJS) -o $@ $(LDFLAGS) + +# Assembly +$(BUILD_DIR)/%.s.o: %.s + mkdir -p $(dir $@) + $(AS) $(ASFLAGS) -c $< -o $@ + +# C source +$(BUILD_DIR)/%.c.o: %.c + mkdir -p $(dir $@) + $(CC) $(CPPFLAGS) $(CFLAGS) -c $< -o $@ + +# C++ source +$(BUILD_DIR)/%.cpp.o: %.cpp + mkdir -p $(dir $@) + $(CXX) $(CPPFLAGS) $(CXXFLAGS) -c $< -o $@ + +.PHONY: debug release tidy format clangdb clean test + +debug: CPPFLAGS += -g -fsanitize=address -fsanitize=pointer-compare -fsanitize=pointer-subtract -fsanitize=leak -fno-omit-frame-pointer -fsanitize=undefined -fsanitize=bounds-strict -fsanitize=float-divide-by-zero -fsanitize=float-cast-overflow +debug: $(BUILD_DIR)/$(TARGET_EXEC) + +release: CPPFLAGS += -fno-delete-null-pointer-checks -fno-strict-overflow -fno-strict-aliasing -ftrivial-auto-var-init=zero +release: $(BUILD_DIR)/$(TARGET_EXEC) + +clangdb: clean + bear -- make + +tidy: + clang-tidy $(SRCS) + +format: + clang-format --style=WebKit $(SRCS) + +clean: + $(RM) -r $(BUILD_DIR) + +test: $(BUILD_DIR)/$(TARGET_EXEC) + ./src/test/test.sh + +-include $(DEPS) diff --git a/src/log.h b/src/log.h new file mode 100644 index 0000000..e0860a0 --- /dev/null +++ b/src/log.h @@ -0,0 +1,61 @@ +#ifndef LOG_H +#define LOG_H + +#include +#include +#include + + +/* Logging macros +Usage: +- Define int LOGLVL +- Use logging macros for corresponding level with printf format string +- Use assert macros to test conditions and log/handle errors +*/ + +/* Use to not jump to the error label when using ASSERT: + #define GOTO_ERROR +*/ + +extern int LOGLVL; +#define LOGLVL_ERROR 0 +#define LOGLVL_WARN 1 +#define LOGLVL_INFO 2 +#define LOGLVL_DEBUG 3 + +#define ERRNOMSG(x) (errno ? strerror(errno) : "n/a") + +#define LOG_ERROR(...) if (LOGLVL >= LOGLVL_ERROR) {\ + fprintf (stderr, "[ERROR]\t[%s:%d, %s; ERRNO: %s]\t", __FILE__, __LINE__,\ + __func__, ERRNOMSG(errno));\ + fprintf (stderr, __VA_ARGS__);\ + fprintf (stderr, "\n"); } + +#define LOG_WARN(...) if (LOGLVL >= LOGLVL_WARN) {\ + fprintf (stderr, "[WARN]\t[%s:%d, %s; ERRNO: %s]\t", __FILE__, __LINE__,\ + __func__, ERRNOMSG(errno));\ + fprintf (stderr, __VA_ARGS__);\ + fprintf (stderr, "\n"); } + +#define LOG_INFO(...) if (LOGLVL >= LOGLVL_INFO) {\ + fprintf (stderr, "[INFO]\t[%s:%d, %s; ERRNO: %s]\t", __FILE__, __LINE__,\ + __func__, ERRNOMSG(errno));\ + fprintf (stderr, __VA_ARGS__);\ + fprintf (stderr, "\n"); } + +#define LOG_DEBUG(...) if (LOGLVL >= LOGLVL_DEBUG) {\ + fprintf (stderr, "[DEBUG]\t[%s:%d, %s; ERRNO: %s]\t", __FILE__, __LINE__,\ + __func__, ERRNOMSG(errno));\ + fprintf (stderr, __VA_ARGS__);\ + fprintf (stderr, "\n"); } + +#ifndef GOTO_ERROR +#define GOTO_ERROR goto error +#endif + +#define ASSERT(x, ...) if (!(x)) { LOG_ERROR(__VA_ARGS__); errno = 0; GOTO_ERROR;} +#define ASSERT_WARN(x, ...) if (!(x)) { LOG_WARN(__VA_ARGS__); errno = 0; GOTO_ERROR;} +#define ASSERT_INFO(x, ...) if (!(x)) { LOG_INFO(__VA_ARGS__); errno = 0; } +#define ASSERT_DEBUG(x, ...) if (!(x)) { LOG_DEBUG(__VA_ARGS__); errno = 0; } + +#endif diff --git a/src/test/ex1.txt b/src/test/ex1.txt new file mode 100644 index 0000000..00bad94 --- /dev/null +++ b/src/test/ex1.txt @@ -0,0 +1,12 @@ +This is a test + +This is a test1 +This is a test2 + + + + + +This is a test3 +This is a test4 +This is a test5 diff --git a/src/test/ex2.txt b/src/test/ex2.txt new file mode 100644 index 0000000..d9f7740 --- /dev/null +++ b/src/test/ex2.txt @@ -0,0 +1,6 @@ + + + + +b + diff --git a/src/test/ex3.txt b/src/test/ex3.txt new file mode 100644 index 0000000..5765a5e --- /dev/null +++ b/src/test/ex3.txt @@ -0,0 +1,9 @@ + + + +as + + asdfls + askdlf + a asdf + a asdf diff --git a/src/test/test.sh b/src/test/test.sh new file mode 100755 index 0000000..4ff0266 --- /dev/null +++ b/src/test/test.sh @@ -0,0 +1,6 @@ +#!/bin/sh + +TEST_BINARY=./build/typetest +TEST_DIR=./src/test + +find "$TEST_DIR" -name "ex*.txt" -exec "$TEST_BINARY" {} \; diff --git a/src/typetest.c b/src/typetest.c new file mode 100644 index 0000000..708c86f --- /dev/null +++ b/src/typetest.c @@ -0,0 +1,213 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include "log.h" + +#define C_BLACK "\x1b[30m" +#define C_RED "\x1b[31m" +#define C_GREEN "\x1b[32m" +#define C_YELLOW "\x1b[33m" +#define C_BLUE "\x1b[34m" +#define C_MAGENTA "\x1b[35m" +#define C_CYAN "\x1b[36m" +#define C_WHITE "\x1b[37m" +#define C_NONE "\x1b[m" + +#define LOGLVL LOGLVL_WARN + +// Limit maximum number of lines and length of lines +#define LINE_LENGTH 120 +#define LINE_COUNT 40 + + +// Disable terminal echo +struct termios setnoecho(int term) { + struct termios t_orig; + tcgetattr(term, &t_orig); + + struct termios t = t_orig; + t.c_lflag &= (unsigned int) ~ECHO; + tcsetattr(term, TCSANOW, &t); + + return t_orig; +} + +// Restore original terminal settings +void setecho(int term, struct termios t_orig) { + tcsetattr(term, TCSANOW, &t_orig); +} + +// Read lines from file into an array of lines +char **readlines(char **linearray, unsigned long num_lines, char *filename, bool use_stdin) { + FILE *file = NULL; + + ASSERT(linearray != NULL, "No line array provided"); + + if (use_stdin) { + file = stdin; + } else { + ASSERT(filename != NULL, "No filename provided"); + file = fopen(filename, "r"); + ASSERT(file != NULL, "Failed to open file %s", filename); + } + + // Read lines from file into linearray + char *line = NULL; + for (unsigned int i = 0; !feof(file) && i < num_lines; ++i) { + line = calloc(1, LINE_LENGTH * sizeof(char)); + + char *r = fgets(line, LINE_LENGTH, file); + if (r == NULL) { + free(line); + continue; + } + ASSERT(ferror(file) == 0, "Failed to read from file"); + + // Don't add blank lines + bool no_printable_chars = true; + for (unsigned int j = 0; j < LINE_LENGTH; ++j) { + if (line[j] == '\0') break; + if (isprint(line[j])) { + no_printable_chars = false; + break; + } + } + if (no_printable_chars || line[0] == '\n') { + free(line); + --i; + continue; + } + linearray[i] = line; + line = NULL; + } + + return linearray; + +error: + if (file != NULL) { + ASSERT(fclose(file) != EOF, "Failed to close file"); + } + return NULL; +} + +// Read line of input into a string +char *getinput(FILE *instream, char *line) { + if (fgets(line, LINE_LENGTH, instream) == NULL && feof(instream)) { + free(line); + return NULL; + } + return line; +} + +// Compare input to a template string and color characters that are the same +// green and different characters red +void checkinput(char *template, char *input) { + printf("%s", C_GREEN); + + unsigned long template_len = strlen(template); + unsigned long input_len = strlen(input); + + if (input_len == 0) { + printf("\n"); + return; + } + + // Ignore leading whitespace + bool leading_whitespace = true; + for (unsigned long i = 0, j = 0; i < template_len && j < input_len; ++i, ++j) { + // Advance to at least first non-whitespace character + if (leading_whitespace) { + while (!isgraph(template[i]) && i < template_len) { + ++i; + } + while (!isgraph(input[j]) && j < input_len) { + ++j; + } + leading_whitespace = false; + } + + if (template[i] == input[j]) { + printf("%c", template[i]); + } else { + printf("%s%c%s", C_RED, input[j], C_GREEN); + } + } + printf("\n%s", C_NONE); +} + +// Interactive typing loop +void loop(char **linearray, FILE *instream) { + char *input = malloc(LINE_LENGTH); + for (unsigned int i = 0; i < LINE_COUNT; ++i) { + if (linearray[i] == NULL || strcmp(linearray[i], "") == 0) { + break; + } + printf("%s>%s %s%s ", C_CYAN, C_YELLOW, linearray[i], C_NONE); + + getinput(instream, input); + if (input == NULL) return; + + checkinput(linearray[i], input); + } + free(input); +} + +int main(int argc, char **argv) { + bool use_stdin = false; + + if (argc > 2 || (argc == 1 && isatty(0))) { + printf("Usage: %s filename\n", argv[0]); + return 1; + } else { + // Reading input from a pipe + if (!isatty(0)) { + use_stdin = true; + } + } + FILE *termf = NULL; + + char **linearray = calloc(LINE_COUNT, LINE_LENGTH * sizeof(char *)); + ASSERT(linearray != NULL, "Failed to allocate linearray"); + + char **rl = readlines(linearray, LINE_COUNT, argv[1], use_stdin); + if (rl == NULL) return 1; + + // Get path for controlling terminal and open fd + char termpath[L_ctermid]; + ctermid(termpath); + termf = fopen(termpath, "r+"); + int term = fileno(termf); + + // Disable echoing inputted characters + struct termios t_orig = setnoecho(term); + + // Run the typing test + loop(linearray, termf); + + // Reenable echoing + setecho(term, t_orig); + + for (unsigned int i = 0; i < LINE_COUNT; ++i) { + free(linearray[i]); + } + free(linearray); + if (termf) { + if (fclose(termf) == EOF) return 1; + } + return 0; + +error: + for (unsigned int i = 0; i < LINE_COUNT; ++i) { + free(linearray[i]); + } + free(linearray); + if (termf) { + if (fclose(termf) == EOF) return 1; + } + return 1; +} -- 2.39.5