Add isWithin to pkg/path.

R=rnystrom@google.com
BUG=14980

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

git-svn-id: https://dart.googlecode.com/svn/branches/bleeding_edge/dart/pkg/path@30317 260f80e4-7a28-3924-810f-c04153c831b5
diff --git a/lib/path.dart b/lib/path.dart
index 5d49097..5f469ed 100644
--- a/lib/path.dart
+++ b/lib/path.dart
@@ -301,6 +301,13 @@
 String relative(String path, {String from}) =>
     _builder.relative(path, from: from);
 
+/// Returns `true` if [child] is a path beneath `parent`, and `false` otherwise.
+///
+///     path.isWithin('/root/path', '/root/path/a'); // -> true
+///     path.isWithin('/root/path', '/root/other'); // -> false
+///     path.isWithin('/root/path', '/root/path') // -> false
+bool isWithin(String parent, String child) => _builder.isWithin(parent, child);
+
 /// Removes a trailing extension from the last part of [path].
 ///
 ///     withoutExtension('path/to/foo.dart'); // -> 'path/to/foo'
@@ -664,6 +671,11 @@
   ///
   ///     var builder = new Builder(r'some/relative/path');
   ///     builder.relative(r'/absolute/path'); // -> '/absolute/path'
+  ///
+  /// If [root] is relative, it may be impossible to determine a path from
+  /// [from] to [path]. For example, if [root] and [path] are "." and [from] is
+  /// "/", no path can be determined. In this case, a [PathException] will be
+  /// thrown.
   String relative(String path, {String from}) {
     from = from == null ? root : this.join(root, from);
 
@@ -681,7 +693,7 @@
     // If the path is still relative and `from` is absolute, we're unable to
     // find a path from `from` to `path`.
     if (this.isRelative(path) && this.isAbsolute(from)) {
-      throw new ArgumentError('Unable to find a path to "$path" from "$from".');
+      throw new PathException('Unable to find a path to "$path" from "$from".');
     }
 
     var fromParsed = _parse(from)..normalize();
@@ -715,7 +727,7 @@
     // 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".');
+      throw new PathException('Unable to find a path to "$path" from "$from".');
     }
     _growListFront(pathParsed.parts, fromParsed.parts.length, '..');
     pathParsed.separators[0] = '';
@@ -739,6 +751,27 @@
     return pathParsed.toString();
   }
 
+  /// Returns `true` if [child] is a path beneath `parent`, and `false`
+  /// otherwise.
+  ///
+  ///     path.isWithin('/root/path', '/root/path/a'); // -> true
+  ///     path.isWithin('/root/path', '/root/other'); // -> false
+  ///     path.isWithin('/root/path', '/root/path') // -> false
+  bool isWithin(String parent, String child) {
+    var relative;
+    try {
+      relative = this.relative(child, from: parent);
+    } on PathException catch (_) {
+      // If no relative path from [parent] to [child] is found, [child]
+      // definitely isn't a child of [parent].
+      return false;
+    }
+
+    var parts = this.split(relative);
+    return this.isRelative(relative) && parts.first != '..' &&
+        parts.first != '.';
+  }
+
   /// Removes a trailing extension from the last part of [path].
   ///
   ///     builder.withoutExtension('path/to/foo.dart'); // -> 'path/to/foo'
@@ -1204,3 +1237,13 @@
       style, root, isRootRelative,
       new List.from(parts), new List.from(separators));
 }
+
+/// An exception class that's thrown when a path operation is unable to be
+/// computed accurately.
+class PathException implements Exception {
+  String message;
+
+  PathException(this.message);
+
+  String toString() => "PathException: $message";
+}
diff --git a/test/posix_test.dart b/test/posix_test.dart
index 1156379..b693fcf 100644
--- a/test/posix_test.dart
+++ b/test/posix_test.dart
@@ -7,6 +7,8 @@
 import 'package:unittest/unittest.dart';
 import 'package:path/path.dart' as path;
 
+import 'utils.dart';
+
 main() {
   var builder = new path.Builder(style: path.Style.posix, root: '/root/path');
 
@@ -398,7 +400,7 @@
     test('with a root parameter and a relative root', () {
       var r = new path.Builder(style: path.Style.posix, root: 'relative/root');
       expect(r.relative('/foo/bar/baz', from: '/foo/bar'), equals('baz'));
-      expect(() => r.relative('..', from: '/foo/bar'), throwsArgumentError);
+      expect(() => r.relative('..', from: '/foo/bar'), throwsPathException);
       expect(r.relative('/foo/bar/baz', from: 'foo/bar'),
           equals('/foo/bar/baz'));
       expect(r.relative('..', from: 'foo/bar'), equals('../../..'));
@@ -411,6 +413,27 @@
     });
   });
 
+  group('isWithin', () {
+    test('simple cases', () {
+      expect(builder.isWithin('foo/bar', 'foo/bar'), isFalse);
+      expect(builder.isWithin('foo/bar', 'foo/bar/baz'), isTrue);
+      expect(builder.isWithin('foo/bar', 'foo/baz'), isFalse);
+      expect(builder.isWithin('foo/bar', '../path/foo/bar/baz'), isTrue);
+      expect(builder.isWithin('/', '/foo/bar'), isTrue);
+      expect(builder.isWithin('baz', '/root/path/baz/bang'), isTrue);
+      expect(builder.isWithin('baz', '/root/path/bang/baz'), isFalse);
+    });
+
+    test('from a relative root', () {
+      var r = new path.Builder(style: path.Style.posix, root: 'foo/bar');
+      expect(builder.isWithin('.', 'a/b/c'), isTrue);
+      expect(builder.isWithin('.', '../a/b/c'), isFalse);
+      expect(builder.isWithin('.', '../../a/foo/b/c'), isFalse);
+      expect(builder.isWithin('/', '/baz/bang'), isTrue);
+      expect(builder.isWithin('.', '/baz/bang'), isFalse);
+    });
+  });
+
   group('resolve', () {
     test('allows up to seven parts', () {
       expect(builder.resolve('a'), '/root/path/a');
diff --git a/test/relative_test.dart b/test/relative_test.dart
index b9cd254..e9762f4 100644
--- a/test/relative_test.dart
+++ b/test/relative_test.dart
@@ -7,6 +7,8 @@
 import "package:unittest/unittest.dart";
 import "package:path/path.dart" as path;
 
+import "utils.dart";
+
 void main() {
   test("test relative", () {
     relativeTest(new path.Builder(style: path.Style.posix, root: '.'), '/');
@@ -75,11 +77,11 @@
 
   // Should always throw - no relative path can be constructed.
   if (isRelative) {
-    expect(() => builder.relative('.', from: '..'), throwsArgumentError);
+    expect(() => builder.relative('.', from: '..'), throwsPathException);
     expect(() => builder.relative('a/b', from: '../../d'),
-           throwsArgumentError);
+           throwsPathException);
     expect(() => builder.relative('a/b', from: '${prefix}a/b'),
-           throwsArgumentError);
+           throwsPathException);
     // 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 d43e88c..9fb6f07 100644
--- a/test/url_test.dart
+++ b/test/url_test.dart
@@ -615,6 +615,39 @@
     });
   });
 
+  group('isWithin', () {
+    test('simple cases', () {
+      expect(builder.isWithin('foo/bar', 'foo/bar'), isFalse);
+      expect(builder.isWithin('foo/bar', 'foo/bar/baz'), isTrue);
+      expect(builder.isWithin('foo/bar', 'foo/baz'), isFalse);
+      expect(builder.isWithin('foo/bar', '../path/foo/bar/baz'), isTrue);
+      expect(builder.isWithin(
+              'http://dartlang.org', 'http://dartlang.org/foo/bar'),
+          isTrue);
+      expect(builder.isWithin(
+              'http://dartlang.org', 'http://pub.dartlang.org/foo/bar'),
+          isFalse);
+      expect(builder.isWithin('http://dartlang.org', '/foo/bar'), isTrue);
+      expect(builder.isWithin('http://dartlang.org/foo', '/foo/bar'), isTrue);
+      expect(builder.isWithin('http://dartlang.org/foo', '/bar/baz'), isFalse);
+      expect(builder.isWithin('baz', 'http://dartlang.org/root/path/baz/bang'),
+          isTrue);
+      expect(builder.isWithin('baz', 'http://dartlang.org/root/path/bang/baz'),
+          isFalse);
+    });
+
+    test('from a relative root', () {
+      var r = new path.Builder(style: path.Style.url, root: 'foo/bar');
+      expect(builder.isWithin('.', 'a/b/c'), isTrue);
+      expect(builder.isWithin('.', '../a/b/c'), isFalse);
+      expect(builder.isWithin('.', '../../a/foo/b/c'), isFalse);
+      expect(builder.isWithin(
+              'http://dartlang.org/', 'http://dartlang.org/baz/bang'),
+          isTrue);
+      expect(builder.isWithin('.', 'http://dartlang.org/baz/bang'), isFalse);
+    });
+  });
+
   group('resolve', () {
     test('allows up to seven parts', () {
       expect(builder.resolve('a'), 'http://dartlang.org/root/path/a');
diff --git a/test/utils.dart b/test/utils.dart
new file mode 100644
index 0000000..1730798
--- /dev/null
+++ b/test/utils.dart
@@ -0,0 +1,11 @@
+// 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.
+
+library path.test.utils;
+
+import "package:unittest/unittest.dart";
+import "package:path/path.dart" as path;
+
+/// A matcher for a closure that throws a [path.PathException].
+final throwsPathException = throwsA(new isInstanceOf<path.PathException>());
diff --git a/test/windows_test.dart b/test/windows_test.dart
index 8c75474..f1a2395 100644
--- a/test/windows_test.dart
+++ b/test/windows_test.dart
@@ -7,6 +7,8 @@
 import 'package:unittest/unittest.dart';
 import 'package:path/path.dart' as path;
 
+import 'utils.dart';
+
 main() {
   var builder = new path.Builder(style: path.Style.windows,
                                  root: r'C:\root\path');
@@ -504,7 +506,7 @@
     test('with a root parameter and a relative root', () {
       var r = new path.Builder(style: path.Style.windows, root: r'relative\root');
       expect(r.relative(r'C:\foo\bar\baz', from: r'C:\foo\bar'), equals('baz'));
-      expect(() => r.relative('..', from: r'C:\foo\bar'), throwsArgumentError);
+      expect(() => r.relative('..', from: r'C:\foo\bar'), throwsPathException);
       expect(r.relative(r'C:\foo\bar\baz', from: r'foo\bar'),
           equals(r'C:\foo\bar\baz'));
       expect(r.relative('..', from: r'foo\bar'), equals(r'..\..\..'));
@@ -523,6 +525,31 @@
     });
   });
 
+  group('isWithin', () {
+    test('simple cases', () {
+      expect(builder.isWithin(r'foo\bar', r'foo\bar'), isFalse);
+      expect(builder.isWithin(r'foo\bar', r'foo\bar\baz'), isTrue);
+      expect(builder.isWithin(r'foo\bar', r'foo\baz'), isFalse);
+      expect(builder.isWithin(r'foo\bar', r'..\path\foo\bar\baz'), isTrue);
+      expect(builder.isWithin(r'C:\', r'C:\foo\bar'), isTrue);
+      expect(builder.isWithin(r'C:\', r'D:\foo\bar'), isFalse);
+      expect(builder.isWithin(r'C:\', r'\foo\bar'), isTrue);
+      expect(builder.isWithin(r'C:\foo', r'\foo\bar'), isTrue);
+      expect(builder.isWithin(r'C:\foo', r'\bar\baz'), isFalse);
+      expect(builder.isWithin(r'baz', r'C:\root\path\baz\bang'), isTrue);
+      expect(builder.isWithin(r'baz', r'C:\root\path\bang\baz'), isFalse);
+    });
+
+    test('from a relative root', () {
+      var r = new path.Builder(style: path.Style.windows, root: r'foo\bar');
+      expect(builder.isWithin('.', r'a\b\c'), isTrue);
+      expect(builder.isWithin('.', r'..\a\b\c'), isFalse);
+      expect(builder.isWithin('.', r'..\..\a\foo\b\c'), isFalse);
+      expect(builder.isWithin(r'C:\', r'C:\baz\bang'), isTrue);
+      expect(builder.isWithin('.', r'C:\baz\bang'), isFalse);
+    });
+  });
+
   group('resolve', () {
     test('allows up to seven parts', () {
       expect(builder.resolve('a'), r'C:\root\path\a');