Port dart:io Path tests to package:path.

BUG=
R=nweiz@google.com, rnystrom@google.com

Review URL: https://codereview.chromium.org//19231002

git-svn-id: https://dart.googlecode.com/svn/branches/bleeding_edge/dart/pkg/path@25403 260f80e4-7a28-3924-810f-c04153c831b5
diff --git a/README.md b/README.md
index dacacaa..4108ce0 100644
--- a/README.md
+++ b/README.md
@@ -73,6 +73,16 @@
 
     builder.dirname('path/to/'); // -> 'path'
 
+If an absolute path contains no directories, only a root, then the root
+is returned.
+
+    path.dirname('/');    // -> '/' (posix)
+    path.dirname('c:\');  // -> 'c:\' (windows)
+
+If a relative path has no directories, then '.' is returned.
+    path.dirname('foo');  // -> '.'
+    path.dirname('');     // -> '.'
+
 ### String extension(String path)
 
 Gets the file extension of [path]: the portion of [basename] from the last
@@ -200,6 +210,9 @@
     path.relative('/root/other.dart',
         from: '/root/path'); // -> '../other.dart'
 
+If [path] and/or [from] are relative paths, they are assumed to be relative
+to the current directory.
+
 Since there is no relative path from one drive letter to another on Windows,
 this will return an absolute path in that case.
 
diff --git a/lib/path.dart b/lib/path.dart
index cc0493c..bb88953 100644
--- a/lib/path.dart
+++ b/lib/path.dart
@@ -106,6 +106,17 @@
 /// Trailing separators are ignored.
 ///
 ///     builder.dirname('path/to/'); // -> 'path'
+///
+/// If an absolute path contains no directories, only a root, then the root
+/// is returned.
+///
+///     path.dirname('/');  // -> '/' (posix)
+///     path.dirname('c:\');  // -> 'c:\' (windows)
+///
+/// If a relative path has no directories, then '.' is returned.
+///
+///     path.dirname('foo');  // -> '.'
+///     path.dirname('');  // -> '.'
 String dirname(String path) => _builder.dirname(path);
 
 /// Gets the file extension of [path]: the portion of [basename] from the last
@@ -248,6 +259,9 @@
 ///     path.relative('/root/other.dart',
 ///         from: '/root/path'); // -> '../other.dart'
 ///
+/// If [path] and/or [from] are relative paths, they are assumed to be relative
+/// to the current directory.
+///
 /// Since there is no relative path from one drive letter to another on Windows,
 /// or from one hostname to another for URLs, this will return an absolute path
 /// in those cases.
@@ -582,8 +596,6 @@
   ///
   ///     builder.normalize('path/./to/..//file.text'); // -> 'path/file.txt'
   String normalize(String path) {
-    if (path == '') return path;
-
     var parsed = _parse(path);
     parsed.normalize();
     return parsed.toString();
@@ -613,6 +625,9 @@
   ///     builder.relative('/root/other.dart',
   ///         from: '/root/path'); // -> '../other.dart'
   ///
+  /// If [path] and/or [from] are relative paths, they are assumed to be
+  /// relative to [root].
+  ///
   /// Since there is no relative path from one drive letter to another on
   /// Windows, this will return an absolute path in that case.
   ///
@@ -624,8 +639,6 @@
   ///     var builder = new Builder(r'some/relative/path');
   ///     builder.relative(r'/absolute/path'); // -> '/absolute/path'
   String relative(String path, {String from}) {
-    if (path == '') return '.';
-
     from = from == null ? root : this.join(root, from);
 
     // We can't determine the path from a relative path to an absolute path.
@@ -672,8 +685,12 @@
       pathParsed.separators.removeAt(1);
     }
 
-    // If there are any directories left in the root path, we need to walk up
-    // out of them.
+    // If there are any directories left in the from path, we need to walk up
+    // out of them. If a directory left in the from path is '..', it cannot
+    // be cancelled by adding a '..'.
+    if (fromParsed.parts.length > 0 && fromParsed.parts[0] == '..') {
+      throw new ArgumentError('Unable to find a path to "$path" from "$from".');
+    }
     _growListFront(pathParsed.parts, fromParsed.parts.length, '..');
     pathParsed.separators[0] = '';
     pathParsed.separators.insertAll(1,
@@ -682,6 +699,13 @@
     // Corner case: the paths completely collapsed.
     if (pathParsed.parts.length == 0) return '.';
 
+    // Corner case: path was '.' and some '..' directories were added in front.
+    // Don't add a final '/.' in that case.
+    if (pathParsed.parts.length > 1 && pathParsed.parts.last == '.') {
+      pathParsed.parts.removeLast();
+      pathParsed.separators..removeLast()..removeLast()..add('');
+    }
+
     // Make it relative.
     pathParsed.root = '';
     pathParsed.removeTrailingSeparators();
@@ -1042,7 +1066,8 @@
     return copy._splitExtension()[0];
   }
 
-  bool get hasTrailingSeparator => !parts.isEmpty && (parts.last == '' || separators.last != '');
+  bool get hasTrailingSeparator =>
+      !parts.isEmpty && (parts.last == '' || separators.last != '');
 
   void removeTrailingSeparators() {
     while (!parts.isEmpty && parts.last == '') {
diff --git a/test/posix_test.dart b/test/posix_test.dart
index c26a5ba..3afb3bb 100644
--- a/test/posix_test.dart
+++ b/test/posix_test.dart
@@ -23,6 +23,8 @@
 
   test('extension', () {
     expect(builder.extension(''), '');
+    expect(builder.extension('.'), '');
+    expect(builder.extension('..'), '');
     expect(builder.extension('foo.dart'), '.dart');
     expect(builder.extension('foo.dart.js'), '.js');
     expect(builder.extension('a.b/c'), '');
@@ -41,12 +43,16 @@
 
   test('dirname', () {
     expect(builder.dirname(''), '.');
+    expect(builder.dirname('.'), '.');
+    expect(builder.dirname('..'), '.');
+    expect(builder.dirname('../..'), '..');
     expect(builder.dirname('a'), '.');
     expect(builder.dirname('a/b'), 'a');
     expect(builder.dirname('a/b/c'), 'a/b');
     expect(builder.dirname('a/b.c'), 'a');
     expect(builder.dirname('a/'), '.');
     expect(builder.dirname('a/.'), 'a');
+    expect(builder.dirname('a/..'), 'a');
     expect(builder.dirname(r'a\b/c'), r'a\b');
     expect(builder.dirname('/a'), '/');
     expect(builder.dirname('///a'), '/');
@@ -61,12 +67,16 @@
 
   test('basename', () {
     expect(builder.basename(''), '');
+    expect(builder.basename('.'), '.');
+    expect(builder.basename('..'), '..');
+    expect(builder.basename('.foo'), '.foo');
     expect(builder.basename('a'), 'a');
     expect(builder.basename('a/b'), 'b');
     expect(builder.basename('a/b/c'), 'c');
     expect(builder.basename('a/b.c'), 'b.c');
     expect(builder.basename('a/'), 'a');
     expect(builder.basename('a/.'), '.');
+    expect(builder.basename('a/..'), '..');
     expect(builder.basename(r'a\b/c'), 'c');
     expect(builder.basename('/a'), 'a');
     expect(builder.basename('/'), '/');
@@ -79,6 +89,8 @@
 
   test('basenameWithoutExtension', () {
     expect(builder.basenameWithoutExtension(''), '');
+    expect(builder.basenameWithoutExtension('.'), '.');
+    expect(builder.basenameWithoutExtension('..'), '..');
     expect(builder.basenameWithoutExtension('a'), 'a');
     expect(builder.basenameWithoutExtension('a/b'), 'b');
     expect(builder.basenameWithoutExtension('a/b/c'), 'c');
@@ -93,6 +105,7 @@
     expect(builder.basenameWithoutExtension('a//b'), 'b');
     expect(builder.basenameWithoutExtension('a/b.c/'), 'b');
     expect(builder.basenameWithoutExtension('a/b.c//'), 'b');
+    expect(builder.basenameWithoutExtension('a/b c.d e'), 'b c');
   });
 
   test('isAbsolute', () {
@@ -103,6 +116,8 @@
     expect(builder.isAbsolute('/a/b'), true);
     expect(builder.isAbsolute('~'), false);
     expect(builder.isAbsolute('.'), false);
+    expect(builder.isAbsolute('..'), false);
+    expect(builder.isAbsolute('.foo'), false);
     expect(builder.isAbsolute('../a'), false);
     expect(builder.isAbsolute('C:/a'), false);
     expect(builder.isAbsolute(r'C:\a'), false);
@@ -117,6 +132,8 @@
     expect(builder.isRelative('/a/b'), false);
     expect(builder.isRelative('~'), true);
     expect(builder.isRelative('.'), true);
+    expect(builder.isRelative('..'), true);
+    expect(builder.isRelative('.foo'), true);
     expect(builder.isRelative('../a'), true);
     expect(builder.isRelative('C:/a'), true);
     expect(builder.isRelative(r'C:\a'), true);
@@ -165,6 +182,14 @@
       expect(() => builder.join('a', null, 'b'), throwsArgumentError);
       expect(() => builder.join(null, 'a'), throwsArgumentError);
     });
+
+    test('join does not modify internal ., .., or trailing separators', () {
+      expect(builder.join('a/', 'b/c/'), 'a/b/c/');
+      expect(builder.join('a/b/./c/..//', 'd/.././..//e/f//'),
+             'a/b/./c/..//d/.././..//e/f//');
+      expect(builder.join('a/b', 'c/../../../..'), 'a/b/c/../../../..');
+      expect(builder.join('a', 'b${builder.separator}'), 'a/b/');
+    });
   });
 
   group('joinAll', () {
@@ -212,12 +237,17 @@
 
   group('normalize', () {
     test('simple cases', () {
-      expect(builder.normalize(''), '');
+      expect(builder.normalize(''), '.');
       expect(builder.normalize('.'), '.');
       expect(builder.normalize('..'), '..');
       expect(builder.normalize('a'), 'a');
       expect(builder.normalize('/'), '/');
       expect(builder.normalize(r'\'), r'\');
+      expect(builder.normalize('C:/'), 'C:');
+      expect(builder.normalize(r'C:\'), r'C:\');
+      expect(builder.normalize(r'\\'), r'\\');
+      expect(builder.normalize('a/./\xc5\u0bf8-;\u{1f085}\u{00}/c/d/../'),
+             'a/\xc5\u0bf8-;\u{1f085}\u{00}/c');
     });
 
     test('collapses redundant separators', () {
@@ -249,24 +279,38 @@
       expect(builder.normalize('/..'), '/');
       expect(builder.normalize('/../../..'), '/');
       expect(builder.normalize('/../../../a'), '/a');
+      expect(builder.normalize('c:/..'), '.');
+      expect(builder.normalize('A:/../../..'), '../..');
       expect(builder.normalize('a/..'), '.');
       expect(builder.normalize('a/b/..'), 'a');
       expect(builder.normalize('a/../b'), 'b');
       expect(builder.normalize('a/./../b'), 'b');
       expect(builder.normalize('a/b/c/../../d/e/..'), 'a/d');
       expect(builder.normalize('a/b/../../../../c'), '../../c');
+      expect(builder.normalize(r'z/a/b/../../..\../c'), r'z/..\../c');
+      expect(builder.normalize(r'a/b\c/../d'), 'a/d');
     });
 
     test('does not walk before root on absolute paths', () {
       expect(builder.normalize('..'), '..');
       expect(builder.normalize('../'), '..');
+      expect(builder.normalize('http://dartlang.org/..'), 'http:');
+      expect(builder.normalize('http://dartlang.org/../../a'), 'a');
+      expect(builder.normalize('file:///..'), '.');
+      expect(builder.normalize('file:///../../a'), '../a');
       expect(builder.normalize('/..'), '/');
       expect(builder.normalize('a/..'), '.');
+      expect(builder.normalize('../a'), '../a');
+      expect(builder.normalize('/../a'), '/a');
+      expect(builder.normalize('c:/../a'), 'a');
+      expect(builder.normalize('/../a'), '/a');
       expect(builder.normalize('a/b/..'), 'a');
+      expect(builder.normalize('../a/b/..'), '../a');
       expect(builder.normalize('a/../b'), 'b');
       expect(builder.normalize('a/./../b'), 'b');
       expect(builder.normalize('a/b/c/../../d/e/..'), 'a/d');
       expect(builder.normalize('a/b/../../../../c'), '../../c');
+      expect(builder.normalize('a/b/c/../../..d/./.e/f././'), 'a/..d/.e/f.');
     });
 
     test('removes trailing separators', () {
@@ -274,6 +318,7 @@
       expect(builder.normalize('.//'), '.');
       expect(builder.normalize('a/'), 'a');
       expect(builder.normalize('a/b/'), 'a/b');
+      expect(builder.normalize(r'a/b\'), r'a/b\');
       expect(builder.normalize('a/b///'), 'a/b');
     });
   });
diff --git a/test/relative_test.dart b/test/relative_test.dart
new file mode 100644
index 0000000..b9cd254
--- /dev/null
+++ b/test/relative_test.dart
@@ -0,0 +1,86 @@
+// Copyright (c) 2013, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+//
+// Test "relative" on all styles of path.Builder, on all platforms.
+
+import "package:unittest/unittest.dart";
+import "package:path/path.dart" as path;
+
+void main() {
+  test("test relative", () {
+    relativeTest(new path.Builder(style: path.Style.posix, root: '.'), '/');
+    relativeTest(new path.Builder(style: path.Style.posix, root: '/'), '/');
+    relativeTest(new path.Builder(style: path.Style.windows, root: r'd:\'),
+                 r'c:\');
+    relativeTest(new path.Builder(style: path.Style.windows, root: '.'),
+                 r'c:\');
+    relativeTest(new path.Builder(style: path.Style.url, root: 'file:///'),
+                 'http://myserver/');
+    relativeTest(new path.Builder(style: path.Style.url, root: '.'),
+                 'http://myserver/');
+    relativeTest(new path.Builder(style: path.Style.url, root: 'file:///'),
+                 '/');
+    relativeTest(new path.Builder(style: path.Style.url, root: '.'), '/');
+  });
+}
+
+void relativeTest(path.Builder builder, String prefix) {
+  var isRelative = (builder.root == '.');
+  // Cases where the arguments are absolute paths.
+  expectRelative(result, pathArg, fromArg) {
+    expect(builder.normalize(result), builder.relative(pathArg, from: fromArg));
+  }
+
+  expectRelative('c/d', '${prefix}a/b/c/d', '${prefix}a/b');
+  expectRelative('c/d', '${prefix}a/b/c/d', '${prefix}a/b/');
+  expectRelative('.', '${prefix}a', '${prefix}a');
+  // Trailing slashes in the inputs have no effect.
+  expectRelative('../../z/x/y', '${prefix}a/b/z/x/y', '${prefix}a/b/c/d/');
+  expectRelative('../../z/x/y', '${prefix}a/b/z/x/y', '${prefix}a/b/c/d');
+  expectRelative('../../z/x/y', '${prefix}a/b/z/x/y/', '${prefix}a/b/c/d');
+  expectRelative('../../../z/x/y', '${prefix}z/x/y', '${prefix}a/b/c');
+  expectRelative('../../../z/x/y', '${prefix}z/x/y', '${prefix}a/b/c/');
+
+  // Cases where the arguments are relative paths.
+  expectRelative('c/d', 'a/b/c/d', 'a/b');
+  expectRelative('.', 'a/b/c', 'a/b/c');
+  expectRelative('.', 'a/d/../b/c', 'a/b/c/');
+  expectRelative('.', '', '');
+  expectRelative('.', '.', '');
+  expectRelative('.', '', '.');
+  expectRelative('.', '.', '.');
+  expectRelative('.', '..', '..');
+  if (isRelative) expectRelative('..', '..', '.');
+  expectRelative('a', 'a', '');
+  expectRelative('a', 'a', '.');
+  expectRelative('..', '.', 'a');
+  expectRelative('.', 'a/b/f/../c', 'a/e/../b/c');
+  expectRelative('d', 'a/b/f/../c/d', 'a/e/../b/c');
+  expectRelative('..', 'a/b/f/../c', 'a/e/../b/c/e/');
+  expectRelative('../..', '', 'a/b/');
+  if (isRelative) expectRelative('../../..', '..', 'a/b/');
+  expectRelative('../b/c/d', 'b/c/d/', 'a/');
+  expectRelative('../a/b/c', 'x/y/a//b/./f/../c', 'x//y/z');
+
+  // Case where from is an exact substring of path.
+  expectRelative('a/b', '${prefix}x/y//a/b', '${prefix}x/y/');
+  expectRelative('a/b', 'x/y//a/b', 'x/y/');
+  expectRelative('../ya/b', '${prefix}x/ya/b', '${prefix}x/y');
+  expectRelative('../ya/b', 'x/ya/b', 'x/y');
+  expectRelative('../b', 'x/y/../b', 'x/y/.');
+  expectRelative('a/b/c', 'x/y/a//b/./f/../c', 'x/y');
+  expectRelative('.', '${prefix}x/y//', '${prefix}x/y/');
+  expectRelative('.', '${prefix}x/y/', '${prefix}x/y');
+
+  // Should always throw - no relative path can be constructed.
+  if (isRelative) {
+    expect(() => builder.relative('.', from: '..'), throwsArgumentError);
+    expect(() => builder.relative('a/b', from: '../../d'),
+           throwsArgumentError);
+    expect(() => builder.relative('a/b', from: '${prefix}a/b'),
+           throwsArgumentError);
+    // An absolute path relative from a relative path returns the absolute path.
+    expectRelative('${prefix}a/b', '${prefix}a/b', 'c/d');
+  }
+}
diff --git a/test/url_test.dart b/test/url_test.dart
index d040a59..1cd256d 100644
--- a/test/url_test.dart
+++ b/test/url_test.dart
@@ -89,10 +89,13 @@
     expect(builder.basename('a//'), 'a');
     expect(builder.basename('a/b//'), 'b');
     expect(builder.basename('a//b'), 'b');
+    expect(builder.basename('a b/c d.e f'), 'c d.e f');
   });
 
   test('basenameWithoutExtension', () {
     expect(builder.basenameWithoutExtension(''), '');
+    expect(builder.basenameWithoutExtension('.'), '.');
+    expect(builder.basenameWithoutExtension('..'), '..');
     expect(builder.basenameWithoutExtension('a'), 'a');
     expect(builder.basenameWithoutExtension('a/b'), 'b');
     expect(builder.basenameWithoutExtension('a/b/c'), 'c');
@@ -107,6 +110,7 @@
     expect(builder.basenameWithoutExtension('a//b'), 'b');
     expect(builder.basenameWithoutExtension('a/b.c/'), 'b');
     expect(builder.basenameWithoutExtension('a/b.c//'), 'b');
+    expect(builder.basenameWithoutExtension('a/b c.d e.f g'), 'b c.d e');
   });
 
   test('isAbsolute', () {
@@ -235,6 +239,14 @@
       expect(() => builder.join('a', null, 'b'), throwsArgumentError);
       expect(() => builder.join(null, 'a'), throwsArgumentError);
     });
+
+    test('Join does not modify internal ., .., or trailing separators', () {
+      expect(builder.join('a/', 'b/c/'), 'a/b/c/');
+      expect(builder.join('a/b/./c/..//', 'd/.././..//e/f//'),
+             'a/b/./c/..//d/.././..//e/f//');
+      expect(builder.join('a/b', 'c/../../../..'), 'a/b/c/../../../..');
+      expect(builder.join('a', 'b${builder.separator}'), 'a/b/');
+    });
   });
 
   group('joinAll', () {
@@ -305,7 +317,7 @@
 
   group('normalize', () {
     test('simple cases', () {
-      expect(builder.normalize(''), '');
+      expect(builder.normalize(''), '.');
       expect(builder.normalize('.'), '.');
       expect(builder.normalize('..'), '..');
       expect(builder.normalize('a'), 'a');
@@ -315,6 +327,11 @@
       expect(builder.normalize('file:///'), 'file://');
       expect(builder.normalize('/'), '/');
       expect(builder.normalize(r'\'), r'\');
+      expect(builder.normalize('C:/'), 'C:');
+      expect(builder.normalize(r'C:\'), r'C:\');
+      expect(builder.normalize(r'\\'), r'\\');
+      expect(builder.normalize('a/./\xc5\u0bf8-;\u{1f085}\u{00}/c/d/../'),
+             'a/\xc5\u0bf8-;\u{1f085}\u{00}/c');
     });
 
     test('collapses redundant separators', () {
@@ -360,12 +377,16 @@
           'http://dartlang.org/a');
       expect(builder.normalize('file:///../../../a'), 'file:///a');
       expect(builder.normalize('/../../../a'), '/a');
+      expect(builder.normalize('c:/..'), '.');
+      expect(builder.normalize('A:/../../..'), '../..');
       expect(builder.normalize('a/..'), '.');
       expect(builder.normalize('a/b/..'), 'a');
       expect(builder.normalize('a/../b'), 'b');
       expect(builder.normalize('a/./../b'), 'b');
       expect(builder.normalize('a/b/c/../../d/e/..'), 'a/d');
       expect(builder.normalize('a/b/../../../../c'), '../../c');
+      expect(builder.normalize('z/a/b/../../..\../c'), 'z/..\../c');
+      expect(builder.normalize('a/b\c/../d'), 'a/d');
     });
 
     test('does not walk before root on absolute paths', () {
@@ -373,14 +394,23 @@
       expect(builder.normalize('../'), '..');
       expect(builder.normalize('http://dartlang.org/..'),
           'http://dartlang.org');
+      expect(builder.normalize('http://dartlang.org/../a'),
+             'http://dartlang.org/a');
       expect(builder.normalize('file:///..'), 'file://');
+      expect(builder.normalize('file:///../a'), 'file:///a');
       expect(builder.normalize('/..'), '/');
       expect(builder.normalize('a/..'), '.');
+      expect(builder.normalize('../a'), '../a');
+      expect(builder.normalize('/../a'), '/a');
+      expect(builder.normalize('c:/../a'), 'a');
+      expect(builder.normalize('/../a'), '/a');
       expect(builder.normalize('a/b/..'), 'a');
+      expect(builder.normalize('../a/b/..'), '../a');
       expect(builder.normalize('a/../b'), 'b');
       expect(builder.normalize('a/./../b'), 'b');
       expect(builder.normalize('a/b/c/../../d/e/..'), 'a/d');
       expect(builder.normalize('a/b/../../../../c'), '../../c');
+      expect(builder.normalize('a/b/c/../../..d/./.e/f././'), 'a/..d/.e/f.');
     });
 
     test('removes trailing separators', () {
@@ -388,6 +418,7 @@
       expect(builder.normalize('.//'), '.');
       expect(builder.normalize('a/'), 'a');
       expect(builder.normalize('a/b/'), 'a/b');
+      expect(builder.normalize(r'a/b\'), r'a/b\');
       expect(builder.normalize('a/b///'), 'a/b');
     });
   });
diff --git a/test/windows_test.dart b/test/windows_test.dart
index 7409439..1966b7f 100644
--- a/test/windows_test.dart
+++ b/test/windows_test.dart
@@ -25,8 +25,12 @@
 
   test('extension', () {
     expect(builder.extension(''), '');
+    expect(builder.extension('.'), '');
+    expect(builder.extension('..'), '');
+    expect(builder.extension('a/..'), '');
     expect(builder.extension('foo.dart'), '.dart');
     expect(builder.extension('foo.dart.js'), '.js');
+    expect(builder.extension('foo bar\gule fisk.dart.js'), '.js');
     expect(builder.extension(r'a.b\c'), '');
     expect(builder.extension('a.b/c.d'), '.d');
     expect(builder.extension(r'~\.bashrc'), '');
@@ -64,10 +68,14 @@
     expect(builder.dirname(r'a\\'), r'.');
     expect(builder.dirname(r'a\b\\'), 'a');
     expect(builder.dirname(r'a\\b'), 'a');
+    expect(builder.dirname(r'foo bar\gule fisk'), 'foo bar');
   });
 
   test('basename', () {
     expect(builder.basename(r''), '');
+    expect(builder.basename(r'.'), '.');
+    expect(builder.basename(r'..'), '..');
+    expect(builder.basename(r'.hest'), '.hest');
     expect(builder.basename(r'a'), 'a');
     expect(builder.basename(r'a\b'), 'b');
     expect(builder.basename(r'a\b\c'), 'c');
@@ -83,10 +91,15 @@
     expect(builder.basename(r'a\\'), 'a');
     expect(builder.basename(r'a\b\\'), 'b');
     expect(builder.basename(r'a\\b'), 'b');
+    expect(builder.basename(r'a\\b'), 'b');
+    expect(builder.basename(r'a\fisk hest.ma pa'), 'fisk hest.ma pa');
   });
 
   test('basenameWithoutExtension', () {
     expect(builder.basenameWithoutExtension(''), '');
+    expect(builder.basenameWithoutExtension('.'), '.');
+    expect(builder.basenameWithoutExtension('..'), '..');
+    expect(builder.basenameWithoutExtension('.hest'), '.hest');
     expect(builder.basenameWithoutExtension('a'), 'a');
     expect(builder.basenameWithoutExtension(r'a\b'), 'b');
     expect(builder.basenameWithoutExtension(r'a\b\c'), 'c');
@@ -101,10 +114,13 @@
     expect(builder.basenameWithoutExtension(r'a\\b'), 'b');
     expect(builder.basenameWithoutExtension(r'a\b.c\'), 'b');
     expect(builder.basenameWithoutExtension(r'a\b.c\\'), 'b');
+    expect(builder.basenameWithoutExtension(r'C:\f h.ma pa.f s'), 'f h.ma pa');
   });
 
   test('isAbsolute', () {
     expect(builder.isAbsolute(''), false);
+    expect(builder.isAbsolute('.'), false);
+    expect(builder.isAbsolute('..'), false);
     expect(builder.isAbsolute('a'), false);
     expect(builder.isAbsolute(r'a\b'), false);
     expect(builder.isAbsolute(r'\a'), false);
@@ -124,6 +140,8 @@
 
   test('isRelative', () {
     expect(builder.isRelative(''), true);
+    expect(builder.isRelative('.'), true);
+    expect(builder.isRelative('..'), true);
     expect(builder.isRelative('a'), true);
     expect(builder.isRelative(r'a\b'), true);
     expect(builder.isRelative(r'\a'), true);
@@ -184,6 +202,14 @@
       expect(() => builder.join('a', null, 'b'), throwsArgumentError);
       expect(() => builder.join(null, 'a'), throwsArgumentError);
     });
+
+    test('join does not modify internal ., .., or trailing separators', () {
+      expect(builder.join('a/', 'b/c/'), 'a/b/c/');
+      expect(builder.join(r'a\b\./c\..\\', r'd\..\.\..\\e\f\\'),
+             r'a\b\./c\..\\d\..\.\..\\e\f\\');
+      expect(builder.join(r'a\b', r'c\..\..\..\..'), r'a\b\c\..\..\..\..');
+      expect(builder.join(r'a', 'b${builder.separator}'), r'a\b\');
+    });
   });
 
   group('joinAll', () {
@@ -238,13 +264,17 @@
 
   group('normalize', () {
     test('simple cases', () {
-      expect(builder.normalize(''), '');
+      expect(builder.normalize(''), '.');
       expect(builder.normalize('.'), '.');
       expect(builder.normalize('..'), '..');
       expect(builder.normalize('a'), 'a');
+      expect(builder.normalize(r'\'), '.');
+      expect(builder.normalize('/'), r'.');
       expect(builder.normalize('C:/'), r'C:\');
       expect(builder.normalize(r'C:\'), r'C:\');
       expect(builder.normalize(r'\\'), r'\\');
+      expect(builder.normalize('a\\.\\\xc5\u0bf8-;\u{1f085}\u{00}\\c\\d\\..\\'),
+             'a\\\xc5\u0bf8-;\u{1f085}\u{00}\x5cc');
     });
 
     test('collapses redundant separators', () {
@@ -278,12 +308,19 @@
       expect(builder.normalize(r'c:\..'), r'c:\');
       expect(builder.normalize(r'A:/..\..\..'), r'A:\');
       expect(builder.normalize(r'b:\..\..\..\a'), r'b:\a');
+      expect(builder.normalize(r'b:\r\..\..\..\a\c\.\..'), r'b:\a');
       expect(builder.normalize(r'a\..'), '.');
+      expect(builder.normalize(r'..\a'), r'..\a');
+      expect(builder.normalize(r'c:\..\a'), r'c:\a');
+      // A path starting with '\' is not an absolute path on Windows.
+      expect(builder.normalize(r'\..\a'), r'..\a');
       expect(builder.normalize(r'a\b\..'), 'a');
+      expect(builder.normalize(r'..\a\b\..'), r'..\a');
       expect(builder.normalize(r'a\..\b'), 'b');
       expect(builder.normalize(r'a\.\..\b'), 'b');
       expect(builder.normalize(r'a\b\c\..\..\d\e\..'), r'a\d');
       expect(builder.normalize(r'a\b\..\..\..\..\c'), r'..\..\c');
+      expect(builder.normalize(r'a/b/c/../../..d/./.e/f././'), r'a\..d\.e\f.');
     });
 
     test('removes trailing separators', () {