add m_dirname that implement a non broken libgen-like function.
[apps/madmutt.git] / lib-lib / file.c
1 /*
2  *  This program is free software; you can redistribute it and/or modify
3  *  it under the terms of the GNU General Public License as published by
4  *  the Free Software Foundation; either version 2 of the License, or (at
5  *  your option) any later version.
6  *
7  *  This program is distributed in the hope that it will be useful, but
8  *  WITHOUT ANY WARRANTY; without even the implied warranty of
9  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
10  *  General Public License for more details.
11  *
12  *  You should have received a copy of the GNU General Public License
13  *  along with this program; if not, write to the Free Software
14  *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
15  *  MA 02110-1301, USA.
16  *
17  *  Copyright © 2006 Pierre Habouzit
18  */
19 /*
20  * Copyright notice from original mutt:
21  * Copyright (C) 1996-2000 Michael R. Elkins <me@mutt.org>
22  * Copyright (C) 1999-2000 Thomas Roessler <roessler@does-not-exist.org>
23  *
24  * This file is part of mutt-ng, see http://www.muttng.org/.
25  * It's licensed under the GNU General Public License,
26  * please see the file GPL in the top level source directory.
27  */
28
29 #include "lib-lib.h"
30
31 #include <utime.h>
32
33 #ifndef O_NOFOLLOW
34 #  define O_NOFOLLOW  0
35 #endif
36
37 /* FIXME: ugly to the max */
38 extern short Umask;
39
40 static int compare_stat(struct stat *osb, struct stat *nsb)
41 {
42     if (osb->st_dev  != nsb->st_dev || osb->st_ino != nsb->st_ino
43     || osb->st_rdev != nsb->st_rdev)
44     {
45         return -1;
46     }
47
48     return 0;
49 }
50
51 /****************************************************************************/
52 /* fd ops                                                                   */
53 /****************************************************************************/
54
55 int safe_open(const char *path, int flags)
56 {
57     struct stat osb, nsb;
58     int fd;
59
60     umask(Umask);
61
62     if ((fd = open(path, flags, 0666)) < 0)
63         return fd;
64
65     /* make sure the file is not symlink */
66     if (lstat (path, &osb) < 0 || fstat(fd, &nsb) < 0
67     ||  compare_stat(&osb, &nsb) == -1)
68     {
69         close(fd);
70         return -1;
71     }
72
73     return (fd);
74 }
75
76
77 int safe_symlink(const char *oldpath, const char *newpath)
78 {
79     struct stat osb, nsb;
80
81     if (!oldpath || !newpath)
82         return -1;
83
84     if (unlink(newpath) < 0 && errno != ENOENT)
85         return -1;
86
87     if (oldpath[0] == '/') {
88         if (symlink(oldpath, newpath) < 0)
89             return -1;
90     } else {
91         char abs_oldpath[_POSIX_PATH_MAX];
92         ssize_t len;
93
94         if (!getcwd(abs_oldpath, sizeof(abs_oldpath)))
95             return -1;
96
97         len = m_strcat(abs_oldpath, sizeof(abs_oldpath), "/");
98         if (len >= ssizeof(abs_oldpath))
99             return -1;
100
101         len += m_strcpy(abs_oldpath + len, sizeof(abs_oldpath) - len, oldpath);
102         if (len >= ssizeof(abs_oldpath))
103             return -1;
104
105         if (symlink (abs_oldpath, newpath) < 0)
106             return -1;
107     }
108
109     if (stat(oldpath, &osb) < 0
110     ||  stat(newpath, &nsb) < 0
111     ||  compare_stat(&osb, &nsb) < 0)
112     {
113         unlink (newpath);
114         return -1;
115     }
116
117     return 0;
118 }
119
120 /*
121  * This function is supposed to do nfs-safe renaming of files.
122  *
123  * Warning: We don't check whether src and target are equal.
124  */
125 int safe_rename(const char *src, const char *target)
126 {
127     struct stat ssb, tsb;
128
129     if (!src || !target)
130         return -1;
131
132     if (link(src, target) != 0) {
133         /*
134          * Coda does not allow cross-directory links, but tells
135          * us it's a cross-filesystem linking attempt.
136          *
137          * However, the Coda rename call is allegedly safe to use.
138          *
139          * With other file systems, rename should just fail when 
140          * the files reside on different file systems, so it's safe
141          * to try it here.
142          *
143          */
144
145         if (errno == EXDEV)
146             return rename(src, target);
147
148         return -1;
149     }
150
151     /* Stat both links and check if they are equal. */
152
153     if (stat(src, &ssb) < 0 || stat(target, &tsb) < 0) {
154         return -1;
155     }
156
157     /* 
158      * pretend that the link failed because the target file
159      * did already exist.
160      */
161
162     if (compare_stat(&ssb, &tsb) == -1) {
163         errno = EEXIST;
164         return -1;
165     }
166
167     /*
168      * Unlink the original link.  Should we really ignore the return
169      * value here? XXX
170      */
171
172     unlink(src);
173     return 0;
174 }
175
176 void mutt_unlink(const char *s)
177 {
178     struct stat sb, sb2;
179
180     if (lstat(s, &sb) == 0 && S_ISREG(sb.st_mode)) {
181         int fd;
182         FILE *f;
183
184         /* Defend against symlink attacks */
185
186         if ((fd = open(s, O_RDWR | O_NOFOLLOW)) < 0)
187             return;
188
189         if ((fstat(fd, &sb2) != 0) || !S_ISREG(sb2.st_mode)
190         || (sb.st_dev != sb2.st_dev) || (sb.st_ino != sb2.st_ino))
191         {
192             close (fd);
193             return;
194         }
195
196         if ((f = fdopen(fd, "r+"))) {
197             char buf[BUFSIZ];
198             unlink(s);
199
200             p_clear(buf, countof(buf));
201             while (sb.st_size > 0) {
202                 fwrite(buf, 1, MIN(ssizeof(buf), sb.st_size), f);
203                 sb.st_size -= MIN(ssizeof(buf), sb.st_size);
204             }
205             m_fclose(&f);
206         }
207     }
208 }
209
210
211 /****************************************************************************/
212 /* FILE* ops                                                                */
213 /****************************************************************************/
214
215 /* when opening files for writing, make sure the file doesn't already exist
216    to avoid race conditions.                                                */
217 FILE *safe_fopen(const char *path, const char *mode)
218 {
219     /* first set the current umask */
220     umask(Umask);
221
222     if (mode[0] == 'w') {
223         int fd;
224         int flags = O_CREAT | O_EXCL | O_NOFOLLOW;
225
226         flags |= (mode[1] == '+' ? O_RDWR : O_WRONLY);
227
228         if ((fd = safe_open(path, flags)) < 0)
229             return (NULL);
230
231         return fdopen (fd, mode);
232     }
233
234     return fopen(path, mode);
235 }
236
237 /* If rfc1524_expand_command() is used on a recv'd message, then
238  * the filename doesn't exist yet, but if its used while sending a message,
239  * then we need to rename the existing file.
240  *
241  * This function returns 0 on successful move, 1 on old file doesn't exist,
242  * 2 on new file already exists, and 3 on other failure.
243  */
244
245 /* note on access(2) use: No dangling symlink problems here due to
246  * safe_fopen().
247  */
248 int mutt_rename_file(char *oldfile, char *newfile)
249 {
250     FILE *ofp, *nfp;
251
252     if (access(oldfile, F_OK) != 0)
253         return 1;
254     if (access(newfile, F_OK) == 0)
255         return 2;
256
257     ofp = fopen(oldfile, "r");
258     if (!ofp)
259         return 3;
260
261     nfp = safe_fopen(newfile, "w");
262     if (!nfp) {
263         m_fclose(&ofp);
264         return 3;
265     }
266
267     mutt_copy_stream(ofp, nfp);
268     m_fclose(&nfp);
269     m_fclose(&ofp);
270     mutt_unlink(oldfile);
271     return 0;
272 }
273
274 /* Read a line from ``fp'' into the dynamically allocated ``s'',
275  * increasing ``s'' if necessary. The ending "\n" or "\r\n" is removed.
276  * If a line ends with "\", this char and the linefeed is removed,
277  * and the next line is read too.
278  */
279 char *mutt_read_line(char *s, ssize_t *size, FILE * fp, int *line)
280 {
281     ssize_t offset = 0;
282     char *ch;
283
284     if (!s) {
285         s = p_new(char, STRING);
286         *size = STRING;
287     }
288
289     for (;;) {
290         if (fgets(s + offset, *size - offset, fp) == NULL) {
291             p_delete(&s);
292             return NULL;
293         }
294         if ((ch = strchr(s + offset, '\n')) != NULL) {
295             (*line)++;
296             *ch = 0;
297             if (ch > s && *(ch - 1) == '\r')
298                 *--ch = 0;
299             if (ch == s || *(ch - 1) != '\\')
300                 return s;
301             offset = ch - s - 1;
302         } else {
303             int c;
304
305             c = getc (fp);            /* This is kind of a hack. We want to know if the
306                                          char at the current point in the input stream is EOF.
307                                          feof() will only tell us if we've already hit EOF, not
308                                          if the next character is EOF. So, we need to read in
309                                          the next character and manually check if it is EOF. */
310             if (c == EOF) {
311                 /* The last line of fp isn't \n terminated */
312                 (*line)++;
313                 return s;
314             } else {
315                 ungetc (c, fp);         /* undo our dammage */
316                 /* There wasn't room for the line -- increase ``s'' */
317                 offset = *size - 1;     /* overwrite the terminating 0 */
318                 *size += STRING;
319                 p_realloc(&s, *size);
320             }
321         }
322     }
323 }
324
325 int mutt_copy_stream(FILE *fin, FILE *fout)
326 {
327     char buf[BUFSIZ];
328     size_t l;
329
330     while ((l = fread(buf, 1, sizeof(buf), fin)) > 0) {
331         if (fwrite(buf, 1, l, fout) != l)
332             return -1;
333     }
334
335     return 0;
336 }
337
338 int mutt_copy_bytes(FILE *in, FILE *out, ssize_t size)
339 {
340     char buf[BUFSIZ];
341
342     while (size > 0) {
343         size_t chunk = MIN(size, ssizeof(buf));
344
345         if ((chunk = fread(buf, 1, chunk, in)) < 1)
346             break;
347         if (fwrite(buf, 1, chunk, out) != chunk) {
348             return -1;
349         }
350         size -= chunk;
351     }
352
353     return 0;
354 }
355
356
357 /****************************************************************************/
358 /* path manipulations                                                       */
359 /****************************************************************************/
360
361 const char *mutt_basename(const char *f)
362 {
363     const char *p = strrchr(f, '/');
364     return p ? p + 1 : f;
365 }
366
367 ssize_t m_dirname(char *dst, ssize_t dlen, const char *path)
368 {
369     int plen = m_strlen(path);
370
371     while (plen > 0 && path[plen - 1] == '/')
372         plen--;
373
374     while (plen > 0 && path[plen - 1] != '/')
375         plen--;
376
377     while (plen > 0 && path[plen - 1] == '/')
378         plen--;
379
380     if (plen)
381         return m_strncpy(dst, dlen, path, plen);
382
383     if (*path == '/')
384         return m_strcpy(dst, dlen, "/");
385     return m_strcpy(dst, dlen, ".");
386 }
387
388 char *
389 mutt_concat_path(char *d, ssize_t n, const char *dir, const char *fname)
390 {
391     int pos;
392
393     pos = m_strcpy(d, n, dir);
394     if (pos >= n - 1)
395         return d;
396
397     if (pos && d[pos - 1] != '/')
398         d[pos++] = '/';
399
400     m_strcpy(d + pos, n - pos, fname);
401     return d;
402 }
403
404
405 void mutt_sanitize_filename(char *s, short slash)
406 {
407     static char safe_chars[] =
408         "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+@{}._-:%/";
409
410     if (!s)
411         return;
412
413     for (; *s; s++) {
414         if ((slash && *s == '/') || !strchr (safe_chars, *s))
415             *s = '_';
416     }
417 }
418
419 /* prepare a file name to survive the shell's quoting rules.
420  * From the Unix programming FAQ by way of Liviu.
421  */
422
423 /* FIXME: API is very wrong */
424 ssize_t mutt_quote_filename(char *d, ssize_t l, const char *s)
425 {
426     ssize_t i, j = 0;
427
428     if (!s) {
429         *d = '\0';
430         return 0;
431     }
432
433     /* leave some space for the trailing characters. */
434     l -= 6;
435
436     d[j++] = '\'';
437
438     for (i = 0; j < l && s[i]; i++) {
439         if (s[i] == '\'' || s[i] == '`') {
440             d[j++] = '\'';
441             d[j++] = '\\';
442             d[j++] = s[i];
443             d[j++] = '\'';
444         } else {
445             d[j++] = s[i];
446         }
447     }
448
449     d[j++] = '\'';
450     d[j] = '\0';
451
452     return j;
453 }
454
455 ssize_t m_file_fmt(char *dst, ssize_t n, const char *fmt, const char *src)
456 {
457     ssize_t pos = 0;
458     const char *p;
459     int hadfname = 0;
460
461     for (p = strchr(fmt, '%'); p; p = strchr(fmt, '%')) {
462         if (p[1] == 's') {
463             pos += m_strncpy(dst + pos, n - pos, fmt, p - fmt);
464             pos += m_strcpy(dst + pos, n - pos, src);
465             fmt  = p + 2;
466             hadfname = 1;
467         } else {
468             pos += m_strncpy(dst + pos, n - pos, fmt, p + 1 - fmt);
469             fmt = p + 1 + (p[1] == '%');
470         }
471     }
472     pos += m_strcpy(dst + pos, n - pos, fmt);
473
474     if (!hadfname)
475         pos += snprintf(dst + pos, n - pos, " %s", src);
476
477     return pos;
478 }
479
480 ssize_t
481 m_quotefile_fmt(char *dst, ssize_t n, const char *fmt, const char *src)
482 {
483     char tmp[LONG_STRING];
484     mutt_quote_filename(tmp, sizeof(tmp), src);
485     return m_file_fmt(dst, n, fmt, tmp);
486 }
487
488 static ssize_t
489 m_tempftplize(char *dst, ssize_t dlen, const char *fmt, const char *s)
490 {
491     const char *p;
492
493     while ((p = strchr(fmt, '/'))) {
494         fmt = p + 1;
495     }
496
497     if (!*fmt)
498         return m_strcpy(dst, dlen, s);
499
500     for (p = strchr(fmt, '%'); p; p = strchr(p, '%')) {
501         if (p[1] == 's')
502             return m_file_fmt(dst, dlen, fmt, s);
503
504         p += 1 + (p[1] == '%');
505     }
506
507     p = strrchr(fmt, '.');
508     if (p) {
509         return snprintf(dst, dlen, "%s%s", s, p);
510     } else {
511         return snprintf(dst, dlen, "%s.%s", s, fmt);
512     }
513 }
514
515 int m_tempfd(char *dst, ssize_t n, const char *dir, const char *fmt)
516 {
517     char raw[_POSIX_PATH_MAX], tpl[_POSIX_PATH_MAX];
518     const char *path = fmt ? tpl : raw;
519     int fd;
520
521   rand_again:
522     snprintf(raw, sizeof(raw), "%s/madmutt-%04x-%04x-%08x",
523              dir, (int)getuid(), (int)getpid(), (int)rand());
524
525     if (fmt) {
526         m_tempftplize(tpl, sizeof(tpl), fmt, raw);
527     }
528
529     fd = open(path, O_CREAT | O_EXCL | O_RDWR | O_NOFOLLOW, 0600);
530
531     if (fd < 0) {
532         if (errno == EEXIST)
533             goto rand_again;
534         return -1;
535     }
536
537     m_strcpy(dst, n, path);
538     return fd;
539 }
540
541 FILE *m_tempfile(char *dst, ssize_t n, const char *dir, const char *fmt)
542 {
543     int fd = m_tempfd(dst, n, dir, fmt);
544     return fd < 0 ? NULL : fdopen(fd, "w+");
545 }
546
547 /****************************************************************************/
548 /* misc                                                                     */
549 /****************************************************************************/
550
551 /* Decrease a file's modification time by 1 second */
552 time_t m_decrease_mtime(const char *path, struct stat *st)
553 {
554     struct utimbuf utim;
555     struct stat _st;
556     time_t mtime;
557
558     if (!st) {
559         if (stat(path, &_st) == -1)
560             return -1;
561         st = &_st;
562     }
563
564     if ((mtime = st->st_mtime) == time(NULL)) {
565         mtime -= 1;
566         utim.actime = mtime;
567         utim.modtime = mtime;
568         utime(path, &utim);
569     }
570
571     return mtime;
572 }
573