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