linkup.c (5839B)
1#include <linux/limits.h> 2#include <sys/stat.h> 3#include <fcntl.h> 4#include <dirent.h> 5#include <unistd.h> 6#include <errno.h> 7#include <string.h> 8#include <stdarg.h> 9#include <stdbool.h> 10#include <stdio.h> 11#include <stdlib.h> 12 13static bool force = false; 14static bool install = true; 15static bool nodirs = false; 16static bool verbose = false; 17 18static const char **excluded = NULL; 19static size_t excludecnt = 0; 20static size_t excludecap = 0; 21 22static void 23info(const char *fmt, ...) 24{ 25 va_list ap; 26 27 if (!verbose) return; 28 29 va_start(ap, fmt); 30 vfprintf(stderr, fmt, ap); 31 va_end(ap); 32} 33 34static void 35die(const char *fmt, ...) 36{ 37 va_list ap; 38 39 va_start(ap, fmt); 40 fputs("linkup: ", stderr); 41 vfprintf(stderr, fmt, ap); 42 if (*fmt && fmt[strlen(fmt)-1] == ':') { 43 fputc(' ', stderr); 44 perror(NULL); 45 } else { 46 fputc('\n', stderr); 47 } 48 va_end(ap); 49 50 exit(1); 51} 52 53void 54normalize(const char *src, char *dst) 55{ 56 const char *sep, *tok; 57 char *end; 58 size_t len; 59 60 end = dst; 61 *end = '\0'; 62 63 tok = src; 64 while (tok) { 65 sep = strchr(tok, '/'); 66 len = sep ? (size_t) (sep - tok) : strlen(tok); 67 if (len && strncmp(tok, ".", len)) { 68 if (!strncmp(tok, "..", len)) { 69 end = strrchr(dst, '/'); 70 if (!end) end = dst; 71 *end = '\0'; 72 } else { 73 *end++ = '/'; 74 strncpy(end, tok, len); 75 end += len; 76 *end = '\0'; 77 } 78 } 79 tok = sep ? sep + 1 : NULL; 80 } 81} 82 83 84static void 85realparent(int dirfd, const char *dirpath, const char *file, char *parent) 86{ 87 char tmpbuf[PATH_MAX]; 88 ssize_t len; 89 char *sep; 90 91 if ((len = readlinkat(dirfd, file, tmpbuf, PATH_MAX-1)) == -1) 92 die("readlink '%s/%s':", dirpath, file); 93 tmpbuf[len] = '\0'; 94 95 if (tmpbuf[0] != '/') { /* relative path */ 96 strncpy(parent, tmpbuf, PATH_MAX); 97 snprintf(tmpbuf, PATH_MAX, "%s/%s", dirpath, parent); 98 } 99 100 normalize(tmpbuf, parent); 101 if ((sep = strrchr(parent, '/'))) 102 *sep = '\0'; 103} 104 105static void 106exclude(const char *arg) 107{ 108 if (excludecnt == excludecap) { 109 excludecap = (excludecap + 1) * 2; 110 excluded = realloc(excluded, excludecap * sizeof(const char *)); 111 } 112 excluded[excludecnt] = arg; 113 excludecnt++; 114} 115 116static void 117do_install(const char *src_path, const char *dst_path) 118{ 119 char src_fpath[PATH_MAX], dst_fpath[PATH_MAX]; 120 struct dirent *ent; 121 struct stat attr; 122 DIR *src_dir, *dst_dir; 123 int i; 124 125 src_dir = opendir(src_path); 126 if (!src_dir) die("opendir '%s'", src_path); 127 128 dst_dir = opendir(dst_path); 129 if (!dst_dir) die("opendir '%s'", dst_path); 130 131 while ((ent = readdir(src_dir))) { 132 if (!strcmp(ent->d_name, ".")) 133 continue; 134 if (!strcmp(ent->d_name, "..")) 135 continue; 136 137 for (i = 0; i < excludecnt; i++) { 138 if (!strcmp(excluded[i], ent->d_name)) 139 break; 140 } 141 if (i != excludecnt) { 142 info("skip %s/%s\n", src_path, ent->d_name); 143 continue; 144 } 145 146 snprintf(src_fpath, PATH_MAX, "%s/%s", src_path, ent->d_name); 147 snprintf(dst_fpath, PATH_MAX, "%s/%s", dst_path, ent->d_name); 148 149 /* ensure link destination does not exist */ 150 if (!fstatat(dirfd(dst_dir), ent->d_name, 151 &attr, AT_SYMLINK_NOFOLLOW)) { 152 if (ent->d_type == DT_DIR && (attr.st_mode & S_IFMT) == S_IFDIR) { 153 info("skip %s\n", src_fpath); 154 continue; 155 } 156 if (!force && (attr.st_mode & S_IFMT) != S_IFLNK) 157 die("dst '%s' already exists", dst_fpath); 158 // TODO: skip if points to src_fpath already 159 info("rm %s\n", dst_fpath); 160 if (remove(dst_fpath)) // TODO: removeall 161 die("remove '%s' failed:", dst_fpath); 162 } else { 163 if (nodirs && ent->d_type == DT_DIR) { 164 info("skip %s\n", src_fpath); 165 continue; 166 } 167 } 168 169 info("link %s\n", src_fpath, dst_fpath); 170 if (symlink(src_fpath, dst_fpath) < 0) 171 die("linking '%s' failed:", dst_fpath); 172 } 173 174 /* cleanup dangling symlinks */ 175 while ((ent = readdir(dst_dir))) { 176 if (ent->d_type != DT_LNK) 177 continue; 178 179 /* must point into src */ 180 realparent(dirfd(dst_dir), dst_path, ent->d_name, dst_fpath); 181 if (strcmp(dst_fpath, src_path)) 182 continue; 183 184 /* with missing destination */ 185 if (faccessat(dirfd(dst_dir), ent->d_name, F_OK, 0)) { 186 info(" unlink %s/%s\n", dst_path, ent->d_name); 187 if (unlinkat(dirfd(dst_dir), ent->d_name, 0)) 188 die("unlinking '%s/%s' failed:", dst_path, ent->d_name); 189 } 190 } 191 192 closedir(dst_dir); 193 closedir(src_dir); 194} 195 196static void 197do_uninstall(const char *src_path, const char *dst_path) 198{ 199 struct dirent *ent; 200 char dst_fpath[PATH_MAX]; 201 DIR *dst_dir; 202 203 dst_dir = opendir(dst_path); 204 if (!dst_dir) die("opendir '%s':", dst_path); 205 206 while ((ent = readdir(dst_dir))) { 207 if (ent->d_type != DT_LNK) 208 continue; 209 210 /* must point into src */ 211 realparent(dirfd(dst_dir), dst_path, ent->d_name, dst_fpath); 212 if (strcmp(dst_fpath, src_path)) 213 continue; 214 215 if (unlinkat(dirfd(dst_dir), ent->d_name, 0)) 216 die("failed to remove '%s/%s':", dst_path, ent->d_name); 217 } 218 219 closedir(dst_dir); 220} 221 222int 223main(int argc, const char **argv) 224{ 225 char src_path[PATH_MAX], dst_path[PATH_MAX]; 226 const char *src_arg, *dst_arg; 227 const char **arg; 228 229 src_arg = dst_arg = NULL; 230 for (arg = &argv[1]; *arg; arg++) { 231 if (!strcmp(*arg, "-x") || !strcmp(*arg, "--exclude")) { 232 exclude(*++arg); 233 } else if (!strcmp(*arg, "-u") || !strcmp(*arg, "--uninstall")) { 234 install = false; 235 } else if (!strcmp(*arg, "-f") || !strcmp(*arg, "--force")) { 236 force = true; 237 } else if (!strcmp(*arg, "-v") || !strcmp(*arg, "--verbose")) { 238 verbose = true; 239 } else if (!strcmp(*arg, "--xdirs")) { 240 nodirs = true; 241 } else if (!src_arg) { 242 src_arg = *arg; 243 } else if (!dst_arg) { 244 dst_arg = *arg; 245 } else { 246 break; 247 } 248 } 249 250 if (!src_arg || !dst_arg || *arg) { 251 fprintf(stderr, "Usage: linkup [-h] [-x FILE..] SRC DST\n"); 252 return 1; 253 } 254 255 if (!realpath(src_arg, src_path)) 256 die("realpath '%s':", src_path); 257 258 if (!realpath(dst_arg, dst_path)) 259 die("realpath '%s':", dst_path); 260 261 if (install) { 262 do_install(src_path, dst_path); 263 } else { 264 do_uninstall(src_path, dst_path); 265 } 266 267 free(excluded); 268}