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_dirsep(p) ((p) == '/' || (p) == '\\')
23 #define path__is_absolute(p) \
24 (git__isalpha((p)[0]) && (p)[1] == ':' && ((p)[2] == '\\' || (p)[2] == '/'))
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] == '/'))
30 #define path__is_unc(p) \
31 (((p)[0] == '\\' && (p)[1] == '\\') || ((p)[0] == '/' && (p)[1] == '/'))
33 #define PATH__MAX_UNC_LEN (32767)
35 GIT_INLINE(int) path__cwd(wchar_t *path
, int size
)
39 if ((len
= GetCurrentDirectoryW(size
, path
)) == 0) {
40 errno
= GetLastError() == ERROR_ACCESS_DENIED
? EACCES
: ENOENT
;
42 } else if (len
> size
) {
47 /* The Win32 APIs may return "\\?\" once you've used it first.
48 * But it may not. What a gloriously predictible API!
50 if (wcsncmp(path
, PATH__NT_NAMESPACE
, PATH__NT_NAMESPACE_LEN
))
53 len
-= PATH__NT_NAMESPACE_LEN
;
55 memmove(path
, path
+ PATH__NT_NAMESPACE_LEN
, sizeof(wchar_t) * len
);
59 static wchar_t *path__skip_server(wchar_t *path
)
63 for (c
= path
; *c
; c
++) {
64 if (path__is_dirsep(*c
))
71 static wchar_t *path__skip_prefix(wchar_t *path
)
73 if (path__is_nt_namespace(path
)) {
74 path
+= PATH__NT_NAMESPACE_LEN
;
76 if (wcsncmp(path
, L
"UNC\\", 4) == 0)
77 path
= path__skip_server(path
+ 4);
78 else if (path__is_absolute(path
))
79 path
+= PATH__ABSOLUTE_LEN
;
80 } else if (path__is_absolute(path
)) {
81 path
+= PATH__ABSOLUTE_LEN
;
82 } else if (path__is_unc(path
)) {
83 path
= path__skip_server(path
+ 2);
89 int git_win32_path_canonicalize(git_win32_path path
)
91 wchar_t *base
, *from
, *to
, *next
;
94 base
= to
= path__skip_prefix(path
);
96 /* Unposixify if the prefix */
97 for (from
= path
; from
< to
; from
++) {
103 for (next
= from
; *next
; ++next
) {
115 if (len
== 1 && from
[0] == L
'.')
116 /* do nothing with singleton dot */;
118 else if (len
== 2 && from
[0] == L
'.' && from
[1] == L
'.') {
120 /* no more path segments to strip, eat the "../" */
126 /* back up a path segment */
127 while (to
> base
&& to
[-1] == L
'\\') to
--;
128 while (to
> base
&& to
[-1] != L
'\\') to
--;
131 if (*next
== L
'\\' && *from
!= L
'\\')
135 memmove(to
, from
, sizeof(wchar_t) * len
);
142 while (*from
== L
'\\') from
++;
145 /* Strip trailing backslashes */
146 while (to
> base
&& to
[-1] == L
'\\') to
--;
153 int git_win32_path__cwd(wchar_t *out
, size_t len
)
157 if ((cwd_len
= path__cwd(out
, len
)) < 0)
161 if (wcsncmp(L
"\\\\", out
, 2) == 0) {
162 /* Our buffer must be at least 5 characters larger than the
163 * current working directory: we swallow one of the leading
164 * '\'s, but we we add a 'UNC' specifier to the path, plus
165 * a trailing directory separator, plus a NUL.
167 if (cwd_len
> MAX_PATH
- 4) {
168 errno
= ENAMETOOLONG
;
172 memmove(out
+2, out
, sizeof(wchar_t) * cwd_len
);
180 /* Our buffer must be at least 2 characters larger than the current
181 * working directory. (One character for the directory separator,
184 else if (cwd_len
> MAX_PATH
- 2) {
185 errno
= ENAMETOOLONG
;
192 int git_win32_path_from_utf8(git_win32_path out
, const char *src
)
196 /* All win32 paths are in NT-prefixed format, beginning with "\\?\". */
197 memcpy(dest
, PATH__NT_NAMESPACE
, sizeof(wchar_t) * PATH__NT_NAMESPACE_LEN
);
198 dest
+= PATH__NT_NAMESPACE_LEN
;
200 /* See if this is an absolute path (beginning with a drive letter) */
201 if (path__is_absolute(src
)) {
202 if (git__utf8_to_16(dest
, MAX_PATH
, src
) < 0)
205 /* File-prefixed NT-style paths beginning with \\?\ */
206 else if (path__is_nt_namespace(src
)) {
207 /* Skip the NT prefix, the destination already contains it */
208 if (git__utf8_to_16(dest
, MAX_PATH
, src
+ PATH__NT_NAMESPACE_LEN
) < 0)
212 else if (path__is_unc(src
)) {
213 memcpy(dest
, L
"UNC\\", sizeof(wchar_t) * 4);
216 /* Skip the leading "\\" */
217 if (git__utf8_to_16(dest
, MAX_PATH
- 2, src
+ 2) < 0)
220 /* Absolute paths omitting the drive letter */
221 else if (src
[0] == '\\' || src
[0] == '/') {
222 if (path__cwd(dest
, MAX_PATH
) < 0)
225 if (!path__is_absolute(dest
)) {
230 /* Skip the drive letter specification ("C:") */
231 if (git__utf8_to_16(dest
+ 2, MAX_PATH
- 2, src
) < 0)
238 if ((cwd_len
= git_win32_path__cwd(dest
, MAX_PATH
)) < 0)
241 dest
[cwd_len
++] = L
'\\';
243 if (git__utf8_to_16(dest
+ cwd_len
, MAX_PATH
- cwd_len
, src
) < 0)
247 return git_win32_path_canonicalize(out
);
250 int git_win32_path_to_utf8(git_win32_utf8_path dest
, const wchar_t *src
)
255 /* Strip NT namespacing "\\?\" */
256 if (path__is_nt_namespace(src
)) {
259 /* "\\?\UNC\server\share" -> "\\server\share" */
260 if (wcsncmp(src
, L
"UNC\\", 4) == 0) {
263 memcpy(dest
, "\\\\", 2);
268 if ((len
= git__utf16_to_8(out
, GIT_WIN_PATH_UTF8
, src
)) < 0)
271 git_path_mkposix(dest
);
276 char *git_win32_path_8dot3_name(const char *path
)
278 git_win32_path longpath
, shortpath
;
281 int len
, namelen
= 1;
283 if (git_win32_path_from_utf8(longpath
, path
) < 0)
286 len
= GetShortPathNameW(longpath
, shortpath
, GIT_WIN_PATH_UTF16
);
288 while (len
&& shortpath
[len
-1] == L
'\\')
289 shortpath
[--len
] = L
'\0';
291 if (len
== 0 || len
>= GIT_WIN_PATH_UTF16
)
294 for (start
= shortpath
+ (len
- 1);
295 start
> shortpath
&& *(start
-1) != '/' && *(start
-1) != '\\';
299 /* We may not have actually been given a short name. But if we have,
300 * it will be in the ASCII byte range, so we don't need to worry about
301 * multi-byte sequences and can allocate naively.
303 if (namelen
> 12 || (shortname
= git__malloc(namelen
+ 1)) == NULL
)
306 if ((len
= git__utf16_to_8(shortname
, namelen
+ 1, start
)) < 0)
312 #if !defined(__MINGW32__)
313 int git_win32_path_dirload_with_stat(
317 const char *start_stat
,
318 const char *end_stat
,
319 git_vector
*contents
)
322 git_path_with_stat
*ps
;
323 git_win32_path pathw
;
325 int(*strncomp
)(const char *a
, const char *b
, size_t sz
);
327 size_t start_len
= start_stat
? strlen(start_stat
) : 0;
328 size_t end_len
= end_stat
? strlen(end_stat
) : 0;
329 size_t path_size
= strlen(path
);
330 const char *repo_path
= path
+ prefix_len
;
331 size_t repo_path_len
= strlen(repo_path
);
332 char work_path
[PATH__MAX_UNC_LEN
];
333 git_win32_path target
;
337 if (!git_win32__findfirstfile_filter(pathw
, path
)) {
339 giterr_set(GITERR_OS
, "Could not parse the path '%s'", path
);
340 goto clean_up_and_exit
;
343 strncomp
= (flags
& GIT_PATH_DIR_IGNORE_CASE
) != 0
347 /* use of FIND_FIRST_EX_LARGE_FETCH flag in the FindFirstFileExW call could benefit perormance
348 * here when querying large repositories on Windows 7 (0x0600) or newer versions of Windows.
349 * doing so could introduce compatibility issues on older versions of Windows. */
350 dir
= git__calloc(1, sizeof(DIR));
351 dir
->h
= FindFirstFileExW(pathw
, FindExInfoBasic
, &dir
->f
, FindExSearchNameMatch
, NULL
, 0);
353 if (dir
->h
== INVALID_HANDLE_VALUE
) {
355 giterr_set(GITERR_OS
, "Could not open directory '%s'", path
);
356 goto clean_up_and_exit
;
359 if (repo_path_len
> PATH__MAX_UNC_LEN
) {
361 giterr_set(GITERR_OS
, "Could not open directory '%s'", path
);
362 goto clean_up_and_exit
;
365 memcpy(work_path
, repo_path
, repo_path_len
);
368 if (!git_path_is_dot_or_dotdotW(dir
->f
.cFileName
)) {
369 path_len
= git__utf16_to_8(work_path
+ repo_path_len
, ARRAYSIZE(work_path
) - repo_path_len
, dir
->f
.cFileName
);
371 work_path
[path_len
+ repo_path_len
] = '\0';
372 path_len
= path_len
+ repo_path_len
;
374 cmp_len
= min(start_len
, path_len
);
375 if (!(cmp_len
&& strncomp(work_path
, start_stat
, cmp_len
) < 0)) {
376 cmp_len
= min(end_len
, path_len
);
377 if (!(cmp_len
&& strncomp(work_path
, end_stat
, cmp_len
) > 0)) {
380 if (dir
->f
.dwFileAttributes
& FILE_ATTRIBUTE_DIRECTORY
)
385 if (!(dir
->f
.dwFileAttributes
& FILE_ATTRIBUTE_READONLY
))
388 ps
= git__calloc(1, sizeof(git_path_with_stat
) + path_len
+ 2);
389 memcpy(ps
->path
, work_path
, path_len
+ 1);
390 ps
->path_len
= path_len
;
391 ps
->st
.st_atime
= filetime_to_time_t(&dir
->f
.ftLastAccessTime
);
392 ps
->st
.st_ctime
= filetime_to_time_t(&dir
->f
.ftCreationTime
);
393 ps
->st
.st_mtime
= filetime_to_time_t(&dir
->f
.ftLastWriteTime
);
394 ps
->st
.st_size
= dir
->f
.nFileSizeHigh
;
395 ps
->st
.st_size
<<= 32;
396 ps
->st
.st_size
|= dir
->f
.nFileSizeLow
;
397 ps
->st
.st_dev
= ps
->st
.st_rdev
= (_getdrive() - 1);
398 ps
->st
.st_mode
= (mode_t
)fMode
;
404 if (dir
->f
.dwFileAttributes
& FILE_ATTRIBUTE_REPARSE_POINT
) {
405 if (git_win32_path_readlink_w(target
, dir
->f
.cFileName
) >= 0) {
406 ps
->st
.st_mode
= (ps
->st
.st_mode
& ~S_IFMT
) | S_IFLNK
;
408 /* st_size gets the UTF-8 length of the target name, in bytes,
409 * not counting the NULL terminator */
410 if ((ps
->st
.st_size
= git__utf16_to_8(NULL
, 0, target
)) < 0) {
412 giterr_set(GITERR_OS
, "Could not manage reparse link '%s'", dir
->f
.cFileName
);
413 goto clean_up_and_exit
;
418 if (S_ISDIR(ps
->st
.st_mode
)) {
419 ps
->path
[ps
->path_len
++] = '/';
420 ps
->path
[ps
->path_len
] = '\0';
421 } else if (!S_ISREG(ps
->st
.st_mode
) && !S_ISLNK(ps
->st
.st_mode
)) {
427 git_vector_insert(contents
, ps
);
432 memset(&dir
->f
, 0, sizeof(git_path_with_stat
));
435 if (!FindNextFileW(dir
->h
, &dir
->f
)) {
436 if (GetLastError() == ERROR_NO_MORE_FILES
)
440 giterr_set(GITERR_OS
, "Could not get attributes for file in '%s'", path
);
441 goto clean_up_and_exit
;
446 /* sort now that directory suffix is added */
447 git_vector_sort(contents
);
460 static bool path_is_volume(wchar_t *target
, size_t target_len
)
462 return (target_len
&& wcsncmp(target
, L
"\\??\\Volume{", 11) == 0);
465 /* On success, returns the length, in characters, of the path stored in dest.
466 * On failure, returns a negative value. */
467 int git_win32_path_readlink_w(git_win32_path dest
, const git_win32_path path
)
469 BYTE buf
[MAXIMUM_REPARSE_DATA_BUFFER_SIZE
];
470 GIT_REPARSE_DATA_BUFFER
*reparse_buf
= (GIT_REPARSE_DATA_BUFFER
*)buf
;
471 HANDLE handle
= NULL
;
478 handle
= CreateFileW(path
, GENERIC_READ
,
479 FILE_SHARE_READ
| FILE_SHARE_DELETE
, NULL
, OPEN_EXISTING
,
480 FILE_FLAG_OPEN_REPARSE_POINT
| FILE_FLAG_BACKUP_SEMANTICS
, NULL
);
482 if (handle
== INVALID_HANDLE_VALUE
) {
487 if (!DeviceIoControl(handle
, FSCTL_GET_REPARSE_POINT
, NULL
, 0,
488 reparse_buf
, sizeof(buf
), &ioctl_ret
, NULL
)) {
493 switch (reparse_buf
->ReparseTag
) {
494 case IO_REPARSE_TAG_SYMLINK
:
495 target
= reparse_buf
->SymbolicLinkReparseBuffer
.PathBuffer
+
496 (reparse_buf
->SymbolicLinkReparseBuffer
.SubstituteNameOffset
/ sizeof(WCHAR
));
497 target_len
= reparse_buf
->SymbolicLinkReparseBuffer
.SubstituteNameLength
/ sizeof(WCHAR
);
499 case IO_REPARSE_TAG_MOUNT_POINT
:
500 target
= reparse_buf
->MountPointReparseBuffer
.PathBuffer
+
501 (reparse_buf
->MountPointReparseBuffer
.SubstituteNameOffset
/ sizeof(WCHAR
));
502 target_len
= reparse_buf
->MountPointReparseBuffer
.SubstituteNameLength
/ sizeof(WCHAR
);
509 if (path_is_volume(target
, target_len
)) {
510 /* This path is a reparse point that represents another volume mounted
511 * at this location, it is not a symbolic link our input was canonical.
515 } else if (target_len
) {
516 /* The path may need to have a prefix removed. */
517 target_len
= git_win32__canonicalize_path(target
, target_len
);
519 /* Need one additional character in the target buffer
520 * for the terminating NULL. */
521 if (GIT_WIN_PATH_UTF16
> target_len
) {
522 wcscpy(dest
, target
);
523 error
= (int)target_len
;