diff --git a/.github/ISSUE_TEMPLATE/path.md b/.github/ISSUE_TEMPLATE/path.md new file mode 100644 index 00000000..bed57e84 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/path.md @@ -0,0 +1,5 @@ +--- +name: "package:path" +about: "Create a bug or file a feature request against package:path." +labels: "package:path" +--- \ No newline at end of file diff --git a/.github/labeler.yml b/.github/labeler.yml index 7d8b8dc6..03fe1f47 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -31,3 +31,7 @@ "package:os_detect": - changed-files: - any-glob-to-any-file: 'pkgs/os_detect/**' + +"package:path": + - changed-files: + - any-glob-to-any-file: 'pkgs/path/**' diff --git a/.github/workflows/path.yaml b/.github/workflows/path.yaml new file mode 100644 index 00000000..6748f2cf --- /dev/null +++ b/.github/workflows/path.yaml @@ -0,0 +1,64 @@ +name: package:path + +on: + # Run CI on pushes to the main branch, and on PRs against main. + push: + branches: [ main ] + paths: + - '.github/workflows/path.yaml' + - 'pkgs/path/**' + pull_request: + branches: [ main ] + paths: + - '.github/workflows/path.yaml' + - 'pkgs/path/**' + schedule: + - cron: "0 0 * * 0" +env: + PUB_ENVIRONMENT: bot.github + +defaults: + run: + working-directory: pkgs/path/ + +jobs: + # Check code formatting and static analysis on a single OS (linux) + # against Dart dev and stable. + analyze: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + sdk: [dev] + steps: + - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 + - uses: dart-lang/setup-dart@0a8a0fc875eb934c15d08629302413c671d3f672 + with: + sdk: ${{ matrix.sdk }} + - id: install + name: Install dependencies + run: dart pub get + - name: Check formatting + run: dart format --output=none --set-exit-if-changed . + if: always() && steps.install.outcome == 'success' + - name: Analyze code + run: dart analyze --fatal-infos + if: always() && steps.install.outcome == 'success' + + test: + needs: analyze + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest] + sdk: [3.4, dev] + steps: + - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 + - uses: dart-lang/setup-dart@0a8a0fc875eb934c15d08629302413c671d3f672 + with: + sdk: ${{ matrix.sdk }} + - run: dart pub get + - run: dart test --platform vm,chrome + - name: Run Chrome tests - wasm + run: dart test --platform chrome --compiler dart2wasm + if: always() && steps.install.outcome == 'success' diff --git a/README.md b/README.md index 9da7f911..4565c818 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ This repository is home to various Dart packages under the [dart.dev](https://pu | [fixnum](pkgs/fixnum/) | Library for 32- and 64-bit signed fixed-width integers. | [![pub package](https://img.shields.io/pub/v/fixnum.svg)](https://pub.dev/packages/fixnum) | | [logging](pkgs/logging/) | Provides APIs for debugging and error logging. | [![pub package](https://img.shields.io/pub/v/logging.svg)](https://pub.dev/packages/logging) | | [os_detect](pkgs/os_detect/) | Platform independent OS detection. | [![pub package](https://img.shields.io/pub/v/os_detect.svg)](https://pub.dev/packages/os_detect) | +| [path](pkgs/path/) | A string-based path manipulation library for all of the path operations you know and love. | [![pub package](https://img.shields.io/pub/v/path.svg)](https://pub.dev/packages/path) | ## Publishing automation diff --git a/pkgs/path/.gitignore b/pkgs/path/.gitignore new file mode 100644 index 00000000..8608a826 --- /dev/null +++ b/pkgs/path/.gitignore @@ -0,0 +1,16 @@ +# Don’t commit the following directories created by pub. +.buildlog +.pub/ +.dart_tool/ +.idea/ +build/ +.packages + +# Or the files created by dart2js. +*.dart.js +*.js_ +*.js.deps +*.js.map + +# Include when developing application packages. +pubspec.lock diff --git a/pkgs/path/CHANGELOG.md b/pkgs/path/CHANGELOG.md new file mode 100644 index 00000000..c91afe16 --- /dev/null +++ b/pkgs/path/CHANGELOG.md @@ -0,0 +1,186 @@ +## 1.9.1 + +- Require Dart 3.4 +- Move to `dart-lang/core` monorepo. + +## 1.9.0 + +* Allow percent-encoded colons (`%3a`) in drive letters in `fromUri`. +* Fixed an issue with the `split` method doc comment. +* Require Dart 3.0 + +## 1.8.3 + +* Support up to 16 arguments in join function and up to 15 arguments in absolute function. + +## 1.8.2 + +* Enable the `avoid_dynamic_calls` lint. +* Populate the pubspec `repository` field. + +## 1.8.1 + +* Don't crash when an empty string is passed to `toUri()`. + +## 1.8.0 + +* Stable release for null safety. + +## 1.8.0-nullsafety.3 + +* Update SDK constraints to `>=2.12.0-0 <3.0.0` based on beta release + guidelines. + +## 1.8.0-nullsafety.2 + +* Allow prerelease versions of the 2.12 sdk. + +## 1.8.0-nullsafety.1 + +* Allow 2.10 stable and 2.11.0 dev SDK versions. + +## 1.8.0-nullsafety + +* Migrate to null safety. + +## 1.7.0 + +* Add support for multiple extension in `context.extension()`. + +## 1.6.4 + +* Fixed a number of lints that affect the package health score. + +* Added an example. + +## 1.6.3 + +* Don't throw a FileSystemException from `current` if the working directory has + been deleted, but we have a cached one we can use. + +## 1.6.2 + +* Set max SDK version to `<3.0.0`, and adjust other dependencies. + +## 1.6.1 + +* Drop the `retype` implementation for compatibility with the latest SDK. + +## 1.6.0 + +* Add a `PathMap` class that uses path equality for its keys. + +* Add a `PathSet` class that uses path equality for its contents. + +## 1.5.1 + +* Fix a number of bugs that occurred when the current working directory was `/` + on Linux or Mac OS. + +## 1.5.0 + +* Add a `setExtension()` top-level function and `Context` method. + +## 1.4.2 + +* Treat `package:` URLs as absolute. + +* Normalize `c:\foo\.` to `c:\foo`. + +## 1.4.1 + +* Root-relative URLs like `/foo` are now resolved relative to the drive letter + for `file` URLs that begin with a Windows-style drive letter. This matches the + [WHATWG URL specification][]. + +[WHATWG URL specification]: https://url.spec.whatwg.org/#file-slash-state + +* When a root-relative URLs like `/foo` is converted to a Windows path using + `fromUrl()`, it is now resolved relative to the drive letter. This matches + IE's behavior. + +## 1.4.0 + +* Add `equals()`, `hash()` and `canonicalize()` top-level functions and + `Context` methods. These make it easier to treat paths as map keys. + +* Properly compare Windows paths case-insensitively. + +* Further improve the performance of `isWithin()`. + +## 1.3.9 + +* Further improve the performance of `isWithin()` when paths contain `/.` + sequences that aren't `/../`. + +## 1.3.8 + +* Improve the performance of `isWithin()` when the paths don't contain + asymmetrical `.` or `..` components. + +* Improve the performance of `relative()` when `from` is `null` and the path is + already relative. + +* Improve the performance of `current` when the current directory hasn't + changed. + +## 1.3.7 + +* Improve the performance of `absolute()` and `normalize()`. + +## 1.3.6 + +* Ensure that `path.toUri` preserves trailing slashes for relative paths. + +## 1.3.5 + +* Added type annotations to top-level and static fields. + +## 1.3.4 + +* Fix dev_compiler warnings. + +## 1.3.3 + +* Performance improvement in `Context.relative` - don't call `current` if `from` + is not relative. + +## 1.3.2 + +* Fix some analyzer hints. + +## 1.3.1 + +* Add a number of performance improvements. + +## 1.3.0 + +* Expose a top-level `context` field that provides access to a `Context` object + for the current system. + +## 1.2.3 + +* Don't cache path Context based on cwd, as cwd involves a system-call to + compute. + +## 1.2.2 + +* Remove the documentation link from the pubspec so this is linked to + pub.dev by default. + +# 1.2.1 + +* Many members on `Style` that provided access to patterns and functions used + internally for parsing paths have been deprecated. + +* Manually parse paths (rather than using RegExps to do so) for better + performance. + +# 1.2.0 + +* Added `path.prettyUri`, which produces a human-readable representation of a + URI. + +# 1.1.0 + +* `path.fromUri` now accepts strings as well as `Uri` objects. diff --git a/pkgs/path/LICENSE b/pkgs/path/LICENSE new file mode 100644 index 00000000..000cd7be --- /dev/null +++ b/pkgs/path/LICENSE @@ -0,0 +1,27 @@ +Copyright 2014, the Dart project authors. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google LLC nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/pkgs/path/README.md b/pkgs/path/README.md new file mode 100644 index 00000000..53ec8891 --- /dev/null +++ b/pkgs/path/README.md @@ -0,0 +1,120 @@ +[![Dart CI](https://github.com/dart-lang/core/actions/workflows/path.yaml/badge.svg)](https://github.com/dart-lang/core/actions/workflows/path.yaml) +[![pub package](https://img.shields.io/pub/v/path.svg)](https://pub.dev/packages/path) +[![package publisher](https://img.shields.io/pub/publisher/path.svg)](https://pub.dev/packages/path/publisher) + +A comprehensive, cross-platform path manipulation library for Dart. + +The path package provides common operations for manipulating paths: +joining, splitting, normalizing, etc. + +We've tried very hard to make this library do the "right" thing on whatever +platform you run it on, including in the browser. When you use the top-level +functions, it will assume the current platform's path style and work with +that. If you want to explicitly work with paths of a specific style, you can +construct a [`p.Context`][Context] for that style. + +[Context]: https://pub.dev/documentation/path/latest/path/Context-class.html + +## Using + +The path library was designed to be imported with a prefix, though you don't +have to if you don't want to: + +```dart +import 'package:path/path.dart' as p; +``` + +The most common way to use the library is through the top-level functions. +These manipulate path strings based on your current working directory and +the path style (POSIX, Windows, or URLs) of the host platform. For example: + +```dart +p.join('directory', 'file.txt'); +``` + +This calls the top-level `join()` function to join the "directory" and +"file.txt" using the current platform's directory separator. + +If you want to work with paths for a specific platform regardless of the underlying platform that the program is running on, you can create a +[Context] and give it an explicit [Style]: + +```dart +var context = p.Context(style: Style.windows); +context.join('directory', 'file.txt'); +``` + +This will join "directory" and "file.txt" using the Windows path separator, +even when the program is run on a POSIX machine. + +## Stability + +The `path` package is used by many Dart packages, and as such it strives for a +very high degree of stability. For the same reason, though, releasing a new +major version would probably cause a lot of versioning pain, so some flexibility +is necessary. + +We try to guarantee that **operations with valid inputs and correct output will +not change**. Operations, where one or more inputs are invalid according to the +semantics of the corresponding platform, may produce different outputs over time. +Operations for which `path` produces incorrect output will also change so that +we can fix bugs. + +Also, the `path` package's URL handling is based on [the WHATWG URL spec][]. +This is a living standard, and some parts of it haven't yet been entirely +solidified by vendor support. The `path` package reserves the right to change +its URL behavior if the underlying specification changes, although if the change +is big enough to break many valid uses we may elect to treat it as a breaking +change anyway. + +[the WHATWG URL spec]: https://url.spec.whatwg.org/ + +## FAQ + +### Where can I use this? + +The `path` package runs on the Dart VM and in the browser under both dart2js and +Dartium. On the browser, `window.location.href` is used as the current path. + +### Why doesn't this make paths first-class objects? + +When you have path *objects*, then every API that takes a path has to decide if +it accepts strings, path objects, or both. + + * Accepting strings is the most convenient, but then it seems weird to have + these path objects that aren't actually accepted by anything that needs a + path. Once you've created a path, you have to always call `.toString()` on + it before you can do anything useful with it. + + * Requiring objects forces users to wrap path strings in these objects, which + is tedious. It also means coupling that API to whatever library defines this + path class. If there are multiple "path" libraries that each define their + own path types, then any library that works with paths has to pick which one + it uses. + + * Taking both means you can't type your API. That defeats the purpose of + having a path type: why have a type if your APIs can't annotate that they + expect it? + +Given that, we've decided this library should simply treat paths as strings. + +### How cross-platform is this? + +We believe this library handles most of the corner cases of Windows paths +(POSIX paths are generally pretty straightforward): + + * It understands that *both* "/" and "\\" are valid path separators, not just + "\\". + + * It can accurately tell if a path is absolute based on drive-letters or UNC + prefix. + + * It understands that "/foo" is not an absolute path on Windows. + + * It knows that "C:\foo\one.txt" and "c:/foo\two.txt" are two files in the + same directory. + +### What is a "path" in the browser? + +If you use this package in a browser, then it considers the "platform" to be +the browser itself and uses URL strings to represent "browser paths". + diff --git a/pkgs/path/analysis_options.yaml b/pkgs/path/analysis_options.yaml new file mode 100644 index 00000000..0b63e947 --- /dev/null +++ b/pkgs/path/analysis_options.yaml @@ -0,0 +1,17 @@ +include: package:dart_flutter_team_lints/analysis_options.yaml + +linter: + rules: + - avoid_private_typedef_functions + - avoid_unused_constructor_parameters + - avoid_void_async + - cancel_subscriptions + - join_return_with_assignment + - missing_whitespace_between_adjacent_strings + - no_runtimeType_toString + - package_api_docs + - prefer_const_declarations + - prefer_expression_function_bodies + - prefer_final_locals + - unnecessary_breaks + - use_string_buffers diff --git a/pkgs/path/benchmark/benchmark.dart b/pkgs/path/benchmark/benchmark.dart new file mode 100644 index 00000000..b6647bb2 --- /dev/null +++ b/pkgs/path/benchmark/benchmark.dart @@ -0,0 +1,106 @@ +// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:path/path.dart' as p; + +/// Some hopefully real-world representative platform-independent paths. +const genericPaths = [ + '.', + '..', + 'out/ReleaseIA32/packages', + 'lib', + 'lib/src/', + 'lib/src/style/url.dart', + 'test/./not/.././normalized', + 'benchmark/really/long/path/with/many/components.dart', +]; + +/// Some platform-specific paths. +final platformPaths = { + p.Style.posix: [ + '/', + '/home/user/dart/sdk/lib/indexed_db/dart2js/indexed_db_dart2js.dart', + ], + p.Style.url: [ + 'https://example.server.org/443643002/path?top=yes#fragment', + ], + p.Style.windows: [ + r'C:\User\me\', + r'\\server\share\my\folders\some\file.data', + ], +}; + +/// The command line arguments passed to this script. +late final List arguments; + +void main(List args) { + arguments = args; + + for (var style in [p.Style.posix, p.Style.url, p.Style.windows]) { + final context = p.Context(style: style); + final files = [...genericPaths, ...platformPaths[style]!]; + + void benchmark(String name, void Function(String) function) { + runBenchmark('${style.name}-$name', 100000, () { + for (var file in files) { + function(file); + } + }); + } + + void benchmarkPairs(String name, void Function(String, String) function) { + runBenchmark('${style.name}-$name', 1000, () { + for (var file1 in files) { + for (var file2 in files) { + function(file1, file2); + } + } + }); + } + + benchmark('absolute', context.absolute); + benchmark('basename', context.basename); + benchmark('basenameWithoutExtension', context.basenameWithoutExtension); + benchmark('dirname', context.dirname); + benchmark('extension', context.extension); + benchmark('rootPrefix', context.rootPrefix); + benchmark('isAbsolute', context.isAbsolute); + benchmark('isRelative', context.isRelative); + benchmark('isRootRelative', context.isRootRelative); + benchmark('normalize', context.normalize); + benchmark('relative', context.relative); + benchmarkPairs('relative from', (String file, String from) { + try { + context.relative(file, from: from); + } on p.PathException { + // Do nothing. + } + }); + benchmark('toUri', context.toUri); + benchmark('prettyUri', context.prettyUri); + benchmarkPairs('isWithin', context.isWithin); + } + + runBenchmark('current', 100000, () => p.current); +} + +void runBenchmark(String name, int count, void Function() function) { + // If names are passed on the command-line, they select which benchmarks are + // run. + if (arguments.isNotEmpty && !arguments.contains(name)) return; + + // Warmup. + for (var i = 0; i < 10000; i++) { + function(); + } + + final stopwatch = Stopwatch()..start(); + for (var i = 0; i < count; i++) { + function(); + } + + final rate = + (count / stopwatch.elapsedMicroseconds).toStringAsFixed(5).padLeft(9); + print('${name.padLeft(32)}: $rate iter/us (${stopwatch.elapsed})'); +} diff --git a/pkgs/path/example/example.dart b/pkgs/path/example/example.dart new file mode 100644 index 00000000..89271fd4 --- /dev/null +++ b/pkgs/path/example/example.dart @@ -0,0 +1,16 @@ +// Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:path/path.dart' as p; + +void main() { + print('Current path style: ${p.style}'); + + print('Current process path: ${p.current}'); + + print('Separators'); + for (var entry in [p.posix, p.windows, p.url]) { + print(' ${entry.style.toString().padRight(7)}: ${entry.separator}'); + } +} diff --git a/pkgs/path/lib/path.dart b/pkgs/path/lib/path.dart new file mode 100644 index 00000000..40f1ea3c --- /dev/null +++ b/pkgs/path/lib/path.dart @@ -0,0 +1,481 @@ +// Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +/// A comprehensive, cross-platform path manipulation library. +/// +/// The path library was designed to be imported with a prefix, though you don't +/// have to if you don't want to: +/// +/// import 'package:path/path.dart' as p; +/// +/// The most common way to use the library is through the top-level functions. +/// These manipulate path strings based on your current working directory and +/// the path style (POSIX, Windows, or URLs) of the host platform. For example: +/// +/// p.join('directory', 'file.txt'); +/// +/// This calls the top-level [join] function to join "directory" and "file.txt" +/// using the current platform's directory separator. +/// +/// If you want to work with paths for a specific platform regardless of the +/// underlying platform that the program is running on, you can create a +/// [Context] and give it an explicit [Style]: +/// +/// var context = p.Context(style: Style.windows); +/// context.join('directory', 'file.txt'); +/// +/// This will join "directory" and "file.txt" using the Windows path separator, +/// even when the program is run on a POSIX machine. +library; + +import 'src/context.dart'; +import 'src/style.dart'; + +export 'src/context.dart' hide createInternal; +export 'src/path_exception.dart'; +export 'src/path_map.dart'; +export 'src/path_set.dart'; +export 'src/style.dart'; + +/// A default context for manipulating POSIX paths. +final Context posix = Context(style: Style.posix); + +/// A default context for manipulating Windows paths. +final Context windows = Context(style: Style.windows); + +/// A default context for manipulating URLs. +/// +/// URL path equality is undefined for paths that differ only in their +/// percent-encoding or only in the case of their host segment. +final Context url = Context(style: Style.url); + +/// The system path context. +/// +/// This differs from a context created with [Context.new] in that its +/// [Context.current] is always the current working directory, rather than being +/// set once when the context is created. +final Context context = createInternal(); + +/// Returns the [Style] of the current context. +/// +/// This is the style that all top-level path functions will use. +Style get style => context.style; + +/// Gets the path to the current working directory. +/// +/// In the browser, this means the current URL, without the last file segment. +String get current { + // If the current working directory gets deleted out from under the program, + // accessing it will throw an IO exception. In order to avoid transient + // errors, if we already have a cached working directory, catch the error and + // use that. + Uri uri; + try { + uri = Uri.base; + } on Exception { + if (_current != null) return _current!; + rethrow; + } + + // Converting the base URI to a file path is pretty slow, and the base URI + // rarely changes in practice, so we cache the result here. + if (uri == _currentUriBase) return _current!; + _currentUriBase = uri; + + if (Style.platform == Style.url) { + _current = uri.resolve('.').toString(); + } else { + final path = uri.toFilePath(); + // Remove trailing '/' or '\' unless it is the only thing left + // (for instance the root on Linux). + final lastIndex = path.length - 1; + assert(path[lastIndex] == '/' || path[lastIndex] == '\\'); + _current = lastIndex == 0 ? path : path.substring(0, lastIndex); + } + return _current!; +} + +/// The last value returned by [Uri.base]. +/// +/// This is used to cache the current working directory. +Uri? _currentUriBase; + +/// The last known value of the current working directory. +/// +/// This is cached because [current] is called frequently but rarely actually +/// changes. +String? _current; + +/// Gets the path separator for the current platform. This is `\` on Windows +/// and `/` on other platforms (including the browser). +String get separator => context.separator; + +/// Returns a new path with the given path parts appended to [current]. +/// +/// Equivalent to [join()] with [current] as the first argument. Example: +/// +/// p.absolute('path', 'to/foo'); // -> '/your/current/dir/path/to/foo' +/// +/// Does not [normalize] or [canonicalize] paths. +String absolute(String part1, + [String? part2, + String? part3, + String? part4, + String? part5, + String? part6, + String? part7, + String? part8, + String? part9, + String? part10, + String? part11, + String? part12, + String? part13, + String? part14, + String? part15]) => + context.absolute(part1, part2, part3, part4, part5, part6, part7, part8, + part9, part10, part11, part12, part13, part14, part15); + +/// Gets the part of [path] after the last separator. +/// +/// p.basename('path/to/foo.dart'); // -> 'foo.dart' +/// p.basename('path/to'); // -> 'to' +/// +/// Trailing separators are ignored. +/// +/// p.basename('path/to/'); // -> 'to' +String basename(String path) => context.basename(path); + +/// Gets the part of [path] after the last separator, and without any trailing +/// file extension. +/// +/// p.basenameWithoutExtension('path/to/foo.dart'); // -> 'foo' +/// +/// Trailing separators are ignored. +/// +/// p.basenameWithoutExtension('path/to/foo.dart/'); // -> 'foo' +String basenameWithoutExtension(String path) => + context.basenameWithoutExtension(path); + +/// Gets the part of [path] before the last separator. +/// +/// p.dirname('path/to/foo.dart'); // -> 'path/to' +/// p.dirname('path/to'); // -> 'path' +/// +/// Trailing separators are ignored. +/// +/// p.dirname('path/to/'); // -> 'path' +/// +/// If an absolute path contains no directories, only a root, then the root +/// is returned. +/// +/// p.dirname('/'); // -> '/' (posix) +/// p.dirname('c:\'); // -> 'c:\' (windows) +/// +/// If a relative path has no directories, then '.' is returned. +/// +/// p.dirname('foo'); // -> '.' +/// p.dirname(''); // -> '.' +String dirname(String path) => context.dirname(path); + +/// Gets the file extension of [path]: the portion of [basename] from the last +/// `.` to the end (including the `.` itself). +/// +/// p.extension('path/to/foo.dart'); // -> '.dart' +/// p.extension('path/to/foo'); // -> '' +/// p.extension('path.to/foo'); // -> '' +/// p.extension('path/to/foo.dart.js'); // -> '.js' +/// +/// If the file name starts with a `.`, then that is not considered the +/// extension: +/// +/// p.extension('~/.bashrc'); // -> '' +/// p.extension('~/.notes.txt'); // -> '.txt' +/// +/// Takes an optional parameter `level` which makes possible to return +/// multiple extensions having `level` number of dots. If `level` exceeds the +/// number of dots, the full extension is returned. The value of `level` must +/// be greater than 0, else `RangeError` is thrown. +/// +/// p.extension('foo.bar.dart.js', 2); // -> '.dart.js +/// p.extension('foo.bar.dart.js', 3); // -> '.bar.dart.js' +/// p.extension('foo.bar.dart.js', 10); // -> '.bar.dart.js' +/// p.extension('path/to/foo.bar.dart.js', 2); // -> '.dart.js' +String extension(String path, [int level = 1]) => + context.extension(path, level); + +/// Returns the root of [path], if it's absolute, or the empty string if it's +/// relative. +/// +/// // Unix +/// p.rootPrefix('path/to/foo'); // -> '' +/// p.rootPrefix('/path/to/foo'); // -> '/' +/// +/// // Windows +/// p.rootPrefix(r'path\to\foo'); // -> '' +/// p.rootPrefix(r'C:\path\to\foo'); // -> r'C:\' +/// p.rootPrefix(r'\\server\share\a\b'); // -> r'\\server\share' +/// +/// // URL +/// p.rootPrefix('path/to/foo'); // -> '' +/// p.rootPrefix('https://dart.dev/path/to/foo'); +/// // -> 'https://dart.dev' +String rootPrefix(String path) => context.rootPrefix(path); + +/// Returns `true` if [path] is an absolute path and `false` if it is a +/// relative path. +/// +/// On POSIX systems, absolute paths start with a `/` (forward slash). On +/// Windows, an absolute path starts with `\\`, or a drive letter followed by +/// `:/` or `:\`. For URLs, absolute paths either start with a protocol and +/// optional hostname (e.g. `https://dart.dev`, `file://`) or with a `/`. +/// +/// URLs that start with `/` are known as "root-relative", since they're +/// relative to the root of the current URL. Since root-relative paths are still +/// absolute in every other sense, [isAbsolute] will return true for them. They +/// can be detected using [isRootRelative]. +bool isAbsolute(String path) => context.isAbsolute(path); + +/// Returns `true` if [path] is a relative path and `false` if it is absolute. +/// On POSIX systems, absolute paths start with a `/` (forward slash). On +/// Windows, an absolute path starts with `\\`, or a drive letter followed by +/// `:/` or `:\`. +bool isRelative(String path) => context.isRelative(path); + +/// Returns `true` if [path] is a root-relative path and `false` if it's not. +/// +/// URLs that start with `/` are known as "root-relative", since they're +/// relative to the root of the current URL. Since root-relative paths are still +/// absolute in every other sense, [isAbsolute] will return true for them. They +/// can be detected using [isRootRelative]. +/// +/// No POSIX and Windows paths are root-relative. +bool isRootRelative(String path) => context.isRootRelative(path); + +/// Joins the given path parts into a single path using the current platform's +/// [separator]. Example: +/// +/// p.join('path', 'to', 'foo'); // -> 'path/to/foo' +/// +/// If any part ends in a path separator, then a redundant separator will not +/// be added: +/// +/// p.join('path/', 'to', 'foo'); // -> 'path/to/foo' +/// +/// If a part is an absolute path, then anything before that will be ignored: +/// +/// p.join('path', '/to', 'foo'); // -> '/to/foo' +String join(String part1, + [String? part2, + String? part3, + String? part4, + String? part5, + String? part6, + String? part7, + String? part8, + String? part9, + String? part10, + String? part11, + String? part12, + String? part13, + String? part14, + String? part15, + String? part16]) => + context.join(part1, part2, part3, part4, part5, part6, part7, part8, part9, + part10, part11, part12, part13, part14, part15, part16); + +/// Joins the given path parts into a single path using the current platform's +/// [separator]. Example: +/// +/// p.joinAll(['path', 'to', 'foo']); // -> 'path/to/foo' +/// +/// If any part ends in a path separator, then a redundant separator will not +/// be added: +/// +/// p.joinAll(['path/', 'to', 'foo']); // -> 'path/to/foo' +/// +/// If a part is an absolute path, then anything before that will be ignored: +/// +/// p.joinAll(['path', '/to', 'foo']); // -> '/to/foo' +/// +/// For a fixed number of parts, [join] is usually terser. +String joinAll(Iterable parts) => context.joinAll(parts); + +/// Splits [path] into its components using the current platform's [separator]. +/// +/// p.split('path/to/foo'); // -> ['path', 'to', 'foo'] +/// +/// The path will *not* be normalized before splitting. +/// +/// p.split('path/../foo'); // -> ['path', '..', 'foo'] +/// +/// If [path] is absolute, the root directory will be the first element in the +/// array. Example: +/// +/// // Unix +/// p.split('/path/to/foo'); // -> ['/', 'path', 'to', 'foo'] +/// +/// // Windows +/// p.split(r'C:\path\to\foo'); // -> [r'C:\', 'path', 'to', 'foo'] +/// p.split(r'\\server\share\path\to\foo'); +/// // -> [r'\\server\share', 'foo', 'bar', 'baz'] +/// +/// // Browser +/// p.split('https://dart.dev/path/to/foo'); +/// // -> ['https://dart.dev', 'path', 'to', 'foo'] +List split(String path) => context.split(path); + +/// Canonicalizes [path]. +/// +/// This is guaranteed to return the same path for two different input paths +/// if and only if both input paths point to the same location. Unlike +/// [normalize], it returns absolute paths when possible and canonicalizes +/// ASCII case on Windows. +/// +/// Note that this does not resolve symlinks. +/// +/// If you want a map that uses path keys, it's probably more efficient to use a +/// Map with [equals] and [hash] specified as the callbacks to use for keys than +/// it is to canonicalize every key. +String canonicalize(String path) => context.canonicalize(path); + +/// Normalizes [path], simplifying it by handling `..`, and `.`, and +/// removing redundant path separators whenever possible. +/// +/// Note that this is *not* guaranteed to return the same result for two +/// equivalent input paths. For that, see [canonicalize]. Or, if you're using +/// paths as map keys use [equals] and [hash] as the key callbacks. +/// +/// p.normalize('path/./to/..//file.text'); // -> 'path/file.txt' +String normalize(String path) => context.normalize(path); + +/// Attempts to convert [path] to an equivalent relative path from the current +/// directory. +/// +/// // Given current directory is /root/path: +/// p.relative('/root/path/a/b.dart'); // -> 'a/b.dart' +/// p.relative('/root/other.dart'); // -> '../other.dart' +/// +/// If the [from] argument is passed, [path] is made relative to that instead. +/// +/// p.relative('/root/path/a/b.dart', from: '/root/path'); // -> 'a/b.dart' +/// p.relative('/root/other.dart', from: '/root/path'); +/// // -> '../other.dart' +/// +/// If [path] and/or [from] are relative paths, they are assumed to be relative +/// to the current directory. +/// +/// Since there is no relative path from one drive letter to another on Windows, +/// or from one hostname to another for URLs, this will return an absolute path +/// in those cases. +/// +/// // Windows +/// p.relative(r'D:\other', from: r'C:\home'); // -> 'D:\other' +/// +/// // URL +/// p.relative('https://dart.dev', from: 'https://pub.dev'); +/// // -> 'https://dart.dev' +String relative(String path, {String? from}) => + context.relative(path, from: from); + +/// Returns `true` if [child] is a path beneath `parent`, and `false` otherwise. +/// +/// p.isWithin('/root/path', '/root/path/a'); // -> true +/// p.isWithin('/root/path', '/root/other'); // -> false +/// p.isWithin('/root/path', '/root/path') // -> false +bool isWithin(String parent, String child) => context.isWithin(parent, child); + +/// Returns `true` if [path1] points to the same location as [path2], and +/// `false` otherwise. +/// +/// The [hash] function returns a hash code that matches these equality +/// semantics. +bool equals(String path1, String path2) => context.equals(path1, path2); + +/// Returns a hash code for [path] such that, if [equals] returns `true` for two +/// paths, their hash codes are the same. +/// +/// Note that the same path may have different hash codes on different platforms +/// or with different [current] directories. +int hash(String path) => context.hash(path); + +/// Removes a trailing extension from the last part of [path]. +/// +/// p.withoutExtension('path/to/foo.dart'); // -> 'path/to/foo' +String withoutExtension(String path) => context.withoutExtension(path); + +/// Returns [path] with the trailing extension set to [extension]. +/// +/// If [path] doesn't have a trailing extension, this just adds [extension] to +/// the end. +/// +/// p.setExtension('path/to/foo.dart', '.js') // -> 'path/to/foo.js' +/// p.setExtension('path/to/foo.dart.js', '.map') +/// // -> 'path/to/foo.dart.map' +/// p.setExtension('path/to/foo', '.js') // -> 'path/to/foo.js' +String setExtension(String path, String extension) => + context.setExtension(path, extension); + +/// Returns the path represented by [uri], which may be a [String] or a [Uri]. +/// +/// For POSIX and Windows styles, [uri] must be a `file:` URI. For the URL +/// style, this will just convert [uri] to a string. +/// +/// // POSIX +/// p.fromUri('file:///path/to/foo') // -> '/path/to/foo' +/// +/// // Windows +/// p.fromUri('file:///C:/path/to/foo') // -> r'C:\path\to\foo' +/// +/// // URL +/// p.fromUri('https://dart.dev/path/to/foo') +/// // -> 'https://dart.dev/path/to/foo' +/// +/// If [uri] is relative, a relative path will be returned. +/// +/// p.fromUri('path/to/foo'); // -> 'path/to/foo' +String fromUri(Object? uri) => context.fromUri(uri!); + +/// Returns the URI that represents [path]. +/// +/// For POSIX and Windows styles, this will return a `file:` URI. For the URL +/// style, this will just convert [path] to a [Uri]. +/// +/// // POSIX +/// p.toUri('/path/to/foo') +/// // -> Uri.parse('file:///path/to/foo') +/// +/// // Windows +/// p.toUri(r'C:\path\to\foo') +/// // -> Uri.parse('file:///C:/path/to/foo') +/// +/// // URL +/// p.toUri('https://dart.dev/path/to/foo') +/// // -> Uri.parse('https://dart.dev/path/to/foo') +/// +/// If [path] is relative, a relative URI will be returned. +/// +/// p.toUri('path/to/foo') // -> Uri.parse('path/to/foo') +Uri toUri(String path) => context.toUri(path); + +/// Returns a terse, human-readable representation of [uri]. +/// +/// [uri] can be a [String] or a [Uri]. If it can be made relative to the +/// current working directory, that's done. Otherwise, it's returned as-is. This +/// gracefully handles non-`file:` URIs for [Style.posix] and [Style.windows]. +/// +/// The returned value is meant for human consumption, and may be either URI- +/// or path-formatted. +/// +/// // POSIX at "/root/path" +/// p.prettyUri('file:///root/path/a/b.dart'); // -> 'a/b.dart' +/// p.prettyUri('https://dart.dev/'); // -> 'https://dart.dev' +/// +/// // Windows at "C:\root\path" +/// p.prettyUri('file:///C:/root/path/a/b.dart'); // -> r'a\b.dart' +/// p.prettyUri('https://dart.dev/'); // -> 'https://dart.dev' +/// +/// // URL at "https://dart.dev/root/path" +/// p.prettyUri('https://dart.dev/root/path/a/b.dart'); // -> r'a/b.dart' +/// p.prettyUri('file:///root/path'); // -> 'file:///root/path' +String prettyUri(Object? uri) => context.prettyUri(uri!); diff --git a/pkgs/path/lib/src/characters.dart b/pkgs/path/lib/src/characters.dart new file mode 100644 index 00000000..a0835431 --- /dev/null +++ b/pkgs/path/lib/src/characters.dart @@ -0,0 +1,17 @@ +// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +/// This library contains character-code definitions. +const plus = 0x2b; +const minus = 0x2d; +const period = 0x2e; +const slash = 0x2f; +const zero = 0x30; +const nine = 0x39; +const colon = 0x3a; +const upperA = 0x41; +const upperZ = 0x5a; +const lowerA = 0x61; +const lowerZ = 0x7a; +const backslash = 0x5c; diff --git a/pkgs/path/lib/src/context.dart b/pkgs/path/lib/src/context.dart new file mode 100644 index 00000000..45233321 --- /dev/null +++ b/pkgs/path/lib/src/context.dart @@ -0,0 +1,1201 @@ +// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:math' as math; + +import '../path.dart' as p; +import 'characters.dart' as chars; +import 'internal_style.dart'; +import 'parsed_path.dart'; +import 'path_exception.dart'; +import 'style.dart'; + +Context createInternal() => Context._internal(); + +/// An instantiable class for manipulating paths. Unlike the top-level +/// functions, this lets you explicitly select what platform the paths will use. +class Context { + /// Creates a new path context for the given style and current directory. + /// + /// If [style] is omitted, it uses the host operating system's path style. If + /// only [current] is omitted, it defaults ".". If *both* [style] and + /// [current] are omitted, [current] defaults to the real current working + /// directory. + /// + /// On the browser, [style] defaults to [Style.url] and [current] defaults to + /// the current URL. + factory Context({Style? style, String? current}) { + if (current == null) { + if (style == null) { + current = p.current; + } else { + current = '.'; + } + } + + if (style == null) { + style = Style.platform; + } else if (style is! InternalStyle) { + throw ArgumentError('Only styles defined by the path package are ' + 'allowed.'); + } + + return Context._(style as InternalStyle, current); + } + + /// Create a [Context] to be used internally within path. + Context._internal() + : style = Style.platform as InternalStyle, + _current = null; + + Context._(this.style, this._current); + + /// The style of path that this context works with. + final InternalStyle style; + + /// The current directory given when Context was created. If null, current + /// directory is evaluated from 'p.current'. + final String? _current; + + /// The current directory that relative paths are relative to. + String get current => _current ?? p.current; + + /// Gets the path separator for the context's [style]. On Mac and Linux, + /// this is `/`. On Windows, it's `\`. + String get separator => style.separator; + + /// Returns a new path with the given path parts appended to [current]. + /// + /// Equivalent to [join()] with [current] as the first argument. Example: + /// + /// var context = Context(current: '/root'); + /// context.absolute('path', 'to', 'foo'); // -> '/root/path/to/foo' + /// + /// If [current] isn't absolute, this won't return an absolute path. Does not + /// [normalize] or [canonicalize] paths. + String absolute(String part1, + [String? part2, + String? part3, + String? part4, + String? part5, + String? part6, + String? part7, + String? part8, + String? part9, + String? part10, + String? part11, + String? part12, + String? part13, + String? part14, + String? part15]) { + _validateArgList('absolute', [ + part1, + part2, + part3, + part4, + part5, + part6, + part7, + part8, + part9, + part10, + part11, + part12, + part13, + part14, + part15 + ]); + + // If there's a single absolute path, just return it. This is a lot faster + // for the common case of `p.absolute(path)`. + if (part2 == null && isAbsolute(part1) && !isRootRelative(part1)) { + return part1; + } + + return join(current, part1, part2, part3, part4, part5, part6, part7, part8, + part9, part10, part11, part12, part13, part14, part15); + } + + /// Gets the part of [path] after the last separator on the context's + /// platform. + /// + /// context.basename('path/to/foo.dart'); // -> 'foo.dart' + /// context.basename('path/to'); // -> 'to' + /// + /// Trailing separators are ignored. + /// + /// context.basename('path/to/'); // -> 'to' + String basename(String path) => _parse(path).basename; + + /// Gets the part of [path] after the last separator on the context's + /// platform, and without any trailing file extension. + /// + /// context.basenameWithoutExtension('path/to/foo.dart'); // -> 'foo' + /// + /// Trailing separators are ignored. + /// + /// context.basenameWithoutExtension('path/to/foo.dart/'); // -> 'foo' + String basenameWithoutExtension(String path) => + _parse(path).basenameWithoutExtension; + + /// Gets the part of [path] before the last separator. + /// + /// context.dirname('path/to/foo.dart'); // -> 'path/to' + /// context.dirname('path/to'); // -> 'path' + /// + /// Trailing separators are ignored. + /// + /// context.dirname('path/to/'); // -> 'path' + String dirname(String path) { + final parsed = _parse(path); + parsed.removeTrailingSeparators(); + if (parsed.parts.isEmpty) return parsed.root ?? '.'; + if (parsed.parts.length == 1) return parsed.root ?? '.'; + parsed.parts.removeLast(); + parsed.separators.removeLast(); + parsed.removeTrailingSeparators(); + return parsed.toString(); + } + + /// Gets the file extension of [path]: the portion of [basename] from the last + /// `.` to the end (including the `.` itself). + /// + /// context.extension('path/to/foo.dart'); // -> '.dart' + /// context.extension('path/to/foo'); // -> '' + /// context.extension('path.to/foo'); // -> '' + /// context.extension('path/to/foo.dart.js'); // -> '.js' + /// + /// If the file name starts with a `.`, then it is not considered an + /// extension: + /// + /// context.extension('~/.bashrc'); // -> '' + /// context.extension('~/.notes.txt'); // -> '.txt' + /// + /// Takes an optional parameter `level` which makes possible to return + /// multiple extensions having `level` number of dots. If `level` exceeds the + /// number of dots, the full extension is returned. The value of `level` must + /// be greater than 0, else `RangeError` is thrown. + /// + /// context.extension('foo.bar.dart.js', 2); // -> '.dart.js + /// context.extension('foo.bar.dart.js', 3); // -> '.bar.dart.js' + /// context.extension('foo.bar.dart.js', 10); // -> '.bar.dart.js' + /// context.extension('path/to/foo.bar.dart.js', 2); // -> '.dart.js' + String extension(String path, [int level = 1]) => + _parse(path).extension(level); + + /// Returns the root of [path] if it's absolute, or an empty string if it's + /// relative. + /// + /// // Unix + /// context.rootPrefix('path/to/foo'); // -> '' + /// context.rootPrefix('/path/to/foo'); // -> '/' + /// + /// // Windows + /// context.rootPrefix(r'path\to\foo'); // -> '' + /// context.rootPrefix(r'C:\path\to\foo'); // -> r'C:\' + /// context.rootPrefix(r'\\server\share\a\b'); // -> r'\\server\share' + /// + /// // URL + /// context.rootPrefix('path/to/foo'); // -> '' + /// context.rootPrefix('https://dart.dev/path/to/foo'); + /// // -> 'https://dart.dev' + String rootPrefix(String path) => path.substring(0, style.rootLength(path)); + + /// Returns `true` if [path] is an absolute path and `false` if it is a + /// relative path. + /// + /// On POSIX systems, absolute paths start with a `/` (forward slash). On + /// Windows, an absolute path starts with `\\`, or a drive letter followed by + /// `:/` or `:\`. For URLs, absolute paths either start with a protocol and + /// optional hostname (e.g. `https://dart.dev`, `file://`) or with a `/`. + /// + /// URLs that start with `/` are known as "root-relative", since they're + /// relative to the root of the current URL. Since root-relative paths are + /// still absolute in every other sense, [isAbsolute] will return true for + /// them. They can be detected using [isRootRelative]. + bool isAbsolute(String path) => style.rootLength(path) > 0; + + /// Returns `true` if [path] is a relative path and `false` if it is absolute. + /// On POSIX systems, absolute paths start with a `/` (forward slash). On + /// Windows, an absolute path starts with `\\`, or a drive letter followed by + /// `:/` or `:\`. + bool isRelative(String path) => !isAbsolute(path); + + /// Returns `true` if [path] is a root-relative path and `false` if it's not. + /// + /// URLs that start with `/` are known as "root-relative", since they're + /// relative to the root of the current URL. Since root-relative paths are + /// still absolute in every other sense, [isAbsolute] will return true for + /// them. They can be detected using [isRootRelative]. + /// + /// No POSIX and Windows paths are root-relative. + bool isRootRelative(String path) => style.isRootRelative(path); + + /// Joins the given path parts into a single path. Example: + /// + /// context.join('path', 'to', 'foo'); // -> 'path/to/foo' + /// + /// If any part ends in a path separator, then a redundant separator will not + /// be added: + /// + /// context.join('path/', 'to', 'foo'); // -> 'path/to/foo' + /// + /// If a part is an absolute path, then anything before that will be ignored: + /// + /// context.join('path', '/to', 'foo'); // -> '/to/foo' + /// + String join(String part1, + [String? part2, + String? part3, + String? part4, + String? part5, + String? part6, + String? part7, + String? part8, + String? part9, + String? part10, + String? part11, + String? part12, + String? part13, + String? part14, + String? part15, + String? part16]) { + final parts = [ + part1, + part2, + part3, + part4, + part5, + part6, + part7, + part8, + part9, + part10, + part11, + part12, + part13, + part14, + part15, + part16, + ]; + _validateArgList('join', parts); + return joinAll(parts.whereType()); + } + + /// Joins the given path parts into a single path. Example: + /// + /// context.joinAll(['path', 'to', 'foo']); // -> 'path/to/foo' + /// + /// If any part ends in a path separator, then a redundant separator will not + /// be added: + /// + /// context.joinAll(['path/', 'to', 'foo']); // -> 'path/to/foo' + /// + /// If a part is an absolute path, then anything before that will be ignored: + /// + /// context.joinAll(['path', '/to', 'foo']); // -> '/to/foo' + /// + /// For a fixed number of parts, [join] is usually terser. + String joinAll(Iterable parts) { + final buffer = StringBuffer(); + var needsSeparator = false; + var isAbsoluteAndNotRootRelative = false; + + for (var part in parts.where((part) => part != '')) { + if (isRootRelative(part) && isAbsoluteAndNotRootRelative) { + // If the new part is root-relative, it preserves the previous root but + // replaces the path after it. + final parsed = _parse(part); + final path = buffer.toString(); + parsed.root = + path.substring(0, style.rootLength(path, withDrive: true)); + if (style.needsSeparator(parsed.root!)) { + parsed.separators[0] = style.separator; + } + buffer.clear(); + buffer.write(parsed.toString()); + } else if (isAbsolute(part)) { + isAbsoluteAndNotRootRelative = !isRootRelative(part); + // An absolute path discards everything before it. + buffer.clear(); + buffer.write(part); + } else { + if (part.isNotEmpty && style.containsSeparator(part[0])) { + // The part starts with a separator, so we don't need to add one. + } else if (needsSeparator) { + buffer.write(separator); + } + + buffer.write(part); + } + + // Unless this part ends with a separator, we'll need to add one before + // the next part. + needsSeparator = style.needsSeparator(part); + } + + return buffer.toString(); + } + + /// Splits [path] into its components using the current platform's + /// [separator]. Example: + /// + /// context.split('path/to/foo'); // -> ['path', 'to', 'foo'] + /// + /// The path will *not* be normalized before splitting. + /// + /// context.split('path/../foo'); // -> ['path', '..', 'foo'] + /// + /// If [path] is absolute, the root directory will be the first element in the + /// array. Example: + /// + /// // Unix + /// context.split('/path/to/foo'); // -> ['/', 'path', 'to', 'foo'] + /// + /// // Windows + /// context.split(r'C:\path\to\foo'); // -> [r'C:\', 'path', 'to', 'foo'] + /// context.split(r'\\server\share\path\to\foo'); + /// // -> [r'\\server\share', 'path', 'to', 'foo'] + /// + /// // Browser + /// context.split('https://dart.dev/path/to/foo'); + /// // -> ['https://dart.dev', 'path', 'to', 'foo'] + List split(String path) { + final parsed = _parse(path); + // Filter out empty parts that exist due to multiple separators in a row. + parsed.parts = parsed.parts.where((part) => part.isNotEmpty).toList(); + if (parsed.root != null) parsed.parts.insert(0, parsed.root!); + return parsed.parts; + } + + /// Canonicalizes [path]. + /// + /// This is guaranteed to return the same path for two different input paths + /// if and only if both input paths point to the same location. Unlike + /// [normalize], it returns absolute paths when possible and canonicalizes + /// ASCII case on Windows. + /// + /// Note that this does not resolve symlinks. + /// + /// If you want a map that uses path keys, it's probably more efficient to use + /// a Map with [equals] and [hash] specified as the callbacks to use for keys + /// than it is to canonicalize every key. + String canonicalize(String path) { + path = absolute(path); + if (style != Style.windows && !_needsNormalization(path)) return path; + + final parsed = _parse(path); + parsed.normalize(canonicalize: true); + return parsed.toString(); + } + + /// Normalizes [path], simplifying it by handling `..`, and `.`, and + /// removing redundant path separators whenever possible. + /// + /// Note that this is *not* guaranteed to return the same result for two + /// equivalent input paths. For that, see [canonicalize]. Or, if you're using + /// paths as map keys use [equals] and [hash] as the key callbacks. + /// + /// context.normalize('path/./to/..//file.text'); // -> 'path/file.txt' + String normalize(String path) { + if (!_needsNormalization(path)) return path; + + final parsed = _parse(path); + parsed.normalize(); + return parsed.toString(); + } + + /// Returns whether [path] needs to be normalized. + bool _needsNormalization(String path) { + var start = 0; + final codeUnits = path.codeUnits; + int? previousPrevious; + int? previous; + + // Skip past the root before we start looking for snippets that need + // normalization. We want to normalize "//", but not when it's part of + // "http://". + final root = style.rootLength(path); + if (root != 0) { + start = root; + previous = chars.slash; + + // On Windows, the root still needs to be normalized if it contains a + // forward slash. + if (style == Style.windows) { + for (var i = 0; i < root; i++) { + if (codeUnits[i] == chars.slash) return true; + } + } + } + + for (var i = start; i < codeUnits.length; i++) { + final codeUnit = codeUnits[i]; + if (style.isSeparator(codeUnit)) { + // Forward slashes in Windows paths are normalized to backslashes. + if (style == Style.windows && codeUnit == chars.slash) return true; + + // Multiple separators are normalized to single separators. + if (previous != null && style.isSeparator(previous)) return true; + + // Single dots and double dots are normalized to directory traversals. + // + // This can return false positives for ".../", but that's unlikely + // enough that it's probably not going to cause performance issues. + if (previous == chars.period && + (previousPrevious == null || + previousPrevious == chars.period || + style.isSeparator(previousPrevious))) { + return true; + } + } + + previousPrevious = previous; + previous = codeUnit; + } + + // Empty paths are normalized to ".". + if (previous == null) return true; + + // Trailing separators are removed. + if (style.isSeparator(previous)) return true; + + // Single dots and double dots are normalized to directory traversals. + if (previous == chars.period && + (previousPrevious == null || + style.isSeparator(previousPrevious) || + previousPrevious == chars.period)) { + return true; + } + + return false; + } + + /// Attempts to convert [path] to an equivalent relative path relative to + /// [current]. + /// + /// var context = Context(current: '/root/path'); + /// context.relative('/root/path/a/b.dart'); // -> 'a/b.dart' + /// context.relative('/root/other.dart'); // -> '../other.dart' + /// + /// If the [from] argument is passed, [path] is made relative to that instead. + /// + /// context.relative('/root/path/a/b.dart', + /// from: '/root/path'); // -> 'a/b.dart' + /// context.relative('/root/other.dart', + /// from: '/root/path'); // -> '../other.dart' + /// + /// If [path] and/or [from] are relative paths, they are assumed to be + /// relative to [current]. + /// + /// Since there is no relative path from one drive letter to another on + /// Windows, this will return an absolute path in that case. + /// + /// context.relative(r'D:\other', from: r'C:\other'); // -> 'D:\other' + /// + /// This will also return an absolute path if an absolute [path] is passed to + /// a context with a relative path for [current]. + /// + /// var context = Context(r'some/relative/path'); + /// context.relative(r'/absolute/path'); // -> '/absolute/path' + /// + /// If [current] is relative, it may be impossible to determine a path from + /// [from] to [path]. For example, if [current] and [path] are "." and [from] + /// is "/", no path can be determined. In this case, a [PathException] will be + /// thrown. + String relative(String path, {String? from}) { + // Avoid expensive computation if the path is already relative. + if (from == null && isRelative(path)) return normalize(path); + + from = from == null ? current : absolute(from); + + // We can't determine the path from a relative path to an absolute path. + if (isRelative(from) && isAbsolute(path)) { + return normalize(path); + } + + // If the given path is relative, resolve it relative to the context's + // current directory. + if (isRelative(path) || isRootRelative(path)) { + path = absolute(path); + } + + // If the path is still relative and `from` is absolute, we're unable to + // find a path from `from` to `path`. + if (isRelative(path) && isAbsolute(from)) { + throw PathException('Unable to find a path to "$path" from "$from".'); + } + + final fromParsed = _parse(from)..normalize(); + final pathParsed = _parse(path)..normalize(); + + if (fromParsed.parts.isNotEmpty && fromParsed.parts[0] == '.') { + return pathParsed.toString(); + } + + // If the root prefixes don't match (for example, different drive letters + // on Windows), then there is no relative path, so just return the absolute + // one. In Windows, drive letters are case-insenstive and we allow + // calculation of relative paths, even if a path has not been normalized. + if (fromParsed.root != pathParsed.root && + ((fromParsed.root == null || pathParsed.root == null) || + !style.pathsEqual(fromParsed.root!, pathParsed.root!))) { + return pathParsed.toString(); + } + + // Strip off their common prefix. + while (fromParsed.parts.isNotEmpty && + pathParsed.parts.isNotEmpty && + style.pathsEqual(fromParsed.parts[0], pathParsed.parts[0])) { + fromParsed.parts.removeAt(0); + fromParsed.separators.removeAt(1); + pathParsed.parts.removeAt(0); + pathParsed.separators.removeAt(1); + } + + // If there are any directories left in the from path, we need to walk up + // out of them. If a directory left in the from path is '..', it cannot + // be cancelled by adding a '..'. + if (fromParsed.parts.isNotEmpty && fromParsed.parts[0] == '..') { + throw PathException('Unable to find a path to "$path" from "$from".'); + } + pathParsed.parts.insertAll(0, List.filled(fromParsed.parts.length, '..')); + pathParsed.separators[0] = ''; + pathParsed.separators + .insertAll(1, List.filled(fromParsed.parts.length, style.separator)); + + // Corner case: the paths completely collapsed. + if (pathParsed.parts.isEmpty) return '.'; + + // Corner case: path was '.' and some '..' directories were added in front. + // Don't add a final '/.' in that case. + if (pathParsed.parts.length > 1 && pathParsed.parts.last == '.') { + pathParsed.parts.removeLast(); + pathParsed.separators + ..removeLast() + ..removeLast() + ..add(''); + } + + // Make it relative. + pathParsed.root = ''; + pathParsed.removeTrailingSeparators(); + + return pathParsed.toString(); + } + + /// Returns `true` if [child] is a path beneath `parent`, and `false` + /// otherwise. + /// + /// path.isWithin('/root/path', '/root/path/a'); // -> true + /// path.isWithin('/root/path', '/root/other'); // -> false + /// path.isWithin('/root/path', '/root/path'); // -> false + bool isWithin(String parent, String child) => + _isWithinOrEquals(parent, child) == _PathRelation.within; + + /// Returns `true` if [path1] points to the same location as [path2], and + /// `false` otherwise. + /// + /// The [hash] function returns a hash code that matches these equality + /// semantics. + bool equals(String path1, String path2) => + _isWithinOrEquals(path1, path2) == _PathRelation.equal; + + /// Compares two paths and returns an enum value indicating their relationship + /// to one another. + /// + /// This never returns [_PathRelation.inconclusive]. + _PathRelation _isWithinOrEquals(String parent, String child) { + // Make both paths the same level of relative. We're only able to do the + // quick comparison if both paths are in the same format, and making a path + // absolute is faster than making it relative. + final parentIsAbsolute = isAbsolute(parent); + final childIsAbsolute = isAbsolute(child); + if (parentIsAbsolute && !childIsAbsolute) { + child = absolute(child); + if (style.isRootRelative(parent)) parent = absolute(parent); + } else if (childIsAbsolute && !parentIsAbsolute) { + parent = absolute(parent); + if (style.isRootRelative(child)) child = absolute(child); + } else if (childIsAbsolute && parentIsAbsolute) { + final childIsRootRelative = style.isRootRelative(child); + final parentIsRootRelative = style.isRootRelative(parent); + + if (childIsRootRelative && !parentIsRootRelative) { + child = absolute(child); + } else if (parentIsRootRelative && !childIsRootRelative) { + parent = absolute(parent); + } + } + + final result = _isWithinOrEqualsFast(parent, child); + if (result != _PathRelation.inconclusive) return result; + + String relative; + try { + relative = this.relative(child, from: parent); + } on PathException catch (_) { + // If no relative path from [parent] to [child] is found, [child] + // definitely isn't a child of [parent]. + return _PathRelation.different; + } + + if (!isRelative(relative)) return _PathRelation.different; + if (relative == '.') return _PathRelation.equal; + if (relative == '..') return _PathRelation.different; + return (relative.length >= 3 && + relative.startsWith('..') && + style.isSeparator(relative.codeUnitAt(2))) + ? _PathRelation.different + : _PathRelation.within; + } + + /// An optimized implementation of [_isWithinOrEquals] that doesn't handle a + /// few complex cases. + _PathRelation _isWithinOrEqualsFast(String parent, String child) { + // Normally we just bail when we see "." path components, but we can handle + // a single dot easily enough. + if (parent == '.') parent = ''; + + final parentRootLength = style.rootLength(parent); + final childRootLength = style.rootLength(child); + + // If the roots aren't the same length, we know both paths are absolute or + // both are root-relative, and thus that the roots are meaningfully + // different. + // + // isWithin("C:/bar", "//foo/bar/baz") //=> false + // isWithin("http://example.com/", "http://google.com/bar") //=> false + if (parentRootLength != childRootLength) return _PathRelation.different; + + // Make sure that the roots are textually the same as well. + // + // isWithin("C:/bar", "D:/bar/baz") //=> false + // isWithin("http://example.com/", "http://example.org/bar") //=> false + for (var i = 0; i < parentRootLength; i++) { + final parentCodeUnit = parent.codeUnitAt(i); + final childCodeUnit = child.codeUnitAt(i); + if (!style.codeUnitsEqual(parentCodeUnit, childCodeUnit)) { + return _PathRelation.different; + } + } + + // Start by considering the last code unit as a separator, since + // semantically we're starting at a new path component even if we're + // comparing relative paths. + var lastCodeUnit = chars.slash; + + /// The index of the last separator in [parent]. + int? lastParentSeparator; + + // Iterate through both paths as long as they're semantically identical. + var parentIndex = parentRootLength; + var childIndex = childRootLength; + while (parentIndex < parent.length && childIndex < child.length) { + var parentCodeUnit = parent.codeUnitAt(parentIndex); + var childCodeUnit = child.codeUnitAt(childIndex); + if (style.codeUnitsEqual(parentCodeUnit, childCodeUnit)) { + if (style.isSeparator(parentCodeUnit)) { + lastParentSeparator = parentIndex; + } + + lastCodeUnit = parentCodeUnit; + parentIndex++; + childIndex++; + continue; + } + + // Ignore multiple separators in a row. + if (style.isSeparator(parentCodeUnit) && + style.isSeparator(lastCodeUnit)) { + lastParentSeparator = parentIndex; + parentIndex++; + continue; + } else if (style.isSeparator(childCodeUnit) && + style.isSeparator(lastCodeUnit)) { + childIndex++; + continue; + } + + // If a dot comes after a separator, it may be a directory traversal + // operator. To check that, we need to know if it's followed by either + // "/" or "./". Otherwise, it's just a normal non-matching character. + // + // isWithin("foo/./bar", "foo/bar/baz") //=> true + // isWithin("foo/bar/../baz", "foo/bar/.foo") //=> false + if (parentCodeUnit == chars.period && style.isSeparator(lastCodeUnit)) { + parentIndex++; + + // We've hit "/." at the end of the parent path, which we can ignore, + // since the paths were equivalent up to this point. + if (parentIndex == parent.length) break; + parentCodeUnit = parent.codeUnitAt(parentIndex); + + // We've hit "/./", which we can ignore. + if (style.isSeparator(parentCodeUnit)) { + lastParentSeparator = parentIndex; + parentIndex++; + continue; + } + + // We've hit "/..", which may be a directory traversal operator that + // we can't handle on the fast track. + if (parentCodeUnit == chars.period) { + parentIndex++; + if (parentIndex == parent.length || + style.isSeparator(parent.codeUnitAt(parentIndex))) { + return _PathRelation.inconclusive; + } + } + + // If this isn't a directory traversal, fall through so we hit the + // normal handling for mismatched paths. + } + + // This is the same logic as above, but for the child path instead of the + // parent. + if (childCodeUnit == chars.period && style.isSeparator(lastCodeUnit)) { + childIndex++; + if (childIndex == child.length) break; + childCodeUnit = child.codeUnitAt(childIndex); + + if (style.isSeparator(childCodeUnit)) { + childIndex++; + continue; + } + + if (childCodeUnit == chars.period) { + childIndex++; + if (childIndex == child.length || + style.isSeparator(child.codeUnitAt(childIndex))) { + return _PathRelation.inconclusive; + } + } + } + + // If we're here, we've hit two non-matching, non-significant characters. + // As long as the remainders of the two paths don't have any unresolved + // ".." components, we can be confident that [child] is not within + // [parent]. + final childDirection = _pathDirection(child, childIndex); + if (childDirection != _PathDirection.belowRoot) { + return _PathRelation.inconclusive; + } + + final parentDirection = _pathDirection(parent, parentIndex); + if (parentDirection != _PathDirection.belowRoot) { + return _PathRelation.inconclusive; + } + + return _PathRelation.different; + } + + // If the child is shorter than the parent, it's probably not within the + // parent. The only exception is if the parent has some weird ".." stuff + // going on, in which case we do the slow check. + // + // isWithin("foo/bar/baz", "foo/bar") //=> false + // isWithin("foo/bar/baz/../..", "foo/bar") //=> true + if (childIndex == child.length) { + if (parentIndex == parent.length || + style.isSeparator(parent.codeUnitAt(parentIndex))) { + lastParentSeparator = parentIndex; + } else { + lastParentSeparator ??= math.max(0, parentRootLength - 1); + } + + final direction = _pathDirection(parent, lastParentSeparator); + if (direction == _PathDirection.atRoot) return _PathRelation.equal; + return direction == _PathDirection.aboveRoot + ? _PathRelation.inconclusive + : _PathRelation.different; + } + + // We've reached the end of the parent path, which means it's time to make a + // decision. Before we do, though, we'll check the rest of the child to see + // what that tells us. + final direction = _pathDirection(child, childIndex); + + // If there are no more components in the child, then it's the same as + // the parent. + // + // isWithin("foo/bar", "foo/bar") //=> false + // isWithin("foo/bar", "foo/bar//") //=> false + // equals("foo/bar", "foo/bar") //=> true + // equals("foo/bar", "foo/bar//") //=> true + if (direction == _PathDirection.atRoot) return _PathRelation.equal; + + // If there are unresolved ".." components in the child, no decision we make + // will be valid. We'll abort and do the slow check instead. + // + // isWithin("foo/bar", "foo/bar/..") //=> false + // isWithin("foo/bar", "foo/bar/baz/bang/../../..") //=> false + // isWithin("foo/bar", "foo/bar/baz/bang/../../../bar/baz") //=> true + if (direction == _PathDirection.aboveRoot) { + return _PathRelation.inconclusive; + } + + // The child is within the parent if and only if we're on a separator + // boundary. + // + // isWithin("foo/bar", "foo/bar/baz") //=> true + // isWithin("foo/bar/", "foo/bar/baz") //=> true + // isWithin("foo/bar", "foo/barbaz") //=> false + return (style.isSeparator(child.codeUnitAt(childIndex)) || + style.isSeparator(lastCodeUnit)) + ? _PathRelation.within + : _PathRelation.different; + } + + // Returns a [_PathDirection] describing the path represented by [codeUnits] + // starting at [index]. + // + // This ignores leading separators. + // + // pathDirection("foo") //=> below root + // pathDirection("foo/bar/../baz") //=> below root + // pathDirection("//foo/bar/baz") //=> below root + // pathDirection("/") //=> at root + // pathDirection("foo/..") //=> at root + // pathDirection("foo/../baz") //=> reaches root + // pathDirection("foo/../..") //=> above root + // pathDirection("foo/../../foo/bar/baz") //=> above root + _PathDirection _pathDirection(String path, int index) { + var depth = 0; + var reachedRoot = false; + var i = index; + while (i < path.length) { + // Ignore initial separators or doubled separators. + while (i < path.length && style.isSeparator(path.codeUnitAt(i))) { + i++; + } + + // If we're at the end, stop. + if (i == path.length) break; + + // Move through the path component to the next separator. + final start = i; + while (i < path.length && !style.isSeparator(path.codeUnitAt(i))) { + i++; + } + + // See if the path component is ".", "..", or a name. + if (i - start == 1 && path.codeUnitAt(start) == chars.period) { + // Don't change the depth. + } else if (i - start == 2 && + path.codeUnitAt(start) == chars.period && + path.codeUnitAt(start + 1) == chars.period) { + // ".." backs out a directory. + depth--; + + // If we work back beyond the root, stop. + if (depth < 0) break; + + // Record that we reached the root so we don't return + // [_PathDirection.belowRoot]. + if (depth == 0) reachedRoot = true; + } else { + // Step inside a directory. + depth++; + } + + // If we're at the end, stop. + if (i == path.length) break; + + // Move past the separator. + i++; + } + + if (depth < 0) return _PathDirection.aboveRoot; + if (depth == 0) return _PathDirection.atRoot; + if (reachedRoot) return _PathDirection.reachesRoot; + return _PathDirection.belowRoot; + } + + /// Returns a hash code for [path] that matches the semantics of [equals]. + /// + /// Note that the same path may have different hash codes in different + /// [Context]s. + int hash(String path) { + // Make [path] absolute to ensure that equivalent relative and absolute + // paths have the same hash code. + path = absolute(path); + + final result = _hashFast(path); + if (result != null) return result; + + final parsed = _parse(path); + parsed.normalize(); + return _hashFast(parsed.toString())!; + } + + /// An optimized implementation of [hash] that doesn't handle internal `..` + /// components. + /// + /// This will handle `..` components that appear at the beginning of the path. + int? _hashFast(String path) { + var hash = 4603; + var beginning = true; + var wasSeparator = true; + for (var i = 0; i < path.length; i++) { + final codeUnit = style.canonicalizeCodeUnit(path.codeUnitAt(i)); + + // Take advantage of the fact that collisions are allowed to ignore + // separators entirely. This lets us avoid worrying about cases like + // multiple trailing slashes. + if (style.isSeparator(codeUnit)) { + wasSeparator = true; + continue; + } + + if (codeUnit == chars.period && wasSeparator) { + // If a dot comes after a separator, it may be a directory traversal + // operator. To check that, we need to know if it's followed by either + // "/" or "./". Otherwise, it's just a normal character. + // + // hash("foo/./bar") == hash("foo/bar") + + // We've hit "/." at the end of the path, which we can ignore. + if (i + 1 == path.length) break; + + final next = path.codeUnitAt(i + 1); + + // We can just ignore "/./", since they don't affect the semantics of + // the path. + if (style.isSeparator(next)) continue; + + // If the path ends with "/.." or contains "/../", we need to + // canonicalize it before we can hash it. We make an exception for ".."s + // at the beginning of the path, since those may appear even in a + // canonicalized path. + if (!beginning && + next == chars.period && + (i + 2 == path.length || + style.isSeparator(path.codeUnitAt(i + 2)))) { + return null; + } + } + + // Make sure [hash] stays under 32 bits even after multiplication. + hash &= 0x3FFFFFF; + hash *= 33; + hash ^= codeUnit; + wasSeparator = false; + beginning = false; + } + return hash; + } + + /// Removes a trailing extension from the last part of [path]. + /// + /// context.withoutExtension('path/to/foo.dart'); // -> 'path/to/foo' + String withoutExtension(String path) { + final parsed = _parse(path); + + for (var i = parsed.parts.length - 1; i >= 0; i--) { + if (parsed.parts[i].isNotEmpty) { + parsed.parts[i] = parsed.basenameWithoutExtension; + break; + } + } + + return parsed.toString(); + } + + /// Returns [path] with the trailing extension set to [extension]. + /// + /// If [path] doesn't have a trailing extension, this just adds [extension] to + /// the end. + /// + /// context.setExtension('path/to/foo.dart', '.js') + /// // -> 'path/to/foo.js' + /// context.setExtension('path/to/foo.dart.js', '.map') + /// // -> 'path/to/foo.dart.map' + /// context.setExtension('path/to/foo', '.js') + /// // -> 'path/to/foo.js' + String setExtension(String path, String extension) => + withoutExtension(path) + extension; + + /// Returns the path represented by [uri], which may be a [String] or a [Uri]. + /// + /// For POSIX and Windows styles, [uri] must be a `file:` URI. For the URL + /// style, this will just convert [uri] to a string. + /// + /// // POSIX + /// context.fromUri('file:///path/to/foo') + /// // -> '/path/to/foo' + /// + /// // Windows + /// context.fromUri('file:///C:/path/to/foo') + /// // -> r'C:\path\to\foo' + /// + /// // URL + /// context.fromUri('https://dart.dev/path/to/foo') + /// // -> 'https://dart.dev/path/to/foo' + /// + /// If [uri] is relative, a relative path will be returned. + /// + /// path.fromUri('path/to/foo'); // -> 'path/to/foo' + String fromUri(Object? uri) => style.pathFromUri(_parseUri(uri!)); + + /// Returns the URI that represents [path]. + /// + /// For POSIX and Windows styles, this will return a `file:` URI. For the URL + /// style, this will just convert [path] to a [Uri]. + /// + /// // POSIX + /// context.toUri('/path/to/foo') + /// // -> Uri.parse('file:///path/to/foo') + /// + /// // Windows + /// context.toUri(r'C:\path\to\foo') + /// // -> Uri.parse('file:///C:/path/to/foo') + /// + /// // URL + /// context.toUri('https://dart.dev/path/to/foo') + /// // -> Uri.parse('https://dart.dev/path/to/foo') + Uri toUri(String path) { + if (isRelative(path)) { + return style.relativePathToUri(path); + } else { + return style.absolutePathToUri(join(current, path)); + } + } + + /// Returns a terse, human-readable representation of [uri]. + /// + /// [uri] can be a [String] or a [Uri]. If it can be made relative to the + /// current working directory, that's done. Otherwise, it's returned as-is. + /// This gracefully handles non-`file:` URIs for [Style.posix] and + /// [Style.windows]. + /// + /// The returned value is meant for human consumption, and may be either URI- + /// or path-formatted. + /// + /// // POSIX + /// var context = Context(current: '/root/path'); + /// context.prettyUri('file:///root/path/a/b.dart'); // -> 'a/b.dart' + /// context.prettyUri('https://dart.dev/'); // -> 'https://dart.dev' + /// + /// // Windows + /// var context = Context(current: r'C:\root\path'); + /// context.prettyUri('file:///C:/root/path/a/b.dart'); // -> r'a\b.dart' + /// context.prettyUri('https://dart.dev/'); // -> 'https://dart.dev' + /// + /// // URL + /// var context = Context(current: 'https://dart.dev/root/path'); + /// context.prettyUri('https://dart.dev/root/path/a/b.dart'); + /// // -> r'a/b.dart' + /// context.prettyUri('file:///root/path'); // -> 'file:///root/path' + String prettyUri(Object? uri) { + final typedUri = _parseUri(uri!); + if (typedUri.scheme == 'file' && style == Style.url) { + return typedUri.toString(); + } else if (typedUri.scheme != 'file' && + typedUri.scheme != '' && + style != Style.url) { + return typedUri.toString(); + } + + final path = normalize(fromUri(typedUri)); + final rel = relative(path); + + // Only return a relative path if it's actually shorter than the absolute + // path. This avoids ugly things like long "../" chains to get to the root + // and then go back down. + return split(rel).length > split(path).length ? path : rel; + } + + ParsedPath _parse(String path) => ParsedPath.parse(path, style); +} + +/// Parses argument if it's a [String] or returns it intact if it's a [Uri]. +/// +/// Throws an [ArgumentError] otherwise. +Uri _parseUri(Object uri) { + if (uri is String) return Uri.parse(uri); + if (uri is Uri) return uri; + throw ArgumentError.value(uri, 'uri', 'Value must be a String or a Uri'); +} + +/// Validates that there are no non-null arguments following a null one and +/// throws an appropriate [ArgumentError] on failure. +void _validateArgList(String method, List args) { + for (var i = 1; i < args.length; i++) { + // Ignore nulls hanging off the end. + if (args[i] == null || args[i - 1] != null) continue; + + int numArgs; + for (numArgs = args.length; numArgs >= 1; numArgs--) { + if (args[numArgs - 1] != null) break; + } + + // Show the arguments. + final message = StringBuffer(); + message.write('$method('); + message.write(args + .take(numArgs) + .map((arg) => arg == null ? 'null' : '"$arg"') + .join(', ')); + message.write('): part ${i - 1} was null, but part $i was not.'); + throw ArgumentError(message.toString()); + } +} + +/// An enum of possible return values for [Context._pathDirection]. +class _PathDirection { + /// The path contains enough ".." components that at some point it reaches + /// above its original root. + /// + /// Note that this applies even if the path ends beneath its original root. It + /// takes precendence over any other return values that may apple. + static const aboveRoot = _PathDirection('above root'); + + /// The path contains enough ".." components that it ends at its original + /// root. + static const atRoot = _PathDirection('at root'); + + /// The path contains enough ".." components that at some point it reaches its + /// original root, but it ends beneath that root. + static const reachesRoot = _PathDirection('reaches root'); + + /// The path never reaches to or above its original root. + static const belowRoot = _PathDirection('below root'); + + final String name; + + const _PathDirection(this.name); + + @override + String toString() => name; +} + +/// An enum of possible return values for [Context._isWithinOrEquals]. +class _PathRelation { + /// The first path is a proper parent of the second. + /// + /// For example, `foo` is a proper parent of `foo/bar`, but not of `foo`. + static const within = _PathRelation('within'); + + /// The two paths are equivalent. + /// + /// For example, `foo//bar` is equivalent to `foo/bar`. + static const equal = _PathRelation('equal'); + + /// The first path is neither a parent of nor equal to the second. + static const different = _PathRelation('different'); + + /// We couldn't quickly determine any information about the paths' + /// relationship to each other. + /// + /// Only returned by [Context._isWithinOrEqualsFast]. + static const inconclusive = _PathRelation('inconclusive'); + + final String name; + + const _PathRelation(this.name); + + @override + String toString() => name; +} diff --git a/pkgs/path/lib/src/internal_style.dart b/pkgs/path/lib/src/internal_style.dart new file mode 100644 index 00000000..71762c16 --- /dev/null +++ b/pkgs/path/lib/src/internal_style.dart @@ -0,0 +1,90 @@ +// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'context.dart'; +import 'style.dart'; + +/// The internal interface for the [Style] type. +/// +/// Users should be able to pass around instances of [Style] like an enum, but +/// the members that [Context] uses should be hidden from them. Those members +/// are defined on this class instead. +abstract class InternalStyle extends Style { + /// The default path separator for this style. + /// + /// On POSIX, this is `/`. On Windows, it's `\`. + @override + String get separator; + + /// Returns whether [path] contains a separator. + bool containsSeparator(String path); + + /// Returns whether [codeUnit] is the character code of a separator. + bool isSeparator(int codeUnit); + + /// Returns whether this path component needs a separator after it. + /// + /// Windows and POSIX styles just need separators when the previous component + /// doesn't already end in a separator, but the URL always needs to place a + /// separator between the root and the first component, even if the root + /// already ends in a separator character. For example, to join "file://" and + /// "usr", an additional "/" is needed (making "file:///usr"). + bool needsSeparator(String path); + + /// Returns the number of characters of the root part. + /// + /// Returns 0 if the path is relative and 1 if the path is root-relative. + /// + /// If [withDrive] is `true`, this should include the drive letter for `file:` + /// URLs. Non-URL styles may ignore the parameter. + int rootLength(String path, {bool withDrive = false}); + + /// Gets the root prefix of [path] if path is absolute. If [path] is relative, + /// returns `null`. + @override + String? getRoot(String path) { + final length = rootLength(path); + if (length > 0) return path.substring(0, length); + return isRootRelative(path) ? path[0] : null; + } + + /// Returns whether [path] is root-relative. + /// + /// If [path] is relative or absolute and not root-relative, returns `false`. + bool isRootRelative(String path); + + /// Returns the path represented by [uri] in this style. + @override + String pathFromUri(Uri uri); + + /// Returns the URI that represents a relative path. + @override + Uri relativePathToUri(String path) { + if (path.isEmpty) return Uri(); + final segments = context.split(path); + + // Ensure that a trailing slash in the path produces a trailing slash in the + // URL. + if (isSeparator(path.codeUnitAt(path.length - 1))) segments.add(''); + return Uri(pathSegments: segments); + } + + /// Returns the URI that represents [path], which is assumed to be absolute. + @override + Uri absolutePathToUri(String path); + + /// Returns whether [codeUnit1] and [codeUnit2] are considered equivalent for + /// this style. + bool codeUnitsEqual(int codeUnit1, int codeUnit2) => codeUnit1 == codeUnit2; + + /// Returns whether [path1] and [path2] are equivalent. + /// + /// This only needs to handle character-by-character comparison; it can assume + /// the paths are normalized and contain no `..` components. + bool pathsEqual(String path1, String path2) => path1 == path2; + + int canonicalizeCodeUnit(int codeUnit) => codeUnit; + + String canonicalizePart(String part) => part; +} diff --git a/pkgs/path/lib/src/parsed_path.dart b/pkgs/path/lib/src/parsed_path.dart new file mode 100644 index 00000000..60fa8491 --- /dev/null +++ b/pkgs/path/lib/src/parsed_path.dart @@ -0,0 +1,210 @@ +// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'internal_style.dart'; +import 'style.dart'; + +class ParsedPath { + /// The [InternalStyle] that was used to parse this path. + InternalStyle style; + + /// The absolute root portion of the path, or `null` if the path is relative. + /// On POSIX systems, this will be `null` or "/". On Windows, it can be + /// `null`, "//" for a UNC path, or something like "C:\" for paths with drive + /// letters. + String? root; + + /// Whether this path is root-relative. + /// + /// See `Context.isRootRelative`. + bool isRootRelative; + + /// The path-separated parts of the path. All but the last will be + /// directories. + List parts; + + /// The path separators preceding each part. + /// + /// The first one will be an empty string unless the root requires a separator + /// between it and the path. The last one will be an empty string unless the + /// path ends with a trailing separator. + List separators; + + /// The file extension of the last non-empty part, or "" if it doesn't have + /// one. + String extension([int level = 1]) => _splitExtension(level)[1]; + + /// `true` if this is an absolute path. + bool get isAbsolute => root != null; + + factory ParsedPath.parse(String path, InternalStyle style) { + // Remove the root prefix, if any. + final root = style.getRoot(path); + final isRootRelative = style.isRootRelative(path); + if (root != null) path = path.substring(root.length); + + // Split the parts on path separators. + final parts = []; + final separators = []; + + var start = 0; + + if (path.isNotEmpty && style.isSeparator(path.codeUnitAt(0))) { + separators.add(path[0]); + start = 1; + } else { + separators.add(''); + } + + for (var i = start; i < path.length; i++) { + if (style.isSeparator(path.codeUnitAt(i))) { + parts.add(path.substring(start, i)); + separators.add(path[i]); + start = i + 1; + } + } + + // Add the final part, if any. + if (start < path.length) { + parts.add(path.substring(start)); + separators.add(''); + } + + return ParsedPath._(style, root, isRootRelative, parts, separators); + } + + ParsedPath._( + this.style, this.root, this.isRootRelative, this.parts, this.separators); + + String get basename { + final copy = clone(); + copy.removeTrailingSeparators(); + if (copy.parts.isEmpty) return root ?? ''; + return copy.parts.last; + } + + String get basenameWithoutExtension => _splitExtension()[0]; + + bool get hasTrailingSeparator => + parts.isNotEmpty && (parts.last == '' || separators.last != ''); + + void removeTrailingSeparators() { + while (parts.isNotEmpty && parts.last == '') { + parts.removeLast(); + separators.removeLast(); + } + if (separators.isNotEmpty) separators[separators.length - 1] = ''; + } + + void normalize({bool canonicalize = false}) { + // Handle '.', '..', and empty parts. + var leadingDoubles = 0; + final newParts = []; + for (var part in parts) { + if (part == '.' || part == '') { + // Do nothing. Ignore it. + } else if (part == '..') { + // Pop the last part off. + if (newParts.isNotEmpty) { + newParts.removeLast(); + } else { + // Backed out past the beginning, so preserve the "..". + leadingDoubles++; + } + } else { + newParts.add(canonicalize ? style.canonicalizePart(part) : part); + } + } + + // A relative path can back out from the start directory. + if (!isAbsolute) { + newParts.insertAll(0, List.filled(leadingDoubles, '..')); + } + + // If we collapsed down to nothing, do ".". + if (newParts.isEmpty && !isAbsolute) { + newParts.add('.'); + } + + // Canonicalize separators. + parts = newParts; + separators = + List.filled(newParts.length + 1, style.separator, growable: true); + if (!isAbsolute || newParts.isEmpty || !style.needsSeparator(root!)) { + separators[0] = ''; + } + + // Normalize the Windows root if needed. + if (root != null && style == Style.windows) { + if (canonicalize) root = root!.toLowerCase(); + root = root!.replaceAll('/', '\\'); + } + removeTrailingSeparators(); + } + + @override + String toString() { + final builder = StringBuffer(); + if (root != null) builder.write(root); + for (var i = 0; i < parts.length; i++) { + builder.write(separators[i]); + builder.write(parts[i]); + } + builder.write(separators.last); + + return builder.toString(); + } + + /// Returns k-th last index of the `character` in the `path`. + /// + /// If `k` exceeds the count of `character`s in `path`, the left most index + /// of the `character` is returned. + int _kthLastIndexOf(String path, String character, int k) { + var count = 0, leftMostIndexedCharacter = 0; + for (var index = path.length - 1; index >= 0; --index) { + if (path[index] == character) { + leftMostIndexedCharacter = index; + ++count; + if (count == k) { + return index; + } + } + } + return leftMostIndexedCharacter; + } + + /// Splits the last non-empty part of the path into a `[basename, extension]` + /// pair. + /// + /// Takes an optional parameter `level` which makes possible to return + /// multiple extensions having `level` number of dots. If `level` exceeds the + /// number of dots, the path is split at the first most dot. The value of + /// `level` must be greater than 0, else `RangeError` is thrown. + /// + /// Returns a two-element list. The first is the name of the file without any + /// extension. The second is the extension or "" if it has none. + List _splitExtension([int level = 1]) { + if (level <= 0) { + throw RangeError.value( + level, 'level', "level's value must be greater than 0"); + } + + final file = + parts.cast().lastWhere((p) => p != '', orElse: () => null); + + if (file == null) return ['', '']; + if (file == '..') return ['..', '']; + + final lastDot = _kthLastIndexOf(file, '.', level); + + // If there is no dot, or it's the first character, like '.bashrc', it + // doesn't count. + if (lastDot <= 0) return [file, '']; + + return [file.substring(0, lastDot), file.substring(lastDot)]; + } + + ParsedPath clone() => ParsedPath._( + style, root, isRootRelative, List.from(parts), List.from(separators)); +} diff --git a/pkgs/path/lib/src/path_exception.dart b/pkgs/path/lib/src/path_exception.dart new file mode 100644 index 00000000..12a84322 --- /dev/null +++ b/pkgs/path/lib/src/path_exception.dart @@ -0,0 +1,14 @@ +// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +/// An exception class that's thrown when a path operation is unable to be +/// computed accurately. +class PathException implements Exception { + String message; + + PathException(this.message); + + @override + String toString() => 'PathException: $message'; +} diff --git a/pkgs/path/lib/src/path_map.dart b/pkgs/path/lib/src/path_map.dart new file mode 100644 index 00000000..50f4d7e4 --- /dev/null +++ b/pkgs/path/lib/src/path_map.dart @@ -0,0 +1,38 @@ +// Copyright (c) 2018, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:collection'; + +import '../path.dart' as p; + +/// A map whose keys are paths, compared using [p.equals] and [p.hash]. +class PathMap extends MapView { + /// Creates an empty [PathMap] whose keys are compared using `context.equals` + /// and `context.hash`. + /// + /// The [context] defaults to the current path context. + PathMap({p.Context? context}) : super(_create(context)); + + /// Creates a [PathMap] with the same keys and values as [other] whose keys + /// are compared using `context.equals` and `context.hash`. + /// + /// The [context] defaults to the current path context. If multiple keys in + /// [other] represent the same logical path, the last key's value will be + /// used. + PathMap.of(Map other, {p.Context? context}) + : super(_create(context)..addAll(other)); + + /// Creates a map that uses [context] for equality and hashing. + static Map _create(p.Context? context) { + context ??= p.context; + return LinkedHashMap( + equals: (path1, path2) { + if (path1 == null) return path2 == null; + if (path2 == null) return false; + return context!.equals(path1, path2); + }, + hashCode: (path) => path == null ? 0 : context!.hash(path), + isValidKey: (path) => path is String || path == null); + } +} diff --git a/pkgs/path/lib/src/path_set.dart b/pkgs/path/lib/src/path_set.dart new file mode 100644 index 00000000..424c8a1e --- /dev/null +++ b/pkgs/path/lib/src/path_set.dart @@ -0,0 +1,99 @@ +// Copyright (c) 2018, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:collection'; + +import '../path.dart' as p; + +/// A set containing paths, compared using [p.equals] and [p.hash]. +class PathSet extends IterableBase implements Set { + /// The set to which we forward implementation methods. + final Set _inner; + + /// Creates an empty [PathSet] whose contents are compared using + /// `context.equals` and `context.hash`. + /// + /// The [context] defaults to the current path context. + PathSet({p.Context? context}) : _inner = _create(context); + + /// Creates a [PathSet] with the same contents as [other] whose elements are + /// compared using `context.equals` and `context.hash`. + /// + /// The [context] defaults to the current path context. If multiple elements + /// in [other] represent the same logical path, the first value will be + /// used. + PathSet.of(Iterable other, {p.Context? context}) + : _inner = _create(context)..addAll(other); + + /// Creates a set that uses [context] for equality and hashing. + static Set _create(p.Context? context) { + context ??= p.context; + return LinkedHashSet( + equals: (path1, path2) { + if (path1 == null) return path2 == null; + if (path2 == null) return false; + return context!.equals(path1, path2); + }, + hashCode: (path) => path == null ? 0 : context!.hash(path), + isValidKey: (path) => path is String || path == null); + } + + // Normally we'd use DelegatingSetView from the collection package to + // implement these, but we want to avoid adding dependencies from path because + // it's so widely used that even brief version skew can be very painful. + + @override + Iterator get iterator => _inner.iterator; + + @override + int get length => _inner.length; + + @override + bool add(String? value) => _inner.add(value); + + @override + void addAll(Iterable elements) => _inner.addAll(elements); + + @override + Set cast() => _inner.cast(); + + @override + void clear() => _inner.clear(); + + @override + bool contains(Object? element) => _inner.contains(element); + + @override + bool containsAll(Iterable other) => _inner.containsAll(other); + + @override + Set difference(Set other) => _inner.difference(other); + + @override + Set intersection(Set other) => _inner.intersection(other); + + @override + String? lookup(Object? element) => _inner.lookup(element); + + @override + bool remove(Object? value) => _inner.remove(value); + + @override + void removeAll(Iterable elements) => _inner.removeAll(elements); + + @override + void removeWhere(bool Function(String?) test) => _inner.removeWhere(test); + + @override + void retainAll(Iterable elements) => _inner.retainAll(elements); + + @override + void retainWhere(bool Function(String?) test) => _inner.retainWhere(test); + + @override + Set union(Set other) => _inner.union(other); + + @override + Set toSet() => _inner.toSet(); +} diff --git a/pkgs/path/lib/src/style.dart b/pkgs/path/lib/src/style.dart new file mode 100644 index 00000000..e1b4fece --- /dev/null +++ b/pkgs/path/lib/src/style.dart @@ -0,0 +1,85 @@ +// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'context.dart'; +import 'style/posix.dart'; +import 'style/url.dart'; +import 'style/windows.dart'; + +/// An enum type describing a "flavor" of path. +abstract class Style { + /// POSIX-style paths use "/" (forward slash) as separators. Absolute paths + /// start with "/". Used by UNIX, Linux, Mac OS X, and others. + static final Style posix = PosixStyle(); + + /// Windows paths use `\` (backslash) as separators. Absolute paths start with + /// a drive letter followed by a colon (example, `C:`) or two backslashes + /// (`\\`) for UNC paths. + static final Style windows = WindowsStyle(); + + /// URLs aren't filesystem paths, but they're supported to make it easier to + /// manipulate URL paths in the browser. + /// + /// URLs use "/" (forward slash) as separators. Absolute paths either start + /// with a protocol and optional hostname (e.g. `https://dart.dev`, + /// `file://`) or with "/". + static final Style url = UrlStyle(); + + /// The style of the host platform. + /// + /// When running on the command line, this will be [windows] or [posix] based + /// on the host operating system. On a browser, this will be [url]. + static final Style platform = _getPlatformStyle(); + + /// Gets the type of the host platform. + static Style _getPlatformStyle() { + // If we're running a Dart file in the browser from a `file:` URI, + // [Uri.base] will point to a file. If we're running on the standalone, + // it will point to a directory. We can use that fact to determine which + // style to use. + if (Uri.base.scheme != 'file') return Style.url; + if (!Uri.base.path.endsWith('/')) return Style.url; + if (Uri(path: 'a/b').toFilePath() == 'a\\b') return Style.windows; + return Style.posix; + } + + /// The name of this path style. Will be "posix" or "windows". + String get name; + + /// A [Context] that uses this style. + Context get context => Context(style: this); + + @Deprecated('Most Style members will be removed in path 2.0.') + String get separator; + + @Deprecated('Most Style members will be removed in path 2.0.') + Pattern get separatorPattern; + + @Deprecated('Most Style members will be removed in path 2.0.') + Pattern get needsSeparatorPattern; + + @Deprecated('Most Style members will be removed in path 2.0.') + Pattern get rootPattern; + + @Deprecated('Most Style members will be removed in path 2.0.') + Pattern? get relativeRootPattern; + + @Deprecated('Most style members will be removed in path 2.0.') + String? getRoot(String path); + + @Deprecated('Most style members will be removed in path 2.0.') + String? getRelativeRoot(String path); + + @Deprecated('Most style members will be removed in path 2.0.') + String pathFromUri(Uri uri); + + @Deprecated('Most style members will be removed in path 2.0.') + Uri relativePathToUri(String path); + + @Deprecated('Most style members will be removed in path 2.0.') + Uri absolutePathToUri(String path); + + @override + String toString() => name; +} diff --git a/pkgs/path/lib/src/style/posix.dart b/pkgs/path/lib/src/style/posix.dart new file mode 100644 index 00000000..f88e335c --- /dev/null +++ b/pkgs/path/lib/src/style/posix.dart @@ -0,0 +1,74 @@ +// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import '../characters.dart' as chars; +import '../internal_style.dart'; +import '../parsed_path.dart'; + +/// The style for POSIX paths. +class PosixStyle extends InternalStyle { + @override + final name = 'posix'; + @override + final separator = '/'; + final separators = const ['/']; + + // Deprecated properties. + + @override + final separatorPattern = RegExp(r'/'); + @override + final needsSeparatorPattern = RegExp(r'[^/]$'); + @override + final rootPattern = RegExp(r'^/'); + @override + Pattern? get relativeRootPattern => null; + + @override + bool containsSeparator(String path) => path.contains('/'); + + @override + bool isSeparator(int codeUnit) => codeUnit == chars.slash; + + @override + bool needsSeparator(String path) => + path.isNotEmpty && !isSeparator(path.codeUnitAt(path.length - 1)); + + @override + int rootLength(String path, {bool withDrive = false}) { + if (path.isNotEmpty && isSeparator(path.codeUnitAt(0))) return 1; + return 0; + } + + @override + bool isRootRelative(String path) => false; + + @override + String? getRelativeRoot(String path) => null; + + @override + String pathFromUri(Uri uri) { + if (uri.scheme == '' || uri.scheme == 'file') { + return Uri.decodeComponent(uri.path); + } + throw ArgumentError("Uri $uri must have scheme 'file:'."); + } + + @override + Uri absolutePathToUri(String path) { + final parsed = ParsedPath.parse(path, this); + if (parsed.parts.isEmpty) { + // If the path is a bare root (e.g. "/"), [components] will + // currently be empty. We add two empty components so the URL constructor + // produces "file:///", with a trailing slash. + parsed.parts.addAll(['', '']); + } else if (parsed.hasTrailingSeparator) { + // If the path has a trailing slash, add a single empty component so the + // URI has a trailing slash as well. + parsed.parts.add(''); + } + + return Uri(scheme: 'file', pathSegments: parsed.parts); + } +} diff --git a/pkgs/path/lib/src/style/url.dart b/pkgs/path/lib/src/style/url.dart new file mode 100644 index 00000000..a2d3b0ce --- /dev/null +++ b/pkgs/path/lib/src/style/url.dart @@ -0,0 +1,88 @@ +// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import '../characters.dart' as chars; +import '../internal_style.dart'; +import '../utils.dart'; + +/// The style for URL paths. +class UrlStyle extends InternalStyle { + @override + final name = 'url'; + @override + final separator = '/'; + final separators = const ['/']; + + // Deprecated properties. + + @override + final separatorPattern = RegExp(r'/'); + @override + final needsSeparatorPattern = RegExp(r'(^[a-zA-Z][-+.a-zA-Z\d]*://|[^/])$'); + @override + final rootPattern = RegExp(r'[a-zA-Z][-+.a-zA-Z\d]*://[^/]*'); + @override + final relativeRootPattern = RegExp(r'^/'); + + @override + bool containsSeparator(String path) => path.contains('/'); + + @override + bool isSeparator(int codeUnit) => codeUnit == chars.slash; + + @override + bool needsSeparator(String path) { + if (path.isEmpty) return false; + + // A URL that doesn't end in "/" always needs a separator. + if (!isSeparator(path.codeUnitAt(path.length - 1))) return true; + + // A URI that's just "scheme://" needs an extra separator, despite ending + // with "/". + return path.endsWith('://') && rootLength(path) == path.length; + } + + @override + int rootLength(String path, {bool withDrive = false}) { + if (path.isEmpty) return 0; + if (isSeparator(path.codeUnitAt(0))) return 1; + + for (var i = 0; i < path.length; i++) { + final codeUnit = path.codeUnitAt(i); + if (isSeparator(codeUnit)) return 0; + if (codeUnit == chars.colon) { + if (i == 0) return 0; + + // The root part is up until the next '/', or the full path. Skip ':' + // (and '//' if it exists) and search for '/' after that. + if (path.startsWith('//', i + 1)) i += 3; + final index = path.indexOf('/', i); + if (index <= 0) return path.length; + + // file: URLs sometimes consider Windows drive letters part of the root. + // See https://url.spec.whatwg.org/#file-slash-state. + if (!withDrive || path.length < index + 3) return index; + if (!path.startsWith('file://')) return index; + return driveLetterEnd(path, index + 1) ?? index; + } + } + + return 0; + } + + @override + bool isRootRelative(String path) => + path.isNotEmpty && isSeparator(path.codeUnitAt(0)); + + @override + String? getRelativeRoot(String path) => isRootRelative(path) ? '/' : null; + + @override + String pathFromUri(Uri uri) => uri.toString(); + + @override + Uri relativePathToUri(String path) => Uri.parse(path); + @override + Uri absolutePathToUri(String path) => Uri.parse(path); +} diff --git a/pkgs/path/lib/src/style/windows.dart b/pkgs/path/lib/src/style/windows.dart new file mode 100644 index 00000000..b7542c6d --- /dev/null +++ b/pkgs/path/lib/src/style/windows.dart @@ -0,0 +1,182 @@ +// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import '../characters.dart' as chars; +import '../internal_style.dart'; +import '../parsed_path.dart'; +import '../utils.dart'; + +// `0b100000` can be bitwise-ORed with uppercase ASCII letters to get their +// lowercase equivalents. +const _asciiCaseBit = 0x20; + +/// The style for Windows paths. +class WindowsStyle extends InternalStyle { + @override + final name = 'windows'; + @override + final separator = '\\'; + final separators = const ['/', '\\']; + + // Deprecated properties. + + @override + final separatorPattern = RegExp(r'[/\\]'); + @override + final needsSeparatorPattern = RegExp(r'[^/\\]$'); + @override + final rootPattern = RegExp(r'^(\\\\[^\\]+\\[^\\/]+|[a-zA-Z]:[/\\])'); + @override + final relativeRootPattern = RegExp(r'^[/\\](?![/\\])'); + + @override + bool containsSeparator(String path) => path.contains('/'); + + @override + bool isSeparator(int codeUnit) => + codeUnit == chars.slash || codeUnit == chars.backslash; + + @override + bool needsSeparator(String path) { + if (path.isEmpty) return false; + return !isSeparator(path.codeUnitAt(path.length - 1)); + } + + @override + int rootLength(String path, {bool withDrive = false}) { + if (path.isEmpty) return 0; + if (path.codeUnitAt(0) == chars.slash) return 1; + if (path.codeUnitAt(0) == chars.backslash) { + if (path.length < 2 || path.codeUnitAt(1) != chars.backslash) return 1; + // The path is a network share. Search for up to two '\'s, as they are + // the server and share - and part of the root part. + var index = path.indexOf('\\', 2); + if (index > 0) { + index = path.indexOf('\\', index + 1); + if (index > 0) return index; + } + return path.length; + } + // If the path is of the form 'C:/' or 'C:\', with C being any letter, it's + // a root part. + if (path.length < 3) return 0; + // Check for the letter. + if (!isAlphabetic(path.codeUnitAt(0))) return 0; + // Check for the ':'. + if (path.codeUnitAt(1) != chars.colon) return 0; + // Check for either '/' or '\'. + if (!isSeparator(path.codeUnitAt(2))) return 0; + return 3; + } + + @override + bool isRootRelative(String path) => rootLength(path) == 1; + + @override + String? getRelativeRoot(String path) { + final length = rootLength(path); + if (length == 1) return path[0]; + return null; + } + + @override + String pathFromUri(Uri uri) { + if (uri.scheme != '' && uri.scheme != 'file') { + throw ArgumentError("Uri $uri must have scheme 'file:'."); + } + + var path = uri.path; + if (uri.host == '') { + // Drive-letter paths look like "file:///C:/path/to/file". The + // replaceFirst removes the extra initial slash. Otherwise, leave the + // slash to match IE's interpretation of "/foo" as a root-relative path. + if (path.length >= 3 && path.startsWith('/') && isDriveLetter(path, 1)) { + path = path.replaceFirst('/', ''); + } + } else { + // Network paths look like "file://hostname/path/to/file". + path = '\\\\${uri.host}$path'; + } + return Uri.decodeComponent(path.replaceAll('/', '\\')); + } + + @override + Uri absolutePathToUri(String path) { + final parsed = ParsedPath.parse(path, this); + if (parsed.root!.startsWith(r'\\')) { + // Network paths become "file://server/share/path/to/file". + + // The root is of the form "\\server\share". We want "server" to be the + // URI host, and "share" to be the first element of the path. + final rootParts = parsed.root!.split('\\').where((part) => part != ''); + parsed.parts.insert(0, rootParts.last); + + if (parsed.hasTrailingSeparator) { + // If the path has a trailing slash, add a single empty component so the + // URI has a trailing slash as well. + parsed.parts.add(''); + } + + return Uri( + scheme: 'file', host: rootParts.first, pathSegments: parsed.parts); + } else { + // Drive-letter paths become "file:///C:/path/to/file". + + // If the path is a bare root (e.g. "C:\"), [parsed.parts] will currently + // be empty. We add an empty component so the URL constructor produces + // "file:///C:/", with a trailing slash. We also add an empty component if + // the URL otherwise has a trailing slash. + if (parsed.parts.isEmpty || parsed.hasTrailingSeparator) { + parsed.parts.add(''); + } + + // Get rid of the trailing "\" in "C:\" because the URI constructor will + // add a separator on its own. + parsed.parts + .insert(0, parsed.root!.replaceAll('/', '').replaceAll('\\', '')); + + return Uri(scheme: 'file', pathSegments: parsed.parts); + } + } + + @override + bool codeUnitsEqual(int codeUnit1, int codeUnit2) { + if (codeUnit1 == codeUnit2) return true; + + /// Forward slashes and backslashes are equivalent on Windows. + if (codeUnit1 == chars.slash) return codeUnit2 == chars.backslash; + if (codeUnit1 == chars.backslash) return codeUnit2 == chars.slash; + + // If this check fails, the code units are definitely different. If it + // succeeds *and* either codeUnit is an ASCII letter, they're equivalent. + if (codeUnit1 ^ codeUnit2 != _asciiCaseBit) return false; + + // Now we just need to verify that one of the code units is an ASCII letter. + final upperCase1 = codeUnit1 | _asciiCaseBit; + return upperCase1 >= chars.lowerA && upperCase1 <= chars.lowerZ; + } + + @override + bool pathsEqual(String path1, String path2) { + if (identical(path1, path2)) return true; + if (path1.length != path2.length) return false; + for (var i = 0; i < path1.length; i++) { + if (!codeUnitsEqual(path1.codeUnitAt(i), path2.codeUnitAt(i))) { + return false; + } + } + return true; + } + + @override + int canonicalizeCodeUnit(int codeUnit) { + if (codeUnit == chars.slash) return chars.backslash; + if (codeUnit < chars.upperA) return codeUnit; + if (codeUnit > chars.upperZ) return codeUnit; + return codeUnit | _asciiCaseBit; + } + + @override + String canonicalizePart(String part) => part.toLowerCase(); +} diff --git a/pkgs/path/lib/src/utils.dart b/pkgs/path/lib/src/utils.dart new file mode 100644 index 00000000..7c01312d --- /dev/null +++ b/pkgs/path/lib/src/utils.dart @@ -0,0 +1,48 @@ +// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'characters.dart' as chars; + +/// Returns whether [char] is the code for an ASCII letter (uppercase or +/// lowercase). +bool isAlphabetic(int char) => + (char >= chars.upperA && char <= chars.upperZ) || + (char >= chars.lowerA && char <= chars.lowerZ); + +/// Returns whether [char] is the code for an ASCII digit. +bool isNumeric(int char) => char >= chars.zero && char <= chars.nine; + +/// Returns whether [path] has a URL-formatted Windows drive letter beginning at +/// [index]. +bool isDriveLetter(String path, int index) => + driveLetterEnd(path, index) != null; + +/// Returns the index of the first character after the drive letter or a +/// URL-formatted path, or `null` if [index] is not the start of a drive letter. +/// A valid drive letter must be followed by a colon and then either a `/` or +/// the end of string. +/// +/// ``` +/// d:/abc => 3 +/// d:/ => 3 +/// d: => 2 +/// d => null +/// ``` +int? driveLetterEnd(String path, int index) { + if (path.length < index + 2) return null; + if (!isAlphabetic(path.codeUnitAt(index))) return null; + if (path.codeUnitAt(index + 1) != chars.colon) { + // If not a raw colon, check for escaped colon + if (path.length < index + 4) return null; + if (path.substring(index + 1, index + 4).toLowerCase() != '%3a') { + return null; + } + // Offset the index to account for the extra 2 characters from the + // colon encoding. + index += 2; + } + if (path.length == index + 2) return index + 2; + if (path.codeUnitAt(index + 2) != chars.slash) return null; + return index + 3; +} diff --git a/pkgs/path/pubspec.yaml b/pkgs/path/pubspec.yaml new file mode 100644 index 00000000..c0502711 --- /dev/null +++ b/pkgs/path/pubspec.yaml @@ -0,0 +1,17 @@ +name: path +version: 1.9.1 +description: >- + A string-based path manipulation library. All of the path operations you know + and love, with solid support for Windows, POSIX (Linux and Mac OS X), and the + web. +repository: https://github.com/dart-lang/core/tree/main/pkgs/path + +topics: + - file-system + +environment: + sdk: ^3.4.0 + +dev_dependencies: + dart_flutter_team_lints: ^3.0.0 + test: ^1.16.6 diff --git a/pkgs/path/test/browser_test.dart b/pkgs/path/test/browser_test.dart new file mode 100644 index 00000000..cddd846d --- /dev/null +++ b/pkgs/path/test/browser_test.dart @@ -0,0 +1,40 @@ +// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +@TestOn('browser') +library; + +import 'dart:html'; + +import 'package:path/path.dart' as path; +import 'package:test/test.dart'; + +void main() { + group('new Context()', () { + test('uses the window location if root and style are omitted', () { + final context = path.Context(); + expect(context.current, + Uri.parse(window.location.href).resolve('.').toString()); + }); + + test('uses "." if root is omitted', () { + final context = path.Context(style: path.Style.platform); + expect(context.current, '.'); + }); + + test('uses the host platform if style is omitted', () { + final context = path.Context(); + expect(context.style, path.Style.platform); + }); + }); + + test('Style.platform is url', () { + expect(path.Style.platform, path.Style.url); + }); + + test('current', () { + expect( + path.current, Uri.parse(window.location.href).resolve('.').toString()); + }); +} diff --git a/pkgs/path/test/io_test.dart b/pkgs/path/test/io_test.dart new file mode 100644 index 00000000..de22caf7 --- /dev/null +++ b/pkgs/path/test/io_test.dart @@ -0,0 +1,105 @@ +// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +@TestOn('vm') +library; + +import 'dart:io' as io; + +import 'package:path/path.dart' as path; +import 'package:test/test.dart'; + +void main() { + group('new Context()', () { + test('uses the current directory if root and style are omitted', () { + final context = path.Context(); + expect(context.current, io.Directory.current.path); + }); + + test('uses "." if root is omitted', () { + final context = path.Context(style: path.Style.platform); + expect(context.current, '.'); + }); + + test('uses the host platform if style is omitted', () { + final context = path.Context(); + expect(context.style, path.Style.platform); + }); + }); + + test('Style.platform returns the host platform style', () { + if (io.Platform.operatingSystem == 'windows') { + expect(path.Style.platform, path.Style.windows); + } else { + expect(path.Style.platform, path.Style.posix); + } + }); + + group('current', () { + test('returns the current working directory', () { + expect(path.current, io.Directory.current.path); + }); + + test('uses the previous working directory if deleted', () { + final dir = io.Directory.current.path; + try { + final temp = io.Directory.systemTemp.createTempSync('path_test'); + final tempPath = temp.resolveSymbolicLinksSync(); + io.Directory.current = temp; + + // Call "current" once so that it can be cached. + expect(path.normalize(path.absolute(path.current)), equals(tempPath)); + + temp.deleteSync(); + + // Even though the directory no longer exists, no exception is thrown. + expect(path.normalize(path.absolute(path.current)), equals(tempPath)); + } finally { + io.Directory.current = dir; + } + }, + //TODO: Figure out why this is failing on windows and fix! + skip: io.Platform.isWindows ? 'Untriaged failure on Windows' : false); + }); + + test('registers changes to the working directory', () { + final dir = io.Directory.current.path; + try { + expect(path.absolute('foo/bar'), equals(path.join(dir, 'foo/bar'))); + expect( + path.absolute('foo/bar'), equals(path.context.join(dir, 'foo/bar'))); + + io.Directory.current = path.dirname(dir); + expect(path.normalize(path.absolute('foo/bar')), + equals(path.normalize(path.join(dir, '../foo/bar')))); + expect(path.normalize(path.absolute('foo/bar')), + equals(path.normalize(path.context.join(dir, '../foo/bar')))); + } finally { + io.Directory.current = dir; + } + }); + + // Regression test for #35. This tests against the *actual* working directory + // rather than just a custom context because we do some processing in + // [path.current] that has clobbered the root in the past. + test('absolute works on root working directory', () { + final dir = path.current; + try { + io.Directory.current = path.rootPrefix(path.current); + + expect(path.relative(path.absolute('foo/bar'), from: path.current), + path.relative(path.absolute('foo/bar'))); + + expect(path.normalize(path.absolute('foo/bar')), + equals(path.normalize(path.join(path.current, '../foo/bar')))); + + expect(path.normalize(path.absolute('foo/bar')), + equals(path.normalize(path.join(path.current, '../foo/bar')))); + } finally { + io.Directory.current = dir; + } + }, + //TODO(kevmoo): figure out why this is failing on windows and fix! + skip: io.Platform.isWindows ? 'Untriaged failure on Windows' : null); +} diff --git a/pkgs/path/test/path_map_test.dart b/pkgs/path/test/path_map_test.dart new file mode 100644 index 00000000..11c4a3e0 --- /dev/null +++ b/pkgs/path/test/path_map_test.dart @@ -0,0 +1,79 @@ +// Copyright (c) 2018, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:path/path.dart'; +import 'package:test/test.dart'; + +void main() { + group('considers equal', () { + test('two identical paths', () { + final map = PathMap(); + map[join('foo', 'bar')] = 1; + map[join('foo', 'bar')] = 2; + expect(map, hasLength(1)); + expect(map, containsPair(join('foo', 'bar'), 2)); + }); + + test('two logically equivalent paths', () { + final map = PathMap(); + map['foo'] = 1; + map[absolute('foo')] = 2; + expect(map, hasLength(1)); + expect(map, containsPair('foo', 2)); + expect(map, containsPair(absolute('foo'), 2)); + }); + + test('two nulls', () { + final map = PathMap(); + map[null] = 1; + map[null] = 2; + expect(map, hasLength(1)); + expect(map, containsPair(null, 2)); + }); + }); + + group('considers unequal', () { + test('two distinct paths', () { + final map = PathMap(); + map['foo'] = 1; + map['bar'] = 2; + expect(map, hasLength(2)); + expect(map, containsPair('foo', 1)); + expect(map, containsPair('bar', 2)); + }); + + test('a path and null', () { + final map = PathMap(); + map['foo'] = 1; + map[null] = 2; + expect(map, hasLength(2)); + expect(map, containsPair('foo', 1)); + expect(map, containsPair(null, 2)); + }); + }); + + test('uses the custom context', () { + final map = PathMap(context: windows); + map['FOO'] = 1; + map['foo'] = 2; + expect(map, hasLength(1)); + expect(map, containsPair('fOo', 2)); + }); + + group('.of()', () { + test("copies the existing map's keys", () { + final map = PathMap.of({'foo': 1, 'bar': 2}); + expect(map, hasLength(2)); + expect(map, containsPair('foo', 1)); + expect(map, containsPair('bar', 2)); + }); + + test('uses the second value in the case of duplicates', () { + final map = PathMap.of({'foo': 1, absolute('foo'): 2}); + expect(map, hasLength(1)); + expect(map, containsPair('foo', 2)); + expect(map, containsPair(absolute('foo'), 2)); + }); + }); +} diff --git a/pkgs/path/test/path_set_test.dart b/pkgs/path/test/path_set_test.dart new file mode 100644 index 00000000..135f9def --- /dev/null +++ b/pkgs/path/test/path_set_test.dart @@ -0,0 +1,80 @@ +// Copyright (c) 2018, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:path/path.dart'; +import 'package:test/test.dart'; + +void main() { + group('considers equal', () { + test('two identical paths', () { + final set = PathSet(); + expect(set.add(join('foo', 'bar')), isTrue); + expect(set.add(join('foo', 'bar')), isFalse); + expect(set, hasLength(1)); + expect(set, contains(join('foo', 'bar'))); + }); + + test('two logically equivalent paths', () { + final set = PathSet(); + expect(set.add('foo'), isTrue); + expect(set.add(absolute('foo')), isFalse); + expect(set, hasLength(1)); + expect(set, contains('foo')); + expect(set, contains(absolute('foo'))); + }); + + test('two nulls', () { + final set = PathSet(); + expect(set.add(null), isTrue); + expect(set.add(null), isFalse); + expect(set, hasLength(1)); + expect(set, contains(null)); + }); + }); + + group('considers unequal', () { + test('two distinct paths', () { + final set = PathSet(); + expect(set.add('foo'), isTrue); + expect(set.add('bar'), isTrue); + expect(set, hasLength(2)); + expect(set, contains('foo')); + expect(set, contains('bar')); + }); + + test('a path and null', () { + final set = PathSet(); + expect(set.add('foo'), isTrue); + expect(set.add(null), isTrue); + expect(set, hasLength(2)); + expect(set, contains('foo')); + expect(set, contains(null)); + }); + }); + + test('uses the custom context', () { + final set = PathSet(context: windows); + expect(set.add('FOO'), isTrue); + expect(set.add('foo'), isFalse); + expect(set, hasLength(1)); + expect(set, contains('fOo')); + }); + + group('.of()', () { + test("copies the existing set's keys", () { + final set = PathSet.of(['foo', 'bar']); + expect(set, hasLength(2)); + expect(set, contains('foo')); + expect(set, contains('bar')); + }); + + test('uses the first value in the case of duplicates', () { + final set = PathSet.of(['foo', absolute('foo')]); + expect(set, hasLength(1)); + expect(set, contains('foo')); + expect(set, contains(absolute('foo'))); + expect(set.first, 'foo'); + }); + }); +} diff --git a/pkgs/path/test/path_test.dart b/pkgs/path/test/path_test.dart new file mode 100644 index 00000000..b99b78b0 --- /dev/null +++ b/pkgs/path/test/path_test.dart @@ -0,0 +1,54 @@ +// Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:path/path.dart' as path; +import 'package:test/test.dart'; + +void main() { + group('path.Style', () { + test('name', () { + expect(path.Style.posix.name, 'posix'); + expect(path.Style.windows.name, 'windows'); + }); + + test('separator', () { + // ignore: deprecated_member_use_from_same_package + expect(path.Style.posix.separator, '/'); + // ignore: deprecated_member_use_from_same_package + expect(path.Style.windows.separator, '\\'); + }); + + test('toString()', () { + expect(path.Style.posix.toString(), 'posix'); + expect(path.Style.windows.toString(), 'windows'); + }); + }); + + group('new Context()', () { + test('uses the given current directory', () { + final context = path.Context(current: '/a/b/c'); + expect(context.current, '/a/b/c'); + }); + + test('uses the given style', () { + final context = path.Context(style: path.Style.windows); + expect(context.style, path.Style.windows); + }); + }); + + test('posix is a default Context for the POSIX style', () { + expect(path.posix.style, path.Style.posix); + expect(path.posix.current, '.'); + }); + + test('windows is a default Context for the Windows style', () { + expect(path.windows.style, path.Style.windows); + expect(path.windows.current, '.'); + }); + + test('url is a default Context for the URL style', () { + expect(path.url.style, path.Style.url); + expect(path.url.current, '.'); + }); +} diff --git a/pkgs/path/test/posix_test.dart b/pkgs/path/test/posix_test.dart new file mode 100644 index 00000000..121b4e3c --- /dev/null +++ b/pkgs/path/test/posix_test.dart @@ -0,0 +1,692 @@ +// Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:path/path.dart' as path; +import 'package:test/test.dart'; + +import 'utils.dart'; + +void main() { + final context = path.Context(style: path.Style.posix, current: '/root/path'); + + test('separator', () { + expect(context.separator, '/'); + }); + + test('extension', () { + expect(context.extension(''), ''); + expect(context.extension('.'), ''); + expect(context.extension('..'), ''); + expect(context.extension('foo.dart'), '.dart'); + expect(context.extension('foo.dart.js'), '.js'); + expect(context.extension('a.b/c'), ''); + expect(context.extension('a.b/c.d'), '.d'); + expect(context.extension('~/.bashrc'), ''); + expect(context.extension(r'a.b\c'), r'.b\c'); + expect(context.extension('foo.dart/'), '.dart'); + expect(context.extension('foo.dart//'), '.dart'); + expect(context.extension('foo.bar.dart.js', 2), '.dart.js'); + expect(context.extension(r'foo.bar.dart.js', 3), '.bar.dart.js'); + expect(context.extension(r'foo.bar.dart.js', 10), '.bar.dart.js'); + expect(context.extension('a.b/c.d', 2), '.d'); + expect(() => context.extension(r'foo.bar.dart.js', 0), throwsRangeError); + expect(() => context.extension(r'foo.bar.dart.js', -1), throwsRangeError); + }); + + test('rootPrefix', () { + expect(context.rootPrefix(''), ''); + expect(context.rootPrefix('a'), ''); + expect(context.rootPrefix('a/b'), ''); + expect(context.rootPrefix('/a/c'), '/'); + expect(context.rootPrefix('/'), '/'); + }); + + test('dirname', () { + expect(context.dirname(''), '.'); + expect(context.dirname('.'), '.'); + expect(context.dirname('..'), '.'); + expect(context.dirname('../..'), '..'); + expect(context.dirname('a'), '.'); + expect(context.dirname('a/b'), 'a'); + expect(context.dirname('a/b/c'), 'a/b'); + expect(context.dirname('a/b.c'), 'a'); + expect(context.dirname('a/'), '.'); + expect(context.dirname('a/.'), 'a'); + expect(context.dirname('a/..'), 'a'); + expect(context.dirname(r'a\b/c'), r'a\b'); + expect(context.dirname('/a'), '/'); + expect(context.dirname('///a'), '/'); + expect(context.dirname('/'), '/'); + expect(context.dirname('///'), '/'); + expect(context.dirname('a/b/'), 'a'); + expect(context.dirname(r'a/b\c'), 'a'); + expect(context.dirname('a//'), '.'); + expect(context.dirname('a/b//'), 'a'); + expect(context.dirname('a//b'), 'a'); + }); + + test('basename', () { + expect(context.basename(''), ''); + expect(context.basename('.'), '.'); + expect(context.basename('..'), '..'); + expect(context.basename('.foo'), '.foo'); + expect(context.basename('a'), 'a'); + expect(context.basename('a/b'), 'b'); + expect(context.basename('a/b/c'), 'c'); + expect(context.basename('a/b.c'), 'b.c'); + expect(context.basename('a/'), 'a'); + expect(context.basename('a/.'), '.'); + expect(context.basename('a/..'), '..'); + expect(context.basename(r'a\b/c'), 'c'); + expect(context.basename('/a'), 'a'); + expect(context.basename('/'), '/'); + expect(context.basename('a/b/'), 'b'); + expect(context.basename(r'a/b\c'), r'b\c'); + expect(context.basename('a//'), 'a'); + expect(context.basename('a/b//'), 'b'); + expect(context.basename('a//b'), 'b'); + }); + + test('basenameWithoutExtension', () { + expect(context.basenameWithoutExtension(''), ''); + expect(context.basenameWithoutExtension('.'), '.'); + expect(context.basenameWithoutExtension('..'), '..'); + expect(context.basenameWithoutExtension('a'), 'a'); + expect(context.basenameWithoutExtension('a/b'), 'b'); + expect(context.basenameWithoutExtension('a/b/c'), 'c'); + expect(context.basenameWithoutExtension('a/b.c'), 'b'); + expect(context.basenameWithoutExtension('a/'), 'a'); + expect(context.basenameWithoutExtension('a/.'), '.'); + expect(context.basenameWithoutExtension(r'a/b\c'), r'b\c'); + expect(context.basenameWithoutExtension('a/.bashrc'), '.bashrc'); + expect(context.basenameWithoutExtension('a/b/c.d.e'), 'c.d'); + expect(context.basenameWithoutExtension('a//'), 'a'); + expect(context.basenameWithoutExtension('a/b//'), 'b'); + expect(context.basenameWithoutExtension('a//b'), 'b'); + expect(context.basenameWithoutExtension('a/b.c/'), 'b'); + expect(context.basenameWithoutExtension('a/b.c//'), 'b'); + expect(context.basenameWithoutExtension('a/b c.d e'), 'b c'); + }); + + test('isAbsolute', () { + expect(context.isAbsolute(''), false); + expect(context.isAbsolute('a'), false); + expect(context.isAbsolute('a/b'), false); + expect(context.isAbsolute('/a'), true); + expect(context.isAbsolute('/a/b'), true); + expect(context.isAbsolute('~'), false); + expect(context.isAbsolute('.'), false); + expect(context.isAbsolute('..'), false); + expect(context.isAbsolute('.foo'), false); + expect(context.isAbsolute('../a'), false); + expect(context.isAbsolute('C:/a'), false); + expect(context.isAbsolute(r'C:\a'), false); + expect(context.isAbsolute(r'\\a'), false); + }); + + test('isRelative', () { + expect(context.isRelative(''), true); + expect(context.isRelative('a'), true); + expect(context.isRelative('a/b'), true); + expect(context.isRelative('/a'), false); + expect(context.isRelative('/a/b'), false); + expect(context.isRelative('~'), true); + expect(context.isRelative('.'), true); + expect(context.isRelative('..'), true); + expect(context.isRelative('.foo'), true); + expect(context.isRelative('../a'), true); + expect(context.isRelative('C:/a'), true); + expect(context.isRelative(r'C:\a'), true); + expect(context.isRelative(r'\\a'), true); + }); + + group('join', () { + test('allows up to sixteen parts', () { + expect(context.join('a'), 'a'); + expect(context.join('a', 'b'), 'a/b'); + expect(context.join('a', 'b', 'c'), 'a/b/c'); + expect(context.join('a', 'b', 'c', 'd'), 'a/b/c/d'); + expect(context.join('a', 'b', 'c', 'd', 'e'), 'a/b/c/d/e'); + expect(context.join('a', 'b', 'c', 'd', 'e', 'f'), 'a/b/c/d/e/f'); + expect(context.join('a', 'b', 'c', 'd', 'e', 'f', 'g'), 'a/b/c/d/e/f/g'); + expect(context.join('a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'), + 'a/b/c/d/e/f/g/h'); + expect(context.join('a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i'), + 'a/b/c/d/e/f/g/h/i'); + expect(context.join('a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'), + 'a/b/c/d/e/f/g/h/i/j'); + expect( + context.join('a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k'), + 'a/b/c/d/e/f/g/h/i/j/k'); + expect( + context.join( + 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l'), + 'a/b/c/d/e/f/g/h/i/j/k/l'); + expect( + context.join( + 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm'), + 'a/b/c/d/e/f/g/h/i/j/k/l/m'); + expect( + context.join('a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', + 'l', 'm', 'n'), + 'a/b/c/d/e/f/g/h/i/j/k/l/m/n'); + expect( + context.join('a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', + 'l', 'm', 'n', 'o'), + 'a/b/c/d/e/f/g/h/i/j/k/l/m/n/o'); + expect( + context.join('a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', + 'l', 'm', 'n', 'o', 'p'), + 'a/b/c/d/e/f/g/h/i/j/k/l/m/n/o/p'); + }); + + test('does not add separator if a part ends in one', () { + expect(context.join('a/', 'b', 'c/', 'd'), 'a/b/c/d'); + expect(context.join('a\\', 'b'), r'a\/b'); + }); + + test('ignores parts before an absolute path', () { + expect(context.join('a', '/', 'b', 'c'), '/b/c'); + expect(context.join('a', '/b', '/c', 'd'), '/c/d'); + expect(context.join('a', r'c:\b', 'c', 'd'), r'a/c:\b/c/d'); + expect(context.join('a', r'\\b', 'c', 'd'), r'a/\\b/c/d'); + }); + + test('ignores trailing nulls', () { + expect(context.join('a', null), equals('a')); + expect(context.join('a', 'b', 'c', null, null), equals('a/b/c')); + }); + + test('ignores empty strings', () { + expect(context.join(''), ''); + expect(context.join('', ''), ''); + expect(context.join('', 'a'), 'a'); + expect(context.join('a', '', 'b', '', '', '', 'c'), 'a/b/c'); + expect(context.join('a', 'b', ''), 'a/b'); + }); + + test('disallows intermediate nulls', () { + expect(() => context.join('a', null, 'b'), throwsArgumentError); + }); + + test('join does not modify internal ., .., or trailing separators', () { + expect(context.join('a/', 'b/c/'), 'a/b/c/'); + expect(context.join('a/b/./c/..//', 'd/.././..//e/f//'), + 'a/b/./c/..//d/.././..//e/f//'); + expect(context.join('a/b', 'c/../../../..'), 'a/b/c/../../../..'); + expect(context.join('a', 'b${context.separator}'), 'a/b/'); + }); + }); + + group('joinAll', () { + test('allows more than sixteen parts', () { + expect( + context.joinAll([ + 'a', + 'b', + 'c', + 'd', + 'e', + 'f', + 'g', + 'h', + 'i', + 'j', + 'k', + 'l', + 'm', + 'n', + 'o', + 'p', + 'q' + ]), + 'a/b/c/d/e/f/g/h/i/j/k/l/m/n/o/p/q'); + }); + + test('does not add separator if a part ends in one', () { + expect(context.joinAll(['a/', 'b', 'c/', 'd']), 'a/b/c/d'); + expect(context.joinAll(['a\\', 'b']), r'a\/b'); + }); + + test('ignores parts before an absolute path', () { + expect(context.joinAll(['a', '/', 'b', 'c']), '/b/c'); + expect(context.joinAll(['a', '/b', '/c', 'd']), '/c/d'); + expect(context.joinAll(['a', r'c:\b', 'c', 'd']), r'a/c:\b/c/d'); + expect(context.joinAll(['a', r'\\b', 'c', 'd']), r'a/\\b/c/d'); + }); + }); + + group('split', () { + test('simple cases', () { + expect(context.split(''), []); + expect(context.split('.'), ['.']); + expect(context.split('..'), ['..']); + expect(context.split('foo'), equals(['foo'])); + expect(context.split('foo/bar.txt'), equals(['foo', 'bar.txt'])); + expect(context.split('foo/bar/baz'), equals(['foo', 'bar', 'baz'])); + expect(context.split('foo/../bar/./baz'), + equals(['foo', '..', 'bar', '.', 'baz'])); + expect(context.split('foo//bar///baz'), equals(['foo', 'bar', 'baz'])); + expect(context.split('foo/\\/baz'), equals(['foo', '\\', 'baz'])); + expect(context.split('.'), equals(['.'])); + expect(context.split(''), equals([])); + expect(context.split('foo/'), equals(['foo'])); + expect(context.split('//'), equals(['/'])); + }); + + test('includes the root for absolute paths', () { + expect(context.split('/foo/bar/baz'), equals(['/', 'foo', 'bar', 'baz'])); + expect(context.split('/'), equals(['/'])); + }); + }); + + group('normalize', () { + test('simple cases', () { + expect(context.normalize(''), '.'); + expect(context.normalize('.'), '.'); + expect(context.normalize('..'), '..'); + expect(context.normalize('a'), 'a'); + expect(context.normalize('/'), '/'); + expect(context.normalize(r'\'), r'\'); + expect(context.normalize('C:/'), 'C:'); + expect(context.normalize(r'C:\'), r'C:\'); + expect(context.normalize(r'\\'), r'\\'); + expect(context.normalize('a/./\xc5\u0bf8-;\u{1f085}\u{00}/c/d/../'), + 'a/\xc5\u0bf8-;\u{1f085}\u{00}/c'); + }); + + test('collapses redundant separators', () { + expect(context.normalize(r'a/b/c'), r'a/b/c'); + expect(context.normalize(r'a//b///c////d'), r'a/b/c/d'); + }); + + test('does not collapse separators for other platform', () { + expect(context.normalize(r'a\\b\\\c'), r'a\\b\\\c'); + }); + + test('eliminates "." parts', () { + expect(context.normalize('./'), '.'); + expect(context.normalize('/.'), '/'); + expect(context.normalize('/./'), '/'); + expect(context.normalize('./.'), '.'); + expect(context.normalize('a/./b'), 'a/b'); + expect(context.normalize('a/.b/c'), 'a/.b/c'); + expect(context.normalize('a/././b/./c'), 'a/b/c'); + expect(context.normalize('././a'), 'a'); + expect(context.normalize('a/./.'), 'a'); + }); + + test('eliminates ".." parts', () { + expect(context.normalize('..'), '..'); + expect(context.normalize('../'), '..'); + expect(context.normalize('../../..'), '../../..'); + expect(context.normalize('../../../'), '../../..'); + expect(context.normalize('/..'), '/'); + expect(context.normalize('/../../..'), '/'); + expect(context.normalize('/../../../a'), '/a'); + expect(context.normalize('c:/..'), '.'); + expect(context.normalize('A:/../../..'), '../..'); + expect(context.normalize('a/..'), '.'); + expect(context.normalize('a/b/..'), 'a'); + expect(context.normalize('a/../b'), 'b'); + expect(context.normalize('a/./../b'), 'b'); + expect(context.normalize('a/b/c/../../d/e/..'), 'a/d'); + expect(context.normalize('a/b/../../../../c'), '../../c'); + expect(context.normalize(r'z/a/b/../../..\../c'), r'z/..\../c'); + expect(context.normalize(r'a/b\c/../d'), 'a/d'); + }); + + test('does not walk before root on absolute paths', () { + expect(context.normalize('..'), '..'); + expect(context.normalize('../'), '..'); + expect(context.normalize('https://dart.dev/..'), 'https:'); + expect(context.normalize('https://dart.dev/../../a'), 'a'); + expect(context.normalize('file:///..'), '.'); + expect(context.normalize('file:///../../a'), '../a'); + expect(context.normalize('/..'), '/'); + expect(context.normalize('a/..'), '.'); + expect(context.normalize('../a'), '../a'); + expect(context.normalize('/../a'), '/a'); + expect(context.normalize('c:/../a'), 'a'); + expect(context.normalize('/../a'), '/a'); + expect(context.normalize('a/b/..'), 'a'); + expect(context.normalize('../a/b/..'), '../a'); + expect(context.normalize('a/../b'), 'b'); + expect(context.normalize('a/./../b'), 'b'); + expect(context.normalize('a/b/c/../../d/e/..'), 'a/d'); + expect(context.normalize('a/b/../../../../c'), '../../c'); + expect(context.normalize('a/b/c/../../..d/./.e/f././'), 'a/..d/.e/f.'); + }); + + test('removes trailing separators', () { + expect(context.normalize('./'), '.'); + expect(context.normalize('.//'), '.'); + expect(context.normalize('a/'), 'a'); + expect(context.normalize('a/b/'), 'a/b'); + expect(context.normalize(r'a/b\'), r'a/b\'); + expect(context.normalize('a/b///'), 'a/b'); + }); + + test('when canonicalizing', () { + expect(context.canonicalize('.'), '/root/path'); + expect(context.canonicalize('foo/bar'), '/root/path/foo/bar'); + expect(context.canonicalize('FoO'), '/root/path/FoO'); + }); + }); + + group('relative', () { + group('from absolute root', () { + test('given absolute path in root', () { + expect(context.relative('/'), '../..'); + expect(context.relative('/root'), '..'); + expect(context.relative('/root/path'), '.'); + expect(context.relative('/root/path/a'), 'a'); + expect(context.relative('/root/path/a/b.txt'), 'a/b.txt'); + expect(context.relative('/root/a/b.txt'), '../a/b.txt'); + }); + + test('given absolute path outside of root', () { + expect(context.relative('/a/b'), '../../a/b'); + expect(context.relative('/root/path/a'), 'a'); + expect(context.relative('/root/path/a/b.txt'), 'a/b.txt'); + expect(context.relative('/root/a/b.txt'), '../a/b.txt'); + }); + + test('given relative path', () { + // The path is considered relative to the root, so it basically just + // normalizes. + expect(context.relative(''), '.'); + expect(context.relative('.'), '.'); + expect(context.relative('a'), 'a'); + expect(context.relative('a/b.txt'), 'a/b.txt'); + expect(context.relative('../a/b.txt'), '../a/b.txt'); + expect(context.relative('a/./b/../c.txt'), 'a/c.txt'); + }); + + test('is case-sensitive', () { + expect(context.relative('/RoOt'), '../../RoOt'); + expect(context.relative('/rOoT/pAtH/a'), '../../rOoT/pAtH/a'); + }); + + // Regression + test('from root-only path', () { + expect(context.relative('/', from: '/'), '.'); + expect(context.relative('/root/path', from: '/'), 'root/path'); + }); + }); + + group('from relative root', () { + final r = path.Context(style: path.Style.posix, current: 'foo/bar'); + + test('given absolute path', () { + expect(r.relative('/'), equals('/')); + expect(r.relative('/a/b'), equals('/a/b')); + }); + + test('given relative path', () { + // The path is considered relative to the root, so it basically just + // normalizes. + expect(r.relative(''), '.'); + expect(r.relative('.'), '.'); + expect(r.relative('..'), '..'); + expect(r.relative('a'), 'a'); + expect(r.relative('a/b.txt'), 'a/b.txt'); + expect(r.relative('../a/b.txt'), '../a/b.txt'); + expect(r.relative('a/./b/../c.txt'), 'a/c.txt'); + }); + }); + + test('from a root with extension', () { + final r = path.Context(style: path.Style.posix, current: '/dir.ext'); + expect(r.relative('/dir.ext/file'), 'file'); + }); + + test('with a root parameter', () { + expect(context.relative('/foo/bar/baz', from: '/foo/bar'), equals('baz')); + expect(context.relative('..', from: '/foo/bar'), equals('../../root')); + expect(context.relative('/foo/bar/baz', from: 'foo/bar'), + equals('../../../../foo/bar/baz')); + expect(context.relative('..', from: 'foo/bar'), equals('../../..')); + }); + + test('with a root parameter and a relative root', () { + final r = path.Context(style: path.Style.posix, current: 'relative/root'); + expect(r.relative('/foo/bar/baz', from: '/foo/bar'), equals('baz')); + expect(() => r.relative('..', from: '/foo/bar'), throwsPathException); + expect( + r.relative('/foo/bar/baz', from: 'foo/bar'), equals('/foo/bar/baz')); + expect(r.relative('..', from: 'foo/bar'), equals('../../..')); + }); + + test('from a . root', () { + final r = path.Context(style: path.Style.posix, current: '.'); + expect(r.relative('/foo/bar/baz'), equals('/foo/bar/baz')); + expect(r.relative('foo/bar/baz'), equals('foo/bar/baz')); + }); + }); + + group('isWithin', () { + test('simple cases', () { + expect(context.isWithin('foo/bar', 'foo/bar'), isFalse); + expect(context.isWithin('foo/bar', 'foo/bar/baz'), isTrue); + expect(context.isWithin('foo/bar', 'foo/baz'), isFalse); + expect(context.isWithin('foo/bar', '../path/foo/bar/baz'), isTrue); + expect(context.isWithin('/', '/foo/bar'), isTrue); + expect(context.isWithin('baz', '/root/path/baz/bang'), isTrue); + expect(context.isWithin('baz', '/root/path/bang/baz'), isFalse); + }); + + test('complex cases', () { + expect(context.isWithin('foo/./bar', 'foo/bar/baz'), isTrue); + expect(context.isWithin('foo//bar', 'foo/bar/baz'), isTrue); + expect(context.isWithin('foo/qux/../bar', 'foo/bar/baz'), isTrue); + expect(context.isWithin('foo/bar', 'foo/bar/baz/../..'), isFalse); + expect(context.isWithin('foo/bar', 'foo/bar///'), isFalse); + expect(context.isWithin('foo/.bar', 'foo/.bar/baz'), isTrue); + expect(context.isWithin('foo/./bar', 'foo/.bar/baz'), isFalse); + expect(context.isWithin('foo/..bar', 'foo/..bar/baz'), isTrue); + expect(context.isWithin('foo/bar', 'foo/bar/baz/..'), isFalse); + expect(context.isWithin('foo/bar', 'foo/bar/baz/../qux'), isTrue); + }); + + test('from a relative root', () { + final r = path.Context(style: path.Style.posix, current: 'foo/bar'); + expect(r.isWithin('.', 'a/b/c'), isTrue); + expect(r.isWithin('.', '../a/b/c'), isFalse); + expect(r.isWithin('.', '../../a/foo/b/c'), isFalse); + expect(r.isWithin('/', '/baz/bang'), isTrue); + expect(r.isWithin('.', '/baz/bang'), isFalse); + }); + }); + + group('equals and hash', () { + test('simple cases', () { + expectEquals(context, 'foo/bar', 'foo/bar'); + expectNotEquals(context, 'foo/bar', 'foo/bar/baz'); + expectNotEquals(context, 'foo/bar', 'foo'); + expectNotEquals(context, 'foo/bar', 'foo/baz'); + expectEquals(context, 'foo/bar', '../path/foo/bar'); + expectEquals(context, '/', '/'); + expectEquals(context, '/', '../..'); + expectEquals(context, 'baz', '/root/path/baz'); + }); + + test('complex cases', () { + expectEquals(context, 'foo/./bar', 'foo/bar'); + expectEquals(context, 'foo//bar', 'foo/bar'); + expectEquals(context, 'foo/qux/../bar', 'foo/bar'); + expectNotEquals(context, 'foo/qux/../bar', 'foo/qux'); + expectNotEquals(context, 'foo/bar', 'foo/bar/baz/../..'); + expectEquals(context, 'foo/bar', 'foo/bar///'); + expectEquals(context, 'foo/.bar', 'foo/.bar'); + expectNotEquals(context, 'foo/./bar', 'foo/.bar'); + expectEquals(context, 'foo/..bar', 'foo/..bar'); + expectNotEquals(context, 'foo/../bar', 'foo/..bar'); + expectEquals(context, 'foo/bar', 'foo/bar/baz/..'); + expectNotEquals(context, 'FoO/bAr', 'foo/bar'); + }); + + test('from a relative root', () { + final r = path.Context(style: path.Style.posix, current: 'foo/bar'); + expectEquals(r, 'a/b', 'a/b'); + expectNotEquals(r, '.', 'foo/bar'); + expectNotEquals(r, '.', '../a/b'); + expectEquals(r, '.', '../bar'); + expectEquals(r, '/baz/bang', '/baz/bang'); + expectNotEquals(r, 'baz/bang', '/baz/bang'); + }); + }); + + group('absolute', () { + test('allows up to fifteen parts', () { + expect(context.absolute('a'), '/root/path/a'); + expect(context.absolute('a', 'b'), '/root/path/a/b'); + expect(context.absolute('a', 'b', 'c'), '/root/path/a/b/c'); + expect(context.absolute('a', 'b', 'c', 'd'), '/root/path/a/b/c/d'); + expect(context.absolute('a', 'b', 'c', 'd', 'e'), '/root/path/a/b/c/d/e'); + expect(context.absolute('a', 'b', 'c', 'd', 'e', 'f'), + '/root/path/a/b/c/d/e/f'); + expect(context.absolute('a', 'b', 'c', 'd', 'e', 'f', 'g'), + '/root/path/a/b/c/d/e/f/g'); + expect(context.absolute('a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'), + '/root/path/a/b/c/d/e/f/g/h'); + expect(context.absolute('a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i'), + '/root/path/a/b/c/d/e/f/g/h/i'); + expect(context.absolute('a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'), + '/root/path/a/b/c/d/e/f/g/h/i/j'); + expect( + context.absolute( + 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k'), + '/root/path/a/b/c/d/e/f/g/h/i/j/k'); + expect( + context.absolute( + 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l'), + '/root/path/a/b/c/d/e/f/g/h/i/j/k/l'); + expect( + context.absolute( + 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm'), + '/root/path/a/b/c/d/e/f/g/h/i/j/k/l/m'); + expect( + context.absolute('a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', + 'k', 'l', 'm', 'n'), + '/root/path/a/b/c/d/e/f/g/h/i/j/k/l/m/n'); + expect( + context.absolute('a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', + 'k', 'l', 'm', 'n', 'o'), + '/root/path/a/b/c/d/e/f/g/h/i/j/k/l/m/n/o'); + }); + + test('does not add separator if a part ends in one', () { + expect(context.absolute('a/', 'b', 'c/', 'd'), '/root/path/a/b/c/d'); + expect(context.absolute(r'a\', 'b'), r'/root/path/a\/b'); + }); + + test('ignores parts before an absolute path', () { + expect(context.absolute('a', '/b', '/c', 'd'), '/c/d'); + expect( + context.absolute('a', r'c:\b', 'c', 'd'), r'/root/path/a/c:\b/c/d'); + expect(context.absolute('a', r'\\b', 'c', 'd'), r'/root/path/a/\\b/c/d'); + }); + }); + + test('withoutExtension', () { + expect(context.withoutExtension(''), ''); + expect(context.withoutExtension('a'), 'a'); + expect(context.withoutExtension('.a'), '.a'); + expect(context.withoutExtension('a.b'), 'a'); + expect(context.withoutExtension('a/b.c'), 'a/b'); + expect(context.withoutExtension('a/b.c.d'), 'a/b.c'); + expect(context.withoutExtension('a/'), 'a/'); + expect(context.withoutExtension('a/b/'), 'a/b/'); + expect(context.withoutExtension('a/.'), 'a/.'); + expect(context.withoutExtension('a/.b'), 'a/.b'); + expect(context.withoutExtension('a.b/c'), 'a.b/c'); + expect(context.withoutExtension(r'a.b\c'), r'a'); + expect(context.withoutExtension(r'a/b\c'), r'a/b\c'); + expect(context.withoutExtension(r'a/b\c.d'), r'a/b\c'); + expect(context.withoutExtension('a/b.c/'), 'a/b/'); + expect(context.withoutExtension('a/b.c//'), 'a/b//'); + }); + + test('setExtension', () { + expect(context.setExtension('', '.x'), '.x'); + expect(context.setExtension('a', '.x'), 'a.x'); + expect(context.setExtension('.a', '.x'), '.a.x'); + expect(context.setExtension('a.b', '.x'), 'a.x'); + expect(context.setExtension('a/b.c', '.x'), 'a/b.x'); + expect(context.setExtension('a/b.c.d', '.x'), 'a/b.c.x'); + expect(context.setExtension('a/', '.x'), 'a/.x'); + expect(context.setExtension('a/b/', '.x'), 'a/b/.x'); + expect(context.setExtension('a/.', '.x'), 'a/..x'); + expect(context.setExtension('a/.b', '.x'), 'a/.b.x'); + expect(context.setExtension('a.b/c', '.x'), 'a.b/c.x'); + expect(context.setExtension(r'a.b\c', '.x'), r'a.x'); + expect(context.setExtension(r'a/b\c', '.x'), r'a/b\c.x'); + expect(context.setExtension(r'a/b\c.d', '.x'), r'a/b\c.x'); + expect(context.setExtension('a/b.c/', '.x'), 'a/b/.x'); + expect(context.setExtension('a/b.c//', '.x'), 'a/b//.x'); + }); + + group('fromUri', () { + test('with a URI', () { + expect(context.fromUri(Uri.parse('file:///path/to/foo')), '/path/to/foo'); + expect( + context.fromUri(Uri.parse('file:///path/to/foo/')), '/path/to/foo/'); + expect(context.fromUri(Uri.parse('file:///')), '/'); + expect(context.fromUri(Uri.parse('foo/bar')), 'foo/bar'); + expect(context.fromUri(Uri.parse('/path/to/foo')), '/path/to/foo'); + expect(context.fromUri(Uri.parse('///path/to/foo')), '/path/to/foo'); + expect(context.fromUri(Uri.parse('file:///path/to/foo%23bar')), + '/path/to/foo#bar'); + expect(context.fromUri(Uri.parse('_%7B_%7D_%60_%5E_%20_%22_%25_')), + r'_{_}_`_^_ _"_%_'); + expect(() => context.fromUri(Uri.parse('https://dart.dev')), + throwsArgumentError); + }); + + test('with a string', () { + expect(context.fromUri('file:///path/to/foo'), '/path/to/foo'); + }); + }); + + test('toUri', () { + expect(context.toUri('/path/to/foo'), Uri.parse('file:///path/to/foo')); + expect(context.toUri('/path/to/foo/'), Uri.parse('file:///path/to/foo/')); + expect(context.toUri('path/to/foo/'), Uri.parse('path/to/foo/')); + expect(context.toUri('/'), Uri.parse('file:///')); + expect(context.toUri('foo/bar'), Uri.parse('foo/bar')); + expect(context.toUri('/path/to/foo#bar'), + Uri.parse('file:///path/to/foo%23bar')); + expect(context.toUri(r'/_{_}_`_^_ _"_%_'), + Uri.parse('file:///_%7B_%7D_%60_%5E_%20_%22_%25_')); + expect(context.toUri(r'_{_}_`_^_ _"_%_'), + Uri.parse('_%7B_%7D_%60_%5E_%20_%22_%25_')); + expect(context.toUri(''), Uri.parse('')); + }); + + group('prettyUri', () { + test('with a file: URI', () { + expect(context.prettyUri('file:///root/path/a/b'), 'a/b'); + expect(context.prettyUri('file:///root/path/a/../b'), 'b'); + expect(context.prettyUri('file:///other/path/a/b'), '/other/path/a/b'); + expect(context.prettyUri('file:///root/other'), '../other'); + }); + + test('with an http: URI', () { + expect(context.prettyUri('https://dart.dev/a/b'), 'https://dart.dev/a/b'); + }); + + test('with a relative URI', () { + expect(context.prettyUri('a/b'), 'a/b'); + }); + + test('with a root-relative URI', () { + expect(context.prettyUri('/a/b'), '/a/b'); + }); + + test('with a Uri object', () { + expect(context.prettyUri(Uri.parse('a/b')), 'a/b'); + }); + }); +} diff --git a/pkgs/path/test/relative_test.dart b/pkgs/path/test/relative_test.dart new file mode 100644 index 00000000..189af73d --- /dev/null +++ b/pkgs/path/test/relative_test.dart @@ -0,0 +1,106 @@ +// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. +// +// Test "relative" on all styles of path.Context, on all platforms. + +import 'package:path/path.dart' as path; +import 'package:test/test.dart'; + +import 'utils.dart'; + +void main() { + relativeTest(path.Context(style: path.Style.posix, current: '.'), '/'); + relativeTest(path.Context(style: path.Style.posix, current: '/'), '/'); + relativeTest( + path.Context(style: path.Style.windows, current: r'd:\'), + r'c:\', + ); + relativeTest(path.Context(style: path.Style.windows, current: '.'), r'c:\'); + relativeTest( + path.Context(style: path.Style.url, current: 'file:///'), + 'http://myserver/', + ); + relativeTest( + path.Context(style: path.Style.url, current: '.'), + 'http://myserver/', + ); + relativeTest(path.Context(style: path.Style.url, current: 'file:///'), '/'); + relativeTest(path.Context(style: path.Style.url, current: '.'), '/'); +} + +void relativeTest(path.Context context, String prefix) { + // Cases where the arguments are absolute paths. + void expectRelative(String result, String pathArg, String fromArg) { + test('relative $pathArg from $fromArg', () { + expect( + context.relative(pathArg, from: fromArg), + context.normalize(result), + ); + }); + } + + group('${context.style}', () { + expectRelative('c/d', '${prefix}a/b/c/d', '${prefix}a/b'); + expectRelative('c/d', '${prefix}a/b/c/d', '${prefix}a/b/'); + expectRelative('.', '${prefix}a', '${prefix}a'); + // Trailing slashes in the inputs have no effect. + expectRelative('../../z/x/y', '${prefix}a/b/z/x/y', '${prefix}a/b/c/d/'); + expectRelative('../../z/x/y', '${prefix}a/b/z/x/y', '${prefix}a/b/c/d'); + expectRelative('../../z/x/y', '${prefix}a/b/z/x/y/', '${prefix}a/b/c/d'); + expectRelative('../../../z/x/y', '${prefix}z/x/y', '${prefix}a/b/c'); + expectRelative('../../../z/x/y', '${prefix}z/x/y', '${prefix}a/b/c/'); + + // Cases where the arguments are relative paths. + expectRelative('c/d', 'a/b/c/d', 'a/b'); + expectRelative('.', 'a/b/c', 'a/b/c'); + expectRelative('.', 'a/d/../b/c', 'a/b/c/'); + expectRelative('.', '', ''); + expectRelative('.', '.', ''); + expectRelative('.', '', '.'); + expectRelative('.', '.', '.'); + expectRelative('.', '..', '..'); + expectRelative('a', 'a', ''); + expectRelative('a', 'a', '.'); + expectRelative('..', '.', 'a'); + expectRelative('.', 'a/b/f/../c', 'a/e/../b/c'); + expectRelative('d', 'a/b/f/../c/d', 'a/e/../b/c'); + expectRelative('..', 'a/b/f/../c', 'a/e/../b/c/e/'); + expectRelative('../..', '', 'a/b/'); + expectRelative('../b/c/d', 'b/c/d/', 'a/'); + expectRelative('../a/b/c', 'x/y/a//b/./f/../c', 'x//y/z'); + + // Case where from is an exact substring of path. + expectRelative('a/b', '${prefix}x/y//a/b', '${prefix}x/y/'); + expectRelative('a/b', 'x/y//a/b', 'x/y/'); + expectRelative('../ya/b', '${prefix}x/ya/b', '${prefix}x/y'); + expectRelative('../ya/b', 'x/ya/b', 'x/y'); + expectRelative('../b', 'x/y/../b', 'x/y/.'); + expectRelative('a/b/c', 'x/y/a//b/./f/../c', 'x/y'); + expectRelative('.', '${prefix}x/y//', '${prefix}x/y/'); + expectRelative('.', '${prefix}x/y/', '${prefix}x/y'); + + if (context.current == '.') { + group('current directory', () { + // Should always throw - no relative path can be constructed. + test('throws', () { + expect(() => context.relative('.', from: '..'), throwsPathException); + expect( + () => context.relative('a/b', from: '../../d'), + throwsPathException, + ); + expect( + () => context.relative('a/b', from: '${prefix}a/b'), + throwsPathException, + ); + }); + + expectRelative('..', '..', '.'); + expectRelative('../../..', '..', 'a/b/'); + + // absolute path relative from a relative path returns the absolute path + expectRelative('${prefix}a/b', '${prefix}a/b', 'c/d'); + }); + } + }); +} diff --git a/pkgs/path/test/url_test.dart b/pkgs/path/test/url_test.dart new file mode 100644 index 00000000..df4e5823 --- /dev/null +++ b/pkgs/path/test/url_test.dart @@ -0,0 +1,990 @@ +// Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:path/path.dart' as path; +import 'package:test/test.dart'; + +import 'utils.dart'; + +void main() { + final context = path.Context( + style: path.Style.url, current: 'https://dart.dev/root/path'); + + test('separator', () { + expect(context.separator, '/'); + }); + + test('extension', () { + expect(context.extension(''), ''); + expect(context.extension('foo.dart'), '.dart'); + expect(context.extension('foo.dart.js'), '.js'); + expect(context.extension('a.b/c'), ''); + expect(context.extension('a.b/c.d'), '.d'); + expect(context.extension(r'a.b\c'), r'.b\c'); + expect(context.extension('foo.dart/'), '.dart'); + expect(context.extension('foo.dart//'), '.dart'); + }); + + test('rootPrefix', () { + expect(context.rootPrefix(''), ''); + expect(context.rootPrefix('a'), ''); + expect(context.rootPrefix('a/b'), ''); + expect(context.rootPrefix('https://dart.dev/a/c'), 'https://dart.dev'); + expect(context.rootPrefix('file:///a/c'), 'file://'); + expect(context.rootPrefix('/a/c'), '/'); + expect(context.rootPrefix('https://dart.dev/'), 'https://dart.dev'); + expect(context.rootPrefix('file:///'), 'file://'); + expect(context.rootPrefix('https://dart.dev'), 'https://dart.dev'); + expect(context.rootPrefix('file://'), 'file://'); + expect(context.rootPrefix('/'), '/'); + expect(context.rootPrefix('foo/bar://'), ''); + expect(context.rootPrefix('package:foo/bar.dart'), 'package:foo'); + expect(context.rootPrefix('foo/bar:baz/qux'), ''); + }); + + test('dirname', () { + expect(context.dirname(''), '.'); + expect(context.dirname('a'), '.'); + expect(context.dirname('a/b'), 'a'); + expect(context.dirname('a/b/c'), 'a/b'); + expect(context.dirname('a/b.c'), 'a'); + expect(context.dirname('a/'), '.'); + expect(context.dirname('a/.'), 'a'); + expect(context.dirname(r'a\b/c'), r'a\b'); + expect(context.dirname('https://dart.dev/a'), 'https://dart.dev'); + expect(context.dirname('file:///a'), 'file://'); + expect(context.dirname('/a'), '/'); + expect(context.dirname('https://dart.dev///a'), 'https://dart.dev'); + expect(context.dirname('file://///a'), 'file://'); + expect(context.dirname('///a'), '/'); + expect(context.dirname('https://dart.dev/'), 'https://dart.dev'); + expect(context.dirname('https://dart.dev'), 'https://dart.dev'); + expect(context.dirname('file:///'), 'file://'); + expect(context.dirname('file://'), 'file://'); + expect(context.dirname('/'), '/'); + expect(context.dirname('https://dart.dev///'), 'https://dart.dev'); + expect(context.dirname('file://///'), 'file://'); + expect(context.dirname('///'), '/'); + expect(context.dirname('a/b/'), 'a'); + expect(context.dirname(r'a/b\c'), 'a'); + expect(context.dirname('a//'), '.'); + expect(context.dirname('a/b//'), 'a'); + expect(context.dirname('a//b'), 'a'); + }); + + test('basename', () { + expect(context.basename(''), ''); + expect(context.basename('a'), 'a'); + expect(context.basename('a/b'), 'b'); + expect(context.basename('a/b/c'), 'c'); + expect(context.basename('a/b.c'), 'b.c'); + expect(context.basename('a/'), 'a'); + expect(context.basename('a/.'), '.'); + expect(context.basename(r'a\b/c'), 'c'); + expect(context.basename('https://dart.dev/a'), 'a'); + expect(context.basename('file:///a'), 'a'); + expect(context.basename('/a'), 'a'); + expect(context.basename('https://dart.dev/'), 'https://dart.dev'); + expect(context.basename('https://dart.dev'), 'https://dart.dev'); + expect(context.basename('file:///'), 'file://'); + expect(context.basename('file://'), 'file://'); + expect(context.basename('/'), '/'); + expect(context.basename('a/b/'), 'b'); + expect(context.basename(r'a/b\c'), r'b\c'); + expect(context.basename('a//'), 'a'); + expect(context.basename('a/b//'), 'b'); + expect(context.basename('a//b'), 'b'); + expect(context.basename('a b/c d.e f'), 'c d.e f'); + }); + + test('basenameWithoutExtension', () { + expect(context.basenameWithoutExtension(''), ''); + expect(context.basenameWithoutExtension('.'), '.'); + expect(context.basenameWithoutExtension('..'), '..'); + expect(context.basenameWithoutExtension('a'), 'a'); + expect(context.basenameWithoutExtension('a/b'), 'b'); + expect(context.basenameWithoutExtension('a/b/c'), 'c'); + expect(context.basenameWithoutExtension('a/b.c'), 'b'); + expect(context.basenameWithoutExtension('a/'), 'a'); + expect(context.basenameWithoutExtension('a/.'), '.'); + expect(context.basenameWithoutExtension(r'a/b\c'), r'b\c'); + expect(context.basenameWithoutExtension('a/.bashrc'), '.bashrc'); + expect(context.basenameWithoutExtension('a/b/c.d.e'), 'c.d'); + expect(context.basenameWithoutExtension('a//'), 'a'); + expect(context.basenameWithoutExtension('a/b//'), 'b'); + expect(context.basenameWithoutExtension('a//b'), 'b'); + expect(context.basenameWithoutExtension('a/b.c/'), 'b'); + expect(context.basenameWithoutExtension('a/b.c//'), 'b'); + expect(context.basenameWithoutExtension('a/b c.d e.f g'), 'b c.d e'); + }); + + test('isAbsolute', () { + expect(context.isAbsolute(''), false); + expect(context.isAbsolute('a'), false); + expect(context.isAbsolute('a/b'), false); + expect(context.isAbsolute('https://dart.dev/a'), true); + expect(context.isAbsolute('file:///a'), true); + expect(context.isAbsolute('/a'), true); + expect(context.isAbsolute('https://dart.dev/a/b'), true); + expect(context.isAbsolute('file:///a/b'), true); + expect(context.isAbsolute('/a/b'), true); + expect(context.isAbsolute('https://dart.dev/'), true); + expect(context.isAbsolute('file:///'), true); + expect(context.isAbsolute('https://dart.dev'), true); + expect(context.isAbsolute('file://'), true); + expect(context.isAbsolute('/'), true); + expect(context.isAbsolute('~'), false); + expect(context.isAbsolute('.'), false); + expect(context.isAbsolute('../a'), false); + expect(context.isAbsolute('C:/a'), true); + expect(context.isAbsolute(r'C:\a'), true); + expect(context.isAbsolute('package:foo/bar.dart'), true); + expect(context.isAbsolute('foo/bar:baz/qux'), false); + expect(context.isAbsolute(r'\\a'), false); + }); + + test('isRelative', () { + expect(context.isRelative(''), true); + expect(context.isRelative('a'), true); + expect(context.isRelative('a/b'), true); + expect(context.isRelative('https://dart.dev/a'), false); + expect(context.isRelative('file:///a'), false); + expect(context.isRelative('/a'), false); + expect(context.isRelative('https://dart.dev/a/b'), false); + expect(context.isRelative('file:///a/b'), false); + expect(context.isRelative('/a/b'), false); + expect(context.isRelative('https://dart.dev/'), false); + expect(context.isRelative('file:///'), false); + expect(context.isRelative('https://dart.dev'), false); + expect(context.isRelative('file://'), false); + expect(context.isRelative('/'), false); + expect(context.isRelative('~'), true); + expect(context.isRelative('.'), true); + expect(context.isRelative('../a'), true); + expect(context.isRelative('C:/a'), false); + expect(context.isRelative(r'C:\a'), false); + expect(context.isRelative(r'package:foo/bar.dart'), false); + expect(context.isRelative('foo/bar:baz/qux'), true); + expect(context.isRelative(r'\\a'), true); + }); + + test('isRootRelative', () { + expect(context.isRootRelative(''), false); + expect(context.isRootRelative('a'), false); + expect(context.isRootRelative('a/b'), false); + expect(context.isRootRelative('https://dart.dev/a'), false); + expect(context.isRootRelative('file:///a'), false); + expect(context.isRootRelative('/a'), true); + expect(context.isRootRelative('https://dart.dev/a/b'), false); + expect(context.isRootRelative('file:///a/b'), false); + expect(context.isRootRelative('/a/b'), true); + expect(context.isRootRelative('https://dart.dev/'), false); + expect(context.isRootRelative('file:///'), false); + expect(context.isRootRelative('https://dart.dev'), false); + expect(context.isRootRelative('file://'), false); + expect(context.isRootRelative('/'), true); + expect(context.isRootRelative('~'), false); + expect(context.isRootRelative('.'), false); + expect(context.isRootRelative('../a'), false); + expect(context.isRootRelative('C:/a'), false); + expect(context.isRootRelative(r'C:\a'), false); + expect(context.isRootRelative(r'package:foo/bar.dart'), false); + expect(context.isRootRelative('foo/bar:baz/qux'), false); + expect(context.isRootRelative(r'\\a'), false); + }); + + group('join', () { + test('allows up to sixteen parts', () { + expect(context.join('a'), 'a'); + expect(context.join('a', 'b'), 'a/b'); + expect(context.join('a', 'b', 'c'), 'a/b/c'); + expect(context.join('a', 'b', 'c', 'd'), 'a/b/c/d'); + expect(context.join('a', 'b', 'c', 'd', 'e'), 'a/b/c/d/e'); + expect(context.join('a', 'b', 'c', 'd', 'e', 'f'), 'a/b/c/d/e/f'); + expect(context.join('a', 'b', 'c', 'd', 'e', 'f', 'g'), 'a/b/c/d/e/f/g'); + expect(context.join('a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'), + 'a/b/c/d/e/f/g/h'); + expect(context.join('a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i'), + 'a/b/c/d/e/f/g/h/i'); + expect(context.join('a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'), + 'a/b/c/d/e/f/g/h/i/j'); + expect( + context.join('a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k'), + 'a/b/c/d/e/f/g/h/i/j/k'); + expect( + context.join( + 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l'), + 'a/b/c/d/e/f/g/h/i/j/k/l'); + expect( + context.join( + 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm'), + 'a/b/c/d/e/f/g/h/i/j/k/l/m'); + expect( + context.join('a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', + 'l', 'm', 'n'), + 'a/b/c/d/e/f/g/h/i/j/k/l/m/n'); + expect( + context.join('a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', + 'l', 'm', 'n', 'o'), + 'a/b/c/d/e/f/g/h/i/j/k/l/m/n/o'); + expect( + context.join('a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', + 'l', 'm', 'n', 'o', 'p'), + 'a/b/c/d/e/f/g/h/i/j/k/l/m/n/o/p'); + }); + + test('does not add separator if a part ends in one', () { + expect(context.join('a/', 'b', 'c/', 'd'), 'a/b/c/d'); + expect(context.join('a\\', 'b'), r'a\/b'); + }); + + test('ignores parts before an absolute path', () { + expect(context.join('a', 'https://dart.dev', 'b', 'c'), + 'https://dart.dev/b/c'); + expect(context.join('a', 'file://', 'b', 'c'), 'file:///b/c'); + expect(context.join('a', '/', 'b', 'c'), '/b/c'); + expect(context.join('a', '/b', 'https://dart.dev/c', 'd'), + 'https://dart.dev/c/d'); + expect( + context.join('a', 'http://google.com/b', 'https://dart.dev/c', 'd'), + 'https://dart.dev/c/d'); + expect(context.join('a', '/b', '/c', 'd'), '/c/d'); + expect(context.join('a', r'c:\b', 'c', 'd'), r'c:\b/c/d'); + expect(context.join('a', 'package:foo/bar', 'c', 'd'), + r'package:foo/bar/c/d'); + expect(context.join('a', r'\\b', 'c', 'd'), r'a/\\b/c/d'); + }); + + test('preserves roots before a root-relative path', () { + expect(context.join('https://dart.dev', 'a', '/b', 'c'), + 'https://dart.dev/b/c'); + expect(context.join('file://', 'a', '/b', 'c'), 'file:///b/c'); + expect(context.join('file://', 'a', '/b', 'c', '/d'), 'file:///d'); + expect(context.join('package:foo/bar.dart', '/baz.dart'), + 'package:foo/baz.dart'); + }); + + test('ignores trailing nulls', () { + expect(context.join('a', null), equals('a')); + expect(context.join('a', 'b', 'c', null, null), equals('a/b/c')); + }); + + test('ignores empty strings', () { + expect(context.join(''), ''); + expect(context.join('', ''), ''); + expect(context.join('', 'a'), 'a'); + expect(context.join('a', '', 'b', '', '', '', 'c'), 'a/b/c'); + expect(context.join('a', 'b', ''), 'a/b'); + }); + + test('disallows intermediate nulls', () { + expect(() => context.join('a', null, 'b'), throwsArgumentError); + }); + + test('does not modify internal ., .., or trailing separators', () { + expect(context.join('a/', 'b/c/'), 'a/b/c/'); + expect(context.join('a/b/./c/..//', 'd/.././..//e/f//'), + 'a/b/./c/..//d/.././..//e/f//'); + expect(context.join('a/b', 'c/../../../..'), 'a/b/c/../../../..'); + expect(context.join('a', 'b${context.separator}'), 'a/b/'); + }); + + test('treats drive letters as part of the root for file: URLs', () { + expect( + context.join('file:///c:/foo/bar', '/baz/qux'), 'file:///c:/baz/qux'); + expect( + context.join('file:///D:/foo/bar', '/baz/qux'), 'file:///D:/baz/qux'); + expect(context.join('file:///c:/', '/baz/qux'), 'file:///c:/baz/qux'); + expect(context.join('file:///c:', '/baz/qux'), 'file:///c:/baz/qux'); + expect(context.join('file://host/c:/foo/bar', '/baz/qux'), + 'file://host/c:/baz/qux'); + }); + + test( + 'treats drive letters as part of the root for file: URLs ' + 'with encoded colons', () { + expect(context.join('file:///c%3A/foo/bar', '/baz/qux'), + 'file:///c%3A/baz/qux'); + expect(context.join('file:///D%3A/foo/bar', '/baz/qux'), + 'file:///D%3A/baz/qux'); + expect(context.join('file:///c%3A/', '/baz/qux'), 'file:///c%3A/baz/qux'); + expect(context.join('file:///c%3A', '/baz/qux'), 'file:///c%3A/baz/qux'); + expect(context.join('file://host/c%3A/foo/bar', '/baz/qux'), + 'file://host/c%3A/baz/qux'); + }); + + test('treats drive letters as normal components for non-file: URLs', () { + expect(context.join('http://foo.com/c:/foo/bar', '/baz/qux'), + 'http://foo.com/baz/qux'); + expect(context.join('misfile:///c:/foo/bar', '/baz/qux'), + 'misfile:///baz/qux'); + expect( + context.join('filer:///c:/foo/bar', '/baz/qux'), 'filer:///baz/qux'); + }); + }); + + group('joinAll', () { + test('allows more than sixteen parts', () { + expect( + context.joinAll([ + 'a', + 'b', + 'c', + 'd', + 'e', + 'f', + 'g', + 'h', + 'i', + 'j', + 'k', + 'l', + 'm', + 'n', + 'o', + 'p', + 'q' + ]), + 'a/b/c/d/e/f/g/h/i/j/k/l/m/n/o/p/q'); + }); + + test('ignores parts before an absolute path', () { + expect(context.joinAll(['a', 'https://dart.dev', 'b', 'c']), + 'https://dart.dev/b/c'); + expect(context.joinAll(['a', 'file://', 'b', 'c']), 'file:///b/c'); + expect(context.joinAll(['a', '/', 'b', 'c']), '/b/c'); + expect(context.joinAll(['a', '/b', 'https://dart.dev/c', 'd']), + 'https://dart.dev/c/d'); + expect( + context + .joinAll(['a', 'http://google.com/b', 'https://dart.dev/c', 'd']), + 'https://dart.dev/c/d'); + expect(context.joinAll(['a', '/b', '/c', 'd']), '/c/d'); + expect(context.joinAll(['a', r'c:\b', 'c', 'd']), r'c:\b/c/d'); + expect(context.joinAll(['a', 'package:foo/bar', 'c', 'd']), + r'package:foo/bar/c/d'); + expect(context.joinAll(['a', r'\\b', 'c', 'd']), r'a/\\b/c/d'); + }); + + test('preserves roots before a root-relative path', () { + expect(context.joinAll(['https://dart.dev', 'a', '/b', 'c']), + 'https://dart.dev/b/c'); + expect(context.joinAll(['file://', 'a', '/b', 'c']), 'file:///b/c'); + expect(context.joinAll(['file://', 'a', '/b', 'c', '/d']), 'file:///d'); + }); + }); + + group('split', () { + test('simple cases', () { + expect(context.split(''), []); + expect(context.split('.'), ['.']); + expect(context.split('..'), ['..']); + expect(context.split('foo'), equals(['foo'])); + expect(context.split('foo/bar.txt'), equals(['foo', 'bar.txt'])); + expect(context.split('foo/bar/baz'), equals(['foo', 'bar', 'baz'])); + expect(context.split('foo/../bar/./baz'), + equals(['foo', '..', 'bar', '.', 'baz'])); + expect(context.split('foo//bar///baz'), equals(['foo', 'bar', 'baz'])); + expect(context.split('foo/\\/baz'), equals(['foo', '\\', 'baz'])); + expect(context.split('.'), equals(['.'])); + expect(context.split(''), equals([])); + expect(context.split('foo/'), equals(['foo'])); + expect(context.split('https://dart.dev//'), equals(['https://dart.dev'])); + expect(context.split('file:////'), equals(['file://'])); + expect(context.split('//'), equals(['/'])); + }); + + test('includes the root for absolute paths', () { + expect(context.split('https://dart.dev/foo/bar/baz'), + equals(['https://dart.dev', 'foo', 'bar', 'baz'])); + expect(context.split('file:///foo/bar/baz'), + equals(['file://', 'foo', 'bar', 'baz'])); + expect(context.split('/foo/bar/baz'), equals(['/', 'foo', 'bar', 'baz'])); + expect(context.split('https://dart.dev/'), equals(['https://dart.dev'])); + expect(context.split('https://dart.dev'), equals(['https://dart.dev'])); + expect(context.split('file:///'), equals(['file://'])); + expect(context.split('file://'), equals(['file://'])); + expect(context.split('/'), equals(['/'])); + }); + }); + + group('normalize', () { + test('simple cases', () { + expect(context.normalize(''), '.'); + expect(context.normalize('.'), '.'); + expect(context.normalize('..'), '..'); + expect(context.normalize('a'), 'a'); + expect(context.normalize('https://dart.dev/'), 'https://dart.dev'); + expect(context.normalize('https://dart.dev'), 'https://dart.dev'); + expect(context.normalize('file://'), 'file://'); + expect(context.normalize('file:///'), 'file://'); + expect(context.normalize('/'), '/'); + expect(context.normalize(r'\'), r'\'); + expect(context.normalize('C:/'), 'C:'); + expect(context.normalize(r'C:\'), r'C:\'); + expect(context.normalize(r'\\'), r'\\'); + expect(context.normalize('a/./\xc5\u0bf8-;\u{1f085}\u{00}/c/d/../'), + 'a/\xc5\u0bf8-;\u{1f085}\u{00}/c'); + }); + + test('collapses redundant separators', () { + expect(context.normalize(r'a/b/c'), r'a/b/c'); + expect(context.normalize(r'a//b///c////d'), r'a/b/c/d'); + }); + + test('does not collapse separators for other platform', () { + expect(context.normalize(r'a\\b\\\c'), r'a\\b\\\c'); + }); + + test('eliminates "." parts', () { + expect(context.normalize('./'), '.'); + expect(context.normalize('https://dart.dev/.'), 'https://dart.dev'); + expect(context.normalize('file:///.'), 'file://'); + expect(context.normalize('/.'), '/'); + expect(context.normalize('https://dart.dev/./'), 'https://dart.dev'); + expect(context.normalize('file:///./'), 'file://'); + expect(context.normalize('/./'), '/'); + expect(context.normalize('./.'), '.'); + expect(context.normalize('a/./b'), 'a/b'); + expect(context.normalize('a/.b/c'), 'a/.b/c'); + expect(context.normalize('a/././b/./c'), 'a/b/c'); + expect(context.normalize('././a'), 'a'); + expect(context.normalize('a/./.'), 'a'); + }); + + test('eliminates ".." parts', () { + expect(context.normalize('..'), '..'); + expect(context.normalize('../'), '..'); + expect(context.normalize('../../..'), '../../..'); + expect(context.normalize('../../../'), '../../..'); + expect(context.normalize('https://dart.dev/..'), 'https://dart.dev'); + expect(context.normalize('file:///..'), 'file://'); + expect(context.normalize('/..'), '/'); + expect( + context.normalize('https://dart.dev/../../..'), 'https://dart.dev'); + expect(context.normalize('file:///../../..'), 'file://'); + expect(context.normalize('/../../..'), '/'); + expect(context.normalize('https://dart.dev/../../../a'), + 'https://dart.dev/a'); + expect(context.normalize('file:///../../../a'), 'file:///a'); + expect(context.normalize('/../../../a'), '/a'); + expect(context.normalize('c:/..'), 'c:'); + expect(context.normalize('package:foo/..'), 'package:foo'); + expect(context.normalize('A:/../../..'), 'A:'); + expect(context.normalize('a/..'), '.'); + expect(context.normalize('a/b/..'), 'a'); + expect(context.normalize('a/../b'), 'b'); + expect(context.normalize('a/./../b'), 'b'); + expect(context.normalize('a/b/c/../../d/e/..'), 'a/d'); + expect(context.normalize('a/b/../../../../c'), '../../c'); + expect(context.normalize('z/a/b/../../..../c'), 'z/..../c'); + expect(context.normalize('a/bc/../d'), 'a/d'); + }); + + test('does not walk before root on absolute paths', () { + expect(context.normalize('..'), '..'); + expect(context.normalize('../'), '..'); + expect(context.normalize('https://dart.dev/..'), 'https://dart.dev'); + expect(context.normalize('https://dart.dev/../a'), 'https://dart.dev/a'); + expect(context.normalize('file:///..'), 'file://'); + expect(context.normalize('file:///../a'), 'file:///a'); + expect(context.normalize('/..'), '/'); + expect(context.normalize('a/..'), '.'); + expect(context.normalize('../a'), '../a'); + expect(context.normalize('/../a'), '/a'); + expect(context.normalize('c:/../a'), 'c:/a'); + expect(context.normalize('package:foo/../a'), 'package:foo/a'); + expect(context.normalize('/../a'), '/a'); + expect(context.normalize('a/b/..'), 'a'); + expect(context.normalize('../a/b/..'), '../a'); + expect(context.normalize('a/../b'), 'b'); + expect(context.normalize('a/./../b'), 'b'); + expect(context.normalize('a/b/c/../../d/e/..'), 'a/d'); + expect(context.normalize('a/b/../../../../c'), '../../c'); + expect(context.normalize('a/b/c/../../..d/./.e/f././'), 'a/..d/.e/f.'); + }); + + test('removes trailing separators', () { + expect(context.normalize('./'), '.'); + expect(context.normalize('.//'), '.'); + expect(context.normalize('a/'), 'a'); + expect(context.normalize('a/b/'), 'a/b'); + expect(context.normalize(r'a/b\'), r'a/b\'); + expect(context.normalize('a/b///'), 'a/b'); + }); + + test('when canonicalizing', () { + expect(context.canonicalize('.'), 'https://dart.dev/root/path'); + expect(context.canonicalize('foo/bar'), + 'https://dart.dev/root/path/foo/bar'); + expect(context.canonicalize('FoO'), 'https://dart.dev/root/path/FoO'); + expect(context.canonicalize('/foo'), 'https://dart.dev/foo'); + expect(context.canonicalize('http://google.com/foo'), + 'http://google.com/foo'); + }); + }); + + group('relative', () { + group('from absolute root', () { + test('given absolute path in root', () { + expect(context.relative('https://dart.dev'), '../..'); + expect(context.relative('https://dart.dev/'), '../..'); + expect(context.relative('/'), '../..'); + expect(context.relative('https://dart.dev/root'), '..'); + expect(context.relative('/root'), '..'); + expect(context.relative('https://dart.dev/root/path'), '.'); + expect(context.relative('/root/path'), '.'); + expect(context.relative('https://dart.dev/root/path/a'), 'a'); + expect(context.relative('/root/path/a'), 'a'); + expect( + context.relative('https://dart.dev/root/path/a/b.txt'), 'a/b.txt'); + expect(context.relative('/root/path/a/b.txt'), 'a/b.txt'); + expect(context.relative('https://dart.dev/root/a/b.txt'), '../a/b.txt'); + expect(context.relative('/root/a/b.txt'), '../a/b.txt'); + }); + + test('given absolute path outside of root', () { + expect(context.relative('https://dart.dev/a/b'), '../../a/b'); + expect(context.relative('/a/b'), '../../a/b'); + expect(context.relative('https://dart.dev/root/path/a'), 'a'); + expect(context.relative('/root/path/a'), 'a'); + expect( + context.relative('https://dart.dev/root/path/a/b.txt'), 'a/b.txt'); + expect( + context.relative('https://dart.dev/root/path/a/b.txt'), 'a/b.txt'); + expect(context.relative('https://dart.dev/root/a/b.txt'), '../a/b.txt'); + }); + + test('given absolute path with different hostname/protocol', () { + expect(context.relative(r'http://google.com/a/b'), + r'http://google.com/a/b'); + expect(context.relative(r'file:///a/b'), r'file:///a/b'); + }); + + test('given relative path', () { + // The path is considered relative to the root, so it basically just + // normalizes. + expect(context.relative(''), '.'); + expect(context.relative('.'), '.'); + expect(context.relative('a'), 'a'); + expect(context.relative('a/b.txt'), 'a/b.txt'); + expect(context.relative('../a/b.txt'), '../a/b.txt'); + expect(context.relative('a/./b/../c.txt'), 'a/c.txt'); + }); + + test('is case-sensitive', () { + expect( + context.relative('HtTps://dart.dev/root'), 'HtTps://dart.dev/root'); + expect( + context.relative('https://DaRt.DeV/root'), 'https://DaRt.DeV/root'); + expect(context.relative('/RoOt'), '../../RoOt'); + expect(context.relative('/rOoT/pAtH/a'), '../../rOoT/pAtH/a'); + }); + + // Regression + test('from root-only path', () { + expect(context.relative('https://dart.dev', from: 'https://dart.dev'), + '.'); + expect( + context.relative('https://dart.dev/root/path', + from: 'https://dart.dev'), + 'root/path'); + }); + }); + + group('from relative root', () { + final r = path.Context(style: path.Style.url, current: 'foo/bar'); + + test('given absolute path', () { + expect(r.relative('http://google.com/'), equals('http://google.com')); + expect(r.relative('http://google.com'), equals('http://google.com')); + expect(r.relative('file:///'), equals('file://')); + expect(r.relative('file://'), equals('file://')); + expect(r.relative('/'), equals('/')); + expect(r.relative('/a/b'), equals('/a/b')); + }); + + test('given relative path', () { + // The path is considered relative to the root, so it basically just + // normalizes. + expect(r.relative(''), '.'); + expect(r.relative('.'), '.'); + expect(r.relative('..'), '..'); + expect(r.relative('a'), 'a'); + expect(r.relative('a/b.txt'), 'a/b.txt'); + expect(r.relative('../a/b.txt'), '../a/b.txt'); + expect(r.relative('a/./b/../c.txt'), 'a/c.txt'); + }); + }); + + group('from root-relative root', () { + final r = path.Context(style: path.Style.url, current: '/foo/bar'); + + test('given absolute path', () { + expect(r.relative('http://google.com/'), equals('http://google.com')); + expect(r.relative('http://google.com'), equals('http://google.com')); + expect(r.relative('file:///'), equals('file://')); + expect(r.relative('file://'), equals('file://')); + expect(r.relative('/'), equals('../..')); + expect(r.relative('/a/b'), equals('../../a/b')); + }); + + test('given relative path', () { + // The path is considered relative to the root, so it basically just + // normalizes. + expect(r.relative(''), '.'); + expect(r.relative('.'), '.'); + expect(r.relative('..'), '..'); + expect(r.relative('a'), 'a'); + expect(r.relative('a/b.txt'), 'a/b.txt'); + expect(r.relative('../a/b.txt'), '../a/b.txt'); + expect(r.relative('a/./b/../c.txt'), 'a/c.txt'); + }); + }); + + test('from a root with extension', () { + final r = path.Context(style: path.Style.url, current: '/dir.ext'); + expect(r.relative('/dir.ext/file'), 'file'); + }); + + test('with a root parameter', () { + expect(context.relative('/foo/bar/baz', from: '/foo/bar'), equals('baz')); + expect(context.relative('/foo/bar/baz', from: 'https://dart.dev/foo/bar'), + equals('baz')); + expect(context.relative('https://dart.dev/foo/bar/baz', from: '/foo/bar'), + equals('baz')); + expect( + context.relative('https://dart.dev/foo/bar/baz', + from: 'file:///foo/bar'), + equals('https://dart.dev/foo/bar/baz')); + expect( + context.relative('https://dart.dev/foo/bar/baz', + from: 'https://dart.dev/foo/bar'), + equals('baz')); + expect(context.relative('/foo/bar/baz', from: 'file:///foo/bar'), + equals('https://dart.dev/foo/bar/baz')); + expect(context.relative('file:///foo/bar/baz', from: '/foo/bar'), + equals('file:///foo/bar/baz')); + + expect(context.relative('..', from: '/foo/bar'), equals('../../root')); + expect(context.relative('..', from: 'https://dart.dev/foo/bar'), + equals('../../root')); + expect(context.relative('..', from: 'file:///foo/bar'), + equals('https://dart.dev/root')); + expect(context.relative('..', from: '/foo/bar'), equals('../../root')); + + expect(context.relative('https://dart.dev/foo/bar/baz', from: 'foo/bar'), + equals('../../../../foo/bar/baz')); + expect(context.relative('file:///foo/bar/baz', from: 'foo/bar'), + equals('file:///foo/bar/baz')); + expect(context.relative('/foo/bar/baz', from: 'foo/bar'), + equals('../../../../foo/bar/baz')); + + expect(context.relative('..', from: 'foo/bar'), equals('../../..')); + }); + + test('with a root parameter and a relative root', () { + final r = path.Context(style: path.Style.url, current: 'relative/root'); + expect(r.relative('/foo/bar/baz', from: '/foo/bar'), equals('baz')); + expect(r.relative('/foo/bar/baz', from: 'https://dart.dev/foo/bar'), + equals('/foo/bar/baz')); + expect(r.relative('https://dart.dev/foo/bar/baz', from: '/foo/bar'), + equals('https://dart.dev/foo/bar/baz')); + expect( + r.relative('https://dart.dev/foo/bar/baz', from: 'file:///foo/bar'), + equals('https://dart.dev/foo/bar/baz')); + expect( + r.relative('https://dart.dev/foo/bar/baz', + from: 'https://dart.dev/foo/bar'), + equals('baz')); + + expect(r.relative('https://dart.dev/foo/bar/baz', from: 'foo/bar'), + equals('https://dart.dev/foo/bar/baz')); + expect(r.relative('file:///foo/bar/baz', from: 'foo/bar'), + equals('file:///foo/bar/baz')); + expect( + r.relative('/foo/bar/baz', from: 'foo/bar'), equals('/foo/bar/baz')); + + expect(r.relative('..', from: 'foo/bar'), equals('../../..')); + }); + + test('from a . root', () { + final r = path.Context(style: path.Style.url, current: '.'); + expect(r.relative('https://dart.dev/foo/bar/baz'), + equals('https://dart.dev/foo/bar/baz')); + expect(r.relative('file:///foo/bar/baz'), equals('file:///foo/bar/baz')); + expect(r.relative('/foo/bar/baz'), equals('/foo/bar/baz')); + expect(r.relative('foo/bar/baz'), equals('foo/bar/baz')); + }); + }); + + group('isWithin', () { + test('simple cases', () { + expect(context.isWithin('foo/bar', 'foo/bar'), isFalse); + expect(context.isWithin('foo/bar', 'foo/bar/baz'), isTrue); + expect(context.isWithin('foo/bar', 'foo/baz'), isFalse); + expect(context.isWithin('foo/bar', '../path/foo/bar/baz'), isTrue); + expect(context.isWithin('https://dart.dev', 'https://dart.dev/foo/bar'), + isTrue); + expect( + context.isWithin('https://dart.dev', 'http://psub.dart.dev/foo/bar'), + isFalse); + expect(context.isWithin('https://dart.dev', '/foo/bar'), isTrue); + expect(context.isWithin('https://dart.dev/foo', '/foo/bar'), isTrue); + expect(context.isWithin('https://dart.dev/foo', '/bar/baz'), isFalse); + expect(context.isWithin('baz', 'https://dart.dev/root/path/baz/bang'), + isTrue); + expect(context.isWithin('baz', 'https://dart.dev/root/path/bang/baz'), + isFalse); + }); + + test('complex cases', () { + expect(context.isWithin('foo/./bar', 'foo/bar/baz'), isTrue); + expect(context.isWithin('foo//bar', 'foo/bar/baz'), isTrue); + expect(context.isWithin('foo/qux/../bar', 'foo/bar/baz'), isTrue); + expect(context.isWithin('foo/bar', 'foo/bar/baz/../..'), isFalse); + expect(context.isWithin('foo/bar', 'foo/bar///'), isFalse); + expect(context.isWithin('foo/.bar', 'foo/.bar/baz'), isTrue); + expect(context.isWithin('foo/./bar', 'foo/.bar/baz'), isFalse); + expect(context.isWithin('foo/..bar', 'foo/..bar/baz'), isTrue); + expect(context.isWithin('foo/bar', 'foo/bar/baz/..'), isFalse); + expect(context.isWithin('foo/bar', 'foo/bar/baz/../qux'), isTrue); + expect(context.isWithin('http://example.org/', 'http://example.com/foo'), + isFalse); + expect(context.isWithin('http://example.org/', 'https://dart.dev/foo'), + isFalse); + }); + + test('with root-relative paths', () { + expect(context.isWithin('/foo', 'https://dart.dev/foo/bar'), isTrue); + expect(context.isWithin('https://dart.dev/foo', '/foo/bar'), isTrue); + expect(context.isWithin('/root', 'foo/bar'), isTrue); + expect(context.isWithin('foo', '/root/path/foo/bar'), isTrue); + expect(context.isWithin('/foo', '/foo/bar'), isTrue); + }); + + test('from a relative root', () { + final r = path.Context(style: path.Style.url, current: 'foo/bar'); + expect(r.isWithin('.', 'a/b/c'), isTrue); + expect(r.isWithin('.', '../a/b/c'), isFalse); + expect(r.isWithin('.', '../../a/foo/b/c'), isFalse); + expect( + r.isWithin('https://dart.dev/', 'https://dart.dev/baz/bang'), isTrue); + expect(r.isWithin('.', 'https://dart.dev/baz/bang'), isFalse); + }); + }); + + group('equals and hash', () { + test('simple cases', () { + expectEquals(context, 'foo/bar', 'foo/bar'); + expectNotEquals(context, 'foo/bar', 'foo/bar/baz'); + expectNotEquals(context, 'foo/bar', 'foo'); + expectNotEquals(context, 'foo/bar', 'foo/baz'); + expectEquals(context, 'foo/bar', '../path/foo/bar'); + expectEquals(context, 'http://google.com', 'http://google.com'); + expectEquals(context, 'https://dart.dev', '../..'); + expectEquals(context, 'baz', '/root/path/baz'); + }); + + test('complex cases', () { + expectEquals(context, 'foo/./bar', 'foo/bar'); + expectEquals(context, 'foo//bar', 'foo/bar'); + expectEquals(context, 'foo/qux/../bar', 'foo/bar'); + expectNotEquals(context, 'foo/qux/../bar', 'foo/qux'); + expectNotEquals(context, 'foo/bar', 'foo/bar/baz/../..'); + expectEquals(context, 'foo/bar', 'foo/bar///'); + expectEquals(context, 'foo/.bar', 'foo/.bar'); + expectNotEquals(context, 'foo/./bar', 'foo/.bar'); + expectEquals(context, 'foo/..bar', 'foo/..bar'); + expectNotEquals(context, 'foo/../bar', 'foo/..bar'); + expectEquals(context, 'foo/bar', 'foo/bar/baz/..'); + expectNotEquals(context, 'FoO/bAr', 'foo/bar'); + expectEquals(context, 'http://google.com', 'http://google.com/'); + expectEquals(context, 'https://dart.dev/root', '..'); + }); + + test('with root-relative paths', () { + expectEquals(context, '/foo', 'https://dart.dev/foo'); + expectNotEquals(context, '/foo', 'http://google.com/foo'); + expectEquals(context, '/root/path/foo/bar', 'foo/bar'); + }); + + test('from a relative root', () { + final r = path.Context(style: path.Style.posix, current: 'foo/bar'); + expectEquals(r, 'a/b', 'a/b'); + expectNotEquals(r, '.', 'foo/bar'); + expectNotEquals(r, '.', '../a/b'); + expectEquals(r, '.', '../bar'); + expectEquals(r, '/baz/bang', '/baz/bang'); + expectNotEquals(r, 'baz/bang', '/baz/bang'); + }); + }); + + group('absolute', () { + test('allows up to fifteen parts', () { + expect(context.absolute('a'), 'https://dart.dev/root/path/a'); + expect(context.absolute('a', 'b'), 'https://dart.dev/root/path/a/b'); + expect( + context.absolute('a', 'b', 'c'), 'https://dart.dev/root/path/a/b/c'); + expect(context.absolute('a', 'b', 'c', 'd'), + 'https://dart.dev/root/path/a/b/c/d'); + expect(context.absolute('a', 'b', 'c', 'd', 'e'), + 'https://dart.dev/root/path/a/b/c/d/e'); + expect(context.absolute('a', 'b', 'c', 'd', 'e', 'f'), + 'https://dart.dev/root/path/a/b/c/d/e/f'); + expect(context.absolute('a', 'b', 'c', 'd', 'e', 'f', 'g'), + 'https://dart.dev/root/path/a/b/c/d/e/f/g'); + expect(context.absolute('a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'), + 'https://dart.dev/root/path/a/b/c/d/e/f/g/h'); + expect(context.absolute('a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i'), + 'https://dart.dev/root/path/a/b/c/d/e/f/g/h/i'); + expect(context.absolute('a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'), + 'https://dart.dev/root/path/a/b/c/d/e/f/g/h/i/j'); + expect( + context.absolute( + 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k'), + 'https://dart.dev/root/path/a/b/c/d/e/f/g/h/i/j/k'); + expect( + context.absolute( + 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l'), + 'https://dart.dev/root/path/a/b/c/d/e/f/g/h/i/j/k/l'); + expect( + context.absolute( + 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm'), + 'https://dart.dev/root/path/a/b/c/d/e/f/g/h/i/j/k/l/m'); + expect( + context.absolute('a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', + 'k', 'l', 'm', 'n'), + 'https://dart.dev/root/path/a/b/c/d/e/f/g/h/i/j/k/l/m/n'); + expect( + context.absolute('a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', + 'k', 'l', 'm', 'n', 'o'), + 'https://dart.dev/root/path/a/b/c/d/e/f/g/h/i/j/k/l/m/n/o'); + }); + + test('does not add separator if a part ends in one', () { + expect(context.absolute('a/', 'b', 'c/', 'd'), + 'https://dart.dev/root/path/a/b/c/d'); + expect(context.absolute(r'a\', 'b'), r'https://dart.dev/root/path/a\/b'); + }); + + test('ignores parts before an absolute path', () { + expect(context.absolute('a', '/b', '/c', 'd'), 'https://dart.dev/c/d'); + expect(context.absolute('a', '/b', 'file:///c', 'd'), 'file:///c/d'); + expect(context.absolute('a', r'c:\b', 'c', 'd'), r'c:\b/c/d'); + expect(context.absolute('a', r'\\b', 'c', 'd'), + r'https://dart.dev/root/path/a/\\b/c/d'); + }); + }); + + test('withoutExtension', () { + expect(context.withoutExtension(''), ''); + expect(context.withoutExtension('a'), 'a'); + expect(context.withoutExtension('.a'), '.a'); + expect(context.withoutExtension('a.b'), 'a'); + expect(context.withoutExtension('a/b.c'), 'a/b'); + expect(context.withoutExtension('a/b.c.d'), 'a/b.c'); + expect(context.withoutExtension('a/'), 'a/'); + expect(context.withoutExtension('a/b/'), 'a/b/'); + expect(context.withoutExtension('a/.'), 'a/.'); + expect(context.withoutExtension('a/.b'), 'a/.b'); + expect(context.withoutExtension('a.b/c'), 'a.b/c'); + expect(context.withoutExtension(r'a.b\c'), r'a'); + expect(context.withoutExtension(r'a/b\c'), r'a/b\c'); + expect(context.withoutExtension(r'a/b\c.d'), r'a/b\c'); + expect(context.withoutExtension('a/b.c/'), 'a/b/'); + expect(context.withoutExtension('a/b.c//'), 'a/b//'); + }); + + test('setExtension', () { + expect(context.setExtension('', '.x'), '.x'); + expect(context.setExtension('a', '.x'), 'a.x'); + expect(context.setExtension('.a', '.x'), '.a.x'); + expect(context.setExtension('a.b', '.x'), 'a.x'); + expect(context.setExtension('a/b.c', '.x'), 'a/b.x'); + expect(context.setExtension('a/b.c.d', '.x'), 'a/b.c.x'); + expect(context.setExtension('a/', '.x'), 'a/.x'); + expect(context.setExtension('a/b/', '.x'), 'a/b/.x'); + expect(context.setExtension('a/.', '.x'), 'a/..x'); + expect(context.setExtension('a/.b', '.x'), 'a/.b.x'); + expect(context.setExtension('a.b/c', '.x'), 'a.b/c.x'); + expect(context.setExtension(r'a.b\c', '.x'), r'a.x'); + expect(context.setExtension(r'a/b\c', '.x'), r'a/b\c.x'); + expect(context.setExtension(r'a/b\c.d', '.x'), r'a/b\c.x'); + expect(context.setExtension('a/b.c/', '.x'), 'a/b/.x'); + expect(context.setExtension('a/b.c//', '.x'), 'a/b//.x'); + }); + + group('fromUri', () { + test('with a URI', () { + expect(context.fromUri(Uri.parse('https://dart.dev/path/to/foo')), + 'https://dart.dev/path/to/foo'); + expect(context.fromUri(Uri.parse('https://dart.dev/path/to/foo/')), + 'https://dart.dev/path/to/foo/'); + expect(context.fromUri(Uri.parse('file:///path/to/foo')), + 'file:///path/to/foo'); + expect(context.fromUri(Uri.parse('foo/bar')), 'foo/bar'); + expect(context.fromUri(Uri.parse('https://dart.dev/path/to/foo%23bar')), + 'https://dart.dev/path/to/foo%23bar'); + // Since the resulting "path" is also a URL, special characters should + // remain percent-encoded in the result. + expect(context.fromUri(Uri.parse('_%7B_%7D_%60_%5E_%20_%22_%25_')), + r'_%7B_%7D_%60_%5E_%20_%22_%25_'); + }); + + test('with a string', () { + expect(context.fromUri('https://dart.dev/path/to/foo'), + 'https://dart.dev/path/to/foo'); + }); + }); + + test('toUri', () { + expect(context.toUri('https://dart.dev/path/to/foo'), + Uri.parse('https://dart.dev/path/to/foo')); + expect(context.toUri('https://dart.dev/path/to/foo/'), + Uri.parse('https://dart.dev/path/to/foo/')); + expect(context.toUri('path/to/foo/'), Uri.parse('path/to/foo/')); + expect( + context.toUri('file:///path/to/foo'), Uri.parse('file:///path/to/foo')); + expect(context.toUri('foo/bar'), Uri.parse('foo/bar')); + expect(context.toUri('https://dart.dev/path/to/foo%23bar'), + Uri.parse('https://dart.dev/path/to/foo%23bar')); + // Since the input path is also a URI, special characters should already + // be percent encoded there too. + expect(context.toUri(r'http://foo.com/_%7B_%7D_%60_%5E_%20_%22_%25_'), + Uri.parse('http://foo.com/_%7B_%7D_%60_%5E_%20_%22_%25_')); + expect(context.toUri(r'_%7B_%7D_%60_%5E_%20_%22_%25_'), + Uri.parse('_%7B_%7D_%60_%5E_%20_%22_%25_')); + expect(context.toUri(''), Uri.parse('')); + }); + + group('prettyUri', () { + test('with a file: URI', () { + expect(context.prettyUri(Uri.parse('file:///root/path/a/b')), + 'file:///root/path/a/b'); + }); + + test('with an http: URI', () { + expect(context.prettyUri('https://dart.dev/root/path/a/b'), 'a/b'); + expect(context.prettyUri('https://dart.dev/root/path/a/../b'), 'b'); + expect(context.prettyUri('https://dart.dev/other/path/a/b'), + 'https://dart.dev/other/path/a/b'); + expect(context.prettyUri('http://psub.dart.dev/root/path'), + 'http://psub.dart.dev/root/path'); + expect(context.prettyUri('https://dart.dev/root/other'), '../other'); + }); + + test('with a relative URI', () { + expect(context.prettyUri('a/b'), 'a/b'); + }); + + test('with a root-relative URI', () { + expect(context.prettyUri('/a/b'), '/a/b'); + }); + + test('with a Uri object', () { + expect(context.prettyUri(Uri.parse('a/b')), 'a/b'); + }); + }); +} diff --git a/pkgs/path/test/utils.dart b/pkgs/path/test/utils.dart new file mode 100644 index 00000000..08d4a524 --- /dev/null +++ b/pkgs/path/test/utils.dart @@ -0,0 +1,32 @@ +// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:path/path.dart' as p; +import 'package:test/test.dart'; + +/// A matcher for a closure that throws a [p.PathException]. +final throwsPathException = throwsA(const TypeMatcher()); + +void expectEquals(p.Context context, String path1, String path2) { + expect(context.equals(path1, path2), isTrue, + reason: 'Expected "$path1" to equal "$path2".'); + expect(context.equals(path2, path1), isTrue, + reason: 'Expected "$path2" to equal "$path1".'); + expect(context.hash(path1), equals(context.hash(path2)), + reason: 'Expected "$path1" to hash the same as "$path2".'); +} + +void expectNotEquals(p.Context context, String path1, String path2, + {bool allowSameHash = false}) { + expect(context.equals(path1, path2), isFalse, + reason: 'Expected "$path1" not to equal "$path2".'); + expect(context.equals(path2, path1), isFalse, + reason: 'Expected "$path2" not to equal "$path1".'); + + // Hash collisions are allowed, but the test author should be explicitly aware + // when they occur. + if (allowSameHash) return; + expect(context.hash(path1), isNot(equals(context.hash(path2))), + reason: 'Expected "$path1" not to hash the same as "$path2".'); +} diff --git a/pkgs/path/test/windows_test.dart b/pkgs/path/test/windows_test.dart new file mode 100644 index 00000000..180f5600 --- /dev/null +++ b/pkgs/path/test/windows_test.dart @@ -0,0 +1,906 @@ +// Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:path/path.dart' as path; +import 'package:path/src/utils.dart'; +import 'package:test/test.dart'; + +import 'utils.dart'; + +void main() { + final context = + path.Context(style: path.Style.windows, current: r'C:\root\path'); + + test('separator', () { + expect(context.separator, '\\'); + }); + + test('extension', () { + expect(context.extension(''), ''); + expect(context.extension('.'), ''); + expect(context.extension('..'), ''); + expect(context.extension('a/..'), ''); + expect(context.extension('foo.dart'), '.dart'); + expect(context.extension('foo.dart.js'), '.js'); + expect(context.extension('foo bargule fisk.dart.js'), '.js'); + expect(context.extension(r'a.b\c'), ''); + expect(context.extension('a.b/c.d'), '.d'); + expect(context.extension(r'~\.bashrc'), ''); + expect(context.extension(r'a.b/c'), r''); + expect(context.extension(r'foo.dart\'), '.dart'); + expect(context.extension(r'foo.dart\\'), '.dart'); + expect(context.extension('a.b/..', 2), ''); + expect(context.extension('foo.bar.dart.js', 2), '.dart.js'); + expect(context.extension(r'foo.bar.dart.js', 3), '.bar.dart.js'); + expect(context.extension(r'foo.bar.dart.js', 10), '.bar.dart.js'); + expect(context.extension('a.b/c.d', 2), '.d'); + expect(() => context.extension(r'foo.bar.dart.js', 0), throwsRangeError); + expect(() => context.extension(r'foo.bar.dart.js', -1), throwsRangeError); + }); + + test('rootPrefix', () { + expect(context.rootPrefix(''), ''); + expect(context.rootPrefix('a'), ''); + expect(context.rootPrefix(r'a\b'), ''); + expect(context.rootPrefix(r'C:\a\c'), r'C:\'); + expect(context.rootPrefix(r'C:\'), r'C:\'); + expect(context.rootPrefix('C:/'), 'C:/'); + expect(context.rootPrefix(r'\\server\share\a\b'), r'\\server\share'); + expect(context.rootPrefix(r'\\server\share'), r'\\server\share'); + expect(context.rootPrefix(r'\\server\'), r'\\server\'); + expect(context.rootPrefix(r'\\server'), r'\\server'); + expect(context.rootPrefix(r'\a\b'), r'\'); + expect(context.rootPrefix(r'/a/b'), r'/'); + expect(context.rootPrefix(r'\'), r'\'); + expect(context.rootPrefix(r'/'), r'/'); + }); + + test('dirname', () { + expect(context.dirname(r''), '.'); + expect(context.dirname(r'a'), '.'); + expect(context.dirname(r'a\b'), 'a'); + expect(context.dirname(r'a\b\c'), r'a\b'); + expect(context.dirname(r'a\b.c'), 'a'); + expect(context.dirname(r'a\'), '.'); + expect(context.dirname('a/'), '.'); + expect(context.dirname(r'a\.'), 'a'); + expect(context.dirname(r'a\b/c'), r'a\b'); + expect(context.dirname(r'C:\a'), r'C:\'); + expect(context.dirname(r'C:\\\a'), r'C:\'); + expect(context.dirname(r'C:\'), r'C:\'); + expect(context.dirname(r'C:\\\'), r'C:\'); + expect(context.dirname(r'a\b\'), r'a'); + expect(context.dirname(r'a/b\c'), 'a/b'); + expect(context.dirname(r'a\\'), r'.'); + expect(context.dirname(r'a\b\\'), 'a'); + expect(context.dirname(r'a\\b'), 'a'); + expect(context.dirname(r'foo bar\gule fisk'), 'foo bar'); + expect(context.dirname(r'\\server\share'), r'\\server\share'); + expect(context.dirname(r'\\server\share\dir'), r'\\server\share'); + expect(context.dirname(r'\a'), r'\'); + expect(context.dirname(r'/a'), r'/'); + expect(context.dirname(r'\'), r'\'); + expect(context.dirname(r'/'), r'/'); + }); + + test('basename', () { + expect(context.basename(r''), ''); + expect(context.basename(r'.'), '.'); + expect(context.basename(r'..'), '..'); + expect(context.basename(r'.hest'), '.hest'); + expect(context.basename(r'a'), 'a'); + expect(context.basename(r'a\b'), 'b'); + expect(context.basename(r'a\b\c'), 'c'); + expect(context.basename(r'a\b.c'), 'b.c'); + expect(context.basename(r'a\'), 'a'); + expect(context.basename(r'a/'), 'a'); + expect(context.basename(r'a\.'), '.'); + expect(context.basename(r'a\b/c'), r'c'); + expect(context.basename(r'C:\a'), 'a'); + expect(context.basename(r'C:\'), r'C:\'); + expect(context.basename(r'a\b\'), 'b'); + expect(context.basename(r'a/b\c'), 'c'); + expect(context.basename(r'a\\'), 'a'); + expect(context.basename(r'a\b\\'), 'b'); + expect(context.basename(r'a\\b'), 'b'); + expect(context.basename(r'a\\b'), 'b'); + expect(context.basename(r'a\fisk hest.ma pa'), 'fisk hest.ma pa'); + expect(context.basename(r'\\server\share'), r'\\server\share'); + expect(context.basename(r'\\server\share\dir'), r'dir'); + expect(context.basename(r'\a'), r'a'); + expect(context.basename(r'/a'), r'a'); + expect(context.basename(r'\'), r'\'); + expect(context.basename(r'/'), r'/'); + }); + + test('basenameWithoutExtension', () { + expect(context.basenameWithoutExtension(''), ''); + expect(context.basenameWithoutExtension('.'), '.'); + expect(context.basenameWithoutExtension('..'), '..'); + expect(context.basenameWithoutExtension('.hest'), '.hest'); + expect(context.basenameWithoutExtension('a'), 'a'); + expect(context.basenameWithoutExtension(r'a\b'), 'b'); + expect(context.basenameWithoutExtension(r'a\b\c'), 'c'); + expect(context.basenameWithoutExtension(r'a\b.c'), 'b'); + expect(context.basenameWithoutExtension(r'a\'), 'a'); + expect(context.basenameWithoutExtension(r'a\.'), '.'); + expect(context.basenameWithoutExtension(r'a\b/c'), r'c'); + expect(context.basenameWithoutExtension(r'a\.bashrc'), '.bashrc'); + expect(context.basenameWithoutExtension(r'a\b\c.d.e'), 'c.d'); + expect(context.basenameWithoutExtension(r'a\\'), 'a'); + expect(context.basenameWithoutExtension(r'a\b\\'), 'b'); + expect(context.basenameWithoutExtension(r'a\\b'), 'b'); + expect(context.basenameWithoutExtension(r'a\b.c\'), 'b'); + expect(context.basenameWithoutExtension(r'a\b.c\\'), 'b'); + expect(context.basenameWithoutExtension(r'C:\f h.ma pa.f s'), 'f h.ma pa'); + }); + + test('isAbsolute', () { + expect(context.isAbsolute(''), false); + expect(context.isAbsolute('.'), false); + expect(context.isAbsolute('..'), false); + expect(context.isAbsolute('a'), false); + expect(context.isAbsolute(r'a\b'), false); + expect(context.isAbsolute(r'\a\b'), true); + expect(context.isAbsolute(r'\'), true); + expect(context.isAbsolute(r'/a/b'), true); + expect(context.isAbsolute(r'/'), true); + expect(context.isAbsolute('~'), false); + expect(context.isAbsolute('.'), false); + expect(context.isAbsolute(r'..\a'), false); + expect(context.isAbsolute(r'a:/a\b'), true); + expect(context.isAbsolute(r'D:/a/b'), true); + expect(context.isAbsolute(r'c:\'), true); + expect(context.isAbsolute(r'B:\'), true); + expect(context.isAbsolute(r'c:\a'), true); + expect(context.isAbsolute(r'C:\a'), true); + expect(context.isAbsolute(r'\\server\share'), true); + expect(context.isAbsolute(r'\\server\share\path'), true); + }); + + test('isRelative', () { + expect(context.isRelative(''), true); + expect(context.isRelative('.'), true); + expect(context.isRelative('..'), true); + expect(context.isRelative('a'), true); + expect(context.isRelative(r'a\b'), true); + expect(context.isRelative(r'\a\b'), false); + expect(context.isRelative(r'\'), false); + expect(context.isRelative(r'/a/b'), false); + expect(context.isRelative(r'/'), false); + expect(context.isRelative('~'), true); + expect(context.isRelative('.'), true); + expect(context.isRelative(r'..\a'), true); + expect(context.isRelative(r'a:/a\b'), false); + expect(context.isRelative(r'D:/a/b'), false); + expect(context.isRelative(r'c:\'), false); + expect(context.isRelative(r'B:\'), false); + expect(context.isRelative(r'c:\a'), false); + expect(context.isRelative(r'C:\a'), false); + expect(context.isRelative(r'\\server\share'), false); + expect(context.isRelative(r'\\server\share\path'), false); + }); + + test('isRootRelative', () { + expect(context.isRootRelative(''), false); + expect(context.isRootRelative('.'), false); + expect(context.isRootRelative('..'), false); + expect(context.isRootRelative('a'), false); + expect(context.isRootRelative(r'a\b'), false); + expect(context.isRootRelative(r'\a\b'), true); + expect(context.isRootRelative(r'\'), true); + expect(context.isRootRelative(r'/a/b'), true); + expect(context.isRootRelative(r'/'), true); + expect(context.isRootRelative('~'), false); + expect(context.isRootRelative('.'), false); + expect(context.isRootRelative(r'..\a'), false); + expect(context.isRootRelative(r'a:/a\b'), false); + expect(context.isRootRelative(r'D:/a/b'), false); + expect(context.isRootRelative(r'c:\'), false); + expect(context.isRootRelative(r'B:\'), false); + expect(context.isRootRelative(r'c:\a'), false); + expect(context.isRootRelative(r'C:\a'), false); + expect(context.isRootRelative(r'\\server\share'), false); + expect(context.isRootRelative(r'\\server\share\path'), false); + }); + + group('join', () { + test('allows up to sixteen parts', () { + expect(context.join('a'), 'a'); + expect(context.join('a', 'b'), r'a\b'); + expect(context.join('a', 'b', 'c'), r'a\b\c'); + expect(context.join('a', 'b', 'c', 'd'), r'a\b\c\d'); + expect(context.join('a', 'b', 'c', 'd', 'e'), r'a\b\c\d\e'); + expect(context.join('a', 'b', 'c', 'd', 'e', 'f'), r'a\b\c\d\e\f'); + expect(context.join('a', 'b', 'c', 'd', 'e', 'f', 'g'), r'a\b\c\d\e\f\g'); + expect(context.join('a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'), + r'a\b\c\d\e\f\g\h'); + expect(context.join('a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i'), + r'a\b\c\d\e\f\g\h\i'); + expect(context.join('a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'), + r'a\b\c\d\e\f\g\h\i\j'); + expect( + context.join('a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k'), + r'a\b\c\d\e\f\g\h\i\j\k'); + expect( + context.join( + 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l'), + r'a\b\c\d\e\f\g\h\i\j\k\l'); + expect( + context.join( + 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm'), + r'a\b\c\d\e\f\g\h\i\j\k\l\m'); + expect( + context.join('a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', + 'l', 'm', 'n'), + r'a\b\c\d\e\f\g\h\i\j\k\l\m\n'); + expect( + context.join('a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', + 'l', 'm', 'n', 'o'), + r'a\b\c\d\e\f\g\h\i\j\k\l\m\n\o'); + expect( + context.join('a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', + 'l', 'm', 'n', 'o', 'p'), + r'a\b\c\d\e\f\g\h\i\j\k\l\m\n\o\p'); + }); + + test('does not add separator if a part ends or begins in one', () { + expect(context.join(r'a\', 'b', r'c\', 'd'), r'a\b\c\d'); + expect(context.join('a/', 'b'), r'a/b'); + }); + + test('ignores parts before an absolute path', () { + expect(context.join('a', r'\b', r'\c', 'd'), r'\c\d'); + expect(context.join('a', '/b', '/c', 'd'), r'/c\d'); + expect(context.join('a', r'c:\b', 'c', 'd'), r'c:\b\c\d'); + expect(context.join('a', r'\\b\c', r'\\d\e', 'f'), r'\\d\e\f'); + expect(context.join('a', r'c:\b', r'\c', 'd'), r'c:\c\d'); + expect(context.join('a', r'\\b\c\d', r'\e', 'f'), r'\\b\c\e\f'); + }); + + test('ignores trailing nulls', () { + expect(context.join('a', null), equals('a')); + expect(context.join('a', 'b', 'c', null, null), equals(r'a\b\c')); + }); + + test('ignores empty strings', () { + expect(context.join(''), ''); + expect(context.join('', ''), ''); + expect(context.join('', 'a'), 'a'); + expect(context.join('a', '', 'b', '', '', '', 'c'), r'a\b\c'); + expect(context.join('a', 'b', ''), r'a\b'); + }); + + test('disallows intermediate nulls', () { + expect(() => context.join('a', null, 'b'), throwsArgumentError); + }); + + test('join does not modify internal ., .., or trailing separators', () { + expect(context.join('a/', 'b/c/'), 'a/b/c/'); + expect(context.join(r'a\b\./c\..\\', r'd\..\.\..\\e\f\\'), + r'a\b\./c\..\\d\..\.\..\\e\f\\'); + expect(context.join(r'a\b', r'c\..\..\..\..'), r'a\b\c\..\..\..\..'); + expect(context.join(r'a', 'b${context.separator}'), r'a\b\'); + }); + }); + + group('joinAll', () { + test('allows more than sixteen parts', () { + expect( + context.joinAll([ + 'a', + 'b', + 'c', + 'd', + 'e', + 'f', + 'g', + 'h', + 'i', + 'j', + 'k', + 'l', + 'm', + 'n', + 'o', + 'p', + 'q' + ]), + r'a\b\c\d\e\f\g\h\i\j\k\l\m\n\o\p\q'); + }); + + test('does not add separator if a part ends or begins in one', () { + expect(context.joinAll([r'a\', 'b', r'c\', 'd']), r'a\b\c\d'); + expect(context.joinAll(['a/', 'b']), r'a/b'); + }); + + test('ignores parts before an absolute path', () { + expect(context.joinAll(['a', r'\b', r'\c', 'd']), r'\c\d'); + expect(context.joinAll(['a', '/b', '/c', 'd']), r'/c\d'); + expect(context.joinAll(['a', r'c:\b', 'c', 'd']), r'c:\b\c\d'); + expect(context.joinAll(['a', r'\\b\c', r'\\d\e', 'f']), r'\\d\e\f'); + expect(context.joinAll(['a', r'c:\b', r'\c', 'd']), r'c:\c\d'); + expect(context.joinAll(['a', r'\\b\c\d', r'\e', 'f']), r'\\b\c\e\f'); + }); + }); + + group('split', () { + test('simple cases', () { + expect(context.split(''), []); + expect(context.split('.'), ['.']); + expect(context.split('..'), ['..']); + expect(context.split('foo'), equals(['foo'])); + expect(context.split(r'foo\bar.txt'), equals(['foo', 'bar.txt'])); + expect(context.split(r'foo\bar/baz'), equals(['foo', 'bar', 'baz'])); + expect(context.split(r'foo\..\bar\.\baz'), + equals(['foo', '..', 'bar', '.', 'baz'])); + expect(context.split(r'foo\\bar\\\baz'), equals(['foo', 'bar', 'baz'])); + expect(context.split(r'foo\/\baz'), equals(['foo', 'baz'])); + expect(context.split('.'), equals(['.'])); + expect(context.split(''), equals([])); + expect(context.split('foo/'), equals(['foo'])); + expect(context.split(r'C:\'), equals([r'C:\'])); + }); + + test('includes the root for absolute paths', () { + expect(context.split(r'C:\foo\bar\baz'), + equals([r'C:\', 'foo', 'bar', 'baz'])); + expect(context.split(r'C:\\'), equals([r'C:\'])); + + expect(context.split(r'\\server\share\foo\bar\baz'), + equals([r'\\server\share', 'foo', 'bar', 'baz'])); + expect(context.split(r'\\server\share'), equals([r'\\server\share'])); + + expect( + context.split(r'\foo\bar\baz'), equals([r'\', 'foo', 'bar', 'baz'])); + expect(context.split(r'\'), equals([r'\'])); + }); + }); + + group('normalize', () { + test('simple cases', () { + expect(context.normalize(''), '.'); + expect(context.normalize('.'), '.'); + expect(context.normalize('..'), '..'); + expect(context.normalize('a'), 'a'); + expect(context.normalize('/a/b'), r'\a\b'); + expect(context.normalize(r'\'), r'\'); + expect(context.normalize(r'\a\b'), r'\a\b'); + expect(context.normalize('/'), r'\'); + expect(context.normalize('C:/'), r'C:\'); + expect(context.normalize(r'C:\'), r'C:\'); + expect(context.normalize(r'\\server\share'), r'\\server\share'); + expect(context.normalize('a\\.\\\xc5\u0bf8-;\u{1f085}\u{00}\\c\\d\\..\\'), + 'a\\\xc5\u0bf8-;\u{1f085}\u{00}\x5cc'); + }); + + test('collapses redundant separators', () { + expect(context.normalize(r'a\b\c'), r'a\b\c'); + expect(context.normalize(r'a\\b\\\c\\\\d'), r'a\b\c\d'); + }); + + test('eliminates "." parts', () { + expect(context.normalize(r'.\'), '.'); + expect(context.normalize(r'c:\.'), r'c:\'); + expect(context.normalize(r'c:\foo\.'), r'c:\foo'); + expect(context.normalize(r'B:\.\'), r'B:\'); + expect(context.normalize(r'\\server\share\.'), r'\\server\share'); + expect(context.normalize(r'.\.'), '.'); + expect(context.normalize(r'a\.\b'), r'a\b'); + expect(context.normalize(r'a\.b\c'), r'a\.b\c'); + expect(context.normalize(r'a\./.\b\.\c'), r'a\b\c'); + expect(context.normalize(r'.\./a'), 'a'); + expect(context.normalize(r'a/.\.'), 'a'); + expect(context.normalize(r'\.'), r'\'); + expect(context.normalize('/.'), r'\'); + }); + + test('eliminates ".." parts', () { + expect(context.normalize('..'), '..'); + expect(context.normalize(r'..\'), '..'); + expect(context.normalize(r'..\..\..'), r'..\..\..'); + expect(context.normalize(r'../..\..\'), r'..\..\..'); + expect(context.normalize(r'\\server\share\..'), r'\\server\share'); + expect( + context.normalize(r'\\server\share\..\../..\a'), r'\\server\share\a'); + expect(context.normalize(r'c:\..'), r'c:\'); + expect(context.normalize(r'c:\foo\..'), r'c:\'); + expect(context.normalize(r'A:/..\..\..'), r'A:\'); + expect(context.normalize(r'b:\..\..\..\a'), r'b:\a'); + expect(context.normalize(r'b:\r\..\..\..\a\c\.\..'), r'b:\a'); + expect(context.normalize(r'a\..'), '.'); + expect(context.normalize(r'..\a'), r'..\a'); + expect(context.normalize(r'c:\..\a'), r'c:\a'); + expect(context.normalize(r'\..\a'), r'\a'); + expect(context.normalize(r'a\b\..'), 'a'); + expect(context.normalize(r'..\a\b\..'), r'..\a'); + expect(context.normalize(r'a\..\b'), 'b'); + expect(context.normalize(r'a\.\..\b'), 'b'); + expect(context.normalize(r'a\b\c\..\..\d\e\..'), r'a\d'); + expect(context.normalize(r'a\b\..\..\..\..\c'), r'..\..\c'); + expect(context.normalize(r'a/b/c/../../..d/./.e/f././'), r'a\..d\.e\f.'); + }); + + test('removes trailing separators', () { + expect(context.normalize(r'.\'), '.'); + expect(context.normalize(r'.\\'), '.'); + expect(context.normalize(r'a/'), 'a'); + expect(context.normalize(r'a\b\'), r'a\b'); + expect(context.normalize(r'a\b\\\'), r'a\b'); + }); + + test('normalizes separators', () { + expect(context.normalize(r'a/b\c'), r'a\b\c'); + }); + + test('when canonicalizing', () { + expect(context.canonicalize('.'), r'c:\root\path'); + expect(context.canonicalize('foo/bar'), r'c:\root\path\foo\bar'); + expect(context.canonicalize('FoO'), r'c:\root\path\foo'); + expect(context.canonicalize('/foo'), r'c:\foo'); + expect(context.canonicalize('D:/foo'), r'd:\foo'); + }); + }); + + group('relative', () { + group('from absolute root', () { + test('given absolute path in root', () { + expect(context.relative(r'C:\'), r'..\..'); + expect(context.relative(r'C:\root'), '..'); + expect(context.relative(r'\root'), '..'); + expect(context.relative(r'C:\root\path'), '.'); + expect(context.relative(r'\root\path'), '.'); + expect(context.relative(r'C:\root\path\a'), 'a'); + expect(context.relative(r'\root\path\a'), 'a'); + expect(context.relative(r'C:\root\path\a\b.txt'), r'a\b.txt'); + expect(context.relative(r'C:\root\a\b.txt'), r'..\a\b.txt'); + expect(context.relative(r'C:/'), r'..\..'); + expect(context.relative(r'C:/root'), '..'); + expect(context.relative(r'c:\'), r'..\..'); + expect(context.relative(r'c:\root'), '..'); + }); + + test('given absolute path outside of root', () { + expect(context.relative(r'C:\a\b'), r'..\..\a\b'); + expect(context.relative(r'\a\b'), r'..\..\a\b'); + expect(context.relative(r'C:\root\path\a'), 'a'); + expect(context.relative(r'C:\root\path\a\b.txt'), r'a\b.txt'); + expect(context.relative(r'C:\root\a\b.txt'), r'..\a\b.txt'); + expect(context.relative(r'C:/a/b'), r'..\..\a\b'); + expect(context.relative(r'C:/root/path/a'), 'a'); + expect(context.relative(r'c:\a\b'), r'..\..\a\b'); + expect(context.relative(r'c:\root\path\a'), 'a'); + }); + + test('given absolute path on different drive', () { + expect(context.relative(r'D:\a\b'), r'D:\a\b'); + }); + + test('given relative path', () { + // The path is considered relative to the root, so it basically just + // normalizes. + expect(context.relative(''), '.'); + expect(context.relative('.'), '.'); + expect(context.relative('a'), 'a'); + expect(context.relative(r'a\b.txt'), r'a\b.txt'); + expect(context.relative(r'..\a\b.txt'), r'..\a\b.txt'); + expect(context.relative(r'a\.\b\..\c.txt'), r'a\c.txt'); + }); + + test('is case-insensitive', () { + expect(context.relative(r'c:\'), r'..\..'); + expect(context.relative(r'c:\RoOt'), r'..'); + expect(context.relative(r'c:\rOoT\pAtH\a'), r'a'); + }); + + // Regression + test('from root-only path', () { + expect(context.relative(r'C:\', from: r'C:\'), '.'); + expect(context.relative(r'C:\root\path', from: r'C:\'), r'root\path'); + }); + }); + + group('from relative root', () { + final r = path.Context(style: path.Style.windows, current: r'foo\bar'); + + test('given absolute path', () { + expect(r.relative(r'C:\'), equals(r'C:\')); + expect(r.relative(r'C:\a\b'), equals(r'C:\a\b')); + expect(r.relative(r'\'), equals(r'\')); + expect(r.relative(r'\a\b'), equals(r'\a\b')); + }); + + test('given relative path', () { + // The path is considered relative to the root, so it basically just + // normalizes. + expect(r.relative(''), '.'); + expect(r.relative('.'), '.'); + expect(r.relative('..'), '..'); + expect(r.relative('a'), 'a'); + expect(r.relative(r'a\b.txt'), r'a\b.txt'); + expect(r.relative(r'..\a/b.txt'), r'..\a\b.txt'); + expect(r.relative(r'a\./b\../c.txt'), r'a\c.txt'); + }); + }); + + group('from root-relative root', () { + final r = path.Context(style: path.Style.windows, current: r'\foo\bar'); + + test('given absolute path', () { + expect(r.relative(r'C:\'), equals(r'C:\')); + expect(r.relative(r'C:\a\b'), equals(r'C:\a\b')); + expect(r.relative(r'\'), equals(r'..\..')); + expect(r.relative(r'\a\b'), equals(r'..\..\a\b')); + expect(r.relative('/'), equals(r'..\..')); + expect(r.relative('/a/b'), equals(r'..\..\a\b')); + }); + + test('given relative path', () { + // The path is considered relative to the root, so it basically just + // normalizes. + expect(r.relative(''), '.'); + expect(r.relative('.'), '.'); + expect(r.relative('..'), '..'); + expect(r.relative('a'), 'a'); + expect(r.relative(r'a\b.txt'), r'a\b.txt'); + expect(r.relative(r'..\a/b.txt'), r'..\a\b.txt'); + expect(r.relative(r'a\./b\../c.txt'), r'a\c.txt'); + }); + }); + + test('from a root with extension', () { + final r = path.Context(style: path.Style.windows, current: r'C:\dir.ext'); + expect(r.relative(r'C:\dir.ext\file'), 'file'); + }); + + test('with a root parameter', () { + expect(context.relative(r'C:\foo\bar\baz', from: r'C:\foo\bar'), + equals('baz')); + expect( + context.relative('..', from: r'C:\foo\bar'), equals(r'..\..\root')); + expect(context.relative('..', from: r'D:\foo\bar'), equals(r'C:\root')); + expect(context.relative(r'C:\foo\bar\baz', from: r'foo\bar'), + equals(r'..\..\..\..\foo\bar\baz')); + expect(context.relative('..', from: r'foo\bar'), equals(r'..\..\..')); + }); + + test('with a root parameter and a relative root', () { + final r = + path.Context(style: path.Style.windows, current: r'relative\root'); + expect(r.relative(r'C:\foo\bar\baz', from: r'C:\foo\bar'), equals('baz')); + expect(() => r.relative('..', from: r'C:\foo\bar'), throwsPathException); + expect(r.relative(r'C:\foo\bar\baz', from: r'foo\bar'), + equals(r'C:\foo\bar\baz')); + expect(r.relative('..', from: r'foo\bar'), equals(r'..\..\..')); + }); + + test('given absolute with different root prefix', () { + expect(context.relative(r'D:\a\b'), r'D:\a\b'); + expect(context.relative(r'\\server\share\a\b'), r'\\server\share\a\b'); + }); + + test('from a . root', () { + final r = path.Context(style: path.Style.windows, current: '.'); + expect(r.relative(r'C:\foo\bar\baz'), equals(r'C:\foo\bar\baz')); + expect(r.relative(r'foo\bar\baz'), equals(r'foo\bar\baz')); + expect(r.relative(r'\foo\bar\baz'), equals(r'\foo\bar\baz')); + }); + }); + + group('isWithin', () { + test('simple cases', () { + expect(context.isWithin(r'foo\bar', r'foo\bar'), isFalse); + expect(context.isWithin(r'foo\bar', r'foo\bar\baz'), isTrue); + expect(context.isWithin(r'foo\bar', r'foo\baz'), isFalse); + expect(context.isWithin(r'foo\bar', r'..\path\foo\bar\baz'), isTrue); + expect(context.isWithin(r'C:\', r'C:\foo\bar'), isTrue); + expect(context.isWithin(r'C:\', r'D:\foo\bar'), isFalse); + expect(context.isWithin(r'C:\', r'\foo\bar'), isTrue); + expect(context.isWithin(r'C:\foo', r'\foo\bar'), isTrue); + expect(context.isWithin(r'C:\foo', r'\bar\baz'), isFalse); + expect(context.isWithin(r'baz', r'C:\root\path\baz\bang'), isTrue); + expect(context.isWithin(r'baz', r'C:\root\path\bang\baz'), isFalse); + }); + + test('complex cases', () { + expect(context.isWithin(r'foo\.\bar', r'foo\bar\baz'), isTrue); + expect(context.isWithin(r'foo\\bar', r'foo\bar\baz'), isTrue); + expect(context.isWithin(r'foo\qux\..\bar', r'foo\bar\baz'), isTrue); + expect(context.isWithin(r'foo\bar', r'foo\bar\baz\..\..'), isFalse); + expect(context.isWithin(r'foo\bar', r'foo\bar\\\'), isFalse); + expect(context.isWithin(r'foo\.bar', r'foo\.bar\baz'), isTrue); + expect(context.isWithin(r'foo\.\bar', r'foo\.bar\baz'), isFalse); + expect(context.isWithin(r'foo\..bar', r'foo\..bar\baz'), isTrue); + expect(context.isWithin(r'foo\bar', r'foo\bar\baz\..'), isFalse); + expect(context.isWithin(r'foo\bar', r'foo\bar\baz\..\qux'), isTrue); + expect(context.isWithin(r'C:\', 'C:/foo'), isTrue); + expect(context.isWithin(r'C:\', r'D:\foo'), isFalse); + expect(context.isWithin(r'C:\', r'\\foo\bar'), isFalse); + }); + + test('with root-relative paths', () { + expect(context.isWithin(r'\foo', r'C:\foo\bar'), isTrue); + expect(context.isWithin(r'C:\foo', r'\foo\bar'), isTrue); + expect(context.isWithin(r'\root', r'foo\bar'), isTrue); + expect(context.isWithin(r'foo', r'\root\path\foo\bar'), isTrue); + expect(context.isWithin(r'\foo', r'\foo\bar'), isTrue); + }); + + test('from a relative root', () { + final r = path.Context(style: path.Style.windows, current: r'foo\bar'); + expect(r.isWithin('.', r'a\b\c'), isTrue); + expect(r.isWithin('.', r'..\a\b\c'), isFalse); + expect(r.isWithin('.', r'..\..\a\foo\b\c'), isFalse); + expect(r.isWithin(r'C:\', r'C:\baz\bang'), isTrue); + expect(r.isWithin('.', r'C:\baz\bang'), isFalse); + }); + + test('is case-insensitive', () { + expect(context.isWithin(r'FoO', r'fOo\bar'), isTrue); + expect(context.isWithin(r'C:\', r'c:\foo'), isTrue); + expect(context.isWithin(r'fOo\qux\..\BaR', r'FoO\bAr\baz'), isTrue); + }); + }); + + group('equals and hash', () { + test('simple cases', () { + expectEquals(context, r'foo\bar', r'foo\bar'); + expectNotEquals(context, r'foo\bar', r'foo\bar\baz'); + expectNotEquals(context, r'foo\bar', r'foo'); + expectNotEquals(context, r'foo\bar', r'foo\baz'); + expectEquals(context, r'foo\bar', r'..\path\foo\bar'); + expectEquals(context, r'D:\', r'D:\'); + expectEquals(context, r'C:\', r'..\..'); + expectEquals(context, r'baz', r'C:\root\path\baz'); + }); + + test('complex cases', () { + expectEquals(context, r'foo\.\bar', r'foo\bar'); + expectEquals(context, r'foo\\bar', r'foo\bar'); + expectEquals(context, r'foo\qux\..\bar', r'foo\bar'); + expectNotEquals(context, r'foo\qux\..\bar', r'foo\qux'); + expectNotEquals(context, r'foo\bar', r'foo\bar\baz\..\..'); + expectEquals(context, r'foo\bar', r'foo\bar\\\'); + expectEquals(context, r'foo\.bar', r'foo\.bar'); + expectNotEquals(context, r'foo\.\bar', r'foo\.bar'); + expectEquals(context, r'foo\..bar', r'foo\..bar'); + expectNotEquals(context, r'foo\..\bar', r'foo\..bar'); + expectEquals(context, r'foo\bar', r'foo\bar\baz\..'); + expectEquals(context, r'FoO\bAr', r'foo\bar'); + expectEquals(context, r'foo/\bar', r'foo\/bar'); + expectEquals(context, r'c:\', r'C:\'); + expectEquals(context, r'C:\root', r'..'); + }); + + test('with root-relative paths', () { + expectEquals(context, r'\foo', r'C:\foo'); + expectNotEquals(context, r'\foo', 'http://google.com/foo'); + expectEquals(context, r'C:\root\path\foo\bar', r'foo\bar'); + }); + + test('from a relative root', () { + final r = path.Context(style: path.Style.windows, current: r'foo\bar'); + expectEquals(r, r'a\b', r'a\b'); + expectNotEquals(r, '.', r'foo\bar'); + expectNotEquals(r, '.', r'..\a\b'); + expectEquals(r, '.', r'..\bar'); + expectEquals(r, r'C:\baz\bang', r'C:\baz\bang'); + expectNotEquals(r, r'baz\bang', r'C:\baz\bang'); + }); + }); + + group('absolute', () { + test('allows up to fifteen parts', () { + expect(context.absolute('a'), r'C:\root\path\a'); + expect(context.absolute('a', 'b'), r'C:\root\path\a\b'); + expect(context.absolute('a', 'b', 'c'), r'C:\root\path\a\b\c'); + expect(context.absolute('a', 'b', 'c', 'd'), r'C:\root\path\a\b\c\d'); + expect( + context.absolute('a', 'b', 'c', 'd', 'e'), r'C:\root\path\a\b\c\d\e'); + expect(context.absolute('a', 'b', 'c', 'd', 'e', 'f'), + r'C:\root\path\a\b\c\d\e\f'); + expect(context.absolute('a', 'b', 'c', 'd', 'e', 'f', 'g'), + r'C:\root\path\a\b\c\d\e\f\g'); + expect(context.absolute('a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'), + r'C:\root\path\a\b\c\d\e\f\g\h'); + expect(context.absolute('a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i'), + r'C:\root\path\a\b\c\d\e\f\g\h\i'); + expect(context.absolute('a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'), + r'C:\root\path\a\b\c\d\e\f\g\h\i\j'); + expect( + context.absolute( + 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k'), + r'C:\root\path\a\b\c\d\e\f\g\h\i\j\k'); + expect( + context.absolute( + 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l'), + r'C:\root\path\a\b\c\d\e\f\g\h\i\j\k\l'); + expect( + context.absolute( + 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm'), + r'C:\root\path\a\b\c\d\e\f\g\h\i\j\k\l\m'); + expect( + context.absolute('a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', + 'k', 'l', 'm', 'n'), + r'C:\root\path\a\b\c\d\e\f\g\h\i\j\k\l\m\n'); + expect( + context.absolute('a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', + 'k', 'l', 'm', 'n', 'o'), + r'C:\root\path\a\b\c\d\e\f\g\h\i\j\k\l\m\n\o'); + }); + + test('does not add separator if a part ends in one', () { + expect(context.absolute(r'a\', 'b', r'c\', 'd'), r'C:\root\path\a\b\c\d'); + expect(context.absolute('a/', 'b'), r'C:\root\path\a/b'); + }); + + test('ignores parts before an absolute path', () { + expect(context.absolute('a', '/b', '/c', 'd'), r'C:\c\d'); + expect(context.absolute('a', r'\b', r'\c', 'd'), r'C:\c\d'); + expect(context.absolute('a', r'c:\b', 'c', 'd'), r'c:\b\c\d'); + expect(context.absolute('a', r'\\b\c', r'\\d\e', 'f'), r'\\d\e\f'); + }); + }); + + test('withoutExtension', () { + expect(context.withoutExtension(''), ''); + expect(context.withoutExtension('a'), 'a'); + expect(context.withoutExtension('.a'), '.a'); + expect(context.withoutExtension('a.b'), 'a'); + expect(context.withoutExtension(r'a\b.c'), r'a\b'); + expect(context.withoutExtension(r'a\b.c.d'), r'a\b.c'); + expect(context.withoutExtension(r'a\'), r'a\'); + expect(context.withoutExtension(r'a\b\'), r'a\b\'); + expect(context.withoutExtension(r'a\.'), r'a\.'); + expect(context.withoutExtension(r'a\.b'), r'a\.b'); + expect(context.withoutExtension(r'a.b\c'), r'a.b\c'); + expect(context.withoutExtension(r'a/b.c/d'), r'a/b.c/d'); + expect(context.withoutExtension(r'a\b/c'), r'a\b/c'); + expect(context.withoutExtension(r'a\b/c.d'), r'a\b/c'); + expect(context.withoutExtension(r'a.b/c'), r'a.b/c'); + expect(context.withoutExtension(r'a\b.c\'), r'a\b\'); + }); + + test('setExtension', () { + expect(context.setExtension('', '.x'), '.x'); + expect(context.setExtension('a', '.x'), 'a.x'); + expect(context.setExtension('.a', '.x'), '.a.x'); + expect(context.setExtension('a.b', '.x'), 'a.x'); + expect(context.setExtension(r'a\b.c', '.x'), r'a\b.x'); + expect(context.setExtension(r'a\b.c.d', '.x'), r'a\b.c.x'); + expect(context.setExtension(r'a\', '.x'), r'a\.x'); + expect(context.setExtension(r'a\b\', '.x'), r'a\b\.x'); + expect(context.setExtension(r'a\.', '.x'), r'a\..x'); + expect(context.setExtension(r'a\.b', '.x'), r'a\.b.x'); + expect(context.setExtension(r'a.b\c', '.x'), r'a.b\c.x'); + expect(context.setExtension(r'a/b.c/d', '.x'), r'a/b.c/d.x'); + expect(context.setExtension(r'a\b/c', '.x'), r'a\b/c.x'); + expect(context.setExtension(r'a\b/c.d', '.x'), r'a\b/c.x'); + expect(context.setExtension(r'a.b/c', '.x'), r'a.b/c.x'); + expect(context.setExtension(r'a\b.c\', '.x'), r'a\b\.x'); + }); + + group('fromUri', () { + test('with a URI', () { + expect(context.fromUri(Uri.parse('file:///C:/path/to/foo')), + r'C:\path\to\foo'); + expect(context.fromUri(Uri.parse('file:///C%3A/path/to/foo')), + r'C:\path\to\foo'); + expect(context.fromUri(Uri.parse('file://server/share/path/to/foo')), + r'\\server\share\path\to\foo'); + expect(context.fromUri(Uri.parse('file:///C:/')), r'C:\'); + expect(context.fromUri(Uri.parse('file:///C%3A/')), r'C:\'); + expect( + context.fromUri(Uri.parse('file://server/share')), r'\\server\share'); + expect(context.fromUri(Uri.parse('foo/bar')), r'foo\bar'); + expect(context.fromUri(Uri.parse('/C:/path/to/foo')), r'C:\path\to\foo'); + expect( + context.fromUri(Uri.parse('///C:/path/to/foo')), r'C:\path\to\foo'); + expect(context.fromUri(Uri.parse('//server/share/path/to/foo')), + r'\\server\share\path\to\foo'); + expect(context.fromUri(Uri.parse('file:///C:/path/to/foo%23bar')), + r'C:\path\to\foo#bar'); + expect(context.fromUri(Uri.parse('file:///C%3A/path/to/foo%23bar')), + r'C:\path\to\foo#bar'); + expect( + context.fromUri(Uri.parse('file://server/share/path/to/foo%23bar')), + r'\\server\share\path\to\foo#bar'); + expect(context.fromUri(Uri.parse('_%7B_%7D_%60_%5E_%20_%22_%25_')), + r'_{_}_`_^_ _"_%_'); + expect(context.fromUri(Uri.parse('/foo')), r'\foo'); + expect(() => context.fromUri(Uri.parse('https://dart.dev')), + throwsArgumentError); + }); + + test('with a string', () { + expect(context.fromUri('file:///C:/path/to/foo'), r'C:\path\to\foo'); + expect(context.fromUri('file:///C%3A/path/to/foo'), r'C:\path\to\foo'); + }); + }); + + test('toUri', () { + expect( + context.toUri(r'C:\path\to\foo'), Uri.parse('file:///C:/path/to/foo')); + expect(context.toUri(r'C:\path\to\foo\'), + Uri.parse('file:///C:/path/to/foo/')); + expect(context.toUri(r'path\to\foo\'), Uri.parse('path/to/foo/')); + expect(context.toUri(r'C:\'), Uri.parse('file:///C:/')); + expect(context.toUri(r'\\server\share'), Uri.parse('file://server/share')); + expect( + context.toUri(r'\\server\share\'), Uri.parse('file://server/share/')); + expect(context.toUri(r'foo\bar'), Uri.parse('foo/bar')); + expect(context.toUri(r'C:\path\to\foo#bar'), + Uri.parse('file:///C:/path/to/foo%23bar')); + expect(context.toUri(r'\\server\share\path\to\foo#bar'), + Uri.parse('file://server/share/path/to/foo%23bar')); + expect(context.toUri(r'C:\_{_}_`_^_ _"_%_'), + Uri.parse('file:///C:/_%7B_%7D_%60_%5E_%20_%22_%25_')); + expect(context.toUri(r'_{_}_`_^_ _"_%_'), + Uri.parse('_%7B_%7D_%60_%5E_%20_%22_%25_')); + expect(context.toUri(''), Uri.parse('')); + }); + + group('prettyUri', () { + test('with a file: URI', () { + expect(context.prettyUri('file:///C:/root/path/a/b'), r'a\b'); + expect(context.prettyUri('file:///C:/root/path/a/../b'), r'b'); + expect( + context.prettyUri('file:///C:/other/path/a/b'), r'C:\other\path\a\b'); + expect( + context.prettyUri('file:///D:/root/path/a/b'), r'D:\root\path\a\b'); + expect(context.prettyUri('file:///C:/root/other'), r'..\other'); + }); + + test('with a file: URI with encoded colons', () { + expect(context.prettyUri('file:///C%3A/root/path/a/b'), r'a\b'); + expect(context.prettyUri('file:///C%3A/root/path/a/../b'), r'b'); + expect(context.prettyUri('file:///C%3A/other/path/a/b'), + r'C:\other\path\a\b'); + expect( + context.prettyUri('file:///D%3A/root/path/a/b'), r'D:\root\path\a\b'); + expect(context.prettyUri('file:///C%3A/root/other'), r'..\other'); + }); + + test('with an http: URI', () { + expect(context.prettyUri('https://dart.dev/a/b'), 'https://dart.dev/a/b'); + }); + + test('with a relative URI', () { + expect(context.prettyUri('a/b'), r'a\b'); + }); + + test('with a root-relative URI', () { + expect(context.prettyUri('/D:/a/b'), r'D:\a\b'); + }); + + test('with a Uri object', () { + expect(context.prettyUri(Uri.parse('a/b')), r'a\b'); + }); + }); + + test('driveLetterEnd', () { + expect(driveLetterEnd('', 0), null); + expect(driveLetterEnd('foo.dart', 0), null); + expect(driveLetterEnd('@', 0), null); + + expect(driveLetterEnd('c:', 0), 2); + + // colons + expect(driveLetterEnd('c:/', 0), 3); + expect(driveLetterEnd('c:/a', 0), 3); + + // escaped colons lowercase + expect(driveLetterEnd('c%3a/', 0), 5); + expect(driveLetterEnd('c%3a/a', 0), 5); + + // escaped colons uppercase + expect(driveLetterEnd('c%3A/', 0), 5); + expect(driveLetterEnd('c%3A/a', 0), 5); + + // non-drive letter + expect(driveLetterEnd('ab:/c', 0), null); + expect(driveLetterEnd('ab%3a/c', 0), null); + expect(driveLetterEnd('ab%3A/c', 0), null); + }); +}