Skip to content

Commit

Permalink
Handle escaped colons in Windows file:// URIs (dart-lang/path#149)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
DanTup authored Aug 9, 2023
1 parent 9bd8dd9 commit f571c4a
Show file tree
Hide file tree
Showing 6 changed files with 92 additions and 13 deletions.
3 changes: 2 additions & 1 deletion pkgs/path/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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

Expand Down
3 changes: 1 addition & 2 deletions pkgs/path/lib/src/style/url.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}

Expand Down
36 changes: 30 additions & 6 deletions pkgs/path/lib/src/utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
2 changes: 1 addition & 1 deletion pkgs/path/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -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
Expand Down
15 changes: 14 additions & 1 deletion pkgs/path/test/url_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -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');
Expand Down
46 changes: 44 additions & 2 deletions pkgs/path/test/windows_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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');
Expand Down Expand Up @@ -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');
Expand All @@ -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');
Expand All @@ -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');
Expand All @@ -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');
});
});

Expand Down Expand Up @@ -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');
});
Expand All @@ -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);
});
}

0 comments on commit f571c4a

Please sign in to comment.