]> git.proxmox.com Git - libgit2.git/blob - src/win32/path_w32.c
Improvements to status performance on Windows.
[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 #define PATH__MAX_UNC_LEN (32767)
34
35 GIT_INLINE(int) path__cwd(wchar_t *path, int size)
36 {
37 int len;
38
39 if ((len = GetCurrentDirectoryW(size, path)) == 0) {
40 errno = GetLastError() == ERROR_ACCESS_DENIED ? EACCES : ENOENT;
41 return -1;
42 } else if (len > size) {
43 errno = ENAMETOOLONG;
44 return -1;
45 }
46
47 /* The Win32 APIs may return "\\?\" once you've used it first.
48 * But it may not. What a gloriously predictible API!
49 */
50 if (wcsncmp(path, PATH__NT_NAMESPACE, PATH__NT_NAMESPACE_LEN))
51 return len;
52
53 len -= PATH__NT_NAMESPACE_LEN;
54
55 memmove(path, path + PATH__NT_NAMESPACE_LEN, sizeof(wchar_t) * len);
56 return len;
57 }
58
59 static wchar_t *path__skip_server(wchar_t *path)
60 {
61 wchar_t *c;
62
63 for (c = path; *c; c++) {
64 if (path__is_dirsep(*c))
65 return c + 1;
66 }
67
68 return c;
69 }
70
71 static wchar_t *path__skip_prefix(wchar_t *path)
72 {
73 if (path__is_nt_namespace(path)) {
74 path += PATH__NT_NAMESPACE_LEN;
75
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);
84 }
85
86 return path;
87 }
88
89 int git_win32_path_canonicalize(git_win32_path path)
90 {
91 wchar_t *base, *from, *to, *next;
92 size_t len;
93
94 base = to = path__skip_prefix(path);
95
96 /* Unposixify if the prefix */
97 for (from = path; from < to; from++) {
98 if (*from == L'/')
99 *from = L'\\';
100 }
101
102 while (*from) {
103 for (next = from; *next; ++next) {
104 if (*next == L'/') {
105 *next = L'\\';
106 break;
107 }
108
109 if (*next == L'\\')
110 break;
111 }
112
113 len = next - from;
114
115 if (len == 1 && from[0] == L'.')
116 /* do nothing with singleton dot */;
117
118 else if (len == 2 && from[0] == L'.' && from[1] == L'.') {
119 if (to == base) {
120 /* no more path segments to strip, eat the "../" */
121 if (*next == L'\\')
122 len++;
123
124 base = to;
125 } else {
126 /* back up a path segment */
127 while (to > base && to[-1] == L'\\') to--;
128 while (to > base && to[-1] != L'\\') to--;
129 }
130 } else {
131 if (*next == L'\\' && *from != L'\\')
132 len++;
133
134 if (to != from)
135 memmove(to, from, sizeof(wchar_t) * len);
136
137 to += len;
138 }
139
140 from += len;
141
142 while (*from == L'\\') from++;
143 }
144
145 /* Strip trailing backslashes */
146 while (to > base && to[-1] == L'\\') to--;
147
148 *to = L'\0';
149
150 return (to - path);
151 }
152
153 int git_win32_path__cwd(wchar_t *out, size_t len)
154 {
155 int cwd_len;
156
157 if ((cwd_len = path__cwd(out, len)) < 0)
158 return -1;
159
160 /* UNC paths */
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.
166 */
167 if (cwd_len > MAX_PATH - 4) {
168 errno = ENAMETOOLONG;
169 return -1;
170 }
171
172 memmove(out+2, out, sizeof(wchar_t) * cwd_len);
173 out[0] = L'U';
174 out[1] = L'N';
175 out[2] = L'C';
176
177 cwd_len += 2;
178 }
179
180 /* Our buffer must be at least 2 characters larger than the current
181 * working directory. (One character for the directory separator,
182 * one for the null.
183 */
184 else if (cwd_len > MAX_PATH - 2) {
185 errno = ENAMETOOLONG;
186 return -1;
187 }
188
189 return cwd_len;
190 }
191
192 int git_win32_path_from_utf8(git_win32_path out, const char *src)
193 {
194 wchar_t *dest = out;
195
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;
199
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)
203 return -1;
204 }
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)
209 return -1;
210 }
211 /* UNC paths */
212 else if (path__is_unc(src)) {
213 memcpy(dest, L"UNC\\", sizeof(wchar_t) * 4);
214 dest += 4;
215
216 /* Skip the leading "\\" */
217 if (git__utf8_to_16(dest, MAX_PATH - 2, src + 2) < 0)
218 return -1;
219 }
220 /* Absolute paths omitting the drive letter */
221 else if (src[0] == '\\' || src[0] == '/') {
222 if (path__cwd(dest, MAX_PATH) < 0)
223 return -1;
224
225 if (!path__is_absolute(dest)) {
226 errno = ENOENT;
227 return -1;
228 }
229
230 /* Skip the drive letter specification ("C:") */
231 if (git__utf8_to_16(dest + 2, MAX_PATH - 2, src) < 0)
232 return -1;
233 }
234 /* Relative paths */
235 else {
236 int cwd_len;
237
238 if ((cwd_len = git_win32_path__cwd(dest, MAX_PATH)) < 0)
239 return -1;
240
241 dest[cwd_len++] = L'\\';
242
243 if (git__utf8_to_16(dest + cwd_len, MAX_PATH - cwd_len, src) < 0)
244 return -1;
245 }
246
247 return git_win32_path_canonicalize(out);
248 }
249
250 int git_win32_path_to_utf8(git_win32_utf8_path dest, const wchar_t *src)
251 {
252 char *out = dest;
253 int len;
254
255 /* Strip NT namespacing "\\?\" */
256 if (path__is_nt_namespace(src)) {
257 src += 4;
258
259 /* "\\?\UNC\server\share" -> "\\server\share" */
260 if (wcsncmp(src, L"UNC\\", 4) == 0) {
261 src += 4;
262
263 memcpy(dest, "\\\\", 2);
264 out = dest + 2;
265 }
266 }
267
268 if ((len = git__utf16_to_8(out, GIT_WIN_PATH_UTF8, src)) < 0)
269 return len;
270
271 git_path_mkposix(dest);
272
273 return len;
274 }
275
276 char *git_win32_path_8dot3_name(const char *path)
277 {
278 git_win32_path longpath, shortpath;
279 wchar_t *start;
280 char *shortname;
281 int len, namelen = 1;
282
283 if (git_win32_path_from_utf8(longpath, path) < 0)
284 return NULL;
285
286 len = GetShortPathNameW(longpath, shortpath, GIT_WIN_PATH_UTF16);
287
288 while (len && shortpath[len-1] == L'\\')
289 shortpath[--len] = L'\0';
290
291 if (len == 0 || len >= GIT_WIN_PATH_UTF16)
292 return NULL;
293
294 for (start = shortpath + (len - 1);
295 start > shortpath && *(start-1) != '/' && *(start-1) != '\\';
296 start--)
297 namelen++;
298
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.
302 */
303 if (namelen > 12 || (shortname = git__malloc(namelen + 1)) == NULL)
304 return NULL;
305
306 if ((len = git__utf16_to_8(shortname, namelen + 1, start)) < 0)
307 return NULL;
308
309 return shortname;
310 }
311
312 #if !defined(__MINGW32__)
313 int git_win32_path_dirload_with_stat(
314 const char *path,
315 size_t prefix_len,
316 unsigned int flags,
317 const char *start_stat,
318 const char *end_stat,
319 git_vector *contents)
320 {
321 int error = 0;
322 git_path_with_stat *ps;
323 git_win32_path pathw;
324 DIR *dir;
325 int(*strncomp)(const char *a, const char *b, size_t sz);
326 size_t cmp_len;
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;
334 size_t path_len;
335 int fMode;
336
337 if (!git_win32__findfirstfile_filter(pathw, path)) {
338 error = -1;
339 giterr_set(GITERR_OS, "Could not parse the path '%s'", path);
340 goto clean_up_and_exit;
341 }
342
343 strncomp = (flags & GIT_PATH_DIR_IGNORE_CASE) != 0
344 ? git__strncasecmp
345 : git__strncmp;
346
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);
352 dir->first = 1;
353 if (dir->h == INVALID_HANDLE_VALUE) {
354 error = -1;
355 giterr_set(GITERR_OS, "Could not open directory '%s'", path);
356 goto clean_up_and_exit;
357 }
358
359 if (repo_path_len > PATH__MAX_UNC_LEN) {
360 error = -1;
361 giterr_set(GITERR_OS, "Could not open directory '%s'", path);
362 goto clean_up_and_exit;
363 }
364
365 memcpy(work_path, repo_path, repo_path_len);
366
367 while (dir) {
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);
370
371 work_path[path_len + repo_path_len] = '\0';
372 path_len = path_len + repo_path_len;
373
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)) {
378 fMode = S_IREAD;
379
380 if (dir->f.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)
381 fMode |= S_IFDIR;
382 else
383 fMode |= S_IFREG;
384
385 if (!(dir->f.dwFileAttributes & FILE_ATTRIBUTE_READONLY))
386 fMode |= S_IWRITE;
387
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;
399 ps->st.st_ino = 0;
400 ps->st.st_gid = 0;
401 ps->st.st_uid = 0;
402 ps->st.st_nlink = 1;
403
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;
407
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) {
411 error = -1;
412 giterr_set(GITERR_OS, "Could not manage reparse link '%s'", dir->f.cFileName);
413 goto clean_up_and_exit;
414 }
415 }
416 }
417
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)) {
422 git__free(ps);
423 ps = NULL;
424 }
425
426 if (ps)
427 git_vector_insert(contents, ps);
428 }
429 }
430 }
431
432 memset(&dir->f, 0, sizeof(git_path_with_stat));
433 dir->first = 0;
434
435 if (!FindNextFileW(dir->h, &dir->f)) {
436 if (GetLastError() == ERROR_NO_MORE_FILES)
437 break;
438 else {
439 error = -1;
440 giterr_set(GITERR_OS, "Could not get attributes for file in '%s'", path);
441 goto clean_up_and_exit;
442 }
443 }
444 }
445
446 /* sort now that directory suffix is added */
447 git_vector_sort(contents);
448
449 clean_up_and_exit:
450
451 if (dir) {
452 FindClose(dir->h);
453 free(dir);
454 }
455
456 return error;
457 }
458 #endif
459
460 static bool path_is_volume(wchar_t *target, size_t target_len)
461 {
462 return (target_len && wcsncmp(target, L"\\??\\Volume{", 11) == 0);
463 }
464
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)
468 {
469 BYTE buf[MAXIMUM_REPARSE_DATA_BUFFER_SIZE];
470 GIT_REPARSE_DATA_BUFFER *reparse_buf = (GIT_REPARSE_DATA_BUFFER *)buf;
471 HANDLE handle = NULL;
472 DWORD ioctl_ret;
473 wchar_t *target;
474 size_t target_len;
475
476 int error = -1;
477
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);
481
482 if (handle == INVALID_HANDLE_VALUE) {
483 errno = ENOENT;
484 return -1;
485 }
486
487 if (!DeviceIoControl(handle, FSCTL_GET_REPARSE_POINT, NULL, 0,
488 reparse_buf, sizeof(buf), &ioctl_ret, NULL)) {
489 errno = EINVAL;
490 goto on_error;
491 }
492
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);
498 break;
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);
503 break;
504 default:
505 errno = EINVAL;
506 goto on_error;
507 }
508
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.
512 */
513 errno = EINVAL;
514 error = -1;
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);
518
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;
524 }
525 }
526
527 on_error:
528 CloseHandle(handle);
529 return error;
530 }