Skip to content
This repository has been archived by the owner on Oct 18, 2024. It is now read-only.

Handle escaped colons in Windows file:// URIs #149

Merged
merged 2 commits into from
Aug 9, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion 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 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 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) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nit] What do you think about the name drivePathIndex

Can the doc call out the specific behavior when path is exactly the drive letter with no following character?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After re-reading the implementation, I think driveLetterEnd is a fine name.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've update the docs, let me know if this seems ok.

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 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 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 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);
});
}