Line data Source code
1 : // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file
2 : // for details. All rights reserved. Use of this source code is governed by a
3 : // BSD-style license that can be found in the LICENSE file.
4 :
5 : import 'dart:math' as math;
6 :
7 : import '../path.dart' as p;
8 : import 'characters.dart' as chars;
9 : import 'internal_style.dart';
10 : import 'parsed_path.dart';
11 : import 'path_exception.dart';
12 : import 'style.dart';
13 :
14 22 : Context createInternal() => Context._internal();
15 :
16 : /// An instantiable class for manipulating paths. Unlike the top-level
17 : /// functions, this lets you explicitly select what platform the paths will use.
18 : class Context {
19 : /// Creates a new path context for the given style and current directory.
20 : ///
21 : /// If [style] is omitted, it uses the host operating system's path style. If
22 : /// only [current] is omitted, it defaults ".". If *both* [style] and
23 : /// [current] are omitted, [current] defaults to the real current working
24 : /// directory.
25 : ///
26 : /// On the browser, [style] defaults to [Style.url] and [current] defaults to
27 : /// the current URL.
28 0 : factory Context({Style? style, String? current}) {
29 : if (current == null) {
30 : if (style == null) {
31 0 : current = p.current;
32 : } else {
33 : current = '.';
34 : }
35 : }
36 :
37 : if (style == null) {
38 0 : style = Style.platform;
39 0 : } else if (style is! InternalStyle) {
40 0 : throw ArgumentError('Only styles defined by the path package are '
41 : 'allowed.');
42 : }
43 :
44 0 : return Context._(style as InternalStyle, current);
45 : }
46 :
47 : /// Create a [Context] to be used internally within path.
48 11 : Context._internal()
49 11 : : style = Style.platform as InternalStyle,
50 : _current = null;
51 :
52 0 : Context._(this.style, this._current);
53 :
54 : /// The style of path that this context works with.
55 : final InternalStyle style;
56 :
57 : /// The current directory given when Context was created. If null, current
58 : /// directory is evaluated from 'p.current'.
59 : final String? _current;
60 :
61 : /// The current directory that relative paths are relative to.
62 33 : String get current => _current ?? p.current;
63 :
64 : /// Gets the path separator for the context's [style]. On Mac and Linux,
65 : /// this is `/`. On Windows, it's `\`.
66 0 : String get separator => style.separator;
67 :
68 : /// Returns a new path with the given path parts appended to [current].
69 : ///
70 : /// Equivalent to [join()] with [current] as the first argument. Example:
71 : ///
72 : /// var context = Context(current: '/root');
73 : /// context.absolute('path', 'to', 'foo'); // -> '/root/path/to/foo'
74 : ///
75 : /// If [current] isn't absolute, this won't return an absolute path. Does not
76 : /// [normalize] or [canonicalize] paths.
77 0 : String absolute(String part1,
78 : [String? part2,
79 : String? part3,
80 : String? part4,
81 : String? part5,
82 : String? part6,
83 : String? part7]) {
84 0 : _validateArgList(
85 0 : 'absolute', [part1, part2, part3, part4, part5, part6, part7]);
86 :
87 : // If there's a single absolute path, just return it. This is a lot faster
88 : // for the common case of `p.absolute(path)`.
89 0 : if (part2 == null && isAbsolute(part1) && !isRootRelative(part1)) {
90 : return part1;
91 : }
92 :
93 0 : return join(current, part1, part2, part3, part4, part5, part6, part7);
94 : }
95 :
96 : /// Gets the part of [path] after the last separator on the context's
97 : /// platform.
98 : ///
99 : /// context.basename('path/to/foo.dart'); // -> 'foo.dart'
100 : /// context.basename('path/to'); // -> 'to'
101 : ///
102 : /// Trailing separators are ignored.
103 : ///
104 : /// context.basename('path/to/'); // -> 'to'
105 0 : String basename(String path) => _parse(path).basename;
106 :
107 : /// Gets the part of [path] after the last separator on the context's
108 : /// platform, and without any trailing file extension.
109 : ///
110 : /// context.basenameWithoutExtension('path/to/foo.dart'); // -> 'foo'
111 : ///
112 : /// Trailing separators are ignored.
113 : ///
114 : /// context.basenameWithoutExtension('path/to/foo.dart/'); // -> 'foo'
115 0 : String basenameWithoutExtension(String path) =>
116 0 : _parse(path).basenameWithoutExtension;
117 :
118 : /// Gets the part of [path] before the last separator.
119 : ///
120 : /// context.dirname('path/to/foo.dart'); // -> 'path/to'
121 : /// context.dirname('path/to'); // -> 'path'
122 : ///
123 : /// Trailing separators are ignored.
124 : ///
125 : /// context.dirname('path/to/'); // -> 'path'
126 0 : String dirname(String path) {
127 0 : final parsed = _parse(path);
128 0 : parsed.removeTrailingSeparators();
129 0 : if (parsed.parts.isEmpty) return parsed.root ?? '.';
130 0 : if (parsed.parts.length == 1) return parsed.root ?? '.';
131 0 : parsed.parts.removeLast();
132 0 : parsed.separators.removeLast();
133 0 : parsed.removeTrailingSeparators();
134 0 : return parsed.toString();
135 : }
136 :
137 : /// Gets the file extension of [path]: the portion of [basename] from the last
138 : /// `.` to the end (including the `.` itself).
139 : ///
140 : /// context.extension('path/to/foo.dart'); // -> '.dart'
141 : /// context.extension('path/to/foo'); // -> ''
142 : /// context.extension('path.to/foo'); // -> ''
143 : /// context.extension('path/to/foo.dart.js'); // -> '.js'
144 : ///
145 : /// If the file name starts with a `.`, then it is not considered an
146 : /// extension:
147 : ///
148 : /// context.extension('~/.bashrc'); // -> ''
149 : /// context.extension('~/.notes.txt'); // -> '.txt'
150 : ///
151 : /// Takes an optional parameter `level` which makes possible to return
152 : /// multiple extensions having `level` number of dots. If `level` exceeds the
153 : /// number of dots, the full extension is returned. The value of `level` must
154 : /// be greater than 0, else `RangeError` is thrown.
155 : ///
156 : /// context.extension('foo.bar.dart.js', 2); // -> '.dart.js
157 : /// context.extension('foo.bar.dart.js', 3); // -> '.bar.dart.js'
158 : /// context.extension('foo.bar.dart.js', 10); // -> '.bar.dart.js'
159 : /// context.extension('path/to/foo.bar.dart.js', 2); // -> '.dart.js'
160 0 : String extension(String path, [int level = 1]) =>
161 0 : _parse(path).extension(level);
162 :
163 : /// Returns the root of [path] if it's absolute, or an empty string if it's
164 : /// relative.
165 : ///
166 : /// // Unix
167 : /// context.rootPrefix('path/to/foo'); // -> ''
168 : /// context.rootPrefix('/path/to/foo'); // -> '/'
169 : ///
170 : /// // Windows
171 : /// context.rootPrefix(r'path\to\foo'); // -> ''
172 : /// context.rootPrefix(r'C:\path\to\foo'); // -> r'C:\'
173 : /// context.rootPrefix(r'\\server\share\a\b'); // -> r'\\server\share'
174 : ///
175 : /// // URL
176 : /// context.rootPrefix('path/to/foo'); // -> ''
177 : /// context.rootPrefix('https://dart.dev/path/to/foo');
178 : /// // -> 'https://dart.dev'
179 0 : String rootPrefix(String path) => path.substring(0, style.rootLength(path));
180 :
181 : /// Returns `true` if [path] is an absolute path and `false` if it is a
182 : /// relative path.
183 : ///
184 : /// On POSIX systems, absolute paths start with a `/` (forward slash). On
185 : /// Windows, an absolute path starts with `\\`, or a drive letter followed by
186 : /// `:/` or `:\`. For URLs, absolute paths either start with a protocol and
187 : /// optional hostname (e.g. `https://dart.dev`, `file://`) or with a `/`.
188 : ///
189 : /// URLs that start with `/` are known as "root-relative", since they're
190 : /// relative to the root of the current URL. Since root-relative paths are
191 : /// still absolute in every other sense, [isAbsolute] will return true for
192 : /// them. They can be detected using [isRootRelative].
193 44 : bool isAbsolute(String path) => style.rootLength(path) > 0;
194 :
195 : /// Returns `true` if [path] is a relative path and `false` if it is absolute.
196 : /// On POSIX systems, absolute paths start with a `/` (forward slash). On
197 : /// Windows, an absolute path starts with `\\`, or a drive letter followed by
198 : /// `:/` or `:\`.
199 22 : bool isRelative(String path) => !isAbsolute(path);
200 :
201 : /// Returns `true` if [path] is a root-relative path and `false` if it's not.
202 : ///
203 : /// URLs that start with `/` are known as "root-relative", since they're
204 : /// relative to the root of the current URL. Since root-relative paths are
205 : /// still absolute in every other sense, [isAbsolute] will return true for
206 : /// them. They can be detected using [isRootRelative].
207 : ///
208 : /// No POSIX and Windows paths are root-relative.
209 33 : bool isRootRelative(String path) => style.isRootRelative(path);
210 :
211 : /// Joins the given path parts into a single path. Example:
212 : ///
213 : /// context.join('path', 'to', 'foo'); // -> 'path/to/foo'
214 : ///
215 : /// If any part ends in a path separator, then a redundant separator will not
216 : /// be added:
217 : ///
218 : /// context.join('path/', 'to', 'foo'); // -> 'path/to/foo
219 : ///
220 : /// If a part is an absolute path, then anything before that will be ignored:
221 : ///
222 : /// context.join('path', '/to', 'foo'); // -> '/to/foo'
223 : ///
224 0 : String join(String part1,
225 : [String? part2,
226 : String? part3,
227 : String? part4,
228 : String? part5,
229 : String? part6,
230 : String? part7,
231 : String? part8]) {
232 0 : final parts = <String?>[
233 : part1,
234 : part2,
235 : part3,
236 : part4,
237 : part5,
238 : part6,
239 : part7,
240 : part8
241 : ];
242 0 : _validateArgList('join', parts);
243 0 : return joinAll(parts.whereType<String>());
244 : }
245 :
246 : /// Joins the given path parts into a single path. Example:
247 : ///
248 : /// context.joinAll(['path', 'to', 'foo']); // -> 'path/to/foo'
249 : ///
250 : /// If any part ends in a path separator, then a redundant separator will not
251 : /// be added:
252 : ///
253 : /// context.joinAll(['path/', 'to', 'foo']); // -> 'path/to/foo
254 : ///
255 : /// If a part is an absolute path, then anything before that will be ignored:
256 : ///
257 : /// context.joinAll(['path', '/to', 'foo']); // -> '/to/foo'
258 : ///
259 : /// For a fixed number of parts, [join] is usually terser.
260 0 : String joinAll(Iterable<String> parts) {
261 0 : final buffer = StringBuffer();
262 : var needsSeparator = false;
263 : var isAbsoluteAndNotRootRelative = false;
264 :
265 0 : for (var part in parts.where((part) => part != '')) {
266 0 : if (isRootRelative(part) && isAbsoluteAndNotRootRelative) {
267 : // If the new part is root-relative, it preserves the previous root but
268 : // replaces the path after it.
269 0 : final parsed = _parse(part);
270 0 : final path = buffer.toString();
271 0 : parsed.root =
272 0 : path.substring(0, style.rootLength(path, withDrive: true));
273 0 : if (style.needsSeparator(parsed.root!)) {
274 0 : parsed.separators[0] = style.separator;
275 : }
276 0 : buffer.clear();
277 0 : buffer.write(parsed.toString());
278 0 : } else if (isAbsolute(part)) {
279 0 : isAbsoluteAndNotRootRelative = !isRootRelative(part);
280 : // An absolute path discards everything before it.
281 0 : buffer.clear();
282 0 : buffer.write(part);
283 : } else {
284 0 : if (part.isNotEmpty && style.containsSeparator(part[0])) {
285 : // The part starts with a separator, so we don't need to add one.
286 : } else if (needsSeparator) {
287 0 : buffer.write(separator);
288 : }
289 :
290 0 : buffer.write(part);
291 : }
292 :
293 : // Unless this part ends with a separator, we'll need to add one before
294 : // the next part.
295 0 : needsSeparator = style.needsSeparator(part);
296 : }
297 :
298 0 : return buffer.toString();
299 : }
300 :
301 : /// Splits [path] into its components using the current platform's
302 : /// [separator]. Example:
303 : ///
304 : /// context.split('path/to/foo'); // -> ['path', 'to', 'foo']
305 : ///
306 : /// The path will *not* be normalized before splitting.
307 : ///
308 : /// context.split('path/../foo'); // -> ['path', '..', 'foo']
309 : ///
310 : /// If [path] is absolute, the root directory will be the first element in the
311 : /// array. Example:
312 : ///
313 : /// // Unix
314 : /// context.split('/path/to/foo'); // -> ['/', 'path', 'to', 'foo']
315 : ///
316 : /// // Windows
317 : /// context.split(r'C:\path\to\foo'); // -> [r'C:\', 'path', 'to', 'foo']
318 : /// context.split(r'\\server\share\path\to\foo');
319 : /// // -> [r'\\server\share', 'foo', 'bar', 'baz']
320 : ///
321 : /// // Browser
322 : /// context.split('https://dart.dev/path/to/foo');
323 : /// // -> ['https://dart.dev', 'path', 'to', 'foo']
324 11 : List<String> split(String path) {
325 11 : final parsed = _parse(path);
326 : // Filter out empty parts that exist due to multiple separators in a row.
327 66 : parsed.parts = parsed.parts.where((part) => part.isNotEmpty).toList();
328 44 : if (parsed.root != null) parsed.parts.insert(0, parsed.root!);
329 11 : return parsed.parts;
330 : }
331 :
332 : /// Canonicalizes [path].
333 : ///
334 : /// This is guaranteed to return the same path for two different input paths
335 : /// if and only if both input paths point to the same location. Unlike
336 : /// [normalize], it returns absolute paths when possible and canonicalizes
337 : /// ASCII case on Windows.
338 : ///
339 : /// Note that this does not resolve symlinks.
340 : ///
341 : /// If you want a map that uses path keys, it's probably more efficient to use
342 : /// a Map with [equals] and [hash] specified as the callbacks to use for keys
343 : /// than it is to canonicalize every key.
344 0 : String canonicalize(String path) {
345 0 : path = absolute(path);
346 0 : if (style != Style.windows && !_needsNormalization(path)) return path;
347 :
348 0 : final parsed = _parse(path);
349 0 : parsed.normalize(canonicalize: true);
350 0 : return parsed.toString();
351 : }
352 :
353 : /// Normalizes [path], simplifying it by handling `..`, and `.`, and
354 : /// removing redundant path separators whenever possible.
355 : ///
356 : /// Note that this is *not* guaranteed to return the same result for two
357 : /// equivalent input paths. For that, see [canonicalize]. Or, if you're using
358 : /// paths as map keys use [equals] and [hash] as the key callbacks.
359 : ///
360 : /// context.normalize('path/./to/..//file.text'); // -> 'path/file.txt'
361 11 : String normalize(String path) {
362 11 : if (!_needsNormalization(path)) return path;
363 :
364 0 : final parsed = _parse(path);
365 0 : parsed.normalize();
366 0 : return parsed.toString();
367 : }
368 :
369 : /// Returns whether [path] needs to be normalized.
370 11 : bool _needsNormalization(String path) {
371 : var start = 0;
372 11 : final codeUnits = path.codeUnits;
373 : int? previousPrevious;
374 : int? previous;
375 :
376 : // Skip past the root before we start looking for snippets that need
377 : // normalization. We want to normalize "//", but not when it's part of
378 : // "http://".
379 22 : final root = style.rootLength(path);
380 11 : if (root != 0) {
381 : start = root;
382 : previous = chars.slash;
383 :
384 : // On Windows, the root still needs to be normalized if it contains a
385 : // forward slash.
386 33 : if (style == Style.windows) {
387 0 : for (var i = 0; i < root; i++) {
388 0 : if (codeUnits[i] == chars.slash) return true;
389 : }
390 : }
391 : }
392 :
393 33 : for (var i = start; i < codeUnits.length; i++) {
394 11 : final codeUnit = codeUnits[i];
395 22 : if (style.isSeparator(codeUnit)) {
396 : // Forward slashes in Windows paths are normalized to backslashes.
397 33 : if (style == Style.windows && codeUnit == chars.slash) return true;
398 :
399 : // Multiple separators are normalized to single separators.
400 22 : if (previous != null && style.isSeparator(previous)) return true;
401 :
402 : // Single dots and double dots are normalized to directory traversals.
403 : //
404 : // This can return false positives for ".../", but that's unlikely
405 : // enough that it's probably not going to cause performance issues.
406 11 : if (previous == chars.period &&
407 : (previousPrevious == null ||
408 0 : previousPrevious == chars.period ||
409 0 : style.isSeparator(previousPrevious))) {
410 : return true;
411 : }
412 : }
413 :
414 : previousPrevious = previous;
415 : previous = codeUnit;
416 : }
417 :
418 : // Empty paths are normalized to ".".
419 : if (previous == null) return true;
420 :
421 : // Trailing separators are removed.
422 22 : if (style.isSeparator(previous)) return true;
423 :
424 : // Single dots and double dots are normalized to directory traversals.
425 11 : if (previous == chars.period &&
426 : (previousPrevious == null ||
427 0 : style.isSeparator(previousPrevious) ||
428 0 : previousPrevious == chars.period)) {
429 : return true;
430 : }
431 :
432 : return false;
433 : }
434 :
435 : /// Attempts to convert [path] to an equivalent relative path relative to
436 : /// [current].
437 : ///
438 : /// var context = Context(current: '/root/path');
439 : /// context.relative('/root/path/a/b.dart'); // -> 'a/b.dart'
440 : /// context.relative('/root/other.dart'); // -> '../other.dart'
441 : ///
442 : /// If the [from] argument is passed, [path] is made relative to that instead.
443 : ///
444 : /// context.relative('/root/path/a/b.dart',
445 : /// from: '/root/path'); // -> 'a/b.dart'
446 : /// context.relative('/root/other.dart',
447 : /// from: '/root/path'); // -> '../other.dart'
448 : ///
449 : /// If [path] and/or [from] are relative paths, they are assumed to be
450 : /// relative to [current].
451 : ///
452 : /// Since there is no relative path from one drive letter to another on
453 : /// Windows, this will return an absolute path in that case.
454 : ///
455 : /// context.relative(r'D:\other', from: r'C:\other'); // -> 'D:\other'
456 : ///
457 : /// This will also return an absolute path if an absolute [path] is passed to
458 : /// a context with a relative path for [current].
459 : ///
460 : /// var context = Context(r'some/relative/path');
461 : /// context.relative(r'/absolute/path'); // -> '/absolute/path'
462 : ///
463 : /// If [current] is relative, it may be impossible to determine a path from
464 : /// [from] to [path]. For example, if [current] and [path] are "." and [from]
465 : /// is "/", no path can be determined. In this case, a [PathException] will be
466 : /// thrown.
467 11 : String relative(String path, {String? from}) {
468 : // Avoid expensive computation if the path is already relative.
469 11 : if (from == null && isRelative(path)) return normalize(path);
470 :
471 11 : from = from == null ? current : absolute(from);
472 :
473 : // We can't determine the path from a relative path to an absolute path.
474 11 : if (isRelative(from) && isAbsolute(path)) {
475 0 : return normalize(path);
476 : }
477 :
478 : // If the given path is relative, resolve it relative to the context's
479 : // current directory.
480 22 : if (isRelative(path) || isRootRelative(path)) {
481 0 : path = absolute(path);
482 : }
483 :
484 : // If the path is still relative and `from` is absolute, we're unable to
485 : // find a path from `from` to `path`.
486 11 : if (isRelative(path) && isAbsolute(from)) {
487 0 : throw PathException('Unable to find a path to "$path" from "$from".');
488 : }
489 :
490 22 : final fromParsed = _parse(from)..normalize();
491 22 : final pathParsed = _parse(path)..normalize();
492 :
493 55 : if (fromParsed.parts.isNotEmpty && fromParsed.parts[0] == '.') {
494 0 : return pathParsed.toString();
495 : }
496 :
497 : // If the root prefixes don't match (for example, different drive letters
498 : // on Windows), then there is no relative path, so just return the absolute
499 : // one. In Windows, drive letters are case-insenstive and we allow
500 : // calculation of relative paths, even if a path has not been normalized.
501 33 : if (fromParsed.root != pathParsed.root &&
502 0 : ((fromParsed.root == null || pathParsed.root == null) ||
503 0 : !style.pathsEqual(fromParsed.root!, pathParsed.root!))) {
504 0 : return pathParsed.toString();
505 : }
506 :
507 : // Strip off their common prefix.
508 22 : while (fromParsed.parts.isNotEmpty &&
509 22 : pathParsed.parts.isNotEmpty &&
510 66 : style.pathsEqual(fromParsed.parts[0], pathParsed.parts[0])) {
511 22 : fromParsed.parts.removeAt(0);
512 22 : fromParsed.separators.removeAt(1);
513 22 : pathParsed.parts.removeAt(0);
514 22 : pathParsed.separators.removeAt(1);
515 : }
516 :
517 : // If there are any directories left in the from path, we need to walk up
518 : // out of them. If a directory left in the from path is '..', it cannot
519 : // be cancelled by adding a '..'.
520 22 : if (fromParsed.parts.isNotEmpty && fromParsed.parts[0] == '..') {
521 0 : throw PathException('Unable to find a path to "$path" from "$from".');
522 : }
523 55 : pathParsed.parts.insertAll(0, List.filled(fromParsed.parts.length, '..'));
524 22 : pathParsed.separators[0] = '';
525 11 : pathParsed.separators
526 66 : .insertAll(1, List.filled(fromParsed.parts.length, style.separator));
527 :
528 : // Corner case: the paths completely collapsed.
529 22 : if (pathParsed.parts.isEmpty) return '.';
530 :
531 : // Corner case: path was '.' and some '..' directories were added in front.
532 : // Don't add a final '/.' in that case.
533 66 : if (pathParsed.parts.length > 1 && pathParsed.parts.last == '.') {
534 0 : pathParsed.parts.removeLast();
535 0 : pathParsed.separators
536 0 : ..removeLast()
537 0 : ..removeLast()
538 0 : ..add('');
539 : }
540 :
541 : // Make it relative.
542 11 : pathParsed.root = '';
543 11 : pathParsed.removeTrailingSeparators();
544 :
545 11 : return pathParsed.toString();
546 : }
547 :
548 : /// Returns `true` if [child] is a path beneath `parent`, and `false`
549 : /// otherwise.
550 : ///
551 : /// path.isWithin('/root/path', '/root/path/a'); // -> true
552 : /// path.isWithin('/root/path', '/root/other'); // -> false
553 : /// path.isWithin('/root/path', '/root/path'); // -> false
554 0 : bool isWithin(String parent, String child) =>
555 0 : _isWithinOrEquals(parent, child) == _PathRelation.within;
556 :
557 : /// Returns `true` if [path1] points to the same location as [path2], and
558 : /// `false` otherwise.
559 : ///
560 : /// The [hash] function returns a hash code that matches these equality
561 : /// semantics.
562 0 : bool equals(String path1, String path2) =>
563 0 : _isWithinOrEquals(path1, path2) == _PathRelation.equal;
564 :
565 : /// Compares two paths and returns an enum value indicating their relationship
566 : /// to one another.
567 : ///
568 : /// This never returns [_PathRelation.inconclusive].
569 0 : _PathRelation _isWithinOrEquals(String parent, String child) {
570 : // Make both paths the same level of relative. We're only able to do the
571 : // quick comparison if both paths are in the same format, and making a path
572 : // absolute is faster than making it relative.
573 0 : final parentIsAbsolute = isAbsolute(parent);
574 0 : final childIsAbsolute = isAbsolute(child);
575 : if (parentIsAbsolute && !childIsAbsolute) {
576 0 : child = absolute(child);
577 0 : if (style.isRootRelative(parent)) parent = absolute(parent);
578 : } else if (childIsAbsolute && !parentIsAbsolute) {
579 0 : parent = absolute(parent);
580 0 : if (style.isRootRelative(child)) child = absolute(child);
581 : } else if (childIsAbsolute && parentIsAbsolute) {
582 0 : final childIsRootRelative = style.isRootRelative(child);
583 0 : final parentIsRootRelative = style.isRootRelative(parent);
584 :
585 : if (childIsRootRelative && !parentIsRootRelative) {
586 0 : child = absolute(child);
587 : } else if (parentIsRootRelative && !childIsRootRelative) {
588 0 : parent = absolute(parent);
589 : }
590 : }
591 :
592 0 : final result = _isWithinOrEqualsFast(parent, child);
593 0 : if (result != _PathRelation.inconclusive) return result;
594 :
595 : String relative;
596 : try {
597 0 : relative = this.relative(child, from: parent);
598 0 : } on PathException catch (_) {
599 : // If no relative path from [parent] to [child] is found, [child]
600 : // definitely isn't a child of [parent].
601 : return _PathRelation.different;
602 : }
603 :
604 0 : if (!isRelative(relative)) return _PathRelation.different;
605 0 : if (relative == '.') return _PathRelation.equal;
606 0 : if (relative == '..') return _PathRelation.different;
607 0 : return (relative.length >= 3 &&
608 0 : relative.startsWith('..') &&
609 0 : style.isSeparator(relative.codeUnitAt(2)))
610 : ? _PathRelation.different
611 : : _PathRelation.within;
612 : }
613 :
614 : /// An optimized implementation of [_isWithinOrEquals] that doesn't handle a
615 : /// few complex cases.
616 0 : _PathRelation _isWithinOrEqualsFast(String parent, String child) {
617 : // Normally we just bail when we see "." path components, but we can handle
618 : // a single dot easily enough.
619 0 : if (parent == '.') parent = '';
620 :
621 0 : final parentRootLength = style.rootLength(parent);
622 0 : final childRootLength = style.rootLength(child);
623 :
624 : // If the roots aren't the same length, we know both paths are absolute or
625 : // both are root-relative, and thus that the roots are meaningfully
626 : // different.
627 : //
628 : // isWithin("C:/bar", "//foo/bar/baz") //=> false
629 : // isWithin("http://example.com/", "http://google.com/bar") //=> false
630 0 : if (parentRootLength != childRootLength) return _PathRelation.different;
631 :
632 : // Make sure that the roots are textually the same as well.
633 : //
634 : // isWithin("C:/bar", "D:/bar/baz") //=> false
635 : // isWithin("http://example.com/", "http://example.org/bar") //=> false
636 0 : for (var i = 0; i < parentRootLength; i++) {
637 0 : final parentCodeUnit = parent.codeUnitAt(i);
638 0 : final childCodeUnit = child.codeUnitAt(i);
639 0 : if (!style.codeUnitsEqual(parentCodeUnit, childCodeUnit)) {
640 : return _PathRelation.different;
641 : }
642 : }
643 :
644 : // Start by considering the last code unit as a separator, since
645 : // semantically we're starting at a new path component even if we're
646 : // comparing relative paths.
647 : var lastCodeUnit = chars.slash;
648 :
649 : /// The index of the last separator in [parent].
650 : int? lastParentSeparator;
651 :
652 : // Iterate through both paths as long as they're semantically identical.
653 : var parentIndex = parentRootLength;
654 : var childIndex = childRootLength;
655 0 : while (parentIndex < parent.length && childIndex < child.length) {
656 0 : var parentCodeUnit = parent.codeUnitAt(parentIndex);
657 0 : var childCodeUnit = child.codeUnitAt(childIndex);
658 0 : if (style.codeUnitsEqual(parentCodeUnit, childCodeUnit)) {
659 0 : if (style.isSeparator(parentCodeUnit)) {
660 : lastParentSeparator = parentIndex;
661 : }
662 :
663 : lastCodeUnit = parentCodeUnit;
664 0 : parentIndex++;
665 0 : childIndex++;
666 : continue;
667 : }
668 :
669 : // Ignore multiple separators in a row.
670 0 : if (style.isSeparator(parentCodeUnit) &&
671 0 : style.isSeparator(lastCodeUnit)) {
672 : lastParentSeparator = parentIndex;
673 0 : parentIndex++;
674 : continue;
675 0 : } else if (style.isSeparator(childCodeUnit) &&
676 0 : style.isSeparator(lastCodeUnit)) {
677 0 : childIndex++;
678 : continue;
679 : }
680 :
681 : // If a dot comes after a separator, it may be a directory traversal
682 : // operator. To check that, we need to know if it's followed by either
683 : // "/" or "./". Otherwise, it's just a normal non-matching character.
684 : //
685 : // isWithin("foo/./bar", "foo/bar/baz") //=> true
686 : // isWithin("foo/bar/../baz", "foo/bar/.foo") //=> false
687 0 : if (parentCodeUnit == chars.period && style.isSeparator(lastCodeUnit)) {
688 0 : parentIndex++;
689 :
690 : // We've hit "/." at the end of the parent path, which we can ignore,
691 : // since the paths were equivalent up to this point.
692 0 : if (parentIndex == parent.length) break;
693 0 : parentCodeUnit = parent.codeUnitAt(parentIndex);
694 :
695 : // We've hit "/./", which we can ignore.
696 0 : if (style.isSeparator(parentCodeUnit)) {
697 : lastParentSeparator = parentIndex;
698 0 : parentIndex++;
699 : continue;
700 : }
701 :
702 : // We've hit "/..", which may be a directory traversal operator that
703 : // we can't handle on the fast track.
704 0 : if (parentCodeUnit == chars.period) {
705 0 : parentIndex++;
706 0 : if (parentIndex == parent.length ||
707 0 : style.isSeparator(parent.codeUnitAt(parentIndex))) {
708 : return _PathRelation.inconclusive;
709 : }
710 : }
711 :
712 : // If this isn't a directory traversal, fall through so we hit the
713 : // normal handling for mismatched paths.
714 : }
715 :
716 : // This is the same logic as above, but for the child path instead of the
717 : // parent.
718 0 : if (childCodeUnit == chars.period && style.isSeparator(lastCodeUnit)) {
719 0 : childIndex++;
720 0 : if (childIndex == child.length) break;
721 0 : childCodeUnit = child.codeUnitAt(childIndex);
722 :
723 0 : if (style.isSeparator(childCodeUnit)) {
724 0 : childIndex++;
725 : continue;
726 : }
727 :
728 0 : if (childCodeUnit == chars.period) {
729 0 : childIndex++;
730 0 : if (childIndex == child.length ||
731 0 : style.isSeparator(child.codeUnitAt(childIndex))) {
732 : return _PathRelation.inconclusive;
733 : }
734 : }
735 : }
736 :
737 : // If we're here, we've hit two non-matching, non-significant characters.
738 : // As long as the remainders of the two paths don't have any unresolved
739 : // ".." components, we can be confident that [child] is not within
740 : // [parent].
741 0 : final childDirection = _pathDirection(child, childIndex);
742 0 : if (childDirection != _PathDirection.belowRoot) {
743 : return _PathRelation.inconclusive;
744 : }
745 :
746 0 : final parentDirection = _pathDirection(parent, parentIndex);
747 0 : if (parentDirection != _PathDirection.belowRoot) {
748 : return _PathRelation.inconclusive;
749 : }
750 :
751 : return _PathRelation.different;
752 : }
753 :
754 : // If the child is shorter than the parent, it's probably not within the
755 : // parent. The only exception is if the parent has some weird ".." stuff
756 : // going on, in which case we do the slow check.
757 : //
758 : // isWithin("foo/bar/baz", "foo/bar") //=> false
759 : // isWithin("foo/bar/baz/../..", "foo/bar") //=> true
760 0 : if (childIndex == child.length) {
761 0 : if (parentIndex == parent.length ||
762 0 : style.isSeparator(parent.codeUnitAt(parentIndex))) {
763 : lastParentSeparator = parentIndex;
764 : } else {
765 0 : lastParentSeparator ??= math.max(0, parentRootLength - 1);
766 : }
767 :
768 0 : final direction = _pathDirection(parent, lastParentSeparator);
769 0 : if (direction == _PathDirection.atRoot) return _PathRelation.equal;
770 0 : return direction == _PathDirection.aboveRoot
771 : ? _PathRelation.inconclusive
772 : : _PathRelation.different;
773 : }
774 :
775 : // We've reached the end of the parent path, which means it's time to make a
776 : // decision. Before we do, though, we'll check the rest of the child to see
777 : // what that tells us.
778 0 : final direction = _pathDirection(child, childIndex);
779 :
780 : // If there are no more components in the child, then it's the same as
781 : // the parent.
782 : //
783 : // isWithin("foo/bar", "foo/bar") //=> false
784 : // isWithin("foo/bar", "foo/bar//") //=> false
785 : // equals("foo/bar", "foo/bar") //=> true
786 : // equals("foo/bar", "foo/bar//") //=> true
787 0 : if (direction == _PathDirection.atRoot) return _PathRelation.equal;
788 :
789 : // If there are unresolved ".." components in the child, no decision we make
790 : // will be valid. We'll abort and do the slow check instead.
791 : //
792 : // isWithin("foo/bar", "foo/bar/..") //=> false
793 : // isWithin("foo/bar", "foo/bar/baz/bang/../../..") //=> false
794 : // isWithin("foo/bar", "foo/bar/baz/bang/../../../bar/baz") //=> true
795 0 : if (direction == _PathDirection.aboveRoot) {
796 : return _PathRelation.inconclusive;
797 : }
798 :
799 : // The child is within the parent if and only if we're on a separator
800 : // boundary.
801 : //
802 : // isWithin("foo/bar", "foo/bar/baz") //=> true
803 : // isWithin("foo/bar/", "foo/bar/baz") //=> true
804 : // isWithin("foo/bar", "foo/barbaz") //=> false
805 0 : return (style.isSeparator(child.codeUnitAt(childIndex)) ||
806 0 : style.isSeparator(lastCodeUnit))
807 : ? _PathRelation.within
808 : : _PathRelation.different;
809 : }
810 :
811 : // Returns a [_PathDirection] describing the path represented by [codeUnits]
812 : // starting at [index].
813 : //
814 : // This ignores leading separators.
815 : //
816 : // pathDirection("foo") //=> below root
817 : // pathDirection("foo/bar/../baz") //=> below root
818 : // pathDirection("//foo/bar/baz") //=> below root
819 : // pathDirection("/") //=> at root
820 : // pathDirection("foo/..") //=> at root
821 : // pathDirection("foo/../baz") //=> reaches root
822 : // pathDirection("foo/../..") //=> above root
823 : // pathDirection("foo/../../foo/bar/baz") //=> above root
824 0 : _PathDirection _pathDirection(String path, int index) {
825 : var depth = 0;
826 : var reachedRoot = false;
827 : var i = index;
828 0 : while (i < path.length) {
829 : // Ignore initial separators or doubled separators.
830 0 : while (i < path.length && style.isSeparator(path.codeUnitAt(i))) {
831 0 : i++;
832 : }
833 :
834 : // If we're at the end, stop.
835 0 : if (i == path.length) break;
836 :
837 : // Move through the path component to the next separator.
838 : final start = i;
839 0 : while (i < path.length && !style.isSeparator(path.codeUnitAt(i))) {
840 0 : i++;
841 : }
842 :
843 : // See if the path component is ".", "..", or a name.
844 0 : if (i - start == 1 && path.codeUnitAt(start) == chars.period) {
845 : // Don't change the depth.
846 0 : } else if (i - start == 2 &&
847 0 : path.codeUnitAt(start) == chars.period &&
848 0 : path.codeUnitAt(start + 1) == chars.period) {
849 : // ".." backs out a directory.
850 0 : depth--;
851 :
852 : // If we work back beyond the root, stop.
853 0 : if (depth < 0) break;
854 :
855 : // Record that we reached the root so we don't return
856 : // [_PathDirection.belowRoot].
857 0 : if (depth == 0) reachedRoot = true;
858 : } else {
859 : // Step inside a directory.
860 0 : depth++;
861 : }
862 :
863 : // If we're at the end, stop.
864 0 : if (i == path.length) break;
865 :
866 : // Move past the separator.
867 0 : i++;
868 : }
869 :
870 0 : if (depth < 0) return _PathDirection.aboveRoot;
871 0 : if (depth == 0) return _PathDirection.atRoot;
872 : if (reachedRoot) return _PathDirection.reachesRoot;
873 : return _PathDirection.belowRoot;
874 : }
875 :
876 : /// Returns a hash code for [path] that matches the semantics of [equals].
877 : ///
878 : /// Note that the same path may have different hash codes in different
879 : /// [Context]s.
880 0 : int hash(String path) {
881 : // Make [path] absolute to ensure that equivalent relative and absolute
882 : // paths have the same hash code.
883 0 : path = absolute(path);
884 :
885 0 : final result = _hashFast(path);
886 : if (result != null) return result;
887 :
888 0 : final parsed = _parse(path);
889 0 : parsed.normalize();
890 0 : return _hashFast(parsed.toString())!;
891 : }
892 :
893 : /// An optimized implementation of [hash] that doesn't handle internal `..`
894 : /// components.
895 : ///
896 : /// This will handle `..` components that appear at the beginning of the path.
897 0 : int? _hashFast(String path) {
898 : var hash = 4603;
899 : var beginning = true;
900 : var wasSeparator = true;
901 0 : for (var i = 0; i < path.length; i++) {
902 0 : final codeUnit = style.canonicalizeCodeUnit(path.codeUnitAt(i));
903 :
904 : // Take advantage of the fact that collisions are allowed to ignore
905 : // separators entirely. This lets us avoid worrying about cases like
906 : // multiple trailing slashes.
907 0 : if (style.isSeparator(codeUnit)) {
908 : wasSeparator = true;
909 : continue;
910 : }
911 :
912 0 : if (codeUnit == chars.period && wasSeparator) {
913 : // If a dot comes after a separator, it may be a directory traversal
914 : // operator. To check that, we need to know if it's followed by either
915 : // "/" or "./". Otherwise, it's just a normal character.
916 : //
917 : // hash("foo/./bar") == hash("foo/bar")
918 :
919 : // We've hit "/." at the end of the path, which we can ignore.
920 0 : if (i + 1 == path.length) break;
921 :
922 0 : final next = path.codeUnitAt(i + 1);
923 :
924 : // We can just ignore "/./", since they don't affect the semantics of
925 : // the path.
926 0 : if (style.isSeparator(next)) continue;
927 :
928 : // If the path ends with "/.." or contains "/../", we need to
929 : // canonicalize it before we can hash it. We make an exception for ".."s
930 : // at the beginning of the path, since those may appear even in a
931 : // canonicalized path.
932 : if (!beginning &&
933 0 : next == chars.period &&
934 0 : (i + 2 == path.length ||
935 0 : style.isSeparator(path.codeUnitAt(i + 2)))) {
936 : return null;
937 : }
938 : }
939 :
940 : // Make sure [hash] stays under 32 bits even after multiplication.
941 0 : hash &= 0x3FFFFFF;
942 0 : hash *= 33;
943 0 : hash ^= codeUnit;
944 : wasSeparator = false;
945 : beginning = false;
946 : }
947 : return hash;
948 : }
949 :
950 : /// Removes a trailing extension from the last part of [path].
951 : ///
952 : /// context.withoutExtension('path/to/foo.dart'); // -> 'path/to/foo'
953 0 : String withoutExtension(String path) {
954 0 : final parsed = _parse(path);
955 :
956 0 : for (var i = parsed.parts.length - 1; i >= 0; i--) {
957 0 : if (parsed.parts[i].isNotEmpty) {
958 0 : parsed.parts[i] = parsed.basenameWithoutExtension;
959 : break;
960 : }
961 : }
962 :
963 0 : return parsed.toString();
964 : }
965 :
966 : /// Returns [path] with the trailing extension set to [extension].
967 : ///
968 : /// If [path] doesn't have a trailing extension, this just adds [extension] to
969 : /// the end.
970 : ///
971 : /// context.setExtension('path/to/foo.dart', '.js')
972 : /// // -> 'path/to/foo.js'
973 : /// context.setExtension('path/to/foo.dart.js', '.map')
974 : /// // -> 'path/to/foo.dart.map'
975 : /// context.setExtension('path/to/foo', '.js')
976 : /// // -> 'path/to/foo.js'
977 0 : String setExtension(String path, String extension) =>
978 0 : withoutExtension(path) + extension;
979 :
980 : /// Returns the path represented by [uri], which may be a [String] or a [Uri].
981 : ///
982 : /// For POSIX and Windows styles, [uri] must be a `file:` URI. For the URL
983 : /// style, this will just convert [uri] to a string.
984 : ///
985 : /// // POSIX
986 : /// context.fromUri('file:///path/to/foo')
987 : /// // -> '/path/to/foo'
988 : ///
989 : /// // Windows
990 : /// context.fromUri('file:///C:/path/to/foo')
991 : /// // -> r'C:\path\to\foo'
992 : ///
993 : /// // URL
994 : /// context.fromUri('https://dart.dev/path/to/foo')
995 : /// // -> 'https://dart.dev/path/to/foo'
996 : ///
997 : /// If [uri] is relative, a relative path will be returned.
998 : ///
999 : /// path.fromUri('path/to/foo'); // -> 'path/to/foo'
1000 44 : String fromUri(uri) => style.pathFromUri(_parseUri(uri));
1001 :
1002 : /// Returns the URI that represents [path].
1003 : ///
1004 : /// For POSIX and Windows styles, this will return a `file:` URI. For the URL
1005 : /// style, this will just convert [path] to a [Uri].
1006 : ///
1007 : /// // POSIX
1008 : /// context.toUri('/path/to/foo')
1009 : /// // -> Uri.parse('file:///path/to/foo')
1010 : ///
1011 : /// // Windows
1012 : /// context.toUri(r'C:\path\to\foo')
1013 : /// // -> Uri.parse('file:///C:/path/to/foo')
1014 : ///
1015 : /// // URL
1016 : /// context.toUri('https://dart.dev/path/to/foo')
1017 : /// // -> Uri.parse('https://dart.dev/path/to/foo')
1018 0 : Uri toUri(String path) {
1019 0 : if (isRelative(path)) {
1020 0 : return style.relativePathToUri(path);
1021 : } else {
1022 0 : return style.absolutePathToUri(join(current, path));
1023 : }
1024 : }
1025 :
1026 : /// Returns a terse, human-readable representation of [uri].
1027 : ///
1028 : /// [uri] can be a [String] or a [Uri]. If it can be made relative to the
1029 : /// current working directory, that's done. Otherwise, it's returned as-is.
1030 : /// This gracefully handles non-`file:` URIs for [Style.posix] and
1031 : /// [Style.windows].
1032 : ///
1033 : /// The returned value is meant for human consumption, and may be either URI-
1034 : /// or path-formatted.
1035 : ///
1036 : /// // POSIX
1037 : /// var context = Context(current: '/root/path');
1038 : /// context.prettyUri('file:///root/path/a/b.dart'); // -> 'a/b.dart'
1039 : /// context.prettyUri('https://dart.dev/'); // -> 'https://dart.dev'
1040 : ///
1041 : /// // Windows
1042 : /// var context = Context(current: r'C:\root\path');
1043 : /// context.prettyUri('file:///C:/root/path/a/b.dart'); // -> r'a\b.dart'
1044 : /// context.prettyUri('https://dart.dev/'); // -> 'https://dart.dev'
1045 : ///
1046 : /// // URL
1047 : /// var context = Context(current: 'https://dart.dev/root/path');
1048 : /// context.prettyUri('https://dart.dev/root/path/a/b.dart');
1049 : /// // -> r'a/b.dart'
1050 : /// context.prettyUri('file:///root/path'); // -> 'file:///root/path'
1051 11 : String prettyUri(uri) {
1052 11 : final typedUri = _parseUri(uri);
1053 55 : if (typedUri.scheme == 'file' && style == Style.url) {
1054 0 : return typedUri.toString();
1055 22 : } else if (typedUri.scheme != 'file' &&
1056 22 : typedUri.scheme != '' &&
1057 33 : style != Style.url) {
1058 11 : return typedUri.toString();
1059 : }
1060 :
1061 22 : final path = normalize(fromUri(typedUri));
1062 11 : final rel = relative(path);
1063 :
1064 : // Only return a relative path if it's actually shorter than the absolute
1065 : // path. This avoids ugly things like long "../" chains to get to the root
1066 : // and then go back down.
1067 55 : return split(rel).length > split(path).length ? path : rel;
1068 : }
1069 :
1070 33 : ParsedPath _parse(String path) => ParsedPath.parse(path, style);
1071 : }
1072 :
1073 : /// Parses argument if it's a [String] or returns it intact if it's a [Uri].
1074 : ///
1075 : /// Throws an [ArgumentError] otherwise.
1076 11 : Uri _parseUri(uri) {
1077 11 : if (uri is String) return Uri.parse(uri);
1078 11 : if (uri is Uri) return uri;
1079 0 : throw ArgumentError.value(uri, 'uri', 'Value must be a String or a Uri');
1080 : }
1081 :
1082 : /// Validates that there are no non-null arguments following a null one and
1083 : /// throws an appropriate [ArgumentError] on failure.
1084 0 : void _validateArgList(String method, List<String?> args) {
1085 0 : for (var i = 1; i < args.length; i++) {
1086 : // Ignore nulls hanging off the end.
1087 0 : if (args[i] == null || args[i - 1] != null) continue;
1088 :
1089 : int numArgs;
1090 0 : for (numArgs = args.length; numArgs >= 1; numArgs--) {
1091 0 : if (args[numArgs - 1] != null) break;
1092 : }
1093 :
1094 : // Show the arguments.
1095 0 : final message = StringBuffer();
1096 0 : message.write('$method(');
1097 0 : message.write(args
1098 0 : .take(numArgs)
1099 0 : .map((arg) => arg == null ? 'null' : '"$arg"')
1100 0 : .join(', '));
1101 0 : message.write('): part ${i - 1} was null, but part $i was not.');
1102 0 : throw ArgumentError(message.toString());
1103 : }
1104 : }
1105 :
1106 : /// An enum of possible return values for [Context._pathDirection].
1107 : class _PathDirection {
1108 : /// The path contains enough ".." components that at some point it reaches
1109 : /// above its original root.
1110 : ///
1111 : /// Note that this applies even if the path ends beneath its original root. It
1112 : /// takes precendence over any other return values that may apple.
1113 : static const aboveRoot = _PathDirection('above root');
1114 :
1115 : /// The path contains enough ".." components that it ends at its original
1116 : /// root.
1117 : static const atRoot = _PathDirection('at root');
1118 :
1119 : /// The path contains enough ".." components that at some point it reaches its
1120 : /// original root, but it ends beneath that root.
1121 : static const reachesRoot = _PathDirection('reaches root');
1122 :
1123 : /// The path never reaches to or above its original root.
1124 : static const belowRoot = _PathDirection('below root');
1125 :
1126 : final String name;
1127 :
1128 11 : const _PathDirection(this.name);
1129 :
1130 0 : @override
1131 0 : String toString() => name;
1132 : }
1133 :
1134 : /// An enum of possible return values for [Context._isWithinOrEquals].
1135 : class _PathRelation {
1136 : /// The first path is a proper parent of the second.
1137 : ///
1138 : /// For example, `foo` is a proper parent of `foo/bar`, but not of `foo`.
1139 : static const within = _PathRelation('within');
1140 :
1141 : /// The two paths are equivalent.
1142 : ///
1143 : /// For example, `foo//bar` is equivalent to `foo/bar`.
1144 : static const equal = _PathRelation('equal');
1145 :
1146 : /// The first path is neither a parent of nor equal to the second.
1147 : static const different = _PathRelation('different');
1148 :
1149 : /// We couldn't quickly determine any information about the paths'
1150 : /// relationship to each other.
1151 : ///
1152 : /// Only returned by [Context._isWithinOrEqualsFast].
1153 : static const inconclusive = _PathRelation('inconclusive');
1154 :
1155 : final String name;
1156 :
1157 11 : const _PathRelation(this.name);
1158 :
1159 0 : @override
1160 0 : String toString() => name;
1161 : }
|