| 1 | /* |
| 2 | * This file is part of DisOrder. |
| 3 | * Copyright (C) 2006 Richard Kettlewell |
| 4 | * |
| 5 | * This program is free software; you can redistribute it and/or modify |
| 6 | * it under the terms of the GNU General Public License as published by |
| 7 | * the Free Software Foundation; either version 2 of the License, or |
| 8 | * (at your option) any later version. |
| 9 | * |
| 10 | * This program is distributed in the hope that it will be useful, but |
| 11 | * WITHOUT ANY WARRANTY; without even the implied warranty of |
| 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
| 13 | * General Public License for more details. |
| 14 | * |
| 15 | * You should have received a copy of the GNU General Public License |
| 16 | * along with this program; if not, write to the Free Software |
| 17 | * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 |
| 18 | * USA |
| 19 | */ |
| 20 | |
| 21 | #include <config.h> |
| 22 | #include "types.h" |
| 23 | |
| 24 | #include <getopt.h> |
| 25 | #include <unistd.h> |
| 26 | #include <locale.h> |
| 27 | #include <fcntl.h> |
| 28 | #include <errno.h> |
| 29 | #include <dirent.h> |
| 30 | #include <sys/stat.h> |
| 31 | #include <langinfo.h> |
| 32 | #include <string.h> |
| 33 | #include <fnmatch.h> |
| 34 | |
| 35 | #include "syscalls.h" |
| 36 | #include "log.h" |
| 37 | #include "printf.h" |
| 38 | #include "charset.h" |
| 39 | #include "defs.h" |
| 40 | #include "mem.h" |
| 41 | |
| 42 | /* Arguments etc ----------------------------------------------------------- */ |
| 43 | |
| 44 | typedef int copyfn(const char *from, const char *to); |
| 45 | typedef int mkdirfn(const char *dir, mode_t mode); |
| 46 | |
| 47 | /* Input and output directories */ |
| 48 | static const char *source, *destination; |
| 49 | |
| 50 | /* Function used to copy or link a file */ |
| 51 | static copyfn *copier = link; |
| 52 | |
| 53 | /* Function used to make a directory */ |
| 54 | static mkdirfn *dirmaker = mkdir; |
| 55 | |
| 56 | /* Various encodings */ |
| 57 | static const char *fromencoding, *toencoding, *tagencoding; |
| 58 | |
| 59 | /* Directory for untagged files */ |
| 60 | static const char *untagged; |
| 61 | |
| 62 | /* Extract tag information? */ |
| 63 | static int extracttags; |
| 64 | |
| 65 | /* Windows-friendly filenames? */ |
| 66 | static int windowsfriendly; |
| 67 | |
| 68 | /* Native character encoding (i.e. from LC_CTYPE) */ |
| 69 | static const char *nativeencoding; |
| 70 | |
| 71 | /* Count of errors */ |
| 72 | static long errors; |
| 73 | |
| 74 | /* Included/excluded filename patterns */ |
| 75 | static struct pattern { |
| 76 | struct pattern *next; |
| 77 | const char *pattern; |
| 78 | int type; |
| 79 | } *patterns, **patterns_end = &patterns; |
| 80 | |
| 81 | static int default_inclusion = 1; |
| 82 | |
| 83 | static const struct option options[] = { |
| 84 | { "help", no_argument, 0, 'h' }, |
| 85 | { "version", no_argument, 0, 'V' }, |
| 86 | { "debug", no_argument, 0, 'd' }, |
| 87 | { "from", required_argument, 0, 'f' }, |
| 88 | { "to", required_argument, 0, 't' }, |
| 89 | { "include", required_argument, 0, 'i' }, |
| 90 | { "exclude", required_argument, 0, 'e' }, |
| 91 | { "extract-tags", no_argument, 0, 'E' }, |
| 92 | { "tag-encoding", required_argument, 0, 'T' }, |
| 93 | { "untagged", required_argument, 0, 'u' }, |
| 94 | { "windows-friendly", no_argument, 0, 'w' }, |
| 95 | { "link", no_argument, 0, 'l' }, |
| 96 | { "symlink", no_argument, 0, 's' }, |
| 97 | { "copy", no_argument, 0, 'c' }, |
| 98 | { "no-action", no_argument, 0, 'n' }, |
| 99 | { 0, 0, 0, 0 } |
| 100 | }; |
| 101 | |
| 102 | /* display usage message and terminate */ |
| 103 | static void help(void) { |
| 104 | xprintf("Usage:\n" |
| 105 | " disorderfm [OPTIONS] SOURCE DESTINATION\n" |
| 106 | "Options:\n" |
| 107 | " --from, -f ENCODING Source encoding\n" |
| 108 | " --to, -t ENCODING Destination encoding\n" |
| 109 | "If neither --from nor --to are specified then no encoding translation is\n" |
| 110 | "performed. If only one is specified then the other defaults to the current\n" |
| 111 | "locale's encoding.\n" |
| 112 | " --windows-friendly, -w Replace illegal characters with '_'\n" |
| 113 | " --include, -i PATTERN Include files matching a glob pattern\n" |
| 114 | " --exclude, -e PATTERN Include files matching a glob pattern\n" |
| 115 | "--include and --exclude may be used multiple times. They are checked in\n" |
| 116 | "order and the first match wins. If --include is ever used then nonmatching\n" |
| 117 | "files are excluded, otherwise they are included.\n" |
| 118 | " --link, -l Link files from source to destination (default)\n" |
| 119 | " --symlink, -s Symlink files from source to destination\n" |
| 120 | " --copy, -c Copy files from source to destination\n" |
| 121 | " --no-action, -n Just report what would be done\n" |
| 122 | " --debug, -d Debug mode\n" |
| 123 | " --help, -h Display usage message\n" |
| 124 | " --version, -V Display version number\n"); |
| 125 | /* TODO: tag extraction stuff when implemented */ |
| 126 | xfclose(stdout); |
| 127 | exit(0); |
| 128 | } |
| 129 | |
| 130 | /* display version number and terminate */ |
| 131 | static void version(void) { |
| 132 | xprintf("disorderfm version %s\n", disorder_version_string); |
| 133 | xfclose(stdout); |
| 134 | exit(0); |
| 135 | } |
| 136 | |
| 137 | /* Utilities --------------------------------------------------------------- */ |
| 138 | |
| 139 | /* Copy FROM to TO. Has the same signature as link/symlink. */ |
| 140 | static int copy(const char *from, const char *to) { |
| 141 | int fdin, fdout; |
| 142 | char buffer[4096]; |
| 143 | int n; |
| 144 | |
| 145 | if((fdin = open(from, O_RDONLY)) < 0) |
| 146 | fatal(errno, "error opening %s", from); |
| 147 | if((fdout = open(to, O_WRONLY|O_CREAT|O_TRUNC, 0666)) < 0) |
| 148 | fatal(errno, "error opening %s", to); |
| 149 | while((n = read(fdin, buffer, sizeof buffer)) > 0) { |
| 150 | if(write(fdout, buffer, n) < 0) |
| 151 | fatal(errno, "error writing to %s", to); |
| 152 | } |
| 153 | if(n < 0) fatal(errno, "error reading %s", from); |
| 154 | if(close(fdout) < 0) fatal(errno, "error closing %s", to); |
| 155 | xclose(fdin); |
| 156 | return 0; |
| 157 | } |
| 158 | |
| 159 | static int nocopy(const char *from, const char *to) { |
| 160 | xprintf("%s -> %s\n", |
| 161 | any2mb(fromencoding, from), |
| 162 | any2mb(toencoding, to)); |
| 163 | return 0; |
| 164 | } |
| 165 | |
| 166 | static int nomkdir(const char *dir, mode_t attribute((unused)) mode) { |
| 167 | xprintf("mkdir %s\n", any2mb(toencoding, dir)); |
| 168 | return 0; |
| 169 | } |
| 170 | |
| 171 | /* Name translation -------------------------------------------------------- */ |
| 172 | |
| 173 | static int bad_windows_char(int c) { |
| 174 | switch(c) { |
| 175 | default: |
| 176 | return 0; |
| 177 | /* Documented as bad by MS */ |
| 178 | case '<': |
| 179 | case '>': |
| 180 | case ':': |
| 181 | case '"': |
| 182 | case '\\': |
| 183 | case '|': |
| 184 | /* Not documented as bad by MS but Samba mangles anyway? */ |
| 185 | case '*': |
| 186 | return 1; |
| 187 | } |
| 188 | } |
| 189 | |
| 190 | /* Return the translated form of PATH */ |
| 191 | static char *nametrans(const char *path) { |
| 192 | char *t = any2any(fromencoding, toencoding, path); |
| 193 | |
| 194 | if(windowsfriendly) { |
| 195 | /* See: |
| 196 | * http://msdn.microsoft.com/library/default.asp?url=/library/en-us/fileio/fs/naming_a_file.asp?frame=true&hidetoc=true */ |
| 197 | /* List of forbidden names */ |
| 198 | static const char *const devicenames[] = { |
| 199 | "CON", "PRN", "AUX", "NUL", "COM1", "COM2", "COM3", "COM4", "COM5", |
| 200 | "COM6", "COM7", "COM8", "COM9", "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", |
| 201 | "LPT6", "LPT7", "LPT8", "LPT9", "CLOCK$" |
| 202 | }; |
| 203 | #define NDEVICENAMES (sizeof devicenames / sizeof *devicenames) |
| 204 | char *s; |
| 205 | size_t n, l; |
| 206 | |
| 207 | /* Certain characters are just not allowed. We replace them with |
| 208 | * underscores. */ |
| 209 | for(s = t; *s; ++s) |
| 210 | if(bad_windows_char((unsigned char)*s)) |
| 211 | *s = '_'; |
| 212 | /* Trailing spaces and dots are not allowed. We just strip them. */ |
| 213 | while(s > t && (s[-1] == ' ' || s[-1] == '.')) |
| 214 | --s; |
| 215 | *s = 0; |
| 216 | /* Reject device names */ |
| 217 | if((s = strchr(t, '.'))) l = s - t; |
| 218 | else l = 0; |
| 219 | for(n = 0; n < NDEVICENAMES; ++n) |
| 220 | if(l == strlen(devicenames[n]) && !strncasecmp(devicenames[n], t, l)) |
| 221 | break; |
| 222 | if(n < NDEVICENAMES) |
| 223 | byte_xasprintf(&t, "_%s", t); |
| 224 | } |
| 225 | return t; |
| 226 | } |
| 227 | |
| 228 | /* The file walker --------------------------------------------------------- */ |
| 229 | |
| 230 | /* Visit file or directory PATH relative to SOURCE. SOURCE is a null pointer |
| 231 | * at the top level. |
| 232 | * |
| 233 | * PATH is something we extracted from the filesystem so by assumption is in |
| 234 | * the FROM encoding, which might _not_ be the same as the current locale's |
| 235 | * encoding. |
| 236 | * |
| 237 | * For most errors we carry on as best we can. |
| 238 | */ |
| 239 | static void visit(const char *path, const char *destpath) { |
| 240 | const struct pattern *p; |
| 241 | struct stat sb; |
| 242 | /* fullsourcepath is the full source pathname for PATH */ |
| 243 | char *fullsourcepath; |
| 244 | /* fulldestpath will be the full destination pathname */ |
| 245 | char *fulldestpath; |
| 246 | /* String to use in error messags. We convert to the current locale; this |
| 247 | * may be somewhat misleading but is necessary to avoid getting EILSEQ in |
| 248 | * error messages. */ |
| 249 | char *errsourcepath, *errdestpath; |
| 250 | |
| 251 | D(("visit %s", path ? path : "NULL")); |
| 252 | |
| 253 | /* Set up all the various path names */ |
| 254 | if(path) { |
| 255 | byte_xasprintf(&fullsourcepath, "%s/%s", |
| 256 | source, path); |
| 257 | byte_xasprintf(&fulldestpath, "%s/%s", |
| 258 | destination, destpath); |
| 259 | byte_xasprintf(&errsourcepath, "%s/%s", |
| 260 | source, any2mb(fromencoding, path)); |
| 261 | byte_xasprintf(&errdestpath, "%s/%s", |
| 262 | destination, any2mb(toencoding, destpath)); |
| 263 | for(p = patterns; p; p = p->next) |
| 264 | if(fnmatch(p->pattern, path, FNM_PATHNAME) == 0) |
| 265 | break; |
| 266 | if(p) { |
| 267 | /* We found a matching pattern */ |
| 268 | if(p->type == 'e') { |
| 269 | D(("%s matches %s therefore excluding", |
| 270 | path, p->pattern)); |
| 271 | return; |
| 272 | } |
| 273 | } else { |
| 274 | /* We did not find a matching pattern */ |
| 275 | if(!default_inclusion) { |
| 276 | D(("%s matches nothing and not including by default", path)); |
| 277 | return; |
| 278 | } |
| 279 | } |
| 280 | } else { |
| 281 | fullsourcepath = errsourcepath = (char *)source; |
| 282 | fulldestpath = errdestpath = (char *)destination; |
| 283 | } |
| 284 | |
| 285 | /* The destination directory might be a subdirectory of the source |
| 286 | * directory. In that case we'd better not descend into it when we encounter |
| 287 | * it in the source. */ |
| 288 | if(!strcmp(fullsourcepath, destination)) { |
| 289 | info("%s matches destination directory, not recursing", errsourcepath); |
| 290 | return; |
| 291 | } |
| 292 | |
| 293 | /* Find out what kind of file we're dealing with */ |
| 294 | if(stat(fullsourcepath, &sb) < 0) { |
| 295 | error(errno, "cannot stat %s", errsourcepath ); |
| 296 | ++errors; |
| 297 | return; |
| 298 | } |
| 299 | if(S_ISREG(sb.st_mode)) { |
| 300 | if(copier != nocopy) |
| 301 | if(unlink(fulldestpath) < 0 && errno != ENOENT) { |
| 302 | error(errno, "cannot remove %s", errdestpath); |
| 303 | ++errors; |
| 304 | return; |
| 305 | } |
| 306 | if(copier(fullsourcepath, fulldestpath) < 0) { |
| 307 | error(errno, "cannot link %s to %s", errsourcepath, errdestpath); |
| 308 | ++errors; |
| 309 | return; |
| 310 | } |
| 311 | } else if(S_ISDIR(sb.st_mode)) { |
| 312 | DIR *dp; |
| 313 | struct dirent *de; |
| 314 | char *childpath, *childdestpath; |
| 315 | |
| 316 | /* We create the directory on the destination side. If it already exists, |
| 317 | * that's fine. */ |
| 318 | if(dirmaker(fulldestpath, 0777) < 0 && errno != EEXIST) { |
| 319 | error(errno, "cannot mkdir %s", errdestpath); |
| 320 | ++errors; |
| 321 | return; |
| 322 | } |
| 323 | /* We read the directory and visit all the files in it in any old order. */ |
| 324 | if(!(dp = opendir(fullsourcepath))) { |
| 325 | error(errno, "cannot open directory %s", errsourcepath); |
| 326 | ++errors; |
| 327 | return; |
| 328 | } |
| 329 | while(((errno = 0), (de = readdir(dp)))) { |
| 330 | if(!strcmp(de->d_name, ".") |
| 331 | || !strcmp(de->d_name, "..")) continue; |
| 332 | if(path) { |
| 333 | byte_xasprintf(&childpath, "%s/%s", path, de->d_name); |
| 334 | byte_xasprintf(&childdestpath, "%s/%s", |
| 335 | destpath, nametrans(de->d_name)); |
| 336 | } else { |
| 337 | childpath = de->d_name; |
| 338 | childdestpath = nametrans(de->d_name); |
| 339 | } |
| 340 | visit(childpath, childdestpath); |
| 341 | } |
| 342 | if(errno) fatal(errno, "error reading directory %s", errsourcepath); |
| 343 | closedir(dp); |
| 344 | } else { |
| 345 | /* We don't handle special files, but we'd better warn the user. */ |
| 346 | info("ignoring %s", errsourcepath); |
| 347 | } |
| 348 | } |
| 349 | |
| 350 | int main(int argc, char **argv) { |
| 351 | int n; |
| 352 | struct pattern *p; |
| 353 | |
| 354 | mem_init(); |
| 355 | if(!setlocale(LC_CTYPE, "")) fatal(errno, "error calling setlocale"); |
| 356 | while((n = getopt_long(argc, argv, "hVdf:t:i:e:ET:u:wlscn", options, 0)) >= 0) { |
| 357 | switch(n) { |
| 358 | case 'h': help(); |
| 359 | case 'V': version(); |
| 360 | case 'd': debugging = 1; break; |
| 361 | case 'f': fromencoding = optarg; break; |
| 362 | case 't': toencoding = optarg; break; |
| 363 | case 'i': |
| 364 | case 'e': |
| 365 | p = xmalloc(sizeof *p); |
| 366 | p->type = n; |
| 367 | p->pattern = optarg; |
| 368 | p->next = 0; |
| 369 | *patterns_end = p; |
| 370 | patterns_end = &p->next; |
| 371 | if(n == 'i') default_inclusion = 0; |
| 372 | break; |
| 373 | case 'E': extracttags = 1; break; |
| 374 | case 'T': tagencoding = optarg; break; |
| 375 | case 'u': untagged = optarg; break; |
| 376 | case 'w': windowsfriendly = 1; break; |
| 377 | case 'l': copier = link; break; |
| 378 | case 's': copier = symlink; break; |
| 379 | case 'c': copier = copy; break; |
| 380 | case 'n': copier = nocopy; dirmaker = nomkdir; break; |
| 381 | default: fatal(0, "invalid option"); |
| 382 | } |
| 383 | } |
| 384 | if(optind == argc) fatal(0, "missing SOURCE and DESTINATION arguments"); |
| 385 | else if(optind + 1 == argc) fatal(0, "missing DESTINATION argument"); |
| 386 | else if(optind + 2 != argc) fatal(0, "redundant extra arguments"); |
| 387 | if(extracttags) fatal(0, "--extract-tags is not implemented yet"); /* TODO */ |
| 388 | if(tagencoding && !extracttags) |
| 389 | fatal(0, "--tag-encoding without --extra-tags does not make sense"); |
| 390 | if(untagged && !extracttags) |
| 391 | fatal(0, "--untagged without --extra-tags does not make sense"); |
| 392 | source = argv[optind]; |
| 393 | destination = argv[optind + 1]; |
| 394 | nativeencoding = nl_langinfo(CODESET); |
| 395 | if(fromencoding || toencoding) { |
| 396 | if(!fromencoding) fromencoding = nativeencoding; |
| 397 | if(!toencoding) toencoding = nativeencoding; |
| 398 | } |
| 399 | if(!tagencoding) tagencoding = nativeencoding; |
| 400 | visit(0, 0); |
| 401 | xfclose(stdout); |
| 402 | if(errors) fprintf(stderr, "%ld errors\n", errors); |
| 403 | return !!errors; |
| 404 | } |
| 405 | |
| 406 | /* |
| 407 | Local Variables: |
| 408 | c-basic-offset:2 |
| 409 | comment-column:40 |
| 410 | fill-column:79 |
| 411 | indent-tabs-mode:nil |
| 412 | End: |
| 413 | */ |