2007-02-08 Marcus Brinkmann <marcus@g10code.de>
[oftpd.git] / src / file_list.c
1 /*
2  * $Id$
3
4  * [wk: code reviewed 2003-08-09]
5  */
6
7 #include <config.h>
8 #include <stdio.h>
9 #include <limits.h>
10 #include <string.h>
11 #include <sys/stat.h>
12 #include <unistd.h>
13 #include <glob.h>
14 #include <stdlib.h>
15 #include <stdarg.h>
16 #include <ctype.h>
17 #include <pthread.h>
18
19 #if TIME_WITH_SYS_TIME
20 # include <sys/time.h>
21 # include <time.h>
22 #else
23 # if HAVE_SYS_TIME_H
24 #  include <sys/time.h>
25 # else
26 #  include <time.h>
27 # endif
28 #endif
29
30 #include "file_list.h"
31 #include "daemon_assert.h"
32
33 /* AIX requires this to be the first thing in the file.  */
34 #ifndef __GNUC__
35 # if HAVE_ALLOCA_H
36 #  include <alloca.h>
37 # else
38 #  ifdef _AIX
39 #pragma alloca
40 #  else
41 #   ifndef alloca /* predefined by HP cc +Olibcalls */
42 char *alloca ();
43 #   endif
44 #  endif
45 # endif
46 #endif
47
48 /* GLOB_PERIOD is defined in Linux but not Solaris */
49 #ifndef GLOB_PERIOD
50 #define GLOB_PERIOD 0
51 #endif /* GLOB_PERIOD */
52
53 /* GLOB_ABORTED is defined in Linux but not on FreeBSD */
54 #ifndef GLOB_ABORTED
55 #ifdef GLOB_ABEND
56 #define GLOB_ABORTED GLOB_ABEND
57 #endif
58 #endif
59
60 static int is_valid_dir(const char *dir);
61 static void fdprintf(int fd, const char *fmt, ...);
62 static const char *skip_ls_options(const char *filespec);
63
64 /* if no localtime_r() is available, provide one */
65 #ifndef HAVE_LOCALTIME_R
66
67 struct tm *localtime_r(const time_t *timep, struct tm *timeptr) 
68 {
69     static pthread_mutex_t time_lock = PTHREAD_MUTEX_INITIALIZER;
70
71     pthread_mutex_lock(&time_lock);
72     *timeptr = *(localtime(timep));
73     pthread_mutex_unlock(&time_lock);
74     return timeptr;
75 }
76 #endif /* HAVE_LOCALTIME_R */
77
78 static void file_nlst_cleanup(glob_t *glob_buf)
79 {
80   globfree (glob_buf);
81 }
82
83 int file_nlst(int out, const char *cur_dir, const char *filespec)
84 {
85     int dir_len;
86     char pattern[PATH_MAX+1];
87     int glob_ret;
88     glob_t glob_buf;
89     int i;
90     char *file_name;
91
92     daemon_assert(out >= 0);
93     daemon_assert(is_valid_dir(cur_dir));
94     daemon_assert(filespec != NULL);
95
96     if (filespec[0] == '/') {
97         cur_dir = "";
98         dir_len = 0;
99     } else {
100         dir_len = strlen(cur_dir);
101         if ((dir_len + 1) > PATH_MAX) {
102             fdprintf(out, "Error; Directory name too long\r\n");
103             return 0;
104         }
105         strcpy(pattern, cur_dir);
106         if ((cur_dir[0] != '/') || (cur_dir[1] != '\0')) {
107             strcat(pattern, "/");
108             dir_len++;
109         }
110     }
111
112     /* FIXME: Stat the directory to see whether something is hidden.  */
113
114     /* make sure we have enough space */
115     if ((dir_len + 1 + strlen(filespec)) > PATH_MAX) {
116         fdprintf(out, "Error; Path name too long\r\n");
117         return 0;
118     }
119     strcat(pattern, filespec);
120     if ( !strncmp (pattern, "/dev", 4)
121          && (!pattern[4] || pattern[4] == '/'))
122       return 1; /* ignore /dev */
123
124     /* do a glob() */
125     memset(&glob_buf, 0, sizeof(glob_buf));
126     glob_ret = glob(pattern, 
127                     GLOB_ERR | GLOB_NOSORT | GLOB_PERIOD, 
128                     NULL, 
129                     &glob_buf);
130     if (glob_ret == GLOB_NOSPACE) {
131         fdprintf(out, "Error; Out of memory\r\n");
132         return 0;
133 #ifdef GLOB_NOMATCH  /* not present in FreeBSD */
134     } else if (glob_ret == GLOB_NOMATCH) {
135         return 1;
136 #endif /* GLOB_NOMATCH */
137 #ifdef GLOB_ABORTED  /* not present in older gcc */
138     } else if (glob_ret == GLOB_ABORTED) {
139         fdprintf(out, "Error; Read error\r\n");
140         return 0;
141 #endif /*GLOB_ABORTED*/
142     } else if (glob_ret != 0) {
143         fdprintf(out, "Error; Unknown glob() error %d\r\n", glob_ret);
144         return 0;
145     }
146
147     pthread_cleanup_push ((void (*)())file_nlst_cleanup, &glob_buf);
148
149     /* print our results */
150     for (i=0; i<glob_buf.gl_pathc; i++) {
151         file_name = glob_buf.gl_pathv[i];
152         if ( !strncmp (file_name, "/dev", 4)
153              && (!file_name[4] || file_name[4] == '/'))
154           continue; /* ignore /dev */
155         if (memcmp(file_name, pattern, dir_len) == 0) {
156           file_name += dir_len;
157         }
158         fdprintf(out, "%s\r\n", file_name);
159     }
160
161     /* This frees glob_buf.  */
162     pthread_cleanup_pop(1);
163
164     return 1;
165 }
166
167 typedef struct {
168     char *name;
169     char *full_path;
170     struct stat stat;
171 } file_info_t;
172
173 struct file_list_cleanup_s
174 {
175   void *file_info;
176   glob_t *glob_buf;
177 };
178
179 static void file_list_cleanup(struct file_list_cleanup_s *info)
180 {
181   if (info->file_info)
182     free (info->file_info);
183   if (info->glob_buf)
184     globfree (info->glob_buf);
185 }
186   
187 int file_list(int out, const char *cur_dir, const char *filespec)
188 {
189     int dir_len;
190     char pattern[PATH_MAX+1];
191     int glob_ret;
192     glob_t glob_buf;
193     int i;
194     file_info_t *file_info;
195     int num_files;
196     unsigned long total_blocks;
197     char *file_name;
198
199     mode_t mode;
200     time_t now;
201     struct tm tm_now;
202     double age;
203     char date_buf[13];
204     char link[PATH_MAX+1];
205     int link_len;
206
207     struct file_list_cleanup_s cleanup_info;
208
209     cleanup_info.file_info = NULL;
210     cleanup_info.glob_buf = NULL;
211
212     daemon_assert(out >= 0);
213     daemon_assert(is_valid_dir(cur_dir));
214     daemon_assert(filespec != NULL);
215
216     filespec = skip_ls_options(filespec);
217
218     if (filespec[0] == '/') {
219         cur_dir = "";
220         dir_len = 0;
221     } else {
222         dir_len = strlen(cur_dir);
223         if ((dir_len + 1) > PATH_MAX) {
224             fdprintf(out, "Error; Directory name too long\r\n");
225             return 0;
226         }
227         strcpy(pattern, cur_dir);
228         if ((cur_dir[0] != '/') || (cur_dir[1] != '\0')) {
229             strcat(pattern, "/");
230             dir_len++;
231         }
232     }
233
234     /* FIXME: Stat the directory to see whether something is hidden.  */
235
236     /* make sure we have enough space */
237     if ((dir_len + 1 + strlen(filespec)) > PATH_MAX) {
238         fdprintf(out, "Error; Path name too long\r\n");
239         return 0;
240     }
241     strcat(pattern, filespec);
242     if ( !strncmp (pattern, "/dev", 4)
243          && (!pattern[4] || pattern[4] == '/'))
244       return 1; /* ignore /dev */
245
246
247     /* do a glob() */
248     memset(&glob_buf, 0, sizeof(glob_buf));
249     glob_ret = glob(pattern, GLOB_ERR, NULL, &glob_buf);
250 #ifndef GLOB_NOMATCH /* FreeBSD */
251     if (glob_ret == GLOB_NOCHECK) {
252 #else
253     if (glob_ret == GLOB_NOMATCH) {
254 #endif
255         fdprintf(out, "total 0\r\n");
256         return 1;
257     } else if (glob_ret == GLOB_NOSPACE) {
258         fdprintf(out, "Error; Out of memory\r\n");
259         return 0;
260 #ifdef GLOB_ABORTED  /* Not present in older gcc. */
261     } else if (glob_ret == GLOB_ABORTED) {
262         fdprintf(out, "Error; Read error\r\n");
263         return 0;
264 #endif /*GLOB_ABORTED*/
265     } else if (glob_ret != 0) {
266         fdprintf(out, "Error; Unknown glob() error %d\r\n", glob_ret);
267         return 0;
268     }
269
270  
271     /* make a buffer to store our information */
272 #ifdef HAVE_ALLOCA
273     file_info = (file_info_t *)alloca(sizeof(file_info_t) * glob_buf.gl_pathc);
274 #else
275     file_info = (file_info_t *)malloc(sizeof(file_info_t) * glob_buf.gl_pathc);
276     cleanup_info.file_info = file_info;
277 #endif
278     if (file_info == NULL) {
279         /* Note that fdprintf is a cancellation point.  */
280         globfree(&glob_buf);
281         fdprintf(out, "Error; Out of memory\r\n");
282         return 0;
283     }
284
285     cleanup_info.glob_buf = &glob_buf;
286     pthread_cleanup_push ((void (*)())file_list_cleanup, &cleanup_info);
287
288     /* collect information */
289     num_files = 0;
290     total_blocks = 0;
291     for (i=0; i<glob_buf.gl_pathc; i++) {
292         size_t n;
293
294         file_name = glob_buf.gl_pathv[i];
295         n = strlen (file_name);
296         if (n >= 7 && !strcmp (file_name+n-7, ".hidden")) {
297             num_files = 0;
298             total_blocks = 0;
299             break;
300         }
301         if ( !strncmp (file_name, "/dev", 4)
302              && (!file_name[4] || file_name[4] == '/'))
303           continue;
304         if (memcmp(file_name, pattern, dir_len) == 0) {
305             file_name += dir_len;
306         }
307         if (lstat(glob_buf.gl_pathv[i], &file_info[num_files].stat) == 0) {
308 #ifdef AC_STRUCT_ST_BLKSIZE
309             total_blocks += file_info[num_files].stat.st_blocks;
310 #endif
311             file_info[num_files].name = file_name;
312             file_info[num_files].full_path = glob_buf.gl_pathv[i];
313             num_files++;
314         }
315     }
316
317     /* okay, we have information, now display it */
318     fdprintf(out, "total %lu\r\n", total_blocks);
319     time(&now);
320     for (i=0; i<num_files; i++) {
321
322         mode = file_info[i].stat.st_mode;
323
324         /* output file type */
325         switch (mode & S_IFMT) {
326             case S_IFSOCK:  fdprintf(out, "s"); break;
327             case S_IFLNK:   fdprintf(out, "l"); break;
328             case S_IFBLK:   fdprintf(out, "b"); break;
329             case S_IFDIR:   fdprintf(out, "d"); break;
330             case S_IFCHR:   fdprintf(out, "c"); break;
331             case S_IFIFO:   fdprintf(out, "p"); break;
332             default:        fdprintf(out, "-"); 
333         }
334
335         /* output permissions */
336         fdprintf(out, (mode & S_IRUSR) ? "r" : "-");
337         fdprintf(out, (mode & S_IWUSR) ? "w" : "-");
338         if (mode & S_ISUID) { 
339             fdprintf(out, (mode & S_IXUSR) ? "s" : "S");
340         } else {
341             fdprintf(out, (mode & S_IXUSR) ? "x" : "-");
342         }
343         fdprintf(out, (mode & S_IRGRP) ? "r" : "-");
344         fdprintf(out, (mode & S_IWGRP) ? "w" : "-");
345         if (mode & S_ISGID) { 
346             fdprintf(out, (mode & S_IXGRP) ? "s" : "S");
347         } else {
348             fdprintf(out, (mode & S_IXGRP) ? "x" : "-");
349         }
350         fdprintf(out, (mode & S_IROTH) ? "r" : "-");
351         fdprintf(out, (mode & S_IWOTH) ? "w" : "-");
352         if (mode & S_ISVTX) {
353             fdprintf(out, (mode & S_IXOTH) ? "t" : "T");
354         } else {
355             fdprintf(out, (mode & S_IXOTH) ? "x" : "-");
356         }
357
358         /* output link & ownership information */
359         fdprintf(out, " %3d %-8d %-8d ", 
360             file_info[i].stat.st_nlink, 
361             file_info[i].stat.st_uid, 
362             file_info[i].stat.st_gid);
363
364         /* output either i-node information or size */
365 #ifdef AC_STRUCT_ST_RDEV
366         if (((mode & S_IFMT) == S_IFBLK) || ((mode & S_IFMT) == S_IFCHR)) {
367             fdprintf(out, "%3d, %3d ", 
368                 (int)((file_info[i].stat.st_rdev >> 8) & 0xff),
369                 (int)(file_info[i].stat.st_rdev & 0xff));
370         } else {
371             fdprintf(out, "%8lu ", 
372                 (unsigned long)file_info[i].stat.st_size);
373         }
374 #else
375         fdprintf(out, "%8lu ", (unsigned long)file_info[i].stat.st_size);
376 #endif
377             
378         /* output date */
379         localtime_r(&file_info[i].stat.st_mtime, &tm_now);
380         age = difftime(now, file_info[i].stat.st_mtime);
381         if ((age > 60 * 60 * 24 * 30 * 6) || (age < -(60 * 60 * 24 * 30 * 6))) {
382             strftime(date_buf, sizeof(date_buf), "%b %e  %Y", &tm_now);
383         } else {
384             strftime(date_buf, sizeof(date_buf), "%b %e %H:%M", &tm_now);
385         }
386         fdprintf(out, "%s ", date_buf);
387
388         /* output filename */
389         fdprintf(out, "%s", file_info[i].name);
390   
391         /* display symbolic link information */
392         if ((mode & S_IFMT) == S_IFLNK) {
393             link_len = readlink(file_info[i].full_path, link, sizeof(link));
394             if (link_len > 0) {
395                 fdprintf(out, " -> ");
396                 link[link_len] = '\0';
397                 fdprintf(out, "%s", link);
398             }
399         }
400
401         /* advance to next line */
402         fdprintf(out, "\r\n");
403     }
404
405     /* This frees file_info (if necessary) and glob_buf.  */
406     pthread_cleanup_pop (1);
407
408     return 1;
409 }
410
411 static int is_valid_dir(const char *dir)
412 {
413     /* directory can not be NULL (of course) */
414     if (dir == NULL) {
415         return 0;
416     }
417  
418     /* directory must be absolute (i.e. start with '/') */
419     if (dir[0] != '/') {
420         return 0;
421     }
422
423     /* length cannot be greater than PATH_MAX */
424     if (strlen(dir) > PATH_MAX) {
425         return 0;
426     }
427
428     /* assume okay */
429     return 1;
430 }
431
432 static void fdprintf(int fd, const char *fmt, ...)
433 {
434     char buf[PATH_MAX+3];
435     int buflen;
436     va_list ap;
437     int amt_written;
438     int write_ret;
439     int state;
440
441     daemon_assert(fd >= 0);
442     daemon_assert(fmt != NULL);
443
444     va_start(ap, fmt);
445     buflen = vsnprintf(buf, sizeof(buf)-1, fmt, ap);
446     va_end(ap);
447     if (buflen <= 0) {
448         return;
449     }
450     if (buflen >= sizeof(buf)) {
451         buflen = sizeof(buf)-1;
452     }
453     buf[buflen] = 0; /* Make extra sure the string is terminated. */
454
455     pthread_setcancelstate (PTHREAD_CANCEL_ENABLE, &state);
456     amt_written = 0;
457     while (amt_written < buflen) {
458         write_ret = write(fd, buf+amt_written, buflen-amt_written);
459         if (write_ret <= 0) {
460             break;
461         }
462         amt_written += write_ret;
463     }
464     pthread_setcancelstate (state, NULL);
465 }
466
467
468 /* 
469   hack workaround clients like Midnight Commander that send:
470       LIST -al /dirname 
471 */
472 static const char *
473 skip_ls_options(const char *filespec)
474 {
475     daemon_assert(filespec != NULL);
476
477     for (;;) {
478         /* end when we've passed all options */
479         if (*filespec != '-') {
480             break;
481         }
482         filespec++;
483
484         /* if we find "--", eat it and any following whitespace then return */
485         if ((filespec[0] == '-') && (filespec[1] == ' ')) {
486             filespec += 2;
487             while (isspace(*filespec)) {
488                 filespec++;
489             }
490             break;
491         }
492
493         /* otherwise, skip this option */
494         while ((*filespec != '\0') && !isspace(*filespec)) {
495             filespec++;
496         }
497
498         /* and skip any whitespace */
499         while (isspace(*filespec)) {
500             filespec++;
501         }
502     }
503
504     daemon_assert(filespec != NULL);
505
506     return filespec;
507 }
508