]> git.proxmox.com Git - libgit2.git/blob - src/win32/path_w32.c
win32: propogate filename too long errors
[libgit2.git] / src / win32 / path_w32.c
1 /*
2 * Copyright (C) the libgit2 contributors. All rights reserved.
3 *
4 * This file is part of libgit2, distributed under the GNU GPL v2 with
5 * a Linking Exception. For full terms see the included COPYING file.
6 */
7
8 #include "common.h"
9 #include "path.h"
10 #include "path_w32.h"
11 #include "utf-conv.h"
12 #include "posix.h"
13 #include "reparse.h"
14 #include "dir.h"
15
16 #define PATH__NT_NAMESPACE L"\\\\?\\"
17 #define PATH__NT_NAMESPACE_LEN 4
18
19 #define PATH__ABSOLUTE_LEN 3
20
21 #define path__is_dirsep(p) ((p) == '/' || (p) == '\\')
22
23 #define path__is_absolute(p) \
24 (git__isalpha((p)[0]) && (p)[1] == ':' && ((p)[2] == '\\' || (p)[2] == '/'))
25
26 #define path__is_nt_namespace(p) \
27 (((p)[0] == '\\' && (p)[1] == '\\' && (p)[2] == '?' && (p)[3] == '\\') || \
28 ((p)[0] == '/' && (p)[1] == '/' && (p)[2] == '?' && (p)[3] == '/'))
29
30 #define path__is_unc(p) \
31 (((p)[0] == '\\' && (p)[1] == '\\') || ((p)[0] == '/' && (p)[1] == '/'))
32
33 GIT_INLINE(int) path__cwd(wchar_t *path, int size)
34 {
35 int len;
36
37 if ((len = GetCurrentDirectoryW(size, path)) == 0) {
38 errno = GetLastError() == ERROR_ACCESS_DENIED ? EACCES : ENOENT;
39 return -1;
40 } else if (len > size) {
41 errno = ENAMETOOLONG;
42 return -1;
43 }
44
45 /* The Win32 APIs may return "\\?\" once you've used it first.
46 * But it may not. What a gloriously predictible API!
47 */
48 if (wcsncmp(path, PATH__NT_NAMESPACE, PATH__NT_NAMESPACE_LEN))
49 return len;
50
51 len -= PATH__NT_NAMESPACE_LEN;
52
53 memmove(path, path + PATH__NT_NAMESPACE_LEN, sizeof(wchar_t) * len);
54 return len;
55 }
56
57 static wchar_t *path__skip_server(wchar_t *path)
58 {
59 wchar_t *c;
60
61 for (c = path; *c; c++) {
62 if (path__is_dirsep(*c))
63 return c + 1;
64 }
65
66 return c;
67 }
68
69 static wchar_t *path__skip_prefix(wchar_t *path)
70 {
71 if (path__is_nt_namespace(path)) {
72 path += PATH__NT_NAMESPACE_LEN;
73
74 if (wcsncmp(path, L"UNC\\", 4) == 0)
75 path = path__skip_server(path + 4);
76 else if (path__is_absolute(path))
77 path += PATH__ABSOLUTE_LEN;
78 } else if (path__is_absolute(path)) {
79 path += PATH__ABSOLUTE_LEN;
80 } else if (path__is_unc(path)) {
81 path = path__skip_server(path + 2);
82 }
83
84 return path;
85 }
86
87 int git_win32_path_canonicalize(git_win32_path path)
88 {
89 wchar_t *base, *from, *to, *next;
90 size_t len;
91
92 base = to = path__skip_prefix(path);
93
94 /* Unposixify if the prefix */
95 for (from = path; from < to; from++) {
96 if (*from == L'/')
97 *from = L'\\';
98 }
99
100 while (*from) {
101 for (next = from; *next; ++next) {
102 if (*next == L'/') {
103 *next = L'\\';
104 break;
105 }
106
107 if (*next == L'\\')
108 break;
109 }
110
111 len = next - from;
112
113 if (len == 1 && from[0] == L'.')
114 /* do nothing with singleton dot */;
115
116 else if (len == 2 && from[0] == L'.' && from[1] == L'.') {
117 if (to == base) {
118 /* no more path segments to strip, eat the "../" */
119 if (*next == L'\\')
120 len++;
121
122 base = to;
123 } else {
124 /* back up a path segment */
125 while (to > base && to[-1] == L'\\') to--;
126 while (to > base && to[-1] != L'\\') to--;
127 }
128 } else {
129 if (*next == L'\\' && *from != L'\\')
130 len++;
131
132 if (to != from)
133 memmove(to, from, sizeof(wchar_t) * len);
134
135 to += len;
136 }
137
138 from += len;
139
140 while (*from == L'\\') from++;
141 }
142
143 /* Strip trailing backslashes */
144 while (to > base && to[-1] == L'\\') to--;
145
146 *to = L'\0';
147
148 return (to - path);
149 }
150
151 int git_win32_path__cwd(wchar_t *out, size_t len)
152 {
153 int cwd_len;
154
155 if ((cwd_len = path__cwd(out, len)) < 0)
156 return -1;
157
158 /* UNC paths */
159 if (wcsncmp(L"\\\\", out, 2) == 0) {
160 /* Our buffer must be at least 5 characters larger than the
161 * current working directory: we swallow one of the leading
162 * '\'s, but we we add a 'UNC' specifier to the path, plus
163 * a trailing directory separator, plus a NUL.
164 */
165 if (cwd_len > MAX_PATH - 4) {
166 errno = ENAMETOOLONG;
167 return -1;
168 }
169
170 memmove(out+2, out, sizeof(wchar_t) * cwd_len);
171 out[0] = L'U';
172 out[1] = L'N';
173 out[2] = L'C';
174
175 cwd_len += 2;
176 }
177
178 /* Our buffer must be at least 2 characters larger than the current
179 * working directory. (One character for the directory separator,
180 * one for the null.
181 */
182 else if (cwd_len > MAX_PATH - 2) {
183 errno = ENAMETOOLONG;
184 return -1;
185 }
186
187 return cwd_len;
188 }
189
190 int git_win32_path_from_utf8(git_win32_path out, const char *src)
191 {
192 wchar_t *dest = out;
193
194 /* All win32 paths are in NT-prefixed format, beginning with "\\?\". */
195 memcpy(dest, PATH__NT_NAMESPACE, sizeof(wchar_t) * PATH__NT_NAMESPACE_LEN);
196 dest += PATH__NT_NAMESPACE_LEN;
197
198 /* See if this is an absolute path (beginning with a drive letter) */
199 if (path__is_absolute(src)) {
200 if (git__utf8_to_16(dest, MAX_PATH, src) < 0)
201 goto on_error;
202 }
203 /* File-prefixed NT-style paths beginning with \\?\ */
204 else if (path__is_nt_namespace(src)) {
205 /* Skip the NT prefix, the destination already contains it */
206 if (git__utf8_to_16(dest, MAX_PATH, src + PATH__NT_NAMESPACE_LEN) < 0)
207 goto on_error;
208 }
209 /* UNC paths */
210 else if (path__is_unc(src)) {
211 memcpy(dest, L"UNC\\", sizeof(wchar_t) * 4);
212 dest += 4;
213
214 /* Skip the leading "\\" */
215 if (git__utf8_to_16(dest, MAX_PATH - 2, src + 2) < 0)
216 goto on_error;
217 }
218 /* Absolute paths omitting the drive letter */
219 else if (src[0] == '\\' || src[0] == '/') {
220 if (path__cwd(dest, MAX_PATH) < 0)
221 goto on_error;
222
223 if (!path__is_absolute(dest)) {
224 errno = ENOENT;
225 goto on_error;
226 }
227
228 /* Skip the drive letter specification ("C:") */
229 if (git__utf8_to_16(dest + 2, MAX_PATH - 2, src) < 0)
230 goto on_error;
231 }
232 /* Relative paths */
233 else {
234 int cwd_len;
235
236 if ((cwd_len = git_win32_path__cwd(dest, MAX_PATH)) < 0)
237 goto on_error;
238
239 dest[cwd_len++] = L'\\';
240
241 if (git__utf8_to_16(dest + cwd_len, MAX_PATH - cwd_len, src) < 0)
242 goto on_error;
243 }
244
245 return git_win32_path_canonicalize(out);
246
247 on_error:
248 /* set windows error code so we can use its error message */
249 if (errno == ENAMETOOLONG)
250 SetLastError(ERROR_FILENAME_EXCED_RANGE);
251
252 return -1;
253 }
254
255 int git_win32_path_to_utf8(git_win32_utf8_path dest, const wchar_t *src)
256 {
257 char *out = dest;
258 int len;
259
260 /* Strip NT namespacing "\\?\" */
261 if (path__is_nt_namespace(src)) {
262 src += 4;
263
264 /* "\\?\UNC\server\share" -> "\\server\share" */
265 if (wcsncmp(src, L"UNC\\", 4) == 0) {
266 src += 4;
267
268 memcpy(dest, "\\\\", 2);
269 out = dest + 2;
270 }
271 }
272
273 if ((len = git__utf16_to_8(out, GIT_WIN_PATH_UTF8, src)) < 0)
274 return len;
275
276 git_path_mkposix(dest);
277
278 return len;
279 }
280
281 char *git_win32_path_8dot3_name(const char *path)
282 {
283 git_win32_path longpath, shortpath;
284 wchar_t *start;
285 char *shortname;
286 int len, namelen = 1;
287
288 if (git_win32_path_from_utf8(longpath, path) < 0)
289 return NULL;
290
291 len = GetShortPathNameW(longpath, shortpath, GIT_WIN_PATH_UTF16);
292
293 while (len && shortpath[len-1] == L'\\')
294 shortpath[--len] = L'\0';
295
296 if (len == 0 || len >= GIT_WIN_PATH_UTF16)
297 return NULL;
298
299 for (start = shortpath + (len - 1);
300 start > shortpath && *(start-1) != '/' && *(start-1) != '\\';
301 start--)
302 namelen++;
303
304 /* We may not have actually been given a short name. But if we have,
305 * it will be in the ASCII byte range, so we don't need to worry about
306 * multi-byte sequences and can allocate naively.
307 */
308 if (namelen > 12 || (shortname = git__malloc(namelen + 1)) == NULL)
309 return NULL;
310
311 if ((len = git__utf16_to_8(shortname, namelen + 1, start)) < 0)
312 return NULL;
313
314 return shortname;
315 }
316
317 static bool path_is_volume(wchar_t *target, size_t target_len)
318 {
319 return (target_len && wcsncmp(target, L"\\??\\Volume{", 11) == 0);
320 }
321
322 /* On success, returns the length, in characters, of the path stored in dest.
323 * On failure, returns a negative value. */
324 int git_win32_path_readlink_w(git_win32_path dest, const git_win32_path path)
325 {
326 BYTE buf[MAXIMUM_REPARSE_DATA_BUFFER_SIZE];
327 GIT_REPARSE_DATA_BUFFER *reparse_buf = (GIT_REPARSE_DATA_BUFFER *)buf;
328 HANDLE handle = NULL;
329 DWORD ioctl_ret;
330 wchar_t *target;
331 size_t target_len;
332
333 int error = -1;
334
335 handle = CreateFileW(path, GENERIC_READ,
336 FILE_SHARE_READ | FILE_SHARE_DELETE, NULL, OPEN_EXISTING,
337 FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS, NULL);
338
339 if (handle == INVALID_HANDLE_VALUE) {
340 errno = ENOENT;
341 return -1;
342 }
343
344 if (!DeviceIoControl(handle, FSCTL_GET_REPARSE_POINT, NULL, 0,
345 reparse_buf, sizeof(buf), &ioctl_ret, NULL)) {
346 errno = EINVAL;
347 goto on_error;
348 }
349
350 switch (reparse_buf->ReparseTag) {
351 case IO_REPARSE_TAG_SYMLINK:
352 target = reparse_buf->SymbolicLinkReparseBuffer.PathBuffer +
353 (reparse_buf->SymbolicLinkReparseBuffer.SubstituteNameOffset / sizeof(WCHAR));
354 target_len = reparse_buf->SymbolicLinkReparseBuffer.SubstituteNameLength / sizeof(WCHAR);
355 break;
356 case IO_REPARSE_TAG_MOUNT_POINT:
357 target = reparse_buf->MountPointReparseBuffer.PathBuffer +
358 (reparse_buf->MountPointReparseBuffer.SubstituteNameOffset / sizeof(WCHAR));
359 target_len = reparse_buf->MountPointReparseBuffer.SubstituteNameLength / sizeof(WCHAR);
360 break;
361 default:
362 errno = EINVAL;
363 goto on_error;
364 }
365
366 if (path_is_volume(target, target_len)) {
367 /* This path is a reparse point that represents another volume mounted
368 * at this location, it is not a symbolic link our input was canonical.
369 */
370 errno = EINVAL;
371 error = -1;
372 } else if (target_len) {
373 /* The path may need to have a prefix removed. */
374 target_len = git_win32__canonicalize_path(target, target_len);
375
376 /* Need one additional character in the target buffer
377 * for the terminating NULL. */
378 if (GIT_WIN_PATH_UTF16 > target_len) {
379 wcscpy(dest, target);
380 error = (int)target_len;
381 }
382 }
383
384 on_error:
385 CloseHandle(handle);
386 return error;
387 }