Skip to content

Commit 15ac508

Browse files
committed
More UNC path handling and unittests
Adds uncpath.py, a py2 compatible utility for working with UNC paths Signed-off-by: javrin <[email protected]>
1 parent 2946047 commit 15ac508

File tree

3 files changed

+268
-15
lines changed

3 files changed

+268
-15
lines changed

src/rez/tests/test_utils.py

Lines changed: 78 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,25 @@
66
test 'utils' modules
77
"""
88
import os
9+
import sys
910

1011
from rez.config import config
1112
from rez.tests.util import TestBase, platform_dependent
1213
from rez.utils import cygpath, filesystem
1314
from rez.utils.platform_ import Platform, platform_
1415

16+
if platform_.name == "windows":
17+
from rez.utils import uncpath
18+
uncpath_available = True
19+
else:
20+
uncpath_available = False
21+
22+
if sys.version_info[:2] >= (3, 3):
23+
from unittest.mock import patch
24+
patch_available = True
25+
else:
26+
patch_available = False
27+
1528

1629
class TestCanonicalPath(TestBase):
1730
class CaseSensitivePlatform(Platform):
@@ -198,6 +211,12 @@ def test_windows_unc_paths(self):
198211
self.assertEqual(cygpath.to_posix_path(
199212
"\\\\server\\share\\folder\\file.txt"), "//server/share/folder/file.txt"
200213
)
214+
self.assertEqual(cygpath.to_posix_path(
215+
"\\\\server\\share/folder/file.txt"), "//server/share/folder/file.txt"
216+
)
217+
self.assertEqual(cygpath.to_posix_path(
218+
r"\\server\share/folder\//file.txt"), "//server/share/folder/file.txt"
219+
)
201220

202221
@platform_dependent(["windows"])
203222
def test_windows_long_paths(self):
@@ -357,14 +376,6 @@ def test_paths_with_no_drive_letter(self):
357376
'\\foo\\bar'
358377
)
359378

360-
self.assertRaisesRegex(
361-
ValueError,
362-
"Cannot convert path to mixed path: '.*' "
363-
"Please ensure that the path is absolute",
364-
cygpath.to_mixed_path,
365-
'\\\\my_folder\\my_file.txt'
366-
)
367-
368379
self.assertRaisesRegex(
369380
ValueError,
370381
"Cannot convert path to mixed path: '.*' "
@@ -394,3 +405,62 @@ def test_dotted_paths(self):
394405
cygpath.to_posix_path,
395406
"./projects/python"
396407
)
408+
409+
@platform_dependent(["windows"])
410+
def test_windows_unc_paths(self):
411+
self.assertRaisesRegex(
412+
ValueError,
413+
"Cannot convert path to mixed path: '.*' "
414+
"Unmapped UNC paths are not supported",
415+
cygpath.to_mixed_path,
416+
'\\\\my_folder\\my_file.txt'
417+
)
418+
self.assertRaisesRegex(
419+
ValueError,
420+
"Cannot convert path to mixed path: '.*' "
421+
"Unmapped UNC paths are not supported",
422+
cygpath.to_mixed_path,
423+
"\\\\Server\\Share\\folder"
424+
)
425+
self.assertRaisesRegex(
426+
ValueError,
427+
"Cannot convert path to mixed path: '.*' "
428+
"Unmapped UNC paths are not supported",
429+
cygpath.to_mixed_path,
430+
"\\\\server\\share\\folder\\file.txt"
431+
)
432+
self.assertRaisesRegex(
433+
ValueError,
434+
"Cannot convert path to mixed path: '.*' "
435+
"Unmapped UNC paths are not supported",
436+
cygpath.to_mixed_path,
437+
"\\\\server\\share/folder/file.txt"
438+
)
439+
self.assertRaisesRegex(
440+
ValueError,
441+
"Cannot convert path to mixed path: '.*' "
442+
"Unmapped UNC paths are not supported",
443+
cygpath.to_mixed_path,
444+
r"\\server\share/folder\//file.txt"
445+
)
446+
447+
@platform_dependent(["windows"])
448+
def test_windows_mapped_unc_paths(self):
449+
if not patch_available:
450+
raise self.skipTest("Patching not available")
451+
with patch.object(cygpath, 'to_mapped_drive', return_value="X:"):
452+
self.assertEqual(
453+
cygpath.to_mixed_path('\\\\server\\share\\folder'), 'X:/folder'
454+
)
455+
456+
457+
class TestToMappedDrive(TestBase):
458+
459+
@platform_dependent(["windows"])
460+
def test_already_mapped_drive(self):
461+
if not uncpath_available:
462+
raise self.skipTest("UNC path util not available")
463+
if not patch_available:
464+
raise self.skipTest("Unittest patch not available")
465+
with patch.object(uncpath, 'to_drive', return_value="X:"):
466+
self.assertEqual(cygpath.to_mapped_drive('\\\\server\\share'), 'X:')

src/rez/utils/cygpath.py

Lines changed: 74 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,15 @@
1313
import re
1414

1515
from rez.config import config
16+
from rez.utils import platform_
1617
from rez.utils.logging_ import print_debug
1718

19+
if platform_.name == "windows":
20+
from rez.utils import uncpath
21+
uncpath_available = True
22+
else:
23+
uncpath_available = False
24+
1825

1926
def log(*msg):
2027
if config.debug("cygpath"):
@@ -78,20 +85,27 @@ def convert(path, mode=None, env_var_seps=None):
7885
def to_posix_path(path):
7986
r"""Convert (eg) 'C:\foo' to '/c/foo'
8087
88+
Note: Especially for UNC paths, and as opposed mixed path conversion, this
89+
function will return a path that is not guaranteed to exist.
90+
8191
Args:
8292
path (str): Path to convert.
8393
8494
Returns:
8595
str: Converted path.
96+
97+
Raises:
98+
ValueError: If the path is not absolute or path is malformed
8699
"""
87100
# Handle Windows long paths
88101
if path.startswith("\\\\?\\"):
89102
path = path[4:]
90103

91-
unc, path = os.path.splitunc(path)
92-
if unc:
93-
path = unc.replace("\\", "/") + path.replace("\\", "/")
94-
return path
104+
# Handle UNC paths
105+
unc, unc_path = os.path.splitdrive(path)
106+
if unc and unc.startswith("\\\\"):
107+
unc_path = unc.replace("\\", "/") + slashify(unc_path)
108+
return unc_path
95109

96110
drive = to_cygdrive(path)
97111

@@ -125,12 +139,35 @@ def to_posix_path(path):
125139
def to_mixed_path(path):
126140
r"""Convert (eg) 'C:\foo\bin' to 'C:/foo/bin'
127141
142+
Note: Especially in the case of UNC paths, this function will return a path
143+
that is practically guaranteed to exist but it is not verified.
144+
128145
Args:
129146
path (str): Path to convert.
130147
131148
Returns:
132149
str: Converted path.
150+
151+
Raises:
152+
ValueError: If the path is not absolute or drive letter is not mapped
153+
to a UNC path.
133154
"""
155+
# Handle Windows long paths
156+
if path.startswith("\\\\?\\"):
157+
path = path[4:]
158+
159+
# Handle UNC paths
160+
# Return mapped drive letter if any, else raise
161+
unc, unc_path = os.path.splitdrive(path)
162+
if unc and unc.startswith("\\\\"):
163+
drive = to_mapped_drive(path)
164+
if drive:
165+
return drive.upper() + slashify(unc_path)
166+
raise ValueError(
167+
"Cannot convert path to mixed path: {!r} "
168+
"Unmapped UNC paths are not supported".format(path)
169+
)
170+
134171
drive, path = os.path.splitdrive(path)
135172

136173
if not drive:
@@ -152,6 +189,14 @@ def to_mixed_path(path):
152189

153190

154191
def slashify(path):
192+
"""Ensures path only contains forward slashes.
193+
194+
Args:
195+
path (str): Path to convert.
196+
197+
Returns:
198+
str: Converted path.
199+
"""
155200
# Remove double backslashes and dots
156201
path = os.path.normpath(path)
157202
# Normalize slashes
@@ -179,9 +224,13 @@ def to_cygdrive(path):
179224
# Normalize forward backslashes to slashes
180225
path = path.replace("\\", "/")
181226

182-
# UNC paths are not supported
183-
unc, _ = os.path.splitunc(path)
184-
if unc:
227+
# Handle UNC paths
228+
# Return mapped drive letter if any, else return ""
229+
unc, _ = os.path.splitdrive(path)
230+
if unc and unc.startswith("\\\\"):
231+
drive = to_mapped_drive(path)
232+
if drive:
233+
return posixpath.sep + drive.lower() + posixpath.sep
185234
return ""
186235

187236
if (
@@ -204,3 +253,21 @@ def to_cygdrive(path):
204253

205254
# Most likely a relative path
206255
return ""
256+
257+
258+
def to_mapped_drive(path):
259+
r"""Convert a UNC path to an NT drive if possible.
260+
261+
(eg) '\\\\server\\share\\folder' -> 'X:'
262+
263+
Args:
264+
path (str): UNC path.
265+
266+
Returns:
267+
str: Drive mapped to UNC, if any.
268+
"""
269+
if not uncpath_available:
270+
return
271+
unc, _ = os.path.splitdrive(path)
272+
if unc and unc.startswith("\\\\"):
273+
return uncpath.to_drive(unc)

src/rez/utils/uncpath.py

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
# SPDX-License-Identifier: Apache-2.0
2+
# Copyright Contributors to the Rez Project
3+
4+
5+
"""Get full UNC path from a network drive letter if it exists
6+
Adapted from: https://stackoverflow.com/a/34809340
7+
8+
Example:
9+
10+
drive_mapping = get_connections()
11+
12+
# Result:
13+
{
14+
'H:': u'\\\\server\\share\\username',
15+
'U:': u'\\\\server\\share\\simcache',
16+
'T:': u'\\\\server\\share',
17+
'C:': None,
18+
'Y:': u'\\\\server\\share\\reference',
19+
'Z:': u'\\\\server\\share\\production'
20+
'K:': u'\\\\server\\share2\\junk'
21+
'L:': u'\\\\server\\share\\library'
22+
'W:': u'\\\\server\\share\\mango'
23+
}
24+
25+
unc = to_unc('H:')
26+
# Result: u'\\\\server\\share\\username'
27+
28+
drive = to_drive('\\\\server\\share\\username')
29+
# Result: u'H:')
30+
31+
"""
32+
import ctypes
33+
from ctypes import wintypes
34+
import os
35+
import string
36+
37+
from rez.backport.lru_cache import lru_cache
38+
39+
mpr = ctypes.WinDLL('mpr')
40+
41+
ERROR_SUCCESS = 0x0000
42+
ERROR_MORE_DATA = 0x00EA
43+
44+
wintypes.LPDWORD = ctypes.POINTER(wintypes.DWORD)
45+
mpr.WNetGetConnectionW.restype = wintypes.DWORD
46+
mpr.WNetGetConnectionW.argtypes = (
47+
wintypes.LPCWSTR, wintypes.LPWSTR, wintypes.LPDWORD
48+
)
49+
50+
51+
@lru_cache()
52+
def get_connections():
53+
"""Get all available drive mappings
54+
55+
Note: This function is cached, so it only runs once per session.
56+
57+
Returns:
58+
dict: Drive mappings
59+
"""
60+
available_drives = [
61+
'%s:' % d for d in string.ascii_uppercase if os.path.exists('%s:' % d)
62+
]
63+
return dict([d, _get_connection(d)] for d in available_drives)
64+
65+
66+
def to_drive(unc):
67+
"""Get drive letter from a UNC path
68+
69+
Args:
70+
unc (str): UNC path
71+
72+
Returns:
73+
str: Drive letter
74+
"""
75+
connections = get_connections()
76+
drive = next(iter(k for k, v in connections.items() if v == unc), None)
77+
return drive
78+
79+
80+
def to_unc(drive):
81+
"""Get UNC path from a drive letter
82+
83+
Args:
84+
drive (str): Drive letter
85+
86+
Returns:
87+
str: UNC path
88+
"""
89+
connections = get_connections()
90+
unc = connections.get(drive, None)
91+
return unc
92+
93+
94+
def _get_connection(local_name, verbose=None):
95+
"""Get full UNC path from a network drive letter if it exists
96+
97+
Args:
98+
local_name (str): Drive letter name
99+
verbose (bool): Print errors
100+
101+
Returns:
102+
str: Full UNC path to connection
103+
"""
104+
length = (wintypes.DWORD * 1)()
105+
result = mpr.WNetGetConnectionW(local_name, None, length)
106+
if result != ERROR_MORE_DATA:
107+
if verbose:
108+
print(ctypes.WinError(result))
109+
return
110+
remote_name = (wintypes.WCHAR * length[0])()
111+
result = mpr.WNetGetConnectionW(local_name, remote_name, length)
112+
if result != ERROR_SUCCESS:
113+
if verbose:
114+
print(ctypes.WinError(result))
115+
return
116+
return remote_name.value

0 commit comments

Comments
 (0)