1 /*
2    sitecopy, for managing remote web sites. File handling.
3    Copyright (C) 1998-2006, Joe Orton <joe@manyfish.co.uk>
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,
11    but WITHOUT ANY WARRANTY; without even the implied warranty of
12    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13    GNU 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., 675 Mass Ave, Cambridge, MA 02139, USA.
18 
19 */
20 
21 #include <config.h>
22 
23 #include <sys/types.h>
24 
25 /* Needed for S_IXUSR */
26 #include <sys/stat.h>
27 
28 #ifdef HAVE_STDLIB_H
29 #include <stdlib.h>
30 #endif
31 #ifdef HAVE_UNISTD_H
32 #include <unistd.h>
33 #endif
34 #ifdef HAVE_STRING_H
35 #include <string.h>
36 #endif
37 
38 #include <ctype.h>
39 
40 #include "basename.h"
41 
42 #include <ne_md5.h>
43 #include <ne_string.h>
44 #include <ne_alloc.h>
45 
46 /* We pick up FNM_LEADING_DIR fnmatch() extension, since we define
47  * _GNU_SOURCE in config.h. */
48 #include <fnmatch.h>
49 
50 #include "frontend.h"
51 #include "sitesi.h"
52 
53 /* fnmatch the filename against list */
54 inline int fnlist_match(const char *filename, const struct fnlist *list);
55 
56 /* Deletes the given file from the given site */
file_delete(struct site * site,struct site_file * item)57 void file_delete(struct site *site, struct site_file *item)
58 {
59     site_stats_decrease(item, site);
60     site_stats_update(site);
61     if (item->prev) {
62 	/* Not first in list */
63 	item->prev->next = item->next;
64     } else {
65 	/* Not last in list */
66 	site->files = item->next;
67     }
68     if (item->next) {
69 	/* Not last in list */
70 	item->next->prev = item->prev;
71     } else {
72 	/* Last in list */
73 	site->files_tail = item->prev;
74     }
75 
76     /* Now really destroy the file */
77     file_state_destroy(&item->local);
78     file_state_destroy(&item->stored);
79     file_state_destroy(&item->server);
80     free(item);
81 }
82 
83 
84 /* Inserts a file into the files list, position chosen by type.  Must
85  * be in critical section on calling. file_set_* ensure this. */
file_insert(enum file_type type,struct site * site)86 static struct site_file *file_insert(enum file_type type, struct site *site)
87 {
88     struct site_file *file;
89     file = ne_calloc(sizeof(struct site_file));
90     if (site->files == NULL) {
91         /* Empty list */
92         site->files = file;
93         site->files_tail = file;
94     } else if (type == file_dir) {
95         /* Append file */
96         site->files_tail->next = file;
97         file->prev = site->files_tail;
98         site->files_tail = file;
99     } else {
100         /* Prepend file */
101         site->files->prev = file;
102         file->next = site->files;
103         site->files = file;
104     }
105     return file;
106 }
107 
108 #define FS_ALPHA(f) ((struct file_state *) (((char *)(f)) + alpha_off))
109 #define FS_BETA(f) ((struct file_state *) (((char *)(f)) + beta_off))
110 
111 /* file_set implementation, used to update the files list.
112  *
113  * file_set takes a file type 'type', a file state 'state', the site,
114  * two structure offsets, alpha_off and beta_off, and a default diff
115  * type, 'default_diff'.  alpha_off represents the offset into a
116  * site_file structure for the state which 'state' represents;
117  * beta_off represents the offset into the structure for the state
118  * against which this state should be compared.
119  */
file_set(enum file_type type,struct file_state * state,struct site * site,size_t alpha_off,size_t beta_off,enum file_diff default_diff)120 static struct site_file *file_set(enum file_type type, struct file_state *state,
121                                   struct site *site,
122                                   size_t alpha_off, size_t beta_off,
123                                   enum file_diff default_diff)
124 {
125     struct site_file *file, *direct = NULL, *moved = NULL, *frename = NULL;
126     enum file_diff dir_diff;
127     char *bname = NULL; /* init to shut up gcc */
128 
129     if (site->checkmoved && type == file_file) {
130         bname = base_name(state->filename);
131     }
132 
133     for (file = site->files; file; file = file->next) {
134         struct file_state *beta = FS_BETA(file);
135 
136         if (beta->exists && direct == NULL
137             && file->type == type
138             && strcmp(beta->filename, state->filename) == 0) {
139             /* Direct match found! */
140             NE_DEBUG(DEBUG_FILES, "Direct match found.\n");
141             direct = file;
142         }
143         /* If this is not a direct match, check for a move/rename candidate,
144          * unless the file already has a complete state and diff is unchanged. */
145         else if (site->checkmoved
146                  && type == file_file && file->type == file_file
147                  && file->diff != file_unchanged
148                  && file_compare(file_file, state,
149                                  beta, site) == file_moved) {
150             /* TODO: There is a slight fuzz here - if checkrenames is true,
151              * we'll always match the first 'direct move' candidate as a
152              * 'rename move'. This shouldn't matter, since we prefer
153              * the move to the rename in the single candidate case,
154              * and in the multiple candidate case. */
155             if (!moved
156                 && strcmp(bname, base_name(beta->filename)) == 0) {
157                 NE_DEBUG(DEBUG_FILES, "Move candidate: %s\n",
158                          beta->filename);
159                 moved = file;
160             } else if (site->checkrenames && frename == NULL) {
161                 NE_DEBUG(DEBUG_FILES, "Rename move candidate: %s\n",
162                          beta->filename);
163                 frename = file;
164             }
165         }
166 
167         /* If all candidates are found, stop looking. */
168         if (direct && moved && frename) {
169             break;
170         }
171     }
172     NE_DEBUG(DEBUG_FILES, "Found: %s-%s-%s\n",
173           direct?"direct":"", moved?"moved":"", frename?"rename":"");
174     /* We prefer a direct move to a rename */
175     if (moved == NULL) moved = frename;
176     if (direct != NULL) {
177         dir_diff = file_compare(type, state, FS_BETA(direct), site);
178         NE_DEBUG(DEBUG_FILES, "Direct compare: %s\n",
179               DEBUG_GIVE_DIFF(dir_diff));
180     } else {
181         dir_diff = default_diff;
182     }
183 
184     /* We prefer a move to a CHANGED direct match. */
185     if ((direct == NULL && moved == NULL)
186         || (direct != NULL && direct->diff == file_moved
187             && moved == NULL && dir_diff != file_unchanged)) {
188         NE_DEBUG(DEBUG_FILES, "Creating new file.\n");
189         file = file_insert(type, site);
190         file->type = type;
191         file->diff = default_diff;
192         if (type == file_file) {
193             file->ignore = file_isignored(state->filename, site);
194         }
195     } else {
196         /* Overwrite file case...
197          * Again, we still prefer a move to a direct match */
198         if (moved != NULL && dir_diff != file_unchanged) {
199             NE_DEBUG(DEBUG_FILES, "Using moved file.\n");
200             file = moved;
201             site_stats_decrease(file, site);
202             file->diff = file_moved;
203         } else {
204             NE_DEBUG(DEBUG_FILES, "Using direct match.\n");
205             file = direct;
206             site_stats_decrease(file, site);
207             file->diff = dir_diff;
208         }
209 
210         if (FS_ALPHA(file)->exists) {
211             /* SHOVE! */
212             struct site_file *other;
213             NE_DEBUG(DEBUG_FILES, "Shoving file:\n");
214             other = file_insert(file->type, site);
215             other->type = file->type;
216             other->diff = default_diff;
217             other->ignore = file->ignore;
218             /* Copy over the stored state for the moved file. */
219             memcpy(FS_ALPHA(other), FS_ALPHA(file), sizeof(struct file_state));
220             DEBUG_DUMP_FILE_PROPS(DEBUG_FILES, file, site);
221             site_stats_increase(other, site);
222         }
223     }
224 
225     /* Finish up - write over the new state */
226     memcpy(FS_ALPHA(file), state, sizeof(struct file_state));
227 
228     /* And update the stats */
229     site_stats_increase(file, site);
230     site_stats_update(site);
231 
232     return file;
233 }
234 
235 #ifndef offsetof
236 #define offsetof(t, m) ((size_t) (((char *)&(((t *)NULL)->m)) - (char *)NULL))
237 #endif
238 
file_set_local(enum file_type type,struct file_state * state,struct site * site)239 struct site_file *file_set_local(enum file_type type, struct file_state *state,
240                                  struct site *site)
241 {
242     return file_set(type, state, site,
243                     offsetof(struct site_file, local),
244                     offsetof(struct site_file, stored),
245                     file_new);
246 }
247 
file_set_stored(enum file_type type,struct file_state * state,struct site * site)248 struct site_file *file_set_stored(enum file_type type, struct file_state *state,
249                                   struct site *site)
250 {
251     return file_set(type, state, site,
252                     offsetof(struct site_file, stored),
253                     offsetof(struct site_file, local),
254                     file_deleted);
255 }
256 
257 /* Prepends  an item to the fnlist. Returns the item. */
fnlist_prepend(struct fnlist ** list)258 struct fnlist *fnlist_prepend(struct fnlist **list)
259 {
260     struct fnlist *item = ne_malloc(sizeof(struct fnlist));
261     item->next = *list;
262     item->prev = NULL;
263     if (*list != NULL) {
264 	(*list)->prev = item;
265     }
266     *list = item;
267     return item;
268 }
269 
270 /* Returns a deep copy of the given fnlist */
fnlist_deep_copy(const struct fnlist * src)271 struct fnlist *fnlist_deep_copy(const struct fnlist *src)
272 {
273     const struct fnlist *iter;
274     struct fnlist *dest = NULL, *prev = NULL, *item = NULL;
275     for (iter = src; iter != NULL; iter = iter->next) {
276 	item = ne_malloc(sizeof(struct fnlist));
277 	item->pattern = ne_strdup(iter->pattern);
278 	item->haspath = iter->haspath;
279 	if (prev != NULL) {
280 	    prev->next = item;
281 	} else {
282 	    /* First item in list */
283 	    dest = item;
284 	}
285 	item->prev = prev;
286 	item->next = NULL;
287 	prev = item;
288     }
289     return dest;
290 }
291 
292 /* Performs fnmatch() of all the strings in the given string list again
293  * the given filename. Returns true if a pattern matches, else false. */
fnlist_match(const char * filename,const struct fnlist * list)294 inline int fnlist_match(const char *filename, const struct fnlist *list)
295 {
296     const struct fnlist *item;
297     const char *bname = base_name(filename);
298 
299     for (item=list; item != NULL; item=item->next) {
300 	NE_DEBUG(DEBUG_FILES, "%s ", item->pattern);
301 	if (item->haspath) {
302 	    if (fnmatch(item->pattern, filename,
303 			 FNM_PATHNAME | FNM_LEADING_DIR) == 0)
304 		break;
305 	} else {
306 	    if (fnmatch(item->pattern, bname, 0) == 0)
307 		break;
308 	}
309     }
310 
311 #ifdef DEBUGGING
312     if (item) {
313 	NE_DEBUG(DEBUG_FILES, "- matched.\n");
314     } else if (list) {
315 	NE_DEBUG(DEBUG_FILES, "\n");
316     } else {
317 	NE_DEBUG(DEBUG_FILES, "(none)\n");
318     }
319 #endif /* DEBUGGING */
320 
321     return (item!=NULL);
322 }
323 
324 /* Returns whether the given filename is excluded from the
325  * given site */
file_isexcluded(const char * filename,struct site * site)326 int file_isexcluded(const char *filename, struct site *site)
327 {
328     NE_DEBUG(DEBUG_FILES, "Matching excludes for %s:\n", filename);
329     return fnlist_match(filename, site->excludes);
330 }
331 
file_isignored(const char * filename,struct site * site)332 int file_isignored(const char *filename, struct site *site)
333 {
334     NE_DEBUG(DEBUG_FILES, "Matching ignores for %s:\n", filename);
335     return fnlist_match(filename, site->ignores);
336 }
337 
file_isascii(const char * filename,struct site * site)338 int file_isascii(const char *filename, struct site *site)
339 {
340     NE_DEBUG(DEBUG_FILES, "Matching asciis for %s:\n", filename);
341     return fnlist_match(filename, site->asciis);
342 }
343 
site_stats_update(struct site * site)344 void site_stats_update(struct site *site)
345 {
346     NE_DEBUG(DEBUG_FILES,
347 	  "Stats: moved=%d new=%d %sdeleted=%d%s changed=%d"
348 	  " ignored=%d unchanged=%d\n",
349 	  site->nummoved, site->numnew, site->nodelete?"[":"",
350 	  site->numdeleted, site->nodelete?"]":"", site->numchanged,
351 	  site->numignored, site->numunchanged);
352     site->remote_is_different = (site->nummoved + site->numnew +
353 				 (site->nodelete?0:site->numdeleted) +
354 				 site->numchanged) > 0;
355     site->local_is_different = (site->nummoved + site->numnew +
356 				site->numdeleted + site->numchanged +
357 				site->numignored) > 0;
358     NE_DEBUG(DEBUG_FILES, "Remote: %s  Local: %s\n",
359 	  site->remote_is_different?"yes":"no",
360 	  site->local_is_different?"yes":"no");
361 }
362 
file_set_diff(struct site_file * file,struct site * site)363 void file_set_diff(struct site_file *file, struct site *site)
364 {
365     site_stats_decrease(file, site);
366     file->diff = file_compare(file->type, &file->local, &file->stored, site);
367     site_stats_increase(file, site);
368     site_stats_update(site);
369 }
370 
file_state_copy(struct file_state * dest,const struct file_state * src,struct site * site)371 void file_state_copy(struct file_state *dest, const struct file_state *src,
372                      struct site *site)
373 {
374     file_state_destroy(dest);
375     memcpy(dest, src, sizeof(struct file_state));
376     if (src->linktarget != NULL) {
377 	dest->linktarget = ne_strdup(src->linktarget);
378     }
379     if (src->filename != NULL) {
380 	dest->filename = ne_strdup(src->filename);
381     }
382 }
383 
file_state_destroy(struct file_state * state)384 void file_state_destroy(struct file_state *state)
385 {
386     if (state->linktarget != NULL) {
387 	free(state->linktarget);
388 	state->linktarget = NULL;
389     }
390     if (state->filename != NULL) {
391 	free(state->filename);
392 	state->filename = NULL;
393     }
394 }
395 
396 /* Checksum the file.
397  * We pass the site atm, since it's likely we will add different
398  * methods of checksumming later on, with better-faster-happier
399  * algorithms.
400  * Returns:
401  *  0 on success
402  *  non-zero on error (e.g., couldn't open file)
403  */
file_checksum(const char * fname,struct file_state * state,struct site * s)404 int file_checksum(const char *fname, struct file_state *state, struct site *s)
405 {
406     int ret;
407     FILE *f;
408     f = fopen(fname, "r" FOPEN_BINARY_FLAGS);
409     if (f == NULL) {
410 	return -1;
411     }
412     ret = ne_md5_stream(f, state->checksum);
413     fclose(f); /* worth checking return value? */
414 #ifdef DEBUGGING
415     {
416 	char tmp[33] = {0};
417 	ne_md5_to_ascii(state->checksum, tmp);
418 	NE_DEBUG(DEBUG_FILES, "Checksum: %s = [%32s]\n", fname, tmp);
419     }
420 #endif /* DEBUGGING */
421     return ret;
422 }
423 
file_full_remote(struct file_state * state,struct site * site)424 char *file_full_remote(struct file_state *state, struct site *site)
425 {
426     char *ret;
427     ret = ne_malloc(strlen(site->remote_root) + strlen(state->filename) + 1);
428     strcpy(ret, site->remote_root);
429     if (site->lowercase) {
430 	int n, off, len;
431 	/* Write the remote filename in lower case */
432 	off = strlen(site->remote_root);
433 	len = strlen(state->filename) + 1; /* +1 for \0 */
434 	for (n = 0; n < len; n++) {
435 	    ret[off+n] = tolower(state->filename[n]);
436 	}
437     } else {
438 	strcat(ret, state->filename);
439     }
440     return ret;
441 }
442 
file_full_local(struct file_state * state,struct site * site)443 char *file_full_local(struct file_state *state, struct site *site)
444 {
445     return ne_concat(site->local_root, state->filename, NULL);
446 }
447 
file_name(const struct site_file * file)448 char *file_name(const struct site_file *file)
449 {
450     if (file->diff == file_deleted) {
451 	return file->stored.filename;
452     } else {
453 	return file->local.filename;
454     }
455 }
456 
457 /* Returns whether the file "contents" have changed or not.
458  * TODO: better name needed. */
file_contents_changed(struct site_file * file,struct site * site)459 int file_contents_changed(struct site_file *file, struct site *site)
460 {
461     int ret = false;
462     if (site->state_method == state_checksum) {
463 	if (memcmp(file->stored.checksum, file->local.checksum, 16))
464 	    ret = true;
465     } else {
466 	if (file->stored.size != file->local.size ||
467 	    file->stored.time != file->local.time)
468 	    ret = true;
469     }
470     if (file->stored.ascii != file->local.ascii)
471 	ret = true;
472     return ret;
473 }
474 
475 /* Return true if the permission of the given file need changing.  */
file_perms_changed(struct site_file * file,struct site * site)476 int file_perms_changed(struct site_file *file, struct site *site)
477 {
478     /* Slightly obscure boolean here...
479      *  If ('permissions all' OR ('permissions exec' and file is chmod u+x)
480      *     AND
481      *     EITHER (in tempupload mode or nooverwrite mode)
482      *         OR the stored file perms are different from the local ones
483      *         OR the file doesn't exist locally or remotely,
484      *
485      * Note that in tempupload and nooverwrite mode, we are
486      * creating a new file, so the permissions on the new file
487      * will always be "wrong".
488      */
489     if (((site->perms == sitep_all) ||
490 	 (((file->local.mode | file->stored.mode) & S_IXUSR) &&
491 	  (site->perms == sitep_exec))) &&
492 	(site->tempupload || site->nooverwrite ||
493 	 file->local.mode != file->stored.mode ||
494 	 file->local.exists != file->stored.exists)) {
495 	return true;
496     } else {
497 	return false;
498     }
499 }
500 
file_uploaded(struct site_file * file,struct site * site)501 void file_uploaded(struct site_file *file, struct site *site)
502 {
503     file->stored.size = file->local.size;
504     if (site->state_method == state_checksum) {
505 	memcpy(file->stored.checksum, file->local.checksum, 16);
506     } else {
507 	file->stored.time = file->local.time;
508     }
509     /* Now the filename */
510     if (file->stored.filename) free(file->stored.filename);
511     file->stored.filename = ne_strdup(file->local.filename);
512     file->stored.ascii = file->local.ascii;
513     file->stored.exists = file->local.exists;
514     file->stored.mode = file->local.mode;
515     /* Update the diff */
516     file_set_diff(file, site);
517 }
518 
file_downloaded(struct site_file * file,struct site * site)519 void file_downloaded(struct site_file *file, struct site *site)
520 {
521     file->local.size = file->stored.size;
522     if (site->state_method == state_checksum) {
523 	memcpy(file->local.checksum, file->stored.checksum, 16);
524     } else {
525 	file->local.time = file->stored.time;
526     }
527     /* Now the filename */
528     if (file->local.filename) free(file->local.filename);
529     file->local.filename = ne_strdup(file->stored.filename);
530     file->local.ascii = file->stored.ascii;
531     file->local.exists = file->stored.exists;
532     file->local.mode = file->stored.mode;
533     file_set_diff(file, site);
534 }
535