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 
path__cwd(wchar_t * path,int size)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 
path__skip_server(wchar_t * path)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 
path__skip_prefix(wchar_t * path)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 
git_win32_path_canonicalize(git_win32_path path)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 
win32_path_cwd(wchar_t * out,size_t len)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 > GIT_WIN_PATH_MAX - 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 > GIT_WIN_PATH_MAX - 2) {
191 		errno = ENAMETOOLONG;
192 		return -1;
193 	}
194 
195 	return cwd_len;
196 }
197 
git_win32_path_from_utf8(git_win32_path out,const char * src)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, GIT_WIN_PATH_MAX, 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, GIT_WIN_PATH_MAX, 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, GIT_WIN_PATH_MAX - 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, GIT_WIN_PATH_MAX) < 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, GIT_WIN_PATH_MAX - 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, GIT_WIN_PATH_MAX)) < 0)
245 			goto on_error;
246 
247 		dest[cwd_len++] = L'\\';
248 
249 		if (git__utf8_to_16(dest + cwd_len, GIT_WIN_PATH_MAX - 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 
git_win32_path_relative_from_utf8(git_win32_path out,const char * src)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, GIT_WIN_PATH_MAX, 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 
git_win32_path_to_utf8(git_win32_utf8_path dest,const wchar_t * src)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 
git_win32_path_8dot3_name(const char * path)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 
path_is_volume(wchar_t * target,size_t target_len)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. */
git_win32_path_readlink_w(git_win32_path dest,const git_win32_path path)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  */
git_win32_path_trim_end(wchar_t * str,size_t len)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  */
git_win32_path_remove_namespace(wchar_t * str,size_t len)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 	/*
496 	 * Sanity check that the new string isn't longer than the old one.
497 	 * (This could only happen due to programmer error introducing a
498 	 * prefix longer than the namespace it replaces.)
499 	 */
500 	if (remainder && len >= remainder_len + prefix_len) {
501 		if (prefix)
502 			memmove(str, prefix, prefix_len * sizeof(wchar_t));
503 
504 		memmove(str + prefix_len, remainder, remainder_len * sizeof(wchar_t));
505 
506 		len = remainder_len + prefix_len;
507 		str[len] = L'\0';
508 	}
509 
510 	return git_win32_path_trim_end(str, len);
511 }
512