1 /**
2 * @file
3 * File management functions
4 *
5 * @authors
6 * Copyright (C) 2017 Richard Russon <rich@flatcap.org>
7 *
8 * @copyright
9 * This program is free software: you can redistribute it and/or modify it under
10 * the terms of the GNU General Public License as published by the Free Software
11 * Foundation, either version 2 of the License, or (at your option) any later
12 * version.
13 *
14 * This program is distributed in the hope that it will be useful, but WITHOUT
15 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
16 * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
17 * details.
18 *
19 * You should have received a copy of the GNU General Public License along with
20 * this program. If not, see <http://www.gnu.org/licenses/>.
21 */
22
23 /**
24 * @page mutt_file File management functions
25 *
26 * Commonly used file/dir management routines.
27 */
28
29 #include "config.h"
30 #include <ctype.h>
31 #include <dirent.h>
32 #include <errno.h>
33 #include <fcntl.h>
34 #include <libgen.h>
35 #include <limits.h>
36 #include <stdbool.h>
37 #include <stdio.h>
38 #include <stdlib.h>
39 #include <string.h>
40 #include <sys/stat.h>
41 #include <time.h>
42 #include <unistd.h>
43 #include <utime.h>
44 #include "config/lib.h"
45 #include "core/lib.h"
46 #include "file.h"
47 #include "buffer.h"
48 #include "date.h"
49 #include "logging.h"
50 #include "memory.h"
51 #include "message.h"
52 #include "path.h"
53 #include "string2.h"
54 #ifdef USE_FLOCK
55 #include <sys/file.h>
56 #endif
57
58 /* these characters must be escaped in regular expressions */
59 static const char rx_special_chars[] = "^.[$()|*+?{\\";
60
61 const char filename_safe_chars[] =
62 "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+@{}._-:%/";
63
64 #define MAX_LOCK_ATTEMPTS 5
65
66 /* This is defined in POSIX:2008 which isn't a build requirement */
67 #ifndef O_NOFOLLOW
68 #define O_NOFOLLOW 0
69 #endif
70
71 /**
72 * compare_stat - Compare the struct stat's of two files/dirs
73 * @param st_old struct stat of the first file/dir
74 * @param st_new struct stat of the second file/dir
75 * @retval true They match
76 *
77 * This compares the device id (st_dev), inode number (st_ino) and special id
78 * (st_rdev) of the files/dirs.
79 */
compare_stat(struct stat * st_old,struct stat * st_new)80 static bool compare_stat(struct stat *st_old, struct stat *st_new)
81 {
82 return (st_old->st_dev == st_new->st_dev) && (st_old->st_ino == st_new->st_ino) &&
83 (st_old->st_rdev == st_new->st_rdev);
84 }
85
86 /**
87 * mkwrapdir - Create a temporary directory next to a file name
88 * @param path Existing filename
89 * @param newfile New filename
90 * @param newdir New directory name
91 * @retval 0 Success
92 * @retval -1 Error
93 */
mkwrapdir(const char * path,struct Buffer * newfile,struct Buffer * newdir)94 static int mkwrapdir(const char *path, struct Buffer *newfile, struct Buffer *newdir)
95 {
96 const char *basename = NULL;
97 int rc = 0;
98
99 struct Buffer parent = mutt_buffer_make(PATH_MAX);
100 mutt_buffer_strcpy(&parent, NONULL(path));
101
102 char *p = strrchr(parent.data, '/');
103 if (p)
104 {
105 *p = '\0';
106 basename = p + 1;
107 }
108 else
109 {
110 mutt_buffer_strcpy(&parent, ".");
111 basename = path;
112 }
113
114 mutt_buffer_printf(newdir, "%s/%s", mutt_buffer_string(&parent), ".muttXXXXXX");
115 if (!mkdtemp(newdir->data))
116 {
117 mutt_debug(LL_DEBUG1, "mkdtemp() failed\n");
118 rc = -1;
119 goto cleanup;
120 }
121
122 mutt_buffer_printf(newfile, "%s/%s", newdir->data, NONULL(basename));
123
124 cleanup:
125 mutt_buffer_dealloc(&parent);
126 return rc;
127 }
128
129 /**
130 * put_file_in_place - Move a file into place
131 * @param path Destination path
132 * @param safe_file Current filename
133 * @param safe_dir Current directory name
134 * @retval 0 Success
135 * @retval -1 Error, see errno
136 */
put_file_in_place(const char * path,const char * safe_file,const char * safe_dir)137 static int put_file_in_place(const char *path, const char *safe_file, const char *safe_dir)
138 {
139 int rc;
140
141 rc = mutt_file_safe_rename(safe_file, path);
142 unlink(safe_file);
143 rmdir(safe_dir);
144 return rc;
145 }
146
147 /**
148 * mutt_file_fclose - Close a FILE handle (and NULL the pointer)
149 * @param[out] fp FILE handle to close
150 * @retval 0 Success
151 * @retval EOF Error, see errno
152 */
mutt_file_fclose(FILE ** fp)153 int mutt_file_fclose(FILE **fp)
154 {
155 if (!fp || !*fp)
156 return 0;
157
158 int rc = fclose(*fp);
159 *fp = NULL;
160 return rc;
161 }
162
163 /**
164 * mutt_file_fsync_close - Flush the data, before closing a file (and NULL the pointer)
165 * @param[out] fp FILE handle to close
166 * @retval 0 Success
167 * @retval EOF Error, see errno
168 */
mutt_file_fsync_close(FILE ** fp)169 int mutt_file_fsync_close(FILE **fp)
170 {
171 if (!fp || !*fp)
172 return 0;
173
174 int rc = 0;
175
176 if (fflush(*fp) || fsync(fileno(*fp)))
177 {
178 int save_errno = errno;
179 rc = -1;
180 mutt_file_fclose(fp);
181 errno = save_errno;
182 }
183 else
184 rc = mutt_file_fclose(fp);
185
186 return rc;
187 }
188
189 /**
190 * mutt_file_unlink - Delete a file, carefully
191 * @param s Filename
192 *
193 * This won't follow symlinks.
194 */
mutt_file_unlink(const char * s)195 void mutt_file_unlink(const char *s)
196 {
197 if (!s)
198 return;
199
200 struct stat st = { 0 };
201 /* Defend against symlink attacks */
202
203 const bool is_regular_file = (lstat(s, &st) == 0) && S_ISREG(st.st_mode);
204 if (!is_regular_file)
205 return;
206
207 const int fd = open(s, O_RDWR | O_NOFOLLOW);
208 if (fd < 0)
209 return;
210
211 struct stat st2 = { 0 };
212 if ((fstat(fd, &st2) != 0) || !S_ISREG(st2.st_mode) ||
213 (st.st_dev != st2.st_dev) || (st.st_ino != st2.st_ino))
214 {
215 close(fd);
216 return;
217 }
218
219 FILE *fp = fdopen(fd, "r+");
220 if (fp)
221 {
222 unlink(s);
223 char buf[2048] = { 0 };
224 while (st.st_size > 0)
225 {
226 fwrite(buf, 1, MIN(sizeof(buf), st.st_size), fp);
227 st.st_size -= MIN(sizeof(buf), st.st_size);
228 }
229 mutt_file_fclose(&fp);
230 }
231 }
232
233 /**
234 * mutt_file_copy_bytes - Copy some content from one file to another
235 * @param fp_in Source file
236 * @param fp_out Destination file
237 * @param size Maximum number of bytes to copy
238 * @retval 0 Success
239 * @retval -1 Error, see errno
240 */
mutt_file_copy_bytes(FILE * fp_in,FILE * fp_out,size_t size)241 int mutt_file_copy_bytes(FILE *fp_in, FILE *fp_out, size_t size)
242 {
243 if (!fp_in || !fp_out)
244 return -1;
245
246 while (size > 0)
247 {
248 char buf[2048];
249 size_t chunk = (size > sizeof(buf)) ? sizeof(buf) : size;
250 chunk = fread(buf, 1, chunk, fp_in);
251 if (chunk < 1)
252 break;
253 if (fwrite(buf, 1, chunk, fp_out) != chunk)
254 return -1;
255
256 size -= chunk;
257 }
258
259 if (fflush(fp_out) != 0)
260 return -1;
261 return 0;
262 }
263
264 /**
265 * mutt_file_copy_stream - Copy the contents of one file into another
266 * @param fp_in Source file
267 * @param fp_out Destination file
268 * @retval n Success, number of bytes copied
269 * @retval -1 Error, see errno
270 */
mutt_file_copy_stream(FILE * fp_in,FILE * fp_out)271 int mutt_file_copy_stream(FILE *fp_in, FILE *fp_out)
272 {
273 if (!fp_in || !fp_out)
274 return -1;
275
276 size_t total = 0;
277 size_t l;
278 char buf[1024];
279
280 while ((l = fread(buf, 1, sizeof(buf), fp_in)) > 0)
281 {
282 if (fwrite(buf, 1, l, fp_out) != l)
283 return -1;
284 total += l;
285 }
286
287 if (fflush(fp_out) != 0)
288 return -1;
289 return total;
290 }
291
292 /**
293 * mutt_file_symlink - Create a symlink
294 * @param oldpath Existing pathname
295 * @param newpath New pathname
296 * @retval 0 Success
297 * @retval -1 Error, see errno
298 */
mutt_file_symlink(const char * oldpath,const char * newpath)299 int mutt_file_symlink(const char *oldpath, const char *newpath)
300 {
301 struct stat st_old = { 0 };
302 struct stat st_new = { 0 };
303
304 if (!oldpath || !newpath)
305 return -1;
306
307 if ((unlink(newpath) == -1) && (errno != ENOENT))
308 return -1;
309
310 if (oldpath[0] == '/')
311 {
312 if (symlink(oldpath, newpath) == -1)
313 return -1;
314 }
315 else
316 {
317 struct Buffer abs_oldpath = mutt_buffer_make(PATH_MAX);
318
319 if (!mutt_path_getcwd(&abs_oldpath))
320 {
321 mutt_buffer_dealloc(&abs_oldpath);
322 return -1;
323 }
324
325 mutt_buffer_addch(&abs_oldpath, '/');
326 mutt_buffer_addstr(&abs_oldpath, oldpath);
327 if (symlink(mutt_buffer_string(&abs_oldpath), newpath) == -1)
328 {
329 mutt_buffer_dealloc(&abs_oldpath);
330 return -1;
331 }
332
333 mutt_buffer_dealloc(&abs_oldpath);
334 }
335
336 if ((stat(oldpath, &st_old) == -1) || (stat(newpath, &st_new) == -1) ||
337 !compare_stat(&st_old, &st_new))
338 {
339 unlink(newpath);
340 return -1;
341 }
342
343 return 0;
344 }
345
346 /**
347 * mutt_file_safe_rename - NFS-safe renaming of files
348 * @param src Original filename
349 * @param target New filename
350 * @retval 0 Success
351 * @retval -1 Error, see errno
352 *
353 * Warning: We don't check whether src and target are equal.
354 */
mutt_file_safe_rename(const char * src,const char * target)355 int mutt_file_safe_rename(const char *src, const char *target)
356 {
357 struct stat st_src = { 0 };
358 struct stat st_target = { 0 };
359 int link_errno;
360
361 if (!src || !target)
362 return -1;
363
364 if (link(src, target) != 0)
365 {
366 link_errno = errno;
367
368 /* It is historically documented that link can return -1 if NFS
369 * dies after creating the link. In that case, we are supposed
370 * to use stat to check if the link was created.
371 *
372 * Derek Martin notes that some implementations of link() follow a
373 * source symlink. It might be more correct to use stat() on src.
374 * I am not doing so to minimize changes in behavior: the function
375 * used lstat() further below for 20 years without issue, and I
376 * believe was never intended to be used on a src symlink. */
377 if ((lstat(src, &st_src) == 0) && (lstat(target, &st_target) == 0) &&
378 (compare_stat(&st_src, &st_target) == 0))
379 {
380 mutt_debug(LL_DEBUG1, "link (%s, %s) reported failure: %s (%d) but actually succeeded\n",
381 src, target, strerror(errno), errno);
382 goto success;
383 }
384
385 errno = link_errno;
386
387 /* Coda does not allow cross-directory links, but tells
388 * us it's a cross-filesystem linking attempt.
389 *
390 * However, the Coda rename call is allegedly safe to use.
391 *
392 * With other file systems, rename should just fail when
393 * the files reside on different file systems, so it's safe
394 * to try it here. */
395 mutt_debug(LL_DEBUG1, "link (%s, %s) failed: %s (%d)\n", src, target,
396 strerror(errno), errno);
397
398 /* FUSE may return ENOSYS. VFAT may return EPERM. FreeBSD's
399 * msdosfs may return EOPNOTSUPP. ENOTSUP can also appear. */
400 if ((errno == EXDEV) || (errno == ENOSYS) || errno == EPERM
401 #ifdef ENOTSUP
402 || errno == ENOTSUP
403 #endif
404 #ifdef EOPNOTSUPP
405 || errno == EOPNOTSUPP
406 #endif
407 )
408 {
409 mutt_debug(LL_DEBUG1, "trying rename\n");
410 if (rename(src, target) == -1)
411 {
412 mutt_debug(LL_DEBUG1, "rename (%s, %s) failed: %s (%d)\n", src, target,
413 strerror(errno), errno);
414 return -1;
415 }
416 mutt_debug(LL_DEBUG1, "rename succeeded\n");
417
418 return 0;
419 }
420
421 return -1;
422 }
423
424 /* Remove the compare_stat() check, because it causes problems with maildir
425 * on filesystems that don't properly support hard links, such as sshfs. The
426 * filesystem creates the link, but the resulting file is given a different
427 * inode number by the sshfs layer. This results in an infinite loop
428 * creating links. */
429 #if 0
430 /* Stat both links and check if they are equal. */
431 if (lstat(src, &st_src) == -1)
432 {
433 mutt_debug(LL_DEBUG1, "#1 can't stat %s: %s (%d)\n", src, strerror(errno), errno);
434 return -1;
435 }
436
437 if (lstat(target, &st_target) == -1)
438 {
439 mutt_debug(LL_DEBUG1, "#2 can't stat %s: %s (%d)\n", src, strerror(errno), errno);
440 return -1;
441 }
442
443 /* pretend that the link failed because the target file did already exist. */
444
445 if (!compare_stat(&st_src, &st_target))
446 {
447 mutt_debug(LL_DEBUG1, "stat blocks for %s and %s diverge; pretending EEXIST\n", src, target);
448 errno = EEXIST;
449 return -1;
450 }
451 #endif
452
453 success:
454 /* Unlink the original link.
455 * Should we really ignore the return value here? XXX */
456 if (unlink(src) == -1)
457 {
458 mutt_debug(LL_DEBUG1, "unlink (%s) failed: %s (%d)\n", src, strerror(errno), errno);
459 }
460
461 return 0;
462 }
463
464 /**
465 * mutt_file_rmtree - Recursively remove a directory
466 * @param path Directory to delete
467 * @retval 0 Success
468 * @retval -1 Error, see errno
469 */
mutt_file_rmtree(const char * path)470 int mutt_file_rmtree(const char *path)
471 {
472 if (!path)
473 return -1;
474
475 struct dirent *de = NULL;
476 struct stat st = { 0 };
477 int rc = 0;
478
479 DIR *dirp = opendir(path);
480 if (!dirp)
481 {
482 mutt_debug(LL_DEBUG1, "error opening directory %s\n", path);
483 return -1;
484 }
485
486 /* We avoid using the buffer pool for this function, because it
487 * invokes recursively to an unknown depth. */
488 struct Buffer cur = mutt_buffer_make(PATH_MAX);
489
490 while ((de = readdir(dirp)))
491 {
492 if ((strcmp(".", de->d_name) == 0) || (strcmp("..", de->d_name) == 0))
493 continue;
494
495 mutt_buffer_printf(&cur, "%s/%s", path, de->d_name);
496 /* XXX make nonrecursive version */
497
498 if (stat(mutt_buffer_string(&cur), &st) == -1)
499 {
500 rc = 1;
501 continue;
502 }
503
504 if (S_ISDIR(st.st_mode))
505 rc |= mutt_file_rmtree(mutt_buffer_string(&cur));
506 else
507 rc |= unlink(mutt_buffer_string(&cur));
508 }
509 closedir(dirp);
510
511 rc |= rmdir(path);
512
513 mutt_buffer_dealloc(&cur);
514 return rc;
515 }
516
517 /**
518 * mutt_file_open - Open a file
519 * @param path Pathname to open
520 * @param flags Flags, e.g. O_EXCL
521 * @retval >0 Success, file handle
522 * @retval -1 Error
523 */
mutt_file_open(const char * path,uint32_t flags)524 int mutt_file_open(const char *path, uint32_t flags)
525 {
526 if (!path)
527 return -1;
528
529 int fd;
530 struct Buffer safe_file = mutt_buffer_make(0);
531 struct Buffer safe_dir = mutt_buffer_make(0);
532
533 if (flags & O_EXCL)
534 {
535 mutt_buffer_alloc(&safe_file, PATH_MAX);
536 mutt_buffer_alloc(&safe_dir, PATH_MAX);
537
538 if (mkwrapdir(path, &safe_file, &safe_dir) == -1)
539 {
540 fd = -1;
541 goto cleanup;
542 }
543
544 fd = open(mutt_buffer_string(&safe_file), flags, 0600);
545 if (fd < 0)
546 {
547 rmdir(mutt_buffer_string(&safe_dir));
548 goto cleanup;
549 }
550
551 /* NFS and I believe cygwin do not handle movement of open files well */
552 close(fd);
553 if (put_file_in_place(path, mutt_buffer_string(&safe_file),
554 mutt_buffer_string(&safe_dir)) == -1)
555 {
556 fd = -1;
557 goto cleanup;
558 }
559 }
560
561 fd = open(path, flags & ~O_EXCL, 0600);
562 if (fd < 0)
563 goto cleanup;
564
565 /* make sure the file is not symlink */
566 struct stat st_old = { 0 };
567 struct stat st_new = { 0 };
568 if (((lstat(path, &st_old) < 0) || (fstat(fd, &st_new) < 0)) ||
569 !compare_stat(&st_old, &st_new))
570 {
571 close(fd);
572 fd = -1;
573 goto cleanup;
574 }
575
576 cleanup:
577 mutt_buffer_dealloc(&safe_file);
578 mutt_buffer_dealloc(&safe_dir);
579
580 return fd;
581 }
582
583 /**
584 * mutt_file_fopen - Call fopen() safely
585 * @param path Filename
586 * @param mode Mode e.g. "r" readonly; "w" read-write
587 * @retval ptr FILE handle
588 * @retval NULL Error, see errno
589 *
590 * When opening files for writing, make sure the file doesn't already exist to
591 * avoid race conditions.
592 */
mutt_file_fopen(const char * path,const char * mode)593 FILE *mutt_file_fopen(const char *path, const char *mode)
594 {
595 if (!path || !mode)
596 return NULL;
597
598 if (mode[0] == 'w')
599 {
600 uint32_t flags = O_CREAT | O_EXCL | O_NOFOLLOW;
601
602 if (mode[1] == '+')
603 flags |= O_RDWR;
604 else
605 flags |= O_WRONLY;
606
607 int fd = mutt_file_open(path, flags);
608 if (fd < 0)
609 return NULL;
610
611 return fdopen(fd, mode);
612 }
613 else
614 return fopen(path, mode);
615 }
616
617 /**
618 * mutt_file_sanitize_filename - Replace unsafe characters in a filename
619 * @param path Filename to make safe
620 * @param slash Replace '/' characters too
621 */
mutt_file_sanitize_filename(char * path,bool slash)622 void mutt_file_sanitize_filename(char *path, bool slash)
623 {
624 if (!path)
625 return;
626
627 for (; *path; path++)
628 {
629 if ((slash && (*path == '/')) || !strchr(filename_safe_chars, *path))
630 *path = '_';
631 }
632 }
633
634 /**
635 * mutt_file_sanitize_regex - Escape any regex-magic characters in a string
636 * @param dest Buffer for result
637 * @param src String to transform
638 * @retval 0 Success
639 * @retval -1 Error
640 */
mutt_file_sanitize_regex(struct Buffer * dest,const char * src)641 int mutt_file_sanitize_regex(struct Buffer *dest, const char *src)
642 {
643 if (!dest || !src)
644 return -1;
645
646 mutt_buffer_reset(dest);
647 while (*src != '\0')
648 {
649 if (strchr(rx_special_chars, *src))
650 mutt_buffer_addch(dest, '\\');
651 mutt_buffer_addch(dest, *src++);
652 }
653
654 return 0;
655 }
656
657 /**
658 * mutt_file_read_line - Read a line from a file
659 * @param[out] line Buffer allocated on the head (optional)
660 * @param[in] size Length of buffer
661 * @param[in] fp File to read
662 * @param[out] line_num Current line number (optional)
663 * @param[in] flags Flags, e.g. #MUTT_RL_CONT
664 * @retval ptr The allocated string
665 *
666 * Read a line from "fp" into the dynamically allocated "line", increasing
667 * "line" if necessary. The ending "\n" or "\r\n" is removed. If a line ends
668 * with "\", this char and the linefeed is removed, and the next line is read
669 * too.
670 */
mutt_file_read_line(char * line,size_t * size,FILE * fp,int * line_num,ReadLineFlags flags)671 char *mutt_file_read_line(char *line, size_t *size, FILE *fp, int *line_num, ReadLineFlags flags)
672 {
673 if (!size || !fp)
674 return NULL;
675
676 size_t offset = 0;
677 char *ch = NULL;
678
679 if (!line)
680 {
681 *size = 256;
682 line = mutt_mem_malloc(*size);
683 }
684
685 while (true)
686 {
687 if (!fgets(line + offset, *size - offset, fp))
688 {
689 FREE(&line);
690 return NULL;
691 }
692 ch = strchr(line + offset, '\n');
693 if (ch)
694 {
695 if (line_num)
696 (*line_num)++;
697 if (flags & MUTT_RL_EOL)
698 return line;
699 *ch = '\0';
700 if ((ch > line) && (*(ch - 1) == '\r'))
701 *--ch = '\0';
702 if (!(flags & MUTT_RL_CONT) || (ch == line) || (*(ch - 1) != '\\'))
703 return line;
704 offset = ch - line - 1;
705 }
706 else
707 {
708 int c;
709 c = getc(fp); /* This is kind of a hack. We want to know if the
710 char at the current point in the input stream is EOF.
711 feof() will only tell us if we've already hit EOF, not
712 if the next character is EOF. So, we need to read in
713 the next character and manually check if it is EOF. */
714 if (c == EOF)
715 {
716 /* The last line of fp isn't \n terminated */
717 if (line_num)
718 (*line_num)++;
719 return line;
720 }
721 else
722 {
723 ungetc(c, fp); /* undo our damage */
724 /* There wasn't room for the line -- increase "line" */
725 offset = *size - 1; /* overwrite the terminating 0 */
726 *size += 256;
727 mutt_mem_realloc(&line, *size);
728 }
729 }
730 }
731 }
732
733 /**
734 * mutt_file_iter_line - Iterate over the lines from an open file pointer
735 * @param iter State of iteration including ptr to line
736 * @param fp File pointer to read from
737 * @param flags Same as mutt_file_read_line()
738 * @retval true Data read
739 * @retval false On eof
740 *
741 * This is a slightly cleaner interface for mutt_file_read_line() which avoids
742 * the eternal C loop initialization ugliness. Use like this:
743 *
744 * ```
745 * struct MuttFileIter iter = { 0 };
746 * while (mutt_file_iter_line(&iter, fp, flags))
747 * {
748 * do_stuff(iter.line, iter.line_num);
749 * }
750 * ```
751 */
mutt_file_iter_line(struct MuttFileIter * iter,FILE * fp,ReadLineFlags flags)752 bool mutt_file_iter_line(struct MuttFileIter *iter, FILE *fp, ReadLineFlags flags)
753 {
754 if (!iter)
755 return false;
756
757 char *p = mutt_file_read_line(iter->line, &iter->size, fp, &iter->line_num, flags);
758 if (!p)
759 return false;
760 iter->line = p;
761 return true;
762 }
763
764 /**
765 * mutt_file_map_lines - Process lines of text read from a file pointer
766 * @param func Callback function to call for each line, see mutt_file_map_t
767 * @param user_data Arbitrary data passed to "func"
768 * @param fp File pointer to read from
769 * @param flags Same as mutt_file_read_line()
770 * @retval true All data mapped
771 * @retval false "func" returns false
772 */
mutt_file_map_lines(mutt_file_map_t func,void * user_data,FILE * fp,ReadLineFlags flags)773 bool mutt_file_map_lines(mutt_file_map_t func, void *user_data, FILE *fp, ReadLineFlags flags)
774 {
775 if (!func || !fp)
776 return false;
777
778 struct MuttFileIter iter = { 0 };
779 while (mutt_file_iter_line(&iter, fp, flags))
780 {
781 if (!(*func)(iter.line, iter.line_num, user_data))
782 {
783 FREE(&iter.line);
784 return false;
785 }
786 }
787 return true;
788 }
789
790 /**
791 * mutt_file_quote_filename - Quote a filename to survive the shell's quoting rules
792 * @param filename String to convert
793 * @param buf Buffer for the result
794 * @param buflen Length of buffer
795 * @retval num Bytes written to the buffer
796 *
797 * From the Unix programming FAQ by way of Liviu.
798 */
mutt_file_quote_filename(const char * filename,char * buf,size_t buflen)799 size_t mutt_file_quote_filename(const char *filename, char *buf, size_t buflen)
800 {
801 if (!buf)
802 return 0;
803
804 if (!filename)
805 {
806 *buf = '\0';
807 return 0;
808 }
809
810 size_t j = 0;
811
812 /* leave some space for the trailing characters. */
813 buflen -= 6;
814
815 buf[j++] = '\'';
816
817 for (size_t i = 0; (j < buflen) && filename[i]; i++)
818 {
819 if ((filename[i] == '\'') || (filename[i] == '`'))
820 {
821 buf[j++] = '\'';
822 buf[j++] = '\\';
823 buf[j++] = filename[i];
824 buf[j++] = '\'';
825 }
826 else
827 buf[j++] = filename[i];
828 }
829
830 buf[j++] = '\'';
831 buf[j] = '\0';
832
833 return j;
834 }
835
836 /**
837 * mutt_buffer_quote_filename - Quote a filename to survive the shell's quoting rules
838 * @param buf Buffer for the result
839 * @param filename String to convert
840 * @param add_outer If true, add 'single quotes' around the result
841 */
mutt_buffer_quote_filename(struct Buffer * buf,const char * filename,bool add_outer)842 void mutt_buffer_quote_filename(struct Buffer *buf, const char *filename, bool add_outer)
843 {
844 if (!buf || !filename)
845 return;
846
847 mutt_buffer_reset(buf);
848 if (add_outer)
849 mutt_buffer_addch(buf, '\'');
850
851 for (; *filename != '\0'; filename++)
852 {
853 if ((*filename == '\'') || (*filename == '`'))
854 {
855 mutt_buffer_addch(buf, '\'');
856 mutt_buffer_addch(buf, '\\');
857 mutt_buffer_addch(buf, *filename);
858 mutt_buffer_addch(buf, '\'');
859 }
860 else
861 mutt_buffer_addch(buf, *filename);
862 }
863
864 if (add_outer)
865 mutt_buffer_addch(buf, '\'');
866 }
867
868 /**
869 * mutt_file_mkdir - Recursively create directories
870 * @param path Directories to create
871 * @param mode Permissions for final directory
872 * @retval 0 Success
873 * @retval -1 Error (errno set)
874 *
875 * Create a directory, creating the parents if necessary. (like mkdir -p)
876 *
877 * @note The permissions are only set on the final directory.
878 * The permissions of any parent directories are determined by the umask.
879 * (This is how "mkdir -p" behaves)
880 */
mutt_file_mkdir(const char * path,mode_t mode)881 int mutt_file_mkdir(const char *path, mode_t mode)
882 {
883 if (!path || (*path == '\0'))
884 {
885 errno = EINVAL;
886 return -1;
887 }
888
889 errno = 0;
890 char tmp_path[PATH_MAX];
891 const size_t len = strlen(path);
892
893 if (len >= sizeof(tmp_path))
894 {
895 errno = ENAMETOOLONG;
896 return -1;
897 }
898
899 struct stat st = { 0 };
900 if ((stat(path, &st) == 0) && S_ISDIR(st.st_mode))
901 return 0;
902
903 /* Create a mutable copy */
904 mutt_str_copy(tmp_path, path, sizeof(tmp_path));
905
906 for (char *p = tmp_path + 1; *p; p++)
907 {
908 if (*p != '/')
909 continue;
910
911 /* Temporarily truncate the path */
912 *p = '\0';
913
914 if ((mkdir(tmp_path, S_IRWXU | S_IRWXG | S_IRWXO) != 0) && (errno != EEXIST))
915 return -1;
916
917 *p = '/';
918 }
919
920 if ((mkdir(tmp_path, mode) != 0) && (errno != EEXIST))
921 return -1;
922
923 return 0;
924 }
925
926 /**
927 * mutt_file_mkstemp_full - Create temporary file safely
928 * @param file Source file of caller
929 * @param line Source line number of caller
930 * @param func Function name of caller
931 * @retval ptr FILE handle
932 * @retval NULL Error, see errno
933 *
934 * Create and immediately unlink a temp file using mkstemp().
935 */
mutt_file_mkstemp_full(const char * file,int line,const char * func)936 FILE *mutt_file_mkstemp_full(const char *file, int line, const char *func)
937 {
938 char name[PATH_MAX];
939
940 const char *const c_tmpdir = cs_subset_path(NeoMutt->sub, "tmpdir");
941 int n = snprintf(name, sizeof(name), "%s/neomutt-XXXXXX", NONULL(c_tmpdir));
942 if (n < 0)
943 return NULL;
944
945 int fd = mkstemp(name);
946 if (fd == -1)
947 return NULL;
948
949 FILE *fp = fdopen(fd, "w+");
950
951 if ((unlink(name) != 0) && (errno != ENOENT))
952 {
953 mutt_file_fclose(&fp);
954 return NULL;
955 }
956
957 MuttLogger(0, file, line, func, 1, "created temp file '%s'\n", name);
958 return fp;
959 }
960
961 /**
962 * mutt_file_decrease_mtime - Decrease a file's modification time by 1 second
963 * @param fp Filename
964 * @param st struct stat for the file (optional)
965 * @retval num Updated Unix mtime
966 * @retval -1 Error, see errno
967 *
968 * If a file's mtime is NOW, then set it to 1 second in the past.
969 */
mutt_file_decrease_mtime(const char * fp,struct stat * st)970 time_t mutt_file_decrease_mtime(const char *fp, struct stat *st)
971 {
972 if (!fp)
973 return -1;
974
975 struct utimbuf utim;
976 struct stat st2 = { 0 };
977 time_t mtime;
978
979 if (!st)
980 {
981 if (stat(fp, &st2) == -1)
982 return -1;
983 st = &st2;
984 }
985
986 mtime = st->st_mtime;
987 if (mtime == mutt_date_epoch())
988 {
989 mtime -= 1;
990 utim.actime = mtime;
991 utim.modtime = mtime;
992 int rc;
993 do
994 {
995 rc = utime(fp, &utim);
996 } while ((rc == -1) && (errno == EINTR));
997
998 if (rc == -1)
999 return -1;
1000 }
1001
1002 return mtime;
1003 }
1004
1005 /**
1006 * mutt_file_set_mtime - Set the modification time of one file from another
1007 * @param from Filename whose mtime should be copied
1008 * @param to Filename to update
1009 */
mutt_file_set_mtime(const char * from,const char * to)1010 void mutt_file_set_mtime(const char *from, const char *to)
1011 {
1012 if (!from || !to)
1013 return;
1014
1015 struct utimbuf utim;
1016 struct stat st = { 0 };
1017
1018 if (stat(from, &st) != -1)
1019 {
1020 utim.actime = st.st_mtime;
1021 utim.modtime = st.st_mtime;
1022 utime(to, &utim);
1023 }
1024 }
1025
1026 /**
1027 * mutt_file_touch_atime - Set the access time to current time
1028 * @param fd File descriptor of the file to alter
1029 *
1030 * This is just as read() would do on !noatime.
1031 * Silently ignored if futimens() isn't supported.
1032 */
mutt_file_touch_atime(int fd)1033 void mutt_file_touch_atime(int fd)
1034 {
1035 #ifdef HAVE_FUTIMENS
1036 struct timespec times[2] = { { 0, UTIME_NOW }, { 0, UTIME_OMIT } };
1037 futimens(fd, times);
1038 #endif
1039 }
1040
1041 /**
1042 * mutt_file_chmod - Set permissions of a file
1043 * @param path Filename
1044 * @param mode the permissions to set
1045 * @retval num Same as chmod(2)
1046 *
1047 * This is essentially chmod(path, mode), see chmod(2).
1048 */
mutt_file_chmod(const char * path,mode_t mode)1049 int mutt_file_chmod(const char *path, mode_t mode)
1050 {
1051 if (!path)
1052 return -1;
1053
1054 return chmod(path, mode);
1055 }
1056
1057 /**
1058 * mutt_file_chmod_add - Add permissions to a file
1059 * @param path Filename
1060 * @param mode the permissions to add
1061 * @retval num Same as chmod(2)
1062 *
1063 * Adds the given permissions to the file. Permissions not mentioned in mode
1064 * will stay as they are. This function resembles the `chmod ugoa+rwxXst`
1065 * command family. Example:
1066 *
1067 * mutt_file_chmod_add(path, S_IWUSR | S_IWGRP | S_IWOTH);
1068 *
1069 * will add write permissions to path but does not alter read and other
1070 * permissions.
1071 *
1072 * @sa mutt_file_chmod_add_stat()
1073 */
mutt_file_chmod_add(const char * path,mode_t mode)1074 int mutt_file_chmod_add(const char *path, mode_t mode)
1075 {
1076 return mutt_file_chmod_add_stat(path, mode, NULL);
1077 }
1078
1079 /**
1080 * mutt_file_chmod_add_stat - Add permissions to a file
1081 * @param path Filename
1082 * @param mode the permissions to add
1083 * @param st struct stat for the file (optional)
1084 * @retval num Same as chmod(2)
1085 *
1086 * Same as mutt_file_chmod_add() but saves a system call to stat() if a
1087 * non-NULL stat structure is given. Useful if the stat structure of the file
1088 * was retrieved before by the calling code. Example:
1089 *
1090 * struct stat st;
1091 * stat(path, &st);
1092 * // ... do something else with st ...
1093 * mutt_file_chmod_add_stat(path, S_IWUSR | S_IWGRP | S_IWOTH, st);
1094 *
1095 * @sa mutt_file_chmod_add()
1096 */
mutt_file_chmod_add_stat(const char * path,mode_t mode,struct stat * st)1097 int mutt_file_chmod_add_stat(const char *path, mode_t mode, struct stat *st)
1098 {
1099 if (!path)
1100 return -1;
1101
1102 struct stat st2 = { 0 };
1103
1104 if (!st)
1105 {
1106 if (stat(path, &st2) == -1)
1107 return -1;
1108 st = &st2;
1109 }
1110 return chmod(path, st->st_mode | mode);
1111 }
1112
1113 /**
1114 * mutt_file_chmod_rm - Remove permissions from a file
1115 * @param path Filename
1116 * @param mode the permissions to remove
1117 * @retval num Same as chmod(2)
1118 *
1119 * Removes the given permissions from the file. Permissions not mentioned in
1120 * mode will stay as they are. This function resembles the `chmod ugoa-rwxXst`
1121 * command family. Example:
1122 *
1123 * mutt_file_chmod_rm(path, S_IWUSR | S_IWGRP | S_IWOTH);
1124 *
1125 * will remove write permissions from path but does not alter read and other
1126 * permissions.
1127 *
1128 * @sa mutt_file_chmod_rm_stat()
1129 */
mutt_file_chmod_rm(const char * path,mode_t mode)1130 int mutt_file_chmod_rm(const char *path, mode_t mode)
1131 {
1132 return mutt_file_chmod_rm_stat(path, mode, NULL);
1133 }
1134
1135 /**
1136 * mutt_file_chmod_rm_stat - Remove permissions from a file
1137 * @param path Filename
1138 * @param mode the permissions to remove
1139 * @param st struct stat for the file (optional)
1140 * @retval num Same as chmod(2)
1141 *
1142 * Same as mutt_file_chmod_rm() but saves a system call to stat() if a non-NULL
1143 * stat structure is given. Useful if the stat structure of the file was
1144 * retrieved before by the calling code. Example:
1145 *
1146 * struct stat st;
1147 * stat(path, &st);
1148 * // ... do something else with st ...
1149 * mutt_file_chmod_rm_stat(path, S_IWUSR | S_IWGRP | S_IWOTH, st);
1150 *
1151 * @sa mutt_file_chmod_rm()
1152 */
mutt_file_chmod_rm_stat(const char * path,mode_t mode,struct stat * st)1153 int mutt_file_chmod_rm_stat(const char *path, mode_t mode, struct stat *st)
1154 {
1155 if (!path)
1156 return -1;
1157
1158 struct stat st2 = { 0 };
1159
1160 if (!st)
1161 {
1162 if (stat(path, &st2) == -1)
1163 return -1;
1164 st = &st2;
1165 }
1166 return chmod(path, st->st_mode & ~mode);
1167 }
1168
1169 #if defined(USE_FCNTL)
1170 /**
1171 * mutt_file_lock - (Try to) Lock a file using fcntl()
1172 * @param fd File descriptor to file
1173 * @param excl If true, try to lock exclusively
1174 * @param timeout If true, Retry #MAX_LOCK_ATTEMPTS times
1175 * @retval 0 Success
1176 * @retval -1 Failure
1177 *
1178 * Use fcntl() to lock a file.
1179 *
1180 * Use mutt_file_unlock() to unlock the file.
1181 */
mutt_file_lock(int fd,bool excl,bool timeout)1182 int mutt_file_lock(int fd, bool excl, bool timeout)
1183 {
1184 struct stat st = { 0 }, prev_sb = { 0 };
1185 int count = 0;
1186 int attempt = 0;
1187
1188 struct flock lck;
1189 memset(&lck, 0, sizeof(struct flock));
1190 lck.l_type = excl ? F_WRLCK : F_RDLCK;
1191 lck.l_whence = SEEK_SET;
1192
1193 while (fcntl(fd, F_SETLK, &lck) == -1)
1194 {
1195 mutt_debug(LL_DEBUG1, "fcntl errno %d\n", errno);
1196 if ((errno != EAGAIN) && (errno != EACCES))
1197 {
1198 mutt_perror("fcntl");
1199 return -1;
1200 }
1201
1202 if (fstat(fd, &st) != 0)
1203 st.st_size = 0;
1204
1205 if (count == 0)
1206 prev_sb = st;
1207
1208 /* only unlock file if it is unchanged */
1209 if ((prev_sb.st_size == st.st_size) && (++count >= (timeout ? MAX_LOCK_ATTEMPTS : 0)))
1210 {
1211 if (timeout)
1212 mutt_error(_("Timeout exceeded while attempting fcntl lock"));
1213 return -1;
1214 }
1215
1216 prev_sb = st;
1217
1218 mutt_message(_("Waiting for fcntl lock... %d"), ++attempt);
1219 sleep(1);
1220 }
1221
1222 return 0;
1223 }
1224
1225 /**
1226 * mutt_file_unlock - Unlock a file previously locked by mutt_file_lock()
1227 * @param fd File descriptor to file
1228 * @retval 0 Always
1229 */
mutt_file_unlock(int fd)1230 int mutt_file_unlock(int fd)
1231 {
1232 struct flock unlockit;
1233
1234 memset(&unlockit, 0, sizeof(struct flock));
1235 unlockit.l_type = F_UNLCK;
1236 unlockit.l_whence = SEEK_SET;
1237 (void) fcntl(fd, F_SETLK, &unlockit);
1238
1239 return 0;
1240 }
1241 #elif defined(USE_FLOCK)
1242 /**
1243 * mutt_file_lock - (Try to) Lock a file using flock()
1244 * @param fd File descriptor to file
1245 * @param excl If true, try to lock exclusively
1246 * @param timeout If true, Retry #MAX_LOCK_ATTEMPTS times
1247 * @retval 0 Success
1248 * @retval -1 Failure
1249 *
1250 * Use flock() to lock a file.
1251 *
1252 * Use mutt_file_unlock() to unlock the file.
1253 */
mutt_file_lock(int fd,bool excl,bool timeout)1254 int mutt_file_lock(int fd, bool excl, bool timeout)
1255 {
1256 struct stat st = { 0 }, prev_sb = { 0 };
1257 int rc = 0;
1258 int count = 0;
1259 int attempt = 0;
1260
1261 while (flock(fd, (excl ? LOCK_EX : LOCK_SH) | LOCK_NB) == -1)
1262 {
1263 if (errno != EWOULDBLOCK)
1264 {
1265 mutt_perror("flock");
1266 rc = -1;
1267 break;
1268 }
1269
1270 if (fstat(fd, &st) != 0)
1271 st.st_size = 0;
1272
1273 if (count == 0)
1274 prev_sb = st;
1275
1276 /* only unlock file if it is unchanged */
1277 if ((prev_sb.st_size == st.st_size) && (++count >= (timeout ? MAX_LOCK_ATTEMPTS : 0)))
1278 {
1279 if (timeout)
1280 mutt_error(_("Timeout exceeded while attempting flock lock"));
1281 rc = -1;
1282 break;
1283 }
1284
1285 prev_sb = st;
1286
1287 mutt_message(_("Waiting for flock attempt... %d"), ++attempt);
1288 sleep(1);
1289 }
1290
1291 /* release any other locks obtained in this routine */
1292 if (rc != 0)
1293 {
1294 flock(fd, LOCK_UN);
1295 }
1296
1297 return rc;
1298 }
1299
1300 /**
1301 * mutt_file_unlock - Unlock a file previously locked by mutt_file_lock()
1302 * @param fd File descriptor to file
1303 * @retval 0 Always
1304 */
mutt_file_unlock(int fd)1305 int mutt_file_unlock(int fd)
1306 {
1307 flock(fd, LOCK_UN);
1308 return 0;
1309 }
1310 #else
1311 #error "You must select a locking mechanism via USE_FCNTL or USE_FLOCK"
1312 #endif
1313
1314 /**
1315 * mutt_file_unlink_empty - Delete a file if it's empty
1316 * @param path File to delete
1317 */
mutt_file_unlink_empty(const char * path)1318 void mutt_file_unlink_empty(const char *path)
1319 {
1320 if (!path)
1321 return;
1322
1323 struct stat st = { 0 };
1324
1325 int fd = open(path, O_RDWR);
1326 if (fd == -1)
1327 return;
1328
1329 if (mutt_file_lock(fd, true, true) == -1)
1330 {
1331 close(fd);
1332 return;
1333 }
1334
1335 if ((fstat(fd, &st) == 0) && (st.st_size == 0))
1336 unlink(path);
1337
1338 mutt_file_unlock(fd);
1339 close(fd);
1340 }
1341
1342 /**
1343 * mutt_file_rename - Rename a file
1344 * @param oldfile Old filename
1345 * @param newfile New filename
1346 * @retval 0 Success
1347 * @retval 1 Old file doesn't exist
1348 * @retval 2 New file already exists
1349 * @retval 3 Some other error
1350 *
1351 * @note on access(2) use No dangling symlink problems here due to
1352 * mutt_file_fopen().
1353 */
mutt_file_rename(const char * oldfile,const char * newfile)1354 int mutt_file_rename(const char *oldfile, const char *newfile)
1355 {
1356 if (!oldfile || !newfile)
1357 return -1;
1358 if (access(oldfile, F_OK) != 0)
1359 return 1;
1360 if (access(newfile, F_OK) == 0)
1361 return 2;
1362
1363 FILE *fp_old = fopen(oldfile, "r");
1364 if (!fp_old)
1365 return 3;
1366 FILE *fp_new = mutt_file_fopen(newfile, "w");
1367 if (!fp_new)
1368 {
1369 mutt_file_fclose(&fp_old);
1370 return 3;
1371 }
1372 mutt_file_copy_stream(fp_old, fp_new);
1373 mutt_file_fclose(&fp_new);
1374 mutt_file_fclose(&fp_old);
1375 mutt_file_unlink(oldfile);
1376 return 0;
1377 }
1378
1379 /**
1380 * mutt_file_read_keyword - Read a keyword from a file
1381 * @param file File to read
1382 * @param buf Buffer to store the keyword
1383 * @param buflen Length of the buf
1384 * @retval ptr Start of the keyword
1385 *
1386 * Read one line from the start of a file.
1387 * Skip any leading whitespace and extract the first token.
1388 */
mutt_file_read_keyword(const char * file,char * buf,size_t buflen)1389 char *mutt_file_read_keyword(const char *file, char *buf, size_t buflen)
1390 {
1391 FILE *fp = mutt_file_fopen(file, "r");
1392 if (!fp)
1393 return NULL;
1394
1395 buf = fgets(buf, buflen, fp);
1396 mutt_file_fclose(&fp);
1397
1398 if (!buf)
1399 return NULL;
1400
1401 SKIPWS(buf);
1402 char *start = buf;
1403
1404 while ((*buf != '\0') && !isspace(*buf))
1405 buf++;
1406
1407 *buf = '\0';
1408
1409 return start;
1410 }
1411
1412 /**
1413 * mutt_file_check_empty - Is the mailbox empty
1414 * @param path Path to mailbox
1415 * @retval 1 Mailbox is empty
1416 * @retval 0 Mailbox is not empty
1417 * @retval -1 Error
1418 */
mutt_file_check_empty(const char * path)1419 int mutt_file_check_empty(const char *path)
1420 {
1421 if (!path)
1422 return -1;
1423
1424 struct stat st = { 0 };
1425 if (stat(path, &st) == -1)
1426 return -1;
1427
1428 return st.st_size == 0;
1429 }
1430
1431 /**
1432 * mutt_buffer_file_expand_fmt_quote - Replace `%s` in a string with a filename
1433 * @param dest Buffer for the result
1434 * @param fmt printf-like format string
1435 * @param src Filename to substitute
1436 *
1437 * This function also quotes the file to prevent shell problems.
1438 */
mutt_buffer_file_expand_fmt_quote(struct Buffer * dest,const char * fmt,const char * src)1439 void mutt_buffer_file_expand_fmt_quote(struct Buffer *dest, const char *fmt, const char *src)
1440 {
1441 struct Buffer tmp = mutt_buffer_make(PATH_MAX);
1442
1443 mutt_buffer_quote_filename(&tmp, src, true);
1444 mutt_file_expand_fmt(dest, fmt, mutt_buffer_string(&tmp));
1445 mutt_buffer_dealloc(&tmp);
1446 }
1447
1448 /**
1449 * mutt_file_expand_fmt - Replace `%s` in a string with a filename
1450 * @param dest Buffer for the result
1451 * @param fmt printf-like format string
1452 * @param src Filename to substitute
1453 */
mutt_file_expand_fmt(struct Buffer * dest,const char * fmt,const char * src)1454 void mutt_file_expand_fmt(struct Buffer *dest, const char *fmt, const char *src)
1455 {
1456 if (!dest || !fmt || !src)
1457 return;
1458
1459 const char *p = NULL;
1460 bool found = false;
1461
1462 mutt_buffer_reset(dest);
1463
1464 for (p = fmt; *p; p++)
1465 {
1466 if (*p == '%')
1467 {
1468 switch (p[1])
1469 {
1470 case '%':
1471 mutt_buffer_addch(dest, *p++);
1472 break;
1473 case 's':
1474 found = true;
1475 mutt_buffer_addstr(dest, src);
1476 p++;
1477 break;
1478 default:
1479 mutt_buffer_addch(dest, *p);
1480 break;
1481 }
1482 }
1483 else
1484 {
1485 mutt_buffer_addch(dest, *p);
1486 }
1487 }
1488
1489 if (!found)
1490 {
1491 mutt_buffer_addch(dest, ' ');
1492 mutt_buffer_addstr(dest, src);
1493 }
1494 }
1495
1496 /**
1497 * mutt_file_get_size - Get the size of a file
1498 * @param path File to measure
1499 * @retval num Size in bytes
1500 * @retval 0 Error
1501 */
mutt_file_get_size(const char * path)1502 long mutt_file_get_size(const char *path)
1503 {
1504 if (!path)
1505 return 0;
1506
1507 struct stat st = { 0 };
1508 if (stat(path, &st) != 0)
1509 return 0;
1510
1511 return st.st_size;
1512 }
1513
1514 /**
1515 * mutt_file_get_size_fp - Get the size of a file
1516 * @param fp FILE* to measure
1517 * @retval num Size in bytes
1518 * @retval 0 Error
1519 */
mutt_file_get_size_fp(FILE * fp)1520 long mutt_file_get_size_fp(FILE *fp)
1521 {
1522 if (!fp)
1523 return 0;
1524
1525 struct stat st = { 0 };
1526 if (fstat(fileno(fp), &st) != 0)
1527 return 0;
1528
1529 return st.st_size;
1530 }
1531
1532 /**
1533 * mutt_file_timespec_compare - Compare to time values
1534 * @param a First time value
1535 * @param b Second time value
1536 * @retval -1 a precedes b
1537 * @retval 0 a and b are identical
1538 * @retval 1 b precedes a
1539 */
mutt_file_timespec_compare(struct timespec * a,struct timespec * b)1540 int mutt_file_timespec_compare(struct timespec *a, struct timespec *b)
1541 {
1542 if (!a || !b)
1543 return 0;
1544 if (a->tv_sec < b->tv_sec)
1545 return -1;
1546 if (a->tv_sec > b->tv_sec)
1547 return 1;
1548
1549 if (a->tv_nsec < b->tv_nsec)
1550 return -1;
1551 if (a->tv_nsec > b->tv_nsec)
1552 return 1;
1553 return 0;
1554 }
1555
1556 /**
1557 * mutt_file_get_stat_timespec - Read the stat() time into a time value
1558 * @param dest Time value to populate
1559 * @param st stat info
1560 * @param type Type of stat info to read, e.g. #MUTT_STAT_ATIME
1561 */
mutt_file_get_stat_timespec(struct timespec * dest,struct stat * st,enum MuttStatType type)1562 void mutt_file_get_stat_timespec(struct timespec *dest, struct stat *st, enum MuttStatType type)
1563 {
1564 if (!dest || !st)
1565 return;
1566
1567 dest->tv_sec = 0;
1568 dest->tv_nsec = 0;
1569
1570 switch (type)
1571 {
1572 case MUTT_STAT_ATIME:
1573 dest->tv_sec = st->st_atime;
1574 #ifdef HAVE_STRUCT_STAT_ST_ATIM_TV_NSEC
1575 dest->tv_nsec = st->st_atim.tv_nsec;
1576 #endif
1577 break;
1578 case MUTT_STAT_MTIME:
1579 dest->tv_sec = st->st_mtime;
1580 #ifdef HAVE_STRUCT_STAT_ST_MTIM_TV_NSEC
1581 dest->tv_nsec = st->st_mtim.tv_nsec;
1582 #endif
1583 break;
1584 case MUTT_STAT_CTIME:
1585 dest->tv_sec = st->st_ctime;
1586 #ifdef HAVE_STRUCT_STAT_ST_CTIM_TV_NSEC
1587 dest->tv_nsec = st->st_ctim.tv_nsec;
1588 #endif
1589 break;
1590 }
1591 }
1592
1593 /**
1594 * mutt_file_stat_timespec_compare - Compare stat info with a time value
1595 * @param st stat info
1596 * @param type Type of stat info, e.g. #MUTT_STAT_ATIME
1597 * @param b Time value
1598 * @retval -1 a precedes b
1599 * @retval 0 a and b are identical
1600 * @retval 1 b precedes a
1601 */
mutt_file_stat_timespec_compare(struct stat * st,enum MuttStatType type,struct timespec * b)1602 int mutt_file_stat_timespec_compare(struct stat *st, enum MuttStatType type,
1603 struct timespec *b)
1604 {
1605 if (!st || !b)
1606 return 0;
1607
1608 struct timespec a = { 0 };
1609
1610 mutt_file_get_stat_timespec(&a, st, type);
1611 return mutt_file_timespec_compare(&a, b);
1612 }
1613
1614 /**
1615 * mutt_file_stat_compare - Compare two stat infos
1616 * @param st1 First stat info
1617 * @param st1_type Type of first stat info, e.g. #MUTT_STAT_ATIME
1618 * @param st2 Second stat info
1619 * @param st2_type Type of second stat info, e.g. #MUTT_STAT_ATIME
1620 * @retval -1 a precedes b
1621 * @retval 0 a and b are identical
1622 * @retval 1 b precedes a
1623 */
mutt_file_stat_compare(struct stat * st1,enum MuttStatType st1_type,struct stat * st2,enum MuttStatType st2_type)1624 int mutt_file_stat_compare(struct stat *st1, enum MuttStatType st1_type,
1625 struct stat *st2, enum MuttStatType st2_type)
1626 {
1627 if (!st1 || !st2)
1628 return 0;
1629
1630 struct timespec a = { 0 };
1631 struct timespec b = { 0 };
1632
1633 mutt_file_get_stat_timespec(&a, st1, st1_type);
1634 mutt_file_get_stat_timespec(&b, st2, st2_type);
1635 return mutt_file_timespec_compare(&a, &b);
1636 }
1637
1638 /**
1639 * mutt_file_resolve_symlink - Resolve a symlink in place
1640 * @param buf Input/output path
1641 */
mutt_file_resolve_symlink(struct Buffer * buf)1642 void mutt_file_resolve_symlink(struct Buffer *buf)
1643 {
1644 struct stat st = { 0 };
1645 int rc = lstat(mutt_buffer_string(buf), &st);
1646 if ((rc != -1) && S_ISLNK(st.st_mode))
1647 {
1648 char path[PATH_MAX];
1649 if (realpath(mutt_buffer_string(buf), path))
1650 {
1651 mutt_buffer_strcpy(buf, path);
1652 }
1653 }
1654 }
1655