This commit is contained in:
anon 2025-01-24 22:57:46 +01:00
parent a2a5aacf63
commit 42922b74c0
8 changed files with 344 additions and 60 deletions

View File

@ -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

View File

@ -3,9 +3,10 @@
#include <stdio.h>
#include <dirent.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>
#include <pwd.h>
#include <grp.h>
#include <sys/stat.h>
#include <linux/limits.h>
#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;
}

View File

@ -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;
}

View File

@ -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,
};

View File

@ -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;
}

View File

@ -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

View File

@ -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);

View File

@ -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