]> git.proxmox.com Git - libgit2.git/blob - src/win32/path_w32.c
New upstream version 1.4.3+dfsg.1
[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 "path_w32.h"
9
10 #include "fs_path.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_nt_namespace(p) \
22 (((p)[0] == '\\' && (p)[1] == '\\' && (p)[2] == '?' && (p)[3] == '\\') || \
23 ((p)[0] == '/' && (p)[1] == '/' && (p)[2] == '?' && (p)[3] == '/'))
24
25 #define path__is_unc(p) \
26 (((p)[0] == '\\' && (p)[1] == '\\') || ((p)[0] == '/' && (p)[1] == '/'))
27
28 #define path__startswith_slash(p) \
29 ((p)[0] == '\\' || (p)[0] == '/')
30
31 GIT_INLINE(int) path__cwd(wchar_t *path, int size)
32 {
33 int len;
34
35 if ((len = GetCurrentDirectoryW(size, path)) == 0) {
36 errno = GetLastError() == ERROR_ACCESS_DENIED ? EACCES : ENOENT;
37 return -1;
38 } else if (len > size) {
39 errno = ENAMETOOLONG;
40 return -1;
41 }
42
43 /* The Win32 APIs may return "\\?\" once you've used it first.
44 * But it may not. What a gloriously predictable API!
45 */
46 if (wcsncmp(path, PATH__NT_NAMESPACE, PATH__NT_NAMESPACE_LEN))
47 return len;
48
49 len -= PATH__NT_NAMESPACE_LEN;
50
51 memmove(path, path + PATH__NT_NAMESPACE_LEN, sizeof(wchar_t) * len);
52 return len;
53 }
54
55 static wchar_t *path__skip_server(wchar_t *path)
56 {
57 wchar_t *c;
58
59 for (c = path; *c; c++) {
60 if (git_fs_path_is_dirsep(*c))
61 return c + 1;
62 }
63
64 return c;
65 }
66
67 static wchar_t *path__skip_prefix(wchar_t *path)
68 {
69 if (path__is_nt_namespace(path)) {
70 path += PATH__NT_NAMESPACE_LEN;
71
72 if (wcsncmp(path, L"UNC\\", 4) == 0)
73 path = path__skip_server(path + 4);
74 else if (git_fs_path_is_absolute(path))
75 path += PATH__ABSOLUTE_LEN;
76 } else if (git_fs_path_is_absolute(path)) {
77 path += PATH__ABSOLUTE_LEN;
78 } else if (path__is_unc(path)) {
79 path = path__skip_server(path + 2);
80 }
81
82 return path;
83 }
84
85 int git_win32_path_canonicalize(git_win32_path path)
86 {
87 wchar_t *base, *from, *to, *next;
88 size_t len;
89
90 base = to = path__skip_prefix(path);
91
92 /* Unposixify if the prefix */
93 for (from = path; from < to; from++) {
94 if (*from == L'/')
95 *from = L'\\';
96 }
97
98 while (*from) {
99 for (next = from; *next; ++next) {
100 if (*next == L'/') {
101 *next = L'\\';
102 break;
103 }
104
105 if (*next == L'\\')
106 break;
107 }
108
109 len = next - from;
110
111 if (len == 1 && from[0] == L'.')
112 /* do nothing with singleton dot */;
113
114 else if (len == 2 && from[0] == L'.' && from[1] == L'.') {
115 if (to == base) {
116 /* no more path segments to strip, eat the "../" */
117 if (*next == L'\\')
118 len++;
119
120 base = to;
121 } else {
122 /* back up a path segment */
123 while (to > base && to[-1] == L'\\') to--;
124 while (to > base && to[-1] != L'\\') to--;
125 }
126 } else {
127 if (*next == L'\\' && *from != L'\\')
128 len++;
129
130 if (to != from)
131 memmove(to, from, sizeof(wchar_t) * len);
132
133 to += len;
134 }
135
136 from += len;
137
138 while (*from == L'\\') from++;
139 }
140
141 /* Strip trailing backslashes */
142 while (to > base && to[-1] == L'\\') to--;
143
144 *to = L'\0';
145
146 if ((to - path) > INT_MAX) {
147 SetLastError(ERROR_FILENAME_EXCED_RANGE);
148 return -1;
149 }
150
151 return (int)(to - path);
152 }
153
154 static int git_win32_path_join(
155 git_win32_path dest,
156 const wchar_t *one,
157 size_t one_len,
158 const wchar_t *two,
159 size_t two_len)
160 {
161 size_t backslash = 0;
162
163 if (one_len && two_len && one[one_len - 1] != L'\\')
164 backslash = 1;
165
166 if (one_len + two_len + backslash > MAX_PATH) {
167 git_error_set(GIT_ERROR_INVALID, "path too long");
168 return -1;
169 }
170
171 memmove(dest, one, one_len * sizeof(wchar_t));
172
173 if (backslash)
174 dest[one_len] = L'\\';
175
176 memcpy(dest + one_len + backslash, two, two_len * sizeof(wchar_t));
177 dest[one_len + backslash + two_len] = L'\0';
178
179 return 0;
180 }
181
182 struct win32_path_iter {
183 wchar_t *env;
184 const wchar_t *current_dir;
185 };
186
187 static int win32_path_iter_init(struct win32_path_iter *iter)
188 {
189 DWORD len = GetEnvironmentVariableW(L"PATH", NULL, 0);
190
191 if (!len && GetLastError() == ERROR_ENVVAR_NOT_FOUND) {
192 iter->env = NULL;
193 iter->current_dir = NULL;
194 return 0;
195 } else if (!len) {
196 git_error_set(GIT_ERROR_OS, "could not load PATH");
197 return -1;
198 }
199
200 iter->env = git__malloc(len * sizeof(wchar_t));
201 GIT_ERROR_CHECK_ALLOC(iter->env);
202
203 len = GetEnvironmentVariableW(L"PATH", iter->env, len);
204
205 if (len == 0) {
206 git_error_set(GIT_ERROR_OS, "could not load PATH");
207 return -1;
208 }
209
210 iter->current_dir = iter->env;
211 return 0;
212 }
213
214 static int win32_path_iter_next(
215 const wchar_t **out,
216 size_t *out_len,
217 struct win32_path_iter *iter)
218 {
219 const wchar_t *start;
220 wchar_t term;
221 size_t len = 0;
222
223 if (!iter->current_dir || !*iter->current_dir)
224 return GIT_ITEROVER;
225
226 term = (*iter->current_dir == L'"') ? *iter->current_dir++ : L';';
227 start = iter->current_dir;
228
229 while (*iter->current_dir && *iter->current_dir != term) {
230 iter->current_dir++;
231 len++;
232 }
233
234 *out = start;
235 *out_len = len;
236
237 if (term == L'"' && *iter->current_dir)
238 iter->current_dir++;
239
240 while (*iter->current_dir == L';')
241 iter->current_dir++;
242
243 return 0;
244 }
245
246 static void win32_path_iter_dispose(struct win32_path_iter *iter)
247 {
248 if (!iter)
249 return;
250
251 git__free(iter->env);
252 iter->env = NULL;
253 iter->current_dir = NULL;
254 }
255
256 int git_win32_path_find_executable(git_win32_path fullpath, wchar_t *exe)
257 {
258 struct win32_path_iter path_iter;
259 const wchar_t *dir;
260 size_t dir_len, exe_len = wcslen(exe);
261 bool found = false;
262
263 if (win32_path_iter_init(&path_iter) < 0)
264 return -1;
265
266 while (win32_path_iter_next(&dir, &dir_len, &path_iter) != GIT_ITEROVER) {
267 if (git_win32_path_join(fullpath, dir, dir_len, exe, exe_len) < 0)
268 continue;
269
270 if (_waccess(fullpath, 0) == 0) {
271 found = true;
272 break;
273 }
274 }
275
276 win32_path_iter_dispose(&path_iter);
277
278 if (found)
279 return 0;
280
281 fullpath[0] = L'\0';
282 return GIT_ENOTFOUND;
283 }
284
285 static int win32_path_cwd(wchar_t *out, size_t len)
286 {
287 int cwd_len;
288
289 if (len > INT_MAX) {
290 errno = ENAMETOOLONG;
291 return -1;
292 }
293
294 if ((cwd_len = path__cwd(out, (int)len)) < 0)
295 return -1;
296
297 /* UNC paths */
298 if (wcsncmp(L"\\\\", out, 2) == 0) {
299 /* Our buffer must be at least 5 characters larger than the
300 * current working directory: we swallow one of the leading
301 * '\'s, but we we add a 'UNC' specifier to the path, plus
302 * a trailing directory separator, plus a NUL.
303 */
304 if (cwd_len > GIT_WIN_PATH_MAX - 4) {
305 errno = ENAMETOOLONG;
306 return -1;
307 }
308
309 memmove(out+2, out, sizeof(wchar_t) * cwd_len);
310 out[0] = L'U';
311 out[1] = L'N';
312 out[2] = L'C';
313
314 cwd_len += 2;
315 }
316
317 /* Our buffer must be at least 2 characters larger than the current
318 * working directory. (One character for the directory separator,
319 * one for the null.
320 */
321 else if (cwd_len > GIT_WIN_PATH_MAX - 2) {
322 errno = ENAMETOOLONG;
323 return -1;
324 }
325
326 return cwd_len;
327 }
328
329 int git_win32_path_from_utf8(git_win32_path out, const char *src)
330 {
331 wchar_t *dest = out;
332
333 /* All win32 paths are in NT-prefixed format, beginning with "\\?\". */
334 memcpy(dest, PATH__NT_NAMESPACE, sizeof(wchar_t) * PATH__NT_NAMESPACE_LEN);
335 dest += PATH__NT_NAMESPACE_LEN;
336
337 /* See if this is an absolute path (beginning with a drive letter) */
338 if (git_fs_path_is_absolute(src)) {
339 if (git__utf8_to_16(dest, GIT_WIN_PATH_MAX, src) < 0)
340 goto on_error;
341 }
342 /* File-prefixed NT-style paths beginning with \\?\ */
343 else if (path__is_nt_namespace(src)) {
344 /* Skip the NT prefix, the destination already contains it */
345 if (git__utf8_to_16(dest, GIT_WIN_PATH_MAX, src + PATH__NT_NAMESPACE_LEN) < 0)
346 goto on_error;
347 }
348 /* UNC paths */
349 else if (path__is_unc(src)) {
350 memcpy(dest, L"UNC\\", sizeof(wchar_t) * 4);
351 dest += 4;
352
353 /* Skip the leading "\\" */
354 if (git__utf8_to_16(dest, GIT_WIN_PATH_MAX - 2, src + 2) < 0)
355 goto on_error;
356 }
357 /* Absolute paths omitting the drive letter */
358 else if (path__startswith_slash(src)) {
359 if (path__cwd(dest, GIT_WIN_PATH_MAX) < 0)
360 goto on_error;
361
362 if (!git_fs_path_is_absolute(dest)) {
363 errno = ENOENT;
364 goto on_error;
365 }
366
367 /* Skip the drive letter specification ("C:") */
368 if (git__utf8_to_16(dest + 2, GIT_WIN_PATH_MAX - 2, src) < 0)
369 goto on_error;
370 }
371 /* Relative paths */
372 else {
373 int cwd_len;
374
375 if ((cwd_len = win32_path_cwd(dest, GIT_WIN_PATH_MAX)) < 0)
376 goto on_error;
377
378 dest[cwd_len++] = L'\\';
379
380 if (git__utf8_to_16(dest + cwd_len, GIT_WIN_PATH_MAX - cwd_len, src) < 0)
381 goto on_error;
382 }
383
384 return git_win32_path_canonicalize(out);
385
386 on_error:
387 /* set windows error code so we can use its error message */
388 if (errno == ENAMETOOLONG)
389 SetLastError(ERROR_FILENAME_EXCED_RANGE);
390
391 return -1;
392 }
393
394 int git_win32_path_relative_from_utf8(git_win32_path out, const char *src)
395 {
396 wchar_t *dest = out, *p;
397 int len;
398
399 /* Handle absolute paths */
400 if (git_fs_path_is_absolute(src) ||
401 path__is_nt_namespace(src) ||
402 path__is_unc(src) ||
403 path__startswith_slash(src)) {
404 return git_win32_path_from_utf8(out, src);
405 }
406
407 if ((len = git__utf8_to_16(dest, GIT_WIN_PATH_MAX, src)) < 0)
408 return -1;
409
410 for (p = dest; p < (dest + len); p++) {
411 if (*p == L'/')
412 *p = L'\\';
413 }
414
415 return len;
416 }
417
418 int git_win32_path_to_utf8(git_win32_utf8_path dest, const wchar_t *src)
419 {
420 char *out = dest;
421 int len;
422
423 /* Strip NT namespacing "\\?\" */
424 if (path__is_nt_namespace(src)) {
425 src += 4;
426
427 /* "\\?\UNC\server\share" -> "\\server\share" */
428 if (wcsncmp(src, L"UNC\\", 4) == 0) {
429 src += 4;
430
431 memcpy(dest, "\\\\", 2);
432 out = dest + 2;
433 }
434 }
435
436 if ((len = git__utf16_to_8(out, GIT_WIN_PATH_UTF8, src)) < 0)
437 return len;
438
439 git_fs_path_mkposix(dest);
440
441 return len;
442 }
443
444 char *git_win32_path_8dot3_name(const char *path)
445 {
446 git_win32_path longpath, shortpath;
447 wchar_t *start;
448 char *shortname;
449 int len, namelen = 1;
450
451 if (git_win32_path_from_utf8(longpath, path) < 0)
452 return NULL;
453
454 len = GetShortPathNameW(longpath, shortpath, GIT_WIN_PATH_UTF16);
455
456 while (len && shortpath[len-1] == L'\\')
457 shortpath[--len] = L'\0';
458
459 if (len == 0 || len >= GIT_WIN_PATH_UTF16)
460 return NULL;
461
462 for (start = shortpath + (len - 1);
463 start > shortpath && *(start-1) != '/' && *(start-1) != '\\';
464 start--)
465 namelen++;
466
467 /* We may not have actually been given a short name. But if we have,
468 * it will be in the ASCII byte range, so we don't need to worry about
469 * multi-byte sequences and can allocate naively.
470 */
471 if (namelen > 12 || (shortname = git__malloc(namelen + 1)) == NULL)
472 return NULL;
473
474 if ((len = git__utf16_to_8(shortname, namelen + 1, start)) < 0)
475 return NULL;
476
477 return shortname;
478 }
479
480 static bool path_is_volume(wchar_t *target, size_t target_len)
481 {
482 return (target_len && wcsncmp(target, L"\\??\\Volume{", 11) == 0);
483 }
484
485 /* On success, returns the length, in characters, of the path stored in dest.
486 * On failure, returns a negative value. */
487 int git_win32_path_readlink_w(git_win32_path dest, const git_win32_path path)
488 {
489 BYTE buf[MAXIMUM_REPARSE_DATA_BUFFER_SIZE];
490 GIT_REPARSE_DATA_BUFFER *reparse_buf = (GIT_REPARSE_DATA_BUFFER *)buf;
491 HANDLE handle = NULL;
492 DWORD ioctl_ret;
493 wchar_t *target;
494 size_t target_len;
495
496 int error = -1;
497
498 handle = CreateFileW(path, GENERIC_READ,
499 FILE_SHARE_READ | FILE_SHARE_DELETE, NULL, OPEN_EXISTING,
500 FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS, NULL);
501
502 if (handle == INVALID_HANDLE_VALUE) {
503 errno = ENOENT;
504 return -1;
505 }
506
507 if (!DeviceIoControl(handle, FSCTL_GET_REPARSE_POINT, NULL, 0,
508 reparse_buf, sizeof(buf), &ioctl_ret, NULL)) {
509 errno = EINVAL;
510 goto on_error;
511 }
512
513 switch (reparse_buf->ReparseTag) {
514 case IO_REPARSE_TAG_SYMLINK:
515 target = reparse_buf->ReparseBuffer.SymbolicLink.PathBuffer +
516 (reparse_buf->ReparseBuffer.SymbolicLink.SubstituteNameOffset / sizeof(WCHAR));
517 target_len = reparse_buf->ReparseBuffer.SymbolicLink.SubstituteNameLength / sizeof(WCHAR);
518 break;
519 case IO_REPARSE_TAG_MOUNT_POINT:
520 target = reparse_buf->ReparseBuffer.MountPoint.PathBuffer +
521 (reparse_buf->ReparseBuffer.MountPoint.SubstituteNameOffset / sizeof(WCHAR));
522 target_len = reparse_buf->ReparseBuffer.MountPoint.SubstituteNameLength / sizeof(WCHAR);
523 break;
524 default:
525 errno = EINVAL;
526 goto on_error;
527 }
528
529 if (path_is_volume(target, target_len)) {
530 /* This path is a reparse point that represents another volume mounted
531 * at this location, it is not a symbolic link our input was canonical.
532 */
533 errno = EINVAL;
534 error = -1;
535 } else if (target_len) {
536 /* The path may need to have a namespace prefix removed. */
537 target_len = git_win32_path_remove_namespace(target, target_len);
538
539 /* Need one additional character in the target buffer
540 * for the terminating NULL. */
541 if (GIT_WIN_PATH_UTF16 > target_len) {
542 wcscpy(dest, target);
543 error = (int)target_len;
544 }
545 }
546
547 on_error:
548 CloseHandle(handle);
549 return error;
550 }
551
552 /**
553 * Removes any trailing backslashes from a path, except in the case of a drive
554 * letter path (C:\, D:\, etc.). This function cannot fail.
555 *
556 * @param path The path which should be trimmed.
557 * @return The length of the modified string (<= the input length)
558 */
559 size_t git_win32_path_trim_end(wchar_t *str, size_t len)
560 {
561 while (1) {
562 if (!len || str[len - 1] != L'\\')
563 break;
564
565 /*
566 * Don't trim backslashes from drive letter paths, which
567 * are 3 characters long and of the form C:\, D:\, etc.
568 */
569 if (len == 3 && git_win32__isalpha(str[0]) && str[1] == ':')
570 break;
571
572 len--;
573 }
574
575 str[len] = L'\0';
576
577 return len;
578 }
579
580 /**
581 * Removes any of the following namespace prefixes from a path,
582 * if found: "\??\", "\\?\", "\\?\UNC\". This function cannot fail.
583 *
584 * @param path The path which should be converted.
585 * @return The length of the modified string (<= the input length)
586 */
587 size_t git_win32_path_remove_namespace(wchar_t *str, size_t len)
588 {
589 static const wchar_t dosdevices_namespace[] = L"\\\?\?\\";
590 static const wchar_t nt_namespace[] = L"\\\\?\\";
591 static const wchar_t unc_namespace_remainder[] = L"UNC\\";
592 static const wchar_t unc_prefix[] = L"\\\\";
593
594 const wchar_t *prefix = NULL, *remainder = NULL;
595 size_t prefix_len = 0, remainder_len = 0;
596
597 /* "\??\" -- DOS Devices prefix */
598 if (len >= CONST_STRLEN(dosdevices_namespace) &&
599 !wcsncmp(str, dosdevices_namespace, CONST_STRLEN(dosdevices_namespace))) {
600 remainder = str + CONST_STRLEN(dosdevices_namespace);
601 remainder_len = len - CONST_STRLEN(dosdevices_namespace);
602 }
603 /* "\\?\" -- NT namespace prefix */
604 else if (len >= CONST_STRLEN(nt_namespace) &&
605 !wcsncmp(str, nt_namespace, CONST_STRLEN(nt_namespace))) {
606 remainder = str + CONST_STRLEN(nt_namespace);
607 remainder_len = len - CONST_STRLEN(nt_namespace);
608 }
609
610 /* "\??\UNC\", "\\?\UNC\" -- UNC prefix */
611 if (remainder_len >= CONST_STRLEN(unc_namespace_remainder) &&
612 !wcsncmp(remainder, unc_namespace_remainder, CONST_STRLEN(unc_namespace_remainder))) {
613
614 /*
615 * The proper Win32 path for a UNC share has "\\" at beginning of it
616 * and looks like "\\server\share\<folderStructure>". So remove the
617 * UNC namespace and add a prefix of "\\" in its place.
618 */
619 remainder += CONST_STRLEN(unc_namespace_remainder);
620 remainder_len -= CONST_STRLEN(unc_namespace_remainder);
621
622 prefix = unc_prefix;
623 prefix_len = CONST_STRLEN(unc_prefix);
624 }
625
626 /*
627 * Sanity check that the new string isn't longer than the old one.
628 * (This could only happen due to programmer error introducing a
629 * prefix longer than the namespace it replaces.)
630 */
631 if (remainder && len >= remainder_len + prefix_len) {
632 if (prefix)
633 memmove(str, prefix, prefix_len * sizeof(wchar_t));
634
635 memmove(str + prefix_len, remainder, remainder_len * sizeof(wchar_t));
636
637 len = remainder_len + prefix_len;
638 str[len] = L'\0';
639 }
640
641 return git_win32_path_trim_end(str, len);
642 }