initial checkin
[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 <setjmp.h>
32 #include <beecrypt/beecrypt.h>
33 #include <unistd.h>
34 #include <getopt.h>
35 #include <string.h>
36 #include <assert.h>
37 #include <stdlib.h>
38 #include <fcntl.h>
39 #include <dirent.h>
40 #include <errno.h>
41 #include <signal.h>
42 #include <limits.h>
43 #include <sys/mman.h>
44 #include <sys/stat.h>
45
46 #define ENSC_WRAPPERS_STDLIB    1
47 #define ENSC_WRAPPERS_UNISTD    1
48 #define ENSC_WRAPPERS_FCNTL     1
49 #define ENSC_WRAPPERS_DIRENT    1
50 #include <wrappers.h>
51
52
53 #define HASH_BLOCKSIZE          0x10000000
54 #define HASH_MINSIZE            0x10
55
56
57
58 #define CMD_HELP                0x8000
59 #define CMD_VERSION             0x8001
60
61 #define CMD_DESTINATION         0x1000
62 #define CMD_INSECURE            0x1001
63 #define CMD_SLEDGE              0x1002
64 #define CMD_MANUALLY            0x1003
65
66 struct option const
67 CMDLINE_OPTIONS[] = {
68   { "help",         no_argument,        0, CMD_HELP },
69   { "version",      no_argument,        0, CMD_VERSION },
70   { "destination",  required_argument,  0, CMD_DESTINATION },
71   { "insecure",     no_argument,        0, CMD_INSECURE },
72   { "sledgehammer", no_argument,        0, CMD_SLEDGE },
73   { "manually",     no_argument,        0, CMD_MANUALLY },
74   { 0,0,0,0 }
75 };
76
77   // SHA1, grouped by 4 digits + hash-collision counter + 2* '/' + NULL
78 typedef char                    HashPath[160/4 + (160/4/4) + sizeof(unsigned int)*2 + 3];
79
80 struct WalkdownInfo
81 {
82     PathInfo                    state;
83     struct MatchList            dst_list;
84     HashDirCollection           hash_dirs;
85     size_t                      hash_dirs_max_size;
86
87     hashFunctionContext         hash_context;
88 };
89
90 int                             wrapper_exit_code = 1;
91 struct Arguments const          *global_args;
92 struct WalkdownInfo             global_info;
93 static struct SkipReason        skip_reason;
94
95 #include "vhashify-init.hc"
96
97 int Global_getVerbosity() {
98   return global_args->verbosity;
99 }
100
101 int Global_doRenew() {
102   return true;
103 }
104
105 static void
106 showHelp(char const *cmd)
107 {
108   WRITE_MSG(1, "Usage:\n  ");
109   WRITE_STR(1, cmd);
110   WRITE_MSG(1,
111             " [-Rnv] <vserver>\n    or\n  ");
112   WRITE_STR(1, cmd);
113   WRITE_MSG(1,
114             " --manually [-Rnvx] [--] <hashdir> <path> <excludelist>\n\n"
115             "  --manually      ...  hashify generic paths; excludelists must be generated\n"
116             "                       manually\n"
117             "  -R              ...  revert operation; dehashify files\n"
118             "  -n              ...  do not modify anything; just show what there will be\n"
119             "                       done (in combination with '-v')\n"
120             "  -v              ...  verbose mode\n"
121             "Please report bugs to " PACKAGE_BUGREPORT "\n");
122
123   exit(0);
124 }
125
126 static void
127 showVersion()
128 {
129   WRITE_MSG(1,
130             "vhashify " VERSION " -- hashifies vservers and/or directories\n"
131             "This program is part of " PACKAGE_STRING "\n\n"
132             "Copyright (C) 2005 Enrico Scholz\n"
133             VERSION_COPYRIGHT_DISCLAIMER);
134   exit(0);
135 }
136
137 int
138 HashDirInfo_compareDevice(void const *lhs_v, void const *rhs_v)
139 {
140   struct HashDirInfo const * const      lhs = lhs_v;
141   dev_t const * const                   rhs = rhs_v;
142
143   assert(lhs!=0 && rhs!=0);
144   return lhs->device - *rhs;
145 }
146
147 PathInfo const *
148 HashDirInfo_findDevice(HashDirCollection const *coll, dev_t dev)
149 {
150   struct HashDirInfo const      *res;
151
152   res = Vector_searchSelfOrg_const(coll, &dev,
153                                    HashDirInfo_compareDevice, vecSHIFT_ONCE);
154
155   if (res!=0) return &res->path;
156   else        return 0;
157 }
158
159 #include "vserver-visitdir.hc"
160
161 static bool
162 checkFstat(PathInfo const * const basename,
163            struct stat * const st)
164 {
165   assert(basename->d[0] != '/');
166
167     // local file does not exist... strange
168     // TODO: message
169   skip_reason.r = rsFSTAT;
170   if (lstat(basename->d, st)==-1) return false;
171
172     // this is a directory and succeeds everytime
173   if (S_ISDIR(st->st_mode))
174     return true;
175
176     // ignore symlinks
177   skip_reason.r = rsSYMLINK;
178   if (S_ISLNK(st->st_mode))       return false;
179
180     // ignore special files
181   skip_reason.r = rsSPECIAL;
182   if (!S_ISREG(st->st_mode) &&
183       !S_ISDIR(st->st_mode))      return false;
184   
185     // ignore small files
186   skip_reason.r = rsTOOSMALL;
187   if (st->st_size < HASH_MINSIZE) return false;
188   
189   skip_reason.r = rsUNIFIED;
190   if ((!global_args->do_revert && !(st->st_nlink==1 || Unify_isIUnlinkable(basename->d))) ||
191       ( global_args->do_revert &&                      Unify_isIUnlinkable(basename->d)))
192     return false;
193
194   return true;
195 }
196
197 static jmp_buf                  bus_error_restore;
198 static volatile sig_atomic_t    bus_error;
199
200 static void
201 handlerSIGBUS(int UNUSED num)
202 {
203   bus_error = 1;
204   longjmp(bus_error_restore, 1);
205 }
206
207 static bool
208 convertDigest(HashPath d_path)
209 {
210   static char const             HEX_DIGIT[] = "0123456789abcdef";
211   hashFunctionContext * const   h_ctx    = &global_info.hash_context;
212   size_t                        d_size   = h_ctx->algo->digestsize;
213     
214   unsigned char                 digest[d_size];
215   size_t                        out = 0;
216
217   if (hashFunctionContextDigest(h_ctx, digest)==-1)
218     return false;
219   
220   for (size_t in=0;
221        out+1<sizeof(HashPath)-(sizeof(unsigned int)*2 + 2) && in<d_size;
222        ++in) {
223     if (in%2 == 0 && in>0) d_path[out++]='/';
224     d_path[out++] = HEX_DIGIT[digest[in] >>    4];
225     d_path[out++] = HEX_DIGIT[digest[in] &  0x0f];
226   }
227   d_path[out++] = '\0';
228   
229   return true;
230 }
231
232 static bool
233 addStatHash(hashFunctionContext *h_ctx, struct stat const * const st)
234 {
235 #define DECL_ATTR(X)    __typeof__(st->st_##X)  X
236 #define SET_ATTR(X)     .X = st->st_##X
237   
238   struct __attribute__((__packed__)) {
239     DECL_ATTR(mode);
240     DECL_ATTR(uid);
241     DECL_ATTR(gid);
242     DECL_ATTR(rdev);
243     DECL_ATTR(size);
244     DECL_ATTR(mtime);
245   }             tmp = {
246     SET_ATTR(mode),
247     SET_ATTR(uid),
248     SET_ATTR(gid),
249     SET_ATTR(rdev),
250     SET_ATTR(size),
251     SET_ATTR(mtime)
252   };
253
254   return hashFunctionContextUpdate(h_ctx, (void *)&tmp, sizeof tmp)!=-1;
255 }
256
257 static bool
258 calculateHashFromFD(int fd, HashPath d_path, struct stat const * const st)
259 {
260   hashFunctionContext * const   h_ctx    = &global_info.hash_context;
261   bool                          res      = false;
262   loff_t                        offset   = 0;
263   void                          *buf     = 0;
264   off_t                         size     = st->st_size;
265   loff_t                        cur_size = 0;
266
267
268   if (hashFunctionContextReset(h_ctx)==-1 ||
269       !addStatHash(h_ctx, st))
270     return false;
271
272   bus_error = 0;
273   if (setjmp(bus_error_restore)!=0) goto out;
274
275   while (offset < size) {
276     size_t      real_size = size-offset;
277     cur_size = real_size;
278       //cur_size = (real_size + PAGESIZE-1)/PAGESIZE * PAGESIZE;
279     if (cur_size>HASH_BLOCKSIZE) cur_size = HASH_BLOCKSIZE;
280
281     buf     = mmap(0, cur_size, PROT_READ, MAP_SHARED, fd, offset);
282     offset += real_size;
283
284     madvise(buf, cur_size, MADV_SEQUENTIAL);    // ignore error...
285
286     if (buf==0) goto out;
287     if (hashFunctionContextUpdate(h_ctx, buf, real_size)==-1) goto out;
288
289     munmap(buf, cur_size);
290     buf = 0;
291   }
292
293   if (!convertDigest(d_path)) goto out;
294     
295   res = true;
296
297   out:
298   if (buf!=0) munmap(buf, cur_size);
299   return res;
300 }
301
302 bool
303 calculateHash(PathInfo const *filename, HashPath d_path, struct stat const * const st)
304 {
305   int           fd  = open(filename->d, O_NOFOLLOW|O_NONBLOCK|O_RDONLY|O_NOCTTY);
306   struct stat   fst;
307   bool          res = false;
308
309   do {
310     if (fd==-1) {
311       int       old_errno = errno;
312       WRITE_MSG(2, "Failed to open '");
313       WRITE_STR(2, filename->d);
314       errno = old_errno;
315       perror("'");
316       break;;
317     }
318   
319     if (fstat(fd, &fst)==-1 ||
320         fst.st_dev!=st->st_dev || fst.st_ino!=st->st_ino) {
321       WRITE_MSG(2, "An unexpected event occured while stating '");
322       WRITE_STR(2, filename->d);
323       WRITE_MSG(2, "'.\n");
324       break;
325     }
326
327     if (!calculateHashFromFD(fd, d_path, st)) {
328       WRITE_MSG(2, "Failed to calculate hash for '");
329       WRITE_STR(2, filename->d);
330       WRITE_MSG(2, "'.\n");
331       break;
332     }
333
334     res = true;
335   } while (false);
336   
337   if (fd!=-1) close(fd);
338   return res;
339 }
340
341 static bool
342 mkdirRecursive(char const *path)
343 {
344   struct stat           st;
345
346   if (path[0]!='/')       return false; // only absolute paths
347   if (lstat(path,&st)!=-1) return true;
348
349   char                  buf[strlen(path)+1];
350   char *                ptr = buf+1;
351   
352   strcpy(buf, path);
353
354   while ((ptr = strchr(ptr, '/'))!=0) {
355     *ptr = '\0';
356     if (mkdir(buf, 0700)==-1 && errno!=EEXIST) {
357       int               old_errno = errno;
358       WRITE_MSG(2, "mkdir('");
359       WRITE_STR(2, buf);
360       errno = old_errno;
361       perror("')");
362       return false;
363     }
364     *ptr = '/';
365     ++ptr;
366   }
367
368   return true;
369 }
370
371 static bool
372 resolveCollisions(char *result, PathInfo const *root, HashPath d_path, struct stat *st)
373 {
374   strcpy(result, root->d);      // 'root' ends on '/' already (see initHashList())
375   strcat(result, d_path);
376   
377   char                  *ptr = result + strlen(result);
378   unsigned int          idx  = 0;
379   char                  buf[sizeof(int)*2 + 1];
380   size_t                len;
381
382   *ptr++             = '/';
383   *ptr               = '\0';
384   ptr[sizeof(int)*2] = '\0';
385
386   if (!mkdirRecursive(result))
387     return false;
388
389   for (;; ++idx) {
390     len = utilvserver_fmt_xuint(buf, idx);
391     memset(ptr, '0', sizeof(int)*2 - len);
392     memcpy(ptr + sizeof(int)*2 - len, buf, len);
393
394     struct stat         new_st;
395     if (lstat(result, &new_st)==-1) {
396       if (errno!=ENOENT) {
397         int             old_errno = errno;
398         WRITE_MSG(2, "lstat('");
399         WRITE_STR(2, buf);
400         errno = old_errno;
401         perror("')");
402         return false;
403       }
404     }
405     else if (!Unify_isUnifyable(st, &new_st))
406       continue;         // continue with next number
407     else
408       break;            // ok, we finish here
409
410     int         fd = open(result, O_NOFOLLOW|O_EXCL|O_CREAT|O_WRONLY, 0200);
411
412     if (fd==-1) {
413       int               old_errno = errno;
414       WRITE_MSG(2, "open('");
415       WRITE_STR(2, buf);
416       errno = old_errno;
417       perror("')");
418       return false;
419     }
420
421     close(fd);
422     break;
423   }
424
425   return true;
426 }
427
428 static char const *
429 checkDirEntry(PathInfo const *path, PathInfo const *basename,
430               bool *is_dir, struct stat *st, char *result_buf)
431 {
432     //printf("checkDirEntry(%s, %s, %u)\n", path->d, d_path, is_dir);
433
434   struct WalkdownInfo const * const     info       = &global_info;
435
436   // Check if it is in the exclude/include list of the destination vserver and
437   // abort when it is not matching an allowed entry
438   skip_reason.r      = rsEXCL;
439   if (MatchList_compare(&info->dst_list, path->d)!=stINCLUDE) return 0;
440
441   if (checkFstat(basename, st)) {
442     PathInfo const      *hash_root_path;
443     HashPath            d_path;
444     
445     *is_dir = S_ISDIR(st->st_mode);
446
447     if (!*is_dir &&
448         !((hash_root_path = HashDirInfo_findDevice(&info->hash_dirs, st->st_dev))!=0 &&
449           calculateHash(basename, d_path, st) &&
450           resolveCollisions(result_buf, hash_root_path, d_path, st)))
451       return 0;
452
453     return result_buf;
454   }
455
456   return 0;
457 }
458
459 static uint64_t
460 visitDirEntry(struct dirent const *ent)
461 {
462   uint64_t                      res      = 0;
463   char const *                  dirname  = ent->d_name;
464   PathInfo                      path     = global_info.state;
465   PathInfo                      tmp_path = {
466     .d = dirname,
467     .l = strlen(dirname)
468   };
469   char                          path_buf[ENSC_PI_APPSZ(path, tmp_path)];
470   char const                    *match = 0;
471
472   
473   PathInfo_append(&path, &tmp_path, path_buf);
474
475   bool                          is_dotfile    = isDotfile(dirname);
476   bool                          is_dir;
477   struct stat                   src_stat;
478   char                          tmpbuf[global_info.hash_dirs_max_size +
479                                        sizeof(HashPath) + 2];
480   
481   skip_reason.r = rsDOTFILE;
482
483   if (is_dotfile ||
484       (match=checkDirEntry(&path, &tmp_path,
485                            &is_dir, &src_stat, tmpbuf))==0) {
486
487     return 0;
488   }
489
490   if (is_dir) {
491     res = visitDir(dirname, &src_stat);
492   }
493   else {
494     printf("%s <- %s\n", match, path.d);
495     res = 0;
496   }
497
498   return res;
499     
500 }
501
502 int main(int argc, char *argv[])
503 {
504   struct Arguments      args = {
505     .mode               =  mdVSERVER,
506     .hash_dir           =  0,
507     .verbosity          =  0,
508     .insecure           =  0,
509   };
510
511   Vector_init(&global_info.hash_dirs, sizeof(struct HashDirInfo));
512
513   if (hashFunctionContextInit(&global_info.hash_context,
514                               hashFunctionDefault())==-1)
515     return EXIT_FAILURE;
516
517
518   global_args = &args;
519   while (1) {
520     int         c = getopt_long(argc, argv, "",
521                                 CMDLINE_OPTIONS, 0);
522     if (c==-1) break;
523
524     switch (c) {
525       case CMD_HELP             :  showHelp(argv[0]);
526       case CMD_VERSION          :  showVersion();
527       case CMD_DESTINATION      :  args.hash_dir    = optarg; break;
528       case CMD_MANUALLY         :  args.mode        = mdMANUALLY; break;
529       case CMD_INSECURE         :  args.insecure    = 1; break;
530       case CMD_SLEDGE           :  args.insecure    = 2; break;
531       case 'v'                  :  ++args.verbosity; break;
532       default           :
533         WRITE_MSG(2, "Try '");
534         WRITE_STR(2, argv[0]);
535         WRITE_MSG(2, " --help\" for more information.\n");
536         return EXIT_FAILURE;
537         break;
538     }
539   }
540
541   if (argc==optind) {
542     WRITE_MSG(2, "No directory/vserver given\n");
543     return EXIT_FAILURE;
544   }
545
546   if (args.hash_dir==0 && args.mode==mdMANUALLY) {
547     WRITE_MSG(2, "'--manually' requires '--destination'\n");
548     return EXIT_FAILURE;
549   }
550
551   switch (args.mode) {
552     case mdMANUALLY     :  initModeManually(&args, argc-optind, argv+optind); break;
553     case mdVSERVER      :  initModeVserver (&args, argc-optind, argv+optind); break;
554     default             :  assert(false); return EXIT_FAILURE;
555   };
556
557   if (Global_getVerbosity()>=1)
558     WRITE_MSG(1, "Starting to traverse directories...\n");
559
560   signal(SIGBUS, handlerSIGBUS);
561   
562   Echdir(global_info.dst_list.root.d);
563   visitDir("/", 0);  
564 }