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