Index: lib/src/context.dart |
diff --git a/lib/src/context.dart b/lib/src/context.dart |
index 44bdde994ea46daec0052052e8aaea4bc176be2e..891e3fbd63d8f75a0d0c3dab9e6923a53699a1a4 100644 |
--- a/lib/src/context.dart |
+++ b/lib/src/context.dart |
@@ -411,13 +411,7 @@ class Context { |
/// "/", no path can be determined. In this case, a [PathException] will be |
/// thrown. |
String relative(String path, {String from}) { |
- // Avoid calling [current] since it is slow and calling join() when |
- // [from] is absolute does nothing. |
- if (from == null) { |
- from = current; |
- } else if (this.isRelative(from) || this.isRootRelative(from)) { |
- from = this.join(current, from); |
- } |
+ from = from == null ? current : absolute(from); |
// We can't determine the path from a relative path to an absolute path. |
if (this.isRelative(from) && this.isAbsolute(path)) { |
@@ -503,6 +497,31 @@ class Context { |
/// path.isWithin('/root/path', '/root/other'); // -> false |
/// path.isWithin('/root/path', '/root/path'); // -> false |
bool isWithin(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. |
+ var parentIsAbsolute = isAbsolute(parent); |
+ var childIsAbsolute = isAbsolute(child); |
+ if (parentIsAbsolute && !childIsAbsolute) { |
+ child = absolute(child); |
+ if (style.isRootRelative(parent)) parent = absolute(parent); |
+ } else if (childIsAbsolute && !parentIsAbsolute) { |
+ parent = absolute(parent); |
+ if (style.isRootRelative(child)) child = absolute(child); |
+ } else if (childIsAbsolute && parentIsAbsolute) { |
+ var childIsRootRelative = style.isRootRelative(child); |
+ var parentIsRootRelative = style.isRootRelative(parent); |
+ |
+ if (childIsRootRelative && !parentIsRootRelative) { |
+ child = absolute(child); |
+ } else if (parentIsRootRelative && !childIsRootRelative) { |
+ parent = absolute(parent); |
+ } |
+ } |
+ |
+ var fastResult = _isWithinFast(parent, child); |
+ if (fastResult != null) return fastResult; |
+ |
var relative; |
try { |
relative = this.relative(child, from: parent); |
@@ -518,6 +537,228 @@ class Context { |
parts.first != '.'; |
} |
+ /// An optimized implementation of [isWithin] that doesn't handle a few |
+ /// complex cases. |
+ bool _isWithinFast(String parent, String child) { |
Bob Nystrom
2015/11/24 17:22:13
Possibly dumb question. How much mileage would we
nweiz
2015/12/01 00:15:15
That gives a false positive for isWithin("/foo/bar
|
+ // Normally we just bail when we see "." path components, but we can handle |
+ // a single dot easily enough. |
+ if (parent == '.') parent = ''; |
+ |
+ var parentRootLength = style.rootLength(parent); |
+ var childRootLength = style.rootLength(child); |
+ |
+ // If the roots aren't the same length, we know both paths are absolute or |
+ // both are root-relative, and thus that the roots are meaningfully |
+ // different. |
+ // |
+ // 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; |
+ |
+ // 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; |
+ } |
+ } |
+ |
+ // Start by considering the last code unit as a separator, since |
+ // semantically we're starting at a new path component even if we're |
+ // comparing relative paths. |
+ var lastCodeUnit = chars.SLASH; |
+ |
+ // 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; |
+ } |
+ |
+ // Different separators are considered identical. |
+ var parentIsSeparator = style.isSeparator(parentCodeUnit); |
+ var childIsSeparator = style.isSeparator(childCodeUnit); |
+ if (parentIsSeparator && childIsSeparator) { |
+ lastCodeUnit = parentCodeUnit; |
+ parentIndex++; |
+ childIndex++; |
+ continue; |
+ } |
+ |
+ // Ignore multiple separators in a row. |
+ if (parentIsSeparator && style.isSeparator(lastCodeUnit)) { |
+ parentIndex++; |
+ continue; |
+ } else if (childIsSeparator && style.isSeparator(lastCodeUnit)) { |
+ childIndex++; |
+ continue; |
+ } |
+ |
+ // If a dot comes after a separator or another dot, it may be a |
+ // directory traversal operator. Otherwise, it's just a normal |
+ // non-matching character. |
+ // |
+ // isWithin("foo/./bar", "foo/bar/baz") //=> true |
+ // isWithin("foo/bar/../baz", "foo/bar/.foo") //=> false |
+ // |
+ // We could stay on the fast path for "/./", but that adds a lot of |
+ // complexity and isn't likely to come up much in practice. |
+ if ((parentCodeUnit == chars.PERIOD || childCodeUnit == chars.PERIOD) && |
+ (style.isSeparator(lastCodeUnit) || lastCodeUnit == chars.PERIOD)) { |
+ return null; |
+ } |
+ |
+ // If we're here, we've hit two non-matching, non-significant characters. |
+ // 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]. |
+ if (_checkRemainder(childCodeUnits, childIndex) < 1) return null; |
+ if (_checkRemainder(parentCodeUnits, parentIndex) < 1) return null; |
+ return false; |
+ } |
+ |
+ // If the child is shorter than the parent, it's probably not within the |
+ // parent. The only exception is if the parent has some weird ".." stuff |
+ // going on, in which case we do the slow check. |
+ // |
+ // isWithin("foo/bar/baz", "foo/bar") //=> false |
+ // isWithin("foo/bar/baz/../..", "foo/bar") //=> true |
+ if (childIndex == child.length) { |
+ var result = _checkRemainder(parentCodeUnits, parentIndex); |
+ return result < 0 ? null : false; |
+ } |
+ |
+ // 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 result = _checkRemainder(childCodeUnits, childIndex); |
+ |
+ // If there are no more components in the child, then it's the same as |
+ // the parent, not within it. |
+ // |
+ // isWithin("foo/bar", "foo/bar") //=> false |
+ // isWithin("foo/bar", "foo/bar//") //=> false |
+ if (result == 0) return false; |
+ |
+ // If there are unresolved ".." components in the child, no decision we make |
+ // will be valid. We'll abort and do the slow check instead. |
+ // |
+ // 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 (result < 0) return null; |
+ |
+ // The child is within the parent if and only if we're on a separator |
+ // boundary. |
+ // |
+ // 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); |
+ } |
+ |
+ // Returns the information about the path represented by [codeUnits] after |
+ // [index]. |
+ // |
+ // Specifically, this returns: |
+ // |
+ // * A negative number if the path contains ".." components that at any point |
+ // cause the directory to go above the root. |
+ // |
+ // * Zero if the path has no components, or if it contains ".." that at any |
+ // point cause the directory to go back to the root (but not above it). |
+ // |
+ // * A positive number otherwise. |
+ // |
+ // This ignores leading separators. |
+ // |
+ // checkRemainder("foo") //=> + |
+ // checkRemainder("foo/bar/../baz") //=> + |
+ // checkRemainder("//foo/bar/baz") //=> + |
+ // checkRemainder("/") //=> 0 |
+ // checkRemainder("foo/../baz") //=> 0 |
+ // checkRemainder("foo/../..") //=> - |
+ // checkRemainder("foo/../../foo/bar/baz") //=> - |
+ int _checkRemainder(List<int> codeUnits, int index) { |
Bob Nystrom
2015/11/24 17:22:13
How about _pathDirection()? Something other than "
nweiz
2015/12/01 00:15:15
Done.
|
+ var depth = 0; |
+ |
+ // We initially consider ourselves to be after an invisible root separator. |
+ var afterSeparator = true; |
+ for (var i = index; i < codeUnits.length; i++) { |
+ var codeUnit = codeUnits[i]; |
+ |
+ if (style.isSeparator(codeUnit)) { |
+ // Ignore doubled separators (and initial separators). |
+ if (!afterSeparator) depth++; |
+ afterSeparator = true; |
+ continue; |
+ } |
+ |
+ // Ignore non-meaningful characters. |
+ if (codeUnit != chars.PERIOD || !afterSeparator) { |
+ afterSeparator = false; |
+ continue; |
+ } |
+ |
+ // Move forward so we're positioned after "/.". |
+ i++; |
+ |
+ if (i == codeUnits.length) return depth; |
+ codeUnit = codeUnits[i]; |
+ |
+ // Ignore "/./", and don't increment the depth for the trailing slash. |
+ if (style.isSeparator(codeUnit)) continue; |
+ |
+ // "/.foo" isn't anything meaningful. |
+ if (codeUnit != chars.PERIOD) { |
+ afterSeparator = false; |
+ continue; |
+ } |
+ |
+ // Move forward again so we're positioned after "/..". |
+ i++; |
+ |
+ if (i == codeUnits.length) return depth - 1; |
+ codeUnit = codeUnits[i]; |
+ |
+ // "/../" decreases depth. |
+ if (style.isSeparator(codeUnit)) { |
+ depth--; |
+ if (depth == 0) return 0; |
+ if (depth < 0) return depth; |
+ continue; |
+ } |
+ |
+ // "/..foo" isn't anything meaningful. |
+ afterSeparator = false; |
+ } |
Bob Nystrom
2015/11/24 17:22:13
Instead of a for loop with a sort of state machine
nweiz
2015/12/01 00:15:15
Done. I also realized that there were some bugs in
|
+ |
+ // If the path didn't have a trailing separator, add another unit of depth |
+ // to account for the current component. We have to do this because depth is counted |
Bob Nystrom
2015/11/24 17:22:13
Long line.
nweiz
2015/12/01 00:15:15
Acknowledged.
|
+ // on each trailing separator. |
+ return afterSeparator ? depth : depth + 1; |
+ } |
+ |
/// Removes a trailing extension from the last part of [path]. |
/// |
/// context.withoutExtension('path/to/foo.dart'); // -> 'path/to/foo' |