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