]> git.proxmox.com Git - libgit2.git/blob - src/win32/path_w32.c
b955b024c1e5b19ab1ef37f46998a88bd84b21a5
[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 GIT_INLINE(int) path__cwd(wchar_t *path, int size)
29 {
30 int len;
31
32 if ((len = GetCurrentDirectoryW(size, path)) == 0) {
33 errno = GetLastError() == ERROR_ACCESS_DENIED ? EACCES : ENOENT;
34 return -1;
35 } else if (len > size) {
36 errno = ENAMETOOLONG;
37 return -1;
38 }
39
40 /* The Win32 APIs may return "\\?\" once you've used it first.
41 * But it may not. What a gloriously predictible API!
42 */
43 if (wcsncmp(path, PATH__NT_NAMESPACE, PATH__NT_NAMESPACE_LEN))
44 return len;
45
46 len -= PATH__NT_NAMESPACE_LEN;
47
48 memmove(path, path + PATH__NT_NAMESPACE_LEN, sizeof(wchar_t) * len);
49 return len;
50 }
51
52 static wchar_t *path__skip_server(wchar_t *path)
53 {
54 wchar_t *c;
55
56 for (c = path; *c; c++) {
57 if (git_path_is_dirsep(*c))
58 return c + 1;
59 }
60
61 return c;
62 }
63
64 static wchar_t *path__skip_prefix(wchar_t *path)
65 {
66 if (path__is_nt_namespace(path)) {
67 path += PATH__NT_NAMESPACE_LEN;
68
69 if (wcsncmp(path, L"UNC\\", 4) == 0)
70 path = path__skip_server(path + 4);
71 else if (git_path_is_absolute(path))
72 path += PATH__ABSOLUTE_LEN;
73 } else if (git_path_is_absolute(path)) {
74 path += PATH__ABSOLUTE_LEN;
75 } else if (path__is_unc(path)) {
76 path = path__skip_server(path + 2);
77 }
78
79 return path;
80 }
81
82 int git_win32_path_canonicalize(git_win32_path path)
83 {
84 wchar_t *base, *from, *to, *next;
85 size_t len;
86
87 base = to = path__skip_prefix(path);
88
89 /* Unposixify if the prefix */
90 for (from = path; from < to; from++) {
91 if (*from == L'/')
92 *from = L'\\';
93 }
94
95 while (*from) {
96 for (next = from; *next; ++next) {
97 if (*next == L'/') {
98 *next = L'\\';
99 break;
100 }
101
102 if (*next == L'\\')
103 break;
104 }
105
106 len = next - from;
107
108 if (len == 1 && from[0] == L'.')
109 /* do nothing with singleton dot */;
110
111 else if (len == 2 && from[0] == L'.' && from[1] == L'.') {
112 if (to == base) {
113 /* no more path segments to strip, eat the "../" */
114 if (*next == L'\\')
115 len++;
116
117 base = to;
118 } else {
119 /* back up a path segment */
120 while (to > base && to[-1] == L'\\') to--;
121 while (to > base && to[-1] != L'\\') to--;
122 }
123 } else {
124 if (*next == L'\\' && *from != L'\\')
125 len++;
126
127 if (to != from)
128 memmove(to, from, sizeof(wchar_t) * len);
129
130 to += len;
131 }
132
133 from += len;
134
135 while (*from == L'\\') from++;
136 }
137
138 /* Strip trailing backslashes */
139 while (to > base && to[-1] == L'\\') to--;
140
141 *to = L'\0';
142
143 return (to - path);
144 }
145
146 int git_win32_path__cwd(wchar_t *out, size_t len)
147 {
148 int cwd_len;
149
150 if ((cwd_len = path__cwd(out, len)) < 0)
151 return -1;
152
153 /* UNC paths */
154 if (wcsncmp(L"\\\\", out, 2) == 0) {
155 /* Our buffer must be at least 5 characters larger than the
156 * current working directory: we swallow one of the leading
157 * '\'s, but we we add a 'UNC' specifier to the path, plus
158 * a trailing directory separator, plus a NUL.
159 */
160 if (cwd_len > MAX_PATH - 4) {
161 errno = ENAMETOOLONG;
162 return -1;
163 }
164
165 memmove(out+2, out, sizeof(wchar_t) * cwd_len);
166 out[0] = L'U';
167 out[1] = L'N';
168 out[2] = L'C';
169
170 cwd_len += 2;
171 }
172
173 /* Our buffer must be at least 2 characters larger than the current
174 * working directory. (One character for the directory separator,
175 * one for the null.
176 */
177 else if (cwd_len > MAX_PATH - 2) {
178 errno = ENAMETOOLONG;
179 return -1;
180 }
181
182 return cwd_len;
183 }
184
185 int git_win32_path_from_utf8(git_win32_path out, const char *src)
186 {
187 wchar_t *dest = out;
188
189 /* All win32 paths are in NT-prefixed format, beginning with "\\?\". */
190 memcpy(dest, PATH__NT_NAMESPACE, sizeof(wchar_t) * PATH__NT_NAMESPACE_LEN);
191 dest += PATH__NT_NAMESPACE_LEN;
192
193 /* See if this is an absolute path (beginning with a drive letter) */
194 if (git_path_is_absolute(src)) {
195 if (git__utf8_to_16(dest, MAX_PATH, src) < 0)
196 goto on_error;
197 }
198 /* File-prefixed NT-style paths beginning with \\?\ */
199 else if (path__is_nt_namespace(src)) {
200 /* Skip the NT prefix, the destination already contains it */
201 if (git__utf8_to_16(dest, MAX_PATH, src + PATH__NT_NAMESPACE_LEN) < 0)
202 goto on_error;
203 }
204 /* UNC paths */
205 else if (path__is_unc(src)) {
206 memcpy(dest, L"UNC\\", sizeof(wchar_t) * 4);
207 dest += 4;
208
209 /* Skip the leading "\\" */
210 if (git__utf8_to_16(dest, MAX_PATH - 2, src + 2) < 0)
211 goto on_error;
212 }
213 /* Absolute paths omitting the drive letter */
214 else if (src[0] == '\\' || src[0] == '/') {
215 if (path__cwd(dest, MAX_PATH) < 0)
216 goto on_error;
217
218 if (!git_path_is_absolute(dest)) {
219 errno = ENOENT;
220 goto on_error;
221 }
222
223 /* Skip the drive letter specification ("C:") */
224 if (git__utf8_to_16(dest + 2, MAX_PATH - 2, src) < 0)
225 goto on_error;
226 }
227 /* Relative paths */
228 else {
229 int cwd_len;
230
231 if ((cwd_len = git_win32_path__cwd(dest, MAX_PATH)) < 0)
232 goto on_error;
233
234 dest[cwd_len++] = L'\\';
235
236 if (git__utf8_to_16(dest + cwd_len, MAX_PATH - cwd_len, src) < 0)
237 goto on_error;
238 }
239
240 return git_win32_path_canonicalize(out);
241
242 on_error:
243 /* set windows error code so we can use its error message */
244 if (errno == ENAMETOOLONG)
245 SetLastError(ERROR_FILENAME_EXCED_RANGE);
246
247 return -1;
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 static bool path_is_volume(wchar_t *target, size_t target_len)
313 {
314 return (target_len && wcsncmp(target, L"\\??\\Volume{", 11) == 0);
315 }
316
317 /* On success, returns the length, in characters, of the path stored in dest.
318 * On failure, returns a negative value. */
319 int git_win32_path_readlink_w(git_win32_path dest, const git_win32_path path)
320 {
321 BYTE buf[MAXIMUM_REPARSE_DATA_BUFFER_SIZE];
322 GIT_REPARSE_DATA_BUFFER *reparse_buf = (GIT_REPARSE_DATA_BUFFER *)buf;
323 HANDLE handle = NULL;
324 DWORD ioctl_ret;
325 wchar_t *target;
326 size_t target_len;
327
328 int error = -1;
329
330 handle = CreateFileW(path, GENERIC_READ,
331 FILE_SHARE_READ | FILE_SHARE_DELETE, NULL, OPEN_EXISTING,
332 FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS, NULL);
333
334 if (handle == INVALID_HANDLE_VALUE) {
335 errno = ENOENT;
336 return -1;
337 }
338
339 if (!DeviceIoControl(handle, FSCTL_GET_REPARSE_POINT, NULL, 0,
340 reparse_buf, sizeof(buf), &ioctl_ret, NULL)) {
341 errno = EINVAL;
342 goto on_error;
343 }
344
345 switch (reparse_buf->ReparseTag) {
346 case IO_REPARSE_TAG_SYMLINK:
347 target = reparse_buf->SymbolicLinkReparseBuffer.PathBuffer +
348 (reparse_buf->SymbolicLinkReparseBuffer.SubstituteNameOffset / sizeof(WCHAR));
349 target_len = reparse_buf->SymbolicLinkReparseBuffer.SubstituteNameLength / sizeof(WCHAR);
350 break;
351 case IO_REPARSE_TAG_MOUNT_POINT:
352 target = reparse_buf->MountPointReparseBuffer.PathBuffer +
353 (reparse_buf->MountPointReparseBuffer.SubstituteNameOffset / sizeof(WCHAR));
354 target_len = reparse_buf->MountPointReparseBuffer.SubstituteNameLength / sizeof(WCHAR);
355 break;
356 default:
357 errno = EINVAL;
358 goto on_error;
359 }
360
361 if (path_is_volume(target, target_len)) {
362 /* This path is a reparse point that represents another volume mounted
363 * at this location, it is not a symbolic link our input was canonical.
364 */
365 errno = EINVAL;
366 error = -1;
367 } else if (target_len) {
368 /* The path may need to have a namespace prefix removed. */
369 target_len = git_win32_path_remove_namespace(target, target_len);
370
371 /* Need one additional character in the target buffer
372 * for the terminating NULL. */
373 if (GIT_WIN_PATH_UTF16 > target_len) {
374 wcscpy(dest, target);
375 error = (int)target_len;
376 }
377 }
378
379 on_error:
380 CloseHandle(handle);
381 return error;
382 }
383
384 /**
385 * Removes any trailing backslashes from a path, except in the case of a drive
386 * letter path (C:\, D:\, etc.). This function cannot fail.
387 *
388 * @param path The path which should be trimmed.
389 * @return The length of the modified string (<= the input length)
390 */
391 size_t git_win32_path_trim_end(wchar_t *str, size_t len)
392 {
393 while (1) {
394 if (!len || str[len - 1] != L'\\')
395 break;
396
397 /*
398 * Don't trim backslashes from drive letter paths, which
399 * are 3 characters long and of the form C:\, D:\, etc.
400 */
401 if (len == 3 && git_win32__isalpha(str[0]) && str[1] == ':')
402 break;
403
404 len--;
405 }
406
407 str[len] = L'\0';
408
409 return len;
410 }
411
412 /**
413 * Removes any of the following namespace prefixes from a path,
414 * if found: "\??\", "\\?\", "\\?\UNC\". This function cannot fail.
415 *
416 * @param path The path which should be converted.
417 * @return The length of the modified string (<= the input length)
418 */
419 size_t git_win32_path_remove_namespace(wchar_t *str, size_t len)
420 {
421 static const wchar_t dosdevices_namespace[] = L"\\\?\?\\";
422 static const wchar_t nt_namespace[] = L"\\\\?\\";
423 static const wchar_t unc_namespace_remainder[] = L"UNC\\";
424 static const wchar_t unc_prefix[] = L"\\\\";
425
426 const wchar_t *prefix = NULL, *remainder = NULL;
427 size_t prefix_len = 0, remainder_len = 0;
428
429 /* "\??\" -- DOS Devices prefix */
430 if (len >= CONST_STRLEN(dosdevices_namespace) &&
431 !wcsncmp(str, dosdevices_namespace, CONST_STRLEN(dosdevices_namespace))) {
432 remainder = str + CONST_STRLEN(dosdevices_namespace);
433 remainder_len = len - CONST_STRLEN(dosdevices_namespace);
434 }
435 /* "\\?\" -- NT namespace prefix */
436 else if (len >= CONST_STRLEN(nt_namespace) &&
437 !wcsncmp(str, nt_namespace, CONST_STRLEN(nt_namespace))) {
438 remainder = str + CONST_STRLEN(nt_namespace);
439 remainder_len = len - CONST_STRLEN(nt_namespace);
440 }
441
442 /* "\??\UNC\", "\\?\UNC\" -- UNC prefix */
443 if (remainder_len >= CONST_STRLEN(unc_namespace_remainder) &&
444 !wcsncmp(remainder, unc_namespace_remainder, CONST_STRLEN(unc_namespace_remainder))) {
445
446 /*
447 * The proper Win32 path for a UNC share has "\\" at beginning of it
448 * and looks like "\\server\share\<folderStructure>". So remove the
449 * UNC namespace and add a prefix of "\\" in its place.
450 */
451 remainder += CONST_STRLEN(unc_namespace_remainder);
452 remainder_len -= CONST_STRLEN(unc_namespace_remainder);
453
454 prefix = unc_prefix;
455 prefix_len = CONST_STRLEN(unc_prefix);
456 }
457
458 if (remainder) {
459 /*
460 * Sanity check that the new string isn't longer than the old one.
461 * (This could only happen due to programmer error introducing a
462 * prefix longer than the namespace it replaces.)
463 */
464 assert(len >= remainder_len + prefix_len);
465
466 if (prefix)
467 memmove(str, prefix, prefix_len * sizeof(wchar_t));
468
469 memmove(str + prefix_len, remainder, remainder_len * sizeof(wchar_t));
470
471 len = remainder_len + prefix_len;
472 str[len] = L'\0';
473 }
474
475 return git_win32_path_trim_end(str, len);
476 }