workaround warning messages regarding 'warn_unused_return_value' when
[util-vserver.git] / util-vserver / src / vhashify.c
1 // $Id$    --*- c -*--
2
3 // Copyright (C) 2005 Enrico Scholz <enrico.scholz@informatik.tu-chemnitz.de>
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; version 2 of the License.
8 //  
9 // This program is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 // GNU General Public License for more details.
13 //  
14 // You should have received a copy of the GNU General Public License
15 // along with this program; if not, write to the Free Software
16 // Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
17
18
19 #ifdef HAVE_CONFIG_H
20 #  include <config.h>
21 #endif
22
23 #include "vhashify.h"
24 #include "util.h"
25
26 #include "lib/internal.h"
27 #include "lib_internal/matchlist.h"
28 #include "lib_internal/unify.h"
29 #include "ensc_vector/vector.h"
30
31 #include <beecrypt/beecrypt.h>
32
33 #include <setjmp.h>
34 #include <unistd.h>
35 #include <getopt.h>
36 #include <string.h>
37 #include <assert.h>
38 #include <stdlib.h>
39 #include <fcntl.h>
40 #include <dirent.h>
41 #include <errno.h>
42 #include <signal.h>
43 #include <limits.h>
44 #include <sys/mman.h>
45 #include <sys/stat.h>
46
47 #define ENSC_WRAPPERS_STDLIB    1
48 #define ENSC_WRAPPERS_UNISTD    1
49 #define ENSC_WRAPPERS_FCNTL     1
50 #define ENSC_WRAPPERS_DIRENT    1
51 #include <wrappers.h>
52
53
54 #define HASH_BLOCKSIZE          0x10000000u
55 #define HASH_MINSIZE            0x10
56
57 #if HASH_MINSIZE<=0
58 #  error HASH_MINSIZE must be not '0'
59 #endif
60
61
62 #define CMD_HELP                0x8000
63 #define CMD_VERSION             0x8001
64
65 #define CMD_DESTINATION         0x1000
66 #define CMD_INSECURE            0x1001
67 #define CMD_SLEDGE              0x1002
68 #define CMD_MANUALLY            0x1003
69 #define CMD_REFRESH             0x1004
70
71 struct option const
72 CMDLINE_OPTIONS[] = {
73   { "help",         no_argument,        0, CMD_HELP },
74   { "version",      no_argument,        0, CMD_VERSION },
75   { "destination",  required_argument,  0, CMD_DESTINATION },
76   { "insecure",     no_argument,        0, CMD_INSECURE },
77   { "sledgehammer", no_argument,        0, CMD_SLEDGE },
78   { "manually",     no_argument,        0, CMD_MANUALLY },
79   { "refresh",      no_argument,        0, CMD_REFRESH },
80   { "dry-run",      no_argument,        0, 'n' },
81   { "verbose",      no_argument,        0, 'v' },
82   { 0,0,0,0 }
83 };
84
85   // SHA1, grouped by 4 digits + hash-collision counter + 2* '/' + NULL
86 typedef char                    HashPath[160/4 + (160/4/4) + sizeof(unsigned int)*2 + 3];
87
88 struct WalkdownInfo
89 {
90     PathInfo                    state;
91     struct MatchList            dst_list;
92     HashDirCollection           hash_dirs;
93     size_t                      hash_dirs_max_size;
94
95     hashFunctionContext         hash_context;
96 };
97
98 int                             wrapper_exit_code = 1;
99 struct Arguments const          *global_args;
100 struct WalkdownInfo             global_info;
101 static struct SkipReason        skip_reason;
102
103 #include "vhashify-init.hc"
104
105 int Global_getVerbosity() {
106   return global_args->verbosity;
107 }
108
109 int Global_doRenew() {
110   return true;
111 }
112
113 int Global_isVserverRunning() {
114     // TODO
115   return global_args->insecure<2;
116 }
117
118 static void
119 showHelp(char const *cmd)
120 {
121   WRITE_MSG(1, "Usage:\n  ");
122   WRITE_STR(1, cmd);
123   WRITE_MSG(1,
124             " [-nv] [--refresh] <vserver>\n    or\n  ");
125   WRITE_STR(1, cmd);
126   WRITE_MSG(1,
127             " --manually [-nv] [--] <hashdir> <path> <excludelist>\n\n"
128             "  --manually      ...  hashify generic paths; excludelists must be generated\n"
129             "                       manually\n"
130             "  --refresh       ...  hashify already hashified files also\n"
131             "  -n              ...  do not modify anything; just show what there will be\n"
132             "                       done (in combination with '-v')\n"
133             "  -v              ...  verbose mode\n"
134             "Please report bugs to " PACKAGE_BUGREPORT "\n");
135
136   exit(0);
137 }
138
139 static void
140 showVersion()
141 {
142   WRITE_MSG(1,
143             "vhashify " VERSION " -- hashifies vservers and/or directories\n"
144             "This program is part of " PACKAGE_STRING "\n\n"
145             "Copyright (C) 2005 Enrico Scholz\n"
146             VERSION_COPYRIGHT_DISCLAIMER);
147   exit(0);
148 }
149
150 int
151 HashDirInfo_compareDevice(void const *lhs_v, void const *rhs_v)
152 {
153   struct HashDirInfo const * const      lhs = lhs_v;
154   dev_t const * const                   rhs = rhs_v;
155
156   assert(lhs!=0 && rhs!=0);
157   return lhs->device - *rhs;
158 }
159
160 PathInfo const *
161 HashDirInfo_findDevice(HashDirCollection const *coll, dev_t dev)
162 {
163   struct HashDirInfo const      *res;
164
165   res = Vector_searchSelfOrg_const(coll, &dev,
166                                    HashDirInfo_compareDevice, vecSHIFT_ONCE);
167
168   if (res!=0) return &res->path;
169   else        return 0;
170 }
171
172 #include "vserver-visitdir.hc"
173
174 static bool
175 checkFstat(PathInfo const * const basename,
176            struct stat * const st)
177 {
178   assert(basename->d[0] != '/');
179
180     // local file does not exist... strange
181     // TODO: message
182   skip_reason.r = rsFSTAT;
183   if (lstat(basename->d, st)==-1) return false;
184
185     // this is a directory and succeeds everytime
186   if (S_ISDIR(st->st_mode))
187     return true;
188
189     // ignore symlinks
190   skip_reason.r = rsSYMLINK;
191   if (S_ISLNK(st->st_mode))       return false;
192
193     // ignore special files
194   skip_reason.r = rsSPECIAL;
195   if (!S_ISREG(st->st_mode) &&
196       !S_ISDIR(st->st_mode))      return false;
197   
198     // ignore small files
199   skip_reason.r = rsTOOSMALL;
200   if (st->st_size < HASH_MINSIZE) return false;
201   
202   switch (Unify_isIUnlinkable(basename->d)) {
203     case unifyUNSUPPORTED       :  skip_reason.r = rsUNSUPPORTED; return false;
204     case unifyBUSY              :
205         // do an implicit refresh on busy files when there are no active links
206       if (st->st_nlink>1 && !global_args->do_refresh) {
207           // TODO: message
208         skip_reason.r = rsUNIFIED;
209         return false;
210       }
211       break;
212     default                     :  break;
213   }
214
215   return true;
216 }
217
218 static jmp_buf                  bus_error_restore;
219 static volatile sig_atomic_t    bus_error;
220
221 static void
222 handlerSIGBUS(int UNUSED num)
223 {
224   bus_error = 1;
225   longjmp(bus_error_restore, 1);
226 }
227
228 static bool
229 convertDigest(HashPath d_path)
230 {
231   static char const             HEX_DIGIT[] = "0123456789abcdef";
232   hashFunctionContext * const   h_ctx    = &global_info.hash_context;
233   size_t                        d_size   = h_ctx->algo->digestsize;
234     
235   unsigned char                 digest[d_size];
236   size_t                        out = 0;
237
238   if (hashFunctionContextDigest(h_ctx, digest)==-1)
239     return false;
240   
241   for (size_t in=0;
242        out+1<sizeof(HashPath)-(sizeof(unsigned int)*2 + 2) && in<d_size;
243        ++in) {
244     if (in%2 == 0 && in>0) d_path[out++]='/';
245     d_path[out++] = HEX_DIGIT[digest[in] >>    4];
246     d_path[out++] = HEX_DIGIT[digest[in] &  0x0f];
247   }
248   d_path[out++] = '\0';
249   
250   return true;
251 }
252
253 static bool
254 addStatHash(hashFunctionContext *h_ctx, struct stat const * const st)
255 {
256 #define DECL_ATTR(X)    __typeof__(st->st_##X)  X
257 #define SET_ATTR(X)     .X = st->st_##X
258   
259   struct __attribute__((__packed__)) {
260     DECL_ATTR(mode);
261     DECL_ATTR(uid);
262     DECL_ATTR(gid);
263     DECL_ATTR(rdev);
264     DECL_ATTR(size);
265     DECL_ATTR(mtime);
266   }             tmp = {
267     SET_ATTR(mode),
268     SET_ATTR(uid),
269     SET_ATTR(gid),
270     SET_ATTR(rdev),
271     SET_ATTR(size),
272     SET_ATTR(mtime)
273   };
274
275   return hashFunctionContextUpdate(h_ctx, (void *)&tmp, sizeof tmp)!=-1;
276 }
277
278 static bool
279 calculateHashFromFD(int fd, HashPath d_path, struct stat const * const st)
280 {
281   hashFunctionContext * const   h_ctx    = &global_info.hash_context;
282   bool                          res      = false;
283   loff_t                        offset   = 0;
284   void                          *buf     = 0;
285   off_t                         size     = st->st_size;
286   loff_t                        cur_size = 0;
287
288
289   if (hashFunctionContextReset(h_ctx)==-1 ||
290       !addStatHash(h_ctx, st))
291     return false;
292
293   bus_error = 0;
294   if (setjmp(bus_error_restore)!=0) goto out;
295
296   while (offset < size) {
297     cur_size = size-offset;
298     if (cur_size>HASH_BLOCKSIZE) cur_size = HASH_BLOCKSIZE;
299
300     buf     = mmap(0, cur_size, PROT_READ, MAP_SHARED, fd, offset);
301     if (buf==0) goto out;
302     
303     offset += cur_size;
304     madvise(buf, cur_size, MADV_SEQUENTIAL);    // ignore error...
305
306     if (hashFunctionContextUpdate(h_ctx, buf, cur_size)==-1) goto out;
307
308     munmap(buf, cur_size);
309     buf = 0;
310   }
311
312   if (!convertDigest(d_path)) goto out;
313     
314   res = true;
315
316   out:
317   if (buf!=0) munmap(buf, cur_size);
318   return res;
319 }
320
321 static bool
322 calculateHash(PathInfo const *filename, HashPath d_path, struct stat const * const st)
323 {
324   int           fd  = open(filename->d, O_NOFOLLOW|O_NONBLOCK|O_RDONLY|O_NOCTTY);
325   struct stat   fst;
326   bool          res = false;
327
328   do {
329     if (fd==-1) {
330       int       old_errno = errno;
331       WRITE_MSG(2, "Failed to open '");
332       WRITE_STR(2, filename->d);
333       errno = old_errno;
334       perror("'");
335       break;;
336     }
337   
338     if (fstat(fd, &fst)==-1 ||
339         fst.st_dev!=st->st_dev || fst.st_ino!=st->st_ino) {
340       WRITE_MSG(2, "An unexpected event occured while stating '");
341       WRITE_STR(2, filename->d);
342       WRITE_MSG(2, "'.\n");
343       break;
344     }
345
346     if (!calculateHashFromFD(fd, d_path, st)) {
347       WRITE_MSG(2, "Failed to calculate hash for '");
348       WRITE_STR(2, filename->d);
349       WRITE_MSG(2, "'.\n");
350       break;
351     }
352
353     res = true;
354   } while (false);
355   
356   if (fd!=-1) close(fd);
357   return res;
358 }
359
360 static bool
361 mkdirRecursive(char const *path)
362 {
363   struct stat           st;
364
365   if (path[0]!='/')        return false; // only absolute paths
366   if (lstat(path,&st)!=-1) return true;
367
368   char                  buf[strlen(path)+1];
369   char *                ptr = buf+1;
370   
371   strcpy(buf, path);
372
373   while ((ptr = strchr(ptr, '/'))!=0) {
374     *ptr = '\0';
375     if (mkdir(buf, 0700)==-1 && errno!=EEXIST) {
376       int               old_errno = errno;
377       WRITE_MSG(2, "mkdir('");
378       WRITE_STR(2, buf);
379       errno = old_errno;
380       perror("')");
381       return false;
382     }
383     *ptr = '/';
384     ++ptr;
385   }
386
387   return true;
388 }
389
390 static bool
391 resolveCollisions(char *result, PathInfo const *root, HashPath d_path,
392                   struct stat *st, struct stat *hash_st)
393 {
394   strcpy(result, root->d);      // 'root' ends on '/' already (see initHashList())
395   strcat(result, d_path);
396   
397   char                  *ptr = result + strlen(result);
398   unsigned int          idx  = 0;
399   char                  buf[sizeof(int)*2 + 1];
400   size_t                len;
401
402   *ptr++             = '/';
403   *ptr               = '\0';
404   ptr[sizeof(int)*2] = '\0';
405
406   if (!global_args->dry_run &&
407       !mkdirRecursive(result))
408     return false;
409
410   for (;; ++idx) {
411     len = utilvserver_fmt_xuint(buf, idx);
412     memset(ptr, '0', sizeof(int)*2 - len);
413     memcpy(ptr + sizeof(int)*2 - len, buf, len);
414
415     if (lstat(result, hash_st)==-1) {
416       if (global_args->dry_run && errno!=ENOENT) {
417         int             old_errno = errno;
418         WRITE_MSG(2, "lstat('");
419         WRITE_STR(2, buf);
420         errno = old_errno;
421         perror("')");
422         return false;
423       }
424     }
425     else if (Unify_isUnified(st, hash_st)) {
426       skip_reason.r = rsUNIFIED;
427       return false;
428     }
429     else if (!Unify_isUnifyable(st, hash_st))
430       continue;         // continue with next number*****
431     else
432       break;            // ok, we finish here
433
434     if (!global_args->dry_run) {
435       int               fd = open(result, O_NOFOLLOW|O_EXCL|O_CREAT|O_WRONLY, 0200);
436
437       if (global_args->dry_run && fd==-1) {
438         int             old_errno = errno;
439         WRITE_MSG(2, "open('");
440         WRITE_STR(2, buf);
441         errno = old_errno;
442         perror("')");
443         return false;
444       }
445
446       close(fd);
447     }
448
449       // HACK: avoid an additional lstat on the resulting hash-file
450     hash_st->st_size = 0;
451     break;
452   }
453
454   return true;
455 }
456
457 static char const *
458 checkDirEntry(PathInfo const *path, PathInfo const *basename,
459               bool *is_dir,
460               struct stat *st, struct stat *hash_st,
461               char *result_buf)
462 {
463     //printf("checkDirEntry(%s, %s, %u)\n", path->d, d_path, is_dir);
464
465   struct WalkdownInfo const * const     info       = &global_info;
466
467   // Check if it is in the exclude/include list of the destination vserver and
468   // abort when it is not matching an allowed entry
469   skip_reason.r      = rsEXCL;
470   if (MatchList_compare(&info->dst_list, path->d)!=stINCLUDE) return 0;
471
472   if (checkFstat(basename, st)) {
473     PathInfo const      *hash_root_path;
474     HashPath            d_path;
475     
476     *is_dir = S_ISDIR(st->st_mode);
477
478     if (!*is_dir &&
479         !((skip_reason.r = rsWRONGDEV,
480            (hash_root_path = HashDirInfo_findDevice(&info->hash_dirs, st->st_dev))!=0) &&
481           (skip_reason.r = rsGENERAL,
482            calculateHash(basename, d_path, st)) &&
483           resolveCollisions(result_buf, hash_root_path, d_path, st, hash_st)))
484       return 0;
485
486     return result_buf;
487   }
488
489   return 0;
490 }
491
492 static void
493 printSkipReason()
494 {
495   WRITE_MSG(1, " (");
496   switch (skip_reason.r) {
497     case rsDOTFILE      :  WRITE_MSG(1, "dotfile"); break;
498     case rsEXCL         :  WRITE_MSG(1, "excluded"); break;
499     case rsTOOSMALL     :  WRITE_MSG(1, "too small"); break;
500     case rsUNSUPPORTED  :  WRITE_MSG(1, "operation not supported"); break;
501     case rsFSTAT        :  WRITE_MSG(1, "fstat error"); break;
502     case rsSYMLINK      :  WRITE_MSG(1, "symlink"); break;
503     case rsUNIFIED      :  WRITE_MSG(1, "already unified"); break;
504     case rsSPECIAL      :  WRITE_MSG(1, "non regular file"); break;
505     case rsWRONGDEV     :  WRITE_MSG(1, "no matching device"); break;
506     case rsGENERAL      :  WRITE_MSG(1, "general error"); break;
507     default             :  assert(false); abort();
508   }
509   WRITE_MSG(1, ")");
510 }
511
512 static bool
513 doit(char const *src, char const *dst,
514      struct stat const *src_st, struct stat const *dst_st,
515      PathInfo const *path)
516 {
517   if (global_args->dry_run || Global_getVerbosity()>=2) {
518     WRITE_MSG(1, "unifying   '");
519     Vwrite(1, path->d, path->l);
520     WRITE_MSG(1, "'");
521     
522     if (Global_getVerbosity()>=4) {
523       WRITE_MSG(1, " (to '");
524       WRITE_STR(1, dst);
525       WRITE_MSG(1, "')");
526     }
527
528     WRITE_MSG(1, "\n");
529   }
530
531     // abort here in dry-run mode
532   if (global_args->dry_run) return true;
533
534   if (dst_st->st_size==0) {
535       // file was not unified yet
536     
537     if (Global_isVserverRunning()) {
538       (void)unlink(dst);
539       if (Unify_copy (src, src_st, dst) &&
540           // the mixed 'dst' and 'src_st' params are intentionally...
541           Unify_unify(dst, src_st, src, false))
542         return true;
543     }
544     else if (Unify_unify(src, src_st, dst, true))
545       return true;
546
547     (void)unlink(dst);  // cleanup in error-case
548   }
549     // there exists already a reference-file
550   else if (Unify_unify(dst, dst_st, src, false))
551     return true;
552
553   return false;
554 }
555
556 static uint64_t
557 visitDirEntry(struct dirent const *ent)
558 {
559   uint64_t                      res      = 0;
560   char const *                  dirname  = ent->d_name;
561   PathInfo                      path     = global_info.state;
562   PathInfo                      tmp_path = {
563     .d = dirname,
564     .l = strlen(dirname)
565   };
566   char                          path_buf[ENSC_PI_APPSZ(path, tmp_path)];
567   char const                    *match = 0;
568
569   
570   PathInfo_append(&path, &tmp_path, path_buf);
571
572   bool                          is_dotfile    = isDotfile(dirname);
573   bool                          is_dir;
574   struct stat                   src_stat = { .st_mode=0 };
575   struct stat                   hash_stat;
576   char                          tmpbuf[global_info.hash_dirs_max_size +
577                                        sizeof(HashPath) + 2];
578   
579   skip_reason.r = rsDOTFILE;
580
581   if (is_dotfile ||
582       (match=checkDirEntry(&path, &tmp_path,
583                            &is_dir, &src_stat, &hash_stat,
584                            tmpbuf))==0) {
585
586     bool        is_link = !is_dotfile && S_ISLNK(src_stat.st_mode);
587
588     if (Global_getVerbosity()>=1 &&
589         (Global_getVerbosity()>=3 || skip_reason.r!=rsUNIFIED) &&
590         ((!is_dotfile && !is_link) ||
591          (Global_getVerbosity()>=6 && is_dotfile) ||
592          (Global_getVerbosity()>=6 && is_link)) ) {
593       WRITE_MSG(1, "  skipping '");
594       Vwrite(1, path.d, path.l);
595       WRITE_MSG(1, "'");
596       if (Global_getVerbosity()>=2) printSkipReason();
597       WRITE_MSG(1, "\n");
598     }
599
600     return 0;
601   }
602
603   if (is_dir) {
604     res = visitDir(dirname, &src_stat);
605   }
606   else if (doit(dirname, match, &src_stat, &hash_stat, &path))
607     res = 1;
608   else {
609       // TODO: message
610     res = 0;
611   }
612
613   return res;
614     
615 }
616
617 int main(int argc, char *argv[])
618 {
619   struct Arguments      args = {
620     .mode               =  mdVSERVER,
621     .hash_dir           =  0,
622     .verbosity          =  0,
623     .insecure           =  0,
624     .dry_run            =  false,
625     .do_refresh         =  false,
626   };
627
628   Vector_init(&global_info.hash_dirs, sizeof(struct HashDirInfo));
629
630   if (hashFunctionContextInit(&global_info.hash_context,
631                               hashFunctionDefault())==-1)
632     return EXIT_FAILURE;
633
634
635   global_args = &args;
636   while (1) {
637     int         c = getopt_long(argc, argv, "+nv",
638                                 CMDLINE_OPTIONS, 0);
639     if (c==-1) break;
640
641     switch (c) {
642       case CMD_HELP             :  showHelp(argv[0]);
643       case CMD_VERSION          :  showVersion();
644       case CMD_DESTINATION      :  args.hash_dir    = optarg; break;
645       case CMD_MANUALLY         :  args.mode        = mdMANUALLY; break;
646       case CMD_INSECURE         :  args.insecure    = 1;    break;
647       case CMD_SLEDGE           :  args.insecure    = 2;    break;
648       case CMD_REFRESH          :  args.do_refresh  = true; break;
649       case 'n'                  :  args.dry_run     = true; break;
650       case 'v'                  :  ++args.verbosity; break;
651       default           :
652         WRITE_MSG(2, "Try '");
653         WRITE_STR(2, argv[0]);
654         WRITE_MSG(2, " --help\" for more information.\n");
655         return EXIT_FAILURE;
656         break;
657     }
658   }
659
660   if (argc==optind) {
661     WRITE_MSG(2, "No directory/vserver given\n");
662     return EXIT_FAILURE;
663   }
664
665   if (args.hash_dir==0 && args.mode==mdMANUALLY) {
666     WRITE_MSG(2, "'--manually' requires '--destination'\n");
667     return EXIT_FAILURE;
668   }
669
670   switch (args.mode) {
671     case mdMANUALLY     :  initModeManually(&args, argc-optind, argv+optind); break;
672     case mdVSERVER      :  initModeVserver (&args, argc-optind, argv+optind); break;
673     default             :  assert(false); return EXIT_FAILURE;
674   };
675
676   if (Global_getVerbosity()>=1)
677     WRITE_MSG(1, "Starting to traverse directories...\n");
678
679   signal(SIGBUS, handlerSIGBUS);
680   
681   Echdir(global_info.dst_list.root.d);
682   visitDir("/", 0);
683
684 #ifndef NDEBUG
685   MatchList_destroy(&global_info.dst_list);
686   freeHashList(&global_info.hash_dirs);
687   hashFunctionContextFree(&global_info.hash_context);
688 #endif
689 }