2 * Copyright (C) the libgit2 contributors. All rights reserved.
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.
16 #define PATH__NT_NAMESPACE L"\\\\?\\"
17 #define PATH__NT_NAMESPACE_LEN 4
19 #define PATH__ABSOLUTE_LEN 3
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] == '/'))
25 #define path__is_unc(p) \
26 (((p)[0] == '\\' && (p)[1] == '\\') || ((p)[0] == '/' && (p)[1] == '/'))
28 #define path__startswith_slash(p) \
29 ((p)[0] == '\\' || (p)[0] == '/')
31 GIT_INLINE(int) path__cwd(wchar_t *path
, int size
)
35 if ((len
= GetCurrentDirectoryW(size
, path
)) == 0) {
36 errno
= GetLastError() == ERROR_ACCESS_DENIED
? EACCES
: ENOENT
;
38 } else if (len
> size
) {
43 /* The Win32 APIs may return "\\?\" once you've used it first.
44 * But it may not. What a gloriously predictable API!
46 if (wcsncmp(path
, PATH__NT_NAMESPACE
, PATH__NT_NAMESPACE_LEN
))
49 len
-= PATH__NT_NAMESPACE_LEN
;
51 memmove(path
, path
+ PATH__NT_NAMESPACE_LEN
, sizeof(wchar_t) * len
);
55 static wchar_t *path__skip_server(wchar_t *path
)
59 for (c
= path
; *c
; c
++) {
60 if (git_fs_path_is_dirsep(*c
))
67 static wchar_t *path__skip_prefix(wchar_t *path
)
69 if (path__is_nt_namespace(path
)) {
70 path
+= PATH__NT_NAMESPACE_LEN
;
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);
85 int git_win32_path_canonicalize(git_win32_path path
)
87 wchar_t *base
, *from
, *to
, *next
;
90 base
= to
= path__skip_prefix(path
);
92 /* Unposixify if the prefix */
93 for (from
= path
; from
< to
; from
++) {
99 for (next
= from
; *next
; ++next
) {
111 if (len
== 1 && from
[0] == L
'.')
112 /* do nothing with singleton dot */;
114 else if (len
== 2 && from
[0] == L
'.' && from
[1] == L
'.') {
116 /* no more path segments to strip, eat the "../" */
122 /* back up a path segment */
123 while (to
> base
&& to
[-1] == L
'\\') to
--;
124 while (to
> base
&& to
[-1] != L
'\\') to
--;
127 if (*next
== L
'\\' && *from
!= L
'\\')
131 memmove(to
, from
, sizeof(wchar_t) * len
);
138 while (*from
== L
'\\') from
++;
141 /* Strip trailing backslashes */
142 while (to
> base
&& to
[-1] == L
'\\') to
--;
146 if ((to
- path
) > INT_MAX
) {
147 SetLastError(ERROR_FILENAME_EXCED_RANGE
);
151 return (int)(to
- path
);
154 static int git_win32_path_join(
161 size_t backslash
= 0;
163 if (one_len
&& two_len
&& one
[one_len
- 1] != L
'\\')
166 if (one_len
+ two_len
+ backslash
> MAX_PATH
) {
167 git_error_set(GIT_ERROR_INVALID
, "path too long");
171 memmove(dest
, one
, one_len
* sizeof(wchar_t));
174 dest
[one_len
] = L
'\\';
176 memcpy(dest
+ one_len
+ backslash
, two
, two_len
* sizeof(wchar_t));
177 dest
[one_len
+ backslash
+ two_len
] = L
'\0';
182 struct win32_path_iter
{
184 const wchar_t *current_dir
;
187 static int win32_path_iter_init(struct win32_path_iter
*iter
)
189 DWORD len
= GetEnvironmentVariableW(L
"PATH", NULL
, 0);
191 if (!len
&& GetLastError() == ERROR_ENVVAR_NOT_FOUND
) {
193 iter
->current_dir
= NULL
;
196 git_error_set(GIT_ERROR_OS
, "could not load PATH");
200 iter
->env
= git__malloc(len
* sizeof(wchar_t));
201 GIT_ERROR_CHECK_ALLOC(iter
->env
);
203 len
= GetEnvironmentVariableW(L
"PATH", iter
->env
, len
);
206 git_error_set(GIT_ERROR_OS
, "could not load PATH");
210 iter
->current_dir
= iter
->env
;
214 static int win32_path_iter_next(
217 struct win32_path_iter
*iter
)
219 const wchar_t *start
;
223 if (!iter
->current_dir
|| !*iter
->current_dir
)
226 term
= (*iter
->current_dir
== L
'"') ? *iter
->current_dir
++ : L
';';
227 start
= iter
->current_dir
;
229 while (*iter
->current_dir
&& *iter
->current_dir
!= term
) {
237 if (term
== L
'"' && *iter
->current_dir
)
240 while (*iter
->current_dir
== L
';')
246 static void win32_path_iter_dispose(struct win32_path_iter
*iter
)
251 git__free(iter
->env
);
253 iter
->current_dir
= NULL
;
256 int git_win32_path_find_executable(git_win32_path fullpath
, wchar_t *exe
)
258 struct win32_path_iter path_iter
;
260 size_t dir_len
, exe_len
= wcslen(exe
);
263 if (win32_path_iter_init(&path_iter
) < 0)
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)
270 if (_waccess(fullpath
, 0) == 0) {
276 win32_path_iter_dispose(&path_iter
);
282 return GIT_ENOTFOUND
;
285 static int win32_path_cwd(wchar_t *out
, size_t len
)
290 errno
= ENAMETOOLONG
;
294 if ((cwd_len
= path__cwd(out
, (int)len
)) < 0)
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.
304 if (cwd_len
> GIT_WIN_PATH_MAX
- 4) {
305 errno
= ENAMETOOLONG
;
309 memmove(out
+2, out
, sizeof(wchar_t) * cwd_len
);
317 /* Our buffer must be at least 2 characters larger than the current
318 * working directory. (One character for the directory separator,
321 else if (cwd_len
> GIT_WIN_PATH_MAX
- 2) {
322 errno
= ENAMETOOLONG
;
329 int git_win32_path_from_utf8(git_win32_path out
, const char *src
)
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
;
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)
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)
349 else if (path__is_unc(src
)) {
350 memcpy(dest
, L
"UNC\\", sizeof(wchar_t) * 4);
353 /* Skip the leading "\\" */
354 if (git__utf8_to_16(dest
, GIT_WIN_PATH_MAX
- 2, src
+ 2) < 0)
357 /* Absolute paths omitting the drive letter */
358 else if (path__startswith_slash(src
)) {
359 if (path__cwd(dest
, GIT_WIN_PATH_MAX
) < 0)
362 if (!git_fs_path_is_absolute(dest
)) {
367 /* Skip the drive letter specification ("C:") */
368 if (git__utf8_to_16(dest
+ 2, GIT_WIN_PATH_MAX
- 2, src
) < 0)
375 if ((cwd_len
= win32_path_cwd(dest
, GIT_WIN_PATH_MAX
)) < 0)
378 dest
[cwd_len
++] = L
'\\';
380 if (git__utf8_to_16(dest
+ cwd_len
, GIT_WIN_PATH_MAX
- cwd_len
, src
) < 0)
384 return git_win32_path_canonicalize(out
);
387 /* set windows error code so we can use its error message */
388 if (errno
== ENAMETOOLONG
)
389 SetLastError(ERROR_FILENAME_EXCED_RANGE
);
394 int git_win32_path_relative_from_utf8(git_win32_path out
, const char *src
)
396 wchar_t *dest
= out
, *p
;
399 /* Handle absolute paths */
400 if (git_fs_path_is_absolute(src
) ||
401 path__is_nt_namespace(src
) ||
403 path__startswith_slash(src
)) {
404 return git_win32_path_from_utf8(out
, src
);
407 if ((len
= git__utf8_to_16(dest
, GIT_WIN_PATH_MAX
, src
)) < 0)
410 for (p
= dest
; p
< (dest
+ len
); p
++) {
418 int git_win32_path_to_utf8(git_win32_utf8_path dest
, const wchar_t *src
)
423 /* Strip NT namespacing "\\?\" */
424 if (path__is_nt_namespace(src
)) {
427 /* "\\?\UNC\server\share" -> "\\server\share" */
428 if (wcsncmp(src
, L
"UNC\\", 4) == 0) {
431 memcpy(dest
, "\\\\", 2);
436 if ((len
= git__utf16_to_8(out
, GIT_WIN_PATH_UTF8
, src
)) < 0)
439 git_fs_path_mkposix(dest
);
444 char *git_win32_path_8dot3_name(const char *path
)
446 git_win32_path longpath
, shortpath
;
449 int len
, namelen
= 1;
451 if (git_win32_path_from_utf8(longpath
, path
) < 0)
454 len
= GetShortPathNameW(longpath
, shortpath
, GIT_WIN_PATH_UTF16
);
456 while (len
&& shortpath
[len
-1] == L
'\\')
457 shortpath
[--len
] = L
'\0';
459 if (len
== 0 || len
>= GIT_WIN_PATH_UTF16
)
462 for (start
= shortpath
+ (len
- 1);
463 start
> shortpath
&& *(start
-1) != '/' && *(start
-1) != '\\';
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.
471 if (namelen
> 12 || (shortname
= git__malloc(namelen
+ 1)) == NULL
)
474 if ((len
= git__utf16_to_8(shortname
, namelen
+ 1, start
)) < 0)
480 static bool path_is_volume(wchar_t *target
, size_t target_len
)
482 return (target_len
&& wcsncmp(target
, L
"\\??\\Volume{", 11) == 0);
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
)
489 BYTE buf
[MAXIMUM_REPARSE_DATA_BUFFER_SIZE
];
490 GIT_REPARSE_DATA_BUFFER
*reparse_buf
= (GIT_REPARSE_DATA_BUFFER
*)buf
;
491 HANDLE handle
= NULL
;
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
);
502 if (handle
== INVALID_HANDLE_VALUE
) {
507 if (!DeviceIoControl(handle
, FSCTL_GET_REPARSE_POINT
, NULL
, 0,
508 reparse_buf
, sizeof(buf
), &ioctl_ret
, NULL
)) {
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
);
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
);
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.
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
);
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
;
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.
556 * @param path The path which should be trimmed.
557 * @return The length of the modified string (<= the input length)
559 size_t git_win32_path_trim_end(wchar_t *str
, size_t len
)
562 if (!len
|| str
[len
- 1] != L
'\\')
566 * Don't trim backslashes from drive letter paths, which
567 * are 3 characters long and of the form C:\, D:\, etc.
569 if (len
== 3 && git_win32__isalpha(str
[0]) && str
[1] == ':')
581 * Removes any of the following namespace prefixes from a path,
582 * if found: "\??\", "\\?\", "\\?\UNC\". This function cannot fail.
584 * @param path The path which should be converted.
585 * @return The length of the modified string (<= the input length)
587 size_t git_win32_path_remove_namespace(wchar_t *str
, size_t len
)
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
"\\\\";
594 const wchar_t *prefix
= NULL
, *remainder
= NULL
;
595 size_t prefix_len
= 0, remainder_len
= 0;
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
);
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
);
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
))) {
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.
619 remainder
+= CONST_STRLEN(unc_namespace_remainder
);
620 remainder_len
-= CONST_STRLEN(unc_namespace_remainder
);
623 prefix_len
= CONST_STRLEN(unc_prefix
);
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.)
631 if (remainder
&& len
>= remainder_len
+ prefix_len
) {
633 memmove(str
, prefix
, prefix_len
* sizeof(wchar_t));
635 memmove(str
+ prefix_len
, remainder
, remainder_len
* sizeof(wchar_t));
637 len
= remainder_len
+ prefix_len
;
641 return git_win32_path_trim_end(str
, len
);