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 1 commit
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: 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
27 changes: 21 additions & 6 deletions lib/src/utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,25 @@ 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 `null`
/// if [index] is not the start of a drive letter.
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;
}
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
18 changes: 17 additions & 1 deletion test/windows_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -761,7 +761,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 +784,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 +800,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 +814,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 +851,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 Down