From f571c4a6c3cf31ac39c1dcc399881b4f08fbd52d Mon Sep 17 00:00:00 2001 From: Danny Tuppeny Date: Wed, 9 Aug 2023 16:55:32 +0100 Subject: [PATCH] Handle escaped colons in Windows file:// URIs (dart-lang/path#149) Fixes dart-lang/path#148 Using `Uri().toFilePath()` handles escaped colons in drive letters, but `Context.fromUri()` currently does not. Allow the percent encoded colon by checking both formats and adjusting to the correct index for the following character when the percent encoding makes the drive letter longer than the typical two characters. --- pkgs/path/CHANGELOG.md | 3 ++- pkgs/path/lib/src/style/url.dart | 3 +-- pkgs/path/lib/src/utils.dart | 36 ++++++++++++++++++++----- pkgs/path/pubspec.yaml | 2 +- pkgs/path/test/url_test.dart | 15 ++++++++++- pkgs/path/test/windows_test.dart | 46 ++++++++++++++++++++++++++++++-- 6 files changed, 92 insertions(+), 13 deletions(-) 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); + }); }