Skip to content

Commit b89dadc

Browse files
committed
Make URL strategy better at recognizing URLs.
More precise scheme detection, more precise authority detection, more precise drive-letter detection. Generally recognizes that a path ends at `#` or `?`, and doesn't parse into that. Recognizes only valid schemes as schemes. Ends authority at `/`, `#` or `?` or end-of-path, not just at `/`. Allows drive letter ended by `#` or `?` or end-of-path, not just `/`.
1 parent 61e6771 commit b89dadc

File tree

3 files changed

+107
-21
lines changed

3 files changed

+107
-21
lines changed

pkgs/path/lib/src/characters.dart

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,23 @@
22
// for details. All rights reserved. Use of this source code is governed by a
33
// BSD-style license that can be found in the LICENSE file.
44

5-
/// This library contains character-code definitions.
5+
// Character-code constants.
6+
7+
const hash = 0x23;
68
const plus = 0x2b;
79
const minus = 0x2d;
810
const period = 0x2e;
911
const slash = 0x2f;
1012
const zero = 0x30;
1113
const nine = 0x39;
1214
const colon = 0x3a;
15+
const question = 0x3f;
1316
const upperA = 0x41;
1417
const upperZ = 0x5a;
1518
const lowerA = 0x61;
1619
const lowerZ = 0x7a;
1720
const backslash = 0x5c;
21+
22+
bool isLetter(int char) => lowerA <= (char |= 0x20) && char <= lowerZ;
23+
24+
bool isDigit(int char) => char ^ zero <= 9;

pkgs/path/lib/src/style/url.dart

Lines changed: 77 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -43,32 +43,89 @@ class UrlStyle extends InternalStyle {
4343
return path.endsWith('://') && rootLength(path) == path.length;
4444
}
4545

46+
/// Checks if [path] starts with `"file:"`, case insensitively.
47+
static bool _startsWithFileColon(String path) {
48+
if (path.length < 5) return false;
49+
const f = 0x66;
50+
const i = 0x69;
51+
const l = 0x6c;
52+
const e = 0x65;
53+
return path.codeUnitAt(4) == chars.colon &&
54+
(path.codeUnitAt(0) | 0x20) == f &&
55+
(path.codeUnitAt(1) | 0x20) == i &&
56+
(path.codeUnitAt(2) | 0x20) == l &&
57+
(path.codeUnitAt(3) | 0x20) == e;
58+
}
59+
4660
@override
4761
int rootLength(String path, {bool withDrive = false}) {
4862
if (path.isEmpty) return 0;
49-
if (isSeparator(path.codeUnitAt(0))) return 1;
50-
51-
for (var i = 0; i < path.length; i++) {
52-
final codeUnit = path.codeUnitAt(i);
53-
if (isSeparator(codeUnit)) return 0;
54-
if (codeUnit == chars.colon) {
55-
if (i == 0) return 0;
56-
57-
// The root part is up until the next '/', or the full path. Skip ':'
58-
// (and '//' if it exists) and search for '/' after that.
59-
if (path.startsWith('//', i + 1)) i += 3;
60-
final index = path.indexOf('/', i);
61-
if (index <= 0) return path.length;
62-
63-
// file: URLs sometimes consider Windows drive letters part of the root.
64-
// See https://url.spec.whatwg.org/#file-slash-state.
65-
if (!withDrive || path.length < index + 3) return index;
66-
if (!path.startsWith('file://')) return index;
67-
return driveLetterEnd(path, index + 1) ?? index;
63+
if (withDrive && _startsWithFileColon(path)) {
64+
return _rootAuthorityLength(path, 5, withDrive: true);
65+
}
66+
final firstChar = path.codeUnitAt(0);
67+
if (chars.isLetter(firstChar)) {
68+
// Check if starting with scheme or drive letter.
69+
for (var i = 1; i < path.length; i++) {
70+
final codeUnit = path.codeUnitAt(i);
71+
if (chars.isLetter(codeUnit) ||
72+
chars.isDigit(codeUnit) ||
73+
codeUnit == chars.plus ||
74+
codeUnit == chars.minus ||
75+
codeUnit == chars.period) {
76+
continue;
77+
}
78+
if (codeUnit == chars.colon) {
79+
return _rootAuthorityLength(path, i + 1, withDrive: false);
80+
}
81+
break;
6882
}
83+
return 0;
6984
}
85+
return _rootAuthorityLength(path, 0, withDrive: false);
86+
}
7087

71-
return 0;
88+
/// Checks for authority part at start or after scheme.
89+
///
90+
/// If found, includes this in the root length.
91+
///
92+
/// Includes an authority starting at `//` until the next `/`, `?` or `#`,
93+
/// or the end of the path.
94+
int _rootAuthorityLength(String path, int index, {required bool withDrive}) {
95+
if (path.startsWith('//', index)) {
96+
index += 2;
97+
while (true) {
98+
if (index == path.length) return index;
99+
final codeUnit = path.codeUnitAt(index);
100+
if (codeUnit == chars.question || codeUnit == chars.hash) return index;
101+
index++;
102+
if (isSeparator(codeUnit)) break;
103+
}
104+
}
105+
if (withDrive) return _withDrive(path, index);
106+
return index;
107+
}
108+
109+
/// Checks for `[a-z]:/`, or `[a-z]:` when followed by `?` or `#` or nothing.
110+
///
111+
/// If found, includes this in the root length.
112+
int _withDrive(String path, int index) {
113+
final afterDrive = index + 2;
114+
if (path.length < afterDrive ||
115+
!chars.isLetter(path.codeUnitAt(index)) ||
116+
path.codeUnitAt(index + 1) != chars.colon) {
117+
return index;
118+
}
119+
if (path.length == afterDrive) return afterDrive;
120+
final nextChar = path.codeUnitAt(afterDrive);
121+
if (nextChar == chars.slash) {
122+
// Include following slash in root.
123+
return afterDrive + 1;
124+
}
125+
if (nextChar == chars.question || nextChar == chars.hash) {
126+
return afterDrive;
127+
}
128+
return index;
72129
}
73130

74131
@override

pkgs/path/test/url_test.dart

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,19 @@ void main() {
167167
expect(context.isRelative(r'package:foo/bar.dart'), false);
168168
expect(context.isRelative('foo/bar:baz/qux'), true);
169169
expect(context.isRelative(r'\\a'), true);
170+
expect(context.isRelative('/c:/a'), false);
171+
expect(context.isRelative('file:///c:/a'), false);
172+
expect(context.isRelative('/c:/'), false);
173+
expect(context.isRelative('file:///c:/'), false);
174+
expect(context.isRelative('a2:a'), false);
175+
expect(context.isRelative('a+:a'), false);
176+
expect(context.isRelative('a-:a'), false);
177+
expect(context.isRelative('a.:a'), false);
178+
expect(context.isRelative('2:a'), true);
179+
expect(context.isRelative('+:a'), true);
180+
expect(context.isRelative('-:a'), true);
181+
expect(context.isRelative('.:a'), true);
182+
expect(context.isRelative(':a/'), true);
170183
});
171184

172185
test('isRootRelative', () {
@@ -192,6 +205,11 @@ void main() {
192205
expect(context.isRootRelative(r'package:foo/bar.dart'), false);
193206
expect(context.isRootRelative('foo/bar:baz/qux'), false);
194207
expect(context.isRootRelative(r'\\a'), false);
208+
expect(context.isRootRelative('/c:/a'), true);
209+
expect(context.isRootRelative('file:///c:/a'), false);
210+
expect(context.isRootRelative('/c:/'), true);
211+
expect(context.isRootRelative('file:///c:/'), false);
212+
expect(context.isRootRelative('//c:/'), false);
195213
});
196214

197215
group('join', () {
@@ -232,6 +250,10 @@ void main() {
232250
context.join('a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k',
233251
'l', 'm', 'n', 'o', 'p'),
234252
'a/b/c/d/e/f/g/h/i/j/k/l/m/n/o/p');
253+
254+
for (final absolute in ['a:/', '/a', '//a']) {
255+
expect(context.join('a', absolute), absolute);
256+
}
235257
});
236258

237259
test('does not add separator if a part ends in one', () {

0 commit comments

Comments
 (0)