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');