From 9824208d05a2ba476cbb3f583fa2df3080a00967 Mon Sep 17 00:00:00 2001 From: Joursoir Date: Thu, 25 Feb 2021 13:06:35 +0000 Subject: add recursive mkdir, improve error handling --- src/implementation.c | 150 ++++++++++----------- src/implementation.h | 16 ++- src/main.c | 373 +++++++++++++++++++++++++++++---------------------- 3 files changed, 295 insertions(+), 244 deletions(-) (limited to 'src') diff --git a/src/implementation.c b/src/implementation.c index 17b2c9a..334b7e1 100644 --- a/src/implementation.c +++ b/src/implementation.c @@ -1,10 +1,12 @@ #include #include +#include #include #include #include #include #include +#include #include "handerror.h" #include "easydir.h" @@ -50,38 +52,46 @@ static void copyText(char *password) #endif } -void checkForbiddenPaths(char *path) // check two dot in path +/* check two dot in path */ +int checkForbiddenPaths(char *path) { + int i, length; int firstdot = 0; - for(int i=0; i < strlen(path); i++) + for(i = 0, length = strlen(path); i < length; i++) { - if(path[i] == '.') - firstdot ? printError("Error: please, don't use forbidden paths\n") : firstdot++; + if(path[i] == '.') { + if(firstdot) + return 1; + firstdot++; + } else firstdot = 0; } + return 0; } -char *getGPGKey(char *dest, size_t size) +char *getGPGKey() { + int size_gpgkey = sizeof(char) * GPG_PUBLICKEY_MAXLENGTH; + char *pub_gpgkey = malloc(size_gpgkey + sizeof(char)); + FILE *fileGPG = fopen(".gpg-key", "r"); if(fileGPG == NULL) { - if(errno == ENOENT) printError("error: No GPG key exists. Use \"lpass init\"."); + if(errno == ENOENT) + printError("error: No GPG key exists. Use \"lpass init\"."); callError(121); } - if(!fgets(dest, size, fileGPG)) { + if(!fgets(pub_gpgkey, size_gpgkey, fileGPG)) { callError(122); } fclose(fileGPG); - return dest; + return pub_gpgkey; } char* getPassword(char *path_pass, char *password, size_t size, int flag_copy) { - int size_gpgkey = sizeof(char) * GPG_PUBLICKEY_MAXLENGTH; - char *public_gpgkey = (char *) malloc(size_gpgkey); - getGPGKey(public_gpgkey, size_gpgkey); + char *public_gpgkey = getGPGKey(); char *arguments[] = {"gpg", "-d", "--quiet", "-r", public_gpgkey, "-o", path_pass, gPath_pass, NULL}; easyFork("gpg", arguments); @@ -112,90 +122,74 @@ void nonvisibleEnter(int status) tcsetattr(0, TCSANOW, &term_settings); } -void insertPass(char *add_path, char *password, int flag_copy) +static void mkdir_recursive(const char *path) { - /* add_path = banks/france/[number] - gPath_pass = banks/france/[number].gpg - gPath_subdir = banks/france */ + char *subpath, *fullpath; - int size_gpgkey = sizeof(char) * GPG_PUBLICKEY_MAXLENGTH; - char *public_gpgkey = (char *) malloc(size_gpgkey); - getGPGKey(public_gpgkey, size_gpgkey); + fullpath = strdup(path); + subpath = dirname(fullpath); + if(subpath[0] != '.') + mkdir_recursive(subpath); - char *mkdir_arg[] = {"mkdir", "-p", gPath_subdir, NULL}; - easyFork("mkdir", mkdir_arg); + mkdir(path, S_IRWXU); + free(fullpath); +} + +int insertPass(char *path, char *password, int flag_copy) +{ + char *public_gpgkey = getGPGKey(); + + // create directories + char *tmp, *dirs_path; + tmp = strdup(path); + dirs_path = dirname(tmp); + if(dirs_path[0] != '.') + mkdir_recursive(dirs_path); + free(tmp); // create file, copy password there - FILE *filePass; - filePass = fopen(add_path, "w"); - if(filePass == NULL) { - callError(108); + FILE *file_pass; + file_pass = fopen(path, "w"); + if(file_pass == NULL) { + free(public_gpgkey); + return 1; } - fputs(password, filePass); - fclose(filePass); + fputs(password, file_pass); + fclose(file_pass); - if(flag_copy) copyText(password); + if(flag_copy) + copyText(password); // encryption - char *encrypt_arg[] = {"gpg", "--quiet", "--yes", "-r", public_gpgkey, "-e", add_path, NULL}; + char *encrypt_arg[] = {"gpg", "--quiet", "--yes", "-r", public_gpgkey, "-e", path, NULL}; easyFork("gpg", encrypt_arg); - remove(add_path); + remove(path); free(public_gpgkey); + return 0; } -char *typePass(char *text, char *dest, int minlen, int maxlen) +char *getInput(int minlen, int maxlen) { - printf("%s", text); - if(fgets(dest, sizeof(char)*maxlen, stdin) == NULL) { - nonvisibleEnter(0); - printError("lpass: Unexpected end of file\n"); - } + size_t size = sizeof(char) * maxlen; + char *pass = malloc(size + sizeof(char)); // +1 for '\0' + int len; - int len = strlen(dest); - if(len < minlen || len > maxlen) { - nonvisibleEnter(0); - printError("lpass: incorrect password\n"); - } - - if(dest[len-1] == '\n') { - dest[len-1] = '\0'; + if(fgets(pass, size, stdin) == NULL) { + free(pass); + return NULL; } - #if defined(DEBUG) - printf("%s", dest); - #endif - - printf("\n"); // for new line - return dest; -} - -int userEnterPassword(int minlen, int maxlen, char *path_insert, int flag_echo, int flag_copy) -{ - char *pass_one = malloc(sizeof(char) * maxlen); - int rvalue = 0; - if(!flag_echo) { - char *pass_two = malloc(sizeof(char) * maxlen); - - nonvisibleEnter(1); // change terminal work - typePass("Type your password: ", pass_one, minlen, maxlen); - typePass("Type your password again: ", pass_two, minlen, maxlen); - nonvisibleEnter(0); - - if(strcmp(pass_one, pass_two) == 0) { - insertPass(path_insert, pass_one, flag_copy); - rvalue = 1; - } - free(pass_two); - } - else { - typePass("Type your password: ", pass_one, minlen, maxlen); - insertPass(path_insert, pass_one, flag_copy); - rvalue = 1; + len = strlen(pass); + if(len < minlen) { + free(pass); + return NULL; } - free(pass_one); - return rvalue; + if(pass[len-1] == '\n') + pass[len-1] = '\0'; + + return pass; } char *generatePassword(char *dest, int amount) @@ -230,13 +224,13 @@ int getOverwriteAnswer(char *path) switch(answer) { case 'Y': - case 'y': return 1; + case 'y': return OW_YES; case 'N': - case 'n': return 0; + case 'n': return OW_NO; case EOF: printError("Error: Unexpected end of file\n"); default: { printf("Overwrite? (Y/N) "); break; } } } - return -1; + return 2; } diff --git a/src/implementation.h b/src/implementation.h index 52f4dc7..bcb6985 100644 --- a/src/implementation.h +++ b/src/implementation.h @@ -1,15 +1,19 @@ #ifndef IMPLEMENTATION_H #define IMPLEMENTATION_H -#define GPG_PUBLICKEY_MAXLENGTH 1025 // +1 for '\0' +#define GPG_PUBLICKEY_MAXLENGTH 1024 -void checkForbiddenPaths(char *path); -char *getGPGKey(char *dest, size_t size); +enum asnwers { + OW_YES = 0, + OW_NO = 1, +}; + +int checkForbiddenPaths(char *path); +char *getGPGKey(); char* getPassword(char *path_pass, char *password, size_t size, int flag_copy); void nonvisibleEnter(int status); -void insertPass(char *add_path, char *password, int flag_copy); -char *typePass(char *text, char *dest, int minlen, int maxlen); -int userEnterPassword(int minlen, int maxlen, char *path_insert, int flag_echo, int flag_copy); +int insertPass(char *path, char *password, int flag_copy); +char *getInput(int minlen, int maxlen); char *generatePassword(char *dest, int amount); int getOverwriteAnswer(char *path); diff --git a/src/main.c b/src/main.c index 7cffde2..6f27dbc 100644 --- a/src/main.c +++ b/src/main.c @@ -21,13 +21,17 @@ #include "implementation.h" #include "exec-cmd.h" +enum constants { + maxlen_texteditor = 16, + minlen_pass = 1, + maxlen_pass = 128, + stdlen_pass = 14 +}; + #define VERSION "1.0c" #define DATE_RELEASE "14 January, 2021" #define STANDARD_TEXTEDITOR "vim" #define MAXLEN_TEXTEDITOR 16 -#define MINLEN_PASSWORD 1 -#define MAXLEN_PASSWORD 128 -#define STANDARD_AMOUNT_GENERATE_SYMBOLS 14 #define LOCKPASS_DIR ".lock-password/" #define GPGKEY_FILE ".gpg-key" @@ -47,7 +51,7 @@ #ifdef DEBUG #define dbgprint(...) fprintf(stderr, "Debug: " __VA_ARGS__) #else - #define dbgprint(...) ; + #define dbgprint(...) #endif struct cmd_struct { @@ -91,7 +95,8 @@ int cmd_init(int argc, char *argv[]) { const char description[] = "init gpg-key\n"; char *gpg_key = argv[2]; - if(gpg_key == NULL) usageprint("%s", description); + if(gpg_key == NULL) + usageprint("%s", description); // create .gpg-key in storage FILE *filekey = fopen(GPGKEY_FILE, "w"); @@ -105,6 +110,88 @@ int cmd_init(int argc, char *argv[]) return 0; } +int cmd_insert(int argc, char *argv[]) +{ + const char description[] = "insert [-ecf] passname\n"; + int flag_echo = 0, flag_force = 0, flag_copy = 0, result; + const struct option long_options[] = { + {"echo", no_argument, NULL, 'e'}, + {"force", no_argument, NULL, 'f'}, + {"copy", no_argument, NULL, 'c'}, + {NULL, 0, NULL, 0} + }; + + while((result = getopt_long(argc, argv, "efc", long_options, NULL)) != -1) { + switch(result) { + case 'e': { flag_echo = 1; break; } + case 'f': { flag_force = 1; break; } + case 'c': { flag_copy = 1; break; } + default: usageprint("%s", description); + } + } + + if(optind < argc) optind++; // for skip "insert" + dbgprint("passname: %s\n", argv[optind]); + + char *path = argv[optind]; + if(path == NULL) + usageprint("%s", description); + + result = checkForbiddenPaths(path); + if(result) + errprint("You have used forbidden paths\n"); + + if(checkFileExist(path) == F_SUCCESS) { + if(!flag_force) { + if(getOverwriteAnswer(path) != OW_YES) + return 1; + } + } + + char *f_pass; + if(!flag_echo) { + char *s_pass; + nonvisibleEnter(1); // change terminal work + + fputs("Type your password: ", stdout); + f_pass = getInput(minlen_pass, maxlen_pass); + fputs("\n", stdout); + if(f_pass == NULL) { + nonvisibleEnter(0); + errprint("Incorrect password"); + } + + fputs("Type your password again: ", stdout); + s_pass = getInput(minlen_pass, maxlen_pass); + fputs("\n", stdout); + nonvisibleEnter(0); + if(s_pass == NULL) { + free(f_pass); + errprint("Incorrect password"); + } + + if(strcmp(f_pass, s_pass) != 0) { + free(f_pass); + free(s_pass); + errprint("Password do not match"); + } + free(s_pass); + } + else { + fputs("Type your password: ", stdout); + f_pass = getInput(minlen_pass, maxlen_pass); + if(f_pass == NULL) + errprint("Incorrect password"); + } + + result = insertPass(path, f_pass, flag_copy); + if(result) + errprint("Can't add password to LockPassword"); + + printf("Password added successfully for %s\n", path); + return 0; +} + int cmd_edit(int argc, char *argv[]) { const char description[] = "edit [-t=text-editor] passname\n"; @@ -132,13 +219,14 @@ int cmd_edit(int argc, char *argv[]) } if(optind < argc) optind++; // for skip "edit" - dbgprint("passname: %s\n", argv[optind]); - + char *path_to_password = argv[optind]; if(argv[optind] == NULL) usageprint("%s", description); - char *path_to_password = argv[optind]; + dbgprint("passname: %s\n", argv[optind]); - checkForbiddenPaths(path_to_password); + result = checkForbiddenPaths(path_to_password); + if(result) + errprint("You have used forbidden paths\n"); globalSplitPath(path_to_password); result = checkFileExist(gPath_pass); @@ -167,9 +255,7 @@ int cmd_edit(int argc, char *argv[]) // end configure // decryption - int size_gpgkey = sizeof(char) * GPG_PUBLICKEY_MAXLENGTH; - char *public_gpgkey = (char *) malloc(size_gpgkey); - getGPGKey(public_gpgkey, size_gpgkey); + char *public_gpgkey = getGPGKey(); char *decrypt_arg[] = {"gpg", "-d", "--quiet", "-r", public_gpgkey, "-o", path_to_password, gPath_pass, NULL}; easyFork("gpg", decrypt_arg); @@ -179,8 +265,8 @@ int cmd_edit(int argc, char *argv[]) easyFork(text_editor, editor_arg); // delete '\n' and paste good pass - char password[MAXLEN_PASSWORD]; - fileCropLineFeed(path_to_password, password, MAXLEN_PASSWORD); + char password[maxlen_pass]; + fileCropLineFeed(path_to_password, password, maxlen_pass); FILE *file = fopen(path_to_password, "w"); if(file == NULL) callError(108); @@ -196,72 +282,10 @@ int cmd_edit(int argc, char *argv[]) return 0; } -int cmd_move(int argc, char *argv[]) -{ - /* we have a two situation: - 1) mv file file - 2) mv file directory */ - - const char description[] = "mv [-f] old-path new-path\n"; - const struct option long_options[] = { - {"force", no_argument, NULL, 'f'}, - {NULL, 0, NULL, 0} - }; - - int result, flag_force = 0; - while((result = getopt_long(argc, argv, "f", long_options, NULL)) != -1) { - switch(result) { - case 'f': { flag_force = 1; break; } - default: usageprint("%s", description); - } - } - - if(optind < argc) optind++; // for skip "move" - if(!argv[optind] || !argv[optind+1]) - usageprint("%s", description); - dbgprint("old-path: %s\n", argv[optind]); - dbgprint("new-path: %s\n", argv[optind+1]); - - char *old_path = argv[optind]; - checkForbiddenPaths(old_path); - globalSplitPath(old_path); - result = checkFileExist(gPath_pass); - if(result != F_SUCCESS) { - if(result == F_ISDIR) errprint("It is a directory\n"); - errprint("No such file exists\n"); - } - - char *old_path_gpg = gPath_pass; - char *old_path_subdir = gPath_subdir; - - char *new_path = argv[optind+1]; - checkForbiddenPaths(new_path); - globalSplitPath(new_path); - - if(checkFileExist(new_path) == F_ISDIR) - ; - else if(checkFileExist(gPath_pass) == F_SUCCESS) { - if(!flag_force) { - if(getOverwriteAnswer(new_path) != 1) - return 1; - } - new_path = gPath_pass; - } - else errprint("No such new-path exists\n"); - - char *arguments[] = {"mv", "-f", old_path_gpg, new_path, NULL}; - easyFork("mv", arguments); - - rmdir(old_path_subdir); - free(old_path_subdir); - free(old_path_gpg); - return 0; -} - int cmd_generate(int argc, char *argv[]) { const char description[] = "generate [-l=pass-length] [-f] passname\n"; - int pass_length = STANDARD_AMOUNT_GENERATE_SYMBOLS; + int pass_length = stdlen_pass; int flag_force = 0, flag_copy = 0, result; const struct option long_options[] = { {"length", required_argument, NULL, 'l'}, @@ -287,15 +311,17 @@ int cmd_generate(int argc, char *argv[]) usageprint("%s", description); char *path_to_password = argv[optind]; - if(pass_length < MINLEN_PASSWORD || pass_length > MAXLEN_PASSWORD) + if(pass_length < minlen_pass || pass_length > maxlen_pass) errprint("You typed an incorrect number\n"); - checkForbiddenPaths(path_to_password); + result = checkForbiddenPaths(path_to_password); + if(result) + errprint("You have used forbidden paths\n"); globalSplitPath(path_to_password); if(checkFileExist(gPath_pass) == F_SUCCESS) { if(!flag_force) { - if(getOverwriteAnswer(path_to_password) != 1) + if(getOverwriteAnswer(path_to_password) != OW_YES) return 1; } } @@ -304,77 +330,139 @@ int cmd_generate(int argc, char *argv[]) char gpass[pass_length]; generatePassword(gpass, pass_length); - insertPass(path_to_password, gpass, flag_copy); + result = insertPass(path_to_password, gpass, flag_copy); + if(result) + errprint("Can't add password to LockPassword"); + if(!flag_copy) printf("Generated password: %s\n", gpass); printf("Password added successfully for %s\n", path_to_password); return 0; } -int cmd_insert(int argc, char *argv[]) +int cmd_remove(int argc, char *argv[]) { - const char description[] = "insert [-ecf] passname\n"; - int flag_echo = 0, flag_force = 0, flag_copy = 0, result; + const char description[] = "rm passname\n"; + int result; + char *path = argv[2]; + if(!path) + usageprint("%s", description); + + result = checkForbiddenPaths(path); + if(result) + errprint("You have used forbidden paths\n"); + globalSplitPath(path); + + result = checkFileExist(gPath_pass); + if(result != F_SUCCESS) { + if(result == F_ISDIR) errprint("It is a directory\n"); + errprint("No such file exists\n"); + } + + if(unlink(gPath_pass) == 0) + rmdir(gPath_subdir); + return 0; +} + +int cmd_move(int argc, char *argv[]) +{ + /* we have a two situation: + 1) mv file file + 2) mv file directory */ + + const char description[] = "mv [-f] old-path new-path\n"; const struct option long_options[] = { - {"echo", no_argument, NULL, 'e'}, {"force", no_argument, NULL, 'f'}, - {"copy", no_argument, NULL, 'c'}, {NULL, 0, NULL, 0} }; - while((result = getopt_long(argc, argv, "efc", long_options, NULL)) != -1) { + int result, flag_force = 0; + while((result = getopt_long(argc, argv, "f", long_options, NULL)) != -1) { switch(result) { - case 'e': { flag_echo = 1; break; } case 'f': { flag_force = 1; break; } - case 'c': { flag_copy = 1; break; } default: usageprint("%s", description); } } - if(optind < argc) optind++; // for skip "insert" - dbgprint("passname: %s\n", argv[optind]); - - if(argv[optind] == NULL) + if(optind < argc) optind++; // for skip "move" + if(!argv[optind] || !argv[optind+1]) usageprint("%s", description); - char *path_to_password = argv[optind]; + dbgprint("old-path: %s\n", argv[optind]); + dbgprint("new-path: %s\n", argv[optind+1]); - checkForbiddenPaths(path_to_password); - globalSplitPath(path_to_password); + char *old_path = argv[optind]; + result = checkForbiddenPaths(old_path); + if(result) + errprint("You have used forbidden paths\n"); + globalSplitPath(old_path); + result = checkFileExist(gPath_pass); + if(result != F_SUCCESS) { + if(result == F_ISDIR) errprint("It is a directory\n"); + errprint("No such file exists\n"); + } - if(checkFileExist(gPath_pass) == F_SUCCESS) { + char *old_path_gpg = gPath_pass; + char *old_path_subdir = gPath_subdir; + + char *new_path = argv[optind+1]; + result = checkForbiddenPaths(new_path); + if(result) + errprint("You have used forbidden paths\n"); + globalSplitPath(new_path); + + if(checkFileExist(new_path) == F_ISDIR) + ; + else if(checkFileExist(gPath_pass) == F_SUCCESS) { if(!flag_force) { - if(getOverwriteAnswer(path_to_password) != 1) + if(getOverwriteAnswer(new_path) != OW_YES) return 1; } + new_path = gPath_pass; } - - if(userEnterPassword(MINLEN_PASSWORD, MAXLEN_PASSWORD, - path_to_password, flag_echo, flag_copy) == 1) { - printf("Password added successfully for %s\n", path_to_password); - } - else - printf("Passwords do not match\n"); + else errprint("No such new-path exists\n"); + + char *arguments[] = {"mv", "-f", old_path_gpg, new_path, NULL}; + easyFork("mv", arguments); + + rmdir(old_path_subdir); + free(old_path_subdir); + free(old_path_gpg); return 0; } -int cmd_remove(int argc, char *argv[]) +int cmd_help(int argc, char *argv[]) { - const char description[] = "rm passname\n"; - int res; - char *path = argv[2]; - if(!path) - usageprint("%s", description); + printf("Synopsis:\n" + "\tlpass [command] [arguments] ...\n" - checkForbiddenPaths(path); - globalSplitPath(path); + "Commands:\n" + "\tinit gpg-key\n" + "\t\tInitialize the password manager using the passed gpg-key.\n" + "\tinsert [-e, --echo] [-c, --copy] [-f, --force] passname\n" + "\t\tAdd the specified passname to the password manager.\n" + "\tedit [-t, --text-editor=text-editor] passname\n" + "\t\tOpen the specified passname in a text editor, waiting for changes.\n" + "\tgenerate [-l, --length=pass-length] [-c, --copy] [-f, --force] passname\n" + "\t\tGenerate a random password and write it in passname.\n" + "\tmv [-f, --force] old-path new-path\n" + "\t\tMove/rename old-path to new-path.\n" + "\trm passname\n" + "\t\tRemove the passname you specified from the password manager.\n" + "\thelp\n" + "\t\tPrint help information about commands and the application itself.\n" + "\tversion\n" + "\t\tPrint version information.\n" - res = checkFileExist(gPath_pass); - if(res != F_SUCCESS) { - if(res == F_ISDIR) errprint("It is a directory\n"); - errprint("No such file exists\n"); - } + "\nMore information may be found in the lpass(1) man page.\n"); + return 0; +} - if(unlink(gPath_pass) == 0) - rmdir(gPath_subdir); +int cmd_version(int argc, char *argv[]) +{ + printf("LockPassword v%s\n" + "Release date: %s\n\n" + "Code was written by Joursoir\n" + "This is free and unencumbered software released into the public domain.\n\n", + VERSION, DATE_RELEASE); return 0; } @@ -396,9 +484,11 @@ int cmd_showtree(int argc, char *argv[]) } if(argv[optind]) { + result = checkForbiddenPaths(argv[optind]); + if(result) + errprint("You have used forbidden paths\n"); path = malloc(sizeof(char) * (strlen(argv[optind]) + 1)); strcpy(path, argv[optind]); - checkForbiddenPaths(path); } else { path = malloc(sizeof(char) * 2); @@ -430,8 +520,8 @@ int cmd_showtree(int argc, char *argv[]) if(checkFileExist(gPath_pass) == F_SUCCESS) { - char password[MAXLEN_PASSWORD]; - getPassword(path, password, sizeof(char)*MAXLEN_PASSWORD, flag_copy); + char password[maxlen_pass]; + getPassword(path, password, sizeof(char)*maxlen_pass, flag_copy); if(!flag_copy) printf("%s\n", password); } else errprint("%s is not in the password storage\n", path); @@ -441,43 +531,6 @@ int cmd_showtree(int argc, char *argv[]) return 0; } -int cmd_help(int argc, char *argv[]) -{ - printf("Synopsis:\n" - "\tlpass [command] [arguments] ...\n" - - "Commands:\n" - "\tinit gpg-key\n" - "\t\tInitialize the password manager using the passed gpg-key.\n" - "\tinsert [-e, --echo] [-c, --copy] [-f, --force] passname\n" - "\t\tAdd the specified passname to the password manager.\n" - "\tedit [-t, --text-editor=text-editor] passname\n" - "\t\tOpen the specified passname in a text editor, waiting for changes.\n" - "\tgenerate [-l, --length=pass-length] [-c, --copy] [-f, --force] passname\n" - "\t\tGenerate a random password and write it in passname.\n" - "\tmv [-f, --force] old-path new-path\n" - "\t\tMove/rename old-path to new-path.\n" - "\trm passname\n" - "\t\tRemove the passname you specified from the password manager.\n" - "\thelp\n" - "\t\tPrint help information about commands and the application itself.\n" - "\tversion\n" - "\t\tPrint version information.\n" - - "\nMore information may be found in the lpass(1) man page.\n"); - return 0; -} - -int cmd_version(int argc, char *argv[]) -{ - printf("LockPassword v%s\n" - "Release date: %s\n\n" - "Code was written by Joursoir\n" - "This is free and unencumbered software released into the public domain.\n\n", - VERSION, DATE_RELEASE); - return 0; -} - static struct cmd_struct *get_cmd(const char *name) { struct cmd_struct *ptr; -- cgit v1.2.3-18-g5258