Add new equality and canonicalization functions. (#16)
Closes #14
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3c9fb6b..2818f7b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,4 +1,7 @@
-## 1.3.10
+## 1.4.0
+
+* Add `equals()`, `hash()` and `canonicalize()` top-level functions and
+ `Context` methods. These make it easier to treat paths as map keys.
* Properly compare Windows paths case-insensitively.
diff --git a/lib/path.dart b/lib/path.dart
index 25236ea..59cc520 100644
--- a/lib/path.dart
+++ b/lib/path.dart
@@ -281,9 +281,27 @@
/// // -> ['http://dartlang.org', 'path', 'to', 'foo']
List<String> split(String path) => context.split(path);
+/// Canonicalizes [path].
+///
+/// This is guaranteed to return the same path for two different input paths
+/// if and only if both input paths point to the same location. Unlike
+/// [normalize], it returns absolute paths when possible and canonicalizes
+/// ASCII case on Windows.
+///
+/// Note that this does not resolve symlinks.
+///
+/// If you want a map that uses path keys, it's probably more efficient to
+/// pass [equals] and [hash] to [new HashMap] than it is to canonicalize every
+/// key.
+String canonicalize(String path) => context.canonicalize(path);
+
/// Normalizes [path], simplifying it by handling `..`, and `.`, and
/// removing redundant path separators whenever possible.
///
+/// Note that this is *not* guaranteed to return the same result for two
+/// equivalent input paths. For that, see [canonicalize]. Or, if you're using
+/// paths as map keys, pass [equals] and [hash] to [new HashMap].
+///
/// path.normalize('path/./to/..//file.text'); // -> 'path/file.txt'
String normalize(String path) => context.normalize(path);
@@ -324,6 +342,20 @@
/// path.isWithin('/root/path', '/root/path') // -> false
bool isWithin(String parent, String child) => context.isWithin(parent, child);
+/// Returns `true` if [path1] points to the same location as [path2], and
+/// `false` otherwise.
+///
+/// The [hash] function returns a hash code that matches these equality
+/// semantics.
+bool equals(String path1, String path2) => context.equals(path1, path2);
+
+/// Returns a hash code for [path] such that, if [equals] returns `true` for two
+/// paths, their hash codes are the same.
+///
+/// Note that the same path may have different hash codes on different platforms
+/// or with different [current] directories.
+int hash(String path) => context.hash(path);
+
/// Removes a trailing extension from the last part of [path].
///
/// withoutExtension('path/to/foo.dart'); // -> 'path/to/foo'
diff --git a/lib/src/context.dart b/lib/src/context.dart
index 3e84cae..af97f90 100644
--- a/lib/src/context.dart
+++ b/lib/src/context.dart
@@ -2,6 +2,8 @@
// 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.
+import 'dart:math' as math;
+
import 'characters.dart' as chars;
import 'internal_style.dart';
import 'style.dart';
@@ -298,9 +300,34 @@
return parsed.parts;
}
+ /// Canonicalizes [path].
+ ///
+ /// This is guaranteed to return the same path for two different input paths
+ /// if and only if both input paths point to the same location. Unlike
+ /// [normalize], it returns absolute paths when possible and canonicalizes
+ /// ASCII case on Windows.
+ ///
+ /// Note that this does not resolve symlinks.
+ ///
+ /// If you want a map that uses path keys, it's probably more efficient to
+ /// pass [equals] and [hash] to [new HashMap] than it is to canonicalize every
+ /// key.
+ String canonicalize(String path) {
+ path = absolute(path);
+ if (style != Style.windows && !_needsNormalization(path)) return path;
+
+ var parsed = _parse(path);
+ parsed.normalize(canonicalize: true);
+ return parsed.toString();
+ }
+
/// Normalizes [path], simplifying it by handling `..`, and `.`, and
/// removing redundant path separators whenever possible.
///
+ /// Note that this is *not* guaranteed to return the same result for two
+ /// equivalent input paths. For that, see [canonicalize]. Or, if you're using
+ /// paths as map keys, pass [equals] and [hash] to [new HashMap].
+ ///
/// context.normalize('path/./to/..//file.text'); // -> 'path/file.txt'
String normalize(String path) {
if (!_needsNormalization(path)) return path;
@@ -496,7 +523,22 @@
/// 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) {
+ bool isWithin(String parent, String child) =>
+ _isWithinOrEquals(parent, child) == _PathRelation.within;
+
+ /// Returns `true` if [path1] points to the same location as [path2], and
+ /// `false` otherwise.
+ ///
+ /// The [hash] function returns a hash code that matches these equality
+ /// semantics.
+ bool equals(String path1, String path2) =>
+ _isWithinOrEquals(path1, path2) == _PathRelation.equal;
+
+ /// Compares two paths and returns an enum value indicating their relationship
+ /// to one another.
+ ///
+ /// This never returns [_PathRelation.inconclusive].
+ _PathRelation _isWithinOrEquals(String parent, String child) {
// Make both paths the same level of relative. We're only able to do the
// quick comparison if both paths are in the same format, and making a path
// absolute is faster than making it relative.
@@ -519,8 +561,8 @@
}
}
- var fastResult = _isWithinFast(parent, child);
- if (fastResult != null) return fastResult;
+ var result = _isWithinOrEqualsFast(parent, child);
+ if (result != _PathRelation.inconclusive) return result;
var relative;
try {
@@ -528,18 +570,22 @@
} on PathException catch (_) {
// If no relative path from [parent] to [child] is found, [child]
// definitely isn't a child of [parent].
- return false;
+ return _PathRelation.different;
}
- var parts = this.split(relative);
- return this.isRelative(relative) &&
- parts.first != '..' &&
- parts.first != '.';
+ if (!this.isRelative(relative)) return _PathRelation.different;
+ if (relative == '.') return _PathRelation.equal;
+ if (relative == '..') return _PathRelation.different;
+ return (relative.length >= 3 &&
+ relative.startsWith('..') &&
+ style.isSeparator(relative.codeUnitAt(2)))
+ ? _PathRelation.different
+ : _PathRelation.within;
}
- /// An optimized implementation of [isWithin] that doesn't handle a few
- /// complex cases.
- bool _isWithinFast(String parent, String child) {
+ /// An optimized implementation of [_isWithinOrEquals] that doesn't handle a
+ /// few complex cases.
+ _PathRelation _isWithinOrEqualsFast(String parent, String child) {
// Normally we just bail when we see "." path components, but we can handle
// a single dot easily enough.
if (parent == '.') parent = '';
@@ -553,7 +599,7 @@
//
// isWithin("C:/bar", "//foo/bar/baz") //=> false
// isWithin("http://example.com/", "http://google.com/bar") //=> false
- if (parentRootLength != childRootLength) return false;
+ if (parentRootLength != childRootLength) return _PathRelation.different;
// Make sure that the roots are textually the same as well.
//
@@ -562,7 +608,9 @@
for (var i = 0; i < parentRootLength; i++) {
var parentCodeUnit = parent.codeUnitAt(i);
var childCodeUnit = child.codeUnitAt(i);
- if (!style.codeUnitsEqual(parentCodeUnit, childCodeUnit)) return false;
+ if (!style.codeUnitsEqual(parentCodeUnit, childCodeUnit)) {
+ return _PathRelation.different;
+ }
}
// Start by considering the last code unit as a separator, since
@@ -570,6 +618,9 @@
// comparing relative paths.
var lastCodeUnit = chars.SLASH;
+ /// The index of the last separator in [parent].
+ int lastParentSeparator;
+
// Iterate through both paths as long as they're semantically identical.
var parentIndex = parentRootLength;
var childIndex = childRootLength;
@@ -577,6 +628,10 @@
var parentCodeUnit = parent.codeUnitAt(parentIndex);
var childCodeUnit = child.codeUnitAt(childIndex);
if (style.codeUnitsEqual(parentCodeUnit, childCodeUnit)) {
+ if (style.isSeparator(parentCodeUnit)) {
+ lastParentSeparator = parentIndex;
+ }
+
lastCodeUnit = parentCodeUnit;
parentIndex++;
childIndex++;
@@ -586,6 +641,7 @@
// Ignore multiple separators in a row.
if (style.isSeparator(parentCodeUnit) &&
style.isSeparator(lastCodeUnit)) {
+ lastParentSeparator = parentIndex;
parentIndex++;
continue;
} else if (style.isSeparator(childCodeUnit) &&
@@ -594,35 +650,34 @@
continue;
}
- if (parentCodeUnit == chars.PERIOD) {
- // If a dot comes after a separator, it may be a directory traversal
- // operator. To check that, we need to know if it's followed by either
- // "/" or "./". Otherwise, it's just a normal non-matching character.
- //
- // isWithin("foo/./bar", "foo/bar/baz") //=> true
- // isWithin("foo/bar/../baz", "foo/bar/.foo") //=> false
- if (style.isSeparator(lastCodeUnit)) {
+ // If a dot comes after a separator, it may be a directory traversal
+ // operator. To check that, we need to know if it's followed by either
+ // "/" or "./". Otherwise, it's just a normal non-matching character.
+ //
+ // isWithin("foo/./bar", "foo/bar/baz") //=> true
+ // isWithin("foo/bar/../baz", "foo/bar/.foo") //=> false
+ if (parentCodeUnit == chars.PERIOD && style.isSeparator(lastCodeUnit)) {
+ parentIndex++;
+
+ // We've hit "/." at the end of the parent path, which we can ignore,
+ // since the paths were equivalent up to this point.
+ if (parentIndex == parent.length) break;
+ parentCodeUnit = parent.codeUnitAt(parentIndex);
+
+ // We've hit "/./", which we can ignore.
+ if (style.isSeparator(parentCodeUnit)) {
+ lastParentSeparator = parentIndex;
parentIndex++;
+ continue;
+ }
- // We've hit "/." at the end of the parent path, which we can ignore,
- // since the paths were equivalent up to this point.
- if (parentIndex == parent.length) break;
- parentCodeUnit = parent.codeUnitAt(parentIndex);
-
- // We've hit "/./", which we can ignore.
- if (style.isSeparator(parentCodeUnit)) {
- parentIndex++;
- continue;
- }
-
- // We've hit "/..", which may be a directory traversal operator that
- // we can't handle on the fast track.
- if (parentCodeUnit == chars.PERIOD) {
- parentIndex++;
- if (parentIndex == parent.length ||
- style.isSeparator(parent.codeUnitAt(parentIndex))) {
- return null;
- }
+ // We've hit "/..", which may be a directory traversal operator that
+ // we can't handle on the fast track.
+ if (parentCodeUnit == chars.PERIOD) {
+ parentIndex++;
+ if (parentIndex == parent.length ||
+ style.isSeparator(parent.codeUnitAt(parentIndex))) {
+ return _PathRelation.inconclusive;
}
}
@@ -632,23 +687,21 @@
// This is the same logic as above, but for the child path instead of the
// parent.
- if (childCodeUnit == chars.PERIOD) {
- if (style.isSeparator(lastCodeUnit)) {
+ if (childCodeUnit == chars.PERIOD && style.isSeparator(lastCodeUnit)) {
+ childIndex++;
+ if (childIndex == child.length) break;
+ childCodeUnit = child.codeUnitAt(childIndex);
+
+ if (style.isSeparator(childCodeUnit)) {
childIndex++;
- if (childIndex == child.length) break;
- childCodeUnit = child.codeUnitAt(childIndex);
+ continue;
+ }
- if (style.isSeparator(childCodeUnit)) {
- childIndex++;
- continue;
- }
-
- if (childCodeUnit == chars.PERIOD) {
- childIndex++;
- if (childIndex == child.length ||
- style.isSeparator(child.codeUnitAt(childIndex))) {
- return null;
- }
+ if (childCodeUnit == chars.PERIOD) {
+ childIndex++;
+ if (childIndex == child.length ||
+ style.isSeparator(child.codeUnitAt(childIndex))) {
+ return _PathRelation.inconclusive;
}
}
}
@@ -658,11 +711,16 @@
// ".." components, we can be confident that [child] is not within
// [parent].
var childDirection = _pathDirection(child, childIndex);
- if (childDirection != _PathDirection.belowRoot) return null;
- var parentDirection = _pathDirection(parent, parentIndex);
- if (parentDirection != _PathDirection.belowRoot) return null;
+ if (childDirection != _PathDirection.belowRoot) {
+ return _PathRelation.inconclusive;
+ }
- return false;
+ var parentDirection = _pathDirection(parent, parentIndex);
+ if (parentDirection != _PathDirection.belowRoot) {
+ return _PathRelation.inconclusive;
+ }
+
+ return _PathRelation.different;
}
// If the child is shorter than the parent, it's probably not within the
@@ -672,8 +730,19 @@
// isWithin("foo/bar/baz", "foo/bar") //=> false
// isWithin("foo/bar/baz/../..", "foo/bar") //=> true
if (childIndex == child.length) {
- var direction = _pathDirection(parent, parentIndex);
- return direction == _PathDirection.aboveRoot ? null : false;
+ if (parentIndex == parent.length ||
+ style.isSeparator(parent.codeUnitAt(parentIndex))) {
+ lastParentSeparator = parentIndex;
+ } else {
+ lastParentSeparator ??= math.max(0, parentRootLength - 1);
+ }
+
+ var direction = _pathDirection(parent,
+ lastParentSeparator ?? parentRootLength - 1);
+ if (direction == _PathDirection.atRoot) return _PathRelation.equal;
+ return direction == _PathDirection.aboveRoot
+ ? _PathRelation.inconclusive
+ : _PathRelation.different;
}
// We've reached the end of the parent path, which means it's time to make a
@@ -682,11 +751,13 @@
var direction = _pathDirection(child, childIndex);
// If there are no more components in the child, then it's the same as
- // the parent, not within it.
+ // the parent.
//
// isWithin("foo/bar", "foo/bar") //=> false
// isWithin("foo/bar", "foo/bar//") //=> false
- if (direction == _PathDirection.atRoot) return false;
+ // equals("foo/bar", "foo/bar") //=> true
+ // equals("foo/bar", "foo/bar//") //=> true
+ if (direction == _PathDirection.atRoot) return _PathRelation.equal;
// If there are unresolved ".." components in the child, no decision we make
// will be valid. We'll abort and do the slow check instead.
@@ -694,7 +765,9 @@
// isWithin("foo/bar", "foo/bar/..") //=> false
// isWithin("foo/bar", "foo/bar/baz/bang/../../..") //=> false
// isWithin("foo/bar", "foo/bar/baz/bang/../../../bar/baz") //=> true
- if (direction == _PathDirection.aboveRoot) return null;
+ if (direction == _PathDirection.aboveRoot) {
+ return _PathRelation.inconclusive;
+ }
// The child is within the parent if and only if we're on a separator
// boundary.
@@ -702,12 +775,14 @@
// isWithin("foo/bar", "foo/bar/baz") //=> true
// isWithin("foo/bar/", "foo/bar/baz") //=> true
// isWithin("foo/bar", "foo/barbaz") //=> false
- return style.isSeparator(child.codeUnitAt(childIndex)) ||
- style.isSeparator(lastCodeUnit);
+ return (style.isSeparator(child.codeUnitAt(childIndex)) ||
+ style.isSeparator(lastCodeUnit))
+ ? _PathRelation.within
+ : _PathRelation.different;
}
// Returns a [_PathDirection] describing the path represented by [codeUnits]
- // after [index].
+ // starting at [index].
//
// This ignores leading separators.
//
@@ -771,6 +846,80 @@
return _PathDirection.belowRoot;
}
+ /// Returns a hash code for [path] that matches the semantics of [equals].
+ ///
+ /// Note that the same path may have different hash codes in different
+ /// [Context]s.
+ int hash(String path) {
+ // Make [path] absolute to ensure that equivalent relative and absolute
+ // paths have the same hash code.
+ path = absolute(path);
+
+ var result = _hashFast(path);
+ if (result != null) return result;
+
+ var parsed = _parse(path);
+ parsed.normalize();
+ return _hashFast(parsed.toString());
+ }
+
+ /// An optimized implementation of [hash] that doesn't handle internal `..`
+ /// components.
+ ///
+ /// This will handle `..` components that appear at the beginning of the path.
+ int _hashFast(String path) {
+ var hash = 4603;
+ var beginning = true;
+ var wasSeparator = true;
+ for (var i = 0; i < path.length; i++) {
+ var codeUnit = style.canonicalizeCodeUnit(path.codeUnitAt(i));
+
+ // Take advantage of the fact that collisions are allowed to ignore
+ // separators entirely. This lets us avoid worrying about cases like
+ // multiple trailing slashes.
+ if (style.isSeparator(codeUnit)) {
+ wasSeparator = true;
+ continue;
+ }
+
+ if (codeUnit == chars.PERIOD && wasSeparator) {
+ // If a dot comes after a separator, it may be a directory traversal
+ // operator. To check that, we need to know if it's followed by either
+ // "/" or "./". Otherwise, it's just a normal character.
+ //
+ // hash("foo/./bar") == hash("foo/bar")
+
+ // We've hit "/." at the end of the path, which we can ignore.
+ if (i + 1 == path.length) break;
+
+ var next = path.codeUnitAt(i + 1);
+
+ // We can just ignore "/./", since they don't affect the semantics of
+ // the path.
+ if (style.isSeparator(next)) continue;
+
+ // If the path ends with "/.." or contains "/../", we need to
+ // canonicalize it before we can hash it. We make an exception for ".."s
+ // at the beginning of the path, since those may appear even in a
+ // canonicalized path.
+ if (!beginning &&
+ next == chars.PERIOD &&
+ (i + 2 == path.length ||
+ style.isSeparator(path.codeUnitAt(i + 2)))) {
+ return null;
+ }
+ }
+
+ // Make sure [hash] stays under 32 bits even after multiplication.
+ hash &= 0x3FFFFFF;
+ hash *= 33;
+ hash ^= codeUnit;
+ wasSeparator = false;
+ beginning = false;
+ }
+ return hash;
+ }
+
/// Removes a trailing extension from the last part of [path].
///
/// context.withoutExtension('path/to/foo.dart'); // -> 'path/to/foo'
@@ -930,3 +1079,32 @@
String toString() => name;
}
+
+/// An enum of possible return values for [Context._isWithinOrEquals].
+class _PathRelation {
+ /// The first path is a proper parent of the second.
+ ///
+ /// For example, `foo` is a proper parent of `foo/bar`, but not of `foo`.
+ static const within = const _PathRelation("within");
+
+ /// The two paths are equivalent.
+ ///
+ /// For example, `foo//bar` is equivalent to `foo/bar`.
+ static const equal = const _PathRelation("equal");
+
+ /// The first path is neither a parent of nor equal to the second.
+ static const different = const _PathRelation("different");
+
+ /// We couldn't quickly determine any information about the paths'
+ /// relationship to each other.
+ ///
+ /// Only returned by [Context._isWithinOrEqualsFast].
+ static const inconclusive = const _PathRelation("inconclusive");
+
+ final String name;
+
+ const _PathRelation(this.name);
+
+ String toString() => name;
+}
+
diff --git a/lib/src/internal_style.dart b/lib/src/internal_style.dart
index 1450d53..1874afc 100644
--- a/lib/src/internal_style.dart
+++ b/lib/src/internal_style.dart
@@ -76,4 +76,8 @@
/// This only needs to handle character-by-character comparison; it can assume
/// the paths are normalized and contain no `..` components.
bool pathsEqual(String path1, String path2) => path1 == path2;
+
+ int canonicalizeCodeUnit(int codeUnit) => codeUnit;
+
+ String canonicalizePart(String part) => part;
}
diff --git a/lib/src/parsed_path.dart b/lib/src/parsed_path.dart
index a3d68c7..619ffbf 100644
--- a/lib/src/parsed_path.dart
+++ b/lib/src/parsed_path.dart
@@ -97,7 +97,7 @@
if (separators.length > 0) separators[separators.length - 1] = '';
}
- void normalize() {
+ void normalize({bool canonicalize: false}) {
// Handle '.', '..', and empty parts.
var leadingDoubles = 0;
var newParts = <String>[];
@@ -113,7 +113,7 @@
leadingDoubles++;
}
} else {
- newParts.add(part);
+ newParts.add(canonicalize ? style.canonicalizePart(part) : part);
}
}
@@ -139,6 +139,7 @@
// Normalize the Windows root if needed.
if (root != null && style == Style.windows) {
+ if (canonicalize) root = root.toLowerCase();
root = root.replaceAll('/', '\\');
}
removeTrailingSeparators();
diff --git a/lib/src/style/windows.dart b/lib/src/style/windows.dart
index 0d89764..57989af 100644
--- a/lib/src/style/windows.dart
+++ b/lib/src/style/windows.dart
@@ -151,4 +151,13 @@
}
return true;
}
+
+ int canonicalizeCodeUnit(int codeUnit) {
+ if (codeUnit == chars.SLASH) return chars.BACKSLASH;
+ if (codeUnit < chars.UPPER_A) return codeUnit;
+ if (codeUnit > chars.UPPER_Z) return codeUnit;
+ return codeUnit | _asciiCaseBit;
+ }
+
+ String canonicalizePart(String part) => part.toLowerCase();
}
diff --git a/pubspec.yaml b/pubspec.yaml
index 31bf492..a0475a1 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,5 +1,5 @@
name: path
-version: 1.3.10-dev
+version: 1.4.0-dev
author: Dart Team <misc@dartlang.org>
description: >
A string-based path manipulation library. All of the path operations you know
diff --git a/test/posix_test.dart b/test/posix_test.dart
index 7b55dd3..4549ebd 100644
--- a/test/posix_test.dart
+++ b/test/posix_test.dart
@@ -317,6 +317,12 @@
expect(context.normalize(r'a/b\'), r'a/b\');
expect(context.normalize('a/b///'), 'a/b');
});
+
+ test('when canonicalizing', () {
+ expect(context.canonicalize('.'), '/root/path');
+ expect(context.canonicalize('foo/bar'), '/root/path/foo/bar');
+ expect(context.canonicalize('FoO'), '/root/path/FoO');
+ });
});
group('relative', () {
@@ -445,6 +451,44 @@
});
});
+ group('equals and hash', () {
+ test('simple cases', () {
+ expectEquals(context, 'foo/bar', 'foo/bar');
+ expectNotEquals(context, 'foo/bar', 'foo/bar/baz');
+ expectNotEquals(context, 'foo/bar', 'foo');
+ expectNotEquals(context, 'foo/bar', 'foo/baz');
+ expectEquals(context, 'foo/bar', '../path/foo/bar');
+ expectEquals(context, '/', '/');
+ expectEquals(context, '/', '../..');
+ expectEquals(context, 'baz', '/root/path/baz');
+ });
+
+ test('complex cases', () {
+ expectEquals(context, 'foo/./bar', 'foo/bar');
+ expectEquals(context, 'foo//bar', 'foo/bar');
+ expectEquals(context, 'foo/qux/../bar', 'foo/bar');
+ expectNotEquals(context, 'foo/qux/../bar', 'foo/qux');
+ expectNotEquals(context, 'foo/bar', 'foo/bar/baz/../..');
+ expectEquals(context, 'foo/bar', 'foo/bar///');
+ expectEquals(context, 'foo/.bar', 'foo/.bar');
+ expectNotEquals(context, 'foo/./bar', 'foo/.bar');
+ expectEquals(context, 'foo/..bar', 'foo/..bar');
+ expectNotEquals(context, 'foo/../bar', 'foo/..bar');
+ expectEquals(context, 'foo/bar', 'foo/bar/baz/..');
+ expectNotEquals(context, 'FoO/bAr', 'foo/bar');
+ });
+
+ test('from a relative root', () {
+ var r = new path.Context(style: path.Style.posix, current: 'foo/bar');
+ expectEquals(r, 'a/b', 'a/b');
+ expectNotEquals(r, '.', 'foo/bar');
+ expectNotEquals(r, '.', '../a/b');
+ expectEquals(r, '.', '../bar');
+ expectEquals(r, '/baz/bang', '/baz/bang');
+ expectNotEquals(r, 'baz/bang', '/baz/bang');
+ });
+ });
+
group('absolute', () {
test('allows up to seven parts', () {
expect(context.absolute('a'), '/root/path/a');
diff --git a/test/url_test.dart b/test/url_test.dart
index 4adfb48..32c02da 100644
--- a/test/url_test.dart
+++ b/test/url_test.dart
@@ -5,6 +5,8 @@
import 'package:test/test.dart';
import 'package:path/path.dart' as path;
+import 'utils.dart';
+
main() {
var context = new path.Context(
style: path.Style.url, current: 'http://dartlang.org/root/path');
@@ -427,6 +429,16 @@
expect(context.normalize(r'a/b\'), r'a/b\');
expect(context.normalize('a/b///'), 'a/b');
});
+
+ test('when canonicalizing', () {
+ expect(context.canonicalize('.'), 'http://dartlang.org/root/path');
+ expect(context.canonicalize('foo/bar'),
+ 'http://dartlang.org/root/path/foo/bar');
+ expect(context.canonicalize('FoO'), 'http://dartlang.org/root/path/FoO');
+ expect(context.canonicalize('/foo'), 'http://dartlang.org/foo');
+ expect(context.canonicalize('http://google.com/foo'),
+ 'http://google.com/foo');
+ });
});
group('relative', () {
@@ -674,6 +686,52 @@
});
});
+ group('equals and hash', () {
+ test('simple cases', () {
+ expectEquals(context, 'foo/bar', 'foo/bar');
+ expectNotEquals(context, 'foo/bar', 'foo/bar/baz');
+ expectNotEquals(context, 'foo/bar', 'foo');
+ expectNotEquals(context, 'foo/bar', 'foo/baz');
+ expectEquals(context, 'foo/bar', '../path/foo/bar');
+ expectEquals(context, 'http://google.com', 'http://google.com');
+ expectEquals(context, 'http://dartlang.org', '../..');
+ expectEquals(context, 'baz', '/root/path/baz');
+ });
+
+ test('complex cases', () {
+ expectEquals(context, 'foo/./bar', 'foo/bar');
+ expectEquals(context, 'foo//bar', 'foo/bar');
+ expectEquals(context, 'foo/qux/../bar', 'foo/bar');
+ expectNotEquals(context, 'foo/qux/../bar', 'foo/qux');
+ expectNotEquals(context, 'foo/bar', 'foo/bar/baz/../..');
+ expectEquals(context, 'foo/bar', 'foo/bar///');
+ expectEquals(context, 'foo/.bar', 'foo/.bar');
+ expectNotEquals(context, 'foo/./bar', 'foo/.bar');
+ expectEquals(context, 'foo/..bar', 'foo/..bar');
+ expectNotEquals(context, 'foo/../bar', 'foo/..bar');
+ expectEquals(context, 'foo/bar', 'foo/bar/baz/..');
+ expectNotEquals(context, 'FoO/bAr', 'foo/bar');
+ expectEquals(context, 'http://google.com', 'http://google.com/');
+ expectEquals(context, 'http://dartlang.org/root', '..');
+ });
+
+ test('with root-relative paths', () {
+ expectEquals(context, '/foo', 'http://dartlang.org/foo');
+ expectNotEquals(context, '/foo', 'http://google.com/foo');
+ expectEquals(context, '/root/path/foo/bar', 'foo/bar');
+ });
+
+ test('from a relative root', () {
+ var r = new path.Context(style: path.Style.posix, current: 'foo/bar');
+ expectEquals(r, 'a/b', 'a/b');
+ expectNotEquals(r, '.', 'foo/bar');
+ expectNotEquals(r, '.', '../a/b');
+ expectEquals(r, '.', '../bar');
+ expectEquals(r, '/baz/bang', '/baz/bang');
+ expectNotEquals(r, 'baz/bang', '/baz/bang');
+ });
+ });
+
group('absolute', () {
test('allows up to seven parts', () {
expect(context.absolute('a'), 'http://dartlang.org/root/path/a');
diff --git a/test/utils.dart b/test/utils.dart
index ae3ea46..5e22ce1 100644
--- a/test/utils.dart
+++ b/test/utils.dart
@@ -3,7 +3,30 @@
// BSD-style license that can be found in the LICENSE file.
import "package:test/test.dart";
-import "package:path/path.dart" as path;
+import "package:path/path.dart" as p;
/// A matcher for a closure that throws a [path.PathException].
-final throwsPathException = throwsA(new isInstanceOf<path.PathException>());
+final throwsPathException = throwsA(new isInstanceOf<p.PathException>());
+
+void expectEquals(p.Context context, String path1, String path2) {
+ expect(context.equals(path1, path2), isTrue,
+ reason: 'Expected "$path1" to equal "$path2".');
+ expect(context.equals(path2, path1), isTrue,
+ reason: 'Expected "$path2" to equal "$path1".');
+ expect(context.hash(path1), equals(context.hash(path2)),
+ reason: 'Expected "$path1" to hash the same as "$path2".');
+}
+
+void expectNotEquals(p.Context context, String path1, String path2,
+ {bool allowSameHash: false}) {
+ expect(context.equals(path1, path2), isFalse,
+ reason: 'Expected "$path1" not to equal "$path2".');
+ expect(context.equals(path2, path1), isFalse,
+ reason: 'Expected "$path2" not to equal "$path1".');
+
+ // Hash collisions are allowed, but the test author should be explicitly aware
+ // when they occur.
+ if (allowSameHash) return;
+ expect(context.hash(path1), isNot(equals(context.hash(path2))),
+ reason: 'Expected "$path1" not to hash the same as "$path2".');
+}
diff --git a/test/windows_test.dart b/test/windows_test.dart
index 8e72bb1..f4453a7 100644
--- a/test/windows_test.dart
+++ b/test/windows_test.dart
@@ -378,6 +378,14 @@
test('normalizes separators', () {
expect(context.normalize(r'a/b\c'), r'a\b\c');
});
+
+ test('when canonicalizing', () {
+ expect(context.canonicalize('.'), r'c:\root\path');
+ expect(context.canonicalize('foo/bar'), r'c:\root\path\foo\bar');
+ expect(context.canonicalize('FoO'), r'c:\root\path\foo');
+ expect(context.canonicalize('/foo'), r'c:\foo');
+ expect(context.canonicalize('D:/foo'), r'd:\foo');
+ });
});
group('relative', () {
@@ -581,6 +589,53 @@
});
});
+ group('equals and hash', () {
+ test('simple cases', () {
+ expectEquals(context, r'foo\bar', r'foo\bar');
+ expectNotEquals(context, r'foo\bar', r'foo\bar\baz');
+ expectNotEquals(context, r'foo\bar', r'foo');
+ expectNotEquals(context, r'foo\bar', r'foo\baz');
+ expectEquals(context, r'foo\bar', r'..\path\foo\bar');
+ expectEquals(context, r'D:\', r'D:\');
+ expectEquals(context, r'C:\', r'..\..');
+ expectEquals(context, r'baz', r'C:\root\path\baz');
+ });
+
+ test('complex cases', () {
+ expectEquals(context, r'foo\.\bar', r'foo\bar');
+ expectEquals(context, r'foo\\bar', r'foo\bar');
+ expectEquals(context, r'foo\qux\..\bar', r'foo\bar');
+ expectNotEquals(context, r'foo\qux\..\bar', r'foo\qux');
+ expectNotEquals(context, r'foo\bar', r'foo\bar\baz\..\..');
+ expectEquals(context, r'foo\bar', r'foo\bar\\\');
+ expectEquals(context, r'foo\.bar', r'foo\.bar');
+ expectNotEquals(context, r'foo\.\bar', r'foo\.bar');
+ expectEquals(context, r'foo\..bar', r'foo\..bar');
+ expectNotEquals(context, r'foo\..\bar', r'foo\..bar');
+ expectEquals(context, r'foo\bar', r'foo\bar\baz\..');
+ expectEquals(context, r'FoO\bAr', r'foo\bar');
+ expectEquals(context, r'foo/\bar', r'foo\/bar');
+ expectEquals(context, r'c:\', r'C:\');
+ expectEquals(context, r'C:\root', r'..');
+ });
+
+ test('with root-relative paths', () {
+ expectEquals(context, r'\foo', r'C:\foo');
+ expectNotEquals(context, r'\foo', 'http://google.com/foo');
+ expectEquals(context, r'C:\root\path\foo\bar', r'foo\bar');
+ });
+
+ test('from a relative root', () {
+ var r = new path.Context(style: path.Style.windows, current: r'foo\bar');
+ expectEquals(r, r'a\b', r'a\b');
+ expectNotEquals(r, '.', r'foo\bar');
+ expectNotEquals(r, '.', r'..\a\b');
+ expectEquals(r, '.', r'..\bar');
+ expectEquals(r, r'C:\baz\bang', r'C:\baz\bang');
+ expectNotEquals(r, r'baz\bang', r'C:\baz\bang');
+ });
+ });
+
group('absolute', () {
test('allows up to seven parts', () {
expect(context.absolute('a'), r'C:\root\path\a');