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