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