From 42922b74c0387de327c6284ec7fd25bd56444f32 Mon Sep 17 00:00:00 2001 From: anon Date: Fri, 24 Jan 2025 22:57:46 +0100 Subject: [PATCH] bump --- README.md | 7 +- source/directive.c | 160 ++++++++++++++++++++++++++++++++++------- source/error.c | 21 +++--- source/error.h | 3 + source/file_utils.c | 97 ++++++++++++++++++++++--- source/file_utils.h | 35 +++++++-- source/opts.h | 1 + test/CMDTEST_vimdir.rb | 80 +++++++++++++++++++-- 8 files changed, 344 insertions(+), 60 deletions(-) diff --git a/README.md b/README.md index 0aa02a7..ec5909b 100644 --- a/README.md +++ b/README.md @@ -101,14 +101,13 @@ its (changed) value is ignored. - [X] dryrun - [X] recursion - [X] display directories with a trailing `/` -- [ ] touching -- [ ] mkdir +- [ ] touching / mkdir - [X] renaming - [X] deletion - [X] change file permissions - [X] change owner -- [ ] swapping -- [ ] copying +- [X] swapping +- [X] copying - [X] specify the deletion method (so trash can be supported) - [X] use `${VIMDIREDITOR}` - [X] `NO_COLOR` / color diff --git a/source/directive.c b/source/directive.c index 9c43a17..634d4d9 100644 --- a/source/directive.c +++ b/source/directive.c @@ -3,9 +3,10 @@ #include #include #include -#include +#include #include #include +#include #include #include "kvec.h" @@ -29,6 +30,8 @@ int entry_cmp(const void * a, const void * b) { // For qsort() static kvec_t(entry_t) entries; static kvec_t(const char*) directory_queue; +static kvec_t(move_data_t) move_data; + static @@ -37,8 +40,8 @@ int add_directory(const char * const folder) { CHECK_OPEN(dir, folder, return 1); char full_path[PATH_MAX]; - struct stat file_stat; struct dirent * mydirent; + struct stat file_stat; entry_t entry; while ((mydirent = readdir(dir)) != NULL) { if (strcmp(mydirent->d_name, ".") == 0 @@ -72,9 +75,10 @@ int add_directory(const char * const folder) { } int init_directive_c(const char * const folder) { - init_file_utils(is_dry_run); + init_file_utils(is_dry_run, custom_rm); kv_init(entries); kv_init(directory_queue); + kv_init(move_data); kv_push(const char*, directory_queue, folder); while (directory_queue.n) { @@ -98,8 +102,16 @@ int deinit_directive_c(void) { free(kv_A(entries, i).name); } + for (int i = 0; i < move_data.n; i++) { + move_data_t move = kv_A(move_data, i); + free(move.orig_name); + free(move.curt_name); + free(move.dest_name); + } + kv_destroy(directory_queue); kv_destroy(entries); + kv_destroy(move_data); deinit_file_utis(); return 0; @@ -159,36 +171,88 @@ int execute_directive_file(FILE * f) { #define NEXT_FIELD do { \ if (*(sp = next_field(sp)) == '\0') { \ errorn(E_FORMAT); \ - return 1; \ + goto recovery; \ } \ } while (0) - const int LINE_SIZE = 1024; #define CHECK_FORMAT(n, x) do { \ if (n != x) { \ errorn(E_FORMAT); \ - return 1; \ + goto recovery; \ } \ } while (0) + /* io buffering + */ + const int LINE_SIZE = 4096; char line[LINE_SIZE]; - char buffer[1024]; + char buffer[LINE_SIZE/2]; + /* String Pointer, indexing `line` + */ char * sp; - int id; + /* alias reference to the current entry being operated on + */ + entry_t * entry; + /* since new files fille be missing from `entries`, + * but we only the the latest one, + * we buffer it on the stack + */ + char touch_buffer[LINE_SIZE/2]; + entry_t touch_entry; while (fgets(line, LINE_SIZE, f) != NULL) { sp = line; // ID - int e = sscanf(line, "%d\t", &id); - // creation - if (e != 1) { - sscanf(sp, "%s\n", buffer); - mytouch(buffer); - continue; - } - NEXT_FIELD; + do { + int id; + int e = sscanf(line, "%d\t", &id); + if (e == 1) { // normal entry + if (id < 0 + || id > entries.n) { + errorn(E_INDEX, id); + goto recovery; + } - entry_t * entry = &kv_A(entries, id); - entry->is_mentioned = true; + entry = &kv_A(entries, id); + } else { // creation + char * const saved_sp = sp; + // skip to the name + if (do_permissions) { NEXT_FIELD; } + if (do_owner) { NEXT_FIELD; } + + CHECK_FORMAT(1, sscanf(sp, "%s\n", touch_buffer)); + + mytouch(touch_buffer); + + struct stat file_stat; + int es = stat(touch_buffer, &file_stat); + CHECK_OPEN(!(es == -1), touch_buffer, goto recovery); // XXX + + touch_entry = (entry_t) { + .name = touch_buffer, + .st = file_stat, + .is_mentioned = false, + }; + entry = &touch_entry; + + sp = saved_sp; + } + + NEXT_FIELD; + } while (0); + + // Copy + if (entry->is_mentioned) { + char * const saved_sp = sp; + // skip to the name + if (do_permissions) { NEXT_FIELD; } + if (do_owner) { NEXT_FIELD; } + + CHECK_FORMAT(1, sscanf(sp, "%s\n", buffer)); + + mycopy(entry->name, buffer); + + sp = saved_sp; + } // Permission if (do_permissions) { @@ -218,16 +282,31 @@ int execute_directive_file(FILE * f) { NEXT_FIELD; } - // Name - CHECK_FORMAT(1, sscanf(sp, "%s\n", buffer)); - size_t len = strlen(buffer); - if (buffer[len-1] == '/') { - buffer[len-1] = '\0'; - } + // Name (move) + if (!entry->is_mentioned) { + CHECK_FORMAT(1, sscanf(sp, "%s\n", buffer)); + size_t len = strlen(buffer); + if (buffer[len-1] == '/') { + buffer[len-1] = '\0'; + } - if (strcmp(entry->name, buffer)) { - mymove(entry->name, buffer); + if (strcmp(entry->name, buffer)) { + if (access(buffer, F_OK)) { + mymove(entry->name, buffer); + } else { + move_data_t move = mytempmove(entry->name, buffer); + if (!move.orig_name) { + errorn(E_FILE_SWAP, entry->name, buffer); + goto recovery; + } + + kv_push(move_data_t, move_data, move); + } + } } + + // -- Poke + entry->is_mentioned = true; } // Deletion @@ -238,6 +317,35 @@ int execute_directive_file(FILE * f) { } } + // Swap (move) + for (int i = 0; i < move_data.n; i++) { + move_data_t move = kv_A(move_data, i); + // NOTE: we could be overwritting here; + // thats the behaviour the user would expect + int result = mymove(move.curt_name, move.dest_name); + // on the otherhand, upon error, + // you dont want your files replaced + if (result + && !access(move.orig_name, F_OK)) { + // the result of this is intentionally unchecked + mymove(move.curt_name, move.orig_name); + } + } + #undef NEXT_FIELD + #undef CHECK_FORMAT return 0; + + recovery: + /* If an error is encountered, we wish to leave the filesystem in a "valid" state. + * Therefor, files waiting to be swapped (possessing a temporary name) are restored back + * (if possible, if we run into another error, theres not much to do). + */ + for (int i = 0; i < move_data.n; i++) { + move_data_t move = kv_A(move_data, i); + if (!access(move.orig_name, F_OK)) { + mymove(move.curt_name, move.orig_name); + } + } + return 1; } diff --git a/source/error.c b/source/error.c index e690c7b..afe94a1 100644 --- a/source/error.c +++ b/source/error.c @@ -27,15 +27,18 @@ void errorn(int n, ...) { va_start(argv, n); switch (n) { - case E_OPEN_EDITOR: verror("failed to open editor '%s'", argv); break; - case E_FILE_ACCESS: verror("failed to interact with file '%s'", argv); break; - case E_FILE_DELETE: verror("failed to delete file '%s'", argv); break; - case E_FILE_MOVE: verror("failed to move '%s' to '%s'", argv); break; - case E_FILE_CHOWN: verror("failed to chown '%s'", argv); break; - case E_NO_USER: verror("failed to find the user '%s'", argv); break; - case E_NO_GROUP: verror("failed to find the group '%s'", argv); break; - case E_FORMAT: verror("directive-file format violation", argv); break; - case E_FLAG: verror("unknown flag '%c'", argv); break; + case E_OPEN_EDITOR: verror("failed to open editor '%s'", argv); break; + case E_FILE_ACCESS: verror("failed to interact with file '%s'", argv); break; + case E_FILE_DELETE: verror("failed to delete file '%s'", argv); break; + case E_FILE_MOVE: verror("failed to move '%s' to '%s'", argv); break; + case E_FILE_SWAP: verror("failed to swap '%s' with '%s'", argv); break; + case E_FILE_COPY: verror("failed to copy '%s' to '%s'", argv); break; + case E_FILE_CHOWN: verror("failed to chown '%s'", argv); break; + case E_NO_USER: verror("failed to find the user '%s'", argv); break; + case E_NO_GROUP: verror("failed to find the group '%s'", argv); break; + case E_FORMAT: verror("directive-file format violation", argv); break; + case E_INDEX: verror("file index violation encountered (%d)", argv); break; + case E_FLAG: verror("unknown flag '%c'", argv); break; default: verror("unknown error encountered; this is an illegal inner state", 0); break; } diff --git a/source/error.h b/source/error.h index a90d45c..061e9ea 100644 --- a/source/error.h +++ b/source/error.h @@ -6,10 +6,13 @@ enum { E_FILE_ACCESS, E_FILE_DELETE, E_FILE_MOVE, + E_FILE_SWAP, + E_FILE_COPY, E_FILE_CHOWN, E_NO_USER, E_NO_GROUP, E_FORMAT, + E_INDEX, E_FLAG, }; diff --git a/source/file_utils.c b/source/file_utils.c index 5cf6496..e7e06b8 100644 --- a/source/file_utils.c +++ b/source/file_utils.c @@ -26,18 +26,25 @@ int (*mydelete)(const char *filename) = NULL; int (*mychmod)(const char *filename, mode_t mode) = NULL; int (*mychown)(const char *filename, const char *owner, const char *group) = NULL; int (*mymove)(const char *filename, const char *newname) = NULL; +int (*mycopy)(const char *filename, const char *newname) = NULL; +move_data_t (*mytempmove)(const char *filename, const char *newname) = NULL; static int dry_touch(const char * filename); static int dry_delete(const char * filename); static int dry_chmod(const char * filename, mode_t mode); static int dry_chown(const char * filename, const char * owner, const char * group); static int dry_move(const char * filename, const char * newname); +static int dry_copy(const char * filename, const char * newname); +static move_data_t dry_mytempmove(const char *filename, const char *newname); static int moist_touch(const char * filename); static int moist_delete(const char * filename); static int moist_chmod(const char * filename, mode_t mode); static int moist_chown(const char * filename, const char * owner, const char * group); static int moist_move(const char * filename, const char * newname); +static int moist_copy(const char * filename, const char * newname); +static move_data_t moist_mytempmove(const char *filename, const char *newname); + int init_file_utils(bool is_dry_run, const char * custom_rm_) { custom_rm = custom_rm_; @@ -47,12 +54,16 @@ int init_file_utils(bool is_dry_run, const char * custom_rm_) { mychmod = dry_chmod; mychown = dry_chown; mymove = dry_move; + mycopy = dry_copy; + mytempmove = dry_mytempmove; } else { - //mytouch = moist_touch; - //mydelete = moist_delete; - //mychmod = moist_chmod; - //mychown = moist_chown; - //mymove = moist_move; + mytouch = moist_touch; + mydelete = moist_delete; + mychmod = moist_chmod; + mychown = moist_chown; + mymove = moist_move; + mycopy = moist_copy; + mytempmove = moist_mytempmove; } return 0; @@ -78,8 +89,8 @@ char mode_type_to_char(mode_t m) { case S_IFBLK: return 'b'; // block device case S_IFIFO: return 'p'; // fifo (pipe) case S_IFLNK: return 'l'; // symbolic link - case S_IFSOCK: return 's'; // socket - default: return '?'; // unknown + case S_IFSOCK: return 's'; // socket + default: return '?'; // unknown } } @@ -138,7 +149,7 @@ mode_t str_to_mode(const char *permissions) { // --- Dry implementations static int dry_touch(const char * filename) { - warning("touch '%s'", filename); + warning("touch '%s' (subsequent stats will fail)", filename); return 0; } @@ -163,10 +174,26 @@ int dry_chown(const char * filename, const char * owner, const char * group) { static int dry_move(const char * filename, const char * newname) { - warning("rename '%s' (-> %s)", filename, newname); + warning("rename '%s' (-> '%s')", filename, newname); return 0; } +static +int dry_copy(const char * filename, const char * newname) { + warning("copy '%s' (as '%s')", filename, newname); + return 0; +} + +static +move_data_t dry_mytempmove(const char * filename, const char * newname) { + warning("swap detected in a dry-run ('%s' <-> '%s'); the following logs will be inaccurate", filename, newname); + return (move_data_t) { + .orig_name = strdup(filename), + .curt_name = strdup(filename), + .dest_name = strdup(newname), + }; +} + // --- Moist implementations static int moist_touch(const char * filename) { @@ -244,3 +271,55 @@ int moist_move(const char * filename, const char * newname) { } return 0; } + +static +int moist_copy(const char * filename, const char * newname) { + // Is using system for copying terrible? yes. + // Do I have know a better solution thats not filled with footguns? no. + size_t cmd_len = strlen("cp -a") + + sizeof(' ') + strlen(filename) + + sizeof(' ') + strlen(newname) + + 1 + ; + char cmd[cmd_len]; + + snprintf(cmd, cmd_len, "cp -a %s %s", filename, newname); + + int result = system(cmd); + if (result == 127 + || result == -1 + || (WIFEXITED(result) && WEXITSTATUS(result) != 0)) { + errorn(E_FILE_COPY, filename, newname); + return 1; + } + + return 0; +} + +static +move_data_t moist_mytempmove(const char * filename, const char * newname) { + move_data_t r = { + .orig_name = NULL, + .curt_name = NULL, + .dest_name = NULL, + }; + + const int COLISION_DIGITS = 3; + const size_t buf_size = strlen(filename) + COLISION_DIGITS + sizeof("~"); + char buffer[buf_size]; + + unsigned n = 0; + do { + snprintf(buffer, buf_size, "%s~%d", filename, n++); + if (n > 10 * COLISION_DIGITS) { goto end; } + } while (!access(buffer, F_OK)); + + if (mymove(filename, buffer)) { goto end; } + + r.orig_name = strdup(filename); + r.curt_name = strdup(buffer); + r.dest_name = strdup(newname); + + end: + return r; +} diff --git a/source/file_utils.h b/source/file_utils.h index c4c9177..5c67818 100644 --- a/source/file_utils.h +++ b/source/file_utils.h @@ -14,10 +14,35 @@ extern mode_t char_to_mode_type(const char c); extern char * mode_to_str(mode_t mode, char * buffer); extern mode_t str_to_mode(const char *permissions); -extern int (*mytouch)(const char *filename); -extern int (*mydelete)(const char *filename); -extern int (*mychmod)(const char *filename, mode_t mode); -extern int (*mychown)(const char *filename, const char *owner, const char *group); -extern int (*mymove)(const char *filename, const char *newname); +extern int (*mytouch)(const char * filename); +extern int (*mydelete)(const char * filename); +extern int (*mychmod)(const char * filename, mode_t mode); +extern int (*mychown)(const char * filename, const char * owner, const char * group); +extern int (*mymove)(const char * filename, const char * newname); +extern int (*mycopy)(const char * filename, const char * newname); + +/* Swapping file names is only possible with an intermediate rename, + * and it also means that we can't headlessly replace files. + * For this reason we buffer failing renames until + * the rest of the file operations are completed. + * However, we have the following concerns: + * 1) we don't want to move the file out of its directory, + * so that in case of an error, + * its not lost *somewhere* on the filesystem + * 2) a simple and predictable name is preferable, + * because if an error is encountered, + * we want the user to easily recognize his files + * 3) the temp name might already be taken + * Points 1 and 2 cause 3 to exists. + * To deal with 3, we use the tactic employed by the original vidir: + * + we try incrementing suffixes until something works or we get bored + */ +typedef struct { + char * orig_name; + char * curt_name; + char * dest_name; +} move_data_t; + +extern move_data_t (*mytempmove)(const char * filename, const char * newname); #endif diff --git a/source/opts.h b/source/opts.h index 0591da3..24d474e 100644 --- a/source/opts.h +++ b/source/opts.h @@ -1,6 +1,7 @@ #ifndef OPTS_H #define OPTS_H +// NOTE: these two should not clobber on globals extern void get_env(void); extern void parse_args(int argc, char * * argv); diff --git a/test/CMDTEST_vimdir.rb b/test/CMDTEST_vimdir.rb index fa2dce0..039a981 100644 --- a/test/CMDTEST_vimdir.rb +++ b/test/CMDTEST_vimdir.rb @@ -31,7 +31,7 @@ class CMDTEST_basic < Cmdtest::Testcase cmd "vimdir ./this/directory/does/not/exist/" do exit_nonzero created_files ["vimdir_test_file.vimdir"] - stderr_equal /.+error.+/ + stderr_equal /\A.+error.+\n\z/ end end @@ -91,6 +91,21 @@ class CMDTEST_mydir < Cmdtest::Testcase end end + def test_false_entry + File.write('target.txt', + [ + "005\t./mydir/.gitkeep", + ].join("\n") + ) + + cmd "EDITOR=./replacer.sh vimdir -n ./mydir/" do + exit_nonzero + created_files ["vimdir_test_file.vimdir"] + removed_files ["target.txt"] + stderr_equal /\A.+error.+\n\z/ + end + end + def test_permission_contents expected = [ "000\t-rw-r--r--\t./mydir/.gitkeep", @@ -149,7 +164,7 @@ class CMDTEST_mydir < Cmdtest::Testcase exit_zero created_files ["vimdir_test_file.vimdir"] removed_files ["target.txt"] - stderr_equal /^.*delete '.*file.txt'.*$/ + stderr_equal /\A.*delete '.*file.txt'.*\n\z/ end end @@ -197,7 +212,43 @@ class CMDTEST_mydir < Cmdtest::Testcase exit_zero created_files ["vimdir_test_file.vimdir"] removed_files ["target.txt"] - stderr_equal /^.*chmod '.*script.sh' (.+).*$/ + stderr_equal /\A.*chmod '.*script.sh' \(.+\).*\n\z/ + end + end + + def test_copy_file + File.write('target.txt', + [ + "000\t./mydir/.gitkeep", + "001\t./mydir/file.txt", + "001\t./mydir/file2.txt", + "002\t./mydir/script.sh", + ].join("\n") + ) + + cmd "EDITOR=./replacer.sh vimdir -n ./mydir/" do + exit_zero + created_files ["vimdir_test_file.vimdir"] + removed_files ["target.txt"] + stderr_equal /\A.*copy.*'.*file2.txt'.*\n\z/ + end + end + + def test_touch_file + File.write('target.txt', + [ + "000\t./mydir/.gitkeep", + "001\t./mydir/file.txt", + "002\t./mydir/script.sh", + "./mydir/new.txt", + ].join("\n") + ) + + cmd "EDITOR=./replacer.sh vimdir -n ./mydir/" do + exit_nonzero + created_files ["vimdir_test_file.vimdir"] + removed_files ["target.txt"] + stderr_equal /\A.*touch '.*new.txt'.*\n.*error.*\n\z/ end end end @@ -212,7 +263,6 @@ class CMDTEST_mynesteddir < Cmdtest::Testcase def setup import_file "test/replacer.sh", "./" import_file "test/saver.sh", "./" - import_file "test/memoryhole.sh", "./" import_directory "test/mynesteddir/", "./mynesteddir/" end @@ -251,11 +301,11 @@ class CMDTEST_myswapdir < Cmdtest::Testcase import_directory "test/myswapdir/", "./myswapdir/" end - def test_swap + def test_dry_swap File.write('target.txt', [ "000\t./myswapdir/file2.txt", - "002\t./myswapdir/file1.txt", + "001\t./myswapdir/file1.txt", ].join("\n") ) @@ -263,7 +313,23 @@ class CMDTEST_myswapdir < Cmdtest::Testcase exit_zero created_files ["vimdir_test_file.vimdir"] removed_files ["target.txt"] - stderr_equal /.+/ + stderr_equal /.+swap.+/ + end + end + + def test_swap + File.write('target.txt', + [ + "000\t./myswapdir/file2.txt", + "001\t./myswapdir/file1.txt", + ].join("\n") + ) + + cmd "EDITOR=./replacer.sh vimdir ./myswapdir/" do + exit_zero + created_files ["vimdir_test_file.vimdir"] + removed_files ["target.txt"] + changed_files ["myswapdir/file1.txt", "myswapdir/file2.txt"] end end end