1 /*******************************************************************************
2 Copyright (c) 2014 Vladimir Kondratyev <vladimir@kondratyev.su>
3 SPDX-License-Identifier: MIT
4
5 Permission is hereby granted, free of charge, to any person obtaining a copy
6 of this software and associated documentation files (the "Software"), to deal
7 in the Software without restriction, including without limitation the rights
8 to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 copies of the Software, and to permit persons to whom the Software is
10 furnished to do so, subject to the following conditions:
11
12 The above copyright notice and this permission notice shall be included in
13 all copies or substantial portions of the Software.
14
15 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 THE SOFTWARE.
22 *******************************************************************************/
23
24 #include <sys/param.h> /* MAXPATHLEN */
25 #include <sys/types.h>
26 #include <sys/stat.h> /* stat */
27
28 #include <assert.h>
29 #include <dirent.h> /* opendir */
30 #include <errno.h> /* errno */
31 #include <fcntl.h> /* fcntl */
32 #include <limits.h> /* PATH_MAX */
33 #include <stdlib.h> /* malloc */
34 #include <string.h> /* memset */
35 #include <unistd.h> /* fchdir */
36
37 #include "compat.h"
38 #include "config.h"
39
40 typedef struct dirpath_t {
41 ino_t inode; /* inode number */
42 dev_t dev; /* device number */
43 char *path; /* full path to inode */
44 RB_ENTRY(dirpath_t) link; /* RB tree links */
45 } dirpath_t;
46
47 /* directory path cache */
48 static RB_HEAD(dp, dirpath_t) dirs = RB_INITIALIZER (&dirs);
49 /* directory path cache mutex */
50 static pthread_mutex_t dirs_mtx = PTHREAD_MUTEX_INITIALIZER;
51
52 /**
53 * Custom comparison function that can compare directory inode values
54 * through pointers passed by RB tree functions
55 *
56 * @param[in] dp1 A pointer to a first directory to compare
57 * @param[in] dp2 A pointer to a second directory to compare
58 * @return An -1, 0, or +1 if the first inode is considered to be respectively
59 * less than, equal to, or greater than the second one.
60 **/
61 static int
dirpath_cmp(dirpath_t * dp1,dirpath_t * dp2)62 dirpath_cmp (dirpath_t *dp1, dirpath_t *dp2)
63 {
64 if (dp1->dev == dp2->dev)
65 return ((dp1->inode > dp2->inode) - (dp1->inode < dp2->inode));
66 else
67 return ((dp1->dev > dp2->dev) - (dp1->dev < dp2->dev));
68 }
69
70 RB_GENERATE(dp, dirpath_t, link, dirpath_cmp);
71
72 /**
73 * Returns a pointer to the absolute pathname of the directory by filedes
74 * NOTE: It uses unsafe fchdir if no F_GETPATH fcntl have been found
75 *
76 * @param[in] fd A file descriptor of opened directory
77 * @return a pointer to the pathname on success, NULL otherwise
78 **/
79 static char *
fd_getpath(int fd)80 fd_getpath (int fd)
81 {
82 assert (fd != -1);
83
84 char *path = NULL;
85 struct stat st;
86
87 if (fstat (fd, &st) == -1) {
88 return NULL;
89 }
90
91 if (!S_ISDIR (st.st_mode)) {
92 errno = ENOTDIR;
93 return NULL;
94 }
95
96 if (st.st_nlink == 0) {
97 errno = ENOENT;
98 return NULL;
99 }
100
101 #if defined (F_GETPATH)
102 path = malloc (MAXPATHLEN);
103 if (path != NULL && fcntl (fd, F_GETPATH, path) == -1) {
104 free (path);
105 path = NULL;
106 }
107 #elif defined (ENABLE_UNSAFE_FCHDIR)
108 /*
109 * Using of this code path can be unsafe in multithreading applications as
110 * following code do a temporary change of global current working directory
111 * via fchdir call. Consider renaming of watched directory as relatively
112 * rare operation so catching such a race is unlikely
113 */
114 DIR *save = opendir(".");
115
116 if (fchdir (fd) == 0) {
117 path = malloc (PATH_MAX);
118 if (path != NULL && getcwd (path, PATH_MAX) == NULL) {
119 free (path);
120 path = NULL;
121 }
122 }
123
124 if (save != NULL) {
125 int saved_errno = errno;
126 fchdir (dirfd (save));
127 closedir (save);
128 errno = saved_errno;
129 }
130 #endif /* ENABLE_UNSAFE_FCHDIR && F_GETPATH */
131
132 return path;
133 }
134
135 /**
136 * Remove directory path from directory path cache.
137 * Should be called with dirs_mtx mutex held
138 *
139 * @param [in] dir A pointer to a directory to remove from cache
140 **/
141 static void
dir_remove(dirpath_t * dir)142 dir_remove (dirpath_t *dir)
143 {
144 if (dir->path != NULL) {
145 free (dir->path);
146 }
147 RB_REMOVE (dp, &dirs, dir);
148 free (dir);
149 }
150
151 /**
152 * Insert directory path into directory path cache.
153 * Should be called with dirs_mtx mutex held
154 *
155 * @param [in] path A directory path to be cached
156 * @param [in] inode A inode number of cached directory path
157 * @param [in] dev A device number of cached directory path
158 * @return A pointer to allocated cache entry
159 **/
160 static dirpath_t *
dir_insert(const char * path,ino_t inode,dev_t dev)161 dir_insert (const char* path, ino_t inode, dev_t dev)
162 {
163 assert (path != NULL);
164
165 dirpath_t *newdp, *olddp;
166
167 newdp = calloc (1, sizeof (dirpath_t));
168 if (newdp == NULL) {
169 return NULL;
170 }
171
172 newdp->inode = inode;
173 newdp->dev = dev;
174
175 olddp = RB_FIND (dp, &dirs, newdp);
176 if (olddp != NULL) {
177 free (newdp);
178 if (strcmp (path, olddp->path)) {
179 free (olddp->path);
180 olddp->path = strdup (path);
181 }
182 newdp = olddp;
183 } else {
184 newdp->path = strdup (path);
185 RB_INSERT (dp, &dirs, newdp);
186 }
187
188 if (newdp->path == NULL) {
189 dir_remove (newdp);
190 newdp = NULL;
191 }
192
193 return newdp;
194 }
195
196 /**
197 * Find cached directory path corresponding a given inode number
198 *
199 * @param[in] fd A file descriptor of opened directory
200 * @return a pointer to the pathname on success, NULL otherwise
201 **/
202 char *
fd_getpath_cached(int fd)203 fd_getpath_cached (int fd)
204 {
205 assert (fd != -1);
206
207 dirpath_t find, *dir;
208 struct stat st1, st2;
209 char *path;
210
211 if (fstat (fd, &st1) == -1) {
212 return NULL;
213 }
214
215 if (!S_ISDIR (st1.st_mode)) {
216 errno = ENOTDIR;
217 return NULL;
218 }
219
220 find.inode = st1.st_ino;
221 find.dev = st1.st_dev;
222
223 pthread_mutex_lock (&dirs_mtx);
224
225 dir = RB_FIND (dp, &dirs, &find);
226 if (dir == NULL || stat (dir->path, &st2) != 0
227 || dir->inode != st2.st_ino || dir->dev != st2.st_dev) {
228
229 path = fd_getpath (fd);
230 if (path != NULL) {
231 dir = dir_insert (path, st1.st_ino, st1.st_dev);
232 free (path);
233 } else {
234 if (dir != NULL) {
235 dir_remove (dir);
236 dir = NULL;
237 }
238 }
239 }
240
241 pthread_mutex_unlock (&dirs_mtx);
242
243 return dir != NULL ? dir->path : NULL;
244 }
245
246 /**
247 * Create a file path using its name and a path to its directory.
248 *
249 * @param[in] dir A path to a file directory. May end with a '/'.
250 * @param[in] file File name.
251 * @return A concatenated path. Should be freed with free().
252 **/
253 static char*
path_concat(const char * dir,const char * file)254 path_concat (const char *dir, const char *file)
255 {
256 assert (dir != NULL);
257 assert (file != NULL);
258
259 size_t dir_len, file_len, alloc_sz;
260 char *path;
261
262 dir_len = strlen (dir);
263 file_len = strlen (file);
264 alloc_sz = dir_len + file_len + 2;
265
266 path = malloc (alloc_sz);
267 if (path != NULL) {
268
269 strlcpy (path, dir, alloc_sz);
270
271 if (dir[dir_len - 1] != '/') {
272 ++dir_len;
273 path[dir_len - 1] = '/';
274 }
275
276 strlcpy (path + dir_len, file, file_len + 1);
277 }
278
279 return path;
280 }
281
282 /**
283 * Create a file path using its name and a filedes of its directory.
284 *
285 * @param[in] fd A file descriptor of opened directory.
286 * @param[in] file File name.
287 * @return A concatenated path. Should be freed with free().
288 **/
289 char*
fd_concat(int fd,const char * file)290 fd_concat (int fd, const char *file)
291 {
292 char *path = NULL;
293 struct stat st;
294
295 if (fd == AT_FDCWD || file[0] == '/') {
296
297 if (stat (file, &st) != -1
298 && S_ISDIR (st.st_mode)
299 && (path = malloc (PATH_MAX + 1)) != NULL
300 && realpath (file, path) == path) {
301
302 pthread_mutex_lock (&dirs_mtx);
303 dir_insert (path, st.st_ino, st.st_dev);
304 pthread_mutex_unlock (&dirs_mtx);
305 } else {
306 if (path == NULL) {
307 path = strdup (file);
308 } else {
309 strlcpy (path, file, PATH_MAX + 1);
310 }
311 }
312 } else {
313
314 char *dirpath = fd_getpath_cached (fd);
315 if (dirpath != NULL) {
316 path = path_concat (dirpath, file);
317 }
318 }
319
320 return path;
321 }
322