@@ -372,18 +372,22 @@ class Context {
372
372
/// Canonicalizes [path] .
373
373
///
374
374
/// 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
376
376
/// [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.)
378
379
///
379
380
/// Note that this does not resolve symlinks.
380
381
///
381
382
/// If you want a map that uses path keys, it's probably more efficient to use
382
383
/// a Map with [equals] and [hash] specified as the callbacks to use for keys
383
384
/// than it is to canonicalize every key.
385
+ ///
384
386
String canonicalize (String path) {
385
387
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;
387
391
388
392
final parsed = _parse (path);
389
393
parsed.normalize (canonicalize: true );
@@ -395,7 +399,7 @@ class Context {
395
399
///
396
400
/// Note that this is *not* guaranteed to return the same result for two
397
401
/// 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.
399
403
///
400
404
/// context.normalize('path/./to/..//file.text'); // -> 'path/file.txt'
401
405
String normalize (String path) {
@@ -408,68 +412,76 @@ class Context {
408
412
409
413
/// Returns whether [path] needs to be normalized.
410
414
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;
415
444
416
445
// Skip past the root before we start looking for snippets that need
417
446
// normalization. We want to normalize "//", but not when it's part of
418
447
// "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
+ }
424
453
// On Windows, the root still needs to be normalized if it contains a
425
454
// forward slash.
426
455
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 ;
429
458
}
430
459
}
431
460
}
432
461
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) ;
435
464
if (style.isSeparator (codeUnit)) {
465
+ // If ending empty, `.` or `..` path segment.
466
+ if (state >= stateSeparator && state < stateNotDots) return true ;
436
467
// Forward slashes in Windows paths are normalized to backslashes.
437
468
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.
450
478
return true ;
451
479
}
452
480
}
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 ;
470
481
}
471
482
472
- return false ;
483
+ // Otherwise only normalize if there are separators and single/double dots.
484
+ return state >= stateSeparator && state < stateNotDots;
473
485
}
474
486
475
487
/// Attempts to convert [path] to an equivalent relative path relative to
@@ -1020,7 +1032,9 @@ class Context {
1020
1032
/// Returns the path represented by [uri] , which may be a [String] or a [Uri] .
1021
1033
///
1022
1034
/// 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
+ ///
1024
1038
///
1025
1039
/// // POSIX
1026
1040
/// context.fromUri('file:///path/to/foo')
@@ -1036,7 +1050,11 @@ class Context {
1036
1050
///
1037
1051
/// If [uri] is relative, a relative path will be returned.
1038
1052
///
1053
+ /// // POSIX
1039
1054
/// path.fromUri('path/to/foo'); // -> 'path/to/foo'
1055
+ ///
1056
+ /// // Windows
1057
+ /// path.fromUri('/C:/foo'); // -> r'C:\foo`
1040
1058
String fromUri (Object ? uri) => style.pathFromUri (_parseUri (uri! ));
1041
1059
1042
1060
/// Returns the URI that represents [path] .
0 commit comments