diff --git a/pkgs/path/CHANGELOG.md b/pkgs/path/CHANGELOG.md index f1073b0e..88a3d942 100644 --- a/pkgs/path/CHANGELOG.md +++ b/pkgs/path/CHANGELOG.md @@ -1,7 +1,8 @@ -## 1.8.4-wip +## 1.9.0-wip * Require Dart 3.0 * Fixed an issue with the `split` method doc comment. +* Allow percent-encoded colons (`%3a`) in drive letters in `fromUri`. ## 1.8.3 diff --git a/pkgs/path/lib/src/style/url.dart b/pkgs/path/lib/src/style/url.dart index 3ff87ced..a2d3b0ce 100644 --- a/pkgs/path/lib/src/style/url.dart +++ b/pkgs/path/lib/src/style/url.dart @@ -64,8 +64,7 @@ class UrlStyle extends InternalStyle { // See https://url.spec.whatwg.org/#file-slash-state. if (!withDrive || path.length < index + 3) return index; if (!path.startsWith('file://')) return index; - if (!isDriveLetter(path, index + 1)) return index; - return path.length == index + 3 ? index + 3 : index + 4; + return driveLetterEnd(path, index + 1) ?? index; } } diff --git a/pkgs/path/lib/src/utils.dart b/pkgs/path/lib/src/utils.dart index 2443ccd4..7c01312d 100644 --- a/pkgs/path/lib/src/utils.dart +++ b/pkgs/path/lib/src/utils.dart @@ -15,10 +15,34 @@ bool isNumeric(int char) => char >= chars.zero && char <= chars.nine; /// Returns whether [path] has a URL-formatted Windows drive letter beginning at /// [index]. -bool isDriveLetter(String path, int index) { - if (path.length < index + 2) return false; - if (!isAlphabetic(path.codeUnitAt(index))) return false; - if (path.codeUnitAt(index + 1) != chars.colon) return false; - if (path.length == index + 2) return true; - return path.codeUnitAt(index + 2) == chars.slash; +bool isDriveLetter(String path, int index) => + driveLetterEnd(path, index) != null; + +/// Returns the index of the first character after the drive letter or a +/// URL-formatted path, or `null` if [index] is not the start of a drive letter. +/// A valid drive letter must be followed by a colon and then either a `/` or +/// the end of string. +/// +/// ``` +/// d:/abc => 3 +/// d:/ => 3 +/// d: => 2 +/// d => null +/// ``` +int? driveLetterEnd(String path, int index) { + if (path.length < index + 2) return null; + if (!isAlphabetic(path.codeUnitAt(index))) return null; + if (path.codeUnitAt(index + 1) != chars.colon) { + // If not a raw colon, check for escaped colon + if (path.length < index + 4) return null; + if (path.substring(index + 1, index + 4).toLowerCase() != '%3a') { + return null; + } + // Offset the index to account for the extra 2 characters from the + // colon encoding. + index += 2; + } + if (path.length == index + 2) return index + 2; + if (path.codeUnitAt(index + 2) != chars.slash) return null; + return index + 3; } diff --git a/pkgs/path/pubspec.yaml b/pkgs/path/pubspec.yaml index 65ceee56..63bbeccf 100644 --- a/pkgs/path/pubspec.yaml +++ b/pkgs/path/pubspec.yaml @@ -1,5 +1,5 @@ name: path -version: 1.8.4-wip +version: 1.9.0-wip description: >- A string-based path manipulation library. All of the path operations you know and love, with solid support for Windows, POSIX (Linux and Mac OS X), and the diff --git a/pkgs/path/test/url_test.dart b/pkgs/path/test/url_test.dart index 53a24047..8a043dc5 100644 --- a/pkgs/path/test/url_test.dart +++ b/pkgs/path/test/url_test.dart @@ -301,6 +301,19 @@ void main() { 'file://host/c:/baz/qux'); }); + test( + 'treats drive letters as part of the root for file: URLs ' + 'with encoded colons', () { + expect(context.join('file:///c%3A/foo/bar', '/baz/qux'), + 'file:///c%3A/baz/qux'); + expect(context.join('file:///D%3A/foo/bar', '/baz/qux'), + 'file:///D%3A/baz/qux'); + expect(context.join('file:///c%3A/', '/baz/qux'), 'file:///c%3A/baz/qux'); + expect(context.join('file:///c%3A', '/baz/qux'), 'file:///c%3A/baz/qux'); + expect(context.join('file://host/c%3A/foo/bar', '/baz/qux'), + 'file://host/c%3A/baz/qux'); + }); + test('treats drive letters as normal components for non-file: URLs', () { expect(context.join('http://foo.com/c:/foo/bar', '/baz/qux'), 'http://foo.com/baz/qux'); @@ -884,7 +897,7 @@ void main() { expect(context.withoutExtension('a/b.c//'), 'a/b//'); }); - test('withoutExtension', () { + test('setExtension', () { expect(context.setExtension('', '.x'), '.x'); expect(context.setExtension('a', '.x'), 'a.x'); expect(context.setExtension('.a', '.x'), '.a.x'); diff --git a/pkgs/path/test/windows_test.dart b/pkgs/path/test/windows_test.dart index 4036fc66..0bc2d4eb 100644 --- a/pkgs/path/test/windows_test.dart +++ b/pkgs/path/test/windows_test.dart @@ -3,6 +3,7 @@ // BSD-style license that can be found in the LICENSE file. import 'package:path/path.dart' as path; +import 'package:path/src/utils.dart'; import 'package:test/test.dart'; import 'utils.dart'; @@ -43,7 +44,7 @@ void main() { expect(context.rootPrefix('a'), ''); expect(context.rootPrefix(r'a\b'), ''); expect(context.rootPrefix(r'C:\a\c'), r'C:\'); - expect(context.rootPrefix('C:\\'), r'C:\'); + expect(context.rootPrefix(r'C:\'), r'C:\'); expect(context.rootPrefix('C:/'), 'C:/'); expect(context.rootPrefix(r'\\server\share\a\b'), r'\\server\share'); expect(context.rootPrefix(r'\\server\share'), r'\\server\share'); @@ -761,7 +762,7 @@ void main() { expect(context.withoutExtension(r'a\b.c\'), r'a\b\'); }); - test('withoutExtension', () { + test('setExtension', () { expect(context.setExtension('', '.x'), '.x'); expect(context.setExtension('a', '.x'), 'a.x'); expect(context.setExtension('.a', '.x'), '.a.x'); @@ -784,9 +785,12 @@ void main() { test('with a URI', () { expect(context.fromUri(Uri.parse('file:///C:/path/to/foo')), r'C:\path\to\foo'); + expect(context.fromUri(Uri.parse('file:///C%3A/path/to/foo')), + r'C:\path\to\foo'); expect(context.fromUri(Uri.parse('file://server/share/path/to/foo')), r'\\server\share\path\to\foo'); expect(context.fromUri(Uri.parse('file:///C:/')), r'C:\'); + expect(context.fromUri(Uri.parse('file:///C%3A/')), r'C:\'); expect( context.fromUri(Uri.parse('file://server/share')), r'\\server\share'); expect(context.fromUri(Uri.parse('foo/bar')), r'foo\bar'); @@ -797,6 +801,8 @@ void main() { r'\\server\share\path\to\foo'); expect(context.fromUri(Uri.parse('file:///C:/path/to/foo%23bar')), r'C:\path\to\foo#bar'); + expect(context.fromUri(Uri.parse('file:///C%3A/path/to/foo%23bar')), + r'C:\path\to\foo#bar'); expect( context.fromUri(Uri.parse('file://server/share/path/to/foo%23bar')), r'\\server\share\path\to\foo#bar'); @@ -809,6 +815,7 @@ void main() { test('with a string', () { expect(context.fromUri('file:///C:/path/to/foo'), r'C:\path\to\foo'); + expect(context.fromUri('file:///C%3A/path/to/foo'), r'C:\path\to\foo'); }); }); @@ -845,6 +852,16 @@ void main() { expect(context.prettyUri('file:///C:/root/other'), r'..\other'); }); + test('with a file: URI with encoded colons', () { + expect(context.prettyUri('file:///C%3A/root/path/a/b'), r'a\b'); + expect(context.prettyUri('file:///C%3A/root/path/a/../b'), r'b'); + expect(context.prettyUri('file:///C%3A/other/path/a/b'), + r'C:\other\path\a\b'); + expect( + context.prettyUri('file:///D%3A/root/path/a/b'), r'D:\root\path\a\b'); + expect(context.prettyUri('file:///C%3A/root/other'), r'..\other'); + }); + test('with an http: URI', () { expect(context.prettyUri('https://dart.dev/a/b'), 'https://dart.dev/a/b'); }); @@ -861,4 +878,29 @@ void main() { expect(context.prettyUri(Uri.parse('a/b')), r'a\b'); }); }); + + test('driveLetterEnd', () { + expect(driveLetterEnd('', 0), null); + expect(driveLetterEnd('foo.dart', 0), null); + expect(driveLetterEnd('@', 0), null); + + expect(driveLetterEnd('c:', 0), 2); + + // colons + expect(driveLetterEnd('c:/', 0), 3); + expect(driveLetterEnd('c:/a', 0), 3); + + // escaped colons lowercase + expect(driveLetterEnd('c%3a/', 0), 5); + expect(driveLetterEnd('c%3a/a', 0), 5); + + // escaped colons uppercase + expect(driveLetterEnd('c%3A/', 0), 5); + expect(driveLetterEnd('c%3A/a', 0), 5); + + // non-drive letter + expect(driveLetterEnd('ab:/c', 0), null); + expect(driveLetterEnd('ab%3a/c', 0), null); + expect(driveLetterEnd('ab%3A/c', 0), null); + }); }