fchdir: fix logic bugs
[gnulib.git] / lib / fchdir.c
1 /* fchdir replacement.
2    Copyright (C) 2006-2009 Free Software Foundation, Inc.
3
4    This program is free software: you can redistribute it and/or modify
5    it under the terms of the GNU General Public License as published by
6    the Free Software Foundation; either version 3 of the License, or
7    (at your option) any later version.
8
9    This program is distributed in the hope that it will be useful,
10    but WITHOUT ANY WARRANTY; without even the implied warranty of
11    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12    GNU General Public License for more details.
13
14    You should have received a copy of the GNU General Public License
15    along with this program.  If not, see <http://www.gnu.org/licenses/>.  */
16
17 #include <config.h>
18
19 /* Specification.  */
20 #include <unistd.h>
21
22 #include <assert.h>
23 #include <dirent.h>
24 #include <errno.h>
25 #include <fcntl.h>
26 #include <stdbool.h>
27 #include <stdlib.h>
28 #include <string.h>
29 #include <sys/types.h>
30 #include <sys/stat.h>
31
32 #ifndef REPLACE_OPEN_DIRECTORY
33 # define REPLACE_OPEN_DIRECTORY 0
34 #endif
35
36 #ifndef HAVE_CANONICALIZE_FILE_NAME
37 # if GNULIB_CANONICALIZE || GNULIB_CANONICALIZE_LGPL
38 #  define HAVE_CANONICALIZE_FILE_NAME 1
39 # else
40 #  define HAVE_CANONICALIZE_FILE_NAME 0
41 #  define canonicalize_file_name(name) NULL
42 # endif
43 #endif
44
45 /* This replacement assumes that a directory is not renamed while opened
46    through a file descriptor.
47
48    FIXME: On mingw, this would be possible to enforce if we were to
49    also open a HANDLE to each directory currently visited by a file
50    descriptor, since mingw refuses to rename any in-use file system
51    object.  */
52
53 /* Array of file descriptors opened.  If it points to a directory, it stores
54    info about this directory.  */
55 typedef struct
56 {
57   char *name;       /* Absolute name of the directory, or NULL.  */
58   /* FIXME - add a DIR* member to make dirfd possible on mingw?  */
59 } dir_info_t;
60 static dir_info_t *dirs;
61 static size_t dirs_allocated;
62
63 /* Try to ensure dirs has enough room for a slot at index fd.  Return
64    false and set errno to ENOMEM on allocation failure.  */
65 static bool
66 ensure_dirs_slot (size_t fd)
67 {
68   if (fd >= dirs_allocated)
69     {
70       size_t new_allocated;
71       dir_info_t *new_dirs;
72
73       new_allocated = 2 * dirs_allocated + 1;
74       if (new_allocated <= fd)
75         new_allocated = fd + 1;
76       new_dirs =
77         (dirs != NULL
78          ? (dir_info_t *) realloc (dirs, new_allocated * sizeof *dirs)
79          : (dir_info_t *) malloc (new_allocated * sizeof *dirs));
80       if (new_dirs == NULL)
81         return false;
82       memset (new_dirs + dirs_allocated, 0,
83               (new_allocated - dirs_allocated) * sizeof *dirs);
84       dirs = new_dirs;
85       dirs_allocated = new_allocated;
86     }
87   return true;
88 }
89
90 /* Return the canonical name of DIR in malloc'd storage.  */
91 static char *
92 get_name (char const *dir)
93 {
94   char *result;
95   if (REPLACE_OPEN_DIRECTORY || !HAVE_CANONICALIZE_FILE_NAME)
96     {
97       /* The function canonicalize_file_name has not yet been ported
98          to mingw, with all its drive letter and backslash quirks.
99          Fortunately, getcwd is reliable in this case, but we ensure
100          we can get back to where we started before using it.  Treat
101          "." as a special case, as it is frequently encountered.  */
102       char *cwd = getcwd (NULL, 0);
103       int saved_errno;
104       if (dir[0] == '.' && dir[1] == '\0')
105         return cwd;
106       if (chdir (cwd))
107         return NULL;
108       result = chdir (dir) ? NULL : getcwd (NULL, 0);
109       saved_errno = errno;
110       if (chdir (cwd))
111         abort ();
112       free (cwd);
113       errno = saved_errno;
114     }
115   else
116     {
117       /* Avoid changing the directory.  */
118       result = canonicalize_file_name (dir);
119     }
120   return result;
121 }
122
123 /* Hook into the gnulib replacements for open() and close() to keep track
124    of the open file descriptors.  */
125
126 /* Close FD, cleaning up any fd to name mapping if fd was visiting a
127    directory.  */
128 void
129 _gl_unregister_fd (int fd)
130 {
131   if (fd >= 0 && fd < dirs_allocated)
132     {
133       free (dirs[fd].name);
134       dirs[fd].name = NULL;
135     }
136 }
137
138 /* Mark FD as visiting FILENAME.  FD must be non-negative, and refer
139    to an open file descriptor.  If REPLACE_OPEN_DIRECTORY is non-zero,
140    this should only be called if FD is visiting a directory.  Close FD
141    and return -1 if there is insufficient memory to track the
142    directory name; otherwise return FD.  */
143 int
144 _gl_register_fd (int fd, const char *filename)
145 {
146   struct stat statbuf;
147
148   assert (0 <= fd);
149   if (REPLACE_OPEN_DIRECTORY
150       || (fstat (fd, &statbuf) == 0 && S_ISDIR (statbuf.st_mode)))
151     {
152       if (!ensure_dirs_slot (fd)
153           || (dirs[fd].name = get_name (filename)) == NULL)
154         {
155           int saved_errno = errno;
156           close (fd);
157           errno = saved_errno;
158           return -1;
159         }
160     }
161   return fd;
162 }
163
164 /* Mark NEWFD as a duplicate of OLDFD; useful from dup, dup2, dup3,
165    and fcntl.  Both arguments must be valid and distinct file
166    descriptors.  Close NEWFD and return -1 if OLDFD is tracking a
167    directory, but there is insufficient memory to track the same
168    directory in NEWFD; otherwise return NEWFD.
169
170    FIXME: Need to implement rpl_fcntl in gnulib, and have it call
171    this.  */
172 int
173 _gl_register_dup (int oldfd, int newfd)
174 {
175   assert (0 <= oldfd && 0 <= newfd && oldfd != newfd);
176   if (oldfd < dirs_allocated && dirs[oldfd].name)
177     {
178       /* Duplicated a directory; must ensure newfd is allocated.  */
179       if (!ensure_dirs_slot (newfd)
180           || (dirs[newfd].name = strdup (dirs[oldfd].name)) == NULL)
181         {
182           int saved_errno = errno;
183           close (newfd);
184           errno = saved_errno;
185           newfd = -1;
186         }
187     }
188   else if (newfd < dirs_allocated)
189     {
190       /* Duplicated a non-directory; ensure newfd is cleared.  */
191       free (dirs[newfd].name);
192       dirs[newfd].name = NULL;
193     }
194   return newfd;
195 }
196
197 /* If FD is currently visiting a directory, then return the name of
198    that directory.  Otherwise, return NULL and set errno.  */
199 const char *
200 _gl_directory_name (int fd)
201 {
202   if (0 <= fd && fd < dirs_allocated && dirs[fd].name != NULL)
203     return dirs[fd].name;
204   /* At this point, fd is either invalid, or open but not a directory.
205      If dup2 fails, errno is correctly EBADF.  */
206   if (0 <= fd)
207     {
208       if (dup2 (fd, fd) == fd)
209         errno = ENOTDIR;
210     }
211   else
212     errno = EBADF;
213   return NULL;
214 }
215
216 /* Return stat information about FD in STATBUF.  Needed when
217    rpl_open() used a dummy file to work around an open() that can't
218    normally visit directories.  */
219 #undef fstat
220 int
221 rpl_fstat (int fd, struct stat *statbuf)
222 {
223   if (REPLACE_OPEN_DIRECTORY
224       && 0 <= fd && fd < dirs_allocated && dirs[fd].name != NULL)
225     return stat (dirs[fd].name, statbuf);
226   return fstat (fd, statbuf);
227 }
228
229 /* Override opendir() and closedir(), to keep track of the open file
230    descriptors.  Needed because there is a function dirfd().  */
231
232 int
233 rpl_closedir (DIR *dp)
234 #undef closedir
235 {
236   int fd = dirfd (dp);
237   int retval = closedir (dp);
238
239   if (retval >= 0)
240     _gl_unregister_fd (fd);
241   return retval;
242 }
243
244 DIR *
245 rpl_opendir (const char *filename)
246 #undef opendir
247 {
248   DIR *dp;
249
250   dp = opendir (filename);
251   if (dp != NULL)
252     {
253       int fd = dirfd (dp);
254       if (0 <= fd && _gl_register_fd (fd, filename) != fd)
255         {
256           int saved_errno = errno;
257           closedir (dp);
258           errno = saved_errno;
259           return NULL;
260         }
261     }
262   return dp;
263 }
264
265 /* Override dup(), to keep track of open file descriptors.  */
266
267 int
268 rpl_dup (int oldfd)
269 #undef dup
270 {
271   int newfd = dup (oldfd);
272
273   if (0 <= newfd)
274     newfd = _gl_register_dup (oldfd, newfd);
275   return newfd;
276 }
277
278
279 /* Implement fchdir() in terms of chdir().  */
280
281 int
282 fchdir (int fd)
283 {
284   const char *name = _gl_directory_name (fd);
285   return name ? chdir (name) : -1;
286 }