Index: packages/path/lib/src/context.dart |
diff --git a/packages/path/lib/src/context.dart b/packages/path/lib/src/context.dart |
index d10a29f148da84030400fb77cccfcdf4f7dcc431..a00ca29a889c4cdf3aca8a3971882a9d3b20e640 100644 |
--- a/packages/path/lib/src/context.dart |
+++ b/packages/path/lib/src/context.dart |
@@ -2,7 +2,7 @@ |
// 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.context; |
+import 'dart:math' as math; |
import 'characters.dart' as chars; |
import 'internal_style.dart'; |
@@ -245,7 +245,9 @@ class Context { |
// If the new part is root-relative, it preserves the previous root but |
// replaces the path after it. |
var parsed = _parse(part); |
- parsed.root = this.rootPrefix(buffer.toString()); |
+ var path = buffer.toString(); |
+ parsed.root = path.substring( |
+ 0, style.rootLength(path, withDrive: true)); |
if (style.needsSeparator(parsed.root)) { |
parsed.separators[0] = style.separator; |
} |
@@ -300,9 +302,34 @@ class Context { |
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; |
@@ -370,7 +397,7 @@ class Context { |
// Single dots and double dots are normalized to directory traversals. |
if (previous == chars.PERIOD && |
(previousPrevious == null || |
- previousPrevious == chars.SLASH || |
+ style.isSeparator(previousPrevious) || |
previousPrevious == chars.PERIOD)) { |
return true; |
} |
@@ -446,15 +473,14 @@ class Context { |
// calculation of relative paths, even if a path has not been normalized. |
if (fromParsed.root != pathParsed.root && |
((fromParsed.root == null || pathParsed.root == null) || |
- fromParsed.root.toLowerCase().replaceAll('/', '\\') != |
- pathParsed.root.toLowerCase().replaceAll('/', '\\'))) { |
+ !style.pathsEqual(fromParsed.root, pathParsed.root))) { |
return pathParsed.toString(); |
} |
// Strip off their common prefix. |
while (fromParsed.parts.length > 0 && |
pathParsed.parts.length > 0 && |
- fromParsed.parts[0] == pathParsed.parts[0]) { |
+ style.pathsEqual(fromParsed.parts[0], pathParsed.parts[0])) { |
fromParsed.parts.removeAt(0); |
fromParsed.separators.removeAt(1); |
pathParsed.parts.removeAt(0); |
@@ -499,7 +525,22 @@ class Context { |
/// 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. |
@@ -522,8 +563,8 @@ class Context { |
} |
} |
- var fastResult = _isWithinFast(parent, child); |
- if (fastResult != null) return fastResult; |
+ var result = _isWithinOrEqualsFast(parent, child); |
+ if (result != _PathRelation.inconclusive) return result; |
var relative; |
try { |
@@ -531,18 +572,22 @@ class Context { |
} 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 = ''; |
@@ -556,26 +601,17 @@ class Context { |
// |
// isWithin("C:/bar", "//foo/bar/baz") //=> false |
// isWithin("http://example.com/", "http://google.com/bar") //=> false |
- if (parentRootLength != childRootLength) return false; |
- |
- var parentCodeUnits = parent.codeUnits; |
- var childCodeUnits = child.codeUnits; |
+ if (parentRootLength != childRootLength) return _PathRelation.different; |
// Make sure that the roots are textually the same as well. |
// |
// isWithin("C:/bar", "D:/bar/baz") //=> false |
// isWithin("http://example.com/", "http://example.org/bar") //=> false |
for (var i = 0; i < parentRootLength; i++) { |
- var parentCodeUnit = parentCodeUnits[i]; |
- var childCodeUnit = childCodeUnits[i]; |
- if (parentCodeUnit == childCodeUnit) continue; |
- |
- // If both code units are separators, that's fine too. |
- // |
- // isWithin("C:/", r"C:\foo") //=> true |
- if (!style.isSeparator(parentCodeUnit) || |
- !style.isSeparator(childCodeUnit)) { |
- return false; |
+ var parentCodeUnit = parent.codeUnitAt(i); |
+ var childCodeUnit = child.codeUnitAt(i); |
+ if (!style.codeUnitsEqual(parentCodeUnit, childCodeUnit)) { |
+ return _PathRelation.different; |
} |
} |
@@ -584,23 +620,20 @@ class Context { |
// 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; |
while (parentIndex < parent.length && childIndex < child.length) { |
- var parentCodeUnit = parentCodeUnits[parentIndex]; |
- var childCodeUnit = childCodeUnits[childIndex]; |
- if (parentCodeUnit == childCodeUnit) { |
- lastCodeUnit = parentCodeUnit; |
- parentIndex++; |
- childIndex++; |
- continue; |
- } |
+ var parentCodeUnit = parent.codeUnitAt(parentIndex); |
+ var childCodeUnit = child.codeUnitAt(childIndex); |
+ if (style.codeUnitsEqual(parentCodeUnit, childCodeUnit)) { |
+ if (style.isSeparator(parentCodeUnit)) { |
+ lastParentSeparator = parentIndex; |
+ } |
- // Different separators are considered identical. |
- var parentIsSeparator = style.isSeparator(parentCodeUnit); |
- var childIsSeparator = style.isSeparator(childCodeUnit); |
- if (parentIsSeparator && childIsSeparator) { |
lastCodeUnit = parentCodeUnit; |
parentIndex++; |
childIndex++; |
@@ -608,43 +641,45 @@ class Context { |
} |
// Ignore multiple separators in a row. |
- if (parentIsSeparator && style.isSeparator(lastCodeUnit)) { |
+ if (style.isSeparator(parentCodeUnit) && |
+ style.isSeparator(lastCodeUnit)) { |
+ lastParentSeparator = parentIndex; |
parentIndex++; |
continue; |
- } else if (childIsSeparator && style.isSeparator(lastCodeUnit)) { |
+ } else if (style.isSeparator(childCodeUnit) && |
+ style.isSeparator(lastCodeUnit)) { |
childIndex++; |
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)) { |
- parentIndex++; |
+ // 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 = parentCodeUnits[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)) { |
- parentIndex++; |
- continue; |
- } |
+ // We've hit "/./", which we can ignore. |
+ if (style.isSeparator(parentCodeUnit)) { |
+ lastParentSeparator = parentIndex; |
+ 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(parentCodeUnits[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; |
} |
} |
@@ -654,23 +689,21 @@ class Context { |
// This is the same logic as above, but for the child path instead of the |
// parent. |
- if (childCodeUnit == chars.PERIOD) { |
- if (style.isSeparator(lastCodeUnit)) { |
- childIndex++; |
- if (childIndex == child.length) break; |
- childCodeUnit = childCodeUnits[childIndex]; |
+ if (childCodeUnit == chars.PERIOD && style.isSeparator(lastCodeUnit)) { |
+ childIndex++; |
+ if (childIndex == child.length) break; |
+ childCodeUnit = child.codeUnitAt(childIndex); |
- if (style.isSeparator(childCodeUnit)) { |
- childIndex++; |
- continue; |
- } |
+ if (style.isSeparator(childCodeUnit)) { |
+ childIndex++; |
+ continue; |
+ } |
- if (childCodeUnit == chars.PERIOD) { |
- childIndex++; |
- if (childIndex == child.length || |
- style.isSeparator(childCodeUnits[childIndex])) { |
- return null; |
- } |
+ if (childCodeUnit == chars.PERIOD) { |
+ childIndex++; |
+ if (childIndex == child.length || |
+ style.isSeparator(child.codeUnitAt(childIndex))) { |
+ return _PathRelation.inconclusive; |
} |
} |
} |
@@ -679,12 +712,17 @@ class Context { |
// As long as the remainders of the two paths don't have any unresolved |
// ".." components, we can be confident that [child] is not within |
// [parent]. |
- var childDirection = _pathDirection(childCodeUnits, childIndex); |
- if (childDirection != _PathDirection.belowRoot) return null; |
- var parentDirection = _pathDirection(parentCodeUnits, parentIndex); |
- if (parentDirection != _PathDirection.belowRoot) return null; |
+ var childDirection = _pathDirection(child, childIndex); |
+ if (childDirection != _PathDirection.belowRoot) { |
+ return _PathRelation.inconclusive; |
+ } |
+ |
+ var parentDirection = _pathDirection(parent, parentIndex); |
+ if (parentDirection != _PathDirection.belowRoot) { |
+ return _PathRelation.inconclusive; |
+ } |
- return false; |
+ return _PathRelation.different; |
} |
// If the child is shorter than the parent, it's probably not within the |
@@ -694,21 +732,34 @@ class Context { |
// isWithin("foo/bar/baz", "foo/bar") //=> false |
// isWithin("foo/bar/baz/../..", "foo/bar") //=> true |
if (childIndex == child.length) { |
- var direction = _pathDirection(parentCodeUnits, 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 |
// decision. Before we do, though, we'll check the rest of the child to see |
// what that tells us. |
- var direction = _pathDirection(childCodeUnits, childIndex); |
+ 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. |
@@ -716,7 +767,9 @@ class Context { |
// 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. |
@@ -724,12 +777,14 @@ class Context { |
// isWithin("foo/bar", "foo/bar/baz") //=> true |
// isWithin("foo/bar/", "foo/bar/baz") //=> true |
// isWithin("foo/bar", "foo/barbaz") //=> false |
- return style.isSeparator(childCodeUnits[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. |
// |
@@ -741,31 +796,31 @@ class Context { |
// pathDirection("foo/../baz") //=> reaches root |
// pathDirection("foo/../..") //=> above root |
// pathDirection("foo/../../foo/bar/baz") //=> above root |
- _PathDirection _pathDirection(List<int> codeUnits, int index) { |
+ _PathDirection _pathDirection(String path, int index) { |
var depth = 0; |
var reachedRoot = false; |
var i = index; |
- while (i < codeUnits.length) { |
+ while (i < path.length) { |
// Ignore initial separators or doubled separators. |
- while (i < codeUnits.length && style.isSeparator(codeUnits[i])) { |
+ while (i < path.length && style.isSeparator(path.codeUnitAt(i))) { |
i++; |
} |
// If we're at the end, stop. |
- if (i == codeUnits.length) break; |
+ if (i == path.length) break; |
// Move through the path component to the next separator. |
var start = i; |
- while (i < codeUnits.length && !style.isSeparator(codeUnits[i])) { |
+ while (i < path.length && !style.isSeparator(path.codeUnitAt(i))) { |
i++; |
} |
// See if the path component is ".", "..", or a name. |
- if (i - start == 1 && codeUnits[start] == chars.PERIOD) { |
+ if (i - start == 1 && path.codeUnitAt(start) == chars.PERIOD) { |
// Don't change the depth. |
} else if (i - start == 2 && |
- codeUnits[start] == chars.PERIOD && |
- codeUnits[start + 1] == chars.PERIOD) { |
+ path.codeUnitAt(start) == chars.PERIOD && |
+ path.codeUnitAt(start + 1) == chars.PERIOD) { |
// ".." backs out a directory. |
depth--; |
@@ -781,7 +836,7 @@ class Context { |
} |
// If we're at the end, stop. |
- if (i == codeUnits.length) break; |
+ if (i == path.length) break; |
// Move past the separator. |
i++; |
@@ -793,6 +848,80 @@ class Context { |
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' |
@@ -952,3 +1081,32 @@ class _PathDirection { |
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; |
+} |
+ |