aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.github/workflows/cmake.yml45
-rw-r--r--CMakeLists.txt14
-rw-r--r--main.c320
-rw-r--r--tests.c353
-rw-r--r--version_compare.c369
-rw-r--r--version_compare.h18
6 files changed, 800 insertions, 319 deletions
diff --git a/.github/workflows/cmake.yml b/.github/workflows/cmake.yml
new file mode 100644
index 0000000..8e3eb97
--- /dev/null
+++ b/.github/workflows/cmake.yml
@@ -0,0 +1,45 @@
+name: CMake
+
+on:
+ push:
+ branches: [ master ]
+ pull_request:
+ branches: [ master ]
+
+jobs:
+ tests:
+ name: ${{ matrix.name }}
+ runs-on: ${{ matrix.os }}
+ strategy:
+ fail-fast: true
+ matrix:
+ include:
+ - name: Debug (Linux)
+ os: ubuntu-latest
+ build_type: Debug
+
+ - name: Debug (MacOS)
+ os: macos-latest
+ build_type: Debug
+
+ - name: Release (Linux)
+ os: ubuntu-latest
+ build_type: Release
+
+ - name: Release (MacOS)
+ os: macos-latest
+ build_type: Release
+
+ steps:
+ - uses: actions/checkout@v2
+
+ - name: Configure CMake
+ run: cmake -B ${{ github.workspace }}/build -DCMAKE_BUILD_TYPE=${{ matrix.build_type }}
+
+ - name: Build
+ run: cmake --build ${{ github.workspace }}/build --config ${{ matrix.build_type }}
+
+ - name: Test
+ working-directory: ${{ github.workspace }}/build
+ run: ctest -V -C ${{ matrix.build_type }}
+
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 8ccb593..f780fe9 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -11,4 +11,16 @@ else()
add_compile_options(-Wall -Wextra -pedantic)
endif()
-add_executable(version_compare main.c)
+include(CTest)
+
+add_library(vcmp STATIC version_compare.c version_compare.h)
+target_compile_definitions(vcmp PUBLIC ENABLE_TESTING=1)
+
+add_executable(test_version_compare tests.c version_compare.h)
+target_link_libraries(test_version_compare vcmp)
+
+add_test(test test_version_compare)
+add_executable(version_compare main.c version_compare.h)
+target_link_libraries(version_compare vcmp)
+
+
diff --git a/main.c b/main.c
index 0f08e88..77df8a9 100644
--- a/main.c
+++ b/main.c
@@ -1,321 +1,5 @@
-#include <stdio.h>
-#include <stdlib.h>
-#include <string.h>
-#include <ctype.h>
-
-#define GT 1 << 1
-#define LT 1 << 2
-#define EQ 1 << 3
-#define NOT 1 << 4
-
-/**
- * Remove leading whitespace from string
- * @param s input string
- * @return pointer to string
- */
-char *lstrip(char **s) {
- char *orig;
- char *end;
-
- orig = (*s);
- if (!isblank(*(*s)) || *(*s) == '\0') {
- return (*s);
- }
-
- while (isblank(*(*s)) && *(*s) != '\0') {
- end = (*s);
- (*s)++;
- }
- memmove(orig, (*s), strlen(end));
-
- (*s) = orig;
- return (*s);
-}
-
-/**
- * Remove tailing whitespace from string
- * @param s input string
- * @return pointer to string
- */
-char *rstrip(char **s) {
- char *orig;
- char *end;
-
- orig = (*s);
- if (strlen((*s)) > 1) {
- (*s) = (*s) + strlen((*s)) - 1;
- } else {
- return (*s);
- }
-
- if (!isblank(*(*s)) || *(*s) == '\0') {
- (*s) = orig;
- return (*s);
- }
-
- while (isblank(*(*s)) && *(*s) != '\0') {
- end = (*s);
- (*s)--;
- }
- *end = '\0';
-
- (*s) = orig;
- return (*s);
-}
-
-/**
- * Reduces multiple whitespace between characters
- * @param s input string
- * @return pointer to string
- */
-char *collapse_whitespace(char **s) {
- char *orig;
- char *space;
-
- lstrip(s);
- rstrip(s);
-
- space = NULL;
- orig = (*s);
- while (*(*s) != '\0') {
- char *space_next;
-
- if (space) {
- int numspace = 0;
- while (isblank(*space) && *space != '\0') {
- numspace++;
- space++;
- }
-
- //memmove((*s), space, numspace);
- memmove((*s), space, strlen(space) + numspace);
- (*s)[strlen((*s))] = '\0';
- space = NULL;
- }
-
- space_next = (*s) + 1;
- if (isblank(*(*s)) && (space_next != NULL && isblank(*space_next))) {
- space = (*s);
- (*s)++;
- continue;
- }
-
- (*s)++;
- }
- (*s) = orig;
- return (*s);
-}
-
-/**
- * Sum each part of a '.'-delimited version string
- * @param str version string
- * @return sum of each part
- */
-int version_sum(const char *str) {
- int result;
- char *s, *ptr, *end;
-
- result = 0;
- s = strdup(str);
- ptr = s;
- end = ptr;
-
- // Parsing stops at the first non-alpha, non-'.' character
- // Digits are processed until the first invalid character
- // I'm torn whether this should be considered an error
- while (end != NULL) {
- result += (int) strtoul(ptr, &end, 10);
- ptr = end;
- if (*ptr == '.')
- ptr++;
- else if (isalpha(*ptr)) {
- result += *ptr - ('a' - 1);
- ptr++;
- }
- else
- end = NULL;
- }
-
- free(s);
- return result;
-}
-
-/**
- * Convert version operator(s) to flags
- * @param str input string
- * @return operator flags
- */
-int version_parse_operator(char *str) {
- const char *valid = "><=!";
- char *pos;
- char *strp;
- int result;
-
- strp = str;
- pos = strp;
- result = 0;
- while ((pos = strpbrk(pos, valid)) != NULL) {
- switch (*pos) {
- case '>':
- result |= GT;
- break;
- case '<':
- result |= LT;
- break;
- case '=':
- result |= EQ;
- break;
- case '!':
- result |= NOT;
- break;
- }
- strp = pos++;
- }
-
- return result;
-}
-
-/**
- * Compare version strings based on flag(s)
- * @param flags verison operators
- * @param aa version1
- * @param bb version2
- * @return 1 flag operation is true
- * @return 0 flag operation is false
- */
-int version_compare(int flags, const char *aa, const char *bb) {
- int result, result_a, result_b;
-
- result_a = version_sum(aa);
- result_b = version_sum(bb);
- result = 0;
-
- if (flags & GT && flags & EQ)
- result |= result_a >= result_b;
- else if (flags & LT && flags & EQ)
- result |= result_a <= result_b;
- else if (flags & NOT && flags & EQ)
- result |= result_a != result_b;
- else if (flags & GT)
- result |= result_a > result_b;
- else if (flags & LT)
- result |= result_a < result_b;
- else if (flags & EQ)
- result |= result_a == result_b;
-
- return result;
-}
-
-void usage(char *prog) {
- char *name;
- name = strrchr(prog, '/');
- if (!name)
- name = prog;
- else
- name++;
-
- char *examples[] = {
- "{v} execution example:\n",
- " %s \"1.2.3 > 1.2.3\"\n",
- " 0\n",
- " %s \"1.2.3 >= 1.2.3\"\n",
- " 1\n",
- " %s \"1.2.3 < 1.2.3\"\n",
- " 0\n",
- " %s \"1.2.3 <= 1.2.3\"\n",
- " 1\n",
- " %s \"1.2.3 != 1.2.3\"\n",
- " 0\n",
- " %s \"1.2.3 = 1.2.3\"\n",
- " 1\n",
- "\n",
- "{v1} {operator} {v2} execution example:\n",
- " %s \"1.2.3\" \">\" \"1.2.3\"\n",
- " 0\n",
- " %s \"1.2.3\" \">=\" \"1.2.3\"\n",
- " 1\n",
- " %s \"1.2.3\" \"<\" \"1.2.3\"\n",
- " 0\n",
- " %s \"1.2.3\" \"<=\" \"1.2.3\"\n",
- " 1\n",
- " %s \"1.2.3\" \"!=\" \"1.2.3\"\n",
- " 0\n",
- " %s \"1.2.3\" \"=\" \"1.2.3\"\n",
- " 1\n",
- NULL,
- };
-
- printf("usage: %s {{v} | {v1} {operator} {v2}}\n", name);
- for (int i = 0; examples[i] != NULL; i++) {
- char *output;
-
- output = examples[i];
- if (strchr(examples[i], '%')) {
- printf(output, name);
- } else {
- printf("%s", output);
- }
- }
- puts("");
-}
+#include "version_compare.h"
int main(int argc, char *argv[]) {
- int result, op, must_free;
- char *v1, *v2, *operator, *arg, *arg_orig, *token;
- char *tokens[4] = {NULL, NULL, NULL, NULL};
-
- if (argc < 2) {
- fprintf(stderr, "Not enough arguments.\n");
- usage(argv[0]);
- exit(1);
- }
-
- must_free = 0;
- if (argc < 3) {
- int i;
- i = 0;
- arg = strdup(argv[1]);
- arg_orig = arg;
- collapse_whitespace(&arg);
-
- for (; (token = strsep(&arg, " ")) != NULL; i++) {
- tokens[i] = strdup(token);
- }
- arg = arg_orig;
-
- if (i < 3) {
- fprintf(stderr, "Invalid version spec (missing whitespace or token?): '%s'\n", argv[1]);
- usage(argv[0]);
- exit(-2);
- }
-
- v1 = tokens[0];
- operator = tokens[1];
- v2 = tokens[2];
- must_free = 1;
- } else {
- collapse_whitespace(&argv[1]);
- collapse_whitespace(&argv[2]);
- collapse_whitespace(&argv[3]);
- v1 = argv[1];
- operator = argv[2];
- v2 = argv[3];
- }
-
- op = version_parse_operator(operator);
- if (op < 0) {
- fprintf(stderr, "Invalid operator sequence: %s\n", operator);
- exit(1);
- }
-
- result = version_compare(op, v1, v2);
- printf("%d\n", result);
-
- if (must_free) {
- for (int i = 0; tokens[i] != NULL; i++) {
- free(tokens[i]);
- }
- free(arg);
- }
- return 0;
+ return entry(argc, argv);
}
diff --git a/tests.c b/tests.c
new file mode 100644
index 0000000..0eb56ef
--- /dev/null
+++ b/tests.c
@@ -0,0 +1,353 @@
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include "version_compare.h"
+
+struct TestCase_strings {
+ char *s;
+ const char *result;
+};
+
+static struct TestCase_strings test_cases_collapse_whitespace[] = {
+ {"", ""},
+ {" ", ""},
+ {" ", ""},
+ {" leading", "leading"},
+ {"trailing ", "trailing"},
+ {" leading", "leading"},
+ {" leading and trailing ", "leading and trailing"},
+ {"This line will be collapsed", "This line will be collapsed"},
+};
+
+static struct TestCase_strings test_cases_lstrip[] = {
+ {"", ""},
+ {" ", ""},
+ {" ", ""},
+ {" leading", "leading"},
+ {"trailing ", "trailing "},
+ {" leading", "leading"},
+ {" leading and trailing ", "leading and trailing "},
+ {"This line will be collapsed", "This line will be collapsed"},
+};
+
+static struct TestCase_strings test_cases_rstrip[] = {
+ {"", ""},
+ {" ", ""},
+ {" ", ""},
+ {" leading", " leading"},
+ {"trailing ", "trailing"},
+ {" leading", " leading"},
+ {" leading and trailing ", " leading and trailing"},
+ {"This line will be collapsed", "This line will be collapsed"},
+};
+
+struct TestCase_version_compare {
+ char *a, *op, *b;
+ int result;
+};
+
+static struct TestCase_version_compare test_cases_version_compare[] = {
+ {"0", "=", "0", 1},
+ {"0", "<", "1",1},
+ {"0", "<=", "1",1},
+ {"0", ">", "1",0},
+ {"0", ">=", "1",0},
+ {"0", "!=", "1",1},
+
+ {"1a", "=", "1b", 0},
+ {"1a", "<", "1b", 1},
+ {"1a", "<=", "1b", 1},
+ {"1a", ">", "1b", 0},
+ {"1a", ">=", "1b", 0},
+ {"1a", "!=", "1b", 1},
+
+ {"1.0", "=", "1.0.0", 1},
+ {"1.0", "<", "1.0.0", 0},
+ {"1.0", "<=", "1.0.0", 1},
+ {"1.0", ">", "1.0.0", 0},
+ {"1.0", ">=", "1.0.0", 1},
+ {"1.0", "!=", "1.0.0", 0},
+
+ {"1.0a", "=", "1.0.0", 0},
+ {"1.0a", "<", "1.0.0", 0},
+ {"1.0a", "<=", "1.0.0", 0},
+ {"1.0a", ">", "1.0.0", 1},
+ {"1.0a", ">=", "1.0.0", 1},
+ {"1.0a", "!=", "1.0.0", 1},
+
+ {"2022.1", "=", "2022.4", 0},
+ {"2022.1", "<", "2022.4", 1},
+ {"2022.1", "<=", "2022.4", 1},
+ {"2022.1", ">", "2022.4", 0},
+ {"2022.1", ">=", "2022.4", 0},
+ {"2022.1", "!=", "2022.4", 1},
+
+ {"2022.4", "=", "2022.1", 0},
+ {"2022.4", "<", "2022.1", 0},
+ {"2022.4", "<=", "2022.1", 0},
+ {"2022.4", ">", "2022.1", 1},
+ {"2022.4", ">=", "2022.1", 1},
+ {"2022.4", "!=", "2022.1", 1},
+};
+
+static struct TestCase_version_compare error_cases_version_compare[] = {
+ // nonsense++
+ {"", "=", "", -1},
+ {" ", "=", " ", -1},
+ {"a", "", "a", -1},
+ {"a", "", "b", -1},
+ {"a", "@", "b", -1},
+};
+
+static int run_cases_version_compare(struct TestCase_version_compare tests[], size_t size) {
+ int failed = 0;
+ for (size_t i = 0; i < size; i++) {
+ int result = 0;
+ struct TestCase_version_compare *test = &tests[i];
+ int op = version_parse_operator(test->op);
+ result = version_compare(op, test->a, test->b);
+
+ printf("%s %s %s is %s", test->a, test->op, test->b, result ? "TRUE" : "FALSE" );
+ if (test->result != result) {
+ printf(" [FAILED: got %d, expected %d]\n", result, test->result);
+ failed++;
+ } else {
+ puts("");
+ }
+ }
+ return failed;
+}
+
+typedef char *(*strfn) (char **s);
+
+static int run_cases_string(struct TestCase_strings tests[], size_t size, strfn fn) {
+ int failed = 0;
+ for (size_t i = 0; i < size; i++) {
+ int result = 0;
+ struct TestCase_strings *test = &tests[i];
+ char *s = strdup(test->s);
+
+ fn(&s);
+ result = strcmp(test->result, s);
+
+ printf("'%s' is %s", s, !result ? "CORRECT" : "INCORRECT" );
+ if (result) {
+ printf(" [FAILED: got '%s', expected '%s']\n", s, test->result);
+ failed++;
+ } else {
+ puts("");
+ }
+ free(s);
+ }
+ return failed;
+}
+
+
+char **make_argv(int *argc, char *argv[]) {
+ char **args;
+ *argc = 0;
+
+ for (size_t i = 0; argv[i] != NULL; i++) {
+ *argc = *argc + 1;
+ }
+
+ //char **args = calloc(*argc + 1, sizeof(*argv));
+ args = calloc(*argc + 1, sizeof(*argv));
+ if (!args) {
+ perror("unable to allocate args array");
+ return NULL;
+ }
+
+ args[0] = strdup("version_compare");
+ if (!args[0]) {
+ perror("unable to allocate mock program name");
+ return NULL;
+ }
+
+ for (int i = 0; i < *argc; i++) {
+ args[i + 1] = strdup(argv[i]);
+ if (!args[i + 1]) {
+ perror("unable to allocate argument in array");
+ return NULL;
+ }
+ }
+ *argc = *argc + 1;
+ return args;
+}
+
+void free_argv(int argc, char *argv[]) {
+ for (int i = 0; i < argc; i++) {
+ free(argv[i]);
+ }
+ free(argv);
+}
+
+int run_program(char *argv[]) {
+ int result = 0;
+ int argc = 0;
+ char **args = NULL;
+
+ args = make_argv(&argc, argv);
+ if (!args) {
+ perror("make_argv failed");
+ return -1;
+ }
+ const char *filename = "stdout.log";
+ char data[255] = {0};
+ char *end = NULL;
+ int o_stdout;
+ int save_stdout;
+
+
+ o_stdout = open(filename, O_RDWR | O_CREAT | O_TRUNC, 0600);
+ if (o_stdout == -1) {
+ perror("unable to open stdout log");
+ goto run_program_failed;
+ }
+
+ save_stdout = dup(fileno(stdout));
+ if (save_stdout == -1) {
+ perror("unable to duplicate stdout");
+ goto run_program_failed;
+ }
+
+ fflush(stdout);
+ if (dup2(o_stdout, fileno(stdout)) == -1) {
+ perror("unable to redirect stdout");
+ goto run_program_failed;
+ }
+
+ result = entry(argc, args);
+
+ fflush(stdout);
+ close(o_stdout);
+
+ if (dup2(save_stdout, fileno(stdout)) == -1) {
+ free_argv(argc, args);
+ perror("unable to restore stdout");
+ goto run_program_failed;
+ }
+ close(save_stdout);
+
+ if (result < 0) {
+ free_argv(argc, args);
+ return result;
+ }
+
+ if (access(filename, F_OK) < 0) {
+ perror(filename);
+ goto run_program_failed;
+ }
+
+ FILE *fp = fopen(filename, "r");
+ if (!fp) {
+ perror("unable to open output log file");
+ goto run_program_failed;
+ }
+
+ if ((fgets(data, sizeof(data) - 1, fp)) == NULL) {
+ fprintf(stderr, "no output recorded in main program execution\n");
+ remove(filename);
+ fclose(fp);
+ goto run_program_failed;
+ }
+
+ result = (int)strtol(data, &end, 10);
+ if (!end) {
+ fprintf(stderr, "unexpected error in main program execution\n");
+ remove(filename);
+ fclose(fp);
+ goto run_program_failed;
+ }
+
+ fclose(fp);
+ remove(filename);
+ free_argv(argc, args);
+ return result;
+
+run_program_failed:
+ free_argv(argc, args);
+ return -1;
+}
+
+static int run_cases_program_split(struct TestCase_version_compare tests[], size_t size) {
+ int failed = 0;
+ for (size_t i = 0; i < size; i++) {
+ int result = 0;
+ struct TestCase_version_compare *test = &tests[i];
+ result = run_program((char *[]){test->a, test->op, test->b, NULL});
+
+ printf("%s %s %s is %s", test->a, test->op, test->b, result ? "TRUE" : "FALSE" );
+ if (test->result != result) {
+ printf(" [FAILED: got %d, expected %d]\n", result, test->result);
+ failed++;
+ } else {
+ puts("");
+ }
+ }
+ return failed;
+}
+
+static int run_cases_program_standalone(struct TestCase_version_compare tests[], size_t size) {
+ int failed = 0;
+ for (size_t i = 0; i < size; i++) {
+ int result = 0;
+ struct TestCase_version_compare *test = &tests[i];
+ char data[255] = {0};
+
+ snprintf(data, sizeof(data) - 1, "%s %s %s", test->a, test->op, test->b);
+ result = run_program((char *[]){data, NULL});
+
+ printf("%s %s %s is %s", test->a, test->op, test->b, result ? "TRUE" : "FALSE" );
+ if (test->result != result) {
+ printf(" [FAILED: got %d, expected %d]\n", result, test->result);
+ failed++;
+ } else {
+ puts("");
+ }
+ }
+ return failed;
+}
+
+int main() {
+ int failed = 0;
+
+ printf("\nTEST version_compare()\n");
+ failed += run_cases_version_compare(test_cases_version_compare,
+ sizeof(test_cases_version_compare) / sizeof(test_cases_version_compare[0]));
+ printf("\nTEST version_compare errors()\n");
+ failed += run_cases_version_compare(error_cases_version_compare,
+ sizeof(error_cases_version_compare) / sizeof(error_cases_version_compare[0]));
+ printf("\nTEST collapse_whitespace()\n");
+ failed += run_cases_string(test_cases_collapse_whitespace,
+ sizeof(test_cases_collapse_whitespace) / sizeof(test_cases_collapse_whitespace[0]),
+ &collapse_whitespace);
+ printf("\nTEST lstrip()\n");
+ failed += run_cases_string(test_cases_lstrip,
+ sizeof(test_cases_lstrip) / sizeof(test_cases_lstrip[0]),
+ &lstrip);
+ printf("\nTEST rstrip()\n");
+ failed += run_cases_string(test_cases_rstrip,
+ sizeof(test_cases_rstrip) / sizeof(test_cases_rstrip[0]),
+ &rstrip);
+
+ printf("\nTEST main program entry point (split string)\n");
+ failed += run_cases_program_split(test_cases_version_compare,
+ sizeof(test_cases_version_compare) / sizeof(test_cases_version_compare[0]));
+
+ printf("\nTEST main program entry point errors (split string)\n");
+ failed += run_cases_program_split(error_cases_version_compare,
+ sizeof(error_cases_version_compare) / sizeof(error_cases_version_compare[0]));
+
+ printf("\nTEST main program entry point (standalone string)\n");
+ failed += run_cases_program_standalone(test_cases_version_compare,
+ sizeof(test_cases_version_compare) / sizeof(test_cases_version_compare[0]));
+
+ printf("\nTEST main program entry point errors (standalone string)\n");
+ failed += run_cases_program_standalone(error_cases_version_compare,
+ sizeof(error_cases_version_compare) / sizeof(error_cases_version_compare[0]));
+
+ return failed != 0;
+}
diff --git a/version_compare.c b/version_compare.c
new file mode 100644
index 0000000..c6aaca7
--- /dev/null
+++ b/version_compare.c
@@ -0,0 +1,369 @@
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <ctype.h>
+#include "version_compare.h"
+
+/**
+ * Determine whether a string consists of only whitespace, or not
+ * @parm str string to check for whitespace
+ * @return 0 if string is not empty
+ * @return 1 if string is empty
+ */
+int isempty(char *str) {
+ char *ptr;
+ size_t len;
+
+ ptr = str;
+ len = strlen(ptr);
+ while (isblank(*ptr)) {
+ len--;
+ ptr++;
+ }
+ return (int)(len == 0);
+}
+
+/**
+ * Remove leading whitespace from string
+ * @param s input string
+ * @return pointer to string
+ */
+char *lstrip(char **s) {
+ char *orig;
+ char *end;
+
+ orig = (*s);
+ if (!isblank(*(*s)) || *(*s) == '\0') {
+ return (*s);
+ }
+
+ while (isblank(*(*s)) && *(*s) != '\0') {
+ end = (*s);
+ (*s)++;
+ }
+ memmove(orig, (*s), strlen(end));
+
+ (*s) = orig;
+ return (*s);
+}
+
+/**
+ * Remove tailing whitespace from string
+ * @param s input string
+ * @return pointer to string
+ */
+char *rstrip(char **s) {
+ char *orig;
+ char *end;
+ size_t len;
+
+ orig = (*s);
+ len = strlen((*s));
+ if (len > 1) {
+ // Jump to end of the string
+ (*s) = (*s) + len - 1;
+ }
+
+ if (!isblank(*(*s)) || *(*s) == '\0') {
+ (*s) = orig;
+ return (*s);
+ }
+
+ while (len != 0 && isblank(*(*s)) && *(*s) != '\0') {
+ end = (*s);
+ (*s)--;
+ --len;
+ }
+ *end = '\0';
+
+ (*s) = orig;
+ return (*s);
+}
+
+/**
+ * Reduces multiple whitespace between characters
+ * @param s input string
+ * @return pointer to string
+ */
+char *collapse_whitespace(char **s) {
+ char *orig;
+ char *space;
+
+ lstrip(s);
+ rstrip(s);
+
+ space = NULL;
+ orig = (*s);
+ while (*(*s) != '\0') {
+ char *space_next;
+
+ if (space) {
+ int numspace = 0;
+ while (isblank(*space) && *space != '\0') {
+ numspace++;
+ space++;
+ }
+
+ memmove((*s), space, strlen(space) - 1 + numspace);
+ space = NULL;
+ }
+
+ space_next = (*s) + 1;
+ if (isblank(*(*s)) && (space_next != NULL && isblank(*space_next))) {
+ space = (*s);
+ (*s)++;
+ continue;
+ }
+
+ (*s)++;
+ }
+ (*s) = orig;
+ return (*s);
+}
+
+/**
+ * Sum each part of a '.'-delimited version string
+ * @param str version string
+ * @return sum of each part
+ * @return -1 on error
+ */
+int version_sum(const char *str) {
+ int result;
+ char *s, *ptr, *end;
+
+ if (!str || isempty(str)) {
+ return -1;
+ }
+
+ result = 0;
+ s = strdup(str);
+ if (!s) {
+ return -1;
+ }
+ ptr = s;
+ end = ptr;
+
+ // Parsing stops at the first non-alpha, non-'.' character
+ // Digits are processed until the first invalid character
+ // I'm torn whether this should be considered an error
+ while (end != NULL) {
+ result += (int) strtoul(ptr, &end, 10);
+ ptr = end;
+ if (*ptr == '.')
+ ptr++;
+ else if (isalpha(*ptr)) {
+ result += *ptr - ('a' - 1);
+ ptr++;
+ }
+ else
+ end = NULL;
+ }
+
+ free(s);
+ return result;
+}
+
+/**
+ * Convert version operator(s) to flags
+ * @param str input string
+ * @return operator flags
+ */
+int version_parse_operator(char *str) {
+ const char *valid = "><=!";
+ char *pos;
+ char *strp;
+ int result;
+
+ strp = str;
+ pos = strp;
+ result = 0;
+
+ if (isempty(str)) {
+ return -1;
+ }
+ while ((pos = strpbrk(pos, valid)) != NULL) {
+ switch (*pos) {
+ case '>':
+ result |= GT;
+ break;
+ case '<':
+ result |= LT;
+ break;
+ case '=':
+ result |= EQ;
+ break;
+ case '!':
+ result |= NOT;
+ break;
+ }
+ strp = pos++;
+ }
+
+ if (!result)
+ return -1;
+ return result;
+}
+
+/**
+ * Compare version strings based on flag(s)
+ * @param flags verison operators
+ * @param aa version1
+ * @param bb version2
+ * @return 1 flag operation is true
+ * @return 0 flag operation is false
+ */
+int version_compare(int flags, const char *aa, const char *bb) {
+ int result, result_a, result_b;
+
+ if (!flags || flags < 0) {
+ return -1;
+ }
+
+ result_a = version_sum(aa);
+ if (result_a < 0)
+ return -1;
+
+ result_b = version_sum(bb);
+ if (result_b < 0)
+ return -1;
+
+ result = 0;
+ if (flags & GT && flags & EQ)
+ result |= result_a >= result_b;
+ else if (flags & LT && flags & EQ)
+ result |= result_a <= result_b;
+ else if (flags & NOT && flags & EQ)
+ result |= result_a != result_b;
+ else if (flags & GT)
+ result |= result_a > result_b;
+ else if (flags & LT)
+ result |= result_a < result_b;
+ else if (flags & EQ)
+ result |= result_a == result_b;
+
+ return result;
+}
+
+void usage(char *prog) {
+ char *name;
+ name = strrchr(prog, '/');
+ if (!name)
+ name = prog;
+ else
+ name++;
+
+ char *examples[] = {
+ "{v} execution example:\n",
+ " %s \"1.2.3 > 1.2.3\"\n",
+ " 0\n",
+ " %s \"1.2.3 >= 1.2.3\"\n",
+ " 1\n",
+ " %s \"1.2.3 < 1.2.3\"\n",
+ " 0\n",
+ " %s \"1.2.3 <= 1.2.3\"\n",
+ " 1\n",
+ " %s \"1.2.3 != 1.2.3\"\n",
+ " 0\n",
+ " %s \"1.2.3 = 1.2.3\"\n",
+ " 1\n",
+ "\n",
+ "{v1} {operator} {v2} execution example:\n",
+ " %s \"1.2.3\" \">\" \"1.2.3\"\n",
+ " 0\n",
+ " %s \"1.2.3\" \">=\" \"1.2.3\"\n",
+ " 1\n",
+ " %s \"1.2.3\" \"<\" \"1.2.3\"\n",
+ " 0\n",
+ " %s \"1.2.3\" \"<=\" \"1.2.3\"\n",
+ " 1\n",
+ " %s \"1.2.3\" \"!=\" \"1.2.3\"\n",
+ " 0\n",
+ " %s \"1.2.3\" \"=\" \"1.2.3\"\n",
+ " 1\n",
+ NULL,
+ };
+
+ printf("usage: %s {{v} | {v1} {operator} {v2}}\n", name);
+ for (int i = 0; examples[i] != NULL; i++) {
+ char *output;
+
+ output = examples[i];
+ if (strchr(examples[i], '%')) {
+ printf(output, name);
+ } else {
+ printf("%s", output);
+ }
+ }
+ puts("");
+}
+
+
+int entry(int argc, char *argv[]) {
+ int result, op, must_free, ntokens, die;
+ char *v1, *v2, *operator, *arg, *arg_orig, *token;
+ char *tokens[4] = {NULL, NULL, NULL, NULL};
+
+ if (argc < 2) {
+ fprintf(stderr, "Not enough arguments.\n");
+ usage(argv[0]);
+ return 1;
+ }
+
+ die = 0;
+ must_free = 0;
+ if (argc < 3) {
+ int i;
+ i = 0;
+ ntokens = 0;
+ must_free = 1;
+ arg = strdup(argv[1]);
+ arg_orig = arg;
+ collapse_whitespace(&arg);
+
+ for (; (token = strsep(&arg, " ")) != NULL; i++, ntokens++) {
+ tokens[i] = strdup(token);
+ }
+ arg = arg_orig;
+
+ if (i < 3) {
+ fprintf(stderr, "Invalid version spec (missing whitespace or token?): '%s'\n", argv[1]);
+ usage(argv[0]);
+ die = 1;
+ goto free_tokens_and_die;
+ }
+
+ v1 = tokens[0];
+ operator = tokens[1];
+ v2 = tokens[2];
+ } else {
+ collapse_whitespace(&argv[1]);
+ collapse_whitespace(&argv[2]);
+ collapse_whitespace(&argv[3]);
+ v1 = argv[1];
+ operator = argv[2];
+ v2 = argv[3];
+ }
+
+ op = version_parse_operator(operator);
+ if (op < 0) {
+ fprintf(stderr, "Invalid operator sequence: '%s'\n", operator);
+ die = 1;
+ goto free_tokens_and_die;
+ }
+
+ result = version_compare(op, v1, v2);
+ printf("%d\n", result);
+
+free_tokens_and_die:
+ if (must_free) {
+ for (int i = 0; i < ntokens; i++) {
+ free(tokens[i]);
+ }
+ free(arg);
+ }
+
+ if (die)
+ return -1;
+
+ return 0;
+}
diff --git a/version_compare.h b/version_compare.h
new file mode 100644
index 0000000..a951e53
--- /dev/null
+++ b/version_compare.h
@@ -0,0 +1,18 @@
+#ifndef VERSION_COMPARE_VERSION_COMPARE_H
+#define VERSION_COMPARE_VERSION_COMPARE_H
+
+#define GT 1 << 1
+#define LT 1 << 2
+#define EQ 1 << 3
+#define NOT 1 << 4
+
+int isempty(char *str);
+char *lstrip(char **s);
+char *rstrip(char **s);
+char *collapse_whitespace(char **s);
+int version_sum(const char *str);
+int version_parse_operator(char *str);
+int version_compare(int flags, const char *aa, const char *bb);
+int entry(int argc, char *argv[]);
+
+#endif //VERSION_COMPARE_VERSION_COMPARE_H \ No newline at end of file