-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcommand.c
More file actions
497 lines (475 loc) · 14.2 KB
/
command.c
File metadata and controls
497 lines (475 loc) · 14.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
#include "command.h"
#include "options/hidden.h"
#include "options/link.h"
#include "options/list.h"
#include "options/none.h"
#include "options/unlink.h"
#include <errno.h>
#include <fnmatch.h>
#include <ftw.h>
#include <limits.h>
#include <pwd.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>
// Supress unused compiler warnings with a
// compiler specific variable attribute
#define UNUSED __attribute__((unused))
// Defining colored output
#define ANSI_COLOR_RED "\x1b[31m"
#define ANSI_COLOR_GREEN "\x1b[32m"
#define ANSI_COLOR_YELLOW "\x1b[33m"
#define ANSI_COLOR_BLUE "\x1b[34m"
#define ANSI_COLOR_MAGENTA "\x1b[35m"
#define ANSI_COLOR_CYAN "\x1b[36m"
#define ANSI_COLOR_RESET "\x1b[0m"
#define GREEN(str) ANSI_COLOR_GREEN str ANSI_COLOR_RESET
// Setting some options globally for now so we can
// access them in places we can't easily get to like
// inside functions passed as pointers to ftw
static list_opts_t glist_opts = {0};
static const char *ROOT_DIRECTORY = "/";
static const char *CURRENT_DIRECTORY = ".";
static const char *VERSION = "0.0.1";
/**
* Map a command to a better suited enum
*/
command_t map_command(char *command) {
const struct {
command_t val;
const char *str;
} map[] = {{NONE, ""}, {LINK, "link"}, {LIST, "list"}, {UNLINK, "unlink"}};
size_t length = sizeof(map) / sizeof(map[0]);
for (size_t i = 0; i < length; i++) {
if (!strcmp(command, map[i].str)) {
return map[i].val;
}
}
return NONE;
}
/**
* Print help information on how to use
* command-line flags and accepted arguments
*/
void print_none_usage(char **argv) {
printf("Usage: %s <command> [options]\n\n", argv[0]);
printf("Command-line dotfiles management\n\n");
printf(
"stuff is a tool intended to make it simple to manage dotfiles\n"
"using a repository with version control. There is a list of\n"
"commands and options below that can be called with the additional\n"
"`--help' flag for more information.\n\n"
);
printf("Commands:\n");
printf(" link Link local files or directories\n");
printf(" list List all of the tracked dotfiles\n");
printf(" unlink Unlink local files or directories\n\n");
printf("Options:\n");
printf(" -h, --help Print this help and exit\n");
printf(" -v, --version Print the current version number\n");
printf(" -r, --root Specify a path for another link location\n\n");
}
/**
* Print help information for link command
* command-line flags and accepted arguments
*/
void print_link_usage(char **argv) {
printf("Usage: %s link <path> [options]\n\n", argv[0]);
printf("Link local files or directories\n\n");
printf(
"Linked files and folders are mapped to their system location based\n"
"on the project directory structure with files in the root of the\n"
"project mapping to the root of the system.\n\n"
);
printf("Options:\n");
printf(" -h, --force Link even if a link exists\n");
printf(" -h, --help Print this help and exit\n\n");
}
/**
* Print help information for unlink command
* command-line flags and accepted arguments
*/
void print_unlink_usage(char **argv) {
printf("Usage: %s unlink <path> [options]\n\n", argv[0]);
printf("Unlink local files or directories\n\n");
printf(
"Linked files and folders are unmapped from their system location\n"
"based on the project directory structure with files in the root of\n"
"the project mapping to the root of the system.\n\n"
);
printf("Options:\n");
printf(" -h, --help Print this help and exit\n\n");
}
/**
* Print help information for list command
* command-line flags and accepted arguments
*/
void print_list_usage(char **argv) {
printf("Usage: %s list [options]\n\n", argv[0]);
printf("List all of the tracked dotfiles\n\n");
printf(
"All files discovered from the project root are listed using\n"
"their system location with links highlighted in green.\n\n"
);
printf("Options:\n");
printf(" -h, --help Print this help and exit\n");
printf(" -l, --linked Filter for files actually linked\n");
printf(" -o, --owner List the owner with the linked file\n\n");
}
/**
* Read file stats for a file or an expected link which
* often won't exist but we want to handle nicely
*/
int get_file_stats(const char *filename, struct stat *sb) {
if (stat(filename, sb) == -1) {
// Assuming no such file or directory
return 1;
}
// We only want to handle certain files
// directory, symlink, regular file
int mode = sb->st_mode & S_IFMT;
switch (mode) {
case S_IFDIR:
case S_IFLNK:
case S_IFREG:
// Allowed list
break;
default:
fprintf(stderr, "Unsupported file mode `%d'\n", mode);
exit(EXIT_FAILURE);
}
return 0;
}
/**
* Make the system path for some local path where a first version
* doesn't do special mapping. Allocates memory for the returned
* pointer which needs to be freed by the caller. This also
* handles a custom root defined in the hidden options.
*/
char *make_link_path(const char *fpath) {
// We're only supporting calling from the
// project root directory for now
char prefix[PATH_MAX + 1];
realpath(CURRENT_DIRECTORY, prefix);
char fabspath[PATH_MAX + 1];
realpath(fpath, fabspath);
char *pos = strstr(fabspath, prefix);
// Substring doesn't match
if (pos == NULL) {
fprintf(stderr, "File outside project `%s'\n", fpath);
exit(EXIT_FAILURE);
}
char next_prefix[PATH_MAX + 1];
realpath(ghidden_opts.rvalue, next_prefix);
char *suffix = &fabspath[strlen(prefix) + 1];
// Adding 1 makes enough room to add "/" if needed
int lpathlen = strlen(next_prefix) + 1 + strlen(suffix);
char *lpath = (char *)malloc((lpathlen + 1) * sizeof(char));
strcpy(lpath, next_prefix);
// Just next_prefix = "/" ends with a slash so
// we need to add it for all the other cases
if (strcmp(next_prefix, ROOT_DIRECTORY)) {
strcat(lpath, ROOT_DIRECTORY);
}
return strcat(lpath, suffix);
}
/**
* Given a link path and name buffer, fills the
* name buffer with the user who owns the link
*/
char *get_link_owner(char *lpath) {
struct stat lsb;
// Gives the stats for the link instead
// of the file that the link links to
lstat(lpath, &lsb);
struct passwd *pwd;
pwd = getpwuid(lsb.st_uid);
char *username = pwd->pw_name;
char *owner = (char *)malloc((strlen(username) + 1) * sizeof(char));
return strcpy(owner, username);
}
/**
* Checks whether a file or folder is allowed to be displayed
* based on a set of glob patterns for directories we don't allow
*/
int is_directory_allowed(const char *fpath) {
int EMPTY_FLAGS = 0;
// Hardcoding hidden git and current directory but
// should move to persisted file input later
int ignore_patterns_length = 2;
char *ignore_patterns[] = {"./.git*", "."};
for (int i = 0; i < ignore_patterns_length; i++) {
char *pattern = ignore_patterns[i];
if (fnmatch(pattern, fpath, EMPTY_FLAGS) == 0) {
// Match with the ignork
// list means we ignore it
return 0;
}
}
return 1;
}
/**
* Handle a file or directory entry based on
* global list options and log to stdout
*/
int treat_entry(
const char *fpath, UNUSED const struct stat *sb, UNUSED int tflag
) {
if (is_directory_allowed(fpath)) {
struct stat fsb, lsb;
int errfile = get_file_stats(fpath, &fsb);
if (errfile) {
fprintf(stderr, "Non-existent file `%s'\n", fpath);
exit(EXIT_FAILURE);
}
char *lpath = make_link_path(fpath);
int errlink = get_file_stats(lpath, &lsb);
// Using fstat for both files and links to see if
// the actual file stats are the same for both. This
// doesn't distinguish between a link and a file.
if (!errlink && fsb.st_ino == lsb.st_ino) {
if (glist_opts.oflag) {
char *owner = get_link_owner(lpath);
printf(GREEN("%s %s\n"), owner, lpath);
} else {
printf(GREEN("%s") "\n", lpath);
}
} else if (!glist_opts.lflag) {
// Don't care about unlinked owners
printf("%s\n", fpath);
}
free(lpath);
}
return 0;
}
/**
* Handle NONE command
*/
void treat_none(int argc, char **argv) {
none_opts_t opts = {0, 0};
int subind = 0;
if (set_none_options(argc, argv, &opts, &subind) != 0) {
fprintf(stderr, "Failure setting options\n");
exit(EXIT_FAILURE);
}
if (ghidden_opts.dflag) {
print_none_options(argc, argv, &opts);
}
// Not supporting non-options
if (subind < argc) {
fprintf(stderr, "Invalid non-option `%s'\n", argv[subind]);
exit(EXIT_FAILURE);
}
// We give priority to certain options
// and stop executing depending
if (opts.hflag) {
print_none_usage(argv);
exit(EXIT_SUCCESS);
}
if (opts.vflag) {
// Hardcoded version for now
printf("stuff version %s\n", VERSION);
}
// Best to call usage explicitly
// when no arguments given
if (argc == 1) {
print_none_usage(argv);
}
}
/**
* Unlinks a link, deletes a file, or removes
* a directory depending on the given path
*/
void attempt_unlink(char *fpath) {
// Check the file actually exists
// Only unlink if we have a preimage
struct stat fsb;
int errfile = get_file_stats(fpath, &fsb);
if (errfile) {
fprintf(stderr, "Non-existent file path `%s'\n", fpath);
exit(EXIT_FAILURE);
}
char *lpath = make_link_path(fpath);
// Check if the link actually exists
struct stat lsb;
int errlink = get_file_stats(lpath, &lsb);
if (errlink) {
fprintf(stderr, "Non-existent link path `%s'\n", lpath);
exit(EXIT_FAILURE);
}
// Specify the full path
int errunlink = remove(lpath);
if (errunlink) {
perror("Issue unlinking path");
fprintf(stderr, "Couldn't unlink path `%s'\n", lpath);
exit(EXIT_FAILURE);
}
free(lpath);
}
/**
* Tracks a link, meaning creates it
*/
void add_link(char *fpath, char *lpath, link_opts_t *opts) {
// Check the file actually exists
struct stat fsb;
int errfile = get_file_stats(fpath, &fsb);
if (errfile) {
fprintf(stderr, "Non-existent path `%s'\n", fpath);
exit(EXIT_FAILURE);
}
char fabspath[PATH_MAX + 1];
realpath(fpath, fabspath);
// Specify the full path because locations are relative
// to directory of the link. This implicitly throws when
// trying to relink a file that's already linked.
int errlink = symlink(fabspath, lpath);
if (errlink) {
if (errno == EEXIST && opts->fflag) {
// Useful when downgrading permissions
attempt_unlink(fpath);
int nerrlink = symlink(fabspath, lpath);
if (nerrlink) {
perror("Issue creating forced link");
fprintf(stderr, "Couldn't force link file `%s'\n", fpath);
exit(EXIT_FAILURE);
}
} else {
perror("Issue creating link");
fprintf(stderr, "Couldn't link file `%s'\n", fpath);
exit(EXIT_FAILURE);
}
}
}
/**
* Handle LINK command
*/
void treat_link(int argc, char **argv) {
link_opts_t opts = {0};
int subind = 0;
if (set_link_options(argc, argv, &opts, &subind) != 0) {
fprintf(stderr, "Failure setting link options\n");
exit(EXIT_FAILURE);
}
if (ghidden_opts.dflag) {
print_link_options(argc, argv, &opts);
}
// Current should be LINK and next should be local path
// so if we don't have an argument just show the help
if (++subind >= argc) {
print_link_usage(argv);
exit(EXIT_SUCCESS);
}
// The next argument is invalid since
// we only accept a single argument
if (++subind < argc) {
fprintf(stderr, "Invalid link non-option `%s'\n", argv[subind]);
exit(EXIT_FAILURE);
}
// Reset for future use
subind--;
// We give priority to certain options
// and stop executing depending
if (opts.hflag) {
print_link_usage(argv);
exit(EXIT_SUCCESS);
}
// Actually add the link
char *fpath = argv[subind];
char *lpath = make_link_path(fpath);
add_link(fpath, lpath, &opts);
printf(GREEN("%s") "\n", lpath);
free(lpath);
}
/**
* Handle UNLINK command
*/
void treat_unlink(int argc, char **argv) {
unlink_opts_t opts = {0};
int subind = 0;
if (set_unlink_options(argc, argv, &opts, &subind) != 0) {
fprintf(stderr, "Failure setting unlink options\n");
exit(EXIT_FAILURE);
}
if (ghidden_opts.dflag) {
print_unlink_options(argc, argv, &opts);
}
// Current should be UNLINK and next should be local path
// so if we don't have an argument just show the help
if (++subind >= argc) {
print_unlink_usage(argv);
exit(EXIT_SUCCESS);
}
// The next argument is invalid since
// we only accept a single argument
if (++subind < argc) {
fprintf(stderr, "Invalid unlink non-option `%s'\n", argv[subind]);
exit(EXIT_FAILURE);
}
// Reset for future use
subind--;
// We give priority to certain options
// and stop executing depending
if (opts.hflag) {
print_unlink_usage(argv);
exit(EXIT_SUCCESS);
}
// Actually remove the link
char *fpath = argv[subind];
attempt_unlink(fpath);
printf("%s\n", fpath);
}
/**
* Handle LIST command
*/
void treat_list(int argc, char **argv) {
int subind = 0;
if (set_list_options(argc, argv, &glist_opts, &subind) != 0) {
fprintf(stderr, "Failure setting list options\n");
exit(EXIT_FAILURE);
}
if (ghidden_opts.dflag) {
print_list_options(argc, argv, &glist_opts);
}
// Current should be LIST so next
// is invalid if within limit
if (++subind < argc) {
fprintf(stderr, "Invalid list non-option `%s'\n", argv[subind]);
exit(EXIT_FAILURE);
}
// We give priority to certain options
// and stop executing depending
if (glist_opts.hflag) {
print_list_usage(argv);
exit(EXIT_SUCCESS);
}
// Concurrently handle 20 entries at a time
if (ftw(CURRENT_DIRECTORY, treat_entry, 20) == -1) {
fprintf(stderr, "Error walking directory\n");
exit(EXIT_FAILURE);
}
}
/**
* Handle functionality specific to a command or lack thereof
*/
void treat_command(char *command, int argc, char **argv) {
switch (map_command(command)) {
case NONE:
treat_none(argc, argv);
break;
case LINK:
treat_link(argc, argv);
break;
case LIST:
treat_list(argc, argv);
break;
case UNLINK:
treat_unlink(argc, argv);
break;
default:
fprintf(stderr, "Unreachable treat_command\n");
exit(EXIT_FAILURE);
}
}