@@ -372,18 +372,22 @@ class Context {
372372 /// Canonicalizes [path] .
373373 ///
374374 /// This is guaranteed to return the same path for two different input paths
375- /// if and only if both input paths point to the same location. Unlike
375+ /// only if both input paths point to the same location. Unlike
376376 /// [normalize] , it returns absolute paths when possible and canonicalizes
377- /// ASCII case on Windows.
377+ /// ASCII case on Windows, and scheme and authority case for URLs (but does
378+ /// not normalize or canonicalize `%` -escapes.)
378379 ///
379380 /// Note that this does not resolve symlinks.
380381 ///
381382 /// If you want a map that uses path keys, it's probably more efficient to use
382383 /// a Map with [equals] and [hash] specified as the callbacks to use for keys
383384 /// than it is to canonicalize every key.
385+ ///
384386 String canonicalize (String path) {
385387 path = absolute (path);
386- if (style != Style .windows && ! _needsNormalization (path)) return path;
388+ // Windows and URL styles need to case-canonicalize, even if it doesn't
389+ // need to normalize anything.
390+ if (style == Style .posix && ! _needsNormalization (path)) return path;
387391
388392 final parsed = _parse (path);
389393 parsed.normalize (canonicalize: true );
@@ -395,7 +399,7 @@ class Context {
395399 ///
396400 /// Note that this is *not* guaranteed to return the same result for two
397401 /// equivalent input paths. For that, see [canonicalize] . Or, if you're using
398- /// paths as map keys use [equals] and [hash] as the key callbacks.
402+ /// paths as map keys, use [equals] and [hash] as the key callbacks.
399403 ///
400404 /// context.normalize('path/./to/..//file.text'); // -> 'path/file.txt'
401405 String normalize (String path) {
@@ -408,68 +412,76 @@ class Context {
408412
409413 /// Returns whether [path] needs to be normalized.
410414 bool _needsNormalization (String path) {
411- var start = 0 ;
412- final codeUnits = path.codeUnits;
413- int ? previousPrevious;
414- int ? previous;
415+ // Empty paths are normalized to ".".
416+ if (path.isEmpty) return true ;
417+
418+ // At start, no previous separator.
419+ const stateStart = 0 ;
420+
421+ // Previous character was a separator.
422+ const stateSeparator = 1 ;
423+
424+ // Added to state for each `.` seen.
425+ const stateDotCount = 2 ;
426+
427+ // Path segment that contains anything other than nothing, `.` or `..`.
428+ //
429+ // Includes any value at or above this one.
430+ const stateNotDots = 6 ;
431+
432+ // Current state of the last few characters.
433+ //
434+ // Seeing a separator resets to [stateSeparator].
435+ // Seeing a `.` adds [stateDotCount].
436+ // Seeing any non-separator or more than two dots will
437+ // bring the value above [stateNotDots].
438+ // (The separator may be optional at the start, seeing one is fine,
439+ // and seeing dots will start counting.)
440+ // (That is, `/` has value 1, `/.` value 3, ``/..` value 5, and anything
441+ // else is 6 or above, except at the very start where empty path, `.`
442+ // and `..` have values 0, 2 and 4.)
443+ var state = stateStart;
415444
416445 // Skip past the root before we start looking for snippets that need
417446 // normalization. We want to normalize "//", but not when it's part of
418447 // "http://".
419- final root = style.rootLength (path);
420- if (root != 0 ) {
421- start = root;
422- previous = chars.slash ;
423-
448+ final start = style.rootLength (path);
449+ if (start != 0 ) {
450+ if (style. isSeparator (path. codeUnitAt ( start - 1 ))) {
451+ state = stateSeparator ;
452+ }
424453 // On Windows, the root still needs to be normalized if it contains a
425454 // forward slash.
426455 if (style == Style .windows) {
427- for (var i = 0 ; i < root ; i++ ) {
428- if (codeUnits[i] == chars.slash) return true ;
456+ for (var i = 0 ; i < start ; i++ ) {
457+ if (path. codeUnitAt (i) == chars.slash) return true ;
429458 }
430459 }
431460 }
432461
433- for (var i = start; i < codeUnits .length; i++ ) {
434- final codeUnit = codeUnits[i] ;
462+ for (var i = start; i < path .length; i++ ) {
463+ final codeUnit = path. codeUnitAt (i) ;
435464 if (style.isSeparator (codeUnit)) {
465+ // If ending empty, `.` or `..` path segment.
466+ if (state >= stateSeparator && state < stateNotDots) return true ;
436467 // Forward slashes in Windows paths are normalized to backslashes.
437468 if (style == Style .windows && codeUnit == chars.slash) return true ;
438-
439- // Multiple separators are normalized to single separators.
440- if (previous != null && style.isSeparator (previous)) return true ;
441-
442- // Single dots and double dots are normalized to directory traversals.
443- //
444- // This can return false positives for ".../", but that's unlikely
445- // enough that it's probably not going to cause performance issues.
446- if (previous == chars.period &&
447- (previousPrevious == null ||
448- previousPrevious == chars.period ||
449- style.isSeparator (previousPrevious))) {
469+ state = stateSeparator;
470+ } else if (codeUnit == chars.period) {
471+ state += stateDotCount;
472+ } else {
473+ state = stateNotDots;
474+ if (style == Style .url &&
475+ (codeUnit == chars.question || codeUnit == chars.hash)) {
476+ // Normalize away `?` query parts and `#` fragment parts in URL
477+ // styled paths.
450478 return true ;
451479 }
452480 }
453-
454- previousPrevious = previous;
455- previous = codeUnit;
456- }
457-
458- // Empty paths are normalized to ".".
459- if (previous == null ) return true ;
460-
461- // Trailing separators are removed.
462- if (style.isSeparator (previous)) return true ;
463-
464- // Single dots and double dots are normalized to directory traversals.
465- if (previous == chars.period &&
466- (previousPrevious == null ||
467- style.isSeparator (previousPrevious) ||
468- previousPrevious == chars.period)) {
469- return true ;
470481 }
471482
472- return false ;
483+ // Otherwise only normalize if there are separators and single/double dots.
484+ return state >= stateSeparator && state < stateNotDots;
473485 }
474486
475487 /// Attempts to convert [path] to an equivalent relative path relative to
@@ -1020,7 +1032,9 @@ class Context {
10201032 /// Returns the path represented by [uri] , which may be a [String] or a [Uri] .
10211033 ///
10221034 /// For POSIX and Windows styles, [uri] must be a `file:` URI. For the URL
1023- /// style, this will just convert [uri] to a string.
1035+ /// style, this will just convert [uri] to a string, but if the input was
1036+ /// a string, it will be parsed and normalized as a [Uri] first.
1037+ ///
10241038 ///
10251039 /// // POSIX
10261040 /// context.fromUri('file:///path/to/foo')
@@ -1036,7 +1050,11 @@ class Context {
10361050 ///
10371051 /// If [uri] is relative, a relative path will be returned.
10381052 ///
1053+ /// // POSIX
10391054 /// path.fromUri('path/to/foo'); // -> 'path/to/foo'
1055+ ///
1056+ /// // Windows
1057+ /// path.fromUri('/C:/foo'); // -> r'C:\foo`
10401058 String fromUri (Object ? uri) => style.pathFromUri (_parseUri (uri! ));
10411059
10421060 /// Returns the URI that represents [path] .
0 commit comments