Skip to content

Commit 7c791f3

Browse files
authored
Merge pull request libgit2#4852 from libgit2/ethomson/unc_paths
Win32 path canonicalization refactoring
2 parents 6cc14ae + a34f5b0 commit 7c791f3

File tree

6 files changed

+172
-97
lines changed

6 files changed

+172
-97
lines changed

src/win32/path_w32.c

Lines changed: 101 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,7 @@ int git_win32_path_from_utf8(git_win32_path out, const char *src)
220220
goto on_error;
221221
}
222222

223-
/* Skip the drive letter specification ("C:") */
223+
/* Skip the drive letter specification ("C:") */
224224
if (git__utf8_to_16(dest + 2, MAX_PATH - 2, src) < 0)
225225
goto on_error;
226226
}
@@ -315,7 +315,7 @@ static bool path_is_volume(wchar_t *target, size_t target_len)
315315
}
316316

317317
/* On success, returns the length, in characters, of the path stored in dest.
318-
* On failure, returns a negative value. */
318+
* On failure, returns a negative value. */
319319
int git_win32_path_readlink_w(git_win32_path dest, const git_win32_path path)
320320
{
321321
BYTE buf[MAXIMUM_REPARSE_DATA_BUFFER_SIZE];
@@ -360,16 +360,16 @@ int git_win32_path_readlink_w(git_win32_path dest, const git_win32_path path)
360360

361361
if (path_is_volume(target, target_len)) {
362362
/* 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-
*/
363+
* at this location, it is not a symbolic link our input was canonical.
364+
*/
365365
errno = EINVAL;
366366
error = -1;
367367
} else if (target_len) {
368-
/* The path may need to have a prefix removed. */
369-
target_len = git_win32__canonicalize_path(target, target_len);
368+
/* The path may need to have a namespace prefix removed. */
369+
target_len = git_win32_path_remove_namespace(target, target_len);
370370

371371
/* Need one additional character in the target buffer
372-
* for the terminating NULL. */
372+
* for the terminating NULL. */
373373
if (GIT_WIN_PATH_UTF16 > target_len) {
374374
wcscpy(dest, target);
375375
error = (int)target_len;
@@ -380,3 +380,97 @@ int git_win32_path_readlink_w(git_win32_path dest, const git_win32_path path)
380380
CloseHandle(handle);
381381
return error;
382382
}
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+
}

src/win32/path_w32.h

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,4 +83,22 @@ extern char *git_win32_path_8dot3_name(const char *path);
8383

8484
extern int git_win32_path_readlink_w(git_win32_path dest, const git_win32_path path);
8585

86+
/**
87+
* Removes any trailing backslashes from a path, except in the case of a drive
88+
* letter path (C:\, D:\, etc.). This function cannot fail.
89+
*
90+
* @param path The path which should be trimmed.
91+
* @return The length of the modified string (<= the input length)
92+
*/
93+
size_t git_win32_path_trim_end(wchar_t *str, size_t len);
94+
95+
/**
96+
* Removes any of the following namespace prefixes from a path,
97+
* if found: "\??\", "\\?\", "\\?\UNC\". This function cannot fail.
98+
*
99+
* @param path The path which should be converted.
100+
* @return The length of the modified string (<= the input length)
101+
*/
102+
size_t git_win32_path_remove_namespace(wchar_t *str, size_t len);
103+
86104
#endif

src/win32/posix_w32.c

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -354,7 +354,7 @@ static int do_lstat(const char *path, struct stat *buf, bool posixly_correct)
354354
if ((len = git_win32_path_from_utf8(path_w, path)) < 0)
355355
return -1;
356356

357-
git_win32__path_trim_end(path_w, len);
357+
git_win32_path_trim_end(path_w, len);
358358

359359
return lstat_w(path_w, buf, posixly_correct);
360360
}
@@ -648,8 +648,8 @@ static int getfinalpath_w(
648648
if (!dwChars || dwChars >= GIT_WIN_PATH_UTF16)
649649
return -1;
650650

651-
/* The path may be delivered to us with a prefix; canonicalize */
652-
return (int)git_win32__canonicalize_path(dest, dwChars);
651+
/* The path may be delivered to us with a namespace prefix; remove */
652+
return (int)git_win32_path_remove_namespace(dest, dwChars);
653653
}
654654

655655
static int follow_and_lstat_link(git_win32_path path, struct stat* buf)

src/win32/w32_util.c

Lines changed: 0 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -93,71 +93,3 @@ int git_win32__hidden(bool *out, const char *path)
9393
*out = (attrs & FILE_ATTRIBUTE_HIDDEN) ? true : false;
9494
return 0;
9595
}
96-
97-
/**
98-
* Removes any trailing backslashes from a path, except in the case of a drive
99-
* letter path (C:\, D:\, etc.). This function cannot fail.
100-
*
101-
* @param path The path which should be trimmed.
102-
* @return The length of the modified string (<= the input length)
103-
*/
104-
size_t git_win32__path_trim_end(wchar_t *str, size_t len)
105-
{
106-
while (1) {
107-
if (!len || str[len - 1] != L'\\')
108-
break;
109-
110-
/* Don't trim backslashes from drive letter paths, which
111-
* are 3 characters long and of the form C:\, D:\, etc. */
112-
if (len == 3 && git_win32__isalpha(str[0]) && str[1] == ':')
113-
break;
114-
115-
len--;
116-
}
117-
118-
str[len] = L'\0';
119-
120-
return len;
121-
}
122-
123-
/**
124-
* Removes any of the following namespace prefixes from a path,
125-
* if found: "\??\", "\\?\", "\\?\UNC\". This function cannot fail.
126-
*
127-
* @param path The path which should be converted.
128-
* @return The length of the modified string (<= the input length)
129-
*/
130-
size_t git_win32__canonicalize_path(wchar_t *str, size_t len)
131-
{
132-
static const wchar_t dosdevices_prefix[] = L"\\\?\?\\";
133-
static const wchar_t nt_prefix[] = L"\\\\?\\";
134-
static const wchar_t unc_prefix[] = L"UNC\\";
135-
size_t to_advance = 0;
136-
137-
/* "\??\" -- DOS Devices prefix */
138-
if (len >= CONST_STRLEN(dosdevices_prefix) &&
139-
!wcsncmp(str, dosdevices_prefix, CONST_STRLEN(dosdevices_prefix))) {
140-
to_advance += CONST_STRLEN(dosdevices_prefix);
141-
len -= CONST_STRLEN(dosdevices_prefix);
142-
}
143-
/* "\\?\" -- NT namespace prefix */
144-
else if (len >= CONST_STRLEN(nt_prefix) &&
145-
!wcsncmp(str, nt_prefix, CONST_STRLEN(nt_prefix))) {
146-
to_advance += CONST_STRLEN(nt_prefix);
147-
len -= CONST_STRLEN(nt_prefix);
148-
}
149-
150-
/* "\??\UNC\", "\\?\UNC\" -- UNC prefix */
151-
if (to_advance && len >= CONST_STRLEN(unc_prefix) &&
152-
!wcsncmp(str + to_advance, unc_prefix, CONST_STRLEN(unc_prefix))) {
153-
to_advance += CONST_STRLEN(unc_prefix);
154-
len -= CONST_STRLEN(unc_prefix);
155-
}
156-
157-
if (to_advance) {
158-
memmove(str, str + to_advance, len * sizeof(wchar_t));
159-
str[len] = L'\0';
160-
}
161-
162-
return git_win32__path_trim_end(str, len);
163-
}

src/win32/w32_util.h

Lines changed: 0 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -59,24 +59,6 @@ extern int git_win32__set_hidden(const char *path, bool hidden);
5959
*/
6060
extern int git_win32__hidden(bool *hidden, const char *path);
6161

62-
/**
63-
* Removes any trailing backslashes from a path, except in the case of a drive
64-
* letter path (C:\, D:\, etc.). This function cannot fail.
65-
*
66-
* @param path The path which should be trimmed.
67-
* @return The length of the modified string (<= the input length)
68-
*/
69-
size_t git_win32__path_trim_end(wchar_t *str, size_t len);
70-
71-
/**
72-
* Removes any of the following namespace prefixes from a path,
73-
* if found: "\??\", "\\?\", "\\?\UNC\". This function cannot fail.
74-
*
75-
* @param path The path which should be converted.
76-
* @return The length of the modified string (<= the input length)
77-
*/
78-
size_t git_win32__canonicalize_path(wchar_t *str, size_t len);
79-
8062
/**
8163
* Converts a FILETIME structure to a struct timespec.
8264
*

tests/path/win32.c

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ void test_path_win32__absolute_from_relative(void)
129129
#endif
130130
}
131131

132-
void test_canonicalize(const wchar_t *in, const wchar_t *expected)
132+
static void test_canonicalize(const wchar_t *in, const wchar_t *expected)
133133
{
134134
#ifdef GIT_WIN32
135135
git_win32_path canonical;
@@ -145,6 +145,55 @@ void test_canonicalize(const wchar_t *in, const wchar_t *expected)
145145
#endif
146146
}
147147

148+
static void test_remove_namespace(const wchar_t *in, const wchar_t *expected)
149+
{
150+
#ifdef GIT_WIN32
151+
git_win32_path canonical;
152+
153+
cl_assert(wcslen(in) < MAX_PATH);
154+
wcscpy(canonical, in);
155+
156+
cl_must_pass(git_win32_path_remove_namespace(canonical, wcslen(in)));
157+
cl_assert_equal_wcs(expected, canonical);
158+
#else
159+
GIT_UNUSED(in);
160+
GIT_UNUSED(expected);
161+
#endif
162+
}
163+
164+
void test_path_win32__remove_namespace(void)
165+
{
166+
test_remove_namespace(L"\\\\?\\C:\\Temp\\Foo", L"C:\\Temp\\Foo");
167+
test_remove_namespace(L"\\\\?\\C:\\", L"C:\\");
168+
test_remove_namespace(L"\\\\?\\", L"");
169+
170+
test_remove_namespace(L"\\??\\C:\\Temp\\Foo", L"C:\\Temp\\Foo");
171+
test_remove_namespace(L"\\??\\C:\\", L"C:\\");
172+
test_remove_namespace(L"\\??\\", L"");
173+
174+
test_remove_namespace(L"\\\\?\\UNC\\server\\C$\\folder", L"\\\\server\\C$\\folder");
175+
test_remove_namespace(L"\\\\?\\UNC\\server\\C$\\folder", L"\\\\server\\C$\\folder");
176+
test_remove_namespace(L"\\\\?\\UNC\\server\\C$", L"\\\\server\\C$");
177+
test_remove_namespace(L"\\\\?\\UNC\\server\\", L"\\\\server");
178+
test_remove_namespace(L"\\\\?\\UNC\\server", L"\\\\server");
179+
180+
test_remove_namespace(L"\\??\\UNC\\server\\C$\\folder", L"\\\\server\\C$\\folder");
181+
test_remove_namespace(L"\\??\\UNC\\server\\C$\\folder", L"\\\\server\\C$\\folder");
182+
test_remove_namespace(L"\\??\\UNC\\server\\C$", L"\\\\server\\C$");
183+
test_remove_namespace(L"\\??\\UNC\\server\\", L"\\\\server");
184+
test_remove_namespace(L"\\??\\UNC\\server", L"\\\\server");
185+
186+
test_remove_namespace(L"\\\\server\\C$\\folder", L"\\\\server\\C$\\folder");
187+
test_remove_namespace(L"\\\\server\\C$", L"\\\\server\\C$");
188+
test_remove_namespace(L"\\\\server\\", L"\\\\server");
189+
test_remove_namespace(L"\\\\server", L"\\\\server");
190+
191+
test_remove_namespace(L"C:\\Foo\\Bar", L"C:\\Foo\\Bar");
192+
test_remove_namespace(L"C:\\", L"C:\\");
193+
test_remove_namespace(L"", L"");
194+
195+
}
196+
148197
void test_path_win32__canonicalize(void)
149198
{
150199
#ifdef GIT_WIN32

0 commit comments

Comments
 (0)