]> git.proxmox.com Git - libgit2.git/blob - src/win32/path_w32.c
9faddcf3bfe7035eb3d7fe8ebbedec038ac321fc
[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 "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 predictible 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_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_path_is_absolute(path))
75 path += PATH__ABSOLUTE_LEN;
76 } else if (git_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 win32_path_cwd(wchar_t *out, size_t len)
155 {
156 int cwd_len;
157
158 if (len > INT_MAX) {
159 errno = ENAMETOOLONG;
160 return -1;
161 }
162
163 if ((cwd_len = path__cwd(out, (int)len)) < 0)
164 return -1;
165
166 /* UNC paths */
167 if (wcsncmp(L"\\\\", out, 2) == 0) {
168 /* Our buffer must be at least 5 characters larger than the
169 * current working directory: we swallow one of the leading
170 * '\'s, but we we add a 'UNC' specifier to the path, plus
171 * a trailing directory separator, plus a NUL.
172 */
173 if (cwd_len > MAX_PATH - 4) {
174 errno = ENAMETOOLONG;
175 return -1;
176 }
177
178 memmove(out+2, out, sizeof(wchar_t) * cwd_len);
179 out[0] = L'U';
180 out[1] = L'N';
181 out[2] = L'C';
182
183 cwd_len += 2;
184 }
185
186 /* Our buffer must be at least 2 characters larger than the current
187 * working directory. (One character for the directory separator,
188 * one for the null.
189 */
190 else if (cwd_len > MAX_PATH - 2) {
191 errno = ENAMETOOLONG;
192 return -1;
193 }
194
195 return cwd_len;
196 }
197
198 int git_win32_path_from_utf8(git_win32_path out, const char *src)
199 {
200 wchar_t *dest = out;
201
202 /* All win32 paths are in NT-prefixed format, beginning with "\\?\". */
203 memcpy(dest, PATH__NT_NAMESPACE, sizeof(wchar_t) * PATH__NT_NAMESPACE_LEN);
204 dest += PATH__NT_NAMESPACE_LEN;
205
206 /* See if this is an absolute path (beginning with a drive letter) */
207 if (git_path_is_absolute(src)) {
208 if (git__utf8_to_16(dest, MAX_PATH, src) < 0)
209 goto on_error;
210 }
211 /* File-prefixed NT-style paths beginning with \\?\ */
212 else if (path__is_nt_namespace(src)) {
213 /* Skip the NT prefix, the destination already contains it */
214 if (git__utf8_to_16(dest, MAX_PATH, src + PATH__NT_NAMESPACE_LEN) < 0)
215 goto on_error;
216 }
217 /* UNC paths */
218 else if (path__is_unc(src)) {
219 memcpy(dest, L"UNC\\", sizeof(wchar_t) * 4);
220 dest += 4;
221
222 /* Skip the leading "\\" */
223 if (git__utf8_to_16(dest, MAX_PATH - 2, src + 2) < 0)
224 goto on_error;
225 }
226 /* Absolute paths omitting the drive letter */
227 else if (path__startswith_slash(src)) {
228 if (path__cwd(dest, MAX_PATH) < 0)
229 goto on_error;
230
231 if (!git_path_is_absolute(dest)) {
232 errno = ENOENT;
233 goto on_error;
234 }
235
236 /* Skip the drive letter specification ("C:") */
237 if (git__utf8_to_16(dest + 2, MAX_PATH - 2, src) < 0)
238 goto on_error;
239 }
240 /* Relative paths */
241 else {
242 int cwd_len;
243
244 if ((cwd_len = win32_path_cwd(dest, MAX_PATH)) < 0)
245 goto on_error;
246
247 dest[cwd_len++] = L'\\';
248
249 if (git__utf8_to_16(dest + cwd_len, MAX_PATH - cwd_len, src) < 0)
250 goto on_error;
251 }
252
253 return git_win32_path_canonicalize(out);
254
255 on_error:
256 /* set windows error code so we can use its error message */
257 if (errno == ENAMETOOLONG)
258 SetLastError(ERROR_FILENAME_EXCED_RANGE);
259
260 return -1;
261 }
262
263 int git_win32_path_relative_from_utf8(git_win32_path out, const char *src)
264 {
265 wchar_t *dest = out, *p;
266 int len;
267
268 /* Handle absolute paths */
269 if (git_path_is_absolute(src) ||
270 path__is_nt_namespace(src) ||
271 path__is_unc(src) ||
272 path__startswith_slash(src)) {
273 return git_win32_path_from_utf8(out, src);
274 }
275
276 if ((len = git__utf8_to_16(dest, MAX_PATH, src)) < 0)
277 return -1;
278
279 for (p = dest; p < (dest + len); p++) {
280 if (*p == L'/')
281 *p = L'\\';
282 }
283
284 return len;
285 }
286
287 int git_win32_path_to_utf8(git_win32_utf8_path dest, const wchar_t *src)
288 {
289 char *out = dest;
290 int len;
291
292 /* Strip NT namespacing "\\?\" */
293 if (path__is_nt_namespace(src)) {
294 src += 4;
295
296 /* "\\?\UNC\server\share" -> "\\server\share" */
297 if (wcsncmp(src, L"UNC\\", 4) == 0) {
298 src += 4;
299
300 memcpy(dest, "\\\\", 2);
301 out = dest + 2;
302 }
303 }
304
305 if ((len = git__utf16_to_8(out, GIT_WIN_PATH_UTF8, src)) < 0)
306 return len;
307
308 git_path_mkposix(dest);
309
310 return len;
311 }
312
313 char *git_win32_path_8dot3_name(const char *path)
314 {
315 git_win32_path longpath, shortpath;
316 wchar_t *start;
317 char *shortname;
318 int len, namelen = 1;
319
320 if (git_win32_path_from_utf8(longpath, path) < 0)
321 return NULL;
322
323 len = GetShortPathNameW(longpath, shortpath, GIT_WIN_PATH_UTF16);
324
325 while (len && shortpath[len-1] == L'\\')
326 shortpath[--len] = L'\0';
327
328 if (len == 0 || len >= GIT_WIN_PATH_UTF16)
329 return NULL;
330
331 for (start = shortpath + (len - 1);
332 start > shortpath && *(start-1) != '/' && *(start-1) != '\\';
333 start--)
334 namelen++;
335
336 /* We may not have actually been given a short name. But if we have,
337 * it will be in the ASCII byte range, so we don't need to worry about
338 * multi-byte sequences and can allocate naively.
339 */
340 if (namelen > 12 || (shortname = git__malloc(namelen + 1)) == NULL)
341 return NULL;
342
343 if ((len = git__utf16_to_8(shortname, namelen + 1, start)) < 0)
344 return NULL;
345
346 return shortname;
347 }
348
349 static bool path_is_volume(wchar_t *target, size_t target_len)
350 {
351 return (target_len && wcsncmp(target, L"\\??\\Volume{", 11) == 0);
352 }
353
354 /* On success, returns the length, in characters, of the path stored in dest.
355 * On failure, returns a negative value. */
356 int git_win32_path_readlink_w(git_win32_path dest, const git_win32_path path)
357 {
358 BYTE buf[MAXIMUM_REPARSE_DATA_BUFFER_SIZE];
359 GIT_REPARSE_DATA_BUFFER *reparse_buf = (GIT_REPARSE_DATA_BUFFER *)buf;
360 HANDLE handle = NULL;
361 DWORD ioctl_ret;
362 wchar_t *target;
363 size_t target_len;
364
365 int error = -1;
366
367 handle = CreateFileW(path, GENERIC_READ,
368 FILE_SHARE_READ | FILE_SHARE_DELETE, NULL, OPEN_EXISTING,
369 FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS, NULL);
370
371 if (handle == INVALID_HANDLE_VALUE) {
372 errno = ENOENT;
373 return -1;
374 }
375
376 if (!DeviceIoControl(handle, FSCTL_GET_REPARSE_POINT, NULL, 0,
377 reparse_buf, sizeof(buf), &ioctl_ret, NULL)) {
378 errno = EINVAL;
379 goto on_error;
380 }
381
382 switch (reparse_buf->ReparseTag) {
383 case IO_REPARSE_TAG_SYMLINK:
384 target = reparse_buf->SymbolicLinkReparseBuffer.PathBuffer +
385 (reparse_buf->SymbolicLinkReparseBuffer.SubstituteNameOffset / sizeof(WCHAR));
386 target_len = reparse_buf->SymbolicLinkReparseBuffer.SubstituteNameLength / sizeof(WCHAR);
387 break;
388 case IO_REPARSE_TAG_MOUNT_POINT:
389 target = reparse_buf->MountPointReparseBuffer.PathBuffer +
390 (reparse_buf->MountPointReparseBuffer.SubstituteNameOffset / sizeof(WCHAR));
391 target_len = reparse_buf->MountPointReparseBuffer.SubstituteNameLength / sizeof(WCHAR);
392 break;
393 default:
394 errno = EINVAL;
395 goto on_error;
396 }
397
398 if (path_is_volume(target, target_len)) {
399 /* This path is a reparse point that represents another volume mounted
400 * at this location, it is not a symbolic link our input was canonical.
401 */
402 errno = EINVAL;
403 error = -1;
404 } else if (target_len) {
405 /* The path may need to have a namespace prefix removed. */
406 target_len = git_win32_path_remove_namespace(target, target_len);
407
408 /* Need one additional character in the target buffer
409 * for the terminating NULL. */
410 if (GIT_WIN_PATH_UTF16 > target_len) {
411 wcscpy(dest, target);
412 error = (int)target_len;
413 }
414 }
415
416 on_error:
417 CloseHandle(handle);
418 return error;
419 }
420
421 /**
422 * Removes any trailing backslashes from a path, except in the case of a drive
423 * letter path (C:\, D:\, etc.). This function cannot fail.
424 *
425 * @param path The path which should be trimmed.
426 * @return The length of the modified string (<= the input length)
427 */
428 size_t git_win32_path_trim_end(wchar_t *str, size_t len)
429 {
430 while (1) {
431 if (!len || str[len - 1] != L'\\')
432 break;
433
434 /*
435 * Don't trim backslashes from drive letter paths, which
436 * are 3 characters long and of the form C:\, D:\, etc.
437 */
438 if (len == 3 && git_win32__isalpha(str[0]) && str[1] == ':')
439 break;
440
441 len--;
442 }
443
444 str[len] = L'\0';
445
446 return len;
447 }
448
449 /**
450 * Removes any of the following namespace prefixes from a path,
451 * if found: "\??\", "\\?\", "\\?\UNC\". This function cannot fail.
452 *
453 * @param path The path which should be converted.
454 * @return The length of the modified string (<= the input length)
455 */
456 size_t git_win32_path_remove_namespace(wchar_t *str, size_t len)
457 {
458 static const wchar_t dosdevices_namespace[] = L"\\\?\?\\";
459 static const wchar_t nt_namespace[] = L"\\\\?\\";
460 static const wchar_t unc_namespace_remainder[] = L"UNC\\";
461 static const wchar_t unc_prefix[] = L"\\\\";
462
463 const wchar_t *prefix = NULL, *remainder = NULL;
464 size_t prefix_len = 0, remainder_len = 0;
465
466 /* "\??\" -- DOS Devices prefix */
467 if (len >= CONST_STRLEN(dosdevices_namespace) &&
468 !wcsncmp(str, dosdevices_namespace, CONST_STRLEN(dosdevices_namespace))) {
469 remainder = str + CONST_STRLEN(dosdevices_namespace);
470 remainder_len = len - CONST_STRLEN(dosdevices_namespace);
471 }
472 /* "\\?\" -- NT namespace prefix */
473 else if (len >= CONST_STRLEN(nt_namespace) &&
474 !wcsncmp(str, nt_namespace, CONST_STRLEN(nt_namespace))) {
475 remainder = str + CONST_STRLEN(nt_namespace);
476 remainder_len = len - CONST_STRLEN(nt_namespace);
477 }
478
479 /* "\??\UNC\", "\\?\UNC\" -- UNC prefix */
480 if (remainder_len >= CONST_STRLEN(unc_namespace_remainder) &&
481 !wcsncmp(remainder, unc_namespace_remainder, CONST_STRLEN(unc_namespace_remainder))) {
482
483 /*
484 * The proper Win32 path for a UNC share has "\\" at beginning of it
485 * and looks like "\\server\share\<folderStructure>". So remove the
486 * UNC namespace and add a prefix of "\\" in its place.
487 */
488 remainder += CONST_STRLEN(unc_namespace_remainder);
489 remainder_len -= CONST_STRLEN(unc_namespace_remainder);
490
491 prefix = unc_prefix;
492 prefix_len = CONST_STRLEN(unc_prefix);
493 }
494
495 if (remainder) {
496 /*
497 * Sanity check that the new string isn't longer than the old one.
498 * (This could only happen due to programmer error introducing a
499 * prefix longer than the namespace it replaces.)
500 */
501 assert(len >= remainder_len + prefix_len);
502
503 if (prefix)
504 memmove(str, prefix, prefix_len * sizeof(wchar_t));
505
506 memmove(str + prefix_len, remainder, remainder_len * sizeof(wchar_t));
507
508 len = remainder_len + prefix_len;
509 str[len] = L'\0';
510 }
511
512 return git_win32_path_trim_end(str, len);
513 }