Commits

Gary Oberbrunner  committed 8ec3921

Fix for issue #1420, Windows UNC path handling. Applied patch from Benoit Belley after updating it to fit into trunk, and added a couple of extra tests from issues 1420, 1857 and 1948.

  • Participants
  • Parent commits 04c48a1

Comments (0)

Files changed (4)

File src/CHANGES.txt

 
 RELEASE 2.1.0.alpha.yyyymmdd - NEW DATE WILL BE INSERTED HERE
 
+  From Benoit Belley:
+
+    - Much improved support for Windows UNC paths (\\SERVERNAME).
+
   From Gary Oberbrunner:
 
     - Fix Install() when the source and target are directories and the

File src/RELEASE.txt

       disk is corrected.
     - A problem with FS Entries which are dirs and have builders
       is corrected.
+    - A problem with Install() of a dir when the dest dir exists
+      is corrected.
     - Windows subprocess output should now be more reliable.
     - The users guide and man page have various fixes.
     - Appending to default $*FLAGS in a copied environment
       now works properly.
     - LaTeX scanner is improved for broken lines or embedded spaces.
+    - Windows UNC paths (\\SERVER\SHARE\dir) now work much better.
 
     - List fixes of outright bugs
 

File src/engine/SCons/Node/FS.py

 # 
 
 do_splitdrive = None
+_my_splitdrive =None
 
 def initialize_do_splitdrive():
     global do_splitdrive
+    global has_unc
     drive, path = os.path.splitdrive('X:/foo')
-    do_splitdrive = not not drive
+    has_unc = hasattr(os.path, 'splitunc')
+
+    do_splitdrive = not not drive or has_unc
+
+    global _my_splitdrive
+    if has_unc:
+        def splitdrive(p):
+            if p[1:2] == ':':
+                return p[:2], p[2:]
+            if p[0:2] == '//':
+                # Note that we leave a leading slash in the path
+                # because UNC paths are always absolute.
+                return '//', p[1:]
+            return '', p
+    else:
+        def splitdrive(p):
+            if p[1:2] == ':':
+                return p[:2], p[2:]
+            return '', p
+    _my_splitdrive = splitdrive
+
+    # Keep some commonly used values in global variables to skip to
+    # module look-up costs.
+    global OS_SEP
+    global UNC_PREFIX
+    global os_sep_is_slash
+    
+    OS_SEP = os.sep
+    UNC_PREFIX = OS_SEP + OS_SEP
+    os_sep_is_slash = OS_SEP == '/'
 
 initialize_do_splitdrive()
 
-#
-
-needs_normpath_check = None
-
-def initialize_normpath_check():
-    """
-    Initialize the normpath_check regular expression.
-
-    This function is used by the unit tests to re-initialize the pattern
-    when testing for behavior with different values of os.sep.
-    """
-    global needs_normpath_check
-    if os.sep == '/':
-        pattern = r'.*/|\.$|\.\.$'
-    else:
-        pattern = r'.*[/%s]|\.$|\.\.$' % re.escape(os.sep)
-    needs_normpath_check = re.compile(pattern)
-
-initialize_normpath_check()
+# Used to avoid invoking os.path.normpath if not necessary.
+needs_normpath_check = re.compile(
+    r'''
+      # We need to renormalize the path if it contains any consecutive
+      # '/' characters.
+      .*// |
+
+      # We need to renormalize the path if it contains a '..' directory.
+      # Note that we check for all the following cases:
+      #
+      #    a) The path is a single '..'
+      #    b) The path starts with '..'. E.g. '../' or '../moredirs'
+      #       but we not match '..abc/'.
+      #    c) The path ends with '..'. E.g. '/..' or 'dirs/..'
+      #    d) The path contains a '..' in the middle. 
+      #       E.g. dirs/../moredirs
+
+      (.*/)?\.\.(?:/|$) |
+
+      # We need to renormalize the path if it contains a '.'
+      # directory, but NOT if it is a single '.'  '/' characters. We
+      # do not want to match a single '.' because this case is checked
+      # for explicitely since this is common enough case.
+      #
+      # Note that we check for all the following cases:
+      #
+      #    a) We don't match a single '.'
+      #    b) We match if the path starts with '.'. E.g. './' or
+      #       './moredirs' but we not match '.abc/'.
+      #    c) We match if the path ends with '.'. E.g. '/.' or
+      #    'dirs/.'
+      #    d) We match if the path contains a '.' in the middle.
+      #       E.g. dirs/./moredirs
+
+      \./|.*/\.(?:/|$)
+
+    ''', 
+    re.VERBOSE
+    )
+needs_normpath_match = needs_normpath_check.match
 
 #
 # SCons.Action objects for interacting with the outside world.
     def __get_posix_path(self):
         """Return the path with / as the path separator,
         regardless of platform."""
-        if os.sep == '/':
+        if os_sep_is_slash:
             return self
         else:
             entry = self.get()
-            r = entry.get_path().replace(os.sep, '/')
+            r = entry.get_path().replace(OS_SEP, '/')
             return SCons.Subst.SpecialAttrWrapper(r, entry.name + "_posix")
 
     def __get_windows_path(self):
         """Return the path with \ as the path separator,
         regardless of platform."""
-        if os.sep == '\\':
+        if OS_SEP == '\\':
             return self
         else:
             entry = self.get()
-            r = entry.get_path().replace(os.sep, '\\')
+            r = entry.get_path().replace(OS_SEP, '\\')
             return SCons.Subst.SpecialAttrWrapper(r, entry.name + "_windows")
 
     def __get_srcnode(self):
         if self == dir:
             return '.'
         path_elems = self.path_elements
+        pathname = ''
         try: i = path_elems.index(dir)
-        except ValueError: pass
-        else: path_elems = path_elems[i+1:]
-        path_elems = [n.name for n in path_elems]
-        return os.sep.join(path_elems)
+        except ValueError: 
+            for p in path_elems[:-1]:
+                pathname += p.dirname
+        else:
+            for p in path_elems[i+1:-1]:
+                pathname += p.dirname
+        return pathname + path_elems[-1].name
 
     def set_src_builder(self, builder):
         """Set the source code builder for this node."""
             self.pathTop = os.getcwd()
         else:
             self.pathTop = path
-        self.defaultDrive = _my_normcase(os.path.splitdrive(self.pathTop)[0])
+        self.defaultDrive = _my_normcase(_my_splitdrive(self.pathTop)[0])
 
         self.Top = self.Dir(self.pathTop)
         self.Top.path = '.'
         # str(p) in case it's something like a proxy object
         p = str(p)
 
-        initial_hash = (p[0:1] == '#')
-        if initial_hash:
+        if not os_sep_is_slash:
+            p = p.replace(OS_SEP, '/')
+
+        if p[0:1] == '#':
             # There was an initial '#', so we strip it and override
             # whatever directory they may have specified with the
             # top-level SConstruct directory.
             p = p[1:]
             directory = self.Top
 
-        if directory and not isinstance(directory, Dir):
-            directory = self.Dir(directory)
-
-        if do_splitdrive:
-            drive, p = os.path.splitdrive(p)
+            # There might be a drive letter following the
+            # '#'. Although it is not described in the SCons man page,
+            # the regression test suite explicitly tests for that
+            # syntax. It seems to mean the following thing:
+            #
+            #   Assuming the the SCons top dir is in C:/xxx/yyy,
+            #   '#X:/toto' means X:/xxx/yyy/toto.
+            #
+            # i.e. it assumes that the X: drive has a directory
+            # structure similar to the one found on drive C:.
+            if do_splitdrive:
+                drive, p = _my_splitdrive(p)
+                if drive:
+                    root = self.get_root(drive)
+                else:
+                    root = directory.root
+            else:
+                root = directory.root
+
+            # We can only strip trailing after splitting the drive
+            # since the drive might the UNC '//' prefix.
+            p = p.strip('/')
+
+            needs_normpath = needs_normpath_match(p)
+            
+            # The path is relative to the top-level SCons directory.
+            if p in ('', '.'):
+                p = directory.labspath
+            else:
+                p = directory.labspath + '/' + p
         else:
-            drive = ''
-        if drive and not p:
-            # This causes a naked drive letter to be treated as a synonym
-            # for the root directory on that drive.
-            p = os.sep
-        absolute = os.path.isabs(p)
-
-        needs_normpath = needs_normpath_check.match(p)
-
-        if initial_hash or not absolute:
-            # This is a relative lookup, either to the top-level
-            # SConstruct directory (because of the initial '#') or to
-            # the current directory (the path name is not absolute).
-            # Add the string to the appropriate directory lookup path,
-            # after which the whole thing gets normalized.
-            if not directory:
-                directory = self._cwd
-            if p:
-                p = directory.labspath + '/' + p
+            if do_splitdrive:
+                drive, p = _my_splitdrive(p)
+                if drive and not p:
+                    # This causes a naked drive letter to be treated
+                    # as a synonym for the root directory on that
+                    # drive.
+                    p = '/'
             else:
-                p = directory.labspath
-
-        if needs_normpath:
-            p = os.path.normpath(p)
-
-        if drive or absolute:
-            root = self.get_root(drive)
-        else:
-            if not directory:
-                directory = self._cwd
-            root = directory.root
-
-        if os.sep != '/':
-            p = p.replace(os.sep, '/')
+                drive = ''
+
+            # We can only strip trailing '/' since the drive might the
+            # UNC '//' prefix.
+            if p != '/':
+                p = p.rstrip('/')
+
+            needs_normpath = needs_normpath_match(p)
+
+            if p[0:1] == '/':
+                # Absolute path
+                root = self.get_root(drive)
+            else:
+                # This is a relative lookup or to the current directory
+                # (the path name is not absolute).  Add the string to the
+                # appropriate directory lookup path, after which the whole
+                # thing gets normalized.
+                if directory:
+                    if not isinstance(directory, Dir):
+                        directory = self.Dir(directory)
+                else:
+                    directory = self._cwd
+
+                if p in ('', '.'):
+                    p = directory.labspath
+                else:
+                    p = directory.labspath + '/' + p
+
+                if drive:
+                    root = self.get_root(drive)
+                else:
+                    root = directory.root
+
+        if needs_normpath is not None:
+            # Normalize a pathname. Will return the same result for
+            # equivalent paths.
+            #
+            # We take advantage of the fact that we have an absolute
+            # path here for sure. In addition, we know that the
+            # components of lookup path are separated by slashes at
+            # this point. Because of this, this code is about 2X
+            # faster than calling os.path.normpath() followed by
+            # replacing os.sep with '/' again.
+            ins = p.split('/')[1:]
+            outs = []
+            for d in ins:
+                if d == '..':
+                    try:
+                        outs.pop()
+                    except IndexError:
+                        pass
+                elif d not in ('', '.'):
+                    outs.append(d)
+            p = '/' + '/'.join(outs)
+
         return root._lookup_abs(p, fsclass, create)
 
     def Entry(self, name, directory = None, create = 1):
         top = self.fs.Top
         root = top.root
         if do_splitdrive:
-            drive, s = os.path.splitdrive(s)
+            drive, s = _my_splitdrive(s)
             if drive:
                 root = self.fs.get_root(drive)
         if not os.path.isabs(s):
         self.variant_dirs = []
         self.root = self.dir.root
 
+        # For directories, we make a difference between the directory
+        # 'name' and the directory 'dirname'. The 'name' attribute is
+        # used when we need to print the 'name' of the directory or
+        # when we it is used as the last part of a path. The 'dirname'
+        # is used when the directory is not the last element of the
+        # path. The main reason for making that distinction is that
+        # for RoorDir's the dirname can not be easily inferred from
+        # the name. For example, we have to add a '/' after a drive
+        # letter but not after a UNC path prefix ('//').
+        self.dirname = self.name + OS_SEP
+
         # Don't just reset the executor, replace its action list,
         # because it might have some pre-or post-actions that need to
         # be preserved.
         """
         return self.fs.File(name, self)
 
-    def _lookup_rel(self, name, klass, create=1):
-        """
-        Looks up a *normalized* relative path name, relative to this
-        directory.
-
-        This method is intended for use by internal lookups with
-        already-normalized path data.  For general-purpose lookups,
-        use the Entry(), Dir() and File() methods above.
-
-        This method does *no* input checking and will die or give
-        incorrect results if it's passed a non-normalized path name (e.g.,
-        a path containing '..'), an absolute path name, a top-relative
-        ('#foo') path name, or any kind of object.
-        """
-        name = self.entry_labspath(name)
-        return self.root._lookup_abs(name, klass, create)
-
     def link(self, srcdir, duplicate):
         """Set this directory as the variant directory for the
         supplied source directory."""
             if fname == '.':
                 fname = dir.name
             else:
-                fname = dir.name + os.sep + fname
+                fname = dir.name + OS_SEP + fname
             dir = dir.up()
 
         self._memo['get_all_rdirs'] = list(result)
             self.__clearRepositoryCache()
 
     def up(self):
-        return self.entries['..']
+        return self.dir
 
     def _rel_path_key(self, other):
         return str(other)
                     if dir_rel_path == '.':
                         result = other.name
                     else:
-                        result = dir_rel_path + os.sep + other.name
+                        result = dir_rel_path + OS_SEP + other.name
         else:
             i = self.path_elements.index(other) + 1
 
             path_elems = ['..'] * (len(self.path_elements) - i) \
                          + [n.name for n in other.path_elements[i:]]
              
-            result = os.sep.join(path_elems)
+            result = OS_SEP.join(path_elems)
 
         memo_dict[other] = result
 
         return stamp
 
     def entry_abspath(self, name):
-        return self.abspath + os.sep + name
+        return self.abspath + OS_SEP + name
 
     def entry_labspath(self, name):
         return self.labspath + '/' + name
 
     def entry_path(self, name):
-        return self.path + os.sep + name
+        return self.path + OS_SEP + name
 
     def entry_tpath(self, name):
-        return self.tpath + os.sep + name
+        return self.tpath + OS_SEP + name
 
     def entry_exists_on_disk(self, name):
         try:
             if result is None:
                 # Belt-and-suspenders for Windows:  check directly for
                 # 8.3 file names that don't show up in os.listdir().
-                result = os.path.exists(self.abspath + os.sep + name)
+                result = os.path.exists(self.abspath + OS_SEP + name)
                 d[name] = result
             return result
         else:
         while dir:
             if dir.srcdir:
                 result.append(dir.srcdir.Dir(dirname))
-            dirname = dir.name + os.sep + dirname
+            dirname = dir.name + OS_SEP + dirname
             dir = dir.up()
 
         self._memo['srcdir_list'] = result
     add a separator when creating the path names of entries within
     this directory.
     """
-    def __init__(self, name, fs):
+    def __init__(self, drive, fs):
         if __debug__: logInstanceCreation(self, 'Node.FS.RootDir')
         # We're going to be our own parent directory (".." entry and .dir
         # attribute) so we have to set up some values so Base.__init__()
         self.path_elements = []
         self.duplicate = 0
         self.root = self
+
+        # Handle all the types of drives:
+        if drive == '':
+            # No drive, regular UNIX root or Windows default drive.
+            name = OS_SEP 
+            dirname = OS_SEP
+        elif drive == '//':
+            # UNC path
+            name = UNC_PREFIX
+            dirname = UNC_PREFIX
+        else:
+            # Windows drive letter
+            name = drive
+            dirname = drive + OS_SEP
+
         Base.__init__(self, name, self, fs)
 
-        # Now set our paths to what we really want them to be: the
-        # initial drive letter (the name) plus the directory separator,
-        # except for the "lookup abspath," which does not have the
-        # drive letter.
-        self.abspath = name + os.sep
+        # Now set our paths to what we really want them to be. The
+        # name should already contain any necessary separators, such
+        # as the initial drive letter (the name) plus the directory
+        # separator, except for the "lookup abspath," which does not
+        # have the drive letter.
+        self.abspath = dirname
         self.labspath = ''
-        self.path = name + os.sep
-        self.tpath = name + os.sep
+        self.path = dirname
+        self.tpath = dirname
         self._morph()
 
+        # Must be reset after Dir._morph() is invoked...
+        self.dirname = dirname
+
         self._lookupDict = {}
 
-        # The // and os.sep + os.sep entries are necessary because
-        # os.path.normpath() seems to preserve double slashes at the
-        # beginning of a path (presumably for UNC path names), but
-        # collapses triple slashes to a single slash.
         self._lookupDict[''] = self
         self._lookupDict['/'] = self
-        self._lookupDict['//'] = self
-        self._lookupDict[os.sep] = self
-        self._lookupDict[os.sep + os.sep] = self
+
+        # The // entry is necessary because os.path.normpath()
+        # preserves double slashes at the beginning of a path on Posix
+        # platforms.
+        if not has_unc:
+            self._lookupDict['//'] = self
 
     def must_be_same(self, klass):
         if klass is Dir:
                 raise SCons.Errors.UserError(msg)
             # There is no Node for this path name, and we're allowed
             # to create it.
-            dir_name, file_name = os.path.split(p)
+            dir_name, file_name = p.rsplit('/',1)
             dir_node = self._lookup_abs(dir_name, Dir)
             result = klass(file_name, dir_node, self.fs)
 
         top = self.fs.Top
         root = top.root
         if do_splitdrive:
-            drive, s = os.path.splitdrive(s)
+            drive, s = _my_splitdrive(s)
             if drive:
                 root = self.fs.get_root(drive)
         if not os.path.isabs(s):
         usual string representation: relative to the top-level SConstruct
         directory, or an absolute path if it's outside.
         """
-        if os.sep == '/':
+        if os_sep_is_slash:
             node_to_str = str
         else:
             def node_to_str(n):
                 except AttributeError:
                     s = str(n)
                 else:
-                    s = s.replace(os.sep, '/')
+                    s = s.replace(OS_SEP, '/')
                 return s
         for attr in ['bsources', 'bdepends', 'bimplicit']:
             try:
         if fd is None:
             fd = self.default_filedir
         dir, name = os.path.split(fd)
-        drive, d = os.path.splitdrive(dir)
-        if not name and d[:1] in ('/', os.sep):
+        drive, d = _my_splitdrive(dir)
+        if not name and d[:1] in ('/', OS_SEP):
             #return p.fs.get_root(drive).dir_on_disk(name)
             return p.fs.get_root(drive)
         if dir:

File src/engine/SCons/Node/FSTests.py

         assert format == expect, (repr(expect), repr(format))
 
 class FSTestCase(_tempdirTestCase):
+    def test_needs_normpath(self):
+        """Test the needs_normpath Regular expression
+
+        This test case verifies that the regular expression used to
+        determine whether a path needs normalization works as
+        expected.
+        """
+        needs_normpath_match = SCons.Node.FS.needs_normpath_match
+
+        do_not_need_normpath = [
+            ".",
+            "/",
+            "/a",
+            "/aa",
+            "/a/",
+            "/aa/",
+            "/a/b",
+            "/aa/bb",
+            "/a/b/",
+            "/aa/bb/",
+
+            "",
+            "a",
+            "aa",
+            "a/",
+            "aa/",
+            "a/b",
+            "aa/bb",
+            "a/b/",
+            "aa/bb/",
+
+            "a.",
+            "a..",
+            "/a.",
+            "/a..",
+            "a./",
+            "a../",
+            "/a./",
+            "/a../",
+
+
+            ".a",
+            "..a",
+            "/.a",
+            "/..a",
+            ".a/",
+            "..a/",
+            "/.a/",
+            "/..a/",
+            ]
+        for p in do_not_need_normpath:
+            assert needs_normpath_match(p) is None, p
+
+        needs_normpath = [
+            "//",
+            "//a",
+            "//aa",
+            "//a/",
+            "//a/",
+            "/aa//",
+
+            "//a/b",
+            "//aa/bb",
+            "//a/b/",
+            "//aa/bb/",
+
+            "/a//b",
+            "/aa//bb",
+            "/a/b//",
+            "/aa/bb//",
+
+            "/a/b//",
+            "/aa/bb//",
+
+            "a//",
+            "aa//",
+            "a//b",
+            "aa//bb",
+            "a//b/",
+            "aa//bb/",
+            "a/b//",
+            "aa/bb//",
+
+            "..",
+            "/.",
+            "/..",
+            "./",
+            "../",
+            "/./",
+            "/../",
+
+            "a/.",
+            "a/..",
+            "./a",
+            "../a",
+            "a/./a",
+            "a/../a",
+            ]
+        for p in needs_normpath:
+            assert needs_normpath_match(p) is not None, p
+
     def test_runTest(self):
         """Test FS (file system) Node operations
 
             path = strip_slash(path_)
             abspath = strip_slash(abspath_)
             up_path = strip_slash(up_path_)
+
             name = abspath.split(os.sep)[-1]
 
+            if not name:
+                if drive:
+                    name = drive
+                else:
+                    name = os.sep
+
+            if dir.up() is None:
+                dir_up_path =  dir.path
+            else:
+                dir_up_path =  dir.up().path
+
             assert dir.name == name, \
                    "dir.name %s != expected name %s" % \
                    (dir.name, name)
             assert dir.get_abspath() == abspath, \
                    "dir.abspath %s != expected absolute path %s" % \
                    (dir.get_abspath(), abspath)
-            assert dir.up().path == up_path, \
+            assert dir_up_path == up_path, \
                    "dir.up().path %s != expected parent path %s" % \
-                   (dir.up().path, up_path)
+                   (dir_up_path, up_path)
 
         for sep in seps:
 
             def Dir_test(lpath, path_, abspath_, up_path_, sep=sep, func=_do_Dir_test):
                 return func(lpath, path_, abspath_, up_path_, sep)
-
+            
+            Dir_test('/',           '/',           '/',               '/')
             Dir_test('',            './',          sub_dir,           sub)
             Dir_test('foo',         'foo/',        sub_dir_foo,       './')
             Dir_test('foo/bar',     'foo/bar/',    sub_dir_foo_bar,   'foo/')
 
         test.subdir('sub', ['sub', 'dir'])
 
-        def drive_workpath(drive, dirs, test=test):
+        def drive_workpath(dirs, test=test):
             x = test.workpath(*dirs)
             drive, path = os.path.splitdrive(x)
             return 'X:' + path
 
-        wp              = drive_workpath('X:', [''])
+        wp              = drive_workpath([''])
 
         if wp[-1] in (os.sep, '/'):
             tmp         = os.path.split(wp[:-1])[0]
 
         tmp_foo         = os.path.join(tmp, 'foo')
 
-        foo             = drive_workpath('X:', ['foo'])
-        foo_bar         = drive_workpath('X:', ['foo', 'bar'])
-        sub             = drive_workpath('X:', ['sub', ''])
-        sub_dir         = drive_workpath('X:', ['sub', 'dir', ''])
-        sub_dir_foo     = drive_workpath('X:', ['sub', 'dir', 'foo', ''])
-        sub_dir_foo_bar = drive_workpath('X:', ['sub', 'dir', 'foo', 'bar', ''])
-        sub_foo         = drive_workpath('X:', ['sub', 'foo', ''])
+        foo             = drive_workpath(['foo'])
+        foo_bar         = drive_workpath(['foo', 'bar'])
+        sub             = drive_workpath(['sub', ''])
+        sub_dir         = drive_workpath(['sub', 'dir', ''])
+        sub_dir_foo     = drive_workpath(['sub', 'dir', 'foo', ''])
+        sub_dir_foo_bar = drive_workpath(['sub', 'dir', 'foo', 'bar', ''])
+        sub_foo         = drive_workpath(['sub', 'foo', ''])
 
         fs = SCons.Node.FS.FS()
 
             os.path = ntpath
             os.sep = '\\'
             SCons.Node.FS.initialize_do_splitdrive()
-            SCons.Node.FS.initialize_normpath_check()
 
             for sep in seps:
 
             os.path = save_os_path
             os.sep = save_os_sep
             SCons.Node.FS.initialize_do_splitdrive()
-            SCons.Node.FS.initialize_normpath_check()
+
+    def test_unc_path(self):
+        """Test UNC path look-ups"""
+
+        test = self.test
+
+        test.subdir('sub', ['sub', 'dir'])
+
+        def strip_slash(p):
+            if p[-1] == os.sep and len(p) > 3:
+                p = p[:-1]
+            return p
+
+        def unc_workpath(dirs, test=test):
+            import ntpath
+            x = apply(test.workpath, dirs)
+            drive, path = ntpath.splitdrive(x)
+            unc, path = ntpath.splitunc(path)
+            path = strip_slash(path)
+            return '//' + path[1:]
+
+        wp              = unc_workpath([''])
+
+        if wp[-1] in (os.sep, '/'):
+            tmp         = os.path.split(wp[:-1])[0]
+        else:
+            tmp         = os.path.split(wp)[0]
+
+        parent_tmp      = os.path.split(tmp)[0]
+
+        tmp_foo         = os.path.join(tmp, 'foo')
+
+        foo             = unc_workpath(['foo'])
+        foo_bar         = unc_workpath(['foo', 'bar'])
+        sub             = unc_workpath(['sub', ''])
+        sub_dir         = unc_workpath(['sub', 'dir', ''])
+        sub_dir_foo     = unc_workpath(['sub', 'dir', 'foo', ''])
+        sub_dir_foo_bar = unc_workpath(['sub', 'dir', 'foo', 'bar', ''])
+        sub_foo         = unc_workpath(['sub', 'foo', ''])
+
+        fs = SCons.Node.FS.FS()
+
+        seps = [os.sep]
+        if os.sep != '/':
+            seps = seps + ['/']
+
+        def _do_Dir_test(lpath, path, up_path, sep, fileSys=fs):
+            dir = fileSys.Dir(lpath.replace('/', sep))
+
+            if os.sep != '/':
+                path = path.replace('/', os.sep)
+                up_path = up_path.replace('/', os.sep)
+
+            if path == os.sep + os.sep:
+                name = os.sep + os.sep
+            else:
+                name = path.split(os.sep)[-1]
+
+            if dir.up() is None:
+                dir_up_path =  dir.path
+            else:
+                dir_up_path =  dir.up().path
+
+            assert dir.name == name, \
+                   "dir.name %s != expected name %s" % \
+                   (dir.name, name)
+            assert dir.path == path, \
+                   "dir.path %s != expected path %s" % \
+                   (dir.path, path)
+            assert str(dir) == path, \
+                   "str(dir) %s != expected path %s" % \
+                   (str(dir), path)
+            assert dir_up_path == up_path, \
+                   "dir.up().path %s != expected parent path %s" % \
+                   (dir.up().path, up_path)
+
+        save_os_path = os.path
+        save_os_sep = os.sep
+        try:
+            import ntpath
+            os.path = ntpath
+            os.sep = '\\'
+            SCons.Node.FS.initialize_do_splitdrive()
+
+            for sep in seps:
+
+                def Dir_test(lpath, path_, up_path_, sep=sep, func=_do_Dir_test):
+                    return func(lpath, path_, up_path_, sep)
+
+                Dir_test('//foo',           '//foo',       '//')
+                Dir_test('//foo/bar',       '//foo/bar',   '//foo')
+                Dir_test('//',              '//',          '//')
+                Dir_test('//..',            '//',          '//')
+                Dir_test('//foo/..',        '//',          '//')
+                Dir_test('//../foo',        '//foo',       '//')
+                Dir_test('//.',             '//',          '//')
+                Dir_test('//./.',           '//',          '//')
+                Dir_test('//foo/./bar',     '//foo/bar',   '//foo')
+                Dir_test('//foo/../bar',    '//bar',       '//')
+                Dir_test('//foo/../../bar', '//bar',       '//')
+                Dir_test('//foo/bar/../..', '//',          '//')
+                Dir_test('#//',         wp,            tmp)
+                Dir_test('#//../foo',   tmp_foo,       tmp)
+                Dir_test('#//../foo',   tmp_foo,       tmp)
+                Dir_test('#//foo/bar',  foo_bar,       foo)
+                Dir_test('#//foo/bar',  foo_bar,       foo)
+                Dir_test('#//',         wp,            tmp)
+        finally:
+            os.path = save_os_path
+            os.sep = save_os_sep
+            SCons.Node.FS.initialize_do_splitdrive()
 
     def test_target_from_source(self):
         """Test the method for generating target nodes from sources"""
         above_path = os.path.join(*['..']*len(dirs) + ['above'])
         above = d2.Dir(above_path)
 
+    def test_lookup_uncpath(self):
+        """Testing looking up a UNC path on Windows"""
+        if sys.platform not in ('win32',):
+            return
+        test = self.test
+        fs = self.fs
+        path='//servername/C$/foo'
+        f = self.fs._lookup('//servername/C$/foo', fs.Dir('#'), SCons.Node.FS.File)
+        # before the fix in this commit, this returned 'C:\servername\C$\foo'
+        # Should be a normalized Windows UNC path as below.
+        assert str(f) == r'\\servername\C$\foo', \
+        'UNC path %s got looked up as %s'%(path, f)
+ 
+    def test_unc_drive_letter(self):
+        """Test drive-letter lookup for windows UNC-style directories"""
+        if sys.platform not in ('win32',):
+            return
+        share = self.fs.Dir(r'\\SERVER\SHARE\Directory')
+        assert str(share) == r'\\SERVER\SHARE\Directory', str(share)
+
     def test_rel_path(self):
         """Test the rel_path() method"""
         test = self.test