Make Windows path manipulation case-insensitive. (#15)
See #14
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a404f70..3c9fb6b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,7 @@
## 1.3.10
+* Properly compare Windows paths case-insensitively.
+
* Further improve the performance of `isWithin()`.
## 1.3.9
diff --git a/lib/src/context.dart b/lib/src/context.dart
index 8819706..3e84cae 100644
--- a/lib/src/context.dart
+++ b/lib/src/context.dart
@@ -444,15 +444,14 @@
// 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);
@@ -563,15 +562,7 @@
for (var i = 0; i < parentRootLength; i++) {
var parentCodeUnit = parent.codeUnitAt(i);
var childCodeUnit = child.codeUnitAt(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;
- }
+ if (!style.codeUnitsEqual(parentCodeUnit, childCodeUnit)) return false;
}
// Start by considering the last code unit as a separator, since
@@ -585,17 +576,7 @@
while (parentIndex < parent.length && childIndex < child.length) {
var parentCodeUnit = parent.codeUnitAt(parentIndex);
var childCodeUnit = child.codeUnitAt(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) {
+ if (style.codeUnitsEqual(parentCodeUnit, childCodeUnit)) {
lastCodeUnit = parentCodeUnit;
parentIndex++;
childIndex++;
@@ -603,10 +584,12 @@
}
// Ignore multiple separators in a row.
- if (parentIsSeparator && style.isSeparator(lastCodeUnit)) {
+ if (style.isSeparator(parentCodeUnit) &&
+ style.isSeparator(lastCodeUnit)) {
parentIndex++;
continue;
- } else if (childIsSeparator && style.isSeparator(lastCodeUnit)) {
+ } else if (style.isSeparator(childCodeUnit) &&
+ style.isSeparator(lastCodeUnit)) {
childIndex++;
continue;
}
diff --git a/lib/src/internal_style.dart b/lib/src/internal_style.dart
index 84bc67e..1450d53 100644
--- a/lib/src/internal_style.dart
+++ b/lib/src/internal_style.dart
@@ -66,4 +66,14 @@
/// Returns the URI that represents [path], which is assumed to be absolute.
Uri absolutePathToUri(String path);
+
+ /// Returns whether [codeUnit1] and [codeUnit2] are considered equivalent for
+ /// this style.
+ bool codeUnitsEqual(int codeUnit1, int codeUnit2) => codeUnit1 == codeUnit2;
+
+ /// Returns whether [path1] and [path2] are equivalent.
+ ///
+ /// 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;
}
diff --git a/lib/src/style/windows.dart b/lib/src/style/windows.dart
index 8223486..0d89764 100644
--- a/lib/src/style/windows.dart
+++ b/lib/src/style/windows.dart
@@ -7,6 +7,10 @@
import '../parsed_path.dart';
import '../utils.dart';
+// `0b100000` can be bitwise-ORed with uppercase ASCII letters to get their
+// lowercase equivalents.
+const _asciiCaseBit = 0x20;
+
/// The style for Windows paths.
class WindowsStyle extends InternalStyle {
WindowsStyle();
@@ -120,4 +124,31 @@
return new Uri(scheme: 'file', pathSegments: parsed.parts);
}
}
+
+ bool codeUnitsEqual(int codeUnit1, int codeUnit2) {
+ if (codeUnit1 == codeUnit2) return true;
+
+ /// Forward slashes and backslashes are equivalent on Windows.
+ if (codeUnit1 == chars.SLASH) return codeUnit2 == chars.BACKSLASH;
+ if (codeUnit1 == chars.BACKSLASH) return codeUnit2 == chars.SLASH;
+
+ // If this check fails, the code units are definitely different. If it
+ // succeeds *and* either codeUnit is an ASCII letter, they're equivalent.
+ if (codeUnit1 ^ codeUnit2 != _asciiCaseBit) return false;
+
+ // Now we just need to verify that one of the code units is an ASCII letter.
+ var upperCase1 = codeUnit1 | _asciiCaseBit;
+ return upperCase1 >= chars.LOWER_A && upperCase1 <= chars.LOWER_Z;
+ }
+
+ bool pathsEqual(String path1, String path2) {
+ if (identical(path1, path2)) return true;
+ if (path1.length != path2.length) return false;
+ for (var i = 0; i < path1.length; i++) {
+ if (!codeUnitsEqual(path1.codeUnitAt(i), path2.codeUnitAt(i))) {
+ return false;
+ }
+ }
+ return true;
+ }
}
diff --git a/test/posix_test.dart b/test/posix_test.dart
index 7c5aaf4..7b55dd3 100644
--- a/test/posix_test.dart
+++ b/test/posix_test.dart
@@ -348,6 +348,11 @@
expect(context.relative('a/./b/../c.txt'), 'a/c.txt');
});
+ test('is case-sensitive', () {
+ expect(context.relative('/RoOt'), '../../RoOt');
+ expect(context.relative('/rOoT/pAtH/a'), '../../rOoT/pAtH/a');
+ });
+
// Regression
test('from root-only path', () {
expect(context.relative('/', from: '/'), '.');
diff --git a/test/url_test.dart b/test/url_test.dart
index c81893a..4adfb48 100644
--- a/test/url_test.dart
+++ b/test/url_test.dart
@@ -479,6 +479,15 @@
expect(context.relative('a/./b/../c.txt'), 'a/c.txt');
});
+ test('is case-sensitive', () {
+ expect(context.relative('HtTp://dartlang.org/root'),
+ 'HtTp://dartlang.org/root');
+ expect(context.relative('http://DaRtLaNg.OrG/root'),
+ 'http://DaRtLaNg.OrG/root');
+ expect(context.relative('/RoOt'), '../../RoOt');
+ expect(context.relative('/rOoT/pAtH/a'), '../../rOoT/pAtH/a');
+ });
+
// Regression
test('from root-only path', () {
expect(context.relative('http://dartlang.org',
diff --git a/test/windows_test.dart b/test/windows_test.dart
index 8b83c03..8e72bb1 100644
--- a/test/windows_test.dart
+++ b/test/windows_test.dart
@@ -425,6 +425,12 @@
expect(context.relative(r'a\.\b\..\c.txt'), r'a\c.txt');
});
+ test('is case-insensitive', () {
+ expect(context.relative(r'c:\'), r'..\..');
+ expect(context.relative(r'c:\RoOt'), r'..');
+ expect(context.relative(r'c:\rOoT\pAtH\a'), r'a');
+ });
+
// Regression
test('from root-only path', () {
expect(context.relative(r'C:\', from: r'C:\'), '.');
@@ -567,6 +573,12 @@
expect(r.isWithin(r'C:\', r'C:\baz\bang'), isTrue);
expect(r.isWithin('.', r'C:\baz\bang'), isFalse);
});
+
+ test('is case-insensitive', () {
+ expect(context.isWithin(r'FoO', r'fOo\bar'), isTrue);
+ expect(context.isWithin(r'C:\', r'c:\foo'), isTrue);
+ expect(context.isWithin(r'fOo\qux\..\BaR', r'FoO\bAr\baz'), isTrue);
+ });
});
group('absolute', () {