]> git.rkrishnan.org Git - tahoe-lafs/tahoe-lafs.git/blobdiff - src/allmydata/util/fileutil.py
Ensure the 'base' argument to abspath_expanduser_unicode takes effect on Windows...
[tahoe-lafs/tahoe-lafs.git] / src / allmydata / util / fileutil.py
index 9446a3f6a99888d5ea84507fb74d5da6564ebb84..4dd501ce82a505eaf8cb16b4d14aa218dae655a9 100644 (file)
@@ -263,9 +263,11 @@ def read(path):
     finally:
         rf.close()
 
-def put_file(pathname, inf):
+def put_file(path, inf):
+    precondition_abspath(path)
+
     # TODO: create temporary file and move into place?
-    outf = open(os.path.expanduser(pathname), "wb")
+    outf = open(path, "wb")
     try:
         while True:
             data = inf.read(32768)
@@ -276,6 +278,20 @@ def put_file(pathname, inf):
         outf.close()
 
 
+def precondition_abspath(path):
+    if not isinstance(path, unicode):
+        raise AssertionError("an abspath must be a Unicode string")
+
+    if sys.platform == "win32":
+        # This intentionally doesn't view absolute paths starting with a drive specification, or
+        # paths relative to the current drive, as acceptable.
+        if not path.startswith("\\\\"):
+            raise AssertionError("an abspath should be normalized using abspath_expanduser_unicode")
+    else:
+        # This intentionally doesn't view the path '~' or paths starting with '~/' as acceptable.
+        if not os.path.isabs(path):
+            raise AssertionError("an abspath should be normalized using abspath_expanduser_unicode")
+
 # Work around <http://bugs.python.org/issue3426>. This code is adapted from
 # <http://svn.python.org/view/python/trunk/Lib/ntpath.py?revision=78247&view=markup>
 # with some simplifications.
@@ -286,34 +302,78 @@ try:
 except ImportError:
     pass
 
-def abspath_expanduser_unicode(path):
-    """Return the absolute version of a path."""
-    assert isinstance(path, unicode), path
+def abspath_expanduser_unicode(path, base=None):
+    """
+    Return the absolute version of a path. If 'base' is given and 'path' is relative,
+    the path will be expanded relative to 'base'.
+    'path' must be a Unicode string. 'base', if given, must be a Unicode string
+    corresponding to an absolute path as returned by a previous call to
+    abspath_expanduser_unicode.
+    """
+    if not isinstance(path, unicode):
+        raise AssertionError("paths must be Unicode strings")
+    if base is not None:
+        precondition_abspath(base)
 
-    path = os.path.expanduser(path)
+    path = expanduser(path)
 
     if _getfullpathname:
-        # On Windows, os.path.isabs will return True for paths without a drive letter,
+        # On Windows, os.path.isabs will incorrectly return True
+        # for paths without a drive letter (that are not UNC paths),
         # e.g. "\\". See <http://bugs.python.org/issue1669539>.
         try:
-            path = _getfullpathname(path or u".")
+            if base is None:
+                path = _getfullpathname(path or u".")
+            else:
+                path = _getfullpathname(os.path.join(base, path))
         except OSError:
             pass
 
     if not os.path.isabs(path):
-        path = os.path.join(os.getcwdu(), path)
+        if base is None:
+            path = os.path.join(os.getcwdu(), path)
+        else:
+            path = os.path.join(base, path)
 
     # We won't hit <http://bugs.python.org/issue5827> because
     # there is always at least one Unicode path component.
-    return os.path.normpath(path)
+    path = os.path.normpath(path)
+
+    if sys.platform == "win32":
+        path = to_windows_long_path(path)
+
+    return path
+
+def to_windows_long_path(path):
+    # '/' is normally a perfectly valid path component separator in Windows.
+    # However, when using the "\\?\" syntax it is not recognized, so we
+    # replace it with '\' here.
+    path = path.replace(u"/", u"\\")
+
+    # Note that other normalizations such as removing '.' and '..' should
+    # be done outside this function.
+
+    if path.startswith(u"\\\\?\\") or path.startswith(u"\\\\.\\"):
+        return path
+    elif path.startswith(u"\\\\"):
+        return u"\\\\?\\UNC\\" + path[2 :]
+    else:
+        return u"\\\\?\\" + path
 
 
 have_GetDiskFreeSpaceExW = False
 if sys.platform == "win32":
-    try:
-        from ctypes import WINFUNCTYPE, windll, POINTER, byref, c_ulonglong
-        from ctypes.wintypes import BOOL, DWORD, LPCWSTR
+    from ctypes import WINFUNCTYPE, windll, POINTER, byref, c_ulonglong, create_unicode_buffer
+    from ctypes.wintypes import BOOL, DWORD, LPCWSTR, LPWSTR
+
+    # <http://msdn.microsoft.com/en-us/library/ms679360%28v=VS.85%29.aspx>
+    GetLastError = WINFUNCTYPE(DWORD)(("GetLastError", windll.kernel32))
+
+    # <http://msdn.microsoft.com/en-us/library/windows/desktop/ms683188%28v=vs.85%29.aspx>
+    GetEnvironmentVariableW = WINFUNCTYPE(DWORD, LPCWSTR, LPWSTR, DWORD)(
+        ("GetEnvironmentVariableW", windll.kernel32))
 
+    try:
         # <http://msdn.microsoft.com/en-us/library/aa383742%28v=VS.85%29.aspx>
         PULARGE_INTEGER = POINTER(c_ulonglong)
 
@@ -321,14 +381,59 @@ if sys.platform == "win32":
         GetDiskFreeSpaceExW = WINFUNCTYPE(BOOL, LPCWSTR, PULARGE_INTEGER, PULARGE_INTEGER, PULARGE_INTEGER)(
             ("GetDiskFreeSpaceExW", windll.kernel32))
 
-        # <http://msdn.microsoft.com/en-us/library/ms679360%28v=VS.85%29.aspx>
-        GetLastError = WINFUNCTYPE(DWORD)(("GetLastError", windll.kernel32))
-
         have_GetDiskFreeSpaceExW = True
     except Exception:
         import traceback
         traceback.print_exc()
 
+def expanduser(path):
+    # os.path.expanduser is hopelessly broken for Unicode paths on Windows (ticket #1674).
+    if sys.platform == "win32":
+        return windows_expanduser(path)
+    else:
+        return os.path.expanduser(path)
+
+def windows_expanduser(path):
+    if not path.startswith('~'):
+        return path
+    home_drive = windows_getenv(u'HOMEDRIVE')
+    home_path = windows_getenv(u'HOMEPATH')
+    if path == '~':
+        return os.path.join(home_drive, home_path)
+    elif path.startswith('~/') or path.startswith('~\\'):
+        return os.path.join(home_drive, home_path, path[2 :])
+    else:
+        return path
+
+def windows_getenv(name):
+    # Based on <http://stackoverflow.com/questions/2608200/problems-with-umlauts-in-python-appdata-environvent-variable/2608368#2608368>,
+    # with improved error handling.
+    if not isinstance(name, unicode):
+        raise AssertionError("name must be Unicode")
+
+    n = GetEnvironmentVariableW(name, None, 0)
+    # GetEnvironmentVariableW returns DWORD, so n cannot be negative.
+    if n == 0:
+        err = GetLastError()
+        raise OSError("Windows error %d attempting to read size of environment variable %r"
+                      % (err, name))
+    if n == 1:
+        # Avoid an ambiguity between a zero-length string and an error in the return value of the
+        # call to GetEnvironmentVariableW below.
+        return u""
+
+    buf = create_unicode_buffer(u'\0'*n)
+    retval = GetEnvironmentVariableW(name, buf, n)
+    if retval == 0:
+        err = GetLastError()
+        raise OSError("Windows error %d attempting to read environment variable %r"
+                      % (err, name))
+    if retval >= n:
+        raise OSError("Unexpected result %d (expected less than %d) from GetEnvironmentVariableW attempting to read environment variable %r"
+                      % (retval, n, name))
+
+    return buf.value
+
 def get_disk_stats(whichdir, reserved_space=0):
     """Return disk statistics for the storage disk, in the form of a dict
     with the following fields.