safelink.c (3612B)
1 #include <errno.h> /* for errno */ 2 #include <fcntl.h> /* for mkdirat() openat() */ 3 #include <unistd.h> /* for linkat() */ 4 #include <stdio.h> /* for renameat() */ 5 #include <stdbool.h> /* for bool */ 6 7 #include <skalibs/strerr.h> 8 #include <skalibs/random.h> 9 10 #define PROG "safelink" 11 #define USAGE "safelink oldpath newpath" 12 #define BUFLEN 260 13 14 const char* opendir_nofollow(const char *pathname, int *fd, bool create_dirs) 15 { 16 char next[BUFLEN]; 17 int fd1, fd2; 18 const char *base; /* final part (basename) to return */ 19 const char *s1 = pathname; 20 char *s2 = next; 21 22 if(pathname[0] == '/') { 23 fd1 = open("/", O_NOFOLLOW | O_DIRECTORY | O_RDONLY); 24 if(fd1 < 0) { 25 strerr_diefu1sys(111, "open() root"); 26 } 27 s1++; 28 } else { 29 fd1 = open(".", O_NOFOLLOW | O_DIRECTORY | O_RDONLY); 30 if(fd1 < 0) { 31 strerr_diefu1sys(111, "open() CWD"); 32 } 33 } 34 base = s1; 35 while (*s1) { 36 /* Copy character by character from pathname[] to next[]. */ 37 *(s2++) = *(s1++); 38 if (s2 >= &next[BUFLEN]) { 39 strerr_dief1x(100, "filename exceeded buffer size"); 40 } 41 if(*s1 == 0) { 42 /* end of string, return dir fd and final path component */ 43 *fd = fd1; 44 return base; 45 } 46 if(*s1 == '/') { 47 if(*s1) { s1++; } 48 if(s2 == next) { continue; } /* skip empty filename or trailing slash */ 49 *s2 = 0; /* null-terminate the string in next[] */ 50 s2 = next; /* reset s2 pointer to the start of next[] for next dirname */ 51 52 /* Try opening the directory in next[]. */ 53 fd2 = openat(fd1, next, O_NOFOLLOW | O_DIRECTORY | O_RDONLY); 54 if(fd2 < 1) { 55 if(errno == ENOENT) { 56 if(!create_dirs) { 57 strerr_dief2x(111, "directory does not exist: ", next); 58 } 59 /* Create the missing directory. */ 60 if(mkdirat(fd1, next, 0777) != 0) { 61 strerr_diefu2sys(111, "mkdirat(): ", next); 62 } 63 /* Open the newly created directory. */ 64 fd2 = openat(fd1, next, O_NOFOLLOW | O_DIRECTORY | O_RDONLY); 65 if(fd2 < 1) { 66 strerr_diefu2sys(111, "openat(): ", next); 67 } 68 } else { 69 strerr_diefu2sys(111, "openat(): ", next); 70 } 71 } 72 73 /* Close the parent directory and replace reference to it with the newly opened one. */ 74 close(fd1); 75 fd1 = fd2; 76 base = s1; 77 } 78 } 79 } 80 81 int main (int argc, char const *const *argv) 82 { 83 if (argc != 3) { 84 strerr_dieusage(100, USAGE); 85 } 86 int old_fd, new_fd; 87 const char *old_base, *new_base; 88 old_base = opendir_nofollow(argv[1], &old_fd, false); 89 if(old_base[0] == 0) { 90 strerr_dief2x(100, "malformed path: ", argv[1]); 91 } 92 new_base = opendir_nofollow(argv[2], &new_fd, true); 93 if(new_base[0] == 0) { 94 strerr_dief2x(100, "malformed path: ", argv[2]); 95 } 96 if(linkat(old_fd, old_base, new_fd, new_base, 0) == 0) { 97 return 0; /* created hardlink at the correct location */ 98 } 99 if(errno != EEXIST) { 100 strerr_diefu1sys(111, "linkat()"); 101 } 102 /* file with such name already exists, so try again with different one and atomically replace */ 103 char tmp_base[BUFLEN]; 104 tmp_base[0] = '.'; 105 tmp_base[1] = 't'; 106 tmp_base[2] = 'm'; 107 tmp_base[3] = 'p'; 108 tmp_base[4] = '.'; 109 random_name(&tmp_base[5], 58); 110 tmp_base[64] = 0; 111 if(linkat(old_fd, old_base, new_fd, tmp_base, 0) != 0) { 112 strerr_diefu2sys(111, "linkat() to temporary name: ", tmp_base); 113 } 114 if(renameat(new_fd, tmp_base, new_fd, new_base) != 0) { 115 strerr_diefu4sys(111, "renameat() from temporary name: ", tmp_base, " to:", new_base); 116 } 117 return 0; 118 } 119 /* vim: sw=2 sts=2 et 120 */